Aqoole_Hateenaの技術日記

vulkan+raytraceで色々描いてます

Render Passの作成について

Render passの作成

render passの作成に当たりattachmentとsubpassの記述が必要。subpassとはattachmentにアクセスするrender passのsubsetである。
Render PassとFramebufferについて - Aqoole_Hateenaの技術日記
subpassの記述にもattachmentの記述が必要で、attachmentの記述にはdescription構造体とreference構造体の2種類ある。

Attachmentについて

Attachmentとは

VkAttachmentDescriptionで定義されるimage resourcesのこと。VkAttachmentReferenceのVkImageLayoutではlayoutを指定する際に、どの用途のimageかを指定する。(VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMALやVK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMALなど)

VkAttachmentDescription


VkAttachmentDescription - Structure specifying an attachment description

// Provided by VK_VERSION_1_0
typedef struct VkAttachmentDescription {
    VkAttachmentDescriptionFlags    flags;
    VkFormat                        format;
    VkSampleCountFlagBits           samples;
    VkAttachmentLoadOp              loadOp;
    VkAttachmentStoreOp             storeOp;
    VkAttachmentLoadOp              stencilLoadOp;
    VkAttachmentStoreOp             stencilStoreOp;
    VkImageLayout                   initialLayout;
    VkImageLayout                   finalLayout;
} VkAttachmentDescription;

  • flags is a bitmask of VkAttachmentDescriptionFlagBits specifying additional properties of the attachment.
  • format is a VkFormat value specifying the format of the image view that will be used for the attachment.
  • samples is a VkSampleCountFlagBits value specifying the number of samples of the image.
  • loadOp is a VkAttachmentLoadOp value specifying how the contents of color and depth components of the attachment are treated at the beginning of the subpass where it is first used.
  • storeOp is a VkAttachmentStoreOp value specifying how the contents of color and depth components of the attachment are treated at the end of the subpass where it is last used.
  • stencilLoadOp is a VkAttachmentLoadOp value specifying how the contents of stencil components of the attachment are treated at the beginning of the subpass where it is first used.
  • stencilStoreOp is a VkAttachmentStoreOp value specifying how the contents of stencil components of the attachment are treated at the end of the last subpass where it is used.
  • initialLayout is the layout the attachment image subresource will be in when a render pass instance begins.
  • finalLayout is the layout the attachment image subresource will be transitioned to when a render pass instance ends.

VkAttachmentDescription(3)

formatだったりsamplesだったりで、imageに関わるパラメータが定義されていることがわかる。loadOpやstoreOpはsubpassに関わるパラメータである。

VkAttachmentReference


VkAttachmentReference - Structure specifying an attachment reference

// Provided by VK_VERSION_1_0
typedef struct VkAttachmentReference {
    uint32_t         attachment;
    VkImageLayout    layout;
} VkAttachmentReference;

  • attachment is either an integer value identifying an attachment at the corresponding index in VkRenderPassCreateInfo::pAttachments, or VK_ATTACHMENT_UNUSED to signify that this attachment is not used.
  • layout is a VkImageLayout value specifying the layout the attachment uses during the subpass.

VkAttachmentReference(3)

attachmentはcreateinfoに登録するindex, layoutはattachementのlayoutでVK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMALやVK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMALなど。

Subpassの作成


VkSubpassDescription - Structure specifying a subpass description

