< the skull

FIRE IN ASSEMBLY: A TINY DOS FIRE EFFECT
========================================
2026-04-25

I created this project as a small experiment in old-school graphics programming
using 16-bit x86 Assembly. The program runs as a DOS `.COM` executable and
renders a classic fire effect in VGA mode 13h, the famous `320x200` graphics
mode with 256 colors.

Repository: https://github.com/runawaydevil/fire-asm [1]

The goal was not to build something practical in the modern sense. This is not a
Windows GUI program, not a Linux terminal program, and not something meant to
run directly in PowerShell. It is a tiny real-mode DOS program that talks
directly to BIOS interrupts, VGA memory, and I/O ports.

That is exactly what makes it interesting.

A DOS `.COM` Program
--------------------

The file starts with:

  org 0x100       ; For .com file.
  ;org 0x7c00      ; For MBR.

The `org 0x100` directive tells NASM that this program is meant to be loaded as
a DOS `.COM` file. In DOS, `.COM` programs are loaded at offset `0x100`, because
the first 256 bytes of the segment are reserved for the Program Segment Prefix.

There is also a commented alternative:

  ;org 0x7c00      ; For MBR.

That is for a boot-sector style version. A boot sector is loaded at memory
address `0x7C00`, which is why that origin would be used for an MBR experiment.
In this project, though, I am using the safer and simpler `.COM` format. The
MBR-related padding and boot signature are also commented out at the end of the
file:

  ;times (510 - ($ - $$)) db 0      ; Pad to 510 bytes
  ;db 0x55                          ; MBR boot signature.
  ;db 0xaa

Entering VGA Mode 13h
---------------------

The first real action is switching the display into VGA mode 13h:

  mov ax, 0x13
  int 0x10

BIOS interrupt `int 0x10` handles video services. By placing `0x13` in `AX`, I
ask the BIOS to switch the machine into graphics mode `13h`.

Mode 13h gives me:

  * 320 pixels wide
  * 200 pixels tall
  * 256 colors
  * 1 byte per pixel
  * a linear framebuffer at segment `0xA000`

That last part is the fun part. Since each pixel is just one byte, drawing
becomes very direct: write values into memory, and pixels appear on the screen.

Setting Segment Registers
-------------------------

After switching video mode, the program makes sure `DS` and `ES` point to the
same segment as the code:

  push cs
  push cs
  pop ds
  pop es

In 16-bit real mode, segment registers matter a lot. Code, data, and video
memory are addressed through segment:offset pairs. Since the message string and
palette data are stored inside the same binary as the code, I need `DS` and `ES`
to point to the program segment before reading from them.

Writing Text to the Screen
--------------------------

Before the fire animation starts, the program writes a small message:

  mov ax, 0x1300
  mov bx, 0xf
  mov cx, (msg_end - msg)
  mov dx, ((10 << 8) + (40 / 2 - (msg_end - msg) / 2))
  mov bp, msg
  int 0x10

The message itself is:

  msg: db 'www.hanshq.net/fire.html'
  msg_end:

This uses BIOS video interrupt `int 0x10`, function `AH=13h`, which writes a
string to the screen.

The interesting part is this expression:

  (msg_end - msg)

This computes the length of the string at assembly time. Instead of hardcoding
the number of characters, the code lets NASM calculate it from the labels.

This part:

  ((10 << 8) + (40 / 2 - (msg_end - msg) / 2))

packs the row and column into `DX`. The high byte `DH` stores the row, and the
low byte `DL` stores the column. The code places the message around row 10 and
horizontally centers it based on its length.

Programming the Fire Palette
----------------------------

A fire effect depends heavily on color. The program defines a custom palette:

  firepal:
          db     0,   0,   0,   0,   1,   1,   0,   4,   5,   0,   7,   9
          db     0,   8,  11,   0,   9,  12,  15,   6,   8,  25,   4,   4
          db    33,   3,   3,  40,   2,   2,  48,   2,   2,  55,   1,   1
          db    63,   0,   0,  63,   0,   0,  63,   3,   0,  63,   7,   0
          ; ...
  firepal_end:

The values are VGA DAC color values. Each color uses three bytes: red, green,
and blue. VGA DAC channels use values from `0` to `63`, not `0` to `255`.

The palette is loaded using direct I/O port access:

  cli
  mov dx, 0x3c8
  xor al, al
  out dx, al
  inc dx
  mov cx, (firepal_end - firepal)
  mov si, firepal

Port `0x3C8` selects the starting DAC color index. Port `0x3C9`, reached after
`inc dx`, receives the actual RGB values.

Then the program writes the palette:

  setpal1:
          lodsb
          out dx, al
          loop setpal1

After the defined palette ends, the remaining palette entries are filled with
maximum intensity:

  mov al, 63
  mov cx, (256 * 3 - (firepal_end - firepal))
  setpal2:
          out dx, al
          loop setpal2

