JARU 0.9.6
These are the release notes of JARU 0.9.6 as it shipped; what changed afterwards is in JARU 0.9.7.
- Date: July 2026
- Components: IDE (0.9.6.1, build 54) · Windows VM · ESP32 / ESP32-S3 VM · ESP32-P4 VM · Compiler
- Previous version: 0.9.5
This is by far the biggest leap the ecosystem has taken so far. The four main blocks are:
- A brand-new physics and collision engine, written from scratch, with real rotation, impulse-based contacts,
sleeping, torque and CCD. - A complete sound and music system: an engine in every VM, a tracker and an instrument editor in the IDE, a MIDI importer and a dedicated binary format.
- A new VM for the ESP32-P4 with MIPI-DSI drivers.
- Two additions to the language: the
structtype andfnlambda functions, together with unified callbacks (a function, a method or a string work everywhere).
On top of that, the IDE's code intelligence is reinforced across the board, and a long run of fixes lands in the VM, the compiler and the resource editors.
Highlights
- New physics engine in all three VMs: stable rotation, Coulomb friction, island-based
sleeping,applyTorque/applyForceAt,sprite.bullet(CCD) and category-based collision filters. sprite.onCollision(callback, minImpulse): Angry Birds-style impact callbacks, with an impulse threshold.fnlambda functions with capture by reference (includingthis), and unified callbacks:Timer,gpio,MQTTandonCollisionall accept a function, an object method or a name in a string.- New
structtype in the language (compiler + all three VMs + full IDE support). - Sound and music system: 4 tone channels + noise + 2 PCM, per-instrument ADSR, PWM and tremolo, exact tuning in millihertz, and a sample rate per board profile.
- Music editor / tracker in the IDE, with a MIDI importer, a graphical instrument editor and a General MIDI preset picker.
- ESP32-P4 VM with double buffering and MIPI drivers (ST7703, ST7701).
- Sturdier IDE intelligence: inheritance in completion, multi-level Ctrl+click, per-file inference and false-positive cleanup.
Breaking changes
There are only a few, but they affect projects written with 0.9.5.
| Change | What to do |
|---|---|
sprite.spin now really means degrees per second. It carried an extra factor of 60, so it turned 60 times faster than documented. | Multiply the old values by 60 if you want the speed you were actually seeing. |
sprite.acceleration is deprecated as an alias of sprite.force; it will be removed in 1.0. Its real semantics were always "a single-step force", not a persistent acceleration. | Rename it to force whenever it's convenient. |
scene.maxDelta is now a property of each scene. It used to write a file-level global, so touching it in one scene changed it in all of them (and it didn't even read its argument). | Set it in every scene that needs it. |
friction and elasticity are clamped to [0,1] at all three entry points (sprite, grid and setters). | Out-of-range values used to produce NaN or gain energy; they are no longer accepted. |
| Friction between sprites is Coulomb friction, bounded by the normal impulse. | Gentle grazes no longer brake, and real static friction appears on ramps. It may be noticeable in games that relied on the old viscous braking. |
| VM error messages are all in English. 130 Spanish literals were still there, with three different wordings for the same family of error. | Only affects code that compares the text of e.message; compare the error code instead. |
The ESP32-S3 application partition grows from 0x140000 to 0x160000, and littlefs moves to 0x170000. | A full flash is required (firmware + filesystem), not just the firmware. |
Language and compiler
New struct type
- First-class
structtype: definition, instantiation and field access. - The compiler emits the
oSTRUCTopcode; supported by all three VMs. - Callable fields: you can store a function in a field and call it with
a.field(). - Full IDE support (see below) and a dedicated guide page, in both languages.
fn lambda functions
- Syntax
fn(params): body end. A lambda is a value: you can store it in a variable, put it in a list or a map, return it from a function or pass it straight in as a callback. - Capture by reference: the lambda sees later changes to the captured variable, and the variable stays alive as long as the lambda exists, even if the function it was born in has already returned.
- Inside a method, a lambda also captures
this. - Recursion works by assigning the lambda to a variable first and calling through it.
- Documented in
guide/functions(ES + EN), with the IDE's highlighter, live parser and completion up to date.
Unified callbacks
Where each module used to accept something different, the four entry points now accept the same thing: a direct reference (myFunction), an object method (object.method), a name in a string ("myFunction") or an inline fn lambda.
It includes GC roots for live callbacks plus jam and double-pop fixes in the dispatcher.
Compiler fixes
this.property++/this.property--no longer raise a spurious error.- Fixed the compiler's exit code (it was being overwritten with 0 in
main). - Fragmented line table: the synthetic nodes for array-literal indices and dimensions carried line 0, which broke RLE grouping and duplicated entries. They now inherit the real line; both the
.jxrand the RAM the table takes up shrink. - Strict validation of
functionsCountand of the trailing.jxrdata on load. - Parent function names are interned and matched by identity in the loader.
platform() finally documented properly
The published table had been wrong for a long time (it documented 3 = Windows when Windows is 4). It is correct now, and it adds the constants column, which wasn't documented anywhere:
| Value | Platform | Constant |
|---|---|---|
0 | Unknown | — |
1 | ESP32 | HW_ESP32 |
2 | ESP32-S3 | HW_ESP32S3 |
3 | Reserved | — |
4 | Windows (SDL2) | HW_WIN32 |
5 | Web (Emscripten) | HW_WEB |
6 | ESP32-P4 | HW_ESP32P4 |
Physics and collision engine
The old engine is replaced by a new one (broadphase, narrowphase, contact solver and grid collision), present in all three VMs and validated with dedicated test batteries: PhysicsAudit 101/101, CollisionLab 35/35, Collision2 13/13, PhysProps 25/25 and JaruTest 14/14 (608 asserts).
New sprite API
| Property / method | What it does |
|---|---|
force | Single-step force (replaces acceleration, which stays on as a deprecated alias). |
rotates | Allows or blocks the body's physical rotation. |
angularVel | Angular velocity in degrees per second. |
angularDamping | Per-body angular damping (Box2D model w *= 1/(1+dt·d)). |
gravityScale | Per-body gravity multiplier (0 = floats, 2 = heavy, negative = falls upwards). |
damping | Per-body linear damping, no contact required (water, air). |
category / collideWith | Box2D-style bitmask collision filter. |
bullet | Enables continuous CCD on the body: a fast mover won't tunnel through another body between two steps. |
onCollision(cb [, minImpulse]) | Impact callback, with an optional impulse threshold. |
applyForce(fx, fy) | Force at the center of mass. |
applyImpulse(ix, iy) | Instantaneous impulse. |
applyTorque(t) | Pure torque; does not move the center. |
applyForceAt(fx, fy, px, py) | Force at a world point: generates torque r × F. |
New scene API
scene.sleeping: auto-sleep for resting bodies, over complete contact islands and with no extra heap. Turning it off wakes them all up.scene.sleepAll(): requests that the whole scene go to sleep; the request is consumed by the next full physics step.scene.maxDeltabecomes a real per-scene property.- One world per scene: the static pool of 4
Worlds with LRU recycling is gone. Each scene owns its own, created lazily and released when the scene is destroyed, so the engine's RAM goes back to the system right away.
Rotation and geometry
- The dynamic origin (center of mass and of rotation) is now the center of the collider; the pivot stays as a visual anchor. With an off-center pivot acting as the center of mass, every normal impulse created a phantom torque and the collider orbited its own corner.
solveContactsreordered to the Box2D criterion: velocity impulses first, positional pass afterwards. Correcting position first misaligned the lever arms and drained spin (~8%/s with friction 0).- The collider's center, its offset and the bottom edge used by one-way platforms rotate with the sprite, both against other sprites and against the grid.
spininvalidates the rotation cache: a sprite turning viaspinused to collide with the vertices of its last manualangle.groundedis evaluated at the center of the contact patch, and flat grid faces apply a single impulse there (no phantom torque when landing flat).- Sprites with subpixel rendering on Windows (
SDL_RenderCopyExF): truncating to integers made the physical rotation wobble.
Collision against GridMap
- Absolute repositioning onto the tile face: the solid-tile branch added the sweep overlap to the final position, sinking the sprite on fast falls, going through thin walls and lifting it twice as much when landing astride two tiles.
- Internal-face filter (ghost collisions) in the rectangular version too, with the blocking mask kept separate from the detection mask: a one-way platform no longer masks a solid. This is the defect that forced the double-collider trick in games.
- Anti-embedding net redesigned: a single synthetic contact computed as the minimum translation that pushes the rect out of the union of blocking tiles, evaluated at the final position and in the sprite's frame (correct on wrapping maps too).
- A circle's corner contacts are resolved by radial penetration, not by AABB, which over-corrected ~3 px for 0.1 px of real penetration.
hitsGrid(grid)reports every overlapping tile again, just as the documentation says; the face filter stays only in the physical resolution.- Circle and rectangle sweeps against the grid unified, with correct tile coordinates, wrapping and deduplication.
Performance
- CCD with early rejection:
runCcdcopied the wholeBody(140 bytes) and swept against every body in the world. With 6 bullets and 75 bodies that was up to 1,350 sweeps per step. A conservative motion AABB now rejects before copying. - CCD only kicks in if the scene has at least one
bulletbody: a scene with no bullets doesn't even pay for the traversal. - Mass and inertia are recomputed only if they could have changed (mass, shape or type), instead of 3–4 floating-point divisions per body per step.
- The trigonometry cache is reused in the grid scan: the one-way path was reaching ~18
libmcalls per tile examined. - The rotation cache drops from 44 to 12 bytes per sprite.
Fixes
- Inactive sprites no longer run physics: a sprite with
isActive = falsewas still being integrated and colliding in the solver, contrary to the documentation. - Restitution threshold calibrated: it was doing three jobs at once and broke
onGroundwith the default elasticity. A sprite resting on aSTATICbody goes from 35–56% to 99.6% of frames withonGroundtrue. - Three
stepRate/maxDeltabugs: astepRatebelow1/maxDeltasimulated less time than it consumed and the game ran in slow motion with no warning (stepRate 10ran at 50%; now at 97%); and the accumulator's debt grew without bound when bailing out at the sub-step cap, sending the simulation racing once the frame rate recovered (stepRate 240at 331%; now at 106%). - Null-deref crash with
VISUALsprites grazed by aDYNAMICone. - Oversized overlap in circle-vs-rect and circle-vs-OBB when the bottom edge was the closest one (ejections of tens of pixels).
scene.iterssaturated at 255: theuint8_tcast wrapped 256 → 0 and silently disabled the sprite-vs-sprite solver.- 0–255 range validation on the grid tile sets, ported to ESP32 and ESP32-P4 (it only existed on Windows).
- Circle internal-face filter was inverted, a pre-existing bug: the pairing was crossed, so it never suppressed anything.
- Polygon fill and closing implemented on ESP32 and ESP32-P4 (
draw.polygon(vertices, fill)), honoring the backbuffer and the P4's screen offsets.
Known limitation
The engine cannot hold stacks of two or more dynamic bodies at rest: energy pumps in because contact warm-starting is missing. It is analyzed and measured in PLAN_WARMSTARTING.md; no lever available from .aru avoids it.
Sound and music
This whole block is new: 0.9.5 had no sound system.
Engine
- Channels: four tone channels (0–3), one noise channel and two PCM channels.
- Per-instrument ADSR envelopes, SID-style gate model.
- Waveforms: square, saw, triangle, pulse and sine, with a polyBLEP filter to cut unwanted harmonics.
Sound.pwm(channel, depth, rate [, delayMs]): duty LFO, audible onPULSE(at full depth it sweeps ±40 duty points).Sound.tremolo(channel, depth, rate [, delayMs]): amplitude LFO, on tones and noise, with a unipolar dip down to silence.- Vibrato per instrument and per channel.
- Exact tuning in millihertz: a single frequency table per MIDI note, independent of the sample rate, with the low end in tune (A0 = exactly 27.5 Hz).
Sound.tone/pulse/slideFreqaccept fractional frequencies (Sound.tone(0, 65.406, 200)). - Sample rate selectable per board profile (22050 / 44100). The noise clock is fixed at 22050 with a Q16 counter, so a song sounds the same on 22 and 44 kHz boards.
- Low latency on ESP32:
block_framesdefaults to 256, about 58 ms at 22050 Hz, while keeping a 35 ms anti-underrun cushion. - Anti-click: legato retrigger with no cut to zero, free oscillator phase and per-frame volume ramps. Every wave starts at its zero crossing, including the triangle, whose phase 0 is the trough and used to jump out of silence.
- SFX priorities respected by the music: the sequencer's note-offs no longer kill an SFX with
priority > 0. Transport (play / seek / stopMusic) still silences everything. - PCM channel balance tuned, and a Dark instrument for the noise channel.
Compiled music format (.jmu)
- Compact binary, generated by the IDE when the project is compiled.
- Instrument table in the header plus a
SELECTopcode: the full patch (envelope, vibrato, PWM and tremolo) travels once and the rows just select it. - 3-bit channel field, to make room for the four tone channels, with the control byte kept separate.
- Pitch slides travel in millihertz, the same unit and precision as the notes, so a pitch bend doesn't leave the note out of tune when released.
- The header tempo is restored on replay or seek.
MIDI importer
- Volume and expression (CC7 and CC11): many orchestral MIDIs carry their phrasing in CC11 on top of a static balance CC7, and those songs lost all their dynamics. They are now combined at query time as
CC7 × CC11 / 127, to tolerate files with CC7 on a setup track and CC11 on the note track. - Honors the timbre's nominal volume: every imported note carried an absolute volume derived only from velocity and CC7, which threw away the balance designed in TIMBRES (in particular the compensation for the triangle waves).
- GM note 58 (Vibraslap) mapped: it was the only one of the 47 in the percussion map left unassigned, and it sounded like a snare.
- Exact tuning with pitch bend: the whole note resolved from the table, the fractional bend in millihertz.
Virtual machines
ESP32-P4 VM
- VM implementation for the ESP32-P4, with double buffer and display-mode handling.
- New display drivers: ST7703 and ST7701 (MIPI-DSI).
- Automatic fallback to PSRAM when the double buffer can't be allocated in SRAM (ESP32 / ESP32-S3).
Other platform changes
- LovyanGFX updated to 1.2.21.
- QSPI driver and test example.
- SPI buses (SPI1/SPI2/SPI3) unified, with no extra labels.
draw.fontvalidated to 0–8:draw.font(9)dereferenced a LovyanGFXnullptr, anddraw.font(200)read past the table when drawing text. The documentation now lists the six real fonts with their height and character set (it used to document 4 of the 9 entries).Bitmap.loadtells the failure causes apart: it throwsERR_OUT_OF_MEMORYwith its own message instead of the genericERR_FILE_NOT_FOUND. Same forSprite.loadframes.- Stricter validation when reading
boot.cfg: a missing key keeps its default value instead of turning into 0, and undeclared pins stay at −1. platformio.inisupports both boards at once:[env:esp32dev]and[env:esp32s3dev]on top of a shared[base]section, with no need to swap the file to change board.- Fixed jerky map scrolling in the Windows VM.
readLinestopped responding on reaching EOF, writing heap garbage over the buffer.
VM and interpreter fixes
foreachinsidetry/switchread theObjIteratorraw.- Indexed writes and
++/--on the iterator variable. - Boolean map keys no longer collide with integers; object keys hash by pointer.
- Iterating a non-iterable throws properly in
oBUILD_ITER;foreachover an expression no longer crashes. - Phantom tombstones in
size()when overwriting a live key, plus theAdjustCapacitycrash. - Stack leak in
forwith a literal boolean condition. - Null terminator on the HTTP response on Windows.
VM optimization (measured on the board)
A performance campaign ported to all four platforms:
- Dispatch and interpreter:
ipcached in aRun()local, interrupt safepoints and integer fast paths. - Fused opcodes
JUMP_IF_FALSE_POP/JUMP_IF_TRUE_POP. oCONSTANT8: constant loads with an 8-bit index (bytecode ~−2.3%).- Objects and hash tables: constructor cache, operator bitmask, inline properties in
ObjInstance,Entryfrom 24 → 16 bytes (tables −33%), hot helpers inIRAM_ATTR. - Garbage collector:
CollectGarbageremoved fromAdjustCapacity, perennial roots with a boundary GC, and a census of dynamic strings and perennial modules.
IDE
Music editor / tracker
- Built-in tracker-style editor, with the IDE's own format and conversion to the VM format at compile time.
- Active channel and cell highlighting: a livelier gradient on the channel header, a 3 px gold bar where its data starts, the focused field (Note/Inst/Vol/Dur) in white, and a 2 px box on the exact cell under the cursor.
- Graphical instrument editor with parameters, ADSR envelopes, sine wave, PWM and tremolo.
- General MIDI preset picker: family and program combos, percussion included, fed from the importer's GM data.
- MIDI importer to JARU music, increasingly consistent on envelopes and instruments.
- Preview faithful to the engine: the IDE player mirrors the anti-click, the master bus, the triangle onset and the tremolo onset, and plays at the active board profile's sample rate (it reopens the SDL device when the board changes).
- Preview buffer at 2048 frames at 22050 Hz (twice that at 44100, to keep the same ~92 ms), which removes the underrun clicks.
- VU meters fixed: the player publishes each block's raw peak, with no decay, and the visual falloff lives in the editor's 33 ms tick, so it no longer depends on the audio buffer size.
- The status bar shows
playing/paused @ 22kHz|44kHz. - Editing improvements: copy / cut / paste, multi-row selection, keyboard input, icons on the buttons, and the “Copy ms (seekMusic)” button restyled to match the tracker.
Resource editors
- Icons on the buttons of the image editor, the music editor and the map editor (line, oval, pencil, rectangle, brush, eraser, fill, resize, piano, add/delete/duplicate row…).
- Tilemap editor: undo/redo, better modification detection, tileset extraction honoring the background color, and a checkbox to exclude the map from the build.
- Sprite editor: an indicator on the Save button when the sprite has been modified.
- Image editor: fixed crop (it rescaled to the base image) and the crash when loading a very tall image after a smaller one.
ESP32 tools and hardware
- ESP32 Tools form redesigned, with hardware profiles and user devices.
- Display bus management decoupled from the controller chip: every controller and every bus (SPI / QSPI / I80 / MIPI-DSI) shows up on any SoC, and the user picks the right combination.
- The Rate field in the profile editor goes from a free-form
TEditto a 22050 / 44100 combo, andblock_framesnow defaults to 256. - Devices management reorganized.
Code intelligence
- Inherited members offered in completion and in the call tip.
- Ctrl+click and hover resolve the segment under the cursor, including chains 3+ levels deep.
- The hover's inferred type text filters per file.
- Full
structsupport in the highlighter, the live parser, the beautifier, the checker and the inference table; the Watch expands structs, and there is field completion and tooltips afterp.. - Support for
fnlambdas in the lexer, the live parser, the highlighter and completion. - Error tracker: inheritance cycle diagnostics and an anti-hang guard (the
A:B:Acase). - Fewer false positives in inference and in the error tracker; validation and native methods for
struct. - Support for
.txtand.mdfiles (excluded from syntax coloring and error detection) and error lines marked in the minimap. - A guard that prevents compiling with no active project.
- Hints up to date with the whole new API:
bullet,gravityScale,damping,category,collideWith,angularDamping,onCollision,applyTorque,applyForceAt,scene.sleeping,scene.sleepAll,Sound.pwm,Sound.tremolo, thedraw.fontrange and theplatform()values.
Other IDE improvements
- AI agent that generates I2C drivers, launched from the IDE's embedded Python. It writes the drivers in English, with
DRV_ES8311.aruas the canonical example. - Hover and pressed gradients derived from each button's color: the IDE's 171 buttons all shared the same blue gradient, so buttons with a color of their own (the orange of Save Device, the green of Scan Ports, the red of Cancel…) turned blue when touched. They are now regenerated in their own hue, keeping the structure of the original gradient.
- Errors form: multi-row selection, a context menu to copy to the clipboard, and better text contrast.
- Visual consistency:
#211E28background color on more forms. - Fixed the “ñ” character not showing up in the code editor.
- Improvements to suggestion ranking and to the minimap.
Documentation and website
- Structs page in the guide (ES + EN) and a section on callbacks stored in fields.
fnlambdas section inguide/functions, with the four ways of passing a callback (function, method, string and lambda) documented inTimer,gpio,MQTTandsprite.onCollision.spritemodule rewritten in its physics section:applyForce,applyImpulse,applyTorque,applyForceAt,rotates,angularVel,angularDampingandbullet, plus the correction of whataccelerationreally means — it was documented as a persistent acceleration when it always was a single-step force.scenemodule:sleepingandsleepAll.soundmodule fully documented:tone,pulse,noise,fade, slides,setRelease,envelope,vibrato,pwm,tremolo,setMusicVolume,isMusicPlaying, and fractional frequencies.platform()table fixed in both languages, with the constants column, and a completedraw.fontfont table.- SEO: general improvements, plus Google Analytics (GA4) integration with an event on downloads.
- URLs unified in English across both languages (no more crossed slugs) and redirects for the structs pages.
- New blog entries (music tracker; the IDE for prototyping and teaching with the ESP32).
Installation / upgrade notes
- Windows IDE: installer
JARU_IDE_0.9.6_Setup(ProductVersion 0.9.6.1, build 54). - ESP32 / ESP32-S3 VM: build
JARU_VM_ESP32_0.9.6(PlatformIO, LovyanGFX 1.2.21). - ESP32-P4 VM: target
esp32-p4(ESP-IDF). - Rebuild your project after upgrading so the resources are regenerated with the new compiler.
- Check the breaking changes table above:
sprite.spin,sprite.accelerationandscene.maxDeltaare the three points that may require code changes.