I’m trying to recreate the DCPU-16 for Unity that has a console and can render the output to a TMPro text on a canvas. What the code should do is display A B C onto the TMPro text, but nothing appears. I’m pretty new to this and don’t completely understand what I’m doing. If you can help me fix my code or give me some recommendations for learning more about this, I would greatly appreciate it. If anything is unclear, leave a comment and I will try to help.
I made most of this code about 2 years ago, so it might be a little messy.
Here is the DCPU-16:
using System;
using System.Collections.Generic;
using UnityEngine;
public class DCPU16
{
// 8 General-Purpose Registers
public ushort[] Registers = new ushort[8]; // Renamed from 'registers' to 'Registers'
// 64K Words of Memory
private ushort[] memory = new ushort[0x10000]; // 64K = 2^16
// Special Registers
private ushort pc = 0x0000; // Program Counter
private ushort sp = 0xFFFF; // Stack Pointer (starts at top of memory)
private ushort ex = 0x0000; // Extra Register (overflow/underflow)
private ushort ia = 0x0000; // Interrupt Address (for interrupt handling)
// Interrupt Queue
private Queue<ushort> interruptQueue = new Queue<ushort>();
private bool interruptsEnabled = true;
// Methods go here...
public ushort ReadMemory(ushort address)
{
return memory[address];
}
private void WriteMemory(ushort address, ushort value)
{
memory[address] = value;
Debug.Log($"Memory Write - Address: 0x{address:X4}, Value: {(char)value} (0x{value:X4})");
}
public void LoadProgram(ushort[] program)
{
if (program.Length > memory.Length)
{
throw new ArgumentException("Program exceeds available memory");
}
Array.Copy(program, memory, program.Length);
pc = 0x0000; // Reset program counter to start of program
}
private void ExecuteInstruction()
{
// Fetch instruction
ushort instruction = ReadMemory(pc++);
// Decode opcode and operands
ushort opcode = (ushort)(instruction & 0x1F); // Lowest 5 bits
ushort b = (ushort)((instruction >> 5) & 0x1F); // Middle 5 bits
ushort a = (ushort)((instruction >> 10) & 0x3F); // Top 6 bits
// Checks if the DCPU-16 is running.
// Debug.Log($"PC: {pc}, Instruction: 0x{ReadMemory(pc):X4}");
// Execute based on opcode
if (opcode == 0) // Special opcodes
{
ExecuteSpecialInstruction(a, b);
}
else
{
ExecuteBasicInstruction(opcode, a, b);
}
if (instruction == 0x7F81) // Hypothetical HLT
{
Debug.Log("Program halted.");
return; // Exit execution loop
}
}
private void ExecuteBasicInstruction(ushort opcode, ushort a, ushort b)
{
ushort valueA = GetOperandValue(a);
ushort valueB = GetOperandValue(b); // Used in some instructions
switch (opcode)
{
case 0x01: // SET: b = a
Debug.Log($"SET: b = a. Setting Register[{b}] = {valueA}");
SetOperandValue(b, valueA);
break;
case 0x02: // ADD: b += a
ushort result = (ushort)(valueB + valueA);
ex = (ushort)((valueB + valueA) > 0xFFFF ? 1 : 0); // Set EX for overflow
SetOperandValue(b, result);
break;
case 0x03: // SUB: b -= a
result = (ushort)(valueB - valueA);
ex = (ushort)((valueB < valueA) ? 0xFFFF : 0); // Underflow handling
SetOperandValue(b, result);
break;
case 0x04: // MUL: b *= a
result = (ushort)(valueB * valueA);
ex = (ushort)((valueB * valueA) >> 16); // Store high bits in EX
SetOperandValue(b, result);
break;
// Add other opcodes (DIV, MOD, SHL, SHR, AND, OR, XOR, etc.)
default:
throw new InvalidOperationException($"Unknown opcode: {opcode}");
}
}
private void ExecuteSpecialInstruction(ushort a, ushort b)
{
switch (a)
{
case 0x01: // JSR: Call subroutine
Push(pc); // Save current PC
pc = GetOperandValue(b); // Jump to address
break;
// Add other special opcodes
}
}
private void SetOperandValue(ushort code, ushort value)
{
if (code < 0x08) {
Registers[code] = value;
}
else if (code < 0x10) {
memory[Registers[code - 0x08]] = value;
Debug.Log($"Memory Write - Address: 0x{Registers[code - 0x08]:X4}, Value: 0x{value:X4}");
}
else if (code == 0x1F) {
memory[ReadMemory(pc++)] = value;
Debug.Log($"Memory Write (literal) - Address: 0x{(pc-1):X4}, Value: 0x{value:X4}");
}
}
private void Push(ushort value)
{
if (sp == 0x0000) throw new StackOverflowException("Stack overflow");
memory[sp--] = value;
}
private ushort Pop()
{
if (sp == 0xFFFF) throw new InvalidOperationException("Stack underflow");
return memory[++sp];
}
private ushort GetOperandValue(ushort code)
{
if (code < 0x08) return Registers[code]; // Register A-J
if (code < 0x10) return ReadMemory(Registers[code - 0x08]); // [Register]
if (code == 0x1F) return ReadMemory(pc++); // Next word (literal value)
return (ushort)(code - 0x21); // Literal (0x00-0x1F -> -1 to 30)
}
private void QueueInterrupt(ushort message)
{
if (interruptsEnabled)
interruptQueue.Enqueue(message);
}
public interface IPeripheral
{
void HandleInterrupt(ushort message);
void Tick(); // Called every CPU cycle (e.g., for clocks or timers)
}
public void Tick()
{
if (ia != 0) HandleInterrupts(); // Check for interrupts
ExecuteInstruction(); // Process the next instruction
}
private void HandleInterrupts()
{
if (interruptsEnabled && interruptQueue.Count > 0)
{
ushort message = interruptQueue.Dequeue();
interruptsEnabled = false; // Disable nested interrupts
// Push current state onto the stack
Push(pc);
Push(Registers[0]); // Save register A (common convention)
// Jump to the interrupt address
pc = ia;
// Store the message in register A
Registers[0] = message;
}
}
private List<IPeripheral> peripherals = new List<IPeripheral>();
public void AddPeripheral(IPeripheral peripheral)
{
peripherals.Add(peripheral);
}
}
The DCPU-16 Manager:
using TMPro; // Add this for TextMeshPro components
using UnityEngine;
public class DCPU16Manager : MonoBehaviour
{
private DCPU16 cpu;
private Console console;
public TextMeshProUGUI consoleText;
void Start()
{
// Initialize the DCPU-16
cpu = new DCPU16();
// Initialize the Console peripheral
ushort consoleBaseAddress = 0x8000; // Example address for console memory
console = new Console(cpu, consoleBaseAddress, 32, 16); // 32x16 console
cpu.AddPeripheral(console);
// Attach console renderer
ConsoleRenderer renderer = gameObject.AddComponent<ConsoleRenderer>();
renderer.console = console;
renderer.consoleText = consoleText;
// Load a demo program
// ushort[] program = {
// 0x7C01, 0x0041, // SET A, 'A' (ASCII for 'A')
// 0x7DE1, 0x8000, // SET [0x8000], A (Write 'A' to console memory at 0x8000)
// 0x7C01, 0x0042, // SET A, 'B' (ASCII for 'B')
// 0x7DE1, 0x8001, // SET [0x8001], A (Write 'B' to console memory at 0x8001)
// 0x7C01, 0x0043, // SET A, 'C' (ASCII for 'C')
// 0x7DE1, 0x8002, // SET [0x8002], A (Write 'C' to console memory at 0x8002)
// 0x7F81, 0x0000 // HLT (hypothetical: halt CPU for simplicity)
// };
ushort[] program = {
0x7C01, 0x0041, // SET A, 'A' (ASCII for 'A')
0x7DE1, 0x8000, // SET [0x8000], A (Write 'A' to console memory at 0x8000)
0x7F81, 0x0000 // HLT (halt)
};
cpu.LoadProgram(program);
}
void Update()
{
// Run the DCPU-16 for a few cycles per frame
for (int i = 0; i < 100; i++)
{
cpu.Tick();
}
Debug.Log("CPU Ticked. Checking Console Buffer:");
char[,] buffer = console.GetScreenBuffer();
for (int y = 0; y < buffer.GetLength(0); y++)
{
string line = "";
for (int x = 0; x < buffer.GetLength(1); x++)
{
line += buffer[y, x];
}
Debug.Log(line);
}
}
}
The console:
using System.Collections.Generic; // Add this for Queue<T>
using UnityEngine;
public class Console : DCPU16.IPeripheral
{
private DCPU16 cpu;
private ushort baseAddress; // Starting memory address of the console
private int width;
private int height;
// Display Buffer (for Unity rendering)
private char[,] screenBuffer;
// Input Queue (to send characters to the DCPU-16)
private Queue<ushort> inputQueue = new Queue<ushort>();
public Console(DCPU16 cpu, ushort baseAddress, int width, int height)
{
this.cpu = cpu;
this.baseAddress = baseAddress;
this.width = width;
this.height = height;
this.screenBuffer = new char[height, width];
}
public void HandleInterrupt(ushort message)
{
if (message == 0x01) // Example: Get a character from the input queue
{
if (inputQueue.Count > 0)
{
ushort input = inputQueue.Dequeue(); // Get the next character
cpu.Registers[0] = input; // Place the character in register A
}
else
{
cpu.Registers[0] = 0; // No input available
}
}
}
public void Tick()
{
Debug.Log("Console.Tick called");
for (int y = 0; y < height; y++)
{
for (int x = 0; x < width; x++)
{
ushort address = (ushort)(baseAddress + y * width + x);
ushort charCode = cpu.ReadMemory(address);
screenBuffer[y, x] = (char)(charCode & 0x7F); // ASCII character
Debug.Log($"Reading Memory Address: 0x{address:X4}, Value: 0x{charCode:X4} -> '{(char)charCode}'");
}
}
}
public void AddInput(char c)
{
inputQueue.Enqueue((ushort)c);
}
public char[,] GetScreenBuffer()
{
return screenBuffer;
}
}
The Console Renderer
using UnityEngine;
using TMPro;
public class ConsoleRenderer : MonoBehaviour
{
public Console console; // Attach the Console peripheral
public TextMeshProUGUI consoleText; // TextMeshPro object for display
void Update()
{
// Update the console display
RenderConsole();
}
void RenderConsole()
{
Debug.Log("Rendering Console...");
if (console == null || consoleText == null) return;
char[,] buffer = console.GetScreenBuffer();
int width = buffer.GetLength(1);
int height = buffer.GetLength(0);
string consoleContent = "";
for (int y = 0; y < height; y++)
{
string line = "";
for (int x = 0; x < width; x++)
{
line += buffer[y, x];
}
consoleContent += line + "\n"; // Add a new line after each row
}
consoleText.text = consoleContent;
Debug.Log($"Console Content:\n{consoleContent}");
}
}
And keyboard input.
using UnityEngine;
public class KeyboardInput : MonoBehaviour
{
public Console console; // Attach the Console peripheral
void Update()
{
foreach (char c in Input.inputString)
{
console.AddInput(c); // Add typed character to the input queue
}
}
}
I couldn’t figure out what else to do so I came here. If I should look somewhere else, please tell me.