{"id":19704549,"url":"https://github.com/spencertorres/click-v","last_synced_at":"2025-07-23T04:05:20.831Z","repository":{"id":243814099,"uuid":"813378466","full_name":"SpencerTorres/Click-V","owner":"SpencerTorres","description":"A RISC-V emulator built with ClickHouse SQL","archived":false,"fork":false,"pushed_at":"2025-06-05T01:03:55.000Z","size":55,"stargazers_count":31,"open_issues_count":0,"forks_count":1,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-06-05T04:54:25.700Z","etag":null,"topics":["clickhouse","database","risc-v","riscv","riscv-emulator","riscv32","sql"],"latest_commit_sha":null,"homepage":"","language":"Go","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/SpencerTorres.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null}},"created_at":"2024-06-11T01:13:56.000Z","updated_at":"2025-06-05T02:48:00.000Z","dependencies_parsed_at":"2025-06-05T02:52:56.434Z","dependency_job_id":"9bcd9cbd-9e13-4bcc-a18d-6f0f3b2cde24","html_url":"https://github.com/SpencerTorres/Click-V","commit_stats":null,"previous_names":["spencertorres/click-v"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/SpencerTorres/Click-V","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/SpencerTorres%2FClick-V","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/SpencerTorres%2FClick-V/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/SpencerTorres%2FClick-V/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/SpencerTorres%2FClick-V/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/SpencerTorres","download_url":"https://codeload.github.com/SpencerTorres/Click-V/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/SpencerTorres%2FClick-V/sbom","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":266614368,"owners_count":23956358,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","status":"online","status_checked_at":"2025-07-23T02:00:09.312Z","response_time":66,"last_error":null,"robots_txt_status":null,"robots_txt_updated_at":null,"robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["clickhouse","database","risc-v","riscv","riscv-emulator","riscv32","sql"],"created_at":"2024-11-11T21:23:13.806Z","updated_at":"2025-07-23T04:05:20.815Z","avatar_url":"https://github.com/SpencerTorres.png","language":"Go","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Click-V\nA RISC-V emulator built with ClickHouse SQL.\n\nThis emulator makes ClickHouse truly Turing complete. We are one step closer to running ClickHouse in ClickHouse.\n\nThis project/repository isn't dev-friendly yet, I'm just uploading it here as a backup in case my PC catches fire.\n\n## How it works\n\nThe system will react to the following insert command:\n\n```sql\nINSERT INTO clickv.clock (_) VALUES ()\n```\n\nThis command will trigger a large set of branched materialized views and `Null` tables that filter out the program's instructions to simulate reading/writing from registers and memory.\n\nExternal host machine access works via a single UDF with a custom binary format that gets read/written as an `Array(UInt8)`.\n\nThe program is able to perform any logic. Printing to a console table and drawing are built-in.\nIt can also open/close/read/write/seek files and sockets via the ClickOS UDF.\n\nFor more details, see the [architecture](#architecture) section.\n\n### Performance\n\nI tried to use every optimization trick in the book to get this to run fast, unfortunately there is a MAJOR bottleneck to the performance of this emulator due to a bug in ClickHouse KVStorage logic. Because ClickHouse doesn't have an internal KV-type storage engine, I use Redis for registers/memory. But there is a bug with `allow_experimental_analyzer=1` where instead of doing a single `MGET`, it will `SCAN` all keys and *then* `MGET` multiple times.\nI haven't submitted a bug report yet, but I did investigate it. More notes are commented in the file `/sql/click-v.sql:11`.\n\nAs it is now, the CPU runs at around `17hz`, but during early development this was significantly higher. It *can* perform better, but when every almost every instruction depends on a register read, it kills performance quickly. It gets worse with more memory allocated in the emulator.\n\n## How to run\n\nSteps:\n- Set up a ClickHouse v25 image\n- Set up a Redis-like server for registers/memory access (plain redis works fine, dragonfly was slower, there's also a built-in server in `/system/mem`)\n- Run all SQL statements in `/sql/click-v.sql` (confirm your redis host is correct, right now it points to `host.docker.internal:6379`)\n- Load your own RISC-V 32i program into `INSERT INTO clickv.load_program (hex) VALUES ('FFFFFFFF')` (make sure your hex instructions are in the correct direction)\n- Either clock the system via `INSERT INTO clickv.clock (_) VALUES ()`, or use the auto-clock in `/system/clock`\n\nYou can now monitor the program with the following commands:\n- Show program instructions + current instruction: `SELECT * FROM clickv.display_program;`\n- Show all 32 registers: `SELECT * FROM clickv.display_registers;`\n- Show memory (with o parameter for offset): `SELECT * FROM clickv.display_memory(o=1024);`\n- Show console: `SELECT * FROM clickv.display_console FORMAT TSV;`\n- Setup live view (optional): `SET allow_experimental_live_view = 1;`\n- (After frame setup) Show current drawn frame: `SELECT * FROM clickv.display_frame FORMAT RawBLOB;`\n- (After frame setup) Show live-updating frame: `WATCH clickv.display_frame FORMAT RawBLOB;`\n\nFor more help/commands, see the bottom of `/sql/click-v.sql` file.\nROM/RAM/Graphics Memory is configurable.\n\n## Components\n\n### ClickHouse\n\nDepends on ClickHouse v24.\nNo other setup is required for basic emulator.\nFor handling syscalls, you will need to set up the ClickOS UDF, but this is optional.\n\n### Clock\n\n*path: `/system/cmd/clock`*\n\nThis program simply runs the clock for you, as fast as possible.\nWill output clock speed and total cycles to console.\n\n### ClickOS\n\n*path: `/system/cmd/clickos-server`*\n*path: `/system/cmd/clickos-client`*\n\nOptional program to give the emulated program access to the host system/network.\n\nThis is a client/server application.\nThe client runs as a ClickHouse executable UDF, and then forwards requests to the server.\nThe server will then handle all syscalls (such as reading/writing to a file, opening a UDP socket, etc.)\n\nYou will need to set up the UDF in your ClickHouse server. Easiest way is to make two Docker volume binds: one to the UDF XML, and the other to built binary (you must `go build` for your docker env/arch)\n\nRun the server to listen/handle syscalls. File paths are relative to the working directory of the ClickOS server process.\n\n### rs-demo\n\n*path: `/rs-demo`*\n\nThis is a demo rust program that can be compiled to run in the emulator.\nI have some boilerplate for syscalls, with some OS abstractions for `read`, `write`, `seek`, `socket`, `open`, `close`, etc.\nI also have some code that handles drawing to the screen.\n\nTo get the program hex, I made a script called `gethex.sh`.\nYou can copy/paste this directly into the program input for the emulator.\n\nThis program contains a linker script that defines the memory ranges for ROM, RAM, Stack size, and VRAM.\n\n### Mem (Redis-replacement)\n\n*path: `/system/cmd/mem`*\n\nThis program will store the registers/memory for the emulator.\nDragonfly was slow for this use case, Redis was faster, but this program is optimized to use exact amounts of memory + sequential reads.\n\nNote: there is a bug with ClickHouse where **ALL** queries use `SCAN`, even direct `k=1` queries.\nThis is a huge hit to performance, and will require a patch to ClickHouse to fix.\n\n\n### RISC-V Instruction Test suite\n\n*path: `/system/test/instruction_test.go`*\n\nHow do we know any of these instructions do what they're supposed to do?\nTo answer this, I made a unit test for each instruction.\nIt is now much easier to see if the instructions are compliant with the specification when isolated.\n\nThis file will run a test for each instruction, some with different test cases.\nIt also prints out the performance of each instruction. You'll notice some instructions are more costly than others.\n\n\n# Architecture\n\nI will simplify this into several components:\n- Clock\n- Program Counter (PC)\n- Memory\n- Registers\n- Instructions\n- Syscalls\n\n## Clock\n\n*Schema: no schema*\n\nAs the name suggests, this is the clock for the emulated CPU.\nThis is implemented as a `Null` table. When you insert into this, it will cascade down a set of materialized views.\n\n## Program Counter (PC)\n\n*Schema: `value UInt32`*\n\nThis is a `Memory` table with limits to store exactly `1` row.\nIt stores a single `UInt32`, which represents the current instruction.\n\n## Memory\n*Schema: `address UInt32, value UInt8`*\n\nMemory contains the program instructions (ROM), as well as RAM and VRAM (for the display).\n\n#### Engine choice\n\nWhile I originally had this implemented as a `Memory` table, it was clear that this would not\nwork for larger programs.\nWhen writing to memory, it would push out the oldest row.\nIt would also require adding a `timestamp` field of some kind to each row, since it could contain duplicates. `ReplacingMergeTree` was also considered, but this writes to disk, and would have duplicates before the parts are processed (which is likely in a high-speed emulator environment).\n\nIt can be done, but it would require having a lot of duplicated rows, with enough space so that old memory would have a low probability of falling out of the table. Too much memory usage.\n\nSo I then switched to a `Redis` table engine. This is the optimal structure, since it operates as a fast in-memory KV store with no duplicates.\nThis works perfectly, except for how the newer version of ClickHouse ALWAYS runs a full `SCAN` with multiple `MGET` calls.\n\nMemory can be read via a `JOIN` or sub-query, even in multiple bytes.\nMemory can be written in multiple bytes using `arrayJoin` into the `memory` table.\n\n## Registers\n*Schema: `address UInt8, value UInt32`*\n\nRegisters are implemented the same as memory, but with 32 fixed registers.\n\n## Instructions\n\nThe first materialized view hit by the `clock` table is `get_next_instruction`.\nThis will parse the `pc`, `instruction`, `opcode`, and `funct3` and send it to the next layer of materialized views. The idea with these layers is to reduce the number of function calls and queries for parsing the instruction.\n\nThe next layer will then split by instruction type. For example: **R-type**, **I-type**, **S-type**, **jump**, **ecall**, etc.\nThese views have a `WHERE` condition that blocks them from inserting into the next layer of `Null` tables, which again reduces the number of queries/function calls.\n\nWithin each of these types (such as **R-type**) is the materialized views for the individual instruction. At this point it will do the final check to see which instruction it is, and then forward to another `Null` table for executing the instruction. By this point, there's no other path for that instruction, and all the expensive queries can be made.\n\nEach instruction (with the exception of jumps and branches) will have another materialized view at the end that increments the `pc` by `4`. Materialized views are executed in the order they are created, so this works flawlessly for executing sequential logic.\n\nDepending on the instruction, the output will either write to the main `registers` or `memory` table. Instructions can also read from these table via a `JOIN`.\n\nWith the layers of filtering, it keeps the execution path short for the ClickHouse server.\nThis also offers an easy way to measure performance per-instruction, since the original `clock` insert will not return until the last materialized view is finished.\n\n## Syscalls (`ecall`)\n\nRISC-V has a special instruction for returning control to the operating system: `ecall`.\nThe Click-V emulator is able to make use of this special instruction for 3 major features:\n1. writing to a `print` table, to replicate `stdout`\n2. writing to a `frame` table, trigger rendering the data within VRAM into a terminal-displayed frame.\n3. making external calls to the host system via ClickOS (read/write files, communicate over UDP socket, anything else you can imagine)\n\n`ecall` is implemented same as the other instructions, but due to the expensive nature of these calls, they are hidden behind another layer of materialized views to prevent unnecessary sub-queries from being triggered.\n\nThe syscall number is read from register `a7`, and the arguments are passed in the other `aX` registers. Depending on the call, the result/status code will be returned back in `a0`.\n\nAll syscalls have been implemented in the `rs-demo` program.\n\n### print\n\nThis call is really simple, it just reads from memory using `text_ptr` and `text_len`, and then inserts the result into the `print` table.\n\n### draw\n\nThis call will read from video memory and split up the bytes into a terminal-based image with ANSI colors. You can use the `LIVE VIEW` / `WATCH` API to get this to update in real time.\n\n### ClickOS\n\nExternal system access is managed by ClickOS. These calls are able to read/write to/from emulator memory in order to implement file descriptors for interacting with the host system.\n\nAccess to the host system is implemented via a ClickHouse executable UDF. The memory gets inserted/returned as an `Array(UInt8)`.\n\nWith a similar API to the Linux kernel, these usually rely on a `buffer_ptr` and `buffer_len` for exposing program memory.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fspencertorres%2Fclick-v","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fspencertorres%2Fclick-v","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fspencertorres%2Fclick-v/lists"}