perf(daemon): reuse attachment render capture scratch
- commit
- 533cc8ff3bfe53f70bc7c2a4cea40585e4202f05
- author
- brice · · 2026-07-12 06:11 UTC
- parent
- 9e9e07f
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
4 files · +93 -20
M internal/usecase/daemon/client.go +1 -0
| @@ -37,6 +37,7 @@ type attachedClient struct { | |||
| 37 | 37 | coordinatorReadyEpoch atomic.Uint64 | |
| 38 | 38 | echoAck atomic.Uint64 | |
| 39 | 39 | pipelineCache composeCacheInput // only touched while sendMu is held | |
| 40 | + | renderScratch renderCaptureScratch // only touched while sendMu is held | |
| 40 | 41 | captureFrames map[layout.PaneID]capturedPaneRenderState // only touched while sendMu is held | |
| 41 | 42 | resizePaint pendingByteTimer // guarded by sendMu | |
| 42 | 43 | resizePaintGeneration uint64 // guarded by sendMu | |
| @@ -37,6 +37,7 @@ type attachedClient struct { | |||||
| 37 | coordinatorReadyEpoch atomic.Uint64 | 37 | coordinatorReadyEpoch atomic.Uint64 | ||
| 38 | echoAck atomic.Uint64 | 38 | echoAck atomic.Uint64 | ||
| 39 | pipelineCache composeCacheInput // only touched while sendMu is held | 39 | pipelineCache composeCacheInput // only touched while sendMu is held | ||
| 40 | + | renderScratch renderCaptureScratch // only touched while sendMu is held | |||
| 40 | captureFrames map[layout.PaneID]capturedPaneRenderState // only touched while sendMu is held | 41 | captureFrames map[layout.PaneID]capturedPaneRenderState // only touched while sendMu is held | ||
| 41 | resizePaint pendingByteTimer // guarded by sendMu | 42 | resizePaint pendingByteTimer // guarded by sendMu | ||
| 42 | resizePaintGeneration uint64 // guarded by sendMu | 43 | resizePaintGeneration uint64 // guarded by sendMu | ||
M internal/usecase/daemon/render.go +5 -2
| @@ -309,8 +309,11 @@ func (d *Daemon) paintForResizeGeneration(sess *session, ac *attachedClient, res | |||
| 309 | 309 | // Title refresh may inspect process state, so it remains before the capture | |
| 310 | 310 | // boundary and outside tab/pane ownership locks. | |
| 311 | 311 | tb.mu.Lock() | |
| 312 | - | layoutSnap := solveTabLayoutLocked(tb) | |
| 313 | - | titleIDs := titleBarPaneIDs(layoutSnap.placements, layoutSnap.ok) | |
| 312 | + | titleIDs := ac.renderScratch.titleIDs[:0] | |
| 313 | + | if tb.tree != nil { | |
| 314 | + | titleIDs = appendStackPaneIDs(titleIDs, tb.tree.Root) | |
| 315 | + | } | |
| 316 | + | ac.renderScratch.titleIDs = titleIDs | |
| 314 | 317 | floating := tb.floating.pane | |
| 315 | 318 | hasFloating := tb.floating.state == floatingVisible && floating != nil | |
| 316 | 319 | tb.mu.Unlock() | |
| @@ -309,8 +309,11 @@ func (d *Daemon) paintForResizeGeneration(sess *session, ac *attachedClient, res | |||||
| 309 | // Title refresh may inspect process state, so it remains before the capture | 309 | // Title refresh may inspect process state, so it remains before the capture | ||
| 310 | // boundary and outside tab/pane ownership locks. | 310 | // boundary and outside tab/pane ownership locks. | ||
| 311 | tb.mu.Lock() | 311 | tb.mu.Lock() | ||
| 312 | - | layoutSnap := solveTabLayoutLocked(tb) | 312 | + | titleIDs := ac.renderScratch.titleIDs[:0] |
| 313 | - | titleIDs := titleBarPaneIDs(layoutSnap.placements, layoutSnap.ok) | 313 | + | if tb.tree != nil { |
| 314 | + | titleIDs = appendStackPaneIDs(titleIDs, tb.tree.Root) | |||
| 315 | + | } | |||
| 316 | + | ac.renderScratch.titleIDs = titleIDs | |||
| 314 | floating := tb.floating.pane | 317 | floating := tb.floating.pane | ||
| 315 | hasFloating := tb.floating.state == floatingVisible && floating != nil | 318 | hasFloating := tb.floating.state == floatingVisible && floating != nil | ||
| 316 | tb.mu.Unlock() | 319 | tb.mu.Unlock() | ||
M internal/usecase/daemon/render_capture.go +80 -15
| @@ -23,6 +23,21 @@ const ( | |||
| 23 | 23 | // capturedRenderState is the immutable hand-off from authoritative daemon | |
| 24 | 24 | // state to composition. Every field is a value or an owned bounded copy; the | |
| 25 | 25 | // composer must not retain or consult session, tab, pane, or overlay owners. | |
| 26 | + | // renderCaptureScratch owns bounded snapshots for one attachment. The complete | |
| 27 | + | // capture/compose/emit transaction holds that attachment's sendMu, so these | |
| 28 | + | // buffers cannot be observed or reused before emission completes. | |
| 29 | + | type renderCaptureScratch struct { | |
| 30 | + | state capturedRenderState | |
| 31 | + | panes []capturedPaneRenderState | |
| 32 | + | placements []layout.Placement | |
| 33 | + | statusTabs []statusTab | |
| 34 | + | mru []recentSession | |
| 35 | + | ranked []rankedRecent | |
| 36 | + | titleIDs []layout.PaneID | |
| 37 | + | treeNodes []layout.Node | |
| 38 | + | treeUsed int | |
| 39 | + | } | |
| 40 | + | ||
| 26 | 41 | type capturedRenderState struct { | |
| 27 | 42 | attachment *attachedClient // identity only; never dereferenced by composition | |
| 28 | 43 | attachmentEpoch uint64 | |
| @@ -170,25 +185,36 @@ func captureRenderState(sess *session, ac *attachedClient, bars barState, overla | |||
| 170 | 185 | epoch := ac.coordinatorEpoch.Load() | |
| 171 | 186 | sess.mu.Unlock() | |
| 172 | 187 | ||
| 173 | - | bars.status.tabs = append([]statusTab(nil), bars.status.tabs...) | |
| 174 | - | bars.mru = append([]recentSession(nil), bars.mru...) | |
| 175 | - | bars.rankedRecent = append([]rankedRecent(nil), bars.rankedRecent...) | |
| 176 | - | preview = clonePickerPreview(preview) | |
| 188 | + | scratch := &ac.renderScratch | |
| 189 | + | scratch.statusTabs = append(scratch.statusTabs[:0], bars.status.tabs...) | |
| 190 | + | bars.status.tabs = scratch.statusTabs | |
| 191 | + | scratch.mru = append(scratch.mru[:0], bars.mru...) | |
| 192 | + | bars.mru = scratch.mru | |
| 193 | + | scratch.ranked = append(scratch.ranked[:0], bars.rankedRecent...) | |
| 194 | + | bars.rankedRecent = scratch.ranked | |
| 195 | + | if len(preview.Rows) > 0 { | |
| 196 | + | preview = clonePickerPreview(preview) | |
| 197 | + | } else { | |
| 198 | + | preview = picker.Preview{Width: preview.Width, Height: preview.Height} | |
| 199 | + | } | |
| 177 | 200 | ||
| 178 | 201 | tb.mu.Lock() | |
| 179 | 202 | defer tb.mu.Unlock() | |
| 180 | 203 | layoutSnap := solveTabLayoutLocked(tb) | |
| 181 | - | state := &capturedRenderState{ | |
| 204 | + | scratch.placements = append(scratch.placements[:0], layoutSnap.placements...) | |
| 205 | + | scratch.treeUsed = 0 | |
| 206 | + | var root *layout.Node | |
| 207 | + | if tb.tree != nil { | |
| 208 | + | root = cloneLayoutNodeIntoScratch(tb.tree.Root, scratch) | |
| 209 | + | } | |
| 210 | + | state := &scratch.state | |
| 211 | + | *state = capturedRenderState{ | |
| 182 | 212 | attachment: ac, attachmentEpoch: epoch, reset: reset, bars: bars, theme: bars.theme, | |
| 183 | 213 | overlays: overlays, preview: preview, | |
| 184 | - | layout: capturedTabLayout{root: func() *layout.Node { | |
| 185 | - | if tb.tree == nil { | |
| 186 | - | return nil | |
| 187 | - | } | |
| 188 | - | return tb.tree.Clone().Root | |
| 189 | - | }(), area: layoutSnap.area, focus: layoutSnap.focus, placements: append([]layout.Placement(nil), layoutSnap.placements...), fingerprint: layoutSnap.fingerprint, valid: layoutSnap.ok}, | |
| 214 | + | layout: capturedTabLayout{root: root, area: layoutSnap.area, focus: layoutSnap.focus, placements: scratch.placements, fingerprint: layoutSnap.fingerprint, valid: layoutSnap.ok}, | |
| 190 | 215 | floatingGeneration: tb.floating.generation, | |
| 191 | 216 | } | |
| 217 | + | state.panes = scratch.panes[:0] | |
| 192 | 218 | state.tabGeneration = uint64(len(layoutSnap.fingerprint)) | |
| 193 | 219 | for _, placement := range layoutSnap.placements { | |
| 194 | 220 | p := tb.panes[placement.ID] | |
| @@ -204,20 +230,20 @@ func captureRenderState(sess *session, ac *attachedClient, bars barState, overla | |||
| 204 | 230 | ac.captureFrames = make(map[layout.PaneID]capturedPaneRenderState) | |
| 205 | 231 | } | |
| 206 | 232 | captured := capturePaneRenderStateLockedInto(p, visible, mode, ac.captureFrames[p.id]) | |
| 207 | - | captured.placement = placement | |
| 208 | - | captured.focused = placement.ID == layoutSnap.focus | |
| 233 | + | captured.placement, captured.focused = placement, placement.ID == layoutSnap.focus | |
| 209 | 234 | if captured.focused { | |
| 210 | 235 | state.cursor = captureCursorInputsLocked(p, placement.Content, overlays) | |
| 211 | 236 | } | |
| 212 | 237 | p.mu.Unlock() | |
| 213 | - | translated := make([]renderer.Damage, 0, len(captured.damage)) | |
| 238 | + | translated := captured.rawDamage[:0] | |
| 214 | 239 | for _, damage := range captured.damage { | |
| 215 | 240 | translated = append(translated, translatePaneDamage(damage, placement.Content, layoutSnap.area)...) | |
| 216 | 241 | } | |
| 217 | - | captured.damage = translated | |
| 242 | + | captured.rawDamage, captured.damage = captured.damage, translated | |
| 218 | 243 | ac.captureFrames[captured.id] = captured | |
| 219 | 244 | state.panes = append(state.panes, captured) | |
| 220 | 245 | } | |
| 246 | + | scratch.panes = state.panes | |
| 221 | 247 | if tb.floating.state == floatingVisible && tb.floating.pane != nil { | |
| 222 | 248 | p := tb.floating.pane | |
| 223 | 249 | p.mu.Lock() | |
| @@ -234,6 +260,25 @@ func captureRenderState(sess *session, ac *attachedClient, bars barState, overla | |||
| 234 | 260 | return state, true | |
| 235 | 261 | } | |
| 236 | 262 | ||
| 263 | + | func cloneLayoutNodeIntoScratch(src *layout.Node, scratch *renderCaptureScratch) *layout.Node { | |
| 264 | + | if src == nil { | |
| 265 | + | return nil | |
| 266 | + | } | |
| 267 | + | if scratch.treeUsed == len(scratch.treeNodes) { | |
| 268 | + | scratch.treeNodes = append(scratch.treeNodes, layout.Node{}) | |
| 269 | + | } | |
| 270 | + | index := scratch.treeUsed | |
| 271 | + | scratch.treeUsed++ | |
| 272 | + | dst := &scratch.treeNodes[index] | |
| 273 | + | children := dst.Children[:0] | |
| 274 | + | *dst = *src | |
| 275 | + | dst.Children = children | |
| 276 | + | for _, child := range src.Children { | |
| 277 | + | dst.Children = append(dst.Children, cloneLayoutNodeIntoScratch(child, scratch)) | |
| 278 | + | } | |
| 279 | + | return dst | |
| 280 | + | } | |
| 281 | + | ||
| 237 | 282 | func captureCursorInputsLocked(p *pane, content domain.Rect, overlays capturedOverlayRenderState) capturedCursorInputs { | |
| 238 | 283 | style, hasStyle := p.screen.CursorStyle() | |
| 239 | 284 | hidden := overlays.copyActive || overlays.copySearchActive || overlays.pickerActive || overlays.paletteActive || overlays.promptActive | |
| @@ -247,3 +292,23 @@ func clonePickerPreview(in picker.Preview) picker.Preview { | |||
| 247 | 292 | } | |
| 248 | 293 | return out | |
| 249 | 294 | } | |
| 295 | + | ||
| 296 | + | // appendStackPaneIDs returns exactly the panes whose solved placement can own a | |
| 297 | + | // stack title bar. The caller holds tab.mu, and dst is attachment scratch. | |
| 298 | + | func appendStackPaneIDs(dst []layout.PaneID, node *layout.Node) []layout.PaneID { | |
| 299 | + | if node == nil { | |
| 300 | + | return dst | |
| 301 | + | } | |
| 302 | + | if node.Kind == layout.Stack { | |
| 303 | + | for _, child := range node.Children { | |
| 304 | + | if child != nil && child.Kind == layout.Leaf { | |
| 305 | + | dst = append(dst, child.Leaf) | |
| 306 | + | } | |
| 307 | + | } | |
| 308 | + | return dst | |
| 309 | + | } | |
| 310 | + | for _, child := range node.Children { | |
| 311 | + | dst = appendStackPaneIDs(dst, child) | |
| 312 | + | } | |
| 313 | + | return dst | |
| 314 | + | } | |
| @@ -23,6 +23,21 @@ const ( | |||||
| 23 | // capturedRenderState is the immutable hand-off from authoritative daemon | 23 | // capturedRenderState is the immutable hand-off from authoritative daemon | ||
| 24 | // state to composition. Every field is a value or an owned bounded copy; the | 24 | // state to composition. Every field is a value or an owned bounded copy; the | ||
| 25 | // composer must not retain or consult session, tab, pane, or overlay owners. | 25 | // composer must not retain or consult session, tab, pane, or overlay owners. | ||
| 26 | + | // renderCaptureScratch owns bounded snapshots for one attachment. The complete | |||
| 27 | + | // capture/compose/emit transaction holds that attachment's sendMu, so these | |||
| 28 | + | // buffers cannot be observed or reused before emission completes. | |||
| 29 | + | type renderCaptureScratch struct { | |||
| 30 | + | state capturedRenderState | |||
| 31 | + | panes []capturedPaneRenderState | |||
| 32 | + | placements []layout.Placement | |||
| 33 | + | statusTabs []statusTab | |||
| 34 | + | mru []recentSession | |||
| 35 | + | ranked []rankedRecent | |||
| 36 | + | titleIDs []layout.PaneID | |||
| 37 | + | treeNodes []layout.Node | |||
| 38 | + | treeUsed int | |||
| 39 | + | } | |||
| 40 | + | ||||
| 26 | type capturedRenderState struct { | 41 | type capturedRenderState struct { | ||
| 27 | attachment *attachedClient // identity only; never dereferenced by composition | 42 | attachment *attachedClient // identity only; never dereferenced by composition | ||
| 28 | attachmentEpoch uint64 | 43 | attachmentEpoch uint64 | ||
| @@ -170,25 +185,36 @@ func captureRenderState(sess *session, ac *attachedClient, bars barState, overla | |||||
| 170 | epoch := ac.coordinatorEpoch.Load() | 185 | epoch := ac.coordinatorEpoch.Load() | ||
| 171 | sess.mu.Unlock() | 186 | sess.mu.Unlock() | ||
| 172 | 187 | ||||
| 173 | - | bars.status.tabs = append([]statusTab(nil), bars.status.tabs...) | 188 | + | scratch := &ac.renderScratch |
| 174 | - | bars.mru = append([]recentSession(nil), bars.mru...) | 189 | + | scratch.statusTabs = append(scratch.statusTabs[:0], bars.status.tabs...) |
| 175 | - | bars.rankedRecent = append([]rankedRecent(nil), bars.rankedRecent...) | 190 | + | bars.status.tabs = scratch.statusTabs |
| 176 | - | preview = clonePickerPreview(preview) | 191 | + | scratch.mru = append(scratch.mru[:0], bars.mru...) |
| 192 | + | bars.mru = scratch.mru | |||
| 193 | + | scratch.ranked = append(scratch.ranked[:0], bars.rankedRecent...) | |||
| 194 | + | bars.rankedRecent = scratch.ranked | |||
| 195 | + | if len(preview.Rows) > 0 { | |||
| 196 | + | preview = clonePickerPreview(preview) | |||
| 197 | + | } else { | |||
| 198 | + | preview = picker.Preview{Width: preview.Width, Height: preview.Height} | |||
| 199 | + | } | |||
| 177 | 200 | ||||
| 178 | tb.mu.Lock() | 201 | tb.mu.Lock() | ||
| 179 | defer tb.mu.Unlock() | 202 | defer tb.mu.Unlock() | ||
| 180 | layoutSnap := solveTabLayoutLocked(tb) | 203 | layoutSnap := solveTabLayoutLocked(tb) | ||
| 181 | - | state := &capturedRenderState{ | 204 | + | scratch.placements = append(scratch.placements[:0], layoutSnap.placements...) |
| 205 | + | scratch.treeUsed = 0 | |||
| 206 | + | var root *layout.Node | |||
| 207 | + | if tb.tree != nil { | |||
| 208 | + | root = cloneLayoutNodeIntoScratch(tb.tree.Root, scratch) | |||
| 209 | + | } | |||
| 210 | + | state := &scratch.state | |||
| 211 | + | *state = capturedRenderState{ | |||
| 182 | attachment: ac, attachmentEpoch: epoch, reset: reset, bars: bars, theme: bars.theme, | 212 | attachment: ac, attachmentEpoch: epoch, reset: reset, bars: bars, theme: bars.theme, | ||
| 183 | overlays: overlays, preview: preview, | 213 | overlays: overlays, preview: preview, | ||
| 184 | - | layout: capturedTabLayout{root: func() *layout.Node { | 214 | + | layout: capturedTabLayout{root: root, area: layoutSnap.area, focus: layoutSnap.focus, placements: scratch.placements, fingerprint: layoutSnap.fingerprint, valid: layoutSnap.ok}, |
| 185 | - | if tb.tree == nil { | |||
| 186 | - | return nil | |||
| 187 | - | } | |||
| 188 | - | return tb.tree.Clone().Root | |||
| 189 | - | }(), area: layoutSnap.area, focus: layoutSnap.focus, placements: append([]layout.Placement(nil), layoutSnap.placements...), fingerprint: layoutSnap.fingerprint, valid: layoutSnap.ok}, | |||
| 190 | floatingGeneration: tb.floating.generation, | 215 | floatingGeneration: tb.floating.generation, | ||
| 191 | } | 216 | } | ||
| 217 | + | state.panes = scratch.panes[:0] | |||
| 192 | state.tabGeneration = uint64(len(layoutSnap.fingerprint)) | 218 | state.tabGeneration = uint64(len(layoutSnap.fingerprint)) | ||
| 193 | for _, placement := range layoutSnap.placements { | 219 | for _, placement := range layoutSnap.placements { | ||
| 194 | p := tb.panes[placement.ID] | 220 | p := tb.panes[placement.ID] | ||
| @@ -204,20 +230,20 @@ func captureRenderState(sess *session, ac *attachedClient, bars barState, overla | |||||
| 204 | ac.captureFrames = make(map[layout.PaneID]capturedPaneRenderState) | 230 | ac.captureFrames = make(map[layout.PaneID]capturedPaneRenderState) | ||
| 205 | } | 231 | } | ||
| 206 | captured := capturePaneRenderStateLockedInto(p, visible, mode, ac.captureFrames[p.id]) | 232 | captured := capturePaneRenderStateLockedInto(p, visible, mode, ac.captureFrames[p.id]) | ||
| 207 | - | captured.placement = placement | 233 | + | captured.placement, captured.focused = placement, placement.ID == layoutSnap.focus |
| 208 | - | captured.focused = placement.ID == layoutSnap.focus | |||
| 209 | if captured.focused { | 234 | if captured.focused { | ||
| 210 | state.cursor = captureCursorInputsLocked(p, placement.Content, overlays) | 235 | state.cursor = captureCursorInputsLocked(p, placement.Content, overlays) | ||
| 211 | } | 236 | } | ||
| 212 | p.mu.Unlock() | 237 | p.mu.Unlock() | ||
| 213 | - | translated := make([]renderer.Damage, 0, len(captured.damage)) | 238 | + | translated := captured.rawDamage[:0] |
| 214 | for _, damage := range captured.damage { | 239 | for _, damage := range captured.damage { | ||
| 215 | translated = append(translated, translatePaneDamage(damage, placement.Content, layoutSnap.area)...) | 240 | translated = append(translated, translatePaneDamage(damage, placement.Content, layoutSnap.area)...) | ||
| 216 | } | 241 | } | ||
| 217 | - | captured.damage = translated | 242 | + | captured.rawDamage, captured.damage = captured.damage, translated |
| 218 | ac.captureFrames[captured.id] = captured | 243 | ac.captureFrames[captured.id] = captured | ||
| 219 | state.panes = append(state.panes, captured) | 244 | state.panes = append(state.panes, captured) | ||
| 220 | } | 245 | } | ||
| 246 | + | scratch.panes = state.panes | |||
| 221 | if tb.floating.state == floatingVisible && tb.floating.pane != nil { | 247 | if tb.floating.state == floatingVisible && tb.floating.pane != nil { | ||
| 222 | p := tb.floating.pane | 248 | p := tb.floating.pane | ||
| 223 | p.mu.Lock() | 249 | p.mu.Lock() | ||
| @@ -234,6 +260,25 @@ func captureRenderState(sess *session, ac *attachedClient, bars barState, overla | |||||
| 234 | return state, true | 260 | return state, true | ||
| 235 | } | 261 | } | ||
| 236 | 262 | ||||
| 263 | + | func cloneLayoutNodeIntoScratch(src *layout.Node, scratch *renderCaptureScratch) *layout.Node { | |||
| 264 | + | if src == nil { | |||
| 265 | + | return nil | |||
| 266 | + | } | |||
| 267 | + | if scratch.treeUsed == len(scratch.treeNodes) { | |||
| 268 | + | scratch.treeNodes = append(scratch.treeNodes, layout.Node{}) | |||
| 269 | + | } | |||
| 270 | + | index := scratch.treeUsed | |||
| 271 | + | scratch.treeUsed++ | |||
| 272 | + | dst := &scratch.treeNodes[index] | |||
| 273 | + | children := dst.Children[:0] | |||
| 274 | + | *dst = *src | |||
| 275 | + | dst.Children = children | |||
| 276 | + | for _, child := range src.Children { | |||
| 277 | + | dst.Children = append(dst.Children, cloneLayoutNodeIntoScratch(child, scratch)) | |||
| 278 | + | } | |||
| 279 | + | return dst | |||
| 280 | + | } | |||
| 281 | + | ||||
| 237 | func captureCursorInputsLocked(p *pane, content domain.Rect, overlays capturedOverlayRenderState) capturedCursorInputs { | 282 | func captureCursorInputsLocked(p *pane, content domain.Rect, overlays capturedOverlayRenderState) capturedCursorInputs { | ||
| 238 | style, hasStyle := p.screen.CursorStyle() | 283 | style, hasStyle := p.screen.CursorStyle() | ||
| 239 | hidden := overlays.copyActive || overlays.copySearchActive || overlays.pickerActive || overlays.paletteActive || overlays.promptActive | 284 | hidden := overlays.copyActive || overlays.copySearchActive || overlays.pickerActive || overlays.paletteActive || overlays.promptActive | ||
| @@ -247,3 +292,23 @@ func clonePickerPreview(in picker.Preview) picker.Preview { | |||||
| 247 | } | 292 | } | ||
| 248 | return out | 293 | return out | ||
| 249 | } | 294 | } | ||
| 295 | + | ||||
| 296 | + | // appendStackPaneIDs returns exactly the panes whose solved placement can own a | |||
| 297 | + | // stack title bar. The caller holds tab.mu, and dst is attachment scratch. | |||
| 298 | + | func appendStackPaneIDs(dst []layout.PaneID, node *layout.Node) []layout.PaneID { | |||
| 299 | + | if node == nil { | |||
| 300 | + | return dst | |||
| 301 | + | } | |||
| 302 | + | if node.Kind == layout.Stack { | |||
| 303 | + | for _, child := range node.Children { | |||
| 304 | + | if child != nil && child.Kind == layout.Leaf { | |||
| 305 | + | dst = append(dst, child.Leaf) | |||
| 306 | + | } | |||
| 307 | + | } | |||
| 308 | + | return dst | |||
| 309 | + | } | |||
| 310 | + | for _, child := range node.Children { | |||
| 311 | + | dst = appendStackPaneIDs(dst, child) | |||
| 312 | + | } | |||
| 313 | + | return dst | |||
| 314 | + | } | |||
M internal/usecase/daemon/render_pipeline.go +7 -3
| @@ -21,6 +21,7 @@ type composeCacheInput struct { | |||
| 21 | 21 | frame renderer.Frame | |
| 22 | 22 | layoutFingerprint string | |
| 23 | 23 | titleGenerations map[layout.PaneID]uint64 | |
| 24 | + | damage []renderer.Damage | |
| 24 | 25 | floatingGeneration uint64 | |
| 25 | 26 | floatingGeometry floatingGeometry | |
| 26 | 27 | floatingTitleGeneration uint64 | |
| @@ -60,8 +61,11 @@ func composeFrame(state capturedRenderState, in composeCacheInput) composedRende | |||
| 60 | 61 | } | |
| 61 | 62 | ||
| 62 | 63 | full := state.reset || !in.valid || in.frame.Width != width || in.frame.Height != rows+2 || in.layoutFingerprint != state.layout.fingerprint || in.theme != state.theme | |
| 63 | - | titles := make(map[layout.PaneID]uint64, len(state.panes)) | |
| 64 | - | var damage []renderer.Damage | |
| 64 | + | titles := in.titleGenerations | |
| 65 | + | if titles == nil { | |
| 66 | + | titles = make(map[layout.PaneID]uint64, len(state.panes)) | |
| 67 | + | } | |
| 68 | + | damage := in.damage[:0] | |
| 65 | 69 | for _, pane := range state.panes { | |
| 66 | 70 | pl := offsetPlacement(pane.placement, 0, 1) | |
| 67 | 71 | if pl.TitleBar.Height > 0 { | |
| @@ -110,7 +114,7 @@ func composeFrame(state capturedRenderState, in composeCacheInput) composedRende | |||
| 110 | 114 | cursorInputs := state.cursor | |
| 111 | 115 | cursorInputs.hiddenByOverlay = cursorInputs.hiddenByOverlay || state.overlays.active() | |
| 112 | 116 | cursor := desiredCapturedCursor(cursorInputs) | |
| 113 | - | outCache := composeCacheInput{valid: !state.overlays.active(), frame: baseFrame, layoutFingerprint: state.layout.fingerprint, theme: state.theme, titleGenerations: titles, floatingGeneration: state.floating.generation, floatingGeometry: state.floating.geometry.translate(content.X, content.Y), floatingTitleGeneration: state.floating.titleGeneration, bars: in.bars} | |
| 117 | + | outCache := composeCacheInput{valid: !state.overlays.active(), frame: baseFrame, layoutFingerprint: state.layout.fingerprint, theme: state.theme, titleGenerations: titles, damage: damage, floatingGeneration: state.floating.generation, floatingGeometry: state.floating.geometry.translate(content.X, content.Y), floatingTitleGeneration: state.floating.titleGeneration, bars: in.bars} | |
| 114 | 118 | outCache.bars.capture(baseFrame.Row(0), baseFrame.Row(rows+1)) | |
| 115 | 119 | return composedRenderFrame{frame: frame, damage: damage, cursor: cursor, cache: outCache, reset: state.reset || state.overlays.active()} | |
| 116 | 120 | } | |
| @@ -21,6 +21,7 @@ type composeCacheInput struct { | |||||
| 21 | frame renderer.Frame | 21 | frame renderer.Frame | ||
| 22 | layoutFingerprint string | 22 | layoutFingerprint string | ||
| 23 | titleGenerations map[layout.PaneID]uint64 | 23 | titleGenerations map[layout.PaneID]uint64 | ||
| 24 | + | damage []renderer.Damage | |||
| 24 | floatingGeneration uint64 | 25 | floatingGeneration uint64 | ||
| 25 | floatingGeometry floatingGeometry | 26 | floatingGeometry floatingGeometry | ||
| 26 | floatingTitleGeneration uint64 | 27 | floatingTitleGeneration uint64 | ||
| @@ -60,8 +61,11 @@ func composeFrame(state capturedRenderState, in composeCacheInput) composedRende | |||||
| 60 | } | 61 | } | ||
| 61 | 62 | ||||
| 62 | full := state.reset || !in.valid || in.frame.Width != width || in.frame.Height != rows+2 || in.layoutFingerprint != state.layout.fingerprint || in.theme != state.theme | 63 | full := state.reset || !in.valid || in.frame.Width != width || in.frame.Height != rows+2 || in.layoutFingerprint != state.layout.fingerprint || in.theme != state.theme | ||
| 63 | - | titles := make(map[layout.PaneID]uint64, len(state.panes)) | 64 | + | titles := in.titleGenerations |
| 64 | - | var damage []renderer.Damage | 65 | + | if titles == nil { |
| 66 | + | titles = make(map[layout.PaneID]uint64, len(state.panes)) | |||
| 67 | + | } | |||
| 68 | + | damage := in.damage[:0] | |||
| 65 | for _, pane := range state.panes { | 69 | for _, pane := range state.panes { | ||
| 66 | pl := offsetPlacement(pane.placement, 0, 1) | 70 | pl := offsetPlacement(pane.placement, 0, 1) | ||
| 67 | if pl.TitleBar.Height > 0 { | 71 | if pl.TitleBar.Height > 0 { | ||
| @@ -110,7 +114,7 @@ func composeFrame(state capturedRenderState, in composeCacheInput) composedRende | |||||
| 110 | cursorInputs := state.cursor | 114 | cursorInputs := state.cursor | ||
| 111 | cursorInputs.hiddenByOverlay = cursorInputs.hiddenByOverlay || state.overlays.active() | 115 | cursorInputs.hiddenByOverlay = cursorInputs.hiddenByOverlay || state.overlays.active() | ||
| 112 | cursor := desiredCapturedCursor(cursorInputs) | 116 | cursor := desiredCapturedCursor(cursorInputs) | ||
| 113 | - | outCache := composeCacheInput{valid: !state.overlays.active(), frame: baseFrame, layoutFingerprint: state.layout.fingerprint, theme: state.theme, titleGenerations: titles, floatingGeneration: state.floating.generation, floatingGeometry: state.floating.geometry.translate(content.X, content.Y), floatingTitleGeneration: state.floating.titleGeneration, bars: in.bars} | 117 | + | outCache := composeCacheInput{valid: !state.overlays.active(), frame: baseFrame, layoutFingerprint: state.layout.fingerprint, theme: state.theme, titleGenerations: titles, damage: damage, floatingGeneration: state.floating.generation, floatingGeometry: state.floating.geometry.translate(content.X, content.Y), floatingTitleGeneration: state.floating.titleGeneration, bars: in.bars} |
| 114 | outCache.bars.capture(baseFrame.Row(0), baseFrame.Row(rows+1)) | 118 | outCache.bars.capture(baseFrame.Row(0), baseFrame.Row(rows+1)) | ||
| 115 | return composedRenderFrame{frame: frame, damage: damage, cursor: cursor, cache: outCache, reset: state.reset || state.overlays.active()} | 119 | return composedRenderFrame{frame: frame, damage: damage, cursor: cursor, cache: outCache, reset: state.reset || state.overlays.active()} | ||
| 116 | } | 120 | } | ||