ludic build on every core: the IR split with llvm-split and compiled by a clang per part at once

Most of a build was one clang -O2 on one .ll (the game: 32 s of a 41 s headless build, one core).
Both paths that assemble - the CLI's (build.ludic: ludicc --emit-llvm, then clang) and ludicc's own
(-o, which ludic bundle and the examples use) - now cut the program's IR into N parts with llvm-split
(externalizing what the parts share), compile them with one clang each in parallel (-x ir -O<opt>
-mmacosx-version-min=11.0, the link's own clang taking the objects where it took the .ll), and remove
the parts and objects after. N is $LUDIC_JOBS, else min(cores, free GB / 1.5).

It needs an llvm-split and a clang of the same LLVM (Homebrew's LLVM 22 writes attributes Apple's
clang 17 cannot read): $LUDIC_LLVM, else /opt/homebrew/opt/llvm/bin. With either missing, on Windows
(its shell cannot run the parts at once yet), with LUDIC_SPLIT=0, or when a part fails, it compiles the
.ll whole as before.

$LUDIC_OPT=1 is a developer's faster build; ludic bundle sets LUDIC_OPT=2 for its compile whatever the
shell says.

Measured before the compile-only rule (this Mac, 12 cores, one build at a time):
- the game headless: 37-41 s -> 13-15.5 s (8 parts / by free memory), peak 2.1 GB -> 1.0-1.1 GB;
- the lab headless: 43.1 s -> 12.7 s, peak 2.4 GB -> 1.0 GB;
- the game at LUDIC_OPT=1, split: 11.8 s (fps cost not measured).
Both built and linked clean; the goldens and a headless shot of the result are not run here.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Orkun ÇAKILKAYA 2026-09-29 17:12:27 +03:00
parent 9f94be7cca
commit 9cc3f24c56
10 changed files with 4119 additions and 2288 deletions

View file

@ -54,7 +54,12 @@ function compile_app(src: pointer, out: pointer, mode: int, save: bool) -> bool
if mode == 2 {
if not shq(`{ludicc()} --headless{title_flag()}{unsafe_flag()} {src} --emit-llvm -o {ll}`) { return false }
if not shq(`{cc()} -O2 {ll}{gl_link_flags(ll)}{vk_link_flags(ll)}{http_link_flags(ll)}{udp_link_flags(ll)}{process_link_flags(ll)}{threads_link_flags(ll)}{native_link_flags(ll)}{pbf} -o {out}`) { return false }
let objs = split_objs(ll, out) # on every core when it can (split.ludic)
var input = ll
if objs != "" { input = objs }
let linked = shq(`{cc()} -O{split_opt()} {input}{gl_link_flags(ll)}{vk_link_flags(ll)}{http_link_flags(ll)}{udp_link_flags(ll)}{process_link_flags(ll)}{threads_link_flags(ll)}{native_link_flags(ll)}{pbf} -o {out}`)
if objs != "" { shell(`rm -f {objs}`) }
if not linked { return false }
g_built_natives = native_libs_of(ll)
if not save { shell(`rm -f {ll}`) }
return true
@ -65,7 +70,12 @@ 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{gl_link_flags(ll)}{vk_link_flags(ll)}{http_link_flags(ll)}{udp_link_flags(ll)}{process_link_flags(ll)}{threads_link_flags(ll)}{native_link_flags(ll)}{pbf} -o {out}`) { return false }
let objs = split_objs(ll, out)
var input = ll
if objs != "" { input = objs }
let linked = shq(`{cc()} -O{split_opt()} {input} {cocoa} {audio} -framework Cocoa -Wl,-needed_framework,GameController -Wl,-needed_framework,AVFoundation -Wl,-rpath,@loader_path{gl_link_flags(ll)}{vk_link_flags(ll)}{http_link_flags(ll)}{udp_link_flags(ll)}{process_link_flags(ll)}{threads_link_flags(ll)}{native_link_flags(ll)}{pbf} -o {out}`)
if objs != "" { shell(`rm -f {objs}`) }
if not linked { return false }
g_built_natives = native_libs_of(ll)
if not save { shell(`rm -f {ll}`) }
return true