https://github.com/sysprog21/vga-nyancat
Hardware-accelerated Nyancat animation on VGA display, implemented in Verilog RTL
https://github.com/sysprog21/vga-nyancat
nyancat verilator verilog-hdl
Last synced: 6 days ago
JSON representation
Hardware-accelerated Nyancat animation on VGA display, implemented in Verilog RTL
- Host: GitHub
- URL: https://github.com/sysprog21/vga-nyancat
- Owner: sysprog21
- License: mit
- Created: 2025-10-17T06:23:33.000Z (17 days ago)
- Default Branch: main
- Last Pushed: 2025-10-18T14:35:54.000Z (15 days ago)
- Last Synced: 2025-10-19T05:15:05.621Z (15 days ago)
- Topics: nyancat, verilator, verilog-hdl
- Language: C++
- Homepage:
- Size: 104 KB
- Stars: 9
- Watchers: 1
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- License: LICENSE
Awesome Lists containing this project
README
# VGA Nyancat

Hardware-accelerated Nyancat (Pop-Tart Cat) animation on VGA display,
implemented in Verilog RTL and simulated using [Verilator](https://verilator.org/).
Features real-time hardware scaling, ROM-based animation storage, and a 2-stage rendering pipeline.
Note: This is an educational hardware design project demonstrating VGA timing,
ROM-based graphics, and hardware animation techniques.
The Nyancat character and animation are used under fair use for educational purposes.
## Features
- 12-frame animation cycling at ~11 fps (90ms per frame)
- Real-time 8× hardware scaling from 64×64 source to 512×512 display
- Efficient storage using 4-bit character indices + 14-color palette (230× compression)
- Pipelined rendering with 2-stage ROM lookup for minimal latency
- VESA-compliant timing at 640×480 @ 72Hz (31.5 MHz pixel clock)
- Automated data generation from upstream [klange/nyancat](https://github.com/klange/nyancat) source
## Prerequisites
Ensure that you have the required dependencies installed:
Ubuntu/Debian:
```shell
sudo apt-get install libsdl2-dev verilator python3
```
macOS:
```shell
brew install sdl2 verilator python3
```
## Building and Running
To build and run the interactive simulation:
```shell
make run
```
This will automatically:
1. Download animation source via curl/wget (if needed)
2. Generate animation data files
3. Build the Verilator simulation
4. Launch the interactive display
Interactive controls:
- p key: Save current frame to test.png
- ESC key: Reset animation
- q key: Quit
## Testing
To run automated tests and generate a test frame:
```shell
make check
```
This generates `test.png` containing a single animation frame.
## Code Formatting
Format all Verilog and C++ source files:
```shell
make indent
```
This project follows the `.verilog-style` guidelines for consistent code formatting:
- Verilog files formatted with `verible-verilog-format`
- C++ files formatted with `clang-format`
Install verible from [chipsalliance/verible releases](https://github.com/chipsalliance/verible/releases).
## How It Works
### Data Generation Pipeline
This project automatically extracts animation data from the upstream [klange/nyancat](https://github.com/klange/nyancat) repository and converts it to hardware-friendly format:
```
┌────────────────────────────────────────────────────────────┐
│ 1. Source Acquisition (Automated) │
│ make → Download animation.c (52KB) via curl/wget │
│ → Save to build/animation.c │
│ │
│ 2. Data Extraction (scripts/gen-nyancat.py) │
│ Input: animation.c (ASCII art frames) │
│ Parse: Extract 12 frames of 64×64 character data │
│ Output: Character indices + color palette │
│ │
│ 3. Format Conversion │
│ ASCII characters → 4-bit indices (0-13) │
│ RGB888 colors → 6-bit VGA (RRGGBB) │
│ │
│ 4. Hardware Files Generated │
│ build/nyancat-frames.hex: 49,152 lines (4-bit each) │
│ build/nyancat-colors.hex: 14 colors in 6-bit format │
└────────────────────────────────────────────────────────────┘
```
Character to Index Mapping:
The script maps each ASCII character from the animation to a 4-bit index:
| Char | Index | Color | RGB | VGA 6-bit |
|------|-------|-------|-----|-----------|
| `,` | 0 | Blue background | (0,49,105) | `000001` |
| `.` | 1 | White stars | (255,255,255) | `111111` |
| `'` | 2 | Black border | (0,0,0) | `000000` |
| `@` | 3 | Tan poptart | (255,205,152) | `111110` |
| ... | ... | ... | ... | ... |
| `%` | 13 | Pink cheeks | (255,163,152) | `111010` |
Conversion Process:
1. Parse animation.c: Extract frame data using regex patterns
2. Build color map: Map 14 unique ASCII characters to palette indices
3. Convert frames: Transform each 64×64 character grid to 4-bit indices
4. Generate RGB to VGA: Convert 24-bit RGB to 6-bit VGA format (2R2G2B)
Result: 230× compression (24KB vs 5.4MB for raw RGB888 storage)
### System Architecture
```
┌─────────────────────────────────────┐
│ VGA Nyancat Top Module │
└───────────┬─────────────────┬───────┘
│ │
┌────────────▼──────────┐ │
│ VGA Sync Generator │ │
│ (vga-sync-gen.v) │ │
│ │ │
│ • H/V counters │ │
│ • Sync pulse gen │ │
│ • Pixel coordinates │ │
└────────────┬──────────┘ │
│ │
{x_px, y_px, activevideo} │
│ │
┌────────────▼─────────────────▼──────┐
│ Nyancat Animation Renderer │
│ (nyancat.v) │
│ ┌──────────────────────────────┐ │
│ │ Coordinate Transformation │ │
│ │ • Remove offset │ │
│ │ • Descale by 8 │ │
│ │ • Calculate ROM address │ │
│ └──────────┬───────────────────┘ │
│ │ │
│ ┌──────────▼───────────────────┐ │
│ │ 2-Stage Pipeline │ │
│ │ │ │
│ │ Stage 1: frame_mem[addr] │ │
│ │ → char_idx │ │
│ │ │ │
│ │ Stage 2: color_mem[char_idx]│ │
│ │ → color │ │
│ └──────────┬───────────────────┘ │
│ │ │
└─────────────┼───────────────────────┘
│
rrggbb (6-bit color)
│
▼
VGA Display
```
### Data Flow Pipeline
The rendering pipeline transforms pixel coordinates into colors through multiple stages:
```
Clock Input Stage 1 Stage 2 Stage 3 Output
Cycle Coordinates ROM Addressing Char Lookup Color Lookup
───── ───────────── ────────────── ─────────────── ──────────── ──────
N (x_px, y_px) ──▶ Transform ────────▶ [pipeline reg] ───▶ [pipeline reg] ──▶ (blank)
addr calculated
N+1 (x_px+1, y_px) ─▶ Transform ────────▶ frame_mem[addr] ──▶ [pipeline reg] ──▶ (blank)
addr calculated char_idx fetched
N+2 (x_px+2, y_px) ─▶ Transform ────────▶ frame_mem[addr] ──▶ color_mem[idx] ──▶ color(N)
addr calculated char_idx fetched color fetched ↑
|
2-clock latency ───────────
```
### Memory Organization
```
┌─────────────────────────────────────────────────────────────────────┐
│ Frame Memory (frame_mem): 49,152 × 4 bits = 24 KB │
├─────────────────────────────────────────────────────────────────────┤
│ Frame 0 (4096 entries) ┌──────────────────────┐ │
│ [0..4095] │ 64 × 64 = 4096 │ │
│ │ 4-bit char indices │ │
│ Frame 1 (4096 entries) │ Values: 0-13 │ │
│ [4096..8191] └──────────────────────┘ │
│ │
│ ... │
│ │
│ Frame 11 (4096 entries) ┌──────────────────────┐ │
│ [45056..49151] │ Last frame data │ │
│ └──────────────────────┘ │
│ │
│ ROM Address Calculation: │
│ addr = (frame_index × 4096) + (src_y × 64) + src_x │
└─────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────┐
│ Color Palette (color_mem): 16 × 6 bits = 12 bytes │
├─────────────────────────────────────────────────────────────────────┤
│ Index Color 6-bit (RRGGBB) RGB888 │
│ ───── ────────── ────────────── ─────────────────────── │
│ 0 Dark Blue 000001 ( 0, 49, 105) │
│ 1 White 111111 (255, 255, 255) │
│ 2 Black 000000 ( 0, 0, 0) │
│ ... (10 more colors) │
│ 13 Light Pink 111010 (255, 163, 152) │
│ 14-15 (unused) │
└─────────────────────────────────────────────────────────────────────┘
```
### VGA Timing Diagram
```
One complete frame (640×480 @ 72Hz):
Horizontal timing (per line, 832 pixels):
├────────┬──────────┬───────────┬──────────────────────────┤
│ FP │ SYNC │ BP │ ACTIVE │
│ 24px │ 40px │ 128px │ 640px │
│ │ (hsync=0)│ │ (visible data) │
└────────┴──────────┴───────────┴──────────────────────────┘
◄──────────────────────────────────────────────────────────►
832 pixels × 31.5 MHz = 26.4 µs per line
Vertical timing (per frame, 520 lines):
├────────┬──────────┬───────────┬──────────────────────────┤
│ FP │ SYNC │ BP │ ACTIVE │
│ 9 ln │ 3 ln │ 28 ln │ 480 ln │
│ │ (vsync=0)│ │ (visible lines) │
└────────┴──────────┴───────────┴──────────────────────────┘
◄──────────────────────────────────────────────────────────►
520 lines × 26.4 µs = 13.73 ms per frame (~72.8 Hz)
Active video region (where animation is rendered):
┌───────────────────────────────────────────────────┐
│ 640 × 480 VGA display │
│ ┌─────────────────────────────────┐ │
│ │ │ │
│ │ 512 × 512 Nyancat │ ◄─ Centered horizontally
│ 64 │ (8× scaled from 64×64) │ 64 │
│ px │ │ px │
│ │ │ margin │
│ └─────────────────────────────────┘ │
│ │
└───────────────────────────────────────────────────┘
Note: Bottom 32 lines of animation are clipped (512 > 480)
```
## Command-Line Options
```shell
./obj_dir/Vvga_nyancat --save-png output.png # Save a single frame and exit
./obj_dir/Vvga_nyancat --help # Show help message
```
## Technical Details
### Animation Data Storage
| Parameter | Value | Details |
|-----------|-------|---------|
| Source frame size | 64×64 pixels | Original animation resolution |
| Total frames | 12 | Complete animation loop |
| Storage format | 4-bit indices | Character codes (0-13) |
| Frame memory | 49,152 × 4 bits | 24 KB total (12 × 4096 entries) |
| Color palette | 14 colors × 6 bits | 12 bytes (2R2G2B VGA format) |
| Total ROM | ~24 KB | 230× smaller than raw RGB888 (5.4 MB) |
### VGA Display Timing
| Parameter | Value | Calculation |
|-----------|-------|-------------|
| Resolution | 640×480 @ 72Hz | VESA standard timing |
| Pixel clock | 31.5 MHz | Standard VGA clock |
| Horizontal total | 832 pixels/line | FP(24) + SYNC(40) + BP(128) + ACTIVE(640) |
| Vertical total | 520 lines/frame | FP(9) + SYNC(3) + BP(28) + ACTIVE(480) |
| Line period | 26.4 µs | 832 ÷ 31.5 MHz |
| Frame period | 13.73 ms | 520 × 26.4 µs |
| Refresh rate | 72.8 Hz | 1 ÷ 13.73 ms |
| Clocks/frame | 432,640 | 832 × 520 |
### Animation Timing
| Parameter | Value | Details |
|-----------|-------|---------|
| Frame duration | 90 ms | Target animation speed |
| Clocks/frame | 2,835,000 | 90 ms × 31.5 MHz |
| Animation rate | ~11.1 fps | 31.5 MHz ÷ 2,835,000 |
| Total loop time | 1.08 seconds | 12 frames × 90 ms |
### Hardware Implementation
| Feature | Implementation | Benefit |
|---------|----------------|---------|
| Scaling | 8× nearest-neighbor | Simple bit-shift (÷8 = >>3) |
| Pipeline stages | 2 (frame ROM → palette ROM) | 2-clock latency, full throughput |
| Display area | 512×512 centered | Symmetric margins (64px sides) |
| Coordinate transform | Offset removal + descaling | Minimal logic complexity |
| Frame sequencing | 22-bit counter + 4-bit index | Automatic wrap at 12 frames |
| ROM address calc | Bit concatenation + OR | Zero-delay, no multipliers |
| ROM reads | Synchronous block RAM | Synthesis-friendly implementation |
### Data Generation Automation
The build system automatically handles all data generation:
Makefile workflow:
```
make all
↓
1. Check if build/animation.c exists
↓ (if not)
2. Download animation.c (52KB) via curl or wget
Source: https://raw.githubusercontent.com/klange/nyancat/...
↓
3. Run scripts/gen-nyancat.py build/animation.c build/
↓
4. Generate build/nyancat-frames.hex (49,152 lines)
↓
5. Generate build/nyancat-colors.hex (14 colors)
↓
6. Run Verilator to generate C++ files
↓
7. Compile C++ simulation binary
↓
Build complete (~4.7 seconds from clean state)
```
Available Make targets:
```shell
make all # Build everything (default)
make build # Same as 'all', explicit build target
make run # Build and launch interactive simulation
make check # Build and generate test.png
make clean # Remove build artifacts (keep build/ directory)
make distclean # Remove everything including build/ directory
make regen-data # Force regeneration of animation data
make indent # Format all Verilog and C++ source files
```
Manual data regeneration:
```shell
# Force regeneration using existing upstream source
make regen-data
# Clean everything and rebuild from scratch
make distclean && make all
# Generate from custom source file
python3 scripts/gen-nyancat.py /path/to/animation.c
```
Data file format:
`nyancat-frames.hex` - One hex digit per line (4 bits):
```
// Frame 0
0 ← Background pixel (char ',')
0
1 ← Star pixel (char '.')
...
```
`nyancat-colors.hex` - VGA 6-bit colors with comments:
```
01 // 0: ',' RGB(0,49,105) ← Background
3f // 1: '.' RGB(255,255,255) ← Stars
00 // 2: ''' RGB(0,0,0) ← Black
...
```
## Project Structure
```
vga-nyancat/
├── rtl/ # Hardware RTL modules
│ ├── vga-sync-gen.v # VGA sync generator (640×480@72Hz)
│ │ # • Generates hsync/vsync pulses
│ │ # • Outputs pixel coordinates
│ │ # • Provides activevideo flag
│ │
│ ├── nyancat.v # Nyancat animation renderer
│ │ # • Frame sequencing (12 frames)
│ │ # • Coordinate transformation
│ │ # • 2-stage ROM pipeline
│ │ # • ROM: 49,152×4b + 16×6b
│ │
│ └── vga-nyancat.v # Top-level integration
│ # • Connects sync gen to renderer
│ # • Reset polarity conversion
│
├── sim/ # Simulation testbench
│ └── main.cpp # Verilator + SDL2 wrapper
│ # • SDL2 framebuffer rendering
│ # • Standalone PNG encoder (no deps)
│ # • Interactive controls
│
├── scripts/ # Data generation tools
│ └── gen-nyancat.py # Animation data extractor
│ # • Downloads klange/nyancat source
│ # • Parses ASCII art frames
│ # • Generates hex files
│
├── build/ # Generated files (gitignored)
│ ├── animation.c # Downloaded source (52KB)
│ ├── nyancat-frames.hex # Frame data (49,152 lines)
│ ├── nyancat-colors.hex # Color palette (14 colors)
│ ├── Vvga_nyancat # Simulation binary
│ └── test.png # Generated test image
│
├── Makefile # Build automation
│ # • Data generation
│ # • Verilator compilation
│ # • Test targets
│
└── README.md # This file
```
### Module Hierarchy
```
vga_nyancat (top)
├── vga_sync_gen
│ ├── Inputs: px_clk, reset
│ └── Outputs: hsync, vsync, x_px[9:0], y_px[9:0], activevideo
│
└── nyancat
├── Inputs: px_clk, reset, x_px[9:0], y_px[9:0], activevideo
└── Outputs: rrggbb[5:0]
```
## License
VGA Nyancat is available under a permissive MIT-style license.
Use of this source code is governed by a MIT license that can be found in the [LICENSE](LICENSE) file.