Rust のメモリレイアウトなどの話

はじめに

以前 twitter で書いてたことを整理していなかったので、改めて見直し&整理をしようとしていたのですが、その過程で知らなかった事があれこれ出てきたので合わせてまとめてみました。

Representation

割と重大な事が書いてありました。

C など他言語から考えると、構造体のフィールド定義は上から順番にメモリが配置されるものですが、 Rust では struct, enum, union ではフィールドの再配置 (Representation) が行われます。

このうち Default Representation の "repr(Rust)" (とコード上で記述する事はないですが、便宜上そう表現するようです) ではメモリ効率を考慮して Rust コンパイラが適切な順番に並べ替えをするようです。

これに対し C Representation の "repr(C)" は C との連携のため、 C の仕様に準拠できるように再配置されます。つまり並び替えがされません。

実際にこの二つを比較してみます。

pub struct StructI1 {
    a: i8,
    b: i16,
    c: i32,
}

#[repr(C)]
pub struct StructI2 {
    a: i8,
    b: i16,
    c: i32,
}

let a = StructI1 { a: 7, b: 8, c: 9 };
let b = StructI2 { a: 7, b: 8, c: 9 };

これをデバッガから a (StructI1) と b (StructI2) の内容を観察してみます。

f:id:tan-y:20200919013222p:plain

右上の StructI1 は 値が 9, 8, 7 となっていて順番が入れ替わっています。一方 repr(C) を付与した StructI2 は 7, 8, 9 とそのままの順番になっています。

null pointer optimization

  • std::mem::size_of::<&i64>
  • std::mem::size_of::<Option::<&i64>>

この二つのサイズが同じになるのも repr(Rust) の再配置の一環で "null pointer optimization" という仕組みになります。 repr(Rust) のドキュメント によると

  • 値のもつ Rust の列挙型の場合、アイテム (variant) とデーター (data) で構成される
  • Option のようなデーターの持たないもの (None) とデーターを持つがそのデーターが null にならないもの (Some(&i64) など) の 2 種類で構成されている enum の場合、 null 値が variant の代わりになる (区別ができる) ので variant の分の領域を省略する

となります。また、これは Option の特例的な最適化ではなく、同じような構成にすれば独自に定義した enum でも適用されます。

また、 Option:: の T が参照だけではなく、関数ポインタ、 Box 、 Vec 、 String など null を持たない参照 (ポインタ) 型が該当します。この "null を持たない" の条件は内部管理が Unique 型で行っているものが該当しているようです。

一方、 * (ポインタ) は null がありえるので該当しません。

  • std::mem::size_of::<*const i64>
  • std::mem::size_of::<Option::<*const i64>>

この二つはサイズが異なります。

packed / align

どちらも Representation でレイアウト調整を指示するものです。 packed が使用領域を詰める方向に調整し、 align はアライメントを合わせる (使用領域を広げる) 方向に調整する違いがあります。一般的には repr(C) と合わせて使うかなと思いますが Default Representation の場合も有効です。

enum の representations

enum の representation は C と "Primitive representations" が使えます。

repr(C) は「C 言語と互換性を持つ」なので int 型と同じサイズになるかと思いますが、 C の int 型は処理系依存なのでちょっと曖昧な感じがします。

"Primitive representations" は "repr(整数型名)" と記述します (repr(i32) など) 。 C++ の scoped enum など幅が明確な場合はこちらを使用した方がよいかと思います。

これらはデーターを持たせない enum で有効で、データーを持たせると指定は無視され enum の Default Representation が適用されます。

関数ポインタの扱い

ドキュメントには見当たらなかったのですが下記のような感じでした。

  • 関数は関数ポインタ変数に格納できる
  • 関数ポインタは null を持てない
  • 関数ポインタは普通のポインタにキャストできる
fn sum(a:i32, b:i32) -> i32 {
    a + b
}

let p = sum as *const();
  • 普通のポインタから関数ポインタへのキャストは transmute で行う (null がありえるので Option にした方が望ましい)
let f = std::mem::transmute::<_, Option::<fn(i32, i32) -> i32>>(p);
  • Rust の関数ポインタと C の関数ポインタは同じ (呼び出し規約を合わせる必要はある)
  • trait object は fat pointer なので関数ポインタとは互換性がない (参照: トレイトオブジェクト)

おわりに

推測交じりの記事になってしまいましたが、現時点の理解でのまとめということで。

Default Representation は具体的な挙動がよくわからないのでコンパイラのバージョン間の互換性とか若干不安になるのですが、

ABI は安定してないみたいなので気にしたら負けっぽいです。ライブラリも原則ソースコードからのビルドですし特に問題ない。どうしても気にするなら Rust 内であっても repr(C) を使用する、ということですね。

Rust で Vulkan を使う

はじめに

思い立って Vulkan の勉強をすることにしたのですが、 Rust 力がまだまだなので「Rust で Vulkan のコード書きながら勉強すれば一石二鳥では・・・?」と考え実行してみた件についてのまとめ記事です。

