Compare commits
No commits in common. "main" and "v0.4.0" have entirely different histories.
2209 changed files with 88536 additions and 792628 deletions
|
|
@ -24,7 +24,7 @@ labels:
|
|||
|
||||
## Environment
|
||||
|
||||
- Command used (e.g. `bin/ludic build foo.ludic --headless`):
|
||||
- Command used (e.g. `bin/x app foo.ludic --headless`):
|
||||
- Target (native macOS / headless / web-wasm):
|
||||
- Commit (`git rev-parse --short HEAD`):
|
||||
- OS / arch:
|
||||
|
|
|
|||
|
|
@ -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/x test` passes.
|
||||
- [ ] For compiler/runtime changes: `bin/x reseed && bin/x 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/x docs-gen && bin/x 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/x/main.ludic -o bin/x
|
||||
|
||||
- name: Rebuild the compiler from the seed and assert byte-identity
|
||||
# `ludic-dev bootstrap-cfree` assembles the seed with clang, has that seed
|
||||
# `x 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/x 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 `x test` / `x 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, `x 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/x/main.ludic -o bin/x
|
||||
bin/x build
|
||||
|
||||
- name: Regression suite (ludic-dev test)
|
||||
run: bin/ludic-dev test
|
||||
- name: Regression suite (x test)
|
||||
run: bin/x test
|
||||
|
||||
- name: Editor-toolchain suite (ludic-dev test-tools)
|
||||
- name: Editor-toolchain suite (x 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/x 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 `x test-tools`.)
|
||||
bin/x check-impl
|
||||
bin/x check-vocabulary
|
||||
bin/x check-docs
|
||||
bin/x docs-gen --out build/pages
|
||||
bin/x docs-check build/pages
|
||||
|
|
|
|||
|
|
@ -35,15 +35,10 @@ jobs:
|
|||
# - pull_request: base branch .. this commit
|
||||
# - push: the pushed range (event.before .. this commit)
|
||||
# - new branch / unknown: just the tip commit
|
||||
# `event.before` is only usable if it still resolves: a force-push
|
||||
# rewrites (and a gc can remove) the commit it names, which made this
|
||||
# job fail with "Invalid revision range" on an otherwise clean push.
|
||||
# Fall back to the tip commit in that case.
|
||||
if [ -n "${BASE:-}" ]; then
|
||||
git fetch --quiet origin "${BASE}" 2>/dev/null || true
|
||||
RANGE="origin/${BASE}..${GITHUB_SHA}"
|
||||
elif [ -n "${BEFORE:-}" ] && ! printf '%s' "$BEFORE" | grep -qE '^0+$' \
|
||||
&& git cat-file -e "${BEFORE}^{commit}" 2>/dev/null; then
|
||||
elif [ -n "${BEFORE:-}" ] && ! printf '%s' "$BEFORE" | grep -qE '^0+$'; then
|
||||
RANGE="${BEFORE}..${GITHUB_SHA}"
|
||||
else
|
||||
RANGE="${GITHUB_SHA}~1..${GITHUB_SHA}"
|
||||
|
|
|
|||
|
|
@ -10,22 +10,10 @@ on:
|
|||
paths:
|
||||
- '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'
|
||||
- 'tools/x/**'
|
||||
- '.forgejo/workflows/docs.yml'
|
||||
workflow_dispatch: {}
|
||||
|
||||
# Deploying is a force-push of an orphan branch, so two runs racing can land out
|
||||
# of order and leave `pages` holding the older build — the site would silently
|
||||
# go backwards with both runs green. Serialise them, and let a newer push cancel
|
||||
# an older one that is still building rather than queue behind it.
|
||||
concurrency:
|
||||
group: pages-deploy
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
|
|
@ -37,8 +25,8 @@ jobs:
|
|||
# "Waiting" forever with "no online runner found matching this label".
|
||||
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
|
||||
# (clang assembles the seed into bin/ludicc, which compiles bin/x) exactly
|
||||
# like the ci workflow, then runs `x 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 +51,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/x/main.ludic -o bin/x
|
||||
bin/x docs-gen --out ../public
|
||||
bin/x 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 `x 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/x/main.ludic -o bin/x
|
||||
bin/x 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/x test
|
||||
bin/x test-tools
|
||||
bin/x 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
|
||||
# x 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/x publish "$TAG"
|
||||
|
|
|
|||
1
.gitattributes
vendored
1
.gitattributes
vendored
|
|
@ -1 +0,0 @@
|
|||
packages/*/lib/** filter=lfs diff=lfs merge=lfs -text
|
||||
33
.gitignore
vendored
33
.gitignore
vendored
|
|
@ -1,6 +1,6 @@
|
|||
# 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` /
|
||||
# `bin/ludic clean`). Root-anchored so a source dir named "build" elsewhere is never
|
||||
# (build/out.ppm) and the docs site all land under build/ (see `bin/x build` /
|
||||
# `bin/x 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,14 +9,14 @@
|
|||
# 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, x, ludic-fmt, ludic-lsp) — all built
|
||||
# into bin/ by the one-line bootstrap + `bin/x build`; never checked in. The
|
||||
# only thing published is the source and the LLVM-IR seed (selfhost/ludicc.seed.ll).
|
||||
/bin/
|
||||
|
||||
# package manager (issue #63): the per-project linked view into the global
|
||||
# content-addressed store, and the optional hermetic copy from `ludic vendor`. Both
|
||||
# are regenerated by `ludic get` / `ludic vendor` — package.ludic + package.lock.ludic
|
||||
# content-addressed store, and the optional hermetic copy from `x vendor`. Both
|
||||
# are regenerated by `x get` / `x vendor` — package.ludic + package.lock.ludic
|
||||
# are the tracked source of truth, so these stay out of the tree.
|
||||
ludic_modules/
|
||||
vendor/
|
||||
|
|
@ -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,23 +39,5 @@ tools/editors/jetbrains/.kotlin/
|
|||
__pycache__/
|
||||
*.pyc
|
||||
|
||||
# Release artifacts produced by `ludic-dev release`
|
||||
# Release artifacts produced by `x release`
|
||||
/dist/
|
||||
|
||||
# Build/release tarballs anywhere in the tree. `git -C <repo> archive -o foo.tgz`
|
||||
# resolves -o relative to the repo, not the caller's directory, so a stray
|
||||
# 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/
|
||||
|
|
|
|||
1035
CHANGELOG.md
1035
CHANGELOG.md
File diff suppressed because it is too large
Load diff
93
COMPILING.md
93
COMPILING.md
|
|
@ -1,46 +1,37 @@
|
|||
# Compiling Ludic
|
||||
|
||||
> **Note:** `ludicc` is **written in Ludic** (`selfhost/*.ludic`) and built from a
|
||||
> checked-in IR seed — the C compiler this document once described has been
|
||||
> deleted. The native pipeline below (Ludic → LLVM IR → object → binary) is
|
||||
> unchanged. `ludicc` drives clang itself (via an `os_system` intrinsic), so
|
||||
> `ludicc app.ludic -o bin/app` and `--emit-llvm` work directly. `--fmt` is
|
||||
> reimplemented as a lex+parse gate (the doc-check hook). The `--target`/
|
||||
> cross-compile and `--shared` paths are still features of the old C driver not
|
||||
> yet re-implemented on the self-hosted toolchain. See the
|
||||
> [Bootstrap deep-dive](https://git.workshopsoft.io/workshopsoft/ludic/wiki/Bootstrap) §5.7 on the wiki.
|
||||
> **Note (2026-08-27):** `ludicc` is now **written in Ludic** (`selfhost/*.ludic`)
|
||||
> and built from a checked-in IR seed — the C compiler this document describes has
|
||||
> been deleted. The native pipeline below (Ludic → LLVM IR → object → binary) is
|
||||
> unchanged. `ludicc` now drives clang itself (via an `os_system` intrinsic), so
|
||||
> `ludicc app.ludic -o bin/app` and `--emit-llvm` work directly, and a sibling
|
||||
> command `ludic app.ludic` compiles to a temporary binary and runs it in one
|
||||
> step. The whole toolchain is built by `bin/x build`; `bin/x app` remains as a
|
||||
> convenience wrapper over the compiler. `--fmt` is reimplemented as a lex+parse
|
||||
> gate (the doc-check hook). The `--target`/cross-compile and `--shared` paths are
|
||||
> still features of the old C driver not yet re-implemented on the self-hosted
|
||||
> toolchain. See the [Bootstrap deep-dive](https://git.workshopsoft.io/workshopsoft/ludic/wiki/Bootstrap) §5.7 on the wiki.
|
||||
>
|
||||
> Most people never invoke `ludicc` directly: the `ludic` CLI drives it.
|
||||
> From a clean checkout, build the compiler and the task-runner in one line, then
|
||||
> let `bin/x` do the rest (run it from the repository root):
|
||||
>
|
||||
> ```bash
|
||||
> curl -fsSL https://workshopsoft.pages.workshopsoft.io/ludic/install.sh | sh # the toolchain, into ~/.ludic
|
||||
> ludic new mygame && cd mygame
|
||||
> ludic run # compile + run
|
||||
> ludic build --headless # compile, deterministic render
|
||||
> # one-time bootstrap: clang assembles the seed, then ludicc compiles bin/x
|
||||
> mkdir -p bin && clang selfhost/ludicc.seed.ll -o bin/ludicc && bin/ludicc tools/x/main.ludic -o bin/x
|
||||
> bin/x build # rebuild the whole toolchain into bin/
|
||||
> # (ludicc, ludic, x, ludic-fmt, ludic-lsp)
|
||||
> bin/ludicc examples/games/snake.ludic -o bin/snake # compile
|
||||
> bin/ludic examples/games/snake.ludic # compile + run
|
||||
> bin/x help # list every command
|
||||
> ```
|
||||
>
|
||||
> From a clean checkout, the compiler and the CLI come up in two lines and the
|
||||
> CLI does the rest (run it from the repository root):
|
||||
>
|
||||
> ```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/
|
||||
> # (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
|
||||
> bin/ludic help # every command
|
||||
> ```
|
||||
>
|
||||
> A `.ludic` file with handlers is a game and links windowed by default;
|
||||
> `--headless` and `--windowed` force the mode. The engine runtime
|
||||
> (`runtime/native/cocoa.ll`, the spliced `runtime/native/*.ludic`) and the
|
||||
> bundled `ludic.*` packages are found under the **install root**: `$LUDIC_HOME`
|
||||
> if set, otherwise derived from the binary's own location — the parent of its
|
||||
> `bin/` directory, which is both `~/.ludic` for an install and the repository
|
||||
> root for a checkout. `$LUDIC_CC` overrides the assembler/linker (default
|
||||
> `clang`).
|
||||
> The binaries are multi-call (one native binary under two names): invoked as
|
||||
> `ludicc` it compiles, as `ludic` it compiles-and-runs. A `.ludic` file with
|
||||
> handlers is a game and links windowed by default; `--headless` and `--windowed`
|
||||
> force the mode. The runtime (`runtime/native/cocoa.ll`) is found via
|
||||
> `$LUDIC_HOME`, defaulting to the directory the binary sits in — keep them in
|
||||
> `bin/`, or set `LUDIC_HOME` and put them on `PATH`. `$LUDIC_CC` overrides the
|
||||
> assembler/linker (default `clang`).
|
||||
|
||||
|
||||
`ludicc` is a compiler, not a translator. It lexes, parses, checks and lowers
|
||||
|
|
@ -73,9 +64,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` |
|
||||
|
|
@ -85,17 +73,17 @@ the old C driver and are **not yet re-implemented** on the self-hosted toolchain
|
|||
(see the note at the top). The rows above the line work today via the
|
||||
self-hosted `ludicc`.
|
||||
|
||||
`bin/ludic build` wraps the common cases:
|
||||
`bin/x app` wraps the common cases:
|
||||
|
||||
```bash
|
||||
bin/ludic build examples/games/snake.ludic # -> build/snake (native)
|
||||
bin/ludic build examples/library/combat.ludic --lib # -> build/libcombat.* (library)
|
||||
bin/ludic build examples/games/snake.ludic --headless # -> build/snake_headless (out.ppm)
|
||||
bin/ludic build examples/games/snake.ludic --web # -> build/web/ (browser)
|
||||
bin/x app examples/games/snake.ludic # -> build/snake (native)
|
||||
bin/x app examples/library/combat.ludic --lib # -> build/libcombat.* (library)
|
||||
bin/x app examples/games/snake.ludic --headless # -> build/snake_headless (out.ppm)
|
||||
bin/x app examples/games/snake.ludic --web # -> build/web/ (browser)
|
||||
```
|
||||
|
||||
The `--lib` and `--web` targets were part of the old C driver and are **not yet
|
||||
re-implemented** on the self-hosted toolchain — `bin/ludic build` supports the native
|
||||
re-implemented** on the self-hosted toolchain — `bin/x app` supports the native
|
||||
windowed and `--headless` builds today.
|
||||
|
||||
## Programs and libraries
|
||||
|
|
@ -235,7 +223,7 @@ only the triple changes.
|
|||
```
|
||||
|
||||
```bash
|
||||
bin/ludic build examples/games/chronorift.ludic --web
|
||||
bin/x app examples/games/chronorift.ludic --web
|
||||
python3 -m http.server -d build/web 8000 # then open http://localhost:8000/
|
||||
```
|
||||
|
||||
|
|
@ -323,7 +311,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/x test`
|
||||
asserts exactly that, which is a much stronger check on the backend than
|
||||
"it started".
|
||||
|
||||
|
|
@ -337,13 +325,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/x 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/x build-cli`) accept:
|
||||
|
||||
```
|
||||
<file.ludic> the program to compile (first non-flag argument)
|
||||
|
|
@ -353,18 +341,15 @@ 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
|
||||
--run compile then run (what `ludic run` uses)
|
||||
--run compile then run (implicit when invoked as `ludic`)
|
||||
(unknown -flags are ignored with a warning, never taken as the input file)
|
||||
|
||||
environment:
|
||||
LUDIC_CC the LLVM that assembles IR and drives the linker (clang)
|
||||
LUDIC_HOME the install root — runtime/, packages/, VERSION
|
||||
(default: the parent of the binary's bin/ directory)
|
||||
LUDIC_MODULES the project's fetched packages (default: ./ludic_modules)
|
||||
LUDIC_HOME where runtime/native/ lives (default: the binary's dir)
|
||||
```
|
||||
|
||||
Mode is automatic when neither `--windowed` nor `--headless` is given: a program
|
||||
|
|
|
|||
155
CONTRIBUTING.md
155
CONTRIBUTING.md
|
|
@ -18,25 +18,18 @@ runtime, and the tooling are all written in Ludic and built by Ludic.
|
|||
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
|
||||
mkdir -p bin && clang selfhost/ludicc.seed.ll -o bin/ludicc && bin/ludicc tools/x/main.ludic -o bin/x
|
||||
```
|
||||
|
||||
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/x`, the Ludic task runner that 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 help # what a user of the language sees
|
||||
bin/x build # rebuild the whole toolchain into bin/ (ludicc, ludic, x, ludic-fmt, ludic-lsp)
|
||||
bin/x help # list every command
|
||||
```
|
||||
|
||||
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 `x` from the repository root, so `assets/` and `selfhost/` resolve.
|
||||
|
||||
## The development loop
|
||||
|
||||
|
|
@ -44,18 +37,18 @@ 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/x reseed # regenerate selfhost/ludicc.seed.ll after a compiler change
|
||||
bin/x bootstrap-cfree # rebuild the compiler from the seed with NO C compiler in the loop
|
||||
bin/x 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 clean # remove build/, out.ppm and stray artifacts
|
||||
bin/x app <file.ludic> [--headless] # compile a program to a native app in build/
|
||||
bin/x selfhost-test # correctness + bootstrap fixpoints
|
||||
bin/x test-tools # the editor-toolchain suite (ludic-fmt, ludic-lsp)
|
||||
bin/x clean # remove build/, out.ppm and stray artifacts
|
||||
```
|
||||
|
||||
## Adding to the standard library
|
||||
|
|
@ -68,7 +61,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/x docs-gen --out build/pages && bin/x 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.
|
||||
|
|
@ -77,106 +70,43 @@ The stdlib lives in the runtime (`runtime/`) and is surfaced as namespaces
|
|||
## Versioning & releases
|
||||
|
||||
The toolchain is versioned with [SemVer](https://semver.org); `VERSION` is the
|
||||
single source of truth and `ludicc --version` (or `ludic version`) reports it.
|
||||
single source of truth and `ludicc --version` (or `x version`) reports it.
|
||||
|
||||
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
|
||||
x 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
|
||||
x 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
|
||||
`x 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 `x test`, `x test-tools` and `x 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
|
||||
source tarball, a Linux toolchain build, `SHA256SUMS`, and that version's
|
||||
`CHANGELOG.md` section as the notes. It refuses to publish if the tag and
|
||||
`VERSION` disagree or the changelog has no section for it.
|
||||
|
||||
Each toolchain artifact is a complete install root — `bin/` beside `runtime/`,
|
||||
`packages/` and `VERSION` — which is exactly what `install.sh` unpacks into
|
||||
`~/.ludic`. A release with no artifact for a platform is not a broken install
|
||||
there: the installer falls back to bootstrapping from the source tarball's IR
|
||||
seed. But the macOS artifacts are the ones most people get, so attach them.
|
||||
|
||||
macOS artifacts cannot be produced on the Linux runner — a `darwin-arm64` build
|
||||
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:
|
||||
macOS artifacts cannot be produced on the Linux runner. To attach one, run the
|
||||
same command CI runs from a Mac — it only adds assets the release is missing:
|
||||
|
||||
```bash
|
||||
FORGEJO_TOKEN=… ludic-dev publish v0.4.0
|
||||
```
|
||||
|
||||
Checksums are one `.sha256` file per artifact rather than a single `SHA256SUMS`,
|
||||
precisely because a release can be assembled from more than one host and an
|
||||
asset that already exists is never overwritten. Verify one with:
|
||||
|
||||
```bash
|
||||
shasum -a 256 -c ludic-0.4.0-src.tar.gz.sha256
|
||||
FORGEJO_TOKEN=… x publish v0.4.0
|
||||
```
|
||||
|
||||
The tag doubles as the reproducible bootstrap point: the source archive plus its
|
||||
checked-in seed rebuild that exact toolchain.
|
||||
|
||||
## Where the name and the URLs live
|
||||
|
||||
The language may yet be renamed and the project may yet move hosts, so the
|
||||
things that carry a name are kept few and listed here rather than discovered one
|
||||
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`
|
||||
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` |
|
||||
| `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 |
|
||||
|
||||
**The name itself.** A rename touches, in rough order of blast radius:
|
||||
|
||||
- **The file extension** `.ludic` — the compiler (`strip_ludic`, `do_import`,
|
||||
`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 install root** `~/.ludic` and the source directories `tools/ludic-cli/`,
|
||||
`tools/ludic-tools/`, `packages/ludic.*`.
|
||||
- **The environment variables** `LUDIC_HOME`, `LUDIC_CC`, `LUDIC_MODULES`,
|
||||
`LUDIC_STORE`, `LUDIC_PKG_PROXY`, `LUDIC_INSTALL_URL`, `LUDIC_KEEP_TMP`,
|
||||
`LUDIC_COVERAGE` — keep the old names working for a release if anyone has them
|
||||
in a script.
|
||||
- **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`.
|
||||
- **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
|
||||
toolchain, stages an install, and runs `new` → `build` → `test` through it, so a
|
||||
half-finished rename fails there rather than in someone's terminal.
|
||||
|
||||
## Conventions
|
||||
|
||||
- **Commits:** [Conventional Commits](https://www.conventionalcommits.org) —
|
||||
|
|
@ -191,7 +121,7 @@ half-finished rename fails there rather than in someone's terminal.
|
|||
| `perf` | a performance improvement |
|
||||
| `docs` | documentation only (`docs/`, README, comments) |
|
||||
| `test` | tests only |
|
||||
| `build` | the build/bootstrap machinery (seed, `bin/ludic`, linking) |
|
||||
| `build` | the build/bootstrap machinery (seed, `bin/x`, linking) |
|
||||
| `ci` | CI workflows under `.forgejo/` |
|
||||
| `style` | formatting/whitespace, no behaviour change |
|
||||
| `chore` | routine housekeeping with no other bucket |
|
||||
|
|
@ -213,7 +143,7 @@ half-finished rename fails there rather than in someone's terminal.
|
|||
so a green local commit is a green CI run.
|
||||
|
||||
- **Formatting:** `ludic-fmt` is the source of truth (2-space indent, LF, UTF-8);
|
||||
the repo `.editorconfig` mirrors it. Run `bin/ludic fmt` on files you touch.
|
||||
the repo `.editorconfig` mirrors it. Run `bin/ludic-fmt -w` on files you touch.
|
||||
The contract CI enforces is *idempotence* — `ludic-fmt` re-run on its own output
|
||||
is a no-op — which leaves deliberate hand alignment in place; it is not a
|
||||
blanket `fmt(x) == x`.
|
||||
|
|
@ -241,44 +171,11 @@ 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/x test` (and `bin/x 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.
|
||||
|
||||
## CI (self-hosted runners)
|
||||
|
||||
Every workflow starts by cloning `${{ github.server_url }}/${{ github.repository }}`.
|
||||
On a self-hosted Forgejo runner that URL is usually the instance's *internal*
|
||||
address (e.g. `http://forgejo:3000`), so **the job container must be able to
|
||||
resolve it**. The runner puts each job on a fresh per-job network by default,
|
||||
which the Forgejo container is not attached to — so the clone fails with:
|
||||
|
||||
```
|
||||
fatal: unable to access 'http://forgejo:3000/…': Could not resolve host: forgejo
|
||||
```
|
||||
|
||||
Give the runner a config that pins job containers to a network Forgejo is also
|
||||
on. A dedicated network is better than the general application network, so a CI
|
||||
job cannot reach unrelated services:
|
||||
|
||||
```yaml
|
||||
# the runner's config.yml, passed with: forgejo-runner daemon --config …
|
||||
container:
|
||||
network: forgejo-ci
|
||||
```
|
||||
|
||||
with `forgejo-ci` attached to the Forgejo container as well. Verify it without
|
||||
running a workflow:
|
||||
|
||||
```bash
|
||||
docker run --rm --network forgejo-ci alpine:3 getent hosts forgejo
|
||||
```
|
||||
|
||||
This failure mode is intermittent if left unfixed: Docker forwards names it
|
||||
cannot resolve to the host's resolver, which may answer for the container name
|
||||
often enough that CI looks healthy for a while.
|
||||
|
||||
## Reporting issues
|
||||
|
||||
Use the templates under [`.forgejo/issue_template/`](.forgejo/issue_template):
|
||||
|
|
|
|||
1694
LANGUAGE.md
1694
LANGUAGE.md
File diff suppressed because it is too large
Load diff
295
README.md
295
README.md
|
|
@ -1,200 +1,189 @@
|
|||
# Ludic
|
||||
|
||||
A compiled language for 2D games. The entity-component system is part of the
|
||||
syntax, the runtime is deterministic fixed-point, and `ludicc` lowers Ludic
|
||||
straight to LLVM IR — **no C is generated, compiled or linked in a build.**
|
||||
Ludic is an **ahead-of-time compiled** language for 2D games with an
|
||||
entity-component core, a deterministic fixed-point runtime, and its graphics
|
||||
stack built into the language. `ludicc` lowers Ludic straight to LLVM IR and
|
||||
emits a native binary — and **`ludicc` is itself written in Ludic**, compiles
|
||||
its own source to a byte-exact fixpoint, and rebuilds from a checked-in IR seed
|
||||
with **no C compiler in the loop**.
|
||||
|
||||
The compiler is written in Ludic. It compiles its own source to a byte-exact
|
||||
fixpoint and rebuilds from a checked-in IR seed with clang alone; CI asserts
|
||||
that on every push.
|
||||
|
||||
- **Documentation:** <https://workshopsoft.pages.workshopsoft.io/ludic/>
|
||||
- **API reference:** <https://workshopsoft.pages.workshopsoft.io/ludic/api.html>
|
||||
- **Issues:** <https://git.workshopsoft.io/workshopsoft/ludic/issues>
|
||||
|
||||
```ludic
|
||||
program Hello {
|
||||
|
||||
property Position { column: int = 0, row: int = 0 }
|
||||
property Velocity { delta_x: int = 0, delta_y: int = 0 }
|
||||
|
||||
handler SpawnEnemies phase Start {
|
||||
spawn Enemy { Position { column: 3, row: 4 }, Velocity { delta_x: 1, delta_y: 0 } }
|
||||
spawn Enemy { Position { column: 10, row: 2 }, Velocity { delta_x: 0, delta_y: 1 } }
|
||||
}
|
||||
|
||||
# a handler declares the entities it touches; the body runs
|
||||
# once per match, with each property bound by name.
|
||||
@Queries(these: [Position, Velocity])
|
||||
handler AdvancePositions phase FixedUpdate {
|
||||
Position.column += Velocity.delta_x
|
||||
Position.row += Velocity.delta_y
|
||||
}
|
||||
}
|
||||
```
|
||||
.ludic ──► ludicc ──► LLVM IR ──► object ──► native binary
|
||||
(in Ludic)
|
||||
```
|
||||
|
||||
## Getting started
|
||||
**No C is generated, compiled or linked in a build.** No interpreter, no
|
||||
transpiler, no C runtime: the framebuffer, sprites, PNG/DEFLATE decoding,
|
||||
TrueType text, the retained UI, the registers and the RNG are all written in
|
||||
Ludic (`runtime/native/*.ludic`); only the window seam — the `win_*` functions
|
||||
for the window, keys, mouse, cursor, gamepad and touch — is hand-written LLVM IR
|
||||
against the platform ABI (`runtime/native/cocoa.ll`),
|
||||
the same floor Rust and Swift stand on.
|
||||
|
||||
Install the toolchain — the compiler, the `ludic` CLI, the engine runtime, the
|
||||
formatter and the language server — with one command:
|
||||
## Backends
|
||||
|
||||
| Backend | Status |
|
||||
|---|---|
|
||||
| **Native 2D** (macOS/Cocoa window; headless render for CI) | **Shipping** — the default `bin/x app` target. |
|
||||
| **Web / wasm32** | **In progress.** The browser platform layer is in-tree and documented — `runtime/web/` (the `<canvas>` window `platform.js`, the libc-free `wasm.ll` floor) and a Node harness that diffs native vs. wasm frame-for-frame (`tools/ludic-web/run.mjs`). Emitting wasm was a capability of the retired C compiler and is **not yet re-wired on the self-hosted toolchain**; see [COMPILING.md](COMPILING.md). |
|
||||
|
||||
The same is true of `--target` cross-compilation and `--shared` libraries: both
|
||||
are designed and documented, both lived in the old C compiler, and both are
|
||||
pending re-implementation on the self-hosted native toolchain.
|
||||
|
||||
## Quick start
|
||||
|
||||
`bin/x` is the project's task runner — one native binary, written in Ludic and
|
||||
compiled by Ludic, that replaces every build/test/bootstrap shell script.
|
||||
Bootstrap it once from a clean checkout (the only step Ludic can't do for
|
||||
itself, since compiling Ludic needs a compiler) with clang alone:
|
||||
|
||||
```bash
|
||||
curl -fsSL https://workshopsoft.pages.workshopsoft.io/ludic/install.sh | sh
|
||||
mkdir -p bin && clang selfhost/ludicc.seed.ll -o bin/ludicc && bin/ludicc tools/x/main.ludic -o bin/x
|
||||
```
|
||||
|
||||
It installs into `~/.ludic` and puts `~/.ludic/bin` on your `PATH` in every
|
||||
shell — the PATH line lives in `~/.ludic/env`, sourced from `~/.profile`,
|
||||
`~/.zshenv` and your bash or fish config. Nothing else on the machine is touched;
|
||||
uninstalling is `rm -rf ~/.ludic` and deleting those two-line blocks. Where a
|
||||
prebuilt toolchain exists for your platform it is downloaded and verified against
|
||||
a published checksum; where it does not, the installer bootstraps from the
|
||||
compiler's own IR seed with clang. Either way you need clang (or Xcode's Command
|
||||
Line Tools) to link, since Ludic emits LLVM IR and links it natively.
|
||||
|
||||
Then make a game:
|
||||
Then build the whole toolchain and run a game:
|
||||
|
||||
```bash
|
||||
ludic new mygame
|
||||
cd mygame
|
||||
ludic run # compiles src/main.ludic and opens a native window
|
||||
bin/x build # -> bin/{ludicc,ludic,x,ludic-fmt,ludic-lsp}
|
||||
bin/x app examples/games/snake.ludic # compile + open a native window
|
||||
./build/snake
|
||||
```
|
||||
|
||||
`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:
|
||||
Render a frame headlessly (what CI checks) — output lands in `build/`, never the
|
||||
repo root:
|
||||
|
||||
```bash
|
||||
ludic test
|
||||
ludic build --headless
|
||||
printf 'ddddwww' | ./build/mygame_headless # writes build/out.ppm
|
||||
bin/x app examples/games/chronorift.ludic --headless
|
||||
mkdir -p build && printf 'ddddwww' | ./build/chronorift_headless # writes build/out.ppm
|
||||
sips -s format png build/out.ppm --out frame.png
|
||||
```
|
||||
|
||||
`ludic help` lists every command, and `ludic doctor` checks the install.
|
||||
[`examples/`](examples/README.md) is a tour grouped by intent: games, rendering,
|
||||
ECS, events, networking, language features and the standard library — compile any
|
||||
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:
|
||||
Run the suites:
|
||||
|
||||
```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/x test # full regression: compiler builds from seed, every example, golden renders
|
||||
bin/x selfhost-test # correctness + the self-hosting / C-free bootstrap fixpoints
|
||||
bin/x help # every command
|
||||
```
|
||||
|
||||
## The language
|
||||
|
||||
- **ECS in the syntax.** `property`, `model` and `handler` are keywords. Query
|
||||
with `for (a, b) in query [A, B, {Tag}] where <expr> { … }`; `spawn` and
|
||||
`despawn` recycle entity slots; `@`-annotations drive lifecycle hooks.
|
||||
- **Deterministic by construction.** Q16.16 `fixed` arithmetic and a seeded RNG
|
||||
give the same frame byte-for-byte on every run — the basis for replays,
|
||||
lockstep netcode and golden-image tests.
|
||||
- **Scenes and state machines.** `scene` / `layer` / `become` model
|
||||
mutually-exclusive game states with enter and exit hooks; `match` / `machine`
|
||||
/ `state` handle dispatch and per-entity FSMs.
|
||||
- **Events and networking.** A cancellable event bus (`event` / `emit` / `@On`)
|
||||
and networking primitives (`@Sync`, ownership, RPCs) over a built-in transport.
|
||||
- **Batteries in the language.** Framebuffer primitives, PNG sprites, TrueType
|
||||
text and a retained `ui` widget tree declared as data, plus a namespaced
|
||||
standard library (`Math`, `Text`, `List`, `Random`, `Crypto`, `Tiled`, …).
|
||||
- **Whole-world snapshots.** `save()` and `load()` serialize every entity,
|
||||
property and program `var` in one call.
|
||||
|
||||
[LANGUAGE.md](LANGUAGE.md) is the full reference; the
|
||||
[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.
|
||||
Add a dependency (URL-as-identity, MVS resolution, content-addressed store):
|
||||
|
||||
```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
|
||||
bin/x add git.workshopsoft.io/user/pkg # resolve + fetch + link into ludic_modules/
|
||||
bin/x get # install everything in package.ludic, write the lock
|
||||
bin/x verify # check the locked packages against the store
|
||||
```
|
||||
|
||||
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.
|
||||
See [`docs/PACKAGES.md`](docs/PACKAGES.md) for the manifest, lockfile and package model.
|
||||
|
||||
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).
|
||||
## Layout
|
||||
|
||||
## Packages
|
||||
| Path | What it is |
|
||||
|------|-----------|
|
||||
| [`selfhost/*.ludic`](selfhost/) | **the compiler, written in Ludic** — lexer, parser, and the LLVM-IR backend (ECS storage, queries, spawn, `match`/`machine`, UI, scenes, save/load, fixed-point). Built from `selfhost/ludicc.seed.ll` with clang alone. |
|
||||
| [`selfhost/golden/renders.sha256`](selfhost/golden/renders.sha256) | text baseline of render-output hashes (replaces binary `.ppm` fixtures); regenerate with `bin/x golden`. |
|
||||
| [`tools/x/*.ludic`](tools/x/) | **the task runner, written in Ludic** — one binary (`bin/x`) that builds, tests, bootstraps and reseeds the project, replacing every shell script. |
|
||||
| [`runtime/native/`](runtime/native/) | the runtime **in Ludic** for the native path: `core` (framebuffer, input, RNG), `image`/`inflate` (PNG + DEFLATE, no zlib), `truetype` (glyph rasterizer), `ui` (retained widget tree); plus `cocoa.ll`, the macOS window seam in LLVM IR. |
|
||||
| [`runtime/web/`](runtime/web/) | the browser platform layer: `platform.js` (the `<canvas>` window), `wasm.ll` (the libc-free floor), `index.html`. |
|
||||
| [`examples/`](examples/README.md) | the example tour, grouped by intent — `games/`, `rendering/`, `ecs/`, `events/`, `networking/`, `lang/`, `library/`. See [examples/README.md](examples/README.md). |
|
||||
| [`tools/ludic-tools/`](tools/ludic-tools/) | the editor toolchain **in Ludic**: `ludic-fmt` (formatter) and `ludic-lsp` (language server) — one lexer, one vocabulary shared by both. |
|
||||
| [`tools/editors/`](tools/editors/README.md) | plugins for VS Code and JetBrains, plus config for Neovim, Helix, Emacs, Sublime and Zed. |
|
||||
| [`docs/`](docs/) | the per-symbol API reference, regenerated into the docs site. |
|
||||
| [`COMPILING.md`](COMPILING.md) | the native pipeline: `ludicc → LLVM IR → exe`, the `rt_*` runtime protocol, and the (pending) wasm/cross-compile/shared-library paths. |
|
||||
|
||||
Dependencies are identified by URL, resolved with minimal version selection, and
|
||||
cached in a content-addressed store:
|
||||
The design and roadmap material lives on the **[wiki](https://git.workshopsoft.io/workshopsoft/ludic/wiki)**:
|
||||
the [Events](https://git.workshopsoft.io/workshopsoft/ludic/wiki/Design/Events),
|
||||
[Networking](https://git.workshopsoft.io/workshopsoft/ludic/wiki/Design/Networking),
|
||||
[Scenes](https://git.workshopsoft.io/workshopsoft/ludic/wiki/Design/Scenes),
|
||||
[Lifecycle](https://git.workshopsoft.io/workshopsoft/ludic/wiki/Design/Lifecycle),
|
||||
[Mobile](https://git.workshopsoft.io/workshopsoft/ludic/wiki/Design/Mobile) and
|
||||
[Syntax-redesign](https://git.workshopsoft.io/workshopsoft/ludic/wiki/Design/Syntax-Redesign)
|
||||
design records, the [Bootstrap deep-dive](https://git.workshopsoft.io/workshopsoft/ludic/wiki/Bootstrap),
|
||||
and the [Luanti roadmap](https://git.workshopsoft.io/workshopsoft/ludic/wiki/Roadmap/Luanti).
|
||||
The root keeps only this README plus the two user-facing references,
|
||||
[`LANGUAGE.md`](LANGUAGE.md) and [`COMPILING.md`](COMPILING.md).
|
||||
|
||||
```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
|
||||
```
|
||||
## Language at a glance
|
||||
|
||||
The `ludic.*` packages — canonical ECS components, the gameplay, platformer,
|
||||
RPG, shooter and NPC-AI modules — ship with the toolchain, so importing one needs
|
||||
no fetch step at all.
|
||||
- `program` / `property` (typed fields + defaults) / `model` (named entity kinds)
|
||||
/ `system` (`phase`, `@annotations`, `reads`/`writes`).
|
||||
- ECS queries `for (a, b) in query [A, B, {Tag}] where <expr> { … }`,
|
||||
`spawn`/`despawn` with slot reuse, `@`-driven lifecycle hooks.
|
||||
- An **event bus** (`event` / `emit` / `@On`, cancellable, `@Public` promotion)
|
||||
and **networking** primitives (`@Sync`, ownership, RPCs) over a built-in
|
||||
loopback transport — all deterministic, all pure Ludic.
|
||||
- `scene` / `layer` / `become`, `match` / `machine` + `state`.
|
||||
- Types `int`, `fixed` (Q16.16), `bool`, `entity`, `str`, `byte`, typed buffers;
|
||||
a growing namespaced **standard library** (`Math`, `Vector`, `Time`/`Date`/
|
||||
`Duration`/`Clock`, `Random`, `Hash`, `Crypto`, sorting, …).
|
||||
- Deterministic seeded RNG and `save()`/`load()` snapshot of the whole World.
|
||||
- Built-in 2D: framebuffer primitives, PNG sprites, TrueType text, 9-slice, and
|
||||
a retained `ui` widget tree declared as data.
|
||||
|
||||
See [`docs/PACKAGES.md`](docs/PACKAGES.md) for the manifest and lockfile model.
|
||||
See [LANGUAGE.md](LANGUAGE.md) for the full reference, and
|
||||
[examples/README.md](examples/README.md) for runnable demos of each feature.
|
||||
|
||||
## Editor support
|
||||
|
||||
Editors spawn `ludic lsp`; the server ships with the toolchain, so there is
|
||||
nothing extra to install. It speaks LSP 3.17 over stdio, so one binary serves
|
||||
every editor: completion, diagnostics from the compiler itself, go-to-definition
|
||||
and rename across imports, and comment-preserving formatting. `ludic fmt` runs
|
||||
the same formatter as a CLI, for pre-commit hooks. Both understand
|
||||
```` ```ludic ```` fences in Markdown. Plugins and drop-in config for VS Code, JetBrains, Neovim,
|
||||
Helix, Emacs, Sublime and Zed are in [`tools/editors/`](tools/editors/README.md).
|
||||
```bash
|
||||
bin/x tools # -> bin/ludic-fmt, bin/ludic-lsp
|
||||
```
|
||||
|
||||
## Status
|
||||
`ludic-lsp` speaks LSP 3.17 over stdio, so one binary serves every editor:
|
||||
context-aware completion, diagnostics from the compiler itself,
|
||||
go-to-definition and rename across `import`ed files, and comment-preserving
|
||||
formatting. `ludic-fmt` is the same formatter as a CLI, for pre-commit hooks and
|
||||
CI. Both also understand ```` ```ludic ```` fences in Markdown. Plugins and
|
||||
drop-in config are in [`tools/editors/`](tools/editors/README.md).
|
||||
|
||||
The native 2D backend ships: a Cocoa window on macOS, a headless renderer for
|
||||
CI, and the whole runtime — framebuffer, PNG/DEFLATE decoding, TrueType
|
||||
rasterizer, retained UI, RNG — written in Ludic under
|
||||
[`runtime/native/`](runtime/native/). Only the window seam (`win_*`: window,
|
||||
keys, mouse, cursor, gamepad, touch) is hand-written LLVM IR against the
|
||||
platform ABI, the same floor Rust and Swift stand on.
|
||||
## Chrono Rift — the flagship game
|
||||
|
||||
The **web/wasm32 backend is not currently available.** The browser platform
|
||||
layer is in-tree under [`runtime/web/`](runtime/web/), but emitting wasm was a
|
||||
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).
|
||||
[`examples/games/chronorift.ludic`](examples/games/chronorift.ludic) is a
|
||||
playable co-op JRPG — overworld, dungeon, random encounters, a turn-based co-op
|
||||
battle, a boss, an item shop and snapshot save/load — split across modules under
|
||||
[`games/chronorift/`](examples/games/chronorift/). Its art is CC0
|
||||
[Kenney](https://kenney.nl) sprites, decoded from PNG at runtime by the
|
||||
Ludic-written PNG/DEFLATE decoder — no zlib, no external dependency.
|
||||
|
||||
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).
|
||||
- **Overworld:** `WASD` move, `K` save, `L` load.
|
||||
- **Battle (local co-op):** P1/Knight `W`/`S` select, `Space` confirm;
|
||||
P2/Mage `I`/`K` select, `J` confirm.
|
||||
|
||||
## Status & roadmap
|
||||
|
||||
The compiler self-hosts to a byte-exact fixpoint and rebuilds from its IR seed
|
||||
with no C compiler; the ECS runtime, windowed + headless 2D rendering, the event
|
||||
bus, the deterministic networking stack, scenes, and save/load are all in place
|
||||
and covered by `bin/x test`. CI gates every push and PR on the build, the test
|
||||
suites, and that C-free fixpoint. The toolchain is versioned with SemVer
|
||||
(`ludicc --version`); releases and the `CHANGELOG.md` are cut from changesets by
|
||||
`x release`. Upgrading from 0.2? See the
|
||||
[0.2 → 0.3 migration guide](docs/MIGRATION-0.2-to-0.3.md).
|
||||
|
||||
Active work and proposals — the standard library, a fuller type system,
|
||||
rendering/animation/lighting extras, input, filesystem/IO, testing, and
|
||||
re-wiring the web/wasm and cross-compile backends — are tracked as issues, not
|
||||
inlined here:
|
||||
|
||||
- **Issues & proposals:** <https://git.workshopsoft.io/workshopsoft/ludic/issues>
|
||||
- **Docs site (API reference):** <https://workshopsoft.pages.workshopsoft.io/ludic/>
|
||||
- **Wiki (design & roadmap):** <https://git.workshopsoft.io/workshopsoft/ludic/wiki>
|
||||
|
||||
## Contributing
|
||||
|
||||
[CONTRIBUTING.md](CONTRIBUTING.md) covers the development loop, the commit and
|
||||
code conventions, how the bootstrap fixpoint works, and what a self-hosted CI
|
||||
runner needs. Issue and pull-request templates are under
|
||||
[`.forgejo/`](.forgejo/).
|
||||
See [CONTRIBUTING.md](CONTRIBUTING.md) for the development loop
|
||||
(`bin/x reseed` → `bin/x bootstrap-cfree` → `bin/x test`), the code and commit
|
||||
conventions, and how the bootstrap fixpoint works. Issue and pull-request
|
||||
templates live under [`.forgejo/`](.forgejo/).
|
||||
|
||||
## License
|
||||
|
||||
The compiler and runtime are licensed under the
|
||||
[Apache License 2.0](LICENSE) (`SPDX-License-Identifier: Apache-2.0`).
|
||||
The Ludic compiler and runtime source are licensed under the
|
||||
[Apache License 2.0](LICENSE) (`SPDX-License-Identifier: Apache-2.0`) — a
|
||||
permissive license with an explicit patent grant.
|
||||
|
||||
The bundled [Kenney](https://kenney.nl) art under `assets/kenney/` is
|
||||
third-party and released under **CC0 1.0**; each pack keeps its own
|
||||
`License.txt`. Code and assets are licensed separately — Apache-2.0 covers the
|
||||
source, not the art.
|
||||
third-party and released under **CC0 1.0** (public domain); each pack keeps its
|
||||
own `License.txt`. Code and assets are licensed separately: Apache-2.0 covers
|
||||
the source, not the art.
|
||||
|
|
|
|||
2
VERSION
2
VERSION
|
|
@ -1 +1 @@
|
|||
0.22.0
|
||||
0.4.0
|
||||
|
|
|
|||
|
|
@ -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. `x 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 `x 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
|
||||
x 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.
|
||||
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