fix: build
This commit is contained in:
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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 *)¶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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user