Aqoole_Hateenaの技術日記

vulkan+raytraceで色々描いてます

GL_EXT_nonuniform_qualifierについて

概要

vulkan + ray tracingのチュートリアルのコードを読んだときに気になっていた、GL_EXT_nonuniform_qualifierについて仕様の調査を行う。

仕様書

以下のGitHubが仕様書のようだ。
GLSL/GL_EXT_nonuniform_qualifier.txt at master · KhronosGroup/GLSL · GitHub

この仕様書内では以下のOpenGL Shading Language Specificationの仕様書を参照する。
https://www.khronos.org/registry/OpenGL/specs/gl/GLSLangSpec.4.60.pdf

Overview

This extension adds a "nonuniform" type qualifier and constructor, which
is required by the Vulkan API to be used when indexing descriptor
bindings with an index that is not dynamically uniform.

This extension also allows arrays of resources declared using unsized
arrays to become run-time sized arrays.

とある。
nonuniformという修飾子を追加して、bindされるresourcesをdynamicに扱えるようにする。

layout(binding = 3, set = 0, scalar) buffer Vertices {Vertex3D v[];} vertices[];

例えばこのように、vulkan側ではレイアウトだけ定義しているresourcesをshader側でサイズの指定なしに宣言することができて

    Vertex3D v0 = vertices[nonuniformEXT(objId)].v[ind.x];

shader内ではこのようにしてresourcesにアクセスできる。
verticesを複数の要素からなる配列として扱える。

Vukan API側のextension

Vulkan APIを使用するプログラムの側でも拡張機能を使用する対応が必要。
VK_EXT_descriptor_indexing(3)

device_extensions.push_back("VK_EXT_descriptor_indexing");

のようにVK_EXT_descriptor_indexingの拡張機能を有効にする。

Description

This extension adds several small features which together enable applications to create large descriptor sets containing substantially all of their resources, and selecting amongst those resources with dynamic (non-uniform) indexes in the shader. There are feature enables and SPIR-V capabilities for non-uniform descriptor indexing in the shader, and non-uniform indexing in the shader requires use of a new NonUniformEXT decoration defined in the SPV_EXT_descriptor_indexing SPIR-V extension.

GLSLでの内容と同じことが記述されているが、shader内にてdescriptor setで渡すresourcesにdynamicにアクセスし、使用できる。

Section 4.1.9 Arrays

Section 4.1自体はGLSLでの基本となる型を定義している章となる。

仕様書のこの章としては、サイズが定義されていない場合の扱いについて、書き足すものとなる。
以下で今の仕様と異なっている箇所をピックアップする。

An array whose size is not specified in a declaration is _unsized_.
Unsized arrays can either be implicitly sized or run-time sized.
A _run-time sized_ array has its size determined by a buffer or
descriptor set bound via the API.

宣言時に全体のサイズを定義しない箇所が追記されている。
この場合は配列の最大indexでもってサイズが定義されるか、(vulkan)APIによってランタイムで定められる。

Unsized arrays must not be passed as an argument to a function,

unsized arrayは関数に引数として渡せない。

Section 4.x Nonuniform qualifier

nonuniformEXT qualifierが追加となる。
nonuniformEXT修飾子は変数や式がdynamic uniformでないときに使用する。

The nonuniformEXT qualifier can also be used with constructor syntax to
assert that an expression is not dynamically uniform. For example:

layout(location = 0) flat in int i;
layout(set = 0, binding = 0) uniform sampler2D tex[2];

color = texture(tex[nonuniformEXT(i)], ...);

This constructor syntax takes a single argument of any type and returns
the value with the same type, qualified with nonuniformEXT.

サンプルコード内にもあるような使い方としては以上が挙げられ、解説されている。

GL_EXT_scalar_block_layoutについて

概要

Vulkan + Ray Tracingのコードを書き始めたときから気になっていたGL_EXT_scalar_block_layoutの拡張機能について調べてみた。

GL_EXT_scalar_block_layoutについて

仕様書はこちら
GLSL/GL_EXT_scalar_block_layout.txt at master · KhronosGroup/GLSL · GitHub

vulkanのray tracingでも見たことがあるような感じで、GitHubにテキストが置かれているだけの仕様書となっている。

OpenGL Shading Language Specificationを参照する形式で記述されていので、こちらも見ながら仕様書を読む必要がある。
https://www.khronos.org/registry/OpenGL/specs/gl/GLSLangSpec.4.60.pdf

Requirement

仕様書には、以下のように書かれている。

  • Written against SPIR-V 1.3.
  • Written against GLSL 4.6.
  • Written against Vulkan 1.1 with the VK_EXT_scalar_block_layout extension.

「この仕様書は~に対して書かれる」といった書かれ方だが、素直に上記のバージョンが必要であると読んでよさそう。

