Commit graph

256 commits

Author SHA1 Message Date
dc9138846f chore(release): v0.14.1
All checks were successful
bootstrap / cfree-fixpoint (push) Successful in 29s
ci / build-and-test (push) Successful in 3m22s
commit-lint / conventional-commits (push) Successful in 4s
release / publish (push) Successful in 3m12s
2026-09-13 12:46:46 +03:00
bfec11926f fix(render3d): a rim from outline_model goes out when the caller stops asking
The queue was only emptied by the next outline_model call, so the last highlighted tree kept
its rim after the player looked away. r3d_frame now calls outline_frame, which drops a batch
the previous frame closed and nobody reopened.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-13 12:46:46 +03:00
751532831a chore(release): v0.14.0
All checks were successful
bootstrap / cfree-fixpoint (push) Successful in 30s
ci / build-and-test (push) Successful in 3m21s
commit-lint / conventional-commits (push) Successful in 4s
release / publish (push) Successful in 3m11s
2026-09-13 05:03:41 +03:00
e58e8ae263 feat(render3d): UTF-8 overlay text and fonts that list their code points
ov_text, ov_text_w and ov_text_wrap decode UTF-8 instead of drawing bytes 32-126. font.json
may list the atlas's code points in "codes"; an atlas without it reads as before (ASCII
from "first"). A missing code point draws as '?', a cut-off sequence as one '?'.
overlay_font(dir) loads or swaps the atlas at run time, for a language with its own script.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-13 05:03:41 +03:00
a90c58eb09 chore(release): v0.13.3
All checks were successful
bootstrap / cfree-fixpoint (push) Successful in 30s
ci / build-and-test (push) Successful in 3m21s
commit-lint / conventional-commits (push) Successful in 4s
release / publish (push) Successful in 3m12s
2026-09-13 04:18:38 +03:00
d6ffdfa638 perf(render3d): a depth prepass for the near tree foliage
Needle-card crowns are many cut-out quads deep and a discarding shader turns early depth
rejection off, so every card behind the front one ran the full lighting shader. The near
tree LODs now write depth first (depth.frag, the same alpha coverage test), terrain under
them is rejected before shading, and the lit pass draws them with no discard and an equal
depth test (invariant gl_Position). R3D_NOPREPASS=1 restores the old path.

Dense forest on a Windows PC: 61 -> 78 fps at 3840x2160, 116 -> 164 fps at 1920x1080.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-13 04:18:38 +03:00
21bc611455 chore(release): v0.13.2
All checks were successful
bootstrap / cfree-fixpoint (push) Successful in 30s
ci / build-and-test (push) Successful in 3m22s
commit-lint / conventional-commits (push) Successful in 4s
docs / build-and-deploy (push) Successful in 38s
release / publish (push) Successful in 3m12s
2026-09-13 03:48:57 +03:00
39a7593521 fix(cli): link http.ll only for a program that calls hs_send
http_link_flags grepped the IR for "@hs_", which every program's header
declares, so `ludic build` linked the HTTP transport and Foundation into
everything - invisible on macOS, a failed link on Linux, where it broke four CI
cases (ludic run, the installed layout, ludic new, the seed build through
`ludic build`). It now looks for a call to hs_send: examples/library/http.ludic
has one, snake.ludic (which still declares ten @hs_ names) has none.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-13 03:45:04 +03:00
096f851408 chore(release): v0.13.1
Some checks failed
bootstrap / cfree-fixpoint (push) Successful in 30s
ci / build-and-test (push) Failing after 3m2s
commit-lint / conventional-commits (push) Successful in 4s
release / publish (push) Failing after 3m2s
2026-09-13 03:32:18 +03:00
7586c3ad25 fix(ci): supply _NSGetExecutablePath on Linux, so the seed links and CI publishes
The asset-pack boot calls _NSGetExecutablePath, which glibc does not have, so
the Linux link of the compiler seed failed in every CI job - build-and-test,
cfree-fixpoint and every publish run - since packs landed. The last release CI
created was v0.7.0. The Linux shim now defines it over /proc/self/exe.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-13 03:32:18 +03:00
762d39bdff chore(release): v0.13.0
Some checks failed
bootstrap / cfree-fixpoint (push) Failing after 21s
ci / build-and-test (push) Failing after 11s
commit-lint / conventional-commits (push) Successful in 4s
docs / build-and-deploy (push) Failing after 8s
release / publish (push) Failing after 11s
2026-09-13 03:29:55 +03:00
fb4d904ab6 Merge branch 'fix/arrows-http-backspace': held arrow keys, ludic build Http.* link, macOS Backspace
# Conflicts:
#	tools/ludic-cli/build.ludic
2026-09-13 03:28:29 +03:00
40d78cbd92 feat(windows): Http.* over WinHTTP, and the splash and window icon through WIC
http_win.ll implements http.ll's hs_* contract over WinHTTP: the request is a
record built on the game thread, the whole exchange runs on a worker thread,
TLS uses the system's certificate checks, and headers are answered from the
kept request handle. ludicc links it with winhttp instead of refusing Http.* on
Windows.

