Compare commits
No commits in common. "main" and "v0.5.2" have entirely different histories.
2198 changed files with 87843 additions and 790347 deletions
|
|
@ -8,13 +8,13 @@ Closes #
|
|||
|
||||
## Checklist
|
||||
|
||||
- [ ] `bin/ludic-dev test` passes.
|
||||
- [ ] For compiler/runtime changes: `bin/ludic-dev reseed && bin/ludic-dev bootstrap-cfree`
|
||||
- [ ] `bin/ludic dev test` passes.
|
||||
- [ ] For compiler/runtime changes: `bin/ludic dev reseed && bin/ludic dev bootstrap-cfree`
|
||||
still reaches the self-hosting fixpoint with no C compiler in the loop.
|
||||
- [ ] `ludic-fmt` leaves the touched files unchanged (2-space, LF, UTF-8).
|
||||
- [ ] New/changed stdlib symbols are documented under `docs/language/**` and
|
||||
registered in `tools/docgen/inventory.json`
|
||||
(`bin/ludic-dev docs-gen && bin/ludic-dev docs-check build/pages` passes).
|
||||
(`bin/ludic dev docs-gen && bin/ludic dev docs-check build/pages` passes).
|
||||
- [ ] Commits follow [Conventional Commits](https://www.conventionalcommits.org).
|
||||
- [ ] No new C / Python / JS in tooling (Ludic only), and no generated
|
||||
artifacts committed outside `build/` / `bin/`.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -43,11 +43,11 @@ jobs:
|
|||
set -eu
|
||||
mkdir -p bin
|
||||
clang-16 tools/ci/linux_stdio_shim.ll selfhost/ludicc.seed.ll -o bin/ludicc
|
||||
bin/ludicc tools/ludic-cli/dev.ludic -o bin/ludic-dev
|
||||
bin/ludicc tools/ludic-cli/main.ludic -o bin/ludic
|
||||
|
||||
- name: Rebuild the compiler from the seed and assert byte-identity
|
||||
# `ludic-dev bootstrap-cfree` assembles the seed with clang, has that seed
|
||||
# `ludic dev bootstrap-cfree` assembles the seed with clang, has that seed
|
||||
# compiler recompile selfhost.ludic to out.ll, and `cmp`s out.ll against
|
||||
# the checked-in seed. It returns non-zero if they differ — i.e. if the
|
||||
# seed is stale relative to the compiler source.
|
||||
run: bin/ludic-dev bootstrap-cfree
|
||||
run: bin/ludic dev bootstrap-cfree
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ name: ci
|
|||
|
||||
# Build the language toolchain from its IR seed and run the regression suites on
|
||||
# every push to main and every pull request. Until this landed the only workflow
|
||||
# was docs.yml, so nothing gated a change on `ludic-dev test` / `ludic-dev test-tools` or on the
|
||||
# was docs.yml, so nothing gated a change on `ludic dev test` / `ludic dev test-tools` or on the
|
||||
# compiler even building from the seed. See also bootstrap.yml, which proves the
|
||||
# C-free self-rebuild reproduces the seed byte-for-byte.
|
||||
on:
|
||||
|
|
@ -45,10 +45,10 @@ jobs:
|
|||
# The toolchain is macOS-first; on this Linux runner it links against a
|
||||
# tiny C-free IR shim that supplies the Darwin standard-stream globals
|
||||
# (__stdoutp/__stderrp) over glibc's stdout/stderr. Injected through
|
||||
# LUDIC_CC so every clang invocation — the seed bootstrap, `ludic-dev build`,
|
||||
# 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)
|
||||
|
|
@ -60,20 +60,20 @@ jobs:
|
|||
# pre-built binaries: the language builds itself from source + seed.
|
||||
mkdir -p bin
|
||||
clang-16 tools/ci/linux_stdio_shim.ll selfhost/ludicc.seed.ll -o bin/ludicc
|
||||
bin/ludicc tools/ludic-cli/dev.ludic -o bin/ludic-dev
|
||||
bin/ludic-dev build
|
||||
bin/ludicc tools/ludic-cli/main.ludic -o bin/ludic
|
||||
bin/ludic dev build
|
||||
|
||||
- name: Regression suite (ludic-dev test)
|
||||
run: bin/ludic-dev test
|
||||
- name: Regression suite (ludic dev test)
|
||||
run: bin/ludic dev test
|
||||
|
||||
- name: Editor-toolchain suite (ludic-dev test-tools)
|
||||
- name: Editor-toolchain suite (ludic dev test-tools)
|
||||
# Grammar/lexer/vocabulary sync, ludic-fmt idempotence (the project's
|
||||
# formatting contract — hand alignment is deliberately preserved, so the
|
||||
# gate is fmt(fmt(x)) == fmt(x), not fmt(x) == x), and the JSON/XML editor
|
||||
# assets. Cross-file LSP behaviour and the golden renders are macOS-ABI
|
||||
# bound and skip here — visibly — until the runtime's directory walk and
|
||||
# windowing are portable.
|
||||
run: bin/ludic-dev test-tools
|
||||
run: bin/ludic dev test-tools
|
||||
|
||||
- name: Docs cover the implementation
|
||||
run: |
|
||||
|
|
@ -81,9 +81,9 @@ jobs:
|
|||
# The whole docs toolchain is written in Ludic and runs through x —
|
||||
# no Python anywhere. check-impl / check-vocabulary / check-docs guard
|
||||
# the sources; docs-gen builds the site and docs-check is its coverage
|
||||
# + integrity guard. (check-vocabulary also runs in `ludic-dev test-tools`.)
|
||||
bin/ludic-dev check-impl
|
||||
bin/ludic-dev check-vocabulary
|
||||
bin/ludic-dev check-docs
|
||||
bin/ludic-dev docs-gen --out build/pages
|
||||
bin/ludic-dev docs-check build/pages
|
||||
# + integrity guard. (check-vocabulary also runs in `ludic dev test-tools`.)
|
||||
bin/ludic dev check-impl
|
||||
bin/ludic dev check-vocabulary
|
||||
bin/ludic dev check-docs
|
||||
bin/ludic dev docs-gen --out build/pages
|
||||
bin/ludic dev docs-check build/pages
|
||||
|
|
|
|||
|
|
@ -11,10 +11,6 @@ on:
|
|||
- 'docs/**'
|
||||
- 'tools/docgen/**'
|
||||
- 'tools/ludic-cli/**'
|
||||
# the site publishes the installer, so a change to it has to redeploy the
|
||||
# site — otherwise a fixed install.sh sits in main while the old one is
|
||||
# still what `curl … | sh` fetches
|
||||
- 'install.sh'
|
||||
- '.forgejo/workflows/docs.yml'
|
||||
workflow_dispatch: {}
|
||||
|
||||
|
|
@ -38,7 +34,7 @@ jobs:
|
|||
runs-on: docker
|
||||
# The generator is now Ludic, so this builds the toolchain from its IR seed
|
||||
# (clang assembles the seed into bin/ludicc, which compiles bin/ludic) exactly
|
||||
# like the ci workflow, then runs `ludic-dev docs-gen`. node:20-bookworm carries git
|
||||
# like the ci workflow, then runs `ludic dev docs-gen`. node:20-bookworm carries git
|
||||
# for the clone + publish; clang-16 is the only extra the bootstrap needs.
|
||||
container: node:20-bookworm
|
||||
steps:
|
||||
|
|
@ -63,13 +59,13 @@ 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
|
||||
bin/ludicc tools/ludic-cli/dev.ludic -o bin/ludic-dev
|
||||
bin/ludic-dev docs-gen --out ../public
|
||||
bin/ludic-dev docs-check ../public
|
||||
bin/ludicc tools/ludic-cli/main.ludic -o bin/ludic
|
||||
bin/ludic dev docs-gen --out ../public
|
||||
bin/ludic dev docs-check ../public
|
||||
cd ..
|
||||
echo "--- generated files ---"
|
||||
ls -la public
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
name: release
|
||||
|
||||
# Cutting a release is `ludic-dev release` + `git push --tags`; everything after that
|
||||
# Cutting a release is `ludic dev release` + `git push --tags`; everything after that
|
||||
# happens here. Before this workflow existed the artifacts were built on whatever
|
||||
# machine the maintainer happened to be sitting at, from whatever was in bin/ at
|
||||
# the time, with no checksums and nothing proving the tagged tree even passed its
|
||||
|
|
@ -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
|
||||
|
|
@ -71,15 +71,15 @@ jobs:
|
|||
set -eu
|
||||
mkdir -p bin
|
||||
clang-16 tools/ci/linux_stdio_shim.ll selfhost/ludicc.seed.ll -o bin/ludicc
|
||||
bin/ludicc tools/ludic-cli/dev.ludic -o bin/ludic-dev
|
||||
bin/ludic-dev build
|
||||
bin/ludicc tools/ludic-cli/main.ludic -o bin/ludic
|
||||
bin/ludic dev build
|
||||
|
||||
- name: The tagged tree must pass its own suites
|
||||
run: |
|
||||
set -eu
|
||||
bin/ludic-dev test
|
||||
bin/ludic-dev test-tools
|
||||
bin/ludic-dev bootstrap-cfree
|
||||
bin/ludic dev test
|
||||
bin/ludic dev test-tools
|
||||
bin/ludic dev bootstrap-cfree
|
||||
|
||||
- name: Publish the release
|
||||
env:
|
||||
|
|
@ -91,9 +91,9 @@ jobs:
|
|||
echo "::error::No FORGEJO_TOKEN secret; cannot create the release."
|
||||
exit 1
|
||||
fi
|
||||
# ludic-dev publish builds dist/ (source tarball from the tag, this host's
|
||||
# ludic dev publish builds dist/ (source tarball from the tag, this host's
|
||||
# toolchain, SHA256SUMS), takes the notes from the CHANGELOG section,
|
||||
# and creates the release. Re-running it only adds missing assets, so
|
||||
# a maintainer can afterwards attach the macOS toolchain from a Mac
|
||||
# with the same command.
|
||||
bin/ludic-dev publish "$TAG"
|
||||
bin/ludic dev publish "$TAG"
|
||||
|
|
|
|||
1
.gitattributes
vendored
1
.gitattributes
vendored
|
|
@ -1 +0,0 @@
|
|||
packages/*/lib/** filter=lfs diff=lfs merge=lfs -text
|
||||
21
.gitignore
vendored
21
.gitignore
vendored
|
|
@ -1,5 +1,5 @@
|
|||
# Generated build tree: LLVM IR, objects, compiled apps, the headless render
|
||||
# (build/out.ppm) and the docs site all land under build/ (see `bin/ludic-dev build` /
|
||||
# (build/out.ppm) and the docs site all land under build/ (see `bin/ludic dev build` /
|
||||
# `bin/ludic clean`). Root-anchored so a source dir named "build" elsewhere is never
|
||||
# accidentally ignored. Nothing is written to the repo root any more.
|
||||
/build/
|
||||
|
|
@ -9,8 +9,8 @@
|
|||
# packaged plugin .zip are local-only build inputs/outputs.
|
||||
*.zip
|
||||
|
||||
# the toolchain binaries (ludicc, ludic, ludic-dev, ludic-fmt, ludic-lsp) — all built
|
||||
# into bin/ by the one-line bootstrap + `bin/ludic-dev build`; never checked in. The
|
||||
# the toolchain binaries (ludicc, ludic, ludic-fmt, ludic-lsp) — all built
|
||||
# into bin/ by the one-line bootstrap + `bin/ludic dev build`; never checked in. The
|
||||
# only thing published is the source and the LLVM-IR seed (selfhost/ludicc.seed.ll).
|
||||
/bin/
|
||||
|
||||
|
|
@ -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/
|
||||
|
|
@ -40,7 +39,7 @@ tools/editors/jetbrains/.kotlin/
|
|||
__pycache__/
|
||||
*.pyc
|
||||
|
||||
# Release artifacts produced by `ludic-dev release`
|
||||
# Release artifacts produced by `ludic dev release`
|
||||
/dist/
|
||||
|
||||
# Build/release tarballs anywhere in the tree. `git -C <repo> archive -o foo.tgz`
|
||||
|
|
@ -48,15 +47,3 @@ __pycache__/
|
|||
# archive lands in the root and a blanket `git add -A` will commit it.
|
||||
*.tar.gz
|
||||
*.tgz
|
||||
|
||||
# The CC0 Poly Haven downloads are fetched, not committed (`ludic-dev fetch-assets`
|
||||
# reads the manifest that ships with the renderer, packages/ludic.render3d/assets.manifest,
|
||||
# so a game outside this repository fetches the same set with `ludic assets`).
|
||||
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/
|
||||
|
|
|
|||
923
CHANGELOG.md
923
CHANGELOG.md
|
|
@ -7,929 +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
|
||||
|
||||
- **A game can use the renderer from outside this repository.** `ludic.render3d` reads two
|
||||
things from disk at run time — its GLSL, and the scanned CC0 materials — and both were
|
||||
found only by a path relative to the working directory, so the renderer worked in a
|
||||
Ludic checkout and nowhere else. A game living in its own repository now needs to copy
|
||||
neither.
|
||||
|
||||
**The shaders come from the package**, wherever the package is. They belong to
|
||||
`ludic.render3d` and ship with it, so the renderer looks for them beside the project
|
||||
first (a Ludic checkout, where they are under `packages/`) and then under the install
|
||||
root, `$LUDIC_HOME/packages/ludic.render3d` — the same place the compiler already
|
||||
resolves `import "ludic.render3d/r3d.ludic"` from. Nothing to vendor, and no version of
|
||||
the shaders that can drift from the version of the code that compiles them.
|
||||
|
||||
**`ludic assets [--force]`** fetches the scanned materials and the HDRI sky into
|
||||
`assets/polyhaven/` of whatever project you run it in. The list of what to fetch is the
|
||||
renderer's own — the renderer decides which materials it wants — so it moved out of the
|
||||
repository's `assets/` and into the package as
|
||||
`packages/ludic.render3d/assets.manifest`, where it ships with the toolchain. A game
|
||||
does not keep its own copy of that list and so cannot fall out of step with the
|
||||
renderer's material set. `ludic dev fetch-assets` is the same command from a checkout.
|
||||
|
||||
**A URL-shaped module built its binary into directories.** `project_name` took
|
||||
everything after the last dot of the manifest's module path, which for a package
|
||||
identified the way the package manager identifies them —
|
||||
`git.host.io/user/name` — is inside the *host*: the build wrote
|
||||
`build/io/user/name` instead of `build/name`. The last path segment comes first now, and
|
||||
a dot inside that segment still separates namespace from package, so `ludic.render3d`
|
||||
still builds as `render3d`.
|
||||
|
||||
**The camping game has moved out** to its own repository —
|
||||
[Maroon Lake](https://git.workshopsoft.io/workshopsoft/maroon-lake) — taking
|
||||
`examples/rendering/valley.ludic`, `hiker.ludic`, `camp/`, and 175 MB of survey data and
|
||||
scanned kit with it. It was here as a demo of the renderer and became a game, and an
|
||||
engine repository should not be carrying a game's assets. It is now the first consumer
|
||||
of everything above, which is the point: what the renderer needs a game to be able to do,
|
||||
it can now do from outside. `examples/rendering/smooth.ludic` stays as the renderer's
|
||||
example in this tree.
|
||||
- **OpenGL for Ludic, and a 3D renderer on it.** `Gl.*` binds the whole OpenGL 4.1
|
||||
core API — every `gl*` entry point of the platform `gl3.h` as `Gl.<snake_name>(…)`
|
||||
with every `GL_*` constant, generated by `ludic-dev glgen` with per-call ABI thunks
|
||||
(`runtime/native/gl_thunks.ll`; float/double parameters take `fixed`). Windowed
|
||||
builds get an `NSOpenGLContext` on the existing window at Retina resolution
|
||||
(`cocoa.ll`); headless builds render into an offscreen CGL context, so a program
|
||||
that uses `Gl.*` renders and screenshots identically under the test harness.
|
||||
`Gl.open / swap / screenshot / program / vao / floats …` cover the glue, and the
|
||||
IEEE-float helpers (`f_add`, `mem_put_f32`, …) let Q16.16 programs fill real
|
||||
float vertex and uniform data. `Gl.*` links `gl.ll + gl_thunks.ll + OpenGL.framework`
|
||||
only when used; every other build is byte-identical.
|
||||
|
||||
The `ludic.render3d` package (`packages/ludic.render3d`) is a physically based
|
||||
renderer written on `Gl.*`: HDRI sky with image-based lighting (irradiance, GGX
|
||||
prefiltered, split-sum BRDF, sun extracted from the map), GPU-generated terrain
|
||||
with scanned PBR materials (stochastic anti-tiling, triplanar rock, slope/altitude
|
||||
splatting), cascaded shadow maps with PCF and world-unit biasing, a glTF loader
|
||||
for scanned models, instanced vegetation with baked impostors, procedural grass
|
||||
and lupines with wind and translucency, 4x MSAA with alpha-to-coverage, SSAO,
|
||||
still water, cloud shadows, aerial perspective, an HDR pipeline with bloom,
|
||||
auto-exposure, ACES tonemapping, grading, sharpening and grain. See
|
||||
`examples/rendering/smooth.ludic`, and the Maroon Lake game (git.workshopsoft.io/workshopsoft/maroon-lake) for a
|
||||
game built on it. The renderer's CC0 materials are fetched with `ludic assets`.
|
||||
|
||||
**16-bit PNGs**: the renderer's texture loader keeps 16-bit samples (normal /
|
||||
displacement maps) and uploads them as `RGB16` / `R16`.
|
||||
|
||||
### Fixes
|
||||
|
||||
- **Resizing the window (or entering fullscreen) no longer empties the world.** It left
|
||||
`gl error 1286` — `GL_INVALID_FRAMEBUFFER_OPERATION` — on every frame from there on, with
|
||||
the terrain, the trees, the grass and the water gone and only the sky drawn.
|
||||
|
||||
The sun-visibility pass added in `changes/terrain-perf.md` borrows the depth buffer the
|
||||
frame is about to be drawn with, so that rasterising it doubles as a depth prepass. It
|
||||
was storing that borrowed texture in its `Target`, and a `Target` deletes whatever its
|
||||
`depth` names when it is freed. On the first resize the sequence was: `post_free` deletes
|
||||
the frame's depth texture, `post_init` immediately makes the replacement — and GL hands
|
||||
back the name that was just freed — and then the visibility target, rebuilt for the new
|
||||
size, deleted that name believing it was its own. The scene framebuffer lost its depth
|
||||
attachment. What is left is a colour-only framebuffer, which is *complete*, so drawing
|
||||
carried on with no depth test at all: the sky is a fullscreen quad drawn last, and with
|
||||
nothing left to fail against it painted over the entire valley. The 1286s came from the
|
||||
passes whose own attachment now named a texture that no longer existed.
|
||||
|
||||
A borrowed attachment is never written into the target now, and the frame's depth is
|
||||
attached afresh at the start of each pass — it is a different texture every time the
|
||||
screen-sized buffers are rebuilt, and one `glFramebufferTexture2D` per pass is cheaper
|
||||
than any scheme for noticing that it changed.
|
||||
|
||||
`R3D_RESIZE_AT=<frame>` rebuilds every screen-sized buffer from that frame on, cycling
|
||||
through four drawable sizes every few frames. A window cannot be resized in a headless
|
||||
run, so this is the only way to reach the path; it reproduced the fault in one frame and
|
||||
now runs twenty resizes, with the fly camera and with the game, without an error.
|
||||
- An upgrade keeps the package store. The installer replaces the whole install
|
||||
root, and `ludic add` caches packages in `~/.ludic/store` — so re-running the
|
||||
one-liner deleted every package a project had fetched. The store is carried
|
||||
across now; everything else in the root belongs to the toolchain and is replaced.
|
||||
|
||||
### Performance
|
||||
|
||||
- **Ground cover stops re-growing itself.** Walking a streamed world hitched, and the
|
||||
hitch got worse the longer you played. Measured in the Maroon Lake game, with a new hitch
|
||||
report rather than guessed at.
|
||||
|
||||
**The chunk cache had a cliff, not a slope.** A stream cached 4096 chunks and then
|
||||
stopped remembering: past that the chunk was generated, used for one frame and thrown
|
||||
away, so every ring walk regenerated it, for the rest of the session. It arrives after
|
||||
enough of the map has been walked — six evictions' worth over seven kilometres, so an
|
||||
ordinary session reaches it — and it is the point where cover starts visibly re-growing
|
||||
as you turn. `stream_evict` now drops the half of the cache nobody has asked for in the
|
||||
longest time (chunks carry the walk that last wanted them) and rebuilds the index over
|
||||
what is left. Over a 7 km traversal: generation total **9073 ms → 2230 ms**, the worst
|
||||
single frame's generation **11.4 ms → 3.1 ms**, median frame 10.8 → 9.0 ms. With a cache
|
||||
deliberately sized to saturate early, the same run goes from 2748 frames generating to
|
||||
1548, and from a 13.3 ms median to 9.2. `R3D_NOEVICT` restores the old behaviour for
|
||||
comparison, `R3D_STREAM_CAP=<n>` sets the cache size.
|
||||
|
||||
The other half of that hitch was in the game's own cover generator, and went with it to
|
||||
the Maroon Lake game's repository: its candidates were paying for a second noise field, four
|
||||
height samples and a path distance before the drift field that rules out most of the
|
||||
meadow — 2341 µs → 518 µs per chunk, bit-identical output. Worth repeating in any
|
||||
generator: a `stream_fill` is called for tens of thousands of candidates per chunk, so
|
||||
the order of its tests is most of its cost.
|
||||
|
||||
**The hitch report** (`R3D_PROF=1`) is what found both. It prints the slowest frames of
|
||||
the run with what was in each: CPU versus GPU wait, cover generated, instance bytes
|
||||
uploaded, the game's own tick, and the renderer phase that took longest. Alongside it,
|
||||
per-chunk generation cost by stream and band, a census of what the caches hold, and a
|
||||
stutter figure — the frame time a run spent beyond 1.2x its own median — because a mean
|
||||
cannot show a hitch and a maximum is one unlucky frame.
|
||||
|
||||
It also found two content bugs in the game it was measured on, which is the report doing
|
||||
its job: a cover stream whose placement rule never fires still pays full generation cost,
|
||||
and the census makes that visible — 3364 cached chunks holding zero instances.
|
||||
- **The ground costs half what it did.** Measured in the Maroon Lake game, the terrain was 10.3 ms of
|
||||
a 22.2 ms frame; it is now 5.4 ms of 16.4 ms — 45 fps to 61 fps at 1080p, with the
|
||||
frame otherwise unchanged (every viewpoint tested stays above 54 dB PSNR against the
|
||||
old renderer, with no channel differing by more than 7/255).
|
||||
|
||||
**Measure by frame time, not by the pass timers.** `R3D_PROF`'s per-pass
|
||||
`GL_TIME_ELAPSED` queries cannot be trusted on this driver: with the ground's shading
|
||||
work removed the terrain query fell from 10.5 ms to 1.3 ms while the frame time did
|
||||
not move at all. Every number above and below is a median real frame time, taken by
|
||||
switching one thing off (`prof_ft_report`); the pass timers are still printed, and are
|
||||
still useful for spotting a pass that appears out of nowhere, but they cannot size one.
|
||||
`R3D_NOTERRAIN` skips the ground, `R3D_TNEARONLY` / `R3D_TFARONLY` draw every patch
|
||||
with one tier's program, and `R3D_RES=<w>x<h>` renders at another size — the three
|
||||
switches that say whether a cost is the ground, which tier it is in, and whether it is
|
||||
pixels at all.
|
||||
|
||||
**Each detail tier is its own program.** `terrain.frag` holds a detailed near tier and
|
||||
a cheap far one and chose between them per pixel, so every pixel of the valley walls
|
||||
was compiled — and scheduled — for a near path it never ran. CDLOD selection now knows
|
||||
which tiers a patch can contain: one that never comes within the split draws with
|
||||
`FAR_ONLY`, one wholly inside it with `NEAR_ONLY`, and only the ring of patches that
|
||||
straddle the band needs the program that holds both and cross-fades. Pixel-identical,
|
||||
and it makes the tiers separately measurable: the near tier costs 7.5 ms over a whole
|
||||
frame, the far tier 2.3 ms.
|
||||
|
||||
**The sun visibility is its own pass** (`tersun.frag`). The same CDLOD patches are
|
||||
rasterised once into a screen-sized R8 buffer that holds nothing but each ground
|
||||
pixel's sun visibility, and `terrain.frag` fetches it by fragment coordinate. The pass
|
||||
costs 0.27 ms, shares the frame's depth buffer so it doubles as a depth prepass, and
|
||||
takes the cascade read out of the shader that covers the screen. It picks its tier —
|
||||
filtered PCF near, a single tap far — over the same cross-faded band the ground uses,
|
||||
so the boundary is not a contour you can find on the hillside.
|
||||
|
||||
**Nothing is sampled for a weight of zero.** The ground sampled all four of its
|
||||
materials for every pixel and then blended three of them at zero: a meadow pixel took
|
||||
nine taps of triplanar rock, a cliff pixel nine taps of stochastic grass, and every
|
||||
pixel in the valley took the snow tile and the four noise fields behind the lake's
|
||||
shore wash — a wash that is a hairline along one shore within 120 m of the camera. The
|
||||
survey photograph's classification now runs first, because it is what decides which
|
||||
materials are present; each material block sits behind its own weight; the ridge field
|
||||
that ragged the snow line is skipped 160 m below it, where it cannot change anything;
|
||||
and inside the stochastic blend a cell's rotation, offset and rotated gradients are
|
||||
computed inside its own test, so a cell whose sharpened weight rounds away costs
|
||||
nothing. All of it exact where the weight is zero, and it is most of the win.
|
||||
|
||||
Material sampling is what remains (2.8 ms of the 5.4): scanned 2K tiles taken at 16x
|
||||
anisotropy on ground seen at a grazing angle. `R3D_ANISO=<n>` sets the filter (the
|
||||
default is unchanged at 16; 4 is worth 1.0 ms and 1 is worth 1.7 ms).
|
||||
|
||||
The shadow pass is 1.2 ms of the frame and has nothing to give: re-using the far
|
||||
cascades between frames — their windows are snapped to a 14 m and a 64 m grid — is
|
||||
worth 0.15 ms standing still and nothing while walking, so it is not in the tree.
|
||||
## v0.6.1 — 2026-09-05
|
||||
|
||||
### Fixes
|
||||
|
||||
- **`ludic new` scaffolded a project that would not compile.** The project name went
|
||||
straight into the `program <Name>` identifier, so `ludic new my-game` wrote
|
||||
`program My-Game` — a subtraction — and the first `ludic run` failed with
|
||||
`expected '{', got '-'`. A name is now turned into a valid identifier
|
||||
(`my-game` → `MyGame`, `2048` → `Game2048`), and a name that cannot be a
|
||||
directory or a package is refused with the rule rather than mangled.
|
||||
|
||||
Found while auditing every command's flags and arguments, along with:
|
||||
|
||||
- **Unknown options are errors.** `ludic build --headles` silently built a
|
||||
windowed binary; `ludic build -o` with no path silently ignored it. Both now
|
||||
say what is wrong and exit non-zero.
|
||||
- **`ludic fmt` formats in place**, as its help always claimed — it was printing
|
||||
the file to stdout and changing nothing. `ludic fmt --check` reports drift
|
||||
without writing, for a hook or CI.
|
||||
- **`ludic test nosuch.ludic`** says the file does not exist instead of passing it
|
||||
to the compiler.
|
||||
- **`ludic build-lib`** reported failures as a shell syntax error (an
|
||||
interpolation written in a non-interpolating string), and its no-argument
|
||||
auto-detection picked up `package.lock.ludic` as a module to compile.
|
||||
- Error messages that still began with `x:` — the CLI's old name — now say
|
||||
`ludic:`.
|
||||
## v0.6.0 — 2026-09-05
|
||||
|
||||
### Refactoring
|
||||
|
||||
- **`ludic` is only the language's command line now.** The toolchain's own tasks —
|
||||
building the compiler from its IR seed, the regression suites, the docs site,
|
||||
releases — moved out of it into a separate `ludic-dev` binary that is built from
|
||||
a checkout and is not part of an install.
|
||||
|
||||
- **`ludic help` is what a user can actually do**: `new`, `run`, `build`, `test`,
|
||||
`add`, `fmt`, `lsp`, `doctor`, `upgrade`. No section about a repository they do
|
||||
not have. Typing `ludic dev …` says where those tasks went rather than failing
|
||||
as an unknown command.
|
||||
- **`ludic dev <task>` becomes `ludic-dev <task>`** for contributors; every task
|
||||
is otherwise unchanged. The bootstrap is now
|
||||
`bin/ludicc tools/ludic-cli/dev.ludic -o bin/ludic-dev && bin/ludic-dev build`.
|
||||
- The shipped binary drops from ~880 KB to ~190 KB, since none of the release,
|
||||
docs-generation or bootstrap machinery is linked into it any more.
|
||||
|
||||
### CI
|
||||
|
||||
- The docs deploy triggers on a change to `install.sh`. The site publishes the
|
||||
installer, but the workflow's path filter did not mention it — so a release that
|
||||
only fixed `install.sh` left the old script live at the URL the landing page
|
||||
tells people to pipe into `sh`.
|
||||
## v0.5.2 — 2026-09-05
|
||||
|
||||
### Fixes
|
||||
|
|
|
|||
14
COMPILING.md
14
COMPILING.md
|
|
@ -25,8 +25,8 @@
|
|||
> ```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/ludic-dev build # the whole toolchain into bin/
|
||||
> bin/ludicc tools/ludic-cli/main.ludic -o bin/ludic
|
||||
> 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
|
||||
> bin/ludic build examples/games/snake.ludic # or through the CLI
|
||||
|
|
@ -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` |
|
||||
|
|
@ -323,7 +320,7 @@ node tools/ludic-web/run.mjs build/web/snake_headless.wasm --stdin=ddss
|
|||
```
|
||||
|
||||
Because Ludic is fixed-point and its RNG is seeded, the native headless binary
|
||||
and the wasm one must render byte-identical frames from the same input. `bin/ludic-dev test`
|
||||
and the wasm one must render byte-identical frames from the same input. `bin/ludic dev test`
|
||||
asserts exactly that, which is a much stronger check on the backend than
|
||||
"it started".
|
||||
|
||||
|
|
@ -337,13 +334,13 @@ entity allocator, save/load snapshots, the frame loop, the window, and the whole
|
|||
graphics stack — framebuffer, PNG decoding, sprites, 9-slice, TrueType text and
|
||||
the retained UI.
|
||||
|
||||
None of it goes through C. `bin/ludic-dev test` asserts that directly: no C source
|
||||
None of it goes through C. `bin/ludic dev test` asserts that directly: no C source
|
||||
survives in `runtime/`, no C emitter survives in `ludicc`, and the examples all
|
||||
build, run and render from IR alone.
|
||||
|
||||
## Every flag
|
||||
|
||||
The self-hosted `ludicc`/`ludic` (built with `bin/ludic-dev build-cli`) accept:
|
||||
The self-hosted `ludicc`/`ludic` (built with `bin/ludic dev build-cli`) accept:
|
||||
|
||||
```
|
||||
<file.ludic> the program to compile (first non-flag argument)
|
||||
|
|
@ -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,24 +19,23 @@ 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/main.ludic -o bin/ludic
|
||||
```
|
||||
|
||||
That gives you `bin/ludic-dev`, the contributor tool: it replaces every
|
||||
build/test shell script in the repo and builds everything, including itself and
|
||||
`bin/ludic`. It is deliberately a separate binary from the `ludic` users install
|
||||
— that one carries none of these tasks and is never asked to.
|
||||
That gives you `bin/ludic`, the CLI — the same binary users install, which also
|
||||
carries the toolchain's own tasks under `ludic dev` and replaces every build/test
|
||||
shell script in the repo. From then on it builds everything — including itself:
|
||||
|
||||
```bash
|
||||
bin/ludic-dev build # the whole toolchain into bin/ (ludicc, ludic, ludic-dev, ludic-fmt, ludic-lsp)
|
||||
bin/ludic-dev help # every contributor task
|
||||
bin/ludic dev build # the whole toolchain into bin/ (ludicc, ludic, ludic-fmt, ludic-lsp)
|
||||
bin/ludic dev help # every contributor task
|
||||
bin/ludic help # what a user of the language sees
|
||||
```
|
||||
|
||||
Always run `ludic-dev` from the repository root, so `assets/` and `selfhost/`
|
||||
resolve. (A checkout is also an install root: `bin/` beside `runtime/` and
|
||||
`packages/`, exactly the shape `install.sh` lays down under `~/.ludic`, which is
|
||||
why `bin/ludic` behaves there exactly as an installed one does.)
|
||||
Always run `ludic dev` from the repository root, so `assets/` and `selfhost/`
|
||||
resolve. (A checkout is an install root: `bin/` beside `runtime/` and
|
||||
`packages/`, exactly the shape `install.sh` lays down under `~/.ludic`. That is
|
||||
why the same binary serves both.)
|
||||
|
||||
## The development loop
|
||||
|
||||
|
|
@ -44,17 +43,17 @@ When you change the compiler or runtime, prove the self-hosting fixpoint still
|
|||
holds before you push:
|
||||
|
||||
```bash
|
||||
bin/ludic-dev reseed # regenerate selfhost/ludicc.seed.ll after a compiler change
|
||||
bin/ludic-dev bootstrap-cfree # rebuild the compiler from the seed with NO C compiler in the loop
|
||||
bin/ludic-dev test # the full regression suite
|
||||
bin/ludic dev reseed # regenerate selfhost/ludicc.seed.ll after a compiler change
|
||||
bin/ludic dev bootstrap-cfree # rebuild the compiler from the seed with NO C compiler in the loop
|
||||
bin/ludic dev test # the full regression suite
|
||||
```
|
||||
|
||||
Other useful targets:
|
||||
|
||||
```bash
|
||||
bin/ludic build <file.ludic> [--headless] # compile a program to a native app in build/
|
||||
bin/ludic-dev selfhost-test # correctness + bootstrap fixpoints
|
||||
bin/ludic-dev test-tools # the editor-toolchain suite (ludic-fmt, ludic-lsp)
|
||||
bin/ludic dev selfhost-test # correctness + bootstrap fixpoints
|
||||
bin/ludic dev test-tools # the editor-toolchain suite (ludic-fmt, ludic-lsp)
|
||||
bin/ludic clean # remove build/, out.ppm and stray artifacts
|
||||
```
|
||||
|
||||
|
|
@ -68,7 +67,7 @@ The stdlib lives in the runtime (`runtime/`) and is surfaced as namespaces
|
|||
and register its id in `tools/docgen/inventory.json`. Each documented
|
||||
namespace gets exactly **one** directory (the docs check enforces this).
|
||||
3. Add or extend an example under `examples/` and a case in the test suite.
|
||||
4. Run `bin/ludic-dev docs-gen --out build/pages && bin/ludic-dev docs-check build/pages` — the
|
||||
4. Run `bin/ludic dev docs-gen --out build/pages && bin/ludic dev docs-check build/pages` — the
|
||||
check fails if any inventory symbol lacks a page or is still seed text.
|
||||
5. Add a **changeset** for the user-facing change: a small file under
|
||||
[`changes/`](changes/README.md) with a `bump:` level and a one-line summary.
|
||||
|
|
@ -83,22 +82,22 @@ Releases are changeset-driven. Every user-facing change ships with a changeset
|
|||
(step 5 above). Read the next release before cutting it:
|
||||
|
||||
```bash
|
||||
ludic-dev release --dry-run # render the CHANGELOG section, write nothing
|
||||
ludic dev release --dry-run # render the CHANGELOG section, write nothing
|
||||
```
|
||||
|
||||
Then cut it:
|
||||
|
||||
```bash
|
||||
ludic-dev release [major|minor|patch] # omit the level to derive it from the changesets
|
||||
ludic dev release [major|minor|patch] # omit the level to derive it from the changesets
|
||||
git push origin main --follow-tags
|
||||
```
|
||||
|
||||
`ludic-dev release` aggregates the pending changesets into a new `CHANGELOG.md` section
|
||||
`ludic dev release` aggregates the pending changesets into a new `CHANGELOG.md` section
|
||||
— grouped by change type, with each changeset's markdown kept intact — bumps
|
||||
`VERSION`, commits `chore(release): vX.Y.Z`, and tags it.
|
||||
|
||||
**Pushing the tag is what publishes.** The `release` workflow builds the
|
||||
toolchain from the IR seed, runs `ludic-dev test`, `ludic-dev test-tools` and `ludic-dev bootstrap-cfree`
|
||||
toolchain from the IR seed, runs `ludic dev test`, `ludic dev test-tools` and `ludic dev bootstrap-cfree`
|
||||
against the tagged tree, and only then creates the Forgejo release — with the
|
||||
source tarball, a Linux toolchain build, a `.sha256` beside each, and that version's
|
||||
`CHANGELOG.md` section as the notes. It refuses to publish if the tag and
|
||||
|
|
@ -114,10 +113,10 @@ macOS artifacts cannot be produced on the Linux runner — a `darwin-arm64` buil
|
|||
needs a macOS host, and there is no cross-compile path (it would need the Xcode
|
||||
SDK and a Mach-O linker). Attaching one therefore means either registering a
|
||||
macOS runner and giving it a job, or running the same command CI runs from a
|
||||
Mac. Either way it is `ludic-dev publish`, which only adds assets the release is missing:
|
||||
Mac. Either way it is `ludic dev publish`, which only adds assets the release is missing:
|
||||
|
||||
```bash
|
||||
FORGEJO_TOKEN=… ludic-dev publish v0.4.0
|
||||
FORGEJO_TOKEN=… ludic dev publish v0.4.0
|
||||
```
|
||||
|
||||
Checksums are one `.sha256` file per artifact rather than a single `SHA256SUMS`,
|
||||
|
|
@ -139,14 +138,14 @@ broken link at a time. Everything host-shaped has an environment override, so a
|
|||
move can be rehearsed before it is committed.
|
||||
|
||||
**Hosts and URLs.** The install one-liner is served from the documentation site,
|
||||
which publishes `install.sh` beside the pages that quote it (`ludic-dev docs-gen`
|
||||
which publishes `install.sh` beside the pages that quote it (`ludic dev docs-gen`
|
||||
copies it in; `docs-check` fails without it). Change the host in:
|
||||
|
||||
| Where | What |
|
||||
|---|---|
|
||||
| `install.sh` | `REPO_API`, `REPO_URL`, `INSTALL_URL` — each `${LUDIC_…:-default}`, so `LUDIC_REPO_URL=… sh install.sh` tests a move without editing anything |
|
||||
| `tools/ludic-cli/project.ludic` | `install_url()` (`$LUDIC_INSTALL_URL`), used by `ludic upgrade` and `ludic doctor` |
|
||||
| `tools/ludic-cli/forgejo.ludic` | `FORGEJO_API_DEFAULT` (`$LUDIC_FORGEJO_API`), used by `ludic-dev publish` |
|
||||
| `tools/ludic-cli/forgejo.ludic` | `FORGEJO_API_DEFAULT` (`$LUDIC_FORGEJO_API`), used by `ludic dev publish` |
|
||||
| `docs/site/site.json` | `repo_url`, the `start.terminal` one-liner, and the doc links in `nav_links` |
|
||||
| Prose | `README.md`, `COMPILING.md`, `tools/editors/README.md`, and the two editor plugins' "server not found" messages |
|
||||
|
||||
|
|
@ -156,10 +155,9 @@ copies it in; `docs-check` fails without it). Change the host in:
|
|||
`is_ludic_file`), every editor asset (`tools/editors/shared/*.json`,
|
||||
`vscode/package.json`, the JetBrains `LudicFileType`), and every source file
|
||||
in the tree.
|
||||
- **The binaries** `ludic`, `ludicc`, `ludic-dev`, `ludic-fmt`, `ludic-lsp` —
|
||||
`cmd_dev_build` in `toolchain.ludic`, the release staging in `release.ludic`,
|
||||
`install.sh`, the editors' executable-name lists. Only the first, third and
|
||||
fourth of those ship: `ludic-dev` is built from a checkout and stays there.
|
||||
- **The binaries** `ludic`, `ludicc`, `ludic-fmt`, `ludic-lsp` — `build.ludic`'s
|
||||
`cmd_dev_build`, the release staging in `release.ludic`, `install.sh`, the
|
||||
editors' executable-name lists.
|
||||
- **The install root** `~/.ludic` and the source directories `tools/ludic-cli/`,
|
||||
`tools/ludic-tools/`, `packages/ludic.*`.
|
||||
- **The environment variables** `LUDIC_HOME`, `LUDIC_CC`, `LUDIC_MODULES`,
|
||||
|
|
@ -169,11 +167,11 @@ copies it in; `docs-check` fails without it). Change the host in:
|
|||
- **Identifiers that are contracts with other software**: the TextMate scope
|
||||
`source.ludic`, the VS Code language id `ludic`, the JetBrains plugin id
|
||||
`io.ludic.ide`, and the `ludic` code-fence tag understood by the Markdown
|
||||
injection and by `ludic-dev check-docs`.
|
||||
injection and by `ludic dev check-docs`.
|
||||
- **The prose**: `README.md`, `LANGUAGE.md`, `COMPILING.md`, `docs/**`, and
|
||||
`docs/site/site.json`'s `brand`/`meta`.
|
||||
|
||||
`ludic-dev test` is the safety net for the mechanical part — it builds the
|
||||
`ludic dev test` is the safety net for the mechanical part — it builds the
|
||||
toolchain, stages an install, and runs `new` → `build` → `test` through it, so a
|
||||
half-finished rename fails there rather than in someone's terminal.
|
||||
|
||||
|
|
@ -241,7 +239,7 @@ non-destructive version of "tidy the history" without touching a single commit.
|
|||
## Pull requests
|
||||
|
||||
- Base your branch on `main`.
|
||||
- Ensure `bin/ludic-dev test` (and `bin/ludic-dev bootstrap-cfree` for compiler/runtime changes)
|
||||
- Ensure `bin/ludic dev test` (and `bin/ludic dev bootstrap-cfree` for compiler/runtime changes)
|
||||
pass, and that `ludic-fmt` leaves your files unchanged.
|
||||
- Fill in the PR template checklist. Reference the issue you close with
|
||||
`Closes #NN` in the description or a commit message.
|
||||
|
|
|
|||
1668
LANGUAGE.md
1668
LANGUAGE.md
File diff suppressed because it is too large
Load diff
47
README.md
47
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
|
||||
|
|
@ -77,18 +77,16 @@ of them with `ludic build examples/games/snake.ludic`.
|
|||
|
||||
### Building from a checkout
|
||||
|
||||
Contributors also get `ludic-dev`, a second binary carrying the toolchain's own
|
||||
tasks — building the compiler, the suites, the docs site, releases. It is built
|
||||
from a checkout and is not part of an install, so nothing a user runs is mixed
|
||||
up with it. Bootstrapping is the only step Ludic cannot do for itself, since
|
||||
compiling Ludic needs a compiler — clang assembles the checked-in IR seed, and
|
||||
that compiler builds the rest:
|
||||
Contributors work from the repository, where the same CLI carries the toolchain's
|
||||
own tasks under `ludic dev`. Bootstrapping is the only step Ludic cannot do for
|
||||
itself, since compiling Ludic needs a compiler — clang assembles the checked-in
|
||||
IR seed, and that compiler builds the rest:
|
||||
|
||||
```bash
|
||||
mkdir -p bin && clang selfhost/ludicc.seed.ll -o bin/ludicc
|
||||
bin/ludicc tools/ludic-cli/dev.ludic -o bin/ludic-dev
|
||||
bin/ludic-dev build # -> bin/{ludicc,ludic,ludic-dev,ludic-fmt,ludic-lsp}
|
||||
bin/ludic-dev test # the regression suite
|
||||
bin/ludicc tools/ludic-cli/main.ludic -o bin/ludic
|
||||
bin/ludic dev build # -> bin/{ludicc,ludic,ludic-fmt,ludic-lsp}
|
||||
bin/ludic dev test # the regression suite
|
||||
```
|
||||
|
||||
## The language
|
||||
|
|
@ -114,28 +112,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 +120,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
|
||||
```
|
||||
|
||||
|
|
@ -179,7 +154,7 @@ capability of the retired C compiler and has not been re-wired on the
|
|||
self-hosted toolchain. `--target` cross-compilation and `--shared` libraries are
|
||||
in the same position. See [COMPILING.md](COMPILING.md).
|
||||
|
||||
Releases follow SemVer and are cut from changesets by `ludic-dev release`, then built
|
||||
Releases follow SemVer and are cut from changesets by `ludic dev release`, then built
|
||||
and published by CI from the tag; see [CHANGELOG.md](CHANGELOG.md).
|
||||
|
||||
## Contributing
|
||||
|
|
|
|||
2
VERSION
2
VERSION
|
|
@ -1 +1 @@
|
|||
0.22.0
|
||||
0.5.2
|
||||
|
|
|
|||
|
|
@ -1,3 +0,0 @@
|
|||
Everything under assets/polyhaven/ is fetched from https://polyhaven.com and is
|
||||
released by Poly Haven under CC0 1.0 (public domain). Files are not committed;
|
||||
see manifest.txt for the exact sources. Re-fetch with tools/glgen/fetch_assets.sh.
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
# Changesets
|
||||
|
||||
A **changeset** is one small Markdown file describing a single user-facing change,
|
||||
dropped in this directory. `ludic-dev release` consumes every changeset here into a new
|
||||
dropped in this directory. `ludic dev release` consumes every changeset here into a new
|
||||
`CHANGELOG.md` section, bumps `VERSION`, and deletes the consumed files.
|
||||
|
||||
## Format
|
||||
|
|
@ -14,7 +14,7 @@ the changelog. Markdown is fine.
|
|||
```
|
||||
|
||||
- `bump:` — `major`, `minor`, or `patch` (SemVer). The release version is bumped
|
||||
by the **highest** level among the pending changesets (unless `ludic-dev release <level>`
|
||||
by the **highest** level among the pending changesets (unless `ludic dev release <level>`
|
||||
overrides it).
|
||||
- `type:` — the Conventional Commit type (`feat`, `fix`, `perf`, `docs`, …). It
|
||||
decides which group the change lands in: `feat` → **Features**, `fix` →
|
||||
|
|
@ -47,5 +47,5 @@ filename works except this `README.md`, which the release step always skips.
|
|||
Preview how the next release will read before cutting it — this writes nothing:
|
||||
|
||||
```bash
|
||||
ludic-dev release --dry-run
|
||||
ludic dev release --dry-run
|
||||
```
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
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