Overview

Adds a new block layout (scalar) for uniform, push constant, and storage
buffer blocks.
This new layout aligns values only to the scalar components of the block
and its composite members.

Additionally, this extension now allows uniform blocks to be decorated
with the std430 layout.

GL_EXT_scalar_block_layoutの拡張機能の肝は新しいblock layoutの追加であり、
そのblock layoutの追加はuniform, push constant, and storage buffer blocksに対して行われる。

またuniform blocksがstd430 layoutで修飾されていてもOKとある。

詳細

Section 4.4

表の1行目の1セルにscalarを追加とあるので、以下のようになる。

Layout Qualifier Qualifier Only Individual Variables Block Block Member Allowed Interfaces
shared
packed
std140
std430
scalar
X X uniform /
buffer

Section 4.4.5

Such a structure and each structure member have a base offset
and a base alignment, from which an aligned offset is computed
by rounding the base offset up to a multiple of the base
alignment.

scalerの修飾子をつけたuniformやbufferのoffsetやalignmentを定義しており、offsetとalignmentのルールはvulkanとのinterfaceで定めている内容に沿っている。

具体例の考察

#extension GL_EXT_scalar_block_layout : enable
layout(binding = 3, set = 0, scalar) buffer Vertices {Vertex3D v[];} vertices[];

例えば上記のようにglslシェーダの中に記述できる。
拡張機能の有効化によりlayout修飾子にscalarが追加されたので、layout()内でscalarを宣言できる。

bufferのメンバーはVertex3Dというユーザ定義の構造体である。
この場合

5. If the member is a structure, the base alignment of the structure is
N, where N is the largest base alignment value of any of its members.

とあり、最大のalignmentとなる要素がvec4であったとすれば、base alignmentはvec4のものとなる。
base alignmentが決まればaligned offsetも決まるため、そこからbase offsetもおそらく計算できそう。
このようにしてbase offsetとbase alignmentが決まるので、シェーダの中で

 Vertex3D v0 = vertices[0].v[0]

のようにして、vulkanからシェーダに渡しているデータの要素を取ってこれる。
ray tracingシェーダの場合、頂点の情報はraygen shaderから渡されない限り、各シェーダ(pixcelごと)が独自にbufferにアクセスする必要があるので、上記の拡張機能を利用できる。

参考リポジトリ

私が書いているサンプルコードは以下にあります。
GitHub - kodai731/Aqoole-Engine-Android-Vulkan-Rendering-Engine-: Android + Vulkan Rendering Engine

モデルのレンダリング完成(Android + Vulkan + Ray Tracing)

完成図

モデルのレンダリングだけでなく、キューブに反射するモデルも描けているところがray tracingポイントです。

1
2
3

前回からの変更点

