inital commit кек
This commit is contained in:
@@ -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