DEV Community

Cover image for Building SimArch: A Real-Time CPU and Boot-Process Simulator in Kotlin and Jetpack Compose
Alfino Hatta
Alfino Hatta

Posted on

Building SimArch: A Real-Time CPU and Boot-Process Simulator in Kotlin and Jetpack Compose

How I turned the question "what actually happens when I press the power button?" into a native Android app, and what I learned about animation, architecture, and teaching along the way.

Introduction

Most people who study computer science can recite that the CPU fetches, decodes, and executes instructions. Far fewer can picture what that looks like while it happens. Where does the first instruction come from when the machine has just been powered on and RAM is empty? Who decides that the kernel gets copied from disk into memory? What physically changes when a program makes a system call and the processor moves from Ring 3 to Ring 0?

Textbooks answer these questions with static diagrams. Those diagrams are accurate, but they hide the thing that makes computers hard to understand: time. Signals propagate, rails stabilize, buses carry packets, and pipeline stages overlap. A picture can't show any of it.

I built SimArch to fix that. It is an interactive, real-time simulator that models hardware bootstrapping, memory buses, a 5-stage CPU pipeline, x86-64 assembly execution, and the mediation between hardware and software. It runs natively on Android, is written in Kotlin with Jetpack Compose, and is open source.

👉 Source code: github.com/alfinohatta/simarch

In this article I'll walk through how I designed and built it: the problem, the architecture, the animation engine at its core, the state machines behind each scene, and the smaller decisions that made it usable as a teaching tool.

The Problem I Wanted to Solve

Abstract concepts like clock signal propagation, voltage rail stabilization, register transfers, cache misses, interrupt handling, and pipeline stalls are difficult to absorb from static material. The usual alternatives are expensive: an oscilloscope, or an FPGA kit that costs money and takes time to learn.

I set four objectives:

  1. Interactive visualization. Show signal buses, voltage rails, and memory transfers as live animation, not slides.
  2. Academic accuracy. Model the real details, such as the BIOS/UEFI reset vector at 0xFFFFFFF0 and DMA transfers from storage into RAM.
  3. Two levels of explanation. Offer a simple "Kid Story Mode" and a rigorous "Tech Architecture Mode" for the same components.
  4. Smooth performance on a phone. Animations that stutter distract from the lesson, so the rendering path had to be efficient.

The intended audience is university computer science and ICT students preparing for exams or oral project defenses, lecturers who want to demo hardware behavior live, and working engineers who want a clearer mental model of kernel and hardware interaction.

What SimArch Does

The app has two primary workflows, plus supporting tools.

Part 1: System bootstrapping (5 scenes)

  1. Power distribution. The PSU brings up +12V, +5V, and +3.3V rails across the motherboard's voltage regulators.
  2. Firmware and POST. The CPU begins executing at the reset vector 0xFFFFFFF0 and BIOS/UEFI runs its Power-On Self-Test.
  3. Kernel DMA loading. The OS kernel binary is transferred from storage into SDRAM.
  4. Driver and OS handoff. Firmware hands control to the kernel, which runs in Ring 0.
  5. Ready state. The desktop appears and the CPU settles into an interrupt (IRQ) idle wait loop.

Part 2: Program execution (9 steps)

The user picks an application profile (Google Chrome, Microsoft Word, Calculator, or a Python IDE) and watches the full chain:

User Input → OS Kernel → Storage → RAM → CPU → GPU / I/O → Monitor Display

This flow also shows the transition from User Mode (Ring 3) to Kernel Mode (Ring 0) during a system call.

Supporting tools

  • A 5-stage pipeline visualizer and an x86-64 assembly inspector
  • A register monitor for PC, MAR, IR, EAX, and status flags
  • A storage benchmark simulator comparing HDD, SATA SSD, and NVMe
  • A custom bus signal dispatcher for the control, address, and data buses
  • A searchable component reference with category filters
  • A quiz for exam and defense practice

Choosing the Stack

Layer Choice Why
Language Kotlin 2.0.21 Null safety, coroutines, extension functions
UI Jetpack Compose + Material 3 Declarative, state-driven, canvas-friendly
State Coroutines + StateFlow Reactive streams for frame loops and scene transitions
Navigation Navigation Compose Single-activity app with composable routes
Build Gradle Kotlin DSL + version catalog Centralized dependency versions
Tests JUnit 4 + Espresso Unit and instrumentation coverage

Why Compose fits a simulation app

A simulator is, at its core, a function that maps "the state of the machine at time t" to "pixels on screen." That is exactly what Compose encourages. I keep the simulation state in one place, expose it as a StateFlow, and let composables observe it. When the state changes, the UI redraws. I never manually invalidate views or mutate widget properties, which removes a whole class of synchronization bugs that would be painful in an animation-heavy app.

Compose's Canvas gave me the low-level drawing control for particles, oscilloscope traces, and the motherboard, while Material 3 supplied polished conventional UI (cards, chips, bottom sheets) so I didn't have to design those from scratch.

Architecture: Clean Layers with MVVM

I organized the app into three layers, plus a dedicated animation engine.

Presentation (Composables)
        ↓ observes StateFlow
