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
85 changes: 85 additions & 0 deletions native/macos-host/Sources/RsnapHostBridge/HostFFI.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2470,6 +2470,91 @@ public final class RsnapLiveSampler: @unchecked Sendable {
)
}

public func nextRegionFrame(
monitor: MonitorSnapshot,
pixelRect: CGRect,
afterFrameSequence: UInt64,
waitForFresh: Bool
) throws -> RGBARegionFrameSnapshot? {
stateLock.lock()
defer { stateLock.unlock() }

let encodedPixelRect = try Self.encode(
pixelRect: pixelRect,
context: "encoding live RGBA pixel region")
let encodedMonitor = RsnapMonitorRect(
id: monitor.id,
origin: RsnapPoint(
x: Int32(monitor.frame.origin.x.rounded()),
y: Int32(monitor.frame.origin.y.rounded())
),
width: UInt32(max(monitor.frame.width.rounded(), 0)),
height: UInt32(max(monitor.frame.height.rounded(), 0)),
scale_factor_x1000: monitor.scaleFactorX1000
)
var frameSequence: UInt64 = 0
var frameAgeMicroseconds: UInt64 = 0
var ownedRegion = RsnapOwnedRgbaRegion()
let status = rsnap_live_sampler_take_next_region_rgba_pixels_after_seq(
handle,
encodedMonitor,
encodedPixelRect,
afterFrameSequence,
UInt8(waitForFresh ? 1 : 0),
&frameSequence,
&frameAgeMicroseconds,
&ownedRegion
)
let code = rsnap_status_code(status)
if code == 3 {
return nil
}
if code != 0 {
throw HostBridgeError.ffiStatus(
context: "taking next live RGBA pixel region frame",
code: code
)
}
guard let region = rsnapOwnedRgbaSnapshot(from: ownedRegion) else {
return nil
}
return RGBARegionFrameSnapshot(
frameSequence: frameSequence,
frameAgeMicroseconds: frameAgeMicroseconds,
region: region
)
}

private static func encode(pixelRect: CGRect, context: String) throws -> RsnapPixelRect {
let x = pixelRect.origin.x.rounded()
let y = pixelRect.origin.y.rounded()
let width = pixelRect.width.rounded()
let height = pixelRect.height.rounded()
let maxValue = CGFloat(UInt32.max)

guard
x >= 0,
y >= 0,
width > 0,
height > 0,
x <= maxValue,
y <= maxValue,
width <= maxValue,
height <= maxValue
else {
throw HostBridgeError.ffiStatus(
context: context,
code: RSNAP_STATUS_INVALID_INPUT.rawValue)
}

return RsnapPixelRect(
x: UInt32(x),
y: UInt32(y),
width: UInt32(width),
height: UInt32(height)
)
}

/// Returns the live sampler's cache-only full-monitor snapshot.
///
/// This API does not expose the original frame capture time or stream sequence. Do not use it
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -393,6 +393,20 @@ final class CaptureOverlayController {
)
}

func nextRegionFrame(
in rect: CGRect,
pixelRect: CGRect,
afterFrameSequence: UInt64,
waitForFresh: Bool
) -> RGBARegionFrameSnapshot? {
liveFrameStream.nextRegionFrame(
in: rect,
pixelRect: pixelRect,
afterFrameSequence: afterFrameSequence,
waitForFresh: waitForFresh
)
}

