Skip to main content

The Power of Pride Bundle 2026 — $10 PWYC Edition
On Sale: GamesAssetsToolsTabletopComics
Indie game storeFree gamesFun gamesHorror games
Game developmentAssetsComics
SalesBundles
Jobs
TagsGame Engines

NeevorPL

17
Posts
2
Topics
1
Following
A member registered 84 days ago

Recent community posts

Actually, now that I think about it… since you are already prepared to break open the source code and add custom components, why settle for just a bigger generic ROM?

There is a much better, high-performance architectural solution for your 128x128 setup: write a custom ColorROM component directly in C#.

Instead of forcing DLS to simulate a massive grid of thousands of virtual gates and memory cells (which will inevitably tank your simulation frame rate), you can implement the entire memory array as a native C# multi-dimensional array or dictionary inside your custom chip class.

This approach gives you two massive advantages:

A native C# memory lookup is orders of magnitude faster than a gate-level ROM matrix simulation. Your 128x128 display will actually run smoothly in real time.

Since it’s pure C#, you aren't limited to just spitting out raw static data. You could easily implement real-time color space conversions (like HSV to RGB), masking, or mathematical transformations directly inside the chip's logic before outputting the signals to your display.

If you are going to mod the codebase anyway, writing a dedicated, optimized graphics helper chip is definitely the most elegant and “engineer-approved” way to solve the VRAM bottleneck!

The inputs and outputs for components are actually implemented quite nicely in DLS. I haven’t tried modifying them myself, but from a static code analysis, it looks like just adding the appropriate new ChipType to the ChipDescription method should do the trick. Most of the magic happens right at the beginning of the SimChip method in Assets/Scripts/Simulation/SimChip.cs.

Adding a larger ROM size shouldn’t be too painful either. The UI editor for it is written in a very elegant, clean way, so it should theoretically accept data arrays of any arbitrary size without breaking the layout.

The real question is how the simulation engine itself will handle the performance overhead once you start cycling through that much data in real time. ;)

Haha, okay, now we are on the same page! To be honest, I use exactly the same approach.

I wrote a microcode compiler for my CPU, and filling a matrix of 3x8 ROM chips by hand was a massive pain in the ass. So, I also ended up writing a Python script to handle it. I even named it nero_burning_rom.

However, looking at it from the developer’s perspective, keeping data formatted in a single, massive native ROM file so that DLS understands it correctly is going to be just as much of a headache to maintain.

That being said, I am actually planning to run the original Commodore PLUS/4 ROM on my processor in the future, so I will eventually need a proper 64KB ROM block anyway just to make things easier. But even when I get to that point, I will still use an external script to parse the raw binary ROMs and inject them straight into the DLS project JSON. It’s just more efficient.

If you do end up messing with the source code and come up with a clean solution for a bigger ROM component, let me know! Unless, of course, I beat you to it and implement it first—though that’s unlikely anytime soon, as I’m currently stuck deep in RST/IRQ/NMI interrupts and replicating the TED chip.

Hi, To be honest, managing multiple memory components and designing proper address decoding logic is a fundamental part of building any complex digital logic circuit. If handling, placing, and wiring 16 ROM chips is considered too tedious, boring, or overwhelming, then digital logic simulation and DLS might simply not be the right hobby for you. This discipline is all about building complex systems from basic blocks.

If you want to keep your main canvas clean, the proper way to handle this is by encapsulating those 16 ROMs and your decoding logic inside a single Subcircuit. You do it once, and you get a clean, reusable custom component with the exact capacity you need. Because of this, I highly doubt customizable or massive native ROM sizes will ever be added to DLS. There is simply no need for them when you can easily build them yourself using the components already available in the tool.

Latest version? It is not working anymore this way.

Read this: https://itch.io/t/6359531/how-to-build-your-own-cpu-in-dls-a-minimalist-guide

(4 edits)

Seeing many people attempting to build a processor “from the inside out” (i.e., mindlessly connecting individual logic gates without a structured plan), here is a concrete roadmap. We aren’t building a full MOS or Z80 replica right away. Instead, we will create a minimalist architecture capable of memory reads/writes, jumps, and basic arithmetic operations. Here is the correct sequence of abstraction layers required to complete the project successfully:

Step 1: Define the ISA (Instruction Set Architecture)