So the palette starts dark, passes through red and orange tones, then eventually
reaches white heat.

The use of `cli` and `sti` disables and re-enables interrupts while programming
the palette:

  cli
  ; ...
  sti

That is a very low-level detail, and it shows what kind of environment this
program expects: real-mode DOS-style execution, not a protected modern OS
process.

Copying the Framebuffer into a Scratch Buffer
---------------------------------------------

The fire is not computed directly on the visible framebuffer. Instead, the code
creates a scratch buffer in memory.

First, it sets `DS` to video memory:

  push 0xa000
  pop ds

Then it sets `ES` to a buffer located `0x1000` paragraphs after the program
segment:

  push cs
  pop ax
  add ax, 0x1000
  mov es, ax

Then it copies the current screen into that scratch buffer:

  xor si, si
  xor di, di
  mov cx, (320 * 200 / 2)
  cld
  rep movsw

Since `movsw` copies words, each iteration copies two bytes. The screen has `320
200 = 64000` bytes, so the loop runs `320 200 / 2` times.

This is a classic optimization: copy two bytes at a time instead of one.

Generating the Bottom Row
-------------------------

The fire begins at the bottom of the screen. The program writes random intensity
values into the bottom row:

  mov cx, 320
  xchg bp, ax
  bottomrow:
          imul ax, 181
          add ax, 359
          xchg al, ah
          stosb
          xchg ah, al
          loop bottomrow
  xchg ax, bp

This is a small pseudo-random number generator based on:

  x = 181 * x + 359

The code uses the high byte of `AX` as the random-looking value:

  xchg al, ah
  stosb
  xchg ah, al

`stosb` stores one byte at the address pointed to by `ES:DI`.

This bottom row acts as the heat source. Bright random values at the bottom
become the fuel that gets blurred and cooled upward.

Propagating the Fire Upward
---------------------------

The core of the effect is here:

  mov cx, (320 * 50)
  mov si, di
  add si, 320
  propagate:
          xor ax, ax
          mov al, [si]
          add al, [si - 1]
          adc ah, 0
          add al, [si + 1]
          adc ah, 0
          add al, [si + 320]
          adc ah, 0
          imul ax, 15
          shr ax, 6
          stosb
          dec si
          loop propagate

For each pixel, the program looks at several pixels below it:

  [si]
  [si - 1]
  [si + 1]
  [si + 320]

That means:

  * pixel below
  * pixel below-left
  * pixel below-right
  * pixel two rows below

Then it combines those values, multiplies by `15`, and divides by `64` using a
right shift:

  imul ax, 15
  shr ax, 6

This does two things at once:

  * It averages the nearby heat values.
  * It cools the result slightly.

That is the whole fire illusion. Heat starts at the bottom, gets averaged
upward, and loses intensity as it rises.

The algorithm is simple, but visually effective. It is the same general idea
used in many classic demo scene fire effects.

Why the Code Works Backwards
----------------------------

Inside the main loop, the code sets the direction flag:

  std

This makes string operations like `stosb` decrement the index register instead
of incrementing it.

Then it starts from the end of the buffer:

  mov di, (320 * 200 - 1)

So the program processes pixels backwards through the framebuffer.

Later, before copying the final image, it clears the direction flag again:

  cld

This matters because `rep movsw` expects to copy forward. Forgetting to clear
the direction flag would cause nasty memory-copy bugs.

This is one of those Assembly details that looks tiny but can completely break a
program.

Mirroring Fire Onto the Text
----------------------------

The program also mirrors part of the fire effect onto the text area:

  mov dx, 15
  mov di, (90 * 320)
  mov si, (178 * 320)

Then it loops over rows and pixels:

  mirrorouter:
          mov cx, 320
  mirrorinner:
          mov al, [di]
          test al, al
          lodsb
          jnz mirrorwrite
          xor al, al
  mirrorwrite:
          stosb
          loop mirrorinner
          add si, 640
          dec dx
          jnz mirrorouter

The key idea is this:

  test al, al
  ; ...
  jnz mirrorwrite
  xor al, al

If the destination pixel is non-zero, it keeps the incoming fire value. If the
destination pixel is zero, it writes zero.

That means the fire only appears where something already exists in the
destination area, creating a kind of glowing reflection or flame overlay on the
text.

Frame Timing
------------

To avoid running as fast as the CPU allows, the program waits for one BIOS timer
tick:

  xor ax, ax
  int 0x1a
  mov bx, dx
  sleeploop:
          xor ax, ax
          int 0x1a
          cmp dx, bx
          je sleeploop

BIOS interrupt `int 0x1A` reads the system timer tick count. The code waits
until the tick value changes.

A DOS timer tick happens about 18.2 times per second, so this gives the
animation a simple frame delay.

It is crude, but very appropriate for this kind of program.

Copying the Frame to VGA Memory
-------------------------------

After calculating the frame in the scratch buffer, the program copies it to real
video memory:

  cld
  push 0xa000
  pop es
  mov cx, (320 * (200 - 3) / 2)
  xor si, si
  mov di, (320 * 3)
  rep movsw

Again, video memory starts at segment `0xA000` in mode 13h.

The destination starts at:

  mov di, (320 * 3)

This scrolls the copied image down by three rows. The comment explains why:

  ; Scroll down three rows to avoid noisy pixels.

This is a small visual cleanup trick.

Exiting Cleanly
---------------

The program checks for a key press:

  mov ah, 1
  int 0x16
  jz mainloop

BIOS interrupt `int 0x16` handles keyboard input. Function `AH=1` checks whether
a key is available. If no key is pressed, the program jumps back to the main
animation loop.

When a key is pressed, it consumes the key:

  xor ah, ah
  int 0x16

Then it restores text mode:

  mov ax, 0x3
  int 0x10

Finally, it exits through DOS:

  mov ax, 0x4c00
  int 0x21

This is the correct DOS-style way to terminate a `.COM` program.

How to Build and Run It on Windows
----------------------------------

Because this is a DOS real-mode program, it should not be run directly in
PowerShell. PowerShell is only used to assemble the source code and launch the
emulator.

You need two tools:

  * NASM, to assemble the code
  * DOSBox-X, to run the DOS `.COM` program

Install them with `winget`:

  winget install -e --id NASM.NASM
  winget install -e --id joncampbell123.DOSBox-X

After installing, close and reopen PowerShell so the updated `PATH` is loaded.

Then go to the project folder:

  cd C:\Users\pablo\Downloads\asm

Assemble the source file:

  nasm -f bin .\fire.asm -o .\fire.com

The `-f bin` option tells NASM to output a flat binary. That is important
because a DOS `.COM` file is not a modern executable format with headers and
sections. It is basically raw machine code loaded into memory at offset `0x100`.

Then run it with DOSBox-X:

  dosbox-x -c "mount c `"$($PWD.Path)`"" -c "c:" -c "fire.com"

This command does three things:

  -c "mount c `"$($PWD.Path)`""

Mounts the current PowerShell directory as drive `C:` inside DOSBox-X.

  -c "c:"

Switches to that mounted drive.

  -c "fire.com"

Runs the program.

Press any key to exit the fire animation.

Troubleshooting
---------------

If PowerShell says:

  nasm: The term 'nasm' is not recognized

then NASM is either not installed or not in your `PATH`.

First try closing and reopening PowerShell.

Then test:

  nasm -v

If it still fails, find `nasm.exe` manually:

  Get-ChildItem "C:\Program Files", "C:\Program Files (x86)", "$env:LOCALAPPDATA" `
    -Recurse -Filter nasm.exe -ErrorAction SilentlyContinue |
    Select-Object -First 10 FullName

If you find something like:

  C:\Program Files\NASM\nasm.exe

you can run it directly:

  & "C:\Program Files\NASM\nasm.exe" -f bin .\fire.asm -o .\fire.com

If DOSBox-X is not recognized, close and reopen PowerShell as well, then test:

  dosbox-x

Why This Cannot Run Directly in PowerShell
------------------------------------------

This code uses instructions and interrupts like:

  int 0x10
  int 0x16
  int 0x1a
  int 0x21
  out dx, al

These belong to BIOS, DOS, or direct hardware-style programming.

Modern Windows does not let normal programs call BIOS interrupts or directly
write to VGA hardware ports. Also, 64-bit Windows does not run 16-bit DOS
programs natively.

That is why DOSBox-X is necessary. It emulates the old PC environment that this
program expects.

What I Learned
--------------

This project is small, but it touches a lot of low-level concepts:

  * real-mode x86 Assembly
  * DOS `.COM` program layout
  * VGA mode 13h
  * direct framebuffer access
  * BIOS video and keyboard interrupts
  * VGA palette programming
  * pseudo-random number generation
  * pixel propagation algorithms
  * timing through BIOS ticks
  * manual memory copying

The fire effect itself is not magic. It is just a buffer of intensity values, a
random heat source at the bottom, and a cooling average that moves upward. But
that simplicity is exactly what makes it beautiful.

A few lines of Assembly are enough to create something animated, visual, and
alive-looking.

This is why I like projects like this: they strip programming down to the
machine. No engine, no framework, no window manager, no graphics API. Just
registers, memory, pixels, and timing.

Suggested Repository Structure
------------------------------

For the GitHub repository, I would use something like this:

  fire-asm/
  ├── README.md
  ├── fire.asm
  └── media/
      └── fire-demo.gif

Links:
  [1] https://github.com/runawaydevil/fire-asm


< the skull