win32.ll decodes the splash and App.set_icon images with WIC (ole32): the
splash is a topmost borderless window at the artwork's size in points times the
display scale, composited over its background colour; the icon becomes an
HICON set on the window now or when win_open makes one.

Verified on the PC: examples/library/http.ludic prints its expected output, a
real GET to https://git.workshopsoft.io/ returns 200 with its body and
Content-Type, and the bundled Maroon Lake shows its splash centred at launch and
its icon in the title bar.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-13 03:24:50 +03:00
55b0e2ebf6 feat(windows): the ludic CLI and ludic bundle on Windows
The CLI's shell commands go through shell(), which is run() on POSIX and a
scratch script handed to Git for Windows' bash on Windows (exit codes read
directly there). compile_app links through `ludicc -o` on Windows, ludic run
starts the .exe, and ludic-dev build and ensure_ludicc assemble
selfhost/ludicc.win.seed.ll, which ludic-dev reseed now writes beside the
macOS seed. ludic bundle makes build/<name>/ with a GUI-subsystem exe carrying
the .ico beside `app icon` as an llvm-rc resource, game.lpak and packs.index;
ludicc gains --gui and --link, and quotes its whole link line for cmd.exe.

Verified: ludic-dev test 135/135, selfhost-test 32/32; on the PC a checkout
bootstraps from the Windows seed, ludic-dev build makes the toolchain, and
`ludic bundle` makes build/Maroon Lake/, which runs from its own folder.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-13 03:01:33 +03:00
885943e73f fix(cocoa): the Delete key reports Key.Backspace (8)
AppKit gives kVK_Delete (51) the character NSDeleteCharacter, 127, which is
what both ev_keyval (the held-key set) and win_poll (the per-frame key)
received, so Key.Backspace, which folds to 8, never matched. Both now map key
code 51 to 8.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-13 03:00:29 +03:00
09247c57cf fix(cli): ludic build links http.ll for a program that uses Http.*
compile_app linked gl.ll when the IR named @lgl_* but never http.ll and
Foundation for @hs_*, so examples/library/http.ludic failed to link under
`ludic build` on every hs_* symbol while `ludicc -o` built it. http_link_flags
greps for @hs_ and links them in both modes; ludic-dev test now builds the
example through `ludic build` as well.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-13 03:00:29 +03:00
6117f73602 fix(input): Key.Up/Down/Left/Right are the held set's 128-131
The arrow Key constants folded to the codes of w/s/a/d, which is what the
per-frame key reports for an arrow, but cocoa.ll has always stored an arrow in
the held-key set under 128-131. So Input.key_down(Key.Up) read the W bit and
Input.move_i's arrow half never moved anything. Input.key keeps its WASD alias,
so games comparing it with 'w' (snake, chronorift) still take the arrows.

New example input_arrows.ludic, checked by ludic-dev test.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-13 03:00:29 +03:00
63d9f61bf1 feat(windows): a window sized by the display's scale, as a Retina Mac does
A game asking for 960 x 540 got 960 x 540 pixels - a quarter of a 4K panel.
w_fit_scale turns the display's DPI into a whole-number scale (96 -> 1, 168 and
192 -> 2), brought down until the window fits; win_open and win_gl_resize size
the client area with it, win_gl_scale reports it the way cocoa.ll reports the
backing scale, and the mouse still arrives in the game's own units.

