A RISC-V processor implemented in the console of Counter-Strike 2 using a pile of .cfg configuration files.
It relies on no mods, plugins, or external programs — it is assembled entirely from the Source 2 engine's built-in console commands such as alias, exec, and echo. It supports the RV32I base instruction set + Zmmul multiplication extension, and can genuinely fetch, decode, and execute instructions, running programs like helloworld and fibonacci. In theory it can be used on VAC-protected servers such as competitive mode, requiring neither sv_cheats 1 nor any external assistance, with output via say_team.
Strictly speaking it is not a "chip" but a CPU emulator written on top of the console alias system and computing at hexadecimal bit-width. The fact that it runs at all — as the author puts it — is already a miracle.
- Features
- Quick Start
- Run Modes
- Compiling Your Own Programs
- Supported Instruction Set
- System Calls
- How It Works
- Memory Model
- Project Structure
- Dependencies & Environment
- Performance
- AI Usage
- FAQ
- Pure
.cfgimplementation: no C++, no plugins — all logic is Source 2 console commands. - Complete RV32I base instruction set: all six instruction formats — R / I / S / B / U / J — are covered.
- Zmmul multiplication extension: supports
mul,mulh,mulhsu,mulhu(no division). - 32 general-purpose registers x0–x31: x0 is hardwired to 0, per the RISC-V spec.
- 64 KB of addressable memory: byte-granularity read/write, with sign-extending
lb/lh/lbu/lhu/lwandsb/sh/sw. - System calls: character output, character input, and halt via
ecall. - Built-in ELF→storage conversion toolchain: cross-compiled RISC-V ELF files can be loaded directly into cfg memory.
- Three run modes: manual single-step, continuous, and asynchronous background execution.
Copy all files inside ./Main (note: not the Main folder itself) into CS2's cfg folder, for example:
D:\SteamLibrary\steamapps\common\Counter-Strike Global Offensive\game\csgo\cfg
Pick a preset storage from ./Storages (e.g. helloworld) and place the entire folder (this time including the folder) into the same CS2 cfg folder above.
Preset images:
| Folder | Contents |
|---|---|
Storages/helloworld |
A C program that prints Hello world! |
Storages/fibonacci |
The Fibonacci sequence |
Storages/speedtest |
An image used for performance testing |
Since output uses say_team, first enter a practice-mode map, then open the console and run:
exec cpu_run_manual
loop
Afterwards, every time a character is output, you must manually type loop again to continue (this prevents the console from flooding and freezing).
Main provides three entry points:
| Entry | Command | Description |
|---|---|---|
| Manual | exec cpu_run_manual |
Type loop again after each output character; most stable |
| Continuous | exec cpu_run |
Just loop straight to the end |
| Async | exec cpu_run_async |
Loops in the background via exec_async, yielding frames with break |
Related control commands (see Main/cpu_predefine.cfg):
cpu_pause: pause execution; afterwardsloopno longer continues.cpu_resume: resume execution.exit: halt the program, printing!!!程序运行完毕!!!.
Output is uniformly prefixed with
[[CPU]]and shown in team chat viasay_team. Control characters (newline, carriage return, etc.) are displayed in a readable form such as[LF],[CR].
The scripts that load ELF / binary into memory are all under ./Compiling, ultimately producing a storage folder (i.e. the RAM image).
- Write your C code in
./Compiling/C(seehelloworld.candsyscall.hfor reference). - Run
Compile.py; it invokes make to cross-compile and outputs a.elfinto./Compiling/Mem. - Run
Store.py, select the ELF file, enter the chunk count and stack chunk count, and the generated storage is placed into./Compiling/storage.
You can also directly run 编译+储存.py: this is a rough compilation script that automatically compiles the code in ./Compiling/C, generates the storage, and copies it to ./Main/storage, overwriting the current memory image.
Assembly with riscv-none-elf-as.py: uses the GNU toolchain (riscv-none-elf-as/ld) to assemble.asmfiles in./Compiling/Assemblyinto.elf.Assembly with rvasm.py: uses the lightweight assembler frompip install rvasm, producing a.mem(hex text).
After compiling, run Store.py as well to write it into cfg storage.
Instruction decoding is done in Main/inst/cpu_inst_predefine.cfg, dispatching by opcode prefix to each instruction type:
| Type | Instructions |
|---|---|
| R | add sub and or xor sll srl sra slt sltu |
| I | addi andi ori xori slti sltiu slli srli srai |
| L | lb lh lw lbu lhu |
| S | sb sh sw |
| B | beq bne blt bge bltu bgeu |
| J | jal jalr |
| U | lui auipc |
| SYSTEM | ecall ebreak |
| Zmmul | mul mulh mulhsu mulhu |
fence / fence.i (the FENCE class) is recognized but ignored — a single-threaded CPU has no use for memory barriers.
ecall dispatches based on the value of a7 (x17) (see Main/inst/cpu_inst_ecall.cfg):
| Number | Name | Behavior |
|---|---|---|
10 |
exit |
Halt |
11 |
print_char |
Output the low 8 bits of a0 (x10) as a character |
12 |
read_char |
Pause and read a character from the console into a0 |
The corresponding C inline wrappers are in Compiling/C/syscall.h: sys_exit(), sys_print_char(), sys_read_char(), sys_print().
The Source engine's alias is essentially a text-substitution macro: when executing a line, the console expands an alias into the string it stores and then executes it piece by piece. Therefore:
- Variable:
alias rX0 0— definesrX0as a "value"; reading it performs the substitution. - Assignment:
alias rX0 5— redefinesrX0. - Function call:
alias foo "echo hi", then executingfooinvokes it. - Branching / conditionals: using
alias 0 .../alias 1 ...maps a single hexadecimal or binary digit to different follow-up actions.
The whole CPU is woven from thousands of lines of such aliases: fetch → decode → read registers → compute → write back → increment PC, expanded and executed step by step in the console.
This project uses hexadecimal as its fundamental bit-width. One hexadecimal digit (0–f) represents 4 bits, and a lookup table from alias 0 ... to alias f ... can process a whole nibble in one step; if pure binary were used instead, the number of expansion layers for instructions and data would multiply several times over, making the already-glacial speed completely unwatchable.
The CPU uses several "internal registers" for data movement (defined in Main/cpu_inner_registers.cfg):
rL: 8 bits (2 hex digits), initial memory read/write.rM: 32 bits (8 hex digits), staging for general-purpose register read/write.rX/rY: 32 bits, operation operands.rT: 32 bits, temporary buffer.rI: the current instruction;bI: the instruction's binary representation.rP: the program counter (PC, 4 hex digits, covering the 64 KB address space).rR: the memory read pointer (a memory read pointer independent ofrP, 4 hex digits, covering the 64 KB address space).
Each "register" is really a chain of alias digits rXn (n=0–7), paired with "loaders" like rXncl to implement per-digit read/write via lookup tables.
Taking a single addi as an example, the rough flow is:
rPpoints at the current instruction address; through memory addressing, the 32-bit instruction is read intorM/rI/bI.cpu_inst_stepreads the low 7 binary bits of the opcode one by one, hitsopcode_Ivia theop_loadlookup table.cpu_inst_type_iextracts the immediate,rs1,funct3, andrd; loadsrs1intorXand the immediate intorY.- Executes
cpu_logic_add_y_to_x(addition is done by the big lookup table generated byScripts/generators/AddGenerate.py). - Writes the result back to
rd, incrementsrPby 4, and moves to the next instruction.
Logic operations (add/subtract, and/or/xor, shifts, comparisons, multiplication) all live in Main/logic/, and most are pre-generated large lookup tables — the 16×16 results are expanded directly into aliases in exchange for "one-step" execution.
- The theoretical total space is 64 KB, addressed from
0x0000using 4 hex digits (16 bits). - Memory is divided into 256-byte "chunks", each corresponding to a
cpu_storage_chunkXX.cfg. Because the complete 64 KB would require far too many files and aliases, this project only generates the chunks containing the memory used by the program and the stack memory. - Each byte is stored as two hex characters; the addresser
cpu_adr_chunkXX.cfgroutes a 4-digit address to the specific chunk and byte. - Several chunks at the high addresses are reserved for use as the stack (the stack chunk count is specified when
Store.pygenerates it). - Out-of-bounds access hits
out_load, which replaces the read/write operation with a no-op, so the console never crashes.
Each generated chunk is further split into several sub-files (__c_0, __c1_0, __lp, etc.), because the alias expansion volume in a single cfg is too large and must be loaded in shards via exec.
RiscV/
├── Main/ # CPU core (all the cfg logic)
│ ├── cpu_main.cfg # Entry point, loads each part in order
│ ├── cpu_predefine*.cfg # Predefined "constants/tools/aliases"
│ ├── cpu_registers*.cfg # x0–x31 general-purpose registers
│ ├── cpu_inner_registers*.cfg # Internal registers rL/rM/rX/rY/rT/rI/rP
│ ├── cpu_run*.cfg # The three run-mode entry points
│ ├── cpu_system_*.cfg # System calls (putchar / readchar)
│ ├── inst/ # Instruction decoding and per-type implementations
│ ├── logic/ # Arithmetic/logic/shift/compare/multiply units
│ ├── storage/ # Currently loaded memory image (overwritten by compile scripts)
│ ├── _cpu_testcases.cfg # Test cases
│ └── _debug/ # Debug memory image
├── Compiling/ # Program → storage compile/load tooling
│ ├── Compile.py # Cross-compiles C code
│ ├── Store.py # Parses ELF/.mem and writes into cfg storage
│ ├── 编译+储存.py # One-click compile + load + copy into Main
│ ├── makefile # Cross-compilation rules
│ ├── compact.ld # Linker script (64KB, code starts at 0)
│ ├── C/ # C sources (helloworld.c, syscall.h)
│ ├── Assembly/ # Assembly tests (speedtest.asm, bugtest.asm)
│ ├── CLI/ # cli_beautify terminal beautification utility
│ └── Mem/ # Build artifacts (.elf/.o/.mem)
├── Scripts/ # Development helper scripts
│ ├── 0ExpansionToolkits.py # Interactive alias loop-expansion tool
│ └── generators/ # Generators for lookup logic (add, shift, and/or, etc.)
├── Storages/ # Preset memory images (helloworld / fibonacci / speedtest)
├── testcase制造方法.md # How to create test cases
└── README.md
To run the CPU you only need CS2 itself — no additional software. The items below are only needed when compiling your own programs or writing cfg code:
| Dependency | Purpose | Install |
|---|---|---|
riscv-none-elf- GNU toolchain |
Cross-compile C / assembly | Install manually (the prefix can be changed at the top of the scripts) |
mingw32-make |
Run the makefile on Windows | Ships with MinGW |
| Python 3 | Run the compile/load scripts | python.org |
pyperclip |
Clipboard read/write in 0ExpansionToolkits.py |
pip install pyperclip |
rvasm |
Lightweight assembly (optional path) | pip install rvasm |
Compiling/CLI/cli_beautify.pydepends onctypes/msvcrton Windows and ontermioson Linux/macOS, so it is cross-platform.
Measured via Compiling/Assembly/speedtest.asm (300 addi + halt), the execution rate reaches an astonishing:
9.1 Hz
Yes — not MHz, not KHz, but Hz.
Due to the limitations of the language itself (alias is text substitution, not native instructions), the whole CPU has no "parallel execution" whatsoever, and every instruction goes through countless text expansions and lookups. Honestly, the fact that this thing runs at all is itself a miracle.
The main CPU emulator code was written entirely by a human, "handcrafted the old-fashioned way." On the AI side, Deepseek-V4-flash was primarily used. The following are the parts generated or assisted by AI:
- The
.elfreading inCompiling/Store.py(parse_elf) - The assembly tests under
Compiling/Assembly/(for testing speed and bugs; entirely AI-generated) - C-related code (used to generate RAM, largely unrelated to the core CPU code)
Compiling/C/syscall.hCompiling/C/helloworld.cCompiling/CLI/*terminal beautification utilityCompiling/compact.ldlinker scriptCompiling/makefile
Scripts/0ExpansionToolkits.pyexpansion tool — the base framework and hexadecimal expansion were done by AI- During development, the AI was consulted to learn the rv32i_zmmul instruction set
- Some test cases in
Main/_cpu_testcases.cfgwere generated by AI - This
README.mdwas polished and expanded by AI based onreadme_待润色.md
Q: Why do I have to enter a practice-mode map?
A: Output uses say_team, and team chat is only displayed properly inside a server/map (practice mode is enough).
Q: Why do I have to type loop again after each output character?
A: The CS2 engine does not allow say_team to be executed too rapidly. Using echo instead would drown the output in the large amount of information produced by exec. Switching to cpu_run (continuous) or cpu_run_async (async, requires sv_cheats 1) avoids the manual loop, but you first need to replace say_team with echo in the output cfg code.
Q: Why doesn't pause work?
A: pause is a reserved CS2 command; the project uses cpu_pause / cpu_resume instead.
Q: How complex a program can it run? A: Memory is only 64 KB and it is extremely slow, so it suits small demo programs (helloworld, fibonacci, etc.) and educational / meme purposes.
Q: Can I modify the instruction set myself?
A: Yes. Decoding is in Main/inst/cpu_inst_predefine.cfg, each instruction in Main/inst/, the compute units in Main/logic/, and the generators in Scripts/generators/.