Back to Blog
NewsOctober 24, 202612 min read

How to Run a City Builder on 16 MHz Like SimCity

SimCity ran a whole city on a 16 MHz 386SX by skipping most of the simulation each frame. Here is the mod16 trick, rebuilt in C in six steps.

How to Run a City Builder on 16 MHz Like SimCity

9 min read

Came here from Instagram, LinkedIn, X or YouTube? Here is the full source list you came for. Everything below is built from these, nothing else.

The article

The discussion

In this tutorial you will build the trick that let the original SimCity run a whole city on an Intel 386SX at 16 MHz. It fits in about 20 lines of C. By the end you will have three things: a simulation loop that runs one slice of the city per frame instead of all of it, a version that splits one big job across frames, and a version that decides how much work to do at runtime.

Why bother in 2026? Because the same idea still ships in modern games, and it works in plain app code too. The article that sparked this put it in one question: "Do I really need to run all this code every frame?"

Key takeaways

  • SimCity lists the Intel 386SX in its system requirements, a CPU that ran at 16 MHz. The author first wrote 12.5 MHz, then corrected it.
  • The source code was released under the GPL in 2008 as Micropolis. The name changed because only the code was opened, not the trademark.
  • One function, Simulate() in s_sim.c, runs the city. It takes a mod16 counter that loops 0 to 15 and picks which part of the simulation runs this frame.
  • The cost: a system reacts to the player in 8 frames on average, 15 frames at worst. Fine for a city, a disaster for a shooter.
  • Assetto Corsa (2014) does the same with reflections: it updates only some of the 6 cubemap faces per frame, set by a variable called FACES_PER_FRAME.

What are you building, and why does it work?

The original SimCity had to update traffic, fire, power and crime on hardware the article describes as about a thousand times less powerful than a 1 GHz chip. Calling every update function every frame would have overwhelmed it.

So Maxis did not. The game skips most of the simulation on any given frame.

Where you would normally expect a deltaTime argument, Simulate() takes mod16. That number counts 0, 1, 2 up to 15 and wraps. Each value decides which system gets its turn. The real function calls things like CrimeScan(), FireAnalysis() and CollectTax, but only one slice of them per frame.

Here is what you will write:

  1. A naive loop that updates everything every frame.
  2. A wrapping 0 to 15 frame counter.
  3. A dispatcher that runs one system per slot.
  4. A way to spread a map scan across 16 frames.
  5. A fixed-size chunker for one expensive job, the Assetto Corsa way.
  6. A time-budgeted version that sizes the work at runtime.

The code below is mine, written to show the pattern. It is not the Micropolis source. The article's own snippet was a trimmed version of the real thing, and I am not reproducing it here.

What do you need before you start?

  • A C compiler. gcc or clang is fine.
  • Basic C: functions, switch, for loops, the % operator.
  • For step 6, a browser console or any TypeScript setup. The logic is the same in any language.
  • Optional: the Micropolis source, if you want to read s_sim.c next to this. Search for Micropolis, the GPL release of SimCity.

No game engine needed. Everything here prints to the terminal.

Step 1: What does the naive version look like?

Start with the version that does everything, every frame. This is your baseline.

#include <stdio.h>

void sim_traffic(void) { puts("  traffic"); }
void sim_power(void)   { puts("  power"); }
void sim_fire(void)    { puts("  fire"); }
void sim_crime(void)   { puts("  crime"); }
void sim_taxes(void)   { puts("  taxes"); }

void simulate_naive(void) {
    sim_traffic();
    sim_power();
    sim_fire();
    sim_crime();
    sim_taxes();
}

int main(void) {
    for (int frame = 0; frame < 3; frame++) {
        printf("frame %d\n", frame);
        simulate_naive();
    }
    return 0;
}

Every frame pays for all five systems. On a modern machine that is nothing. On a 16 MHz chip with a real city behind each function, it is the reason your frame rate dies.

Step 2: How do you add the mod16 counter?

Add a counter that loops through 0 to 15. Pass it in the way SimCity passes mod16.

static int frame_counter = 0;

int next_slot(void) {
    int slot = frame_counter;
    frame_counter = (frame_counter + 1) % 16;
    return slot;
}

Why 16? It is the number the original used, and the name says it. You can pick any count, but a power of two lets you swap % 16 for & 15 if you are counting cycles.

Step 3: How do you run one system per frame?

Now replace the naive function with a dispatcher. Each slot runs a different system. In my sketch, power runs twice per cycle and taxes once, just to show you can weight systems by how often they matter.

void simulate(int mod16) {
    switch (mod16) {
    case 0:  sim_traffic(); break;
    case 4:  sim_power();   break;
    case 8:  sim_fire();    break;
    case 10: sim_crime();   break;
    case 12: sim_power();   break;
    case 15: sim_taxes();   break;
    default: break;
    }
}

int main(void) {
    for (int i = 0; i < 32; i++) {
        int slot = next_slot();
        printf("frame %2d (slot %2d)\n", i, slot);
        simulate(slot);
    }
    return 0;
}

Run it. Most frames print nothing. That is the point. Your expensive work now lands on 6 frames out of every 16, and never more than one system at a time.

This is also where you pay the price. If the player builds a power plant right after slot 4 ran, power will not notice until the next slot 4 or slot 12. The article puts the general cost at 8 frames on average and 15 in the worst case for a system to react to the player.

The author's take on that delay is worth quoting: for a city it "might actually feel better than an instant reaction." A city that responds a beat later reads as a city settling, not as lag.

Step 4: How do you split one big scan across 16 frames?

Running one system per frame helps, but a single system can still be too heavy. A crime or fire scan over the whole map is a loop over every tile.

Use the same counter to scan a different band of rows each frame.