上記リポジトリで使用した crate の紹介などをしていきます。

Vulkan の Rust Binding を導入する

Rust は「~を Rust から使ってみたい」と思って検索すると大体なんとかなるケースが多く、 C/C++ の主要な SDK 、ライブラリには割と高い確率で Binding ライブラリが creates.io に存在しています。

今回勉強にあたって使ってみた Vulkan の Binding は下記のものです。

  • Vulkano
  • Ash

他にもいくつか存在しているようですが、この二つが最も使われているもののようです (crates.io 調べ) 。

API のレベル (高 > 低) は個人的な感覚として

Vulkano > vulkan.hpp (C++) > Ash > vulakn.h (C)

の順かなという感じがしました。

Vulkano

Vulkano は比較的「高レベル API」のようです。ラッパーも Arc で管理されていて、インスタンスの破棄に関しては基本的に配慮しなくてもよいようになっています。初期化もある程度隠蔽化されていて比較的手続きが少なく書けそうです。 unsafe も原則的には出てきません。今回は試していませんが、 Rust のコードの中に記述したシェーダーのコンパイルをするマクロの定義もあるようです。

Vulkan の知識があって、 Rust で Vulkan のコードを書くのであれば面倒な部分をある程度簡略化できて便利そうですが、 Vulkan の全てをコントロールしたい、といった場合は向かないかもしれません。また、公式の C/C++ API との差異により、 Vulkan の知識がない状態から参考書を参考に勉強を始めると差異の吸収に手間取るかもしれません。

Ash

Ash は "A very lightweight wrapper" を謳っているくらいなので非常に薄いラッパー API を提供しています。

No validation, everything is unsafe

と書かれており、ほとんどの API は unsafe ブロックの中で使用する必要があります。 drop trait の実装もされていないのでインスタンス管理も自分で行う必要があります。

基本的に Vulkan の API をそのまま Rust で見える形にしたものに近く、 C/C++ のコードをそのまま Rust で書き直すのもそれほど難しくはないように思います。 API 的には C++ の vulkan.hpp に近く、各種構造体は Builder pattern で設定できるので C API ほど面倒くさいという事もないように思いました。 vulkan.hpp からデストラクタがなくなった?というのが近いかなという感じです。

drop trait が実装されていないのが面倒に感じるかもしれませんが、 Vulkan Memory Allocator のようなリソースマネージャーと組み合わせて使う場合は固定的な drop trait は実装されていると逆に困ってしまうことになるので、これはこれでよいかなと思います。

その他のライブラリ

scopeguard

スコープを抜ける際に実行するコードを記述できるようにする crate です。 C++ ではお馴染みだと思います。 Swift の defer ですね。

Ash では drop trait がないので後始末コードは全て自前で書く必要があります。ある程度規模のあるコードであれば管理用の構造体に drop trait を実装すればよいと思いますが、学習コードなどで 1 メソッド内で完結してしまう場合はなかなか面倒です。そういった場合にこの crate を使うと後始末処理を細かく書く必要がなくなって楽です。

この scopeguard crate は defer! マクロという、 Swift の defer そのままの使い勝手を実現できるようになっているのでとても楽です。

let device = unsafe { instance.create_device( /*略*/ ).unwrap() };

defer! {
    unsafe { device.destroy_device(None); }
}

nalgebra

glmath の Rust 版という観点から nalgebra の中の nalgebra-glm を使用してみました。後から調べてみると cgmath-rs を使ってみてもよかったかもとは思いました。

vk-mem

Vulkan Memory Allocator の Rust Binding です。 vk-mem 自体が Ash に依存しているので Vulkano との組み合わせは難しいのではないかと思います (Vulkano をあきらめた切っ掛けでもあります) 。

glfw-rs

OpenGL / Vulkan のデスクトップアプリを簡単に作れる GLFW の Rust Binding です。

今回は参考書のコードを極力改変せずにそのまま Rust で書き直す、としていたので使いましたが、 GLFW を使う強い理由がないのであれば winit を使った方が Rust では簡単かもしれません。

おわりに

「Rust で Vulkan の勉強をする」はこれから習得しようとしている Vulkan という技術自体に加えてその Binding ラッパーの使い方も把握しないといけないのでどうしても遠回りになってはしまいます。当初はその辺から戸惑ってしまいましたが、 Ash は差異も小さかったので使い方をつかんできてからは Rust と Vulkan の勉強の両立っぽくできてきたかなあという感じはあります。

とはいっても Rust のコードはそこそこ書けたのでよかったですが、 Vulkan の理解はまだ全然なので継続していこうと考えています (やりたい事への達成にはまだまだ遠い・・・) 。

C/C++ だと各種 SDK などの事前準備がちょっと面倒ですが、 Rust は Cargo が強力なので始めるまでが圧倒的に楽なのはとてもよいです。

Ash は vulkan.hpp とほとんど変わらないような感じですし、リソースの破棄は些末な事と思いますので個人的には Ash をメインで使っていくことになるかなあと思います。

Rust/WinRT を試す (その 2)

はじめに

前回記事以降も順調にアップデートがされているようなので改めて調べてみました。

crates.io に登録

https://crates.io/crates/winrt

crates.io に登録されましたので、 Cargo.toml で GitHub の Git を参照するのではなく、 crates.io の登録バージョン指定で参照できるようになりました。

[dependencies]
winrt = "0.7.1"

build.rs 対応

前回記事で

端的に言うとコード補完がほぼききません。これは WindowsAPI 部分が winmd に定義されているためと思われます。

と書いた点が解消されそうな感じになってきました。

bindgen と同じように build.rs でビルド時に winmd から静的なコード生成をします。これにより IDE から binding コードの参照が可能になりました。

現時点では README.md 等にも記載がないのですが、次のようにすることで利用できました。

Cargo.toml の設定

ビルドプロセスで処理するので build-dependencies にも定義が必要になります。

[dependencies]
winrt = "0.7.1"

[build-dependencies]
winrt = "0.7.1"

build.rs の設定

プロジェクト直下 (Cargo.toml と同じ位置) に build.rs を作成し、次のコードを記述します。

winrt::build!(
    dependencies
        os
    types
        windows::data::xml::dom::*
        windows::foundation::*
        windows::ui::*
);

fn main() {
    build();
}

winrt::build マクロの中身は従来の import マクロのものと同じです。

main.rs / lib.rs の設定

コード側には include 定義を記述します。 main.rs か lib.rs の先頭に下記記述を追加します。

include!(concat!(env!("OUT_DIR"), "/winrt.rs"));

import マクロ定義は削除してください。

これで一回ビルドすると binding コードが生成されてエディッター上から参照できるようになります。

f:id:tan-y:20200721005032p:plain

前回を思うと結構感動します。ただ私の環境では VSCode ではうまく参照ができない事も多く、 CLion では全く見えませんでした。この辺りは IDE 側でも対応を進める必要があるのかもしれません。

NuGet 対応

NuGet からの import も可能になったようです。ただこれは若干特殊な操作を必要とします。

これは実装例が winrt-rs 下にあるのでそちらを参照してください。

まず "cargo winrt" というコマンドを使えるようにする必要があります。

$ cargo install cargo-winrt

※ cargo-winrt も crates.io に登録されています

NuGet も build.rs での前処理になるので Cargo.toml の dependencies, build-dependencies に winrt の参照を設定します。また、それに加えて参照する NuGet パッケージの定義もします。

[package.metadata.winrt.dependencies]
"Microsoft.AI.MachineLearning" = "1.3.0"
"Microsoft.Windows.SDK.Contracts" = "10.0.19041.1"

build.rs でも参照する NuGet のパッケージを指定していきます。

winrt::build!(
    dependencies
        nuget: Microsoft.AI.MachineLearning
        nuget: Microsoft.Windows.SDK.Contracts
    types
        microsoft::ai::machine_learning::*
        windows::foundation::Uri
);

ビルドは通常の "cargo build" ではなく、 cargo-winrt を通して "cargo winrt build" で行います。 NuGet の依存解決のために Cargo に拡張を加えたのではないかと思います。これだと IDE 上からショートカットキーでビルド、というわけにもいかなくなってしまうので (この方針で確定なら) IDE 側にも対応が求められそうです。

おわりに

(個人的に) 最大の懸案だった winmd の binding コードへの参照が可能になってきているのでなんとか使えそうになってきたかなあという感じです。開発もアクティブに進められているようなのでこれからも状況を見ていきたいと思います。

Windows で Unity as a Library

はじめに

Windows でも Unity as a Library ができる、と Twitter で見かけたので試してみました。

unity.com

Windows では Unity 2019.3 以降での対応のようです。この記事では Unity 2019.4.1f1 で行っています。

ホスト側は .NET Core 3.1 + Windows Forms で作りました。

f:id:tan-y:20200625073117p:plain
動作例 / © UTJ/UCL

github.com

手順

Windows でやる場合、方法が 3 通りあります。

  • a) Windows Standalone / Unity コードを外部プロセスとして起動する
  • b) Windows Standalone / Unity コードを DLL として自身のプロセスにロードする
  • c) UWP

ここでは a) と b) について取り上げます。

まず適当に Unity のプロジェクトを作成し、 Windows Standalone でビルドしておきます。

Unity コードを外部プロセスとして起動する

Unity のアプリを起動する際のコマンドラインオプションに "-ParentHWND" が追加されています。このオプションで親ウィンドウとなるウィンドウのハンドルを指定します。ウィンドウの親子関係は異なるプロセス間でも成立できます (これができないとウェブブラウザのプロセス分離ができないことになります) 。

