<?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: GigaFloppa</title>
    <description>The latest articles on DEV Community by GigaFloppa (@gigafloppa).</description>
    <link>https://dev.to/gigafloppa</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%2F4125202%2F61f4f638-6e54-4b07-ad8d-9c3f187b7e7c.png</url>
      <title>DEV Community: GigaFloppa</title>
      <link>https://dev.to/gigafloppa</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/gigafloppa"/>
    <language>en</language>
    <item>
      <title>Simulating 30 Million microbes in Browser</title>
      <dc:creator>GigaFloppa</dc:creator>
      <pubDate>Mon, 14 Sep 2026 22:51:22 +0000</pubDate>
      <link>https://dev.to/gigafloppa/simulating-30-million-microbes-in-browser-3n86</link>
      <guid>https://dev.to/gigafloppa/simulating-30-million-microbes-in-browser-3n86</guid>
      <description>&lt;h1&gt;
  
  
  Simulating 30 Million Microbes in the Browser
&lt;/h1&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fszgnzkt0e9lwt5q33h3s.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fszgnzkt0e9lwt5q33h3s.png" alt="Microbes simulation" width="800" height="634"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;WebGPU gives the browser direct access to modern GPU capabilities — not just for rendering, but also for general-purpose computation through compute shaders.&lt;/p&gt;

&lt;p&gt;But how far can we actually push it? What happens if, instead of running a small compute demo, we try to build a full simulation with tens of millions of active objects?&lt;/p&gt;

&lt;p&gt;That is what I wanted to find out.&lt;/p&gt;

&lt;h2&gt;
  
  
  Background
&lt;/h2&gt;

&lt;p&gt;For this experiment, I decided to revive an evolution simulator I had written many years ago and move its core logic from the CPU to the GPU.&lt;/p&gt;

&lt;p&gt;The original simulator was written in C# and had a fairly conventional architecture: a two-dimensional array of objects, a separate list of living cells, and a &lt;code&gt;foreach&lt;/code&gt; loop that executed their logic once per tick.&lt;/p&gt;

&lt;p&gt;Each cell would inspect its surroundings, choose an action, and immediately modify the shared world. That simple model was enough for populations to emerge, compete, and evolve over time.&lt;/p&gt;

&lt;p&gt;The old web version supported worlds of up to &lt;code&gt;200 × 200&lt;/code&gt; cells. This time, I wanted to push that to &lt;code&gt;8192 × 4096&lt;/code&gt; — roughly 33 million positions, each of which can potentially contain a living cell.&lt;/p&gt;

&lt;h2&gt;
  
  
  First, Memory
&lt;/h2&gt;

&lt;p&gt;With 33 million positions, even a single 32-bit value per position already takes about &lt;code&gt;128 MiB&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;And one value is nowhere near enough. A cell also needs an age, species, energy level, individual traits, plus temporary data used while calculating the next state of the simulation.&lt;/p&gt;

&lt;p&gt;On the CPU, the natural solution is to put all of that into a &lt;code&gt;Cell&lt;/code&gt; object. At this scale, however, that approach gets expensive very quickly. Adding just one extra 4-byte field per position can potentially cost another &lt;code&gt;128 MiB&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;So in the GPU version, I split the simulation state across several arrays. One stores the basic information about what occupies each position, while energy and individual traits are kept separately. Species-level data, such as the genome, is stored independently rather than duplicated for every cell. Temporary data needed only during a tick lives in its own buffers as well.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqjt7l80417eesjoau0c3.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqjt7l80417eesjoau0c3.png" alt="Memory layout" width="800" height="234"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This separation is useful for more than just reducing memory usage.&lt;/p&gt;

&lt;p&gt;Different compute passes need different pieces of state. A shader that updates energy does not need access to everything about a cell, while the renderer has no reason to see temporary movement requests.&lt;/p&gt;

&lt;p&gt;As a result, the memory layout is shaped not only by what needs to be stored, but also by how that data moves through the GPU.&lt;/p&gt;

&lt;h2&gt;
  
  
  From Sequential to Parallel Ticks
&lt;/h2&gt;

&lt;p&gt;In the old version, the game simply walked through the list of living cells one by one.&lt;/p&gt;

&lt;p&gt;A cell inspected its neighbors, chose an action, and immediately changed the shared world. The next cell would then see the result of that change.&lt;/p&gt;

