단 16바이트의 x86 어셈블리 코드만으로 컴퓨터 비디오 메모리를 연산 공간으로 활용해 무한한 시어핀스키 프랙탈을 그리고 이를 동시에 오디오 데이터로 해석하여 소리를 만들어내는 기술적 성과를 소개합니다. 극도로 제한된 환경이라는 데모씬(demoscene)의 도전 과제를 해결한 이 코드는 XOR 연산과 셀룰러 오토마톤 원리를 통해 수학적 아름다움과 효율성을 완벽하게 결합했습니다.
번역된 본문
일어나라!(wake up!) 16b
2026년 5월 네덜란드 오멘(Ommen)에서 열린 Outline Demoparty에서 공개되었습니다. 이는 16바이트 x86 어셈블리라는 극한의 환경에서 알고리즘적 밀도(algorithmic density)를 탐구한 결과물입니다.
[영상 보기] [Demozoo 항목]
데모씬(demoscene) 분야에서 극도로 제한된 환경 내에서 무엇을 달성할 수 있는지 탐구하는 것은 매우 가치 있는 기술적 도전입니다. 다음 16바이트의 x86 리얼 모드 DOS 어셈블리 코드는 알고리즘적 밀도를 극한으로 끌어올린 정교한 결과물입니다. 이 코드가 실행되면 컴퓨터의 비디오 메모리를 연산 공간으로 활용하여 무한한 시어핀스키(Sierpinski) 프랙탈을 그리는 동시에, 해당 기하학적 구조를 오디오 데이터로 해석하여 소리를 만들어냅니다.
int 10h ; 2 바이트
mov bh, 0xb8 ; 2 바이트
mov ds, bx ; 2 바이트
L: lodsb ; 1 바이트
sub si, byte 57 ; 3 바이트
xor [si], al ; 2 바이트
out 61h, al ; 2 바이트
jmp short L ; 2 바이트
캔버스: 초깃값이 세팅된 공간(A Primed Void)
이 코드는 표준 BIOS 인터럽트인 int 10h 로 시작합니다. 이는 비디오 모드 0을 초기화하여 40x25 텍스트 모드 그리드를 설정합니다. 이어지는 명령어들은 데이터 세그먼트(DS)를 VGA/CGA 텍스트 버퍼의 물리적 메모리 주소인 0xB800으로 지정합니다. BIOS가 이 인터럽트 동안 화면을 지울 때, 메모리를 완전한 0으로 채우지는 않습니다. 텍스트 모드에서는 각 문자 공간이 ASCII 문자와 색상 속성, 이렇게 두 바이트로 구성됩니다. BIOS는 2,000개의 모든 문자 슬롯을 균일하게 초기화합니다. ASCII 바이트는 0x20(공백 문자)으로, 색상 바이트는 0x07(검은색 배경에 밝은 회색 텍스트)으로 설정됩니다. 화면은 완전히 비어 있는 것처럼 보이지만, 실제로는 균일한 데이터 패턴이 채워진 캔버스입니다.
균일성의 중요성: 우리가 탐구할 수학적 진행 과정은 예측 가능한 환경에 의존합니다. 메모리에 무작위 잔여 데이터가 포함되어 있으면 알고리즘 계산이 이를 그대로 흡수해 오류가 발생합니다. 셀룰러 오토마타(cellular automaton)에서 예기치 않은 비트 하나는 패턴을 망칠 수 있습니다. 상당히 균일한 메모리 공간은 프랙탈이 명확하게 형성될 수 있는 기반을 제공합니다.
엔진: 덧셈 기반 누적 합(Additive Prefix Sums)
프랙탈의 순수한 수학적 원리를 이해하기 위해 잠시 변수들을 분리해 보겠습니다. 기본 0x20 초기화 상태 대신 완벽하게 0이 된 상태를 모델링할 것입니다. 또한 xor 대신 add 를 사용하고, 누산기(accumulator) AL에 값 2가 로드되어 있다고 가정하여 메모리 공간을 한 번에 16바이트씩 앞으로 건너뛰며 살펴보겠습니다.
리얼 모드 DOS 세그먼트는 정확히 65,536바이트로 구성됩니다. 반복당 16바이트씩 앞으로 이동하므로, 세그먼트 전체를 순회하는 데는 정확히 4,096단계가 필요합니다(65536 / 16 = 4096). SI 레지스터가 0xFFFF를 넘어서면 깔끔게 0x0000으로 되돌아갑니다. 루프가 진행됨에 따라 현재 누산기 값을 메모리 셀에 더하고, 업데이트된 값을 다시 누산기로 읽어옵니다. 이것은 효과적으로 누적 합(running prefix sum)을 생성합니다.
4,096은 256(8비트 레지스터의 용량)의 배수이므로 세그먼트가 처음으로 되돌아갈 때 수학적 자리올림이 완벽하게 정렬되어, 전체 스윕이 끝날 때마다 AL이 2로 깔끔하게 재설정됩니다. 세그먼트를 통과하는 패스 p 동안 수정된 k번째 셀의 값은 우리의 초기값인 2로 스케일링된 이항 계수 수열을 따릅니다.
A^{(p)}[k] ≡ 2 \binom{k+p}{p-1} \pmod{256}
다음 표는 16개 메모리 셀에 걸쳐 계산의 처음 16단계를 보여주며, 값들이 행 단위로 어떻게 누적되는지 설명합니다.
패스(Pass) \ 셀(Cell)
결정화: XOR 연산과 시어핀스키 변환(The Sierpinski Shift)
이러한 10진수 값 안에는 더 깊은 패턴이 존재합니다. 이진 덧셈을 수행할 때 비트 평면(bit-planes)은 인접한 위치로 자리올림이 발생합니다. 그러나 연산의 자리올림을 버리고 엄격하게 모듈로 2(modulo 2) 덧셈을 수행하면 배타적 논리합(XOR) 연산이 됩니다. add 대신 xor 를 사용하면 알고리즘이 비트 평면을 분리해 냅니다. 우리가 모델링한 시작 값이 2(이진수 00000010)이기 때문에 비트 1(Bit 1)만 이 특정 계산의 영향을 받습니다. 계단식으로 전개되는 10진수는 0x00과 0x02 사이를 오가는 순수한 토글(toggle) 상태가 됩니다.
이러한 진행 과정은 스티븐 울프람(Stephen Wolfram)의 기본 셀룰러 오토마타에서 '규칙 60(Rule 60)'에 완벽하게 매핑됩니다.
wake up! 16b Released at the Outline Demoparty in May 2026, Ommen, NL An exploration of algorithmic density in 16 bytes of x86 assembly. Watch Video Demozoo Entry In the demoscene, exploring what can be achieved within extreme constraints is a rewarding technical challenge. The following 16 bytes of x86 real-mode DOS assembly code represent a careful exercise in algorithmic density. When executed, it utilizes the computer's video memory as a calculation space to draw an infinite Sierpinski fractal, while simultaneously interpreting that geometry as audio data. int 10h ; 2 bytes mov bh, 0xb8 ; 2 bytes mov ds, bx ; 2 bytes L: lodsb ; 1 byte sub si, byte 57 ; 3 bytes xor [si], al ; 2 bytes out 61h, al ; 2 bytes jmp short L ; 2 bytes 1. The Canvas: A Primed Void The code begins with a standard BIOS interrupt: int 10h . This initializes Video Mode 0, establishing a 40x25 text mode grid. The subsequent instructions point the Data Segment ( DS ) to 0xB800 , the physical memory address of the VGA/CGA text buffer. When the BIOS clears the screen during this interrupt, it does not fill the memory with absolute zeroes. In text mode, every character space consists of two bytes: the ASCII character and the color attribute. The BIOS initializes all 2,000 character slots uniformly: the ASCII byte is set to 0x20 (the Space character), and the color byte is set to 0x07 (Light Gray text on a Black background). While the screen appears completely empty, it is actually a canvas primed with a uniform pattern of data. The Importance of Uniformity: The mathematical progression we are about to explore relies on a predictable environment. If the memory contained random artifact data, the algorithmic calculations would ingest those discrepancies. In a cellular automaton, an unexpected bit can disrupt the pattern. A reasonably uniform memory space provides a foundation for the fractal to emerge clearly. 2. The Engine: Additive Prefix Sums To understand the pure mathematics of the fractal, let us temporarily isolate our variables. We will model a perfectly zeroed state instead of the base 0x20 initialization. Additionally, we will substitute add instead of xor , and step forward by 16 bytes at a time across this memory, assuming the accumulator AL is loaded with the value 2 . A real-mode DOS segment spans exactly 65,536 bytes. By moving forward 16 bytes per iteration, it takes exactly 4,096 steps to traverse the segment (\( 65536 / 16 = 4096 \)). When the SI register advances past 0xFFFF , it wraps cleanly back to 0x0000 . As the loop progresses, it adds the current value of the accumulator to the memory cell, reading the updated value back into the accumulator. This effectively creates a running prefix sum. Because 4,096 is a multiple of 256 (the capacity of our 8-bit register), the mathematical carryover aligns when the segment wraps, cleanly resetting AL to 2 at the end of each full sweep. The value of the \( k \)-th modified cell during pass \( p \) across the segment follows a binomial coefficient sequence, scaled by our initial value of 2: $$A^{(p)}[k] \equiv 2 \binom{k+p}{p-1} \pmod{256}$$ The following table illustrates the first 16 steps of calculation across 16 memory cells, demonstrating how the values accumulate row by row: Pass \ Cell 3. Crystallization: XOR and the Sierpinski Shift A deeper pattern is present within those decimal values. When performing binary addition, the bit-planes carry over into adjacent positions. However, if we discard the arithmetic carry and perform addition strictly modulo 2, we are left with the Exclusive OR (XOR) operation. By using xor instead of add , the algorithm isolates the bit-planes. Because our modeled starting value is 2 (binary 00000010 ), only Bit 1 is ever affected by this specific calculation. The cascading decimal numbers become a pure toggle between 0x00 and 0x02 . This progression maps perfectly to Rule 60 in Stephen Wolfram's elementary cellular automata: $$Cell^{(p)}[k] = Cell^{(p-1)}[k] \oplus Cell^{(p)}[k-1]$$ According to Lucas's Theorem, this XOR relationship is mathematically guaranteed to match the state of Bit 1 from the additive table. We can validate this by visualizing the binary propagation (where '2' denotes Bit 1 being set): Pass \ Cell 4. The Voice of the Machine: Translating Data to Audio A remarkably elegant detail lies in the instruction: out 61h, al Port 61h interfaces with the internal PC speaker. Bit 1 of this port directly controls the speaker cone by pushing it outward when set to 1, and returning it when set to 0. Our routine computes the fractal via XOR, updates the memory, and immediately sends that byte to the speaker port. Because the algorithm specifically isolates and toggles Bit 1, the geometry of the Sierpinski triangle serves as a direct set of instructions for the speaker cone. The execution speed of the CPU establishes the functional sample rate. The patterns of 1s and 0s generated by the fractal yield distinct square waves, varying naturally in pulse width and frequency. The following plots visualize the states from the table above as audio waveforms: When the fractal produces an alternating row (e.g., Pass 2), it outputs a higher-frequency square wave. When the structure introduces larger blocks of zeroes (the empty regions within the triangles, such as in Pass 4), the speaker cone remains stationary, resulting in rhythmic pauses. The resulting audio is a direct sonic representation of the mathematical structure. 5. The 56-Byte Step: Octave Shifts and Diagonal Shears Returning to the actual code, we notice it does not step by 16. The instruction sub si, byte 57 , combined with the increment from lodsb , results in a net movement of -56 bytes per iteration. The routine traverses memory in reverse. This adjustment alters both the auditory frequency and the visual layout of the output. The Audio: 7-Wrap Sweeps and Frequency Halving While a 16-byte step completes a segment sweep in exactly 4,096 iterations, 56 does not divide 65,536 evenly (\( \gcd(56, 65536) = 8 \)). The loop visits only offsets that are multiples of 8, requiring 8,192 iterations to hit all available addresses, and wrapping around the segment 7 times before returning to 0x0000 . Since 8,192 is divisible by 256, the mathematical continuity of the Sierpinski sequence is preserved. However, because the macro-cycle is now 8,192 steps long rather than 4,096, it takes twice as many CPU cycles to complete a pass. This halves the fundamental frequency of the system, dropping the auditory rhythm by one octave , yielding a slower and deeper tone. It is here that the routine's dual-purpose nature truly shines. The algorithm writes directly to the ASCII character bytes, where Bit 1 is exclusively responsible for toggling the speaker cone. Meanwhile, the remaining seven bits mutate into a cascade of pseudo-random ASCII glyphs, providing the chaotic visual texture of the production. Remarkably, sending this entire mixed-data byte directly to system port 61h , which historically manages various low-level motherboard controls, does not disrupt the system. In standard DOS environments and modern emulators, pushing these extra bits to the port is effectively harmless. This elegant coincidence allows the visual character data to safely double as the audio signal without causing a hardware crash. The Visuals: Diagonal Structuring We can also observe how a -56 byte step maps to a 40-character (80-byte) screen width: Stepping backward by 56 bytes is spatially equivalent to moving forward by 24 bytes on an 80-byte grid (\( -56 \equiv 24 \pmod{80} \)). Given that each character space occupies 2 bytes, a 24-byte shift equals exactly 12 columns . The sequence progresses up 1 row and right 12 columns per step. To determine the number of distinct columns visited, we calculate the Greatest Common Divisor: \( \gcd(12, 40) = 4 \). The arithmetic ensures the loop visits exactly one-quarter of the columns (\( 40 / 4 = 10 \)). Consequently, the frac