< 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