Fire in Assembly: A Tiny DOS Fire Effect
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
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
.COMprogram
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
.COMprogram 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