調べてみると、Android 11からはandroid ndkが提供する画像デコーダライブラリが使える模様。
pngjpegで描かれるテクスチャを読み込んでvulkanにimage bufferとして渡すときに使用する。
画像デコーダー  |  Android NDK  |  Android Developers
上記で行えることはpng/jpegイメージを読み込んでvoid*に書き込むところまでなので、そのデータをvulkan bufferに渡すところは自分で用意する必要がある。

    AAsset* file = AAssetManager_open(app->activity->assetManager,
                                      imagePath, AASSET_MODE_STREAMING);
    AImageDecoder *decoder;
    if(AImageDecoder_createFromAAsset(file, &decoder) != ANDROID_IMAGE_DECODER_SUCCESS)
        __android_log_print(ANDROID_LOG_DEBUG, "aqoole error", "error in decode image %d", 0);
    const AImageDecoderHeaderInfo* info = AImageDecoder_getHeaderInfo(decoder);
    int32_t width = AImageDecoderHeaderInfo_getWidth(info);
    int32_t height = AImageDecoderHeaderInfo_getHeight(info);
    AndroidBitmapFormat format =
            (AndroidBitmapFormat) AImageDecoderHeaderInfo_getAndroidBitmapFormat(info);
    size_t stride = AImageDecoder_getMinimumStride(decoder);  // Image decoder does not use padding by default
    size_t size = height * stride;
    void* pixels = malloc(size);
    if(AImageDecoder_decodeImage(decoder, pixels, stride, size) != ANDROID_IMAGE_DECODER_SUCCESS)
        __android_log_print(ANDROID_LOG_DEBUG, "aqoole error", "error in decode image %d", 0);
    AImageDecoder_delete(decoder);
    AAsset_close(file);
    //copy data to tmp buffer
    VkDeviceSize imageSize = size;
    int texWidth = width;
    int texHeight = height;
    VkBuffer stagingBuffer;
    VkDeviceMemory stagingBufferMemory;
    AEBuffer::CreateBuffer(mDevice, imageSize, VK_BUFFER_USAGE_TRANSFER_SRC_BIT,
                           VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT, stagingBuffer,
                           stagingBufferMemory);
    AEBuffer::CopyData(mDevice, stagingBufferMemory, imageSize, (void*)pixels);
    //create image
    AEImage::CreateImage2D(mDevice, (uint32_t)texWidth, (uint32_t)texHeight, VK_FORMAT_B8G8R8A8_UNORM,
                           VK_IMAGE_TILING_OPTIMAL, VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_USAGE_TRANSFER_DST_BIT |
                                                                               VK_IMAGE_USAGE_SAMPLED_BIT, VK_SHARING_MODE_EXCLUSIVE, VK_SAMPLE_COUNT_1_BIT, &mImage);
    //bind image to memory
    AEImage::BindImageMemory(mDevice, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT,
                             &mImage, &mImageMemory);
    //command buffer create
    AECommandBuffer singleTimeCommandBuffer(mDevice, commandPool);
    //begin single time command
    AECommand::BeginSingleTimeCommands(&singleTimeCommandBuffer);
    AEImage::TransitionImageLayout(mDevice, &singleTimeCommandBuffer, VK_IMAGE_LAYOUT_UNDEFINED,
                                   VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, &mImage);
    //copy buffer to image memory
    AEImage::CopyBufferToImage(mDevice, &singleTimeCommandBuffer, (uint32_t)texWidth, (uint32_t)texHeight,
                               &mImage, &stagingBuffer);
    AEImage::TransitionImageLayout(mDevice, &singleTimeCommandBuffer, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
                                   VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, &mImage);
    //end command
    AECommand::EndSingleTimeCommands(&singleTimeCommandBuffer, queue);
    //create image view
    AEImage::CreateImageView2D(mDevice, &mImage, VK_FORMAT_B8G8R8A8_UNORM, VK_IMAGE_ASPECT_COLOR_BIT,
                               &mImageView, 1);
    //clean buffer
    vkFreeMemory(*mDevice->GetDevice(), stagingBufferMemory, nullptr);
    vkDestroyBuffer(*mDevice->GetDevice(), stagingBuffer, nullptr);
    free(pixels);

ただ上記は読み込み方法をstb_imageから変更しただけで、根本解決ではなかった。

rchitシェーダ

根本原因は配列の読み込みミスだった。
vertices[nonuniformEXT(objId)]とすることでrayが衝突したオブジェクトごとのvertexデータを取ってこれるが、今回はtextureデータを使用する関係で別のbufferにデータを格納していた。

layout(binding = 3, set = 0, scalar) buffer Vertices {Vertex3D v[];} vertices[];
layout(binding = 5, set = 0, scalar) buffer Verticesobj {Vertex3DObj vobj[];} verticesobj[];

今回の場合ではVerticesobj のブロックはモデルしか使わないので、アクセスはverticesobj[0].vobj[ind.x];などとすれば解決した。
オブジェクト番号と配列の番号が一致しないときはverticesobj[nonuniformEXT(objId)].vobj[ind.x];としてはダメ。

モデルの描画(Android + Ray Tracing)

モデルのレンダリング

f:id:Aqoole_Hateena:20220416224801p:plain
2
f:id:Aqoole_Hateena:20220416224732p:plain
1

思ったのと違うが、stbi_loadが使えない影響でstbi_load_from_memoryにしているからだろうか?
ただ部位ごとにはtextureで区切れているので、今はこれでよしとしようか。
ガラスで透過した絵が簡単に描けるところもray tracingの強み。

お借りしているモデルはこちら
https://skfb.ly/6S9UN
LICENSE Creative Commons Attribution
https://creativecommons.org/licenses/by/4.0/legalcode

今後の課題

単純な配列をshaderに渡せなかったので、今はmtl(マテリアル)の区切り位置のoffsetを決め打ちでハードコーディングしているので、shaderに渡せるようにしたい。
そのために、

#extension GL_EXT_scalar_block_layout : enable
#extension GL_EXT_nonuniform_qualifier : enable
#extension GL_GOOGLE_include_directive : enable
layout(binding = 3, set = 0, scalar) buffer Vertices {Vertex3D v[];} vertices[];
layout(binding = 4, set = 0) buffer Indices {uint i[];} indices[];

このあたりの、拡張機能を用いてのshaderにbufferを渡している部分の調査と理解が必要。

オブジェクトが増えてくると、descriptor setとかlayoutとかのvulkan側のコーディングが大変になってくるので、何か工夫したい。
pipelineを分けるとか?

モデルのレンダリング (Vulkan + Ray Tracing)

やりたいこと

ray tracingパイプラインでモデルをレンダリングしたい。
モデル自体は描画されるが、うまく色がつかない。
上部のcubesはガラスをイメージし、透過できるようにしている。

