Skip to content

Latest commit

 

History

History
139 lines (123 loc) · 10.7 KB

File metadata and controls

139 lines (123 loc) · 10.7 KB

Wabbitemu Multiplatform Migration Plan

This document outlines the tasks required to refactor the Wabbitemu Android application into a Kotlin Compose Multiplatform app targeting iOS and Desktop. The C code will be kept as-is and compiled natively for each platform.

The plan is structured as a series of sequential phases (the outer lists). Within each phase, the inner lists represent parallelable tasks, where each task is scoped to be an appropriate size for a single Pull Request (PR).

Phase 1: Project Restructuring & Build System Modernization

These tasks lay the foundation for Kotlin Multiplatform (KMP) by updating the build system and separating the existing Android app into its own module.

  • 1.1. Migrate Build Scripts to Kotlin DSL:
    • Migrate the root build.gradle to build.gradle.kts.
    • Migrate app/build.gradle to app/build.gradle.kts.
    • Migrate settings.gradle to settings.gradle.kts.
  • 1.2. Implement Gradle Version Catalog:
    • Create gradle/libs.versions.toml.
    • Move all existing dependencies and plugin versions from build scripts to the version catalog.
    • Update all build.gradle.kts files to use the version catalog.
  • 1.3. Extract Android Application Module:
    • Create a new module directory (e.g., androidApp).
    • Move the existing app/src directory, AndroidManifest.xml, and Android-specific resources into androidApp/src/main.
    • Update settings.gradle.kts to include :androidApp instead of :app.
  • 1.4. Initialize Shared KMP Module:
    • Create a new module directory shared.
    • Configure shared/build.gradle.kts with the Kotlin Multiplatform plugin.
    • Define targets for android, iosX64, iosArm64, iosSimulatorArm64, and jvm (Desktop).
    • Set up the basic source sets (commonMain, androidMain, iosMain, desktopMain).
    • Update settings.gradle.kts to include :shared and add the shared module as a dependency to androidApp.

Phase 2: Decoupling and Compiling Native C/C++ Code

The core emulator logic written in C/C++ must be decoupled from the Android JNI bindings (wabbitemujni.c) so it can be compiled and linked on all target platforms.

  • 2.1. Isolate Core C Logic:
    • Create a new directory structure (e.g., shared/src/nativeInterop/cinterop/wabbitemu).
    • Move the core emulator files (core/, hardware/, utilities/) into this directory.
    • Leave wabbitemujni.c and wabbitemujni.h in the androidApp module (or an Android-specific native folder) since they contain JNI specifics.
  • 2.2. Configure iOS Static Library Build:
    • Create an Xcode project or a build script (e.g., using CMake or raw Clang invocations) to compile the core C files into an iOS static library (.a).
    • Ensure the build script handles all required iOS architectures (arm64, x86_64).
    • Verify the library compiles successfully on macOS.
  • 2.3. Configure Desktop Dynamic Library Build:
    • Create a CMakeLists.txt in the native core directory specifically for Desktop builds.
    • Configure CMake to compile the core C files into a dynamic library (.dylib for macOS, .dll for Windows, .so for Linux).
    • Verify the library compiles successfully on a desktop environment.
  • 2.4. Generate Kotlin Native Bindings (cinterop):
    • Create a .def file (e.g., wabbitemu.def) in shared/src/nativeInterop/cinterop/.
    • Configure the .def file to point to the core C headers (e.g., calc.h, state.h).
    • Update shared/build.gradle.kts to configure the cinterop task for all iOS targets, ensuring Kotlin Native can generate bindings for the C code.

Phase 3: Multiplatform Emulator Core API (expect/actual)

This phase bridges the gap between the Kotlin code and the underlying C code on each platform using Kotlin's expect/actual mechanism.

  • 3.1. Define Shared Interface:
    • In shared/src/commonMain/kotlin/io/github/angelsl/wabbitemu/calc/, create CalcInterface.kt.
    • Define expect class CalcInterface mirroring the static methods currently in the Java CalcInterface.
  • 3.2. Implement Android JNI Bridge:
    • In shared/src/androidMain/kotlin/io/github/angelsl/wabbitemu/calc/, implement the actual class CalcInterface.
    • Map the actual methods to the existing JNI calls defined in wabbitemujni.c.
    • Ensure the Wabbitemu native library is loaded correctly in the Android target.
  • 3.3. Implement iOS cinterop Bridge:
    • In shared/src/iosMain/kotlin/io/github/angelsl/wabbitemu/calc/, implement the actual class CalcInterface.
    • Map the actual methods to the Kotlin Native bindings generated by cinterop (from Phase 2).
    • Handle C-pointers and memory management manually where necessary using kotlinx.cinterop.
  • 3.4. Implement Desktop JNI/JNA Bridge:
    • In shared/src/desktopMain/kotlin/io/github/angelsl/wabbitemu/calc/, implement the actual class CalcInterface.
    • Choose between JNI (writing new C++ wrappers for the Desktop dynamic library) or JNA (Java Native Access) to interface with the dynamic library built in Phase 2.
    • Map the actual methods to the chosen bridge.