// Provided by VK_VERSION_1_0
typedef struct VkSubpassDescription {
    VkSubpassDescriptionFlags       flags;
    VkPipelineBindPoint             pipelineBindPoint;
    uint32_t                        inputAttachmentCount;
    const VkAttachmentReference*    pInputAttachments;
    uint32_t                        colorAttachmentCount;
    const VkAttachmentReference*    pColorAttachments;
    const VkAttachmentReference*    pResolveAttachments;
    const VkAttachmentReference*    pDepthStencilAttachment;
    uint32_t                        preserveAttachmentCount;
    const uint32_t*                 pPreserveAttachments;
} VkSubpassDescription;

  • flags is a bitmask of VkSubpassDescriptionFlagBits specifying usage of the subpass.
  • pipelineBindPoint is a VkPipelineBindPoint value specifying the pipeline type supported for this subpass.
  • inputAttachmentCount is the number of input attachments.
  • pInputAttachments is a pointer to an array of VkAttachmentReference structures defining the input attachments for this subpass and their layouts.
  • colorAttachmentCount is the number of color attachments.
  • pColorAttachments is a pointer to an array of colorAttachmentCount VkAttachmentReference structures defining the color attachments for this subpass and their layouts.
  • pResolveAttachments is NULL or a pointer to an array of colorAttachmentCount VkAttachmentReference structures defining the resolve attachments for this subpass and their layouts.
  • pDepthStencilAttachment is a pointer to a VkAttachmentReference structure specifying the depth/stencil attachment for this subpass and its layout.
  • preserveAttachmentCount is the number of preserved attachments.
  • pPreserveAttachments is a pointer to an array of preserveAttachmentCount render pass attachment indices identifying attachments that are not used by this subpass, but whose contents must be preserved throughout the subpass.

VkSubpassDescription(3)

resolveについて、これはmultisamplingのときに使われるパラメータで、samplingに使用されるcolor attachmentを定義する。


If flags does not include VK_SUBPASS_DESCRIPTION_SHADER_RESOLVE_BIT_QCOM, and if pResolveAttachments is not NULL, each of its elements corresponds to a color attachment (the element in pColorAttachments at the same index), and a multisample resolve operation is defined for each attachment. At the end of each subpass, multisample resolve operations read the subpass’s color attachments, and resolve the samples for each pixel within the render area to the same pixel location in the corresponding resolve attachments, unless the resolve attachment index is VK_ATTACHMENT_UNUSED.
VkSubpassDescription(3)


Therefore we will have to add only one new attachment for color which is a so-called resolve attachment:
Multisampling - Vulkan Tutorial

multisamplingのtutorialを紹介しているページでは、multisamplingを有効にするために、resolve attachmentを追加する例が紹介されている。

Android NDKを用いたC++でのtouch位置取得方法

android_main()でコールバック関数の登録

android_appにinput eventが通知された際に呼び出されるコールバック関数を登録する。

int32_t handle_input(struct android_app* app, AInputEvent* inputEvent)
{
  //define app process
}

void android_main(struct android_app* app) {
:
:
  // Set the callback to process system events
  app->onInputEvent = handle_input;
:
:
}

eventの取得

コールバック関数の中にて、android/input.hに定義されている関数を用いてeventを取得する。
例:

int32_t AInputEvent_getType(const AInputEvent* event);

int32_t AInputEvent_getDeviceId(const AInputEvent* event);

int32_t AInputEvent_getSource(const AInputEvent* event);

各関数で取得できる値の説明はandroid公式に書かれている。
Input  |  Android NDK  |  Android Developers

touchしたx座標、y座標を取得する具体例

float touchPosition[2] = {0.0f, 0.0f};

int32_t handle_input(struct android_app* app, AInputEvent* inputEvent)
{
  int32_t eventType = AInputEvent_getType(inputEvent);
  switch (eventType)
  {
      case AINPUT_EVENT_TYPE_KEY :
        break;
      case AINPUT_EVENT_TYPE_MOTION :
        touchPosition[0] = AMotionEvent_getX(inputEvent, 0);
        touchPosition[1] = AMotionEvent_getY(inputEvent, 0);
        break;
      case AINPUT_EVENT_TYPE_FOCUS :
        break;
  }
  return 0;
}

void android_main(struct android_app* app) {
  app->onInputEvent = handle_input;
  :
  :
  // Main loop
  do {
       :
       :
        userFunction(touchPosition);
       :
       :
    }
  } while (app->destroyRequested == 0);
}

