Example: VulkanExamples/p07-img
シリーズ post 7(1:サーフェス、 2:最初の三角形、 3:頂点バッファ、 4:プッシュ定数、 5:テクスチャ、 6:コンピュート+ストレージバッファ)。 ここでコンピュートとグラフィックスがついに1フレームで出会う:
コンピュートシェーダがプラズマパターンをストレージイメージ に書き込み、——1つのバリアを挟んで——グラフィックスパイプライン がフルスクリーン三角形上でその同じイメージをサンプルする。 ピクセルデータは GPU を離れない。
仕組み
ストレージイメージ対サンプルイメージ
image2D(ストレージ) | sampler2D(サンプル) | |
|---|---|---|
| シェーダアクセス | 整数座標の imageStore/img | float UV の texture() |
| 必要レイアウト | eGeneral | eShaderReadOnlyOptimal |
| フィルタリング/mip | なし | 完全なサンプラ状態 |
| GLSL バインディング | layout(rgba8) uniform image2D | uniform sampler2D |
ストレージイメージはサンプラパス全体をスキップする——コンピュート
シェーダは生のピクセル座標で imageStore(img, p, color) を
書き込む。フォーマット修飾子(rgba8)は GLSL 側で必須。
1つのイメージ、2つのデスクリプタ役割
同じ VkImageView が1つのデスクリプタセットの2つのバインディング
に現れる:バインディング 0 は eStorageImage(コンピュート
ステージ、記録レイアウト eGeneral)、バインディング 1 は
eCombinedImageSampler(フラグメントステージ、
eShaderReadOnlyOptimal)。各デスクリプタエントリはその
バインディングが使われるときイメージがいなければならない
レイアウトを保持する——だから同じビュー上の2つのエントリは
レイアウトについて不一致で、両方正しい。
イメージは両方の用途で作られる:eStorage | eSampled。
このポストが実際に教える順序問題
1つのコマンドバッファ内でシーケンスは:
barrier eShaderRead(または Undefined) -> eGeneral [コンピュートが書く]
dispatch(256/16, 256/16)
barrier eGeneral -> eShaderReadOnlyOptimal [フラグメントが読む]
eShaderWrite -> eShaderRead
eComputeShader -> eFragmentShader
draw(3)2番目のバリアが要点:コンピュートの書き込みを可視 (アクセスマスク遷移)かつ再梱包(サンプリング最適タイル へのレイアウト遷移)にする。スキップするとフラグメント シェーダはガーベジを読む——たいていクラッシュではなくただ 間違ったピクセル、最悪の種類の Vulkan バグ。
ドローがイメージを eShaderReadOnlyOptimal にするので、
フレーム2以降はまず eGeneral に戻す——だから record() は
前のレイアウトを追跡状態(Shared::layout)として受け取り、
バリアの oldLayout は決して推測しない。
ビルド&実行
g++ -std=c++23 -O2 main.cpp -o app -lvulkan -lglfw -lshaderc
./app # windowed
./app --headless out.ppm # offscreen -> PPM
python3 check.py out.ppmチェック
コンピュートシェーダはテクセル毎に (r,g,b) = (tx, ty, tx XOR ty)
を書き込む——スクリプトが解析的に反転できるパターン:サンプルされた
各画面ピクセルについてソーステクセルを計算し、3チャンネルすべてを
1 ulp の許容範囲で比較する。
#!/usr/bin/env python3
# SPDX-License-Identifier: CC0-1.0
# check.py — post 7 の compute->graphics パイプラインを検証。
# ストレージイメージは 256x256;コンピュートシェーダはテクセル毎に
# (r,g,b) = (tx, ty, tx^ty) を書く。フルスクリーン三角形はそれを
# 640x480 に NEAREST でサンプルするので、画面ピクセル (x,y) は
# テクセル (floor((x+0.5)*256/640), floor((y+0.5)*256/480)) を読む。
import sys
def read_ppm(path):
d = open(path, "rb").read()
hdr, data = d.split(b"255\n", 1)
toks = hdr.split()
assert toks[0] == b"P6"
w, h = int(toks[1]), int(toks[2])
assert len(data) == w * h * 3
return w, h, data
def px(d, w, x, y):
i = (y * w + x) * 3
return tuple(d[i:i + 3])
def main():
path = sys.argv[1] if len(sys.argv) > 1 else "out.ppm"
w, h, d = read_ppm(path)
assert (w, h) == (640, 480)
checked = 0
for y in range(30, h, 60):
for x in range(40, w, 80):
tx = int((x + 0.5) * 256 / 640)
ty = int((y + 0.5) * 256 / 480)
want = (tx, ty, tx ^ ty)
got = px(d, w, x, y)
# NEAREST + UNORM 往復:各チャンネル 1 ulp まで許容
assert all(abs(a - b) <= 1 for a, b in zip(got, want)), \
f"px({x},{y})={got} want {want} (texel {tx},{ty})"
checked += 1
print(f"OK: {checked} pixels match the compute pattern exactly")
if __name__ == "__main__":
main()$ ./app --headless out.ppm && python3 check.py out.ppm
device: AMD Radeon RX 7800 XT (RADV NAVI32)
wrote out.ppm
OK: 64 pixels match the compute pattern exactly完全なソース
// SPDX-License-Identifier: CC0-1.0
//
// Post 7:1フレームでの compute -> graphics。コンピュートシェーダが
// プラズマパターンを*ストレージイメージ*に書き込み;イメージ
// レイアウトバリアの後、グラフィックスパイプラインがフルスクリーン
// 三角形上で同じイメージをサンプルする。1つのデスクリプタセットが
// 両ステージに仕える(バインディング 0 = コンピュート用ストレージ
// イメージ、バインディング 1 = フラグメント用結合サンプラ)、両方の
// デスクリプタが同じ VkImageView を指すから。
//
// ./app ウィンドウ:プラズマテクスチャ
// ./app --headless o.ppm 1フレーム -> check.py 用 PPM
//
// ビルド: g++ -std=c++23 -O2 main.cpp -o app -lvulkan -lglfw -lshaderc
#include "vkmini.hpp"
#include <GLFW/glfw3.h>
constexpr uint32_t kTex = 256;
// ---------------- シェーダ -------------------------------------------
static const char* kComp = R"GLSL(
#version 460
layout(local_size_x = 16, local_size_y = 16) in;
// ストレージイメージ:整数座標での書き込み可能ピクセル、
// サンプラは絡まない。フォーマット修飾子(rgba8)は必須。
layout(set = 0, binding = 0, rgba8) uniform image2D img;
void main() {
ivec2 p = ivec2(gl_GlobalInvocationID.xy);
vec2 uv = vec2(p) / 255.0;
// plasma: r = x gradient, g = y gradient, b = xor pattern
float b = float((p.x ^ p.y) & 255) / 255.0;
imageStore(img, p, vec4(uv.x, uv.y, b, 1.0));
}
)GLSL";
static const char* kVert = R"GLSL(
#version 460
layout(location = 0) out vec2 vUV;
void main() {
vUV = vec2((gl_VertexIndex << 1) & 2, gl_VertexIndex & 2);
gl_Position = vec4(vUV * 2.0 - 1.0, 0.0, 1.0);
}
)GLSL";
static const char* kFrag = R"GLSL(
#version 460
layout(location = 0) in vec2 vUV;
layout(location = 0) out vec4 outColor;
layout(set = 0, binding = 1) uniform sampler2D tex;
void main() { outColor = texture(tex, vUV); }
)GLSL";
// ---------------- リソース ------------------------------------------
struct Shared {
vkmini::Vk::Image img; // storage + sampled
vk::UniqueSampler sampler;
vk::UniqueDescriptorSetLayout setLayout;
vk::UniqueDescriptorPool pool;
vk::DescriptorSet set;
vk::ImageLayout layout = vk::ImageLayout::eUndefined; // 追跡
};
/// 1つのイメージ、2つのデスクリプタ役割。セットレイアウトは
/// 2つのバインディングを持つ:
/// binding 0 eStorageImage (コンピュートステージ)
/// binding 1 eCombinedImageSampler (フラグメントステージ)
/// そして1つの VkDescriptorSet が両方を同じイメージビューに向ける。
static Shared makeShared(vkmini::Vk& vk) {
Shared o;
o.img = vk.createImage(kTex, kTex, vk::Format::eR8G8B8A8Unorm,
vk::ImageUsageFlagBits::eStorage |
vk::ImageUsageFlagBits::eSampled);
vk::SamplerCreateInfo si{};
si.setMagFilter(vk::Filter::eNearest)
.setMinFilter(vk::Filter::eNearest)
.setAddressModeU(vk::SamplerAddressMode::eClampToEdge)
.setAddressModeV(vk::SamplerAddressMode::eClampToEdge)
.setAddressModeW(vk::SamplerAddressMode::eClampToEdge);
o.sampler = vk.device->createSamplerUnique(si);
std::array lbs{
vk::DescriptorSetLayoutBinding{}
.setBinding(0)
.setDescriptorType(vk::DescriptorType::eStorageImage)
.setDescriptorCount(1)
.setStageFlags(vk::ShaderStageFlagBits::eCompute),
vk::DescriptorSetLayoutBinding{}
.setBinding(1)
.setDescriptorType(
vk::DescriptorType::eCombinedImageSampler)
.setDescriptorCount(1)
.setStageFlags(vk::ShaderStageFlagBits::eFragment),
};
vk::DescriptorSetLayoutCreateInfo li{};
li.setBindings(lbs);
o.setLayout = vk.device->createDescriptorSetLayoutUnique(li);
std::array pss{
vk::DescriptorPoolSize{vk::DescriptorType::eStorageImage, 1},
vk::DescriptorPoolSize{
vk::DescriptorType::eCombinedImageSampler, 1},
};
vk::DescriptorPoolCreateInfo pi{};
pi.setMaxSets(1).setPoolSizes(pss);
o.pool = vk.device->createDescriptorPoolUnique(pi);
vk::DescriptorSetAllocateInfo ai{};
ai.setDescriptorPool(o.pool.get())
.setSetLayouts(o.setLayout.get());
o.set = vk.device->allocateDescriptorSets(ai).front();
// バインディング 0 はコンピュートステージが書き込む
// eGeneral レイアウトを指す;バインディング 1 は
// eShaderReadOnlyOptimal を指す。デスクリプタに記録された
// レイアウトは*そのバインディングが使われるとき*のイメージ
// レイアウトと一致しなければならない。
vk::DescriptorImageInfo store{};
store.setImageView(o.img.view.get())
.setImageLayout(vk::ImageLayout::eGeneral);
vk::DescriptorImageInfo sample{};
sample.setSampler(o.sampler.get())
.setImageView(o.img.view.get())
.setImageLayout(vk::ImageLayout::eShaderReadOnlyOptimal);
std::array writes{
vk::WriteDescriptorSet{}.setDstSet(o.set).setDstBinding(0)
.setDescriptorType(vk::DescriptorType::eStorageImage)
.setImageInfo(store),
vk::WriteDescriptorSet{}.setDstSet(o.set).setDstBinding(1)
.setDescriptorType(
vk::DescriptorType::eCombinedImageSampler)
.setImageInfo(sample),
};
vk.device->updateDescriptorSets(writes, {});
return o;
}
// ---------------- パイプライン ------------------------------------------
static vk::UniquePipeline makeCompute(vkmini::Vk& vk,
vk::UniquePipelineLayout& lay,
vk::DescriptorSetLayout sl) {
auto cs = vk.shader(kComp, shaderc_compute_shader, "plasma.comp");
vk::PipelineLayoutCreateInfo li{};
li.setSetLayouts(sl);
lay = vk.device->createPipelineLayoutUnique(li);
vk::PipelineShaderStageCreateInfo st{};
st.setStage(vk::ShaderStageFlagBits::eCompute)
.setModule(cs.get())
.setPName("main");
vk::ComputePipelineCreateInfo ci{};
ci.setStage(st).setLayout(lay.get());
auto res = vk.device->createComputePipelineUnique({}, ci);
if (res.result != vk::Result::eSuccess)
throw std::runtime_error("compute pipeline failed");
return std::move(res.value);
}
static vk::UniquePipeline makeGraphics(
vkmini::Vk& vk, vk::UniquePipelineLayout& lay,
vk::DescriptorSetLayout sl, vk::Format fmt) {
auto vs = vk.shader(kVert, shaderc_vertex_shader, "fs.vert");
auto fs = vk.shader(kFrag, shaderc_fragment_shader, "fs.frag");
vk::PipelineShaderStageCreateInfo stages[2];
stages[0].setStage(vk::ShaderStageFlagBits::eVertex)
.setModule(vs.get())
.setPName("main");
stages[1].setStage(vk::ShaderStageFlagBits::eFragment)
.setModule(fs.get())
.setPName("main");
vk::PipelineVertexInputStateCreateInfo vin{};
vk::PipelineInputAssemblyStateCreateInfo ia{};
ia.setTopology(vk::PrimitiveTopology::eTriangleList);
vk::PipelineViewportStateCreateInfo vp{};
vp.setViewportCount(1).setScissorCount(1);
std::array dynStates{vk::DynamicState::eViewport,
vk::DynamicState::eScissor};
vk::PipelineDynamicStateCreateInfo dyn{};
dyn.setDynamicStates(dynStates);
vk::PipelineRasterizationStateCreateInfo rs{};
rs.setPolygonMode(vk::PolygonMode::eFill)
.setCullMode(vk::CullModeFlagBits::eNone)
.setFrontFace(vk::FrontFace::eCounterClockwise)
.setLineWidth(1.0f);
vk::PipelineMultisampleStateCreateInfo ms{};
ms.setRasterizationSamples(vk::SampleCountFlagBits::e1);
vk::PipelineColorBlendAttachmentState cb{};
cb.setColorWriteMask(
vk::ColorComponentFlagBits::eR | vk::ColorComponentFlagBits::eG |
vk::ColorComponentFlagBits::eB | vk::ColorComponentFlagBits::eA);
vk::PipelineColorBlendStateCreateInfo blend{};
blend.setAttachments(cb);
vk::PipelineLayoutCreateInfo li{};
li.setSetLayouts(sl);
lay = vk.device->createPipelineLayoutUnique(li);
vk::PipelineRenderingCreateInfo rendering{};
rendering.setColorAttachmentFormats(fmt);
vk::GraphicsPipelineCreateInfo gi{};
gi.setStages(stages)
.setPVertexInputState(&vin)
.setPInputAssemblyState(&ia)
.setPViewportState(&vp)
.setPDynamicState(&dyn)
.setPRasterizationState(&rs)
.setPMultisampleState(&ms)
.setPColorBlendState(&blend)
.setLayout(lay.get())
.setPNext(&rendering);
auto res = vk.device->createGraphicsPipelineUnique({}, gi);
if (res.result != vk::Result::eSuccess)
throw std::runtime_error("graphics pipeline failed");
return std::move(res.value);
}
static void imgBarrier(vk::CommandBuffer c, vk::Image img,
vk::ImageLayout ol, vk::ImageLayout nl,
vk::AccessFlags src, vk::AccessFlags dst,
vk::PipelineStageFlags srcStage,
vk::PipelineStageFlags dstStage) {
vk::ImageMemoryBarrier b{};
b.setOldLayout(ol)
.setNewLayout(nl)
.setSrcQueueFamilyIndex(VK_QUEUE_FAMILY_IGNORED)
.setDstQueueFamilyIndex(VK_QUEUE_FAMILY_IGNORED)
.setImage(img)
.setSubresourceRange({vk::ImageAspectFlagBits::eColor, 0, 1,
0, 1})
.setSrcAccessMask(src)
.setDstAccessMask(dst);
c.pipelineBarrier(srcStage, dstStage, {}, {}, {}, b);
}
/// このポストの核心——1つのコマンドバッファ内:
/// 1. 共有イメージを sh.layout -> eGeneral に遷移
/// 2. それに書き込むコンピュートシェーダをディスパッチ
/// 3. バリア eGeneral -> eShaderReadOnlyOptimal
/// (eShaderWrite -> eShaderRead, compute -> fragment)
/// 4. 同じイメージをサンプルするフルスクリーンドロー
/// ホストはイメージのレイアウトを `sh.layout` で追跡するので
/// フレーム2以降は eShaderReadOnlyOptimal から eGeneral に戻す。
static void record(vk::CommandBuffer c, Shared& sh,
vk::Pipeline cpipe, vk::PipelineLayout clay,
vk::Pipeline gpipe, vk::PipelineLayout glay,
vk::ImageView target, vk::Extent2D extent) {
imgBarrier(c, sh.img.img.get(), sh.layout,
vk::ImageLayout::eGeneral,
sh.layout == vk::ImageLayout::eUndefined
? vk::AccessFlags{}
: vk::AccessFlagBits::eShaderRead,
vk::AccessFlagBits::eShaderWrite,
vk::PipelineStageFlagBits::eTopOfPipe,
vk::PipelineStageFlagBits::eComputeShader);
c.bindPipeline(vk::PipelineBindPoint::eCompute, cpipe);
c.bindDescriptorSets(vk::PipelineBindPoint::eCompute, clay, 0,
sh.set, {});
c.dispatch(kTex / 16, kTex / 16, 1);
// compute 書き込み -> fragment 読み取り:1つのバリアでの
// レイアウト+可視化。これがこのポストが追加する同期のすべて。
imgBarrier(c, sh.img.img.get(), vk::ImageLayout::eGeneral,
vk::ImageLayout::eShaderReadOnlyOptimal,
vk::AccessFlagBits::eShaderWrite,
vk::AccessFlagBits::eShaderRead,
vk::PipelineStageFlagBits::eComputeShader,
vk::PipelineStageFlagBits::eFragmentShader);
sh.layout = vk::ImageLayout::eShaderReadOnlyOptimal;
vk::RenderingAttachmentInfo att{};
att.setImageView(target)
.setImageLayout(vk::ImageLayout::eColorAttachmentOptimal)
.setLoadOp(vk::AttachmentLoadOp::eClear)
.setStoreOp(vk::AttachmentStoreOp::eStore)
.setClearValue(vk::ClearValue{
vk::ClearColorValue{std::array{0.f, 0.f, 0.f, 1.f}}});
vk::RenderingInfo ri{};
ri.setRenderArea({{0, 0}, extent})
.setLayerCount(1)
.setColorAttachments(att);
c.beginRendering(ri);
c.bindPipeline(vk::PipelineBindPoint::eGraphics, gpipe);
c.bindDescriptorSets(vk::PipelineBindPoint::eGraphics, glay, 0,
sh.set, {});
vk::Viewport vp{0, 0, float(extent.width), float(extent.height),
0.f, 1.f};
c.setViewport(0, vp);
vk::Rect2D sc{{0, 0}, extent};
c.setScissor(0, sc);
c.draw(3, 1, 0, 0);
c.endRendering();
}
int main(int argc, char** argv) {
bool headless = argc > 1 && std::string(argv[1]) == "--headless";
const char* ppmOut = argc > 2 ? argv[2] : "out.ppm";
vkmini::Vk vk;
GLFWwindow* win = nullptr;
vk::UniqueSurfaceKHR surface;
if (!headless) {
glfwInit();
glfwWindowHint(GLFW_CLIENT_API, GLFW_NO_API);
win = glfwCreateWindow(640, 480, "vkmini p07", nullptr, nullptr);
}
std::vector<const char*> instExts;
if (!headless) {
uint32_t n = 0;
const char** e = glfwGetRequiredInstanceExtensions(&n);
instExts.assign(e, e + n);
}
vk.createInstance(instExts);
if (!headless) {
VkSurfaceKHR raw;
glfwCreateWindowSurface(vk.instance.get(), win, nullptr, &raw);
surface = vk::UniqueSurfaceKHR(raw, vk.instance.get());
}
vk.pickPhysicalDevice();
vk.createDevice(headless ? std::vector<const char*>{}
: std::vector<const char*>{
VK_KHR_SWAPCHAIN_EXTENSION_NAME});
Shared sh = makeShared(vk);
vk::UniquePipelineLayout clay, glay;
auto cpipe = makeCompute(vk, clay, sh.setLayout.get());
if (headless) {
vkmini::Headless hl;
hl.init(vk, 640, 480);
auto gpipe = makeGraphics(vk, glay, sh.setLayout.get(),
vk::Format::eR8G8B8A8Unorm);
hl.render(vk::ImageLayout::eColorAttachmentOptimal,
[&](vk::CommandBuffer c) {
record(c, sh, cpipe.get(), clay.get(),
gpipe.get(), glay.get(),
hl.color.view.get(), hl.extent);
});
hl.savePpm(ppmOut);
printf("wrote %s\n", ppmOut);
return 0;
}
vkmini::Swapchain sc;
sc.create(vk, vk.phys, surface.get(), 640, 480);
auto gpipe = makeGraphics(vk, glay, sh.setLayout.get(), sc.format);
auto cmd = vk.device->allocateCommandBuffersUnique(
{vk.pool.get(), vk::CommandBufferLevel::ePrimary, 1});
auto acqSem = vk.device->createSemaphoreUnique({});
auto doneSem = vk.device->createSemaphoreUnique({});
auto fence = vk.device->createFenceUnique({});
for (int f = 0; f < 240 && !glfwWindowShouldClose(win); ++f) {
glfwPollEvents();
vk.device->resetFences(fence.get());
auto acq = vk.device->acquireNextImageKHR(
sc.sc.get(), UINT64_MAX, acqSem.get(), {});
vk::Image img = sc.images[acq.value];
cmd[0]->reset();
cmd[0]->begin(
{vk::CommandBufferUsageFlagBits::eOneTimeSubmit});
// スワップチェーンイメージ:UNDEFINED -> COLOR_ATTACHMENT
imgBarrier(cmd[0].get(), img, vk::ImageLayout::eUndefined,
vk::ImageLayout::eColorAttachmentOptimal,
vk::AccessFlags{},
vk::AccessFlagBits::eColorAttachmentWrite,
vk::PipelineStageFlagBits::eTopOfPipe,
vk::PipelineStageFlagBits::eColorAttachmentOutput);
record(cmd[0].get(), sh, cpipe.get(), clay.get(), gpipe.get(),
glay.get(), sc.views[acq.value].get(), sc.extent);
imgBarrier(cmd[0].get(), img,
vk::ImageLayout::eColorAttachmentOptimal,
vk::ImageLayout::ePresentSrcKHR,
vk::AccessFlagBits::eColorAttachmentWrite,
vk::AccessFlags{},
vk::PipelineStageFlagBits::eColorAttachmentOutput,
vk::PipelineStageFlagBits::eBottomOfPipe);
cmd[0]->end();
vk::PipelineStageFlags waitStage =
vk::PipelineStageFlagBits::eColorAttachmentOutput;
vk::SubmitInfo si{};
si.setWaitSemaphores(acqSem.get())
.setWaitDstStageMask(waitStage)
.setCommandBuffers(cmd[0].get())
.setSignalSemaphores(doneSem.get());
vk.queue.submit(si, fence.get());
vk::PresentInfoKHR pi{};
pi.setWaitSemaphores(doneSem.get())
.setSwapchains(sc.sc.get())
.setImageIndices(acq.value);
(void)vk.queue.presentKHR(pi);
(void)vk.device->waitForFences(fence.get(), VK_TRUE,
UINT64_MAX);
}
vk.queue.waitIdle();
sc.views.clear();
sc.sc.reset();
surface.reset();
glfwDestroyWindow(win);
glfwTerminate();
return 0;
}このシリーズのすべてのソースコードは CC0 1.0 Universal(CC0-1.0) の下でリリース——パブリックドメイン献身、帰属表示不要。
シリーズ次回:フライト中フレーム——これまでのウィンドウループは フレーム毎のフェンス待ちで CPU と GPU を直列化していた。最終 ポストはそれをダブルバッファ同期オブジェクトに置き換えて、 CPU と GPU が適切にオーバーラップするようにする。