原理的にはこれだけなのですが、実際には親ウィンドウの状態変更に応じた処理 (リサイズなど) が必要になります。 Unity によるサンプル を参考に汎用的な対応コードを実装しました。

  • UnityLibrary.cs
    • Unity のウィンドウハンドル取得
    • Unity ウィンドウのリサイズ
    • Unity ウィンドウの有効化 / 無効化
    • Unity ウィンドウのクローズ

Unity コードを DLL として自身のプロセスにロードする

Unity の実行ファイル本体は UnityPlayer.dll になります。ビルド時に生成されている .exe ではなく、自前で UnityPlayer.dll をロードし、エントリーポイントを呼び出す事でも Unity アプリを起動することができます。これ自体は以前から可能のようですが、ここで指定する起動パラメーターに先ほどの -ParentHWND を指定することで親ウィンドウをした上で起動することができるようになっています。

こちらの手法ではインプロセス実行になるため理屈的には Unity 側とホスト側でメモリ (ポインタ) を共有できることになります (実際にはどうやってポインタ値の受け渡しをするのか検討の必要があります) 。

UnityPlayer.dll をロードする場合、いくつか事前準備が必要になります。

必要なファイルのコピー

まず、ビルドした Unity アプリケーションをホスト側の実行ファイルと同じ位置にコピーする必要があります。厳密には Unity アプリの実行時は カレントディレクトリが Unity アプリと同じになっていなくてはならない という制約に準拠するためです。外部プロセス実行の場合はどこに置いてもそのプロセス下での制約になるのでホスト側は影響を受けません。

また、コピーの際は "???_Data" というフォルダはコピー後に ??? の部分をホスト側の実行ファイル名と同じものにリネームする必要があります。例えば

  • Unity ビルドの実行ファイル名: UnityApp.exe
  • ホストの実行ファイル名: Win32.exe

だった場合 "UnityApp_Data" というフォルダをコピー先で "Win32_Data" にリネームする必要があります。

UnityPlayer.dll のロード ~ 実行

delegate int FnUnityMain(IntPtr hInstance, IntPtr hPrevInstance, [MarshalAs(UnmanagedType.LPWStr)] string lpCmdLine, int nShowCmd);

DLL をロードして "UnityMain" のエントリーポイントを取得、実行をします。

var p = GetProcAddress(_dll, "UnityMain");
var fnUnityMain = Marshal.GetDelegateForFunctionPointer<FnUnityMain>(p);
fnUnityMain(Process.GetCurrentProcess().Handle, IntPtr.Zero, arg, 1);

arg には "-ParentHWND" で親ウィンドウを指定するオプションを設定します。

Unity - ホスト間で通信をする

通常の Win32 ウィンドウの中に Unity アプリを表示することができるようになりました。しかし、これだけだと単に表示がされるだけで統合がされているような感じはしません。組み込むからにはホスト側から Unity の操作をしたり、 Unity の情報をホスト側に伝えたくなるわけです。

iOS ではそういうルートも用意されているっぽい ですが Windows では特にないので、別の方法を考えます。

ということで今回は MagicOnion を使ってみました。 MagicOnion は gRPC を通信基盤としているもののインターフェース定義を全て C# で行えるので使い勝手がとてもよいです。

MagicOnion の導入はこちらの記事がとても参考になります。ありがとうございます。

qiita.com

今回は Unity がライブラリ、つまり WinForms → Unity という呼び出しが主になりますが、 Unity は MagicOnion のサーバーになれないので WinForms 側をサーバーに仕立てます。この構成だと "サーバー → クライアント" という呼び出しが主になってしまいます。 MagicOnion は "サーバー → クライアント" 通信では登録済クライアントに対するブロードキャストのみですが、今回は 1 対 1 通信と決まっているのでブロードキャストでも結果ほぼ変わらないのでまあいいかなと。

public interface IUnityChanControllerReceiver
{
    void SetAnimation(AnimeType animeType);
    void SetMessageText(string msg);
}

public interface IUnityChanController : IStreamingHub<IUnityChanController, IUnityChanControllerReceiver>
{
    Task RegisterAsync();
    Task UnregisterAsync();

    Task SetAnimationAsync(AnimeType animeType);
    Task SetMessageTextAsync(string msg);
}

これに対する IUnityChanController (WinForms 側) の実装は次のようにしました。

private IGroup _group;

async Task IUnityChanController.RegisterAsync()
{
    _group = await Group.AddAsync("UnityLibrary");
}

Task IUnityChanController.UnregisterAsync()
{
    return _group?.RemoveAsync(Context).AsTask();
}

Task IUnityChanController.SetAnimationAsync(AnimeType animeType)
{
    Broadcast(_group).OnSetAnimation(animeType);
    return Task.CompletedTask;
}

Task IUnityChanController.SetMessageTextAsync(string msg)
{
    Broadcast(_group).OnSetMessageText(msg);
    return Task.CompletedTask;
}

SetAnimationAsync, SetMessageTextAsync は対応する Receiver に Broadcast でそのままたらしい回しをします。

WinForms 側の Receiver は何もしない空実装であるのに対し、 Unity 側はアニメーション制御だったり UI の更新をしたりしています。 使う時は Unity / WinForms 双方から StreamingHubClient.Connect で接続し、 RegisterAsync で Group の登録をして Broadcast の送受信をできるようにしておきます。実際に Broadcast を受けて何かするのは Unity 側のみ、ということになります。

WinForms の UI から SetAnimationAsync, SetMessageTextAsync を呼び出せば結果的に Unity 側の処理が呼び出されるという構造になりました。

おわりに

Windows での Unity as a Library は実際提供されている機能的には極々限られていて、特に標準で通信手段が用意されていないのはなかなかつらいものがあります。 Unity との通信には MagicOnion を使ってみましたが、 Broadcast を使うしかないですし実際はあんまりお勧めできないかと思います。他の方法といってもパイプとか泥臭い方法になりそうですが・・・

実際のアプリ開発でこの機能を利用しようとした場合、 Unity 側とホスト側で完全に分離してしまうので作業がなかなか大変なことになってしまうとは思います。そこまでしてネイティブ UI を使いたい場合は一考の余地ありといったところでしょうか。

Rust/WinRT を試す

はじめに

先日 Microsoft から preview がリリースされた Rust で WinRT API を使用できるようにするためのものです。 C++/WinRT の Rust 版、みたいなもののようです。 Microsoft は割と Rust に本気なのかもしれません。

個人的にかなり気になるものなので試してみました。

crate.io には以前から WinRT の Binding ライブラリがありますが、こちらとは別のものです。

WinRT とは

あまりご存じない方もいるかもしれないので軽く触れておきます。

WinRT は正確には "Windows Runtime" という Windows 8 と合わせて登場した (当時) 新しい API セットの総称で、現在 UWP と言われている "Windows ストアアプリ (Metro スタイルアプリ)" を開発するために提供されたのが最初でした。

ちなみに Windows 8 の ARM 版の名前が "Windows RT" であったため略称にするとどっちの事を言っているのよくわからないという問題がありました。

基本的な特徴としては

  • COM をベースにしたネイティブ API である (.NET ではない)
  • API 定義はテキスト (ヘッダーファイル) ではなくバイナリ (Windows Metadata / winmd) で提供される

かと思います (他にも非同期とか UI とかありますが、根幹はこの二つかなと) 。

winmd は COM 的には Type Library に相当するもので、バイナリ構造としては .NET と互換性のあるものとなっています。各言語から WinRT の API にアクセスする場合、この winmd からインターフェース情報を取得し、相互運用を実現しています。

Rust/WinRT を導入する

README.md の "Getting started" に書かれている通りです。

[dependencies]
winrt = { git = "https://github.com/microsoft/winrt-rs" }

Cargo.toml に winrt の crate を参照に追加するだけです。注意点として toolkit が msvc である必要があります。 gnu では通りません。これは UWP 用のバイナリをビルドするので当然かなあと思ったのですが、そういう理由ではなさそうです。

winmd ファイルのあるパスへの参照が解決できないため、というのが理由のようです。まあ確かに API 的に WinRT 互換で winmd で API 定義が公開されているならコンパイル時にはプラットフォーム依存はしていないと言えるのもしれません。いずれにせよ、現時点では toolkit に制約がありますので注意してください。

とりあえず README.md のサンプルをコピペしてビルド、実行をしてみると、動作することは確認できます。

コードを書いてみる

・・・ちょっと、いや大分厳しい。

端的に言うとコード補完がほぼききません。これは WindowsAPI 部分が winmd に定義されているためと思われます。

winmd のインポートは "winmd_macros" という crate に定義されている import マクロで行います。

winrt::import!(
    dependencies
        os
    modules
        "windows.data.xml.dom"
        "windows.foundation"
        "windows.ui"
);

この import というマクロですが、どうも "proc_macro" というもので定義されています。

"Procedural Macros" は通常のマクロのようにテンプレートに穴埋めするのではなく、コンパイル時に動的にコード生成をする仕組みの事のようです。なるほど。これでは winmd 部分を Rust の文法でコード補完をかけるのは専用の対応を入れないと不可能ですね・・・

関連した Issue は上がっているので、実用になるかどうかはこれ次第ですかね。

おわりに

現状ではコード補完がきかないのが致命的すぎるので、 Windows と Rust が相当好きでも使うにはかなりハードルが高いと思います。試すだけなら Rust らしくとても簡単ですが。

こちらのコードも読んでみましたが、ウィンドウを開くだけでもちょっと手続きが面倒くさそうな感じなので、また機会を改めて (Win32 での UWP UI の利用の仕方についての理解が必要そう?) 挑戦してみようと思います。

Rust で手作業で DLL の動的ロードをする (Windows)

はじめに

DLL を自前でロードする必要が出てきたのでやり方をまとめました。

  • 目的の API へのアクセスに #link 属性は使わない
  • win32-rs 等は使わない

要点

DLL ロードに必要な API だけ #link 属性で取得します。

use std::ffi::c_void;
type Error = u32;

#[link(name = "kernel32")]
#[no_mangle]
extern "stdcall" {
    fn GetLastError() -> Error;
    fn LoadLibraryExW(lpLibFileName: *const u16, hFile: *const c_void, dwFlags: u32) -> *const c_void;
    fn FreeLibrary(hLibModule: *const c_void) -> i32;
    fn GetProcAddress(hModule: *const c_void, lpProcName: *const u8) -> *const c_void;
}

Win32 の W 系 APIUTF-16 文字列なので、変換する関数を用意します。

trait IntoNullTerminatedU16 {
    fn to_nullterminated_u16(&self) -> Vec<u16>;
}

impl IntoNullTerminatedU16 for str {
    //  略
}

Win32 API の多くは GetLastError でエラー取得をします。のでそれに対応した関数も用意します。

trait ToResult: Sized {
    fn to_result(&self) -> Result<Self, Error>;
}

impl ToResult for *const c_void {
    //  略
}

この組み合わせで LoadLibrary は次のように行いました。

let h = LoadLibraryExW("user32.dll".to_nullterminated_u16().as_ptr(),
                        ptr::null(),
                        0x800).to_result().unwrap();

API の関数ポインタを Rust の関数ポインタに変換するには次のようにします。 Rust → FFI の関数ポインタ (生ポインタ) への変換は as でできますが、逆は transmute で行います。

type FnMessageBox = extern "stdcall" fn(hWnd: *const c_void, lpText: *const u16, lpCaption: *const u16, uType: u32) -> i32;

let p = GetProcAddress(h, "MessageBoxW\0".as_ptr()).to_result().unwrap();
let fn_message_box = std::mem::transmute::<_, FnMessageBox>(p);

fn_message_box(ptr::null(),
                "Hello, Rust!".to_nullterminated_u16().as_ptr(),
                "MessageBox".to_nullterminated_u16().as_ptr(),
                0);

コード

gist485b3a3703f95bccac5d1503e55db8b4

おわりに

やってる事は簡単なのですが、 Rust の作法がいま一つよくわかってなくてこれでよいのか自信がないのでつっこみをお待ちしてます。

Unity 2020.1 の HDR ディスプレイ出力機能を試す

はじめに

この記事は以前書いた記事の続きです。

Unity 2020.1 では HDR ディスプレイ出力機能が強化されています。

https://blogs.unity3d.com/jp/2020/03/17/unity-2020-1-beta-is-now-available-for-feedback/

上記記事によると

  • PS4 / Xbox OneHDR 出力に対応 (Desktop PC を含めて Single API で対応)
  • Unity Editor 上の HDR プレビューに対応
  • HDR サポートの C# API

といった機能が追加されたとのこと。 PS4 / XONE は試しようもないですが、それ以外について一つずつ見ていこうと思います。

この記事では Unity 2020.1b5 で確認しています。

HDR 出力 / Unity Editor 上の HDR プレビュー対応

We have added HDR display support for the Editor, allowing developers who use displays that support HDR to take advantage of it. This change supports DX12 and Metal.

との記載があり Unity Editor 上で HDR 出力が行えるようになっています。

Windows では DX12 モードで起動する必要があります。 現時点ではデフォルトでは DX11 なので -force-d3d12 オプションをつけて Unity Editor を起動する必要があります。ビルドしたアプリケーションについては DX11 でも HDR は機能しますので Graphics API を Direct3D11 にしてビルドしても OK です (DX12 は Editor 上での条件) 。

2019.3 より Project Settings に下記設定をすることで HDR ディスプレイ出力が有効にできます。

  • "Player - Rendering - Color Space" を "Linear" に設定する
  • "Player - Rendering - Use display in HDR mode" のチェックを ON にする

上記で HDR ディスプレイ出力が有効になりますが、条件を満たしている環境では Unity Editor 上でもそのまま HDR ディスプレイ出力が行われます。

f:id:tan-y:20200412105511p:plain

HDR mode ではさらに "Swap Chain Bit Depth" という指定があり、これが "Bit Depth 10" か "Bit Depth 16" で挙動が違うことが確認できています。この点については前回記事でも触れていて、 "Bit Depth 10" では色がおかしいと書きました。

前回記事では検証が不十分だったのですが Unity としては次のような動作になっているようです。

Mac で DisplayP3 の設定をした時の挙動は環境がないので未確認

Unity のドキュメントによると

  • Bit Depth 10 → Unity will use the R10G10B10A2 buffer format and Rec2020 primaries with ST2084 PQ encoding.
  • Bit Depth 16 → Unity will use the R16G16B16A16 buffer format and Rec709 primaries with linear color (no encoding).

と書かれています。これは Windows の DXGI の仕様に準じています (DXGI はこの 2 つのフォーマットで HDR 出力ができる) 。

これに先に記述した "HDR ディスプレイに出力 → 最終レンダリング結果をそのまま出力" を合わせて考えると、 Unity のエンジン側でなく、アプリの方で指定の Bit Depth に対応した色空間に変換する必要がある ということになります。

従来、 Unity で言う色空間 (Color Space) は ドキュメントの説明 によると

  • Color Space は Gamma と Linear の 2 種類である
  • 標準的な Gamma は sRGB である

ということになっているので、まあ次のような事だろうという事は想定できます。というか Unity に限らず暗黙的にそのような扱いだったはずと思います。

色域 伝達特性
Gamma sRGB BT.709 sRGB
Linear scRGB BT.709 Linear

色空間については下記記事もご覧ください。

ちなみに scRGB は有効な値の範囲が仕様として定義されている範囲がせまい (-0.5~7.5) ですが処理中においては特に気にする必要はないと思います。

HDR 出力有効時の注意点として (少なくとも 2020.1 時点では) Unity 標準の Post Process (PPS, URP, HDRP) の Tone mapping を適用すると 0.0~1.0 の範囲に丸め込まれるので HDR じゃなくなります。従って Tone mapping は標準 Post Process 以外の方法で行う必要があります。

Bit Depth 10 の場合

Bit Depth 10 は "Rec.2020 (BT.2020) かつ "ST 2084(PQ)" となっています。これは Unity の Color Space と互換性がありません。実は私は 2019.3 で試していた時は Unity 側で色空間変換をしてくれるものと思っていたのですがそうではありませんでした。

色空間変換をしてくれない、ということは自前でなんとかする必要があります。

Bit Depth 16 の場合

Bit Depth 16 は "Rec.709 (BT.709)" かつ "Linear" となっています。つまり Color Space = Linear に設定した際の中間レンダリングフォーマットそのもの ということになります。従ってこの設定にした場合は特に意識しないで正しい色になります。

HDR サポート の C# API

HDR の対応状況は SystemInfo.hdrDisplaySupportFlags より取得できます。

HDR 出力に関する詳細な設定は HDROutputSettings クラス で行えます。設定できるものは下記の 2 つです (SystemInfo.hdrDisplaySupportFlags の状況で設定の可否があると思います) 。

  • active → HDR が有効かどうか
  • automaticHDRTonemapping → 自動 HDR トーンマッピングを有効にするかどうか

active についてはプロパティ自体は get のみですが RequestHDRModeChange メソッド により切り替えができます。

上記以外のプロパティは available プロパティ が true じゃないと例外が出るので、まずこのプロパティを確認してから操作をするようにすべきでしょう。 avalilable プロパティは Player Settings の "Use display in HDR mode" が ON になっていると (かつ HDR が有効にできるプラットフォーム) true になります。 OFF にしていると対応環境でも available = false なので「Editor 上では SDR で作業したい」と OFF にしてそのままビルドするとはまりそうなので注意が必要です。

BitDepth は実行時に変更することはできません (まあ実行時に切り替えるようなものではないですが) 。 BitDepth に基づいた設定は displayColorGamut, format, graphicsFormat から判断できます。これらの情報を元に必要に応じて色空間変換処理を適用すればよいかと思います。

その他、ディスプレイの性能 (最大、平均輝度など) を取得するプロパティも存在します。が、経験上この値の信頼性はあまりない (ディスプレイが正しい値を返す保証がない) と思いますので参考程度に留め、性能に応じてトーンマッピングのパラメーターを調整するのであればキャリブレーション機能があった方が望ましいと思います。

automaticHDRToneMapping は具体的にどういった挙動をするのか試した感じではよくわかりませんでした・・・ (らしい絵を作ってみないとだめかも)

トーンマッピングと色空間変換

automaticHDRToneMapping のドキュメント によるとトーンマッピングを自前でやる場合には PostEffect や OnRenderImage で、と書いてあります (これがカスタムトーンマッピングの事?) 。 URP や HDRP で OnRenderImage は使えないので別の手段で対応する必要があります。ただ私があれこれ試してみたところ URP 8.0.1 では URP 組み込み PostEffect の後に独自の PostEffect を差し込むことはできません でした。前だったら差し込めましたが、トーンマッピングと色空間変換は一番最後に適用すべきなのであまり意味がないですね。私の調べ方が不十分なだけかもしれませんが。

BT.709 / Linear → BT.2020 / PQ の変換は Microsoft の D3D12 HDR sample にそのものずばりの PixelShader のコードがあるので、それを利用するのが手っ取り早いでしょう。

表示例

Bit Depth 設定と Post Process による色空間変換の適用によってレンダリング結果にどのような違いが生じるかを見ていきます。

画像は適当な Sphere に強めの Light をあてただけのものです。 Tone mapping はしていません。

