inital commit кек
This commit is contained in:
@@ -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";
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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
|
||||
"""
|
||||
}
|
||||
@@ -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%" %*
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user