Quantcast
Channel: 英特尔开发人员专区文章
Viewing all 583 articles
Browse latest View live

Intel SGX远程认证服务API V2升级指南

$
0
0

概要

本文的目标读者是SGX的开发者,撰写此文旨在帮助他们升级他们的Intel® SGX远程认证服务API到V2版本(V1版本将于2018年1月份弃用),如果你想更多的了解什么是SGX,或者想更多的了解Intel® SGX远程认证服务的相关细节(工作原理,如何申请测试服务等),请参阅此文最后的参考文献。

什么是Intel® SGX远程认证服务

Intel® SGX除了为离线应用提供硬件级别的保护之外,还为需要联网的应用,比如DRM应用,银行交易应用等提供“客户端向服务器证明自己合法性”的能力,我们称这个过程为远程认证(Remote Attestation)。在这个过程中,客户端的软硬件平台信息,以及相关Enclave的指纹信息等将会首先发送到开发者的服务器(Service Provider),然后由开发者的服务器转发给SGX的远程认证服务器(Attestation Service),SGX远程认证服务器将会对收到的信息进行合法性验证,并将验证结果返回给开发者的服务器,此时开发者的服务器便可得知发起验证的客户端是否可信,并采取对应的下一步行动。

Intel® SGX 远程认证服务 API

在远程认证的过程中,开发者的服务器与SGX远程认证服务器之间采用标准的TLS网络通信协议(最低支持TLS1.2版本),SGX远程认证服务器以REST API的形式向开发者服务器提供服务,让开发者服务器可以实现诸如提交远程认证,获取远程认证结果,获取EPID吊销列表等相关功能。我们称这些REST API为远程认证服务API。目前为远程认证服务API已经发布V2版本,并弃用之前的V1版本,开发者需要尽快更新他们的服务器相关实现,对接到V2版本。

如何升级Intel® SGX 远程认证服务 API 到V2版本

首先,开发者需要将API请求发送到V2版本对应的URL地址。比如,在V1中,我们获取SigRL时,我们发送请求到/attestation/sgx/v1/sigrl/{gid},当升级到V2版本时,我们需要将对应的请求发送到/attestation/sgx/v2/sigrl/{gid}这个地址。这一点适用于其它所有API。

另外一个比较大的改动是远程认证报告签名验证机制的改变。在V1版本中,我们通过/attestation/sgx/v1/report来获取认证报告,报告的签名包含在X-IASReport-Signature这个response header中。提取了远程认证报告签名之后,我们读取预存在开发者服务器本机的SGX远程认证服务器的公钥,然后用公钥对签名进行验证。而在V2版本中,当我们获取远程认证报告(/attestation/sgx/v2/report)时,在返回的Response Header中会多附加一个X-IASReport-Signing-Certificate的项目,这个项目中包含了远程认证报告签名所用的证书链,我们首先需要用预存在开发者服务器本机的SGX远程认证根证书对该证书链的合法性进行验证,在确保该证书链合法的前提下,再用该证书链对远程认证报告的签名进行验证。具体的实现可以参考以下代码片段(注:以下代码基于openssl V1.0.2j开发,仅供参考,不能直接作为真实的实现):

1.加载预存在开发者服务器本机的SGX远程认证根证书

// 加载Intel远程认证服务的根证书ra.crt(此证书需要事先预置在开发者服务器本机)
BIO *bio_ra = BIO_new_file("ra.crt","r");
X509 *cert_ra = PEM_read_bio_X509(bio_ra,NULL,NULL,NULL);

2.用SGX远程认证根证书验证证书链的合法性

/* “X-IASReport-Signing-Certificate”这个http header中包含了证书链,此处略去了
	提取证书链到str_cert_chain的代码 */
// 用步骤1中加载的Intel根证书验证证书链str_cert_chain的合法性
bool res_cert_chain_verify = false;
X509_STORE *store;
X509_STORE_CTX *ctx;
store = X509_STORE_new();
ctx = X509_STORE_CTX_new();
X509_STORE_add_cert(store, cert_ra);
BIO *bio_cert_chain = BIO_new_mem_buf((void*)str_cert_chain, -1);
X509_INFO *itmp;
STACK_OF(X509_INFO) *inf = PEM_X509_INFO_read_bio(bio_cert_chain, NULL, NULL, NULL);
for (int i = 0; i < sk_X509_INFO_num(inf); i++) {
	itmp = sk_X509_INFO_value(inf, i);
	if (itmp->x509) {
	      X509_STORE_CTX_init(ctx, store, cert_ra, NULL);
	      if(! X509_verify_cert(ctx) ){
		  res_cert_chain_verify = false;
	      }else if(i == sk_X509_INFO_num(inf) -1){
		  res_cert_chain_verify = true;
	      }
	      X509_STORE_CTX_cleanup(ctx);
	}
}
sk_X509_INFO_pop_free(inf, X509_INFO_free);
BIO_free(bio_cert_chain);
X509_STORE_CTX_free(ctx);
X509_STORE_free(store);

3.用证书链验证远程认证报告签名的合法性

bool res_sig_verify = false;
if( res_cert_chain_verify ){
	// 用IAS公钥验证远程报告签名

	// 从证书链str_cert_chain中提取对报告签名的IAS公钥
	BIO *bio_sp = BIO_new(BIO_s_mem());
	BIO_write(bio_sp, str_cert_chain, strlen(str_cert_chain));
	X509 *cert_sp = PEM_read_bio_X509(bio_sp, NULL, NULL, NULL);
	EVP_PKEY *pubkey_sp = X509_get_pubkey(cert_sp);

	// 用IAS公钥验证远程报告的签名
	RSA *rsa = EVP_PKEY_get1_RSA(pubkey_sp);
	QByteArray digest = QCryptographicHash::hash(reportBody, QCryptographicHash::Sha256);
	res_sig_verify = RSA_verify(NID_sha256, (unsigned char*)digest.data(), SHA256_DIGEST_LENGTH, (unsigned char*)signature.data(), 256, rsa);

	BIO_free_all(bio_sp);
	X509_free(cert_sp);
	EVP_PKEY_free(pubkey_sp);
}

总结

SGX远程认证服务提供了必要的基础设施,让服务器可以判断正在请求服务的客户端是否处于安全可信的环境中 (硬件+软件) ,并且建立加密的信道,从而可以安全的将机密数据下发到可信的客户端,以及拒绝为不可信的客户端提供服务。作为远程认证服务的升级,API版本2在兼顾API兼容性的同时,完善和加强了远程认证报告的验证流程,是一次非常有意义的升级。

参考文献

Intel® Software Guard Extensions (Intel® SGX): https://software.intel.com/zh-cn/sgx/details

Attestation Service for Intel® Software Guard Extensions (Intel® SGX) API Documentation:https://software.intel.com/sites/default/files/managed/7e/3b/ias-api-spec.pdf

Intel® Software Guard Extensions Remote Attestation End-to-End Example: https://software.intel.com/zh-cn/articles/intel-software-guard-extensions-remote-attestation-end-to-end-example

关于作者

裴凡江是英特尔软件与服务事业部的一名应用软件工程师,专注于安全领域。主要负责与开发者一起合作,将英特尔平台的相关安全技术(例如SGX)应用到开发者的产品中,以实现安全相关的应用场景(比如指纹支付),或者提高应用本身的安全性。


以 Pirate Cove 为例:如何将 Steam*VR 集成至 Unity*

$
0
0

查看 PDF [1065 kb]

本文的目标受众是谁?

本文主要面向希望将 Steam*VR 集成至场景之中的 Unity* 开发人员。我将假设读者已安装 HTC Vive* 且能够正常运行。如果没有,请访问 SteamVR 网站查看具体说明

我为何创建 Pirate Cove VR 场景?

我的工作重点随时变化,需要逐渐倾斜至虚拟现实 (VR),使其融于 Unity 环境之中。我想将 SteamVR 集成至 Unity 之中、设计场景,并支持远距传动以便在场景之中灵活移动。

本文主要介绍我在 VR 集成过程中所积累的经验,并展示如何实现 VR 和场景的有机结合。本文的重点不是如何设计场景以及如何使用 Unity 实现一切正常运行,而是介绍如何将 VR 集成至场景之中。

我的创建目标是什么?

我尝试创建虚拟现实视觉体验。尽管使用 Unity 来创建体验,但它不是游戏本身。我创建的视觉体验模拟以海盗为主题的小型热带岛屿。我选择对我有利的事物;毕竟我生活在雨水充沛的太平洋西北地区。所以我希望我的体验带有一些热带风情。

我使用的工具有哪些?

我使用 Unity* 5.6。我从 Unity 资产商店购买了一些资产。资产的选择均围绕如何设置生活着大量海盗的热带岛屿:

  • Pirates Island
  • SteamVR
  • Hessburg Navel Cutter
  • Aquas

还有一些其他的免费零星物品。

我为创建该项目积累了哪些知识?

我曾使用过 Unity,以及英特尔® 实感™ 技术。我们的 SDK 包含英特尔实感 Unity 插件,而且我编写过插件,并创建了有关该插件的培训示例。

到目前为止我没有真正尝试过在 Unity 中设计第一人称类型的游戏,也从不担心性能、每秒帧数 (FPS) 等相关问题。使用英特尔实感技术和其他简单支持工具时编写过脚本。但没有设计过大型项目或场景,也没有处理过与此相关的问题。

该项目的终极目标是什么?

我希望通过项目创建更好地了解 Unity 以及如何打造 VR 场景。我希望了解正常运行 VR 的难度。如果成功将会有哪些收获?性能是否可接受?如果存在帧速率低的问题,我是否能够保持稳定,继续创建该项目?

以及从中获得乐趣。我希望从该项目创建过程中获得乐趣,并积累经验,这也是我为何选择创建以海盗为主题的热带岛屿的原因。我个人对从前的加勒比海盗时代非常着迷。

我对此存在哪些误解?

如前所述,在使用 Unity 方面,经验并不丰富。

第一个误解是关于渲染对象和渲染时机。这是什么意思?由于某种原因,我假设地形中包含譬如 3D 模型(比如巨大的悬崖),如果仅将悬崖的一半放在地形之上,Unity 不会尝试渲染不可见部分。我认为是某种类型的渲染算法阻止了 Unity 渲染处于地形之下的对象。显然不是这样。Unity 仍然渲染整个 3D 悬崖模型。

这种相同的概念适用于两个不同的 3D 悬崖模型。例如,如果有两个悬崖游戏对象,我假设将其中一座悬崖推入另一座悬崖之中,以创造出一座巨大悬崖的幻象,那么任何不可见的几何体或纹理将不会被渲染。同样不是这么回事。

显然,如果有几何体和纹理,无论是否隐藏在其他对象之中,Unity 都会将其渲染。我们需要考虑到这一点。这对我的工作没有多大影响,也没有找到解决办法,我只是在正常使用 Unity 的过程中发现了这一问题。

性能

性能是我积累最多经验的一个方面。使用 Unity 的地形工具来设计场景非常简单。添加资产也非常简单。并不是说能够非常简单地设计出色的场景,只是说你可以轻松地明白如何设计场景中的对象。尽管我自己认为 Pirates Cove场景十分漂亮,但其他人可能会嗤之以鼻。但这是我第一次设计场景,而且我并不是尝试创建第一人称射击游戏。它是一种虚拟的视觉体验。

FPS:与他人探讨 VR 时我了解到适用于 VR 的 FPS 是 90。我一开始尝试使用 Unity Stats 窗口。在 Unity 论坛上与他人交谈时我发现该窗口并不是能够达到真正 FPS 性能的最佳工具。别人建议我使用 FPS Display 脚本。显然这种方法比较准确。

遮挡剔除:这种情况十分有趣。我正尝试解决一个完全不相关的问题时,一个同事过来帮忙。我们谈到 FPS 和可以帮助解决渲染问题的技巧,他介绍我使用遮挡剔除。他向我展示了一种人工操作方法:如何确定盒子的大小和形状。我必须承认,我只是简单地打开 Unity 的遮挡剔除窗口,让它自己算出遮蔽范围。这似乎帮助我解决了性能问题。

植被:我没有意识到为地形添加草地会对场景产生巨大的影响。我看过在其他场景中大片草丛在风中摇曳。因此我非常兴奋;将 FPS 将至 0,Unity 因此超载。我没有解决这一问题,而是简单地移除草丛,并使用三叶草纹理让场景看起来比较美观,但没有任何绘制调用。

如何让 Vive* 在场景中运行

我在文章开头提过,假设读者的工作站设置并运行 Vive。这是我找到的现有文章的浓缩版 HTC Vive Tutorial for Unity。我不打算深入介绍控制器条目,因为本文将介绍重点远距传动。我修改了远距传动版本,改动较小,因为我认为适度更好,更确切地说是在操作的过程中有所发现。

在进行 HTC Vive* 开发之前,必须下载和导入 SteamVR 插件。

screenshot of Steam*VR plugin logo

导入插件后,项目中将出现以下文件结构:

screenshot of file structure in unity

在项目中,我在名为 VR 的层级结构中创建了根文件夹。在这里我拷贝了 SteamVR 和 CameraRig 预制件。你无需创建 VR 文件夹;我这里只是为了便于项目整理。

screenshot of folder organization

只需将 SteamVR 插件添加至项目层级结构,无需进行额外操作;看 CameraRig,

我将 CameraRig 放在场景中我希望一开始启动的地方。

screenshot of game environment

放置 SteamVR CamerRig 预制件后,我需要删除主摄像头,以避免冲突。这时我可以启动场景,四处观察。我不能移动,但可以从静止的点四处观察,可以看到手中的控制器。不能随意走动,但至少可以看到任何地方。

screenshot of game environment

获得追踪对象

追踪对象包含手部控制器和耳机。有了这个代码示例,我完全不担心耳机;但需要从手部控制器获得输入。这对追踪按钮点击等事物来说非常有必要。

首先我们必须获得带有脚本的追踪对象实例。这里追踪对象是控制器;可通过 Awake 函数获得实例。

void Awake( )

{

    _trackedController = GetComponent( );

}

然后,如果想测试是否能从手部控制器获得输入,可以通过以下 Get 函数选择特定控制器。它使用该脚本连接的追踪对象(手部控制器):

private SteamVR_Controller.Device HandController
{
    get
    {
        return SteamVR_Controller.Input( ( int )_trackedObj.index );
    }
}

创建远距传动系统

现在我们想添加在场景中四处走动的功能。为此,我们必须创建脚本,知道如何读取手部控制器的输入。我创建了一个新的 Unity C# 脚本,名为 TeleportSystem.cs。

我们不仅需要脚本,还需要激光指示器,在本特定项目中为十字线。十字线不是强制性的,只是为了增添场景中的辨别力,因为用户可将该十字线用作视觉反馈工具。我只创建了一个带骷髅头的简单圆圈。

创建激光

将立方体扔在场景中即可创建激光;激光在场景中足够高,不会干扰场景中的其他资产。从这里将其坐标设为:x = 0.005,y = 0.005,z = 1。这样形成一个细长立方体形状的激光。

screenshot of model in the unity envrionment

激光创建完成后,以预制件的形式保存并删除场景中的原始立方体,因为已不再需要。

创建十字线

我希望激光的末端带有一个定制的十字线,这点不是必需的,只是比较酷而已。我创建了一个预制件,即上面带贴花的圆形网格。

screenshot of pirate flag decal

screenshot of the unity inspector panel

设置层级

这一步骤非常重要。你需要告诉激光/十字线哪些可以远距传动,哪些不可以。例如,可能不支持用户在水上远距传动,或者不希望支持他们在悬崖边上远距传动。你可以使用层级,将他们限制在场景中特定的区域。我创建了两个层级 — Teleportable 和 NotTeleportable。

screenshot of game layers in unity

我将可以远距传动的事物,比如地形本身、草棚和楼梯,放在 Teleportable 层。用户无法远距传动的事物,比如场景中的悬崖或其他事物,放在 NotTeleportable 层。

定义变量时,我确定了双层掩码。一个掩码包含所有层级。另一个是非远距传动掩码,表示这些层级无法远距传动。

// Layer mask to filter the areas on which teleports are allowed
public LayerMask _teleportMask;

// Layer mask specifies which layers we can NOT teleport to.
public LayerMask _nonTeleportMask;

确定公共层级掩码时,它们均包含在脚本中。包含多个下拉列表,你可以从中选择不允许远距传动的层级。

screenshot of unity CSharp script

设置层级与 LayerMatchTest 函数搭配使用。

/// <summary>
/// Checks to see if a GameObject is on a layer in a LayerMask.
/// </summary>
/// <param name="layers">Layers we don't want to teleport to</param>
/// <param name="objInQuestion">Object that the raytrace hit</param>
/// <returns> true if the provided GameObject's Layer matches one of the Layers in the provided LayerMask.</returns>
private static bool LayerMatchTest( LayerMask layers, GameObject objInQuestion )
{
    return( ( 1 << objInQuestion.layer ) & layers ) != 0;
}

调用 LayerMatchTest()时,将发送包含无法远距传动层级列表的层级掩码,以及 HitTest 检测到的游戏对象。测试将表明该对象是否在非远距传动层级列表中。

更新帧

void Update( )
{
    // If the touchpad is held down
    if ( HandController.GetPress( SteamVR_Controller.ButtonMask.Touchpad ) )
    {
        _doTeleport = false;

        // Shoot a ray from controller.If it hits something make it store the point where it hit and show
        // the laser.Takes into account the layers which can be teleported onto
        if ( Physics.Raycast( _trackedController.transform.position, transform.forward, out _hitPoint, 100, _teleportMask ) )
        {
            // Determine if we are pointing at something which is on an approved teleport layer.
            // Notice that we are sending in layers we DON'T want to teleport to.
            _doTeleport = !LayerMatchTest( _nonTeleportMask, _hitPoint.collider.gameObject );

            if( _doTeleport )
            {
                PointLaser( );
            }
            else
            {
                DisplayLaser( false );
            }
        }
    }
    else
    {
        // Hide _laser when player releases touchpad
        DisplayLaser( false );
    }
    if( HandController.GetPressUp( SteamVR_Controller.ButtonMask.Touchpad ) && _doTeleport )
    {
        TeleportToNewPosition();
        ResetTeleporting( );
    }
}

更新时,代码将测试是否按下了控制器触控板按钮。如果按下了,将获得 Raycast 命中。请注意,我将发送包含一切事物的远距传动掩码。然后对命中点进行层级匹配测试。通过调用 LayerMatchTest 函数,确定是否命中了可远处传动或不可远距传动的事物。请注意,我将发送我不希望远距传动的层级列表。它将返回一个布尔值,之后该值用于确定我们能否远距传动。

如果可以远距传动,将使用 PointLaser 函数显示激光。在该函数中,我将告诉激光预制件看向 HitTest 的位置。接下来将激光预制件从控制器拉伸至 HitTest 位置。同时重新定位激光末端的十字线。

private void PointLaser( )
{
    DisplayLaser( true );

    // Position laser between controller and point where raycast hits.Use Lerp because you can
    // give it two positions and the % it should travel.If you pass it .5f, which is 50%
    // you get the precise middle point.
    _laser.transform.position = Vector3.Lerp( _trackedController.transform.position, _hitPoint.point, .5f );

    // Point the laser at position where raycast hits.
    _laser.transform.LookAt( _hitPoint.point );

    // Scale the laser so it fits perfectly between the two positions
    _laser.transform.localScale = new Vector3( _laser.transform.localScale.x,
                                            _laser.transform.localScale.y,
                                            _hitPoint.distance );

    _reticle.transform.position = _hitPoint.point + _VRReticleOffset;
}

如果 HitTest 指向非远距传动层级,我将确保通过 DisplayLaser 函数关闭激光指示器。

在该函数的末端,如果触控板已按下,且 shouldTeleport 变量为真值,我调用 Teleport 函数将用户远距传动至新的位置。

private void TeleportToNewPosition( )
{
    // Calculate the difference between the positions of the camera's rig's center and players head.
    Vector3 difference = _VRCameraTransform.position - _VRHeadTransform.position;

    // Reset the y-position for the above difference to 0, because the calculation doesn’t consider the
    // vertical position of the player’s head
    difference.y = 0;

    _VRCameraTransform.position =  _hitPoint.point + difference;
}

总结

这就是我准备和运行场景的全过程。涉及从互联网获取资源、阅读他人发布的文章,以及测试和纠错。希望大家能够通过本文有所收获,随时欢迎大家与我联系。

出于完整性考虑,下面我提供完整的脚本:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;


/// <summary>
/// Used to teleport the players location in the scene.Attach to SteamVR's ControllerRig/Controller left and right
/// </summary>
public class TeleportSystem :MonoBehaviour
{
    // The controller itself
    private SteamVR_TrackedObject _trackedController;

    // SteamVR CameraRig transform
    public Transform _VRCameraTransform;

    // Reference to the laser prefab set in Inspecter
    public GameObject _VRLaserPrefab;

    // Ref to teleport reticle prefab set in Inspecter
    public GameObject _VRReticlePrefab;

    // Stores a reference to an instance of the laser
    private GameObject _laser;

    // Ref to instance of reticle
    private GameObject _reticle;

    // Ref to players head (the camera)
    public Transform _VRHeadTransform;

    // Reticle offset from the ground
    public Vector3 _VRReticleOffset;

    // Layer mask to filter the areas on which teleports are allowed
    public LayerMask _teleportMask;

    // Layer mask specifies which layers we can NOT teleport to.
    public LayerMask _nonTeleportMask;

    // True when a valid teleport location is found
    private bool _doTeleport;

    // Location where the user is pointing the hand held controller and releases the button
    private RaycastHit _hitPoint;




    /// <summary>
    /// Gets the tracked object.Can be either a controller or the head mount.
    /// But because this script will be on a hand controller, don't have to worry about
    /// knowing if it's a head or hand controller, this will only get the hand controller.
    /// </summary>
    void Awake( )
    {
        _trackedController = GetComponent<SteamVR_TrackedObject>( );
    }


    /// <summary>
    /// Initialize the two prefabs
    /// </summary>
    void Start( )
    {
        // Spawn prefabs, init the classes _hitPoint

        _laser      = Instantiate( _VRLaserPrefab );
        _reticle    = Instantiate( _VRReticlePrefab );

        _hitPoint   = new RaycastHit( );

    }


    /// <summary>
    /// Checks to see if player holding down touchpad button, if so, are the trying to teleport to a legit location
    /// </summary>
    void Update( )
    {
        // If the touchpad is held down
        if ( HandController.GetPress( SteamVR_Controller.ButtonMask.Touchpad ) )
        {
            _doTeleport = false;

            // Shoot a ray from controller.If it hits something make it store the point where it hit and show
            // the laser.Takes into account the layers which can be teleported onto
            if ( Physics.Raycast( _trackedController.transform.position, transform.forward, out _hitPoint, 100, _teleportMask ) )
            {
                // Determine if we are pointing at something which is on an approved teleport layer.
                // Notice that we are sending in layers we DON'T want to teleport to.
                _doTeleport = !LayerMatchTest( _nonTeleportMask, _hitPoint.collider.gameObject );

                if( _doTeleport )
                    PointLaser( );
                else
                    DisplayLaser( false );
            }
        }
        else
        {
            // Hide _laser when player releases touchpad
            DisplayLaser( false );
        }
        if( HandController.GetPressUp( SteamVR_Controller.ButtonMask.Touchpad ) && _doTeleport )
        {
            TeleportToNewPosition( );
            DisplayLaser( false );
        }
    }


    /// <summary>
    /// Gets the specific hand contoller this script is attached to, left or right controller
    /// </summary>
    private SteamVR_Controller.Device HandController
    {
        get
        {
            return SteamVR_Controller.Input( ( int )_trackedController.index );
        }
    }


    /// <summary>
    /// Checks to see if a GameObject is on a layer in a LayerMask.
    /// </summary>
    /// <param name="layers">Layers we don't want to teleport to</param>
    /// <param name="objInQuestion">Object that the raytrace hit</param>
    /// <returns>true if the provided GameObject's Layer matches one of the Layers in the provided LayerMask.</returns>
    private static bool LayerMatchTest( LayerMask layers, GameObject objInQuestion )
    {
        return( ( 1 << objInQuestion.layer ) & layers ) != 0;
    }


    /// <summary>
    /// Displays the lazer and reticle
    /// </summary>
    /// <param name="showIt">Flag </param>
    private void DisplayLaser( bool showIt )
    {
        // Show _laser and reticle
        _laser.SetActive( showIt );
        _reticle.SetActive( showIt );
    }



    /// <summary>
    /// Displays the laser prefab, streteches it out as needed
    /// </summary>
    /// <param name="hit">Where the Raycast hit</param>
    private void PointLaser( )
    {
        DisplayLaser( true );

        // Position laser between controller and point where raycast hits.Use Lerp because you can
        // give it two positions and the % it should travel.If you pass it .5f, which is 50%
        // you get the precise middle point.
        _laser.transform.position = Vector3.Lerp( _trackedController.transform.position, _hitPoint.point, .5f );

        // Point the laser at position where raycast hits.
        _laser.transform.LookAt( _hitPoint.point );

        // Scale the laser so it fits perfectly between the two positions
        _laser.transform.localScale = new Vector3( _laser.transform.localScale.x,
                                                    _laser.transform.localScale.y,
                                                    _hitPoint.distance );

        _reticle.transform.position = _hitPoint.point + _VRReticleOffset;
    }



    /// <summary>
    /// Calculates the difference between the cameraRig and head position.This ensures that
    /// the head ends up at the teleport spot, not just the cameraRig.
    /// </summary>
    /// <returns></returns>
    private void TeleportToNewPosition( )
    {
        Vector3 difference = _VRCameraTransform.position - _VRHeadTransform.position;
        difference.y = 0;
        _VRCameraTransform.position =  _hitPoint.point + difference;
    }
}

关于作者

Rick Blacker 任职于英特尔® 软件和服务事业部,主要负责以 Primer VR 应用开发为重点的虚拟现实。

使用英特尔® SPMD 程序编译器实现游戏 CPU 的矢量化

$
0
0

下载 GitHub* 代码示例

简介

基于 LLVM*英特尔® SPMD 程序编译器 (在之前的文档中通常被称作 ISPC)并不是 Gnu* 编译器套装 (GCC) 或 Microsoft* C++ 编译器的替代品;它更类似于面向 CPU 的着色器编译器,可生成适用多种指令集的矢量指令,如英特尔® SIMD 流指令扩展 2(英特尔® SSE2)、英特尔® SIMD 流指令扩展 4(英特尔® SSE4)、英特尔® 高级矢量扩展指令集(英特尔® AVX)、英特尔® AVX2 等。输入基于 C 的着色器或内核,输出预编译对象文件,您的应用中将包含一个头文件。通过使用少量关键字,编译器便可以得到关于在 CPU 矢量单元上分配工作的明确指示。

如果开发人员选择将内联函数直接编写到代码库,显式矢量化便可提供更高性能,但是这样做极为复杂,维护成本较高。英特尔 SPMD 程序编译器 内核使用高级语言编写而成,因此开发成本较低。相比英特尔 SSE4 等最常见的指令集,它还可以轻松支持多个指令集,为运行代码的 CPU 提供最佳性能。

本文的目的并不是教会读者如何编写英特尔 SPMD 程序编译器内核;本文简单介绍了如何将英特尔 SPMD 程序编译器插入 Microsoft Visual Studio* 解决方案,提供了关于如何将简单的高级着色语言* (HLSL*) 计算着色器导入英特尔 SPMD 程序编译器内核的指导。如欲获取关于英特尔 SPMD 程序编译器更详细的概述,请参阅在线文档

本文提供的示例代码基于 Microsoft DirectX* 12 n 体示例的修改版本,为了支持英特尔 SPMD 程序编译器矢量化计算内核,已导入该版本。本文的目的并不是显示 GPU 的性能增量,而是显示从标准标量 CPU 代码迁移至矢量化 CPU 代码实现的性能提升。

由于本应用原始示例中的 CPU 负载非常小,显然不能代表一个游戏,但是本应用显示了在多个 CPU 内核上使用矢量单元可能会提升性能。


图 1.经过修改的 n 体重力示例截屏

原始 DirectX* 12 n 体重力示例

开始移植英特尔 SPMD 程序编译器之前,了解原始示例及其目的非常有帮助。编写 DirectX 12 n 体重力示例是为了介绍如何使用 DirectX 12 中的独立计算引擎执行异步计算,也就是在 GPU 上并行执行粒子更新与粒子渲染。示例生成 10,000 个粒子,逐帧更新与渲染粒子。更新包括每个粒子与其他粒子的交互,每次模拟 tick 生成 100,000,000 个交互。

HLSL 计算着色器将计算线程映射至每个粒子,以执行更新。双缓冲粒子数据,因此,对于每一帧,GPU 从缓冲 1 开始渲染,异步更新缓冲 2,然后翻转缓冲,为下一帧做准备。

就是这么简单。不仅简单,它还是英特尔 SPMD 程序编译器移植的绝佳选择,因为异步计算任务适用于出色地运行于 CPU;代码和引擎可在并发执行路径中执行计算。通过将某些负载迁移至多数情况下未被充分利用的 CPU,GPU 可以更快地完成帧,或者完成更多工作,同时充分利用 CPU。

移植到英特尔® SPMD 程序编译器

建议首先从 HLSL 移植到标量 C/C++。这样确保了算法的正确性,生成了正确的结果,正确地与其它应用交互,以及恰当处理多个线程(如果适用)。听起来轻而易举,但是需要注意以下几点:

  1. 如何在 GPU 和 CPU 之间共享内存。
  2. 如何同步 GPU 和 CPU。
  3. 如何面向单指令/多数据 (SIMD) 和多线程划分任务。
  4. 将 HLSL 代码移植到标量 C。
  5. 将标量 C 移植到英特尔 SPMD 程序编译器内核。

某些操作相对简单。

共享内存

我们知道需要在 CPU 和 GPU 之间共享内存,但是如何共享?幸运的是,DirectX 12 提供一些选项,如将 GPU 缓冲映射到 CPU 内存等。为了简化示例,最大程度减少代码变更,我们重新使用了用于初始化 GPU 粒子缓冲的粒子上传临时缓冲,并创建了面向 GPU 访问的双缓冲 CPU 副本。使用模式成为:

  • 在 CPU 中更新 CPU 可访问的粒子缓冲。
  • 使用原始上传临时缓冲调用 DirectX 12 助手 UpdateSubresources,GPU 粒子缓冲作为目的地。
  • 绑定 GPU 粒子缓冲并渲染。

同步

如果原始异步计算内核已经有一个用于配置计算和渲染之间交互的 DirectX 12 栅栏对象,同步将自然发生,重新利用它通知渲染引擎副本已完成。

划分工作

为了划分工作,我们应首先考虑 GPU 如何划分工作,因为这可能同样适用于 CPU。计算着色器通过两种方式控制工作的划分。首先是调度大小,指的是录制命令流时传输至 API 调用的大小。描述了运行的工作组的数量和维度。第二个是本地工作组的数量和维度,该工作组被硬编码为着色器。本地工作组的每个项目可视作一个工作线程,如果使用了共享内存,每个线程可与工作组中的其他线程共享信息。

通过观察 nBodyGravityCS.hlsl计算着色器发现,本地工作组的规模为 128 x 1 x 1,并使用共享内存优化某些粒子负载,但是在 CPU 上没有必要这样做。除此以外,线程之间没有交互,每个线程与外层循环执行不同的粒子,与内层循环上的所有其它粒子交互。