Verified on a 3840x2160 panel at 175%: windowed gl_triangle's 640x360 window
draws a 1280x720 frame.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-13 02:46:00 +03:00
3300fb3957 feat(windows): sound, through XAudio2
audio_win.ll implements audio.ll's snd_* contract over XAudio2 from IR: a
source voice per clip, restart on play, LoopCount for loops, SetVolume,
SetFrequencyRatio and a balance pan through SetOutputMatrix, with the COM
slots taken from the SDK's xaudio2.h. Clips are RIFF WAVE read through
lp_pak_open (weakly referenced), so a packed sound loads directly. ludicc links
it with xaudio2 and ole32, and silences xinput.lib's importeddllmain warning.

Verified: ludic-dev test 135/135, selfhost-test 32/32; on the PC a harness
loads, plays, pans, loops and stops clips, and Maroon Lake windowed reports
"sound: 31 of 31 clips loaded".

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-13 02:42:48 +03:00
00d25dcc3e feat(windows): a window - win32.ll, WGL on it, and a windowed Windows build
win32.ll implements cocoa.ll's contracts over user32 and xinput: the message
pump, the held-key set and frame key in Ludic codes, the mouse with raw input
for cursor mode 2, clip-based cursor modes released on focus loss, XInput pads,
and the software framebuffer present. win32_gl.ll puts the WGL 4.1 core context
on that window (vsync, borderless full screen); gl_win.ll's context code is now
lgl_wgl_core, shared with the headless hidden window, whose win_gl_* stubs move
to gl_win_nowin.ll. audio_win.ll links Audio.* silently for now.

Verified: macOS fixpoint, ludic-dev test 135/135, selfhost-test 32/32; on the
PC gl_triangle renders identically headless and in a real window, and Maroon
Lake builds windowed and runs a playtest to frame 240 in the desktop session.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-13 02:33:53 +03:00
1bc255bc40 fix(windows): Fs.remove removes an empty directory, as POSIX remove does
The UCRT's remove() deletes files only, so a tree removed bottom-up left every
directory behind. emit_win defines remove over DeleteFileA, falling back to
RemoveDirectoryA. Found by Maroon Lake's selftest26 ("1 left behind"), which now
passes on Windows with the rest of that game's self-tests.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-13 02:27:21 +03:00
cb05721a89 feat(windows): Gl.* on Windows, through WGL and a driver-filled thunk table
gl_win.ll creates a hidden-window WGL 4.1 core context and carries gl.ll's
float/memory helpers, with ldexp and QueryPerformanceCounter in place of the
libSystem calls. glgen now also writes gl_thunks_win.ll: the same 478 thunks,
calling through pointers that @lgl_win_load fills from wglGetProcAddress (and
opengl32.dll for GL 1.1). ludicc links the pair against opengl32/gdi32/user32
on a Windows target.

Verified: gl_api.ludic and gl_thunks.ll regenerate byte-identically, ludic-dev
test 135/135, selfhost-test 32/32, and headless gl_triangle on an RTX 3070 Ti
matches the macOS frame to within one level per channel.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-13 02:10:36 +03:00
5801005fca feat(windows): a Windows target for ludicc, and the compiler builds itself there
--target <triple> (default: the host, from OS=Windows_NT) selects the Windows
runtime. emit_win.ludic defines the POSIX names the backend already calls over
the UCRT and Win32 in IR, so rename replaces an existing file, ftell is 64-bit,
and fopen is binary. Known folders follow %APPDATA% / %LOCALAPPDATA% / %TEMP%,
and the driver speaks cmd.exe, writes .exe outputs and finds LLVM's clang.

