diff --git a/.gitignore b/.gitignore
index 524f096..aed3588 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,24 +1,52 @@
-# Compiled class file
+# Gradle cache + build outputs
+.gradle/
+**/.gradle/
+**/build/
+local.properties
+
+# IDE
+.idea/
+*.iml
+*.ipr
+*.iws
+.vscode/
+*.swp
+*.swo
+*~
+
+# OS
+.DS_Store
+Thumbs.db
+
+# Java
*.class
-
-# Log file
-*.log
-
-# BlueJ files
-*.ctxt
-
-# Mobile Tools for Java (J2ME)
-.mtj.tmp/
-
-# Package Files #
*.jar
-*.war
-*.nar
-*.ear
+!modules/**/*.jar
+!gradle/wrapper/gradle-wrapper.jar
+!android/gradle/wrapper/gradle-wrapper.jar
+
+# Logs
+*.log
+hs_err_pid*
+
+# Package files
*.zip
*.tar.gz
*.rar
+*.deb
-# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
-hs_err_pid*
+# BlueJ
+*.ctxt
+.mtj.tmp/
+
+# Virtual machine crash logs
replay_pid*
+
+# Windows portable runtime (prebuilt binaries)
+dist/windows-portable/runtime/
+dist/windows-portable/app/cbe-emu.jar
+
+# Compiled binaries
+*.exe
+*.dll
+*.dex
diff --git a/MANUAL.md b/MANUAL.md
new file mode 100644
index 0000000..1b8d7c1
--- /dev/null
+++ b/MANUAL.md
@@ -0,0 +1,1042 @@
+# CBE Platform — Полное руководство по созданию плагинов и систем
+
+## Содержание
+
+1. [Введение](#1-введение)
+2. [Архитектура CBE](#2-архитектура-cbe)
+3. [Формат плагина (.cbeplugin)](#3-формат-плагина-cbeplugin)
+4. [Создание модуля CPU](#4-создание-модуля-cpu)
+5. [Создание модуля RAM](#5-создание-модуля-ram)
+6. [Создание модуля GPU](#6-создание-модуля-gpu)
+7. [Создание модуля KBD (клавиатура)](#7-создание-модуля-kbd)
+8. [Создание модуля SND (звук)](#8-создание-модуля-snd)
+9. [Создание модуля BIOS](#9-создание-модуля-bios)
+10. [Создание модуля DISK](#10-создание-модуля-disk)
+11. [Семантические операции инструкций](#11-семантические-операции)
+12. [Написание программ на ассемблере](#12-написание-программ-на-ассемблере)
+13. [Написание программ на C/C++](#13-написание-программ-на-cc)
+14. [Написание программ на Python](#14-написание-программ-на-python)
+15. [Hex-машинный код](#15-hex-машинный-код)
+16. [Сборка плагинов (cbecc)](#16-сборка-плагинов-cbecc)
+17. [Тулчейн: компиляция из разных языков](#17-тулчейн-компиляция-из-разных-языков)
+18. [Система POST-диагностики](#18-система-post-диагностики)
+19. [Memory-mapped I/O](#19-memory-mapped-io)
+20. [Бесконечные шаги и детектор циклов](#20-бесконечные-шаги-и-детектор-циклов)
+21. [Тёмная тема GUI](#21-тёмная-тема-gui)
+22. [Примеры](#22-примеры)
+23. [Устранение неполадок](#23-устранение-неполадок)
+24. [Справочник команд](#24-справочник-команд)
+
+---
+
+## 1. Введение
+
+**CBE (Create. Build. Execute.)** — платформа для эмуляции и создания собственных компьютерных компонентов: процессоров, оперативной памяти, видеокарт, дисков, BIOS, клавиатур и звуковых карт. Всё это собирается в единую архитектуру и запускается через эмулятор с графическим интерфейсом.
+
+Основная идея: вы описываете поведение компонента через JSON-конфигурацию и/или бинарные данные, а платформа предоставляет среду выполнения с:
+
+- Эмулятором на Java с GUI (Swing)
+- Компилятором .cbeplugin
+- Тулчейном для ассемблера, C/C++, Python, hex
+- Системой POST-диагностики
+- Детектором бесконечных циклов
+- Тёмной темой
+
+### Ключевые концепции
+
+- **Plugin** — модуль в формате `.cbeplugin`, содержащий метаданные, инструкции, микрокод и данные
+- **ModuleInstance** — экземпляр загруженного модуля в эмуляторе
+- **Engine** — оркестратор, управляющий всеми модулями и циклом выполнения CPU
+- **Bus** — системная шина, связывающая модули
+- **Registers** — регистры CPU
+- **POST** — Power-On Self-Test, система самодиагностики при старте
+- **Memory-mapped I/O** — регистры ввода-вывода, отображённые на адресное пространство
+
+---
+
+## 2. Архитектура CBE
+
+```
+┌─────────────────────────────────────────────────────────────┐
+│ CBE Emulator │
+├─────────────────────────────────────────────────────────────┤
+│ ┌───────────┐ ┌──────────┐ ┌──────────┐ ┌───────────┐ │
+│ │ CPU │ │ RAM │ │ GPU │ │ KBD │ │
+│ │ (плагин) │ │ (плагин) │ │ (плагин) │ │ (плагин) │ │
+│ └─────┬─────┘ └────┬─────┘ └────┬─────┘ └─────┬─────┘ │
+│ │ │ │ │ │
+│ ┌─────┴──────────────┴─────────────┴───────────────┴─────┐ │
+│ │ System Bus (шина) │ │
+│ └─────────────────────────────────────────────────────────┘ │
+│ │ │ │ │ │
+│ ┌─────┴─────┐ ┌────┴────┐ ┌─────┴─────┐ ┌──────┴─────┐ │
+│ │ SND/Sound│ │ BIOS │ │ DISK │ │ POST │ │
+│ │ (плагин) │ │ (плагин)│ │ (плагин) │ │ (встроено) │ │
+│ └───────────┘ └─────────┘ └───────────┘ └────────────┘ │
+└─────────────────────────────────────────────────────────────┘
+```
+
+### Модули (ModuleType)
+
+| Тип | ID | Описание |
+|---------|----|--------------------------------------------------------------|
+| CPU | 0 | Процессор. Определяет регистры, набор инструкций, микрокод |
+| RAM | 1 | Оперативная память с банками данных |
+| DISK | 2 | Диск с секторной адресацией (512 байт/сектор) |
+| GPU | 3 | Видеокарта. 80x25 текстовый режим, VRAM, курсор |
+| BIOS | 4 | BIOS с информацией о системе |
+| KBD | 5 | Клавиатура с буфером клавиш, handshake-протокол |
+| SND | 6 | Звуковая карта (PC Speaker beep) |
+| DATA_ONLY | 7 | Только данные, без логики |
+
+### CompileMode
+
+| Режим | ID | Описание |
+|------------|----|------------------------------------|
+| FULL | 0 | Только логика (инструкции + микрокод) |
+| HYBRID | 1 | Логика + данные |
+| PACK_ONLY | 2 | Только данные |
+
+---
+
+## 3. Формат плагина (.cbeplugin)
+
+### Структура файла
+
+Бинарный формат с little-endian числами:
+
+```
+Смещение Размер Поле
+-------- ------ ------------------------------------
+0 8 Магическая строка: "CBE_PLUG\0"
+8 4 Версия (int32 LE)
+12 4 Размер заголовка (int32 LE, всегда 62)
+16 1 ModuleType (byte)
+17 1 CompileMode (byte)
+18 4 Смещение секции метаданных (int32 LE)
+22 4 Длина секции метаданных
+26 4 Смещение таблицы опкодов
+30 4 Длина таблицы опкодов
+34 4 Смещение таблицы микрокода
+38 4 Длина таблицы микрокода
+42 4 Смещение байткода обработчика
+46 4 Длина байткода обработчика
+50 4 Смещение секции данных
+54 4 Длина секции данных
+58 4 CRC32 (покрывает байты 0..57)
+```
+
+### Секции после заголовка
+
+- **Метаданные:** JSON-строка с name, arch, module_type, version, tdp, frequency
+- **Таблица опкодов:** JSON-массив объектов инструкций
+- **Микрокод:** JSON-объект { command: [byte, ...] }
+- **Байткод обработчика:** зарезервировано (raw bytecode)
+- **Секция данных:** [bank_count:4][bank0_size:4][bank0_data...][bank1_size:4]...
+
+---
+
+## 4. Создание модуля CPU
+
+### Структура директории
+
+```
+my-cpu.cpu/
+├── module.json # Метаданные модуля
+├── registers.json # Определения регистров
+├── instructions/ # Определения инструкций
+│ ├── 0x00.json # NOP
+│ ├── 0x01.json # MOV
+│ └── ...
+├── microcode/
+│ └── bus.json # Сигналы шины
+├── roms/
+│ └── boot.bin # Boot ROM (исполняемый код)
+├── banks/ # Банки данных (опционально)
+│ └── bank_0.bin
+├── program.asm # Исходник программы (альтернатива boot.bin)
+├── program.c # Исходник на C
+├── program.py # Исходник на Python
+└── program.hex # Hex-машинный код
+```
+
+### module.json
+
+```json
+{
+ "name": "MyCPU",
+ "arch": "my-8bit",
+ "module_type": "cpu",
+ "version": 1,
+ "tdp": 5.0,
+ "memory_size": 65536,
+ "frequency": 1000000
+}
+```
+
+Поля:
+- `name` — название процессора
+- `arch` — архитектура (строка)
+- `module_type` — "cpu"
+- `tdp` — тепловыделение (W)
+- `memory_size` — размер адресного пространства (байт)
+- `frequency` — частота в Hz
+
+### registers.json
+
+```json
+{
+ "a": 0,
+ "b": 0,
+ "c": 0,
+ "d": 0,
+ "pc": 0,
+ "sp": 0,
+ "carry": 0,
+ "zero": 0
+}
+```
+
+Каждый регистр — ключ с начальным значением. `pc` (program counter) и `sp` (stack pointer) обязательны.
+
+### Определение инструкции
+
+Файл `instructions/0x00.json`:
+
+```json
+{
+ "opcode": 0,
+ "mnemonic": "NOP",
+ "args": [],
+ "cycles": 1,
+ "semantics": [
+ {"op": "nop"}
+ ]
+}
+```
+
+Инструкция с аргументами:
+
+```json
+{
+ "opcode": 2,
+ "mnemonic": "MOV_IMM_A",
+ "args": [
+ {"name": "value", "type": "imm"}
+ ],
+ "cycles": 2,
+ "semantics": [
+ {"op": "load_imm", "to": "a", "value": "$next"}
+ ]
+}
+```
+
+- `opcode` — числовой код операции (0-255)
+- `mnemonic` — мнемоника для ассемблера
+- `args` — список аргументов (name + type: "reg", "imm", "address")
+- `cycles` — тактов на выполнение
+- `semantics` — список семантических операций (см. раздел 11)
+
+### Микрокод (bus.json)
+
+```json
+{
+ "memory_read": [0x01, 0x00],
+ "memory_write": [0x02, 0x00],
+ "io_read": [0x03],
+ "io_write": [0x04]
+}
+```
+
+Каждый сигнал — имя и массив байт.
+
+### Boot ROM
+
+Файл `roms/boot.bin` содержит исполняемый код, который загружается в начало адресного пространства CPU при старте. Максимальный размер — 64KB. Можно создать:
+- через ассемблер (program.asm)
+- через компилятор C (program.c)
+- через Python-транслятор (program.py)
+- как hex-дамп (program.hex)
+
+---
+
+## 5. Создание модуля RAM
+
+```
+my-ram.ram/
+├── module.json
+├── banks/
+│ └── bank_0.bin # Начальные данные RAM
+└── controller/ # Опциональный контроллер
+ └── microcode/
+ └── bus.json
+```
+
+### module.json
+
+```json
+{
+ "name": "MyRAM",
+ "arch": "generic-ram",
+ "module_type": "ram",
+ "version": 1,
+ "tdp": 2.0,
+ "bank_size": 256,
+ "bank_count": 1
+}
+```
+
+Поля:
+- `bank_size` — размер банка в байтах
+- `bank_count` — количество банков
+
+---
+
+## 6. Создание модуля GPU
+
+```
+my-gpu.gpu/
+├── module.json
+├── banks/
+│ └── bank_0.bin # VRAM (2000 байт для 80x25)
+└── microcode/
+ └── bus.json
+```
+
+### module.json
+
+```json
+{
+ "name": "MyGPU",
+ "arch": "vga-text-80x25",
+ "module_type": "gpu",
+ "version": 1,
+ "tdp": 10.0,
+ "vram_size": 2000,
+ "rows": 25,
+ "cols": 80
+}
+```
+
+Поля:
+- `vram_size` — размер видеопамяти (80x25 = 2000)
+- `rows` — количество строк
+- `cols` — количество колонок
+
+GPU отображается как 80x25 текстовый терминал (а-ля QEMU). Вывод символов осуществляется записью в порт 0xC0.
+
+---
+
+## 7. Создание модуля KBD
+
+```
+my-kbd.kbd/
+└── module.json
+```
+
+### module.json
+
+```json
+{
+ "name": "MyKeyboard",
+ "arch": "ps2-like",
+ "module_type": "kbd",
+ "version": 1,
+ "tdp": 0.5,
+ "buffer_size": 32
+}
+```
+
+Поля:
+- `buffer_size` — размер буфера клавиш
+
+Клавиатура работает через memory-mapped I/O:
+- Чтение `0xFB` — данные клавиши (peek, не удаляет)
+- Чтение `0xFA` — статус (0 = нет клавиши, 1 = есть)
+- Запись `0xFA = 0` — acknowledge (удаляет клавишу из буфера)
+
+---
+
+## 8. Создание модуля SND
+
+```
+my-snd.snd/
+└── module.json
+```
+
+### module.json
+
+```json
+{
+ "name": "MySound",
+ "arch": "pc-speaker",
+ "module_type": "snd",
+ "version": 1,
+ "tdp": 1.0
+}
+```
+
+Любая запись в порт `0xF9` вызывает системный звуковой сигнал (beep). Частота ограничена 10 beep/сек.
+
+---
+
+## 9. Создание модуля BIOS
+
+```
+my-bios.bios/
+└── module.json
+```
+
+### module.json
+
+```json
+{
+ "name": "MyBIOS",
+ "arch": "generic-bios",
+ "module_type": "bios",
+ "version": 1,
+ "tdp": 1.0
+}
+```
+
+BIOS хранит строку информации о системе, доступную через `engine.getSystemInfo()`. Триггер перезапуска диагностики — запись в порт `0xF7`.
+
+---
+
+## 10. Создание модуля DISK
+
+```
+my-disk.disk/
+├── module.json
+└── banks/
+ └── bank_0.bin # Данные диска
+```
+
+### module.json
+
+```json
+{
+ "name": "MyDisk",
+ "arch": "ata-like",
+ "module_type": "disk",
+ "version": 1,
+ "tdp": 5.0,
+ "sector_size": 512
+}
+```
+
+Диск работает через порты:
+- `0xB0` — запись LBA сектора (инициирует чтение)
+- `0xB1` — статус готовности (1 = готов)
+- `0xB2+` — данные сектора (512 байт)
+
+Если последние 2 байта сектора 0 равны `0x55 0xAA`, диск считается загрузочным.
+
+### Загрузка с диска (Boot flow)
+
+При старте эмулятор выполняет POST-диагностику, которая:
+1. Пишет BIOS-программу в память CPU (адреса `0x00..0x15`)
+2. Строит POST-текст и размещает его в буфере display (`0x16+`)
+3. Если диск загрузочный (сектор 0 содержит `0x55 0xAA`):
+ - Копирует сектор 0 в память CPU по адресу `0x80`
+ - Заменяет idle-цикл BIOS (`0x14..0x15`) на `JMP_IMM 0x80`
+4. CPU начинает выполнение с `PC=0`:
+ - Исполняет BIOS-программу, которая выводит POST-текст на GPU
+ - После вывода POST-текста переходит к `0x14` → прыжок на `0x80`
+ - CPU выполняет загрузочный код из сектора 0
+
+Пример создания загрузочного диска (Python):
+```python
+# TinyCPU bytecode for boot sector at address 0x80
+# Prints "BOOT OK!" to GPU via port 0xC0, then halts
+program = bytes([
+ 0x02, 0x42, # MOV_IMM_A 'B'
+ 0x0A, 0xC0, # STORE_A 0xC0
+ 0x02, 0x4F, # MOV_IMM_A 'O'
+ 0x0A, 0xC0, # STORE_A 0xC0
+ 0x02, 0x4F, # MOV_IMM_A 'O'
+ 0x0A, 0xC0, # STORE_A 0xC0
+ 0x02, 0x54, # MOV_IMM_A 'T'
+ 0x0A, 0xC0, # STORE_A 0xC0
+ 0x02, 0x20, # MOV_IMM_A ' '
+ 0x0A, 0xC0, # STORE_A 0xC0
+ 0x02, 0x4F, # MOV_IMM_A 'O'
+ 0x0A, 0xC0, # STORE_A 0xC0
+ 0x02, 0x4B, # MOV_IMM_A 'K'
+ 0x0A, 0xC0, # STORE_A 0xC0
+ 0x02, 0x21, # MOV_IMM_A '!'
+ 0x0A, 0xC0, # STORE_A 0xC0
+ 0xFF, # HLT
+])
+sector = program + b'\x00' * (510 - len(program)) + b'\x55\xAA'
+```
+
+Готовый пример: `examples/system-disk/disk.img/`.
+
+---
+
+## 11. Семантические операции
+
+| Операция | Описание |
+|-------------|--------------------------------------------------------------|
+| `copy` | Копирует значение из `from` в `to` |
+| `load_imm` | Загружает непосредственное значение (literal или `$next`) |
+| `add` | Складывает `from` + `to`, сохраняет в `result`, обновляет флаги carry/zero |
+| `sub` | Вычитает `to` из `from`, обновляет флаги borrow/zero |
+| `cmp` | Сравнивает `from` - `to`, обновляет флаги |
+| `store` | Записывает значение в память `mem[address]` |
+| `load` | Читает из памяти `mem[address]` в регистр |
+| `jmp` | Безусловный переход на адрес в `to` |
+| `jcc` | Условный переход, если флаг равен ожидаемому |
+| `jmp_imm` | Переход на байт, следующий за опкодом |
+| `jcc_imm` | Условный переход на байт за опкодом |
+| `call` | Push адреса возврата в стек, прыжок |
+| `ret` | Pop адреса из стека, прыжок |
+| `push` | Push значения в стек |
+| `pop` | Pop значения из стека |
+| `inc` | Инкремент значения |
+| `nop` | Нет операции |
+
+### Специальные значения
+
+- `$next` — следующий байт в памяти (используется для immediate значений)
+- `arg.X` — ссылка на аргумент инструкции по имени
+- `mem[N]` — прямая ссылка на память по адресу N
+- `mem[arg.X]` — косвенная адресация через аргумент
+
+### Условные переходы
+
+```json
+{
+ "op": "jcc_imm",
+ "condition": "zero,1",
+ "to": "$next"
+}
+```
+
+Формат условия: `"имя_флага,ожидаемое_значение"`. Например `"zero,1"`, `"carry,0"`.
+
+---
+
+## 12. Написание программ на ассемблере
+
+Ассемблер CBE поддерживает мнемоники, определённые в инструкциях CPU.
+
+### Синтаксис
+
+```asm
+; комментарий
+
+label: ; определение метки
+ MNEMONIC ; инструкция без аргументов
+ MNEMONIC arg1, arg2 ; инструкция с аргументами
+
+.org 0x100 ; установка адреса
+.byte 0xAB ; эмит байта
+.db "hello", 0 ; эмит строки с нулевым терминатором
+```
+
+### Адресация
+
+- Числа: `0x10` (hex), `10` (dec), `0b1010` (bin), `$10` (hex)
+- Метки: `loop_start`, `loop_start+5`
+- Символы: `'A'` (ASCII код)
+
+### Пример
+
+```asm
+; Программа Hello World
+.org 0x00
+
+start:
+ MOV_IMM_A 0x48 ; 'H'
+ STORE_A 0xC0 ; вывод на GPU
+ MOV_IMM_A 0x65 ; 'e'
+ STORE_A 0xC0
+ MOV_IMM_A 0x6C ; 'l'
+ STORE_A 0xC0
+ MOV_IMM_A 0x6C ; 'l'
+ STORE_A 0xC0
+ MOV_IMM_A 0x6F ; 'o'
+ STORE_A 0xC0
+ HLT
+```
+
+### Сборка
+
+```bash
+# Собрать .asm в .bin с архитектурой TinyCPU
+cbecc asm program.asm -o roms/boot.bin --arch examples/tiny-cpu.cpu
+
+# Или собрать весь плагин (авто-детекция program.asm)
+cbecc build my-cpu.cpu -o build/my-cpu.cbeplugin
+```
+
+---
+
+## 13. Написание программ на C/C++
+
+### Поддержка
+
+Транслятор C → TinyCPU поддерживает:
+
+- Объявление переменных: `int x = 5;`, `char c;`
+- Присваивание: `x = expr;`
+- Вывод на GPU: `write_gpu('A');`
+- Вывод в порт: `outb(addr, value);`
+- Циклы: `while`, `for` (ограниченно)
+- Условные операторы: `if` (ограниченно)
+- `return 0` → HLT
+
+### Пример
+
+```c
+void write_gpu(char c) {
+ // Транслируется в MOV_IMM_A c; STORE_A 0xC0
+}
+
+int main() {
+ write_gpu('H');
+ write_gpu('e');
+ write_gpu('l');
+ write_gpu('l');
+ write_gpu('o');
+ write_gpu('\n');
+ return 0;
+}
+```
+
+### Компиляция
+
+```bash
+# Для TinyCPU (встроенный транслятор)
+cbecc ccompile program.c -o roms/boot.bin --arch tinycpu
+
+# Для нативной архитектуры (x86_64 через gcc)
+cbecc ccompile program.c -o program.bin --arch x86_64
+
+# C++
+cbecc ccompile program.cpp -o roms/boot.bin --arch tinycpu
+```
+
+### Для реальных архитектур
+
+Для не-tinycpu архитектур `cbecc ccompile` использует:
+1. `gcc` для компиляции в объектный файл
+2. `ld` или `objcopy` для извлечения flat binary
+
+Убедитесь, что установлены `gcc`, `binutils`.
+
+---
+
+## 14. Написание программ на Python
+
+### Поддержка
+
+Транслятор Python → TinyCPU поддерживает упрощённый синтаксис:
+
+- Присваивание: `x = 5`
+- Вывод: `print(x)` → GPU (0xC0)
+- Циклы: `for i in range(n):`, `while condition:`
+- Условные операторы: `if x == 5:`
+- Комментарии: `# comment`
+
+### Пример
+
+```python
+# Hello World для TinyCPU
+print("Hello from Python!")
+
+# Счётчик
+count = 0
+while count < 5:
+ print(count)
+ count = count + 1
+```
+
+### Трансляция
+
+```bash
+cbecc py program.py -o roms/boot.bin
+# Или в составе сборки плагина (auto-detect)
+cbecc build my-cpu.cpu -o build/my-cpu.cbeplugin
+```
+
+---
+
+## 15. Hex-машинный код
+
+Hex-загрузчик поддерживает форматы:
+- Простые hex-байты: `02 48 0A C0 FF`
+- C-стиль: `{0x02, 0x48, 0x0A, 0xC0, 0xFF}`
+- Intel HEX (.hex): `:1000000002480AC0FF...`
+- Один байт на строку
+- Разделители: пробелы, запятые, новые строки
+
+### Пример
+
+```
+; boot.bin в hex-формате
+; MOV_IMM_A 'H' ; STORE_A 0xC0 ; HLT
+02 48 0A C0 FF
+```
+
+### Конвертация
+
+```bash
+cbecc hex program.hex -o roms/boot.bin
+```
+
+---
+
+## 16. Сборка плагинов (cbecc)
+
+### Основные команды
+
+```bash
+# Сборка из source-директории (авто-детекция program.*)
+cbecc build my-plugin.cpu -o build/my-plugin.cbeplugin
+
+# Сборка с указанием бинарника программы
+cbecc plugin my-plugin.cpu --program build/program.bin -o build/my-plugin.cbeplugin
+
+# Сборка стандартная (без авто-детекции)
+java -cp modules/cbecc/build/libs/cbecc-0.1.0.jar:modules/loader/build/libs/loader-0.1.0.jar:modules/core/build/libs/core-0.1.0.jar com.cbe.cbecc.Main build my-plugin.cpu -o build/my-plugin.cbeplugin
+```
+
+### Процесс сборки
+
+1. Чтение `module.json`
+2. Определение ModuleType
+3. Парсинг метаданных
+4. Если есть `program.asm/py/c/cpp/hex` — компиляция в `roms/boot.bin`
+5. Построение секции метаданных (JSON)
+6. Построение таблицы опкодов (JSON-массив из инструкций)
+7. Построение таблицы микрокода (JSON)
+8. Построение секции данных (банки + ROMs)
+9. Вычисление CRC32
+10. Запись .cbeplugin файла
+
+---
+
+## 17. Тулчейн: компиляция из разных языков
+
+### Общая схема
+
+```
+program.asm ──→ Assembler ──→ boot.bin
+program.c ──→ CCompiler ──→ boot.bin (gcc + objcopy или встроенный транслятор)
+program.py ──→ PythonTranslator ──→ boot.bin
+program.hex ──→ HexLoader ──→ boot.bin
+ │
+ ▼
+ module.json + registers.json + instructions/ + microcode/
+ │
+ ▼
+ Compiler (.cbeplugin)
+ │
+ ▼
+ my-plugin.cbeplugin ←── run.sh/gradle
+```
+
+### Авто-детекция
+
+При запуске `cbecc build
`, тулчейн проверяет файлы в порядке приоритета:
+
+1. `program.asm` → Assembler
+2. `program.py` → PythonTranslator
+3. `program.c` → CCompiler
+4. `program.cpp` → CCompiler
+5. `program.hex` → HexLoader
+
+Если ни один не найден, ищет `roms/boot.bin`.
+
+---
+
+## 18. Система POST-диагностики
+
+POST (Power-On Self-Test) автоматически выполняется при первом шаге CPU.
+
+### Коды POST
+
+| Код | Описание |
+|-----|---------------------------------------|
+| 0x12| CPU не обнаружен |
+| 0x61| Диск обнаружен |
+| 0x73| CPU обнаружен |
+| 0x74| RAM обнаружена |
+| 0x75| GPU обнаружен |
+| 0x76| KBD обнаружена |
+| 0x77| SND обнаружена |
+| 0x78| Все устройства проверены |
+| 0x00| BOOT_OK — система готова |
+| 0xFF| WARNING — не все компоненты найдены |
+
+### LED индикаторы
+
+- PWR — питание
+- CPU — процессор
+- MEM — память
+- VID — видео
+- KBD — клавиатура
+- SND — звук
+- DSK — диск
+- CLK — тактовый генератор
+
+Адреса memory-mapped I/O для POST:
+- `0xFE` — POST code
+- `0xFD` — Error code
+- `0xFC` — LED status
+
+---
+
+## 19. Memory-mapped I/O
+
+| Адрес | Размер | Назначение |
+|-------|--------|-------------------------------------|
+| 0xB0 | 1 | DISK_SECTOR_ADDR (LBA сектора) |
+| 0xB1 | 1 | DISK_READY_ADDR (1 = готов) |
+| 0xB2 | 512 | DISK_DATA_BASE (данные сектора) |
+| 0xC0 | 1 | GPU_OUT_CHAR (вывод символа) |
+| 0xD0 | 16 | DEVICE_TABLE_BASE (таблица устройств)|
+| 0xE0 | 16 | BIOS_INFO_BASE (информация BIOS) |
+| 0xF7 | 1 | BIOS_TRIGGER (перезапуск диагностики)|
+| 0xF9 | 1 | SND_BEEP_ADDR (звуковой сигнал) |
+| 0xFA | 1 | KBD_STATUS_ADDR (0 = нет, 1 = есть) |
+| 0xFB | 1 | KBD_DATA_ADDR (код клавиши) |
+| 0xFC | 1 | LED_STATUS_ADDR (LED индикаторы) |
+| 0xFD | 1 | ERROR_CODE_ADDR (код ошибки) |
+| 0xFE | 1 | POST_CODE_ADDR (POST код) |
+
+---
+
+## 20. Бесконечные шаги и детектор циклов
+
+### Бесконечное выполнение
+
+В CBE нет максимального лимита шагов. CPU выполняется:
+
+- **В GUI:** пока не нажата кнопка Stop или CPU не выполнит HLT (0xFF)
+- **В консоли:** пока CPU не выполнит HLT или процесс не будет прерван
+
+### Детектор бесконечных циклов
+
+В Engine встроен `LoopDetector`, который:
+
+1. Записывает последние 64 значения PC
+2. Анализирует повторяющиеся паттерны (длиной 1-8)
+3. При обнаружении цикла выводит сообщение в консоль
+4. **НЕ останавливает выполнение** — циклы не прерываются
+
+Пример сообщения:
+```
+[LoopDetector] Infinite loop detected: jump pattern [0x14, 0x06] repeats indefinitely. Execution continues.
+```
+
+---
+
+## 21. Тёмная тема GUI
+
+Эмулятор CBE использует тёмную тему по умолчанию.
+
+### Цветовая схема
+
+| Элемент | Цвет |
+|------------------|------------------------|
+| Фон | #1E1E24 |
+| Панели | #26262E |
+| Границы | #32323C |
+| Текст основной | #C8C8D2 |
+| Текст яркий | #DCDCF0 |
+| Акцент (синий) | #50A0FF |
+| Зелёный (терминал)| #50DC78 |
+| Оранжевый (7-seg) | #FF6414 |
+
+### Компоненты
+
+Все панели, текстовые поля, списки, скроллы, кнопки и рамки используют тёмную тему, настроенную через UIManager и явные цвета компонентов.
+
+### Изменение темы
+
+Для возврата к системной теме, закомментируйте `applyDarkUIManager()` в `EmulatorWindow.java` и раскомментируйте:
+```java
+UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
+```
+
+---
+
+## 22. Примеры
+
+### Полный пример плагина
+
+Структура:
+```
+examples/tiny-cpu.cpu/
+├── module.json
+├── registers.json
+├── instructions/ (18 инструкций)
+│ ├── 0x00.json NOP
+│ ├── 0x01.json MOV_A_B
+│ ├── 0x02.json MOV_IMM_A
+│ ├── 0x03.json ADD
+│ ├── 0x04.json SUB
+│ ├── 0x05.json CMP_A_B
+│ ├── 0x06.json JMP
+│ ├── 0x07.json JC
+│ ├── 0x08.json CMP_A_B (reuse)
+│ ├── 0x09.json MOV_IMM_B
+│ ├── 0x0A.json STORE_A
+│ ├── 0x0B.json LOAD_A
+│ ├── 0x0C.json JMP_IMM
+│ ├── 0x0D.json JZ_IMM
+│ ├── 0x0E.json RET
+│ ├── 0x0F.json INC_A
+│ ├── 0x10.json CALL
+│ └── 0x11.json (stack ops)
+├── microcode/
+│ └── bus.json
+└── roms/
+ └── boot.bin
+```
+
+### Запуск эмулятора
+
+```bash
+# Сборка + запуск с GUI
+./run.sh
+
+# Только сборка
+./run.sh build
+
+# Без GUI (консоль)
+./run.sh nogui
+
+# Прямой запуск с кастомными плагинами
+java -cp modules/gui/build/libs/gui-0.1.0.jar:modules/loader/build/libs/loader-0.1.0.jar:modules/core/build/libs/core-0.1.0.jar com.cbe.gui.Main \
+ --cpu build/tiny-cpu.cbeplugin \
+ --ram build/basic-ram.cbeplugin \
+ --gpu build/vga-display.cbeplugin
+
+# Собрать fat-jar
+./run.sh jar
+```
+
+---
+
+## 23. Устранение неполадок
+
+### "Invalid magic: expected CBEPLUGIN"
+
+Файл .cbeplugin повреждён или это не валидный плагин. Пересоберите:
+```bash
+./run.sh build
+```
+
+### "Missing module.json"
+
+Убедитесь, что source-директория содержит `module.json`.
+
+### "No CPU loaded"
+
+Используйте флаг `--cpu`:
+```bash
+# По директории
+--cpu examples/tiny-cpu.cpu
+# По .cbeplugin
+--cpu build/tiny-cpu.cbeplugin
+```
+
+### "Checksum mismatch"
+
+Файл .cbeplugin повреждён. Пересоберите.
+
+### "Unknown opcode: 0xNN"
+
+Инструкция с данным опкодом не определена в вашем CPU. Проверьте `instructions/`.
+
+### GPU не отображает ничего
+
+1. Убедитесь, что GPU загружен (`--gpu examples/vga-display.gpu`)
+2. CPU должен писать в порт `0xC0`
+3. Проверьте, что boot ROM содержит валидный код
+4. После HLT (0xFF) CPU останавливается — новые символы не выводятся
+
+### Клавиатура не реагирует
+
+1. Убедитесь, что KBD модуль загружен
+2. CPU должен читать статус (`0xFA`) и данные (`0xFB`)
+3. После чтения данных CPU должен записать `0` в статус (`0xFA`) для acknowledge
+
+---
+
+## 24. Справочник команд
+
+### cbecc
+
+```bash
+# Сборка плагина из директории
+cbecc build -o
+
+# Ассемблирование .asm → .bin
+cbecc asm -o --arch
+
+# Конвертация hex → .bin
+cbecc hex -o
+
+# Компиляция C/C++ → .bin
+cbecc ccompile -o --arch
+
+# Трансляция Python → .bin
+cbecc py -o
+
+# Сборка плагина с готовой программой
+cbecc plugin --program -o
+```
+
+### cbe-emu
+
+```bash
+# Запуск с GUI
+./run.sh
+
+# Запуск без GUI
+./run.sh nogui
+
+# Прямой запуск Java
+java -cp com.cbe.gui.Main \
+ --cpu --ram --gpu \
+ [--kbd ] [--snd ] [--bios ] [--disk ] \
+ [--program ] [--nogui]
+
+# Параметры RAM
+--ram --ram-base --ram-size
+```
+
+### Gradle
+
+```bash
+# Полная сборка всех модулей
+./gradlew build
+
+# Сборка fat-jar
+./gradlew :modules:gui:stage
+
+# Сборка native-образа (jpackage)
+./gradlew :modules:gui:packageImage
+
+# Сборка Windows portable
+./gradlew :modules:gui:portableWindowsZip
+```
+
+### run.sh
+
+```bash
+./run.sh build # пересобрать все jar
+./run.sh compile # скомпилировать example-плагины
+./run.sh nogui # запуск без GUI
+./run.sh disk # загрузка с диска (nogui)
+./run.sh # запуск с GUI
+./run.sh jar # fat-jar
+./run.sh image # native-образ
+./run.sh windows # Windows portable
+```
diff --git a/README.md b/README.md
index 70ec394..077b601 100644
--- a/README.md
+++ b/README.md
@@ -1,9 +1,7 @@
# CBE
C.B.E. - CREATE. BUILD. EXECUTE.
-Что такое C.B.E.?
-
-C.B.E. - Платфорам для создания своего:
+Платформа для создания своего:
- процессора
- контроллера оперативки
- видюхи
@@ -11,4 +9,40 @@ C.B.E. - Платфорам для создания своего:
- чипсета
- архитектуры
- почти всего в компе
-- та даже gpu дрйвер что бы выводить изображение в COM1-to-VGA
+- та даже gpu драйвер что бы выводить изображение в COM1-to-VGA
+
+## Новые возможности
+
+- **Тёмная тема** — стильный тёмный GUI для эмулятора
+- **Бесконечные шаги** — нет лимита на количество инструкций, детектор бесконечных циклов
+- **Мультиязычный тулчейн** — пишите программы для своего CPU на ASM, C/C++, Python или hex-машинном коде
+- **Полное руководство** — см. [MANUAL.md](MANUAL.md) для детального описания
+
+## Быстрый старт
+
+```bash
+./run.sh # Сборка + запуск с GUI
+./run.sh build # Только сборка
+./run.sh nogui # Запуск без GUI
+```
+
+## Инструкция
+
+Полное руководство по созданию плагинов, систем, написанию программ на разных языках и т.п.:
+→ **[MANUAL.md](MANUAL.md)**
+
+## Примеры
+
+```bash
+# Собрать плагин из директории с авто-детекцией program.asm/.c/.py/.hex
+cbecc build examples/asm-demo.cpu -o build/asm-demo.cbeplugin
+
+# Ассемблировать .asm в .bin
+cbecc asm examples/asm-demo.cpu/program.asm -o build/demo.bin --arch examples/tiny-cpu.cpu
+
+# Транслировать Python в байткод
+cbecc py examples/asm-demo.cpu/program.py -o build/demo.bin
+
+# Сконвертировать hex в бинарник
+cbecc hex examples/asm-demo.cpu/program.hex -o build/demo.bin
+```
diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts
new file mode 100644
index 0000000..e582e85
--- /dev/null
+++ b/android/app/build.gradle.kts
@@ -0,0 +1,57 @@
+plugins {
+ alias(libs.plugins.android.application)
+ alias(libs.plugins.kotlin.android)
+}
+
+android {
+ namespace = "com.cbe.android"
+ compileSdk = 35
+
+ defaultConfig {
+ applicationId = "com.cbe.android"
+ minSdk = 26
+ targetSdk = 35
+ versionCode = 1
+ versionName = "1.0.0"
+ }
+
+ buildTypes {
+ release {
+ isMinifyEnabled = true
+ proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"))
+ }
+ }
+
+ compileOptions {
+ sourceCompatibility = JavaVersion.VERSION_17
+ targetCompatibility = JavaVersion.VERSION_17
+ }
+
+ kotlinOptions {
+ jvmTarget = "17"
+ }
+
+ buildFeatures {
+ compose = true
+ }
+
+ composeOptions {
+ kotlinCompilerExtensionVersion = "1.5.11"
+ }
+}
+
+dependencies {
+ implementation(project(":cbe-core"))
+ implementation(libs.androidx.core.ktx)
+ implementation(libs.androidx.lifecycle.runtime.ktx)
+ implementation(libs.androidx.lifecycle.viewmodel.compose)
+ implementation(libs.androidx.activity.compose)
+ implementation(platform(libs.androidx.compose.bom))
+ implementation(libs.androidx.compose.ui)
+ implementation(libs.androidx.compose.ui.graphics)
+ implementation(libs.androidx.compose.ui.tooling.preview)
+ implementation(libs.androidx.compose.material3)
+ implementation(libs.androidx.compose.foundation)
+ implementation(libs.androidx.compose.material.icons.core)
+ debugImplementation(libs.androidx.compose.ui.tooling)
+}
diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..0fb32a0
--- /dev/null
+++ b/android/app/src/main/AndroidManifest.xml
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/android/app/src/main/assets/plugins/basic-kbd.cbeplugin b/android/app/src/main/assets/plugins/basic-kbd.cbeplugin
new file mode 100644
index 0000000..44a9d33
Binary files /dev/null and b/android/app/src/main/assets/plugins/basic-kbd.cbeplugin differ
diff --git a/android/app/src/main/assets/plugins/basic-ram.cbeplugin b/android/app/src/main/assets/plugins/basic-ram.cbeplugin
new file mode 100644
index 0000000..d16365f
Binary files /dev/null and b/android/app/src/main/assets/plugins/basic-ram.cbeplugin differ
diff --git a/android/app/src/main/assets/plugins/basic-snd.cbeplugin b/android/app/src/main/assets/plugins/basic-snd.cbeplugin
new file mode 100644
index 0000000..d30d7a1
Binary files /dev/null and b/android/app/src/main/assets/plugins/basic-snd.cbeplugin differ
diff --git a/android/app/src/main/assets/plugins/big-disk.cbeplugin b/android/app/src/main/assets/plugins/big-disk.cbeplugin
new file mode 100644
index 0000000..7f74108
Binary files /dev/null and b/android/app/src/main/assets/plugins/big-disk.cbeplugin differ
diff --git a/android/app/src/main/assets/plugins/big-ram.cbeplugin b/android/app/src/main/assets/plugins/big-ram.cbeplugin
new file mode 100644
index 0000000..fdde200
Binary files /dev/null and b/android/app/src/main/assets/plugins/big-ram.cbeplugin differ
diff --git a/android/app/src/main/assets/plugins/system-disk.cbeplugin b/android/app/src/main/assets/plugins/system-disk.cbeplugin
new file mode 100644
index 0000000..aff50eb
Binary files /dev/null and b/android/app/src/main/assets/plugins/system-disk.cbeplugin differ
diff --git a/android/app/src/main/assets/plugins/tiny-bios.cbeplugin b/android/app/src/main/assets/plugins/tiny-bios.cbeplugin
new file mode 100644
index 0000000..c0cadf2
Binary files /dev/null and b/android/app/src/main/assets/plugins/tiny-bios.cbeplugin differ
diff --git a/android/app/src/main/assets/plugins/tiny-cpu.cbeplugin b/android/app/src/main/assets/plugins/tiny-cpu.cbeplugin
new file mode 100644
index 0000000..f78e4a1
Binary files /dev/null and b/android/app/src/main/assets/plugins/tiny-cpu.cbeplugin differ
diff --git a/android/app/src/main/assets/plugins/vga-display.cbeplugin b/android/app/src/main/assets/plugins/vga-display.cbeplugin
new file mode 100644
index 0000000..352dd6b
Binary files /dev/null and b/android/app/src/main/assets/plugins/vga-display.cbeplugin differ
diff --git a/android/app/src/main/assets/plugins/vga-plus.cbeplugin b/android/app/src/main/assets/plugins/vga-plus.cbeplugin
new file mode 100644
index 0000000..337171d
Binary files /dev/null and b/android/app/src/main/assets/plugins/vga-plus.cbeplugin differ
diff --git a/android/app/src/main/java/com/cbe/android/CbeApp.kt b/android/app/src/main/java/com/cbe/android/CbeApp.kt
new file mode 100644
index 0000000..49fb5c7
--- /dev/null
+++ b/android/app/src/main/java/com/cbe/android/CbeApp.kt
@@ -0,0 +1,15 @@
+package com.cbe.android
+
+import android.app.Application
+import com.cbe.android.engine.ModuleProvider
+
+class CbeApp : Application() {
+ lateinit var moduleProvider: ModuleProvider
+ private set
+
+ override fun onCreate() {
+ super.onCreate()
+ moduleProvider = ModuleProvider(this)
+ moduleProvider.setupBeep()
+ }
+}
diff --git a/android/app/src/main/java/com/cbe/android/EmulatorViewModel.kt b/android/app/src/main/java/com/cbe/android/EmulatorViewModel.kt
new file mode 100644
index 0000000..d3ca206
--- /dev/null
+++ b/android/app/src/main/java/com/cbe/android/EmulatorViewModel.kt
@@ -0,0 +1,170 @@
+package com.cbe.android
+
+import android.app.Application
+import androidx.lifecycle.AndroidViewModel
+import androidx.lifecycle.viewModelScope
+import com.cbe.android.engine.AndroidEngine
+import com.cbe.android.engine.ModuleProvider
+import com.cbe.android.engine.PluginConfig
+import com.cbe.android.engine.PluginEntry
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.isActive
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.asStateFlow
+
+data class EmulatorUiState(
+ val gpuText: String = "",
+ val postCode: Int = 0,
+ val ledStatus: Int = 0,
+ val isRunning: Boolean = false,
+ val isHalted: Boolean = false,
+ val instructionCount: Long = 0,
+ val systemInfo: String = "",
+ val statusMessage: String = "",
+ val pluginConfig: PluginConfig = PluginConfig(),
+ val pluginEntries: List = emptyList(),
+ val pluginPaths: Map = emptyMap(),
+ val speedIndex: Int = 3,
+ val speedLabel: String = "1x",
+ val speedIps: Long = 100_000
+)
+
+class EmulatorViewModel(application: Application) : AndroidViewModel(application) {
+
+ private val _uiState = MutableStateFlow(EmulatorUiState())
+ val uiState: StateFlow = _uiState.asStateFlow()
+
+ private val engine = AndroidEngine()
+ private var runJob: Job? = null
+ private var refreshJob: Job? = null
+
+ private val moduleProvider get() = getApplication().moduleProvider
+
+ init {
+ engine.init()
+ }
+
+ fun initialize() {
+ viewModelScope.launch {
+ refreshPluginList()
+ reloadPlugins()
+ }
+ }
+
+ fun refreshPluginList() {
+ viewModelScope.launch {
+ val entries = moduleProvider.listAllPlugins()
+ val paths = moduleProvider.getPluginPaths()
+ _uiState.value = _uiState.value.copy(
+ pluginEntries = entries,
+ pluginPaths = paths
+ )
+ }
+ }
+
+ fun setSpeed(index: Int) {
+ engine.setSpeed(index)
+ val speed = engine.speed
+ _uiState.value = _uiState.value.copy(
+ speedIndex = index,
+ speedLabel = speed.label,
+ speedIps = speed.ips
+ )
+ }
+
+ fun updatePluginConfig(slot: String, pluginName: String) {
+ viewModelScope.launch {
+ val cfg = _uiState.value.pluginConfig
+ val newCfg = when (slot) {
+ "cpu" -> cfg.copy(cpu = pluginName)
+ "ram" -> cfg.copy(ram = pluginName)
+ "gpu" -> cfg.copy(gpu = pluginName)
+ "kbd" -> cfg.copy(kbd = pluginName)
+ "snd" -> cfg.copy(snd = pluginName)
+ "bios" -> cfg.copy(bios = pluginName)
+ "disk" -> cfg.copy(disk = if (pluginName == "none") null else pluginName)
+ else -> cfg
+ }
+ reloadPlugins(newCfg)
+ }
+ }
+
+ fun reloadPlugins(config: PluginConfig? = null) {
+ viewModelScope.launch {
+ val cfg = config ?: _uiState.value.pluginConfig
+ _uiState.value = _uiState.value.copy(pluginConfig = cfg)
+
+ try {
+ val plugins = moduleProvider.extractPlugins(cfg)
+ engine.loadModules(plugins)
+ _uiState.value = _uiState.value.copy(
+ systemInfo = engine.systemInfo,
+ statusMessage = ""
+ )
+ startPolling()
+ } catch (e: Exception) {
+ _uiState.value = _uiState.value.copy(
+ statusMessage = "Failed to load: ${e.message}"
+ )
+ }
+ }
+ }
+
+ private fun startPolling() {
+ refreshJob?.cancel()
+ refreshJob = viewModelScope.launch {
+ while (isActive) {
+ updateUiState()
+ delay(50)
+ }
+ }
+ }
+
+ private fun updateUiState() {
+ _uiState.value = _uiState.value.copy(
+ gpuText = engine.getGpuText() ?: "",
+ postCode = engine.postCode,
+ ledStatus = engine.ledStatus,
+ isRunning = engine.isRunning,
+ isHalted = engine.isHalted,
+ instructionCount = engine.instructionsExecuted
+ )
+ engine.markGpuClean()
+ }
+
+ fun runFull() {
+ if (engine.isRunning || engine.isHalted) return
+ runJob = viewModelScope.launch {
+ engine.runFull()
+ }
+ }
+
+ fun pause() {
+ runJob?.cancel()
+ }
+
+ fun pushKey(keyCode: Int) {
+ engine.pushKey(keyCode)
+ }
+
+ fun reset() {
+ runJob?.cancel()
+ engine.reset()
+ updateUiState()
+ viewModelScope.launch {
+ withContext(Dispatchers.IO) { engine.engine.runDiagnostics() }
+ updateUiState()
+ }
+ }
+
+ override fun onCleared() {
+ super.onCleared()
+ runJob?.cancel()
+ refreshJob?.cancel()
+ }
+}
diff --git a/android/app/src/main/java/com/cbe/android/MainActivity.kt b/android/app/src/main/java/com/cbe/android/MainActivity.kt
new file mode 100644
index 0000000..9bff8f5
--- /dev/null
+++ b/android/app/src/main/java/com/cbe/android/MainActivity.kt
@@ -0,0 +1,29 @@
+package com.cbe.android
+
+import android.os.Bundle
+import androidx.activity.ComponentActivity
+import androidx.activity.compose.setContent
+import androidx.activity.enableEdgeToEdge
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.material3.Surface
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.ui.Modifier
+import androidx.lifecycle.viewmodel.compose.viewModel
+import com.cbe.android.ui.MainScreen
+import com.cbe.android.ui.theme.CbeTheme
+
+class MainActivity : ComponentActivity() {
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ enableEdgeToEdge()
+ setContent {
+ CbeTheme {
+ Surface(modifier = Modifier.fillMaxSize()) {
+ val viewModel: EmulatorViewModel = viewModel()
+ LaunchedEffect(Unit) { viewModel.initialize() }
+ MainScreen(viewModel)
+ }
+ }
+ }
+ }
+}
diff --git a/android/app/src/main/java/com/cbe/android/engine/AndroidEngine.kt b/android/app/src/main/java/com/cbe/android/engine/AndroidEngine.kt
new file mode 100644
index 0000000..792c4ff
--- /dev/null
+++ b/android/app/src/main/java/com/cbe/android/engine/AndroidEngine.kt
@@ -0,0 +1,183 @@
+package com.cbe.android.engine
+
+import com.cbe.loader.Engine
+import com.cbe.loader.SimpleRegisters
+import kotlin.coroutines.coroutineContext
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.withContext
+import java.io.File
+
+data class SpeedConfig(
+ val label: String = "1x",
+ val batchSize: Int = 1000,
+ val batchDelayMs: Long = 10
+) {
+ /** Estimated instructions per second */
+ val ips: Long get() = (batchSize * 1000L) / maxOf(batchDelayMs, 1)
+}
+
+class AndroidEngine {
+
+ val engine = Engine()
+ val registers = SimpleRegisters()
+
+ var postCode: Int = 0
+ private set
+ var ledStatus: Int = 0
+ private set
+ var postDescription: String = ""
+ private set
+
+ var isRunning: Boolean = false
+ private set
+ var isHalted: Boolean = false
+ private set
+ var instructionsExecuted: Long = 0
+ private set
+ var systemInfo: String = ""
+ private set
+
+ var speed: SpeedConfig = SPEEDS[3] // default = 1x
+ private set
+
+ private var postListener: Engine.PostListener? = null
+
+ companion object {
+ val SPEEDS = listOf(
+ SpeedConfig("0.25x", 100, 40),
+ SpeedConfig("0.5x", 250, 20),
+ SpeedConfig("1x", 1000, 10),
+ SpeedConfig("2x", 2000, 5),
+ SpeedConfig("4x", 5000, 2),
+ SpeedConfig("MAX", 50000, 0)
+ )
+ }
+
+ fun init() {
+ postListener = Engine.PostListener { code, leds, desc ->
+ postCode = code
+ ledStatus = leds
+ postDescription = desc ?: ""
+ }
+ engine.addPostListener(postListener!!)
+ }
+
+ fun setSpeed(index: Int) {
+ if (index in SPEEDS.indices) speed = SPEEDS[index]
+ }
+
+ suspend fun loadModules(plugins: Map) = withContext(Dispatchers.IO) {
+ val cpu = plugins["tiny-cpu"]
+ val ram = plugins["basic-ram"]
+ val gpu = plugins["vga-display"]
+ val kbd = plugins["basic-kbd"]
+ val snd = plugins["basic-snd"]
+ val bios = plugins["tiny-bios"]
+ val disk = plugins["big-disk"] ?: plugins["system-disk"]
+
+ if (cpu != null) engine.loadCompiledCpu(cpu.toPath())
+ if (ram != null) engine.loadCompiledRam(ram.toPath(), 0x00)
+ if (gpu != null) engine.loadCompiledGpu(gpu.toPath())
+ if (kbd != null) engine.loadCompiledKbd(kbd.toPath())
+ if (snd != null) engine.loadCompiledSnd(snd.toPath())
+ if (bios != null) engine.loadCompiledBios(bios.toPath())
+ if (disk != null) engine.loadCompiledDisk(disk.toPath())
+
+ systemInfo = engine.runDiagnostics()
+ }
+
+ suspend fun runFull() {
+ if (isRunning || isHalted) return
+ isRunning = true
+ try {
+ while (coroutineContext[Job]?.isActive != false && !isHalted) {
+ val batchStart = instructionsExecuted
+ val limit = speed.batchSize
+ while (coroutineContext[Job]?.isActive != false && !isHalted
+ && instructionsExecuted - batchStart < limit) {
+ val alive = engine.step(registers)
+ instructionsExecuted++
+ if (!alive) {
+ isHalted = true
+ break
+ }
+ }
+ if (speed.batchDelayMs > 0 && !isHalted && coroutineContext[Job]?.isActive != false) {
+ delay(speed.batchDelayMs)
+ }
+ }
+ } finally {
+ isRunning = false
+ }
+ }
+
+ fun step(): Boolean {
+ if (isHalted) return false
+ val alive = engine.step(registers)
+ instructionsExecuted++
+ if (!alive) isHalted = true
+ return alive
+ }
+
+ suspend fun runSteps(count: Int): Int {
+ if (isRunning || isHalted) return 0
+ isRunning = true
+ try {
+ var executed = 0
+ for (i in 0 until count) {
+ if (coroutineContext[Job]?.isActive == false) break
+ if (!engine.step(registers)) {
+ isHalted = true
+ break
+ }
+ executed++
+ }
+ instructionsExecuted += executed
+ return executed
+ } finally {
+ isRunning = false
+ }
+ }
+
+ fun pushKey(keyCode: Int) {
+ engine.pushKey(keyCode)
+ }
+
+ fun reset() {
+ engine.reset()
+ registers.write("pc", 0)
+ registers.write("sp", 0x80)
+ isHalted = false
+ isRunning = false
+ instructionsExecuted = 0
+ postCode = 0
+ ledStatus = 0
+ }
+
+ fun getGpuText(): String? {
+ return engine.sourceGpu?.readString()
+ ?: engine.compiledGpu?.readString()
+ }
+
+ fun getGpuRows(): Int {
+ return engine.sourceGpu?.getRows()
+ ?: engine.compiledGpu?.getRows() ?: 0
+ }
+
+ fun getGpuCols(): Int {
+ return engine.sourceGpu?.getCols()
+ ?: engine.compiledGpu?.getCols() ?: 0
+ }
+
+ fun isGpuDirty(): Boolean {
+ return engine.sourceGpu?.isDirty()
+ ?: engine.compiledGpu?.isDirty() ?: false
+ }
+
+ fun markGpuClean() {
+ engine.sourceGpu?.markClean()
+ engine.compiledGpu?.markClean()
+ }
+}
diff --git a/android/app/src/main/java/com/cbe/android/engine/ModuleProvider.kt b/android/app/src/main/java/com/cbe/android/engine/ModuleProvider.kt
new file mode 100644
index 0000000..74e734e
--- /dev/null
+++ b/android/app/src/main/java/com/cbe/android/engine/ModuleProvider.kt
@@ -0,0 +1,172 @@
+package com.cbe.android.engine
+
+import android.content.Context
+import android.os.Environment
+import com.cbe.loader.AudioBridge
+import com.cbe.loader.BeepHandler
+import java.io.File
+import java.io.FileOutputStream
+
+data class PluginEntry(
+ val name: String,
+ val file: File,
+ val source: String // "assets", "external", "media"
+)
+
+data class PluginConfig(
+ val cpu: String = "tiny-cpu.cbeplugin",
+ val ram: String = "basic-ram.cbeplugin",
+ val gpu: String = "vga-display.cbeplugin",
+ val kbd: String = "basic-kbd.cbeplugin",
+ val snd: String = "basic-snd.cbeplugin",
+ val bios: String = "tiny-bios.cbeplugin",
+ val disk: String? = "big-disk.cbeplugin"
+) {
+ fun allPlugins(): List {
+ val list = mutableListOf(cpu, ram, gpu, kbd, snd, bios)
+ if (disk != null) list.add(disk)
+ return list
+ }
+
+ companion object {
+ val SLOTS = listOf("cpu", "ram", "gpu", "kbd", "snd", "bios", "disk")
+ }
+}
+
+class ModuleProvider(private val context: Context) {
+
+ /** Internal directory where assets are extracted */
+ private val pluginDir: File by lazy {
+ File(context.filesDir, "plugins").also { it.mkdirs() }
+ }
+
+ /** App-specific external storage: /sdcard/Android/data//files/plugins/ */
+ private val externalPluginDir: File by lazy {
+ context.getExternalFilesDir("plugins")?.also { it.mkdirs() }
+ ?: File(context.filesDir, "external-plugins").also { it.mkdirs() }
+ }
+
+ /** Shared media path: /sdcard/Android/media/com.cbe/ */
+ private val mediaPluginDir: File by lazy {
+ try {
+ File(Environment.getExternalStorageDirectory(), "Android/media/com.cbe")
+ } catch (e: Exception) {
+ File(context.filesDir, "media-plugins").also { it.mkdirs() }
+ }
+ }
+
+ /** Scan all sources and return available plugins with their source info.
+ * External files override internal ones with the same name. */
+ fun listAllPlugins(): List {
+ val seen = mutableSetOf()
+ val result = mutableListOf()
+
+ // External overrides (highest priority)
+ for (dir in listOf(mediaPluginDir, externalPluginDir)) {
+ if (dir.exists() && dir.isDirectory) {
+ val source = if (dir == mediaPluginDir) "media" else "external"
+ dir.listFiles { f -> f.name.endsWith(".cbeplugin") }?.forEach { f ->
+ if (seen.add(f.name)) {
+ result.add(PluginEntry(f.name, f, source))
+ }
+ }
+ }
+ }
+
+ // Internal assets (lower priority)
+ try {
+ context.assets.list("plugins")?.forEach { name ->
+ if (name.endsWith(".cbeplugin") && seen.add(name)) {
+ val target = File(pluginDir, name)
+ result.add(PluginEntry(name, target, "assets"))
+ }
+ }
+ } catch (e: Exception) {
+ android.util.Log.w("ModuleProvider", "Failed to list assets: ${e.message}")
+ }
+
+ return result
+ }
+
+ /** Extract all needed plugins from assets to internal storage if not already there.
+ * External files are used in-place (no copy needed). */
+ fun extractPlugins(config: PluginConfig = PluginConfig()): Map {
+ val all = listAllPlugins()
+ val byName = all.associateBy { it.name }
+ val result = mutableMapOf()
+
+ for (name in config.allPlugins()) {
+ val entry = byName[name]
+ if (entry != null && entry.file.exists()) {
+ // External file found — use directly
+ val key = name.removeSuffix(".cbeplugin")
+ result[key] = entry.file
+ } else {
+ // Not found externally — extract from assets
+ val target = File(pluginDir, name)
+ if (!target.exists()) {
+ try {
+ context.assets.open("plugins/$name").use { input ->
+ FileOutputStream(target).use { output ->
+ input.copyTo(output)
+ }
+ }
+ android.util.Log.i("ModuleProvider", "Extracted $name to $target")
+ } catch (e: Exception) {
+ android.util.Log.w("ModuleProvider", "Failed to extract $name: ${e.message}")
+ }
+ }
+ if (target.exists()) {
+ val key = name.removeSuffix(".cbeplugin")
+ result[key] = target
+ }
+ }
+ }
+ return result
+ }
+
+ companion object {
+ /** List plugins grouped by their likely slot type based on filename patterns. */
+ fun classifyPlugins(entries: List): Map> {
+ val result = linkedMapOf>()
+ for (slot in PluginConfig.SLOTS) result[slot] = mutableListOf()
+ result["other"] = mutableListOf()
+
+ for (e in entries) {
+ val name = e.name.lowercase()
+ val slot = when {
+ name.contains("cpu") || name.contains("tiny") -> "cpu"
+ name.contains("ram") || name.contains("memory") -> "ram"
+ name.contains("gpu") || name.contains("vga") || name.contains("video") || name.contains("display") -> "gpu"
+ name.contains("kbd") || name.contains("keyboard") || name.contains("key") -> "kbd"
+ name.contains("snd") || name.contains("sound") || name.contains("audio") || name.contains("speaker") -> "snd"
+ name.contains("bios") || name.contains("system") -> "bios"
+ name.contains("disk") || name.contains("disc") || name.contains("storage") -> "disk"
+ else -> "other"
+ }
+ result.getOrPut(slot) { mutableListOf() }.add(e)
+ }
+ return result
+ }
+ }
+
+ fun getPluginPaths(): Map {
+ return mapOf(
+ "Internal (assets)" to pluginDir.absolutePath,
+ "External (app)" to externalPluginDir.absolutePath,
+ "Media (shared)" to mediaPluginDir.absolutePath
+ )
+ }
+
+ fun setupBeep() {
+ AudioBridge.setBeepHandler(BeepHandler {
+ try {
+ val toneGen = android.media.ToneGenerator(
+ android.media.ToneGenerator.TONE_DTMF_0, 60
+ )
+ toneGen.startTone(android.media.ToneGenerator.TONE_DTMF_0, 80)
+ toneGen.release()
+ } catch (_: Exception) {}
+ })
+ }
+}
diff --git a/android/app/src/main/java/com/cbe/android/ui/KeyboardSheet.kt b/android/app/src/main/java/com/cbe/android/ui/KeyboardSheet.kt
new file mode 100644
index 0000000..5d65478
--- /dev/null
+++ b/android/app/src/main/java/com/cbe/android/ui/KeyboardSheet.kt
@@ -0,0 +1,132 @@
+package com.cbe.android.ui
+
+import androidx.compose.foundation.layout.*
+import androidx.compose.material3.*
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.text.font.FontFamily
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun KeyboardSheet(
+ onKey: (Int) -> Unit,
+ onDismiss: () -> Unit,
+ modifier: Modifier = Modifier
+) {
+ val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
+
+ ModalBottomSheet(
+ onDismissRequest = onDismiss,
+ sheetState = sheetState,
+ containerColor = MaterialTheme.colorScheme.surface,
+ modifier = modifier
+ ) {
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(16.dp),
+ verticalArrangement = Arrangement.spacedBy(6.dp),
+ horizontalAlignment = Alignment.CenterHorizontally
+ ) {
+ // Row 1: 0-9
+ KeyboardRow(keys = listOf(
+ "0" to 0x30, "1" to 0x31, "2" to 0x32, "3" to 0x33,
+ "4" to 0x34, "5" to 0x35, "6" to 0x36, "7" to 0x37,
+ "8" to 0x38, "9" to 0x39
+ ), onKey)
+
+ // Row 2: A-Z top half
+ KeyboardRow(keys = listOf(
+ "Q" to 0x51, "W" to 0x57, "E" to 0x45, "R" to 0x52,
+ "T" to 0x54, "Y" to 0x59, "U" to 0x55, "I" to 0x49,
+ "O" to 0x4F, "P" to 0x50
+ ), onKey)
+
+ // Row 3: A-Z bottom half
+ KeyboardRow(keys = listOf(
+ "A" to 0x41, "S" to 0x53, "D" to 0x44, "F" to 0x46,
+ "G" to 0x47, "H" to 0x48, "J" to 0x4A, "K" to 0x4B,
+ "L" to 0x4C
+ ), onKey)
+
+ // Row 4: Shift, Z-M, Enter, Backspace
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ horizontalArrangement = Arrangement.spacedBy(4.dp)
+ ) {
+ KeyButton("Shift", modifier = Modifier.weight(1.5f)) {
+ onKey(0x10) // SHIFT
+ }
+ for ((label, code) in listOf(
+ "Z" to 0x5A, "X" to 0x58, "C" to 0x43,
+ "V" to 0x56, "B" to 0x42, "N" to 0x4E, "M" to 0x4D
+ )) {
+ KeyButton(label, modifier = Modifier.weight(1f)) { onKey(code) }
+ }
+ KeyButton("", modifier = Modifier.weight(1f)) {}
+ }
+
+ // Row 5: Space, function keys
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ horizontalArrangement = Arrangement.spacedBy(4.dp)
+ ) {
+ KeyButton("Enter", modifier = Modifier.weight(2f)) {
+ onKey(0x0D) // CR
+ }
+ KeyButton("Space", modifier = Modifier.weight(4f)) {
+ onKey(0x20) // Space
+ }
+ KeyButton("BS", modifier = Modifier.weight(1.5f)) {
+ onKey(0x08) // Backspace
+ }
+ }
+
+ Spacer(Modifier.height(16.dp))
+ }
+ }
+}
+
+@Composable
+private fun KeyboardRow(
+ keys: List>,
+ onKey: (Int) -> Unit
+) {
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ horizontalArrangement = Arrangement.spacedBy(4.dp)
+ ) {
+ for ((label, code) in keys) {
+ KeyButton(label, modifier = Modifier.weight(1f)) { onKey(code) }
+ }
+ }
+}
+
+@Composable
+private fun KeyButton(
+ label: String,
+ modifier: Modifier = Modifier,
+ onClick: () -> Unit
+) {
+ Button(
+ onClick = onClick,
+ modifier = modifier.height(44.dp),
+ shape = MaterialTheme.shapes.small,
+ contentPadding = PaddingValues(0.dp),
+ colors = ButtonDefaults.buttonColors(
+ containerColor = MaterialTheme.colorScheme.surfaceVariant
+ )
+ ) {
+ Text(
+ text = label,
+ style = MaterialTheme.typography.labelLarge.copy(
+ fontFamily = FontFamily.Monospace,
+ fontSize = 14.sp
+ ),
+ maxLines = 1
+ )
+ }
+}
diff --git a/android/app/src/main/java/com/cbe/android/ui/MainScreen.kt b/android/app/src/main/java/com/cbe/android/ui/MainScreen.kt
new file mode 100644
index 0000000..d5e3fbd
--- /dev/null
+++ b/android/app/src/main/java/com/cbe/android/ui/MainScreen.kt
@@ -0,0 +1,230 @@
+package com.cbe.android.ui
+
+import androidx.compose.foundation.layout.*
+import androidx.compose.foundation.selection.selectable
+import androidx.compose.foundation.selection.selectableGroup
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.PlayArrow
+import androidx.compose.material.icons.filled.Refresh
+import androidx.compose.material.icons.filled.Close
+import androidx.compose.material.icons.filled.Settings
+import androidx.compose.material3.*
+import androidx.compose.runtime.*
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.semantics.Role
+import androidx.compose.ui.text.font.FontFamily
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import com.cbe.android.EmulatorViewModel
+import com.cbe.android.engine.AndroidEngine
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun MainScreen(viewModel: EmulatorViewModel) {
+ val uiState by viewModel.uiState.collectAsState()
+ var showKeyboard by remember { mutableStateOf(false) }
+ var showPlugins by remember { mutableStateOf(false) }
+
+ Scaffold(
+ topBar = {
+ TopAppBar(
+ title = {
+ Text(
+ text = "CBE Emulator",
+ style = MaterialTheme.typography.headlineMedium
+ )
+ },
+ actions = {
+ if (uiState.instructionCount > 0) {
+ Text(
+ text = formatCount(uiState.instructionCount),
+ style = MaterialTheme.typography.labelMedium,
+ modifier = Modifier.padding(end = 8.dp)
+ )
+ }
+ IconButton(onClick = {
+ if (uiState.isRunning) viewModel.pause()
+ else viewModel.runFull()
+ }) {
+ Icon(
+ imageVector = if (uiState.isRunning) Icons.Default.Close
+ else Icons.Default.PlayArrow,
+ contentDescription = if (uiState.isRunning) "Pause" else "Run"
+ )
+ }
+ IconButton(onClick = { viewModel.reset() }) {
+ Icon(
+ imageVector = Icons.Default.Refresh,
+ contentDescription = "Reset"
+ )
+ }
+ },
+ colors = TopAppBarDefaults.topAppBarColors(
+ containerColor = MaterialTheme.colorScheme.background
+ )
+ )
+ },
+ bottomBar = {
+ BottomAppBar(
+ containerColor = MaterialTheme.colorScheme.background
+ ) {
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ horizontalArrangement = Arrangement.SpaceEvenly
+ ) {
+ FilledTonalButton(
+ onClick = { showPlugins = !showPlugins },
+ modifier = Modifier.height(48.dp)
+ ) {
+ Icon(Icons.Default.Settings, contentDescription = null)
+ Spacer(Modifier.width(4.dp))
+ Text("Plugins")
+ }
+ FilledTonalButton(
+ onClick = { showKeyboard = !showKeyboard },
+ modifier = Modifier.height(48.dp)
+ ) {
+ Text(if (showKeyboard) "Hide KBD" else "Show KBD")
+ }
+ }
+ }
+ }
+ ) { padding ->
+ Column(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(padding)
+ .padding(vertical = 8.dp),
+ verticalArrangement = Arrangement.spacedBy(8.dp),
+ horizontalAlignment = Alignment.CenterHorizontally
+ ) {
+ // Speed controls
+ SpeedControl(
+ speedIndex = uiState.speedIndex,
+ speedLabel = uiState.speedLabel,
+ speedIps = uiState.speedIps,
+ onSelectSpeed = { viewModel.setSpeed(it) }
+ )
+
+ // GPU Screen
+ ScreenPanel(
+ text = uiState.gpuText,
+ modifier = Modifier.weight(1f)
+ )
+
+ // POST Panel
+ PostPanel(
+ postCode = uiState.postCode,
+ ledStatus = uiState.ledStatus
+ )
+
+ // Status info
+ if (uiState.isHalted) {
+ Text(
+ text = "HALTED after ${formatCount(uiState.instructionCount)} instructions",
+ style = MaterialTheme.typography.labelMedium.copy(
+ color = MaterialTheme.colorScheme.error
+ )
+ )
+ }
+ if (uiState.statusMessage.isNotEmpty()) {
+ Text(
+ text = uiState.statusMessage,
+ style = MaterialTheme.typography.labelSmall.copy(
+ color = MaterialTheme.colorScheme.error
+ )
+ )
+ }
+ }
+ }
+
+ if (showKeyboard) {
+ KeyboardSheet(
+ onKey = { viewModel.pushKey(it) },
+ onDismiss = { showKeyboard = false }
+ )
+ }
+
+ if (showPlugins) {
+ PluginSheet(
+ entries = uiState.pluginEntries,
+ pluginConfig = uiState.pluginConfig,
+ pluginPaths = uiState.pluginPaths,
+ onSelectPlugin = { slot, name -> viewModel.updatePluginConfig(slot, name) },
+ onRefresh = { viewModel.refreshPluginList() },
+ onDismiss = { showPlugins = false }
+ )
+ }
+}
+
+@Composable
+private fun SpeedControl(
+ speedIndex: Int,
+ speedLabel: String,
+ speedIps: Long,
+ onSelectSpeed: (Int) -> Unit
+) {
+ Card(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(horizontal = 8.dp)
+ ) {
+ Column(modifier = Modifier.padding(8.dp)) {
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ horizontalArrangement = Arrangement.SpaceBetween,
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Text(
+ text = "Speed: $speedLabel",
+ style = MaterialTheme.typography.labelLarge,
+ fontWeight = FontWeight.SemiBold
+ )
+ Text(
+ text = formatIps(speedIps),
+ style = MaterialTheme.typography.labelSmall
+ )
+ }
+ Spacer(Modifier.height(4.dp))
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .selectableGroup(),
+ horizontalArrangement = Arrangement.spacedBy(4.dp)
+ ) {
+ AndroidEngine.SPEEDS.forEachIndexed { index, speedCfg ->
+ FilterChip(
+ selected = index == speedIndex,
+ onClick = { onSelectSpeed(index) },
+ label = {
+ Text(
+ text = speedCfg.label,
+ fontSize = 11.sp,
+ maxLines = 1
+ )
+ },
+ modifier = Modifier.weight(1f)
+ )
+ }
+ }
+ }
+ }
+}
+
+private fun formatCount(count: Long): String {
+ return when {
+ count < 1000 -> "$count steps"
+ count < 1_000_000 -> "${count / 1000}K steps"
+ else -> "${count / 1_000_000}M steps"
+ }
+}
+
+private fun formatIps(ips: Long): String {
+ return when {
+ ips < 1000 -> "$ips IPS"
+ ips < 1_000_000 -> "${ips / 1000}K IPS"
+ else -> "${ips / 1_000_000}M IPS"
+ }
+}
diff --git a/android/app/src/main/java/com/cbe/android/ui/PluginSheet.kt b/android/app/src/main/java/com/cbe/android/ui/PluginSheet.kt
new file mode 100644
index 0000000..a7961e3
--- /dev/null
+++ b/android/app/src/main/java/com/cbe/android/ui/PluginSheet.kt
@@ -0,0 +1,230 @@
+package com.cbe.android.ui
+
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.*
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material3.*
+import androidx.compose.runtime.*
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.font.FontFamily
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import com.cbe.android.engine.ModuleProvider
+import com.cbe.android.engine.PluginConfig
+import com.cbe.android.engine.PluginEntry
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun PluginSheet(
+ entries: List,
+ pluginConfig: PluginConfig,
+ pluginPaths: Map,
+ onSelectPlugin: (slot: String, pluginName: String) -> Unit,
+ onRefresh: () -> Unit,
+ onDismiss: () -> Unit
+) {
+ val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
+
+ val classified = remember(entries) {
+ ModuleProvider.classifyPlugins(entries)
+ }
+
+ ModalBottomSheet(
+ onDismissRequest = onDismiss,
+ sheetState = sheetState,
+ containerColor = MaterialTheme.colorScheme.surface,
+ modifier = Modifier.fillMaxHeight(0.85f)
+ ) {
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(horizontal = 16.dp)
+ .verticalScroll(rememberScrollState()),
+ verticalArrangement = Arrangement.spacedBy(12.dp)
+ ) {
+ Text("Plugin Configuration", style = MaterialTheme.typography.titleLarge)
+
+ // Current config summary
+ Card(modifier = Modifier.fillMaxWidth()) {
+ Column(modifier = Modifier.padding(12.dp)) {
+ Text("Current Config", style = MaterialTheme.typography.labelLarge)
+ Spacer(Modifier.height(4.dp))
+ PluginConfigSummary(pluginConfig)
+ }
+ }
+
+ // Paths
+ Card(modifier = Modifier.fillMaxWidth()) {
+ Column(modifier = Modifier.padding(12.dp)) {
+ Text("Plugin Paths", style = MaterialTheme.typography.labelLarge)
+ Spacer(Modifier.height(4.dp))
+ for ((label, path) in pluginPaths) {
+ Text(
+ text = "$label: $path",
+ style = MaterialTheme.typography.bodySmall.copy(
+ fontFamily = FontFamily.Monospace,
+ fontSize = 10.sp
+ )
+ )
+ }
+ }
+ }
+
+ // Plugin selectors by slot
+ Text("Select Plugins", style = MaterialTheme.typography.titleMedium)
+ for (slot in PluginConfig.SLOTS) {
+ val slotEntries = classified[slot].orEmpty()
+ if (slotEntries.isEmpty()) continue
+
+ val currentName = when (slot) {
+ "cpu" -> pluginConfig.cpu
+ "ram" -> pluginConfig.ram
+ "gpu" -> pluginConfig.gpu
+ "kbd" -> pluginConfig.kbd
+ "snd" -> pluginConfig.snd
+ "bios" -> pluginConfig.bios
+ "disk" -> pluginConfig.disk ?: "none"
+ else -> "?"
+ }
+
+ SlotSelector(
+ slot = slot,
+ current = currentName,
+ entries = slotEntries,
+ onSelect = { name -> onSelectPlugin(slot, name) }
+ )
+ }
+
+ // Other (unclassified) plugins
+ val other = classified["other"].orEmpty()
+ if (other.isNotEmpty()) {
+ Text("Other Plugins", style = MaterialTheme.typography.titleSmall)
+ for (entry in other) {
+ Text(
+ text = " ${entry.name} (${entry.source})",
+ style = MaterialTheme.typography.bodySmall.copy(
+ fontFamily = FontFamily.Monospace,
+ fontSize = 10.sp
+ )
+ )
+ }
+ }
+
+ // Refresh button
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ horizontalArrangement = Arrangement.SpaceEvenly
+ ) {
+ OutlinedButton(onClick = onRefresh) {
+ Text("Rescan Files")
+ }
+ FilledTonalButton(onClick = onDismiss) {
+ Text("Done")
+ }
+ }
+
+ Spacer(Modifier.height(24.dp))
+ }
+ }
+}
+
+@Composable
+private fun PluginConfigSummary(config: PluginConfig) {
+ Column(verticalArrangement = Arrangement.spacedBy(2.dp)) {
+ Text(
+ text = "CPU: ${config.cpu}",
+ style = TextStyle(fontFamily = FontFamily.Monospace, fontSize = 11.sp)
+ )
+ Text(
+ text = "RAM: ${config.ram}",
+ style = TextStyle(fontFamily = FontFamily.Monospace, fontSize = 11.sp)
+ )
+ Text(
+ text = "GPU: ${config.gpu}",
+ style = TextStyle(fontFamily = FontFamily.Monospace, fontSize = 11.sp)
+ )
+ Text(
+ text = "KBD: ${config.kbd}",
+ style = TextStyle(fontFamily = FontFamily.Monospace, fontSize = 11.sp)
+ )
+ Text(
+ text = "SND: ${config.snd}",
+ style = TextStyle(fontFamily = FontFamily.Monospace, fontSize = 11.sp)
+ )
+ Text(
+ text = "BIOS: ${config.bios}",
+ style = TextStyle(fontFamily = FontFamily.Monospace, fontSize = 11.sp)
+ )
+ Text(
+ text = "DISK: ${config.disk ?: "(none)"}",
+ style = TextStyle(fontFamily = FontFamily.Monospace, fontSize = 11.sp)
+ )
+ }
+}
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+private fun SlotSelector(
+ slot: String,
+ current: String,
+ entries: List,
+ onSelect: (String) -> Unit
+) {
+ var expanded by remember { mutableStateOf(false) }
+
+ Column(modifier = Modifier.fillMaxWidth()) {
+ Text(
+ text = slot.uppercase(),
+ style = MaterialTheme.typography.labelLarge,
+ fontWeight = FontWeight.SemiBold
+ )
+ Spacer(Modifier.height(4.dp))
+ ExposedDropdownMenuBox(
+ expanded = expanded,
+ onExpandedChange = { expanded = !expanded }
+ ) {
+ OutlinedTextField(
+ value = current,
+ onValueChange = {},
+ readOnly = true,
+ trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded) },
+ modifier = Modifier
+ .menuAnchor()
+ .fillMaxWidth(),
+ textStyle = MaterialTheme.typography.bodySmall.copy(
+ fontFamily = FontFamily.Monospace,
+ fontSize = 12.sp
+ )
+ )
+ ExposedDropdownMenu(
+ expanded = expanded,
+ onDismissRequest = { expanded = false }
+ ) {
+ if (slot == "disk") {
+ DropdownMenuItem(
+ text = { Text("(none)") },
+ onClick = {
+ onSelect("none")
+ expanded = false
+ }
+ )
+ }
+ for (entry in entries) {
+ val label = "${entry.name} [${entry.source}]"
+ DropdownMenuItem(
+ text = { Text(label, fontSize = 12.sp) },
+ onClick = {
+ onSelect(entry.name)
+ expanded = false
+ }
+ )
+ }
+ }
+ }
+ Spacer(Modifier.height(8.dp))
+ }
+}
diff --git a/android/app/src/main/java/com/cbe/android/ui/PostPanel.kt b/android/app/src/main/java/com/cbe/android/ui/PostPanel.kt
new file mode 100644
index 0000000..a9a656c
--- /dev/null
+++ b/android/app/src/main/java/com/cbe/android/ui/PostPanel.kt
@@ -0,0 +1,99 @@
+package com.cbe.android.ui
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.*
+import androidx.compose.foundation.shape.CircleShape
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Surface
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.text.font.FontFamily
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import com.cbe.android.ui.theme.*
+
+data class LedSpec(val name: String, val bit: Int, val color: Color)
+
+val LEDS = listOf(
+ LedSpec("PWR", 0, LedGreen),
+ LedSpec("CPU", 1, LedRed),
+ LedSpec("MEM", 2, LedAmber),
+ LedSpec("VID", 3, LedBlue),
+ LedSpec("KBD", 4, LedGreen),
+ LedSpec("SND", 5, LedAmber),
+ LedSpec("DSK", 6, LedBlue),
+ LedSpec("CLK", 7, LedRed)
+)
+
+@Composable
+fun PostPanel(
+ postCode: Int,
+ ledStatus: Int,
+ modifier: Modifier = Modifier
+) {
+ Surface(
+ modifier = modifier
+ .fillMaxWidth()
+ .padding(horizontal = 8.dp),
+ shape = RoundedCornerShape(12.dp),
+ color = MaterialTheme.colorScheme.surfaceVariant,
+ tonalElevation = 2.dp
+ ) {
+ Column(
+ modifier = Modifier.padding(12.dp),
+ verticalArrangement = Arrangement.spacedBy(8.dp)
+ ) {
+ // 7-segment display
+ Box(
+ modifier = Modifier
+ .fillMaxWidth()
+ .background(SegBackground, RoundedCornerShape(8.dp))
+ .padding(horizontal = 16.dp, vertical = 12.dp),
+ contentAlignment = Alignment.Center
+ ) {
+ Text(
+ text = String.format("%02X", postCode and 0xFF),
+ style = MaterialTheme.typography.displayLarge.copy(
+ fontFamily = FontFamily.Monospace,
+ fontSize = 36.sp,
+ color = SegActive
+ )
+ )
+ }
+
+ // LED row
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ horizontalArrangement = Arrangement.SpaceEvenly
+ ) {
+ for (led in LEDS) {
+ val isOn = (ledStatus shr led.bit) and 1 == 1
+ Column(
+ horizontalAlignment = Alignment.CenterHorizontally
+ ) {
+ Box(
+ modifier = Modifier
+ .size(10.dp)
+ .clip(CircleShape)
+ .background(if (isOn) led.color else LedOff)
+ )
+ Spacer(Modifier.height(2.dp))
+ Text(
+ text = led.name,
+ style = MaterialTheme.typography.labelSmall.copy(
+ fontSize = 8.sp,
+ color = if (isOn) MaterialTheme.colorScheme.onSurface
+ else MaterialTheme.colorScheme.onSurfaceVariant
+ )
+ )
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/android/app/src/main/java/com/cbe/android/ui/ScreenPanel.kt b/android/app/src/main/java/com/cbe/android/ui/ScreenPanel.kt
new file mode 100644
index 0000000..d042fbb
--- /dev/null
+++ b/android/app/src/main/java/com/cbe/android/ui/ScreenPanel.kt
@@ -0,0 +1,72 @@
+package com.cbe.android.ui
+
+import androidx.compose.foundation.Canvas
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.*
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.foundation.text.BasicText
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Surface
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.font.FontFamily
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import com.cbe.android.ui.theme.ScreenBezel
+import com.cbe.android.ui.theme.ScreenBg
+import com.cbe.android.ui.theme.ScreenText
+
+@Composable
+fun ScreenPanel(
+ text: String,
+ modifier: Modifier = Modifier
+) {
+ Surface(
+ modifier = modifier
+ .fillMaxWidth()
+ .padding(horizontal = 8.dp),
+ shape = RoundedCornerShape(12.dp),
+ color = ScreenBezel,
+ tonalElevation = 4.dp
+ ) {
+ Box(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(12.dp)
+ .clip(RoundedCornerShape(4.dp))
+ .background(ScreenBg)
+ ) {
+ BasicText(
+ text = text,
+ style = TextStyle(
+ fontFamily = FontFamily.Monospace,
+ fontSize = 9.sp,
+ color = ScreenText,
+ lineHeight = 11.sp
+ ),
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(8.dp)
+ )
+
+ Canvas(modifier = Modifier.matchParentSize()) {
+ val scanAlpha = 0.04f
+ var y = 0f
+ val step = 5f
+ while (y < size.height) {
+ drawLine(
+ color = Color.Black.copy(alpha = scanAlpha),
+ start = Offset(0f, y),
+ end = Offset(size.width, y),
+ strokeWidth = 1f
+ )
+ y += step
+ }
+ }
+ }
+ }
+}
diff --git a/android/app/src/main/java/com/cbe/android/ui/theme/Color.kt b/android/app/src/main/java/com/cbe/android/ui/theme/Color.kt
new file mode 100644
index 0000000..0013d37
--- /dev/null
+++ b/android/app/src/main/java/com/cbe/android/ui/theme/Color.kt
@@ -0,0 +1,41 @@
+package com.cbe.android.ui.theme
+
+import androidx.compose.ui.graphics.Color
+
+// Dark theme - retro terminal vibe with amber/teal accents
+val DarkBackground = Color(0xFF0D1117)
+val DarkSurface = Color(0xFF161B22)
+val DarkSurfaceVariant = Color(0xFF21262D)
+val DarkOutline = Color(0xFF30363D)
+
+val Amber = Color(0xFFFFB000)
+val AmberDim = Color(0xFFB8860B)
+val AmberGlow = Color(0xFFFFD700)
+val Teal = Color(0xFF00BFA5)
+val TealDim = Color(0xFF00897B)
+val Green = Color(0xFF00FF41)
+val Red = Color(0xFFFF3333)
+val Blue = Color(0xFF58A6FF)
+val White = Color(0xFFE6EDF3)
+val WhiteDim = Color(0xFF8B949E)
+
+// LED colors
+val LedRed = Color(0xFFFF3333)
+val LedGreen = Color(0xFF00FF41)
+val LedAmber = Color(0xFFFFB000)
+val LedBlue = Color(0xFF58A6FF)
+val LedOff = Color(0xFF1A1A2E)
+
+// 7-segment display colors
+val SegBackground = Color(0xFF0A0A0F)
+val SegActive = Color(0xFFFF3333)
+val SegInactive = Color(0xFF331111)
+
+// GPU screen colors
+val ScreenBg = Color(0xFF000000)
+val ScreenText = Color(0xFFC0C0C0)
+val ScreenScanline = Color(0x08000000)
+val ScreenBezel = Color(0xFF2A2A2A)
+
+// Material3 light (used only for system chrome)
+val LightBackground = Color(0xFFF6F8FA)
diff --git a/android/app/src/main/java/com/cbe/android/ui/theme/Theme.kt b/android/app/src/main/java/com/cbe/android/ui/theme/Theme.kt
new file mode 100644
index 0000000..bb4cf82
--- /dev/null
+++ b/android/app/src/main/java/com/cbe/android/ui/theme/Theme.kt
@@ -0,0 +1,52 @@
+package com.cbe.android.ui.theme
+
+import android.app.Activity
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.darkColorScheme
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.SideEffect
+import androidx.compose.ui.graphics.toArgb
+import androidx.compose.ui.platform.LocalView
+import androidx.core.view.WindowCompat
+
+private val DarkColorScheme = darkColorScheme(
+ primary = Amber,
+ onPrimary = DarkBackground,
+ primaryContainer = AmberDim,
+ secondary = Teal,
+ onSecondary = DarkBackground,
+ secondaryContainer = TealDim,
+ tertiary = Green,
+ background = DarkBackground,
+ onBackground = White,
+ surface = DarkSurface,
+ onSurface = White,
+ surfaceVariant = DarkSurfaceVariant,
+ onSurfaceVariant = WhiteDim,
+ outline = DarkOutline,
+ error = Red,
+ onError = DarkBackground
+)
+
+@Composable
+fun CbeTheme(content: @Composable () -> Unit) {
+ val colorScheme = DarkColorScheme
+ val view = LocalView.current
+ if (!view.isInEditMode) {
+ SideEffect {
+ val window = (view.context as Activity).window
+ window.statusBarColor = DarkBackground.toArgb()
+ window.navigationBarColor = DarkBackground.toArgb()
+ WindowCompat.getInsetsController(window, view).apply {
+ isAppearanceLightStatusBars = false
+ isAppearanceLightNavigationBars = false
+ }
+ }
+ }
+
+ MaterialTheme(
+ colorScheme = colorScheme,
+ typography = Typography,
+ content = content
+ )
+}
diff --git a/android/app/src/main/java/com/cbe/android/ui/theme/Type.kt b/android/app/src/main/java/com/cbe/android/ui/theme/Type.kt
new file mode 100644
index 0000000..50ac586
--- /dev/null
+++ b/android/app/src/main/java/com/cbe/android/ui/theme/Type.kt
@@ -0,0 +1,79 @@
+package com.cbe.android.ui.theme
+
+import androidx.compose.material3.Typography
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.font.Font
+import androidx.compose.ui.text.font.FontFamily
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.unit.sp
+
+val MonospaceFamily = FontFamily.Monospace
+
+val Typography = Typography(
+ displayLarge = TextStyle(
+ fontFamily = MonospaceFamily,
+ fontWeight = FontWeight.Bold,
+ fontSize = 32.sp,
+ lineHeight = 40.sp
+ ),
+ headlineLarge = TextStyle(
+ fontFamily = MonospaceFamily,
+ fontWeight = FontWeight.Bold,
+ fontSize = 24.sp,
+ lineHeight = 32.sp
+ ),
+ headlineMedium = TextStyle(
+ fontFamily = MonospaceFamily,
+ fontWeight = FontWeight.SemiBold,
+ fontSize = 20.sp,
+ lineHeight = 28.sp
+ ),
+ titleLarge = TextStyle(
+ fontFamily = FontFamily.SansSerif,
+ fontWeight = FontWeight.SemiBold,
+ fontSize = 18.sp,
+ lineHeight = 24.sp
+ ),
+ titleMedium = TextStyle(
+ fontFamily = FontFamily.SansSerif,
+ fontWeight = FontWeight.Medium,
+ fontSize = 14.sp,
+ lineHeight = 20.sp
+ ),
+ bodyLarge = TextStyle(
+ fontFamily = FontFamily.SansSerif,
+ fontWeight = FontWeight.Normal,
+ fontSize = 16.sp,
+ lineHeight = 24.sp
+ ),
+ bodyMedium = TextStyle(
+ fontFamily = FontFamily.SansSerif,
+ fontWeight = FontWeight.Normal,
+ fontSize = 14.sp,
+ lineHeight = 20.sp
+ ),
+ bodySmall = TextStyle(
+ fontFamily = MonospaceFamily,
+ fontWeight = FontWeight.Normal,
+ fontSize = 12.sp,
+ lineHeight = 16.sp
+ ),
+ labelLarge = TextStyle(
+ fontFamily = MonospaceFamily,
+ fontWeight = FontWeight.Medium,
+ fontSize = 14.sp,
+ lineHeight = 20.sp
+ ),
+ labelMedium = TextStyle(
+ fontFamily = MonospaceFamily,
+ fontWeight = FontWeight.Medium,
+ fontSize = 12.sp,
+ lineHeight = 16.sp
+ ),
+ labelSmall = TextStyle(
+ fontFamily = MonospaceFamily,
+ fontWeight = FontWeight.Medium,
+ fontSize = 10.sp,
+ lineHeight = 14.sp
+ )
+)
diff --git a/android/app/src/main/res/drawable/ic_launcher_foreground.xml b/android/app/src/main/res/drawable/ic_launcher_foreground.xml
new file mode 100644
index 0000000..d16a3eb
--- /dev/null
+++ b/android/app/src/main/res/drawable/ic_launcher_foreground.xml
@@ -0,0 +1,54 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
new file mode 100644
index 0000000..25bb108
--- /dev/null
+++ b/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
new file mode 100644
index 0000000..25bb108
--- /dev/null
+++ b/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/android/app/src/main/res/values/ic_launcher_background.xml b/android/app/src/main/res/values/ic_launcher_background.xml
new file mode 100644
index 0000000..cc09442
--- /dev/null
+++ b/android/app/src/main/res/values/ic_launcher_background.xml
@@ -0,0 +1,4 @@
+
+
+ #0D1117
+
diff --git a/android/app/src/main/res/values/styles.xml b/android/app/src/main/res/values/styles.xml
new file mode 100644
index 0000000..a1055d7
--- /dev/null
+++ b/android/app/src/main/res/values/styles.xml
@@ -0,0 +1,4 @@
+
+
+
+
diff --git a/android/build.gradle.kts b/android/build.gradle.kts
new file mode 100644
index 0000000..119965d
--- /dev/null
+++ b/android/build.gradle.kts
@@ -0,0 +1,5 @@
+plugins {
+ alias(libs.plugins.android.application) apply false
+ alias(libs.plugins.android.library) apply false
+ alias(libs.plugins.kotlin.android) apply false
+}
diff --git a/android/cbe-core/build.gradle.kts b/android/cbe-core/build.gradle.kts
new file mode 100644
index 0000000..f0ace14
--- /dev/null
+++ b/android/cbe-core/build.gradle.kts
@@ -0,0 +1,26 @@
+plugins {
+ alias(libs.plugins.android.library)
+ alias(libs.plugins.kotlin.android)
+}
+
+android {
+ namespace = "com.cbe"
+ compileSdk = 35
+
+ defaultConfig {
+ minSdk = 26
+ }
+
+ compileOptions {
+ sourceCompatibility = JavaVersion.VERSION_17
+ targetCompatibility = JavaVersion.VERSION_17
+ }
+
+ kotlinOptions {
+ jvmTarget = "17"
+ }
+}
+
+dependencies {
+ implementation(libs.androidx.core.ktx)
+}
diff --git a/android/cbe-core/src/main/java/com/cbe/core/Bus.java b/android/cbe-core/src/main/java/com/cbe/core/Bus.java
new file mode 100644
index 0000000..e946b53
--- /dev/null
+++ b/android/cbe-core/src/main/java/com/cbe/core/Bus.java
@@ -0,0 +1,13 @@
+package com.cbe.core;
+
+public interface Bus {
+ byte read(int address);
+ void write(int address, byte data);
+ int readWord(int address);
+ void writeWord(int address, int value);
+ long clock();
+ long tick();
+ void attach(String deviceName, int baseAddress, int size);
+ ModuleInstance getDevice(String name);
+ void reset();
+}
diff --git a/android/cbe-core/src/main/java/com/cbe/core/CbePluginConstants.java b/android/cbe-core/src/main/java/com/cbe/core/CbePluginConstants.java
new file mode 100644
index 0000000..4dc33e6
--- /dev/null
+++ b/android/cbe-core/src/main/java/com/cbe/core/CbePluginConstants.java
@@ -0,0 +1,25 @@
+package com.cbe.core;
+
+public final class CbePluginConstants {
+ public static final String MAGIC = "CBE_PLUG";
+ public static final int HEADER_SIZE = 62;
+
+ public static final int OFF_MAGIC = 0;
+ public static final int OFF_VERSION = 8;
+ public static final int OFF_HEADER_SIZE = 12;
+ public static final int OFF_MODULE_TYPE = 16;
+ public static final int OFF_COMPILE_MODE = 17;
+ public static final int OFF_METADATA_OFF = 18;
+ public static final int OFF_METADATA_LEN = 22;
+ public static final int OFF_OPCODE_OFF = 26;
+ public static final int OFF_OPCODE_LEN = 30;
+ public static final int OFF_MICROCODE_OFF = 34;
+ public static final int OFF_MICROCODE_LEN = 38;
+ public static final int OFF_HANDLER_OFF = 42;
+ public static final int OFF_HANDLER_LEN = 46;
+ public static final int OFF_DATA_OFF = 50;
+ public static final int OFF_DATA_LEN = 54;
+ public static final int OFF_CHECKSUM = 58;
+
+ private CbePluginConstants() {}
+}
diff --git a/android/cbe-core/src/main/java/com/cbe/core/CompileMode.java b/android/cbe-core/src/main/java/com/cbe/core/CompileMode.java
new file mode 100644
index 0000000..0ff4b34
--- /dev/null
+++ b/android/cbe-core/src/main/java/com/cbe/core/CompileMode.java
@@ -0,0 +1,24 @@
+package com.cbe.core;
+
+public enum CompileMode {
+ FULL((byte) 0),
+ HYBRID((byte) 1),
+ PACK_ONLY((byte) 2);
+
+ private final byte id;
+
+ CompileMode(byte id) {
+ this.id = id;
+ }
+
+ public byte getId() {
+ return id;
+ }
+
+ public static CompileMode fromId(byte id) {
+ for (CompileMode m : values()) {
+ if (m.id == id) return m;
+ }
+ throw new IllegalArgumentException("Unknown CompileMode id: " + id);
+ }
+}
diff --git a/android/cbe-core/src/main/java/com/cbe/core/Instruction.java b/android/cbe-core/src/main/java/com/cbe/core/Instruction.java
new file mode 100644
index 0000000..d69881f
--- /dev/null
+++ b/android/cbe-core/src/main/java/com/cbe/core/Instruction.java
@@ -0,0 +1,104 @@
+package com.cbe.core;
+
+import java.util.Collections;
+import java.util.List;
+
+public class Instruction {
+ private final int opcode;
+ private final String mnemonic;
+ private final List args;
+ private final int cycles;
+ private final List semantics;
+
+ public Instruction(int opcode, String mnemonic, List args, int cycles, List semantics) {
+ this.opcode = opcode;
+ this.mnemonic = mnemonic;
+ this.args = args != null ? Collections.unmodifiableList(args) : Collections.emptyList();
+ this.cycles = cycles;
+ this.semantics = semantics != null ? Collections.unmodifiableList(semantics) : Collections.emptyList();
+ }
+
+ public int getOpcode() {
+ return opcode;
+ }
+
+ public String getMnemonic() {
+ return mnemonic;
+ }
+
+ public List getArgs() {
+ return args;
+ }
+
+ public int getCycles() {
+ return cycles;
+ }
+
+ public List getSemantics() {
+ return semantics;
+ }
+
+ public static class Arg {
+ private final String name;
+ private final String type;
+
+ public Arg(String name, String type) {
+ this.name = name;
+ this.type = type;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public String getType() {
+ return type;
+ }
+ }
+
+ public static class SemanticOp {
+ private final String operation;
+ private final String from;
+ private final String to;
+ private final String value;
+ private final String condition;
+ private final String result;
+
+ public SemanticOp(String operation, String from, String to, String value, String condition) {
+ this(operation, from, to, value, condition, null);
+ }
+
+ public SemanticOp(String operation, String from, String to, String value, String condition, String result) {
+ this.operation = operation;
+ this.from = from;
+ this.to = to;
+ this.value = value;
+ this.condition = condition;
+ this.result = result;
+ }
+
+ public String getOperation() {
+ return operation;
+ }
+
+ public String getFrom() {
+ return from;
+ }
+
+ public String getTo() {
+ return to;
+ }
+
+ public String getValue() {
+ return value;
+ }
+
+ public String getCondition() {
+ return condition;
+ }
+
+ public String getResult() {
+ return result;
+ }
+ }
+}
diff --git a/android/cbe-core/src/main/java/com/cbe/core/InstructionExecutor.java b/android/cbe-core/src/main/java/com/cbe/core/InstructionExecutor.java
new file mode 100644
index 0000000..b8b0d6b
--- /dev/null
+++ b/android/cbe-core/src/main/java/com/cbe/core/InstructionExecutor.java
@@ -0,0 +1,386 @@
+package com.cbe.core;
+
+import java.util.Map;
+
+public class InstructionExecutor {
+
+ private static final int MASK_8BIT = 0xFF;
+
+ // Pre-computed operation IDs for faster dispatch
+ private static final int OP_NOP = 0;
+ private static final int OP_COPY = 1;
+ private static final int OP_LOAD_IMM = 2;
+ private static final int OP_ADD = 3;
+ private static final int OP_SUB = 4;
+ private static final int OP_CMP = 5;
+ private static final int OP_STORE = 6;
+ private static final int OP_LOAD = 7;
+ private static final int OP_JMP = 8;
+ private static final int OP_JCC = 9;
+ private static final int OP_JMP_IMM = 10;
+ private static final int OP_JCC_IMM = 11;
+ private static final int OP_CALL = 12;
+ private static final int OP_RET = 13;
+ private static final int OP_PUSH = 14;
+ private static final int OP_POP = 15;
+ private static final int OP_INC = 16;
+
+ private static final java.util.HashMap OP_IDS = new java.util.HashMap();
+ static {
+ OP_IDS.put("nop", OP_NOP);
+ OP_IDS.put("copy", OP_COPY);
+ OP_IDS.put("load_imm", OP_LOAD_IMM);
+ OP_IDS.put("add", OP_ADD);
+ OP_IDS.put("sub", OP_SUB);
+ OP_IDS.put("cmp", OP_CMP);
+ OP_IDS.put("store", OP_STORE);
+ OP_IDS.put("load", OP_LOAD);
+ OP_IDS.put("jmp", OP_JMP);
+ OP_IDS.put("jcc", OP_JCC);
+ OP_IDS.put("jmp_imm", OP_JMP_IMM);
+ OP_IDS.put("jcc_imm", OP_JCC_IMM);
+ OP_IDS.put("call", OP_CALL);
+ OP_IDS.put("ret", OP_RET);
+ OP_IDS.put("push", OP_PUSH);
+ OP_IDS.put("pop", OP_POP);
+ OP_IDS.put("inc", OP_INC);
+ }
+
+ // Cached spec type: 0=reg, 1=mem[const], 2=arg.X, 3=imm.X, 4=mem[arg.X], 5=$next
+ private static int classifySpec(String spec) {
+ if (spec == null || spec.isEmpty()) return 0;
+ char c = spec.charAt(0);
+ if (c == 'a' && spec.startsWith("arg.")) return 2;
+ if (c == 'i' && spec.startsWith("imm.")) return 3;
+ if (c == 'm' && spec.startsWith("mem[")) {
+ if (spec.endsWith("]")) {
+ if (spec.length() > 5 && spec.charAt(4) == 'a' && spec.startsWith("mem[arg.")) return 4;
+ // Check if it's $next
+ if (spec.equals("mem[$next]")) return 5;
+ return 1; // mem[const]
+ }
+ }
+ if (c == '$' && spec.equals("$next")) return 5;
+ return 0; // reg or numeric literal
+ }
+
+ // Pre-computed spec info for quick operand resolution
+ private static class SpecInfo {
+ final int type; // 0=reg, 1=mem[const], 2=arg.X, 3=imm.X, 4=mem[arg.X], 5=$next
+ final String value; // register name, argument name, or numeric string
+
+ SpecInfo(String spec) {
+ this.type = classifySpec(spec);
+ switch (type) {
+ case 1: this.value = spec.substring(4, spec.length() - 1); break;
+ case 2: this.value = spec.substring(4); break;
+ case 3: this.value = spec.substring(4); break;
+ case 4: this.value = spec.substring(8, spec.length() - 1); break;
+ default: this.value = spec; break;
+ }
+ }
+ }
+
+ private final Map instructionMap;
+
+ // Per-instruction cached spec info
+ private final java.util.HashMap specCache;
+
+ private static class CachedSpecs {
+ final int[] opIds;
+ final SpecInfo[] fromSpecs;
+ final SpecInfo[] toSpecs;
+ final SpecInfo[] resultSpecs;
+ final String[] valueSpecs;
+ final String[] conditions;
+
+ CachedSpecs(Instruction inst) {
+ java.util.List ops = inst.getSemantics();
+ int n = ops.size();
+ opIds = new int[n];
+ fromSpecs = new SpecInfo[n];
+ toSpecs = new SpecInfo[n];
+ resultSpecs = new SpecInfo[n];
+ valueSpecs = new String[n];
+ conditions = new String[n];
+ for (int i = 0; i < n; i++) {
+ Instruction.SemanticOp op = ops.get(i);
+ Integer id = OP_IDS.get(op.getOperation());
+ opIds[i] = id != null ? id : -1;
+ fromSpecs[i] = new SpecInfo(op.getFrom());
+ toSpecs[i] = new SpecInfo(op.getTo());
+ resultSpecs[i] = new SpecInfo(op.getResult());
+ valueSpecs[i] = op.getValue();
+ conditions[i] = op.getCondition();
+ }
+ }
+ }
+
+ public InstructionExecutor(Map instructionMap) {
+ this.instructionMap = instructionMap;
+ this.specCache = new java.util.HashMap();
+ }
+
+ public OpcodeResult execute(int opcode, Registers regs, Bus bus, byte[] memory) {
+ Instruction inst = instructionMap.get(opcode);
+ if (inst == null) {
+ throw new IllegalArgumentException("Unknown opcode: 0x" + Integer.toHexString(opcode & 0xFF));
+ }
+
+ // Always 8-bit for TinyCPU
+ int mask = MASK_8BIT;
+
+ CachedSpecs cached = specCache.get(inst);
+ if (cached == null) {
+ cached = new CachedSpecs(inst);
+ specCache.put(inst, cached);
+ }
+
+ for (int i = 0; i < cached.opIds.length; i++) {
+ executeCached(cached, i, regs, bus, memory, mask);
+ }
+
+ return OpcodeResult.ok(inst.getCycles());
+ }
+
+ private void executeCached(CachedSpecs cached, int idx, Registers regs, Bus bus, byte[] memory, int mask) {
+ switch (cached.opIds[idx]) {
+ case OP_NOP:
+ break;
+ case OP_COPY: {
+ int srcVal = resolveValueCached(cached.fromSpecs[idx], regs, memory);
+ writeCached(cached.toSpecs[idx], srcVal & mask, regs, memory);
+ break;
+ }
+ case OP_LOAD_IMM: {
+ SpecInfo to = cached.toSpecs[idx];
+ if (to.type != 0 || to.value == null) break;
+ int immValue;
+ String valSpec = cached.valueSpecs[idx];
+ if (valSpec != null && valSpec.equals("$next")) {
+ int nextAddr = regs.read("pc");
+ immValue = nextAddr < memory.length ? (memory[nextAddr] & 0xFF) : 0;
+ regs.write("pc", nextAddr + 1);
+ } else if (valSpec != null) {
+ immValue = Integer.parseInt(valSpec.trim());
+ } else {
+ immValue = 0;
+ }
+ regs.write(to.value, immValue & mask);
+ break;
+ }
+ case OP_ADD: {
+ int srcVal = resolveValueCached(cached.fromSpecs[idx], regs, memory);
+ int dstVal = resolveValueCached(cached.toSpecs[idx], regs, memory);
+ int result = srcVal + dstVal;
+ int masked = result & mask;
+ String dest = cached.resultSpecs[idx].value != null ? cached.resultSpecs[idx].value : cached.toSpecs[idx].value;
+ if (dest != null) regs.write(dest, masked);
+ regs.write("carry", (result != masked) ? 1 : 0);
+ regs.write("zero", (masked == 0) ? 1 : 0);
+ break;
+ }
+ case OP_SUB: {
+ int a = resolveValueCached(cached.fromSpecs[idx], regs, memory);
+ int b = resolveValueCached(cached.toSpecs[idx], regs, memory);
+ int result = a - b;
+ int masked = result & mask;
+ String dest = cached.resultSpecs[idx].value != null ? cached.resultSpecs[idx].value : cached.toSpecs[idx].value;
+ if (dest != null) regs.write(dest, masked);
+ regs.write("carry", (result < 0) ? 1 : 0);
+ regs.write("zero", (masked == 0) ? 1 : 0);
+ break;
+ }
+ case OP_CMP: {
+ int a = resolveValueCached(cached.fromSpecs[idx], regs, memory);
+ int b = resolveValueCached(cached.toSpecs[idx], regs, memory);
+ int result = a - b;
+ regs.write("zero", ((result & mask) == 0) ? 1 : 0);
+ regs.write("carry", (result < 0) ? 1 : 0);
+ break;
+ }
+ case OP_STORE: {
+ int val = resolveValueCached(cached.fromSpecs[idx], regs, memory);
+ int addr = resolveAddressCached(cached.toSpecs[idx], regs, memory);
+ if (addr >= 0 && addr < memory.length) {
+ memory[addr] = (byte) (val & 0xFF);
+ }
+ break;
+ }
+ case OP_LOAD: {
+ int addr = resolveAddressCached(cached.fromSpecs[idx], regs, memory);
+ int val = (addr >= 0 && addr < memory.length) ? (memory[addr] & 0xFF) : 0;
+ writeCached(cached.toSpecs[idx], val, regs, memory);
+ break;
+ }
+ case OP_JMP: {
+ int addr = resolveValueCached(cached.toSpecs[idx], regs, memory);
+ regs.write("pc", addr & mask);
+ break;
+ }
+ case OP_JCC: {
+ String cond = cached.conditions[idx];
+ if (cond != null) {
+ int comma = cond.indexOf(',');
+ if (comma > 0) {
+ String flagName = cond.substring(0, comma).trim();
+ int expected = Integer.parseInt(cond.substring(comma + 1).trim());
+ if (regs.read(flagName) == expected) {
+ int addr = resolveValueCached(cached.toSpecs[idx], regs, memory);
+ regs.write("pc", addr & mask);
+ }
+ }
+ }
+ break;
+ }
+ case OP_JMP_IMM: {
+ int nextAddr = regs.read("pc");
+ if (nextAddr < memory.length) {
+ regs.write("pc", memory[nextAddr] & 0xFF);
+ }
+ break;
+ }
+ case OP_JCC_IMM: {
+ String cond = cached.conditions[idx];
+ int nextAddr = regs.read("pc");
+ if (cond != null && nextAddr < memory.length) {
+ int comma = cond.indexOf(',');
+ if (comma > 0) {
+ String flagName = cond.substring(0, comma).trim();
+ int expected = Integer.parseInt(cond.substring(comma + 1).trim());
+ int immTarget = memory[nextAddr] & 0xFF;
+ if (regs.read(flagName) == expected) {
+ regs.write("pc", immTarget);
+ } else {
+ regs.write("pc", nextAddr + 1);
+ }
+ }
+ }
+ break;
+ }
+ case OP_CALL: {
+ int retAddr = regs.read("pc");
+ int sp = regs.read("sp");
+ if (sp > 0 && sp < memory.length) {
+ memory[sp - 1] = (byte) (retAddr & 0xFF);
+ regs.write("sp", sp - 1);
+ regs.write("pc", resolveValueCached(cached.toSpecs[idx], regs, memory) & mask);
+ }
+ break;
+ }
+ case OP_RET: {
+ int sp = regs.read("sp");
+ if (sp >= 0 && sp < memory.length) {
+ regs.write("sp", sp + 1);
+ regs.write("pc", memory[sp] & 0xFF);
+ }
+ break;
+ }
+ case OP_PUSH: {
+ int sp = regs.read("sp");
+ int val = resolveValueCached(cached.fromSpecs[idx], regs, memory);
+ if (sp > 0 && sp < memory.length) {
+ memory[sp - 1] = (byte) (val & 0xFF);
+ regs.write("sp", sp - 1);
+ }
+ break;
+ }
+ case OP_POP: {
+ int sp = regs.read("sp");
+ if (sp >= 0 && sp < memory.length) {
+ int val = memory[sp] & 0xFF;
+ regs.write("sp", sp + 1);
+ writeCached(cached.toSpecs[idx], val, regs, memory);
+ }
+ break;
+ }
+ case OP_INC: {
+ int val = resolveValueCached(cached.fromSpecs[idx], regs, memory);
+ writeCached(cached.toSpecs[idx], (val + 1) & mask, regs, memory);
+ break;
+ }
+ default:
+ throw new IllegalArgumentException("Unknown semantic operation: " + cached.opIds[idx]);
+ }
+ }
+
+ private static int resolveAddressCached(SpecInfo spec, Registers regs, byte[] memory) {
+ switch (spec.type) {
+ case 5: { // $next
+ int pc = regs.read("pc");
+ if (pc < memory.length) {
+ regs.write("pc", pc + 1);
+ return memory[pc] & 0xFF;
+ }
+ return 0;
+ }
+ case 1: // mem[const]
+ return Integer.parseInt(spec.value);
+ case 2: // arg.X
+ return regs.read(spec.value);
+ case 4: // mem[arg.X]
+ return regs.read(spec.value);
+ case 0: // register value as address
+ return regs.read(spec.value);
+ default:
+ return 0;
+ }
+ }
+
+ private static int resolveValueCached(SpecInfo spec, Registers regs, byte[] memory) {
+ switch (spec.type) {
+ case 0: { // register or numeric literal
+ String v = spec.value;
+ if (v == null || v.isEmpty()) return 0;
+ char c = v.charAt(0);
+ if (c >= '0' && c <= '9') {
+ try { return Integer.parseInt(v); } catch (NumberFormatException e) { return regs.read(v); }
+ }
+ if (c == '-') {
+ try { return Integer.parseInt(v); } catch (NumberFormatException e) { return regs.read(v); }
+ }
+ return regs.read(v);
+ }
+ case 1: // mem[const]
+ return memory[Integer.parseInt(spec.value)] & 0xFF;
+ case 2: // arg.X
+ return regs.read(spec.value);
+ case 3: // imm.X
+ return Integer.parseInt(spec.value);
+ case 4: { // mem[arg.X]
+ int addr = regs.read(spec.value);
+ return addr < memory.length ? (memory[addr] & 0xFF) : 0;
+ }
+ case 5: { // $next
+ int pc = regs.read("pc");
+ if (pc < memory.length) {
+ regs.write("pc", pc + 1);
+ return memory[pc] & 0xFF;
+ }
+ return 0;
+ }
+ default:
+ return 0;
+ }
+ }
+
+ private static void writeCached(SpecInfo spec, int value, Registers regs, byte[] memory) {
+ if (spec == null || spec.value == null) return;
+ switch (spec.type) {
+ case 0:
+ regs.write(spec.value, value);
+ break;
+ case 1: // mem[const]
+ memory[Integer.parseInt(spec.value)] = (byte) value;
+ break;
+ case 2: // arg.X
+ regs.write(spec.value, value);
+ break;
+ case 4: { // mem[arg.X]
+ int addr = regs.read(spec.value);
+ if (addr >= 0 && addr < memory.length) memory[addr] = (byte) value;
+ break;
+ }
+ }
+ }
+}
diff --git a/android/cbe-core/src/main/java/com/cbe/core/ModuleInstance.java b/android/cbe-core/src/main/java/com/cbe/core/ModuleInstance.java
new file mode 100644
index 0000000..31c134e
--- /dev/null
+++ b/android/cbe-core/src/main/java/com/cbe/core/ModuleInstance.java
@@ -0,0 +1,13 @@
+package com.cbe.core;
+
+public interface ModuleInstance {
+ ModuleMetadata getMetadata();
+ String getName();
+ void init(Bus bus);
+ void reset();
+ void destroy();
+ OpcodeResult executeOpcode(int opcode, Registers regs, Bus bus);
+ byte[] readData(int bank, int offset, int size);
+ void writeData(int bank, int offset, byte[] data);
+ byte[] getMicrocode(String command);
+}
diff --git a/android/cbe-core/src/main/java/com/cbe/core/ModuleMetadata.java b/android/cbe-core/src/main/java/com/cbe/core/ModuleMetadata.java
new file mode 100644
index 0000000..696081a
--- /dev/null
+++ b/android/cbe-core/src/main/java/com/cbe/core/ModuleMetadata.java
@@ -0,0 +1,51 @@
+package com.cbe.core;
+
+import java.io.Serializable;
+
+public class ModuleMetadata implements Serializable {
+ private static final long serialVersionUID = 2L;
+
+ private final String name;
+ private final String arch;
+ private final ModuleType type;
+ private final int version;
+ private final float tdp;
+ private final int maxFrequency; // Hz, 0 = unknown
+
+ public ModuleMetadata(String name, String arch, ModuleType type, int version, float tdp) {
+ this(name, arch, type, version, tdp, 0);
+ }
+
+ public ModuleMetadata(String name, String arch, ModuleType type, int version, float tdp, int maxFrequency) {
+ this.name = name;
+ this.arch = arch;
+ this.type = type;
+ this.version = version;
+ this.tdp = tdp;
+ this.maxFrequency = maxFrequency;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public String getArch() {
+ return arch;
+ }
+
+ public ModuleType getType() {
+ return type;
+ }
+
+ public int getVersion() {
+ return version;
+ }
+
+ public float getTdp() {
+ return tdp;
+ }
+
+ public int getMaxFrequency() {
+ return maxFrequency;
+ }
+}
diff --git a/android/cbe-core/src/main/java/com/cbe/core/ModuleType.java b/android/cbe-core/src/main/java/com/cbe/core/ModuleType.java
new file mode 100644
index 0000000..1c62c1a
--- /dev/null
+++ b/android/cbe-core/src/main/java/com/cbe/core/ModuleType.java
@@ -0,0 +1,29 @@
+package com.cbe.core;
+
+public enum ModuleType {
+ CPU((byte) 0),
+ RAM((byte) 1),
+ DISK((byte) 2),
+ GPU((byte) 3),
+ BIOS((byte) 4),
+ KBD((byte) 5),
+ SND((byte) 6),
+ DATA_ONLY((byte) 7);
+
+ private final byte id;
+
+ ModuleType(byte id) {
+ this.id = id;
+ }
+
+ public byte getId() {
+ return id;
+ }
+
+ public static ModuleType fromId(byte id) {
+ for (ModuleType t : values()) {
+ if (t.id == id) return t;
+ }
+ throw new IllegalArgumentException("Unknown ModuleType id: " + id);
+ }
+}
diff --git a/android/cbe-core/src/main/java/com/cbe/core/OpcodeResult.java b/android/cbe-core/src/main/java/com/cbe/core/OpcodeResult.java
new file mode 100644
index 0000000..c5ede16
--- /dev/null
+++ b/android/cbe-core/src/main/java/com/cbe/core/OpcodeResult.java
@@ -0,0 +1,27 @@
+package com.cbe.core;
+
+public class OpcodeResult {
+ private final boolean halt;
+ private final int cyclesConsumed;
+
+ public OpcodeResult(boolean halt, int cyclesConsumed) {
+ this.halt = halt;
+ this.cyclesConsumed = cyclesConsumed;
+ }
+
+ public static OpcodeResult ok(int cycles) {
+ return new OpcodeResult(false, cycles);
+ }
+
+ public static OpcodeResult halt(int cycles) {
+ return new OpcodeResult(true, cycles);
+ }
+
+ public boolean isHalt() {
+ return halt;
+ }
+
+ public int getCyclesConsumed() {
+ return cyclesConsumed;
+ }
+}
diff --git a/android/cbe-core/src/main/java/com/cbe/core/PostCode.java b/android/cbe-core/src/main/java/com/cbe/core/PostCode.java
new file mode 100644
index 0000000..ca9cf96
--- /dev/null
+++ b/android/cbe-core/src/main/java/com/cbe/core/PostCode.java
@@ -0,0 +1,132 @@
+package com.cbe.core;
+
+/**
+ * POST (Power-On Self-Test) codes.
+ * Each code is a single byte that the system reports as it initializes.
+ *
+ * Layout convention:
+ * 0x0_ = power-on phase
+ * 0x1_ = CPU
+ * 0x2_ = memory
+ * 0x3_ = video
+ * 0x4_ = keyboard
+ * 0x5_ = sound
+ * 0x6_ = disk
+ * 0x7_ = BIOS
+ * 0x8_ = OS / boot
+ * 0xA_ = runtime
+ * 0xE_ = warning
+ * 0xF_ = fatal
+ *
+ * The engine writes the current POST code to the special memory address
+ * {@link #POST_CODE_ADDR}. The GUI reads from there for visualization.
+ *
+ * The COMPONENT_OK bitmask lives in the LED register at {@link #LED_STATUS_ADDR}.
+ */
+public enum PostCode {
+ POWER_ON(0x01),
+ CPU_DETECTED(0x10), CPU_OK(0x11), CPU_FAIL(0x12),
+ MEM_DETECTED(0x20), MEM_OK(0x21), MEM_FAIL(0x22),
+ VIDEO_DETECTED(0x30), VIDEO_OK(0x31), VIDEO_FAIL(0x32),
+ KBD_DETECTED(0x40), KBD_OK(0x41), KBD_FAIL(0x42),
+ SND_DETECTED(0x50), SND_OK(0x51), SND_FAIL(0x52),
+ DISK_DETECTED(0x60), DISK_OK(0x61), DISK_FAIL(0x62),
+ BIOS_LOADING(0x70), BIOS_OK(0x71), BIOS_FAIL(0x72),
+ BIOS_CPU(0x73), BIOS_RAM(0x74), BIOS_GPU(0x75),
+ BIOS_KBD(0x76), BIOS_SND(0x77), BIOS_DONE(0x78),
+ BOOT_LOAD(0x80), BOOT_OK(0x81),
+ RUNTIME(0xA0),
+ WARNING(0xE0),
+ FATAL(0xF0);
+
+ public static final int POST_CODE_ADDR = 0xFE; // current POST code
+ public static final int ERROR_CODE_ADDR = 0xFD; // last error code
+ public static final int LED_STATUS_ADDR = 0xFC; // bitmask of components OK
+ public static final int KBD_DATA_ADDR = 0xFB; // KBD: next key (or 0 if none)
+ public static final int KBD_STATUS_ADDR = 0xFA; // KBD: bit0=key available
+ public static final int SND_BEEP_ADDR = 0xF9; // SND: any write triggers beep
+ public static final int BIOS_TRIGGER = 0xF7; // write any value to invoke BIOS diagnostics
+ public static final int BIOS_INFO_BASE = 0xE0; // 16 bytes of BIOS info (0xE0..0xEF)
+ public static final int DEVICE_TABLE_BASE = 0xD0; // 16 bytes device table (0xD0..0xDF)
+ public static final int GPU_OUT_CHAR = 0xC0; // write byte to output char on GPU display
+ public static final int DISK_SECTOR_ADDR = 0xB0; // write sector LBA here to queue a disk read
+ public static final int DISK_READY_ADDR = 0xB1; // read: 1=disk I/O ready, 0=busy
+ public static final int DISK_DATA_BASE = 0xB2; // 256 bytes of disk sector data (0xB2..0xFF)
+
+ // LED bits for the visualizer
+ public static final int LED_PWR = 0x01;
+ public static final int LED_CPU = 0x02;
+ public static final int LED_MEM = 0x04;
+ public static final int LED_VID = 0x08;
+ public static final int LED_KBD = 0x10;
+ public static final int LED_SND = 0x20;
+ public static final int LED_DSK = 0x40;
+ public static final int LED_CLK = 0x80;
+
+ public final int code;
+
+ PostCode(int code) { this.code = code; }
+
+ public static int componentLed(int code) {
+ int hi = (code >> 4) & 0x0F;
+ if (hi == 0x7) {
+ // BIOS codes (0x70-0x7F): map to the component being tested
+ int lo = code & 0x0F;
+ switch (lo) {
+ case 0x3: return LED_CPU; // BIOS_CPU
+ case 0x4: return LED_MEM; // BIOS_RAM
+ case 0x5: return LED_VID; // BIOS_GPU
+ case 0x6: return LED_KBD; // BIOS_KBD
+ case 0x7: return LED_SND; // BIOS_SND
+ case 0x8: return LED_DSK; // BIOS_DONE / boot attempt
+ default: return 0;
+ }
+ }
+ switch (hi) {
+ case 0x0: return LED_PWR;
+ case 0x1: return LED_CPU;
+ case 0x2: return LED_MEM;
+ case 0x3: return LED_VID;
+ case 0x4: return LED_KBD;
+ case 0x5: return LED_SND;
+ case 0x6: return LED_DSK;
+ case 0x8: return LED_DSK; // BOOT - disk LED
+ case 0xA: return LED_CLK; // runtime
+ default: return 0;
+ }
+ }
+
+ public static String describe(int code) {
+ switch (code) {
+ case 0x00: return "Idle";
+ case 0x01: return "Power on";
+ case 0x10: return "CPU detected";
+ case 0x11: return "CPU OK";
+ case 0x12: return "CPU FAIL";
+ case 0x20: return "Memory detected";
+ case 0x21: return "Memory OK";
+ case 0x22: return "Memory FAIL";
+ case 0x30: return "Video detected";
+ case 0x31: return "Video OK";
+ case 0x32: return "Video FAIL";
+ case 0x40: return "Keyboard detected";
+ case 0x41: return "Keyboard OK";
+ case 0x42: return "Keyboard FAIL";
+ case 0x50: return "Sound detected";
+ case 0x51: return "Sound OK";
+ case 0x52: return "Sound FAIL";
+ case 0x60: return "Disk detected";
+ case 0x61: return "Disk OK";
+ case 0x62: return "Disk FAIL";
+ case 0x70: return "BIOS loading";
+ case 0x71: return "BIOS OK";
+ case 0x72: return "BIOS FAIL";
+ case 0x80: return "Boot loader";
+ case 0x81: return "Boot OK";
+ case 0xA0: return "Runtime";
+ case 0xE0: return "WARNING";
+ case 0xF0: return "FATAL";
+ default: return "Code 0x" + Integer.toHexString(code);
+ }
+ }
+}
diff --git a/android/cbe-core/src/main/java/com/cbe/core/Registers.java b/android/cbe-core/src/main/java/com/cbe/core/Registers.java
new file mode 100644
index 0000000..4cbab6d
--- /dev/null
+++ b/android/cbe-core/src/main/java/com/cbe/core/Registers.java
@@ -0,0 +1,8 @@
+package com.cbe.core;
+
+public interface Registers {
+ int read(String name);
+ void write(String name, int value);
+ java.util.Set names();
+ void reset();
+}
diff --git a/android/cbe-core/src/main/java/com/cbe/loader/AbstractModuleInstance.java b/android/cbe-core/src/main/java/com/cbe/loader/AbstractModuleInstance.java
new file mode 100644
index 0000000..d1a3cc0
--- /dev/null
+++ b/android/cbe-core/src/main/java/com/cbe/loader/AbstractModuleInstance.java
@@ -0,0 +1,66 @@
+package com.cbe.loader;
+
+import com.cbe.core.*;
+
+import java.util.Arrays;
+
+public abstract class AbstractModuleInstance implements ModuleInstance {
+ protected final ModuleMetadata metadata;
+ protected final byte[][] dataBanks;
+
+ public AbstractModuleInstance(ModuleMetadata metadata, int bankCount, int bankSize) {
+ this.metadata = metadata;
+ this.dataBanks = new byte[bankCount][bankSize];
+ }
+
+ public AbstractModuleInstance(ModuleMetadata metadata, byte[][] dataBanks) {
+ this.metadata = metadata;
+ this.dataBanks = dataBanks;
+ }
+
+ public AbstractModuleInstance(ModuleMetadata metadata) {
+ this.metadata = metadata;
+ this.dataBanks = new byte[0][];
+ }
+
+ @Override
+ public ModuleMetadata getMetadata() {
+ return metadata;
+ }
+
+ @Override
+ public String getName() {
+ return metadata.getName();
+ }
+
+ @Override
+ public void init(Bus bus) {
+ }
+
+ @Override
+ public void reset() {
+ }
+
+ @Override
+ public void destroy() {
+ }
+
+ @Override
+ public byte[] readData(int bank, int offset, int size) {
+ if (bank < 0 || bank >= dataBanks.length) return new byte[size];
+ byte[] bankData = dataBanks[bank];
+ if (bankData == null) return new byte[size];
+ if (offset >= bankData.length) return new byte[size];
+ int actualSize = Math.min(size, bankData.length - offset);
+ return Arrays.copyOfRange(bankData, offset, offset + actualSize);
+ }
+
+ @Override
+ public void writeData(int bank, int offset, byte[] data) {
+ if (bank < 0 || bank >= dataBanks.length) return;
+ byte[] bankData = dataBanks[bank];
+ if (bankData == null || offset >= bankData.length) return;
+ int actualSize = Math.min(data.length, bankData.length - offset);
+ System.arraycopy(data, 0, bankData, offset, actualSize);
+ }
+}
diff --git a/android/cbe-core/src/main/java/com/cbe/loader/AudioBridge.java b/android/cbe-core/src/main/java/com/cbe/loader/AudioBridge.java
new file mode 100644
index 0000000..e34669b
--- /dev/null
+++ b/android/cbe-core/src/main/java/com/cbe/loader/AudioBridge.java
@@ -0,0 +1,13 @@
+package com.cbe.loader;
+
+public class AudioBridge {
+ private static BeepHandler handler;
+
+ public static void setBeepHandler(BeepHandler h) {
+ handler = h;
+ }
+
+ public static void beep() {
+ if (handler != null) handler.beep();
+ }
+}
diff --git a/android/cbe-core/src/main/java/com/cbe/loader/BeepHandler.java b/android/cbe-core/src/main/java/com/cbe/loader/BeepHandler.java
new file mode 100644
index 0000000..8276dc1
--- /dev/null
+++ b/android/cbe-core/src/main/java/com/cbe/loader/BeepHandler.java
@@ -0,0 +1,5 @@
+package com.cbe.loader;
+
+public interface BeepHandler {
+ void beep();
+}
diff --git a/android/cbe-core/src/main/java/com/cbe/loader/CompiledModuleLoader.java b/android/cbe-core/src/main/java/com/cbe/loader/CompiledModuleLoader.java
new file mode 100644
index 0000000..be850c5
--- /dev/null
+++ b/android/cbe-core/src/main/java/com/cbe/loader/CompiledModuleLoader.java
@@ -0,0 +1,568 @@
+package com.cbe.loader;
+
+import com.cbe.core.*;
+
+import java.io.*;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.*;
+import java.util.zip.CRC32;
+
+public class CompiledModuleLoader {
+
+ public ModuleInstance load(Path file) throws ModuleLoadException {
+ try {
+ byte[] raw = Files.readAllBytes(file);
+ return parse(raw);
+ } catch (IOException e) {
+ throw new ModuleLoadException("Failed to read .cbeplugin: " + file, e);
+ }
+ }
+
+ public ModuleInstance loadFromBytes(byte[] raw) throws ModuleLoadException {
+ return parse(raw);
+ }
+
+ private ModuleInstance parse(byte[] raw) throws ModuleLoadException {
+ if (raw.length < CbePluginConstants.HEADER_SIZE) {
+ throw new ModuleLoadException("File too small for valid .cbeplugin");
+ }
+
+ ByteBuffer buf = ByteBuffer.wrap(raw).order(ByteOrder.LITTLE_ENDIAN);
+
+ byte[] magicBytes = new byte[8];
+ buf.get(magicBytes);
+ String magic = new String(magicBytes, StandardCharsets.US_ASCII);
+ if (!magic.equals(CbePluginConstants.MAGIC)) {
+ throw new ModuleLoadException("Invalid magic: expected CBEPLUGIN, got " + magic);
+ }
+
+ int version = buf.getInt() & 0xFFFFFFFF;
+ int headerSize = buf.getInt() & 0xFFFFFFFF;
+ ModuleType moduleType = ModuleType.fromId(buf.get());
+ CompileMode compileMode = CompileMode.fromId(buf.get());
+
+ int metadataOff = buf.getInt();
+ int metadataLen = buf.getInt();
+ int opcodeOff = buf.getInt();
+ int opcodeLen = buf.getInt();
+ int microcodeOff = buf.getInt();
+ int microcodeLen = buf.getInt();
+ int handlerOff = buf.getInt();
+ int handlerLen = buf.getInt();
+ int dataOff = buf.getInt();
+ int dataLen = buf.getInt();
+
+ int storedChecksum = buf.getInt();
+
+ // Verify checksum (everything before checksum field)
+ CRC32 crc = new CRC32();
+ crc.update(raw, 0, CbePluginConstants.OFF_CHECKSUM);
+ if ((int) crc.getValue() != storedChecksum) {
+ throw new ModuleLoadException("Checksum mismatch");
+ }
+
+ // Parse metadata
+ ModuleMetadata metadata = parseMetadata(raw, metadataOff, metadataLen);
+
+ // Load opcode table
+ Map instructions = new HashMap();
+ if (opcodeOff > 0 && opcodeLen > 0) {
+ instructions = parseOpcodeTable(raw, opcodeOff, opcodeLen);
+ }
+
+ // Load microcodes
+ Map microcodes = new HashMap();
+ if (microcodeOff > 0 && microcodeLen > 0) {
+ microcodes = parseMicrocodeTable(raw, microcodeOff, microcodeLen);
+ }
+
+ // Load handler bytecode
+ byte[] handlerBytecode = new byte[0];
+ if (handlerOff > 0 && handlerLen > 0) {
+ handlerBytecode = Arrays.copyOfRange(raw, handlerOff, handlerOff + handlerLen);
+ }
+
+ // Load data banks
+ byte[][] dataBanks = new byte[0][];
+ if (dataOff > 0 && dataLen > 0) {
+ dataBanks = parseDataSection(raw, dataOff, dataLen);
+ }
+
+ switch (moduleType) {
+ case CPU:
+ return new CompiledCpuInstance(metadata, instructions, microcodes, handlerBytecode, dataBanks);
+ case RAM:
+ return new CompiledRamInstance(metadata, dataBanks, microcodes);
+ case GPU:
+ return new CompiledGpuInstance(metadata, dataBanks, microcodes);
+ case KBD:
+ return new CompiledKbdInstance(metadata);
+ case SND:
+ return new CompiledSndInstance(metadata);
+ case BIOS:
+ return new CompiledBiosInstance(metadata);
+ case DISK:
+ return new CompiledDiskInstance(metadata, dataBanks);
+ default:
+ throw new ModuleLoadException("Unsupported compiled module type: " + moduleType);
+ }
+ }
+
+ private ModuleMetadata parseMetadata(byte[] raw, int off, int len) {
+ if (len <= 0) {
+ return new ModuleMetadata("unknown", "generic", ModuleType.DATA_ONLY, 1, 0);
+ }
+ String json = new String(raw, off, len, StandardCharsets.UTF_8);
+ return metadataFromJson(json);
+ }
+
+ private ModuleMetadata metadataFromJson(String json) {
+ try {
+ Map map = JsonUtils.parseObject(json);
+ String name = map.containsKey("name") ? map.get("name").toString() : "unknown";
+ String arch = map.containsKey("arch") ? map.get("arch").toString() : "generic";
+ int version = map.containsKey("version") ? ((Number) map.get("version")).intValue() : 1;
+ float tdp = map.containsKey("tdp") ? ((Number) map.get("tdp")).floatValue() : 0;
+ int freq = map.containsKey("frequency") ? ((Number) map.get("frequency")).intValue() : 0;
+ ModuleType type = ModuleType.DATA_ONLY;
+ if (map.containsKey("module_type")) {
+ try {
+ type = ModuleType.valueOf(map.get("module_type").toString().toUpperCase());
+ } catch (IllegalArgumentException e) {
+ type = ModuleType.DATA_ONLY;
+ }
+ }
+ return new ModuleMetadata(name, arch, type, version, tdp, freq);
+ } catch (Exception e) {
+ return new ModuleMetadata("unknown", "generic", ModuleType.DATA_ONLY, 1, 0);
+ }
+ }
+
+ private Map parseOpcodeTable(byte[] raw, int off, int len) {
+ Map result = new HashMap();
+ String json = new String(raw, off, len, StandardCharsets.UTF_8);
+ try {
+ Object parsed = JsonUtils.parseValue(json);
+ if (parsed instanceof List) {
+ List