Fix thread sync
Reformat Metal files Rewrite Metal render loop to not use DisplayLink so we can use the amazing afterMinimumDuration API call Improve frame drop algorithm Misc fixes to HDR <-> SDR switching, deprecation warning cleanup Fix AVSB mode Fix renderingBackend selected value Add Rendering Backend setting to iPhone Let users choose which renderer to use. Refactor metrics into a singleton Fix tvOS build Port moonlight-qt's Metal renderer Bump imgui to latest Misc tweaks and refactoring Switch iOS to use Opus float32 audio, in line with other clients. It was already being sent from the server as float32, so this is simply decoding it with less loss of dynamic range. Give FrameQueue a faster data structure and full test coverage. Remove pts frame pacing, it's not a fit for iOS. Don't allow very transparent opacity for middle stats overlay box Change wording to select button instead of remote button Add tvOS gesture for double-clicking remote button to show/hide stats Remove kVTDecodeFrame_1xRealTimePlayback, this was found to seriously impact decode times Move decode time stat out of async block Enabe FQ logging Misc cleanup/fixes iPad: 26 will require UIRequiresFullScreen to be false, and for all orientations to be supported Updated iPhone and iPad settings panels Add setting for graph opacity. Enable stats and graphs to be toggled via two finger swipe Fix wait time in displaylink, fix settings spacing, stats fixes Settings for frame queue size, graphs. Misc refactoring and cleanup Bump moonlight-common-c Improve logging readability Fix host frametime and frame queue stats to be recorded during input not output. Show min/max/avg decode time Add bytes per frame graph Started on settings changes, both modes working very well Misc refactoring, move graphs around, both pacing functions working well Set frameDropTarget to 2 always Use raw RTP timestamps without converting to floating point for even more accurate playback Standard frame pacing: remove +3 logic Switch to NSRunLoopCommonModes for displayLink Add frame pacing algorithm based on moonlight-qt's Pacer class as a simpler baseline Switch to os_unfair_lock, misc tweaks Large refactor that now uses VTDecompressionSession based on PR535 from felipejfc. This now allows more control over queue size and allows dropping arbitrary frames, but introduces a lot of new problems bump moonlight-common-c to fix 32-bit rollover issue LiWait timing graph Add displaylink interval graph Graphs checkpoint ImGui graph working checkpoint ImGui working checkpoint Add pending frames to stats, add a define for verbose logging DisplayLink WIP Add imgui & implot modules Frame pacing: alternate method based on thieman's pullFrames thread Frame pacing: simplify displayLink, support passing targetTimestamp down to the sample buffer Point to my common-c with some timing changes. Add stats gestures: 2-finger swipe down to show stats, and 2-finger swipe up to hide Limit refresh rate to 60 when in low power mode Display screen refresh rate in stats overlay, remove decode time and explain why it can't be measured Adjust the refresh rate of the display to match the stream framerate and improve battery life Don't use presentationTimeMs when creating CMSampleTimingInfo Fix overlayView build for iOS Refactor CADisplayLink callback for improved frame delivery. Support a user-configurable buffered frame count. [opt] Use UILabel for stats overlay
This commit is contained in:
1 parent
51e915b598
commit
df2b67ac99
61 files changed
+6080
-458
No files matched your search
Whitespace-only changes.
@@ -0,0 +1,40 @@
|
||||
//
|
||||
// TemporarySettings.h
|
||||
// Moonlight
|
||||
//
|
||||
// Created by Cameron Gutman on 12/1/15.
|
||||
// Copyright © 2015 Moonlight Stream. All rights reserved.
|
||||
//
|
||||
|
||||
#import "Settings+CoreDataClass.h"
|
||||
|
||||
@interface TemporarySettings : NSObject
|
||||
|
||||
@property (nonatomic, retain) Settings * parent;
|
||||
|
||||
@property (nonatomic, retain) NSNumber * bitrate;
|
||||
@property (nonatomic, retain) NSNumber * framerate;
|
||||
@property (nonatomic, retain) NSNumber * height;
|
||||
@property (nonatomic, retain) NSNumber * width;
|
||||
@property (nonatomic, retain) NSNumber * audioConfig;
|
||||
@property (nonatomic, retain) NSNumber * onscreenControls;
|
||||
@property (nonatomic, retain) NSString * uniqueId;
|
||||
@property (nonatomic) enum {
|
||||
CODEC_PREF_AUTO,
|
||||
CODEC_PREF_H264,
|
||||
CODEC_PREF_HEVC,
|
||||
CODEC_PREF_AV1,
|
||||
} preferredCodec;
|
||||
@property (nonatomic) BOOL useFramePacing;
|
||||
@property (nonatomic) BOOL multiController;
|
||||
@property (nonatomic) BOOL swapABXYButtons;
|
||||
@property (nonatomic) BOOL playAudioOnPC;
|
||||
@property (nonatomic) BOOL optimizeGames;
|
||||
@property (nonatomic) BOOL enableHdr;
|
||||
@property (nonatomic) BOOL btMouseSupport;
|
||||
@property (nonatomic) BOOL absoluteTouchMode;
|
||||
@property (nonatomic) BOOL statsOverlay;
|
||||
|
||||
- (id) initFromSettings:(Settings*)settings;
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,36 @@
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <Metal/Metal.h>
|
||||
#import <MetalKit/MetalKit.h>
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
#import "FloatBuffer.h"
|
||||
#import "Plot.h"
|
||||
|
||||
typedef void (^MetricsHandler)(int plotId, CFTimeInterval value);
|
||||
|
||||
@interface ImGuiRenderer : UIViewController
|
||||
@end
|
||||
|
||||
@interface ImGuiRenderer () <MTKViewDelegate>
|
||||
@property (nonatomic) CGRect bounds;
|
||||
@property (nonatomic, readonly) MTKView * _Nonnull mtkView;
|
||||
@property (nonatomic, strong) id <MTLDevice> _Nonnull device;
|
||||
@property (nonatomic, strong) id <MTLCommandQueue> _Nonnull commandQueue;
|
||||
@property (nonatomic) struct PlotDef * _Nonnull plots;
|
||||
@property (nonatomic) FloatBuffer * _Nonnull frametimes;
|
||||
@property (nonatomic) BOOL enableGraphs;
|
||||
@property (nonatomic) float graphOpacity;
|
||||
@property (nonatomic) BOOL imguiRunning;
|
||||
@property (nonatomic) MetricsHandler _Nonnull metricsHandler;
|
||||
|
||||
-(nonnull instancetype) initWithFrame:(CGRect)bounds
|
||||
streamFps:(int)streamFps
|
||||
enableGraphs:(BOOL)enableGraphs
|
||||
graphOpacity:(int)graphOpacity;
|
||||
-(void) start;
|
||||
-(void) show;
|
||||
-(void) hide;
|
||||
-(void) stop;
|
||||
-(void) observeFloat:(int)plotId value:(CFTimeInterval)value;
|
||||
-(void) observeFloatReturnMetrics:(int)plotId value:(CFTimeInterval)value plotMetrics:(PlotMetrics * _Nullable)plotMetrics;
|
||||
@end
|
||||
@@ -0,0 +1,425 @@
|
||||
#import <AVFoundation/AVFoundation.h>
|
||||
#import <Metal/Metal.h>
|
||||
#import "ImGuiRenderer.h"
|
||||
#import "ImGuiPlots.h"
|
||||
|
||||
// This will fully disable ImGui by compiling it out. The in-app setting enableGraphs
|
||||
// will also remove all ImGui overhead.
|
||||
//#define IMGUI_DISABLE
|
||||
|
||||
// If this is defined, ImGui takes over all touch control while using the app.
|
||||
// This is only needed if you want to enable and interact with the demo. I'm sure it's possible
|
||||
// to pass-through touch events when touching non-ImGui portions of the screen
|
||||
// but I couldn't figure it out. I did not try to get controller support working.
|
||||
//#define IMGUI_STEALS_TOUCH
|
||||
|
||||
#import "imgui.h"
|
||||
#import "imgui_impl_metal.h"
|
||||
|
||||
// ImGui code needs to live in this file because it's an Objective-C++ class, and can call C++ code.
|
||||
|
||||
@implementation ImGuiRenderer
|
||||
|
||||
-(nonnull instancetype)initWithFrame:(CGRect)bounds
|
||||
streamFps:(int)streamFps
|
||||
enableGraphs:(BOOL)enableGraphs
|
||||
graphOpacity:(int)graphOpacity
|
||||
{
|
||||
self = [super init];
|
||||
|
||||
_enableGraphs = enableGraphs;
|
||||
_graphOpacity = (float)(graphOpacity / 100.0);
|
||||
_bounds = bounds;
|
||||
_device = MTLCreateSystemDefaultDevice();
|
||||
_commandQueue = [_device newCommandQueue];
|
||||
|
||||
_plots = [ImGuiPlots sharedInstance].plots;
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
-(void)ImGui_Init {
|
||||
// self-contained startup for ImGui so it can be dynamically toggled easily
|
||||
|
||||
#if !defined(IMGUI_DISABLE)
|
||||
if (_enableGraphs && !_imguiRunning) {
|
||||
IMGUI_CHECKVERSION();
|
||||
ImGui::CreateContext();
|
||||
ImGuiIO& io = ImGui::GetIO(); (void)io;
|
||||
// io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard; // Enable Keyboard Controls
|
||||
// io.ConfigFlags |= ImGuiConfigFlags_NavEnableGamepad; // Enable Gamepad Controls
|
||||
|
||||
ImGui::StyleColorsDark();
|
||||
|
||||
ImGui_ImplMetal_Init(_device);
|
||||
_imguiRunning = YES;
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
-(void)ImGui_Deinit {
|
||||
if (_imguiRunning) {
|
||||
#if !defined(IMGUI_DISABLE)
|
||||
ImGui_ImplMetal_Shutdown();
|
||||
ImGui::DestroyContext();
|
||||
#endif
|
||||
_imguiRunning = NO;
|
||||
}
|
||||
}
|
||||
|
||||
-(MTKView *)mtkView
|
||||
{
|
||||
return (MTKView *)self.view;
|
||||
}
|
||||
|
||||
-(void)loadView
|
||||
{
|
||||
self.view = [[MTKView alloc] initWithFrame:self.bounds];
|
||||
}
|
||||
|
||||
-(void)viewDidLoad
|
||||
{
|
||||
[super viewDidLoad];
|
||||
|
||||
self.mtkView.device = self.device;
|
||||
self.mtkView.delegate = self;
|
||||
self.mtkView.preferredFramesPerSecond = 60; // ImGui overlay will always render at this rate
|
||||
self.mtkView.opaque = NO;
|
||||
self.mtkView.enableSetNeedsDisplay = NO;
|
||||
|
||||
if (_enableGraphs) {
|
||||
[self ImGui_Init];
|
||||
self.mtkView.paused = NO;
|
||||
}
|
||||
else {
|
||||
self.mtkView.paused = YES;
|
||||
}
|
||||
}
|
||||
|
||||
// start & stop are used to show/hide graphs when swiping stats in or out
|
||||
-(void)start {
|
||||
[self ImGui_Init];
|
||||
if (_enableGraphs) {
|
||||
self.mtkView.paused = NO;
|
||||
}
|
||||
}
|
||||
|
||||
-(void)show {
|
||||
[self.mtkView setHidden:NO];
|
||||
}
|
||||
|
||||
-(void)hide {
|
||||
[self.mtkView setHidden:YES];
|
||||
}
|
||||
|
||||
-(void)stop {
|
||||
self.mtkView.paused = YES;
|
||||
[self ImGui_Deinit];
|
||||
}
|
||||
|
||||
// Only called when mtkView.paused is false
|
||||
- (void)drawInMTKView:(MTKView *)view
|
||||
{
|
||||
#if !defined(IMGUI_DISABLE)
|
||||
ImGuiIO &io = ImGui::GetIO();
|
||||
io.DisplaySize.x = view.bounds.size.width;
|
||||
io.DisplaySize.y = view.bounds.size.height;
|
||||
|
||||
CGFloat framebufferScale = view.window.screen.scale ?: UIScreen.mainScreen.scale;
|
||||
io.DisplayFramebufferScale = ImVec2(framebufferScale, framebufferScale);
|
||||
|
||||
id<MTLCommandBuffer> commandBuffer = [self.commandQueue commandBuffer];
|
||||
|
||||
MTLRenderPassDescriptor* renderPassDescriptor = view.currentRenderPassDescriptor;
|
||||
if (renderPassDescriptor == nil) {
|
||||
[commandBuffer commit];
|
||||
return;
|
||||
}
|
||||
|
||||
// Start the Dear ImGui frame
|
||||
ImGui_ImplMetal_NewFrame(renderPassDescriptor);
|
||||
ImGui::NewFrame();
|
||||
|
||||
// 1. Show the big demo window (Most of the sample code is in ImGui::ShowDemoWindow()! You can browse its code to learn more about Dear ImGui!).
|
||||
static bool show_demo_window = false;
|
||||
if (show_demo_window)
|
||||
ImGui::ShowDemoWindow(&show_demo_window);
|
||||
|
||||
// Custom Moonlight stuff goes here
|
||||
[self drawStatsGraphs];
|
||||
|
||||
// Rendering
|
||||
ImGui::Render();
|
||||
ImDrawData* draw_data = ImGui::GetDrawData();
|
||||
|
||||
// This looks silly when clear_color is all zeros but the original example uses this method to tint or make transparent the rest of the viewport
|
||||
static ImVec4 clear_color = ImVec4(0, 0, 0, 0);
|
||||
renderPassDescriptor.colorAttachments[0].clearColor = MTLClearColorMake(clear_color.x * clear_color.w, clear_color.y * clear_color.w, clear_color.z * clear_color.w, clear_color.w);
|
||||
|
||||
id <MTLRenderCommandEncoder> renderEncoder = [commandBuffer renderCommandEncoderWithDescriptor:renderPassDescriptor];
|
||||
[renderEncoder pushDebugGroup:@"Dear ImGui rendering"];
|
||||
ImGui_ImplMetal_RenderDrawData(draw_data, commandBuffer, renderEncoder);
|
||||
[renderEncoder popDebugGroup];
|
||||
[renderEncoder endEncoding];
|
||||
|
||||
// Present
|
||||
#if TARGET_OS_SIMULATOR
|
||||
[commandBuffer presentDrawable:view.currentDrawable];
|
||||
#else
|
||||
[commandBuffer presentDrawable:view.currentDrawable afterMinimumDuration:1.0 / view.preferredFramesPerSecond];
|
||||
#endif
|
||||
[commandBuffer commit];
|
||||
#endif
|
||||
}
|
||||
|
||||
- (void)mtkView:(MTKView *)view drawableSizeWillChange:(CGSize)size
|
||||
{
|
||||
}
|
||||
|
||||
- (void)viewDidDisappear:(BOOL)animated
|
||||
{
|
||||
[super viewDidDisappear:animated];
|
||||
|
||||
[self ImGui_Deinit];
|
||||
}
|
||||
|
||||
//-----------------------------------------------------------------------------------
|
||||
// Input processing
|
||||
//-----------------------------------------------------------------------------------
|
||||
|
||||
// This touch mapping is super cheesy/hacky. We treat any touch on the screen
|
||||
// as if it were a depressed left mouse button, and we don't bother handling
|
||||
// multitouch correctly at all. This causes the "cursor" to behave very erratically
|
||||
// when there are multiple active touches. But for demo purposes, single-touch
|
||||
// interaction actually works surprisingly well.
|
||||
#if !defined(IMGUI_DISABLE)
|
||||
# if defined(IMGUI_STEALS_TOUCH)
|
||||
-(BOOL)updateIOWithTouchEvent:(UIEvent *)event
|
||||
{
|
||||
UITouch *anyTouch = event.allTouches.anyObject;
|
||||
CGPoint touchLocation = [anyTouch locationInView:self.view];
|
||||
ImGuiIO &io = ImGui::GetIO();
|
||||
io.AddMouseSourceEvent(ImGuiMouseSource_TouchScreen);
|
||||
io.AddMousePosEvent(touchLocation.x, touchLocation.y);
|
||||
|
||||
BOOL hasActiveTouch = NO;
|
||||
for (UITouch *touch in event.allTouches)
|
||||
{
|
||||
if (touch.phase != UITouchPhaseEnded && touch.phase != UITouchPhaseCancelled)
|
||||
{
|
||||
hasActiveTouch = YES;
|
||||
break;
|
||||
}
|
||||
}
|
||||
io.AddMouseButtonEvent(0, hasActiveTouch);
|
||||
return YES;
|
||||
}
|
||||
# endif
|
||||
#endif
|
||||
|
||||
#if !defined(IMGUI_DISABLE)
|
||||
# if defined(IMGUI_STEALS_TOUCH)
|
||||
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event { [self updateIOWithTouchEvent:event]; }
|
||||
-(void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event { [self updateIOWithTouchEvent:event]; }
|
||||
-(void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event { [self updateIOWithTouchEvent:event]; }
|
||||
-(void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event { [self updateIOWithTouchEvent:event]; }
|
||||
# endif
|
||||
#endif
|
||||
|
||||
/// Stats Graphs, we can still track data this way even with ImGui disabled
|
||||
|
||||
- (void) observeFloat:(int)plotId value:(CFTimeInterval)value {
|
||||
[self.plots[plotId].buffer addValue:(float)value];
|
||||
}
|
||||
|
||||
- (void) observeFloatReturnMetrics:(int)plotId value:(CFTimeInterval)value plotMetrics:(PlotMetrics *)plotMetrics {
|
||||
[self.plots[plotId].buffer addValue:(float)value];
|
||||
if (plotMetrics != nil) {
|
||||
[self.plots[plotId].buffer copyMetrics:plotMetrics];
|
||||
}
|
||||
}
|
||||
|
||||
#if !defined(IMGUI_DISABLE)
|
||||
inline static float getValue(void *buffer, int idx) {
|
||||
float *fbuffer = (float *)buffer;
|
||||
float v = fbuffer[idx];
|
||||
// clip the top of frametime graphs so they're less ugly
|
||||
if (v > 50)
|
||||
v = 49.9;
|
||||
|
||||
return v;
|
||||
}
|
||||
|
||||
- (void) drawStatsGraphs {
|
||||
// we malloc a buffer for frametimes once and reuse it
|
||||
static float * buffers[8] = {
|
||||
(float *)malloc(sizeof(float) * 512),
|
||||
(float *)malloc(sizeof(float) * 512),
|
||||
(float *)malloc(sizeof(float) * 512),
|
||||
(float *)malloc(sizeof(float) * 512),
|
||||
(float *)malloc(sizeof(float) * 512),
|
||||
(float *)malloc(sizeof(float) * 512),
|
||||
(float *)malloc(sizeof(float) * 512),
|
||||
(float *)malloc(sizeof(float) * 512)
|
||||
};
|
||||
|
||||
ImGuiIO &io = ImGui::GetIO();
|
||||
|
||||
// Try to make the graphs suck less on common device sizes
|
||||
float graphW = 0.0f;
|
||||
float graphH = 0.0f;
|
||||
switch ((int)io.DisplaySize.x) {
|
||||
case 1920: // ATV 4K 1920x1080 2x
|
||||
graphW = 525.0f; graphH = 80.0f; break;
|
||||
case 1376: // iPad Pro 1376x1032 2x
|
||||
graphW = 450.0f; graphH = 45.0f; break;
|
||||
case 1366: // iPad Air 1366x1024 2x
|
||||
graphW = 446.0f; graphW = 44.0f; break;
|
||||
case 1194: // Vision Pro (iPad mode) 1194x834 2x
|
||||
graphW = 379.0f; graphH = 45.0f; break;
|
||||
case 1133: // iPad Mini 1133x744 2x
|
||||
graphW = 360.0f; graphH = 45.0f; break;
|
||||
case 874: // iPhone 16 Pro 874x402 3x
|
||||
graphW = 275.0f; graphH = 45.0f; break;
|
||||
// TODO:
|
||||
default:
|
||||
float x = io.DisplaySize.x;
|
||||
float y = io.DisplaySize.y;
|
||||
if (io.DisplayFramebufferScale.x > 2.0f) {
|
||||
x = x * io.DisplayFramebufferScale.x / 2.0f;
|
||||
y = y * io.DisplayFramebufferScale.y / 2.0f;
|
||||
}
|
||||
graphW = x * 0.327f;
|
||||
graphH = y * 0.044f;
|
||||
}
|
||||
|
||||
LogOnce(LOG_I, @"Drawing graphs of size %.1f x %.1f in viewport %.1f x %.1f scale %.0f,%.0f using opacity %.2f",
|
||||
graphW, graphH,
|
||||
io.DisplaySize.x, io.DisplaySize.y,
|
||||
io.DisplayFramebufferScale.x, io.DisplayFramebufferScale.y,
|
||||
_graphOpacity);
|
||||
|
||||
// Left side - 2 graphs
|
||||
ImVec2 windowSize(graphW, (graphH * 4));
|
||||
ImVec2 windowPos(10.0f, 10.0f);
|
||||
ImGui::SetNextWindowPos(windowPos, ImGuiCond_Always, ImVec2(0.0f, 0.0f)); // pivot (0,0) = top-left
|
||||
ImGui::SetNextWindowSize(windowSize, ImGuiCond_Always);
|
||||
ImGuiWindowFlags flags = ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoNavFocus | ImGuiWindowFlags_NoBackground |
|
||||
ImGuiWindowFlags_NoSavedSettings;
|
||||
ImGui::Begin("##StatsLeft", nullptr, flags);
|
||||
|
||||
// Dimensions of each graph
|
||||
ImVec2 avail = ImGui::GetContentRegionAvail();
|
||||
float fullW = avail.x;
|
||||
|
||||
// First 2 on left
|
||||
for (int i = 0; i < PlotCount; i++) {
|
||||
if (self.plots[i].side != PLOT_LEFT) continue;
|
||||
|
||||
float minY, maxY;
|
||||
int countF = [self.plots[i].buffer copyValuesIntoBuffer:buffers[i] min:&minY max:&maxY];
|
||||
float avgF = [self.plots[i].buffer averageValue];
|
||||
if (!countF) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Ugly, but can't get ImPlot to build for iOS
|
||||
char label[64];
|
||||
switch (self.plots[i].labelType) {
|
||||
case PLOT_LABEL_MIN_MAX_AVG:
|
||||
sprintf(label, "%s %.1f/%.1f/%.1f %s", self.plots[i].title, minY, maxY, avgF, self.plots[i].unit);
|
||||
break;
|
||||
case PLOT_LABEL_MIN_MAX_AVG_INT:
|
||||
sprintf(label, "%s %d/%d/%.1f %s", self.plots[i].title, (int)minY, (int)maxY, avgF, self.plots[i].unit);
|
||||
break;
|
||||
case PLOT_LABEL_TOTAL_INT:
|
||||
sprintf(label, "%s %d %s", self.plots[i].title, (int)[self.plots[i].buffer total], self.plots[i].unit);
|
||||
break;
|
||||
}
|
||||
float scaleMin = FLT_MAX;
|
||||
float scaleMax = FLT_MAX;
|
||||
if (self.plots[i].scaleTarget) {
|
||||
// optionally center the graph on a target such as the ideal frametime
|
||||
float ideal = (float)self.plots[i].scaleTarget;
|
||||
scaleMin = ideal - (2 * ideal);
|
||||
scaleMax = ideal + (2 * ideal);
|
||||
}
|
||||
if (self.plots[i].scaleMin)
|
||||
scaleMin = self.plots[i].scaleMin;
|
||||
if (self.plots[i].scaleMax)
|
||||
scaleMax = self.plots[i].scaleMax;
|
||||
ImGui::PushID(i);
|
||||
ImGui::PushStyleColor(ImGuiCol_FrameBg, ImVec4(0.16f, 0.29f, 0.48f, _graphOpacity)); // dark
|
||||
//ImGui::PushStyleColor(ImGuiCol_FrameBg, ImVec4(0.43f, 0.43f, 0.43f, 0.39f)); // classic
|
||||
ImGui::PushStyleColor(ImGuiCol_PlotLines, ImVec4(0.0f, 1.0f, 0.0f, 1.0f)); // green
|
||||
ImGui::PlotLines("##xx", buffers[i], countF, 0, (countF > 0 ? label : "no data"), scaleMin, scaleMax, ImVec2(fullW, graphH));
|
||||
ImGui::PopStyleColor(2);
|
||||
ImGui::PopID();
|
||||
}
|
||||
ImGui::End();
|
||||
|
||||
// Right side - 2 graphs
|
||||
windowPos = ImVec2(io.DisplaySize.x - 10.0f, 10.0f); // 10px margin
|
||||
ImGui::SetNextWindowPos(windowPos, ImGuiCond_Always, ImVec2(1.0f, 0.0f)); // pivot (1,0) = top-right
|
||||
ImGui::SetNextWindowSize(windowSize, ImGuiCond_Always);
|
||||
flags = ImGuiWindowFlags_NoDecoration |
|
||||
ImGuiWindowFlags_NoMove |
|
||||
ImGuiWindowFlags_NoNavFocus |
|
||||
ImGuiWindowFlags_NoBackground |
|
||||
ImGuiWindowFlags_NoSavedSettings;
|
||||
ImGui::Begin("##StatsRight", nullptr, flags);
|
||||
|
||||
for (int i = 2; i < PlotCount; i++) {
|
||||
if (self.plots[i].side != PLOT_RIGHT) continue;
|
||||
|
||||
float minY, maxY;
|
||||
int countF = [self.plots[i].buffer copyValuesIntoBuffer:buffers[i] min:&minY max:&maxY];
|
||||
float avgF = [self.plots[i].buffer averageValue];
|
||||
if (!countF) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Ugly, but can't get ImPlot to build for iOS
|
||||
char label[64];
|
||||
switch (self.plots[i].labelType) {
|
||||
case PLOT_LABEL_MIN_MAX_AVG:
|
||||
sprintf(label, "%s %.1f/%.1f/%.1f %s", self.plots[i].title, minY, maxY, avgF, self.plots[i].unit);
|
||||
break;
|
||||
case PLOT_LABEL_MIN_MAX_AVG_INT:
|
||||
sprintf(label, "%s %d/%d/%.1f %s", self.plots[i].title, (int)minY, (int)maxY, avgF, self.plots[i].unit);
|
||||
break;
|
||||
case PLOT_LABEL_TOTAL_INT:
|
||||
sprintf(label, "%s %d %s", self.plots[i].title, (int)[self.plots[i].buffer total], self.plots[i].unit);
|
||||
break;
|
||||
}
|
||||
float scaleMin = FLT_MAX;
|
||||
float scaleMax = FLT_MAX;
|
||||
if (self.plots[i].scaleTarget) {
|
||||
// optionally center the graph on a target such as the ideal frametime
|
||||
float ideal = (float)self.plots[i].scaleTarget;
|
||||
scaleMin = ideal - (2 * ideal);
|
||||
scaleMax = ideal + (2 * ideal);
|
||||
}
|
||||
if (self.plots[i].scaleMin)
|
||||
scaleMin = self.plots[i].scaleMin;
|
||||
if (self.plots[i].scaleMax)
|
||||
scaleMax = self.plots[i].scaleMax;
|
||||
ImGui::PushID(i);
|
||||
ImGui::PushStyleColor(ImGuiCol_FrameBg, ImVec4(0.16f, 0.29f, 0.48f, _graphOpacity)); // dark
|
||||
ImGui::PushStyleColor(ImGuiCol_PlotLines, ImVec4(0.0f, 1.0f, 0.0f, 1.0f)); // green
|
||||
if (i == PLOT_FRAMETIME || i == PLOT_HOST_FRAMETIME) {
|
||||
// getValue() clips at max 50
|
||||
ImGui::PlotLines("##xx", getValue, buffers[i], countF, 0, (countF > 0 ? label : "no data"), scaleMin, scaleMax, ImVec2(fullW, graphH));
|
||||
} else {
|
||||
ImGui::PlotLines("##xx", buffers[i], countF, 0, (countF > 0 ? label : "no data"), scaleMin, scaleMax, ImVec2(fullW, graphH));
|
||||
}
|
||||
ImGui::PopStyleColor(2);
|
||||
ImGui::PopID();
|
||||
}
|
||||
ImGui::End();
|
||||
|
||||
#endif
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
Whitespace-only changes.
Whitespace-only changes.
@@ -0,0 +1,37 @@
|
||||
//
|
||||
// StreamView.h
|
||||
// Moonlight
|
||||
//
|
||||
// Created by Cameron Gutman on 10/19/14.
|
||||
// Copyright (c) 2014 Moonlight Stream. All rights reserved.
|
||||
//
|
||||
|
||||
#import "ControllerSupport.h"
|
||||
#import "OnScreenControls.h"
|
||||
#import "Moonlight-Swift.h"
|
||||
#import "StreamConfiguration.h"
|
||||
|
||||
@protocol UserInteractionDelegate <NSObject>
|
||||
|
||||
- (void) userInteractionBegan;
|
||||
- (void) userInteractionEnded;
|
||||
|
||||
@end
|
||||
|
||||
#if TARGET_OS_TV
|
||||
@interface StreamView : UIView <X1KitMouseDelegate, UITextFieldDelegate>
|
||||
#else
|
||||
@interface StreamView : UIView <X1KitMouseDelegate, UITextFieldDelegate, UIPointerInteractionDelegate>
|
||||
#endif
|
||||
|
||||
- (void) setupStreamView:(ControllerSupport*)controllerSupport
|
||||
interactionDelegate:(id<UserInteractionDelegate>)interactionDelegate
|
||||
config:(StreamConfiguration*)streamConfig;
|
||||
- (void) showOnScreenControls;
|
||||
- (OnScreenControlsLevel) getCurrentOscState;
|
||||
|
||||
#if !TARGET_OS_TV
|
||||
- (void) updateCursorLocation:(CGPoint)location isMouse:(BOOL)isMouse;
|
||||
#endif
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,13 @@
|
||||
// When enabled, rendering occurs on the main application thread.
|
||||
// This can make responding to UI events during redraw simpler
|
||||
// to manage because UI calls usually need to occur on the main thread.
|
||||
// When disabled, rendering occurs on a background thread, allowing
|
||||
// the UI to respond more quickly in some cases because events can
|
||||
// process asynchronously from potentially CPU-intensive rendering code.
|
||||
#define RENDER_ON_MAIN_THREAD 0
|
||||
|
||||
// When enabled, the drawable's size updates automatically whenever
|
||||
// the view resizes. When disabled, you can update the drawable's
|
||||
// size explicitly outside the view class.
|
||||
#define AUTOMATICALLY_RESIZE 1
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
#import <QuartzCore/CAMetalLayer.h>
|
||||
#import <QuartzCore/QuartzCore.h>
|
||||
#import "ConnectionCallbacks.h"
|
||||
#import "Frame.h"
|
||||
#import "Plot.h"
|
||||
|
||||
@interface MetalVideoRenderer : NSObject
|
||||
|
||||
@property (atomic) CFTimeInterval averageGPUTime;
|
||||
@property (nonatomic) NSUInteger sampleCount;
|
||||
@property (nonatomic) MTLPixelFormat colorPixelFormat;
|
||||
@property (nonatomic, nonnull) CGColorSpaceRef colorspace;
|
||||
@property (nonatomic) id<CAMetalDrawable> _Nullable nextDrawable;
|
||||
|
||||
- (nonnull instancetype)initWithMetalDevice:(nonnull id<MTLDevice>)device
|
||||
drawablePixelFormat:(MTLPixelFormat)drawablePixelFormat
|
||||
framerate:(float)framerate;
|
||||
- (void)renderFrame:(nonnull Frame *)frame toLayer:(nonnull CAMetalLayer *)layer;
|
||||
- (void)waitToRenderTo:(nonnull CAMetalLayer *)layer;
|
||||
- (void)drawableResize:(CGSize)drawableSize;
|
||||
- (void)plotFrametime:(CFTimeInterval)presentedTime;
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,524 @@
|
||||
#import "MetalVideoRenderer.h"
|
||||
#import <CoreVideo/CoreVideo.h>
|
||||
#import <Metal/Metal.h>
|
||||
#import <MetalKit/MetalKit.h>
|
||||
#import "ImGuiPlots.h"
|
||||
|
||||
#include <Limelight.h>
|
||||
|
||||
#define MAX_VIDEO_PLANES 3
|
||||
|
||||
struct CscParams {
|
||||
vector_float3 matrix[3];
|
||||
vector_float3 offsets;
|
||||
};
|
||||
|
||||
struct ParamBuffer {
|
||||
struct CscParams cscParams;
|
||||
};
|
||||
|
||||
static const struct CscParams k_CscParams_Bt601Lim = {
|
||||
// CSC Matrix
|
||||
{{1.1644f, 0.0f, 1.5960f}, {1.1644f, -0.3917f, -0.8129f}, {1.1644f, 2.0172f, 0.0f}},
|
||||
|
||||
// Offsets
|
||||
{16.0f / 255.0f, 128.0f / 255.0f, 128.0f / 255.0f},
|
||||
};
|
||||
static const struct CscParams k_CscParams_Bt601Full = {
|
||||
{
|
||||
{1.0f, 0.0f, 1.4020f},
|
||||
{1.0f, -0.3441f, -0.7141f},
|
||||
{1.0f, 1.7720f, 0.0f},
|
||||
},
|
||||
{0.0f, 128.0f / 255.0f, 128.0f / 255.0f},
|
||||
};
|
||||
static const struct CscParams k_CscParams_Bt709Lim = {
|
||||
{
|
||||
{1.1644f, 0.0f, 1.7927f},
|
||||
{1.1644f, -0.2132f, -0.5329f},
|
||||
{1.1644f, 2.1124f, 0.0f},
|
||||
},
|
||||
{16.0f / 255.0f, 128.0f / 255.0f, 128.0f / 255.0f},
|
||||
};
|
||||
static const struct CscParams k_CscParams_Bt709Full = {
|
||||
{
|
||||
{1.0f, 0.0f, 1.5748f},
|
||||
{1.0f, -0.1873f, -0.4681f},
|
||||
{1.0f, 1.8556f, 0.0f},
|
||||
},
|
||||
{0.0f, 128.0f / 255.0f, 128.0f / 255.0f},
|
||||
};
|
||||
static const struct CscParams k_CscParams_Bt2020Lim = {
|
||||
{
|
||||
{1.1644f, 0.0f, 1.6781f},
|
||||
{1.1644f, -0.1874f, -0.6505f},
|
||||
{1.1644f, 2.1418f, 0.0f},
|
||||
},
|
||||
{16.0f / 255.0f, 128.0f / 255.0f, 128.0f / 255.0f},
|
||||
};
|
||||
static const struct CscParams k_CscParams_Bt2020Full = {
|
||||
{
|
||||
{1.0f, 0.0f, 1.4746f},
|
||||
{1.0f, -0.1646f, -0.5714f},
|
||||
{1.0f, 1.8814f, 0.0f},
|
||||
},
|
||||
{0.0f, 128.0f / 255.0f, 128.0f / 255.0f},
|
||||
};
|
||||
|
||||
struct Vertex {
|
||||
vector_float4 position;
|
||||
vector_float2 texCoord;
|
||||
};
|
||||
|
||||
static const NSUInteger MaxFramesInFlight = 3;
|
||||
|
||||
@implementation MetalVideoRenderer {
|
||||
dispatch_queue_t _sq;
|
||||
id<MTLDevice> _device;
|
||||
float _framerate;
|
||||
id<ConnectionCallbacks> _callbacks;
|
||||
id<MTLCommandQueue> _commandQueue;
|
||||
id<MTLLibrary> _shaderLibrary;
|
||||
id<MTLRenderPipelineState> _videoPipelineState;
|
||||
MTLRenderPassDescriptor *_renderPassDescriptor;
|
||||
id<MTLTexture> _videoTexture;
|
||||
CVMetalTextureCacheRef _textureCache;
|
||||
CVMetalTextureRef _cvMetalTextures[MAX_VIDEO_PLANES];
|
||||
|
||||
int _lastColorSpace;
|
||||
BOOL _lastFullRange;
|
||||
size_t _lastFrameWidth;
|
||||
size_t _lastFrameHeight;
|
||||
size_t _lastDrawableWidth;
|
||||
size_t _lastDrawableHeight;
|
||||
id<MTLBuffer> _CscParamsBuffer;
|
||||
id<MTLBuffer> _VideoVertexBuffer;
|
||||
CFTimeInterval _lastPresented;
|
||||
|
||||
// https://developer.apple.com/documentation/metal/synchronizing-cpu-and-gpu-work?language=objc
|
||||
dispatch_semaphore_t _inFlightSemaphore;
|
||||
}
|
||||
|
||||
- (instancetype)initWithMetalDevice:(id<MTLDevice>)device drawablePixelFormat:(MTLPixelFormat)drawablePixelFormat framerate:(float)framerate {
|
||||
self = [super init];
|
||||
if (self) {
|
||||
_sq = dispatch_queue_create("com.moonlight.MetalVideoRenderer",
|
||||
dispatch_queue_attr_make_with_qos_class(DISPATCH_QUEUE_SERIAL, QOS_CLASS_USER_INTERACTIVE, 0));
|
||||
_averageGPUTime = (1.0f / framerate) / 2;
|
||||
_device = device;
|
||||
_nextDrawable = nil;
|
||||
_colorPixelFormat = MTLPixelFormatBGR10A2Unorm;
|
||||
_colorspace = CGColorSpaceCreateWithName(kCGColorSpaceITUR_2100_PQ);
|
||||
_framerate = framerate;
|
||||
_commandQueue = [_device newCommandQueue];
|
||||
_lastColorSpace = -1;
|
||||
_lastFullRange = NO;
|
||||
_lastPresented = 0;
|
||||
_inFlightSemaphore = dispatch_semaphore_create(MaxFramesInFlight);
|
||||
|
||||
CFStringRef keys[1] = {kCVMetalTextureUsage};
|
||||
NSUInteger values[1] = {MTLTextureUsageShaderRead};
|
||||
CFDictionaryRef cacheAttributes = CFDictionaryCreate(kCFAllocatorDefault, (const void **)keys, (const void **)values, 1, NULL, NULL);
|
||||
CVMetalTextureCacheCreate(kCFAllocatorDefault, cacheAttributes, _device, NULL, &_textureCache);
|
||||
CFRelease(cacheAttributes);
|
||||
|
||||
_renderPassDescriptor = [MTLRenderPassDescriptor new];
|
||||
_renderPassDescriptor.colorAttachments[0].loadAction = MTLLoadActionClear;
|
||||
_renderPassDescriptor.colorAttachments[0].clearColor = MTLClearColorMake(0, 0, 0, 0);
|
||||
_renderPassDescriptor.colorAttachments[0].storeAction = MTLStoreActionStore;
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
#if !TARGET_OS_TV
|
||||
- (void)applyEDRFromFrame:(Frame *)frame toLayer:(CAMetalLayer *)layer {
|
||||
CFDictionaryRef ext = [frame getFormatDescExtensions];
|
||||
|
||||
Log(LOG_I, @"ext: %@", ext);
|
||||
|
||||
CFDataRef masteringData = CFDictionaryGetValue(ext, kCMFormatDescriptionExtension_MasteringDisplayColorVolume);
|
||||
CFDataRef contentDataRef = CFDictionaryGetValue(ext, kCMFormatDescriptionExtension_ContentLightLevelInfo);
|
||||
|
||||
if (masteringData) {
|
||||
Log(LOG_I, @"ext MDCV %@", masteringData);
|
||||
}
|
||||
if (contentDataRef) {
|
||||
Log(LOG_I, @"ext CLLI %@", contentDataRef);
|
||||
}
|
||||
|
||||
if (masteringData && CFDataGetLength(masteringData) == 24 && contentDataRef && CFDataGetLength(contentDataRef) == 4) {
|
||||
NSData *displayData = (__bridge NSData *)masteringData;
|
||||
NSData *contentData = (__bridge NSData *)contentDataRef;
|
||||
|
||||
layer.wantsExtendedDynamicRangeContent = YES;
|
||||
layer.pixelFormat = MTLPixelFormatRGBA16Float;
|
||||
CFStringRef name = kCGColorSpaceExtendedLinearITUR_2020;
|
||||
CGColorSpaceRef colorspace = CGColorSpaceCreateWithName(name);
|
||||
layer.colorspace = colorspace;
|
||||
|
||||
layer.EDRMetadata = [CAEDRMetadata HDR10MetadataWithDisplayInfo:displayData contentInfo:contentData opticalOutputScale:100.0f];
|
||||
|
||||
Log(LOG_I, @"EDRMetadata set from MDCV %@ and CLLI %@", displayData, contentData);
|
||||
} else {
|
||||
layer.wantsExtendedDynamicRangeContent = YES;
|
||||
layer.pixelFormat = MTLPixelFormatRGBA16Float;
|
||||
CFStringRef name = kCGColorSpaceExtendedLinearITUR_2020;
|
||||
CGColorSpaceRef colorspace = CGColorSpaceCreateWithName(name);
|
||||
layer.colorspace = colorspace;
|
||||
|
||||
layer.EDRMetadata = [CAEDRMetadata HDR10MetadataWithMinLuminance:0.0005f maxLuminance:1000.0f opticalOutputScale:100.0f];
|
||||
|
||||
Log(LOG_I, @"EDRMetadata set for 1000 nits");
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
- (int)getFrameColorspaceAndRange:(Frame *)frame isFullRange:(BOOL *)isFullRange {
|
||||
CFDictionaryRef ext = [frame getFormatDescExtensions];
|
||||
|
||||
// FQLog(LOG_I, @"%@", ext);
|
||||
|
||||
// Full Range boolean
|
||||
CFBooleanRef fullRangeRef = CFDictionaryGetValue(ext, kCMFormatDescriptionExtension_FullRangeVideo);
|
||||
*isFullRange = NO;
|
||||
if (fullRangeRef && CFGetTypeID(fullRangeRef) == CFBooleanGetTypeID()) {
|
||||
*isFullRange = CFBooleanGetValue(fullRangeRef);
|
||||
}
|
||||
|
||||
// Colorspace
|
||||
CFStringRef frame_color = CFDictionaryGetValue(ext, kCVImageBufferColorPrimariesKey);
|
||||
if (CFEqual(frame_color, kCVImageBufferColorPrimaries_ITU_R_709_2)) {
|
||||
return COLORSPACE_REC_709;
|
||||
} else if (CFEqual(frame_color, kCVImageBufferColorPrimaries_ITU_R_2020)) {
|
||||
return COLORSPACE_REC_2020;
|
||||
}
|
||||
return COLORSPACE_REC_601;
|
||||
}
|
||||
|
||||
- (BOOL)updateColorSpaceForFrame:(Frame *)frame toLayer:(CAMetalLayer *)layer layerDidChange:(BOOL *)layerDidChange {
|
||||
BOOL fullRange = NO;
|
||||
int colorspace = [self getFrameColorspaceAndRange:frame isFullRange:&fullRange];
|
||||
if (colorspace != _lastColorSpace || fullRange != _lastFullRange) {
|
||||
CGColorSpaceRef newColorSpace = nil;
|
||||
MTLPixelFormat newPixelFormat = layer.pixelFormat;
|
||||
BOOL isHDR = NO;
|
||||
struct ParamBuffer paramBuffer;
|
||||
|
||||
switch (colorspace) {
|
||||
case COLORSPACE_REC_709:
|
||||
newColorSpace = CGColorSpaceCreateWithName(kCGColorSpaceITUR_709);
|
||||
newPixelFormat = MTLPixelFormatBGRA8Unorm;
|
||||
paramBuffer.cscParams = (fullRange ? k_CscParams_Bt709Full : k_CscParams_Bt709Lim);
|
||||
break;
|
||||
case COLORSPACE_REC_2020: {
|
||||
CFDictionaryRef ext = [frame getFormatDescExtensions];
|
||||
CFStringRef frame_trc = CFDictionaryGetValue(ext, kCVImageBufferTransferFunctionKey);
|
||||
if (CFEqual(frame_trc, kCVImageBufferTransferFunction_SMPTE_ST_2084_PQ)) {
|
||||
isHDR = YES;
|
||||
newColorSpace = CGColorSpaceCreateWithName(kCGColorSpaceITUR_2100_PQ);
|
||||
newPixelFormat = MTLPixelFormatBGR10A2Unorm;
|
||||
} else {
|
||||
// SDR 2020
|
||||
newColorSpace = CGColorSpaceCreateWithName(kCGColorSpaceITUR_2020);
|
||||
newPixelFormat = MTLPixelFormatBGR10A2Unorm;
|
||||
}
|
||||
paramBuffer.cscParams = (fullRange ? k_CscParams_Bt2020Full : k_CscParams_Bt2020Lim);
|
||||
break;
|
||||
}
|
||||
case COLORSPACE_REC_601:
|
||||
newColorSpace = CGColorSpaceCreateWithName(kCGColorSpaceSRGB);
|
||||
newPixelFormat = MTLPixelFormatBGRA8Unorm;
|
||||
paramBuffer.cscParams = (fullRange ? k_CscParams_Bt601Full : k_CscParams_Bt601Lim);
|
||||
}
|
||||
|
||||
// The CAMetalLayer retains the CGColorSpace
|
||||
if (newColorSpace || newPixelFormat != layer.pixelFormat) {
|
||||
*layerDidChange = YES;
|
||||
if (newColorSpace) {
|
||||
Log(LOG_I,
|
||||
@"Frame colorspace %@ - changing MetalLayer's colorspace to %@",
|
||||
colorspace == COLORSPACE_REC_709 ? @"REC_709"
|
||||
: colorspace == COLORSPACE_REC_2020 ? @"REC_2020"
|
||||
: colorspace == COLORSPACE_REC_601 ? @"REC_601 (sRGB)"
|
||||
: [NSString stringWithFormat:@"Unknown: %d", colorspace],
|
||||
newColorSpace);
|
||||
}
|
||||
if (newPixelFormat != layer.pixelFormat) {
|
||||
Log(LOG_I,
|
||||
@"Frame pixel format %@ - changing MetalLayer's pixel format to %@",
|
||||
layer.pixelFormat == MTLPixelFormatBGRA8Unorm ? @"MTLPixelFormatBGRA8Unorm"
|
||||
: layer.pixelFormat == MTLPixelFormatBGR10A2Unorm ? @"MTLPixelFormatBGR10A2Unorm"
|
||||
: [NSString stringWithFormat:@"Unknown: %lu", layer.pixelFormat],
|
||||
newPixelFormat == MTLPixelFormatBGRA8Unorm ? @"MTLPixelFormatBGRA8Unorm"
|
||||
: newPixelFormat == MTLPixelFormatBGR10A2Unorm ? @"MTLPixelFormatBGR10A2Unorm"
|
||||
: [NSString stringWithFormat:@"Unknown: %lu", (unsigned long)layer.pixelFormat]);
|
||||
}
|
||||
#if !RENDER_ON_MAIN_THREAD
|
||||
// These can only be changed on the main thread
|
||||
dispatch_sync(dispatch_get_main_queue(), ^{
|
||||
#endif
|
||||
#if !TARGET_OS_TV
|
||||
if (isHDR) {
|
||||
layer.wantsExtendedDynamicRangeContent = YES;
|
||||
}
|
||||
#endif
|
||||
layer.colorspace = newColorSpace;
|
||||
layer.pixelFormat = newPixelFormat;
|
||||
#if !RENDER_ON_MAIN_THREAD
|
||||
});
|
||||
#endif
|
||||
CGColorSpaceRelease(newColorSpace);
|
||||
}
|
||||
|
||||
// Create the new colorspace parameter buffer for our fragment shader
|
||||
MTLResourceOptions bufferOptions = MTLResourceStorageModeShared;
|
||||
_CscParamsBuffer = [_device newBufferWithBytes:(void *)¶mBuffer length:sizeof(paramBuffer) options:bufferOptions];
|
||||
if (!_CscParamsBuffer) {
|
||||
Log(LOG_E, @"Failed to create CSC parameters buffer");
|
||||
return NO;
|
||||
}
|
||||
|
||||
_lastColorSpace = colorspace;
|
||||
_lastFullRange = fullRange;
|
||||
}
|
||||
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (void)scaleSource:(CGRect *)src toDest:(CGRect *)dst {
|
||||
int dstH = ceilf((float)dst->size.width * src->size.height / src->size.width);
|
||||
int dstW = ceilf((float)dst->size.height * src->size.width / src->size.height);
|
||||
|
||||
if (dstH > dst->size.height) {
|
||||
dst->origin.x += (dst->size.width - dstW) / 2;
|
||||
dst->size.width = dstW;
|
||||
} else {
|
||||
dst->origin.y += (dst->size.height - dstH) / 2;
|
||||
dst->size.height = dstH;
|
||||
}
|
||||
}
|
||||
|
||||
- (void)screenSpace:(CGRect *)src toNormalizedDeviceCoords:(CGRect *)dst withDrawableWidth:(int)viewportWidth drawableHeight:(int)viewportHeight {
|
||||
dst->origin.x = ((float)src->origin.x / (viewportWidth / 2.0f)) - 1.0f;
|
||||
dst->origin.y = ((float)src->origin.y / (viewportHeight / 2.0f)) - 1.0f;
|
||||
dst->size.width = (float)src->size.width / (viewportWidth / 2.0f);
|
||||
dst->size.height = (float)src->size.height / (viewportHeight / 2.0f);
|
||||
}
|
||||
|
||||
- (BOOL)updateVideoRegionSizeForFrame:(Frame *)frame toLayer:(CAMetalLayer *)layer {
|
||||
int drawableWidth = layer.drawableSize.width;
|
||||
int drawableHeight = layer.drawableSize.height;
|
||||
|
||||
// Check if anything has changed since the last vertex buffer upload
|
||||
if (_VideoVertexBuffer && [frame width] == _lastFrameWidth && [frame height] == _lastFrameHeight && drawableWidth == _lastDrawableWidth &&
|
||||
drawableHeight == _lastDrawableHeight) {
|
||||
// Nothing to do
|
||||
return YES;
|
||||
}
|
||||
|
||||
// Determine the correct scaled size for the video region
|
||||
CGRect src = CGRectMake(0.0, 0.0, [frame width], [frame height]);
|
||||
CGRect dst = CGRectMake(0.0, 0.0, drawableWidth, drawableHeight);
|
||||
[self scaleSource:&src toDest:&dst];
|
||||
|
||||
// Convert screen space to normalized device coordinates
|
||||
CGRect renderRect;
|
||||
[self screenSpace:&dst toNormalizedDeviceCoords:&renderRect withDrawableWidth:drawableWidth drawableHeight:drawableHeight];
|
||||
|
||||
struct Vertex verts[] = {
|
||||
{{renderRect.origin.x, renderRect.origin.y, 0.0f, 1.0f}, {0.0f, 1.0f}},
|
||||
{{renderRect.origin.x, renderRect.origin.y + renderRect.size.height, 0.0f, 1.0f}, {0.0f, 0}},
|
||||
{{renderRect.origin.x + renderRect.size.width, renderRect.origin.y, 0.0f, 1.0f}, {1.0f, 1.0f}},
|
||||
{{renderRect.origin.x + renderRect.size.width, renderRect.origin.y + renderRect.size.height, 0.0f, 1.0f}, {1.0f, 0}},
|
||||
};
|
||||
|
||||
MTLResourceOptions bufferOptions = MTLResourceStorageModeShared;
|
||||
_VideoVertexBuffer = [_device newBufferWithBytes:verts length:sizeof(verts) options:bufferOptions];
|
||||
if (!_VideoVertexBuffer) {
|
||||
Log(LOG_E, @"Failed to create video vertex buffer");
|
||||
return NO;
|
||||
}
|
||||
|
||||
_lastFrameWidth = [frame width];
|
||||
_lastFrameHeight = [frame height];
|
||||
_lastDrawableWidth = drawableWidth;
|
||||
_lastDrawableHeight = drawableHeight;
|
||||
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (void)discardNextDrawable {
|
||||
_nextDrawable = nil;
|
||||
}
|
||||
|
||||
- (void)renderFrame:(Frame *)frame toLayer:(CAMetalLayer *)layer {
|
||||
// Handle changes to the frame's colorspace from last time we rendered
|
||||
BOOL layerDidChange = NO;
|
||||
if (![self updateColorSpaceForFrame:frame toLayer:layer layerDidChange:&layerDidChange]) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (layerDidChange && frame.frameNumber > 1) {
|
||||
Log(LOG_I, @"Metal frame changed layer's colorspace and/or pixel format, returning for new drawable");
|
||||
[self discardNextDrawable];
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle changes to the video size or drawable size
|
||||
if (![self updateVideoRegionSizeForFrame:frame toLayer:layer]) {
|
||||
return;
|
||||
}
|
||||
|
||||
FQLog(LOG_I, @"[%d / %.3f ms] Metal frame rendering", frame.frameNumber, frame.pts);
|
||||
|
||||
#if !TARGET_OS_TV
|
||||
// Experimental EDR handling based on frame metadata
|
||||
//[self applyEDRFromFrame:frame toLayer:layer];
|
||||
#endif
|
||||
|
||||
size_t planes = CVPixelBufferGetPlaneCount(frame.pixelBuffer);
|
||||
assert(planes == 2 || planes == 3);
|
||||
|
||||
MTLRenderPipelineDescriptor *pipelineDesc = [MTLRenderPipelineDescriptor new];
|
||||
id<MTLLibrary> defaultLibrary = [_device newDefaultLibrary];
|
||||
pipelineDesc.vertexFunction = [defaultLibrary newFunctionWithName:@"vs_draw"];
|
||||
pipelineDesc.fragmentFunction = [defaultLibrary newFunctionWithName:planes == 2 ? @"ps_draw_biplanar" : @"ps_draw_triplanar"];
|
||||
pipelineDesc.colorAttachments[0].pixelFormat = layer.pixelFormat;
|
||||
pipelineDesc.vertexBuffers[0].mutability = MTLMutabilityImmutable;
|
||||
|
||||
NSError *error = nil;
|
||||
_videoPipelineState = [_device newRenderPipelineStateWithDescriptor:pipelineDesc error:&error];
|
||||
if (!_videoPipelineState) {
|
||||
Log(LOG_E, @"Failed to create video pipeline state: %@", error);
|
||||
return;
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < planes; i++) {
|
||||
MTLPixelFormat fmt;
|
||||
|
||||
switch (CVPixelBufferGetPixelFormatType(frame.pixelBuffer)) {
|
||||
case kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange:
|
||||
case kCVPixelFormatType_444YpCbCr8BiPlanarVideoRange:
|
||||
case kCVPixelFormatType_420YpCbCr8BiPlanarFullRange:
|
||||
case kCVPixelFormatType_444YpCbCr8BiPlanarFullRange:
|
||||
fmt = (i == 0) ? MTLPixelFormatR8Unorm : MTLPixelFormatRG8Unorm;
|
||||
break;
|
||||
|
||||
case kCVPixelFormatType_420YpCbCr10BiPlanarFullRange:
|
||||
case kCVPixelFormatType_444YpCbCr10BiPlanarFullRange:
|
||||
case kCVPixelFormatType_420YpCbCr10BiPlanarVideoRange:
|
||||
case kCVPixelFormatType_444YpCbCr10BiPlanarVideoRange:
|
||||
fmt = (i == 0) ? MTLPixelFormatR16Unorm : MTLPixelFormatRG16Unorm;
|
||||
break;
|
||||
|
||||
default:
|
||||
Log(LOG_E, @"Unknown pixel format: %@", CVPixelBufferGetPixelFormatType(frame.pixelBuffer));
|
||||
return;
|
||||
}
|
||||
|
||||
if (_cvMetalTextures[i]) {
|
||||
CVBufferRelease(_cvMetalTextures[i]);
|
||||
}
|
||||
CVReturn err = CVMetalTextureCacheCreateTextureFromImage(kCFAllocatorDefault,
|
||||
_textureCache,
|
||||
frame.pixelBuffer,
|
||||
NULL,
|
||||
fmt,
|
||||
CVPixelBufferGetWidthOfPlane(frame.pixelBuffer, i),
|
||||
CVPixelBufferGetHeightOfPlane(frame.pixelBuffer, i),
|
||||
i,
|
||||
&_cvMetalTextures[i]);
|
||||
if (err != kCVReturnSuccess) {
|
||||
Log(LOG_E, @"CVMetalTextureCacheCreateTextureFromImage() failed: %d", err);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
_renderPassDescriptor.colorAttachments[0].texture = _nextDrawable.texture;
|
||||
|
||||
id<MTLCommandBuffer> commandBuffer = [_commandQueue commandBuffer];
|
||||
id<MTLRenderCommandEncoder> renderEncoder = [commandBuffer renderCommandEncoderWithDescriptor:_renderPassDescriptor];
|
||||
|
||||
[renderEncoder setRenderPipelineState:_videoPipelineState];
|
||||
for (size_t i = 0; i < planes; i++) {
|
||||
[renderEncoder setFragmentTexture:CVMetalTextureGetTexture(_cvMetalTextures[i]) atIndex:i];
|
||||
}
|
||||
[commandBuffer addCompletedHandler:^(id<MTLCommandBuffer> cb) {
|
||||
const CFTimeInterval GPUTime = cb.GPUEndTime - cb.GPUStartTime;
|
||||
const double alpha = 0.25f;
|
||||
self->_averageGPUTime = (GPUTime * alpha) + (self->_averageGPUTime * (1.0 - alpha));
|
||||
|
||||
// Free textures after completion of rendering per CVMetalTextureCache requirements
|
||||
for (size_t i = 0; i < planes; i++) {
|
||||
CVBufferRelease(self->_cvMetalTextures[i]);
|
||||
self->_cvMetalTextures[i] = nil;
|
||||
}
|
||||
|
||||
CVMetalTextureCacheFlush(self->_textureCache, 0);
|
||||
}];
|
||||
|
||||
[renderEncoder setFragmentBuffer:_CscParamsBuffer offset:0 atIndex:0];
|
||||
[renderEncoder setVertexBuffer:_VideoVertexBuffer offset:0 atIndex:0];
|
||||
[renderEncoder drawPrimitives:MTLPrimitiveTypeTriangleStrip vertexStart:0 vertexCount:4];
|
||||
[renderEncoder endEncoding];
|
||||
|
||||
__block dispatch_semaphore_t block_semaphore = _inFlightSemaphore;
|
||||
[commandBuffer addCompletedHandler:^(id<MTLCommandBuffer> buffer) {
|
||||
dispatch_semaphore_signal(block_semaphore);
|
||||
}];
|
||||
|
||||
__weak typeof(self) self_ = self;
|
||||
[_nextDrawable addPresentedHandler:^(id<MTLDrawable> d) {
|
||||
if (self_) {
|
||||
[self_ plotFrametime:d.presentedTime];
|
||||
}
|
||||
}];
|
||||
|
||||
#if TARGET_OS_SIMULATOR
|
||||
[commandBuffer presentDrawable:_nextDrawable];
|
||||
#else
|
||||
// present for a minimum duration for best frame pacing
|
||||
[commandBuffer presentDrawable:_nextDrawable afterMinimumDuration:1.0f / _framerate];
|
||||
#endif
|
||||
|
||||
[commandBuffer commit];
|
||||
|
||||
// Wait for the command buffer to complete and free our CVMetalTextureCache references
|
||||
[commandBuffer waitUntilCompleted];
|
||||
|
||||
_nextDrawable = nil;
|
||||
}
|
||||
|
||||
- (void)plotFrametime:(CFTimeInterval)presentedTime {
|
||||
if (_lastPresented > 0) {
|
||||
CFTimeInterval frametime = presentedTime - _lastPresented;
|
||||
[[ImGuiPlots sharedInstance] observeFloat:PLOT_FRAMETIME value:(frametime * 1000.0)];
|
||||
}
|
||||
_lastPresented = presentedTime;
|
||||
}
|
||||
|
||||
- (void)waitToRenderTo:(nonnull CAMetalLayer *)layer {
|
||||
if (!_nextDrawable) {
|
||||
// Wait for the next available drawable
|
||||
_nextDrawable = [layer nextDrawable];
|
||||
if (!_nextDrawable) {
|
||||
Log(LOG_E, @"Error getting nextDrawable from CAMetalLayer");
|
||||
return;
|
||||
}
|
||||
|
||||
// Wait to ensure only `MaxFramesInFlight` number of frames are getting processed
|
||||
// by any stage in the Metal pipeline (CPU, GPU, Metal, Drivers, etc.).
|
||||
dispatch_semaphore_wait(_inFlightSemaphore, DISPATCH_TIME_FOREVER);
|
||||
}
|
||||
}
|
||||
|
||||
/// Responds to the drawable's size or orientation changes.
|
||||
- (void)drawableResize:(CGSize)drawableSize {
|
||||
[self resize:drawableSize];
|
||||
}
|
||||
|
||||
- (void)resize:(CGSize)size {
|
||||
// TODO?
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,36 @@
|
||||
#import <Metal/Metal.h>
|
||||
#import <QuartzCore/CAMetalDisplayLink.h>
|
||||
#import <QuartzCore/CAMetalLayer.h>
|
||||
#import "MetalConfig.h"
|
||||
|
||||
#if TARGET_OS_IOS || TARGET_OS_TV
|
||||
#import <UIKit/UIKit.h>
|
||||
#define PlatformView UIView
|
||||
#else
|
||||
#import <AppKit/AppKit.h>
|
||||
#define PlatformView NSView
|
||||
#endif
|
||||
|
||||
// The protocol to provide resize and redraw callbacks to a delegate.
|
||||
@protocol MetalViewDelegate <NSObject>
|
||||
|
||||
- (void)drawableResize:(CGSize)size;
|
||||
- (void)renderTo:(nonnull CAMetalLayer *)layer;
|
||||
- (void)waitToRenderTo:(nonnull CAMetalLayer *)layer;
|
||||
|
||||
@end
|
||||
|
||||
// The Metal game view base class.
|
||||
@interface MetalView : PlatformView <CALayerDelegate>
|
||||
|
||||
@property (nonatomic, nonnull, readonly) CAMetalLayer *metalLayer;
|
||||
@property (nonatomic, getter=isPaused) BOOL paused;
|
||||
@property (nonatomic, nullable) id<MetalViewDelegate> delegate;
|
||||
@property (nonatomic) float framerate;
|
||||
|
||||
- (void)initCommon;
|
||||
#if AUTOMATICALLY_RESIZE
|
||||
- (void)resizeDrawable:(CGFloat)scaleFactor;
|
||||
#endif
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,205 @@
|
||||
// This is based on the following Apple example:
|
||||
// https://developer.apple.com/documentation/metal/achieving-smooth-frame-rates-with-a-metal-display-link?language=objc
|
||||
// https://developer.apple.com/wwdc23/10123/
|
||||
|
||||
#import "MetalView.h"
|
||||
#import "MetalConfig.h"
|
||||
|
||||
@implementation MetalView {
|
||||
#if !RENDER_ON_MAIN_THREAD
|
||||
// The secondary thread containing the render loop.
|
||||
NSThread *_renderThread;
|
||||
|
||||
// The flag to indicate that rendering needs to cease on the main thread.
|
||||
BOOL _continueRunLoop;
|
||||
#endif
|
||||
}
|
||||
|
||||
#pragma mark - Initialization and Setup.
|
||||
|
||||
- (instancetype)initWithFrame:(CGRect)frame {
|
||||
self = [super initWithFrame:frame];
|
||||
if (self) {
|
||||
[self initCommon];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (instancetype)initWithCoder:(NSCoder *)aDecoder {
|
||||
self = [super initWithCoder:aDecoder];
|
||||
if (self) {
|
||||
[self initCommon];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)initCommon {
|
||||
#if TARGET_OS_OSX
|
||||
self.wantsLayer = YES;
|
||||
|
||||
self.layerContentsRedrawPolicy = NSViewLayerContentsRedrawDuringViewResize;
|
||||
#endif
|
||||
|
||||
_metalLayer = (CAMetalLayer *)self.layer;
|
||||
|
||||
self.layer.delegate = self;
|
||||
}
|
||||
|
||||
#if TARGET_OS_IOS || TARGET_OS_TV
|
||||
+ (Class)layerClass {
|
||||
return [CAMetalLayer class];
|
||||
}
|
||||
|
||||
- (void)didMoveToWindow {
|
||||
[self movedToWindow];
|
||||
}
|
||||
#else
|
||||
- (CALayer *)makeBackingLayer {
|
||||
return [CAMetalLayer layer];
|
||||
}
|
||||
|
||||
- (void)viewDidMoveToWindow {
|
||||
[self movedToWindow];
|
||||
}
|
||||
#endif // END TARGET_OS_IOS || TARGET_OS_TV
|
||||
|
||||
- (void)movedToWindow {
|
||||
#if !RENDER_ON_MAIN_THREAD
|
||||
// Protect _continueRunLoop with a `@synchronized` block because it's accessed by the separate
|
||||
// animation thread.
|
||||
@synchronized(self) {
|
||||
// Stop the animation loop, allowing it to complete if it's in progress.
|
||||
_continueRunLoop = NO;
|
||||
}
|
||||
|
||||
// Create and start a secondary NSThread that has another run runloop. The NSThread
|
||||
// class calls the 'runThread' method at the start of the secondary thread's execution.
|
||||
_renderThread = [[NSThread alloc] initWithTarget:self selector:@selector(runThread) object:nil];
|
||||
_continueRunLoop = YES;
|
||||
_renderThread.qualityOfService = NSQualityOfServiceUserInteractive;
|
||||
[_renderThread start];
|
||||
#endif // END !RENDER_ON_MAIN_THREAD
|
||||
|
||||
// Perform any actions that need to know the size and scale of the drawable. When UIKit calls
|
||||
// didMoveToWindow after the view initialization, this is the first opportunity to notify
|
||||
// components of the drawable's size.
|
||||
#if AUTOMATICALLY_RESIZE
|
||||
#if TARGET_OS_IOS || TARGET_OS_TV
|
||||
[self resizeDrawable:self.window.screen.nativeScale];
|
||||
#else
|
||||
[self resizeDrawable:self.window.screen.backingScaleFactor];
|
||||
#endif
|
||||
#else
|
||||
// Notify the delegate of the default drawable size when the system can calculate it.
|
||||
CGSize defaultDrawableSize = self.bounds.size;
|
||||
defaultDrawableSize.width *= self.layer.contentsScale;
|
||||
defaultDrawableSize.height *= self.layer.contentsScale;
|
||||
[self.delegate drawableResize:defaultDrawableSize];
|
||||
#endif
|
||||
}
|
||||
|
||||
#if !RENDER_ON_MAIN_THREAD
|
||||
- (void)runThread {
|
||||
// The system sets the '_continueRunLoop' ivar outside this thread, so it needs to synchronize. Create a
|
||||
// 'continueRunLoop' local var that the system can set from the _continueRunLoop ivar in a @synchronized block.
|
||||
BOOL continueRunLoop = YES;
|
||||
|
||||
// Begin the run loop.
|
||||
while (continueRunLoop) {
|
||||
@autoreleasepool {
|
||||
[_delegate waitToRenderTo:_metalLayer];
|
||||
|
||||
@synchronized(self) {
|
||||
continueRunLoop = _continueRunLoop;
|
||||
}
|
||||
if (!continueRunLoop) {
|
||||
break;
|
||||
}
|
||||
|
||||
[_delegate renderTo:_metalLayer];
|
||||
|
||||
@synchronized(self) {
|
||||
continueRunLoop = _continueRunLoop;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif // END !RENDER_ON_MAIN_THREAD
|
||||
|
||||
#pragma mark - Resizing
|
||||
|
||||
#if AUTOMATICALLY_RESIZE
|
||||
|
||||
// Override all methods that indicate the view's size has changed.
|
||||
|
||||
#if TARGET_OS_IOS || TARGET_OS_TV
|
||||
- (void)setContentScaleFactor:(CGFloat)contentScaleFactor {
|
||||
[super setContentScaleFactor:contentScaleFactor];
|
||||
[self resizeDrawable:self.window.screen.nativeScale];
|
||||
}
|
||||
|
||||
- (void)layoutSubviews {
|
||||
[super layoutSubviews];
|
||||
[self resizeDrawable:self.window.screen.nativeScale];
|
||||
}
|
||||
|
||||
- (void)setFrame:(CGRect)frame {
|
||||
[super setFrame:frame];
|
||||
[self resizeDrawable:self.window.screen.nativeScale];
|
||||
}
|
||||
|
||||
- (void)setBounds:(CGRect)bounds {
|
||||
[super setBounds:bounds];
|
||||
[self resizeDrawable:self.window.screen.nativeScale];
|
||||
}
|
||||
#else
|
||||
- (void)viewDidChangeBackingProperties {
|
||||
[super viewDidChangeBackingProperties];
|
||||
[self resizeDrawable:self.window.screen.backingScaleFactor];
|
||||
}
|
||||
|
||||
- (void)setFrameSize:(NSSize)size {
|
||||
[super setFrameSize:size];
|
||||
[self resizeDrawable:self.window.screen.backingScaleFactor];
|
||||
}
|
||||
|
||||
- (void)setBoundsSize:(NSSize)size {
|
||||
[super setBoundsSize:size];
|
||||
[self resizeDrawable:self.window.screen.backingScaleFactor];
|
||||
}
|
||||
#endif
|
||||
|
||||
- (void)resizeDrawable:(CGFloat)scaleFactor {
|
||||
CGSize newSize = self.bounds.size;
|
||||
newSize.width *= scaleFactor;
|
||||
newSize.height *= scaleFactor;
|
||||
|
||||
if (newSize.width <= 0 || newSize.width <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
#if RENDER_ON_MAIN_THREAD
|
||||
if (newSize.width == _metalLayer.drawableSize.width && newSize.height == _metalLayer.drawableSize.height) {
|
||||
return;
|
||||
}
|
||||
|
||||
_metalLayer.drawableSize = newSize;
|
||||
|
||||
[_delegate drawableResize:newSize];
|
||||
#else
|
||||
// The system calls all AppKit and UIKit calls that notify of a resize on the main thread. Use
|
||||
// a synchronized block to ensure that resize notifications on the delegate are atomic.
|
||||
@synchronized(_metalLayer) {
|
||||
if (newSize.width == _metalLayer.drawableSize.width && newSize.height == _metalLayer.drawableSize.height) {
|
||||
return;
|
||||
}
|
||||
|
||||
_metalLayer.drawableSize = newSize;
|
||||
|
||||
[_delegate drawableResize:newSize];
|
||||
}
|
||||
#endif
|
||||
}
|
||||
#endif // END AUTOMATICALLY_RESIZE
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,24 @@
|
||||
#import <Metal/Metal.h>
|
||||
#import "FrameQueue.h"
|
||||
#import "ImGuiRenderer.h"
|
||||
#import "MetalVideoRenderer.h"
|
||||
#import "MetalView.h"
|
||||
|
||||
#if TARGET_OS_IOS || TARGET_OS_TV
|
||||
#import <UIKit/UIKit.h>
|
||||
#define PlatformViewController UIViewController
|
||||
#else
|
||||
#import <AppKit/AppKit.h>
|
||||
#define PlatformViewController NSViewController
|
||||
#endif
|
||||
|
||||
@interface MetalViewController : PlatformViewController <MetalViewDelegate>
|
||||
|
||||
@property (nonatomic) CGRect bounds;
|
||||
|
||||
- (nonnull instancetype)initWithFrame:(CGRect)bounds
|
||||
framerate:(float)framerate
|
||||
enableHdr:(BOOL)enableHdr
|
||||
metricsHandler:(MetricsHandler _Nonnull)metricsHandler;
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,144 @@
|
||||
/*
|
||||
See the LICENSE.txt file for this sample’s licensing information.
|
||||
|
||||
Abstract:
|
||||
The implementation of the cross-platform game view controller.
|
||||
*/
|
||||
|
||||
#import "MetalViewController.h"
|
||||
#import "FrameQueue.h"
|
||||
#import "ImGuiRenderer.h"
|
||||
#import "MetalVideoRenderer.h"
|
||||
|
||||
@implementation MetalViewController {
|
||||
/// A queue to initialize the renderer asynchronously from the main thread.
|
||||
dispatch_queue_t _dispatch_queue;
|
||||
FrameQueue *_frameQueue;
|
||||
float _framerate;
|
||||
BOOL _enableHdr;
|
||||
MetalView *_metalView;
|
||||
MetalVideoRenderer *_renderer;
|
||||
MetricsHandler _metricsHandler;
|
||||
BOOL _stopping;
|
||||
}
|
||||
|
||||
- (nonnull instancetype)initWithFrame:(CGRect)bounds framerate:(float)framerate enableHdr:(BOOL)enableHdr metricsHandler:(MetricsHandler)metricsHandler {
|
||||
self = [super init];
|
||||
if (self) {
|
||||
_bounds = bounds;
|
||||
_frameQueue = [FrameQueue sharedInstance];
|
||||
_framerate = framerate;
|
||||
_enableHdr = enableHdr;
|
||||
_metricsHandler = metricsHandler;
|
||||
_stopping = NO;
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)loadView {
|
||||
self.view = [[MetalView alloc] initWithFrame:_bounds];
|
||||
}
|
||||
|
||||
- (void)viewDidLoad {
|
||||
[super viewDidLoad];
|
||||
|
||||
/// A queue to initialize the renderer asynchronously from the main thread.
|
||||
_dispatch_queue = dispatch_queue_create("com.moonlight.Metal", DISPATCH_QUEUE_CONCURRENT);
|
||||
|
||||
__block MetalView *view = (MetalView *)self.view;
|
||||
if (!view) {
|
||||
Log(LOG_E, @"The view attached to MetalViewController isn't a MetalView.");
|
||||
return;
|
||||
}
|
||||
_metalView = view;
|
||||
_metalView.delegate = self;
|
||||
_metalView.framerate = _framerate;
|
||||
|
||||
// Select the device to render with.
|
||||
id<MTLDevice> device = MTLCreateSystemDefaultDevice();
|
||||
if (!device) {
|
||||
Log(LOG_E, @"Metal isn't supported on this device.");
|
||||
self.view = [[PlatformView alloc] initWithFrame:self.view.frame];
|
||||
return;
|
||||
}
|
||||
view.metalLayer.device = device;
|
||||
|
||||
// Initialize the renderer.
|
||||
MetalVideoRenderer *renderer = [[MetalVideoRenderer alloc] initWithMetalDevice:device
|
||||
drawablePixelFormat:MTLPixelFormatBGR10A2Unorm
|
||||
framerate:self->_framerate];
|
||||
if (!renderer) {
|
||||
Log(LOG_E, @"The renderer couldn't be initialized.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Initialize the renderer-dependent view properties.
|
||||
view.metalLayer.pixelFormat = renderer.colorPixelFormat;
|
||||
view.metalLayer.colorspace = renderer.colorspace;
|
||||
view.metalLayer.maximumDrawableCount = 3;
|
||||
|
||||
self->_renderer = renderer;
|
||||
}
|
||||
|
||||
- (void)waitToRenderTo:(nonnull CAMetalLayer *)layer {
|
||||
if (!_stopping) {
|
||||
// Renderer obtains a nextDrawable, waiting if necessary
|
||||
FQLog(LOG_I, @"[MetalViewController] caling [_renderer waitToRenderTo:layer]");
|
||||
[_renderer waitToRenderTo:layer];
|
||||
|
||||
// If we don't have a frame yet, wait on that too
|
||||
FQLog(LOG_I, @"[MetalViewController] caling [_renderer waitForEnqueue]");
|
||||
[_frameQueue waitForEnqueue];
|
||||
}
|
||||
}
|
||||
|
||||
/// Draw frame (used by manual loop)
|
||||
- (void)renderTo:(nonnull CAMetalLayer *)layer {
|
||||
if (!_renderer) {
|
||||
return;
|
||||
}
|
||||
|
||||
CFTimeInterval timeout = (1.0f / _framerate) - _renderer.averageGPUTime;
|
||||
FQLog(LOG_I, @"[MetalViewController] caling [_frameQueue dequeueWithTimeout:%f]", timeout);
|
||||
Frame *frame = [_frameQueue dequeueWithTimeout:timeout];
|
||||
if (frame) {
|
||||
FQLog(LOG_I, @"[MetalViewController] calling [_renderer renderFrame] %@", frame);
|
||||
[_renderer renderFrame:frame toLayer:layer];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)drawableResize:(CGSize)size {
|
||||
[_renderer drawableResize:size];
|
||||
}
|
||||
|
||||
#if TARGET_OS_IOS
|
||||
/// Hides the Home indicator button automatically.
|
||||
- (BOOL)prefersHomeIndicatorAutoHidden {
|
||||
return YES;
|
||||
}
|
||||
#endif
|
||||
|
||||
#if TARGET_OS_OSX
|
||||
/// Makes the view controller the first responder to receive keyboard events.
|
||||
- (void)viewDidAppear {
|
||||
[_metalView.window makeFirstResponder:self];
|
||||
}
|
||||
|
||||
/// Receives the keydown events to avoid system beeps.
|
||||
///
|
||||
/// The `GameInputKeyboardMouse` class handles keyboard events.
|
||||
- (void)keyDown:(NSEvent *)event {
|
||||
// Reference the parameter to avoid an unused parameter warning.
|
||||
(void)(event);
|
||||
}
|
||||
|
||||
/// Receives the keyup events to avoid system beeps.
|
||||
///
|
||||
/// The `GameInputKeyboardMouse` class handles keyboard events.
|
||||
- (void)keyUp:(NSEvent *)event {
|
||||
// Reference the parameter to avoid an unused parameter warning.
|
||||
(void)(event);
|
||||
}
|
||||
#endif
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,63 @@
|
||||
#include <metal_stdlib>
|
||||
#include <simd/simd.h>
|
||||
|
||||
using namespace metal;
|
||||
|
||||
struct Vertex
|
||||
{
|
||||
float4 position [[ position ]];
|
||||
float2 texCoords;
|
||||
};
|
||||
|
||||
struct CscParams
|
||||
{
|
||||
float3 matrix[3];
|
||||
float3 offsets;
|
||||
};
|
||||
|
||||
constexpr sampler s(coord::normalized, address::clamp_to_edge, filter::linear);
|
||||
|
||||
vertex Vertex vs_draw(constant Vertex *vertices [[ buffer(0) ]], uint id [[ vertex_id ]])
|
||||
{
|
||||
return vertices[id];
|
||||
}
|
||||
|
||||
fragment float4 ps_draw_biplanar(Vertex v [[ stage_in ]],
|
||||
constant CscParams &cscParams [[ buffer(0) ]],
|
||||
texture2d<float> luminancePlane [[ texture(0) ]],
|
||||
texture2d<float> chrominancePlane [[ texture(1) ]])
|
||||
{
|
||||
float3 yuv = float3(luminancePlane.sample(s, v.texCoords).r,
|
||||
chrominancePlane.sample(s, v.texCoords).rg);
|
||||
yuv -= cscParams.offsets;
|
||||
|
||||
float3 rgb;
|
||||
rgb.r = dot(yuv, cscParams.matrix[0]);
|
||||
rgb.g = dot(yuv, cscParams.matrix[1]);
|
||||
rgb.b = dot(yuv, cscParams.matrix[2]);
|
||||
return float4(rgb, 1.0f);
|
||||
}
|
||||
|
||||
fragment float4 ps_draw_triplanar(Vertex v [[ stage_in ]],
|
||||
constant CscParams &cscParams [[ buffer(0) ]],
|
||||
texture2d<float> luminancePlane [[ texture(0) ]],
|
||||
texture2d<float> chrominancePlaneU [[ texture(1) ]],
|
||||
texture2d<float> chrominancePlaneV [[ texture(2) ]])
|
||||
{
|
||||
float3 yuv = float3(luminancePlane.sample(s, v.texCoords).r,
|
||||
chrominancePlaneU.sample(s, v.texCoords).r,
|
||||
chrominancePlaneV.sample(s, v.texCoords).r);
|
||||
yuv -= cscParams.offsets;
|
||||
|
||||
float3 rgb;
|
||||
rgb.r = dot(yuv, cscParams.matrix[0]);
|
||||
rgb.g = dot(yuv, cscParams.matrix[1]);
|
||||
rgb.b = dot(yuv, cscParams.matrix[2]);
|
||||
return float4(rgb, 1.0f);
|
||||
}
|
||||
|
||||
fragment float4 ps_draw_rgb(Vertex v [[ stage_in ]],
|
||||
texture2d<float> rgbTexture [[ texture(0) ]])
|
||||
{
|
||||
return rgbTexture.sample(s, v.texCoords);
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <VideoToolbox/VideoToolbox.h>
|
||||
|
||||
#import "FloatBuffer.h"
|
||||
#import "Plot.h"
|
||||
|
||||
@interface ImGuiPlots : NSObject
|
||||
|
||||
struct PlotDef {
|
||||
FloatBuffer * _Nonnull buffer;
|
||||
const char * _Nonnull title;
|
||||
PlotSide side;
|
||||
PlotLabelType labelType;
|
||||
const char * _Nonnull unit;
|
||||
double scaleMin, scaleMax, scaleTarget;
|
||||
float minY;
|
||||
float maxY;
|
||||
};
|
||||
|
||||
@property (nonatomic) struct PlotDef * _Nonnull plots;
|
||||
|
||||
+ (instancetype _Nonnull)sharedInstance;
|
||||
|
||||
- (instancetype _Nonnull)init NS_UNAVAILABLE;
|
||||
+ (instancetype _Nonnull)new NS_UNAVAILABLE;
|
||||
|
||||
- (void)observeFloat:(int)plotId value:(CFTimeInterval)value;
|
||||
- (void)observeFloatReturnMetrics:(int)plotId value:(CFTimeInterval)value plotMetrics:(PlotMetrics * _Nullable)plotMetrics;
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,95 @@
|
||||
#import "ImGuiPlots.h"
|
||||
|
||||
@implementation ImGuiPlots
|
||||
|
||||
+ (instancetype)sharedInstance {
|
||||
static ImGuiPlots *sharedInstance = nil;
|
||||
static dispatch_once_t onceToken;
|
||||
dispatch_once(&onceToken, ^{
|
||||
sharedInstance = [[super allocWithZone:NULL] _initSingleton];
|
||||
});
|
||||
return sharedInstance;
|
||||
}
|
||||
|
||||
// Prevent others from using alloc/init directly
|
||||
+ (instancetype)allocWithZone:(struct _NSZone *)zone {
|
||||
return [self sharedInstance];
|
||||
}
|
||||
|
||||
// If someone tries to copy it, just return the same instance
|
||||
- (id)copyWithZone:(NSZone *)zone {
|
||||
return self;
|
||||
}
|
||||
|
||||
- (id)mutableCopyWithZone:(NSZone *)zone {
|
||||
return self;
|
||||
}
|
||||
|
||||
-(nonnull instancetype)_initSingleton
|
||||
{
|
||||
self = [super init];
|
||||
if (self) {
|
||||
_plots = (PlotDef *)calloc(PlotCount, sizeof(PlotDef));
|
||||
|
||||
_plots[PLOT_FRAMETIME] = {
|
||||
.title = "Frametime",
|
||||
.side = PLOT_LEFT,
|
||||
.unit = "ms",
|
||||
.scaleMin = (1000.0 / 120) - 1,
|
||||
.scaleMax = 50.0f, // (1000.0 / streamFps) * 3,
|
||||
.buffer = [[FloatBuffer alloc] initWithCapacity:512]
|
||||
};
|
||||
|
||||
_plots[PLOT_HOST_FRAMETIME] = {
|
||||
.title = "Host Frametime",
|
||||
.side = PLOT_LEFT,
|
||||
.unit = "ms",
|
||||
.scaleMin = (1000.0 / 120) - 1,
|
||||
.scaleMax = 50.0f, // (1000.0 / streamFps) * 3,
|
||||
.buffer = [[FloatBuffer alloc] initWithCapacity:512]
|
||||
};
|
||||
|
||||
_plots[PLOT_QUEUED_FRAMES] = {
|
||||
.title = "Frame queue",
|
||||
.side = PLOT_RIGHT,
|
||||
.labelType = PLOT_LABEL_MIN_MAX_AVG_INT,
|
||||
.unit = "",
|
||||
.scaleMin = -0.5,
|
||||
.scaleMax = 15,
|
||||
.buffer = [[FloatBuffer alloc] initWithCapacity:512]
|
||||
};
|
||||
|
||||
_plots[PLOT_DROPPED] = {
|
||||
.title = "Frames dropped",
|
||||
.side = PLOT_RIGHT,
|
||||
.labelType = PLOT_LABEL_TOTAL_INT,
|
||||
.unit = "",
|
||||
.scaleTarget = 2,
|
||||
.buffer = [[FloatBuffer alloc] initWithCapacity:512]
|
||||
};
|
||||
|
||||
// not graphed, but used for stats
|
||||
|
||||
_plots[PLOT_DECODE] = {
|
||||
.title = "Decode time",
|
||||
.labelType = PLOT_LABEL_MIN_MAX_AVG,
|
||||
.unit = "ms",
|
||||
.buffer = [[FloatBuffer alloc] initWithCapacity:512]
|
||||
};
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void) observeFloat:(int)plotId value:(CFTimeInterval)value {
|
||||
[_plots[plotId].buffer addValue:(float)value];
|
||||
}
|
||||
|
||||
- (void) observeFloatReturnMetrics:(int)plotId value:(CFTimeInterval)value plotMetrics:(PlotMetrics *)plotMetrics {
|
||||
[_plots[plotId].buffer addValue:(float)value];
|
||||
if (plotMetrics != nil) {
|
||||
[_plots[plotId].buffer copyMetrics:plotMetrics];
|
||||
}
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -31,7 +31,8 @@
|
||||
@property (strong, nonatomic) IBOutlet UISegmentedControl *codecSelector;
|
||||
@property (strong, nonatomic) IBOutlet UISegmentedControl *yuv444Selector;
|
||||
@property (strong, nonatomic) IBOutlet UISegmentedControl *hdrSelector;
|
||||
@property (strong, nonatomic) IBOutlet UISegmentedControl *framePacingSelector;
|
||||
@property (strong, nonatomic) IBOutlet UILabel *frameQueueSizeLabel;
|
||||
@property (strong, nonatomic) IBOutlet UISlider *frameQueueSizeSlider;
|
||||
@property (strong, nonatomic) IBOutlet UISegmentedControl *btMouseSelector;
|
||||
@property (strong, nonatomic) IBOutlet UISegmentedControl *reverseMouseWheelDirectionSelector;
|
||||
@property (strong, nonatomic) IBOutlet UISegmentedControl *statsOverlaySelector;
|
||||
@@ -61,6 +62,10 @@
|
||||
|
||||
@property (strong, nonatomic) IBOutlet UISegmentedControl *externalDisplayModeSelector;
|
||||
@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 ignored "-Wunguarded-availability"
|
||||
|
||||
@@ -22,6 +22,8 @@
|
||||
bool justEnteredSettingsViewDoNotOpenOscLayoutTool;
|
||||
uint16_t oscLayoutFingers;
|
||||
CustomEdgeSlideGestureRecognizer *slideToCloseSettingsViewRecognizer;
|
||||
NSInteger _frameQueueSize;
|
||||
NSInteger _graphOpacity;
|
||||
}
|
||||
|
||||
@dynamic overrideUserInterfaceStyle;
|
||||
@@ -199,7 +201,7 @@ BOOL isCustomResolution(CGSize res) {
|
||||
}
|
||||
|
||||
for (int i = 0; i < RESOLUTION_TABLE_CUSTOM_INDEX; i++) {
|
||||
|
||||
|
||||
if ((res.width == resolutionTable[i].width && res.height == resolutionTable[i].height) || (res.height == resolutionTable[i].width && res.width == resolutionTable[i].height)) {
|
||||
return NO;
|
||||
}
|
||||
@@ -215,7 +217,7 @@ BOOL isCustomResolution(CGSize res) {
|
||||
- (bool)isFullScreenRequired {
|
||||
NSDictionary *infoDictionary = [[NSBundle mainBundle] infoDictionary];
|
||||
NSNumber *requiresFullScreen = infoDictionary[@"UIRequiresFullScreen"];
|
||||
|
||||
|
||||
if (requiresFullScreen != nil) {
|
||||
return [requiresFullScreen boolValue];
|
||||
}
|
||||
@@ -240,7 +242,7 @@ BOOL isCustomResolution(CGSize res) {
|
||||
appWindowHeight = bounds.size.height * screenScale;
|
||||
}
|
||||
bool needSwapWidthAndHeight = appWindowWidth > appWindowHeight;
|
||||
|
||||
|
||||
resolutionTable[4] = CGSizeMake(safeAreaWidth, appWindowHeight);
|
||||
|
||||
for(uint8_t i=0;i<7;i++){
|
||||
@@ -340,7 +342,7 @@ BOOL isCustomResolution(CGSize res) {
|
||||
- (void)viewDidLoad {
|
||||
//[self pushDownExistingWidgets];
|
||||
//[self addExitButtonOnTop];
|
||||
|
||||
|
||||
self->slideToCloseSettingsViewRecognizer = [[CustomEdgeSlideGestureRecognizer alloc] initWithTarget:self action:@selector(edgeSwiped)];
|
||||
slideToCloseSettingsViewRecognizer.edges = UIRectEdgeLeft;
|
||||
slideToCloseSettingsViewRecognizer.normalizedThresholdDistance = 0.0;
|
||||
@@ -349,13 +351,13 @@ BOOL isCustomResolution(CGSize res) {
|
||||
slideToCloseSettingsViewRecognizer.delaysTouchesBegan = NO;
|
||||
slideToCloseSettingsViewRecognizer.delaysTouchesEnded = NO;
|
||||
[self.view addGestureRecognizer:slideToCloseSettingsViewRecognizer];
|
||||
|
||||
|
||||
justEnteredSettingsViewDoNotOpenOscLayoutTool = true;
|
||||
[[NSNotificationCenter defaultCenter] addObserver:self
|
||||
selector:@selector(deviceOrientationDidChange) // handle orientation change since i made portrait mode available
|
||||
name:UIDeviceOrientationDidChangeNotification
|
||||
object:nil];
|
||||
|
||||
|
||||
// Always run settings in dark mode because we want the light fonts
|
||||
if (@available(iOS 13.0, tvOS 13.0, *)) {
|
||||
self.overrideUserInterfaceStyle = UIUserInterfaceStyleDark;
|
||||
@@ -366,6 +368,8 @@ BOOL isCustomResolution(CGSize res) {
|
||||
|
||||
// Ensure we pick a bitrate that falls exactly onto a slider notch
|
||||
_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
|
||||
UIWindow *window = UIApplication.sharedApplication.windows.firstObject;
|
||||
@@ -379,20 +383,26 @@ BOOL isCustomResolution(CGSize res) {
|
||||
UITapGestureRecognizer *resolutionDisplayViewTap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(resolutionDisplayViewTapped:)];
|
||||
[self.resolutionDisplayView addGestureRecognizer:resolutionDisplayViewTap];
|
||||
|
||||
resolutionTable[0] = CGSizeMake(640, 360);
|
||||
resolutionTable[1] = CGSizeMake(1280, 720);
|
||||
resolutionTable[2] = CGSizeMake(1920, 1080);
|
||||
resolutionTable[0] = CGSizeMake(1280, 720);
|
||||
resolutionTable[1] = CGSizeMake(1920, 1080);
|
||||
resolutionTable[2] = CGSizeMake(2560, 1440);
|
||||
resolutionTable[3] = CGSizeMake(3840, 2160);
|
||||
resolutionTable[4] = CGSizeMake(safeAreaWidth, fullScreenHeight);
|
||||
resolutionTable[5] = CGSizeMake(fullScreenWidth, fullScreenHeight);
|
||||
resolutionTable[6] = CGSizeMake([currentSettings.width integerValue], [currentSettings.height integerValue]); // custom initial value
|
||||
[self updateResolutionTable];
|
||||
|
||||
|
||||
// Don't populate the custom entry unless we have a custom resolution
|
||||
if (!isCustomResolution(resolutionTable[6])) {
|
||||
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;
|
||||
switch ([currentSettings.framerate integerValue]) {
|
||||
case 30:
|
||||
@@ -402,9 +412,12 @@ BOOL isCustomResolution(CGSize res) {
|
||||
case 60:
|
||||
framerate = 1;
|
||||
break;
|
||||
case 120:
|
||||
case 90:
|
||||
framerate = 2;
|
||||
break;
|
||||
case 120:
|
||||
framerate = 3;
|
||||
break;
|
||||
}
|
||||
|
||||
NSInteger resolution = currentSettings.resolutionSelected.integerValue;
|
||||
@@ -412,17 +425,6 @@ BOOL isCustomResolution(CGSize res) {
|
||||
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
|
||||
#if defined(__IPHONE_16_0) || defined(__TVOS_16_0)
|
||||
if (!VTIsHardwareDecodeSupported(kCMVideoCodecType_AV1))
|
||||
@@ -463,12 +465,26 @@ BOOL isCustomResolution(CGSize res) {
|
||||
else {
|
||||
[self.hdrSelector setSelectedSegmentIndex:currentSettings.enableHdr ? 1 : 0];
|
||||
}
|
||||
|
||||
|
||||
[self.yuv444Selector setSelectedSegmentIndex:currentSettings.enableYUV444 ? 1 : 0];
|
||||
[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.optimizeSettingsSelector setSelectedSegmentIndex:currentSettings.optimizeGames ? 1 : 0];
|
||||
[self.framePacingSelector setSelectedSegmentIndex:currentSettings.useFramePacing ? 1 : 0];
|
||||
[self.frameQueueSizeSlider setMinimumValue:1];
|
||||
[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.swapABXYButtonsSelector setSelectedSegmentIndex:currentSettings.swapABXYButtons ? 1 : 0];
|
||||
[self.audioOnPCSelector setSelectedSegmentIndex:currentSettings.playAudioOnPC ? 1 : 0];
|
||||
@@ -485,20 +501,20 @@ BOOL isCustomResolution(CGSize res) {
|
||||
[self.bitrateSlider addTarget:self action:@selector(bitrateSliderMoved) forControlEvents:UIControlEventValueChanged];
|
||||
[self updateBitrateText];
|
||||
[self updateResolutionDisplayViewText];
|
||||
|
||||
|
||||
// Unlock Display Orientation setting
|
||||
bool unlockDisplayOrientationSelectorEnabled = [self isFullScreenRequired];//need "requires fullscreen" enabled in the app bunddle to make runtime orientation limitation woring
|
||||
if(unlockDisplayOrientationSelectorEnabled) [self.unlockDisplayOrientationSelector setSelectedSegmentIndex:currentSettings.unlockDisplayOrientation ? 1 : 0];
|
||||
else [self.unlockDisplayOrientationSelector setSelectedSegmentIndex:1]; // can't lock screen orientation in this mode = Display Orientation always unlocked
|
||||
[self.unlockDisplayOrientationSelector setEnabled:unlockDisplayOrientationSelectorEnabled];
|
||||
|
||||
|
||||
|
||||
// lift streamview setting
|
||||
[self.liftStreamViewForKeyboardSelector setSelectedSegmentIndex:currentSettings.liftStreamViewForKeyboard ? 1 : 0];// Load old setting
|
||||
|
||||
|
||||
// showkeyboard toolbar setting
|
||||
[self.showKeyboardToolbarSelector setSelectedSegmentIndex:currentSettings.showKeyboardToolbar ? 1 : 0];// Load old setting
|
||||
|
||||
|
||||
// reverse mouse wheel direction setting
|
||||
[self.reverseMouseWheelDirectionSelector setSelectedSegmentIndex:currentSettings.reverseMouseWheelDirection ? 1 : 0];// Load old setting
|
||||
|
||||
@@ -512,11 +528,11 @@ BOOL isCustomResolution(CGSize res) {
|
||||
[self.slideToMenuDistanceSlider setValue:currentSettings.slideToSettingsDistance.floatValue];
|
||||
[self.slideToMenuDistanceSlider addTarget:self action:@selector(slideToMenuDistanceSliderMoved) forControlEvents:(UIControlEventValueChanged)]; // Update label display when slider is being moved.
|
||||
[self slideToMenuDistanceSliderMoved];
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
//TouchMode & OSC Related Settings:
|
||||
|
||||
|
||||
// pointer veloc setting, will be enable/disabled by touchMode
|
||||
[self.pointerVelocityModeDividerSlider setValue: (uint8_t)(currentSettings.pointerVelocityModeDivider.floatValue * 100) animated:YES]; // Load old setting.
|
||||
[self.pointerVelocityModeDividerSlider addTarget:self action:@selector(pointerVelocityModeDividerSliderMoved) forControlEvents:(UIControlEventValueChanged)]; // Update label display when slider is being moved.
|
||||
@@ -526,13 +542,13 @@ BOOL isCustomResolution(CGSize res) {
|
||||
[self.touchPointerVelocityFactorSlider setValue: [self map_SliderValue_fromVelocFactor: currentSettings.touchPointerVelocityFactor.floatValue] animated:YES]; // Load old setting.
|
||||
[self.touchPointerVelocityFactorSlider addTarget:self action:@selector(touchPointerVelocityFactorSliderMoved) forControlEvents:(UIControlEventValueChanged)]; // Update label display when slider is being moved.
|
||||
[self touchPointerVelocityFactorSliderMoved];
|
||||
|
||||
|
||||
// init relative touch mouse pointer veloc setting, will be enable/disabled by touchMode
|
||||
[self.mousePointerVelocityFactorSlider setValue:[self map_SliderValue_fromVelocFactor: currentSettings.mousePointerVelocityFactor.floatValue] animated:YES]; // Load old setting.
|
||||
[self.mousePointerVelocityFactorSlider addTarget:self action:@selector(mousePointerVelocityFactorSliderMoved) forControlEvents:(UIControlEventValueChanged)]; // Update label display when slider is being moved.
|
||||
[self mousePointerVelocityFactorSliderMoved];
|
||||
|
||||
|
||||
|
||||
|
||||
// these settings will be affected by onscreenControl & touchMode, must be loaded before them.
|
||||
// NSLog(@"osc tool fingers setting test: %d", currentSettings.oscLayoutToolFingers.intValue);
|
||||
self->oscLayoutFingers = (uint16_t)currentSettings.oscLayoutToolFingers.intValue; // load old setting of oscLayoutFingers
|
||||
@@ -546,7 +562,7 @@ BOOL isCustomResolution(CGSize res) {
|
||||
[self.onscreenControlSelector addTarget:self action:@selector(onscreenControlChanged) forControlEvents:UIControlEventValueChanged];
|
||||
[self onscreenControlChanged];
|
||||
[self.largerStickLR1Selector setSelectedSegmentIndex:currentSettings.largerStickLR1 ? 1 : 0]; // load old setting of largerStickLR1
|
||||
|
||||
|
||||
// tap exclusion area size for custom OSC, must be loaded before touchMode & osc selector.
|
||||
[self.oscTapExlusionAreaSizeSlider setValue:currentSettings.oscTapExlusionAreaSize.floatValue * 100 animated:YES]; // Load old setting.
|
||||
[self.oscTapExlusionAreaSizeSlider addTarget:self action:@selector(oscTapExlusionAreaSizeSliderMoved) forControlEvents:(UIControlEventValueChanged)]; // Update label display when slider is being moved.
|
||||
@@ -559,7 +575,7 @@ BOOL isCustomResolution(CGSize res) {
|
||||
[self.touchModeSelector addTarget:self action:@selector(touchModeChanged) forControlEvents:UIControlEventValueChanged];
|
||||
[self touchModeChanged];
|
||||
|
||||
|
||||
|
||||
// init CustomOSC stuff
|
||||
/* sets a reference to the correct 'LayoutOnScreenControlsViewController' depending on whether the user is on an iPhone or iPad */
|
||||
self.layoutOnScreenControlsVC = [[LayoutOnScreenControlsViewController alloc] init];
|
||||
@@ -603,16 +619,16 @@ BOOL isCustomResolution(CGSize res) {
|
||||
UIAlertController *alertController = [UIAlertController alertControllerWithTitle:[LocalizationHelper localizedStringForKey:@"Rebase in Stream View"]
|
||||
message:[LocalizationHelper localizedStringForKey:@"Tap %d fingers to change layout in stream view, or change the number of fingers required to:", self->oscLayoutFingers]
|
||||
preferredStyle:UIAlertControllerStyleAlert];
|
||||
|
||||
|
||||
[alertController addTextFieldWithConfigurationHandler:^(UITextField *textField) {
|
||||
textField.placeholder = [LocalizationHelper localizedStringForKey:@"%d", self->oscLayoutFingers];
|
||||
textField.keyboardType = UIKeyboardTypeNumberPad;
|
||||
}];
|
||||
|
||||
|
||||
UIAlertAction *cancelAction = [UIAlertAction actionWithTitle:[LocalizationHelper localizedStringForKey:@"Cancel"]
|
||||
style:UIAlertActionStyleCancel
|
||||
handler:nil];
|
||||
|
||||
|
||||
UIAlertAction *okAction = [UIAlertAction actionWithTitle:[LocalizationHelper localizedStringForKey:@"OK"]
|
||||
style:UIAlertActionStyleDefault
|
||||
handler:^(UIAlertAction *action) {
|
||||
@@ -625,16 +641,16 @@ BOOL isCustomResolution(CGSize res) {
|
||||
} else {
|
||||
NSLog(@"OK button tapped with no change");
|
||||
}
|
||||
|
||||
|
||||
// Continue execution after the alert is dismissed
|
||||
if (!self->_mainFrameViewController.settingsExpandedInStreamView) {
|
||||
[self invokeOscLayout]; // Don't open osc layout tool immediately during streaming
|
||||
}
|
||||
|
||||
|
||||
[self.onscreenControllerLabel setText:[LocalizationHelper localizedStringForKey: @"Tap %d Fingers to Change OSC Layout in Stream View", self->oscLayoutFingers]]; //update the osc label
|
||||
[self keyboardToggleFingerNumSliderMoved]; //update keyboard toggle number;
|
||||
}];
|
||||
|
||||
|
||||
[alertController addAction:cancelAction];
|
||||
[alertController addAction:okAction];
|
||||
[self presentViewController:alertController animated:YES completion:nil];
|
||||
@@ -649,7 +665,7 @@ BOOL isCustomResolution(CGSize res) {
|
||||
|
||||
|
||||
- (void)onscreenControlChanged{
|
||||
|
||||
|
||||
BOOL isIPhone = ([UIDevice currentDevice].userInterfaceIdiom == UIUserInterfaceIdiomPhone);
|
||||
if (isIPhone) {
|
||||
UIStoryboard *storyboard = [UIStoryboard storyboardWithName:@"iPhone" bundle:nil];
|
||||
@@ -660,7 +676,7 @@ BOOL isCustomResolution(CGSize res) {
|
||||
self.layoutOnScreenControlsVC = [storyboard instantiateViewControllerWithIdentifier:@"LayoutOnScreenControlsViewController"];
|
||||
self.layoutOnScreenControlsVC.modalPresentationStyle = UIModalPresentationFullScreen;
|
||||
}
|
||||
|
||||
|
||||
bool customOscEnabled = [self isOnScreenControllerOrButtonEnabled] && [self.onscreenControlSelector selectedSegmentIndex] == OnScreenControlsLevelCustom;
|
||||
// [self widget:self.keyboardToggleFingerNumSlider setEnabled:!customOscEnabled];
|
||||
[self widget:self.oscTapExlusionAreaSizeSlider setEnabled:customOscEnabled];
|
||||
@@ -747,10 +763,10 @@ BOOL isCustomResolution(CGSize res) {
|
||||
bool oscSelectorEnabled = [self.touchModeSelector selectedSegmentIndex] == RELATIVE_TOUCH || [self.touchModeSelector selectedSegmentIndex] == REGULAR_NATIVE_TOUCH || [self.touchModeSelector selectedSegmentIndex] == ABSOLUTE_TOUCH;
|
||||
bool customOscEnabled = [self isOnScreenControllerOrButtonEnabled] && [self.onscreenControlSelector selectedSegmentIndex] == OnScreenControlsLevelCustom;
|
||||
bool isNativeTouch = [self.touchModeSelector selectedSegmentIndex] == PURE_NATIVE_TOUCH || [self.touchModeSelector selectedSegmentIndex] == REGULAR_NATIVE_TOUCH;
|
||||
|
||||
|
||||
[self.onscreenControlSelector setEnabled:oscSelectorEnabled];
|
||||
[self.largerStickLR1Selector setEnabled:oscSelectorEnabled]; // this selector stay aligned with oscSelector
|
||||
|
||||
|
||||
[self widget:self.pointerVelocityModeDividerSlider setEnabled:isNativeTouch]; // pointer velocity scaling works only in native touch mode.
|
||||
[self widget:self.touchPointerVelocityFactorSlider setEnabled:isNativeTouch]; // pointer velocity scaling works only in native touch mode.
|
||||
[self widget:self.mousePointerVelocityFactorSlider setEnabled:[self.touchModeSelector selectedSegmentIndex] == RELATIVE_TOUCH]; // mouse velocity scaling works only in relative touch mode.
|
||||
@@ -828,7 +844,7 @@ BOOL isCustomResolution(CGSize res) {
|
||||
}
|
||||
|
||||
defaultBitrate = round(resolutionFactor * frameRateFactor) * 1000;
|
||||
_bitrate = MIN(defaultBitrate, 100000);
|
||||
_bitrate = MIN(defaultBitrate, 150000);
|
||||
[self.bitrateSlider setValue:[self getSliderValueForBitrate:_bitrate] animated:YES];
|
||||
|
||||
[self updateBitrateText];
|
||||
@@ -966,14 +982,14 @@ BOOL isCustomResolution(CGSize res) {
|
||||
- (void) keyboardToggleFingerNumSliderMoved{
|
||||
// bool oscEnabled = [self isOnScreenControllerOrButtonEnabled];
|
||||
bool customOscEnabled = [self isOnScreenControllerOrButtonEnabled] && [self.onscreenControlSelector selectedSegmentIndex] == OnScreenControlsLevelCustom;
|
||||
|
||||
|
||||
CGFloat sliderValue = self.keyboardToggleFingerNumSlider.value;
|
||||
if(customOscEnabled){
|
||||
// exclude self->oscLayoutFingers when custom osc is enabled
|
||||
if(sliderValue > self->oscLayoutFingers - 1 && sliderValue < self->oscLayoutFingers) [self.keyboardToggleFingerNumSlider setValue: self->oscLayoutFingers - 1];
|
||||
if(sliderValue >= self->oscLayoutFingers && sliderValue < self->oscLayoutFingers + 1) [self.keyboardToggleFingerNumSlider setValue: self->oscLayoutFingers + 1];
|
||||
}
|
||||
|
||||
|
||||
sliderValue = self.keyboardToggleFingerNumSlider.value;
|
||||
if(sliderValue > 10.5f) [self.keyboardToggleFingerNumLabel setText:[LocalizationHelper localizedStringForKey:@"Local Keyboard Toggle Disabled"]];
|
||||
else [self.keyboardToggleFingerNumLabel setText:[LocalizationHelper localizedStringForKey:@"To Toggle Local Keyboard: Tap %d Fingers", (uint16_t)sliderValue]]; // Initiate label display, exclude 5 fingers.
|
||||
@@ -1002,6 +1018,8 @@ BOOL isCustomResolution(CGSize res) {
|
||||
case 1:
|
||||
return 60;
|
||||
case 2:
|
||||
return 90;
|
||||
case 3:
|
||||
return 120;
|
||||
default:
|
||||
abort();
|
||||
@@ -1050,6 +1068,30 @@ BOOL isCustomResolution(CGSize res) {
|
||||
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 {
|
||||
DataManager* dataMan = [[DataManager alloc] init];
|
||||
NSInteger framerate = [self getChosenFrameRate];
|
||||
@@ -1073,6 +1115,7 @@ BOOL isCustomResolution(CGSize res) {
|
||||
BOOL largerStickLR1 = [self.largerStickLR1Selector selectedSegmentIndex] == 1;
|
||||
BOOL liftStreamViewForKeyboard = [self.liftStreamViewForKeyboardSelector selectedSegmentIndex] == 1;
|
||||
BOOL showKeyboardToolbar = [self.showKeyboardToolbarSelector selectedSegmentIndex] == 1;
|
||||
NSInteger renderingBackend = [self.renderingBackendSelector selectedSegmentIndex];
|
||||
BOOL optimizeGames = [self.optimizeSettingsSelector selectedSegmentIndex] == 1;
|
||||
BOOL multiController = [self.multiControllerSelector selectedSegmentIndex] == 1;
|
||||
BOOL swapABXYButtons = [self.swapABXYButtonsSelector selectedSegmentIndex] == 1;
|
||||
@@ -1085,6 +1128,8 @@ BOOL isCustomResolution(CGSize res) {
|
||||
NSInteger touchMode = [self.touchModeSelector selectedSegmentIndex];
|
||||
NSInteger statsOverlayLevel = [self.statsOverlaySelector selectedSegmentIndex];
|
||||
BOOL statsOverlayEnabled = statsOverlayLevel != 0;
|
||||
BOOL statsOverlay = [self.statsOverlaySelector selectedSegmentIndex] == 1;
|
||||
BOOL enableGraphs = [self.enableGraphsSelector selectedSegmentIndex] == 1;
|
||||
BOOL enableHdr = [self.hdrSelector selectedSegmentIndex] == 1;
|
||||
BOOL unlockDisplayOrientation = [self.unlockDisplayOrientationSelector selectedSegmentIndex] == 1;
|
||||
NSInteger resolutionSelected = [self.resolutionSelector selectedSegmentIndex];
|
||||
@@ -1117,16 +1162,20 @@ BOOL isCustomResolution(CGSize res) {
|
||||
preferredCodec:preferredCodec
|
||||
enableYUV444:enableYUV444
|
||||
useFramePacing:useFramePacing
|
||||
frameQueueSize:_frameQueueSize
|
||||
enableHdr:enableHdr
|
||||
btMouseSupport:btMouseSupport
|
||||
// absoluteTouchMode:absoluteTouchMode
|
||||
touchMode:touchMode
|
||||
statsOverlayLevel:statsOverlayLevel
|
||||
statsOverlayEnabled:statsOverlayEnabled
|
||||
unlockDisplayOrientation:unlockDisplayOrientation
|
||||
resolutionSelected:resolutionSelected
|
||||
externalDisplayMode:externalDisplayMode
|
||||
localMousePointerMode:localMousePointerMode];
|
||||
statsOverlay:statsOverlay
|
||||
enableGraphs:enableGraphs
|
||||
graphOpacity:_graphOpacity
|
||||
renderingBackend:renderingBackend];
|
||||
}
|
||||
|
||||
- (void)didReceiveMemoryWarning {
|
||||
|
||||
@@ -0,0 +1,714 @@
|
||||
//
|
||||
// StreamFrameViewController.m
|
||||
// Moonlight
|
||||
//
|
||||
// Created by Diego Waxemberg on 1/18/14.
|
||||
// Copyright (c) 2015 Moonlight Stream. All rights reserved.
|
||||
//
|
||||
|
||||
#import "StreamFrameViewController.h"
|
||||
#import "MainFrameViewController.h"
|
||||
#import "VideoDecoderRenderer.h"
|
||||
#import "StreamManager.h"
|
||||
#import "ControllerSupport.h"
|
||||
#import "DataManager.h"
|
||||
|
||||
#include <sys/socket.h>
|
||||
#include <netinet/in.h>
|
||||
#include <arpa/inet.h>
|
||||
#include <Limelight.h>
|
||||
|
||||
#if TARGET_OS_TV
|
||||
#import <AVFoundation/AVDisplayCriteria.h>
|
||||
#import <AVKit/AVDisplayManager.h>
|
||||
#import <AVKit/UIWindow.h>
|
||||
#endif
|
||||
|
||||
@interface AVDisplayCriteria()
|
||||
@property(readonly) int videoDynamicRange;
|
||||
@property(readonly, nonatomic) float refreshRate;
|
||||
- (id)initWithRefreshRate:(float)arg1 videoDynamicRange:(int)arg2;
|
||||
@end
|
||||
|
||||
@implementation StreamFrameViewController {
|
||||
ControllerSupport *_controllerSupport;
|
||||
StreamManager *_streamMan;
|
||||
TemporarySettings *_settings;
|
||||
NSTimer *_inactivityTimer;
|
||||
NSTimer *_statsUpdateTimer;
|
||||
UITapGestureRecognizer *_menuTapGestureRecognizer;
|
||||
UITapGestureRecognizer *_menuDoubleTapGestureRecognizer;
|
||||
UITapGestureRecognizer *_playPauseTapGestureRecognizer;
|
||||
UITextView *_overlayView;
|
||||
UILabel *_stageLabel;
|
||||
UILabel *_tipLabel;
|
||||
UIActivityIndicatorView *_spinner;
|
||||
StreamView *_streamView;
|
||||
UIScrollView *_scrollView;
|
||||
BOOL _userIsInteracting;
|
||||
CGSize _keyboardSize;
|
||||
|
||||
#if !TARGET_OS_TV
|
||||
UIScreenEdgePanGestureRecognizer *_exitSwipeRecognizer;
|
||||
#endif
|
||||
}
|
||||
|
||||
- (void)viewDidAppear:(BOOL)animated
|
||||
{
|
||||
[super viewDidAppear:animated];
|
||||
|
||||
#if !TARGET_OS_TV
|
||||
[[self revealViewController] setPrimaryViewController:self];
|
||||
#endif
|
||||
}
|
||||
|
||||
#if TARGET_OS_TV
|
||||
- (void)controllerPauseButtonPressed:(id)sender { }
|
||||
- (void)controllerPauseButtonDoublePressed:(id)sender {
|
||||
Log(LOG_I, @"Menu double-pressed -- backing out of stream");
|
||||
[self returnToMainFrame];
|
||||
}
|
||||
- (void)controllerPlayPauseButtonPressed:(id)sender {
|
||||
Log(LOG_I, @"Play/Pause button pressed -- backing out of stream");
|
||||
[self returnToMainFrame];
|
||||
}
|
||||
#endif
|
||||
|
||||
|
||||
- (void)viewDidLoad
|
||||
{
|
||||
[super viewDidLoad];
|
||||
|
||||
[self.navigationController setNavigationBarHidden:YES animated:YES];
|
||||
|
||||
[UIApplication sharedApplication].idleTimerDisabled = YES;
|
||||
|
||||
_settings = [[[DataManager alloc] init] getSettings];
|
||||
|
||||
_stageLabel = [[UILabel alloc] init];
|
||||
[_stageLabel setUserInteractionEnabled:NO];
|
||||
[_stageLabel setText:[NSString stringWithFormat:@"Starting %@...", self.streamConfig.appName]];
|
||||
[_stageLabel sizeToFit];
|
||||
_stageLabel.textAlignment = NSTextAlignmentCenter;
|
||||
_stageLabel.textColor = [UIColor whiteColor];
|
||||
_stageLabel.center = CGPointMake(self.view.frame.size.width / 2, self.view.frame.size.height / 2);
|
||||
|
||||
_spinner = [[UIActivityIndicatorView alloc] init];
|
||||
[_spinner setUserInteractionEnabled:NO];
|
||||
#if TARGET_OS_TV
|
||||
[_spinner setActivityIndicatorViewStyle:UIActivityIndicatorViewStyleWhiteLarge];
|
||||
#else
|
||||
[_spinner setActivityIndicatorViewStyle:UIActivityIndicatorViewStyleWhite];
|
||||
#endif
|
||||
[_spinner sizeToFit];
|
||||
[_spinner startAnimating];
|
||||
_spinner.center = CGPointMake(self.view.frame.size.width / 2, self.view.frame.size.height / 2 - _stageLabel.frame.size.height - _spinner.frame.size.height);
|
||||
|
||||
_controllerSupport = [[ControllerSupport alloc] initWithConfig:self.streamConfig delegate:self];
|
||||
_inactivityTimer = nil;
|
||||
|
||||
_streamView = [[StreamView alloc] initWithFrame:self.view.frame];
|
||||
[_streamView setupStreamView:_controllerSupport interactionDelegate:self config:self.streamConfig];
|
||||
|
||||
#if TARGET_OS_TV
|
||||
if (!_menuTapGestureRecognizer || !_menuDoubleTapGestureRecognizer || !_playPauseTapGestureRecognizer) {
|
||||
_menuTapGestureRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(controllerPauseButtonPressed:)];
|
||||
_menuTapGestureRecognizer.allowedPressTypes = @[@(UIPressTypeMenu)];
|
||||
|
||||
_playPauseTapGestureRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(controllerPlayPauseButtonPressed:)];
|
||||
_playPauseTapGestureRecognizer.allowedPressTypes = @[@(UIPressTypePlayPause)];
|
||||
|
||||
_menuDoubleTapGestureRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(controllerPauseButtonDoublePressed:)];
|
||||
_menuDoubleTapGestureRecognizer.numberOfTapsRequired = 2;
|
||||
[_menuTapGestureRecognizer requireGestureRecognizerToFail:_menuDoubleTapGestureRecognizer];
|
||||
_menuDoubleTapGestureRecognizer.allowedPressTypes = @[@(UIPressTypeMenu)];
|
||||
}
|
||||
|
||||
[self.view addGestureRecognizer:_menuTapGestureRecognizer];
|
||||
[self.view addGestureRecognizer:_menuDoubleTapGestureRecognizer];
|
||||
[self.view addGestureRecognizer:_playPauseTapGestureRecognizer];
|
||||
|
||||
#else
|
||||
_exitSwipeRecognizer = [[UIScreenEdgePanGestureRecognizer alloc] initWithTarget:self action:@selector(edgeSwiped)];
|
||||
_exitSwipeRecognizer.edges = UIRectEdgeLeft;
|
||||
_exitSwipeRecognizer.delaysTouchesBegan = NO;
|
||||
_exitSwipeRecognizer.delaysTouchesEnded = NO;
|
||||
|
||||
[self.view addGestureRecognizer:_exitSwipeRecognizer];
|
||||
#endif
|
||||
|
||||
_tipLabel = [[UILabel alloc] init];
|
||||
[_tipLabel setUserInteractionEnabled:NO];
|
||||
|
||||
#if TARGET_OS_TV
|
||||
[_tipLabel setText:@"Tip: Tap the Play/Pause button on the Apple TV Remote to disconnect from your PC"];
|
||||
#else
|
||||
[_tipLabel setText:@"Tip: Swipe from the left edge to disconnect from your PC"];
|
||||
#endif
|
||||
|
||||
[_tipLabel sizeToFit];
|
||||
_tipLabel.textColor = [UIColor whiteColor];
|
||||
_tipLabel.textAlignment = NSTextAlignmentCenter;
|
||||
_tipLabel.center = CGPointMake(self.view.frame.size.width / 2, self.view.frame.size.height * 0.9);
|
||||
|
||||
_streamMan = [[StreamManager alloc] initWithConfig:self.streamConfig
|
||||
renderView:_streamView
|
||||
connectionCallbacks:self];
|
||||
NSOperationQueue* opQueue = [[NSOperationQueue alloc] init];
|
||||
[opQueue addOperation:_streamMan];
|
||||
|
||||
[[NSNotificationCenter defaultCenter] addObserver:self
|
||||
selector:@selector(applicationWillResignActive:)
|
||||
name:UIApplicationWillResignActiveNotification
|
||||
object:nil];
|
||||
|
||||
[[NSNotificationCenter defaultCenter] addObserver: self
|
||||
selector: @selector(applicationDidBecomeActive:)
|
||||
name: UIApplicationDidBecomeActiveNotification
|
||||
object: nil];
|
||||
|
||||
[[NSNotificationCenter defaultCenter] addObserver: self
|
||||
selector: @selector(applicationDidEnterBackground:)
|
||||
name: UIApplicationDidEnterBackgroundNotification
|
||||
object: nil];
|
||||
|
||||
#if 0
|
||||
// FIXME: This doesn't work reliably on iPad for some reason. Showing and hiding the keyboard
|
||||
// several times in a row will not correctly restore the state of the UIScrollView.
|
||||
[[NSNotificationCenter defaultCenter] addObserver: self
|
||||
selector: @selector(keyboardWillShow:)
|
||||
name: UIKeyboardWillShowNotification
|
||||
object: nil];
|
||||
|
||||
[[NSNotificationCenter defaultCenter] addObserver: self
|
||||
selector: @selector(keyboardWillHide:)
|
||||
name: UIKeyboardWillHideNotification
|
||||
object: nil];
|
||||
#endif
|
||||
|
||||
// Only enable scroll and zoom in absolute touch mode
|
||||
if (_settings.absoluteTouchMode) {
|
||||
_scrollView = [[UIScrollView alloc] initWithFrame:self.view.frame];
|
||||
#if !TARGET_OS_TV
|
||||
[_scrollView.panGestureRecognizer setMinimumNumberOfTouches:2];
|
||||
#endif
|
||||
[_scrollView setShowsHorizontalScrollIndicator:NO];
|
||||
[_scrollView setShowsVerticalScrollIndicator:NO];
|
||||
[_scrollView setDelegate:self];
|
||||
[_scrollView setMaximumZoomScale:10.0f];
|
||||
|
||||
// Add StreamView inside a UIScrollView for absolute mode
|
||||
[_scrollView addSubview:_streamView];
|
||||
[self.view addSubview:_scrollView];
|
||||
}
|
||||
else {
|
||||
// Add StreamView directly in relative mode
|
||||
[self.view addSubview:_streamView];
|
||||
}
|
||||
|
||||
[self.view addSubview:_stageLabel];
|
||||
[self.view addSubview:_spinner];
|
||||
[self.view addSubview:_tipLabel];
|
||||
}
|
||||
|
||||
- (UIView *)viewForZoomingInScrollView:(UIScrollView *)scrollView {
|
||||
return _streamView;
|
||||
}
|
||||
|
||||
- (void)willMoveToParentViewController:(UIViewController *)parent {
|
||||
// Only cleanup when we're being destroyed
|
||||
if (parent == nil) {
|
||||
[_controllerSupport cleanup];
|
||||
[UIApplication sharedApplication].idleTimerDisabled = NO;
|
||||
[_streamMan stopStream];
|
||||
if (_inactivityTimer != nil) {
|
||||
[_inactivityTimer invalidate];
|
||||
_inactivityTimer = nil;
|
||||
}
|
||||
[[NSNotificationCenter defaultCenter] removeObserver:self];
|
||||
}
|
||||
}
|
||||
|
||||
#if 0
|
||||
- (void)keyboardWillShow:(NSNotification *)notification {
|
||||
_keyboardSize = [[[notification userInfo] objectForKey:UIKeyboardFrameBeginUserInfoKey] CGRectValue].size;
|
||||
|
||||
[UIView animateWithDuration:0.3 animations:^{
|
||||
CGRect frame = self->_scrollView.frame;
|
||||
frame.size.height -= self->_keyboardSize.height;
|
||||
self->_scrollView.frame = frame;
|
||||
}];
|
||||
}
|
||||
|
||||
-(void)keyboardWillHide:(NSNotification *)notification {
|
||||
// NOTE: UIKeyboardFrameEndUserInfoKey returns a different keyboard size
|
||||
// than UIKeyboardFrameBeginUserInfoKey, so it's unsuitable for use here
|
||||
// to undo the changes made by keyboardWillShow.
|
||||
|
||||
[UIView animateWithDuration:0.3 animations:^{
|
||||
CGRect frame = self->_scrollView.frame;
|
||||
frame.size.height += self->_keyboardSize.height;
|
||||
self->_scrollView.frame = frame;
|
||||
}];
|
||||
}
|
||||
#endif
|
||||
|
||||
- (void)updateStatsOverlay {
|
||||
NSString* overlayText = [self->_streamMan getStatsOverlayText];
|
||||
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
[self updateOverlayText:overlayText];
|
||||
});
|
||||
}
|
||||
|
||||
- (void)updateOverlayText:(NSString*)text {
|
||||
if (_overlayView == nil) {
|
||||
_overlayView = [[UITextView alloc] init];
|
||||
#if !TARGET_OS_TV
|
||||
[_overlayView setEditable:NO];
|
||||
#endif
|
||||
[_overlayView setUserInteractionEnabled:NO];
|
||||
[_overlayView setSelectable:NO];
|
||||
[_overlayView setScrollEnabled:NO];
|
||||
|
||||
// HACK: If not using stats overlay, center the text
|
||||
if (_statsUpdateTimer == nil) {
|
||||
[_overlayView setTextAlignment:NSTextAlignmentCenter];
|
||||
}
|
||||
|
||||
[_overlayView setTextColor:[UIColor lightGrayColor]];
|
||||
[_overlayView setBackgroundColor:[UIColor blackColor]];
|
||||
#if TARGET_OS_TV
|
||||
[_overlayView setFont:[UIFont systemFontOfSize:24]];
|
||||
#else
|
||||
[_overlayView setFont:[UIFont systemFontOfSize:12]];
|
||||
#endif
|
||||
[_overlayView setAlpha:0.5];
|
||||
[self.view addSubview:_overlayView];
|
||||
}
|
||||
|
||||
if (text != nil) {
|
||||
// We set our bounds to the maximum width in order to work around a bug where
|
||||
// sizeToFit interacts badly with the UITextView's line breaks, causing the
|
||||
// width to get smaller and smaller each time as more line breaks are inserted.
|
||||
[_overlayView setBounds:CGRectMake(self.view.frame.origin.x,
|
||||
_overlayView.frame.origin.y,
|
||||
self.view.frame.size.width,
|
||||
_overlayView.frame.size.height)];
|
||||
[_overlayView setText:text];
|
||||
[_overlayView sizeToFit];
|
||||
[_overlayView setCenter:CGPointMake(self.view.frame.size.width / 2, _overlayView.frame.size.height / 2)];
|
||||
[_overlayView setHidden:NO];
|
||||
}
|
||||
else {
|
||||
[_overlayView setHidden:YES];
|
||||
}
|
||||
}
|
||||
|
||||
- (void) returnToMainFrame {
|
||||
// Reset display mode back to default
|
||||
[self updatePreferredDisplayMode:NO];
|
||||
|
||||
[_statsUpdateTimer invalidate];
|
||||
_statsUpdateTimer = nil;
|
||||
|
||||
[self.navigationController popToRootViewControllerAnimated:YES];
|
||||
}
|
||||
|
||||
// This will fire if the user opens control center or gets a low battery message
|
||||
- (void)applicationWillResignActive:(NSNotification *)notification {
|
||||
if (_inactivityTimer != nil) {
|
||||
[_inactivityTimer invalidate];
|
||||
}
|
||||
|
||||
#if !TARGET_OS_TV
|
||||
// Terminate the stream if the app is inactive for 60 seconds
|
||||
Log(LOG_I, @"Starting inactivity termination timer");
|
||||
_inactivityTimer = [NSTimer scheduledTimerWithTimeInterval:60
|
||||
target:self
|
||||
selector:@selector(inactiveTimerExpired:)
|
||||
userInfo:nil
|
||||
repeats:NO];
|
||||
#endif
|
||||
}
|
||||
|
||||
- (void)inactiveTimerExpired:(NSTimer*)timer {
|
||||
Log(LOG_I, @"Terminating stream after inactivity");
|
||||
|
||||
[self returnToMainFrame];
|
||||
|
||||
_inactivityTimer = nil;
|
||||
}
|
||||
|
||||
- (void)applicationDidBecomeActive:(NSNotification *)notification {
|
||||
// Stop the background timer, since we're foregrounded again
|
||||
if (_inactivityTimer != nil) {
|
||||
Log(LOG_I, @"Stopping inactivity timer after becoming active again");
|
||||
[_inactivityTimer invalidate];
|
||||
_inactivityTimer = nil;
|
||||
}
|
||||
}
|
||||
|
||||
// This fires when the home button is pressed
|
||||
- (void)applicationDidEnterBackground:(UIApplication *)application {
|
||||
Log(LOG_I, @"Terminating stream immediately for backgrounding");
|
||||
|
||||
if (_inactivityTimer != nil) {
|
||||
[_inactivityTimer invalidate];
|
||||
_inactivityTimer = nil;
|
||||
}
|
||||
|
||||
[self returnToMainFrame];
|
||||
}
|
||||
|
||||
- (void)edgeSwiped {
|
||||
Log(LOG_I, @"User swiped to end stream");
|
||||
|
||||
[self returnToMainFrame];
|
||||
}
|
||||
|
||||
- (void) connectionStarted {
|
||||
Log(LOG_I, @"Connection started");
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
// Leave the spinner spinning until it's obscured by
|
||||
// the first frame of video.
|
||||
self->_stageLabel.hidden = YES;
|
||||
self->_tipLabel.hidden = YES;
|
||||
|
||||
[self->_streamView showOnScreenControls];
|
||||
|
||||
[self->_controllerSupport connectionEstablished];
|
||||
|
||||
if (self->_settings.statsOverlay) {
|
||||
self->_statsUpdateTimer = [NSTimer scheduledTimerWithTimeInterval:1.0f
|
||||
target:self
|
||||
selector:@selector(updateStatsOverlay)
|
||||
userInfo:nil
|
||||
repeats:YES];
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
- (void)connectionTerminated:(int)errorCode {
|
||||
Log(LOG_I, @"Connection terminated: %d", errorCode);
|
||||
|
||||
unsigned int portFlags = LiGetPortFlagsFromTerminationErrorCode(errorCode);
|
||||
unsigned int portTestResults = LiTestClientConnectivity(CONN_TEST_SERVER, 443, portFlags);
|
||||
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
// Allow the display to go to sleep now
|
||||
[UIApplication sharedApplication].idleTimerDisabled = NO;
|
||||
|
||||
NSString* title;
|
||||
NSString* message;
|
||||
|
||||
if (portTestResults != ML_TEST_RESULT_INCONCLUSIVE && portTestResults != 0) {
|
||||
title = @"Connection Error";
|
||||
message = @"Your device's network connection is blocking Moonlight. Streaming may not work while connected to this network.";
|
||||
}
|
||||
else {
|
||||
switch (errorCode) {
|
||||
case ML_ERROR_GRACEFUL_TERMINATION:
|
||||
[self returnToMainFrame];
|
||||
return;
|
||||
|
||||
case ML_ERROR_NO_VIDEO_TRAFFIC:
|
||||
title = @"Connection Error";
|
||||
message = @"No video received from host.";
|
||||
if (portFlags != 0) {
|
||||
char failingPorts[256];
|
||||
LiStringifyPortFlags(portFlags, "\n", failingPorts, sizeof(failingPorts));
|
||||
message = [message stringByAppendingString:[NSString stringWithFormat:@"\n\nCheck your firewall and port forwarding rules for port(s):\n%s", failingPorts]];
|
||||
}
|
||||
break;
|
||||
|
||||
case ML_ERROR_NO_VIDEO_FRAME:
|
||||
title = @"Connection Error";
|
||||
message = @"Your network connection isn't performing well. Reduce your video bitrate setting or try a faster connection.";
|
||||
break;
|
||||
|
||||
case ML_ERROR_UNEXPECTED_EARLY_TERMINATION:
|
||||
case ML_ERROR_PROTECTED_CONTENT:
|
||||
title = @"Connection Error";
|
||||
message = @"Something went wrong on your host PC when starting the stream.\n\nMake sure you don't have any DRM-protected content open on your host PC. You can also try restarting your host PC.\n\nIf the issue persists, try reinstalling your GPU drivers and GeForce Experience.";
|
||||
break;
|
||||
|
||||
case ML_ERROR_FRAME_CONVERSION:
|
||||
title = @"Connection Error";
|
||||
message = @"The host PC reported a fatal video encoding error.\n\nTry disabling HDR mode, changing the streaming resolution, or changing your host PC's display resolution.";
|
||||
break;
|
||||
|
||||
default:
|
||||
{
|
||||
NSString* errorString;
|
||||
if (abs(errorCode) > 1000) {
|
||||
// We'll assume large errors are hex values
|
||||
errorString = [NSString stringWithFormat:@"%08X", (uint32_t)errorCode];
|
||||
}
|
||||
else {
|
||||
// Smaller values will just be printed as decimal (probably errno.h values)
|
||||
errorString = [NSString stringWithFormat:@"%d", errorCode];
|
||||
}
|
||||
|
||||
title = @"Connection Terminated";
|
||||
message = [NSString stringWithFormat: @"The connection was terminated\n\nError code: %@", errorString];
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
UIAlertController* conTermAlert = [UIAlertController alertControllerWithTitle:title
|
||||
message:message
|
||||
preferredStyle:UIAlertControllerStyleAlert];
|
||||
[Utils addHelpOptionToDialog:conTermAlert];
|
||||
[conTermAlert addAction:[UIAlertAction actionWithTitle:@"OK" style:UIAlertActionStyleDefault handler:^(UIAlertAction* action){
|
||||
[self returnToMainFrame];
|
||||
}]];
|
||||
[self presentViewController:conTermAlert animated:YES completion:nil];
|
||||
});
|
||||
|
||||
[_streamMan stopStream];
|
||||
}
|
||||
|
||||
- (void) stageStarting:(const char*)stageName {
|
||||
Log(LOG_I, @"Starting %s", stageName);
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
NSString* lowerCase = [NSString stringWithFormat:@"%s in progress...", stageName];
|
||||
NSString* titleCase = [[[lowerCase substringToIndex:1] uppercaseString] stringByAppendingString:[lowerCase substringFromIndex:1]];
|
||||
[self->_stageLabel setText:titleCase];
|
||||
[self->_stageLabel sizeToFit];
|
||||
self->_stageLabel.center = CGPointMake(self.view.frame.size.width / 2, self->_stageLabel.center.y);
|
||||
});
|
||||
}
|
||||
|
||||
- (void) stageComplete:(const char*)stageName {
|
||||
}
|
||||
|
||||
- (void) stageFailed:(const char*)stageName withError:(int)errorCode portTestFlags:(int)portTestFlags {
|
||||
Log(LOG_I, @"Stage %s failed: %d", stageName, errorCode);
|
||||
|
||||
unsigned int portTestResults = LiTestClientConnectivity(CONN_TEST_SERVER, 443, portTestFlags);
|
||||
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
// Allow the display to go to sleep now
|
||||
[UIApplication sharedApplication].idleTimerDisabled = NO;
|
||||
|
||||
NSString* message = [NSString stringWithFormat:@"%s failed with error %d", stageName, errorCode];
|
||||
if (portTestFlags != 0) {
|
||||
char failingPorts[256];
|
||||
LiStringifyPortFlags(portTestFlags, "\n", failingPorts, sizeof(failingPorts));
|
||||
message = [message stringByAppendingString:[NSString stringWithFormat:@"\n\nCheck your firewall and port forwarding rules for port(s):\n%s", failingPorts]];
|
||||
}
|
||||
if (portTestResults != ML_TEST_RESULT_INCONCLUSIVE && portTestResults != 0) {
|
||||
message = [message stringByAppendingString:@"\n\nYour device's network connection is blocking Moonlight. Streaming may not work while connected to this network."];
|
||||
}
|
||||
|
||||
UIAlertController* alert = [UIAlertController alertControllerWithTitle:@"Connection Failed"
|
||||
message:message
|
||||
preferredStyle:UIAlertControllerStyleAlert];
|
||||
[Utils addHelpOptionToDialog:alert];
|
||||
[alert addAction:[UIAlertAction actionWithTitle:@"OK" style:UIAlertActionStyleDefault handler:^(UIAlertAction* action){
|
||||
[self returnToMainFrame];
|
||||
}]];
|
||||
[self presentViewController:alert animated:YES completion:nil];
|
||||
});
|
||||
|
||||
[_streamMan stopStream];
|
||||
}
|
||||
|
||||
- (void) launchFailed:(NSString*)message {
|
||||
Log(LOG_I, @"Launch failed: %@", message);
|
||||
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
// Allow the display to go to sleep now
|
||||
[UIApplication sharedApplication].idleTimerDisabled = NO;
|
||||
|
||||
UIAlertController* alert = [UIAlertController alertControllerWithTitle:@"Connection Error"
|
||||
message:message
|
||||
preferredStyle:UIAlertControllerStyleAlert];
|
||||
[Utils addHelpOptionToDialog:alert];
|
||||
[alert addAction:[UIAlertAction actionWithTitle:@"OK" style:UIAlertActionStyleDefault handler:^(UIAlertAction* action){
|
||||
[self returnToMainFrame];
|
||||
}]];
|
||||
[self presentViewController:alert animated:YES completion:nil];
|
||||
});
|
||||
}
|
||||
|
||||
- (void)rumble:(unsigned short)controllerNumber lowFreqMotor:(unsigned short)lowFreqMotor highFreqMotor:(unsigned short)highFreqMotor {
|
||||
Log(LOG_I, @"Rumble on gamepad %d: %04x %04x", controllerNumber, lowFreqMotor, highFreqMotor);
|
||||
|
||||
[_controllerSupport rumble:controllerNumber lowFreqMotor:lowFreqMotor highFreqMotor:highFreqMotor];
|
||||
}
|
||||
|
||||
- (void) rumbleTriggers:(uint16_t)controllerNumber leftTrigger:(uint16_t)leftTrigger rightTrigger:(uint16_t)rightTrigger {
|
||||
Log(LOG_I, @"Trigger rumble on gamepad %d: %04x %04x", controllerNumber, leftTrigger, rightTrigger);
|
||||
|
||||
[_controllerSupport rumbleTriggers:controllerNumber leftTrigger:leftTrigger rightTrigger:rightTrigger];
|
||||
}
|
||||
|
||||
- (void) setMotionEventState:(uint16_t)controllerNumber motionType:(uint8_t)motionType reportRateHz:(uint16_t)reportRateHz {
|
||||
Log(LOG_I, @"Set motion state on gamepad %d: %02x %u Hz", controllerNumber, motionType, reportRateHz);
|
||||
|
||||
[_controllerSupport setMotionEventState:controllerNumber motionType:motionType reportRateHz:reportRateHz];
|
||||
}
|
||||
|
||||
- (void) setControllerLed:(uint16_t)controllerNumber r:(uint8_t)r g:(uint8_t)g b:(uint8_t)b {
|
||||
Log(LOG_I, @"Set controller LED on gamepad %d: l%02x%02x%02x", controllerNumber, r, g, b);
|
||||
|
||||
[_controllerSupport setControllerLed:controllerNumber r:r g:g b:b];
|
||||
}
|
||||
|
||||
- (void)connectionStatusUpdate:(int)status {
|
||||
Log(LOG_W, @"Connection status update: %d", status);
|
||||
|
||||
// The stats overlay takes precedence over these warnings
|
||||
if (_statsUpdateTimer != nil) {
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
switch (status) {
|
||||
case CONN_STATUS_OKAY:
|
||||
[self updateOverlayText:nil];
|
||||
break;
|
||||
|
||||
case CONN_STATUS_POOR:
|
||||
if (self->_streamConfig.bitRate > 5000) {
|
||||
[self updateOverlayText:@"Slow connection to PC\nReduce your bitrate"];
|
||||
}
|
||||
else {
|
||||
[self updateOverlayText:@"Poor connection to PC"];
|
||||
}
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
- (void) updatePreferredDisplayMode:(BOOL)streamActive {
|
||||
#if TARGET_OS_TV
|
||||
if (@available(tvOS 11.2, *)) {
|
||||
UIWindow* window = [[[UIApplication sharedApplication] delegate] window];
|
||||
AVDisplayManager* displayManager = [window avDisplayManager];
|
||||
|
||||
// This logic comes from Kodi and MrMC
|
||||
if (streamActive) {
|
||||
int dynamicRange;
|
||||
|
||||
if (LiGetCurrentHostDisplayHdrMode()) {
|
||||
dynamicRange = 2; // HDR10
|
||||
}
|
||||
else {
|
||||
dynamicRange = 0; // SDR
|
||||
}
|
||||
|
||||
AVDisplayCriteria* displayCriteria = [[AVDisplayCriteria alloc] initWithRefreshRate:[_settings.framerate floatValue]
|
||||
videoDynamicRange:dynamicRange];
|
||||
displayManager.preferredDisplayCriteria = displayCriteria;
|
||||
}
|
||||
else {
|
||||
// Switch back to the default display mode
|
||||
displayManager.preferredDisplayCriteria = nil;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
- (void) setHdrMode:(bool)enabled {
|
||||
Log(LOG_I, @"HDR is now: %s", enabled ? "active" : "inactive");
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
[self updatePreferredDisplayMode:YES];
|
||||
});
|
||||
}
|
||||
|
||||
- (void) videoContentShown {
|
||||
[_spinner stopAnimating];
|
||||
[self.view setBackgroundColor:[UIColor blackColor]];
|
||||
}
|
||||
|
||||
- (void)didReceiveMemoryWarning
|
||||
{
|
||||
[super didReceiveMemoryWarning];
|
||||
// Dispose of any resources that can be recreated.
|
||||
}
|
||||
|
||||
- (void)gamepadPresenceChanged {
|
||||
#if !TARGET_OS_TV
|
||||
if (@available(iOS 11.0, *)) {
|
||||
[self setNeedsUpdateOfHomeIndicatorAutoHidden];
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
- (void)mousePresenceChanged {
|
||||
#if !TARGET_OS_TV
|
||||
if (@available(iOS 14.0, *)) {
|
||||
[self setNeedsUpdateOfPrefersPointerLocked];
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
- (void) streamExitRequested {
|
||||
Log(LOG_I, @"Gamepad combo requested stream exit");
|
||||
|
||||
[self returnToMainFrame];
|
||||
}
|
||||
|
||||
- (void)userInteractionBegan {
|
||||
// Disable hiding home bar when user is interacting.
|
||||
// iOS will force it to be shown anyway, but it will
|
||||
// also discard our edges deferring system gestures unless
|
||||
// we willingly give up home bar hiding preference.
|
||||
_userIsInteracting = YES;
|
||||
#if !TARGET_OS_TV
|
||||
if (@available(iOS 11.0, *)) {
|
||||
[self setNeedsUpdateOfHomeIndicatorAutoHidden];
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
- (void)userInteractionEnded {
|
||||
// Enable home bar hiding again if conditions allow
|
||||
_userIsInteracting = NO;
|
||||
#if !TARGET_OS_TV
|
||||
if (@available(iOS 11.0, *)) {
|
||||
[self setNeedsUpdateOfHomeIndicatorAutoHidden];
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
#if !TARGET_OS_TV
|
||||
// Require a confirmation when streaming to activate a system gesture
|
||||
- (UIRectEdge)preferredScreenEdgesDeferringSystemGestures {
|
||||
return UIRectEdgeAll;
|
||||
}
|
||||
|
||||
- (BOOL)prefersHomeIndicatorAutoHidden {
|
||||
if ([_controllerSupport getConnectedGamepadCount] > 0 &&
|
||||
[_streamView getCurrentOscState] == OnScreenControlsLevelOff &&
|
||||
_userIsInteracting == NO) {
|
||||
// Autohide the home bar when a gamepad is connected
|
||||
// and the on-screen controls are disabled. We can't
|
||||
// do this all the time because any touch on the display
|
||||
// will cause the home indicator to reappear, and our
|
||||
// preferredScreenEdgesDeferringSystemGestures will also
|
||||
// be suppressed (leading to possible errant exits of the
|
||||
// stream).
|
||||
return YES;
|
||||
}
|
||||
|
||||
return NO;
|
||||
}
|
||||
|
||||
- (BOOL)shouldAutorotate {
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (BOOL)prefersPointerLocked {
|
||||
// Pointer lock breaks the UIKit mouse APIs, which is a problem because
|
||||
// GCMouse is horribly broken on iOS 14.0 for certain mice. Only lock
|
||||
// the cursor if there is a GCMouse present.
|
||||
return [GCMouse mice].count > 0;
|
||||
}
|
||||
#endif
|
||||
|
||||
@end
|
||||
Reference in new issue
Block a user