fix: build

This commit is contained in:
Acaki
2025-07-12 02:00:33 +08:00
parent df2b67ac99
commit d69688678d
36 changed files with 565 additions and 836 deletions
View File
-40
View File
@@ -1,40 +0,0 @@
//
// 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
-36
View File
@@ -1,36 +0,0 @@
#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
-425
View File
@@ -1,425 +0,0 @@
#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
-37
View File
@@ -1,37 +0,0 @@
//
// 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
-13
View File
@@ -1,13 +0,0 @@
// 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
-23
View File
@@ -1,23 +0,0 @@
#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
-524
View File
@@ -1,524 +0,0 @@
#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 *)&paramBuffer 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
-36
View File
@@ -1,36 +0,0 @@
#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
-205
View File
@@ -1,205 +0,0 @@
// 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
-24
View File
@@ -1,24 +0,0 @@
#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
-144
View File
@@ -1,144 +0,0 @@
/*
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
-63
View File
@@ -1,63 +0,0 @@
#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);
}
-30
View File
@@ -1,30 +0,0 @@
#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
-95
View File
@@ -1,95 +0,0 @@
#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
@@ -1128,7 +1128,6 @@ 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;
@@ -1172,7 +1171,6 @@ BOOL isCustomResolution(CGSize res) {
resolutionSelected:resolutionSelected
externalDisplayMode:externalDisplayMode
localMousePointerMode:localMousePointerMode];
statsOverlay:statsOverlay
enableGraphs:enableGraphs
graphOpacity:_graphOpacity
renderingBackend:renderingBackend];
@@ -1,714 +0,0 @@
//
// 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