256-byte DOS intro · HellMood/Desire · Revision 2026
What is this?
Endbot is a complete audio-visual demo that fits in exactly 256 bytes. It runs under DOS (via DosBox-X) and renders in real-time: a robot sprite with progressive bullet damage, a growing explosion, a scrolling checkerboard landscape, and a MIDI soundtrack - all from a single tiny .com file.
Build - ASM → COM
You need FASM (Flat Assembler). One command produces a raw binary with no linker step:
fasm endbot.asm endbot.com
Run - DosBox-X Setup
The intro writes to MIDI port 0x330 for music. Use DosBox-X (not plain DOSBox) for its proper MPU-401 emulation.
dosbox-x.conf
[dosbox] machine=vgaonly memsize=4 [cpu] cputype=386 cycles=auto [midi] mpu401=uart mididevice=fluidsynth # use win32 on Windows, fluidsynth on Linux/macOS fluid.soundfont= # path to a GM .sf2, e.g. /usr/share/sounds/sf2/FluidR3_GM.sf2 [sblaster] sbtype=none [mixer] rate=44100
dosbox-x -conf dosbox-x.conf endbot.com
Press ESC to quit. The code sends MIDI reset (0xFF) before exit so no notes hang open.
Code Walkthrough
1 - File Header & Init
; "Endbot" - 256b intro for DosBox-X ; presented at Revision 2026 ; by HellMood/Desire %define initbp 0x91C ; BP=Timer, use constant to ease calculations org 100h push 0xa000-20*4 ; VGA Memory Adress, slightly offset to beautify pop es ; into ES mov al,0x13 ; 13h : 320 * 200 Pixels in 256 Colors int 0x10 ; Set! mov cl,Nasenatmungsgeraeusch ; Dump the whole Code to MIDI until this point mov di,si ; Align Music Pointer with Pixel Pointer for 1st check
initbp is the time value already present when the program starts for size reasons. All timing is relative to it, so animation is consistent regardless of DosBox-X startup time. int 10h / AL=13h sets VGA mode 13h: 320×200, 256 colors, flat 64 000-byte framebuffer at A000:0000. CL is loaded with the byte length of the combined music+sprite block - used by rep outsb on the first frame boundary to stream it all to MIDI port 0x330 in one shot.
2 - Main Loop & Per-Frame Stuff
StartFrame:
mov dx,0x330 ; Set MIDI Port
sub di,si ; Advance Pixel by 4*n ( +1 by stosb is a nice dither)
jnz StartPixel ; If not at the end of a frame, skip per-frame-stuff
mov ax,bp ;
sub al,initbp&0xFF ; hit four times, at time = 0, 256, 512, 768
jnz NoSound ; Output exactly four MIDI notes, except at start
rep outsb ; CX is 4 except the first time!
NoSound:
mov al,1193182/256/30 ; PIT frequency / High Byte / FPS
out 40h,al ; Set Timer to ~ 30 FPS
hlt ; Sync against the timer
inc bp ; Next time step
xor byte [si-Nasenatmungsgeraeusch+Colors+1-8],cl ; angry flicker!
in al,0x60 ; Key Code in AL
sub al,2 ; Will leave 0xFF in AL on Exit!
jc Quit ; ESC -> Stop Sounds -> END
The outer loop runs once per pixel. DI is the pixel counter; when it wraps to zero (every 65 536 iterations = one full 320×200 frame) the per-frame block executes. Timer sync: writing to PIT port 40h then hlt pauses the CPU until the next hardware interrupt, locking to ~30 FPS. MIDI beats: rep outsb streams CX bytes from [SI] to the MIDI port whenever BP mod 256 == 0 - four beat events over the demo's runtime; CX is 4 on beats 2–4 and the full block length on beat 1. The keyboard check reads scancode port 60h; ESC has scancode 1, subtracting 2 leaves 0xFF with carry set → exit branch.
3 - Per-Pixel: X/Y Decode & Explosion Check
StartPixel:
mov ax,0xcccd ; The Rrrola trick to get X,Y in DL,DH
mul di ; there we go
mov ax,bp ; get timer in AX
sub ax,(768+initbp) ; check if it's explosion time
mov cx,dx ; "DrawSprite" gets x,y in DX as CX
jc NoExplosion ; No explosion yet! (CF != ZF)
jz Flash ; The very moment of impact, flash orange once
The Rrrola trick: multiplying the linear pixel index DI by 0xCCCD and reading the high-word gives approximate X (DL) and Y (DH) without a division instruction - 5 bytes of machine code vs 8+ for a proper divide+modulo. AX = BP − (768 + initbp) gives time-since-explosion: negative (CF set) = not yet; zero (ZF set) = exact impact frame; positive = expanding circle.
4 - Explosion Circle, Flash & Exit
sub ch,al ; After impact, move Bot down
xchg bx,ax ; Save "time since impact" (TSI) in BL
mov al,dh ; Y in AL
sub al,77 ; center Y
imul al ; Y*Y
sub bl,ah ; TSI - Y*Y
mov al,dl ; X in AL
add al,128 ; center X
imul al ; X*X
cmp bl,ah ; TSI - Y*Y - X*X = TSI - R*R = Circle
Flash:
mov al,0x2a ; A beautiful orange value
jz PlotPixel ; ON the circle -> plot orange
jnl BackGround ; INSIDE -> dissolve into background
js NoExplosion ; not yet exploded, and before 128 time steps after TSI
Quit: ; (RET = Quit, top Stack = 0, [0] = int 20h = quit)
out dx,al ; Either has 0xFF (silence) from above ...
ret ; ... or outputs one trash byte after silence xD
The explosion is a mathematically clean expanding circle: X² + Y² = TSI defines the ring edge, growing as time increases. Pixels on the edge are plotted orange (0x2A); pixels inside dissolve into the background. The bot drifts downward after the blast (sub ch,al). Exit uses ret: the stack top holds 0, and address 0 in a .com segment contains int 20h - the DOS "program terminate" call - saves 1 byte.
5 - Sprite: Entrance, Animation & Damage
NoExplosion:
mov ax,bp ; get timer in AX
cmp ax, 256+initbp ; first 256 time steps = no Sprite
jl BackGround
test al,32 ; mirror
jnz NoAniFlip ; on
not al ; x-axis
NoAniFlip:
and al,63 ; filter to last 6 bits
add al,148 ; adjust X position of Sprite
add cl,al ; timebased X zig zag movement of Sprite
js BackGround ; inside signed byte range = Sprite
sub ch,36+8 ; adjust Y position of Sprite
js BackGround ; inside signed byte range = Sprite
DrawSprite: ; Draw the 128x128 Pixel Sprite
mov bx,Sprite ; Sprite data adress
mov ax,bp ; Get timer in AX
sub ax,(512+initbp) ; Time for damage already?
jc SkipDamage ; AL = Time since shooting
xor al,1010101b ; Pseudo Random Impact Location
btc word [bx],ax ; Directly flip sprite bits
SkipDamage:
shrd ax,cx,18 ; SCALE and transfer of local X,Y
test al,16 ; need to mirror?
jz NoFlip ; mirror on y-axis
not al ; if needed
NoFlip:
and ax,00011110b*256 + 00011110b ; filter : range&even
xchg cx,ax ; just to get AX to CX
add bl,ch ; offset Sprite to correct line
mov ax,[bx] ; get the line data
shr ax,cl ; shift down by (mirrored) column
mov bl,Colors-1 ; CLUT -> 0 = transparent
and al,3 ; set zero flag if "transparent" == 0
xlat ; looks up 3 Colors, discarded if 4th
jnz PlotPixel ; if not transparent, directly PlotPixel
The robot sprite (by Steffest/Desire) is 16 rows of 16-bit bitmasks. Each pixel uses 2 bits → 4 values: 0 = transparent, 1–3 = lookup into Colors. The zigzag horizontal movement comes from toggling bit 5 of the timer with test al,32 / not al. btc (bit-test-and-complement) flips one sprite bit per frame from frame 512 onward, accumulating visible bullet holes over time. xlat performs a 1-byte palette lookup: AL = [BX+AL].
6 - Background: Sky & Scrolling Landscape
BackGround:
mov al,0x4e+72 ; a bluetiful color =)
test dh,dh ; are we over the horizon?
jns PlotPixel ; if yes, just plot the Blue
sub dh,-(128+13) ; are we wrapped around to sky again!?
js PlotPixel ; if yes, just plot the Blue
mov ax,bp ; timer as depth, *DIV DANGER*
div dh ; Constant / Y as distance in AL
xchg dx,ax ; DL=distance AL=X
add al,128 ; center X
imul dl ; X' in AH
add dx,bp ; distance += time (plane movement)
xchg dx,ax ; DH= X' AL=distance'
xor dh,al ; checker pattern
imul dh ; distortion texture
aam 9 ; irregular filter & range
add al,212 ; offset to "landscape" colors
PlotPixel:
stosb ; FINALLY, plot the damn pixel!
mov cx,4 ; for sound routines above and bot flicker
jmp StartFrame ; Rinse and Repeat
Pixels with positive DH (below the horizon) are solid blue sky. The landscape uses div dh for a perspective depth value (BP/Y), perspective-corrects the X coordinate, then XORs for a scrolling checker pattern. The "*DIV DANGER*" note flags that DH < 13 would cause a divide fault - the horizon checks above ensure it never reaches zero here. aam 9 (ASCII Adjust after Multiply) is a 2-byte modulo-9, providing irregular color banding across the landscape palette entries. stosb writes the final pixel to ES:[DI] and auto-increments DI; CX is restored to 4 for the music/flicker logic on the next frame boundary.
7 - Data: Sprite, MIDI & Colors
Sprite: dw 0000111101110000b ; Pixels by Steffest / Desire dw 0111110111110100b ; he was so nice to anticipate dw 1111011111111100b ; i need reusable Sprite Data dw 0101111101111100b ; so he left me the following dw 0111111010110000b ; two bytes of actual MIDI data dw 1111111111110000b ; inside the middle of the Bot dw 1111111100000000b ; ************************************** dw 0111111111000000b ; 0xC07F, MIDI, set gunshot on channel 0 dw 1011011111110000b ; ************************************** dw 1011101111110000b ; this (too) is dumped to MIDI at start dw 1011101101111100b ; or is it a coincidence? dw 1011101110111100b ; a 1 in 65536 coincidence? dw 1011101110011111b ; Who knows ;) dw 0111011101111111b dw 1111111111111111b ; this gets modified by the code so ;dw 1111111111111011b ; this patch has to be applied 11 -> 10 dw 1111111111111100b MusicData: db 0xc9,56 ; set drum channel to SFX db 0x99 ; play on drum channel : db 70,0x58 ; helicopter, moderate db 81,0x7F ; wind, maximum Colors: db 0x15,0x2C,0x13 ; bright gray, yellow, dark gray MusicData2: db 0xc1,91,0x91,37,127 ; Setup Bass, Play at Maximum Burst: db 0xb3,0,9,0xc3 ; Burst noise enable plus set (part1) Nasenatmungsgeraeusch: db 125,0x93,40,127 ; set (part2) and play the robo noise Gunsound: db 0x99, 70+3,90,0 ; machine gun + free byte (4 byte align) ExplosionAndTheEnd: db 0xFF,0x90,34,127 ; Kill all sounds, then explosion sound
The sprite bitmap and MIDI music share one continuous memory block. At startup the entire block is streamed to the MIDI port via rep outsb. Row 8 of the sprite contain the bytes 0xC0 and 0x7F - a valid MIDI program-change command (channel 0, patch 127 = gunshot). The comment "a 1 in 65536 coincidence?" is of cause rhetorical: it was designed that way... or was it? ;) The commented-out sprite row shows the original value before the runtime code overwrites it.
Size Tricks
| Trick | Why it saves bytes |
|---|---|
push imm / pop es | 4 bytes vs 5 for mov ax,… / mov es,ax |
| BP as global clock | Reuses existing register, no dedicated counter variable |
mul 0xCCCD for X/Y | 5 bytes vs a full 16-bit divide+modulo sequence |
stosb for pixel write | 1 byte; auto-increments DI for free |
hlt for frame sync | 1 byte halt until next hardware IRQ |
xlat for color lookup | 1 byte table lookup via BX+AL |
btc for sprite damage | Bit flip + memory address in one instruction |
aam 9 for modulo | 2 byte; AH = AL÷9, AL = AL mod 9 |
| Sprite doubles as MIDI init data | Same bytes serve two purposes simultaneously |
ret to quit DOS | 1 byte; stack top = 0 = int 20h = exit |