如何安全地将渲染与更新模型解耦?

2024-01-13

与一些游戏开发人员交谈时,他们建议基于 OpenGL ES 的高性能游戏引擎不会处理主线程上的所有内容。这使得游戏引擎能够在具有多个 CPU 核心的设备上表现更好。

他们说我可以将更新与渲染分离。所以如果我理解正确的话,游戏引擎运行循环可以像这样工作:

  1. 设置一个 CADisplayLink 来调用render method.

  2. render方法在后台渲染当前的世界模型。

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

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

对我来说,这一切都感觉很奇怪。有人可以解释或链接到这种并发渲染+模型更新在现实中是如何完成的吗?令我难以置信的是,这如何不会导致问题,因为如果模型更新比渲染或其他方式花费的时间更长怎么办?谁在等待什么以及何时等待。

我试图理解的是如何从高层次的角度从理论上和细节上实现这一点。


在“现实”中,有很多不同的方法。没有“一种真正的方法”。什么适合您实际上取决于lot关于您在问题中没有讨论的因素,但无论如何我都会尝试。我也不确定如何CADisplayLink就是你想要的。我通常认为这对于需要帧同步(即音频和视频口型同步)的事情很有用,这听起来不像您所需要的,但让我们看看几种不同的方法可以做到这一点。我认为你问题的关键在于模型和视图之间是否需要第二个“层”。

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

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

  1. 用户事件进入主线程
  2. 事件处理程序触发对控制器方法的调用。
  3. 控制器方法更新模型状态。
  4. 模型状态的更改会使视图状态无效。 (IE。-setNeedsDisplay)
  5. 当下一帧到来时,窗口服务器将触发从当前模型状态重新渲染视图状态并显示结果

请注意,步骤 1-4 可以在步骤 5 发生之间发生多次,但是,由于这是一个单线程应用程序,当步骤 5 发生时,步骤 1-4 不会发生,并且用户事件正在排队等待步骤 5 完成。假设步骤 1-4“非常快”,这通常会以预期的方式丢帧。

将渲染与主线程解耦

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

  1. 用户事件进入主线程
  2. 事件处理程序触发对控制器方法的调用。
  3. 控制器方法更新模型状态。
  4. 模型状态的更改会将异步渲染任务排入队列以供后台执行。
  5. 如果异步渲染任务完成,它将生成的位图放置在视图已知的位置,并调用-setNeedsDisplay在视图上。
  6. 当下一帧到来时,窗口服务器将触发对-drawRect在视图上,现在的实现是从“已知共享位置”获取最近完成的位图并将其复制到视图中。

这里有一些细微差别。让我们首先考虑这样一种情况:您只是尝试将渲染与主线程分离(暂时忽略多核的利用——稍后会详细介绍):

几乎可以肯定,您永远不会希望同时运行多个渲染任务。一旦开始渲染一帧,您可能不想取消/停止渲染它。您可能希望将未来未启动的渲染操作排队到单个插槽队列中,该队列始终包含最后一个排队的未启动渲染操作。这应该为您提供合理的丢帧行为,这样您就不会出现“落后”的渲染帧,您应该直接丢掉帧。

如果存在一个完全渲染但尚未显示的框架,我想你always想要显示该框架。考虑到这一点,您不想打电话-setNeedsDisplay直到位图完成并位于已知位置。

您将需要跨线程同步访问。例如,当您将渲染操作排入队列时,最简单的方法是获取模型状态的只读快照,并将其传递给渲染操作,渲染操作只会从快照中读取。这使您不必与“实时”游戏模型同步(该模型可能会通过控制器方法在主线程上发生变化,以响应未来的用户事件。)另一个同步挑战是将已完成的位图传递到视图和召唤-setNeedsDisplay。最简单的方法可能是让图像成为视图上的一个属性,并分派该属性的设置(带有完整的图像)并调用-setNeedsDisplay转到主线程。

