Commit graph

111 commits

Author SHA1 Message Date
4499cedcb8 runtime: Json.write_file, and Json.encode through a kept buffer
The encoder appends into RtJsonState's buffer (grown only past the biggest document yet): ints and
Q16.16 fixeds written as digits in place, floats through string() and freed. Json.encode copies the
answer out once; Json.write_file hands the buffer to Fs.write_text (.tmp + rename) and keeps nothing.
json_saves.ludic: exact text, the file equals encode, parse round-trips, 1000 saves grow 0; clean
under MallocScribble. json_quote (the + builder) is gone.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 15:32:36 +03:00
254097657e runtime: Log, DateTime.format, Input.text, Path, Mime, Fs, Os and Text keep nothing per call
Found by reading every builtin (Os.platform's 8 KB per call started it). Log builds its line only at
or above the threshold and frees it; DateTime.format folds through + so its pieces go; Input.text
encodes into one buffer; Path/Mime/Fs/Os free their temporaries on every path; string results of
Text/Path/Mime/DateTime/Os dirs are fresh and Text frees a fresh argument. Reseeded.
runtime_temps.ludic: 19.8 MB -> 0 over 20,000 rounds, 64 KB -> 0 over 200 of file work; clean under
MallocScribble. string_temps still 0.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 13:55:49 +03:00
2fe4018d28 Merge branch 'r3d/zero-leak' into lang/uifree 2026-09-28 11:40:43 +03:00
a465984c52 fix(render3d, runtime): a glTF document is freed whole, layouts found by number, block records reused
- Json.free_all (value_free_all): a parsed tree's nodes, lists and strings. render3d frees each
  glTF document that way at the next load; the names kept out of it are copies (a primitive's
  material, a skin's joints, an animation clip's name in ludic.anim) - a model's strings were
  ~640 KB left behind per load
- jp_number made a digits list per decimal in a document and never freed it
- gvk_layout_id matches a mesh's layout as numbers in a scratch made once, against the layouts
  known end to end; a new mesh no longer builds a key string
- gvk_mem_new puts a new block into the record of one given back rather than appending, so a
  buffer made again every few frames no longer grows the block lists

steady.ludic adds a glTF parsed and freed whole 200 times: 0 bytes (38,400 before the digits fix),
beside the buffer path and the frame, still 0. A model loaded and let go still keeps ~2 KB a round
(texture and buffer handles are not reused yet); it is bounded at 8 KB a round.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 11:36:42 +03:00
0ec39f8e4e Merge branch 'r3d/zero-leak' into lang/uifree 2026-09-28 11:21:22 +03:00
866037a0d7 fix(render3d): nothing allocated in the steady state - the Vulkan allocator reuses its records
gvk_mem_new made a one-slot []pointer per allocation, and turned the requirement's size and
alignment into strings to read them as ints; gvk_list_drop_last rebuilt the spare-record list to
drop its last entry; gvk_mem_id did the string round trip on every free. One slot is kept
(gvk_map_slot), int() truncates a long, the spare list pops. Every Text.to_int(string(x)) in
render3d is int(x) now.

Vk.heap_bytes() (vk_mac.ll: malloc_zone_statistics' size_in_use; 0 on Windows) and
examples/rendering/steady.ludic, in the suite: a buffer released and made again 5000 times and
600 whole frames gain 0 bytes each - the allocator before this, 1,120,000 over the 5000.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 11:14:56 +03:00
50f255eea4 ludic.ui, runtime: the last per-frame allocations in the interface - the top popover found without a list, the popover draw list and the clip stack kept, a select's options in a list the node keeps (and matched to its value without writing the index), a number's text kept on its Value until it changes (value_num_set / Value.num_text), a key's label kept per key code until the layout changes it
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 11:11:48 +03:00
4d8526ad7e ludic.ui, runtime, compiler: a component call's answer is written into a pooled record while the screen is built (call(p, name, args, into); value_into_*), a component root's passes pooled, an icon's atlas and name read in place; reseeded
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 01:51:56 +03:00
6c663cc87b Merge branch 'r3d/moltenvk' into lang/foundations 2026-09-28 01:17:39 +03:00
9425bdc4e5 feat(render3d): 22.10 - ludic.render3d carries MoltenVK, so a Mac program has its Vulkan driver
native/build.sh builds MoltenVK v1.4.2 from its pinned, checksummed tag (its dependencies at the
commits its ExternalRevisions pins), thinned to arm64, id @rpath/libMoltenVK.dylib, signed ad hoc;
its licence goes in native/LICENSE-MoltenVK. Every render3d program links it through its rpath -
the package's lib/ while developing, Contents/Frameworks in a bundle, where ludic bundle puts and
signs it - and vk_mac.ll also looks for @rpath/libMoltenVK.dylib (after an SDK loader, so the
validation layer still stacks in development). The suite's SDK stand-in (vk_env) is gone: the
render checks draw on the MoltenVK the package carries. gpu_is_gl() removed; nothing calls it.

ludic-dev test 306/306 with no Vulkan SDK in the environment.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 01:16:26 +03:00
fa119d234b ludic.ui, runtime, compiler: an expression's Value comes from the screen's pool while it is built (never a state's start or an action's), true and false shared, calc() terms pooled and read in place, a model's list fields filled in place (value_set_ints/strs/floats/bools); reseeded
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 01:05:35 +03:00
429eeb7754 ludic.ui, compiler: a component's model is filled in place into an object its instance keeps (value_set_*); colours, border-images and a class screen's stub made once; object-fit into a kept list - benchmark 20 -> 10 KB a frame
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 00:17:47 +03:00
51a5e06f5c ludic.ui: props kept an instance, a call's arguments a depth, a loop's index shared, an absent attribute's text without a Value; runtime Value.clear - benchmark 36 -> 20 KB a frame
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 23:46:18 +03:00
46888b6716 runtime, ludic.ui: a scalar Value is one allocation, not three (lists only for a list or an object); layout and cascade lists kept a depth; small numbers' text made once - benchmark 63 -> 36 KB a frame
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 23:43:25 +03:00
80764f85c1 fix(render3d): the Vulkan renderer stops leaking as it loads and draws
- every texture upload malloc'd a CPU copy of its pixels and never freed it (236 MB over the
  valley's boot, and again for every model loaded later): the pixels are converted straight
  into the mapped staging buffer
- the per-level and per-layer views gvk_view_of made outlived their texture, and on MoltenVK a
  view keeps its Metal texture alive: a released texture takes its views with it, and the cache
  is keyed by numbers instead of a string built on every call
- the Vulkan structs filled for a draw, pass, barrier, descriptor set, buffer, allocation or
  upload (about sixty call sites) come from a reused 1 MB scratch ring (gvk_tmp)
- the descriptor-set cache and the retired buffers are emptied in place, not replaced; the grass
  cull's dispatch arguments are made once
- macOS drains an autorelease pool each frame (Vk.frame_pool, lvk_frame_pool in vk_mac.ll)
- R3D_VK_PROF reports Vulkan objects made and destroyed by kind, and every cache's length
- examples/rendering/smooth presents through render3d, so it runs on Vulkan too

The full valley on headless Vulkan loads to 2.18 GB and holds (it passed 8 GB while loading
before). ludic-dev test 305/305, selfhost-test 33/33.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 23:03:38 +03:00
9539f7bcec perf(json): 23.2 - a parsed tree can be let go (Value.free / Json.free: its nodes and lists, not its strings), a string is parsed in one allocation instead of one per character, the parser's cursor is freed, and gltf_load frees the last file's tree and the text it parsed
300 parses of a 446 KB glTF: 2118 MB before, 528 MB with the one-allocation string, 94 MB freed.
A headless Maroon Lake at play: 2334 -> 2195 MB by footprint.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 22:17:27 +03:00
38d4ea72b1 feat(render3d): Vulkan in a macOS window through MoltenVK; window built-ins take a slice's elements; the NEAR_FADE SPIR-V
- a CAMetalLayer on the view (cocoa.ll win_metal_layer), VK_EXT_metal_surface, QuartzCore linked
  with Vk.*; the drawable measured after the layer sets the backing scale
- vk_mac.ll opens MoltenVK directly after any loader: a bundle ships only libMoltenVK.dylib
- a covered window is not presented to (win_visible); one frame in flight on macOS
  (R3D_VK_INFLIGHT), with images, buffers and descriptor pools held until it is done
- win_held / win_mouse / win_pad / win_touch / win_text / win_present pass slice elements (arg_buf):
  every windowed program died on its first input poll
- variants.list and SPIR-V for the three NEAR_FADE foliage programs

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 00:52:45 +03:00
02448e176c wip(0.S3): the runtime migrated - ludic migrate state --runtime <every program>: 331 vars into 25 states (RtInputState, RtGlState, ...), 2 lets; its states are made before it boots; no module-level var is let through outside --globals
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 16:02:12 +03:00
7b17b4a1b9 wip(0.S3): outside the runtime, its drawable size, scale, screen and tile size are read and set through its functions; a migration reports such a reference instead of threading the runtime's state
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 15:47:21 +03:00
f5e7fe4245 fix(runtime): Random.* in any program; value_* called directly brings the value tree
The seeded random numbers lived in the ECS runtime, so a tool or a
program of test blocks got "unknown function rng_range". They are
runtime/native/rng.ludic now, spliced on Random.* or a bare rng_*/seed
call nobody defines, and imported by core.ludic for a game. A bare
value_*/json_* call nothing in the program defines splices the value
tree the way Value.* does. Reseed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 04:59:48 +03:00
1dad96102a feat(ui): ludic.ui is a UI framework - input, controls, render3d backend, the CSS a game needs
A game no longer writes a host:
- The runtime reads Input itself: focus and keyboard navigation (Tab, arrows, Enter/Space,
  autofocus), the pointer (hover, :active, click on release, drag), and scroll boxes with the
  wheel, a draggable scrollbar, clipping and scroll-into-view.
- HTML's controls are built in (button, checkbox, radio, range, select, text, key capture), made
  of plain parts a stylesheet styles, each reporting with on-change and event.value.
- ludic.ui/render3d.ludic draws with render3d's overlay: textures, named atlases (icon:NAME),
  nine-slice border-image, rounded rects and rings, clipping, and a scale.
- Hooks for the program's language, sounds and clock (ui_translator, ui_sounds, ui_clock).
- ui_dev: hot reload, errors on screen, LUDIC_UI_DUMP.
- CSS:
  - colours as #rgb / #rrggbbaa / rgb() / rgba() / names;
  - border-radius and outline (following the radius), box-shadow, background-image and a tinted
    border-image;
  - group opacity, @keyframes / animation, transition;
  - :focus, :focus-visible and :focus-within.
- HTML mixed content, and boolean attributes.
- render3d gains tex_width / tex_height, and the XML reader keeps text runs in order.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-24 17:26:09 +03:00
637d07400e feat(ui): ludic.ui grows toward HTML, CSS and React (Phase 0.U1-U4)
- Natives: ui_native(tag, measure, draw) makes an element the program draws itself; ui_fire
  runs its on-<event> with event.value. input, select and textarea have simple defaults.
- React:
  - keyed <each>, <let>, <provide> context through components, <fragment>;
  - named slots, default props on <component>;
  - on-mount / on-unmount;
  - inline text inside text elements.
- CSS:
  - custom properties and var();
  - position relative/absolute/fixed with insets and z-index;
  - em/rem/vw/vh and @media;
  - wrapping text and ellipsis, overflow;
  - + and ~ combinators, :nth-child(an+b), :checked, :active.
- Developing: errors with file:line, ui_errors(), ui_reload() keeping state, and an
  inspector-style ui_dump.
- view fields infer the type of a literal or a named function's result.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-24 16:17:25 +03:00
66a2bc2214 feat(lang): L11 views and templates - the UI is markup, not code
A `view Name { field = x; function q(..); on e(..) }` declaration is the one bridge between a
program and its UI: it writes view_<name>() -> UiView, whose model is a Value of every field and
whose call runs a query or an event by name.

ludic.ui is a template runtime:
- HTML-shaped XML screens and components, loaded at run time;
- {expression} bindings, if/else/each, props, slots, per-instance state;
- on-press / onclick actions (event, set, emit);
- component libraries (export="true", <import src as>).

Styling:
- stylesheets in <style> or importable .lss files (@import);
- CSS selectors (#id, .class, [attr=v], descendant and > combinators, :hover, :disabled,
  :first-child, :last-child, :nth-child, :not) weighed by specificity;
- the box model and flex under CSS's property names.

Also:
- default parameters, and positional-then-named calls;
- Value gains a float kind;
- a function shadowing a runtime one is refused;
- an index is evaluated before the slice is read;
- runtime errors name the right file.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-24 15:58:47 +03:00
b0b0b62bce feat(lang): L7 memory is safe unless it says unsafe
The typed buffers are slices: words/floats/fixeds/doubles/pointers(n) make
zeroed, bounds-checked []int/[]float/... and the type names mean them. buffer(n)
is a []byte, with text_of, Fs.read_bytes/write_bytes and view(xs, start, n).
bytes(), indexing a raw pointer or bytes, free, resize, Memory.*, raw file calls,
data_of and C externs are refused outside unsafe { } / unsafe function, and a
project's own files may write unsafe only with --unsafe; the runtime and packages
are the platform. A slice passed to an extern goes as its data.

What the change found: Sync's atomics on a slice header, words(n) uninitialised,
input's fixed axes in ints, truetype's fixed outlines as ints, skin matrices
typed int, gl_shader's source table made from raw bytes. render3d gets safe
entry points (safe_api.ludic). Rendering is byte-identical; a frame costs the same.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-24 12:53:27 +03:00
9259808f80 feat(lang): L6 namespaces declared in Ludic - alias
`alias meth(labels) = target` in a namespace block makes Ns.meth a call to
target with those labels (the target's own parameter names without a list). The
engine's 41 table-driven namespaces - 438 methods: Http, Udp, Process, Json,
Value, Screen, Input, Audio, World, Tiled, ... - leave emit_ns_call for
runtime/native/namespaces.ludic, spliced into every program; 532 lines of
compiler go and the seed shrinks by 23k lines of IR. The game's IR is byte
identical. The checker checks an alias call's arguments against its target.
Still built in: the inline namespaces (Math, Text, List, Vector, Color, Time,
Date, ...) and the methods that pick a target by argument type.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-24 02:40:57 +03:00
57b66bdf47 feat(lang): L4 type checker between parse and emit
selfhost/check/ walks every function, the entry, tests, globals' initializers and
@On listeners with real scopes, and refuses mixed number kinds, text and numbers,
two record types, mismatched slices and fn types, wrong argument counts, wrong
returns and wrong push elements - every mix-up at once, each at its line.
LUDIC_CHECK_REPORT=1 lists them by category. pointer stays untyped (L7's).

What it found is fixed: render3d's HDR scan calling the float-bits extern f_lt
with floats; ludic.shooter's right-stick aim overflowing past half a push;
prof.ludic storing longs in []int; extern arguments now coerced to their
parameters. Text-returning runtime functions say string; Assets.ready says bool.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-24 01:34:49 +03:00
e6f565a6c4 carry: the runtime and render3d work Maroon Lake builds against
Input.text, App.monitor_count / window_to_monitor / window_fixed,
gl_sleep_us, and the grass and collide changes, uncommitted on main and
depended on by the game; carried here so the language work starts from
what the game actually uses. main's working tree is untouched.
2026-09-23 15:55:02 +03:00
f7f47152bd fix(runtime): 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.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-20 01:24:37 +03:00
4979cc0c94 feat: every key on the keyboard is bindable - the F-row, the six-pack, Caps Lock and the numpad
The platform gave these keys 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
(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.

w_keyval and ev_keyval now name them, with the same codes on both: 132-143 F1-F12,
144-149 Home / End / PageUp / PageDown / Insert / Delete, 150 Caps Lock, 152-161 the
numpad digits, 162-166 its * + - . and /. The numpad's Enter is Enter.

Key.F1, Key.Home, Key.Numpad0 and the rest fold at compile time, and Input.key_label
names them without asking the layout - a key that types nothing is called the same thing
on every layout.

examples/library/input_typeless_keys.ludic covers the codes, the names, the held set and
the press edge; 150 passed in ludic-dev test, 33 in selfhost-test.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-18 00:57:02 +03:00
1256bbfec7 fix(window): the title a program passes to gl_open / gvk_open reaches the window
The runtime opens a game's window before main, titled with the program's name, and
gl_open attached to it without passing its title on (render3d's gvk_open called win_open,
which returns early on Windows and opened a second window on macOS). win_set_title in
cocoa.ll / win32.ll (setTitle: / SetWindowTextW, UTF-8 -> UTF-16; a no-op without a window,
stubbed headless) retitles it from gl_open, and win_open on an open window retitles it on
both platforms.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-17 14:02:23 +03:00
43d5eb5b88 feat(launcher): Process.*, Http.save_to/received/expected, App.window_hide/show
Process.spawn/poll/kill/free - non-blocking child processes with no shell: posix_spawn on
macOS (process.ll), CreateProcessW with MSVC-quoted arguments and no console window on
Windows (process_win.ll), linked only when a program uses Process.*.

Http.save_to streams a response body into a file (NSURLSession with a run-time delegate
class on macOS, the WinHTTP read loop on Windows); Http.received / Http.expected report
progress while it is pending. Freeing a pending request cancels it and parks the slot
until the worker has finished.

App.window_hide / App.window_show take the game's window off the screen and back without
closing it; the run goes on while hidden. Docs, examples, tests and a changeset.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-17 13:32:23 +03:00
ebb1a00352 feat(udp): Udp.* - polled IPv4 datagrams on macOS and Windows
Udp.open/port/send/recv/from_ip/from_port/close/resolve/local_ip/ip/ip_text, native
BSD sockets (udp.ll) and Winsock (udp_win.ll, -lws2_32), linked only when a program
uses Udp.*; example, docs and tests.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-17 11:16:14 +03:00
619476ffed fix(input): keys are physical positions on every layout; Input.key_label names them
The Windows runtime keyed the held set by the character MapVirtualKeyA gave a
virtual key, so a game's WASD belonged to whatever the active layout put there,
and with an input method on every letter arrived as VK_PROCESSKEY. The typing
block is now read from the scancode (the arrows, numpad and F-keys still by
virtual key); macOS reads keyCode the same way. Input.key_label(key) names a key
in the player's own layout (Windows) or as its US character elsewhere.

Verified on Windows with SendInput into a live window: VK_Z carrying W's scancode
holds 'w', VK_PROCESSKEY carrying A's holds 'a', Caps Lock changes nothing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-15 23:26:53 +03:00
a868378b99 feat(vk): call shapes for the acceleration-structure commands the interposer lacks
NVIDIA Streamline's interposer exports none of VK_KHR_acceleration_structure's
commands, so ray tracing looks each up per device with vkGetDeviceProcAddr and
calls it through a pointer: create (four pointers -> result), destroy (device,
handle, allocator), build sizes (device, type, info, counts, sizes), the
command-buffer build (buffer, count, infos, ranges) and the device address
(device, info -> u64). Both runtimes assemble; nothing uses them yet.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-15 20:22:12 +03:00
750d13779d feat(render3d): mesh-shader grass
The chunked grass path draws each chunk as one mesh-shader dispatch when the
setting asks and the card has VK_EXT_mesh_shader: work group y is a tile, x a
batch of 16 of its blades, and a blade the placement, density, frustum, water
or slope tests reject emits nothing - the instanced path still runs its eight
vertices to a degenerate position. grass.mesh generates the same blades as
grass.vert (grass_blade_mesh(4)'s rows, the same hashes, sway and lighting
normal), capped at the instanced path's 65535 a tile.

Vulkan: VK_EXT_mesh_shader with meshShader, and maintenance4 (glslang's mesh
stages declare LocalSizeId); vkCmdDrawMeshTasksEXT looked up per device, as the
Streamline interposer exports none; a *.mesh program's pipeline takes the mesh
stage and no vertex input, its bindings the mesh stage bit. gpu_has_mesh,
gpu_draw_mesh_tasks; r3d_mesh_grass and R3D_MESH_GRASS / R3D_NO_MESH.
bin/ludic-dev rebuilt: the committed binary predated the shader tool's mesh
support and compiled grass.mesh as a vertex stage.

PC (RTX 3070 Ti): the camp matches the chunked path; validation only the
no-window present-id message.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-15 20:20:42 +03:00
0224af64ab feat(vk): mesh-shader plumbing - *.mesh variants compile, a pointer call for commands the interposer lacks
ludic-dev shaders: a variant whose first file is *.mesh compiles that stage with
glslang -S mesh for Vulkan 1.3, without the vertex stage's depth-remap wrapper
or invariant gl_Position (a mesh shader writes an array of positions and
remaps depth itself). Its SPIR-V keeps the .vert name, so the manifest and the
loader are unchanged. The 51 existing programs build identical SPIR-V.

runtime: lsl_call_piii(fn, ptr, i32, i32, i32) calls a command-buffer command
through a pointer. NVIDIA Streamline's interposer exports no
vkCmdDrawMeshTasksEXT, so it is to be looked up per device with
vkGetDeviceProcAddr. Both runtimes assemble.

Nothing uses either yet.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-15 20:00:26 +03:00
ad8f44b78a feat(vk): load NVIDIA Streamline's interposer as the Vulkan loader on request
vk_sl_prefer(1) before Vk.open() makes the Windows loader try sl.interposer.dll
beside the executable and fall back to vulkan-1.dll. Thunks for the sl* exports
and for calling feature functions from slGetFeatureFunction; macOS stubs.
Verified on an RTX 3070 Ti: slInit eOk, DLSS, DLSS-RR, Reflex and PCL supported,
DLSS-G reports no supported adapter (needs RTX 40).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-15 19:05:27 +03:00
c10abd9f9f feat(jobs): real OS threads - Job.parallel_for, fn name, thread-safe Sync
- `fn name` names a top-level function as a value (E_FNREF, lowers to @fn_<name>); the worker
  entry point for Job.parallel_for, which checks it takes (int, pointer-like) and returns void.
- runtime/native/threads.ll (pthreads) and threads_win.ll (Win32 SRWLOCK/CONDITION_VARIABLE): a
  pool of one worker per core but one, parked between batches; every thread claims chunks by
  compare-and-swap. Linked only into programs that use Job/Promise/Sync, by `ludicc -o`,
  `ludic build` and the test suite's build helper.
- Sync.* is real: native mutexes, atomics as cmpxchg retry loops (neither clang takes atomicrw,
  the PC's rejects seq_consistent), mutex-guarded channels, Sync.cpu_count from the OS.
- spawn/despawn on a pool thread stop the program with a located panic.
- examples/library/threads.ludic and its test; docs for fn, Job.parallel_for, Job.is_worker.
- Reseeded (bootstrap-cfree: out.ll == seed.ll). 141/141 on macOS; jobs, threads and the guard
  pass on Windows from the reseeded Windows seed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-15 13:52:22 +03:00
1b7408c6a1 feat(render3d): Vulkan in the game's window on Windows
- gpu_select takes Vulkan for a window on Windows; gvk_init asks for VK_KHR_surface,
  VK_KHR_win32_surface and VK_KHR_swapchain when the renderer will present. A window elsewhere
  stays on OpenGL with the reason.
- gvk_open opens the window without a GL context and makes the screen images at its client area;
  gvk_swap_make builds the swapchain on the Win32 surface (FIFO with vsync, mailbox or immediate
  without) and rebuilds it when it is out of date, suboptimal or the window changes size.
- gvk_present blits the screen image into the acquired swapchain image, flipped (the screen keeps
  OpenGL's bottom-up rows), and presents it.
- Samplers pointed at a texture unit with u_i and then fed by binding units - the actors do this -
  read that unit's texture; on Vulkan they drew with the white stand-in (the tent, the log, the
  chair, the workbench).
- gl.ll carries a weak win_gl_drawable for headless macOS builds.

On the RTX 3070 Ti the windowed build runs the valley through Vulkan at 3840x2160 with the HUD and
the frame-rate readout (16 fps: every upload still waits on the frame, and buffers are
host-visible). The headless Vulkan frame is unchanged; OpenGL frames byte-identical at the five
viewpoints with 59 self-tests passing. The actor fix is compiled and OpenGL-verified, not yet
seen on the PC; blades at the grass's middle distance still draw black on Vulkan.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-15 13:08:40 +03:00
51d82063aa feat(runtime): the native window, for a graphics API that makes its own surface
win_native_window / win_native_instance hand a Vulkan swapchain what it is created on: the
HWND and module instance on Windows (VkWin32SurfaceCreateInfoKHR), the game's view on macOS.
Headless builds define both as null (weak on macOS, so a windowed link keeps cocoa.ll's), so
code that asks for them links everywhere and simply finds no window.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-15 10:42:19 +03:00
208cad7ca1 feat(vk): Vk.* - Vulkan 1.0-1.4 generated from the registry, loaded at run time
ludic-dev vkgen reads vk.xml into runtime/native/vk_api.ludic (constants, every struct's
<Struct>_sizeof and <Struct>_<field> offsets, one extern per command) and vk_thunks.ll.
Every size and offset was compiled against the SDK's C headers; `ludic-dev test` checks the
tracked files against the registry wherever the Vulkan SDK is installed.

vk_win.ll (vulkan-1.dll) and vk_mac.ll (libvulkan.1.dylib, MoltenVK) open the loader at run
time, so a program built with Vk.* starts on a machine without Vulkan. ludicc and ludic
build link both for any program that uses Vk.*. The seeds are regenerated for the new
compiler.

vk_probe reports what a machine's Vulkan can do; vk_compute dispatches a Slang compute
shader and reads the picture back, clean under the validation layer on an RTX 3070 Ti and
on an M4 Pro through MoltenVK.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-15 09:52:12 +03:00
28e8769a8b fix(input): mouse_dx/dy report no motion on the first frame or across a cursor-mode change
The delta was this frame's position minus the last, and the last started at 0,0, so the
first frame reported the cursor's whole distance from the corner as motion. A cursor-mode
change did the same, switching between a locked cursor's virtual reticle and the real
cursor. Maroon Lake's camera adds mouse_dy to its pitch and came up pointing at the ground.

examples/library/input_mouse_rebase.ludic covers both cases, plus ordinary motion and
re-setting the mode already in force.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 15:35:26 +03:00
fb4d904ab6 Merge branch 'fix/arrows-http-backspace': held arrow keys, ludic build Http.* link, macOS Backspace
# Conflicts:
#	tools/ludic-cli/build.ludic
2026-09-13 03:28:29 +03:00
40d78cbd92 feat(windows): Http.* over WinHTTP, and the splash and window icon through WIC
http_win.ll implements http.ll's hs_* contract over WinHTTP: the request is a
record built on the game thread, the whole exchange runs on a worker thread,
TLS uses the system's certificate checks, and headers are answered from the
kept request handle. ludicc links it with winhttp instead of refusing Http.* on
Windows.

win32.ll decodes the splash and App.set_icon images with WIC (ole32): the
splash is a topmost borderless window at the artwork's size in points times the
display scale, composited over its background colour; the icon becomes an
HICON set on the window now or when win_open makes one.

Verified on the PC: examples/library/http.ludic prints its expected output, a
real GET to https://git.workshopsoft.io/ returns 200 with its body and
Content-Type, and the bundled Maroon Lake shows its splash centred at launch and
its icon in the title bar.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-13 03:24:50 +03:00
885943e73f fix(cocoa): the Delete key reports Key.Backspace (8)
AppKit gives kVK_Delete (51) the character NSDeleteCharacter, 127, which is
what both ev_keyval (the held-key set) and win_poll (the per-frame key)
received, so Key.Backspace, which folds to 8, never matched. Both now map key
code 51 to 8.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-13 03:00:29 +03:00
63d9f61bf1 feat(windows): a window sized by the display's scale, as a Retina Mac does
A game asking for 960 x 540 got 960 x 540 pixels - a quarter of a 4K panel.
w_fit_scale turns the display's DPI into a whole-number scale (96 -> 1, 168 and
192 -> 2), brought down until the window fits; win_open and win_gl_resize size
the client area with it, win_gl_scale reports it the way cocoa.ll reports the
backing scale, and the mouse still arrives in the game's own units.

Verified on a 3840x2160 panel at 175%: windowed gl_triangle's 640x360 window
draws a 1280x720 frame.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-13 02:46:00 +03:00
3300fb3957 feat(windows): sound, through XAudio2
audio_win.ll implements audio.ll's snd_* contract over XAudio2 from IR: a
source voice per clip, restart on play, LoopCount for loops, SetVolume,
SetFrequencyRatio and a balance pan through SetOutputMatrix, with the COM
slots taken from the SDK's xaudio2.h. Clips are RIFF WAVE read through
lp_pak_open (weakly referenced), so a packed sound loads directly. ludicc links
it with xaudio2 and ole32, and silences xinput.lib's importeddllmain warning.

Verified: ludic-dev test 135/135, selfhost-test 32/32; on the PC a harness
loads, plays, pans, loops and stops clips, and Maroon Lake windowed reports
"sound: 31 of 31 clips loaded".

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-13 02:42:48 +03:00
00d25dcc3e feat(windows): a window - win32.ll, WGL on it, and a windowed Windows build
win32.ll implements cocoa.ll's contracts over user32 and xinput: the message
pump, the held-key set and frame key in Ludic codes, the mouse with raw input
for cursor mode 2, clip-based cursor modes released on focus loss, XInput pads,
and the software framebuffer present. win32_gl.ll puts the WGL 4.1 core context
on that window (vsync, borderless full screen); gl_win.ll's context code is now
lgl_wgl_core, shared with the headless hidden window, whose win_gl_* stubs move
to gl_win_nowin.ll. audio_win.ll links Audio.* silently for now.

Verified: macOS fixpoint, ludic-dev test 135/135, selfhost-test 32/32; on the
PC gl_triangle renders identically headless and in a real window, and Maroon
Lake builds windowed and runs a playtest to frame 240 in the desktop session.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-13 02:33:53 +03:00
cb05721a89 feat(windows): Gl.* on Windows, through WGL and a driver-filled thunk table
gl_win.ll creates a hidden-window WGL 4.1 core context and carries gl.ll's
float/memory helpers, with ldexp and QueryPerformanceCounter in place of the
libSystem calls. glgen now also writes gl_thunks_win.ll: the same 478 thunks,
calling through pointers that @lgl_win_load fills from wglGetProcAddress (and
opengl32.dll for GL 1.1). ludicc links the pair against opengl32/gdi32/user32
on a Windows target.

Verified: gl_api.ludic and gl_thunks.ll regenerate byte-identically, ludic-dev
test 135/135, selfhost-test 32/32, and headless gl_triangle on an RTX 3070 Ti
matches the macOS frame to within one level per channel.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-13 02:10:36 +03:00
38b6b81cd7 feat(app): App.set_icon, so a game without a bundle still has an icon
`ludic bundle` builds an AppIcon.icns and macOS reads it out of the .app, so a
shipped game has an icon. `ludic build` produces a bare executable: no bundle, no
CFBundleIconFile, and so no icon at all - macOS draws the generic green "exec"
tile. That is the build a developer runs every day, which is why "the game has no
icon" can be true for months while the bundle is perfect. It was here: the .app's
icns validated against iconutil, the plist was right, the signature was right,
and NSWorkspace rendered the artwork - and the binary beside it still had the
exec tile.

App.set_icon(path) takes the bytes through lp_pak_open, so a packed path and a
loose one both work and this does not repeat Audio.load's trick of taking a
filesystem path only. NSData copies them, so the buffer goes straight back. A
missing or undecodable image leaves the existing icon alone rather than clearing
it; a bundled app is unaffected; headless links no AppKit and compiles it away.

Verified the Cocoa sequence against an ObjC twin doing the same message sends:
before, a bare binary's applicationIconImage is the generic 128x128 tile; after,
it is the 1024x1024 artwork.

Backend change, so selfhost/ludicc.seed.ll is reseeded: the bootstrap fixpoint
and the C-free rebuild from the seed both pass.
2026-09-12 12:55:12 +03:00