Verified: macOS three-stage fixpoint, ludic-dev test 135/135, and on Windows
the Mac-emitted Windows IR and the Windows-built compiler's IR are identical
through two generations.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-13 01:59:41 +03:00
572e09b2b1 chore(release): v0.12.1
Some checks failed
bootstrap / cfree-fixpoint (push) Failing after 10s
ci / build-and-test (push) Failing after 9s
commit-lint / conventional-commits (push) Successful in 2s
release / publish (push) Failing after 10s
2026-09-13 00:29:37 +03:00
3e76785b40 fix(render3d): lakes clipped to their ellipse; r3d_fog_base
A water body other than the reflecting one drew its whole bounding rectangle,
so the corners outside the lake's carved ellipse showed water over dry ground;
those bodies now discard outside the ellipse. r3d_fog_base (default 0) is the
height the height fog is measured from, so maps sharing one datum at different
heights get the same air.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-13 00:29:37 +03:00
7a0c03dd21 chore(release): v0.12.0
Some checks failed
release / publish (push) Failing after 10s
2026-09-13 00:03:56 +03:00
1ce164a01e Merge branch 'feat/render3d-reload' 2026-09-13 00:03:40 +03:00
a4a3d75cd9 feat(render3d): replace the world at run time, water bodies, terrain_sea
terrain_reload/terrain_unload release one map's height field, survey, photograph
and patch bounds and generate another at any TERRAIN_HALF; scatter_clear_all
(with streams), actor_clear_all, col_reset and mesh_free empty the scene;
water_body_add/water_bodies_clear draw several still-water planes with one
reflecting; terrain_sea separates the coast's sea level from the carved lake's,
and grass keeps off both. Fixes CDLOD patch bounds for TERRAIN_HALF != 4096 and
water_init leaking a mesh and program per call. examples/rendering/reload.ludic.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-13 00:03:40 +03:00
70bda0c4df chore(release): v0.11.1
Some checks failed
bootstrap / cfree-fixpoint (push) Failing after 9s
ci / build-and-test (push) Failing after 9s
commit-lint / conventional-commits (push) Successful in 3s
release / publish (push) Failing after 10s
2026-09-12 13:55:19 +03:00
219e638316 fix(render3d): col_resolve threw a body ~1000x too far from dead centre
The degenerate case - a point exactly on a collider's axis, where there is no
direction to push it - set the normal to (1, 0), which is already a unit vector,
and then divided it by the clamped d = 0.001 along with the genuine normals. The
push came out a thousand times too big: dead centre on a 0.5 m trunk moved a body
about 800 m rather than the 0.85 m that clears it.

Off-centre the arithmetic was right, and off-centre is how anything arrives at a
trunk on foot, so nothing in play ever hit it. A teleport, a spawn, or a world
generator dropping something onto an existing collider would have.

(ex, ez) / d is a unit vector for every d > 0, because d is its own length -
there was never anything to clamp and nothing that could grow. The normal is now
built once and explicitly, and the clamp is gone.

Verified through a game, which is the only harness this package has: render3d's
own float helpers need the Gl runtime spliced, so collide.ludic cannot be
compiled standalone for a unit test. Maroon Lake's selftest12 stands a body dead
centre on a trunk and asserts the push never exceeds the two radii added together
- which is the definition of being pushed clear, and so the tightest honest bound
available. It reports 798.88 m before this change and 0.80 m after, with the
off-centre case unchanged at 0.66 m.
2026-09-12 13:55:19 +03:00
b16b7aaf0b chore(release): v0.11.0
Some checks failed
bootstrap / cfree-fixpoint (push) Failing after 19s
ci / build-and-test (push) Failing after 9s
commit-lint / conventional-commits (push) Successful in 2s
docs / build-and-deploy (push) Failing after 8s
release / publish (push) Failing after 10s
2026-09-12 12:55:12 +03:00
38b6b81cd7 feat(app): App.set_icon, so a game without a bundle still has an icon
`ludic bundle` builds an AppIcon.icns and macOS reads it out of the .app, so a
shipped game has an icon. `ludic build` produces a bare executable: no bundle, no
CFBundleIconFile, and so no icon at all - macOS draws the generic green "exec"
tile. That is the build a developer runs every day, which is why "the game has no
icon" can be true for months while the bundle is perfect. It was here: the .app's
icns validated against iconutil, the plist was right, the signature was right,
and NSWorkspace rendered the artwork - and the binary beside it still had the
exec tile.

App.set_icon(path) takes the bytes 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 them, so the buffer goes straight back. A
missing or undecodable image leaves the existing icon alone rather than clearing
it; a bundled app is unaffected; headless links no AppKit and compiles it away.

