FreeJ2ME Technical Assessment¶
Task: E3-R001 — Research only
Scope: hex007/freej2me (upstream FreeJ2ME), with notes on related forks
Date: 2026-08-04
CoreJ2 context: Epic 3 Emulator Bridge skeleton already exists (EmulatorBridgeProtocol, DefaultEmulatorBridge, LaunchConfiguration, EmulatorSession)
This document studies how FreeJ2ME works so CoreJ2 can design a clean integration without coupling Library, ImportEngine, Repository, SwiftData, or ViewModels to the runtime.
Source inspected: shallow clone of https://github.com/hex007/freej2me
Related fork (more active packaging / CLI): https://github.com/TASEmulators/freej2me-plus
License: GPL-3.0 (critical product/legal constraint for App Store distribution)
Executive summary¶
FreeJ2ME is a desktop JVM J2ME/MIDP emulator, not a native C/C++ core that draws frames by itself.
| Layer | Role |
|---|---|
javax.microedition.* |
Reimplemented MIDP/CLDC APIs (Java) |
org.recompile.mobile.* |
Platform core: LCD buffer, MIDlet loader, audio, keys |
| Frontends | AWT window, Libretro (pipe + RGB), SDL2 (pipe + RGB) |
| Vendored ASM | Bytecode rewrite when loading MIDlet classes |
Best integration reference for CoreJ2: the Libretro / SDL “painter → RGB framebuffer” path, not the AWT Frame UI.
Hardest constraint for iOS: FreeJ2ME assumes a full Java SE runtime (java.awt, javax.sound.sampled, java.io.File, reflection, ASM defineClass). App Store iOS cannot simply host HotSpot + AWT. Integration therefore requires either:
- an embedded/alternative Java runtime strategy (technically and policy-constrained), or
- a platform-port where
MobilePlatform/ graphics / audio backends are replaced with iOS-native implementations while keeping MIDP APIs + MIDlet loading logic as intact as possible.
1. Overall architecture¶
1.1 Main modules¶
src/
├── org/recompile/freej2me/ # Frontends + config UI
│ ├── FreeJ2ME.java # AWT standalone main
│ ├── Libretro.java # Libretro “BIOS” jar (stdin/stdout protocol)
│ ├── Anbu.java # SDL2 frontend (Java side)
│ ├── Config.java # Per-game settings overlay
│ └── ScreenShot.java
├── org/recompile/mobile/ # Runtime core
│ ├── Mobile.java # Global accessors (Display, Platform, key constants)
│ ├── MobilePlatform.java # LCD, loadJar/runJar, key/pointer, painter hook
│ ├── MIDletLoader.java # URLClassLoader + manifest + ASM rewrite + startApp
│ ├── PlatformGraphics.java # Draws into BufferedImage LCD
│ ├── PlatformImage.java
│ ├── PlatformPlayer.java # MMAPI Player → Java Sound / MIDI
│ └── PlatformFont.java
├── javax/microedition/... # MIDP / RMS / Media / M3G / IO stubs & impls
├── org/objectweb/asm/ # Vendored ASM for class rewriting
├── libretro/ # Native C libretro core (spawns `java -jar`)
└── sdl2/ # Native SDL helper (frame/input IPC)
Build (build.xml) produces three jars:
| Artifact | Main-Class | Purpose |
|---|---|---|
freej2me.jar |
FreeJ2ME |
Desktop AWT UI |
freej2me-lr.jar |
Libretro |
Libretro companion jar |
freej2me-sdl.jar |
Anbu |
SDL2 companion jar |
1.2 Startup flow (conceptual)¶
Frontend main(args)
→ Mobile.setPlatform(new MobilePlatform(width, height))
→ setPainter(Runnable) // how LCD pixels leave the core
→ MobilePlatform.loadJar(url) // MIDletLoader(URLClassLoader)
→ optional Config.init()
→ MobilePlatform.runJar() // reflective MIDlet.startApp()
→ MIDlet draws via Canvas/GameCanvas
→ flushGraphics/repaint → PlatformGraphics → painter.run()
1.3 Runtime lifecycle¶
- Construct platform — allocate LCD
PlatformImage+PlatformGraphics, createGraphics3D. - Attach frontend painter — AWT paints a
Canvas; Libretro/SDL push RGB bytes. - Load MIDlet — parse manifest, rewrite classes, instantiate MIDlet.
- Start — invoke
startApp()(and later pause/destroy via MIDlet APIs as games request). - Event + draw loop — game/timer threads call
repaint/flushGraphics;Display.callSeriallyruns on aTimer(~17 ms). - Teardown — AWT exits process on window close; Libretro/SDL depend on host process lifetime. There is no clean multi-session API designed for embedding inside another app.
2. Launch process¶
2.1 How a JAR is opened¶
MobilePlatform.loadJar(String jarurl):
Expected URL form: file:///absolute/path/to/game.jar
(Windows needs the extra slash style documented in README.)
MIDletLoader:
- Extends
URLClassLoader - Reads
META-INF/MANIFEST.MF(several case variants) - Extracts
MIDlet-Name, icon, main class fromMIDlet-1/ related attributes - Sets system/app properties (
microedition.platform,MIDP-2.0, etc.)
2.2 Where execution begins¶
MobilePlatform.runJar() → loader.start():
loadClass(mainClassName)(with ASM rewrite infindClasspath)- Reflective no-arg constructor →
MIDletinstance MIDlet.initAppProperties(properties)- Reflective
startApp()invoke
That is the true “game start” entry — not a FreeJ2ME-specific game loop owned by the frontend.
2.3 Required launch parameters¶
AWT (FreeJ2ME) CLI:
Defaults if omitted: LCD 240×320, file dialog if no jar arg.
Libretro jar receives structured numeric args from the native core (width, height, rotate, phone type, fps, sound, …) then loads the jar via protocol messages over stdin.
SDL (Anbu) expects jar path + width + height (and starts native SDL helper).
Per-game config (Config) can override resolution/sound/phone/rotate/fps; saved under working-directory config paths. Config often wins over CLI.
For CoreJ2 mapping:
| CoreJ2 | FreeJ2ME |
|---|---|
InstalledGame.jarURL |
file:// URL into loadJar |
Future LaunchConfiguration size |
MobilePlatform width/height |
| Future phone profile | Nokia/Siemens/Motorola flags in Mobile / Config |
| Future sound toggle | Mobile.sound |
| App Documents sandbox | MobilePlatform.dataPath for RMS/config |
3. Rendering¶
3.1 How frames are produced¶
- MIDlet draws into a per-
Canvas/DisplayablePlatformImage. Canvas.repaint/GameCanvas.flushGraphics/Display.setCurrentpath calls:
MobilePlatform.flushGraphics / repaint
→ PlatformGraphics.flushGraphics(...) // blit into LCD BufferedImage
→ painter.run() // frontend presents LCD
- LCD pixels live in
MobilePlatform.getLCD()→BufferedImage(PlatformImage.getCanvas()).
3.2 Rendering backends¶
| Frontend | Backend |
|---|---|
| AWT | java.awt Frame + Canvas.paint draws getLCD() |
| Libretro | Painter copies LCD into an internal BufferedImage; native core consumes RGB frames via IPC; jar prints +READY and speaks a binary stdin protocol |
| SDL2 | Painter extracts getRGB(...) → RGB888 byte[] written to SDL process pipe |
There is no OpenGL/Metal in the Java core. Everything is software framebuffer (BufferedImage / int RGB).
3.3 Where an iOS renderer should connect¶
Primary hook: MobilePlatform.setPainter(Runnable).
A future iOS adapter should:
- Own an iOS surface (Metal / CoreAnimation /
UIImage/CGImagepath). - Install a painter that reads
getLCD()pixel data (ARGB int buffer). - Upload/convert to a GPU texture or
CGImageon the render thread.
Do not depend on AWT FreeJ2ME.LCD for iOS.
Libretro/Anbu already prove the correct separation: core draws LCD; frontend only presents pixels.
4. Input¶
4.1 Keyboard mapping¶
Frontends map host keys → Mobile keycodes (Mobile.KEY_NUM*, Nokia softkeys, etc.).
Documented defaults (KEYMAP.md / README):
Q/W— softkeys- Arrows — nav or 2/4/6/8 depending on phone mode
- Number row / numpad — keypad
E/R—*/#- Enter — fire / 5
- Esc — FreeJ2ME config menu (AWT)
Phone profiles change arrow semantics (Standard vs Nokia vs Siemens vs Motorola).
4.2 Pointer / touch¶
AWT mouse listeners convert scaled/rotated coordinates to:
MobilePlatform.pointerPressed/Released/Dragged(x, y)- Forwarded to
Display.getCurrent()(Canvaspointer methods)
4.3 Event dispatch¶
Host input
→ Frontend mapping
→ MobilePlatform.keyPressed/Released/Repeated or pointer*
→ update GameCanvas keyState bitmask (for getKeyStates)
→ Displayable/Canvas key*/pointer* callbacks
Libretro polls stdin on a 1 ms Timer and synthesizes the same MobilePlatform calls.
iOS implication: Map touch / virtual keypad → the same MobilePlatform methods. Never send UIKit events into MIDP types from ViewModels.
5. Audio¶
5.1 Pipeline¶
javax.microedition.media.Manager / Player
→ PlatformPlayer
→ MIDI: javax.sound.midi.Sequencer
→ WAV: javax.sound.sampled.AudioSystem + Clip
→ IMA-ADPCM WAV: WavImaAdpcmDecoder then Clip
→ unsupported types: silent stub player
Gated by Mobile.sound.
5.2 APIs used¶
- Java Sound (
javax.sound.sampled) - Java MIDI (
javax.sound.midi)
5.3 Integration points¶
Replace or wrap PlatformPlayer backends with AVAudioEngine / AudioQueue equivalents while keeping the javax.microedition.media.Player façade.
AMR/MPEG paths are incomplete upstream (“No Player For…” stubs).
6. MIDP runtime¶
6.1 Display¶
javax.microedition.lcdui.Display:
- Singleton wired through
Mobile.setDisplay setCurrent(Displayable)triggers flush of current screen imagecallSerially(Runnable)queued and drained byTimerevery 17 ms (~60 Hz)
6.2 Canvas¶
Abstract Canvas creates a full-screen PlatformImage, implements key game-action mapping, and routes repaint to MobilePlatform.repaint.
6.3 GameCanvas¶
Extends Canvas; flushGraphics copies the offscreen buffer through MobilePlatform.flushGraphics (immediate present via painter).
getKeyStates() reads MobilePlatform.keyState bitmask updated in updateKeyState.
6.4 Event loop¶
There is no single global FreeJ2ME game loop.
- MIDlets use their own
Threads /Timers /Display.callSerially - Frontends may sleep in paint for FPS limit (
Thread.sleepwhenlimitFPS > 0) - Libretro IO timer is separate (input pump)
CoreJ2 must assume multiple Java threads calling into painter and audio.
7. Threading model¶
| Thread / timer | Role |
|---|---|
| Main / EDT (AWT) | Window, key/mouse, some painting |
| MIDlet threads | Game logic, animation, blocking I/O |
Display Timer (~17 ms) |
callSerially queue |
| Libretro Timer (1 ms) | stdin command pump |
| Java Sound / MIDI threads | Audio playback callbacks |
| Painter | Often invoked on the thread that called flushGraphics/repaint |
No dedicated “render thread” abstraction in the core — presentation is synchronous with painter.run() after blit.
iOS risk: Painter may run off the main thread. The adapter must hop to MainActor/Metal queue safely.
8. File access¶
8.1 Resource loading¶
- MIDlet resources via
URLClassLoader/MIDletLoader.getMIDletResourceAsStream Mobile.getResourceAsStreamdelegates to loader
8.2 RMS¶
javax.microedition.rms.RecordStore persists under:
Uses java.io.File / Files.createDirectories / FileOutputStream.
8.3 Save / config files¶
- Config:
{dataPath}/config/{appname}/...(Config.java) - Screenshots:
{dataPath}/screenshots - Working directory matters; FreeJ2ME expects a writable filesystem root via
MobilePlatform.dataPath
CoreJ2 mapping: set dataPath to something like
Documents/JavaOne/Saves/<gameUUID>/ so RMS stays per-game and sandbox-safe.
9. Native dependencies¶
9.1 Java version¶
- Targets classic Java SE APIs used by Ant
javac(effectively Java 8-era surface: AWT, Java Sound, MIDI). - Requires a JRE/JDK on host for desktop/libretro (“JRE 8+” in community docs).
9.2 Third-party / vendored¶
- ASM (vendored under
org.objectweb.asm) for MIDlet bytecode rewriting - Optional native:
src/libretroC coresrc/sdl2C++ helper
9.3 Platform assumptions¶
- Desktop OS process model
- AWT available (AWT frontend)
- Blocking filesystem paths
- Ability to
defineClass/ reflective MIDlet start - Libretro variant assumes ability to spawn
java -jar(problematic in iOS sandbox)
9.4 License¶
GPL-3.0 — distributing a modified FreeJ2ME runtime linked into CoreJ2 has compliance implications (source offer, copyleft). Treat as a first-class product risk, not only a technical one.
10. Integration proposal (CoreJ2)¶
10.1 Keep these completely independent¶
| CoreJ2 area | Must remain free of FreeJ2ME types |
|---|---|
| Library UI | No |
| ImportEngine / Pipeline / Steps | No |
GameLibraryRepository / SwiftData |
No |
| ViewModels | Depend only on EmulatorBridgeProtocol |
Domain InstalledGame |
Only supplies jarURL + ids to LaunchConfiguration |
10.2 Recommended layering¶
ViewModel
→ EmulatorBridgeProtocol.launch(LaunchConfiguration)
→ FreeJ2MERuntimeAdapter (Epic 3 implementation detail)
→ MobilePlatform + MIDletLoader + MIDP APIs
→ iOSPainter (Metal/CG)
→ iOSAudioBackend
→ iOSFileDataPath
10.3 Minimum adapter layer¶
FreeJ2MERuntimeAdapter (name illustrative) should own:
- Mapping
LaunchConfiguration→file://jar URL + LCD size + phone profile +dataPath - Constructing
MobilePlatform, installing iOS painter/audio/file backends - Calling
loadJar/runJar - Forwarding pause/resume/stop to MIDlet lifecycle when exposed
- Converting LCD pixels → frames for SwiftUI/Metal
- Converting Swift input events →
MobilePlatformkey/pointer APIs - Isolating all
org.recompile.*/javax.microedition.*imports inside the Emulator feature module
10.4 Responsibilities of EmulatorBridge¶
Already aligned with E3-US001:
- Stable Swift API for the app (
launch, later pause/resume/stop) - Session identity (
EmulatorSession) - Error surface suitable for UI alerts
- No FreeJ2ME types in the protocol
- Swap implementations via DI (
DefaultEmulatorBridge→ real adapter)
10.5 Responsibilities of FreeJ2MERuntimeAdapter¶
- Host/runtime bring-up
- Painter / audio / RMS path bridging to iOS
- Thread hops to MainActor for UI frames
- Lifecycle of one game session
- Logging/telemetry boundaries
10.6 Parts that should never be modified inside FreeJ2ME (prefer)¶
Avoid editing upstream unless unavoidable:
| Leave alone | Why |
|---|---|
| MIDlet game jars | Content |
| Vendored ASM tree | Stability / legal surface |
Most javax.microedition.* semantics |
Compatibility |
| Libretro C core / AWT UI | Wrong target for iOS |
| Game-specific hacks scattered in core | Prefer adapter or documented forks |
Acceptable thin change points (if a fork is unavoidable):
MobilePlatform.setPainterusage (no change needed if already injectable)- New platform backends replacing
PlatformPlayer/ AWT-dependent pieces dataPathinitialization- Possibly abstracting
BufferedImageaccess behind an interface in a CoreJ2-managed fork, not drive-by edits
10.7 Major risks¶
| Risk | Severity | Notes |
|---|---|---|
| No JVM on iOS App Store | Critical | Biggest architectural risk; may force Multi-OS Engine / custom runtime / large port |
| AWT / Java Sound dependency | High | Must replace for iOS |
| GPL-3 copyleft | High | Legal/distribution strategy needed early |
| Thread-unsafe painter callbacks | High | Must synchronize for Metal/UIKit |
System.exit in loader error paths |
Medium | Embedding-hostile; needs fork guard |
| Incomplete audio codecs | Medium | Some games silent |
| Per-game config / RMS path assumptions | Medium | Must sandbox under Documents |
| freej2me-plus divergence | Medium | Choose upstream vs plus early |
| M3G / JSR optional APIs | Medium | 3D games may fail |
10.8 Practical integration strategies (ordered)¶
-
Framebuffer-adapter strategy (recommended research target)
Keep MIDP + loader; replace presentation/audio/FS backends; run on an iOS-capable Java runtime if one is chosen. -
Process/libretro strategy
Unrealistic on iOS (can’t spawnjava -jarlike RetroArch desktop). -
Full rewrite of FreeJ2ME in Swift
Out of scope; abandons “don’t modify FreeJ2ME much.” -
Server-side / remote play
Not aligned with native emulator product goals.
Scores¶
| Metric | Score | Rationale |
|---|---|---|
| Architecture complexity | 7 / 10 | Clean MIDP core + injectable painter, but three frontends, ASM loading, timers, and SE dependencies add complexity |
| Estimated integration difficulty (into native iOS CoreJ2) | 9 / 10 | Not because FreeJ2ME is undocumented — because running its JVM/AWT stack on iOS is the hard part |
Desktop embedding difficulty would be ~5/10 (Libretro-style). iOS App Store shipping is the jump to 9.
Suggested Epic 3 roadmap (after this assessment)¶
- E3-R001 — this assessment (done as documentation).
- Runtime decision spike — choose how Java bytecode will execute on device (policy + tech). Gate all coding on this.
- Expand
EmulatorBridgeProtocol— pause/resume/stop + frame callback / input APIs (still placeholders OK). FreeJ2MERuntimeAdapterskeleton — mapsLaunchConfiguration→ load/run, still no pixels on device if runtime undecided.- iOS Painter prototype — LCD ARGB → Metal/CGImage (can be stubbed with synthetic frames first).
- Input bridge — virtual keypad →
MobilePlatformkeycodes. - Audio backend spike — replace Java Sound for one WAV/MIDI path.
- RMS
dataPathsandbox — per-game Documents folder. - Wire Library “Play” — ViewModel →
EmulatorBridgeonly. - Compatibility harness — corpus of JARs; track FreeJ2ME vs CoreJ2 results.
- Legal review — GPL-3 compliance plan before TestFlight.
Until step 2 is decided, treat FreeJ2ME as a vendored research dependency, not a production iOS runtime.
Relationship to existing CoreJ2 docs¶
docs/EMULATOR_ARCHITECTURE.md— CoreJ2-side bridge contracts (Swift).docs/FREEJ2ME_ASSESSMENT.md— FreeJ2ME-side runtime reality (this file).
Together they define the seam: Swift bridge stays stable; FreeJ2ME stays behind an adapter; Library/Import never import FreeJ2ME.
References¶
- Upstream repo: https://github.com/hex007/freej2me
- Active plus fork: https://github.com/TASEmulators/freej2me-plus
- Key source files reviewed:
FreeJ2ME.java,Libretro.java,Anbu.java,MobilePlatform.java,Mobile.java,MIDletLoader.java,PlatformPlayer.java,PlatformGraphics.java,Display.java,Canvas.java,GameCanvas.java,RecordStore.java,build.xml,KEYMAP.md,README.md