{"id":50308055,"url":"https://github.com/ujjwalvivek/baremetal","last_synced_at":"2026-05-28T18:01:44.049Z","repository":{"id":354407904,"uuid":"1223350144","full_name":"ujjwalvivek/baremetal","owner":"ujjwalvivek","description":"A DDA raycaster and terminal game engine. Pure x86-64 assembly with syscalls. No libc and runtime.","archived":false,"fork":false,"pushed_at":"2026-04-28T13:07:28.000Z","size":27,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-04-28T13:33:45.158Z","etag":null,"topics":["assembly","dda-algorithm","x86-64"],"latest_commit_sha":null,"homepage":"https://baremetal.ujjwalvivek.com","language":"Assembly","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/ujjwalvivek.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,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-04-28T08:35:47.000Z","updated_at":"2026-04-28T13:07:35.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/ujjwalvivek/baremetal","commit_stats":null,"previous_names":["ujjwalvivek/baremetal"],"tags_count":null,"template":false,"template_full_name":null,"purl":"pkg:github/ujjwalvivek/baremetal","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ujjwalvivek%2Fbaremetal","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ujjwalvivek%2Fbaremetal/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ujjwalvivek%2Fbaremetal/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ujjwalvivek%2Fbaremetal/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/ujjwalvivek","download_url":"https://codeload.github.com/ujjwalvivek/baremetal/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ujjwalvivek%2Fbaremetal/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":33619972,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-05-28T02:00:06.440Z","response_time":99,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","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":["assembly","dda-algorithm","x86-64"],"created_at":"2026-05-28T18:01:43.427Z","updated_at":"2026-05-28T18:01:44.032Z","avatar_url":"https://github.com/ujjwalvivek.png","language":"Assembly","funding_links":[],"categories":[],"sub_categories":[],"readme":"# BAREMETAL\n\n![Echopoint SVG](https://echopoint.ujjwalvivek.com/svg/badges/custom?leftText=x86-64\u0026rightText=assembly\u0026badgeColor=808000\u0026textColor=ffffff)\n![Echopoint SVG](https://echopoint.ujjwalvivek.com/svg/badges/custom?leftText=Linux+only\u0026rightText=Static+binary\u0026badgeColor=804000\u0026textColor=ffffff)\n![Echopoint SVG](https://echopoint.ujjwalvivek.com/svg/badges/custom?leftText=Intel+syntax\u0026badgeColor=004080\u0026textColor=ffffff)\n![Echopoint SVG](https://echopoint.ujjwalvivek.com/svg/badges/custom?leftText=16KB+code\u0026rightText=128KB+data%2Bbss\u0026badgeColor=400040\u0026textColor=ffffff)\n\n\u003cimg width=\"1000\" alt=\"baremetal\" src=\"https://github.com/user-attachments/assets/72c03fd5-6db2-402a-8e23-807127002295\" /\u003e\n\n## Constraints\n\nEntry point is `_start`. No malloc, printf, memcpy. Every syscall is a raw `syscall` instruction. All memory is statically allocated in `.data` or `.bss`: sizes fixed at link time. The FPU is never touched; trigonometry runs on integer registers via LUTs and Q8 fixed-point. One `write(1, buf, len)` per frame: no per-character writes, no ioctl inside the render path.\n\n## Build\n\n```bash\nnasm -f elf64 -g -F dwarf \u003cfile\u003e.asm -o \u003cfile\u003e.o\nld -static -o baremetal entry.o terminal.o render.o input.o timing.o math.o game.o\n```\n\nDWARF is on by default. Stripped for production. Link order doesn't matter here but the Makefile preserves it for readability.\n\n---\n\n## Module Map\n\n```nasm\nentry.asm      ;_start, fixed-timestep game loop, shutdown\nterminal.asm   ;termios raw mode, alternate screen, TIOCGWINSZ, signal handlers\nrender.asm     ;frame buffer, raycaster projection, minimap, flush\ninput.asm      ;poll+read drain loop, key state flags\ntiming.asm     ;clock_gettime(CLOCK_MONOTONIC), elapsed_ns, nanosleep\nmath.asm       ;sin/cos LUTs (×1024, 360 entries), Q8 helpers, int_to_ascii\ngame.asm       ;world map, player state, DDA raycaster, update_game\n```\n\nCross-module linkage is `global`/`extern` only. No shared headers: symbol names are the ABI.\n\n## Game Loop\n\nFixed 60fps timestep. `FRAME_NS = 16666666`.\n\n```nasm\ninit_terminal → get_terminal_size → init_game → render_init → loop:\n  get_time(time_start)\n  process_input\n  quit_flag? → shutdown\n  update_game\n  render_frame\n  get_time(time_current)\n  elapsed = elapsed_ns(time_start, time_current)\n  if FRAME_NS - elapsed \u003e 0: sleep_remaining(FRAME_NS - elapsed)\n```\n\n`nanosleep` woken by a signal returns early with `EINTR`. The loop doesn't retry: it just runs the next frame. At 60fps the drift is imperceptible.\n\n## Terminal (`terminal.asm`)\n\nInit: `ioctl(TCGETS)` to save termios, then modify in place: clear `ICANON | ECHO | ISIG | IEXTEN` from `c_lflag`, `OPOST` from `c_oflag`, `BRKINT | ICRNL | INPCK | ISTRIP | IXON` from `c_iflag`, set `VMIN=0 VTIME=0`. Apply via `ioctl(TCSETS)`. Then `ESC[?1049h` (alternate screen), `ESC[?25l` (hide cursor), `ESC[2J` (clear once). Signal handlers last.\n\nRestore: `ioctl(TCSETS)` with the saved struct, `ESC[?1049l`, `ESC[?25h`, `ESC[0m`.\n\n`SIGINT`, `SIGTERM`, `SIGSEGV` all call `restore_terminal` then `exit(1)`. The signal handler's `sa_flags` must include `SA_RESTORER` and `sa_restorer` must point to a trampoline that calls `rt_sigreturn` (syscall 15). The kernel requires this on x86-64. Leave it out and you get SIGSEGV on signal return.\n\n`get_terminal_size`: `ioctl(TIOCGWINSZ)`. Falls back to 80×24 if either dimension is zero.\n\nThe kernel `termios` struct layout: not glibc's, which differs:\n\n```nasm\n 0: c_iflag  (4 bytes)\n 4: c_oflag  (4 bytes)\n 8: c_cflag  (4 bytes)\n12: c_lflag  (4 bytes)\n16: c_line   (1 byte)\n17: c_cc[19]: VTIME at [5] (offset 22), VMIN at [6] (offset 23)\n```\n\n## Fixed-Point\n\nQ8: real value encoded as `real × 256`. Integer part in bits 63..8, fraction in 7..0. Addition and subtraction work directly on the encoded values. To get the map cell index: `sar rax, 8`.\n\nThe LUTs are scaled ×1024, not ×256. After a LUT multiply, shift right by 10:\n\n```nasm\nimul rax, [cos_table + rdi*8]\nsar  rax, 10                    ; Q8 result\n```\n\nThe scale matters: ×256 gives 256 discrete sine values, which produces visible stair-stepping in wall heights at typical screen sizes. ×1024 is enough resolution that the quantization is below the display threshold.\n\n## Math (`math.asm`)\n\n`sin_table` / `cos_table`: 360 × `dq` in `.data`, precomputed as `round(sin/cos(i°) × 1024)`. ~5.6KB total. Index as `[table + angle*8]`.\n\n`int_to_ascii(rax=value, rdi=dest)`: divide-by-10 loop into `digit_buf`, write forward. Returns `rdi` past last byte, `rax` = byte count. Preserves `rbx`, `r12`–`r15`. Zero handled as a special case before the loop.\n\n`lut_mul(rdi=Q8, rsi=LUT_val)`: `(rdi × rsi) \u003e\u003e 10`. Returns Q8.\n\n`fp_div(rdi, rsi)`: `(rdi \u003c\u003c 8) / rsi`. Used to compute `delta_dist` in DDA setup.\n\n`abs_val(rdi)`: branchless via `cqo` / `xor` / `sub`.\n\n## Input (`input.asm`)\n\nEach frame: zero all four direction flags, drain stdin with `poll(timeout=0)` + `read(1 byte)` until poll returns 0. Each recognised byte sets its flag and clears only its axis-opposite: W clears S, A clears D, and vice versa. Non-opposing pairs (W+D, W+A) both set. The terminal sends held keys as repeated bytes in the buffer; last byte on each axis wins within the drain, independent of the other axis. `quit_flag` just gets set and is never cleared.\n\n## World Map (`game.asm`)\n\n256-byte array, row-major, 0=open 1=wall, `MAP_WIDTH = MAP_HEIGHT = 16`. Four rooms (NW/NE/SW/SE) connected by an E-W corridor (rows 7–8) and a N-S corridor (cols 7–8). Outer perimeter is solid. Doorways at col 3 and col 12 in the dividing walls at rows 6 and 9.\n\nPlayer spawns at `(7\u003c\u003c8)|128` on both axes: map cell (7,7), fractional offset 0.5, center of the corridor, facing east. The non-integer start position matters: a player exactly on a grid line with an adjacent wall produces `side_dist = 0`, which collapses `perp_dist` to 0 and causes a divide-by-zero in the projection.\n\nCell lookup: `world_map[map_y * 16 + map_x]`.\n\n## DDA Raycaster (`game.asm: cast_ray`)\n\n```nasm\nin:  rdi = ray angle (0..359)\nout: rax = perpendicular wall distance (map units × 1024), min 1\n```\n\n`delta_dist_x/y = |1024² / ray_dir_x/y|`: the distance along the ray between consecutive vertical or horizontal grid crossings. When a ray direction component is zero, the corresponding delta is set to `DDA_INF (0x3FFFFFFFFFFFFFFF)` so that axis is never stepped.\n\nInitial `side_dist` is computed from the fractional part of the Q8 player position: how far the ray travels from the player's subgrid offset to the first grid boundary on each axis. For a ray moving in the negative direction: `(frac × delta) \u003e\u003e 8`. Positive: `((256 - frac) × delta) \u003e\u003e 8`.\n\nThe loop compares `side_dist_x` vs `side_dist_y`, steps the smaller, increments the corresponding map coordinate, checks `world_map`. Max 32 iterations: sufficient for a 16×16 map with solid perimeter.\n\nThe perpendicular distance: `side_dist_at_hit - delta_dist`. This is the distance to the grid line that was just crossed, measured perpendicular to the view plane: not the Euclidean distance to the hit point. It directly cancels the fisheye distortion. No separate cosine correction needed.\n\nWall collision in `update_game` tests X and Y independently: proposed `new_x` checked with current `map_y`, proposed `new_y` checked with the (possibly already updated) `map_x`. Both axes can move in the same frame. That's wall sliding.\n\n## Render (`render.asm`)\n\n128KB static frame buffer. `buf_pos` is the write head. Single `write(1, frame_buffer, buf_pos - frame_buffer)` at end of frame.\n\n`clear_buffer`: resets `buf_pos`, writes `ESC[H`. Cursor home, not clear: the alt screen overwrites in place, no blank flash.\n\n`render_init`: draws the box border and blanks the interior. Called once at startup.\n\n`render_frame` runs in two passes.\n\n**Pass 1**: at entry, compute `render_scr_cols = min(term_cols - 2, 512)` and `render_scr_rows = min(term_rows - 2, 200)`. These drive every loop bound in this frame. For each column `c`:\n\n```\nangle  = player_angle - 30 + (c × 60 / (render_scr_cols - 1)), normalised to [0,359]\ndist   = cast_ray(angle)\nwall_h = (render_scr_rows × 1024) / dist, capped at render_scr_rows\ntop    = (render_scr_rows - wall_h) / 2\nbot    = top + wall_h - 1\nshade  = dist \u003c 2048 → 0x88 (█) | \u003c 4096 → 0x93 (▓) | \u003c 8192 → 0x92 (▒) | else → 0x91 (░)\n```\n\nResults go into `col_char[c]`, `col_top[c]`, `col_bot[c]`: 512-byte `.bss` arrays.\n\n**Pass 2**: row-major. Per row: `append_cursor_move(r+2, 2)`, then per column: space if above `col_top`, `.` if below `col_bot`, `0xE2 0x96 \u003ccol_char[c]\u003e` (3-byte UTF-8 block) if inside. The 3 buffer bytes encode 1 terminal column: don't conflate byte offsets with display positions.\n\n`render_minimap`: runs after Pass 2, before flush. Writes 16×16 ASCII into columns `[term_cols - 16, term_cols - 1]`, rows 2–17. `#` / `.` / `@` (player at `player_x \u003e\u003e 8`, `player_y \u003e\u003e 8`). Overwrites whatever the raycaster put there.\n\n`append_cursor_move(rdi=row, rsi=col)`: emits `ESC[\u003crow\u003e;\u003ccol\u003eH` using `int_to_ascii`. Preserves `rbx`, `r12`, `r13`.\n\n## Timing (`timing.asm`)\n\n`clock_gettime(CLOCK_MONOTONIC)` writes a 16-byte `timespec`. `elapsed_ns` is `(Δsec × 1e9) + Δnsec`. Frame sleep is `nanosleep` with `tv_sec=0` and the nanosecond remainder in `tv_nsec`. The struct is `{tv_sec at 0, tv_nsec at 8}`: putting the nanosecond value in `tv_sec` by accident gives a ~16 million second sleep.\n\n## ABI\n\nSysV x86-64. `rbx`, `rbp`, `r12`–`r15` are callee-saved: push/pop at function entry/exit if used. Everything else is caller-saved; assume it's destroyed after any call. Clobbering a callee-saved register without saving it won't crash immediately: it corrupts whatever the caller stored there, and you'll spend an hour in GDB before tracing it back.\n\nStack is 16-byte aligned before `call`. The return address push leaves it misaligned by 8 on function entry. Functions that call other functions and haven't pushed an odd number of 8-byte values need `sub rsp, 8` to re-align before the first nested call.\n\nSignal handlers run on the same stack. The red zone below `rsp` is not safe to use.\n\n## Known Issues\n\n`col_top` and `col_bot` are single bytes. Values above 255 truncate silently. `MAX_SCREEN_ROWS` is capped at 200, so this doesn't bite in practice.\n\nAngle normalisation does one add or subtract. If `ROT_SPEED` were ever \u003e= 360 this would wrap incorrectly: it's 5, so it won't.\n\nSIGWINCH is not handled. Resize after startup corrupts the border until restart.\n\nDDA iteration cap is 32. Fine for a 16×16 map. Raise it if the map grows past `MAP_WIDTH + MAP_HEIGHT \u003e 32`.\n\n## Debugging\n\n```bash\ngdb ./baremetal\n(gdb) layout asm\n(gdb) layout regs\n(gdb) b cast_ray\n(gdb) b render_frame\n(gdb) si\n(gdb) x/16xb \u0026world_map\n(gdb) x/gd \u0026player_x            # raw Q8 value\n(gdb) p *(long*)\u0026player_x \u003e\u003e 8  # map cell\n```\n\nTerminal stuck in raw mode after a crash: `reset`.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fujjwalvivek%2Fbaremetal","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fujjwalvivek%2Fbaremetal","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fujjwalvivek%2Fbaremetal/lists"}