Domain (ViewModels + immutable models)
        ↓ drives
Core Animation Engine (SystemAnimationEngine)
        ↓ reads
Data (Repositories)
Enter fullscreen mode Exit fullscreen mode

Presentation layer

Screens like HomeScreen, StartupScreen, ExecutionScreen, ComponentReferenceScreen, and AboutScreen are hosted by a single NavHost. Reusable pieces live in a components package: CpuPipelineVisualizer, AssemblyInstructionVisualizer, PerformanceBenchmarkSimulator, CustomBusSignalDispatcher, AnimationControlsBar, RelationshipDiagram, and ComponentInfoDialog.

Composables stay thin. They render state and forward user actions (play(), nextStep(), selectApp()) to a ViewModel.

Domain layer

StartupViewModel and ExecutionViewModel coordinate scene transitions, advance simulation steps, and update register values. Models such as ComponentData, StartupSceneInfo, ExecutionStepInfo, and UserProfile are immutable. I annotated ComponentData with @Immutable to tell Compose's compiler that instances won't change after creation, which lets it skip recomposition when nothing relevant has changed. That matters when the screen is animating continuously.

Data layer

Repositories provide component definitions (including architecture roles and the HDD/SSD/NVMe comparison table), quiz questions, and user preferences. Preferences are stored in SharedPreferences, and nothing leaves the device.

Why this separation paid off

Because the simulation lives outside the UI, I can change how a scene looks without touching how it behaves, and vice versa. Both simulation screens also share one engine instead of each reinventing timing logic.

The Core: A Coroutine-Driven Animation Engine

SystemAnimationEngine is the piece that makes everything feel alive. It runs a coroutine frame loop on a 30 ms tick, roughly 33 frames per second, and on each tick advances three independent systems.

1. The clock signal

The oscilloscope-style display needs a square wave whose high and low states alternate at a steady rate. On each tick the engine updates a phase and derives whether the clock is currently high. Because the clock is a pure function of elapsed time, pausing and resuming behave predictably.

2. Bus particles

When data moves between components, I represent it as particles traveling along the control, address, or data bus. Each particle has a position along a path and advances a little every tick. When it arrives, the engine can trigger the next event, such as a RAM cell lighting up or the CPU latching a value.

3. The camera

To keep learners focused, the view gently zooms and pans toward whichever component is currently active. Rather than jumping, the camera uses spring interpolation, so it accelerates and settles smoothly. The zoom stays subtle (1.04x to 1.06x), enough to guide attention without disorienting anyone. A "1.0x Full View" button recenters the scene instantly.

Here is a simplified sketch of the loop's shape (illustrative, not the actual implementation):

// Illustrative sketch only
class SystemAnimationEngine {
    var clockHigh = false
    val particles = mutableListOf<BusParticle>()
    var camera = CameraState()

    fun tick(deltaMs: Long) {
        updateClock(deltaMs)
        updateParticles(deltaMs)
        camera = camera.springToward(target = activeComponentFocus(), deltaMs)
    }
}

// In the ViewModel
viewModelScope.launch {
    while (isActive && isPlaying) {
        engine.tick(deltaMs = 30)
        _uiState.update { it.copy(
            clockHigh = engine.clockHigh,
            particles = engine.particles.toList(),
            camera = engine.camera
        ) }
        delay(30)
    }
}
Enter fullscreen mode Exit fullscreen mode

Rendering performance

Updating state at ~33 Hz is only half the story. The other half is drawing efficiently. For zoom and pan I used graphicsLayer modifiers, which apply transforms on the GPU at draw time instead of triggering a full recomposition and re-measure for every camera movement. Combined with immutable models, this kept animation smooth on mobile hardware.

Modeling Hardware as State Machines

Both workflows are sequences of discrete phases, so I modeled them as finite state machines. Each scene or step defines:

  • which components are highlighted as active
  • which buses carry signals, and in which direction
  • the explanation text for the current phase
  • which register values should change

For example, the Firmware/POST scene involves the CPU fetching from 0xFFFFFFF0, the address bus carrying that value, and the BIOS ROM responding on the data bus. Encoding this as data plus transitions means the same playback controls (play, pause, step forward, replay) work for every scene. Moving between phases is just a state transition, and "pause" means the transition timer stops.

This also made content easier to author. Adding a new application profile in Part 2 means describing its steps, not writing new animation code.

Visualizing the 5-Stage Pipeline

The pipeline visualizer is where the abstract becomes concrete. Instruction tokens move through the five classic stages:

  • IF: Instruction Fetch
  • ID: Instruction Decode
  • EX: Execute
  • MEM: Memory Access
  • WB: Writeback

Seeing several tokens occupy different stages simultaneously is the clearest way to explain why pipelining improves throughput. Next to it, the assembly inspector shows a disassembly listing (MOV, PUSH, CALL, INT 0x80) with the current instruction highlighted in lockstep, and the register monitor shows live values for:

  • PC, the Program Counter
  • MAR, the Memory Address Register
  • IR, the Instruction Register
  • EAX, a general-purpose register
  • status flags

Keeping these three views consistent was one of the trickier design problems, because they must all reflect the same underlying step. The solution was a single source of truth for "current instruction and cycle," with each view deriving what it displays from that state.

Showing the Hardware-Software Boundary

One of the most misunderstood topics in an operating systems course is how user programs get the kernel to do work on their behalf. In Part 2, when the simulated application needs to read from storage, the flow visibly crosses from User Mode (Ring 3) into Kernel Mode (Ring 0) through a system call (the INT 0x80 instruction appears in the assembly view). The kernel then mediates access to storage and memory, loads the needed pages into RAM, and returns control.

Making this boundary a visible event, not a footnote, was a deliberate choice. It reinforces the core theme of the project: hardware and software are not separate stories, they are one system with a carefully guarded seam.

Two Ways to Explain Everything

Every component has two explanations backed by the same data model.

Kid Story Mode uses metaphors: the power supply is a juice box, the CPU is the chief chef, RAM is the workbench, and storage is the toy chest.

Tech Architecture Mode gives the real details: clock speeds, bus interfaces, register names, and memory bandwidths.

A toggle switches between them, so the same screen serves someone meeting the ideas for the first time and someone preparing for a technical exam. Implementation-wise, this was cheap: one additional text field per component and a preference flag. The educational payoff was much larger than the engineering cost.

The Storage Benchmark Simulator

Storage speed is easy to state and hard to feel. The benchmark simulator runs a live comparison of three technologies:

Storage Approx. throughput Approx. boot time
HDD ~150 MB/s ~45 s
SATA SSD ~550 MB/s ~14 s
NVMe PCIe 4.0 SSD ~7000 MB/s ~4 s

Watching bars fill at wildly different rates makes the gap between them real, and it connects back to the DMA kernel-loading scene: the same transfer, just at different speeds.

Interactive Bus Signals

The custom bus signal dispatcher lets users trigger packets on the control bus, address bus, and data bus themselves. Passive watching teaches less than active experimentation, and this gives learners a way to ask "what if I send this here?" and see the result. It also reinforces the distinction between the three buses: addresses say where, data says what, and control says how.

Supporting Learning: Reference and Quiz

Beyond the simulations, the app includes a two-column component reference with real-time search and category chips (All Components, Core Processors, Memory and Storage, Firmware and OS, I/O and Network). Tapping a component opens a bottom sheet with its function, its role in the architecture, its Kid Story analogy, and, for storage, the comparison table.

A quiz feature adds self-assessment for exam and oral-defense practice, with scores persisted locally.

Security and Privacy Decisions

Even a simple educational app benefits from good hygiene:

  • Input sanitization. Free-text fields such as student name and group are length-limited with take(60) and stripped of <, >, ", and ' characters.
  • Local data only. Preferences and quiz scores stay in private SharedPreferences, with no network transmission.
  • Release hardening. Release builds use R8/ProGuard for code shrinking and obfuscation.

Testing and Tooling

The project includes unit tests (testDebugUnitTest) and instrumentation tests (connectedDebugAndroidTest). The most valuable candidates for unit testing are the pure pieces: the state machine transitions, the clock derivation, and the spring camera math. They have no Android dependencies, so they run fast on the JVM.

Lessons Learned

  1. Separate the simulation from the UI early. A dedicated engine kept both screens simple and made behavior testable.
  2. Immutability is a performance feature in Compose. @Immutable models and read-only state reduced wasted recomposition.
  3. Prefer GPU transforms for continuous motion. graphicsLayer for zoom and pan avoided expensive layout work each frame.
  4. Model content as data. Treating scenes and steps as data plus a state machine turned "add a new scenario" into an authoring task.
  5. One source of truth per concept. Register values, the current instruction, and the pipeline stage all derive from the same state.
  6. Pedagogy shapes engineering. Features like dual explanation modes and the bus dispatcher came from asking what a learner needs, not what is easiest to build.

What's Next

  • Pipeline hazards: stalls, data forwarding, and branch mispredictions
  • Cache hierarchy: visible L1/L2/L3 hits and misses with replacement policies
  • User-authored assembly: write a few instructions and step through them
  • Interrupt scenarios: a keyboard IRQ interrupting a running program
  • More test coverage around the engine and state machines
  • A public release so students can install it without building from source

Try It Yourself

git clone https://github.com/alfinohatta/simarch.git
cd simarch
./gradlew installDebug
Enter fullscreen mode Exit fullscreen mode

Requirements: Android Studio Ladybug (2024.2.1) or newer, JDK 11 or 17, and Android SDK 36 (minSdk 24). Other useful commands:

./gradlew assembleDebug            # build a debug APK
./gradlew testDebugUnitTest        # run unit tests
./gradlew connectedDebugAndroidTest # run instrumentation tests
Enter fullscreen mode Exit fullscreen mode

SimArch is released under AGPL-3.0, and contributions are welcome. Fork the repo, create a feature branch, and open a pull request.

⭐ Source code: github.com/alfinohatta/simarch

If SimArch helped you finally see how a computer boots, or you have an idea for the next scene to animate, I'd love to hear it. Open an issue on GitHub or leave a comment below.

Top comments (0)