inital commit кек
This commit is contained in:
+45
-17
@@ -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
|
*.class
|
||||||
|
|
||||||
# Log file
|
|
||||||
*.log
|
|
||||||
|
|
||||||
# BlueJ files
|
|
||||||
*.ctxt
|
|
||||||
|
|
||||||
# Mobile Tools for Java (J2ME)
|
|
||||||
.mtj.tmp/
|
|
||||||
|
|
||||||
# Package Files #
|
|
||||||
*.jar
|
*.jar
|
||||||
*.war
|
!modules/**/*.jar
|
||||||
*.nar
|
!gradle/wrapper/gradle-wrapper.jar
|
||||||
*.ear
|
!android/gradle/wrapper/gradle-wrapper.jar
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
*.log
|
||||||
|
hs_err_pid*
|
||||||
|
|
||||||
|
# Package files
|
||||||
*.zip
|
*.zip
|
||||||
*.tar.gz
|
*.tar.gz
|
||||||
*.rar
|
*.rar
|
||||||
|
*.deb
|
||||||
|
|
||||||
# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
|
# BlueJ
|
||||||
hs_err_pid*
|
*.ctxt
|
||||||
|
.mtj.tmp/
|
||||||
|
|
||||||
|
# Virtual machine crash logs
|
||||||
replay_pid*
|
replay_pid*
|
||||||
|
|
||||||
|
# Windows portable runtime (prebuilt binaries)
|
||||||
|
dist/windows-portable/runtime/
|
||||||
|
dist/windows-portable/app/cbe-emu.jar
|
||||||
|
|
||||||
|
# Compiled binaries
|
||||||
|
*.exe
|
||||||
|
*.dll
|
||||||
|
*.dex
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
# CBE
|
# CBE
|
||||||
C.B.E. - CREATE. BUILD. EXECUTE.
|
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
|
||||||
|
```
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools">
|
||||||
|
|
||||||
|
<uses-permission android:name="android.permission.VIBRATE" />
|
||||||
|
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"
|
||||||
|
android:maxSdkVersion="32" />
|
||||||
|
|
||||||
|
<application
|
||||||
|
android:name=".CbeApp"
|
||||||
|
android:allowBackup="true"
|
||||||
|
android:icon="@mipmap/ic_launcher"
|
||||||
|
android:label="CBE Emulator"
|
||||||
|
android:supportsRtl="true"
|
||||||
|
android:theme="@style/Theme.CBE.Emulator"
|
||||||
|
tools:targetApi="35">
|
||||||
|
|
||||||
|
<activity
|
||||||
|
android:name=".MainActivity"
|
||||||
|
android:exported="true"
|
||||||
|
android:configChanges="orientation|screenSize|screenLayout|keyboardHidden"
|
||||||
|
android:windowSoftInputMode="adjustResize">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.MAIN" />
|
||||||
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
|
</intent-filter>
|
||||||
|
</activity>
|
||||||
|
</application>
|
||||||
|
|
||||||
|
</manifest>
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<PluginEntry> = emptyList(),
|
||||||
|
val pluginPaths: Map<String, String> = 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<EmulatorUiState> = _uiState.asStateFlow()
|
||||||
|
|
||||||
|
private val engine = AndroidEngine()
|
||||||
|
private var runJob: Job? = null
|
||||||
|
private var refreshJob: Job? = null
|
||||||
|
|
||||||
|
private val moduleProvider get() = getApplication<CbeApp>().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()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<String, File>) = 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()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<String> {
|
||||||
|
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/<pkg>/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<PluginEntry> {
|
||||||
|
val seen = mutableSetOf<String>()
|
||||||
|
val result = mutableListOf<PluginEntry>()
|
||||||
|
|
||||||
|
// 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<String, File> {
|
||||||
|
val all = listAllPlugins()
|
||||||
|
val byName = all.associateBy { it.name }
|
||||||
|
val result = mutableMapOf<String, File>()
|
||||||
|
|
||||||
|
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<PluginEntry>): Map<String, List<PluginEntry>> {
|
||||||
|
val result = linkedMapOf<String, MutableList<PluginEntry>>()
|
||||||
|
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<String, String> {
|
||||||
|
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) {}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<Pair<String, Int>>,
|
||||||
|
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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<PluginEntry>,
|
||||||
|
pluginConfig: PluginConfig,
|
||||||
|
pluginPaths: Map<String, String>,
|
||||||
|
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<PluginEntry>,
|
||||||
|
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))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
@@ -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
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
)
|
||||||
|
)
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="108dp"
|
||||||
|
android:height="108dp"
|
||||||
|
android:viewportWidth="108"
|
||||||
|
android:viewportHeight="108">
|
||||||
|
<group android:translateX="12" android:translateY="12">
|
||||||
|
<!-- Retro monitor body -->
|
||||||
|
<path
|
||||||
|
android:fillColor="#FF21262D"
|
||||||
|
android:pathData="M8,8h68v52H8z" />
|
||||||
|
<!-- Screen -->
|
||||||
|
<path
|
||||||
|
android:fillColor="#FF0D1117"
|
||||||
|
android:pathData="M14,14h56v36H14z" />
|
||||||
|
<!-- "CBE" text on screen -->
|
||||||
|
<path
|
||||||
|
android:fillColor="#FF00FF41"
|
||||||
|
android:pathData="M20,24h4v18h-4z" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#FF00FF41"
|
||||||
|
android:pathData="M24,24h14v4H24z" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#FF00FF41"
|
||||||
|
android:pathData="M24,31h12v4H24z" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#FF00FF41"
|
||||||
|
android:pathData="M24,38h14v4H24z" />
|
||||||
|
<!-- Prompt cursor -->
|
||||||
|
<path
|
||||||
|
android:fillColor="#FFFFB000"
|
||||||
|
android:pathData="M62,38h4v4h-4z" />
|
||||||
|
<!-- Monitor stand -->
|
||||||
|
<path
|
||||||
|
android:fillColor="#FF30363D"
|
||||||
|
android:pathData="M30,60h24v6H30z" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#FF30363D"
|
||||||
|
android:pathData="M38,66h8v10H38z" />
|
||||||
|
<!-- Stand base -->
|
||||||
|
<path
|
||||||
|
android:fillColor="#FF30363D"
|
||||||
|
android:pathData="M28,76h28v4H28z" />
|
||||||
|
<!-- LEDs -->
|
||||||
|
<path
|
||||||
|
android:fillColor="#FF00FF41"
|
||||||
|
android:pathData="M58,56a3,3,0,1,1,0,6z" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#FFFF3333"
|
||||||
|
android:pathData="M66,56a3,3,0,1,1,0,6z" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#FFFFB000"
|
||||||
|
android:pathData="M74,56a3,3,0,1,1,0,6z" />
|
||||||
|
</group>
|
||||||
|
</vector>
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<background android:drawable="@color/ic_background"/>
|
||||||
|
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
|
||||||
|
</adaptive-icon>
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<background android:drawable="@color/ic_background"/>
|
||||||
|
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
|
||||||
|
</adaptive-icon>
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<color name="ic_background">#0D1117</color>
|
||||||
|
</resources>
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<style name="Theme.CBE.Emulator" parent="android:Theme.Material.NoActionBar" />
|
||||||
|
</resources>
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
@@ -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() {}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<Arg> args;
|
||||||
|
private final int cycles;
|
||||||
|
private final List<SemanticOp> semantics;
|
||||||
|
|
||||||
|
public Instruction(int opcode, String mnemonic, List<Arg> args, int cycles, List<SemanticOp> semantics) {
|
||||||
|
this.opcode = opcode;
|
||||||
|
this.mnemonic = mnemonic;
|
||||||
|
this.args = args != null ? Collections.unmodifiableList(args) : Collections.<Arg>emptyList();
|
||||||
|
this.cycles = cycles;
|
||||||
|
this.semantics = semantics != null ? Collections.unmodifiableList(semantics) : Collections.<SemanticOp>emptyList();
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getOpcode() {
|
||||||
|
return opcode;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getMnemonic() {
|
||||||
|
return mnemonic;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<Arg> getArgs() {
|
||||||
|
return args;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getCycles() {
|
||||||
|
return cycles;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<SemanticOp> 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<String, Integer> OP_IDS = new java.util.HashMap<String, Integer>();
|
||||||
|
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<Integer, Instruction> instructionMap;
|
||||||
|
|
||||||
|
// Per-instruction cached spec info
|
||||||
|
private final java.util.HashMap<Instruction, CachedSpecs> 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<Instruction.SemanticOp> 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<Integer, Instruction> instructionMap) {
|
||||||
|
this.instructionMap = instructionMap;
|
||||||
|
this.specCache = new java.util.HashMap<Instruction, CachedSpecs>();
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
package com.cbe.core;
|
||||||
|
|
||||||
|
public interface Registers {
|
||||||
|
int read(String name);
|
||||||
|
void write(String name, int value);
|
||||||
|
java.util.Set<String> names();
|
||||||
|
void reset();
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
package com.cbe.loader;
|
||||||
|
|
||||||
|
public interface BeepHandler {
|
||||||
|
void beep();
|
||||||
|
}
|
||||||
@@ -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<Integer, Instruction> instructions = new HashMap<Integer, Instruction>();
|
||||||
|
if (opcodeOff > 0 && opcodeLen > 0) {
|
||||||
|
instructions = parseOpcodeTable(raw, opcodeOff, opcodeLen);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load microcodes
|
||||||
|
Map<String, byte[]> microcodes = new HashMap<String, byte[]>();
|
||||||
|
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<String, Object> 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<Integer, Instruction> parseOpcodeTable(byte[] raw, int off, int len) {
|
||||||
|
Map<Integer, Instruction> result = new HashMap<Integer, Instruction>();
|
||||||
|
String json = new String(raw, off, len, StandardCharsets.UTF_8);
|
||||||
|
try {
|
||||||
|
Object parsed = JsonUtils.parseValue(json);
|
||||||
|
if (parsed instanceof List) {
|
||||||
|
List<Object> list = (List<Object>) parsed;
|
||||||
|
for (Object item : list) {
|
||||||
|
if (item instanceof Map) {
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
Map<String, Object> map = (Map<String, Object>) item;
|
||||||
|
int opcode = ((Number) map.get("opcode")).intValue();
|
||||||
|
String mnemonic = map.containsKey("mnemonic") ? map.get("mnemonic").toString() : "UNK";
|
||||||
|
int cycles = map.containsKey("cycles") ? ((Number) map.get("cycles")).intValue() : 1;
|
||||||
|
|
||||||
|
List<Instruction.Arg> args = new ArrayList<Instruction.Arg>();
|
||||||
|
if (map.containsKey("args")) {
|
||||||
|
for (Object a : (List<Object>) map.get("args")) {
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
Map<String, Object> am = (Map<String, Object>) a;
|
||||||
|
args.add(new Instruction.Arg(
|
||||||
|
am.get("name").toString(),
|
||||||
|
am.get("type").toString()
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
List<Instruction.SemanticOp> semantics = new ArrayList<Instruction.SemanticOp>();
|
||||||
|
if (map.containsKey("semantics")) {
|
||||||
|
for (Object s : (List<Object>) map.get("semantics")) {
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
Map<String, Object> sm = (Map<String, Object>) s;
|
||||||
|
semantics.add(new Instruction.SemanticOp(
|
||||||
|
sm.containsKey("op") ? sm.get("op").toString() : null,
|
||||||
|
sm.containsKey("from") ? sm.get("from").toString() : null,
|
||||||
|
sm.containsKey("to") ? sm.get("to").toString() : null,
|
||||||
|
sm.containsKey("value") ? sm.get("value").toString() : null,
|
||||||
|
sm.containsKey("condition") ? sm.get("condition").toString() : null,
|
||||||
|
sm.containsKey("result") ? sm.get("result").toString() : null
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
result.put(opcode, new Instruction(opcode, mnemonic, args, cycles, semantics));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
// If parsing fails, return empty map
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map<String, byte[]> parseMicrocodeTable(byte[] raw, int off, int len) {
|
||||||
|
Map<String, byte[]> result = new HashMap<String, byte[]>();
|
||||||
|
String json = new String(raw, off, len, StandardCharsets.UTF_8);
|
||||||
|
try {
|
||||||
|
Object parsed = JsonUtils.parseValue(json);
|
||||||
|
if (parsed instanceof Map) {
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
Map<String, Object> map = (Map<String, Object>) parsed;
|
||||||
|
for (Map.Entry<String, Object> entry : map.entrySet()) {
|
||||||
|
if (entry.getValue() instanceof String) {
|
||||||
|
result.put(entry.getKey(), ((String) entry.getValue()).getBytes(StandardCharsets.UTF_8));
|
||||||
|
} else if (entry.getValue() instanceof List) {
|
||||||
|
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||||
|
for (Object v : (List<Object>) entry.getValue()) {
|
||||||
|
if (v instanceof Number) baos.write(((Number) v).byteValue());
|
||||||
|
}
|
||||||
|
result.put(entry.getKey(), baos.toByteArray());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
// ignore parse errors, return empty
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private byte[][] parseDataSection(byte[] raw, int off, int len) {
|
||||||
|
// Simple format: [bank_count:4bytes][bank0_size:4bytes][bank0_data...][bank1_size:4bytes]...
|
||||||
|
if (len < 4) return new byte[0][];
|
||||||
|
ByteBuffer buf = ByteBuffer.wrap(raw, off, len).order(ByteOrder.LITTLE_ENDIAN);
|
||||||
|
int bankCount = buf.getInt();
|
||||||
|
if (bankCount <= 0 || bankCount > 256) return new byte[0][];
|
||||||
|
byte[][] banks = new byte[bankCount][];
|
||||||
|
for (int i = 0; i < bankCount; i++) {
|
||||||
|
if (buf.remaining() < 4) break;
|
||||||
|
int bankSize = buf.getInt();
|
||||||
|
if (bankSize <= 0 || bankSize > 1024 * 1024) break;
|
||||||
|
bankSize = Math.min(bankSize, buf.remaining());
|
||||||
|
banks[i] = new byte[bankSize];
|
||||||
|
buf.get(banks[i]);
|
||||||
|
}
|
||||||
|
return banks;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----- Compiled CPU Instance -----
|
||||||
|
|
||||||
|
public static class CompiledCpuInstance extends AbstractModuleInstance {
|
||||||
|
private final Map<Integer, Instruction> instructions;
|
||||||
|
private final InstructionExecutor executor;
|
||||||
|
private final Map<String, byte[]> microcodes;
|
||||||
|
private final byte[] workingMemory;
|
||||||
|
|
||||||
|
CompiledCpuInstance(ModuleMetadata metadata, Map<Integer, Instruction> instructions,
|
||||||
|
Map<String, byte[]> microcodes, byte[] handlerBytecode,
|
||||||
|
byte[][] dataBanks) {
|
||||||
|
super(metadata, dataBanks);
|
||||||
|
this.instructions = instructions;
|
||||||
|
this.executor = new InstructionExecutor(instructions);
|
||||||
|
this.microcodes = microcodes;
|
||||||
|
// Unified 64K address space: copy the program ROM at the start, zero-fill the rest.
|
||||||
|
// This prevents STORE/LOAD from corrupting the program ROM while still letting
|
||||||
|
// programs read constants from the ROM area via $next or mem[N] with N < romSize.
|
||||||
|
this.workingMemory = new byte[65536];
|
||||||
|
if (dataBanks.length > 0 && dataBanks[0] != null) {
|
||||||
|
int copyLen = Math.min(dataBanks[0].length, 65536);
|
||||||
|
System.arraycopy(dataBanks[0], 0, workingMemory, 0, copyLen);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[] getWorkingMemory() { return workingMemory; }
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public OpcodeResult executeOpcode(int opcode, Registers regs, Bus bus) {
|
||||||
|
if (opcode == 0xFF) {
|
||||||
|
return OpcodeResult.halt(1);
|
||||||
|
}
|
||||||
|
return executor.execute(opcode, regs, bus, workingMemory);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public byte[] getMicrocode(String command) {
|
||||||
|
byte[] mc = microcodes.get(command);
|
||||||
|
return mc != null ? mc.clone() : new byte[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[] getRawDataBank(int index) {
|
||||||
|
if (index < 0 || index >= dataBanks.length) return null;
|
||||||
|
return dataBanks[index];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----- Compiled RAM Instance -----
|
||||||
|
|
||||||
|
private static class CompiledRamInstance extends AbstractModuleInstance {
|
||||||
|
private final Map<String, byte[]> microcodes;
|
||||||
|
|
||||||
|
CompiledRamInstance(ModuleMetadata metadata, byte[][] banks, Map<String, byte[]> microcodes) {
|
||||||
|
super(metadata, banks);
|
||||||
|
this.microcodes = microcodes;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public OpcodeResult executeOpcode(int opcode, Registers regs, Bus bus) {
|
||||||
|
return OpcodeResult.ok(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public byte[] getMicrocode(String command) {
|
||||||
|
byte[] mc = microcodes.get(command);
|
||||||
|
return mc != null ? mc.clone() : new byte[0];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class CompiledGpuInstance extends AbstractModuleInstance {
|
||||||
|
private final Map<String, byte[]> microcodes;
|
||||||
|
private int cursorX;
|
||||||
|
private int cursorY;
|
||||||
|
private boolean needsRedraw;
|
||||||
|
private int rows = 25;
|
||||||
|
private int cols = 80;
|
||||||
|
|
||||||
|
CompiledGpuInstance(ModuleMetadata metadata, byte[][] banks, Map<String, byte[]> microcodes) {
|
||||||
|
super(metadata, banks);
|
||||||
|
this.microcodes = microcodes;
|
||||||
|
this.cursorX = 0;
|
||||||
|
this.cursorY = 0;
|
||||||
|
this.needsRedraw = true;
|
||||||
|
if (banks.length > 0 && banks[0].length >= 2000) {
|
||||||
|
this.rows = 25;
|
||||||
|
this.cols = banks[0].length / 25;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public OpcodeResult executeOpcode(int opcode, Registers regs, Bus bus) {
|
||||||
|
return OpcodeResult.ok(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public byte[] getMicrocode(String command) {
|
||||||
|
byte[] mc = microcodes.get(command);
|
||||||
|
return mc != null ? mc.clone() : new byte[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[] getVram() {
|
||||||
|
return dataBanks.length > 0 ? dataBanks[0] : new byte[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getRows() { return rows; }
|
||||||
|
public int getCols() { return cols; }
|
||||||
|
public int getCursorX() { return cursorX; }
|
||||||
|
public int getCursorY() { return cursorY; }
|
||||||
|
public boolean isDirty() { return needsRedraw; }
|
||||||
|
public void markClean() { needsRedraw = false; }
|
||||||
|
|
||||||
|
public void writeChar(char c) {
|
||||||
|
if (dataBanks.length == 0) return;
|
||||||
|
byte[] vram = dataBanks[0];
|
||||||
|
int offset = cursorY * cols + cursorX;
|
||||||
|
if (offset >= vram.length) return;
|
||||||
|
vram[offset] = (byte) (c & 0x7F);
|
||||||
|
cursorX++;
|
||||||
|
if (cursorX >= cols) {
|
||||||
|
cursorX = 0;
|
||||||
|
cursorY++;
|
||||||
|
if (cursorY >= rows) scroll();
|
||||||
|
}
|
||||||
|
needsRedraw = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void writeString(String s) {
|
||||||
|
for (int i = 0; i < s.length(); i++) {
|
||||||
|
char c = s.charAt(i);
|
||||||
|
if (c == '\n') {
|
||||||
|
cursorX = 0;
|
||||||
|
cursorY++;
|
||||||
|
if (cursorY >= rows) scroll();
|
||||||
|
} else {
|
||||||
|
writeChar(c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
needsRedraw = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void clear() {
|
||||||
|
if (dataBanks.length == 0) return;
|
||||||
|
byte[] vram = dataBanks[0];
|
||||||
|
for (int i = 0; i < vram.length; i++) vram[i] = 0;
|
||||||
|
cursorX = 0;
|
||||||
|
cursorY = 0;
|
||||||
|
needsRedraw = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setCursor(int x, int y) {
|
||||||
|
this.cursorX = Math.max(0, Math.min(cols - 1, x));
|
||||||
|
this.cursorY = Math.max(0, Math.min(rows - 1, y));
|
||||||
|
needsRedraw = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void scroll() {
|
||||||
|
if (dataBanks.length == 0) return;
|
||||||
|
byte[] vram = dataBanks[0];
|
||||||
|
for (int row = 0; row < rows - 1; row++) {
|
||||||
|
for (int col = 0; col < cols; col++) {
|
||||||
|
vram[row * cols + col] = vram[(row + 1) * cols + col];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (int col = 0; col < cols; col++) {
|
||||||
|
vram[(rows - 1) * cols + col] = 0;
|
||||||
|
}
|
||||||
|
cursorY = rows - 1;
|
||||||
|
needsRedraw = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String readString() {
|
||||||
|
if (dataBanks.length == 0) return "";
|
||||||
|
byte[] vram = dataBanks[0];
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
for (int row = 0; row < rows; row++) {
|
||||||
|
for (int col = 0; col < cols; col++) {
|
||||||
|
int b = vram[row * cols + col] & 0xFF;
|
||||||
|
if (b == 0) sb.append(' ');
|
||||||
|
else sb.append((char) (b & 0x7F));
|
||||||
|
}
|
||||||
|
if (row < rows - 1) sb.append('\n');
|
||||||
|
}
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========================================================================
|
||||||
|
// Compiled KBD - keyboard input device
|
||||||
|
// ========================================================================
|
||||||
|
public static class CompiledKbdInstance extends AbstractModuleInstance {
|
||||||
|
private final java.util.concurrent.LinkedBlockingQueue<Integer> keyBuffer;
|
||||||
|
|
||||||
|
CompiledKbdInstance(ModuleMetadata metadata) {
|
||||||
|
super(metadata);
|
||||||
|
this.keyBuffer = new java.util.concurrent.LinkedBlockingQueue<Integer>(32);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public OpcodeResult executeOpcode(int opcode, Registers regs, Bus bus) {
|
||||||
|
return OpcodeResult.ok(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public byte[] readData(int bank, int offset, int size) {
|
||||||
|
byte[] result = new byte[size];
|
||||||
|
if (offset == com.cbe.core.PostCode.KBD_DATA_ADDR) {
|
||||||
|
// Peek: don't remove. CPU consumes by writing 0 to KBD_STATUS_ADDR.
|
||||||
|
Integer k = keyBuffer.peek();
|
||||||
|
result[0] = (byte) (k != null ? (k & 0xFF) : 0);
|
||||||
|
} else if (offset == com.cbe.core.PostCode.KBD_STATUS_ADDR) {
|
||||||
|
result[0] = (byte) (keyBuffer.isEmpty() ? 0 : 1);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void writeData(int bank, int offset, byte[] data) {
|
||||||
|
// Writing 0 to KBD_STATUS_ADDR acknowledges the key (consume it).
|
||||||
|
if (offset == com.cbe.core.PostCode.KBD_STATUS_ADDR && data != null && data.length > 0
|
||||||
|
&& (data[0] & 0xFF) == 0) {
|
||||||
|
keyBuffer.poll();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public byte[] getMicrocode(String command) { return new byte[0]; }
|
||||||
|
|
||||||
|
public void pushKey(int keyCode) {
|
||||||
|
if (keyBuffer.remainingCapacity() == 0) keyBuffer.poll();
|
||||||
|
keyBuffer.offer(keyCode & 0xFF);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void clear() { keyBuffer.clear(); }
|
||||||
|
public int available() { return keyBuffer.size(); }
|
||||||
|
|
||||||
|
/** Consume the next key (called by the Engine when the CPU writes 0 to KBD_STATUS_ADDR). */
|
||||||
|
public void acknowledge() { keyBuffer.poll(); }
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========================================================================
|
||||||
|
// Compiled SND - sound/beep device
|
||||||
|
// ========================================================================
|
||||||
|
public static class CompiledSndInstance extends AbstractModuleInstance {
|
||||||
|
private long lastBeepNs = 0;
|
||||||
|
private int beepCount = 0;
|
||||||
|
|
||||||
|
CompiledSndInstance(ModuleMetadata metadata) {
|
||||||
|
super(metadata);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public OpcodeResult executeOpcode(int opcode, Registers regs, Bus bus) {
|
||||||
|
return OpcodeResult.ok(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public byte[] getMicrocode(String command) { return new byte[0]; }
|
||||||
|
|
||||||
|
public void beep() {
|
||||||
|
long now = System.nanoTime();
|
||||||
|
if (now - lastBeepNs < 100_000_000L) return;
|
||||||
|
lastBeepNs = now;
|
||||||
|
beepCount++;
|
||||||
|
AudioBridge.beep();
|
||||||
|
}
|
||||||
|
|
||||||
|
public int beepCount() { return beepCount; }
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========================================================================
|
||||||
|
// Compiled DISK - provides sector storage
|
||||||
|
// ========================================================================
|
||||||
|
public static class CompiledDiskInstance extends AbstractModuleInstance {
|
||||||
|
private static final int SECTOR_SIZE = 512;
|
||||||
|
|
||||||
|
CompiledDiskInstance(ModuleMetadata metadata, byte[][] dataBanks) {
|
||||||
|
super(metadata, dataBanks);
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getSectorSize() { return SECTOR_SIZE; }
|
||||||
|
public int getSectorCount() {
|
||||||
|
return dataBanks.length > 0 && dataBanks[0] != null ? dataBanks[0].length / SECTOR_SIZE : 0;
|
||||||
|
}
|
||||||
|
public byte[] getDiskData() {
|
||||||
|
return dataBanks.length > 0 ? dataBanks[0] : new byte[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Read one sector (0-indexed LBA). Returns sector bytes or 0-filled if out of range. */
|
||||||
|
public byte[] readSector(int lba) {
|
||||||
|
if (lba < 0 || dataBanks.length == 0 || dataBanks[0] == null) return new byte[SECTOR_SIZE];
|
||||||
|
long off = (long) lba * SECTOR_SIZE;
|
||||||
|
if (off >= dataBanks[0].length) return new byte[SECTOR_SIZE];
|
||||||
|
int end = (int) Math.min(off + SECTOR_SIZE, dataBanks[0].length);
|
||||||
|
return java.util.Arrays.copyOfRange(dataBanks[0], (int) off, end);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public OpcodeResult executeOpcode(int opcode, Registers regs, Bus bus) {
|
||||||
|
return OpcodeResult.ok(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public byte[] getMicrocode(String command) { return new byte[0]; }
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========================================================================
|
||||||
|
// Compiled BIOS - boot ROM
|
||||||
|
// ========================================================================
|
||||||
|
public static class CompiledBiosInstance extends AbstractModuleInstance {
|
||||||
|
private String systemInfo = "CBE BIOS v0.1 (no scan yet)";
|
||||||
|
|
||||||
|
CompiledBiosInstance(ModuleMetadata metadata) {
|
||||||
|
super(metadata);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public OpcodeResult executeOpcode(int opcode, Registers regs, Bus bus) {
|
||||||
|
return OpcodeResult.ok(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public byte[] getMicrocode(String command) { return new byte[0]; }
|
||||||
|
|
||||||
|
public void setSystemInfo(String info) { this.systemInfo = info; }
|
||||||
|
public String getSystemInfo() { return systemInfo; }
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,96 @@
|
|||||||
|
package com.cbe.loader;
|
||||||
|
|
||||||
|
import java.util.*;
|
||||||
|
|
||||||
|
public final class JsonUtils {
|
||||||
|
|
||||||
|
private JsonUtils() {}
|
||||||
|
|
||||||
|
public static Map<String, Object> parseObject(String json) {
|
||||||
|
json = json.trim();
|
||||||
|
if (!json.startsWith("{")) return new LinkedHashMap<String, Object>();
|
||||||
|
json = json.substring(1, json.lastIndexOf('}')).trim();
|
||||||
|
if (json.isEmpty()) return new LinkedHashMap<String, Object>();
|
||||||
|
|
||||||
|
Map<String, Object> result = new LinkedHashMap<String, Object>();
|
||||||
|
List<String> pairs = splitPairs(json);
|
||||||
|
for (String pair : pairs) {
|
||||||
|
int colon = findColon(pair);
|
||||||
|
if (colon < 0) continue;
|
||||||
|
String key = pair.substring(0, colon).trim();
|
||||||
|
if (key.startsWith("\"") && key.endsWith("\"")) {
|
||||||
|
key = key.substring(1, key.length() - 1);
|
||||||
|
}
|
||||||
|
String valueStr = pair.substring(colon + 1).trim();
|
||||||
|
result.put(key, parseValue(valueStr));
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static List<Object> parseArray(String json) {
|
||||||
|
json = json.trim();
|
||||||
|
if (!json.startsWith("[")) return new ArrayList<Object>();
|
||||||
|
json = json.substring(1, json.lastIndexOf(']')).trim();
|
||||||
|
if (json.isEmpty()) return new ArrayList<Object>();
|
||||||
|
|
||||||
|
List<Object> result = new ArrayList<Object>();
|
||||||
|
List<String> items = splitPairs(json);
|
||||||
|
for (String item : items) {
|
||||||
|
result.add(parseValue(item.trim()));
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Object parseValue(String json) {
|
||||||
|
json = json.trim();
|
||||||
|
if (json.startsWith("{")) return parseObject(json);
|
||||||
|
if (json.startsWith("[")) return parseArray(json);
|
||||||
|
if (json.startsWith("\"")) {
|
||||||
|
return json.substring(1, json.length() - 1);
|
||||||
|
}
|
||||||
|
if (json.equals("true")) return Boolean.TRUE;
|
||||||
|
if (json.equals("false")) return Boolean.FALSE;
|
||||||
|
if (json.equals("null")) return null;
|
||||||
|
try {
|
||||||
|
if (json.contains(".") || json.contains("e") || json.contains("E")) {
|
||||||
|
return Double.parseDouble(json);
|
||||||
|
}
|
||||||
|
return Integer.parseInt(json);
|
||||||
|
} catch (NumberFormatException e) {
|
||||||
|
return json;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<String> splitPairs(String json) {
|
||||||
|
List<String> pairs = new ArrayList<String>();
|
||||||
|
int depth = 0;
|
||||||
|
int last = 0;
|
||||||
|
boolean inString = false;
|
||||||
|
for (int i = 0; i < json.length(); i++) {
|
||||||
|
char c = json.charAt(i);
|
||||||
|
if (c == '"' && (i == 0 || json.charAt(i - 1) != '\\')) {
|
||||||
|
inString = !inString;
|
||||||
|
}
|
||||||
|
if (!inString) {
|
||||||
|
if (c == '{' || c == '[') depth++;
|
||||||
|
if (c == '}' || c == ']') depth--;
|
||||||
|
if ((c == ',' || c == ';') && depth == 0) {
|
||||||
|
pairs.add(json.substring(last, i));
|
||||||
|
last = i + 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pairs.add(json.substring(last));
|
||||||
|
return pairs;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int findColon(String s) {
|
||||||
|
boolean inString = false;
|
||||||
|
for (int i = 0; i < s.length(); i++) {
|
||||||
|
char c = s.charAt(i);
|
||||||
|
if (c == '"') inString = !inString;
|
||||||
|
if (c == ':' && !inString) return i;
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
package com.cbe.loader;
|
||||||
|
|
||||||
|
public class ModuleLoadException extends Exception {
|
||||||
|
public ModuleLoadException(String message) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ModuleLoadException(String message, Throwable cause) {
|
||||||
|
super(message, cause);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
package com.cbe.loader;
|
||||||
|
|
||||||
|
import com.cbe.core.ModuleInstance;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
|
||||||
|
public class ModuleLoader {
|
||||||
|
private final SourceModuleLoader sourceLoader;
|
||||||
|
private final CompiledModuleLoader compiledLoader;
|
||||||
|
|
||||||
|
public ModuleLoader() {
|
||||||
|
this.sourceLoader = new SourceModuleLoader();
|
||||||
|
this.compiledLoader = new CompiledModuleLoader();
|
||||||
|
}
|
||||||
|
|
||||||
|
public ModuleInstance load(Path path) throws ModuleLoadException {
|
||||||
|
if (!Files.exists(path)) {
|
||||||
|
throw new ModuleLoadException("Path does not exist: " + path.toAbsolutePath());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Files.isDirectory(path)) {
|
||||||
|
return sourceLoader.load(path);
|
||||||
|
} else {
|
||||||
|
String fileName = path.getFileName().toString().toLowerCase();
|
||||||
|
if (fileName.endsWith(".cbeplugin")) {
|
||||||
|
return compiledLoader.load(path);
|
||||||
|
}
|
||||||
|
throw new ModuleLoadException("Unknown module file format: " + fileName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public ModuleInstance loadSource(Path dir) throws ModuleLoadException {
|
||||||
|
return sourceLoader.load(dir);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ModuleInstance loadCompiled(Path file) throws ModuleLoadException {
|
||||||
|
return compiledLoader.load(file);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,108 @@
|
|||||||
|
package com.cbe.loader;
|
||||||
|
|
||||||
|
import com.cbe.core.Bus;
|
||||||
|
import com.cbe.core.ModuleInstance;
|
||||||
|
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
public class SimpleBus implements Bus {
|
||||||
|
private final Map<Integer, BusMapping> mappings;
|
||||||
|
private final Map<String, ModuleInstance> devices;
|
||||||
|
private long clockCycle;
|
||||||
|
|
||||||
|
public SimpleBus() {
|
||||||
|
this.mappings = new LinkedHashMap<Integer, BusMapping>();
|
||||||
|
this.devices = new LinkedHashMap<String, ModuleInstance>();
|
||||||
|
this.clockCycle = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public byte read(int address) {
|
||||||
|
BusMapping mapping = findMapping(address);
|
||||||
|
if (mapping != null) {
|
||||||
|
int localAddr = address - mapping.baseAddress;
|
||||||
|
byte[] data = mapping.device.readData(0, localAddr, 1);
|
||||||
|
return data != null && data.length > 0 ? data[0] : 0;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void write(int address, byte data) {
|
||||||
|
BusMapping mapping = findMapping(address);
|
||||||
|
if (mapping != null) {
|
||||||
|
int localAddr = address - mapping.baseAddress;
|
||||||
|
mapping.device.writeData(0, localAddr, new byte[]{data});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int readWord(int address) {
|
||||||
|
int low = read(address) & 0xFF;
|
||||||
|
int high = read(address + 1) & 0xFF;
|
||||||
|
return (high << 8) | low;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void writeWord(int address, int value) {
|
||||||
|
write(address, (byte) (value & 0xFF));
|
||||||
|
write(address + 1, (byte) ((value >> 8) & 0xFF));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long clock() {
|
||||||
|
return clockCycle;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long tick() {
|
||||||
|
return ++clockCycle;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void attach(String deviceName, int baseAddress, int size) {
|
||||||
|
ModuleInstance dev = devices.get(deviceName);
|
||||||
|
if (dev != null) {
|
||||||
|
mappings.put(baseAddress, new BusMapping(dev, baseAddress, size));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void registerDevice(String name, ModuleInstance device) {
|
||||||
|
devices.put(name, device);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ModuleInstance getDevice(String name) {
|
||||||
|
return devices.get(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void reset() {
|
||||||
|
clockCycle = 0;
|
||||||
|
for (ModuleInstance dev : devices.values()) {
|
||||||
|
dev.reset();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private BusMapping findMapping(int address) {
|
||||||
|
for (BusMapping m : mappings.values()) {
|
||||||
|
if (address >= m.baseAddress && address < m.baseAddress + m.size) {
|
||||||
|
return m;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class BusMapping {
|
||||||
|
final ModuleInstance device;
|
||||||
|
final int baseAddress;
|
||||||
|
final int size;
|
||||||
|
|
||||||
|
BusMapping(ModuleInstance device, int baseAddress, int size) {
|
||||||
|
this.device = device;
|
||||||
|
this.baseAddress = baseAddress;
|
||||||
|
this.size = size;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
package com.cbe.loader;
|
||||||
|
|
||||||
|
import com.cbe.core.Registers;
|
||||||
|
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
public class SimpleRegisters implements Registers {
|
||||||
|
private final Map<String, Integer> registers;
|
||||||
|
private final Map<String, Integer> defaults;
|
||||||
|
|
||||||
|
public SimpleRegisters() {
|
||||||
|
this(new LinkedHashMap<String, Integer>());
|
||||||
|
}
|
||||||
|
|
||||||
|
public SimpleRegisters(Map<String, Integer> initialValues) {
|
||||||
|
this.registers = new LinkedHashMap<String, Integer>();
|
||||||
|
this.defaults = new LinkedHashMap<String, Integer>();
|
||||||
|
if (initialValues != null) {
|
||||||
|
for (Map.Entry<String, Integer> e : initialValues.entrySet()) {
|
||||||
|
int val = e.getValue() != null ? e.getValue() : 0;
|
||||||
|
this.registers.put(e.getKey(), val);
|
||||||
|
this.defaults.put(e.getKey(), val);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int read(String name) {
|
||||||
|
Integer val = registers.get(name);
|
||||||
|
return val != null ? val : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void write(String name, int value) {
|
||||||
|
registers.put(name, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Set<String> names() {
|
||||||
|
return Collections.unmodifiableSet(registers.keySet());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void reset() {
|
||||||
|
registers.clear();
|
||||||
|
registers.putAll(defaults);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,723 @@
|
|||||||
|
package com.cbe.loader;
|
||||||
|
|
||||||
|
import com.cbe.core.*;
|
||||||
|
|
||||||
|
import java.io.*;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.nio.file.*;
|
||||||
|
import java.util.*;
|
||||||
|
|
||||||
|
public class SourceModuleLoader {
|
||||||
|
|
||||||
|
public ModuleInstance load(Path dir) throws ModuleLoadException {
|
||||||
|
Path moduleJson = dir.resolve("module.json");
|
||||||
|
if (!Files.exists(moduleJson)) {
|
||||||
|
throw new ModuleLoadException("Missing module.json in " + dir.toAbsolutePath());
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, Object> config = readJson(moduleJson);
|
||||||
|
ModuleType type = parseModuleType(config);
|
||||||
|
ModuleMetadata metadata = parseMetadata(config, type);
|
||||||
|
|
||||||
|
switch (type) {
|
||||||
|
case CPU:
|
||||||
|
return loadCpu(dir, metadata, config);
|
||||||
|
case RAM:
|
||||||
|
return loadRam(dir, metadata, config);
|
||||||
|
case GPU:
|
||||||
|
return loadGpu(dir, metadata, config);
|
||||||
|
case KBD:
|
||||||
|
return loadKbd(dir, metadata, config);
|
||||||
|
case SND:
|
||||||
|
return loadSnd(dir, metadata, config);
|
||||||
|
case BIOS:
|
||||||
|
return loadBios(dir, metadata, config);
|
||||||
|
case DISK:
|
||||||
|
return loadDisk(dir, metadata, config);
|
||||||
|
default:
|
||||||
|
throw new ModuleLoadException("Unsupported module type: " + type);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
private ModuleMetadata parseMetadata(Map<String, Object> config, ModuleType type) {
|
||||||
|
String name = getString(config, "name", "unknown");
|
||||||
|
String arch = getString(config, "arch", "generic");
|
||||||
|
int version = getInt(config, "version", 1);
|
||||||
|
float tdp = getFloat(config, "tdp", 0.0f);
|
||||||
|
int freq = getInt(config, "frequency", 0);
|
||||||
|
return new ModuleMetadata(name, arch, type, version, tdp, freq);
|
||||||
|
}
|
||||||
|
|
||||||
|
private ModuleType parseModuleType(Map<String, Object> config) throws ModuleLoadException {
|
||||||
|
String typeStr = getString(config, "module_type", null);
|
||||||
|
if (typeStr == null) {
|
||||||
|
throw new ModuleLoadException("Missing 'module_type' in module.json");
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return ModuleType.valueOf(typeStr.toUpperCase());
|
||||||
|
} catch (IllegalArgumentException e) {
|
||||||
|
throw new ModuleLoadException("Unknown module_type: " + typeStr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private ModuleInstance loadCpu(Path dir, ModuleMetadata metadata, Map<String, Object> config) throws ModuleLoadException {
|
||||||
|
Path registersFile = dir.resolve("registers.json");
|
||||||
|
if (!Files.exists(registersFile)) {
|
||||||
|
throw new ModuleLoadException("CPU module missing registers.json");
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, Integer> regDefaults = parseRegisters(registersFile);
|
||||||
|
|
||||||
|
Path instructionsDir = dir.resolve("instructions");
|
||||||
|
Map<Integer, Instruction> instructions = new HashMap<Integer, Instruction>();
|
||||||
|
if (Files.isDirectory(instructionsDir)) {
|
||||||
|
List<Path> instFiles = listJsonFiles(instructionsDir);
|
||||||
|
for (Path f : instFiles) {
|
||||||
|
Instruction inst = parseInstruction(f);
|
||||||
|
instructions.put(inst.getOpcode(), inst);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Path microcodeDir = dir.resolve("microcode");
|
||||||
|
Map<String, byte[]> microcodes = new HashMap<String, byte[]>();
|
||||||
|
if (Files.isDirectory(microcodeDir)) {
|
||||||
|
loadMicrocodes(microcodeDir, microcodes);
|
||||||
|
}
|
||||||
|
|
||||||
|
Path romsDir = dir.resolve("roms");
|
||||||
|
List<byte[]> romBanks = new ArrayList<byte[]>();
|
||||||
|
if (Files.isDirectory(romsDir)) {
|
||||||
|
loadRoms(romsDir, romBanks);
|
||||||
|
}
|
||||||
|
|
||||||
|
int memorySize = getInt(config, "memory_size", 256);
|
||||||
|
return new CpuModuleInstance(metadata, regDefaults, instructions, microcodes, romBanks, memorySize);
|
||||||
|
}
|
||||||
|
|
||||||
|
private ModuleInstance loadRam(Path dir, ModuleMetadata metadata, Map<String, Object> config) throws ModuleLoadException {
|
||||||
|
int bankSize = getInt(config, "bank_size", 256);
|
||||||
|
int bankCount = getInt(config, "bank_count", 1);
|
||||||
|
|
||||||
|
byte[][] banks = new byte[bankCount][bankSize];
|
||||||
|
|
||||||
|
Path banksDir = dir.resolve("banks");
|
||||||
|
if (Files.isDirectory(banksDir)) {
|
||||||
|
List<Path> bankFiles = new ArrayList<Path>();
|
||||||
|
try (DirectoryStream<Path> stream = Files.newDirectoryStream(banksDir, "*.bin")) {
|
||||||
|
for (Path p : stream) bankFiles.add(p);
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new ModuleLoadException("Failed to list banks", e);
|
||||||
|
}
|
||||||
|
Collections.sort(bankFiles);
|
||||||
|
for (int i = 0; i < Math.min(bankFiles.size(), bankCount); i++) {
|
||||||
|
try {
|
||||||
|
byte[] data = Files.readAllBytes(bankFiles.get(i));
|
||||||
|
System.arraycopy(data, 0, banks[i], 0, Math.min(data.length, bankSize));
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new ModuleLoadException("Failed to read bank: " + bankFiles.get(i), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Path controllerDir = dir.resolve("controller");
|
||||||
|
Map<String, byte[]> microcodes = new HashMap<String, byte[]>();
|
||||||
|
if (Files.isDirectory(controllerDir)) {
|
||||||
|
Path mcDir = controllerDir.resolve("microcode");
|
||||||
|
if (Files.isDirectory(mcDir)) {
|
||||||
|
loadMicrocodes(mcDir, microcodes);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return new RamModuleInstance(metadata, banks, microcodes);
|
||||||
|
}
|
||||||
|
|
||||||
|
private ModuleInstance loadGpu(Path dir, ModuleMetadata metadata, Map<String, Object> config) throws ModuleLoadException {
|
||||||
|
int vramSize = getInt(config, "vram_size", 2000);
|
||||||
|
int rows = getInt(config, "rows", 25);
|
||||||
|
int cols = getInt(config, "cols", 80);
|
||||||
|
|
||||||
|
byte[][] vram = new byte[1][vramSize];
|
||||||
|
|
||||||
|
Path banksDir = dir.resolve("banks");
|
||||||
|
if (Files.isDirectory(banksDir)) {
|
||||||
|
List<Path> bankFiles = new ArrayList<Path>();
|
||||||
|
try (DirectoryStream<Path> stream = Files.newDirectoryStream(banksDir, "*.bin")) {
|
||||||
|
for (Path p : stream) bankFiles.add(p);
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new ModuleLoadException("Failed to list VRAM banks", e);
|
||||||
|
}
|
||||||
|
if (!bankFiles.isEmpty()) {
|
||||||
|
try {
|
||||||
|
byte[] data = Files.readAllBytes(bankFiles.get(0));
|
||||||
|
System.arraycopy(data, 0, vram[0], 0, Math.min(data.length, vramSize));
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new ModuleLoadException("Failed to read VRAM", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Path microcodeDir = dir.resolve("microcode");
|
||||||
|
Map<String, byte[]> microcodes = new HashMap<String, byte[]>();
|
||||||
|
if (Files.isDirectory(microcodeDir)) {
|
||||||
|
loadMicrocodes(microcodeDir, microcodes);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new GpuModuleInstance(metadata, vram, microcodes, rows, cols);
|
||||||
|
}
|
||||||
|
|
||||||
|
private ModuleInstance loadKbd(Path dir, ModuleMetadata metadata, Map<String, Object> config) throws ModuleLoadException {
|
||||||
|
int bufferSize = getInt(config, "buffer_size", 16);
|
||||||
|
return new KbdModuleInstance(metadata, bufferSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
private ModuleInstance loadSnd(Path dir, ModuleMetadata metadata, Map<String, Object> config) throws ModuleLoadException {
|
||||||
|
return new SndModuleInstance(metadata);
|
||||||
|
}
|
||||||
|
|
||||||
|
private ModuleInstance loadBios(Path dir, ModuleMetadata metadata, Map<String, Object> config) throws ModuleLoadException {
|
||||||
|
return new BiosModuleInstance(metadata);
|
||||||
|
}
|
||||||
|
|
||||||
|
private ModuleInstance loadDisk(Path dir, ModuleMetadata metadata, Map<String, Object> config) throws ModuleLoadException {
|
||||||
|
int sectorSize = getInt(config, "sector_size", 512);
|
||||||
|
List<byte[]> banks = new ArrayList<byte[]>();
|
||||||
|
Path banksDir = dir.resolve("banks");
|
||||||
|
if (Files.isDirectory(banksDir)) {
|
||||||
|
loadRoms(banksDir, banks);
|
||||||
|
}
|
||||||
|
if (banks.isEmpty()) {
|
||||||
|
banks.add(new byte[sectorSize]);
|
||||||
|
}
|
||||||
|
return new DiskModuleInstance(metadata, banks.get(0));
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
private Map<String, Integer> parseRegisters(Path file) throws ModuleLoadException {
|
||||||
|
Map<String, Object> json = readJson(file);
|
||||||
|
Map<String, Integer> registers = new LinkedHashMap<String, Integer>();
|
||||||
|
for (Map.Entry<String, Object> entry : json.entrySet()) {
|
||||||
|
if (entry.getValue() instanceof Number) {
|
||||||
|
registers.put(entry.getKey(), ((Number) entry.getValue()).intValue());
|
||||||
|
} else {
|
||||||
|
registers.put(entry.getKey(), 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return registers;
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
private Instruction parseInstruction(Path file) throws ModuleLoadException {
|
||||||
|
Map<String, Object> json = readJson(file);
|
||||||
|
int opcode = getInt(json, "opcode", -1);
|
||||||
|
if (opcode < 0) {
|
||||||
|
throw new ModuleLoadException("Instruction missing opcode: " + file);
|
||||||
|
}
|
||||||
|
String mnemonic = getString(json, "mnemonic", "UNK");
|
||||||
|
|
||||||
|
List<Map<String, Object>> argsRaw = (List<Map<String, Object>>) json.get("args");
|
||||||
|
List<Instruction.Arg> args = new ArrayList<Instruction.Arg>();
|
||||||
|
if (argsRaw != null) {
|
||||||
|
for (Map<String, Object> a : argsRaw) {
|
||||||
|
args.add(new Instruction.Arg(
|
||||||
|
getString(a, "name", "?"),
|
||||||
|
getString(a, "type", "reg")
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int cycles = getInt(json, "cycles", 1);
|
||||||
|
|
||||||
|
List<Map<String, Object>> semRaw = (List<Map<String, Object>>) json.get("semantics");
|
||||||
|
List<Instruction.SemanticOp> semantics = new ArrayList<Instruction.SemanticOp>();
|
||||||
|
if (semRaw != null) {
|
||||||
|
for (Map<String, Object> s : semRaw) {
|
||||||
|
semantics.add(new Instruction.SemanticOp(
|
||||||
|
getString(s, "op", null),
|
||||||
|
getString(s, "from", null),
|
||||||
|
getString(s, "to", null),
|
||||||
|
getString(s, "value", null),
|
||||||
|
getString(s, "condition", null),
|
||||||
|
getString(s, "result", null)
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Instruction(opcode, mnemonic, args, cycles, semantics);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void loadMicrocodes(Path dir, Map<String, byte[]> microcodes) throws ModuleLoadException {
|
||||||
|
Path busFile = dir.resolve("bus.json");
|
||||||
|
if (Files.exists(busFile)) {
|
||||||
|
Map<String, Object> json = readJson(busFile);
|
||||||
|
for (Map.Entry<String, Object> entry : json.entrySet()) {
|
||||||
|
if (entry.getValue() instanceof String) {
|
||||||
|
microcodes.put(entry.getKey(), ((String) entry.getValue()).getBytes(StandardCharsets.UTF_8));
|
||||||
|
} else if (entry.getValue() instanceof List) {
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
List<Object> list = (List<Object>) entry.getValue();
|
||||||
|
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||||
|
for (Object v : list) {
|
||||||
|
if (v instanceof Number) {
|
||||||
|
baos.write(((Number) v).byteValue());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
microcodes.put(entry.getKey(), baos.toByteArray());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void loadRoms(Path dir, List<byte[]> romBanks) throws ModuleLoadException {
|
||||||
|
try {
|
||||||
|
List<Path> romFiles = new ArrayList<Path>();
|
||||||
|
try (DirectoryStream<Path> stream = Files.newDirectoryStream(dir, "*.bin")) {
|
||||||
|
for (Path p : stream) romFiles.add(p);
|
||||||
|
}
|
||||||
|
Collections.sort(romFiles);
|
||||||
|
for (Path f : romFiles) {
|
||||||
|
romBanks.add(Files.readAllBytes(f));
|
||||||
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new ModuleLoadException("Failed to load ROMs", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<Path> listJsonFiles(Path dir) throws ModuleLoadException {
|
||||||
|
List<Path> files = new ArrayList<Path>();
|
||||||
|
try (DirectoryStream<Path> stream = Files.newDirectoryStream(dir, "*.json")) {
|
||||||
|
for (Path p : stream) files.add(p);
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new ModuleLoadException("Failed to list JSON files in " + dir, e);
|
||||||
|
}
|
||||||
|
Collections.sort(files);
|
||||||
|
return files;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map<String, Object> readJson(Path file) throws ModuleLoadException {
|
||||||
|
try {
|
||||||
|
String content = new String(Files.readAllBytes(file), StandardCharsets.UTF_8);
|
||||||
|
return JsonUtils.parseObject(content);
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new ModuleLoadException("Failed to read " + file, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getString(Map<String, Object> map, String key, String def) {
|
||||||
|
Object v = map.get(key);
|
||||||
|
return v != null ? v.toString() : def;
|
||||||
|
}
|
||||||
|
|
||||||
|
private int getInt(Map<String, Object> map, String key, int def) {
|
||||||
|
Object v = map.get(key);
|
||||||
|
if (v instanceof Number) return ((Number) v).intValue();
|
||||||
|
if (v != null) {
|
||||||
|
try { return Integer.parseInt(v.toString()); } catch (NumberFormatException e) {}
|
||||||
|
}
|
||||||
|
return def;
|
||||||
|
}
|
||||||
|
|
||||||
|
private float getFloat(Map<String, Object> map, String key, float def) {
|
||||||
|
Object v = map.get(key);
|
||||||
|
if (v instanceof Number) return ((Number) v).floatValue();
|
||||||
|
return def;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----- CPU Module Instance -----
|
||||||
|
|
||||||
|
private static class CpuModuleInstance extends AbstractModuleInstance {
|
||||||
|
private final Map<String, Integer> regDefaults;
|
||||||
|
private final Map<Integer, Instruction> instructions;
|
||||||
|
private final InstructionExecutor executor;
|
||||||
|
private final Map<String, byte[]> microcodes;
|
||||||
|
private final int memorySize;
|
||||||
|
private SimpleRegisters registers;
|
||||||
|
private byte[] memory;
|
||||||
|
|
||||||
|
CpuModuleInstance(ModuleMetadata metadata, Map<String, Integer> regDefaults,
|
||||||
|
Map<Integer, Instruction> instructions,
|
||||||
|
Map<String, byte[]> microcodes,
|
||||||
|
List<byte[]> romBanks, int memorySize) {
|
||||||
|
super(metadata, 0, 0);
|
||||||
|
this.regDefaults = regDefaults;
|
||||||
|
this.instructions = instructions;
|
||||||
|
this.executor = new InstructionExecutor(instructions);
|
||||||
|
this.microcodes = microcodes;
|
||||||
|
this.memorySize = memorySize;
|
||||||
|
this.memory = new byte[memorySize];
|
||||||
|
this.registers = new SimpleRegisters(regDefaults);
|
||||||
|
|
||||||
|
// Load ROM banks into memory
|
||||||
|
int offset = 0;
|
||||||
|
for (byte[] rom : romBanks) {
|
||||||
|
int len = Math.min(rom.length, memory.length - offset);
|
||||||
|
System.arraycopy(rom, 0, memory, offset, len);
|
||||||
|
offset += len;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void init(Bus bus) {
|
||||||
|
this.registers = new SimpleRegisters(regDefaults);
|
||||||
|
this.memory = new byte[memorySize];
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void reset() {
|
||||||
|
registers.reset();
|
||||||
|
memory = new byte[memorySize];
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void destroy() {
|
||||||
|
memory = null;
|
||||||
|
registers = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public OpcodeResult executeOpcode(int opcode, Registers regs, Bus bus) {
|
||||||
|
if (opcode == 0xFF) {
|
||||||
|
return OpcodeResult.halt(1);
|
||||||
|
}
|
||||||
|
return executor.execute(opcode, regs, bus, memory);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public byte[] readData(int bank, int offset, int size) {
|
||||||
|
if (bank != 0) return new byte[size];
|
||||||
|
int actualSize = Math.min(size, memory.length - offset);
|
||||||
|
if (actualSize <= 0) return new byte[size];
|
||||||
|
return Arrays.copyOfRange(memory, offset, offset + actualSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void writeData(int bank, int offset, byte[] data) {
|
||||||
|
if (bank != 0) return;
|
||||||
|
int actualSize = Math.min(data.length, memory.length - offset);
|
||||||
|
if (actualSize <= 0) return;
|
||||||
|
System.arraycopy(data, 0, memory, offset, actualSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public byte[] getMicrocode(String command) {
|
||||||
|
return microcodes.get(command);
|
||||||
|
}
|
||||||
|
|
||||||
|
SimpleRegisters getRegisters() {
|
||||||
|
return registers;
|
||||||
|
}
|
||||||
|
|
||||||
|
byte[] getMemory() {
|
||||||
|
return memory;
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<Integer, Instruction> getInstructions() {
|
||||||
|
return instructions;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----- RAM Module Instance -----
|
||||||
|
|
||||||
|
private static class RamModuleInstance extends AbstractModuleInstance {
|
||||||
|
private final Map<String, byte[]> microcodes;
|
||||||
|
|
||||||
|
RamModuleInstance(ModuleMetadata metadata, byte[][] banks, Map<String, byte[]> microcodes) {
|
||||||
|
super(metadata, banks);
|
||||||
|
this.microcodes = microcodes;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public OpcodeResult executeOpcode(int opcode, Registers regs, Bus bus) {
|
||||||
|
return OpcodeResult.ok(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public byte[] getMicrocode(String command) {
|
||||||
|
byte[] mc = microcodes.get(command);
|
||||||
|
return mc != null ? mc.clone() : new byte[0];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----- GPU Module Instance -----
|
||||||
|
|
||||||
|
public static class GpuModuleInstance extends AbstractModuleInstance {
|
||||||
|
private final Map<String, byte[]> microcodes;
|
||||||
|
private final int rows;
|
||||||
|
private final int cols;
|
||||||
|
private int cursorX;
|
||||||
|
private int cursorY;
|
||||||
|
private boolean needsRedraw;
|
||||||
|
private long lastRedrawCheck;
|
||||||
|
|
||||||
|
GpuModuleInstance(ModuleMetadata metadata, byte[][] vram, Map<String, byte[]> microcodes,
|
||||||
|
int rows, int cols) {
|
||||||
|
super(metadata, vram);
|
||||||
|
this.microcodes = microcodes;
|
||||||
|
this.rows = rows;
|
||||||
|
this.cols = cols;
|
||||||
|
this.cursorX = 0;
|
||||||
|
this.cursorY = 0;
|
||||||
|
this.needsRedraw = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public OpcodeResult executeOpcode(int opcode, Registers regs, Bus bus) {
|
||||||
|
return OpcodeResult.ok(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public byte[] getMicrocode(String command) {
|
||||||
|
byte[] mc = microcodes.get(command);
|
||||||
|
return mc != null ? mc.clone() : new byte[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[] getVram() {
|
||||||
|
return dataBanks.length > 0 ? dataBanks[0] : new byte[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getRows() {
|
||||||
|
return rows;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getCols() {
|
||||||
|
return cols;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getCursorX() {
|
||||||
|
return cursorX;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getCursorY() {
|
||||||
|
return cursorY;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isDirty() {
|
||||||
|
return needsRedraw;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void markClean() {
|
||||||
|
needsRedraw = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void writeChar(char c) {
|
||||||
|
if (dataBanks.length == 0) return;
|
||||||
|
byte[] vram = dataBanks[0];
|
||||||
|
int offset = cursorY * cols + cursorX;
|
||||||
|
if (offset >= vram.length) return;
|
||||||
|
vram[offset] = (byte) (c & 0x7F);
|
||||||
|
cursorX++;
|
||||||
|
if (cursorX >= cols) {
|
||||||
|
cursorX = 0;
|
||||||
|
cursorY++;
|
||||||
|
if (cursorY >= rows) {
|
||||||
|
scroll();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
needsRedraw = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void writeString(String s) {
|
||||||
|
for (int i = 0; i < s.length(); i++) {
|
||||||
|
char c = s.charAt(i);
|
||||||
|
if (c == '\n') {
|
||||||
|
cursorX = 0;
|
||||||
|
cursorY++;
|
||||||
|
if (cursorY >= rows) scroll();
|
||||||
|
} else {
|
||||||
|
writeChar(c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
needsRedraw = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void clear() {
|
||||||
|
if (dataBanks.length == 0) return;
|
||||||
|
byte[] vram = dataBanks[0];
|
||||||
|
for (int i = 0; i < vram.length; i++) vram[i] = 0;
|
||||||
|
cursorX = 0;
|
||||||
|
cursorY = 0;
|
||||||
|
needsRedraw = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setCursor(int x, int y) {
|
||||||
|
this.cursorX = Math.max(0, Math.min(cols - 1, x));
|
||||||
|
this.cursorY = Math.max(0, Math.min(rows - 1, y));
|
||||||
|
needsRedraw = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void scroll() {
|
||||||
|
if (dataBanks.length == 0) return;
|
||||||
|
byte[] vram = dataBanks[0];
|
||||||
|
for (int row = 0; row < rows - 1; row++) {
|
||||||
|
for (int col = 0; col < cols; col++) {
|
||||||
|
vram[row * cols + col] = vram[(row + 1) * cols + col];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (int col = 0; col < cols; col++) {
|
||||||
|
vram[(rows - 1) * cols + col] = 0;
|
||||||
|
}
|
||||||
|
cursorY = rows - 1;
|
||||||
|
needsRedraw = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String readString() {
|
||||||
|
if (dataBanks.length == 0) return "";
|
||||||
|
byte[] vram = dataBanks[0];
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
for (int row = 0; row < rows; row++) {
|
||||||
|
for (int col = 0; col < cols; col++) {
|
||||||
|
int b = vram[row * cols + col] & 0xFF;
|
||||||
|
if (b == 0) {
|
||||||
|
sb.append(' ');
|
||||||
|
} else {
|
||||||
|
sb.append((char) (b & 0x7F));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (row < rows - 1) sb.append('\n');
|
||||||
|
}
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========================================================================
|
||||||
|
// KBD Module - keyboard input device
|
||||||
|
// Memory-mapped at PostCode.KBD_DATA_ADDR / KBD_STATUS_ADDR
|
||||||
|
// ========================================================================
|
||||||
|
public static class KbdModuleInstance extends AbstractModuleInstance {
|
||||||
|
private final java.util.concurrent.LinkedBlockingQueue<Integer> keyBuffer;
|
||||||
|
private final int bufferSize;
|
||||||
|
|
||||||
|
KbdModuleInstance(ModuleMetadata metadata, int bufferSize) {
|
||||||
|
super(metadata);
|
||||||
|
this.bufferSize = bufferSize;
|
||||||
|
this.keyBuffer = new java.util.concurrent.LinkedBlockingQueue<Integer>(bufferSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public OpcodeResult executeOpcode(int opcode, Registers regs, Bus bus) {
|
||||||
|
return OpcodeResult.ok(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public byte[] readData(int bank, int offset, int size) {
|
||||||
|
byte[] result = new byte[size];
|
||||||
|
if (offset == com.cbe.core.PostCode.KBD_DATA_ADDR) {
|
||||||
|
// Peek: don't remove. CPU consumes by writing 0 to KBD_STATUS_ADDR
|
||||||
|
// (a simple "ack" handshake) or by reading from an internal pointer.
|
||||||
|
Integer k = keyBuffer.peek();
|
||||||
|
result[0] = (byte) (k != null ? (k & 0xFF) : 0);
|
||||||
|
} else if (offset == com.cbe.core.PostCode.KBD_STATUS_ADDR) {
|
||||||
|
result[0] = (byte) (keyBuffer.isEmpty() ? 0 : 1);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void writeData(int bank, int offset, byte[] data) {
|
||||||
|
// Writing 0 to KBD_STATUS_ADDR acknowledges the key (consume it).
|
||||||
|
if (offset == com.cbe.core.PostCode.KBD_STATUS_ADDR && data != null && data.length > 0
|
||||||
|
&& (data[0] & 0xFF) == 0) {
|
||||||
|
keyBuffer.poll();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public byte[] getMicrocode(String command) { return new byte[0]; }
|
||||||
|
|
||||||
|
public void pushKey(int keyCode) {
|
||||||
|
if (keyBuffer.remainingCapacity() == 0) keyBuffer.poll();
|
||||||
|
keyBuffer.offer(keyCode & 0xFF);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void clear() { keyBuffer.clear(); }
|
||||||
|
|
||||||
|
public int bufferSize() { return bufferSize; }
|
||||||
|
public int available() { return keyBuffer.size(); }
|
||||||
|
|
||||||
|
/** Consume the next key (called by the Engine when the CPU writes 0 to KBD_STATUS_ADDR). */
|
||||||
|
public void acknowledge() { keyBuffer.poll(); }
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========================================================================
|
||||||
|
// SND Module - sound/beep output device
|
||||||
|
// Writing to PostCode.SND_BEEP_ADDR triggers a system beep
|
||||||
|
// ========================================================================
|
||||||
|
public static class SndModuleInstance extends AbstractModuleInstance {
|
||||||
|
private long lastBeepNs = 0;
|
||||||
|
private int beepCount = 0;
|
||||||
|
|
||||||
|
SndModuleInstance(ModuleMetadata metadata) {
|
||||||
|
super(metadata);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public OpcodeResult executeOpcode(int opcode, Registers regs, Bus bus) {
|
||||||
|
return OpcodeResult.ok(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public byte[] getMicrocode(String command) { return new byte[0]; }
|
||||||
|
|
||||||
|
public void beep() {
|
||||||
|
long now = System.nanoTime();
|
||||||
|
if (now - lastBeepNs < 100_000_000L) return;
|
||||||
|
lastBeepNs = now;
|
||||||
|
beepCount++;
|
||||||
|
AudioBridge.beep();
|
||||||
|
}
|
||||||
|
|
||||||
|
public int beepCount() { return beepCount; }
|
||||||
|
public void resetCount() { beepCount = 0; }
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========================================================================
|
||||||
|
// BIOS Module - placeholder for boot ROM / POST sequence
|
||||||
|
// ========================================================================
|
||||||
|
public static class DiskModuleInstance extends AbstractModuleInstance {
|
||||||
|
private final byte[] diskData;
|
||||||
|
private static final int SECTOR_SIZE = 512;
|
||||||
|
|
||||||
|
DiskModuleInstance(ModuleMetadata metadata, byte[] diskData) {
|
||||||
|
super(metadata, new byte[][]{diskData});
|
||||||
|
this.diskData = diskData;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getSectorSize() { return SECTOR_SIZE; }
|
||||||
|
public int getSectorCount() { return diskData.length / SECTOR_SIZE; }
|
||||||
|
public byte[] getDiskData() { return diskData; }
|
||||||
|
|
||||||
|
/** Read one sector (0-indexed LBA). Returns sector bytes or 0-filled if out of range. */
|
||||||
|
public byte[] readSector(int lba) {
|
||||||
|
int off = lba * SECTOR_SIZE;
|
||||||
|
if (lba < 0 || off >= diskData.length) return new byte[SECTOR_SIZE];
|
||||||
|
return java.util.Arrays.copyOfRange(diskData, off, Math.min(off + SECTOR_SIZE, diskData.length));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public OpcodeResult executeOpcode(int opcode, Registers regs, Bus bus) {
|
||||||
|
return OpcodeResult.ok(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public byte[] getMicrocode(String command) { return new byte[0]; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class BiosModuleInstance extends AbstractModuleInstance {
|
||||||
|
private String systemInfo = "CBE BIOS v0.1 (no scan yet)";
|
||||||
|
|
||||||
|
BiosModuleInstance(ModuleMetadata metadata) {
|
||||||
|
super(metadata);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public OpcodeResult executeOpcode(int opcode, Registers regs, Bus bus) {
|
||||||
|
return OpcodeResult.ok(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public byte[] getMicrocode(String command) { return new byte[0]; }
|
||||||
|
|
||||||
|
public void setSystemInfo(String info) { this.systemInfo = info; }
|
||||||
|
public String getSystemInfo() { return systemInfo; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
|
||||||
|
android.useAndroidX=true
|
||||||
|
kotlin.code.style=official
|
||||||
|
android.nonTransitiveRClass=true
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
[versions]
|
||||||
|
agp = "8.2.2"
|
||||||
|
kotlin = "1.9.23"
|
||||||
|
coreKtx = "1.13.1"
|
||||||
|
lifecycleRuntimeKtx = "2.8.3"
|
||||||
|
activityCompose = "1.9.0"
|
||||||
|
composeBom = "2024.06.00"
|
||||||
|
composeCompiler = "1.5.11"
|
||||||
|
|
||||||
|
[libraries]
|
||||||
|
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
|
||||||
|
androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycleRuntimeKtx" }
|
||||||
|
androidx-lifecycle-viewmodel-compose = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-compose", version.ref = "lifecycleRuntimeKtx" }
|
||||||
|
androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activityCompose" }
|
||||||
|
androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" }
|
||||||
|
androidx-compose-ui = { group = "androidx.compose.ui", name = "ui" }
|
||||||
|
androidx-compose-ui-graphics = { group = "androidx.compose.ui", name = "ui-graphics" }
|
||||||
|
androidx-compose-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" }
|
||||||
|
androidx-compose-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" }
|
||||||
|
androidx-compose-material3 = { group = "androidx.compose.material3", name = "material3" }
|
||||||
|
androidx-compose-foundation = { group = "androidx.compose.foundation", name = "foundation" }
|
||||||
|
androidx-compose-material-icons-core = { group = "androidx.compose.material", name = "material-icons-core" }
|
||||||
|
|
||||||
|
[plugins]
|
||||||
|
android-application = { id = "com.android.application", version.ref = "agp" }
|
||||||
|
android-library = { id = "com.android.library", version.ref = "agp" }
|
||||||
|
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
distributionBase=GRADLE_USER_HOME
|
||||||
|
distributionPath=wrapper/dists
|
||||||
|
distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip
|
||||||
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
|
zipStorePath=wrapper/dists
|
||||||
+176
@@ -0,0 +1,176 @@
|
|||||||
|
#!/usr/bin/env sh
|
||||||
|
|
||||||
|
##############################################################################
|
||||||
|
##
|
||||||
|
## Gradle start up script for UN*X
|
||||||
|
##
|
||||||
|
##############################################################################
|
||||||
|
|
||||||
|
# Attempt to set APP_HOME
|
||||||
|
# Resolve links: $0 may be a link
|
||||||
|
PRG="$0"
|
||||||
|
# Need this for relative symlinks.
|
||||||
|
while [ -h "$PRG" ] ; do
|
||||||
|
ls=`ls -ld "$PRG"`
|
||||||
|
link=`expr "$ls" : '.*-> \(.*\)$'`
|
||||||
|
if expr "$link" : '/.*' > /dev/null; then
|
||||||
|
PRG="$link"
|
||||||
|
else
|
||||||
|
PRG=`dirname "$PRG"`"/$link"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
SAVED="`pwd`"
|
||||||
|
cd "`dirname \"$PRG\"`/" >/dev/null
|
||||||
|
APP_HOME="`pwd -P`"
|
||||||
|
cd "$SAVED" >/dev/null
|
||||||
|
|
||||||
|
APP_NAME="Gradle"
|
||||||
|
APP_BASE_NAME=`basename "$0"`
|
||||||
|
|
||||||
|
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||||
|
DEFAULT_JVM_OPTS=""
|
||||||
|
|
||||||
|
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||||
|
MAX_FD="maximum"
|
||||||
|
|
||||||
|
warn () {
|
||||||
|
echo "$*"
|
||||||
|
}
|
||||||
|
|
||||||
|
die () {
|
||||||
|
echo
|
||||||
|
echo "$*"
|
||||||
|
echo
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# OS specific support (must be 'true' or 'false').
|
||||||
|
cygwin=false
|
||||||
|
msys=false
|
||||||
|
darwin=false
|
||||||
|
nonstop=false
|
||||||
|
case "`uname`" in
|
||||||
|
CYGWIN* )
|
||||||
|
cygwin=true
|
||||||
|
;;
|
||||||
|
Darwin* )
|
||||||
|
darwin=true
|
||||||
|
;;
|
||||||
|
MINGW* )
|
||||||
|
msys=true
|
||||||
|
;;
|
||||||
|
NONSTOP* )
|
||||||
|
nonstop=true
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
||||||
|
|
||||||
|
# Determine the Java command to use to start the JVM.
|
||||||
|
if [ -n "$JAVA_HOME" ] ; then
|
||||||
|
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
|
||||||
|
# IBM's JDK on AIX uses strange locations for the executables
|
||||||
|
JAVACMD="$JAVA_HOME/jre/sh/java"
|
||||||
|
else
|
||||||
|
JAVACMD="$JAVA_HOME/bin/java"
|
||||||
|
fi
|
||||||
|
if [ ! -x "$JAVACMD" ] ; then
|
||||||
|
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
|
||||||
|
|
||||||
|
Please set the JAVA_HOME variable in your environment to match the
|
||||||
|
location of your Java installation."
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
JAVACMD="java"
|
||||||
|
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||||
|
|
||||||
|
Please set the JAVA_HOME variable in your environment to match the
|
||||||
|
location of your Java installation."
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Increase the maximum file descriptors if we can.
|
||||||
|
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
|
||||||
|
MAX_FD_LIMIT=`ulimit -H -n`
|
||||||
|
if [ $? -eq 0 ] ; then
|
||||||
|
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
|
||||||
|
MAX_FD="$MAX_FD_LIMIT"
|
||||||
|
fi
|
||||||
|
ulimit -n $MAX_FD
|
||||||
|
if [ $? -ne 0 ] ; then
|
||||||
|
warn "Could not set maximum file descriptor limit: $MAX_FD"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# For Darwin, add options to specify how the application appears in the dock
|
||||||
|
if $darwin; then
|
||||||
|
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
|
||||||
|
fi
|
||||||
|
|
||||||
|
# For Cygwin, switch paths to Windows format before running java
|
||||||
|
if $cygwin ; then
|
||||||
|
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
|
||||||
|
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
|
||||||
|
JAVACMD=`cygpath --unix "$JAVACMD"`
|
||||||
|
|
||||||
|
# We build the pattern for arguments to be converted via cygpath
|
||||||
|
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
|
||||||
|
SEP=""
|
||||||
|
for dir in $ROOTDIRSRAW ; do
|
||||||
|
ROOTDIRS="$ROOTDIRS$SEP$dir"
|
||||||
|
SEP="|"
|
||||||
|
done
|
||||||
|
OURCYGPATTERN="(^($ROOTDIRS))"
|
||||||
|
# Add a user-defined pattern to the cygpath arguments
|
||||||
|
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
|
||||||
|
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
|
||||||
|
fi
|
||||||
|
# Now convert the arguments - kludge to limit ourselves to /bin/sh
|
||||||
|
i=0
|
||||||
|
for arg in "$@" ; do
|
||||||
|
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
|
||||||
|
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
|
||||||
|
|
||||||
|
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
|
||||||
|
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
|
||||||
|
else
|
||||||
|
eval `echo args$i`="\"$arg\""
|
||||||
|
fi
|
||||||
|
i=$((i+1))
|
||||||
|
done
|
||||||
|
case $i in
|
||||||
|
(0) set -- ;;
|
||||||
|
(1) set -- "$args0" ;;
|
||||||
|
(2) set -- "$args0" "$args1" ;;
|
||||||
|
(3) set -- "$args0" "$args1" "$args2" ;;
|
||||||
|
(4) set -- "$args0" "$args1" "$args2" "$args3" ;;
|
||||||
|
(5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
|
||||||
|
(6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
|
||||||
|
(7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
|
||||||
|
(8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
|
||||||
|
(9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
|
||||||
|
esac
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Escape application args
|
||||||
|
save () {
|
||||||
|
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
|
||||||
|
echo " "
|
||||||
|
}
|
||||||
|
APP_ARGS=$(save "$@")
|
||||||
|
|
||||||
|
# Collect all arguments for the java command, following the shell quoting and substitution rules
|
||||||
|
if $JAVACMD --add-opens java.base/java.lang=ALL-UNNAMED -version ; then
|
||||||
|
DEFAULT_JVM_OPTS="--add-opens java.base/java.lang=ALL-UNNAMED $DEFAULT_JVM_OPTS"
|
||||||
|
fi
|
||||||
|
|
||||||
|
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
|
||||||
|
|
||||||
|
# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
|
||||||
|
if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
|
||||||
|
cd "$(dirname "$0")"
|
||||||
|
fi
|
||||||
|
|
||||||
|
exec "$JAVACMD" "$@"
|
||||||
Vendored
+84
@@ -0,0 +1,84 @@
|
|||||||
|
@if "%DEBUG%" == "" @echo off
|
||||||
|
@rem ##########################################################################
|
||||||
|
@rem
|
||||||
|
@rem Gradle startup script for Windows
|
||||||
|
@rem
|
||||||
|
@rem ##########################################################################
|
||||||
|
|
||||||
|
@rem Set local scope for the variables with windows NT shell
|
||||||
|
if "%OS%"=="Windows_NT" setlocal
|
||||||
|
|
||||||
|
set DIRNAME=%~dp0
|
||||||
|
if "%DIRNAME%" == "" set DIRNAME=.
|
||||||
|
set APP_BASE_NAME=%~n0
|
||||||
|
set APP_HOME=%DIRNAME%
|
||||||
|
|
||||||
|
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||||
|
set DEFAULT_JVM_OPTS=
|
||||||
|
|
||||||
|
@rem Find java.exe
|
||||||
|
if defined JAVA_HOME goto findJavaFromJavaHome
|
||||||
|
|
||||||
|
set JAVA_EXE=java.exe
|
||||||
|
%JAVA_EXE% -version >NUL 2>&1
|
||||||
|
if "%ERRORLEVEL%" == "0" goto init
|
||||||
|
|
||||||
|
echo.
|
||||||
|
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||||
|
echo.
|
||||||
|
echo Please set the JAVA_HOME variable in your environment to match the
|
||||||
|
echo location of your Java installation.
|
||||||
|
|
||||||
|
goto fail
|
||||||
|
|
||||||
|
:findJavaFromJavaHome
|
||||||
|
set JAVA_HOME=%JAVA_HOME:"=%
|
||||||
|
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||||
|
|
||||||
|
if exist "%JAVA_EXE%" goto init
|
||||||
|
|
||||||
|
echo.
|
||||||
|
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
|
||||||
|
echo.
|
||||||
|
echo Please set the JAVA_HOME variable in your environment to match the
|
||||||
|
echo location of your Java installation.
|
||||||
|
|
||||||
|
goto fail
|
||||||
|
|
||||||
|
:init
|
||||||
|
@rem Get command-line arguments, handling Windows variants
|
||||||
|
|
||||||
|
if not "%OS%" == "Windows_NT" goto win9xME_args
|
||||||
|
|
||||||
|
:win9xME_args
|
||||||
|
@rem Slurp the command line arguments.
|
||||||
|
set CMD_LINE_ARGS=
|
||||||
|
set _SKIP=2
|
||||||
|
|
||||||
|
:win9xME_args_slurp
|
||||||
|
if "x%~1" == "x" goto execute
|
||||||
|
|
||||||
|
set CMD_LINE_ARGS=%*
|
||||||
|
|
||||||
|
:execute
|
||||||
|
@rem Setup the command line
|
||||||
|
|
||||||
|
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
||||||
|
|
||||||
|
@rem Execute Gradle
|
||||||
|
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
|
||||||
|
|
||||||
|
:end
|
||||||
|
@rem End local scope for the variables with windows NT shell
|
||||||
|
if "%ERRORLEVEL%"=="0" goto mainEnd
|
||||||
|
|
||||||
|
:fail
|
||||||
|
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
||||||
|
rem the _cmd.exe /c_ return code!
|
||||||
|
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
|
||||||
|
exit /b 1
|
||||||
|
|
||||||
|
:mainEnd
|
||||||
|
if "%OS%"=="Windows_NT" endlocal
|
||||||
|
|
||||||
|
:omega
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
pluginManagement {
|
||||||
|
repositories {
|
||||||
|
google()
|
||||||
|
mavenCentral()
|
||||||
|
gradlePluginPortal()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencyResolutionManagement {
|
||||||
|
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
|
||||||
|
repositories {
|
||||||
|
google()
|
||||||
|
mavenCentral()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
rootProject.name = "CBE-Emulator"
|
||||||
|
include(":app")
|
||||||
|
include(":cbe-core")
|
||||||
Executable
+9
@@ -0,0 +1,9 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -e
|
||||||
|
cd "$(dirname "$0")/android"
|
||||||
|
export JAVA_HOME=/usr/lib/jvm/java-21-openjdk-amd64
|
||||||
|
export PATH=$JAVA_HOME/bin:$PATH
|
||||||
|
export ANDROID_HOME=/opt/android-sdk
|
||||||
|
./gradlew assembleDebug "$@"
|
||||||
|
cp app/build/outputs/apk/debug/app-debug.apk "$(dirname "$0")/build/cbe-emulator.apk"
|
||||||
|
echo "APK: build/cbe-emulator.apk ($(du -h app/build/outputs/apk/debug/app-debug.apk | cut -f1))"
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
@echo off
|
||||||
|
REM ===========================================================
|
||||||
|
REM CBE Emulator - Windows build script
|
||||||
|
REM Run this on Windows with JDK 14+ installed.
|
||||||
|
REM Produces: build\installer\CBE-Emulator.exe (or .msi)
|
||||||
|
REM ===========================================================
|
||||||
|
|
||||||
|
setlocal
|
||||||
|
cd /d "%~dp0"
|
||||||
|
|
||||||
|
echo.
|
||||||
|
echo === CBE Emulator - Windows build ===
|
||||||
|
echo.
|
||||||
|
|
||||||
|
REM 1. Build the project
|
||||||
|
echo [1/4] Building Gradle project...
|
||||||
|
call gradlew.bat :modules:gui:stage --no-daemon
|
||||||
|
if errorlevel 1 goto :err
|
||||||
|
|
||||||
|
REM 2. Detect Java
|
||||||
|
where jpackage >nul 2>nul
|
||||||
|
if errorlevel 1 (
|
||||||
|
echo jpackage not found in PATH. Please install JDK 14+.
|
||||||
|
exit /b 1
|
||||||
|
)
|
||||||
|
|
||||||
|
REM 3. Choose installer type (default: exe)
|
||||||
|
set PKG_TYPE=exe
|
||||||
|
if not "%~1"=="" set PKG_TYPE=%~1
|
||||||
|
|
||||||
|
REM 4. Run jpackage
|
||||||
|
echo.
|
||||||
|
echo [2/4] Packaging with jpackage (type=%PKG_TYPE%)...
|
||||||
|
if exist build\installer rmdir /s /q build\installer
|
||||||
|
jpackage ^
|
||||||
|
--input modules\gui\build\stage ^
|
||||||
|
--dest build\installer ^
|
||||||
|
--name CBE-Emulator ^
|
||||||
|
--main-jar cbe-emu.jar ^
|
||||||
|
--main-class com.cbe.gui.Main ^
|
||||||
|
--app-version 0.1.0 ^
|
||||||
|
--vendor "CBE Project" ^
|
||||||
|
--description "CBE Platform - Emulator" ^
|
||||||
|
--type %PKG_TYPE% ^
|
||||||
|
--java-options "-Xmx256m"
|
||||||
|
if errorlevel 1 goto :err
|
||||||
|
|
||||||
|
echo.
|
||||||
|
echo [3/4] Done!
|
||||||
|
echo Output: build\installer\
|
||||||
|
dir /b build\installer
|
||||||
|
|
||||||
|
REM 4. Stage plugins next to the .exe for convenience
|
||||||
|
echo.
|
||||||
|
echo [4/4] Copying example plugins next to launcher...
|
||||||
|
if not exist build\plugins mkdir build\plugins
|
||||||
|
copy examples\tiny-cpu.cpu\roms\boot.bin build\plugins\ >nul
|
||||||
|
copy examples\basic-ram.ram\*.* build\plugins\ >nul
|
||||||
|
copy examples\vga-display.gpu\*.* build\plugins\ >nul
|
||||||
|
|
||||||
|
echo.
|
||||||
|
echo Build complete. Find your installer at:
|
||||||
|
echo build\installer\
|
||||||
|
echo.
|
||||||
|
exit /b 0
|
||||||
|
|
||||||
|
:err
|
||||||
|
echo.
|
||||||
|
echo BUILD FAILED.
|
||||||
|
exit /b 1
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
# ============================================================
|
||||||
|
# CBE Emulator - Windows build script (PowerShell)
|
||||||
|
# Run on Windows with JDK 14+ installed.
|
||||||
|
# Usage:
|
||||||
|
# .\build-windows.ps1 # builds app-image (folder with .exe)
|
||||||
|
# .\build-windows.ps1 -Type msi # builds MSI installer
|
||||||
|
# .\build-windows.ps1 -Type exe # builds EXE installer
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
param(
|
||||||
|
[ValidateSet('app-image', 'msi', 'exe')]
|
||||||
|
[string]$Type = 'app-image'
|
||||||
|
)
|
||||||
|
|
||||||
|
$ErrorActionPreference = 'Stop'
|
||||||
|
Set-Location $PSScriptRoot
|
||||||
|
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host "=== CBE Emulator - Windows build ===" -ForegroundColor Cyan
|
||||||
|
Write-Host ""
|
||||||
|
|
||||||
|
# 1. Build
|
||||||
|
Write-Host "[1/4] Building Gradle project..." -ForegroundColor Yellow
|
||||||
|
& .\gradlew.bat :modules:gui:stage --no-daemon
|
||||||
|
if ($LASTEXITCODE -ne 0) { Write-Error "Gradle build failed"; exit 1 }
|
||||||
|
|
||||||
|
# 2. Detect jpackage
|
||||||
|
$jpackage = (Get-Command jpackage -ErrorAction SilentlyContinue)
|
||||||
|
if (-not $jpackage) {
|
||||||
|
Write-Error "jpackage not found in PATH. Please install JDK 14+."
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# 3. jpackage
|
||||||
|
$stageDir = "modules\gui\build\stage"
|
||||||
|
$outDir = "build\installer"
|
||||||
|
|
||||||
|
if (Test-Path $outDir) { Remove-Item -Recurse -Force $outDir }
|
||||||
|
New-Item -ItemType Directory -Path $outDir | Out-Null
|
||||||
|
|
||||||
|
Write-Host "[2/4] Running jpackage (type=$Type)..." -ForegroundColor Yellow
|
||||||
|
& jpackage `
|
||||||
|
--input $stageDir `
|
||||||
|
--dest $outDir `
|
||||||
|
--name CBE-Emulator `
|
||||||
|
--main-jar cbe-emu.jar `
|
||||||
|
--main-class com.cbe.gui.Main `
|
||||||
|
--app-version 0.1.0 `
|
||||||
|
--vendor "CBE Project" `
|
||||||
|
--description "CBE Platform - Emulator" `
|
||||||
|
--type $Type `
|
||||||
|
--java-options "-Xmx256m"
|
||||||
|
if ($LASTEXITCODE -ne 0) { Write-Error "jpackage failed"; exit 1 }
|
||||||
|
|
||||||
|
Write-Host "[3/4] Output:" -ForegroundColor Green
|
||||||
|
Get-ChildItem $outDir | ForEach-Object { Write-Host " $($_.FullName)" }
|
||||||
|
|
||||||
|
# 4. Stage example plugins
|
||||||
|
$pluginsDir = "build\plugins"
|
||||||
|
if (-not (Test-Path $pluginsDir)) { New-Item -ItemType Directory -Path $pluginsDir | Out-Null }
|
||||||
|
Copy-Item "examples\tiny-cpu.cpu\*" -Recurse -Force -Destination $pluginsDir
|
||||||
|
Copy-Item "examples\basic-ram.ram\*" -Recurse -Force -Destination $pluginsDir
|
||||||
|
Copy-Item "examples\vga-display.gpu\*" -Recurse -Force -Destination $pluginsDir
|
||||||
|
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host "[4/4] Done." -ForegroundColor Green
|
||||||
|
Write-Host "Installer/output: $outDir" -ForegroundColor Green
|
||||||
|
Write-Host "Example plugins: $pluginsDir" -ForegroundColor Green
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host "For app-image type, the launcher is at:" -ForegroundColor Cyan
|
||||||
|
Write-Host " $outDir\CBE-Emulator\CBE-Emulator.exe" -ForegroundColor White
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
plugins {
|
||||||
|
id 'java'
|
||||||
|
}
|
||||||
|
|
||||||
|
allprojects {
|
||||||
|
group = 'com.cbe'
|
||||||
|
version = '0.1.0'
|
||||||
|
|
||||||
|
repositories {
|
||||||
|
mavenCentral()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
subprojects {
|
||||||
|
apply plugin: 'java'
|
||||||
|
|
||||||
|
sourceCompatibility = 1.8
|
||||||
|
targetCompatibility = 1.8
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
compileOnly 'com.google.code.findbugs:jsr305:3.0.2'
|
||||||
|
testImplementation 'junit:junit:4.13.2'
|
||||||
|
}
|
||||||
|
}
|
||||||
+27
@@ -0,0 +1,27 @@
|
|||||||
|
@echo off
|
||||||
|
REM ===========================================================
|
||||||
|
REM CBE Emulator launcher
|
||||||
|
REM Double-click to run with embedded plugins.
|
||||||
|
REM No arguments needed - all plugins are inside cbe-emu.jar
|
||||||
|
REM ===========================================================
|
||||||
|
|
||||||
|
setlocal
|
||||||
|
cd /d "%~dp0"
|
||||||
|
|
||||||
|
set "JAVA_EXE=%~dp0runtime\bin\javaw.exe"
|
||||||
|
set "JAR=%~dp0app\cbe-emu.jar"
|
||||||
|
|
||||||
|
if not exist "%JAVA_EXE%" (
|
||||||
|
echo [ERROR] JRE not found at %JAVA_EXE%
|
||||||
|
pause
|
||||||
|
exit /b 1
|
||||||
|
)
|
||||||
|
|
||||||
|
if not exist "%JAR%" (
|
||||||
|
echo [ERROR] cbe-emu.jar not found at %JAR%
|
||||||
|
pause
|
||||||
|
exit /b 1
|
||||||
|
)
|
||||||
|
|
||||||
|
REM Launch GUI. Use %* to forward any extra args.
|
||||||
|
"%JAVA_EXE%" -Xmx256m -jar "%JAR%" %*
|
||||||
+40
@@ -0,0 +1,40 @@
|
|||||||
|
' ===========================================================
|
||||||
|
' CBE Emulator launcher (VBScript - no console window)
|
||||||
|
' Double-click to run with embedded plugins, silently.
|
||||||
|
' ===========================================================
|
||||||
|
|
||||||
|
Option Explicit
|
||||||
|
|
||||||
|
Dim shell, fso, javaExe, jar, baseDir, cmd
|
||||||
|
|
||||||
|
Set shell = CreateObject("WScript.Shell")
|
||||||
|
Set fso = CreateObject("Scripting.FileSystemObject")
|
||||||
|
|
||||||
|
baseDir = fso.GetParentFolderName(WScript.ScriptFullName)
|
||||||
|
javaExe = baseDir & "\runtime\bin\javaw.exe"
|
||||||
|
jar = baseDir & "\app\cbe-emu.jar"
|
||||||
|
|
||||||
|
If Not fso.FileExists(javaExe) Then
|
||||||
|
MsgBox "JRE not found at:" & vbCrLf & javaExe, 16, "CBE Emulator"
|
||||||
|
WScript.Quit 1
|
||||||
|
End If
|
||||||
|
|
||||||
|
If Not fso.FileExists(jar) Then
|
||||||
|
MsgBox "cbe-emu.jar not found at:" & vbCrLf & jar, 16, "CBE Emulator"
|
||||||
|
WScript.Quit 1
|
||||||
|
End If
|
||||||
|
|
||||||
|
' Collect extra args after the .vbs filename
|
||||||
|
Dim extra
|
||||||
|
extra = ""
|
||||||
|
Dim i
|
||||||
|
For i = 0 To WScript.Arguments.Count - 1
|
||||||
|
extra = extra & " """ & WScript.Arguments(i) & """"
|
||||||
|
Next
|
||||||
|
|
||||||
|
' Build command. 0 = hide window, False = don't wait for return
|
||||||
|
cmd = """" & javaExe & """ -Xmx256m -jar """ & jar & """ " & extra
|
||||||
|
shell.Run cmd, 0, False
|
||||||
|
|
||||||
|
Set shell = Nothing
|
||||||
|
Set fso = Nothing
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"name": "boot-disk",
|
||||||
|
"arch": "tiny-8bit",
|
||||||
|
"module_type": "disk",
|
||||||
|
"version": 1,
|
||||||
|
"sector_size": 512,
|
||||||
|
"description": "Bootable disk with sector 0 bootloader"
|
||||||
|
}
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"opcode": 0,
|
||||||
|
"mnemonic": "NOP",
|
||||||
|
"args": [],
|
||||||
|
"cycles": 1,
|
||||||
|
"semantics": [
|
||||||
|
{"op": "nop"}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"opcode": 1,
|
||||||
|
"mnemonic": "MOV_A_B",
|
||||||
|
"cycles": 1,
|
||||||
|
"semantics": [
|
||||||
|
{"op": "copy", "from": "b", "to": "a"}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"opcode": 2,
|
||||||
|
"mnemonic": "MOV_IMM_A",
|
||||||
|
"cycles": 1,
|
||||||
|
"semantics": [
|
||||||
|
{"op": "load_imm", "to": "a", "value": "$next"}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"opcode": 3,
|
||||||
|
"mnemonic": "ADD_A_B",
|
||||||
|
"cycles": 2,
|
||||||
|
"semantics": [
|
||||||
|
{"op": "add", "from": "b", "to": "a", "result": "a"}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"opcode": 4,
|
||||||
|
"mnemonic": "SUB_A_B",
|
||||||
|
"cycles": 2,
|
||||||
|
"semantics": [
|
||||||
|
{"op": "sub", "from": "a", "to": "b", "result": "a"}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"opcode": 5,
|
||||||
|
"mnemonic": "JMP_A",
|
||||||
|
"cycles": 2,
|
||||||
|
"semantics": [
|
||||||
|
{"op": "jmp", "to": "a"}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"opcode": 6,
|
||||||
|
"mnemonic": "JZ_A",
|
||||||
|
"cycles": 2,
|
||||||
|
"semantics": [
|
||||||
|
{"op": "jcc", "to": "a", "condition": "zero,1"}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"opcode": 7,
|
||||||
|
"mnemonic": "JC_A",
|
||||||
|
"cycles": 2,
|
||||||
|
"semantics": [
|
||||||
|
{"op": "jcc", "to": "a", "condition": "carry,1"}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"opcode": 8,
|
||||||
|
"mnemonic": "CMP_A_B",
|
||||||
|
"cycles": 1,
|
||||||
|
"semantics": [
|
||||||
|
{"op": "cmp", "from": "a", "to": "b"}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"opcode": 9,
|
||||||
|
"mnemonic": "MOV_IMM_B",
|
||||||
|
"cycles": 1,
|
||||||
|
"semantics": [
|
||||||
|
{"op": "load_imm", "to": "b", "value": "$next"}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"opcode": 10,
|
||||||
|
"mnemonic": "STORE_A",
|
||||||
|
"cycles": 2,
|
||||||
|
"semantics": [
|
||||||
|
{"op": "store", "from": "a", "to": "mem[$next]"}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"opcode": 11,
|
||||||
|
"mnemonic": "LOAD_A",
|
||||||
|
"cycles": 2,
|
||||||
|
"semantics": [
|
||||||
|
{"op": "load", "from": "mem[$next]", "to": "a"}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"opcode": 12,
|
||||||
|
"mnemonic": "JMP_IMM",
|
||||||
|
"cycles": 2,
|
||||||
|
"semantics": [
|
||||||
|
{"op": "jmp_imm"}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"opcode": 13,
|
||||||
|
"mnemonic": "JZ_IMM",
|
||||||
|
"cycles": 2,
|
||||||
|
"semantics": [
|
||||||
|
{"op": "jcc_imm", "condition": "zero,1"}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"opcode": 14,
|
||||||
|
"mnemonic": "JC_IMM",
|
||||||
|
"cycles": 2,
|
||||||
|
"semantics": [
|
||||||
|
{"op": "jcc_imm", "condition": "carry,1"}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"opcode": 15,
|
||||||
|
"mnemonic": "INC_A",
|
||||||
|
"cycles": 1,
|
||||||
|
"semantics": [
|
||||||
|
{"op": "inc", "from": "a", "to": "a"}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"opcode": 16,
|
||||||
|
"mnemonic": "CALL_IMM",
|
||||||
|
"cycles": 3,
|
||||||
|
"semantics": [
|
||||||
|
{"op": "call", "to": "$next"}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"opcode": 17,
|
||||||
|
"mnemonic": "RET",
|
||||||
|
"cycles": 2,
|
||||||
|
"semantics": [
|
||||||
|
{"op": "ret"}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"opcode": 255,
|
||||||
|
"mnemonic": "HLT",
|
||||||
|
"args": [],
|
||||||
|
"cycles": 1,
|
||||||
|
"semantics": [
|
||||||
|
{"op": "nop"}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"memory_read": [0x01, 0x00],
|
||||||
|
"memory_write": [0x02, 0x00],
|
||||||
|
"io_read": [0x03],
|
||||||
|
"io_write": [0x04]
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user