Phase 4: Abstracting Business Logic & Platform Dependencies

The core application logic (CalculatorManager, CalcThread, etc.) needs to be moved to the shared module, which requires abstracting away Android-specific APIs.

  • 4.1. Abstract Logging:
    • In shared/src/commonMain/, create an expect class Logger.
    • Implement actual class Logger using android.util.Log in androidMain.
    • Implement actual class Logger using NSLog or os_log in iosMain.
    • Implement actual class Logger using standard output (println) or a JVM logging framework in desktopMain.
    • Replace all Android Log.* calls in the core logic with the new Logger.
  • 4.2. Abstract SharedPreferences:
    • Add a multiplatform settings library (e.g., com.russhwolf:multiplatform-settings) to the version catalog and shared build script.
    • Refactor the code to use this library instead of Android's SharedPreferences.
  • 4.3. Abstract File System Access:
    • Create an expect class FileSystem in commonMain for reading/writing files and handling paths (e.g., for ROMs and savestates).
    • Implement the actual class in androidMain using Android's Context/FilesDir.
    • Implement the actual class in iosMain using NSFileManager.
    • Implement the actual class in desktopMain using java.io.File or java.nio.file.
    • Refactor the native C code (or the Kotlin bridge) to accept raw file bytes or standardized paths instead of Android Uris where applicable.
  • 4.4. Migrate Core Logic to Shared Module:
    • Move CalculatorManager.java (converting to Kotlin), CalcThread.java, MainThread.java, and related models to shared/src/commonMain.
    • Refactor CalcThread and MainThread to use Kotlin Coroutines (kotlinx.coroutines) instead of Java Threads, ensuring thread safety and platform compatibility.

Phase 5: Compose Multiplatform Shared UI Foundation

This phase focuses on rebuilding the UI using Compose Multiplatform, replacing the Android XML layouts and Views.

  • 5.1. Setup Compose Multiplatform:
    • Add Compose Multiplatform plugins and dependencies to the version catalog and the shared build script.
    • Define a shared Theme.kt (Typography, Colors, Shapes) in shared/src/commonMain.
  • 5.2. Migrate Navigation and Main Layout:
    • Implement a shared navigation system (e.g., using Voyager or a custom state-based router) in commonMain.
    • Create the main App scaffolding (Drawer, TopAppBar) in Compose.
  • 5.3. Migrate the Setup Wizard UI:
    • Port the LandingPageView, ChooseOsPageView, ModelPageView, and OsDownloadPageView from Android XML to Compose UI in commonMain.
    • Connect these new Compose screens to the abstracted CalculatorManager and file system.
  • 5.4. Migrate Settings and About Screens:
    • Port the SettingsActivity and AboutActivity to Compose UI in commonMain.
  • 5.5. Abstract File Picker UI:
    • Create an expect fun openFilePicker() in commonMain.
    • Implement the actual function in androidMain using Android Intents (ACTION_GET_CONTENT).
    • Implement the actual function in iosMain using UIDocumentPickerViewController.
    • Implement the actual function in desktopMain using a Swing JFileChooser or JavaFX FileChooser.

Phase 6: Emulator Canvas & UI Interactivity

The most complex UI component is the emulator screen and the interactive calculator skin.

  • 6.1. Port the Emulator LCD Renderer:
    • Replace WabbitLCD.java (which uses Android SurfaceView) with a Compose Canvas in commonMain.
    • Implement logic to receive the raw ARGB pixel buffer from the CalcInterface and draw it onto the Compose Canvas efficiently (e.g., using ImageBitmap).
  • 6.2. Port the Calculator Skin Renderer:
    • Refactor SkinBitmapLoader.java and CalcSkin.java into Compose UI.
    • Load the skin images as Compose ImageBitmaps.
    • Implement dynamic layout logic to position the skin and the LCD canvas correctly based on the device screen size and constraints.
  • 6.3. Implement Cross-Platform Touch Input:
    • Replace Android's onTouchEvent handling with Compose's Modifier.pointerInput.
    • Map the pointer events (down, move, up) to the corresponding key press/release logic in CalcInterface.
    • Ensure the hitboxes for the calculator buttons scale correctly with the Compose layout.

Phase 7: Platform Assembly & Final Integration

The final phase involves wiring up the entry points for each platform to launch the shared Compose UI.

  • 7.1. Android Entry Point Integration:
    • Update WabbitemuActivity.kt in the androidApp module to launch the shared Compose UI using setContent { App() }.
    • Ensure all necessary Android permissions (e.g., storage) are requested correctly.
  • 7.2. Desktop Entry Point Integration:
    • Create a new module or source set for the desktop application.
    • Implement the main main() function using Compose for Desktop's Window { App() }.
    • Ensure the dynamic libraries built in Phase 2 are bundled and loaded correctly at runtime.
  • 7.3. iOS Entry Point Integration:
    • Create a standard Xcode project for the iOS app.
    • Configure the Xcode project to link against the shared Kotlin framework generated by KMP.
    • Write App.swift (or ViewController.swift) to use ComposeUIViewController to host the shared KMP UI.
    • Ensure the static C library built in Phase 2 is linked correctly in the Xcode build phases.