Skip to content

Commit a2dbd5e

Browse files
committed
perf: batch render state reads
1 parent cb5b3e3 commit a2dbd5e

1 file changed

Lines changed: 97 additions & 81 deletions

File tree

Sources/GhosttyVt/Terminal+Render.swift

Lines changed: 97 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,26 @@
11
import GhosttyVtRaw
22

33
extension Terminal {
4+
private static let frameDataKeys: [GhosttyRenderStateData] = [
5+
GHOSTTY_RENDER_STATE_DATA_COLS,
6+
GHOSTTY_RENDER_STATE_DATA_ROWS,
7+
GHOSTTY_RENDER_STATE_DATA_DIRTY,
8+
]
9+
10+
private static let cursorDataKeys: [GhosttyRenderStateData] = [
11+
GHOSTTY_RENDER_STATE_DATA_CURSOR_VISIBLE,
12+
GHOSTTY_RENDER_STATE_DATA_CURSOR_BLINKING,
13+
GHOSTTY_RENDER_STATE_DATA_CURSOR_PASSWORD_INPUT,
14+
GHOSTTY_RENDER_STATE_DATA_CURSOR_VIEWPORT_HAS_VALUE,
15+
GHOSTTY_RENDER_STATE_DATA_CURSOR_VISUAL_STYLE,
16+
]
17+
18+
private static let cursorPositionDataKeys: [GhosttyRenderStateData] = [
19+
GHOSTTY_RENDER_STATE_DATA_CURSOR_VIEWPORT_X,
20+
GHOSTTY_RENDER_STATE_DATA_CURSOR_VIEWPORT_Y,
21+
GHOSTTY_RENDER_STATE_DATA_CURSOR_VIEWPORT_WIDE_TAIL,
22+
]
23+
424
/// Captures a Swift-owned incremental render update.
525
///
626
/// This uses libghostty-vt's two-phase render update. Terminal access is
@@ -16,85 +36,75 @@ extension Terminal {
1636
try Self.check(beginResult)
1737
try Self.check(ghostty_render_state_end_update(renderState))
1838

19-
let size = try frameSize()
20-
let dirtyState = try frameDirtyState()
21-
let theme = try frameTheme(refresh: dirtyState == .full || cachedTheme == nil)
39+
let metadata = try frameMetadata()
40+
let theme = try frameTheme(refresh: metadata.dirtyState == .full || cachedTheme == nil)
2241
let cursor = try frameCursor()
23-
let rows = try changedRows(for: dirtyState, size: size)
42+
let rows = try changedRows(for: metadata.dirtyState, size: metadata.size)
2443

2544
try clearGlobalDirtyState()
2645

2746
return .init(
28-
size: size,
29-
dirtyState: dirtyState,
47+
size: metadata.size,
48+
dirtyState: metadata.dirtyState,
3049
theme: theme,
3150
cursor: cursor,
3251
rows: rows
3352
)
3453
}
3554

36-
private func frameSize() throws -> Size {
55+
private func frameMetadata() throws -> (size: Size, dirtyState: TerminalFrame.DirtyState) {
3756
var columns: UInt16 = 0
3857
var rows: UInt16 = 0
39-
try Self.check(ghostty_render_state_get(renderState, GHOSTTY_RENDER_STATE_DATA_COLS, &columns))
40-
try Self.check(ghostty_render_state_get(renderState, GHOSTTY_RENDER_STATE_DATA_ROWS, &rows))
41-
return .init(columns: columns, rows: rows)
42-
}
58+
var rawDirtyState: GhosttyRenderStateDirty = GHOSTTY_RENDER_STATE_DIRTY_FALSE
59+
var written = 0
4360

44-
private func frameDirtyState() throws -> TerminalFrame.DirtyState {
45-
var rawState: GhosttyRenderStateDirty = GHOSTTY_RENDER_STATE_DIRTY_FALSE
46-
try Self.check(ghostty_render_state_get(renderState, GHOSTTY_RENDER_STATE_DATA_DIRTY, &rawState))
61+
let result = COutputPointers.withPointers(&columns, &rows, &rawDirtyState) { values in
62+
Self.frameDataKeys.withUnsafeBufferPointer { keyBuffer in
63+
ghostty_render_state_get_multi(
64+
renderState,
65+
keyBuffer.count,
66+
keyBuffer.baseAddress,
67+
values.baseAddress,
68+
&written
69+
)
70+
}
71+
}
72+
try Self.check(result)
73+
guard written == Self.frameDataKeys.count else {
74+
throw TerminalError.unexpectedResult
75+
}
4776

48-
switch rawState {
77+
let dirtyState: TerminalFrame.DirtyState
78+
switch rawDirtyState {
4979
case GHOSTTY_RENDER_STATE_DIRTY_PARTIAL:
50-
return .partial
80+
dirtyState = .partial
5181
case GHOSTTY_RENDER_STATE_DIRTY_FULL:
52-
return .full
82+
dirtyState = .full
5383
default:
54-
return .clean
84+
dirtyState = .clean
5585
}
86+
87+
return (.init(columns: columns, rows: rows), dirtyState)
5688
}
5789

5890
private func frameTheme(refresh: Bool) throws -> TerminalFrame.Theme {
5991
if !refresh, let cachedTheme {
6092
return cachedTheme
6193
}
6294

63-
var background = GhosttyColorRgb()
64-
var foreground = GhosttyColorRgb()
65-
var hasCursor = false
66-
var cursor = GhosttyColorRgb()
67-
var palette = [GhosttyColorRgb](repeating: GhosttyColorRgb(), count: 256)
68-
69-
try Self.check(
70-
ghostty_render_state_get(renderState, GHOSTTY_RENDER_STATE_DATA_COLOR_BACKGROUND, &background)
71-
)
72-
try Self.check(
73-
ghostty_render_state_get(renderState, GHOSTTY_RENDER_STATE_DATA_COLOR_FOREGROUND, &foreground)
74-
)
75-
try Self.check(
76-
ghostty_render_state_get(renderState, GHOSTTY_RENDER_STATE_DATA_COLOR_CURSOR_HAS_VALUE, &hasCursor)
77-
)
78-
if hasCursor {
79-
try Self.check(
80-
ghostty_render_state_get(renderState, GHOSTTY_RENDER_STATE_DATA_COLOR_CURSOR, &cursor)
81-
)
82-
}
95+
var colors = GhosttyRenderStateColors()
96+
colors.size = MemoryLayout<GhosttyRenderStateColors>.size
97+
try Self.check(ghostty_render_state_colors_get(renderState, &colors))
8398

84-
let paletteResult = palette.withUnsafeMutableBufferPointer { buffer in
85-
ghostty_render_state_get(
86-
renderState,
87-
GHOSTTY_RENDER_STATE_DATA_COLOR_PALETTE,
88-
UnsafeMutableRawPointer(buffer.baseAddress!)
89-
)
99+
let palette = withUnsafeBytes(of: &colors.palette) { bytes in
100+
bytes.bindMemory(to: GhosttyColorRgb.self).map(Self.color(from:))
90101
}
91-
try Self.check(paletteResult)
92102

93103
let theme = TerminalFrame.Theme(
94-
foreground: Self.color(from: foreground),
95-
background: Self.color(from: background),
96-
cursor: hasCursor ? Self.color(from: cursor) : nil,
97-
palette: palette.map(Self.color(from:))
104+
foreground: Self.color(from: colors.foreground),
105+
background: Self.color(from: colors.background),
106+
cursor: colors.cursor_has_value ? Self.color(from: colors.cursor) : nil,
107+
palette: palette
98108
)
99109
cachedTheme = theme
100110
return theme
@@ -107,25 +117,28 @@ extension Terminal {
107117
var hasPosition = false
108118
var style: GhosttyRenderStateCursorVisualStyle = GHOSTTY_RENDER_STATE_CURSOR_VISUAL_STYLE_BLOCK
109119

110-
try Self.check(ghostty_render_state_get(renderState, GHOSTTY_RENDER_STATE_DATA_CURSOR_VISIBLE, &visible))
111-
try Self.check(ghostty_render_state_get(renderState, GHOSTTY_RENDER_STATE_DATA_CURSOR_BLINKING, &blinking))
112-
try Self.check(
113-
ghostty_render_state_get(
114-
renderState,
115-
GHOSTTY_RENDER_STATE_DATA_CURSOR_PASSWORD_INPUT,
116-
&passwordInput
117-
)
118-
)
119-
try Self.check(
120-
ghostty_render_state_get(
121-
renderState,
122-
GHOSTTY_RENDER_STATE_DATA_CURSOR_VIEWPORT_HAS_VALUE,
123-
&hasPosition
124-
)
125-
)
126-
try Self.check(
127-
ghostty_render_state_get(renderState, GHOSTTY_RENDER_STATE_DATA_CURSOR_VISUAL_STYLE, &style)
128-
)
120+
var written = 0
121+
let result = COutputPointers.withPointers(
122+
&visible,
123+
&blinking,
124+
&passwordInput,
125+
&hasPosition,
126+
&style
127+
) { values in
128+
Self.cursorDataKeys.withUnsafeBufferPointer { keyBuffer in
129+
ghostty_render_state_get_multi(
130+
renderState,
131+
keyBuffer.count,
132+
keyBuffer.baseAddress,
133+
values.baseAddress,
134+
&written
135+
)
136+
}
137+
}
138+
try Self.check(result)
139+
guard written == Self.cursorDataKeys.count else {
140+
throw TerminalError.unexpectedResult
141+
}
129142

130143
guard hasPosition else {
131144
return .init(
@@ -141,19 +154,22 @@ extension Terminal {
141154
var column: UInt16 = 0
142155
var row: UInt16 = 0
143156
var isWideTail = false
144-
try Self.check(
145-
ghostty_render_state_get(renderState, GHOSTTY_RENDER_STATE_DATA_CURSOR_VIEWPORT_X, &column)
146-
)
147-
try Self.check(
148-
ghostty_render_state_get(renderState, GHOSTTY_RENDER_STATE_DATA_CURSOR_VIEWPORT_Y, &row)
149-
)
150-
try Self.check(
151-
ghostty_render_state_get(
152-
renderState,
153-
GHOSTTY_RENDER_STATE_DATA_CURSOR_VIEWPORT_WIDE_TAIL,
154-
&isWideTail
155-
)
156-
)
157+
written = 0
158+
let positionResult = COutputPointers.withPointers(&column, &row, &isWideTail) { values in
159+
Self.cursorPositionDataKeys.withUnsafeBufferPointer { keyBuffer in
160+
ghostty_render_state_get_multi(
161+
renderState,
162+
keyBuffer.count,
163+
keyBuffer.baseAddress,
164+
values.baseAddress,
165+
&written
166+
)
167+
}
168+
}
169+
try Self.check(positionResult)
170+
guard written == Self.cursorPositionDataKeys.count else {
171+
throw TerminalError.unexpectedResult
172+
}
157173

158174
return .init(
159175
isVisible: visible,

0 commit comments

Comments
 (0)