Skip to content

散らかし跡の描画を差分更新にして全画面再描画をやめる#9

Closed
winebarrel wants to merge 3 commits into
mainfrom
litter-incremental-repaint
Closed

散らかし跡の描画を差分更新にして全画面再描画をやめる#9
winebarrel wants to merge 3 commits into
mainfrom
litter-incremental-repaint

Conversation

@winebarrel

Copy link
Copy Markdown
Owner

概要

PR #8 の crit レビューで収束したパフォーマンス指摘(#3、および #5)に対応します。

これまで散らかし跡のオーバーレイは litterView.needsDisplay = true全体を無効化し、draw()全ディスプレイ union 全体をクリアしていたため、猫が走行・爪とぎしている間はフルスクリーン相当の再合成が ~10〜60fps で走り続けていました。

変更

  • LitterField.update が、このtickで出現/消滅した mark のグローバル矩形(追加・寿命切れ・キャップ退避)を返すように変更。onTick はその矩形だけを LitterView.invalidateGlobal で無効化する。
  • fadingRects() を追加し、フェード中の mark だけを ~10fps で部分無効化。
  • draw()dirtyRect のみクリアし、dirtyRect と交差しない mark はスキップ。
  • ベース CGColor を2つキャッシュし、濃淡は ctx.setAlpha で適用(mark ごと・フレームごとの色オブジェクト生成をゼロに)。

これで再描画コストが「変更ピクセル数」に比例し、画面面積に依存しなくなります。常駐ペットとしての電力・CPU 負荷を大きく下げます。

見送った指摘(記録どおり)

確認

  • ビルド成功(xcodebuild
  • swiftformat / swiftlint クリーン

🤖 Generated with Claude Code

Address the converged performance finding (#3, plus #5): the overlay was
invalidated wholesale (litterView.needsDisplay = true) and draw() cleared
the entire multi-screen bounds every pass, so an active cat forced a full
virtual-screen recomposite ~10-60fps.

- LitterField.update now returns the global rects of marks that appeared
  or vanished (added, aged out, or evicted by the cap); onTick invalidates
  only those via LitterView.invalidateGlobal. fadingRects() drives the
  ~10fps refresh of marks in their fade window.
- draw() clears only dirtyRect and skips marks that don't intersect it.
- Cache the two base CGColors and apply per-mark opacity with ctx.setAlpha,
  so no color object is allocated per mark per frame.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pp772scLq2mo9cDaybWrLu
@winebarrel
winebarrel force-pushed the litter-incremental-repaint branch from 363f24d to 4d8bb36 Compare July 12, 2026 06:30
winebarrel and others added 2 commits July 12, 2026 15:39
Follow-up to the incremental repaint: draw() cleared the whole dirtyRect,
which AppKit hands as the union bounding box of all invalidated rects. When
marks are scattered — a long trail, or a whole tail fading at once via
fadingRects() — that union spans most of the screen even though the real
changed area is tiny, degrading back toward the old full-screen cost.

Use getRectsBeingDrawn(_:count:) to clear and hit-test each disjoint
sub-rect AppKit actually invalidated, so cost tracks changed pixels rather
than how far apart the changes are.

Also document that LitterView depends on being non-layer-backed: the
incremental model trusts pixels outside the invalidated rects to survive,
which a wantsLayer=true view would not guarantee.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pp772scLq2mo9cDaybWrLu
The per-sub-rect approach added overhead (per-mark rects.contains scan,
extra region bookkeeping) that raised load in practice rather than lowering
it, so drop it and keep the simpler #3 model: clear the single dirtyRect
AppKit hands us and repaint marks intersecting it.

The non-layer-backed dependency note on LitterView still applies and is
kept.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pp772scLq2mo9cDaybWrLu
@winebarrel winebarrel closed this Jul 12, 2026
@winebarrel
winebarrel deleted the litter-incremental-repaint branch July 12, 2026 06:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant