如何安全地将渲染与更新模型分离?

时间:2013-12-02 11:06:47

标签: ios concurrency grand-central-dispatch game-engine game-loop

与一些游戏开发者交谈时,他们认为基于OpenGL ES的高性能游戏引擎无法处理主线程上的所有内容。这允许游戏引擎在具有多个CPU核心的设备上执行得更好。

他们说我可以从渲染中解除更新。因此,如果我理解这一点,游戏引擎运行循环可以像这样工作:

  1. 设置调用render方法的CADisplayLink。

  2. render方法在后台呈现当前世界模型。

  3. render方法然后在主线程上调用update方法。

  4. 因此,当它在后台渲染时,它可以同时为下一次迭代更新世界模型。

    对我而言,这一切都让人感到非常不安。有人可以解释或链接到这种并发渲染+模型更新是如何在现实中完成的吗?令我难以理解的是,这不会导致问题,因为如果模型更新需要比渲染或其他方式更长的时间。谁等待什么,何时等待。

    我试图理解的是,从理论上讲,从高层次的角度来看,这也是如何实现的。

2 个答案:

答案 0 :(得分:9)

现实"有很多不同的方法。没有"一种真正的方式。"什么适合你真的取决于很多你没有在你的问题中讨论的因素,但我还是会采取行动。我也不确定CADisplayLink你想要的是什么。我通常认为这对于需要帧同步(即同步音频和视频)的东西很有用,它听起来并不像你需要的那样,但让我们看看你可能采用的几种不同的方式做这个。我认为你的问题的关键在于是否需要第二层""在模型和视图之间。

背景:单线程(即仅主线程)示例

