Skip to content

Instantly share code, notes, and snippets.

@camel-cdr
Created February 19, 2026 22:54
Show Gist options
  • Select an option

  • Save camel-cdr/bd5b197ab140ad6df259916df1439066 to your computer and use it in GitHub Desktop.

Select an option

Save camel-cdr/bd5b197ab140ad6df259916df1439066 to your computer and use it in GitHub Desktop.
Visualizing the RISC-V Instruction Set

Visualizing the RISC-V Instruction Set

Earlier today, I came across the blog post "Visualizing the ARM64 Instruction Set" and got inspired to give it a shot my self.

After hacking together a quick script and fiddling with the bit order and colors for way too long, I managed to create a decent visualization of the RISC-V instruction encoding. You can find my code below.

The following graphics cover the 64-bit part of the RISC-V ISA, including all ratified 64-bit extensions, with opcodes extracted from the riscv/riscv-opcodes repo.

I mapped the opcodes to 2D coordinates with a Morton space-filling curve. Using one pixel per opcode would've resulted in a way too large image, so I ignored bits 19-16 (rs1) and 7-8 (low part of rd). I choose these bits, because I almost no 32-bit instruction uses the rs1 and rd fields to encode the instruction type. The reason for only ignoring the lower part of rd, is because the "C" extension uses the higher bits to encode the instruction type.

Instruction size breakdown

RISC-V reserves a 3/4th of it's "32-bit" opcode space to encode the RISC-V "C" extension, which encodes "compressed" 16-bit versions of existing 32-bit instructions to reduce code size.

It also reserves part of the encoding space for "custom" vendor instructions and for >32-bit instruction encodings. The specific >32-bit encoding hasn't been ratified yet and is subject to change, so it may allocate the full reserved area or less.

1

32-bit opcode space

The 32-bit opcode space is broken down into 32 major opcodes, which aren't specifically highlighted in this graphic, but you can definitely tell where the borders lie.

My goal with the visualization was to match the ARM64 one, so I tried my best to group RISC-V extensions into broad categories that matched the ARM64 ones:

