| 967 | |
| 968 | const CACHE_SLOTS = 4 |
| 969 | function cacheWrite( |
| 970 | node: Node, |
| 971 | aW: number, |
| 972 | aH: number, |
| 973 | wM: MeasureMode, |
| 974 | hM: MeasureMode, |
| 975 | oW: number, |
| 976 | oH: number, |
| 977 | fW: boolean, |
| 978 | fH: boolean, |
| 979 | wasDirty: boolean, |
| 980 | ): void { |
| 981 | if (!node._cIn) { |
| 982 | node._cIn = new Float64Array(CACHE_SLOTS * 8) |
| 983 | node._cOut = new Float64Array(CACHE_SLOTS * 2) |
| 984 | } |
| 985 | // First write after a dirty clears stale entries from before the dirty. |
| 986 | // _cGen < _generation means entries are from a previous calculateLayout; |
| 987 | // if wasDirty, the subtree changed since then → old dimensions invalid. |
| 988 | // Clean nodes' old entries stay — same subtree → same result for same |
| 989 | // inputs, so cross-generation caching works (the scroll hot path where |
| 990 | // 499 clean messages cache-hit while one dirty leaf recomputes). |
| 991 | if (wasDirty && node._cGen !== _generation) { |
| 992 | node._cN = 0 |
| 993 | node._cWr = 0 |
| 994 | } |
| 995 | // LRU write index wraps; _cN stays at CACHE_SLOTS so the read scan always |
| 996 | // checks all populated slots (not just those since last wrap). |
| 997 | const i = node._cWr++ % CACHE_SLOTS |
| 998 | if (node._cN < CACHE_SLOTS) node._cN = node._cWr |
| 999 | const o = i * 8 |
| 1000 | const cIn = node._cIn |
| 1001 | cIn[o] = aW |
| 1002 | cIn[o + 1] = aH |
| 1003 | cIn[o + 2] = wM |
| 1004 | cIn[o + 3] = hM |
| 1005 | cIn[o + 4] = oW |
| 1006 | cIn[o + 5] = oH |
| 1007 | cIn[o + 6] = fW ? 1 : 0 |
| 1008 | cIn[o + 7] = fH ? 1 : 0 |
| 1009 | node._cOut![i * 2] = node.layout.width |
| 1010 | node._cOut![i * 2 + 1] = node.layout.height |
| 1011 | node._cGen = _generation |
| 1012 | } |
| 1013 | |
| 1014 | // Store computed layout.width/height into the single-slot cache output fields. |
| 1015 | // _hasL/_hasM inputs are committed at the TOP of layoutNode (before compute); |