让我们首先考虑一个普通的单线程应用程序如何工作:

  1. 用户事件在主线程中进入
  2. 事件处理程序触发对控制器方法的调用。
  3. 控制器方法更新模型状态。
  4. 对模型状态的更改使视图状态无效。 (即-setNeedsDisplay
  5. 当下一帧出现时,窗口服务器将触发从当前模型状态重新呈现视图状态并显示结果
  6. 请注意,在步骤5的出现之间可能会发生多次步骤1-4,但是,由于这是单线程应用程序,而第5步发生,因此步骤1-4没有发生,用户事件正在排队等待第5步完成。这通常会以预期的方式丢帧,假设步骤1-4非常快"。

    从主线程中解耦渲染

    现在,让我们考虑您要将渲染卸载到后台线程的情况。在这种情况下,序列应如下所示:

    1. 用户事件在主线程中进入
    2. 事件处理程序触发对控制器方法的调用。
    3. 控制器方法更新模型状态。
    4. 对模型状态的更改将后台执行的异步呈现任务排入队列。
    5. 如果异步呈现任务完成,它会将结果位图放在视图已知的某个位置,并在视图上调用-setNeedsDisplay
    6. 当下一帧出现时,窗口服务器将触发对该视图的-drawRect的调用,该调用现在实现为从"已知的共享位置"中获取最近完成的位图。并将其复制到视图中。
    7. 这里有一些细微差别。让我们首先考虑你只是试图将渲染与主线程分离的情况(暂时忽略多核的利用 - 稍后):

      您几乎肯定不会想要同时运行多个渲染任务。一旦开始渲染帧,您可能不想取消/停止渲染它。您可能希望将未来的未启动渲染操作排队到单个插槽队列中,该队列始终包含最后排队的未启动渲染操作。这应该给你合理的帧丢弃行为,这样你就不会得到"落后"渲染你应该放弃的帧。

      如果存在完全渲染但尚未显示的帧,我认为总是想要显示该帧。考虑到这一点,您不希望在视图上调用-setNeedsDisplay,直到位图完成并位于已知位置。

      您需要跨线程同步访问权限。例如,当您将渲染操作排入队列时,最简单的方法是获取模型状态的只读快照,并将其传递给渲染操作,渲染操作只能从快照中读取。这使您无需与" live"同步。游戏模型(可能是由您的控制器方法在主线程上发生变化以响应未来的用户事件。)另一个同步挑战是将完成的位图传递给视图并调用-setNeedsDisplay。最简单的方法可能是将图像作为视图中的属性,并将该属性的设置(使用完成的图像)​​和调用-setNeedsDisplay分配给主线程。

      这里有一点问题:如果用户事件以高速率进入,并且您能够在单个显示帧(1/60秒)的持续时间内渲染多个帧,那么可能最终渲染掉落在地板上的位图。这种方法的优点是始终在显示时为视图提供最新的帧(减少感知延迟),但它具有* dis *优势,它会导致渲染永远不会得到的帧的所有计算成本显示(即电源)。这里的权利交易对于每种情况都会有所不同,并且可能包括更细粒度的调整。

      利用多个核心 - 固有的并行渲染

      假设您已经如上所述从主线程中解耦渲染,并且您的渲染操作本身具有可并行化的特性,那么只需并行化您的一个渲染操作,同时继续以相同的方式与视图交互,您应该免费获得多核并行性。也许您可以将每个帧划分为N个区块,其中N是核心数,然后一旦所有N个区块完成渲染,您可以将它们拼凑在一起并将它们传递给视图,就好像渲染操作是单片的一样。如果您正在使用模型的只读快照,则N个tile任务的设置成本应该是最小的(因为它们都可以使用相同的源模型。)

      利用多个核心 - 固有的串行渲染

      如果您的渲染操作本质上是串行的(在我的经验中大多数情况下),您选择使用多个核心就是拥有与核心一样多的渲染操作。当一个帧完成时,它会发出任何已排队或仍处于飞行状态的信号,但之前,它们可能放弃并取消的渲染操作,然后它将自己设置为由视图显示,就像在仅去耦示例中一样。 / p>

      正如仅在解耦情况中所提到的,这总是在显示时为视图提供最新的帧,但是它会导致渲染从未显示的帧的所有计算(即功率)成本。

      当模型慢时......

      我还没有解决过根据用户事件太慢而实际更新模型的情况,因为从某种意义上说,如果情况如此,那么在很多方面,你不再关心渲染了。如果模型甚至无法跟上,那么渲染如何才能跟上?此外,假设您找到了一种互锁渲染和模型计算的方法,渲染总是从模型计算中抢夺循环,根据定义,模型计算总是落后。换句话说,当事物本身无法每秒更新N次时,您无法每秒渲染N次。

      我可以设想一些情况,你可以将像连续运行的物理模拟这样的东西卸载到后台线程。这样的系统必须自己管理其实时性能,并且假设它这样做,那么您将面临将来自该系统的结果与传入用户事件流同步的挑战。这真是一团糟。

      在常见的情况下,确实希望事件处理和模型变异比实时更快方式,并且渲染成为"难部分&#34。我很难想象一个有意义的案例,其中模型更新是限制因素,但你仍然关心解耦渲染的性能。

      换句话说:如果你的模型只能以10Hz更新,那么以10Hz以上的速度更新你的视图是没有意义的。当用户事件的速度远远超过10Hz时,就会出现这种情况的主要挑战。这个挑战将是有意义地丢弃,采样或合并传入的事件,以保持有意义并提供良好的用户体验。

      一些代码

      以下是基于Xcode中Cocoa应用程序模板的分离背景渲染效果的简单示例。 (我在编写完这个基于OS X的示例之后实现了,该问题被标记为ios,所以我猜这是"无论它值得什么&# 34)

      @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, readwrite, assign) CGPoint lastMouseLocation;
      @end
      
      @interface MyBackgroundRenderingView : NSView
      @property (nonatomic, readwrite, 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, readwrite, 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, readwrite, 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, 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, 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, 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, 0), ^{ NSLog(@"Displayed a frame"); });
      }
      
      @end
      

      结论

      在摘要中,只是将渲染与主线程分离,但不一定是并行化(即第一种情况)可能就足够了。为了更进一步,您可能想要研究并行化每帧渲染操作的方法。并行化多个框架的绘图带来了一些优势,但在像iOS这样的电池供电环境中,它可能会将您的应用/游戏变成电池耗尽。

      对于模型更新(而不是渲染)是限制性试剂的任何情况,正确的方法将在很大程度上取决于具体情况,并且与渲染相比,更难以概括。

答案 1 :(得分:1)

我的2美分值。

GL游戏在我有限的理解中总是更新然后渲染。

更新周期基本上将所有游戏内视觉变化部分(即:位置/颜色/等)更新为下一个时间值。这可以在工作线程中完成,可能在您的情况下,提前排队,排队到未来的t,t + 1,t + 2,t + n未来值。

渲染周期使用上面的计算值选择性地(t,t + 1,t + 2,t + n)执行主线程内的实际渲染。所有渲染必须在主线程内完成,否则您将开始看到特殊的伪像。在渲染周期中,根据重叠的时间值,您可以跳过帧/快进(即:渲染t + 1,t + 4值)或以慢动作播放(t + 0.1,t + 0.2)。

祝你学习顺利!