refactor: remove unnecessary changes

This commit is contained in:
Acaki
2025-08-16 04:09:02 +08:00
parent 34eb2ccd31
commit b3664d0e2d
14 changed files with 95 additions and 864 deletions
@@ -32,8 +32,7 @@
@property (strong, nonatomic) IBOutlet UISegmentedControl *yuv444Selector; @property (strong, nonatomic) IBOutlet UISegmentedControl *yuv444Selector;
@property (strong, nonatomic) IBOutlet UISegmentedControl *pipSelector; @property (strong, nonatomic) IBOutlet UISegmentedControl *pipSelector;
@property (strong, nonatomic) IBOutlet UISegmentedControl *hdrSelector; @property (strong, nonatomic) IBOutlet UISegmentedControl *hdrSelector;
@property (strong, nonatomic) IBOutlet UILabel *frameQueueSizeLabel; @property (strong, nonatomic) IBOutlet UISegmentedControl *framePacingSelector;
@property (strong, nonatomic) IBOutlet UISlider *frameQueueSizeSlider;
@property (strong, nonatomic) IBOutlet UISegmentedControl *btMouseSelector; @property (strong, nonatomic) IBOutlet UISegmentedControl *btMouseSelector;
@property (strong, nonatomic) IBOutlet UISegmentedControl *reverseMouseWheelDirectionSelector; @property (strong, nonatomic) IBOutlet UISegmentedControl *reverseMouseWheelDirectionSelector;
@property (strong, nonatomic) IBOutlet UISegmentedControl *statsOverlaySelector; @property (strong, nonatomic) IBOutlet UISegmentedControl *statsOverlaySelector;
@@ -63,10 +62,6 @@
@property (strong, nonatomic) IBOutlet UISegmentedControl *externalDisplayModeSelector; @property (strong, nonatomic) IBOutlet UISegmentedControl *externalDisplayModeSelector;
@property (strong, nonatomic) IBOutlet UISegmentedControl *localMousePointerModeSelector; @property (strong, nonatomic) IBOutlet UISegmentedControl *localMousePointerModeSelector;
@property (strong, nonatomic) IBOutlet UILabel *enableGraphsLabel;
@property (strong, nonatomic) IBOutlet UISegmentedControl *enableGraphsSelector;
@property (strong, nonatomic) IBOutlet UIStepper *graphOpacityStepper;
@property (strong, nonatomic) IBOutlet UISegmentedControl *renderingBackendSelector;
#pragma clang diagnostic push #pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wunguarded-availability" #pragma clang diagnostic ignored "-Wunguarded-availability"
@@ -22,8 +22,6 @@
bool justEnteredSettingsViewDoNotOpenOscLayoutTool; bool justEnteredSettingsViewDoNotOpenOscLayoutTool;
uint16_t oscLayoutFingers; uint16_t oscLayoutFingers;
CustomEdgeSlideGestureRecognizer *slideToCloseSettingsViewRecognizer; CustomEdgeSlideGestureRecognizer *slideToCloseSettingsViewRecognizer;
NSInteger _frameQueueSize;
NSInteger _graphOpacity;
} }
@dynamic overrideUserInterfaceStyle; @dynamic overrideUserInterfaceStyle;
@@ -368,8 +366,6 @@ BOOL isCustomResolution(CGSize res) {
// Ensure we pick a bitrate that falls exactly onto a slider notch // Ensure we pick a bitrate that falls exactly onto a slider notch
_bitrate = bitrateTable[[self getSliderValueForBitrate:[currentSettings.bitrate intValue]]]; _bitrate = bitrateTable[[self getSliderValueForBitrate:[currentSettings.bitrate intValue]]];
_frameQueueSize = [currentSettings.frameQueueSize intValue];
_graphOpacity = [currentSettings.graphOpacity intValue];
// Get the size of the screen with and without safe area insets // Get the size of the screen with and without safe area insets
UIWindow *window = UIApplication.sharedApplication.windows.firstObject; UIWindow *window = UIApplication.sharedApplication.windows.firstObject;
@@ -383,9 +379,9 @@ BOOL isCustomResolution(CGSize res) {
UITapGestureRecognizer *resolutionDisplayViewTap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(resolutionDisplayViewTapped:)]; UITapGestureRecognizer *resolutionDisplayViewTap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(resolutionDisplayViewTapped:)];
[self.resolutionDisplayView addGestureRecognizer:resolutionDisplayViewTap]; [self.resolutionDisplayView addGestureRecognizer:resolutionDisplayViewTap];
resolutionTable[0] = CGSizeMake(1280, 720); resolutionTable[0] = CGSizeMake(640, 360);
resolutionTable[1] = CGSizeMake(1920, 1080); resolutionTable[1] = CGSizeMake(1280, 720);
resolutionTable[2] = CGSizeMake(2560, 1440); resolutionTable[2] = CGSizeMake(1920, 1080);
resolutionTable[3] = CGSizeMake(3840, 2160); resolutionTable[3] = CGSizeMake(3840, 2160);
resolutionTable[4] = CGSizeMake(safeAreaWidth, fullScreenHeight); resolutionTable[4] = CGSizeMake(safeAreaWidth, fullScreenHeight);
resolutionTable[5] = CGSizeMake(fullScreenWidth, fullScreenHeight); resolutionTable[5] = CGSizeMake(fullScreenWidth, fullScreenHeight);
@@ -397,12 +393,6 @@ BOOL isCustomResolution(CGSize res) {
resolutionTable[6] = CGSizeMake(0, 0); resolutionTable[6] = CGSizeMake(0, 0);
} }
// Customize framerate list for ProMotion devices
if ([[UIScreen mainScreen] maximumFramesPerSecond] >= 90) {
[self.framerateSelector setEnabled:YES forSegmentAtIndex:2]; // enable 90
[self.framerateSelector setEnabled:YES forSegmentAtIndex:3]; // enable 120
}
NSInteger framerate; NSInteger framerate;
switch ([currentSettings.framerate integerValue]) { switch ([currentSettings.framerate integerValue]) {
case 30: case 30:
@@ -412,11 +402,8 @@ BOOL isCustomResolution(CGSize res) {
case 60: case 60:
framerate = 1; framerate = 1;
break; break;
case 90:
framerate = 2;
break;
case 120: case 120:
framerate = 3; framerate = 2;
break; break;
} }
@@ -425,6 +412,17 @@ BOOL isCustomResolution(CGSize res) {
resolution = 0; resolution = 0;
} }
// Only show the 120 FPS option if we have a > 60-ish Hz display
bool enable120Fps = false;
if (@available(iOS 10.3, tvOS 10.3, *)) {
if ([UIScreen mainScreen].maximumFramesPerSecond > 62) {
enable120Fps = true;
}
}
if (!enable120Fps) {
[self.framerateSelector removeSegmentAtIndex:2 animated:NO];
}
// Disable codec selector segments for unsupported codecs // Disable codec selector segments for unsupported codecs
#if defined(__IPHONE_16_0) || defined(__TVOS_16_0) #if defined(__IPHONE_16_0) || defined(__TVOS_16_0)
if (!VTIsHardwareDecodeSupported(kCMVideoCodecType_AV1)) if (!VTIsHardwareDecodeSupported(kCMVideoCodecType_AV1))
@@ -469,23 +467,9 @@ BOOL isCustomResolution(CGSize res) {
[self.yuv444Selector setSelectedSegmentIndex:currentSettings.enableYUV444 ? 1 : 0]; [self.yuv444Selector setSelectedSegmentIndex:currentSettings.enableYUV444 ? 1 : 0];
[self.pipSelector setSelectedSegmentIndex:currentSettings.enablePIP ? 1 : 0]; [self.pipSelector setSelectedSegmentIndex:currentSettings.enablePIP ? 1 : 0];
[self.statsOverlaySelector setSelectedSegmentIndex:currentSettings.statsOverlayLevel.intValue]; [self.statsOverlaySelector setSelectedSegmentIndex:currentSettings.statsOverlayLevel.intValue];
NSInteger renderingBackend = [currentSettings.renderingBackend integerValue];
[self.renderingBackendSelector setSelectedSegmentIndex:renderingBackend];
[self.enableGraphsSelector setSelectedSegmentIndex:currentSettings.enableGraphs ? 1 : 0];
[self.enableGraphsSelector addTarget:self action:@selector(enableGraphsChanged) forControlEvents:UIControlEventValueChanged];
[self enableGraphsChanged];
[self.graphOpacityStepper setMinimumValue:0];
[self.graphOpacityStepper setMaximumValue:100];
[self.graphOpacityStepper setValue:_graphOpacity];
[self.graphOpacityStepper addTarget:self action:@selector(graphOpacityStepperMoved) forControlEvents:UIControlEventValueChanged];
[self updateGraphOpacityText];
[self.btMouseSelector setSelectedSegmentIndex:currentSettings.btMouseSupport ? 1 : 0]; [self.btMouseSelector setSelectedSegmentIndex:currentSettings.btMouseSupport ? 1 : 0];
[self.optimizeSettingsSelector setSelectedSegmentIndex:currentSettings.optimizeGames ? 1 : 0]; [self.optimizeSettingsSelector setSelectedSegmentIndex:currentSettings.optimizeGames ? 1 : 0];
[self.frameQueueSizeSlider setMinimumValue:1]; [self.framePacingSelector setSelectedSegmentIndex:currentSettings.useFramePacing ? 1 : 0];
[self.frameQueueSizeSlider setMaximumValue:5];
[self.frameQueueSizeSlider setValue:_frameQueueSize];
[self.frameQueueSizeSlider addTarget:self action:@selector(frameQueueSizeSliderMoved) forControlEvents:UIControlEventValueChanged];
[self updateFrameQueueSizeText];
[self.multiControllerSelector setSelectedSegmentIndex:currentSettings.multiController ? 1 : 0]; [self.multiControllerSelector setSelectedSegmentIndex:currentSettings.multiController ? 1 : 0];
[self.swapABXYButtonsSelector setSelectedSegmentIndex:currentSettings.swapABXYButtons ? 1 : 0]; [self.swapABXYButtonsSelector setSelectedSegmentIndex:currentSettings.swapABXYButtons ? 1 : 0];
[self.audioOnPCSelector setSelectedSegmentIndex:currentSettings.playAudioOnPC ? 1 : 0]; [self.audioOnPCSelector setSelectedSegmentIndex:currentSettings.playAudioOnPC ? 1 : 0];
@@ -845,7 +829,7 @@ BOOL isCustomResolution(CGSize res) {
} }
defaultBitrate = round(resolutionFactor * frameRateFactor) * 1000; defaultBitrate = round(resolutionFactor * frameRateFactor) * 1000;
_bitrate = MIN(defaultBitrate, 150000); _bitrate = MIN(defaultBitrate, 100000);
[self.bitrateSlider setValue:[self getSliderValueForBitrate:_bitrate] animated:YES]; [self.bitrateSlider setValue:[self getSliderValueForBitrate:_bitrate] animated:YES];
[self updateBitrateText]; [self updateBitrateText];
@@ -1019,8 +1003,6 @@ BOOL isCustomResolution(CGSize res) {
case 1: case 1:
return 60; return 60;
case 2: case 2:
return 90;
case 3:
return 120; return 120;
default: default:
abort(); abort();
@@ -1069,30 +1051,6 @@ BOOL isCustomResolution(CGSize res) {
return resolutionTable[[self.resolutionSelector selectedSegmentIndex]].width; return resolutionTable[[self.resolutionSelector selectedSegmentIndex]].width;
} }
- (void) frameQueueSizeSliderMoved {
assert(self.frameQueueSizeSlider.value >= 0 && self.frameQueueSizeSlider.value <= 5);
_frameQueueSize = (int)self.frameQueueSizeSlider.value;
[self updateFrameQueueSizeText];
}
- (void) updateFrameQueueSizeText {
[self.frameQueueSizeLabel setText:[NSString stringWithFormat:@"Frames to buffer: %ld", _frameQueueSize ]];
}
- (void) enableGraphsChanged {
[self.graphOpacityStepper setEnabled:[self.enableGraphsSelector selectedSegmentIndex] == 1 ? YES : NO];
}
- (void) graphOpacityStepperMoved {
assert(self.graphOpacityStepper.value >= 0 && self.graphOpacityStepper.value <= 100);
_graphOpacity = (int)self.graphOpacityStepper.value;
[self updateGraphOpacityText];
}
- (void) updateGraphOpacityText {
[self.enableGraphsLabel setText:[NSString stringWithFormat:@"Performance Graphs - Opacity: %ld%%", _graphOpacity ]];
}
- (void) saveSettings { - (void) saveSettings {
DataManager* dataMan = [[DataManager alloc] init]; DataManager* dataMan = [[DataManager alloc] init];
NSInteger framerate = [self getChosenFrameRate]; NSInteger framerate = [self getChosenFrameRate];
@@ -1116,7 +1074,6 @@ BOOL isCustomResolution(CGSize res) {
BOOL largerStickLR1 = [self.largerStickLR1Selector selectedSegmentIndex] == 1; BOOL largerStickLR1 = [self.largerStickLR1Selector selectedSegmentIndex] == 1;
BOOL liftStreamViewForKeyboard = [self.liftStreamViewForKeyboardSelector selectedSegmentIndex] == 1; BOOL liftStreamViewForKeyboard = [self.liftStreamViewForKeyboardSelector selectedSegmentIndex] == 1;
BOOL showKeyboardToolbar = [self.showKeyboardToolbarSelector selectedSegmentIndex] == 1; BOOL showKeyboardToolbar = [self.showKeyboardToolbarSelector selectedSegmentIndex] == 1;
NSInteger renderingBackend = [self.renderingBackendSelector selectedSegmentIndex];
BOOL optimizeGames = [self.optimizeSettingsSelector selectedSegmentIndex] == 1; BOOL optimizeGames = [self.optimizeSettingsSelector selectedSegmentIndex] == 1;
BOOL multiController = [self.multiControllerSelector selectedSegmentIndex] == 1; BOOL multiController = [self.multiControllerSelector selectedSegmentIndex] == 1;
BOOL swapABXYButtons = [self.swapABXYButtonsSelector selectedSegmentIndex] == 1; BOOL swapABXYButtons = [self.swapABXYButtonsSelector selectedSegmentIndex] == 1;
@@ -1130,7 +1087,6 @@ BOOL isCustomResolution(CGSize res) {
NSInteger touchMode = [self.touchModeSelector selectedSegmentIndex]; NSInteger touchMode = [self.touchModeSelector selectedSegmentIndex];
NSInteger statsOverlayLevel = [self.statsOverlaySelector selectedSegmentIndex]; NSInteger statsOverlayLevel = [self.statsOverlaySelector selectedSegmentIndex];
BOOL statsOverlayEnabled = statsOverlayLevel != 0; BOOL statsOverlayEnabled = statsOverlayLevel != 0;
BOOL enableGraphs = [self.enableGraphsSelector selectedSegmentIndex] == 1;
BOOL enableHdr = [self.hdrSelector selectedSegmentIndex] == 1; BOOL enableHdr = [self.hdrSelector selectedSegmentIndex] == 1;
BOOL unlockDisplayOrientation = [self.unlockDisplayOrientationSelector selectedSegmentIndex] == 1; BOOL unlockDisplayOrientation = [self.unlockDisplayOrientationSelector selectedSegmentIndex] == 1;
NSInteger resolutionSelected = [self.resolutionSelector selectedSegmentIndex]; NSInteger resolutionSelected = [self.resolutionSelector selectedSegmentIndex];
@@ -1164,19 +1120,16 @@ BOOL isCustomResolution(CGSize res) {
enableYUV444:enableYUV444 enableYUV444:enableYUV444
enablePIP:enablePIP enablePIP:enablePIP
useFramePacing:useFramePacing useFramePacing:useFramePacing
frameQueueSize:_frameQueueSize
enableHdr:enableHdr enableHdr:enableHdr
btMouseSupport:btMouseSupport btMouseSupport:btMouseSupport
// absoluteTouchMode:absoluteTouchMode // absoluteTouchMode:absoluteTouchMode
touchMode:touchMode touchMode:touchMode
statsOverlayLevel:statsOverlayLevel statsOverlayLevel:statsOverlayLevel
statsOverlayEnabled:statsOverlayEnabled
unlockDisplayOrientation:unlockDisplayOrientation unlockDisplayOrientation:unlockDisplayOrientation
resolutionSelected:resolutionSelected resolutionSelected:resolutionSelected
externalDisplayMode:externalDisplayMode externalDisplayMode:externalDisplayMode
localMousePointerMode:localMousePointerMode]; localMousePointerMode:localMousePointerMode];
enableGraphs:enableGraphs
graphOpacity:_graphOpacity
renderingBackend:renderingBackend];
} }
- (void)didReceiveMemoryWarning { - (void)didReceiveMemoryWarning {
-2
View File
@@ -48,8 +48,6 @@
</array> </array>
<key>NSLocalNetworkUsageDescription</key> <key>NSLocalNetworkUsageDescription</key>
<string>Moonlight uses the local network to connect to your gaming PC for streaming.</string> <string>Moonlight uses the local network to connect to your gaming PC for streaming.</string>
<key>UIDesignRequiresCompatibility</key>
<true/>
<key>UIMainStoryboardFile</key> <key>UIMainStoryboardFile</key>
<string>Main</string> <string>Main</string>
<key>UIRequiredDeviceCapabilities</key> <key>UIRequiredDeviceCapabilities</key>
+39 -111
View File
@@ -36,44 +36,6 @@
<string>4K</string> <string>4K</string>
</array> </array>
</dict> </dict>
<dict>
<key>Type</key>
<string>PSToggleSwitchSpecifier</string>
<key>Title</key>
<string>Request HDR Video</string>
<key>Key</key>
<string>enableHdr</string>
<key>DefaultValue</key>
<false/>
</dict>
<dict>
<key>Type</key>
<string>PSMultiValueSpecifier</string>
<key>Title</key>
<string>Audio Configuration</string>
<key>Key</key>
<string>audioConfig</string>
<key>DefaultValue</key>
<string>2</string>
<key>Titles</key>
<array>
<string>Stereo</string>
<string>LPCM 5.1</string>
<string>LPCM 7.1</string>
</array>
<key>Values</key>
<array>
<string>2</string>
<string>6</string>
<string>8</string>
</array>
</dict>
<dict>
<key>Type</key>
<string>PSGroupSpecifier</string>
<key>Title</key>
<string>Enable Match Content Rate for best results</string>
</dict>
<dict> <dict>
<key>Type</key> <key>Type</key>
<string>PSMultiValueSpecifier</string> <string>PSMultiValueSpecifier</string>
@@ -85,16 +47,12 @@
<string>60</string> <string>60</string>
<key>Values</key> <key>Values</key>
<array> <array>
<string>24</string>
<string>30</string> <string>30</string>
<string>59.94</string>
<string>60</string> <string>60</string>
</array> </array>
<key>Titles</key> <key>Titles</key>
<array> <array>
<string>24 FPS</string>
<string>30 FPS</string> <string>30 FPS</string>
<string>59.94 FPS (experimental)</string>
<string>60 FPS</string> <string>60 FPS</string>
</array> </array>
</dict> </dict>
@@ -138,6 +96,28 @@
<string>150000</string> <string>150000</string>
</array> </array>
</dict> </dict>
<dict>
<key>Type</key>
<string>PSMultiValueSpecifier</string>
<key>Title</key>
<string>Audio Configuration</string>
<key>Key</key>
<string>audioConfig</string>
<key>DefaultValue</key>
<string>2</string>
<key>Titles</key>
<array>
<string>Stereo</string>
<string>LPCM 5.1</string>
<string>LPCM 7.1</string>
</array>
<key>Values</key>
<array>
<string>2</string>
<string>6</string>
<string>8</string>
</array>
</dict>
<dict> <dict>
<key>Type</key> <key>Type</key>
<string>PSGroupSpecifier</string> <string>PSGroupSpecifier</string>
@@ -232,53 +212,37 @@
<key>Type</key> <key>Type</key>
<string>PSMultiValueSpecifier</string> <string>PSMultiValueSpecifier</string>
<key>Title</key> <key>Title</key>
<string>Frames to Buffer</string> <string>Frame Pacing Preference</string>
<key>Key</key> <key>Key</key>
<string>frameQueueSize</string> <string>useFramePacing</string>
<key>DefaultValue</key>
<integer>2</integer>
<key>Values</key>
<array>
<integer>1</integer>
<integer>2</integer>
<integer>3</integer>
<integer>4</integer>
<integer>5</integer>
</array>
<key>Titles</key>
<array>
<string>1 - Lowest Latency</string>
<string>2 - Recommended</string>
<string>3 - Smoother Video</string>
<string>4</string>
<string>5</string>
</array>
</dict>
<dict>
<key>Type</key>
<string>PSMultiValueSpecifier</string>
<key>Title</key>
<string>Rendering Backend</string>
<key>Key</key>
<string>renderingBackend</string>
<key>DefaultValue</key> <key>DefaultValue</key>
<string>0</string> <string>0</string>
<key>Values</key> <key>Values</key>
<array> <array>
<integer>0</integer> <string>0</string>
<integer>1</integer> <string>1</string>
</array> </array>
<key>Titles</key> <key>Titles</key>
<array> <array>
<string>Metal</string> <string>Lowest Latency</string>
<string>AVSampleBuffer</string> <string>Smoothest Video</string>
</array> </array>
</dict> </dict>
<dict>
<key>Type</key>
<string>PSToggleSwitchSpecifier</string>
<key>Title</key>
<string>Request HDR Video</string>
<key>Key</key>
<string>enableHdr</string>
<key>DefaultValue</key>
<false/>
</dict>
<dict> <dict>
<key>Type</key> <key>Type</key>
<string>PSGroupSpecifier</string> <string>PSGroupSpecifier</string>
<key>Title</key> <key>Title</key>
<string>Double-click select button for stats</string> <string>Troubleshooting</string>
</dict> </dict>
<dict> <dict>
<key>Type</key> <key>Type</key>
@@ -290,42 +254,6 @@
<key>DefaultValue</key> <key>DefaultValue</key>
<false/> <false/>
</dict> </dict>
<dict>
<key>Type</key>
<string>PSToggleSwitchSpecifier</string>
<key>Title</key>
<string>Performance Graphs</string>
<key>Key</key>
<string>enableGraphs</string>
<key>DefaultValue</key>
<false/>
</dict>
<dict>
<key>Type</key>
<string>PSMultiValueSpecifier</string>
<key>Title</key>
<string>Performance Graph Opacity</string>
<key>Key</key>
<string>graphOpacity</string>
<key>DefaultValue</key>
<integer>50</integer>
<key>Values</key>
<array>
<integer>0</integer>
<integer>25</integer>
<integer>50</integer>
<integer>75</integer>
<integer>100</integer>
</array>
<key>Titles</key>
<array>
<string>0% - Transparent</string>
<string>25%</string>
<string>50%</string>
<string>75%</string>
<string>100% - Opaque</string>
</array>
</dict>
</array> </array>
</dict> </dict>
</plist> </plist>
-271
View File
@@ -1,271 +0,0 @@
#import <XCTest/XCTest.h>
#import <CoreMedia/CoreMedia.h>
#define FRAME_QUEUE_VERBOSE
#import "FrameQueue.h"
#import "Logger.h"
#include "Limelight.h"
@interface FrameQueueTests : XCTestCase
@property (nonatomic, strong) FrameQueue *queue;
@end
@implementation FrameQueueTests {
int _frameNumber;
}
- (void)setUp {
[super setUp];
self.queue = [FrameQueue sharedInstance];
[self.queue setHighWaterMark:3];
_frameNumber = 0;
}
- (void)tearDown {
[self.queue clear];
self.queue = nil;
[super tearDown];
}
- (CMSampleBufferRef)makeEmptySampleBufferFor:(uint32_t)tick {
CMSampleTimingInfo sampleTiming = {
.duration = kCMTimeInvalid,
.presentationTimeStamp = CMTimeMake((int64_t)tick, 90000),
.decodeTimeStamp = kCMTimeInvalid,
};
CMSampleBufferRef sampleBuffer = NULL;
OSStatus status = CMSampleBufferCreate(kCFAllocatorDefault,
NULL, NO, NULL, NULL, NULL,
1, 1, &sampleTiming, 0, NULL,
&sampleBuffer);
XCTAssertEqual(status, noErr, @"makeEmptySampleBufferFor:%d ok", tick);
return sampleBuffer;
}
- (Frame *)makeFrameNumber:(int)num type:(int)type pts:(uint32_t)tick {
CMSampleBufferRef buf = [self makeEmptySampleBufferFor:tick];
CFRetain(buf); // emulate retain that happens in CMSampleBufferCreateReadyWithImageBuffer
Frame *frame = [[Frame alloc] initWithSampleBuffer:buf
frameNumber:num
frameType:type];
_frameNumber = num; // reset the counter
return frame;
}
- (Frame *)makeFrame {
const uint32_t ONE_FRAME = 90000 / 60;
static uint32_t tick = 0;
Frame *frame = [self makeFrameNumber:++_frameNumber
type:FRAME_TYPE_PFRAME
pts:tick];
tick += ONE_FRAME;
return frame;
}
- (void)testEnqueueThenDequeue {
Frame *f1 = [self makeFrameNumber:1 type:FRAME_TYPE_IDR pts:0];
Frame *f2 = [self makeFrame];
[self.queue enqueue:f1];
[self.queue enqueue:f2];
XCTAssertEqual([self.queue count], 2);
Frame *out1 = [self.queue dequeue];
XCTAssertEqual(out1.frameNumber, 1);
XCTAssertEqual([self.queue count], 1);
Frame *out2 = [self.queue dequeue];
XCTAssertEqual(out2.frameNumber, 2);
XCTAssertEqual([self.queue count], 0);
// empty now
XCTAssertNil([self.queue dequeue]);
}
- (void)testOverflow {
[self.queue setHighWaterMark:3];
for (int i = 0; i < self.queue.highWaterMark; i++) {
[self.queue enqueue:[self makeFrame]];
}
XCTAssertEqual([self.queue count], 3);
// queue should be full at 3
// 4: dropped, 5: queued, causes 1 to be dropped
// 6: dropped, 7: queued, causes 2 to be dropped
// 8: dropped, 9: queued, causes 3 to be dropped
for (int i = 0; i < 6; i++) {
[self.queue enqueue:[self makeFrame]];
}
XCTAssertEqual([self.queue count], 3);
XCTAssertEqual([[self.queue frameDropMetrics] total], 6);
Log(LOG_I, @"queue after overflow: %@", self.queue);
for (NSNumber *expected in @[@5, @7, @9]) {
Frame *frame = [self.queue dequeue];
XCTAssertEqual(frame.frameNumber, expected.integerValue);
}
}
- (void)testOverflowWithIDR {
[self.queue setHighWaterMark:5];
[self.queue enqueue:[self makeFrameNumber:1 type:FRAME_TYPE_IDR pts:0]];
[self.queue enqueue:[self makeFrame]];
[self.queue enqueue:[self makeFrameNumber:3 type:FRAME_TYPE_IDR pts:0]];
[self.queue enqueue:[self makeFrame]];
[self.queue enqueue:[self makeFrame]];
XCTAssertEqual([self.queue count], 5);
// queue should be full at 5
// 6: dropped, 7: queued, does NOT drop 1 (IDR)
// 8: dropped, 9: queued, does NOT drop 1 (IDR) - will be stuck here until it's dequeued
// 10: dropped, 11: queued, does NOT drop 3 (IDR)
for (int i = 0; i < 6; i++) {
[self.queue enqueue:[self makeFrame]];
}
XCTAssertEqual([self.queue count], 8);
XCTAssertEqual([[self.queue frameDropMetrics] total], 3);
Log(LOG_I, @"queue after overflow: %@", self.queue);
for (NSNumber *expected in @[@1, @2, @3, @4, @5, @7, @9, @11]) {
XCTAssertEqual([self.queue dequeue].frameNumber, expected.integerValue);
}
}
- (void)testClear {
for (int i = 0; i < self.queue.highWaterMark; i++) {
[self.queue enqueue:[self makeFrame]];
}
XCTAssertEqual([self.queue count], self.queue.highWaterMark);
[self.queue clear];
XCTAssertEqual([self.queue count], 0);
XCTAssertEqual([[self.queue frameDropMetrics] total], 0);
XCTAssertNil([self.queue dequeue]);
}
- (void)testDequeueWithTimeout {
[self.queue enqueue:[self makeFrame]];
// should return immediately with an item in the queue
CFTimeInterval t0 = CACurrentMediaTime();
Frame *out = [self.queue dequeueWithTimeout:1];
XCTAssertNotNil(out);
XCTAssertEqual(out.frameNumber, 1);
XCTAssertLessThan(CACurrentMediaTime() - t0, 0.9);
// should return nil after waiting
t0 = CACurrentMediaTime();
XCTAssertNil([self.queue dequeueWithTimeout:0.2]);
XCTAssertGreaterThan(CACurrentMediaTime() - t0, 0.2);
// no timeout
t0 = CACurrentMediaTime();
XCTAssertNil([self.queue dequeueWithTimeout:0]);
XCTAssertLessThan(CACurrentMediaTime() - t0, 0.1);
}
- (void)testDuration {
// 2nd frame will compute 1st frame's duration. makeFrame generates sequential frames at 60fps
[self.queue clear];
[self.queue enqueue:[self makeFrame]];
[self.queue enqueue:[self makeFrame]];
XCTAssertEqual([self.queue count], 2);
Frame *first = [self.queue dequeue];
XCTAssertEqualWithAccuracy(first.duration, (float)1.0f / 60, 0.0001f);
}
- (void)testEstimatedFramerate {
// prime the start point
[self.queue estimatedFramerate];
for (int i = 0; i < 24 * 5; i++) {
// these will all get dropped but they are still counted for fps
[self.queue enqueue:[self makeFrame]];
[NSThread sleepForTimeInterval:1.0f / 24];
}
XCTAssertEqualWithAccuracy([self.queue estimatedFramerate], 24.0f, 4.0f);
}
- (void)testProducerConsumer {
static const int TotalFrames = 500;
// We expect exactly kTotalFrames dequeues.
XCTestExpectation *dequeueAll = [self expectationWithDescription:@"consumer dequeued all frames"];
dequeueAll.expectedFulfillmentCount = TotalFrames;
// Producer
dispatch_queue_t producerQ = dispatch_queue_create("moonlight.test.producer", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(producerQ, ^{
for (int i = 0; i < TotalFrames; i++) {
[self.queue enqueue:[self makeFrame]];
[NSThread sleepForTimeInterval:0.002];
}
});
// Consumer
dispatch_queue_t consumerQ = dispatch_queue_create("moonlight.test.consumer", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(consumerQ, ^{
for (int expected = 0; expected < TotalFrames; expected++) {
Frame *frame = [self.queue dequeueWithTimeout:1.0];
XCTAssertNotNil(frame, @"Expected frame %d, but got nil", expected);
XCTAssertEqual(frame.frameNumber, expected + 1, @"Expected frameNumber %d but got %d", expected + 1, frame.frameNumber);
[dequeueAll fulfill];
}
});
// Wait up to 10s for all frames to be consumed
[self waitForExpectationsWithTimeout:10.0 handler:^(NSError * _Nullable error) {
if (error) {
XCTFail(@"Timed out waiting for consumer: %@", error);
}
XCTAssertEqual([self.queue count], 0, @"Queue should be empty at end");
}];
}
- (void)testPerformance {
XCTMeasureOptions *options = [[XCTMeasureOptions alloc] init];
options.iterationCount = 20;
if (@available(iOS 13.0, *)) {
static const int TotalFrames = 1000;
dispatch_queue_t producerQ = dispatch_queue_create("moonlight.test.producer", DISPATCH_QUEUE_CONCURRENT);
dispatch_queue_t consumerQ = dispatch_queue_create("moonlight.test.consumer", DISPATCH_QUEUE_CONCURRENT);
[self measureWithOptions:options block:^{
XCTestExpectation *dequeueAll = [self expectationWithDescription:@"consumer dequeued all frames"];
dequeueAll.expectedFulfillmentCount = TotalFrames;
[self.queue setHighWaterMark:5];
// Producer
dispatch_async(producerQ, ^{
for (int i = 0; i < TotalFrames; i++) {
//[self.queue enqueue:[self makeFrame]];
[self.queue enqueue:[self makeFrame] withSlackSize:3];
[NSThread sleepForTimeInterval:0.001];
}
});
// Consumer
dispatch_async(consumerQ, ^{
for (int expected = 0; expected < TotalFrames; expected++) {
[self.queue dequeueWithTimeout:1.0f];
[dequeueAll fulfill];
}
});
// Wait up to 10s for all frames to be consumed
[self waitForExpectationsWithTimeout:100.0 handler:^(NSError * _Nullable error) {
if (error) {
XCTFail(@"Timed out waiting for consumer: %@", error);
[self.queue clear];
}
}];
}];
}
}
@end
-5
View File
@@ -95,32 +95,27 @@ static NSString* DB_NAME = @"Limelight_iOS.sqlite";
{ {
// Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state.
// Use this method to pause ongoing tasks, disable timers, and throttle down OpenGL ES frame rates. Games should use this method to pause the game. // Use this method to pause ongoing tasks, disable timers, and throttle down OpenGL ES frame rates. Games should use this method to pause the game.
Log(LOG_I, @"TODO: applicationWillResignActive");
} }
- (void)applicationDidEnterBackground:(UIApplication *)application - (void)applicationDidEnterBackground:(UIApplication *)application
{ {
// Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later. // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later.
// If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits.
Log(LOG_I, @"TODO: applicationDidEnterBackground");
} }
- (void)applicationWillEnterForeground:(UIApplication *)application - (void)applicationWillEnterForeground:(UIApplication *)application
{ {
// Called as part of the transition from the background to the inactive state; here you can undo many of the changes made on entering the background. // Called as part of the transition from the background to the inactive state; here you can undo many of the changes made on entering the background.
Log(LOG_I, @"TODO: applicationWillEnterForeground");
} }
- (void)applicationDidBecomeActive:(UIApplication *)application - (void)applicationDidBecomeActive:(UIApplication *)application
{ {
// Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface.
Log(LOG_I, @"TODO: applicationDidBecomeActive");
} }
- (void)applicationWillTerminate:(UIApplication *)application - (void)applicationWillTerminate:(UIApplication *)application
{ {
// Saves changes in the application's managed object context before the application terminates. // Saves changes in the application's managed object context before the application terminates.
Log(LOG_I, @"applicationWillTerminate, calling saveContext");
[self saveContext]; [self saveContext];
} }
+1
View File
@@ -239,6 +239,7 @@
}); });
else { else {
for (UITouch* touch in touches){ for (UITouch* touch in touches){
// continue to the next loop if current touch is already captured by OSC. works only in regular native touch
if(self->activateCoordSelector) [self updatePointerObjInDict:touch]; if(self->activateCoordSelector) [self updatePointerObjInDict:touch];
[self sendTouchEvent:touch withTouchtype:LI_TOUCH_EVENT_MOVE]; [self sendTouchEvent:touch withTouchtype:LI_TOUCH_EVENT_MOVE];
[[self getPointerObjFromDict:touch] doesNeedResetCoords]; // execute the judging of doesReachBoundary for current pointer instance. (happens after the event is sent to Sunshine service) [[self getPointerObjFromDict:touch] doesNeedResetCoords]; // execute the judging of doesReachBoundary for current pointer instance. (happens after the event is sent to Sunshine service)
-11
View File
@@ -580,9 +580,6 @@ CFStringRef __currentColorSpace;
fragmentShaderName = is10BitInput ? @"ps_draw_triplanar_10bit" : @"ps_draw_triplanar_8bit"; fragmentShaderName = is10BitInput ? @"ps_draw_triplanar_10bit" : @"ps_draw_triplanar_8bit";
} }
Log(LOG_I, @"DEBUG: CVPixelBuffer format: 0x%X, planes: %zu, is10BitInput: %d, shader: %@, framebuffer format: %lu",
pixelFormatType, planes, is10BitInput, fragmentShaderName, (unsigned long)framebufferPixelFormat);
pipelineDesc.colorAttachments[0].pixelFormat = framebufferPixelFormat; pipelineDesc.colorAttachments[0].pixelFormat = framebufferPixelFormat;
pipelineDesc.vertexBuffers[0].mutability = MTLMutabilityImmutable; pipelineDesc.vertexBuffers[0].mutability = MTLMutabilityImmutable;
@@ -638,10 +635,6 @@ CFStringRef __currentColorSpace;
return; return;
} else { } else {
id<MTLTexture> texture = CVMetalTextureGetTexture(_cvMetalTextures[0]); id<MTLTexture> texture = CVMetalTextureGetTexture(_cvMetalTextures[0]);
Log(LOG_I, @"DEBUG: Created BGRA texture: format=%lu, width=%zu, height=%zu",
(unsigned long)MTLPixelFormatBGRA8Unorm,
CVPixelBufferGetWidth(frame.pixelBuffer),
CVPixelBufferGetHeight(frame.pixelBuffer));
} }
} else { } else {
// Handle planar YUV formats // Handle planar YUV formats
@@ -683,10 +676,6 @@ CFStringRef __currentColorSpace;
return; return;
} else { } else {
id<MTLTexture> texture = CVMetalTextureGetTexture(_cvMetalTextures[i]); id<MTLTexture> texture = CVMetalTextureGetTexture(_cvMetalTextures[i]);
Log(LOG_I, @"DEBUG: Created texture for plane %zu: format=%lu, width=%zu, height=%zu",
i, (unsigned long)fmt,
CVPixelBufferGetWidthOfPlane(frame.pixelBuffer, i),
CVPixelBufferGetHeightOfPlane(frame.pixelBuffer, i));
} }
} }
} }
-1
View File
@@ -7,7 +7,6 @@
// //
#import <Metal/Metal.h> #import <Metal/Metal.h>
#import <QuartzCore/CAMetalDisplayLink.h>
#import <QuartzCore/CAMetalLayer.h> #import <QuartzCore/CAMetalLayer.h>
#import <UIKit/UIKit.h> #import <UIKit/UIKit.h>
#import "MetalConfig.h" #import "MetalConfig.h"
View File
-88
View File
@@ -1,88 +0,0 @@
#pragma mark DisplayLink - RTP PTS timestamp-based frame pacing (experimental)
// TODO: handle presentationTimeUs rollover every 13 hours
// This frame pacing method attempts to use timestamps from Sunshine to determine when
// frames should be displayed. Sunshine v2025.600+ required.
- (void)framePacingUsingTimestamps:(CADisplayLink *)link {
// All times are in seconds
static CFTimeInterval anchorLocal = 0.0f;
static CMTime anchorHost;
static CFTimeInterval lastTargetLocal = 0.0f;
CFTimeInterval start = link.timestamp;
CFTimeInterval deadline = link.targetTimestamp;
_displayRefreshRate = 1.0f / (deadline - start);
[self checkDisplayLayer];
Frame *frame = nil;
CFTimeInterval dl0 = CACurrentMediaTime();
static CFTimeInterval avgOverhead = 0.004f; // averaged each callback
CFTimeInterval waitFor = deadline - dl0 - avgOverhead;
if (waitFor < 0.001f) {
waitFor = 0.001f;
}
// Process the next frame for display
frame = [_frameQueue dequeueWithTimeout:waitFor];
if (frame) {
CFTimeInterval now = CACurrentMediaTime();
// during stream startup, wait and let the first frames through untouched
if (!_seenFrameOne) {
Log(LOG_I, @"Frame pacing (pts mode): target %f Hz with %d FPS stream", _displayRefreshRate, self->frameRate);
_seenFrameOne = YES;
// Just let frame 1 through, we don't want to anchor to it
Log(LOG_I, @"[%.3f] rendering frame %d / %.3f, queue size %d",
deadline, frame.frameNumber, frame.pts, [_frameQueue count]);
[self renderFrame:frame atTime:frame.pts90];
return;
}
if (!CMTIME_IS_VALID(anchorHost)) {
// Use this frame as the anchor point. This is normally frame 2.
anchorHost = frame.pts90;
anchorLocal = now;
Log(LOG_I, @"Setting anchor point: anchorHost=%.3f == anchorLocal=%.3f",
CMTimeGetSeconds(anchorHost), anchorLocal);
}
//CMTime targetTime = CMTimeSubtract(frame.pts90, frameQueue.ptsCorrection); // total of all dropped frames
CMTime targetTime = frame.pts90;
CFTimeInterval targetLocal = CMTimeGetSeconds(targetTime);
#ifdef DISPLAYLINK_VERBOSE
// iOS double-buffering renders the frame we queue here during the interval +2 from now
CMTime hostDelta = CMTimeSubtract(frame.pts90, anchorHost);
CFTimeInterval hostDeltaF = CMTimeGetSeconds(hostDelta);
CMTime drift = CMTimeSubtract(hostDelta, CMTimeMakeWithSeconds(now - anchorLocal, NSEC_PER_SEC));
Log(LOG_I, @"[%.3f] rendering frame %d / %.3f @ %.3f, drift %.3f, queue size %d",
deadline - anchorLocal, frame.frameNumber, hostDeltaF, targetLocal,
CMTimeGetSeconds(drift), [_frameQueue count]);
#endif
// Schedule the frame at its original timestamp, adjusted by a correction factor (total duration of all dropped frames)
[self renderFrame:frame atTime:targetTime];
// Update metrics
if (lastTargetLocal != 0) {
[self->_callbacks observeFloat:PLOT_FRAMETIME value:(targetLocal - lastTargetLocal) * 1000.0];
}
lastTargetLocal = targetLocal;
// weighted moving average of how much time displayLink needs after dequeuing a frame.
// This is used to avoid overshooting a vsync by waiting too long.
const double alpha = 0.1f;
avgOverhead = ((CACurrentMediaTime() - dl0) * alpha) + (avgOverhead * (1.0 - alpha));
}
// are we returning too late?
CFTimeInterval dl2 = CACurrentMediaTime();
if (dl2 > deadline) {
Log(LOG_W, @"*** returned past deadline by %.3f ms (%f > %f)", (dl2 - deadline) * 1000.0, dl2, deadline);
}
}
@@ -374,7 +374,7 @@
FB290E2619B37A4E004C83CF /* Project object */ = { FB290E2619B37A4E004C83CF /* Project object */ = {
isa = PBXProject; isa = PBXProject;
attributes = { attributes = {
BuildIndependentTargetsInParallel = NO; BuildIndependentTargetsInParallel = YES;
LastUpgradeCheck = 1510; LastUpgradeCheck = 1510;
ORGANIZATIONNAME = "Moonlight Stream"; ORGANIZATIONNAME = "Moonlight Stream";
TargetAttributes = { TargetAttributes = {
-268
View File
@@ -1,268 +0,0 @@
#!/usr/bin/env perl
#
# Don't try to follow the logic in here, XML::Twig allows for some real nonsense
use strict;
use warnings;
use Getopt::Long qw(GetOptions);
use Pod::Usage qw(pod2usage);
use XML::Twig;
my $inSettings = 0;
my @list = ();
my @clipboard = ();
my $help = 0;
my $after;
my $move;
my $to;
GetOptions(
'help|?' => \$help,
'add-after=s' => \$after,
'move=s' => \$move,
'to=s' => \$to,
) or pod2usage(2);
pod2usage(1) if $help || ($move && !$to) || (!$move && $to);
my $xml = shift || pod2usage(2);
my $twig = XML::Twig->new(
twig_handlers => {
'#COMMENT' => sub {
my ($t, $comment) = @_;
if ($comment->{comment} eq 'Settings View Controller') {
$inSettings = 1;
}
else {
$inSettings = 0;
}
},
label => sub {
my ($t, $label) = @_;
if ($inSettings) {
push @list, [ label => $label ];
}
},
segmentedControl => sub {
my ($t, $control) = @_;
if ($inSettings) {
push @list, [ control => $control ];
}
},
slider => sub {
my ($t, $slider) = @_;
if ($inSettings) {
push @list, [ slider => $slider ];
}
},
view => sub {
my ($t, $view) = @_;
if ($inSettings) {
push @list, [ view => $view ];
}
},
},
comments => 'process',
keep_atts_order => 1,
pretty_print => 'indented',
empty_tags => 'normal',
);
$twig->parsefile($xml);
if ($after) {
do_after($twig, $after);
}
elsif ($move && $to) {
do_move($twig, $move, $to);
}
do_output($twig);
sub do_after {
my ($twig, $after) = @_;
my $LABEL_TEMPLATE = qq{
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" fixedFrame="YES" text="New Label Template" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="%s">
<rect key="frame" x="16" y="999" width="35" height="21"></rect>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"></autoresizingMask>
<fontDescription key="fontDescription" type="system" pointSize="17"></fontDescription>
<color key="textColor" red="0.93902439019999995" green="0.9625305918" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"></color>
<nil key="highlightedColor"></nil>
</label>};
my $CONTROL_TEMPLATE = qq{
<segmentedControl opaque="NO" contentMode="scaleToFill" fixedFrame="YES" contentHorizontalAlignment="left" contentVerticalAlignment="top" segmentControlStyle="plain" selectedSegmentIndex="0" translatesAutoresizingMaskIntoConstraints="NO" id="%s" userLabel="New Control Template">
<rect key="frame" x="16" y="999" width="459" height="28"></rect>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"></autoresizingMask>
<segments>
<segment title="No"></segment>
<segment title="Yes"></segment>
</segments>
<color key="tintColor" red="0.6716768742" green="0.61711704730000005" blue="0.99902987480000005" alpha="1" colorSpace="custom" customColorSpace="sRGB"></color>
<color key="selectedSegmentTintColor" red="0.6716768742" green="0.61711704730000005" blue="0.99902987480000005" alpha="1" colorSpace="custom" customColorSpace="sRGB"></color>
</segmentedControl>};
my @to_add = (
[ label => XML::Twig::Elt->parse( sprintf($LABEL_TEMPLATE, generate_id()) ) ],
[ control => XML::Twig::Elt->parse( sprintf($CONTROL_TEMPLATE, generate_id()) ) ],
);
# Find where to insert them
for (my $i = 0; $i < scalar @list; $i++) {
my ($type, $e) = @{$list[$i]};
if ($type eq 'label' && ($e->att('text') // q{}) eq $after) {
($type, $e) = @{$list[$i + 1]};
# paste the block to a new location
for my $tm (@to_add) {
$tm->[1]->paste(after => $e);
$e = $tm->[1];
}
# also move the block in the ordering list, which is where the y values are adjusted
$i += 2;
splice(@list, $i, 0, @to_add);
}
}
}
sub do_move {
my ($twig, $move, $to) = @_;
my @to_move = ();
# Pull out the bits to move
for (my $i = 0; $i < scalar @list; $i++) {
my ($type, $e) = @{$list[$i]};
if ($type eq 'label' && ($e->att('text') // q{}) eq $move) {
# move this item and the next one (the control)
push @to_move, [ $type => $e->cut ];
($type, $e) = @{$list[$i + 1]};
push @to_move, [ $type => $e->cut ];
splice(@list, $i, 2);
last;
}
}
# Find where to insert them
for (my $i = 0; $i < scalar @list; $i++) {
my ($type, $e) = @{$list[$i]};
if ($type eq 'label' && ($e->att('text') // q{}) eq $to) {
($type, $e) = @{$list[$i + 1]};
# paste the block to a new location
for my $tm (@to_move) {
$tm->[1]->paste(after => $e);
$e = $tm->[1];
}
# also move the block in the ordering list, which is where the y values are adjusted
$i += 2;
splice(@list, $i, 0, @to_move);
}
}
}
sub do_output {
my $twig = shift;
# first label
my $x = 16;
my $y = 20;
# spacing
my $label_to_control = 29;
my $control_to_label = 35;
my $label_to_label = 43;
my $slider_to_next = 29;
my $view_to_next = 43;
my $seen_rdv = 0;
my $last = q{};
printf STDERR "%30s %s %s\n\n", q{}, "auto", "current";
for my $row (@list) {
my ($type, $e) = @{$row};
my $rect = $e->has_child('rect');
my $orig_y = $rect->att('y');
$rect->set_att(x => $x);
$rect->set_att(y => $y);
if ($type eq "label") {
my $text = $e->att('userLabel') // $e->att('text') // "(unknown label)";
printf STDERR "%30s %d %4d\n", $text, $y, $orig_y;
if ($text =~ /Caption/) { # space after captions is a bit larger
$y += $label_to_label;
}
else {
$y += $label_to_control;
}
}
elsif ($type eq "control") {
my ($userLabel) = $e->att('userLabel') // "(unknown control)";
printf STDERR "%30s %d %4d\n", $userLabel, $y, $orig_y;
$y += $control_to_label;
}
elsif ($type eq "slider") {
printf STDERR "%30s %d %4d\n", "slider", $y, $orig_y;
$y += $slider_to_next;
}
elsif ($type eq "view") {
my $customClass = $e->att('customClass') // q{};
if ($customClass && $customClass eq "UIScrollView") {
# This is the entire view, so we're done
last;
}
printf STDERR "%30s %d %4d\n", "view", $y, $orig_y;
$y += $view_to_next;
}
$last = $type;
}
# Output the entire XML with our changes
$twig->print;
}
sub generate_id {
# generate an id of the form K4A-oH-Bs0
my $chars = [ split //, 'abcdefghiklmnopqrstuvwxyzABCDEFGHIKLMNOPQRSTUVWXYZ0123456789' ];
my $random_fragment = sub {
my $length = shift;
return '' if $length < 1;
my $fragment = '';
$fragment .= $chars->[int(rand(scalar @{$chars}))] for (1..$length);
return $fragment;
};
return join '-', (
$random_fragment->(3),
$random_fragment->(2),
$random_fragment->(3),
);
}
=pod
=head1 NAME
SettingsHelper.pl - Less painful storyboard settings elements
=head1 SYNOPSIS
SettingsHelper.pl [options] [iPad.storyboard] > New.storyboard
Run with no options to display the list of items in a storyboard.
Options:
--add-after LABEL Insert a new empty label + control pair after the given label (and its control)
--move FROM --to TO Move the first label and control after the "to" label
--help This help message.
=cut