这应该非常适合 CPU 矢量宽度,因此我们使用面向英特尔 AVX2 的 8 x 1 x 1 或面向英特尔 SSE4 的 4 x 1 x 1 更换 128 x 1 x 1。我们也可以将调度大小用作多线程代码的提示,根据 SIMD 宽度,将 10,000 个粒子除以 8 或 4。但是,由于我们已经发现每个线程之间没有关联,可以简单地将粒子数量除以线程池中的可用线程数量,或除以设备上的可用逻辑内核数量,启用了英特尔® 超线程技术的典型 4 核 CPU 上的内核数量为 8。移植其他计算着色器时,可能需要考虑更多因素。

这为我们提供了以下伪代码:

For each thread
		Process N particles where N is 10000/threadCount
		      For each M particles from N, where M is the SIMD width
		           Test interaction with all 10000 particles

移植 HLSL* 至标量 C

编写英特尔 SPMD 程序编译器内核时,除非您有丰富的经验,否则建议您首先编写一个标量 C 版本。这将确保所有应用粘接、多线程和内存操作在开始矢量化之前已经运行。

为此,nBodyGravityCS.hlsl中的多数 HLSL 代码可在 C 中工作,除了为粒子添加外层循环和将着色器数学矢量类型改为使用基于 C 的类型以外,只需极少的修改。在本示例中,float4/float3 类型与 DirectX XMFLOAT4/XMFLOAT3 类型交换,某些矢量数学操作被划分为标量操作。

CPU 粒子缓冲被用于读写,如上所述,借助用于同步的原始栅栏将写入缓冲上传至 GPU。为了实现线程化,示例使用了并行模式库中的 Microsoft’s concurrency::parallel_for结构。

代码可以在 D3D12nBodyGravity::SimulateCPU()D3D12nBodyGravity::ProcessParticles()中找到。

运行标量代码后,建议快速检查性能,以确保迁移至英特尔 SPMD 程序编译器前,没有需要解决的算法热点。在本示例中,使用英特尔® VTune™工具分析基本热点得出,反平方根 (sqrt) 位于热路径上,因此,被替换为来自 Quake* 的快速反平方根近似值,后者能小幅改进性能,由于失去了精度,改善效果不明显。

移植标量 C 至标量英特尔® SPMD 程序编译器

为了创建英特尔 SPMD 程序编译器内核以及将它们连接至您的应用(对 Microsoft Visual Studio 的修改将在后文中介绍)而修改构建系统后,可以开始编写英特尔 SPMD 程序编译器代码并将它连接至您的应用。

钩子

为了从您的应用代码中调用任何英特尔 SPMD 程序编译器内核,需要添加自动生成的相关输出头文件,然后按照调用正常库的方式调用导出的函数,请记住,所有声明包含在 ispc命名空间中。在本示例中,我们从 SimulateCPU()函数中调用 ispc::ProcessParticles

矢量数学

钩子建成后,下一步便是运行标量英特尔 SPMD 程序编译器代码,然后对它进行矢量化。简单修改后,多数标量 C 代码可直接进入英特尔 SPMD 程序编译器内核。在本示例中,尽管英特尔 SPMD 程序编译器提供模板化矢量类型,但是类型仅面向存储,因此,需要定义所有矢量数学类型和新的结构。完成后,所有 XMFLOAT 类型均被转换为 Vec3 和 Vec4 类型。

关键字

我们现在需要利用英特尔 SPMD 程序编译器的特定关键字修饰代码,帮助引导矢量化和编译。第一个关键字是函数签名使用的 export,类似于调用协定,以通知英特尔 SPMD 程序编译器内核入口点的所在。会产生两个影响。首先,将函数签名和任何所需的结构添加至自动生成的头文件,但是由于所有参数必须是标量,也为函数签名带来了限制,导致我们采用了另外两个关键字,varyinguniform

统一变量描述了无法共享的标量变量,但是它的内容可以在所有 SIMD 通道上共享,同时,不同的变量将被矢量化,在所有 SIMD 通道上得到独特的值。默认情况下,所有变量均不同,因此,尽管可以添加关键字,但是本示例没有采用。首次创建本内核的标量版本时,我们将使用统一关键字修饰变量,以确保变量是严格的标量。

英特尔 SPMD 程序编译器标准库

英特尔 SPMD 程序编译器提供一个能帮助移植的标准库,包含许多常见函数,如 floatbits() 和 intbits(),这些函数是快速反平方根函数进行浮点投影所必需的。

矢量化英特尔 SPMD 程序编译器内核

如果英特尔 SPMD 程序编译器内核按照预期正常运行,应立即实施矢量化。通常情况下,确定并行化的对象以及如何并行化最复杂。按照常规经验,移植 GPU 计算着色器需要遵循 GPU 矢量化的原有模式,在本示例中,核心计算内核被多个 GPU 执行单元并行调用。因此,我们为粒子更新的标量版本添加的新外层循环最需要进行矢量化。

对于矢量 ISA,分散/集中操作非常昂贵(尽管英特尔 AVX2 指令集对此进行了改进),因此,数据布局同样重要,连续内存位置通常更适用于频繁加载/存储。

并行循环

在 n 体示例中遵循这个经验规则,对外层循环进行矢量化,内层循环为标量。因此,将 8 个粒子加载至英特尔 AVX 寄存器中,并在全部 10,000 个粒子中测试这 8 个粒子。这 10,000 个位置将被视作标量变量,在所有 SIMD 通道上共享,没有分散/集中成本。英特尔 SPMD 程序编译器对我们隐藏实际的矢量宽度(除非我们非常想知道),可以提供出色的提取,以透明支持英特尔 SSE4、英特尔® 高级矢量扩展指令集 512(英特尔® AVX-512)等指令集中的各种 SIMD 宽度。

通过将外层 for循环替换为英特尔 SPMD 程序编译器 foreach循环,实施矢量化,指导英特尔 SPMD 程序编译器在 N 尺寸的数据块范围内迭代,N 是当前的矢量宽度。因此,一旦 foreach循环迭代器 ii被用于取消阵列变量,对于矢量中每个 SIMD 通道,ii 的值均不同,确保每个通道运行相异的粒子。

数据布局

此时,简要介绍数据布局非常重要。在 CPU 上使用矢量寄存器时,高效加载和卸载寄存器非常重要;不这样做将导致性能大幅下降。为此,矢量寄存器需要从阵列结构 (SoA) 数据来源中加载数据,因此,可以使用单条指令将相邻内存值的矢量宽度直接加载至运行中的矢量寄存器。如果无法实现,需要降低收集操作的速度,以加载非相邻值的矢量宽度至矢量寄存器,需要通过分散操作再次保存数据。

在本示例中,类似于许多图形应用,粒子数据保存于结构阵列 (AoS) 布局中。可将其转换为 SoA,以避免分散/集中,但是鉴于算法的本质,相比处理内层循环中的 10,000 个标量粒子,外层循环所需的分散/集中成本更低,因此,数据保存为 AoS。

矢量变量

我们的目标是矢量化外层循环,同时保持内层循环的标量格式,因此,外层循环粒子的矢量宽度将处理相同的内层循环粒子。为此,我们宣告 pos, vel,accel可变,从而将外层循环粒子的位置、速度和加速度加载至矢量寄存器。具体做法是删除添加至标量内核的 uniform修饰,这样一来,英特尔 SPMD 程序编译器知道需要矢量化这些变量。

通过 bodyBodyInteractionQ_rsqrt函数传播,以确保正确地矢量化。这只是遵循变量传输和检查编译器错误的一个示例。最终,Q_rsqrt实现了完全矢量化,对 bodyBodyInteraction进行了大范围矢量化,内层循环粒子位置 thatPos为标量。

这就是所需的全部,现在,英特尔 SPMD 程序编译器矢量化内核开始运行,提供优于标量版本的卓越性能。

性能

在两个不同的英特尔 CPU 上对修改后的 n 体应用进行测试,并使用 PresentMon*捕获性能数据,在每颗 CPU 上各运行 3 次,每次 10 秒,记录帧的时长,然后取平均值。结果显示,相比标量 C/C++ 代码,面向英特尔 AVX2 的英特尔 SPMD 程序编译器内核实现了 8–10 倍的性能提升。两台设备均使用 Nvidia* 1080 GTX GPU,并使用了所有可用的 CPU 内核。

处理器

标量 CPU 实施

英特尔® AVX2 实施(使用英特尔® SPMD 程序编译器编译)

提升

英特尔® 酷睿™ i7-7700K 处理器

92.37 毫秒

8.42 毫秒

10.97 倍

英特尔酷睿 I7-6950X 处理器至尊版

55.84 毫秒

6.44 毫秒

8.67 倍

如何将英特尔 SPMD 程序编译器集成至 Microsoft Visual Studio*

  1. 确保英特尔 SPMD 程序编译器在路径上或者可以在 Microsoft Visual Studio* 中轻松定位。
  2. 将英特尔 SPMD 程序编译器内核添加至您项目。如果文件类型无法识别,将不能在默认情况下创建。
  3. 右键点击文件 Properties,将项目类型改为 Custom Build Tool:

  4. 单击 OK并重新打开 Property 页面,便可以修改自定义创建工具。

    a. 使用以下命令行格式:

    ispc -O2 <filename> -o <output obj> -h <output header> --target=<target backends> --opt=fast-math

    b. 本示例中使用的完整命令行为:

    $(ProjectDir)..\..\..\..\third_party\ispc\ispc -O2 "%(Filename).ispc" -o "$(IntDir)%(Filename).obj" -h "$(ProjectDir)%(Filename)_ispc.h" --target=sse4,avx2 --opt=fast-math

    c. 添加编译器生成的相关输出 ie. obj文件:

    $(IntDir)%(Filename).obj;$(IntDir)%(Filename)_sse4.obj;$(IntDir)%(Filename)_avx2.obj

    d. 将 Link Object设置为 Yes

  5. 现在编译您的英特尔 SPMD 程序编译器内核。如果编译成功,将生成一个头文件和一个目标文件。
  6. 将头文件添加至您的项目,并将它放入应用源代码中。

  7. 从相关位置调用英特尔 SPMD 程序编译器内核,请记住,从内核中导出的任何函数都将位于英特尔 SPMD 程序编译器命名空间:

总结

本文的目的在于展示英特尔 SPMD 程序编译器能帮助开发人员将高度矢量化的 GPU 计算内核轻松迁移到矢量化 CPU 代码,以充分利用备用 CPU 周期,为用户提供更丰富的游戏体验。任何自然进行矢量化的工作负载都可以借助英特尔 SPMD 程序编译器内核轻松提升性能,无需使用标量代码。英特尔 SPMD 程序编译器可减少开发和维护时间,也可以便捷地支持全新指令集。

追逐 VR《美国梦》,澳大利亚出品

$
0
0

原文由 Intel Game Dev 发表于 VentureBeat*:追逐 VR《美国梦》,澳大利亚出品。请访问 VentureBeat 的英特尔页面,了解更多游戏开发新闻和相关主题。

logo/banner page for Australian game The American Dream

一款带领大家重温 20 世纪 50 年代美国文化的 VR 游戏由澳大利亚墨尔本的小型独立游戏开发团队制作而成?Didn’t quite see that one coming.双手由枪支代替,哪怕是最简单的事情,也可以通过开枪来完成。是的,你必须坚持下去,看看外国人对某些美国文化的评论。或许并不适合所有人。

“我们让您以持枪美国人的身份领略生活!”Samurai Punk 总经理兼美术师 Nicholas McDonnell 笑着说。

《美国梦 VR》大胆地通过游戏讽刺美国的枪支制度和文化,这必然会在政治界掀起轩然大波。毫无无疑,这是一种赢得关注的方式,但它展示了一种新手团队希望制作游戏的游戏类型。但它不仅仅是一款以讽刺意味来吸引注意力的游戏。

McDonnell 与联合创始人 Winston Tang 自 2013 年中期开始合作开发游戏,在工作之余参加 game jam。2014 年,他们勇敢地尝试制作一款 McDonnell 描述为“俄罗斯方块混搭”的游戏。事实证明,和所有首次尝试一样,这是一次很好的实践。他补充说:“失败是成功之母,我们从这次失败中学到了很多,了解了如何制作游戏,如何推广游戏(或更准确的说法是如何不推广游戏)。”

接下来是颇受好评的分屏射击游戏《作弊达人(Screencheat)》,他们通过这款游戏展现了创意十足的游戏理念和稀奇古怪的幽默。为此 Samurai Punk 增加了两名员工,团队成员增加至五人,他们希望呈现的共同愿景是打造一支更加稳定的游戏团队。McDonnell 说:“我们尝试别人不愿意尝试的事情,我们通过政治讽刺手段进入小众市场 [VR],讽刺美国的枪支文化。我们希望制作能够传达某种意义的游戏,借此引发社会关注或传达有趣的游戏机制或风格。”

与许多类似的独立工作室一样,他们在墨尔本的共用工作空间从事游戏开发。同样,在采访时也会有很大的背景杂音,提示 McDonnell 反驳道:“嘿,Winston,让你的农场安静点!”)[插入你自己的澳大利亚口音,来欣赏墨尔本当地小型游戏工作室的疯狂。]

游戏畅销,加上 McDonnell 以工作室总经理的身份争取来的政府资助、投资或当地小额捐赠,让他们意识到 VR 是制作《美国梦》的理想平台。

Animated gif of gun, teddy bear and toddler
上图:略微不同的“枪支展”。

如何制作《美国梦》

McDonnell 说:“说实话,《美国梦》最初来源于一个玩笑。我们开玩笑说我们扮演许多枪手,随即产生了一个问题“《使命召唤》的人回到家后会做些什么?”他会不会通过开枪来开门,拿瓶啤酒,击落瓶盖?”最主要的部分就是,枪是枪手的主要特征,当你朝特定方向延伸这一古怪的概念,最终的结果是,在《美国梦 VR》中,枪支代替双手,通过开枪完成所有事情,比如冲咖啡,做汉堡等等。

成功解决 PlayStation VR 套件问题后,McDonnell 开始进入 VR 部分,他说:“路线控制器开始提供我们所需的新选项。传统 FPS 游戏就是持枪完成指向-点击动作。在《美国梦》中,VR 使游戏设计变得更加合理。这当然不是一项财政决策,但似乎在 VR 中实现了全面的稳定增长。”

Animated gif of gun, assembly rollers, dog
上图:枪械公司赞助游乐园“ride”,教美国人用枪完成一切,包括做汉堡和冲咖啡。

学习如何在 VR 中编程是这个小团队遇到的其中一个较大的挑战。McDonnell 解释说:“每种平台都有自己的支持方式,Vive 主要由社区推动;PlayStation 比较传统,拥有开发人员论坛、私人支持和账号经理。”

系统之间的追踪距离也有很大的不同,因此团队需要为《美国梦》制定重要的游戏决策。“我们从 Vive 开始,然后降至用 Oculus 追踪,然后降至 PS4.....开发人员做完这个做那个,这听起来就像是一个噩梦。我们不想增加自己的工作负担,因为我们学会了新的 VR 编程方法。”

我们将游戏场景换成子弹形状的货车。McDonnell 说:“场景设置在枪械公司运营的 Epcot 中心,他们在那里教美国人如何使用枪支。”Buddy Washington 以拉布拉多犬的口吻提供游戏旁白,为玩家引路。你不能自己随意走动。

Screenshot of 4th of July event, gun, dog, fireworks

上图:做完汉堡后,开枪点燃烟花庆祝美国独立日。

McDonnell 说:“构建了坐在房间开枪的功能后,开始想玩家每天能做的事情。吃饭、喝咖啡、上班、上厕所、生孩子。然后开始想有哪些好玩并有身体互动的事情。不能朝移动的目标开枪,更像是完成一系列任务。以做汉堡为例,只需朝环境中指定的地方开枪,就可拿起汉堡递给客户。”

游戏通过这种机制,带领大家回到 20 世纪 50 年代的美国,经历人生的各个阶段。McDonnell 补充说:“许多灵感都来源于视频游戏,因为我们扮演过许多射击者。例如在《半条命 Half-Life》中,需要对着门锁开枪,把门打开,或打开自动贩售机拿可乐。”

不出所料,社区反映褒贬不一。McDonnell 说:“YouTube 上的评论是 50-50。”他对此感到非常自豪。他继续说道:“有的觉得上厕所很搞笑,有的说‘这是什么?’或‘愚蠢的澳大利亚人’,也有的说‘耶,去他妈的美国’。”

一部特别的美国独立日预告片清楚地展现了这种机制如何运行。无论观众的反映如何,都代表着这个小团队所面临的风险和机遇。他们认为这个世界仍然有许多问题需要解决。

McDonnell 问道:“我们投入一款三年游戏?消失在茫茫宇宙中?或者不管《美国梦》反响如何,继续执行下一项计划?”

我想我们可以来猜猜这些问题的答案。

使用英特尔® 架构中的深度神经网络检测无人看管行李

$
0
0

随着世界中面临的潜在安全威胁变得越来越多,企业对部署复杂的监控系统的需求开始不断增长。能够作为一个直观的“机器人眼睛”,准确实时地检测无人看管行李的智能系统,已经成为机场、火车站、商场和其他公共区域的安保人员的关键需求。本文介绍了用于火车站的无人看管行李检测的 Microsoft Common Objects in Context (MS-COCO) 检测模式。

1.物体检测算法的演进

图像分类涉及在预定义标签之中预测图像的标签。这会假定图像中有单个相关物体,占据图像的较大部分。检测不仅在于找到物体的类别,而且在于确定物体在图像中的体积。可将物体分层到图像的任何地方,且可以是任何大小(比例)。因此,当图像有多个物体、物体较小,同时需要准确的位置和图像时,物体分类将会用处不大。

传统的检测方法涉及使用块式方向直方图(SIFT 或 HOG)功能,这无法在标准数据集中实现高精度,如 PASCAL VOC。这些方法会编码物体的低级别特性,因此无法有效区分不同的标签。基于深度学习(卷积网络)的方法成为图像物体检测的领先方法。各种网络拓扑已经随着时间的变化进行了演进,如图 1 所示。

图 1:检测算法的演进 [1]。

图 1:检测算法的演进 [1]。

2.安装

2.1 构建和安装为英特尔® 架构优化的 Caffe*

Caffe 可以在各种平台上安装使用,支持不同开发工具和库的组合。这里,我们将介绍在基于 Ubuntu* 的系统上,使用英特尔® 数学核心函数库 2017 构建和安装为英特尔® 架构优化的 Caffe*。请参见 git* 克隆 https://github.com/intel/caffe

1.克隆为英特尔架构优化的 Caffe,删除所有依赖关系。

导航到本地 caffe 目录,复制 makefile.config.example 并重命名为 makefile.config。

2.确保 makefile.config 中的以下行取消注释。

makefile.config

# 仅 CPU 开关(取消注释以在无 GPU 支持的情况下进行构建)

CPU_ONLY := 1

3.安装 OpenCV*。

针对计算机视觉和图像增强,安装 OpenCV 3.2 版本。

sudo apt-get install python-opencv

当使用 OpenCV 3 或更高版本时,注意在运行之前启用 Makefile.config 中的 OPENCV_VERSION := 3。

4.构建本地 Caffe。

导航到本地 caffe 目录。

NUM_THREADS=41
make -j $NUM_THREADS

5.安装和加载 Python*模块。

make pycaffe
pip install pandas
pip install scipy
    import sys
    CAFFE_ROOT = 'path/to/caffe'
    sys.path.append(CAFFE_ROOT)
    import caffe
    caffe.set_mode_cpu()

3.解决方案架构和设计

我们的解决方案旨在确定公共区域的无人看管行李,如火车站、机场等,然后出发报警。使用第 3.3 节定义的业务规则通过监控视频完成检测。网络拓扑

在图 1 提到的不同检测技术中,我们决定选择为英特尔架构优化的 Single Shot multibox Detector (SSD) [2]。研究人员表示,这一技术拥有出色的性能,即使在嵌入式系统和高端设备中也有出色表现,由此可被用于实时检测。

图 2.输入图像和功能图

图 2.输入图像和功能图

训练期间,SSD 仅需每个物体的输入图像和地面实况 (GT) 行李箱。在卷积方式中,将评估几个不同比例的特征图每个位置的少量(在我们的示例中,四个)不同纵横比的默认行李箱 [(b) 和 (c) 中的8 × 8 和 4 × 4](参见图 2)。SSD 充分利用更快 RCNN [3] 区域建议网络 (RPN) [4],将其直接用于分类每个之前的行李箱的物体,不是仅对物体置信进行评分。

对于每个默认行李箱,网络将预测所有物体类别的形状偏移和置信 [(c1, c2, ..., cp)]。训练期间,首先将默认行李箱与地面实况行李箱匹配。例如,两个默认行李箱(一个有一只猫,一个有一只狗)将被匹配,这被视为正。默认行李箱之外的行李箱被视为负。在本地化损失之间加权总和模型损失(示例:Smooth L1) 和置信损失(示例:Softmax)。

由于我们的使用案例涉及行李检测,因此 SSD 网络需要使用不同类型的行李进行训练,或者我们可以使用预训练的模型,如 使用 MS-COCO 数据集进行训练的 SSD300。我们决定使用预训练模型,这可从以下网址下载:https://github.com/weiliu89/caffe/tree/ssd#models

3.1 设计和范围

本使用案例的范围限为检测在一段时间内无人看管的行李。确定准确的所有人和行李跟踪不在本使用案例的范围内。

由于模型推理期间会生成大量行李箱,因此推理期间有效执行非极大值抑制 (NMS) 非常重要。使用 0.01 的置信阈值可以筛选出大部分行李箱。然后,可以使用每类别 0.45 的 Jaccard 重叠应用 NMS,保留每张图像的前 400 次检测。图 3 显示了在监控视频上运行检测的流程图。

图 3.检测流程图.

图 3.检测流程图。     

使用 OpenCV 将监控视频细分为帧,带可配置的每秒帧。生成帧后,它们将被传给检测模型,以四坐标(xmin、xmax、ymin 和 ymax)的形式确定不同物体的体积,并为不同的可能物体提供分类分数。应用 NMS 阈值和设置置信阈值可以减少预测数量,同时保持最佳的预测。OpenCV 被用于绘制矩形行李箱,检测的行李和人员周围有各种颜色。

3.2 定义业务规则

在我们的背景中,被遗弃的行李定义为所有人遗弃的行李物品。每个行李物品有一个所有人,每个人拥有最多一个行李物品。行李定义为包含可以随身携带的所有行李类型。示例:皮盒、袋子、帆布包、背包、包裹和手提箱。

以下规则适用于有人看管和无人看管的行李:

  • 所有行李都由带行李进入场景的人员拥有,直到行李不再与该人物理接触。
  • 此时,距离为 20 英寸(空间规则)时,行李仅由所有人看管。使用欧几里得距离测量所有距离。
  • 当所有人远离 b 米时,行李物品变为无人看管(其中 b ≥ a)。在这种情况下,系统会应用时空规则,从而检测本行李物品是否被遗弃(触发报警事件)。
  • 时空规则可以确定遗弃:行李物品被所有人留下无人看管连续 t 秒,所有人期间未重新来取该行李或行李被第二方看管(通过物理接触调查,此时可能会发出盗窃/篡改事件)。下图(图 7)显示了无人看管 t (=10) 秒的行李物品,此时将触发报警事件。这里,我们将时间 t 与每秒帧数 f 关联。如果我们的输入视频每秒有 t 帧,则 t 秒应定义为 (t*f) 帧。简而言之,无人看管 (t*f) 连续帧的袋子将触发报警。

3.3 推理 MS-COCO 模型

使用 Python 2.7.6 和 OpenCV 3.2 完成实施或推理。执行以下步骤(包含的代码片段用作参考):

  1. 读取输入视频:

    CAFFE_ROOT = '/home/979648/SSD/caffe/
    # -> 读取视频文件,并存储到目录
    TEST_VIDEO = cv2.VideoCapture(os.getcwd()+
    ‘InputVideo/SurveillanceVideo.avi')
    MODEL_DEF = 'deploy.prototxt'
  2. 加载网络架构。

    net = caffe.Net(MODEL_DEF, MODEL_WEIGHTS,caffe.TEST)
  3. 按帧读取视频,并基于模型推理每个帧,从而获得检测和分类分数。

    success, image = TEST_VIDEO.read()
    if (success):
        refObj = None
        imageToNet = cv2.resize(image, (300, 300))
        image_convert = np.swapaxes(np.swapaxes(imageToNet, 1, 2), 0, 1)
        net.blobs['data'].data[...]= image_convert
        # Forward pass.
        detections = net.forward()['detection_out']
    
        # Parse the outputs.
        det_label = detections[0, 0, :, 1]
        det_conf = detections[0, 0, :, 2]
        det_xmin = detections[0, 0, :, 3]
        det_ymin = detections[0, 0, :, 4]
        det_xmax = detections[0, 0, :, 5]
        det_ymax = detections[0, 0, :, 6]
    
        # Get detections with confidence higher than 0.6.
        top_indices = [i for i, conf in enumerate(det_conf) if conf >=    CONFIDENCE]
    
        top_conf = det_conf[top_indices]
    
        top_label_indices = det_label[top_indices].tolist()
        top_labels = get_labelname(labelmap, top_label_indices)
        top_xmin = det_xmin[top_indices]
        top_ymin = det_ymin[top_indices]
        top_xmax = det_xmax[top_indices]
        top_ymax = det_ymax[top_indices]
    
        colors = plt.cm.hsv(np.linspace(0, 1, 21)).tolist()
    
        currentAxis = plt.gca()
        # print('Detected Size : ', top_conf.shape[0])
    
        detectionDF = pd.DataFrame()
        if (top_conf.shape[0] != 0):
            for i in xrange(top_conf.shape[0]):
                xmin = int(round(top_xmin[i] * image.shape[1]))
                ymin = int(round(top_ymin[i] * image.shape[0]))
                xmax = int(round(top_xmax[i] * image.shape[1]))
                ymax = int(round(top_ymax[i] * image.shape[0]))
                score = top_conf[i]
                label = int(top_label_indices[i])
                label_name = top_labels[i]
                display_txt = '%s: %.2f' % (label_name, score)
                detectionDF = detectionDF.append(
                        {'label_name': label_name, 'score': score, 'xmin': xmin, 'ymin': ymin, 'xmax': xmax, 'ymax': ymax},
                        ignore_index=True)
    
    		detectionDF = detectionDF.sort('score', ascending=False)
  4. 要计算图像中的物体之间的距离,必须使用参考物体。参考物体有两个主要属性:

       a) 一些可测量单位的物体尺寸,如英寸或毫米。在这种情况下,我们认为尺寸是英寸。

       b) 我们可以轻松找到并识别图像中的参考物体。

    此外,必须假定参考物体的近似宽度。在这种情况下,我们假定手提箱的宽度 (args[‘width’]) 是 27 英寸。

    $ pip install imutils
    if refObj is None:
    	  # unpack the ordered bounding box, then compute the
    	  # midpoint between the top-left and top-right points,
    	  # followed by the midpoint between the top-right and
    	  # bottom-right
    	  (tl, tr, br, bl) = box
    	  (tlblX, tlblY) = midpoint(tl, bl)
    	  (trbrX, trbrY) = midpoint(tr, br)
    
    	  # compute the Euclidean distance between the midpoints,
    	  # then construct the reference object
    	  D = dist.euclidean((tlblX, tlblY), (trbrX, trbrY))
    	  refObj = (box, (cX, cY), D / args["width"])
    	  continue
  5. 一旦获得参考物体,可以计算图像中的参考物体和其他物体之间的距离。此时将会应用业务规则,然后触发适当的报警。在这种情况下,将在物体上突出显示红色行李箱。

    if refObj != None:
    D = dist.euclidean((objBotX, objBotY), (int(tlblX), int(tlblY))) /  refObj[2]
    (mX, mY) = midpoint((objBotX, objBotY), (tlblX, tlblY))
    
    //apply spacio temporal rule
         // Highlight with Green/Yellow /Red

     

  6. 保存处理的图像,然后附加到输出视频。

4.实验结果

当在样本 YouTube* 视频上运行推理使用案例时,获得了以下检测(参见图 4—7),该视频可从以下网址获得:https://www.youtube.com/watch?v=fpTG4ELZ3bE

图 4:带着行李进入场景的人员,当前是安全的(突出显示为绿色)。Figure 2
图 4:带着行李进入场景的人员,当前是安全的(突出显示为绿色)。
图 5:所有人远离行李。图 6:系统发出警告信号。
图 5:所有人远离行李。图 6:系统发出警告信号。
图 7:所有人在帧之外,系统发出视频报警(闪烁红色)。
图 7:所有人在帧之外,系统发出视频报警(闪烁红色)。

5.结论和未来工作

我们观察到,系统可以准确检测中高质量图像中的行李。对于多个所有人,该系统还能够检测不止一个行李。然而,系统无法检测低质量视频的行李。距离计算不包括焦距、摄像头角度和平面,因此当前计算逻辑存在限制。当前系统还无法跟踪行李。

使用英特尔® 至强® 处理器 E5-2699 v4 @ 2.20 GHz 推理模型,该处理器具有 22 核和 64 GB 可用内存。未来工作包括确定行李所有人及跟踪行李,增强当前使用案例。还将推理具有不同角度和焦距的视频,从而判断系统的有效性。我们工作的下一阶段还将考虑并行化推理模型的工作。

6.参考资料和链接

使用英特尔®GPU的性能调优达到153帧MobileNet-SSD目标检测

$
0
0

介绍

随着自动驾驶,智能视频监控,人脸检测和各种行人计数应用的兴起,快速、 准确的目标检测系统处于上升的需求。目标检测不仅需要确定图像中每个目标是什么,还需要把目标用方框标记出来。这使得目标检测比传统的图像分类更加复杂,计算量更大。

本文采用的MobileNet-SSD 模型来自 https://github.com/chuanqi305/MobileNet-SSD(MobileNet-SSD 也被选为谷歌对象检测 API),本文将展示如何在英特尔®GPU上优化MobileNet-SSD来获得总153 fps(6 路视频流,每路25fps)的性能。

英特尔® GPU 优化的clCaffe

本优化基于英特尔® clCaffe, GitHub 地址是 https://github.com/01org/caffe ,操作系统是最新的 Ubuntu 16.04 (内核版本是 4.7.10)。

在Skull NUC i7 (其GPU是Iris 580 Pro,GT4e架构72EU),如果直接运行视频分析demo,demo code 在/examples/video_analysis/video_analysis.cpp , 取batch为6的时候,FP16的性能只有50fps (浮点数是 32 位的称为 FP32,目前CPU只支持FP32,而 GPU 支持半精度浮点数的格式,这被称为 FP16,也就是16位的浮点数)。但理论值为 160 fps (理论值可以使用caffe time命令来获取,理论值只计算推理时间,不包括前处理时间) 。实际值和理论值之间存在着巨大的差距。相对于FP16,FP32的性能和理论值120fps差距很小,但是为了获得最大化性能, 本文讲解如何优化FP16,以获得在batch=6(也就是6路视频流)的时候接近160fps的总性能。

如何优化

多线程

总的任务可以分成3个线程。线程 A 将视频解码然后缩放图像到 300 * 300 并把图像转换成FP16的数组,最后拷贝到GPU的内存。线程 B 将进行推理。线程 C 在屏幕上绘制结果。更改为多线程后在没有显示的情况下(显示的优化将在3.4提及)FP32 可以得到 118 fps (理论值为 120 fps),但 FP16 只有 105 fps。因为MobileNet-SSD的FP16对比FP32并没有任何精度损失, FP16 可以在同精度下获得最佳性能。FP16 优化对我们是很关键的。不同于FP32 ,在CPU上做FP32转FP16是使用CPU软件模拟的,它会导致大量的Cache miss和并浪费太多的内存带宽。这样就会导致推理的速度受到很大影响。

在GPU上做FP16到 FP32转换

既然CPU 不支持 FP16,FP16的转换通过软件模拟性能低,而GPU是支持FP16的数据格式的,那么把FP32 到 FP16的转换放在 GPU 中应该可以得到比较好的结果。此GPU内核的伪码为,FP16 = (FP32 -Mean) * Scale (注意为了最大化性能,此GPU内核还做了一些从图形转浮点数组的前处理)。线程 A 将用软解去解码视频 (之所以使用软解解码,是因为开源的硬件解码libyami会占用一些 GPU 资源,导致低性能的推理,闭源的MediaSDK在本文中还没有验证过),然后调用 FP32 到 FP16 的kernel 代码在GPU内做FP32到FP16的转换,但是测试结果显示帧率反而更糟糕了,有时是甚至低于 80 fps。

导致慢的原因是GPU GT4e架构是有缓存的,当图像被一帧一帧的放到GPU内存中时,后面批量输入的数据会被从缓存中移除,这将导致很缓慢的内存读取速度。解决方法是在GPU做推理前才填充GPU内存,保证GPU在做FP32转FP16的时候是从GPU Cache读取的。做了此优化后,帧率在没有显示的时候达到了148 fps,带显示则是122fps。

让 Ubuntu 总是保持最高的 GPU 性能

理论值160 fps,在测试此理论数值的时候 x-window系统是关闭的,但是在此demo中需要有显示输出,在x-window开启的时候,发现系统开机几分钟后,理论值降低到150fps。最后发现当屏幕变暗后,也就是screen lock后的性能才会降低到 150 fps。根本原因目前还不太清楚,猜测是进入待机状态后,Ubuntu 会做一些限制 GPU 性能的节电命令。取消屏幕Dim和Lock,保持屏幕常亮后,帧率理论值能稳定在160fps。

显示优化

当添加了6路显示后,总体性能从158 fps 下降到 130 fps。第一步优化是从 6 窗口转变为 1 窗口,也就是把6个640*480的图像拼合成一个 1920 * 960 帧,并在一个窗口中显示。此优化后帧率上升到143 fps,但是和158fps的帧率还是有差距。第二步优化是切换到硬件加速的显示,在 Ubuntu 中,它被称为 XCB (就像 DirectDraw,使用GPU内的2D加速器,所以对OpenCL计算的影响很小)。这里要注意,不要使用 OpenGL刷新,因为 GPU 已经满载了,OpenGL加速的刷新 比XLib 软件刷新更为缓慢。此优化后的性能达到了我们的最终目标153 fps。

结论

除了上面的4步优化,还有一些小的优化,比如RGBtoYUV 或 YUVtoRGB使用 libyuv 来做转换,因为它支持 avx SIMD软件加速,而OpenCV 颜色空间转换性能较差。图像缩放可以使用OpenCV,它和libyuv的性能差距不大。这里还要注意,不要用 OpenCL 加速 OpenCV,因为GPU 已经满载用于推理 (如果你运行 intel_gpu_top,你可以发现 GPU已经满载了)。

做完上述所有这些优化后6路MobileNet-SSD目标检测总帧率为153 fps,模型是从开源网站上获取的,并没有做任何模型的优化。实际上因为 MobileNet 是"depthwise可分离的卷积",通常它可以比较方便的做卷积的剪枝,剪枝大约为20%,且没有任何精度损失。所以真正的帧率可以比 153 fps 更高 !

关于作者

李雨洺是英特尔软件与服务事业部的一名应用软件工程师,专注于图像处理领域。 主要负责与开发者一起合作,将英特尔平台的相关图像技术(例如图像编码,图像深度学习处理)应用到开发者的产品中,以实现图像处理相关的应用场景,并提高应用在英特尔®平台上的性能。

在英特尔® 架构上构建和安装 TensorFlow Serving

$
0
0

简介

本教程系列的第一篇在英特尔® 架构上构建和安装 TensorFlow*,展示了如何通过 GitHub* 存储库中的可用资源构建和安装在英特尔® 架构上优化的 TensorFlow。本文所提供的信息将介绍如何构建和安装TensorFlow* Serving — 专为生产环境设计的高性能机器学习服务系统。

安装概述

本文所述的安装指南源于 TensorFlow Serving GitHub网站提供的信息。下文中的步骤仅供大致了解安装流程;由于第三方信息可能随时更改,因此建议您同时查看 TensorFlow Serving网站提供的信息。

重要提示:下面提供的分步指南假设读者已阅读过教程 在英特尔® 架构上构建和安装 TensorFlow,该教程提供了 Bazel* 构建工具的安装步骤,以及其他本文未介绍的必要相关性。

安装 gRPC*

首先安装 Google Protocol RPC* 存储库 (gRPC*) — 用于实施远程过程调用 (RPC) 服务的框架。

sudo pip install grpcio

安装相关性

接下来,通过以下命令确保已安装其他 TensorFlow Serving 相关性:

sudo apt-get update && sudo apt-get install -y \
build-essential \
curl \
libcurl3-dev \
git \
libfreetype6-dev \
libpng12-dev \
libzmq3-dev \
pkg-config \
python-dev \
python-numpy \
python-pip \
software-properties-common \
swig \
zip \
zlib1g-dev

安装 TensorFlow* Serving

使用以下命令,从 GitHub 存储库克隆 TensorFlow Serving:

   git clone --recurse-submodules https://github.com/tensorflow/serving

克隆过程中创建的 serving/tensorflow目录包含一个名为 “configure” 的脚本,该脚本必须执行,以定义路径名称、相关性和其他构建配置选项。对于在英特尔架构上优化的 TensorFlow,该脚本还支持您设置英特尔® 数学核心函数库(英特尔® MKL)的相关环境设置。发布以下命令:

cd serving/tensorflow
./configure

Important:选择 ‘Y’ 构建支持 MKL 的 TensorFlow,并选择 ‘Y’ 从 Web 中下载 MKL LIB。为其他配置参数选择默认设置。

cd ..
bazel build --config=mkl --copt="-DEIGEN_USE_VML" tensorflow_serving/...

测试安装情况

使用以下命令,测试 TensorFlow Serving 的安装情况:

bazel test tensorflow_serving/...

如果一切运行良好,将出现如下图 1 所示的结果。

Screenshot of a command prompt window with results of correct installation

图 1.TensorFlow Serving 安装测试结果。

后续文章

本系列教程的下一篇文章将介绍如何培训和保存 TensorFlow 模型、如何在 TensorFlow Serving 中托管该模型,以及如何在客户端应用中引用该模型。

没有任何秘密的 API:Vulkan* 简介第 6 部分

$
0
0

下载

查看 PDF [736 KB]

Back to: 第 5 部分分期资源


教程 6:描述符集 – 在着色器中使用纹理

我们知道如何创建显卡管线以及如何使用着色器在屏幕上绘制几何体。我们也学会了如何创建缓冲区并将它们用作顶点数据的来源(顶点缓冲区)。现在我们需要了解如何为着色器提供数据 — 我们将学习如何使用资源,比如着色器源代码中的采样器和图像,以及如何设置应用与可编程着色器阶段之间的界面。

在本教程中,我们将重点介绍类似于 OpenGL* 纹理的功能。但 Vulkan* 中没有此类对象。只有两种可以保存数据的资源:缓冲区和图像(还有 push constant,我们将通过单独的教程进行介绍)。它们均可提供给着色器,但需要调用资源描述符,不能直接提供给着色器。事实上,它们聚集在包装程序或称为描述符集的容器对象中。我们可在单个描述符集中放置多个资源,但需要按照这种集合的预定义结构。这种结构定义单个描述符集的内容 — 其中的资源类型、每种资源的数量,以及它们的顺序。在名为描述符集布局的对象中指定这类描述。编写着色器指示时需要指定类似的描述。它们共同组成 API(我们的应用)和可编程管线(着色器)之间的界面。

准备好布局和创建描述符集后,我们可以对其进行填充;这样可定义我们希望在着色器中使用的具体对象(缓冲区和/或图像)。之后,在发布命令缓冲区中的绘制命令之前,我们需要绑定此集合与命令缓冲区。这样我们可以使用着色器源代码中的资源;例如,从采样的图像(纹理)提取数据,或读取保存在统一缓冲区中的统一变量值。

在本部分教程中,我们将了解如何创建描述符集布局和描述符集本身。还将准备采样器和图像,以便将其制作成着色器中的纹理。我们还将了解如何在着色器中使用它们。

如前所述,本教程根据没有任何秘密的 API:Vulkan 简介教程的前面几部分所介绍的知识,仅介绍与所述主题的不同之处及重要之处。

创建图像

我们首先创建将来用作纹理的图像。图像代表连续内存区,将根据图像创建期间定义的规则进行编译。Vulkan 中仅有三种基本图像:1D、2D 和 3D。图像可以有 mipmap(细节层)、多个阵列层(要求至少一个),或每帧采样数。所有这些参数均在图像创建期间指定。在代码示例中,我们创建最常用的 2D 图像,包含每像素一个样本和 4 个 RGBA 组件。

VkImageCreateInfo image_create_info = {
  VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO,  // VkStructureType        sType;
  nullptr,                              // const void            *pNext
  0,                                    // VkImageCreateFlags     flags
  VK_IMAGE_TYPE_2D,                     // VkImageType            imageType
  VK_FORMAT_R8G8B8A8_UNORM,             // VkFormat               format
  {                                     // VkExtent3D             extent
    width,                                // uint32_t               width
    height,                               // uint32_t               height
    1                                     // uint32_t               depth
  },
  1,                                    // uint32_t               mipLevels
  1,                                    // uint32_t               arrayLayers
  VK_SAMPLE_COUNT_1_BIT,                // VkSampleCountFlagBits  samples
  VK_IMAGE_TILING_OPTIMAL,              // VkImageTiling          tiling
  VK_IMAGE_USAGE_TRANSFER_DST_BIT |     // VkImageUsageFlags      usage
  VK_IMAGE_USAGE_SAMPLED_BIT,
  VK_SHARING_MODE_EXCLUSIVE,            // VkSharingMode          sharingMode
  0,                                    // uint32_t               queueFamilyIndexCount
  nullptr,                              // const uint32_t        *pQueueFamilyIndices
  VK_IMAGE_LAYOUT_UNDEFINED             // VkImageLayout          initialLayout
};

return vkCreateImage( GetDevice(), &image_create_info, nullptr, image ) == VK_SUCCESS;

1.Tutorial06.cpp, function CreateImage()

创建图像时我们需要准备 VkImageCreateInfo 类型的结构。该结构包含创建图像所需的基本参数集。这个参数可通过以下成分来指定:

  • sType – 结构类型。必须等于 VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO 的值。
  • pNext – 为扩展功能预留的指示器。
  • flags – 描述图像其他属性的参数。通过该参数,我们可规定通过稀疏内存备份该图像。但有一个更有趣的值:VK_IMAGE_CREATE_CUBE_COMPATIBLE_BIT,支持我们将图像用作立方图。如果没有其他要求,可将该参数设为 0。
  • imageType – 图像的基本类型(维数):1D、2D 或 3D。
  • format – 图像格式:组件数量、每个组件的位数,以及数据类型。
  • extent – 每种维度下的图像大小(纹素/像素数量)。
  • mipLevels – 细节层数量 (mipmap)。
  • arrayLayers – 阵列层数量。
  • samples – 每纹数样本数量(正常图像为 1,多样本图像大于 1)。
  • tiling – 定义图像的内层内存结构:线性或最佳。
  • usage – 定义我们希望该图像在整个生命周期中的所有用途。
  • sharingMode – 规定是否每次从多个家族中排队访问该图像(与创建交换链或缓冲区时所使用的的 sharingMode 参数相同。)
  • queueFamilyIndexCount – pQueueFamilyIndices 阵列中的元素数量(仅指定并发共享模式时使用)。
  • pQueueFamilyIndices – 所有队列的索引阵列(队列通过它访问图像)(仅指定并发共享模式时使用)。
  • initialLayout – 用于创建图像的内存布局。我们仅提供未定义或预初始化布局。在命令缓冲区中使用图像之前,我们还需要进行布局过渡。

图像创建期间定义的大部分参数都具备自解释性,或类似于创建其他资源时所使用的参数。但这些参数还需要进一步的解释。

区块指图像的内层内存结构(但不可与布局混淆)。图像可能包含线性或最佳区块(缓冲区通常包含线性区块)。包含线性区块的图像以线性的方式布局纹素,一个接一个,一排接一排。我们可以查询所有相关图像的内存参数(偏移和大小、行、阵列和深度步长)。这样我们可知道图像内容如何保存在内存之中。此类区块可用于(通过映射图像内存)直接将数据拷贝至图像。遗憾的是,包含线性区块的图像存在多种限制。例如,Vulkan 规格规定仅 2D 图像必须支持线性区块。硬件厂商可能在其他类型的图形中实施线性区块支持,但不是强制性的,所以我们不依赖此类支持。但更重要的是,线性区块图像的性能不及其他同类最佳区块图像。

当我们指定图像最佳区块,意味着我们不了解内存的结构。执行应用的每种平台都可能以完全不同的方式保存图像内容,在但实际应用中并不能映射图像内存,也不能直接将图像拷贝至 CPU 或从 CPU 拷贝图像(需要使用分期资源,缓冲区或图像)。但这样我们可以创建任何想要的图像(不存在类似线性区块图像的限制),而且应用也能实现更高的性能。因此强烈建议经常指定图像最佳区块。

现在我们重点介绍 initialLayout 参数。如前关于交换链的教程所述,布局规定图像的内存布局,并与我们使用图像的方式有着密切的关系。每种特定用途都有其自己的内存布局。以特定方式使用图像之前,需要执行布局过渡。例如,交换链图像仅以 VK_IMAGE_LAYOUT_PRESENT_SRC_KHR 的布局在屏幕上显示。如果想渲染成图像,需要将其内存布局设为 VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL。还有一种通用布局,允许我们以任何方式使用图像,但会影响性能,不建议使用这种布局(仅在必要时使用)。

现在,如果我们想更换图像的使用方式,需要执行上面所说的布局过渡。必须指定当前的(旧)布局和新布局。旧布局包含 1-2 个值:当前图像布局或未定义布局。指定当前图像布局的值时,图像内容在过渡期间保存。但如果不需要图像内容,我们可以提供未定义布局。这样布局过渡的速度更快。

此时将用到 initialLayout 参数。我们指定 1-2 个值 — 未定义或预初始化。预初始化布局值帮助我们在图像的第一次布局过渡期间保存图像内容。这样我们可以通过内存映射将数据拷贝至图像,但这一做法并不实用。可以直接(通过内存映射)将数据拷贝至线性区块图像,如前所述这样存在诸多限制。实际上来说,这些图像只能用作分期资源 — 在 GPU 和 CPU 之间传输数据。我们也可以使用缓冲区传输数据;因为使用缓冲区拷贝数据比使用线性区块图像更简单。

总而言之,在大部分情况下,未定义布局可用于 initialLayout 参数。在这种情况下,图像内容不能直接(通过映射内存)初始化。但如果我们想初始化,可以使用临时缓冲区将数据拷贝至图像。本教程将介绍这种方法。

最后一点是记住这种用法。与缓冲区类似,创建图像时,需要指定用于使用图像的所有方法。之后不能更改,也不能以创建过程中没有指定的方法使用图像。这里我们想在着色器中将图像用作纹理。为此我们指定 VK_IMAGE_USAGE_SAMPLED_BIT 用途。还需要将数据上传至图像的方法。我们将从图像文件中读取数据,并将其拷贝至图像对象。具体方法是使用分期资源传输数据。在这种情况下,图像将成为传输操作对象,因此我们指定 VK_IMAGE_USAGE_TRANSFER_DST_BIT 用途。

现在,如果我们有所有参数的值,将可以创建图像。具体做法是,调用 vkCreateImage()函数,我们需要为该函数提供逻辑设备句柄、上述结构指示器,以及 VkImage 类型变量指示器(其中保存有已创建图像的句柄)。

分配图像内存

与缓冲区类似,图像没有自己的内存,因此使用之前,需要绑定内存和图像。为此,我们首先需要了解待绑定至图像的内存的属性。为此我们调用 vkGetImageMemoryRequirements()函数。

VkMemoryRequirements image_memory_requirements;
vkGetImageMemoryRequirements( GetDevice(), Vulkan.Image.Handle, &image_memory_requirements );

2.Tutorial06.cpp, function AllocateImageMemory()

上述调用将所需内存参数保存在 image_memory_requirements 变量中。它将告诉我们所需的内存量以及特定物理设备支持的哪种内存可用于图像内存分配。如果不知道特定物理设备支持的内存类型,可调用vkGetPhysicalDeviceMemoryProperties()函数了解相关信息。之前的教程对此进行了介绍,当时我们为缓冲区分配内存。接下来,迭代可用内存类型并检查哪些兼容我们的图像。

for( uint32_t i = 0; i < memory_properties.memoryTypeCount; ++i ) {
  if( (image_memory_requirements.memoryTypeBits & (1 << i)) &&
    (memory_properties.memoryTypes[i].propertyFlags & property) ) {

    VkMemoryAllocateInfo memory_allocate_info = {
      VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO, // VkStructureType  sType
      nullptr,                                // const void      *pNext
      image_memory_requirements.size,         // VkDeviceSize     allocationSize
      i                                       // uint32_t         memoryTypeIndex
    };

    if( vkAllocateMemory( GetDevice(), &memory_allocate_info, nullptr, memory ) == VK_SUCCESS ) {
      return true;
    }
  }
}
return false;

3.Tutorial06.cpp, function AllocateImageMemory()

每种类型都有特定的属性集。如果想绑定内存和图像,也有我们自己的具体要求。例如,我们可能需要通过映射直接访问内存,因此该内存必须主机可见。如果有其他要求,可对比每种可用内存的属性。如果发现有匹配的属性,可以使用特定内存类型并通过调用 vkAllocateMemory()函数分配内存对象。

然后需要绑定内存和图像。为此我们调用 vkBindImageMemory()函数并提供与内存绑定的图像的句柄、内存对象的句柄,以及从内存对象开始的偏移,比如:

if( vkBindImageMemory( GetDevice(), Vulkan.Image.Handle, Vulkan.Image.Memory, 0 ) != VK_SUCCESS ) {
  std::cout << "Could not bind memory to an image!"<< std::endl;
  return false;
}

4.Tutorial06.cpp, function CreateTexture()

绑定内存和对象时偏移值非常重要。Vulkan 中的资源对内存偏移对齐有特定的要求。image_memory_requirements 变量也提供了具体的要求。绑定内存时提供的偏移必须是变量的对齐成分的倍数。0 通常是有效的偏移值。

当然,如果我们想绑定内存和图像,不需要每次创建新的内存对象。最好创建一少部分比较大的内存对象,并通过提供相应的偏移值绑定其中的一部分。

创建图像视图

如果想在应用中使用图像,我们很少提供图像的句柄,而通常使用图像视图。它们提供一个额外层以解析将在特定环境中使用的图像内容。例如,我们有一个多层图像(2D 阵列),想仅渲染至特定的阵列层。为此我们创建一个图像视图,以便定义我们想使用的那层。另外一个例子是包含 6 个阵列层的图像。我们可以使用图像视图,将其解析成立方图。

Vulkan 简介第 3 部分:第一个三角形介绍过如何创建图像视图,因此这部分仅提供所使用的源代码。

VkImageViewCreateInfo image_view_create_info = {
  VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO, // VkStructureType          sType
  nullptr,                                  // const void              *pNext
  0,                                        // VkImageViewCreateFlags   flags
  image_parameters.Handle,                  // VkImage                  image
  VK_IMAGE_VIEW_TYPE_2D,                    // VkImageViewType          viewType
  VK_FORMAT_R8G8B8A8_UNORM,                 // VkFormat                 format
  {                                         // VkComponentMapping       components
    VK_COMPONENT_SWIZZLE_IDENTITY,            // VkComponentSwizzle       r
    VK_COMPONENT_SWIZZLE_IDENTITY,            // VkComponentSwizzle       g
    VK_COMPONENT_SWIZZLE_IDENTITY,            // VkComponentSwizzle       b
    VK_COMPONENT_SWIZZLE_IDENTITY             // VkComponentSwizzle       a
  },
  {                                         // VkImageSubresourceRange  subresourceRange
    VK_IMAGE_ASPECT_COLOR_BIT,                // VkImageAspectFlags       aspectMask
    0,                                        // uint32_t                 baseMipLevel
    1,                                        // uint32_t                 levelCount
    0,                                        // uint32_t                 baseArrayLayer
    1                                         // uint32_t                 layerCount
  }
};

return vkCreateImageView( GetDevice(), &image_view_create_info, nullptr, &image_parameters.View ) == VK_SUCCESS;

5.Tutorial06.cpp, function CreateImageView()

将数据拷贝至图像

现在我们需要将数据拷贝至图像。我们使用临时缓冲区来完成这一步骤。首先创建一个足以保存图像数据的缓冲区。接下来分配主机可见(可映射)的内存,并将其绑定至缓冲区。然后将数据拷贝至缓冲区的内存,如下所示:

// Prepare data in staging buffer
void *staging_buffer_memory_pointer;
if( vkMapMemory( GetDevice(), Vulkan.StagingBuffer.Memory, 0, data_size, 0, &staging_buffer_memory_pointer ) != VK_SUCCESS ) {
  std::cout << "Could not map memory and upload texture data to a staging buffer!"<< std::endl;
  return false;
}

memcpy( staging_buffer_memory_pointer, texture_data, data_size );

VkMappedMemoryRange flush_range = {
  VK_STRUCTURE_TYPE_MAPPED_MEMORY_RANGE,  // VkStructureType   sType
  nullptr,                                // const void       *pNext
  Vulkan.StagingBuffer.Memory,            // VkDeviceMemory    memory
  0,                                      // VkDeviceSize      offset
  data_size                               // VkDeviceSize      size
};
vkFlushMappedMemoryRanges( GetDevice(), 1, &flush_range );

vkUnmapMemory( GetDevice(), Vulkan.StagingBuffer.Memory );

6.Tutorial06.cpp, function CopyTextureData()

我们映射缓冲区的内存。此操作为我们提供一个指示器,其用法与其他 C++ 指示器相同。我们为其拷贝纹理数据,并告知驱动程序在该操作中(我们刷新内存)哪部分缓冲区的内存有所变化。最后,取消内存映射,但这不是必需的步骤。

通过以下代码从文件中读取图像数据:

int width = 0, height = 0, data_size = 0;
std::vector texture_data = Tools::GetImageData( "Data06/texture.png", 4, &width, &height, nullptr, &data_size );
if( texture_data.size() == 0 ) {
  return false;
}

if( !CopyTextureData( &texture_data[0], data_size, width, height ) ) {
  std::cout << "Could not upload texture data to device memory!"<< std::endl;
  return false;
}

7.Tutorial06.cpp, function CreateTexture()

为实现本教程的目的,我们将下图用作纹理:

Image of a large semi truck with intel logo on the side, speeding down the road

 

将数据从缓冲区拷贝至图像这一操作要求记录命令缓冲区并将其提交至队列。调用 vkBeginCommandBuffer()函数开始记录:

// Prepare command buffer to copy data from staging buffer to a vertex buffer
VkCommandBufferBeginInfo command_buffer_begin_info = {
  VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO,  // VkStructureType                        sType
  nullptr,                                      // const void                            *pNext
  VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT,  // VkCommandBufferUsageFlags              flags
  nullptr                                       // const VkCommandBufferInheritanceInfo  *pInheritanceInfo
};

VkCommandBuffer command_buffer = Vulkan.RenderingResources[0].CommandBuffer;

vkBeginCommandBuffer( command_buffer, &command_buffer_begin_info);

8.Tutorial06.cpp, function CopyTextureData()

在记录命令缓冲区一开始,需要对图像执行布局过渡。我们想将数据拷贝至图像,因此要将布局改成 VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL。我们需明确执行这一操作,具体做法是使用图像内存壁垒并调用 vkCmdPipelineBarrier()函数:

VkImageSubresourceRange image_subresource_range = {
  VK_IMAGE_ASPECT_COLOR_BIT,              // VkImageAspectFlags        aspectMask
  0,                                      // uint32_t                  baseMipLevel
  1,                                      // uint32_t                  levelCount
  0,                                      // uint32_t                  baseArrayLayer
  1                                       // uint32_t                  layerCount
};

VkImageMemoryBarrier image_memory_barrier_from_undefined_to_transfer_dst = {
  VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER, // VkStructureType           sType
  nullptr,                                // const void               *pNext
  0,                                      // VkAccessFlags             srcAccessMask
  VK_ACCESS_TRANSFER_WRITE_BIT,           // VkAccessFlags             dstAccessMask
  VK_IMAGE_LAYOUT_UNDEFINED,              // VkImageLayout             oldLayout
  VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,   // VkImageLayout             newLayout
  VK_QUEUE_FAMILY_IGNORED,                // uint32_t                  srcQueueFamilyIndex
  VK_QUEUE_FAMILY_IGNORED,                // uint32_t                  dstQueueFamilyIndex
  Vulkan.Image.Handle,                    // VkImage                   image
  image_subresource_range                 // VkImageSubresourceRange   subresourceRange
};
vkCmdPipelineBarrier( command_buffer, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, nullptr, 0, nullptr, 1, &image_memory_barrier_from_undefined_to_transfer_dst);

9.Tutorial06.cpp, function CopyTextureData()

接下来拷贝数据。为此我们需要提供描述数据来源和目的地的参数:希望更新哪部分图像 (imageSubresource member)、所提供部分中的特定区域 (imageOffset),以及图像的大小。关于数据来源,我们需要提供启动数据的缓冲区内存一开始的偏移、数据的结构,以及缓冲区内想象图像的大小(行与列的大小)。幸运的是,我们能够以适合图像的方式保存数据。这样我们可将两个参数(bufferRowLength 和 bufferImageHeight)都设为 0,规定按照图像大小紧密封装数据。

VkBufferImageCopy buffer_image_copy_info = {
  0,                                  // VkDeviceSize               bufferOffset
  0,                                  // uint32_t                   bufferRowLength
  0,                                  // uint32_t                   bufferImageHeight
  {                                   // VkImageSubresourceLayers   imageSubresource
    VK_IMAGE_ASPECT_COLOR_BIT,          // VkImageAspectFlags         aspectMask
    0,                                  // uint32_t                   mipLevel
    0,                                  // uint32_t                   baseArrayLayer
    1                                   // uint32_t                   layerCount
  },
  {                                   // VkOffset3D                 imageOffset
    0,                                  // int32_t                    x
    0,                                  // int32_t                    y
    0                                   // int32_t                    z
  },
  {                                   // VkExtent3D                 imageExtent
    width,                              // uint32_t                   width
    height,                             // uint32_t                   height
    1                                   // uint32_t                   depth
  }
};
vkCmdCopyBufferToImage( command_buffer, Vulkan.StagingBuffer.Handle, Vulkan.Image.Handle, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, &buffer_image_copy_info );

10.Tutorial06.cpp, function CopyTextureData()

最后是执行另外一种布局过渡。我们的图像将在着色器中用作纹理,因此需要将其过渡至 VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL 布局。之后可以结束命令缓冲区,将其提交至队列,并等待传输完成(在真实应用中,我们应该等待并用其他的方法(例如使用旗语)同步操作,以避免不必要的管线停顿)。

VkImageMemoryBarrier image_memory_barrier_from_transfer_to_shader_read = {
  VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER,   // VkStructureType              sType
  nullptr,                                  // const void                  *pNext
  VK_ACCESS_TRANSFER_WRITE_BIT,             // VkAccessFlags                srcAccessMask
  VK_ACCESS_SHADER_READ_BIT,                // VkAccessFlags                dstAccessMask
  VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,     // VkImageLayout                oldLayout
  VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, // VkImageLayout                newLayout
  VK_QUEUE_FAMILY_IGNORED,                  // uint32_t                     srcQueueFamilyIndex
  VK_QUEUE_FAMILY_IGNORED,                  // uint32_t                     dstQueueFamilyIndex
  Vulkan.Image.Handle,                      // VkImage                      image
  image_subresource_range                   // VkImageSubresourceRange      subresourceRange
};
vkCmdPipelineBarrier( command_buffer, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, 0, 0, nullptr, 0, nullptr, 1, &image_memory_barrier_from_transfer_to_shader_read);

vkEndCommandBuffer( command_buffer );

// Submit command buffer and copy data from staging buffer to a vertex buffer
VkSubmitInfo submit_info = {
  VK_STRUCTURE_TYPE_SUBMIT_INFO,            // VkStructureType              sType
  nullptr,                                  // const void                  *pNext
  0,                                        // uint32_t                     waitSemaphoreCount
  nullptr,                                  // const VkSemaphore           *pWaitSemaphores
  nullptr,                                  // const VkPipelineStageFlags  *pWaitDstStageMask;
  1,                                        // uint32_t                     commandBufferCount
  &command_buffer,                          // const VkCommandBuffer       *pCommandBuffers
  0,                                        // uint32_t                     signalSemaphoreCount
  nullptr                                   // const VkSemaphore           *pSignalSemaphores
};

if( vkQueueSubmit( GetGraphicsQueue().Handle, 1, &submit_info, VK_NULL_HANDLE ) != VK_SUCCESS ) {
  return false;
}

vkDeviceWaitIdle( GetDevice() );

11.Tutorial06.cpp, function CopyTextureData()

现在我们的图像已创建并完全初始化(包含相应的数据)。但还未准备好纹理。

创建采样器

在 OpenGL 中创建纹理时,图像和采样参数都已指定。在之后的 OpenGL 版本中,我们还可创建独立的采样器对象。在着色器中,我们通常创建 sampler2D 类型的变量,它还结合了图像及其采样参数(采样器)。在 Vulkan 中,我们需要单独创建图像和采样器。

采样器规定如何在着色器中读取图像数据:是否启用过滤、是否想使用 mipmap(或 mipmap 的特定子界),或我们想使用的寻址模式(锁定或包装)。

VkSamplerCreateInfo sampler_create_info = {
  VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO,  // VkStructureType        sType
  nullptr,                                // const void*            pNext
  0,                                      // VkSamplerCreateFlags   flags
  VK_FILTER_LINEAR,                       // VkFilter               magFilter
  VK_FILTER_LINEAR,                       // VkFilter               minFilter
  VK_SAMPLER_MIPMAP_MODE_NEAREST,         // VkSamplerMipmapMode    mipmapMode
  VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE,  // VkSamplerAddressMode   addressModeU
  VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE,  // VkSamplerAddressMode   addressModeV
  VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE,  // VkSamplerAddressMode   addressModeW
  0.0f,                                   // float                  mipLodBias
  VK_FALSE,                               // VkBool32               anisotropyEnable
  1.0f,                                   // float                  maxAnisotropy
  VK_FALSE,                               // VkBool32               compareEnable
  VK_COMPARE_OP_ALWAYS,                   // VkCompareOp            compareOp
  0.0f,                                   // float                  minLod
  0.0f,                                   // float                  maxLod
  VK_BORDER_COLOR_FLOAT_TRANSPARENT_BLACK,// VkBorderColor          borderColor
  VK_FALSE                                // VkBool32               unnormalizedCoordinates
};

return vkCreateSampler( GetDevice(), &sampler_create_info, nullptr, sampler ) == VK_SUCCESS;

12.Tutorial06.cpp, function CreateSampler()

上述所有参数均通过 VkSamplerCreateInfo 类型的变量来指定。它包含许多成分:

  • sType – 结构类型。必须等于 VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO 的值。
  • pNext – 为扩展功能预留的指示器。
  • flags – 必须设为 0。改参数留作以后使用。
  • magFilter – 用于放大的过滤类型(最近或线性)。
  • minFilter – 用于缩小的过滤类型(最近或线性)。
  • mipmapMode – 用于查找 mipmap 的过滤类型(最近或线性)。
  • addressModeU - <0.0; 1.0> 范围外的 U 坐标寻址模式。
  • addressModeV - <0.0; 1.0> 范围外的 V 坐标寻址模式。
  • addressModeW - <0.0; 1.0> 范围外的 W 坐标寻址模式。
  • mipLodBias - 添加至 mipmap 细节层计算的偏差值。如果想抵消从特定 mipmap 提取数据,可以提供 0.0 之外的值。
  • anisotropyEnable - 定义是否使用异性过滤的参数。
  • maxAnisotropy - 用于异性过滤的最大容许值(锁定值)。
  • compareEnable - 在纹理查找过程中支持对比参考值。
  • compareOp - compareEnable 参数设为真值时在查找期间执行的对比类型。
  • minLod - 数据提取过程中使用的最小容许细节层。如果计算出的细节层(mipmap 层)小于该值,它将被锁定。
  • maxLod - 数据提取过程中使用的最大容许细节层。如果计算出的细节层(mipmap 层)大于该值,它将被锁定。
  • borderColor - 规定边框像素的预定义颜色。边框颜色在寻址模式包含边框颜色锁定时使用。
  • unnormalizedCoordinates - 通常(当该参数设为假值)我们使用标准化 <0.0; 1.0> 范围提供纹理坐标。当设为真值时,该参数允许我们指定我们想使用标准化坐标并通过纹素寻址纹理(在 <0; texture dimension> 范围中,类似于 OpenGL 的矩形纹理)。

采样器对象通过调用 vkCreateSampler()函数创建,我们为它提供一个针对上述结构的指示器。

使用描述符集

我们创建了图像、绑定了内存,甚至将数据上传至图像。还创建了采样器以为纹理设置采样参数。现在我们想使用该纹理。我们如何使用?通过描述符集。

我们在教程开头说过,着色器中使用的资源被称为描述符。Vulkan 中有 11 种描述符:

  • 采样器 - 定义读取图像数据的方式。在着色器中,采样器可用于多个图像。
  • 采样图像 - 定义在着色器中用于读取数据的图像。我们使用不同的采样器从单个图像中读取数据。
  • 合并图像采样器 - 这些描述符将采样器和采样图像合成一个对象。从 API(我们的应用)的角度来说,我们仍然需要创建采样器和图像,但在着色器中它们以单个对象的形式出现。使用它们要优于(性能更高)使用单独的采样器和采样图像。
  • 存储图像 - 该描述符支持我们读取和保存图像中的数据。
  • 输入附件 - 这是渲染通道附件的特定用法。如果想从在相同渲染通道中用作附件的图像读取数据,只能通过输入附件进行。使用这种方法时不需要结束渲染通道并启动另一个渲染通道,但仅限于片段着色器和每个片段着色器的单个位置(片段着色器的特定实例可从与片段着色器坐标相关的坐标读取数据)。
  • 统一缓冲区(及其动态变体) - 统一缓冲区支持我们读取统一变量的数据。在 Vulkan 中,这种变量放在全局范围内,我们需要使用统一缓冲区。
  • 存储缓冲区(及其动态变体) - 存储缓冲区支持我们读取和保存变量中的数据。
  • 统一纹素缓冲区 - 它们支持将缓冲区内容处理成包含纹理数据,解析成带有指定数量的组件和格式的纹素。这样我们可访问大型数据阵列(比统一缓冲区大)。
  • 存储纹素缓冲区 - 类似于统一纹素缓冲区。不仅用于读取数据,还可用于保存数据。

上述所有描述符均通过采样器、图像或缓冲区创建。但我们使用并在着色器中访问它们的方式不同。此类访问的所有额外参数都会对性能产生影响。例如,使用存储缓冲区只能读取数据,但读取数据的速度可能比将数据保存在存储缓冲区快。同样,纹素缓冲区支持我们访问的元素比统一缓冲区多,但这样可能会降低性能。我们应该记住,要选择适合需求的描述符。

在本教程中我们想使用纹理。为此我们创建了图像和采样器。我们还将使用它们准备合并图像采样器描述符。

创建描述符集布局

准备着色器在创建描述符集布局开始时使用的资源。描述符集是不透明的对象,其中保存了资源的句柄。布局规定了描述符集的结构 — 包含哪种类型的描述符、每种类型的描述符有多少,以及它们的顺序如何。

image of a diagram

创建描述符集布局,首先定义特定集中的所有可用描述符的参数。具体做法是填充 VkDescriptorSetLayoutBinding 类型变量结构:

VkDescriptorSetLayoutBinding layout_binding = {
  0,                                          // uint32_t             binding
  VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER,  // VkDescriptorType     descriptorType
  1,                                          // uint32_t             descriptorCount
  VK_SHADER_STAGE_FRAGMENT_BIT,               // VkShaderStageFlags   stageFlags
  nullptr                                     // const VkSampler     *pImmutableSamplers
};

13.Tutorial06.cpp, function CreateDescriptorSetLayout()

上述描述包含以下成分:

  • binding – 特定集的描述符索引。单个布局(和集)的所有描述符必须唯一绑定。相同的绑定可在着色器中用于访问描述符。
  • descriptorType – 描述符(采样器、统一缓冲区等)的类型
  • descriptorCount – 指定类型以阵列形式访问的描述符数量。单个描述符应该使用的值为 1。
  • stageFlags – 定义所有将访问特定描述符的着色器阶段的一套标记。为了提高性能,我们应仅规定将访问特定资源的阶段。
  • pImmutableSamplers – 影响仅在布局中永久绑定(以后不能更改)的采样器。但我们不必担心这个参数,可通过将该参数设为 null,以其他描述符的形式绑定采样器。

在示例中,我们想仅使用合并图像采样器的一个描述符,仅片段着色器访问该描述符。它将成为特定布局中的第一个(绑定 0)描述符。为避免浪费内存,应该绑定地尽可能紧凑(尽量接近 0),因为驱动程序可能会为描述符插槽分配内存,即使不用。

我们可以为从单个集访问的其他描述符准备类似的参数。然后为 VkDescriptorSetLayoutCreateInfo 类型的变量提供变量指示器:

VkDescriptorSetLayoutCreateInfo descriptor_set_layout_create_info = {
  VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO,  // VkStructureType                      sType
  nullptr,                                              // const void                          *pNext
  0,                                                    // VkDescriptorSetLayoutCreateFlags     flags
  1,                                                    // uint32_t                             bindingCount&layout_binding                                       // const VkDescriptorSetLayoutBinding  *pBindings
};

if( vkCreateDescriptorSetLayout( GetDevice(), &descriptor_set_layout_create_info, nullptr, &Vulkan.DescriptorSet.Layout ) != VK_SUCCESS ) {
  std::cout << "Could not create descriptor set layout!"<< std::endl;
  return false;
}

14.Tutorial06.cpp, function CreateDescriptorSetLayout()

该结构仅包含几个成分:

  • sType – 结构类型。必须等于 VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO 的值。
  • pNext – 为扩展功能预留的指示器。
  • flags – 该参数支持我们为创建布局提供另外的选项。但因为它们通过扩展连接,所以我们将该参数设为 0。
  • bindingCount – 绑定的数量,pBindings 阵列中的元素。
  • pBindings – 特定布局中所有资源的描述阵列指示器。该阵列必须大于 bindingCount 参数的值。

填充完该结构后,我们可调用 vkCreateDescriptorSetLayout()函数创建描述符集布局。稍后我们需要多次使用该布局。

创建描述符池

下一步是准备描述符集。描述符集与命令缓冲区类似,不能直接创建,而是从池中分配。分配描述符集之前,需要创建一个描述符池。

VkDescriptorPoolSize pool_size = {
  VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER,      // VkDescriptorType               type
  1                                               // uint32_t                       descriptorCount
};

VkDescriptorPoolCreateInfo descriptor_pool_create_info = {
  VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO,  // VkStructureType                sType
  nullptr,                                        // const void                    *pNext
  0,                                              // VkDescriptorPoolCreateFlags    flags
  1,                                              // uint32_t                       maxSets
  1,                                              // uint32_t                       poolSizeCount
  &pool_size                                      // const VkDescriptorPoolSize    *pPoolSizes
};

if( vkCreateDescriptorPool( GetDevice(), &descriptor_pool_create_info, nullptr, &Vulkan.DescriptorSet.Pool ) != VK_SUCCESS ) {
  std::cout << "Could not create descriptor pool!"<< std::endl;
  return false;
}

15.Tutorial06.cpp, function CreateDescriptorPool()

创建描述符池涉及到规定从中分配的描述符集数量。同时我们还需要规定描述符的类型,以及跨所有描述符集从池中分配多少描述符集。例如,如果想从特定池中分配一个采样图像和一个存储缓冲区,我们可从该池中分配两个描述符集。进行该操作时,如果分配一个带有采样图像的描述符集,第二个描述符可以仅包含一个存储缓冲区。如果从该池分配的单个描述符集包含两种资源,我们将不能分配另一个描述符集,因为它必须是空的。在描述符池创建期间,我们定义可分配的描述符总数和描述符集总数。该操作包含两个步骤。

首先准备 VkDescriptorPoolSize 类型的变量,它规定描述符类型以及可从池中分类的指定类型的描述符总数。接下来向 VkDescriptorPoolCreateInfo 类型的变量提供此类变量阵列。它包含以下成分:

  • sType – 结构类型。在本情况下应设为 VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO。
  • pNext – 为扩展功能预留的指示器。
  • flags – 该参数定义(当使用 VK_DESCRIPTOR_POOL_CREATE_FREE_DESCRIPTOR_SET_BIT 标记)从池中分配的描述符集是否单独释放或重设。如果该参数设为 0,从池中分配的所有描述符集只能通过重设整个池来一次性(批量)完成重设。
  • maxSets – 从池中分配的描述符集总数。
  • poolSizeCount – 定义 pPoolSizes 阵列中元素的数量。
  • pPoolSizes – 包含与 poolSizeCount 一样多的描述符类型的阵列,以及可从池中分配的类型的描述符总数的指示器。

在本示例中,我们想仅分配一个描述符集,它仅包含一个合并图像采样器类型的描述符。我们根据示例准备参数并通过调用 vkCreateDescriptorPool()函数创建描述符池。

分配描述符集

现在我们准备分配描述符集。所用的代码很短:

VkDescriptorSetAllocateInfo descriptor_set_allocate_info = {
  VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO, // VkStructureType                sType
  nullptr,                                        // const void                    *pNext
  Vulkan.DescriptorSet.Pool,                      // VkDescriptorPool               descriptorPool
  1,                                              // uint32_t                       descriptorSetCount&Vulkan.DescriptorSet.Layout                    // const VkDescriptorSetLayout   *pSetLayouts
};

if( vkAllocateDescriptorSets( GetDevice(), &descriptor_set_allocate_info, &Vulkan.DescriptorSet.Handle ) != VK_SUCCESS ) {
  std::cout << "Could not allocate descriptor set!"<< std::endl;
  return false;
}

16.Tutorial06.cpp, function AllocateDescriptorSet()

为分配描述符集,我们需要准备 VkDescriptorSetAllocateInfo 类型的变量,它包含以下成分:

  • sType – 标准结构类型。为达到分配描述符集的目的,我们需要将它设为 VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO 的值。
  • pNext – 为扩展功能预留的指示器。
  • descriptorPool – 分配命令缓冲区的描述符池的句柄。
  • descriptorSetCount – 希望分配的描述符集数量(以及 pSetLayouts 成分中的元素数量)。
  • pSetLayouts – 至少包含 descriptorSetCount 元素的阵列指示器。该阵列的每个元素都必须包含定义已分配描述符集内部结构的描述符集布局(例如元素可能重复,我们可一次分配 5 个描述符集,均包含相同的布局)。

从上述结构中可看出,我们需要提供描述符集布局。因此我们才需要提前创建它们。为了从提供的池中分配指定数量的描述符集,我们需要向 vkAllocateDescriptorSets()函数提供上述结构的指示器。

更新描述符集

我们准备了一个描述符集,但它是空的,未完成初始化。现在我们需要填充或更新它。这意味着我们将告诉驱动程序哪些资源应用于该集中的描述符。

可通过以下两种方法更新描述符集:

  • 写入描述符集 — 使用这种这种方法时我们提供新资源。
  • 拷贝其他描述符集的数据 — 如果有之前更新过的描述符集,并想在另一描述符集中使用一部分描述符,我们可以拷贝它们;这种方法比直接从 CPU 编写描述符集更快。

因为我们没有其他描述符集,所以需要直接写入一个描述符集。我们需要为每种描述符准备两种结构。第一种是所有描述符通用的 VkWriteDescriptorSet 结构。它包含以下成分:

  • sType – 结构类型。需要使用 VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET 值。
  • pNext – 为扩展功能预留的指示器。
  • dstSet – 我们希望更新(填充特定资源)的描述符集的句柄。
  • dstBinding – 我们希望更新的描述符集中的索引。必须提供在创建描述符集布局期间规定的绑定。另外,指定的绑定必须对应所提供的描述符类型。
  • dstArrayElement – 规定我们希望更新的第一个阵列索引。使用一种 VkWriteDescriptorSet 结构可更新一个阵列的多个元素。例如,我们有一个包含 4 个元素的采样器阵列,并且我们希望更新最后 2 个(索引为 2 和 3);我们可提供两个采样器并从索引 2 开始更新该阵列。
  • descriptorCount – 我们希望更新的描述符数量(pImageInfo, pBufferInfo 或 pTexelBufferView 阵列中的元素数量)。对于普通描述符,我们将该值设为 1。但对于阵列,我们可提供更大的值。
  • descriptorType – 即将更新的描述符类型。必须与在描述符集布局创建期间提供的且包含相同绑定(描述符集中的索引)的描述符类型相同。
  • pImageInfo – 至少包含 VkDescriptorImageInfo 类型的 descriptorCount 元素的阵列指示器。如果想更新 VK_DESCRIPTOR_TYPE_SAMPLER、VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE、VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER、VK_DESCRIPTOR_TYPE_STORAGE_IMAGE 或 VK_DESCRIPTOR_TYPE_INPUT_ATTACHMENT 描述符,每种元素都必须包含特定资源的句柄。
  • pBufferInfo – 至少包含 VkDescriptorBufferInfo 类型的 descriptorCount 元素的阵列指示器。如果想更新 VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER、VK_DESCRIPTOR_TYPE_STORAGE_BUFFER、VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER_DYNAMIC或 VK_DESCRIPTOR_TYPE_STORAGE_BUFFER_DYNAMIC 描述符,每种元素都必须包含特定资源的句柄。
  • pTexelBufferView – 至少包含 descriptorCount VkBufferView 句柄的阵列。该阵列用于更新 VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER、VK_DESCRIPTOR_TYPE_STORAGE_BUFFER、VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER_DYNAMIC 或 VK_DESCRIPTOR_TYPE_STORAGE_BUFFER_DYNAMIC 描述符。

根据我们希望更新的描述符类型,我们需要准备 VkDescriptorImageInfo、VkDescriptorBufferInfo 或 VkBufferView 类型的变量(或变量阵列)。这里,我们想更新合并图像采样器描述符,因此我们需要准备 VkDescriptorImageInfo 类型的变量。它包含以下成分:

  • sampler – 采样器对象的句柄。
  • imageView – 图像视图的句柄。
  • imageLayout – 这里我们提供在着色器中访问描述符时图像将呈现的布局。

在该结构中,我们提供特定资源的参数;指向已创建的并希望在着色器中使用的有效资源。结构成分将根据描述符的类型初始化。例如,如果更新采样器,我们仅需提供一个采样器的句柄。如果更新采样图像,我们需要提供图像视图的句柄以及图像的布局。但图像不会(像在渲染通道中那样)自动过渡到这种布局。我们需要执行布局过渡,通过管线壁垒明确进行或在有输入附件的情况下通过渲染通道进行。此外,我们需要提供对应于特定用途的布局。

在本示例中我们想使用纹理。可通过两种方法进行,一种是使用单独的采样器和采用图像描述符,另一种是使用合并图像采样器描述符(像在常见 OpenGL 应用中那样)。后一种方法更好(部分硬件平台从合并图像采样器抽样数据的速度比从单个采样器和采样图像抽样快),我们下面来介绍这种方法。如果想更新合并图像采样器,需要提供 VkDescriptorImageInfo 结构的所有成分:

VkDescriptorImageInfo image_info = {
  Vulkan.Image.Sampler,                       // VkSampler                      sampler
  Vulkan.Image.View,                          // VkImageView                    imageView
  VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL    // VkImageLayout                  imageLayout
};

VkWriteDescriptorSet descriptor_writes = {
  VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET,     // VkStructureType                sType
  nullptr,                                    // const void                    *pNext
  Vulkan.DescriptorSet.Handle,                // VkDescriptorSet                dstSet
  0,                                          // uint32_t                       dstBinding
  0,                                          // uint32_t                       dstArrayElement
  1,                                          // uint32_t                       descriptorCount
  VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER,  // VkDescriptorType               descriptorType
  &image_info,                                // const VkDescriptorImageInfo   *pImageInfo
  nullptr,                                    // const VkDescriptorBufferInfo  *pBufferInfo
  nullptr                                     // const VkBufferView            *pTexelBufferView
};

vkUpdateDescriptorSets( GetDevice(), 1, &descriptor_writes, 0, nullptr );

17.Tutorial06.cpp, function UpdateDescriptorSet()

VkDescriptorImageInfo 类型的变量指示器稍后将在 VkWriteDescriptorSet 类型的变量中提供。由于我们仅更新一个描述符,因此只需要两种结构的一个实例。不过我们也可以一次更新多个描述符,这样我们需要准备多个变量,之后提供给 vkUpdateDescriptorSets()函数。

创建管线布局

我们没有创建管线布局。如果想使用描述符,不仅需要分配和更新描述符集。我们准备的特定资源大部分是为了在着色器中使用,但描述符集可用于保存特定资源的句柄。这些句柄将在记录命令缓冲区时提供。我们需要为围挡的另一侧准备信息:驱动程序还需知道特定管线需要访问哪类资源。这种信息在创建管线时将起关键作用,因为它可能影响其内部结构或着色器编译。而且这种信息在管线布局中提供。

管线布局保存有关特定管线将访问的资源类型的信息。这些资源涉及描述符和 push constant 范围。现在我们跳过 push constant,重点介绍描述符。

为创建管线布局和准备有关该管线访问的资源类型信息,我们需要提供描述符集布局阵列。这可通过 VkPipelineLayoutCreateInfo 类型的变量的以下成分来完成:

  • sType – 为扩展功能预留的指示器。
  • flags – 该参数留作将来使用。
  • setLayoutCount – pSetLayouts 成分中的元素数量,以及管线可使用的描述符数量。
  • pSetLayouts – 包含描述符集布局的阵列。
  • pushConstantRangeCount – push constant 范围的数量。
  • pPushConstantRanges – 描述 push constant 范围的元素阵列。

这里将再次用到描述符集布局。单个描述符集布局定义单个描述符集所包含的资源类型。而且这些布局的阵列定义特定管线需访问的资源类型。

如果创建管线布局,我们只需调用 vkCreatePipelineLayout()函数即可。我们在 Vulkan 简介第 3 部分:第一个三角形中已创建过管线布局。但我们创建的是空白布局(没有 push constants,也不访问描述符资源)。这里我们创建一种比较典型的管线布局。

VkPipelineLayoutCreateInfo layout_create_info = {
  VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO,  // VkStructureType                sType
  nullptr,                                        // const void                    *pNext
  0,                                              // VkPipelineLayoutCreateFlags    flags
  1,                                              // uint32_t                       setLayoutCount&Vulkan.DescriptorSet.Layout,                   // const VkDescriptorSetLayout   *pSetLayouts
  0,                                              // uint32_t                       pushConstantRangeCount
  nullptr                                         // const VkPushConstantRange     *pPushConstantRanges
};

if( vkCreatePipelineLayout( GetDevice(), &layout_create_info, nullptr, &Vulkan.PipelineLayout ) != VK_SUCCESS ) {
  std::cout << "Could not create pipeline layout!"<< std::endl;
  return false;
}
return true;

18.Tutorial06.cpp, function CreatePipelineLayout()

此布局稍后将在管线创建期间提供。我们还需要在记录命令缓冲区期间绑定描述符集时使用该布局。因此我们需要保存管线布局句柄。

绑定描述符集

最后一件事是在记录期间将描述符集绑定至命令缓冲区。我们可有多个不同的描述符集或多个类似的描述符集(布局相同),但它们可能包含不同的资源句柄。在渲染期间使用哪些描述符将在命令缓冲区记录期间指定。进行绘制之前,我们需要(根据绘制参数)设置有效状态。对每个记录的命令缓冲区都需要重新设置。

绘制操作要求使用渲染通道和管线。如果管线使用描述符资源(当着色器访问图像或缓冲区),我们需要调用 vkCmdBindDescriptorSets()函数,绑定描述符集。我们必须为该函数提供管线布局句柄以及描述符集阵列句柄。将描述符集绑定至特定索引。绑定描述符集的特定索引必须对应管线创建期间索引相同的布局。

vkCmdBeginRenderPass( command_buffer, &render_pass_begin_info, VK_SUBPASS_CONTENTS_INLINE );

vkCmdBindPipeline( command_buffer, VK_PIPELINE_BIND_POINT_GRAPHICS, Vulkan.GraphicsPipeline );

// ...

vkCmdBindDescriptorSets( command_buffer, VK_PIPELINE_BIND_POINT_GRAPHICS, Vulkan.PipelineLayout, 0, 1, &Vulkan.DescriptorSet.Handle, 0, nullptr );

vkCmdDraw( command_buffer, 4, 1, 0, 0 );

vkCmdEndRenderPass( command_buffer );

19.Tutorial06.cpp, function PrepareFrame()

在着色器中访问描述符

另外,我们需要编写合适的着色器。在本示例中,我们仅在片段着色器中访问纹理,因此只需呈现片段着色器。

本教程的开头部分涉及到了描述符集、描述符集中的绑定以及绑定描述符集。可同时将多个描述符集绑定至一个命令缓冲区。每个描述符集可包含多种资源。该数据与我们在着色器中使用的特定地址是相对应的。可通过如下所示的 layout() 说明符定义该地址:

layout(set=S, binding=B) uniform <variable type> <variable name>

Set定义通过 vkCmdBindDescriptorSets()函数绑定的描述符集的索引。Binding规定所提供的描述符集中的资源的索引,对应创建描述符集布局期间所定义的绑定。在本示例中只有一个描述符集,索引为 0,包含一个绑定 0 的合并图像采样器。在着色器中通过 sampler1D、sampler2D 或 sampler 3D 变量访问合并图像采样器。因此片段着色器的源代码如下所示:

#version 450

layout(set=0, binding=0) uniform sampler2D u_Texture;

layout(location = 0) in vec2 v_Texcoord;

layout(location = 0) out vec4 o_Color;

void main() {
  o_Color = texture( u_Texture, v_Texcoord );
}

20.shader.frag, -

Tutorial06 执行

下方所示为该示例项目生成的最终图像:

Image of a large semi truck with intel logo on the side, speeding down the road

我们渲染纹理已应用于表面的四边形。该四边形应调整大小(和方位)以匹配窗口的大小和形状(如果拉伸窗口,该四边形和图像也会被拉伸)。

清理

结束应用之前应进行清理。

// ...

if( Vulkan.GraphicsPipeline != VK_NULL_HANDLE ) {
  vkDestroyPipeline( GetDevice(), Vulkan.GraphicsPipeline, nullptr );
  Vulkan.GraphicsPipeline = VK_NULL_HANDLE;
}

if( Vulkan.PipelineLayout != VK_NULL_HANDLE ) {
  vkDestroyPipelineLayout( GetDevice(), Vulkan.PipelineLayout, nullptr );
  Vulkan.PipelineLayout = VK_NULL_HANDLE;
}

// ...

if( Vulkan.DescriptorSet.Pool != VK_NULL_HANDLE ) {
  vkDestroyDescriptorPool( GetDevice(), Vulkan.DescriptorSet.Pool, nullptr );
  Vulkan.DescriptorSet.Pool = VK_NULL_HANDLE;
}

if( Vulkan.DescriptorSet.Layout != VK_NULL_HANDLE ) {
  vkDestroyDescriptorSetLayout( GetDevice(), Vulkan.DescriptorSet.Layout, nullptr );
  Vulkan.DescriptorSet.Layout = VK_NULL_HANDLE;
}

if( Vulkan.Image.Sampler != VK_NULL_HANDLE ) {
  vkDestroySampler( GetDevice(), Vulkan.Image.Sampler, nullptr );
  Vulkan.Image.Sampler = VK_NULL_HANDLE;
}

if( Vulkan.Image.View != VK_NULL_HANDLE ) {
  vkDestroyImageView( GetDevice(), Vulkan.Image.View, nullptr );
  Vulkan.Image.View = VK_NULL_HANDLE;
}

if( Vulkan.Image.Handle != VK_NULL_HANDLE ) {
  vkDestroyImage( GetDevice(), Vulkan.Image.Handle, nullptr );
  Vulkan.Image.Handle = VK_NULL_HANDLE;
}

if( Vulkan.Image.Memory != VK_NULL_HANDLE ) {
  vkFreeMemory( GetDevice(), Vulkan.Image.Memory, nullptr );
  Vulkan.Image.Memory = VK_NULL_HANDLE;
}

21.Tutorial06.cpp, function destructor

我们调用 vkDestroyPipeline()vkDestroyPipelineLayout()函数,破坏管线及其布局。接下来分别调用 vkDestroyDescriptorPool()vkDestroyDescriptorSetLayout()函数破坏描述符池和描述符集布局。肯定会破坏其他资源,但我们知道如何操作。注意,我们不释放描述符集。如果在描述符池创建期间提供有相应的标记,可以单独释放各描述符集。但没有必要 — 破坏描述符池时,所有从该池中分配的集均已释放。

结论

本教程主要介绍了如何在着色器中使用纹理(实际上是合并图像采样器)。为此我们创建了图像,并为此分配和绑定内存。还创建了图像视图。然后将数据从缓冲区拷贝至图像,以对其内容进行初始化。我们还创建了采样器对象,以定义如何在着色器中读取图像数据。

然后准备描述符集。首先创建描述符集布局。之后创建描述符池,以从中分配单个描述符集。我们通过采样器和图像视图的句柄更新该描述符集。

描述符集布局还用于定义显卡管线访问的资源。该操作在管线布局创建期间完成。然后在将描述符集绑定至命令缓冲区时使用该布局。

我们还学习了如何准备着色器代码,以便访问合并图像采样器以读取数据(以纹理的形式采样)。这一操作在渲染简单几何体期间所使用的片段着色器中完成。这样我们将纹理应用于几何体表面。

在下一教程中我们将学习如何在着色器中使用统一缓冲区。


该示例源代码根据“英特尔示例源代码许可协议”发布


没有任何秘密的 API:Vulkan* 简介第 5 部分

$
0
0

下载

查看 PDF [723KB]

返回:第 4 部分顶点属性
前往:第 6 部分描述符集


教程 5:分期资源 – 在缓冲区之间复制数据

本部分教程将重点关注如何改进性能。同时,我们将为下一篇教程做准备,下一篇教程将介绍图像与描述符(着色器资源)借助收集的信息,您将更轻松地掌握下一部分,最大限度发挥显卡硬件的性能。

什么是“分期资源”或“临时缓冲区”?它们指的是中间或临时资源,用于将数据从应用 (CPU) 传输到显卡内存 (GPU)。我们需要通过他们提升应用性能

在教程的第 4 部分,我们学习了如何使用缓冲区,将其绑定至主机可见内存,映射该内存,并将数据从 CPU 传输到 GPU。这个方法简单便捷,但是我们需要了解显卡内存的主机可见部分并不是最高效的部分。相比应用无法直接访问的内存(应用无法映射),它们的速度通常缓慢得多。这导致我们的应用以次优的方式执行。

解决该问题的办法是渲染过程中的所有资源始终使用设备本地内存。但是由于应用无法访问设备本地内存,我们不能将数据从 CPU 直接传输到内存。这就是我们需要中间或分期资源的原因。

在本部分教程,我们将借助顶点属性数据将缓冲区绑定至设备本地内存。我们将使用临时缓冲区协调从 CPU 到顶点缓冲区的数据传输。

再次声明,仅介绍本教程和上一教程(第 4 部分)的不同之处。

创建渲染资源

此次,我将渲染资源创建移至代码的开头部分。稍后,我们需要记录与提交一个命令缓冲区,以将数据从分期资源传输至顶点缓冲区。我还重构了渲染资源创建代码,以消除多个循环,将它们替换为单个循环。在这个循环中,我们创建构成虚拟帧的所有资源。

bool Tutorial05::CreateRenderingResources() {
  if( !CreateCommandPool( GetGraphicsQueue().FamilyIndex, &Vulkan.CommandPool ) ) {
    return false;
  }

  for( size_t i = 0; i < Vulkan.RenderingResources.size(); ++i ) {
    if( !AllocateCommandBuffers( Vulkan.CommandPool, 1, &Vulkan.RenderingResources[i].CommandBuffer ) ) {
      return false;
    }

    if( !CreateSemaphore( &Vulkan.RenderingResources[i].ImageAvailableSemaphore ) ) {
      return false;
    }

    if( !CreateSemaphore( &Vulkan.RenderingResources[i].FinishedRenderingSemaphore ) ) {
      return false;
    }

    if( !CreateFence( VK_FENCE_CREATE_SIGNALED_BIT, &Vulkan.RenderingResources[i].Fence ) ) {
      return false;
    }
  }
  return true;
}

bool Tutorial05::CreateCommandPool( uint32_t queue_family_index, VkCommandPool *pool ) {
  VkCommandPoolCreateInfo cmd_pool_create_info = {
    VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO,       // VkStructureType                sType
    nullptr,                                          // const void                    *pNext
    VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT | // VkCommandPoolCreateFlags       flags
    VK_COMMAND_POOL_CREATE_TRANSIENT_BIT,
    queue_family_index                                // uint32_t                       queueFamilyIndex
  };

  if( vkCreateCommandPool( GetDevice(), &cmd_pool_create_info, nullptr, pool ) != VK_SUCCESS ) {
    std::cout << "Could not create command pool!"<< std::endl;
    return false;
  }
  return true;
}

bool Tutorial05::AllocateCommandBuffers( VkCommandPool pool, uint32_t count, VkCommandBuffer *command_buffers ) {
  VkCommandBufferAllocateInfo command_buffer_allocate_info = {
    VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO,   // VkStructureType                sType
    nullptr,                                          // const void                    *pNext
    pool,                                             // VkCommandPool                  commandPool
    VK_COMMAND_BUFFER_LEVEL_PRIMARY,                  // VkCommandBufferLevel           level
    count                                             // uint32_t                       bufferCount
  };

  if( vkAllocateCommandBuffers( GetDevice(), &command_buffer_allocate_info, command_buffers ) != VK_SUCCESS ) {
    std::cout << "Could not allocate command buffer!"<< std::endl;
    return false;
  }
  return true;
}

bool Tutorial05::CreateSemaphore( VkSemaphore *semaphore ) {
  VkSemaphoreCreateInfo semaphore_create_info = {
    VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO,          // VkStructureType                sType
    nullptr,                                          // const void*                    pNext
    0                                                 // VkSemaphoreCreateFlags         flags
  };

  if( vkCreateSemaphore( GetDevice(), &semaphore_create_info, nullptr, semaphore ) != VK_SUCCESS ) {
    std::cout << "Could not create semaphore!"<< std::endl;
    return false;
  }
  return true;
}