f:id:Aqoole_Hateena:20220412230509p:plain
モザイクのモデル

stb_imageの工夫

pngなどの画像データはstb_imageのライブラリを用いてるが、AndroidではそのままではFileやfopen()は使えないため、stbi_load()ではなくstbi_load_from_memoryで読み込んでいる。

    AAsset* file = AAssetManager_open(app->activity->assetManager,
                                      imagePath, AASSET_MODE_BUFFER);
    size_t fileLength = AAsset_getLength(file);
    auto fileContent = new unsigned char[fileLength];
    AAsset_read(file, fileContent, fileLength);
    AAsset_close(file);
    int texWidth, texHeight, texChannels;
    stbi_uc* pixels = stbi_load_from_memory(fileContent, (int)fileLength, &texWidth, &texHeight, &texChannels, STBI_rgb_alpha);
    VkDeviceSize imageSize = texWidth * texHeight * 4;

今後に向けて

モデルに色がつかない原因を調査する。
shaderの側でtextureにアクセスできていないか、vulkan appの側でうまく用意できていないか。

ガラスキューブをレイトレーシングで

ray tracingでガラスのレンダリング

f:id:Aqoole_Hateena:20220407230451p:plain
ガラスキューブ1
f:id:Aqoole_Hateena:20220407230526p:plain
ガラスキューブ2

ray tracingの真骨頂といえば、透過と反射だと(個人的には)思っていて、Androidでray tracingするからには、どこまで反射と屈折の回数を増やして計算できるのだろうと思っていた。
図の例では、3回反射と屈折を繰り返している。4回行うと途中でエラーしたので、この辺が限界のよう。
ray tracingでは、何も考えずにレイを飛ばしてヒットした先でレイを飛ばす、のようなことを行うとレイがミスする点がだんだん増えて図のように黒くなる点が増える?と思っているので、この辺りは要修正か。

環境

バイス : Samsung Galaxy S22
SoC : Exynos 2200 (Xclipse 920)

シェーダー

図のレンダリングはrchitシェーダーを2つ(AとBとする)使用してレンダリングしており、AのシェーダーからtraceRayEXTを呼び出し、レイがヒットした先でBのシェーダーを用いて色を取ってくる、といったことを繰り返している。
AMD系のGPUはrecursiveで呼び出すことができないので(昔試したときはそうだった)、このような手法にしている。
また時間があれば、シェーダーの中身も詳しく書きたい。

cubesに色がつかない

まとめ

複数オブジェクトを描画してみようとしているが、cubesに色がつかない

こんな感じ

f:id:Aqoole_Hateena:20220407044907p:plain
cubesに赤色がつかない

調べたこと

descriptor setのindex buffersをbindするときに

VUID-VkWriteDescriptorSet-dstArrayElement-00321

のメッセージが出ている。
khronos公式ページVkWriteDescriptorSet(3)でエラーメッセージを確認すると

The sum of dstArrayElement and descriptorCount must be less than or equal to the number of array elements in the descriptor set binding specified by dstBinding, and all applicable consecutive bindings, as described by https://www.khronos.org/registry/vulkan/specs/1.3-extensions/html/vkspec.html#descriptorsets-updates-consecutive

VkWriteDescriptorSet の定義は以下。

// Provided by VK_VERSION_1_0
typedef struct VkWriteDescriptorSet {
    VkStructureType                  sType;
    const void*                      pNext;
    VkDescriptorSet                  dstSet;
    uint32_t                         dstBinding;
    uint32_t                         dstArrayElement;
    uint32_t                         descriptorCount;
    VkDescriptorType                 descriptorType;
    const VkDescriptorImageInfo*     pImageInfo;
    const VkDescriptorBufferInfo*    pBufferInfo;
    const VkBufferView*              pTexelBufferView;
} VkWriteDescriptorSet;

構造体の中のdstArrayElement+descriptorCountがdescriptor setの配列要素数以下にならなければならないようで、こちらはlayoutの作成時に指定する。

この記事を書きながら修正点に気づいたので、書き換えて実行してみるとcubesは赤く表示された

f:id:Aqoole_Hateena:20220407050654p:plain
赤いcubes

最後に

Hatena BlogはQiitaのようなcodeだけ書くような場所ではないと思うので、最後に雑感を少し。
プログラムの日記を書こうとすると、最後までビルドできないこともしばしばで、どのタイミングで記事にしようか迷うこともある。
今回は成功していない場合にでも投稿しようと思ったが、これでも意味はあるのだろうか?
同じエラーが出た人がいて、エラーのキーワードで検索したときに引っかかるかもしれないが、その場合には解決策が書かれていなければその人の問題は解決しない。。。
とりあえず書いておいて、解決すればリンクを貼るとかでもよいけど、それも大変そう。
まぁ何日か書きながら考えよう。