座標系

getX, getYで取得できる数値は、以下のようなウィンドウ左上を原点とする座標系のピクセル位置である。

f:id:Aqoole_Hateena:20211226213352p:plain
座標系

Bufferにデータをコピーする方法について

サンプルコード

git hubで公開されている、android + vulkanでのbufferにデータをコピーするの部分について考察してみた。
android-vulkan-tutorials/tutorial05_triangle at master · googlesamples/android-vulkan-tutorials · GitHub
サンプルとしては以上を上げているが、Androidに関わらずvulkan API共通の処理でもある。

vkMapMemory()

仕様書には以下のように書かれている。vkMapMemory(3)


vkMapMemory - Map a memory object into application address space
To retrieve a host virtual address pointer to a region of a mappable memory object, call (vkMapMemory)
After a successful call to vkMapMemory the memory object memory is considered to be currently host mapped.

// Provided by VK_VERSION_1_0
VkResult vkMapMemory(
    VkDevice                                    device,
    VkDeviceMemory                              memory,
    VkDeviceSize                                offset,
    VkDeviceSize                                size,
    VkMemoryMapFlags                            flags,
    void**                                      ppData);

パラメーターの説明は以下の通り。

Parameters

  • memory is the VkDeviceMemory object to be mapped.
  • ppData is a pointer to a void * variable in which is returned a host-accessible pointer to the beginning of the mapped range. This pointer minus offset must be aligned to at least VkPhysicalDeviceLimits::minMemoryMapAlignment.

関数実行後、VkDeviceMemoryのmemoryがapplication addressのppDataにマップされる。これにより、device側のメモリにデータを書き込むことができる。

memcpy()

これはC++の標準関数で、Vulkan APIの関数ではない。Microsoft Docsには以下のように記載されている。
memcpy、wmemcpy | Microsoft Docs


バッファー間でバイトをコピーします。これらの関数のより安全なバージョンを使用できます。 wmemcpy_sしてください。

void *memcpy(
   void *dest,
   const void *src,
   size_t count
);
wchar_t *wmemcpy(
   wchar_t *dest,
   const wchar_t *src,
   size_t count
);

パラメーター

  • dest
    • コピー先のバッファー。
  • src
    • コピー元のバッファー。
  • count
    • コピーする文字数。

よくあるmemcpy()の例で言えば

vkMapMemory(device.device_, deviceMemory, 0, allocInfo.allocationSize, 0, &data)
memcpy(data, vertexData, sizeof(vertexData));

があるが、これはvertexDataというapp側の配列をdataにコピーしている。vkMapMemory()によってdataは事前にdevice memoryにmapされているので、結果device側のメモリにvertex dataを書き込むことができている。

vkUnmapMemory

Vulkan® 1.2.203 - A Specification (with all registered Vulkan extensions)

To unmap a memory object once host access to it is no longer needed by the application, call: (vkUnmapMemory)

data<->device memory間のマップが不要になれば、vkUnmapMemoryでunmapする。

main loopのsynchronization考察

サンプルコード

git hubで公開されている、android + vulkanでのsynchronizationの部分について考察してみた。
android-vulkan-tutorials/tutorial05_triangle at master · googlesamples/android-vulkan-tutorials · GitHub

main loopの関数は以下

