| 856 | * maxX/maxY should already be clamped to both screen bounds by the caller. |
| 857 | */ |
| 858 | export function blitRegion( |
| 859 | dst: Screen, |
| 860 | src: Screen, |
| 861 | regionX: number, |
| 862 | regionY: number, |
| 863 | maxX: number, |
| 864 | maxY: number, |
| 865 | ): void { |
| 866 | regionX = Math.max(0, regionX) |
| 867 | regionY = Math.max(0, regionY) |
| 868 | if (regionX >= maxX || regionY >= maxY) return |
| 869 | |
| 870 | const rowLen = maxX - regionX |
| 871 | const srcStride = src.width << 1 |
| 872 | const dstStride = dst.width << 1 |
| 873 | const rowBytes = rowLen << 1 // 2 Int32s per cell |
| 874 | const srcCells = src.cells |
| 875 | const dstCells = dst.cells |
| 876 | const srcNoSel = src.noSelect |
| 877 | const dstNoSel = dst.noSelect |
| 878 | |
| 879 | // softWrap is per-row — copy the row range regardless of stride/width. |
| 880 | // Partial-width blits still carry the row's wrap provenance since the |
| 881 | // blitted content (a cached ink-text node) is what set the bit. |
| 882 | dst.softWrap.set(src.softWrap.subarray(regionY, maxY), regionY) |
| 883 | |
| 884 | // Fast path: contiguous memory when copying full-width rows at same stride |
| 885 | if (regionX === 0 && maxX === src.width && src.width === dst.width) { |
| 886 | const srcStart = regionY * srcStride |
| 887 | const totalBytes = (maxY - regionY) * srcStride |
| 888 | dstCells.set( |
| 889 | srcCells.subarray(srcStart, srcStart + totalBytes), |
| 890 | srcStart, // srcStart === dstStart when strides match and regionX === 0 |
| 891 | ) |
| 892 | // noSelect is 1 byte/cell vs cells' 8 — same region, different scale |
| 893 | const nsStart = regionY * src.width |
| 894 | const nsLen = (maxY - regionY) * src.width |
| 895 | dstNoSel.set(srcNoSel.subarray(nsStart, nsStart + nsLen), nsStart) |
| 896 | } else { |
| 897 | // Per-row copy for partial-width or mismatched-stride regions |
| 898 | let srcRowCI = regionY * srcStride + (regionX << 1) |
| 899 | let dstRowCI = regionY * dstStride + (regionX << 1) |
| 900 | let srcRowNS = regionY * src.width + regionX |
| 901 | let dstRowNS = regionY * dst.width + regionX |
| 902 | for (let y = regionY; y < maxY; y++) { |
| 903 | dstCells.set(srcCells.subarray(srcRowCI, srcRowCI + rowBytes), dstRowCI) |
| 904 | dstNoSel.set(srcNoSel.subarray(srcRowNS, srcRowNS + rowLen), dstRowNS) |
| 905 | srcRowCI += srcStride |
| 906 | dstRowCI += dstStride |
| 907 | srcRowNS += src.width |
| 908 | dstRowNS += dst.width |
| 909 | } |
| 910 | } |
| 911 | |
| 912 | // Compute damage once for the whole region |
| 913 | const regionRect = { |
| 914 | x: regionX, |
| 915 | y: regionY, |