画像 説明
1
f:id:tan-y:20200411141437p:plain
適切に sRGB 変換されたもの
2
f:id:tan-y:20200411141649p:plain
Bit Depth 10 にレンダリング結果を無変換でそのまま表示したもの
3
f:id:tan-y:20200411141737p:plain
レンダリング結果を BT.2020 / PQ に変換したものをそのまま SDR で表示したもの

1 はあからさまにおかしく見えますが、これは強い Light をあてて白飛びしているためです。

  • SDR (sRGB)
  • Bit Depth 16
  • Bit Depth 10 かつ Post Process で BT.709 / Linear → BT.2020 / PQ 変換したもの

を SDR キャプチャしたもので全て同じ表示になります。

f:id:tan-y:20200411142120p:plain
Bit Depth 16 出力映像の波形モニター

これは Bit Depth 16 でレンダリングしたものを Windows 10 の機能で JPEG XR に HDR キャプチャしたものを波形モニターで見たものです。 1.0 オーバーの部分は SDR 下では全て飛びます。 HDR 環境では綺麗な描画で見ることができます。

2 は単純に設定を Bit Depth 10 にしただけのものです。これだとわかりにくいですが色空間が合っていないので 1 と色合いが変わってしまっています。

3 は PQ のソースをそのまま sRGB に表示した場合の見栄えです。傾向として色合いが大分浅く見えます。これも色空間が合っていないので色は正しくありません。が 0~1 の範囲に値が収まるので SDR 環境で HDR のグラデーションのかかり具合が視認できます。

Unity Editor ではウィンドウ内の描画が丸ごと指定の色空間になります。

f:id:tan-y:20200411143847p:plain
Bit Depth 10 の UI
f:id:tan-y:20200411143922p:plain
Bit Depth 16 の UI

UI は BT.709 / Linear でレンダリングしていると思われます。そのため Bit Depth 16 の UI は通常の SDR と同じに見えますが、 Bit Depth 10 は白っぽくなってしまっています。これは UI 部分の色空間変換をせず、そのまま出力してしまっているためでしょう。せめて UI の色くらいはなんとかして欲しいですね・・・

Unity でのレンダリング色空間を考える

Unity の Player Settings で設定する "Color Space" は実際のところ "伝達特性" の定義であって色域については規定がされてなく、最終出力の sRGB から BT.709 を色域とする、といった形になっているのではないかと思います。実際のところは最終的な出力時に色空間変換を自前ですれば (Bit Depth 10 出力有効時はそもそも必要) レンダリング時の色空間はなんでもよいわけです。

まず前提として scRGB (BT.709 / Linear) は負の値を許容することで広色域になっていますが、そもそも負の値を扱えるバッファが必要になります。 Frame Debugger で観察してみるとレンダリングバッファは次のフォーマットのようです。 (ColorSpace = Linear)

GraphicsFormat
Legacy R16G16B16A16_SFloat
URP B10G11R11_UFloatPack32
HDRP B10G11R11_UFloatPack32 or R16G16B16A16_SFloat

B10G11R11_UFloatPack32 は 1 ピクセルあたり 32bit ですが符号ビットがないので負の値を持てません。一方 R16G16B16A16_SFloat は負の値が持てるものの 1 ピクセルあたり 64bit となりメモリ負荷が高いのと URP では選択できないようです。

BT.709 より広い色域のものにすれば 0~1 の範囲で扱うことが可能になります。一方で BT.709 と全く互換性がない事からワークフローも見直す必要が出てくると思います。もしも違う色域にするなら BT.2020 が現在の HDR コンテンツが採用している標準的な色域で表現範囲も一番広いのでよいのではないかと思います。

以上の事から次のように考えられます。

  • BT.709 は sRGB と色域が同じなので従来ワークフローと互換性が高いが、広色域にする場合は負の値が表現できる必要がある
    • 負の値が表現できる R16G16B16A16_SFloat はメモリ負荷が高めなのでそれを許容する必要がある
    • 色域については BT.709 の範囲で留め、 HDR 表現のみ採用するという判断も考えられる
  • BT.2020 などより広色域のものにすれば負の値を扱わずに広色域にすることが可能になるが、ワークフローの見直しが必要になる
    • RGB 値が BT.709 と異なるものになる (BT.709 下で色選択しても正しくなくなる)
    • テクスチャのインポート時にもあらかじめ色域を変換しておく必要がある
    • Editor 上でのプレビューが難しくなるかもしれない

BT.2020 は Unity 自体にサポートが入ってほしいかなあと思います。

おわりに

HDR 出力制御の C# API が追加されたことにより、 Unity でも現実的に HDR 出力に対応したアプリの開発が可能になってきたと思います。一方でレンダリング色空間や Unity Editor の UI の色空間の扱いなどアプリ開発者側的に不便なところもまだあると思います。 今後の拡張にも期待していきたいところです。