code.pablomurad.com

fire-asm

Tiny 16-bit DOS fire demo in x86 assembly

Initial commit

Add the DOS fire demo source, screenshot, and project documentation as the initial repository state.

Made-with: Cursor
author
Pablo Murad
date
commit
37364e6afe439b143df524585984f264026c86a3
changes
5 files +342 −0
@@ -0,0 +1,1 @@+DOSBOX/
@@ -0,0 +1,150 @@+# Fire ASM
+
+![FIRE running in DOSBox-X](fire.png)
+
+Repository: [runawaydevil/fire-asm](https://github.com/runawaydevil/fire-asm)
+
+This is a tiny 16-bit real mode DOS fire demo written in x86 assembly.
+
+No engine, no runtime, no framework. Just BIOS calls, VGA mode 13h, a hand-built
+palette, a scratch buffer, and a loop that keeps pushing heat upward until you
+press a key.
+
+## The Language
+
+The source is NASM-style x86 assembly. It is built as a `.COM` program, which is
+about as bare-metal as DOS application code gets:
+
+- execution starts at offset `0x100`
+- code and data live in the same segment
+- there is no executable header
+- DOS loads the program and jumps straight in
+- BIOS and DOS interrupts are used directly
+
+The first line says it plainly:
+
+```asm
+org 0x100
+```
+
+That tells the assembler to lay out addresses for a DOS `.COM` file. There is
+also a commented `org 0x7c00` path in the file, plus the commented boot
+signature at the bottom, because the same kind of code can be pushed toward an
+MBR-style boot sector with the right constraints.
+
+## Building It
+
+Use NASM to build a flat binary:
+
+```bat
+nasm -f bin fire.asm -o FIRE.COM
+```
+
+Then run it inside DOSBox or DOSBox-X:
+
+```bat
+FIRE.COM
+```
+
+Press any key to leave the demo. The program restores text mode before exiting.
+
+## What It Does
+
+The demo enters VGA mode 13h:
+
+```asm
+mov ax, 0x13
+int 0x10
+```
+
+Mode 13h gives you a 320x200 framebuffer with one byte per pixel. The video
+memory starts at `A000:0000`, so drawing is just writing color indexes into a
+64,000 byte linear buffer.
+
+After switching video modes, the program writes the text `pablomurad.com` using
+BIOS interrupt `10h`, function `13h`. Then it builds the VGA palette by talking
+directly to the DAC ports:
+
+- `0x3c8`: DAC address write register
+- `0x3c9`: DAC data register
+
+The palette starts dark, ramps through red and yellow, then fills the rest with
+white heat. Old school VGA palette tricks make the simulation cheap: the fire
+buffer stores only byte-sized intensity values, and the hardware palette turns
+those values into color.
+
+## The Fire Loop
+
+The bottom row is seeded with pseudo-random heat:
+
+```asm
+imul ax, 181
+add ax, 359
+```
+
+That is a small linear congruential generator. It is not cryptography, it is not
+pretty, but it is fast and good enough to shake the flame.
+
+Each frame, the code walks backward through the buffer. For every pixel, it
+looks at nearby pixels below it:
+
+- directly below
+- below-left
+- below-right
+- two rows below
+
+It adds those values, scales the sum by `15 / 64`, and writes the cooled result
+above:
+
+```asm
+imul ax, 15
+shr ax, 6
+```
+
+That tiny average-and-cool step is the whole fire effect. Heat is born at the
+bottom, smeared upward by neighboring pixels, and slowly loses energy as it
+rises.
+
+## Buffers And Video Memory
+
+The program does not render directly into the visible framebuffer all the time.
+It copies the initial screen into a scratch buffer at a segment offset from the
+program segment, then computes frames there.
+
+At the end of each loop it copies the scratch buffer back to VGA memory:
+
+```asm
+push 0xa000
+pop es
+rep movsw
+```
+
+The copy uses word moves for speed. This is the kind of code where `ds`, `es`,
+`si`, `di`, and the direction flag matter. There is no abstraction layer between
+the program and the machine.
+
+## Text Reflection
+
+The text is part of the trick. The program mirrors some lower fire rows onto the
+area where the text lives, but only where destination pixels are already
+non-zero. That keeps the logo shape while borrowing color and motion from the
+flame below.
+
+In other words: the text is a mask, the fire is the texture, and the CPU is doing
+the compositing one byte at a time.
+
+## Timing And Exit
+
+Frame pacing uses BIOS interrupt `1Ah`, waiting for the system timer tick. That
+tick runs at about 18.2 Hz on a classic PC.
+
+Keyboard polling uses BIOS interrupt `16h`. When a key is waiting, the program
+consumes it, restores text mode with `int 10h`, and exits through DOS interrupt
+`21h`.
+
+## Files
+
+- `fire.asm`: the NASM source.
+- `fire.png`: screenshot of the demo running in DOSBox-X.
+- `FIRE.COM`: generated binary, if you build it locally.
+
@@ -0,0 +1,191 @@+        org 0x100       ; For .com file.
+        ;org 0x7c00      ; For MBR.
+
+section .text
+start:
+        ; Enter mode 13h: 320x200, 1 byte (256 colors) per pixel.
+        mov ax, 0x13
+        int 0x10
+
+        ; Make sure es and ds point to our segment (cs).
+        push cs
+        push cs
+        pop ds
+        pop es
+
+        ; Write string.
+        mov ax, 0x1300          ; ah=13h, al=write mode
+        mov bx, 0xf             ; bh=page number (0), bl=attribute (white)
+        mov cx, (msg_end - msg) ; cx=length
+        mov dx, ((10 << 8) + (40 / 2 - (msg_end - msg) / 2)) ; dh=row, cl=column
+        mov bp, msg             ; es:bp=string address
+        int 0x10
+
+        ; Set up the palette.
+        ; Jare's original FirePal:
+        cli             ; No interrupts while we do this, please.
+        mov dx, 0x3c8   ; DAC Address Write Mode Register
+        xor al, al
+        out dx, al      ; Start setting DAC register 0
+        inc dx          ; DAC Data Register
+        mov cx, (firepal_end - firepal)
+        mov si, firepal
+setpal1:
+        lodsb
+        out dx, al      ; Set DAC register (3 byte writes per register)
+        loop setpal1
+        mov al, 63
+        mov cx, (256 * 3 - (firepal_end - firepal))
+setpal2:
+        out dx, al      ; Set remaining registers to "white heat".
+        loop setpal2
+        sti             ; Re-enable interrupts.
+
+        ; A buffer at offset 0x1000 from our segment will be used for preparing
+        ; the frames. Copy the current framebuffer (the text) there.
+        push 0xa000
+        pop ds
+        push cs
+        pop ax
+        add ax, 0x1000
+        mov es, ax
+        xor si, si
+        xor di, di
+        mov cx, (320 * 200 / 2)
+        cld
+        rep movsw       ; Copy two bytes at a time.
+
+        push es
+        pop ds
+mainloop:
+        ; On entry to the loop, es and ds should point to the scratch buffer.
+
+        ; Since we'll be working "backwards" through the framebuffer, set the
+        ; direction flag, meaning stosb etc. will decrement the index registers.
+        std
+
+        ; Let di point to the pixel to be written.
+        mov di, (320 * 200 - 1)
+
+        ; Write random values to the bottom row.
+        ; For random numbers, use "x = 181 * x + 359" from
+        ; Tom Dickens "Random Number Generator for Microcontrollers"
+        ; http://home.earthlink.net/~tdickens/68hc11/random/68hc11random.html
+        mov cx, 320
+        xchg bp, ax     ; Fetch the seed from bp.
+bottomrow:
+        imul ax, 181
+        add ax, 359
+        xchg al, ah     ; It's the high 8 bits that are random.
+        stosb
+        xchg ah, al
+        loop bottomrow
+        xchg ax, bp     ; Store the seed in bp for next time.
+
+        ; For the next 50 rows, propagate the fire upwards.
+        mov cx, (320 * 50)
+        mov si, di
+        add si, 320     ; si points at the pixel below di.
+propagate:
+        ; Add the pixel below, below-left, below-right and two steps below.
+        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       ; Compute floor(sum * 15 / 64), averaging and cooling.
+        stosb
+        dec si
+        loop propagate
+
+        ; Mirror some of the fire onto the text.
+        mov dx, 15              ; Loop count, decrementing.
+        mov di, (90 * 320)      ; Destination pixel.
+        mov si, (178 * 320)     ; Source pixel.
+mirrorouter:
+        mov cx, 320     ; Loop over each pixel in the row.
+mirrorinner:
+        mov al, [di]    ; Load destination pixel.
+        test al, al     ; Check if its zero.
+        lodsb           ; Load the source pixel into al.
+        jnz mirrorwrite ; For non-zero destination pixel, don't zero al.
+        xor al, al
+mirrorwrite:
+        stosb           ; Write al to the destination pixel.
+        loop mirrorinner
+        add si, 640     ; Bump si to the row below the one just processed.
+        dec dx
+        jnz mirrorouter
+
+        ; Sleep for one system clock tick (about 1/18.2 s).
+        xor ax, ax
+        int 0x1a        ; Returns nbr of clock ticks in cx:dx.
+        mov bx, dx
+sleeploop:
+        xor ax, ax
+        int 0x1a
+        cmp dx, bx
+        je sleeploop
+
+        ; Copy from the scratch buffer to the framebuffer.
+        cld
+        push 0xa000
+        pop es
+        mov cx, (320 * (200 - 3) / 2)
+        xor si, si
+        mov di, (320 * 3)       ; Scroll down three rows to avoid noisy pixels.
+        rep movsw
+
+        ; Restore es to point to the scratch buffer.
+        push ds
+        pop es
+
+        ; Check for key press.
+        mov ah, 1
+        int 0x16
+        jz mainloop
+
+done:
+        ; Fetch key from buffer.
+        xor ah, ah
+        int 0x16
+
+        ; Return to mode 3.
+        mov ax, 0x3
+        int 0x10
+
+        ; Exit with code 0.
+        mov ax, 0x4c00
+        int 0x21
+
+; Data.
+msg: db 'pablomurad.com'
+msg_end:
+
+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
+        db    63,  10,   0,  63,  13,   0,  63,  16,   0,  63,  20,   0
+        db    63,  23,   0,  63,  26,   0,  63,  29,   0,  63,  33,   0
+        db    63,  36,   0,  63,  39,   0,  63,  39,   0,  63,  40,   0
+        db    63,  40,   0,  63,  41,   0,  63,  42,   0,  63,  42,   0
+        db    63,  43,   0,  63,  44,   0,  63,  44,   0,  63,  45,   0
+        db    63,  45,   0,  63,  46,   0,  63,  47,   0,  63,  47,   0
+        db    63,  48,   0,  63,  49,   0,  63,  49,   0,  63,  50,   0
+        db    63,  51,   0,  63,  51,   0,  63,  52,   0,  63,  53,   0
+        db    63,  53,   0,  63,  54,   0,  63,  55,   0,  63,  55,   0
+        db    63,  56,   0,  63,  57,   0,  63,  57,   0,  63,  58,   0
+        db    63,  58,   0,  63,  59,   0,  63,  60,   0,  63,  60,   0
+        db    63,  61,   0,  63,  62,   0,  63,  62,   0,  63,  63,   0
+firepal_end:
+
+        ; For MBR:
+        ;times (510 - ($ - $$)) db 0      ; Pad to 510 bytes
+        ;db 0x55                          ; MBR boot signature.
+        ;db 0xaa