inital commit кек

This commit is contained in:
SashegDev
2026-06-04 03:12:17 +00:00
parent 82675f402d
commit f2888dea3a
190 changed files with 18421 additions and 21 deletions
+57
View File
@@ -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)
}
+30
View File
@@ -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>
@@ -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>
+5
View File
@@ -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
}
+26
View File
@@ -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; }
}
}
+4
View File
@@ -0,0 +1,4 @@
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
android.useAndroidX=true
kotlin.code.style=official
android.nonTransitiveRClass=true
+27
View File
@@ -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" }
+5
View File
@@ -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
Vendored Executable
+176
View File
@@ -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" "$@"
+84
View File
@@ -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
+19
View File
@@ -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")