feat(gl): OpenGL 4.1 and the ludic.render3d renderer
`Gl.*` binds the whole OpenGL 4.1 core API — every entry point of the platform gl3.h with every GL_* constant, generated by `ludic-dev glgen` with per-call ABI thunks. Windowed builds get an NSOpenGLContext on the existing window at Retina resolution; headless builds render into an offscreen CGL context, so a program that uses Gl.* renders and screenshots identically under the test harness. It links gl.ll, the thunks and OpenGL.framework only when used; every other build stays byte-identical. packages/ludic.render3d is a physically based renderer written on that surface: HDRI image-based lighting, GPU-generated terrain with scanned PBR materials, CDLOD, cascaded shadows, glTF with skinning, instanced vegetation with impostors, procedural grass, water, SSAO, and an HDR pipeline with bloom, auto-exposure and ACES. It also carries this session's work on it: the terrain at half its cost (10.3 -> 5.4 ms of frame), the streaming hitch that got worse the longer you played, a resize that emptied the world, and the packaging that lets a game use the renderer from its own repository — `ludic assets`, the material manifest shipping with the package, and shader lookup falling back to the install root. See changes/ for each, with its numbers. The camping game that drove all of it has moved out to its own repository, Maroon Lake; examples/rendering/smooth.ludic stays as the renderer's example here. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
470971bf70
commit
f25289db20
90 changed files with 35316 additions and 19853 deletions
73
tools/ludic-cli/assets.ludic
Normal file
73
tools/ludic-cli/assets.ludic
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
# assets.ludic — fetch the CC0 Poly Haven assets the renderer uses
|
||||
# (ported off tools/glgen/fetch_assets.sh, the last shell script in the tree).
|
||||
#
|
||||
# ludic assets [--force] (a game, fetching what the renderer needs)
|
||||
# ludic dev fetch-assets [--force] (the same thing, from a Ludic checkout)
|
||||
#
|
||||
# The manifest is `packages/ludic.render3d/assets.manifest` — one `<relative path> <url>`
|
||||
# pair per line — and it belongs to the renderer, not to any one project: the renderer
|
||||
# decides which scanned materials it wants. It ships with the package (and so with the
|
||||
# toolchain), so a game outside this repo fetches the right set without keeping its own
|
||||
# copy of the list. The files land in the PROJECT, under assets/polyhaven/: they are
|
||||
# large and redistributable from their origin, so they are fetched rather than tracked
|
||||
# by anyone.
|
||||
|
||||
# the manifest that belongs to the renderer, in a checkout or in the install
|
||||
function assets_manifest_path() -> pointer {
|
||||
let local = "packages/ludic.render3d/assets.manifest"
|
||||
if file_exists(local) { return local }
|
||||
var home = getenv("LUDIC_HOME")
|
||||
if home == null or slen(home) == 0 { home = `{getenv_or("HOME", "")}/.ludic` }
|
||||
let inst = `{home}/packages/ludic.render3d/assets.manifest`
|
||||
if file_exists(inst) { return inst }
|
||||
return ""
|
||||
}
|
||||
|
||||
function cmd_fetch_assets() -> int {
|
||||
let force = arg_count() > 2 and arg(2) == "--force"
|
||||
let root = "assets/polyhaven"
|
||||
let mpath = assets_manifest_path()
|
||||
if mpath == "" { err("ludic assets: cannot find packages/ludic.render3d/assets.manifest (is the toolchain installed?)\n"); return 1 }
|
||||
let manifest = read_file(mpath)
|
||||
if manifest == null { err(`ludic assets: cannot read {mpath}\n`); return 1 }
|
||||
|
||||
var got = 0
|
||||
var had = 0
|
||||
var failed = 0
|
||||
let n = slen(manifest)
|
||||
var i = 0
|
||||
while i < n {
|
||||
var j = i
|
||||
while j < n and manifest[j] != 10 { j += 1 }
|
||||
let line = Text.trim(str_sub(manifest, i, j))
|
||||
i = j + 1
|
||||
if slen(line) == 0 { continue }
|
||||
|
||||
# split the line into <relative path> <url> on the first run of whitespace
|
||||
let ln = slen(line)
|
||||
var k = 0
|
||||
while k < ln and not str_space(line[k]) { k += 1 }
|
||||
let rel = str_sub(line, 0, k)
|
||||
while k < ln and str_space(line[k]) { k += 1 }
|
||||
let url = Text.trim(str_sub(line, k, ln))
|
||||
if slen(rel) == 0 or slen(url) == 0 { continue }
|
||||
|
||||
let dest = `{root}/{rel}`
|
||||
if not force and shq(`test -s {dest}`) {
|
||||
had += 1
|
||||
continue
|
||||
}
|
||||
print(`fetch {rel}`)
|
||||
run(`mkdir -p "$(dirname {dest})"`)
|
||||
if not shq(`curl -sSL --retry 3 -o {dest} {url}`) {
|
||||
err(`fetch-assets: cannot fetch {rel}\n`)
|
||||
run(`rm -f {dest}`)
|
||||
failed += 1
|
||||
} else {
|
||||
got += 1
|
||||
}
|
||||
}
|
||||
if failed > 0 { err(`fetch-assets: {string(failed)} failed\n`); return 1 }
|
||||
print(`OK {string(got)} fetched, {string(had)} already present`)
|
||||
return 0
|
||||
}
|
||||
|
|
@ -36,7 +36,7 @@ function compile_app(src: pointer, out: pointer, mode: int, save: bool) -> bool
|
|||
|
||||
if mode == 2 {
|
||||
if not shq(`{ludicc()} --headless {src} --emit-llvm -o {ll}`) { return false }
|
||||
if not shq(`{cc()} -O2 {ll}{pbf} -o {out}`) { return false }
|
||||
if not shq(`{cc()} -O2 {ll}{gl_link_flags(ll)}{pbf} -o {out}`) { return false }
|
||||
if not save { run(`rm -f {ll}`) }
|
||||
return true
|
||||
}
|
||||
|
|
@ -46,11 +46,19 @@ function compile_app(src: pointer, out: pointer, mode: int, save: bool) -> bool
|
|||
# canonical `ludicc -o` path links it only when Audio.* is used.
|
||||
let cocoa = `{home}runtime/native/cocoa.ll`
|
||||
let audio = `{home}runtime/native/audio.ll`
|
||||
if not shq(`{cc()} -O2 {ll} {cocoa} {audio} -framework Cocoa -Wl,-needed_framework,GameController -Wl,-needed_framework,AVFoundation -Wl,-rpath,@loader_path{pbf} -o {out}`) { return false }
|
||||
if not shq(`{cc()} -O2 {ll} {cocoa} {audio} -framework Cocoa -Wl,-needed_framework,GameController -Wl,-needed_framework,AVFoundation -Wl,-rpath,@loader_path{gl_link_flags(ll)}{pbf} -o {out}`) { return false }
|
||||
if not save { run(`rm -f {ll}`) }
|
||||
return true
|
||||
}
|
||||
|
||||
# A program that uses Gl.* references the @lgl_* thunks; link the OpenGL backend
|
||||
# (gl.ll + gl_thunks.ll + OpenGL.framework) only then, so other builds are untouched.
|
||||
function gl_link_flags(ll: pointer) -> pointer {
|
||||
if not shq(`grep -q "@lgl_" {ll}`) { return "" }
|
||||
let home = ludic_home()
|
||||
return ` {home}runtime/native/gl.ll {home}runtime/native/gl_thunks.ll -framework OpenGL`
|
||||
}
|
||||
|
||||
# the directory part of a path, without the trailing '/' ("" when there is none)
|
||||
function dir_of_path(p: pointer) -> pointer {
|
||||
var last = -1
|
||||
|
|
|
|||
|
|
@ -32,6 +32,8 @@ program LudicDev {
|
|||
import "docgen.ludic"
|
||||
import "docgen_gen.ludic"
|
||||
import "docgen_check.ludic"
|
||||
import "glgen.ludic"
|
||||
import "assets.ludic"
|
||||
import "release.ludic"
|
||||
import "pkg.ludic"
|
||||
import "pkg_test.ludic"
|
||||
|
|
@ -47,6 +49,7 @@ program LudicDev {
|
|||
print(" build-cli build just bin/ludicc from the IR seed")
|
||||
print(" tools [--install] [--test] build the editor toolchain (ludic-fmt, ludic-lsp)")
|
||||
print(" clean remove build/")
|
||||
print(" fetch-assets [--force] fetch the CC0 Poly Haven assets the rendering examples use")
|
||||
print("")
|
||||
print("test:")
|
||||
print(" test the full regression suite")
|
||||
|
|
@ -65,6 +68,7 @@ program LudicDev {
|
|||
print(" docs-gen [--out DIR] generate the documentation site (default build/pages)")
|
||||
print(" docs-check [DIR] coverage/integrity guard over a generated docs site")
|
||||
print(" docs-palette [--check] regenerate emit_color.ludic + palette.json from the palette table")
|
||||
print(" glgen [--check] regenerate gl_api.ludic + gl_thunks.ll from the platform gl3.h")
|
||||
print("")
|
||||
print("release:")
|
||||
print(" release [major|minor|patch] [--dry-run] [--publish]")
|
||||
|
|
@ -101,6 +105,8 @@ program LudicDev {
|
|||
if (cmd == "check-vocabulary") { return cmd_check_vocab() }
|
||||
if (cmd == "lint-asset") { return cmd_lint_asset() }
|
||||
if (cmd == "docs-palette") { return cmd_docs_palette() }
|
||||
if (cmd == "glgen") { return cmd_glgen() }
|
||||
if (cmd == "fetch-assets") { return cmd_fetch_assets() }
|
||||
if (cmd == "docs-gen") { return cmd_docs_gen() }
|
||||
if (cmd == "docs-check") { return cmd_docs_check() }
|
||||
if (cmd == "golden") { return cmd_golden() }
|
||||
|
|
|
|||
542
tools/ludic-cli/glgen.ludic
Normal file
542
tools/ludic-cli/glgen.ludic
Normal file
|
|
@ -0,0 +1,542 @@
|
|||
# glgen.ludic — the OpenGL binding generator, in Ludic (ported off glgen.py, the
|
||||
# same way docgen.ludic was ported off gen.py). One x subcommand:
|
||||
#
|
||||
# ludic-dev glgen [--check] read the platform gl3.h and emit
|
||||
# runtime/native/gl_api.ludic + runtime/native/gl_thunks.ll;
|
||||
# --check regenerates into scratch files and compares,
|
||||
# so the drift guard judges the working tree.
|
||||
#
|
||||
# Both outputs are tracked, so a build never runs this: it is the tool you run
|
||||
# when the SDK's gl3.h changes. Output is byte-identical to the Python generator
|
||||
# it replaces (verified by the drift guard in `ludic-dev test`).
|
||||
|
||||
# ---- parsed constants -------------------------------------------------------
|
||||
var glg_cname: []pointer = null # GL_DEPTH_BUFFER_BIT
|
||||
var glg_ctype: []pointer = null # "int" | "long"
|
||||
var glg_cval: []pointer = null # the literal as it is emitted
|
||||
|
||||
# ---- parsed entry points ----------------------------------------------------
|
||||
# Parallel arrays; the params of function f are the pcount[f] entries of the flat
|
||||
# pp_* arrays starting at poff[f]. Flat arrays keep this free of nested slices.
|
||||
var glg_fname: []pointer = null # "CullFace" (the gl prefix already stripped)
|
||||
var glg_frbase: []pointer = null # return base type ("void", "GLuint", …)
|
||||
var glg_frstar: []int = null # return pointer depth
|
||||
var glg_fpoff: []int = null
|
||||
var glg_fpcnt: []int = null
|
||||
var glg_ppbase: []pointer = null
|
||||
var glg_ppstar: []int = null
|
||||
var glg_ppname: []pointer = null
|
||||
|
||||
# ---- character helpers ------------------------------------------------------
|
||||
function glg_upper(c: int) -> bool { return c >= 'A' and c <= 'Z' }
|
||||
function glg_lower(c: int) -> bool { return c >= 'a' and c <= 'z' }
|
||||
function glg_digit(c: int) -> bool { return c >= '0' and c <= '9' }
|
||||
function glg_hexdig(c: int) -> bool {
|
||||
return glg_digit(c) or (c >= 'a' and c <= 'f') or (c >= 'A' and c <= 'F')
|
||||
}
|
||||
# a GL_* macro name character
|
||||
function glg_namech(c: int) -> bool { return glg_upper(c) or glg_digit(c) or c == '_' }
|
||||
|
||||
# a fresh NUL-terminated copy of s[a..b)
|
||||
|
||||
# CamelCase -> snake_case, exactly as glgen.py's snake(): an underscore goes in
|
||||
# before an upper-case letter that follows a lower-case one, nowhere else.
|
||||
function glg_snake(n: pointer) -> pointer {
|
||||
let b = sb_new()
|
||||
let m = slen(n)
|
||||
var i = 0
|
||||
while i < m {
|
||||
let c = n[i]
|
||||
if glg_upper(c) and i > 0 and glg_lower(n[i - 1]) { sb_putc(b, '_') }
|
||||
if glg_upper(c) { sb_putc(b, c + 32) } else { sb_putc(b, c) }
|
||||
i += 1
|
||||
}
|
||||
return sb_str(b)
|
||||
}
|
||||
|
||||
# ---- the type tables (glgen.py's INT / LONG / FLT / DBL) --------------------
|
||||
# Membership in a space-delimited list, matched as whole tokens — one line per
|
||||
# table instead of a chain of `or`s that a newline would cut in half.
|
||||
function glg_in(t: pointer, set: pointer) -> bool {
|
||||
let n = slen(set)
|
||||
var p = 0
|
||||
while p <= n {
|
||||
var q = p
|
||||
while q < n and set[q] != 32 { q += 1 }
|
||||
if str_sub(set, p, q) == t { return true }
|
||||
if q >= n { break }
|
||||
p = q + 1
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
function glg_is_int(t: pointer) -> bool {
|
||||
return glg_in(t, "GLenum GLuint GLint GLsizei GLbitfield GLshort GLushort GLbyte GLubyte GLhalf GLchar GLboolean GLfixed")
|
||||
}
|
||||
function glg_is_long(t: pointer) -> bool {
|
||||
return glg_in(t, "GLsizeiptr GLintptr GLint64 GLuint64 GLint64EXT GLuint64EXT")
|
||||
}
|
||||
function glg_is_flt(t: pointer) -> bool { return glg_in(t, "GLfloat GLclampf") }
|
||||
function glg_is_dbl(t: pointer) -> bool { return glg_in(t, "GLdouble GLclampd") }
|
||||
|
||||
# the LLVM type a C parameter of this shape has
|
||||
function glg_ir_ty(base: pointer, star: int) -> pointer {
|
||||
if star > 0 { return "ptr" }
|
||||
if base == "GLboolean" { return "i8" }
|
||||
if base == "GLbyte" or base == "GLubyte" or base == "GLchar" { return "i8" }
|
||||
if base == "GLshort" or base == "GLushort" or base == "GLhalf" { return "i16" }
|
||||
if glg_is_int(base) { return "i32" }
|
||||
if glg_is_long(base) { return "i64" }
|
||||
if glg_is_flt(base) { return "float" }
|
||||
if glg_is_dbl(base) { return "double" }
|
||||
if base == "GLsync" { return "ptr" }
|
||||
if base == "void" { return "void" }
|
||||
return null
|
||||
}
|
||||
|
||||
# the Ludic type it is bound as
|
||||
function glg_ludic_ty(base: pointer, star: int) -> pointer {
|
||||
if star > 0 or base == "GLsync" { return "pointer" }
|
||||
if glg_is_int(base) { return "int" }
|
||||
if glg_is_long(base) { return "long" }
|
||||
if glg_is_flt(base) or glg_is_dbl(base) { return "fixed" }
|
||||
if base == "void" { return "void" }
|
||||
return null
|
||||
}
|
||||
|
||||
# the Ludic ABI type carrying an LLVM type across the extern boundary
|
||||
function glg_abi_ty(ct: pointer) -> pointer {
|
||||
if ct == "i64" { return "i64" }
|
||||
if ct == "ptr" { return "ptr" }
|
||||
if ct == "void" { return "void" }
|
||||
return "i32"
|
||||
}
|
||||
|
||||
# ---- parameter names --------------------------------------------------------
|
||||
# A Ludic keyword or type name cannot label a parameter. glVertexAttribPointer's
|
||||
# `pointer` is a byte offset, so it says so rather than wearing a trailing _.
|
||||
function glg_reserved(n: pointer) -> bool {
|
||||
if glg_in(n, "program import property model enum ui namespace const var function extern handler entry") { return true }
|
||||
if glg_in(n, "event scene test phase query on cancellable public layer start") { return true }
|
||||
if glg_in(n, "let return if else while for in spawn despawn enable disable match machine state become where prefab") { return true }
|
||||
if glg_in(n, "and or not break continue new emit cancel try") { return true }
|
||||
if glg_in(n, "int long fixed countdown bool entity string pointer byte words fixeds pointers") { return true }
|
||||
if glg_in(n, "Vector IVec2 Rect void true false null") { return true }
|
||||
return false
|
||||
}
|
||||
function glg_pname(raw: pointer, i: int) -> pointer {
|
||||
var n = raw
|
||||
if slen(n) == 0 { n = `a{string(i)}` }
|
||||
n = glg_snake(n)
|
||||
if n == "pointer" { return "offset" }
|
||||
if n == "program" { return "prog" }
|
||||
if n == "start" { return "first" }
|
||||
if n == "string" { return "text" }
|
||||
if n == "layer" { return "level" }
|
||||
if glg_reserved(n) { return n + "_" }
|
||||
return n
|
||||
}
|
||||
|
||||
# ---- one C declarator -> (base, star, name) ---------------------------------
|
||||
# glgen.py's ctype_of: count the stars, drop every `const`, split what is left.
|
||||
# Sets glg_t_base / glg_t_star / glg_t_name (Ludic has no tuple return).
|
||||
var glg_t_base: pointer = null
|
||||
var glg_t_star: int = 0
|
||||
var glg_t_name: pointer = null
|
||||
|
||||
function glg_ctype_of(decl: pointer) -> void {
|
||||
let m = slen(decl)
|
||||
var star = 0
|
||||
var i = 0
|
||||
while i < m {
|
||||
if decl[i] == '*' { star += 1 }
|
||||
i += 1
|
||||
}
|
||||
# drop `const` (whole words) and turn '*' into a separator
|
||||
let b = sb_new()
|
||||
i = 0
|
||||
while i < m {
|
||||
if decl[i] == 'c' and i + 5 <= m and str_sub(decl, i, i + 5) == "const" {
|
||||
i += 5
|
||||
sb_putc(b, 32)
|
||||
} else {
|
||||
if decl[i] == '*' { sb_putc(b, 32) } else { sb_putc(b, decl[i]) }
|
||||
i += 1
|
||||
}
|
||||
}
|
||||
let flat = sb_str(b)
|
||||
# split on whitespace
|
||||
let parts = new []pointer
|
||||
let fn = slen(flat)
|
||||
var p = 0
|
||||
while p < fn {
|
||||
while p < fn and str_space(flat[p]) { p += 1 }
|
||||
if p >= fn { break }
|
||||
var q = p
|
||||
while q < fn and not str_space(flat[q]) { q += 1 }
|
||||
push(parts, str_sub(flat, p, q))
|
||||
p = q
|
||||
}
|
||||
glg_t_star = star
|
||||
if len(parts) == 0 { glg_t_base = ""; glg_t_name = ""; return }
|
||||
glg_t_base = parts[0]
|
||||
if len(parts) > 1 { glg_t_name = parts[1] } else { glg_t_name = "" }
|
||||
}
|
||||
|
||||
# ---- constants --------------------------------------------------------------
|
||||
# `#define GL_NAME <0xHEX|-?DEC>[u|U|ull|ULL]` and nothing else on the line —
|
||||
# the shape glgen.py's regex accepted. Returns false when the line is not one.
|
||||
var glg_v_hex: bool = false
|
||||
var glg_v_digits: pointer = null # the numeric text, suffix stripped
|
||||
var glg_v_neg: bool = false
|
||||
|
||||
function glg_num_of(tok: pointer) -> bool {
|
||||
let n = slen(tok)
|
||||
var i = 0
|
||||
glg_v_neg = false
|
||||
glg_v_hex = false
|
||||
if i < n and tok[i] == '-' { glg_v_neg = true; i += 1 }
|
||||
let numstart = i
|
||||
if i + 1 < n and tok[i] == '0' and tok[i + 1] == 'x' {
|
||||
if glg_v_neg { return false }
|
||||
glg_v_hex = true
|
||||
i += 2
|
||||
let ds = i
|
||||
while i < n and glg_hexdig(tok[i]) { i += 1 }
|
||||
if i == ds { return false }
|
||||
} else {
|
||||
let ds = i
|
||||
while i < n and glg_digit(tok[i]) { i += 1 }
|
||||
if i == ds { return false }
|
||||
}
|
||||
glg_v_digits = str_sub(tok, numstart, i)
|
||||
# what is left must be one of the accepted integer suffixes
|
||||
let suf = str_sub(tok, i, n)
|
||||
if suf == "" or suf == "u" or suf == "U" or suf == "ull" or suf == "ULL" { return true }
|
||||
return false
|
||||
}
|
||||
|
||||
# accumulate hex digits into an i32: the wrap makes an 8-digit value with the top
|
||||
# bit set come out as the signed integer with those exact bits (glgen.py's v - 2^32).
|
||||
function glg_hex_i32(h: pointer) -> int {
|
||||
var v = 0
|
||||
var i = 0
|
||||
while i < slen(h) {
|
||||
let c = h[i]
|
||||
var d = 0
|
||||
if glg_digit(c) { d = c - '0' }
|
||||
if c >= 'a' and c <= 'f' { d = c - 'a' + 10 }
|
||||
if c >= 'A' and c <= 'F' { d = c - 'A' + 10 }
|
||||
v = v * 16 + d
|
||||
i += 1
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
function glg_parse_define(line: pointer) -> void {
|
||||
let n = slen(line)
|
||||
if n < 8 { return }
|
||||
if str_sub(line, 0, 8) != "#define " { return }
|
||||
var i = 8
|
||||
while i < n and str_space(line[i]) { i += 1 }
|
||||
let ns = i
|
||||
while i < n and glg_namech(line[i]) { i += 1 }
|
||||
let name = str_sub(line, ns, i)
|
||||
if slen(name) < 4 { return }
|
||||
if str_sub(name, 0, 3) != "GL_" { return }
|
||||
# the name must be followed by whitespace (not '(' — that is a macro)
|
||||
if i >= n or not str_space(line[i]) { return }
|
||||
while i < n and str_space(line[i]) { i += 1 }
|
||||
let vs = i
|
||||
while i < n and not str_space(line[i]) { i += 1 }
|
||||
let tok = str_sub(line, vs, i)
|
||||
# trailing whitespace only
|
||||
while i < n and str_space(line[i]) { i += 1 }
|
||||
if i != n { return }
|
||||
if slen(tok) == 0 { return }
|
||||
# glgen.py skips the GL_VERSION_* feature macros and any repeated name
|
||||
if slen(name) >= 11 and str_sub(name, 0, 11) == "GL_VERSION_" { return }
|
||||
var k = 0
|
||||
while k < len(glg_cname) {
|
||||
if glg_cname[k] == name { return }
|
||||
k += 1
|
||||
}
|
||||
if not glg_num_of(tok) { return }
|
||||
|
||||
# classify the width exactly as glgen.py did, but by digit count so a 64-bit
|
||||
# all-ones literal never has to be parsed into a signed 64-bit register.
|
||||
if glg_v_hex {
|
||||
var d = glg_v_digits
|
||||
var h = str_sub(d, 2, slen(d)) # drop the 0x
|
||||
var z = 0
|
||||
while z < slen(h) - 1 and h[z] == '0' { z += 1 }
|
||||
h = str_sub(h, z, slen(h))
|
||||
let hn = slen(h)
|
||||
if hn > 8 {
|
||||
push(glg_cname, name); push(glg_ctype, "long"); push(glg_cval, "-1")
|
||||
return
|
||||
}
|
||||
if hn == 8 and not (h[0] >= '0' and h[0] <= '7') {
|
||||
# 0x8… — the same bits as a negative i32; emit the signed decimal
|
||||
let v = glg_hex_i32(h)
|
||||
push(glg_cname, name); push(glg_ctype, "int"); push(glg_cval, string(v))
|
||||
return
|
||||
}
|
||||
push(glg_cname, name); push(glg_ctype, "int"); push(glg_cval, glg_v_digits)
|
||||
return
|
||||
}
|
||||
var lit = glg_v_digits
|
||||
if glg_v_neg { lit = "-" + lit }
|
||||
push(glg_cname, name); push(glg_ctype, "int"); push(glg_cval, lit)
|
||||
}
|
||||
|
||||
# ---- entry points -----------------------------------------------------------
|
||||
# `GLAPI <ret> APIENTRY gl<Name> (<args>) …;` — every gl3.h declaration is on one
|
||||
# line, so a line scan matches what glgen.py's re.M regex did.
|
||||
function glg_parse_glapi(line: pointer) -> void {
|
||||
let n = slen(line)
|
||||
if n < 6 { return }
|
||||
if str_sub(line, 0, 6) != "GLAPI " { return }
|
||||
let ap = Text.index_of(line, " APIENTRY ")
|
||||
if ap < 0 { return }
|
||||
let ret = Text.trim(str_sub(line, 6, ap))
|
||||
var i = ap + 10
|
||||
while i < n and str_space(line[i]) { i += 1 }
|
||||
let ns = i
|
||||
while i < n and (glg_upper(line[i]) or glg_lower(line[i]) or glg_digit(line[i]) or line[i] == '_') { i += 1 }
|
||||
let fname = str_sub(line, ns, i)
|
||||
if slen(fname) < 3 { return }
|
||||
if str_sub(fname, 0, 2) != "gl" { return }
|
||||
while i < n and str_space(line[i]) { i += 1 }
|
||||
if i >= n or line[i] != '(' { return }
|
||||
let argstart = i + 1
|
||||
var depth = 1
|
||||
i += 1
|
||||
while i < n and depth > 0 {
|
||||
if line[i] == '(' { depth += 1 }
|
||||
if line[i] == ')' { depth -= 1 }
|
||||
if depth == 0 { break }
|
||||
i += 1
|
||||
}
|
||||
if depth != 0 { return }
|
||||
let args = str_sub(line, argstart, i)
|
||||
let bare = str_sub(fname, 2, slen(fname))
|
||||
var k = 0
|
||||
while k < len(glg_fname) {
|
||||
if glg_fname[k] == bare { return }
|
||||
k += 1
|
||||
}
|
||||
|
||||
glg_ctype_of(ret)
|
||||
let rbase = glg_t_base
|
||||
let rstar = glg_t_star
|
||||
|
||||
let poff = len(glg_ppbase)
|
||||
var pcnt = 0
|
||||
let at = Text.trim(args)
|
||||
if at != "void" and at != "" {
|
||||
# split the argument list on commas (no function-pointer args in gl3.h)
|
||||
let an = slen(args)
|
||||
var p = 0
|
||||
while p <= an {
|
||||
var q = p
|
||||
while q < an and args[q] != ',' { q += 1 }
|
||||
let one = str_sub(args, p, q)
|
||||
glg_ctype_of(one)
|
||||
push(glg_ppbase, glg_t_base)
|
||||
push(glg_ppstar, glg_t_star)
|
||||
push(glg_ppname, glg_t_name)
|
||||
pcnt += 1
|
||||
if q >= an { break }
|
||||
p = q + 1
|
||||
}
|
||||
}
|
||||
push(glg_fname, bare)
|
||||
push(glg_frbase, rbase)
|
||||
push(glg_frstar, rstar)
|
||||
push(glg_fpoff, poff)
|
||||
push(glg_fpcnt, pcnt)
|
||||
}
|
||||
|
||||
function glg_parse(h: pointer) -> void {
|
||||
glg_cname = new []pointer; glg_ctype = new []pointer; glg_cval = new []pointer
|
||||
glg_fname = new []pointer; glg_frbase = new []pointer; glg_frstar = new []int
|
||||
glg_fpoff = new []int; glg_fpcnt = new []int
|
||||
glg_ppbase = new []pointer; glg_ppstar = new []int; glg_ppname = new []pointer
|
||||
let n = slen(h)
|
||||
var i = 0
|
||||
while i < n {
|
||||
var j = i
|
||||
while j < n and h[j] != 10 { j += 1 }
|
||||
let line = str_sub(h, i, j)
|
||||
if slen(line) > 0 {
|
||||
if line[0] == '#' { glg_parse_define(line) }
|
||||
if line[0] == 'G' { glg_parse_glapi(line) }
|
||||
}
|
||||
i = j + 1
|
||||
}
|
||||
}
|
||||
|
||||
# ---- emit gl_api.ludic ------------------------------------------------------
|
||||
function glg_emit_ludic(path: pointer) -> bool {
|
||||
let b = sb_new()
|
||||
sb_puts(b, "# ============================================================================\n")
|
||||
sb_puts(b, "# gl_api.ludic — the OpenGL 4.1 core API, bound for Ludic. GENERATED by\n")
|
||||
sb_puts(b, "# `ludic-dev glgen` from the platform gl3.h: every entry point and every GL_* constant.\n")
|
||||
sb_puts(b, "# Do not edit by hand; regenerate. float/double parameters take `fixed`; the IR\n")
|
||||
sb_puts(b, "# thunks in runtime/native/gl.ll (also generated) convert at the C boundary.\n")
|
||||
sb_puts(b, "# ============================================================================\n")
|
||||
sb_puts(b, "\n")
|
||||
var i = 0
|
||||
while i < len(glg_cname) {
|
||||
sb_puts(b, `const {glg_cname[i]}: {glg_ctype[i]} = {glg_cval[i]}\n`)
|
||||
i += 1
|
||||
}
|
||||
sb_puts(b, "\n")
|
||||
i = 0
|
||||
while i < len(glg_fname) {
|
||||
let lname = "gl_" + glg_snake(glg_fname[i])
|
||||
let ps = sb_new()
|
||||
var p = 0
|
||||
while p < glg_fpcnt[i] {
|
||||
let ix = glg_fpoff[i] + p
|
||||
if p > 0 { sb_puts(ps, ", ") }
|
||||
let lt = glg_ludic_ty(glg_ppbase[ix], glg_ppstar[ix])
|
||||
sb_puts(ps, `{glg_pname(glg_ppname[ix], p)}: {lt}`)
|
||||
p += 1
|
||||
}
|
||||
let rt = glg_ludic_ty(glg_frbase[i], glg_frstar[i])
|
||||
var rets = ""
|
||||
if rt != "void" { rets = " -> " + rt }
|
||||
sb_puts(b, `extern function {lname}({sb_str(ps)}){rets} = "lgl_{glg_fname[i]}"\n`)
|
||||
i += 1
|
||||
}
|
||||
return write_file(path, sb_str(b))
|
||||
}
|
||||
|
||||
# ---- emit gl_thunks.ll ------------------------------------------------------
|
||||
function glg_emit_thunks(path: pointer) -> bool {
|
||||
let b = sb_new()
|
||||
sb_puts(b, "; ============================================================================\n")
|
||||
sb_puts(b, "; gl_thunks.ll — one thunk per OpenGL 4.1 core entry point. GENERATED by\n")
|
||||
sb_puts(b, "; `ludic-dev glgen` from gl3.h. Each @lgl_* takes the Ludic ABI (i32 / i64 / ptr,\n")
|
||||
sb_puts(b, "; float and double as Q16.16 fixed) and calls the real gl* with exact C types.\n")
|
||||
sb_puts(b, "; ============================================================================\n")
|
||||
sb_puts(b, "\n")
|
||||
var i = 0
|
||||
while i < len(glg_fname) {
|
||||
let cret = glg_ir_ty(glg_frbase[i], glg_frstar[i])
|
||||
let ps = sb_new()
|
||||
var p = 0
|
||||
while p < glg_fpcnt[i] {
|
||||
let ix = glg_fpoff[i] + p
|
||||
if p > 0 { sb_puts(ps, ", ") }
|
||||
sb_puts(ps, glg_ir_ty(glg_ppbase[ix], glg_ppstar[ix]))
|
||||
p += 1
|
||||
}
|
||||
sb_puts(b, `declare {cret} @gl{glg_fname[i]}({sb_str(ps)})\n`)
|
||||
i += 1
|
||||
}
|
||||
sb_puts(b, "\n")
|
||||
i = 0
|
||||
while i < len(glg_fname) {
|
||||
let cret = glg_ir_ty(glg_frbase[i], glg_frstar[i])
|
||||
let lret = glg_abi_ty(cret)
|
||||
let sig = sb_new()
|
||||
let body = sb_new()
|
||||
let call = sb_new()
|
||||
var p = 0
|
||||
while p < glg_fpcnt[i] {
|
||||
let ix = glg_fpoff[i] + p
|
||||
let ct = glg_ir_ty(glg_ppbase[ix], glg_ppstar[ix])
|
||||
let lt = glg_abi_ty(ct)
|
||||
if p > 0 { sb_puts(sig, ", "); sb_puts(call, ", ") }
|
||||
sb_puts(sig, `{lt} %a{string(p)}`)
|
||||
if ct == lt {
|
||||
sb_puts(call, `{ct} %a{string(p)}`)
|
||||
} else {
|
||||
if ct == "i8" or ct == "i16" {
|
||||
sb_puts(body, ` %c{string(p)} = trunc i32 %a{string(p)} to {ct}\n`)
|
||||
sb_puts(call, `{ct} %c{string(p)}`)
|
||||
} else {
|
||||
# float / double: the Ludic side passes Q16.16, so scale by 1/65536
|
||||
sb_puts(body, ` %f{string(p)} = sitofp i32 %a{string(p)} to {ct}\n`)
|
||||
sb_puts(body, ` %c{string(p)} = fmul {ct} %f{string(p)}, 0x3EF0000000000000\n`)
|
||||
sb_puts(call, `{ct} %c{string(p)}`)
|
||||
}
|
||||
}
|
||||
p += 1
|
||||
}
|
||||
sb_puts(b, `define {lret} @lgl_{glg_fname[i]}({sb_str(sig)}) `)
|
||||
sb_puts(b, "{\n")
|
||||
sb_puts(b, "entry:\n")
|
||||
sb_puts(b, sb_str(body))
|
||||
let invoke = `call {cret} @gl{glg_fname[i]}({sb_str(call)})`
|
||||
if cret == "void" {
|
||||
sb_puts(b, ` {invoke}\n`)
|
||||
sb_puts(b, " ret void\n")
|
||||
} else {
|
||||
if cret == lret {
|
||||
sb_puts(b, ` %r = {invoke}\n`)
|
||||
sb_puts(b, ` ret {lret} %r\n`)
|
||||
} else {
|
||||
if cret == "i8" or cret == "i16" {
|
||||
sb_puts(b, ` %r = {invoke}\n`)
|
||||
sb_puts(b, ` %z = zext {cret} %r to i32\n`)
|
||||
sb_puts(b, " ret i32 %z\n")
|
||||
} else {
|
||||
sb_puts(b, ` %r = {invoke}\n`)
|
||||
sb_puts(b, ` %m = fmul {cret} %r, 65536.0\n`)
|
||||
sb_puts(b, ` %z = fptosi {cret} %m to i32\n`)
|
||||
sb_puts(b, " ret i32 %z\n")
|
||||
}
|
||||
}
|
||||
}
|
||||
sb_puts(b, "}\n")
|
||||
i += 1
|
||||
}
|
||||
return write_file(path, sb_str(b))
|
||||
}
|
||||
|
||||
# ---- the task ---------------------------------------------------------------
|
||||
# `ludic-dev glgen` rewrites the two tracked outputs; `--check` regenerates into
|
||||
# scratch files and compares, so the guard judges the working tree, not git HEAD.
|
||||
function cmd_glgen() -> int {
|
||||
let check = arg_count() > 2 and arg(2) == "--check"
|
||||
let sdk = Text.trim(capture("xcrun --show-sdk-path"))
|
||||
if sdk == "" { err("glgen: no macOS SDK (xcrun --show-sdk-path)\n"); return 1 }
|
||||
let hpath = `{sdk}/System/Library/Frameworks/OpenGL.framework/Headers/gl3.h`
|
||||
let h = read_file(hpath)
|
||||
if h == null { err(`glgen: cannot read {hpath}\n`); return 1 }
|
||||
glg_parse(h)
|
||||
if len(glg_fname) == 0 { err("glgen: no GLAPI declarations found\n"); return 1 }
|
||||
|
||||
# every type in the header must be one this generator knows how to bind
|
||||
var i = 0
|
||||
while i < len(glg_fname) {
|
||||
if glg_ir_ty(glg_frbase[i], glg_frstar[i]) == null or glg_ludic_ty(glg_frbase[i], glg_frstar[i]) == null {
|
||||
err(`glgen: unknown return type {glg_frbase[i]} on gl{glg_fname[i]}\n`); return 1
|
||||
}
|
||||
var p = 0
|
||||
while p < glg_fpcnt[i] {
|
||||
let ix = glg_fpoff[i] + p
|
||||
if glg_ir_ty(glg_ppbase[ix], glg_ppstar[ix]) == null or glg_ludic_ty(glg_ppbase[ix], glg_ppstar[ix]) == null {
|
||||
err(`glgen: unknown parameter type {glg_ppbase[ix]} on gl{glg_fname[i]}\n`); return 1
|
||||
}
|
||||
p += 1
|
||||
}
|
||||
i += 1
|
||||
}
|
||||
|
||||
var api_out = "runtime/native/gl_api.ludic"
|
||||
var thunk_out = "runtime/native/gl_thunks.ll"
|
||||
if check { api_out = tmp_path("gl_api.ludic"); thunk_out = tmp_path("gl_thunks.ll") }
|
||||
if not glg_emit_ludic(api_out) { err("glgen: cannot write gl_api.ludic\n"); return 1 }
|
||||
if not glg_emit_thunks(thunk_out) { err("glgen: cannot write gl_thunks.ll\n"); return 1 }
|
||||
if check {
|
||||
if not shq(`cmp -s {api_out} runtime/native/gl_api.ludic`) { err("gl_api.ludic drifted from gl3.h (run: ludic-dev glgen)\n"); return 1 }
|
||||
if not shq(`cmp -s {thunk_out} runtime/native/gl_thunks.ll`) { err("gl_thunks.ll drifted from gl3.h (run: ludic-dev glgen)\n"); return 1 }
|
||||
}
|
||||
print(`OK {string(len(glg_cname))} constants, {string(len(glg_fname))} entry points`)
|
||||
return 0
|
||||
}
|
||||
|
|
@ -24,6 +24,7 @@ program Ludic {
|
|||
import "build.ludic"
|
||||
import "project.ludic"
|
||||
import "pkg.ludic"
|
||||
import "assets.ludic"
|
||||
|
||||
function usage() -> void {
|
||||
print("ludic — the toolchain for the Ludic language")
|
||||
|
|
@ -44,6 +45,7 @@ program Ludic {
|
|||
print(" update [module] bump a dependency (or all) to its latest published version")
|
||||
print(" verify check every locked package against the store by content hash")
|
||||
print(" vendor copy the resolved packages into ./vendor for offline builds")
|
||||
print(" assets [--force] fetch the CC0 materials the renderer needs into assets/polyhaven/")
|
||||
print(" build-lib <module.ludic> compile a package's module to a prebuilt dylib in lib/<target>/")
|
||||
print(" link-flags print the clang flags to link this project's prebuilt module dylibs")
|
||||
print("")
|
||||
|
|
@ -85,6 +87,7 @@ program Ludic {
|
|||
if (cmd == "update") { return cmd_pkg_update() }
|
||||
if (cmd == "verify") { return cmd_pkg_verify() }
|
||||
if (cmd == "vendor") { return cmd_pkg_vendor() }
|
||||
if (cmd == "assets") { return cmd_fetch_assets() }
|
||||
if (cmd == "build-lib") { return cmd_pkg_build_lib() }
|
||||
if (cmd == "link-flags") { return cmd_pkg_link_flags() }
|
||||
|
||||
|
|
|
|||
|
|
@ -35,6 +35,22 @@ function write_file(path: pointer, s: pointer) -> bool {
|
|||
return true
|
||||
}
|
||||
|
||||
# ---- byte strings ------------------------------------------------------------------
|
||||
# s[a, b) as a fresh NUL-terminated string; out-of-range ends are clamped.
|
||||
function str_sub(s: pointer, a: int, b: int) -> pointer {
|
||||
var lo = a
|
||||
if lo < 0 { lo = 0 }
|
||||
var hi = b
|
||||
if hi < lo { hi = lo }
|
||||
let out = bytes(hi - lo + 1)
|
||||
var i = lo
|
||||
var k = 0
|
||||
while i < hi { out[k] = s[i]; k += 1; i += 1 }
|
||||
out[k] = 0
|
||||
return out
|
||||
}
|
||||
function str_space(c: int) -> bool { return c == 32 or c == 9 or c == 13 }
|
||||
|
||||
function file_exists(path: pointer) -> bool { return shq(`test -e {path}`) }
|
||||
function is_exec(path: pointer) -> bool { return shq(`test -x {path}`) }
|
||||
# is `a` newer than `b` (like the shell's `-nt`)?
|
||||
|
|
|
|||
|
|
@ -22,14 +22,27 @@ function manifest_name() -> pointer {
|
|||
|
||||
# the name to give the built binary: the manifest's module (its last dotted
|
||||
# segment, so ludic.snake builds `snake`), else the entry file's base name.
|
||||
# The built binary's name, from the manifest's module path when there is one.
|
||||
#
|
||||
# A module path is a URL — `git.host/user/maroon-lake` — so the last path segment comes
|
||||
# first: taking the last DOT of that would have cut inside the host and produced
|
||||
# `io/user/maroon-lake`, which git-hosted names all share and which `build/{name}` then
|
||||
# turned into directories. Within the segment a dot still separates a namespace from the
|
||||
# package (`ludic.render3d` builds as `render3d`).
|
||||
function project_name(entry: pointer) -> pointer {
|
||||
let mod = manifest_name()
|
||||
var mod = manifest_name()
|
||||
if mod != "" {
|
||||
var last = -1
|
||||
var n = 0
|
||||
while mod[n] != 0 { n += 1 }
|
||||
var slash = -1
|
||||
var i = 0
|
||||
while mod[i] != 0 { if mod[i] == '.' { last = i }; i += 1 }
|
||||
if last >= 0 { return mod[last + 1..i] }
|
||||
return mod
|
||||
while i < n { if mod[i] == '/' { slash = i }; i += 1 }
|
||||
if slash >= 0 { mod = mod[slash + 1..n]; n = n - slash - 1 }
|
||||
var last = -1
|
||||
i = 0
|
||||
while i < n { if mod[i] == '.' { last = i }; i += 1 }
|
||||
if last >= 0 { return mod[last + 1..n] }
|
||||
if n > 0 { return mod }
|
||||
}
|
||||
return capture_line(`basename {entry} .ludic`)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -447,6 +447,7 @@ function cmd_dev_test() -> int {
|
|||
smoke("events/events")
|
||||
smoke("networking/net_rt")
|
||||
smoke("library/cursor_capture") # #89 Input.cursor_mode compiles (no-op headless; windowed links cocoa.ll)
|
||||
smoke("rendering/gl_triangle") # Gl.* (OpenGL 4.1 core) compiles headless; the run needs a GPU context
|
||||
|
||||
print("== the compiler and the CLI (ludicc / ludic) ==")
|
||||
# ludicc comes out of the IR seed with clang alone; the CLI is then compiled
|
||||
|
|
@ -525,5 +526,14 @@ function cmd_dev_test() -> int {
|
|||
ok("ludic-dev docs-palette regenerates emit_color.ludic + palette.json byte-identically")
|
||||
} else { bad2("ludic-dev docs-palette --check", capture_line(`tail -1 {tmp_dir()}/pal.out`)) }
|
||||
|
||||
# the OpenGL binding generator is the same shape: gl_api.ludic + gl_thunks.ll are
|
||||
# tracked, and --check regenerates them from the platform gl3.h and compares. The
|
||||
# header is macOS-only, so this guard cannot run off Darwin.
|
||||
if is_darwin() {
|
||||
if shq(`bin/ludic-dev glgen --check > {tmp_dir()}/glgen.out 2>&1`) {
|
||||
ok("ludic-dev glgen regenerates gl_api.ludic + gl_thunks.ll byte-identically")
|
||||
} else { bad2("ludic-dev glgen --check", capture_line(`tail -1 {tmp_dir()}/glgen.out`)) }
|
||||
} else { skip("ludic-dev glgen --check (needs the macOS OpenGL headers)") }
|
||||
|
||||
return report()
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue