Skip to content

Commit d84ddd2

Browse files
committed
Update docs, provide additional api for tecolot
1 parent b50e3b6 commit d84ddd2

3 files changed

Lines changed: 144 additions & 0 deletions

File tree

Sources/SwiftTerm/Apple/AppleTerminalView.swift

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3004,6 +3004,25 @@ extension TerminalView {
30043004
requestImmediateFrame()
30053005
}
30063006

3007+
/// Asks for the terminal to be redrawn.
3008+
///
3009+
/// Call this after changing terminal state behind SwiftTerm's back —
3010+
/// through ``getTerminal()``, a soft or hard reset, a palette swap — where
3011+
/// there is no way for the view to know the display is now stale. Output
3012+
/// fed through ``feed(byteArray:)`` needs no such call.
3013+
///
3014+
/// **`setNeedsDisplay` is not a substitute.** With a GPU renderer the view
3015+
/// does not draw through AppKit at all: `draw(_:)` returns immediately and
3016+
/// frames come from the frame driver, so an invalidation is silently
3017+
/// dropped. That has been true whenever Metal was enabled, and Metal is
3018+
/// the default on macOS since the render loop landed.
3019+
///
3020+
/// Safe to call from any thread.
3021+
public func requestRedraw ()
3022+
{
3023+
frameDriver?.markDirty()
3024+
}
3025+
30073026
/// Asks for a frame ahead of the display cadence. Safe from any thread.
30083027
///
30093028
/// With a render loop running this signals the loop directly instead of
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
# Adopting the render loop
2+
3+
What changes for an application moving to the branch where SwiftTerm prepares
4+
and draws frames off the main thread.
5+
6+
## Overview
7+
8+
On macOS, a terminal using the GPU renderer now prepares and submits its frames
9+
on a dedicated render thread, driven by the display link. The main thread
10+
captures view state, signals the loop, and applies the AppKit side effects the
11+
loop hands back. Main-thread stall p99 under a flood drops from roughly 7–19 ms
12+
to 0.03–0.12 ms.
13+
14+
**No API was removed and nothing became illegal.** Most applications need to
15+
change nothing. The list below is what is worth checking, in the order it is
16+
likely to matter.
17+
18+
## 1. `setNeedsDisplay` does not force a terminal repaint
19+
20+
If your code calls `setNeedsDisplay` on the terminal view to refresh it after
21+
changing terminal state directly — a soft reset, a palette swap, a renderer
22+
toggle — **that call does nothing.** `TerminalView.draw(_:)` returns
23+
immediately whenever a GPU renderer is attached, and frames come from the frame
24+
driver instead.
25+
26+
This was already true whenever Metal was enabled. It matters more now because
27+
Metal is the default on macOS.
28+
29+
```swift
30+
// Before: silently does nothing under a GPU renderer.
31+
terminal.getTerminal().softReset()
32+
terminal.setNeedsDisplay(terminal.bounds)
33+
34+
// After:
35+
terminal.getTerminal().softReset()
36+
terminal.requestRedraw()
37+
```
38+
39+
``TerminalView/requestRedraw()`` is safe from any thread. Output fed through
40+
``TerminalView/feed(byteArray:)`` never needs it — that path marks the frame
41+
itself.
42+
43+
## 2. Metal is on by default, on a new surface
44+
45+
``TerminalView/setUseMetal(_:)`` is unchanged. What changed is what you get:
46+
a `CAMetalLayer` this view owns, drawn from the render loop, rather than an
47+
`MTKView` drawn from AppKit's display cycle.
48+
49+
- ``TerminalView/usesMetalLayerSurface`` and
50+
``TerminalView/setUsesMetalLayerSurface(_:)`` select the surface at runtime.
51+
Setting it while Metal is running rebuilds the surface in place.
52+
- ``TerminalView/isUsingRenderLoop`` reports whether frames are actually being
53+
prepared off the main thread.
54+
- `SWIFTTERM_METAL_LAYER=0` in the environment starts on the old `MTKView`
55+
surface. That is the rollback if you hit something; please report it.
56+
57+
`MTKView` remains the only surface on iOS.
58+
59+
## 3. You can call `send` and `feed` from any thread
60+
61+
``TerminalView/send(data:)`` used to assert it was on the main thread. It no
62+
longer does: it takes the terminal lock around the state it mutates. An SSH or
63+
agent transport can call it directly.
64+
65+
If you marshal input to the main thread only to satisfy that old contract, you
66+
can stop.
67+
68+
The one rule: do not call it from inside a terminal delegate callback. Those
69+
run with the lock held, and a precondition catches the mistake rather than
70+
deadlocking.
71+
72+
## 4. `sizeChanged` may arrive a frame later than the resize
73+
74+
During a live window drag, SwiftTerm coalesces resizes to one per display
75+
frame. Your ``TerminalViewDelegate/sizeChanged(source:newCols:newRows:)`` is
76+
therefore called after the drag step that caused it, not inside it.
77+
78+
That breaks the re-entrancy guard applications write when they resize a window
79+
from this callback:
80+
81+
```swift
82+
// Fragile: `changingSize` is already false when the callback arrives.
83+
if changingSize { return }
84+
changingSize = true
85+
window.setFrame(optimal, display: true, animate: true)
86+
changingSize = false
87+
```
88+
89+
Compare frames instead of using a flag, and do not animate the resize —
90+
animating it is a measurable performance problem in its own right. <doc:Embedding>
91+
has the numbers and a correct implementation.
92+
93+
Applications that do not resize a window from `sizeChanged` — anything with a
94+
fixed layout, or SwiftUI-driven sizing — are unaffected.
95+
96+
## 5. Delegate callbacks and your own threads
97+
98+
Unchanged, but easier to get wrong now that more of SwiftTerm is concurrent:
99+
terminal delegate methods can fire on the parse thread with the terminal lock
100+
held. Capture what you need and hop to the main queue yourself. Do not call
101+
back into SwiftTerm APIs that take the lock.
102+
103+
## 6. What you get for free
104+
105+
No action needed for any of these:
106+
107+
- Frames continue while the main thread is busy.
108+
- Input-to-glyph latency roughly halved; the old 150 ms post-input special case
109+
is gone.
110+
- The terminal stops drawing entirely when occluded, miniaturised, or the
111+
application is hidden.
112+
- The synchronized-output safety valve fires even while the main thread is
113+
blocked, so an application that sets DECSET 2026 and never clears it can no
114+
longer wedge the display.
115+
116+
## Topics
117+
118+
### Related
119+
120+
- <doc:Embedding>
121+
- <doc:GPURendering>
122+
- ``TerminalView/requestRedraw()``
123+
- ``TerminalView/setUsesMetalLayerSurface(_:)``

Sources/SwiftTerm/Documentation.docc/Documentation.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ testing, and screen-scraping terminal output.
6565
### Views
6666

6767
- <doc:Embedding>
68+
- <doc:AdoptingTheRenderLoop>
6869
- ``TerminalView``
6970
- ``TerminalViewDelegate``
7071

@@ -84,6 +85,7 @@ testing, and screen-scraping terminal output.
8485

8586
- <doc:GettingStarted>
8687
- <doc:Embedding>
88+
- <doc:AdoptingTheRenderLoop>
8789
- <doc:Customization>
8890
- <doc:BiDi>
8991
- <doc:GPURendering>

0 commit comments

Comments
 (0)