1
1
mirror of https://github.com/qvacua/vimr.git synced 2024-11-24 11:37:32 +03:00

GH-666 Optimize rendering

Too slow: When we parallelize the CTLine computation, it's quite fast,
but then, the CPU usage is very very high. Still, parallelizing.
This commit is contained in:
Tae Won Ha 2018-09-01 08:53:04 +02:00
parent 9b8e0b6d42
commit d531835e4c
8 changed files with 173 additions and 110 deletions

View File

@ -40,9 +40,36 @@ extension NvimView {
).set()
dirtyRects.fill()
self.runs(intersecting: dirtyRects).forEach { row in
self.draw(row, in: context)
let attrsRuns = self.runs(intersecting: dirtyRects)
let runs = attrsRuns.parallelMap {
// let runs = attrsRuns.map {
run -> (attrsRun: AttributesRun, fontGlyphRuns: [FontGlyphRun]) in
let font = FontUtils.font(adding: run.attrs.fontTrait, to: self.font)
let fontGlyphRuns = self.typesetter.fontGlyphRunsWithLigatures(
nvimUtf16Cells: run.cells.map { Array($0.string.utf16) },
startColumn: run.cells.startIndex,
offset: CGPoint(
x: self.xOffset, y: run.location.y + self.baselineOffset
),
font: font,
cellWidth: self.cellSize.width
)
return (attrsRun: run, fontGlyphRuns: fontGlyphRuns)
}
let defaultAttrs = self.cellAttributesCollection.defaultAttributes
runs.forEach { (attrsRun, fontGlyphRuns) in
self.runDrawer.draw(
attrsRun,
fontGlyphRuns: fontGlyphRuns,
defaultAttributes: defaultAttrs,
in: context
)
}
// self.drawCursor(context: context)
#if DEBUG
@ -278,6 +305,7 @@ extension NvimView {
of: newFont, linespacing: self.linespacing
)
self.baselineOffset = self.cellSize.height - CTFontGetAscent(newFont)
self.resizeNeoVimUi(to: self.bounds.size)
}
}

View File