bool VulkanDrawFrame(void) {
  uint32_t nextIndex;
  // Get the framebuffer index we should draw in
  CALL_VK(vkAcquireNextImageKHR(device.device_, swapchain.swapchain_,
                                UINT64_MAX, render.semaphore_, VK_NULL_HANDLE,
                                &nextIndex));
  CALL_VK(vkResetFences(device.device_, 1, &render.fence_));

  VkPipelineStageFlags waitStageMask =
      VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT;
  VkSubmitInfo submit_info = {.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO,
                              .pNext = nullptr,
                              .waitSemaphoreCount = 1,
                              .pWaitSemaphores = &render.semaphore_,
                              .pWaitDstStageMask = &waitStageMask,
                              .commandBufferCount = 1,
                              .pCommandBuffers = &render.cmdBuffer_[nextIndex],
                              .signalSemaphoreCount = 0,
                              .pSignalSemaphores = nullptr};
  CALL_VK(vkQueueSubmit(device.queues_[0], 1, &submit_info, render.fence_));
  CALL_VK(
      vkWaitForFences(device.device_, 1, &render.fence_, VK_TRUE, 100000000));

  LOGI("Drawing frames......");

  VkResult result;
  VkPresentInfoKHR presentInfo{
      .sType = VK_STRUCTURE_TYPE_PRESENT_INFO_KHR,
      .pNext = nullptr,
      .waitSemaphoreCount = 0,
      .pWaitSemaphores = nullptr,
      .swapchainCount = 1,
      .pSwapchains = &swapchain.swapchain_,
      .pImageIndices = &nextIndex,
      .pResults = &result,
  };
  vkQueuePresentKHR(device.queues_[0], &presentInfo);
  return true;
}

vkAcquireNextImageKHR()

次の描画可能なimageのindexを取得する関数。
vkAcquireNextImageKHR(3)


semaphore is VK_NULL_HANDLE or a semaphore to signal.
fence is VK_NULL_HANDLE or a fence to signal.
If semaphore is not VK_NULL_HANDLE it must be unsignaled
If semaphore is not VK_NULL_HANDLE it must not have any uncompleted signal or wait operations pending

空のsemaphoreを登録するべきで、すでに何かを待っているsemaphoreは登録できない。

vkResetFences()

vkResetFences(3)
fenceをリセットする。
何かを待っているfenceに対しては実行できない。

vkQueueSubmit()

vkQueueSubmit(3)


vkQueueSubmit - Submits a sequence of semaphores or command buffers to a queue

とあるので、semaphoresと実行するcommand bufferを送る。
コマンドバッファーのindexの特定にnextimage indexが必要で、これは最初のvkAcquireNextImageKHR()で取得する。


fence is an optional handle to a fence to be signaled once all submitted command buffers have completed execution. If fence is not VK_NULL_HANDLE, it defines a fence signal operation.

とあるので、ここでfenceのパラメータを入力しておくと、ここで送ったcommand bufferがすべて実行された場合にfenceに信号が送られる。

vkWaitForFences()

vkWaitForFences(3)


vkWaitForFences - Wait for one or more fences to become signaled

とあるので、この例ではvkQueueSubmit()で送られたfenceに信号が送られるまで待つ。

vkQueuePresentKHR()

vkQueuePresentKHR(3)


vkQueuePresentKHR - Queue an image for presentation

command bufferで登録されたcommandがすべて実行完了していることからimageが出来上がっているので、準備が完了しているnext imageを描画する。

Image Layout Transitionsについて

Image Layout Transitions

memory dependenciesの例としてImage Layout Transitions*1がある。

Image Layouts

仕様書にはこのように書かれている。Vulkan® 1.2.202 - A Specification (with all registered Vulkan extensions)


Images are stored in implementation-dependent opaque layouts in memory. Each layout has limitations on what kinds of operations are supported for image subresources using the layout. At any given time, the data representing an image subresource in memory exists in a particular layout which is determined by the most recent layout transition that was performed on that image subresource. Applications have control over which layout each image subresource uses, and can transition an image subresource from one layout to another. Transitions can happen with an image memory barrier, included as part of a vkCmdPipelineBarrier or a vkCmdWaitEvents command buffer command (see Image Memory Barriers), or as part of a subpass dependency within a render pass (see VkSubpassDependency).

Imageは実装依存のlayoutでメモリに保存されており、各layoutごとにサポートされているoperationsが異なる。Image layoutは直前に行われたlayoutのままのため、アプリケーションの側で用途に応じてlayoutを遷移させる必要がある。

