Example: VulkanExamples/p33-blur
マルチパスポストプロセッシング記事は
フラグメントパスでポストプロセッシングを行った——
テクスチャにレンダーし、全画面三角形がサンプルして
フィルタ結果を書く。もう1つの方法はコンピュートパス:
sampler2D がレンダー済みイメージを読み、image2D が
結果を書き、1つの vkCmdDispatch がフレーム全体をカバー。
2番目のグラフィックスパイプラインも頂点処理もなく——
local_size タイルは GPU ワークグループに自然にマップ。
チェーン
example.txt
graphics pass -> imgA (ColorAttachmentOptimal)
barrier: eColorAttachmentWrite -> eShaderRead
(ColorAttachmentOptimal -> ShaderReadOnlyOptimal)
compute pass -> samples imgA, imageStore -> imgB (eGeneral)
barrier: eShaderWrite -> eTransferRead (General -> TransferSrc)
readback -> vkCmdCopyImageToBuffer -> PPMすべて1つのコマンドバッファ——バリアが継ぎ目。
example.txt
layout(local_size_x = 16, local_size_y = 16) in;
layout(binding = 0) uniform sampler2D src;
layout(binding = 1, rgba8) uniform writeonly image2D dst;
void main() {
ivec2 p = ivec2(gl_GlobalInvocationID.xy);
vec3 acc = vec3(0);
for (int dy = -2; dy <= 2; ++dy)
for (int dx = -2; dx <= 2; ++dx)
acc += texture(src, (vec2(p)+vec2(dx,dy))/128.0).rgb;
imageStore(dst, p, vec4(acc/25.0, 1.0));
}ポストエフェクト向けコンピュート vs フラグメント
| フラグメントパス | コンピュートパス | |
|---|---|---|
| 近傍アクセス | texture() ギャザー——自然 | texture() または共有メモリ |
| 出力 | ブレンドステート、MSAAリゾルブ、レイヤー化 rt | 任意の imageStore、アトミック |
| タイル再利用 | キャッシュ頼み | shared メモリ = 明示的タイルキャッシュ |
| 勝る場面 | 単純なピクセルごとフィルタ | 再利用可能な近傍、リダクション、非ラスタ出力のフィルタ |
共有メモリ内の分離可能ブラー(水平→垂直)は古典的な コンピュートの勝ちパターン——このデモは明確さのため 5×5ボックスのまま。
やってはいけない方法
ColorAttachmentOptimalのままのアタッチメントをサンプル しない——遷移する(デモの中間バリア);eGeneralでも 動くがeShaderReadOnlyOptimalがバリデータがディスクリプタに 期待するレイアウト。- サンプリングをクランプ——
eClampToEdgeまたはボーダー チェック;範囲外を読むとゴミがエッジピクセルに ラップ/リピート。 - ストレージイメージに
writeonly——コンパイラに read-modify-write パスをスキップしてよいと伝える;読み書きには 修飾子なしのimage2Dを使いコヒーレンシに注意。 - ワークグループサイズは制限——
local_size_x*y*z≤maxComputeWorkGroupInvocations(ここでは1024);gl_WorkGroupID算術でタイル化し、1グループを過大にしない。
ビルドと実行
build.sh
g++ -std=c++23 -O2 main.cpp -o app -lvulkan -lglfw -lshaderc
./app --headless out.ppm && python3 check.py out.ppmcheck.py はブラーが実際に動いたことを検証:シャープな
チェッカーボードは2レベルのみ;ブラー出力はセル境界に
中間調ピクセルの大きな帯を含む(3584個検出)。
check.py
#!/usr/bin/env python3
# SPDX-License-Identifier: CC0-1.0
# check.py — コンピュートブラーが動いたことを検証:8x8 白黒
# チェッカーボードの5x5ボックスブラーはエッジを柔らげる——
# 境界ピクセルは純粋な0.05/0.9レベルではなく中間グレーになる。
import sys
def read_ppm(path):
d = open(path, "rb").read()
hdr, data = d.split(b"255\n", 1)
toks = hdr.split()
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 d[i]
def main():
path = sys.argv[1] if len(sys.argv) > 1 else "out.ppm"
w, h, d = read_ppm(path)
assert (w, h) == (128, 128)
vals = [px(d, w, x, y) for y in range(h) for x in range(w)]
lo, hi = min(vals), max(vals)
mids = sum(1 for v in vals if 60 < v < 180)
# ブラーなしチェッカー = 約13と約230のみ;ブラーが中間調を追加
print(f"range: {lo}..{hi}, mid-tone pixels: {mids}")
assert lo < 30 and hi > 200, "checkerboard levels missing"
assert mids > 500, f"too few blurred mid-tones: {mids}"
print("OK: compute blur produced softened checkerboard edges")
if __name__ == "__main__":
main()output.txt
range: 13..229, mid-tone pixels: 3584
OK: compute blur produced softened checkerboard edges完全なソース
main.cpp
// SPDX-License-Identifier: CC0-1.0
//
// コンピュートポストフィルタ:グラフィックスパスがシャープな
// チェッカーボードをイメージ A(カラーアタッチメント)に
// レンダーし、コンピュートパスがボックスブラーをイメージ B
// (ストレージイメージ、eGeneral)にかけ、B が外にコピーされる。
// ポストプロセッシングステージとしてのコンピュート——
// 1ディスパッチで texture サンプラー読み+image2D 書き、
// 2番目のグラフィックスパイプラインなし。
//
// ./app --headless o.ppm 1フレーム -> check.py 用 PPM
// ./app ウィンドウ
//
// Build: g++ -std=c++23 -O2 main.cpp -o app -lvulkan -lglfw -lshaderc
#include "vkmini.hpp"
#include <GLFW/glfw3.h>
// シャープな8x8チェッカーボード、64x64 上の NDC -1..+1
static const char* kVert = R"GLSL(
#version 460
layout(location = 0) out vec2 vUV;
void main() {
vec2 p = vec2((gl_VertexIndex << 1 & 2), gl_VertexIndex & 2);
vUV = p;
gl_Position = vec4(p * 2.0 - 1.0, 0, 1);
}
)GLSL";
static const char* kFrag = R"GLSL(
#version 460
layout(location = 0) in vec2 vUV;
layout(location = 0) out vec4 outColor;
void main() {
bool e = ((int(vUV.x * 8) ^ int(vUV.y * 8)) & 1) != 0;
outColor = vec4(e ? vec3(0.9) : vec3(0.05), 1.0);
}
)GLSL";
// 5x5 ボックスブラー:入力を texture()、結果を imageStore()
static const char* kComp = R"GLSL(
#version 460
layout(local_size_x = 16, local_size_y = 16) in;
layout(binding = 0) uniform sampler2D src;
layout(binding = 1, rgba8) uniform writeonly image2D dst;
void main() {
ivec2 p = ivec2(gl_GlobalInvocationID.xy);
vec2 ts = 1.0 / vec2(imageSize(dst));
vec3 acc = vec3(0);
for (int dy = -2; dy <= 2; ++dy)
for (int dx = -2; dx <= 2; ++dx)
acc += texture(src, (vec2(p) + vec2(dx, dy)) * ts).rgb;
imageStore(dst, p, vec4(acc / 25.0, 1.0));
}
)GLSL";
int main(int argc, char** argv) {
bool headless = argc > 1 && std::string(argv[1]) == "--headless";
const char* ppmOut = argc > 2 ? argv[2] : "out.ppm";
(void)headless;
vkmini::Vk vk;
vk.createInstance({});
vk.pickPhysicalDevice();
vk.createDevice({});
const uint32_t S = 128;
vk::ImageSubresourceRange sr{vk::ImageAspectFlagBits::eColor,
0, 1, 0, 1};
// イメージ A:グラフィックスがチェッカーボードをここにレンダー
auto imgA = vk.createImage(
S, S, vk::Format::eR8G8B8A8Unorm,
vk::ImageUsageFlagBits::eColorAttachment |
vk::ImageUsageFlagBits::eSampled);
// イメージ B:コンピュートがブラー結果を書く
auto imgB = vk.createImage(
S, S, vk::Format::eR8G8B8A8Unorm,
vk::ImageUsageFlagBits::eStorage |
vk::ImageUsageFlagBits::eTransferSrc);
// ---- グラフィックスパイプライン(チェッカーボード)-----------
auto vs = vk.shader(kVert, shaderc_vertex_shader, "s.vert");
auto fs = vk.shader(kFrag, shaderc_fragment_shader, "s.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);
auto glayout = vk.device->createPipelineLayoutUnique({});
vk::Format cfmt = vk::Format::eR8G8B8A8Unorm;
vk::PipelineRenderingCreateInfo rendering{};
rendering.setColorAttachmentFormats(cfmt);
vk::GraphicsPipelineCreateInfo gi{};
gi.setStages(stages)
.setPVertexInputState(&vin)
.setPInputAssemblyState(&ia)
.setPViewportState(&vp)
.setPDynamicState(&dyn)
.setPRasterizationState(&rs)
.setPMultisampleState(&ms)
.setPColorBlendState(&blend)
.setLayout(glayout.get())
.setPNext(&rendering);
auto gpres = vk.device->createGraphicsPipelineUnique({}, gi);
if (gpres.result != vk::Result::eSuccess)
throw std::runtime_error("graphics pipeline failed");
auto gpipe = std::move(gpres.value);
// ---- コンピュートパイプライン(ブラー)------------------------
auto cs = vk.shader(kComp, shaderc_compute_shader, "s.comp");
std::array cbds{
vk::DescriptorSetLayoutBinding{
0, vk::DescriptorType::eCombinedImageSampler, 1,
vk::ShaderStageFlagBits::eCompute},
vk::DescriptorSetLayoutBinding{
1, vk::DescriptorType::eStorageImage, 1,
vk::ShaderStageFlagBits::eCompute}};
vk::DescriptorSetLayoutCreateInfo cdslci{};
cdslci.setBindings(cbds);
auto cdsl = vk.device->createDescriptorSetLayoutUnique(cdslci);
vk::DescriptorSetLayout cdslH = cdsl.get();
vk::PipelineLayoutCreateInfo cplci{};
cplci.setSetLayouts(cdslH);
auto clayout = vk.device->createPipelineLayoutUnique(cplci);
vk::ComputePipelineCreateInfo cpi{};
cpi.stage.setStage(vk::ShaderStageFlagBits::eCompute)
.setModule(cs.get())
.setPName("main");
cpi.setLayout(clayout.get());
auto cres = vk.device->createComputePipelineUnique({}, cpi);
if (cres.result != vk::Result::eSuccess)
throw std::runtime_error("compute pipeline failed");
auto cpipe = std::move(cres.value);
// ディスクリプタ:binding0 = imgA サンプル、binding1 = imgB ストレージ
vk::DescriptorPoolSize ps[2]{
{vk::DescriptorType::eCombinedImageSampler, 1},
{vk::DescriptorType::eStorageImage, 1}};
auto dpool = vk.device->createDescriptorPoolUnique(
{{vk::DescriptorPoolCreateFlagBits::eFreeDescriptorSet}, 2,
ps});
vk::DescriptorSetAllocateInfo dsai{};
dsai.setDescriptorPool(dpool.get()).setSetLayouts(cdslH);
auto dsets = vk.device->allocateDescriptorSetsUnique(dsai);
auto sampler = vk.device->createSamplerUnique(
{{},
vk::Filter::eNearest,
vk::Filter::eNearest,
vk::SamplerMipmapMode::eNearest,
vk::SamplerAddressMode::eClampToEdge,
vk::SamplerAddressMode::eClampToEdge,
vk::SamplerAddressMode::eClampToEdge});
vk::DescriptorImageInfo ia_{sampler.get(), imgA.view.get(),
vk::ImageLayout::
eShaderReadOnlyOptimal};
vk::DescriptorImageInfo ib_{VK_NULL_HANDLE, imgB.view.get(),
vk::ImageLayout::eGeneral};
vk::WriteDescriptorSet w[2];
w[0].setDstSet(dsets[0].get())
.setDstBinding(0)
.setDescriptorType(vk::DescriptorType::eCombinedImageSampler)
.setImageInfo(ia_);
w[1].setDstSet(dsets[0].get())
.setDstBinding(1)
.setDescriptorType(vk::DescriptorType::eStorageImage)
.setImageInfo(ib_);
vk.device->updateDescriptorSets(w, {});
// ---- 1コマンドバッファ:draw -> barrier -> blur -> 読み戻し ---
vkmini::Headless hl;
hl.init(vk, S, S);
auto cbuf = std::move(
vk.device->allocateCommandBuffersUnique(
{vk.pool.get(), vk::CommandBufferLevel::ePrimary, 1})
.front());
vk::CommandBuffer c = cbuf.get();
c.begin({vk::CommandBufferUsageFlagBits::eOneTimeSubmit});
// imgA: undefined -> カラーアタッチメント
vk::ImageMemoryBarrier b1{};
b1.setOldLayout(vk::ImageLayout::eUndefined)
.setNewLayout(vk::ImageLayout::eColorAttachmentOptimal)
.setSrcQueueFamilyIndex(VK_QUEUE_FAMILY_IGNORED)
.setDstQueueFamilyIndex(VK_QUEUE_FAMILY_IGNORED)
.setImage(imgA.img.get())
.setSubresourceRange(sr)
.setSrcAccessMask({})
.setDstAccessMask(
vk::AccessFlagBits::eColorAttachmentWrite);
c.pipelineBarrier(vk::PipelineStageFlagBits::eTopOfPipe,
vk::PipelineStageFlagBits::
eColorAttachmentOutput,
{}, {}, {}, b1);
vk::RenderingAttachmentInfo att{};
att.setImageView(imgA.view.get())
.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}, {S, S}})
.setLayerCount(1)
.setColorAttachments(att);
c.beginRendering(ri);
vk::Viewport v{0, 0, float(S), float(S), 0.f, 1.f};
c.setViewport(0, v);
vk::Rect2D sc{{0, 0}, {S, S}};
c.setScissor(0, sc);
c.bindPipeline(vk::PipelineBindPoint::eGraphics, gpipe.get());
c.draw(3, 1, 0, 0);
c.endRendering();
// imgA: アタッチメント -> シェーダー読み取り;imgB: undefined -> general
vk::ImageMemoryBarrier b2{};
b2.setOldLayout(vk::ImageLayout::eColorAttachmentOptimal)
.setNewLayout(vk::ImageLayout::eShaderReadOnlyOptimal)
.setSrcQueueFamilyIndex(VK_QUEUE_FAMILY_IGNORED)
.setDstQueueFamilyIndex(VK_QUEUE_FAMILY_IGNORED)
.setImage(imgA.img.get())
.setSubresourceRange(sr)
.setSrcAccessMask(
vk::AccessFlagBits::eColorAttachmentWrite)
.setDstAccessMask(vk::AccessFlagBits::eShaderRead);
vk::ImageMemoryBarrier b3{};
b3.setOldLayout(vk::ImageLayout::eUndefined)
.setNewLayout(vk::ImageLayout::eGeneral)
.setSrcQueueFamilyIndex(VK_QUEUE_FAMILY_IGNORED)
.setDstQueueFamilyIndex(VK_QUEUE_FAMILY_IGNORED)
.setImage(imgB.img.get())
.setSubresourceRange(sr)
.setSrcAccessMask({})
.setDstAccessMask(vk::AccessFlagBits::eShaderWrite);
std::array brs{b2, b3};
c.pipelineBarrier(vk::PipelineStageFlagBits::
eColorAttachmentOutput,
vk::PipelineStageFlagBits::eComputeShader, {},
{}, {}, brs);
c.bindPipeline(vk::PipelineBindPoint::eCompute, cpipe.get());
c.bindDescriptorSets(vk::PipelineBindPoint::eCompute,
clayout.get(), 0, dsets[0].get(), {});
c.dispatch(S / 16, S / 16, 1);
// imgB: general -> transfer src、読み戻しバッファにコピー
vk::ImageMemoryBarrier b4{};
b4.setOldLayout(vk::ImageLayout::eGeneral)
.setNewLayout(vk::ImageLayout::eTransferSrcOptimal)
.setSrcQueueFamilyIndex(VK_QUEUE_FAMILY_IGNORED)
.setDstQueueFamilyIndex(VK_QUEUE_FAMILY_IGNORED)
.setImage(imgB.img.get())
.setSubresourceRange(sr)
.setSrcAccessMask(vk::AccessFlagBits::eShaderWrite)
.setDstAccessMask(vk::AccessFlagBits::eTransferRead);
c.pipelineBarrier(vk::PipelineStageFlagBits::eComputeShader,
vk::PipelineStageFlagBits::eTransfer, {}, {},
{}, b4);
vk::BufferImageCopy r{};
r.setImageSubresource(
{vk::ImageAspectFlagBits::eColor, 0, 0, 1})
.setImageExtent(vk::Extent3D{S, S, 1});
c.copyImageToBuffer(imgB.img.get(),
vk::ImageLayout::eTransferSrcOptimal,
hl.readback.buf.get(), r);
c.end();
vk::SubmitInfo si{};
si.setCommandBuffers(c);
auto f = vk.device->createFenceUnique({});
vk.queue.submit(si, f.get());
(void)vk.device->waitForFences(f.get(), true, UINT64_MAX);
printf("wrote %s\n", ppmOut);
hl.savePpm(ppmOut);
return 0;
}このシリーズのすべてのソースコードは CC0 1.0 Universal (CC0-1.0) でリリース——パブリックドメイン献納、帰属表示 不要。
この記事が役に立った場合は、TechOverflow での新しい記事の調査と公開を支援するため、「Buy Me a Coffee」でのご支援、または PayPal 経由でのご寄付をご検討ください