Before placing a single gate, you must strictly define your instruction set. A set of 4 to 8 elementary instructions is perfect for a start. Determine the format of the opcodes and their arguments, for example:

  • NOP – Does nothing.
  • BRK – Stops the processor (deasserts the clock enable signal until a hardware RESET).
  • LOAD A, #value – Read a literal byte directly from the memory location following the opcode into Register A (Immediate mode).
  • LOAD A, [addr] – Read a byte from the specified RAM address into Register A (Absolute mode).
  • LOAD B, #value – Read a literal byte directly from the memory location following the opcode into Register B (Immediate mode).
  • LOAD B, [addr] – Read a byte from the specified RAM address into Register B (Absolute mode).
  • STORE A, [addr] – Write the current contents of Register A into the specified RAM address.
  • STORE B, [addr] – Write the current contents of Register B into the specified RAM address.
  • TAB – Transfer the contents of Register A directly into Register B.
  • TBA – Transfer the contents of Register B directly into Register A.
  • ADD A, #value – Add a literal byte directly to the contents of Register A (Immediate mode).
  • ADD A, [addr] – Fetch a byte from the specified RAM address and add it to Register A (Absolute mode).
  • ADD A, B – Add the contents of Register B to Register A.
  • SUB A, #value – Subtract a literal byte directly from the contents of Register A using ALU logic (Immediate mode).
  • SUB A, [addr] – Fetch a byte from the specified RAM address and subtract it from Register A using ALU logic (Absolute mode).
  • SUB A, B – Subtract the contents of Register B from Register A using ALU logic.
  • JUMP [addr] – Unconditional jump (overwrite the Program Counter with the target address).

I am stopping at these few, because more and more ideas for instructions and address modes that MUST be implemented keep popping into my head ;)

Step 2: Design the Data Path

Prepare a structure of registers connected by data and address buses. Every register output connected to a bus must be isolated using a tri-state buffer or controlled via a multiplexer to prevent bus contention. The minimum required blocks are:

  • PC (Program Counter) - A counter indicating the address of the instruction currently being fetched.
  • AR (Address Register) - A register that latches the address for the RAM.
  • A, B - General-purpose registers (Accumulator and helper register).
  • ALU (Arithmetic Logic Unit) - The arithmetic and logic core.

💡 ALU Tip: Instead of designing an ALU from scratch, it is best to implement the architecture presented by Sebastien Lague (the author of DLS) in his YouTube series on building a computer. He demonstrates step-by-step how to build a unit that handles both addition and subtraction using simple adders controlled by inversion and carry-in lines.

Step 3: The Flags Register – The Link Between ALU and CPU

Borrowing from the MOS 6502 architecture, the processor must be able to react to the results of mathematical operations. This is handled by the Status Register (P - Processor Status). Each bit (flag) is hardware-set by the ALU after executing an ADD or SUB instruction:

  • Z (Zero) Flag - Set to 1 if the ALU result is exactly zero.
  • N (Negative) Flag - Copies the highest bit of the result (the sign bit in two’s complement). If bit 7 is 1, the result is negative.
  • C (Carry) Flag - For ADD, it signals an unsigned overflow (result > 255). For SUB, it acts as a borrow indicator—Sebastien’s ALU naturally generates this signal by analyzing the output carry bit when the second operand is inverted.
  • V (Overflow) Flag - Indicates a signed overflow (Two’s Complement) when an operation on two numbers of the same sign yields a result with the opposite sign. These flags are critical because, in the next stage of your processor’s development, they will allow you to implement conditional jumps (e.g., BEQ – branch if Z=1).

Step 4: The Sequencer – Microstep Counter

Executing a single instruction requires several clock cycles (control phases, also kwnown as Instruction Cycle or Fetch-Decode-Execute Cycle). For instance, a LOAD instruction needs time to:

  1. Assert the PC onto the address bus and latch the operation code (Opcode Fetch).
  2. Decode the instruction within the Control Unit.
  3. Assert the argument’s address onto the address bus.
  4. Read data from RAM and latch it into the destination register.

To manage this process, a step counter is required (e.g., a 3-bit counter counting from 0 to 7) which resets back to its initial state (0) upon completing the full instruction sequence.

Step 5: The Control Unit Based on Microcode

Designing combinational control logic purely out of logic gates becomes unmanageable as the number of instructions grows. The most efficient approach is using a ROM component as a microcode decoder.The ROM input address is a combination of: [Current Opcode] + [Current Microstep Counter Value]. The ROM output is a wide Control Word (e.g., 16, 24, or 32 bits), where each individual bit is physically wired to a control line (Enable, Write, Read, Clear) of a specific block within the CPU.