这里有一个小问题:如果用户事件以很高的速率传入,并且您能够在单个显示帧的持续时间内(1/60 秒)渲染多个帧,那么您could最终渲染的位图掉落在地板上。这种方法的优点是始终在显示时向视图提供最新的帧(减少感知延迟),但它的缺点是它会产生渲染从未获得的帧的所有计算成本。显示(即功率)。这里的正确权衡因每种情况而异,并且可能包括更细粒度的调整。

利用多核——本质上的并行渲染

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

利用多核——固有的串行渲染

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

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

当模型速度缓慢时...

我还没有解决实际上是基于用户事件的模型更新太慢的情况,因为从某种意义上来说,如果是这样的话,在很多方面,你就不再关心渲染了。如果渲染如何跟上model甚至跟不上?此外,假设您找到了一种将渲染和模型计算互锁的方法,渲染总是会抢夺模型计算的周期,根据定义,模型计算总是落后的。换句话说,当某个东西本身无法每秒更新 N 次时,你就不能希望每秒渲染该东西 N 次。

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

在常见情况下,您really希望事件处理和模型突变是way比实时更快,并且渲染是“困难的部分”。我很难想象一个有意义的情况,其中模型更新是限制因素,但您仍然关心解耦渲染以提高性能。

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

一些代码