2
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <immintrin.h>
#define STB_IMAGE_WRITE_IMPLEMENTATION
#include "stb_image_write.h"
#include "encoding.out.h"
/*
* This "encoding.out.h" was auto-generated by running
* 'PYTHONPATH=src python -m riscv_opcodes -c "rv*"' in
* https://github.com/riscv/riscv-opcodes (a2766fd)
* with the following patch applied:
* diff --git a/src/riscv_opcodes/c_utils.py b/src/riscv_opcodes/c_utils.py
* index 198a37f..236fe6f 100644
* --- a/src/riscv_opcodes/c_utils.py
* +++ b/src/riscv_opcodes/c_utils.py
* @@ -15,2 +15,3 @@ def make_c(instr_dict: InstrDict):
* declare_insn_str = ""
* + myexts = set()
* for i in instr_dict:
* @@ -22,3 +23,10 @@ def make_c(instr_dict: InstrDict):
* )
* - declare_insn_str += f'DECLARE_INSN({i.replace(".","_")}, MATCH_{i.upper().replace(".","_")}, MASK_{i.upper().replace(".","_")})\n'
* + myexts.add(instr_dict[i]["extension"][0])
* + declare_insn_str += f'DECLARE_INSN(EXT_{instr_dict[i]["extension"][0]}, {i.replace(".","_")}, MATCH_{i.upper().replace(".","_")}, MASK_{i.upper().replace(".","_")})\n'
* + ext_enum = ""
* + ext_str = ""
* + for e in myexts:
* + ext_enum += ( f'EXT_{e},\n' )
* + ext_str += ( f'"{e}",')
* +
*
* @@ -65,2 +73,7 @@ def make_c(instr_dict: InstrDict):
* {mask_match_str}
* +typedef enum Ext {{
* +EXT_NONE,
* +{ext_enum}EXT_COUNT
* +}} Ext;
* +const char *ext2str[EXT_COUNT] = {{ {ext_str} }};
* {csr_names_str}
*/
#define ARR_LEN(a) (sizeof (a) / sizeof *(a))
typedef struct ExtGroup {
const char *name;
Ext *exts;
} ExtGroup;
#define EXT_custom EXT_COUNT
#define EXT_gt32b (EXT_COUNT+1)
typedef struct Insn { uint32_t ext, match, mask; } Insn;
Insn insns[] = {
{EXT_custom, 0b1011011, 0b1011111 },
{EXT_custom, 0b0101011, 0b0111111 },
{EXT_gt32b, 0b0011111, 0b0011111 },
#define DECLARE_INSN(ext,name,match,mask) {ext, match, mask},
#include "encoding.out.h"
#undef DECLARE_INSN
};
#define SEL 1 // select between first and second image
#if SEL == 1
static const uint32_t BG = 0xFF100408;
static uint32_t colors[] = { 0x5F8E02, 0x002ECB, 0x1A95FF, };
static ExtGroup groups[] = {
{ "16-bit", (Ext[]){ EXT_rv_c, EXT_rv64_c, EXT_rv_c_d, EXT_rv_zcb, EXT_rv64_zcb, 0 } },
{ "custom", (Ext[]){ EXT_custom, 0 } },
{ "32-bit", (Ext[]){ EXT_rv_i, EXT_rv64_i, EXT_rv_m, EXT_rv64_m, EXT_rv_zicond, EXT_rv_zicsr, EXT_rv_zifencei, EXT_rv_zknh, EXT_rv64_zknd, EXT_rv_zksed, EXT_rv_zksh, EXT_rv64_zkne, EXT_rv64_zknh, EXT_rv_zba, EXT_rv64_zba, EXT_rv_zbb, EXT_rv64_zbb, EXT_rv_zbs, EXT_rv64_zbs, EXT_rv_zbc, EXT_rv_zbkb, EXT_rv_zbkx, EXT_rv64_zbkb, EXT_rv_a, EXT_rv64_a, EXT_rv_zabha, EXT_rv_zabha_zacas, EXT_rv_zacas, EXT_rv64_zacas, EXT_rv_zawrs, EXT_rv_s, EXT_rv_sdext, EXT_rv_smrnmi, EXT_rv_ssctr, EXT_rv_svinval, EXT_rv_system, EXT_rv_h, EXT_rv64_h, EXT_rv_svinval_h, EXT_rv_zfbfmin, EXT_rv_zicbo, EXT_rv_zimop, EXT_rv_zfh_zfa, EXT_rv_zfh, EXT_rv64_zfh, EXT_rv_zfhmin, EXT_rv_d_zfhmin, EXT_rv_f, EXT_rv64_f, EXT_rv_f_zfa, EXT_rv_d, EXT_rv64_d, EXT_rv_d_zfa, EXT_rv_q, EXT_rv64_q, EXT_rv_q_zfa, EXT_rv64_q_zfa, EXT_rv_q_zfhmin, EXT_rv_v, EXT_rv_zvbb, EXT_rv_zvbc, EXT_rv_zvkg, EXT_rv_zvkned, EXT_rv_zvknha, EXT_rv_zvksed, EXT_rv_zvksh, EXT_rv_zvfbfmin, EXT_rv_zvfbfwma, 0 } },
};
#elif SEL == 2
static const uint32_t BG = 0xFF332827;
static uint32_t colors[] = { 0xF8F8F2, 0xEFD966, 0x5ACFDC, 0x1F97FD, 0xFF81AE, 0x7226F9, };
static ExtGroup groups[] = {
{ "custom", (Ext[]){ EXT_custom, 0 } },
//{ "C", (Ext[]){ EXT_rv_c, EXT_rv64_c, EXT_rv_c_d, EXT_rv_zcb, EXT_rv64_zcb, 0 } },
{ "general", (Ext[]){ EXT_rv_i, EXT_rv64_i, EXT_rv_m, EXT_rv64_m, EXT_rv_zicond, EXT_rv_zicsr, EXT_rv_zifencei, EXT_rv_zknh, EXT_rv64_zknd, EXT_rv_zksed, EXT_rv_zksh, EXT_rv64_zkne, EXT_rv64_zknh, EXT_rv_zba, EXT_rv64_zba, EXT_rv_zbb, EXT_rv64_zbb, EXT_rv_zbs, EXT_rv64_zbs, EXT_rv_zbc, EXT_rv_zbkb, EXT_rv_zbkx, EXT_rv64_zbkb, EXT_rv_a, EXT_rv64_a, EXT_rv_zabha, EXT_rv_zabha_zacas, EXT_rv_zacas, EXT_rv64_zacas, EXT_rv_zawrs, 0 } },
{ "system", (Ext[]){ EXT_rv_zicsr, EXT_rv_zifencei, EXT_rv_s, EXT_rv_sdext, EXT_rv_smrnmi, EXT_rv_ssctr, EXT_rv_svinval, EXT_rv_system, EXT_rv_h, EXT_rv64_h, EXT_rv_svinval_h, EXT_rv_zicbo, EXT_rv_zimop, 0 } },
{ "FP", (Ext[]){ EXT_rv_zfh_zfa, EXT_rv_zfh, EXT_rv64_zfh, EXT_rv_zfhmin, EXT_rv_d_zfhmin, EXT_rv_f, EXT_rv64_f, EXT_rv_f_zfa, EXT_rv_d, EXT_rv64_d, EXT_rv_d_zfa, EXT_rv_q, EXT_rv64_q, EXT_rv_q_zfa, EXT_rv64_q_zfa, EXT_rv_q_zfhmin, EXT_rv_zfbfmin, 0 } },
{ "V", (Ext[]){ EXT_rv_v, EXT_rv_zvbb, EXT_rv_zvbc, EXT_rv_zvfbfmin, EXT_rv_zvfbfwma, 0 } },
{ "Zvk", (Ext[]){ EXT_rv_zvkg, EXT_rv_zvkned, EXT_rv_zvknha, EXT_rv_zvksed, EXT_rv_zvksh, 0 } },
};
#endif
_Static_assert(ARR_LEN(colors) >= ARR_LEN(groups), "insufficient colors defined");
#if SEL == 1
#define WIDTH (1ull<<13)
#elif SEL == 2
#define WIDTH (1ull<<11)
#endif
#include <immintrin.h>
static uint32_t
perm(uint32_t u) {
// u = _pext_u32(u,0b11111111111100001111111001111111);
u = (u & 0b1111111) | ((u>>2) & (0b1111111<<7)) | ((u>>6) & (0b111111111111<<14));
// reverse bits
u = ((u >> 1) & 0x55555555) | ((u & 0x55555555) << 1);
u = ((u >> 2) & 0x33333333) | ((u & 0x33333333) << 2);
u = ((u >> 4) & 0x0F0F0F0F) | ((u & 0x0F0F0F0F) << 4);
#if SEL == 1
u = __builtin_bswap32(u) >> 6;
#elif SEL == 2
u = __builtin_bswap32(u)<<2 >> 10;
#endif
// interleave for morton code
uint32_t x = _pext_u32(u, 0b01010101010101010101010101010101);
uint32_t y = _pext_u32(u, 0b10101010101010101010101010101010);
return (x+y*WIDTH) & (WIDTH*WIDTH-1);
}
int
main(void)
{
size_t total = 0;
uint32_t *img = malloc(WIDTH*WIDTH * sizeof *img);
for (size_t i = 0; i < WIDTH*WIDTH; ++i)
img[i] = BG;
for (size_t g = 0; g < ARR_LEN(groups); ++g) {
uint32_t color = colors[g];
printf("%s:\t0x%08X\n", groups[g].name, color | 0xFF000000);
for (Ext *e = groups[g].exts; *e; ++e) {
for (size_t i = 0; i < ARR_LEN(insns); ++i) {
if (insns[i].ext != *e) continue;
uint32_t match = insns[i].match, mask = ~insns[i].mask;
match = perm(match); mask = perm(mask);
uint32_t cnt = 1ull << __builtin_popcount(mask);
uint32_t opcode = 0;
for (uint32_t c = 0; c < cnt; ++c) {
++total;
size_t idx = opcode & mask | match;
uint32_t col = img[idx];
img[idx] = col != BG ? col : (color | 0xFF000000);
opcode = (opcode | ~mask) + 1;
}
}
}
}
printf("%zu\n", total);
stbi_write_png("out.png", WIDTH, WIDTH, 4, img, WIDTH*4);
return 0;
}
@dzaima
Copy link

dzaima commented Feb 20, 2026

With instruction bits rearranged differently to make certain opcode patterns more visible, plus interpolating colors for representing encoding space utilization sub-pixel (https://gist.github.com/dzaima/645c92635c2717805b65b22d1d9222b0):
1o 2o

@camel-cdr
Copy link
Author

here are some unratified extensions:

  • red: Zibi (branch immediate)
  • green: additional V extensions (zvabd, zvzip, various dot product things)
  • blue: current IME and SiFive AME proposal
  • yellow: P proposal

This uses a slightly different bit order.

out

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment