diff --git a/.github/workflows/android.yml b/.github/workflows/android.yml index 993d081..dec2add 100644 --- a/.github/workflows/android.yml +++ b/.github/workflows/android.yml @@ -11,6 +11,8 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 + with: + submodules: recursive - uses: actions/setup-java@v4 with: distribution: temurin diff --git a/.github/workflows/rebuild-core.yml b/.github/workflows/rebuild-core.yml index c6c8647..8dc941b 100644 --- a/.github/workflows/rebuild-core.yml +++ b/.github/workflows/rebuild-core.yml @@ -15,6 +15,8 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 + with: + submodules: recursive - name: Install Rust Android toolchain run: | diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 165342e..2fbcb41 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -16,6 +16,8 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 + with: + submodules: recursive - uses: actions/setup-java@v4 with: diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..a4fa6bb --- /dev/null +++ b/.gitmodules @@ -0,0 +1,4 @@ +[submodule "native/DingooEmu"] + path = native/DingooEmu + url = https://github.com/uplush/DingooEmu.git + branch = dingoobox-integration diff --git a/README.md b/README.md index d0300c0..78c3265 100644 --- a/README.md +++ b/README.md @@ -67,27 +67,39 @@ am start -n io.github.uplush.dingoobox/.MainActivity -e ROM "{file.path}" ## 构建 -仓库包含供 `arm64-v8a` 使用的预编译 `libdingooemu.so` 和 -`libc++_shared.so`。但首次构建 1.0.2 必须先重新编译一次 Rust 核心,才能将 -游戏内“退出游戏”自动返回修复写入 `libdingooemu.so`: +DingooEmu 核心通过 Git Submodule 集成在 `native/DingooEmu`,主仓库会锁定到 +经过 DingooBox 验证的核心提交。 -```powershell -rustup target add aarch64-linux-android -cargo install cargo-ndk -$env:ANDROID_NDK_HOME = "$env:LOCALAPPDATA\Android\Sdk\ndk\28.2.13676358" -.\scripts\build-core.ps1 +首次克隆请使用: + +```bash +git clone --recurse-submodules https://github.com/uplush/DingooBox.git +cd DingooBox +``` + +如果已经克隆过仓库,或核心目录为空,请执行: + +```bash +git submodule update --init --recursive ``` -随后构建 APK: +仓库同时包含供 `arm64-v8a` 使用的预编译 `libdingooemu.so` 和 +`libc++_shared.so`,可以直接构建 APK: ```bash ./gradlew assembleDebug ``` +Windows PowerShell: + +```powershell +.\gradlew.bat assembleDebug +``` + 生成文件位于 `app/build/outputs/apk/debug/app-debug.apk`。Debug 与 Release -构建使用相同的正式包名,不再追加 `.debug`。 +构建使用相同的正式包名,不追加 `.debug`。 -存档失败诊断:清空日志、复现一次失败,再导出两个原生标签: +存档失败诊断:清空日志、复现一次失败,再导出原生标签: ```powershell adb logcat -c @@ -97,25 +109,29 @@ adb logcat -d -s DingooState:I DingooCore:E *:S ## 重新构建 DingooEmu 核心 +修改或更新核心源码后,需要重新生成 `libdingooemu.so`。 + Linux/macOS: ```bash +git submodule update --init --recursive rustup target add aarch64-linux-android cargo install cargo-ndk ANDROID_NDK_HOME=/path/to/ndk/28.2.13676358 ./scripts/build-core.sh +./gradlew assembleDebug ``` Windows PowerShell: ```powershell +git submodule update --init --recursive rustup target add aarch64-linux-android cargo install cargo-ndk -$env:ANDROID_NDK_HOME = "C:\path\to\ndk\28.2.13676358" -.\scripts\build-core.ps1 +$env:ANDROID_NDK_HOME = "$env:LOCALAPPDATA\Android\Sdk\ndk\28.2.13676358" +powershell.exe -NoProfile -ExecutionPolicy Bypass -File .\scripts\build-core.ps1 +.\gradlew.bat assembleDebug ``` -然后运行 `./gradlew assembleDebug`(Windows 使用 `.\gradlew.bat assembleDebug`)。 - ## 已知限制 - 在线封面下载仅匹配 DingooEmu 上游验证截图目录中的 `.app` 游戏;目录外游戏仍可从长按菜单选择本地图片。 diff --git a/app/src/main/jniLibs/arm64-v8a/libdingooemu.so b/app/src/main/jniLibs/arm64-v8a/libdingooemu.so index b5d7537..b5e12ef 100755 Binary files a/app/src/main/jniLibs/arm64-v8a/libdingooemu.so and b/app/src/main/jniLibs/arm64-v8a/libdingooemu.so differ diff --git a/native/DingooEmu b/native/DingooEmu new file mode 160000 index 0000000..bca43a3 --- /dev/null +++ b/native/DingooEmu @@ -0,0 +1 @@ +Subproject commit bca43a38e1b771eb8bec466ab799249f4c6eeaf8 diff --git a/native/DingooEmu/.github/dependabot.yml b/native/DingooEmu/.github/dependabot.yml deleted file mode 100644 index d69e053..0000000 --- a/native/DingooEmu/.github/dependabot.yml +++ /dev/null @@ -1,11 +0,0 @@ -version: 2 -updates: - - package-ecosystem: "github-actions" - directory: "/" - schedule: - interval: "daily" - - - package-ecosystem: "cargo" - directory: "/" - schedule: - interval: "daily" diff --git a/native/DingooEmu/.github/workflows/ci.yml b/native/DingooEmu/.github/workflows/ci.yml deleted file mode 100644 index 4388812..0000000 --- a/native/DingooEmu/.github/workflows/ci.yml +++ /dev/null @@ -1,39 +0,0 @@ -name: DingooEmu CI - -on: - push: - branches: [master] - pull_request: - branches: [master] - -jobs: - check: - name: Check & Lint - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v7 - - - name: Install Linux build dependencies - run: | - sudo apt-get update - sudo apt-get install -y libasound2-dev libx11-dev libxkbcommon-dev - - - name: Setup Rust toolchain - uses: dtolnay/rust-toolchain@stable - with: - components: clippy, rustfmt - - - name: Cache Rust artifacts - uses: swatinem/rust-cache@v2 - - - name: Check formatting - run: cargo fmt --all -- --check - - - name: Run clippy - run: cargo clippy --workspace --all-targets --all-features -- -D warnings - - - name: Build - run: cargo build --workspace --release - - - name: Run tests - run: cargo test --workspace diff --git a/native/DingooEmu/.github/workflows/codeql-analysis.yml b/native/DingooEmu/.github/workflows/codeql-analysis.yml deleted file mode 100644 index e320d23..0000000 --- a/native/DingooEmu/.github/workflows/codeql-analysis.yml +++ /dev/null @@ -1,46 +0,0 @@ -name: "CodeQL" - -on: - push: - branches: [master] - pull_request: - branches: [master] - schedule: - - cron: '24 17 * * 6' - -jobs: - analyze: - name: Analyze - runs-on: ubuntu-latest - permissions: - actions: read - contents: read - security-events: write - - strategy: - fail-fast: false - matrix: - language: ['rust'] - - steps: - - name: Checkout repository - uses: actions/checkout@v7 - - - name: Install Linux build dependencies - run: | - sudo apt-get update - sudo apt-get install -y libasound2-dev libx11-dev libxkbcommon-dev - - - name: Setup Rust toolchain - uses: dtolnay/rust-toolchain@stable - - - name: Initialize CodeQL - uses: github/codeql-action/init@v4.37.7 - with: - languages: ${{ matrix.language }} - - - name: Build - run: cargo build --workspace - - - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v4.37.7 diff --git a/native/DingooEmu/.github/workflows/nightly-build.yml b/native/DingooEmu/.github/workflows/nightly-build.yml deleted file mode 100644 index 687a64a..0000000 --- a/native/DingooEmu/.github/workflows/nightly-build.yml +++ /dev/null @@ -1,396 +0,0 @@ -name: Nightly Build - -# Build native binaries every night and publish them as a rolling -# `nightly-build` GitHub prerelease, so users can always grab the -# latest from master without waiting for a tagged version. -# -# Triggers: -# - Cron at 20:00 UTC (04:00 Beijing) every day. -# - Manual workflow_dispatch. -# - Every push to master (so the rolling release always reflects HEAD). - -on: - schedule: - - cron: "0 20 * * *" - workflow_dispatch: - push: - branches: [master] - -permissions: - contents: write - -jobs: - build: - name: Build (${{ matrix.target }}) - strategy: - fail-fast: false - matrix: - include: - - platform: ubuntu-22.04 - target: x86_64-unknown-linux-gnu - artifact: dingoo-emu-linux-x86_64 - - platform: ubuntu-24.04-arm - target: aarch64-unknown-linux-gnu - artifact: dingoo-emu-linux-aarch64 - - platform: macos-latest - target: x86_64-apple-darwin - artifact: dingoo-emu-macos-x86_64 - - platform: macos-latest - target: aarch64-apple-darwin - artifact: dingoo-emu-macos-aarch64 - - platform: windows-latest - target: x86_64-pc-windows-msvc - artifact: dingoo-emu-windows-x86_64 - - runs-on: ${{ matrix.platform }} - - steps: - - name: Compute version metadata - id: ver - shell: bash - run: | - echo "DATE=$(date -u +%Y%m%d)" >> "$GITHUB_OUTPUT" - echo "DATETIME=$(date -u '+%Y-%m-%d %H:%M:%S UTC')" >> "$GITHUB_OUTPUT" - echo "SHORT_SHA=$(echo "${{ github.sha }}" | cut -c1-7)" >> "$GITHUB_OUTPUT" - - - name: Checkout - uses: actions/checkout@v7 - - - name: Setup Rust toolchain - uses: dtolnay/rust-toolchain@stable - with: - targets: ${{ matrix.target }} - - - name: Cache Rust artifacts - uses: swatinem/rust-cache@v2 - with: - key: nightly-${{ matrix.target }} - - - name: Install Linux build dependencies - if: startsWith(matrix.platform, 'ubuntu') - run: | - sudo apt-get update - sudo apt-get install -y libasound2-dev libx11-dev libxkbcommon-dev - timeout-minutes: 10 - - - name: Build standalone - run: cargo build -p dingooemu --release --target ${{ matrix.target }} - - - name: Build libretro core - run: cargo build -p dingooemu-libretro --release --target ${{ matrix.target }} - - - name: Stage libretro core - shell: bash - run: | - out="libretro/${{ matrix.artifact }}" - mkdir -p "$out" - rel="target/${{ matrix.target }}/release" - # Cargo names the cdylib after its lib target (dingooemu); copy it - # to the dingooemu_libretro. name RetroArch expects. - for f in "dingooemu.dll:dingooemu_libretro.dll" \ - "libdingooemu.so:dingooemu_libretro.so" \ - "libdingooemu.dylib:dingooemu_libretro.dylib"; do - src="${f%%:*}"; dst="${f##*:}" - if [ -f "$rel/$src" ]; then - cp "$rel/$src" "$out/$dst" - break - fi - done - cp crates/dingooemu-libretro/dingooemu_libretro.info "$out/dingooemu_libretro.info" - ls -la "$out" - - - name: Package libretro (Unix) - if: runner.os != 'Windows' - shell: bash - run: | - cd libretro - tar czf ../${{ matrix.artifact }}-libretro.tar.gz ${{ matrix.artifact }} - cd .. - - - name: Package libretro (Windows) - if: runner.os == 'Windows' - shell: bash - run: | - cd libretro - 7z a ../${{ matrix.artifact }}-libretro.zip ${{ matrix.artifact }} - cd .. - - - name: Package (Unix) - if: runner.os != 'Windows' - shell: bash - run: | - cd target/${{ matrix.target }}/release - tar czf ../../../${{ matrix.artifact }}.tar.gz dingoo-emu - cd ../../.. - - - name: Package (Windows) - if: runner.os == 'Windows' - shell: bash - run: | - cd target/${{ matrix.target }}/release - 7z a ../../../${{ matrix.artifact }}.zip dingoo-emu.exe - cd ../../.. - - - name: Upload artifact - uses: actions/upload-artifact@v7 - with: - name: ${{ matrix.artifact }} - path: | - ${{ matrix.artifact }}.* - ${{ matrix.artifact }}-libretro.* - retention-days: 7 - - build-android: - name: Build (Android) - runs-on: ubuntu-22.04 - - steps: - - name: Checkout - uses: actions/checkout@v7 - - - name: Setup Rust toolchain - uses: dtolnay/rust-toolchain@stable - with: - targets: aarch64-linux-android,armv7-linux-androideabi,i686-linux-android,x86_64-linux-android - - - name: Setup Android NDK - uses: nttld/setup-ndk@v1 - id: setup-ndk - with: - ndk-version: r27c - - - name: Install cargo-ndk - run: cargo install cargo-ndk --locked - - - name: Cache Rust artifacts - uses: swatinem/rust-cache@v2 - with: - key: nightly-android - - - name: Build libretro core for Android - env: - ANDROID_NDK_HOME: ${{ steps.setup-ndk.outputs.ndk-path }} - ANDROID_NDK_ROOT: ${{ steps.setup-ndk.outputs.ndk-path }} - run: | - cargo ndk \ - -t arm64-v8a -t armeabi-v7a -t x86 -t x86_64 \ - --platform 21 \ - build -p dingooemu-libretro --release - - - name: Stage libretro core (Android) - shell: bash - run: | - out="libretro/dingoo-emu-android" - mkdir -p "$out/arm64-v8a" "$out/armeabi-v7a" "$out/x86" "$out/x86_64" - cp target/aarch64-linux-android/release/libdingooemu.so "$out/arm64-v8a/dingooemu_libretro_android.so" - cp target/armv7-linux-androideabi/release/libdingooemu.so "$out/armeabi-v7a/dingooemu_libretro_android.so" - cp target/i686-linux-android/release/libdingooemu.so "$out/x86/dingooemu_libretro_android.so" - cp target/x86_64-linux-android/release/libdingooemu.so "$out/x86_64/dingooemu_libretro_android.so" - cp crates/dingooemu-libretro/dingooemu_libretro.info "$out/" - cp LICENSE THIRD_PARTY_LICENSES.md "$out/" - ls -laR "$out" - - - name: Package libretro - shell: bash - run: | - cd libretro - tar czf ../dingoo-emu-android-libretro.tar.gz dingoo-emu-android - cd .. - - - name: Upload artifact - uses: actions/upload-artifact@v7 - with: - name: dingoo-emu-android - path: dingoo-emu-android-libretro.tar.gz - retention-days: 7 - - build-ios: - name: Build (iOS) - runs-on: macos-latest - - steps: - - name: Checkout - uses: actions/checkout@v7 - - - name: Setup Rust toolchain - uses: dtolnay/rust-toolchain@stable - with: - targets: aarch64-apple-ios,x86_64-apple-ios,aarch64-apple-ios-sim - - - name: Cache Rust artifacts - uses: swatinem/rust-cache@v2 - with: - key: nightly-ios - - - name: Build libretro core (arm64 device) - run: cargo build -p dingooemu-libretro --release --target aarch64-apple-ios - - - name: Build libretro core (x86_64 device) - run: cargo build -p dingooemu-libretro --release --target x86_64-apple-ios - - - name: Build libretro core (arm64 simulator) - run: cargo build -p dingooemu-libretro --release --target aarch64-apple-ios-sim - - - name: Create universal binary and stage - shell: bash - run: | - out="libretro/dingoo-emu-ios" - mkdir -p "$out/simulator" - # Universal binary for real devices (arm64 + x86_64) - lipo -create \ - target/aarch64-apple-ios/release/libdingooemu.dylib \ - target/x86_64-apple-ios/release/libdingooemu.dylib \ - -output "$out/dingooemu_libretro_ios.dylib" - # Simulator binary (arm64 only) - cp target/aarch64-apple-ios-sim/release/libdingooemu.dylib \ - "$out/simulator/dingooemu_libretro_ios.dylib" - cp crates/dingooemu-libretro/dingooemu_libretro.info "$out/" - ls -laR "$out" - - - name: Package libretro - shell: bash - run: | - cd libretro - tar czf ../dingoo-emu-ios-libretro.tar.gz dingoo-emu-ios - cd .. - - - name: Upload artifact - uses: actions/upload-artifact@v7 - with: - name: dingoo-emu-ios - path: dingoo-emu-ios-libretro.tar.gz - retention-days: 7 - - build-webos: - name: Build (webOS) - runs-on: ubuntu-22.04 - env: - WEBOS_TOOLCHAIN_URL: https://github.com/openlgtv/buildroot-nc4/releases/latest/download/arm-webos-linux-gnueabi_sdk-buildroot-x86_64.tar.gz - RUST_TARGET: armv7-unknown-linux-gnueabi - - steps: - - name: Checkout - uses: actions/checkout@v7 - - - name: Setup Rust toolchain - uses: dtolnay/rust-toolchain@stable - with: - targets: ${{ env.RUST_TARGET }} - - - name: Install webOS toolchain - shell: bash - run: | - curl --fail --location --retry 3 \ - "$WEBOS_TOOLCHAIN_URL" \ - --output "$RUNNER_TEMP/webos-toolchain.tar.gz" - mkdir -p "$RUNNER_TEMP/webos-toolchain" - tar xzf "$RUNNER_TEMP/webos-toolchain.tar.gz" \ - -C "$RUNNER_TEMP/webos-toolchain" --strip-components=1 - "$RUNNER_TEMP/webos-toolchain/relocate-sdk.sh" - echo "$RUNNER_TEMP/webos-toolchain/bin" >> "$GITHUB_PATH" - echo "WEBOS_TOOLCHAIN=$RUNNER_TEMP/webos-toolchain" >> "$GITHUB_ENV" - - - name: Cache Rust artifacts - uses: swatinem/rust-cache@v2 - with: - key: nightly-webos - - - name: Build libretro core - env: - CARGO_TARGET_ARMV7_UNKNOWN_LINUX_GNUEABI_LINKER: arm-webos-linux-gnueabi-gcc - CC_armv7_unknown_linux_gnueabi: arm-webos-linux-gnueabi-gcc - AR_armv7_unknown_linux_gnueabi: arm-webos-linux-gnueabi-ar - run: cargo build -p dingooemu-libretro --release --target "$RUST_TARGET" - - - name: Verify ELF binary - shell: bash - run: | - arm-webos-linux-gnueabi-readelf -h target/$RUST_TARGET/release/libdingooemu.so - arm-webos-linux-gnueabi-readelf -d target/$RUST_TARGET/release/libdingooemu.so - - - name: Stage libretro core (webOS) - shell: bash - run: | - out="libretro/dingoo-emu-webos" - mkdir -p "$out" - cp target/$RUST_TARGET/release/libdingooemu.so "$out/dingooemu_libretro.so" - cp crates/dingooemu-libretro/dingooemu_libretro.info "$out/" - ls -laR "$out" - - - name: Package libretro - shell: bash - run: | - cd libretro - tar czf ../dingoo-emu-webos-libretro.tar.gz dingoo-emu-webos - cd .. - - - name: Upload artifact - uses: actions/upload-artifact@v7 - with: - name: dingoo-emu-webos - path: dingoo-emu-webos-libretro.tar.gz - retention-days: 7 - - release: - name: Publish Nightly Release - needs: [build, build-android, build-ios, build-webos] - runs-on: ubuntu-latest - steps: - - name: Compute version metadata - id: ver - shell: bash - run: | - echo "DATE=$(date -u +%Y%m%d)" >> "$GITHUB_OUTPUT" - echo "DATETIME=$(date -u '+%Y-%m-%d %H:%M:%S UTC')" >> "$GITHUB_OUTPUT" - echo "SHORT_SHA=$(echo "${{ github.sha }}" | cut -c1-7)" >> "$GITHUB_OUTPUT" - - - name: Download all artifacts - uses: actions/download-artifact@v8 - with: - path: artifacts - - - name: Flatten artifacts - shell: bash - run: | - mkdir -p release-assets - find artifacts -type f \( \ - -name '*.tar.gz' -o \ - -name '*.zip' \ - \) -exec cp {} release-assets/ \; - ls -la release-assets - - - name: Recreate nightly-build release - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - REPO: ${{ github.repository }} - DATE: ${{ steps.ver.outputs.DATE }} - DATETIME: ${{ steps.ver.outputs.DATETIME }} - SHORT_SHA: ${{ steps.ver.outputs.SHORT_SHA }} - shell: bash - run: | - # Delete previous rolling release + tag - gh release delete nightly-build --repo "$REPO" --yes --cleanup-tag 2>/dev/null || true - gh api -X DELETE "repos/$REPO/git/refs/tags/nightly-build" 2>/dev/null || true - - notes=$(cat < name RetroArch expects. - for f in "dingooemu.dll:dingooemu_libretro.dll" \ - "libdingooemu.so:dingooemu_libretro.so" \ - "libdingooemu.dylib:dingooemu_libretro.dylib"; do - src="${f%%:*}"; dst="${f##*:}" - if [ -f "$rel/$src" ]; then - cp "$rel/$src" "$out/$dst" - break - fi - done - cp crates/dingooemu-libretro/dingooemu_libretro.info "$out/dingooemu_libretro.info" - ls -la "$out" - - - name: Package (Unix) - if: runner.os != 'Windows' - run: | - cd target/${{ matrix.target }}/release - tar czf ../../../${{ matrix.artifact }}.tar.gz dingoo-emu - cd ../../.. - - - name: Package (Windows) - if: runner.os == 'Windows' - run: | - cd target/${{ matrix.target }}/release - 7z a ../../../${{ matrix.artifact }}.zip dingoo-emu.exe - cd ../../.. - - - name: Package libretro (Unix) - if: runner.os != 'Windows' - shell: bash - run: | - cd libretro - tar czf ../${{ matrix.artifact }}-libretro.tar.gz ${{ matrix.artifact }} - cd .. - - - name: Package libretro (Windows) - if: runner.os == 'Windows' - shell: bash - run: | - cd libretro - 7z a ../${{ matrix.artifact }}-libretro.zip ${{ matrix.artifact }} - cd .. - - - name: Upload artifact - uses: actions/upload-artifact@v7 - with: - name: ${{ matrix.artifact }} - path: | - ${{ matrix.artifact }}.* - ${{ matrix.artifact }}-libretro.* - - build-android: - name: Build (Android) - runs-on: ubuntu-22.04 - - steps: - - name: Checkout - uses: actions/checkout@v7 - - - name: Setup Rust toolchain - uses: dtolnay/rust-toolchain@stable - with: - targets: aarch64-linux-android,armv7-linux-androideabi,i686-linux-android,x86_64-linux-android - - - name: Setup Android NDK - uses: nttld/setup-ndk@v1 - with: - ndk-version: r27c - - - name: Install cargo-ndk - run: cargo install cargo-ndk --locked - - - name: Cache Rust artifacts - uses: swatinem/rust-cache@v2 - with: - key: release-android - - - name: Build libretro core for Android - run: | - cargo ndk \ - -t arm64-v8a -t armeabi-v7a -t x86 -t x86_64 \ - --platform 21 \ - build -p dingooemu-libretro --release - - - name: Stage libretro core - shell: bash - run: | - out="libretro/dingoo-emu-android" - mkdir -p "$out/arm64-v8a" "$out/armeabi-v7a" "$out/x86" "$out/x86_64" - cp target/aarch64-linux-android/release/libdingooemu.so "$out/arm64-v8a/dingooemu_libretro_android.so" - cp target/armv7-linux-androideabi/release/libdingooemu.so "$out/armeabi-v7a/dingooemu_libretro_android.so" - cp target/i686-linux-android/release/libdingooemu.so "$out/x86/dingooemu_libretro_android.so" - cp target/x86_64-linux-android/release/libdingooemu.so "$out/x86_64/dingooemu_libretro_android.so" - cp crates/dingooemu-libretro/dingooemu_libretro.info "$out/" - cp LICENSE THIRD_PARTY_LICENSES.md "$out/" - ls -laR "$out" - - - name: Package libretro - shell: bash - run: | - cd libretro - tar czf ../dingoo-emu-android-libretro.tar.gz dingoo-emu-android - cd .. - - - name: Upload artifact - uses: actions/upload-artifact@v7 - with: - name: dingoo-emu-android - path: dingoo-emu-android-libretro.tar.gz - - build-ios: - name: Build (iOS) - runs-on: macos-latest - - steps: - - name: Checkout - uses: actions/checkout@v7 - - - name: Setup Rust toolchain - uses: dtolnay/rust-toolchain@stable - with: - targets: aarch64-apple-ios,x86_64-apple-ios,aarch64-apple-ios-sim - - - name: Cache Rust artifacts - uses: swatinem/rust-cache@v2 - with: - key: release-ios - - - name: Build libretro core (arm64 device) - run: cargo build -p dingooemu-libretro --release --target aarch64-apple-ios - - - name: Build libretro core (x86_64 device) - run: cargo build -p dingooemu-libretro --release --target x86_64-apple-ios - - - name: Build libretro core (arm64 simulator) - run: cargo build -p dingooemu-libretro --release --target aarch64-apple-ios-sim - - - name: Create universal binary and stage - shell: bash - run: | - out="libretro/dingoo-emu-ios" - mkdir -p "$out/simulator" - # Universal binary for real devices (arm64 + x86_64) - lipo -create \ - target/aarch64-apple-ios/release/libdingooemu.dylib \ - target/x86_64-apple-ios/release/libdingooemu.dylib \ - -output "$out/dingooemu_libretro_ios.dylib" - # Simulator binary (arm64 only) - cp target/aarch64-apple-ios-sim/release/libdingooemu.dylib \ - "$out/simulator/dingooemu_libretro_ios.dylib" - cp crates/dingooemu-libretro/dingooemu_libretro.info "$out/" - ls -laR "$out" - - - name: Package libretro - shell: bash - run: | - cd libretro - tar czf ../dingoo-emu-ios-libretro.tar.gz dingoo-emu-ios - cd .. - - - name: Upload artifact - uses: actions/upload-artifact@v7 - with: - name: dingoo-emu-ios - path: dingoo-emu-ios-libretro.tar.gz - - build-webos: - name: Build (webOS) - runs-on: ubuntu-22.04 - env: - WEBOS_TOOLCHAIN_URL: https://github.com/openlgtv/buildroot-nc4/releases/latest/download/arm-webos-linux-gnueabi_sdk-buildroot-x86_64.tar.gz - RUST_TARGET: armv7-unknown-linux-gnueabi - - steps: - - name: Checkout - uses: actions/checkout@v7 - - - name: Setup Rust toolchain - uses: dtolnay/rust-toolchain@stable - with: - targets: ${{ env.RUST_TARGET }} - - - name: Install webOS toolchain - shell: bash - run: | - curl --fail --location --retry 3 \ - "$WEBOS_TOOLCHAIN_URL" \ - --output "$RUNNER_TEMP/webos-toolchain.tar.gz" - mkdir -p "$RUNNER_TEMP/webos-toolchain" - tar xzf "$RUNNER_TEMP/webos-toolchain.tar.gz" \ - -C "$RUNNER_TEMP/webos-toolchain" --strip-components=1 - "$RUNNER_TEMP/webos-toolchain/relocate-sdk.sh" - echo "$RUNNER_TEMP/webos-toolchain/bin" >> "$GITHUB_PATH" - echo "WEBOS_TOOLCHAIN=$RUNNER_TEMP/webos-toolchain" >> "$GITHUB_ENV" - - - name: Cache Rust artifacts - uses: swatinem/rust-cache@v2 - with: - key: release-webos - - - name: Build libretro core - env: - CARGO_TARGET_ARMV7_UNKNOWN_LINUX_GNUEABI_LINKER: arm-webos-linux-gnueabi-gcc - CC_armv7_unknown_linux_gnueabi: arm-webos-linux-gnueabi-gcc - AR_armv7_unknown_linux_gnueabi: arm-webos-linux-gnueabi-ar - run: cargo build -p dingooemu-libretro --release --target "$RUST_TARGET" - - - name: Verify ELF binary - shell: bash - run: | - arm-webos-linux-gnueabi-readelf -h target/$RUST_TARGET/release/libdingooemu.so - arm-webos-linux-gnueabi-readelf -d target/$RUST_TARGET/release/libdingooemu.so - - - name: Stage libretro core - shell: bash - run: | - out="libretro/dingoo-emu-webos" - mkdir -p "$out" - cp target/$RUST_TARGET/release/libdingooemu.so "$out/dingooemu_libretro.so" - cp crates/dingooemu-libretro/dingooemu_libretro.info "$out/" - ls -laR "$out" - - - name: Package libretro - shell: bash - run: | - cd libretro - tar czf ../dingoo-emu-webos-libretro.tar.gz dingoo-emu-webos - cd .. - - - name: Upload artifact - uses: actions/upload-artifact@v7 - with: - name: dingoo-emu-webos - path: dingoo-emu-webos-libretro.tar.gz - - release: - name: Create Release - needs: [build, build-android, build-ios, build-webos] - runs-on: ubuntu-latest - if: startsWith(github.ref, 'refs/tags/') - steps: - - name: Download all artifacts - uses: actions/download-artifact@v8 - with: - path: artifacts - - - name: Create GitHub Release - uses: softprops/action-gh-release@v3 - with: - draft: true - prerelease: false - generate_release_notes: true - name: "DingooEmu ${{ github.ref_name }}" - body: | - DingooEmu ${{ github.ref_name }} - - ## Downloads - - ### Standalone emulator - - **Linux x86_64**: `dingoo-emu-linux-x86_64.tar.gz` - - **Linux aarch64**: `dingoo-emu-linux-aarch64.tar.gz` - - **macOS x86_64**: `dingoo-emu-macos-x86_64.tar.gz` - - **macOS aarch64**: `dingoo-emu-macos-aarch64.tar.gz` - - **Windows x86_64**: `dingoo-emu-windows-x86_64.zip` - - ### libretro core (for RetroArch) - - Files ending in `-libretro.*` contain `dingooemu_libretro.`, - ready to drop into RetroArch. - - ### Android libretro core (for RetroArch on Android) - - `dingoo-emu-android-libretro.tar.gz` contains - `dingooemu_libretro_android.so` for the `arm64-v8a`, - `armeabi-v7a`, `x86` and `x86_64` ABIs. - - ### iOS libretro core (for RetroArch on iOS) - - `dingoo-emu-ios-libretro.tar.gz` contains: - - `dingooemu_libretro_ios.dylib` for real devices (arm64) - - `simulator/dingooemu_libretro_ios.dylib` for iOS Simulator (Apple Silicon) - - ### webOS libretro core (for RetroArch on LG webOS TVs) - - `dingoo-emu-webos-libretro.tar.gz` contains - `dingooemu_libretro.so` for the webOS ARMv7 ABI. - files: artifacts/**/* - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/native/DingooEmu/.github/workflows/sonar-report.yml b/native/DingooEmu/.github/workflows/sonar-report.yml deleted file mode 100644 index 1c268d1..0000000 --- a/native/DingooEmu/.github/workflows/sonar-report.yml +++ /dev/null @@ -1,43 +0,0 @@ -name: SonarQube Analysis - -on: - push: - branches: [master] - pull_request: - branches: [master] - -jobs: - sonarqube: - name: SonarQube Analysis - runs-on: ubuntu-latest - # Skip entirely for fork PRs — secrets are not available - if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository - steps: - - uses: actions/checkout@v7 - with: - fetch-depth: 0 - - - name: Install Linux build dependencies - run: | - sudo apt-get update - sudo apt-get install -y libasound2-dev libx11-dev libxkbcommon-dev - - - name: Setup Rust toolchain - uses: dtolnay/rust-toolchain@stable - - - name: Cache Rust artifacts - uses: swatinem/rust-cache@v2 - - - name: Build - run: cargo build --workspace - - - name: Run tests with coverage - run: | - cargo install cargo-tarpaulin - cargo tarpaulin --out xml --output-dir coverage - - - name: SonarQube Scan - uses: SonarSource/sonarqube-scan-action@v8 - env: - SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} - SONAR_HOST_URL: https://sonarcloud.io diff --git a/native/DingooEmu/.gitignore b/native/DingooEmu/.gitignore deleted file mode 100644 index 8a01fdc..0000000 --- a/native/DingooEmu/.gitignore +++ /dev/null @@ -1,7 +0,0 @@ -tmp/ -target/ -.codegraph/ -.claude/ -.vscode/ -CLAUDE.md -AGENTS.md diff --git a/native/DingooEmu/.gitlab-ci.yml b/native/DingooEmu/.gitlab-ci.yml deleted file mode 100644 index 90c7342..0000000 --- a/native/DingooEmu/.gitlab-ci.yml +++ /dev/null @@ -1,125 +0,0 @@ -# DESCRIPTION: GitLab CI/CD for libRetro (NOT FOR GitLab-proper) -# -# This recipe is consumed by the Libretro buildbot so the DingooEmu core can -# be distributed through RetroArch's Online Updater > Core Downloader. -# -# DingooEmu is written in Rust, so it uses the official Rust CI templates from -# libretro-infrastructure/ci-templates. Those templates run -# `cargo build --release --target ` and rename the produced cdylib to -# `${CORENAME}_libretro.`. The cdylib lib target is named `dingooemu` -# (see crates/dingooemu-libretro/Cargo.toml) and the workspace `default-members` -# is set to the libretro core, so the bare cargo build only builds the core. - -# Core definitions -.core-defs: - variables: - CORENAME: dingooemu - -include: - # Windows 64-bit - - project: 'libretro-infrastructure/ci-templates' - file: '/rust-windows-x64.yml' - - # Linux 64-bit - - project: 'libretro-infrastructure/ci-templates' - file: '/rust-linux-x64.yml' - - # macOS/iOS/tvOS - - project: 'libretro-infrastructure/ci-templates' - file: '/rust-apple.yml' - - # Android - - project: 'libretro-infrastructure/ci-templates' - file: '/rust-android-jni.yml' - - # webOS - - project: 'libretro-infrastructure/ci-templates' - file: '/rust-webos.yml' - -stages: - - build-prepare - - build-shared - - build-static - -# Windows 64-bit -libretro-build-windows-x64: - extends: - - .libretro-rust-windows-x64-default - - .core-defs - -# Linux 64-bit -libretro-build-linux-x64: - extends: - - .libretro-rust-linux-x64-default - - .core-defs - -# Linux aarch64 -# Cross-compiled from the same image as the x64 job, the way that image already -# cross-compiles the Apple and Android arm64 targets. There is no -# rust-linux-aarch64.yml template yet (libretro/libretro-super#2030) and the -# rust image is amd64-only, so the target and its linker are added here. -libretro-build-linux-aarch64: - extends: - - .libretro-rust-linux-x64 - - .core-defs - variables: - TARGET_ARCH: aarch64-unknown-linux-gnu - STRIP: aarch64-linux-gnu-strip - CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER: aarch64-linux-gnu-gcc - before_script: - - apt-get update -qy && apt-get install -yq gcc-aarch64-linux-gnu - - rustup target add aarch64-unknown-linux-gnu - -# macOS 64-bit -libretro-build-osx-x64: - extends: - - .libretro-rust-osx-x86_64-default - - .core-defs - -# macOS ARM 64-bit -libretro-build-osx-arm64: - extends: - - .libretro-rust-osx-arm64-default - - .core-defs - -# Android ARMv7a -android-armeabi-v7a: - extends: - - .libretro-rust-android-jni-armeabi-v7a - - .core-defs - -# Android ARMv8a -android-arm64-v8a: - extends: - - .libretro-rust-android-jni-arm64-v8a - - .core-defs - -# Android 64-bit x86 -android-x86_64: - extends: - - .libretro-rust-android-jni-x86_64 - - .core-defs - -# Android 32-bit x86 -android-x86: - extends: - - .libretro-rust-android-jni-x86 - - .core-defs - -# iOS -libretro-build-ios-arm64: - extends: - - .libretro-rust-ios-arm64-default - - .core-defs - -# tvOS -libretro-build-tvos-arm64: - extends: - - .libretro-rust-tvos-arm64-default - - .core-defs - -# webOS -libretro-build-webos-armv7a: - extends: - - .libretro-rust-webos-armv7a-default - - .core-defs diff --git a/native/DingooEmu/Cargo.lock b/native/DingooEmu/Cargo.lock deleted file mode 100644 index 3c325d0..0000000 --- a/native/DingooEmu/Cargo.lock +++ /dev/null @@ -1,3101 +0,0 @@ -# This file is automatically @generated by Cargo. -# It is not intended for manual editing. -version = 4 - -[[package]] -name = "adler2" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" - -[[package]] -name = "aho-corasick" -version = "1.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" -dependencies = [ - "memchr", -] - -[[package]] -name = "aligned" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee4508988c62edf04abd8d92897fca0c2995d907ce1dfeaf369dac3716a40685" -dependencies = [ - "as-slice", -] - -[[package]] -name = "aligned-vec" -version = "0.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc890384c8602f339876ded803c97ad529f3842aba97f6392b3dba0dd171769b" -dependencies = [ - "equator", -] - -[[package]] -name = "allocator-api2" -version = "0.2.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" - -[[package]] -name = "alsa" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "812947049edcd670a82cd5c73c3661d2e58468577ba8489de58e1a73c04cbd5d" -dependencies = [ - "alsa-sys", - "bitflags 2.13.1", - "cfg-if", - "libc", -] - -[[package]] -name = "alsa-sys" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad7569085a265dd3f607ebecce7458eaab2132a84393534c95b18dcbc3f31e04" -dependencies = [ - "libc", - "pkg-config", -] - -[[package]] -name = "anstream" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" -dependencies = [ - "anstyle", - "anstyle-parse", - "anstyle-query", - "anstyle-wincon", - "colorchoice", - "is_terminal_polyfill", - "utf8parse", -] - -[[package]] -name = "anstyle" -version = "1.0.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" - -[[package]] -name = "anstyle-parse" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" -dependencies = [ - "utf8parse", -] - -[[package]] -name = "anstyle-query" -version = "1.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" -dependencies = [ - "windows-sys 0.61.2", -] - -[[package]] -name = "anstyle-wincon" -version = "3.0.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" -dependencies = [ - "anstyle", - "once_cell_polyfill", - "windows-sys 0.61.2", -] - -[[package]] -name = "anyhow" -version = "1.0.104" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" - -[[package]] -name = "arbitrary" -version = "1.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" - -[[package]] -name = "arg_enum_proc_macro" -version = "0.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ae92a5119aa49cdbcf6b9f893fe4e1d98b04ccbf82ee0584ad948a44a734dea" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.119", -] - -[[package]] -name = "arrayvec" -version = "0.7.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3fb67a6e08acf24fdeccbac2cb6ac4305825bd1f117462e0e6f2f193345ad56" - -[[package]] -name = "as-slice" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "516b6b4f0e40d50dcda9365d53964ec74560ad4284da2e7fc97122cd83174516" -dependencies = [ - "stable_deref_trait", -] - -[[package]] -name = "autocfg" -version = "1.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" - -[[package]] -name = "av-scenechange" -version = "0.14.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f321d77c20e19b92c39e7471cf986812cbb46659d2af674adc4331ef3f18394" -dependencies = [ - "aligned", - "anyhow", - "arg_enum_proc_macro", - "arrayvec", - "log", - "num-rational", - "num-traits", - "pastey", - "rayon", - "thiserror 2.0.20", - "v_frame", - "y4m", -] - -[[package]] -name = "av1-grain" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8cfddb07216410377231960af4fcab838eaa12e013417781b78bd95ee22077f8" -dependencies = [ - "anyhow", - "arrayvec", - "log", - "nom", - "num-rational", - "v_frame", -] - -[[package]] -name = "avif-serialize" -version = "0.8.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7178fe5f7d460b13895ebb9dcb28a3a6216d2df2574a0806cb51b555d297f38" -dependencies = [ - "arrayvec", -] - -[[package]] -name = "bincode" -version = "1.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" -dependencies = [ - "serde", -] - -[[package]] -name = "bit_field" -version = "0.10.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e4b40c7323adcfc0a41c4b88143ed58346ff65a288fc144329c5c45e05d70c6" - -[[package]] -name = "bitflags" -version = "1.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" - -[[package]] -name = "bitflags" -version = "2.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da" - -[[package]] -name = "bitstream-io" -version = "4.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7eff00be299a18769011411c9def0d827e8f2d7bf0c3dbf53633147a8867fd1f" -dependencies = [ - "no_std_io2", -] - -[[package]] -name = "block2" -version = "0.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5" -dependencies = [ - "objc2", -] - -[[package]] -name = "built" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c0e531d93d39c34eef561e929e8a7f86d77a5af08aac4f6d6e39976c51858e9" - -[[package]] -name = "bumpalo" -version = "3.20.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" -dependencies = [ - "allocator-api2", -] - -[[package]] -name = "bytemuck" -version = "1.25.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6aedf8ae72766347502cf3cb4f41cf5e9cc37d28bee90f1fdaaae15f9cf9424" - -[[package]] -name = "byteorder" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" - -[[package]] -name = "byteorder-lite" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" - -[[package]] -name = "bytes" -version = "1.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" - -[[package]] -name = "cargo-husky" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b02b629252fe8ef6460461409564e2c21d0c8e77e0944f3d189ff06c4e932ad" - -[[package]] -name = "cc" -version = "1.2.67" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e17dd265a7d0f31ef544e1b20e03add05d3b45b491b633b10d67145d2acc1a38" -dependencies = [ - "find-msvc-tools", - "jobserver", - "libc", - "shlex", -] - -[[package]] -name = "cesu8" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" - -[[package]] -name = "cfg-if" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" - -[[package]] -name = "chacha20" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d524456ba66e72eb8b115ff89e01e497f8e6d11d78b70b1aa13c0fbd97540a81" -dependencies = [ - "cfg-if", - "cpufeatures", - "rand_core 0.10.1", -] - -[[package]] -name = "clap" -version = "4.6.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "473c7e07f409a8d772161724aa8db6a765a2532a70f9667eeb7b49d3d02fbdca" -dependencies = [ - "clap_builder", - "clap_derive", -] - -[[package]] -name = "clap_builder" -version = "4.6.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b48fea5a88e9ae728a2dcbedbfc0e730f7d60da42e1cb049a83c9fb8b789889" -dependencies = [ - "anstream", - "anstyle", - "clap_lex", - "strsim", -] - -[[package]] -name = "clap_derive" -version = "4.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d012d2b9d65aca7f18f4d9878a045bc17899bba951561ba5ec3c2ba1eed9a061" -dependencies = [ - "heck", - "proc-macro2", - "quote", - "syn 3.0.3", -] - -[[package]] -name = "clap_lex" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" - -[[package]] -name = "color_quant" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" - -[[package]] -name = "colorchoice" -version = "1.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" - -[[package]] -name = "combine" -version = "4.6.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" -dependencies = [ - "bytes", - "memchr", -] - -[[package]] -name = "console_error_panic_hook" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a06aeb73f470f66dcdbf7223caeebb85984942f22f1adb2a088cf9668146bbbc" -dependencies = [ - "cfg-if", - "wasm-bindgen", -] - -[[package]] -name = "coreaudio-rs" -version = "0.14.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d5d7dca3ebcf65a035582c9ad4385371a9d9ee6537474d2a278f4e1e475bb58" -dependencies = [ - "bitflags 2.13.1", - "libc", - "objc2-audio-toolbox", - "objc2-core-audio", - "objc2-core-audio-types", - "objc2-core-foundation", -] - -[[package]] -name = "cpal" -version = "0.17.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8942da362c0f0d895d7cac616263f2f9424edc5687364dfd1d25ef7eba506d7" -dependencies = [ - "alsa", - "coreaudio-rs", - "dasp_sample", - "jni", - "js-sys", - "libc", - "mach2 0.5.0", - "ndk", - "ndk-context", - "num-derive", - "num-traits", - "objc2", - "objc2-audio-toolbox", - "objc2-avf-audio", - "objc2-core-audio", - "objc2-core-audio-types", - "objc2-core-foundation", - "objc2-foundation", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", - "windows", -] - -[[package]] -name = "cpufeatures" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" -dependencies = [ - "libc", -] - -[[package]] -name = "cranelift" -version = "0.131.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bde609ad0d2ac3255f3363fad230a7f4c6dfd2ff4bf26381c53e459cc7a4fe89" -dependencies = [ - "cranelift-codegen", - "cranelift-frontend", - "cranelift-module", -] - -[[package]] -name = "cranelift-assembler-x64" -version = "0.131.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3867f7a56768640a79fc660d2f60298251dc6d65b5d1c907706cd1afff024957" -dependencies = [ - "cranelift-assembler-x64-meta", -] - -[[package]] -name = "cranelift-assembler-x64-meta" -version = "0.131.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0661d63dcf8fc4a6538c1ee4d523917c5b27e9fce7a4114cdf9e2b30b4043cf" -dependencies = [ - "cranelift-srcgen", -] - -[[package]] -name = "cranelift-bforest" -version = "0.131.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8d535b489159ea63e3c40dfbe8d0e12bfb71f2a14845ef2407353e06c5a697c" -dependencies = [ - "cranelift-entity", - "wasmtime-internal-core", -] - -[[package]] -name = "cranelift-bitset" -version = "0.131.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3af4f7d421b2354deb01d714266022f38fcdbebc9f5f1ec6d310d3c27286d9e" -dependencies = [ - "wasmtime-internal-core", -] - -[[package]] -name = "cranelift-codegen" -version = "0.131.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09fe4c289e67e0221d1705734a57f95e25c289ed0ead7728743ea21285fc4cf1" -dependencies = [ - "bumpalo", - "cranelift-assembler-x64", - "cranelift-bforest", - "cranelift-bitset", - "cranelift-codegen-meta", - "cranelift-codegen-shared", - "cranelift-control", - "cranelift-entity", - "cranelift-isle", - "gimli", - "hashbrown 0.16.1", - "libm", - "log", - "regalloc2", - "rustc-hash", - "serde", - "smallvec", - "target-lexicon", - "wasmtime-internal-core", -] - -[[package]] -name = "cranelift-codegen-meta" -version = "0.131.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3063e5363dc5ee6ee8edd930314582c08eb91c209b9564da1cd667f6424b9b3" -dependencies = [ - "cranelift-assembler-x64-meta", - "cranelift-codegen-shared", - "cranelift-srcgen", - "heck", -] - -[[package]] -name = "cranelift-codegen-shared" -version = "0.131.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c34b9c8dbc9edf37744918e56898d4979ef1e764e8e4bbe8b4d50250838ddfe8" - -[[package]] -name = "cranelift-control" -version = "0.131.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4eed9dc54204dc99aad19669bca50142659ed583396d9a99a2aef34d7c136ef4" -dependencies = [ - "arbitrary", -] - -[[package]] -name = "cranelift-entity" -version = "0.131.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9aa2846b239a046217ecf95cfed0e31be4e86843785d07438ad33f456871e888" -dependencies = [ - "cranelift-bitset", - "wasmtime-internal-core", -] - -[[package]] -name = "cranelift-frontend" -version = "0.131.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "144f70fa9cd07efb83497c12dc8fb73f360a690cd990c44e8ceebc293d8c13b5" -dependencies = [ - "cranelift-codegen", - "log", - "smallvec", - "target-lexicon", -] - -[[package]] -name = "cranelift-isle" -version = "0.131.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0733ca5b2aaa5f6d5d6a1439e3c44280d34730d4d5c262ca08c6775c8d83f191" - -[[package]] -name = "cranelift-jit" -version = "0.131.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96aa44fbb64b5b652fd77aa0a5bb3a1609ca4eae7204fd547991245cc19a18d0" -dependencies = [ - "anyhow", - "cranelift-codegen", - "cranelift-control", - "cranelift-entity", - "cranelift-module", - "cranelift-native", - "libc", - "log", - "region", - "target-lexicon", - "wasmtime-internal-jit-icache-coherence", - "windows-sys 0.61.2", -] - -[[package]] -name = "cranelift-module" -version = "0.131.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dab212cf4630da14726d76333152920694c52f544ff5cc520329b4fa256fe78c" -dependencies = [ - "anyhow", - "cranelift-codegen", - "cranelift-control", -] - -[[package]] -name = "cranelift-native" -version = "0.131.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75b1290d6193b171172d5fe9a6e42326edf487a79f211fbf1e76f912a4aed035" -dependencies = [ - "cranelift-codegen", - "libc", - "target-lexicon", -] - -[[package]] -name = "cranelift-srcgen" -version = "0.131.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce0d5c2b4d719566816a0f1c9a9712d35d61e27df0ffd6c72a9afec9048db6c0" - -[[package]] -name = "crc32fast" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "crossbeam-deque" -version = "0.8.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5181e0de7b61eb03a81e347d6dd8797bae9da5146707b51077e2d71a54ec0ceb" -dependencies = [ - "crossbeam-epoch", - "crossbeam-utils", -] - -[[package]] -name = "crossbeam-epoch" -version = "0.9.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d6914041f254d6e9176c01941b21115dcfb7089e55135a35411081bd106ef3f" -dependencies = [ - "crossbeam-utils", -] - -[[package]] -name = "crossbeam-utils" -version = "0.8.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61803da095bee82a81bb1a452ecc25d3b2f1416d1897eb86430c6159ef717c17" - -[[package]] -name = "crunchy" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" - -[[package]] -name = "dasp_sample" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c87e182de0887fd5361989c677c4e8f5000cd9491d6d563161a8f3a5519fc7f" - -[[package]] -name = "defmt" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2953bfe4f93bbd20cc71198842756f77d161884c99ebbabc41d80231ded88d1" -dependencies = [ - "bitflags 1.3.2", - "defmt-macros", -] - -[[package]] -name = "defmt-macros" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bad9c72e7ca2137e0dc3813245a0d282fd6daad32fd800af018306a9169b5fe8" -dependencies = [ - "defmt-parser", - "proc-macro2", - "quote", - "syn 2.0.119", -] - -[[package]] -name = "defmt-parser" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10d60334b3b2e7c9d91ef8150abfb6fa4c1c39ebbcf4a81c2e346aad939fee3e" -dependencies = [ - "thiserror 2.0.20", -] - -[[package]] -name = "dingooemu" -version = "0.2.0" -dependencies = [ - "anyhow", - "clap", - "dingooemu-core", - "env_logger", - "log", - "minifb", - "serde_json", -] - -[[package]] -name = "dingooemu-core" -version = "0.2.0" -dependencies = [ - "anyhow", - "bincode", - "byteorder", - "cargo-husky", - "cranelift", - "cranelift-jit", - "cranelift-module", - "cranelift-native", - "crc32fast", - "env_logger", - "image", - "log", - "lz4_flex", - "rodio", - "serde", - "serde_json", - "thiserror 2.0.20", -] - -[[package]] -name = "dingooemu-libretro" -version = "0.2.0" -dependencies = [ - "dingooemu-core", - "log", -] - -[[package]] -name = "dispatch2" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" -dependencies = [ - "bitflags 2.13.1", - "objc2", -] - -[[package]] -name = "dlib" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab8ecd87370524b461f8557c119c405552c396ed91fc0a8eec68679eab26f94a" -dependencies = [ - "libloading", -] - -[[package]] -name = "downcast-rs" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2" - -[[package]] -name = "either" -version = "1.16.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e" - -[[package]] -name = "encoding_rs" -version = "0.8.35" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "env_filter" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "900d271a03799a1ee8d1ca9b19893b48ca674a9284fefcfb85f05e74ed314217" -dependencies = [ - "log", - "regex", -] - -[[package]] -name = "env_logger" -version = "0.11.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de671bd27a75a797dc9ae289ba1e77276e75e2026408aab65185384e2d5cd3f6" -dependencies = [ - "anstream", - "anstyle", - "env_filter", - "jiff", - "log", -] - -[[package]] -name = "equator" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4711b213838dfee0117e3be6ac926007d7f433d7bbe33595975d4190cb07e6fc" -dependencies = [ - "equator-macro", -] - -[[package]] -name = "equator-macro" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44f23cf4b44bfce11a86ace86f8a73ffdec849c9fd00a386a53d278bd9e81fb3" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.119", -] - -[[package]] -name = "equivalent" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" - -[[package]] -name = "errno" -version = "0.3.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" -dependencies = [ - "libc", - "windows-sys 0.61.2", -] - -[[package]] -name = "exr" -version = "1.74.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4300e043a56aa2cb633c01af81ca8f699a321879a7854d3896a0ba89056363be" -dependencies = [ - "bit_field", - "half", - "lebe", - "miniz_oxide", - "rayon-core", - "smallvec", - "zune-inflate", -] - -[[package]] -name = "extended" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af9673d8203fcb076b19dfd17e38b3d4ae9f44959416ea532ce72415a6020365" - -[[package]] -name = "fastrand" -version = "2.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" - -[[package]] -name = "fax" -version = "0.2.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "caf1079563223d5d59d83c85886a56e586cfd5c1a26292e971a0fa266531ac5a" - -[[package]] -name = "fdeflate" -version = "0.3.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c" -dependencies = [ - "simd-adler32", -] - -[[package]] -name = "find-msvc-tools" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" - -[[package]] -name = "flate2" -version = "1.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" -dependencies = [ - "crc32fast", - "miniz_oxide", -] - -[[package]] -name = "fnv" -version = "1.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" - -[[package]] -name = "foldhash" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" - -[[package]] -name = "futures" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d" -dependencies = [ - "futures-channel", - "futures-core", - "futures-executor", - "futures-io", - "futures-sink", - "futures-task", - "futures-util", -] - -[[package]] -name = "futures-channel" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" -dependencies = [ - "futures-core", - "futures-sink", -] - -[[package]] -name = "futures-core" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" - -[[package]] -name = "futures-executor" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" -dependencies = [ - "futures-core", - "futures-task", - "futures-util", -] - -[[package]] -name = "futures-io" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" - -[[package]] -name = "futures-macro" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.119", -] - -[[package]] -name = "futures-sink" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" - -[[package]] -name = "futures-task" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" - -[[package]] -name = "futures-util" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" -dependencies = [ - "futures-channel", - "futures-core", - "futures-io", - "futures-macro", - "futures-sink", - "futures-task", - "memchr", - "pin-project-lite", - "slab", -] - -[[package]] -name = "getrandom" -version = "0.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" -dependencies = [ - "cfg-if", - "libc", - "r-efi 5.3.0", - "wasip2", -] - -[[package]] -name = "getrandom" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" -dependencies = [ - "cfg-if", - "libc", - "r-efi 6.0.0", - "rand_core 0.10.1", -] - -[[package]] -name = "gif" -version = "0.14.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee8cfcc411d9adbbaba82fb72661cc1bcca13e8bba98b364e62b2dba8f960159" -dependencies = [ - "color_quant", - "weezl", -] - -[[package]] -name = "gimli" -version = "0.33.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bf7f043f89559805f8c7cacc432749b2fa0d0a0a9ee46ce47164ed5ba7f126c" -dependencies = [ - "fnv", - "hashbrown 0.16.1", - "indexmap", - "stable_deref_trait", -] - -[[package]] -name = "half" -version = "2.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" -dependencies = [ - "cfg-if", - "crunchy", - "zerocopy", -] - -[[package]] -name = "hashbrown" -version = "0.16.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" -dependencies = [ - "foldhash", -] - -[[package]] -name = "hashbrown" -version = "0.17.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" - -[[package]] -name = "heck" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" - -[[package]] -name = "image" -version = "0.25.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85ab80394333c02fe689eaf900ab500fbd0c2213da414687ebf995a65d5a6104" -dependencies = [ - "bytemuck", - "byteorder-lite", - "color_quant", - "exr", - "gif", - "image-webp", - "moxcms", - "num-traits", - "png", - "qoi", - "ravif", - "rayon", - "rgb", - "tiff", - "zune-core", - "zune-jpeg", -] - -[[package]] -name = "image-webp" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "525e9ff3e1a4be2fbea1fdf0e98686a6d98b4d8f937e1bf7402245af1909e8c3" -dependencies = [ - "byteorder-lite", - "quick-error", -] - -[[package]] -name = "imgref" -version = "1.12.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89194689a993ab15268672e99e7b0e19da2da3268ac682e8f02d29d4d1434cd7" - -[[package]] -name = "indexmap" -version = "2.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" -dependencies = [ - "equivalent", - "hashbrown 0.17.1", -] - -[[package]] -name = "instant" -version = "0.1.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222" -dependencies = [ - "cfg-if", - "js-sys", - "wasm-bindgen", - "web-sys", -] - -[[package]] -name = "interpolate_name" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c34819042dc3d3971c46c2190835914dfbe0c3c13f61449b2997f4e9722dfa60" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.119", -] - -[[package]] -name = "is_terminal_polyfill" -version = "1.70.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" - -[[package]] -name = "itertools" -version = "0.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" -dependencies = [ - "either", -] - -[[package]] -name = "itoa" -version = "1.0.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" - -[[package]] -name = "jiff" -version = "0.2.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "961d16382652bfdd8c6f68b223b26a8c93e0d475c672f414411db31c6c5c900e" -dependencies = [ - "defmt", - "jiff-static", - "log", - "portable-atomic", - "portable-atomic-util", - "serde_core", -] - -[[package]] -name = "jiff-static" -version = "0.2.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0879bd39df99c4c5e2c6615ccc026391a423dde10532c573e6086eb94a802cc" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.119", -] - -[[package]] -name = "jni" -version = "0.21.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" -dependencies = [ - "cesu8", - "cfg-if", - "combine", - "jni-sys 0.3.1", - "log", - "thiserror 1.0.69", - "walkdir", - "windows-sys 0.45.0", -] - -[[package]] -name = "jni-sys" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41a652e1f9b6e0275df1f15b32661cf0d4b78d4d87ddec5e0c3c20f097433258" -dependencies = [ - "jni-sys 0.4.1", -] - -[[package]] -name = "jni-sys" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2" -dependencies = [ - "jni-sys-macros", -] - -[[package]] -name = "jni-sys-macros" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" -dependencies = [ - "quote", - "syn 2.0.119", -] - -[[package]] -name = "jobserver" -version = "0.1.35" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c00acbd29eabad4a2392fa0e921c874934dbbf4194312ad20f04a0ed67a3cb3" -dependencies = [ - "getrandom 0.4.3", - "libc", -] - -[[package]] -name = "js-sys" -version = "0.3.103" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53b44bfcdb3f8d5837a46dae1ca9660a837176eee74a28b229bc626816589102" -dependencies = [ - "cfg-if", - "futures-util", - "wasm-bindgen", -] - -[[package]] -name = "lazy_static" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" - -[[package]] -name = "lebe" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a79a3332a6609480d7d0c9eab957bca6b455b91bb84e66d19f5ff66294b85b8" - -[[package]] -name = "libc" -version = "0.2.186" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" - -[[package]] -name = "libfuzzer-sys" -version = "0.4.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9fd2f41a1cba099f79a0b6b6c35656cf7c03351a7bae8ff0f28f25270f929d2" -dependencies = [ - "arbitrary", - "cc", -] - -[[package]] -name = "libloading" -version = "0.8.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" -dependencies = [ - "cfg-if", - "windows-link", -] - -[[package]] -name = "libm" -version = "0.2.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" - -[[package]] -name = "libredox" -version = "0.1.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c943259e342f1e06ff2da7a83eabdfe7f92ce10262688dbf1895ff0b3e6e4652" -dependencies = [ - "bitflags 2.13.1", - "libc", - "plain", - "redox_syscall", -] - -[[package]] -name = "linux-raw-sys" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" - -[[package]] -name = "log" -version = "0.4.33" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" - -[[package]] -name = "loop9" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fae87c125b03c1d2c0150c90365d7d6bcc53fb73a9acaef207d2d065860f062" -dependencies = [ - "imgref", -] - -[[package]] -name = "lz4_flex" -version = "0.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ecbdfe44b1bd960b68170b417450a628c43f7cf56bb3c5317e61cb230ee7f226" -dependencies = [ - "twox-hash", -] - -[[package]] -name = "mach2" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d640282b302c0bb0a2a8e0233ead9035e3bed871f0b7e81fe4a1ec829765db44" -dependencies = [ - "libc", -] - -[[package]] -name = "mach2" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a1b95cd5421ec55b445b5ae102f5ea0e768de1f82bd3001e11f426c269c3aea" -dependencies = [ - "libc", -] - -[[package]] -name = "maybe-rayon" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ea1f30cedd69f0a2954655f7188c6a834246d2bcf1e315e2ac40c4b24dc9519" -dependencies = [ - "cfg-if", - "rayon", -] - -[[package]] -name = "memchr" -version = "2.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" - -[[package]] -name = "memoffset" -version = "0.6.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5aa361d4faea93603064a027415f07bd8e1d5c88c9fbf68bf56a285428fd79ce" -dependencies = [ - "autocfg", -] - -[[package]] -name = "minifb" -version = "0.28.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1a093126f2ed9012fc0b146934c97eb0273e54983680a8bf5309b6b4a365b32" -dependencies = [ - "cc", - "console_error_panic_hook", - "dlib", - "futures", - "instant", - "js-sys", - "lazy_static", - "libc", - "orbclient", - "raw-window-handle", - "serde", - "serde_derive", - "tempfile", - "wasm-bindgen", - "wasm-bindgen-futures", - "wayland-client", - "wayland-cursor", - "wayland-protocols", - "web-sys", - "winapi", - "x11-dl", -] - -[[package]] -name = "miniz_oxide" -version = "0.8.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" -dependencies = [ - "adler2", - "simd-adler32", -] - -[[package]] -name = "moxcms" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb85c154ba489f01b25c0d36ae69a87e4a1c73a72631fc6c0eb6dde34a73e44b" -dependencies = [ - "num-traits", - "pxfm", -] - -[[package]] -name = "ndk" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4" -dependencies = [ - "bitflags 2.13.1", - "jni-sys 0.3.1", - "log", - "ndk-sys", - "num_enum", - "thiserror 1.0.69", -] - -[[package]] -name = "ndk-context" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b" - -[[package]] -name = "ndk-sys" -version = "0.6.0+11769913" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee6cda3051665f1fb8d9e08fc35c96d5a244fb1be711a03b71118828afc9a873" -dependencies = [ - "jni-sys 0.3.1", -] - -[[package]] -name = "new_debug_unreachable" -version = "1.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" - -[[package]] -name = "nix" -version = "0.24.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa52e972a9a719cecb6864fb88568781eb706bac2cd1d4f04a648542dbf78069" -dependencies = [ - "bitflags 1.3.2", - "cfg-if", - "libc", - "memoffset", -] - -[[package]] -name = "no_std_io2" -version = "0.9.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "418abd1b6d34fbf6cae440dc874771b0525a604428704c76e48b29a5e67b8003" -dependencies = [ - "memchr", -] - -[[package]] -name = "nom" -version = "8.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df9761775871bdef83bee530e60050f7e54b1105350d6884eb0fb4f46c2f9405" -dependencies = [ - "memchr", -] - -[[package]] -name = "noop_proc_macro" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0676bb32a98c1a483ce53e500a81ad9c3d5b3f7c920c28c24e9cb0980d0b5bc8" - -[[package]] -name = "num-bigint" -version = "0.4.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c89e69e7e0f03bea5ef08013795c25018e101932225a656383bd384495ecc367" -dependencies = [ - "num-integer", - "num-traits", -] - -[[package]] -name = "num-derive" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.119", -] - -[[package]] -name = "num-integer" -version = "0.1.46" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" -dependencies = [ - "num-traits", -] - -[[package]] -name = "num-rational" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" -dependencies = [ - "num-bigint", - "num-integer", - "num-traits", -] - -[[package]] -name = "num-traits" -version = "0.2.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" -dependencies = [ - "autocfg", - "libm", -] - -[[package]] -name = "num_enum" -version = "0.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d0bca838442ec211fa11de3a8b0e0e8f3a4522575b5c4c06ed722e005036f26" -dependencies = [ - "num_enum_derive", - "rustversion", -] - -[[package]] -name = "num_enum_derive" -version = "0.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "680998035259dcfcafe653688bf2aa6d3e2dc05e98be6ab46afb089dc84f1df8" -dependencies = [ - "proc-macro-crate", - "proc-macro2", - "quote", - "syn 2.0.119", -] - -[[package]] -name = "objc2" -version = "0.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a12a8ed07aefc768292f076dc3ac8c48f3781c8f2d5851dd3d98950e8c5a89f" -dependencies = [ - "objc2-encode", -] - -[[package]] -name = "objc2-audio-toolbox" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6948501a91121d6399b79abaa33a8aa4ea7857fe019f341b8c23ad6e81b79b08" -dependencies = [ - "bitflags 2.13.1", - "libc", - "objc2", - "objc2-core-audio", - "objc2-core-audio-types", - "objc2-core-foundation", - "objc2-foundation", -] - -[[package]] -name = "objc2-avf-audio" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13a380031deed8e99db00065c45937da434ca987c034e13b87e4441f9e4090be" -dependencies = [ - "objc2", - "objc2-foundation", -] - -[[package]] -name = "objc2-core-audio" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1eebcea8b0dbff5f7c8504f3107c68fc061a3eb44932051c8cf8a68d969c3b2" -dependencies = [ - "dispatch2", - "objc2", - "objc2-core-audio-types", - "objc2-core-foundation", - "objc2-foundation", -] - -[[package]] -name = "objc2-core-audio-types" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a89f2ec274a0cf4a32642b2991e8b351a404d290da87bb6a9a9d8632490bd1c" -dependencies = [ - "bitflags 2.13.1", - "objc2", -] - -[[package]] -name = "objc2-core-foundation" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" -dependencies = [ - "bitflags 2.13.1", - "block2", - "dispatch2", - "libc", - "objc2", -] - -[[package]] -name = "objc2-encode" -version = "4.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" - -[[package]] -name = "objc2-foundation" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" -dependencies = [ - "bitflags 2.13.1", - "block2", - "libc", - "objc2", - "objc2-core-foundation", -] - -[[package]] -name = "once_cell" -version = "1.21.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" - -[[package]] -name = "once_cell_polyfill" -version = "1.70.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" - -[[package]] -name = "orbclient" -version = "0.3.55" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5df339f526ea9a60e371768d50efc2f2508c7203290731565d1f7a6f71d21747" -dependencies = [ - "libc", - "libredox", - "sdl2", -] - -[[package]] -name = "paste" -version = "1.0.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" - -[[package]] -name = "pastey" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35fb2e5f958ec131621fdd531e9fc186ed768cbe395337403ae56c17a74c68ec" - -[[package]] -name = "pin-project-lite" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" - -[[package]] -name = "pkg-config" -version = "0.3.33" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" - -[[package]] -name = "plain" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" - -[[package]] -name = "png" -version = "0.18.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61" -dependencies = [ - "bitflags 2.13.1", - "crc32fast", - "fdeflate", - "flate2", - "miniz_oxide", -] - -[[package]] -name = "portable-atomic" -version = "1.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d20d5497ef88037a52ff98267d066e7f11fcc5e99bbfbd58a42336193aacec3" - -[[package]] -name = "portable-atomic-util" -version = "0.2.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2a106d1259c23fac8e543272398ae0e3c0b8d33c88ed73d0cc71b0f1d902618" -dependencies = [ - "portable-atomic", -] - -[[package]] -name = "ppv-lite86" -version = "0.2.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" -dependencies = [ - "zerocopy", -] - -[[package]] -name = "proc-macro-crate" -version = "3.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" -dependencies = [ - "toml_edit", -] - -[[package]] -name = "proc-macro2" -version = "1.0.106" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "profiling" -version = "1.0.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d595e54a326bc53c1c197b32d295e14b169e3cfeaa8dc82b529f947fba6bcf5" -dependencies = [ - "profiling-procmacros", -] - -[[package]] -name = "profiling-procmacros" -version = "1.0.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4488a4a36b9a4ba6b9334a32a39971f77c1436ec82c38707bce707699cc3bbcb" -dependencies = [ - "quote", - "syn 2.0.119", -] - -[[package]] -name = "pxfm" -version = "0.1.30" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d55d956fa96f5ec02be2e13af0e20391a5aa83d6a074e3ad368959d0fab299ea" - -[[package]] -name = "qoi" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f6d64c71eb498fe9eae14ce4ec935c555749aef511cca85b5568910d6e48001" -dependencies = [ - "bytemuck", -] - -[[package]] -name = "quick-error" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" - -[[package]] -name = "quote" -version = "1.0.46" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfbc457d0c7a0759a614551b11a6409e5951f6c7537be1f1b7682b9ae9230368" -dependencies = [ - "proc-macro2", -] - -[[package]] -name = "r-efi" -version = "5.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" - -[[package]] -name = "r-efi" -version = "6.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" - -[[package]] -name = "rand" -version = "0.9.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9ef1d0d795eb7d84685bca4f72f3649f064e6641543d3a8c415898726a57b41" -dependencies = [ - "rand_chacha", - "rand_core 0.9.5", -] - -[[package]] -name = "rand" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7f5fa3a058cd35567ef9bfa5e75732bee0f9e4c55fa90477bef2dfcdbc4be80" -dependencies = [ - "chacha20", - "getrandom 0.4.3", - "rand_core 0.10.1", -] - -[[package]] -name = "rand_chacha" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" -dependencies = [ - "ppv-lite86", - "rand_core 0.9.5", -] - -[[package]] -name = "rand_core" -version = "0.9.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" -dependencies = [ - "getrandom 0.3.4", -] - -[[package]] -name = "rand_core" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" - -[[package]] -name = "rand_distr" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d431c2703ccf129de4d45253c03f49ebb22b97d6ad79ee3ecfc7e3f4862c1d8" -dependencies = [ - "num-traits", - "rand 0.10.2", -] - -[[package]] -name = "rav1e" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43b6dd56e85d9483277cde964fd1bdb0428de4fec5ebba7540995639a21cb32b" -dependencies = [ - "aligned-vec", - "arbitrary", - "arg_enum_proc_macro", - "arrayvec", - "av-scenechange", - "av1-grain", - "bitstream-io", - "built", - "cfg-if", - "interpolate_name", - "itertools", - "libc", - "libfuzzer-sys", - "log", - "maybe-rayon", - "new_debug_unreachable", - "noop_proc_macro", - "num-derive", - "num-traits", - "paste", - "profiling", - "rand 0.9.5", - "rand_chacha", - "simd_helpers", - "thiserror 2.0.20", - "v_frame", - "wasm-bindgen", -] - -[[package]] -name = "ravif" -version = "0.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e52310197d971b0f5be7fe6b57530dcd27beb35c1b013f29d66c1ad73fbbcc45" -dependencies = [ - "avif-serialize", - "imgref", - "loop9", - "quick-error", - "rav1e", - "rayon", - "rgb", -] - -[[package]] -name = "raw-window-handle" -version = "0.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" - -[[package]] -name = "rayon" -version = "1.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d" -dependencies = [ - "either", - "rayon-core", -] - -[[package]] -name = "rayon-core" -version = "1.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" -dependencies = [ - "crossbeam-deque", - "crossbeam-utils", -] - -[[package]] -name = "redox_syscall" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5102a6aaa05aa011a238e178e6bca86d2cb56fc9f586d37cb80f5bca6e07759" -dependencies = [ - "bitflags 2.13.1", -] - -[[package]] -name = "regalloc2" -version = "0.15.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "757712e8e61590d6d4f5d563483755538b5aa13467837a3b41cd9832509a7f85" -dependencies = [ - "allocator-api2", - "bumpalo", - "hashbrown 0.17.1", - "log", - "rustc-hash", - "smallvec", -] - -[[package]] -name = "regex" -version = "1.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d" -dependencies = [ - "aho-corasick", - "memchr", - "regex-automata", - "regex-syntax", -] - -[[package]] -name = "regex-automata" -version = "0.4.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fcfdb36bda0c880c5931cdc7a2bcdc8ba4556847b9d912bca70bc94708711ad" -dependencies = [ - "aho-corasick", - "memchr", - "regex-syntax", -] - -[[package]] -name = "regex-syntax" -version = "0.8.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" - -[[package]] -name = "region" -version = "3.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6b6ebd13bc009aef9cd476c1310d49ac354d36e240cf1bd753290f3dc7199a7" -dependencies = [ - "bitflags 1.3.2", - "libc", - "mach2 0.4.3", - "windows-sys 0.52.0", -] - -[[package]] -name = "rgb" -version = "0.8.53" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47b34b781b31e5d73e9fbc8689c70551fd1ade9a19e3e28cfec8580a79290cc4" - -[[package]] -name = "rodio" -version = "0.22.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0a536bb79db59098ef71a4dd4246c02eb87b316deceb1b68e0cde7167ec01eb" -dependencies = [ - "cpal", - "dasp_sample", - "num-rational", - "rand 0.10.2", - "rand_distr", - "rtrb", - "symphonia", - "thiserror 2.0.20", -] - -[[package]] -name = "rtrb" -version = "0.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ade083ccbb4bf536df69d1f6432cc23deb7acccff86b183f3923a6fd56a1153" - -[[package]] -name = "rustc-hash" -version = "2.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d" - -[[package]] -name = "rustix" -version = "1.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" -dependencies = [ - "bitflags 2.13.1", - "errno", - "libc", - "linux-raw-sys", - "windows-sys 0.61.2", -] - -[[package]] -name = "rustversion" -version = "1.0.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" - -[[package]] -name = "same-file" -version = "1.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" -dependencies = [ - "winapi-util", -] - -[[package]] -name = "scoped-tls" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" - -[[package]] -name = "sdl2" -version = "0.38.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d42407afc6a8ab67e36f92e80b8ba34cbdc55aaeed05249efe9a2e8d0e9feef" -dependencies = [ - "bitflags 1.3.2", - "lazy_static", - "libc", - "sdl2-sys", -] - -[[package]] -name = "sdl2-sys" -version = "0.38.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ff61407fc75d4b0bbc93dc7e4d6c196439965fbef8e4a4f003a36095823eac0" -dependencies = [ - "cfg-if", - "libc", - "version-compare", -] - -[[package]] -name = "serde" -version = "1.0.229" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" -dependencies = [ - "serde_core", - "serde_derive", -] - -[[package]] -name = "serde_core" -version = "1.0.229" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" -dependencies = [ - "serde_derive", -] - -[[package]] -name = "serde_derive" -version = "1.0.229" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" -dependencies = [ - "proc-macro2", - "quote", - "syn 3.0.3", -] - -[[package]] -name = "serde_json" -version = "1.0.151" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" -dependencies = [ - "itoa", - "memchr", - "serde", - "serde_core", - "zmij", -] - -[[package]] -name = "shlex" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" - -[[package]] -name = "simd-adler32" -version = "0.3.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a219298ac11a56ea9a6d2120044824d6f01aeb034955e7af7bc16858527deea" - -[[package]] -name = "simd_helpers" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95890f873bec569a0362c235787f3aca6e1e887302ba4840839bcc6459c42da6" -dependencies = [ - "quote", -] - -[[package]] -name = "slab" -version = "0.4.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" - -[[package]] -name = "smallvec" -version = "1.15.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" - -[[package]] -name = "stable_deref_trait" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" - -[[package]] -name = "strsim" -version = "0.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" - -[[package]] -name = "symphonia" -version = "0.5.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5773a4c030a19d9bfaa090f49746ff35c75dfddfa700df7a5939d5e076a57039" -dependencies = [ - "lazy_static", - "symphonia-bundle-flac", - "symphonia-bundle-mp3", - "symphonia-codec-aac", - "symphonia-codec-pcm", - "symphonia-codec-vorbis", - "symphonia-core", - "symphonia-format-isomp4", - "symphonia-format-ogg", - "symphonia-format-riff", - "symphonia-metadata", -] - -[[package]] -name = "symphonia-bundle-flac" -version = "0.5.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c91565e180aea25d9b80a910c546802526ffd0072d0b8974e3ebe59b686c9976" -dependencies = [ - "log", - "symphonia-core", - "symphonia-metadata", - "symphonia-utils-xiph", -] - -[[package]] -name = "symphonia-bundle-mp3" -version = "0.5.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4872dd6bb56bf5eac799e3e957aa1981086c3e613b27e0ac23b176054f7c57ed" -dependencies = [ - "lazy_static", - "log", - "symphonia-core", - "symphonia-metadata", -] - -[[package]] -name = "symphonia-codec-aac" -version = "0.5.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c263845aa86881416849c1729a54c7f55164f8b96111dba59de46849e73a790" -dependencies = [ - "lazy_static", - "log", - "symphonia-core", -] - -[[package]] -name = "symphonia-codec-pcm" -version = "0.5.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e89d716c01541ad3ebe7c91ce4c8d38a7cf266a3f7b2f090b108fb0cb031d95" -dependencies = [ - "log", - "symphonia-core", -] - -[[package]] -name = "symphonia-codec-vorbis" -version = "0.5.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f025837c309cd69ffef572750b4a2257b59552c5399a5e49707cc5b1b85d1c73" -dependencies = [ - "log", - "symphonia-core", - "symphonia-utils-xiph", -] - -[[package]] -name = "symphonia-core" -version = "0.5.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea00cc4f79b7f6bb7ff87eddc065a1066f3a43fe1875979056672c9ef948c2af" -dependencies = [ - "arrayvec", - "bitflags 1.3.2", - "bytemuck", - "lazy_static", - "log", -] - -[[package]] -name = "symphonia-format-isomp4" -version = "0.5.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "243739585d11f81daf8dac8d9f3d18cc7898f6c09a259675fc364b382c30e0a5" -dependencies = [ - "encoding_rs", - "log", - "symphonia-core", - "symphonia-metadata", - "symphonia-utils-xiph", -] - -[[package]] -name = "symphonia-format-ogg" -version = "0.5.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b4955c67c1ed3aa8ae8428d04ca8397fbef6a19b2b051e73b5da8b1435639cb" -dependencies = [ - "log", - "symphonia-core", - "symphonia-metadata", - "symphonia-utils-xiph", -] - -[[package]] -name = "symphonia-format-riff" -version = "0.5.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2d7c3df0e7d94efb68401d81906eae73c02b40d5ec1a141962c592d0f11a96f" -dependencies = [ - "extended", - "log", - "symphonia-core", - "symphonia-metadata", -] - -[[package]] -name = "symphonia-metadata" -version = "0.5.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36306ff42b9ffe6e5afc99d49e121e0bd62fe79b9db7b9681d48e29fa19e6b16" -dependencies = [ - "encoding_rs", - "lazy_static", - "log", - "symphonia-core", -] - -[[package]] -name = "symphonia-utils-xiph" -version = "0.5.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee27c85ab799a338446b68eec77abf42e1a6f1bb490656e121c6e27bfbab9f16" -dependencies = [ - "symphonia-core", - "symphonia-metadata", -] - -[[package]] -name = "syn" -version = "2.0.119" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "syn" -version = "3.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "target-lexicon" -version = "0.13.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "adb6935a6f5c20170eeceb1a3835a49e12e19d792f6dd344ccc76a985ca5a6ca" - -[[package]] -name = "tempfile" -version = "3.27.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" -dependencies = [ - "fastrand", - "getrandom 0.4.3", - "once_cell", - "rustix", - "windows-sys 0.61.2", -] - -[[package]] -name = "thiserror" -version = "1.0.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" -dependencies = [ - "thiserror-impl 1.0.69", -] - -[[package]] -name = "thiserror" -version = "2.0.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec86235f5fcc2a73650310756d2ac5b138a5780bbbdfae3eeccec992c435ba4f" -dependencies = [ - "thiserror-impl 2.0.20", -] - -[[package]] -name = "thiserror-impl" -version = "1.0.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.119", -] - -[[package]] -name = "thiserror-impl" -version = "2.0.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc04cd3e1236dd4a98afca4569f2deb3f120e5422a4023be2cb683f8486292af" -dependencies = [ - "proc-macro2", - "quote", - "syn 3.0.3", -] - -[[package]] -name = "tiff" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b63feaf3343d35b6ca4d50483f94843803b0f51634937cc2ec519fc32232bc52" -dependencies = [ - "fax", - "flate2", - "half", - "quick-error", - "weezl", - "zune-jpeg", -] - -[[package]] -name = "toml_datetime" -version = "1.1.1+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" -dependencies = [ - "serde_core", -] - -[[package]] -name = "toml_edit" -version = "0.25.13+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6975367e4d2ef766d86af01ffad14b622fecc8d4357a998fbc4deb6e9bacaf9b" -dependencies = [ - "indexmap", - "toml_datetime", - "toml_parser", - "winnow", -] - -[[package]] -name = "toml_parser" -version = "1.1.3+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d38ac1cf9b95face32296c0a3ede1fdc270627c9d9c02a7274dd6d960dc4d56" -dependencies = [ - "winnow", -] - -[[package]] -name = "twox-hash" -version = "2.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8464ec13c3691491391d9fce00f6416c9a48e46972f72d7865688be2080192c9" - -[[package]] -name = "unicode-ident" -version = "1.0.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" - -[[package]] -name = "utf8parse" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" - -[[package]] -name = "v_frame" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "666b7727c8875d6ab5db9533418d7c764233ac9c0cff1d469aec8fa127597be2" -dependencies = [ - "aligned-vec", - "num-traits", - "wasm-bindgen", -] - -[[package]] -name = "version-compare" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "579a42fc0b8e0c63b76519a339be31bed574929511fa53c1a3acae26eb258f29" - -[[package]] -name = "walkdir" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" -dependencies = [ - "same-file", - "winapi-util", -] - -[[package]] -name = "wasip2" -version = "1.0.4+wasi-0.2.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487" -dependencies = [ - "wit-bindgen", -] - -[[package]] -name = "wasm-bindgen" -version = "0.2.126" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b067c0c11094aef6b7a801c1e34a26affafdf3d051dba08456b868789aaf9a4" -dependencies = [ - "cfg-if", - "once_cell", - "rustversion", - "serde", - "serde_json", - "wasm-bindgen-macro", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-futures" -version = "0.4.76" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c62df1340f32221cb9c54d6a27b030e3dba64361d4a95bed55f9aacb44da291d" -dependencies = [ - "js-sys", - "wasm-bindgen", -] - -[[package]] -name = "wasm-bindgen-macro" -version = "0.2.126" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "167ce5e579f6bcf889c4f7175a8a5a585de84e8ff93976ce393efa5f2837aab1" -dependencies = [ - "quote", - "wasm-bindgen-macro-support", -] - -[[package]] -name = "wasm-bindgen-macro-support" -version = "0.2.126" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3997c7839262f4ef12cf90b818d6340c18e80f263f1a94bf157d0ec4420380e" -dependencies = [ - "bumpalo", - "proc-macro2", - "quote", - "syn 2.0.119", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-shared" -version = "0.2.126" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc1b4cb0cc549fcf58d7dfc081778139b3d283a081644e833e84682ad71cea24" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "wasmtime-internal-core" -version = "44.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aedd3947487d0afdd37accb981466fcd60571e898004c8955111f88686581dfc" -dependencies = [ - "hashbrown 0.16.1", - "libm", -] - -[[package]] -name = "wasmtime-internal-jit-icache-coherence" -version = "44.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10005b038e662775ac002f233e429447a58892e89918580fa67ce8cdd9192d0a" -dependencies = [ - "cfg-if", - "libc", - "wasmtime-internal-core", - "windows-sys 0.61.2", -] - -[[package]] -name = "wayland-client" -version = "0.29.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f3b068c05a039c9f755f881dc50f01732214f5685e379829759088967c46715" -dependencies = [ - "bitflags 1.3.2", - "downcast-rs", - "libc", - "nix", - "scoped-tls", - "wayland-commons", - "wayland-scanner", - "wayland-sys", -] - -[[package]] -name = "wayland-commons" -version = "0.29.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8691f134d584a33a6606d9d717b95c4fa20065605f798a3f350d78dced02a902" -dependencies = [ - "nix", - "once_cell", - "smallvec", - "wayland-sys", -] - -[[package]] -name = "wayland-cursor" -version = "0.29.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6865c6b66f13d6257bef1cd40cbfe8ef2f150fb8ebbdb1e8e873455931377661" -dependencies = [ - "nix", - "wayland-client", - "xcursor", -] - -[[package]] -name = "wayland-protocols" -version = "0.29.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b950621f9354b322ee817a23474e479b34be96c2e909c14f7bc0100e9a970bc6" -dependencies = [ - "bitflags 1.3.2", - "wayland-client", - "wayland-commons", - "wayland-scanner", -] - -[[package]] -name = "wayland-scanner" -version = "0.29.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f4303d8fa22ab852f789e75a967f0a2cdc430a607751c0499bada3e451cbd53" -dependencies = [ - "proc-macro2", - "quote", - "xml-rs", -] - -[[package]] -name = "wayland-sys" -version = "0.29.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be12ce1a3c39ec7dba25594b97b42cb3195d54953ddb9d3d95a7c3902bc6e9d4" -dependencies = [ - "dlib", - "lazy_static", - "pkg-config", -] - -[[package]] -name = "web-sys" -version = "0.3.103" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8622dcb61c0bcc9fffa6938bed81210af2da9a7e4a1a834b2e37a59b6dfb6141" -dependencies = [ - "js-sys", - "wasm-bindgen", -] - -[[package]] -name = "weezl" -version = "0.1.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a28ac98ddc8b9274cb41bb4d9d4d5c425b6020c50c46f25559911905610b4a88" - -[[package]] -name = "winapi" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" -dependencies = [ - "winapi-i686-pc-windows-gnu", - "winapi-x86_64-pc-windows-gnu", -] - -[[package]] -name = "winapi-i686-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" - -[[package]] -name = "winapi-util" -version = "0.1.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" -dependencies = [ - "windows-sys 0.61.2", -] - -[[package]] -name = "winapi-x86_64-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" - -[[package]] -name = "windows" -version = "0.62.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "527fadee13e0c05939a6a05d5bd6eec6cd2e3dbd648b9f8e447c6518133d8580" -dependencies = [ - "windows-collections", - "windows-core", - "windows-future", - "windows-numerics", -] - -[[package]] -name = "windows-collections" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23b2d95af1a8a14a3c7367e1ed4fc9c20e0a26e79551b1454d72583c97cc6610" -dependencies = [ - "windows-core", -] - -[[package]] -name = "windows-core" -version = "0.62.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" -dependencies = [ - "windows-implement", - "windows-interface", - "windows-link", - "windows-result", - "windows-strings", -] - -[[package]] -name = "windows-future" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1d6f90251fe18a279739e78025bd6ddc52a7e22f921070ccdc67dde84c605cb" -dependencies = [ - "windows-core", - "windows-link", - "windows-threading", -] - -[[package]] -name = "windows-implement" -version = "0.60.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.119", -] - -[[package]] -name = "windows-interface" -version = "0.59.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.119", -] - -[[package]] -name = "windows-link" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" - -[[package]] -name = "windows-numerics" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e2e40844ac143cdb44aead537bbf727de9b044e107a0f1220392177d15b0f26" -dependencies = [ - "windows-core", - "windows-link", -] - -[[package]] -name = "windows-result" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" -dependencies = [ - "windows-link", -] - -[[package]] -name = "windows-strings" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" -dependencies = [ - "windows-link", -] - -[[package]] -name = "windows-sys" -version = "0.45.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" -dependencies = [ - "windows-targets 0.42.2", -] - -[[package]] -name = "windows-sys" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" -dependencies = [ - "windows-targets 0.52.6", -] - -[[package]] -name = "windows-sys" -version = "0.61.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" -dependencies = [ - "windows-link", -] - -[[package]] -name = "windows-targets" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" -dependencies = [ - "windows_aarch64_gnullvm 0.42.2", - "windows_aarch64_msvc 0.42.2", - "windows_i686_gnu 0.42.2", - "windows_i686_msvc 0.42.2", - "windows_x86_64_gnu 0.42.2", - "windows_x86_64_gnullvm 0.42.2", - "windows_x86_64_msvc 0.42.2", -] - -[[package]] -name = "windows-targets" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" -dependencies = [ - "windows_aarch64_gnullvm 0.52.6", - "windows_aarch64_msvc 0.52.6", - "windows_i686_gnu 0.52.6", - "windows_i686_gnullvm", - "windows_i686_msvc 0.52.6", - "windows_x86_64_gnu 0.52.6", - "windows_x86_64_gnullvm 0.52.6", - "windows_x86_64_msvc 0.52.6", -] - -[[package]] -name = "windows-threading" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3949bd5b99cafdf1c7ca86b43ca564028dfe27d66958f2470940f73d86d75b37" -dependencies = [ - "windows-link", -] - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" - -[[package]] -name = "windows_i686_gnu" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" - -[[package]] -name = "windows_i686_gnu" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" - -[[package]] -name = "windows_i686_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" - -[[package]] -name = "windows_i686_msvc" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" - -[[package]] -name = "windows_i686_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" - -[[package]] -name = "winnow" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81" -dependencies = [ - "memchr", -] - -[[package]] -name = "wit-bindgen" -version = "0.57.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" - -[[package]] -name = "x11-dl" -version = "2.21.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38735924fedd5314a6e548792904ed8c6de6636285cb9fec04d5b1db85c1516f" -dependencies = [ - "libc", - "once_cell", - "pkg-config", -] - -[[package]] -name = "xcursor" -version = "0.3.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bec9e4a500ca8864c5b47b8b482a73d62e4237670e5b5f1d6b9e3cae50f28f2b" - -[[package]] -name = "xml-rs" -version = "0.8.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ae8337f8a065cfc972643663ea4279e04e7256de865aa66fe25cec5fb912d3f" - -[[package]] -name = "y4m" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a5a4b21e1a62b67a2970e6831bc091d7b87e119e7f9791aef9702e3bef04448" - -[[package]] -name = "zerocopy" -version = "0.8.54" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7cbbc0a705a0fd05cc3676525980d2bf5a9bc4adac6d6475209a7887cf59d19" -dependencies = [ - "zerocopy-derive", -] - -[[package]] -name = "zerocopy-derive" -version = "0.8.54" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2e817b7b52d0c7358d3246da9d69935ebb18116b2b102b4230dac079b4862f5" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.119", -] - -[[package]] -name = "zmij" -version = "1.0.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" - -[[package]] -name = "zune-core" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb8a0807f7c01457d0379ba880ba6322660448ddebc890ce29bb64da71fb40f9" - -[[package]] -name = "zune-inflate" -version = "0.2.54" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73ab332fe2f6680068f3582b16a24f90ad7096d5d39b974d1c0aff0125116f02" -dependencies = [ - "simd-adler32", -] - -[[package]] -name = "zune-jpeg" -version = "0.5.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27bc9d5b815bc103f142aa054f561d9187d191692ec7c2d1e2b4737f8dbd7296" -dependencies = [ - "zune-core", -] diff --git a/native/DingooEmu/Cargo.toml b/native/DingooEmu/Cargo.toml deleted file mode 100644 index 5a0c1ff..0000000 --- a/native/DingooEmu/Cargo.toml +++ /dev/null @@ -1,43 +0,0 @@ -[workspace] -resolver = "2" -members = [ - "crates/dingooemu-core", - "crates/dingooemu", - "crates/dingooemu-libretro", -] -# The libretro buildbot runs a bare `cargo build --release --target ` at -# the repo root. Restrict the default build set to the libretro core so the CI -# does not try to cross-compile the standalone GUI binary (minifb/rodio, which -# need X11/ALSA). Explicit `-p` / `--workspace` selections are unaffected. -default-members = ["crates/dingooemu-libretro"] - -[workspace.package] -version = "0.2.0" -edition = "2021" -description = "A Dingoo A320 emulator written in Rust" -license = "BSD-3-Clause" - -[workspace.dependencies] -dingooemu-core = { path = "crates/dingooemu-core" } -anyhow = "=1.0.104" -thiserror = "=2.0.20" -log = "=0.4.33" -serde = { version = "=1.0.229", features = ["derive"] } -serde_json = "=1.0.151" -crc32fast = "=1.5.0" -bincode = "=1.3.3" -lz4_flex = "=0.14.0" -image = "=0.25.10" -clap = { version = "=4.6.6", features = ["derive"] } -minifb = "=0.28.0" -rodio = { version = "=0.22.2", features = ["mp3"] } -env_logger = "=0.11.11" -cranelift = "=0.131.3" -cranelift-jit = "=0.131.3" -cranelift-module = "=0.131.3" -cranelift-native = "=0.131.3" - -[profile.release] -opt-level = 3 -lto = true -strip = true diff --git a/native/DingooEmu/LICENSE b/native/DingooEmu/LICENSE deleted file mode 100644 index 380be80..0000000 --- a/native/DingooEmu/LICENSE +++ /dev/null @@ -1,28 +0,0 @@ -BSD 3-Clause License - -Copyright (c) 2025, Aloys (AloysHF) - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are met: - -1. Redistributions of source code must retain the above copyright notice, this - list of conditions and the following disclaimer. - -2. Redistributions in binary form must reproduce the above copyright notice, - this list of conditions and the following disclaimer in the documentation - and/or other materials provided with the distribution. - -3. Neither the name of the copyright holder nor the names of its - contributors may be used to endorse or promote products derived from - this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE -FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL -DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR -SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER -CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, -OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/native/DingooEmu/README.md b/native/DingooEmu/README.md deleted file mode 100644 index c19f3c8..0000000 --- a/native/DingooEmu/README.md +++ /dev/null @@ -1,171 +0,0 @@ -# Dingoo A320 Emulator — A Dingoo A320 emulator written in Rust - -

- Dingoo A320 Emulator -

- -

- Website - CI - Gitlab Pipeline Status - Release - Downloads - Quality Gate Status - License: BSD 3-Clause - Discord - QQ Group -

- -Dingoo A320 is a handheld game console powered by the Ingenic JZ4740 MIPS SoC. This emulator runs `.app` game files from the Dingoo ecosystem through high-level emulation of the MIPS32 CPU and Dingoo SDK. - -## Features - -- **Tiered MIPS32 CPU execution** — Cached interpreter on every platform, plus native translation of hot blocks on 64-bit Android -- **Real-time scheduling** — Guest timing stays at 60 Hz without requiring one host-side dispatch per hardware clock cycle -- **HLE (High-Level Emulation)** — Dingoo SDK functions for graphics, focused-window key callbacks, audio, timing, files, and companion-content discovery implemented in Rust -- **Auditable compatibility diagnostics** — Aggregate unknown SDK calls and emit per-game JSON reports for review -- **`.app` file support** — Parse and load Dingoo A320 game container format -- **Frame rendering** — Native 320×240 RGB565 framebuffer output -- **PCM audio output** — Dingoo waveout playback with format conversion, volume, and resampling -- **Screenshot mode** — Headless frame capture for automated testing and preview generation -- **Batch screenshot** — Process multiple `.app` files with `scripts/batch-screenshots.ps1` -- **RetroArch integration** — libretro core with video, audio, RetroPad input, reset, and persistent game saves -- **Cross-platform** — Windows, Linux, macOS - -## Usage - -### Standalone Mode - -Download the latest binary from the -[Releases](https://github.com/AloysHF/DingooEmu/releases) page and run: - -```bash -dingooemu path/to/game.app -``` - -See the [Standalone Emulator](docs/Standalone-Emulator.md) guide for -installation, keyboard controls, screenshot mode, and all command-line options. - -### RetroArch Mode - - - -Install **Dingoo A320 (DingooEmu)** from RetroArch's Core Downloader, or -install the release files manually, then load a `.app` game through -**Load Content**. - -See the [RetroArch Core](docs/RetroArch-Core.md) guide for installation, -supported platforms and features, RetroPad mapping, core options, and cheats. - -## Building - -Requires [Rust](https://www.rust-lang.org/tools/install) (stable). - -### Standalone Mode (Default) - -```bash -cargo build -p dingooemu --release -cargo run -p dingooemu --release -- path/to/game.app -cargo run -p dingooemu --release -- --fullscreen path/to/game.app -``` - -The binary is produced at `target/release/dingoo-emu` (`dingoo-emu.exe` on -Windows). - -### Libretro Core (for RetroArch) - -```bash -cargo build -p dingooemu-libretro --release -``` - -Cargo names the cdylib after its lib target, so this produces -`dingooemu_libretro.dll` on Windows, `libdingooemu_libretro.so` on Linux, or -`libdingooemu_libretro.dylib` on macOS under `target/release/`. RetroArch -expects the core file to be named `dingooemu_libretro.`, so remove the -leading `lib` from the Linux or macOS output before copying it into -RetroArch's `cores/` directory. - -For Android cross-compilation, see -[Android Libretro Core](docs/Android-Libretro-Core.md). For iOS, see -[iOS Libretro Core](docs/iOS-Libretro-Core.md). - -## Testing - -Run the unit tests: - -```bash -cargo test --workspace -``` - -## Architecture - -``` -crates/ -├── dingooemu-core/ # Platform-independent emulator engine (library) -│ └── src/ -│ ├── lib.rs # Crate root (module declarations) -│ ├── emulator.rs # Shared Emulator (both front-ends) -│ ├── emulator/ -│ │ └── sdk_hle/ # Runtime SDK dispatch and implementations -│ │ ├── mod.rs # Single HLE dispatcher -│ │ ├── graphics.rs # LCD and framebuffer calls -│ │ ├── gui.rs # Focused-window key message dispatch -│ │ ├── input.rs # Buttons and input events -│ │ ├── audio.rs # PCM and wave output -│ │ ├── files.rs # Resources, files, and saves -│ │ ├── tasks.rs # Tasks and semaphores -│ │ └── system.rs # Memory, timing, and system calls -│ ├── cpu.rs # Cached-block MIPS32 CPU interpreter -│ ├── jit.rs # Optional native translator for hot MIPS32 blocks -│ ├── memory.rs # Memory bus (32MB address space) -│ ├── video.rs # Framebuffer and screen rendering -│ ├── audio.rs # Audio engine (PCM output) -│ ├── input.rs # Button state management -│ ├── app_loader.rs # .app container parser -│ └── error.rs # Error types -├── dingooemu/ # Standalone binary (-> dingooemu) -│ └── src/ -│ └── main.rs # Window loop and CLI front-end -└── dingooemu-libretro/ # libretro cdylib (-> dingooemu_libretro.{dll,so,dylib}) - ├── dingooemu_libretro.info # RetroArch core metadata - └── src/ - ├── lib.rs # cdylib crate root - └── libretro/ - ├── api.rs # Exported libretro functions - ├── callbacks.rs # Callback management - └── types.rs # libretro type definitions -``` - -## Game Compatibility - -Compatibility results are experimental and cover startup and initial rendering -only. See [Game Compatibility](docs/Game-Compatibility.md) for the current -39-build results and screenshots. - -## Keyboard Controls - -| Key | Dingoo Button | -|-----|---------------| -| Arrow keys | D-pad | -| X | A | -| Z | B | -| S | X | -| A | Y | -| Enter | START | -| Right Shift | SELECT | -| Q | L shoulder | -| W | R shoulder | -| Esc | Exit | - -The standalone defaults match RetroArch's standard keyboard bindings for the -equivalent RetroPad buttons. - -## Contribute - -Contributions are welcome! Whether you're interested in fixing bugs, adding features, improving documentation, or testing game compatibility, we'd love your help. See [CONTRIBUTING.md](docs/CONTRIBUTING.md) for details. - -## License - -This project is licensed under the [BSD 3-Clause License](LICENSE). -JIT-enabled Android binaries also contain compatible third-party components; -their complete terms are included in [THIRD_PARTY_LICENSES.md](THIRD_PARTY_LICENSES.md). diff --git a/native/DingooEmu/THIRD_PARTY_LICENSES.md b/native/DingooEmu/THIRD_PARTY_LICENSES.md deleted file mode 100644 index 3e3e066..0000000 --- a/native/DingooEmu/THIRD_PARTY_LICENSES.md +++ /dev/null @@ -1,267 +0,0 @@ -# JIT Third-Party Licenses - -DingooEmu's own source code remains licensed under the BSD 3-Clause License -in `LICENSE`. JIT-enabled Android binaries additionally incorporate the -following compatible third-party components: - -- Cranelift and Wasmtime internal support crates: Apache-2.0 WITH LLVM-exception -- `region`: MIT - -The complete license texts distributed by those crates follow. - -## Cranelift and Wasmtime - -```text - - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright [yyyy] [name of copyright owner] - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - - ---- LLVM Exceptions to the Apache 2.0 License ---- - -As an exception, if, as a result of your compiling your source code, portions -of this Software are embedded into an Object form of such source code, you -may redistribute such embedded portions in such Object form without complying -with the conditions of Sections 4(a), 4(b) and 4(d) of the License. - -In addition, if you combine or link compiled forms of this Software with -software that is licensed under the GPLv2 ("Combined Software") and if a -court of competent jurisdiction determines that the patent provision (Section -3), the indemnity provision (Section 9) or other Section of the License -conflicts with the conditions of the GPLv2, you may retroactively and -prospectively choose to deem waived or otherwise exclude such Section(s) of -the License, but only in their entirety and only with respect to the Combined -Software. - - -``` - -## region - -```text -Copyright (c) 2016 Elliott Linder - -Permission is hereby granted, free of charge, to any -person obtaining a copy of this software and associated -documentation files (the "Software"), to deal in the -Software without restriction, including without -limitation the rights to use, copy, modify, merge, -publish, distribute, sublicense, and/or sell copies of -the Software, and to permit persons to whom the Software -is furnished to do so, subject to the following -conditions: - -The above copyright notice and this permission notice -shall be included in all copies or substantial portions -of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF -ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED -TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A -PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT -SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY -CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR -IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -DEALINGS IN THE SOFTWARE. - -``` diff --git a/native/DingooEmu/crates/dingooemu-core/Cargo.toml b/native/DingooEmu/crates/dingooemu-core/Cargo.toml deleted file mode 100644 index fce5f07..0000000 --- a/native/DingooEmu/crates/dingooemu-core/Cargo.toml +++ /dev/null @@ -1,36 +0,0 @@ -[package] -name = "dingooemu-core" -description.workspace = true -version.workspace = true -edition.workspace = true -license.workspace = true - -[dependencies] -anyhow.workspace = true -thiserror.workspace = true -log.workspace = true -serde.workspace = true -serde_json.workspace = true -crc32fast.workspace = true -bincode.workspace = true -lz4_flex.workspace = true -byteorder = "1" -image.workspace = true -rodio = { workspace = true, optional = true } -cranelift = { workspace = true, optional = true } -cranelift-jit = { workspace = true, optional = true } -cranelift-module = { workspace = true, optional = true } -cranelift-native = { workspace = true, optional = true } - -[dev-dependencies] -env_logger.workspace = true -cargo-husky = { version = "=1.5.0", default-features = false, features = ["precommit-hook", "run-cargo-fmt", "run-cargo-clippy"] } - -[features] -standalone = ["dep:rodio"] -jit = [ - "dep:cranelift", - "dep:cranelift-jit", - "dep:cranelift-module", - "dep:cranelift-native", -] diff --git a/native/DingooEmu/crates/dingooemu-core/src/app_loader.rs b/native/DingooEmu/crates/dingooemu-core/src/app_loader.rs deleted file mode 100644 index 9e929bc..0000000 --- a/native/DingooEmu/crates/dingooemu-core/src/app_loader.rs +++ /dev/null @@ -1,913 +0,0 @@ -use crate::error::{Result, SimulatorError}; -use std::path::Path; - -/// Magic bytes for .app format chunks -#[allow(dead_code)] -const MAGIC_CCDL: &[u8; 4] = b"CCDL"; -#[allow(dead_code)] -const MAGIC_IMPT: &[u8; 4] = b"IMPT"; -#[allow(dead_code)] -const MAGIC_EXPT: &[u8; 4] = b"EXPT"; -#[allow(dead_code)] -const MAGIC_RAWD: &[u8; 4] = b"RAWD"; -#[allow(dead_code)] -const MAGIC_ERPT: &[u8; 4] = b"ERPT"; - -/// Fixed offsets for chunk descriptors -#[allow(dead_code)] -const CCDL_OFFSET: u32 = 0; -const IMPT_OFFSET: u32 = 0x20; -const EXPT_OFFSET: u32 = 0x40; -const RAWD_OFFSET: u32 = 0x60; -const ERPT_OFFSET: u32 = 0x80; - -/// Minimum valid file size -const MIN_FILE_SIZE: usize = 0x80; // 128 bytes - -/// Chunk descriptor (16 bytes) -#[derive(Debug, Clone, Default)] -pub struct ChunkHeader { - /// 4-byte ASCII identifier - pub ident: [u8; 4], - /// Type field - pub chunk_type: u32, - /// Absolute file offset of chunk payload - pub offset: u32, - /// Byte size of chunk payload - pub size: u32, -} - -/// RAWD header (extended 32 bytes) -#[derive(Debug, Clone, Default)] -pub struct RawdHeader { - /// Base chunk header - pub base: ChunkHeader, - /// Entry point address - pub entry: u32, - /// Load base address (origin) - pub origin: u32, - /// Total program size (>= rawd.size) - pub program_size: u32, -} - -/// Symbol table entry (16 bytes) -#[derive(Debug, Clone)] -pub struct SymbolEntry { - /// Offset into string table - pub string_offset: u32, - /// Unknown field - pub unknown0: u32, - /// Unknown field - pub unknown1: u32, - /// Address/offset associated with this symbol - pub address: u32, - /// Resolved symbol name - pub name: String, -} - -/// Resource table entry -#[derive(Debug, Clone)] -pub struct ResourceEntry { - /// Resource kind - pub kind: ResourceKind, - /// Resource name - pub name: String, - /// Absolute file offset - pub offset: u32, - /// Byte size - pub size: u32, - /// XOR key (0x40 for ERPT, 0 for others) - pub xor_key: u8, -} - -/// Resource table kind -#[derive(Debug, Clone, Copy, PartialEq)] -pub enum ResourceKind { - /// ERPT (XOR 0x40 encoded) - Erpt, - /// Packed (short-name format) - Packed, - /// Packed64 (long-path format) - Packed64, -} - -/// Complete parsed .app file structure -#[derive(Debug, Clone)] -pub struct AppImage { - /// Raw file data - pub data: Vec, - /// Import table descriptor - pub impt: ChunkHeader, - /// Export table descriptor - pub expt: ChunkHeader, - /// RAWD header - pub rawd: RawdHeader, - /// Whether ERPT chunk exists - pub has_erpt: bool, - /// ERPT descriptor (if present) - pub erpt: ChunkHeader, - /// Parsed import symbols - pub imports: Vec, - /// Parsed export symbols - pub exports: Vec, - /// All detected resources - pub resources: Vec, -} - -impl AppImage { - /// Load and parse an .app file from disk - pub fn from_path>(path: P) -> Result { - let data = std::fs::read(path.as_ref())?; - Self::parse(&data) - } - - /// Parse .app data from a byte slice - pub fn parse(data: &[u8]) -> Result { - // Validate minimum size - if data.len() < MIN_FILE_SIZE { - return Err(SimulatorError::InvalidAppFormat(format!( - "File too small: {} bytes (minimum {})", - data.len(), - MIN_FILE_SIZE - ))); - } - - // Validate CCDL magic at offset 0 - if data[0..4] != *MAGIC_CCDL { - return Err(SimulatorError::InvalidAppFormat( - "Invalid CCDL magic".to_string(), - )); - } - - // Parse chunk descriptors at fixed offsets - let impt = read_chunk_header(data, IMPT_OFFSET as usize)?; - let expt = read_chunk_header(data, EXPT_OFFSET as usize)?; - let mut rawd = read_rawd_header(data, RAWD_OFFSET as usize)?; - - // Validate RAWD - if rawd.entry == 0 { - return Err(SimulatorError::InvalidAppFormat( - "RAW entry point is zero".to_string(), - )); - } - let rawd_end = rawd - .base - .offset - .checked_add(rawd.base.size) - .ok_or_else(|| { - SimulatorError::InvalidAppFormat("RAW payload range overflow".to_string()) - })?; - if rawd_end as usize > data.len() { - return Err(SimulatorError::InvalidAppFormat( - "RAW payload out of bounds".to_string(), - )); - } - if rawd.program_size < rawd.base.size { - log::warn!( - "RAW program_size {} is smaller than payload {}; using payload size", - rawd.program_size, - rawd.base.size - ); - rawd.program_size = rawd.base.size; - } - - // Check for ERPT chunk - let (has_erpt, erpt) = if data.len() >= ERPT_OFFSET as usize + 16 { - let chunk = read_chunk_header(data, ERPT_OFFSET as usize)?; - if chunk.ident == *MAGIC_ERPT { - (true, chunk) - } else { - (false, ChunkHeader::default()) - } - } else { - (false, ChunkHeader::default()) - }; - - // Parse symbol tables - let imports = if impt.size > 0 { - parse_symbol_table(data, impt.offset as usize, impt.size as usize)? - } else { - Vec::new() - }; - - let exports = if expt.size > 0 { - parse_symbol_table(data, expt.offset as usize, expt.size as usize)? - } else { - Vec::new() - }; - - // Parse resources - let resources = parse_resources(data, &rawd, has_erpt, &erpt)?; - - log::info!( - "Parsed .app: entry={:#010x}, base={:#010x}, size={}, imports={}, exports={}, resources={}", - rawd.entry, - rawd.origin, - rawd.program_size, - imports.len(), - exports.len(), - resources.len() - ); - - Ok(Self { - data: data.to_vec(), - impt, - expt, - rawd, - has_erpt, - erpt, - imports, - exports, - resources, - }) - } - - /// Get the executable data (RAWD payload) - pub fn executable(&self) -> &[u8] { - let start = self.rawd.base.offset as usize; - let end = start + self.rawd.base.size as usize; - &self.data[start..end] - } - - /// Get the entry point address - pub fn entry_point(&self) -> u32 { - self.rawd.entry - } - - /// Get the load base address - pub fn load_base(&self) -> u32 { - self.rawd.origin - } - - /// Get program size - pub fn program_size(&self) -> u32 { - self.rawd.program_size - } - - /// Find a resource by name, accepting path separator and basename variants. - pub fn find_resource(&self, name: &str) -> Option<&ResourceEntry> { - let requested = normalize_resource_name(name); - self.resources - .iter() - .find(|r| normalize_resource_name(&r.name) == requested) - .or_else(|| { - let requested_base = resource_basename(&requested); - self.resources.iter().find(|r| { - resource_basename(&normalize_resource_name(&r.name)) == requested_base - }) - }) - } - - /// Get resource data (decoded) - pub fn get_resource_data(&self, resource: &ResourceEntry) -> Vec { - let start = resource.offset as usize; - let end = start + resource.size as usize; - let mut data = self.data[start..end].to_vec(); - - // Apply XOR decoding if needed - if resource.xor_key != 0 { - for b in &mut data { - *b ^= resource.xor_key; - } - } - - data - } -} - -/// Read a 16-byte chunk header -fn read_chunk_header(data: &[u8], offset: usize) -> Result { - if data.len() < offset + 16 { - return Err(SimulatorError::InvalidAppFormat(format!( - "Chunk header at {:#x} out of bounds", - offset - ))); - } - - let ident = [ - data[offset], - data[offset + 1], - data[offset + 2], - data[offset + 3], - ]; - let chunk_type = u32::from_le_bytes([ - data[offset + 4], - data[offset + 5], - data[offset + 6], - data[offset + 7], - ]); - let chunk_offset = u32::from_le_bytes([ - data[offset + 8], - data[offset + 9], - data[offset + 10], - data[offset + 11], - ]); - let size = u32::from_le_bytes([ - data[offset + 12], - data[offset + 13], - data[offset + 14], - data[offset + 15], - ]); - - Ok(ChunkHeader { - ident, - chunk_type, - offset: chunk_offset, - size, - }) -} - -/// Read the RAWD header (extended 32 bytes) -fn read_rawd_header(data: &[u8], offset: usize) -> Result { - if data.len() < offset + 32 { - return Err(SimulatorError::InvalidAppFormat( - "RAW header out of bounds".to_string(), - )); - } - - let base = read_chunk_header(data, offset)?; - - let entry = u32::from_le_bytes([ - data[offset + 20], - data[offset + 21], - data[offset + 22], - data[offset + 23], - ]); - let origin = u32::from_le_bytes([ - data[offset + 24], - data[offset + 25], - data[offset + 26], - data[offset + 27], - ]); - let program_size = u32::from_le_bytes([ - data[offset + 28], - data[offset + 29], - data[offset + 30], - data[offset + 31], - ]); - - Ok(RawdHeader { - base, - entry, - origin, - program_size, - }) -} - -/// Parse a symbol table (IMPT or EXPT) -fn parse_symbol_table(data: &[u8], offset: usize, size: usize) -> Result> { - if size < 16 { - return Ok(Vec::new()); - } - - // Read count (first 4 bytes) - let count = u32::from_le_bytes([ - data[offset], - data[offset + 1], - data[offset + 2], - data[offset + 3], - ]) as usize; - - // Sanity check - if count > 4096 { - log::warn!("Symbol table count {} too large, skipping", count); - return Ok(Vec::new()); - } - - // String table starts after 16-byte header + count * 16-byte entries - let entries_start = offset + 16; - let strings_start = entries_start + count * 16; - - if strings_start > offset + size { - return Err(SimulatorError::InvalidAppFormat( - "Symbol table truncated".to_string(), - )); - } - - let mut symbols = Vec::with_capacity(count); - - for i in 0..count { - let entry_offset = entries_start + i * 16; - - let string_offset = u32::from_le_bytes([ - data[entry_offset], - data[entry_offset + 1], - data[entry_offset + 2], - data[entry_offset + 3], - ]); - let unknown0 = u32::from_le_bytes([ - data[entry_offset + 4], - data[entry_offset + 5], - data[entry_offset + 6], - data[entry_offset + 7], - ]); - let unknown1 = u32::from_le_bytes([ - data[entry_offset + 8], - data[entry_offset + 9], - data[entry_offset + 10], - data[entry_offset + 11], - ]); - let address = u32::from_le_bytes([ - data[entry_offset + 12], - data[entry_offset + 13], - data[entry_offset + 14], - data[entry_offset + 15], - ]); - - // Resolve name from string table - let name = read_cstring(data, strings_start + string_offset as usize, 256); - - symbols.push(SymbolEntry { - string_offset, - unknown0, - unknown1, - address, - name, - }); - } - - Ok(symbols) -} - -/// Parse resource tables (ERPT, packed, packed64) -fn parse_resources( - data: &[u8], - rawd: &RawdHeader, - has_erpt: bool, - erpt: &ChunkHeader, -) -> Result> { - let mut resources = Vec::new(); - - // Try ERPT first - if has_erpt && erpt.size > 0 { - resources = parse_erpt_resources(data, erpt)?; - if !resources.is_empty() { - return Ok(resources); - } - } - - // Try packed resources after RAWD payload, scanning aligned package tables. - let rawd_end = rawd.base.offset + rawd.base.size; - if rawd_end < data.len() as u32 { - resources = parse_packed_resources(data, rawd_end as usize); - if !resources.is_empty() { - return Ok(resources); - } - } - - // Try packed64 resources - if rawd_end < data.len() as u32 { - resources = parse_packed64_resources(data, rawd_end as usize); - } - - Ok(resources) -} - -/// Parse ERPT resources (XOR 0x40 encoded) -fn parse_erpt_resources(data: &[u8], erpt: &ChunkHeader) -> Result> { - const RECORD_SIZE: usize = 0x1FC; - const NAME_SIZE: usize = 0x1F4; - - let offset = erpt.offset as usize; - let size = erpt.size as usize; - - if size < 4 { - return Ok(Vec::new()); - } - if offset > data.len().saturating_sub(4) { - return Ok(Vec::new()); - } - let chunk_end = offset.saturating_add(size).min(data.len()); - let declared_count = u32::from_le_bytes([ - data[offset], - data[offset + 1], - data[offset + 2], - data[offset + 3], - ]) as usize; - let count = declared_count.min((size - 4) / RECORD_SIZE); - - let mut resources = Vec::with_capacity(count); - - for i in 0..count { - let record_offset = offset + 4 + i * RECORD_SIZE; - - if record_offset + RECORD_SIZE > chunk_end { - break; - } - - // Read name (null-terminated) - let name = read_cstring(data, record_offset, NAME_SIZE); - - // Read size and relative offset - let res_size = u32::from_le_bytes([ - data[record_offset + NAME_SIZE], - data[record_offset + NAME_SIZE + 1], - data[record_offset + NAME_SIZE + 2], - data[record_offset + NAME_SIZE + 3], - ]); - let rel_offset = u32::from_le_bytes([ - data[record_offset + NAME_SIZE + 4], - data[record_offset + NAME_SIZE + 5], - data[record_offset + NAME_SIZE + 6], - data[record_offset + NAME_SIZE + 7], - ]); - - // Absolute offset = ERPT chunk offset + relative offset - let abs_offset = erpt.offset + rel_offset; - - resources.push(ResourceEntry { - kind: ResourceKind::Erpt, - name, - offset: abs_offset, - size: res_size, - xor_key: 0x40, - }); - } - - Ok(resources) -} - -/// Parse packed resources (short-name format, 36-byte records) -fn parse_packed_resources(data: &[u8], start: usize) -> Vec { - const MAX_TABLES: usize = 8; - const SCAN_ALIGNMENT: usize = 0x1000; - - let mut table_starts = Vec::new(); - - if is_valid_packed_table(data, start) { - table_starts.push(start); - } - - let mut scan = align_up(start, SCAN_ALIGNMENT); - while scan + 2 < data.len() && table_starts.len() < MAX_TABLES { - if scan != start && is_valid_packed_table(data, scan) { - table_starts.push(scan); - } - scan = scan.saturating_add(SCAN_ALIGNMENT); - } - - let mut resources = Vec::new(); - for (index, &table_start) in table_starts.iter().enumerate() { - let package_end = table_starts.get(index + 1).copied().unwrap_or(data.len()); - parse_packed_table(data, table_start, package_end, &mut resources); - } - - resources -} - -fn align_up(value: usize, alignment: usize) -> usize { - (value + alignment - 1) & !(alignment - 1) -} - -fn is_valid_packed_table(data: &[u8], start: usize) -> bool { - const RECORD_SIZE: usize = 36; - const NAME_SIZE: usize = 32; - const MIN_VALID_SAMPLE: usize = 4; - - if data.len() < start + 2 { - return false; - } - - let count = u16::from_le_bytes([data[start], data[start + 1]]) as usize; - if count == 0 || count > 1024 { - return false; - } - - let table_size = 2 + count * RECORD_SIZE; - if start + table_size > data.len() { - return false; - } - - let sample_count = count.min(32); - let mut valid = 0usize; - let mut last_offset = table_size as u32; - - for i in 0..sample_count { - let record_offset = start + 2 + i * RECORD_SIZE; - let name = read_cstring(data, record_offset, NAME_SIZE); - let rel_offset = read_u32_at(data, record_offset + NAME_SIZE); - - if !name.is_empty() - && name.contains('.') - && is_printable_ascii(&name) - && rel_offset >= table_size as u32 - && start + (rel_offset as usize) < data.len() - && rel_offset >= last_offset - { - valid += 1; - last_offset = rel_offset; - } - } - - valid >= MIN_VALID_SAMPLE && valid * 2 >= sample_count -} - -fn parse_packed_table( - data: &[u8], - start: usize, - package_end: usize, - resources: &mut Vec, -) { - const RECORD_SIZE: usize = 36; - const NAME_SIZE: usize = 32; - - let count = u16::from_le_bytes([data[start], data[start + 1]]) as usize; - let table_size = 2 + count * RECORD_SIZE; - let mut entries = Vec::new(); - - for i in 0..count { - let record_offset = start + 2 + i * RECORD_SIZE; - let name = read_cstring(data, record_offset, NAME_SIZE); - let rel_offset = read_u32_at(data, record_offset + NAME_SIZE) as usize; - - if name.is_empty() - || !name.contains('.') - || !is_printable_ascii(&name) - || rel_offset < table_size - || start + rel_offset >= package_end - { - continue; - } - - entries.push((start + rel_offset, name)); - } - - entries.sort_by_key(|(offset, _)| *offset); - entries.dedup_by(|a, b| { - a.0 == b.0 && normalize_resource_name(&a.1) == normalize_resource_name(&b.1) - }); - - for i in 0..entries.len() { - let (offset, name) = &entries[i]; - let next_offset = entries - .iter() - .skip(i + 1) - .map(|(next, _)| *next) - .find(|next| next > offset) - .unwrap_or(package_end); - - if next_offset > *offset { - resources.push(ResourceEntry { - kind: ResourceKind::Packed, - name: name.clone(), - offset: *offset as u32, - size: (next_offset - *offset) as u32, - xor_key: 0, - }); - } - } -} - -fn read_u32_at(data: &[u8], offset: usize) -> u32 { - u32::from_le_bytes([ - data[offset], - data[offset + 1], - data[offset + 2], - data[offset + 3], - ]) -} -/// Parse packed64 resources (long-path format, 68-byte records) -fn parse_packed64_resources(data: &[u8], start: usize) -> Vec { - const RECORD_SIZE: usize = 0x44; - const NAME_SIZE: usize = 0x40; - - let mut resources = Vec::new(); - let mut offset = start; - - // Read records until invalid entry - let mut prev_offset = 0u32; - let mut first_stored_offset = None; - - loop { - if offset + RECORD_SIZE > data.len() { - break; - } - - let stored_offset = u32::from_le_bytes([ - data[offset], - data[offset + 1], - data[offset + 2], - data[offset + 3], - ]); - - // Read name (null-terminated) - let name = read_cstring(data, offset + 4, NAME_SIZE); - - // Validate: name should start with ".\" and contain a dot - if !name.starts_with(".\\") || !name.contains('.') || !is_printable_ascii(&name) { - break; - } - - // Validate offset is monotonically non-decreasing - if stored_offset < prev_offset && first_stored_offset.is_some() { - break; - } - - if first_stored_offset.is_none() { - first_stored_offset = Some(stored_offset); - } - - prev_offset = stored_offset; - offset += RECORD_SIZE; - } - - // Calculate data bias (first stored offset - table end) - if let Some(first_offset) = first_stored_offset { - let table_end = offset as u32; - let data_bias = first_offset - table_end; - - // Re-parse with correct offsets - offset = start; - let mut prev = 0u32; - let mut stored_offsets = Vec::new(); - - loop { - if offset + RECORD_SIZE > data.len() { - break; - } - - let stored = u32::from_le_bytes([ - data[offset], - data[offset + 1], - data[offset + 2], - data[offset + 3], - ]); - let name = read_cstring(data, offset + 4, NAME_SIZE); - - if !name.starts_with(".\\") || !name.contains('.') || !is_printable_ascii(&name) { - break; - } - - if stored < prev && !stored_offsets.is_empty() { - break; - } - - stored_offsets.push((stored, name)); - prev = stored; - offset += RECORD_SIZE; - } - - // Convert to ResourceEntry - for i in 0..stored_offsets.len() { - let (stored, name) = &stored_offsets[i]; - let abs_offset = stored - data_bias; - - let size = if i + 1 < stored_offsets.len() { - stored_offsets[i + 1].0 - data_bias - abs_offset - } else { - // Approximate size - (data.len() as u32) - abs_offset - }; - - resources.push(ResourceEntry { - kind: ResourceKind::Packed64, - name: name.clone(), - offset: abs_offset, - size, - xor_key: 0, - }); - } - } - - resources -} - -/// Read a null-terminated C string -fn read_cstring(data: &[u8], offset: usize, max_len: usize) -> String { - let mut s = String::new(); - for i in 0..max_len { - if offset + i >= data.len() { - break; - } - let b = data[offset + i]; - if b == 0 { - break; - } - s.push(b as char); - } - s -} - -fn normalize_resource_name(name: &str) -> String { - let mut s = name.replace('/', "\\").to_ascii_lowercase(); - while let Some(stripped) = s.strip_prefix(".\\") { - s = stripped.to_string(); - } - while let Some(stripped) = s.strip_prefix('\\') { - s = stripped.to_string(); - } - s -} - -fn resource_basename(name: &str) -> &str { - name.rsplit(['\\', '/', ':']).next().unwrap_or(name) -} -/// Check if a string is printable ASCII -fn is_printable_ascii(s: &str) -> bool { - s.bytes() - .all(|b| (0x20..0x7F).contains(&b) || b == b'\t' || b == b'\n' || b == b'\r') -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_read_chunk_header() { - let mut data = vec![0u8; 64]; - // Write IMPT header at offset 32 - data[32..36].copy_from_slice(b"IMPT"); - data[36..40].copy_from_slice(&1u32.to_le_bytes()); - data[40..44].copy_from_slice(&0x1000u32.to_le_bytes()); - data[44..48].copy_from_slice(&0x200u32.to_le_bytes()); - - let header = read_chunk_header(&data, 32).unwrap(); - assert_eq!(header.ident, *MAGIC_IMPT); - assert_eq!(header.chunk_type, 1); - assert_eq!(header.offset, 0x1000); - assert_eq!(header.size, 0x200); - } - - #[test] - fn test_read_rawd_header() { - let mut data = vec![0u8; 128]; - // Write RAWD header at offset 96 - data[96..100].copy_from_slice(b"RAWD"); - data[116..120].copy_from_slice(&0x8000_0000u32.to_le_bytes()); // entry - data[120..124].copy_from_slice(&0x8000_0000u32.to_le_bytes()); // origin - data[124..128].copy_from_slice(&0x1000u32.to_le_bytes()); // program_size - - let header = read_rawd_header(&data, 96).unwrap(); - assert_eq!(header.base.ident, *MAGIC_RAWD); - assert_eq!(header.entry, 0x8000_0000); - assert_eq!(header.origin, 0x8000_0000); - assert_eq!(header.program_size, 0x1000); - } - - #[test] - fn test_parse_uses_raw_payload_size_when_program_size_is_smaller() { - let mut data = vec![0u8; 256]; - data[0..4].copy_from_slice(b"CCDL"); - data[0x20..0x24].copy_from_slice(b"IMPT"); - data[0x40..0x44].copy_from_slice(b"EXPT"); - data[0x60..0x64].copy_from_slice(b"RAWD"); - data[0x68..0x6C].copy_from_slice(&0x80u32.to_le_bytes()); - data[0x6C..0x70].copy_from_slice(&0x20u32.to_le_bytes()); - data[0x74..0x78].copy_from_slice(&0x8000_0000u32.to_le_bytes()); - data[0x78..0x7C].copy_from_slice(&0x8000_0000u32.to_le_bytes()); - data[0x7C..0x80].copy_from_slice(&0x18u32.to_le_bytes()); - - let app = AppImage::parse(&data).unwrap(); - - assert_eq!(app.program_size(), 0x20); - assert_eq!(app.executable().len(), 0x20); - } - - #[test] - fn test_read_cstring() { - let data = b"Hello\0World\0"; - assert_eq!(read_cstring(data, 0, 100), "Hello"); - assert_eq!(read_cstring(data, 6, 100), "World"); - } - - #[test] - fn test_is_printable_ascii() { - assert!(is_printable_ascii("Hello")); - assert!(is_printable_ascii("Test 123")); - assert!(is_printable_ascii("Hello\nWorld")); // Newline is allowed - assert!(is_printable_ascii("Hello\tWorld")); // Tab is allowed - assert!(is_printable_ascii("Hello\r\nWorld")); // CR+LF is allowed - } - - #[test] - fn test_erpt_out_of_file_range_is_ignored() { - let data = vec![0u8; 128]; - let erpt = ChunkHeader { - offset: data.len() as u32, - size: 64, - ..ChunkHeader::default() - }; - - let resources = parse_erpt_resources(&data, &erpt).unwrap(); - - assert!(resources.is_empty()); - } - - #[test] - fn test_erpt_size_is_clamped_to_file() { - const RECORD_SIZE: usize = 0x1FC; - let mut data = vec![0u8; 4 + RECORD_SIZE]; - data[0..4].copy_from_slice(&1u32.to_le_bytes()); - data[4..14].copy_from_slice(b"asset.bin\0"); - let erpt = ChunkHeader { - offset: 0, - size: (data.len() + 128) as u32, - ..ChunkHeader::default() - }; - - let resources = parse_erpt_resources(&data, &erpt).unwrap(); - - assert_eq!(resources.len(), 1); - assert_eq!(resources[0].name, "asset.bin"); - } -} diff --git a/native/DingooEmu/crates/dingooemu-core/src/audio.rs b/native/DingooEmu/crates/dingooemu-core/src/audio.rs deleted file mode 100644 index f128685..0000000 --- a/native/DingooEmu/crates/dingooemu-core/src/audio.rs +++ /dev/null @@ -1,518 +0,0 @@ -//! Dingoo PCM audio handling. - -#[cfg(not(feature = "standalone"))] -use std::collections::VecDeque; -#[cfg(feature = "standalone")] -use std::num::NonZero; - -pub const OUTPUT_SAMPLE_RATE: u32 = 22_050; - -#[cfg(not(feature = "standalone"))] -const VIDEO_FRAMES_PER_SECOND: u32 = 60; -#[cfg(not(feature = "standalone"))] -const MAX_QUEUED_AUDIO_FRAMES: usize = OUTPUT_SAMPLE_RATE as usize / 2; -#[cfg(feature = "standalone")] -const MAX_QUEUED_AUDIO_BUFFERS: usize = 4; - -#[cfg(feature = "standalone")] -fn host_output_enabled_default() -> bool { - true -} - -#[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)] -pub enum SampleFormat { - U8, - S16Le, -} - -impl SampleFormat { - pub fn from_sdk_value(value: u16) -> Option { - match value { - 8 => Some(Self::U8), - 16 => Some(Self::S16Le), - _ => None, - } - } -} - -#[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)] -pub struct AudioConfig { - pub sample_rate: u32, - pub format: SampleFormat, - pub channels: u8, - pub volume: u8, -} - -impl AudioConfig { - pub fn new(sample_rate: u32, format: u16, channels: u8, volume: u8) -> Option { - if sample_rate == 0 || !(1..=2).contains(&channels) { - return None; - } - Some(Self { - sample_rate, - format: SampleFormat::from_sdk_value(format)?, - channels, - volume, - }) - } -} - -#[derive(serde::Serialize, serde::Deserialize)] -pub struct Audio { - config: Option, - volume: u8, - master_volume: u8, - muted: bool, - #[cfg(feature = "standalone")] - #[serde(skip)] - mixer_device: Option, - #[cfg(feature = "standalone")] - #[serde(skip)] - mixer: Option, - #[cfg(feature = "standalone")] - #[serde(skip)] - player: Option, - #[cfg(feature = "standalone")] - #[serde(skip, default = "host_output_enabled_default")] - host_output_enabled: bool, - #[cfg(feature = "standalone")] - #[serde(skip)] - virtual_buffered_frame_units: u64, - #[cfg(not(feature = "standalone"))] - pending_samples: VecDeque, - #[cfg(not(feature = "standalone"))] - output_frame_remainder: u32, - #[cfg(not(feature = "standalone"))] - resampler: StreamingResampler, -} - -impl Audio { - pub fn new() -> Self { - Self { - config: None, - volume: 100, - master_volume: 100, - muted: false, - #[cfg(feature = "standalone")] - mixer_device: None, - #[cfg(feature = "standalone")] - mixer: None, - #[cfg(feature = "standalone")] - player: None, - #[cfg(feature = "standalone")] - host_output_enabled: true, - #[cfg(feature = "standalone")] - virtual_buffered_frame_units: 0, - #[cfg(not(feature = "standalone"))] - pending_samples: VecDeque::new(), - #[cfg(not(feature = "standalone"))] - output_frame_remainder: 0, - #[cfg(not(feature = "standalone"))] - resampler: StreamingResampler::default(), - } - } - - pub fn open(&mut self, config: AudioConfig) -> bool { - self.close(); - self.volume = config.volume; - self.config = Some(config); - #[cfg(feature = "standalone")] - { - self.virtual_buffered_frame_units = 0; - } - - #[cfg(feature = "standalone")] - { - if self.host_output_enabled && self.mixer_device.is_none() { - match rodio::DeviceSinkBuilder::open_default_sink() { - Ok(mut device) => { - device.log_on_drop(false); - self.mixer = Some(device.mixer().clone()); - self.mixer_device = Some(device); - } - Err(error) => { - log::warn!("Failed to initialize audio output: {error}"); - } - } - } - - if self.host_output_enabled { - if let Some(mixer) = self.mixer.as_ref() { - let player = rodio::Player::connect_new(mixer); - player.set_volume(self.effective_volume()); - self.player = Some(player); - } - } - } - - #[cfg(not(feature = "standalone"))] - self.resampler.reset(config.sample_rate); - - log::info!( - "Audio opened: {} Hz, {:?}, {} channel(s), volume {}", - config.sample_rate, - config.format, - config.channels, - config.volume - ); - true - } - - pub fn close(&mut self) -> bool { - #[cfg(feature = "standalone")] - { - if let Some(player) = self.player.take() { - player.stop(); - } - self.virtual_buffered_frame_units = 0; - } - - #[cfg(not(feature = "standalone"))] - { - self.pending_samples.clear(); - self.output_frame_remainder = 0; - self.resampler = StreamingResampler::default(); - } - - self.config = None; - true - } - - pub fn can_write(&self) -> bool { - if self.config.is_none() || self.muted || self.volume == 0 { - return true; - } - - #[cfg(feature = "standalone")] - { - if self.host_output_enabled { - self.player - .as_ref() - .is_none_or(|player| player.len() < MAX_QUEUED_AUDIO_BUFFERS) - } else { - self.config.is_some_and(|config| { - self.virtual_buffered_frame_units < u64::from(config.sample_rate) * 30 - }) - } - } - - #[cfg(not(feature = "standalone"))] - { - self.pending_samples.len() / 2 < MAX_QUEUED_AUDIO_FRAMES - } - } - - pub fn write(&mut self, data: &[u8]) -> bool { - let Some(config) = self.config else { - return false; - }; - if data.is_empty() { - return false; - } - if self.muted || self.volume == 0 { - return true; - } - if !self.can_write() { - return false; - } - - let samples = decode_pcm(data, config.format, config.channels); - if samples.is_empty() { - return false; - } - let peak = samples - .iter() - .fold(0.0f32, |peak, sample| peak.max(sample.abs())); - - #[cfg(feature = "standalone")] - { - if !self.host_output_enabled { - let frames = (samples.len() / config.channels as usize) as u64; - self.virtual_buffered_frame_units = self - .virtual_buffered_frame_units - .saturating_add(frames.saturating_mul(60)); - return true; - } - let Some(player) = self.player.as_ref() else { - return true; - }; - let channels = NonZero::::new(config.channels as u16).unwrap(); - let sample_rate = NonZero::::new(config.sample_rate).unwrap(); - player.append(rodio::buffer::SamplesBuffer::new( - channels, - sample_rate, - samples, - )); - } - - #[cfg(not(feature = "standalone"))] - self.resampler.push( - &samples, - config.channels as usize, - self.effective_volume(), - &mut self.pending_samples, - ); - - log::trace!( - "Queued {} bytes of guest PCM audio (peak {peak:.3})", - data.len() - ); - true - } - - pub fn set_volume(&mut self, volume: u32) -> bool { - self.volume = volume.min(u8::MAX as u32) as u8; - #[cfg(feature = "standalone")] - if let Some(player) = self.player.as_ref() { - player.set_volume(self.effective_volume()); - } - true - } - - pub fn set_master_volume(&mut self, volume: u8) { - self.master_volume = volume.min(100); - #[cfg(feature = "standalone")] - if let Some(player) = self.player.as_ref() { - player.set_volume(self.effective_volume()); - } - } - - pub fn master_volume(&self) -> u8 { - self.master_volume - } - - #[cfg(feature = "standalone")] - pub fn set_host_output_enabled(&mut self, enabled: bool) { - self.host_output_enabled = enabled; - if !enabled { - if let Some(player) = self.player.take() { - player.stop(); - } - self.mixer = None; - self.mixer_device = None; - self.virtual_buffered_frame_units = 0; - } - } - - #[cfg(feature = "standalone")] - pub fn host_output_enabled(&self) -> bool { - self.host_output_enabled - } - - pub fn set_muted(&mut self, muted: bool) -> bool { - self.muted = muted; - #[cfg(feature = "standalone")] - if let Some(player) = self.player.as_ref() { - player.set_volume(self.effective_volume()); - } - #[cfg(not(feature = "standalone"))] - if muted { - self.pending_samples.clear(); - } - true - } - - pub fn take_frame_samples(&mut self) -> Vec { - #[cfg(feature = "standalone")] - { - Vec::new() - } - - #[cfg(not(feature = "standalone"))] - { - self.output_frame_remainder += OUTPUT_SAMPLE_RATE; - let frame_count = (self.output_frame_remainder / VIDEO_FRAMES_PER_SECOND) as usize; - self.output_frame_remainder %= VIDEO_FRAMES_PER_SECOND; - - let mut output = Vec::with_capacity(frame_count * 2); - for _ in 0..frame_count * 2 { - output.push(self.pending_samples.pop_front().unwrap_or(0)); - } - output - } - } - - pub fn config(&self) -> Option { - self.config - } - - pub fn advance_frame(&mut self) { - #[cfg(feature = "standalone")] - if !self.host_output_enabled { - if let Some(config) = self.config { - self.virtual_buffered_frame_units = self - .virtual_buffered_frame_units - .saturating_sub(u64::from(config.sample_rate)); - } - } - } - - pub(crate) fn resume_after_state_load(&mut self) { - #[cfg(feature = "standalone")] - if let Some(config) = self.config.take() { - let volume = self.volume; - let master_volume = self.master_volume; - let muted = self.muted; - self.open(config); - self.set_volume(volume as u32); - self.set_master_volume(master_volume); - self.set_muted(muted); - } - } - - fn effective_volume(&self) -> f32 { - if self.muted { - return 0.0; - } - if self.volume <= 100 { - self.volume as f32 / 100.0 * self.master_volume as f32 / 100.0 - } else { - self.volume as f32 / 255.0 * self.master_volume as f32 / 100.0 - } - } -} - -impl Default for Audio { - fn default() -> Self { - Self::new() - } -} - -fn decode_pcm(data: &[u8], format: SampleFormat, channels: u8) -> Vec { - let mut samples = match format { - SampleFormat::U8 => data - .iter() - .map(|&sample| (sample as f32 - 128.0) / 128.0) - .collect::>(), - SampleFormat::S16Le => data - .as_chunks::<2>() - .0 - .iter() - .map(|sample| i16::from_le_bytes(*sample) as f32 / 32768.0) - .collect::>(), - }; - samples.truncate(samples.len() / channels as usize * channels as usize); - samples -} - -#[cfg(not(feature = "standalone"))] -#[derive(Default, serde::Serialize, serde::Deserialize)] -struct StreamingResampler { - input_rate: u32, - input_frames_seen: u64, - next_output_time: u64, - previous_frame: Option<[f32; 2]>, -} - -#[cfg(not(feature = "standalone"))] -impl StreamingResampler { - fn reset(&mut self, input_rate: u32) { - self.input_rate = input_rate; - self.input_frames_seen = 0; - self.next_output_time = 0; - self.previous_frame = None; - } - - fn push(&mut self, samples: &[f32], channels: usize, volume: f32, output: &mut VecDeque) { - for input in samples.chunks_exact(channels) { - let current = [input[0], input[1.min(channels - 1)]]; - let current_index = self.input_frames_seen; - - if let Some(previous) = self.previous_frame { - let segment_start = (current_index - 1) * OUTPUT_SAMPLE_RATE as u64; - let segment_end = current_index * OUTPUT_SAMPLE_RATE as u64; - while self.next_output_time <= segment_end { - let fraction = - (self.next_output_time - segment_start) as f32 / OUTPUT_SAMPLE_RATE as f32; - let left = previous[0] + (current[0] - previous[0]) * fraction; - let right = previous[1] + (current[1] - previous[1]) * fraction; - output.push_back(float_to_i16(left * volume)); - output.push_back(float_to_i16(right * volume)); - self.next_output_time += self.input_rate as u64; - } - } else { - output.push_back(float_to_i16(current[0] * volume)); - output.push_back(float_to_i16(current[1] * volume)); - self.next_output_time = self.input_rate as u64; - } - - self.previous_frame = Some(current); - self.input_frames_seen += 1; - } - } -} - -#[cfg(not(feature = "standalone"))] -fn float_to_i16(sample: f32) -> i16 { - (sample * 32767.0).clamp(i16::MIN as f32, i16::MAX as f32) as i16 -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn decodes_s16le_mono_pcm() { - let samples = decode_pcm(&[0x00, 0x80, 0x00, 0x40], SampleFormat::S16Le, 1); - - assert_eq!(samples, vec![-1.0, 0.5]); - } - - #[test] - fn drops_incomplete_pcm_frames() { - let samples = decode_pcm(&[0, 128, 255], SampleFormat::U8, 2); - - assert_eq!(samples.len(), 2); - } - - #[test] - fn master_volume_is_clamped_to_percent_range() { - let mut audio = Audio::new(); - audio.set_master_volume(35); - assert_eq!(audio.master_volume(), 35); - audio.set_master_volume(255); - assert_eq!(audio.master_volume(), 100); - } - - #[cfg(feature = "standalone")] - #[test] - fn disabled_host_output_uses_the_emulated_frame_clock_for_backpressure() { - let mut audio = Audio::new(); - audio.set_host_output_enabled(false); - assert!(!audio.host_output_enabled()); - assert!(audio.open(AudioConfig::new(8_000, 16, 1, 100).unwrap())); - - assert!(audio.write(&vec![0; 8_000])); - assert!(!audio.can_write()); - audio.advance_frame(); - assert!(audio.can_write()); - } - - #[cfg(not(feature = "standalone"))] - #[test] - fn resamples_mono_audio_for_one_libretro_frame() { - let mut audio = Audio::new(); - let config = AudioConfig::new(16_000, 16, 1, 100).unwrap(); - assert!(audio.open(config)); - - let mut pcm = Vec::new(); - for sample in 0..1_600 { - let value = if sample % 2 == 0 { - 10_000i16 - } else { - -10_000i16 - }; - pcm.extend_from_slice(&value.to_le_bytes()); - } - assert!(audio.write(&pcm)); - - let output = audio.take_frame_samples(); - assert_eq!(output.len(), (OUTPUT_SAMPLE_RATE as usize / 60) * 2); - assert!(output.iter().any(|&sample| sample != 0)); - assert!(output - .as_chunks::<2>() - .0 - .iter() - .all(|frame| frame[0] == frame[1])); - } -} diff --git a/native/DingooEmu/crates/dingooemu-core/src/cheats.rs b/native/DingooEmu/crates/dingooemu-core/src/cheats.rs deleted file mode 100644 index ef5f55b..0000000 --- a/native/DingooEmu/crates/dingooemu-core/src/cheats.rs +++ /dev/null @@ -1,289 +0,0 @@ -use std::collections::BTreeMap; -use std::str::FromStr; - -use thiserror::Error; - -use crate::cpu::Cpu; -use crate::memory::Memory; - -#[derive(Debug, Error, PartialEq, Eq)] -pub enum CheatParseError { - #[error("cheat code is empty")] - Empty, - #[error("cheat code must use '=' syntax")] - MissingValue, - #[error("unknown cheat target '{0}'")] - UnknownTarget(String), - #[error("invalid numeric value '{0}'")] - InvalidNumber(String), - #[error("{0}-bit cheat value is out of range")] - ValueOutOfRange(u32), - #[error("{0}-bit memory address 0x{1:08X} is not aligned")] - MisalignedAddress(u32, u32), - #[error("memory range 0x{address:08X}..0x{end:08X} is not writable RAM or framebuffer")] - InvalidMemoryRange { address: u32, end: u32 }, - #[error("unknown MIPS register '{0}'")] - InvalidRegister(String), -} - -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub enum MemoryWidth { - U8, - U16, - U32, -} - -impl MemoryWidth { - const fn bytes(self) -> u32 { - match self { - Self::U8 => 1, - Self::U16 => 2, - Self::U32 => 4, - } - } - - const fn bits(self) -> u32 { - self.bytes() * 8 - } - - const fn max_value(self) -> u64 { - match self { - Self::U8 => u8::MAX as u64, - Self::U16 => u16::MAX as u64, - Self::U32 => u32::MAX as u64, - } - } -} - -#[derive(Clone, Debug, PartialEq, Eq)] -pub enum CheatRule { - Memory { - width: MemoryWidth, - address: u32, - value: u32, - }, - Register { - index: usize, - value: u32, - }, -} - -impl CheatRule { - fn validate(&self, memory: &Memory) -> Result<(), CheatParseError> { - let Self::Memory { width, address, .. } = self else { - return Ok(()); - }; - let bytes = width.bytes(); - if address % bytes != 0 { - return Err(CheatParseError::MisalignedAddress(width.bits(), *address)); - } - if !memory.is_cheat_writable_range(*address, bytes as usize) { - return Err(CheatParseError::InvalidMemoryRange { - address: *address, - end: address.saturating_add(bytes - 1), - }); - } - Ok(()) - } - - fn apply(&self, memory: &mut Memory, cpu: &mut Cpu) { - let result = match *self { - Self::Memory { - width, - address, - value, - } => match width { - MemoryWidth::U8 => memory.write_u8(address, value as u8), - MemoryWidth::U16 => memory.write_u16(address, value as u16), - MemoryWidth::U32 => memory.write_u32(address, value), - }, - Self::Register { index, value } => { - cpu.regs.write(index, value); - Ok(()) - } - }; - if let Err(error) = result { - log::warn!("Failed to apply cheat: {error}"); - } - } -} - -impl FromStr for CheatRule { - type Err = CheatParseError; - - fn from_str(input: &str) -> Result { - let input = input.trim(); - if input.is_empty() { - return Err(CheatParseError::Empty); - } - let (target, value) = input.split_once('=').ok_or(CheatParseError::MissingValue)?; - let target = target.trim(); - let value = parse_number(value)?; - let target_lower = target.to_ascii_lowercase(); - - for (prefix, width) in [ - ("mem8:", MemoryWidth::U8), - ("mem16:", MemoryWidth::U16), - ("mem32:", MemoryWidth::U32), - ] { - if let Some(address) = target_lower.strip_prefix(prefix) { - if value > width.max_value() { - return Err(CheatParseError::ValueOutOfRange(width.bits())); - } - return Ok(Self::Memory { - width, - address: parse_u32(address)?, - value: value as u32, - }); - } - } - - if let Some(register) = target_lower.strip_prefix("reg:") { - let register = register.trim().trim_start_matches('r'); - let index = register - .parse::() - .ok() - .filter(|index| *index <= 31) - .ok_or_else(|| CheatParseError::InvalidRegister(register.to_string()))?; - return Ok(Self::Register { - index, - value: u32::try_from(value).map_err(|_| CheatParseError::ValueOutOfRange(32))?, - }); - } - - Err(CheatParseError::UnknownTarget(target.to_string())) - } -} - -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct CheatSlot { - pub enabled: bool, - pub code: String, - pub rule: CheatRule, -} - -#[derive(Clone, Debug, Default, PartialEq, Eq)] -pub struct CheatManager { - slots: BTreeMap, -} - -impl CheatManager { - pub fn clear(&mut self) { - self.slots.clear(); - } - - pub fn set_slot( - &mut self, - index: u32, - enabled: bool, - code: &str, - memory: &Memory, - ) -> Result<(), CheatParseError> { - let code = code.trim(); - if code.is_empty() { - self.slots.remove(&index); - return Ok(()); - } - let rule = CheatRule::from_str(code)?; - self.set_rule(index, enabled, code.to_string(), rule, memory) - } - - pub fn set_parsed_rule( - &mut self, - index: u32, - enabled: bool, - rule: CheatRule, - memory: &Memory, - ) -> Result<(), CheatParseError> { - self.set_rule(index, enabled, String::new(), rule, memory) - } - - fn set_rule( - &mut self, - index: u32, - enabled: bool, - code: String, - rule: CheatRule, - memory: &Memory, - ) -> Result<(), CheatParseError> { - rule.validate(memory)?; - self.slots.insert( - index, - CheatSlot { - enabled, - code, - rule, - }, - ); - Ok(()) - } - - pub fn get_slot(&self, index: u32) -> Option<&CheatSlot> { - self.slots.get(&index) - } - - pub fn apply(&self, memory: &mut Memory, cpu: &mut Cpu) { - for slot in self.slots.values().filter(|slot| slot.enabled) { - slot.rule.apply(memory, cpu); - } - } -} - -fn parse_number(input: &str) -> Result { - let input = input.trim(); - if input.is_empty() { - return Err(CheatParseError::InvalidNumber(input.to_string())); - } - input - .strip_prefix("0x") - .or_else(|| input.strip_prefix("0X")) - .map_or_else(|| input.parse::(), |hex| u64::from_str_radix(hex, 16)) - .map_err(|_| CheatParseError::InvalidNumber(input.to_string())) -} - -fn parse_u32(input: &str) -> Result { - u32::try_from(parse_number(input)?) - .map_err(|_| CheatParseError::InvalidNumber(input.trim().to_string())) -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn parses_and_validates_rules() { - assert_eq!( - "mem16:0x1234=99".parse(), - Ok(CheatRule::Memory { - width: MemoryWidth::U16, - address: 0x1234, - value: 99, - }) - ); - assert_eq!( - "reg:r31=0x80001000".parse(), - Ok(CheatRule::Register { - index: 31, - value: 0x8000_1000, - }) - ); - assert!(matches!( - "mem8:0x100=256".parse::(), - Err(CheatParseError::ValueOutOfRange(8)) - )); - } - - #[test] - fn applies_only_enabled_slots() { - let mut memory = Memory::new(); - let mut cpu = Cpu::new(0); - let mut cheats = CheatManager::default(); - cheats - .set_slot(0, true, "mem32:0x100=0x12345678", &memory) - .unwrap(); - cheats.set_slot(1, false, "reg:r4=7", &memory).unwrap(); - cheats.apply(&mut memory, &mut cpu); - assert_eq!(memory.read_u32(0x100).unwrap(), 0x1234_5678); - assert_eq!(cpu.regs.read(4), 0); - } -} diff --git a/native/DingooEmu/crates/dingooemu-core/src/cpu.rs b/native/DingooEmu/crates/dingooemu-core/src/cpu.rs deleted file mode 100644 index 80de41e..0000000 --- a/native/DingooEmu/crates/dingooemu-core/src/cpu.rs +++ /dev/null @@ -1,1059 +0,0 @@ -use crate::error::{Result, SimulatorError}; -use crate::memory::Memory; - -#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)] -pub enum UnknownInstructionPolicy { - Stop, - #[default] - Skip, -} - -/// MIPS32 register file -#[repr(C)] -#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] -pub struct Registers { - /// General purpose registers (R0-R31) - /// R0 is hardwired to zero - pub gpr: [u32; 32], - /// Program counter - pub pc: u32, - /// Hi register (multiply/divide results) - pub hi: u32, - /// Lo register (multiply/divide results) - pub lo: u32, -} - -impl Registers { - /// Create new registers with PC at the specified address - pub fn new(entry_point: u32) -> Self { - let mut regs = Self { - gpr: [0; 32], - pc: entry_point, - hi: 0, - lo: 0, - }; - // R0 is always zero - regs.gpr[0] = 0; - regs - } - - /// Read a register (R0 always returns 0) - pub fn read(&self, reg: usize) -> u32 { - if reg == 0 { - 0 - } else { - self.gpr[reg] - } - } - - /// Write a register (writes to R0 are ignored) - pub fn write(&mut self, reg: usize, value: u32) { - if reg != 0 { - self.gpr[reg] = value; - } - } -} - -/// MIPS32 CPU for Dingoo A320 (Ingenic JZ4740 XBurst) -#[derive(Clone, serde::Serialize, serde::Deserialize)] -pub struct Cpu { - /// Register file - pub regs: Registers, - /// Instruction count (for debugging/profiling) - pub instruction_count: u64, - /// Running state - running: bool, - /// Branch delay slot: true if we need to apply branch target after this instruction - pub branch_delay: bool, - /// Branch target address (set when branch is taken) - pub branch_target: u32, - /// Whether branch_delay was set in the CURRENT step (not from previous) - branch_delay_pending: bool, - unknown_instruction_policy: UnknownInstructionPolicy, -} - -impl Cpu { - /// Create a new CPU with the specified entry point - pub fn new(entry_point: u32) -> Self { - Self { - regs: Registers::new(entry_point), - instruction_count: 0, - running: false, - branch_delay: false, - branch_target: 0, - branch_delay_pending: false, - unknown_instruction_policy: UnknownInstructionPolicy::default(), - } - } - - /// Start the CPU - pub fn start(&mut self) { - self.running = true; - } - - /// Stop the CPU - pub fn stop(&mut self) { - self.running = false; - } - - /// Check if CPU is running - pub fn is_running(&self) -> bool { - self.running - } - - pub fn set_unknown_instruction_policy(&mut self, policy: UnknownInstructionPolicy) { - self.unknown_instruction_policy = policy; - } - - fn handle_unknown_instruction(&self, instr: u32) -> Result<()> { - let pc = self.regs.pc.wrapping_sub(4); - match self.unknown_instruction_policy { - UnknownInstructionPolicy::Stop => Err(SimulatorError::InvalidInstruction { pc, instr }), - UnknownInstructionPolicy::Skip => { - log::warn!("Skipping unimplemented instruction {instr:#010x} at PC={pc:#010x}"); - Ok(()) - } - } - } - - /// Execute one instruction - pub fn step(&mut self, memory: &mut Memory) -> Result<()> { - if !self.running { - return Ok(()); - } - - let instr = memory.fetch_instruction(self.regs.pc)?; - self.step_fetched(instr, memory) - } - - /// Execute an instruction that was fetched by the runtime. - pub(crate) fn step_fetched(&mut self, instr: u32, memory: &mut Memory) -> Result<()> { - if self.step_fetched_unaccounted(instr, memory)? { - self.instruction_count += 1; - } - Ok(()) - } - - /// Execute a fetched instruction without updating the profiling counter. - pub(crate) fn step_fetched_unaccounted( - &mut self, - instr: u32, - memory: &mut Memory, - ) -> Result { - if !self.running { - return Ok(false); - } - // If we have a pending branch (from previous instruction), - // the delay slot is the NEXT instruction to execute - if self.branch_delay { - // The delay slot is at PC (which is already pointing to the delay slot) - // Execute it first - self.regs.pc = self.regs.pc.wrapping_add(4); - self.execute_instruction(instr, memory)?; - - // After delay slot executes, apply the branch target - self.branch_delay = false; - self.regs.pc = self.branch_target; - self.regs.gpr[0] = 0; - return Ok(true); - } - - // Normal instruction execution - self.regs.pc = self.regs.pc.wrapping_add(4); - self.execute_instruction(instr, memory)?; - - // R0 is always zero - self.regs.gpr[0] = 0; - - Ok(true) - } - - pub(crate) fn account_instructions(&mut self, count: u64) { - self.instruction_count += count; - } - - /// Take a branch (sets delay slot) - fn branch(&mut self, offset: i32) { - // Calculate branch target: PC + (offset << 2) - // Note: PC has already been incremented by 4 in step() - let target = self.regs.pc.wrapping_add((offset << 2) as u32); - self.branch_delay = true; - self.branch_target = target; - self.branch_delay_pending = true; // Mark that this branch was set in current step - } - - /// Execute a single instruction - fn execute_instruction(&mut self, instr: u32, memory: &mut Memory) -> Result<()> { - if instr == 0 { - return Ok(()); - } - // Extract opcode (bits 31-26) - let opcode = (instr >> 26) & 0x3F; - - match opcode { - 0x00 => self.execute_special(instr, memory), // R-type - 0x01 => self.execute_regimm(instr), // REGIMM - 0x02 => self.execute_j(instr), // J - 0x03 => self.execute_jal(instr), // JAL - 0x04 => self.execute_beq(instr), // BEQ - 0x05 => self.execute_bne(instr), // BNE - 0x06 => self.execute_blez(instr), // BLEZ - 0x07 => self.execute_bgtz(instr), // BGTZ - 0x08 => self.execute_addi(instr), // ADDI - 0x09 => self.execute_addiu(instr), // ADDIU - 0x0A => self.execute_slti(instr), // SLTI - 0x0B => self.execute_sltiu(instr), // SLTIU - 0x0C => self.execute_andi(instr), // ANDI - 0x0D => self.execute_ori(instr), // ORI - 0x0E => self.execute_xori(instr), // XORI - 0x0F => self.execute_lui(instr), // LUI - 0x1C => self.execute_special2(instr, memory), // SPECIAL2 - 0x20 => self.execute_lb(instr, memory), // LB - 0x21 => self.execute_lh(instr, memory), // LH - 0x22 => self.execute_lwl(instr, memory), // LWL - 0x23 => self.execute_lw(instr, memory), // LW - 0x24 => self.execute_lbu(instr, memory), // LBU - 0x25 => self.execute_lhu(instr, memory), // LHU - 0x26 => self.execute_lwr(instr, memory), // LWR - 0x28 => self.execute_sb(instr, memory), // SB - 0x29 => self.execute_sh(instr, memory), // SH - 0x2A => self.execute_swl(instr, memory), // SWL - 0x2B => self.execute_sw(instr, memory), // SW - 0x2E => self.execute_swr(instr, memory), // SWR - 0x30 => self.execute_ll(instr, memory), // LL - 0x33 => Ok(()), // PREF - 0x38 => self.execute_sc(instr, memory), // SC - _ => self.handle_unknown_instruction(instr), - } - } - - /// Execute R-type instructions (opcode = 0x00) - fn execute_special(&mut self, instr: u32, _memory: &mut Memory) -> Result<()> { - let funct = instr & 0x3F; - let rs = ((instr >> 21) & 0x1F) as usize; - let rt = ((instr >> 16) & 0x1F) as usize; - let rd = ((instr >> 11) & 0x1F) as usize; - let shamt = (instr >> 6) & 0x1F; - - match funct { - 0x00 => self.regs.write(rd, self.regs.read(rt) << shamt), // SLL - 0x02 => self.regs.write(rd, self.regs.read(rt) >> shamt), // SRL - 0x03 => { - let result = (self.regs.read(rt) as i32) >> shamt; - self.regs.write(rd, result as u32); - } // SRA - 0x04 => self - .regs - .write(rd, self.regs.read(rt) << (self.regs.read(rs) & 0x1F)), // SLLV - 0x06 => self - .regs - .write(rd, self.regs.read(rt) >> (self.regs.read(rs) & 0x1F)), // SRLV - 0x07 => { - let shift = self.regs.read(rs) & 0x1F; - let result = (self.regs.read(rt) as i32) >> shift; - self.regs.write(rd, result as u32); - } // SRAV - 0x08 => { - // JR - Jump Register - let target = self.regs.read(rs); - self.branch_delay = true; - self.branch_target = target; - } - 0x09 => { - // JALR - Jump and Link Register - let target = self.regs.read(rs); - self.regs.write(rd, self.regs.pc.wrapping_add(4)); // Save return address - self.branch_delay = true; - self.branch_target = target; - } - 0x0A => { - // MOVZ - Move Conditional on Zero - if self.regs.read(rt) == 0 { - self.regs.write(rd, self.regs.read(rs)); - } - } - 0x0B => { - // MOVN - Move Conditional on Not Zero - if self.regs.read(rt) != 0 { - self.regs.write(rd, self.regs.read(rs)); - } - } - 0x0C => { - // SYSCALL - // TODO: Implement syscall handling - log::warn!("SYSCALL at PC={:#010x}", self.regs.pc.wrapping_sub(4)); - } - 0x0D => { - // BREAK - log::warn!("BREAK at PC={:#010x}", self.regs.pc.wrapping_sub(4)); - self.running = false; - } - 0x0F => { - // SYNC - // No-op in interpreter - } - 0x10 => self.regs.write(rd, self.regs.hi), // MFHI - 0x11 => self.regs.hi = self.regs.read(rs), // MTHI - 0x12 => self.regs.write(rd, self.regs.lo), // MFLO - 0x13 => self.regs.lo = self.regs.read(rs), // MTLO - 0x18 => { - // MULT - let a = self.regs.read(rs) as i32 as i64; - let b = self.regs.read(rt) as i32 as i64; - let result = a * b; - self.regs.hi = (result >> 32) as u32; - self.regs.lo = result as u32; - } - 0x19 => { - // MULTU - let a = self.regs.read(rs) as u64; - let b = self.regs.read(rt) as u64; - let result = a * b; - self.regs.hi = (result >> 32) as u32; - self.regs.lo = result as u32; - } - 0x1A => { - // DIV - let n = self.regs.read(rs) as i32; - let d = self.regs.read(rt) as i32; - if let Some(q) = n.checked_div(d) { - self.regs.lo = q as u32; - self.regs.hi = n.wrapping_rem(d) as u32; - } - } - 0x1B => { - // DIVU - let n = self.regs.read(rs); - let d = self.regs.read(rt); - if let Some(q) = n.checked_div(d) { - self.regs.lo = q; - self.regs.hi = n % d; - } - } - 0x20 => { - // ADD (with overflow check) - let a = self.regs.read(rs) as i32; - let b = self.regs.read(rt) as i32; - let result = a.wrapping_add(b); - self.regs.write(rd, result as u32); - } - 0x21 => { - // ADDU - let result = self.regs.read(rs).wrapping_add(self.regs.read(rt)); - self.regs.write(rd, result); - } - 0x22 => { - // SUB (with overflow check) - let a = self.regs.read(rs) as i32; - let b = self.regs.read(rt) as i32; - let result = a.wrapping_sub(b); - self.regs.write(rd, result as u32); - } - 0x23 => { - // SUBU - let result = self.regs.read(rs).wrapping_sub(self.regs.read(rt)); - self.regs.write(rd, result); - } - 0x24 => self.regs.write(rd, self.regs.read(rs) & self.regs.read(rt)), // AND - 0x25 => self.regs.write(rd, self.regs.read(rs) | self.regs.read(rt)), // OR - 0x26 => self.regs.write(rd, self.regs.read(rs) ^ self.regs.read(rt)), // XOR - 0x27 => self - .regs - .write(rd, !(self.regs.read(rs) | self.regs.read(rt))), // NOR - 0x2A => { - // SLT - let a = self.regs.read(rs) as i32; - let b = self.regs.read(rt) as i32; - self.regs.write(rd, if a < b { 1 } else { 0 }); - } - 0x2B => { - // SLTU - let a = self.regs.read(rs); - let b = self.regs.read(rt); - self.regs.write(rd, if a < b { 1 } else { 0 }); - } - 0x34 => { - // TEQ - if self.regs.read(rs) == self.regs.read(rt) { - return Err(SimulatorError::CpuError { - pc: self.regs.pc.wrapping_sub(4), - message: "trap on equal".to_string(), - }); - } - } - _ => return self.handle_unknown_instruction(instr), - } - Ok(()) - } - - /// Execute J-type instruction - fn execute_j(&mut self, instr: u32) -> Result<()> { - let target = instr & 0x03FF_FFFF; - let pc = self.regs.pc.wrapping_sub(4); // Get branch instruction address - let jump_target = (pc & 0xF000_0000) | (target << 2); - self.branch_delay = true; - self.branch_target = jump_target; - Ok(()) - } - - /// Execute JAL-type instruction - fn execute_jal(&mut self, instr: u32) -> Result<()> { - let target = instr & 0x03FF_FFFF; - let pc = self.regs.pc.wrapping_sub(4); // Get branch instruction address - let jump_target = (pc & 0xF000_0000) | (target << 2); - self.regs.write(31, self.regs.pc.wrapping_add(4)); // Save return address - self.branch_delay = true; - self.branch_target = jump_target; - Ok(()) - } - - /// Execute BEQ instruction - fn execute_beq(&mut self, instr: u32) -> Result<()> { - let rs = ((instr >> 21) & 0x1F) as usize; - let rt = ((instr >> 16) & 0x1F) as usize; - let offset = instr as i16 as i32; - if self.regs.read(rs) == self.regs.read(rt) { - self.branch(offset); - } - Ok(()) - } - - /// Execute BNE instruction - fn execute_bne(&mut self, instr: u32) -> Result<()> { - let rs = ((instr >> 21) & 0x1F) as usize; - let rt = ((instr >> 16) & 0x1F) as usize; - let offset = instr as i16 as i32; - if self.regs.read(rs) != self.regs.read(rt) { - self.branch(offset); - } - Ok(()) - } - - /// Execute ADDI instruction - fn execute_addi(&mut self, instr: u32) -> Result<()> { - let rs = ((instr >> 21) & 0x1F) as usize; - let rt = ((instr >> 16) & 0x1F) as usize; - let imm = instr as i16 as i32; - let result = (self.regs.read(rs) as i32).wrapping_add(imm); - self.regs.write(rt, result as u32); - Ok(()) - } - - /// Execute ADDIU instruction - fn execute_addiu(&mut self, instr: u32) -> Result<()> { - let rs = ((instr >> 21) & 0x1F) as usize; - let rt = ((instr >> 16) & 0x1F) as usize; - let imm = instr as i16 as i32; - let result = self.regs.read(rs).wrapping_add(imm as u32); - self.regs.write(rt, result); - Ok(()) - } - - /// Execute SLTI instruction - fn execute_slti(&mut self, instr: u32) -> Result<()> { - let rs = ((instr >> 21) & 0x1F) as usize; - let rt = ((instr >> 16) & 0x1F) as usize; - let imm = instr as i16 as i32; - let a = self.regs.read(rs) as i32; - self.regs.write(rt, if a < imm { 1 } else { 0 }); - Ok(()) - } - - /// Execute SLTIU instruction - fn execute_sltiu(&mut self, instr: u32) -> Result<()> { - let rs = ((instr >> 21) & 0x1F) as usize; - let rt = ((instr >> 16) & 0x1F) as usize; - let imm = instr as i16 as u32; - let a = self.regs.read(rs); - self.regs.write(rt, if a < imm { 1 } else { 0 }); - Ok(()) - } - - /// Execute ANDI instruction - fn execute_andi(&mut self, instr: u32) -> Result<()> { - let rs = ((instr >> 21) & 0x1F) as usize; - let rt = ((instr >> 16) & 0x1F) as usize; - let imm = instr as u16 as u32; - let result = self.regs.read(rs) & imm; - self.regs.write(rt, result); - Ok(()) - } - - /// Execute ORI instruction - fn execute_ori(&mut self, instr: u32) -> Result<()> { - let rs = ((instr >> 21) & 0x1F) as usize; - let rt = ((instr >> 16) & 0x1F) as usize; - let imm = instr as u16 as u32; - let result = self.regs.read(rs) | imm; - self.regs.write(rt, result); - Ok(()) - } - - /// Execute XORI instruction - fn execute_xori(&mut self, instr: u32) -> Result<()> { - let rs = ((instr >> 21) & 0x1F) as usize; - let rt = ((instr >> 16) & 0x1F) as usize; - let imm = instr as u16 as u32; - let result = self.regs.read(rs) ^ imm; - self.regs.write(rt, result); - Ok(()) - } - - /// Execute LUI instruction - fn execute_lui(&mut self, instr: u32) -> Result<()> { - let rt = ((instr >> 16) & 0x1F) as usize; - let imm = instr as u16; - self.regs.write(rt, (imm as u32) << 16); - Ok(()) - } - - /// Execute LB instruction - fn execute_lb(&mut self, instr: u32, memory: &mut Memory) -> Result<()> { - let rs = ((instr >> 21) & 0x1F) as usize; - let rt = ((instr >> 16) & 0x1F) as usize; - let offset = instr as i16 as i32; - let addr = self.regs.read(rs).wrapping_add(offset as u32); - let value = memory.read_u8(addr)? as i8 as i32 as u32; - self.regs.write(rt, value); - Ok(()) - } - - /// Execute LH instruction - fn execute_lh(&mut self, instr: u32, memory: &mut Memory) -> Result<()> { - let rs = ((instr >> 21) & 0x1F) as usize; - let rt = ((instr >> 16) & 0x1F) as usize; - let offset = instr as i16 as i32; - let addr = self.regs.read(rs).wrapping_add(offset as u32); - let value = memory.read_u16(addr)? as i16 as i32 as u32; - self.regs.write(rt, value); - Ok(()) - } - - /// Execute LW instruction - fn execute_lw(&mut self, instr: u32, memory: &mut Memory) -> Result<()> { - let rs = ((instr >> 21) & 0x1F) as usize; - let rt = ((instr >> 16) & 0x1F) as usize; - let offset = instr as i16 as i32; - let addr = self.regs.read(rs).wrapping_add(offset as u32); - let value = memory.read_u32(addr)?; - self.regs.write(rt, value); - Ok(()) - } - - /// Execute LBU instruction - fn execute_lbu(&mut self, instr: u32, memory: &mut Memory) -> Result<()> { - let rs = ((instr >> 21) & 0x1F) as usize; - let rt = ((instr >> 16) & 0x1F) as usize; - let offset = instr as i16 as i32; - let addr = self.regs.read(rs).wrapping_add(offset as u32); - let value = memory.read_u8(addr)? as u32; - self.regs.write(rt, value); - Ok(()) - } - - /// Execute LHU instruction - fn execute_lhu(&mut self, instr: u32, memory: &mut Memory) -> Result<()> { - let rs = ((instr >> 21) & 0x1F) as usize; - let rt = ((instr >> 16) & 0x1F) as usize; - let offset = instr as i16 as i32; - let addr = self.regs.read(rs).wrapping_add(offset as u32); - let value = memory.read_u16(addr)? as u32; - self.regs.write(rt, value); - Ok(()) - } - - /// Execute SB instruction - fn execute_sb(&mut self, instr: u32, memory: &mut Memory) -> Result<()> { - let rs = ((instr >> 21) & 0x1F) as usize; - let rt = ((instr >> 16) & 0x1F) as usize; - let offset = instr as i16 as i32; - let addr = self.regs.read(rs).wrapping_add(offset as u32); - let value = self.regs.read(rt) as u8; - memory.write_u8(addr, value)?; - Ok(()) - } - - /// Execute SH instruction - fn execute_sh(&mut self, instr: u32, memory: &mut Memory) -> Result<()> { - let rs = ((instr >> 21) & 0x1F) as usize; - let rt = ((instr >> 16) & 0x1F) as usize; - let offset = instr as i16 as i32; - let addr = self.regs.read(rs).wrapping_add(offset as u32); - let value = self.regs.read(rt) as u16; - memory.write_u16(addr, value)?; - Ok(()) - } - - /// Execute SW instruction - fn execute_sw(&mut self, instr: u32, memory: &mut Memory) -> Result<()> { - let rs = ((instr >> 21) & 0x1F) as usize; - let rt = ((instr >> 16) & 0x1F) as usize; - let offset = instr as i16 as i32; - let addr = self.regs.read(rs).wrapping_add(offset as u32); - let value = self.regs.read(rt); - memory.write_u32(addr, value)?; - Ok(()) - } - - /// Execute REGIMM instructions (opcode = 0x01) - fn execute_regimm(&mut self, instr: u32) -> Result<()> { - let rt = ((instr >> 16) & 0x1F) as usize; - let rs = ((instr >> 21) & 0x1F) as usize; - let offset = instr as i16 as i32; - - match rt { - 0x00 => { - // BLTZ - Branch on Less Than Zero - if (self.regs.read(rs) as i32) < 0 { - self.branch(offset); - } - } - 0x01 => { - // BGEZ - Branch on Greater Than or Equal to Zero - if (self.regs.read(rs) as i32) >= 0 { - self.branch(offset); - } - } - 0x10 => { - // BLTZAL - Branch on Less Than Zero and Link - self.regs.write(31, self.regs.pc.wrapping_add(4)); - if (self.regs.read(rs) as i32) < 0 { - self.branch(offset); - } - } - 0x11 => { - // BGEZAL - Branch on Greater Than or Equal to Zero and Link - self.regs.write(31, self.regs.pc.wrapping_add(4)); - if (self.regs.read(rs) as i32) >= 0 { - self.branch(offset); - } - } - _ => return self.handle_unknown_instruction(instr), - } - Ok(()) - } - - /// Execute BLEZ instruction - fn execute_blez(&mut self, instr: u32) -> Result<()> { - let rs = ((instr >> 21) & 0x1F) as usize; - let offset = instr as i16 as i32; - if (self.regs.read(rs) as i32) <= 0 { - self.branch(offset); - } - Ok(()) - } - - /// Execute BGTZ instruction - fn execute_bgtz(&mut self, instr: u32) -> Result<()> { - let rs = ((instr >> 21) & 0x1F) as usize; - let offset = instr as i16 as i32; - if (self.regs.read(rs) as i32) > 0 { - self.branch(offset); - } - Ok(()) - } - - /// Execute LWL instruction (Load Word Left) - fn execute_lwl(&mut self, instr: u32, memory: &mut Memory) -> Result<()> { - let rs = ((instr >> 21) & 0x1F) as usize; - let rt = ((instr >> 16) & 0x1F) as usize; - let offset = instr as i16 as i32; - let addr = self.regs.read(rs).wrapping_add(offset as u32); - let aligned_addr = addr & !3; - let byte = addr & 3; - - let mem_val = memory.read_u32(aligned_addr)?; - let rt_val = self.regs.read(rt); - - // Combine bytes based on alignment - let result = match byte { - 0 => (mem_val << 24) | (rt_val & 0x00FFFFFF), - 1 => (mem_val << 16) | (rt_val & 0x0000FFFF), - 2 => (mem_val << 8) | (rt_val & 0x000000FF), - 3 => mem_val, - _ => unreachable!(), - }; - - self.regs.write(rt, result); - Ok(()) - } - - /// Execute LWR instruction (Load Word Right) - fn execute_lwr(&mut self, instr: u32, memory: &mut Memory) -> Result<()> { - let rs = ((instr >> 21) & 0x1F) as usize; - let rt = ((instr >> 16) & 0x1F) as usize; - let offset = instr as i16 as i32; - let addr = self.regs.read(rs).wrapping_add(offset as u32); - let aligned_addr = addr & !3; - let byte = addr & 3; - - let mem_val = memory.read_u32(aligned_addr)?; - let rt_val = self.regs.read(rt); - - // Combine bytes based on alignment - let result = match byte { - 0 => mem_val, - 1 => (rt_val & 0xFF000000) | (mem_val >> 8), - 2 => (rt_val & 0xFFFF0000) | (mem_val >> 16), - 3 => (rt_val & 0xFFFFFF00) | (mem_val >> 24), - _ => unreachable!(), - }; - - self.regs.write(rt, result); - Ok(()) - } - - /// Execute SWL instruction (Store Word Left) - fn execute_swl(&mut self, instr: u32, memory: &mut Memory) -> Result<()> { - let rs = ((instr >> 21) & 0x1F) as usize; - let rt = ((instr >> 16) & 0x1F) as usize; - let offset = instr as i16 as i32; - let addr = self.regs.read(rs).wrapping_add(offset as u32); - let aligned_addr = addr & !3; - let byte = addr & 3; - - let mem_val = memory.read_u32(aligned_addr)?; - let rt_val = self.regs.read(rt); - - let result = match byte { - 0 => (mem_val & 0xFFFFFF00) | (rt_val >> 24), - 1 => (mem_val & 0xFFFF0000) | (rt_val >> 16), - 2 => (mem_val & 0xFF000000) | (rt_val >> 8), - 3 => rt_val, - _ => unreachable!(), - }; - - memory.write_u32(aligned_addr, result)?; - Ok(()) - } - - /// Execute SWR instruction (Store Word Right) - fn execute_swr(&mut self, instr: u32, memory: &mut Memory) -> Result<()> { - let rs = ((instr >> 21) & 0x1F) as usize; - let rt = ((instr >> 16) & 0x1F) as usize; - let offset = instr as i16 as i32; - let addr = self.regs.read(rs).wrapping_add(offset as u32); - let aligned_addr = addr & !3; - let byte = addr & 3; - - let mem_val = memory.read_u32(aligned_addr)?; - let rt_val = self.regs.read(rt); - - let result = match byte { - 0 => rt_val, - 1 => (mem_val & 0x000000FF) | (rt_val << 8), - 2 => (mem_val & 0x0000FFFF) | (rt_val << 16), - 3 => (mem_val & 0x00FFFFFF) | (rt_val << 24), - _ => unreachable!(), - }; - - memory.write_u32(aligned_addr, result)?; - Ok(()) - } - - /// Execute LL instruction (Load Linked). - fn execute_ll(&mut self, instr: u32, memory: &mut Memory) -> Result<()> { - let rs = ((instr >> 21) & 0x1F) as usize; - let rt = ((instr >> 16) & 0x1F) as usize; - let offset = instr as i16 as i32; - let addr = self.regs.read(rs).wrapping_add(offset as u32); - let value = memory.read_u32(addr)?; - self.regs.write(rt, value); - Ok(()) - } - - /// Execute SC instruction (Store Conditional). - fn execute_sc(&mut self, instr: u32, memory: &mut Memory) -> Result<()> { - let rs = ((instr >> 21) & 0x1F) as usize; - let rt = ((instr >> 16) & 0x1F) as usize; - let offset = instr as i16 as i32; - let addr = self.regs.read(rs).wrapping_add(offset as u32); - let value = self.regs.read(rt); - - // Guest contexts execute serially, so model the reservation as - // uncontended and report a successful conditional store. - memory.write_u32(addr, value)?; - self.regs.write(rt, 1); - Ok(()) - } - - /// Execute SPECIAL2 instructions (opcode = 0x1c) - fn execute_special2(&mut self, instr: u32, _memory: &mut Memory) -> Result<()> { - let funct = instr & 0x3F; - let rs = ((instr >> 21) & 0x1F) as usize; - let rt = ((instr >> 16) & 0x1F) as usize; - let rd = ((instr >> 11) & 0x1F) as usize; - - match funct { - 0x00 => { - // MADD - Multiply and Add - let a = self.regs.read(rs) as i64; - let b = self.regs.read(rt) as i64; - let acc = ((self.regs.hi as i64) << 32) | (self.regs.lo as i64); - let result = acc.wrapping_add(a * b); - self.regs.hi = (result >> 32) as u32; - self.regs.lo = result as u32; - } - 0x01 => { - // MADDU - Multiply and Add Unsigned - let a = self.regs.read(rs) as u64; - let b = self.regs.read(rt) as u64; - let acc = ((self.regs.hi as u64) << 32) | (self.regs.lo as u64); - let result = acc.wrapping_add(a * b); - self.regs.hi = (result >> 32) as u32; - self.regs.lo = result as u32; - } - 0x02 => { - // MUL - Multiply (result in Rd) - let a = self.regs.read(rs) as i32; - let b = self.regs.read(rt) as i32; - let result = a.wrapping_mul(b); - self.regs.write(rd, result as u32); - } - 0x04 => { - // MSUB - Multiply and Subtract - let a = self.regs.read(rs) as i64; - let b = self.regs.read(rt) as i64; - let acc = ((self.regs.hi as i64) << 32) | (self.regs.lo as i64); - let result = acc.wrapping_sub(a * b); - self.regs.hi = (result >> 32) as u32; - self.regs.lo = result as u32; - } - 0x05 => { - // MSUBU - Multiply and Subtract Unsigned - let a = self.regs.read(rs) as u64; - let b = self.regs.read(rt) as u64; - let acc = ((self.regs.hi as u64) << 32) | (self.regs.lo as u64); - let result = acc.wrapping_sub(a * b); - self.regs.hi = (result >> 32) as u32; - self.regs.lo = result as u32; - } - 0x20 => { - // CLZ - Count Leading Zeros - let val = self.regs.read(rs); - let count = val.leading_zeros(); - self.regs.write(rd, count); - } - 0x21 => { - // CLO - Count Leading Ones - let val = self.regs.read(rs); - let count = (!val).leading_zeros(); - self.regs.write(rd, count); - } - _ => return self.handle_unknown_instruction(instr), - } - Ok(()) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn unknown_instruction_policy_can_stop_or_skip() { - let instruction = 0xfc00_0000; - let mut memory = Memory::new(); - memory.write_u32(0, instruction).unwrap(); - - let mut skip_cpu = Cpu::new(0); - skip_cpu.start(); - assert!(skip_cpu.step(&mut memory).is_ok()); - assert_eq!(skip_cpu.regs.pc, 4); - - let mut stop_cpu = Cpu::new(0); - stop_cpu.set_unknown_instruction_policy(UnknownInstructionPolicy::Stop); - stop_cpu.start(); - assert!(matches!( - stop_cpu.step(&mut memory), - Err(SimulatorError::InvalidInstruction { pc: 0, instr }) if instr == instruction - )); - } - - #[test] - fn test_cpu_creation() { - let cpu = Cpu::new(0x8000_0000); - assert_eq!(cpu.regs.pc, 0x8000_0000); - assert_eq!(cpu.regs.read(0), 0); // R0 always zero - } - - #[test] - fn test_addiu() { - let mut cpu = Cpu::new(0); - let mut mem = Memory::new(); - // ADDIU $t0, $zero, 0x1234 - // opcode=0x09, rs=0, rt=8, imm=0x1234 - let instr = (0x09 << 26) | (8 << 16) | 0x1234; - mem.write_u32(0, instr).unwrap(); - cpu.start(); - cpu.step(&mut mem).unwrap(); - assert_eq!(cpu.regs.read(8), 0x1234); - } - - #[test] - fn test_nop_advances_pc_and_instruction_count() { - let mut cpu = Cpu::new(0); - let mut mem = Memory::new(); - cpu.start(); - - cpu.step(&mut mem).unwrap(); - - assert_eq!(cpu.regs.pc, 4); - assert_eq!(cpu.instruction_count, 1); - } - - #[test] - fn test_jal_sets_ra_after_delay_slot() { - let mut cpu = Cpu::new(0); - let mut mem = Memory::new(); - - // JAL 0x10 - let jal = (0x03 << 26) | 0x04; - mem.write_u32(0, jal).unwrap(); - mem.write_u32(4, 0).unwrap(); // delay slot - mem.write_u32(0x10, 0).unwrap(); - - cpu.start(); - cpu.step(&mut mem).unwrap(); - assert_eq!(cpu.regs.read(31), 8); - - cpu.step(&mut mem).unwrap(); - assert_eq!(cpu.regs.pc, 0x10); - } - - #[test] - fn test_lwl_lwr_loads_unaligned_little_endian_word() { - let mut cpu = Cpu::new(0); - let mut mem = Memory::new(); - mem.load_data(0x100, &[0x11, 0x22, 0x33, 0x44, 0x55]) - .unwrap(); - - let lwl = (0x22 << 26) | (4 << 21) | (8 << 16) | 3; - let lwr = (0x26 << 26) | (4 << 21) | (8 << 16); - mem.write_u32(0, lwl).unwrap(); - mem.write_u32(4, lwr).unwrap(); - cpu.regs.write(4, 0x101); - cpu.regs.write(8, 0xDEAD_BEEF); - cpu.start(); - - cpu.step(&mut mem).unwrap(); - cpu.step(&mut mem).unwrap(); - - assert_eq!(cpu.regs.read(8), 0x5544_3322); - } - - #[test] - fn test_swl_swr_stores_unaligned_little_endian_word() { - let mut cpu = Cpu::new(0); - let mut mem = Memory::new(); - mem.load_data(0x100, &[0x11, 0x22, 0x33, 0x44, 0x55]) - .unwrap(); - - let swl = (0x2A << 26) | (4 << 21) | (8 << 16) | 3; - let swr = (0x2E << 26) | (4 << 21) | (8 << 16); - mem.write_u32(0, swl).unwrap(); - mem.write_u32(4, swr).unwrap(); - cpu.regs.write(4, 0x101); - cpu.regs.write(8, 0xA1B2_C3D4); - cpu.start(); - - cpu.step(&mut mem).unwrap(); - cpu.step(&mut mem).unwrap(); - - assert_eq!( - (0..5) - .map(|offset| mem.read_u8(0x100 + offset).unwrap()) - .collect::>(), - vec![0x11, 0xD4, 0xC3, 0xB2, 0xA1] - ); - } - - #[test] - fn test_ll_sc_updates_memory_and_reports_success() { - let mut cpu = Cpu::new(0); - let mut mem = Memory::new(); - mem.write_u32(0x100, 41).unwrap(); - - let ll = (0x30 << 26) | (4 << 21) | (2 << 16); - let addiu = (0x09 << 26) | (2 << 21) | (2 << 16) | 1; - let sc = (0x38 << 26) | (4 << 21) | (2 << 16); - mem.write_u32(0, ll).unwrap(); - mem.write_u32(4, addiu).unwrap(); - mem.write_u32(8, sc).unwrap(); - cpu.regs.write(4, 0x100); - cpu.start(); - - cpu.step(&mut mem).unwrap(); - cpu.step(&mut mem).unwrap(); - cpu.step(&mut mem).unwrap(); - - assert_eq!(mem.read_u32(0x100).unwrap(), 42); - assert_eq!(cpu.regs.read(2), 1); - } - - #[test] - fn test_teq_only_traps_equal_operands() { - let teq = (4 << 21) | (5 << 16) | 0x34; - let mut mem = Memory::new(); - mem.write_u32(0, teq).unwrap(); - - let mut unequal = Cpu::new(0); - unequal.regs.write(4, 1); - unequal.regs.write(5, 2); - unequal.start(); - assert!(unequal.step(&mut mem).is_ok()); - - let mut equal = Cpu::new(0); - equal.regs.write(4, 1); - equal.regs.write(5, 1); - equal.start(); - assert!(matches!( - equal.step(&mut mem), - Err(SimulatorError::CpuError { pc: 0, .. }) - )); - } - - #[test] - fn test_mult_sign_extends_negative_operands() { - let mut cpu = Cpu::new(0); - let mut mem = Memory::new(); - cpu.regs.write(8, (-7_i32) as u32); - cpu.regs.write(9, 3); - mem.write_u32(0, (8 << 21) | (9 << 16) | 0x18).unwrap(); - cpu.start(); - - cpu.step(&mut mem).unwrap(); - - assert_eq!(cpu.regs.hi, u32::MAX); - assert_eq!(cpu.regs.lo, (-21_i32) as u32); - } - - #[test] - fn test_lui() { - let mut cpu = Cpu::new(0); - let mut mem = Memory::new(); - // LUI $t0, 0xABCD - // opcode=0x0F, rs=0, rt=8, imm=0xABCD - let instr = (0x0F << 26) | (8 << 16) | 0xABCD; - mem.write_u32(0, instr).unwrap(); - cpu.start(); - cpu.step(&mut mem).unwrap(); - assert_eq!(cpu.regs.read(8), 0xABCD_0000); - } - - #[test] - fn test_pref_is_ignored() { - let mut cpu = Cpu::new(0); - let mut mem = Memory::new(); - let pref = (0x33 << 26) | (4 << 21) | (7 << 16) | 0x1234; - mem.write_u32(0, pref).unwrap(); - cpu.regs.write(4, 0x1000); - cpu.start(); - - cpu.step(&mut mem).unwrap(); - - assert_eq!(cpu.regs.pc, 4); - assert_eq!(cpu.regs.read(4), 0x1000); - assert_eq!(cpu.instruction_count, 1); - } -} diff --git a/native/DingooEmu/crates/dingooemu-core/src/emulator.rs b/native/DingooEmu/crates/dingooemu-core/src/emulator.rs deleted file mode 100644 index 290835f..0000000 --- a/native/DingooEmu/crates/dingooemu-core/src/emulator.rs +++ /dev/null @@ -1,2887 +0,0 @@ -use crate::app_loader::{AppImage, ResourceKind}; -use crate::audio::{Audio, AudioConfig}; -use crate::cheats::{CheatManager, CheatParseError, CheatRule}; -use crate::cpu::Cpu; -use crate::error::{Result, SimulatorError}; -use crate::input::Input; -#[cfg(feature = "jit")] -use crate::jit::JitEngine; -use crate::memory::Memory; -use crate::video::Video; -use std::collections::{BTreeMap, BTreeSet, HashMap}; -use std::path::{Component, Path, PathBuf}; - -mod sdk_hle; - -const CPU_CLOCK_HZ: u64 = 336_000_000; -const FRAMES_PER_SECOND: u64 = 60; -const OS_TICKS_PER_SECOND: u64 = 100; -const CYCLES_PER_FRAME: u64 = CPU_CLOCK_HZ / FRAMES_PER_SECOND; -// Model the pipeline and memory stalls with a conservative average CPI. -const CPU_CYCLES_PER_INSTRUCTION: u64 = 2; -const STANDARD_APP_LOAD_BASE: u32 = 0x80A0_0000; -const MAX_AUDIO_WRITE_BYTES: u32 = 4 * 1024 * 1024; -const TASK_QUANTUM_CYCLES: u64 = 4_096; -const TASK_RETURN_ADDRESS: u32 = u32::MAX; -const MAX_GUEST_TASKS: usize = 16; -const HOOK_FILTER_WORDS: usize = 1_024; -const MAX_INSTRUCTION_BLOCK_LEN: usize = 64; -const INSTRUCTION_BLOCK_CACHE_SLOTS: usize = 4_096; -const FILE_SEARCH_NAME_OFFSET: u32 = 0x12; -const FILE_SEARCH_NAME_CAPACITY: usize = 256; - -/// Behavior when the guest calls an SDK function without an HLE implementation. -#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] -pub enum UnknownHlePolicy { - /// Record the call and return zero to preserve compatibility. - #[default] - Report, - /// Record the call and stop unless the function name is allowlisted. - Stop, -} - -/// Aggregated diagnostics for one unknown SDK HLE function. -#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize)] -pub struct UnknownHleCall { - pub name: String, - pub count: u64, - pub import_address: u32, - pub first_pc: u32, - pub first_arguments: [u32; 4], -} - -fn hook_filter_location(address: u32) -> (usize, u64) { - let bit_index = (address as usize >> 2) & (HOOK_FILTER_WORDS * u64::BITS as usize - 1); - ( - bit_index / u64::BITS as usize, - 1 << (bit_index % u64::BITS as usize), - ) -} - -struct CachedInstructionBlock { - start: u32, - len: u8, - instructions: [u32; MAX_INSTRUCTION_BLOCK_LEN], -} - -fn instruction_block_cache_index(address: u32) -> usize { - (address as usize >> 2) & (INSTRUCTION_BLOCK_CACHE_SLOTS - 1) -} - -fn empty_instruction_block_cache() -> Box<[CachedInstructionBlock]> { - std::iter::repeat_with(|| CachedInstructionBlock { - start: 0, - len: 0, - instructions: [0; MAX_INSTRUCTION_BLOCK_LEN], - }) - .take(INSTRUCTION_BLOCK_CACHE_SLOTS) - .collect::>() - .into_boxed_slice() -} - -#[derive(Clone, serde::Serialize, serde::Deserialize)] -struct OpenFile { - data: Vec, - position: usize, - data_ptr: u32, - save_path: Option, - writable: bool, - dirty: bool, -} - -#[derive(Clone, serde::Serialize, serde::Deserialize)] -struct FileSearch { - entries: Vec, - next_index: usize, -} - -#[derive(Clone, serde::Serialize, serde::Deserialize)] -struct GuiState { - key_messages_enabled: bool, - windows: BTreeMap, - focused_window: Option, - next_window_handle: u32, - reported_key: u32, - message_buffer: Option, - key_info_buffer: Option, -} - -impl Default for GuiState { - fn default() -> Self { - Self { - key_messages_enabled: false, - windows: BTreeMap::new(), - focused_window: None, - next_window_handle: 1, - reported_key: 0, - message_buffer: None, - key_info_buffer: None, - } - } -} - -#[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)] -enum TaskWait { - AudioWrite, - Semaphore(u32), - UntilCycle(u64), -} - -#[derive(Clone, serde::Serialize, serde::Deserialize)] -struct GuestTask { - cpu: Cpu, - priority: u32, - wait: Option, -} - -#[derive(serde::Serialize)] -struct EmulatorStateRef<'a> { - cpu: &'a Cpu, - memory: &'a Memory, - video: &'a Video, - input: &'a Input, - audio: &'a Audio, - frame_count: u64, - cycle_count: u64, - tasks: &'a [GuestTask], - scheduler_cursor: usize, - main_wait: Option, - active_task: Option, - semaphores: &'a HashMap, - next_semaphore_handle: u32, - open_files: HashMap, - next_file_handle: u32, - file_searches: &'a HashMap, - gui: &'a GuiState, - app_main_args_initialized: bool, - locale_ansi_buffer: Option, - framebuffer_submitted: bool, -} - -#[derive(serde::Deserialize)] -struct EmulatorState { - cpu: Cpu, - memory: Memory, - video: Video, - input: Input, - audio: Audio, - frame_count: u64, - cycle_count: u64, - tasks: Vec, - scheduler_cursor: usize, - main_wait: Option, - active_task: Option, - semaphores: HashMap, - next_semaphore_handle: u32, - open_files: HashMap, - next_file_handle: u32, - file_searches: HashMap, - gui: GuiState, - app_main_args_initialized: bool, - locale_ansi_buffer: Option, - framebuffer_submitted: bool, -} - -fn prepare_resource_file_data(name: &str, kind: ResourceKind, data: Vec) -> Vec { - let is_bin = name - .rsplit_once('.') - .is_some_and(|(_, extension)| extension.eq_ignore_ascii_case("bin")); - if kind != ResourceKind::Packed || !is_bin || data.len() < 12 { - return data; - } - - let record_count = u16::from_le_bytes([data[0], data[1]]); - let declared_size = u32::from_le_bytes([data[8], data[9], data[10], data[11]]); - let Ok(data_size) = u32::try_from(data.len()) else { - return data; - }; - if record_count == 0 || declared_size <= data_size { - return data; - } - - let payload = &data[4..]; - let Ok(payload_size) = u32::try_from(payload.len()) else { - return data; - }; - let Some(view_size) = data.len().checked_add(16) else { - return data; - }; - let mut view = vec![0; view_size]; - view[0..4].copy_from_slice(&1_u32.to_le_bytes()); - view[8..12].copy_from_slice(&payload_size.to_le_bytes()); - view[12..12 + payload.len()].copy_from_slice(payload); - view[16..20].fill(0); - view -} - -/// Main emulator struct that ties all components together -pub struct Emulator { - /// CPU core - pub cpu: Cpu, - /// Memory system - pub memory: Memory, - /// Video subsystem - pub video: Video, - /// Input subsystem - pub input: Input, - /// PCM audio subsystem - pub audio: Audio, - /// Frontend-managed memory and register freeze rules - cheats: CheatManager, - /// Frame count - frame_count: u64, - /// Emulated CPU cycles elapsed - cycle_count: u64, - /// Cooperatively scheduled guest tasks - tasks: Vec, - /// Scheduler position preserved across frontend frames - scheduler_cursor: usize, - /// Wait state for the main guest task - main_wait: Option, - /// Task whose CPU is currently swapped into `cpu` - active_task: Option, - /// uC/OS-II semaphore counts by guest handle - semaphores: HashMap, - /// Next guest semaphore handle - next_semaphore_handle: u32, - /// Parsed app image (for resource access) - app: Option, - /// Import address to function name mapping (for diagnostics) - #[allow(dead_code)] - import_addrs: HashMap, - /// Hooked addresses (for SDK function interception) - hooked_addrs: HashMap, - /// Behavior for SDK imports without an HLE implementation - unknown_hle_policy: UnknownHlePolicy, - /// Function names allowed to retain compatibility-stub behavior in strict mode - unknown_hle_allowlist: BTreeSet, - /// Unknown SDK calls aggregated by function name for diagnostics - unknown_hle_calls: BTreeMap, - /// Fast rejection filter for non-hook instruction addresses - hook_filter: Box<[u64]>, - /// Direct-mapped cache of sequential guest instruction blocks - instruction_blocks: Box<[CachedInstructionBlock]>, - /// Native code cache for hot MIPS instruction blocks - #[cfg(feature = "jit")] - jit: JitEngine, - /// Open guest resource files - open_files: HashMap, - /// Host directory used for persistent guest-created files - save_directory: Option, - /// Next guest file handle - next_file_handle: u32, - /// Active file searches keyed by the guest find-data address - file_searches: HashMap, - /// Minimal window-manager state used to deliver guest key messages - gui: GuiState, - /// AppMain export address - app_main_entry: Option, - /// AppMain startup check hook address - app_main_init_check_address: Option, - /// Whether AppMain startup arguments were installed - app_main_args_initialized: bool, - /// Original app path for AppMain - app_path: String, - /// Reusable guest buffer for ANSI string conversions - locale_ansi_buffer: Option, - /// Whether the guest submitted a framebuffer this tick - framebuffer_submitted: bool, -} - -impl Emulator { - /// Create a new emulator from an .app file path - pub fn from_path>(path: P) -> Result { - let path = path.as_ref(); - let app = AppImage::from_path(path)?; - Self::from_app_with_path(app, path.to_string_lossy().into_owned()) - } - - /// Create a new emulator from a parsed AppImage - pub fn from_app(app: AppImage) -> Result { - Self::from_app_with_path(app, String::new()) - } - - fn from_app_with_path(app: AppImage, app_path: String) -> Result { - let mut memory = Memory::new(); - - // Load executable into memory at the load base address (KSEG0) - let load_base = app.load_base(); - let executable = app.executable().to_vec(); - memory.load_data(load_base, &executable)?; - - // Also map at physical address (for games that use physical addressing) - let physical_addr = load_base & 0x1FFF_FFFF; - if physical_addr != load_base { - memory.load_data(physical_addr, &executable)?; - } - - // Map framebuffer at a fixed guest-visible address - // The game writes directly to this address - let fb_addr = crate::video::VM_LCD_FB_ADDRESS; - let fb_size = crate::video::FRAMEBUFFER_SIZE; - // Reserve space in memory for framebuffer (zero it out) - for i in 0..fb_size { - let _ = memory.write_u8(fb_addr + i as u32, 0); - } - - let mut cpu = Cpu::new(app.entry_point()); - let app_main_entry = app - .exports - .iter() - .find(|export| export.name == "AppMain") - .map(|export| export.address); - - if let Some(app_main_entry) = app_main_entry { - cpu.regs.write(31, app_main_entry); - cpu.regs.write(25, app.entry_point()); - } - // Older app layouts require the loader to request LCD initialization. - cpu.regs - .write(5, u32::from(load_base != STANDARD_APP_LOAD_BASE)); - - // Initialize stack pointer to a reasonable value in RAM - // Stack grows downward from top of RAM (32MB) - cpu.regs.write(29, 0x01FF_FFF0); // $sp = top of RAM - 16 - - // Use a fixed guest-visible framebuffer address - // The game writes directly to this address - let video = Video::new(); - - let input = Input::new(); - let audio = Audio::new(); - // Build import address map for SDK hooking - // The game uses physical addressing, not KSEG0 - // So we need to hook physical addresses - let mut import_addrs = HashMap::new(); - let mut hooked_addrs = HashMap::new(); - let mut hook_filter = vec![0; HOOK_FILTER_WORDS].into_boxed_slice(); - for import in &app.imports { - // Physical address (what the game actually uses) - let phys = import.address & 0x1FFF_FFFF; - import_addrs.insert(phys, import.name.clone()); - hooked_addrs.insert(phys, import.name.clone()); - let (word, mask) = hook_filter_location(phys); - hook_filter[word] |= mask; - // Also hook KSEG0 address (for completeness) - if phys != import.address { - import_addrs.insert(import.address, import.name.clone()); - hooked_addrs.insert(import.address, import.name.clone()); - let (word, mask) = hook_filter_location(import.address); - hook_filter[word] |= mask; - } - } - - log::debug!( - "Emulator initialized: entry={:#010x}, base={:#010x}, physical={:#010x}, framebuffer={:#010x}, imports={}, hooked={}", - app.entry_point(), - load_base, - physical_addr, - crate::video::VM_LCD_FB_ADDRESS, - import_addrs.len(), - hooked_addrs.len() - ); - - for (addr, name) in hooked_addrs.iter().take(5) { - log::trace!("Hooked SDK import: {:#010x} = {}", addr, name); - } - - let save_directory = Path::new(&app_path).parent().map(Path::to_path_buf); - Ok(Self { - cpu, - memory, - video, - input, - audio, - cheats: CheatManager::default(), - frame_count: 0, - cycle_count: 0, - tasks: Vec::new(), - scheduler_cursor: 0, - main_wait: None, - active_task: None, - semaphores: HashMap::new(), - next_semaphore_handle: 1, - app: Some(app), - import_addrs, - hooked_addrs, - unknown_hle_policy: UnknownHlePolicy::default(), - unknown_hle_allowlist: BTreeSet::new(), - unknown_hle_calls: BTreeMap::new(), - hook_filter, - instruction_blocks: empty_instruction_block_cache(), - #[cfg(feature = "jit")] - jit: JitEngine::new(), - open_files: HashMap::new(), - save_directory, - next_file_handle: 1, - file_searches: HashMap::new(), - gui: GuiState::default(), - app_main_entry, - app_main_init_check_address: app_main_entry.map(|addr| addr.wrapping_add(0x34)), - app_main_args_initialized: false, - app_path, - locale_ansi_buffer: None, - framebuffer_submitted: false, - }) - } - - fn install_app_main_args(&mut self) -> Result<()> { - if self.app_main_args_initialized { - return Ok(()); - } - self.app_main_args_initialized = true; - - let path = if self.app_path.is_empty() { - "game.app" - } else { - self.app_path - .rsplit(['/', '\\']) - .next() - .unwrap_or(&self.app_path) - }; - let wide: Vec = path.encode_utf16().chain(std::iter::once(0)).collect(); - let byte_len = (wide.len() * 2) as u32; - let ptr = self.memory.malloc(byte_len); - if ptr == 0 { - return Ok(()); - } - for (i, word) in wide.iter().enumerate() { - self.memory - .write_u16(ptr.wrapping_add((i * 2) as u32), *word)?; - } - self.cpu.regs.write(4, ptr); - if let Some(app_main_entry) = self.app_main_entry { - self.cpu.regs.write(25, app_main_entry); - } - Ok(()) - } - /// Start the emulator - pub fn start(&mut self) { - self.cpu.start(); - log::info!("Emulator started"); - } - - /// Configure how unknown SDK HLE calls affect execution. - pub fn set_unknown_hle_policy(&mut self, policy: UnknownHlePolicy) { - self.unknown_hle_policy = policy; - } - - /// Replace the exact function-name allowlist used by strict HLE mode. - pub fn set_unknown_hle_allowlist(&mut self, names: I) - where - I: IntoIterator, - S: Into, - { - self.unknown_hle_allowlist = names.into_iter().map(Into::into).collect(); - } - - /// Return unknown HLE diagnostics in stable function-name order. - pub fn unknown_hle_calls(&self) -> impl ExactSizeIterator { - self.unknown_hle_calls.values() - } - - /// Clear all unknown HLE observations collected during this run. - pub fn clear_unknown_hle_calls(&mut self) { - self.unknown_hle_calls.clear(); - } - - fn record_unknown_hle( - &mut self, - name: &str, - import_address: u32, - return_address: u32, - ) -> Result<()> { - let first_pc = return_address.wrapping_sub(8); - let first_arguments = [ - self.cpu.regs.read(4), - self.cpu.regs.read(5), - self.cpu.regs.read(6), - self.cpu.regs.read(7), - ]; - let is_first = if let Some(call) = self.unknown_hle_calls.get_mut(name) { - call.count = call.count.saturating_add(1); - false - } else { - self.unknown_hle_calls.insert( - name.to_string(), - UnknownHleCall { - name: name.to_string(), - count: 1, - import_address, - first_pc, - first_arguments, - }, - ); - true - }; - - if is_first { - log::warn!( - "Unknown SDK HLE {name} first called at {first_pc:#010x} (import {import_address:#010x}); calls are aggregated" - ); - } - - if self.unknown_hle_policy == UnknownHlePolicy::Stop - && !self.unknown_hle_allowlist.contains(name) - { - return Err(SimulatorError::UnknownHle { - name: name.to_string(), - pc: first_pc, - import_address, - arguments: first_arguments, - }); - } - Ok(()) - } - - /// Stop the emulator - pub fn stop(&mut self) { - self.flush_save_files(); - self.cpu.stop(); - log::info!("Emulator stopped"); - } - - /// Rebuild all mutable runtime state from the loaded app image. - pub fn reset(&mut self) -> Result<()> { - self.flush_save_files(); - let app = self - .app - .clone() - .ok_or_else(|| "cannot reset an emulator without a loaded app".to_string())?; - let was_running = self.is_running(); - let mut replacement = Self::from_app_with_path(app, self.app_path.clone())?; - replacement.save_directory = self.save_directory.clone(); - replacement.cheats = self.cheats.clone(); - replacement.unknown_hle_policy = self.unknown_hle_policy; - replacement.unknown_hle_allowlist = self.unknown_hle_allowlist.clone(); - self.memory.copy_state_from(&replacement.memory); - std::mem::swap(&mut replacement.memory, &mut self.memory); - if was_running { - replacement.start(); - } - *self = replacement; - log::info!("Emulator reset"); - Ok(()) - } - - /// Return the fixed buffer capacity required for a serialized state. - pub fn serialized_state_size(&self) -> usize { - crate::save_state::SERIALIZED_SIZE - } - - /// Serialize the complete mutable runtime state into a fixed-size buffer. - pub fn serialize_state(&self, output: &mut [u8]) -> anyhow::Result<()> { - let app = self - .app - .as_ref() - .ok_or_else(|| anyhow::anyhow!("cannot save state without loaded content"))?; - let mut open_files = self.open_files.clone(); - for file in open_files.values_mut() { - let Some(path) = file.save_path.take() else { - continue; - }; - let root = self - .save_directory - .as_ref() - .ok_or_else(|| anyhow::anyhow!("save file has no configured save directory"))?; - let relative = path - .strip_prefix(root) - .map_err(|_| anyhow::anyhow!("save file is outside the configured directory"))?; - if !safe_relative_path(relative) { - anyhow::bail!("save file has an unsafe relative path"); - } - file.save_path = Some(relative.to_path_buf()); - } - let state = EmulatorStateRef { - cpu: &self.cpu, - memory: &self.memory, - video: &self.video, - input: &self.input, - audio: &self.audio, - frame_count: self.frame_count, - cycle_count: self.cycle_count, - tasks: &self.tasks, - scheduler_cursor: self.scheduler_cursor, - main_wait: self.main_wait, - active_task: self.active_task, - semaphores: &self.semaphores, - next_semaphore_handle: self.next_semaphore_handle, - open_files, - next_file_handle: self.next_file_handle, - file_searches: &self.file_searches, - gui: &self.gui, - app_main_args_initialized: self.app_main_args_initialized, - locale_ansi_buffer: self.locale_ansi_buffer, - framebuffer_submitted: self.framebuffer_submitted, - }; - crate::save_state::encode(&state, crc32fast::hash(&app.data), output) - } - - /// Restore a serialized state without changing the emulator on failure. - pub fn unserialize_state(&mut self, input: &[u8]) -> anyhow::Result<()> { - let app = self - .app - .clone() - .ok_or_else(|| anyhow::anyhow!("cannot load state without loaded content"))?; - let mut state: EmulatorState = - crate::save_state::decode(input, crc32fast::hash(&app.data))?; - if !state.memory.snapshot_layout_is_valid() || !state.video.snapshot_layout_is_valid() { - anyhow::bail!("save state has an incompatible memory layout"); - } - if state.tasks.len() > MAX_GUEST_TASKS - || state - .active_task - .is_some_and(|index| index >= state.tasks.len()) - { - anyhow::bail!("save state has an invalid task layout"); - } - for file in state.open_files.values_mut() { - let Some(relative) = file.save_path.take() else { - continue; - }; - if !safe_relative_path(&relative) { - anyhow::bail!("save state contains an unsafe save path"); - } - let root = self - .save_directory - .as_ref() - .ok_or_else(|| anyhow::anyhow!("save state requires a save directory"))?; - file.save_path = Some(root.join(relative)); - } - - let was_running = state.cpu.is_running(); - #[cfg(feature = "standalone")] - let host_audio_output_enabled = self.audio.host_output_enabled(); - let mut replacement = Self::from_app_with_path(app, self.app_path.clone())?; - replacement.save_directory = self.save_directory.clone(); - replacement.cheats = self.cheats.clone(); - replacement.unknown_hle_policy = self.unknown_hle_policy; - replacement.unknown_hle_allowlist = self.unknown_hle_allowlist.clone(); - replacement.unknown_hle_calls = self.unknown_hle_calls.clone(); - replacement.cpu = state.cpu; - self.memory.copy_state_from(&state.memory); - std::mem::swap(&mut replacement.memory, &mut self.memory); - replacement.video = state.video; - replacement.input = state.input; - replacement.audio = state.audio; - #[cfg(feature = "standalone")] - replacement - .audio - .set_host_output_enabled(host_audio_output_enabled); - replacement.audio.resume_after_state_load(); - replacement.frame_count = state.frame_count; - replacement.cycle_count = state.cycle_count; - replacement.tasks = state.tasks; - replacement.scheduler_cursor = state.scheduler_cursor; - replacement.main_wait = state.main_wait; - replacement.active_task = state.active_task; - replacement.semaphores = state.semaphores; - replacement.next_semaphore_handle = state.next_semaphore_handle; - replacement.open_files = state.open_files; - replacement.next_file_handle = state.next_file_handle; - replacement.file_searches = state.file_searches; - replacement.gui = state.gui; - replacement.app_main_args_initialized = state.app_main_args_initialized; - replacement.locale_ansi_buffer = state.locale_ansi_buffer; - replacement.framebuffer_submitted = state.framebuffer_submitted; - if was_running { - replacement.cpu.start(); - } - *self = replacement; - Ok(()) - } - - /// Run one frame of emulation - pub fn tick(&mut self) -> Result<()> { - #[cfg(feature = "jit")] - self.jit.begin_frame(); - self.framebuffer_submitted = false; - self.cheats.apply(&mut self.memory, &mut self.cpu); - - let mut remaining_cycles = CYCLES_PER_FRAME; - let mut idle_contexts = 0usize; - while remaining_cycles > 0 { - let context_count = self.tasks.len() + 1; - if self.scheduler_cursor >= context_count { - self.scheduler_cursor = 0; - } - let slice_cycles = remaining_cycles.min(TASK_QUANTUM_CYCLES); - let executed = if self.scheduler_cursor == 0 { - self.active_task = None; - self.run_active_cpu_slice(slice_cycles)? - } else { - self.run_task_slice(self.scheduler_cursor - 1, slice_cycles)? - }; - self.scheduler_cursor = (self.scheduler_cursor + 1) % context_count; - - if executed == 0 { - idle_contexts += 1; - if idle_contexts >= context_count { - self.cycle_count = self.cycle_count.wrapping_add(slice_cycles); - remaining_cycles -= slice_cycles; - idle_contexts = 0; - } - } else { - remaining_cycles -= executed; - idle_contexts = 0; - } - - if self.framebuffer_submitted { - self.cycle_count = self.cycle_count.wrapping_add(remaining_cycles); - remaining_cycles = 0; - } - } - self.tasks.retain(|task| task.cpu.is_running()); - - // Use a fallback sync for tests or apps that draw without an explicit submit. - if !self.framebuffer_submitted { - self.sync_framebuffer(); - } - - self.video.advance_frame(); - self.audio.advance_frame(); - self.frame_count += 1; - - Ok(()) - } - - fn run_task_slice(&mut self, task_index: usize, cycles: u64) -> Result { - let task_cpu = std::mem::replace(&mut self.tasks[task_index].cpu, Cpu::new(0)); - let main_cpu = std::mem::replace(&mut self.cpu, task_cpu); - self.active_task = Some(task_index); - let result = self.run_active_cpu_slice(cycles); - let task_cpu = std::mem::replace(&mut self.cpu, main_cpu); - self.tasks[task_index].cpu = task_cpu; - self.active_task = None; - result - } - - fn run_active_cpu_slice(&mut self, cycles: u64) -> Result { - if self.active_context_waiting() || !self.cpu.is_running() { - return Ok(0); - } - - let mut executed = 0; - while executed < cycles { - if self.cpu.regs.pc == TASK_RETURN_ADDRESS { - self.cpu.stop(); - break; - } - - let pc = self.cpu.regs.pc; - if self.active_task.is_none() - && Some(pc) == self.app_main_entry - && !self.app_main_args_initialized - { - self.install_app_main_args()?; - } - if self.active_task.is_none() && Some(pc) == self.app_main_init_check_address { - self.cpu.regs.write(2, 1); - } - - let (hook_word, hook_mask) = hook_filter_location(pc); - let func_name = (self.hook_filter[hook_word] & hook_mask != 0) - .then(|| self.hooked_addrs.get(&pc)) - .flatten() - .cloned(); - if let Some(func_name) = func_name { - log::trace!("SDK hook: PC={:#010x} = {}", pc, func_name); - sdk_hle::dispatch(self, pc, &func_name)?; - self.cycle_count = self.cycle_count.wrapping_add(CPU_CYCLES_PER_INSTRUCTION); - executed += CPU_CYCLES_PER_INSTRUCTION; - if self.framebuffer_submitted - || self.active_context_waiting() - || !self.cpu.is_running() - { - break; - } - } else { - let completed = self.run_cached_instruction_block(pc, cycles - executed)?; - let completed_cycles = completed * CPU_CYCLES_PER_INSTRUCTION; - self.cycle_count = self.cycle_count.wrapping_add(completed_cycles); - executed += completed_cycles; - } - } - Ok(executed) - } - - fn run_cached_instruction_block(&mut self, start: u32, remaining_cycles: u64) -> Result { - self.ensure_instruction_block(start)?; - let instruction_limit = (remaining_cycles / CPU_CYCLES_PER_INSTRUCTION) as usize; - let cache_index = instruction_block_cache_index(start); - - #[cfg(feature = "jit")] - if !self.cpu.branch_delay { - let block = &self.instruction_blocks[cache_index]; - let ram = self.memory.jit_ram_ptr(); - let framebuffer = self.memory.jit_framebuffer_ptr(); - if let Some(completed) = self.jit.execute( - start, - &block.instructions[..block.len as usize], - instruction_limit, - &mut self.cpu.regs, - ram, - framebuffer, - ) { - self.cpu.account_instructions(completed); - return Ok(completed); - } - } - - let block = &self.instruction_blocks[cache_index]; - let mut completed = 0u64; - - for &instruction in block.instructions[..block.len as usize] - .iter() - .take(instruction_limit) - { - let current_pc = self.cpu.regs.pc; - let step_result = self - .cpu - .step_fetched_unaccounted(instruction, &mut self.memory); - if step_result.is_err() { - self.cpu.account_instructions(completed); - self.cycle_count = self - .cycle_count - .wrapping_add(completed * CPU_CYCLES_PER_INSTRUCTION); - } - if !step_result? { - break; - } - completed += 1; - if !self.cpu.is_running() || self.cpu.regs.pc != current_pc.wrapping_add(4) { - break; - } - } - - self.cpu.account_instructions(completed); - Ok(completed) - } - - fn ensure_instruction_block(&mut self, start: u32) -> Result<()> { - let cache_index = instruction_block_cache_index(start); - if self.instruction_blocks[cache_index].len != 0 - && self.instruction_blocks[cache_index].start == start - { - return Ok(()); - } - - let mut instructions = [0; MAX_INSTRUCTION_BLOCK_LEN]; - let mut instruction_count = 0usize; - let mut address = start; - while instruction_count < MAX_INSTRUCTION_BLOCK_LEN { - if address != start && self.is_instruction_block_boundary(address) { - break; - } - instructions[instruction_count] = self.memory.fetch_instruction(address)?; - instruction_count += 1; - address = address.wrapping_add(4); - } - self.instruction_blocks[cache_index] = CachedInstructionBlock { - start, - len: instruction_count as u8, - instructions, - }; - Ok(()) - } - - fn is_instruction_block_boundary(&self, address: u32) -> bool { - if address == TASK_RETURN_ADDRESS - || Some(address) == self.app_main_entry - || Some(address) == self.app_main_init_check_address - { - return true; - } - let (hook_word, hook_mask) = hook_filter_location(address); - self.hook_filter[hook_word] & hook_mask != 0 && self.hooked_addrs.contains_key(&address) - } - - pub(crate) fn clear_instruction_cache(&mut self) { - for block in &mut self.instruction_blocks { - block.len = 0; - } - #[cfg(feature = "jit")] - self.jit.clear(); - } - - /// Enable or disable native translation of hot CPU blocks. - pub fn set_jit_enabled(&mut self, enabled: bool) { - #[cfg(feature = "jit")] - self.jit.set_enabled(enabled); - #[cfg(not(feature = "jit"))] - let _ = enabled; - } - - fn active_context_waiting(&mut self) -> bool { - let cycle_count = self.cycle_count; - let wait = if let Some(task_index) = self.active_task { - self.tasks[task_index].wait - } else { - self.main_wait - }; - match wait { - Some(TaskWait::AudioWrite) if self.audio.can_write() => { - self.clear_active_wait(); - false - } - Some(TaskWait::UntilCycle(deadline)) if cycle_count >= deadline => { - self.clear_active_wait(); - false - } - Some(_) => true, - None => false, - } - } - - fn create_guest_task( - &mut self, - entry: u32, - data_ptr: u32, - stack_ptr: u32, - priority: u32, - ) -> bool { - if entry == 0 || self.tasks.len() >= MAX_GUEST_TASKS { - return false; - } - - let mut cpu = Cpu::new(entry); - cpu.regs.write(4, data_ptr); - cpu.regs.write(25, entry); - cpu.regs.write(29, stack_ptr); - cpu.regs.write(31, TASK_RETURN_ADDRESS); - cpu.start(); - self.tasks.push(GuestTask { - cpu, - priority, - wait: None, - }); - log::debug!( - "Created guest task: entry={entry:#010x}, data={data_ptr:#010x}, stack={stack_ptr:#010x}, priority={priority}" - ); - true - } - - /// Implements the uC/OS-II OSTaskDel semantics used by Dingoo SDK apps. - /// Priority 0xff means the currently executing task. While a guest task - /// runs, its CPU is temporarily installed as `self.cpu`, so stopping that - /// CPU correctly terminates either the guest task or the main application. - fn delete_guest_task(&mut self, priority: u32) -> bool { - const OS_PRIO_SELF: u32 = 0xff; - - if priority == OS_PRIO_SELF { - self.cpu.stop(); - return true; - } - - if let Some(task_index) = self.active_task { - if self.tasks[task_index].priority == priority { - self.cpu.stop(); - return true; - } - } - - if let Some(task) = self - .tasks - .iter_mut() - .find(|task| task.priority == priority) - { - task.cpu.stop(); - return true; - } - - false - } - - fn set_active_wait(&mut self, wait: TaskWait) { - if let Some(task_index) = self.active_task { - self.tasks[task_index].wait = Some(wait); - } else { - self.main_wait = Some(wait); - } - } - - fn delay_active_until(&mut self, deadline: u64) { - if deadline > self.cycle_count { - self.set_active_wait(TaskWait::UntilCycle(deadline)); - } - } - - fn create_semaphore(&mut self, count: u32) -> u32 { - let handle = self.next_semaphore_handle; - self.next_semaphore_handle = self.next_semaphore_handle.wrapping_add(1).max(1); - self.semaphores.insert(handle, count); - handle - } - - fn pend_semaphore(&mut self, handle: u32) -> bool { - let Some(count) = self.semaphores.get_mut(&handle) else { - return false; - }; - if *count > 0 { - *count -= 1; - } else { - self.set_active_wait(TaskWait::Semaphore(handle)); - } - true - } - - fn post_semaphore(&mut self, handle: u32) -> bool { - if !self.semaphores.contains_key(&handle) { - return false; - } - - if self.main_wait == Some(TaskWait::Semaphore(handle)) { - self.main_wait = None; - return true; - } - if let Some(task_index) = self - .tasks - .iter() - .enumerate() - .filter(|(_, task)| task.wait == Some(TaskWait::Semaphore(handle))) - .min_by_key(|(_, task)| task.priority) - .map(|(index, _)| index) - { - self.tasks[task_index].wait = None; - return true; - } - - if let Some(count) = self.semaphores.get_mut(&handle) { - *count = count.saturating_add(1); - } - true - } - - fn read_guest_c_string_bytes(&self, ptr: u32) -> Vec { - let mut bytes = Vec::new(); - let mut offset = 0u32; - while let Ok(b) = self.memory.read_u8(ptr.wrapping_add(offset)) { - if b == 0 { - break; - } - bytes.push(b); - offset = offset.wrapping_add(1); - if bytes.len() >= 1024 { - break; - } - } - bytes - } - - fn read_guest_c_string(&self, ptr: u32) -> String { - String::from_utf8_lossy(&self.read_guest_c_string_bytes(ptr)).into_owned() - } - - fn guest_c_string_hex(&self, ptr: u32) -> String { - self.read_guest_c_string_bytes(ptr) - .iter() - .map(|byte| format!("{byte:02X}")) - .collect::>() - .join(" ") - } - - fn read_guest_w_string_words(&self, ptr: u32) -> Vec { - let mut words = Vec::new(); - let mut offset = 0u32; - while let Ok(w) = self.memory.read_u16(ptr.wrapping_add(offset)) { - if w == 0 { - break; - } - words.push(w); - offset = offset.wrapping_add(2); - if words.len() >= 1024 { - break; - } - } - words - } - - fn read_guest_w_string(&self, ptr: u32) -> String { - String::from_utf16_lossy(&self.read_guest_w_string_words(ptr)) - } - - fn guest_w_string_hex(&self, ptr: u32) -> String { - self.read_guest_w_string_words(ptr) - .iter() - .map(|word| format!("{word:04X}")) - .collect::>() - .join(" ") - } - - fn guest_printf_arg(&self, index: usize) -> Result { - match index { - 0 => Ok(self.cpu.regs.read(6)), - 1 => Ok(self.cpu.regs.read(7)), - _ => { - let stack_offset = 8u32.wrapping_add((index as u32).wrapping_mul(4)); - self.memory - .read_u32(self.cpu.regs.read(29).wrapping_add(stack_offset)) - } - } - } - - fn format_guest_printf(&self, format: &str) -> Result { - let bytes = format.as_bytes(); - let mut output = String::new(); - let mut cursor = 0; - let mut arg_index = 0; - - while cursor < bytes.len() { - if bytes[cursor] != b'%' { - output.push(bytes[cursor] as char); - cursor += 1; - continue; - } - - cursor += 1; - if cursor < bytes.len() && bytes[cursor] == b'%' { - output.push('%'); - cursor += 1; - continue; - } - - let mut left_aligned = false; - let mut show_sign = false; - let mut space_sign = false; - let mut alternate = false; - let mut zero_padded = false; - while cursor < bytes.len() { - match bytes[cursor] { - b'-' => left_aligned = true, - b'+' => show_sign = true, - b' ' => space_sign = true, - b'#' => alternate = true, - b'0' => zero_padded = true, - _ => break, - } - cursor += 1; - } - - let width_start = cursor; - while cursor < bytes.len() && bytes[cursor].is_ascii_digit() { - cursor += 1; - } - let width = if cursor > width_start { - format[width_start..cursor].parse::().ok() - } else { - None - }; - - let precision = if cursor < bytes.len() && bytes[cursor] == b'.' { - cursor += 1; - let precision_start = cursor; - while cursor < bytes.len() && bytes[cursor].is_ascii_digit() { - cursor += 1; - } - Some( - format[precision_start..cursor] - .parse::() - .unwrap_or(0), - ) - } else { - None - }; - - while cursor < bytes.len() - && matches!(bytes[cursor], b'h' | b'l' | b'j' | b'z' | b't' | b'L') - { - cursor += 1; - } - if cursor >= bytes.len() { - output.push('%'); - break; - } - - let conversion = bytes[cursor]; - cursor += 1; - let argument = self.guest_printf_arg(arg_index)?; - arg_index += 1; - - let (mut field, numeric_prefix_len) = match conversion { - b's' => { - let value = self.read_guest_c_string(argument); - let value = precision - .map(|limit| value.chars().take(limit).collect()) - .unwrap_or(value); - (value, 0) - } - b'c' => ((argument as u8 as char).to_string(), 0), - b'd' | b'i' => { - let signed = argument as i32 as i64; - let sign = if signed < 0 { - "-" - } else if show_sign { - "+" - } else if space_sign { - " " - } else { - "" - }; - let digits = signed.unsigned_abs().to_string(); - let digits = Self::apply_integer_precision(digits, precision, argument == 0); - (format!("{sign}{digits}"), sign.len()) - } - b'u' => { - let digits = argument.to_string(); - ( - Self::apply_integer_precision(digits, precision, argument == 0), - 0, - ) - } - b'o' => { - let mut prefix = ""; - let digits = format!("{argument:o}"); - if alternate && !digits.starts_with('0') { - prefix = "0"; - } - let digits = Self::apply_integer_precision(digits, precision, argument == 0); - (format!("{prefix}{digits}"), prefix.len()) - } - b'x' | b'X' | b'p' => { - let uppercase = conversion == b'X'; - let prefix = if conversion == b'p' || (alternate && argument != 0) { - if uppercase { - "0X" - } else { - "0x" - } - } else { - "" - }; - let digits = if uppercase { - format!("{argument:X}") - } else { - format!("{argument:x}") - }; - let digits = Self::apply_integer_precision(digits, precision, argument == 0); - (format!("{prefix}{digits}"), prefix.len()) - } - _ => { - output.push('%'); - output.push(conversion as char); - continue; - } - }; - - if let Some(width) = width { - if field.len() < width { - let padding = width - field.len(); - if left_aligned { - field.push_str(&" ".repeat(padding)); - } else if zero_padded && precision.is_none() && numeric_prefix_len > 0 { - field.insert_str(numeric_prefix_len, &"0".repeat(padding)); - } else { - let padding_char = - if zero_padded && precision.is_none() && numeric_prefix_len == 0 { - '0' - } else { - ' ' - }; - field.insert_str(0, &padding_char.to_string().repeat(padding)); - } - } - } - output.push_str(&field); - } - - Ok(output) - } - - fn apply_integer_precision( - mut digits: String, - precision: Option, - is_zero: bool, - ) -> String { - let Some(precision) = precision else { - return digits; - }; - if precision == 0 && is_zero { - return String::new(); - } - if digits.len() < precision { - digits.insert_str(0, &"0".repeat(precision - digits.len())); - } - digits - } - - fn convert_guest_w_string_to_ansi(&mut self, ptr: u32) -> u32 { - const MAX_CHARS: u32 = 511; - const BUFFER_SIZE: u32 = MAX_CHARS + 1; - - if self.locale_ansi_buffer == Some(ptr) { - return ptr; - } - - let mut bytes = Vec::new(); - for index in 0..MAX_CHARS { - let Ok(word) = self.memory.read_u16(ptr.wrapping_add(index * 2)) else { - return 0; - }; - if word == 0 { - break; - } - bytes.push(if (0x20..=0x7E).contains(&word) { - word as u8 - } else { - b'?' - }); - } - bytes.push(0); - - let output = match self.locale_ansi_buffer { - Some(output) => output, - None => { - let output = self.memory.malloc(BUFFER_SIZE); - if output == 0 { - return 0; - } - self.locale_ansi_buffer = Some(output); - output - } - }; - if self.memory.load_data(output, &bytes).is_err() { - return 0; - } - output - } - - fn resource_name_from_args(&self, args: &[u32]) -> Option { - args.iter().find_map(|&ptr| { - if ptr < 0x10000 { - return None; - } - let name = self.read_guest_c_string(ptr); - (!name.is_empty()).then_some(name) - }) - } - fn open_resource_file(&mut self, name: &str) -> u32 { - let Some(app) = self.app.as_ref() else { - return 0; - }; - let Some(resource) = app.find_resource(name) else { - log::trace!("Resource open failed: {name}"); - return 0; - }; - - let kind = resource.kind; - let resource_data = app.get_resource_data(resource); - let data = prepare_resource_file_data(name, kind, resource_data); - let handle = self.next_file_handle; - self.next_file_handle = self.next_file_handle.wrapping_add(1).max(1); - let size = data.len(); - self.open_files.insert( - handle, - OpenFile { - data, - position: 0, - data_ptr: 0, - save_path: None, - writable: false, - dirty: false, - }, - ); - log::trace!("Resource opened: {name} -> {handle} ({size} bytes)"); - handle - } - - fn open_host_file(&mut self, name: &str) -> u32 { - let path = self.resolve_host_file_path(name); - let Ok(data) = std::fs::read(&path) else { - log::trace!(" host file open failed: {}", name); - return 0; - }; - - let handle = self.next_file_handle; - self.next_file_handle = self.next_file_handle.wrapping_add(1).max(1); - let size = data.len(); - self.open_files.insert( - handle, - OpenFile { - data, - position: 0, - data_ptr: 0, - save_path: None, - writable: false, - dirty: false, - }, - ); - log::trace!(" host file open: {} -> {} ({} bytes)", name, handle, size); - handle - } - - fn resolve_host_file_path(&self, name: &str) -> PathBuf { - let normalized_name = name.replace(['/', '\\'], std::path::MAIN_SEPARATOR_STR); - let path = PathBuf::from(normalized_name); - if path.is_absolute() { - return path; - } - - let Some(separator) = self.app_path.rfind(['/', '\\']) else { - return path; - }; - let app_directory = - self.app_path[..separator].replace(['/', '\\'], std::path::MAIN_SEPARATOR_STR); - Path::new(&app_directory).join(path) - } - - fn begin_file_search(&mut self, pattern: &str, attributes: u32, data_ptr: u32) -> Result { - self.file_searches.remove(&data_ptr); - if data_ptr == 0 { - return Ok(u32::MAX); - } - - let Some(entries) = self.collect_file_search_entries(pattern, attributes) else { - return Ok(u32::MAX); - }; - let Some(first) = entries.first().cloned() else { - return Ok(u32::MAX); - }; - - self.write_file_search_name(data_ptr, &first)?; - self.file_searches.insert( - data_ptr, - FileSearch { - entries, - next_index: 1, - }, - ); - Ok(0) - } - - fn next_file_search(&mut self, data_ptr: u32) -> Result { - let Some(name) = self.file_searches.get_mut(&data_ptr).and_then(|search| { - let name = search.entries.get(search.next_index)?.clone(); - search.next_index += 1; - Some(name) - }) else { - return Ok(u32::MAX); - }; - - self.write_file_search_name(data_ptr, &name)?; - Ok(0) - } - - fn close_file_search(&mut self, data_ptr: u32) -> u32 { - self.file_searches.remove(&data_ptr); - 0 - } - - fn collect_file_search_entries(&self, pattern: &str, attributes: u32) -> Option> { - let normalized = normalize_guest_search_pattern(pattern)?; - let (directory, file_pattern) = normalized - .rsplit_once('/') - .map_or(("", normalized.as_str()), |(directory, pattern)| { - (directory, pattern) - }); - let file_pattern = if file_pattern.is_empty() { - "*" - } else { - file_pattern - }; - - let app_directory = Path::new(&self.app_path).parent().unwrap_or(Path::new(".")); - let search_directory = if directory.is_empty() { - app_directory.to_path_buf() - } else { - app_directory.join(directory.replace('/', std::path::MAIN_SEPARATOR_STR)) - }; - let root = app_directory.canonicalize().ok()?; - let search_directory = search_directory.canonicalize().ok()?; - if !search_directory.starts_with(&root) { - return None; - } - - let find_directories = attributes & 0x10 != 0; - let mut entries = std::fs::read_dir(search_directory) - .ok()? - .filter_map(|entry| entry.ok()) - .filter_map(|entry| { - let file_type = entry.file_type().ok()?; - if (find_directories && !file_type.is_dir()) - || (!find_directories && !file_type.is_file()) - { - return None; - } - let name = entry.file_name().to_string_lossy().into_owned(); - wildcard_matches(file_pattern, &name).then_some(name) - }) - .collect::>(); - entries.sort_by(|left, right| { - left.to_ascii_lowercase() - .cmp(&right.to_ascii_lowercase()) - .then_with(|| left.cmp(right)) - }); - Some(entries) - } - - fn write_file_search_name(&mut self, data_ptr: u32, name: &str) -> Result<()> { - let name = name.as_bytes(); - let length = name.len().min(FILE_SEARCH_NAME_CAPACITY - 1); - let destination = data_ptr.wrapping_add(FILE_SEARCH_NAME_OFFSET); - for (offset, byte) in name.iter().copied().take(length).enumerate() { - self.memory - .write_u8(destination.wrapping_add(offset as u32), byte)?; - } - self.memory - .write_u8(destination.wrapping_add(length as u32), 0)?; - Ok(()) - } - - fn open_memory_file( - &mut self, - data: Vec, - save_path: PathBuf, - append: bool, - writable: bool, - dirty: bool, - ) -> u32 { - let handle = self.next_file_handle; - self.next_file_handle = self.next_file_handle.wrapping_add(1).max(1); - let position = if append { data.len() } else { 0 }; - self.open_files.insert( - handle, - OpenFile { - data, - position, - data_ptr: 0, - save_path: Some(save_path), - writable, - dirty, - }, - ); - handle - } - - fn save_file_path(&self, name: &str) -> Option { - let root = self.save_directory.as_ref()?; - let normalized = name.replace('\\', "/"); - let mut relative = PathBuf::new(); - for (index, component) in normalized.split('/').enumerate() { - if component.is_empty() || component == "." { - continue; - } - if component == ".." || component.contains('\0') { - return None; - } - let component = if index == 0 - && component.len() == 2 - && component.as_bytes()[0].is_ascii_alphabetic() - && component.ends_with(':') - { - &component[..1] - } else { - component - }; - if component.contains(':') { - return None; - } - relative.push(component); - } - (!relative.as_os_str().is_empty()).then(|| root.join(relative)) - } - - fn open_save_file(&mut self, name: &str, operation: u8, writable: bool) -> u32 { - let Some(path) = self.save_file_path(name) else { - log::warn!("Rejected guest save path: {name}"); - return 0; - }; - let data = match operation { - b'w' => Vec::new(), - b'a' | b'r' => match std::fs::read(&path) { - Ok(data) => data, - Err(error) if operation == b'a' && error.kind() == std::io::ErrorKind::NotFound => { - Vec::new() - } - Err(_) => return 0, - }, - _ => return 0, - }; - self.open_memory_file( - data, - path, - operation == b'a', - writable, - writable && operation != b'r', - ) - } - - fn open_guest_file(&mut self, name: &str, mode: &str) -> u32 { - let operation = mode.as_bytes().first().copied().unwrap_or(b'r'); - let writable = operation == b'w' || operation == b'a' || mode.contains('+'); - if writable { - let handle = self.open_save_file(name, operation, true); - if handle != 0 { - return handle; - } - if operation != b'r' { - return 0; - } - } else { - let handle = self.open_save_file(name, b'r', false); - if handle != 0 { - return handle; - } - } - - let handle = match self.open_resource_file(name) { - 0 => self.open_host_file(name), - handle => handle, - }; - if handle == 0 { - // Keep this at warning level: Android's libretro bridge filters - // trace/info logs, and the missing guest filename is essential - // for distinguishing a firmware-return request from an ordinary - // optional resource lookup. - log::warn!("Guest file open failed: {name} (mode: {mode})"); - } - handle - } - - fn flush_save_file(&mut self, handle: u32) -> std::io::Result<()> { - let Some(file) = self.open_files.get_mut(&handle) else { - return Ok(()); - }; - if !file.dirty { - return Ok(()); - } - let Some(path) = file.save_path.as_ref() else { - return Ok(()); - }; - if let Some(parent) = path.parent() { - std::fs::create_dir_all(parent)?; - } - std::fs::write(path, &file.data)?; - file.dirty = false; - Ok(()) - } - - /// Flush all modified guest save files to the configured save directory. - pub fn flush_save_files(&mut self) { - let handles: Vec = self.open_files.keys().copied().collect(); - for handle in handles { - if let Err(error) = self.flush_save_file(handle) { - log::error!("Failed to flush guest save file {handle}: {error}"); - } - } - } - - /// Set the directory used for persistent guest-created files. - pub fn set_save_directory>(&mut self, directory: P) { - self.flush_save_files(); - self.save_directory = Some(directory.into()); - } - - fn read_file(&mut self, dest: u32, size: u32, count: u32, handle: u32) -> Result { - let Some(file) = self.open_files.get_mut(&handle) else { - return Ok(0); - }; - let requested = (size as usize).saturating_mul(count as usize); - if requested == 0 { - return Ok(0); - } - - let remaining = file.data.len().saturating_sub(file.position); - let bytes_to_copy = requested.min(remaining); - for i in 0..bytes_to_copy { - self.memory - .write_u8(dest.wrapping_add(i as u32), file.data[file.position + i])?; - } - file.position += bytes_to_copy; - - if size == 0 { - Ok(0) - } else { - Ok((bytes_to_copy / size as usize) as u32) - } - } - - fn write_file(&mut self, src: u32, size: u32, count: u32, handle: u32) -> Result { - let requested = (size as usize).saturating_mul(count as usize); - if requested == 0 { - return Ok(0); - } - - let mut data = Vec::with_capacity(requested); - for offset in 0..requested { - data.push(self.memory.read_u8(src.wrapping_add(offset as u32))?); - } - - let Some(file) = self.open_files.get_mut(&handle) else { - return Ok(0); - }; - if !file.writable { - return Ok(0); - } - let end = file.position.saturating_add(requested); - if file.data.len() < end { - file.data.resize(end, 0); - } - file.data[file.position..end].copy_from_slice(&data); - file.position = end; - file.dirty = true; - - Ok(count) - } - - fn read_resource_data( - &mut self, - handle: u32, - buffer: u32, - buffer_len: u32, - read_len: u32, - ) -> Result { - let Some(file) = self.open_files.get_mut(&handle) else { - return Ok(0); - }; - - if buffer == 0 { - if file.data_ptr == 0 { - let ptr = self.memory.malloc(file.data.len() as u32); - if ptr == 0 { - return Ok(0); - } - for (i, &byte) in file.data.iter().enumerate() { - self.memory.write_u8(ptr.wrapping_add(i as u32), byte)?; - } - file.data_ptr = ptr; - } - return Ok(file.data_ptr); - } - - let remaining = file.data.len().saturating_sub(file.position); - let mut copy_size = if read_len != 0 && buffer_len > 1 { - (read_len as usize).saturating_mul(buffer_len as usize) - } else if read_len != 0 { - read_len as usize - } else { - buffer_len as usize - }; - if copy_size == 0 || copy_size > remaining { - copy_size = remaining; - } - - for i in 0..copy_size { - self.memory - .write_u8(buffer.wrapping_add(i as u32), file.data[file.position + i])?; - } - file.position += copy_size; - - if read_len != 0 { - Ok((copy_size / read_len as usize) as u32) - } else { - Ok(copy_size as u32) - } - } - fn seek_file(&mut self, handle: u32, offset: i32, origin: u32) -> u32 { - let Some(file) = self.open_files.get_mut(&handle) else { - return u32::MAX; - }; - - let base = match origin { - 0 => 0i64, - 1 => file.position as i64, - 2 => file.data.len() as i64, - _ => return u32::MAX, - }; - let next = base + offset as i64; - if next < 0 { - return u32::MAX; - } - file.position = (next as usize).min(file.data.len()); - 0 - } - - /// Sync framebuffer from guest memory to video subsystem - /// The game writes directly to the fixed framebuffer address - fn sync_framebuffer(&mut self) { - let fb_data = &self.memory.framebuffer()[..crate::video::FRAMEBUFFER_SIZE]; - - self.framebuffer_submitted = true; - let dst = self.video.framebuffer_mut(); - dst.copy_from_slice(fb_data); - if log::log_enabled!(log::Level::Trace) { - let non_zero_count = fb_data.iter().filter(|&&byte| byte != 0).count(); - log::trace!( - " sync_framebuffer: {}/{} non-zero bytes", - non_zero_count, - fb_data.len() - ); - } - } - - fn clear_active_wait(&mut self) { - if let Some(task_index) = self.active_task { - self.tasks[task_index].wait = None; - } else { - self.main_wait = None; - } - } - - /// Set the button state - pub fn set_buttons(&mut self, buttons: u32) { - self.input.set_buttons(buttons); - } - - /// Install or update a frontend cheat slot. - pub fn set_cheat( - &mut self, - index: u32, - enabled: bool, - code: &str, - ) -> std::result::Result<(), CheatParseError> { - self.cheats.set_slot(index, enabled, code, &self.memory)?; - self.clear_instruction_cache(); - Ok(()) - } - - /// Install an already parsed cheat rule. - pub fn set_parsed_cheat( - &mut self, - index: u32, - enabled: bool, - rule: CheatRule, - ) -> std::result::Result<(), CheatParseError> { - self.cheats - .set_parsed_rule(index, enabled, rule, &self.memory)?; - self.clear_instruction_cache(); - Ok(()) - } - - /// Remove every configured cheat slot. - pub fn clear_cheats(&mut self) { - self.cheats.clear(); - self.clear_instruction_cache(); - } - - /// Get one video frame of interleaved stereo audio. - pub fn take_audio_samples(&mut self) -> Vec { - self.audio.take_frame_samples() - } - - /// Get the fixed frontend audio sample rate. - pub fn audio_sample_rate(&self) -> u32 { - crate::audio::OUTPUT_SAMPLE_RATE - } - - /// Get the current frame count - pub fn frame_count(&self) -> u64 { - self.frame_count - } - - /// Check if the emulator is running - pub fn is_running(&self) -> bool { - self.cpu.is_running() - } - - /// Get the app image (for resource access) - pub fn app(&self) -> Option<&AppImage> { - self.app.as_ref() - } -} - -fn safe_relative_path(path: &Path) -> bool { - !path.as_os_str().is_empty() - && path - .components() - .all(|component| matches!(component, Component::Normal(_) | Component::CurDir)) -} - -fn normalize_guest_search_pattern(pattern: &str) -> Option { - let mut normalized = pattern.replace('\\', "/"); - if normalized.as_bytes().get(1) == Some(&b':') - && normalized - .as_bytes() - .first() - .is_some_and(u8::is_ascii_alphabetic) - { - normalized.drain(..2); - } - let normalized = normalized.trim_start_matches('/'); - let path = Path::new(normalized); - if path - .components() - .any(|component| !matches!(component, Component::Normal(_) | Component::CurDir)) - { - return None; - } - Some(normalized.to_string()) -} - -fn wildcard_matches(pattern: &str, name: &str) -> bool { - let pattern = if pattern.eq_ignore_ascii_case("*.*") { - "*" - } else { - pattern - }; - let pattern = pattern.as_bytes(); - let name = name.as_bytes(); - let (mut pattern_index, mut name_index) = (0, 0); - let (mut star_index, mut retry_name_index) = (None, 0); - - while name_index < name.len() { - if pattern_index < pattern.len() - && (pattern[pattern_index] == b'?' - || pattern[pattern_index].eq_ignore_ascii_case(&name[name_index])) - { - pattern_index += 1; - name_index += 1; - } else if pattern_index < pattern.len() && pattern[pattern_index] == b'*' { - star_index = Some(pattern_index); - pattern_index += 1; - retry_name_index = name_index; - } else if let Some(star) = star_index { - retry_name_index += 1; - name_index = retry_name_index; - pattern_index = star + 1; - } else { - return false; - } - } - - while pattern_index < pattern.len() && pattern[pattern_index] == b'*' { - pattern_index += 1; - } - pattern_index == pattern.len() -} - -impl Default for Emulator { - fn default() -> Self { - Self { - cpu: Cpu::new(0x8000_0000), - memory: Memory::new(), - video: Video::new(), - input: Input::new(), - audio: Audio::new(), - cheats: CheatManager::default(), - frame_count: 0, - cycle_count: 0, - tasks: Vec::new(), - scheduler_cursor: 0, - main_wait: None, - active_task: None, - semaphores: HashMap::new(), - next_semaphore_handle: 1, - app: None, - import_addrs: HashMap::new(), - hooked_addrs: HashMap::new(), - unknown_hle_policy: UnknownHlePolicy::default(), - unknown_hle_allowlist: BTreeSet::new(), - unknown_hle_calls: BTreeMap::new(), - hook_filter: vec![0; HOOK_FILTER_WORDS].into_boxed_slice(), - instruction_blocks: empty_instruction_block_cache(), - #[cfg(feature = "jit")] - jit: JitEngine::new(), - open_files: HashMap::new(), - save_directory: None, - next_file_handle: 1, - file_searches: HashMap::new(), - gui: GuiState::default(), - app_main_entry: None, - app_main_init_check_address: None, - app_main_args_initialized: false, - app_path: String::new(), - locale_ansi_buffer: None, - framebuffer_submitted: false, - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - - fn minimal_app() -> AppImage { - let mut data = vec![0u8; 132]; - data[0..4].copy_from_slice(b"CCDL"); - data[0x20..0x24].copy_from_slice(b"IMPT"); - data[0x40..0x44].copy_from_slice(b"EXPT"); - data[0x60..0x64].copy_from_slice(b"RAWD"); - data[0x68..0x6c].copy_from_slice(&128u32.to_le_bytes()); - data[0x6c..0x70].copy_from_slice(&4u32.to_le_bytes()); - data[0x74..0x78].copy_from_slice(&0x8000_0000u32.to_le_bytes()); - data[0x78..0x7c].copy_from_slice(&0x8000_0000u32.to_le_bytes()); - data[0x7c..0x80].copy_from_slice(&4u32.to_le_bytes()); - AppImage::parse(&data).unwrap() - } - - fn try_invoke_sdk_import(emu: &mut Emulator, address: u32, function_name: &str) -> Result { - emu.hooked_addrs.insert(address, function_name.to_string()); - let (word, mask) = hook_filter_location(address); - emu.hook_filter[word] |= mask; - emu.cpu.regs.pc = address; - emu.cpu.start(); - - emu.run_active_cpu_slice(CPU_CYCLES_PER_INSTRUCTION) - } - - fn invoke_sdk_import(emu: &mut Emulator, address: u32, function_name: &str) { - assert_eq!( - try_invoke_sdk_import(emu, address, function_name).unwrap(), - CPU_CYCLES_PER_INSTRUCTION - ); - } - - #[test] - fn test_emulator_creation() { - let emu = Emulator::default(); - assert_eq!(emu.frame_count(), 0); - assert!(!emu.is_running()); - } - - #[test] - fn unknown_hle_calls_are_aggregated_by_name() { - let mut emu = Emulator::default(); - emu.cpu.regs.write(31, 0x8000_1008); - for (register, value) in (4..=7).zip([1, 2, 3, 4]) { - emu.cpu.regs.write(register, value); - } - - invoke_sdk_import(&mut emu, 0x1000, "missing_sdk_call"); - emu.cpu.regs.write(31, 0x8000_2008); - emu.cpu.regs.write(4, 99); - invoke_sdk_import(&mut emu, 0x1004, "missing_sdk_call"); - - let calls: Vec<_> = emu.unknown_hle_calls().cloned().collect(); - assert_eq!( - calls, - vec![UnknownHleCall { - name: "missing_sdk_call".to_string(), - count: 2, - import_address: 0x1000, - first_pc: 0x8000_1000, - first_arguments: [1, 2, 3, 4], - }] - ); - assert_eq!(emu.cpu.regs.read(2), 0); - assert_eq!(emu.cpu.regs.pc, 0x8000_2008); - } - - #[test] - fn strict_unknown_hle_policy_stops_and_keeps_diagnostics() { - let mut emu = Emulator::default(); - emu.set_unknown_hle_policy(UnknownHlePolicy::Stop); - emu.cpu.regs.write(31, 0x8000_3008); - emu.cpu.regs.write(4, 0x1234); - - let error = try_invoke_sdk_import(&mut emu, 0x2000, "strict_missing").unwrap_err(); - assert!(matches!( - error, - SimulatorError::UnknownHle { - ref name, - pc: 0x8000_3000, - import_address: 0x2000, - arguments: [0x1234, 0, 0, 0], - } if name == "strict_missing" - )); - assert_eq!(emu.unknown_hle_calls().len(), 1); - } - - #[test] - fn strict_unknown_hle_allowlist_is_exact_and_survives_reset() { - let mut emu = Emulator::from_app(minimal_app()).unwrap(); - emu.set_unknown_hle_policy(UnknownHlePolicy::Stop); - emu.set_unknown_hle_allowlist(["allowed_missing"]); - emu.cpu.regs.write(31, 0x8000_4008); - - invoke_sdk_import(&mut emu, 0x3000, "allowed_missing"); - assert_eq!(emu.unknown_hle_calls().len(), 1); - - emu.reset().unwrap(); - assert_eq!(emu.unknown_hle_policy, UnknownHlePolicy::Stop); - assert!(emu.unknown_hle_allowlist.contains("allowed_missing")); - assert_eq!(emu.unknown_hle_calls().len(), 0); - - emu.cpu.regs.write(31, 0x8000_5008); - invoke_sdk_import(&mut emu, 0x3004, "allowed_missing"); - assert!(try_invoke_sdk_import(&mut emu, 0x3008, "Allowed_Missing").is_err()); - } - - #[test] - fn test_legacy_load_base_requests_lcd_initialization() { - let legacy = Emulator::from_app(minimal_app()).unwrap(); - assert_eq!(legacy.cpu.regs.read(5), 1); - - let mut standard_app = minimal_app(); - standard_app.rawd.origin = STANDARD_APP_LOAD_BASE; - let standard = Emulator::from_app(standard_app).unwrap(); - assert_eq!(standard.cpu.regs.read(5), 0); - } - - #[test] - fn test_reset_rebuilds_loaded_runtime_state() { - let mut emu = Emulator::from_app(minimal_app()).unwrap(); - emu.start(); - emu.memory.write_u32(0x1000, 0x1234_5678).unwrap(); - emu.set_buttons(crate::input::BUTTON_A); - emu.frame_count = 42; - emu.cycle_count = 123; - - emu.reset().unwrap(); - - assert!(emu.is_running()); - assert_eq!(emu.cpu.regs.pc, 0x8000_0000); - assert_eq!(emu.memory.read_u32(0x1000).unwrap(), 0); - assert_eq!(emu.input.buttons(), 0); - assert_eq!(emu.frame_count, 0); - assert_eq!(emu.cycle_count, 0); - } - - #[test] - fn test_guest_task_executes_with_shared_memory() { - let mut emu = Emulator::default(); - let entry = 0x1000; - let addiu_t0 = (0x09 << 26) | (8 << 16) | 0x1234; - let sw_t0 = (0x2B << 26) | (8 << 16) | 0x2000; - let jr_ra = (31 << 21) | 0x08; - emu.memory.write_u32(entry, addiu_t0).unwrap(); - emu.memory.write_u32(entry + 4, sw_t0).unwrap(); - emu.memory.write_u32(entry + 8, jr_ra).unwrap(); - emu.memory.write_u32(entry + 12, 0).unwrap(); - - assert!(emu.create_guest_task(entry, 0, 0x3000, 16)); - emu.tick().unwrap(); - - assert_eq!(emu.memory.read_u32(0x2000).unwrap(), 0x1234); - assert!(emu.tasks.is_empty()); - } - - #[test] - fn test_tick_uses_interpreter_cycle_cost() { - let mut emu = Emulator::default(); - emu.start(); - - emu.tick().unwrap(); - - assert_eq!( - emu.cpu.instruction_count, - CYCLES_PER_FRAME / CPU_CYCLES_PER_INSTRUCTION - ); - assert_eq!(emu.cycle_count, CYCLES_PER_FRAME); - } - - #[cfg(feature = "jit")] - #[test] - #[ignore = "manual JIT throughput benchmark"] - fn benchmark_jit_hot_integer_loop() { - fn make_emulator(jit_enabled: bool, block_count: u32) -> Emulator { - let mut emu = Emulator::default(); - for block_index in 0..block_count { - let block_address = 0x8000_0000 + block_index * 64; - let next_address = 0x8000_0000 + ((block_index + 1) % block_count) * 64; - let instructions = [ - (0x09 << 26) | (8 << 21) | (8 << 16) | 1, - (9 << 21) | (8 << 16) | (9 << 11) | 0x21, - (9 << 21) | (8 << 16) | (10 << 11) | 0x26, - (10 << 21) | (8 << 16) | (11 << 11) | 0x25, - (11 << 16) | (12 << 11) | (3 << 6), - (12 << 16) | (13 << 11) | (2 << 6) | 0x02, - (13 << 21) | (8 << 16) | (14 << 11) | 0x24, - (0x2b << 26) | (14 << 16) | 0x0200, - (0x23 << 26) | (15 << 16) | 0x0200, - (15 << 21) | (8 << 16) | (16 << 11) | 0x23, - (0x0d << 26) | (16 << 21) | (17 << 16) | 0x55aa, - (0x0e << 26) | (17 << 21) | (18 << 16) | 0xa55a, - (18 << 21) | (8 << 16) | (19 << 11) | 0x2b, - (19 << 21) | (9 << 16) | (20 << 11) | 0x21, - (0x02 << 26) | ((next_address >> 2) & 0x03ff_ffff), - 0, - ]; - for (index, instruction) in instructions.into_iter().enumerate() { - emu.memory - .write_u32(block_address + (index as u32) * 4, instruction) - .unwrap(); - } - } - emu.set_jit_enabled(jit_enabled); - emu.start(); - emu - } - - fn measure(mut emu: Emulator) -> (std::time::Duration, std::time::Duration) { - let cold_start = std::time::Instant::now(); - emu.tick().unwrap(); - let cold = cold_start.elapsed(); - let warm_start = std::time::Instant::now(); - for _ in 0..5 { - emu.tick().unwrap(); - } - (cold, warm_start.elapsed()) - } - - let (interpreter_cold, interpreter_warm) = measure(make_emulator(false, 1)); - let (jit_cold, jit_warm) = measure(make_emulator(true, 1)); - eprintln!( - "cold: interpreter={interpreter_cold:?} jit={jit_cold:?} ratio={:.2}x; warm: interpreter={interpreter_warm:?} jit={jit_warm:?} speedup={:.2}x", - jit_cold.as_secs_f64() / interpreter_cold.as_secs_f64(), - interpreter_warm.as_secs_f64() / jit_warm.as_secs_f64() - ); - let (many_interpreter_cold, many_interpreter_warm) = measure(make_emulator(false, 64)); - let (many_jit_cold, many_jit_warm) = measure(make_emulator(true, 64)); - eprintln!( - "64 blocks cold: interpreter={many_interpreter_cold:?} jit={many_jit_cold:?} ratio={:.2}x; warm: interpreter={many_interpreter_warm:?} jit={many_jit_warm:?} ratio={:.2}x", - many_jit_cold.as_secs_f64() / many_interpreter_cold.as_secs_f64(), - many_jit_warm.as_secs_f64() / many_interpreter_warm.as_secs_f64() - ); - } - - #[test] - fn test_tick_stops_after_framebuffer_submission() { - let mut emu = Emulator::default(); - let hook_address = 0x1000; - emu.hooked_addrs - .insert(hook_address, "lcd_set_frame".to_string()); - let (word, mask) = hook_filter_location(hook_address); - emu.hook_filter[word] |= mask; - emu.video.framebuffer_mut().fill(0xff); - emu.cpu.regs.pc = hook_address; - emu.cpu.regs.write(31, hook_address + 4); - emu.start(); - - emu.tick().unwrap(); - - assert_eq!(emu.cpu.regs.pc, hook_address + 4); - assert_eq!(emu.cpu.instruction_count, 0); - assert_eq!(emu.cycle_count, CYCLES_PER_FRAME); - assert!(emu.video.framebuffer().iter().all(|&byte| byte == 0)); - } - - #[test] - fn test_instruction_blocks_stop_before_sdk_hooks() { - let mut emu = Emulator::default(); - let hook_address = 0x1020; - emu.hooked_addrs - .insert(hook_address, "lcd_set_frame".to_string()); - let (word, mask) = hook_filter_location(hook_address); - emu.hook_filter[word] |= mask; - - emu.ensure_instruction_block(0x1000).unwrap(); - - let cache_index = instruction_block_cache_index(0x1000); - assert_eq!(emu.instruction_blocks[cache_index].len, 8); - } - - #[test] - fn test_instruction_cache_is_cleared_by_guest_invalidation() { - let mut emu = Emulator::default(); - let cache_index = instruction_block_cache_index(0x1000); - emu.instruction_blocks[cache_index] = CachedInstructionBlock { - start: 0x1000, - len: 1, - instructions: [0; MAX_INSTRUCTION_BLOCK_LEN], - }; - - invoke_sdk_import(&mut emu, 0x2000, "__icache_invalidate_all"); - - assert!(emu.instruction_blocks.iter().all(|block| block.len == 0)); - } - - #[test] - fn test_hle_modules_execute_through_runtime_hooks() { - let mut emu = Emulator::default(); - let return_address = 0x9000; - emu.cpu.regs.write(31, return_address); - - emu.memory.load_data(0x100, b"hello\0").unwrap(); - emu.cpu.regs.write(4, 0x100); - invoke_sdk_import(&mut emu, 0x1000, "strlen"); - assert_eq!(emu.cpu.regs.read(2), 5); - assert_eq!(emu.cpu.regs.pc, return_address); - - invoke_sdk_import(&mut emu, 0x1004, "LCD_GetXSize"); - assert_eq!(emu.cpu.regs.read(2), crate::video::SCREEN_WIDTH); - - emu.set_buttons(crate::input::BUTTON_A); - invoke_sdk_import(&mut emu, 0x1008, "kbd_get_key"); - assert_eq!(emu.cpu.regs.read(2), crate::input::BUTTON_A); - - invoke_sdk_import(&mut emu, 0x100c, "pcm_ioctl"); - assert_eq!(emu.cpu.regs.read(2), 0); - - emu.cpu.regs.write(4, 2); - invoke_sdk_import(&mut emu, 0x1010, "OSSemCreate"); - assert_eq!(emu.semaphores.get(&emu.cpu.regs.read(2)), Some(&2)); - - invoke_sdk_import(&mut emu, 0x1014, "get_dl_handle"); - assert_eq!(emu.cpu.regs.read(2), 0); - } - - #[test] - fn test_gui_exec_dispatches_key_transitions_to_focused_guest_window() { - let mut emu = Emulator::default(); - let return_address = 0x9000; - let callback = 0x4000; - let stack_pointer = 0x3000; - emu.cpu.regs.write(29, stack_pointer); - emu.cpu.regs.write(31, return_address); - emu.memory.write_u32(stack_pointer + 20, callback).unwrap(); - - invoke_sdk_import(&mut emu, 0x1100, "WM_CreateWindow"); - let window = emu.cpu.regs.read(2); - assert_ne!(window, 0); - - emu.cpu.regs.write(4, window); - invoke_sdk_import(&mut emu, 0x1104, "WM_SetFocus"); - invoke_sdk_import(&mut emu, 0x1108, "open_gui_key_msg"); - - emu.set_buttons(crate::input::BUTTON_RIGHT); - invoke_sdk_import(&mut emu, 0x110c, "GUI_Exec"); - assert_eq!(emu.cpu.regs.pc, callback); - assert_eq!(emu.cpu.regs.read(31), return_address); - let message = emu.cpu.regs.read(4); - assert_eq!(emu.memory.read_u32(message).unwrap(), 14); - assert_eq!(emu.memory.read_u32(message + 4).unwrap(), window); - let key_info = emu.memory.read_u32(message + 8).unwrap(); - assert_eq!(emu.memory.read_u32(key_info).unwrap(), 18); - assert_eq!(emu.memory.read_u32(key_info + 4).unwrap(), 1); - - emu.cpu.regs.write(31, return_address); - invoke_sdk_import(&mut emu, 0x110c, "GUI_Exec"); - assert_eq!(emu.cpu.regs.pc, return_address); - - emu.set_buttons(0); - emu.cpu.regs.write(31, return_address); - invoke_sdk_import(&mut emu, 0x110c, "GUI_Exec"); - assert_eq!(emu.cpu.regs.pc, callback); - assert_eq!(emu.memory.read_u32(key_info).unwrap(), 18); - assert_eq!(emu.memory.read_u32(key_info + 4).unwrap(), 0); - } - - #[test] - fn test_semaphore_wakes_waiting_main_task() { - let mut emu = Emulator::default(); - let semaphore = emu.create_semaphore(0); - - assert!(emu.pend_semaphore(semaphore)); - assert_eq!(emu.main_wait, Some(TaskWait::Semaphore(semaphore))); - assert!(emu.post_semaphore(semaphore)); - assert_eq!(emu.main_wait, None); - } - - #[test] - fn test_packed_bin_resource_view_inserts_header() { - let mut data = vec![0; 24]; - data[0..2].copy_from_slice(&1_u16.to_le_bytes()); - data[4..8].copy_from_slice(&0x1122_3344_u32.to_le_bytes()); - data[8..12].copy_from_slice(&0x260a_1300_u32.to_le_bytes()); - data[12..24].copy_from_slice(&[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]); - - let view = prepare_resource_file_data("brick.bin", ResourceKind::Packed, data); - - assert_eq!(view.len(), 40); - assert_eq!(u32::from_le_bytes(view[0..4].try_into().unwrap()), 1); - assert_eq!(u32::from_le_bytes(view[4..8].try_into().unwrap()), 0); - assert_eq!(u32::from_le_bytes(view[8..12].try_into().unwrap()), 20); - assert_eq!(&view[12..16], &0x1122_3344_u32.to_le_bytes()); - assert_eq!(&view[16..20], &[0; 4]); - assert_eq!(&view[20..32], &[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]); - assert_eq!(&view[32..40], &[0; 8]); - } - - #[test] - fn test_regular_resource_data_remains_unchanged() { - let mut data = vec![0x5a; 12]; - data[8..12].copy_from_slice(&12_u32.to_le_bytes()); - - assert_eq!( - prepare_resource_file_data("image.bin", ResourceKind::Packed, data.clone()), - data - ); - } - - #[test] - fn test_guest_timers_advance_with_emulated_cycles() { - let mut emu = Emulator { - cycle_count: CPU_CLOCK_HZ / OS_TICKS_PER_SECOND, - ..Default::default() - }; - - invoke_sdk_import(&mut emu, 0, "OSTimeGet"); - assert_eq!(emu.cpu.regs.read(2), 1); - - invoke_sdk_import(&mut emu, 0, "GetTickCount"); - assert_eq!(emu.cpu.regs.read(2), 10_000); - } - - #[test] - fn test_sprintf_builds_guest_path() { - let mut emu = Emulator::default(); - let destination = 0x8001_0000; - let format = 0x8001_0100; - let directory = 0x8001_0200; - emu.memory.load_data(format, b"%ssplash.tga\0").unwrap(); - emu.memory.load_data(directory, b"games/astro/\0").unwrap(); - emu.cpu.regs.write(4, destination); - emu.cpu.regs.write(5, format); - emu.cpu.regs.write(6, directory); - - invoke_sdk_import(&mut emu, 0, "sprintf"); - - assert_eq!( - emu.read_guest_c_string(destination), - "games/astro/splash.tga" - ); - assert_eq!(emu.cpu.regs.read(2), 22); - } - - #[test] - fn test_sprintf_reads_stack_varargs() { - let mut emu = Emulator::default(); - let destination = 0x8001_0000; - let format = 0x8001_0100; - let stack = 0x8001_1000; - emu.memory - .load_data(format, b"Ver: %lu.%lu.%04lu\0") - .unwrap(); - emu.cpu.regs.write(4, destination); - emu.cpu.regs.write(5, format); - emu.cpu.regs.write(6, 1); - emu.cpu.regs.write(7, 2); - emu.cpu.regs.write(29, stack); - emu.memory.write_u32(stack + 16, 3).unwrap(); - - invoke_sdk_import(&mut emu, 0, "sprintf"); - - assert_eq!(emu.read_guest_c_string(destination), "Ver: 1.2.0003"); - assert_eq!(emu.cpu.regs.read(2), 13); - } - - #[test] - fn test_app_main_receives_file_name() { - let mut emu = Emulator { - app_path: "games/astro/Astro-Lander.app".to_string(), - ..Default::default() - }; - - emu.install_app_main_args().unwrap(); - - assert_eq!( - emu.read_guest_w_string(emu.cpu.regs.read(4)), - "Astro-Lander.app" - ); - } - - #[test] - fn test_host_file_path_resolves_from_app_directory() { - let emu = Emulator { - app_path: "games/astro/Astro-Lander.app".to_string(), - ..Default::default() - }; - - assert_eq!( - emu.resolve_host_file_path(r"assets\splash.tga"), - Path::new("games") - .join("astro") - .join("assets") - .join("splash.tga") - ); - } - - #[test] - fn test_to_locale_ansi_preserves_input_and_reuses_output_buffer() { - let mut emu = Emulator::default(); - let ptr = 0x100; - for (index, word) in "Ali中.app\0".encode_utf16().enumerate() { - emu.memory - .write_u16(ptr + (index as u32 * 2), word) - .unwrap(); - } - emu.cpu.regs.write(4, ptr); - emu.cpu.regs.write(31, 0x1234); - - invoke_sdk_import(&mut emu, 0, "__to_locale_ansi"); - - let output = emu.cpu.regs.read(2); - assert_ne!(output, ptr); - assert_eq!(emu.cpu.regs.pc, 0x1234); - assert_eq!( - (0..9) - .map(|offset| emu.memory.read_u8(output + offset).unwrap()) - .collect::>(), - b"Ali?.app\0" - ); - assert_eq!(emu.read_guest_w_string(ptr), "Ali中.app"); - - emu.cpu.regs.write(4, ptr); - invoke_sdk_import(&mut emu, 0, "__to_locale_ansi"); - - assert_eq!(emu.cpu.regs.read(2), output); - assert_eq!(emu.read_guest_c_string(output), "Ali?.app"); - } - - #[test] - fn test_get_system_model_writes_a320_as_utf16() { - let mut emu = Emulator::default(); - let ptr = 0x100; - emu.cpu.regs.write(4, ptr); - emu.cpu.regs.write(31, 0x1234); - - invoke_sdk_import(&mut emu, 0, "cmGetSysModel"); - - assert_eq!(emu.cpu.regs.read(2), 0); - assert_eq!(emu.cpu.regs.pc, 0x1234); - assert_eq!(emu.read_guest_w_string(ptr), "A320"); - } - - #[test] - fn test_u8_conversion_aliases_read_little_endian_values() { - let mut emu = Emulator::default(); - let ptr = 0x100; - emu.memory - .load_data(ptr, &[0x18, 0xC2, 0x01, 0x00]) - .unwrap(); - emu.cpu.regs.write(4, ptr); - - for name in ["U8TOU16", "U8TOX16"] { - invoke_sdk_import(&mut emu, 0, name); - assert_eq!(emu.cpu.regs.read(2), 0xC218); - } - - for name in ["U8TOU32", "U8TOX32"] { - invoke_sdk_import(&mut emu, 0, name); - assert_eq!(emu.cpu.regs.read(2), 0x0001_C218); - } - } - - #[test] - fn test_lcd_size_matches_a320_display() { - let mut emu = Emulator::default(); - - invoke_sdk_import(&mut emu, 0, "LCD_GetXSize"); - assert_eq!(emu.cpu.regs.read(2), crate::video::SCREEN_WIDTH); - - invoke_sdk_import(&mut emu, 0, "LCD_GetYSize"); - assert_eq!(emu.cpu.regs.read(2), crate::video::SCREEN_HEIGHT); - } - - #[cfg(not(feature = "standalone"))] - #[test] - fn test_waveout_hle_opens_and_queues_guest_pcm() { - let mut emu = Emulator::default(); - let args_ptr = 0x1000; - emu.memory.write_u32(args_ptr, 16_000).unwrap(); - emu.memory.write_u16(args_ptr + 4, 16).unwrap(); - emu.memory.write_u8(args_ptr + 6, 1).unwrap(); - emu.memory.write_u8(args_ptr + 7, 100).unwrap(); - emu.cpu.regs.write(4, args_ptr); - invoke_sdk_import(&mut emu, 0, "waveout_open"); - - assert_eq!(emu.cpu.regs.read(2), 1); - assert_eq!(emu.audio.config(), AudioConfig::new(16_000, 16, 1, 100)); - - let buffer_ptr = 0x2000; - for index in 0..1_600u32 { - let sample = if index % 2 == 0 { - 10_000i16 - } else { - -10_000i16 - }; - emu.memory - .write_u16(buffer_ptr + index * 2, sample as u16) - .unwrap(); - } - emu.cpu.regs.write(5, buffer_ptr); - emu.cpu.regs.write(6, 3_200); - invoke_sdk_import(&mut emu, 0, "waveout_write"); - - assert_eq!(emu.cpu.regs.read(2), 1); - assert!(emu.take_audio_samples().iter().any(|&sample| sample != 0)); - } - - #[cfg(not(feature = "standalone"))] - #[test] - fn test_waveout_write_retries_after_queue_space_is_available() { - let mut emu = Emulator::default(); - let config = AudioConfig::new(16_000, 16, 1, 100).unwrap(); - assert!(emu.audio.open(config)); - assert!(emu.audio.write(&vec![0; 32_000])); - assert!(!emu.audio.can_write()); - - let hook_address = 0x4000; - let return_address = 0x1234; - let buffer_ptr = 0x2000; - emu.memory - .load_data(buffer_ptr, &1_000i16.to_le_bytes()) - .unwrap(); - emu.cpu.regs.pc = hook_address; - emu.cpu.regs.write(2, 0xdead_beef); - emu.cpu.regs.write(5, buffer_ptr); - emu.cpu.regs.write(6, 2); - emu.cpu.regs.write(31, return_address); - - invoke_sdk_import(&mut emu, hook_address, "waveout_write"); - - assert_eq!(emu.main_wait, Some(TaskWait::AudioWrite)); - assert_eq!(emu.cpu.regs.pc, hook_address); - assert_eq!(emu.cpu.regs.read(2), 0xdead_beef); - - for _ in 0..60 { - if emu.audio.can_write() { - break; - } - emu.take_audio_samples(); - } - assert!(emu.audio.can_write()); - assert!(!emu.active_context_waiting()); - - invoke_sdk_import(&mut emu, hook_address, "waveout_write"); - - assert_eq!(emu.main_wait, None); - assert_eq!(emu.cpu.regs.pc, return_address); - assert_eq!(emu.cpu.regs.read(2), 1); - } - - #[test] - fn test_writable_file_persists_and_reopens() { - let mut emu = Emulator::default(); - let save_directory = std::env::temp_dir().join(format!( - "dingooemu-save-test-{}-{}", - std::process::id(), - std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .unwrap() - .as_nanos() - )); - emu.set_save_directory(&save_directory); - emu.memory.load_data(0x100, b"test.log\0").unwrap(); - emu.memory.load_data(0x120, b"w\0").unwrap(); - emu.memory.load_data(0x140, b"abcdef").unwrap(); - emu.cpu.regs.write(4, 0x100); - emu.cpu.regs.write(5, 0x120); - - invoke_sdk_import(&mut emu, 0, "fsys_fopen"); - let handle = emu.cpu.regs.read(2); - assert_ne!(handle, 0); - - emu.cpu.regs.write(4, 0x140); - emu.cpu.regs.write(5, 2); - emu.cpu.regs.write(6, 3); - emu.cpu.regs.write(7, handle); - invoke_sdk_import(&mut emu, 0, "fsys_fwrite"); - - assert_eq!(emu.cpu.regs.read(2), 3); - assert_eq!(emu.open_files[&handle].data, b"abcdef"); - - emu.cpu.regs.write(4, handle); - invoke_sdk_import(&mut emu, 0, "fsys_fclose"); - assert_eq!(emu.cpu.regs.read(2), 0); - assert_eq!( - std::fs::read(save_directory.join("test.log")).unwrap(), - b"abcdef" - ); - - emu.memory.load_data(0x120, b"r\0").unwrap(); - emu.cpu.regs.write(4, 0x100); - emu.cpu.regs.write(5, 0x120); - invoke_sdk_import(&mut emu, 0, "fsys_fopen"); - let reopened = emu.cpu.regs.read(2); - assert_eq!(emu.open_files[&reopened].data, b"abcdef"); - - std::fs::remove_dir_all(save_directory).unwrap(); - } - - #[test] - fn test_read_only_file_can_be_read_but_not_written() { - let mut emu = Emulator::default(); - let handle = 7; - emu.open_files.insert( - handle, - OpenFile { - data: b"resource".to_vec(), - position: 0, - data_ptr: 0, - save_path: None, - writable: false, - dirty: false, - }, - ); - - assert_eq!(emu.read_file(0x100, 1, 8, handle).unwrap(), 8); - let read_back = (0..8) - .map(|offset| emu.memory.read_u8(0x100 + offset).unwrap()) - .collect::>(); - assert_eq!(read_back, b"resource"); - - emu.open_files.get_mut(&handle).unwrap().position = 0; - emu.memory.load_data(0x200, b"modified").unwrap(); - assert_eq!(emu.write_file(0x200, 1, 8, handle).unwrap(), 0); - assert_eq!(emu.open_files[&handle].data, b"resource"); - assert!(!emu.open_files[&handle].dirty); - } - - #[test] - fn test_guest_save_path_cannot_escape_save_directory() { - let mut emu = Emulator::default(); - emu.set_save_directory(std::env::temp_dir().join("dingooemu-save-root")); - - assert!(emu.save_file_path("save/profile.dat").is_some()); - assert!(emu.save_file_path("A:\\save\\profile.dat").is_some()); - assert!(emu.save_file_path("../outside.dat").is_none()); - assert!(emu.save_file_path("save/../../outside.dat").is_none()); - } - - #[test] - fn test_file_search_enumerates_matching_entries_and_terminates() { - let directory = - std::env::temp_dir().join(format!("dingooemu-file-search-{}", std::process::id())); - let _ = std::fs::remove_dir_all(&directory); - std::fs::create_dir_all(directory.join("Music")).unwrap(); - std::fs::write(directory.join("Dn-Beyond.xm"), b"xm").unwrap(); - std::fs::write(directory.join("Fountain.mod"), b"mod").unwrap(); - - let mut emu = Emulator { - app_path: directory.join("GooPlayer.app").display().to_string(), - ..Default::default() - }; - emu.memory.load_data(0x100, b"*\0").unwrap(); - emu.cpu.regs.write(4, 0x100); - emu.cpu.regs.write(5, 0x10); - emu.cpu.regs.write(6, 0x200); - invoke_sdk_import(&mut emu, 0x1000, "fsys_findfirst"); - assert_eq!(emu.cpu.regs.read(2), 0); - assert_eq!( - emu.read_guest_c_string(0x200 + FILE_SEARCH_NAME_OFFSET), - "Music" - ); - - emu.cpu.regs.write(4, 0x200); - invoke_sdk_import(&mut emu, 0x1004, "fsys_findnext"); - assert_eq!(emu.cpu.regs.read(2), u32::MAX); - - emu.cpu.regs.write(4, 0x100); - emu.cpu.regs.write(5, 0); - emu.cpu.regs.write(6, 0x300); - invoke_sdk_import(&mut emu, 0x1008, "fsys_findfirst"); - assert_eq!(emu.cpu.regs.read(2), 0); - assert_eq!( - emu.read_guest_c_string(0x300 + FILE_SEARCH_NAME_OFFSET), - "Dn-Beyond.xm" - ); - - emu.cpu.regs.write(4, 0x300); - invoke_sdk_import(&mut emu, 0x100c, "fsys_findnext"); - assert_eq!(emu.cpu.regs.read(2), 0); - assert_eq!( - emu.read_guest_c_string(0x300 + FILE_SEARCH_NAME_OFFSET), - "Fountain.mod" - ); - - emu.cpu.regs.write(4, 0x200); - invoke_sdk_import(&mut emu, 0x1010, "fsys_findclose"); - assert_eq!(emu.cpu.regs.read(2), 0); - assert!(!emu.file_searches.contains_key(&0x200)); - - std::fs::remove_dir_all(directory).unwrap(); - } - - #[test] - fn test_file_search_filters_wildcards_and_rejects_parent_paths() { - let directory = - std::env::temp_dir().join(format!("dingooemu-file-filter-{}", std::process::id())); - let _ = std::fs::remove_dir_all(&directory); - std::fs::create_dir_all(&directory).unwrap(); - std::fs::write(directory.join("Track.XM"), b"xm").unwrap(); - std::fs::write(directory.join("Track.mod"), b"mod").unwrap(); - - let mut emu = Emulator { - app_path: directory.join("GooPlayer.app").display().to_string(), - ..Default::default() - }; - assert_eq!(emu.begin_file_search("*.xm", 0, 0x200).unwrap(), 0); - assert_eq!( - emu.read_guest_c_string(0x200 + FILE_SEARCH_NAME_OFFSET), - "Track.XM" - ); - assert_eq!(emu.next_file_search(0x200).unwrap(), u32::MAX); - assert_eq!( - emu.begin_file_search("../*", 0x10, 0x300).unwrap(), - u32::MAX - ); - - std::fs::remove_dir_all(directory).unwrap(); - } - - #[test] - fn test_save_state_round_trip_and_transactional_rejection() { - let mut emu = Emulator::from_app(minimal_app()).unwrap(); - let save_directory = std::env::temp_dir().join("dingooemu-state-save-root"); - emu.set_save_directory(&save_directory); - emu.start(); - emu.cpu.regs.write(8, 0x1234_5678); - emu.memory.write_u32(0x2000, 0xaabb_ccdd).unwrap(); - emu.input.set_buttons(crate::input::BUTTON_A); - emu.frame_count = 42; - emu.cycle_count = 987_654; - emu.open_files.insert( - 7, - OpenFile { - data: b"open save data".to_vec(), - position: 4, - data_ptr: 0, - save_path: Some(save_directory.join("profile.dat")), - writable: true, - dirty: true, - }, - ); - emu.file_searches.insert( - 0x3000, - FileSearch { - entries: vec!["first.mod".to_string(), "second.xm".to_string()], - next_index: 1, - }, - ); - emu.gui.key_messages_enabled = true; - emu.gui.windows.insert(3, 0x8000_4000); - emu.gui.focused_window = Some(3); - emu.gui.next_window_handle = 4; - emu.gui.reported_key = 18; - emu.gui.message_buffer = Some(0x2100); - emu.gui.key_info_buffer = Some(0x2200); - - let mut state = vec![0; emu.serialized_state_size()]; - emu.serialize_state(&mut state).unwrap(); - emu.cpu.regs.write(8, 0); - emu.memory.write_u32(0x2000, 0).unwrap(); - emu.input.set_buttons(0); - emu.frame_count = 0; - emu.open_files.clear(); - emu.file_searches.clear(); - emu.gui = GuiState::default(); - - emu.unserialize_state(&state).unwrap(); - assert_eq!(emu.cpu.regs.read(8), 0x1234_5678); - assert_eq!(emu.memory.read_u32(0x2000).unwrap(), 0xaabb_ccdd); - assert_eq!(emu.input.buttons(), crate::input::BUTTON_A); - assert_eq!(emu.frame_count, 42); - assert_eq!(emu.cycle_count, 987_654); - assert!(emu.is_running()); - assert_eq!(emu.open_files[&7].data, b"open save data"); - assert_eq!(emu.file_searches[&0x3000].next_index, 1); - assert_eq!(emu.file_searches[&0x3000].entries[1], "second.xm"); - assert!(emu.gui.key_messages_enabled); - assert_eq!(emu.gui.windows[&3], 0x8000_4000); - assert_eq!(emu.gui.focused_window, Some(3)); - assert_eq!(emu.gui.next_window_handle, 4); - assert_eq!(emu.gui.reported_key, 18); - assert_eq!(emu.gui.message_buffer, Some(0x2100)); - assert_eq!(emu.gui.key_info_buffer, Some(0x2200)); - assert_eq!( - emu.open_files[&7].save_path.as_deref(), - Some(save_directory.join("profile.dat").as_path()) - ); - - let unchanged_pc = emu.cpu.regs.pc; - let unchanged_memory = emu.memory.read_u32(0x2000).unwrap(); - state[32] ^= 1; - assert!(emu.unserialize_state(&state).is_err()); - assert_eq!(emu.cpu.regs.pc, unchanged_pc); - assert_eq!(emu.memory.read_u32(0x2000).unwrap(), unchanged_memory); - - state[32] ^= 1; - let mut other_app = minimal_app(); - other_app.data.push(1); - let mut other = Emulator::from_app(other_app).unwrap(); - other.cpu.regs.pc = 0x8765_4321; - assert!(other.unserialize_state(&state).is_err()); - assert_eq!(other.cpu.regs.pc, 0x8765_4321); - } -} diff --git a/native/DingooEmu/crates/dingooemu-core/src/emulator/sdk_hle/audio.rs b/native/DingooEmu/crates/dingooemu-core/src/emulator/sdk_hle/audio.rs deleted file mode 100644 index 4a67d1c..0000000 --- a/native/DingooEmu/crates/dingooemu-core/src/emulator/sdk_hle/audio.rs +++ /dev/null @@ -1,72 +0,0 @@ -use super::super::{AudioConfig, Emulator, TaskWait, MAX_AUDIO_WRITE_BYTES}; -use super::HandlerResult; -use crate::error::Result; - -pub(super) fn handle(emu: &mut Emulator, func_name: &str) -> Result { - match func_name { - "_waveout_open" | "waveout_open" => { - let args_ptr = emu.cpu.regs.read(4); - let config = AudioConfig::new( - emu.memory.read_u32(args_ptr)?, - emu.memory.read_u16(args_ptr.wrapping_add(4))?, - emu.memory.read_u8(args_ptr.wrapping_add(6))?, - emu.memory.read_u8(args_ptr.wrapping_add(7))?, - ); - let opened = config.is_some_and(|config| emu.audio.open(config)); - emu.cpu.regs.write(2, u32::from(opened)); - log::trace!(" waveout_open({args_ptr:#010x}) = {opened}"); - } - "waveout_write" => { - let buffer_ptr = emu.cpu.regs.read(5); - let count = emu.cpu.regs.read(6); - let written = if count == 0 || count > MAX_AUDIO_WRITE_BYTES { - false - } else if !emu.audio.can_write() { - emu.set_active_wait(TaskWait::AudioWrite); - log::trace!( - " waveout_write({buffer_ptr:#010x}, {count}) deferred until queue space is available" - ); - return Ok(HandlerResult::Deferred); - } else { - let mut data = Vec::with_capacity(count as usize); - for offset in 0..count { - data.push(emu.memory.read_u8(buffer_ptr.wrapping_add(offset))?); - } - emu.audio.write(&data) - }; - emu.cpu.regs.write(2, u32::from(written)); - log::trace!( - " waveout_write({buffer_ptr:#010x}, {count}) = {}", - u32::from(written) - ); - } - "waveout_can_write" | "pcm_can_write" => { - let can_write = emu.audio.can_write(); - emu.cpu.regs.write(2, u32::from(can_write)); - log::trace!(" {func_name}() = {}", u32::from(can_write)); - } - "waveout_close" | "waveout_close_at_once" => { - let closed = emu.audio.close(); - emu.cpu.regs.write(2, u32::from(closed)); - log::trace!(" {func_name}() = {}", u32::from(closed)); - } - "_waveout_set_volume" | "waveout_set_volume" => { - let volume = emu.cpu.regs.read(4); - let updated = emu.audio.set_volume(volume); - emu.cpu.regs.write(2, u32::from(updated)); - log::trace!(" {func_name}({volume}) = {}", u32::from(updated)); - } - "HP_Mute_sw" => { - let muted = emu.cpu.regs.read(4) != 0; - let updated = emu.audio.set_muted(muted); - emu.cpu.regs.write(2, u32::from(updated)); - log::trace!(" HP_Mute_sw({muted}) = {}", u32::from(updated)); - } - "pcm_ioctl" => { - emu.cpu.regs.write(2, 0); - log::trace!(" pcm_ioctl() = 0"); - } - _ => return Ok(HandlerResult::NotHandled), - } - Ok(HandlerResult::Complete) -} diff --git a/native/DingooEmu/crates/dingooemu-core/src/emulator/sdk_hle/files.rs b/native/DingooEmu/crates/dingooemu-core/src/emulator/sdk_hle/files.rs deleted file mode 100644 index d8c8e74..0000000 --- a/native/DingooEmu/crates/dingooemu-core/src/emulator/sdk_hle/files.rs +++ /dev/null @@ -1,181 +0,0 @@ -use super::{Emulator, HandlerResult}; -use crate::error::Result; - -fn is_firmware_return_request(name_words: &[u16], mode: &str) -> bool { - // Observed when some Dingoo A320 titles choose their built-in Exit item. - // The firmware hand-off reaches fsys_fopenW with this invalid UTF-16 - // sentinel (an unpaired low surrogate followed by U+80AA), then displays - // "resource file could not be loaded" forever when no Dingoo shell exists. - mode == "rb" && name_words == [0xDE88, 0x80AA] -} - -pub(super) fn handle(emu: &mut Emulator, func_name: &str) -> Result { - match func_name { - "get_dl_handle" => { - emu.cpu.regs.write(2, u32::from(emu.app.is_some())); - } - "dl_res_open" => { - let name = emu.resource_name_from_args(&[ - emu.cpu.regs.read(6), - emu.cpu.regs.read(5), - emu.cpu.regs.read(4), - ]); - let handle = name - .as_deref() - .map(|name| emu.open_resource_file(name)) - .unwrap_or(0); - if handle == 0 { - match name.as_deref() { - Some(name) => log::warn!("Embedded resource open failed: {name}"), - None => log::warn!("Embedded resource open failed: invalid name pointer"), - } - } - emu.cpu.regs.write(2, handle); - } - "dl_res_get_size" => { - let handle = emu.cpu.regs.read(4); - let size = emu - .open_files - .get(&handle) - .map(|file| file.data.len() as u32) - .unwrap_or(0); - emu.cpu.regs.write(2, size); - } - "dl_res_get_data" => { - let handle = emu.cpu.regs.read(4); - let buffer = emu.cpu.regs.read(5); - let buffer_len = emu.cpu.regs.read(6); - let read_len = emu.cpu.regs.read(7); - let result = emu.read_resource_data(handle, buffer, buffer_len, read_len)?; - emu.cpu.regs.write(2, result); - } - "dl_res_close" => { - let handle = emu.cpu.regs.read(4); - emu.open_files.remove(&handle); - emu.cpu.regs.write(2, 0); - } - "fopen" | "fsys_fopen" => { - let name_ptr = emu.cpu.regs.read(4); - let name = emu.read_guest_c_string(name_ptr); - let mode = emu.read_guest_c_string(emu.cpu.regs.read(5)); - let handle = emu.open_guest_file(&name, &mode); - if handle == 0 { - let raw_name = emu.guest_c_string_hex(name_ptr); - log::warn!( - "Guest filename bytes: api={func_name} ptr={name_ptr:#010x} hex=[{raw_name}]" - ); - } - emu.cpu.regs.write(2, handle); - log::trace!(" {func_name}({name}, {mode}) = {handle}"); - } - "fsys_fopenW" => { - let name_ptr = emu.cpu.regs.read(4); - let name_words = emu.read_guest_w_string_words(name_ptr); - let name = String::from_utf16_lossy(&name_words); - let mode = emu.read_guest_w_string(emu.cpu.regs.read(5)); - log::trace!(" fsys_fopenW({name}, {mode})"); - if is_firmware_return_request(&name_words, &mode) { - log::warn!( - "Dingoo firmware return request detected; stopping content for frontend shutdown" - ); - emu.cpu.regs.write(2, 0); - emu.cpu.stop(); - return Ok(HandlerResult::Complete); - } - let handle = emu.open_guest_file(&name, &mode); - if handle == 0 { - let raw_name = emu.guest_w_string_hex(name_ptr); - log::warn!( - "Guest filename words: api=fsys_fopenW ptr={name_ptr:#010x} utf16=[{raw_name}]" - ); - } - emu.cpu.regs.write(2, handle); - } - "fclose" | "fsys_fclose" | "fsys_fcloseW" => { - let handle = emu.cpu.regs.read(4); - let result = emu.flush_save_file(handle); - emu.open_files.remove(&handle); - emu.cpu - .regs - .write(2, if result.is_ok() { 0 } else { u32::MAX }); - } - "fread" | "fsys_fread" => { - let dest = emu.cpu.regs.read(4); - let size = emu.cpu.regs.read(5); - let count = emu.cpu.regs.read(6); - let handle = emu.cpu.regs.read(7); - let read = emu.read_file(dest, size, count, handle)?; - emu.cpu.regs.write(2, read); - } - "fseek" | "fsys_fseek" => { - let handle = emu.cpu.regs.read(4); - let offset = emu.cpu.regs.read(5) as i32; - let origin = emu.cpu.regs.read(6); - let result = emu.seek_file(handle, offset, origin); - emu.cpu.regs.write(2, result); - } - "ftell" | "fsys_ftell" => { - let handle = emu.cpu.regs.read(4); - let position = emu - .open_files - .get(&handle) - .map(|file| file.position as u32) - .unwrap_or(u32::MAX); - emu.cpu.regs.write(2, position); - } - "feof" | "fsys_feof" => { - let handle = emu.cpu.regs.read(4); - let eof = emu - .open_files - .get(&handle) - .map(|file| u32::from(file.position >= file.data.len())) - .unwrap_or(1); - emu.cpu.regs.write(2, eof); - } - "fwrite" | "fsys_fwrite" => { - let source = emu.cpu.regs.read(4); - let size = emu.cpu.regs.read(5); - let count = emu.cpu.regs.read(6); - let handle = emu.cpu.regs.read(7); - let written = emu.write_file(source, size, count, handle)?; - emu.cpu.regs.write(2, written); - log::trace!(" {func_name}() = {written}"); - } - "fsys_findfirst" => { - let pattern = emu.read_guest_c_string(emu.cpu.regs.read(4)); - let attributes = emu.cpu.regs.read(5); - let data_ptr = emu.cpu.regs.read(6); - let result = emu.begin_file_search(&pattern, attributes, data_ptr)?; - emu.cpu.regs.write(2, result); - log::trace!( - " fsys_findfirst({pattern}, {attributes:#x}, {data_ptr:#010x}) = {result:#010x}" - ); - } - "fsys_findnext" => { - let data_ptr = emu.cpu.regs.read(4); - let result = emu.next_file_search(data_ptr)?; - emu.cpu.regs.write(2, result); - log::trace!(" fsys_findnext({data_ptr:#010x}) = {result:#010x}"); - } - "fsys_findclose" => { - let data_ptr = emu.cpu.regs.read(4); - let result = emu.close_file_search(data_ptr); - emu.cpu.regs.write(2, result); - log::trace!(" fsys_findclose({data_ptr:#010x}) = {result}"); - } - _ => return Ok(HandlerResult::NotHandled), - } - Ok(HandlerResult::Complete) -} - -#[cfg(test)] -mod tests { - use super::is_firmware_return_request; - - #[test] - fn recognizes_observed_firmware_return_sentinel() { - assert!(is_firmware_return_request(&[0xDE88, 0x80AA], "rb")); - assert!(!is_firmware_return_request(&[0xDE88, 0x80AA], "wb")); - assert!(!is_firmware_return_request(&[b'a' as u16], "rb")); - } -} diff --git a/native/DingooEmu/crates/dingooemu-core/src/emulator/sdk_hle/graphics.rs b/native/DingooEmu/crates/dingooemu-core/src/emulator/sdk_hle/graphics.rs deleted file mode 100644 index 9a5dc8e..0000000 --- a/native/DingooEmu/crates/dingooemu-core/src/emulator/sdk_hle/graphics.rs +++ /dev/null @@ -1,34 +0,0 @@ -use super::{Emulator, HandlerResult}; -use crate::error::Result; - -pub(super) fn handle(emu: &mut Emulator, func_name: &str) -> Result { - match func_name { - "_lcd_get_frame" | "lcd_get_frame" | "lcd_get_cframe" => { - let address = crate::video::VM_LCD_FB_ADDRESS; - emu.cpu.regs.write(2, address); - log::trace!(" lcd_get_frame() = {address:#010x}"); - } - "_lcd_set_frame" | "lcd_set_frame" | "ap_lcd_set_frame" => { - emu.sync_framebuffer(); - log::trace!(" lcd_set_frame() - framebuffer updated"); - } - "lcd_flip" => { - emu.sync_framebuffer(); - log::trace!(" lcd_flip() - framebuffer updated"); - } - "LcdGetDisMode" => { - emu.cpu.regs.write(2, 0); - log::trace!(" LcdGetDisMode() = 0"); - } - "LCD_GetXSize" => { - emu.cpu.regs.write(2, crate::video::SCREEN_WIDTH); - log::trace!(" LCD_GetXSize() = {}", crate::video::SCREEN_WIDTH); - } - "LCD_GetYSize" => { - emu.cpu.regs.write(2, crate::video::SCREEN_HEIGHT); - log::trace!(" LCD_GetYSize() = {}", crate::video::SCREEN_HEIGHT); - } - _ => return Ok(HandlerResult::NotHandled), - } - Ok(HandlerResult::Complete) -} diff --git a/native/DingooEmu/crates/dingooemu-core/src/emulator/sdk_hle/gui.rs b/native/DingooEmu/crates/dingooemu-core/src/emulator/sdk_hle/gui.rs deleted file mode 100644 index af3764f..0000000 --- a/native/DingooEmu/crates/dingooemu-core/src/emulator/sdk_hle/gui.rs +++ /dev/null @@ -1,132 +0,0 @@ -use super::{Emulator, HandlerResult}; -use crate::error::Result; - -const WM_KEY: u32 = 14; -const WM_MESSAGE_SIZE: u32 = 12; -const GUI_KEY_INFO_SIZE: u32 = 8; -const GUI_KEY_ENTER: u32 = 13; -const GUI_KEY_LEFT: u32 = 16; -const GUI_KEY_UP: u32 = 17; -const GUI_KEY_RIGHT: u32 = 18; -const GUI_KEY_DOWN: u32 = 19; -const GUI_KEY_ESCAPE: u32 = 27; - -pub(super) fn handle(emu: &mut Emulator, func_name: &str) -> Result { - match func_name { - "open_gui_key_msg" => { - emu.gui.key_messages_enabled = true; - emu.cpu.regs.write(2, 0); - log::trace!(" open_gui_key_msg() - key messages enabled"); - } - "WM_CreateWindow" => create_window(emu)?, - "WM_SetFocus" => set_focus(emu), - "GUI_Exec" => return execute_gui(emu), - _ => return Ok(HandlerResult::NotHandled), - } - Ok(HandlerResult::Complete) -} - -fn create_window(emu: &mut Emulator) -> Result<()> { - let stack_pointer = emu.cpu.regs.read(29); - let callback = emu.memory.read_u32(stack_pointer.wrapping_add(20))?; - if callback == 0 { - emu.cpu.regs.write(2, 0); - return Ok(()); - } - - let handle = emu.gui.next_window_handle; - emu.gui.next_window_handle = emu.gui.next_window_handle.wrapping_add(1).max(1); - emu.gui.windows.insert(handle, callback); - emu.cpu.regs.write(2, handle); - log::trace!(" WM_CreateWindow(callback={callback:#010x}) = {handle:#010x}"); - Ok(()) -} - -fn set_focus(emu: &mut Emulator) { - let handle = emu.cpu.regs.read(4); - if emu.gui.windows.contains_key(&handle) { - emu.gui.focused_window = Some(handle); - } - emu.cpu.regs.write(2, 0); - log::trace!(" WM_SetFocus({handle:#010x})"); -} - -fn execute_gui(emu: &mut Emulator) -> Result { - emu.cpu.regs.write(2, 0); - if !emu.gui.key_messages_enabled { - return Ok(HandlerResult::Complete); - } - - let current_key = gui_key_code(emu.input.buttons()); - let (key, pressed, next_reported_key) = - if emu.gui.reported_key != 0 && emu.gui.reported_key != current_key { - let key = emu.gui.reported_key; - (key, 0, 0) - } else if emu.gui.reported_key == 0 && current_key != 0 { - (current_key, 1, current_key) - } else { - return Ok(HandlerResult::Complete); - }; - - let Some(window) = emu.gui.focused_window else { - return Ok(HandlerResult::Complete); - }; - let Some(&callback) = emu.gui.windows.get(&window) else { - return Ok(HandlerResult::Complete); - }; - - let message = match emu.gui.message_buffer { - Some(address) => address, - None => { - let address = emu.memory.malloc(WM_MESSAGE_SIZE); - if address == 0 { - return Ok(HandlerResult::Complete); - } - emu.gui.message_buffer = Some(address); - address - } - }; - let key_info = match emu.gui.key_info_buffer { - Some(address) => address, - None => { - let address = emu.memory.malloc(GUI_KEY_INFO_SIZE); - if address == 0 { - return Ok(HandlerResult::Complete); - } - emu.gui.key_info_buffer = Some(address); - address - } - }; - - emu.memory.write_u32(key_info, key)?; - emu.memory.write_u32(key_info.wrapping_add(4), pressed)?; - emu.memory.write_u32(message, WM_KEY)?; - emu.memory.write_u32(message.wrapping_add(4), window)?; - emu.memory.write_u32(message.wrapping_add(8), key_info)?; - - emu.gui.reported_key = next_reported_key; - emu.cpu.regs.write(4, message); - emu.cpu.regs.pc = callback; - log::trace!( - " GUI_Exec() - dispatching WM_KEY key={key} pressed={pressed} to {callback:#010x}" - ); - Ok(HandlerResult::GuestCallback) -} - -fn gui_key_code(buttons: u32) -> u32 { - if buttons & crate::input::BUTTON_LEFT != 0 { - GUI_KEY_LEFT - } else if buttons & crate::input::BUTTON_UP != 0 { - GUI_KEY_UP - } else if buttons & crate::input::BUTTON_RIGHT != 0 { - GUI_KEY_RIGHT - } else if buttons & crate::input::BUTTON_DOWN != 0 { - GUI_KEY_DOWN - } else if buttons & crate::input::BUTTON_A != 0 { - GUI_KEY_ENTER - } else if buttons & crate::input::BUTTON_B != 0 { - GUI_KEY_ESCAPE - } else { - 0 - } -} diff --git a/native/DingooEmu/crates/dingooemu-core/src/emulator/sdk_hle/input.rs b/native/DingooEmu/crates/dingooemu-core/src/emulator/sdk_hle/input.rs deleted file mode 100644 index 1f3850f..0000000 --- a/native/DingooEmu/crates/dingooemu-core/src/emulator/sdk_hle/input.rs +++ /dev/null @@ -1,29 +0,0 @@ -use super::{Emulator, HandlerResult}; -use crate::error::Result; - -pub(super) fn handle(emu: &mut Emulator, func_name: &str) -> Result { - match func_name { - "_kbd_get_status" | "kbd_get_status" => { - let status_ptr = emu.cpu.regs.read(4); - let (pressed, released, status) = emu.input.take_status(); - emu.memory.write_u32(status_ptr, pressed)?; - emu.memory.write_u32(status_ptr.wrapping_add(4), released)?; - emu.memory.write_u32(status_ptr.wrapping_add(8), status)?; - log::trace!( - " kbd_get_status({status_ptr:#010x}) pressed={pressed:#010x} released={released:#010x} status={status:#010x}" - ); - } - "_kbd_get_key" | "kbd_get_key" => { - let status = emu.input.buttons(); - emu.cpu.regs.write(2, status); - log::trace!(" kbd_get_key() = {status:#010x}"); - } - "_sys_judge_event" | "sys_judge_event" => { - let pending = u32::from(emu.input.take_pending_event()); - emu.cpu.regs.write(2, pending); - log::trace!(" sys_judge_event() = {pending}"); - } - _ => return Ok(HandlerResult::NotHandled), - } - Ok(HandlerResult::Complete) -} diff --git a/native/DingooEmu/crates/dingooemu-core/src/emulator/sdk_hle/mod.rs b/native/DingooEmu/crates/dingooemu-core/src/emulator/sdk_hle/mod.rs deleted file mode 100644 index c44b2f4..0000000 --- a/native/DingooEmu/crates/dingooemu-core/src/emulator/sdk_hle/mod.rs +++ /dev/null @@ -1,57 +0,0 @@ -use super::Emulator; -use crate::error::Result; - -mod audio; -mod files; -mod graphics; -mod gui; -mod input; -mod system; -mod tasks; - -#[derive(Clone, Copy, Debug, Eq, PartialEq)] -pub(super) enum HandlerResult { - NotHandled, - Complete, - Deferred, - GuestCallback, -} - -type Handler = fn(&mut Emulator, &str) -> Result; - -const HANDLERS: [Handler; 7] = [ - graphics::handle, - gui::handle, - input::handle, - audio::handle, - files::handle, - tasks::handle, - system::handle, -]; - -pub(super) fn dispatch(emu: &mut Emulator, addr: u32, func_name: &str) -> Result<()> { - log::trace!("SDK call: {addr:#010x} = {func_name}"); - let return_address = emu.cpu.regs.read(31); - - let mut result = HandlerResult::NotHandled; - for handler in HANDLERS { - result = handler(emu, func_name)?; - if result != HandlerResult::NotHandled { - break; - } - } - - match result { - HandlerResult::Complete => emu.cpu.regs.pc = return_address, - HandlerResult::Deferred => {} - HandlerResult::GuestCallback => {} - HandlerResult::NotHandled => { - emu.record_unknown_hle(func_name, addr, return_address)?; - emu.cpu.regs.write(2, 0); - emu.cpu.regs.pc = return_address; - log::trace!(" {func_name}() = 0 (compatibility stub)"); - } - } - emu.cpu.regs.gpr[0] = 0; - Ok(()) -} diff --git a/native/DingooEmu/crates/dingooemu-core/src/emulator/sdk_hle/system.rs b/native/DingooEmu/crates/dingooemu-core/src/emulator/sdk_hle/system.rs deleted file mode 100644 index 7cd9020..0000000 --- a/native/DingooEmu/crates/dingooemu-core/src/emulator/sdk_hle/system.rs +++ /dev/null @@ -1,152 +0,0 @@ -use super::super::{Emulator, CPU_CLOCK_HZ, OS_TICKS_PER_SECOND}; -use super::HandlerResult; -use crate::error::Result; - -pub(super) fn handle(emu: &mut Emulator, func_name: &str) -> Result { - match func_name { - "malloc" => { - let size = emu.cpu.regs.read(4); - let pointer = emu.memory.malloc(size); - emu.cpu.regs.write(2, pointer); - log::info!( - " malloc({size}) = {pointer:#010x} (heap_ptr={:#010x})", - emu.memory.heap_ptr() - ); - } - "free" => { - let pointer = emu.cpu.regs.read(4); - emu.memory.free(pointer); - log::trace!(" free({pointer:#010x})"); - } - "realloc" => { - let pointer = emu.cpu.regs.read(4); - let size = emu.cpu.regs.read(5); - let new_pointer = emu.memory.realloc(pointer, size); - emu.cpu.regs.write(2, new_pointer); - log::trace!(" realloc({pointer:#010x}, {size}) = {new_pointer:#010x}"); - } - "memset" => { - let pointer = emu.cpu.regs.read(4); - let value = emu.cpu.regs.read(5) as u8; - let size = emu.cpu.regs.read(6); - emu.memory.memset(pointer, value, size); - emu.cpu.regs.write(2, pointer); - log::trace!(" memset({pointer:#010x}, {value:#04x}, {size})"); - } - "memcpy" => { - let destination = emu.cpu.regs.read(4); - let source = emu.cpu.regs.read(5); - let size = emu.cpu.regs.read(6); - emu.memory.memcpy(destination, source, size)?; - emu.cpu.regs.write(2, destination); - log::trace!(" memcpy({destination:#010x}, {source:#010x}, {size})"); - } - "strlen" => { - let pointer = emu.cpu.regs.read(4); - let length = emu.memory.read_string_len(pointer); - emu.cpu.regs.write(2, length); - log::trace!(" strlen({pointer:#010x}) = {length}"); - } - "__to_locale_ansi" => { - let pointer = emu.cpu.regs.read(4); - let result = emu.convert_guest_w_string_to_ansi(pointer); - emu.cpu.regs.write(2, result); - log::trace!(" __to_locale_ansi({pointer:#010x}) = {result:#010x}"); - } - "cmGetSysModel" => { - let pointer = emu.cpu.regs.read(4); - for (index, word) in "A320\0".encode_utf16().enumerate() { - emu.memory - .write_u16(pointer.wrapping_add((index * 2) as u32), word)?; - } - emu.cpu.regs.write(2, 0); - log::trace!(" cmGetSysModel({pointer:#010x}) = 0"); - } - "get_current_language" => { - // Dingoo SDK locale.h: LANG_CHINESE_SIMPLIFIED = 0. - // Returning this explicitly keeps games from falling through the - // generic unknown-HLE compatibility stub and documents the ABI. - emu.cpu.regs.write(2, 0); - log::trace!(" get_current_language() = 0 (Simplified Chinese)"); - } - "U8TOU16" | "U8TOX16" => { - let pointer = emu.cpu.regs.read(4); - let value = emu.memory.read_u16(pointer)? as u32; - emu.cpu.regs.write(2, value); - log::trace!(" {func_name}({pointer:#010x}) = {value:#06x}"); - } - "U8TOU32" | "U8TOX32" => { - let pointer = emu.cpu.regs.read(4); - let value = emu.memory.read_u32(pointer)?; - emu.cpu.regs.write(2, value); - log::trace!(" {func_name}({pointer:#010x}) = {value:#010x}"); - } - "OSTimeGet" => { - let ticks = emu - .cycle_count - .saturating_mul(OS_TICKS_PER_SECOND) - .checked_div(CPU_CLOCK_HZ) - .unwrap_or(0) as u32; - emu.cpu.regs.write(2, ticks); - log::trace!(" OSTimeGet() = {ticks}"); - } - "GetTickCount" => { - let micros = emu - .cycle_count - .saturating_mul(1_000_000) - .checked_div(CPU_CLOCK_HZ) - .unwrap_or(0) as u32; - emu.cpu.regs.write(2, micros); - log::trace!(" GetTickCount() = {micros}"); - } - "delay_ms" | "mdelay" => { - let milliseconds = emu.cpu.regs.read(4); - let delay_cycles = (milliseconds as u64).saturating_mul(CPU_CLOCK_HZ) / 1_000; - emu.delay_active_until(emu.cycle_count.saturating_add(delay_cycles)); - log::trace!(" delay_ms({milliseconds})"); - } - "StartSwTimer" => { - emu.cpu.regs.write(2, 0); - log::trace!(" StartSwTimer() = 0"); - } - "OSTimeDly" => { - let ticks = emu.cpu.regs.read(4); - let delay_cycles = (ticks as u64).saturating_mul(CPU_CLOCK_HZ) / OS_TICKS_PER_SECOND; - emu.delay_active_until(emu.cycle_count.saturating_add(delay_cycles)); - log::trace!(" OSTimeDly({ticks})"); - } - "udelay" => { - let microseconds = emu.cpu.regs.read(4); - let delay_cycles = (microseconds as u64).saturating_mul(CPU_CLOCK_HZ) / 1_000_000; - emu.delay_active_until(emu.cycle_count.saturating_add(delay_cycles)); - log::trace!(" udelay({microseconds})"); - } - "vxGoHome" | "abort" | "TaskMediaFunStop" => { - emu.cpu.stop(); - log::trace!(" {func_name} -> stopping"); - } - "sprintf" => { - let destination = emu.cpu.regs.read(4); - let format = emu.read_guest_c_string(emu.cpu.regs.read(5)); - let rendered = emu.format_guest_printf(&format)?; - let mut bytes = rendered.as_bytes().to_vec(); - bytes.push(0); - emu.memory.load_data(destination, &bytes)?; - emu.cpu.regs.write(2, rendered.len() as u32); - log::trace!(" sprintf({format}) = {}", rendered.len()); - } - "printf" | "fprintf" => { - emu.cpu.regs.write(2, 0); - log::trace!(" {func_name}() = 0 (stub)"); - } - "__icache_invalidate_all" => { - emu.clear_instruction_cache(); - log::trace!(" {func_name} (instruction cache cleared)"); - } - "__dcache_writeback_all" => { - log::trace!(" {func_name} (no-op)"); - } - _ => return Ok(HandlerResult::NotHandled), - } - Ok(HandlerResult::Complete) -} diff --git a/native/DingooEmu/crates/dingooemu-core/src/emulator/sdk_hle/tasks.rs b/native/DingooEmu/crates/dingooemu-core/src/emulator/sdk_hle/tasks.rs deleted file mode 100644 index a536500..0000000 --- a/native/DingooEmu/crates/dingooemu-core/src/emulator/sdk_hle/tasks.rs +++ /dev/null @@ -1,63 +0,0 @@ -use super::{Emulator, HandlerResult}; -use crate::error::Result; - -pub(super) fn handle(emu: &mut Emulator, func_name: &str) -> Result { - match func_name { - "OSTaskCreate" => { - let entry = emu.cpu.regs.read(4); - let data_ptr = emu.cpu.regs.read(5); - let stack_ptr = emu.cpu.regs.read(6); - let priority = emu.cpu.regs.read(7); - let created = emu.create_guest_task(entry, data_ptr, stack_ptr, priority); - emu.cpu.regs.write(2, if created { 0 } else { u32::MAX }); - log::trace!( - " OSTaskCreate({entry:#010x}, {data_ptr:#010x}, {stack_ptr:#010x}, {priority}) = {}", - emu.cpu.regs.read(2) - ); - } - "OSTaskDel" => { - let priority = emu.cpu.regs.read(4); - let deleted = emu.delete_guest_task(priority); - emu.cpu.regs.write(2, if deleted { 0 } else { 1 }); - log::trace!( - " OSTaskDel({priority}) = {}", - emu.cpu.regs.read(2) - ); - } - "OSSemCreate" => { - let count = emu.cpu.regs.read(4); - let handle = emu.create_semaphore(count); - emu.cpu.regs.write(2, handle); - log::trace!(" OSSemCreate({count}) = {handle}"); - } - "OSSemPend" => { - let handle = emu.cpu.regs.read(4); - let error_ptr = emu.cpu.regs.read(6); - let pending = emu.pend_semaphore(handle); - if error_ptr != 0 { - emu.memory - .write_u8(error_ptr, if pending { 0 } else { 1 })?; - } - log::trace!(" OSSemPend({handle}) = {pending}"); - } - "OSSemPost" => { - let handle = emu.cpu.regs.read(4); - let posted = emu.post_semaphore(handle); - emu.cpu.regs.write(2, if posted { 0 } else { 1 }); - log::trace!(" OSSemPost({handle}) = {}", emu.cpu.regs.read(2)); - } - "OSSemDel" => { - let handle = emu.cpu.regs.read(4); - let error_ptr = emu.cpu.regs.read(6); - let removed = emu.semaphores.remove(&handle).is_some(); - if error_ptr != 0 { - emu.memory - .write_u8(error_ptr, if removed { 0 } else { 1 })?; - } - emu.cpu.regs.write(2, 0); - log::trace!(" OSSemDel({handle}) = {removed}"); - } - _ => return Ok(HandlerResult::NotHandled), - } - Ok(HandlerResult::Complete) -} diff --git a/native/DingooEmu/crates/dingooemu-core/src/error.rs b/native/DingooEmu/crates/dingooemu-core/src/error.rs deleted file mode 100644 index d7057b7..0000000 --- a/native/DingooEmu/crates/dingooemu-core/src/error.rs +++ /dev/null @@ -1,57 +0,0 @@ -/// Result type alias for convenience -pub type Result = std::result::Result; - -/// Errors that can occur during emulation -#[derive(Debug, thiserror::Error)] -pub enum SimulatorError { - /// Invalid .app file format - #[error("invalid app format: {0}")] - InvalidAppFormat(String), - - /// Memory access error - #[error("memory error at {addr:#010x}: {message}")] - MemoryError { addr: u32, message: String }, - - /// CPU execution error - #[error("CPU error at {pc:#010x}: {message}")] - CpuError { pc: u32, message: String }, - - /// Invalid instruction - #[error("invalid instruction {instr:#010x} at {pc:#010x}")] - InvalidInstruction { pc: u32, instr: u32 }, - - /// SDK HLE call error - #[error("SDK HLE error: {0}")] - SdkHleError(String), - - /// Unknown SDK HLE call rejected by the active policy - #[error( - "unknown SDK HLE {name} at {pc:#010x} (import {import_address:#010x}, arguments {arguments:#010x?})" - )] - UnknownHle { - name: String, - pc: u32, - import_address: u32, - arguments: [u32; 4], - }, - - /// I/O error - #[error("I/O error: {0}")] - IoError(#[from] std::io::Error), - - /// Other errors - #[error("{0}")] - Other(String), -} - -impl From for SimulatorError { - fn from(s: String) -> Self { - SimulatorError::Other(s) - } -} - -impl From<&str> for SimulatorError { - fn from(s: &str) -> Self { - SimulatorError::Other(s.to_string()) - } -} diff --git a/native/DingooEmu/crates/dingooemu-core/src/input.rs b/native/DingooEmu/crates/dingooemu-core/src/input.rs deleted file mode 100644 index ec809b8..0000000 --- a/native/DingooEmu/crates/dingooemu-core/src/input.rs +++ /dev/null @@ -1,181 +0,0 @@ -/// Dingoo A320 button masks -pub const BUTTON_UP: u32 = 1 << 20; -pub const BUTTON_DOWN: u32 = 1 << 27; -pub const BUTTON_LEFT: u32 = 1 << 28; -pub const BUTTON_RIGHT: u32 = 1 << 18; -pub const BUTTON_A: u32 = 1 << 31; -pub const BUTTON_B: u32 = 1 << 21; -pub const BUTTON_X: u32 = 1 << 16; -pub const BUTTON_Y: u32 = 1 << 6; -pub const BUTTON_START: u32 = 1 << 11; -pub const BUTTON_SELECT: u32 = 1 << 10; -pub const BUTTON_L: u32 = 1 << 8; -pub const BUTTON_R: u32 = 1 << 29; - -/// Input subsystem -#[derive(serde::Serialize, serde::Deserialize)] -pub struct Input { - /// Current button state (bitmask) - buttons: u32, - /// Buttons pressed since the last status poll - pressed: u32, - /// Buttons released since the last status poll - released: u32, - /// Whether a system/input event is pending - event_pending: bool, - repeat_delay: u32, - repeat_period: u32, - held_frames: [u32; 32], - swap_ab: bool, -} - -impl Input { - /// Create a new input subsystem - pub fn new() -> Self { - Self { - buttons: 0, - pressed: 0, - released: 0, - event_pending: false, - repeat_delay: 0, - repeat_period: 1, - held_frames: [0; 32], - swap_ab: false, - } - } - - /// Get the current button state - pub fn buttons(&self) -> u32 { - self.buttons - } - - /// Set the button state - pub fn set_buttons(&mut self, buttons: u32) { - let buttons = if self.swap_ab { - let without_ab = buttons & !(BUTTON_A | BUTTON_B); - without_ab - | if buttons & BUTTON_A != 0 { BUTTON_B } else { 0 } - | if buttons & BUTTON_B != 0 { BUTTON_A } else { 0 } - } else { - buttons - }; - let changed = self.buttons ^ buttons; - self.pressed |= changed & buttons; - self.released |= changed & self.buttons; - if changed != 0 { - self.event_pending = true; - } - for bit in 0..32 { - let mask = 1u32 << bit; - if buttons & mask == 0 { - self.held_frames[bit] = 0; - } else if self.buttons & mask == 0 { - self.held_frames[bit] = 1; - } else { - self.held_frames[bit] = self.held_frames[bit].saturating_add(1); - let held = self.held_frames[bit]; - if self.repeat_delay > 0 - && held >= self.repeat_delay - && (held - self.repeat_delay).is_multiple_of(self.repeat_period) - { - self.pressed |= mask; - self.event_pending = true; - } - } - } - self.buttons = buttons; - } - - pub fn set_repeat_timing(&mut self, delay: u32, period: u32) { - self.repeat_delay = delay; - self.repeat_period = period.max(1); - } - - pub fn set_swap_ab(&mut self, swap_ab: bool) { - self.swap_ab = swap_ab; - } - - /// Get and clear the Dingoo key status structure fields. - pub fn take_status(&mut self) -> (u32, u32, u32) { - let status = (self.pressed, self.released, self.buttons); - self.pressed = 0; - self.released = 0; - status - } - - /// Get and clear whether an input event is pending. - pub fn take_pending_event(&mut self) -> bool { - let pending = self.event_pending; - self.event_pending = false; - pending - } - - /// Check if a specific button is pressed - pub fn is_pressed(&self, button: u32) -> bool { - (self.buttons & button) != 0 - } - - /// Press a button - pub fn press(&mut self, button: u32) { - self.set_buttons(self.buttons | button); - } - - /// Release a button - pub fn release(&mut self, button: u32) { - self.set_buttons(self.buttons & !button); - } -} - -impl Default for Input { - fn default() -> Self { - Self::new() - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_input_creation() { - let input = Input::new(); - assert_eq!(input.buttons(), 0); - } - - #[test] - fn test_button_press_release() { - let mut input = Input::new(); - input.press(BUTTON_A); - assert!(input.is_pressed(BUTTON_A)); - assert!(!input.is_pressed(BUTTON_B)); - - input.release(BUTTON_A); - assert!(!input.is_pressed(BUTTON_A)); - } - - #[test] - fn held_button_repeats_after_configured_delay() { - let mut input = Input::new(); - input.set_repeat_timing(3, 2); - input.set_buttons(BUTTON_A); - assert_eq!(input.take_status().0, BUTTON_A); - input.set_buttons(BUTTON_A); - assert_eq!(input.take_status().0, 0); - input.set_buttons(BUTTON_A); - assert_eq!(input.take_status().0, BUTTON_A); - input.set_buttons(BUTTON_A); - assert_eq!(input.take_status().0, 0); - input.set_buttons(BUTTON_A); - assert_eq!(input.take_status().0, BUTTON_A); - } - - #[test] - fn swap_ab_changes_logical_button_state() { - let mut input = Input::new(); - input.set_swap_ab(true); - input.set_buttons(BUTTON_A); - assert_eq!(input.buttons(), BUTTON_B); - input.set_buttons(BUTTON_B); - assert_eq!(input.buttons(), BUTTON_A); - } -} diff --git a/native/DingooEmu/crates/dingooemu-core/src/jit.rs b/native/DingooEmu/crates/dingooemu-core/src/jit.rs deleted file mode 100644 index dbfa287..0000000 --- a/native/DingooEmu/crates/dingooemu-core/src/jit.rs +++ /dev/null @@ -1,1484 +0,0 @@ -use crate::cpu::Registers; -use cranelift::codegen::ir::{types, AbiParam, Function, InstBuilder, MemFlags, Signature, Value}; -use cranelift::codegen::settings::{self, Configurable}; -use cranelift::frontend::{FunctionBuilder, FunctionBuilderContext}; -use cranelift::prelude::IntCC; -use cranelift_jit::{JITBuilder, JITModule}; -use cranelift_module::{default_libcall_names, Linkage, Module}; -use std::collections::HashMap; -use std::hash::{BuildHasherDefault, Hasher}; -use std::mem::{offset_of, transmute}; -use std::time::Instant; - -const HOT_BLOCK_THRESHOLD: u16 = 256; -const MIN_COMPILED_BLOCK_LEN: usize = 4; -const MAX_JIT_CACHE_ENTRIES: usize = 32_768; -const FAST_JIT_CACHE_SLOTS: usize = 4_096; -const MAX_ZERO_INSTRUCTION_EXITS: u8 = 4; -const COMPILE_COOLDOWN_FRAMES: u8 = 3; -const REGISTER_COUNT: usize = 34; -const HI_INDEX: usize = 32; -const LO_INDEX: usize = 33; -const KSEG_MASK: u32 = 0x1fff_ffff; - -type JitBlockFn = unsafe extern "C" fn(*mut Registers, *mut u8, *mut u8) -> u64; - -#[derive(Clone, Copy)] -struct CompiledBlock { - function: JitBlockFn, - instruction_count: usize, -} - -#[derive(Clone, Copy, Default)] -struct FastJitCacheEntry { - start: u32, - block: Option, -} - -#[derive(Default)] -struct JitCacheEntry { - hits: u16, - failed: bool, - zero_instruction_exits: u8, - block: Option, -} - -#[derive(Default)] -struct GuestAddressHasher(u64); - -impl Hasher for GuestAddressHasher { - fn finish(&self) -> u64 { - self.0 - } - - fn write(&mut self, bytes: &[u8]) { - self.0 = bytes.iter().fold(0, |hash, &byte| { - hash.wrapping_mul(16777619) ^ u64::from(byte) - }); - } - - fn write_u32(&mut self, value: u32) { - self.0 = u64::from(value >> 2); - } -} - -type JitEntryMap = HashMap>; - -struct Compiler { - module: JITModule, - context: cranelift::codegen::Context, - builder_context: FunctionBuilderContext, - next_function_id: u64, -} - -pub(crate) struct JitEngine { - compiler: Option, - entries: JitEntryMap, - fast_entries: Box<[FastJitCacheEntry]>, - enabled: bool, - compile_budget: u8, - compile_cooldown: u8, - compiled_block_count: u64, -} - -impl JitEngine { - pub(crate) fn new() -> Self { - let compiler = match Compiler::new() { - Ok(compiler) => { - log::info!("MIPS JIT backend initialized"); - Some(compiler) - } - Err(error) => { - log::warn!("MIPS JIT backend unavailable: {error}"); - None - } - }; - Self { - compiler, - entries: HashMap::with_capacity_and_hasher(4_096, BuildHasherDefault::default()), - fast_entries: vec![FastJitCacheEntry::default(); FAST_JIT_CACHE_SLOTS] - .into_boxed_slice(), - enabled: true, - compile_budget: 1, - compile_cooldown: 0, - compiled_block_count: 0, - } - } - - pub(crate) fn set_enabled(&mut self, enabled: bool) { - self.enabled = enabled; - } - - pub(crate) fn clear(&mut self) { - self.entries.clear(); - self.fast_entries.fill(FastJitCacheEntry::default()); - if self.compiled_block_count != 0 { - // Discard every function pointer before dropping the module that owns it. - if let Some(compiler) = self.compiler.take() { - drop(compiler); - self.compiler = match Compiler::new() { - Ok(compiler) => Some(compiler), - Err(error) => { - log::warn!( - "MIPS JIT backend unavailable after cache invalidation: {error}" - ); - None - } - }; - } - self.compiled_block_count = 0; - } - self.compile_budget = 1; - self.compile_cooldown = 0; - } - - pub(crate) fn begin_frame(&mut self) { - if self.compile_cooldown == 0 { - self.compile_budget = 1; - } else { - self.compile_cooldown -= 1; - self.compile_budget = 0; - } - } - - pub(crate) fn execute( - &mut self, - start: u32, - instructions: &[u32], - instruction_limit: usize, - registers: &mut Registers, - ram: *mut u8, - framebuffer: *mut u8, - ) -> Option { - if !self.enabled || instruction_limit == 0 || self.compiler.is_none() { - return None; - } - - let fast_index = (start as usize >> 2) & (FAST_JIT_CACHE_SLOTS - 1); - let fast_entry = self.fast_entries[fast_index]; - if fast_entry.start == start { - if let Some(block) = fast_entry.block { - let completed = - execute_compiled_block(block, instruction_limit, registers, ram, framebuffer); - if let Some(completed) = completed { - if completed != 0 { - return Some(completed); - } - if let Some(entry) = self.entries.get_mut(&start) { - entry.zero_instruction_exits = - entry.zero_instruction_exits.saturating_add(1); - if entry.zero_instruction_exits >= MAX_ZERO_INSTRUCTION_EXITS { - entry.block = None; - entry.failed = true; - self.fast_entries[fast_index].block = None; - } - } - } - return None; - } - } - - let at_capacity = self.entries.len() >= MAX_JIT_CACHE_ENTRIES; - let entry = match self.entries.entry(start) { - std::collections::hash_map::Entry::Occupied(entry) => entry.into_mut(), - std::collections::hash_map::Entry::Vacant(entry) => { - if at_capacity { - return None; - } - entry.insert(JitCacheEntry::default()) - } - }; - - if let Some(block) = entry.block { - self.fast_entries[fast_index] = FastJitCacheEntry { - start, - block: Some(block), - }; - let completed = - execute_compiled_block(block, instruction_limit, registers, ram, framebuffer); - if let Some(completed) = completed { - if completed == 0 { - entry.zero_instruction_exits = entry.zero_instruction_exits.saturating_add(1); - if entry.zero_instruction_exits >= MAX_ZERO_INSTRUCTION_EXITS { - entry.block = None; - entry.failed = true; - self.fast_entries[fast_index].block = None; - } - return None; - } - entry.zero_instruction_exits = 0; - return Some(completed); - } - return None; - } - if entry.failed { - return None; - } - - entry.hits = entry.hits.saturating_add(1); - if entry.hits < HOT_BLOCK_THRESHOLD || self.compile_budget == 0 { - return None; - } - if candidate_instruction_count(instructions) < MIN_COMPILED_BLOCK_LEN { - entry.failed = true; - return None; - } - - self.compile_budget = 0; - self.compile_cooldown = COMPILE_COOLDOWN_FRAMES; - let compile_start = Instant::now(); - let compile_result = self - .compiler - .as_mut() - .expect("compiler presence checked above") - .compile(start, instructions); - match compile_result { - Ok(Some(block)) => { - self.compiled_block_count = self.compiled_block_count.wrapping_add(1); - log::debug!( - "JIT compiled block {start:#010x}: instructions={} elapsed_us={} total={}", - block.instruction_count, - compile_start.elapsed().as_micros(), - self.compiled_block_count - ); - entry.block = Some(block); - self.fast_entries[fast_index] = FastJitCacheEntry { - start, - block: Some(block), - }; - execute_compiled_block(block, instruction_limit, registers, ram, framebuffer) - .filter(|&count| count != 0) - } - Ok(None) => { - entry.failed = true; - None - } - Err(error) => { - log::warn!("Failed to compile MIPS block at {start:#010x}: {error}"); - entry.failed = true; - None - } - } - } -} - -fn execute_compiled_block( - block: CompiledBlock, - instruction_limit: usize, - registers: &mut Registers, - ram: *mut u8, - framebuffer: *mut u8, -) -> Option { - if block.instruction_count > instruction_limit { - return None; - } - // SAFETY: Cranelift emitted this function with the exact signature below. - // Registers has a stable C layout and both pointers remain valid for the call. - let completed = unsafe { (block.function)(registers, ram, framebuffer) }; - Some(completed) -} - -fn candidate_instruction_count(instructions: &[u32]) -> usize { - let mut index = 0; - while index < instructions.len() { - let instruction = instructions[index]; - if is_control_flow(instruction) { - return if index + 1 < instructions.len() - && is_supported_delay_instruction(instructions[index + 1]) - && lowerable_branch(instruction) - { - index + 2 - } else { - index - }; - } - if !is_memory_instruction(instruction) && !is_supported_register_instruction(instruction) { - return index; - } - index += 1; - } - index -} - -fn lowerable_branch(instruction: u32) -> bool { - match instruction >> 26 { - 0 => matches!(instruction & 0x3f, 0x08 | 0x09), - 0x01 => matches!((instruction >> 16) & 0x1f, 0x00 | 0x01 | 0x10 | 0x11), - 0x02..=0x07 => true, - _ => false, - } -} - -impl Compiler { - fn new() -> anyhow::Result { - let mut flag_builder = settings::builder(); - flag_builder.set("opt_level", "speed")?; - flag_builder.set("enable_alias_analysis", "true")?; - let isa_builder = cranelift_native::builder() - .map_err(|error| anyhow::anyhow!("unsupported JIT host: {error}"))?; - let isa = isa_builder.finish(settings::Flags::new(flag_builder))?; - let builder = JITBuilder::with_isa(isa, default_libcall_names()); - let module = JITModule::new(builder); - let context = module.make_context(); - Ok(Self { - module, - context, - builder_context: FunctionBuilderContext::new(), - next_function_id: 0, - }) - } - - fn compile( - &mut self, - start: u32, - instructions: &[u32], - ) -> anyhow::Result> { - self.context.clear(); - self.builder_context = FunctionBuilderContext::new(); - let target_config = self.module.target_config(); - let pointer_type = target_config.pointer_type(); - debug_assert_eq!(pointer_type, types::I64); - - let mut signature = Signature::new(target_config.default_call_conv); - signature.params.push(AbiParam::new(pointer_type)); - signature.params.push(AbiParam::new(pointer_type)); - signature.params.push(AbiParam::new(pointer_type)); - signature.returns.push(AbiParam::new(types::I64)); - self.context.func = Function::with_name_signature( - cranelift::codegen::ir::UserFuncName::user(0, self.next_function_id as u32), - signature.clone(), - ); - - let instruction_count = { - let mut builder = - FunctionBuilder::new(&mut self.context.func, &mut self.builder_context); - let entry = builder.create_block(); - builder.append_block_params_for_function_params(entry); - builder.switch_to_block(entry); - builder.seal_block(entry); - let registers = builder.block_params(entry)[0]; - let ram = builder.block_params(entry)[1]; - let framebuffer = builder.block_params(entry)[2]; - let mut state = LoweringState::new(registers, ram, framebuffer); - let count = lower_block(&mut builder, &mut state, start, instructions); - if count != 0 { - builder.finalize(); - } - count - }; - - if instruction_count == 0 { - return Ok(None); - } - - let name = format!("jit_mips_block_{}", self.next_function_id); - self.next_function_id = self.next_function_id.wrapping_add(1); - let function_id = self - .module - .declare_function(&name, Linkage::Local, &signature)?; - self.module - .define_function(function_id, &mut self.context)?; - self.module.clear_context(&mut self.context); - self.module.finalize_definitions()?; - let code = self.module.get_finalized_function(function_id); - // SAFETY: The emitted function uses the JitBlockFn ABI and parameter types. - let function = unsafe { transmute::<*const u8, JitBlockFn>(code) }; - Ok(Some(CompiledBlock { - function, - instruction_count, - })) - } -} - -struct LoweringState { - registers: Value, - ram: Value, - framebuffer: Value, - values: [Option; REGISTER_COUNT], - dirty: [bool; REGISTER_COUNT], -} - -impl LoweringState { - fn new(registers: Value, ram: Value, framebuffer: Value) -> Self { - Self { - registers, - ram, - framebuffer, - values: [None; REGISTER_COUNT], - dirty: [false; REGISTER_COUNT], - } - } -} - -fn lower_block( - builder: &mut FunctionBuilder<'_>, - state: &mut LoweringState, - start: u32, - instructions: &[u32], -) -> usize { - let mut index = 0usize; - while index < instructions.len() { - let instruction = instructions[index]; - if is_control_flow(instruction) { - if index + 1 >= instructions.len() - || !is_supported_delay_instruction(instructions[index + 1]) - { - break; - } - let branch_pc = start.wrapping_add((index as u32).wrapping_mul(4)); - let Some(target) = lower_branch(builder, state, instruction, branch_pc) else { - break; - }; - if !lower_register_instruction(builder, state, instructions[index + 1]) { - break; - } - index += 2; - emit_exit(builder, state, target, index as u64); - return index; - } - - if is_memory_instruction(instruction) { - let pc = start.wrapping_add((index as u32).wrapping_mul(4)); - if !lower_memory_instruction(builder, state, instruction, pc, index as u64) { - break; - } - } else if !lower_register_instruction(builder, state, instruction) { - break; - } - index += 1; - } - - if index != 0 { - let pc = iconst_u32(builder, start.wrapping_add((index as u32).wrapping_mul(4))); - emit_exit(builder, state, pc, index as u64); - } - index -} - -fn is_control_flow(instruction: u32) -> bool { - let opcode = instruction >> 26; - matches!(opcode, 0x01..=0x07) || (opcode == 0 && matches!(instruction & 0x3f, 0x08 | 0x09)) -} - -fn is_supported_delay_instruction(instruction: u32) -> bool { - !is_control_flow(instruction) - && !is_memory_instruction(instruction) - && is_supported_register_instruction(instruction) -} - -fn is_supported_register_instruction(instruction: u32) -> bool { - if instruction == 0 { - return true; - } - let opcode = instruction >> 26; - match opcode { - 0 => matches!( - instruction & 0x3f, - 0x00 | 0x02 - | 0x03 - | 0x04 - | 0x06 - | 0x07 - | 0x0a - | 0x0b - | 0x0f - | 0x10 - | 0x11 - | 0x12 - | 0x13 - | 0x18 - | 0x19 - | 0x1a - | 0x1b - | 0x20..=0x27 | 0x2a | 0x2b - ), - 0x08..=0x0f | 0x33 => true, - 0x1c => matches!( - instruction & 0x3f, - 0x00 | 0x01 | 0x02 | 0x04 | 0x05 | 0x20 | 0x21 - ), - _ => false, - } -} - -fn is_memory_instruction(instruction: u32) -> bool { - matches!( - instruction >> 26, - 0x20 | 0x21 - | 0x22 - | 0x23 - | 0x24 - | 0x25 - | 0x26 - | 0x28 - | 0x29 - | 0x2a - | 0x2b - | 0x2e - | 0x30 - | 0x38 - ) -} - -fn lower_register_instruction( - builder: &mut FunctionBuilder<'_>, - state: &mut LoweringState, - instruction: u32, -) -> bool { - if instruction == 0 { - return true; - } - let opcode = instruction >> 26; - let rs = ((instruction >> 21) & 0x1f) as usize; - let rt = ((instruction >> 16) & 0x1f) as usize; - let rd = ((instruction >> 11) & 0x1f) as usize; - let shamt = (instruction >> 6) & 0x1f; - - let result = match opcode { - 0 => match instruction & 0x3f { - 0x00 => { - let value = read_register(builder, state, rt); - Some((rd, builder.ins().ishl_imm(value, i64::from(shamt)))) - } - 0x02 => { - let value = read_register(builder, state, rt); - Some((rd, builder.ins().ushr_imm(value, i64::from(shamt)))) - } - 0x03 => { - let value = read_register(builder, state, rt); - Some((rd, builder.ins().sshr_imm(value, i64::from(shamt)))) - } - 0x04 | 0x06 | 0x07 => { - let value = read_register(builder, state, rt); - let shift = read_register(builder, state, rs); - let shift = builder.ins().band_imm(shift, 0x1f); - let shifted = match instruction & 0x3f { - 0x04 => builder.ins().ishl(value, shift), - 0x06 => builder.ins().ushr(value, shift), - _ => builder.ins().sshr(value, shift), - }; - Some((rd, shifted)) - } - 0x0a | 0x0b => { - let source = read_register(builder, state, rs); - let condition_value = read_register(builder, state, rt); - let current = read_register(builder, state, rd); - let condition = builder.ins().icmp_imm( - if instruction & 0x3f == 0x0a { - IntCC::Equal - } else { - IntCC::NotEqual - }, - condition_value, - 0, - ); - Some((rd, builder.ins().select(condition, source, current))) - } - 0x0f => None, - 0x10 => Some((rd, read_special_register(builder, state, HI_INDEX))), - 0x11 => { - let value = read_register(builder, state, rs); - write_special_register(state, HI_INDEX, value); - None - } - 0x12 => Some((rd, read_special_register(builder, state, LO_INDEX))), - 0x13 => { - let value = read_register(builder, state, rs); - write_special_register(state, LO_INDEX, value); - None - } - 0x18 | 0x19 => { - let left = read_register(builder, state, rs); - let right = read_register(builder, state, rt); - let (left, right) = if instruction & 0x3f == 0x18 { - ( - builder.ins().sextend(types::I64, left), - builder.ins().sextend(types::I64, right), - ) - } else { - ( - builder.ins().uextend(types::I64, left), - builder.ins().uextend(types::I64, right), - ) - }; - let product = builder.ins().imul(left, right); - write_i64_pair(builder, state, product); - None - } - 0x1a | 0x1b => { - let numerator = read_register(builder, state, rs); - let denominator = read_register(builder, state, rt); - let zero = builder.ins().icmp_imm(IntCC::Equal, denominator, 0); - let invalid = if instruction & 0x3f == 0x1a { - let minimum = iconst_u32(builder, i32::MIN as u32); - let negative_one = iconst_u32(builder, u32::MAX); - let is_minimum = builder.ins().icmp(IntCC::Equal, numerator, minimum); - let is_negative_one = - builder.ins().icmp(IntCC::Equal, denominator, negative_one); - let overflow = builder.ins().band(is_minimum, is_negative_one); - builder.ins().bor(zero, overflow) - } else { - zero - }; - let one = iconst_u32(builder, 1); - let safe_denominator = builder.ins().select(invalid, one, denominator); - let (quotient, remainder) = if instruction & 0x3f == 0x1a { - ( - builder.ins().sdiv(numerator, safe_denominator), - builder.ins().srem(numerator, safe_denominator), - ) - } else { - ( - builder.ins().udiv(numerator, safe_denominator), - builder.ins().urem(numerator, safe_denominator), - ) - }; - let old_lo = read_special_register(builder, state, LO_INDEX); - let old_hi = read_special_register(builder, state, HI_INDEX); - let lo = builder.ins().select(invalid, old_lo, quotient); - let hi = builder.ins().select(invalid, old_hi, remainder); - write_special_register(state, LO_INDEX, lo); - write_special_register(state, HI_INDEX, hi); - None - } - 0x20 | 0x21 => { - let left = read_register(builder, state, rs); - let right = read_register(builder, state, rt); - Some((rd, builder.ins().iadd(left, right))) - } - 0x22 | 0x23 => { - let left = read_register(builder, state, rs); - let right = read_register(builder, state, rt); - Some((rd, builder.ins().isub(left, right))) - } - 0x24 => { - let left = read_register(builder, state, rs); - let right = read_register(builder, state, rt); - Some((rd, builder.ins().band(left, right))) - } - 0x25 => { - let left = read_register(builder, state, rs); - let right = read_register(builder, state, rt); - Some((rd, builder.ins().bor(left, right))) - } - 0x26 => { - let left = read_register(builder, state, rs); - let right = read_register(builder, state, rt); - Some((rd, builder.ins().bxor(left, right))) - } - 0x27 => { - let left = read_register(builder, state, rs); - let right = read_register(builder, state, rt); - let value = builder.ins().bor(left, right); - Some((rd, builder.ins().bnot(value))) - } - 0x2a | 0x2b => { - let left = read_register(builder, state, rs); - let right = read_register(builder, state, rt); - let condition = builder.ins().icmp( - if instruction & 0x3f == 0x2a { - IntCC::SignedLessThan - } else { - IntCC::UnsignedLessThan - }, - left, - right, - ); - Some((rd, builder.ins().uextend(types::I32, condition))) - } - _ => return false, - }, - 0x08 | 0x09 => { - let source = read_register(builder, state, rs); - let immediate = iconst_u32(builder, instruction as i16 as i32 as u32); - Some((rt, builder.ins().iadd(source, immediate))) - } - 0x0a | 0x0b => { - let source = read_register(builder, state, rs); - let immediate = iconst_u32(builder, instruction as i16 as i32 as u32); - let condition = builder.ins().icmp( - if opcode == 0x0a { - IntCC::SignedLessThan - } else { - IntCC::UnsignedLessThan - }, - source, - immediate, - ); - Some((rt, builder.ins().uextend(types::I32, condition))) - } - 0x0c => { - let source = read_register(builder, state, rs); - Some(( - rt, - builder - .ins() - .band_imm(source, i64::from(instruction as u16)), - )) - } - 0x0d => { - let source = read_register(builder, state, rs); - Some(( - rt, - builder.ins().bor_imm(source, i64::from(instruction as u16)), - )) - } - 0x0e => { - let source = read_register(builder, state, rs); - Some(( - rt, - builder - .ins() - .bxor_imm(source, i64::from(instruction as u16)), - )) - } - 0x0f => Some((rt, iconst_u32(builder, u32::from(instruction as u16) << 16))), - 0x1c if matches!( - instruction & 0x3f, - 0x00 | 0x01 | 0x02 | 0x04 | 0x05 | 0x20 | 0x21 - ) => - { - lower_special2(builder, state, instruction, rs, rt, rd); - None - } - 0x33 => None, - _ => return false, - }; - - if let Some((register, value)) = result { - write_register(state, register, value); - } - true -} - -fn lower_special2( - builder: &mut FunctionBuilder<'_>, - state: &mut LoweringState, - instruction: u32, - rs: usize, - rt: usize, - rd: usize, -) { - match instruction & 0x3f { - 0x00 | 0x01 | 0x04 | 0x05 => { - let left = read_register(builder, state, rs); - let right = read_register(builder, state, rt); - let left = builder.ins().uextend(types::I64, left); - let right = builder.ins().uextend(types::I64, right); - let product = builder.ins().imul(left, right); - let hi = read_special_register(builder, state, HI_INDEX); - let lo = read_special_register(builder, state, LO_INDEX); - let hi = builder.ins().uextend(types::I64, hi); - let hi = builder.ins().ishl_imm(hi, 32); - let lo = builder.ins().uextend(types::I64, lo); - let accumulator = builder.ins().bor(hi, lo); - let result = if matches!(instruction & 0x3f, 0x00 | 0x01) { - builder.ins().iadd(accumulator, product) - } else { - builder.ins().isub(accumulator, product) - }; - write_i64_pair(builder, state, result); - } - 0x02 => { - let left = read_register(builder, state, rs); - let right = read_register(builder, state, rt); - let result = builder.ins().imul(left, right); - write_register(state, rd, result); - } - 0x20 | 0x21 => { - let value = read_register(builder, state, rs); - let value = if instruction & 0x3f == 0x21 { - builder.ins().bnot(value) - } else { - value - }; - let result = builder.ins().clz(value); - write_register(state, rd, result); - } - _ => unreachable!("instruction support checked before lowering"), - } -} - -fn lower_branch( - builder: &mut FunctionBuilder<'_>, - state: &mut LoweringState, - instruction: u32, - branch_pc: u32, -) -> Option { - let opcode = instruction >> 26; - let rs = ((instruction >> 21) & 0x1f) as usize; - let rt = ((instruction >> 16) & 0x1f) as usize; - let fallthrough = branch_pc.wrapping_add(8); - let branch_target = branch_pc - .wrapping_add(4) - .wrapping_add(((instruction as i16 as i32) << 2) as u32); - - match opcode { - 0 => match instruction & 0x3f { - 0x08 => Some(read_register(builder, state, rs)), - 0x09 => { - let target = read_register(builder, state, rs); - write_register( - state, - ((instruction >> 11) & 0x1f) as usize, - iconst_u32(builder, fallthrough), - ); - Some(target) - } - _ => None, - }, - 0x01 => { - let kind = rt; - if matches!(kind, 0x10 | 0x11) { - let link = iconst_u32(builder, fallthrough); - write_register(state, 31, link); - } - let source = read_register(builder, state, rs); - let condition = match kind { - 0x00 | 0x10 => builder.ins().icmp_imm(IntCC::SignedLessThan, source, 0), - 0x01 | 0x11 => builder - .ins() - .icmp_imm(IntCC::SignedGreaterThanOrEqual, source, 0), - _ => return None, - }; - Some(select_branch_target( - builder, - condition, - branch_target, - fallthrough, - )) - } - 0x02 | 0x03 => { - if opcode == 0x03 { - let link = iconst_u32(builder, fallthrough); - write_register(state, 31, link); - } - Some(iconst_u32( - builder, - (branch_pc & 0xf000_0000) | ((instruction & 0x03ff_ffff) << 2), - )) - } - 0x04 | 0x05 => { - let left = read_register(builder, state, rs); - let right = read_register(builder, state, rt); - let condition = builder.ins().icmp( - if opcode == 0x04 { - IntCC::Equal - } else { - IntCC::NotEqual - }, - left, - right, - ); - Some(select_branch_target( - builder, - condition, - branch_target, - fallthrough, - )) - } - 0x06 | 0x07 => { - let source = read_register(builder, state, rs); - let condition = builder.ins().icmp_imm( - if opcode == 0x06 { - IntCC::SignedLessThanOrEqual - } else { - IntCC::SignedGreaterThan - }, - source, - 0, - ); - Some(select_branch_target( - builder, - condition, - branch_target, - fallthrough, - )) - } - _ => None, - } -} - -fn select_branch_target( - builder: &mut FunctionBuilder<'_>, - condition: Value, - taken: u32, - fallthrough: u32, -) -> Value { - let taken = iconst_u32(builder, taken); - let fallthrough = iconst_u32(builder, fallthrough); - builder.ins().select(condition, taken, fallthrough) -} - -fn lower_memory_instruction( - builder: &mut FunctionBuilder<'_>, - state: &mut LoweringState, - instruction: u32, - pc: u32, - completed: u64, -) -> bool { - let opcode = instruction >> 26; - let rs = ((instruction >> 21) & 0x1f) as usize; - let rt = ((instruction >> 16) & 0x1f) as usize; - let width = match opcode { - 0x20 | 0x24 | 0x28 => 1u32, - 0x21 | 0x25 | 0x29 => 2, - 0x22 | 0x23 | 0x26 | 0x2a | 0x2b | 0x2e | 0x30 | 0x38 => 4, - _ => return false, - }; - let base = read_register(builder, state, rs); - let offset = iconst_u32(builder, instruction as i16 as i32 as u32); - let address = builder.ins().iadd(base, offset); - let unaligned = matches!(opcode, 0x22 | 0x26 | 0x2a | 0x2e); - let access_address = if unaligned { - builder.ins().band_imm(address, i64::from(!3u32)) - } else { - address - }; - let physical = translate_address(builder, access_address); - let ram_in_bounds = builder.ins().icmp_imm( - IntCC::UnsignedLessThanOrEqual, - physical, - i64::from(crate::memory::RAM_SIZE - width), - ); - let physical_pointer = builder.ins().uextend(types::I64, physical); - let ram_address = builder.ins().iadd(state.ram, physical_pointer); - let fast = builder.create_block(); - builder.append_block_param(fast, types::I64); - let check_framebuffer = builder.create_block(); - let slow = builder.create_block(); - let ram_args = [ram_address.into()]; - builder - .ins() - .brif(ram_in_bounds, fast, &ram_args, check_framebuffer, &[]); - - builder.switch_to_block(check_framebuffer); - builder.seal_block(check_framebuffer); - let mut framebuffer_address = state.framebuffer; - let mut mapped = None; - for base in crate::memory::LCD_FRAMEBUFFER_ALIASES { - let alias_offset = builder.ins().iadd_imm(access_address, -i64::from(base)); - let alias_in_bounds = builder.ins().icmp_imm( - IntCC::UnsignedLessThanOrEqual, - alias_offset, - (crate::video::FRAMEBUFFER_MAP_SIZE as u32 - width) as i64, - ); - let alias_pointer = builder.ins().uextend(types::I64, alias_offset); - let alias_address = builder.ins().iadd(state.framebuffer, alias_pointer); - framebuffer_address = - builder - .ins() - .select(alias_in_bounds, alias_address, framebuffer_address); - mapped = Some(match mapped { - Some(previous) => builder.ins().bor(previous, alias_in_bounds), - None => alias_in_bounds, - }); - } - let framebuffer_args = [framebuffer_address.into()]; - builder.ins().brif( - mapped.expect("framebuffer has at least one alias"), - fast, - &framebuffer_args, - slow, - &[], - ); - - builder.switch_to_block(slow); - builder.seal_block(slow); - let bailout_pc = iconst_u32(builder, pc); - emit_exit(builder, state, bailout_pc, completed); - - builder.switch_to_block(fast); - builder.seal_block(fast); - let host_address = builder.block_params(fast)[0]; - let flags = MemFlags::new(); - - match opcode { - 0x20 | 0x21 | 0x23 | 0x24 | 0x25 | 0x30 => { - let load_type = match width { - 1 => types::I8, - 2 => types::I16, - _ => types::I32, - }; - let value = builder.ins().load(load_type, flags, host_address, 0); - let value = match opcode { - 0x20 | 0x21 => builder.ins().sextend(types::I32, value), - 0x24 | 0x25 => builder.ins().uextend(types::I32, value), - _ => value, - }; - write_register(state, rt, value); - } - 0x22 | 0x26 => { - let memory = builder.ins().load(types::I32, flags, host_address, 0); - let current = read_register(builder, state, rt); - let byte = builder.ins().band_imm(address, 3); - let cases = if opcode == 0x22 { - [ - merge_shift_left(builder, memory, 24, current, 0x00ff_ffff), - merge_shift_left(builder, memory, 16, current, 0x0000_ffff), - merge_shift_left(builder, memory, 8, current, 0x0000_00ff), - memory, - ] - } else { - [ - memory, - merge_shift_right(builder, memory, 8, current, 0xff00_0000), - merge_shift_right(builder, memory, 16, current, 0xffff_0000), - merge_shift_right(builder, memory, 24, current, 0xffff_ff00), - ] - }; - let value = select_byte_case(builder, byte, cases); - write_register(state, rt, value); - } - 0x28 | 0x29 | 0x2b | 0x38 => { - let value = read_register(builder, state, rt); - let value = match width { - 1 => builder.ins().ireduce(types::I8, value), - 2 => builder.ins().ireduce(types::I16, value), - _ => value, - }; - builder.ins().store(flags, value, host_address, 0); - if opcode == 0x38 { - let success = iconst_u32(builder, 1); - write_register(state, rt, success); - } - } - 0x2a | 0x2e => { - let memory = builder.ins().load(types::I32, flags, host_address, 0); - let value = read_register(builder, state, rt); - let byte = builder.ins().band_imm(address, 3); - let cases = if opcode == 0x2a { - [ - merge_store_right(builder, memory, 0xffff_ff00, value, 24), - merge_store_right(builder, memory, 0xffff_0000, value, 16), - merge_store_right(builder, memory, 0xff00_0000, value, 8), - value, - ] - } else { - [ - value, - merge_store_left(builder, memory, 0x0000_00ff, value, 8), - merge_store_left(builder, memory, 0x0000_ffff, value, 16), - merge_store_left(builder, memory, 0x00ff_ffff, value, 24), - ] - }; - let value = select_byte_case(builder, byte, cases); - builder.ins().store(flags, value, host_address, 0); - } - _ => return false, - } - true -} - -fn merge_shift_left( - builder: &mut FunctionBuilder<'_>, - memory: Value, - shift: i64, - current: Value, - mask: u32, -) -> Value { - let memory = builder.ins().ishl_imm(memory, shift); - let current = builder.ins().band_imm(current, i64::from(mask)); - builder.ins().bor(memory, current) -} - -fn merge_shift_right( - builder: &mut FunctionBuilder<'_>, - memory: Value, - shift: i64, - current: Value, - mask: u32, -) -> Value { - let memory = builder.ins().ushr_imm(memory, shift); - let current = builder.ins().band_imm(current, i64::from(mask)); - builder.ins().bor(memory, current) -} - -fn merge_store_right( - builder: &mut FunctionBuilder<'_>, - memory: Value, - mask: u32, - value: Value, - shift: i64, -) -> Value { - let memory = builder.ins().band_imm(memory, i64::from(mask)); - let value = builder.ins().ushr_imm(value, shift); - builder.ins().bor(memory, value) -} - -fn merge_store_left( - builder: &mut FunctionBuilder<'_>, - memory: Value, - mask: u32, - value: Value, - shift: i64, -) -> Value { - let memory = builder.ins().band_imm(memory, i64::from(mask)); - let value = builder.ins().ishl_imm(value, shift); - builder.ins().bor(memory, value) -} - -fn select_byte_case(builder: &mut FunctionBuilder<'_>, byte: Value, cases: [Value; 4]) -> Value { - let mut result = cases[3]; - for index in (0..3).rev() { - let matches = builder.ins().icmp_imm(IntCC::Equal, byte, index as i64); - result = builder.ins().select(matches, cases[index], result); - } - result -} - -fn translate_address(builder: &mut FunctionBuilder<'_>, address: Value) -> Value { - let segment = builder.ins().band_imm(address, i64::from(0xe000_0000_u32)); - let kseg0 = iconst_u32(builder, 0x8000_0000); - let kseg1 = iconst_u32(builder, 0xa000_0000); - let is_kseg0 = builder.ins().icmp(IntCC::Equal, segment, kseg0); - let is_kseg1 = builder.ins().icmp(IntCC::Equal, segment, kseg1); - let masked = builder.ins().band_imm(address, i64::from(KSEG_MASK)); - let physical = builder.ins().select(is_kseg0, masked, address); - builder.ins().select(is_kseg1, masked, physical) -} - -fn read_register( - builder: &mut FunctionBuilder<'_>, - state: &mut LoweringState, - register: usize, -) -> Value { - if register == 0 { - return iconst_u32(builder, 0); - } - read_special_register(builder, state, register) -} - -fn write_register(state: &mut LoweringState, register: usize, value: Value) { - if register != 0 { - write_special_register(state, register, value); - } -} - -fn read_special_register( - builder: &mut FunctionBuilder<'_>, - state: &mut LoweringState, - register: usize, -) -> Value { - if let Some(value) = state.values[register] { - return value; - } - let value = builder.ins().load( - types::I32, - MemFlags::new(), - state.registers, - register_offset(register), - ); - state.values[register] = Some(value); - value -} - -fn write_special_register(state: &mut LoweringState, register: usize, value: Value) { - state.values[register] = Some(value); - state.dirty[register] = true; -} - -fn write_i64_pair(builder: &mut FunctionBuilder<'_>, state: &mut LoweringState, value: Value) { - let lo = builder.ins().ireduce(types::I32, value); - let hi = builder.ins().ushr_imm(value, 32); - let hi = builder.ins().ireduce(types::I32, hi); - write_special_register(state, LO_INDEX, lo); - write_special_register(state, HI_INDEX, hi); -} - -fn register_offset(register: usize) -> i32 { - let offset = match register { - 0..=31 => offset_of!(Registers, gpr) + register * std::mem::size_of::(), - HI_INDEX => offset_of!(Registers, hi), - LO_INDEX => offset_of!(Registers, lo), - _ => unreachable!("invalid register index"), - }; - offset as i32 -} - -fn emit_exit(builder: &mut FunctionBuilder<'_>, state: &LoweringState, pc: Value, completed: u64) { - for register in 1..REGISTER_COUNT { - if state.dirty[register] { - builder.ins().store( - MemFlags::new(), - state.values[register].expect("dirty registers always have a value"), - state.registers, - register_offset(register), - ); - } - } - builder.ins().store( - MemFlags::new(), - pc, - state.registers, - offset_of!(Registers, pc) as i32, - ); - let completed = builder.ins().iconst(types::I64, completed as i64); - builder.ins().return_(&[completed]); -} - -fn iconst_u32(builder: &mut FunctionBuilder<'_>, value: u32) -> Value { - builder.ins().iconst(types::I32, i64::from(value)) -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::cpu::Cpu; - use crate::memory::Memory; - - #[test] - fn register_layout_matches_jit_offsets() { - assert_eq!(register_offset(0), offset_of!(Registers, gpr) as i32); - assert_eq!(register_offset(HI_INDEX), offset_of!(Registers, hi) as i32); - assert_eq!(register_offset(LO_INDEX), offset_of!(Registers, lo) as i32); - } - - #[test] - fn compilation_budget_preserves_colliding_hot_blocks() { - let instructions = [ - (0x09 << 26) | (8 << 16) | 1, - (0x09 << 26) | (8 << 21) | (8 << 16) | 1, - (8 << 21) | (8 << 16) | (9 << 11) | 0x21, - (9 << 21) | (8 << 16) | (10 << 11) | 0x26, - ]; - let first_start = 0x1000; - let second_start = first_start + 0x4000; - let mut engine = JitEngine::new(); - let mut registers = Registers::new(first_start); - let mut memory = Memory::new(); - let ram = memory.jit_ram_ptr(); - let framebuffer = memory.jit_framebuffer_ptr(); - - for _ in 0..HOT_BLOCK_THRESHOLD { - let _ = engine.execute( - first_start, - &instructions, - instructions.len(), - &mut registers, - ram, - framebuffer, - ); - } - assert!(engine.entries[&first_start].block.is_some()); - - registers.pc = second_start; - for _ in 0..HOT_BLOCK_THRESHOLD { - assert!(engine - .execute( - second_start, - &instructions, - instructions.len(), - &mut registers, - ram, - framebuffer, - ) - .is_none()); - } - assert!(engine.entries[&second_start].block.is_none()); - - for _ in 0..=COMPILE_COOLDOWN_FRAMES { - engine.begin_frame(); - } - assert!(engine - .execute( - second_start, - &instructions, - instructions.len(), - &mut registers, - ram, - framebuffer, - ) - .is_some()); - assert!(engine.entries[&first_start].block.is_some()); - assert!(engine.entries[&second_start].block.is_some()); - } - - #[test] - fn clearing_cache_recreates_compiler_after_native_code_generation() { - let instructions = [ - (0x09 << 26) | (8 << 16) | 1, - (0x09 << 26) | (8 << 21) | (8 << 16) | 1, - (8 << 21) | (8 << 16) | (9 << 11) | 0x21, - (9 << 21) | (8 << 16) | (10 << 11) | 0x26, - ]; - let start = 0x1000; - let mut engine = JitEngine::new(); - let mut registers = Registers::new(start); - let mut memory = Memory::new(); - let ram = memory.jit_ram_ptr(); - let framebuffer = memory.jit_framebuffer_ptr(); - - for _ in 0..HOT_BLOCK_THRESHOLD { - let _ = engine.execute( - start, - &instructions, - instructions.len(), - &mut registers, - ram, - framebuffer, - ); - } - assert_eq!(engine.compiled_block_count, 1); - assert_eq!(engine.compiler.as_ref().unwrap().next_function_id, 1); - - engine.clear(); - - assert!(engine.entries.is_empty()); - assert!(engine - .fast_entries - .iter() - .all(|entry| entry.block.is_none())); - assert_eq!(engine.compiled_block_count, 0); - assert_eq!(engine.compiler.as_ref().unwrap().next_function_id, 0); - } - - #[test] - fn compiled_block_matches_interpreter_state() { - let start = 0x1000; - let instructions = [ - (0x09 << 26) | (8 << 16) | 5, // addiu t0, zero, 5 - (0x09 << 26) | (8 << 21) | (9 << 16) | 0xfffe, // addiu t1, t0, -2 - (9 << 16) | (10 << 11) | (4 << 6), // sll t2, t1, 4 - (0x2b << 26) | (10 << 16) | 0x0200, // sw t2, 0x200(zero) - (0x23 << 26) | (11 << 16) | 0x0200, // lw t3, 0x200(zero) - (0x04 << 26) | (11 << 21) | (10 << 16) | 2, // beq t3, t2, +2 - (0x09 << 26) | (12 << 16) | 7, // addiu t4, zero, 7 - ]; - - let mut compiler = Compiler::new().unwrap(); - let block = compiler.compile(start, &instructions).unwrap().unwrap(); - assert_eq!(block.instruction_count, instructions.len()); - - let mut jit_registers = Registers::new(start); - let mut jit_memory = Memory::new(); - let ram = jit_memory.jit_ram_ptr(); - let framebuffer = jit_memory.jit_framebuffer_ptr(); - let completed = unsafe { (block.function)(&mut jit_registers, ram, framebuffer) }; - assert_eq!(completed, instructions.len() as u64); - - let mut interpreter = Cpu::new(start); - let mut interpreter_memory = Memory::new(); - interpreter.start(); - for &instruction in &instructions { - assert!(interpreter - .step_fetched_unaccounted(instruction, &mut interpreter_memory) - .unwrap()); - } - - assert_eq!(jit_registers.gpr, interpreter.regs.gpr); - assert_eq!(jit_registers.pc, interpreter.regs.pc); - assert_eq!(jit_registers.hi, interpreter.regs.hi); - assert_eq!(jit_registers.lo, interpreter.regs.lo); - assert_eq!(jit_memory.read_u32(0x200).unwrap(), 48); - assert_eq!( - jit_memory.read_u32(0x200).unwrap(), - interpreter_memory.read_u32(0x200).unwrap() - ); - } - - #[test] - fn extended_integer_and_unaligned_memory_match_interpreter() { - let start = 0x1800; - let instructions = [ - (8 << 21) | (9 << 16) | 0x1a, // div t0, t1 - (10 << 11) | 0x12, // mflo t2 - (11 << 11) | 0x10, // mfhi t3 - (0x22 << 26) | (4 << 21) | (5 << 16) | 3, // lwl a1, 3(a0) - (0x26 << 26) | (4 << 21) | (5 << 16), // lwr a1, 0(a0) - (0x2a << 26) | (6 << 21) | (7 << 16) | 3, // swl a3, 3(a2) - (0x2e << 26) | (6 << 21) | (7 << 16), // swr a3, 0(a2) - (8 << 21) | (12 << 16) | 0x1a, // div t0, t4 - (13 << 11) | 0x12, // mflo t5 - (14 << 11) | 0x10, // mfhi t6 - ]; - let mut compiler = Compiler::new().unwrap(); - let block = compiler.compile(start, &instructions).unwrap().unwrap(); - - let mut jit_registers = Registers::new(start); - jit_registers.write(4, 0x101); - jit_registers.write(5, 0xdead_beef); - jit_registers.write(6, 0x201); - jit_registers.write(7, 0x4433_2211); - jit_registers.write(8, (-7i32) as u32); - jit_registers.write(9, 3); - jit_registers.write(12, 0); - let mut interpreter = Cpu::new(start); - interpreter.regs = jit_registers.clone(); - interpreter.start(); - - let mut jit_memory = Memory::new(); - let mut interpreter_memory = Memory::new(); - for memory in [&mut jit_memory, &mut interpreter_memory] { - memory - .load_data(0x100, &[0x11, 0x22, 0x33, 0x44, 0x55]) - .unwrap(); - memory.load_data(0x200, &[0xaa; 8]).unwrap(); - } - let ram = jit_memory.jit_ram_ptr(); - let framebuffer = jit_memory.jit_framebuffer_ptr(); - let completed = unsafe { (block.function)(&mut jit_registers, ram, framebuffer) }; - for &instruction in &instructions { - interpreter - .step_fetched_unaccounted(instruction, &mut interpreter_memory) - .unwrap(); - } - - assert_eq!(completed, instructions.len() as u64); - assert_eq!(jit_registers.gpr, interpreter.regs.gpr); - assert_eq!(jit_registers.pc, interpreter.regs.pc); - assert_eq!(jit_registers.hi, interpreter.regs.hi); - assert_eq!(jit_registers.lo, interpreter.regs.lo); - assert_eq!( - &jit_memory.system_ram()[0x200..0x208], - &interpreter_memory.system_ram()[0x200..0x208] - ); - } - - #[test] - fn special_memory_mapping_bails_out_before_side_effects() { - let start = 0x2000; - let instructions = [ - (0x2b << 26) | (9 << 21) | (8 << 16), - (0x09 << 26) | (10 << 16) | 1, - ]; - let mut compiler = Compiler::new().unwrap(); - let block = compiler.compile(start, &instructions).unwrap().unwrap(); - let mut registers = Registers::new(start); - registers.write(8, 0x1234_5678); - registers.write(9, 0x1308_0000); - let mut memory = Memory::new(); - let ram = memory.jit_ram_ptr(); - let framebuffer = memory.jit_framebuffer_ptr(); - - let completed = unsafe { (block.function)(&mut registers, ram, framebuffer) }; - - assert_eq!(completed, 0); - assert_eq!(registers.pc, start); - assert_eq!(registers.read(10), 0); - assert_eq!(memory.read_u32(0x1308_0000).unwrap(), 0); - } - - #[test] - fn framebuffer_alias_uses_native_memory_path() { - let start = 0x3000; - let instructions = [(0x2b << 26) | (9 << 21) | (8 << 16)]; - let mut compiler = Compiler::new().unwrap(); - let block = compiler.compile(start, &instructions).unwrap().unwrap(); - let mut registers = Registers::new(start); - registers.write(8, 0x1234_5678); - registers.write(9, crate::video::VM_LCD_FB_ADDRESS); - let mut memory = Memory::new(); - let ram = memory.jit_ram_ptr(); - let framebuffer = memory.jit_framebuffer_ptr(); - - let completed = unsafe { (block.function)(&mut registers, ram, framebuffer) }; - - assert_eq!(completed, 1); - assert_eq!(registers.pc, start + 4); - assert_eq!( - memory.read_u32(crate::video::VM_LCD_FB_ADDRESS).unwrap(), - 0x1234_5678 - ); - } -} diff --git a/native/DingooEmu/crates/dingooemu-core/src/lib.rs b/native/DingooEmu/crates/dingooemu-core/src/lib.rs deleted file mode 100644 index c50e28b..0000000 --- a/native/DingooEmu/crates/dingooemu-core/src/lib.rs +++ /dev/null @@ -1,22 +0,0 @@ -//! dingooemu-core: Platform-independent Dingoo A320 emulator engine -//! -//! This crate contains all emulation logic with no dependency on any windowing -//! or audio output device. Both front-ends (standalone and libretro) link -//! against this crate. - -pub mod app_loader; -pub mod audio; -pub mod cheats; -pub mod cpu; -pub mod emulator; -pub mod error; -pub mod input; -#[cfg(feature = "jit")] -mod jit; -pub mod memory; -mod save_state; -pub mod video; - -// Re-export main types for convenience -pub use emulator::{Emulator, UnknownHleCall, UnknownHlePolicy}; -pub use error::{Result, SimulatorError}; diff --git a/native/DingooEmu/crates/dingooemu-core/src/memory.rs b/native/DingooEmu/crates/dingooemu-core/src/memory.rs deleted file mode 100644 index a61a6a6..0000000 --- a/native/DingooEmu/crates/dingooemu-core/src/memory.rs +++ /dev/null @@ -1,804 +0,0 @@ -use crate::error::{Result, SimulatorError}; -use crate::video::FRAMEBUFFER_MAP_SIZE; - -/// Dingoo A320 memory regions -const RAM_BASE: u32 = 0x0000_0000; -pub(crate) const RAM_SIZE: u32 = 32 * 1024 * 1024; // 32 MB - -/// MIPS KSEG0 mask (strip top 3 bits for cached segment) -const KSEG0_MASK: u32 = 0x1FFF_FFFF; - -/// Guest-visible aliases for the LCD framebuffer mapping. -pub(crate) const LCD_FRAMEBUFFER_ALIASES: [u32; 4] = [ - crate::video::VM_LCD_FB_ADDRESS, - 0x1400_0000, - 0x9000_0000, - 0x1000_0000, -]; -const IPU_BASE: u32 = 0x1308_0000; -const IPU_REGISTER_SIZE: usize = 0x100; -const IPU_CTRL_OFFSET: usize = 0x00; -const IPU_STATUS_OFFSET: usize = 0x04; -const IPU_D_FMT_OFFSET: usize = 0x08; -const IPU_Y_ADDR_OFFSET: usize = 0x0C; -const IPU_U_ADDR_OFFSET: usize = 0x10; -const IPU_V_ADDR_OFFSET: usize = 0x14; -const IPU_IN_GS_OFFSET: usize = 0x18; -const IPU_Y_STRIDE_OFFSET: usize = 0x1C; -const IPU_UV_STRIDE_OFFSET: usize = 0x20; -const IPU_OUT_ADDR_OFFSET: usize = 0x24; -const IPU_OUT_GS_OFFSET: usize = 0x28; -const IPU_OUT_STRIDE_OFFSET: usize = 0x2C; -const IPU_CTRL_RUN: u32 = 1 << 1; -const IPU_STATUS_OUT_END: u8 = 1; -const TCU_BASE: u32 = 0x1000_2000; -const TCU_SIZE: u32 = 0xA0; -const TCU_FULL_MATCH_FLAG_OFFSET: u32 = 0x20; -const TCU_FULL_MATCH_TIMER2: u32 = 1 << 2; -const GPIO_BASE: u32 = 0x1001_0000; -const GPIO_SIZE: u32 = 0x400; - -/// Memory manager for the Dingoo A320 -#[derive(serde::Serialize, serde::Deserialize)] -pub struct Memory { - /// Main RAM (32 MB) - ram: Box<[u8]>, - /// LCD framebuffer memory shared by all guest aliases - framebuffer: Box<[u8]>, - /// Image processing unit registers - ipu_registers: Box<[u8]>, - /// Heap pointer (next allocation address) - heap_ptr: u32, - /// Heap allocations (addr -> size) - allocations: std::collections::HashMap, - /// Reusable free heap blocks (addr, size), sorted by address - free_blocks: Vec<(u32, u32)>, - /// Write tracking: addresses that were written to - write_log: Vec, -} - -impl Memory { - /// Create a new memory instance with all RAM zeroed - pub fn new() -> Self { - // Heap starts in the middle of RAM (16MB offset) - Self { - ram: vec![0u8; RAM_SIZE as usize].into_boxed_slice(), - framebuffer: vec![0u8; FRAMEBUFFER_MAP_SIZE].into_boxed_slice(), - ipu_registers: { - let mut registers = vec![0u8; IPU_REGISTER_SIZE].into_boxed_slice(); - registers[IPU_STATUS_OFFSET] = IPU_STATUS_OUT_END; - registers - }, - heap_ptr: 0x0100_0000, // 16MB - allocations: std::collections::HashMap::new(), - free_blocks: Vec::new(), - write_log: Vec::new(), - } - } - - #[cfg(feature = "jit")] - #[inline(always)] - pub(crate) fn jit_ram_ptr(&mut self) -> *mut u8 { - self.ram.as_mut_ptr() - } - - #[cfg(feature = "jit")] - #[inline(always)] - pub(crate) fn jit_framebuffer_ptr(&mut self) -> *mut u8 { - self.framebuffer.as_mut_ptr() - } - - /// Get a copy of the write log and clear it - pub fn consume_write_log(&mut self) -> Vec { - std::mem::take(&mut self.write_log) - } - - /// Return the offset inside the LCD framebuffer mapping, if any. - fn framebuffer_offset(&self, addr: u32) -> Option { - LCD_FRAMEBUFFER_ALIASES.iter().find_map(|&base| { - let offset = addr.wrapping_sub(base); - (offset < FRAMEBUFFER_MAP_SIZE as u32).then_some(offset as usize) - }) - } - - fn ipu_register_offset(&self, addr: u32) -> Option { - let physical = self.translate_address(addr); - let offset = physical.wrapping_sub(IPU_BASE); - (offset < IPU_REGISTER_SIZE as u32).then_some(offset as usize) - } - - fn peripheral_offset(&self, addr: u32, base: u32, size: u32, access_size: u32) -> Option { - let offset = self.translate_address(addr).wrapping_sub(base); - (offset.checked_add(access_size)? <= size).then_some(offset) - } - - fn stubbed_peripheral_read(&self, addr: u32, access_size: u32) -> Option { - if let Some(offset) = self.peripheral_offset(addr, TCU_BASE, TCU_SIZE, access_size) { - // Frame timing is driven by tick(), so report timer 2 as completed. - return Some(if offset == TCU_FULL_MATCH_FLAG_OFFSET { - TCU_FULL_MATCH_TIMER2 - } else { - 0 - }); - } - self.peripheral_offset(addr, GPIO_BASE, GPIO_SIZE, access_size) - .map(|_| 0) - } - - fn is_stubbed_peripheral_write(&self, addr: u32, access_size: u32) -> bool { - self.peripheral_offset(addr, TCU_BASE, TCU_SIZE, access_size) - .or_else(|| self.peripheral_offset(addr, GPIO_BASE, GPIO_SIZE, access_size)) - .is_some() - } - - fn ipu_register_u32(&self, offset: usize) -> u32 { - u32::from_le_bytes([ - self.ipu_registers[offset], - self.ipu_registers[offset + 1], - self.ipu_registers[offset + 2], - self.ipu_registers[offset + 3], - ]) - } - - fn run_ipu(&mut self) -> Result<()> { - let format = self.ipu_register_u32(IPU_D_FMT_OFFSET); - if format & 0x3 != 1 { - self.ipu_registers[IPU_STATUS_OFFSET] = IPU_STATUS_OUT_END; - return Ok(()); - } - - let input_geometry = self.ipu_register_u32(IPU_IN_GS_OFFSET); - let input_width = (input_geometry >> 16) as usize; - let input_height = (input_geometry & 0xFFFF) as usize; - let output_geometry = self.ipu_register_u32(IPU_OUT_GS_OFFSET); - let output_height = (output_geometry & 0xFFFF) as usize; - let output_stride = self.ipu_register_u32(IPU_OUT_STRIDE_OFFSET) as usize; - let output_width = output_stride / 4; - - if input_width == 0 || input_height == 0 || output_width == 0 || output_height == 0 { - self.ipu_registers[IPU_STATUS_OFFSET] = IPU_STATUS_OUT_END; - return Ok(()); - } - - let y_addr = self.ipu_register_u32(IPU_Y_ADDR_OFFSET); - let u_addr = self.ipu_register_u32(IPU_U_ADDR_OFFSET); - let v_addr = self.ipu_register_u32(IPU_V_ADDR_OFFSET); - let output_addr = self.ipu_register_u32(IPU_OUT_ADDR_OFFSET); - let y_stride = self.ipu_register_u32(IPU_Y_STRIDE_OFFSET) as usize; - let uv_stride = self.ipu_register_u32(IPU_UV_STRIDE_OFFSET); - let u_stride = (uv_stride >> 16) as usize; - let v_stride = (uv_stride & 0xFFFF) as usize; - - for output_y in 0..output_height { - let input_y = output_y * input_height / output_height; - for output_x in 0..output_width { - let input_x = output_x * input_width / output_width; - let y = self.read_u8(y_addr.wrapping_add((input_y * y_stride + input_x) as u32))? - as i32; - let u = self - .read_u8(u_addr.wrapping_add((input_y * u_stride + input_x / 2) as u32))? - as i32 - - 128; - let v = self - .read_u8(v_addr.wrapping_add((input_y * v_stride + input_x / 2) as u32))? - as i32 - - 128; - let luma = 1192 * (y - 16).max(0); - let red = ((luma + 1634 * v) >> 10).clamp(0, 255) as u8; - let green = ((luma - 400 * u - 833 * v) >> 10).clamp(0, 255) as u8; - let blue = ((luma + 2066 * u) >> 10).clamp(0, 255) as u8; - let pixel_addr = - output_addr.wrapping_add((output_y * output_stride + output_x * 4) as u32); - self.write_u8(pixel_addr, blue)?; - self.write_u8(pixel_addr.wrapping_add(1), green)?; - self.write_u8(pixel_addr.wrapping_add(2), red)?; - self.write_u8(pixel_addr.wrapping_add(3), 0)?; - } - } - - self.ipu_registers[IPU_STATUS_OFFSET] = IPU_STATUS_OUT_END; - Ok(()) - } - - /// Translate MIPS virtual address to physical address - /// Handles KSEG0 (0x80000000-0x9FFFFFFF) and KSEG1 (0xA0000000-0xBFFFFFFF) - #[inline(always)] - fn translate_address(&self, addr: u32) -> u32 { - match addr { - // KSEG0: Cached, maps to 0x00000000-0x1FFFFFFF - 0x8000_0000..=0x9FFF_FFFF => addr & KSEG0_MASK, - // KSEG1: Uncached, maps to 0x00000000-0x1FFFFFFF - 0xA000_0000..=0xBFFF_FFFF => addr & KSEG0_MASK, - // KSEG2/KSEG3: Not commonly used, pass through - _ => addr, - } - } - - /// Fetch an instruction through the common RAM path. - #[inline(always)] - pub(crate) fn fetch_instruction(&self, addr: u32) -> Result { - let physical = self.translate_address(addr) as usize; - if physical <= self.ram.len() - std::mem::size_of::() { - let bytes: [u8; 4] = self.ram[physical..physical + 4] - .try_into() - .expect("instruction slice has a fixed length"); - return Ok(u32::from_le_bytes(bytes)); - } - self.read_u32(addr) - } - - /// Read a byte from memory - pub fn read_u8(&self, addr: u32) -> Result { - let phys_addr = self.translate_address(addr); - if phys_addr < RAM_SIZE { - return Ok(self.ram[phys_addr as usize]); - } - if let Some(offset) = self.framebuffer_offset(addr) { - return Ok(self.framebuffer[offset]); - } - if let Some(offset) = self.ipu_register_offset(addr) { - return Ok(self.ipu_registers[offset]); - } - if let Some(value) = self.stubbed_peripheral_read(addr, 1) { - return Ok(value as u8); - } - - Err(SimulatorError::MemoryError { - addr, - message: "out of bounds".to_string(), - }) - } - - /// Read a 16-bit value from memory (little-endian) - pub fn read_u16(&self, addr: u32) -> Result { - let phys_addr = self.translate_address(addr) as usize; - if let Some(bytes) = self.ram.get(phys_addr..).and_then(|bytes| bytes.get(..2)) { - return Ok(u16::from_le_bytes([bytes[0], bytes[1]])); - } - if let Some(offset) = self.framebuffer_offset(addr) { - if let Some(bytes) = self.framebuffer.get(offset..offset + 2) { - return Ok(u16::from_le_bytes([bytes[0], bytes[1]])); - } - } - if let Some(offset) = self.ipu_register_offset(addr) { - if let Some(bytes) = self.ipu_registers.get(offset..offset + 2) { - return Ok(u16::from_le_bytes([bytes[0], bytes[1]])); - } - } - if let Some(value) = self.stubbed_peripheral_read(addr, 2) { - return Ok(value as u16); - } - Err(SimulatorError::MemoryError { - addr, - message: "out of bounds".to_string(), - }) - } - - /// Read a 32-bit value from memory (little-endian) - pub fn read_u32(&self, addr: u32) -> Result { - let phys_addr = self.translate_address(addr) as usize; - if let Some(bytes) = self.ram.get(phys_addr..).and_then(|bytes| bytes.get(..4)) { - return Ok(u32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]])); - } - if let Some(offset) = self.framebuffer_offset(addr) { - if let Some(bytes) = self.framebuffer.get(offset..offset + 4) { - return Ok(u32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]])); - } - } - if let Some(offset) = self.ipu_register_offset(addr) { - if let Some(bytes) = self.ipu_registers.get(offset..offset + 4) { - return Ok(u32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]])); - } - } - if let Some(value) = self.stubbed_peripheral_read(addr, 4) { - return Ok(value); - } - Err(SimulatorError::MemoryError { - addr, - message: "out of bounds".to_string(), - }) - } - - /// Write a byte to memory - pub fn write_u8(&mut self, addr: u32, value: u8) -> Result<()> { - let phys_addr = self.translate_address(addr); - if phys_addr < RAM_SIZE { - self.ram[phys_addr as usize] = value; - self.track_writes(phys_addr, 1); - return Ok(()); - } - if let Some(offset) = self.framebuffer_offset(addr) { - self.framebuffer[offset] = value; - self.track_writes(addr, 1); - return Ok(()); - } - if let Some(offset) = self.ipu_register_offset(addr) { - self.ipu_registers[offset] = value; - return Ok(()); - } - if self.is_stubbed_peripheral_write(addr, 1) { - return Ok(()); - } - - Err(SimulatorError::MemoryError { - addr, - message: "out of bounds".to_string(), - }) - } - - /// Write a 16-bit value to memory (little-endian) - pub fn write_u16(&mut self, addr: u32, value: u16) -> Result<()> { - let bytes = value.to_le_bytes(); - let phys_addr = self.translate_address(addr) as usize; - if let Some(destination) = self - .ram - .get_mut(phys_addr..) - .and_then(|destination| destination.get_mut(..2)) - { - destination.copy_from_slice(&bytes); - self.track_writes(phys_addr as u32, bytes.len()); - return Ok(()); - } - if let Some(offset) = self.framebuffer_offset(addr) { - if let Some(destination) = self.framebuffer.get_mut(offset..offset + bytes.len()) { - destination.copy_from_slice(&bytes); - self.track_writes(addr, bytes.len()); - return Ok(()); - } - } - if let Some(offset) = self.ipu_register_offset(addr) { - if let Some(destination) = self.ipu_registers.get_mut(offset..offset + bytes.len()) { - destination.copy_from_slice(&bytes); - return Ok(()); - } - } - if self.is_stubbed_peripheral_write(addr, 2) { - return Ok(()); - } - Err(SimulatorError::MemoryError { - addr, - message: "out of bounds".to_string(), - }) - } - - /// Write a 32-bit value to memory (little-endian) - pub fn write_u32(&mut self, addr: u32, value: u32) -> Result<()> { - let bytes = value.to_le_bytes(); - let phys_addr = self.translate_address(addr) as usize; - if let Some(destination) = self - .ram - .get_mut(phys_addr..) - .and_then(|destination| destination.get_mut(..4)) - { - destination.copy_from_slice(&bytes); - self.track_writes(phys_addr as u32, bytes.len()); - return Ok(()); - } - if let Some(offset) = self.framebuffer_offset(addr) { - if let Some(destination) = self.framebuffer.get_mut(offset..offset + bytes.len()) { - destination.copy_from_slice(&bytes); - self.track_writes(addr, bytes.len()); - return Ok(()); - } - } - if let Some(offset) = self.ipu_register_offset(addr) { - log::trace!("IPU write: {:#010x} = {:#010x}", addr, value); - if let Some(destination) = self.ipu_registers.get_mut(offset..offset + bytes.len()) { - destination.copy_from_slice(&bytes); - if offset == IPU_CTRL_OFFSET && value & IPU_CTRL_RUN != 0 { - self.run_ipu()?; - } - return Ok(()); - } - } - if self.is_stubbed_peripheral_write(addr, 4) { - return Ok(()); - } - Err(SimulatorError::MemoryError { - addr, - message: "out of bounds".to_string(), - }) - } - - /// Load data into memory at the specified address - pub fn load_data(&mut self, addr: u32, data: &[u8]) -> Result<()> { - for (i, &byte) in data.iter().enumerate() { - self.write_u8(addr.wrapping_add(i as u32), byte)?; - } - Ok(()) - } - - /// Get a slice of memory (for direct access) - pub fn as_slice(&self) -> &[u8] { - &self.ram - } - - /// Get a mutable slice of memory (for direct access) - pub fn as_mut_slice(&mut self) -> &mut [u8] { - &mut self.ram - } - - /// Get the complete frontend-visible system RAM region. - pub fn system_ram(&self) -> &[u8] { - &self.ram - } - - /// Get mutable frontend-visible system RAM. - pub fn system_ram_mut(&mut self) -> &mut [u8] { - &mut self.ram - } - - pub(crate) fn is_cheat_writable_range(&self, addr: u32, len: usize) -> bool { - let physical = self.translate_address(addr) as usize; - if physical - .checked_add(len) - .is_some_and(|end| end <= self.ram.len()) - { - return true; - } - self.framebuffer_offset(addr) - .and_then(|offset| offset.checked_add(len)) - .is_some_and(|end| end <= self.framebuffer.len()) - } - - /// Get the shared LCD framebuffer mapping. - pub fn framebuffer(&self) -> &[u8] { - &self.framebuffer - } - - /// Get the complete frontend-visible LCD framebuffer mapping. - pub fn framebuffer_mut(&mut self) -> &mut [u8] { - &mut self.framebuffer - } - - pub(crate) fn copy_state_from(&mut self, source: &Self) { - self.ram.copy_from_slice(&source.ram); - self.framebuffer.copy_from_slice(&source.framebuffer); - self.ipu_registers.copy_from_slice(&source.ipu_registers); - self.heap_ptr = source.heap_ptr; - self.allocations.clone_from(&source.allocations); - self.free_blocks.clone_from(&source.free_blocks); - self.write_log.clone_from(&source.write_log); - } - - pub(crate) fn snapshot_layout_is_valid(&self) -> bool { - self.ram.len() == RAM_SIZE as usize - && self.framebuffer.len() == FRAMEBUFFER_MAP_SIZE - && self.ipu_registers.len() == IPU_REGISTER_SIZE - } - - #[inline(always)] - fn track_writes(&mut self, addr: u32, count: usize) { - #[cfg(debug_assertions)] - { - let count = count.min(1000usize.saturating_sub(self.write_log.len())); - self.write_log - .extend((0..count).map(|offset| addr.wrapping_add(offset as u32))); - } - #[cfg(not(debug_assertions))] - let _ = (addr, count); - } - - /// Allocate memory from the heap - pub fn malloc(&mut self, size: u32) -> u32 { - if size == 0 { - return 0; - } - - let Some(aligned_size) = size.checked_add(3).map(|value| value & !3) else { - log::warn!("malloc failed: invalid allocation size"); - return 0; - }; - - if let Some(index) = self - .free_blocks - .iter() - .position(|&(_, block_size)| block_size >= aligned_size) - { - let (ptr, block_size) = self.free_blocks[index]; - if block_size == aligned_size { - self.free_blocks.remove(index); - } else { - self.free_blocks[index] = - (ptr.wrapping_add(aligned_size), block_size - aligned_size); - } - self.allocations.insert(ptr, aligned_size); - return ptr; - } - - let ptr = self.heap_ptr; - - let Some(end) = ptr.checked_add(aligned_size) else { - log::warn!("malloc failed: not enough memory"); - return 0; - }; - if end > RAM_BASE + RAM_SIZE { - log::warn!("malloc failed: not enough memory"); - return 0; - } - - self.heap_ptr = end; - self.allocations.insert(ptr, aligned_size); - ptr - } - - /// Free previously allocated memory - pub fn free(&mut self, ptr: u32) { - let Some(size) = (ptr != 0).then(|| self.allocations.remove(&ptr)).flatten() else { - return; - }; - - let insert_at = self - .free_blocks - .partition_point(|&(address, _)| address < ptr); - self.free_blocks.insert(insert_at, (ptr, size)); - - let mut index = insert_at.saturating_sub(1); - while index + 1 < self.free_blocks.len() { - let (address, block_size) = self.free_blocks[index]; - let (next_address, next_size) = self.free_blocks[index + 1]; - if address.checked_add(block_size) != Some(next_address) { - index += 1; - continue; - } - self.free_blocks[index].1 = block_size + next_size; - self.free_blocks.remove(index + 1); - } - - while let Some(&(address, block_size)) = self.free_blocks.last() { - if address.checked_add(block_size) != Some(self.heap_ptr) { - break; - } - self.heap_ptr = address; - self.free_blocks.pop(); - } - } - - /// Reallocate memory - pub fn realloc(&mut self, ptr: u32, new_size: u32) -> u32 { - if ptr == 0 { - return self.malloc(new_size); - } - - if new_size == 0 { - self.free(ptr); - return 0; - } - - let Some(&old_size) = self.allocations.get(&ptr) else { - return 0; - }; - let Some(aligned_size) = new_size.checked_add(3).map(|value| value & !3) else { - return 0; - }; - if aligned_size <= old_size { - self.allocations.insert(ptr, aligned_size); - if aligned_size < old_size { - let tail = ptr.wrapping_add(aligned_size); - self.allocations.insert(tail, old_size - aligned_size); - self.free(tail); - } - return ptr; - } - - let new_ptr = self.malloc(new_size); - if new_ptr == 0 { - return 0; - } - - let old_data: Vec = (0..old_size) - .filter_map(|i| self.read_u8(ptr.wrapping_add(i)).ok()) - .collect(); - for (i, &byte) in old_data.iter().enumerate() { - let _ = self.write_u8(new_ptr.wrapping_add(i as u32), byte); - } - self.free(ptr); - new_ptr - } - - /// Set memory to a value - pub fn memset(&mut self, ptr: u32, value: u8, size: u32) { - for i in 0..size { - let _ = self.write_u8(ptr.wrapping_add(i), value); - } - } - - /// Copy memory (handles overlapping regions) - pub fn memcpy(&mut self, dest: u32, src: u32, size: u32) -> Result<()> { - // Read source data first to handle overlapping regions - let data: Vec = (0..size) - .filter_map(|i| self.read_u8(src.wrapping_add(i)).ok()) - .collect(); - - for (i, &byte) in data.iter().enumerate() { - self.write_u8(dest.wrapping_add(i as u32), byte)?; - } - Ok(()) - } - - /// Read a null-terminated string length - pub fn read_string_len(&self, ptr: u32) -> u32 { - let mut len = 0; - while let Ok(b) = self.read_u8(ptr.wrapping_add(len)) { - if b == 0 { - break; - } - len += 1; - } - len - } - - /// Get current heap pointer - pub fn heap_ptr(&self) -> u32 { - self.heap_ptr - } -} - -impl Default for Memory { - fn default() -> Self { - Self::new() - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_memory_read_write() { - let mut mem = Memory::new(); - mem.write_u8(0x0000_0000, 0xAB).unwrap(); - assert_eq!(mem.read_u8(0x0000_0000).unwrap(), 0xAB); - } - - #[test] - fn test_memory_u16() { - let mut mem = Memory::new(); - mem.write_u16(0x0000_0000, 0x1234).unwrap(); - assert_eq!(mem.read_u16(0x0000_0000).unwrap(), 0x1234); - } - - #[test] - fn test_memory_u32() { - let mut mem = Memory::new(); - mem.write_u32(0x0000_0000, 0x1234_5678).unwrap(); - assert_eq!(mem.read_u32(0x0000_0000).unwrap(), 0x1234_5678); - } - - #[test] - fn test_instruction_fetch_uses_cached_ram_alias() { - let mut mem = Memory::new(); - mem.write_u32(0x1000, 0x1234_5678).unwrap(); - - assert_eq!(mem.fetch_instruction(0x8000_1000).unwrap(), 0x1234_5678); - } - - #[test] - fn test_write_tracking_is_limited_to_debug_builds() { - let mut mem = Memory::new(); - mem.write_u32(0x1000, 0x1234_5678).unwrap(); - let writes = mem.consume_write_log(); - - #[cfg(debug_assertions)] - assert_eq!(writes, [0x1000, 0x1001, 0x1002, 0x1003]); - #[cfg(not(debug_assertions))] - assert!(writes.is_empty()); - } - - #[test] - fn test_framebuffer_aliases_share_storage() { - let mut mem = Memory::new(); - - mem.write_u8(crate::video::VM_LCD_FB_ADDRESS, 0x12).unwrap(); - assert_eq!(mem.read_u8(0x1400_0000).unwrap(), 0x12); - - mem.write_u8(0x9000_0004, 0x34).unwrap(); - assert_eq!(mem.read_u8(0x1000_0004).unwrap(), 0x34); - - mem.write_u32(crate::video::VM_LCD_FB_ADDRESS + 8, 0x1234_5678) - .unwrap(); - assert_eq!(mem.read_u32(0x1400_0008).unwrap(), 0x1234_5678); - } - - #[test] - fn test_ipu_converts_planar_yuv422_to_rgb888() { - let mut mem = Memory::new(); - mem.load_data(0x100, &[235; 4]).unwrap(); - mem.load_data(0x110, &[128; 2]).unwrap(); - mem.load_data(0x120, &[128; 2]).unwrap(); - mem.write_u32(IPU_BASE + IPU_D_FMT_OFFSET as u32, 1) - .unwrap(); - mem.write_u32(IPU_BASE + IPU_Y_ADDR_OFFSET as u32, 0x100) - .unwrap(); - mem.write_u32(IPU_BASE + IPU_U_ADDR_OFFSET as u32, 0x110) - .unwrap(); - mem.write_u32(IPU_BASE + IPU_V_ADDR_OFFSET as u32, 0x120) - .unwrap(); - mem.write_u32(IPU_BASE + IPU_IN_GS_OFFSET as u32, 2 << 16 | 2) - .unwrap(); - mem.write_u32(IPU_BASE + IPU_Y_STRIDE_OFFSET as u32, 2) - .unwrap(); - mem.write_u32(IPU_BASE + IPU_UV_STRIDE_OFFSET as u32, 1 << 16 | 1) - .unwrap(); - mem.write_u32(IPU_BASE + IPU_OUT_ADDR_OFFSET as u32, 0x200) - .unwrap(); - mem.write_u32(IPU_BASE + IPU_OUT_GS_OFFSET as u32, 2 << 16 | 2) - .unwrap(); - mem.write_u32(IPU_BASE + IPU_OUT_STRIDE_OFFSET as u32, 8) - .unwrap(); - - mem.write_u32(IPU_BASE + IPU_CTRL_OFFSET as u32, IPU_CTRL_RUN) - .unwrap(); - - assert_eq!( - (0..4) - .map(|offset| mem.read_u8(0x200 + offset).unwrap()) - .collect::>(), - [254, 254, 254, 0] - ); - assert_eq!( - mem.read_u32(IPU_BASE + IPU_STATUS_OFFSET as u32).unwrap() - & u32::from(IPU_STATUS_OUT_END), - u32::from(IPU_STATUS_OUT_END) - ); - } - - #[test] - fn test_tcu_and_gpio_initialization_registers_are_available() { - let mut mem = Memory::new(); - - mem.write_u32(0xB000_203C, 4).unwrap(); - mem.write_u16(0xB000_2068, 0).unwrap(); - mem.write_u16(0xB000_206C, 4).unwrap(); - mem.write_u32(0xB001_0334, u32::MAX).unwrap(); - - assert_eq!(mem.read_u32(0xB000_2020).unwrap(), 4); - assert_eq!(mem.read_u16(0xB000_206C).unwrap(), 0); - assert_eq!(mem.read_u32(0xB001_0334).unwrap(), 0); - assert!(mem.read_u8(0xB000_20A0).is_err()); - assert!(mem.write_u32(0xB001_0400, 0).is_err()); - } - - #[test] - fn test_malloc_reuses_and_splits_freed_blocks() { - let mut mem = Memory::new(); - let first = mem.malloc(64); - let second = mem.malloc(32); - - mem.free(first); - - assert_eq!(mem.malloc(48), first); - assert_eq!(mem.malloc(16), first + 48); - assert_eq!(mem.malloc(4), second + 32); - } - - #[test] - fn test_free_coalesces_adjacent_blocks() { - let mut mem = Memory::new(); - let first = mem.malloc(32); - let second = mem.malloc(32); - let _guard = mem.malloc(4); - - mem.free(first); - mem.free(second); - - assert_eq!(mem.malloc(64), first); - } - - #[test] - fn test_realloc_failure_preserves_original_block() { - let mut mem = Memory::new(); - let ptr = mem.malloc(16); - mem.write_u32(ptr, 0x1234_5678).unwrap(); - - assert_eq!(mem.realloc(ptr, RAM_SIZE), 0); - assert_eq!(mem.read_u32(ptr).unwrap(), 0x1234_5678); - assert_eq!(mem.allocations.get(&ptr), Some(&16)); - } - - #[test] - fn test_memory_bounds_check() { - let mem = Memory::new(); - assert!(mem.read_u8(RAM_SIZE).is_err()); - } -} diff --git a/native/DingooEmu/crates/dingooemu-core/src/save_state.rs b/native/DingooEmu/crates/dingooemu-core/src/save_state.rs deleted file mode 100644 index 7e6425d..0000000 --- a/native/DingooEmu/crates/dingooemu-core/src/save_state.rs +++ /dev/null @@ -1,108 +0,0 @@ -use anyhow::{bail, Context, Result}; -use bincode::Options; -use serde::de::DeserializeOwned; -use serde::Serialize; - -const MAGIC: &[u8; 8] = b"DINGSTAT"; -const VERSION: u32 = 3; -const HEADER_SIZE: usize = 32; -// A Dingoo snapshot always contains the 32 MiB guest RAM and may also contain -// resources that the guest deliberately keeps open. Real applications can -// therefore exceed the original 64 MiB ceiling even though their LZ4 payload -// still fits in the fixed libretro serialization buffer. -const MAX_DECODED_SIZE: usize = 128 * 1024 * 1024; - -/// Fixed capacity required by the libretro serialization API. -pub const SERIALIZED_SIZE: usize = 48 * 1024 * 1024; - -pub fn encode(value: &T, content_crc32: u32, output: &mut [u8]) -> Result<()> { - if output.len() < SERIALIZED_SIZE { - bail!( - "save-state buffer is too small: got {}, need {}", - output.len(), - SERIALIZED_SIZE - ); - } - - let decoded = codec() - .serialize(value) - .context("failed to encode save-state payload")?; - if decoded.len() > MAX_DECODED_SIZE { - bail!( - "save-state decoded payload is {} bytes; limit is {} bytes", - decoded.len(), - MAX_DECODED_SIZE - ); - } - - let payload = lz4_flex::compress(&decoded); - if payload.len() > SERIALIZED_SIZE - HEADER_SIZE { - bail!( - "save-state compressed payload is {} bytes; fixed capacity is {} bytes", - payload.len(), - SERIALIZED_SIZE - HEADER_SIZE - ); - } - - output.fill(0); - output[..8].copy_from_slice(MAGIC); - output[8..12].copy_from_slice(&VERSION.to_le_bytes()); - output[12..16].copy_from_slice(&content_crc32.to_le_bytes()); - output[16..20].copy_from_slice(&(payload.len() as u32).to_le_bytes()); - output[20..24].copy_from_slice(&(decoded.len() as u32).to_le_bytes()); - output[24..28].copy_from_slice(&crc32fast::hash(&payload).to_le_bytes()); - output[HEADER_SIZE..HEADER_SIZE + payload.len()].copy_from_slice(&payload); - Ok(()) -} - -pub fn decode(input: &[u8], expected_content_crc32: u32) -> Result { - if input.len() < HEADER_SIZE { - bail!("save state is truncated"); - } - if &input[..8] != MAGIC { - bail!("invalid save-state signature"); - } - - let version = read_u32(input, 8); - if version != VERSION { - bail!("unsupported save-state version {version}"); - } - if read_u32(input, 12) != expected_content_crc32 { - bail!("save state belongs to different content data"); - } - - let payload_len = read_u32(input, 16) as usize; - let decoded_len = read_u32(input, 20) as usize; - if decoded_len > MAX_DECODED_SIZE { - bail!( - "save-state declares {} decoded bytes; limit is {} bytes", - decoded_len, - MAX_DECODED_SIZE - ); - } - let payload_end = HEADER_SIZE - .checked_add(payload_len) - .filter(|&end| end <= input.len() && end <= SERIALIZED_SIZE) - .context("invalid save-state payload length")?; - let payload = &input[HEADER_SIZE..payload_end]; - if crc32fast::hash(payload) != read_u32(input, 24) { - bail!("save-state checksum mismatch"); - } - - let decoded = - lz4_flex::decompress(payload, decoded_len).context("failed to decompress save state")?; - codec() - .with_limit(MAX_DECODED_SIZE as u64) - .deserialize(&decoded) - .context("failed to decode save state") -} - -fn codec() -> impl Options { - bincode::DefaultOptions::new() - .with_fixint_encoding() - .reject_trailing_bytes() -} - -fn read_u32(input: &[u8], offset: usize) -> u32 { - u32::from_le_bytes(input[offset..offset + 4].try_into().unwrap()) -} diff --git a/native/DingooEmu/crates/dingooemu-core/src/video.rs b/native/DingooEmu/crates/dingooemu-core/src/video.rs deleted file mode 100644 index a19612b..0000000 --- a/native/DingooEmu/crates/dingooemu-core/src/video.rs +++ /dev/null @@ -1,172 +0,0 @@ -/// Video constants for Dingoo A320 -pub const SCREEN_WIDTH: u32 = 320; -pub const SCREEN_HEIGHT: u32 = 240; -pub const FRAMEBUFFER_SIZE: usize = (SCREEN_WIDTH * SCREEN_HEIGHT * 2) as usize; // RGB565 - -/// Rounded LCD framebuffer mapping size. -pub const FRAMEBUFFER_MAP_SIZE: usize = 0x0002_6000; - -/// Fixed framebuffer address returned by the SDK. -pub const VM_LCD_FB_ADDRESS: u32 = 0x9400_0000; - -/// Video subsystem -#[derive(serde::Serialize, serde::Deserialize)] -pub struct Video { - /// Framebuffer in RGB565 format (host-side copy) - framebuffer: Box<[u8]>, - /// Whether the framebuffer has been updated - fb_dirty: bool, - /// Frame count for FPS tracking - frame_count: u64, -} - -impl Video { - /// Create a new video subsystem - pub fn new() -> Self { - Self { - framebuffer: vec![0u8; FRAMEBUFFER_SIZE].into_boxed_slice(), - fb_dirty: false, - frame_count: 0, - } - } - - /// Get the fixed framebuffer address - pub fn framebuffer_addr(&self) -> u32 { - VM_LCD_FB_ADDRESS - } - - /// Get a reference to the framebuffer - pub fn framebuffer(&self) -> &[u8] { - &self.framebuffer - } - - /// Get a mutable reference to the framebuffer - pub fn framebuffer_mut(&mut self) -> &mut [u8] { - &mut self.framebuffer - } - - pub(crate) fn snapshot_layout_is_valid(&self) -> bool { - self.framebuffer.len() == FRAMEBUFFER_SIZE - } - - /// Mark framebuffer as dirty (needs sync from guest memory) - pub fn mark_dirty(&mut self) { - self.fb_dirty = true; - } - - /// Check if framebuffer needs sync - pub fn is_dirty(&self) -> bool { - self.fb_dirty - } - - /// Clear dirty flag after sync - pub fn clear_dirty(&mut self) { - self.fb_dirty = false; - } - - /// Convert RGB565 framebuffer to XRGB8888 (for rendering) - pub fn to_xrgb8888(&self) -> Vec { - let mut pixels = Vec::with_capacity((SCREEN_WIDTH * SCREEN_HEIGHT) as usize); - - for y in 0..SCREEN_HEIGHT { - for x in 0..SCREEN_WIDTH { - let offset = ((y * SCREEN_WIDTH + x) * 2) as usize; - let rgb565 = - u16::from_le_bytes([self.framebuffer[offset], self.framebuffer[offset + 1]]); - - // RGB565 to XRGB8888 - let r = ((rgb565 >> 11) & 0x1F) as u32; - let g = ((rgb565 >> 5) & 0x3F) as u32; - let b = (rgb565 & 0x1F) as u32; - - // Expand to 8-bit - let r8 = (r << 3) | (r >> 2); - let g8 = (g << 2) | (g >> 4); - let b8 = (b << 3) | (b >> 2); - - pixels.push((0xFF << 24) | (r8 << 16) | (g8 << 8) | b8); - } - } - - pixels - } - - /// Calculate a deterministic CRC32 over the raw RGB565 framebuffer. - pub fn framebuffer_crc32(&self) -> u32 { - crc32fast::hash(&self.framebuffer) - } - - /// Save the current framebuffer as a PNG screenshot. - pub fn save_screenshot(&self, path: &std::path::Path) -> anyhow::Result<()> { - use image::RgbaImage; - let mut img = RgbaImage::new(SCREEN_WIDTH, SCREEN_HEIGHT); - for y in 0..SCREEN_HEIGHT { - for x in 0..SCREEN_WIDTH { - let offset = ((y * SCREEN_WIDTH + x) * 2) as usize; - let rgb565 = - u16::from_le_bytes([self.framebuffer[offset], self.framebuffer[offset + 1]]); - let r5 = (rgb565 >> 11) & 0x1F; - let g6 = (rgb565 >> 5) & 0x3F; - let b5 = rgb565 & 0x1F; - let r = ((r5 << 3) | (r5 >> 2)) as u8; - let g = ((g6 << 2) | (g6 >> 4)) as u8; - let b = ((b5 << 3) | (b5 >> 2)) as u8; - img.put_pixel(x, y, image::Rgba([r, g, b, 0xFF])); - } - } - img.save(path)?; - Ok(()) - } - - /// Increment frame counter - pub fn advance_frame(&mut self) { - self.frame_count += 1; - } - - /// Get current frame count - pub fn frame_count(&self) -> u64 { - self.frame_count - } -} - -impl Default for Video { - fn default() -> Self { - Self::new() - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_video_creation() { - let video = Video::new(); - assert_eq!(video.framebuffer_addr(), VM_LCD_FB_ADDRESS); - assert_eq!(video.framebuffer().len(), FRAMEBUFFER_SIZE); - } - - #[test] - fn test_rgb565_conversion() { - let mut video = Video::new(); - // Set a white pixel (0xFFFF in RGB565) - video.framebuffer_mut()[0] = 0xFF; - video.framebuffer_mut()[1] = 0xFF; - - let xrgb = video.to_xrgb8888(); - assert_eq!(xrgb[0], 0xFFFF_FFFF); // White - } - - #[test] - fn framebuffer_crc32_tracks_raw_rgb565_pixels() { - let mut video = Video::new(); - let initial = video.framebuffer_crc32(); - video.framebuffer_mut()[0..2].copy_from_slice(&0xffff_u16.to_le_bytes()); - - assert_ne!(video.framebuffer_crc32(), initial); - assert_eq!( - video.framebuffer_crc32(), - crc32fast::hash(video.framebuffer()) - ); - } -} diff --git a/native/DingooEmu/crates/dingooemu-core/tests/gooplayer_test.rs b/native/DingooEmu/crates/dingooemu-core/tests/gooplayer_test.rs deleted file mode 100644 index 177c3b7..0000000 --- a/native/DingooEmu/crates/dingooemu-core/tests/gooplayer_test.rs +++ /dev/null @@ -1,47 +0,0 @@ -use dingooemu_core::Emulator; -use std::path::PathBuf; - -fn gooplayer_app_path() -> Option { - if let Some(path) = std::env::var_os("DINGOOEMU_GOOPLAYER_APP").map(PathBuf::from) { - return path.exists().then_some(path); - } - - let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR")) - .join("../..") - .join("tmp/dingoo_game/GooPlayer/GooPlayer.app"); - path.exists().then_some(path) -} - -#[test] -fn test_gooplayer_discovers_tracks_and_opens_playlist() { - let Some(app_path) = gooplayer_app_path() else { - eprintln!("Skipping: set DINGOOEMU_GOOPLAYER_APP to test GooPlayer.app"); - return; - }; - - let mut emu = Emulator::from_path(&app_path).expect("Failed to load GooPlayer.app"); - emu.start(); - for _ in 0..300 { - emu.tick().expect("GooPlayer.app tick failed"); - } - - let highlighted_pixels = emu - .video - .framebuffer() - .as_chunks::<2>() - .0 - .iter() - .map(|pixel| u16::from_le_bytes(*pixel)) - .filter(|pixel| { - let red = (pixel >> 11) & 0x1f; - let green = (pixel >> 5) & 0x3f; - let blue = pixel & 0x1f; - red >= 24 && green >= 48 && blue <= 12 - }) - .count(); - - assert!( - highlighted_pixels > 10, - "GooPlayer.app did not leave its title screen and render a selected track" - ); -} diff --git a/native/DingooEmu/crates/dingooemu-core/tests/integration.rs b/native/DingooEmu/crates/dingooemu-core/tests/integration.rs deleted file mode 100644 index 0ef5db3..0000000 --- a/native/DingooEmu/crates/dingooemu-core/tests/integration.rs +++ /dev/null @@ -1,145 +0,0 @@ -use dingooemu_core::Emulator; - -/// Test loading a simple .app file -#[test] -#[ignore] // Requires actual .app files -fn test_load_simple_game() { - let app_path = "test_games/simple.app"; - if !std::path::Path::new(app_path).exists() { - eprintln!("Skipping: test game not found at {}", app_path); - return; - } - - let mut emu = Emulator::from_path(app_path).unwrap(); - emu.start(); - - // Run 100 frames - for _frame in 0..100 { - emu.tick().unwrap(); - } - - // Verify CPU is still running or has executed instructions - assert!( - emu.cpu.is_running() || emu.cpu.instruction_count > 0, - "CPU should have executed some instructions" - ); -} - -/// Test .app parsing -#[test] -fn test_app_parsing() { - use dingooemu_core::app_loader::AppImage; - - // Create a minimal .app file for testing - let mut data = vec![0u8; 256]; - - // CCDL magic at offset 0 - data[0..4].copy_from_slice(b"CCDL"); - - // IMPT descriptor at offset 0x20 - data[0x20..0x24].copy_from_slice(b"IMPT"); - - // EXPT descriptor at offset 0x40 - data[0x40..0x44].copy_from_slice(b"EXPT"); - - // RAWD descriptor at offset 0x60 - data[0x60..0x64].copy_from_slice(b"RAWD"); - - // RAWD header fields - data[0x74..0x78].copy_from_slice(&0x8000_0000u32.to_le_bytes()); // entry point - data[0x78..0x7C].copy_from_slice(&0x8000_0000u32.to_le_bytes()); // origin - data[0x7C..0x80].copy_from_slice(&0x80u32.to_le_bytes()); // program_size - - // Parse the .app file - let app = AppImage::parse(&data).unwrap(); - - assert_eq!(app.entry_point(), 0x8000_0000); - assert_eq!(app.load_base(), 0x8000_0000); - assert_eq!(app.program_size(), 0x80); -} - -/// Test CPU instruction execution -#[test] -fn test_cpu_instructions() { - use dingooemu_core::cpu::Cpu; - use dingooemu_core::memory::Memory; - - let mut cpu = Cpu::new(0); - let mut mem = Memory::new(); - - // ADDIU $t0, $zero, 0x1234 - let instr = (0x09 << 26) | (8 << 16) | 0x1234; - mem.write_u32(0, instr).unwrap(); - - cpu.start(); - cpu.step(&mut mem).unwrap(); - - assert_eq!(cpu.regs.read(8), 0x1234); -} - -/// Test branch instructions -#[test] -fn test_branch_instructions() { - use dingooemu_core::cpu::Cpu; - use dingooemu_core::memory::Memory; - - let mut cpu = Cpu::new(0); - let mut mem = Memory::new(); - - // BEQ $zero, $zero, offset=1 (branch to PC+4+1*4 = 8) - // opcode=0x04, rs=0, rt=0, offset=1 - let beq = (0x04 << 26) | 1; - mem.write_u32(0, beq).unwrap(); - - // NOP (delay slot) - mem.write_u32(4, 0).unwrap(); - - // ADDIU $t0, $zero, 0x1234 (target at address 8) - let addiu = (0x09 << 26) | (8 << 16) | 0x1234; - mem.write_u32(8, addiu).unwrap(); - - cpu.start(); - - // Execute BEQ (sets delay slot) - cpu.step(&mut mem).unwrap(); - eprintln!( - "Step 1 - After BEQ: PC={:#010x}, branch_delay={}, branch_target={:#010x}", - cpu.regs.pc, cpu.branch_delay, cpu.branch_target - ); - - // Execute delay slot (NOP) - cpu.step(&mut mem).unwrap(); - eprintln!( - "Step 2 - After NOP: PC={:#010x}, branch_delay={}", - cpu.regs.pc, cpu.branch_delay - ); - - // Execute target instruction - cpu.step(&mut mem).unwrap(); - eprintln!( - "Step 3 - After ADDIU: PC={:#010x}, $t0={:#010x}", - cpu.regs.pc, - cpu.regs.read(8) - ); - - assert_eq!(cpu.regs.read(8), 0x1234); -} - -/// Test memory operations -#[test] -fn test_memory_operations() { - use dingooemu_core::memory::Memory; - - let mut mem = Memory::new(); - - // Test malloc - let ptr = mem.malloc(100); - assert!(ptr != 0); - - // Test write and read - mem.write_u32(ptr, 0x1234_5678).unwrap(); - assert_eq!(mem.read_u32(ptr).unwrap(), 0x1234_5678); - - // Test free - mem.free(ptr); -} diff --git a/native/DingooEmu/crates/dingooemu-core/tests/overlord_fighter_test.rs b/native/DingooEmu/crates/dingooemu-core/tests/overlord_fighter_test.rs deleted file mode 100644 index add187f..0000000 --- a/native/DingooEmu/crates/dingooemu-core/tests/overlord_fighter_test.rs +++ /dev/null @@ -1,56 +0,0 @@ -use dingooemu_core::input::BUTTON_A; -use dingooemu_core::Emulator; -use std::path::PathBuf; - -fn overlord_fighter_app_path() -> Option { - if let Some(path) = std::env::var_os("DINGOOEMU_OVERLORD_FIGHTER_APP").map(PathBuf::from) { - return path.exists().then_some(path); - } - - let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR")) - .join("../..") - .join("tmp/dingoo_game/Overlord-Fighter.app"); - path.exists().then_some(path) -} - -fn run_menu(app_path: &PathBuf, with_input: bool) -> Emulator { - let mut emu = Emulator::from_path(app_path).expect("Failed to load Overlord-Fighter.app"); - emu.start(); - for frame in 0..120 { - let pressed = with_input && (80..85).contains(&frame); - emu.set_buttons(if pressed { BUTTON_A } else { 0 }); - emu.tick().expect("Overlord-Fighter.app tick failed"); - } - emu -} - -#[test] -fn test_overlord_fighter_accepts_title_screen_input() { - let Some(app_path) = overlord_fighter_app_path() else { - eprintln!("Skipping: set DINGOOEMU_OVERLORD_FIGHTER_APP to test Overlord-Fighter.app"); - return; - }; - - let control = run_menu(&app_path, false); - let input = run_menu(&app_path, true); - assert_eq!(control.video.framebuffer_crc32(), 0xd7d5_e307); - assert_eq!(input.video.framebuffer_crc32(), 0x7999_d125); - - let unknown: Vec<_> = input - .unknown_hle_calls() - .map(|call| call.name.as_str()) - .collect(); - for implemented in [ - "open_gui_key_msg", - "WM_CreateWindow", - "WM_SetFocus", - "GUI_Exec", - "U8TOX16", - "U8TOX32", - ] { - assert!( - !unknown.contains(&implemented), - "implemented GUI call remained unknown: {implemented}" - ); - } -} diff --git a/native/DingooEmu/crates/dingooemu-core/tests/snake_test.rs b/native/DingooEmu/crates/dingooemu-core/tests/snake_test.rs deleted file mode 100644 index b481db5..0000000 --- a/native/DingooEmu/crates/dingooemu-core/tests/snake_test.rs +++ /dev/null @@ -1,121 +0,0 @@ -#[cfg(not(feature = "standalone"))] -use dingooemu_core::audio::{AudioConfig, SampleFormat}; -#[cfg(not(feature = "standalone"))] -use dingooemu_core::input::{BUTTON_A, BUTTON_DOWN}; -use dingooemu_core::Emulator; -use std::path::PathBuf; - -fn snake_app_path() -> Option { - if let Some(path) = std::env::var_os("DINGOOEMU_SNAKE_APP").map(PathBuf::from) { - return path.exists().then_some(path); - } - - let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR")) - .join("../..") - .join("tmp/dingoo_game/Snake.app"); - path.exists().then_some(path) -} - -/// Test loading and rendering the bundled local Snake.app sample. -#[test] -fn test_snake_app_reaches_framebuffer() { - let Some(app_path) = snake_app_path() else { - eprintln!("Skipping: set DINGOOEMU_SNAKE_APP to test Snake.app"); - return; - }; - - let mut emu = Emulator::from_path(&app_path).expect("Failed to load Snake.app"); - emu.start(); - - for _ in 0..12 { - emu.tick().expect("Snake.app tick failed"); - } - - let framebuffer = emu.video.framebuffer(); - let non_zero = framebuffer.iter().filter(|&&b| b != 0).count(); - let unique_colors = framebuffer - .as_chunks::<2>() - .0 - .iter() - .map(|pixel| u16::from_le_bytes(*pixel)) - .collect::>() - .len(); - - assert!(emu.cpu.instruction_count > 0); - assert!( - non_zero > 0, - "Snake.app did not produce framebuffer pixels after {} instructions", - emu.cpu.instruction_count - ); - assert!( - unique_colors > 8, - "Snake.app framebuffer is still effectively solid: {unique_colors} colors" - ); -} - -#[test] -fn test_snake_app_does_not_force_startup_audio() { - let Some(app_path) = snake_app_path() else { - eprintln!("Skipping: set DINGOOEMU_SNAKE_APP to test Snake.app"); - return; - }; - - let mut emu = Emulator::from_path(&app_path).expect("Failed to load Snake.app"); - emu.start(); - - for _ in 0..20 { - emu.tick().expect("Snake.app tick failed"); - } - - assert!( - emu.audio.config().is_none(), - "Snake.app audio was started without a guest request" - ); -} - -#[test] -#[cfg(not(feature = "standalone"))] -fn test_snake_app_enables_audio_from_menu() { - let Some(app_path) = snake_app_path() else { - eprintln!("Skipping: set DINGOOEMU_SNAKE_APP to test Snake.app"); - return; - }; - - let mut emu = Emulator::from_path(&app_path).expect("Failed to load Snake.app"); - emu.start(); - - let inputs = [ - (BUTTON_DOWN, 30..36), - (BUTTON_DOWN, 90..96), - (BUTTON_DOWN, 150..156), - (BUTTON_A, 210..216), - ]; - let mut non_zero_samples = 0usize; - for frame in 0..300 { - let buttons = inputs - .iter() - .find_map(|(button, frames)| frames.contains(&frame).then_some(*button)) - .unwrap_or(0); - emu.set_buttons(buttons); - emu.tick().expect("Snake.app tick failed"); - non_zero_samples += emu - .take_audio_samples() - .into_iter() - .filter(|sample| *sample != 0) - .count(); - } - - assert_eq!( - emu.audio.config(), - Some(AudioConfig { - sample_rate: 8_000, - format: SampleFormat::S16Le, - channels: 1, - volume: 50, - }) - ); - assert!( - non_zero_samples > 0, - "Snake.app produced only silent audio after enabling sound" - ); -} diff --git a/native/DingooEmu/crates/dingooemu-core/tests/sword_and_fairy_test.rs b/native/DingooEmu/crates/dingooemu-core/tests/sword_and_fairy_test.rs deleted file mode 100644 index adc5a8f..0000000 --- a/native/DingooEmu/crates/dingooemu-core/tests/sword_and_fairy_test.rs +++ /dev/null @@ -1,50 +0,0 @@ -use dingooemu_core::Emulator; -use std::collections::BTreeSet; -use std::path::PathBuf; - -fn sword_and_fairy_app_path() -> Option { - if let Some(path) = std::env::var_os("DINGOOEMU_SWORD_AND_FAIRY_APP").map(PathBuf::from) { - return path.exists().then_some(path); - } - - let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR")) - .join("../..") - .join("tmp/dingoo_game/仙剑奇侠传/仙剑奇侠传.APP"); - path.exists().then_some(path) -} - -#[test] -fn test_sword_and_fairy_reaches_framebuffer() { - let Some(app_path) = sword_and_fairy_app_path() else { - eprintln!("Skipping: set DINGOOEMU_SWORD_AND_FAIRY_APP to test the game"); - return; - }; - - let mut emu = Emulator::from_path(&app_path).expect("Failed to load game"); - emu.start(); - - for _ in 0..60 { - emu.tick().expect("Game tick failed"); - } - - let framebuffer = emu.video.framebuffer(); - let non_zero = framebuffer.iter().filter(|&&byte| byte != 0).count(); - let unique_colors = framebuffer - .as_chunks::<2>() - .0 - .iter() - .map(|pixel| u16::from_le_bytes(*pixel)) - .collect::>() - .len(); - - assert!(emu.cpu.instruction_count > 0); - assert!( - non_zero > 0, - "Game did not produce framebuffer pixels after {} instructions", - emu.cpu.instruction_count - ); - assert!( - unique_colors > 1, - "Game framebuffer is still solid after startup: {unique_colors} color" - ); -} diff --git a/native/DingooEmu/crates/dingooemu-libretro/Cargo.toml b/native/DingooEmu/crates/dingooemu-libretro/Cargo.toml deleted file mode 100644 index bc54ac7..0000000 --- a/native/DingooEmu/crates/dingooemu-libretro/Cargo.toml +++ /dev/null @@ -1,24 +0,0 @@ -[package] -name = "dingooemu-libretro" -description.workspace = true -version.workspace = true -edition.workspace = true -license.workspace = true - -[lib] -# The libretro buildbot's official Rust CI templates build with -# `cargo build --release` and then rename the cdylib by appending `_libretro` -# (e.g. libdingooemu.so -> dingooemu_libretro.so), so the lib target is -# named `dingooemu` (matching CORENAME in .gitlab-ci.yml). For local RetroArch -# installs, rename the produced cdylib to dingooemu_libretro. manually. -name = "dingooemu" -crate-type = ["cdylib"] - -[dependencies] -log.workspace = true - -[target.'cfg(all(target_os = "android", target_pointer_width = "64", any(target_arch = "aarch64", target_arch = "x86_64")))'.dependencies] -dingooemu-core = { workspace = true, features = ["jit"] } - -[target.'cfg(not(all(target_os = "android", target_pointer_width = "64", any(target_arch = "aarch64", target_arch = "x86_64"))))'.dependencies] -dingooemu-core.workspace = true diff --git a/native/DingooEmu/crates/dingooemu-libretro/dingooemu_libretro.info b/native/DingooEmu/crates/dingooemu-libretro/dingooemu_libretro.info deleted file mode 100644 index 7ec8ec2..0000000 --- a/native/DingooEmu/crates/dingooemu-libretro/dingooemu_libretro.info +++ /dev/null @@ -1,32 +0,0 @@ -# Software Information -display_name = "Dingoo A320 (DingooEmu)" -authors = "Aloys" -supported_extensions = "app" -corename = "dingooemu" -categories = "Emulator" -license = "BSD-3-Clause" -permissions = "" -display_version = "0.2.0" - -# Hardware Information -manufacturer = "Ingenic" -systemname = "Dingoo A320" -systemid = "dingoo-a320" - -# Libretro Features -database = "Dingoo A320" -supports_no_game = "false" -firmware_count = 0 -savestate = "true" -cheats = "true" -input_descriptors = "true" -memory_descriptors = "true" -libretro_saves = "false" -core_options = "true" -load_subsystem = "false" -hw_render = "false" -needs_fullpath = "true" -disk_control = "false" -is_experimental = "false" - -description = "DingooEmu is an emulator for the Dingoo A320 handheld game console powered by the Ingenic JZ4740 MIPS SoC. This libretro core supports .app game files from the Dingoo ecosystem." diff --git a/native/DingooEmu/crates/dingooemu-libretro/src/api.rs b/native/DingooEmu/crates/dingooemu-libretro/src/api.rs deleted file mode 100644 index f6fd3a6..0000000 --- a/native/DingooEmu/crates/dingooemu-libretro/src/api.rs +++ /dev/null @@ -1,976 +0,0 @@ -use std::ffi::{c_void, CStr}; -use std::os::raw::{c_char, c_uint}; -use std::ptr; - -use dingooemu_core::cpu::UnknownInstructionPolicy; -use dingooemu_core::input::{ - BUTTON_A, BUTTON_B, BUTTON_DOWN, BUTTON_L, BUTTON_LEFT, BUTTON_R, BUTTON_RIGHT, BUTTON_SELECT, - BUTTON_START, BUTTON_UP, BUTTON_X, BUTTON_Y, -}; -use dingooemu_core::video::{SCREEN_HEIGHT, SCREEN_WIDTH}; -use dingooemu_core::Emulator; - -use crate::callbacks; -use crate::constants::*; -use crate::types::*; -use crate::EMULATOR; - -const PERFORMANCE_LEVEL: u32 = 4; -const AUDIO_SAMPLE_RATE: f64 = 22_050.0; -const FRAMES_PER_SECOND: f64 = 60.0; - -#[no_mangle] -pub extern "C" fn retro_set_environment(callback: RetroEnvironmentCallback) { - callbacks::set_environment(callback); - set_core_options(); -} - -#[no_mangle] -pub extern "C" fn retro_set_video_refresh(callback: RetroVideoRefreshCallback) { - callbacks::set_video_refresh(callback); -} - -#[no_mangle] -pub extern "C" fn retro_set_audio_sample(callback: RetroAudioSampleCallback) { - callbacks::set_audio_sample(callback); -} - -#[no_mangle] -pub extern "C" fn retro_set_audio_sample_batch(callback: RetroAudioSampleBatchCallback) { - callbacks::set_audio_sample_batch(callback); -} - -#[no_mangle] -pub extern "C" fn retro_set_input_poll(callback: RetroInputPollCallback) { - callbacks::set_input_poll(callback); -} - -#[no_mangle] -pub extern "C" fn retro_set_input_state(callback: RetroInputStateCallback) { - callbacks::set_input_state(callback); -} - -#[no_mangle] -pub extern "C" fn retro_init() { - callbacks::initialize_log_interface(); - crate::logger::initialize(); - log::info!("Libretro core initialized"); -} - -#[no_mangle] -pub extern "C" fn retro_deinit() { - unsafe { EMULATOR = None }; - log::info!("Libretro core deinitialized"); -} - -#[no_mangle] -pub extern "C" fn retro_api_version() -> c_uint { - RETRO_API_VERSION -} - -#[no_mangle] -pub extern "C" fn retro_get_system_info(info: *mut RetroSystemInfo) { - let Some(info) = (unsafe { info.as_mut() }) else { - return; - }; - - info.library_name = c"DingooEmu".as_ptr(); - info.library_version = c"0.2.0".as_ptr(); - info.valid_extensions = c"app".as_ptr(); - info.need_fullpath = true; - info.block_extract = false; -} - -#[no_mangle] -pub extern "C" fn retro_get_system_av_info(info: *mut RetroSystemAvInfo) { - let Some(info) = (unsafe { info.as_mut() }) else { - return; - }; - - info.geometry = RetroGameGeometry { - base_width: SCREEN_WIDTH, - base_height: SCREEN_HEIGHT, - max_width: SCREEN_WIDTH, - max_height: SCREEN_HEIGHT, - aspect_ratio: SCREEN_WIDTH as f32 / SCREEN_HEIGHT as f32, - }; - info.timing = RetroSystemTiming { - fps: FRAMES_PER_SECOND, - sample_rate: AUDIO_SAMPLE_RATE, - }; -} - -#[no_mangle] -pub extern "C" fn retro_set_controller_port_device(port: c_uint, device: c_uint) { - if port == 0 && device != RETRO_DEVICE_NONE && device != RETRO_DEVICE_JOYPAD { - log::warn!("Unsupported controller device {device} on port {port}"); - } -} - -#[no_mangle] -pub extern "C" fn retro_get_region() -> c_uint { - RETRO_REGION_NTSC -} - -#[no_mangle] -pub extern "C" fn retro_load_game(info: *const RetroGameInfo) -> bool { - let Some(info) = (unsafe { info.as_ref() }) else { - return false; - }; - if info.path.is_null() { - return false; - } - - let path = match unsafe { CStr::from_ptr(info.path) }.to_str() { - Ok(path) => path, - Err(error) => { - log::error!("Content path is not valid UTF-8: {error}"); - return false; - } - }; - unsafe { EMULATOR = None }; - - if !set_pixel_format() { - log::error!("Frontend rejected the required RGB565 pixel format"); - return false; - } - register_input_descriptors(); - set_performance_level(); - - match Emulator::from_path(path) { - Ok(mut emulator) => { - if let Some(save_directory) = frontend_directory(RETRO_ENVIRONMENT_GET_SAVE_DIRECTORY) { - emulator.set_save_directory(save_directory); - } - apply_core_options(&mut emulator); - emulator.start(); - unsafe { EMULATOR = Some(emulator) }; - if let Some(emulator) = unsafe { EMULATOR.as_mut() } { - register_memory_maps(emulator); - } - log::info!("Loaded content: {path}"); - true - } - Err(error) => { - log::error!("Failed to load content: {error}"); - false - } - } -} - -#[no_mangle] -pub extern "C" fn retro_load_game_special( - _game_type: c_uint, - _info: *const RetroGameInfo, - _num_info: usize, -) -> bool { - false -} - -#[no_mangle] -pub extern "C" fn retro_unload_game() { - if let Some(emulator) = unsafe { EMULATOR.as_mut() } { - emulator.flush_save_files(); - } - unsafe { EMULATOR = None }; -} - -#[no_mangle] -pub extern "C" fn retro_run() { - if core_options_changed() { - if let Some(emulator) = unsafe { EMULATOR.as_mut() } { - apply_core_options(emulator); - } - } - let Some(emulator) = (unsafe { EMULATOR.as_mut() }) else { - return; - }; - - callbacks::input_poll(); - let buttons = - query_joypad_buttons(|id| callbacks::input_state(0, RETRO_DEVICE_JOYPAD, 0, id) != 0); - emulator.set_buttons(buttons); - - if let Err(error) = emulator.tick() { - // Execution errors leave the guest at the same invalid PC. Retrying - // every frame only repeats the error forever and presents a white - // screen. Treat the failed content as ended so the frontend can - // unload it, flush persistent saves, and return to its library. - log::error!("Frame execution failed; requesting frontend shutdown: {error}"); - callbacks::environment( - RETRO_ENVIRONMENT_SHUTDOWN, - ptr::null_mut(), - ); - return; - } - - // A Dingoo application returns from its main entry point when the player - // chooses its built-in Exit command. Tell the frontend that content ended - // instead of continuing to publish the core's empty fallback framebuffer. - if !emulator.is_running() { - log::info!("Content exited normally"); - callbacks::environment( - RETRO_ENVIRONMENT_SHUTDOWN, - ptr::null_mut(), - ); - return; - } - - callbacks::video_refresh( - emulator.video.framebuffer().as_ptr().cast(), - SCREEN_WIDTH, - SCREEN_HEIGHT, - SCREEN_WIDTH as usize * std::mem::size_of::(), - ); - - let samples = emulator.take_audio_samples(); - if callbacks::audio_sample_batch(samples.as_ptr(), samples.len() / 2).is_none() { - for sample in samples.as_chunks::<2>().0.iter() { - callbacks::audio_sample(sample[0], sample[1]); - } - } -} - -#[no_mangle] -pub extern "C" fn retro_reset() { - let Some(emulator) = (unsafe { EMULATOR.as_mut() }) else { - return; - }; - if let Err(error) = emulator.reset() { - log::error!("Reset failed: {error}"); - } else { - apply_core_options(emulator); - } -} - -#[no_mangle] -pub extern "C" fn retro_serialize_size() -> usize { - unsafe { EMULATOR.as_ref() }.map_or(0, Emulator::serialized_state_size) -} - -#[no_mangle] -pub extern "C" fn retro_serialize(data: *mut c_void, size: usize) -> bool { - let Some(emulator) = (unsafe { EMULATOR.as_ref() }) else { - return false; - }; - if data.is_null() || size < emulator.serialized_state_size() { - return false; - } - let output = unsafe { std::slice::from_raw_parts_mut(data.cast::(), size) }; - match emulator.serialize_state(output) { - Ok(()) => true, - Err(error) => { - log::error!("Failed to serialize state: {error}"); - false - } - } -} - -#[no_mangle] -pub extern "C" fn retro_unserialize(data: *const c_void, size: usize) -> bool { - let Some(emulator) = (unsafe { EMULATOR.as_mut() }) else { - return false; - }; - if data.is_null() || size < emulator.serialized_state_size() { - return false; - } - let input = unsafe { std::slice::from_raw_parts(data.cast::(), size) }; - match emulator.unserialize_state(input) { - Ok(()) => { - apply_core_options(emulator); - true - } - Err(error) => { - log::error!("Failed to unserialize state: {error}"); - false - } - } -} - -#[no_mangle] -pub extern "C" fn retro_cheat_reset() { - if let Some(emulator) = unsafe { EMULATOR.as_mut() } { - emulator.clear_cheats(); - } -} - -#[no_mangle] -pub extern "C" fn retro_cheat_set(index: c_uint, enabled: bool, code: *const c_char) { - let Some(emulator) = (unsafe { EMULATOR.as_mut() }) else { - return; - }; - if code.is_null() { - log::warn!("Ignored null cheat code for slot {index}"); - return; - } - let code = unsafe { CStr::from_ptr(code) }.to_string_lossy(); - if let Err(error) = emulator.set_cheat(index, enabled, &code) { - log::warn!("Rejected cheat slot {index}: {error}"); - } -} - -#[no_mangle] -pub extern "C" fn retro_get_memory_data(id: c_uint) -> *mut c_void { - let Some(emulator) = (unsafe { EMULATOR.as_mut() }) else { - return ptr::null_mut(); - }; - match id & RETRO_MEMORY_MASK { - RETRO_MEMORY_SYSTEM_RAM => emulator.memory.system_ram_mut().as_mut_ptr().cast(), - RETRO_MEMORY_VIDEO_RAM => emulator.memory.framebuffer_mut().as_mut_ptr().cast(), - _ => ptr::null_mut(), - } -} - -#[no_mangle] -pub extern "C" fn retro_get_memory_size(id: c_uint) -> usize { - let Some(emulator) = (unsafe { EMULATOR.as_ref() }) else { - return 0; - }; - match id & RETRO_MEMORY_MASK { - RETRO_MEMORY_SYSTEM_RAM => emulator.memory.system_ram().len(), - RETRO_MEMORY_VIDEO_RAM => emulator.memory.framebuffer().len(), - _ => 0, - } -} - -fn set_pixel_format() -> bool { - let mut format = RETRO_PIXEL_FORMAT_RGB565; - callbacks::environment( - RETRO_ENVIRONMENT_SET_PIXEL_FORMAT, - (&mut format as *mut u32).cast(), - ) -} - -fn frontend_directory(command: u32) -> Option { - let mut value: *const c_char = ptr::null(); - if !callbacks::environment(command, (&mut value as *mut *const c_char).cast()) - || value.is_null() - { - return None; - } - let path = unsafe { CStr::from_ptr(value) }.to_string_lossy(); - (!path.is_empty()).then(|| std::path::PathBuf::from(path.as_ref())) -} - -fn register_memory_maps(emulator: &mut Emulator) { - let descriptors = [ - RetroMemoryDescriptor { - flags: RETRO_MEMDESC_SYSTEM_RAM, - ptr: emulator.memory.system_ram_mut().as_mut_ptr().cast(), - offset: 0, - start: 0, - select: 0, - disconnect: 0, - len: emulator.memory.system_ram().len(), - addrspace: c"Dingoo".as_ptr(), - }, - RetroMemoryDescriptor { - flags: RETRO_MEMDESC_VIDEO_RAM, - ptr: emulator.memory.framebuffer_mut().as_mut_ptr().cast(), - offset: 0, - start: dingooemu_core::video::VM_LCD_FB_ADDRESS as usize, - select: 0, - disconnect: 0, - len: emulator.memory.framebuffer().len(), - addrspace: c"Dingoo".as_ptr(), - }, - ]; - let memory_map = RetroMemoryMap { - descriptors: descriptors.as_ptr(), - num_descriptors: descriptors.len() as u32, - }; - if !callbacks::environment( - RETRO_ENVIRONMENT_SET_MEMORY_MAPS, - (&memory_map as *const RetroMemoryMap).cast_mut().cast(), - ) { - log::warn!("Frontend did not accept memory descriptors"); - } -} - -fn set_performance_level() { - let mut level = PERFORMANCE_LEVEL; - callbacks::environment( - RETRO_ENVIRONMENT_SET_PERFORMANCE_LEVEL, - (&mut level as *mut u32).cast(), - ); -} - -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -struct CoreOptions { - volume: u8, - repeat_delay: u32, - repeat_period: u32, - swap_ab: bool, - debug_logging: bool, - unknown_instruction_policy: UnknownInstructionPolicy, - jit_enabled: bool, -} - -impl Default for CoreOptions { - fn default() -> Self { - Self { - volume: 100, - repeat_delay: 24, - repeat_period: 6, - swap_ab: false, - debug_logging: false, - unknown_instruction_policy: UnknownInstructionPolicy::Skip, - jit_enabled: true, - } - } -} - -fn core_option_variables() -> Vec { - let mut variables = vec![ - RetroVariable { - key: c"dingooemu_volume".as_ptr(), - value: c"Audio Volume (%); 100|90|80|70|60|50|40|30|20|10|0".as_ptr(), - }, - RetroVariable { - key: c"dingooemu_repeat_delay".as_ptr(), - value: c"Key Auto-Repeat Delay (frames); 24|0|2|4|6|8|10|12|16|20|30|45|60".as_ptr(), - }, - RetroVariable { - key: c"dingooemu_repeat_period".as_ptr(), - value: c"Key Auto-Repeat Period (frames); 6|1|2|3|4|5|8|10|12|15|20|30".as_ptr(), - }, - RetroVariable { - key: c"dingooemu_swap_ab".as_ptr(), - value: c"Swap A/B Buttons; disabled|enabled".as_ptr(), - }, - RetroVariable { - key: c"dingooemu_debug_logging".as_ptr(), - value: c"CPU/HLE Debug Logging; disabled|enabled".as_ptr(), - }, - RetroVariable { - key: c"dingooemu_unknown_instruction".as_ptr(), - value: c"Unknown MIPS Instruction Policy; skip|stop".as_ptr(), - }, - ]; - #[cfg(all( - target_os = "android", - target_pointer_width = "64", - any(target_arch = "aarch64", target_arch = "x86_64") - ))] - variables.push(RetroVariable { - key: c"dingooemu_cpu_engine".as_ptr(), - value: c"CPU Execution Engine; jit|interpreter".as_ptr(), - }); - variables.push(RetroVariable { - key: ptr::null(), - value: ptr::null(), - }); - variables -} - -fn set_core_options() { - let variables = core_option_variables(); - callbacks::environment( - RETRO_ENVIRONMENT_SET_VARIABLES, - variables.as_ptr().cast_mut().cast(), - ); -} - -fn get_core_option(key: &CStr) -> Option { - let mut variable = RetroVariable { - key: key.as_ptr(), - value: ptr::null(), - }; - let success = callbacks::environment( - RETRO_ENVIRONMENT_GET_VARIABLE, - (&mut variable as *mut RetroVariable).cast(), - ); - if success && !variable.value.is_null() { - unsafe { - CStr::from_ptr(variable.value) - .to_str() - .ok() - .map(str::to_owned) - } - } else { - None - } -} - -fn core_options_changed() -> bool { - let mut updated = false; - callbacks::environment( - RETRO_ENVIRONMENT_GET_VARIABLE_UPDATE, - (&mut updated as *mut bool).cast(), - ) && updated -} - -fn read_core_options(mut get: impl FnMut(&CStr) -> Option) -> CoreOptions { - let mut options = CoreOptions::default(); - if let Some(volume) = get(c"dingooemu_volume").and_then(|value| value.parse::().ok()) { - options.volume = volume.min(100); - } - if let Some(delay) = get(c"dingooemu_repeat_delay").and_then(|value| value.parse::().ok()) - { - options.repeat_delay = delay; - } - if let Some(period) = - get(c"dingooemu_repeat_period").and_then(|value| value.parse::().ok()) - { - options.repeat_period = period.max(1); - } - if let Some(swap) = get(c"dingooemu_swap_ab") { - options.swap_ab = swap == "enabled"; - } - if let Some(debug) = get(c"dingooemu_debug_logging") { - options.debug_logging = debug == "enabled"; - } - if let Some(policy) = get(c"dingooemu_unknown_instruction") { - options.unknown_instruction_policy = if policy == "stop" { - UnknownInstructionPolicy::Stop - } else { - UnknownInstructionPolicy::Skip - }; - } - if let Some(engine) = get(c"dingooemu_cpu_engine") { - options.jit_enabled = engine != "interpreter"; - } - options -} - -fn apply_core_options(emulator: &mut Emulator) { - let options = read_core_options(get_core_option); - emulator.audio.set_master_volume(options.volume); - emulator - .input - .set_repeat_timing(options.repeat_delay, options.repeat_period); - emulator.input.set_swap_ab(options.swap_ab); - crate::logger::set_debug_logging(options.debug_logging); - emulator - .cpu - .set_unknown_instruction_policy(options.unknown_instruction_policy); - emulator.set_jit_enabled(options.jit_enabled); - log::info!( - "Core options applied: volume={} repeat_delay={} repeat_period={} swap_ab={} debug_logging={} unknown_instruction={:?} cpu_engine={}", - options.volume, - options.repeat_delay, - options.repeat_period, - options.swap_ab, - options.debug_logging, - options.unknown_instruction_policy, - if options.jit_enabled { "jit" } else { "interpreter" } - ); -} - -fn input_descriptors() -> [RetroInputDescriptor; 13] { - let descriptor = |id, description: &'static CStr| RetroInputDescriptor { - port: 0, - device: RETRO_DEVICE_JOYPAD, - index: 0, - id, - description: description.as_ptr(), - }; - - [ - descriptor(RETRO_DEVICE_ID_JOYPAD_UP, c"D-Pad Up"), - descriptor(RETRO_DEVICE_ID_JOYPAD_DOWN, c"D-Pad Down"), - descriptor(RETRO_DEVICE_ID_JOYPAD_LEFT, c"D-Pad Left"), - descriptor(RETRO_DEVICE_ID_JOYPAD_RIGHT, c"D-Pad Right"), - descriptor(RETRO_DEVICE_ID_JOYPAD_A, c"A"), - descriptor(RETRO_DEVICE_ID_JOYPAD_B, c"B"), - descriptor(RETRO_DEVICE_ID_JOYPAD_X, c"X"), - descriptor(RETRO_DEVICE_ID_JOYPAD_Y, c"Y"), - descriptor(RETRO_DEVICE_ID_JOYPAD_START, c"Start"), - descriptor(RETRO_DEVICE_ID_JOYPAD_SELECT, c"Select"), - descriptor(RETRO_DEVICE_ID_JOYPAD_L, c"L"), - descriptor(RETRO_DEVICE_ID_JOYPAD_R, c"R"), - RetroInputDescriptor { - port: 0, - device: 0, - index: 0, - id: 0, - description: ptr::null(), - }, - ] -} - -fn register_input_descriptors() { - let descriptors = input_descriptors(); - callbacks::environment( - RETRO_ENVIRONMENT_SET_INPUT_DESCRIPTORS, - descriptors.as_ptr().cast_mut().cast(), - ); -} - -fn query_joypad_buttons(mut pressed: impl FnMut(u32) -> bool) -> u32 { - const BUTTON_MAP: [(u32, u32); 12] = [ - (RETRO_DEVICE_ID_JOYPAD_UP, BUTTON_UP), - (RETRO_DEVICE_ID_JOYPAD_DOWN, BUTTON_DOWN), - (RETRO_DEVICE_ID_JOYPAD_LEFT, BUTTON_LEFT), - (RETRO_DEVICE_ID_JOYPAD_RIGHT, BUTTON_RIGHT), - (RETRO_DEVICE_ID_JOYPAD_A, BUTTON_A), - (RETRO_DEVICE_ID_JOYPAD_B, BUTTON_B), - (RETRO_DEVICE_ID_JOYPAD_X, BUTTON_X), - (RETRO_DEVICE_ID_JOYPAD_Y, BUTTON_Y), - (RETRO_DEVICE_ID_JOYPAD_START, BUTTON_START), - (RETRO_DEVICE_ID_JOYPAD_SELECT, BUTTON_SELECT), - (RETRO_DEVICE_ID_JOYPAD_L, BUTTON_L), - (RETRO_DEVICE_ID_JOYPAD_R, BUTTON_R), - ]; - - BUTTON_MAP.iter().fold(0, |buttons, (id, mask)| { - if pressed(*id) { - buttons | mask - } else { - buttons - } - }) -} - -#[cfg(test)] -mod tests { - use std::ffi::CString; - use std::sync::atomic::{AtomicBool, AtomicU32, Ordering}; - use std::sync::Mutex; - - use super::*; - - static TEST_LOCK: Mutex<()> = Mutex::new(()); - static PIXEL_FORMAT: AtomicU32 = AtomicU32::new(u32::MAX); - static INPUT_DESCRIPTORS_SET: AtomicBool = AtomicBool::new(false); - static INPUT_POLLED: AtomicBool = AtomicBool::new(false); - static VIDEO_WIDTH: AtomicU32 = AtomicU32::new(0); - static AUDIO_BATCH_CALLED: AtomicBool = AtomicBool::new(false); - static MEMORY_MAPS_SET: AtomicBool = AtomicBool::new(false); - static SAVE_DIRECTORY: Mutex> = Mutex::new(None); - - unsafe extern "C" fn test_environment(command: u32, data: *mut c_void) -> bool { - match command { - RETRO_ENVIRONMENT_SET_PIXEL_FORMAT => { - PIXEL_FORMAT.store(*(data.cast::()), Ordering::SeqCst); - true - } - RETRO_ENVIRONMENT_SET_INPUT_DESCRIPTORS => { - let descriptors = data.cast::(); - INPUT_DESCRIPTORS_SET - .store(!(*descriptors).description.is_null(), Ordering::SeqCst); - true - } - RETRO_ENVIRONMENT_SET_PERFORMANCE_LEVEL => true, - RETRO_ENVIRONMENT_SET_MEMORY_MAPS => { - let memory_map = &*data.cast::(); - MEMORY_MAPS_SET.store( - memory_map.num_descriptors == 2 && !memory_map.descriptors.is_null(), - Ordering::SeqCst, - ); - true - } - RETRO_ENVIRONMENT_SET_VARIABLES => true, - RETRO_ENVIRONMENT_GET_VARIABLE | RETRO_ENVIRONMENT_GET_VARIABLE_UPDATE => false, - RETRO_ENVIRONMENT_GET_LOG_INTERFACE => false, - RETRO_ENVIRONMENT_GET_SAVE_DIRECTORY => { - let directory = SAVE_DIRECTORY.lock().unwrap(); - let Some(directory) = directory.as_ref() else { - return false; - }; - *data.cast::<*const c_char>() = directory.as_ptr(); - true - } - _ => false, - } - } - - unsafe extern "C" fn test_video_refresh( - _data: *const c_void, - width: u32, - height: u32, - pitch: usize, - ) { - assert_eq!(height, SCREEN_HEIGHT); - assert_eq!(pitch, SCREEN_WIDTH as usize * std::mem::size_of::()); - VIDEO_WIDTH.store(width, Ordering::SeqCst); - } - - unsafe extern "C" fn test_audio_batch(_data: *const i16, _frames: usize) -> usize { - AUDIO_BATCH_CALLED.store(true, Ordering::SeqCst); - 0 - } - - unsafe extern "C" fn test_input_poll() { - INPUT_POLLED.store(true, Ordering::SeqCst); - } - - unsafe extern "C" fn test_input_state(port: u32, device: u32, index: u32, id: u32) -> i16 { - assert_eq!((port, device, index), (0, RETRO_DEVICE_JOYPAD, 0)); - i16::from(matches!( - id, - RETRO_DEVICE_ID_JOYPAD_A | RETRO_DEVICE_ID_JOYPAD_START - )) - } - - fn minimal_app_bytes() -> Vec { - let mut data = vec![0u8; 132]; - data[0..4].copy_from_slice(b"CCDL"); - data[0x20..0x24].copy_from_slice(b"IMPT"); - data[0x40..0x44].copy_from_slice(b"EXPT"); - data[0x60..0x64].copy_from_slice(b"RAWD"); - data[0x68..0x6c].copy_from_slice(&128u32.to_le_bytes()); - data[0x6c..0x70].copy_from_slice(&4u32.to_le_bytes()); - data[0x74..0x78].copy_from_slice(&0x8000_0000u32.to_le_bytes()); - data[0x78..0x7c].copy_from_slice(&0x8000_0000u32.to_le_bytes()); - data[0x7c..0x80].copy_from_slice(&4u32.to_le_bytes()); - data - } - - #[test] - fn maps_retropad_buttons_to_dingoo_masks() { - let buttons = query_joypad_buttons(|id| { - matches!( - id, - RETRO_DEVICE_ID_JOYPAD_UP - | RETRO_DEVICE_ID_JOYPAD_A - | RETRO_DEVICE_ID_JOYPAD_START - | RETRO_DEVICE_ID_JOYPAD_R - ) - }); - - assert_eq!(buttons, BUTTON_UP | BUTTON_A | BUTTON_START | BUTTON_R); - } - - #[test] - fn terminates_input_descriptor_array() { - let descriptors = input_descriptors(); - assert_eq!(descriptors.len(), 13); - assert!(descriptors[..12] - .iter() - .all(|descriptor| !descriptor.description.is_null())); - assert!(descriptors[12].description.is_null()); - } - - #[test] - fn volume_core_option_has_stable_key_and_default() { - let variables = core_option_variables(); - assert_eq!( - unsafe { CStr::from_ptr(variables[0].key) }, - c"dingooemu_volume" - ); - assert!(unsafe { CStr::from_ptr(variables[0].value) } - .to_str() - .unwrap() - .ends_with("; 100|90|80|70|60|50|40|30|20|10|0")); - assert!(variables.last().unwrap().key.is_null()); - - let options = - read_core_options(|key| (key == c"dingooemu_volume").then(|| "30".to_string())); - assert_eq!(options.volume, 30); - } - - #[test] - fn repeat_delay_option_supports_live_typematic_control() { - let variables = core_option_variables(); - assert_eq!( - unsafe { CStr::from_ptr(variables[1].key) }, - c"dingooemu_repeat_delay" - ); - let options = - read_core_options(|key| (key == c"dingooemu_repeat_delay").then(|| "12".to_string())); - assert_eq!(options.repeat_delay, 12); - } - - #[test] - fn repeat_period_option_rejects_zero_semantically() { - let variables = core_option_variables(); - assert_eq!( - unsafe { CStr::from_ptr(variables[2].key) }, - c"dingooemu_repeat_period" - ); - let options = - read_core_options(|key| (key == c"dingooemu_repeat_period").then(|| "0".to_string())); - assert_eq!(options.repeat_period, 1); - } - - #[test] - fn swap_ab_option_defaults_to_disabled() { - let variables = core_option_variables(); - assert_eq!( - unsafe { CStr::from_ptr(variables[3].key) }, - c"dingooemu_swap_ab" - ); - assert!(!read_core_options(|_| None).swap_ab); - assert!( - read_core_options(|key| { - (key == c"dingooemu_swap_ab").then(|| "enabled".to_string()) - }) - .swap_ab - ); - } - - #[test] - fn debug_logging_option_defaults_to_disabled() { - let variables = core_option_variables(); - assert_eq!( - unsafe { CStr::from_ptr(variables[4].key) }, - c"dingooemu_debug_logging" - ); - assert!(!read_core_options(|_| None).debug_logging); - assert!( - read_core_options(|key| { - (key == c"dingooemu_debug_logging").then(|| "enabled".to_string()) - }) - .debug_logging - ); - } - - #[test] - fn unknown_instruction_option_preserves_skip_default() { - let variables = core_option_variables(); - assert_eq!( - unsafe { CStr::from_ptr(variables[5].key) }, - c"dingooemu_unknown_instruction" - ); - assert_eq!( - read_core_options(|_| None).unknown_instruction_policy, - UnknownInstructionPolicy::Skip - ); - assert_eq!( - read_core_options(|key| { - (key == c"dingooemu_unknown_instruction").then(|| "stop".to_string()) - }) - .unknown_instruction_policy, - UnknownInstructionPolicy::Stop - ); - } - - #[test] - #[cfg(all( - target_os = "android", - target_pointer_width = "64", - any(target_arch = "aarch64", target_arch = "x86_64") - ))] - fn cpu_engine_option_defaults_to_jit_and_allows_interpreter() { - let variables = core_option_variables(); - assert_eq!( - unsafe { CStr::from_ptr(variables[6].key) }, - c"dingooemu_cpu_engine" - ); - assert!(read_core_options(|_| None).jit_enabled); - assert!( - !read_core_options(|key| { - (key == c"dingooemu_cpu_engine").then(|| "interpreter".to_string()) - }) - .jit_enabled - ); - } - - #[test] - fn loads_starts_resets_and_unloads_content() { - let _guard = TEST_LOCK.lock().unwrap(); - PIXEL_FORMAT.store(u32::MAX, Ordering::SeqCst); - INPUT_DESCRIPTORS_SET.store(false, Ordering::SeqCst); - INPUT_POLLED.store(false, Ordering::SeqCst); - VIDEO_WIDTH.store(0, Ordering::SeqCst); - AUDIO_BATCH_CALLED.store(false, Ordering::SeqCst); - MEMORY_MAPS_SET.store(false, Ordering::SeqCst); - - let path = std::env::temp_dir().join(format!( - "dingooemu-libretro-test-{}.app", - std::process::id() - )); - std::fs::write(&path, minimal_app_bytes()).unwrap(); - let path_string = CString::new(path.to_string_lossy().as_bytes()).unwrap(); - *SAVE_DIRECTORY.lock().unwrap() = - Some(CString::new(path.parent().unwrap().to_string_lossy().as_bytes()).unwrap()); - let info = RetroGameInfo { - path: path_string.as_ptr(), - data: ptr::null(), - size: 0, - meta: ptr::null(), - }; - - retro_set_environment(Some(test_environment)); - retro_set_video_refresh(Some(test_video_refresh)); - retro_set_audio_sample_batch(Some(test_audio_batch)); - retro_set_input_poll(Some(test_input_poll)); - retro_set_input_state(Some(test_input_state)); - retro_init(); - assert!(retro_load_game(&info)); - assert_eq!( - PIXEL_FORMAT.load(Ordering::SeqCst), - RETRO_PIXEL_FORMAT_RGB565 - ); - assert!(INPUT_DESCRIPTORS_SET.load(Ordering::SeqCst)); - assert!(MEMORY_MAPS_SET.load(Ordering::SeqCst)); - assert_eq!( - retro_get_memory_size(RETRO_MEMORY_SYSTEM_RAM), - 32 * 1024 * 1024 - ); - assert!(retro_get_memory_size(RETRO_MEMORY_VIDEO_RAM) >= (320 * 240 * 2) as usize); - let system_ram = retro_get_memory_data(RETRO_MEMORY_SYSTEM_RAM); - let video_ram = retro_get_memory_data(RETRO_MEMORY_VIDEO_RAM); - assert!(!system_ram.is_null()); - assert!(!video_ram.is_null()); - - retro_run(); - assert!(INPUT_POLLED.load(Ordering::SeqCst)); - assert_eq!(VIDEO_WIDTH.load(Ordering::SeqCst), SCREEN_WIDTH); - assert!(AUDIO_BATCH_CALLED.load(Ordering::SeqCst)); - let cheat = CString::new("mem32:0x1000=0xfeedbeef").unwrap(); - retro_cheat_set(0, true, cheat.as_ptr()); - retro_run(); - unsafe { - let emulator = EMULATOR.as_mut().unwrap(); - assert!(emulator.is_running()); - assert_eq!(emulator.input.buttons(), BUTTON_A | BUTTON_START); - assert_eq!(emulator.memory.read_u32(0x1000).unwrap(), 0xfeed_beef); - emulator.memory.write_u32(0x1000, 0x1234_5678).unwrap(); - } - - let mut state = vec![0u8; retro_serialize_size()]; - assert!(retro_serialize(state.as_mut_ptr().cast(), state.len())); - unsafe { - EMULATOR - .as_mut() - .unwrap() - .memory - .write_u32(0x1000, 0) - .unwrap(); - } - assert!(retro_unserialize(state.as_ptr().cast(), state.len())); - assert_eq!(retro_get_memory_data(RETRO_MEMORY_SYSTEM_RAM), system_ram); - assert_eq!(retro_get_memory_data(RETRO_MEMORY_VIDEO_RAM), video_ram); - unsafe { - assert_eq!( - EMULATOR.as_ref().unwrap().memory.read_u32(0x1000).unwrap(), - 0x1234_5678 - ); - } - retro_cheat_reset(); - unsafe { - EMULATOR - .as_mut() - .unwrap() - .memory - .write_u32(0x1000, 0) - .unwrap(); - } - retro_run(); - unsafe { - assert_eq!( - EMULATOR.as_ref().unwrap().memory.read_u32(0x1000).unwrap(), - 0 - ); - } - - retro_reset(); - assert_eq!(retro_get_memory_data(RETRO_MEMORY_SYSTEM_RAM), system_ram); - assert_eq!(retro_get_memory_data(RETRO_MEMORY_VIDEO_RAM), video_ram); - unsafe { - let emulator = EMULATOR.as_ref().unwrap(); - assert!(emulator.is_running()); - assert_eq!(emulator.memory.read_u32(0x1000).unwrap(), 0); - } - - retro_unload_game(); - assert!(retro_get_memory_data(RETRO_MEMORY_SYSTEM_RAM).is_null()); - assert_eq!(retro_get_memory_size(RETRO_MEMORY_SYSTEM_RAM), 0); - unsafe { assert!(EMULATOR.is_none()) }; - retro_deinit(); - *SAVE_DIRECTORY.lock().unwrap() = None; - std::fs::remove_file(path).unwrap(); - } -} diff --git a/native/DingooEmu/crates/dingooemu-libretro/src/callbacks.rs b/native/DingooEmu/crates/dingooemu-libretro/src/callbacks.rs deleted file mode 100644 index 4901126..0000000 --- a/native/DingooEmu/crates/dingooemu-libretro/src/callbacks.rs +++ /dev/null @@ -1,121 +0,0 @@ -use std::ffi::c_void; - -use crate::constants::RETRO_ENVIRONMENT_GET_LOG_INTERFACE; -use crate::types::*; - -struct Callbacks { - environment: RetroEnvironmentCallback, - video_refresh: RetroVideoRefreshCallback, - audio_sample: RetroAudioSampleCallback, - audio_sample_batch: RetroAudioSampleBatchCallback, - input_poll: RetroInputPollCallback, - input_state: RetroInputStateCallback, - log: RetroLogPrintfCallback, -} - -impl Callbacks { - const fn new() -> Self { - Self { - environment: None, - video_refresh: None, - audio_sample: None, - audio_sample_batch: None, - input_poll: None, - input_state: None, - log: None, - } - } -} - -static mut CALLBACKS: Callbacks = Callbacks::new(); - -pub fn set_environment(callback: RetroEnvironmentCallback) { - unsafe { CALLBACKS.environment = callback }; -} - -pub fn set_video_refresh(callback: RetroVideoRefreshCallback) { - unsafe { CALLBACKS.video_refresh = callback }; -} - -pub fn set_audio_sample(callback: RetroAudioSampleCallback) { - unsafe { CALLBACKS.audio_sample = callback }; -} - -pub fn set_audio_sample_batch(callback: RetroAudioSampleBatchCallback) { - unsafe { CALLBACKS.audio_sample_batch = callback }; -} - -pub fn set_input_poll(callback: RetroInputPollCallback) { - unsafe { CALLBACKS.input_poll = callback }; -} - -pub fn set_input_state(callback: RetroInputStateCallback) { - unsafe { CALLBACKS.input_state = callback }; -} - -pub fn initialize_log_interface() { - unsafe { CALLBACKS.log = None }; - let mut interface = RetroLogCallback { log: None }; - if environment( - RETRO_ENVIRONMENT_GET_LOG_INTERFACE, - &mut interface as *mut RetroLogCallback as *mut c_void, - ) { - unsafe { CALLBACKS.log = interface.log }; - } -} - -pub fn environment(command: u32, data: *mut c_void) -> bool { - unsafe { - CALLBACKS - .environment - .is_some_and(|callback| callback(command, data)) - } -} - -pub fn video_refresh(data: *const c_void, width: u32, height: u32, pitch: usize) { - unsafe { - if let Some(callback) = CALLBACKS.video_refresh { - callback(data, width, height, pitch); - } - } -} - -pub fn audio_sample(left: i16, right: i16) { - unsafe { - if let Some(callback) = CALLBACKS.audio_sample { - callback(left, right); - } - } -} - -pub fn audio_sample_batch(data: *const i16, frames: usize) -> Option { - unsafe { - CALLBACKS - .audio_sample_batch - .map(|callback| callback(data, frames)) - } -} - -pub fn input_poll() { - unsafe { - if let Some(callback) = CALLBACKS.input_poll { - callback(); - } - } -} - -pub fn input_state(port: u32, device: u32, index: u32, id: u32) -> i16 { - unsafe { - CALLBACKS - .input_state - .map_or(0, |callback| callback(port, device, index, id)) - } -} - -pub fn log(level: u32, message: *const std::os::raw::c_char) { - unsafe { - if let Some(callback) = CALLBACKS.log { - callback(level, message); - } - } -} diff --git a/native/DingooEmu/crates/dingooemu-libretro/src/constants.rs b/native/DingooEmu/crates/dingooemu-libretro/src/constants.rs deleted file mode 100644 index 47db8aa..0000000 --- a/native/DingooEmu/crates/dingooemu-libretro/src/constants.rs +++ /dev/null @@ -1,43 +0,0 @@ -pub const RETRO_API_VERSION: u32 = 1; -pub const RETRO_DEVICE_NONE: u32 = 0; -pub const RETRO_DEVICE_JOYPAD: u32 = 1; - -pub const RETRO_DEVICE_ID_JOYPAD_B: u32 = 0; -pub const RETRO_DEVICE_ID_JOYPAD_Y: u32 = 1; -pub const RETRO_DEVICE_ID_JOYPAD_SELECT: u32 = 2; -pub const RETRO_DEVICE_ID_JOYPAD_START: u32 = 3; -pub const RETRO_DEVICE_ID_JOYPAD_UP: u32 = 4; -pub const RETRO_DEVICE_ID_JOYPAD_DOWN: u32 = 5; -pub const RETRO_DEVICE_ID_JOYPAD_LEFT: u32 = 6; -pub const RETRO_DEVICE_ID_JOYPAD_RIGHT: u32 = 7; -pub const RETRO_DEVICE_ID_JOYPAD_A: u32 = 8; -pub const RETRO_DEVICE_ID_JOYPAD_X: u32 = 9; -pub const RETRO_DEVICE_ID_JOYPAD_L: u32 = 10; -pub const RETRO_DEVICE_ID_JOYPAD_R: u32 = 11; - -pub const RETRO_REGION_NTSC: u32 = 0; - -pub const RETRO_ENVIRONMENT_SHUTDOWN: u32 = 7; -pub const RETRO_ENVIRONMENT_SET_PERFORMANCE_LEVEL: u32 = 8; -pub const RETRO_ENVIRONMENT_SET_PIXEL_FORMAT: u32 = 10; -pub const RETRO_ENVIRONMENT_SET_INPUT_DESCRIPTORS: u32 = 11; -pub const RETRO_ENVIRONMENT_GET_VARIABLE: u32 = 15; -pub const RETRO_ENVIRONMENT_SET_VARIABLES: u32 = 16; -pub const RETRO_ENVIRONMENT_GET_VARIABLE_UPDATE: u32 = 17; -pub const RETRO_ENVIRONMENT_GET_LOG_INTERFACE: u32 = 27; -pub const RETRO_ENVIRONMENT_GET_SAVE_DIRECTORY: u32 = 31; -pub const RETRO_ENVIRONMENT_SET_MEMORY_MAPS: u32 = 36 | 0x1_0000; - -pub const RETRO_MEMORY_MASK: u32 = 0xFF; -pub const RETRO_MEMORY_SYSTEM_RAM: u32 = 2; -pub const RETRO_MEMORY_VIDEO_RAM: u32 = 3; - -pub const RETRO_MEMDESC_SYSTEM_RAM: u64 = 1 << 2; -pub const RETRO_MEMDESC_VIDEO_RAM: u64 = 1 << 4; - -pub const RETRO_PIXEL_FORMAT_RGB565: u32 = 2; - -pub const RETRO_LOG_DEBUG: u32 = 0; -pub const RETRO_LOG_INFO: u32 = 1; -pub const RETRO_LOG_WARN: u32 = 2; -pub const RETRO_LOG_ERROR: u32 = 3; diff --git a/native/DingooEmu/crates/dingooemu-libretro/src/lib.rs b/native/DingooEmu/crates/dingooemu-libretro/src/lib.rs deleted file mode 100644 index 36f67de..0000000 --- a/native/DingooEmu/crates/dingooemu-libretro/src/lib.rs +++ /dev/null @@ -1,15 +0,0 @@ -//! RetroArch libretro core for Dingoo A320 software. - -#![allow(dead_code)] -#![allow(static_mut_refs)] -#![allow(clippy::not_unsafe_ptr_arg_deref)] - -mod api; -mod callbacks; -mod constants; -mod logger; -mod types; - -use dingooemu_core::Emulator; - -static mut EMULATOR: Option = None; diff --git a/native/DingooEmu/crates/dingooemu-libretro/src/logger.rs b/native/DingooEmu/crates/dingooemu-libretro/src/logger.rs deleted file mode 100644 index fd0fa6f..0000000 --- a/native/DingooEmu/crates/dingooemu-libretro/src/logger.rs +++ /dev/null @@ -1,63 +0,0 @@ -use std::ffi::CString; -use std::sync::atomic::{AtomicBool, Ordering}; - -use crate::callbacks; -use crate::constants::{RETRO_LOG_DEBUG, RETRO_LOG_ERROR, RETRO_LOG_INFO, RETRO_LOG_WARN}; - -struct LibretroLogger; - -static LOGGER: LibretroLogger = LibretroLogger; -static DEBUG_LOGGING: AtomicBool = AtomicBool::new(false); - -impl log::Log for LibretroLogger { - fn enabled(&self, metadata: &log::Metadata<'_>) -> bool { - metadata.level() <= log::Level::Info || DEBUG_LOGGING.load(Ordering::Relaxed) - } - - fn log(&self, record: &log::Record<'_>) { - if !self.enabled(record.metadata()) { - return; - } - - let level = match record.level() { - log::Level::Error => RETRO_LOG_ERROR, - log::Level::Warn => RETRO_LOG_WARN, - log::Level::Info => RETRO_LOG_INFO, - log::Level::Debug | log::Level::Trace => RETRO_LOG_DEBUG, - }; - let text = format!("[DingooEmu] {}\n", record.args()).replace('%', "%%"); - if let Ok(message) = CString::new(text) { - callbacks::log(level, message.as_ptr()); - } - } - - fn flush(&self) {} -} - -pub fn initialize() { - if log::set_logger(&LOGGER).is_ok() { - log::set_max_level(log::LevelFilter::Debug); - } -} - -pub fn set_debug_logging(enabled: bool) { - DEBUG_LOGGING.store(enabled, Ordering::Relaxed); -} - -#[cfg(test)] -mod tests { - use super::*; - use log::Log; - - #[test] - fn debug_records_follow_live_setting() { - let debug = log::Metadata::builder().level(log::Level::Debug).build(); - let info = log::Metadata::builder().level(log::Level::Info).build(); - set_debug_logging(false); - assert!(!LOGGER.enabled(&debug)); - assert!(LOGGER.enabled(&info)); - set_debug_logging(true); - assert!(LOGGER.enabled(&debug)); - set_debug_logging(false); - } -} diff --git a/native/DingooEmu/crates/dingooemu-libretro/src/types.rs b/native/DingooEmu/crates/dingooemu-libretro/src/types.rs deleted file mode 100644 index 298f222..0000000 --- a/native/DingooEmu/crates/dingooemu-libretro/src/types.rs +++ /dev/null @@ -1,91 +0,0 @@ -use std::ffi::c_void; -use std::os::raw::{c_char, c_uint}; - -pub type RetroEnvironmentCallback = Option bool>; -pub type RetroVideoRefreshCallback = - Option; -pub type RetroAudioSampleCallback = Option; -pub type RetroAudioSampleBatchCallback = Option usize>; -pub type RetroInputPollCallback = Option; -pub type RetroInputStateCallback = - Option i16>; -pub type RetroLogPrintfCallback = Option; - -#[repr(C)] -pub struct RetroSystemInfo { - pub library_name: *const c_char, - pub library_version: *const c_char, - pub valid_extensions: *const c_char, - pub need_fullpath: bool, - pub block_extract: bool, -} - -#[repr(C)] -pub struct RetroSystemAvInfo { - pub geometry: RetroGameGeometry, - pub timing: RetroSystemTiming, -} - -#[repr(C)] -pub struct RetroGameGeometry { - pub base_width: c_uint, - pub base_height: c_uint, - pub max_width: c_uint, - pub max_height: c_uint, - pub aspect_ratio: f32, -} - -#[repr(C)] -pub struct RetroSystemTiming { - pub fps: f64, - pub sample_rate: f64, -} - -#[repr(C)] -pub struct RetroGameInfo { - pub path: *const c_char, - pub data: *const c_void, - pub size: usize, - pub meta: *const c_char, -} - -#[repr(C)] -#[derive(Clone, Copy)] -pub struct RetroInputDescriptor { - pub port: c_uint, - pub device: c_uint, - pub index: c_uint, - pub id: c_uint, - pub description: *const c_char, -} - -#[repr(C)] -pub struct RetroLogCallback { - pub log: RetroLogPrintfCallback, -} - -#[repr(C)] -#[derive(Clone, Copy)] -pub struct RetroVariable { - pub key: *const c_char, - pub value: *const c_char, -} - -#[repr(C)] -#[derive(Clone, Copy)] -pub struct RetroMemoryDescriptor { - pub flags: u64, - pub ptr: *mut c_void, - pub offset: usize, - pub start: usize, - pub select: usize, - pub disconnect: usize, - pub len: usize, - pub addrspace: *const c_char, -} - -#[repr(C)] -pub struct RetroMemoryMap { - pub descriptors: *const RetroMemoryDescriptor, - pub num_descriptors: c_uint, -} diff --git a/native/DingooEmu/crates/dingooemu/Cargo.toml b/native/DingooEmu/crates/dingooemu/Cargo.toml deleted file mode 100644 index cff6c39..0000000 --- a/native/DingooEmu/crates/dingooemu/Cargo.toml +++ /dev/null @@ -1,19 +0,0 @@ -[package] -name = "dingooemu" -description.workspace = true -version.workspace = true -edition.workspace = true -license.workspace = true - -[[bin]] -name = "dingoo-emu" -path = "src/main.rs" - -[dependencies] -dingooemu-core = { workspace = true, features = ["standalone"] } -clap.workspace = true -minifb.workspace = true -log.workspace = true -env_logger.workspace = true -anyhow.workspace = true -serde_json.workspace = true diff --git a/native/DingooEmu/crates/dingooemu/src/gamepad_overlay.rs b/native/DingooEmu/crates/dingooemu/src/gamepad_overlay.rs deleted file mode 100644 index bebead6..0000000 --- a/native/DingooEmu/crates/dingooemu/src/gamepad_overlay.rs +++ /dev/null @@ -1,165 +0,0 @@ -use dingooemu_core::input::{ - BUTTON_A, BUTTON_B, BUTTON_DOWN, BUTTON_L, BUTTON_LEFT, BUTTON_R, BUTTON_RIGHT, BUTTON_SELECT, - BUTTON_START, BUTTON_UP, BUTTON_X, BUTTON_Y, -}; - -const IDLE: u32 = 0x00404040; -const PRESSED: u32 = 0x0000d8ff; -const ACTION_PRESSED: u32 = 0x00ff9f1a; - -pub fn draw(buffer: &mut [u32], width: usize, height: usize, buttons: u32) { - if width == 0 || height == 0 || buffer.len() < width.saturating_mul(height) { - return; - } - let unit = (width.min(height) / 48).clamp(1, 6); - let dpad_x = unit * 6; - let dpad_y = height.saturating_sub(unit * 7); - button( - buffer, - width, - height, - dpad_x, - dpad_y.saturating_sub(unit * 3), - unit * 2, - buttons & BUTTON_UP != 0, - PRESSED, - ); - button( - buffer, - width, - height, - dpad_x, - dpad_y + unit * 3, - unit * 2, - buttons & BUTTON_DOWN != 0, - PRESSED, - ); - button( - buffer, - width, - height, - dpad_x - unit * 3, - dpad_y, - unit * 2, - buttons & BUTTON_LEFT != 0, - PRESSED, - ); - button( - buffer, - width, - height, - dpad_x + unit * 3, - dpad_y, - unit * 2, - buttons & BUTTON_RIGHT != 0, - PRESSED, - ); - - let action_x = width.saturating_sub(unit * 6); - let action_y = height.saturating_sub(unit * 7); - for (offset_x, offset_y, mask) in [ - (0isize, -3isize, BUTTON_X), - (0, 3, BUTTON_B), - (-3, 0, BUTTON_Y), - (3, 0, BUTTON_A), - ] { - button( - buffer, - width, - height, - action_x.saturating_add_signed(offset_x * unit as isize), - action_y.saturating_add_signed(offset_y * unit as isize), - unit * 2, - buttons & mask != 0, - ACTION_PRESSED, - ); - } - - let system_y = height.saturating_sub(unit * 3); - button( - buffer, - width, - height, - (width / 2).saturating_sub(unit * 4), - system_y, - unit * 2, - buttons & BUTTON_SELECT != 0, - PRESSED, - ); - button( - buffer, - width, - height, - width / 2 + unit * 4, - system_y, - unit * 2, - buttons & BUTTON_START != 0, - PRESSED, - ); - button( - buffer, - width, - height, - unit * 3, - unit * 2, - unit * 2, - buttons & BUTTON_L != 0, - PRESSED, - ); - button( - buffer, - width, - height, - width.saturating_sub(unit * 3), - unit * 2, - unit * 2, - buttons & BUTTON_R != 0, - PRESSED, - ); -} - -#[allow(clippy::too_many_arguments)] -fn button( - buffer: &mut [u32], - width: usize, - height: usize, - center_x: usize, - center_y: usize, - radius: usize, - pressed: bool, - pressed_color: u32, -) { - let color = if pressed { pressed_color } else { IDLE }; - let radius_squared = radius * radius; - for y in center_y.saturating_sub(radius)..=(center_y + radius).min(height - 1) { - for x in center_x.saturating_sub(radius)..=(center_x + radius).min(width - 1) { - let distance_x = x.abs_diff(center_x); - let distance_y = y.abs_diff(center_y); - if distance_x * distance_x + distance_y * distance_y <= radius_squared { - buffer[y * width + x] = color; - } - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn overlay_highlights_pressed_direction_and_action_buttons() { - let mut buffer = vec![0; 320 * 240]; - draw(&mut buffer, 320, 240, BUTTON_UP | BUTTON_A); - assert!(buffer.contains(&PRESSED)); - assert!(buffer.contains(&ACTION_PRESSED)); - } - - #[test] - fn overlay_clips_tiny_and_short_buffers() { - let mut tiny = vec![0; 1]; - draw(&mut tiny, 1, 1, u32::MAX); - let mut short = vec![7; 3]; - draw(&mut short, 2, 2, u32::MAX); - assert_eq!(short, [7; 3]); - } -} diff --git a/native/DingooEmu/crates/dingooemu/src/keyboard.rs b/native/DingooEmu/crates/dingooemu/src/keyboard.rs deleted file mode 100644 index 737ebf2..0000000 --- a/native/DingooEmu/crates/dingooemu/src/keyboard.rs +++ /dev/null @@ -1,227 +0,0 @@ -use std::str::FromStr; - -use dingooemu_core::input::{ - BUTTON_A, BUTTON_B, BUTTON_DOWN, BUTTON_L, BUTTON_LEFT, BUTTON_R, BUTTON_RIGHT, BUTTON_SELECT, - BUTTON_START, BUTTON_UP, BUTTON_X, BUTTON_Y, -}; -use minifb::{Key, Window}; - -const DEFAULT_MAPPINGS: &[(u32, Key)] = &[ - (BUTTON_UP, Key::Up), - (BUTTON_DOWN, Key::Down), - (BUTTON_LEFT, Key::Left), - (BUTTON_RIGHT, Key::Right), - (BUTTON_A, Key::X), - (BUTTON_B, Key::Z), - (BUTTON_X, Key::S), - (BUTTON_Y, Key::A), - (BUTTON_SELECT, Key::RightShift), - (BUTTON_START, Key::Enter), - (BUTTON_L, Key::Q), - (BUTTON_R, Key::W), -]; - -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub struct RemapSpec { - button: u32, - key: Key, -} - -impl FromStr for RemapSpec { - type Err = String; - - fn from_str(value: &str) -> Result { - let (button, key) = value - .split_once(':') - .ok_or_else(|| "expected BUTTON:KEY, for example a:space".to_string())?; - Ok(Self { - button: parse_button(button.trim())?, - key: parse_key(key.trim())?, - }) - } -} - -pub struct KeyboardMapper { - mappings: Vec<(u32, Key)>, - swap_ab: bool, -} - -impl KeyboardMapper { - pub fn new(remappings: &[RemapSpec], swap_ab: bool) -> Self { - let mut mappings = DEFAULT_MAPPINGS.to_vec(); - for remapping in remappings { - mappings.retain(|(button, _)| *button != remapping.button); - mappings.push((remapping.button, remapping.key)); - } - Self { mappings, swap_ab } - } - - pub fn pressed_buttons(&self, window: &Window) -> u32 { - self.buttons_from_key_state(|key| window.is_key_down(key)) - } - - fn buttons_from_key_state(&self, mut is_down: impl FnMut(Key) -> bool) -> u32 { - let buttons = self - .mappings - .iter() - .filter(|(_, key)| is_down(*key)) - .fold(0, |buttons, (button, _)| buttons | button); - if self.swap_ab { - let without_ab = buttons & !(BUTTON_A | BUTTON_B); - without_ab - | if buttons & BUTTON_A != 0 { BUTTON_B } else { 0 } - | if buttons & BUTTON_B != 0 { BUTTON_A } else { 0 } - } else { - buttons - } - } -} - -fn parse_button(name: &str) -> Result { - match name.to_ascii_lowercase().as_str() { - "up" => Ok(BUTTON_UP), - "down" => Ok(BUTTON_DOWN), - "left" => Ok(BUTTON_LEFT), - "right" => Ok(BUTTON_RIGHT), - "a" => Ok(BUTTON_A), - "b" => Ok(BUTTON_B), - "x" => Ok(BUTTON_X), - "y" => Ok(BUTTON_Y), - "start" => Ok(BUTTON_START), - "select" => Ok(BUTTON_SELECT), - "l" => Ok(BUTTON_L), - "r" => Ok(BUTTON_R), - _ => Err(format!("unknown Dingoo button '{name}'")), - } -} - -fn parse_key(name: &str) -> Result { - let key = match name.to_ascii_lowercase().as_str() { - "a" => Key::A, - "b" => Key::B, - "c" => Key::C, - "d" => Key::D, - "e" => Key::E, - "f" => Key::F, - "g" => Key::G, - "h" => Key::H, - "i" => Key::I, - "j" => Key::J, - "k" => Key::K, - "l" => Key::L, - "m" => Key::M, - "n" => Key::N, - "o" => Key::O, - "p" => Key::P, - "q" => Key::Q, - "r" => Key::R, - "s" => Key::S, - "t" => Key::T, - "u" => Key::U, - "v" => Key::V, - "w" => Key::W, - "x" => Key::X, - "y" => Key::Y, - "z" => Key::Z, - "0" => Key::Key0, - "1" => Key::Key1, - "2" => Key::Key2, - "3" => Key::Key3, - "4" => Key::Key4, - "5" => Key::Key5, - "6" => Key::Key6, - "7" => Key::Key7, - "8" => Key::Key8, - "9" => Key::Key9, - "f1" => Key::F1, - "f2" => Key::F2, - "f3" => Key::F3, - "f4" => Key::F4, - "f5" => Key::F5, - "f6" => Key::F6, - "f7" => Key::F7, - "f8" => Key::F8, - "f9" => Key::F9, - "f10" => Key::F10, - "f11" => Key::F11, - "f12" => Key::F12, - "up" => Key::Up, - "down" => Key::Down, - "left" => Key::Left, - "right" => Key::Right, - "space" => Key::Space, - "enter" | "return" => Key::Enter, - "backspace" => Key::Backspace, - "tab" => Key::Tab, - "delete" => Key::Delete, - "home" => Key::Home, - "end" => Key::End, - "pageup" => Key::PageUp, - "pagedown" => Key::PageDown, - "leftshift" => Key::LeftShift, - "rightshift" => Key::RightShift, - "leftctrl" => Key::LeftCtrl, - "rightctrl" => Key::RightCtrl, - "leftalt" => Key::LeftAlt, - "rightalt" => Key::RightAlt, - "escape" | "esc" => return Err("escape is reserved for exiting the emulator".to_string()), - _ => return Err(format!("unknown key '{name}'")), - }; - Ok(key) -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn default_mapping_matches_retroarch_keyboard_defaults() { - let mapper = KeyboardMapper::new(&[], false); - for (key, button) in [ - (Key::Up, BUTTON_UP), - (Key::Down, BUTTON_DOWN), - (Key::Left, BUTTON_LEFT), - (Key::Right, BUTTON_RIGHT), - (Key::X, BUTTON_A), - (Key::Z, BUTTON_B), - (Key::S, BUTTON_X), - (Key::A, BUTTON_Y), - (Key::Enter, BUTTON_START), - (Key::RightShift, BUTTON_SELECT), - (Key::Q, BUTTON_L), - (Key::W, BUTTON_R), - ] { - assert_eq!( - mapper.buttons_from_key_state(|pressed| pressed == key), - button - ); - } - } - - #[test] - fn remapping_replaces_all_default_keys_for_a_button() { - let mapper = KeyboardMapper::new(&["a:space".parse().unwrap()], false); - assert_eq!( - mapper.buttons_from_key_state(|key| key == Key::Space), - BUTTON_A - ); - assert_eq!(mapper.buttons_from_key_state(|key| key == Key::X), 0); - } - - #[test] - fn swap_ab_exchanges_logical_button_masks() { - let mapper = KeyboardMapper::new(&[], true); - assert_eq!(mapper.buttons_from_key_state(|key| key == Key::X), BUTTON_B); - assert_eq!(mapper.buttons_from_key_state(|key| key == Key::Z), BUTTON_A); - } - - #[test] - fn parser_accepts_every_button_and_rejects_escape() { - for button in [ - "up", "down", "left", "right", "a", "b", "x", "y", "start", "select", "l", "r", - ] { - assert!(format!("{button}:space").parse::().is_ok()); - } - assert!("a:escape".parse::().is_err()); - } -} diff --git a/native/DingooEmu/crates/dingooemu/src/main.rs b/native/DingooEmu/crates/dingooemu/src/main.rs deleted file mode 100644 index 928d06d..0000000 --- a/native/DingooEmu/crates/dingooemu/src/main.rs +++ /dev/null @@ -1,611 +0,0 @@ -mod gamepad_overlay; -mod keyboard; -mod scaler; - -use clap::{Parser, ValueEnum}; -use dingooemu_core::cheats::CheatRule; -use dingooemu_core::cpu::UnknownInstructionPolicy; -use dingooemu_core::{video::SCREEN_HEIGHT, video::SCREEN_WIDTH, Emulator, UnknownHlePolicy}; -use minifb::{Key, Window, WindowOptions}; -use std::path::{Path, PathBuf}; - -use keyboard::{KeyboardMapper, RemapSpec}; -use scaler::{DisplayScaler, ScaleFilter}; - -#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, ValueEnum)] -enum UnknownInstructionMode { - Stop, - #[default] - Skip, -} - -impl From for UnknownInstructionPolicy { - fn from(mode: UnknownInstructionMode) -> Self { - match mode { - UnknownInstructionMode::Stop => Self::Stop, - UnknownInstructionMode::Skip => Self::Skip, - } - } -} - -#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, ValueEnum)] -enum UnknownHleMode { - #[default] - Report, - Stop, -} - -impl UnknownHleMode { - fn as_str(self) -> &'static str { - match self { - Self::Report => "report", - Self::Stop => "stop", - } - } -} - -impl From for UnknownHlePolicy { - fn from(mode: UnknownHleMode) -> Self { - match mode { - UnknownHleMode::Report => Self::Report, - UnknownHleMode::Stop => Self::Stop, - } - } -} - -#[cfg(target_os = "windows")] -mod screen { - unsafe extern "system" { - fn GetSystemMetrics(index: i32) -> i32; - } - - pub fn size() -> (usize, usize) { - unsafe { (GetSystemMetrics(0) as usize, GetSystemMetrics(1) as usize) } - } -} - -#[cfg(target_os = "linux")] -mod screen { - type Display = *mut core::ffi::c_void; - - #[link(name = "X11")] - unsafe extern "system" { - fn XOpenDisplay(name: *const u8) -> Display; - fn XCloseDisplay(display: Display) -> i32; - fn XDisplayWidth(display: Display, screen: i32) -> i32; - fn XDisplayHeight(display: Display, screen: i32) -> i32; - } - - pub fn size() -> (usize, usize) { - unsafe { - let display = XOpenDisplay(std::ptr::null()); - if display.is_null() { - return (800, 600); - } - let size = ( - XDisplayWidth(display, 0) as usize, - XDisplayHeight(display, 0) as usize, - ); - let _ = XCloseDisplay(display); - size - } - } -} - -#[cfg(target_os = "macos")] -mod screen { - #[link(name = "CoreGraphics", kind = "framework")] - unsafe extern "C" { - fn CGMainDisplayID() -> u32; - fn CGDisplayPixelsWide(display: u32) -> usize; - fn CGDisplayPixelsHigh(display: u32) -> usize; - } - - pub fn size() -> (usize, usize) { - unsafe { - let display = CGMainDisplayID(); - (CGDisplayPixelsWide(display), CGDisplayPixelsHigh(display)) - } - } -} - -/// Dingoo A320 Emulator -#[derive(Parser, Debug)] -#[command( - name = "dingoo-emu", - version, - about = "A Dingoo A320 emulator written in Rust" -)] -struct Args { - /// Path to the .app game file - path: String, - - /// Window scale factor - #[arg( - short, - long, - default_value_t = 2, - value_parser = clap::value_parser!(u32).range(1..=16) - )] - scale: u32, - - /// Run in fullscreen mode - #[arg(short, long)] - fullscreen: bool, - - /// Master audio volume (0-100) - #[arg(short, long, default_value_t = 100, value_parser = clap::value_parser!(u8).range(0..=100))] - volume: u8, - - /// Enable emulator debug logging - #[arg(long)] - debug_logging: bool, - - /// Remap a Dingoo button using BUTTON:KEY syntax - #[arg(long = "remap", value_name = "BUTTON:KEY")] - remappings: Vec, - - /// Swap the emulated A and B buttons - #[arg(long = "swap-ab")] - swap_ab: bool, - - /// Pixel scaling filter for display output - #[arg(long, value_enum, default_value_t = ScaleFilter::Nearest)] - filter: ScaleFilter, - - /// Show the current Dingoo button state over the game frame - #[arg(long)] - show_gamepad: bool, - - /// Frames before a held button starts repeating - #[arg(long = "repeat-delay", default_value_t = 24)] - repeat_delay: u32, - - /// Frames between repeated button presses - #[arg(long = "repeat-period", default_value_t = 6, value_parser = clap::value_parser!(u32).range(1..))] - repeat_period: u32, - - /// Freeze a memory address or MIPS register using TARGET=VALUE syntax - #[arg(long = "cheat", value_name = "RULE")] - cheats: Vec, - - /// Behavior when an unknown MIPS instruction is encountered - #[arg(long, value_enum, default_value_t = UnknownInstructionMode::Skip)] - unknown_instruction_policy: UnknownInstructionMode, - - /// Behavior when an unknown SDK HLE function is called - #[arg(long, value_enum, default_value_t = UnknownHleMode::Report)] - unknown_hle_policy: UnknownHleMode, - - /// Allow an exact unknown SDK function name in strict HLE mode - #[arg(long = "allow-unknown-hle", value_name = "NAME")] - allowed_unknown_hle: Vec, - - /// Write aggregated unknown SDK HLE diagnostics as JSON - #[arg(long = "hle-report", value_name = "PATH")] - hle_report: Option, - - /// Run in headless mode (no window) - #[arg(long)] - headless: bool, - - /// Number of frames to run in headless mode - #[arg(long, default_value_t = 300)] - frames: u32, - - /// Take a screenshot after N frames and exit (saves as PNG) - #[arg(short = 'S', long = "screenshot", value_name = "PATH")] - screenshot: Option, - - /// Number of frames to run before taking screenshot (default: 30) - #[arg(long = "screenshot-frames", default_value = "30")] - screenshot_frames: u32, -} - -fn main() -> anyhow::Result<()> { - // Parse command line arguments - let args = Args::parse(); - - let default_log_filter = if args.debug_logging { "debug" } else { "info" }; - env_logger::Builder::from_env(env_logger::Env::default().default_filter_or(default_log_filter)) - .format_timestamp_millis() - .init(); - - // Load the game - log::info!("Loading game: {}", args.path); - let mut emu = Emulator::from_path(&args.path)?; - emu.cpu - .set_unknown_instruction_policy(args.unknown_instruction_policy.into()); - emu.set_unknown_hle_policy(args.unknown_hle_policy.into()); - emu.set_unknown_hle_allowlist(args.allowed_unknown_hle.iter().cloned()); - emu.audio.set_master_volume(args.volume); - emu.input - .set_repeat_timing(args.repeat_delay, args.repeat_period); - for (index, cheat) in args.cheats.iter().cloned().enumerate() { - emu.set_parsed_cheat(index as u32, true, cheat)?; - } - - emu.audio - .set_host_output_enabled(args.screenshot.is_none() && !args.headless); - - emu.start(); - - let emulation_result = run_emulation(&args, &mut emu); - log_unknown_hle_summary(&emu); - let report_result = write_unknown_hle_report(&args, &emu); - if let Err(error) = &report_result { - log::error!("Failed to write HLE diagnostics: {error}"); - } - emulation_result?; - report_result?; - Ok(()) -} - -fn run_emulation(args: &Args, emu: &mut Emulator) -> anyhow::Result<()> { - // Screenshot mode: run headless for N frames, save PNG, and exit - if let Some(ref screenshot_path) = args.screenshot { - for frame in 0..args.screenshot_frames { - emu.tick()?; - if frame % 60 == 0 { - log::info!("Frame {}", frame); - } - } - emu.video.save_screenshot(screenshot_path)?; - log::info!("Screenshot saved to: {}", screenshot_path.display()); - return Ok(()); - } - - if args.headless { - // Headless mode: run for the requested number of frames - log::info!("Running in headless mode"); - for frame in 0..args.frames { - emu.tick()?; - if frame % 60 == 0 { - log::info!("Frame {}", frame); - } - } - log::info!("Headless run complete: {} frames", args.frames); - } else { - // Windowed mode - let (width, height) = if args.fullscreen { - screen::size() - } else { - ( - (SCREEN_WIDTH * args.scale) as usize, - (SCREEN_HEIGHT * args.scale) as usize, - ) - }; - - let mut window = Window::new( - "Dingoo A320 Emulator", - width, - height, - WindowOptions { - resize: !args.fullscreen, - borderless: args.fullscreen, - scale_mode: minifb::ScaleMode::Stretch, - ..WindowOptions::default() - }, - )?; - - if args.fullscreen { - window.topmost(true); - window.set_position(0, 0); - } - - // Limit to ~60fps - window.set_target_fps(60); - let keyboard = KeyboardMapper::new(&args.remappings, args.swap_ab); - let mut display_scaler = DisplayScaler::new(args.filter); - - // Main loop - while window.is_open() && !window.is_key_down(Key::Escape) { - // Poll input - let buttons = keyboard.pressed_buttons(&window); - emu.set_buttons(buttons); - - // Run one frame - emu.tick()?; - - // Get framebuffer and convert to XRGB8888 - let mut buffer = emu.video.to_xrgb8888(); - if args.show_gamepad { - gamepad_overlay::draw( - &mut buffer, - SCREEN_WIDTH as usize, - SCREEN_HEIGHT as usize, - buttons, - ); - } - let (window_width, window_height) = window.get_size(); - let output = display_scaler.render( - &buffer, - SCREEN_WIDTH as usize, - SCREEN_HEIGHT as usize, - window_width, - window_height, - ); - - // Update window - window.update_with_buffer(output, window_width.max(1), window_height.max(1))?; - } - } - - Ok(()) -} - -fn log_unknown_hle_summary(emu: &Emulator) { - let calls: Vec<_> = emu.unknown_hle_calls().collect(); - if calls.is_empty() { - log::info!("Unknown SDK HLE summary: none"); - return; - } - - log::warn!( - "Unknown SDK HLE summary: {} function(s); compatibility return values may hide missing behavior", - calls.len() - ); - for call in calls { - log::warn!( - " {}: count={}, first_pc={:#010x}, import={:#010x}, arguments={:#010x?}", - call.name, - call.count, - call.first_pc, - call.import_address, - call.first_arguments - ); - } -} - -fn unknown_hle_report(args: &Args, emu: &Emulator) -> serde_json::Value { - let content = Path::new(&args.path) - .file_name() - .and_then(|name| name.to_str()) - .unwrap_or(&args.path); - let unknown_hle: Vec<_> = emu.unknown_hle_calls().collect(); - serde_json::json!({ - "schema_version": 1, - "content": content, - "policy": args.unknown_hle_policy.as_str(), - "allowlist": args.allowed_unknown_hle, - "unknown_hle": unknown_hle, - }) -} - -fn write_unknown_hle_report(args: &Args, emu: &Emulator) -> anyhow::Result<()> { - let Some(path) = args.hle_report.as_ref() else { - return Ok(()); - }; - if let Some(parent) = path - .parent() - .filter(|parent| !parent.as_os_str().is_empty()) - { - std::fs::create_dir_all(parent)?; - } - let output = serde_json::to_vec_pretty(&unknown_hle_report(args, emu))?; - std::fs::write(path, output)?; - log::info!("HLE diagnostics saved to: {}", path.display()); - Ok(()) -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn scale_accepts_supported_range() { - assert_eq!( - Args::try_parse_from(["dingoo-emu", "--scale", "1", "game.app"]) - .unwrap() - .scale, - 1 - ); - assert_eq!( - Args::try_parse_from(["dingoo-emu", "--scale", "16", "game.app"]) - .unwrap() - .scale, - 16 - ); - } - - #[test] - fn scale_rejects_zero_and_excessive_values() { - assert!(Args::try_parse_from(["dingoo-emu", "--scale", "0", "game.app"]).is_err()); - assert!(Args::try_parse_from(["dingoo-emu", "--scale", "17", "game.app"]).is_err()); - } - - #[test] - fn headless_frame_count_defaults_to_300_and_is_configurable() { - assert_eq!( - Args::try_parse_from(["dingoo-emu", "game.app"]) - .unwrap() - .frames, - 300 - ); - assert_eq!( - Args::try_parse_from(["dingoo-emu", "--headless", "--frames", "12", "game.app"]) - .unwrap() - .frames, - 12 - ); - } - - #[test] - fn fullscreen_flag_is_parsed() { - assert!( - Args::try_parse_from(["dingoo-emu", "--fullscreen", "game.app"]) - .unwrap() - .fullscreen - ); - } - - #[test] - fn volume_accepts_percent_range() { - assert_eq!( - Args::try_parse_from(["dingoo-emu", "game.app"]) - .unwrap() - .volume, - 100 - ); - assert_eq!( - Args::try_parse_from(["dingoo-emu", "--volume", "0", "game.app"]) - .unwrap() - .volume, - 0 - ); - assert!(Args::try_parse_from(["dingoo-emu", "--volume", "101", "game.app"]).is_err()); - } - - #[test] - fn debug_logging_flag_is_parsed() { - assert!( - Args::try_parse_from(["dingoo-emu", "--debug-logging", "game.app"]) - .unwrap() - .debug_logging - ); - } - - #[test] - fn repeated_remap_options_are_parsed() { - let args = Args::try_parse_from([ - "dingoo-emu", - "--remap", - "a:space", - "--remap", - "select:tab", - "game.app", - ]) - .unwrap(); - assert_eq!(args.remappings.len(), 2); - } - - #[test] - fn swap_ab_flag_is_parsed() { - assert!( - Args::try_parse_from(["dingoo-emu", "--swap-ab", "game.app"]) - .unwrap() - .swap_ab - ); - } - - #[test] - fn every_display_filter_is_parsed() { - for (name, expected) in [ - ("nearest", ScaleFilter::Nearest), - ("bilinear", ScaleFilter::Bilinear), - ("bicubic", ScaleFilter::Bicubic), - ("xbrz", ScaleFilter::Xbrz), - ] { - assert_eq!( - Args::try_parse_from(["dingoo-emu", "--filter", name, "game.app"]) - .unwrap() - .filter, - expected - ); - } - } - - #[test] - fn show_gamepad_flag_is_parsed() { - assert!( - Args::try_parse_from(["dingoo-emu", "--show-gamepad", "game.app"]) - .unwrap() - .show_gamepad - ); - } - - #[test] - fn repeat_timing_is_configurable_and_period_rejects_zero() { - let args = Args::try_parse_from([ - "dingoo-emu", - "--repeat-delay", - "10", - "--repeat-period", - "2", - "game.app", - ]) - .unwrap(); - assert_eq!((args.repeat_delay, args.repeat_period), (10, 2)); - assert!(Args::try_parse_from(["dingoo-emu", "--repeat-period", "0", "game.app"]).is_err()); - } - - #[test] - fn repeated_cheat_rules_are_parsed() { - let args = Args::try_parse_from([ - "dingoo-emu", - "--cheat", - "mem8:0x100=1", - "--cheat", - "reg:r4=7", - "game.app", - ]) - .unwrap(); - assert_eq!(args.cheats.len(), 2); - } - - #[test] - fn unknown_instruction_policy_accepts_stop_and_skip() { - for (name, expected) in [ - ("stop", UnknownInstructionMode::Stop), - ("skip", UnknownInstructionMode::Skip), - ] { - assert_eq!( - Args::try_parse_from([ - "dingoo-emu", - "--unknown-instruction-policy", - name, - "game.app", - ]) - .unwrap() - .unknown_instruction_policy, - expected - ); - } - } - - #[test] - fn unknown_hle_options_default_to_reporting_and_accept_strict_allowlists() { - let defaults = Args::try_parse_from(["dingoo-emu", "game.app"]).unwrap(); - assert_eq!(defaults.unknown_hle_policy, UnknownHleMode::Report); - assert!(defaults.allowed_unknown_hle.is_empty()); - assert!(defaults.hle_report.is_none()); - - let strict = Args::try_parse_from([ - "dingoo-emu", - "--unknown-hle-policy", - "stop", - "--allow-unknown-hle", - "legacy_one", - "--allow-unknown-hle", - "legacy_two", - "--hle-report", - "report.json", - "game.app", - ]) - .unwrap(); - assert_eq!(strict.unknown_hle_policy, UnknownHleMode::Stop); - assert_eq!(strict.allowed_unknown_hle, ["legacy_one", "legacy_two"]); - assert_eq!(strict.hle_report, Some(PathBuf::from("report.json"))); - } - - #[test] - fn empty_unknown_hle_report_has_a_stable_schema() { - let args = Args::try_parse_from([ - "dingoo-emu", - "--hle-report", - "report.json", - "folder/game.app", - ]) - .unwrap(); - assert_eq!( - unknown_hle_report(&args, &Emulator::default()), - serde_json::json!({ - "schema_version": 1, - "content": "game.app", - "policy": "report", - "allowlist": [], - "unknown_hle": [], - }) - ); - } -} diff --git a/native/DingooEmu/crates/dingooemu/src/scaler.rs b/native/DingooEmu/crates/dingooemu/src/scaler.rs deleted file mode 100644 index ff47b5e..0000000 --- a/native/DingooEmu/crates/dingooemu/src/scaler.rs +++ /dev/null @@ -1,244 +0,0 @@ -use clap::ValueEnum; - -#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, ValueEnum)] -pub enum ScaleFilter { - #[default] - Nearest, - Bilinear, - Bicubic, - Xbrz, -} - -pub struct DisplayScaler { - filter: ScaleFilter, - scaled: Vec, - presentation: Vec, -} - -impl DisplayScaler { - pub fn new(filter: ScaleFilter) -> Self { - Self { - filter, - scaled: Vec::new(), - presentation: Vec::new(), - } - } - - pub fn render( - &mut self, - source: &[u32], - source_width: usize, - source_height: usize, - window_width: usize, - window_height: usize, - ) -> &[u32] { - let window_width = window_width.max(1); - let window_height = window_height.max(1); - let (width, height) = fit_aspect(source_width, source_height, window_width, window_height); - self.scaled.resize(width * height, 0); - scale( - self.filter, - source, - source_width, - source_height, - width, - height, - &mut self.scaled, - ); - - self.presentation.resize(window_width * window_height, 0); - self.presentation.fill(0); - let offset_x = (window_width - width) / 2; - let offset_y = (window_height - height) / 2; - for row in 0..height { - let source_start = row * width; - let destination_start = (row + offset_y) * window_width + offset_x; - self.presentation[destination_start..destination_start + width] - .copy_from_slice(&self.scaled[source_start..source_start + width]); - } - &self.presentation - } -} - -fn fit_aspect( - source_width: usize, - source_height: usize, - window_width: usize, - window_height: usize, -) -> (usize, usize) { - if window_width * source_height <= window_height * source_width { - ( - window_width, - (window_width * source_height / source_width).max(1), - ) - } else { - ( - (window_height * source_width / source_height).max(1), - window_height, - ) - } -} - -#[allow(clippy::too_many_arguments)] -fn scale( - filter: ScaleFilter, - source: &[u32], - source_width: usize, - source_height: usize, - width: usize, - height: usize, - output: &mut [u32], -) { - for destination_y in 0..height { - for destination_x in 0..width { - let source_x = (destination_x as f32 + 0.5) * source_width as f32 / width as f32 - 0.5; - let source_y = - (destination_y as f32 + 0.5) * source_height as f32 / height as f32 - 0.5; - output[destination_y * width + destination_x] = match filter { - ScaleFilter::Nearest => { - sample_nearest(source, source_width, source_height, source_x, source_y) - } - ScaleFilter::Bilinear => { - sample_bilinear(source, source_width, source_height, source_x, source_y) - } - ScaleFilter::Bicubic => { - sample_bicubic(source, source_width, source_height, source_x, source_y) - } - ScaleFilter::Xbrz => { - sample_edge_aware(source, source_width, source_height, source_x, source_y) - } - }; - } - } -} - -fn pixel(source: &[u32], width: usize, height: usize, x: i32, y: i32) -> u32 { - let x = x.clamp(0, width as i32 - 1) as usize; - let y = y.clamp(0, height as i32 - 1) as usize; - source[y * width + x] -} - -fn sample_nearest(source: &[u32], width: usize, height: usize, x: f32, y: f32) -> u32 { - pixel(source, width, height, x.round() as i32, y.round() as i32) -} - -fn sample_bilinear(source: &[u32], width: usize, height: usize, x: f32, y: f32) -> u32 { - let x0 = x.floor() as i32; - let y0 = y.floor() as i32; - let tx = x - x.floor(); - let ty = y - y.floor(); - blend( - &[ - pixel(source, width, height, x0, y0), - pixel(source, width, height, x0 + 1, y0), - pixel(source, width, height, x0, y0 + 1), - pixel(source, width, height, x0 + 1, y0 + 1), - ], - &[ - (1.0 - tx) * (1.0 - ty), - tx * (1.0 - ty), - (1.0 - tx) * ty, - tx * ty, - ], - ) -} - -fn sample_bicubic(source: &[u32], width: usize, height: usize, x: f32, y: f32) -> u32 { - let base_x = x.floor() as i32; - let base_y = y.floor() as i32; - let mut pixels = [0; 16]; - let mut weights = [0.0; 16]; - let mut index = 0; - for offset_y in -1..=2 { - for offset_x in -1..=2 { - pixels[index] = pixel(source, width, height, base_x + offset_x, base_y + offset_y); - weights[index] = - cubic(x - (base_x + offset_x) as f32) * cubic(y - (base_y + offset_y) as f32); - index += 1; - } - } - blend(&pixels, &weights) -} - -fn cubic(value: f32) -> f32 { - let value = value.abs(); - if value <= 1.0 { - 1.5 * value.powi(3) - 2.5 * value.powi(2) + 1.0 - } else if value < 2.0 { - -0.5 * value.powi(3) + 2.5 * value.powi(2) - 4.0 * value + 2.0 - } else { - 0.0 - } -} - -fn sample_edge_aware(source: &[u32], width: usize, height: usize, x: f32, y: f32) -> u32 { - let center_x = x.round() as i32; - let center_y = y.round() as i32; - let center = pixel(source, width, height, center_x, center_y); - let horizontal = if x < center_x as f32 { - pixel(source, width, height, center_x - 1, center_y) - } else { - pixel(source, width, height, center_x + 1, center_y) - }; - let vertical = if y < center_y as f32 { - pixel(source, width, height, center_x, center_y - 1) - } else { - pixel(source, width, height, center_x, center_y + 1) - }; - if horizontal == vertical && color_distance(center, horizontal) > 48 { - blend(&[center, horizontal], &[0.5, 0.5]) - } else { - center - } -} - -fn color_distance(left: u32, right: u32) -> u32 { - [0, 8, 16] - .into_iter() - .map(|shift| ((left >> shift) & 0xff).abs_diff((right >> shift) & 0xff)) - .sum() -} - -fn blend(pixels: &[u32], weights: &[f32]) -> u32 { - let channel = |shift: u32| { - pixels - .iter() - .zip(weights) - .map(|(pixel, weight)| ((*pixel >> shift) & 0xffu32) as f32 * weight) - .sum::() - .round() - .clamp(0.0, 255.0) as u32 - }; - (channel(16) << 16) | (channel(8) << 8) | channel(0) -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn filters_preserve_a_uniform_frame() { - for filter in [ - ScaleFilter::Nearest, - ScaleFilter::Bilinear, - ScaleFilter::Bicubic, - ScaleFilter::Xbrz, - ] { - let mut scaler = DisplayScaler::new(filter); - assert_eq!( - scaler.render(&[0x00123456; 4], 2, 2, 7, 7), - [0x00123456; 49] - ); - } - } - - #[test] - fn presentation_preserves_aspect_ratio() { - let mut scaler = DisplayScaler::new(ScaleFilter::Nearest); - let output = scaler.render(&[0x00123456; 4], 2, 2, 4, 2); - assert_eq!( - output, - [0, 0x00123456, 0x00123456, 0, 0, 0x00123456, 0x00123456, 0] - ); - } -} diff --git a/native/DingooEmu/docs/Android-Libretro-Core.md b/native/DingooEmu/docs/Android-Libretro-Core.md deleted file mode 100644 index 7a6b4fc..0000000 --- a/native/DingooEmu/docs/Android-Libretro-Core.md +++ /dev/null @@ -1,65 +0,0 @@ -# Android Libretro Core - -The DingooEmu libretro core also runs on Android, so it can be reused by most -Android RetroArch-based frontends. - -## Install in RetroArch on Android - -### Via Online Updater (Recommended) - -The easiest way is to download the core directly from RetroArch's built-in Online Updater: - -1. Open RetroArch -2. Go to **Main Menu → Online Updater → Core Downloader** -3. Find and select **Dingoo A320 (DingooEmu)**, wait for the download to complete -4. Go back to **Main Menu → Load Core** — the Dingoo A320 core should appear - -To update an installed core: - -1. Open RetroArch -2. Go to **Main Menu → Online Updater → Update Installed Cores** - -### Manual Installation (Alternative) - -If the Online Updater is not available, you can install the core manually: - -1. **Download** `dingoo-emu-android-libretro.tar.gz` from the - [Releases](https://github.com/AloysHF/DingooEmu/releases) page. It - contains `dingooemu_libretro_android.so` for the `arm64-v8a`, - `armeabi-v7a`, `x86` and `x86_64` ABIs. -2. **Install the core**: copy the `dingooemu_libretro_android.so` matching - your device's ABI (most modern devices are `arm64-v8a`) into RetroArch's - `cores/` directory (typically - `/storage/emulated/0/RetroArch/cores/` or the app's internal `cores/` path), - and copy `dingooemu_libretro.info` into RetroArch's `info/` directory. -3. **Load** the core and content the same way as on desktop. - -## CPU execution engines - -The `arm64-v8a` and `x86_64` cores use a tiered JIT by default. Frequently -executed MIPS32 blocks are translated to native code, while unsupported or -low-frequency paths continue through the cached interpreter. Compilation is -rate-limited to avoid introducing frame-time spikes while a game warms up. - -Use **Quick Menu → Core Options → CPU Execution Engine** to switch to -`interpreter` for compatibility testing. The `armeabi-v7a` and `x86` cores -always use the interpreter and do not expose this option. - -## Building the Android core locally - -Building for Android requires the [Android NDK](https://developer.android.com/ndk) -and [`cargo-ndk`](https://github.com/bbqsrc/cargo-ndk): - -```bash -cargo install cargo-ndk -rustup target add aarch64-linux-android armv7-linux-androideabi i686-linux-android x86_64-linux-android -export ANDROID_NDK_HOME=/path/to/android-ndk - -# Build all four ABIs (artifacts land in target//release/) -cargo ndk -t arm64-v8a -t armeabi-v7a -t x86 -t x86_64 --platform 21 \ - build -p dingooemu-libretro --release -``` - -Each ABI produces `libdingooemu_libretro.so`; rename it to -`dingooemu_libretro_android.so` when installing into RetroArch on Android. -The CI release workflow performs this packaging automatically. diff --git a/native/DingooEmu/docs/CONTRIBUTING.md b/native/DingooEmu/docs/CONTRIBUTING.md deleted file mode 100644 index 914678e..0000000 --- a/native/DingooEmu/docs/CONTRIBUTING.md +++ /dev/null @@ -1,51 +0,0 @@ -# Contributing to DingooEmu - -Thank you for your interest in contributing! - -## Getting Started - -1. Fork the repository -2. Create a feature branch -3. Make your changes -4. Submit a pull request - -## Development Setup - -```bash -git clone https://github.com/your-username/DingooEmu.git -cd DingooEmu -cargo build -``` - -## Code Style - -- Use English for all comments and documentation -- Follow Rust API Guidelines -- Use `cargo fmt` before committing -- Use `cargo clippy` to check for warnings - -## Testing - -```bash -cargo test --workspace -``` - -## Areas Where Help Is Needed - -We welcome contributions in the following areas: - -- **Game compatibility testing** — Test `.app` files and report results -- **MIPS instruction implementation** — Complete the CPU interpreter -- **SDK HLE functions** — Implement missing Dingoo SDK calls -- **Platform porting** — Improve support for macOS, Android, iOS -- **Documentation** — Improve guides, add examples, fix typos -- **Bug reports** — Report issues with specific games or features -- **RetroArch integration** — Improve the libretro core - -Check the [good first issue](https://github.com/AloysHF/DingooEmu/labels/good%20first%20issue) -and [help wanted](https://github.com/AloysHF/DingooEmu/labels/help%20wanted) -labels for beginner-friendly tasks. - -## License - -By contributing, you agree that your contributions will be licensed under the BSD-3-Clause License. diff --git a/native/DingooEmu/docs/Game-Compatibility.md b/native/DingooEmu/docs/Game-Compatibility.md deleted file mode 100644 index 6ce6b23..0000000 --- a/native/DingooEmu/docs/Game-Compatibility.md +++ /dev/null @@ -1,72 +0,0 @@ -# Game Compatibility - -Compatibility is still experimental. The results below cover startup and -initial rendering only and do not imply complete gameplay support. - -The current sample set contains 39 distinct `.app` entries, including alternate -builds of several games. On August 13, 2026, all 39 entries completed the release -batch screenshot process and rendered a non-black frame. The newer Hell Striker -II build and Overlord Fighter use 300-frame and 120-frame capture overrides, -respectively; Sword and Fairy uses 1200 frames because its startup sequence -contains several timed splash screens. Input, audio, save data, extended -gameplay, and full completion still require separate verification. - -Overlord Fighter has a separate real-game regression test for its legacy -resource-conversion imports, title-screen framebuffer output, and direct button -polling. A scripted A-button press leaves the title menu and starts the game. - -GooPlayer's content-discovery check confirms that its startup scan finds the -three companion tracker files in the game directory and opens the playlist -instead of remaining on the title screen. Playback correctness is not part of -this check. - -## Verified Games - -| English Name | 中文名 | Filename | Screenshot | Status | -|--------------|--------|----------|------------|--------| -| Ali Baba | 阿里巴巴 | `tmp/dingoo_game/AliBaba.app` | ![AliBaba](images/AliBaba.png) | ✅ Pass | -| Astro Lander | 星际着陆 | `tmp/dingoo_game/Astro-Lander/Astro-Lander.app` | ![Astro-Lander](images/Astro-Lander__Astro-Lander.png) | ✅ Pass | -| Block Breaker | 打砖块 | `tmp/dingoo_game/Block Breaker.app` | ![Block-Breaker](images/Block_Breaker.png) | ✅ Pass | -| Candy | 糖果屋 | `tmp/dingoo_game/Candy.app` | ![Candy](images/Candy.png) | ✅ Pass | -| Decollation Warrior | 战神刑天 | `tmp/dingoo_game/Decollation-Warrior.app` | ![Decollation-Warrior](images/Decollation-Warrior.png) | ✅ Pass | -| Formula One | F1赛车 | `tmp/dingoo_game/Fomula-One.app` | ![Fomula-One](images/Fomula-One.png) | ✅ Pass | -| GooPlayer | Goo播放器 | `tmp/dingoo_game/GooPlayer/GooPlayer.app` | ![GooPlayer](images/GooPlayer__GooPlayer.png) | ✅ Pass | -| Hell Striker II (2008-12-29 build) | 天地道(2008-12-29 版本) | `tmp/dingoo_game/Hell Striker II-20081229173817.app` | ![Hell-Striker-II-2008-12-29-build](images/Hell_Striker_II-20081229173817.png) | ✅ Pass | -| Hell Striker II (2009-01-22 build) | 天地道(2009-01-22 版本) | `tmp/dingoo_game/Hell Striker II-20090122224048.app` | ![Hell-Striker-II-2009-01-22-build](images/Hell_Striker_II-20090122224048.png) | ✅ Pass | -| Hexa-Virus | 六角病毒(病毒感染) | `tmp/dingoo_game/Hexa-Virus.app` | ![Hexa-Virus](images/Hexa-Virus.png) | ✅ Pass | -| Landlord | 斗地主 | `tmp/dingoo_game/Landlord.app` | ![Landlord](images/Landlord.png) | ✅ Pass | -| Link'em Up | 连连看 | `tmp/dingoo_game/Link'em Up.app` | ![Link-em-Up](images/Link'em_Up.png) | ✅ Pass | -| Manic Miner | 疯狂矿工 | `tmp/dingoo_game/Manic-Miner.app` | ![Manic-Miner](images/Manic-Miner.png) | ✅ Pass | -| Mine Sweeper | 扫雷 | `tmp/dingoo_game/Mine Sweeper.app` | ![Mine-Sweeper](images/Mine_Sweeper.png) | ✅ Pass | -| Mushroom Roulette | 蘑菇轮盘 | `tmp/dingoo_game/Mushroom Roulette.app` | ![Mushroom-Roulette](images/Mushroom_Roulette.png) | ✅ Pass | -| Nose Breaker | 卢比卢比 | `tmp/dingoo_game/Nose Breaker.app` | ![Nose-Breaker](images/Nose_Breaker.png) | ✅ Pass | -| Overlord Fighter (stub build) | 霸王战纪(桩版本) | `tmp/dingoo_game/Overlord-Fighter-Stub.app` | ![Overlord-Fighter-Stub](images/Overlord-Fighter-Stub.png) | ✅ Pass | -| Overlord Fighter | 霸王战纪(Yi-chi King Fighter) | `tmp/dingoo_game/Overlord-Fighter.app` | ![Overlord-Fighter](images/Overlord-Fighter.png) | ✅ Pass | -| Platinum Sudoku | 白金数独 | `tmp/dingoo_game/Platinum Sudoku.app` | ![Platinum-Sudoku](images/Platinum_Sudoku.png) | ✅ Pass | -| PoPo Bash | 泡泡 | `tmp/dingoo_game/PoPo Bash.app` | ![PoPo-Bash](images/PoPo_Bash.png) | ✅ Pass | -| Rick Dangerous | 里克危险 | `tmp/dingoo_game/Rick-Dangerous.app` | ![Rick-Dangerous](images/Rick-Dangerous.png) | ✅ Pass | -| Rubido (2009-05-12 build) | 鲁比多(2009-05-12 版本) | `tmp/dingoo_game/Rubido-20090512001427.app` | ![Rubido-2009-05-12-build](images/Rubido-20090512001427.png) | ✅ Pass | -| Rubido (2009-05-16 build) | 鲁比多(2009-05-16 版本) | `tmp/dingoo_game/Rubido-20090516230856.app` | ![Rubido-2009-05-16-build](images/Rubido-20090516230856.png) | ✅ Pass | -| SameGoo | 消消乐 | `tmp/dingoo_game/SameGoo/samegoo.app` | ![SameGoo](images/SameGoo__samegoo.png) | ✅ Pass | -| Millipede | 千足虫 | `tmp/dingoo_game/Millipede.app` | ![Millipede](images/Millipede.png) | ✅ Pass | -| Snake | 迪克蛇 | `tmp/dingoo_game/Snake.app` | ![Snake](images/Snake.png) | ✅ Pass | -| Sokuban | 推箱子 | `tmp/dingoo_game/Sokuban/Sokuban.app` | ![Sokuban](images/Sokuban__Sokuban.png) | ✅ Pass | -| Spoout | — | `tmp/dingoo_game/Spoout.app` | ![Spoout](images/Spoout.png) | ✅ Pass | -| StopWatch | 秒表 | `tmp/dingoo_game/StopWatch.app` | ![StopWatch](images/StopWatch.png) | ✅ Pass | -| Tetris | 俄罗斯方块 | `tmp/dingoo_game/Tetris.app` | ![Tetris](images/Tetris.png) | ✅ Pass | -| Ultimate Drift (2008-07-16 build) | 极限漂移(2008-07-16 版本) | `tmp/dingoo_game/Ultimate Drift-20080716163042.app` | ![Ultimate-Drift-2008-07-16-build](images/Ultimate_Drift-20080716163042.png) | ✅ Pass | -| Ultimate Drift (2008-11-17 build) | 极限漂移(2008-11-17 版本) | `tmp/dingoo_game/Ultimate Drift-20081117180631.app` | ![Ultimate-Drift-2008-11-17-build](images/Ultimate_Drift-20081117180631.png) | ✅ Pass | -| Zero Gravity | 零重力 | `tmp/dingoo_game/Zero-Gravity.app` | ![Zero-Gravity](images/Zero-Gravity.png) | ✅ Pass | -| Zhao-Chuan RPG | 赵云传 | `tmp/dingoo_game/Zhao-Chuan RPG.app` | ![Zhao-Chuan-RPG](images/Zhao-Chuan_RPG.png) | ✅ Pass | -| Seven Nights (20081217192316) | 七夜(20081217192316) | `tmp/dingoo_game/7day-20081217192316.app` | ![Seven-Nights-20081217192316](images/7day-20081217192316.png) | ✅ Pass | -| Seven Nights (20090715110443) | 七夜(20090715110443) | `tmp/dingoo_game/7day-20090715110443.app` | ![Seven-Nights-20090715110443](images/7day-20090715110443.png) | ✅ Pass | -| Seven Nights (20090715111247) | 七夜(20090715111247) | `tmp/dingoo_game/7day-20090715111247.app` | ![Seven-Nights-20090715111247](images/7day-20090715111247.png) | ✅ Pass | -| Sword and Fairy (root build) | 仙剑奇侠传(根目录版本) | `tmp/dingoo_game/仙剑奇侠传.app` | ![仙剑奇侠传-root-build](images/仙剑奇侠传.png) | ✅ Pass | -| Sword and Fairy (subdirectory build) | 仙剑奇侠传(子目录版本) | `tmp/dingoo_game/仙剑奇侠传/仙剑奇侠传.APP` | ![仙剑奇侠传-subdirectory-build](images/仙剑奇侠传__仙剑奇侠传.png) | ✅ Pass | - -## Status Legend - -| Symbol | Meaning | -|--------|---------| -| ✅ Pass | Starts successfully and renders a non-black frame. | -| ❌ Fail | Crashes, times out, or does not produce a usable screenshot. | diff --git a/native/DingooEmu/docs/Release-Process.md b/native/DingooEmu/docs/Release-Process.md deleted file mode 100644 index 014a25c..0000000 --- a/native/DingooEmu/docs/Release-Process.md +++ /dev/null @@ -1,138 +0,0 @@ -# Release Process - -This document describes how to publish a new release of DingooEmu. - -## Prerequisites - -- Push access to the `master` branch -- Permission to create tags and releases on GitHub - -## Steps - -### 1. Update version numbers - -Version numbers must be updated in **three files**: - -| File | Field | Current | -|------|-------|---------| -| `Cargo.toml` (workspace root) | `[workspace.package] version` | `"0.1.0"` | -| `crates/dingooemu-libretro/dingooemu_libretro.info` | `display_version` | `"0.1.0"` | -| `crates/dingooemu-libretro/src/libretro/api.rs` | `library_version` | `"0.1.0"` | - -All values must match. The `.info` file is copied directly into the release -artifacts — RetroArch reads `display_version` to display the core version to -users. - -```bash -# Example: bumping to 0.2.0 -# 1. Edit Cargo.toml -sed -i 's/^version = "0.1.0"/version = "0.2.0"/' Cargo.toml - -# 2. Edit .info file -sed -i 's/^display_version = "0.1.0"/display_version = "0.2.0"/' \ - crates/dingooemu-libretro/dingooemu_libretro.info -``` - -### 2. Commit the version bump - -```bash -git add Cargo.toml Cargo.lock crates/dingooemu-libretro/dingooemu_libretro.info -git commit -m "chore: bump version to 0.2.0" -git push origin master -``` - -### 3. Create and push a tag - -The release workflow triggers on tags matching `v*` (e.g. `v0.2.0`). - -```bash -git tag v0.2.0 -git push origin v0.2.0 -``` - -### 4. CI builds and creates a draft release - -Pushing the tag triggers `.github/workflows/release.yml`, which: - -1. **Builds standalone binaries** for Linux (x86_64 + aarch64), macOS (x86_64 + aarch64), and Windows -2. **Builds libretro cores** for the same platforms, renaming the cdylib to - `dingooemu_libretro.` and bundling `dingooemu_libretro.info` -3. **Builds the Android libretro core** for `arm64-v8a`, `armeabi-v7a`, `x86`, - `x86_64`, packaged as `dingoo-emu-android-libretro.tar.gz` -4. **Builds the iOS libretro core** for real devices (arm64 + x86_64 universal) - and simulator (arm64), packaged as `dingoo-emu-ios-libretro.tar.gz` -5. **Builds the webOS libretro core** for ARMv7, packaged as - `dingoo-emu-webos-libretro.tar.gz` -6. **Creates a draft GitHub Release** with: - - Auto-generated release notes (PRs and commits since the previous tag) - - A download table for standalone, libretro, Android, iOS, and webOS artifacts - - All build artifacts attached - -### 5. Review and publish the release - -1. Go to [Releases](https://github.com/AloysHF/DingooEmu/releases) -2. Find the draft release created by CI -3. Review the auto-generated changelog — edit if needed (refer to previous release changelogs for style) -4. Verify all expected artifacts are attached: - - `dingoo-emu-linux-x86_64.tar.gz` - - `dingoo-emu-linux-aarch64.tar.gz` - - `dingoo-emu-macos-x86_64.tar.gz` - - `dingoo-emu-macos-aarch64.tar.gz` - - `dingoo-emu-windows-x86_64.zip` - - `*-libretro.*` (one per platform) - - `dingoo-emu-android-libretro.tar.gz` - - `dingoo-emu-ios-libretro.tar.gz` - - `dingoo-emu-webos-libretro.tar.gz` -5. Click **Publish release** - -### 6. Sync `.info` file to upstream libretro-super - -RetroArch's **Online Updater > Core Downloader** reads the `.info` file from the -upstream [libretro-super](https://github.com/libretro/libretro-super) repository -(`dist/info/dingooemu_libretro.info`), not from this repo. If the `.info` file -was changed in this release (version, metadata, supported extensions, etc.), a -PR must be submitted to sync the changes upstream. - -1. Fork [libretro/libretro-super](https://github.com/libretro/libretro-super) -2. Copy `crates/dingooemu-libretro/dingooemu_libretro.info` from this repo - to `dist/info/dingooemu_libretro.info` in the fork -3. Submit a PR to `libretro/libretro-super` — reference the DingooEmu release - tag and list the changed fields in the PR description - -## Troubleshooting - -### CI build fails - -- Check the [Actions](https://github.com/AloysHF/DingooEmu/actions) tab - for the failed run -- The most common failure is a missing Linux build dependency — the CI installs - `libasound2-dev`, `libx11-dev`, and `libxkbcommon-dev` automatically - -### Re-triggering a release - -The release workflow only runs on tag pushes. To re-trigger: - -```bash -# 1. Delete the tag locally and remotely -git tag -d v0.2.0 -git push origin --delete v0.2.0 - -# 2. Re-push the tag (CI will re-run) -git push origin v0.2.0 -``` - -If a draft release was already created by the failed run, delete it from the -[Releases](https://github.com/AloysHF/DingooEmu/releases) page before -re-pushing the tag, otherwise the new run may conflict with the existing draft. - -### Release artifacts missing - -- Verify the tag name starts with `v` (e.g. `v0.2.0`, not `0.2.0`) -- The release workflow only triggers on tag pushes (`v*`); manually dispatching - the workflow from the Actions tab will not create a release - -### Version mismatch in RetroArch - -- Ensure `Cargo.toml`, `dingooemu_libretro.info`, and `api.rs` all have the - same version string -- The `.info` file is bundled as-is into the release artifacts diff --git a/native/DingooEmu/docs/RetroArch-Core.md b/native/DingooEmu/docs/RetroArch-Core.md deleted file mode 100644 index a500724..0000000 --- a/native/DingooEmu/docs/RetroArch-Core.md +++ /dev/null @@ -1,150 +0,0 @@ -# RetroArch Core - -DingooEmu is available as a libretro core for RetroArch on Windows, Linux, -macOS, Android, and iOS. This guide covers installation, loading content, -supported frontend features, and controls. - -## Installation - -### Online Updater - - - -1. Open RetroArch. -2. Go to **Main Menu > Online Updater > Core Downloader**. -3. Select **Dingoo A320 (DingooEmu)**. - -### Manual Installation - -Download the core from the -[Releases](https://github.com/AloysHF/DingooEmu/releases) page and extract -the archive for your operating system and CPU architecture. Copy the core file -to RetroArch's `cores/` directory, and copy `dingooemu_libretro.info` to its -`info/` directory. - -| Platform | Core file | -|---|---| -| Windows | `dingooemu_libretro.dll` | -| Linux | `dingooemu_libretro.so` | -| macOS | `dingooemu_libretro.dylib` | - -### Building from Source - -```bash -cargo build -p dingooemu-libretro --release -``` - -Cargo names the cdylib after its lib target, producing -`dingooemu_libretro.dll` on Windows, `libdingooemu_libretro.so` on Linux, or -`libdingooemu_libretro.dylib` on macOS under `target/release/`. Rename the -Linux or macOS output to remove the leading `lib` before copying it into -RetroArch's `cores/` directory. - -## Supported Platforms - -| Platform | Architectures | Distribution | -|---|---|---| -| Windows | x86_64 | Core Downloader or release archive | -| Linux | x86_64, aarch64 | Core Downloader or release archive | -| macOS | x86_64, Apple silicon | Core Downloader or release archive | -| Android | arm64-v8a, armeabi-v7a, x86, x86_64 | See the Android guide | -| iOS | arm64 devices, Apple silicon simulator | See the iOS guide | - -## Mobile Platforms - -The same libretro core architecture is available on mobile platforms, with -platform-specific installation requirements: - -- [Android Libretro Core](Android-Libretro-Core.md) -- [iOS Libretro Core](iOS-Libretro-Core.md) - -## Loading Games - -1. Open RetroArch and select **Load Core > Dingoo A320 (DingooEmu)**. -2. Select **Load Content**. -3. Choose a `.app` file. - -## Supported Features - -- Video output using the native RGB565 pixel format -- PCM audio output resampled to 22050 Hz stereo -- RetroPad input handling -- `.app` content loading -- Cold reset through RetroArch's **Reset** command -- Persistent guest save files in RetroArch's configured save directory -- Save states with content identity and corruption checks -- Frontend cheat slots for 8/16/32-bit memory and MIPS registers -- Frontend memory access for 32 MiB system RAM and LCD video RAM -- Live core options, including host master volume - -The current basic core does not yet provide subsystem loading. The metadata -marks it unavailable so RetroArch does not present unsupported capabilities. - -## Game Save Files - -Files created through the emulated file API are stored beneath RetroArch's -configured save directory and reopened from there on later sessions. Guest -paths are normalized inside that directory; parent-directory traversal is -rejected. Modified files are flushed when the guest closes them and when the -core resets or unloads content. - -## Save States - -RetroArch save and load state commands capture the complete mutable CPU, -memory, video, input, audio, scheduler, semaphore, and open-file state. Each -state also preserves active file enumeration and focused-window input dispatch, -and contains a format version, content checksum, payload length, and payload -checksum. States for different content and damaged or incompatible states are -rejected without changing the running emulator. - -## Cheats - -RetroArch cheat slots accept `TARGET=VALUE` rules. Supported targets are -`mem8:ADDRESS`, `mem16:ADDRESS`, `mem32:ADDRESS`, and `reg:rN` for MIPS -registers `r0` through `r31`. Numbers may be decimal or use a `0x` hexadecimal -prefix. Enabled slots are applied at the start of every emulated frame; -disabled slots remain configured but do not modify state. - -## Memory Access - -Compatible frontend tools can access the complete 32 MiB system RAM and the -LCD framebuffer mapping through the standard libretro memory API. The core -also registers both regions as memory-map descriptors, including the guest -framebuffer address. Region pointers remain stable across Reset and save-state -loads while content remains loaded. - -## Core Options - -| Option | Values | Default | Behavior | -|---|---|---|---| -| Audio Volume (%) | `100` to `0` in steps of 10 | `100` | Applies a host master gain without replacing the game's own volume. | -| Key Auto-Repeat Delay | frame counts including `0` | `24` | Sets how long a held button waits before repeating; `0` disables repeat. | -| Key Auto-Repeat Period | `1`–`30` frame choices | `6` | Sets the interval between repeat press events. | -| Swap A/B Buttons | `disabled`, `enabled` | `disabled` | Exchanges the emulated A and B button meanings. | -| CPU/HLE Debug Logging | `disabled`, `enabled` | `disabled` | Enables detailed interpreter and HLE records in the frontend log. | -| Unknown MIPS Instruction Policy | `skip`, `stop` | `skip` | Logs and skips unsupported instructions or stops with an execution error. | -| CPU Execution Engine (64-bit Android) | `jit`, `interpreter` | `jit` | Uses native translation for hot CPU blocks on arm64-v8a and x86_64 Android. Other targets and unsupported instructions use the interpreter. Select `interpreter` for compatibility testing. | - -Core option changes are applied while content is running and restored after a -RetroArch reset. - -The JIT waits until a block has executed 256 times and rate-limits native -compilation to one block every four frames. Short blocks and repeatedly -unsupported memory paths remain on the interpreter to avoid runtime stutter. - -## RetroPad Button Mapping - -| RetroPad Button | Dingoo Button | -|---|---| -| D-Pad Left | Left | -| D-Pad Right | Right | -| D-Pad Up | Up | -| D-Pad Down | Down | -| A (SNES East) | A | -| B (SNES South) | B | -| X (SNES North) | X | -| Y (SNES West) | Y | -| Start | Start | -| Select | Select | -| L1 | L shoulder | -| R1 | R shoulder | diff --git a/native/DingooEmu/docs/Standalone-Emulator.md b/native/DingooEmu/docs/Standalone-Emulator.md deleted file mode 100644 index a2e8932..0000000 --- a/native/DingooEmu/docs/Standalone-Emulator.md +++ /dev/null @@ -1,238 +0,0 @@ -# Standalone Emulator - -This guide covers installing and running the standalone `dingooemu` binary, -loading `.app` game files, keyboard controls, screenshot mode, and every -command-line option. - -## Installation - -Download the latest standalone binary for your platform from the -[Releases](https://github.com/AloysHF/DingooEmu/releases) page. - -You can also build it from source: - -```bash -cargo build -p dingooemu --release -``` - -The binary is produced at `target/release/dingooemu` (`.exe` on Windows). - -## Loading Games - -The standalone emulator accepts `.app` files from the Dingoo A320 ecosystem. - -```bash -dingooemu path/to/game.app -``` - -You can always print the built-in help with: - -```bash -dingooemu --help -``` - -## Synopsis - -```text -dingooemu [OPTIONS] -``` - -## Options - -| Option | Value | Default | Description | -|---|---|---|---| -| `` | path | *required* | Path to the `.app` game file. | -| `-s, --scale ` | `1`–`16` | `2` | Validated integer scaling factor for the window. | -| `-f, --fullscreen` | flag | off | Open a borderless window at the desktop resolution. | -| `-v, --volume ` | `0`–`100` | `100` | Set the host master volume; `0` mutes output. | -| `--debug-logging` | flag | off | Enable debug-level emulator logging unless `RUST_LOG` overrides it. | -| `--remap ` | mapping | — | Replace a button's default keyboard mapping; may be repeated. | -| `--swap-ab` | flag | off | Exchange the emulated A and B button masks. | -| `--filter ` | `nearest`, `bilinear`, `bicubic`, `xbrz` | `nearest` | Select the window scaling filter. | -| `--show-gamepad` | flag | off | Overlay the current logical Dingoo button state. | -| `--repeat-delay ` | frames | `24` | Frames before a held button begins generating repeat presses. | -| `--repeat-period ` | frames, at least `1` | `6` | Frames between repeat presses after the delay. | -| `--cheat ` | rule | — | Freeze a memory location or MIPS register once per frame; may be repeated. | -| `--unknown-instruction-policy ` | `stop`, `skip` | `skip` | Stop on or log and skip an unimplemented MIPS instruction. | -| `--unknown-hle-policy ` | `report`, `stop` | `report` | Aggregate unknown SDK calls and continue with zero, or stop at the first non-allowlisted call. | -| `--allow-unknown-hle ` | exact function name | — | Preserve compatibility-stub behavior for one function in strict HLE mode; may be repeated. | -| `--hle-report ` | path | — | Write stable JSON diagnostics with an `unknown_hle` list, including counts and first-call context. | -| `--headless` | flag | off | Run in headless mode (no window). Runs for 300 frames and exits. | -| `--frames ` | integer | `300` | Number of frames to run in headless mode. | -| `-S, --screenshot ` | path | — | Render some frames, save a PNG screenshot, then exit. | -| `--screenshot-frames ` | integer | `30` | Number of frames to run before the screenshot is taken. | - -`--screenshot-frames` only has an effect together with `--screenshot`. - -## Default Key Mappings - -| Key | Dingoo Button | -|-----|---------------| -| Arrow keys | D-pad | -| X | A | -| Z | B | -| S | X | -| A | Y | -| Enter | START | -| Right Shift | SELECT | -| Q | L shoulder | -| W | R shoulder | -| Esc | Exit | - -These defaults match RetroArch's standard keyboard bindings for the equivalent -RetroPad buttons, so each physical key has the same in-game meaning in both -frontends. - -Button names accepted by `--remap` are `up`, `down`, `left`, `right`, `a`, -`b`, `x`, `y`, `start`, `select`, `l`, and `r`. For example: - -```bash -dingooemu --remap a:space --remap select:tab path/to/game.app -``` - -`escape` is reserved for exiting and cannot be assigned. - -Use `--swap-ab` to keep the physical keys while exchanging their in-game A/B -meaning. It is applied after custom key remapping. - -## Display Filters - -`nearest` preserves hard pixel edges, `bilinear` smooths adjacent pixels, -`bicubic` uses sharper cubic interpolation, and `xbrz` applies edge-aware -pixel-art smoothing. All modes preserve the native 4:3 aspect ratio and add -black bars when the window or desktop has a different aspect ratio. - -Use `--show-gamepad` when testing mappings or recording demonstrations. The -overlay is drawn at the native resolution before the selected display filter. - -At 60 Hz, the default repeat delay is about 0.4 seconds and the default repeat -period is about 0.1 seconds. Set `--repeat-delay 0` to disable synthetic repeat. - -## Cheat Rules - -Cheat rules are applied before every emulated frame. Supported targets are -`mem8`, `mem16`, `mem32`, and MIPS registers `r0` through `r31`. Decimal and -`0x`-prefixed hexadecimal values are accepted: - -```bash -dingooemu --cheat mem8:0x80100000=99 --cheat reg:r4=0x1234 game.app -``` - -Writes still pass through the emulator's normal memory validation. Invalid or -out-of-range rules stop startup with a clear error instead of being ignored. - -For compatibility testing, `--unknown-instruction-policy stop` turns the first -unimplemented MIPS instruction into an execution error. The default `skip` -behavior logs the instruction and continues, matching previous releases. - -Unknown SDK calls are always aggregated by exact function name. The default -`--unknown-hle-policy report` returns zero for compatibility and logs only the -first occurrence plus an end-of-run summary. Each entry records the total call -count, import address, first guest call site, and the first `a0` through `a3` -arguments. `--unknown-hle-policy stop` turns the first unknown call into an -execution error after recording it. Use `--allow-unknown-hle NAME` only for a -reviewed compatibility exception; matching is case-sensitive and exact. - -The JSON report is written even when strict mode stops emulation: - -```bash -dingooemu game.app --headless --frames 300 --unknown-hle-policy stop \ - --hle-report hle-report.json -``` - -## Audio Output - -The standalone emulator sends Dingoo PCM audio to the default system output -device. It supports unsigned 8-bit and signed 16-bit little-endian PCM, mono -and stereo streams, guest volume controls, and automatic device resampling. -If the default output device cannot be opened, emulation continues without -audio and logs a warning. When the output queue is full, the guest audio task -waits and retries the same buffer so playback data is not skipped. - -## Game Save Files - -Files created through the emulated file API persist beside the loaded `.app` -file. Guest subdirectories are supported, while parent-directory traversal is -rejected. Modified files are flushed when the guest closes them and when the -emulator stops or resets. - -## Performance - -Use a release build for normal gameplay. The interpreter caches sequential -guest instruction blocks, uses multi-cycle CPU steps, treats guest frame -submissions as frontend frame boundaries, and advances any remaining clock -budget after a completed frame. This keeps timers, task delays, video, and -audio synchronized at 60 Hz without requiring the host to dispatch all 336 -million hardware clock cycles individually. - -## Screenshot Mode - -Take a headless screenshot for automated testing or preview generation: - -```bash -# Take screenshot after 30 frames (default) and save as PNG -dingooemu path/to/game.app --screenshot preview.png - -# Take screenshot after a custom number of frames -dingooemu path/to/game.app --screenshot preview.png --screenshot-frames 60 -``` - -## Batch Screenshot Mode - -Build the standalone emulator and capture every `.app` file under -`tmp/dingoo_game` recursively: - -```powershell -pwsh -NoProfile -File scripts/batch-screenshots.ps1 -``` - -Screenshots are written to `docs/images`, and per-game JSON diagnostics are -written to `tmp/hle-reports`. Every report contains an `unknown_hle` array, -including an empty array when no gaps were observed. The default capture point -is 60 frames, with per-game overrides for slow-starting or -performance-sensitive titles. Explicit parameters apply the requested values -to every game: - -```powershell -pwsh -NoProfile -File scripts/batch-screenshots.ps1 -Frames 60 -TimeoutSeconds 30 -``` - -Run the same set in strict mode, optionally with reviewed exceptions: - -```powershell -pwsh -NoProfile -File scripts/batch-screenshots.ps1 ` - -UnknownHlePolicy stop ` - -AllowUnknownHle legacy_function ` - -ReportDirectory tmp/hle-reports-strict -``` - -## Examples - -```bash -# Basic usage -dingooemu path/to/game.app - -# 2x scaling (default) -dingooemu --scale 2 path/to/game.app - -# 4x scaling -dingooemu --scale 4 path/to/game.app - -# Borderless fullscreen mode -dingooemu --fullscreen path/to/game.app - -# Run at 35% master volume -dingooemu --volume 35 path/to/game.app - -# Enable detailed emulator diagnostics -dingooemu --debug-logging path/to/game.app - -# Take screenshot after 60 frames -dingooemu --screenshot screenshot.png --screenshot-frames 60 game.app - -# Headless mode (no window, runs for 300 frames) -dingooemu --headless path/to/game.app - -# Run exactly 120 frames without opening a window -dingooemu --headless --frames 120 path/to/game.app -``` diff --git a/native/DingooEmu/docs/iOS-Libretro-Core.md b/native/DingooEmu/docs/iOS-Libretro-Core.md deleted file mode 100644 index e628daa..0000000 --- a/native/DingooEmu/docs/iOS-Libretro-Core.md +++ /dev/null @@ -1,112 +0,0 @@ -# iOS Libretro Core - -DingooEmu runs as a libretro core on RetroArch, allowing you to play Dingoo A320 games on iPhone / iPad. - -> **Note**: iOS does not currently support downloading cores via RetroArch's Online Updater. You need to manually inject the core into the RetroArch IPA. This limitation may be resolved in future RetroArch releases. - -## Install in RetroArch on iOS - -### Prerequisites - -- iPhone or iPad (arm64, iOS 15+) -- RetroArch 1.17.0 IPA ([official download](https://buildbot.libretro.com/stable/1.17.0/apple/ios-arm64/RetroArch.ipa)) - - Version 1.17.0 is recommended; newer versions have a different folder structure that makes manual injection more complex -- Download `dingoo-emu-ios-libretro.tar.gz` from the [Releases](https://github.com/AloysHF/DingooEmu/releases) page. It contains: - - `dingooemu_libretro_ios.dylib` — core binary (real devices: arm64 + x86_64 universal) - - `dingooemu_libretro.info` — core metadata -- A file manager and IPA signing app (e.g. ESign, SideStore, or AltStore) - -### Step 1: Rename IPA to ZIP and extract - -1. Rename the downloaded `RetroArch.ipa` to `RetroArch.zip` -2. Extract the ZIP file to get the `Payload/` folder -3. Navigate into `Payload/RetroArch.app/` - -### Step 2: Inject the core binary - -Copy `dingooemu_libretro_ios.dylib` into: `Payload/RetroArch.app/modules/` - -### Step 3: Inject the core metadata - -1. Locate `assets.zip` inside `RetroArch.app/` -2. Extract `assets.zip` and navigate into the extracted `assets/` directory -3. Copy `dingooemu_libretro.info` into `assets/info/` -4. Recompress the entire `assets/` directory, making sure the output is named `assets.zip` -5. Move the new `assets.zip` back into `RetroArch.app/`, replacing the original - -### Step 4: Re-sign the IPA - -This is the most critical step on iOS. The modified IPA must be re-signed before it can be installed. - -**Using ESign on an iOS device:** - -1. Install [ESign](https://www.e-sign.cn/) (or another signing tool) -2. Repackage the modified folder into an `.ipa` file -3. Import the IPA into ESign -4. Select "Sign" → choose your certificate (personal or developer) -5. Install after signing completes - -**Using SideStore or AltStore on Mac/PC:** - -1. Install SideStore or AltStore on your iOS device -2. Install the modified IPA through SideStore/AltStore - -### Step 5: Run Online Updater and restart - -1. Open RetroArch -2. Go to **Main Menu → Online Updater** -3. Run the following updates: - - Update Core Info Files - - Update Assets - - Update Controller Profiles - - Update Databases - - Update Overlays -4. Restart RetroArch - -The Dingoo A320 core should now appear in the core list automatically. - -### Troubleshooting - -#### Core does not appear in RetroArch - -- Make sure you are using RetroArch **1.17.0** (newer versions have a different folder structure) -- Verify `dingooemu_libretro_ios.dylib` is in the `modules/` directory -- Verify `dingooemu_libretro.info` is in `assets/info/` (inside `assets.zip`) -- Ensure the IPA was correctly re-signed - -#### IPA installation fails - -- iOS does not allow installing unsigned IPA files directly -- You must re-sign using ESign, SideStore, or AltStore - -## Building the iOS core locally - -Building for iOS requires [Rust](https://www.rust-lang.org/tools/install) (stable) on macOS, with iOS targets added: - -```bash -rustup target add aarch64-apple-ios x86_64-apple-ios aarch64-apple-ios-sim -``` - -Build for real devices (arm64 + x86_64 universal library): - -```bash -# Build for arm64 (real devices) -cargo build -p dingooemu-libretro --release --target aarch64-apple-ios - -# Build for x86_64 (real devices) -cargo build -p dingooemu-libretro --release --target x86_64-apple-ios - -# Create universal library -lipo -create \ - target/aarch64-apple-ios/release/libdingooemu_libretro.dylib \ - target/x86_64-apple-ios/release/libdingooemu_libretro.dylib \ - -output dingooemu_libretro_ios.dylib -``` - -Build for simulator (Apple Silicon Mac): - -```bash -cargo build -p dingooemu-libretro --release --target aarch64-apple-ios-sim -``` - -> Cargo names the cdylib `libdingooemu_libretro.dylib`; the CI release workflow packages it as `dingooemu_libretro_ios.dylib` along with `dingooemu_libretro.info`. diff --git a/native/DingooEmu/docs/images/7day-20081217192316.png b/native/DingooEmu/docs/images/7day-20081217192316.png deleted file mode 100644 index 3b12320..0000000 Binary files a/native/DingooEmu/docs/images/7day-20081217192316.png and /dev/null differ diff --git a/native/DingooEmu/docs/images/7day-20090715110443.png b/native/DingooEmu/docs/images/7day-20090715110443.png deleted file mode 100644 index 3b12320..0000000 Binary files a/native/DingooEmu/docs/images/7day-20090715110443.png and /dev/null differ diff --git a/native/DingooEmu/docs/images/7day-20090715111247.png b/native/DingooEmu/docs/images/7day-20090715111247.png deleted file mode 100644 index a115f91..0000000 Binary files a/native/DingooEmu/docs/images/7day-20090715111247.png and /dev/null differ diff --git a/native/DingooEmu/docs/images/AliBaba.png b/native/DingooEmu/docs/images/AliBaba.png deleted file mode 100644 index 6b803e3..0000000 Binary files a/native/DingooEmu/docs/images/AliBaba.png and /dev/null differ diff --git a/native/DingooEmu/docs/images/Astro-Lander__Astro-Lander.png b/native/DingooEmu/docs/images/Astro-Lander__Astro-Lander.png deleted file mode 100644 index 98bd4c2..0000000 Binary files a/native/DingooEmu/docs/images/Astro-Lander__Astro-Lander.png and /dev/null differ diff --git a/native/DingooEmu/docs/images/Block_Breaker.png b/native/DingooEmu/docs/images/Block_Breaker.png deleted file mode 100644 index 12b87b2..0000000 Binary files a/native/DingooEmu/docs/images/Block_Breaker.png and /dev/null differ diff --git a/native/DingooEmu/docs/images/Candy.png b/native/DingooEmu/docs/images/Candy.png deleted file mode 100644 index 0683f23..0000000 Binary files a/native/DingooEmu/docs/images/Candy.png and /dev/null differ diff --git a/native/DingooEmu/docs/images/Decollation-Warrior.png b/native/DingooEmu/docs/images/Decollation-Warrior.png deleted file mode 100644 index 5ccfac0..0000000 Binary files a/native/DingooEmu/docs/images/Decollation-Warrior.png and /dev/null differ diff --git a/native/DingooEmu/docs/images/Fomula-One.png b/native/DingooEmu/docs/images/Fomula-One.png deleted file mode 100644 index c871e41..0000000 Binary files a/native/DingooEmu/docs/images/Fomula-One.png and /dev/null differ diff --git a/native/DingooEmu/docs/images/GooPlayer__GooPlayer.png b/native/DingooEmu/docs/images/GooPlayer__GooPlayer.png deleted file mode 100644 index 99d619b..0000000 Binary files a/native/DingooEmu/docs/images/GooPlayer__GooPlayer.png and /dev/null differ diff --git a/native/DingooEmu/docs/images/Hell_Striker_II-20081229173817.png b/native/DingooEmu/docs/images/Hell_Striker_II-20081229173817.png deleted file mode 100644 index 0f55513..0000000 Binary files a/native/DingooEmu/docs/images/Hell_Striker_II-20081229173817.png and /dev/null differ diff --git a/native/DingooEmu/docs/images/Hell_Striker_II-20090122224048.png b/native/DingooEmu/docs/images/Hell_Striker_II-20090122224048.png deleted file mode 100644 index 606d06d..0000000 Binary files a/native/DingooEmu/docs/images/Hell_Striker_II-20090122224048.png and /dev/null differ diff --git a/native/DingooEmu/docs/images/Hexa-Virus.png b/native/DingooEmu/docs/images/Hexa-Virus.png deleted file mode 100644 index 35063ed..0000000 Binary files a/native/DingooEmu/docs/images/Hexa-Virus.png and /dev/null differ diff --git a/native/DingooEmu/docs/images/Landlord.png b/native/DingooEmu/docs/images/Landlord.png deleted file mode 100644 index 26efb8a..0000000 Binary files a/native/DingooEmu/docs/images/Landlord.png and /dev/null differ diff --git a/native/DingooEmu/docs/images/Link'em_Up.png b/native/DingooEmu/docs/images/Link'em_Up.png deleted file mode 100644 index 32fb4dd..0000000 Binary files a/native/DingooEmu/docs/images/Link'em_Up.png and /dev/null differ diff --git a/native/DingooEmu/docs/images/Manic-Miner.png b/native/DingooEmu/docs/images/Manic-Miner.png deleted file mode 100644 index 7270255..0000000 Binary files a/native/DingooEmu/docs/images/Manic-Miner.png and /dev/null differ diff --git a/native/DingooEmu/docs/images/Millipede.png b/native/DingooEmu/docs/images/Millipede.png deleted file mode 100644 index aafaa30..0000000 Binary files a/native/DingooEmu/docs/images/Millipede.png and /dev/null differ diff --git a/native/DingooEmu/docs/images/Mine_Sweeper.png b/native/DingooEmu/docs/images/Mine_Sweeper.png deleted file mode 100644 index a1684ef..0000000 Binary files a/native/DingooEmu/docs/images/Mine_Sweeper.png and /dev/null differ diff --git a/native/DingooEmu/docs/images/Mushroom_Roulette.png b/native/DingooEmu/docs/images/Mushroom_Roulette.png deleted file mode 100644 index 46abc20..0000000 Binary files a/native/DingooEmu/docs/images/Mushroom_Roulette.png and /dev/null differ diff --git a/native/DingooEmu/docs/images/Nose_Breaker.png b/native/DingooEmu/docs/images/Nose_Breaker.png deleted file mode 100644 index 8352035..0000000 Binary files a/native/DingooEmu/docs/images/Nose_Breaker.png and /dev/null differ diff --git a/native/DingooEmu/docs/images/Overlord-Fighter-Stub.png b/native/DingooEmu/docs/images/Overlord-Fighter-Stub.png deleted file mode 100644 index 07106f0..0000000 Binary files a/native/DingooEmu/docs/images/Overlord-Fighter-Stub.png and /dev/null differ diff --git a/native/DingooEmu/docs/images/Overlord-Fighter.png b/native/DingooEmu/docs/images/Overlord-Fighter.png deleted file mode 100644 index 07106f0..0000000 Binary files a/native/DingooEmu/docs/images/Overlord-Fighter.png and /dev/null differ diff --git a/native/DingooEmu/docs/images/Platinum_Sudoku.png b/native/DingooEmu/docs/images/Platinum_Sudoku.png deleted file mode 100644 index 8819703..0000000 Binary files a/native/DingooEmu/docs/images/Platinum_Sudoku.png and /dev/null differ diff --git a/native/DingooEmu/docs/images/PoPo_Bash.png b/native/DingooEmu/docs/images/PoPo_Bash.png deleted file mode 100644 index 0fff95b..0000000 Binary files a/native/DingooEmu/docs/images/PoPo_Bash.png and /dev/null differ diff --git a/native/DingooEmu/docs/images/Rick-Dangerous.png b/native/DingooEmu/docs/images/Rick-Dangerous.png deleted file mode 100644 index d0a116c..0000000 Binary files a/native/DingooEmu/docs/images/Rick-Dangerous.png and /dev/null differ diff --git a/native/DingooEmu/docs/images/Rubido-20090512001427.png b/native/DingooEmu/docs/images/Rubido-20090512001427.png deleted file mode 100644 index 52997c6..0000000 Binary files a/native/DingooEmu/docs/images/Rubido-20090512001427.png and /dev/null differ diff --git a/native/DingooEmu/docs/images/Rubido-20090516230856.png b/native/DingooEmu/docs/images/Rubido-20090516230856.png deleted file mode 100644 index fc8f8bc..0000000 Binary files a/native/DingooEmu/docs/images/Rubido-20090516230856.png and /dev/null differ diff --git a/native/DingooEmu/docs/images/SameGoo__samegoo.png b/native/DingooEmu/docs/images/SameGoo__samegoo.png deleted file mode 100644 index 3d79fae..0000000 Binary files a/native/DingooEmu/docs/images/SameGoo__samegoo.png and /dev/null differ diff --git a/native/DingooEmu/docs/images/Snake.png b/native/DingooEmu/docs/images/Snake.png deleted file mode 100644 index 0e76470..0000000 Binary files a/native/DingooEmu/docs/images/Snake.png and /dev/null differ diff --git a/native/DingooEmu/docs/images/Sokuban__Sokuban.png b/native/DingooEmu/docs/images/Sokuban__Sokuban.png deleted file mode 100644 index 2852ea6..0000000 Binary files a/native/DingooEmu/docs/images/Sokuban__Sokuban.png and /dev/null differ diff --git a/native/DingooEmu/docs/images/Spoout.png b/native/DingooEmu/docs/images/Spoout.png deleted file mode 100644 index dab7f8b..0000000 Binary files a/native/DingooEmu/docs/images/Spoout.png and /dev/null differ diff --git a/native/DingooEmu/docs/images/StopWatch.png b/native/DingooEmu/docs/images/StopWatch.png deleted file mode 100644 index 2bdb9d9..0000000 Binary files a/native/DingooEmu/docs/images/StopWatch.png and /dev/null differ diff --git a/native/DingooEmu/docs/images/Tetris.png b/native/DingooEmu/docs/images/Tetris.png deleted file mode 100644 index 0a4c821..0000000 Binary files a/native/DingooEmu/docs/images/Tetris.png and /dev/null differ diff --git a/native/DingooEmu/docs/images/Ultimate_Drift-20080716163042.png b/native/DingooEmu/docs/images/Ultimate_Drift-20080716163042.png deleted file mode 100644 index 4eb452e..0000000 Binary files a/native/DingooEmu/docs/images/Ultimate_Drift-20080716163042.png and /dev/null differ diff --git a/native/DingooEmu/docs/images/Ultimate_Drift-20081117180631.png b/native/DingooEmu/docs/images/Ultimate_Drift-20081117180631.png deleted file mode 100644 index f595ecc..0000000 Binary files a/native/DingooEmu/docs/images/Ultimate_Drift-20081117180631.png and /dev/null differ diff --git a/native/DingooEmu/docs/images/Zero-Gravity.png b/native/DingooEmu/docs/images/Zero-Gravity.png deleted file mode 100644 index 4d5d5d7..0000000 Binary files a/native/DingooEmu/docs/images/Zero-Gravity.png and /dev/null differ diff --git a/native/DingooEmu/docs/images/Zhao-Chuan_RPG.png b/native/DingooEmu/docs/images/Zhao-Chuan_RPG.png deleted file mode 100644 index 11fd3b5..0000000 Binary files a/native/DingooEmu/docs/images/Zhao-Chuan_RPG.png and /dev/null differ diff --git "a/native/DingooEmu/docs/images/\344\273\231\345\211\221\345\245\207\344\276\240\344\274\240.png" "b/native/DingooEmu/docs/images/\344\273\231\345\211\221\345\245\207\344\276\240\344\274\240.png" deleted file mode 100644 index 89ab82c..0000000 Binary files "a/native/DingooEmu/docs/images/\344\273\231\345\211\221\345\245\207\344\276\240\344\274\240.png" and /dev/null differ diff --git "a/native/DingooEmu/docs/images/\344\273\231\345\211\221\345\245\207\344\276\240\344\274\240__\344\273\231\345\211\221\345\245\207\344\276\240\344\274\240.png" "b/native/DingooEmu/docs/images/\344\273\231\345\211\221\345\245\207\344\276\240\344\274\240__\344\273\231\345\211\221\345\245\207\344\276\240\344\274\240.png" deleted file mode 100644 index 9a29674..0000000 Binary files "a/native/DingooEmu/docs/images/\344\273\231\345\211\221\345\245\207\344\276\240\344\274\240__\344\273\231\345\211\221\345\245\207\344\276\240\344\274\240.png" and /dev/null differ diff --git a/native/DingooEmu/res/logo-banner.png b/native/DingooEmu/res/logo-banner.png deleted file mode 100644 index c81bc70..0000000 Binary files a/native/DingooEmu/res/logo-banner.png and /dev/null differ diff --git a/native/DingooEmu/res/logo.png b/native/DingooEmu/res/logo.png deleted file mode 100644 index d0b57df..0000000 Binary files a/native/DingooEmu/res/logo.png and /dev/null differ diff --git a/native/DingooEmu/scripts/batch-screenshots.ps1 b/native/DingooEmu/scripts/batch-screenshots.ps1 deleted file mode 100644 index f39155e..0000000 --- a/native/DingooEmu/scripts/batch-screenshots.ps1 +++ /dev/null @@ -1,313 +0,0 @@ -<# -.SYNOPSIS - Batch-generate screenshots for all .app games in tmp/dingoo_game. - -.DESCRIPTION - Runs the dingoo-emu binary in screenshot mode for every .app file found - under tmp/dingoo_game recursively. Output PNGs are saved to docs/images - and named after each game file. When no binary is supplied, the latest - release binary is built before capture. - -.PARAMETER Frames - Number of frames to emulate before capturing. Default: 60 (one second at - 60 fps). Known slow-starting games use per-game overrides. - -.PARAMETER Binary - Path to the dingoo-emu binary. Default: the Cargo release build output. - -.PARAMETER TimeoutSeconds - Maximum time allowed for each game. Default: 120 seconds. - -.PARAMETER ReportDirectory - Directory for per-game unknown HLE JSON reports. Default: tmp/hle-reports. - -.PARAMETER UnknownHlePolicy - Unknown SDK HLE behavior. Use report for compatibility runs or stop for - strict validation. Default: report. - -.PARAMETER AllowUnknownHle - Exact unknown SDK function names allowed in strict mode. -#> - -param( - [ValidateRange(1, [int]::MaxValue)] - [int]$Frames = 60, - - [string]$Binary = "", - - [ValidateRange(1, [int]::MaxValue)] - [int]$TimeoutSeconds = 120, - - [string]$ReportDirectory = "", - - [ValidateSet("report", "stop")] - [string]$UnknownHlePolicy = "report", - - [string[]]$AllowUnknownHle = @() -) - -$ErrorActionPreference = "Stop" -[Console]::OutputEncoding = [System.Text.UTF8Encoding]::new($false) -$framesSpecified = $PSBoundParameters.ContainsKey("Frames") - -$repoRoot = Split-Path -Parent $PSScriptRoot -$gameDir = Join-Path $repoRoot "tmp\dingoo_game" -$outDir = Join-Path $repoRoot "docs\images" -if (-not $ReportDirectory) { - $ReportDirectory = Join-Path $repoRoot "tmp\hle-reports" -} - -if (-not (Test-Path -LiteralPath $gameDir -PathType Container)) { - Write-Error "Game directory not found: $gameDir" - exit 1 -} - -New-Item -ItemType Directory -Force -Path $outDir | Out-Null -New-Item -ItemType Directory -Force -Path $ReportDirectory | Out-Null -$ReportDirectory = (Resolve-Path -LiteralPath $ReportDirectory).Path - -if (-not $Binary) { - $Binary = Join-Path $repoRoot "target\release\dingoo-emu.exe" - Write-Host "Building the latest release binary..." -ForegroundColor Yellow - try { - Push-Location $repoRoot - cargo build --release -p dingooemu - if ($LASTEXITCODE -ne 0) { - throw "Release build failed with exit code $LASTEXITCODE." - } - } finally { - Pop-Location - } -} - -if (-not (Test-Path -LiteralPath $Binary -PathType Leaf)) { - Write-Error "Emulator binary not found: $Binary" - exit 1 -} - -$Binary = (Resolve-Path -LiteralPath $Binary).Path - -function ConvertTo-ScreenshotName { - param([string]$BaseName) - - $safeName = $BaseName -replace '\s+', '_' - $safeName = $safeName -replace '[<>:"/\\|?*\x00-\x1F]', '_' - $safeName = $safeName.TrimEnd([char[]]@('.', ' ')) - - if ([string]::IsNullOrWhiteSpace($safeName)) { - return "game" - } - - return $safeName -} - -function Get-CaptureFrames { - param( - [string]$RelativePath, - [int]$DefaultFrames, - [bool]$UsePerformanceOverrides - ) - - if (-not $UsePerformanceOverrides) { - return $DefaultFrames - } - - switch ($RelativePath) { - "7day-20081217192316.app" { return 30 } - "仙剑奇侠传\仙剑奇侠传.APP" { return 1200 } - "Decollation-Warrior.app" { return 30 } - "GooPlayer\GooPlayer.app" { return 300 } - "Hell Striker II-20090122224048.app" { return 300 } - "Overlord-Fighter.app" { return 120 } - "SameGoo\samegoo.app" { return 300 } - "Snake.app" { return 30 } - default { return $DefaultFrames } - } -} - -function Invoke-ScreenshotCapture { - param( - [string]$Executable, - [string]$GamePath, - [string]$ScreenshotPath, - [string]$ReportPath, - [int]$CaptureFrames, - [int]$Timeout, - [string]$HlePolicy, - [string[]]$AllowedUnknownHle - ) - - $startInfo = [System.Diagnostics.ProcessStartInfo]::new() - $startInfo.FileName = $Executable - $startInfo.UseShellExecute = $false - $startInfo.CreateNoWindow = $true - $startInfo.RedirectStandardOutput = $true - $startInfo.RedirectStandardError = $true - $startInfo.ArgumentList.Add($GamePath) - $startInfo.ArgumentList.Add("--screenshot") - $startInfo.ArgumentList.Add($ScreenshotPath) - $startInfo.ArgumentList.Add("--screenshot-frames") - $startInfo.ArgumentList.Add($CaptureFrames.ToString()) - $startInfo.ArgumentList.Add("--unknown-hle-policy") - $startInfo.ArgumentList.Add($HlePolicy) - $startInfo.ArgumentList.Add("--hle-report") - $startInfo.ArgumentList.Add($ReportPath) - foreach ($name in $AllowedUnknownHle) { - $startInfo.ArgumentList.Add("--allow-unknown-hle") - $startInfo.ArgumentList.Add($name) - } - $startInfo.Environment["RUST_LOG"] = - "dingoo_emu=info,dingooemu_core::app_loader=info,dingooemu_core::cpu=off,dingooemu_core::emulator=warn" - $startInfo.Environment["RUST_LOG_STYLE"] = "never" - - $process = [System.Diagnostics.Process]::new() - $process.StartInfo = $startInfo - - try { - if (-not $process.Start()) { - throw "Failed to start emulator process." - } - - $stdoutTask = $process.StandardOutput.ReadToEndAsync() - $stderrTask = $process.StandardError.ReadToEndAsync() - - if (-not $process.WaitForExit($Timeout * 1000)) { - $process.Kill($true) - $process.WaitForExit() - return [pscustomobject]@{ - ExitCode = $null - TimedOut = $true - Output = "Timed out after $Timeout seconds." - } - } - - $stdout = $stdoutTask.GetAwaiter().GetResult() - $stderr = $stderrTask.GetAwaiter().GetResult() - $output = @($stdout.Trim(), $stderr.Trim()) | - Where-Object { $_ } | - Join-String -Separator [Environment]::NewLine - - return [pscustomobject]@{ - ExitCode = $process.ExitCode - TimedOut = $false - Output = $output - } - } finally { - $process.Dispose() - } -} - -Write-Host "Using binary: $Binary" -Write-Host "Game dir: $gameDir" -Write-Host "Output dir: $outDir" -Write-Host "Report dir: $ReportDirectory" -Write-Host "HLE policy: $UnknownHlePolicy" -if ($framesSpecified) { - Write-Host "Frames: $Frames" -} else { - Write-Host "Frames: $Frames (with performance overrides)" -} -Write-Host "Timeout: $TimeoutSeconds seconds per game" -Write-Host "" - -$games = @( - Get-ChildItem -LiteralPath $gameDir -Filter "*.app" -Recurse -File | - Sort-Object FullName -) - -if ($games.Count -eq 0) { - Write-Warning "No .app files found under $gameDir" - exit 0 -} - -Write-Host "Found $($games.Count) game(s).`n" - -$success = 0 -$failed = 0 -$usedNames = [System.Collections.Generic.HashSet[string]]::new( - [System.StringComparer]::OrdinalIgnoreCase -) - -foreach ($game in $games) { - $baseName = [System.IO.Path]::GetFileNameWithoutExtension($game.Name) - $relativePath = [System.IO.Path]::GetRelativePath($gameDir, $game.FullName) - $relativeBaseName = [System.IO.Path]::ChangeExtension($relativePath, $null) - $relativeName = $relativeBaseName -replace '[/\\]+', '__' - $safeName = ConvertTo-ScreenshotName $relativeName - $captureFrames = Get-CaptureFrames $relativePath $Frames (-not $framesSpecified) - - $uniqueName = $safeName - $suffix = 2 - while (-not $usedNames.Add($uniqueName)) { - $uniqueName = "${safeName}_$suffix" - $suffix++ - } - - $safeName = $uniqueName - $outPath = Join-Path $outDir "$safeName.png" - $reportPath = Join-Path $ReportDirectory "$safeName.json" - - if (Test-Path -LiteralPath $outPath) { - Remove-Item -LiteralPath $outPath -Force - } - if (Test-Path -LiteralPath $reportPath) { - Remove-Item -LiteralPath $reportPath -Force - } - - Write-Host -NoNewline " $baseName ($captureFrames frames) ... " - - try { - $result = Invoke-ScreenshotCapture ` - -Executable $Binary ` - -GamePath $game.FullName ` - -ScreenshotPath $outPath ` - -ReportPath $reportPath ` - -CaptureFrames $captureFrames ` - -Timeout $TimeoutSeconds ` - -HlePolicy $UnknownHlePolicy ` - -AllowedUnknownHle $AllowUnknownHle - - if ($result.TimedOut) { - Write-Host "FAILED (timeout)" -ForegroundColor Red - $failed++ - } elseif ($result.ExitCode -ne 0) { - Write-Host "FAILED (exit $($result.ExitCode))" -ForegroundColor Red - if ($result.Output) { - $detailLines = @($result.Output -split '\r?\n' | - Where-Object { $_ } | - Where-Object { $_ -notmatch '^note: run with ' } | - Select-Object -Last 2) - foreach ($line in $detailLines) { - Write-Host " $line" -ForegroundColor DarkGray - } - } - $failed++ - } elseif ((Test-Path -LiteralPath $outPath -PathType Leaf) -and - (Test-Path -LiteralPath $reportPath -PathType Leaf)) { - $size = (Get-Item -LiteralPath $outPath).Length - $reportSize = (Get-Item -LiteralPath $reportPath).Length - $report = Get-Content -Raw -LiteralPath $reportPath | ConvertFrom-Json - $hasUnknownHleList = $null -ne $report.PSObject.Properties["unknown_hle"] - if ($size -gt 0 -and $reportSize -gt 0 -and $hasUnknownHleList) { - Write-Host "OK ($([math]::Round($size / 1KB)) KB)" -ForegroundColor Green - $success++ - } else { - Write-Host "FAILED (empty or invalid output)" -ForegroundColor Red - $failed++ - } - } else { - Write-Host "FAILED (missing screenshot or HLE report)" -ForegroundColor Red - $failed++ - } - } catch { - Write-Host "FAILED ($($_.Exception.Message))" -ForegroundColor Red - $failed++ - } -} - -Write-Host "" -Write-Host "Done: $success succeeded, $failed failed out of $($games.Count) total." - -if ($failed -gt 0) { - exit 1 -} diff --git a/native/DingooEmu/site/css/style.css b/native/DingooEmu/site/css/style.css deleted file mode 100644 index 640a35f..0000000 --- a/native/DingooEmu/site/css/style.css +++ /dev/null @@ -1,1164 +0,0 @@ -/* ============================================================ - DingooEmu — Landing Page Styles - Color palette inspired by retro handheld gaming: - Cyan #00D4FF / #0099CC - Orange #FF6B35 / #E55A2B - Purple #A855F7 / #7C3AED - Dark #0A0E14 / #111820 / #1A2030 / #252D3D - ============================================================ */ - -/* ---- Reset & Base ---- */ -*, *::before, *::after { margin: 0; padding: 0; box-sizing: border-box; } - -:root { - --cyan: #00D4FF; - --cyan-dark: #0099CC; - --orange: #FF6B35; - --orange-dark: #E55A2B; - --purple: #A855F7; - --purple-dark: #7C3AED; - --dark: #0A0E14; - --dark-2: #111820; - --dark-3: #1A2030; - --dark-4: #252D3D; - --text: #E8ECF1; - --text-dim: #8B95A5; - --radius: 12px; - --font-mono: 'SF Mono', 'Fira Code', 'Cascadia Code', 'JetBrains Mono', Consolas, monospace; - --font-sans: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Noto Sans SC', sans-serif; -} - -html { scroll-behavior: smooth; } - -body { - font-family: var(--font-sans); - background: var(--dark); - color: var(--text); - line-height: 1.6; - overflow-x: hidden; -} - -a { color: inherit; text-decoration: none; } -img { max-width: 100%; display: block; } - -.container { - max-width: 1200px; - margin: 0 auto; - padding: 0 24px; -} - -/* ---- Scanline overlay (global) ---- */ -body::after { - content: ''; - position: fixed; - inset: 0; - pointer-events: none; - z-index: 9999; - background: repeating-linear-gradient( - 0deg, - transparent, - transparent 2px, - rgba(0, 212, 255, 0.02) 2px, - rgba(0, 212, 255, 0.02) 4px - ); -} - -/* ============================================================ - NAV - ============================================================ */ -#navbar { - position: fixed; - top: 0; - left: 0; - right: 0; - z-index: 1000; - transition: background 0.3s, backdrop-filter 0.3s, box-shadow 0.3s; -} - -#navbar.scrolled { - background: rgba(10, 14, 20, 0.9); - backdrop-filter: blur(16px); - -webkit-backdrop-filter: blur(16px); - box-shadow: 0 1px 0 rgba(0, 212, 255, 0.2); -} - -.nav-inner { - max-width: 1200px; - margin: 0 auto; - padding: 0 24px; - height: 64px; - display: flex; - align-items: center; - justify-content: space-between; -} - -.nav-logo { - display: flex; - align-items: center; - gap: 10px; - font-weight: 700; - font-size: 1.1rem; -} - -.nav-logo img { border-radius: 6px; height: 36px; width: auto; } - -.nav-links { - display: flex; - align-items: center; - gap: 28px; -} - -.nav-links a { - font-size: 0.9rem; - color: var(--text-dim); - transition: color 0.2s; - font-weight: 500; -} - -.nav-links a:hover { color: var(--cyan); } - -.nav-github { - display: flex; - align-items: center; -} - -.nav-toggle { - display: none; - flex-direction: column; - gap: 5px; - background: none; - border: none; - cursor: pointer; - padding: 4px; -} - -.nav-toggle span { - display: block; - width: 24px; - height: 2px; - background: var(--text); - transition: 0.3s; -} - -/* ============================================================ - HERO - ============================================================ */ -#hero { - position: relative; - min-height: 100vh; - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - text-align: center; - overflow: hidden; -} - -.hero-bg { - position: absolute; - inset: 0; - z-index: 0; -} - -#pixel-canvas { - width: 100%; - height: 100%; - opacity: 0.12; -} - -.hero-content { - position: relative; - z-index: 1; - padding: 0 24px; -} - -.hero-title { - font-size: clamp(2.5rem, 6vw, 5rem); - font-weight: 800; - line-height: 1.1; - margin-bottom: 16px; - letter-spacing: -0.02em; -} - -.hero-title-line { - display: block; -} - -.hero-title-line.accent { - background: linear-gradient(135deg, var(--cyan), var(--purple)); - -webkit-background-clip: text; - -webkit-text-fill-color: transparent; - background-clip: text; -} - -.hero-subtitle { - font-size: clamp(1rem, 2.5vw, 1.5rem); - color: var(--text-dim); - margin-bottom: 12px; - font-weight: 400; -} - -.hero-desc { - font-size: 1rem; - color: var(--text-dim); - max-width: 600px; - margin: 0 auto 28px; - opacity: 0.8; -} - -.hero-badges { - display: flex; - gap: 8px; - justify-content: center; - flex-wrap: wrap; - margin-bottom: 32px; -} - -.hero-badges img { height: 28px; } - -.hero-actions { - display: flex; - gap: 16px; - justify-content: center; - flex-wrap: wrap; -} - -.btn { - display: inline-flex; - align-items: center; - gap: 8px; - padding: 12px 28px; - border-radius: 8px; - font-weight: 600; - font-size: 0.95rem; - transition: all 0.25s; - border: none; - cursor: pointer; -} - -.btn-primary { - background: var(--orange); - color: #fff; - box-shadow: 0 4px 24px rgba(255, 107, 53, 0.3); -} - -.btn-primary:hover { - background: var(--orange-dark); - transform: translateY(-2px); - box-shadow: 0 8px 32px rgba(255, 107, 53, 0.4); -} - -.btn-secondary { - background: var(--dark-4); - color: var(--text); - border: 1px solid rgba(255,255,255,0.1); -} - -.btn-secondary:hover { - background: #3a3a3a; - transform: translateY(-2px); -} - -.hero-scroll { - position: absolute; - bottom: 40px; - left: 50%; - transform: translateX(-50%); - display: flex; - flex-direction: column; - align-items: center; - gap: 8px; - color: var(--text-dim); - font-size: 0.8rem; - animation: fadeInUp 1s 1.5s both; -} - -.scroll-arrow { - width: 20px; - height: 20px; - border-right: 2px solid var(--text-dim); - border-bottom: 2px solid var(--text-dim); - transform: rotate(45deg); - animation: scrollBounce 2s infinite; -} - -@keyframes scrollBounce { - 0%, 100% { transform: rotate(45deg) translateY(0); opacity: 1; } - 50% { transform: rotate(45deg) translateY(6px); opacity: 0.5; } -} - -/* ============================================================ - SECTIONS - ============================================================ */ -.section { - padding: 100px 0; -} - -.section-dark { - background: var(--dark-2); -} - -.section-title { - font-size: clamp(1.8rem, 4vw, 2.8rem); - font-weight: 800; - text-align: center; - margin-bottom: 12px; - letter-spacing: -0.01em; -} - -.section-subtitle { - text-align: center; - color: var(--text-dim); - font-size: 1.05rem; - margin-bottom: 56px; - max-width: 600px; - margin-left: auto; - margin-right: auto; -} - -/* ============================================================ - ABOUT - ============================================================ */ -.about-grid { - display: grid; - grid-template-columns: 1fr 1fr; - gap: 60px; - align-items: center; -} - -.about-text h2 { - font-size: 2rem; - margin-bottom: 20px; -} - -.about-text p { - color: var(--text-dim); - margin-bottom: 16px; - font-size: 1.05rem; -} - -.about-text strong { color: var(--cyan); } - -.diagram-flow { - display: flex; - align-items: center; - justify-content: center; - gap: 16px; -} - -.diagram-node { - display: flex; - flex-direction: column; - align-items: center; - gap: 8px; - padding: 20px 16px; - background: var(--dark-3); - border: 1px solid rgba(255,255,255,0.06); - border-radius: var(--radius); - min-width: 120px; - transition: 0.3s; -} - -.diagram-node:hover { - border-color: var(--purple); - transform: translateY(-4px); -} - -.diagram-node.highlight { - border-color: var(--cyan); - box-shadow: 0 0 30px rgba(0, 212, 255, 0.1); -} - -.diagram-icon { - width: 48px; - height: 48px; - border-radius: 12px; - display: flex; - align-items: center; - justify-content: center; -} - -.chip-icon { - background: linear-gradient(135deg, var(--orange), var(--orange-dark)); - box-shadow: 0 4px 16px rgba(255, 107, 53, 0.3); -} - -.game-icon { - background: linear-gradient(135deg, var(--purple), var(--purple-dark)); - box-shadow: 0 4px 16px rgba(168, 85, 247, 0.3); -} - -.emu-icon { - background: linear-gradient(135deg, var(--cyan), var(--cyan-dark)); - box-shadow: 0 4px 16px rgba(0, 212, 255, 0.3); -} - -.diagram-node span { font-weight: 600; font-size: 0.9rem; } -.diagram-node small { color: var(--text-dim); font-size: 0.75rem; } - -.diagram-arrow { - font-size: 1.5rem; - color: var(--text-dim); - opacity: 0.5; -} - -/* ============================================================ - STATS - ============================================================ */ -.stats-grid { - display: grid; - grid-template-columns: repeat(4, 1fr); - gap: 32px; -} - -.stat-item { - text-align: center; - padding: 40px 16px; - background: var(--dark-3); - border: 1px solid rgba(255,255,255,0.05); - border-radius: var(--radius); - transition: 0.3s; -} - -.stat-item:hover { - border-color: var(--orange); - transform: translateY(-4px); - box-shadow: 0 8px 32px rgba(255, 107, 53, 0.15); -} - -.stat-number { - font-size: 3rem; - font-weight: 800; - background: linear-gradient(135deg, var(--orange), var(--cyan)); - -webkit-background-clip: text; - -webkit-text-fill-color: transparent; - background-clip: text; - line-height: 1; - margin-bottom: 8px; - font-variant-numeric: tabular-nums; -} - -.stat-label { - font-weight: 600; - font-size: 1rem; - margin-bottom: 4px; -} - -.stat-sub { - color: var(--text-dim); - font-size: 0.8rem; -} - -/* ============================================================ - FEATURES - ============================================================ */ -.features-grid { - display: grid; - grid-template-columns: repeat(3, 1fr); - gap: 24px; -} - -.feature-card { - padding: 32px 28px; - background: var(--dark-2); - border: 1px solid rgba(255,255,255,0.05); - border-radius: var(--radius); - transition: all 0.3s; - position: relative; - overflow: hidden; -} - -.feature-card::before { - content: ''; - position: absolute; - top: 0; - left: 0; - right: 0; - height: 3px; - background: linear-gradient(90deg, var(--cyan), var(--orange)); - opacity: 0; - transition: opacity 0.3s; -} - -.feature-card:hover { - border-color: rgba(255,255,255,0.1); - transform: translateY(-4px); - box-shadow: 0 12px 40px rgba(0,0,0,0.3); -} - -.feature-card:hover::before { opacity: 1; } - -.feature-icon { - width: 48px; - height: 48px; - border-radius: 12px; - background: rgba(0, 212, 255, 0.1); - display: flex; - align-items: center; - justify-content: center; - margin-bottom: 20px; - color: var(--cyan); -} - -.feature-icon svg { width: 24px; height: 24px; } - -.feature-card h3 { - font-size: 1.15rem; - margin-bottom: 10px; - font-weight: 700; -} - -.feature-card p { - color: var(--text-dim); - font-size: 0.9rem; - line-height: 1.7; -} - -/* ============================================================ - GALLERY - ============================================================ */ -.gallery-filters { - display: flex; - gap: 8px; - justify-content: center; - flex-wrap: wrap; - margin-bottom: 40px; -} - -.filter-btn { - padding: 8px 20px; - border-radius: 100px; - border: 1px solid rgba(255,255,255,0.1); - background: transparent; - color: var(--text-dim); - font-size: 0.85rem; - font-weight: 500; - cursor: pointer; - transition: all 0.25s; - font-family: var(--font-sans); -} - -.filter-btn:hover { - border-color: var(--cyan); - color: var(--cyan); -} - -.filter-btn.active { - background: var(--cyan); - color: var(--dark); - border-color: var(--cyan); - font-weight: 600; -} - -.gallery-grid { - display: grid; - grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); - gap: 16px; -} - -.gallery-item { - position: relative; - border-radius: var(--radius); - overflow: hidden; - aspect-ratio: 4 / 3; - cursor: pointer; - border: 1px solid rgba(255,255,255,0.05); - transition: all 0.35s; -} - -.gallery-item.hidden { - display: none; -} - -.gallery-item img { - width: 100%; - height: 100%; - object-fit: cover; - transition: transform 0.5s; -} - -.gallery-item:hover { - transform: translateY(-6px); - box-shadow: 0 16px 48px rgba(0,0,0,0.5), 0 0 0 1px rgba(0, 212, 255, 0.3); -} - -.gallery-item:hover img { - transform: scale(1.08); -} - -.gallery-overlay { - position: absolute; - inset: 0; - background: linear-gradient(to top, rgba(10,14,20,0.95) 0%, rgba(10,14,20,0.3) 50%, transparent 100%); - display: flex; - flex-direction: column; - justify-content: flex-end; - padding: 16px; - opacity: 0; - transition: opacity 0.35s; -} - -.gallery-item:hover .gallery-overlay { opacity: 1; } - -.gallery-tag { - display: inline-block; - padding: 2px 8px; - border-radius: 4px; - background: var(--orange); - color: #fff; - font-size: 0.7rem; - font-weight: 700; - margin-bottom: 6px; - width: fit-content; -} - -.gallery-overlay h4 { - font-size: 0.95rem; - font-weight: 700; - margin-bottom: 4px; -} - -.gallery-overlay p { - font-size: 0.78rem; - color: var(--text-dim); - line-height: 1.4; -} - -/* ============================================================ - ARCHITECTURE - ============================================================ */ -.arch-diagram { - max-width: 800px; - margin: 0 auto; - display: flex; - flex-direction: column; - align-items: center; - gap: 0; -} - -.arch-layer { - width: 100%; - padding: 24px; - border-radius: var(--radius); - position: relative; -} - -.arch-label { - font-size: 0.75rem; - font-weight: 700; - text-transform: uppercase; - letter-spacing: 0.1em; - color: var(--text-dim); - margin-bottom: 16px; - text-align: center; -} - -.arch-frontend { - background: linear-gradient(135deg, rgba(168, 85, 247, 0.1), rgba(124, 58, 237, 0.1)); - border: 1px solid rgba(168, 85, 247, 0.2); -} - -.arch-core { - background: linear-gradient(135deg, rgba(255, 107, 53, 0.1), rgba(229, 90, 43, 0.1)); - border: 1px solid rgba(255, 107, 53, 0.2); -} - -.arch-platform { - background: linear-gradient(135deg, rgba(0, 212, 255, 0.1), rgba(0, 153, 204, 0.1)); - border: 1px solid rgba(0, 212, 255, 0.2); -} - -.arch-boxes { - display: flex; - gap: 16px; - justify-content: center; - flex-wrap: wrap; -} - -.arch-box { - background: rgba(0,0,0,0.3); - border: 1px solid rgba(255,255,255,0.08); - border-radius: 8px; - padding: 16px 24px; - text-align: center; - min-width: 160px; - transition: 0.3s; -} - -.arch-box:hover { - border-color: rgba(255,255,255,0.15); - transform: translateY(-2px); -} - -.arch-box strong { - display: block; - font-size: 0.95rem; - margin-bottom: 4px; -} - -.arch-box small { - color: var(--text-dim); - font-size: 0.78rem; -} - -.arch-box.core-box { - min-width: 320px; -} - -.core-modules { - display: flex; - flex-wrap: wrap; - gap: 6px; - margin-top: 12px; - justify-content: center; -} - -.core-modules span { - padding: 4px 10px; - background: rgba(255, 107, 53, 0.15); - border-radius: 4px; - font-size: 0.72rem; - color: var(--orange); - font-weight: 500; -} - -.arch-box.small-box { - padding: 12px 20px; - font-size: 0.85rem; -} - -.arch-connector { - height: 40px; - width: 100px; -} - -.arch-connector svg { - width: 100%; - height: 100%; -} - -/* ============================================================ - QUICK START - ============================================================ */ -.quickstart-grid { - display: grid; - grid-template-columns: repeat(3, 1fr); - gap: 24px; -} - -.quickstart-card { - background: var(--dark-2); - border: 1px solid rgba(255,255,255,0.05); - border-radius: var(--radius); - padding: 32px 28px; - transition: 0.3s; -} - -.quickstart-card:hover { - border-color: rgba(255,255,255,0.1); - transform: translateY(-4px); - box-shadow: 0 12px 40px rgba(0,0,0,0.3); -} - -.qs-header { - display: flex; - align-items: center; - gap: 12px; - margin-bottom: 24px; -} - -.qs-icon { font-size: 1.5rem; } -.qs-header h3 { font-size: 1.2rem; } - -.qs-steps { display: flex; flex-direction: column; gap: 20px; } - -.qs-step { - display: flex; - gap: 14px; - align-items: flex-start; -} - -.qs-num { - width: 28px; - height: 28px; - min-width: 28px; - border-radius: 50%; - background: var(--orange); - color: #fff; - display: flex; - align-items: center; - justify-content: center; - font-size: 0.8rem; - font-weight: 700; -} - -.qs-step p { - font-weight: 600; - font-size: 0.9rem; - margin-bottom: 4px; -} - -.qs-step code { - display: block; - font-family: var(--font-mono); - font-size: 0.78rem; - color: var(--cyan); - background: rgba(0, 212, 255, 0.08); - padding: 6px 10px; - border-radius: 6px; - margin-top: 4px; - word-break: break-all; -} - -/* ============================================================ - FOOTER - ============================================================ */ -#footer { - background: var(--dark-2); - border-top: 1px solid rgba(255,255,255,0.05); - padding: 60px 0 30px; -} - -.footer-grid { - display: grid; - grid-template-columns: 2fr 1fr 1fr 1fr; - gap: 40px; - margin-bottom: 40px; -} - -.footer-brand { - display: flex; - flex-direction: column; - gap: 8px; -} - -.footer-brand img { border-radius: 6px; height: 32px; width: auto; opacity: 0.8; } - -.footer-name { - font-weight: 700; - font-size: 1.1rem; -} - -.footer-brand p { - color: var(--text-dim); - font-size: 0.9rem; -} - -.footer-links h4 { - font-size: 0.85rem; - font-weight: 700; - margin-bottom: 16px; - color: var(--text); -} - -.footer-links a { - display: block; - color: var(--text-dim); - font-size: 0.88rem; - padding: 4px 0; - transition: color 0.2s; -} - -.footer-links a:hover { color: var(--cyan); } - -.footer-bottom { - border-top: 1px solid rgba(255,255,255,0.05); - padding-top: 24px; - text-align: center; -} - -.footer-bottom p { - color: var(--text-dim); - font-size: 0.82rem; -} - -/* ============================================================ - ANIMATIONS - ============================================================ */ -.fade-in-up { - opacity: 0; - transform: translateY(30px); - transition: opacity 0.7s ease, transform 0.7s ease; -} - -.fade-in-up.visible { - opacity: 1; - transform: translateY(0); -} - -@keyframes fadeInUp { - from { opacity: 0; transform: translateY(30px); } - to { opacity: 1; transform: translateY(0); } -} - -/* ============================================================ - RESPONSIVE - ============================================================ */ -@media (max-width: 1024px) { - .features-grid { grid-template-columns: repeat(2, 1fr); } - .quickstart-grid { grid-template-columns: 1fr; } - .footer-grid { grid-template-columns: 1fr 1fr; gap: 32px; } -} - -@media (max-width: 768px) { - .nav-links { display: none; } - .nav-links.open { - display: flex; - flex-direction: column; - position: absolute; - top: 64px; - left: 0; - right: 0; - background: rgba(10, 14, 20, 0.95); - backdrop-filter: blur(16px); - padding: 24px; - gap: 16px; - border-bottom: 1px solid rgba(255,255,255,0.05); - } - .nav-toggle { display: flex; } - - .about-grid { grid-template-columns: 1fr; gap: 40px; } - .stats-grid { grid-template-columns: repeat(2, 1fr); gap: 16px; } - .features-grid { grid-template-columns: 1fr; } - .gallery-grid { grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); gap: 10px; } - .footer-grid { grid-template-columns: 1fr; gap: 24px; } - - .hero-badges img { height: 22px; } - .hero-actions { flex-direction: column; align-items: center; } - - .diagram-flow { flex-direction: column; gap: 12px; } - .diagram-arrow { transform: rotate(90deg); } - - .section { padding: 60px 0; } - - .arch-box.core-box { min-width: auto; } -} - -@media (max-width: 480px) { - .stats-grid { grid-template-columns: 1fr 1fr; } - .stat-number { font-size: 2.2rem; } - .gallery-overlay { opacity: 1; } -} - -/* ============================================================ - LANGUAGE TOGGLE - ============================================================ */ -.lang-toggle { - padding: 4px 12px; - border-radius: 6px; - border: 1px solid rgba(255,255,255,0.15); - background: transparent; - color: var(--cyan); - font-size: 0.8rem; - font-weight: 700; - cursor: pointer; - transition: all 0.25s; - font-family: var(--font-mono); - letter-spacing: 0.05em; -} - -.lang-toggle:hover { - background: rgba(0, 212, 255, 0.1); - border-color: var(--cyan); -} - -/* ============================================================ - GALLERY TABS - ============================================================ */ -.gallery-tabs { - display: flex; - gap: 8px; - justify-content: center; - flex-wrap: wrap; - margin-bottom: 32px; -} - -.gallery-tab { - padding: 8px 20px; - border-radius: 100px; - border: 1px solid rgba(255,255,255,0.1); - background: transparent; - color: var(--text-dim); - font-size: 0.85rem; - font-weight: 500; - cursor: pointer; - transition: all 0.25s; - font-family: var(--font-sans); -} - -.gallery-tab:hover { - border-color: var(--cyan); - color: var(--cyan); -} - -.gallery-tab.active { - background: var(--cyan); - color: var(--dark); - border-color: var(--cyan); - font-weight: 600; -} - -/* ============================================================ - CAROUSEL - ============================================================ */ -.carousel-wrapper { - position: relative; - padding: 0 48px; -} - -.carousel-viewport { - overflow: hidden; - border-radius: var(--radius); -} - -.carousel-track { - display: flex; - gap: 16px; - transition: transform 0.4s cubic-bezier(0.25, 0.46, 0.45, 0.94); -} - -.carousel-card { - flex: 0 0 calc((100% - 48px) / 4); -} - -.carousel-card-inner, -.carousel-card { - position: relative; - border-radius: var(--radius); - overflow: hidden; - aspect-ratio: 4 / 3; - cursor: pointer; - border: 1px solid rgba(255,255,255,0.05); - transition: all 0.35s; -} - -.carousel-card:hover { - transform: translateY(-4px); - box-shadow: 0 12px 36px rgba(0,0,0,0.5), 0 0 0 1px rgba(0, 212, 255, 0.3); - z-index: 2; -} - -.carousel-card img { - width: 100%; - height: 100%; - object-fit: contain; - background: #000; - transition: transform 0.5s; -} - -.carousel-card:hover img { - transform: scale(1.06); -} - -.carousel-card-overlay { - position: absolute; - inset: 0; - background: linear-gradient(to top, rgba(10,14,20,0.95) 0%, rgba(10,14,20,0.3) 50%, transparent 100%); - display: flex; - flex-direction: column; - justify-content: flex-end; - padding: 14px; - opacity: 0; - transition: opacity 0.35s; -} - -.carousel-card:hover .carousel-card-overlay { opacity: 1; } - -.carousel-card-overlay .gallery-tag { - display: inline-block; - padding: 2px 8px; - border-radius: 4px; - background: var(--orange); - color: #fff; - font-size: 0.65rem; - font-weight: 700; - margin-bottom: 4px; - width: fit-content; -} - -.carousel-card-overlay h4 { - font-size: 0.85rem; - font-weight: 700; - margin-bottom: 3px; - line-height: 1.3; -} - -.carousel-card-overlay p { - font-size: 0.72rem; - color: var(--text-dim); - line-height: 1.4; -} - -/* Carousel navigation buttons */ -.carousel-btn { - position: absolute; - top: 50%; - transform: translateY(-50%); - width: 40px; - height: 40px; - border-radius: 50%; - border: 1px solid rgba(255,255,255,0.15); - background: rgba(10, 14, 20, 0.8); - color: var(--text); - font-size: 1.4rem; - cursor: pointer; - transition: all 0.25s; - display: flex; - align-items: center; - justify-content: center; - z-index: 10; - backdrop-filter: blur(8px); -} - -.carousel-btn:hover { - background: var(--cyan); - color: var(--dark); - border-color: var(--cyan); -} - -.carousel-prev { left: 0; } -.carousel-next { right: 0; } - -/* Carousel dots */ -.carousel-dots { - display: flex; - justify-content: center; - gap: 8px; - margin-top: 20px; -} - -.carousel-dot { - width: 8px; - height: 8px; - border-radius: 50%; - background: rgba(255,255,255,0.2); - cursor: pointer; - transition: all 0.25s; -} - -.carousel-dot.active { - background: var(--cyan); - width: 24px; - border-radius: 4px; -} - -.carousel-dot:hover { - background: rgba(255,255,255,0.4); -} - -/* ============================================================ - CAROUSEL RESPONSIVE - ============================================================ */ -@media (max-width: 768px) { - .carousel-wrapper { padding: 0 36px; } - .carousel-card { flex-basis: calc((100% - 16px) / 2); } - .carousel-btn { width: 32px; height: 32px; font-size: 1.1rem; } - .gallery-tab { padding: 6px 14px; font-size: 0.78rem; } -} - -@media (max-width: 480px) { - .carousel-wrapper { padding: 0 28px; } - .carousel-card { flex-basis: 100%; } - .carousel-card-overlay { opacity: 1; } - .carousel-btn { width: 28px; height: 28px; font-size: 1rem; } -} diff --git a/native/DingooEmu/site/index.html b/native/DingooEmu/site/index.html deleted file mode 100644 index 55b54c5..0000000 --- a/native/DingooEmu/site/index.html +++ /dev/null @@ -1,387 +0,0 @@ - - - - - - DingooEmu — Dingoo A320 掌机模拟器 - - - - - - - - - - -
-
- -
-
-

- Dingoo - Emulator -

-

重温经典掌机游戏

-

用 Rust 编写的 Dingoo A320 掌机模拟器,完整支持 Ingenic JZ4740 MIPS SoC 和 Dingoo SDK

-
- Release - License - Downloads - Quality Gate -
- -
-
- 向下滚动探索 -
-
-
- - -
-
-
-
-

什么是 Dingoo A320?

-

Dingoo A320 是一款搭载 Ingenic JZ4740 MIPS 处理器的便携式游戏掌机,于 2009 年发布。它拥有 320×240 分辨率的 2.8 英寸 TFT 屏幕,支持运行原生游戏和多种模拟器。

-

通过 DingooEmu,这些经典的掌机游戏可以在现代设备上重新体验。

-
-
-
-
-
- Ingenic JZ4740 - MIPS32 XBurst @ 336MHz -
-
→
-
-
- .app 容器 - Dingoo 原生游戏 -
-
→
-
-
- DingooEmu - Rust 模拟器 -
-
-
-
-
-
- - -
-
-
-
-
0
-
支持游戏格式
-
.app 容器格式
-
-
-
0
-
MIPS 指令集
-
MIPS32 XBurst 架构
-
-
-
0
-
目标平台
-
Windows / macOS / Linux / Android
-
-
-
0
-
SDK HLE 函数
-
完整 Dingoo SDK 模拟
-
-
-
-
- - -
-
-

核心特性

-

从 MIPS CPU 到 Dingoo SDK,全栈 Rust 实现

-
-
-
- -
-

MIPS32 CPU 与 JIT

-

全平台使用缓存解释器,64 位 Android 还可将高频 MIPS32 指令块动态翻译为本机代码,并对不支持的路径精确回退。

-
-
-
- -
-

Dingoo SDK HLE

-

高层模拟 Dingoo 原生 SDK,包括图形渲染、输入处理、音频播放、文件系统和多任务调度。

-
-
-
- -
-

双前端架构

-

平台无关的核心引擎 + 独立的 Standalone 和 RetroArch 前端,共享 100% 模拟逻辑。

-
-
-
- -
-

.app 容器加载

-

解析 Dingoo 专有的 .app 游戏容器格式,包含 CCDL、IMPT、EXPT、RAWD 等数据块。

-
-
-
- -
-

PCM 音频

-

16-bit PCM 音频输出,支持 22050 Hz 采样率,完整还原掌机音效。

-
-
-
- -
-

RetroArch 核心

-

完整的 libretro 核心,支持 RetroPad 映射、核心选项、即时存档等 RetroArch 生态功能。

-
-
-
-
- - - - - -
-
-

技术架构

-

清晰的三层架构,平台无关的核心引擎

-
-
-
前端
-
-
- dingooemu - Standalone 可执行文件
minifb 窗口
-
-
- dingooemu-libretro - libretro cdylib
RetroArch 核心
-
-
-
-
- - - - -
-
-
核心引擎
-
-
- dingooemu-core - 平台无关的库 -
- MIPS CPU / JIT - Memory - Graphics - Audio - SDK HLE - Input - Timer - File I/O -
-
-
-
-
- - - -
-
-
目标平台
-
-
Windows
-
macOS
-
Linux
-
Android
-
-
-
-
-
- - -
-
-

快速开始

-

几行命令,即刻体验

-
-
-
- 🖥️ -

Standalone

-
-
-
- 1 -
-

下载最新版本

- 从 Releases 页面下载对应平台的二进制文件 -
-
-
- 2 -
-

运行游戏

- dingooemu game.app -
-
-
- 3 -
-

或从源码编译

- cargo build -p dingooemu --release -
-
-
-
-
-
- 🎮 -

RetroArch

-
-
-
- 1 -
-

下载 libretro 核心

- 从 Releases 页面下载对应平台的核心文件 -
-
-
- 2 -
-

安装核心

- 复制到 RetroArch 的 cores/ 目录 -
-
-
- 3 -
-

加载核心并启动

- Load Core → DingooEmu → Load Content -
-
-
-
-
-
- 🔨 -

从源码编译

-
-
-
- 1 -
-

克隆仓库

- git clone https://github.com/AloysHF/DingooEmu.git -
-
-
- 2 -
-

编译 Standalone

- cargo build -p dingooemu --release -
-
-
- 3 -
-

或编译 RetroArch 核心

- cargo build -p dingooemu-libretro --release -
-
-
-
-
-
-
- - - - - - - diff --git a/native/DingooEmu/site/js/main.js b/native/DingooEmu/site/js/main.js deleted file mode 100644 index f4f0654..0000000 --- a/native/DingooEmu/site/js/main.js +++ /dev/null @@ -1,650 +0,0 @@ -// ============================================================ -// DingooEmu — Landing Page Scripts -// i18n + Carousel Gallery + Animations -// ============================================================ - -(function () { - 'use strict'; - - // ================================================================ - // GAME DATA — Dingoo A320 games (from Game-Compatibility.md) - // ================================================================ - var GAMES = [ - { zh: '七夜(20090715111247)', en: 'Seven Nights (20090715111247)', img: 'docs/images/7day-20090715111247.png', descZh: '恐怖冒险游戏', descEn: 'Horror adventure game' }, - { zh: '战神刑天', en: 'Decollation Warrior', img: 'docs/images/Decollation-Warrior.png', descZh: '横版动作过关游戏', descEn: 'Side-scrolling action game' }, - { zh: '天地道(2008-12-29 版本)', en: 'Hell Striker II (2008-12-29 build)', img: 'docs/images/Hell_Striker_II-20081229173817.png', descZh: '射击动作游戏', descEn: 'Shooter action game' }, - { zh: '天地道(2009-01-22 版本)', en: 'Hell Striker II (2009-01-22 build)', img: 'docs/images/Hell_Striker_II-20090122224048.png', descZh: '射击动作游戏', descEn: 'Shooter action game' }, - { zh: '赵云传', en: 'Zhao-Chuan RPG', img: 'docs/images/Zhao-Chuan_RPG.png', descZh: '角色扮演游戏', descEn: 'RPG adventure game' }, - { zh: '阿里巴巴', en: 'Ali Baba', img: 'docs/images/AliBaba.png', descZh: '阿里巴巴主题游戏', descEn: 'Ali Baba themed game' }, - { zh: '星际着陆', en: 'Astro Lander', img: 'docs/images/Astro-Lander__Astro-Lander.png', descZh: '太空着陆游戏', descEn: 'Space landing game' }, - { zh: '打砖块', en: 'Block Breaker', img: 'docs/images/Block_Breaker.png', descZh: '经典打砖块游戏', descEn: 'Classic block breaker game' }, - { zh: '糖果屋', en: 'Candy', img: 'docs/images/Candy.png', descZh: '糖果主题游戏', descEn: 'Candy themed game' }, - { zh: 'F1赛车', en: 'Formula One', img: 'docs/images/Fomula-One.png', descZh: 'F1 方程式赛车', descEn: 'F1 formula racing' }, - { zh: 'Goo播放器', en: 'GooPlayer', img: 'docs/images/GooPlayer__GooPlayer.png', descZh: '音乐播放器', descEn: 'Music player' }, - { zh: '六角病毒', en: 'Hexa-Virus', img: 'docs/images/Hexa-Virus.png', descZh: '六边形消除游戏', descEn: 'Hexagonal matching game' }, - { zh: '斗地主', en: 'Landlord', img: 'docs/images/Landlord.png', descZh: '经典扑克牌游戏', descEn: 'Classic card game' }, - { zh: '连连看', en: "Link'em Up", img: "docs/images/Link'em_Up.png", descZh: '图案配对消除游戏', descEn: 'Pattern matching puzzle' }, - { zh: '疯狂矿工', en: 'Manic Miner', img: 'docs/images/Manic-Miner.png', descZh: '经典矿工游戏', descEn: 'Classic miner game' }, - { zh: '千足虫', en: 'Millipede', img: 'docs/images/Millipede.png', descZh: '经典蜈蚣游戏', descEn: 'Classic centipede game' }, - { zh: '扫雷', en: 'Mine Sweeper', img: 'docs/images/Mine_Sweeper.png', descZh: '经典扫雷游戏', descEn: 'Classic minesweeper game' }, - { zh: '蘑菇轮盘', en: 'Mushroom Roulette', img: 'docs/images/Mushroom_Roulette.png', descZh: '蘑菇主题游戏', descEn: 'Mushroom themed game' }, - { zh: '卢比卢比', en: 'Nose Breaker', img: 'docs/images/Nose_Breaker.png', descZh: '趣味休闲游戏', descEn: 'Fun casual game' }, - { zh: '霸王战纪(桩版本)', en: 'Overlord Fighter (stub build)', img: 'docs/images/Overlord-Fighter-Stub.png', descZh: '格斗游戏', descEn: 'Fighting game' }, - { zh: '霸王战纪', en: 'Overlord Fighter', img: 'docs/images/Overlord-Fighter.png', descZh: '格斗游戏', descEn: 'Fighting game' }, - { zh: '白金数独', en: 'Platinum Sudoku', img: 'docs/images/Platinum_Sudoku.png', descZh: '数字逻辑益智游戏', descEn: 'Number logic puzzle' }, - { zh: '泡泡', en: 'PoPo Bash', img: 'docs/images/PoPo_Bash.png', descZh: '泡泡主题动作游戏', descEn: 'Bubble-themed action game' }, - { zh: '里克危险', en: 'Rick Dangerous', img: 'docs/images/Rick-Dangerous.png', descZh: '经典冒险游戏', descEn: 'Classic adventure game' }, - { zh: '鲁比多(2009-05-12 版本)', en: 'Rubido (2009-05-12 build)', img: 'docs/images/Rubido-20090512001427.png', descZh: '益智消除游戏', descEn: 'Puzzle matching game' }, - { zh: '鲁比多(2009-05-16 版本)', en: 'Rubido (2009-05-16 build)', img: 'docs/images/Rubido-20090516230856.png', descZh: '益智消除游戏', descEn: 'Puzzle matching game' }, - { zh: '消消乐', en: 'SameGoo', img: 'docs/images/SameGoo__samegoo.png', descZh: '同色消除游戏', descEn: 'Same color matching game' }, - { zh: '仙剑奇侠传(根目录版本)', en: 'Sword and Fairy (root build)', img: 'docs/images/仙剑奇侠传.png', descZh: '经典中文角色扮演游戏', descEn: 'Classic Chinese RPG adventure' }, - { zh: '仙剑奇侠传(子目录版本)', en: 'Sword and Fairy (subdirectory build)', img: 'docs/images/仙剑奇侠传__仙剑奇侠传.png', descZh: '经典中文角色扮演游戏', descEn: 'Classic Chinese RPG adventure' }, - { zh: '推箱子', en: 'Sokuban', img: 'docs/images/Sokuban__Sokuban.png', descZh: '经典推箱子益智游戏', descEn: 'Classic Sokoban puzzle' }, - { zh: 'Spoout', en: 'Spoout', img: 'docs/images/Spoout.png', descZh: '休闲动作游戏', descEn: 'Casual action game' }, - { zh: '迪克蛇', en: 'Snake', img: 'docs/images/Snake.png', descZh: '经典贪吃蛇游戏', descEn: 'Classic snake game' }, - { zh: '秒表', en: 'StopWatch', img: 'docs/images/StopWatch.png', descZh: '计时器游戏', descEn: 'Timer game' }, - { zh: '俄罗斯方块', en: 'Tetris', img: 'docs/images/Tetris.png', descZh: '经典方块消除游戏', descEn: 'Classic block puzzle game' }, - { zh: '极限漂移(2008-07-16 版本)', en: 'Ultimate Drift (2008-07-16 build)', img: 'docs/images/Ultimate_Drift-20080716163042.png', descZh: '竞速赛车游戏', descEn: 'Racing car game' }, - { zh: '极限漂移(2008-11-17 版本)', en: 'Ultimate Drift (2008-11-17 build)', img: 'docs/images/Ultimate_Drift-20081117180631.png', descZh: '竞速赛车游戏', descEn: 'Racing car game' }, - { zh: '零重力', en: 'Zero Gravity', img: 'docs/images/Zero-Gravity.png', descZh: '太空主题游戏', descEn: 'Space themed game' }, - { zh: '七夜(20081217192316)', en: 'Seven Nights (20081217192316)', img: 'docs/images/7day-20081217192316.png', descZh: '恐怖冒险游戏', descEn: 'Horror adventure game' }, - { zh: '七夜(20090715110443)', en: 'Seven Nights (20090715110443)', img: 'docs/images/7day-20090715110443.png', descZh: '恐怖冒险游戏', descEn: 'Horror adventure game' } - ]; - - var CATEGORIES = [ - { id: 'all', zh: '全部', en: 'All' } - ]; - - // ================================================================ - // i18n — Translations - // ================================================================ - var translations = { - zh: { - // meta - 'meta-title': 'DingooEmu — Dingoo A320 掌机模拟器', - 'meta-desc': '用 Rust 编写的 Dingoo A320 掌机模拟器,支持 MIPS32 CPU 模拟、64 位 Android JIT 加速、Dingoo SDK HLE 和 RetroArch 核心', - // nav - 'nav-features': '核心特性', - 'nav-games': '游戏库', - 'nav-arch': '技术架构', - 'nav-quickstart': '快速开始', - // hero - 'hero-subtitle': '重温经典掌机游戏', - 'hero-desc': '用 Rust 编写的 Dingoo A320 掌机模拟器,完整支持 Ingenic JZ4740 MIPS SoC 和 Dingoo SDK', - 'hero-download': '下载', - 'hero-github': '查看源码', - 'hero-scroll': '向下滚动探索', - // about - 'about-title': '什么是 Dingoo A320?', - 'about-p1': 'Dingoo A320 是一款搭载 Ingenic JZ4740 MIPS 处理器的便携式游戏掌机,于 2009 年发布。它拥有 320×240 分辨率的 2.8 英寸 TFT 屏幕,支持运行原生游戏和多种模拟器。', - 'about-p2': '通过 DingooEmu,这些经典的掌机游戏可以在现代设备上重新体验。', - 'about-chip': 'Ingenic JZ4740', - 'about-chip-sub': 'MIPS32 XBurst @ 336MHz', - 'about-game': '.app 容器', - 'about-game-sub': 'Dingoo 原生游戏', - 'about-emu-sub': 'Rust 模拟器', - // stats - 'stat-games': '支持游戏格式', - 'stat-games-sub': '.app 容器格式', - 'stat-opcodes': 'MIPS 指令集', - 'stat-opcodes-sub': 'MIPS32 XBurst 架构', - 'stat-platforms': '目标平台', - 'stat-platforms-sub': 'Windows / macOS / Linux / Android', - 'stat-lines': 'SDK HLE 函数', - 'stat-lines-sub': '完整 Dingoo SDK 模拟', - // features - 'feat-title': '核心特性', - 'feat-subtitle': '从 MIPS CPU 到 Dingoo SDK,全栈 Rust 实现', - 'feat-mips-title': 'MIPS32 CPU 与 JIT', - 'feat-mips-desc': '全平台使用缓存解释器,64 位 Android 还可将高频 MIPS32 指令块动态翻译为本机代码,并对不支持的路径精确回退。', - 'feat-sdk-title': 'Dingoo SDK HLE', - 'feat-sdk-desc': '高层模拟 Dingoo 原生 SDK,包括图形渲染、输入处理、音频播放、文件系统和多任务调度。', - 'feat-dual-title': '双前端架构', - 'feat-dual-desc': '平台无关的核心引擎 + 独立的 Standalone 和 RetroArch 前端,共享 100% 模拟逻辑。', - 'feat-app-title': '.app 容器加载', - 'feat-app-desc': '解析 Dingoo 专有的 .app 游戏容器格式,包含 CCDL、IMPT、EXPT、RAWD 等数据块。', - 'feat-audio-title': 'PCM 音频', - 'feat-audio-desc': '16-bit PCM 音频输出,支持 22050 Hz 采样率,完整还原掌机音效。', - 'feat-retro-title': 'RetroArch 核心', - 'feat-retro-desc': '完整的 libretro 核心,支持 RetroPad 映射、核心选项、即时存档等 RetroArch 生态功能。', - // gallery - 'gallery-title': '游戏库', - 'gallery-subtitle': '支持 Dingoo A320 原生游戏和多种模拟器', - // architecture - 'arch-title': '技术架构', - 'arch-subtitle': '清晰的三层架构,平台无关的核心引擎', - 'arch-frontends': '前端', - 'arch-standalone': 'dingooemu', - 'arch-standalone-sub': 'Standalone 可执行文件
minifb 窗口', - 'arch-libretro': 'dingooemu-libretro', - 'arch-libretro-sub': 'libretro cdylib
RetroArch 核心', - 'arch-core': '核心引擎', - 'arch-core-sub': '平台无关的库', - 'arch-cpu': 'MIPS CPU / JIT', - 'arch-platforms': '目标平台', - // quickstart - 'qs-title': '快速开始', - 'qs-subtitle': '几行命令,即刻体验', - 'qs-standalone': 'Standalone', - 'qs-standalone-1': '下载最新版本', - 'qs-standalone-1-sub': '从 Releases 页面下载对应平台的二进制文件', - 'qs-standalone-2': '运行游戏', - 'qs-standalone-3': '或从源码编译', - 'qs-retro': 'RetroArch', - 'qs-retro-1': '下载 libretro 核心', - 'qs-retro-1-sub': '从 Releases 页面下载对应平台的核心文件', - 'qs-retro-2': '安装核心', - 'qs-retro-2-sub': '复制到 RetroArch 的 cores/ 目录', - 'qs-retro-3': '加载核心并启动', - 'qs-build': '从源码编译', - 'qs-build-1': '克隆仓库', - 'qs-build-2': '编译 Standalone', - 'qs-build-3': '或编译 RetroArch 核心', - // footer - 'footer-desc': '用 Rust 编写的 Dingoo A320 掌机模拟器', - 'footer-project': '项目', - 'footer-contributing': '贡献指南', - 'footer-community': '社区', - 'footer-docs': '文档', - 'footer-cli': '独立模拟器', - 'footer-core': 'RetroArch Core', - 'footer-gamelist': '游戏兼容性', - 'footer-copy': 'BSD 3-Clause License © 2025 Aloys. Built with 🦀 Rust.' - }, - en: { - // meta - 'meta-title': 'DingooEmu — Dingoo A320 Handheld Emulator', - 'meta-desc': 'A Dingoo A320 handheld emulator written in Rust with MIPS32 CPU emulation, 64-bit Android JIT acceleration, Dingoo SDK HLE, and a RetroArch core', - // nav - 'nav-features': 'Features', - 'nav-games': 'Games', - 'nav-arch': 'Architecture', - 'nav-quickstart': 'Quick Start', - // hero - 'hero-subtitle': 'Relive Classic Handheld Gaming', - 'hero-desc': 'A Dingoo A320 handheld emulator written in Rust, fully supporting Ingenic JZ4740 MIPS SoC and Dingoo SDK', - 'hero-download': 'Download', - 'hero-github': 'View Source', - 'hero-scroll': 'Scroll to explore', - // about - 'about-title': 'What is Dingoo A320?', - 'about-p1': 'Dingoo A320 is a portable gaming handheld powered by the Ingenic JZ4740 MIPS processor, released in 2009. It features a 2.8-inch TFT screen with 320×240 resolution and supports native games and various emulators.', - 'about-p2': 'Through DingooEmu, these classic handheld games can be experienced again on modern devices.', - 'about-chip': 'Ingenic JZ4740', - 'about-chip-sub': 'MIPS32 XBurst @ 336MHz', - 'about-game': '.app Container', - 'about-game-sub': 'Dingoo Native Games', - 'about-emu-sub': 'Rust Emulator', - // stats - 'stat-games': 'Game Format', - 'stat-games-sub': '.app container format', - 'stat-opcodes': 'MIPS Instructions', - 'stat-opcodes-sub': 'MIPS32 XBurst architecture', - 'stat-platforms': 'Platforms', - 'stat-platforms-sub': 'Windows / macOS / Linux / Android', - 'stat-lines': 'SDK HLE Functions', - 'stat-lines-sub': 'Complete Dingoo SDK emulation', - // features - 'feat-title': 'Core Features', - 'feat-subtitle': 'From MIPS CPU to Dingoo SDK — full-stack Rust implementation', - 'feat-mips-title': 'MIPS32 CPU & JIT', - 'feat-mips-desc': 'A cached interpreter runs on every platform, while 64-bit Android can translate hot MIPS32 blocks to native code with precise fallback for unsupported paths.', - 'feat-sdk-title': 'Dingoo SDK HLE', - 'feat-sdk-desc': 'High-level emulation of Dingoo native SDK including graphics rendering, input handling, audio playback, filesystem, and multi-task scheduling.', - 'feat-dual-title': 'Dual Frontend', - 'feat-dual-desc': 'A platform-independent core engine with separate Standalone and RetroArch frontends, sharing 100% of the emulation logic.', - 'feat-app-title': '.app Container Loading', - 'feat-app-desc': 'Parse Dingoo\'s proprietary .app game container format with CCDL, IMPT, EXPT, RAWD data blocks.', - 'feat-audio-title': 'PCM Audio', - 'feat-audio-desc': '16-bit PCM audio output at 22050 Hz sample rate, faithfully reproducing handheld sound effects.', - 'feat-retro-title': 'RetroArch Core', - 'feat-retro-desc': 'A complete libretro core with RetroPad mapping, core options, save states, and the full RetroArch ecosystem.', - // gallery - 'gallery-title': 'Game Library', - 'gallery-subtitle': 'Supporting Dingoo A320 native games and various emulators', - // architecture - 'arch-title': 'Architecture', - 'arch-subtitle': 'Clean three-layer architecture with a platform-independent core engine', - 'arch-frontends': 'Frontends', - 'arch-standalone': 'dingooemu', - 'arch-standalone-sub': 'Standalone binary · minifb window', - 'arch-libretro': 'dingooemu-libretro', - 'arch-libretro-sub': 'libretro cdylib · RetroArch core', - 'arch-core': 'Core Engine', - 'arch-core-sub': 'Platform-independent library', - 'arch-cpu': 'MIPS CPU / JIT', - 'arch-platforms': 'Platforms', - // quickstart - 'qs-title': 'Quick Start', - 'qs-subtitle': 'A few commands to get started', - 'qs-standalone': 'Standalone', - 'qs-standalone-1': 'Download latest release', - 'qs-standalone-1-sub': 'Get the binary for your platform from the Releases page', - 'qs-standalone-2': 'Run a game', - 'qs-standalone-3': 'Or build from source', - 'qs-retro': 'RetroArch', - 'qs-retro-1': 'Download libretro core', - 'qs-retro-1-sub': 'Get the core for your platform from the Releases page', - 'qs-retro-2': 'Install the core', - 'qs-retro-2-sub': 'Copy to RetroArch\'s cores/ directory', - 'qs-retro-3': 'Load core and start', - 'qs-build': 'Build from Source', - 'qs-build-1': 'Clone the repository', - 'qs-build-2': 'Build Standalone', - 'qs-build-3': 'Or build RetroArch core', - // footer - 'footer-desc': 'A Dingoo A320 handheld emulator written in Rust', - 'footer-project': 'Project', - 'footer-contributing': 'Contributing', - 'footer-community': 'Community', - 'footer-docs': 'Docs', - 'footer-cli': 'Standalone Emulator', - 'footer-core': 'RetroArch Core', - 'footer-gamelist': 'Game Compatibility', - 'footer-copy': 'BSD 3-Clause License © 2025 Aloys. Built with 🦀 Rust.' - } - }; - - var currentLang = localStorage.getItem('dingoo-lang') || (navigator.language.startsWith('zh') ? 'zh' : 'en'); - - // ================================================================ - // i18n — Apply translations - // ================================================================ - function applyLang(lang) { - currentLang = lang; - localStorage.setItem('dingoo-lang', lang); - document.documentElement.lang = lang === 'zh' ? 'zh-CN' : 'en'; - - var t = translations[lang]; - - // Update text content for elements with data-i18n - document.querySelectorAll('[data-i18n]').forEach(function (el) { - var key = el.getAttribute('data-i18n'); - if (t[key] === undefined) return; - // Skip title/meta — handled separately below - if (el.tagName === 'TITLE' || el.tagName === 'META') return; - el.innerHTML = t[key]; - }); - - // Update and meta description - if (t['meta-title']) document.title = t['meta-title']; - var metaDesc = document.querySelector('meta[name="description"]'); - if (metaDesc && t['meta-desc']) metaDesc.setAttribute('content', t['meta-desc']); - - // Update language toggle button text - var langBtn = document.getElementById('lang-toggle'); - if (langBtn) langBtn.textContent = lang === 'zh' ? 'EN' : '中'; - - // Rebuild gallery with correct language - buildGallery(); - } - - // ================================================================ - // GALLERY — Tab + Carousel - // ================================================================ - var currentTab = 'all'; - - function getCatCount(catId) { - if (catId === 'all') return GAMES.length; - return GAMES.filter(function (g) { return g.cat === catId; }).length; - } - - function buildGallery() { - var container = document.getElementById('gallery-dynamic'); - if (!container) return; - - var lang = currentLang; - var html = ''; - - // Tab bar - html += '<div class="gallery-tabs">'; - CATEGORIES.forEach(function (cat) { - var count = getCatCount(cat.id); - var label = lang === 'zh' ? cat.zh : cat.en; - var active = cat.id === currentTab ? ' active' : ''; - html += '<button class="gallery-tab' + active + '" data-cat="' + cat.id + '">' + label + ' (' + count + ')</button>'; - }); - html += '</div>'; - - // Carousel for each category (only show active tab) - CATEGORIES.forEach(function (cat) { - if (cat.id !== currentTab) return; - var games = cat.id === 'all' ? GAMES : GAMES.filter(function (g) { return g.cat === cat.id; }); - var catLabel = lang === 'zh' ? cat.zh : cat.en; - - html += '<div class="carousel-wrapper">'; - html += '<button class="carousel-btn carousel-prev" aria-label="Previous">‹</button>'; - html += '<div class="carousel-viewport">'; - html += '<div class="carousel-track" data-cat="' + cat.id + '">'; - - games.forEach(function (g, i) { - var name = lang === 'zh' ? g.zh + ' ' + g.en : g.en; - var desc = lang === 'zh' ? g.descZh : g.descEn; - html += '<div class="carousel-card">'; - html += ' <img src="' + g.img + '" alt="' + g.en + '" loading="lazy">'; - html += ' <div class="carousel-card-overlay">'; - html += ' <span class="gallery-tag">' + catLabel + '</span>'; - html += ' <h4>' + name + '</h4>'; - html += ' <p>' + desc + '</p>'; - html += ' </div>'; - html += '</div>'; - }); - - html += '</div>'; - html += '</div>'; - html += '<button class="carousel-btn carousel-next" aria-label="Next">›</button>'; - - // Dots - var cardsPerView = window.innerWidth > 768 ? 4 : (window.innerWidth > 480 ? 2 : 1); - var totalPages = Math.ceil(games.length / cardsPerView); - html += '<div class="carousel-dots">'; - for (var d = 0; d < totalPages; d++) { - html += '<span class="carousel-dot' + (d === 0 ? ' active' : '') + '" data-page="' + d + '"></span>'; - } - html += '</div>'; - - html += '</div>'; - }); - - container.innerHTML = html; - - // Bind tab clicks - container.querySelectorAll('.gallery-tab').forEach(function (tab) { - tab.addEventListener('click', function () { - currentTab = tab.getAttribute('data-cat'); - buildGallery(); - }); - }); - - // Bind carousel controls - initCarousel(); - } - - function initCarousel() { - document.querySelectorAll('.carousel-wrapper').forEach(function (wrapper) { - var viewport = wrapper.querySelector('.carousel-viewport'); - var track = wrapper.querySelector('.carousel-track'); - var prevBtn = wrapper.querySelector('.carousel-prev'); - var nextBtn = wrapper.querySelector('.carousel-next'); - var dots = wrapper.querySelectorAll('.carousel-dot'); - if (!viewport || !track) return; - - var page = 0; - - function getCardsPerView() { - return window.innerWidth > 768 ? 4 : (window.innerWidth > 480 ? 2 : 1); - } - - function getTotalPages() { - var cards = track.querySelectorAll('.carousel-card'); - return Math.ceil(cards.length / getCardsPerView()); - } - - function goTo(p) { - var total = getTotalPages(); - page = Math.max(0, Math.min(p, total - 1)); - var cpv = getCardsPerView(); - var card = track.querySelector('.carousel-card'); - var gap = parseFloat(window.getComputedStyle(track).columnGap) || 0; - var pageWidth = card ? cpv * (card.offsetWidth + gap) : viewport.offsetWidth; - var maxOffset = Math.max(0, track.scrollWidth - viewport.clientWidth); - var offset = Math.min(page * pageWidth, maxOffset); - track.style.transform = 'translateX(-' + offset + 'px)'; - - dots.forEach(function (d, i) { - d.classList.toggle('active', i === page); - }); - } - - if (prevBtn) prevBtn.addEventListener('click', function () { goTo(page - 1); }); - if (nextBtn) nextBtn.addEventListener('click', function () { goTo(page + 1); }); - - dots.forEach(function (dot) { - dot.addEventListener('click', function () { - goTo(parseInt(dot.getAttribute('data-page'), 10)); - }); - }); - - // Touch/swipe support - var startX = 0; - var isDragging = false; - viewport.addEventListener('touchstart', function (e) { - startX = e.touches[0].clientX; - isDragging = true; - }, { passive: true }); - viewport.addEventListener('touchend', function (e) { - if (!isDragging) return; - isDragging = false; - var diff = startX - e.changedTouches[0].clientX; - if (Math.abs(diff) > 50) { - goTo(page + (diff > 0 ? 1 : -1)); - } - }, { passive: true }); - }); - } - - // ================================================================ - // NAVBAR — Scroll effect - // ================================================================ - var navbar = document.getElementById('navbar'); - - function onScroll() { - navbar.classList.toggle('scrolled', window.scrollY > 50); - } - - window.addEventListener('scroll', onScroll, { passive: true }); - onScroll(); - - // ---- Mobile nav toggle ---- - var toggle = document.querySelector('.nav-toggle'); - var navLinks = document.querySelector('.nav-links'); - - if (toggle && navLinks) { - toggle.addEventListener('click', function () { - navLinks.classList.toggle('open'); - }); - navLinks.querySelectorAll('a').forEach(function (a) { - a.addEventListener('click', function () { navLinks.classList.remove('open'); }); - }); - } - - // ================================================================ - // SCROLL REVEAL — Intersection Observer - // ================================================================ - var fadeEls = document.querySelectorAll('.fade-in-up'); - - if ('IntersectionObserver' in window) { - var observer = new IntersectionObserver(function (entries) { - entries.forEach(function (entry) { - if (entry.isIntersecting) { - entry.target.classList.add('visible'); - observer.unobserve(entry.target); - } - }); - }, { threshold: 0.1, rootMargin: '0px 0px -40px 0px' }); - - fadeEls.forEach(function (el) { observer.observe(el); }); - } else { - fadeEls.forEach(function (el) { el.classList.add('visible'); }); - } - - // ================================================================ - // ANIMATED COUNTER - // ================================================================ - var statNumbers = document.querySelectorAll('.stat-number[data-target]'); - - function animateCounter(el) { - var target = parseInt(el.dataset.target, 10); - var suffix = el.dataset.suffix || ''; - var duration = 1800; - var start = performance.now(); - - function tick(now) { - var elapsed = now - start; - var progress = Math.min(elapsed / duration, 1); - var eased = 1 - Math.pow(1 - progress, 3); - var current = Math.round(eased * target); - el.textContent = current.toLocaleString() + suffix; - if (progress < 1) requestAnimationFrame(tick); - } - - requestAnimationFrame(tick); - } - - if ('IntersectionObserver' in window) { - var statObserver = new IntersectionObserver(function (entries) { - entries.forEach(function (entry) { - if (entry.isIntersecting) { - animateCounter(entry.target); - statObserver.unobserve(entry.target); - } - }); - }, { threshold: 0.5 }); - - statNumbers.forEach(function (el) { statObserver.observe(el); }); - } else { - statNumbers.forEach(function (el) { animateCounter(el); }); - } - - // ================================================================ - // PIXEL CANVAS — Hero background with retro game pattern - // ================================================================ - var canvas = document.getElementById('pixel-canvas'); - if (canvas && canvas.getContext) { - var ctx = canvas.getContext('2d'); - var w, h, pixels; - var PIXEL_COUNT = 80; - var LINE_DIST = 100; - - function resize() { - w = canvas.width = canvas.offsetWidth; - h = canvas.height = canvas.offsetHeight; - } - - function initPixels() { - pixels = []; - for (var i = 0; i < PIXEL_COUNT; i++) { - pixels.push({ - x: Math.random() * w, - y: Math.random() * h, - vx: (Math.random() - 0.5) * 0.3, - vy: (Math.random() - 0.5) * 0.3, - size: Math.random() * 3 + 1, - color: Math.random() > 0.5 ? 'rgba(0, 212, 255, 0.4)' : 'rgba(255, 107, 53, 0.4)' - }); - } - } - - function draw() { - ctx.clearRect(0, 0, w, h); - - // Draw grid lines for retro feel - ctx.strokeStyle = 'rgba(0, 212, 255, 0.03)'; - ctx.lineWidth = 0.5; - for (var gx = 0; gx < w; gx += 40) { - ctx.beginPath(); - ctx.moveTo(gx, 0); - ctx.lineTo(gx, h); - ctx.stroke(); - } - for (var gy = 0; gy < h; gy += 40) { - ctx.beginPath(); - ctx.moveTo(0, gy); - ctx.lineTo(w, gy); - ctx.stroke(); - } - - // Draw connections - for (var i = 0; i < pixels.length; i++) { - for (var j = i + 1; j < pixels.length; j++) { - var dx = pixels[i].x - pixels[j].x; - var dy = pixels[i].y - pixels[j].y; - var dist = Math.sqrt(dx * dx + dy * dy); - if (dist < LINE_DIST) { - var alpha = (1 - dist / LINE_DIST) * 0.3; - ctx.strokeStyle = 'rgba(0, 212, 255, ' + alpha + ')'; - ctx.lineWidth = 0.5; - ctx.beginPath(); - ctx.moveTo(pixels[i].x, pixels[i].y); - ctx.lineTo(pixels[j].x, pixels[j].y); - ctx.stroke(); - } - } - } - - // Draw pixels - pixels.forEach(function (p) { - ctx.fillStyle = p.color; - ctx.fillRect(p.x, p.y, p.size, p.size); - - p.x += p.vx; - p.y += p.vy; - - if (p.x < 0) p.x = w; - if (p.x > w) p.x = 0; - if (p.y < 0) p.y = h; - if (p.y > h) p.y = 0; - }); - - requestAnimationFrame(draw); - } - - resize(); - initPixels(); - draw(); - - window.addEventListener('resize', function () { - resize(); - initPixels(); - }); - } - - // ================================================================ - // SMOOTH SCROLL - // ================================================================ - document.querySelectorAll('a[href^="#"]').forEach(function (anchor) { - anchor.addEventListener('click', function (e) { - var target = document.querySelector(anchor.getAttribute('href')); - if (target) { - e.preventDefault(); - target.scrollIntoView({ behavior: 'smooth', block: 'start' }); - } - }); - }); - - // ================================================================ - // LANGUAGE TOGGLE - // ================================================================ - var langBtn = document.getElementById('lang-toggle'); - if (langBtn) { - langBtn.addEventListener('click', function () { - applyLang(currentLang === 'zh' ? 'en' : 'zh'); - }); - } - - // ================================================================ - // INIT - // ================================================================ - applyLang(currentLang); - buildGallery(); - - // Re-init scroll reveal for dynamically added elements - if ('IntersectionObserver' in window) { - var revealObserver = new IntersectionObserver(function (entries) { - entries.forEach(function (entry) { - if (entry.isIntersecting) { - entry.target.classList.add('visible'); - revealObserver.unobserve(entry.target); - } - }); - }, { threshold: 0.1, rootMargin: '0px 0px -40px 0px' }); - - // Observe new elements after gallery build - setTimeout(function () { - document.querySelectorAll('.fade-in-up:not(.visible)').forEach(function (el) { - revealObserver.observe(el); - }); - }, 100); - } - -})(); diff --git a/native/DingooEmu/sonar-project.properties b/native/DingooEmu/sonar-project.properties deleted file mode 100644 index d38ca3a..0000000 --- a/native/DingooEmu/sonar-project.properties +++ /dev/null @@ -1,9 +0,0 @@ -sonar.projectKey=AloysHF_DingooEmu -sonar.projectName=DingooEmu -sonar.organization=aloyshf-github - -sonar.sources=crates -sonar.exclusions=**/target/** -sonar.sourceEncoding=UTF-8 - -sonar.rust.cobertura.reportPaths=coverage/cobertura.xml diff --git a/scripts/build-core.ps1 b/scripts/build-core.ps1 index 435db38..3665b6a 100644 --- a/scripts/build-core.ps1 +++ b/scripts/build-core.ps1 @@ -1,6 +1,13 @@ $ErrorActionPreference = "Stop" $ProjectDir = (Resolve-Path (Join-Path $PSScriptRoot "..")).Path +$CoreDir = Join-Path $ProjectDir "native\DingooEmu" +$CoreManifest = Join-Path $CoreDir "Cargo.toml" + +if (-not (Test-Path -LiteralPath $CoreManifest)) { + throw "DingooEmu submodule is not initialized. Run: git submodule update --init --recursive" +} + if (-not $env:ANDROID_NDK_HOME) { throw "ANDROID_NDK_HOME must point to Android NDK 28.2.13676358." } @@ -11,7 +18,7 @@ if (-not (Get-Command cargo-ndk -ErrorAction SilentlyContinue)) { throw "cargo-ndk is required." } -Push-Location (Join-Path $ProjectDir "native\DingooEmu") +Push-Location $CoreDir try { $PreviousRustFlags = $env:RUSTFLAGS $env:RUSTFLAGS = "$PreviousRustFlags -C link-arg=-Wl,-z,max-page-size=16384".Trim() diff --git a/scripts/build-core.sh b/scripts/build-core.sh index 974845c..fcfcd93 100644 --- a/scripts/build-core.sh +++ b/scripts/build-core.sh @@ -3,6 +3,12 @@ set -euo pipefail script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" project_dir="$(cd "$script_dir/.." && pwd)" +core_dir="$project_dir/native/DingooEmu" + +if [[ ! -f "$core_dir/Cargo.toml" ]]; then + echo "DingooEmu submodule is not initialized. Run: git submodule update --init --recursive" >&2 + exit 1 +fi if [[ -z "${ANDROID_NDK_HOME:-}" ]]; then echo "ANDROID_NDK_HOME must point to Android NDK 28.2.13676358." >&2 @@ -12,7 +18,7 @@ fi command -v cargo >/dev/null || { echo "cargo is required." >&2; exit 1; } command -v cargo-ndk >/dev/null || { echo "cargo-ndk is required." >&2; exit 1; } -cd "$project_dir/native/DingooEmu" +cd "$core_dir" RUSTFLAGS="${RUSTFLAGS:-} -C link-arg=-Wl,-z,max-page-size=16384" \ cargo ndk \ -t arm64-v8a \