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

前端之家收集整理的这篇文章主要介绍了ios – 如何安全地将渲染与更新模型分离?前端之家小编觉得挺不错的,现在分享给大家,也给大家做个参考。
与一些游戏开发者交谈时,他们建议基于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 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这样的电池供电环境中,它可能会将您的应用/游戏变成电池耗尽.

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

猜你在找的iOS相关文章