Verified the Cocoa sequence against an ObjC twin doing the same message sends:
before, a bare binary's applicationIconImage is the generic 128x128 tile; after,
it is the 1024x1024 artwork.

Backend change, so selfhost/ludicc.seed.ll is reseeded: the bootstrap fixpoint
and the C-free rebuild from the seed both pass.
2026-09-12 12:55:12 +03:00
925d133466 chore(release): v0.10.1
Some checks failed
bootstrap / cfree-fixpoint (push) Failing after 10s
ci / build-and-test (push) Failing after 9s
commit-lint / conventional-commits (push) Successful in 2s
docs / build-and-deploy (push) Failing after 8s
release / publish (push) Failing after 10s
2026-09-11 19:46:53 +03:00
6bcb5f4ae1 fix(os): save_dir/config_dir/cache_dir follow the platform, not just macOS
They were the macOS layout on every platform - the comment above them even said
"macOS/BSD layout" - so a game built on Linux wrote its saves to
~/Library/Application Support, a directory that means nothing there. Naming the
right directory is the entire reason a program calls these instead of joining a
path itself.

  macOS   save/config  ~/Library/Application Support/<app>
          cache        ~/Library/Caches/<app>
  Linux   save         $XDG_DATA_HOME   or ~/.local/share/<app>
          config       $XDG_CONFIG_HOME or ~/.config/<app>
          cache        $XDG_CACHE_HOME  or ~/.cache/<app>

macOS is unchanged to the byte, deliberately. save_dir and config_dir stay the
same directory there: Apple's home for a config file that is not an
NSUserDefaults plist is Application Support too, and a shipped game's settings
must not move out from under it. On Linux XDG separates the two and so does this.
An XDG variable that is set but EMPTY falls back to the default, which is what
the spec says and what an exported-but-unset variable looks like from a shell.

uname is read once and remembered rather than per call, because save_dir is
called on every save.

Verified on both branches. macOS by running it; the Linux branch by building the
probe's IR with the cached platform flag pinned, which exercises the emitted XDG
code exactly - unset, set, and set-but-empty all resolve as the spec says. The
suite's own case asserts the HOST's convention, so a macOS box covers the Apple
branch and CI covers XDG.

This is a backend change, so selfhost/ludicc.seed.ll is reseeded with it: the
bootstrap fixpoint (gen2 == gen3) and the C-free rebuild from the seed both pass.
2026-09-11 19:46:47 +03:00
c069459343 chore(release): v0.10.0
Some checks failed
bootstrap / cfree-fixpoint (push) Failing after 9s
ci / build-and-test (push) Failing after 9s
commit-lint / conventional-commits (push) Successful in 2s
docs / build-and-deploy (push) Failing after 8s
release / publish (push) Failing after 9s
2026-09-11 18:32:04 +03:00
cf814d4a97 feat(pack): .packignore, so a pack root can leave build artefacts out
A pack root is packed wholesale, and that is the right default - a game writes
`pack "assets"` and everything it opens is in the pack. What also goes in is
everything the game does NOT open: the preview renders a model pipeline leaves
beside its meshes, the intermediate a texture bake writes and never reads again,
the .blend the .gltf came out of. Nothing errors, nothing looks wrong, and the
app is simply bigger than the game. The only way out the manifest offered was
naming every file by hand, which is worse - a list that goes stale the day
someone adds a texture.

So `.packignore`, with gitignore's rules, because that is the file everyone
already knows. Anchored and floating patterns, `preview/` for directories only,
`*` and `?` stopping at a separator where `**` crosses one, `[a-z]` classes, `!`
re-includes with the last line winning, a deeper file beating a shallower one,
and no re-including out of an ignored directory.

The semantics are not claimed, they are checked: the implementation was diffed
against git itself over two fixtures - 35 paths, 19 patterns, nested ignore
files, directory negation, `[!0-9]` and `\#` escaping - and `git check-ignore`
and `ludic pack` agree on every path.

Two rules of its own, because a pack is not a working tree. `.packignore` is
never packed (nothing reads one at run time, and --no-ignore does not bring it
back). And it governs the project's own roots only: a package's resources - the
renderer's shaders above all - are added after the gather, so a stray `*.frag`
in a game's ignore file cannot quietly un-ship what it needs to draw anything.

