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

ios – 如何安全地将渲染与更新模型分离?,第1张

概述与一些游戏开发者交谈时,他们建议基于OpenGL ES的高性能游戏引擎不会处理主线程上的所有内容.这允许游戏引擎在具有多个CPU核心的设备上执行得更好. 他们说我可以从渲染分离更新.所以,如果我理解这一点,游戏引擎运行循环可以像这样工作: >设置调用render方法的CADisplayLink. > render方法在后台渲染当前世界模型. > render方法然后在主线程上调用update方法 与一些游戏开发者交谈时,他们建议基于OpenGL ES的高性能游戏引擎不会处理主线程上的所有内容.这允许游戏引擎在具有多个cpu核心的设备上执行得更好.

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

>设置调用render方法的CAdisplaylink.
> render方法在后台渲染当前世界模型.
> render方法然后在主线程上调用update方法.

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

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

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

解决方法 在“现实”中有许多不同的方法.没有“一种真实的方式”.什么是适合你的,实际上很大程度上取决于你在问题中没有讨论过的因素,但无论如何我都会考虑一下.我也不确定CAdisplaylink在这里是什么样的.我通常认为这对于需要帧同步(即同步音频和视频)的东西很有用,它听起来并不像你需要的那样,但让我们看看你可能采用的几种不同方式.我认为你的问题的关键在于模型和视图之间是否需要第二个“层”.

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

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

>用户事件进入主线程
>事件处理程序触发对控制器方法的调用.
>控制器方法更新模型状态.
>对模型状态的更改使视图状态无效. (即-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 MyVIEwControllerstatic 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这样的电池供电环境中,它可能会将您的应用/游戏变成电池耗尽.

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

总结

以上是内存溢出为你收集整理的ios – 如何安全地将渲染与更新模型分离?全部内容,希望文章能够帮你解决ios – 如何安全地将渲染与更新模型分离?所遇到的程序开发问题。

如果觉得内存溢出网站内容还不错,欢迎将内存溢出网站推荐给程序员好友。

欢迎分享,转载请注明来源:内存溢出

原文地址:https://54852.com/web/1105448.html

(0)
打赏 微信扫一扫微信扫一扫 支付宝扫一扫支付宝扫一扫
上一篇 2022-05-28
下一篇2022-05-28

发表评论

登录后才能评论

评论列表(0条)

    保存