-
v0.15.0
Pre-releaseSome checks failedreleased this
2026-09-15 08:55:37 +02:00 | 920 commits to main since this releasev0.15.0 — 2026-09-15
Features
-
Vulkan for Ludic, and the start of a second renderer beside OpenGL.
Vk.*: every command of Vulkan 1.0-1.4 and of the extensions a modern renderer is
built around (swapchain, HDR colour spaces, ray query and acceleration structures, opacity
micromaps, mesh shaders, variable rate shading, memory budget, pipeline libraries, NVIDIA
low latency, portability for MoltenVK), with every constant and every struct's size
(<Struct>_sizeof) and field offsets (<Struct>_<field>). Generated from the Vulkan
registry byludic-dev vkgenintoruntime/native/vk_api.ludicandvk_thunks.ll;
structs are plain memory filled by name withVk.put_i32/put_i64/put_ptr. Every
size and offset was checked against the SDK's C headers (3128 facts). A C float is float
bits in an int; 64-bit values and non-dispatchable handles arelong.- The loader is opened at run time, never linked:
vk_win.ll(vulkan-1.dll) and
vk_mac.ll(libvulkan.1.dylib, MoltenVK).Vk.open()returning 0 means no Vulkan, and
the program carries on.ludiccandludic buildlink both files for any program that
usesVk.*. examples/rendering/vk_probe.ludicreports what a machine's Vulkan can do;
vk_compute.ludicruns a Slang compute shader (vk_compute.slang) and reads the picture
back - on an RTX 3070 Ti and on an M4 Pro through MoltenVK, clean under the validation layer.- render3d
gpu.ludic: the seam between the renderer and a graphics API. Render state
and uniforms go throughgpu_*/u_*and nowhere else (OpenGL frames unchanged);
R3D_GFX=gl|vkorgpu_requestchooses a backend, falling back to OpenGL with a reason;
gpu_caps_probe()detects, on Windows, the Vulkan 1.3 floor, ray tracing, mesh shaders,
the NVIDIA RTX generation, Reflex and HDR, for a settings screen to grey out what a machine
cannot use (R3D_CAPS=rtx50|rtx40|rtx30|amd|intel|nonepretends, for tests).
Downloads
-
Source code (ZIP)
0 downloads
-
Source code (TAR.GZ)
0 downloads
-
ludic-0.15.0-linux-x86_64.tar.gz
0 downloads · 1,020 KiB
-
ludic-0.15.0-linux-x86_64.tar.gz.sha256
0 downloads · 99 B
-
ludic-0.15.0-src.tar.gz
0 downloads · 2.8 MiB
-
ludic-0.15.0-src.tar.gz.sha256
0 downloads · 90 B
-
-
v0.14.2
Pre-releaseAll checks were successfulreleased this
2026-09-14 14:35:40 +02:00 | 924 commits to main since this releasev0.14.2 — 2026-09-14
Fixes
Input.mouse_dx/mouse_dyno longer jump on the first frame or when the cursor mode changes.
The delta was the position minus the previous frame's, and the previous position started at 0,0,
so the first frame reported the cursor's whole distance from the corner as motion, and switching
between a locked cursor's virtual reticle and the real cursor did the same. A camera that adds
mouse_dyto its pitch came up pointing at the ground with nobody touching the mouse. Both frames
now report no motion; every other frame is unchanged, windowed or headless, on macOS and Windows.
Downloads
-
Source code (ZIP)
0 downloads
-
Source code (TAR.GZ)
0 downloads
-
ludic-0.14.2-linux-x86_64.tar.gz
0 downloads · 942 KiB
-
ludic-0.14.2-linux-x86_64.tar.gz.sha256
0 downloads · 99 B
-
ludic-0.14.2-src.tar.gz
0 downloads · 2.7 MiB
-
ludic-0.14.2-src.tar.gz.sha256
0 downloads · 90 B
-
v0.14.1
Pre-releasereleased this
2026-09-13 11:46:46 +02:00 | 926 commits to main since this releasev0.14.1 — 2026-09-13
Fixes
- A rim from
outline_modelno longer stays on after the caller stops asking — the queue
was only emptied by the nextoutline_modelcall, so a game that highlights what the
crosshair is on left the last highlighted tree outlined for good once the player looked
away.r3d_framenow drops a batch nobody reopened this frame (outline_frame).
Downloads
-
Source code (ZIP)
0 downloads
-
Source code (TAR.GZ)
0 downloads
-
ludic-0.14.1-linux-x86_64.tar.gz
0 downloads · 941 KiB
-
ludic-0.14.1-linux-x86_64.tar.gz.sha256
0 downloads · 99 B
-
ludic-0.14.1-src.tar.gz
0 downloads · 2.7 MiB
-
ludic-0.14.1-src.tar.gz.sha256
0 downloads · 90 B
- A rim from
-
v0.14.0
Pre-releasereleased this
2026-09-13 04:03:41 +02:00 | 928 commits to main since this releasev0.14.0 — 2026-09-13
Features
-
Overlay text is UTF-8, and a font atlas can carry any script —
ludic.render3d'sov_text,
ov_text_wandov_text_wrapdecode UTF-8 instead of drawing bytes 32–126, so a game can show
Turkish, German, Polish, Greek, Cyrillic or whatever its atlas holds.font.jsonmay list the atlas's code points in"codes"(atlas order). An atlas without it is
read as before: consecutive code points from"first", so existing ASCII fonts keep working.- A code point the atlas lacks draws as
?; a control character as a space; a malformed or
cut-off sequence as one?, so a string sliced mid-character still draws. overlay_font(dir)loads or swaps the atlas at run time, for a language that brings its own font.
Downloads
-
Source code (ZIP)
0 downloads
-
Source code (TAR.GZ)
0 downloads
-
ludic-0.14.0-linux-x86_64.tar.gz
0 downloads · 941 KiB
-
ludic-0.14.0-linux-x86_64.tar.gz.sha256
0 downloads · 99 B
-
ludic-0.14.0-src.tar.gz
0 downloads · 2.7 MiB
-
ludic-0.14.0-src.tar.gz.sha256
0 downloads · 90 B
-
-
v0.13.3
Pre-releasereleased this
2026-09-13 03:18:38 +02:00 | 930 commits to main since this releasev0.13.3 — 2026-09-13
Performance
-
Dense forest renders faster — a depth prepass for the near tree foliage in
ludic.render3d.A crown of needle cards is many cut-out quads deep, and a shader that can
discardturns
early depth rejection off, so every card behind the front one ran the full lighting shader.
In a dense stand at 3840x2160 that made the vegetation pass the largest in the frame. The near
tree LODs now write depth first withdepth.frag(the same alpha coverage test), the terrain
beneath them is rejected before shading, and the lit pass draws them with nodiscardand an equal depth test.R3D_NOPREPASS=1restores the old path for comparison.
Downloads
-
Source code (ZIP)
0 downloads
-
Source code (TAR.GZ)
0 downloads
-
ludic-0.13.3-linux-x86_64.tar.gz
0 downloads · 940 KiB
-
ludic-0.13.3-linux-x86_64.tar.gz.sha256
0 downloads · 99 B
-
ludic-0.13.3-src.tar.gz
0 downloads · 2.7 MiB
-
ludic-0.13.3-src.tar.gz.sha256
0 downloads · 90 B
-
-
v0.13.2
Pre-releaseAll checks were successfulreleased this
2026-09-13 02:48:57 +02:00 | 932 commits to main since this releasev0.13.2 — 2026-09-13
Fixes
-
ludic buildlinkshttp.llonly into a program that usesHttp.*.The check that added it grepped the IR for
@hs_, which every program's header declares,
so the HTTP transport and Foundation were linked into everything. That cost nothing visible
on macOS and failed the link on Linux, whereludic build,ludic runand a new project all
broke in CI. It now looks for a call tohs_send.
Downloads
-
Source code (ZIP)
0 downloads
-
Source code (TAR.GZ)
0 downloads
-
ludic-0.13.2-linux-x86_64.tar.gz
0 downloads · 938 KiB
-
ludic-0.13.2-linux-x86_64.tar.gz.sha256
0 downloads · 99 B
-
ludic-0.13.2-src.tar.gz
0 downloads · 2.7 MiB
-
ludic-0.13.2-src.tar.gz.sha256
0 downloads · 90 B
-
-
v0.7.0
Pre-releasereleased this
2026-09-10 02:31:41 +02:00 | 975 commits to main since this releasev0.7.0 — 2026-09-10
Features
-
A game can use the renderer from outside this repository.
ludic.render3dreads two
things from disk at run time — its GLSL, and the scanned CC0 materials — and both were
found only by a path relative to the working directory, so the renderer worked in a
Ludic checkout and nowhere else. A game living in its own repository now needs to copy
neither.The shaders come from the package, wherever the package is. They belong to
ludic.render3dand ship with it, so the renderer looks for them beside the project
first (a Ludic checkout, where they are underpackages/) and then under the install
root,$LUDIC_HOME/packages/ludic.render3d— the same place the compiler already
resolvesimport "ludic.render3d/r3d.ludic"from. Nothing to vendor, and no version of
the shaders that can drift from the version of the code that compiles them.ludic assets [--force]fetches the scanned materials and the HDRI sky into
assets/polyhaven/of whatever project you run it in. The list of what to fetch is the
renderer's own — the renderer decides which materials it wants — so it moved out of the
repository'sassets/and into the package as
packages/ludic.render3d/assets.manifest, where it ships with the toolchain. A game
does not keep its own copy of that list and so cannot fall out of step with the
renderer's material set.ludic dev fetch-assetsis the same command from a checkout.A URL-shaped module built its binary into directories.
project_nametook
everything after the last dot of the manifest's module path, which for a package
identified the way the package manager identifies them —
git.host.io/user/name— is inside the host: the build wrote
build/io/user/nameinstead ofbuild/name. The last path segment comes first now, and
a dot inside that segment still separates namespace from package, soludic.render3d
still builds asrender3d.The camping game has moved out to its own repository —
Maroon Lake — taking
examples/rendering/valley.ludic,hiker.ludic,camp/, and 175 MB of survey data and
scanned kit with it. It was here as a demo of the renderer and became a game, and an
engine repository should not be carrying a game's assets. It is now the first consumer
of everything above, which is the point: what the renderer needs a game to be able to do,
it can now do from outside.examples/rendering/smooth.ludicstays as the renderer's
example in this tree. -
OpenGL for Ludic, and a 3D renderer on it.
Gl.*binds the whole OpenGL 4.1
core API — everygl*entry point of the platformgl3.hasGl.<snake_name>(…)
with everyGL_*constant, generated byludic-dev glgenwith per-call ABI thunks
(runtime/native/gl_thunks.ll; float/double parameters takefixed). Windowed
builds get anNSOpenGLContexton the existing window at Retina resolution
(cocoa.ll); headless builds render into an offscreen CGL context, so a program
that usesGl.*renders and screenshots identically under the test harness.
Gl.open / swap / screenshot / program / vao / floats …cover the glue, and the
IEEE-float helpers (f_add,mem_put_f32, …) let Q16.16 programs fill real
float vertex and uniform data.Gl.*linksgl.ll + gl_thunks.ll + OpenGL.framework
only when used; every other build is byte-identical.The
ludic.render3dpackage (packages/ludic.render3d) is a physically based
renderer written onGl.*: HDRI sky with image-based lighting (irradiance, GGX
prefiltered, split-sum BRDF, sun extracted from the map), GPU-generated terrain
with scanned PBR materials (stochastic anti-tiling, triplanar rock, slope/altitude
splatting), cascaded shadow maps with PCF and world-unit biasing, a glTF loader
for scanned models, instanced vegetation with baked impostors, procedural grass
and lupines with wind and translucency, 4x MSAA with alpha-to-coverage, SSAO,
still water, cloud shadows, aerial perspective, an HDR pipeline with bloom,
auto-exposure, ACES tonemapping, grading, sharpening and grain. See
examples/rendering/smooth.ludic, and the Maroon Lake game (git.workshopsoft.io/workshopsoft/maroon-lake) for a
game built on it. The renderer's CC0 materials are fetched withludic assets.16-bit PNGs: the renderer's texture loader keeps 16-bit samples (normal /
displacement maps) and uploads them asRGB16/R16.
Fixes
-
Resizing the window (or entering fullscreen) no longer empties the world. It left
gl error 1286—GL_INVALID_FRAMEBUFFER_OPERATION— on every frame from there on, with
the terrain, the trees, the grass and the water gone and only the sky drawn.The sun-visibility pass added in
changes/terrain-perf.mdborrows the depth buffer the
frame is about to be drawn with, so that rasterising it doubles as a depth prepass. It
was storing that borrowed texture in itsTarget, and aTargetdeletes whatever its
depthnames when it is freed. On the first resize the sequence was:post_freedeletes
the frame's depth texture,post_initimmediately makes the replacement — and GL hands
back the name that was just freed — and then the visibility target, rebuilt for the new
size, deleted that name believing it was its own. The scene framebuffer lost its depth
attachment. What is left is a colour-only framebuffer, which is complete, so drawing
carried on with no depth test at all: the sky is a fullscreen quad drawn last, and with
nothing left to fail against it painted over the entire valley. The 1286s came from the
passes whose own attachment now named a texture that no longer existed.A borrowed attachment is never written into the target now, and the frame's depth is
attached afresh at the start of each pass — it is a different texture every time the
screen-sized buffers are rebuilt, and oneglFramebufferTexture2Dper pass is cheaper
than any scheme for noticing that it changed.R3D_RESIZE_AT=<frame>rebuilds every screen-sized buffer from that frame on, cycling
through four drawable sizes every few frames. A window cannot be resized in a headless
run, so this is the only way to reach the path; it reproduced the fault in one frame and
now runs twenty resizes, with the fly camera and with the game, without an error. -
An upgrade keeps the package store. The installer replaces the whole install
root, andludic addcaches packages in~/.ludic/store— so re-running the
one-liner deleted every package a project had fetched. The store is carried
across now; everything else in the root belongs to the toolchain and is replaced.
Performance
-
Ground cover stops re-growing itself. Walking a streamed world hitched, and the
hitch got worse the longer you played. Measured in the Maroon Lake game, with a new hitch
report rather than guessed at.The chunk cache had a cliff, not a slope. A stream cached 4096 chunks and then
stopped remembering: past that the chunk was generated, used for one frame and thrown
away, so every ring walk regenerated it, for the rest of the session. It arrives after
enough of the map has been walked — six evictions' worth over seven kilometres, so an
ordinary session reaches it — and it is the point where cover starts visibly re-growing
as you turn.stream_evictnow drops the half of the cache nobody has asked for in the
longest time (chunks carry the walk that last wanted them) and rebuilds the index over
what is left. Over a 7 km traversal: generation total 9073 ms → 2230 ms, the worst
single frame's generation 11.4 ms → 3.1 ms, median frame 10.8 → 9.0 ms. With a cache
deliberately sized to saturate early, the same run goes from 2748 frames generating to
1548, and from a 13.3 ms median to 9.2.R3D_NOEVICTrestores the old behaviour for
comparison,R3D_STREAM_CAP=<n>sets the cache size.The other half of that hitch was in the game's own cover generator, and went with it to
the Maroon Lake game's repository: its candidates were paying for a second noise field, four
height samples and a path distance before the drift field that rules out most of the
meadow — 2341 µs → 518 µs per chunk, bit-identical output. Worth repeating in any
generator: astream_fillis called for tens of thousands of candidates per chunk, so
the order of its tests is most of its cost.The hitch report (
R3D_PROF=1) is what found both. It prints the slowest frames of
the run with what was in each: CPU versus GPU wait, cover generated, instance bytes
uploaded, the game's own tick, and the renderer phase that took longest. Alongside it,
per-chunk generation cost by stream and band, a census of what the caches hold, and a
stutter figure — the frame time a run spent beyond 1.2x its own median — because a mean
cannot show a hitch and a maximum is one unlucky frame.It also found two content bugs in the game it was measured on, which is the report doing
its job: a cover stream whose placement rule never fires still pays full generation cost,
and the census makes that visible — 3364 cached chunks holding zero instances. -
The ground costs half what it did. Measured in the Maroon Lake game, the terrain was 10.3 ms of
a 22.2 ms frame; it is now 5.4 ms of 16.4 ms — 45 fps to 61 fps at 1080p, with the
frame otherwise unchanged (every viewpoint tested stays above 54 dB PSNR against the
old renderer, with no channel differing by more than 7/255).Measure by frame time, not by the pass timers.
R3D_PROF's per-pass
GL_TIME_ELAPSEDqueries cannot be trusted on this driver: with the ground's shading
work removed the terrain query fell from 10.5 ms to 1.3 ms while the frame time did
not move at all. Every number above and below is a median real frame time, taken by
switching one thing off (prof_ft_report); the pass timers are still printed, and are
still useful for spotting a pass that appears out of nowhere, but they cannot size one.
R3D_NOTERRAINskips the ground,R3D_TNEARONLY/R3D_TFARONLYdraw every patch
with one tier's program, andR3D_RES=<w>x<h>renders at another size — the three
switches that say whether a cost is the ground, which tier it is in, and whether it is
pixels at all.Each detail tier is its own program.
terrain.fragholds a detailed near tier and
a cheap far one and chose between them per pixel, so every pixel of the valley walls
was compiled — and scheduled — for a near path it never ran. CDLOD selection now knows
which tiers a patch can contain: one that never comes within the split draws with
FAR_ONLY, one wholly inside it withNEAR_ONLY, and only the ring of patches that
straddle the band needs the program that holds both and cross-fades. Pixel-identical,
and it makes the tiers separately measurable: the near tier costs 7.5 ms over a whole
frame, the far tier 2.3 ms.The sun visibility is its own pass (
tersun.frag). The same CDLOD patches are
rasterised once into a screen-sized R8 buffer that holds nothing but each ground
pixel's sun visibility, andterrain.fragfetches it by fragment coordinate. The pass
costs 0.27 ms, shares the frame's depth buffer so it doubles as a depth prepass, and
takes the cascade read out of the shader that covers the screen. It picks its tier —
filtered PCF near, a single tap far — over the same cross-faded band the ground uses,
so the boundary is not a contour you can find on the hillside.Nothing is sampled for a weight of zero. The ground sampled all four of its
materials for every pixel and then blended three of them at zero: a meadow pixel took
nine taps of triplanar rock, a cliff pixel nine taps of stochastic grass, and every
pixel in the valley took the snow tile and the four noise fields behind the lake's
shore wash — a wash that is a hairline along one shore within 120 m of the camera. The
survey photograph's classification now runs first, because it is what decides which
materials are present; each material block sits behind its own weight; the ridge field
that ragged the snow line is skipped 160 m below it, where it cannot change anything;
and inside the stochastic blend a cell's rotation, offset and rotated gradients are
computed inside its own test, so a cell whose sharpened weight rounds away costs
nothing. All of it exact where the weight is zero, and it is most of the win.Material sampling is what remains (2.8 ms of the 5.4): scanned 2K tiles taken at 16x
anisotropy on ground seen at a grazing angle.R3D_ANISO=<n>sets the filter (the
default is unchanged at 16; 4 is worth 1.0 ms and 1 is worth 1.7 ms).The shadow pass is 1.2 ms of the frame and has nothing to give: re-using the far
cascades between frames — their windows are snapped to a 14 m and a 64 m grid — is
worth 0.15 ms standing still and nothing while walking, so it is not in the tree.
Downloads
-
Source code (ZIP)
0 downloads
-
Source code (TAR.GZ)
0 downloads
-
ludic-0.7.0-linux-x86_64.tar.gz
0 downloads · 759 KiB
-
ludic-0.7.0-linux-x86_64.tar.gz.sha256
0 downloads · 98 B
-
ludic-0.7.0-src.tar.gz
0 downloads · 2 MiB
-
ludic-0.7.0-src.tar.gz.sha256
0 downloads · 89 B
-
-
v0.6.1
Pre-releaseAll checks were successfulreleased this
2026-09-05 22:29:14 +02:00 | 978 commits to main since this releasev0.6.1 — 2026-09-05
Fixes
-
ludic newscaffolded a project that would not compile. The project name went
straight into theprogram <Name>identifier, soludic new my-gamewrote
program My-Game— a subtraction — and the firstludic runfailed with
expected '{', got '-'. A name is now turned into a valid identifier
(my-game→MyGame,2048→Game2048), and a name that cannot be a
directory or a package is refused with the rule rather than mangled.Found while auditing every command's flags and arguments, along with:
- Unknown options are errors.
ludic build --headlessilently built a
windowed binary;ludic build -owith no path silently ignored it. Both now
say what is wrong and exit non-zero. ludic fmtformats in place, as its help always claimed — it was printing
the file to stdout and changing nothing.ludic fmt --checkreports drift
without writing, for a hook or CI.ludic test nosuch.ludicsays the file does not exist instead of passing it
to the compiler.ludic build-libreported failures as a shell syntax error (an
interpolation written in a non-interpolating string), and its no-argument
auto-detection picked uppackage.lock.ludicas a module to compile.- Error messages that still began with
x:— the CLI's old name — now say
ludic:.
- Unknown options are errors.
Downloads
-
Source code (ZIP)
0 downloads
-
Source code (TAR.GZ)
0 downloads
-
ludic-0.6.1-darwin-arm64.tar.gz
1 download · 566 KiB
-
ludic-0.6.1-darwin-arm64.tar.gz.sha256
1 download · 98 B
-
ludic-0.6.1-linux-x86_64.tar.gz
0 downloads · 604 KiB
-
ludic-0.6.1-linux-x86_64.tar.gz.sha256
0 downloads · 98 B
-
ludic-0.6.1-src.tar.gz
0 downloads · 1.8 MiB
-
ludic-0.6.1-src.tar.gz.sha256
0 downloads · 89 B
-
-
v0.6.0
Pre-releaseAll checks were successfulreleased this
2026-09-05 22:15:22 +02:00 | 980 commits to main since this releasev0.6.0 — 2026-09-05
Refactoring
-
ludicis only the language's command line now. The toolchain's own tasks —
building the compiler from its IR seed, the regression suites, the docs site,
releases — moved out of it into a separateludic-devbinary that is built from
a checkout and is not part of an install.ludic helpis what a user can actually do:new,run,build,test,
add,fmt,lsp,doctor,upgrade. No section about a repository they do
not have. Typingludic dev …says where those tasks went rather than failing
as an unknown command.ludic dev <task>becomesludic-dev <task>for contributors; every task
is otherwise unchanged. The bootstrap is now
bin/ludicc tools/ludic-cli/dev.ludic -o bin/ludic-dev && bin/ludic-dev build.- The shipped binary drops from ~880 KB to ~190 KB, since none of the release,
docs-generation or bootstrap machinery is linked into it any more.
CI
- The docs deploy triggers on a change to
install.sh. The site publishes the
installer, but the workflow's path filter did not mention it — so a release that
only fixedinstall.shleft the old script live at the URL the landing page
tells people to pipe intosh.
Downloads
-
Source code (ZIP)
0 downloads
-
Source code (TAR.GZ)
0 downloads
-
ludic-0.6.0-darwin-arm64.tar.gz
0 downloads · 566 KiB
-
ludic-0.6.0-darwin-arm64.tar.gz.sha256
0 downloads · 98 B
-
ludic-0.6.0-linux-x86_64.tar.gz
0 downloads · 601 KiB
-
ludic-0.6.0-linux-x86_64.tar.gz.sha256
0 downloads · 98 B
-
ludic-0.6.0-src.tar.gz
0 downloads · 1.8 MiB
-
ludic-0.6.0-src.tar.gz.sha256
0 downloads · 89 B
-
-
v0.5.2
Pre-releasereleased this
2026-09-05 21:33:35 +02:00 | 983 commits to main since this releasev0.5.2 — 2026-09-05
Fixes
- The installer also covers a non-login interactive
bash— the shell most Linux
terminal emulators start, which reads only~/.bashrc. That file is now created
when it is missing (its presence changes nothing else about how bash starts),
while~/.bash_profileis still only appended to when it already exists, since
creating that one would stop bash reading~/.profile.
Downloads
-
Source code (ZIP)
0 downloads
-
Source code (TAR.GZ)
0 downloads
-
ludic-0.5.2-darwin-arm64.tar.gz
3 downloads · 792 KiB
-
ludic-0.5.2-darwin-arm64.tar.gz.sha256
3 downloads · 98 B
-
ludic-0.5.2-linux-x86_64.tar.gz
0 downloads · 874 KiB
-
ludic-0.5.2-linux-x86_64.tar.gz.sha256
0 downloads · 98 B
-
ludic-0.5.2-src.tar.gz
0 downloads · 1.8 MiB
-
ludic-0.5.2-src.tar.gz.sha256
0 downloads · 89 B
- The installer also covers a non-login interactive