Emulator Architecture¶
CoreJ2 integrates the Java ME runtime through a dedicated Emulator Bridge layer.
The goal is to launch installed games without coupling SwiftUI, Library, or ImportEngine to FreeJ2ME.
FreeJ2ME should be integrated later with as few changes as possible to the original emulator and to the rest of CoreJ2.
Current Status (E3-US001)¶
Epic 3 introduces the skeleton only:
| Piece | Status |
|---|---|
EmulatorBridgeProtocol |
Present |
DefaultEmulatorBridge |
Placeholder (does not start an emulator) |
LaunchConfiguration |
Minimal (InstalledGame) |
EmulatorSession |
Minimal (id + configuration) |
| UI launch flow | Not wired yet |
| FreeJ2ME | Not integrated |
DefaultEmulatorBridge.launch(_:) returns an EmulatorSession and does not start FreeJ2ME or any native process.
Design Principles¶
- Single entry point — UI / ViewModels talk only to
EmulatorBridgeProtocol. - Protocol-oriented — FreeJ2ME is an implementation detail behind the bridge.
- Stable data contracts —
LaunchConfigurationandEmulatorSessionevolve without rewriting Library or Import. - Composition root —
AppDependencyContainerowns wiring (makeEmulatorBridge()). - Isolation — ImportEngine and EmulatorBridge remain independent features.
These match AGENTS.md: bridge FreeJ2ME without modifying the original emulator more than necessary.
Layer Diagram¶
┌─────────────────────────────────────────┐
│ SwiftUI / ViewModels (future launch) │
└───────────────────┬─────────────────────┘
│ LaunchConfiguration
▼
┌─────────────────────────────────────────┐
│ EmulatorBridgeProtocol │
│ (single entry point for launching) │
└───────────────────┬─────────────────────┘
│
▼
┌─────────────────────────────────────────┐
│ DefaultEmulatorBridge │
│ (placeholder → FreeJ2ME later) │
└───────────────────┬─────────────────────┘
│
▼
┌─────────────────────────────────────────┐
│ FreeJ2ME runtime (not integrated) │
│ frames / audio / input / save states │
└─────────────────────────────────────────┘
Library and ImportEngine supply InstalledGame (JAR path, metadata, hash). They never call FreeJ2ME directly.
Source Layout¶
JavaOne/Features/Emulator/
├── Models/
│ ├── LaunchConfiguration.swift
│ └── EmulatorSession.swift
└── Services/
├── EmulatorBridgeProtocol.swift
└── DefaultEmulatorBridge.swift
Registered in AppDependencyContainer as:
private lazy var emulatorBridge: EmulatorBridgeProtocol = DefaultEmulatorBridge()
func makeEmulatorBridge() -> EmulatorBridgeProtocol {
emulatorBridge
}
Core Types¶
LaunchConfiguration¶
Input to a launch request.
| Field | Role |
|---|---|
game |
InstalledGame to run (jarURL, title, hash, …) |
Future fields may include phone profile, screen size, sound options, and save-state restore paths—without changing the bridge entry method signature if possible.
EmulatorSession¶
Result of a successful launch(_:).
| Field | Role |
|---|---|
id |
Session identity |
configuration |
Configuration that produced the session |
Future fields may include lifecycle state (prepared / running / paused), frame callbacks, and teardown handles.
EmulatorBridgeProtocol¶
@MainActor
protocol EmulatorBridgeProtocol {
func launch(_ configuration: LaunchConfiguration) throws -> EmulatorSession
}
Additional lifecycle APIs (pause, resume, stop, input injection) should be added to this protocol as Epic 3 progresses—still without exposing FreeJ2ME types to UI.
Boundary Rules¶
| Allowed | Not allowed |
|---|---|
ViewModel → EmulatorBridgeProtocol |
View → FreeJ2ME |
| Bridge → FreeJ2ME adapter (future) | ImportEngine → EmulatorBridge |
Bridge reads InstalledGame.jarURL |
ViewModel → SwiftData / FreeJ2ME |
| DI swaps bridge implementations | Copy FreeJ2ME APIs into Library models |
FreeJ2ME Integration Strategy¶
- Keep FreeJ2ME sources as intact as practical (vendor / submodule / binary).
- Add a thin adapter inside the Emulator feature (e.g.
FreeJ2MEEmulatorBridgeor a collaborator used byDefaultEmulatorBridge). - Map CoreJ2 types → FreeJ2ME launch args (
jarURL, optional profile). - Map FreeJ2ME runtime events →
EmulatorSession/ callbacks consumed by UI. - Do not leak FreeJ2ME types across the bridge protocol.
Recommended evolution of DefaultEmulatorBridge:
launch(configuration)
→ validate game.jarURL
→ start FreeJ2ME session (adapter)
→ return EmulatorSession bound to that runtime
Until that adapter exists, the placeholder remains safe for Library and Import work.
Roadmap (Epic 3+)¶
Suggested order after the skeleton:
- Wire Library UI / ViewModel to
makeEmulatorBridge()(launch button). - Expand
LaunchConfiguration(display size, audio, phone type). - Session lifecycle: pause / resume / stop.
- FreeJ2ME adapter (frames → renderer surface).
- Input bridge (touch / virtual keypad → FreeJ2ME keys).
- Audio bridge.
- Save states (optional, later).
Import Engine (Epic 2) stays the source of installed JARs and metadata; Emulator Bridge only consumes them.
Testing Guidance¶
- Unit-test bridge behavior against
EmulatorBridgeProtocolfakes in ViewModels. - Keep FreeJ2ME integration tests behind the adapter, not in Library tests.
- Placeholder
DefaultEmulatorBridgemust remain side-effect free (no process launch).
Summary¶
CoreJ2 owns product UX, library, and import.
The Emulator Bridge owns the only path into the runtime.
FreeJ2ME becomes a replaceable backend behind EmulatorBridgeProtocol, which is how Epic 3 stays aligned with the long-term goal in AGENTS.md.