vkCmdPipelineBarrier()

具体的にはvkCmdPipelineBarrier()を用いて行う。vkCmdPipelineBarrier(3)

// Provided by VK_VERSION_1_0
void vkCmdPipelineBarrier(
    VkCommandBuffer                   commandBuffer,
    VkPipelineStageFlags                srcStageMask,
    VkPipelineStageFlags                dstStageMask,
    VkDependencyFlags                  dependencyFlags,
    uint32_t                                      memoryBarrierCount,
    const VkMemoryBarrier*            pMemoryBarriers,
    uint32_t                                      bufferMemoryBarrierCount,
    const VkBufferMemoryBarrier*  pBufferMemoryBarriers,
    uint32_t                                      imageMemoryBarrierCount,
    const VkImageMemoryBarrier* pImageMemoryBarriers);

最後の引数の部分のpImageMemoryBarriersにimage layoutの指定を行う。

// Provided by VK_VERSION_1_0
typedef struct VkImageMemoryBarrier {
    VkStructureType            sType;
    const void*                pNext;
    VkAccessFlags              srcAccessMask;
    VkAccessFlags              dstAccessMask;
    VkImageLayout              oldLayout;
    VkImageLayout              newLayout;
    uint32_t                   srcQueueFamilyIndex;
    uint32_t                   dstQueueFamilyIndex;
    VkImage                    image;
    VkImageSubresourceRange    subresourceRange;
} VkImageMemoryBarrier;

Access MaskとLayoutを指定してbarrier commandを発行する。
発行するタイミングはold layoutsでImageにアクセスする必要がなくなったのち、new layoutで必要になるまでである。
大抵の場合、swapchainのimageをrendering後のVK_IMAGE_LAYOUT_UNDEFINEDから、次にrenderingを行うためのVK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMALに遷移させる必要があると考えている。

Pipeline Stages

上記のvkCmdPipelineBarrier()に、pipeline stagesを入力するフィールドがある。
pipeline stagesについて、仕様書によるとVulkan® 1.2.203 - A Specification (with all registered Vulkan extensions)


The work performed by an action or synchronization command consists of multiple operations, which are performed as a sequence of logically independent steps known as pipeline stages.

複数のoperationsのあるworkはpipeline stagesという論理的に独立したstepsで実行される。
つまりpipeline stagesとは、pipeline内の論理的に独立したstepsである。


If a synchronization command includes a source stage mask, its first synchronization scope only includes execution of the pipeline stages specified in that mask, and its first access scope only includes memory accesses performed by pipeline stages specified in that mask.

If a synchronization command includes a destination stage mask, its second synchronization scope only includes execution of the pipeline stages specified in that mask, and its second access scope only includes memory access performed by pipeline stages specified in that mask.

vkCmdPipelineBarrier()はsrc, dstの両方のmaskを持つので、双方でscopeを持ち、制御している。

*1:e.g. by using an image memory barrier

synchronizationとdependenciesについて

Synchronization

仕様書にはこのように書かれている。Vulkan® 1.2.202 - A Specification (with all registered Vulkan extensions)

Synchronization of access to resources is primarily the responsibility of the application in Vulkan. The order of execution of commands with respect to the host and other commands on the device has few implicit guarantees, and needs to be explicitly specified. Memory caches and other optimizations are also explicitly managed, requiring that the flow of data through the system is largely under application control.

Resourcesのアクセスの同期について、Vulkanではその責任のほとんどをアプリ実装者が担う。ホストメモリと他のデバイスコマンドについては、多少の暗黙の保証はあるが、大部分は明示的に定義される必要がある。

Whilst some implicit guarantees exist between commands, five explicit synchronization mechanisms are exposed by Vulkan:

とのことで、明示的な同期の仕方は5つある。

Fences
Fences can be used to communicate to the host that execution of some task on the device has completed.

Semaphores
Semaphores can be used to control resource access across multiple queues.