💡 Engineering Pro-Tip: To generate the contents of your control ROM, it is best to write a simple compiler in Python. The script should parse a text file containing symbolic state descriptions (e.g., PC_OUT, RAM_READ, AR_IN) and automatically map them to their respective bit positions in the control word, outputting a clean binary file ready to be loaded into DLS. Manually toggling bits in a hex editor is a guarantee for logic bugs.

Step 6: Initialization (RESET) and the First Test

The final stage is implementing the RST (Reset) line. This signal must force an asynchronous load of the start address (e.g., 0x0000) into the PC register and clear the microstep counter. To verify that your logic structures work properly, place a simple sequence of instructions into the RAM:

0x0000: LOAD A, [0x0010]  ; Fetch value from address 0x10
0x0003: SUB A, [0x0011]   ; Subtract value at address 0x11
0x0006: JUMP 0x0003       ; Loop back to repeat the operation

If the microstep counter cycles correctly after a reset, the PC register performs the proper address jumps, and the accumulator stabilizes with the correct values according to the ALU, your processor core is functional. With this solid foundation, you can move on to implementing advanced mechanisms like hardware interrupt handling (IRQ/NMI), additional registers, stack, conditional jumps, indexed address modes or more ALU operations (incl. BCD mode).

Since I am nearing the completion of a MOS 8502 replica — the heart of the 8-bit computers from the Commodore stable — and ultimately plan a full Commodore PLUS/4 replica, I have already built many of these circuits and ironed out most of the edge cases. Feel free to ask about the details. To cover absolutely everything, I would need hours of lectures or reams of paper ;) Which I might actually do someday, who knows.

(1 edit)

Nobody said it was easy :D

Here is a more detailed diagram: http://www.z80.info/gfx/fig214.gif

Understand and recreate all the parts, connect them, and that’s all. I’ve been working on my MOS8502 replica for a year and it is 95% done. Now working on interrupt handling.

https://www.zilog.com/docs/z80/um0080.pdf

Select whatever you want using combinations click, shift+click or ctr+click. Press CRTL+D (duplicate), and click to put duplicated things on canvas.

Rotate chip? You can’t.

(1 edit)

Hi everyone,

I just wanted to express my deepest gratitude for the absolute flood of responses and the overwhelming support I received on my last question. Your collective silence was truly deafening and incredibly inspiring—so much so that it motivated me to just take matters into my own hands.

With no other options, I ended up writing my own transpiler in Python. The script does exactly what I originally asked about: it directly parses the project’s JSON file, safely maps the old NAND-based sub-circuits to my new native C# components, and most importantly, it leaves the original GUIDs completely untouched while correctly recalculating the pin indices. All the wiring remains perfectly intact, and the simulation is finally back to a playable speed.

I’m dropping my code below. Take it and use it, so nobody else has to reinvent the wheel (or wait around for help that’s never coming).

import json
import os
import shutil
import glob

# List of gates to migrate to native Unity C# implementations
NATIVE_GATES = [
    "NOT", "AND", "OR", "XOR", "NOR", "XNOR",
    "AND3", "AND4", "AND8", "OR3", "OR4", "OR5", "OR6", "OR8"
]

CHIPS_DIR = "./Chips"
DELETED_DIR = "./Deleted Chips"