func updateLivePreviewDemand(
point: CGPoint?,
settings: NativeHostSettings,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,13 @@ private func scrollCaptureViewportPointCandidates(

private let nativeScrollCaptureMinimumNonzeroWheelMotionHintRows = 12.0

private struct NativeScrollCaptureGeometry {
let baseImage: CGImage
let baseSnapshot: RGBARegionSnapshot
let pixelRect: CGRect
let samplingRect: CGRect
}

extension CaptureSessionController {
private static let scrollCaptureForwardedEventMarker: Int64 = 0x5253_4E41_5053_4352
private static let scrollCapturePreciseWheelDeltaLimit = 72.0
Expand Down Expand Up @@ -496,18 +503,14 @@ extension CaptureSessionController {
return
}

ensureFrozenBaseImageFromDisplayIfNeeded(for: selection)
let frozenBaseImage =
chromeState.frozenBaseImage ?? frozenBaseImageFromDisplay(for: selection)
guard let frozenBaseImage,
let frozenBaseSnapshot = NativeHostImageBridge.rgbaSnapshot(from: frozenBaseImage)
guard let geometry = nativeScrollCaptureGeometry(for: selection)
else {
try setHostStatusMessage("Scroll capture could not read the selected region.")
refreshOverlay()
return
}
let baseImage = frozenBaseImage
let baseSnapshot = frozenBaseSnapshot
let baseImage = geometry.baseImage
let baseSnapshot = geometry.baseSnapshot
let baseSource = "frozen_display_region"
debugDumpNativeScrollCaptureSnapshot(baseSnapshot, name: "base-\(baseSource)")
let stitcher = try RsnapScrollCaptureSession(
Expand All @@ -517,8 +520,11 @@ extension CaptureSessionController {
var initialState = NativeScrollCaptureState(
stitcher: stitcher,
viewportRect: selection,
viewportPixelRect: geometry.pixelRect,
viewportSamplingRect: geometry.samplingRect,
captureSource: captureSource,
viewportPixelsPerPointY: Double(baseSnapshot.height) / max(Double(selection.height), 1)
viewportPixelsPerPointY: Double(geometry.pixelRect.height)
/ max(Double(selection.height), 1)
)
initialState.sampleUntilUptime =
ProcessInfo.processInfo.systemUptime + Self.scrollCaptureInitialSampleWindow
Expand All @@ -535,7 +541,7 @@ extension CaptureSessionController {
"capture.scroll_capture_started",
captureID: currentCaptureTelemetryID,
detail:
"width=\(baseSnapshot.width),height=\(baseSnapshot.height),x=\(Int(selection.minX.rounded())),y=\(Int(selection.minY.rounded())),mode=manual_universal,baseSource=\(baseSource),liveBaseMatched=false"
"width=\(baseSnapshot.width),height=\(baseSnapshot.height),x=\(Int(selection.minX.rounded())),y=\(Int(selection.minY.rounded())),pixelX=\(Int(geometry.pixelRect.minX.rounded())),pixelY=\(Int(geometry.pixelRect.minY.rounded())),pixelWidth=\(Int(geometry.pixelRect.width.rounded())),pixelHeight=\(Int(geometry.pixelRect.height.rounded())),samplingX=\(Int(geometry.samplingRect.minX.rounded())),samplingY=\(Int(geometry.samplingRect.minY.rounded())),samplingWidth=\(Int(geometry.samplingRect.width.rounded())),samplingHeight=\(Int(geometry.samplingRect.height.rounded())),mode=manual_universal,baseSource=\(baseSource),liveBaseMatched=false"
)
NativeHostTelemetry.captureEvent(
"capture.scroll_capture_mode",
Expand All @@ -561,6 +567,57 @@ extension CaptureSessionController {
scheduleNativeScrollCaptureToolbarBackdropRefresh()
}

private func nativeScrollCaptureGeometry(
for selection: CGRect
) -> NativeScrollCaptureGeometry? {
guard
let displayFrame = chromeState.frozenDisplayFrame,
let displayImage = chromeState.frozenDisplayImage,
let pixelRect = try? RsnapExportEncoder.frozenDisplayCropRect(
imageWidth: displayImage.width,
imageHeight: displayImage.height,
displayFrame: displayFrame,
selection: selection
),
let baseImage = displayImage.cropping(to: pixelRect),
let baseSnapshot = NativeHostImageBridge.rgbaSnapshot(from: baseImage)
else {
return nil
}
let samplingRect = Self.nativeScrollCaptureSamplingRect(
pixelRect: pixelRect,
displayFrame: displayFrame,
displayImageSize: CGSize(
width: CGFloat(displayImage.width),
height: CGFloat(displayImage.height)
)
)

return NativeScrollCaptureGeometry(
baseImage: baseImage,
baseSnapshot: baseSnapshot,
pixelRect: pixelRect,
samplingRect: samplingRect
)
}

private static func nativeScrollCaptureSamplingRect(
pixelRect: CGRect,
displayFrame: CGRect,
displayImageSize: CGSize
) -> CGRect {
let pointsPerPixelX = displayFrame.width / max(displayImageSize.width, 1)
let pointsPerPixelY = displayFrame.height / max(displayImageSize.height, 1)
let height = pixelRect.height * pointsPerPixelY
let maxY = displayFrame.maxY - pixelRect.minY * pointsPerPixelY
return CGRect(
x: displayFrame.minX + pixelRect.minX * pointsPerPixelX,
y: maxY - height,
width: pixelRect.width * pointsPerPixelX,
height: height
)
}

private func configureNativeScrollCaptureChrome(
baseImage: CGImage,
baseSnapshot: RGBARegionSnapshot,
Expand Down Expand Up @@ -625,6 +682,7 @@ extension CaptureSessionController {

let sampledFrames = nativeScrollCaptureSampleFrames(
in: state.viewportRect,
pixelRect: state.viewportPixelRect,
afterFrameSequence: state.lastStreamFrameSequence,
maximumFrameAgeMicroseconds: nativeScrollCaptureMaximumStreamFrameAge(state: state)
)
Expand All @@ -633,7 +691,8 @@ extension CaptureSessionController {
&& nativeScrollCaptureFallbackReadyForInput(state: state)
&& nativeScrollCaptureFallbackAllowed(at: sampleUptime)
? NativeScrollCaptureFallbackRequest(
rect: state.viewportRect,
rect: state.viewportSamplingRect,
pixelRect: state.viewportPixelRect,
source: state.captureSource,
frameSequence: state.lastStreamFrameSequence &+ 1
) : nil
Expand Down Expand Up @@ -687,7 +746,11 @@ extension CaptureSessionController {
in: fallbackRequest.rect,
source: fallbackRequest.source
),
let snapshot = NativeHostImageBridge.rgbaSnapshot(from: image)
let snapshot = NativeHostImageBridge.rgbaSnapshot(from: image),
Self.nativeScrollCaptureFrameMatchesPixelRect(
snapshot,
pixelRect: fallbackRequest.pixelRect
)
{
writeNativeScrollCaptureDebugDump(
snapshot,
Expand Down Expand Up @@ -884,6 +947,7 @@ extension CaptureSessionController {

private func nativeScrollCaptureSampleFrames(
in rect: CGRect,
pixelRect: CGRect,
afterFrameSequence: UInt64,
maximumFrameAgeMicroseconds: UInt64?
) -> [NativeScrollCaptureSampleFrame] {
Expand All @@ -894,6 +958,7 @@ extension CaptureSessionController {
guard
let frame = overlayController?.nextRegionFrame(
in: rect,
pixelRect: pixelRect,
afterFrameSequence: nextAfterFrameSequence,
waitForFresh: false
)
Expand All @@ -906,6 +971,18 @@ extension CaptureSessionController {
nextAfterFrameSequence = frame.frameSequence
continue
}
guard Self.nativeScrollCaptureFrameMatchesPixelRect(frame.region, pixelRect: pixelRect)
else {
NativeHostTelemetry.captureWarning(
"capture.scroll_observe_failed",
captureID: currentCaptureTelemetryID,
stage: "sample_frame",
error:
"live stream returned \(frame.region.width)x\(frame.region.height), expected \(Int(pixelRect.width.rounded()))x\(Int(pixelRect.height.rounded()))"
)
nextAfterFrameSequence = frame.frameSequence
continue
}
frames.append(
NativeScrollCaptureSampleFrame(
region: frame.region,
Expand All @@ -918,6 +995,14 @@ extension CaptureSessionController {
return frames
}

nonisolated private static func nativeScrollCaptureFrameMatchesPixelRect(
_ snapshot: RGBARegionSnapshot,
pixelRect: CGRect
) -> Bool {
snapshot.width == Int(pixelRect.width.rounded())
&& snapshot.height == Int(pixelRect.height.rounded())
}

private func nativeScrollCaptureMaximumStreamFrameAge(
state: NativeScrollCaptureState
) -> UInt64? {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -549,6 +549,8 @@ struct CaptureChromeState {
struct NativeScrollCaptureState {
let stitcher: RsnapScrollCaptureSession
let viewportRect: CGRect
let viewportPixelRect: CGRect
let viewportSamplingRect: CGRect
let captureSource: CaptureSessionController.FrozenCaptureJobSource
let viewportPixelsPerPointY: Double
var sampleLoopScheduled = false
Expand Down
24 changes: 24 additions & 0 deletions native/macos-host/Sources/RsnapNativeHostKit/LiveFrameStream.swift
Original file line number Diff line number Diff line change
Expand Up @@ -273,6 +273,30 @@ final class LiveFrameStreamBroker {
)
}

func nextRegionFrame(
in rect: CGRect,
pixelRect: CGRect,
afterFrameSequence: UInt64,
waitForFresh: Bool
) -> RGBARegionFrameSnapshot? {
guard let monitor = monitor(containing: CGPoint(x: rect.midX, y: rect.midY)) else {
return nil
}
stateLock.lock()
let sampler = self.sampler
let encodedMonitor = samplerMonitorSnapshot(for: monitor)
stateLock.unlock()
guard let sampler else {
return nil
}
return try? sampler.nextRegionFrame(
monitor: encodedMonitor,
pixelRect: pixelRect,
afterFrameSequence: afterFrameSequence,
waitForFresh: waitForFresh
)
}

func prime(at point: CGPoint?) {
guard let point, let monitor = monitor(containing: point) else {
return
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ struct NativeScrollCaptureSampleFrame: Sendable {

struct NativeScrollCaptureFallbackRequest: Sendable {
let rect: CGRect
let pixelRect: CGRect
let source: CaptureSessionController.FrozenCaptureJobSource
let frameSequence: UInt64
}
Expand Down
12 changes: 11 additions & 1 deletion packages/rsnap-host-ffi/include/rsnap_host_ffi.h
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
extern "C" {
#endif

#define RSNAP_HOST_FFI_ABI_VERSION 34u
#define RSNAP_HOST_FFI_ABI_VERSION 35u
#define RSNAP_TOOLBAR_ITEM_CAPACITY 16u
#define RSNAP_STATUS_MESSAGE_CAPACITY 256u
#define RSNAP_LIVE_SAMPLE_PATCH_CAPACITY 4096u
Expand Down Expand Up @@ -724,6 +724,16 @@ enum RsnapStatus rsnap_live_sampler_take_next_region_rgba_after_seq(
uint64_t *out_frame_age_micros,
struct RsnapOwnedRgbaRegion *out_region
);
enum RsnapStatus rsnap_live_sampler_take_next_region_rgba_pixels_after_seq(
RsnapLiveSamplerHandle *handle,
struct RsnapMonitorRect monitor,
struct RsnapPixelRect rect,
uint64_t after_frame_seq,
uint8_t wait_for_fresh,
uint64_t *out_frame_seq,
uint64_t *out_frame_age_micros,
struct RsnapOwnedRgbaRegion *out_region
);
enum RsnapStatus rsnap_live_sampler_peek_latest_monitor_rgba(
RsnapLiveSamplerHandle *handle,
struct RsnapMonitorRect monitor,
Expand Down
Loading