`ludic pack` reports what it left out; `--no-ignore` packs everything so you can
see what a rule is costing. `ludic bundle` gathers through the same path, so the
two agree by construction.
2026-09-11 18:29:32 +03:00
841ae1d442 chore(release): v0.9.1
Some checks failed
bootstrap / cfree-fixpoint (push) Failing after 19s
ci / build-and-test (push) Failing after 9s
commit-lint / conventional-commits (push) Successful in 1s
docs / build-and-deploy (push) Failing after 8s
release / publish (push) Failing after 9s
2026-09-11 15:43:16 +03:00
3137df4fe6 fix(render3d): outline_model's batch survives the whole frame
A frame is drawn by more than one pass - a shadow map, a water reflection, the
scene - and actor_draw runs in each. An Actor's rim survives that because it is
a field on the actor; the queue did not, because the first pass to run emptied
it. A queued outline was drawn into whichever target came first and was gone by
the time the scene was drawn, so nothing appeared with every uniform, matrix
and mesh correct - which took a while to find.

A flush now closes the batch rather than clearing it, and the next
outline_model opens a new one.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-11 15:43:16 +03:00
ee2595e139 chore(release): v0.9.0
Some checks failed
release / publish (push) Failing after 10s
2026-09-11 15:25:46 +03:00
3c27606747 feat: per-voice audio, outlines for what is not an actor, and a coast
Three things a game could not say, each of which had been worked around.

Audio.play_at(id, gain:, pitch:, pan:) fires a one-shot with its own gain,
pitch and stereo position. Audio.volume and Audio.pitch are global - they are
the options screen - so a game placing a sound in the world was fighting them,
and distance attenuation was simply not expressible. The backend already took
volume and rate per call; this adds setPan: alongside them and stops routing
through the master state.

ludic.render3d gains outline_model(model, mat, width, r, g, b): a rim around
something that is not an Actor. The outline pass walked the actor list and
stopped, so instanced scatter - a forest - could not be highlighted at all. It
is a queue flushed by the same pass, which is what gets the depth test right
when the caller does not control pass order.

And terrain_coast(cx, cz, margin, fall), the other way to make an island:
the sea around the survey's own edge rather than cut out of the middle of it.
terrain_island measures a radius from a centre, which drowns two thirds of a
real survey to make an island of the rest; this measures inward from the
boundary, so everything the data covers stays land and the coast is where the
data runs out. Both modes gained a strand - the last few metres of height
either side of the water line compressed, which stretches a cliff plunge out
into beach and shallows.

132 regression tests and the self-host fixpoints pass.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-11 15:25:46 +03:00
39ad8e85d9 chore(release): v0.8.0
Some checks failed
bootstrap / cfree-fixpoint (push) Failing after 9s
ci / build-and-test (push) Failing after 9s
commit-lint / conventional-commits (push) Successful in 2s
docs / build-and-deploy (push) Failing after 8s
release / publish (push) Failing after 9s
2026-09-10 17:20:20 +03:00
cbf38fb316 docs(changes): changesets for the renderer work in this release
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-10 17:20:14 +03:00
d80226f948 fix(pack): follow symlinks, and say so when a root packs nothing
Maroon Lake keeps assets/polyhaven as a symlink into a shared checkout - which
is an ordinary thing to do, and what `ludic assets` encourages - and `find`
does not follow symlinks. The pack was written, reported success, and silently
omitted all 71 files behind the link, including the sky HDRI.

What that looked like from the outside is worth recording, because it is the
failure mode this whole feature has to avoid: the bundled game started, printed
one line about an HDRI it could not read, carried on, and then died in
terrain_height reading offset 0x1cd681c off a null pointer - the CPU height
field, never allocated, because r3d_init had given up several steps earlier. A
missing asset surfaced as a segfault a long way from the cause.

So: `find -L`, and a warning when a declared root contributes no files at all.
A root that packs nothing is nearly always a typo or a link into a tree that was
never fetched, and the warning costs one line where the alternative costs an
afternoon.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-10 17:16:38 +03:00
be74b4de6f 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>
2026-09-10 16:57:27 +03:00
ce5bf0fe0e feat(pack): asset packs, so a built game is a thing you can hand someone
A Ludic game opened its assets by a path relative to the working directory, so
`build/mygame` ran only from the project root and there was nothing to give
anyone but "the binary, and also this whole tree". A game is not one file, but
shipping it has to be.

