• v0.22.0 f094acfdb5

    v0.22.0
    All checks were successful
    bootstrap / cfree-fixpoint (push) Successful in 37s
    ci / build-and-test (push) Successful in 3m51s
    commit-lint / conventional-commits (push) Successful in 6s
    docs / build-and-deploy (push) Successful in 44s
    release / publish (push) Successful in 3m43s
    Pre-release

    orkun released this 2026-09-18 00:04:28 +02:00 | 838 commits to main since this release

    v0.22.0 — 2026-09-18

    Features

    • Every key on the keyboard is bindable — the F-row, Home/End/PageUp/PageDown,
      Insert/Delete, Caps Lock and the numpad reach a game as codes 132-166.

      Until now the platform gave them no code at all, so a game's rebinding screen could
      not take one and nothing said why. Windows asked the active layout what they type and
      got nothing back (w_vk_char answers 0 for a key with no character); macOS let them
      fall through to charactersIgnoringModifiers, which reports NSF1FunctionKey and its
      neighbours at 0xF704 and up - outside the 256-bit held set either way.

      • The codes are the same on both platforms: 132-143 are F1-F12, 144-149 are Home,
        End, PageUp, PageDown, Insert and Delete, 150 is Caps Lock, 152-161 are the numpad
        digits and 162-166 its *, +, -, . and /. The numpad's Enter is Enter.
      • Key.F1, Key.Home, Key.Numpad0 and the rest fold at compile time like the
        other named keys.
      • Input.key_label names them - "F1", "Home", "CapsLock", "Num 0", "Num /" - and
        does not ask the layout, because a key that types nothing is called the same thing
        on every layout.
    Downloads
  • v0.21.1 9dfa2951f6

    v0.21.1
    All checks were successful
    bootstrap / cfree-fixpoint (push) Successful in 36s
    ci / build-and-test (push) Successful in 3m50s
    commit-lint / conventional-commits (push) Successful in 6s
    docs / build-and-deploy (push) Successful in 45s
    release / publish (push) Successful in 3m40s
    Pre-release

    orkun released this 2026-09-17 15:23:57 +02:00 | 840 commits to main since this release

    v0.21.1 — 2026-09-17

    Fixes

    • A windowed game opens under its package's app name. ludic build, ludic run and
      ludic bundle pass it to the compiler (ludicc --title <name>), so game_title() - the
      window's first title - is the name the player knows rather than the program name, which
      showed for up to two seconds before the renderer retitled the window. Without an app name
      it is the program name, as before.
    • Crypto.random_bytes, Crypto.random_hex, Crypto.random_u32 and Uuid.* draw from the
      system CSPRNG on Windows (RtlGenRandom, advapi32). They read /dev/urandom, which Windows
      does not have, and every byte came back zero.
    Downloads
  • v0.21.0 28f661c36f

    v0.21.0
    All checks were successful
    bootstrap / cfree-fixpoint (push) Successful in 36s
    ci / build-and-test (push) Successful in 3m50s
    commit-lint / conventional-commits (push) Successful in 5s
    docs / build-and-deploy (push) Successful in 44s
    release / publish (push) Successful in 3m40s
    Pre-release

    orkun released this 2026-09-17 14:59:35 +02:00 | 842 commits to main since this release

    v0.21.0 — 2026-09-17

    Features

    • A launcher's runtime — start a program, step aside while it runs, and download large
      files with a progress bar.

      • Process.* — Process.spawn(path, args) starts a program directly (no shell) with this
        process's environment and working directory and returns at once; Process.poll(h) is -1
        while it runs, then its exit code (128 + signal for a signal death, 137 after
        Process.kill); Process.free lets a handle go. posix_spawn on macOS
        (runtime/native/process.ll); CreateProcessW on Windows (process_win.ll), with each
        argument quoted by the MSVC rules and no console window. Linked only into a program that
        uses Process.*.
      • Http.save_to(h, path) — before Http.send, streams the response body straight into a
        file (written at path itself, created on send) instead of memory; Http.text of such a
        request is empty and Http.body_len is the bytes written. Http.received(h) and
        Http.expected(h) (the Content-Length, or -1) are live while it is pending. macOS
        streams through an NSURLSession with a delegate built at run time, Windows through the
        WinHTTP read loop. Freeing a pending request now cancels it and parks its slot until the
        worker has let go, rather than freeing what the worker is still writing.
      • App.window_hide() / App.window_show() — take the game's window off the screen and
        bring it back without closing it or its GL / Vulkan surface; the run goes on while it is
        hidden. No-ops headless and before there is a window.

    Fixes

    • A game's window takes the title it asks for. The runtime opens a game's window before main,
      titled with the program name, and Gl.open / render3d's gl_open and gvk_open attached
      to it without passing their title on, so a program Valley that opened "Maroon Lake" showed
      "Valley". win_set_title (cocoa.ll setTitle:, win32.ll SetWindowTextW, UTF-8 to UTF-16)
      now retitles it, and a second win_open on macOS retitles the open window instead of making
      another, as it already did on Windows.
    Downloads
  • v0.20.0 c1db0d71ed

    v0.20.0
    All checks were successful
    bootstrap / cfree-fixpoint (push) Successful in 35s
    ci / build-and-test (push) Successful in 3m41s
    commit-lint / conventional-commits (push) Successful in 5s
    docs / build-and-deploy (push) Successful in 43s
    release / publish (push) Successful in 3m29s
    Pre-release

    orkun released this 2026-09-17 11:19:19 +02:00 | 846 commits to main since this release

    v0.20.0 — 2026-09-17

    Features

    • The JetBrains plugin (1.6.0) has a Ludic tool window for the package, like the Gradle one:
      • an overview of the package (what it is, entry, version, dependencies, scripts, hooks, app);
      • its commands, scripts and hooks, run by double-click, with each command's hooks shown;
      • its dependencies against package.lock.ludic: the locked version, whether it is fetched
        and linked, the indirect ones, and Fetch, Update, Verify, Vendor, Add and Remove;
      • the app preview the app lines describe, and the asset roots with missing ones marked.
        The bar over package.ludic now appears only when something needs doing (fetch, or no
        toolchain), instead of carrying every command.
    Downloads
  • v0.19.0 48c701cf1b

    v0.19.0
    All checks were successful
    bootstrap / cfree-fixpoint (push) Successful in 44s
    ci / build-and-test (push) Successful in 3m40s
    commit-lint / conventional-commits (push) Successful in 5s
    docs / build-and-deploy (push) Successful in 42s
    release / publish (push) Successful in 3m32s
    Pre-release

    orkun released this 2026-09-17 10:56:30 +02:00 | 848 commits to main since this release

    v0.19.0 — 2026-09-17

    Features

    • Udp.*: polled IPv4 datagrams - the transport under a game's own netcode. Udp.open(port)
      binds a non-blocking socket (port 0 picks one, Udp.port says which), Udp.send sends one
      datagram to an address and port, and Udp.recv drains what arrived without ever blocking;
      Udp.from_ip / Udp.from_port name the sender. Udp.ip / Udp.ip_text convert addresses,
      Udp.resolve looks a host name up and Udp.local_ip is the address a player on the same
      network would dial. BSD sockets on macOS (runtime/native/udp.ll), Winsock on Windows
      (udp_win.ll, linked with ws2_32), linked only into a program that uses Udp.*.
    Downloads
  • v0.18.1 6a7f1dd393

    v0.18.1
    All checks were successful
    bootstrap / cfree-fixpoint (push) Successful in 34s
    ci / build-and-test (push) Successful in 3m39s
    commit-lint / conventional-commits (push) Successful in 4s
    release / publish (push) Successful in 3m29s
    Pre-release

    orkun released this 2026-09-16 15:36:18 +02:00 | 850 commits to main since this release

    v0.18.1 — 2026-09-16

    Fixes

    • The release build links libm on Linux, which float/double code needs (glibc keeps
      sinf, sqrtf and the rest there). 0.18.0's release job stopped on this before
      publishing, so 0.18.1 is the first published build with float types, barrel imports,
      package scripts and hooks, and the JetBrains 1.5.0 support.
    Downloads
  • v0.17.0 da57b20156

    v0.17.0
    All checks were successful
    bootstrap / cfree-fixpoint (push) Successful in 44s
    ci / build-and-test (push) Successful in 3m32s
    commit-lint / conventional-commits (push) Successful in 5s
    docs / build-and-deploy (push) Successful in 41s
    release / publish (push) Successful in 3m22s
    Pre-release

    orkun released this 2026-09-16 15:13:00 +02:00 | 856 commits to main since this release

    v0.17.0 — 2026-09-16

    Features

    • Renderer switches stay out of shipped games — every R3D_* environment switch in
      ludic.render3d (debug views, feature kills, file writers, hardware fakes, the Windows
      feature overrides) now goes through one gate: a headless build honours them as before, a
      windowed build only when R3D_DEV is set to anything but 0. See docs/SHIPPING.md.

    Fixes

    • A lake can be the water that mirrors the world — a reflecting body is clipped to its
      ellipse like any other unless it is an unbounded sea, so a map whose reflection belongs to
      its lake no longer floods every hollow in the survey with that lake's level; and the
      terrain's wet shore, forest and scree gates read the carved lake's line inside its outline
      (u_lake), the rule the grass already used, so a lake above the sea keeps its wet bank and
      no forest is painted down its bed.

      terrain_lake_carve(false) keeps a lake's line, outline and shore but skips carving its
      bed, for a height map that already carries a shaped one.

    Downloads
  • v0.16.2 132deac5bc

    v0.16.2
    All checks were successful
    bootstrap / cfree-fixpoint (push) Successful in 33s
    ci / build-and-test (push) Successful in 3m32s
    commit-lint / conventional-commits (push) Successful in 5s
    docs / build-and-deploy (push) Successful in 41s
    release / publish (push) Successful in 3m22s
    Pre-release

    orkun released this 2026-09-15 22:27:04 +02:00 | 862 commits to main since this release

    v0.16.2 — 2026-09-15

    Fixes

    • Keys are physical positions on Windows and macOS, whatever the layout - Input.key_down('w')
      is the key above S on every keyboard.

      • Windows keyed the held set by the character the active layout gave a virtual key, so
        a game's WASD belonged to whatever the layout put there: on AZERTY W and A were other
        keys, and with an input method on (Chinese, Japanese, Korean) every letter arrived as
        VK_PROCESSKEY and no letter key worked at all. The typing block is now read from the
        scancode; the arrows, the numpad and the F-keys still go by virtual key, and the frame
        key (Input.key, typing) keeps the layout's character.
      • macOS did the same through charactersIgnoringModifiers; it now reads keyCode.
      • Input.key_label(key) names a key for the player in their own layout - 'w' shows
        as "W" on QWERTY and "Z" on AZERTY - so a game that shows its bindings never shows a
        key the player cannot find. Windows reads the layout; headless, macOS and the browser
        give the US character.
    Downloads
  • v0.16.1 2a2f463d5b

    v0.16.1
    All checks were successful
    bootstrap / cfree-fixpoint (push) Successful in 33s
    ci / build-and-test (push) Successful in 3m30s
    commit-lint / conventional-commits (push) Successful in 4s
    release / publish (push) Successful in 3m21s
    Pre-release

    orkun released this 2026-09-15 22:01:01 +02:00 | 865 commits to main since this release

    v0.16.1 — 2026-09-15

    Features

    • HDR calibration, and two HDR fixes — the HDR10 picture follows the player's display instead of one fixed curve.

      • r3d_hdr_calibrate(peak, paper, black) — nits as float bits: the brightest the display shows (100-10000),
        where the picture's and the interface's white sit (80-1000, never above the peak) and how far the darkest shade is
        lifted (0-5, fading out by paper white). The tonemap's and the overlay's HDR10 variants read them, and the display's
        HDR metadata is sent again with the new peak. The defaults are the old constants: 1000, 200 and 0.
      • ov_hdr_nits(nits) / ov_hdr_paper() — what the overlay draws next is at that many nits on an HDR10 frame,
        for a calibration screen's test patches; it closes the batch so far. No effect on an SDR frame.
      • Fixed: a crash toggling HDR on the Vulkan renderer. gpu_caps_probe() made and destroyed a second Vulkan
        instance under NVIDIA Streamline's interposer; the next swapchain rebuild then called through a pointer that
        instance had left behind, at address 0. With the Vulkan renderer running, the probe asks its instance instead.
      • Fixed: yellow read as red in HDR. The overlay drew sRGB values straight into the HDR10 swapchain (it now has an
        HDR10 variant, chosen while gpu_hdr_active()), and the tonemap extended highlights per channel, which boosted a
        bright yellow's red far more than its green; it now brightens the graded colour by one factor.
    Downloads
  • v0.16.0 95a65b05ca

    v0.16.0
    Some checks failed
    bootstrap / cfree-fixpoint (push) Successful in 33s
    ci / build-and-test (push) Successful in 3m29s
    commit-lint / conventional-commits (push) Failing after 5s
    docs / build-and-deploy (push) Successful in 41s
    release / publish (push) Successful in 3m19s
    Pre-release

    orkun released this 2026-09-15 19:43:08 +02:00 | 868 commits to main since this release

    v0.16.0 — 2026-09-15

    Features

    • A loading screen can show the renderer's start-up as it happens. r3d_open(w, h, title) opens
      the window and the graphics backend with nothing baked, and r3d_load_count() / r3d_load_step(i)
      run the rest - the sky and its light, the terrain, the shadow and screen targets, the scattered cover
      and actors, the grass - one step at a time, so a game can draw and present a frame between them.
      r3d_init is those same calls in a row and is unchanged for everything that uses it.

    • HDR10 output on the Vulkan renderer — r3d_hdr(on) asks for an ST 2084 / BT.2020 swapchain where the
      display offers one.

      The tonemap's HDR10 variant keeps the SDR picture up to a 200-nit paper white and rolls highlights on to
      1000 nits; the overlay converts the interface to the same white; HDR metadata is set where the loader has
      the command. The screen and LDR images are 10-bit while it is on, and screenshots refuse rather than write
      a PQ image as SDR. OpenGL and the Vulkan SDR frame are unchanged. R3D_HDR overrides the setting.

    • Mesh-shader grass — on a card with VK_EXT_mesh_shader, r3d_mesh_grass(on) draws each grass tile as one
      mesh-shader dispatch sized to that tile's blades, and a blade that is culled emits no vertices at all.

      • ludic-dev shaders — a variant whose first file is *.mesh compiles that stage as a Vulkan 1.3 mesh
        shader; its SPIR-V keeps the .vert name, so the manifest is unchanged.
      • The seam — gpu_has_mesh() and gpu_draw_mesh_tasks(x, y, z); a mesh program's pipeline has no
        vertex input and its bindings the mesh stage. The device enables meshShader and maintenance4.

      Not yet faster: at 4K on an RTX 3070 Ti the grass pass takes 5.0 ms against the chunked path's 1.7, so
      gpu_feature_implemented(GF_MESH_GRASS) stays false and a game should not offer it as a finished setting.

    • More of the renderer's quality is a switch a game can offer.

      • sky_set_quality(width): the prefiltered sky light at 256, 512 or 1024 wide (half as tall),
        baked again at once.
      • post_set_msaa(samples): 1, 2 or 4 samples for the scene on OpenGL, remaking the screen targets in
        place (post_msaa_live() is false on Vulkan, which has no sample counts yet); post_free now
        frees the multisampled framebuffer too.
      • STREAM_BUDGET_US is a variable: the microseconds a frame may spend generating streamed cover.
      • r3d_fog_scale: a multiplier over the fog density the daylight and the weather set.
    • NVIDIA DLSS super resolution and Reflex on the Vulkan renderer — through NVIDIA Streamline 2.14.1.

      • The loader — on Windows Vk.open() takes sl.interposer.dll beside the executable in place of
        vulkan-1.dll when a program asks (Vk.sl_prefer), falling back to plain Vulkan without it; the sl*
        exports and feature functions are reachable from Ludic.
      • render3d — r3d_dlss(mode) upscales the lit HDR frame before bloom and the tonemap, jittered on a
        Halton cycle, with preset K in every mode (the default M cost 18 ms a frame at 4K on an RTX 3070 Ti);
        r3d_dlss_render_w/h give the size to render at. r3d_reflex(mode) sleeps each frame and marks
        simulation, submit and present. R3D_DLSS, R3D_REFLEX, R3D_DLSS_PRESET, R3D_SL_LOG for tests.
      • Commands the interposer lacks — it exports no vkSetHdrMetadataEXT, vkCmdDrawMeshTasksEXT or
        acceleration-structure command; Vk.has checks one and vkGetDeviceProcAddr reaches it.
    • Real OS threads behind Job.* and Sync.* — Job.parallel_for runs work across every core.

      • fn name — an expression naming a top-level function as a value, for a worker's entry point.
        No closures: the data a worker needs is passed in.
      • Job.parallel_for(count, fn work, ctx) — work(i, ctx) for every i in [0, count), split
        into chunks across a worker pool (one thread per core but one, pthreads or Win32) and the calling
        thread; returns when all are done. Job.is_worker() says whether code is on a pool thread.
      • Sync.* is thread-safe — mutexes are real mutexes, atomics are compare-and-swap, channels are
        guarded, and Sync.cpu_count() reports the machine's cores.
      • A worker must not change the world — spawn and despawn on a pool thread stop the program with
        a located message.
    • Texture filtering and shadow resolution as settings a game can change while it runs.

      • r3d_set_anisotropy(level) sets the anisotropic filtering of every mipmapped texture already
        loaded, not only of later uploads, on OpenGL and Vulkan.
      • shadow_set_res(size) remakes the cascades' depth layers at 1024, 2048 or 4096 (shadow_res
        replaces the SHADOW_RES constant); the lighting reads the texel size from the map.
    • app native "<dir>" in package.ludic — ludic bundle on Windows copies that directory's files beside
      the executable: native libraries loaded at run time (NVIDIA Streamline's DLLs, say) and their licence texts,
      which cannot live in the pack.

    Fixes

    • The meadow's grass draws on the Vulkan renderer again — the chunked grass path uploaded its vec4 tile
      table with u_fv, which on Vulkan copies one float per array element, so every tile had no blades.
      u_f4v uploads arrays of vec4.

    Performance

    • The sky's light is baked once at start, not twice. A game that turns the sky at boot sets
      sky_start_yaw before r3d_init, and sky_load bakes the image-based light at that yaw; turning to
      a yaw the light is already baked at (sky_set_yaw) no longer bakes it again. Without it, nothing
      changes.
    Downloads