下面是一个基于 Xcode 中的 Cocoa 应用程序模板的简单示例,展示了解耦背景渲染的外观。 (我意识到after编写这个基于 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, 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 这样的电池供电环境中,它可能会将您的应用程序/游戏变成耗电大户。

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

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

如何安全地将渲染与更新模型解耦? 的相关文章

  • iOS 8 支持动态链接吗?

    直到 iOS7 之前 出于安全考虑 Apple 都不支持动态链接 开发人员之间的代码重用通常依赖于静态库 这些静态库是作为应用程序可执行文件的一部分构建的 在 iOS8 中引入扩展似乎稍微改变了这一点 因为扩展是单独的可执行文件 扩展及其包
  • iPhone iOS 保存从 UIImageJPEGRepresentation() 获得的数据第二次失败:ImageIO: CGImageRead_mapData 'open' failed

    我的 UIImage 操作遇到了一个奇怪的问题 我正在进行保管箱同步 并且必须将我的图像存储为本地文件 为此 我使用以下命令保存它们UIImagePNGRepresentation image or UIImageJPEGRepresent
  • iOS 中如何清除特定域的 cookie?

    我已经搜索了 StackOverflow 上的几乎所有问题来寻找我的问题的答案 我还没有找到任何有用的链接或教程来说明哪种方式最好清除特定域的 cookie 如果有人可以帮助我 请 我自己找到了解决方案 如果您想删除 UIWebView 中
  • 当我从我转向的视图控制器返回时,为什么我的 UITableView 的格式完全出错了?

    我有一个UITableView使用自定义单元格 其中有一些标签可以动态决定单元格的高度 当我点击一个单元格并转到一个新的视图控制器时 返回后所有单元格的格式完全混乱 我无法弄清楚是什么导致了它 这是细胞通常的样子 我对它们设置了一些非常基本
  • UITableview 中的水平和垂直滚动[关闭]

    Closed 这个问题需要多问focused help closed questions 目前不接受答案 I want to make a lineup for a festival You can see what I want to a
  • 将第 3 方库 ZXing 导入 Xcode

    我尝试了多种方法将第 3 方库 ZXing 导入我的 iOS 应用程序 但所有方法都很痛苦 或者根本不起作用 如果有人可以建议我做错了什么 或者提出导入 ZXing 等库的更好方法 我将非常感激 一定比这个容易 这就是我所做的 结果是 My
  • 并发集合和独特元素

    我有一个并发BlockingCollection具有重复的元素 如何修改它以添加或获取不同的元素 默认后备存储BlockingCollection is a ConcurrentQueue 正如其他人指出的那样 使用它来添加不同的项目相当困
  • 检查定位服务是否开启

    我一直在对 CoreLocation 进行一些研究 最近 我遇到了一个在其他地方 但在 Objective C 和 iOS 8 中 已经讨论过的问题 我觉得问这个问题有点傻 但是如何在 iOS 9 上使用 swift 检查是否启用了位置服务
  • 是否有针对不同屏幕尺寸的单独故事板?

    基本上我已经完成了一个应用程序 我唯一的问题是 ATM 机应用程序在设计时只考虑了 4 英寸显示屏 当在 3 5 英寸模拟器上运行时 应用程序会丢失 0 5 英寸 显然 那么我的问题是 如何在 Xcode 5 中为不同的屏幕尺寸设置不同的故
  • UICollectionView 自动滚动到 IndexPath 处的单元格

    在加载集合视图之前 用户设置集合视图数组中的图像数量 所有单元格都不适合屏幕 我有 30 个单元格 但屏幕上只有 6 个 问题 如何在加载 UICollectionView 时自动滚动到具有所需图像的单元格 新的 编辑过的答案 将其添加到v
  • ios 用户如何取消 Facebook 登录?

    当用户到达此屏幕时 无法取消 我能做些什么 为了首先获得这个视图 我正在运行 NSMutableDictionary params NSMutableDictionary dictionaryWithObjectsAndKeys vid l
  • Swift Generics 在使用继承时不会实例化泛型

    我有课Alpha and Berry class Alpha class Berry Alpha 我有一个使用继承及其泛型的函数 func myFunc
  • 如何在 Firebase 控制台中使用 Apple 新的 APN .p8 证书

    随着最近 Apple 开发者帐户的升级 我面临着一个困难 在尝试创建推送通知证书时 它为我提供了 p8 证书 而不是可以导出到 p12 的 APNs 证书 Firebase 控制台仅接受 p12 证书 那么我如何从这些新的 p8 证书中获取
  • 如何在 iOS 9 上可靠地检测是否连接了外部键盘?

    在 iOS 9 之前 确定是否连接外部键盘的最可靠方法是监听UIKeyboardWillShowNotification并使文本字段成为第一响应者 如中所述这个问题 https stackoverflow com questions 289
  • 如何使用 NSUserDefaults 在 Swift 中存储自定义类的数组?

    我有一个名为的自定义类Person当某人输入信息时 它会存储有关某人的各种属性 class Person Person dictionary variable var name String var age String var html
  • 推送动画,没有阴影和停电

    我有一个简单的iOS NavigationController基于应用程序 二UICollectionViews 相继 如果元素打开 第一个合集 被点击时 第二集 将被打开 非常简单 重要的提示 Both UICollectionViews
  • 如何让按钮闪烁?

    我试图在扫描正确时将按钮的颜色 只是闪烁 闪烁 更改为绿色 在出现问题时将按钮的颜色更改为红色 我可以用这样的视图来做到这一点 func flashBG UIView animateWithDuration 0 7 animations s
  • 水平 UICollectionView 单行布局

    我正在尝试使用以下命令设置简单的水平布局UICollectionView 兜圈子却没有达到预期的结果 所以任何指针或例子将不胜感激 我粘贴经常更改的代码但没有成功可能没什么意义 该图像显示两行 第一行是单个项目 尺寸正确并且在中心正确对齐
  • UIImageJPEGRepresentation 在视网膜显示屏上提供 2x 图像

    我有这段代码 它创建一个图像 然后向其添加一些效果并缩小其大小以使其largeThumbnail UIImage originalImage UIImage imageWithData self originalImage thumbnai
  • UIView晃动动画

    我试图在按下按钮时使 UIView 摇动 我正在调整我找到的代码http www cimgf com 2008 02 27 core animation tutorial window shake effect http www cimgf

随机推荐