`ludic pack` writes a .lpak: a header, a name-sorted entry table, a name heap
and the blobs, 16-byte aligned. Entries are stored rather than compressed - PNG,
JPEG and glTF binary arrive compressed already, and a decompressor on the load
path would spend CPU to make the file no smaller.

The reader is spliced into the compiler at the one place every asset in a Ludic
program comes through: file_open. gltf_load, tex_load, Audio.load, Fs.read_text
and the renderer's own shader loads all bottom out there, so routing it through
@lp_pak_open reaches every one of them without any of them knowing. A read of a
packed path becomes an fmemopen over the mapped bytes; everything else is the
fopen it always was. Fs.exists and Fs.size consult the packs too, so a game that
guards a load with Fs.exists keeps finding its assets once they are packed.

The pack is mmap'd rather than read: 165 MB of textures costs one syscall at
boot and pages in only what is touched. @lp_pak_boot runs from
@llvm.global_ctors, before main, so a pack is mounted before the game's first
line - and it reads packs.index, a plain list, so the mount order is explicit
and a later pack shadows an earlier one.

Without a packs.index nothing mounts and every open goes to the filesystem
exactly as before, which is every `ludic run` during development. Packing is a
shipping step and is invisible until you ship.

The compiler reproduces itself byte-exactly and the C-free bootstrap from the
seed still holds.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-10 16:37:23 +03:00
9ba093bf07 fix(render3d): water read the window's size, not the frame it drew into
The water shader's `u_screen`, and the reflection target's size, were both taken
from gl_w/gl_h - the drawable. But gl_FragCoord in that shader runs over the
scene target, which is post_w x post_h. They match only at a render scale of 1;
at anything less the refraction and depth reads landed in the wrong corner of
the frame and the lake showed a squashed copy of it instead of its own bed.

Both now come from the scene target. The reflection is sized from it too, which
also stops it paying for pixels the water never samples. R3D_DUMP_REFL reads the
target's own w/h rather than recomputing them.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-10 16:15:34 +03:00
9c00cd3e44 feat(render3d): terrain_island, a coastline out of the survey
`terrain_island(cx, cz, r, fall)` keeps land out to `r` and then scales the
terrain down into the water over `fall` metres and on to a shelf. Scaling rather
than blending to a fixed bed is what makes the coastline come out of the terrain
that is already there: low ground turns into beach and shallows, high ground
into cliff. `r = 0` leaves the survey alone, so nothing that does not ask for an
island is touched.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-10 16:15:26 +03:00
6043a67631 feat(render3d): give the moon a phase
The moon was fixed opposite the sun and worth a constant trickle of light. It
now carries a phase, 0 new .. 0.5 full .. 1 new again, and that one number
decides three things at once: the disc's terminator, the fraction of its light
that reaches the ground, and where in the sky it rides.

The moon is where the sun was `lag` of a day ago, so a full moon rises as the
sun sets and a new moon travels with the sun and is never seen. A new moon is
now a properly dark night, which is what makes a carried light worth having.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-10 16:15:19 +03:00
2542cb591d feat(render3d): a rim outline around a highlighted actor
An actor gains `outline` (metres of rim) and `ocol` (its colour). The pass draws
the model a second time with its vertices pushed out along their normals and its
front faces culled, so only the far side of the swollen shell survives - a
silhouette exactly `outline` metres wide. It is depth-tested against the scene,
so anything standing in front of the actor hides the rim too.

skin.vert grows an OUTLINE path for the push; outline.frag is the flat-colour
fragment shader it pairs with.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-10 16:15:12 +03:00
9d1df6ec32 chore(release): v0.7.0
All checks were successful
bootstrap / cfree-fixpoint (push) Successful in 24s
ci / build-and-test (push) Successful in 3m3s
commit-lint / conventional-commits (push) Successful in 1s
release / publish (push) Successful in 2m52s
2026-09-10 03:31:41 +03:00