Maps ship outside the game, each one a content-addressed pack (.lmap: the .lpak format) downloaded to the save root. The runtime already served reads from packs mounted at boot (packs.index); now one can come and go while the game runs: - Fs.mount(path) -> bool maps a pack over the ones mounted before it (searched first, as a later packs.index line is); Fs.unmount(path) -> bool gives the mapping back (munmap, UnmapViewOfFile on Windows) and closes the gap in the search order. Each slot keeps its length and path for it. A FILE* still open over one of its entries (a baked_open_range) is closed first. Still 8 packs at most. - baked_path(map, file) is assets/baked/maps/<map>/<file> for a map's bake - what its .lmap carries and the game's own pack never does - and assets/baked/<file> for the rest. The IR assembles for macOS and Windows (llvm-as). Compile-only: nothing mounted or run here. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
362 lines
25 KiB
Text
362 lines
25 KiB
Text
# emit_pak.ludic — the asset pack: one file beside the binary that holds every
|
|
# asset the game opens, and a lookup that makes `file_open` find them there
|
|
# first.
|
|
#
|
|
# The problem it solves: a built game opened its assets by a path relative to the
|
|
# working directory, so `build/mygame` only ran from the project root and there
|
|
# was nothing to hand anyone but "the binary, and also this whole tree". A game
|
|
# is not one file; shipping it has to be.
|
|
#
|
|
# The shape of the answer is deliberately small. `file_open` is the single place
|
|
# every asset in a Ludic program comes through — gltf_load, tex_load,
|
|
# Audio.load, Fs.read_text and the renderer's own shader loads all bottom out
|
|
# there — so the pack is spliced in at exactly that point:
|
|
#
|
|
# file_open(path, mode) -> @lp_pak_open
|
|
# |
|
|
# mode is read and path is in a pack?
|
|
# yes -> fmemopen over the mapped bytes
|
|
# no -> fopen, as before
|
|
#
|
|
# Nothing above it changes. A game reads "assets/kit/hiker/hiker.gltf" whether
|
|
# that is a file on disk during development or a run of bytes inside
|
|
# Maroon Lake.app/Contents/Resources/game.lpak, and cannot tell which.
|
|
#
|
|
# The pack is mmap'd, never read into memory: 165 MB of textures and terrain
|
|
# costs one syscall at boot and pages in only what the game actually touches.
|
|
# Entries are stored, not compressed — PNG, JPEG and glTF binary are already
|
|
# compressed, and a decompressor on the load path would spend CPU to make the
|
|
# file no smaller.
|
|
#
|
|
# ---- the format -------------------------------------------------------------
|
|
#
|
|
# All integers are 32-bit little-endian. The writer is `ludic pack`
|
|
# (tools/ludic-cli/pack.ludic), which is the only thing that produces one.
|
|
#
|
|
# offset 0 "LPAK" magic
|
|
# 4 version 1
|
|
# 8 count number of entries
|
|
# 12 names absolute offset of the name heap
|
|
# 16 data absolute offset of the first blob
|
|
# 20 reserved x 3 zero
|
|
# 32 entries[count], 16 bytes each, SORTED by name (strcmp order):
|
|
# 0 name offset absolute, NUL-terminated
|
|
# 4 data offset absolute
|
|
# 8 data length
|
|
# 12 FNV-1a hash of the blob
|
|
#
|
|
# Sorted names are what make the lookup a binary search rather than a scan, and
|
|
# `ludic pack` sorts because the runtime is entitled to assume it.
|
|
#
|
|
# ---- finding the packs at boot ----------------------------------------------
|
|
#
|
|
# @lp_pak_boot runs from @llvm.global_ctors, before main, so a pack is mounted
|
|
# before the first line of the game's own code. It looks beside the executable —
|
|
# ../Resources first (where it lands inside a .app), then the executable's own
|
|
# directory (where it lands beside a plain binary) — for `packs.index`, a plain
|
|
# text file the bundler writes:
|
|
#
|
|
# pack game.lpak mount this, in this order
|
|
# home Maroon Lake chdir to ~/Library/Application Support/Maroon Lake
|
|
# splash assets/splash.png show this until the game says otherwise
|
|
# splashbg 858370 the colour behind it, 0xRRGGBB as a decimal
|
|
#
|
|
# splashbg is decimal because the runtime parses it with atoi, which is already
|
|
# declared; asking it to parse hex would be a hex parser in LLVM IR to save the
|
|
# bundler one conversion.
|
|
#
|
|
# The order is explicit rather than a sorted glob because it is load-bearing:
|
|
# packs mount in the order listed and a later one shadows an earlier one, which
|
|
# is how a patch or an add-on replaces a file without rewriting the base pack.
|
|
#
|
|
# `home` is what makes a bundled game able to save. A .app launched from Finder
|
|
# starts with the working directory at "/", so every relative write would fail;
|
|
# pointing the process at its own Application Support directory means reads come
|
|
# out of the pack and writes land somewhere real and per-user.
|
|
#
|
|
# Note the asymmetry: the pack is searched BEFORE the filesystem, so a stray or
|
|
# corrupt file in that directory cannot shadow a shipped asset. An override is a
|
|
# pack listed after the base one, never a loose file.
|
|
#
|
|
# Without a packs.index — which is every `ludic run` during development — nothing
|
|
# is mounted, no chdir happens, and every open goes straight to fopen against the
|
|
# working directory exactly as it did before. Packing is a shipping step, and it
|
|
# is invisible until you ship.
|
|
|
|
# The pack runtime leans on the Fs/Path prelude (lp_path_join, lp_path_dir, and
|
|
# the libc declarations both need), so asking for one asks for the other.
|
|
function use_pak() -> void { g_uses_pak = true; g_uses_fsrt = true }
|
|
|
|
# emit_pak_prelude — emitted once per program that opens a file, straight after
|
|
# the Fs prelude whose helpers it calls.
|
|
function emit_pak_prelude() -> void {
|
|
if not g_target_win { # defined by emit_win_prelude on Windows
|
|
emith("declare i32 @open(ptr, i32, ...)\n")
|
|
emith("declare i32 @close(i32)\n")
|
|
emith("declare i64 @lseek(i32, i64, i32)\n")
|
|
if not g_mmap_declared { emith("declare ptr @mmap(ptr, i64, i32, i32, i32, i64)\n"); g_mmap_declared = true }
|
|
emith("declare ptr @fmemopen(ptr, i64, ptr)\n")
|
|
emith("declare i32 @munmap(ptr, i64)\n")
|
|
emith("declare i32 @chdir(ptr)\n")
|
|
emith("declare i32 @_NSGetExecutablePath(ptr, ptr)\n")
|
|
}
|
|
|
|
emith("@.pak_rb = private unnamed_addr constant [3 x i8] c\"rb\\00\"\n")
|
|
emith("@.pak_idx = private unnamed_addr constant [12 x i8] c\"packs.index\\00\"\n")
|
|
emith("@.pak_res = private unnamed_addr constant [13 x i8] c\"../Resources\\00\"\n")
|
|
emith("@.pak_appsup= private unnamed_addr constant [28 x i8] c\"Library/Application Support\\00\"\n")
|
|
emith("@.pak_home = private unnamed_addr constant [5 x i8] c\"HOME\\00\"\n")
|
|
|
|
# up to 8 packs may be mounted; the base pointer of each mapping, in mount order
|
|
emith("@L_pak_base = internal global [8 x ptr] zeroinitializer\n")
|
|
emith("@L_pak_n = internal global i32 0\n")
|
|
emith("@L_pak_len = internal global [8 x i64] zeroinitializer\n") # each mapping's length, for unmount
|
|
emith("@L_pak_path = internal global [8 x ptr] zeroinitializer\n") # and the path it was mounted from
|
|
# the splash, named by packs.index and served out of the pack like any asset
|
|
emith("@L_pak_splash = internal global ptr null\n")
|
|
emith("@L_pak_splashbg = internal global i32 0\n")
|
|
|
|
# The splash lives in cocoa.ll, which is linked only into a windowed binary.
|
|
# A headless build must not so much as name it, so the whole thing is emitted
|
|
# behind this flag rather than guarded at runtime.
|
|
if g_windowed {
|
|
emith("declare void @splash_show(ptr, i32, i32)\n")
|
|
emith("declare void @splash_hide()\n")
|
|
emith("declare void @app_set_icon(ptr, i32)\n")
|
|
emith("declare void @app_window_fixed(i32)\n")
|
|
emith("declare i32 @win_monitor_count()\n")
|
|
emith("declare void @app_window_to_monitor(i32)\n")
|
|
emith("declare void @app_window_hide()\n")
|
|
emith("declare void @app_window_show()\n")
|
|
}
|
|
|
|
emit_pak_mount()
|
|
emit_pak_unmount()
|
|
emit_pak_lookup()
|
|
emit_pak_open()
|
|
emit_pak_boot()
|
|
|
|
# run before main, in every program shape (an `entry` game, the auto-loop, the
|
|
# test runner) without any of them having to know about it
|
|
emith("@llvm.global_ctors = appending global [1 x { i32, ptr, ptr }] ")
|
|
emith("[{ i32, ptr, ptr } { i32 101, ptr @lp_pak_boot, ptr null }]\n")
|
|
}
|
|
|
|
# ---- mounting ---------------------------------------------------------------
|
|
|
|
# mount(path): map the file and keep its base pointer if it is a pack. Returns 1
|
|
# when it was mounted. The descriptor is closed straight away — the mapping
|
|
# outlives it, and holding a descriptor open for the life of the process for no
|
|
# reason is the kind of thing that runs a game out of them.
|
|
function emit_pak_mount() -> void {
|
|
emith("define i32 @lp_pak_mount(ptr %path) {\n")
|
|
emith("entry:\n %n = load i32, ptr @L_pak_n\n %full = icmp sge i32 %n, 8\n br i1 %full, label %no, label %op\n")
|
|
# O_RDONLY = 0
|
|
emith("op:\n %fd = call i32 (ptr, i32, ...) @open(ptr %path, i32 0)\n %bad = icmp slt i32 %fd, 0\n br i1 %bad, label %no, label %sz\n")
|
|
# SEEK_END = 2; a pack is at least a header
|
|
emith("sz:\n %end = call i64 @lseek(i32 %fd, i64 0, i32 2)\n %small = icmp slt i64 %end, 32\n br i1 %small, label %clo, label %mp\n")
|
|
# PROT_READ = 1, MAP_PRIVATE = 2
|
|
emith("mp:\n %base = call ptr @mmap(ptr null, i64 %end, i32 1, i32 2, i32 %fd, i64 0)\n")
|
|
emith(" %mbad = icmp eq ptr %base, inttoptr (i64 -1 to ptr)\n br i1 %mbad, label %clo, label %chk\n")
|
|
# "LPAK" read as a little-endian i32
|
|
emith("chk:\n %m = load i32, ptr %base\n %okm = icmp eq i32 %m, 1262571596\n br i1 %okm, label %keep, label %clo\n")
|
|
emith("keep:\n %slot = getelementptr [8 x ptr], ptr @L_pak_base, i32 0, i32 %n\n store ptr %base, ptr %slot\n")
|
|
# its length and path too, so Fs.unmount can find it and give the mapping back
|
|
emith(" %lslot = getelementptr [8 x i64], ptr @L_pak_len, i32 0, i32 %n\n store i64 %end, ptr %lslot\n")
|
|
emith(" %pcopy = call ptr @lp_fs_strdup(ptr %path)\n %pslot = getelementptr [8 x ptr], ptr @L_pak_path, i32 0, i32 %n\n store ptr %pcopy, ptr %pslot\n")
|
|
emith(" %n1 = add i32 %n, 1\n store i32 %n1, ptr @L_pak_n\n %c1 = call i32 @close(i32 %fd)\n ret i32 1\n")
|
|
emith("clo:\n %c2 = call i32 @close(i32 %fd)\n ret i32 0\n")
|
|
emith("no:\n ret i32 0\n}\n")
|
|
}
|
|
|
|
# unmount(path): the pack mounted from exactly that path given back and taken out of the search, the
|
|
# ones after it moving up a slot; 1 when it was mounted. A map pack downloaded at run time comes and
|
|
# goes (Fs.mount / Fs.unmount); a FILE* still open over one of its entries must be closed first
|
|
function emit_pak_unmount() -> void {
|
|
emith("define i32 @lp_pak_unmount(ptr %path) {\n")
|
|
emith("entry:\n %n = load i32, ptr @L_pak_n\n %ip = alloca i32\n store i32 0, ptr %ip\n br label %find\n")
|
|
emith("find:\n %i = load i32, ptr %ip\n %end = icmp sge i32 %i, %n\n br i1 %end, label %no, label %cmp\n")
|
|
emith("cmp:\n %pp = getelementptr [8 x ptr], ptr @L_pak_path, i32 0, i32 %i\n %p = load ptr, ptr %pp\n %isnull = icmp eq ptr %p, null\n br i1 %isnull, label %next, label %str\n")
|
|
emith("str:\n %c = call i32 @strcmp(ptr %p, ptr %path)\n %same = icmp eq i32 %c, 0\n br i1 %same, label %drop, label %next\n")
|
|
emith("next:\n %i1 = add i32 %i, 1\n store i32 %i1, ptr %ip\n br label %find\n")
|
|
emith("drop:\n %bp = getelementptr [8 x ptr], ptr @L_pak_base, i32 0, i32 %i\n %base = load ptr, ptr %bp\n %lp = getelementptr [8 x i64], ptr @L_pak_len, i32 0, i32 %i\n %len = load i64, ptr %lp\n")
|
|
if g_target_win { emith(" %u = call i32 @UnmapViewOfFile(ptr %base)\n") } else { emith(" %u = call i32 @munmap(ptr %base, i64 %len)\n") }
|
|
emith(" call void @lp_free(ptr %p)\n %jp = alloca i32\n store i32 %i, ptr %jp\n br label %shift\n")
|
|
emith("shift:\n %j = load i32, ptr %jp\n %j1 = add i32 %j, 1\n %more = icmp slt i32 %j1, %n\n br i1 %more, label %mv, label %done\n")
|
|
emith("mv:\n %sb = getelementptr [8 x ptr], ptr @L_pak_base, i32 0, i32 %j1\n %vb = load ptr, ptr %sb\n %db = getelementptr [8 x ptr], ptr @L_pak_base, i32 0, i32 %j\n store ptr %vb, ptr %db\n")
|
|
emith(" %sl = getelementptr [8 x i64], ptr @L_pak_len, i32 0, i32 %j1\n %vl = load i64, ptr %sl\n %dl = getelementptr [8 x i64], ptr @L_pak_len, i32 0, i32 %j\n store i64 %vl, ptr %dl\n")
|
|
emith(" %sp = getelementptr [8 x ptr], ptr @L_pak_path, i32 0, i32 %j1\n %vp = load ptr, ptr %sp\n %dp = getelementptr [8 x ptr], ptr @L_pak_path, i32 0, i32 %j\n store ptr %vp, ptr %dp\n")
|
|
emith(" store i32 %j1, ptr %jp\n br label %shift\n")
|
|
emith("done:\n %n1 = sub i32 %n, 1\n store i32 %n1, ptr @L_pak_n\n %last = getelementptr [8 x ptr], ptr @L_pak_path, i32 0, i32 %n1\n store ptr null, ptr %last\n ret i32 1\n")
|
|
emith("no:\n ret i32 0\n}\n")
|
|
}
|
|
|
|
# ---- lookup -----------------------------------------------------------------
|
|
|
|
function emit_pak_lookup() -> void {
|
|
# a leading "./" is the same path; strip any number of them so "./assets/x"
|
|
# and "assets/x" are one key
|
|
emith("define ptr @lp_pak_norm(ptr %p) {\n")
|
|
emith("entry:\n %c0 = load i8, ptr %p\n %c0i = zext i8 %c0 to i32\n %isdot = icmp eq i32 %c0i, 46\n br i1 %isdot, label %chk2, label %same\n")
|
|
emith("chk2:\n %p1 = getelementptr i8, ptr %p, i32 1\n %c1 = load i8, ptr %p1\n %c1i = zext i8 %c1 to i32\n %issl = icmp eq i32 %c1i, 47\n br i1 %issl, label %skip, label %same\n")
|
|
emith("skip:\n %p2 = getelementptr i8, ptr %p, i32 2\n %r = call ptr @lp_pak_norm(ptr %p2)\n ret ptr %r\n")
|
|
emith("same:\n ret ptr %p\n}\n")
|
|
|
|
# binary search one pack's sorted entry table; null when the name is not in it
|
|
emith("define ptr @lp_pak_find1(ptr %base, ptr %name, ptr %outlen) {\n")
|
|
emith("entry:\n %cntp = getelementptr i8, ptr %base, i32 8\n %cnt = load i32, ptr %cntp\n")
|
|
emith(" %lop = alloca i32\n %hip = alloca i32\n store i32 0, ptr %lop\n %h0 = sub i32 %cnt, 1\n store i32 %h0, ptr %hip\n br label %loop\n")
|
|
emith("loop:\n %lo = load i32, ptr %lop\n %hi = load i32, ptr %hip\n %fin = icmp sgt i32 %lo, %hi\n br i1 %fin, label %miss, label %mid\n")
|
|
emith("mid:\n %sum = add i32 %lo, %hi\n %m = sdiv i32 %sum, 2\n %eo = mul i32 %m, 16\n %eo2 = add i32 %eo, 32\n %ep = getelementptr i8, ptr %base, i32 %eo2\n")
|
|
emith(" %noff = load i32, ptr %ep\n %nptr = getelementptr i8, ptr %base, i32 %noff\n %c = call i32 @strcmp(ptr %nptr, ptr %name)\n")
|
|
emith(" %iseq = icmp eq i32 %c, 0\n br i1 %iseq, label %hit, label %cmp\n")
|
|
emith("cmp:\n %lt = icmp slt i32 %c, 0\n br i1 %lt, label %goup, label %godn\n")
|
|
emith("goup:\n %m1 = add i32 %m, 1\n store i32 %m1, ptr %lop\n br label %loop\n")
|
|
emith("godn:\n %m2 = sub i32 %m, 1\n store i32 %m2, ptr %hip\n br label %loop\n")
|
|
emith("hit:\n %dop = getelementptr i8, ptr %ep, i32 4\n %doff = load i32, ptr %dop\n %dlp = getelementptr i8, ptr %ep, i32 8\n %dlen = load i32, ptr %dlp\n")
|
|
emith(" store i32 %dlen, ptr %outlen\n %dp = getelementptr i8, ptr %base, i32 %doff\n ret ptr %dp\n")
|
|
emith("miss:\n ret ptr null\n}\n")
|
|
|
|
# search every mounted pack, last mounted first, so a pack listed later in
|
|
# packs.index shadows the ones before it
|
|
emith("define ptr @lp_pak_find(ptr %path, ptr %outlen) {\n")
|
|
emith("entry:\n %p = call ptr @lp_pak_norm(ptr %path)\n %np = load i32, ptr @L_pak_n\n")
|
|
emith(" %ip = alloca i32\n %i0 = sub i32 %np, 1\n store i32 %i0, ptr %ip\n br label %loop\n")
|
|
emith("loop:\n %i = load i32, ptr %ip\n %done = icmp slt i32 %i, 0\n br i1 %done, label %miss, label %body\n")
|
|
emith("body:\n %slot = getelementptr [8 x ptr], ptr @L_pak_base, i32 0, i32 %i\n %base = load ptr, ptr %slot\n")
|
|
emith(" %r = call ptr @lp_pak_find1(ptr %base, ptr %p, ptr %outlen)\n %hit = icmp ne ptr %r, null\n br i1 %hit, label %got, label %next\n")
|
|
emith("got:\n ret ptr %r\n")
|
|
emith("next:\n %i1 = sub i32 %i, 1\n store i32 %i1, ptr %ip\n br label %loop\n")
|
|
emith("miss:\n ret ptr null\n}\n")
|
|
|
|
# Fs.exists / Fs.size ask the packs before they ask the filesystem, so a game
|
|
# that guards a load with Fs.exists (the sound layer does) still finds it
|
|
emith("define i32 @lp_pak_has(ptr %path) {\n")
|
|
emith("entry:\n %lp = alloca i32\n store i32 0, ptr %lp\n %b = call ptr @lp_pak_find(ptr %path, ptr %lp)\n")
|
|
emith(" %hit = icmp ne ptr %b, null\n %r = select i1 %hit, i32 1, i32 0\n ret i32 %r\n}\n")
|
|
|
|
emith("define i32 @lp_pak_bytes(ptr %path) {\n")
|
|
emith("entry:\n %lp = alloca i32\n store i32 0, ptr %lp\n %b = call ptr @lp_pak_find(ptr %path, ptr %lp)\n")
|
|
emith(" %hit = icmp ne ptr %b, null\n br i1 %hit, label %yes, label %no\n")
|
|
emith("yes:\n %l = load i32, ptr %lp\n ret i32 %l\n")
|
|
emith("no:\n ret i32 -1\n}\n")
|
|
}
|
|
|
|
# ---- the splice into file_open ----------------------------------------------
|
|
|
|
# A read of a packed path becomes a FILE* over the mapped bytes, so fread/fseek/
|
|
# ftell above it behave exactly as they did against a real file. Anything else —
|
|
# a write, a path that is not packed — is the fopen it always was.
|
|
function emit_pak_open() -> void {
|
|
emith("define ptr @lp_pak_open(ptr %path, ptr %mode) {\n")
|
|
emith("entry:\n %m0 = load i8, ptr %mode\n %m0i = zext i8 %m0 to i32\n %isr = icmp eq i32 %m0i, 114\n br i1 %isr, label %try, label %real\n")
|
|
emith("try:\n %lp = alloca i32\n store i32 0, ptr %lp\n %blob = call ptr @lp_pak_find(ptr %path, ptr %lp)\n")
|
|
emith(" %hit = icmp ne ptr %blob, null\n br i1 %hit, label %mem, label %real\n")
|
|
emith("mem:\n %len = load i32, ptr %lp\n %lz = zext i32 %len to i64\n %f = call ptr @fmemopen(ptr %blob, i64 %lz, ptr @.pak_rb)\n ret ptr %f\n")
|
|
emith("real:\n %rf = call ptr @fopen(ptr %path, ptr %mode)\n ret ptr %rf\n}\n")
|
|
}
|
|
|
|
# ---- boot -------------------------------------------------------------------
|
|
|
|
function emit_pak_boot() -> void {
|
|
# read a whole file into a fresh NUL-terminated buffer, or null
|
|
emith("define ptr @lp_pak_slurp(ptr %path) {\n")
|
|
emith("entry:\n %f = call ptr @fopen(ptr %path, ptr @.pak_rb)\n %bad = icmp eq ptr %f, null\n br i1 %bad, label %no, label %go\n")
|
|
emith("go:\n %s1 = call i32 @fseek(ptr %f, i64 0, i32 2)\n %n64 = call i64 @ftell(ptr %f)\n %s2 = call i32 @fseek(ptr %f, i64 0, i32 0)\n")
|
|
emith(" %cap = add i64 %n64, 1\n %buf = call ptr @lp_malloc(i64 %cap)\n %rd = call i64 @fread(ptr %buf, i64 1, i64 %n64, ptr %f)\n")
|
|
emith(" %e = getelementptr i8, ptr %buf, i64 %rd\n store i8 0, ptr %e\n %c = call i32 @fclose(ptr %f)\n ret ptr %buf\n")
|
|
emith("no:\n ret ptr null\n}\n")
|
|
|
|
# Try one directory: mount everything its packs.index names, and honour a
|
|
# `home` line. Returns the number of packs mounted from it.
|
|
emith("define i32 @lp_pak_boot_dir(ptr %dir) {\n")
|
|
emith("entry:\n %ix = call ptr @lp_path_join(ptr %dir, ptr @.pak_idx)\n %txt = call ptr @lp_pak_slurp(ptr %ix)\n")
|
|
emith(" %none = icmp eq ptr %txt, null\n br i1 %none, label %no, label %scan\n")
|
|
# walk the text line by line, NUL-terminating each in place (it is our copy)
|
|
emith("scan:\n %ip = alloca i32\n %got = alloca i32\n store i32 0, ptr %ip\n store i32 0, ptr %got\n %jp = alloca i32\n br label %line\n")
|
|
emith("line:\n %i = load i32, ptr %ip\n %lp = getelementptr i8, ptr %txt, i32 %i\n %c0 = load i8, ptr %lp\n %c0i = zext i8 %c0 to i32\n")
|
|
emith(" %atend = icmp eq i32 %c0i, 0\n br i1 %atend, label %fin, label %eol\n")
|
|
# find the end of this line
|
|
emith("eol:\n store i32 %i, ptr %jp\n br label %escan\n")
|
|
emith("escan:\n %j = load i32, ptr %jp\n %jq = getelementptr i8, ptr %txt, i32 %j\n %cj = load i8, ptr %jq\n %cji = zext i8 %cj to i32\n")
|
|
emith(" %isnl = icmp eq i32 %cji, 10\n %isz = icmp eq i32 %cji, 0\n %stop = or i1 %isnl, %isz\n br i1 %stop, label %ehave, label %eadv\n")
|
|
emith("eadv:\n %j1 = add i32 %j, 1\n store i32 %j1, ptr %jp\n br label %escan\n")
|
|
emith("ehave:\n %je = load i32, ptr %jp\n %jep = getelementptr i8, ptr %txt, i32 %je\n store i8 0, ptr %jep\n %nexti = add i32 %je, 1\n store i32 %nexti, ptr %ip\n")
|
|
# "pack " is 5 bytes, "home " is 5 bytes
|
|
emith(" %ln = getelementptr i8, ptr %txt, i32 %i\n %isp = call i32 @strncmp(ptr %ln, ptr @.pak_kw_pack, i64 5)\n %pk = icmp eq i32 %isp, 0\n br i1 %pk, label %dopack, label %chkhome\n")
|
|
emith("dopack:\n %pv = getelementptr i8, ptr %ln, i32 5\n %full = call ptr @lp_path_join(ptr %dir, ptr %pv)\n %ok = call i32 @lp_pak_mount(ptr %full)\n")
|
|
emith(" %g = load i32, ptr %got\n %g1 = add i32 %g, %ok\n store i32 %g1, ptr %got\n br label %line\n")
|
|
emith("chkhome:\n %ish = call i32 @strncmp(ptr %ln, ptr @.pak_kw_home, i64 5)\n %hm = icmp eq i32 %ish, 0\n br i1 %hm, label %dohome, label %chkspbg\n")
|
|
emith("dohome:\n %hv = getelementptr i8, ptr %ln, i32 5\n call void @lp_pak_sethome(ptr %hv)\n br label %line\n")
|
|
# splashbg before splash: they share a prefix, and only the byte at 6 tells
|
|
# them apart, so the longer keyword has to be offered the line first
|
|
emith("chkspbg:\n %isb = call i32 @strncmp(ptr %ln, ptr @.pak_kw_spbg, i64 9)\n %bg = icmp eq i32 %isb, 0\n br i1 %bg, label %dospbg, label %chksp\n")
|
|
emith("dospbg:\n %bv = getelementptr i8, ptr %ln, i32 9\n %bi = call i32 @atoi(ptr %bv)\n store i32 %bi, ptr @L_pak_splashbg\n br label %line\n")
|
|
emith("chksp:\n %isS = call i32 @strncmp(ptr %ln, ptr @.pak_kw_splash, i64 7)\n %sp = icmp eq i32 %isS, 0\n br i1 %sp, label %dosp, label %line\n")
|
|
emith("dosp:\n %sv = getelementptr i8, ptr %ln, i32 7\n store ptr %sv, ptr @L_pak_splash\n br label %line\n")
|
|
emith("fin:\n %gf = load i32, ptr %got\n ret i32 %gf\n")
|
|
emith("no:\n ret i32 0\n}\n")
|
|
|
|
emith("@.pak_kw_pack = private unnamed_addr constant [6 x i8] c\"pack \\00\"\n")
|
|
emith("@.pak_kw_home = private unnamed_addr constant [6 x i8] c\"home \\00\"\n")
|
|
emith("@.pak_kw_splash = private unnamed_addr constant [8 x i8] c\"splash \\00\"\n")
|
|
emith("@.pak_kw_spbg = private unnamed_addr constant [10 x i8] c\"splashbg \\00\"\n")
|
|
|
|
# point the process at ~/Library/Application Support/<name>, creating it. A
|
|
# bundled game starts with its working directory at "/", so without this every
|
|
# relative write — every save — would fail. On Windows the place is
|
|
# %APPDATA%\<name>, the same directory Os.save_dir names there.
|
|
emith("define void @lp_pak_sethome(ptr %name) {\n")
|
|
if g_target_win {
|
|
emith("entry:\n %h = call ptr @getenv(ptr @.pak_appdata)\n %nh = icmp eq ptr %h, null\n br i1 %nh, label %out, label %go\n")
|
|
emith("go:\n %b = call ptr @lp_path_join(ptr %h, ptr %name)\n call void @lp_win_slashes(ptr %b)\n")
|
|
} else {
|
|
emith("entry:\n %h = call ptr @getenv(ptr @.pak_home)\n %nh = icmp eq ptr %h, null\n br i1 %nh, label %out, label %go\n")
|
|
emith("go:\n %a = call ptr @lp_path_join(ptr %h, ptr @.pak_appsup)\n %b = call ptr @lp_path_join(ptr %a, ptr %name)\n")
|
|
}
|
|
emith(" %mk = call i32 @mkdir(ptr %b, i32 493)\n %cd = call i32 @chdir(ptr %b)\n br label %out\n")
|
|
emith("out:\n ret void\n}\n")
|
|
emith("@.pak_appdata = private unnamed_addr constant [8 x i8] c\"APPDATA\\00\"\n")
|
|
|
|
# The constructor. Look beside the executable: ../Resources first, which is
|
|
# where the bundler puts things inside a .app, then the executable's own
|
|
# directory, which is where they land beside a plain binary.
|
|
emith("define void @lp_pak_boot() {\n")
|
|
emith("entry:\n %buf = alloca [1024 x i8]\n %szp = alloca i32\n store i32 1024, ptr %szp\n")
|
|
emith(" %rc = call i32 @_NSGetExecutablePath(ptr %buf, ptr %szp)\n %bad = icmp ne i32 %rc, 0\n br i1 %bad, label %out, label %go\n")
|
|
emith("go:\n %dir = call ptr @lp_path_dir(ptr %buf)\n %res = call ptr @lp_path_join(ptr %dir, ptr @.pak_res)\n")
|
|
emith(" %n1 = call i32 @lp_pak_boot_dir(ptr %res)\n %any = icmp sgt i32 %n1, 0\n br i1 %any, label %splash, label %try2\n")
|
|
emith("try2:\n %n2 = call i32 @lp_pak_boot_dir(ptr %dir)\n br label %splash\n")
|
|
emith("splash:\n")
|
|
if g_windowed { emith(" call void @lp_pak_splash_go()\n") }
|
|
emith(" br label %out\n")
|
|
emith("out:\n ret void\n}\n")
|
|
|
|
# The splash is an asset like any other: named by packs.index, found in the
|
|
# pack by the same lookup the game's own loads use, and handed to AppKit as
|
|
# bytes. Nothing here knows what format it is - NSImage reads PNG, JPEG and
|
|
# TIFF from the same NSData without being told.
|
|
if g_windowed {
|
|
emith("define void @lp_pak_splash_go() {\n")
|
|
emith("entry:\n %sp = load ptr, ptr @L_pak_splash\n %none = icmp eq ptr %sp, null\n br i1 %none, label %out, label %find\n")
|
|
emith("find:\n %lp = alloca i32\n store i32 0, ptr %lp\n %blob = call ptr @lp_pak_find(ptr %sp, ptr %lp)\n")
|
|
emith(" %miss = icmp eq ptr %blob, null\n br i1 %miss, label %out, label %show\n")
|
|
emith("show:\n %len = load i32, ptr %lp\n %bg = load i32, ptr @L_pak_splashbg\n")
|
|
emith(" call void @splash_show(ptr %blob, i32 %len, i32 %bg)\n br label %out\n")
|
|
emith("out:\n ret void\n}\n")
|
|
|
|
# App.set_icon(path): the Dock tile for a binary that has no bundle to take one
|
|
# from. Read through lp_pak_open, so a packed path and a loose one both work and
|
|
# this does not repeat Audio.load's trick of taking a filesystem path only.
|
|
# NSData copies the bytes, so the buffer goes straight back.
|
|
emith("define void @lp_app_icon(ptr %path) {\n")
|
|
emith("entry:\n %f = call ptr @lp_pak_open(ptr %path, ptr @.pak_rb)\n %bad = icmp eq ptr %f, null\n br i1 %bad, label %out, label %go\n")
|
|
emith("go:\n %s1 = call i32 @fseek(ptr %f, i64 0, i32 2)\n %n64 = call i64 @ftell(ptr %f)\n %s2 = call i32 @fseek(ptr %f, i64 0, i32 0)\n")
|
|
emith(" %tiny = icmp sle i64 %n64, 0\n br i1 %tiny, label %shut, label %read\n")
|
|
emith("read:\n %buf = call ptr @lp_malloc(i64 %n64)\n %rd = call i64 @fread(ptr %buf, i64 1, i64 %n64, ptr %f)\n")
|
|
emith(" %c1 = call i32 @fclose(ptr %f)\n %n32 = trunc i64 %rd to i32\n")
|
|
emith(" call void @app_set_icon(ptr %buf, i32 %n32)\n call void @lp_free(ptr %buf)\n ret void\n")
|
|
emith("shut:\n %c2 = call i32 @fclose(ptr %f)\n ret void\n")
|
|
emith("out:\n ret void\n}\n")
|
|
}
|
|
}
|