feat(bundle): ship a game as a macOS .app, with a splash it controls
`ludic build` produces a program. Double-clicked it opens a Terminal window, it wears the generic executable icon, it calls itself whatever the file is called, and it carries none of its assets. `ludic bundle` produces an application. Everything it needs is in package.ludic, so the command takes no arguments: an Info.plist and PkgInfo from `app` lines, an .icns built by sips and iconutil at all ten sizes macOS asks for from a single source PNG, the asset pack in Contents/Resources, and an ad-hoc signature - which is not optional on Apple silicon, where an unsigned binary is killed rather than warned about. The bundle identifier falls back to the package path reversed, so a project that never thinks about it still gets a defensible one instead of two apps sharing a key Launch Services hangs the Dock, saved state and permissions off. A bundled game is moved to ~/Library/Application Support/<name> before main, because Finder starts a .app with its working directory at "/" where no save could ever be written. Reads come out of the pack, writes land somewhere real and per-user, and the game's save code needs no change and no platform knowledge. The splash is the other half of looking like an application. A game that loads 165 MB spends a visible moment doing it with nothing on screen, which from the outside is indistinguishable from a launch that failed. splash_show puts a borderless window up from the same constructor that mounts the pack - before main, so it appears while the process is still starting rather than after the slow part it exists to cover - and reads the artwork out of the pack like any other asset. It turns the run loop enough times to be mapped and composited there and then; once composited the backing store survives a busy main thread, so it stays up for the whole load. Nothing hides it automatically. Only the game knows when its first real frame is ready, and a splash that vanishes before that leaves the same black gap it was covering, so the game calls App.splash_hide(). Headless there is no splash and the call lowers to nothing. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
ce5bf0fe0e
commit
be74b4de6f
14 changed files with 34667 additions and 33621 deletions
|
|
@ -234,6 +234,23 @@ function emit_ns_call(ns: pointer, meth: pointer, e: Node) -> Val {
|
|||
# through a freelist (L_alloc pops @L_freen before growing @L_entc), and component
|
||||
# storage is fixed per-entity arrays — so there is no per-spawn heap allocation or
|
||||
# fragmentation. Pool.* just reads those counters so a game can watch reuse.
|
||||
# App.* — the process itself, as distinct from the window. Today that is the
|
||||
# boot splash: the runtime raises it before main from the pack, and only the
|
||||
# game knows when its first real frame is ready to replace it, so taking it
|
||||
# down is the game's call and never the runtime's.
|
||||
#
|
||||
# Headless there is no splash and no cocoa.ll to hold one, so the call lowers
|
||||
# to nothing rather than to a symbol that would not link.
|
||||
if (ns == "App") {
|
||||
if (meth == "splash_hide") {
|
||||
# the declaration lives in the pack prelude, and a splash is a packed
|
||||
# asset, so asking to dismiss one asks for the runtime that raised it
|
||||
use_pak()
|
||||
if g_windowed { emit(" call void @splash_hide()\n") }
|
||||
return val("0", "void")
|
||||
}
|
||||
perr(`unknown builtin App.{meth}`)
|
||||
}
|
||||
if (ns == "Pool") {
|
||||
if (meth == "capacity") { return val(itoa(MAX_ENT), "int") } # max entities
|
||||
if (meth == "reserved") { return val(emit_bind("load i32, ptr @L_entc"), "int") } # slots ever allocated (high-water)
|
||||
|
|
|
|||
|
|
@ -58,6 +58,12 @@
|
|||
#
|
||||
# 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
|
||||
|
|
@ -101,6 +107,17 @@ function emit_pak_prelude() -> void {
|
|||
# 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")
|
||||
# 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")
|
||||
}
|
||||
|
||||
emit_pak_mount()
|
||||
emit_pak_lookup()
|
||||
|
|
@ -232,13 +249,21 @@ function emit_pak_boot() -> void {
|
|||
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 %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_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
|
||||
|
|
@ -256,7 +281,24 @@ function emit_pak_boot() -> void {
|
|||
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 %out, label %try2\n")
|
||||
emith("try2:\n %n2 = call i32 @lp_pak_boot_dir(ptr %dir)\n br label %out\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")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
67340
selfhost/ludicc.seed.ll
67340
selfhost/ludicc.seed.ll
File diff suppressed because it is too large
Load diff
Loading…
Add table
Add a link
Reference in a new issue