@ -10,7 +10,7 @@ import MessagePack
extension NvimView {
func resize(_ value: MessagePackValue) {
final func resize(_ value: MessagePackValue) {
guard let array = MessagePackUtils.array(from: value, ofSize: 2, conversion: { $0.intValue }) else {
return
}
@ -22,16 +22,16 @@ extension NvimView {
}
}
func clear() {
final func clear() {
bridgeLogger.mark()
gui.async {
self.grid.clear()
// self.ugrid.clear()
self.markForRenderWholeView()
}
}
func modeChange(_ value: MessagePackValue) {
final func modeChange(_ value: MessagePackValue) {
guard let mode = MessagePackUtils.value(from: value, conversion: { v -> CursorModeShape? in
guard let rawValue = v.intValue else { return nil }
return CursorModeShape(rawValue: UInt(rawValue))
@ -43,7 +43,7 @@ extension NvimView {
}
}
func scroll(_ value: MessagePackValue) {
final func scroll(_ value: MessagePackValue) {
guard let array = MessagePackUtils.array(
from: value, ofSize: 6, conversion: { $0.intValue }
) else {
@ -64,7 +64,7 @@ extension NvimView {
}
}
func unmark(_ value: MessagePackValue) {
final func unmark(_ value: MessagePackValue) {
// bridgeLogger.debug("\(row):\(column)")
//
// gui.async {
@ -75,8 +75,9 @@ extension NvimView {
// }
}
func flush(_ renderData: [MessagePackValue]) {
bridgeLogger.hr()
final func flush(_ renderData: [MessagePackValue]) {
// bridgeLogger.hr()
bridgeLogger.debug(renderData.count)
gui.async {
renderData.forEach { value in
@ -109,14 +110,14 @@ extension NvimView {
}
}
func setTitle(with value: MessagePackValue) {
final func setTitle(with value: MessagePackValue) {
guard let title = value.stringValue else { return }
bridgeLogger.debug(title)
self.eventsSubject.onNext(.setTitle(title))
}
func stop() {
final func stop() {
bridgeLogger.hr()
try? self.api
.stop()
@ -132,7 +133,7 @@ extension NvimView {
.wait()
}
func autoCommandEvent(_ value: MessagePackValue) {
final func autoCommandEvent(_ value: MessagePackValue) {
guard let array = MessagePackUtils.array(from: value, ofSize: 2, conversion: { $0.intValue }),
let event = NvimAutoCommandEvent(rawValue: array[0]) else { return }
let bufferHandle = array[1]
@ -156,7 +157,7 @@ extension NvimView {
}
}
func ipcBecameInvalid(_ reason: String) {
final func ipcBecameInvalid(_ reason: String) {
bridgeLogger.debug(reason)
self.eventsSubject.onNext(.ipcBecameInvalid(reason))
@ -191,11 +192,11 @@ extension NvimView {
return
}
bridgeLogger.trace(
"row: \(row), startCol: \(startCol), endCol: \(endCol), " +
"clearCol: \(clearCol), clearAttr: \(clearAttr), " +
"chunk: \(chunk), attrIds: \(attrIds)"
)
// bridgeLogger.trace(
// "row: \(row), startCol: \(startCol), endCol: \(endCol), " +
// "clearCol: \(clearCol), clearAttr: \(clearAttr), " +
// "chunk: \(chunk), attrIds: \(attrIds)"
// )
let count = endCol - startCol
guard chunk.count == count && attrIds.count == count else { return }
@ -241,13 +242,13 @@ extension NvimView {
// MARK: - Simple
extension NvimView {
func bell() {
final func bell() {
bridgeLogger.mark()
NSSound.beep()
}
func cwdChanged(_ value: MessagePackValue) {
final func cwdChanged(_ value: MessagePackValue) {
guard let cwd = value.stringValue else { return }
bridgeLogger.debug(cwd)
@ -255,7 +256,7 @@ extension NvimView {
self.eventsSubject.onNext(.cwdChanged)
}
func colorSchemeChanged(_ value: MessagePackValue) {
final func colorSchemeChanged(_ value: MessagePackValue) {
guard let values = MessagePackUtils.array(from: value, ofSize: 5, conversion: { $0.intValue }) else { return }
let theme = Theme(values)
@ -267,7 +268,7 @@ extension NvimView {
}
}
func defaultColorsChanged(_ value: MessagePackValue) {
final func defaultColorsChanged(_ value: MessagePackValue) {
guard let values = MessagePackUtils.array(
from: value, ofSize: 3, conversion: { $0.intValue }
) else {
@ -294,14 +295,14 @@ extension NvimView {
}
}
func setDirty(with value: MessagePackValue) {
final func setDirty(with value: MessagePackValue) {
guard let dirty = value.boolValue else { return }
bridgeLogger.debug(dirty)
self.eventsSubject.onNext(.setDirtyStatus(dirty))
}
func setAttr(with value: MessagePackValue) {
final func setAttr(with value: MessagePackValue) {
guard let array = value.arrayValue else { return }
guard array.count == 6 else { return }
@ -334,38 +335,38 @@ extension NvimView {
}
}
func updateMenu() {
final func updateMenu() {
bridgeLogger.mark()
}
func busyStart() {
final func busyStart() {
bridgeLogger.mark()
}
func busyStop() {
final func busyStop() {
bridgeLogger.mark()
}
func mouseOn() {
final func mouseOn() {
bridgeLogger.mark()
}
func mouseOff() {
final func mouseOff() {
bridgeLogger.mark()
}
func visualBell() {
final func visualBell() {
bridgeLogger.mark()
}
func suspend() {
final func suspend() {
bridgeLogger.mark()
}
}
extension NvimView {
func markForRender(cellPosition position: Position) {
final func markForRender(cellPosition position: Position) {
self.markForRender(position: position)
if self.grid.isCellEmpty(position) {
@ -377,26 +378,26 @@ extension NvimView {
}
}
func markForRender(position: Position) {
final func markForRender(position: Position) {
self.markForRender(row: position.row, column: position.column)
}
func markForRender(screenCursor position: Position) {
final func markForRender(screenCursor position: Position) {
self.markForRender(position: position)
if self.grid.isNextCellEmpty(position) {
self.markForRender(position: self.grid.nextCellPosition(position))
}
}
func markForRenderWholeView() {
final func markForRenderWholeView() {
self.needsDisplay = true
}
func markForRender(region: Region) {
final func markForRender(region: Region) {
self.setNeedsDisplay(self.rect(for: region))
}
func markForRender(row: Int, column: Int) {
final func markForRender(row: Int, column: Int) {
self.setNeedsDisplay(self.rect(forRow: row, column: column))
}
}

View File

@ -380,4 +380,6 @@ public class NvimView: NSView,
// MARK: - Private
private var _linespacing = NvimView.defaultLinespacing
let typesetter = Typesetter()
var baselineOffset = CGFloat(0)
}

View File

@ -5,11 +5,7 @@
import Cocoa
class AttributesRunDrawer {
private let logger = LogContext.stdoutLogger(
as: String(reflecting: AttributesRunDrawer.self)
)
final class AttributesRunDrawer {
var baseFont: NSFont {
didSet {
@ -33,6 +29,38 @@ class AttributesRunDrawer {
self.updateFontMetrics()
}
func draw(
_ run: AttributesRun,
fontGlyphRuns: [FontGlyphRun],
defaultAttributes: CellAttributes,
`in` context: CGContext
) {
context.saveGState()
defer { context.restoreGState() }
self.draw(
backgroundFor: run,
with: defaultAttributes,
in: context
)
context.setFillColor(
ColorUtils.cgColorIgnoringAlpha(run.attrs.effectiveForeground)
)
fontGlyphRuns.forEach { run in
CTFontDrawGlyphs(
run.font,
run.glyphs,
run.positions,
run.glyphs.count,
context
)
}
// TODO: GH-666: Draw underline/curl
}
func draw(
_ run: AttributesRun,
with defaultAttributes: CellAttributes,
@ -58,7 +86,6 @@ class AttributesRunDrawer {
nvimUtf16Cells: run.cells.map { Array($0.string.utf16) },
startColumn: run.cells.startIndex,
offset: CGPoint(x: xOffset, y: run.location.y + self.baselineOffset),
foreground: run.attrs.effectiveForeground,
font: font,
cellWidth: self.cellSize.width
)
@ -68,7 +95,6 @@ class AttributesRunDrawer {
nvimCells: run.cells.map { $0.string },
startColumn: run.cells.startIndex,
offset: CGPoint(x: xOffset, y: run.location.y + self.baselineOffset),
foreground: run.attrs.effectiveForeground,
font: font,
cellWidth: self.cellSize.width
)

View File

@ -13,6 +13,25 @@ extension Array where Element: Hashable {
}
}
extension Array {
/// Does not retain the order of elements.
func parallelMap<T>(_ transform: @escaping (Element) -> T) -> [T] {
var result = Array<T>()
result.reserveCapacity(self.count)
var lock = OS_SPINLOCK_INIT
DispatchQueue.concurrentPerform(iterations: self.count) { i in
let mapped = transform(self[i])
OSSpinLockLock(&lock)
result.append(mapped)
OSSpinLockUnlock(&lock)
}
return result
}
}
extension ArraySlice {
func groupedRanges<T: Equatable>(with marker: (Int, Element, ArraySlice<Element>) -> T) -> [CountableClosedRange<Int>] {

View File

@ -5,13 +5,12 @@
import Cocoa
class Typesetter {
final class Typesetter {
final func fontGlyphRunsWithLigatures(
func fontGlyphRunsWithLigatures(
nvimUtf16Cells: [[Unicode.UTF16.CodeUnit]],
startColumn: Int,
offset: CGPoint,
foreground: Int,
font: NSFont,
cellWidth: CGFloat
) -> [FontGlyphRun] {
@ -21,13 +20,9 @@ class Typesetter {
from: nvimUtf16Cells,
utf16CharsCount: utf16Chars.count
)
let ctRuns = self.ctRuns(
from: utf16Chars, font: font, foreground: foreground
)
let ctRuns = self.ctRuns(from: utf16Chars, font: font)
var result = Array<FontGlyphRun>()
result.reserveCapacity(ctRuns.count)
for run in ctRuns {
let result = ctRuns.map { run -> FontGlyphRun in
let glyphCount = CTRunGetGlyphCount(run)
var glyphs = Array(repeating: CGGlyph(), count: glyphCount)
@ -53,19 +48,16 @@ class Typesetter {
positions[i].y += offset.y
}
guard
let attrs = CTRunGetAttributes(run) as? [NSAttributedStringKey: Any],
let font = attrs[NSAttributedStringKey.font] as? NSFont
else {
// FIXME: GH-666: Return the default font
preconditionFailure("Could not get font from CTRun!")
}
let attrs = CTRunGetAttributes(run)
let font = Unmanaged<NSFont>.fromOpaque(
CFDictionaryGetValue(
attrs, Unmanaged.passUnretained(kCTFontAttributeName).toOpaque()
)
).takeUnretainedValue()
let fontGlyphRun = FontGlyphRun(
return FontGlyphRun(
font: font, glyphs: glyphs, positions: positions
)
result.append(fontGlyphRun)
}
return result
@ -80,36 +72,45 @@ class Typesetter {
var i = 0
repeat {
if nvimUtf16Cells[cellIndex].isEmpty {
cellIndex += 1
cellIndex = cellIndex &+ 1
continue
}
for _ in (0..<nvimUtf16Cells[cellIndex].count) {
cellIndices[i] = cellIndex
i += 1
i = i &+ 1
}
cellIndex += 1
cellIndex = cellIndex &+ 1
} while cellIndex < nvimUtf16Cells.count
return cellIndices
}
private func utf16Chars(from nvimUtf16Cells: [[Unicode.UTF16.CodeUnit]]) -> Array<UInt16> {
var utf16Chars = Array<Unicode.UTF16.CodeUnit>()
utf16Chars.reserveCapacity(Int(Double(nvimUtf16Cells.count) * 1.5))
private func utf16Chars(
from nvimUtf16Cells: [[Unicode.UTF16.CodeUnit]]
) -> Array<UInt16> {
// Using reduce seems to be slower than the following:
var count = 0
for i in 0..<nvimUtf16Cells.count {
utf16Chars.append(contentsOf: nvimUtf16Cells[i])
count = count &+ nvimUtf16Cells[i].count
}
return utf16Chars
// Using append(contentsOf:) seems to be slower than the following:
var result = Array(repeating: Unicode.UTF16.CodeUnit(), count: count)
for i in 0..<nvimUtf16Cells.count {
let cell = nvimUtf16Cells[i]
for j in 0..<cell.count {
result[i + j] = cell[j]
}
}
final func fontGlyphRunsWithoutLigatures(
return result
}
func fontGlyphRunsWithoutLigatures(
nvimCells: [String],
startColumn: Int,
offset: CGPoint,
foreground: Int,
font: NSFont,
cellWidth: CGFloat
) -> [FontGlyphRun] {
@ -124,7 +125,6 @@ class Typesetter {
nvimUtf16Cells: run.nvimUtf16Cells,
startColumn: startColumn + run.startColumn,
offset: offset,
foreground: foreground,
font: font,
cellWidth: cellWidth
)
@ -133,9 +133,11 @@ class Typesetter {
let unichars = self.utf16Chars(from: run.nvimUtf16Cells)
var glyphs = Array<CGGlyph>(repeating: CGGlyph(), count: unichars.count)
let gotAllGlyphs = CTFontGetGlyphsForCharacters(
font, unichars, &glyphs, unichars.count
let gotAllGlyphs = unichars.withUnsafeBufferPointer { pointer in
CTFontGetGlyphsForCharacters(
font, pointer.baseAddress!, &glyphs, unichars.count
)
}
if gotAllGlyphs {
let startColumnForPositions = startColumn + run.startColumn
let endColumn = startColumnForPositions + glyphs.count
@ -160,7 +162,6 @@ class Typesetter {
nvimUtf16Cells: nvimUtf16Cells,
startColumn: startColumn + range.lowerBound,
offset: offset,
foreground: foreground,
font: font,
cellWidth: cellWidth
)
@ -187,21 +188,22 @@ class Typesetter {
}
private func ctRuns(
from utf16Chars: [Unicode.UTF16.CodeUnit],
font: NSFont,
foreground: Int
from utf16Chars: Array<Unicode.UTF16.CodeUnit>,
font: NSFont
) -> [CTRun] {
let attrStr = NSAttributedString(
string: String(utf16CodeUnits: utf16Chars, count: utf16Chars.count),
attributes: [
.font: font,
.foregroundColor: ColorUtils.cgColorIgnoringAlpha(foreground),
.ligature: 1,
.ligature: 1
]
)
let ctLine = CTLineCreateWithAttributedString(attrStr)
guard let ctRuns = CTLineGetGlyphRuns(ctLine) as? [CTRun] else { return [] }
let ctLine = CTLineCreateWithAttributedString(attrStr)
guard let ctRuns = CTLineGetGlyphRuns(ctLine) as? [CTRun] else {
return []
}
return ctRuns
}
@ -220,7 +222,7 @@ class Typesetter {
}
let nvimUtf16Cells = nvimCells.map { Array($0.utf16) }
let utf16Chars = nvimUtf16Cells.flatMap { $0 }
let utf16Chars = self.utf16Chars(from: nvimUtf16Cells)
let hasMoreThanTwoCells = nvimUtf16Cells.count >= 2
let firstCharHasSingleUnichar = nvimUtf16Cells[0].count == 1

View File

@ -11,7 +11,7 @@ struct UCell {
var attrId: Int
}
class UGrid {
final class UGrid {
private(set) var size = Size.zero
private(set) var posision = Position.zero
@ -102,6 +102,10 @@ class UGrid {
))
}
func clear() {
// self.clear(region: self.region)
}
func clear(region: Region) {
// FIXME: sometimes clearRegion gets called without first resizing the Grid.
// Should we handle this?

View File

@ -16,7 +16,6 @@ class TypesetterWithoutLigaturesTest: XCTestCase {
nvimCells: emojiMarked(["a", "b", "c"]),
startColumn: 10,
offset: offset,
foreground: 0,
font: defaultFont,
cellWidth: defaultWidth
)
@ -40,7 +39,6 @@ class TypesetterWithoutLigaturesTest: XCTestCase {
nvimCells: emojiMarked(["ü", "î", "ñ"]),
startColumn: 20,
offset: offset,
foreground: 0,
font: defaultFont,
cellWidth: defaultWidth
)
@ -66,7 +64,6 @@ class TypesetterWithoutLigaturesTest: XCTestCase {
),
startColumn: 10,
offset: offset,
foreground: 0,
font: defaultFont,
cellWidth: defaultWidth
)
@ -127,7 +124,6 @@ class TypesetterWithoutLigaturesTest: XCTestCase {
nvimCells: asciiMarked(["a", "b", "\u{1F600}", "", "\u{1F377}", ""]),
startColumn: 1,
offset: offset,
foreground: 0,
font: defaultFont,
cellWidth: defaultWidth
)
@ -161,7 +157,6 @@ class TypesetterWithoutLigaturesTest: XCTestCase {
nvimCells: asciiMarked(["a", "\u{1F476}", "", "\u{1F3FD}", ""]),
startColumn: 1,
offset: offset,
foreground: 0,
font: defaultFont,
cellWidth: defaultWidth
)
@ -193,7 +188,6 @@ class TypesetterWithoutLigaturesTest: XCTestCase {
nvimCells: asciiMarked(["a", "b", "", "", "", "", "", ""]),
startColumn: 1,
offset: offset,
foreground: 0,
font: defaultFont,
cellWidth: defaultWidth
)
@ -227,7 +221,6 @@ class TypesetterWithoutLigaturesTest: XCTestCase {
nvimCells: asciiMarked(["a", "b", "", "", "", "", "", ""]),
startColumn: 1,
offset: offset,
foreground: 0,
font: defaultFont,
cellWidth: defaultWidth
)
@ -261,7 +254,6 @@ class TypesetterWithoutLigaturesTest: XCTestCase {
nvimCells: emojiMarked(["a", "\u{10437}", "\u{1F14}"]),
startColumn: 1,
offset: offset,
foreground: 0,
font: defaultFont,
cellWidth: defaultWidth
)
@ -301,7 +293,6 @@ class TypesetterWithoutLigaturesTest: XCTestCase {
nvimCells: emojiMarked(["a", "-", "-", ">", "a"]),
startColumn: 1,
offset: offset,
foreground: 0,
font: fira,
cellWidth: firaWidth
)
@ -340,7 +331,6 @@ class TypesetterWithLigaturesTest: XCTestCase {
)),
startColumn: 1,
offset: offset,
foreground: 0,
font: defaultFont,
cellWidth: defaultWidth
)
@ -364,7 +354,6 @@ class TypesetterWithLigaturesTest: XCTestCase {
nvimUtf16Cells: utf16Chars(emojiMarked(["ü", "î", "ñ"])),
startColumn: 10,
offset: offset,
foreground: 0,
font: defaultFont,
cellWidth: defaultWidth
)
@ -392,7 +381,6 @@ class TypesetterWithLigaturesTest: XCTestCase {
),
startColumn: 1,
offset: offset,
foreground: 0,
font: defaultFont,
cellWidth: defaultWidth
)
@ -437,7 +425,6 @@ class TypesetterWithLigaturesTest: XCTestCase {
),
startColumn: 0,
offset: offset,
foreground: 0,
font: defaultFont,
cellWidth: defaultWidth
)
@ -465,7 +452,6 @@ class TypesetterWithLigaturesTest: XCTestCase {
),
startColumn: 0,
offset: offset,
foreground: 0,
font: defaultFont,
cellWidth: defaultWidth
)
@ -493,7 +479,6 @@ class TypesetterWithLigaturesTest: XCTestCase {
)),
startColumn: 1,
offset: offset,
foreground: 0,
font: defaultFont,
cellWidth: defaultWidth
)
@ -518,7 +503,6 @@ class TypesetterWithLigaturesTest: XCTestCase {
),
startColumn: 1,
offset: offset,
foreground: 0,
font: defaultFont,
cellWidth: defaultWidth
)
@ -544,7 +528,6 @@ class TypesetterWithLigaturesTest: XCTestCase {
),
startColumn: 1,
offset: offset,
foreground: 0,
font: defaultFont,
cellWidth: defaultWidth
)
@ -570,7 +553,6 @@ class TypesetterWithLigaturesTest: XCTestCase {
),
startColumn: 0,
offset: offset,
foreground: 0,
font: defaultFont,
cellWidth: defaultWidth
)
@ -602,7 +584,6 @@ class TypesetterWithLigaturesTest: XCTestCase {
),
startColumn: 0,
offset: offset,
foreground: 0,
font: fira,
cellWidth: firaWidth
)