Compare commits
No commits in common. "main" and "v0.7.0" have entirely different histories.
2169 changed files with 91400 additions and 778166 deletions
|
|
@ -35,7 +35,7 @@ jobs:
|
|||
git checkout "${GITHUB_SHA}" 2>/dev/null || git checkout "${GITHUB_REF_NAME:-main}"
|
||||
git log --oneline -1
|
||||
# See ci.yml for why the Linux build injects the stdio shim via LUDIC_CC.
|
||||
echo "LUDIC_CC=clang-16 $(pwd)/tools/ci/linux_stdio_shim.ll -lm" >> "$GITHUB_ENV"
|
||||
echo "LUDIC_CC=clang-16 $(pwd)/tools/ci/linux_stdio_shim.ll" >> "$GITHUB_ENV"
|
||||
echo "LUDIC_HOME=$(pwd)" >> "$GITHUB_ENV"
|
||||
|
||||
- name: Bootstrap x from the seed
|
||||
|
|
|
|||
|
|
@ -48,7 +48,7 @@ jobs:
|
|||
# LUDIC_CC so every clang invocation — the seed bootstrap, `ludic-dev build`,
|
||||
# and each compiled test program — picks it up. Absolute path so it
|
||||
# still resolves if a step changes directory.
|
||||
echo "LUDIC_CC=clang-16 $(pwd)/tools/ci/linux_stdio_shim.ll -lm" >> "$GITHUB_ENV"
|
||||
echo "LUDIC_CC=clang-16 $(pwd)/tools/ci/linux_stdio_shim.ll" >> "$GITHUB_ENV"
|
||||
echo "LUDIC_HOME=$(pwd)" >> "$GITHUB_ENV"
|
||||
|
||||
- name: Bootstrap the toolchain from the IR seed (clang only)
|
||||
|
|
|
|||
|
|
@ -63,7 +63,7 @@ jobs:
|
|||
# tiny C-free IR shim supplying the Darwin stdout/stderr globals over
|
||||
# glibc's, injected through LUDIC_CC. docs-gen is a pure CLI (no
|
||||
# windowing), so the C-free bootstrap is all it needs.
|
||||
export LUDIC_CC="clang-16 $(pwd)/tools/ci/linux_stdio_shim.ll -lm"
|
||||
export LUDIC_CC="clang-16 $(pwd)/tools/ci/linux_stdio_shim.ll"
|
||||
export LUDIC_HOME="$(pwd)"
|
||||
mkdir -p bin
|
||||
clang-16 tools/ci/linux_stdio_shim.ll selfhost/ludicc.seed.ll -o bin/ludicc
|
||||
|
|
|
|||
|
|
@ -49,7 +49,7 @@ jobs:
|
|||
git checkout "$TAG"
|
||||
echo "TAG=$TAG" >> "$GITHUB_ENV"
|
||||
# See ci.yml for why the Linux build injects the stdio shim via LUDIC_CC.
|
||||
echo "LUDIC_CC=clang-16 $(pwd)/tools/ci/linux_stdio_shim.ll -lm" >> "$GITHUB_ENV"
|
||||
echo "LUDIC_CC=clang-16 $(pwd)/tools/ci/linux_stdio_shim.ll" >> "$GITHUB_ENV"
|
||||
echo "LUDIC_HOME=$(pwd)" >> "$GITHUB_ENV"
|
||||
|
||||
- name: The tag, VERSION and CHANGELOG must agree
|
||||
|
|
|
|||
1
.gitattributes
vendored
1
.gitattributes
vendored
|
|
@ -1 +0,0 @@
|
|||
packages/*/lib/** filter=lfs diff=lfs merge=lfs -text
|
||||
6
.gitignore
vendored
6
.gitignore
vendored
|
|
@ -26,7 +26,6 @@ tools/editors/vscode/node_modules/
|
|||
tools/editors/vscode/*.vsix
|
||||
tools/editors/jetbrains/.gradle/
|
||||
tools/editors/jetbrains/build/
|
||||
tools/editors/jetbrains/.kotlin/
|
||||
|
||||
# IntelliJ plugin SDK sandbox (tools/editors/jetbrains)
|
||||
.intellijPlatform/
|
||||
|
|
@ -55,8 +54,3 @@ __pycache__/
|
|||
assets/polyhaven/hdri/
|
||||
assets/polyhaven/textures/
|
||||
assets/polyhaven/models/
|
||||
# `ludic run` beside an example writes its binary into a build/ there
|
||||
examples/**/build/
|
||||
|
||||
# a package native/build.sh writes its objects under the package (phase 15)
|
||||
packages/*/build/
|
||||
|
|
|
|||
684
CHANGELOG.md
684
CHANGELOG.md
|
|
@ -7,690 +7,6 @@ change type. Preview the next one with `ludic dev release --dry-run`.
|
|||
A released section may carry a hand-written summary paragraph above its groups
|
||||
(v0.2.0 has one); the generated bullets below it are not edited by hand.
|
||||
|
||||
## v0.22.0 — 2026-09-18
|
||||
|
||||
### Features
|
||||
|
||||
- **Every key on the keyboard is bindable** — the F-row, Home/End/PageUp/PageDown,
|
||||
Insert/Delete, Caps Lock and the numpad reach a game as codes 132-166.
|
||||
|
||||
Until now the platform gave them no code at all, so a game's rebinding screen could
|
||||
not take one and nothing said why. Windows asked the active layout what they type and
|
||||
got nothing back (`w_vk_char` answers 0 for a key with no character); macOS let them
|
||||
fall through to `charactersIgnoringModifiers`, which reports `NSF1FunctionKey` and its
|
||||
neighbours at 0xF704 and up - outside the 256-bit held set either way.
|
||||
|
||||
- **The codes are the same on both platforms**: 132-143 are F1-F12, 144-149 are Home,
|
||||
End, PageUp, PageDown, Insert and Delete, 150 is Caps Lock, 152-161 are the numpad
|
||||
digits and 162-166 its `*`, `+`, `-`, `.` and `/`. The numpad's Enter is Enter.
|
||||
- **`Key.F1`, `Key.Home`, `Key.Numpad0`** and the rest fold at compile time like the
|
||||
other named keys.
|
||||
- **`Input.key_label`** names them - "F1", "Home", "CapsLock", "Num 0", "Num /" - and
|
||||
does *not* ask the layout, because a key that types nothing is called the same thing
|
||||
on every layout.
|
||||
## v0.21.1 — 2026-09-17
|
||||
|
||||
### Fixes
|
||||
|
||||
- A windowed game opens under its package's `app name`. `ludic build`, `ludic run` and
|
||||
`ludic bundle` pass it to the compiler (`ludicc --title <name>`), so `game_title()` - the
|
||||
window's first title - is the name the player knows rather than the `program` name, which
|
||||
showed for up to two seconds before the renderer retitled the window. Without an `app name`
|
||||
it is the `program` name, as before.
|
||||
- `Crypto.random_bytes`, `Crypto.random_hex`, `Crypto.random_u32` and `Uuid.*` draw from the
|
||||
system CSPRNG on Windows (`RtlGenRandom`, advapi32). They read `/dev/urandom`, which Windows
|
||||
does not have, and every byte came back zero.
|
||||
## v0.21.0 — 2026-09-17
|
||||
|
||||
### Features
|
||||
|
||||
- **A launcher's runtime** — start a program, step aside while it runs, and download large
|
||||
files with a progress bar.
|
||||
|
||||
- **`Process.*`** — `Process.spawn(path, args)` starts a program directly (no shell) with this
|
||||
process's environment and working directory and returns at once; `Process.poll(h)` is `-1`
|
||||
while it runs, then its exit code (`128 + signal` for a signal death, `137` after
|
||||
`Process.kill`); `Process.free` lets a handle go. `posix_spawn` on macOS
|
||||
(`runtime/native/process.ll`); `CreateProcessW` on Windows (`process_win.ll`), with each
|
||||
argument quoted by the MSVC rules and no console window. Linked only into a program that
|
||||
uses `Process.*`.
|
||||
- **`Http.save_to(h, path)`** — before `Http.send`, streams the response body straight into a
|
||||
file (written at `path` itself, created on send) instead of memory; `Http.text` of such a
|
||||
request is empty and `Http.body_len` is the bytes written. **`Http.received(h)`** and
|
||||
**`Http.expected(h)`** (the `Content-Length`, or `-1`) are live while it is pending. macOS
|
||||
streams through an `NSURLSession` with a delegate built at run time, Windows through the
|
||||
WinHTTP read loop. Freeing a pending request now cancels it and parks its slot until the
|
||||
worker has let go, rather than freeing what the worker is still writing.
|
||||
- **`App.window_hide()` / `App.window_show()`** — take the game's window off the screen and
|
||||
bring it back without closing it or its GL / Vulkan surface; the run goes on while it is
|
||||
hidden. No-ops headless and before there is a window.
|
||||
|
||||
### Fixes
|
||||
|
||||
- A game's window takes the title it asks for. The runtime opens a game's window before `main`,
|
||||
titled with the `program` name, and `Gl.open` / render3d's `gl_open` and `gvk_open` attached
|
||||
to it without passing their title on, so a `program Valley` that opened "Maroon Lake" showed
|
||||
"Valley". `win_set_title` (cocoa.ll `setTitle:`, win32.ll `SetWindowTextW`, UTF-8 to UTF-16)
|
||||
now retitles it, and a second `win_open` on macOS retitles the open window instead of making
|
||||
another, as it already did on Windows.
|
||||
## v0.20.0 — 2026-09-17
|
||||
|
||||
### Features
|
||||
|
||||
- The JetBrains plugin (1.6.0) has a Ludic tool window for the package, like the Gradle one:
|
||||
- an overview of the package (what it is, entry, version, dependencies, scripts, hooks, app);
|
||||
- its commands, scripts and hooks, run by double-click, with each command's hooks shown;
|
||||
- its dependencies against package.lock.ludic: the locked version, whether it is fetched
|
||||
and linked, the indirect ones, and Fetch, Update, Verify, Vendor, Add and Remove;
|
||||
- the app preview the `app` lines describe, and the asset roots with missing ones marked.
|
||||
The bar over package.ludic now appears only when something needs doing (fetch, or no
|
||||
toolchain), instead of carrying every command.
|
||||
## v0.19.0 — 2026-09-17
|
||||
|
||||
### Features
|
||||
|
||||
- `Udp.*`: polled IPv4 datagrams - the transport under a game's own netcode. `Udp.open(port)`
|
||||
binds a non-blocking socket (port 0 picks one, `Udp.port` says which), `Udp.send` sends one
|
||||
datagram to an address and port, and `Udp.recv` drains what arrived without ever blocking;
|
||||
`Udp.from_ip` / `Udp.from_port` name the sender. `Udp.ip` / `Udp.ip_text` convert addresses,
|
||||
`Udp.resolve` looks a host name up and `Udp.local_ip` is the address a player on the same
|
||||
network would dial. BSD sockets on macOS (`runtime/native/udp.ll`), Winsock on Windows
|
||||
(`udp_win.ll`, linked with ws2_32), linked only into a program that uses `Udp.*`.
|
||||
## v0.18.1 — 2026-09-16
|
||||
|
||||
### Fixes
|
||||
|
||||
- The release build links libm on Linux, which `float`/`double` code needs (glibc keeps
|
||||
`sinf`, `sqrtf` and the rest there). 0.18.0's release job stopped on this before
|
||||
publishing, so 0.18.1 is the first published build with float types, barrel imports,
|
||||
package scripts and hooks, and the JetBrains 1.5.0 support.
|
||||
## v0.18.0 — 2026-09-16
|
||||
|
||||
### Features
|
||||
|
||||
- Run a single test by name: a compiled test program takes the test's exact name as its
|
||||
first argument (`./tests "adds"`), and `ludic test` gains `--test NAME` to pass it through.
|
||||
A name that matches no test is reported and fails the run. `ludic test --verbose` (`-v`)
|
||||
prints every test's `ok`/`FAIL` line, framed by `RUN <file>` … `PASS|FAIL <file>`, which is
|
||||
what editor test views parse.
|
||||
- The JetBrains plugin covers much more of the IDE:
|
||||
- Run configurations for `ludic run`/`build`/`test` (plus any other `ludic` command), with
|
||||
gutter run buttons on `program` and on every `test "name"` block.
|
||||
- Test results appear in the IDE's test tree, with navigation back to each test block.
|
||||
- `file.ludic:line` locations are clickable in every console.
|
||||
- Smart indentation on Enter and when typing a closing bracket; quotes pair up.
|
||||
- A Color Scheme page and a Code Style page.
|
||||
- Live templates, New Ludic File templates, and a Ludic project in File | New | Project.
|
||||
- A Tools | Ludic menu, and a warning when the toolchain is missing.
|
||||
- `package.ludic` is treated as a package manifest:
|
||||
- a banner offers Fetch, Update, Verify, Run, Test, Build and Bundle, and warns when the lock is
|
||||
missing or older than the manifest;
|
||||
- gutter actions on `package` and `require` lines;
|
||||
- completion and quick documentation for directives and `app` keys;
|
||||
- its own icon.
|
||||
- `script` and `hook` lines complete and are validated; each script has a run button, the
|
||||
package banner has script shortcuts, and Tools | Ludic | Run Script… lists them all.
|
||||
- The toolchain's and the project's packages are listed under External Libraries.
|
||||
- The Structure view, parameter info and code-block navigation now come from `ludic-lsp`.
|
||||
- The plugin is now 1.5.0 and requires IntelliJ Platform 2024.2 or newer and LSP4IJ 0.21.
|
||||
- Fixed: Enter no longer inserts an extra `}`, and Comment Line no longer writes two spaces.
|
||||
- `float` and `double`: IEEE floating point with ordinary operators, so renderer math reads
|
||||
`a * b + c` instead of `f_add(f_mul(a, b), c)`.
|
||||
- Decimal literals take their type from context (`let s: float = 0.1` is exactly 0.1), and
|
||||
stay `fixed` elsewhere, so existing programs keep their meaning.
|
||||
- `int`/`long` promote; `float(x)`, `double(x)`, `int(x)`, `long(x)` and `fixed(x)` convert.
|
||||
- `floats(n)` / `doubles(n)` buffers; float fields, globals, constants and parameters.
|
||||
- `Math.*` computes in float when given one; `string`/`print`/interpolation write the
|
||||
shortest round-tripping decimal; `float_bits` / `float_from_bits` expose the IEEE pattern.
|
||||
- `@deterministic` code may not use floats (it is now checked).
|
||||
- `import "camp"` imports a directory through its barrel, `camp/index.ludic` (a fragment
|
||||
listing the directory's own imports). A directory without one is a clear error. Packages
|
||||
resolve the same way: `import "ludic.render3d"` reads `ludic.render3d/index.ludic`.
|
||||
- `ludic-lsp` navigates like the compiler resolves:
|
||||
- Package imports (`import "ludic.render3d/r3d.ludic"`) resolve from `ludic_modules/` and the
|
||||
toolchain's `packages/`, so their names hover, complete and jump. Import strings are links.
|
||||
- `var` locals are tracked (declaration, usages, rename).
|
||||
- Hover shows the declared or inferred type of locals, parameters and globals.
|
||||
- Hover and go-to-definition on built-ins, built-in types and namespaces show the reference
|
||||
page, with a link to the online docs. Release archives now ship `docs/language` for this.
|
||||
- Go to type declaration (`textDocument/typeDefinition`) is supported.
|
||||
- Signature help carries per-parameter ranges and follows named arguments.
|
||||
- Completion inside a call offers the parameter names not given yet, `Namespace.` offers its
|
||||
members, and results are ranked: locals first, then globals, built-ins and keywords.
|
||||
- `package.ludic` gains `entry`, scripts and lifecycle hooks:
|
||||
- `entry "src/game.ludic"` names the program `ludic run`/`build`/`bundle` compile. Without
|
||||
it, the one file under `src/` that declares a `program` is used.
|
||||
- `script "dev" "ludic run --headless"` defines a command: `ludic dev` (or `ludic script dev`)
|
||||
runs it with any extra arguments, and `ludic scripts` lists them.
|
||||
- `hook before|after <command> "…"` wraps `build`, `run`, `test`, `bundle`, `pack`, `get`, …
|
||||
or a script. A failing `before` hook stops the command; `after` hooks run only on success.
|
||||
- Quoted manifest values accept `\"` and `\\` escapes.
|
||||
|
||||
### Fixes
|
||||
|
||||
- **`==` / `!=` on records is identity again** — only two strings compare by content.
|
||||
|
||||
Every pointer-typed comparison used to be lowered to a C-string content compare,
|
||||
so two distinct records (properties, slices, enums) compared their bytes up to
|
||||
the first zero byte: `a == b` could be true for different objects that shared a
|
||||
leading field, depending on layout. References now compare by identity
|
||||
(`icmp eq ptr`); `string` (and untyped `pointer`/`pointers` text) still compares by content,
|
||||
and comparing a `string` with a non-string reference is a compile error.
|
||||
## v0.17.0 — 2026-09-16
|
||||
|
||||
### Features
|
||||
|
||||
- **Renderer switches stay out of shipped games** — every `R3D_*` environment switch in
|
||||
`ludic.render3d` (debug views, feature kills, file writers, hardware fakes, the Windows
|
||||
feature overrides) now goes through one gate: a headless build honours them as before, a
|
||||
windowed build only when `R3D_DEV` is set to anything but `0`. See `docs/SHIPPING.md`.
|
||||
|
||||
### Fixes
|
||||
|
||||
- **A lake can be the water that mirrors the world** — a reflecting body is clipped to its
|
||||
ellipse like any other unless it is an unbounded sea, so a map whose reflection belongs to
|
||||
its lake no longer floods every hollow in the survey with that lake's level; and the
|
||||
terrain's wet shore, forest and scree gates read the carved lake's line inside its outline
|
||||
(`u_lake`), the rule the grass already used, so a lake above the sea keeps its wet bank and
|
||||
no forest is painted down its bed.
|
||||
|
||||
`terrain_lake_carve(false)` keeps a lake's line, outline and shore but skips carving its
|
||||
bed, for a height map that already carries a shaped one.
|
||||
## v0.16.2 — 2026-09-15
|
||||
|
||||
### Fixes
|
||||
|
||||
- **Keys are physical positions on Windows and macOS, whatever the layout** - `Input.key_down('w')`
|
||||
is the key above S on every keyboard.
|
||||
|
||||
- **Windows** keyed the held set by the character the active layout gave a virtual key, so
|
||||
a game's WASD belonged to whatever the layout put there: on AZERTY W and A were other
|
||||
keys, and with an input method on (Chinese, Japanese, Korean) every letter arrived as
|
||||
VK_PROCESSKEY and no letter key worked at all. The typing block is now read from the
|
||||
scancode; the arrows, the numpad and the F-keys still go by virtual key, and the frame
|
||||
key (`Input.key`, typing) keeps the layout's character.
|
||||
- **macOS** did the same through `charactersIgnoringModifiers`; it now reads `keyCode`.
|
||||
- **`Input.key_label(key)`** names a key for the player in their own layout - `'w'` shows
|
||||
as "W" on QWERTY and "Z" on AZERTY - so a game that shows its bindings never shows a
|
||||
key the player cannot find. Windows reads the layout; headless, macOS and the browser
|
||||
give the US character.
|
||||
## v0.16.1 — 2026-09-15
|
||||
|
||||
### Features
|
||||
|
||||
- **HDR calibration, and two HDR fixes** — the HDR10 picture follows the player's display instead of one fixed curve.
|
||||
|
||||
- **`r3d_hdr_calibrate(peak, paper, black)`** — nits as float bits: the brightest the display shows (100-10000),
|
||||
where the picture's and the interface's white sit (80-1000, never above the peak) and how far the darkest shade is
|
||||
lifted (0-5, fading out by paper white). The tonemap's and the overlay's HDR10 variants read them, and the display's
|
||||
HDR metadata is sent again with the new peak. The defaults are the old constants: 1000, 200 and 0.
|
||||
- **`ov_hdr_nits(nits)` / `ov_hdr_paper()`** — what the overlay draws next is at that many nits on an HDR10 frame,
|
||||
for a calibration screen's test patches; it closes the batch so far. No effect on an SDR frame.
|
||||
- **Fixed: a crash toggling HDR on the Vulkan renderer.** `gpu_caps_probe()` made and destroyed a second Vulkan
|
||||
instance under NVIDIA Streamline's interposer; the next swapchain rebuild then called through a pointer that
|
||||
instance had left behind, at address 0. With the Vulkan renderer running, the probe asks its instance instead.
|
||||
- **Fixed: yellow read as red in HDR.** The overlay drew sRGB values straight into the HDR10 swapchain (it now has an
|
||||
HDR10 variant, chosen while `gpu_hdr_active()`), and the tonemap extended highlights per channel, which boosted a
|
||||
bright yellow's red far more than its green; it now brightens the graded colour by one factor.
|
||||
## v0.16.0 — 2026-09-15
|
||||
|
||||
### Features
|
||||
|
||||
- **A loading screen can show the renderer's start-up as it happens.** `r3d_open(w, h, title)` opens
|
||||
the window and the graphics backend with nothing baked, and `r3d_load_count()` / `r3d_load_step(i)`
|
||||
run the rest - the sky and its light, the terrain, the shadow and screen targets, the scattered cover
|
||||
and actors, the grass - one step at a time, so a game can draw and present a frame between them.
|
||||
`r3d_init` is those same calls in a row and is unchanged for everything that uses it.
|
||||
- **HDR10 output on the Vulkan renderer** — `r3d_hdr(on)` asks for an ST 2084 / BT.2020 swapchain where the
|
||||
display offers one.
|
||||
|
||||
The tonemap's HDR10 variant keeps the SDR picture up to a 200-nit paper white and rolls highlights on to
|
||||
1000 nits; the overlay converts the interface to the same white; HDR metadata is set where the loader has
|
||||
the command. The screen and LDR images are 10-bit while it is on, and screenshots refuse rather than write
|
||||
a PQ image as SDR. OpenGL and the Vulkan SDR frame are unchanged. `R3D_HDR` overrides the setting.
|
||||
- **Mesh-shader grass** — on a card with `VK_EXT_mesh_shader`, `r3d_mesh_grass(on)` draws each grass tile as one
|
||||
mesh-shader dispatch sized to that tile's blades, and a blade that is culled emits no vertices at all.
|
||||
|
||||
- **`ludic-dev shaders`** — a variant whose first file is `*.mesh` compiles that stage as a Vulkan 1.3 mesh
|
||||
shader; its SPIR-V keeps the `.vert` name, so the manifest is unchanged.
|
||||
- **The seam** — `gpu_has_mesh()` and `gpu_draw_mesh_tasks(x, y, z)`; a mesh program's pipeline has no
|
||||
vertex input and its bindings the mesh stage. The device enables `meshShader` and `maintenance4`.
|
||||
|
||||
Not yet faster: at 4K on an RTX 3070 Ti the grass pass takes 5.0 ms against the chunked path's 1.7, so
|
||||
`gpu_feature_implemented(GF_MESH_GRASS)` stays false and a game should not offer it as a finished setting.
|
||||
- **More of the renderer's quality is a switch a game can offer.**
|
||||
|
||||
- `sky_set_quality(width)`: the prefiltered sky light at 256, 512 or 1024 wide (half as tall),
|
||||
baked again at once.
|
||||
- `post_set_msaa(samples)`: 1, 2 or 4 samples for the scene on OpenGL, remaking the screen targets in
|
||||
place (`post_msaa_live()` is false on Vulkan, which has no sample counts yet); `post_free` now
|
||||
frees the multisampled framebuffer too.
|
||||
- `STREAM_BUDGET_US` is a variable: the microseconds a frame may spend generating streamed cover.
|
||||
- `r3d_fog_scale`: a multiplier over the fog density the daylight and the weather set.
|
||||
- **NVIDIA DLSS super resolution and Reflex on the Vulkan renderer** — through NVIDIA Streamline 2.14.1.
|
||||
|
||||
- **The loader** — on Windows `Vk.open()` takes `sl.interposer.dll` beside the executable in place of
|
||||
`vulkan-1.dll` when a program asks (`Vk.sl_prefer`), falling back to plain Vulkan without it; the `sl*`
|
||||
exports and feature functions are reachable from Ludic.
|
||||
- **render3d** — `r3d_dlss(mode)` upscales the lit HDR frame before bloom and the tonemap, jittered on a
|
||||
Halton cycle, with preset K in every mode (the default M cost 18 ms a frame at 4K on an RTX 3070 Ti);
|
||||
`r3d_dlss_render_w/h` give the size to render at. `r3d_reflex(mode)` sleeps each frame and marks
|
||||
simulation, submit and present. `R3D_DLSS`, `R3D_REFLEX`, `R3D_DLSS_PRESET`, `R3D_SL_LOG` for tests.
|
||||
- **Commands the interposer lacks** — it exports no `vkSetHdrMetadataEXT`, `vkCmdDrawMeshTasksEXT` or
|
||||
acceleration-structure command; `Vk.has` checks one and `vkGetDeviceProcAddr` reaches it.
|
||||
- **Real OS threads behind `Job.*` and `Sync.*`** — `Job.parallel_for` runs work across every core.
|
||||
|
||||
- **`fn name`** — an expression naming a top-level function as a value, for a worker's entry point.
|
||||
No closures: the data a worker needs is passed in.
|
||||
- **`Job.parallel_for(count, fn work, ctx)`** — `work(i, ctx)` for every `i` in `[0, count)`, split
|
||||
into chunks across a worker pool (one thread per core but one, pthreads or Win32) and the calling
|
||||
thread; returns when all are done. `Job.is_worker()` says whether code is on a pool thread.
|
||||
- **`Sync.*` is thread-safe** — mutexes are real mutexes, atomics are compare-and-swap, channels are
|
||||
guarded, and `Sync.cpu_count()` reports the machine's cores.
|
||||
- **A worker must not change the world** — `spawn` and `despawn` on a pool thread stop the program with
|
||||
a located message.
|
||||
- **Texture filtering and shadow resolution as settings a game can change while it runs.**
|
||||
|
||||
- `r3d_set_anisotropy(level)` sets the anisotropic filtering of every mipmapped texture already
|
||||
loaded, not only of later uploads, on OpenGL and Vulkan.
|
||||
- `shadow_set_res(size)` remakes the cascades' depth layers at 1024, 2048 or 4096 (`shadow_res`
|
||||
replaces the `SHADOW_RES` constant); the lighting reads the texel size from the map.
|
||||
- **`app native "<dir>"` in `package.ludic`** — `ludic bundle` on Windows copies that directory's files beside
|
||||
the executable: native libraries loaded at run time (NVIDIA Streamline's DLLs, say) and their licence texts,
|
||||
which cannot live in the pack.
|
||||
|
||||
### Fixes
|
||||
|
||||
- **The meadow's grass draws on the Vulkan renderer again** — the chunked grass path uploaded its `vec4` tile
|
||||
table with `u_fv`, which on Vulkan copies one float per array element, so every tile had no blades.
|
||||
`u_f4v` uploads arrays of `vec4`.
|
||||
|
||||
### Performance
|
||||
|
||||
- **The sky's light is baked once at start, not twice.** A game that turns the sky at boot sets
|
||||
`sky_start_yaw` before `r3d_init`, and `sky_load` bakes the image-based light at that yaw; turning to
|
||||
a yaw the light is already baked at (`sky_set_yaw`) no longer bakes it again. Without it, nothing
|
||||
changes.
|
||||
## v0.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 by **`ludic-dev vkgen`** into `runtime/native/vk_api.ludic` and `vk_thunks.ll`;
|
||||
structs are plain memory filled by name with `Vk.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 are `long`.
|
||||
- **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. `ludicc` and `ludic build` link both files for any program that
|
||||
uses `Vk.*`.
|
||||
- `examples/rendering/vk_probe.ludic` reports what a machine's Vulkan can do;
|
||||
`vk_compute.ludic` runs 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 through `gpu_*` / `u_*` and nowhere else (OpenGL frames unchanged);
|
||||
`R3D_GFX=gl|vk` or `gpu_request` chooses 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|none` pretends, for tests).
|
||||
## v0.14.2 — 2026-09-14
|
||||
|
||||
### Fixes
|
||||
|
||||
- **`Input.mouse_dx` / `mouse_dy` no 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_dy` to 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.
|
||||
## v0.14.1 — 2026-09-13
|
||||
|
||||
### Fixes
|
||||
|
||||
- **A rim from `outline_model` no longer stays on after the caller stops asking** — the queue
|
||||
was only emptied by the next `outline_model` call, so a game that highlights what the
|
||||
crosshair is on left the last highlighted tree outlined for good once the player looked
|
||||
away. `r3d_frame` now drops a batch nobody reopened this frame (`outline_frame`).
|
||||
## v0.14.0 — 2026-09-13
|
||||
|
||||
### Features
|
||||
|
||||
- **Overlay text is UTF-8, and a font atlas can carry any script** — `ludic.render3d`'s `ov_text`,
|
||||
`ov_text_w` and `ov_text_wrap` decode UTF-8 instead of drawing bytes 32–126, so a game can show
|
||||
Turkish, German, Polish, Greek, Cyrillic or whatever its atlas holds.
|
||||
|
||||
- `font.json` may 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.
|
||||
## v0.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 `discard` turns
|
||||
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 with `depth.frag` (the same alpha coverage test), the terrain
|
||||
beneath them is rejected before shading, and the lit pass draws them with no `discard` and an equal depth test. `R3D_NOPREPASS=1` restores the old path for comparison.
|
||||
## v0.13.2 — 2026-09-13
|
||||
|
||||
### Fixes
|
||||
|
||||
- **`ludic build` links `http.ll` only into a program that uses `Http.*`.**
|
||||
|
||||
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, where `ludic build`, `ludic run` and a new project all
|
||||
broke in CI. It now looks for a call to `hs_send`.
|
||||
## v0.13.1 — 2026-09-13
|
||||
|
||||
### Fixes
|
||||
|
||||
- **The toolchain links on Linux again** — CI builds, tests and publishes releases.
|
||||
|
||||
The asset-pack boot finds the directory beside the executable with `_NSGetExecutablePath`,
|
||||
which Darwin's libc has and glibc does not, so every Linux link of the compiler seed failed
|
||||
from the release that added packs onward: CI's build, suite and C-free bootstrap jobs, and
|
||||
every `publish` run, so no release after v0.7.0 carried a Linux toolchain or was created by
|
||||
CI at all. `tools/ci/linux_stdio_shim.ll` now supplies it, over `/proc/self/exe`.
|
||||
## v0.13.0 — 2026-09-13
|
||||
|
||||
### Features
|
||||
|
||||
- **Windows target** — `ludicc` builds and runs headless programs on Windows, and builds itself there.
|
||||
|
||||
- **`--target <triple>`** — a triple naming `windows` selects the Windows runtime; without
|
||||
the flag the target is the host, read at run time from `OS=Windows_NT`. The IR still
|
||||
carries no triple, so clang assembles it for the machine it runs on.
|
||||
- **The libc surface, in IR** — on Windows the header emits `emit_win.ludic`, which defines
|
||||
the POSIX names the backend calls (`fopen`, `ftell`, `rename`, `opendir`, `mmap`,
|
||||
`fmemopen`, `uname`, …) over the UCRT and Win32. Three of them fixed silent breakage
|
||||
rather than link errors: `rename` onto an existing file (every `Fs.write_text` after the
|
||||
first), a 32-bit `ftell`, and text-mode `fopen` rewriting `\n` as `\r\n`.
|
||||
- **Known folders** — `Os.save_dir` / `config_dir` are `%APPDATA%\<app>`, `cache_dir` is
|
||||
`%LOCALAPPDATA%\<app>`, `temp_dir` is `%TEMP%`, all with forward slashes; a bundle's
|
||||
`home` line points at `%APPDATA%\<name>`.
|
||||
- **The driver** — finds `C:\Program Files\LLVM\bin\clang.exe` when clang is not on
|
||||
`%PATH%`, writes `.exe` outputs, speaks cmd.exe for its directory and cleanup commands,
|
||||
and reads `argv[0]`, `%PATH%` and `$LUDIC_HOME` with either separator.
|
||||
- **A window** — `win32.ll` is `cocoa.ll`'s contract over user32: the held-key set and frame
|
||||
key in Ludic codes, the mouse in framebuffer pixels with raw input for cursor mode 2,
|
||||
cursor hide/lock/confine that lets go when the window loses the foreground, XInput pads
|
||||
in SDL order with rescaled deadzones, and the software `win_present`. `win32_gl.ll` puts
|
||||
the WGL context on that window with vsync and borderless full screen; a headless build
|
||||
links `gl_win_nowin.ll` instead. The process is per-monitor DPI aware.
|
||||
- **Sound** — `audio_win.ll` is `audio.ll`'s contract over XAudio2: one source voice per
|
||||
clip, loops, volume, rate and a balance pan through the output matrix, RIFF WAVE in PCM or
|
||||
float. It reads a clip through the asset pack, so a bundled game's first `Audio.load`
|
||||
succeeds - AVAudioPlayer takes a filesystem path, which is why macOS cannot.
|
||||
- **The CLI on Windows** — `ludic build`, `ludic run` and `ludic-dev build` work from a Windows
|
||||
checkout. Every shell command the CLI issues runs through Git for Windows' bash
|
||||
(`$LUDIC_BASH` names another), `compile_app` links through `ludicc -o`, and a checkout
|
||||
bootstraps from the new `selfhost/ludicc.win.seed.ll`, which `ludic-dev reseed` now
|
||||
writes beside the macOS seed.
|
||||
- **`ludic bundle` on Windows** — `build/<name>/` holding a GUI-subsystem `<name>.exe`, the
|
||||
`game.lpak` and `packs.index`; an `.ico` beside `app icon` is compiled with `llvm-rc` and
|
||||
linked in. `ludicc` gains `--gui` (no console behind the window) and `--link <file>`.
|
||||
- **`Http.*` on Windows** — `http_win.ll` is `http.ll`'s contract over WinHTTP: a request
|
||||
built on the game thread, the exchange on a worker thread, TLS with the system's
|
||||
certificate checks, and response headers answered from the kept request handle.
|
||||
- **The splash and the window icon** — decoded by WIC from the bytes in the pack. The splash
|
||||
is a topmost borderless window at the artwork's own size in points times the display's
|
||||
scale; `App.set_icon` sets the title bar and taskbar icon of a build with no icon resource.
|
||||
- **`Gl.*` on Windows** — `gl_win.ll` makes a WGL 4.1 core context on a hidden window's DC,
|
||||
and `gl_thunks_win.ll` (generated by `ludic-dev glgen` beside `gl_thunks.ll`) calls
|
||||
every entry point through a table `@lgl_win_load` fills from `wglGetProcAddress`, falling
|
||||
back to `opengl32.dll` for the 1.1 functions it will not return. A headless GL program
|
||||
renders on the GPU there: `gl_triangle` matches the macOS frame to within one level per
|
||||
channel.
|
||||
|
||||
### Fixes
|
||||
|
||||
- **Backspace fires on macOS** — the Delete key reports `Key.Backspace` (8).
|
||||
|
||||
AppKit gives the key marked delete (kVK_Delete, 51) the character `NSDeleteCharacter`, 127,
|
||||
which is what both the held-key set and the per-frame key received, so `Key.Backspace` never
|
||||
matched. `cocoa.ll` maps key code 51 to 8 in both.
|
||||
- **Held arrow keys register** — `Key.Up`, `Key.Down`, `Key.Left` and `Key.Right` are 128-131.
|
||||
|
||||
They folded to the codes of w, s, a and d, which is what the single per-frame key
|
||||
(`Input.key`) reports for an arrow. The held-key set has always stored an arrow under
|
||||
128-131, so `Input.key_down(Key.Up)` tested the W bit and `Input.move_i`'s arrow half never
|
||||
moved anything. `Input.key` keeps its WASD alias: a game comparing it with `'w'` still
|
||||
takes the arrows.
|
||||
- **`ludic build` links `Http.*`** — a program that uses the HTTP client builds through the CLI.
|
||||
|
||||
`compile_app` linked the OpenGL backend when a program named `@lgl_*` but never
|
||||
`runtime/native/http.ll` and Foundation for `@hs_*`, so a `Http.*` program failed to link
|
||||
under `ludic build` / `ludic run` while `ludicc -o` built it. It now links them the same way,
|
||||
windowed and headless.
|
||||
## v0.12.1 — 2026-09-13
|
||||
|
||||
### Features
|
||||
|
||||
- **ludic.render3d: `r3d_fog_base`** — the height the height fog's density is measured from (default 0, the world's y = 0). Two maps sharing one height datum, one far lower than the other, no longer give the lower one many times thicker air.
|
||||
|
||||
### Fixes
|
||||
|
||||
- **ludic.render3d: lakes are ellipses** — a water body other than the reflecting one was drawn as the whole rectangle of its bounds, so the corners between that rectangle and the lake's carved ellipse showed sheets of water over dry ground. Those bodies are now clipped to the ellipse; the reflecting body (the sea) still fills its rectangle.
|
||||
## v0.12.0 — 2026-09-13
|
||||
|
||||
### Features
|
||||
|
||||
- **ludic.render3d: replace the world at run time** — a game can swap its terrain, scatter, colliders, actors and water for another map's without restarting.
|
||||
|
||||
- `terrain_reload(dem, emin, emax, base, ox, oz, ortho, half)` releases the current map's height field, survey, photograph and patch bounds and generates another at any `TERRAIN_HALF`; `terrain_unload()` is the release on its own.
|
||||
- `scatter_clear_all()` (with `stream_clear_all()`), `actor_clear_all()` and `col_reset()` empty the scene for the next map; `mesh_free()` releases a mesh's GL objects.
|
||||
- **Water bodies** — `water_body_add(level, cx, cz, ex, ez, reflect)` and `water_bodies_clear()` draw several still-water planes at their own levels; the first that reflects gets the planar reflection. `water_init` still means one reflecting plane.
|
||||
- **Fix:** the terrain's patch quadtree placed patches at a fixed 32 m leaf, so any `TERRAIN_HALF` other than 4096 put patch bounds in the wrong place; leaves now scale with the map.
|
||||
- **Fix:** `water_init` built a new mesh and compiled a new program on every call.
|
||||
- `terrain_sea(level)` separates the sea's level (the coast, the strand, the shoreline, forest and scree shading) from the carved lake's, so a lake can sit above the sea. Unset, the sea is the lake's level as before.
|
||||
## v0.11.1 — 2026-09-12
|
||||
|
||||
### Fixes
|
||||
|
||||
- **`col_resolve` no longer throws a body across the map when it starts dead centre on a
|
||||
collider.** The degenerate branch — a point exactly on a circle's axis, where there is
|
||||
no direction to push it — chose a unit normal `(1, 0)` and then divided it by the
|
||||
clamped `d = 0.001` anyway, along with the real normals. The push came out a thousand
|
||||
times too large: a body standing exactly on a 0.5 m trunk was moved about 800 m instead
|
||||
of the 0.85 m that clears it.
|
||||
|
||||
Off-centre the arithmetic was correct, and off-centre is how anything arrives at a
|
||||
trunk while walking, so it never showed up in play — only a teleport, a spawn or a
|
||||
world generator placing something on an existing collider could land on the axis.
|
||||
|
||||
`(ex, ez) / d` is always a unit vector for `d > 0`, because `d` is its own length: there
|
||||
was never anything to clamp. The normal is now built once, explicitly, and the clamp is
|
||||
gone.
|
||||
## v0.11.0 — 2026-09-12
|
||||
|
||||
### Features
|
||||
|
||||
- **`App.set_icon(path)`** — an icon for a binary that has no bundle to take one from.
|
||||
|
||||
`ludic bundle` builds an `AppIcon.icns` and macOS reads it from the `.app`, so a
|
||||
shipped game has an icon. `ludic build` produces a bare executable, which has no
|
||||
bundle, no `CFBundleIconFile` and therefore no icon at all — macOS draws the generic
|
||||
green "exec" tile in the Dock. That is the build a developer runs all day and the one
|
||||
they see every session, so "my game has no icon" is true long before it ships.
|
||||
|
||||
```ludic
|
||||
App.set_icon("assets/app/icon.png")
|
||||
```
|
||||
|
||||
The path resolves through the pack first and the filesystem second, like every other
|
||||
asset, so the same call works packed and unpacked — it does not repeat `Audio.load`'s
|
||||
trick of taking a filesystem path only. An image that cannot be found or decoded
|
||||
leaves the current icon alone rather than clearing it, calling it in a bundled app is
|
||||
a harmless no-op, and a headless build compiles it away to nothing.
|
||||
## v0.10.1 — 2026-09-11
|
||||
|
||||
### Fixes
|
||||
|
||||
- **`Os.save_dir` / `Os.config_dir` / `Os.cache_dir` follow the platform.** They were the
|
||||
macOS layout everywhere, 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 building a path.
|
||||
|
||||
| | macOS | Linux |
|
||||
| --- | --- | --- |
|
||||
| `save_dir` | `~/Library/Application Support/<app>` | `$XDG_DATA_HOME` or `~/.local/share/<app>` |
|
||||
| `config_dir` | `~/Library/Application Support/<app>` | `$XDG_CONFIG_HOME` or `~/.config/<app>` |
|
||||
| `cache_dir` | `~/Library/Caches/<app>` | `$XDG_CACHE_HOME` or `~/.cache/<app>` |
|
||||
|
||||
An XDG variable that is set but empty falls back to the default, as the spec requires.
|
||||
macOS is unchanged to the byte — `save_dir` and `config_dir` stay the same directory
|
||||
there, because 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.
|
||||
|
||||
One consequence worth stating plainly: a game already shipped on Linux was writing to
|
||||
the old macOS-shaped path, and this moves it. Those files are not migrated for you —
|
||||
they are wherever `~/Library/Application Support/<app>` ended up on that machine, and a
|
||||
game that has Linux players should move them once on startup.
|
||||
## v0.10.0 — 2026-09-11
|
||||
|
||||
### Features
|
||||
|
||||
- **`.packignore`** — keep build artefacts out of the shipped asset pack.
|
||||
|
||||
A pack root is packed wholesale, so everything a model or texture pipeline leaves
|
||||
beside its output ships too: preview renders, bake intermediates, the `.blend` a
|
||||
`.gltf` came from. Nothing errors and nothing looks wrong — the app is just bigger
|
||||
than the game. The only way out was naming every file in `package.ludic`, a list
|
||||
that goes stale the day someone adds a texture.
|
||||
|
||||
Put a `.packignore` beside the assets instead. The rules are **gitignore's**, down
|
||||
to the parts people rely on without thinking about them: patterns anchored by a
|
||||
slash or floating without one, `preview/` for directories only, `*` and `?` stopping
|
||||
at a separator where `**` crosses it, `[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.
|
||||
|
||||
`ludic pack` reports what it left out, and `--no-ignore` packs everything so you can
|
||||
see what a rule costs. `ludic bundle` gathers the same way. `.packignore` itself is
|
||||
never packed, and the file only governs your own roots — a package's resources, such
|
||||
as the renderer's shaders, are added afterwards and cannot be excluded by accident.
|
||||
## v0.9.1 — 2026-09-11
|
||||
|
||||
### Fixes
|
||||
|
||||
- **`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 of
|
||||
them. 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, so a queued outline was drawn into whichever
|
||||
target happened to come first and was gone by the time the scene was drawn. Nothing
|
||||
appeared, with every uniform, matrix and mesh correct.
|
||||
|
||||
A flush now *closes* the batch rather than clearing it, and the next `outline_model`
|
||||
opens a new one: every pass in a frame sees the same requests, and a caller still needs
|
||||
no frame hook.
|
||||
## v0.9.0 — 2026-09-11
|
||||
|
||||
### Features
|
||||
|
||||
- **`Audio.play_at(id, gain:, pitch:, pan:)`** — fire a one-shot with its own gain, pitch
|
||||
and stereo position, leaving the master settings alone.
|
||||
|
||||
`Audio.volume` and `Audio.pitch` are global: they are there so a player can turn the game
|
||||
down, and a game that used them to place a sound in the world would be fighting its own
|
||||
options screen. This is the per-voice version, and it is what distance attenuation is made
|
||||
of — a 3D game works out how far away a sound is and which side it is on, and says so.
|
||||
`gain` rides on top of the master volume, so the options screen still wins; `pan` runs
|
||||
-1 (hard left) to 1 (hard right).
|
||||
|
||||
A loaded sound is still one player, so firing the same handle again restarts it rather
|
||||
than layering a second copy. Load a handle per variant when several need to overlap.
|
||||
- **`ludic.render3d`: `outline_model(model, mat, width, r, g, b)`** — a rim around something
|
||||
that is not an Actor.
|
||||
|
||||
An Actor has had an `outline` since the outline pass landed, and everything else in a
|
||||
scene had nothing: the pass walked the actor list and stopped. Instanced scatter was the
|
||||
gap that mattered, because a game that highlights whatever the crosshair is on could
|
||||
highlight every object in the world *except* the twenty thousand most common ones — the
|
||||
trees.
|
||||
|
||||
`outline_model` queues a model at a transform from anywhere in the frame and the outline
|
||||
pass flushes it alongside the actors', which is what gets the depth test right: the rim has
|
||||
to be drawn after the scene it is tested against, and a caller does not control pass order.
|
||||
A layer whose vertex shader moves its instances — wind, or standing them on the drawn
|
||||
terrain — should be handed the transform that shader arrives at.
|
||||
- **`ludic.render3d`: `terrain_coast(cx, cz, margin, fall)`** — the other way to make an
|
||||
island, and the one a real survey wants: the sea goes around the survey's **own edge**
|
||||
rather than being cut out of the middle of it.
|
||||
|
||||
`terrain_island` measures a radius out from a centre, which suits a made-up map and
|
||||
drowns most of a real one — an 8 km mountain survey loses two thirds of itself to make an
|
||||
island of the rest. `terrain_coast` measures inward from the boundary instead: everything
|
||||
the data covers stays land, the outer `margin` metres go under water, and the `fall` metres
|
||||
inside that are scaled down into it. The band is wobbled by low-frequency noise, so what
|
||||
comes out is headlands and bays rather than the square the data arrived in.
|
||||
|
||||
Both modes also gained a **strand**. Scaling alone hands a 500 m mountainside a 40-degree
|
||||
plunge into the sea, which is a cliff coast and nothing else; the last few metres of height
|
||||
either side of the water line are now compressed, which stretches them out horizontally
|
||||
into beach and shallows. Inland lakes are untouched — they have their own bed.
|
||||
## v0.8.0 — 2026-09-10
|
||||
|
||||
### Features
|
||||
|
||||
- **A boot splash**, customised by the game. `app splash` in `package.ludic` names
|
||||
the artwork and `app splash_bg` the colour behind it; the runtime raises a
|
||||
borderless window from a constructor that runs before `main`, reading the image
|
||||
out of the asset pack, so it is on screen while the process is still starting
|
||||
rather than after the slow part it exists to cover. Nothing hides it
|
||||
automatically - only the game knows when its first real frame is ready, so the
|
||||
game calls `App.splash_hide()`.
|
||||
- **Asset packs.** `ludic pack` writes every asset a game opens into one `.lpak`
|
||||
beside the binary, and the runtime mounts it before `main`. Nothing about how a
|
||||
game is written changes: the pack is spliced in at `file_open`, the one place
|
||||
every asset comes through, so `gltf_load`, `tex_load`, `Audio.load`,
|
||||
`Fs.read_text` and the renderer's own shader loads all find it without knowing
|
||||
it exists. `Fs.exists` and `Fs.size` consult the packs too. Entries are stored
|
||||
rather than compressed and the pack is `mmap`'d, so 165 MB of terrain costs one
|
||||
syscall at startup and pages in only what is touched. Several packs can be
|
||||
mounted in order, and a later one shadows an earlier one - which is how a patch
|
||||
replaces individual files without rewriting the base pack. See `docs/SHIPPING.md`.
|
||||
- **`ludic bundle`** turns a built game into a real macOS `.app`: `Info.plist` and
|
||||
`PkgInfo` from the manifest, an `.icns` built from one source PNG at every size
|
||||
macOS asks for, the asset pack in `Contents/Resources`, and an ad-hoc signature
|
||||
so it launches on Apple silicon. Everything comes from `app` lines in
|
||||
`package.ludic`, so the command takes no arguments. A bundled game is also moved
|
||||
to `~/Library/Application Support/<name>` at startup, because Finder starts a
|
||||
`.app` with its working directory at `/` where no save could ever be written.
|
||||
- **`ludic.render3d`: `terrain_island(cx, cz, r, fall)`** keeps land out to `r` and then
|
||||
scales the terrain down into the water over `fall` metres. Scaling rather than blending
|
||||
to a fixed bed is what makes the coastline come out of the terrain already there: low
|
||||
ground becomes beach and shallows, high ground becomes cliff. `r = 0` leaves the survey
|
||||
untouched.
|
||||
- **`ludic.render3d`: a rim outline on an actor.** An actor gains `outline` (metres of
|
||||
rim) and `ocol` (its colour). The pass draws the model again with its vertices pushed
|
||||
along their normals and its front faces culled, so only the far side of the swollen
|
||||
shell survives - a silhouette exactly `outline` wide, depth-tested against the scene, so
|
||||
anything standing in front of the actor hides its rim too.
|
||||
- **`ludic.render3d`: the moon has a phase.** One number, 0 new .. 0.5 full .. 1 new
|
||||
again, decides the disc's terminator, how much of its light reaches the ground, and
|
||||
where in the sky it rides - a full moon rises as the sun sets, 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.
|
||||
|
||||
### Fixes
|
||||
|
||||
- **`ludic.render3d`: water read the window's size, not the frame it drew into.** The
|
||||
water shader's `u_screen` and the reflection target were sized from the drawable, but
|
||||
`gl_FragCoord` there runs over the scene target. They match only at a render scale of 1;
|
||||
below that 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.
|
||||
## v0.7.0 — 2026-09-10
|
||||
|
||||
### Features
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@
|
|||
> ```bash
|
||||
> # one-time bootstrap: clang assembles the seed, then ludicc compiles bin/ludic
|
||||
> mkdir -p bin && clang selfhost/ludicc.seed.ll -o bin/ludicc
|
||||
> bin/ludicc --unsafe --globals tools/ludic-cli/dev.ludic -o bin/ludic-dev
|
||||
> bin/ludicc tools/ludic-cli/dev.ludic -o bin/ludic-dev
|
||||
> bin/ludic-dev build # the whole toolchain into bin/
|
||||
> # (ludicc, ludic, ludic-fmt, ludic-lsp)
|
||||
> bin/ludicc examples/games/snake.ludic -o bin/snake # the compiler, directly
|
||||
|
|
@ -73,9 +73,6 @@ point at a different LLVM toolchain if you have one.
|
|||
| a windowed native executable | `ludicc game.ludic -o build/game` |
|
||||
| a headless executable | `ludicc game.ludic --headless -o build/game` |
|
||||
| the IR, to read | `ludicc src.ludic --emit-llvm -o src.ll` |
|
||||
| the schema an editor reads (records, registries and their entries, consts) | `ludicc src.ludic --emit-schema schema.json` |
|
||||
| every error, as a JSON array on stdout | `ludicc src.ludic --check --diagnostics=json` |
|
||||
| the same, with an unsaved buffer on stdin standing for one of its files | `ludicc src.ludic --check --diagnostics=json --stdin-file lib/a.ludic < buf` |
|
||||
| a shared library † | `ludicc lib.ludic --shared -o build/liblib.dylib` |
|
||||
| a game that runs in a browser † | `ludicc game.ludic --target wasm32-unknown-unknown -o build/web/game.wasm` |
|
||||
| an object file † | `ludicc src.ludic -c -o src.o` |
|
||||
|
|
@ -353,7 +350,6 @@ The self-hosted `ludicc`/`ludic` (built with `bin/ludic-dev build-cli`) accept:
|
|||
--windowed force a windowed (Cocoa) build
|
||||
--headless force a headless build (stdin input, out.ppm output)
|
||||
--emit-llvm stop at LLVM IR — write it and exit, no clang
|
||||
--check every check a build makes (types, modules, uses, layers, ports, binds); write nothing
|
||||
--fmt lex + parse only; exit 0 if it parses, 1 on a parse error
|
||||
(the check-docs gate; canonical formatting not yet restored)
|
||||
--save-temps keep the intermediate .ll
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ From a clean checkout, one line lifts the toolchain off the seed:
|
|||
|
||||
```bash
|
||||
mkdir -p bin && clang selfhost/ludicc.seed.ll -o bin/ludicc
|
||||
bin/ludicc --unsafe --globals tools/ludic-cli/dev.ludic -o bin/ludic-dev
|
||||
bin/ludicc tools/ludic-cli/dev.ludic -o bin/ludic-dev
|
||||
```
|
||||
|
||||
That gives you `bin/ludic-dev`, the contributor tool: it replaces every
|
||||
|
|
|
|||
1657
LANGUAGE.md
1657
LANGUAGE.md
File diff suppressed because it is too large
Load diff
29
README.md
29
README.md
|
|
@ -60,9 +60,9 @@ ludic run # compiles src/main.ludic and opens a native window
|
|||
```
|
||||
|
||||
`ludic new` writes a manifest, a program that already moves something on screen,
|
||||
and a test. `ludic build` stops at the binary; `ludic bundle` goes on to the
|
||||
thing you can actually give someone. Rendering is deterministic, so a frame can
|
||||
be produced without a window, which is what CI diffs:
|
||||
and a test. `ludic build` stops at the binary — one self-contained executable
|
||||
with nothing to ship beside it. Rendering is deterministic, so a frame can be
|
||||
produced without a window, which is what CI diffs:
|
||||
|
||||
```bash
|
||||
ludic test
|
||||
|
|
@ -114,28 +114,6 @@ bin/ludic-dev test # the regression suite
|
|||
[API reference](https://workshopsoft.pages.workshopsoft.io/ludic/api.html)
|
||||
documents every symbol on its own page.
|
||||
|
||||
## Shipping
|
||||
|
||||
A built binary is a program, not an application: it opens its assets by a path
|
||||
relative to the working directory, so it runs from the project root and nowhere
|
||||
else, and it wears the generic executable icon.
|
||||
|
||||
```bash
|
||||
ludic pack # every asset the game opens, into one .lpak
|
||||
ludic bundle # ...and that, the binary, an icon and the metadata, as a .app
|
||||
```
|
||||
|
||||
Nothing about how the game is written changes. `gltf_load("assets/kit/hiker",
|
||||
…)` reads a file during development and a run of bytes inside the bundle once
|
||||
shipped, and cannot tell which — the pack is spliced in at `file_open`, the one
|
||||
place every asset in a Ludic program comes through. A bundled game also gets a
|
||||
boot splash it controls (`App.splash_hide()`) and a writable home under
|
||||
Application Support, because Finder starts a `.app` at `/` where no save could
|
||||
be written.
|
||||
|
||||
Without a pack beside it — which is every `ludic run` — nothing mounts and every
|
||||
open goes to the filesystem exactly as before. See [docs/SHIPPING.md](docs/SHIPPING.md).
|
||||
|
||||
## Packages
|
||||
|
||||
Dependencies are identified by URL, resolved with minimal version selection, and
|
||||
|
|
@ -144,7 +122,6 @@ cached in a content-addressed store:
|
|||
```bash
|
||||
ludic add git.workshopsoft.io/user/pkg # resolve, fetch, link into ludic_modules/
|
||||
ludic get # install from package.ludic, write the lock
|
||||
ludic remove git.workshopsoft.io/user/pkg # the inverse of add
|
||||
ludic verify # check locked packages against the store
|
||||
```
|
||||
|
||||
|
|
|
|||
2
VERSION
2
VERSION
|
|
@ -1 +1 @@
|
|||
0.22.0
|
||||
0.7.0
|
||||
|
|
|
|||
|
|
@ -1,8 +0,0 @@
|
|||
bump: patch
|
||||
type: fix
|
||||
`Actor.cast_hidden` separates being DRAWN from CASTING. `ac_visible` rejected any hidden
|
||||
actor from the shadow pass as well as the scene pass, so a game that hides the player's
|
||||
own body - first person, or a viewfinder held to the eye - lost that player's shadow
|
||||
entirely. An actor hidden because the camera is inside its head is still standing in the
|
||||
sun; set this on it and it keeps its shadow. Everything else still stops casting when it
|
||||
is hidden.
|
||||
|
|
@ -1,17 +0,0 @@
|
|||
bump: patch
|
||||
type: fix
|
||||
A leaf is not matte.
|
||||
|
||||
Foliage roughness was pinned to 1.0 and grazing Fresnel switched off, so nothing green in
|
||||
the game had a highlight anywhere: the glint off waxy leaves and wet needles, which is
|
||||
most of what makes a real stand look alive rather than painted, was simply absent.
|
||||
|
||||
The reason it was switched off is real. A crown is card quads, and at a grazing angle the
|
||||
card's normal is a lie, so a plain specular lobe frosted whole crowns white against the
|
||||
sky. So the sheen comes back as its own term gated on exactly that: it fades out as the
|
||||
card turns edge-on, which is where its normal stops meaning anything. A tight lobe for the
|
||||
glint, a weak wide one for the waxy rim, and nothing at all at the angles that frosted.
|
||||
|
||||
Thin-leaf translucency reaches further with it - a backlit stand glows for as far as you
|
||||
can see it, not 140 m - and a dense crown passes 0.45 of it rather than 0.3. The distance
|
||||
cap that stops a two-pixel clump card becoming a lime disc stays.
|
||||
|
|
@ -1,28 +0,0 @@
|
|||
bump: minor
|
||||
type: feat
|
||||
Anti-aliasing that exists, and a lens for the viewfinder.
|
||||
|
||||
THE SHIPPING DEFAULT HAD NO ANTI-ALIASING AT ALL. The temporal resolve was removed (for
|
||||
good reasons - it reprojected water through the surface plane and dragged the mirror image
|
||||
behind the camera), the setting's first option went on saying "Temporal", and MSAA defaults
|
||||
to one sample. Every machine without DLSS - which is every Mac - drew a frame full of grass
|
||||
blades and needle cards with nothing smoothing a single edge.
|
||||
|
||||
FXAA now, in the sharpen pass, because that pass already reads this pixel's neighbourhood
|
||||
and runs last on the LDR image. It has no history, so it cannot drag or smear a reflection.
|
||||
Measured on edge pixels: 25.5% less single-pixel staircase.
|
||||
|
||||
One trap worth recording. The unsharp mask's delta is computed from the RAW image and only
|
||||
then applied to the anti-aliased colour. Taking the centre from the FXAA result and the
|
||||
neighbours from the raw texture measures a difference that is half smoothing and half
|
||||
signal, so the mask sharpens exactly the edges FXAA just softened - measured, that first
|
||||
version was 29% WORSE than no anti-aliasing at all.
|
||||
|
||||
And depth of field, for the photo mode: a disc of taps whose radius is the pixel's circle
|
||||
of confusion, signed so the two sides of the focal plane differ and normalised by the focus
|
||||
distance, because a lens focused at two metres throws a background out far harder than one
|
||||
focused at two hundred. A tap only counts if it is at least as out of focus as the pixel
|
||||
it is blurring into, which is what keeps a sharp foreground from haloing into a blurred
|
||||
background. It runs between the scene and the bloom so a blurred highlight still blooms,
|
||||
and the whole pass is skipped when the aperture is shut - in ordinary play there is no lens
|
||||
and this never draws.
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
bump: minor
|
||||
type: feature
|
||||
**Actions and reducers.** `action PickUp { item: int }` is a typed record of something that
|
||||
happened; `reducer Bag on PickUp(b: mut Bag, a: PickUp) { ... }`, in the module that owns the state,
|
||||
says what it means for that one state - a reducer takes exactly its state and the action, and a
|
||||
second state is refused; `dispatch PickUp { item: 7 }` queues one from anywhere. The queue is drained
|
||||
at the end of every phase of the frame loop, after every phase of ludic.base's `core_tick_all`, and
|
||||
where a program calls `drain_actions()`: in dispatch order, each action's reducers in the order of
|
||||
their states' names, an action a reducer dispatches queued behind (a queue still growing after 64
|
||||
rounds stops the program, naming the action). `ludic deps` reports `widest_function` - the most
|
||||
states any function or entry point of the program takes - and `--check` ratchets it.
|
||||
|
|
@ -1,15 +0,0 @@
|
|||
bump: minor
|
||||
type: feat
|
||||
An aspen leaf hangs on a flattened stalk and turns in air a spruce never feels, and the
|
||||
kit had no way to say so. `layer_flutter(l, v)` gives a scatter layer a per-leaf tremble:
|
||||
the vertex stage offsets each leaf by a phase taken from its own place on the card, so
|
||||
neighbouring leaves are never in step, and writes the result out as a varying the
|
||||
fragment stage uses to flash the leaf's pale underside as it turns. The flash is the part
|
||||
that reads - a still frame of a tremble is a still frame of nothing. One uniform, one
|
||||
varying, no extra pass, and every other layer leaves it at zero.
|
||||
|
||||
Also fixes the sway itself, which was measured in METRES: `hgt * hgt * 0.35` is right for
|
||||
a 40 cm flower and puts ten metres of sideways into a 14 m trunk, so every tall tree in
|
||||
the valley stood bent over like a fishing rod. It is a fraction of the model's own height
|
||||
now, so the tip moves a few per cent of the tree whatever the tree is and the base does
|
||||
not move at all.
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
bump: minor
|
||||
type: feature
|
||||
**`ludic.anim` carries ozz-animation (0.17.0, MIT) as a native library**, the second package to do
|
||||
so after `ludic.physics`. Its skeleton and each clip are built at LOAD from the numbers the package
|
||||
already reads - a skin's parents and rest pose, a clip's flattened channels - so there is no bake
|
||||
step and no new file. `anim_oz_skel(sk)`, `anim_oz_clip(c)`, `anim_oz_ctx(s)` and
|
||||
`anim_oz_sample(ctx, clip, t, rot, pos, n)` are the first step (Maroon Lake's phase 19.1): a sampled
|
||||
rotation agrees with `anim_mix` to within 1e-4 a component. `anim_play` is unchanged. The library is
|
||||
built by `native/build.sh` from the pinned release, and on Windows it imports KERNEL32 alone.
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
bump: minor
|
||||
type: feature
|
||||
**`@Asset(kind, map)`: a path under each map's directory.** A field whose file lives in the map's own folder
|
||||
(a grass kind's density picture, `ground/blades.png`) says so, and `ludicc --check` looks for it in every
|
||||
map - a @PerMap row's in its map, a game-wide row's in all of them - refusing a map that lacks it unless the
|
||||
field is `@Asset(kind, map, optional)`. The schema marks the attribute `"scope": "map"`.
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
bump: patch
|
||||
type: fix
|
||||
**An attribute before `export` is kept.** `@ToClients export event E`, `@Sync export property P`,
|
||||
`@Owned export model M` and the rest lost the attributes written in front of `export`: the parser read
|
||||
them, then parsed the declaration afresh and forgot them - so an exported remote event was silently
|
||||
local; and `export @ToClients event` was refused outright. Attributes and `export` now read in
|
||||
either order into the same declaration.
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
bump: minor
|
||||
type: feature
|
||||
**A bake's inputs can follow the data.** `bake_expand(inputs, map)` gives a Bakes row's inputs as they are
|
||||
hashed: `{map}` put as the map's key, and each input with a `*` put as the paths it matches, sorted as whole
|
||||
paths byte by byte, dot-names left out, a glob matching nothing gone. `bake_maps(first_input)` is the maps a
|
||||
`{map}` row covers: each directory under assets/maps holding its first input (`bake_maps_in` under another
|
||||
root). A runner hashes `bake_inputs_hash(bake_expand(row.inputs, map))`, the same path the check takes, so
|
||||
the two cannot disagree on stale; a game's Python tools are its checked twin.
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
bump: minor
|
||||
type: feature
|
||||
**Bakes you can look at, and three more of the renderer's textures read from one.** `ludic.lab` writes
|
||||
raw 8-bit pixels (1 to 4 channels) as a PNG (`lab_png_write`, `png_write.ludic`, importable alone with its
|
||||
own `LabPngState`) and turns float textures into honest previews (`png_convert.ludic`: R32F min..max as
|
||||
grey, RG16F as red and green x255, HDR RGBA16F as x/(1+x) then sRGB). `ludic.render3d` takes three bakes
|
||||
the game names (`bake_load.ludic`) and makes each as before when one is missing or stale: an impostor's
|
||||
atlases as BC7 with their baked mips, through the compressed upload (`impostor_from_baked`,
|
||||
`impostor_fill_bc7`; a fog opening past its cards reads the bake again instead of painting), the sky's image-based light at the start yaw per prefilter width (`sky_baked_in`; any other
|
||||
turn of the sky is convolved), and the grass carpet (`carpet_from_baked`, `carpet_bytes`).
|
||||
`impostor_from_bytes` returns null, keeping nothing, when the bytes are not the impostor's shape.
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
bump: minor
|
||||
type: feature
|
||||
**Textures a game baked at build time are read before the PNG.** `png_decode` first takes
|
||||
`assets/baked/png/<path>.tex` (a game's `ludic bake` output: the samples, ready to upload), and a cut-out
|
||||
load (`tex_load_ex` with dilate) first takes `assets/baked/cutouts/<path>.bc7` (padded as `tex_dilate`
|
||||
pads, then BC7 with its mips) - so a boot decodes, pads and converts nothing it can take ready-made. Both
|
||||
are ludic.base's baked form, read by hand (baked_tex.ludic: the "LBAK" header, the key and version); a
|
||||
missing or stale one falls back to the PNG as before. `tex_load_dds_at` reads a .dds at an offset.
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
bump: minor
|
||||
type: feature
|
||||
**`bind Purse { money: g_money }` - a port member bound to a variable.** A member that takes nothing
|
||||
may name a global instead of a function; the compiler writes the getter in the bind's file, so the
|
||||
one-line wrapper is gone. A member that takes something is refused a variable.
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
bump: patch
|
||||
type: fix
|
||||
**`ludic build` keeps its LLVM IR out of the project.** The intermediate `.ll` was written beside
|
||||
the binary (`build/<name>.ll`) and deleted after linking, so a project's tree held one for the
|
||||
length of every build, and two builds at once deleted each other's - which surfaced as a
|
||||
`clang: no such file` that read exactly like a compile error. It now goes to the run's own
|
||||
temporary directory and goes with it. `--save-temps` still keeps it at `build/<name>.ll`.
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
bump: patch
|
||||
type: fix
|
||||
**A function named like a built-in a call always takes is refused.** `function words(st, k)` compiled,
|
||||
and every call to it became the built-in `words(n)` - n zeroed ints, with a pointer for n - and LLVM
|
||||
refused the IR far from the cause. A top-level function whose name a call always takes as the
|
||||
compiler's own (`words`, `keep`, `print`, `save`, `load`, `key`, ...; the table is
|
||||
`selfhost/check/check_builtins.ludic`, held to `emit_call` by `ludic-dev syntax --check`) is now an
|
||||
error at its declaration, as `run` already was. Every other built-in (`buffer`, `floats`, `double`,
|
||||
...) yields to a function the program declares, in the checker as it already did in codegen.
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
bump: minor
|
||||
type: feature
|
||||
**`ludic build --check` / `ludicc --check`: check without building.** The parse, the type checker
|
||||
and the module rules (`export`, `uses`, layers, ports, registries) run, and nothing is emitted or
|
||||
linked - about three seconds on Maroon Lake where a build takes about a minute. In this mode the
|
||||
checker asks the module rules at each reference it resolves, since the emitter that usually asks
|
||||
them does not run.
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
bump: minor
|
||||
type: feat
|
||||
**Check an unsaved buffer.** `ludic build --check --diagnostics=json --stdin-file <path>` (and
|
||||
`ludicc --check --stdin-file <path>`) checks the program as usual, but wherever the compiler would open
|
||||
`<path>` - the entry, an import reached through a barrel, a component's `.xml` / `.lss`, an `.lres` -
|
||||
it reads the text on stdin instead, so an editor's diagnostics follow typing without a save. Paths are
|
||||
matched after normalising both (separators, relative to the working directory, `.` / `..` folded).
|
||||
Diagnostics carry the file's usual name with lines and columns in the buffer; a `<path>` the program
|
||||
never opens is reported as one warning.
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
bump: patch
|
||||
type: fix
|
||||
**A chunked table's keys are unique across its map, and not interned.** A row's id is `(map, key)`, so a
|
||||
tree moved into another chunk keeps it, and `ludicc --check` refuses a key written in two of a map's
|
||||
chunk files, naming both. The keys are no longer interned: interning every key a player walked past would
|
||||
have filled the bounded intern table and kept them all for good. A chunk slot keeps its keys in its own
|
||||
buffers, rewritten in place when the slot is refilled; `intern(row.key)` keeps one past `_out`.
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
bump: patch
|
||||
type: feat
|
||||
Things sit ON the ground rather than hovering over it. The screen-space GI pass takes
|
||||
a second, much tighter set of taps (a 0.40 m radius that grows with distance, with a
|
||||
range check so a far surface behind a near one cannot darken it) and folds the result
|
||||
into the ambient occlusion it already had. The wide radius answers "how enclosed is
|
||||
this", which a trunk meeting grass barely registers; the tight one answers "is
|
||||
something touching here", which is the shadow the eye looks for to place an object.
|
||||
It is eight taps on a buffer the pass had already bound.
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
bump: minor
|
||||
type: feature
|
||||
**Namespaces are declared in Ludic.** `alias meth(labels) = target` in a `namespace` block makes
|
||||
`Ns.meth(...)` a call to `target`, taking named arguments by those labels; with no label list the
|
||||
target's own parameter names are the labels. The engine's 41 table-driven namespaces - `Http`,
|
||||
`Udp`, `Process`, `Json`, `Value`, `Screen`, `Input`, `Audio`, `World`, `Tiled` and the rest, 438
|
||||
methods - moved out of the compiler into `runtime/native/namespaces.ludic`, and a package owns an
|
||||
API the same way. The code a program compiles to is unchanged byte for byte, and the checker now
|
||||
checks an alias call's arguments against its target.
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
bump: minor
|
||||
type: feature
|
||||
**`def Recipes from "recipes.lres"` - a game fills a package's open registry from its own resource
|
||||
file.** The entries are checked against the registry's record as the file is read, with errors at
|
||||
the resource file's line, and they are defs of the module that wrote the line: the registry must be
|
||||
open to it, and they sit in the stable order (the declaring module's entries, then other modules'
|
||||
by name, and file order within a file).
|
||||
|
|
@ -1,134 +0,0 @@
|
|||
bump: minor
|
||||
type: feature
|
||||
**Default parameters, components, views and templates.** A parameter can have a default (`pad: float = 8.0`).
|
||||
A call leaves out what it does not change, and may pass its first arguments by position and the
|
||||
rest by name.
|
||||
|
||||
A `view` declaration is the one bridge between a program and its UI. It names the fields a
|
||||
template may read, the functions it may ask and the `on` events it may send, and it writes
|
||||
`view_<name>() -> UiView`.
|
||||
|
||||
A `component Name { prop, state, fields, functions, on events }` declaration beside `Name.xml` and
|
||||
`Name.lss` is a UI component. Its template and styles are compiled in (with `@import` inlined), each
|
||||
mounted instance keeps its own props and state, styles are scoped to it, and a parent's `class`,
|
||||
`style` and `id` land on its root.
|
||||
|
||||
`ludic.ui` is now a template runtime. Screens and components are XML files loaded at run time,
|
||||
with:
|
||||
- `{expression}` bindings;
|
||||
- `<if>`, `<else>` and `<each>`;
|
||||
- props, `<slot/>` and per-instance `<state>`;
|
||||
- `on-press` actions that send events, `set` state or `emit` to the component's user;
|
||||
- component libraries (`export="true"`, `<import src as>`);
|
||||
- HTML's elements (`div`, `p`, `h1`-`h6`, `ul`/`li`, `img`, `hr`, ...), with a default stylesheet;
|
||||
- HTML's attributes: `id`, `class`, `style`, `hidden`, `disabled` and `onclick`, with any other
|
||||
attribute kept for selectors;
|
||||
- the CSS box model (padding and margin in 1-4 values, borders, `px` and `%`) and flex layout
|
||||
(`flex-grow`, `justify-content`, `align-items`/`align-self`, `flex-wrap`, min and max sizes)
|
||||
under CSS's property names;
|
||||
- stylesheets, in a `<style>` or an `.lss` file (a Ludic StyleSheet) that others import and that
|
||||
can `@import` more;
|
||||
- CSS's selectors: `#id`, compound classes, `[attr=value]`, descendant and `>` combinators,
|
||||
`:hover`, `:disabled`, `:first-child`, `:last-child`, `:nth-child`, `:not` and more, weighed by
|
||||
specificity.
|
||||
|
||||
- more CSS: custom properties and `var()`, `position` with insets and `z-index`, `em`/`rem`/`vw`/`vh`,
|
||||
`@media`, wrapping text and ellipsis, `overflow`, `+`/`~`, `:nth-child(an+b)`, `:checked`, `:active`;
|
||||
- more React: keyed lists, `<let>`, `<provide>` context, `<fragment>`, named slots, `on-mount` and
|
||||
`on-unmount`;
|
||||
- native elements a program draws itself (`ui_native`, `ui_fire`), and form controls;
|
||||
- errors with file and line, hot reload (`ui_reload`), and an inspector-style dump.
|
||||
|
||||
The runtime is a UI framework, not only a template engine:
|
||||
- it takes input itself: focus and keyboard navigation, the pointer, scroll boxes, `autofocus`;
|
||||
- it has built-in controls (button, checkbox, radio, range, select, text, key), styled as CSS
|
||||
parts;
|
||||
- `ludic.ui/render3d.ludic` is a render3d backend, with textures, atlases, nine-slices, clipping
|
||||
and scale;
|
||||
- more CSS: `rgba()`/`#rrggbbaa`, `border-radius`, `outline`, `box-shadow`, `background-image`,
|
||||
`border-image`, group `opacity`, `@keyframes` / `animation` / `transition`;
|
||||
- HTML mixed content, and boolean attributes;
|
||||
- `popover` (a top layer that keeps the pointer and keys, with light dismissal), `title` tooltips,
|
||||
and `<progress>` / `<meter>`;
|
||||
- importing `ludic.ui/render3d.ludic` installs the backend, and atlases take rows;
|
||||
- hooks for the program's language, sounds and clock.
|
||||
|
||||
What a game's screens found missing, now in `ludic.ui`:
|
||||
- `<input type="number" min max step>`: typed digits, Enter or leaving it commits them clamped, the
|
||||
arrows step it;
|
||||
- `<input type="key">` listens for any key (Tab and the arrows included) once Enter or a click starts
|
||||
it; Esc stops it, Backspace clears it, `shown` names the value, and `ui_capturing()` tells the host;
|
||||
- `note="..."` under any control's label (`.ui-note`); a range's `decimals`, `format="percent"` and
|
||||
`unit`; a track laid out as a row, with the range's fill as tall as it;
|
||||
- popovers anchored beside an element (`anchor="id"`, or a bare `anchor` for the element before it,
|
||||
`placement`), flipped to the other side and kept on the screen;
|
||||
- `flex-shrink` (a scroll box in a column takes the room its siblings leave), `flex: grow shrink`,
|
||||
`order`, and text in a row wrapping in the room its siblings leave;
|
||||
- `calc()` over px, %, em, rem, vw, vh and `var()`; `width: 0` and `height: 0` mean 0;
|
||||
- `text-shadow`; tooltips of several lines; `ui_opacity()` for a native's draw;
|
||||
- `border-image` drawn as painted with no background colour, tinted by one, and not at all under
|
||||
`transparent`; a picture file drawn untinted (an atlas cell still takes `color`);
|
||||
- the render3d backend loads a picture again when its file changes (`ui_image_reload`), draws a path
|
||||
with a drive letter as a path, and slices a nine-slice by its texture's own width and height;
|
||||
- a component root that is itself a component takes every user's class, style and id, and the
|
||||
sheets that style it are weighed together by specificity;
|
||||
- a component's event may be called `set`; a `string` prop given a number reads it as text; two
|
||||
components of one name are an error naming both files;
|
||||
- the scrollbar is `.ui-scrollbar` and `.ui-thumb`: a press on the thumb holds it where it was taken,
|
||||
a press on the track jumps the thumb's middle there, and neither presses what is under the bar;
|
||||
- a popover's own controls take its presses whatever lies under it, a press outside only closes it,
|
||||
and while one is up the scroll boxes outside it do not take the pointer;
|
||||
- the first gamepad moves the focus (d-pad, left stick), steps ranges and selects, and presses (A)
|
||||
and goes back (B); a held direction, on the pad or the arrow keys, repeats after 0.42 s and then
|
||||
every 0.11 s on the ui clock (`UiInput.held_*`, `pad_a`, `pad_b` for a host);
|
||||
- pointer events: `on-pointerdown` / `pointermove` / `pointerup` / `drag` / `wheel` with `event.x`,
|
||||
`y`, `dx`, `dy`, `button` and `wheel`, and `ui_native_input(tag, fn)` for a native; a press captures
|
||||
the pointer until release; the pointer hits the topmost element in painting order, and
|
||||
`pointer-events: none` lets it through;
|
||||
- `on-down` and `on-up` on a button (the pointer, Enter or A), with `:active` true while it is held
|
||||
there rather than whenever the pointer is down over it;
|
||||
- an anchored popover's `align="start|center|end"`, and `within="id"` (by default the nearest
|
||||
scroll box around it) for the bounds it is flipped against and kept inside;
|
||||
- `text-fit: shrink MIN` shrinks a line to its box, then cuts it with an ellipsis; `line-height`;
|
||||
an `em` reads the font size the element ends with (a `font-size` later in the rule, or in a later
|
||||
rule), not the one it had so far;
|
||||
- `min()`, `max()` and `clamp()`, in `calc()` or on their own; `top` / `right` / `bottom` / `left`
|
||||
as a percentage or a `calc()` of one, of the containing block;
|
||||
- a nine-slice's corners are clamped to half the box in each direction on its own and cut on whole
|
||||
pixels (`ui_nine_cuts`), so a small key cap has no seam;
|
||||
- `scroll-top="{px}"` holds a scroll box at an offset, with `on-scroll` when the player moves it;
|
||||
`ui_scroll_set(id, px)` moves one once;
|
||||
- `linear-gradient(...)` backgrounds; `aspect-ratio`; `object-fit` for pictures (the renderer's
|
||||
`image_w` / `image_h`) and `ui_object_fit` for natives;
|
||||
- `translate="no"` keeps an element's text as written; a title of several lines is translated whole,
|
||||
else line by line;
|
||||
- `<input type="key">` takes a mouse button (`UI_MOUSE_LEFT` / `RIGHT` / `MIDDLE`, 256-258) and is
|
||||
`:capturing` while it listens;
|
||||
- a `title` shows for the keyboard's focus too, after the same half second; a focus ring drawn
|
||||
through a renderer with no `rect` no longer crashes;
|
||||
- a component with no stylesheet of its own reads a theme's `:root` variables from around it (it
|
||||
did; now a test says so);
|
||||
- `ui_scale()` and `ui_box("id")`, the scale and an element's laid-out box, for a host;
|
||||
- `on-submit` on a text field (Enter or A; the focus and text stay unless `clear-on-submit`);
|
||||
- `zoom` on any element, and a length over a length in `calc()` is a plain number;
|
||||
- `on-hold` every frame a button is held, with `event.dt` and `event.t`;
|
||||
- a transition lands exactly on its end value (it had stopped a rounding error short of it, at every
|
||||
frame rate).
|
||||
|
||||
render3d gains `tex_width` / `tex_height`, and the XML reader keeps text runs among elements in
|
||||
order (`mixed`).
|
||||
|
||||
A `view` field set to a literal or a named function's result needs no type.
|
||||
|
||||
Screens are drawn through a registered renderer. `Value` gains a float kind.
|
||||
|
||||
Also:
|
||||
- A program's function named like one of the runtime's is refused; it had been silently taking the
|
||||
runtime's own calls. So is one named like a compiler built-in (`run`, `exit`, `free`, `fill`,
|
||||
...): every call to a program's own `run` compiled into C's `system()`, and clang failed on the IR.
|
||||
- An index is evaluated before the slice's elements are read. A `xs[f()]` whose `f` grew `xs`
|
||||
read stale memory.
|
||||
- A runtime error names the file its expression is in, not the program's.
|
||||
|
||||
Two declarations with one name (a package's private global and a program's, say) are reported as
|
||||
such before type checking. They used to surface as a page of type errors about the wrong type.
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
bump: patch
|
||||
type: feature
|
||||
**`ludic deps --writes` warns about a write through a local alias.** `let t = thing_cur` and then
|
||||
`t.used = 1` writes another module's record just as `thing_cur.used = 1` does; a local bound straight
|
||||
from another module's global (or from such a local) is now followed within its function and each
|
||||
write through it listed as a warning. A reference that arrives from a function's result is not.
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
bump: patch
|
||||
type: fix
|
||||
**`ludic deps`: a reach counts every state apart, however the states are numbered.** The reach and
|
||||
write-reach bitsets packed 60 states to a word, but an `int` is 32 bits, so `1 << 45` came back as bit
|
||||
13 and states 32 apart shared a bit: a function taking both counted one, fewer than it takes, and the
|
||||
counts (`widest_reach`, `widest_write_reach`, `--reach`, `--wreach`) rose and fell with how a program's
|
||||
states happened to be numbered. The sets now hold 30 to a word. On Maroon Lake `widest_reach` goes
|
||||
58 -> 76 and `widest_write_reach` 54 -> 64 - the real numbers, which the old count hid.
|
||||
`examples/state/reach_wide.ludic` (40 states, S00 and S32 taken together) holds it.
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
bump: minor
|
||||
type: feat
|
||||
**`ludic deps` sees through fn values, and lists the widest functions.** A step list or a registry of
|
||||
fn values takes no state and still reaches every state its steps take; `widest_reach` is the most
|
||||
states any function can come to - by a call, a `fn f` it writes, or a global holding fn values it
|
||||
reads - reported beside `widest_function` with how many of them it does not take itself
|
||||
(`the widest reach: app_boot (src/app/boot.ludic:30), 72 states (72 through calls and fn values it
|
||||
does not take)`). `--widest N` lists the N functions that take the most states with what each
|
||||
reaches; `--reach N` orders them by reach. A baseline written before this has no `widest_reach` and
|
||||
does not hold it until it is rewritten.
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
bump: minor
|
||||
type: feat
|
||||
**`ludic deps` says what a function can come to CHANGE** (`widest_write_reach`, and `--wreach N`
|
||||
lists the functions by it): the states it reaches as `mut`, through calls, `fn` values and step
|
||||
lists. Reach itself is sharper: `Port.member()` reaches that member's binding only, and
|
||||
`Registry[i].field` (or a local holding `Registry[i]`) reaches that field only - a question asked of
|
||||
a port or a table that also holds verbs no longer reaches the verbs.
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
bump: minor
|
||||
type: feature
|
||||
**`ludic.devlink`: the interface's verbs.** A `DevlinkUi` port, every member defaulting to "not offered":
|
||||
`ui_screen` (the screen's root class and its components), `ui_model "<Class>" "<out>"` and `ui_tree "<out>"`
|
||||
(the game writes a component's model or the whole tree to a file, no `..`, and the answer names it, so a
|
||||
datagram stays small) and `ui_override "<path>" "<file>"` (a template or stylesheet read from another file
|
||||
and reloaded keeping state; `""` clears one, `"" ""` all). Answered at once; nothing made per frame.
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
bump: minor
|
||||
type: feature
|
||||
**`ludic.devlink`: an editor's live link into a running dev build** (protocol v1, frozen with Ludic
|
||||
Studio). Loopback UDP through the `DevlinkNet` port, one request a frame parsed in place from one fixed
|
||||
8 KB buffer and answered into another; every verb a `DevlinkWorld` member defaulting to "not offered":
|
||||
`ping`, `hello` (the build's schema hash as 16 hex digits), `cam_get` / `cam_set` / `cam_release`, `goto`,
|
||||
`map_load`, `time`, `weather`, `shot`, `pause` / `resume` / `step`, the slow three answered later by id.
|
||||
A socket is opened only when `enabled()` says so (a game binds `dev_tools`), a sender off this machine is
|
||||
dropped, and a connected co-op session refuses everything but `ping` and `hello`.
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
bump: patch
|
||||
type: performance
|
||||
**Cut-out edge padding runs on every core.** `tex_dilate`'s passes hand their rows, sixteen at a time, to
|
||||
`Job.parallel_for`: within a pass a row writes only its own still-masked texels and reads only
|
||||
neighbours the mask already let go, so the bytes are the ones the single-threaded loop made. The worker
|
||||
is handed plain buffers in a `DilateJob` and makes nothing. `tex_dilate_bytes` is the slice-taking
|
||||
form (safe_api.ludic), and `examples/rendering/dilate.ludic` holds the result against the old loop
|
||||
(prints DILATE OK). It was 206 ms of the main thread in a Maroon Lake boot.
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
bump: patch
|
||||
type: fix
|
||||
**A dispatched action no longer allocates a record each time.** `dispatch A { ... }` made a fresh
|
||||
record for the queue, and Ludic frees nothing, so a system dispatching every frame (an input's
|
||||
`Move`, a frame's time) grew the program by a record a frame. The queue now keeps a list per
|
||||
action: a dispatch takes the next one (making one only when all are queued), fills every field
|
||||
as `new` would - given, or its default - and `drain_actions()` hands them all back once the queue
|
||||
is empty. A reducer reads its action only during the drain, so nothing sees a record after it is
|
||||
reused; keep what must last in the state, not the action. `ludic.base`'s `actions_test` holds a
|
||||
reused record getting its defaults back.
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
bump: minor
|
||||
type: feat
|
||||
**A name is defined once, for every kind of declaration.** Two functions with one name were
|
||||
already an error; two `var`s or `const`s (or an enum and a const), or two `property` / `event`
|
||||
records, kept the first definition silently. A game lost months to it: two files both said
|
||||
`KEY_LEFT`, one meaning an arrow key's code and one a binding slot, and the menus read the slot.
|
||||
They are now an error that names both files and lines. `examples/rejected/` holds the two cases,
|
||||
checked by a new `reject_case` in the test runner (an example the compiler must refuse).
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
bump: minor
|
||||
type: feature
|
||||
**The built-in ECS grows.** Every component was a fixed array of 1024 slots, so a game past 1024
|
||||
entities could not have them (and until the last release silently corrupted memory trying). The
|
||||
per-entity stores are heap blocks now, doubled by `L_grow` as entities outgrow them, the new slots
|
||||
zero: 100 000 entities spawn and query. `Prop.has` bounds against the live capacity and
|
||||
`Pool.capacity` answers it. A snapshot (`save`/`load`, `world_save`/`world_load`) records its slot
|
||||
count first and a load grows to it before reading the stores back, so a snapshot's size follows the
|
||||
world's instead of a fixed 1024. A mod's registered components grow with the rest.
|
||||
|
|
@ -1,31 +0,0 @@
|
|||
bump: minor
|
||||
type: feat
|
||||
**A schema for editors, and every error as JSON.** `ludicc --emit-schema out.json` (and `ludic
|
||||
schema [file] [-o FILE]`) writes what the compiler resolved once the program type-checks: every
|
||||
record with its fields' types, defaults, doc comments and places; every registry with its record,
|
||||
prefix, resource file, openness and its entries in their final order after the open-registry merge
|
||||
(key, constant, index, file:line:col of the entry and of each field value, and which file brought
|
||||
which entries in); every const; and every function a `fn` value can name, with the `fn_type` a field sees (its states stripped).
|
||||
Deterministic, `"schema_version": 1`. Fields and registries carry editor attributes on the existing
|
||||
`@` syntax - `@Ref(Registry)`, `@OneOf(PREFIX_)`, `@Range(lo, hi)`, `@Unit("m/s")`, `@Asset("gltf")`,
|
||||
`@Color`, `@Node(field)`, `@Clip(field)`, `@Material(field)`, `@Tint(SLOT)`, `@Derived`, `@Text`, `@Multiline`, `@Key`, and `@AppendOnly` / `@ByKey` on
|
||||
a registry - which change nothing but go into the schema; `@Ref` naming no registry is an error, and
|
||||
so is `@Node` / `@Clip` naming a field that is not a glTF (`@Asset("gltf")`, or an `@Ref` to one), and
|
||||
a listing `@OneOf` (`@OneOf(A, B)`, not a prefix `@OneOf(P_)`) naming a constant that does not exist, and `@Tint` naming no constant. A field may now carry several attributes. `ludicc --check
|
||||
--diagnostics=json` (`ludic build --check --diagnostics=json`) prints every error as one JSON array
|
||||
of `{file, line, col, severity, message}` on stdout; tokens and nodes now know their column.
|
||||
|
||||
`Build.schema_hash()` answers FNV-1a 64 of the program's own schema (the bytes `ludic schema` prints),
|
||||
computed only when a program names it, and 0 under `ludicc --release`, which `ludic bundle` now passes.
|
||||
|
||||
A target no part of the program declares (an `@Ref` registry, an `@Tint` or listed `@OneOf` constant) is
|
||||
a warning and `"unresolved": true` in the schema, so a package can name the game's registry; a name of
|
||||
another kind is an error. `@OneOf` on a string field takes words, and every registry row's value is
|
||||
checked against them.
|
||||
|
||||
The schema has a `components` list: each UI component's module, place, doc, template and stylesheet
|
||||
paths, its `props` and `state` (type, default as written, place, doc), `states_read` (the states its
|
||||
header names, apart from its model), `derived` fields with their types, and the `functions` and
|
||||
`events` its template calls with their parameters (states and instance stripped), and the
|
||||
registered native tags its template uses. A `natives` list gives every `ui_native` /
|
||||
`ui_native_input` call with a literal tag: the tag, the function called, its handler and its place.
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
bump: patch
|
||||
type: fix
|
||||
**A function named like an engine namespace method's target is refused where that method is
|
||||
called.** `Random.range` is `rng_range`, so a package's own `rng_range(a, b, c)` silently took
|
||||
every `Random.range(1, 6)` (and the checker then asked for its third argument). It is now an error
|
||||
naming the function, the namespace method and the call.
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
bump: patch
|
||||
type: fix
|
||||
**`expect_eq` on strings compares their text.** It lowered to an integer compare of two pointers,
|
||||
which the IR refused; now two strings with the same text are equal (a null only to a null), and a
|
||||
failure prints both: `expect_eq failed (got "camp", want "lake")`.
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
bump: patch
|
||||
type: fix
|
||||
**`expect_eq` and `expect_near` take floats.** On a float or a double they compared with an integer
|
||||
instruction, and the build failed in clang ("defined with type 'float' but expected 'i32'"); they
|
||||
compare as floats now (the wider kind of the two) and a failure prints the numbers.
|
||||
|
|
@ -1,12 +0,0 @@
|
|||
bump: minor
|
||||
type: feat
|
||||
**`ludic fmt` for editors.** `ludic fmt --lint --json` prints the violations `--lint` reports as one JSON
|
||||
array on stdout, `[{"file", "line", "col", "rule", "message"}]` ordered by file, line and column (the
|
||||
summary on stderr, `--lint`'s exit status, and the baseline never rewritten). `ludic fmt -` formats
|
||||
stdin to stdout under the project found from the working directory (the nearest `package.ludic`
|
||||
upwards), and refuses a buffer that does not read as Ludic - an open string, a bracket never closed or
|
||||
closed by the wrong one - with exit 2 and `<name>:<line>:<col>: error: ...` on stderr.
|
||||
`--stdin-name <path>` makes the buffer that file: the project is found from its directory, and
|
||||
`ludic fmt - --lint --json --stdin-name <path>` judges it against that file's baseline and `lint
|
||||
paths`, reporting it under the name given. Hooks around `fmt` and `get` read nothing from stdin and
|
||||
write to stderr when the command's stdout is a program's (`--json`, `-`).
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
bump: minor
|
||||
type: feature
|
||||
**`friend module lab of fishing, data` - a friend of some modules, not all.** A scoped friend sees
|
||||
the private names of the modules it names and only the exports of every other; `friend module lab`
|
||||
alone still sees everything.
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
bump: minor
|
||||
type: feat
|
||||
**Functions are values (L2).** `fn(int, float) -> bool` is a type, `fn name` is any top-level
|
||||
function's value (it used to be only a thread worker's address), and a call through a local, a
|
||||
global, a record field, a slice element, a parameter or a result of a function type is an
|
||||
indirect call. Two different function types do not mix, a call through one checks its argument
|
||||
count, and a value may be `null`. A registry can hold behaviour and a package can take
|
||||
callbacks. `Job.parallel_for` still checks that its worker takes (int, pointer) and returns
|
||||
nothing. `ludic-dev selfhost-build` now says why it failed instead of exiting 1 silently.
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
bump: patch
|
||||
type: feature
|
||||
**The blades' density window can be filled without a GPU.** grass_density.ludic's gb_frame is split: gb_window_fill
|
||||
fills the GB_TILES x GB_TILES window of density tiles round the camera's (zeros off the map) and sets its corner,
|
||||
and gb_frame sends it. The same bytes as before; a test reads gb_win after gb_window_fill.
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
bump: minor
|
||||
type: feature
|
||||
**Generic records and functions.** `property Pool<T> { items: []T }`, `function first<T>(xs: []T)
|
||||
-> T` and `function map<T, U>(xs: []T, f: fn(T) -> U) -> []U`; a type writes an instance as
|
||||
`Pool<Thing>`, nested as deep as needed. A call's type arguments come from its arguments, or from
|
||||
the declared type its result is written into, and are refused with the parameter named when
|
||||
neither says. Each instance is compiled once as an ordinary record or function. `ludic-fmt` keeps
|
||||
`Pool<Thing>` together while still spacing `a < b`.
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
bump: minor
|
||||
type: feat
|
||||
**`ludic.render3d`: painted ground layers grow solid things, with ids, and the trample is data.**
|
||||
`ground_fill`'s candidate is its own function, `ground_candidate` (pure `gf_*` steps with the density read
|
||||
between them, into a caller-held `GroundCand`), and `ground_fill` draws exactly its answers - the same
|
||||
operations in the same order as before, so every cover layer grows bit for bit what it did. A layer with
|
||||
`solid: true` is filled at `step0` with band 0's hashes whatever the camera, a far band drawing a stable
|
||||
subset; each thing has an int id from (layer, chunk, cell) (`ground_solid_id`), and `ground_solid_list` /
|
||||
`ground_solid_at` answer a chunk's things or one by id for physics, the nav bake and saves.
|
||||
`r3d_ground_clearing(x, z, r_in, r_out, floor)` hands the trample over as discs the editor can see, beside
|
||||
the `r3d_on_ground_trample` callback, which still works. `tests/ground_fill_test.ludic` holds all of it to
|
||||
`tests/ground_fill_golden.json` (written by `tests/gen/ground_fill_golden.ludic`), the file the studio's
|
||||
TypeScript generator is tested against.
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
bump: patch
|
||||
type: fix
|
||||
**`Http.text` and `Http.header` return copies.** They handed back the handle's own buffer (and on macOS the
|
||||
response object's string), which `Http.free` then released: a text read before the free and used after it
|
||||
was garbage or empty - maroon-lake's map list wrote a 0-byte maps.json. Each call now returns a string that
|
||||
is the caller's to keep. Read a body once per response.
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
bump: minor
|
||||
type: feature
|
||||
**English left is an error (phase 26.9).** Under a `lang` line, a template's own words, a text
|
||||
attribute's, a quoted choice that reads as words and a `@Text` row still holding English now refuse
|
||||
the build, where they were warnings; `ludic deps` still counts them as `english_left`. Hole counts and
|
||||
undescribed splits stay warnings. ludic.ui's own words - the key field's "Right click", "Middle
|
||||
click", "Left click" and "press a key..." - are keys (`ui_tk(ui_st, k"ui.right_click", plain)`,
|
||||
`ui.*` in the program's `.po`), with their plain English for a program that binds no translator.
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
bump: patch
|
||||
type: fix
|
||||
**Text keys below a row, padding, and `tr`'s cast.** A `@Text Key` in a record nested in a registry
|
||||
row (and in each item of a list of them) is filled with its derived key, `<registry>.<row>.<field>.<i>.<field>`,
|
||||
as a top-level one is, and a `@Text []Key` a row leaves out takes `<...>.0`, `.1`, ... for as many as
|
||||
the source `.po` has - so no `.lres` spells a key. `field: null` is no text. `trf` / `trn`'s trailing
|
||||
`""` arguments are padding and not counted against the English's holes. And `string(x)` of a string or
|
||||
a `Key` is no allocation to the escape analysis: it is `x` itself, so a `tr(key)` that returns it
|
||||
passes `arena strict` (a template's lone hole still copies).
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
bump: minor
|
||||
type: change
|
||||
**ludic.i18n draws plain text as it is: the English path is gone (phase 26.9).** `L` makes a key, a
|
||||
key glued into text, or a line bracketed inside another; anything else - a player's name, a chat
|
||||
line, a number - is drawn as it is, in every language, so a player named "Settings" stays
|
||||
"Settings". Removed with it: the lookup of English words (exact lines, patterns with holes, a
|
||||
paragraph a sentence at a time, padding), `Ln` (use `trn(kn"...")`), `i18n_pattern_count`, and an
|
||||
English argument's own lookup inside a key's hole. A language `.po` is read for its keys and
|
||||
plurals only. A game still on English msgids draws them untranslated until they are keys.
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
bump: minor
|
||||
type: feature
|
||||
**`ludic.i18n`: keys (phase 26).** A key names what a text is for, and `en.po` says it in English like any
|
||||
other language. A key is a string with a marker byte (`I18N_KEY`, `I18N_PLURAL`), its arguments after
|
||||
byte 31, so the code that makes text never takes `I18nState`: `tr(k)`, `trf(k, a, b, c, d)` and
|
||||
`trn(k, n, a, b, c)` build it, and `L` makes it into text where it is drawn - the language in use, else
|
||||
en.po (read the first time a key is asked for), else the key itself, `[[key]]` in a developer's build
|
||||
(`i18n_loud`). Holes take their arguments in the language's order, a key argument made first; plural
|
||||
keys go by each language's rule. A string with no marker takes the old English path, so a game can
|
||||
move over a file at a time.
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
bump: patch
|
||||
type: fix
|
||||
**A key used only in a listener counts as used.** The key check walked the program's declarations but
|
||||
not the bodies kept beside them - `@On` listeners, the lifecycle hooks, tests, computed fields and
|
||||
scenes - so a key used only there was never checked against en.po and `ludic schema` listed it
|
||||
unused. They are walked now, as every other pass walks them.
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
bump: minor
|
||||
type: feature
|
||||
**Inflate in a caller-owned context.** `ZInflate` holds everything one inflate works in - the bit reader,
|
||||
the RFC's length and distance tables, and every Huffman table and scratch list a block builds - made once
|
||||
by `z_inflate_new()` and rebuilt in place, so an inflate allocates nothing and each thread that inflates
|
||||
holds a context of its own. `z_inflate_in(z, src, len, out, cap)`, `z_uncompress_in` (zlib) and
|
||||
`z_gunzip_in` (gzip) take the context; `z_inflate` / `z_uncompress` / `z_gunzip` keep their signatures,
|
||||
working in the one context `RtInflateState` holds, so no caller changes.
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
bump: patch
|
||||
type: fix
|
||||
**The most negative int is emitted as itself.** The compiler's `itoa` negated a negative value before
|
||||
taking its digits, and `-(-2147483648)` overflows back to itself, so a literal such as `0x80000000` in an
|
||||
int (`b & 0x80000000`) was written into the IR as a bare `-` and the build failed in LLVM. The digits are
|
||||
now taken off the value as it is. Needs a reseed to reach `bin/ludicc`.
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
bump: patch
|
||||
type: fix
|
||||
**`Json.parse` decodes an escaped string in one pass.** A string with escapes in it was joined a character
|
||||
at a time, and every shorter copy was kept: a long escaped text took gigabytes (ui-preview's story model
|
||||
reached 6.5 GB in three seconds). It is now decoded into one buffer sized from the text. ui-preview's
|
||||
protocol-v1.md states how an `@import` path is resolved and which key a `file` override answers to.
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
bump: patch
|
||||
type: fix
|
||||
**`Json.parse` decodes `\uXXXX`.** An escaped code point is UTF-8 now - a surrogate pair joined into one, a
|
||||
lone surrogate or bad hex as U+FFFD - where the backslash was dropped and the hex kept as text ("iu015f"),
|
||||
and `\t`, `\r`, `\b` and `\f` are the characters they name. Python's `json.dump` writes non-ASCII this way.
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
bump: minor
|
||||
type: feature
|
||||
**`Json.write_file(value, path)` saves without keeping anything.** It writes the text
|
||||
`Json.encode` would give straight to the file (through `path.tmp` and a rename), built in a buffer
|
||||
the runtime keeps, so a game that saves often holds nothing more for it. `Json.encode` itself is
|
||||
built the same way and makes only the string it returns: it used to join its text with `+`, keeping
|
||||
every piece and every number's text. `examples/lang/json_saves.ludic`: 1000 saves, 0 bytes.
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
bump: patch
|
||||
type: feature
|
||||
**ludic.lab: 16-bit PNGs** — `lab_png_write16_from(st, path, w, h, channels, px, at)` writes 16-bit samples (two bytes each, most significant first) the way `lab_png_write_from` writes 8-bit ones, so a test can write a height map render3d's decoder reads back as R16; `lp_png_bytes(stride, h)` is the size of one.
|
||||
|
|
@ -1,39 +0,0 @@
|
|||
bump: minor
|
||||
type: feat
|
||||
Three things a crown and a hand light were getting wrong.
|
||||
|
||||
**An aspen's trunk quaked with its leaves.** `layer_flutter`'s mask was the vertex's
|
||||
HEIGHT alone — anything more than 1.2 m up trembled — so on a ten metre aspen the whole
|
||||
bole moved, and a tree that is supposed to shiver read as a column of cloth. What
|
||||
separates a leaf from a bole is not height, it is distance from the model's own centre
|
||||
line: the leaves are out at the edge of the crown and the trunk is on the axis. The mask
|
||||
is radial now, and small plants keep the old one, because a flower is all leaf.
|
||||
|
||||
**Foliage in the camera's face.** A third-person camera walks into a crown and the branch
|
||||
between it and the body is an opaque wall a metre from the lens; in first person it is the
|
||||
whole screen. `#define NEAR_FADE` dithers tree foliage out inside arm's length of the
|
||||
camera — in the depth PREPASS, so the lit pass never sees those pixels either and the
|
||||
equal-depth optimisation is untouched. `gl_Position.w` is the view depth for a perspective
|
||||
projection, so it costs one varying and no uniform. Blades, cards and flowers are left
|
||||
alone on purpose: they live at the player's feet, they are always that close, and fading
|
||||
them opens a hole in the meadow. Nor is the BOLE ever faded: a trunk and its needle cards
|
||||
are one mesh drawn by one program, so the first cut dissolved the trunk into a dither
|
||||
pattern as you walked up to it - a solid tree you can see through reads as a fault, where
|
||||
a branch getting out of your way reads as the camera being polite. The mask is the same
|
||||
signal the quake uses. The shadow pass keeps every leaf, or a tree would stop shading the
|
||||
ground it stands on as you walked up to it.
|
||||
|
||||
**A hand light had the reach of a candle, whatever it was meant to be.** `handLight`'s
|
||||
falloff was an inverse square windowed off between 26 and 6 metres, with both numbers
|
||||
hard-coded: two per cent of its own near field by ten metres out, so carrying a torch at
|
||||
night lit your boots and nothing else. `daylight_hand` takes a `reach` in metres now and
|
||||
the falloff is a gentle power out to it — about nine tenths at a metre, half at half the
|
||||
reach, a tenth at nine tenths of it, nothing past it. A pool of light with a gradient in
|
||||
it rather than a hotspot with a cliff.
|
||||
|
||||
**And the wheel on macOS did nothing, or everything.** `scrollingDeltaY` is a double, and
|
||||
the Cocoa event pump truncated it to an int PER EVENT before accumulating: a trackpad or a
|
||||
Magic Mouse sends a stream of fractions of a line, every one of which truncated to zero, so
|
||||
the wheel was dead; a notched mouse sends three to ten lines at once, so it jumped. The
|
||||
fraction is accumulated now, a precise delta is scaled from points to notches, and the
|
||||
remainder carries to the next frame - one gesture, one step, on both kinds of mouse.
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
bump: patch
|
||||
type: fix
|
||||
**A template literal inside another's `{…}` hole, and integer literals past 2^31 - 1.** The first
|
||||
crashed the compiler (the outer literal ended at the inner one's backtick); a hole is now read as
|
||||
code, so strings, chars and templates inside it are taken whole. The second was read as a wrapped
|
||||
negative int; a decimal literal past `2147483647`, or a hex one of more than eight digits, is now a
|
||||
`long` with its value, and giving one to an `int` is refused. Eight hex digits or fewer are still a
|
||||
32-bit pattern.
|
||||
|
|
@ -1,35 +0,0 @@
|
|||
bump: minor
|
||||
type: feat
|
||||
The air and the light are the hour's, not one constant apiece.
|
||||
|
||||
Aerial perspective is a curve now. A low sun shines through far more air than a high
|
||||
one and shines ALONG the ground rather than down onto it, so density, height falloff
|
||||
and forward scatter all ride the sun's own elevation, and overcast thickens the air
|
||||
while flattening the scatter. The term that was missing entirely is distance
|
||||
DESATURATION: a surface is pulled toward its own luminance faster than the fog itself
|
||||
arrives. Without it, blending a saturated ridge toward a saturated blue noon sky left
|
||||
a saturated ridge, and a midday frame had a mountain three kilometres off reading as
|
||||
vividly as a bench two metres from the camera.
|
||||
|
||||
The grade is the hour's too. White balance, the shadows' floor, contrast and
|
||||
saturation were nine literals bound at the draw; daylight.ludic writes them now.
|
||||
Noon is the case worth naming: direct sun is warm-white and the only thing filling a
|
||||
midday shadow is a blue sky, so noon gets a cool balance over a blue-lifted shadow
|
||||
with hard contrast between it and the lit ground, and dawn and dusk get the reverse.
|
||||
Gain is deliberately left alone - the grade is `c * gain + lift * (1 - c)`, so it
|
||||
warms the whole frame rather than the highlights.
|
||||
|
||||
The visible sky is relit. It is one HDRI turned on its axis so its sun sits where the
|
||||
hour wants it, and turning a photograph does not change what colour it was taken at -
|
||||
so a sunset had a mid-morning blue overhead. An analytic sky supplies the chroma while
|
||||
the photograph keeps the luminance, so the cloud stays where it is and goes orange at
|
||||
dusk, and the zenith goes deep blue at noon, with no second sky shipped. It fades out
|
||||
under the horizon, where the night's own tint, stars and moon take over, and eases off
|
||||
under heavy cloud.
|
||||
|
||||
And the ground bounce follows the ground: the colour a surface is filled with from
|
||||
below is the map's now, crossing from meadow to rock at its treeline, instead of one
|
||||
green constant everywhere including above the scree.
|
||||
|
||||
R3D_NOAIR=1 restores all of it to what it was, so a before-and-after comes from one
|
||||
binary at one hour. Measured at 400 frames: no cost on either backend.
|
||||
|
|
@ -1,32 +0,0 @@
|
|||
bump: minor
|
||||
type: feat
|
||||
Light you can see: sun shafts, and mist that lies in the valley.
|
||||
|
||||
Everything before this made the air a COLOUR APPLIED TO A SURFACE. Nothing put light in
|
||||
the space between surfaces, so a basin at dawn had no shafts, no pooled mist and no rays
|
||||
off a ridge at any hour, however the fog was tuned.
|
||||
|
||||
A half-resolution march from the camera to the depth buffer, asking the SAME shadow the
|
||||
rest of the frame asks - the cascades, the baked height-field shadow and the cloud mask -
|
||||
so a shaft is cast by the actual trees and the actual ridge, and a passing cloud dims its
|
||||
own rays. Henyey-Greenstein scattering, because real air throws light forward, which is
|
||||
why a low sun fills a valley when you look into it and does almost nothing when you look
|
||||
away. Density and a separate ground-hugging MIST layer ride the sun's elevation from
|
||||
daylight.ludic, so the mist forms in the cold at either end of the day and burns off by
|
||||
mid-morning.
|
||||
|
||||
It composites into the HDR scene with the bloom pyramid's own tent upsample under ONE/ONE
|
||||
blending - which is exactly what was wanted and already existed - and before bloom, so a
|
||||
shaft blooms like the bright thing in the air it is. Into post_hdr and not post_scene:
|
||||
post_scene is the copy the water refracts, and shafts added there would sit under the lake.
|
||||
|
||||
The tuning that mattered was the SKY term. It is added at every step, so at 0.06 it
|
||||
accumulated into a flat grey wash that lifted lit and shadowed air by the same amount -
|
||||
which is the contrast a shaft is made of. The valley came out as one pale sheet with no
|
||||
rays in it. At 0.012 the sun's term dominates and there is light to see rather than fog.
|
||||
|
||||
R3D_NOVOL=1 switches it off for an A/B. Off in Settings skips the pass whole rather than
|
||||
marching once, because a feature is off when it costs nothing.
|
||||
|
||||
Measured, 400 frames at 07:00: OpenGL 7.1 -> 7.3 s, Vulkan 7.2 -> 7.4 s. Backends agree
|
||||
to 0.08/255.
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
bump: minor
|
||||
type: feature
|
||||
**`ludic-fmt` enforces a project's style.** `lint` lines in `package.ludic` state it - one statement
|
||||
per line, file, function and comment-block limits (a file's opening comment its own) - and
|
||||
`ludic-fmt --check <files>` and `ludic-fmt --lint` (the project's paths) fail what breaks them, at the
|
||||
line. A baseline file is the ratchet that lets a rule arrive in an existing codebase: the counts a
|
||||
file had may stay but not grow, and they are lowered as they are fixed.
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
bump: patch
|
||||
type: fix
|
||||
**A `return` inside an `@On` listener ends that listener, not the whole dispatch.** Listeners are
|
||||
compiled into one `@ev_<E>` function, and `return` branched to its exit: every listener declared
|
||||
after the one that returned, and every foreign listener, never heard the event. The shape it
|
||||
broke is the common one - a listener per kind that returns early for every kind but its own -
|
||||
where only the first-declared kind was ever answered. `examples/events/answer.ludic` holds it.
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
bump: minor
|
||||
type: feature
|
||||
**`ludic deps` - the module graph as the compiler resolved it.** It compiles the program with
|
||||
`LUDIC_DEPS` set, and the compiler records every reference its visibility pass sees (from module,
|
||||
to module) and every assignment to another module's global. It prints the modules, dependencies,
|
||||
largest cycle, cross-module writes and globals written from outside; `--graph`, `--dot`, `--writes`,
|
||||
`--uses MOD`, `--check FILE` and `--baseline FILE`.
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
bump: minor
|
||||
type: feature
|
||||
**ludic.lab - the visual lab as a package.** A scene is an entry in the open registry `LabScenes`,
|
||||
shown on a plate: a flat lit disc, a small sky of the package's own with the sun at one hour, and a
|
||||
frame clock. Headless, each of its cameras (`lab_shot`, `lab_shot_at`) settles and is written as
|
||||
`build/lab/<scene>/<shot>.png`; in a window N and P step through them. `tools/lab/run.sh` and
|
||||
`sheet.py` make a contact sheet. ludic.render3d gains `r3d_plate_mode(true)` - no terrain, grass or
|
||||
water is made - and `r3d_sky_path`; the example scene takes about 120 MB resident. `quit()` in a
|
||||
program with no frame loop no longer fails to link.
|
||||
|
|
@ -1,35 +0,0 @@
|
|||
bump: minor
|
||||
type: feat
|
||||
**A state machine as data: `@Machine(Record.field)`** (phase 27.1). A registry marked
|
||||
`@Machine(Deer.mood)` is the transitions of a machine over that enum field of the records a state's
|
||||
`Table<Deer>` holds: its record has `from` and `to` (the enum's variants), `on: string` (an action's
|
||||
name, `""` for a transition the tick asks), `guard: fn(Row<Deer>, reads...) -> bool` and
|
||||
`enter: fn(Row<Deer>, reads...) -> void`; the states are the enum's variants and the start is the
|
||||
field's default. Its rows are data - an `.lres` or `def`s - so the studio edits the machine as a graph.
|
||||
|
||||
- **The compiler writes the machine.** For each action an `on` names, a row reducer in the
|
||||
registry's file (`reducer Deer in Herd.deer on Spook`, beside any the program writes, after them):
|
||||
the row's state, the first transition from it on that action whose guard passes, the field set,
|
||||
`enter` run. When a row leaves a state on a guard alone, `state DeerStepsMachine` (the kept row
|
||||
view) and `deer_steps_tick(m: mut DeerStepsMachine, s: mut Herd, reads...)`, one transition a row
|
||||
a tick, called from the program's system. Guards and enters are called by name; nothing allocates.
|
||||
- **The table is the whole machine.** The field is written by nothing else: an assignment to it or
|
||||
a `machine` block's `become` over it outside the written code is a type error (`Deer.mood is the
|
||||
machine DeerSteps's (@Machine(Deer.mood)) - it changes only by a transition in its table`); a new
|
||||
row takes its state in its `new`. A guard and an enter take the row first, are the record's
|
||||
module's, and keep a row reducer's rules (`r.rec` and `r.h` only, a `@Column` field only through a
|
||||
`@RowVerb`); a guard writes nothing through its row.
|
||||
- **The graph is checked**, each an error naming its row: a state never reached from the start, a
|
||||
state with no way out, an `on` naming no action or an action with no `@Target`, a self-transition
|
||||
with no guard, and two ways out of a state on one trigger behind an unguarded first. Also refused:
|
||||
`@Machine` on anything but a registry, a field that is not a plain enum with a default, a
|
||||
`@Column` field, no table (or two) of the record, a transitions record of another shape, and a
|
||||
machine outside its table's state's module.
|
||||
- **`ludic schema`'s code section** gains `machines` (`registry`, `record`, `field`, `enum`, `table`,
|
||||
`start`, `states`, `actions`, `tick`, `module`, `at`); the registry carries `@Machine` among its
|
||||
attributes. `ludic deps` names a machine's reducer `reducer Deer in Herd.deer on Spook (machine
|
||||
DeerSteps)`. vocab: `@Machine`; docs `annot-machine`; LANGUAGE.md "A machine as data".
|
||||
- Examples `actions/machine` (with `deer_steps.lres`); rejects for an unreachable state, no way out,
|
||||
an unknown action, an action with no row, a self-transition with no guard, an ambiguous trigger, a
|
||||
guard that writes, the field written by hand, `@Machine` off a registry and a field that is no
|
||||
enum. The `machine` block stays for a machine that is only code.
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
bump: minor
|
||||
type: feature
|
||||
**Map-scoped tables: `@PerMap` and `@Chunked(n)` registries** — a registry whose rows live in each map's directory and are read when the map loads, or a chunk at a time.
|
||||
|
||||
`@PerMap @ByKey registry Props of PropRow from "props.lres"` reads `<maps root>/<map>/props.lres` (the root is `package.ludic`'s new `maps "assets/maps"` line), and `@PerMap @Chunked(64) registry Instances of InstRow from "instances/{cx}_{cz}.lres"` one file per chunk. The compiler writes the table's `state` and its verbs in the declaring module (`props_load`, `props_clear`, `props_find`, `props_path`; `instances_in`, `_out`, `_slot`, `_find`, `_clear`, `_path`) and a typed fill over a small runtime `.lres` reader (`runtime/native/lres.ludic`, spliced on demand): a row names any int or float constant by name (resolved at load), a `fn` value by name, nested records and lists. Rows, their lists and the records in them are pooled and refilled in place, and `_find` is an allocation-free hash, so a load allocates nothing past the table's high water and a frame may bring a chunk in. `@Ref(T)` into a map table is a string key, checked against the same map. `ludicc --check` type-checks every map directory's tables against their records with file:line:col diagnostics (`--no-maps` to skip). The schema gives each registry a `"scope"` (`"map"` / `"game"`) and `"chunk"`, and a map `@Ref` `"scope": "map"`; `@Unit` now has canonical ASCII spellings (`deg`, `m/s2`, `N.m`, ...), listed as the schema's `"units"`, and any other spelling is a warning naming the right one.
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
bump: patch
|
||||
type: fix
|
||||
**`ludic build --check` reads the maps only for the package's entry.** A partial program - a unit test, a
|
||||
molecule, a bake's runner - lacks the game's constants, so checking every map's tables against it refused
|
||||
rows the game reads fine (`TH_*`, `NPCK_*` unknown). The maps are now checked with the `entry` package.ludic
|
||||
names (as `ludic build --check` with no file, and tests/pack.sh, do); `--maps` checks them anyway, `--no-maps`
|
||||
never. `ludicc` itself is unchanged: it reads them unless told `--no-maps`.
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
bump: patch
|
||||
type: fix
|
||||
**A `Math.*` call has a type the checker knows.** `Math.max`, `Math.sqrt` and the rest are computed
|
||||
inline by the emitter, and the checker gave every one of them the unknown type, which agrees with
|
||||
everything - so `dispatch Notify { a1: Math.max(5, n) }` put an int into a string field, passed L4
|
||||
and failed in LLVM (`%t63 defined with type i32 but expected ptr`). The checker now gives each the
|
||||
type the emitter does: a float or double first argument is that type (`sign` an int); otherwise
|
||||
`min`, `max`, `abs` and `clamp` keep the first argument's type, `sign`, `floor`, `ceil`, `round`,
|
||||
`posmod`, `wrap` and `ping_pong` are ints and the rest fixed. The bare `min` / `max` / `abs` /
|
||||
`clamp` builtins the same. Where the arguments cannot be told, the result stays unknown.
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
bump: minor
|
||||
type: feature
|
||||
**A loaded model can be let go of: `model_release(model)`.** Its meshes are freed at once, and
|
||||
each texture once no other model uses it - the glTF loader shares textures between models (one
|
||||
path loaded once, a LOD chain borrowing its LOD0's material), so every cached texture now counts
|
||||
the primitives using it, and the last one frees it and forgets it, with any remembered material
|
||||
that named it. A game can unload a species' kit or a map's models when nothing of them is left,
|
||||
and loading one again afterwards is a fresh load. A scatter layer takes room for what it holds
|
||||
instead of its whole capacity up front (it doubles as it fills, to its cap), which in Maroon Lake
|
||||
was 0.4 GB of never-filled instance arrays. `examples/rendering/release.ludic` is the check.
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
bump: minor
|
||||
type: feature
|
||||
**Layers: `module flow in layer app uses base, items`.** The modules of one layer use each other
|
||||
freely and may go round; anything outside the layer is still held to the module's `uses` (a
|
||||
layered module with no `uses` reaches nothing outside its layer), and a cycle is refused unless it
|
||||
stays inside one layer. `ludic deps` shows the layers and counts the largest cycle without the edges
|
||||
inside them, printing the count with them beside it.
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
bump: minor
|
||||
type: feature
|
||||
**Modules.** `module NAME` in a barrel makes its directory a module, and a module's
|
||||
declarations are private to it unless they say `export`: a function, global, record or event of
|
||||
another module used without one is an error naming the module and where to mark it. `friend
|
||||
module NAME` sees everything (a test harness), a file in no module is public, and a package keeps
|
||||
its own module. `LUDIC_VIS_REPORT=1` lists every violation instead of stopping, so an existing
|
||||
program can be given its exports by a script before the rule applies to it.
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
bump: minor
|
||||
type: feature
|
||||
**`module fishing uses base, data` - a module declares what it may reach, and the compiler holds it
|
||||
to that.** A reference from a module that says `uses` into a module it does not name is refused,
|
||||
exported or not, with the use and the fix in the message ("fishing uses items.inv_add (...): add
|
||||
'uses items' to fishing's module line, or take it through a port"). A module with no `uses` keeps
|
||||
the old rule, a package's module is always usable, a friend is not held to it, a cycle in the
|
||||
declared graph is refused, and `LUDIC_VIS_REPORT=1` lists the violations as `uses:` lines.
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
bump: patch
|
||||
type: fix
|
||||
**A frame that draws more than any before no longer grows the heap on the Mac.** MoltenVK's command
|
||||
pooling kept every command object a frame had ever recorded - about 650 bytes for each draw beyond the
|
||||
busiest frame so far, for as long as the game ran. render3d turns it off
|
||||
(`MVK_CONFIG_USE_COMMAND_POOLING=0`, unless the environment already says otherwise) before the first
|
||||
Vulkan call; the objects are made and freed with their command buffer, at no measured cost.
|
||||
`examples/rendering/steady.ludic` ramps a frame from 20 to 200 actors and fails on what pooling left.
|
||||
A buffer written through its mapping is also read once at the start of the next frame's commands,
|
||||
so MoltenVK makes its Metal buffer then rather than at its first draw, however much later that is.
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
bump: minor
|
||||
type: feat
|
||||
**A package can carry a native library.** `native "<target>" "<path>"` in a package's
|
||||
`package.ludic` names a C/C++ library per target (`macos-arm64`, `windows-x64`, ...). The
|
||||
compiler records the libraries of every package a program imports, and every link - `ludicc -o`,
|
||||
`ludic build`, `ludic test`, `ludic bundle` - links them: on macOS with an rpath to the package
|
||||
and to `Contents/Frameworks`, where `ludic bundle` places and signs them; on Windows through the
|
||||
import library, the `.dll` copied beside the executable. `tools/native/lib.sh` builds a library
|
||||
from a pinned, checksummed source; `ludic.nativeecho` is the worked example, and
|
||||
`packages/README.md` says what a shim may pass across.
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
bump: patch
|
||||
type: feat
|
||||
**A native library's licence ships with it.** `ludic bundle` copies each linked package's
|
||||
`native/LICENSE*` files beside the `.exe` on Windows and into `Contents/Resources` on macOS, so
|
||||
Jolt Physics' MIT notice travels with every copy of the game. `tools/native/lib.sh` passes lld-link
|
||||
`-implib`, which Git Bash leaves alone (it rewrote `/implib:` into a Windows path).
|
||||
|
|
@ -1,4 +0,0 @@
|
|||
bump: patch
|
||||
type: fix
|
||||
**The foliage's near-fade programs have SPIR-V.** The three `NEAR_FADE` variants were missing
|
||||
from `variants.list`, so on Vulkan the trees' and cards' programs failed to build.
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
bump: minor
|
||||
type: feat
|
||||
**A `numbers float` file is strict about where its numbers come from.** Two rules, both found
|
||||
converting a renderer and a game from float bits in an `int` to real floats:
|
||||
|
||||
- A decimal literal takes the kind of what it meets. `factor == 0.0` against a `fixed` parameter,
|
||||
`half(3.0)` into a `fixed` one and `var z: fixed = -1.5` are `fixed` literals there, rather
|
||||
than a `fixed` and a `float` that do not mix.
|
||||
- A computed integer never becomes a float implicitly. `count < limit` with an `int` on one side
|
||||
and a `float` on the other is an error that asks for `float(x)`: in a file whose numbers are
|
||||
floats, an `int` meeting one is almost always raw bits, and promoting it compares the bits as a
|
||||
number. It caught a uniform fed an integer as float bits, and a test comparing a tint's bits
|
||||
with its value. Integer constants still promote, and `float(x)` is always allowed.
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
bump: minor
|
||||
type: feat
|
||||
**`numbers float` — a module whose decimal literals are `float`.** A bare `1.5` is `fixed`
|
||||
unless a float is in sight, which is the right default for deterministic code and the wrong
|
||||
one for a renderer or a game whose numbers are all IEEE: `Math.sqrt(2.0)` came out in `fixed`,
|
||||
and every literal needed a `float(...)` round it. A file that says `numbers float` (at the top,
|
||||
or inside a `program` block) gets float literals, and passes the mode on to every non-runtime
|
||||
file it imports, so one line in a barrel or an entry file covers a whole package. Runtime files
|
||||
under `runtime/` are never switched, so the engine's own fixed-point code keeps its meaning.
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
bump: patch
|
||||
type: fix
|
||||
**One MoltenVK in a process.** On a Mac with the Vulkan SDK installed, the runtime opened the SDK's loader
|
||||
(/usr/local/lib/libvulkan.1.dylib) before MoltenVK, and the loader loaded the SDK's own MoltenVK as its
|
||||
driver - beside ludic.render3d's, which every program is linked against: two copies, each with its pools,
|
||||
and the program drew through the SDK's ("MVKBlockObserver is implemented in both"). MoltenVK is opened
|
||||
first now (dlopen hands back the linked copy), and the loader only when a layer is asked for
|
||||
(VK_INSTANCE_LAYERS, VK_LOADER_LAYERS_ENABLE, R3D_VK_LOADER) - then pinned to ludic.render3d's MoltenVK
|
||||
through VK_DRIVER_FILES and lib/macos-arm64/MoltenVK_icd.json, unless the caller named a driver.
|
||||
examples/rendering/steady.ludic's stream round warms up over 300 cells and takes the least of three
|
||||
windows of 150, as the frame round does: one sample of it read +75 KB on a run whose twin read -5 KB.
|
||||
|
|
@ -1,25 +0,0 @@
|
|||
bump: minor
|
||||
type: feat
|
||||
One wind, and everything in the valley is moved by it.
|
||||
|
||||
The grass had a gust built from two sines and the trees had NO gust term at all - just a
|
||||
per-instance wobble - so the meadow rippled and the canopy above it swayed to an unrelated
|
||||
rhythm and nothing ever crossed the valley. A real gust is a WAVE: you watch it come over
|
||||
the grass, it reaches you, and it goes on into the trees behind you.
|
||||
|
||||
wind.glsl is prepended to EVERY stage, so the grass's vertex shader, the crown's vertex
|
||||
shader and the water's fragment shader read the same field at the same world position and
|
||||
cannot drift apart. No uniforms are declared in it on purpose - u_time and u_wind already
|
||||
exist in several of those files and redeclaring them is a compile error in whichever stage
|
||||
includes both - so the caller passes what it already has.
|
||||
|
||||
Two numbers mattered. The gust's WAVELENGTH has to fit inside a view or nothing is ever
|
||||
seen to travel: the first cut was 209 m crest to crest, longer than the meadow you can see,
|
||||
so the whole frame sat in one phase and read as everything breathing together. It is 74 m
|
||||
now and a front crosses a normal view in a couple of seconds. And `stiff` lets one field
|
||||
move a blade a long way and a bole hardly at all.
|
||||
|
||||
R3D_DEBUG_WIND=1 paints the field on the ground, which is the only way to SEE a gust in a
|
||||
still: two shots a moment apart show the fronts and show them moving.
|
||||
|
||||
Measured: no cost. 400 frames, GL 6.9 s, VK 7.2 s.
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
bump: minor
|
||||
type: feature
|
||||
**`export open registry Items of Item` - a registry other modules may add to, in a stable order.**
|
||||
A `def` from another module into a registry that is not open is refused, and a def now goes
|
||||
through visibility like any reference (the registry exported, its module in the definer's `uses`).
|
||||
An open registry's index order is the declaring module's entries first, then every other module's
|
||||
by module name, each in the order it is read - independent of the order a barrel imports them in.
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
bump: patch
|
||||
type: fix
|
||||
**`Os.platform()` and `Os.arch()` allocate nothing after the first call.** Each call malloc'd an
|
||||
8 KB `uname` buffer and let it go, and a game asks the platform every frame in places (a launcher's
|
||||
wait, an update panel, a renderer's present). The buffer is made once and kept.
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
bump: patch
|
||||
type: fix
|
||||
**A name that meets a package's export says whose it is; ludic.ui's `UiAct` is its own.** A game's
|
||||
`UiAct` collided with ludic.ui's, which no program uses - it is private to ludic.ui now, so the game
|
||||
may have one. A real clash - a type named like one a package exports - is still refused, and the
|
||||
message names the package and the way out: `'UiNode' is defined twice (...): ludic_ui exports it, and
|
||||
exported names are one namespace - rename this one, or declare it without export inside a module of
|
||||
your own`.
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
bump: minor
|
||||
type: feat
|
||||
**`ludic.physics`: Jolt Physics as a package.** Shapes (box, sphere, capsule, cylinder, a
|
||||
boulder's dome, a heightfield, a mesh), still, kinematic and dynamic bodies that can be removed
|
||||
for real, rays, a foot's landing height, a push out of what a body stands in, overlaps, buoyancy
|
||||
against a `PhysWater` port with its current, and hard contacts and splashes as facts - stepped at a
|
||||
fixed 1/60 s, deterministic across machines. Jolt v5.6.0 is built here from its pinned tag
|
||||
(`native/build.sh`) through the phase-15 route.
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
bump: patch
|
||||
type: fix
|
||||
**A validation run sees the game's MoltenVK.** With a layer asked for, the loader was pinned to
|
||||
ludic.render3d's MoltenVK only when the shell named no driver - but the SDK's setup-env.sh exports both
|
||||
VK_DRIVER_FILES and VK_ICD_FILENAMES at the SDK's own MoltenVK, so a shell with one left set loaded that
|
||||
one beside ours. The pin now overwrites VK_DRIVER_FILES and clears VK_ICD_FILENAMES; R3D_VK_ANY_DRIVER=1
|
||||
keeps the shell's driver for a run that means to try another.
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
bump: minor
|
||||
type: feat
|
||||
**`ludic remove <module>` and `ludic get --json`.** `remove` is the inverse of `add`: the `require`
|
||||
leaves `package.ludic`, the lock keeps what the remaining requires still reach (read from the store's
|
||||
copies of each package's manifest, no network), and each package leaving the lock loses the
|
||||
`ludic_modules/` link `add` made - never the shared store entry. A module that is not required is
|
||||
refused; source still importing a removed package is a warning. `ludic get --json` prints what the run
|
||||
changed in the lock as one JSON object on stdout - `{"added", "removed", "changed", "unchanged"}`, an
|
||||
entry as the lock records it (`name`, `version`, `hash`, `kind`, `provides`), a change as `{name, from,
|
||||
to, from_hash, to_hash}` - with the resolver's own lines on stderr.
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
bump: minor
|
||||
type: feature
|
||||
**Ports: `port Clock { now: fn() -> int, day: fn() -> int = fn first_day }` in a module and
|
||||
`bind Clock { now: fn game_hours }` where the program is put together.** A module asks for what it
|
||||
needs through `Clock.now()` without naming - or `uses`-ing - the module that answers. A port used
|
||||
and never bound, a bind that leaves out a member with no default, a member the port does not have
|
||||
and a second bind are refused; the binder must see the (exported) port.
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
bump: minor
|
||||
type: feature
|
||||
**A module's private records and events are its own.** A `property` or `event` a module does not
|
||||
export no longer collides with the same spelling in another module: each private one is compiled
|
||||
under its module's name and its module's types, `new`, `emit` and `@On` follow it. Exported ones are
|
||||
one namespace, and two events of one spelling are now refused (the first used to take the other's
|
||||
emits silently). Generic records, entity components and component or view records stay global.
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
bump: minor
|
||||
type: feature
|
||||
**A module's private names are its own.** A function, var or const a module does not export no
|
||||
longer collides with the same spelling in another module, in a file in no module or in the
|
||||
runtime: each private one is compiled under its module's name (`seed$shop`) and its module's
|
||||
references follow it. Exported names are still one namespace, and two exports of one name are
|
||||
still refused. Records and events still share one namespace. A program with no such clash compiles
|
||||
exactly as before.
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
bump: patch
|
||||
type: fix
|
||||
**`ludic migrate state --prune` keeps a state declared after a plain parameter, and a parameter named twice is refused.**
|
||||
A function like `home_keep(r: Records, save_st: mut Save)` was taken to lack `save_st`, and the
|
||||
prune gave it a second one at the front (`home_keep(save_st: mut Save, r: Records, save_st: mut
|
||||
Save)`) - which `ludic build --check` let through and clang then refused. The migration now counts a
|
||||
state anywhere in the list as declared, and the checker refuses a function that names two parameters
|
||||
alike (`add names two parameters n`).
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
bump: minor
|
||||
type: feat
|
||||
**A queue keeps its own count, so a package verb takes only its own state; `ludic migrate state --prune`.**
|
||||
ludic.base's `Queue<T>` carries its name and pending count itself: `queue_new(name)`, `q_push(q, v)`,
|
||||
`q_drain(q)`, `q_clear(q)` take no `BaseState`, and neither does any package verb that pushes a fact -
|
||||
`wallet_earn(wallet_st, n)`, `inv_add(inventory_st, ...)` - so a reducer on a package's state can call
|
||||
its own package's verbs. `core_undrained(tags)` names the queues given to it (`q_tag(q)`) that still
|
||||
hold facts. `ludic migrate state --prune` takes out each state parameter a function no longer uses
|
||||
(nor anything it calls) and the argument that fills it, including an argument for a parameter the
|
||||
callee has dropped: run it over a game to follow this change.
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue