他们说我可以从渲染中分离更新.所以,如果我理解这一点,游戏引擎运行循环可以像这样工作:
>设置调用render方法的CADisplayLink.
> render方法在后台渲染当前世界模型.
> render方法然后在主线程上调用update方法.
因此,当它在后台渲染时,它可以同时为下一次迭代更新世界模型.
对我来说这一切都让人感到很不稳定.有人可以解释或链接到如何在现实中完成模型的并发渲染更新吗?令我难以理解的是,这不会导致问题,因为如果模型更新需要比渲染或其他方式更长的时间.谁等待什么,何时等待.
我试图理解的是,从理论上讲,从高层次的角度来看,这也是如何实现的.
解决方法
背景:单线程(即仅主线程)示例
我们首先考虑一个普通的单线程应用程序如何工作:
>用户事件进入主线程
>事件处理程序触发对控制器方法的调用.
>控制器方法更新模型状态.
>对模型状态的更改使视图状态无效. (即-setNeedsDisplay)
>当下一帧出现时,窗口服务器将触发从当前模型状态重新呈现视图状态并显示结果
请注意,步骤1-4可能会在步骤5的出现之间发生多次,但是,由于这是单线程应用程序,而第5步发生,步骤1-4没有发生,用户事件排队等待第5步完成.假设步骤1-4“非常快”,这通常会以预期的方式丢帧.
从主线程中解耦渲染
现在,让我们考虑您要将渲染卸载到后台线程的情况.在这种情况下,序列应如下所示:
>用户事件进入主线程
>事件处理程序触发对控制器方法的调用.
>控制器方法更新模型状态.
>对模型状态的更改将异步呈现任务排入队列以供后台执行.
>如果异步呈现任务完成,它会将生成的位图放在视图已知的某个位置,并在视图上调用-setNeedsDisplay.
>当下一帧出现时,窗口服务器将触发对视图的-drawRect调用,现在实现为从“已知共享位置”获取最近完成的位图并将其复制到视图中.
这里有一些细微差别.让我们首先考虑你只是试图将渲染与主线程分离的情况(暂时忽略多核的利用 – 稍后):
您几乎肯定不会想要同时运行多个渲染任务.一旦开始渲染帧,您可能不想取消/停止渲染它.您可能希望将未来的未启动渲染操作排队到单个插槽队列中,该队列始终包含最后排队的未启动渲染操作.这应该给你合理的帧丢弃行为,这样你就不会“落后”渲染帧,你应该放弃它.
如果存在完全渲染但尚未显示的帧,我认为您总是希望显示该帧.考虑到这一点,您不希望在视图上调用-setNeedsDisplay,直到位图完成并且在已知位置.
您需要跨线程同步访问.例如,当您将渲染操作排入队列时,最简单的方法是获取模型状态的只读快照,并将其传递给渲染操作,该操作只能从快照中读取.这使您无需与“实时”游戏模型同步(可能由您的控制器方法在主线程上进行突变以响应未来的用户事件.)另一个同步挑战是将完成的位图传递给视图并调用-setNeedsDisplay.最简单的方法可能是将图像作为视图的属性,并将该属性的设置(使用完成的图像)和调用-setNeedsDisplay传递给主线程.
这里有一个小问题:如果用户事件以高速率进入,并且您能够在单个显示帧(1/60秒)的持续时间内渲染多个帧,则最终可能会渲染掉落的位图在地上.这种方法的优点是始终在显示时为视图提供最新的帧(减少感知延迟),但它具有* dis *优势,它会导致渲染永远不会得到的帧的所有计算成本显示(即电源).这里的权利交易对于每种情况都会有所不同,并且可能包括更细粒度的调整.
利用多个核心 – 固有的并行渲染
如上所述,假设你已经将渲染从主线程中解耦,并且你的渲染操作本身就是可并行化的,那么只需将你的一个渲染操作并行化,同时继续以相同的方式与视图交互,你就应该获得多核并行性免费.也许您可以将每个帧划分为N个区块,其中N是核心数,然后一旦所有N个区块完成渲染,您可以将它们拼凑在一起并将它们传递给视图,就好像渲染操作是单片的一样.如果您正在使用模型的只读快照,则N个tile任务的设置成本应该是最小的(因为它们都可以使用相同的源模型.)
利用多个核心 – 固有的串行渲染
如果您的渲染操作本质上是串行的(在我的经验中大多数情况下),您使用多个核心的选择是在内核中使用尽可能多的渲染操作.当一个帧完成时,它将发出任何已排队或仍处于飞行状态的信号,但之前,它们可能放弃并取消的渲染操作,然后它将自己设置为由视图显示,就像在仅去耦示例中一样.
如在仅去耦情况中所提到的,这总是在显示时向视图提供最新的帧,但是它导致渲染从未显示的帧的所有计算(即功率)成本.
当模型慢时……
我没有解决实际上基于用户事件太慢的模型更新的情况,因为从某种意义上说,如果是这种情况,在很多方面,你不再关心渲染.如果模型甚至无法跟上,渲染如何才能跟上?此外,假设您找到了一种互锁渲染和模型计算的方法,渲染总是从模型计算中抢夺循环,根据定义,模型计算总是落后.换句话说,当事物本身无法每秒更新N次时,你不可能希望每秒渲染N次.
我可以设想一些情况,你可以将像连续运行的物理模拟这样的东西卸载到后台线程.这样的系统必须自己管理其实时性能,并假设它这样做,那么你就会遇到将来自该系统的结果与传入用户事件流同步的挑战.一团糟.
在常见的情况下,您确实希望事件处理和模型变异比实时更快,并且渲染是“困难的部分”.我很难想象一个有意义的案例,其中模型更新是限制因素,但你仍然关心解耦渲染的性能.
换句话说:如果你的模型只能以10Hz更新,那么以10Hz以上的速度更新你的视图是没有意义的.当用户事件的速度远远超过10Hz时,就会出现这种情况的主要挑战.这个挑战将是有意义地丢弃,采样或合并传入的事件,以保持有意义并提供良好的用户体验.
一些代码
以下是基于Xcode中的Cocoa应用程序模板,如何解耦背景渲染的简单示例. (我在编写完这个基于OS X的示例后,意识到问题是用ios标记的,所以我想这是“无论它值多少钱”)
@class MyModel; @interface NSAppDelegate : NSObject <NSApplicationDelegate> @property (assign) IBOutlet NSWindow *window; @property (nonatomic,readwrite,copy) MyModel* model; @end @interface MyModel : NSObject <NSMutableCopying> @property (nonatomic,readonly,assign) CGPoint lastMouseLocation; @end @interface MyMutableModel : MyModel @property (nonatomic,assign) CGPoint lastMouseLocation; @end @interface MyBackgroundRenderingView : NSView @property (nonatomic,assign) CGPoint coordinates; @end @interface MyViewController : NSViewController @end @implementation NSAppDelegate { MyViewController* _vc; NSTrackingArea* _trackingArea; } - (void)applicationDidFinishLaunching:(NSNotification *)aNotification { // Insert code here to initialize your application self.window.acceptsMouseMovedEvents = YES; int opts = (NSTrackingActiveAlways | NSTrackingInVisibleRect | NSTrackingMouseMoved); _trackingArea = [[NSTrackingArea alloc] initWithRect: [self.window.contentView bounds] options:opts owner:self userInfo:nil]; [self.window.contentView addTrackingArea: _trackingArea]; _vc = [[MyViewController alloc] initWithNibName: NSStringFromClass([MyViewController class]) bundle: [NSBundle mainBundle]]; _vc.representedObject = self; _vc.view.frame = [self.window.contentView bounds]; [self.window.contentView addSubview: _vc.view]; } - (void)mouseEntered:(NSEvent *)theEvent { } - (void)mouseExited:(NSEvent *)theEvent { } - (void)mouseMoved:(NSEvent *)theEvent { // Update the model for mouse movement. MyMutableModel* mutableModel = self.model.mutableCopy ?: [[MyMutableModel alloc] init]; mutableModel.lastMouseLocation = theEvent.locationInWindow; self.model = mutableModel; } @end @interface MyModel () // Re-declare privately so the setter exists for the mutable subclass to use @property (nonatomic,assign) CGPoint lastMouseLocation; @end @implementation MyModel @synthesize lastMouseLocation; - (id)copyWithZone:(NSZone *)zone { if ([self isMemberOfClass: [MyModel class]]) { return self; } MyModel* copy = [[MyModel alloc] init]; copy.lastMouseLocation = self.lastMouseLocation; return copy; } - (id)mutableCopyWithZone:(NSZone *)zone { MyMutableModel* copy = [[MyMutableModel alloc] init]; copy.lastMouseLocation = self.lastMouseLocation; return copy; } @end @implementation MyMutableModel @end @interface MyViewController (Downcast) - (MyBackgroundRenderingView*)view; // downcast @end @implementation MyViewController static void * const MyViewControllerKVOContext = (void*)&MyViewControllerKVOContext; - (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil { if (self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil]) { [self addObserver: self forKeyPath: @"representedObject.model.lastMouseLocation" options: NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew | NSKeyValueObservingOptionInitial context: MyViewControllerKVOContext]; } return self; } - (void)dealloc { [self removeObserver: self forKeyPath: @"representedObject.model.lastMouseLocation" context: MyViewControllerKVOContext]; } - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context { if (MyViewControllerKVOContext == context) { // update the view... NSValue* oldCoordinates = change[NSKeyValueChangeOldKey]; oldCoordinates = [oldCoordinates isKindOfClass: [NSValue class]] ? oldCoordinates : nil; NSValue* newCoordinates = change[NSKeyValueChangeNewKey]; newCoordinates = [newCoordinates isKindOfClass: [NSValue class]] ? newCoordinates : nil; CGPoint old = CGPointZero,new = CGPointZero; [oldCoordinates getValue: &old]; [newCoordinates getValue: &new]; if (!CGPointEqualToPoint(old,new)) { self.view.coordinates = new; } } else { [super observeValueForKeyPath:keyPath ofObject:object change:change context:context]; } } @end @interface MyBackgroundRenderingView () @property (nonatomic,retain) id toDisplay; // doesn't need to be atomic because it should only ever be used on the main thread. @end @implementation MyBackgroundRenderingView { // Pointer sized reads/ intptr_t _lastFrameStarted; intptr_t _lastFrameDisplayed; CGPoint _coordinates; } @synthesize coordinates = _coordinates; - (void)setCoordinates:(CGPoint)coordinates { _coordinates = coordinates; // instead of setNeedDisplay... [self doBackgroundRenderingForPoint: coordinates]; } - (void)setNeedsDisplay:(BOOL)flag { if (flag) { [self doBackgroundRenderingForPoint: self.coordinates]; } } - (void)doBackgroundRenderingForPoint: (CGPoint)value { NSAssert(NSThread.isMainThread,@"main thread only..."); const intptr_t thisFrame = _lastFrameStarted++; const NSSize imageSize = self.bounds.size; const NSRect imageRect = NSMakeRect(0,imageSize.width,imageSize.height); dispatch_async(dispatch_get_global_queue(0,0),^{ // If another frame is already queued up,don't bother starting this one if (_lastFrameStarted - 1 > thisFrame) { dispatch_async(dispatch_get_global_queue(0,^{ NSLog(@"Not rendering a frame because there's a more recent one queued up already."); }); return; } // introduce an arbitrary fake delay between 1ms and 1/15th of a second) const uint32_t delays = arc4random_uniform(65); for (NSUInteger i = 1; i < delays; i++) { // A later frame has been displayed. Give up on rendering this old frame. if (_lastFrameDisplayed > thisFrame) { dispatch_async(dispatch_get_global_queue(0,^{ NSLog(@"Aborting rendering a frame that wasn't ready in time"); }); return; } usleep(1000); } // render image... NSImage* image = [[NSImage alloc] initWithSize: imageSize]; [image lockFocus]; NSString* coordsString = [NSString stringWithFormat: @"%g,%g",value.x,value.y]; [coordsString drawInRect: imageRect withAttributes: nil]; [image unlockFocus]; NSArray* toDisplay = @[ image,@(thisFrame) ]; dispatch_async(dispatch_get_main_queue(),^{ self.toDisplay = toDisplay; [super setNeedsDisplay: YES]; }); }); } - (void)drawRect:(NSRect)dirtyRect { NSArray* toDisplay = self.toDisplay; if (!toDisplay) return; NSImage* img = toDisplay[0]; const int64_t frameOrdinal = [toDisplay[1] longLongValue]; if (frameOrdinal < _lastFrameDisplayed) return; [img drawInRect: self.bounds]; _lastFrameDisplayed = frameOrdinal; dispatch_async(dispatch_get_global_queue(0,^{ NSLog(@"Displayed a frame"); }); } @end
结论
在摘要中,只是将渲染与主线程分离,但不一定是并行化(即第一种情况)可能就足够了.为了更进一步,您可能想要研究并行化每帧渲染操作的方法.并行化多个帧的绘制带来了一些优势,但在像iOS这样的电池供电环境中,它可能会将您的应用/游戏变成电池耗尽.
对于模型更新而不是渲染的任何情况都是限制性试剂,正确的方法将在很大程度上依赖于具体的情况细节,并且与渲染相比,更难以概括.