<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Sayan Das</title>
    <description>The latest articles on DEV Community by Sayan Das (@sayan_dev).</description>
    <link>https://dev.to/sayan_dev</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4027420%2F71c4fc01-bf65-4930-9143-8778d06354e0.gif</url>
      <title>DEV Community: Sayan Das</title>
      <link>https://dev.to/sayan_dev</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/sayan_dev"/>
    <language>en</language>
    <item>
      <title>Building a 100k+ Gate Digital Logic Simulator in Rust: How I Flattended Hierarchies and Maximized the L1 Cache</title>
      <dc:creator>Sayan Das</dc:creator>
      <pubDate>Mon, 20 Jul 2026 12:20:12 +0000</pubDate>
      <link>https://dev.to/sayan_dev/building-a-100k-gate-digital-logic-simulator-in-rust-how-i-flattended-hierarchies-and-maximized-30kh</link>
      <guid>https://dev.to/sayan_dev/building-a-100k-gate-digital-logic-simulator-in-rust-how-i-flattended-hierarchies-and-maximized-30kh</guid>
      <description>&lt;p&gt;Like many people, I was incredibly inspired by Sebastian Lague’s digital logic simulator videos. I had this "fragile dream" of building a visual, node-based sandbox where you could wire up primitive NAND gates, package them into custom sub-chips, and stack them recursively all the way up to a fully functioning 16-bit or 32-bit CPU running in real-time.&lt;/p&gt;

&lt;p&gt;But if you’ve ever tried scaling up traditional visual logic simulators, you know you hit a brutal performance wall incredibly fast. Classic Object-Oriented patterns—where every logic gate is a heap-allocated node, wires are pointers, and nested sub-chips require recursive tree-walks or virtual dispatch—will tank the engine to 4 FPS the moment your circuit crosses a few thousand gates.&lt;/p&gt;

&lt;p&gt;I wanted raw simulation throughput. Built from scratch using Rust, Macroquad, and &lt;code&gt;egui&lt;/code&gt;, my simulator now comfortably processes over 100,000+ primitive gates in real-time on standard laptop hardware.&lt;/p&gt;

&lt;p&gt;Here is how I used Data-Oriented Design, dynamic parallel execution, and topological cache defragmentation to make hardware scale.&lt;/p&gt;




&lt;h2&gt;
  
  
  1. Making Deep Nesting Free (The Flattening Compiler)
&lt;/h2&gt;

&lt;p&gt;In a naive simulator, if you nest a NAND gate inside an XOR gate, inside a Full Adder, inside an ALU, inside a CPU, your runtime pays a massive pointer-chasing traversal penalty at every single nesting level just to find out what a wire is doing.&lt;/p&gt;

&lt;p&gt;To fix this, I built a synchronous flattening compiler.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[User View: Nested Hierarchy]
CPU ➔ ALU ➔ Full Adder ➔ NAND