bool Tutorial05::CreateFence( VkFenceCreateFlags flags, VkFence *fence ) {
  VkFenceCreateInfo fence_create_info = {
    VK_STRUCTURE_TYPE_FENCE_CREATE_INFO,              // VkStructureType                sType
    nullptr,                                          // const void                    *pNext
    flags                                             // VkFenceCreateFlags             flags
  };

  if( vkCreateFence( GetDevice(), &fence_create_info, nullptr, fence ) != VK_SUCCESS ) {
    std::cout << "Could not create a fence!"<< std::endl;
    return false;
  }
  return true;
}
1.	Tutorial05.cpp

1.Tutorial05.cpp

首先创建一个命令池,我们指示从这个池中分配的命令缓冲区存在时间较短。在本示例中,所有命令缓冲区将在记录之前仅提交一次。

接下来,我们将迭代任意数量的虚拟帧。本代码示例中虚拟帧的数量为 3。在循环内部,我们为每个虚拟帧分配一个命令缓冲区,创建两个旗语(一个用于图像获取,另一个用于显示帧渲染已完成)和一个栅栏。记录命令缓冲区之前,帧缓冲器创建已在绘制函数内部完成。

和第 4 部分使用的渲染资源集相同,您可以通过第 4 部分全面了解代码内部机制。我将跳过渲染通道和显卡管线创建。它们按照和之前完全相同的方式创建。由于此处无任何变动,我们将直接跳至缓冲区创建。

缓冲区创建

以下是创建缓冲区使用的通用代码:

VkBufferCreateInfo buffer_create_info = {
  VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO,             // VkStructureType                sType
  nullptr,                                          // const void                    *pNext
  0,                                                // VkBufferCreateFlags            flags
  buffer.Size,                                      // VkDeviceSize                   size
  usage,                                            // VkBufferUsageFlags             usage
  VK_SHARING_MODE_EXCLUSIVE,                        // VkSharingMode                  sharingMode
  0,                                                // uint32_t                       queueFamilyIndexCount
  nullptr                                           // const uint32_t                *pQueueFamilyIndices
};

if( vkCreateBuffer( GetDevice(), &buffer_create_info, nullptr, &buffer.Handle ) != VK_SUCCESS ) {
  std::cout << "Could not create buffer!"<< std::endl;
  return false;
}

if( !AllocateBufferMemory( buffer.Handle, memoryProperty, &buffer.Memory ) ) {
  std::cout << "Could not allocate memory for a buffer!"<< std::endl;
  return false;
}

if( vkBindBufferMemory( GetDevice(), buffer.Handle, buffer.Memory, 0 ) != VK_SUCCESS ) {
  std::cout << "Could not bind memory to a buffer!"<< std::endl;
  return false;
}

return true;

2.Tutorial05.cpp, function CreateBuffer()

将代码打包至一个 CreateBuffer() 函数,该函数接收缓冲区的使用、大小和所需的内存属性。为了创建缓冲区,需要准备类型变量 VkBufferCreateInfo。该结构包含以下字段:

  • sType – 标准结构类型。此处应等于 VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO。
  • pNext – 为扩展功能预留的指示器。
  • flags – 描述缓冲区其他属性的参数。现在,我们只能指定缓冲区可通过稀疏内存备份。
  • size – 缓冲区大小(字节)。
  • usage – 显示缓冲区既定用途的位字段。
  • sharingMode – 队列共享模式。
  • queueFamilyIndexCount – 并发共享模式下访问缓冲区的各类队列家族数量。
  • pQueueFamilyIndices – 使用并发共享模式时,访问缓冲区的所有队列家族的索引阵列。

目前,我们对绑定稀疏内存不感兴趣。我们不希望在不同设备队列之间共享缓冲区,因此,sharingMode、queueFamilyIndexCount 和 pQueueFamilyIndices 参数与本文无关。size 和 usage 是最重要的参数。我们不能将缓冲区用于缓冲器创建期间未指定的目的。最终,我们需要创建一个足够容纳数据的缓冲区。

为了创建缓冲区,需要调用 vkCreateBuffer()函数,调用成功后,缓冲区句柄被保存在我们提供地址的变量中。但是创建缓冲区远远不够,创建的缓冲区没有存储功能。我们需要将内存对象(或部分内存对象)绑定至缓冲区,以支持存储。如果没有内存对象,需要分配一个内存对象。

每个缓冲区的 usage 可能包含不同的内存要求,当我们想要分配内存对象并将其绑定到缓冲区时,便涉及到这些要求。以下代码示例可将一个内存对象分配至特定缓冲区:

VkMemoryRequirements buffer_memory_requirements;
vkGetBufferMemoryRequirements( GetDevice(), buffer, &buffer_memory_requirements );

VkPhysicalDeviceMemoryProperties memory_properties;
vkGetPhysicalDeviceMemoryProperties( GetPhysicalDevice(), &memory_properties );

for( uint32_t i = 0; i < memory_properties.memoryTypeCount; ++i ) {
  if( (buffer_memory_requirements.memoryTypeBits & (1 << i)) &&
      ((memory_properties.memoryTypes[i].propertyFlags & property) == property) ) {

    VkMemoryAllocateInfo memory_allocate_info = {
      VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO,       // VkStructureType                sType
      nullptr,                                      // const void                    *pNext
      buffer_memory_requirements.size,              // VkDeviceSize                   allocationSize
      i                                             // uint32_t                       memoryTypeIndex
    };

    if( vkAllocateMemory( GetDevice(), &memory_allocate_info, nullptr, memory ) == VK_SUCCESS ) {
      return true;
    }
  }
}
return false;

3.Tutorial05.cpp, function AllocateBufferMemory()

和第 4 部分的代码类似,我们首先查看特定缓冲区的内存要求。然后检查特定物理设备提供的内存属性,包含内存堆数量及其功能等信息。

接下来,我们迭代每个可用的内存类型,并检查它是否满足特定缓冲区的要求。我们还检查了特定内存类型是否支持请求的额外属性,如特定内存类型是否对主机可见。找到匹配对象后,我们将填充 VkMemoryAllocateInfo 结构,并调用一个 vkAllocateMemory()函数。

分配的内存对象便被绑定至缓冲区,从现在起,我们可以在应用中安全使用该缓冲区。

顶点缓冲区创建

顶点缓冲区是我们想要创建的第一个缓冲区,它能存储渲染期间使用的顶点属性数据。在本示例中,我们存储了四边形 4 个顶点的位置和颜色。与之前教程相比,最大的变化在于使用设备本地内存,而非主机可见内存。设备本地内存速度更快,但是我们无法直接将数据从应用复制到设备本地内存。我们需要使用临时缓冲区将数据复制到顶点缓冲区。

我们还需要为该缓冲区指定两个不同的 usage。第一个是 vertex buffer usage,意味着我们想将特定缓冲区用作顶点缓冲区,并从顶点缓冲区获取顶点属性数据。第二个是 transfer dst usage,意味着我们将复制数据至该缓冲区。它将被用作任何传输(复制)操作的目标。

以下代码用于创建包含上述要求的缓冲区:

const std::vector<float>& vertex_data = GetVertexData();

Vulkan.VertexBuffer.Size = static_cast<uint32_t>(vertex_data.size() * sizeof(vertex_data[0]));
if( !CreateBuffer( VK_BUFFER_USAGE_VERTEX_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, Vulkan.VertexBuffer ) ) {
  std::cout << "Could not create vertex buffer!"<< std::endl;
  return false;
}

return true;

4.Tutorial05.cpp, function CreateVertexBuffer()

起初,我们使用顶点数据(在 GetVertexData() 函数中硬编码)检查保留所有顶点值所需的空间。随后调用之前介绍的 CreateBuffer() 函数,以创建一个顶点缓冲区并将设备本地内存绑定于此。

临时缓冲区创建

下面我们将创建一个中间临时缓冲区。由于该缓冲区不在渲染过程中使用,因此,可以将其绑定至速度较慢的主机可见内存。通过这种方式,我们可以映射缓冲区,并从应用中直接复制数据。随后,我们可以将数据从临时缓冲区复制到绑定设备本地内存的任何缓冲区(甚至图像)。如此一来,用于渲染目的的所有资源均被绑定至速度最快的可用内存。我们只需执行其他数据传输操作。

以下代码可用于创建临时缓冲区:

Vulkan.StagingBuffer.Size = 4000;
if( !CreateBuffer( VK_BUFFER_USAGE_TRANSFER_SRC_BIT, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT, Vulkan.StagingBuffer ) ) {
  std::cout << "Could not staging buffer!"<< std::endl;
  return false;
}

return true;

5.Tutorial04.cpp, function CreateStagingBuffer()

我们将从该缓冲区复制数据至其他资源,因此,必须指定一个 transfer src usage(将用作传输操作的来源)。我们可以通过映射直接从应用中复制任何数据。为此,我们需要使用主机可见内存,这就是我们指定这个内存属性的原因。缓冲区大小可随机选择,但是必须足以保存顶点数据。在真实场景中,许多情况下我们应尝试尽可能多地重复使用临时缓冲区,使它的尺寸大到足以包含应用中大多数数据传输操作。当然,如果我们想同时执行多个传输操作,需要创建多个临时缓冲区。

在缓冲区之间复制数据

我们已经创建了两个缓冲区:一个用于顶点属性数据,另一个用作中间缓冲区。现在,我们需要将数据从 CPU 复制到 GPU。为此,需要映射临时缓冲区,获取一个指示器,并使用指示器将数据上传至显卡硬件内存。随后需要记录并提交命令缓冲区,后者将顶点数据从临时缓冲区复制到顶点缓冲区。现在,用于虚拟帧与渲染的所有命令缓冲区均被标记为短期,我们可以在操作中安全使用其中一个缓冲区。

首先,顶点属性数据如下所示:

static const std::vector<float> vertex_data = {
  -0.7f, -0.7f, 0.0f, 1.0f,
  1.0f, 0.0f, 0.0f, 0.0f,
  //
  -0.7f, 0.7f, 0.0f, 1.0f,
  0.0f, 1.0f, 0.0f, 0.0f,
  //
  0.7f, -0.7f, 0.0f, 1.0f,
  0.0f, 0.0f, 1.0f, 0.0f,
  //
  0.7f, 0.7f, 0.0f, 1.0f,
  0.3f, 0.3f, 0.3f, 0.0f
};

return vertex_data;

6.Tutorial05.cpp, function GetVertexData()

它是一个简单的硬编码浮点值阵列。每个顶点数据包含 4 个位置属性组件和 4 个颜色属性组件。如果我们渲染四个顶点,将得到 4 对属性。

以下代码将数据从应用复制到临时缓冲区,随后,从临时缓冲区复制到顶点缓冲区:

// Prepare data in a staging buffer
const std::vector<float>& vertex_data = GetVertexData();

void *staging_buffer_memory_pointer;
if( vkMapMemory( GetDevice(), Vulkan.StagingBuffer.Memory, 0, Vulkan.VertexBuffer.Size, 0, &staging_buffer_memory_pointer) != VK_SUCCESS ) {
  std::cout << "Could not map memory and upload data to a staging buffer!"<< std::endl;
  return false;
}

memcpy( staging_buffer_memory_pointer, &vertex_data[0], Vulkan.VertexBuffer.Size );

VkMappedMemoryRange flush_range = {
  VK_STRUCTURE_TYPE_MAPPED_MEMORY_RANGE,            // VkStructureType                        sType
  nullptr,                                          // const void                            *pNext
  Vulkan.StagingBuffer.Memory,                      // VkDeviceMemory                         memory
  0,                                                // VkDeviceSize                           offset
  Vulkan.VertexBuffer.Size                          // VkDeviceSize                           size
};
vkFlushMappedMemoryRanges( GetDevice(), 1, &flush_range );

vkUnmapMemory( GetDevice(), Vulkan.StagingBuffer.Memory );

// Prepare command buffer to copy data from staging buffer to a vertex buffer
VkCommandBufferBeginInfo command_buffer_begin_info = {
  VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO,      // VkStructureType                        sType
  nullptr,                                          // const void                            *pNext
  VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT,      // VkCommandBufferUsageFlags              flags
  nullptr                                           // const VkCommandBufferInheritanceInfo  *pInheritanceInfo
};

VkCommandBuffer command_buffer = Vulkan.RenderingResources[0].CommandBuffer;

vkBeginCommandBuffer( command_buffer, &command_buffer_begin_info);

VkBufferCopy buffer_copy_info = {
  0,                                                // VkDeviceSize                           srcOffset
  0,                                                // VkDeviceSize                           dstOffset
  Vulkan.VertexBuffer.Size                          // VkDeviceSize                           size
};
vkCmdCopyBuffer( command_buffer, Vulkan.StagingBuffer.Handle, Vulkan.VertexBuffer.Handle, 1, &buffer_copy_info );

VkBufferMemoryBarrier buffer_memory_barrier = {
  VK_STRUCTURE_TYPE_BUFFER_MEMORY_BARRIER,          // VkStructureType                        sType;
  nullptr,                                          // const void                            *pNext
  VK_ACCESS_MEMORY_WRITE_BIT,                       // VkAccessFlags                          srcAccessMask
  VK_ACCESS_VERTEX_ATTRIBUTE_READ_BIT,              // VkAccessFlags                          dstAccessMask
  VK_QUEUE_FAMILY_IGNORED,                          // uint32_t                               srcQueueFamilyIndex
  VK_QUEUE_FAMILY_IGNORED,                          // uint32_t                               dstQueueFamilyIndex
  Vulkan.VertexBuffer.Handle,                       // VkBuffer                               buffer
  0,                                                // VkDeviceSize                           offset
  VK_WHOLE_SIZE                                     // VkDeviceSize                           size
};
vkCmdPipelineBarrier( command_buffer, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_VERTEX_INPUT_BIT, 0, 0, nullptr, 1, &buffer_memory_barrier, 0, nullptr );

vkEndCommandBuffer( command_buffer );

// Submit command buffer and copy data from staging buffer to a vertex buffer
VkSubmitInfo submit_info = {
  VK_STRUCTURE_TYPE_SUBMIT_INFO,                    // VkStructureType                        sType
  nullptr,                                          // const void                            *pNext
  0,                                                // uint32_t                               waitSemaphoreCount
  nullptr,                                          // const VkSemaphore                     *pWaitSemaphores
  nullptr,                                          // const VkPipelineStageFlags            *pWaitDstStageMask;
  1,                                                // uint32_t                               commandBufferCount
  &command_buffer,                                  // const VkCommandBuffer                 *pCommandBuffers
  0,                                                // uint32_t                               signalSemaphoreCount
  nullptr                                           // const VkSemaphore                     *pSignalSemaphores
};

if( vkQueueSubmit( GetGraphicsQueue().Handle, 1, &submit_info, VK_NULL_HANDLE ) != VK_SUCCESS ) {
  return false;
}

vkDeviceWaitIdle( GetDevice() );

return true;

7.Tutorial05.cpp, function CopyVertexData()

起初,我们得到顶点数据,通过调用 vkMapMemory()函数映射临时缓冲区内存。调用过程中,我们指定绑定临时缓冲区的内存句柄和缓冲区大小。我们获得了一个指示器,可以在普通的 memcpy() 函数中使用指示器将数据从应用复制到显卡硬件。

接下来刷新映射内存,以通知驱动程序哪些内存对象被修改。如需要,可以指定多个内存范围。我们需要刷新一个存储区,通过创建一个类型变量 VkMappedMemoryRange 和调用 vkFlushMappedMemoryRanges()函数指定存储区。随后取消映射内存,但是没有必要这样做。我们可以保留一个指示器,以便稍后使用,这样不会影响应用性能。

接下来开始准备命令缓冲区。我们指定在重设前仅提交一次。填充 VkCommandBufferBeginInfo 结构并将它提供给 vkBeginCommandBuffer()函数。

现在执行复制操作。首先创建一个类型变量 VkBufferCopy,包含以下字段:

  • srcOffset – 从中复制数据的源缓冲区中的偏移量(字节)。
  • dstOffset – 向其复制数据的目标缓冲区中的偏移量(字节)。
  • size – 想要复制的数据的大小(字节)。

我们将数据从临时缓冲区的开头复制到顶点缓冲区的开头,因此,指定两个偏移量为零。基于硬编码顶点数据计算顶点缓冲区的大小,因此,复制相同字节量。为了将数据从一个缓冲区复制到另一个缓冲区,需要调用 vkCmdCopyBuffer()函数。

设置缓冲区内存壁垒

我们已经记录了复制操作,但是这并非全部内容。从现在起,缓冲区不再被用作传输操作的目标,而是用作顶点缓冲区。我们需要通知驱动程序缓冲区内存访问类型将发生变化,从现在起,它将用作顶点属性数据的来源。为了实现这个目标,按照之前在交换链图像中使用的方法设置一个内存壁垒。

首先准备一个类型变量 VkBufferMemoryBarrier,包含以下部分:

  • sType – 标准结构类型,此处设置为 VK_STRUCTURE_TYPE_BUFFER_MEMORY_BARRIER。
  • pNext – 为扩展功能预留的指示器。
  • srcAccessMask – 壁垒前在缓冲区上执行的内存操作类型。
  • dstAccessMask –壁垒后将在特定缓冲区上执行的内存操作。
  • srcQueueFamilyIndex – 之前访问缓冲区的队列家族索引。
  • dstQueueFamilyIndex – 从现在起访问缓冲区的队列家族。
  • buffer – 对其设置壁垒的缓冲区句柄。
  • offset – 缓冲区开始时的内存偏移量(从内存的基本偏移量绑定至缓冲区)。
  • size – 将对其设置壁垒的缓冲区存储区的大小。

如上所示,我们可以针对特定缓冲区内存范围设置壁垒。在本示例中,我们面向整个缓冲区设置壁垒,因此,指定偏移量为 0,大小为 VK_WHOLE_SIZE enum。我们不想在不同队列家族之间转让所有权,因此,在 srcQueueFamilyIndex 和 dstQueueFamilyIndex 中使用 VK_QUEUE_FAMILY_IGNORED enum。

srcAccessMask 和 dstAccessMask 是最重要的参数。我们已经将数据从临时缓冲区复制到顶点缓冲区。在壁垒出现之前,顶点缓冲区被用作传输操作和内存编写的目标。这就是我们将 srcAccessMask 字段设置为 VK_ACCESS_MEMORY_WRITE_BIT 的原因。但是从此以后,壁垒缓冲区只能被用作顶点属性数据的来源。因此,我们将 dstAccessMask 字段指定为 VK_ACCESS_VERTEX_ATTRIBUTE_READ_BIT。

为了设置壁垒,我们调用了一个 vkCmdPipelineBarrier()函数。为了完成命令缓冲区记录,我们调用了 vkEndCommandBuffer()。接下来,我们通过调用 vkQueueSubmit()函数来提交命令缓冲区,以执行所有上述操作。

通常情况下,提交命令缓冲区时,我们应该提供一个栅栏。所有传输操作和整个命令缓冲区完成后,将发送信号。为了简单起见,我们调用 vkDeviceWaitIdle(),并等待特定设备上执行的所有操作完成。完成后,我们成功地将数据传输至设备本地内存,并且使用顶点缓冲区时无须担心性能下降。

Tutorial05 Execution

渲染操作的结果和第 4 部分完全相同:

我们渲染了一个四角颜色分别为红、绿、深灰和蓝色的四边形。四边形将调整大小(和形状),以匹配窗口的大小和形状。

清除

在本部分教程,我重构了清除代码。我们创建了两个缓冲区,每个缓冲区包含单独的内存对象。为了避免代码冗余,我准备了一个缓冲区清除函数:

if( buffer.Handle != VK_NULL_HANDLE ) {
  vkDestroyBuffer( GetDevice(), buffer.Handle, nullptr );
  buffer.Handle = VK_NULL_HANDLE;
}

if( buffer.Memory != VK_NULL_HANDLE ) {
  vkFreeMemory( GetDevice(), buffer.Memory, nullptr );
  buffer.Memory = VK_NULL_HANDLE;
}

8.Tutorial05.cpp, function DestroyBuffer()

该函数检查特定缓冲区是否已成功创建,如果成功创建,它将调用一个 vkDestroyBuffer()函数。它还通过调用 vkFreeMemory()函数释放了与特定缓冲区相关的内存。在析构函数中调用 DestroyBuffer() 函数,释放了与本部分教程相关的其他全部资源:

if( GetDevice() != VK_NULL_HANDLE ) {
  vkDeviceWaitIdle( GetDevice() );

  DestroyBuffer( Vulkan.VertexBuffer );

  DestroyBuffer( Vulkan.StagingBuffer );

  if( Vulkan.GraphicsPipeline != VK_NULL_HANDLE ) {
    vkDestroyPipeline( GetDevice(), Vulkan.GraphicsPipeline, nullptr );
    Vulkan.GraphicsPipeline = VK_NULL_HANDLE;
  }

  if( Vulkan.RenderPass != VK_NULL_HANDLE ) {
    vkDestroyRenderPass( GetDevice(), Vulkan.RenderPass, nullptr );
    Vulkan.RenderPass = VK_NULL_HANDLE;
  }

  for( size_t i = 0; i < Vulkan.RenderingResources.size(); ++i ) {
    if( Vulkan.RenderingResources[i].Framebuffer != VK_NULL_HANDLE ) {
      vkDestroyFramebuffer( GetDevice(), Vulkan.RenderingResources[i].Framebuffer, nullptr );
    }
    if( Vulkan.RenderingResources[i].CommandBuffer != VK_NULL_HANDLE ) {
      vkFreeCommandBuffers( GetDevice(), Vulkan.CommandPool, 1, &Vulkan.RenderingResources[i].CommandBuffer );
    }
    if( Vulkan.RenderingResources[i].ImageAvailableSemaphore != VK_NULL_HANDLE ) {
      vkDestroySemaphore( GetDevice(), Vulkan.RenderingResources[i].ImageAvailableSemaphore, nullptr );
    }
    if( Vulkan.RenderingResources[i].FinishedRenderingSemaphore != VK_NULL_HANDLE ) {
      vkDestroySemaphore( GetDevice(), Vulkan.RenderingResources[i].FinishedRenderingSemaphore, nullptr );
    }
    if( Vulkan.RenderingResources[i].Fence != VK_NULL_HANDLE ) {
      vkDestroyFence( GetDevice(), Vulkan.RenderingResources[i].Fence, nullptr );
    }
  }

  if( Vulkan.CommandPool != VK_NULL_HANDLE ) {
    vkDestroyCommandPool( GetDevice(), Vulkan.CommandPool, nullptr );
    Vulkan.CommandPool = VK_NULL_HANDLE;
  }
}

9.Tutorial05.cpp, destructor

首先等待该设备执行的所有操作完成。接下来,破坏顶点和临时缓冲区。然后,按照与创建时相反的顺序破坏所有其他资源:显卡管线、渲染通道和每个虚拟帧的资源,包含一个帧缓冲器、命令缓冲区、两个旗语、一个栅栏和一个帧缓冲器。最终,我们破坏了一个命令缓冲区从中分配的命令池。

结论

在本教程中,我们使用推荐的技术将数据从应用传输到显卡硬件。该数据使渲染过程中的资源实现了最佳性能,支持将数据从应用映射与复制到临时缓冲区。我们只需准备额外的命令缓冲区记录与提交,以在不同缓冲区之间传输数据。

建议使用临时缓冲区,它的用途不仅限于在不同缓冲区之间复制数据。我们可以使用相同的方法将数据从缓冲区复制到图像。本教程的下一部分将通过展示描述符、描述符集、描述符布局等 Vulkan API 的重要组成部分,介绍如何执行上述操作。

前往:没有任何秘密的 API:Vulkan* 简介第 6 部分

该示例源代码根据“英特尔示例源代码许可协议”发布

使用 CPU 增强《Star Trek™:Bridge Crew》的虚拟现实沉浸感

$
0
0

标题: 使用 CPU 增强《Star Trek™:Bridge Crew》的虚拟现实沉浸感 概览: 在虚拟现实 (VR) 细分市场中,环境交互、增强的物理模拟和摧毁的结合为游戏锦上添花,延长了玩家在游戏中花费的时间,增强了他们对游戏的兴趣。本文将向您逐一介绍《Star Trek™:Bridge Crew*》中实施的所有 CPU 密集型特性,提供用于使用基础系统的指令。 正文:

查看 PDF [2MB]

简介

一般而言,CPU 通常不是影响游戏中震撼的沉浸式场景的主要因素。过去,少数游戏将设置呈现给玩家,允许他们调整 CPU 使用,但是许多开发人员认为针对不同的硬件等级实施基于 CPU 的多层系统有些得不偿失。通过与 Ubisoft 的 Red Storm Entertainment* 工作室的通力合作,《Star Trek™:Bridge Crew*》得到了明显改进,我们的目标是借助本文纠正该误解。在虚拟现实 (VR) 细分市场中,环境交互、增强的物理模拟和摧毁的结合为游戏锦上添花,延长了玩家在游戏中花费的时间,增强了他们对游戏的兴趣。鉴于 Oculus* 所需的低端硬件规格,清除根据全球最低规格定制 CPU 工作的想法变得越来越重要。使用可用的系统资源增强动态性和沉浸感将帮助您创建理想的游戏,同时允许尽可能多的玩家访问,我们进行了前所未有的简化。

本文将向您逐一介绍《Star Trek™:Bridge Crew*》中实施的所有 CPU 密集型特性,提供用于使用基础系统的指令。后面的章节简要介绍了对于每个性能层级,认定 CPU 工作过量的标准。最后一部分展示了如何轻松设置 CPU 性能类别,以自动检测最终用户的硬件等级。

《Star Trek™:Bridge Crew*》通过 Unity* 创建,后者将是本文的重点,但是所有概念也适用其他引擎。

请观看以下视频,查看关于运行这些效果的游戏的详细对比。

《Star Trek™:Bridge Crew*》中的 CPU 密集型特性

舰桥破坏 – 结合了物理粒子、刚体物理和实时全局照明 (GI)

Sparks

概述

USS Aegis 的舰桥是游戏的主要焦点之一。几乎所有游戏环节均要求玩家在舰桥中完成,因此,显然应在舰桥中应用绝大多数 CPU 任务,从而为玩家带来超值的体验。大部分时间均消耗在改进舰桥的各种摧毁顺序,将强度元素添加至场景。例如,舰桥被摧毁后,大块碎片飞出,火光在墙壁和地板上飞溅,火势蔓延,照亮了周围物体,并不是从光源直接照射。

什么原因使它成为 CPU 密集型任务?

破坏舰桥充分使用了 Unity 的实时刚体物理、带有大量小粒子的粒子系统(启用碰撞支持)以及各种火焰和火花效果创建的实时全局照明 (GI) 更新,它们可在可用 CPU 内核上扩展。生成了在破坏事件中使用刚体物理的各种碎片对象,激活高端效果后,粒子数量大幅提高。添加了面向粒子的全局碰撞支持和碰撞基元,内含用于创建火花跳跃和分散行为的详细信息。添加了其他使用 CPU 的 Unity 粒子特性,以增强场景,如使用子发射器为火球或火焰添加拖尾。舰桥破坏粒子在屏幕覆盖尺寸中保持较小的外形,以将 GPU 影响降至最低,同时实现预期的外观。破坏事件发生时,某些光源和放射面将闪烁以模拟电源中断。光源闪烁且舰桥上的火焰激活后,更新 GI。下面,我们将进入每个系统,了解如何单独使用它们。

概述

Unity* 的内置粒子系统组件支持外观和行为的多种变化。恰巧,内置粒子系统在可用的 CPU 内核之间出色扩展。用户只需轻点按钮,便可引发粒子系统碰撞以及与环境响应,如果您需要一个高度自定义的行为,可以编写每个粒子运动的脚本(稍后将详细介绍)。使用以下内置碰撞行为时,底层引擎将工作分配给可用内核,支持最大限度扩展系统。因此,您可以基于可用内核的数量扩展粒子数量,同时还需考虑处理器频率和高速缓存大小。只需转至相关的粒子系统组件,检查碰撞复选框,然后选择所需的设置,便可激活粒子碰撞。

碰撞设置组中包括若干选项。您需要考虑选择与世界碰撞,还是定义一系列平面,使粒子与平面碰撞。第一种设置将产生最真实的模拟,因为场景中几乎所有碰撞器都将运行每个粒子更新计算,但是这必然会导致 CPU 成本增加。游戏通常会定义一系列关键平面,用来模拟周围地形,以尽量减少计算,为其他 CPU 密集型效果预留空间。您的设置选择取决于游戏布局和您想取得的视觉效果。例如,以下命令定义 3 个平面为碰撞器:一层地面和两面墙。

ParticleSystem[] ParticleSystems;
    public Transform[] CollisionPlanes;

    public void Awake()
    {
        ParticleSystems = gameObject.GetComponentsInChildren();
        Debug.Log("Initializing ParticleSystemController");
    }