def main():
    if not os.path.exists(DELETED_DIR):
        os.makedirs(DELETED_DIR)
        print(f"Created directory: {DELETED_DIR}")

    gate_mappings = {}

    print("--- STEP 1: BUILDING ID MAP & MOVING OLD FILES ---")
    for gate in NATIVE_GATES:
        filename = f"{gate}.json"
        src_path = os.path.join(CHIPS_DIR, filename)
        dest_path = os.path.join(DELETED_DIR, filename)

        if not os.path.exists(src_path):
            print(f"[Skipped] Not found in {CHIPS_DIR}: {filename}")
            continue

        # Read the old gate JSON to map its complex IDs to sequential 0, 1, 2...
        with open(src_path, 'r', encoding='utf-8') as f:
            data = json.load(f)

        gate_mappings[gate] = {}
        current_new_id = 0

        if 'InputPins' in data:
            for pin in data['InputPins']:
                gate_mappings[gate][pin['ID']] = current_new_id
                current_new_id += 1

        if 'OutputPins' in data:
            for pin in data['OutputPins']:
                gate_mappings[gate][pin['ID']] = current_new_id
                current_new_id += 1

        # Move the old gate file out of the active directory
        try:
            if os.path.exists(dest_path):
                os.remove(dest_path)
            shutil.move(src_path, dest_path)
            print(f"[Mapped & Moved] {filename}")
        except Exception as e:
            print(f"[Error] Could not move {filename}: {e}")

    print("\n--- STEP 2: UPDATING WIRES IN REMAINING CHIPS ---")
    for filepath in glob.glob(os.path.join(CHIPS_DIR, '*.json')):
        filename = os.path.basename(filepath)
        chip_name = filename.replace('.json', '')

        with open(filepath, 'r', encoding='utf-8') as f:
            try:
                data = json.load(f)
            except json.JSONDecodeError:
                print(f"[Error] Failed to parse JSON in {filename}")
                continue

        modified = False
        subchips_to_update = {}

        # Check if the chip uses any of the migrated gates
        if 'SubChips' in data and data['SubChips']:
            for subchip in data['SubChips']:
                if subchip['Name'] in gate_mappings:
                    subchips_to_update[subchip['ID']] = subchip['Name']

        if not subchips_to_update:
            continue

        # Update the wiring to use the new sequential IDs (0, 1, 2...)
        if 'Wires' in data and data['Wires']:
            for wire in data['Wires']:
                # Update Source Pin
                src_owner = wire['SourcePinAddress']['PinOwnerID']
                if src_owner in subchips_to_update:
                    gate_name = subchips_to_update[src_owner]
                    old_pin = wire['SourcePinAddress']['PinID']
                    if old_pin in gate_mappings[gate_name]:
                        wire['SourcePinAddress']['PinID'] = gate_mappings[gate_name][old_pin]
                        modified = True

                # Update Target Pin
                tgt_owner = wire['TargetPinAddress']['PinOwnerID']
                if tgt_owner in subchips_to_update:
                    gate_name = subchips_to_update[tgt_owner]
                    old_pin = wire['TargetPinAddress']['PinID']
                    if old_pin in gate_mappings[gate_name]:
                        wire['TargetPinAddress']['PinID'] = gate_mappings[gate_name][old_pin]
                        modified = True

        # Save the updated chip JSON
        if modified:
            with open(filepath, 'w', encoding='utf-8') as f:
                json.dump(data, f, indent=2)
            print(f"[Updated Wires] -> {filename}")

    print("\n[SUCCESS] ALL DONE! The DLS library is fully migrated to native gates.")


if __name__ == '__main__':
    main()

Hope someone out there finds this useful. On a brighter note, the Commodore PLUS 4 replica is slowly taking shape!

Best regards, Pietrek

Hi everyone,

I’m currently deep into building a Commodore PLUS 4 replica. To keep the simulation speed at a playable level, I’ve moved away from building basic gates (OR, NOT, XOR, etc.) out of NAND sub-circuits. Instead, I’ve implemented them as native C# components within the DLS source code.

I have a large existing project folder where all the logic is already wired using sub-circuits (the standard .json format where a “Gate” is a collection of NANDs). My new native C# gates, while functionally identical, are seen as different entities by the simulator.

I’m looking for a way to “transpile” or convert my existing project JSON so that:

  • References to the old NAND-based sub-circuits are replaced with my new native C# components.

  • All existing wiring (connections) remains intact.

The way DLS saves sub-circuits vs. native components seems to differ in terms of componentName and how pins are indexed.

Doing replacement manually in the editor for a real CPU-scale project is impossible.

Has anyone developed a script or a tool to Batch-Replace components in DLS project files? Or perhaps someone has found a way to “alias” a sub-circuit so the engine treats it as a native script during the simulation update loop?

I’m currently considering writing a Python script to parse and edit the JSON directly, but I’m worried about breaking the internal GUIDs or pin mapping. Any insights or similar experiences would be greatly appreciated!

Best regards, Pietrek

I made it for you.

(1 edit)

Find block diagram of any processor. Let’s try Zilog’s Z80 one. Here is the manual: https://www.zilog.com/docs/z80/um0080.pdf

Recreate all the block - registers, instruction decoder, alu, etc Wire it all together, and voila, you have a Z80 replica ;P

Shift + D?

What you mean “duplicate something”?

MOS 8501 Replica