Error with DCPU-16 display

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.

You should look to yourself!! From your description above, it just sounds like you wrote a bug… and that means… time to start debugging!

By debugging you can find out exactly what your program is doing so you can fix it.

Use the above techniques to get the information you need in order to reason about what the problem is.

You can also use Debug.Log(...); statements to find out if any of your code is even running. Don’t assume it is.

Once you understand what the problem is, you may begin to reason about a solution to the problem.

Remember with Unity the code is only a tiny fraction of the problem space. Everything asset- and scene- wise must also be set up correctly to match the associated code and its assumptions.

If you plan on implementing a CPU instruction parser you are going to seriously need to upgrade your debugging skills. I say this as someone who has written several in-circuit emulators. :slight_smile: It requires an attention abstract detail unlike almost any other type of software endeavor.

1 Like

Thank you for the quick reply! I have implemented some debug lines, and most of them work as expected (i think) The main one that doesn’t work as expected is this:

Debug.Log($"Console Content:\n{consoleContent}");

in the console renderer script. It should return ABC, but I’m not sure why.

I do believe everything in the scene is set up correctly. It’s a mostly empty scene with only the starting scene objects, a canvas containing the console text, and an empty GameObject with the DCPU16 Manager and Keyboard input.

As I previously stated, I’m still pretty new to this, so if you have anything to recommend for learning more about these things, that would be a great help.

Debugging is a subject you can intentionally learn about.

You will only actually learn about debugging by doing it.

Debugging is just proofreading your work really.

1 Like