#define MAP_W 120
#define MAP_H 100
#define SLICES 16

void update_tile(int x, int y) { (void)x; (void)y; }

void scan_slice(int slice) {
    int rows = (MAP_H + SLICES - 1) / SLICES;
    int start = slice * rows;
    int end = start + rows;
    if (end > MAP_H) end = MAP_H;

    for (int y = start; y < end; y++)
        for (int x = 0; x < MAP_W; x++)
            update_tile(x, y);
}

With 100 rows and 16 slices, each frame touches 7 rows instead of 100. The whole map still gets covered once per cycle. The last slices get fewer rows, or none, because of the clamp, which is fine.

Step 5: How do you cut one expensive job into fixed pieces?

The article's second example is Assetto Corsa (2014). Shiny cars need reflections, and the game renders them with regularly updated cubemaps. A cubemap has 6 faces. Updating all 6 every frame is too expensive.

So the game updates only a few faces per frame, depending on the reflection quality setting. The settings screen does not say this, but the .ini file exposes it as FACES_PER_FRAME.

Here is the pattern, with a stand-in render call:

#define CUBE_FACES 6

static int next_face = 0;

void render_cubemap_face(int face) { printf("  face %d\n", face); }

void update_reflections(int faces_per_frame) {
    for (int i = 0; i < faces_per_frame; i++) {
        render_cubemap_face(next_face);
        next_face = (next_face + 1) % CUBE_FACES;
    }
}

Call update_reflections(1) and a full refresh takes 6 frames. Call it with 2 and it takes 3. Call it with 6 and you are back to the naive version. That single number becomes your quality slider.

Notice it is the same idea as SimCity. A fixed-size portion of work, a cursor that remembers where you stopped, and a wrap.

Step 6: How do you size the work at runtime?

Fixed portions are simple, but sometimes the right amount of work changes. The article points out that for rendering, the size of each portion can be decided at runtime, based on things like screen size or the time since the last update. It names Unreal's Lumen as an example: it does not recalculate all the lighting every frame.

A simple way to try this in your own code is a time budget. Keep a queue of work, and each frame do as much as fits in a few milliseconds.

type Job = () => void;

const queue: Job[] = [];
let cursor = 0;

function runWithBudget(budgetMs: number) {
  const start = performance.now();
  while (queue.length > 0 && performance.now() - start < budgetMs) {
    queue[cursor]();
    cursor = (cursor + 1) % queue.length;
  }
}

function frame() {
  runWithBudget(2);
  requestAnimationFrame(frame);
}

requestAnimationFrame(frame);

On a fast machine the loop walks through more jobs per frame. On a slow one it does fewer and the world updates more slowly, but the frame rate holds. The 2 ms is my number for the example, not from any game. Tune it to your own frame time.

What are the common mistakes?

Putting player input through the slicer. The 8 to 15 frame delay is fine for zoning a neighbourhood. The article is blunt that the same lag in a shooter would be a disaster. Keep input, camera and anything the player feels directly on the every-frame path.

Ignoring what stale data looks like. Sliced work means some state is always a little old. The article notes that this is arguably why Lumen can look weird sometimes, for example when reflections are not fully updated. Decide up front which systems can be visibly behind and which cannot.

Losing the cursor. In steps 4 and 5 the only thing that makes the pattern correct is the variable that remembers where you stopped. Reset it by accident, or share one cursor between two jobs, and parts of your world simply never update.

Slicing things that are already cheap. The article's advice on performance work applies here too: you can usually ignore most of the code. It sets aside about 90 percent of SimCity and goes straight to the city simulation. Profile first, then slice the few systems that actually cost you.

Treating it as a rendering-only trick. The article says frame distribution is most common in rendering code, but SimCity uses it for gameplay. In your own app, things like background syncs, search indexing, analytics batching and AI ticks can often run on the same kind of schedule.

What should you do next?

  • Open the Micropolis source and read Simulate() in s_sim.c. Find where mod16 is used and map each case to a system.
  • Take one loop in your own code that runs every frame or every request, and ask the article's question about it.
  • Add a quality setting like FACES_PER_FRAME to something expensive you render, and see how low you can push it before anyone notices.
  • Try step 6 with a budget that changes based on the last frame's time instead of a fixed 2 ms.

FAQ

What CPU did the original SimCity need?

The game lists Intel's 386SX in its system requirements. The author says that chip ran at 16 MHz at launch. An earlier version of the article said 12.5 MHz, which was a mistake and was corrected.

Is the SimCity source code really public?

Yes. It was released under the GPL in 2008 under the name Micropolis. It is still the original game's code, but only the code was opened, not the SimCity trademark, so it had to ship under a different name.

Does skipping simulation frames make the game feel laggy?

Barely, according to the author. A system takes about 8 frames on average to react to the player and 15 at worst. In a shooter that would be a disaster, but for a city the author argues a slight delay might even feel better than an instant reaction.

Is this technique still used in modern games?

Yes. Assetto Corsa (2014) updates only some of the 6 cubemap faces each frame, controlled by FACES_PER_FRAME in its .ini file. Unreal's Lumen avoids recalculating all its lighting every frame, which the author credits for both how good it looks and the odd moments when reflections lag behind.

Pick one every-frame loop in your code today and slice it into 16. Then browse the free tools on Art of Code, or if you want to do this kind of work for a living, check the latest jobs on Art of Code.

Written by Adithya Guttha, Founder of Art of Code.

#simcity#game development#performance#c programming#micropolis

Sign in to like and save posts.

Curious Adithya

Written by

Curious Adithya

@curious_adithya · 5 followers

Discussion

Comments (0)

Want to join the conversation?

Sign in to comment

No comments yet. Be the first to comment!