【WebAssembly】WebGPU勉強中~初期化からメインループまで【WebGPU】

前回

はじめに

前回 WASM のビルド・確認まで出来たので、WebGPU で遊んでいこうと思います。

WebGPU C++ guide を参考に、理解しつつ C++ラッパーに変換しながら進めているので進捗は遅いです。

今回は初期化と、描画領域のクリアが目標です。

見た目は地味ですが、ここまで時間がかかりました ;-(

あと、main() 内に全て収めたいので、セオリーとは反した作りになっています。

CMake にオプション追加

WebGPU を利用できるよう CMakeLists.txt にオプションを追加します。

target_link_options( WebGPUTest PRIVATE
	-sUSE_WEBGPU # WebGPU を利用する
	-sASYNCIFY # WebGPU-C++ で必要
)

C++20 以降も使いたいので設定します。
C++23 は Visual Studio 2022 が中途半端に対応していて混乱するので諦めました。

CMakeLists.txt 全体です。

# cmakeのバーション設定
cmake_minimum_required( VERSION 3.13)

# C++20 を利用する
set( CMAKE_CXX_STANDARD 20)

# プロジェクト設定
project( "WebGPUTest")

add_executable ( WebGPUTest "WebGPUTest.cpp")

# リンクオプション
target_link_options( WebGPUTest PRIVATE
	-sUSE_WEBGPU # WebGPU を利用する
	-sASYNCIFY # WebGPU-C++ で必要
)

# html を出力する
set( CMAKE_EXECUTABLE_SUFFIX .html)

キャッシュを削除し再構成します。

アダプター・デバイスを取得する

まずは WebGPU のインスタンスを作ります。

wgpu::Instance gpuInstance = wgpu::CreateInstance();

次にアダプターを取得し、アダプターからデバイスを取得します。

多くの参考サイトでは 非同期呼び出し後 main() を終了しており、WASM では許容されます。が、私的には受け容れがたいため、取得するまで待つ作りにします。

非同期の取得待ちの部分も手こずりました。
C++を使えるとのことでスレッドを試してみましたが、どうやら通常の thread クラスや std::async など使えないようです。

emscripten 専用の threadシステムも用意されてますが、ブラウザーの対応がまちまちなので、
スレッドは諦めました。

結果 取得できるまで emscripten_sleep() で回すことにします。

Adapterの取得

// Adapter取得
wgpu::Adapter gpuAdapter{};
const wgpu::RequestAdapterOptions adapterOptions{};
gpuInstance.RequestAdapter(
	&adapterOptions,
	[]( WGPURequestAdapterStatus status, WGPUAdapter adapter, char const* message, void* userdata)
	{
		if( status != WGPURequestAdapterStatus_Success)
		{
			std::cerr << "Could not get WebGPU adapter: " << message << std::endl;
			exit( 0);
		}
		wgpu::Adapter& gpuAdapter = *reinterpret_cast<wgpu::Adapter*>( userdata);
		gpuAdapter = wgpu::Adapter::Acquire( adapter);
	},
	reinterpret_cast<void*>( &gpuAdapter)
);
// 取得できるまで回す
while( !gpuAdapter) emscripten_sleep( 1000/60);

Deviceの取得

// Device取得
wgpu::Device gpuDevice{};
wgpu::DeviceDescriptor deviceDescriptor{};
gpuAdapter.RequestDevice(
	&deviceDescriptor,
	[]( WGPURequestDeviceStatus status, WGPUDevice device, const char* message, void* userdata)
	{
		if( status != WGPURequestDeviceStatus_Success)
		{
			std::cerr << "Could not get WebGPU device: " << message << std::endl;
			exit( 0);
		}
		wgpu::Device& gpuDevice = *reinterpret_cast<wgpu::Device*>( userdata);
		gpuDevice = wgpu::Device::Acquire( device);
	},
	reinterpret_cast<void*>( &gpuDevice)
);
// 取得できるまで回す
while( !gpuDevice) emscripten_sleep( 1000/60);

取得されると、コールバック(今回はラムダ式)が呼ばれ、引数 void* userdata を取得する変数の参照に変換し、Acquire() で設定しています。

呼び出し元 main() では取得できるまで sleep しながら待ってます

エラーハンドルを受け取れるようにする

Deviceがエラーを起こした場合にキャッチできるように設定します。
何かしらエラーが起きたら飛んで来ます。

// エラーハンドルを受け取れるようにする
gpuDevice.SetUncapturedErrorCallback(
	[](WGPUErrorType type, char const* message, void* userdata)
	{
		std::cout << type << ": " << message << std::endl;
		exit( 0);
	},
	nullptr
);

コマンドキュー取得

今回はやりませんが、後々頂点情報などをキューに追加するのでこのタイミングでコマンドキューを取得しておきます。

// コマンドキューの取得
wgpu::Queue queue = gpuDevice.GetQueue();

スワップチェーン作成

「スワップチェーン」という用語を知らなく「?」となりましたが、レンダリングバッファ管理やフリップを扱うAPIという感じでしょうか?

描画サーフェスを作成します。

HTML canvas 要素に適切なサーフェスを作ります。

// 描画サーフェス作成
wgpu::SurfaceDescriptorFromCanvasHTMLSelector surfaceDescriptorFromCanvasHTMLSelector{};
surfaceDescriptorFromCanvasHTMLSelector.selector = "#canvas";
wgpu::SurfaceDescriptor surfaceDescriptor
{
	.nextInChain = &surfaceDescriptorFromCanvasHTMLSelector,
};
wgpu::Surface gpuSurface = gpuInstance.CreateSurface( &surfaceDescriptor);

サーフェスからスワップチェーンを作成します。

// スワップチェーン作成
wgpu::SwapChainDescriptor swapChainDescriptor
{
	.usage = wgpu::TextureUsage::RenderAttachment,
	.format = gpuSurface.GetPreferredFormat( gpuAdapter),
	.width = 640,
	.height = 480,
	.presentMode = wgpu::PresentMode::Fifo,
};
wgpu::SwapChain gpuSwapChain = gpuDevice.CreateSwapChain( gpuSurface, &swapChainDescriptor);

深度バッファ・ステンシルバッファ作成

今回は必須ではないですが、深度バッファ・ステンシルバッファを作っておきます。

// 深度・ステンシルバッファ作成
wgpu::TextureDescriptor textureDescriptor
{
	.usage = wgpu::TextureUsage::RenderAttachment,
	.size = wgpu::Extent3D
	{
		.width = 640,
		.height = 480,
		.depthOrArrayLayers = 1,
	},
	.format = wgpu::TextureFormat::Depth24PlusStencil8,
};
wgpu::Texture textureDepthStenci = gpuDevice.CreateTexture( &textureDescriptor);
wgpu::TextureView textureViewDepthStenci = textureDepthStenci.CreateView();

メインループ

本来は emscripten_set_main_loop() を使って モニターやブラウザーの適切なタイミングでレンダリングバッファをスワップさせるのが定石のようですが、今回は main() 内にすべて収めたいため 多少変則的なメインループさせます。

本来は emscripten_set_main_loop を使って loop_func を毎フレーム呼びます。

emscripten_set_main_loop(
	loop_func,
	0,
	false
);

今回のメインループのイメージです。

while( true)
{
	…処理
	emscripten_sleep( 1000/60);
}

まずコマンドエンコーダを取得します。

wgpu::CommandEncoder gpuCommandEncoder = gpuDevice.CreateCommandEncoder();

レンダリングビューを取得します。
取得出来なかった場合は、異常終了します。

wgpu::TextureView gpuTextureView = gpuSwapChain.GetCurrentTextureView();
if( !gpuTextureView)
{
	std::cerr << "Cannot acquire next swap chain texture" << std::endl;
	exit( 0);
}

レンダリングバッファ、深度バッファのクリア設定します。

wgpu::RenderPassColorAttachment renderPassColorAttachment
{
	.view = gpuTextureView,
	.resolveTarget = nullptr,
	.loadOp = wgpu::LoadOp::Clear,
	.storeOp = wgpu::StoreOp::Store,
	.clearValue = {0, 0.5f, 0, 1},
};
wgpu::RenderPassDepthStencilAttachment renderPassDepthStencilAttachment
{
	.view = textureViewDepthStenci,
	.depthLoadOp = wgpu::LoadOp::Clear,
	.depthStoreOp = wgpu::StoreOp::Store,
	.depthClearValue = 1.0f,
	.depthReadOnly = false,
	.stencilLoadOp = wgpu::LoadOp::Clear,
	.stencilStoreOp = wgpu::StoreOp::Store,
	.stencilClearValue = 0,
	.stencilReadOnly = false,
};
wgpu::RenderPassDescriptor renderPassDescriptor
{
	.colorAttachmentCount = 1,
	.colorAttachments = &renderPassColorAttachment,
	.depthStencilAttachment = &renderPassDepthStencilAttachment,
};

設定内容でバッファをクリアし、パスエンコーダーを取得します。

この パスエンコーダーにコマンドを積んでいくことでプリミティブの描画などを行うのですね。なるほど、なるほど。

パスにコマンドを入れ終わったらEnd() します。今回は何も登録してません。

wgpu::RenderPassEncoder pass = gpuCommandEncoder.BeginRenderPass( &renderPassDescriptor);
pass.End();

後は GPU にコマンドを転送し実行してもらいます。

wgpu::CommandBuffer commands = gpuCommandEncoder.Finish();
queue.Submit( 1, &commands);

メインループの全体です。

while( true)
{
	wgpu::CommandEncoder gpuCommandEncoder = gpuDevice.CreateCommandEncoder();

	// 描画領域のクリア
	wgpu::TextureView gpuTextureView = gpuSwapChain.GetCurrentTextureView();
	if( !gpuTextureView)
	{
		std::cerr << "Cannot acquire next swap chain texture" << std::endl;
		exit( 0);
	}
	wgpu::RenderPassColorAttachment renderPassColorAttachment
	{
		.view = gpuTextureView,
		.resolveTarget = nullptr,
		.loadOp = wgpu::LoadOp::Clear,
		.storeOp = wgpu::StoreOp::Store,
		.clearValue = {0, 0.5f, 0, 1},
	};
	wgpu::RenderPassDepthStencilAttachment renderPassDepthStencilAttachment
	{
		.view = textureViewDepthStenci,
		.depthLoadOp = wgpu::LoadOp::Clear,
		.depthStoreOp = wgpu::StoreOp::Store,
		.depthClearValue = 1.0f,
		.depthReadOnly = false,
		.stencilLoadOp = wgpu::LoadOp::Clear,
		.stencilStoreOp = wgpu::StoreOp::Store,
		.stencilClearValue = 0,
		.stencilReadOnly = false,
	};
	wgpu::RenderPassDescriptor renderPassDescriptor
	{
		.colorAttachmentCount = 1,
		.colorAttachments = &renderPassColorAttachment,
		.depthStencilAttachment = &renderPassDepthStencilAttachment,
	};
	// レンダリングパス取得
	wgpu::RenderPassEncoder pass = gpuCommandEncoder.BeginRenderPass( &renderPassDescriptor);
	pass.End();

	// GPUにコマンド転送
	wgpu::CommandBuffer commands = gpuCommandEncoder.Finish();
	queue.Submit( 1, &commands);

	// 1/60秒待機
	emscripten_sleep( 1000/60);
}

コード全体

ひとまず初期化と、メインループが完成しました。
今回はプログラムの流れを把握しやすいよう可能な限り 逐次処理プログラムにしてあります。
基礎が整うまで結構時間がかかりましたね。

#include <iostream>
#include <webgpu/webgpu_cpp.h>
#include <emscripten.h>

//----------------------------------------------------------------------------
int main()
{
	wgpu::Instance gpuInstance = wgpu::CreateInstance();

	// Adapter取得
	wgpu::Adapter gpuAdapter{};
	const wgpu::RequestAdapterOptions adapterOptions{};
	gpuInstance.RequestAdapter(
		&adapterOptions,
		[]( WGPURequestAdapterStatus status, WGPUAdapter adapter, char const* message, void* userdata)
		{
			if( status != WGPURequestAdapterStatus_Success)
			{
				std::cerr << "Could not get WebGPU adapter: " << message << std::endl;
				exit( 0);
			}
			wgpu::Adapter& gpuAdapter = *reinterpret_cast<wgpu::Adapter*>( userdata);
			gpuAdapter = wgpu::Adapter::Acquire( adapter);
		},
		reinterpret_cast<void*>( &gpuAdapter)
	);
	// 取得できるまで回す
	while( !gpuAdapter) emscripten_sleep( 1000/60);

	// Device取得
	wgpu::Device gpuDevice{};
	wgpu::DeviceDescriptor deviceDescriptor{};
	gpuAdapter.RequestDevice(
		&deviceDescriptor,
		[]( WGPURequestDeviceStatus status, WGPUDevice device, char const* message, void* userdata)
		{
			if( status != WGPURequestDeviceStatus_Success)
			{
				std::cerr << "Could not get WebGPU device: " << message << std::endl;
				exit( 0);
			}
			wgpu::Device& gpuDevice = *reinterpret_cast<wgpu::Device*>( userdata);
			gpuDevice = wgpu::Device::Acquire( device);
		},
		reinterpret_cast<void*>( &gpuDevice)
	);
	// 取得できるまで回す
	while( !gpuDevice) emscripten_sleep( 1000/60);

	// エラーハンドルを受け取れるようにする
	gpuDevice.SetUncapturedErrorCallback(
		[](WGPUErrorType type, char const* message, void * userdata)
		{
			std::cout << type << ": " << message << std::endl;
			exit( 0);
		},
		nullptr
	);

	// コマンドキューの取得
	wgpu::Queue queue = gpuDevice.GetQueue();

	// 描画サーフェス作成
	wgpu::SurfaceDescriptorFromCanvasHTMLSelector surfaceDescriptorFromCanvasHTMLSelector{};
	surfaceDescriptorFromCanvasHTMLSelector.selector = "#canvas";
	wgpu::SurfaceDescriptor surfaceDescriptor
	{
		.nextInChain = &surfaceDescriptorFromCanvasHTMLSelector,
	};
	wgpu::Surface gpuSurface = gpuInstance.CreateSurface( &surfaceDescriptor);

	// スワップチェーン作成
	wgpu::SwapChainDescriptor swapChainDescriptor
	{
		.usage = wgpu::TextureUsage::RenderAttachment,
		.format = gpuSurface.GetPreferredFormat( gpuAdapter),
		.width = 640,
		.height = 480,
		.presentMode = wgpu::PresentMode::Fifo,
	};
	wgpu::SwapChain gpuSwapChain = gpuDevice.CreateSwapChain( gpuSurface, &swapChainDescriptor);

	// 深度・ステンシルバッファ作成
	wgpu::TextureDescriptor textureDescriptor
	{
		.usage = wgpu::TextureUsage::RenderAttachment,
		.size = wgpu::Extent3D
		{
			.width = 640,
			.height = 480,
			.depthOrArrayLayers = 1,
		},
		.format = wgpu::TextureFormat::Depth24PlusStencil8,
	};
	wgpu::Texture textureDepthStenci = gpuDevice.CreateTexture( &textureDescriptor);
	wgpu::TextureView textureViewDepthStenci = textureDepthStenci.CreateView();

	// メインループ
	while( true)
	{
		wgpu::CommandEncoder gpuCommandEncoder = gpuDevice.CreateCommandEncoder();

		// 描画領域のクリア
		wgpu::TextureView gpuTextureView = gpuSwapChain.GetCurrentTextureView();
		if( !gpuTextureView)
		{
			std::cerr << "Cannot acquire next swap chain texture" << std::endl;
			exit( 0);
		}
		wgpu::RenderPassColorAttachment renderPassColorAttachment
		{
			.view = gpuTextureView,
			.resolveTarget = nullptr,
			.loadOp = wgpu::LoadOp::Clear,
			.storeOp = wgpu::StoreOp::Store,
			.clearValue = {0, 0.5f, 0, 1},
		};
		wgpu::RenderPassDepthStencilAttachment renderPassDepthStencilAttachment
		{
			.view = textureViewDepthStenci,
			.depthLoadOp = wgpu::LoadOp::Clear,
			.depthStoreOp = wgpu::StoreOp::Store,
			.depthClearValue = 1.0f,
			.depthReadOnly = false,
			.stencilLoadOp = wgpu::LoadOp::Clear,
			.stencilStoreOp = wgpu::StoreOp::Store,
			.stencilClearValue = 0,
			.stencilReadOnly = false,
		};
		wgpu::RenderPassDescriptor renderPassDescriptor
		{
			.colorAttachmentCount = 1,
			.colorAttachments = &renderPassColorAttachment,
			.depthStencilAttachment = &renderPassDepthStencilAttachment,
		};
		// レンダリングパス取得
		wgpu::RenderPassEncoder pass = gpuCommandEncoder.BeginRenderPass( &renderPassDescriptor);
		pass.End();

		// GPUにコマンド転送
		wgpu::CommandBuffer commands = gpuCommandEncoder.Finish();
		queue.Submit( 1, &commands);

		// 1/60秒待機
		emscripten_sleep( 1000/60);
	}

	return 0;
}

最後に

WASM 特有の仕様や、C++23 が中途半端に使え惑わされたりとかなり手探り状態でした。

土俵が出来たので、後は好きにできそうです。

↓次回はプリミティブを表示・動かします。

追記

動作確認報告

動作確認できたのは

  • Windows Chrome
  • Windows Edge
  • Mac Chrome

起動不可は

  • Windows Firefox
  • Mac Safari
  • Mac Firefox
  • ios Chrome
  • ios Safari

Firefox は dom.webgpu.enabled を有効にしても wgpu::Instance.RequestAdapter() から Adapter が取得できず進まない。
それ以外は深く調べてません。

Win/Mac で使うなら Chrome 一択で、ios も含めると WebGPU はまだまだ発展途上と言わざる得ないです。
それに比べ WebGL はすべての環境で動作確認できてます。
現状 WebGPU で何かサービスを展開するのは無謀で、趣味で遊ぶ以外使い道は少なそうです。

各ブラウザーの対応を待ちましょう。

One Comment