Events
Events provide a fine-grained synchronization primitive which can be signaled either within a command buffer or by the host, and can be waited upon within a command buffer or queried on the host.

Pipeline Barriers
Pipeline barriers also provide synchronization control within a command buffer, but at a single point, rather than with separate signal and wait operations.

Render Passes
Render passes provide a useful synchronization framework for most rendering tasks, built upon the concepts in this chapter. Many cases that would otherwise need an application to use other synchronization primitives can be expressed more efficiently as part of a render pass.

fencesはホストに通信するときに使用され、何かデバイス上のタスクが完了しているかどうかを通信するときに使用される。
semaphoresは複数のqueues間のresource accessをコントロールするときに使用する。
eventsはコマンドバッファー内またはホストによって通知され、コマンドバッファー内で待機したり、ホストで照会したりできる、きめ細かいプリミティブな同期を提供する。
pipeline barriersはevents同様にコマンドバッファー内の同期を提供するが、別個のシグナルや待機操作ではなく、シングルポイントで行われる。
render passは、他の同期と併せてフレームワークを構築できる、とのこと。
Vulkan Tutorialで用いる同期の方法は主にfencesとsemaphoresだと思っているが、Render passのレベルで効率的なフレームワークがつくれる、らしい。

実行とメモリ依存

Vulkan® 1.2.202 - A Specification (with all registered Vulkan extensions)
オペレーションとは、ホストやデバイス上などで実行される任意の量のworksである。
同期コマンドは、同時に実行されるオペレーションの実行の依存性とメモリの依存性を明に定義する。

実行の依存性とは、2つのオペレーションがあった場合に、先のオペレーションが完了してから別のオペレーションを開始させることを保証する。

メモリの依存性とは、availability operationsとvisibility operationsのことで、
availability operationsにより、特定のメモリ書き込みにより生成された値が、将来のアクセスのためにメモリドメインでavailableになる。
memory domain operationsにより、source domainで使用可能な書き込みがdestination側で使用可能になる。(host側で書き込んだものがdevice側で使用可能になる感じ)
visibility operationsにより、メモリドメインの値が特定のメモリアクセスから見れるようになる。

BuffersとBuffer Viewsについて

Buffersについて

仕様書によるとVulkan® 1.2.202 - A Specification (with all registered Vulkan extensions)

Vulkan supports two primary resource types: buffers and images. Resources are views of memory with associated formatting and dimensionality. Buffers are essentially unformatted arrays of bytes whereas images contain format information, can be multidimensional and may have associated metadata.

とあるので、BuffersとImagesとはresourceであることがわかる。ここでResourceとはメモリにアクセスするViewのことで、対応したフォーマットと次元数がある。Buffersは本質的には決まったフォーマットを持たないバイトの羅列で、対してImagesは定められたフォーマットをもつ。

続いて以下のようにも書かれている。

Buffers represent linear arrays of data which are used for various purposes by binding them to a graphics or compute pipeline via descriptor sets or via certain commands, or by directly specifying them as parameters to certain commands.

Buffersは線形(一次元)のデータの配列を表し、descriptor setやcommandを通してpipelineにbindされることで使用される。
descriptor setとはpipelineで用いるオブジェクトをまとめたもので、pipeline作成時にlayoutとして予め宣言しておく必要がある。

Buffer Viewsについて

仕様書によるとVulkan® 1.2.202 - A Specification (with all registered Vulkan extensions)

A buffer view represents a contiguous range of a buffer and a specific format to be used to interpret the data. Buffer views are used to enable shaders to access buffer contents interpreted as formatted data.

Buffer viewsはBufferの連続領域を表し、通信するための特定のフォーマットをもつ。Buffer Viewsはshadersが読み書きできるbufferにアクセスできるようにするとある。つまり、Bufferはbuffer viewを用いてアクセスされることにより、shaderはbufferを連続領域として扱える。