&lt;p&gt;This meant that iteration order had quietly become part of the simulation rules.&lt;/p&gt;

&lt;p&gt;That model does not translate directly to the GPU. Thousands of cells can be processed in parallel, and there is no reliable ordering between them.&lt;/p&gt;

&lt;p&gt;For example, two cells might see the same empty position at the same time and both decide to move into it.&lt;/p&gt;

&lt;p&gt;To handle this, a tick is now split into several stages.&lt;/p&gt;

&lt;p&gt;First, cells make their decisions and write an &lt;code&gt;Intent&lt;/code&gt; describing what they want to do. At this point, the world itself remains unchanged.&lt;/p&gt;

&lt;p&gt;Next come &lt;code&gt;Claim&lt;/code&gt;s. These represent competing requests for shared outcomes. If several cells want to occupy the same position, the conflict is resolved separately according to a predefined rule.&lt;/p&gt;

&lt;p&gt;Only after those conflicts have been resolved does the &lt;code&gt;Apply&lt;/code&gt; stage actually modify the world state.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Frtel519yys41lp1a9i4b.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Frtel519yys41lp1a9i4b.png" alt="Computation order" width="288" height="619"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This approach requires additional temporary memory and several compute passes, but it removes the dependency on the unpredictable execution order of thousands of parallel GPU invocations.&lt;/p&gt;

&lt;h2&gt;
  
  
  How Do You Render 33 Million Cells?
&lt;/h2&gt;

&lt;p&gt;At this point, the world already lives and evolves on the GPU.&lt;/p&gt;

&lt;p&gt;So there is an obvious question: why send all of it back to the CPU just to draw it?&lt;/p&gt;

&lt;p&gt;One option would be to copy the world state into JavaScript after every tick and render it from there. But with tens of millions of positions, transferring that much data would quickly become a bottleneck of its own.&lt;/p&gt;

&lt;p&gt;The new version therefore does not maintain a full CPU-side copy of the world. The data needed for rendering stays on the GPU, while JavaScript mostly handles the UI, camera, and user commands.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxjnlxzg1ldlao8rrxutg.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxjnlxzg1ldlao8rrxutg.png" alt="Simulation at a large scale" width="800" height="634"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Zooming out creates a different problem.&lt;/p&gt;

&lt;p&gt;Eventually, an individual cell becomes smaller than a single pixel. At that point, drawing every cell separately no longer makes much sense.&lt;/p&gt;

&lt;p&gt;Instead, the distant view is built from aggregated regions of the world. The farther the camera zooms out, the less important any individual cell becomes and the more useful the larger picture is: population density, resources, and the distribution of species.&lt;/p&gt;

&lt;h2&gt;
  
  
  Results
&lt;/h2&gt;

&lt;p&gt;For a stress test, I filled the entire available world with living cells.&lt;/p&gt;

&lt;p&gt;Even under that load, the simulation manages roughly &lt;code&gt;5–10&lt;/code&gt; full ticks per second on my machine.&lt;/p&gt;

&lt;p&gt;Of course, &lt;code&gt;5–10 ticks/s&lt;/code&gt; is not some universal measure of WebGPU performance. The actual speed depends on the GPU, browser, simulation settings, and the number of living cells.&lt;/p&gt;

&lt;p&gt;What matters to me is the broader result: a simulation with tens of millions of active objects can run at a practical speed entirely inside a regular browser tab.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;When I started this remake, the main challenge I wanted to explore was simple: how large could a living, evolving world become while still running entirely inside the browser?&lt;/p&gt;

&lt;p&gt;The answer turned out to be: surprisingly large.&lt;/p&gt;

&lt;p&gt;Tens of millions of active objects are a perfectly workable scale. But reaching that scale requires more than simply porting existing CPU code to the GPU. The data layout and the simulation logic themselves have to be redesigned around parallel execution.&lt;/p&gt;

&lt;p&gt;If you'd like to see the simulation in action, the current version is available here: &lt;a href="https://gigafloppa.itch.io/the-strongest-survives" rel="noopener noreferrer"&gt;&lt;strong&gt;The Strongest Survives on itch.io&lt;/strong&gt;&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>demoscene</category>
      <category>gamedev</category>
      <category>simulation</category>
    </item>
  </channel>
</rss>
