A rhythm game has to know how far a press lay from the beat. Input only
kept that a key went down during a frame, so a press was known to a
frame (16.7 ms) at best. Hit windows are ±20 to 50 ms.
- Input and the game pointer keep the time stamp of each real press.
Auto repeat of a held key does not count.
- The new event node On key hit hears a key or a click. It gives the
offset to the nearest point of the grid (beat, 1/8 or 1/16), with
bar, beat and the heard second. A calibration input moves the press.
- The SoundLibrary maps the press onto the heard song position through
getOutputTimestamp, minus the limiter. Safari lacks it, there the
audio clock minus the latency is wound back to the press.
- Playbacks.times takes the heard second as a parameter, so the song
position can be asked for a moment in the past.
- A pattern with swing puts its grid points where the swing moves the
notes. The end of a pattern without loop is no grid point.
- A press older than 0.25 s when its frame comes is dropped, which
covers presses during the toolbar pause. Exactly half way between two
points counts as late for the first one.
- Without music playing no event comes, as decided in the issue.
- The 400 step test in tilemap-patch.test.ts gets a 30 s limit. It took
7.45 s in gate 761 while the suite shared the cores of stein2.
Tests: 23 new unit tests (key-hit 19, sound-library 1, audio-switch 1,
catalog and event count). 23 mutations, all red. e2e 65 ALL PASSED,
79 checks. In the new 65k7 three presses of H in one frame lie 4.4 and
4.1 ms apart, and so do their offsets. Without the time stamps all
three get the same offset and only 65k7 goes red. Gate 5251 of 5252
green, the one red count in event-order.test.ts is fixed and checked
on its own.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Since #348 a painted cell worked out only the cells around it, but the
layer's tiles still came as a new list. groupByTile grouped that list
afresh, so meshFor saw no group again and every mesh on the layer was
built anew. On 120x120 that was the last cost that grew with the map.
- A patched list of tiles remembers the list it came from and which
entries went and came (tilesPatch). A cell that is drawn the same
keeps its entry, and a patch that changes nothing drawn hands back
the very same list.
- groupByTile and layerRows patch the groups of the list before
(patchedGroups). An untouched group stays the same array, so its
mesh is not built again. On a layer sorted by Y this holds for each
row and for the groups in it.
- A tile new to the layer still works the whole layer out again, as
since #348.
- Stage 2 of the issue (patching quads inside a mesh) is not needed.
The tilemap geometry no longer shows in the CPU profile.
Measured with the #348 probe, a brush stroke on a copy of Garden of
Sacrifices on stein2:
- Meshes built per frame: 21.9 to 0.94 on 60x60, 40.7 to 1.22 on
120x120.
- fps while painting: 49.4 to 51.1 on 60x60, 42.0 to 45.8 on 120x120.
- Painting now costs both sizes about 10 fps. The gap left between the
sizes is drawing the larger map, which shows without painting too.
Tests:
- Gate on melektaus-atx green, 312 files, 4832 tests.
- The random walk from #348 also compares groups and rows with a fresh
grouping after each of its 400 steps, and counts reused groups.
- A fixed test paints a cell and keeps every other group and row.
- Three counter-checks turn red: entries not removed from a group,
rows sorted by their first cell, groupByTile ignoring the patch.
- e2e 39 + 40 on melektaus-atx: ALL PASSED, 107 checks.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A painted cell worked out the tiles and blocking cells of the whole
map again, so painting got slower the bigger the map. One cell cost
16.8 ms on 60x60 and 54.6 ms on 120x120. Now it costs what is around
it: 3.5 ms and 4.0 ms, against 3.4 ms without painting.
- The brush, the paint command and its undo name the keys they
changed. Each new version of the cells keeps them for the last 16
versions (cellsChangedSince).
- The cell index, the tiles of a layer and the blocking cells patch
only the changed cells and their eight neighbours. Everything else
stays the same object as before.
- Both lists are in cell order now, so a patch is one walk that keeps
every other entry.
- The rectangles are merged row by row. A patch joins again only the
rows around it, one row further than the changed cells, and stacks
the rest as they were.
- A writer that names no keys costs one walk over the cells. A
replaced cell object (load, resize, edits at run time) is worked out
in full as before.
- The tiles a map names come sorted by id. Two trigger tiles on one
cell now keep that order, whichever way they were painted.
- resolveTilemap walked all tiles every frame, even without painting.
It now does so only when the list changed.
Found on the way: deleting an outlined autotile can turn the one below
into a full box, which cuts a scaled box two rows further down. Merging
only the patched rows missed that. It is a test of its own now.
A brush stroke on the map doubled to 120x120 runs at 42 fps instead of
28, p95 of a frame 52 ms instead of 92 ms. What still grows with the
map is the mesh of a layer, which is built again on every paint.
Tests:
- tests/core/tilemap-patch.test.ts (new, 7): 400 random steps on two
layers with every kind of tile, each compared with a fresh full run
- Unit, tilemap and viewport: 1284 green in 57 files
- e2e 39 + 40 (atx): ALL PASSED, 107 checks
- Gate (atx): 312 files, 4831 tests
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>