Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 7 additions & 5 deletions native/swift/Sources/Deview/Frame.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ import Foundation
///
/// Copied rather than read in place, because the pointers in `DeviewScreen` are only valid for the
/// duration of the call that carried them, and the view redraws whenever AppKit says so.
struct Frame {
///
/// Equatable so a frame that changes nothing is not drawn: see `Runtime.present`.
struct Frame: Equatable {
var title = ""
var subtitle = ""
var status = ""
Expand All @@ -24,7 +26,7 @@ struct Frame {
var menu: [String] = []
var menuRow: Int32 = -1

struct Row {
struct Row: Equatable {
var kind: Int32 = 0
var lineNumber: Int32 = -1
var text = ""
Expand All @@ -37,7 +39,7 @@ struct Frame {
var selectLength: Int32 = 0
}

struct Pane {
struct Pane: Equatable {
var header = ""
var rows: [Row] = []

Expand All @@ -54,7 +56,7 @@ struct Frame {
var imageHeight: Int32 = 0
}

struct QueueItem {
struct QueueItem: Equatable {
var label = ""
var selected = false
var failed = false
Expand All @@ -64,7 +66,7 @@ struct Frame {
var tooltip = ""
}

struct Button {
struct Button: Equatable {
var label = ""
var enabled = false
}
Expand Down
88 changes: 87 additions & 1 deletion native/swift/Sources/Deview/Renderer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,11 @@ final class Renderer {
var image: CGImage?
var modified: Date
var length: UInt64

/// The picture scaled down to the device pixels it last filled. Drawing a large picture
/// scaled costs a resample of every source pixel, and a window showing one did that on
/// every redraw; this is a copy.
var scaled: CGImage?
}

/// One character cell. Measured from the font that was actually loaded, which is what the ABI
Expand Down Expand Up @@ -402,9 +407,10 @@ final class Renderer {

checker(bounds, in: context)

let device = context.convertToDeviceSpace(bounds).size
context.saveGState()
context.interpolationQuality = .high
context.draw(picture, in: bounds)
context.draw(fitted(pane.imagePath, picture, device: device), in: bounds)
context.restoreGState()

// An outline, so a picture whose edges are the colour of the pane still has visible extent.
Expand Down Expand Up @@ -438,6 +444,86 @@ final class Renderer {
}
}

/// `picture` scaled down to `device` pixels, kept until the size or the picture changes, or
/// `picture` itself where it is not being scaled down: drawing at or above its own size costs
/// little, and drawing the original there leaves what a capture shows exactly as it was.
private func fitted(_ path: String, _ picture: CGImage, device: CGSize) -> CGImage {
let width = Int(abs(device.width).rounded())
let height = Int(abs(device.height).rounded())
guard width > 0,
height > 0,
width < picture.width || height < picture.height
else {
return picture
}

if let scaled = pictures[path]?.scaled,
scaled.width == width,
scaled.height == height {
return scaled
}

guard let space = CGColorSpace(name: CGColorSpace.sRGB),
let bitmap = CGContext(
data: nil,
width: width,
height: height,
bitsPerComponent: 8,
bytesPerRow: 0,
space: space,
bitmapInfo: CGImageAlphaInfo.premultipliedFirst.rawValue | CGBitmapInfo.byteOrder32Little.rawValue)
else {
return picture
}

bitmap.interpolationQuality = .high
bitmap.draw(picture, in: CGRect(x: 0, y: 0, width: width, height: height))
guard let scaled = bitmap.makeImage() else {
return picture
}

pictures[path]?.scaled = scaled
return scaled
}

/// Whether a picture `frame` shows is not the one last drawn: its file was rewritten, has
/// appeared, or has gone. `Runtime.present` redraws on this as well as on a changed frame,
/// since a re-run can rewrite a received image with the same size and dimensions, which is an
/// identical frame.
func picturesChanged(_ frame: Frame) -> Bool {
for pane in [frame.left, frame.right] {
guard !pane.imagePath.isEmpty,
pane.imageWidth > 0,
pane.imageHeight > 0
else {
continue
}

let cached = pictures[pane.imagePath]
let attributes = try? FileManager.default.attributesOfItem(atPath: pane.imagePath)
guard let modified = attributes?[.modificationDate] as? Date,
let length = attributes?[.size] as? UInt64
else {
// Gone: worth a redraw only to take away what was drawn
if cached != nil {
return true
}

continue
}

guard let cached else {
return true
}

if cached.modified != modified || cached.length != length {
return true
}
}

return false
}

private func picture(_ path: String) -> CGImage? {
guard let attributes = try? FileManager.default.attributesOfItem(atPath: path),
let modified = attributes[.modificationDate] as? Date,
Expand Down
13 changes: 11 additions & 2 deletions native/swift/Sources/Deview/Runtime.swift
Original file line number Diff line number Diff line change
Expand Up @@ -139,8 +139,17 @@ final class Runtime {
return
}

view.model = frame
view.needsDisplay = true
// Drawn only when there is something new to draw. The managed loop presents at 60 fps
// whether or not anything changed, and redrawing the whole window every time kept a core
// busy for a viewer nobody was touching. What is on screen is the frame plus the pictures
// it names, whose files can be rewritten under an unchanged frame. Anything else - a
// resize, a splitter drag, a move to a display of another scale - is invalidated by
// AppKit or by the view as it happens.
if view.model != frame || renderer?.picturesChanged(frame) == true {
view.model = frame
view.needsDisplay = true
}

view.displayIfNeeded()
// After drawing, because both read where the last frame put the queue rows.
view.refreshToolTips()
Expand Down
7 changes: 7 additions & 0 deletions native/swift/Sources/Deview/ViewerView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,13 @@ final class ViewerView: NSView, NSViewToolTipOwner {

override var acceptsFirstResponder: Bool { true }

/// A move to a display of another scale changes the device pixels a picture fills, and so
/// the scaled copy the renderer keeps of it, under a frame that has not changed.
override func viewDidChangeBackingProperties() {
super.viewDidChangeBackingProperties()
needsDisplay = true
}

override func draw(_ dirtyRect: NSRect) {
guard let context = NSGraphicsContext.current?.cgContext else {
return
Expand Down
Binary file not shown.
Binary file not shown.
5 changes: 0 additions & 5 deletions todo.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,3 @@ Native
- [ ] **macOS App Nap can stall the loop while the window is covered** (cannot verify here)
- Nothing opts out (no `beginActivity`, `NSAppSleepDisabled` or power assertion), the only wait is `nextEvent(until: now + 1/60)` (`Runtime.swift:248-253`), and a Focus queued by an arriving patch waits for the next managed frame.
- Check on a Mac: cover the viewer for a minute, confirm Activity Monitor shows App Nap, then time how long a failing inline test takes to bring it forward against an uncovered window.


## Perf

- [ ] macOS repaints the whole window every frame (`native/swift/Sources/Deview/Runtime.swift:139-140`). Redraw only when the frame, bounds or a picture stamp change, and cache scaled pictures.
Loading