public void SetCPULevel(CPUCapabilityManager.SYSTEM_LEVELS sysLevel)
    {
        for (int i = 0; i < ParticleSystems.Length; i++)
        {
            var particleSysMain = ParticleSystems[i].main;
            var particleSysCollision = ParticleSystems[i].collision;
            var particleSysEmission = ParticleSystems[i].emission;
            if (sysLevel == CPUCapabilityManager.SYSTEM_LEVELS.HIGH)
            {
                particleSysEmission.rateOverTime = 400.0f;
                particleSysMain.maxParticles = 20000;
                particleSysCollision.enabled = true;
                particleSysCollision.type = ParticleSystemCollisionType.World;
            }
            else if (sysLevel == CPUCapabilityManager.SYSTEM_LEVELS.MEDIUM)
            {
                particleSysEmission.rateOverTime = 300.0f;
                particleSysMain.maxParticles = 10000;
                particleSysCollision.enabled = true;
                particleSysCollision.type = ParticleSystemCollisionType.World;
            }
            else if (sysLevel == CPUCapabilityManager.SYSTEM_LEVELS.LOW)
            {
                    particleSysEmission.rateOverTime = 200.0f;
                    particleSysMain.maxParticles = 5000;
                    particleSysCollision.enabled = true;
                    particleSysCollision.type = ParticleSystemCollisionType.Planes;
                    for (int j = 0; j < CollisionPlanes.Length; j++)
                    {
                        particleSysCollision.SetPlane(j, CollisionPlanes[j]);
                    }
                }
            else if (sysLevel == CPUCapabilityManager.SYSTEM_LEVELS.OFF)
            {
                particleSysEmission.rateOverTime = 100.0f;
                particleSysMain.maxParticles = 3000;
                particleSysCollision.enabled = false;
            }
        }

CPUCapabilityTester示例中查看更多优化版本

实时全局照明 (GI)

概述

为了实现这个效果,请查看照明窗口 (Window > Lighting) 中的 实时照明 复选框 。 (注:实时 GI 的编辑器性能设置在 Unity* 的最新版本中隐藏,并在后台处理。脚本化 更新设置 仍然可用 – 参阅脚本以获取详细信息) 在 Unity* 早期版本中查看 预计算实时 GI 复选框 (同样位于  Window > Lighting中)。 实时 分辨率 和  CPU 使用是严重影响 CPU 利用率的两个设置。

  • 实时 分辨率 确定每个单元应计算多少个纹理像素。 Unity* 发布的一篇教程 详细介绍了如何正确设置该值。根据实用的常规经验,视觉效果丰富的室内场景需要更多的每单元纹理像素,才能实现最逼真的效果。在广阔的室外场景中,间接的 光线 过渡并不明显,支持将 计算 能力用于其他地方。
  • CPU 利用率 确定了引擎中可用于实时 GI 计算的工作线程数量。最好能确定各种系统级别的可用 CPU 性能,  并以此作为设置的依据。对于低端系统,最好设置为低/中;高端 系统 建议设置为高或不限。关于这些设置的描述详见与相应版本一同发运的 Unity* 文档。


Unity* 5.6.1f1 中的设置


旧版 Unity* 中的设置

void Start () {
        SetCPULevel(CPUCapabilityManager.Singleton.CPUCapabilityLevel);
    }

public void SetCPULevel(CPUCapabilityManager.SYSTEM_LEVELS sysLevel)
    {
        if (sysLevel == CPUCapabilityManager.SYSTEM_LEVELS.HIGH)
        {
            DynamicGI.updateThreshold = 0;
        }
        else if (sysLevel == CPUCapabilityManager.SYSTEM_LEVELS.MEDIUM)
        {
            DynamicGI.updateThreshold = 25;
        }
        else if (sysLevel == CPUCapabilityManager.SYSTEM_LEVELS.LOW)
        {
            DynamicGI.updateThreshold = 50;
        }
        else if (sysLevel == CPUCapabilityManager.SYSTEM_LEVELS.OFF)
        {
            DynamicGI.updateThreshold = 100;
        }
        Debug.Log("(" + gameObject.name + ") System capability set to:" + CPUCapabilityManager.Singleton.CPUCapabilityLevel + ", so setting GI update threshold to:" + DynamicGI.updateThreshold);
    }

是否内置于 Unity*?

是的。启用 实时 GI 效果后,应用使用 Unity* 支持的最高 CPU 利用率设置,快速的更新率可实现最佳效果。

概述

实时 GI模拟场景中光线的跳动与间接照亮对象。团队非常希望使用这个特性,因为 Aegis 前面的大窗户支持星体和破坏效果更新舰桥内部。在大型恒星或星云前移动 Aegis 改变了舰桥的外观,反射了入射光线,通过保持场景外观的一致和增强真实感,提供了更出色的沉浸式体验。

什么原因使它成为 CPU 密集型任务?

Unity 的* 实时 GI消耗了大量的 CPU 计算资源,根据要求的逼真度使用部分可用内核。

动态小行星

云尾与太阳闪焰

如何实现

如果玩家的设备足够先进,可以启用 刚体 物理,将场景中的静态模型变为动态模型。通过 script 在已有对象中添加全新 刚体 组件或动态生成预配置对象的预制件,便可在脚本中实现该目标。动态对象和可交互对象在增强游戏(尤其是虚拟现实游戏)的沉浸感中发挥了巨大作用。

public GameObject[] PotentiallyDynamicObjects;
    int NumDynamicObjects = 0;
    void Start () {
        SetCPULevel(CPUCapabilityManager.Singleton.CPUCapabilityLevel);
    }
    public void SetCPULevel(CPUCapabilityManager.SYSTEM_LEVELS sysLevel)
    {
        if (sysLevel == CPUCapabilityManager.SYSTEM_LEVELS.HIGH)
        {
            NumDynamicObjects = PotentiallyDynamicObjects.Length;
        }
        else if (sysLevel == CPUCapabilityManager.SYSTEM_LEVELS.MEDIUM)
        {
            NumDynamicObjects = PotentiallyDynamicObjects.Length / 2;
        }
        else if (sysLevel == CPUCapabilityManager.SYSTEM_LEVELS.LOW)
        {
            NumDynamicObjects = PotentiallyDynamicObjects.Length / 3;
        }
        else if (sysLevel == CPUCapabilityManager.SYSTEM_LEVELS.OFF)
        {
            NumDynamicObjects = 0;
        }

        Debug.Log("(Obj:"+ gameObject.name + ") System capability set to:" + CPUCapabilityManager.Singleton.CPUCapabilityLevel + ", so setting number of dynamic objects to:" + NumDynamicObjects);

        for (int i = 0; i < NumDynamicObjects; i++)
        {

            Rigidbody objRigidBody = PotentiallyDynamicObjects[i].AddComponent<Rigidbody>();
            objRigidBody.useGravity = true;
            objRigidBody.mass = 10;
            PotentiallyDynamicObjects[i].AddComponent<CustomAsteroidMovement>();
        }
    }

是否内置于 Unity*?

动态小行星使用了 Unity* 的 刚体物理 和 粒子系统,但是生成小行星的系统由《Star Trek™:Bridge Crew*》团队编写与定制。查看以下示例,了解如何自行实施类似系统。

概述

当 Aegis 航行至小行星带时,将在玩家的视图体之外生成额外的小行星,并将它们发射到视图。这些小行星与已就位的小行星发生碰撞,并搅起尘埃。

许多游戏地图还包括小行星带生成器,将大量静态小行星分散在圆柱或球形区域。启用高端 CPU 效果后,舰桥移动时,这些区域将在舰桥特定距离以外放置应用刚体物理的动态小行星。用户会产生这样的印象:小行星带内遍布小碎片,它们相互碰撞,并与更大的行星相撞。由于速度已被用于维持物体运动与场景的活跃,产生一个动态行星带的可能性很小。最后,某些小行星与玩家的战舰或其他小行星碰撞后,将分解为更小的碎片,其它小行星将完好无损地反弹。

这些改变将用户的注意力从天空盒移开,使用户感觉自己身处太空;同时不中断游戏体验。

什么原因使它成为 CPU 密集型任务?

借助刚体物理使小行星带内漂浮着大量动态小行星碎片,实例化非池化碎片以及在小行星解体后移动和生成额外碎片都使用了大量 CPU 时间。

概述

云尾制造了敌方战舰和 Aegis 通过太空时搅动尘埃的假象,增强了沉浸感。太阳闪焰通过将玩家目光从天空盒移开,使玩家认为自己身处遥远的太空,实现了相同的效果。

什么原因使它成为 CPU 密集型任务?

云尾与太阳闪焰使用了脚本化的粒子行为,该行为要求使用主线程上的脚本单独更新粒子。从循环数百个粒子到循环数千个粒子以及通过脚本更新属性占用了大量 CPU 时间,但是支持粒子的自定义行为,这是 Unity* 提供的即购即用普通粒子系统属性无法实现的。请记住,目前必须在主线程上完成,以防止系统像前面提到的粒子碰撞系统那样使用过多内核。请继续关注 Unity* 在 Unite Europe 2017大会上提到的全新 C# 工作系统,后者将通过扩展 Unity* API 优化脚本代码中的多线程。

是否内置于 Unity*?

云尾和太阳闪焰使用了 Unity* 的粒子系统,但是 Red Storm Entertainment 通过编辑脚本,确定粒子如何随时间的推移而移动和改变。尾流效应使用一个粒子系统,从战舰的若干发射器点发射粒子拖尾。单个拖尾内粒子的大小和生命周期取决于它的发射器。拖尾粒子在世界空间内发射,但是发射器点一直附着于战舰,以确保在战舰转向与倾斜时也能从正确位置发射。定制粒子行为脚本在战舰后面添加虚拟“吸引器”对象,该对象通过随机振荡拉回附近的粒子,穿过云层时向战舰后面的拖尾引入湍流。太阳闪焰也使用吸引器行为向外散开粒子,或在向外发射粒子后,将其拉回太阳表面。以下简单示例展示了如何使所有粒子飞向世界起源点。

public ParticleSystem MyParticleSystem;
    ParticleSystem.Particle[] MyParticles = new ParticleSystem.Particle[4000];
    public float ParticleSpeed = 10.0f;

	void Update () {
        int numParticles = MyParticleSystem.GetParticles(MyParticles);

        for(int i = 0; i < numParticles; i++)
        {
            MyParticles[i].position = Vector3.MoveTowards(MyParticles[i].position, Vector3.zero, Time.deltaTime * ParticleSpeed);
        }

        MyParticleSystem.SetParticles(MyParticles, numParticles);
	}

摧毁战舰

如何实现

将模型建造为碎片,以各种断点作为间隔。为 Unity* 中包含网格渲染器的每个游戏对象配备一个 刚体 组件。对象被摧毁后,在每个网格上启用 刚体 组件,并将爆炸力应用到所有网格。如欲了解更多详情,请参阅 Unity* 的 刚体文档 。

// Explosion arguments
    public float ExplosiveForce;
    public float ExplosiveRadius;
    public Transform ExplosiveTransform;    // Centerpoint of explosion

    public Rigidbody BaseRigidBody;
    public GameObject[] PotentiallyDetachableCubes;
    List<Rigidbody> ObjRigidbodies = new List<Rigidbody>();
    bool IsCPUCapable = false;
    bool HasExploded = false;
	void Start ()
    {
        SetCPULevel(CPUCapabilityManager.Singleton.CPUCapabilityLevel);
    }

    public void SetCPULevel(CPUCapabilityManager.SYSTEM_LEVELS sysLevel)
    {
        // Only use if CPU deemed medium or high capability
        if (sysLevel == CPUCapabilityManager.SYSTEM_LEVELS.HIGH
            || sysLevel == CPUCapabilityManager.SYSTEM_LEVELS.MEDIUM)
        {
            IsCPUCapable = true;

            // add rigidbodies to all little cubes
            for (int i = 0; i < PotentiallyDetachableCubes.Length; i++)
            {
                Rigidbody CurrRigidbody = PotentiallyDetachableCubes[i].AddComponent<Rigidbody>();
                CurrRigidbody.isKinematic = true;
                CurrRigidbody.useGravity = false;
                ObjRigidbodies.Add(CurrRigidbody);
            }
            Debug.Log("(ExplosionController) System capability set to:" + CPUCapabilityManager.Singleton.CPUCapabilityLevel + ", so object (" + gameObject.name + ") is destructible");
        }
        else
        {

            Debug.Log("(ExplosionController) System capability set to:" + CPUCapabilityManager.Singleton.CPUCapabilityLevel + ", so object (" + gameObject.name + ") not destructible");
        }
    }

    public void ExplodeObject()
    {
        HasExploded = true;
        if (IsCPUCapable)
        {
            BaseRigidBody.useGravity = false;
            BaseRigidBody.isKinematic = true;
            BoxCollider[] BaseColliders = GetComponents<BoxCollider>();
            for(int i = 0; i < BaseColliders.Length; i++)
            {
                BaseColliders[i].enabled = false;
            }
            for (int i = 0; i < ObjRigidbodies.Count; i++)
            {
                Rigidbody CurrRigidbody = ObjRigidbodies[i];
                CurrRigidbody.isKinematic = false;
                CurrRigidbody.useGravity = true;
                CurrRigidbody.AddExplosionForce(ExplosiveForce, ExplosiveTransform.position, ExplosiveRadius);
                ObjRigidbodies[i].gameObject.AddComponent<BoxCollider>();
            }
        }
        else
        {
            // Boring destruction implementation
            BaseRigidBody.AddExplosionForce(ExplosiveForce, ExplosiveTransform.position, ExplosiveRadius);
        }
    }

    void OnCollisionEnter(Collision collision)
    {
        if(!HasExploded)
        {
            ExplosiveTransform.position = collision.contacts[0].point;
            ExplodeObject();
        }
    }

概述

战舰摧毁特性使玩家击败敌人时满足感倍增,从而增加了游戏的刺激性。在游戏中,一般会通过爆炸效果遮挡爆炸对象,以掩饰从场景中删除废弃 游戏对象时使用的 popping 效果。借助高端配置中的 CPU 性能,我们可以将模型分成若干部分,朝不同方向发射,甚至可以添加子摧毁。每个碎片都可以与场景装置相撞,最终消失或停留(如果系统能够处理)。

什么原因使它成为 CPU 密集型任务?

艺术家将战舰分解为包含刚体组件的多个不同部分,初始化各个部分后通过物理力制作动画。启动与其他对象(如小行星或战舰)的碰撞,以确保在动画环境中显示逼真的行为。此外,战舰的每块爆炸碎片都添加了粒子拖尾。

是否内置于 Unity*?

该特性的刚体和物理属性完全内置,使用针对 Unity* 的方法添加爆炸力至战舰碎片。随后制作动画,并借助 Unity* 的刚体物理系统与对象碰撞。Unity* 粒子系统用于发射带有子发射器的粒子,以在碎片后面创建拖尾,但是最高级别的粒子位置由脚本管理,以确保它们依附于爆炸的战舰碎片,无需担心父项的坐标空间。

CPU 性能检测插件

我们已经介绍了《Star Trek™:Bridge Crew*》中添加的所有特性,但是,如何确定目标系统能处理哪些特性?为了尽量减少麻烦,我们创建了一个易于使用的 Unity*插件(包含源代码)。代码附有面向 Unity* 和原生实施的示例代码,用作一个工具盒,为您提供帮助定义目标系统类型的系统指标。上文的许多示例已经集成至样本,方便用户使用。包括以下步骤:

  1. 定义您的 CPU 性能级别。
        public enum SYSTEM_LEVELS
        {
            OFF,
            LOW,
            MEDIUM,
            HIGH,
            NUM_SYSTEMS
        };
  2. 设置 CPU 值阙值。插件提供了各种指标,如逻辑/物理内核数量、最大频率、系统内存等。但是,如果您想考虑其他因素,可以随时添加其他指标。对于多数基本使用,提供的指标应该够用。
            // i5-4590
            LowSettings.NumLogicalCores = 4;
            LowSettings.UsablePhysMemoryGB = 8;
            LowSettings.MaxBaseFrequency = 3.3;
            LowSettings.CacheSizeMB = 6;
    
            // i7 - 7820HK - Set to turbo mode
            MedSettings.NumLogicalCores = 8;
            MedSettings.UsablePhysMemoryGB = 8;
            MedSettings.MaxBaseFrequency = 3.9;
            MedSettings.CacheSizeMB = 8;
    
            // i7-6700k
            HighSettings.NumLogicalCores = 8;
            HighSettings.UsablePhysMemoryGB = 8;
            HighSettings.MaxBaseFrequency = 4.0;
            HighSettings.CacheSizeMB = 8;
    
  3. 初始化插件并确定用户是否运行英特尔® 处理器。
    void QueryCPU()
        {
            InitializeResources();
            if (IsIntelCPU())
            {
                // Your performance categorization code
            }
            else
            {
                Debug.Log("You are not running on an Intel CPU");
            }
        }
  4. 询问目标系统。
    StringBuilder cpuNameBuffer = new StringBuilder(BufferSize);
                GetProcessorName(cpuNameBuffer, ref BufferSize);
                SysLogicalCores = GetNumLogicalCores();
                SysUsablePhysMemoryGB = GetUsablePhysMemoryGB();
                SysMaxBaseFrequency = GetMaxBaseFrequency();
                SysCacheSizeMB = GetCacheSizeMB();
  5. 对比阙值,以确定测试系统属于前面定义的何种性能级别。
        bool IsSystemHigherThanThreshold(SystemThreshold threshold)
        {
            if (threshold.NumLogicalCores < SysLogicalCores && threshold.MaxBaseFrequency < SysMaxBaseFrequency&& threshold.UsablePhysMemoryGB < SysUsablePhysMemoryGB && threshold.CacheSizeMB < SysCacheSizeMB)
            {
                return true;
            }
            return false;
        }
    SYSTEM_LEVELS MySystemLevel = SYSTEM_LEVELS.OFF;
    
    if (IsSystemHigherThanThreshold(HighSettings) || IsWhitelistedCPU(SYSTEM_LEVELS.HIGH))
            {
                MySystemLevel = SYSTEM_LEVELS.HIGH;
            }
            else if (IsSystemHigherThanThreshold(MedSettings) || IsWhitelistedCPU(SYSTEM_LEVELS.MEDIUM))
            {
                MySystemLevel = SYSTEM_LEVELS.MEDIUM;
            }
            else if (IsSystemHigherThanThreshold(LowSettings) || IsWhitelistedCPU(SYSTEM_LEVELS.OFF))
            {
                MySystemLevel = SYSTEM_LEVELS.LOW;
            }
            else
            {
                MySystemLevel = SYSTEM_LEVELS.OFF;
            }
    
            Debug.Log("Your system level has been categorized as:" + MySystemLevel);

性能分析与注意事项

和 GPU 工作相同,我们需要验证特性集的组合 CPU 利用率没有超过目标,以持续提供异步空间扭曲(极力避免 Star Trek™ 的双关语)和二次投影触发器。我们想要确保无论运行于何种设备,游戏的每秒帧数能维持在 90,同时最大限度地使用 CPU。《Star Trek™:Bridge Crew*》团队决定将特性集分为 3 个等级:OffPartialFull。因此,我们在匹配 Off阙值的设备上测试了Full组特性。


GPUView 显示配备 HSW i5-4590 CPU + GTX 1080 GPU 的台式机系统的工作分布

CPU显卡场景配置运行更新周期新帧掉帧生成的合成帧

HSW i5-4590

GTX1080

任务 5,初始弯曲之后

完整设置

1

11861

5993

58

5810

2

11731

6584

56

5091

3

11909

6175

101

5633

 

 

平均值

11833.67

6250.67

71.67

5511.33

生成的合成帧数不为零,表示在低端 CPU 上完整特性集的 CPU 工作超过每帧 11.1 毫秒的阙值

以上 GPUView 截屏显示从 presentpresent耗时约 22 毫秒(突出显示)。Present表示最后帧已生成并且为提交至 头盔显示器 (HMD)做好准备的时间,可以从帧速率(转换为 45 fps)的角度理解。从 90 到 45 fps 意味着我们正不断通过运行于“Off”级系统的配置触发 ASW。查看任务 5 的 3 次测试运行,我们发现 ASW触发器平均生成了约 5500 个合成帧。将这些沉浸式特性集成至 Oculus min-spec 便失去了效果,这和我们预想的一致。我们没有在所有配置中删除特性集,而是将特性集绑定运行时可确定的硬件级别,以激活合适的特性集,为不同硬件级别的玩家提供最佳游戏体验。如果查看运行于高端目标(英特尔® 酷睿 i7-7700K 处理器)的相同配置,会发现不同的结果。


GPUView 显示搭载 KBL i7-7700K CPU + GTX 1080 GPU 的台式机系统上的工作分布

CPU显卡场景配置运行更新周期新帧掉帧生成的合成帧

KBL i7-7700k

GTX1080

任务 5,初始弯曲之后

完整设置

1

11703

11666

37

0

2

11654

11617

37

0

3

11700

11672

28

0

 

 

平均值

11685.67

11651.67

34.00

0.00

生成的合成帧数为零,表示高端 CPU 上完整特性集的 CPU 工作未超过每帧 11.1 毫秒的阙值

借助高端目标提供的额外的逻辑内核、更高的频率和更大的高速缓存,所有工作都将扩展,并能在 11.1 毫秒的规定时间内完成,以达到 90 fps 的帧速率。整个过程中,CPU 工作的平均每帧持续时间在 9-10.3 毫秒之间。这意味着高端目标的性能几乎达到了极限,但是仍维持稳定的 90 fps,并使用了所有的可用资源。我们达到了最佳结果!我们完成了“Off”和“Full”特性集的测试。此时,我们需要选择“Full”特性集中的一个子集,并在基于英特尔酷睿 i7-7700HK 处理器的 VR 就绪型笔记本电脑上启用该子集。这是“Partial”特性集的中间目标。我们希望保留真正影响舰桥内部的特性,因此我们提高这些特性的优先级,然后逐一移除其他特性,直到得到最佳结果。最终,我们只需移除动态尾流效应和动态小行星,便可在笔记本电脑上轻松实现 90 fps 的帧速率。以下 GPUView* 截屏显示了运行于 VR 就绪型测试笔记本电脑的“Partial”特性集。


GPUView 显示搭载 KBL i7-7820HK CPU + GTX 1080 GPU 的虚拟现实游戏笔记本电脑上的工作分布

CPU显卡场景配置运行更新周期新帧掉帧生成的合成帧

KBL i7-7820HK

GTX1080

任务 5,初始弯曲之后

完整设置

1

11887

11242

116

529

2

11881

11315

110

456

3

11792

10912

125

755

 

 

平均值

11853.33

11156.33

117.00

580.00

生成的合成帧数不为零,表示在 VR 就绪型笔记本电脑上完整特性集的 CPU 工作超过每帧 11.1 毫秒的阙值

CPU显卡场景配置运行更新周期新帧掉帧生成的合成帧

KBL i7-7820HK

GTX1080

任务 5,初始弯曲之后

部分设置

1

11882

11844

38

0

2

10171

10146

25

0

3

11971

11933

38

0

 

 

平均值

11341.33

11307.67

33.67

0.00

生成的合成帧数为零,表示在 VR 就绪型笔记本电脑上部分特性集的 CPU 工作未超过每帧 11.1 毫秒的阙值

结论

总体而言,更逼真、更高分辨率的模拟以及更多动态实体的使用是 CPU 利用率上升的主要原因;现在可以在许多 CPU 上启用之前较昂贵的物理模拟。此外,可以使用动画/反向运动学 (IK)、布料模拟、群集、流体模拟、程序生成等其他 CPU 密集型系统生成更丰富、更逼真的世界。行业内已存在面向显卡的设置级别,现在我们应着手考虑为 CPU 设置划分级别。开发游戏时,请思考各个硬件级别上未被充分挖掘的所有计算潜能,考虑如何利用这些潜能使游戏变得与众不同。如欲获取更多信息,请点击以下链接。尽情探索。

  • 特别鸣谢 Kevin Coyle 和 Red Storm Entertainment 团队的其他成员,他们与我们通力合作,协助文章的完成**

其他资源

“发挥震撼的显卡性能:使用 CPU 增强《Star Trek™:Bridge Crew》的虚拟现实沉浸感”

作者在 Unite 2016 大会上展示了文章的信息。

会议描述– 如今,许多游戏和体特别强调 GPU 工作,但是内置于现代主流 CPU 的多核处于闲置状态。此演讲探索了 Ubisoft 的 Red Storm 工作室和英特尔如何通过使用 Unity* 在《Star Trek™:Bridge Crew》中提供最卓越的沉浸感,以充分利用可用资源。了解如何在您的游戏中实现震撼的视觉效果,同时将对 GPU 的性能影响降至最低!

Catlike 编码

Catlike 编码提供众多出色的 CPU/数学密集型教程,任何人可以轻松掌握与运行。本教程以 Unity* 为主,但是由于内容不依赖任何特定的 API,因此适用于所有引擎。强烈推荐对程序生成、曲线/样条使用、网格变形、纹理处理、噪声等感兴趣的用户使用该教程。

面向视频游戏的流体模拟(系列)

本文是一篇编写出色的教程,介绍了如何实施面向视频游戏的多核流体模拟。本文非常适合初学者,涵盖了从概念到实施的所有信息。文章结尾处将提供源代码,读者可以将其添加至引擎,了解如何使用代码模拟各种流体类型。

链接:https://software.intel.com/zh-cn/articles/fluid-simulation-for-video-games-part-1

在Windows上编译tensorflow教程

$
0
0

背景介绍

最简单的tensorflow的安装方法是在pip一键式安装官方预编译好的包 pip install tensorflow 通常这种预编译的包的编译参数选择是为了最大兼容性而不是为了最优性能,导致在使用过程中,每次运行代码都会输出一大堆的warning信息。例如在安装了谷歌官方的tensorflow 1.3.0包后,运行以下测试代码时

Console会输出

表示此tensorflow版本只用到了CPU SSE指令集优化,可以运行在很多老架构的CPU指令集上。为了充分利用AVX/AVX2来加速tensorflow的CPU版本的计算速度,需要自己下载tensorflow的源码,在编译时使用这些指令集。以下教程基于最新的tensorflow 1.3.1源码,利用用Visual Studio 2015/Visual Studio 2017来编译一个基于AVX/AVX2的CPU版本的tensorflow python安装包。

编译过程

准备工作

VS2015编译过程

  • 打开VS2015 64位命令行编译环境
    <img alt="" class=" data-fid=" 604126"="" data-cke-saved-src="https://software.intel.com/sites/default/files/managed/7d/5f/tftow-pic3.png" src="https://software.intel.com/sites/default/files/managed/7d/5f/tftow-pic3.png" typeof="foaf:Image">
  • 在命令行环境中进入tensorflow的源码路径tensorflow-1.3.1\tensorflow\contrib\cmake 新建一个文件夹build2015,进入build2015文件夹
  • 用CMake生成VS2015的编译项目
    C:\...\build2015>cmake .. -A x64 -DCMAKE_BUILD_TYPE=Release
    -DSWIG_EXECUTABLE=C:\work\swigwin-3.0.12\swig.exe
    -DPYTHON_EXECUTABLE="C:\Users\jgu23\AppData\Local\Continuum\Anaconda3\python.exe"
    -DPYTHON_LIBRARIES="C:\Users\jgu23\AppData\Local\Continuum\Anaconda3\libs\python36.lib"
    -Dtensorflow_WIN_CPU_SIMD_OPTIONS=/arch:AVX
  • 用MSBuild编译生成tensorflow的pip安装包 C:\...\build2015>MSBuild /p:Configuration=Release tf_python_build_pip_package.vcxproj 编译时需要联网,git会从网上下载必要的项目包经过漫长的编译过程(我的编译环境是i5 7440hq + 12G内存)
  • pip安装tensorflow pip安装包 如果编译没错误的话,进入到C:\...\build2015\tf_python\dist目录下,可以看到一个文件 tensorflow-1.3.1-cp36-cp36m-win_amd64.whl 这个就是编译出来的tensorflow安装包在进入Anaconda Prompt界面

    进入到C:\...\build2017\tf_python\dist目录下,输入“pip install tensorflow-1.3.1-cp36-cp36m-win_amd64.whl” 到这里,tensorflow就已经编译安装成功了
  • 最后验证一下,运行代码

    Console输出

    说明这个tensorflow包支持AVX指令集
  • 注意
    VS2015编译tensorflow最高只支持到AVX, 如果CMake生成编译项目时指定CPU指令集支持到AVX2,编译时将出现error C3861: 'xxx': identifier not found错误。要支持AVX2指令集,必须用VS2017编译

VS2017编译过程

  • 打开VS2017 64位命令行编译环境
  • 在命令行环境中进入tensorflow的源码路径tensorflow-1.3.1\tensorflow\contrib\cmake 新建一个文件夹build2017,进入build2017文件夹
  • 用CMake生成VS2017的编译项目
    C:\...\build2017>cmake .. -A x64 -DCMAKE_BUILD_TYPE=Release
    -DSWIG_EXECUTABLE=C:\work\swigwin-3.0.12\swig.exe
    -DPYTHON_EXECUTABLE="C:\Users\jgu23\AppData\Local\Continuum\Anaconda3\python.exe"
    -DPYTHON_LIBRARIES="C:\Users\jgu23\AppData\Local\Continuum\Anaconda3\libs\python36.lib"
    -Dtensorflow_WIN_CPU_SIMD_OPTIONS=/arch:AVX2
  • 这里比VS2015的编译多了一步
    用VS2017打开tf_core_kernels.vcxproj

    进入Properties->Configuration Properties->VC++ Directories->Executable Directories

    将Executable Directories下的$(VC_ExecutablePath_x64)改为$(VC_ExecutablePath_x64_x64)

    保存项目,退出
  • 用MSBuild编译生成tensorflow的pip安装包C:\...\build2017>MSBuild /p:Configuration=Release tf_python_build_pip_package.vcxproj编译时需要联网,git会从网上下载必要的项目包,经过漫长的编译过程(我的编译环境是i5 7440hq + 12G内存)
  • pip安装tensorflow 安装包
    如果编译没错误的话,进入到C:\...\build2017\tf_python\dist目录下,可以看到一个文件tensorflow-1.3.1-cp36-cp36m-win_amd64.whl,这个就是tensorflow的安装包,在进入Anaconda Prompt界面

    进入到C:\...\build2017\tf_python\dist目录下,输入“pip install tensorflow-1.3.1-cp36-cp36m-win_amd64.whl” 到这里,tensorflow就已经编译安装成功了
  • 最后验证一下,运行代码

    Console输出

    没有任何warning输出,说明这个tensorflow包支持AVX2指令集

常见错误及解决方法

  • 编译re2.vcxproj时出错
    warning C4819: The file contains a character that cannot be represented in the current code page (936). Save the file in Unicode format to prevent data loss
    以及 Error C2001: Newline in constant
    错误原因:此错误和编译平台的windows操作系统的locale设为中文有关,英文的windows没有这个错误
    解决办法: 修改CMakeCache.txt,让MSBuild编译这个项目时,强制按照utf-8编码文件的格式来解析文件,进入C:\...\tensorflow-1.3.1\tensorflow\contrib\cmake\build2017\re2\src\re2目录,用文本编辑器打开CMakeCache.txt,找到以下2行

    在CMAKE_CXX_FLAGS_RELEASE这里添加红色部分,修改为 CMAKE_CXX_FLAGS_RELEASE:STRING=/MD /O2 /Ob2 /DNDEBUG /source-charset:utf-8
  • 编译tf_core_kernels.vcxproj时出错 fatal error c1060: the compiler is out of heap space
    以及 fatal error C1002: compiler is out of heap space in pass 2
    错误原因:VS编译环境默认的编译工具链为32位,tensorflow编译时会消耗大量的内存,导致编译器消耗的内存超出了32位编译器的寻址范围。
    解决方法:
    1. VS2015
      需要运行64位命令行编译环境,在“开始”菜单中选择运行“VS2015 x64 Native Tools Command Prompt”,如本文2.2章中“打开VS2015 64位命令行编译环境”部分所示,或者在命令行里手工切换,首先进入VS2015的安装目录 “cd C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC”,输入“vcvarsall amd64”,参考MSDN “How to: Enable a 64-Bit Visual C++ Toolset on the Command Line”,https://msdn.microsoft.com/en-us/library/x4d2c09s.aspx
    2. VS2017
      可能因为tensorflow项目中CMake脚本对VS2017支持不好,所以无法按照VS2015的修改办法来通过指定64位编译环境的方法来解决这个问题。我们需要用VS2017打开tf_core_kernels.vcxproj,手工将Properties->Configuration Properties->VC++ Directories->Executable Directories下的$(VC_ExecutablePath_x64)改为$(VC_ExecutablePath_x64_x64)
      如本文2.3章中“VS2017打开tf_core_kernels.vcxproj”部分所示

结论

通过手工编译tensorflow源码来支持AVX/AVX2指令,可以消除运行tensorflow程序时烦人的warning提示,并且可以获得比官方预编译版本更快的tensorflow学习/推理速度,节省了tensorflow开发者的时间。

在英特尔® 架构上训练和使用 TensorFlow* 模型

$
0
0

简介

TensorFlow* 是一款领先的深度学习和机器学习框架。截至 2017 年 5 月,该框架集成了面向英特尔® 至强® 处理器和英特尔® 至强融核™ 处理器的多项优化。本文是教程系列的第三部分,旨在提供相关信息帮助开发人员通过 GitHub* 存储库中可用的资源构建、安装和探索使用在英特尔® 架构上优化的 TensorFlow*。

本系列的第一篇教程在英特尔® 架构上构建和安装 TensorFlow*展示了如何通过 GitHub* 存储库中的可用资源构建和安装在英特尔® 架构上优化的 TensorFlow。

本系列的第二篇教程在英特尔® 架构上构建和安装 TensorFlow Serving介绍了如何构建和安装 TensorFlow Serving — 专为生产环境设计的高性能机器学习服务系统。

在本教程中,我们将训练和保存 TensorFlow 模型,构建 TensorFlow 模型服务器,并通过客户端应用测试该服务器。本教程基于 TensorFlow 网站提供的教程 面向 ML 新手的 MNIST 服务 TensorFlow 模型。建议大家在完全了解如何训练和保存 TensorFlow 模型之间,查看这些教程。

训练和保存 MNIST 模型

根据维基百科介绍,MNIST (Modified National Institute of Standards and Technology) 数据库包含 60,000 张训练图像和 10,000 张测试图像,用于机器学习领域的训练与测试。因为 MNIST 数据库相对比较简单,因此经常被用作演示机器学习框架的基础数据集。

首先打开终端,使用以下命令:

cd ~/serving
bazel build //tensorflow_serving/example:mnist_saved_model
rm -rf /tmp/mnist_model
bazel-bin/tensorflow_serving/example/mnist_saved_model /tmp/mnist_model

故障排除:编写时,TensorFlow Serving 存储库识别了一个记录为 “NotFoundError in mnist_export example #421” 的错误。如果使用上述命令后遇到该错误,可尝试以下解决方法:

  1. 打开 serving bazel-bin/tensorflow_serving/example/mnist_saved_model.runfiles/ org_tensorflow/tensorflow/contrib/image/__init__.py
  2. 添加注释 (#),如下所示:
    #from tensorflow.contrib.image.python.ops.single_image_random_dot_stereograms import single_image_random_dot_stereograms
  3. 保存并关闭 __init__.py
  4. 尝试重新发布命令:
    bazel-bin/tensorflow_serving/example/mnist_saved_model /tmp/mnist_model

由于运行 mnist_saved_model时我们省略了 training_iterations 和 model_version command-line 参数,因此它们分别默认为 1000 和 1。因为我们为导出目录传递了 /tmp/mnist_model,因此训练的模型保存在 /tmp/mnist_model/1中。

根据 TensorFlow 教程文档中的解释,“1” 版子目录包含以下文件:

  • saved_model.pb为序列化 tensorflow::SavedModel。它包含关于该模型的一个或多个图表定义,以及模型的元数据,比如签名。
  • variables是保留序列化图表变量的文件。

故障排除:在部分实例中可能会遇到脚本运行下载的培训文件被破坏的问题。 该错误在 GitHub 上定义为 "Not a gzipped file #170"。如有必要,可通过 /tmp目录中的以下命令,手动下载这些文件:

wget http://yann.lecun.com/exdb/mnist/train-images-idx3-ubyte.gz
wget http://yann.lecun.com/exdb/mnist/train-labels-idx1-ubyte.gz
wget http://yann.lecun.com/exdb/mnist/t10k-images-idx3-ubyte.gz
wget http://yann.lecun.com/exdb/mnist/t10k-labels-idx1-ubyte.gz

构建和启动 TensorFlow 模型服务器

使用以下命令,构建 TensorFlow 模型服务器:

bazel build //tensorflow_serving/model_servers:tensorflow_model_server

使用以下命令,启动 TensorFlow 模型服务器:

bazel-bin/tensorflow_serving/model_servers/tensorflow_model_server --port=9000 --model_name=mnist --model_base_path=/tmp/mnist_model/ &

测试 TensorFlow 模型服务器

最后一条命令开始在终端中运行 ModelServer。为了使用安装 TensorFlow Serving 中的 mnist_client实用程序测试服务器,输入 /serving目录中的以下命令:

bazel build //tensorflow_serving/example:mnist_client
bazel-bin/tensorflow_serving/example/mnist_client --num_tests=1000 --server=localhost:9000

如果一切运行良好,将出现如下图 1 所示的结果。

Screenshot of a command prompt window with client test results

图 1.TensorFlow 客户端测试结果。

故障排除:GitHub 上有一个定义为 “gRPC doesn't respect the no_proxy environment variable” 的错误,会导致客户端应用运行时出现 “Endpoint read failed” 错误。使用 env 命令查看是否设置了 http_proxy环境变量。如果设置了,可通过以下命令暂时不设置:

unset http_proxy

总结

在本系列教程中,我们介绍了如何构建 TensorFlow 机器学习框架和 TensorFlow Serving — 专为生产环境设计的高性能机器学习服务系统。训练和保存基于 MNIST 数据集的简单模型,然后使用 TensorFlow 模型服务器部署该模型。最后,使用 GitHub 存储库中的 mnist_client示例演示客户端应用如何运用 TensorFlow 模型服务器执行机器学习参考。

如欲了解更多有关该主题的信息,请访问TensorFlow 网站,它是了解该框架的主要资料来源。文章 TensorFlow 基于现代英特尔® 架构的优化介绍了具体的图形优化、性能试验,以及借助 CPU 优化构建和安装 TensorFlow 的详细信息。

使用英特尔持久性内存编程简介

$
0
0

简介

多年以来,计算机应用在两层之间组织数据:内存和存储。新一代英特尔持久性内存基于英特尔和美光* 共同开发的突破性 3D XPoint™ 介质,推出了第 3 层。学习该技术,了解如何将它提供给应用以及持久性内存支持为何获得了如此积极的反响。

持久性内存技术支持开发兼具存储和内存属性的产品。产品具有存储的持久性,意味着它们将在重启后保留内容,它们可以像内存一样按字节寻址,意味着程序可以就地访问数据结构。

英特尔持久性内存技术能够脱颖而出的重要原因在于它的速度非常快,处理器可以直接访问,节省了传统存储停下来处理数据块 I/O 所需的时间。

优化系统互联

持久性内存令人激动不已的主要原因在于相比现有的存储设备,它能提供更高的性能。基于 NAND 的现代固态盘插入 PCIe* 总线,并使用 NVM Express* 协议通信,对比之下您会发现,它读取数据块的耗时为 80 微秒以上。下表的蓝色区域显示了多数时间用于访问介质。软件堆栈仅占总体访问时间的一小部分,我们可以尝试加快驱动程序的速度,但是不会取得明显的效果。

image of map
图 1: 应用延迟比较

英特尔® 傲腾™ 固态盘也可以使用 3D XPoint 技术插入 PCIe 总线,从而大幅减少了访问介质的时间,软件堆栈和 PCIe 协议的开销成为总体延迟的重要组成部分。为了充分利用 3D XPoint 技术,必须立即着手处理软件和互联的开销问题。此时,持久性内存隆重登场。

通过将介质连接至内存总线,CPU 可以直接访问数据,不需要任何驱动程序或 PCIe 开销。由于在 64 字节高速缓存行内访问内存,因此,CPU 只读取必要部分,不会模仿存储,将每次访问归拢为一个数据块大小。在图 1 中,您可以看到 64 字节读取的延迟非常低。

借助持久性内存,应用在内存和存储层基础上新增了一个数据放置层,持久性内存层的容量超过了 DRAM,相比存储,大幅提升了速度。应用能像使用传统内存一样,就地访问持久性内存驻留数据结构,无需在内存和存储之间反复传输数据块。

为了实现低延迟的直接访问,我们需要一个支持应用连接各种持久性内存的软件架构。

非易失性内存 (NVM) 编程模型

本文显示了高级别存储堆栈。构成堆栈的这些基本模块在几十年中没有太大变化。应用使用标准文件 API 打开文件系统内的文件,文件系统通过驱动程序或一系列驱动程序处理必要的数据块 I/O。所有存储访问均在数据块内实施,通常借助 PCIe 等互联。

image of map
图 2:存储堆栈

从操作系统的角度来看,基本文件 API(如打开/关闭以及读/写)支持已经存在了几十年。使用高级语言编写应用的开发人员可以借助提供更加方便的 API 的库进行编程。这些库最终将内部调用基本 API。

内存映射

Windows* 和 Linux* 均支持内存映射文件,这一特性在很久之前便已推出,但是并不常用。在持久性内存中,面向内存映射文件的 API 非常实用;实际上,它们是存储网络工业协会 (SNIA) 发布的持续性内存编程模型的核心。

打开文件后,内存才能映射文件,因此,应用调用 Windows 上的 CreateFileMappingMapViewOfFile或 Linux 上的 mmap 之前,许可检查已经发生。

image of map
图 3:内存映射文件

调用产生后,文件出现在应用的地址空间内,支持对文件内容的加载/存储访问。存储指令引起的变化刷新至存储后,才能具有持久性,这是内存映射文件的一个重要方面。在 Windows 上,该步骤可借助 FlushViewOfFileFlushFileBuffers完成;在 Linux 上,使用 msync 或 fsync。

在这种情况下,内存映射文件 API 的功能为持久性内存编程提供了极大优势。

持久性编程模型支持字节级访问插入内存总线的非易失性介质,此处通过常用的行业术语 NVDIMM表示,它的全称是非易失性双列直插式内存模块。映射设置完成后,您会发现应用具有 MMU 虚拟到物理映射提供的直接访问。将这些直接映射配置到持久性内存的能力被称作直接访问 (DAX) 特性。普通文件系统和持久性内存感知型文件系统的不同之处在于对该特性的支持。DAX 目前受 Windows 和 Linux 支持。

image of map
图 4:持久性内存编程模型

NVM 库 (NVML) 简介

NVML 是一套开源库,适用于 Linux 和 Windows。如欲获取关于这些库的更多信息,请访问持久性内存编程网站 pmem.io。这些库通过高级别语言支持推进持久性内存编程的采用。目前,C 和 C++ 支持已经过全面验证,并在 Linux 上交付,并作为超前开发版本部署于 Windows。

flowchart
图 5: NVM 库

结论

持久性内存是一项颠覆性技术,本文描述的编程模型提供了访问这项新兴技术的方法。NVM 库提供交易操作支持,以保持数据的一致性和耐用性。该领域仍有许多未解之谜,将继续取得众多激动人心的进展。

如欲了解更多信息,请查看持久性内存编程视频系列。

基于Intel酷睿平台的Linux版本Tensor Flow 1.4性能优化

$
0
0

概要

为了兼容更多的老的硬件平台,谷歌预编译的TensorFlow二进制安装包(通过pip install tensorflow命令安装)中,是没有开启SSE4.x,SVX,FMA,MKL等的支持的,因此,对于目前主流的酷睿平台来说,这个安装包远无法发挥酷睿平台强大的计算能力,而在机器学习中,模型的训练往往会牵扯到非常耗时的巨大的计算量,并且我们在工作中可能需要随时调整我们的模型,以期获得更好的训练效果。在这个前提下,性能的优化显得尤为重要。本文将以Ubuntu 16.04为开发平台,展示如何在酷睿平台上对TensorFlow 1.4进行性能的优化,并给出一些真实的测试数据来展示优化的效果。

搭建TensorFlow 1.4开发环境

首先,我们用git命令clone TensorFlow 1.4的源码到本地

$ git clone https://github.com/tensorflow/tensorflow
$ git checkout r1.4

第二个步骤是安装bazel(TensorFlow的编译工具),这里我们采用谷歌推荐的安装方法,好处是我们可以方便的用Ubuntu的apt命令随时更新bazel的版本

$ sudo apt-get install openjdk-8-jdk
$ echo "deb [arch=amd64] http://storage.googleapis.com/bazel-apt stable jdk1.8" | sudo tee /etc/apt/sources.list.d/bazel.list
$ curl https://bazel.build/bazel-release.pub.gpg | sudo apt-key add –
$ sudo apt-get update && sudo apt-get install bazel	

最后我们需要安装一些python工具,对于python 2.7版本(如果不确定当前python版本号,可以运行python –version来确认),我们需要:

$ sudo apt-get install python-numpy python-dev python-pip python-wheel

对于python 3.x版本,则是:

$ sudo apt-get install python3-numpy python3-dev python3-pip python3-wheel

针对Intel酷睿平台的性能优化

针对酷睿平台的性能优化主要包含两个方面,其一是加入SSE4.x/AVX/AVX2/FMA等高级指令集的编译选项,其二是充分利用MKL加速库。为了达到这个目的,我们需要用以下的编译命令(而不是tensorflow官网上默认的命令):

$ bazel build --config=opt --config=mkl //tensorflow/tools/pip_package:build_pip_package

其中“--config=opt”的选项会自动启用可用的高级指令优化(例如SSE4.x/AVX/AVX2/FMA等,前提是你的设备支持这样的指令), “--config=mkl”选项会下载MKL库(目前MKL为2018版本,如果还没有下载到本地),并对TensorFlow源码用MKL优化编译。

编译的具体步骤可以参考如下命令行输出:

dec@dec-yoga:~/sources/tensorflow$ bazel clean # 清除上一次编译时的配置选项
WARNING: ignoring http_proxy in environment.
..........
INFO: Starting clean (this may take a while). Consider using --async if the clean takes more than several minutes.
dec@dec-yoga:~/sources/tensorflow$ ./configure # 进行编译配置
WARNING: ignoring http_proxy in environment.
You have bazel 0.7.0 installed.
Please specify the location of python. [Default is /usr/bin/python]:


Found possible Python library paths:
  /usr/local/lib/python2.7/dist-packages
  /usr/lib/python2.7/dist-packages
Please input the desired Python library path to use.  Default is [/usr/local/lib/python2.7/dist-packages]

Do you wish to build TensorFlow with jemalloc as malloc support? [Y/n]:
jemalloc as malloc support will be enabled for TensorFlow.

Do you wish to build TensorFlow with Google Cloud Platform support? [Y/n]: n
No Google Cloud Platform support will be enabled for TensorFlow.

Do you wish to build TensorFlow with Hadoop File System support? [Y/n]: n
No Hadoop File System support will be enabled for TensorFlow.

Do you wish to build TensorFlow with Amazon S3 File System support? [Y/n]: n
No Amazon S3 File System support will be enabled for TensorFlow.

Do you wish to build TensorFlow with XLA JIT support? [y/N]:
No XLA JIT support will be enabled for TensorFlow.

Do you wish to build TensorFlow with GDR support? [y/N]:
No GDR support will be enabled for TensorFlow.

Do you wish to build TensorFlow with VERBS support? [y/N]:
No VERBS support will be enabled for TensorFlow.

Do you wish to build TensorFlow with OpenCL support? [y/N]:
No OpenCL support will be enabled for TensorFlow.

Do you wish to build TensorFlow with CUDA support? [y/N]:
No CUDA support will be enabled for TensorFlow.

Do you wish to build TensorFlow with MPI support? [y/N]:
No MPI support will be enabled for TensorFlow.

Please specify optimization flags to use during compilation when bazel option "--config=opt" is specified [Default is -march=native]:


Add "--config=mkl" to your bazel command to build with MKL support.
Please note that MKL on MacOS or windows is still not supported.
If you would like to use a local MKL instead of downloading, please set the environment variable "TF_MKL_ROOT" every time before build.
Configuration finished
dec@dec-yoga:~/sources/tensorflow$ bazel build --config=opt --config=mkl //tensorflow/tools/pip_package:build_pip_package # 针对Intel酷睿平台进行优化编译
	

编译完成之后,就可以生成安装包并且安装

$ bazel-bin/tensorflow/tools/pip_package/build_pip_package /tmp/tensorflow_pkg
$ sudo pip install /tmp/tensorflow_pkg/[生成的安装包文件名].whl

优化前后的性能对比

这里我们采用TensorFlow自带的MNIST示例代码来做对比。为了方便比较,我们给代码传入这样的参数:

--learning_rate=0.01 --max_steps=2000 --batch_size=5000 --hidden1=128 --hidden2=32

并且对代码稍加修改,在main函数最开始加上如下的MKL设置语句(实际上这些设置也可以通过命令行修改Ubuntu系统环境变量来做)。需要注意的是“OMP_NUM_THREAD”这一项设置,一般来说这一项设置通常设为当前硬件平台的核心数,有时候设置为核心数的若干倍时性能最佳。

os.environ["KMP_BLOCKTIME"] = '0'
os.environ["KMP_SETTINGS"] = '1'
os.environ["KMP_AFFINITY"]= 'granularity=fine,verbose,compact,1,0'
os.environ["OMP_NUM_THREADS"]= '8'

然后再在run_training()调用语句前后加上时间戳,并且打印出run_training()所消耗的时间:

start = datetime.utcnow()
print(start)
run_training()
end = datetime.utcnow()
print(end)
print("Time elapsed:", end-start)

最后我们安装/编译不同版本的TensorFlow并且运行我们修改过的MNIST示例代码,来得出我们的测试结果,在i5-7200U的笔记本上,我们得出的测试结果是:

TensorFlow版本官方版本
(直接在Ubuntu shell中pip install tensorflow)
高级指令优化版本
(bazel build--config=opt
//tensorflow/tools/pip_package:
build_pip_package)
高级指令+MKL优化版本
(bazel build--config=opt--config=mkl
//tensorflow/tools/pip_package:
build_pip_package)
run_training()消耗时间(S)0:02:41.2718740:01:09.5906950:01:05.408182
(当 OMP_NUM_THREAD设为8时)

在i7-6700的台式机上,我们得出的测试结果是:

TensorFlow版本官方版本
(直接在Ubuntu shell中pip install tensorflow)
高级指令优化版本
(bazel build--config=opt
//tensorflow/tools/pip_package:
build_pip_package)
高级指令+MKL优化版本
(bazel build--config=opt--config=mkl
//tensorflow/tools/pip_package:
build_pip_package)
run_training()消耗时间(S)0:01:14.3690930:00:38.6573580:00:34.540627
(当OMP_NUM_THREAD设为8时)

总结

在英特尔酷睿平台上,通过针对性的优化编译TensorFlow(SSE4.x,AVX, AVX2,FMA,MKL等),相比谷歌官方的默认编译版本,我们可以获得巨大的性能提升。在i5-7200U平台上,加入高级指令的优化,示例代码性能可以提高一倍以上,加入MKL优化之后还可以获得5%以上额外的性能提升。而在i7-6700平台上,MKL可以更多的发挥酷睿平台多核性能,带来高达10%以上的性能提升。

参考文献

Installing TensorFlow: https://www.tensorflow.org/install/

TensorFlow* Optimizations on Modern Intel® Architecture: https://software.intel.com/en-us/articles/tensorflow-optimizations-on-modern-intel-architecture

关于作者

裴凡江是英特尔软件与服务事业部的一名应用软件工程师,专注于在英特尔平台上与开发者的合作和业务拓展。力求将英特尔卓越的软硬件平台与开发者的软硬件产品完美结合,提供最优客户体验。

如何在英特尔® 架构服务器上仿真持久性内存

$
0
0

简介

本教程将介绍如何在采用 4.3 或更高版本的 Linux* 内核的英特尔® 处理器上使用常规动态随即访问内存 (DRAM) 设置持久性内存仿真。本文介绍硬件配置以及如何设置软件。你可以按照本文中的步骤,在pmem.io中尝试 NVM 库中的 PMEM 编程示例。

为什么使用仿真?

如果你是一名软件开发人员,想尽快开始开发软件或准备 PEME 感知型应用,可以在 PMEM 硬件面市之前在开发过程中使用这种仿真方法。

什么是持久性内存?

传统应用在内存层和存储层之间组织数据。新兴 PMEM 技术引进了第三层。它可以像易失性内存那样,使用处理器加载和保存指令进行访问,但会像存储那样在断电期间保留内容。由于仿真使用的是 DRAM,所以重启期间无法保留数据。

硬件和系统要求

持久性内存仿真以 DRAM 内存为基础,该内存以持久性内存区的形式呈现给操作系统 (OS)。由于它是基于 DRAM 的仿真,因此仿真速度极快,但关机时会丢失所有数据。本教程使用以下硬件:

CPU 和芯片组

英特尔® 至强® 处理器 E5-2699 v4 处理器,2.2 GHz

  • 每块芯片的内核数量:22(仅使用一个内核)
  • 插槽数量:2
  • 芯片组:英特尔® C610 芯片组,QS(B-1 步)
  • 系统总线:9.6 GT/秒英特尔® 快速通道互联技术

平台

平台:英特尔® 服务器系统 R2000WT 产品家族(代号“Wildcat Pass”)

  • BIOS:GRRFSDP1.86B.0271.R00.1510301446 ME:V03.01.03.0018.0 BMC:1.33.8932
  • DIMM 插槽:24
  • 电源:1x1100W

内存

内存大小:256 GB (16X16 GB) DDR4 2133P

品牌/型号:Micron* – MTA36ASF2G72PZ2GATESIG

存储

品牌和型号:1 TB Western Digital* (WD1002FAEX)

操作系统

CentOS* 7.2 with kernel 4.5.3

表 1 - 用于 PMEM 仿真的系统配置。

Linux* 内核

本教程撰写期间使用 Linux 内核 4.5.3。自版本 4.0 起的内核支持持久性内存设备和仿真,但为了简化配置,建议使用高于 4.2 的内核。仿真应与能够处理官方内核的所有 Linux 分发版搭配使用。如欲配置合适的驱动程序安装,可运行 make nconfig ,并启用驱动程序。按照以下说明,图 1 - 图 5 显示了 Kernel Configuration 菜单中关于 NVDIMM Support 的正确设置。

$ make nconfig

        -> Device Drivers -> NVDIMM Support -><M>PMEM; <M>BLK; <*>BTT

Set up the device drivers.
图 1:设置设备驱动程序。

Set up the NVDIMM device.
图 2:设置 NVDIMM 设备。

Setup the file system for Direct Access support.
图 3:设置支持 Direct Access 的文件系统。

Setting for Direct Access support.
图 4:设置 Direct Access (DAX) 支持。

Property of the NVDIMM support.
图 5:NVDIMM Support 属性。

该内核将为 PMEM 驱动程序提供这些区域,以便用于持久性存储。图 6 和图 7 显示了 Kernel Configuration 菜单中关于处理器类型和特性的正确设置。

$ make nconfig

        -> Processor type and features<*>Support non-standard NVDIMMs and ADR protected memory

图 4 和图 5 显示了 Kernel Configuration 菜单中的选择。

Set up the processor to support NVDIMM.
图 6:设置处理器支持 NVDIMM。

Enable the NON-standard NVDIMMs and ADR protected memory.
图 7:启用非标准 NVDIMM 和 ADR 保护内存。

构建您的内核

现在你可以使用以下指令构建内核。

$ make -jX

        其中 X 是设备上的内核数

在构建新内核过程中,以并行方式编译新内核将获得性能优势。我们开展的从一个线程到多个线程的实验表明,编译速度比单个线程快 95%。加上使用多线程编译节省的时间,整个新内核的设置速度将会更加。图 8 和图 9 显示编译不同数量的内核时所达到的利用率和性能提升。

Compiling the kernel sources.
图 8:编译内核源。

Performance gain for compiling the source in parallel.
图 9:并行编译源代码所获得的性能优势。

安装内核

# make modules_install install

Installing the kernel.
图 10:安装内核。

通过修改内核命令行参数,保留一个内存区,以便以持久性内存位置的形式呈现给 OS。从 ss 到 ss+nn 是待使用的内存区。[KMG] 指 kilo、mega、giga。

memmap=nn[KMG]!ss[KMG]

例如,memmap=4G!12G 指保留第12 和第 16 GB 之间的 4GB 每次买。配置在 GRUB 中完成,配置可能因 Linux 分发版的不同而有所差异。下面是两个 GRUB 配置示例。

CentOS 7.0 下的 GRUB 配置

# vi /etc/default/grub
GRUB_CMDLINE_LINUX="memmap=nn[KMG]!ss[KMG]"
On BIOS-based machines:
# grub2-mkconfig -o /boot/grub2/grub.cfg

图 11 显示在 GRUB 文件中增加的 PMEM 声明。图 12 显示执行 GRUB 配置的指令。

Define PMEM regions in the /etc/default/grub file.
图 11:在 /etc/default/grub 文件中定义 PMEM 区。

Generate the boot configuration file bases on the grub template.
图 12:根据 grub 模板生成启动配置文件。

设备重启后,应该能够看到仿真设备呈现 /dev/pmem0…pmem3。尝试获取保留的内存区支持持久性内存仿真,定义持久性 (type 12) 区域的各个内存范围将如图 13 所示。通常建议使用 4GB+ 范围 (memmap=nnG!4G) 的内存,或查找该范围之前或以内的 e820 内存映射。如果没有看到设备,请验证 grub 文件中的 memmap 是否如图 9 所示设置正确,然后按照图 13 所示进行 dmesg(1) 分析。应该可以看到 dmesg 输出快照:dmesg,显示保留的范围。

Persistent memory regions are highlighted as (type 12).
图 13:持久性内存区突出显示为 (type 12)。

你将看到有多个未重叠的区域保留为持久性内存。放置多个 memmap="...!..."条目将导致内核以 /dev/pmem0、/dev/pmem1、/dev/pmem2… 的形式呈现多台设备。

DAX - Direct Access 扩展

DAX(直连)扩展至文件系统可创建 PMEM 感知型环境。部分 distro(例如 Fedora* 24 或更高版本)默认内置了 DAX/PMEM,并提供 NVML。查找内核中通常由 /boot 下方的 distro 提供的配置文件,能够快速查看内核中是否内置了 DAX 和 PMEM。使用以下命令:

# egrep ‘(DAX|PMEM)’ /boot/config-`uname –r`

结果如下所示:

CONFIG_X86_PMEM_LEGACY_DEVICE=y
CONFIG_X86_PMEM_LEGACY=y
CONFIG_BLK_DEV_RAM_DAX=y
CONFIG_BLK_DEV_PMEM=m
CONFIG_FS_DAX=y
CONFIG_FS_DAX_PMD=y
CONFIG_ARCH_HAS_PMEM_API=y

如欲安装包含 DAX(目前 ext4 和 xfs 提供)的文件系统:

# mkdir /mnt/pmemdir
# mkfs.ext4 /dev/pmem3
# mount -o dax /dev/pmem3 /mnt/pmemdir
现在可以在新加载的分区上创建文件,并作为输入提供给 NVML 池。

Persistent memory blocks.
图 14:持久性内存块。

Making a file system.
图 15:制作文件系统。

另外值得一提的是,大家可以通过 ramdisk (i.e., /dev/shm) 仿真持久性内存,或通过设置环境变量 PMEM_IS_PMEM_FORCE=1 强制执行类似 PMEM 的行为。这样可以消除由 msync(2) 造成的性能损失。

结论

现在,大家知道了如何设置在没有实际 PMEM 硬件的情况下构建 PMEM 应用的环境。大家可以借助英特尔® 架构服务器上的其他内核,快速为仿真环境构建支持 PMEM 的新内核。

参考资料

持久性内存编程

作者

Thai Le 是英特尔公司的一名软件工程师,负责云计算和性能计算分析工作。


2017 第十届“英特尔杯”全国大学生软件创新大赛获奖作品

$
0
0

 

第十届"英特尔杯"全国大学生软件创新大赛总决赛合影(点击查看大图

获奖作品展示

•  第十届英特尔杯全国大学生软件创新大赛 — 特等奖(1名)

译motion      同时荣获最具创业潜力奖

团队:Quadra_L      学校:中南大学

译motion是一款专为聋哑人开发,解决其面对面交流障碍的单端辅助app。
Emotion is a single ended auxiliary tool developed for deaf mutes and helps them communicate with blind people or ordinary people.

译motion-项目开发文档

 

•  第十届英特尔杯全国大学生软件创新大赛 — 一等奖(3名)

AIAD      同时荣获最具创业潜力奖

团队:Myvie小组      学校:厦门大学

AIAD 智能广告设计师是一款基于深度学习的手机端产品广告片制作软件,主要是针对有商品推广需求的个体商户,帮助他们打造属于自己的个性化广告片。
AIAD is a mobile phone application for advertising film making, which bases on the Intel Caffe Deep Learning Framework.The main users of AIAD are the businesses who need to promote their products.

AIAD-项目开发文档

 

CareMore儿童伤害预警系统      同时荣获最具创业潜力奖

团队 :TimeMachine      学校:中南大学

CareMore是一款基于云计算,搭载于智能可穿戴设备,应用深度学习技术并结合心率进行分析的儿童险情预警软件系统。
CareMore is a cloud computing based on smart wearable devices, the application of deep learning technology combined with heart rate analysis of the child danger warning software system. 

CareMore儿童伤害预警系统-项目开发文档

 

MoodFeeler

团队 :JoTang      学校:电子科技大学

MoodFeeler心理辅导决策系统通过对心理从业者自身建立的社交网络进行数据挖掘、文本分析、关键字提取以及情感分析等,将抽象的情感信息有机整合,实现情感类别筛选、重点用户监测和消极情绪预警等功能。
MoodFeeler is to solve the problems of domestic psychological practitioners in real time to pay attention to the changes of patients' emotion.

MoodFeeler-项目开发文档

 

•  第十届英特尔杯全国大学生软件创新大赛 — 二等奖(6名)

Visual Music      同时荣获最具创业潜力奖

团队 :AxOne      学校:中南大学

我们致力于开创DIY音乐的模式:其中,由词谱曲功能给每个普通人一个平台能够创作属于“自己的歌”。不需要任何的专业作曲知识,只需输入歌词,简单、方便、有趣、有意义。
We've been working to start the music DIY model: composing a piece of music by words creates a platform for normal people to write music by themselves.

Visual Music-项目开发文档

 

SophoVeillance

团队 :DeeMop      学校:武汉大学

SophoVeillance是一个可定制化的基于深度学习和云计算的智能监控系统。它基于计算机视觉领域的前沿研究成果,具备强大的捕捉、识别、跟踪功能。
SophoVeillance is an intelligent video surveillance system which could be customized for customer based on deep learning and cloud computing. 

SophoVeillance-项目开发文档

 

Smart Monitoring

团队 :SkySee     学校:东北大学

针对高速公路隧道内灯光昏暗,路面反光,车灯闪烁的复杂环境,采用基于深度学习的多目标检测追踪算法,对车辆和行人进行识别与追踪,并监控抛洒物,火灾预警。
Monitor the throwing and fire warnings in a complex environment with dim light, road reflection and flashing lights in expressway tunnels.

Smart Monitoring-项目开发文档

 

DeepJudge

团队 :Hinton      学校:山东大学

为了避免传统裁判打分不公正等问题,我们希望利用深度学习来为跳水赛事自动打分。Deep Judge的效果明显高于接受过一定时间训练的人类,打分精准。
In order to avoid the unfair scoring of traditional referees, we hope to use deep learning to score the diving contests automatically.

DeepJudge-项目开发文档

 

emoAudience

团队 :烧烤火锅烤鱼      学校:上海交通大学

本项目旨在为演讲者提供专业分析和科学建议,并为追踪听众接受效果提供工具与平台。用户只需要上传想要分析的资源,项目将使用基于深度学习的识别功能及表情学,心理学知识做出较为准确、及时地反馈,帮助演讲者有针对性地提升自我。
The project aims to provide professional analysis and scientific advice to the speakers, providing the tool and platform for tracking the audiences’ acceptance.

emoAudience-项目开发文档

 

TTStar幼儿教育机器人

团队 :iRobot     学校:哈尔滨工业大学

这是一款提供全新学习模式的幼教机器人,旨在让家长放下繁重工作中对孩子的担忧,让孩子在与机器人的互动中,学到知识,养成爱学习的习惯。
This is an early childhood education robot with a new learning method, designed to allow busy parents to lay down the children's concerns. 

TTStar幼儿教育机器人-项目开发文档

 

•  第十届英特尔杯全国大学生软件创新大赛 — 三等奖(10名)

DeepFitness      同时荣获最具极客人气奖

团队 :Caffe_Drinker      学校:山东大学

健身缺乏专业性人员指导,锻炼效果差。Deep Fitness可以实现实时识别纠正用户健身动作;随手一拍识别饮食,给出用户的健康分析建议。
You can achieve real-time pose identification and correct the user's fitness action.Through our project can take pictures to identify the food, and will give health analysis and recommendations to users.

DeepFitness-项目开发文档

 

诗韵

团队 :GLT     学校:四川大学

诗韵(Rhyme)是一款以艺术创作为核心、深度学习为亮点的图片分享APP。为用户的图片写诗并分享,同时也提供了图片意境识别、智能摄影助手、图片后期风格迁移等功能。
Rhyme is an artistic creation as the core, deep learning as highlights, photo sharing APP.

诗韵-项目开发文档

 

以手写心

团队 :AMTC_VIT     学校:湖北师范大学

本团队对手语进行了研究,将每个词建立一个数据库,然后利用摄像头获取手势动作,在服务器通过计算分析得到翻译文本。帮助扩大聋哑人的社交圈。
SophoVeillance is an intelligent video surveillance system which could be customized for customer based on deep learning and cloud computing. 

以手写心-项目开发文档

 

基于图像和声音的智能垃圾箱

团队 :saver      学校:山东大学

每年,中国有1.5亿吨的垃圾产生,基于环保、公益以及对新的商业模式的思考,我们提出了智能垃圾箱—saver的想法。
Based on environmental protection, public welfare and thinking about the new business model,We put forward the idea of intelligent trash cans—saver.

基于图像和声音的职能垃圾箱-项目开发文档

 

“Skin Care”-基于深度学习的肌肤健康管理平台

团队 :Devastator      学校:北京邮电大学

我们希望通过深度学习技术,使普通消费者可以方便准确地了解自身的皮肤状况,并进行科学有效地护理,获得一站式全面高效的护肤体验。
The ordinary consumers can easily and accurately understand their skin conditions, scientifically and effectively care their own skins and get a one-stop comprehensive and efficient skin care experience.

“Skin Care”-基于深度学习的肌肤健康管理平台-项目开发文档

 

Protectooth

团队 :SmartDentist      学校:四川大学

本项目搭建智能化口腔视诊平台,使孩子能够通过设备,进行简单的拍摄分析,了解自己的日常口腔状况。
We aim at building a platform for remote oral inspection so that the children can have some insight into their daily oral condition by using our equipment provided for simple analysis after shooting. 

Protectooth-项目开发文档

 

AgriPot

团队 :神一样的α      学校:北京邮电大学

本项目提供了智能识别植株状况并给出专业建议的功能,在作物生长的各个阶段根据用户所上传上来的作物的图片分析这种作物属于生长的哪一阶段,从而用数据告诉用户在这个阶段应该怎么种植这种作物。
This project functions at identifying stats of plants smartly and giving professional advice.

AgriPot-项目开发文档

 

基于深度学习的医疗辅助诊断系统

团队 :Dr_AI      学校:天津大学

本系统旨在运用深度学习的知识建立可靠的医学辅助诊断模型,对大量的影像数据进行有监督训练,完成对未标注的医疗影像的判读与初步诊断,并在多端对数据进行分析及可视化展示。
The system aims to use the knowledge of deep learning to establish a reliable medical diagnostic model, which will be used for training a large numbers of image data supervised.

基于深度学习的医疗辅助诊断系统-项目开发文档

 

Croprotector农业病虫害检测助手

团队 :NOVA      学校:北京邮电大学

Croprotector病虫害检测助手是一个基于深度学习的无人机和机器人检测农作物病虫害的软件。它的目的是解决现代农业大规模种植条件下不易检测农作物病虫害的问题。
Croprotector pest detection assistant is a software based on deep learning of unmanned aerial vehicles and robots to detect crop diseases and insect pests.

Croprotector农业病虫害检测助手-项目开发文档

 

基于深度学习的中医病症辅助诊断系统

团队 :Wall_E      学校:北京邮电大学

该系统为医生提供诊断辅助,为普通群众提供较为全面、可信的健康评估和个性化的科学的疗养方案,同时提供分享交流功能。
The system provides diagnostic assistance for physicians, provides a comprehensive, credible health assessment and personalized scientific convalescence program for the general population, while providing sharing functions.

基于深度学习的中医病症辅助诊断系统-项目开发文档

 

人工智能人脸识别技术在直播平台客户端的应用和优化

$
0
0

作者:Quan Yin

介绍

DLPAT (Deep Learning People Analytic Technology)是在英特尔平台上使用了深度学习技术的计算机视觉SDK。DLPAT可以应用于一 些中国直播行业的软件中,为在这些平台上现场表演的主播提供增强的视觉效果,如增加脸部的美颜,增加实时虚拟换装等。

为了提高人脸识别的准确性,我们采集了大量来自互联网和软件合作伙伴的面部图片,并为了进行深度学习训练对这些数据进行标记。每幅图像的68个面部特征点由工具自动标记后由人工进行微调。 累计标记了一万张面部图片,并使用这些数据加上包含了数百万标记的图片为原始数据来训练深度学习模型。 然后通过深度学习技术对模型进行改进,提取面部标志点特征点,提高精度。

在使用新的标记数据集训练深度学习模型之前,由动态检测的面部特征点有明显的抖动,这是客户所不能接受的。 经过优化后,脸部提取的特征点更加稳定。 抖动的问题得到了改善,能够运用在实际工程中,得到了客户良好的反馈。

性能比较

  • DLib – 使用传统的视觉算法
  • DLPAT – 使用深度学习方法

表格中的数据是基于Intel Core i7 i7-6770H在Windows 10平台上进行的测试

  1. 标准化的平均对齐误差/眼球间的距离分别低于5%, 10% 或 15%的时候:通过标准化的平均对齐误差/眼球间的距离来判断多少误差被控制在5%(10%,15%)。 这是一种通常的统计学方法
  2. 平均绝对百分比误差:MAPE

从这个表格中,我们可以做如下总结:

  • 使用深度学习技术的DLPAT比使用传统视觉算法的DLib在校正平均对齐误差方面具有更好的性能
  • DLPAT中发生的平均误差次数少于DLib中的次数

基于升级后的DLPAT SDK, 独立软件开发商可以整合到其产品以提供以下功能:

测试程序

  • 开启landmark.exe。这个应用程序将开启两个窗口显示优化后的结果和原来的结果。这样可以动态地比较出优化后的效果
  • 从优化的解决方案来看,可以发现动态检测到的面部特征点的位置比原来使用计算机视觉解决方案更稳定。

点击图片查看大图

  • 打开标记工具网站,标记的信息将被显示在网页上

点击图片查看大图

附加信息

DLPAT 架构

DLPAT 特性

DLPAT基于英特尔 IA平台提供了以下功能:

  • 基于人脸的个人信息识别
  • 基于人脸的年龄识别
  • 基于人脸的性别识别 基于人脸的种族识别

总结

DLPAT利用人工智能技术为客户提供了在英特尔客户端平台上的人脸识别解决方案。通过增加使用标记工具标记的人脸数据,提高了对现实场景人脸识别的准确度, 从而帮助客户解决了问题。

使用 BigDL 创建基于图像相似性的房屋推荐

$
0
0

概述

本文将介绍 MLSListings*和英特尔在 Microsoft Azure* 上使用 BigDL1创建的基于图像的房屋推荐系统。使用英特尔BigDL 分布式深度学习框架,该推荐系统经过精心设计,支持高效地索引和查询数百万张房屋图像,从而显著提高房屋购买体验。用户可选择喜欢的房屋照片,让系统推荐用户可能感兴趣的带有类似图像特征的照片列表。系统设计目标为:

  • 根据标题图像的特征和相似性推荐房屋。大多数图像显示房屋正面外部结构,而其他图像可显示该房屋的代表性特征。
  • 低延迟 API,支持在线查询(< 0.1 秒)。

背景

MLSListings Inc. 是加州北部致力于提供房地产资源的上市服务 (MLS) 机构,他们与英特尔和微软合作,将人工智能 (AI) 集成至在线地产平台,以更好地为客户服务。通过集成房地产标准机构 (RESO) API 和英特尔面向 Apache Spark* 的 BigDL 开源深度学习库,这些技术组合在一起,可使用视觉图像显著改进房屋购买搜索过程。这一项目将为高级分析应用创新在房地产行业中的应用铺平道路。

通过图像的相似性对其进行排名,可帮助解决计算机视觉领域中的大量问题。例如,电子零售商向客户展示与过去购买的商品相似的产品,提高线上销售量。几乎每个行业都将其视作颠覆性变革,包括房地产行业,因为过去十年该行业开始越来越多地使用数字技术。在寻找合适房地产的过程中,超过 90% 的购房者选择上网搜索2。房主和房地产专业人士提供房屋特征信息,比如地理位置,大小、年代,以及许多关于房屋内外部结构的照片,以供房地产列表搜索。然而,由于技术上的限制,照片中的大量信息无法提取和编入索引,从而无法增强搜索或服务房地产列表结果。事实上,为我显示类似房屋 是用户进行查询时的最大心愿。现在,利用图像数据库来增强网络和移动数字体验,通过改善搜索相关性来提高用户满意度已成为现实。

作为一种快速崛起的分布式深度学习框架,BigDL 可为大型数据社区提供简单、集成的深层学习功能。BigDL 支持各种深度学习应用,开发人员可通过 BigDL 将深度学习应用编写为标准的 Spark 程序,这些程序可以直接在现有的 Apache Spark 或 Apache Hadoop 集群上运行。

图像相似性概述

在学术领域中,图像相似性可以指语义相似性或视觉相似性。语义相似性指两个图像都包含同一类别的对象。例如,平房和传统住房属于同一类别(都是房子),但看起来可能完全不同。另一方面,视觉相似性并不注重对象类别,而是从视觉角度来衡量图像的外观;例如,公寓和传统住房在外形上可能非常相似。

语义相似性:

视觉相似性:

对于语义相似性来说,通常指的是图像分类问题,使用常见的图像感知模型,比如 GoogLeNet*3或 VGG*4来有效地解决这一问题。

对于视觉相似性来说,纵观历史,有许多技术可应用于这一领域:

  • SIFT、SURF、颜色直方图5
    传统的特征描述符可用来比较图像的相似性。SIFT 特征描述符在均匀缩放、方向和光照改变过程中始终保持不变,适用于譬如在较大的图像中查找小图像的应用。
  • pHash6
    这一数学算法可对图像内容进行分析,并使用 64 位数字指纹来表示其内容。如果两张图像的内容特征类似,那么它们的 pHash 值也会比较接近
  • 基于卷积神经网络 (convnet) 的图像特征(embedding)8
    通过卷积神经网络生成图像特征编码;通常为卷积和池化后的第一个线性层。
  • 孪生网络或Deep Ranking8
    它是一种比较深入的深度学习解决方案,但结果模型很大程度上取决于训练数据,可能会失去通用性。

基于 BigDL 的解决方案

为了基于图像相似性推荐房屋,我们首先将所选查询图像与候选房屋列表的标题图像进行一一比较,接下来生成每个候选房屋的相似度得分并选取排名选择最顶端的结果。通过与领域专家合作,我们开发出了以下计算房屋图像相似度的衡量方法。

    For each image in the candidates, compare with query image {

        class score:Both house front?(Binary Classification)

        tag score:Compare important semantic tags.(Multinomial Classification)

        visual score:Visually similarity score, higher is better

        final Score = class score (decisive)   //~1
                    + tag score (significant)  //~0.3
                    + visual score             //[0,1]
   }

该项目同时使用了语义相似性和视觉相似性。BigDL 提供多项功能支持训练或使用图像相似性模型,包括:

  • 基于 Apache Spark 和 OpenCV* 提供实用的特征提取和转换,以支持在 Spark 上并行预处理图像。
  • 支持 Spark ML* Estimator/Transformer 接口,方便用户在 Spark ML Pipeline内执行深度学习训练和预测。
  • 提供方便的模型调优支持,以及支持模型fine tuning的灵活编程界面。
  • 用户可将预训练的 Caffe*、Torch* 或 TensorFlow* 模型加载到 BigDL 之中,以支持调优或预测。

语义相似性模型

就语义相似性来说,该项目要求使用三种图像分类模型。

模型 1.图像分类:确定是否为房屋正面图片。我们需要区分标题图像是否为房屋的正面。该模型可通过 Places* 数据集上的预训练 GoogLeNet v1 进行调优 (https://github.com/CSAILVision/places365)。我们在训练过程中使用 Places 数据集。

以下代码支持在 BigDL 中借助 DLClassifier* 进行模型训练。我们加载 Places 数据集预训练而成的 Caffe 模型,从Caffe 模型定义中删除了最后两层(线性 (1024 -> 365 和 Softmax)。然后添加了一个包含 classNum 的新线性层,以帮助训练我们所需的分类模型。

模型 2.图像分类:房屋风格(现代,农场,传统,西班牙)。和模型 1 相同,该模型可通过 Places 数据集上训练好的 GoogLeNet v1 模型进行调优。我们使用MLS Listings拥有版权的房屋图片进行训练。

模型 3.图像分类:房屋风格(单层,两层,三层或高层)。和模型 1 相同,该模型可通过 Places 数据集上的预训练 GoogLeNet v1 进行调优。

视觉相似性模型

我们需要计算视觉相似性来获得排名得分。

每次查询时,用户输入图像,与数千张候选图像进行对比,在 0.1 秒内范围前 1,000 个结果。为了满足延迟要求,我们直接对比预先计算好的图片特征。

我们首先创建评估数据集,选择最佳选项来计算图像相似性。在评估数据集中,每个记录包含 3 张图像。

三联图像(查询图像,正面图像,反面图像),其中正面图像更加类似于查询

if (similarity(query image, positive image) > similarity(query image, negative image))
correct += 1
 else
incorrect += 1

图像。

在上述的四种图像相似性计算方法中,孪生图像或深度排名似乎比较精确,但由于缺乏训练数据支持有意义的模型,因此不使用于本项目。在评估数据集的帮助下,我们尝试采用剩余的三种方法,SIFT 和 pHash 所生成的结果准确性较低。我们怀疑是因为这两种方法都无法代表房地产图像的本质特征。

利用在 Places 数据集上的训练的深度学习模型提取图像特征编码(embedding),可达到预期的精度和准确性水平:

网络

特征

精度

Deepbit*

1024 二进制输出

80%

GoogLeNet*

1024 浮点数

84%

VGG-16

25088 浮点数

93%

Similarity (m1, m2) = cosine (embedding (m1), embedding (m2)).

完成 L2 归一化后,可以非常高效地计算出余弦相似性。虽然 VGG-16 嵌入有明显的优势,但我们也尝试使用从评估数据集训练的 SVM 模型为每个嵌入特征分配不同的权重,但这种方法所实现的改进有限,而且我们担心 SVM 模型的通用型不足以覆盖真实世界的图像。

基于图像相似性的房屋推荐

完整的数据流和系统体系结构如下所示:

Image of data flow and system architecture

在生产过程中,该项目可以分成三部分:

  1. 模型训练(离线)
    模型训练主要是指语义模型(在数据集上调优的 GoogLeNet v1),并查找相应的特征以支持视觉相似度计算。可能定期进行再训练,这取决于模型性能或要求的变化。
  2. 图像预测(在线)
    借助第一步训练的语义模型 (GoogLeNet v1) 和预训练的 VGG-16,我们可以将图像转换为标记和特征,并将结果保存在键值高速缓存中。(还可使用 Apache HBase* 或 SQL*)。

    Image conversion map and flow

    所有现有图像和新图像都需要经过上述处理并转换为表格结构,如下所示:

    预测过程定期进行(例如一天一次),或者通过房地产列表条目上传新图像触发推理过程。每张生产图像只需要经历一次预测过程。借助索引图像标注和相似性特征,可在高并发环境中支持实现快速查询性能。
  3. 用于查询的 API(在线)
    住宅推荐系统为向上游用户显示服务 API。每次查询都将查询图像和候选图像作为参数发送。上表显示索引图像信息后,我们可快速完成一至多项查询。关于余弦相似性的处理过程非常高效,而且可以扩展。

演示

我们提供从在线网站上找来的两个示例:

示例 1

Images of houses listings online

示例 2

Images of houses listings online

总结

本文介绍了如何在 Microsoft Azure 上集成英特尔 BigDL 深度学习库,从而帮助MLSListings构建基于图像分析的房屋推荐系统。 我们通过基于caffe模型进行fine tuning生成了三个图片分类模型,以从房地产图像中提取重要的语义标记。我们进一步对比了不同的视觉相似度计算方法,发现通过 VGG 提取的图像特征最为有效。作为端到端行业示例,我们演示了如何利用基于 BigDL 的深度学习,以将更多图像识别创新应用到房地产行业之中。

参考资料

  1. Intel-Analytics/BigDL, https://github.com/intel-analytics/BigDL.
  2. Vision-Based Real Estate Price Estimation, https://arxiv.org/pdf/1707.05489.pdf.
  3. C. Szegedy, W. Liu, Y. Jia, P. Sermanet, S. E. Reed, D. Anguelov, D. Erhan, V. Vanhoucke, and A. Rabinovich, Going Deeper with Convolutions.CoRR, vol. abs/1409.4842, 2014, http://arxiv.org/abs/1409.4842.
  4. Simonyan K, Zisserman A. Very Deep Convolutional Networks for Large-Scale Image Recognition.In:ICLR.2014. p. 1a€“14. arXiv:arXiv:1409.1556v6.
  5. Histogram of Oriented Gradients, https://en.wikipedia.org/wiki/Histogram_of_oriented_gradients.
  6. pHash, The Open Source Perceptual Hash Library, https://www.phash.org/.
  7. Convolutional Neural Networks (CNNs / ConvNets), http://cs231n.github.io/convolutional-networks/.
  8. J. Wang.Learning Fine-Grained Image Similarity with Deep Ranking.https://research.google.com/pubs/archive/42945.pdf.

如何将针对Intel硬件平台优化编译的TensorFlow和Caffe部署到Movidius NCS SDK中

$
0
0

概要

目前Movidius NCS SDK (版本号1.10.01)同时支持Caffe和TensorFlow两种机器学习框架。在Ubuntu 16.04版本的SDK安装过程中,SDK安装程序默认会下载官方的Caffe和TensorFlow,这样确保SDK安装完成之后,示例代码一定够顺利跑起来,对新手开发者非常友好。然而对于有经验的开发者,他们的机器中往往已经存在针对本机优化编译过的TensorFlow/Caffe,比如加入了AVX/MKL/OpenCL等优化的TensorFlow,这比Google开源的CPU编译版本性能有巨大的提升。比如针对Intel多核CPU专门优化的Intel Caffe,或者是针对Intel GEN GPU优化过的clCaffe,在对应的硬件平台上也会比官方默认的Caffe版本有巨大的性能提升。本文将给出一些方法,指导读者如何将针对Intel硬件平台优化编译的TensorFlow和Caffe部署到Movidius NCS SDK中。

Movidius NCS SDK的安装过程解析

为了方便新手快速上手,NCS SDK安装过程设计的非常简明,整个安装过程严格来说只有两个实质性的命令:

1. git clone https://github.com/movidius/ncsdk.git // 复制SDK安装工具到本地,需要注意是这个命令克隆到本地的只是SDK的安装工具,并不是SDK本身,真正的SDK将在执行第2个命令的时候被下载并安装;

2. make install // 执行SDK安装工具,将SDK下载并安装到本地,这个过程包括了一些配置的工作,SDK二进制/库文件的下载,以及Caffe/TensorFlow深度学习框架的部署等;

在Caffe/Tensorflow深度学习框架部署这方面,SDK安装工具配备了一个专门的配置文件(文件名为ncsdk.conf,文件位于安装工具源码的根目录),以下是文件的内容:

从配置文件内容中我们可以看出,NCS SDK默认会下载并安装官方的Caffe(INTEL_CAFFE=no),并且默认会下载并安装TensorFlow(INSTALL_TENSORFLOW=yes)。换言之,NCS SDK并不会优先集成本地已经存在的Caffe/TensorFlow。

为了深入了解NCS SDK部署Caffe/TensorFlow的细节,我们分析了SDK安装工具的源码(git仓库根目录的install.sh),从最开头的一段代码中,我们可以看到真正的SDK的下载链接藏在AWS中(http://ncs-forum-uploads.s3.amazonaws.com/ncsdk/ncsdk_01_10_01/ncsdk_redirector.txt),

点击图片看大图

基于这个信息,我们下载并解压了真正的SDK,然后得到了其中的内容:

最后我们结合了SDK安装工具的install.sh 以及 SDK中的install-ncssdk.sh,总结出了SDK安装的大致过程如下:

a) 下载真正的SDK并解压;

b) 将用户的配置文件(就是前文提到的SDK安装工具中的ncsdk.conf)替换掉SDK中的配置文件;

c) 运行SDK根目录的install-ncssdk.sh – 这个脚本做了大部分最重要的工作,包括下载安装Linux依赖库,下载安装python以及诸多python依赖库,下载并安装/编译深度学习的两个框架(Caffe/TensorFlow),安装Movidius私有的库/二进制等;

d) 清理安装过程的临时文件;

对于我们最关心的Caffe/TensorFlow的部署,我们发现具体的逻辑是这样的(详细的代码请参考SDK安装工具的install.sh 以及 SDK中的install-ncssdk.sh):

1. 对于TensorFlow,SDK安装时首先检查ncsdk.conf中相关配置,如果配置下载TensorFlow(默认),则下载并安装Google官方的TensorFlow 1.3 CPU 编译版本;

2. 对于Caffe,安装程序首先会检查当前是否已经有可以被NCS SDK使用的Caffe,判断的主要项目是:

a. 是否在python中可以import Caffe模块(是否可以成功运行如下python代码):

python3 -c "import caffe" 2> /dev/null

b. 在SDK安装目录(默认是/opt/movidius)必须存在名为“caffe”的超链接,并且指向同目录的文件夹bvlc-caffe(这就是SDK认为“合法”的Caffe);

如果当前没有可以被NCS SDK使用的Caffe(结合分析,如果本机已经存在针对Intel平台优化的Caffe版本,是不可能被NCS SDK认为“可用”的),则视ncsdk.conf中的配置项(INTEL_CAFFE)下载并编译对应的Caffe发行版;

怎样让NCS SDK优先集成本地的Caffe/TensorFlow

基于上面的分析,我们不难发现,要想部署针对Intel平台优化编译的TensorFlow到NCS SDK,在安装SDK之前,我们需要事先安装优化编译的TensorFlow 1.3版本到本机。这里需要强调一点,NCS SDK目前尚不支持最新的TensorFlow 1.4,因此我们必须安装1.3版本(可以通过pip list package命令来查看当前安装的TensorFlow版本)。然后我们将ncsdk.conf配置文件中的“INSTALL_TENSORFLOW”值改成“no”。这样我们在安装NCS SDK的时候就不会下载并覆盖掉我们安装的TensorFlow版本了。

而对于Caffe,为了实现我们的目的,并且不改动SDK的安装脚本,我们可以在安装NCS SDK之前做这些工作:

1. 手动创建安装目录(默认是/opt/movidius);

2. 将我们优化编译的Caffe版本拷贝到/opt/movidius/下并改名成“bvlc-caffe”(谨慎使用mv命令,因为卸载SDK的时候会清空/opt/movidius目录)

3. 在/opt/movidius目录创建名为“caffe”的链接指向“bvlc-caffe” ln -sf bvlc-caffe caffe

4. 确保PYTHONPATH环境变量包含我们的Caffe(可以在bash.rc中做如下配置)

export PYTHONPATH=$PYTHONPATH:/[其他父目录]/caffe/python

参考文献

NCS SDK快速上手:https://developer.movidius.com/start

clCaffe:https://github.com/01org/caffe/wiki/clCaffe

Intel Caffe:https://software.intel.com/en-us/ai-academy/frameworks/caffe

TensorFlow:https://www.tensorflow.org/install/

英特尔平台优化编译TensorFlow:https://software.intel.com/zh-cn/articles/performance-optimization-for-tensorflow-1-dot-4-linux-version-based-on-intel-core-platform

关于作者

裴凡江是英特尔软件与服务事业部的一名应用软件工程师,专注于在英特尔平台上与开发者的合作和业务拓展。力求将英特尔卓越的软硬件平台与开发者的软硬件产品完美结合,提供最优客户体验。

Caffe学习笔记 第一部分 - Windows*下BVLC Caffe的安装与配置

$
0
0

作者:Gu, Jianjun

Tensorflow和Caffe是机器学习初学者常用的2种深度学习框架。相对于Tensorflow有简单的python pip一键安装包,Caffe的安装更考验开发者的计算机编译水平,需要自己下载源码编译。尽管caffe的配置教程网上很多,但是网上caffe的各个分支版本也很多,所以初学者在编译时总是会碰到各种奇怪的问题。本文会针对初学者常用的Windows* + Intel CPU的平台上安装配置Caffe做一个较为完整的总结。

Caffe的各个版本简介

官方版本:伯克利BVLC(Berkeley Vision And Learning Center) 版

https://github.com/BVLC/caffe
这个是Caffe的主版本,由伯克利大学维护。其他所有的Caffe版本都是由这个版本分支出去的。这个版本Caffe可以编译出Linux和Windows*版本,支持Nvidia的CUDA加速,但是对CPU的优化不好,而且有关安装和配置的文档很少。从这个版本编译Caffe,需要很强的自我学习和解决问题能力。

微软的Windows*版本

https://github.com/Microsoft/caffe
微软自己维护的一个版本。这个版本简化了Caffe在Windows*下的步骤,自带了一个VS的Solution项目。这个版本同样支持基于CPU和CUDA的算法实现。

Intel Caffe优化版

https://github.com/intel/caffe
Intel维护的版本,这个版本优化了Intel CPU的实现算法,同时提供对Intel Xeon CPU多核和多节点的支持。这个版本里有些算法库对Windows*支持不太好,所以这个分支编译出的Caffe主要还是运行在Linux的服务器平台上做训练工作。

Intel clCaffe 核显OpenCL优化版

https://github.com/01org/caffe
Intel维护的另一个版本,相对于官网BVLC Caffe上的OpenCL分支,优化了基于Intel 核显GPU的OpenCL加速算法。这个版本同时支持Windows* 和Linux平台,适合在有Intel核显的CPU上做一些推理(Inference)场景的工作。

BVLC caffe的编译

安装编译所需要的软件

配置Windows*的环境变量

为了避免等会CMAKE生成编译脚本的时候找不到一些依赖关系,有的没的路径都加一些,包括Cmake, Git, Ananconda以及Python的路径。

Checkout 官网Caffe的代码

运行以下代码,用git从官网caffe上下载Windows*分支。

git clone https://github.com/BVLC/caffe.git
cd caffe
git checkout windows

修改caffe源码代码里的一些编译参数

  • 修改scripts\build_win.cmd
    因为我们没有定义APPVEYOR,所以直接拉到else(大约69行)以后。
    先从APPVEYOR部分把这部分内容拷贝过来
if !PYTHON_VERSION! EQU 2 (
    set CONDA_ROOT=C:\Miniconda-x64
)
:: Set python 3.5 with conda as the default python
if !PYTHON_VERSION! EQU 3 (
    set CONDA_ROOT=C:\Miniconda35-x64
)
set PATH=!CONDA_ROOT!;!CONDA_ROOT!\Scripts;!CONDA_ROOT!\Library\bin;!PATH!

然后根据Windows*环境设置下图中红色方框的部分

1. 编译器是VS2015,设置MSVC_VERSION=14

2. 不需要NINJA来编译,设置WITH_NINJA=0

3. 没有NVDIA GPU,设置CPU_ONLY=1

4. Python版本为3.X,设置PYTHON_VERSION=3

5. 需要pyCaffe支持,设置BUILD_PYTHON=1

  • 如果Python版本不是2.7或3.5,修改
    cmake\WindowsDownloadPrebuiltDependencies.cmake
    Caffe在编译时会自动从网上下载一个依赖库caffe-builder,这个cmake文件负责根据你系统里的VS版本和python版本自动下载预编译好的caffe-builder包。目前网上只有预编译好的VS2013/2015配python2.7/3.5的caffe-builder,如果是python3.6,编译时会报找不到VS2015配python3.6的caffe-builder包。解决方法有2个办法,要么用python3.5的包,要么自己编译caffe-builder。

    对于复用Python3.5的包,可以按照下图中所示,复制DEPENDENCIES_URL_1900_35和DEPENDENCIES_SHA_1900_35 2行,同时修改35为36。

如果希望使用自己编译VS2015加python3.6的caffe-builder,除了要加入上段说到的2行修改外,还需要注释掉下图中WindowsDownloadPrebuiltDependencies.cmake的下图所示的绿色部分,这部分代码是负责从网上下载依赖包并且解压缩到本地目录的。同时要把caffe-builder编译出的libraries目录拷贝到C:\Users\[user name]\.caffe\dependencies这个目录下。(具体caffe-builder的编译,将在本文第3节详细介绍)

开始编译

在caffe的目录下输入scripts\build_win.cmd

开始编译,一切顺利的话,大约10分钟后就编译好了

验证一下编译的结果

接下来运行一下caffe项目自带的examples里的00-classification的代码来验证一下caffe是否能够正常运行

打开anaconda的命令行,进入caffe的examples目录,运行jupyter notebook

在打开的notebook中打开caffe自带的例子 00-calssification.ipynb

这是用一个训练好的Caffe模型来预测动物图片的例子,图片默认是使用Caffe项目里examples\image\cat.jpg。

一路Shift+Enter运行下去,看到第8步predicted输出

predicted class is: 281

第9步输出

output label: n02123045 tabby, tabby cat

预测结果是猫,说明caffe已经正确编译而且能运行了。 

大功告成,开始你的Caffe学习之旅吧

Caffe依赖库Caffe Builder的编译

这一章主要介绍怎么编译Caffe-Builder项目

安装编译所需要的软件

编译软件的需求同2.1部分

配置Windows*的环境变量

环境变量的配置同2.2部分

下载Caffe-Builder源码

Caffe-Builder的开源项目位于 https://github.com/willyd/caffe-builder
目前最新的release为1.1版,可以从这里下载最新的release 1.1.0版的源码
https://github.com/willyd/caffe-builder/releases

修改caffe-builder源码代码里的一些编译参数

修改主要基于2个方面:
首先在Windows*下Ninja编译系统不容易配置好,所以这里选择了Visual Studio 2015作为编译器。修改build_v140_x64.cmd,将红色部分的’Ninja’改为’Visual Studio 14 2015 Win64’

其次是这个Release 1.1.0发布有点时间了,caffe-builder很多依赖的开源项目都搬家或者版本更新了,需要做相应的修改。

1) 修改packages\protobuf\CmakeLists.txt
将图中所示部分protobuf包的Hash值从原来的
14a532a7538551d5def317bfca41dace
修改为
39d6a4fa549c0cce164aa3064b1492dc

2) 修改packages\hdf5\CmakeLists.txt
将图中所示部分的URL从https://www.hdfgroup.org/ftp/HDF5/releases/hdf5-1.8.16/src/CMkae-hdf5-1.8.16.zip修改为 https://www.hdfgroup.org/ftp/HDF5/releases/hdf5-1.8/hdf5-1.8.16/src/CMkae-hdf5-1.8.16.zip

编译

进到caffe-builder-1.1.0目录下,运行build_v140_x64.cmd开始编译

屏幕输出

最终编译生产的caffe依赖库文件放在build\libraries目录下,你可以将这个libraries目录拷贝到caffe所需要的目录下。

小结

本篇文章主要介绍了官方BVLC Caffe在Windows*下的编译设置过程。BVLC Caffe提供CPU和Nvidia GPU版本的实现,但是在实际学习工作中,初级开发者的电脑平台通常不会装备昂贵的Nvidia显卡,而CPU版本的Caffe因为执行效率不高,只能用来做一些小型的”玩具”实验项目。

在下一篇文章,会介绍一种基于Intel集成GPU核显加速的clCaffe框架。通过 clCaffe框架,开发者可以在中小型项目开发中在硬件成本和产品性能之间找到一个很好的平衡点。

Viewing all 583 articles
Browse latest View live


<script src="https://jsc.adskeeper.com/r/s/rssing.com.1596347.js" async> </script>