[Simulation Layer: Flattened Array]
[ NAND #0, NAND #1, NAND #2, NAND #3, ... ]  &amp;lt;-- One flat, contiguous Slab

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Before the simulation loop ever starts, the engine recursively resolves the entire nested-chip layout and flattens it down to a single, contiguous array of raw primitive primitives (NAND, Input, Output, Clock).&lt;/p&gt;

&lt;p&gt;At runtime, the concept of a "sub-chip" completely ceases to exist. Nesting depth becomes a zero-cost abstraction used strictly for UI organization. A CPU built out of 10,000 deeply nested custom chip instances evaluates exactly as fast as 10,000 raw NAND gates laid out flat on a single canvas.&lt;/p&gt;




&lt;h2&gt;
  
  
  2. Navigating the "Wide vs. Narrow" Pipeline Safely with Rayon
&lt;/h2&gt;

&lt;p&gt;To push past 100k gates, I needed to leverage multi-core processors. However, parallel event processing is notorious for introducing non-deterministic data races.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Safety Net
&lt;/h3&gt;

&lt;p&gt;I used &lt;strong&gt;Tarjan’s Strongly Connected Components (SCC)&lt;/strong&gt; algorithm to calculate an &lt;code&gt;$O(V+E)$&lt;/code&gt; topological depth scheduling layout. By grouping independent gates into explicit depth layers, I could guarantee that all nodes within the exact same depth layer can be evaluated in parallel with 100% thread safety without locks.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Bottleneck: Thread-Pool Overhead
&lt;/h3&gt;

&lt;p&gt;The problem with a pure event-driven simulation loop is that it is highly unpredictable. The workload might be incredibly "wide" on one step (e.g., a 32-bit clock line triggering 32 separate registers simultaneously), but turn entirely "narrow" on the very next step (e.g., a single ALU flag toggling).&lt;/p&gt;

&lt;p&gt;If you unconditionally throw Rayon’s &lt;code&gt;.par_iter()&lt;/code&gt; at small event queues or narrow steps, the overhead of spinning up and waking up worker threads takes longer than just processing the math sequentially on a single core.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Solution: An Intelligent Hardware Profiler
&lt;/h3&gt;

&lt;p&gt;Instead of guessing a hardcoded threshold (which fails spectacularly depending on whether the app runs on a high-end desktop or an Android phone), the simulator runs a quick calibration routine on startup. It benchmarks dummy gate arrays of varying sizes using both single-threaded &lt;code&gt;.iter()&lt;/code&gt; and parallel &lt;code&gt;.par_iter()&lt;/code&gt; to find the exact crossover threshold where multi-threading becomes beneficial on the host machine.&lt;/p&gt;

&lt;p&gt;The simulator then dynamically routes execution mid-cycle depending on the size of the current evaluation queue:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;queue_size&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;current_queue&lt;/span&gt;&lt;span class="nf"&gt;.len&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;updates&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Vec&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;usize&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;u8&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;queue_size&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="py"&gt;.dynamic_threshold&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Hardware profiler determined parallel is faster for this batch size&lt;/span&gt;
    &lt;span class="n"&gt;current_queue&lt;/span&gt;&lt;span class="nf"&gt;.par_iter&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="nf"&gt;.filter_map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;compute_state&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="nf"&gt;.collect&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Single-thread is faster for small queues&lt;/span&gt;
    &lt;span class="n"&gt;current_queue&lt;/span&gt;&lt;span class="nf"&gt;.iter&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="nf"&gt;.filter_map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;compute_state&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="nf"&gt;.collect&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="c1"&gt;// Centralized, sequential lock-free write-back&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;idx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;new_state&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;updates&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="py"&gt;.nodes&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;idx&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="py"&gt;.state&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;new_state&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;next_enqueues&lt;/span&gt;&lt;span class="nf"&gt;.push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;idx&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;By passing an immutable copy of the state array to the parallel closure, &lt;code&gt;compute_state&lt;/code&gt; remains completely read-only. We accumulate the updates into a standard &lt;code&gt;Vec&lt;/code&gt; via &lt;code&gt;.collect()&lt;/code&gt;, and then apply them sequentially on a single thread.&lt;/p&gt;

&lt;p&gt;Because the parallel phase never writes to shared memory, the engine is &lt;strong&gt;completely immune to False Sharing&lt;/strong&gt; and requires zero performance-killing &lt;code&gt;Mutex&lt;/code&gt; or &lt;code&gt;RwLock&lt;/code&gt; wrappers inside the simulation loop.&lt;/p&gt;




&lt;h2&gt;
  
  
  3. Smashing the Cache Locality Wall
&lt;/h2&gt;

&lt;p&gt;Even with a flattened array, random memory allocation is a silent killer. In a large project, users constantly place, delete, and re-wire gates. This leaves structural gaps in the underlying &lt;code&gt;Slab&lt;/code&gt; memory allocator. If Gate #5 reads its input from Gate #95,000, the CPU suffers an L1/L2 cache miss, stalling the processor while it fetches data from main RAM.&lt;/p&gt;

&lt;p&gt;To optimize strictly for the silicon, I implemented an &lt;strong&gt;L1/L2 Cache Defragmentation pass&lt;/strong&gt; directly inside the synchronous compilation pipeline.&lt;/p&gt;

&lt;p&gt;Directly after depth calculation, the engine performs the following:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Extracts all valid nodes out of the current sparse allocation layout.&lt;/li&gt;
&lt;li&gt;Sorts them primarily by topological depth ascending.&lt;/li&gt;
&lt;li&gt;Packs them tightly into a fresh, non-sparse &lt;code&gt;Slab&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Generates an &lt;code&gt;old_to_new&lt;/code&gt; translation mapping vector (using &lt;code&gt;usize::MAX&lt;/code&gt; as a safe sentinel).&lt;/li&gt;
&lt;li&gt;Iterates over the new layout and rewrites all internal source references and active event queues to point to the new packed indices.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Because topologically connected gates are now packed right next to each other in physical RAM, the CPU's hardware prefetcher keeps the L1 cache perfectly saturated. When Rayon grabs chunks of the queue, it processes dense, contiguous memory blocks with minimal cache misses.&lt;/p&gt;




&lt;h2&gt;
  
  
  4. Bypassing the &lt;code&gt;$O(N)$&lt;/code&gt; UI Rendering Bottleneck
&lt;/h2&gt;

&lt;p&gt;While the simulation backend was running at near bare-metal speeds, drawing a massive circuit was crushing the visual rendering engine. Originally, the editor ran an Axis-Aligned Bounding Box (AABB) frustum check sequentially across the entire component vector. Even if you zoomed all the way in on a single gate, the CPU had to loop through 100,000 items every single frame just to realize they were off-screen.&lt;/p&gt;

&lt;p&gt;To solve this, I leveraged the project's existing &lt;strong&gt;Spatial Hash Grid&lt;/strong&gt; (which was originally engineered for &lt;code&gt;$O(1)$&lt;/code&gt; wire deduplication) to instantly query visible canvas boundaries in &lt;code&gt;$O(K)$&lt;/code&gt; time.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;top_left&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="nf"&gt;.to_world_space&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;vec2&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.0&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;bottom_right&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="nf"&gt;.to_world_space&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;vec2&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;screen_width&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="nf"&gt;screen_height&lt;/span&gt;&lt;span class="p"&gt;()));&lt;/span&gt;
&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;viewport_rect&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;Rect&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;top_left&lt;/span&gt;&lt;span class="py"&gt;.x&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;top_left&lt;/span&gt;&lt;span class="py"&gt;.y&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;bottom_right&lt;/span&gt;&lt;span class="py"&gt;.x&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;top_left&lt;/span&gt;&lt;span class="py"&gt;.x&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;bottom_right&lt;/span&gt;&lt;span class="py"&gt;.y&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;top_left&lt;/span&gt;&lt;span class="py"&gt;.y&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Grab only visible components instantly&lt;/span&gt;
&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;visible_comp_ids&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="py"&gt;.canvas.spatial_grid&lt;/span&gt;&lt;span class="nf"&gt;.query_rect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;viewport_rect&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Avoiding the Visual Traps
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Defeating Z-Fighting Flicker:&lt;/strong&gt; A spatial hash query returns a &lt;code&gt;HashSet&lt;/code&gt;, which has a non-deterministic iteration order. If you draw straight from the set, overlapping components will randomly flicker over and under each other every single frame. Dumping the IDs into a &lt;code&gt;Vec&lt;/code&gt; and executing a microsecond &lt;code&gt;.sort_unstable()&lt;/code&gt; before rendering completely stabilizes the layer layout.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The Long Wire Phantom:&lt;/strong&gt; Wires are mathematically derived on the fly from a global map (&lt;code&gt;self.circuit.connections&lt;/code&gt;) by looking up source and target coordinates. By completely separating the wire rendering phase from the culled component drawing phase, a long bus wire that stretches completely across the screen from an off-screen ALU to an off-screen RAM block never clips out or vanishes when you zoom into the empty space between them.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Current Status &amp;amp; What's Next
&lt;/h2&gt;

&lt;p&gt;By separating core simulation into isolated parallel-read and sequential-write loops, defragmenting RAM for cache line alignment, and trading immediate-mode UI iterations for spatial grid queries, the engine is fully capable of running complex hardware architectures.&lt;/p&gt;

&lt;p&gt;The application features full cross-platform compilation via the same codebase (including a touch-native UI layout for Android alongside native desktop binaries for Windows, Linux, and macOS) and tracks full 4-state logic (Floating, Low, High, Contention) so you can actually debug physical bus conflicts.&lt;/p&gt;

&lt;p&gt;The codebase is fully open-source. If you want to dig into the compilation tracing logic, the spatial grid implementation, or compile it yourself, check out the repository here:&lt;/p&gt;

&lt;p&gt;👉 &lt;a href="https://github.com/Sayanthegamer/digital_logic" rel="noopener noreferrer"&gt;Sayanthegamer/digital_logic&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I'd love to hear your thoughts on the architecture. How do you handle graph parallelization, memory layout, or cache defragmentation in your own performance-critical Rust projects?&lt;/p&gt;

</description>
      <category>rust</category>
      <category>programming</category>
      <category>performance</category>
      <category>gamedev</category>
    </item>
  </channel>
</rss>
