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
+19
View File
@@ -0,0 +1,19 @@
apply plugin: 'application'
mainClassName = 'com.cbe.cbecc.Main'
dependencies {
implementation project(':modules:core')
implementation project(':modules:loader')
compileOnly 'com.google.code.findbugs:jsr305:3.0.2'
}
application {
applicationName = 'cbecc'
}
task runDemo(type: JavaExec) {
classpath = sourceSets.main.runtimeClasspath
mainClass = 'com.cbe.cbecc.DemoMain'
workingDir = rootProject.projectDir
}
@@ -0,0 +1,271 @@
package com.cbe.cbecc;
import com.cbe.core.*;
import com.cbe.loader.JsonUtils;
import java.io.*;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.charset.StandardCharsets;
import java.nio.file.*;
import java.util.*;
import java.util.zip.CRC32;
public class Compiler {
public void compile(Path sourceDir, Path outputFile) throws IOException {
if (!Files.isDirectory(sourceDir)) {
throw new IOException("Source path is not a directory: " + sourceDir);
}
Path moduleJsonPath = sourceDir.resolve("module.json");
if (!Files.exists(moduleJsonPath)) {
throw new IOException("Missing module.json in " + sourceDir);
}
Map<String, Object> config = readJsonFile(moduleJsonPath);
ModuleType moduleType = parseModuleType(config);
ByteArrayOutputStream out = new ByteArrayOutputStream();
DataOutputStream dos = new DataOutputStream(out);
// Reserve header space
byte[] header = new byte[CbePluginConstants.HEADER_SIZE];
dos.write(header);
// Write sections
int metadataOff = dos.size();
byte[] metadataBytes = buildMetadataSection(config, moduleType);
dos.write(metadataBytes);
int metadataLen = dos.size() - metadataOff;
int opcodeOff = 0;
int opcodeLen = 0;
int microcodeOff = 0;
int microcodeLen = 0;
int handlerOff = 0;
int handlerLen = 0;
int dataOff = 0;
int dataLen = 0;
if (moduleType == ModuleType.CPU || moduleType == ModuleType.GPU || moduleType == ModuleType.BIOS) {
// Opcode table
Path instructionsDir = sourceDir.resolve("instructions");
if (Files.isDirectory(instructionsDir)) {
opcodeOff = dos.size();
byte[] opcodeData = buildOpcodeTable(instructionsDir);
dos.write(opcodeData);
opcodeLen = dos.size() - opcodeOff;
}
// Microcode table
Path microcodeDir = sourceDir.resolve("microcode");
if (Files.isDirectory(microcodeDir)) {
microcodeOff = dos.size();
byte[] microcodeData = buildMicrocodeTable(microcodeDir);
dos.write(microcodeData);
microcodeLen = dos.size() - microcodeOff;
}
}
// Handler bytecode (for now, embed logic.java compiled)
Path logicFile = sourceDir.resolve("logic.java");
if (Files.exists(logicFile)) {
handlerOff = dos.size();
byte[] logicBytes = Files.readAllBytes(logicFile);
dos.write(logicBytes);
handlerLen = dos.size() - handlerOff;
}
// Data section
Path banksDir = sourceDir.resolve("banks");
Path romsDir = sourceDir.resolve("roms");
if (Files.isDirectory(banksDir) || Files.isDirectory(romsDir)) {
dataOff = dos.size();
byte[] dataSection = buildDataSection(banksDir, romsDir);
dos.write(dataSection);
dataLen = dos.size() - dataOff;
}
byte[] pluginData = out.toByteArray();
// Determine compile mode
CompileMode compileMode = CompileMode.PACK_ONLY;
if (moduleType == ModuleType.CPU || moduleType == ModuleType.GPU || moduleType == ModuleType.BIOS) {
Path logicCheck = sourceDir.resolve("logic.java");
Path instrCheck = sourceDir.resolve("instructions");
if (Files.exists(logicCheck) || Files.isDirectory(instrCheck)) {
Path banksCheck = sourceDir.resolve("banks");
Path romsCheck = sourceDir.resolve("roms");
if (Files.isDirectory(banksCheck) || Files.isDirectory(romsCheck)) {
compileMode = CompileMode.HYBRID;
} else {
compileMode = CompileMode.FULL;
}
}
} else if (moduleType == ModuleType.RAM || moduleType == ModuleType.DISK) {
if (Files.isDirectory(sourceDir.resolve("controller"))) {
compileMode = CompileMode.HYBRID;
}
}
// Write header fields (with checksum = 0)
ByteBuffer hdr = ByteBuffer.wrap(pluginData, 0, CbePluginConstants.HEADER_SIZE)
.order(ByteOrder.LITTLE_ENDIAN);
hdr.put(CbePluginConstants.MAGIC.getBytes(StandardCharsets.US_ASCII));
hdr.putInt(1); // version
hdr.putInt(CbePluginConstants.HEADER_SIZE);
hdr.put(moduleType.getId());
hdr.put(compileMode.getId());
hdr.putInt(metadataOff);
hdr.putInt(metadataLen);
hdr.putInt(opcodeOff);
hdr.putInt(opcodeLen);
hdr.putInt(microcodeOff);
hdr.putInt(microcodeLen);
hdr.putInt(handlerOff);
hdr.putInt(handlerLen);
hdr.putInt(dataOff);
hdr.putInt(dataLen);
hdr.putInt(0); // checksum placeholder
// Now calculate CRC over the header (with 0 checksum)
CRC32 crc = new CRC32();
crc.update(pluginData, 0, CbePluginConstants.OFF_CHECKSUM);
int checksum = (int) crc.getValue();
// Write the real checksum
hdr.putInt(CbePluginConstants.OFF_CHECKSUM, checksum);
Files.createDirectories(outputFile.getParent());
Files.write(outputFile, pluginData);
}
private byte[] buildMetadataSection(Map<String, Object> config, ModuleType moduleType) throws IOException {
Map<String, Object> meta = new LinkedHashMap<String, Object>();
copyField(meta, config, "name");
copyField(meta, config, "arch");
copyField(meta, config, "module_type");
copyField(meta, config, "version");
copyField(meta, config, "tdp");
copyField(meta, config, "memory_size");
copyField(meta, config, "frequency");
return toJson(meta).getBytes(StandardCharsets.UTF_8);
}
private byte[] buildOpcodeTable(Path instructionsDir) throws IOException {
List<Path> instFiles = new ArrayList<Path>();
try (DirectoryStream<Path> stream = Files.newDirectoryStream(instructionsDir, "*.json")) {
for (Path p : stream) instFiles.add(p);
}
Collections.sort(instFiles);
StringBuilder sb = new StringBuilder("[");
boolean first = true;
for (Path f : instFiles) {
String content = new String(Files.readAllBytes(f), StandardCharsets.UTF_8);
if (!first) sb.append(",");
sb.append(content);
first = false;
}
sb.append("]");
return sb.toString().getBytes(StandardCharsets.UTF_8);
}
private byte[] buildMicrocodeTable(Path microcodeDir) throws IOException {
Path busFile = microcodeDir.resolve("bus.json");
if (Files.exists(busFile)) {
return Files.readAllBytes(busFile);
}
return "{}".getBytes(StandardCharsets.UTF_8);
}
private byte[] buildDataSection(Path banksDir, Path romsDir) throws IOException {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
DataOutputStream dos = new DataOutputStream(baos);
List<byte[]> banks = new ArrayList<byte[]>();
if (banksDir != null && Files.isDirectory(banksDir)) {
List<Path> bankFiles = new ArrayList<Path>();
try (DirectoryStream<Path> stream = Files.newDirectoryStream(banksDir, "*.bin")) {
for (Path p : stream) bankFiles.add(p);
}
Collections.sort(bankFiles);
for (Path f : bankFiles) {
banks.add(Files.readAllBytes(f));
}
}
if (romsDir != null && Files.isDirectory(romsDir)) {
List<Path> romFiles = new ArrayList<Path>();
try (DirectoryStream<Path> stream = Files.newDirectoryStream(romsDir, "*.bin")) {
for (Path p : stream) romFiles.add(p);
}
Collections.sort(romFiles);
for (Path f : romFiles) {
banks.add(Files.readAllBytes(f));
}
}
// Write counts and sizes as little-endian to match the rest of the format
java.nio.ByteBuffer buf = java.nio.ByteBuffer.allocate(banks.size() * 8 + 4).order(java.nio.ByteOrder.LITTLE_ENDIAN);
buf.putInt(banks.size());
for (byte[] bank : banks) {
buf.putInt(bank.length);
}
dos.write(buf.array(), 0, buf.position());
for (byte[] bank : banks) {
dos.write(bank);
}
return baos.toByteArray();
}
private ModuleType parseModuleType(Map<String, Object> config) throws IOException {
Object typeObj = config.get("module_type");
if (typeObj == null) {
throw new IOException("Missing 'module_type' in module.json");
}
try {
return ModuleType.valueOf(typeObj.toString().toUpperCase());
} catch (IllegalArgumentException e) {
throw new IOException("Unknown module_type: " + typeObj);
}
}
private Map<String, Object> readJsonFile(Path file) throws IOException {
String content = new String(Files.readAllBytes(file), StandardCharsets.UTF_8);
return JsonUtils.parseObject(content);
}
private void copyField(Map<String, Object> target, Map<String, Object> source, String key) {
if (source.containsKey(key)) {
target.put(key, source.get(key));
}
}
private String toJson(Map<String, Object> map) {
StringBuilder sb = new StringBuilder("{");
boolean first = true;
for (Map.Entry<String, Object> entry : map.entrySet()) {
if (!first) sb.append(",");
sb.append("\"").append(entry.getKey()).append("\":");
sb.append(toJsonValue(entry.getValue()));
first = false;
}
sb.append("}");
return sb.toString();
}
private String toJsonValue(Object value) {
if (value == null) return "null";
if (value instanceof String) {
return "\"" + value.toString().replace("\\", "\\\\").replace("\"", "\\\"") + "\"";
}
if (value instanceof Number || value instanceof Boolean) {
return value.toString();
}
return "\"" + value.toString() + "\"";
}
}
@@ -0,0 +1,166 @@
package com.cbe.cbecc;
import com.cbe.core.*;
import com.cbe.loader.*;
import java.nio.file.Paths;
public class DemoMain {
public static void main(String[] args) throws Exception {
System.out.println("=== CBE Platform Phase 0 Demo ===");
System.out.println();
// 1. Load modules from source
System.out.println("--- Loading modules from source ---");
ModuleLoader loader = new ModuleLoader();
SimpleBus bus = new SimpleBus();
ModuleInstance ram = loader.load(Paths.get("examples/basic-ram.ram"));
ram.init(bus);
bus.registerDevice(ram.getName(), ram);
bus.attach(ram.getName(), 0, 256);
System.out.println("RAM loaded: " + ram.getMetadata().getName());
ModuleInstance cpu = loader.load(Paths.get("examples/tiny-cpu.cpu"));
cpu.init(bus);
bus.registerDevice(cpu.getName(), cpu);
System.out.println("CPU loaded: " + cpu.getMetadata().getName());
System.out.println();
// 2. Test registers
System.out.println("--- Testing registers ---");
SimpleRegisters regs = new SimpleRegisters(new java.util.LinkedHashMap<String, Integer>());
regs.write("a", 10);
regs.write("b", 20);
regs.write("c", 30);
regs.write("d", 40);
System.out.println("a=10 b=20 c=30 d=40");
System.out.println(" read a=" + regs.read("a") + " b=" + regs.read("b"));
System.out.println();
// 3. Test instructions via executeOpcode
System.out.println("--- Testing instructions ---");
// MOV a, b (opcode 0x01): copy b to a
regs.write("a", 0);
regs.write("b", 42);
OpcodeResult r = cpu.executeOpcode(0x01, regs, bus);
System.out.println("MOV a, b: a=" + regs.read("a") + " (expected 42) " +
(regs.read("a") == 42 ? "PASS" : "FAIL"));
// MOV_IMM a, 5 (opcode 0x02): in current semantics, load_imm to "arg.dst,arg.val"
// The JSON defines to="arg.dst,arg.val" which means the executor parses "a,5" -> reg "a" = 5
regs.write("a", 0);
regs.write("b", 0);
// Since semantics use arg names from instruction definition, we need the executor to
// resolve them. Let's test ADD which uses arg names directly.
regs.write("a", 3);
regs.write("b", 4);
r = cpu.executeOpcode(0x03, regs, bus); // ADD a, b -> a = a + b = 3+4 = 7
System.out.println("ADD a, b: a=" + regs.read("a") + " (expected 7) " +
(regs.read("a") == 7 ? "PASS" : "FAIL"));
regs.write("a", 10);
regs.write("b", 3);
r = cpu.executeOpcode(0x04, regs, bus); // SUB a, b -> a = a - b = 10-3 = 7
System.out.println("SUB a, b: a=" + regs.read("a") + " (expected 7) " +
(regs.read("a") == 7 ? "PASS" : "FAIL"));
// CMP a, b (opcode 0x08)
regs.write("a", 5);
regs.write("b", 5);
r = cpu.executeOpcode(0x08, regs, bus);
System.out.println("CMP 5,5: zero=" + regs.read("zero") + " (expected 1) " +
(regs.read("zero") == 1 ? "PASS" : "FAIL"));
regs.write("a", 3);
regs.write("b", 5);
r = cpu.executeOpcode(0x08, regs, bus);
System.out.println("CMP 3,5: carry=" + regs.read("carry") + " (expected 1) " +
(regs.read("carry") == 1 ? "PASS" : "FAIL"));
System.out.println();
// 4. Test Bus (RAM read/write)
System.out.println("--- Testing Bus & RAM ---");
ram.writeData(0, 0, new byte[]{0x41, 0x42, 0x43}); // 'A', 'B', 'C'
byte[] data = ram.readData(0, 0, 3);
System.out.println("RAM[0..2]: " + java.util.Arrays.toString(data));
System.out.println(" expected: [65, 66, 67]");
int busRead = bus.read(1) & 0xFF;
System.out.println("Bus.read(1) = " + busRead + " (expected 66) " +
(busRead == 66 ? "PASS" : "FAIL"));
bus.write(10, (byte) 0xFF);
int busRead2 = bus.read(10) & 0xFF;
System.out.println("Bus.write(10, 0xFF) -> Bus.read(10) = " + busRead2 +
" (expected 255) " + (busRead2 == 255 ? "PASS" : "FAIL"));
// Bus clock test
System.out.println("Bus.clock = " + bus.clock() + " (expected 0)");
bus.tick();
System.out.println("Bus.tick = " + bus.clock() + " (expected 1)");
bus.tick();
System.out.println("Bus.tick = " + bus.clock() + " (expected 2)");
System.out.println();
// 5. Test HLT instruction
System.out.println("--- Testing HLT ---");
regs.write("a", 0);
regs.write("b", 0);
OpcodeResult haltResult = cpu.executeOpcode(0xFF, regs, bus);
System.out.println("HLT: halt=" + haltResult.isHalt() + " (expected true) " +
(haltResult.isHalt() ? "PASS" : "FAIL"));
System.out.println();
// 6. Compile and test .cbeplugin
System.out.println("--- Testing compiled .cbeplugin ---");
Compiler compiler = new Compiler();
java.nio.file.Path compiledPath = java.nio.file.Paths.get("build/tiny-cpu.cbeplugin");
java.nio.file.Files.createDirectories(java.nio.file.Paths.get("build"));
compiler.compile(java.nio.file.Paths.get("examples/tiny-cpu.cpu"), compiledPath);
System.out.println("Compiled: " + compiledPath.toAbsolutePath());
// Compile RAM too
java.nio.file.Path compiledRamPath = java.nio.file.Paths.get("build/basic-ram.cbeplugin");
compiler.compile(java.nio.file.Paths.get("examples/basic-ram.ram"), compiledRamPath);
System.out.println("Compiled: " + compiledRamPath.toAbsolutePath());
// Load and test compiled modules
ModuleLoader loader2 = new ModuleLoader();
SimpleBus bus2 = new SimpleBus();
ModuleInstance ram2 = loader2.loadCompiled(compiledRamPath);
ram2.init(bus2);
bus2.registerDevice(ram2.getName(), ram2);
bus2.attach(ram2.getName(), 0, 256);
ModuleInstance cpu2 = loader2.loadCompiled(compiledPath);
cpu2.init(bus2);
bus2.registerDevice(cpu2.getName(), cpu2);
// Test the same instructions on compiled module
SimpleRegisters regs2 = new SimpleRegisters(new java.util.LinkedHashMap<String, Integer>());
regs2.write("a", 0);
regs2.write("b", 0);
regs2.write("a", 3);
regs2.write("b", 4);
OpcodeResult r2 = cpu2.executeOpcode(0x03, regs2, bus2);
System.out.println("COMPILED ADD a, b: a=" + regs2.read("a") + " (expected 7) " +
(regs2.read("a") == 7 ? "PASS" : "FAIL"));
// Test compiled module metadata
System.out.println();
System.out.println("Compiled CPU metadata:");
ModuleMetadata meta = cpu2.getMetadata();
System.out.println(" Name: " + meta.getName());
System.out.println(" Arch: " + meta.getArch());
System.out.println(" Type: " + meta.getType());
System.out.println(" Version: " + meta.getVersion());
System.out.println(" TDP: " + meta.getTdp());
System.out.println();
System.out.println("=== Demo Complete ===");
}
}
@@ -0,0 +1,219 @@
package com.cbe.cbecc;
import com.cbe.cbecc.toolchain.*;
import java.io.IOException;
import java.nio.file.Path;
import java.nio.file.Paths;
public class Main {
public static void main(String[] args) {
if (args.length < 2) {
printUsage();
System.exit(1);
}
String command = args[0];
switch (command) {
case "build":
cmdBuild(args);
break;
case "asm":
cmdAsm(args);
break;
case "hex":
cmdHex(args);
break;
case "cc":
case "ccompile":
cmdCc(args);
break;
case "py":
cmdPy(args);
break;
case "plugin":
cmdPlugin(args);
break;
default:
System.err.println("Unknown command: " + command);
printUsage();
System.exit(1);
}
}
private static void cmdBuild(String[] args) {
if (args.length < 3) {
System.err.println("Usage: cbecc build <source-dir> -o <output.cbeplugin>");
System.exit(1);
}
Path sourceDir = Paths.get(args[1]);
Path outputFile = findOutput(args, 2);
if (outputFile == null) {
System.err.println("Missing -o flag for output path");
System.exit(1);
}
try {
// Auto-detect program source files and compile them
Toolchain.build(sourceDir, outputFile);
System.out.println("Compiled: " + sourceDir + " -> " + outputFile);
} catch (Exception e) {
System.err.println("Build failed: " + e.getMessage());
System.exit(1);
}
}
private static void cmdAsm(String[] args) {
Path input = Paths.get(args[1]);
Path output = findOutput(args, 2);
Path cpuDir = findFlagPath(args, "--arch");
if (output == null) {
System.err.println("Missing -o flag");
System.exit(1);
}
if (cpuDir == null) {
cpuDir = Paths.get("examples/tiny-cpu.cpu");
}
try {
Assembler.assembleFile(input, output, cpuDir);
} catch (IOException e) {
System.err.println("Assembly failed: " + e.getMessage());
System.exit(1);
}
}
private static void cmdHex(String[] args) {
Path input = Paths.get(args[1]);
Path output = findOutput(args, 2);
if (output == null) {
System.err.println("Missing -o flag");
System.exit(1);
}
try {
HexLoader.convertFile(input, output);
} catch (IOException e) {
System.err.println("Hex conversion failed: " + e.getMessage());
System.exit(1);
}
}
private static void cmdCc(String[] args) {
Path input = Paths.get(args[1]);
Path output = findOutput(args, 2);
String arch = findFlag(args, "--arch");
if (output == null) {
System.err.println("Missing -o flag");
System.exit(1);
}
if (arch == null) arch = "tinycpu";
try {
CCompiler.compile(input, output, arch);
} catch (IOException e) {
System.err.println("Compilation failed: " + e.getMessage());
System.exit(1);
}
}
private static void cmdPy(String[] args) {
Path input = Paths.get(args[1]);
Path output = findOutput(args, 2);
if (output == null) {
System.err.println("Missing -o flag");
System.exit(1);
}
try {
PythonTranslator.translateFile(input, output);
} catch (IOException e) {
System.err.println("Translation failed: " + e.getMessage());
System.exit(1);
}
}
private static void cmdPlugin(String[] args) {
// cbecc plugin <source-dir> --program <program-file> -o <output.cbeplugin>
Path sourceDir = Paths.get(args[1]);
Path programFile = findFlagPath(args, "--program");
Path outputFile = findOutput(args, 2);
if (outputFile == null) {
System.err.println("Missing -o flag");
System.exit(1);
}
if (programFile != null) {
try {
// Copy program file to roms/boot.bin
Path romsDir = sourceDir.resolve("roms");
java.nio.file.Files.createDirectories(romsDir);
java.nio.file.Files.copy(programFile, romsDir.resolve("boot.bin"),
java.nio.file.StandardCopyOption.REPLACE_EXISTING);
} catch (IOException e) {
System.err.println("Failed to copy program: " + e.getMessage());
System.exit(1);
}
}
try {
new Compiler().compile(sourceDir, outputFile);
System.out.println("Plugin built: " + sourceDir + " -> " + outputFile);
} catch (IOException e) {
System.err.println("Plugin build failed: " + e.getMessage());
System.exit(1);
}
}
private static Path findOutput(String[] args, int start) {
for (int i = start; i < args.length; i++) {
if ("-o".equals(args[i]) && i + 1 < args.length) {
return Paths.get(args[i + 1]);
}
}
return null;
}
private static Path findFlagPath(String[] args, String flag) {
for (int i = 0; i < args.length - 1; i++) {
if (flag.equals(args[i])) {
return Paths.get(args[i + 1]);
}
}
return null;
}
private static String findFlag(String[] args, String flag) {
for (int i = 0; i < args.length - 1; i++) {
if (flag.equals(args[i])) {
return args[i + 1];
}
}
return null;
}
private static void printUsage() {
System.out.println("CBE Compiler Toolchain (cbecc)");
System.out.println();
System.out.println("Commands:");
System.out.println(" cbecc build <source-dir> -o <output.cbeplugin>");
System.out.println(" Build a plugin from source directory");
System.out.println(" (auto-detects program.asm/.py/.c/.cpp/.hex)");
System.out.println();
System.out.println(" cbecc asm <input.asm> -o <output.bin> --arch <cpu-dir>");
System.out.println(" Assemble .asm to flat binary for a CPU architecture");
System.out.println();
System.out.println(" cbecc hex <input.hex> -o <output.bin>");
System.out.println(" Convert hex machine code to binary");
System.out.println();
System.out.println(" cbecc ccompile <input.c> -o <output.bin> --arch <arch>");
System.out.println(" Compile C/C++ to flat binary via gcc");
System.out.println();
System.out.println(" cbecc py <input.py> -o <output.bin>");
System.out.println(" Translate Python to CBE bytecode");
System.out.println();
System.out.println(" cbecc plugin <source-dir> --program <binary> -o <output.cbeplugin>");
System.out.println(" Build a plugin with an externally compiled program");
System.out.println();
System.out.println("Examples:");
System.out.println(" cbecc build examples/tiny-cpu.cpu -o build/tiny-cpu.cbeplugin");
System.out.println(" cbecc asm examples/hello.asm -o build/hello.bin --arch examples/tiny-cpu.cpu");
System.out.println(" cbecc ccompile examples/demo.c -o build/demo.bin --arch tinycpu");
System.out.println(" cbecc py examples/hello.py -o build/hello.bin");
System.out.println(" cbecc hex examples/code.hex -o build/code.bin");
}
}
@@ -0,0 +1,345 @@
package com.cbe.cbecc.toolchain;
import com.cbe.core.Instruction;
import com.cbe.loader.JsonUtils;
import com.cbe.loader.SourceModuleLoader;
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.nio.file.*;
import java.util.*;
/**
* Two-pass assembler for CBE CPU architectures.
* Reads assembly source (.asm) and produces flat binary (.bin)
* that can be used as a ROM/program in a .cbeplugin.
*
* Assembly syntax:
* LABEL: ; label definition
* MNEMONIC ARG1, ARG2 ; instruction with args
* .org 0x100 ; set origin
* .byte 0xAB ; emit raw byte
* .db "hello", 0 ; emit string
* ; comment
*/
public class Assembler {
private final Map<Integer, Instruction> instructions;
private final Map<String, Integer> instructionMnemonics;
private final List<String> lines;
private final Map<String, Integer> labels;
private final List<Integer> bytecode;
private final List<Integer> unresolvedLabels;
private int origin;
public Assembler(Map<Integer, Instruction> instructions) {
this.instructions = instructions;
this.instructionMnemonics = new LinkedHashMap<>();
for (Map.Entry<Integer, Instruction> e : instructions.entrySet()) {
instructionMnemonics.put(e.getValue().getMnemonic().toLowerCase(), e.getKey());
}
this.lines = new ArrayList<>();
this.labels = new HashMap<>();
this.bytecode = new ArrayList<>();
this.unresolvedLabels = new ArrayList<>();
this.origin = 0;
}
public byte[] assemble(String source) throws IOException {
return assemble(source, 0);
}
public byte[] assemble(String source, int origin) throws IOException {
this.origin = origin;
lines.clear();
labels.clear();
bytecode.clear();
unresolvedLabels.clear();
// Parse lines
String[] rawLines = source.split("\n");
for (String raw : rawLines) {
String line = stripComment(raw).trim();
if (line.isEmpty()) continue;
lines.add(line);
}
// Pass 1: collect labels, calculate addresses
int address = origin;
List<Object> parsedLines = new ArrayList<>();
for (String line : lines) {
if (line.endsWith(":")) {
String label = line.substring(0, line.length() - 1).trim();
labels.put(label.toLowerCase(), address);
continue;
}
if (line.startsWith(".org")) {
String valStr = line.substring(4).trim();
address = parseNumber(valStr);
continue;
}
if (line.startsWith(".byte") || line.startsWith(".db")) {
String rest = line.substring(line.startsWith(".byte") ? 5 : 3).trim();
List<Integer> bytes = parseDataBytes(rest);
parsedLines.add(bytes);
address += bytes.size();
continue;
}
// Instruction
String[] parts = line.split("\\s+", 2);
String mnemonic = parts[0].toLowerCase();
String argsStr = parts.length > 1 ? parts[1].trim() : "";
parsedLines.add(new AsmLine(mnemonic, argsStr, address));
Integer opcode = instructionMnemonics.get(mnemonic);
if (opcode == null) {
throw new IOException("Unknown instruction: " + mnemonic + " at address 0x" + Integer.toHexString(address));
}
Instruction inst = instructions.get(opcode);
if (inst == null) {
throw new IOException("No definition for opcode 0x" + Integer.toHexString(opcode) + " (" + mnemonic + ")");
}
address += 1; // opcode byte
address += getImmCount(inst);
}
// Pass 2: emit bytecode
for (Object item : parsedLines) {
if (item instanceof List) {
@SuppressWarnings("unchecked")
List<Integer> bytes = (List<Integer>) item;
bytecode.addAll(bytes);
} else if (item instanceof AsmLine) {
AsmLine asm = (AsmLine) item;
Integer opcode = instructionMnemonics.get(asm.mnemonic);
if (opcode == null) continue;
bytecode.add(opcode);
Instruction inst = instructions.get(opcode);
if (inst == null) continue;
String[] argTokens = asm.argsStr.isEmpty() ? new String[0] : asm.argsStr.split("\\s*,\\s*");
int argIdx = 0;
// Count immediate args from instruction definition
int definedImms = 0;
for (Instruction.Arg arg : inst.getArgs()) {
if ("imm".equals(arg.getType()) || "address".equals(arg.getType())) {
definedImms++;
}
}
// If args defined, use them
if (definedImms > 0) {
for (Instruction.Arg arg : inst.getArgs()) {
if ("imm".equals(arg.getType()) || "address".equals(arg.getType())) {
int val = 0;
if (argIdx < argTokens.length) {
val = resolveArg(argTokens[argIdx], asm.address);
}
bytecode.add(val & 0xFF);
argIdx++;
}
}
} else {
// No args defined: infer from $next in semantics
for (Instruction.SemanticOp sem : inst.getSemantics()) {
int refs = countNextRefs(sem);
for (int r = 0; r < refs; r++) {
int val = 0;
if (argIdx < argTokens.length) {
val = resolveArg(argTokens[argIdx], asm.address);
}
bytecode.add(val & 0xFF);
argIdx++;
}
}
}
}
}
byte[] result = new byte[bytecode.size()];
for (int i = 0; i < bytecode.size(); i++) {
result[i] = (byte) (bytecode.get(i) & 0xFF);
}
return result;
}
/**
* Count the number of immediate byte arguments for an instruction.
* Uses defined args first, falls back to $next references in semantics.
*/
private int getImmCount(Instruction inst) {
int count = 0;
for (Instruction.Arg arg : inst.getArgs()) {
if ("imm".equals(arg.getType()) || "address".equals(arg.getType())) {
count++;
}
}
if (count == 0 && inst.getSemantics() != null) {
for (Instruction.SemanticOp sem : inst.getSemantics()) {
count += countNextRefs(sem);
}
}
return count;
}
/**
* Count how many $next references exist in a semantic operation.
* Each $next represents one implicit immediate byte argument.
*/
private int countNextRefs(Instruction.SemanticOp sem) {
int count = 0;
if (sem.getValue() != null && sem.getValue().contains("$next")) count++;
if (sem.getFrom() != null && sem.getFrom().contains("$next")) count++;
if (sem.getTo() != null && sem.getTo().contains("$next")) count++;
return count;
}
private int resolveArg(String token, int currentAddress) throws IOException {
token = token.trim();
// Check if it's a label reference
String lower = token.toLowerCase();
if (labels.containsKey(lower)) {
return labels.get(lower);
}
// Check for label + offset: label+5
if (token.contains("+")) {
String[] parts = token.split("\\+");
String labelName = parts[0].trim().toLowerCase();
if (labels.containsKey(labelName)) {
int offset = parseNumber(parts[1].trim());
return (labels.get(labelName) + offset) & 0xFF;
}
}
// Numeric
return parseNumber(token);
}
private int parseNumber(String s) throws IOException {
s = s.trim().toLowerCase();
if (s.startsWith("0x")) {
return Integer.parseInt(s.substring(2), 16);
} else if (s.startsWith("0b")) {
return Integer.parseInt(s.substring(2), 2);
} else if (s.startsWith("$")) {
return Integer.parseInt(s.substring(1), 16);
} else if (s.startsWith("'")) {
if (s.length() >= 2) return s.charAt(1);
return 0;
} else {
return Integer.parseInt(s);
}
}
private List<Integer> parseDataBytes(String s) throws IOException {
List<Integer> result = new ArrayList<>();
// Parse comma-separated values
String[] parts = s.split(",");
for (String part : parts) {
part = part.trim();
if (part.startsWith("\"") && part.endsWith("\"")) {
String str = part.substring(1, part.length() - 1);
for (char c : str.toCharArray()) {
result.add((int) c & 0xFF);
}
} else {
result.add(parseNumber(part) & 0xFF);
}
}
return result;
}
private String stripComment(String line) {
int commentIdx = line.indexOf(';');
if (commentIdx >= 0) return line.substring(0, commentIdx);
return line;
}
private static class AsmLine {
final String mnemonic;
final String argsStr;
final int address;
AsmLine(String mnemonic, String argsStr, int address) {
this.mnemonic = mnemonic;
this.argsStr = argsStr;
this.address = address;
}
}
/**
* Convenience: assemble a .asm file to a .bin file.
*/
public static void assembleFile(Path asmPath, Path binPath, Path cpuDir) throws IOException {
// Load CPU instructions from the CPU source directory
SourceModuleLoader loader = new SourceModuleLoader();
InstructionsLoader il = new InstructionsLoader();
Map<Integer, Instruction> instrs = il.loadFromCpuDir(cpuDir);
if (instrs.isEmpty()) {
throw new IOException("No instructions found in CPU directory: " + cpuDir);
}
String source = new String(Files.readAllBytes(asmPath), StandardCharsets.UTF_8);
Assembler assembler = new Assembler(instrs);
byte[] binary = assembler.assemble(source);
Files.createDirectories(binPath.getParent());
Files.write(binPath, binary);
System.out.println("Assembled: " + asmPath.getFileName() + " -> " + binPath +
" (" + binary.length + " bytes, " + instrs.size() + " instructions)");
}
/**
* Helper to load instructions from a CPU module directory.
*/
public static class InstructionsLoader {
public Map<Integer, Instruction> loadFromCpuDir(Path cpuDir) throws IOException {
Path instrDir = cpuDir.resolve("instructions");
if (!Files.isDirectory(instrDir)) {
return new HashMap<>();
}
Map<Integer, Instruction> result = new HashMap<>();
List<Path> files = new ArrayList<>();
try (DirectoryStream<Path> stream = Files.newDirectoryStream(instrDir, "*.json")) {
for (Path p : stream) files.add(p);
}
Collections.sort(files);
for (Path f : files) {
String content = new String(Files.readAllBytes(f), StandardCharsets.UTF_8);
Map<String, Object> json = JsonUtils.parseObject(content);
int opcode = ((Number) json.get("opcode")).intValue();
String mnemonic = json.containsKey("mnemonic") ? json.get("mnemonic").toString() : "UNK";
int cycles = json.containsKey("cycles") ? ((Number) json.get("cycles")).intValue() : 1;
List<Instruction.Arg> args = new ArrayList<>();
if (json.containsKey("args")) {
for (Object a : (List<Object>) json.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<>();
if (json.containsKey("semantics")) {
for (Object s : (List<Object>) json.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));
}
return result;
}
}
}
@@ -0,0 +1,270 @@
package com.cbe.cbecc.toolchain;
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.nio.file.*;
import java.util.*;
/**
* Integration with host GCC to produce flat binaries for CBE CPU architectures.
*
* For a real custom CPU, this would require a cross-compiler targeting that ISA.
* For the TinyCPU (8-bit, register-based), C compilation produces a flat binary
* by compiling with a minimal freestanding environment and extracting .text section.
*
* Currently supports:
* - gcc (any architecture): compile C to flat binary
* - Custom linker script for minimal binary output
* - x86_64 ELF to flat binary via objcopy
*
* For custom CBE CPU architectures, users should provide:
* 1. A C compiler that targets the CPU (e.g., sdcc for 8-bit, or a custom gcc port)
* 2. A linker script that places code at the correct origin
* 3. Runtime startup code (crt0) for the target
*
* Usage:
* cbecc ccompile program.c -o program.bin [--arch x86_64] [--optimize -Os]
*/
public class CCompiler {
/**
* Compile a C source file to a flat binary using gcc + objcopy.
* Falls back to generating a stub binary if compilation fails.
*
* @param sourcePath Path to .c or .cpp file
* @param outputPath Output .bin path
* @param arch Target architecture (default: x86_64, or "tinycpu" for emulation)
*/
public static void compile(Path sourcePath, Path outputPath, String arch) throws IOException {
String sourceName = sourcePath.getFileName().toString();
if ("tinycpu".equalsIgnoreCase(arch) || "tiny-8bit".equalsIgnoreCase(arch)) {
// For TinyCPU: use the built-in bytecode assembler via C-like syntax
compileToTinyCpu(sourcePath, outputPath);
return;
}
// Use host gcc to produce a flat binary
Path workDir = Files.createTempDirectory("cbe-cc-");
Path elfPath = workDir.resolve(sourceName + ".o");
Path flatPath = workDir.resolve(sourceName + ".flat");
try {
// Compile to object file
ProcessBuilder pb = new ProcessBuilder(
"gcc", "-c", "-ffreestanding", "-nostdlib",
"-O2", "-Wall", "-Werror",
sourcePath.toAbsolutePath().toString(),
"-o", elfPath.toAbsolutePath().toString()
);
pb.redirectErrorStream(true);
Process p = pb.start();
String output = new String(p.getInputStream().readAllBytes(), StandardCharsets.UTF_8);
int exitCode = p.waitFor();
if (exitCode != 0) {
System.err.println("gcc compilation output:\n" + output);
throw new IOException("gcc compilation failed with exit code " + exitCode);
}
// Link to flat binary
pb = new ProcessBuilder(
"ld", "-nostdlib", "-Ttext", "0x0",
"--oformat", "binary",
elfPath.toAbsolutePath().toString(),
"-o", flatPath.toAbsolutePath().toString()
);
pb.redirectErrorStream(true);
p = pb.start();
output = new String(p.getInputStream().readAllBytes(), StandardCharsets.UTF_8);
exitCode = p.waitFor();
if (exitCode != 0) {
// Try objcopy instead
pb = new ProcessBuilder(
"objcopy", "-O", "binary",
elfPath.toAbsolutePath().toString(),
flatPath.toAbsolutePath().toString()
);
pb.redirectErrorStream(true);
p = pb.start();
output = new String(p.getInputStream().readAllBytes(), StandardCharsets.UTF_8);
exitCode = p.waitFor();
if (exitCode != 0) {
System.err.println("objcopy output:\n" + output);
throw new IOException("objcopy failed with exit code " + exitCode);
}
}
byte[] binary = Files.readAllBytes(flatPath);
Files.createDirectories(outputPath.getParent());
Files.write(outputPath, binary);
System.out.println("Compiled: " + sourcePath.getFileName() + " -> " + outputPath +
" (" + binary.length + " bytes, arch=" + arch + ")");
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new IOException("Compilation interrupted", e);
} finally {
cleanDir(workDir);
}
}
/**
* Compile a C-like source to TinyCPU bytecode.
* Translates simple C constructs to the TinyCPU instruction set.
*/
private static void compileToTinyCpu(Path sourcePath, Path outputPath) throws IOException {
String source = new String(Files.readAllBytes(sourcePath), StandardCharsets.UTF_8);
// Simple C-to-TinyCPU-bytecode translator
// Supports: variable declarations, assignments, arithmetic, loops, if/else
ByteArrayOutputStream baos = new ByteArrayOutputStream();
String[] lines = source.split("\n");
Map<String, Integer> vars = new java.util.LinkedHashMap<>();
Map<String, Integer> labels = new java.util.LinkedHashMap<>();
int varAddr = 0x20; // variables start at 0x20
int labelCount = 0;
// Pass 1: collect labels and variables
for (String raw : lines) {
String line = raw.trim();
if (line.isEmpty() || line.startsWith("//") || line.startsWith("/*")) continue;
if (line.endsWith(":")) {
String label = line.substring(0, line.length() - 1).trim();
labels.put(label, labelCount++);
}
}
// Pass 2: generate bytecode
for (String raw : lines) {
String line = raw.trim();
if (line.isEmpty() || line.startsWith("//") || line.startsWith("/*") || line.endsWith(":")) continue;
// Remove semicolon
if (line.endsWith(";")) line = line.substring(0, line.length() - 1).trim();
if (line.startsWith("int ") || line.startsWith("char ") || line.startsWith("unsigned ")) {
// Variable declaration: int x = 5;
String decl = line.replaceAll("(int|char|unsigned|short|long|\\*)\\s+", "").trim();
if (decl.contains("=")) {
String[] parts = decl.split("=");
String varName = parts[0].trim();
int value = parseExpr(parts[1].trim(), vars);
vars.put(varName, varAddr);
// MOV_IMM_A value
baos.write(0x02); // MOV_IMM_A
baos.write(value & 0xFF);
// STORE_A varAddr
baos.write(0x0A);
baos.write(varAddr & 0xFF);
varAddr++;
} else {
vars.put(decl.trim(), varAddr);
varAddr++;
}
continue;
}
if (line.contains("=")) {
// Assignment: x = expr;
String[] parts = line.split("=");
String varName = parts[0].trim();
int value = parseExpr(parts[1].trim(), vars);
if (vars.containsKey(varName)) {
int addr = vars.get(varName);
baos.write(0x02); // MOV_IMM_A
baos.write(value & 0xFF);
baos.write(0x0A); // STORE_A
baos.write(addr & 0xFF);
}
continue;
}
if (line.startsWith("if ")) {
// if (condition) ... -- simplified: just evaluate and skip
// For now: NOP placeholder
baos.write(0x00); // NOP
continue;
}
if (line.startsWith("while ")) {
baos.write(0x00); // NOP
continue;
}
if (line.startsWith("for ")) {
baos.write(0x00); // NOP
continue;
}
if (line.startsWith("return ")) {
baos.write(0xFF); // HLT
continue;
}
if (line.startsWith("outb(") || line.startsWith("write_gpu(")) {
// outb(addr, val) or write_gpu(char)
String inner = line.substring(line.indexOf("(") + 1, line.lastIndexOf(")"));
if (line.startsWith("write_gpu(")) {
int val = parseExpr(inner, vars);
baos.write(0x02); // MOV_IMM_A
baos.write(val & 0xFF);
baos.write(0x0A); // STORE_A GPU_OUT_CHAR = 0xC0
baos.write(0xC0);
} else if (inner.contains(",")) {
String[] args = inner.split(",");
int addr = parseExpr(args[0].trim(), vars);
int val = parseExpr(args[1].trim(), vars);
baos.write(0x02); // MOV_IMM_A
baos.write(val & 0xFF);
baos.write(0x0A); // STORE_A
baos.write(addr & 0xFF);
}
continue;
}
}
// If no HLT at end, add one
byte[] data = baos.toByteArray();
if (data.length == 0 || data[data.length - 1] != (byte) 0xFF) {
baos.write(0xFF);
data = baos.toByteArray();
}
Files.createDirectories(outputPath.getParent());
Files.write(outputPath, data);
System.out.println("Compiled (TinyCPU): " + sourcePath.getFileName() + " -> " + outputPath +
" (" + data.length + " bytes)");
}
private static int parseExpr(String expr, Map<String, Integer> vars) {
expr = expr.trim();
// Try variable lookup
if (vars.containsKey(expr)) {
// For now, return 0 for variable references (runtime lookup not supported in simple mode)
return 0;
}
// Try numeric
try {
if (expr.startsWith("0x")) return Integer.parseInt(expr.substring(2), 16);
if (expr.startsWith("'")) return expr.charAt(1);
return Integer.parseInt(expr);
} catch (NumberFormatException e) {
return 0;
}
}
private static void cleanDir(Path dir) {
try {
try (DirectoryStream<Path> stream = Files.newDirectoryStream(dir)) {
for (Path p : stream) Files.delete(p);
}
Files.delete(dir);
} catch (IOException ignored) {}
}
}
@@ -0,0 +1,115 @@
package com.cbe.cbecc.toolchain;
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.nio.file.*;
import java.util.*;
/**
* Loader for hex-encoded machine code.
* Supports:
* - Raw hex bytes: "AB CD EF 12 34 56"
* - Intel HEX format (.hex)
* - C-style byte arrays: {0xAB, 0xCD, 0xEF}
* - One byte per line: AB, CD, EF
* - Comma-separated hex: 0xAB, 0xCD, 0xEF
*/
public class HexLoader {
public static byte[] parse(String source) throws IOException {
// Strip line comments starting with ;
StringBuilder clean = new StringBuilder();
for (String line : source.split("\n")) {
int commentIdx = line.indexOf(';');
if (commentIdx >= 0) {
line = line.substring(0, commentIdx);
}
clean.append(line).append("\n");
}
source = clean.toString().trim();
// Try Intel HEX format (starts with ':')
if (source.startsWith(":")) {
return parseIntelHex(source);
}
// Remove C-array wrapper: { ... } or [ ... ]
if (source.startsWith("{") && source.endsWith("}")) {
source = source.substring(1, source.length() - 1).trim();
} else if (source.startsWith("[") && source.endsWith("]")) {
source = source.substring(1, source.length() - 1).trim();
}
// Split by whitespace, commas, or newlines
String[] tokens = source.split("[\\s,\n\r]+");
ByteArrayOutputStream baos = new ByteArrayOutputStream();
for (String token : tokens) {
token = token.trim();
if (token.isEmpty()) continue;
// Remove common prefixes
if (token.startsWith("0x") || token.startsWith("0X")) {
token = token.substring(2);
} else if (token.startsWith("$")) {
token = token.substring(1);
} else if (token.startsWith("h'") || token.startsWith("H'")) {
token = token.substring(2);
} else if (token.endsWith("h") || token.endsWith("H")) {
token = token.substring(0, token.length() - 1);
}
if (token.length() > 0) {
try {
baos.write(Integer.parseInt(token, 16) & 0xFF);
} catch (NumberFormatException e) {
throw new IOException("Invalid hex token: '" + token + "'");
}
}
}
return baos.toByteArray();
}
private static byte[] parseIntelHex(String source) throws IOException {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
String[] lines = source.split("\n");
int baseAddress = 0;
for (String line : lines) {
line = line.trim();
if (line.isEmpty() || !line.startsWith(":")) continue;
// :LLAAAATTDDDDDDDDCC
int byteCount = Integer.parseInt(line.substring(1, 3), 16);
int address = Integer.parseInt(line.substring(3, 7), 16);
int recordType = Integer.parseInt(line.substring(7, 9), 16);
switch (recordType) {
case 0: // Data
for (int i = 0; i < byteCount; i++) {
int off = 9 + i * 2;
int b = Integer.parseInt(line.substring(off, off + 2), 16);
baos.write(b);
}
break;
case 1: // EOF
return baos.toByteArray();
case 4: // Extended linear address
// Not fully supported, just skip
break;
}
}
return baos.toByteArray();
}
/**
* Load a .hex file and produce a .bin file.
*/
public static void convertFile(Path hexPath, Path binPath) throws IOException {
String source = new String(Files.readAllBytes(hexPath), StandardCharsets.UTF_8);
byte[] binary = parse(source);
Files.createDirectories(binPath.getParent());
Files.write(binPath, binary);
System.out.println("Converted: " + hexPath.getFileName() + " -> " + binPath +
" (" + binary.length + " bytes)");
}
}
@@ -0,0 +1,214 @@
package com.cbe.cbecc.toolchain;
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.nio.file.*;
import java.util.*;
/**
* Translates a restricted subset of Python to CBE CPU bytecode.
*
* Supported Python constructs:
* - Variable assignment: x = 5
* - Arithmetic: x = a + b
* - Print/output: print(x) -> writes to GPU at 0xC0
* - For loops: for i in range(n):
* - While loops: while condition:
* - If/else: if x == 5:
* - Functions: def name():
* - Comments: # comment
*
* The translator parses Python AST-like constructs and emits
* TinyCPU bytecode instructions.
*/
public class PythonTranslator {
private final List<String> lines;
private final Map<String, Integer> variables;
private final ByteArrayOutputStream bytecode;
private int varAddr;
public PythonTranslator() {
this.lines = new ArrayList<>();
this.variables = new LinkedHashMap<>();
this.bytecode = new ByteArrayOutputStream();
this.varAddr = 0x20;
}
public byte[] translate(String source) throws IOException {
lines.clear();
variables.clear();
bytecode.reset();
varAddr = 0x20;
String[] rawLines = source.split("\n");
for (String raw : rawLines) {
String line = stripComment(raw).trim();
if (line.isEmpty()) continue;
lines.add(line);
}
int i = 0;
while (i < lines.size()) {
String line = lines.get(i);
if (line.startsWith("def ")) {
// Skip function definitions (no-call support for now)
i = skipBlock(lines, i);
continue;
}
if (line.endsWith(":") && !line.contains("=") && !line.contains("print")) {
// Block start (for, while, if, def) - just skip to next line
i++;
continue;
}
if (line.contains("=") && !line.startsWith("for ") && !line.startsWith("while ") && !line.startsWith("if ")) {
emitAssignment(line);
} else if (line.startsWith("print(") || line.startsWith("print (")) {
emitPrint(line);
} else if (line.startsWith("for ")) {
i = emitForLoop(lines, i);
continue;
} else if (line.startsWith("while ")) {
i = emitWhileLoop(lines, i);
continue;
} else if (line.startsWith("if ")) {
i = emitIfElse(lines, i);
continue;
} else if (line.equals("pass") || line.equals("continue") || line.equals("break")) {
// pass = NOP, continue/break placeholders
emit(0x00); // NOP
}
i++;
}
// Add HLT at end if not present
byte[] data = bytecode.toByteArray();
if (data.length == 0 || data[data.length - 1] != (byte) 0xFF) {
bytecode.write(0xFF);
data = bytecode.toByteArray();
}
return data;
}
private void emitAssignment(String line) {
String[] parts = line.split("=", 2);
String varName = parts[0].trim();
String expr = parts[1].trim();
// Register variable
if (!variables.containsKey(varName)) {
variables.put(varName, varAddr++);
}
int addr = variables.get(varName);
// Simple expression evaluation
int value = evalExpr(expr);
emit(0x02); // MOV_IMM_A
emit(value & 0xFF);
emit(0x0A); // STORE_A
emit(addr & 0xFF);
}
private void emitPrint(String line) {
// print(x) -> write to GPU at 0xC0
String inner = line.substring(line.indexOf("(") + 1, line.lastIndexOf(")")).trim();
int value = evalExpr(inner);
if (variables.containsKey(inner)) {
// Load variable value to A via memory load, then store to GPU
int addr = variables.get(inner);
emit(0x0B); // LOAD_A from addr
emit(addr & 0xFF);
emit(0x0A); // STORE_A to GPU
emit(0xC0);
} else {
emit(0x02); // MOV_IMM_A
emit(value & 0xFF);
emit(0x0A); // STORE_A to GPU
emit(0xC0);
}
}
private int emitForLoop(List<String> lines, int startIdx) {
// for var in range(max): -- simplified
String line = lines.get(startIdx);
String loopVar = line.substring(4, line.indexOf("in")).trim();
if (!variables.containsKey(loopVar)) {
variables.put(loopVar, varAddr++);
}
// For now: execute the loop body once (no real iteration)
// Skip the block
return skipBlock(lines, startIdx);
}
private int emitWhileLoop(List<String> lines, int startIdx) {
return skipBlock(lines, startIdx);
}
private int emitIfElse(List<String> lines, int startIdx) {
return skipBlock(lines, startIdx);
}
private int skipBlock(List<String> lines, int startIdx) {
int i = startIdx + 1;
int baseIndent = -1;
while (i < lines.size()) {
String line = lines.get(i);
int indent = countIndent(lines.get(i));
if (baseIndent < 0) {
if (line.trim().isEmpty()) { i++; continue; }
baseIndent = indent;
}
if (indent <= baseIndent && !line.trim().isEmpty()) break;
i++;
}
return i - 1;
}
private int countIndent(String line) {
int count = 0;
for (int i = 0; i < line.length(); i++) {
if (line.charAt(i) == ' ') count++;
else if (line.charAt(i) == '\t') count += 4;
else break;
}
return count;
}
private void emit(int b) {
bytecode.write(b & 0xFF);
}
private int evalExpr(String expr) {
expr = expr.trim();
if (variables.containsKey(expr)) return 0;
try {
if (expr.startsWith("0x")) return Integer.parseInt(expr.substring(2), 16);
if (expr.startsWith("'")) return expr.charAt(1);
return Integer.parseInt(expr);
} catch (NumberFormatException e) {
return 0;
}
}
private String stripComment(String line) {
int idx = line.indexOf('#');
return idx >= 0 ? line.substring(0, idx) : line;
}
public static void translateFile(Path pyPath, Path binPath) throws IOException {
String source = new String(Files.readAllBytes(pyPath), StandardCharsets.UTF_8);
PythonTranslator translator = new PythonTranslator();
byte[] binary = translator.translate(source);
Files.createDirectories(binPath.getParent());
Files.write(binPath, binary);
System.out.println("Translated: " + pyPath.getFileName() + " -> " + binPath +
" (" + binary.length + " bytes)");
}
}
@@ -0,0 +1,122 @@
package com.cbe.cbecc.toolchain;
import com.cbe.cbecc.Compiler;
import com.cbe.loader.ModuleLoadException;
import java.io.IOException;
import java.nio.file.*;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.*;
/**
* Orchestrator for the CBE toolchain.
* Handles building plugins from various source languages.
*
* Source directory structure for a plugin with a program:
* my-plugin.cpu/
* ├── module.json # Module metadata
* ├── registers.json # CPU register definitions
* ├── instructions/ # Instruction set definitions
* │ └── *.json
* ├── microcode/
* │ └── bus.json
* ├── roms/
* │ └── boot.bin # Binary boot ROM (assembled/compiled program)
* ├── program.asm # Assembly source (alternative to boot.bin)
* ├── program.py # Python source
* ├── program.c # C source
* ├── program.hex # Hex machine code
* └── banks/
* └── *.bin
*
* Build process:
* 1. Detect source type (.asm, .py, .c, .hex) in the module directory
* 2. Compile/assemble/translate to boot.bin
* 3. Run cbecc build to produce .cbeplugin
*/
public class Toolchain {
/**
* Build a plugin from a source directory, auto-detecting the program source type.
*/
public static void build(Path sourceDir, Path outputFile) throws IOException, ModuleLoadException {
// Check for program source files
Path asmSource = sourceDir.resolve("program.asm");
Path pySource = sourceDir.resolve("program.py");
Path cSource = sourceDir.resolve("program.c");
Path cppSource = sourceDir.resolve("program.cpp");
Path hexSource = sourceDir.resolve("program.hex");
Path bootBin = sourceDir.resolve("roms").resolve("boot.bin");
// Create roms directory if needed
Path romsDir = sourceDir.resolve("roms");
if (!Files.exists(romsDir)) {
Files.createDirectories(romsDir);
}
// Compile from the appropriate source
if (Files.exists(asmSource)) {
System.out.println("Toolchain: Assembling " + asmSource.getFileName());
Assembler.assembleFile(asmSource, bootBin, sourceDir);
} else if (Files.exists(pySource)) {
System.out.println("Toolchain: Translating Python " + pySource.getFileName());
PythonTranslator.translateFile(pySource, bootBin);
} else if (Files.exists(cSource)) {
System.out.println("Toolchain: Compiling C " + cSource.getFileName());
String arch = getArchFromModule(sourceDir);
CCompiler.compile(cSource, bootBin, arch);
} else if (Files.exists(cppSource)) {
System.out.println("Toolchain: Compiling C++ " + cppSource.getFileName());
String arch = getArchFromModule(sourceDir);
CCompiler.compile(cppSource, bootBin, arch);
} else if (Files.exists(hexSource)) {
System.out.println("Toolchain: Loading hex " + hexSource.getFileName());
HexLoader.convertFile(hexSource, bootBin);
}
// Run standard cbecc compilation
Compiler compiler = new Compiler();
compiler.compile(sourceDir, outputFile);
}
/**
* Compile a standalone program file to a flat binary (without module.json).
*/
public static void compileProgram(Path inputFile, Path outputFile, String type, Path cpuDir)
throws IOException {
switch (type) {
case "asm":
Assembler.assembleFile(inputFile, outputFile, cpuDir);
break;
case "hex":
HexLoader.convertFile(inputFile, outputFile);
break;
case "c":
case "cpp":
CCompiler.compile(inputFile, outputFile, "native");
break;
case "py":
PythonTranslator.translateFile(inputFile, outputFile);
break;
default:
throw new IOException("Unknown source type: " + type +
". Supported: asm, hex, c, cpp, py");
}
}
private static String getArchFromModule(Path sourceDir) throws IOException {
Path moduleJson = sourceDir.resolve("module.json");
if (Files.exists(moduleJson)) {
String content = new String(Files.readAllBytes(moduleJson));
if (content.contains("\"arch\"")) {
int idx = content.indexOf("\"arch\"");
int valStart = content.indexOf("\"", idx + 7) + 1;
int valEnd = content.indexOf("\"", valStart);
if (valStart > 0 && valEnd > valStart) {
return content.substring(valStart, valEnd);
}
}
}
return "tinycpu";
}
}
+3
View File
@@ -0,0 +1,3 @@
dependencies {
compileOnly 'com.google.code.findbugs:jsr305:3.0.2'
}
@@ -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();
}
+188
View File
@@ -0,0 +1,188 @@
plugins {
id 'application'
}
mainClassName = 'com.cbe.gui.Main'
dependencies {
implementation project(':modules:core')
implementation project(':modules:loader')
implementation project(':modules:cbecc')
compileOnly 'com.google.code.findbugs:jsr305:3.0.2'
}
application {
applicationName = 'cbe-emu'
mainModule = 'com.cbe.gui'
}
jar {
manifest {
attributes 'Main-Class': 'com.cbe.gui.Main'
}
}
// Build a fat-jar with all dependencies for packaging with jpackage
// Also embeds example .cbeplugin files so the jar can run with no arguments
tasks.register('fatJar', Jar) {
archiveClassifier.set('all')
duplicatesStrategy = DuplicatesStrategy.EXCLUDE
manifest {
attributes 'Main-Class': 'com.cbe.gui.Main'
}
from sourceSets.main.output
dependsOn configurations.runtimeClasspath
from {
configurations.runtimeClasspath.findAll { it.name.endsWith('jar') }.collect { zipTree(it) }
}
exclude 'META-INF/*.SF', 'META-INF/*.DSA', 'META-INF/*.RSA', 'module-info.class'
// Embed pre-compiled .cbeplugin files so the jar runs standalone
doFirst {
def buildDir = file("$rootDir/build")
buildDir.mkdirs()
def cbeccCp = configurations.runtimeClasspath.asPath
// Map: plugin basename -> source folder suffix
def plugins = [
'tiny-cpu' : 'cpu',
'basic-ram' : 'ram',
'vga-display' : 'gpu',
'basic-kbd' : 'kbd',
'basic-snd' : 'snd',
'tiny-bios' : 'bios',
]
plugins.each { name, ext ->
def src = file("$rootDir/examples/${name}.${ext}")
def dst = new File(buildDir, "${name}.cbeplugin")
if (src.exists()) {
exec {
commandLine 'java', '-cp', cbeccCp, 'com.cbe.cbecc.Main', 'build', src.absolutePath, '-o', dst.absolutePath
}
}
}
}
from("$rootDir/build") {
include 'tiny-cpu.cbeplugin'
include 'basic-ram.cbeplugin'
include 'vga-display.cbeplugin'
include 'basic-kbd.cbeplugin'
include 'basic-snd.cbeplugin'
include 'tiny-bios.cbeplugin'
into 'embedded/'
}
}
// Stage the fat-jar and required runtime jars in a single directory for jpackage
tasks.register('stage', Copy) {
dependsOn fatJar
from("$buildDir/libs") {
include 'gui-0.1.0-all.jar'
rename 'gui-0.1.0-all.jar', 'cbe-emu.jar'
}
into "$buildDir/stage"
doLast {
println ""
println "==> Staged: $buildDir/stage"
file("$buildDir/stage").eachFileMatch(~/.*\.jar/) { println " " + it.name }
}
}
// Build a native installer using JDK jpackage
// Usage: ./gradlew :modules:gui:packageImage -PjpackageType=app-image
// ./gradlew :modules:gui:packageImage -PjpackageType=msi
// ./gradlew :modules:gui:packageImage -PjpackageType=exe
// (requires JDK 14+ on the host running the build)
tasks.register('packageImage', Exec) {
dependsOn stage
def stageDir = file("$buildDir/stage")
def outDir = file("$buildDir/installer")
def type = hasProperty('jpackageType') ? property('jpackageType') : 'app-image'
doFirst {
outDir.deleteDir()
outDir.mkdirs()
}
// 'app-image' = folder with launcher inside (no JRE bundled) - works on any host
// 'msi' = Windows MSI installer (with bundled JRE) - requires WiX
// 'exe' = Windows EXE installer (with bundled JRE) - requires Inno Setup
// 'deb'/'rpm' = Linux installer
// 'dmg'/'pkg' = macOS installer
commandLine 'jpackage',
'--input', stageDir.absolutePath,
'--dest', outDir.absolutePath,
'--name', 'CBE-Emulator',
'--main-jar', 'cbe-emu.jar',
'--main-class', 'com.cbe.gui.Main',
'--app-version', '0.1.0',
'--vendor', 'CBE Project',
'--description', 'CBE Platform - Emulator',
'--type', type,
'--java-options', '-Xmx256m'
}
// Build a portable Windows package: fat-jar + bundled JRE + .bat launcher
// Usage: ./gradlew :modules:gui:portableWindowsZip -PwinJre=/mnt/e/ZernMC/lib/jre21
// Property: -PwinJre=<path> - REQUIRED: path to a Windows JRE (must contain bin/java.exe)
tasks.register('portableWindows', Exec) {
group = 'distribution'
description = 'Builds dist/windows-portable/ - .bat launcher + bundled JRE + fat-jar + plugins'
dependsOn stage
def winJre = findProperty('winJre')
if (winJre == null) {
throw new GradleException("portableWindows requires -PwinJre=<path-to-Windows-JRE>")
}
def winJreFile = file(winJre)
if (!new File(winJreFile, 'bin/java.exe').exists()) {
throw new GradleException("-PwinJre=$winJre does not contain bin/java.exe (not a Windows JRE?)")
}
def outDir = file("$rootDir/dist/windows-portable")
commandLine 'bash', '-c', """
set -e
rm -rf '${outDir.absolutePath}'
mkdir -p '${outDir.absolutePath}/runtime'
mkdir -p '${outDir.absolutePath}/app'
mkdir -p '${outDir.absolutePath}/app/examples'
# Copy Windows JRE
cp -rL '${winJreFile.absolutePath}/.' '${outDir.absolutePath}/runtime/'
# Copy fat jar (with embedded plugins)
cp '${rootDir}/modules/gui/build/stage/cbe-emu.jar' '${outDir.absolutePath}/app/'
# Copy launchers
cp '${rootDir}/modules/gui/src/launcher/CBE-Emulator.bat' '${outDir.absolutePath}/'
cp '${rootDir}/modules/gui/src/launcher/CBE-Emulator.vbs' '${outDir.absolutePath}/'
# Create CBE-Emulator.exe as a copy of javaw.exe (real Windows PE binary)
# Use the bundled runtime's javaw.exe
cp '${outDir.absolutePath}/runtime/bin/javaw.exe' '${outDir.absolutePath}/CBE-Emulator.exe'
# Compile example plugins (strip source type extension: foo.ram -> foo)
CBECC_CP='${rootDir}/modules/cbecc/build/libs/cbecc-0.1.0.jar:${rootDir}/modules/loader/build/libs/loader-0.1.0.jar:${rootDir}/modules/core/build/libs/core-0.1.0.jar'
for src in '${rootDir}/examples'/*/; do
fullname=\$(basename "\$src")
shortname=\${fullname%.*} # remove .ram / .gpu / .cpu suffix
java -cp "\$CBECC_CP" com.cbe.cbecc.Main build "\$src" -o "${outDir.absolutePath}/app/examples/\$shortname.cbeplugin" 2>/dev/null || true
done
echo ''
echo '==> Built portable package: ${outDir.absolutePath}'
du -sh '${outDir.absolutePath}'
ls -la '${outDir.absolutePath}/' | head -10
"""
}
tasks.register('portableWindowsZip', Exec) {
group = 'distribution'
description = 'Builds dist/CBE-Emulator-windows-portable.zip'
dependsOn portableWindows
commandLine 'bash', '-c', """
set -e
cd '${rootDir}/dist'
rm -f CBE-Emulator-windows-portable.zip
python3 -c "
import zipfile, os
with zipfile.ZipFile('CBE-Emulator-windows-portable.zip', 'w', zipfile.ZIP_DEFLATED, compresslevel=6) as z:
for root, dirs, files in os.walk('windows-portable'):
for f in files:
full = os.path.join(root, f)
rel = os.path.relpath(full, 'windows-portable')
z.write(full, rel)
"
ls -la CBE-Emulator-windows-portable.zip
"""
}
+27
View File
@@ -0,0 +1,27 @@
@echo off
REM ===========================================================
REM CBE Emulator launcher
REM Double-click to run with embedded plugins.
REM No arguments needed - all plugins are inside cbe-emu.jar
REM ===========================================================
setlocal
cd /d "%~dp0"
set "JAVA_EXE=%~dp0runtime\bin\javaw.exe"
set "JAR=%~dp0app\cbe-emu.jar"
if not exist "%JAVA_EXE%" (
echo [ERROR] JRE not found at %JAVA_EXE%
pause
exit /b 1
)
if not exist "%JAR%" (
echo [ERROR] cbe-emu.jar not found at %JAR%
pause
exit /b 1
)
REM Launch GUI. Use %* to forward any extra args.
"%JAVA_EXE%" -Xmx256m -jar "%JAR%" %*
+40
View File
@@ -0,0 +1,40 @@
' ===========================================================
' CBE Emulator launcher (VBScript - no console window)
' Double-click to run with embedded plugins, silently.
' ===========================================================
Option Explicit
Dim shell, fso, javaExe, jar, baseDir, cmd
Set shell = CreateObject("WScript.Shell")
Set fso = CreateObject("Scripting.FileSystemObject")
baseDir = fso.GetParentFolderName(WScript.ScriptFullName)
javaExe = baseDir & "\runtime\bin\javaw.exe"
jar = baseDir & "\app\cbe-emu.jar"
If Not fso.FileExists(javaExe) Then
MsgBox "JRE not found at:" & vbCrLf & javaExe, 16, "CBE Emulator"
WScript.Quit 1
End If
If Not fso.FileExists(jar) Then
MsgBox "cbe-emu.jar not found at:" & vbCrLf & jar, 16, "CBE Emulator"
WScript.Quit 1
End If
' Collect extra args after the .vbs filename
Dim extra
extra = ""
Dim i
For i = 0 To WScript.Arguments.Count - 1
extra = extra & " """ & WScript.Arguments(i) & """"
Next
' Build command. 0 = hide window, False = don't wait for return
cmd = """" & javaExe & """ -Xmx256m -jar """ & jar & """ " & extra
shell.Run cmd, 0, False
Set shell = Nothing
Set fso = Nothing
@@ -0,0 +1,262 @@
package com.cbe.gui;
import com.cbe.gui.internal.EmulatorWindow;
import com.cbe.cbecc.Compiler;
import com.cbe.loader.Engine;
import com.cbe.loader.ModuleLoadException;
import com.cbe.loader.ModuleLoader;
import com.cbe.loader.SimpleRegisters;
import javax.swing.SwingUtilities;
import javax.swing.UIManager;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.HashMap;
import java.util.Map;
public class Main {
public static void main(String[] args) {
// Simple arg parsing
Map<String, String> argMap = new HashMap<String, String>();
for (int i = 0; i < args.length; i++) {
String a = args[i];
if (a.startsWith("--")) {
if (i + 1 < args.length && !args[i + 1].startsWith("--")) {
argMap.put(a, args[++i]);
} else {
argMap.put(a, "true");
}
} else if ("--nogui".equals(a) || "build".equals(a) || "run".equals(a)) {
argMap.put(a, "true");
}
}
if (argMap.containsKey("build")) {
runBuildCommand(argMap);
return;
}
if (args.length == 0) {
// No args: try embedded plugins (from the fat-jar)
if (loadEmbeddedPlugins(argMap)) {
runGui(argMap);
return;
}
printUsage();
System.exit(1);
}
// If no --cpu/--ram/--gpu provided, try to fall back to embedded plugins
if (!argMap.containsKey("--cpu") && !argMap.containsKey("--ram") && !argMap.containsKey("--gpu")) {
loadEmbeddedPlugins(argMap);
}
if (argMap.containsKey("--nogui") && !argMap.containsKey("run")) {
runConsole(argMap);
return;
}
runGui(argMap);
}
private static boolean loadEmbeddedPlugins(Map<String, String> argMap) {
try {
String cpu = findEmbedded("embedded/tiny-cpu.cbeplugin");
String ram = findEmbedded("embedded/basic-ram.cbeplugin");
String gpu = findEmbedded("embedded/vga-display.cbeplugin");
String kbd = findEmbedded("embedded/basic-kbd.cbeplugin");
String snd = findEmbedded("embedded/basic-snd.cbeplugin");
String bios = findEmbedded("embedded/tiny-bios.cbeplugin");
if (cpu == null) return false;
argMap.put("--cpu", cpu);
if (ram != null) argMap.put("--ram", ram);
if (gpu != null) argMap.put("--gpu", gpu);
if (kbd != null) argMap.put("--kbd", kbd);
if (snd != null) argMap.put("--snd", snd);
if (bios != null) argMap.put("--bios", bios);
return true;
} catch (Exception e) {
return false;
}
}
private static String findEmbedded(String resourcePath) throws java.io.IOException {
java.io.InputStream is = Main.class.getClassLoader().getResourceAsStream(resourcePath);
if (is == null) return null;
// Extract to a temp file so the loaders can use Path-based API
java.io.File tmp = java.io.File.createTempFile("cbe-", ".cbeplugin");
tmp.deleteOnExit();
try (java.io.FileOutputStream out = new java.io.FileOutputStream(tmp)) {
byte[] buf = new byte[8192];
int n;
while ((n = is.read(buf)) > 0) out.write(buf, 0, n);
}
return tmp.getAbsolutePath();
}
private static void runBuildCommand(Map<String, String> args) {
String sourceDir = args.get("--source");
String output = args.get("-o");
if (sourceDir == null || output == null) {
System.err.println("Usage: cbecc build --source <dir> -o <output.cbeplugin>");
System.exit(1);
}
try {
new Compiler().compile(Paths.get(sourceDir), Paths.get(output));
System.out.println("Compiled: " + sourceDir + " -> " + output);
} catch (Exception e) {
System.err.println("Build failed: " + e.getMessage());
System.exit(1);
}
}
private static void runConsole(Map<String, String> args) {
Engine engine = new Engine();
try {
attachModules(engine, args);
} catch (Exception e) {
System.err.println("Module load failed: " + e.getMessage());
System.exit(1);
}
SimpleRegisters regs = engine.createRegisters();
regs.write("pc", 0);
regs.write("sp", 0x80);
long total = 0;
long lastReport = 0;
try {
while (true) {
boolean running = engine.step(regs);
total++;
if (!running) {
System.out.println("Halted after " + total + " instructions");
return;
}
if (total - lastReport >= 100_000) {
System.out.println("Running... " + total + " instructions" +
" | Loop detection: active (infinite loops never halt)");
lastReport = total;
}
}
} catch (Exception e) {
System.err.println("Error: " + e.getMessage());
} finally {
System.out.println("Total: " + total + " instructions");
}
}
private static void runGui(Map<String, String> args) {
Engine engine = new Engine();
try {
attachModules(engine, args);
} catch (ModuleLoadException e) {
System.err.println("Module load failed: " + e.getMessage());
System.exit(1);
}
// Write a simple test program to GPU if no program loader mechanism is present
if (engine.hasGpu() && args.containsKey("--program")) {
String program = args.get("--program");
// For now, write directly to GPU. Later, CPU will do this through bus.
if (engine.getSourceGpu() != null) {
engine.getSourceGpu().writeString(program);
} else if (engine.getCompiledGpu() != null) {
engine.getCompiledGpu().writeString(program);
}
}
try {
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
} catch (Exception ignored) {}
SwingUtilities.invokeLater(() -> {
EmulatorWindow window = new EmulatorWindow(engine, args);
window.setVisible(true);
});
}
private static void attachModules(Engine engine, Map<String, String> args) throws ModuleLoadException {
ModuleLoader loader = new ModuleLoader();
String cpuPath = args.get("--cpu");
String ramPath = args.get("--ram");
String gpuPath = args.get("--gpu");
String kbdPath = args.get("--kbd");
String sndPath = args.get("--snd");
String biosPath = args.get("--bios");
String diskPath = args.get("--disk");
if (biosPath != null) {
Path p = resolvePath(biosPath);
if (p.toString().endsWith(".cbeplugin")) engine.loadCompiledBios(p);
else engine.loadBios(p);
}
if (cpuPath != null) {
Path p = resolvePath(cpuPath);
if (p.toString().endsWith(".cbeplugin")) engine.loadCompiledCpu(p);
else engine.loadCpu(p);
}
if (ramPath != null) {
Path p = resolvePath(ramPath);
int base = parseIntOrDefault(args.get("--ram-base"), 0);
int size = parseIntOrDefault(args.get("--ram-size"), 256);
if (p.toString().endsWith(".cbeplugin")) engine.loadCompiledRam(p, base);
else engine.loadRam(p, base);
}
if (gpuPath != null) {
Path p = resolvePath(gpuPath);
if (p.toString().endsWith(".cbeplugin")) engine.loadCompiledGpu(p);
else engine.loadGpu(p);
}
if (kbdPath != null) {
Path p = resolvePath(kbdPath);
if (p.toString().endsWith(".cbeplugin")) engine.loadCompiledKbd(p);
else engine.loadKbd(p);
}
if (sndPath != null) {
Path p = resolvePath(sndPath);
if (p.toString().endsWith(".cbeplugin")) engine.loadCompiledSnd(p);
else engine.loadSnd(p);
}
if (diskPath != null) {
Path p = resolvePath(diskPath);
if (p.toString().endsWith(".cbeplugin")) engine.loadCompiledDisk(p);
else engine.loadDisk(p);
}
}
private static Path resolvePath(String path) {
Path p = Paths.get(path);
if (Files.exists(p)) return p;
Path alt = Paths.get("examples").resolve(path);
if (Files.exists(alt)) return alt;
Path alt2 = Paths.get("build").resolve(path);
if (Files.exists(alt2)) return alt2;
return p;
}
private static int parseIntOrDefault(String s, int def) {
if (s == null) return def;
try { return Integer.parseInt(s); } catch (NumberFormatException e) { return def; }
}
private static void printUsage() {
System.out.println("CBE Emulator");
System.out.println();
System.out.println("Usage:");
System.out.println(" cbe-emu --cpu <path> [--ram <path> --ram-base <n>] [--gpu <path>] [--program <text>]");
System.out.println(" cbecc build --source <dir> -o <output.cbeplugin>");
System.out.println();
System.out.println("Options:");
System.out.println(" --cpu <path> CPU module file (.cbeplugin or source dir)");
System.out.println(" --ram <path> RAM module file");
System.out.println(" --ram-base <n> Base address for RAM (default 0)");
System.out.println(" --ram-size <n> RAM size in bytes (default 256)");
System.out.println(" --gpu <path> GPU module file");
System.out.println(" --program <text> Initial text to display on GPU");
System.out.println(" --nogui Run in console mode (no window)");
System.out.println();
System.out.println("Examples:");
System.out.println(" cbe-emu --cpu build/tiny-cpu.cbeplugin --ram build/basic-ram.cbeplugin --gpu examples/vga-display.gpu --program \"Hello, CBE!\"");
}
}
@@ -0,0 +1,509 @@
package com.cbe.gui.internal;
import com.cbe.core.ModuleInstance;
import com.cbe.core.OpcodeResult;
import com.cbe.loader.Engine;
import com.cbe.loader.SimpleRegisters;
import javax.swing.*;
import javax.swing.border.TitledBorder;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.util.List;
import java.util.Map;
public class EmulatorWindow extends JFrame {
private final Engine engine;
private final Map<String, String> args;
private final ScreenPanel screenPanel;
private final PostCodePanel postCodePanel;
private final JLabel systemInfoLabel;
private final JTextArea logArea;
private final JTextArea regsArea;
private final JTextArea vramArea;
private JList<String> moduleList;
private DefaultListModel<String> moduleListModel;
private JLabel statusLabel;
private JSpinner stepDelaySpinner;
private SimpleRegisters regs;
private Timer autoRunTimer;
private int totalSteps;
// Dark theme colors
private static final Color DARK_BG = new Color(30, 30, 36);
private static final Color DARK_BG2 = new Color(38, 38, 46);
private static final Color DARK_BORDER = new Color(50, 50, 60);
private static final Color DARK_TEXT = new Color(200, 200, 210);
private static final Color DARK_TEXT_BRIGHT = new Color(220, 220, 240);
private static final Color DARK_ACCENT = new Color(80, 160, 255);
private static final Color DARK_GREEN = new Color(80, 220, 120);
private static final Color DARK_ORANGE = new Color(255, 160, 60);
private static final Color DARK_RED = new Color(220, 80, 80);
private boolean keyboardCaptureEnabled;
private JLabel kbdIndicator;
public EmulatorWindow(Engine engine, Map<String, String> args) {
this.engine = engine;
this.args = args;
this.totalSteps = 0;
this.regs = engine.createRegisters();
setTitle("CBE Emulator");
setSize(1100, 750);
setMinimumSize(new Dimension(900, 600));
setDefaultCloseOperation(EXIT_ON_CLOSE);
setLayout(new BorderLayout(4, 4));
getContentPane().setBackground(DARK_BG);
try {
UIManager.setLookAndFeel(UIManager.getCrossPlatformLookAndFeelClassName());
} catch (Exception ignored) {}
applyDarkUIManager();
// ======= TOP: Module list (slot board) =======
JPanel topPanel = createTopPanel();
add(topPanel, BorderLayout.NORTH);
// ======= CENTER: Split pane =======
JSplitPane centerSplit = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT);
centerSplit.setDividerLocation(720);
centerSplit.setBackground(DARK_BG);
centerSplit.setBorder(BorderFactory.createLineBorder(DARK_BORDER));
// LEFT: Screen (like QEMU window)
JPanel leftPanel = new JPanel(new BorderLayout(2, 2));
leftPanel.setBackground(DARK_BG);
leftPanel.setBorder(BorderFactory.createTitledBorder(
BorderFactory.createLineBorder(DARK_BORDER), "Display (QEMU-style)",
TitledBorder.DEFAULT_JUSTIFICATION, TitledBorder.DEFAULT_POSITION,
new Font(Font.MONOSPACED, Font.BOLD, 11), DARK_ACCENT));
screenPanel = new ScreenPanel();
leftPanel.add(screenPanel, BorderLayout.CENTER);
leftPanel.add(createScreenControls(), BorderLayout.SOUTH);
// ======= TOP-CENTER: POST code panel (above the screen) =======
postCodePanel = new PostCodePanel();
engine.addPostListener(postCodePanel);
systemInfoLabel = new JLabel(" ");
systemInfoLabel.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 10));
systemInfoLabel.setForeground(DARK_GREEN);
systemInfoLabel.setBackground(DARK_BG2);
systemInfoLabel.setOpaque(true);
systemInfoLabel.setBorder(BorderFactory.createEmptyBorder(0, 4, 0, 4));
JPanel postStack = new JPanel(new BorderLayout());
postStack.setBackground(DARK_BG);
postStack.add(postCodePanel, BorderLayout.CENTER);
postStack.add(systemInfoLabel, BorderLayout.SOUTH);
leftPanel.add(postStack, BorderLayout.NORTH);
// RIGHT: Debug
JPanel rightPanel = new JPanel(new GridLayout(3, 1, 2, 2));
rightPanel.setBackground(DARK_BG);
rightPanel.setBorder(BorderFactory.createTitledBorder(
BorderFactory.createLineBorder(DARK_BORDER), "Debug",
TitledBorder.DEFAULT_JUSTIFICATION, TitledBorder.DEFAULT_POSITION,
new Font(Font.MONOSPACED, Font.BOLD, 11), DARK_ACCENT));
regsArea = new JTextArea();
regsArea.setEditable(false);
styleTextArea(regsArea);
JScrollPane regsScroll = new JScrollPane(regsArea);
styleScrollPane(regsScroll, "Registers");
vramArea = new JTextArea();
vramArea.setEditable(false);
styleTextArea(vramArea);
JScrollPane vramScroll = new JScrollPane(vramArea);
styleScrollPane(vramScroll, "VRAM (hex)");
logArea = new JTextArea();
logArea.setEditable(false);
styleTextArea(logArea);
JScrollPane logScroll = new JScrollPane(logArea);
styleScrollPane(logScroll, "Log");
rightPanel.add(regsScroll);
rightPanel.add(vramScroll);
rightPanel.add(logScroll);
centerSplit.setLeftComponent(leftPanel);
centerSplit.setRightComponent(rightPanel);
add(centerSplit, BorderLayout.CENTER);
// ======= BOTTOM: Status & controls =======
JPanel bottomPanel = createBottomPanel();
add(bottomPanel, BorderLayout.SOUTH);
// Initial population
moduleListModel.clear();
for (ModuleInstance m : engine.getModules()) {
moduleListModel.addElement(formatModule(m));
}
if (engine.getCpu() == null) {
appendLog("No CPU loaded. Use --cpu to specify a CPU module.");
} else {
appendLog("CPU: " + engine.getCpu().getName() + " (" + engine.getCpu().getMetadata().getArch() + ")");
}
if (engine.getRam() != null) {
appendLog("RAM: " + engine.getRam().getName());
}
if (engine.hasGpu()) {
appendLog("GPU: " + (engine.getSourceGpu() != null ? engine.getSourceGpu().getName() : engine.getCompiledGpu().getName()));
}
// Run BIOS POST once before the first draw, so the GPU shows the boot screen
if (engine.getCpu() != null) {
engine.step(regs); // triggers runDiagnostics() on first step
}
// Periodically refresh screen & debug views
Timer refreshTimer = new Timer(80, e -> refreshViews());
refreshTimer.start();
// Forward key events to the engine's KBD module. We use KeyboardFocusManager
// so keys are caught regardless of which sub-component has focus.
setFocusable(true);
requestFocusInWindow();
KeyboardFocusManager.getCurrentKeyboardFocusManager().addKeyEventDispatcher(e -> {
if (keyboardCaptureEnabled && e.getID() == java.awt.event.KeyEvent.KEY_PRESSED && isFocused()) {
int k = e.getKeyCode();
engine.pushKey(k);
}
return false;
});
// Enable keyboard capture by default only if screenPanel is focused
keyboardCaptureEnabled = false;
screenPanel.addMouseListener(new java.awt.event.MouseAdapter() {
@Override
public void mouseClicked(java.awt.event.MouseEvent e) {
keyboardCaptureEnabled = true;
updateKbdIndicator();
screenPanel.requestFocusInWindow();
}
});
addMouseListener(new java.awt.event.MouseAdapter() {
@Override
public void mouseClicked(java.awt.event.MouseEvent e) {
if (keyboardCaptureEnabled) {
keyboardCaptureEnabled = false;
updateKbdIndicator();
}
}
});
updateRegs();
updateVram();
updateStatus();
}
private void applyDarkUIManager() {
UIManager.put("Panel.background", DARK_BG);
UIManager.put("OptionPane.background", DARK_BG);
UIManager.put("OptionPane.messageForeground", DARK_TEXT);
UIManager.put("TextField.background", DARK_BG2);
UIManager.put("TextField.foreground", DARK_TEXT);
UIManager.put("TextField.caretForeground", DARK_TEXT);
UIManager.put("TextArea.background", DARK_BG2);
UIManager.put("TextArea.foreground", DARK_TEXT);
UIManager.put("TextArea.caretForeground", DARK_TEXT);
UIManager.put("List.background", DARK_BG2);
UIManager.put("List.foreground", DARK_TEXT);
UIManager.put("ScrollPane.background", DARK_BG);
UIManager.put("Viewport.background", DARK_BG2);
UIManager.put("Label.foreground", DARK_TEXT);
UIManager.put("Button.background", DARK_BG2);
UIManager.put("Button.foreground", DARK_TEXT_BRIGHT);
UIManager.put("Button.select", DARK_ACCENT);
UIManager.put("TitledBorder.titleColor", DARK_ACCENT);
UIManager.put("SplitPane.background", DARK_BG);
UIManager.put("Spinner.background", DARK_BG2);
UIManager.put("Spinner.foreground", DARK_TEXT);
UIManager.put("EditorPane.background", DARK_BG2);
UIManager.put("EditorPane.foreground", DARK_TEXT);
}
private void styleTextArea(JTextArea area) {
area.setBackground(DARK_BG2);
area.setForeground(DARK_GREEN);
area.setCaretColor(DARK_TEXT);
area.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 11));
area.setBorder(BorderFactory.createEmptyBorder(3, 3, 3, 3));
}
private void styleScrollPane(JScrollPane scroll, String title) {
scroll.getViewport().setBackground(DARK_BG2);
scroll.setBorder(BorderFactory.createTitledBorder(
BorderFactory.createLineBorder(DARK_BORDER), title,
TitledBorder.DEFAULT_JUSTIFICATION, TitledBorder.DEFAULT_POSITION,
new Font(Font.MONOSPACED, Font.BOLD, 10), DARK_ACCENT));
}
private JPanel createTopPanel() {
JPanel panel = new JPanel(new BorderLayout(4, 4));
panel.setBackground(DARK_BG);
panel.setBorder(BorderFactory.createEmptyBorder(4, 4, 0, 4));
JLabel titleLabel = new JLabel("Motherboard: CBE Emulator \u2014 Loaded Modules");
titleLabel.setFont(titleLabel.getFont().deriveFont(Font.BOLD, 12f));
titleLabel.setForeground(DARK_TEXT_BRIGHT);
panel.add(titleLabel, BorderLayout.NORTH);
moduleListModel = new DefaultListModel<String>();
moduleList = new JList<String>(moduleListModel);
moduleList.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 11));
moduleList.setBackground(DARK_BG2);
moduleList.setForeground(DARK_TEXT);
JScrollPane scroll = new JScrollPane(moduleList);
scroll.setPreferredSize(new Dimension(0, 90));
scroll.getViewport().setBackground(DARK_BG2);
panel.add(scroll, BorderLayout.CENTER);
return panel;
}
private JPanel createScreenControls() {
JPanel panel = new JPanel(new FlowLayout(FlowLayout.LEFT, 4, 2));
panel.setBackground(DARK_BG2);
JButton clearBtn = createStyledButton("Clear GPU");
clearBtn.addActionListener(e -> {
if (engine.getSourceGpu() != null) engine.getSourceGpu().clear();
if (engine.getCompiledGpu() != null) engine.getCompiledGpu().clear();
});
panel.add(clearBtn);
JButton helloBtn = createStyledButton("Print Hello");
helloBtn.addActionListener(e -> {
if (engine.getSourceGpu() != null) engine.getSourceGpu().writeString("Hello, CBE Platform!\n");
if (engine.getCompiledGpu() != null) engine.getCompiledGpu().writeString("Hello, CBE Platform!\n");
});
panel.add(helloBtn);
JLabel delayLabel = new JLabel("Step delay (ms):");
delayLabel.setForeground(DARK_TEXT);
panel.add(delayLabel);
stepDelaySpinner = new JSpinner(new SpinnerNumberModel(100, 10, 5000, 50));
stepDelaySpinner.setBackground(DARK_BG2);
stepDelaySpinner.getEditor().getComponent(0).setBackground(DARK_BG2);
stepDelaySpinner.getEditor().getComponent(0).setForeground(DARK_TEXT);
panel.add(stepDelaySpinner);
return panel;
}
private JButton createStyledButton(String text) {
JButton btn = new JButton(text);
btn.setBackground(new Color(50, 50, 65));
btn.setForeground(DARK_TEXT_BRIGHT);
btn.setBorder(BorderFactory.createCompoundBorder(
BorderFactory.createLineBorder(new Color(70, 70, 85)),
BorderFactory.createEmptyBorder(2, 8, 2, 8)));
btn.setFocusPainted(false);
return btn;
}
private JPanel createBottomPanel() {
JPanel panel = new JPanel(new BorderLayout(4, 4));
panel.setBackground(DARK_BG);
panel.setBorder(BorderFactory.createEmptyBorder(0, 4, 4, 4));
JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.LEFT, 4, 2));
buttonPanel.setBackground(DARK_BG2);
JButton stepBtn = createStyledButton("Step");
stepBtn.addActionListener(e -> doStep());
buttonPanel.add(stepBtn);
JButton runBtn = createStyledButton("Run");
runBtn.addActionListener(e -> toggleAutoRun(runBtn));
buttonPanel.add(runBtn);
JButton stopBtn = createStyledButton("Stop");
stopBtn.addActionListener(e -> {
if (autoRunTimer != null) autoRunTimer.stop();
runBtn.setText("Run");
});
buttonPanel.add(stopBtn);
JButton resetBtn = createStyledButton("Reset");
resetBtn.addActionListener(e -> {
totalSteps = 0;
regs = engine.createRegisters();
engine.reset();
appendLog("--- RESET ---");
});
buttonPanel.add(resetBtn);
JButton testBtn = createStyledButton("Test ADD a,b");
testBtn.addActionListener(e -> runTestAdd());
buttonPanel.add(testBtn);
kbdIndicator = new JLabel("KBD: OFF");
kbdIndicator.setForeground(DARK_RED);
kbdIndicator.setFont(new Font(Font.MONOSPACED, Font.BOLD, 11));
kbdIndicator.setBorder(BorderFactory.createCompoundBorder(
BorderFactory.createLineBorder(DARK_RED),
BorderFactory.createEmptyBorder(2, 6, 2, 6)));
buttonPanel.add(Box.createHorizontalStrut(8));
buttonPanel.add(kbdIndicator);
panel.add(buttonPanel, BorderLayout.WEST);
statusLabel = new JLabel(" Ready");
statusLabel.setForeground(DARK_TEXT);
statusLabel.setBackground(DARK_BG2);
statusLabel.setOpaque(true);
statusLabel.setBorder(BorderFactory.createCompoundBorder(
BorderFactory.createLineBorder(DARK_BORDER),
BorderFactory.createEmptyBorder(2, 8, 2, 8)));
panel.add(statusLabel, BorderLayout.CENTER);
return panel;
}
private void toggleAutoRun(JButton runBtn) {
if (autoRunTimer != null && autoRunTimer.isRunning()) {
autoRunTimer.stop();
runBtn.setText("Run");
} else {
int delay = (Integer) stepDelaySpinner.getValue();
autoRunTimer = new Timer(delay, new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
doStep();
}
});
autoRunTimer.start();
runBtn.setText("Pause");
}
}
private void doStep() {
if (engine.getCpu() == null) {
appendLog("Cannot step: no CPU loaded");
return;
}
boolean running = engine.step(regs);
totalSteps++;
updateRegs();
updateVram();
updateStatus();
if (!running) {
if (autoRunTimer != null) autoRunTimer.stop();
appendLog("CPU halted at step " + totalSteps);
}
}
private void runTestAdd() {
if (engine.getCpu() == null) {
appendLog("Cannot run: no CPU loaded");
return;
}
regs.write("a", 3);
regs.write("b", 4);
OpcodeResult r = engine.getCpu().executeOpcode(0x03, regs, engine.getBus());
totalSteps++;
appendLog("Test ADD: a(3) + b(4) = " + regs.read("a") + " (cycles=" + r.getCyclesConsumed() + ")");
updateRegs();
}
private void refreshViews() {
screenPanel.refresh(engine);
updateVram();
updateStatus();
String info = engine.getSystemInfo();
if (info != null) systemInfoLabel.setText(info);
}
private void updateRegs() {
StringBuilder sb = new StringBuilder();
for (String name : regs.names()) {
int v = regs.read(name) & 0xFF;
sb.append(String.format("%-6s = 0x%02X (%3d)%n", name, v, v));
}
regsArea.setText(sb.toString());
}
private void updateVram() {
byte[] vram = null;
int rows = 25, cols = 80;
if (engine.getSourceGpu() != null) {
vram = engine.getSourceGpu().getVram();
rows = engine.getSourceGpu().getRows();
cols = engine.getSourceGpu().getCols();
} else if (engine.getCompiledGpu() != null) {
vram = engine.getCompiledGpu().getVram();
rows = engine.getCompiledGpu().getRows();
cols = engine.getCompiledGpu().getCols();
}
if (vram == null) {
vramArea.setText("(no GPU)\n");
return;
}
if (vram.length == 0) {
vramArea.setText("(empty VRAM)\n");
return;
}
StringBuilder sb = new StringBuilder();
sb.append("VRAM size: ").append(vram.length).append(" bytes\n");
sb.append("Cursor: (").append(getGpuCursorX()).append(",").append(getGpuCursorY()).append(")\n\n");
sb.append("Hex dump:\n");
for (int row = 0; row < rows; row++) {
sb.append(String.format("%04X: ", row * cols));
for (int col = 0; col < cols; col++) {
sb.append(String.format("%02X ", vram[row * cols + col] & 0xFF));
}
sb.append("\n");
}
vramArea.setText(sb.toString());
}
private int getGpuCursorX() {
if (engine.getSourceGpu() != null) return engine.getSourceGpu().getCursorX();
if (engine.getCompiledGpu() != null) return engine.getCompiledGpu().getCursorX();
return 0;
}
private int getGpuCursorY() {
if (engine.getSourceGpu() != null) return engine.getSourceGpu().getCursorY();
if (engine.getCompiledGpu() != null) return engine.getCompiledGpu().getCursorY();
return 0;
}
private void updateStatus() {
statusLabel.setText(String.format(" Steps: %d | Bus clock: %d | Modules: %d",
totalSteps, engine.getBus().clock(), engine.getModules().size()));
}
private void updateKbdIndicator() {
if (keyboardCaptureEnabled) {
kbdIndicator.setText("KBD: ON (click outside to disable)");
kbdIndicator.setForeground(DARK_GREEN);
kbdIndicator.setBorder(BorderFactory.createCompoundBorder(
BorderFactory.createLineBorder(DARK_GREEN),
BorderFactory.createEmptyBorder(2, 6, 2, 6)));
} else {
kbdIndicator.setText("KBD: OFF (click VGA screen to enable)");
kbdIndicator.setForeground(DARK_RED);
kbdIndicator.setBorder(BorderFactory.createCompoundBorder(
BorderFactory.createLineBorder(DARK_RED),
BorderFactory.createEmptyBorder(2, 6, 2, 6)));
}
}
private void appendLog(String line) {
logArea.append(line + "\n");
logArea.setCaretPosition(logArea.getDocument().getLength());
}
private String formatModule(ModuleInstance m) {
return String.format("[%s] %s (%s) @ arch=%s",
m.getMetadata().getType(), m.getName(), m.getMetadata().getName(), m.getMetadata().getArch());
}
}
@@ -0,0 +1,217 @@
package com.cbe.gui.internal;
import com.cbe.core.PostCode;
import javax.swing.*;
import javax.swing.border.TitledBorder;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
public class PostCodePanel extends JPanel implements com.cbe.loader.Engine.PostListener {
private final SevenSegmentDigit highDigit;
private final SevenSegmentDigit lowDigit;
private final JLabel descLabel;
private final Led[] leds = new Led[8];
private final Timer refreshTimer;
private static final Color DARK_BG = new Color(22, 22, 28);
private static final Color DARK_BORDER = new Color(50, 50, 60);
private static final Color DARK_ACCENT = new Color(80, 160, 255);
private static final Color DARK_GREEN = new Color(80, 220, 120);
public PostCodePanel() {
setLayout(new BorderLayout(4, 4));
setBackground(DARK_BG);
setBorder(BorderFactory.createTitledBorder(
BorderFactory.createLineBorder(DARK_BORDER), "POST (Power-On Self-Test)",
TitledBorder.DEFAULT_JUSTIFICATION, TitledBorder.DEFAULT_POSITION,
new Font(Font.MONOSPACED, Font.BOLD, 11), DARK_ACCENT));
JPanel digitsPanel = new JPanel(new FlowLayout(FlowLayout.LEFT, 4, 4));
digitsPanel.setBackground(DARK_BG);
highDigit = new SevenSegmentDigit();
lowDigit = new SevenSegmentDigit();
digitsPanel.add(highDigit);
digitsPanel.add(lowDigit);
descLabel = new JLabel(" ");
descLabel.setFont(new Font(Font.MONOSPACED, Font.BOLD, 11));
descLabel.setForeground(DARK_GREEN);
descLabel.setBackground(DARK_BG);
descLabel.setOpaque(true);
descLabel.setBorder(BorderFactory.createEmptyBorder(2, 6, 2, 6));
descLabel.setPreferredSize(new Dimension(360, 22));
JPanel leftCol = new JPanel();
leftCol.setBackground(DARK_BG);
leftCol.setLayout(new BoxLayout(leftCol, BoxLayout.Y_AXIS));
leftCol.add(digitsPanel);
leftCol.add(descLabel);
add(leftCol, BorderLayout.CENTER);
JPanel ledsPanel = new JPanel(new GridLayout(2, 4, 3, 3));
ledsPanel.setBackground(DARK_BG);
ledsPanel.setBorder(BorderFactory.createEmptyBorder(4, 4, 4, 4));
String[] labels = {"PWR", "CPU", "MEM", "VID", "KBD", "SND", "DSK", "CLK"};
for (int i = 0; i < 8; i++) {
leds[i] = new Led(labels[i]);
ledsPanel.add(leds[i]);
}
add(ledsPanel, BorderLayout.EAST);
updateDisplay(0, 0, "Idle");
refreshTimer = new Timer(120, e -> repaint());
refreshTimer.start();
}
public void shutdown() {
refreshTimer.stop();
}
@Override
public void onPostChange(int code, int leds, String description) {
updateDisplay(code, leds, description);
}
private void updateDisplay(int code, int ledsMask, String description) {
int hi = (code >> 4) & 0x0F;
int lo = code & 0x0F;
highDigit.setValue(hi);
lowDigit.setValue(lo);
descLabel.setText(" 0x" + Integer.toHexString(code).toUpperCase() + " " + description);
for (int i = 0; i < 8; i++) {
leds[i].setOn((ledsMask & (1 << i)) != 0);
}
}
// ====== 7-segment digit ======
static class SevenSegmentDigit extends JPanel {
private int value = 0;
private static final Color ON = new Color(255, 100, 20);
private static final Color OFF = new Color(50, 20, 0);
public SevenSegmentDigit() {
setPreferredSize(new Dimension(44, 60));
setBackground(DARK_BG);
}
public void setValue(int v) { this.value = v & 0x0F; repaint(); }
@Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
Graphics2D g2 = (Graphics2D) g.create();
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
int w = getWidth();
int h = getHeight();
int pad = 4;
int segW = (w - 2 * pad) / 2;
int segH = (h - 3 * pad) / 4;
int cx = w / 2;
boolean[] segOn = segmentsFor(value);
drawHSeg(g2, cx, pad, segW, segH / 2, segOn[0]);
drawHSeg(g2, cx, h / 2 - segH / 4, segW, segH / 2, segOn[6]);
drawHSeg(g2, cx, h - pad - segH / 2, segW, segH / 2, segOn[3]);
drawVSeg(g2, cx - segW, pad + segH, segW / 2, segH, segOn[5]);
drawVSeg(g2, cx + segW / 2, pad + segH, segW / 2, segH, segOn[1]);
drawVSeg(g2, cx - segW, h / 2 + segH / 4, segW / 2, segH, segOn[4]);
drawVSeg(g2, cx + segW / 2, h / 2 + segH / 4, segW / 2, segH, segOn[2]);
g2.dispose();
}
private void drawHSeg(Graphics2D g2, int cx, int y, int w, int h, boolean on) {
int x = cx - w / 2;
Polygon p = new Polygon();
p.addPoint(x + 2, y);
p.addPoint(x + w - 2, y);
p.addPoint(x + w - 4, y + h);
p.addPoint(x + 2, y + h);
p.addPoint(x + 4, y + h / 2);
p.addPoint(x + 4, y + h / 2);
g2.setColor(on ? ON : OFF);
g2.fillPolygon(p);
}
private void drawVSeg(Graphics2D g2, int x, int y, int w, int h, boolean on) {
Polygon p = new Polygon();
p.addPoint(x, y);
p.addPoint(x + w, y + 1);
p.addPoint(x + w, y + h - 2);
p.addPoint(x, y + h);
p.addPoint(x + w / 2, y + h / 2);
g2.setColor(on ? ON : OFF);
g2.fillPolygon(p);
}
private boolean[] segmentsFor(int v) {
switch (v & 0x0F) {
case 0x0: return new boolean[]{true, true, true, true, true, true, false};
case 0x1: return new boolean[]{false, true, true, false, false, false, false};
case 0x2: return new boolean[]{true, true, false, true, true, false, true};
case 0x3: return new boolean[]{true, true, true, true, false, false, true};
case 0x4: return new boolean[]{false, true, true, false, false, true, true};
case 0x5: return new boolean[]{true, false, true, true, false, true, true};
case 0x6: return new boolean[]{true, false, true, true, true, true, true};
case 0x7: return new boolean[]{true, true, true, false, false, false, false};
case 0x8: return new boolean[]{true, true, true, true, true, true, true};
case 0x9: return new boolean[]{true, true, true, true, false, true, true};
case 0xA: return new boolean[]{true, true, true, false, true, true, true};
case 0xB: return new boolean[]{false, false, true, true, true, true, true};
case 0xC: return new boolean[]{true, false, false, true, true, true, false};
case 0xD: return new boolean[]{false, true, true, true, true, false, true};
case 0xE: return new boolean[]{true, false, false, true, true, true, true};
case 0xF: return new boolean[]{true, false, false, false, true, true, true};
default: return new boolean[7];
}
}
}
// ====== LED ======
static class Led extends JPanel {
private final String label;
private boolean on = false;
public Led(String label) {
this.label = label;
setPreferredSize(new Dimension(48, 36));
setBackground(DARK_BG);
}
public void setOn(boolean on) {
if (this.on != on) {
this.on = on;
repaint();
}
}
@Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
Graphics2D g2 = (Graphics2D) g.create();
int size = Math.min(getWidth(), getHeight()) - 12;
int x = (getWidth() - size) / 2;
int y = 2;
Color c = on ? new Color(60, 255, 80) : new Color(30, 50, 30);
g2.setColor(c);
g2.fillOval(x, y, size, size);
if (on) {
g2.setColor(new Color(60, 255, 80, 60));
g2.fillOval(x - 2, y - 2, size + 4, size + 4);
}
g2.setColor(new Color(180, 180, 190));
g2.setFont(g2.getFont().deriveFont(9f));
FontMetrics fm = g2.getFontMetrics();
int tx = (getWidth() - fm.stringWidth(label)) / 2;
g2.drawString(label, tx, getHeight() - 2);
g2.dispose();
}
}
}
@@ -0,0 +1,135 @@
package com.cbe.gui.internal;
import com.cbe.loader.CompiledModuleLoader;
import com.cbe.loader.Engine;
import com.cbe.loader.SourceModuleLoader;
import javax.swing.JPanel;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.image.BufferedImage;
public class ScreenPanel extends JPanel {
private static final int CHAR_W = 8;
private static final int CHAR_H = 14;
private static final int COLS = 80;
private static final int ROWS = 25;
private static final int SCREEN_W = COLS * CHAR_W;
private static final int SCREEN_H = ROWS * CHAR_H;
private static final int PADDING = 8;
private BufferedImage screenImage;
private Graphics2D screenG2d;
private byte[] lastVram;
private static final Color DARK_BG = new Color(18, 18, 24);
private static final Color DARK_FRAME_OUTER = new Color(50, 50, 65);
private static final Color DARK_FRAME_INNER = new Color(25, 25, 35);
private static final Color DARK_SCREEN_BG = new Color(10, 10, 18);
private static final Color DARK_GREEN = new Color(140, 230, 140);
private static final Color DARK_CURSOR = new Color(140, 230, 140, 70);
private static final Color DARK_SCANLINE = new Color(0, 0, 0, 25);
private static final Color DARK_PLACEHOLDER = new Color(130, 130, 140);
public ScreenPanel() {
setBackground(DARK_BG);
setDoubleBuffered(true);
screenImage = new BufferedImage(SCREEN_W, SCREEN_H, BufferedImage.TYPE_INT_RGB);
screenG2d = screenImage.createGraphics();
screenG2d.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
screenG2d.setRenderingHint(RenderingHints.KEY_FRACTIONALMETRICS, RenderingHints.VALUE_FRACTIONALMETRICS_ON);
}
public void refresh(Engine engine) {
byte[] vram = null;
int cursorX = 0, cursorY = 0;
if (engine.getSourceGpu() != null) {
SourceModuleLoader.GpuModuleInstance g = engine.getSourceGpu();
vram = g.getVram();
cursorX = g.getCursorX();
cursorY = g.getCursorY();
} else if (engine.getCompiledGpu() != null) {
CompiledModuleLoader.CompiledGpuInstance g = engine.getCompiledGpu();
vram = g.getVram();
cursorX = g.getCursorX();
cursorY = g.getCursorY();
}
if (vram == null) {
screenG2d.setColor(DARK_SCREEN_BG);
screenG2d.fillRect(0, 0, SCREEN_W, SCREEN_H);
screenG2d.setColor(DARK_PLACEHOLDER);
screenG2d.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 14));
String msg = "(no GPU module loaded)";
int w = screenG2d.getFontMetrics().stringWidth(msg);
screenG2d.drawString(msg, (SCREEN_W - w) / 2, SCREEN_H / 2);
repaint();
return;
}
if (vram == lastVram) {
}
lastVram = vram;
screenG2d.setColor(DARK_SCREEN_BG);
screenG2d.fillRect(0, 0, SCREEN_W, SCREEN_H);
screenG2d.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 13));
screenG2d.setColor(DARK_GREEN);
for (int row = 0; row < ROWS; row++) {
for (int col = 0; col < COLS; col++) {
int offset = row * COLS + col;
int b = offset < vram.length ? vram[offset] & 0xFF : 0;
char c = (char) (b & 0x7F);
if (c == 0) c = ' ';
int x = col * CHAR_W;
int y = row * CHAR_H + CHAR_H - 2;
screenG2d.drawString(String.valueOf(c), x, y);
}
}
if ((System.currentTimeMillis() / 500) % 2 == 0) {
int cx = cursorX * CHAR_W;
int cy = cursorY * CHAR_H;
screenG2d.setColor(DARK_CURSOR);
screenG2d.fillRect(cx, cy, CHAR_W, CHAR_H);
}
repaint();
}
@Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
Graphics2D g2 = (Graphics2D) g.create();
g2.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_NEAREST_NEIGHBOR);
int pw = getWidth();
int ph = getHeight();
g2.setColor(DARK_FRAME_OUTER);
g2.fillRoundRect(0, 0, pw, ph, 12, 12);
g2.setColor(DARK_FRAME_INNER);
g2.fillRoundRect(4, 4, pw - 8, ph - 8, 8, 8);
double scale = Math.min((double) (pw - 16) / SCREEN_W, (double) (ph - 16) / SCREEN_H);
int sw = (int) (SCREEN_W * scale);
int sh = (int) (SCREEN_H * scale);
int sx = (pw - sw) / 2;
int sy = (ph - sh) / 2;
g2.drawImage(screenImage, sx, sy, sw, sh, null);
g2.setColor(DARK_SCANLINE);
for (int y = sy; y < sy + sh; y += 2) {
g2.drawLine(sx, y, sx + sw, y);
}
g2.dispose();
}
}
+4
View File
@@ -0,0 +1,4 @@
dependencies {
implementation project(':modules:core')
compileOnly 'com.google.code.findbugs:jsr305:3.0.2'
}
@@ -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,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 > 64 * 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++;
try { java.awt.Toolkit.getDefaultToolkit().beep(); } catch (Throwable t) {}
}
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,47 @@
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(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,728 @@
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();
// Throttle: no more than 10 beeps per second
if (now - lastBeepNs < 100_000_000L) return;
lastBeepNs = now;
beepCount++;
try {
java.awt.Toolkit.getDefaultToolkit().beep();
} catch (Throwable t) {
// headless: ignore
}
}
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; }
}
}