diff --git a/screendump/FrameUpdater.h b/screendump/FrameUpdater.h index 1da8323..63543f8 100644 --- a/screendump/FrameUpdater.h +++ b/screendump/FrameUpdater.h @@ -11,6 +11,8 @@ staticBuffer:(IOSurfaceRef)staticBuffer width:(size_t)width height:(size_t)height + nativeWidth:(size_t)nativeWidth + nativeHeight:(size_t)nativeHeight useCADisplayLink:(BOOL)useCADisplayLink renderServerSurface:(IOSurfaceRef)renderServerSurface captureModeBlock:(CaptureMode(^)(void))captureModeBlock; diff --git a/screendump/FrameUpdater.m b/screendump/FrameUpdater.m index 9dbfe75..2063dbc 100644 --- a/screendump/FrameUpdater.m +++ b/screendump/FrameUpdater.m @@ -15,10 +15,12 @@ IOSurfaceRef _staticBuffer; size_t _width; size_t _height; + size_t _nativeWidth; + size_t _nativeHeight; BOOL _useCADisplayLink; // Dual capture mode support - IOSurfaceRef _renderServerSurface; + IOSurfaceRef _renderServerSurface; // Separate surface for CARenderServer (avoids framebuffer locking) CaptureMode(^_captureModeBlock)(void); } @@ -28,11 +30,14 @@ staticBuffer:(IOSurfaceRef)staticBuffer width:(size_t)width height:(size_t)height + nativeWidth:(size_t)nativeWidth + nativeHeight:(size_t)nativeHeight useCADisplayLink:(BOOL)useCADisplayLink renderServerSurface:(IOSurfaceRef)renderServerSurface captureModeBlock:(CaptureMode(^)(void))captureModeBlock { if ((self = [super init])) { _q = [[NSOperationQueue alloc] init]; + _q.maxConcurrentOperationCount = 1; // Serialize to prevent frame queue buildup _updatingFrames = NO; _lastUpdatedSeed = 0; _updateFrameTimer = nil; @@ -43,6 +48,8 @@ _staticBuffer = staticBuffer; _width = width; _height = height; + _nativeWidth = nativeWidth; + _nativeHeight = nativeHeight; _useCADisplayLink = useCADisplayLink; _renderServerSurface = renderServerSurface; @@ -57,6 +64,12 @@ [self stopFrameLoop]; return; } + + // Skip if queue is busy to prevent frame buildup and reduce lock contention + if (_q.operationCount > 0) { + return; + } + bool updateFrame = true; if (!_useCADisplayLink) { bool updateFrame = false; @@ -72,14 +85,22 @@ [_q addOperationWithBlock: ^{ // Get current capture mode CaptureMode mode = _captureModeBlock ? _captureModeBlock() : CaptureModeCARenderServer; + BOOL needsScaling = (_width != _nativeWidth || _height != _nativeHeight); if (mode == CaptureModeIOMobileFramebuffer) { // Legacy mode: Use IOMobileFramebuffer surface transfer (shows passwords) IOSurfaceAcceleratorTransferSurface(_accelerator, _screenSurface, _staticBuffer, NULL, NULL, NULL, NULL); } else { // Default mode: Use CARenderServer (faster, no secure fields) - CARenderServerRenderDisplay(0, CFSTR("LCD"), _renderServerSurface, 0, 0); - IOSurfaceAcceleratorTransferSurface(_accelerator, _renderServerSurface, _staticBuffer, NULL, NULL, NULL, NULL); + // Uses separate surface to avoid touching the live framebuffer + if (needsScaling && _renderServerSurface) { + // Capture to our own surface at native resolution, then scale + CARenderServerRenderDisplay(0, CFSTR("LCD"), _renderServerSurface, 0, 0); + IOSurfaceAcceleratorTransferSurface(_accelerator, _renderServerSurface, _staticBuffer, NULL, NULL, NULL, NULL); + } else { + // No scaling needed - capture directly to output buffer + CARenderServerRenderDisplay(0, CFSTR("LCD"), _staticBuffer, 0, 0); + } } rfbMarkRectAsModified(_rfbScreenInfo, 0, 0, _width, _height); @@ -104,12 +125,13 @@ if (_useCADisplayLink) { CADisplayLink *displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(_updateFrame)]; - // displayLink.preferredFramesPerSecond = 60; // Adjust as needed + displayLink.preferredFramesPerSecond = 60; // Limit to 60 FPS [displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode]; _updateFrameTimer = (NSTimer *)displayLink; } else { dispatch_async(dispatch_get_main_queue(), ^(void){ - _updateFrameTimer = [NSTimer scheduledTimerWithTimeInterval:1/400 target:self selector:@selector(_updateFrame) userInfo:nil repeats:YES]; + // Reduced from 1/400 (400 Hz) to 1/60 (60 Hz) to reduce overhead + _updateFrameTimer = [NSTimer scheduledTimerWithTimeInterval:1.0/400.0 target:self selector:@selector(_updateFrame) userInfo:nil repeats:YES]; }); } } diff --git a/screendump/IOMobileFramebuffer.h b/screendump/IOMobileFramebuffer.h index 7f048ef..bf3a070 100644 --- a/screendump/IOMobileFramebuffer.h +++ b/screendump/IOMobileFramebuffer.h @@ -26,6 +26,11 @@ typedef io_service_t IOMobileFramebufferService; extern void IOSurfaceFlushProcessorCaches(IOSurfaceRef buffer); extern int IOSurfaceLock(IOSurfaceRef surface, uint32_t options, uint32_t *seed); extern int IOSurfaceUnlock(IOSurfaceRef surface, uint32_t options, uint32_t *seed); + +// IOSurface lock options +#define kIOSurfaceLockReadOnly 0x00000001 +#define kIOSurfaceLockAvoidSync 0x00000002 + extern Boolean IOSurfaceIsInUse(IOSurfaceRef buffer); extern CFMutableDictionaryRef IOServiceMatching(const char *name); extern const mach_port_t kIOMasterPortDefault; diff --git a/screendump/ScreenDumpVNC.m b/screendump/ScreenDumpVNC.m index 9a99fb2..8bf8d0b 100644 --- a/screendump/ScreenDumpVNC.m +++ b/screendump/ScreenDumpVNC.m @@ -32,7 +32,7 @@ // Dual capture mode support CaptureMode _captureMode; NSTimer *_legacyModeTimer; - IOSurfaceRef _renderServerSurface; + IOSurfaceRef _renderServerSurface; // Separate surface for CARenderServer (not the live framebuffer) } +(void)load { @@ -117,62 +117,79 @@ } -(void)_adjustResolutionForAspectRatio { - // Only adjust if custom preferences are set - if (_prefsWidth == 0 || _prefsHeight == 0) { - NSLog(@"[ScreenDumpVNC] No custom dimensions set, using native resolution"); - return; - } - - // Validate custom preferences - if (_prefsWidth <= 0 || _prefsHeight <= 0) { - NSLog(@"[ScreenDumpVNC] ERROR: Invalid custom dimensions (%dx%d), ignoring", - _prefsWidth, _prefsHeight); - _prefsWidth = 0; - _prefsHeight = 0; - return; - } - - // Calculate ideal dimensions maintaining aspect ratio - double idealHeight = (double)_prefsWidth / _displayAspectRatio; - double idealWidth = (double)_prefsHeight * _displayAspectRatio; - size_t adjustedWidth, adjustedHeight; - // Choose option that maximizes pixels while staying within BOTH constraints - if (idealHeight <= (double)_prefsHeight) { - // Use full width, adjust height down + // Case 1: No custom dimensions - use native resolution (still need alignment) + if (_prefsWidth == 0 && _prefsHeight == 0) { + NSLog(@"[ScreenDumpVNC] No custom dimensions set, using native resolution"); + adjustedWidth = _nativeWidth; + adjustedHeight = _nativeHeight; + } + // Case 2: Only width specified - calculate height from aspect ratio + else if (_prefsWidth > 0 && _prefsHeight == 0) { + NSLog(@"[ScreenDumpVNC] Custom width %d specified, calculating height from aspect ratio", _prefsWidth); adjustedWidth = _prefsWidth; - adjustedHeight = (size_t)floor(idealHeight); - } else { - // Use full height, adjust width down - adjustedWidth = (size_t)floor(idealWidth); + adjustedHeight = (size_t)floor((double)_prefsWidth / _displayAspectRatio); + } + // Case 3: Only height specified - calculate width from aspect ratio + else if (_prefsWidth == 0 && _prefsHeight > 0) { + NSLog(@"[ScreenDumpVNC] Custom height %d specified, calculating width from aspect ratio", _prefsHeight); + adjustedWidth = (size_t)floor((double)_prefsHeight * _displayAspectRatio); adjustedHeight = _prefsHeight; } + // Case 4: Both specified - fit within constraints maintaining aspect ratio + else { + // Validate custom preferences + if (_prefsWidth <= 0 || _prefsHeight <= 0) { + NSLog(@"[ScreenDumpVNC] ERROR: Invalid custom dimensions (%dx%d), using native", + _prefsWidth, _prefsHeight); + adjustedWidth = _nativeWidth; + adjustedHeight = _nativeHeight; + } else { + // Calculate ideal dimensions maintaining aspect ratio + double idealHeight = (double)_prefsWidth / _displayAspectRatio; + double idealWidth = (double)_prefsHeight * _displayAspectRatio; + + // Choose option that maximizes pixels while staying within BOTH constraints + if (idealHeight <= (double)_prefsHeight) { + // Use full width, adjust height down + adjustedWidth = _prefsWidth; + adjustedHeight = (size_t)floor(idealHeight); + } else { + // Use full height, adjust width down + adjustedWidth = (size_t)floor(idealWidth); + adjustedHeight = _prefsHeight; + } + } + } // Round width down to nearest multiple of 4 (VNC alignment requirement) - adjustedWidth = (adjustedWidth / 4) * 4; + size_t alignedWidth = (adjustedWidth / 4) * 4; // Recalculate height based on aligned width to maintain exact aspect ratio - if (adjustedWidth > 0) { - adjustedHeight = (size_t)floor((double)adjustedWidth / _displayAspectRatio); + size_t alignedHeight; + if (alignedWidth > 0) { + alignedHeight = (size_t)floor((double)alignedWidth / _displayAspectRatio); + } else { + alignedHeight = adjustedHeight; } // Round height down to nearest multiple of 4 for consistency - adjustedHeight = (adjustedHeight / 4) * 4; + alignedHeight = (alignedHeight / 4) * 4; // Ensure minimum dimensions (4x4 to satisfy alignment requirements) - if (adjustedWidth < 4) adjustedWidth = 4; - if (adjustedHeight < 4) adjustedHeight = 4; + if (alignedWidth < 4) alignedWidth = 4; + if (alignedHeight < 4) alignedHeight = 4; - // Log adjustment if changed - if (adjustedWidth != _prefsWidth || adjustedHeight != _prefsHeight) { - NSLog(@"[ScreenDumpVNC] Adjusted resolution from %dx%d to %zux%zu to maintain %.6f aspect ratio", - _prefsWidth, _prefsHeight, adjustedWidth, adjustedHeight, _displayAspectRatio); + // Log adjustment if dimensions changed due to alignment + if (alignedWidth != adjustedWidth || alignedHeight != adjustedHeight) { + NSLog(@"[ScreenDumpVNC] Aligned resolution from %zux%zu to %zux%zu (VNC requires multiples of 4)", + adjustedWidth, adjustedHeight, alignedWidth, alignedHeight); } // Update preferences with corrected values - _prefsWidth = (int)adjustedWidth; - _prefsHeight = (int)adjustedHeight; + _prefsWidth = (int)alignedWidth; + _prefsHeight = (int)alignedHeight; } -(void)setupScreenInfo { @@ -191,12 +208,12 @@ // Calculate display aspect ratio from native IOSurface dimensions [self _calculateDisplayAspectRatio]; - // Adjust custom preferences to maintain aspect ratio + // Adjust/align dimensions (always sets _prefsWidth/_prefsHeight to valid aligned values) [self _adjustResolutionForAspectRatio]; - // Set final dimensions (preferences are now corrected if custom, or will use native) - _width = _prefsWidth == 0 ? _nativeWidth : _prefsWidth; - _height = _prefsHeight == 0 ? _nativeHeight : _prefsHeight; + // Set final dimensions (preferences now always contain aligned values) + _width = _prefsWidth; + _height = _prefsHeight; _sizeImage = IOSurfaceGetAllocSize(_screenSurface); // TODO: do these change at all? this might have been done for perf reasons @@ -206,31 +223,49 @@ bytesPerPixel = 4; // IOSurfaceGetBytesPerElement(_screenSurface); bitsPerSample = 8; + // Align bytes per row to 16 bytes (required for IOSurfaceAccelerator) + size_t staticBytesPerRow = bytesPerPixel * _width; + size_t alignedStaticBytesPerRow = ((staticBytesPerRow + 15) / 16) * 16; + _staticBuffer = IOSurfaceCreate((CFDictionaryRef) [NSDictionary dictionaryWithObjectsAndKeys: @"PurpleEDRAM", kIOSurfaceMemoryRegion, - // [NSNumber numberWithBool:YES], kIOSurfaceIsGlobal, - [NSNumber numberWithInt:bytesPerPixel*_width], kIOSurfaceBytesPerRow, + [NSNumber numberWithInt:alignedStaticBytesPerRow], kIOSurfaceBytesPerRow, [NSNumber numberWithInt:bytesPerPixel], kIOSurfaceBytesPerElement, [NSNumber numberWithInt:_width], kIOSurfaceWidth, [NSNumber numberWithInt:_height], kIOSurfaceHeight, [NSNumber numberWithInt:'BGRA'], kIOSurfacePixelFormat, - [NSNumber numberWithInt:(_width*_height*bytesPerPixel)], kIOSurfaceAllocSize, + [NSNumber numberWithInt:(alignedStaticBytesPerRow * _height)], kIOSurfaceAllocSize, nil]); - // Create surface for CARenderServer capture (default mode) - _renderServerSurface = IOSurfaceCreate((CFDictionaryRef) [NSDictionary dictionaryWithObjectsAndKeys: - @"PurpleEDRAM", kIOSurfaceMemoryRegion, - [NSNumber numberWithInt:bytesPerPixel*_width], kIOSurfaceBytesPerRow, - [NSNumber numberWithInt:bytesPerPixel], kIOSurfaceBytesPerElement, - [NSNumber numberWithInt:_width], kIOSurfaceWidth, - [NSNumber numberWithInt:_height], kIOSurfaceHeight, - [NSNumber numberWithInt:'BGRA'], kIOSurfacePixelFormat, - [NSNumber numberWithInt:(_width*_height*bytesPerPixel)], kIOSurfaceAllocSize, - nil]); + // Create separate surface for CARenderServer capture (NOT the live framebuffer) + // This avoids touching the framebuffer which causes system lag + BOOL needsScaling = (_width != _nativeWidth || _height != _nativeHeight); + if (needsScaling) { + // Align bytes per row to 16 bytes (required for IOSurfaceAccelerator) + size_t nativeBytesPerRow = bytesPerPixel * _nativeWidth; + size_t alignedNativeBytesPerRow = ((nativeBytesPerRow + 15) / 16) * 16; + + _renderServerSurface = IOSurfaceCreate((CFDictionaryRef) [NSDictionary dictionaryWithObjectsAndKeys: + @"PurpleEDRAM", kIOSurfaceMemoryRegion, + [NSNumber numberWithInt:alignedNativeBytesPerRow], kIOSurfaceBytesPerRow, + [NSNumber numberWithInt:bytesPerPixel], kIOSurfaceBytesPerElement, + [NSNumber numberWithInt:_nativeWidth], kIOSurfaceWidth, + [NSNumber numberWithInt:_nativeHeight], kIOSurfaceHeight, + [NSNumber numberWithInt:'BGRA'], kIOSurfacePixelFormat, + [NSNumber numberWithInt:(alignedNativeBytesPerRow * _nativeHeight)], kIOSurfaceAllocSize, + nil]); + NSLog(@"[ScreenDumpVNC] Created CARenderServer surface at %zux%zu (aligned row: %zu)", + _nativeWidth, _nativeHeight, alignedNativeBytesPerRow); + } else { + _renderServerSurface = nil; // Not needed when no scaling + } // Initialize capture mode to CARenderServer (default) _captureMode = CaptureModeCARenderServer; _legacyModeTimer = nil; + + NSLog(@"[ScreenDumpVNC] Native resolution: %zux%zu, Output resolution: %zux%zu", + _nativeWidth, _nativeHeight, _width, _height); } int argc = 1; @@ -261,6 +296,8 @@ staticBuffer:_staticBuffer width:_width height:_height + nativeWidth:_nativeWidth + nativeHeight:_nativeHeight useCADisplayLink:useCADisplayLink renderServerSurface:_renderServerSurface captureModeBlock:^CaptureMode(void) {