Expand the abbreviated pointer types to full words on the language surface: ptr -> pointer (a raw address / FFI handle) ptrs -> pointers (a buffer of pointers) The Ludic type name is distinct from LLVM's own `ptr` spelling: llty() maps `pointer`/`pointers` to LLVM `ptr`, and the emitted IR keeps `ptr`, so only the Ludic-level surface changes. Rewrites type annotations across all sources, the 8 hardcoded pointer type-tags, the `pointers`-buffer indexing in emit_addr, the grammars/LSP/JetBrains tokens, and the docs (type-ptr -> type-pointer, type-ptrs -> type-pointers). int/bool keep their conventional short spelling (like Math). Reseeded; C-free fixpoint holds; all suites green (45/24/29); site + check.py OK. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
486 lines
13 KiB
Text
486 lines
13 KiB
Text
# ============================================================================
|
|
# runtime/native/core.ludic — the Ludic runtime, written in Ludic.
|
|
#
|
|
# This is what the native backend links instead of a C runtime. Everything a
|
|
# game touches through a builtin — the framebuffer, rectangles, text, the
|
|
# registers, the RNG, input, the frame dump — is implemented right here in
|
|
# Ludic and compiled to machine code by ludicc like any other Ludic source.
|
|
#
|
|
# Underneath it there is exactly one layer, and it is not C: the compiler
|
|
# intrinsics (mem_alloc, peek8/poke8, peek32/poke32, file_open/file_write,
|
|
# read_byte, …) lower to direct calls into the platform's C ABI — malloc,
|
|
# fwrite, getchar. That ABI is the operating system's interface, the same floor
|
|
# Rust and Swift stand on. No C source is compiled at any point in the build.
|
|
#
|
|
# A builtin `clear(c)` in a game resolves to `rt_clear(c)` here; that is the
|
|
# whole protocol. Swap this file and you have swapped the runtime.
|
|
#
|
|
# This file is a fragment: ludicc splices it into every native build. It is not
|
|
# a `game`/`module` block of its own.
|
|
# ============================================================================
|
|
|
|
# ---- state ----------------------------------------------------------------
|
|
var rt_fb: words = null # framebuffer, one i32 (0x00RRGGBB) per pixel
|
|
var rt_fbw: int = 320
|
|
var rt_fbh: int = 240
|
|
var rt_regs: words = null # the 64 general-purpose game registers
|
|
var rt_rng: int = 305419896 # xorshift32 state
|
|
var rt_alive: int = 1 # platform still running?
|
|
|
|
# 5x7 glyphs for ASCII 32..90, 7 rows per glyph, each row a 5-bit mask stored
|
|
# biased by '0' so the whole font is one printable string literal.
|
|
function rt_font() -> string {
|
|
return "00000004444404000000000000000000000IJ4:FC000000000000000000000000000000E>O>E0044O4400000448000O000000004012448@@>ACEIA>4<4444>>A168@ON11>11N26:BO22O@N11A>>@@NAA>O124888>AA>AA>>AA?11>04004000000000024842000O0O000842480>A164040000000>AAOAAANAANAAN>A@@@A>LBAAABLO@@L@@OO@@L@@@>A@GAA>AAAOAAA>44444>7222BB<ABDHDBA@@@@@@OAKEEAAAAIEECAA>AAAAA>NAAN@@@>AAAEB=NAANDBA>A@>1A>O444444AAAAAA>AAAAA:4AAAEEKAAA:4:AAAA:4444O1248@O"
|
|
}
|
|
|
|
function rt_init() -> void {
|
|
rt_fb = words(320 * 240)
|
|
rt_regs = words(64)
|
|
fill(rt_regs, 0, 64 * 4)
|
|
rt_map = bytes(96 * 64)
|
|
fill(rt_map, 32, 96 * 64)
|
|
rt_statusbuf = bytes(96)
|
|
rt_statusbuf[0] = 0
|
|
rt_image_init()
|
|
rt_tt_init()
|
|
rt_ui_init()
|
|
rt_clear(0)
|
|
if is_windowed() {
|
|
win_open(rt_fbw, rt_fbh, 3, game_title())
|
|
}
|
|
}
|
|
|
|
function rt_shutdown() -> void {
|
|
if is_windowed() {
|
|
win_close()
|
|
return
|
|
}
|
|
rt_dump_ppm("out.ppm")
|
|
}
|
|
|
|
# ---- framebuffer ----------------------------------------------------------
|
|
function rt_screen_w() -> int { return rt_fbw }
|
|
function rt_screen_h() -> int { return rt_fbh }
|
|
|
|
function rt_clear(c: int) -> void {
|
|
let n = rt_fbw * rt_fbh
|
|
for i in 0 .. n {
|
|
rt_fb[i] = c
|
|
}
|
|
}
|
|
|
|
function rt_put_px(x: int, y: int, c: int) -> void {
|
|
if x < 0 { return }
|
|
if y < 0 { return }
|
|
if x >= rt_fbw { return }
|
|
if y >= rt_fbh { return }
|
|
rt_fb[y * rt_fbw + x] = c
|
|
}
|
|
|
|
function rt_fill_rect(x: int, y: int, w: int, h: int, c: int) -> void {
|
|
let x0 = max(0, x)
|
|
let y0 = max(0, y)
|
|
let x1 = min(rt_fbw, x + w)
|
|
let y1 = min(rt_fbh, y + h)
|
|
var j = y0
|
|
while j < y1 {
|
|
let row = j * rt_fbw
|
|
var i = x0
|
|
while i < x1 {
|
|
rt_fb[row + i] = c
|
|
i = i + 1
|
|
}
|
|
j = j + 1
|
|
}
|
|
}
|
|
|
|
function rt_frame_rect(x: int, y: int, w: int, h: int, c: int) -> void {
|
|
rt_fill_rect(x, y, w, 1, c)
|
|
rt_fill_rect(x, y + h - 1, w, 1, c)
|
|
rt_fill_rect(x, y, 1, h, c)
|
|
rt_fill_rect(x + w - 1, y, 1, h, c)
|
|
}
|
|
|
|
# A straight line by Bresenham's algorithm — integer only, any direction.
|
|
function rt_line(x0: int, y0: int, x1: int, y1: int, c: int) -> void {
|
|
var x = x0; var y = y0
|
|
let dx = abs(x1 - x0); let dy = 0 - abs(y1 - y0)
|
|
var sx = 0 - 1; if x0 < x1 { sx = 1 }
|
|
var sy = 0 - 1; if y0 < y1 { sy = 1 }
|
|
var err = dx + dy
|
|
while true {
|
|
rt_put_px(x, y, c)
|
|
if (x == x1) and (y == y1) { return }
|
|
let e2 = 2 * err
|
|
if e2 >= dy { err = err + dy; x = x + sx }
|
|
if e2 <= dx { err = err + dx; y = y + sy }
|
|
}
|
|
}
|
|
|
|
# A circle outline by the midpoint algorithm (eight-way symmetry).
|
|
function rt_circle(cx: int, cy: int, r: int, c: int) -> void {
|
|
if r < 0 { return }
|
|
var x = r; var y = 0; var err = 1 - r
|
|
while x >= y {
|
|
rt_put_px(cx + x, cy + y, c); rt_put_px(cx + y, cy + x, c)
|
|
rt_put_px(cx - y, cy + x, c); rt_put_px(cx - x, cy + y, c)
|
|
rt_put_px(cx - x, cy - y, c); rt_put_px(cx - y, cy - x, c)
|
|
rt_put_px(cx + y, cy - x, c); rt_put_px(cx + x, cy - y, c)
|
|
y = y + 1
|
|
if err < 0 { err = err + 2 * y + 1 }
|
|
else { x = x - 1; err = err + 2 * (y - x) + 1 }
|
|
}
|
|
}
|
|
|
|
# A filled disc — one horizontal span per row, width from the circle equation.
|
|
function rt_fill_circle(cx: int, cy: int, r: int, c: int) -> void {
|
|
if r < 0 { return }
|
|
let r2 = r * r
|
|
var dy = 0 - r
|
|
while dy <= r {
|
|
var dx = 0
|
|
while (dx + 1) * (dx + 1) + dy * dy <= r2 { dx = dx + 1 }
|
|
rt_fill_rect(cx - dx, cy + dy, 2 * dx + 1, 1, c)
|
|
dy = dy + 1
|
|
}
|
|
}
|
|
|
|
# A triangle outline — three lines.
|
|
function rt_triangle(x0: int, y0: int, x1: int, y1: int, x2: int, y2: int, c: int) -> void {
|
|
rt_line(x0, y0, x1, y1, c)
|
|
rt_line(x1, y1, x2, y2, c)
|
|
rt_line(x2, y2, x0, y0, c)
|
|
}
|
|
|
|
# A filled triangle — bounding-box scan with an edge-sign inside test.
|
|
function rt_fill_triangle(x0: int, y0: int, x1: int, y1: int, x2: int, y2: int, c: int) -> void {
|
|
let minx = min(x0, min(x1, x2)); let maxx = max(x0, max(x1, x2))
|
|
let miny = min(y0, min(y1, y2)); let maxy = max(y0, max(y1, y2))
|
|
var py = miny
|
|
while py <= maxy {
|
|
var px = minx
|
|
while px <= maxx {
|
|
let d0 = (x1 - x0) * (py - y0) - (y1 - y0) * (px - x0)
|
|
let d1 = (x2 - x1) * (py - y1) - (y2 - y1) * (px - x1)
|
|
let d2 = (x0 - x2) * (py - y2) - (y0 - y2) * (px - x2)
|
|
let neg = (d0 < 0) or (d1 < 0) or (d2 < 0)
|
|
let pos = (d0 > 0) or (d1 > 0) or (d2 > 0)
|
|
if not (neg and pos) { rt_put_px(px, py, c) }
|
|
px = px + 1
|
|
}
|
|
py = py + 1
|
|
}
|
|
}
|
|
|
|
# Windowed: hand the framebuffer to the platform layer, which blits it into
|
|
# the view. Headless: nothing to do until shutdown writes the last frame out.
|
|
function rt_present() -> void {
|
|
if is_windowed() {
|
|
win_present(rt_fb, rt_fbw, rt_fbh)
|
|
}
|
|
}
|
|
|
|
# ---- text -----------------------------------------------------------------
|
|
function rt_glyph(x: int, y: int, ch: int, colour: int, sc: int) -> void {
|
|
var c = ch
|
|
if c >= 97 {
|
|
if c <= 122 { c = c - 32 }
|
|
}
|
|
if c < 32 { return }
|
|
if c > 90 { return }
|
|
let base = (c - 32) * 7
|
|
let font = rt_font()
|
|
for row in 0 .. 7 {
|
|
let bits = font[base + row] - 48
|
|
var b = bits
|
|
for cc in 0 .. 5 {
|
|
let on = b / 16
|
|
if on == 1 {
|
|
rt_fill_rect(x + cc * sc, y + row * sc, sc, sc, colour)
|
|
}
|
|
b = (b - on * 16) * 2
|
|
}
|
|
}
|
|
}
|
|
|
|
function rt_text(x: int, y: int, s: string, colour: int, sc: int) -> void {
|
|
var i = 0
|
|
var cx = x
|
|
var ch = s[0]
|
|
while ch != 0 {
|
|
rt_glyph(cx, y, ch, colour, sc)
|
|
cx = cx + 6 * sc
|
|
i = i + 1
|
|
ch = s[i]
|
|
}
|
|
}
|
|
|
|
function rt_text_int(x: int, y: int, n: int, colour: int, sc: int) -> void {
|
|
if n == 0 {
|
|
rt_glyph(x, y, 48, colour, sc)
|
|
return
|
|
}
|
|
var v = n
|
|
var cx = x
|
|
if v < 0 {
|
|
rt_glyph(cx, y, 45, colour, sc)
|
|
cx = cx + 6 * sc
|
|
v = 0 - v
|
|
}
|
|
var digits = 0
|
|
var t = v
|
|
while t > 0 {
|
|
digits = digits + 1
|
|
t = t / 10
|
|
}
|
|
var p = digits
|
|
while p > 0 {
|
|
var div = 1
|
|
for k in 1 .. p {
|
|
div = div * 10
|
|
}
|
|
rt_glyph(cx, y, 48 + (v / div) % 10, colour, sc)
|
|
cx = cx + 6 * sc
|
|
p = p - 1
|
|
}
|
|
}
|
|
|
|
# ---- registers ------------------------------------------------------------
|
|
function rt_reg(i: int) -> int {
|
|
if i < 0 { return 0 }
|
|
if i >= 64 { return 0 }
|
|
return rt_regs[i]
|
|
}
|
|
|
|
function rt_set_reg(i: int, v: int) -> void {
|
|
if i < 0 { return }
|
|
if i >= 64 { return }
|
|
rt_regs[i] = v
|
|
}
|
|
|
|
# ---- rng (xorshift32) -----------------------------------------------------
|
|
function rt_seed(s: int) -> void {
|
|
if s == 0 {
|
|
rt_rng = 305419896
|
|
return
|
|
}
|
|
rt_rng = s
|
|
}
|
|
|
|
# xorshift32 (Marsaglia). Runs on the raw 32-bit pattern, so the sign bit is
|
|
# masked off only when a caller asks for a number.
|
|
function rt_next_rand() -> int {
|
|
var x = rt_rng
|
|
x = (x ^ (x << 13))
|
|
x = (x ^ (x >> 17))
|
|
x = (x ^ (x << 5))
|
|
rt_rng = x
|
|
return (x & 2147483647)
|
|
}
|
|
|
|
function rt_rng_range(lo: int, hi: int) -> int {
|
|
if hi <= lo { return lo }
|
|
return lo + rt_next_rand() % (hi - lo + 1)
|
|
}
|
|
|
|
function rt_rng_chance(pct: int) -> bool {
|
|
return rt_next_rand() % 100 < pct
|
|
}
|
|
|
|
# a deterministic fixed-point value in [0, 1) — the raw 0..65535 is exactly the
|
|
# Q16.16 fraction (fixed and int share the i32 representation).
|
|
function rt_rng_value() -> fixed {
|
|
return rt_rng_range(0, 65535)
|
|
}
|
|
|
|
# a deterministic integer in [0, max) — 0 when max <= 0
|
|
function rt_rng_int(max: int) -> int {
|
|
if max <= 0 { return 0 }
|
|
return rt_rng_range(0, max - 1)
|
|
}
|
|
|
|
# a deterministic +1 or -1
|
|
function rt_rng_sign() -> int {
|
|
if rt_rng_chance(50) { return 1 }
|
|
return 0 - 1
|
|
}
|
|
|
|
# ---- platform: input ------------------------------------------------------
|
|
function rt_poll() -> int {
|
|
if is_windowed() {
|
|
return win_poll()
|
|
}
|
|
let c = read_char()
|
|
if c < 0 {
|
|
rt_alive = 0
|
|
return 0
|
|
}
|
|
if c == 113 { # 'q' quits, as in the headless C platform
|
|
rt_alive = 0
|
|
}
|
|
return c
|
|
}
|
|
|
|
function rt_running() -> bool {
|
|
if is_windowed() {
|
|
return win_running()
|
|
}
|
|
return rt_alive
|
|
}
|
|
|
|
# ---- writing the frame out ------------------------------------------------
|
|
function rt_put_str(buf: pointer, at: int, s: string) -> int {
|
|
var i = 0
|
|
var n = at
|
|
var ch = s[0]
|
|
while ch != 0 {
|
|
buf[n] = ch
|
|
n = n + 1
|
|
i = i + 1
|
|
ch = s[i]
|
|
}
|
|
return n
|
|
}
|
|
|
|
function rt_put_int(buf: pointer, at: int, v: int) -> int {
|
|
if v == 0 {
|
|
buf[at] = 48
|
|
return at + 1
|
|
}
|
|
var digits = 0
|
|
var t = v
|
|
while t > 0 {
|
|
digits = digits + 1
|
|
t = t / 10
|
|
}
|
|
var n = at
|
|
var p = digits
|
|
while p > 0 {
|
|
var div = 1
|
|
for k in 1 .. p {
|
|
div = div * 10
|
|
}
|
|
buf[n] = 48 + (v / div) % 10
|
|
n = n + 1
|
|
p = p - 1
|
|
}
|
|
return n
|
|
}
|
|
|
|
function rt_dump_ppm(path: string) -> void {
|
|
let f = file_open(path, "wb")
|
|
if (f == null) { return }
|
|
|
|
let hdr = bytes(64)
|
|
var n = rt_put_str(hdr, 0, "P6\n")
|
|
n = rt_put_int(hdr, n, rt_fbw)
|
|
n = rt_put_str(hdr, n, " ")
|
|
n = rt_put_int(hdr, n, rt_fbh)
|
|
n = rt_put_str(hdr, n, "\n255\n")
|
|
file_write(f, hdr, n)
|
|
|
|
let px = rt_fbw * rt_fbh
|
|
let buf = bytes(px * 3)
|
|
for i in 0 .. px {
|
|
let c = rt_fb[i]
|
|
buf[i * 3] = (c / 65536) % 256
|
|
buf[i * 3 + 1] = (c / 256) % 256
|
|
buf[i * 3 + 2] = c % 256
|
|
}
|
|
file_write(f, buf, px * 3)
|
|
file_close(f)
|
|
free(buf)
|
|
free(hdr)
|
|
}
|
|
|
|
import "inflate.ludic"
|
|
import "image.ludic"
|
|
import "truetype.ludic"
|
|
import "ui.ludic"
|
|
|
|
# ---- tilemap --------------------------------------------------------------
|
|
# A character grid the game paints with map_row() and reads with tile(). Stored
|
|
# as one byte per cell in a flat buffer; out-of-bounds reads answer '#' so a
|
|
# caller can treat the edge of the world as wall without special-casing it.
|
|
var rt_map: pointer = null
|
|
var rt_mapw: int = 0
|
|
var rt_maph: int = 0
|
|
|
|
function rt_map_size(w: int, h: int) -> void {
|
|
rt_mapw = clamp(w, 0, 96)
|
|
rt_maph = clamp(h, 0, 64)
|
|
fill(rt_map, 32, 96 * 64)
|
|
}
|
|
|
|
function rt_map_row(y: int, s: string) -> void {
|
|
if y < 0 { return }
|
|
if y >= 64 { return }
|
|
var x = 0
|
|
var ch = s[0]
|
|
while ch != 0 {
|
|
if x >= 96 { return }
|
|
rt_map[y * 96 + x] = ch
|
|
x = x + 1
|
|
ch = s[x]
|
|
}
|
|
}
|
|
|
|
function rt_tile(x: int, y: int) -> int {
|
|
if x < 0 { return 35 }
|
|
if y < 0 { return 35 }
|
|
if x >= rt_mapw { return 35 }
|
|
if y >= rt_maph { return 35 }
|
|
return rt_map[y * 96 + x]
|
|
}
|
|
|
|
# ---- status line ----------------------------------------------------------
|
|
# One persistent string of feedback/dialogue, copied into runtime-owned memory
|
|
# so it survives whatever the caller does with the original.
|
|
var rt_statusbuf: pointer = null
|
|
|
|
function rt_status(s: string) -> void {
|
|
var i = 0
|
|
var ch = s[0]
|
|
while ch != 0 {
|
|
if i >= 95 { ch = 0 }
|
|
if ch != 0 {
|
|
rt_statusbuf[i] = ch
|
|
i = i + 1
|
|
ch = s[i]
|
|
}
|
|
}
|
|
rt_statusbuf[i] = 0
|
|
}
|
|
|
|
function rt_status_text() -> pointer {
|
|
return rt_statusbuf
|
|
}
|
|
|
|
# ---- snapshot: the runtime serialises its own half ------------------------
|
|
# The compiler writes the ECS (entities, components, archetype kinds) because
|
|
# only it knows their shape. Everything below belongs to the runtime, so the
|
|
# runtime writes it — same order both ways.
|
|
function rt_save_state(f: pointer) -> void {
|
|
let w: words = bytes(16)
|
|
w[0] = rt_rng
|
|
w[1] = rt_mapw
|
|
w[2] = rt_maph
|
|
w[3] = rt_alive
|
|
file_write(f, w, 16)
|
|
file_write(f, rt_regs, 64 * 4)
|
|
file_write(f, rt_map, 96 * 64)
|
|
file_write(f, rt_statusbuf, 96)
|
|
free(w)
|
|
}
|
|
|
|
function rt_load_state(f: pointer) -> void {
|
|
let w: words = bytes(16)
|
|
file_read(f, w, 16)
|
|
rt_rng = w[0]
|
|
rt_mapw = w[1]
|
|
rt_maph = w[2]
|
|
file_read(f, rt_regs, 64 * 4)
|
|
file_read(f, rt_map, 96 * 64)
|
|
file_read(f, rt_statusbuf, 96)
|
|
free(w)
|
|
}
|