Live++ で C++ をホットリロードする

Live++ で C++ をホットリロードする

blog.jetbrains.com

先週リリースされた JetBrains Rider 2024.3 EAP が Live++ に対応したらしい。従来の Rider は C++ のホットリロードに対応していない。この点 Rider は Visual Studio に遅れを取っていて不便だと思う。

Live++ とは何なのか

Live++ | Your hot-reload superpower

C/C++ でホットリロードを可能にする商用の開発ツールらしい。無料トライアルのあとは月 €11.90 (約2000円) が必要。本記事では Rider において Live++ を利用する内容であるが、Live++ 自体はIDEに依存しない。

導入してみる

  1. まず、Live++ をダウンロードする。以下のページから zip をダウンロードした。内容はAPIのヘッダとDLL、実行ファイル合わせて8ファイル程度あった。

    https://liveplusplus.tech/trial_download.html

  2. スタートガイドに沿って進める。

    https://liveplusplus.tech/docs/documentation.html#compiler_settings

    先ほどダウンロードしたzipの中身をプロジェクト直下に配置した。

    ドキュメントに記載されていたコンパイルオプションとリンカーオプションをそれぞれ設定した。

  3. プロジェクトに以下の C++ ファイルを LivePP_EntryPoint.cpp として追加した。プログラム実行時、Main が呼ばれる前に Live++ のクライアントが起動するようにしている。相対パスは各自のプロジェクトに合わせて変更する。

// include the API for Windows, 64-bit, C++
#include "../LivePP/API/x64/LPP_API_x64_CPP.h"

namespace
{
    struct LivePP_EntryPoint
    {
        lpp::LppDefaultAgent lppAgent;

        LivePP_EntryPoint()
        {
            // create a default agent, loading the Live++ agent from the given path, e.g. "ThirdParty/LivePP"
            lppAgent = lpp::LppCreateDefaultAgent(nullptr, L"../../../LivePP");

            // bail out in case the agent is not valid
            if (!lpp::LppIsValidDefaultAgent(&lppAgent))
            {
                return;
            }

            // enable Live++ for all loaded modules
            lppAgent.EnableModule(
                lpp::LppGetCurrentModulePath(), lpp::LPP_MODULES_OPTION_ALL_IMPORT_MODULES, nullptr, nullptr);

            // run the application
        }

        ~LivePP_EntryPoint()
        {
            // destroy the Live++ agent
            lpp::LppDestroyDefaultAgent(&lppAgent);
        }
    };

    LivePP_EntryPoint s_lpp;
}

以上で設定が終わった。

使ってみる

コードの変更を Ctrl + Alt + F11 でホットリロード出来た。この際、自動でファイル保存はしてくれず、手動でファイルの保存が必要であったので注意。ホットリロードが完了するとトースト通知が表示される。

ゲーム開発の場合、LppSynchronizedAgent を使うと良さそう。これは任意のタイミングでホットリロードを反映することが可能なため、予期せぬタイミングでホットリロードが適応されることによる事故を防げる。Live++ 用の Siv3D Addon · GitHub

Visual Studio ビルドインのホットリロードと比較して

  • 計測はしていないが、体感では Visual Studio よりも Live++ の方がホットリロードする時間が短く快適に感じた。
  • Visual Studio よりも Live++ の方がホットリロードを適応できる場面が多そうであった。例えば、Visual Studio ではラムダ式内部のホットリロードが反映されないことが多々あるのに対し、Live++ では特に問題無さそうであった。
  • Visual Studio では副作用のない小さな修正であっても「この変更はホットリロードに対応していません」と弾かれてしまう現象がしばしば発生するが、Live++ ではメモリレイアウト的な変更でない限り基本的にホットリロードが出来そうであった。

Rider 2024.3 EAP との連携

冒頭の記事によると Live++ のための機能が Rider に追加されているらしいので試してみる。ドキュメントによると、Visual Studio または Rider から Live++ を起動すると自動で検知してくれるようになっているらしい。

When debugging with Visual Studio or Rider, Live++ will attempt to automate the actions necessary to put the process into a mode where it can continue to communicate with Live++. If successful, the Broker UI log will read "Automated ... debugger attached to process with PID XXXXX"; Live++ will then compile your changes and install code patches. Afterwards, the process will once again be held at the same instruction in the debugger.

しかし、やってみてもこの検知されたという旨のメッセージは表示されず、連携が出来ていないようであった。追加の設定を見直したりしてみたが結局原因が分からなかった。後日改めて確認したい。

感想

Rider で高品質なホットリロードが可能になったことにより、大幅に開発効率の向上が見込めそう。まだトライアル中であるが、Live++ に月2000円を払う価値はあると思う。

https://liveplusplus.tech/pricing.html を見ると Free educational license があるらしい。学生は無料で使えるかもしれないので問い合わせた。

追記: 大学のメールアドレスにてお問い合わせをしたところ、ライセンスキーを早速ご送付いただきました。

VisualStudio と Rider で同一ビルドを行う

VisualStudio と Rider で同一ビルドを行う

VisualStudio と Rider で同じプロジェクトをビルドする際、異なるビルド環境と認識されてしまうことがある。こうなるとエディタを切り替えるたびにリビルドが発生してしまい厄介。

同じ環境として扱うためには、以下の設定が同一のものである必要がある。

  • VisualStudio: 拡張機能 > ReShaper > Options > Tools > Build > General
  • Rider: Settings > Tools and Build

Use ReShaper Build のチェックは入れておく。

MSBuild.exe が同じ実行ファイルになっているか等も確かめること。

OpenSiv3DでPSDファイルを読み込む

SivPSD

この記事はSiv3D Advent Calendar 2023の21日目の記事として執筆いたしました。

OpenSiv3Dのゲーム開発でPSDファイルを利用したいと思い、PSD読み込みライブラリをpsd_sdkを用いて作成しました。その導入方法と使い方についてまとめます。

github.com

要件

導入方法

📝 導入するOpenSiv3Dのプロジェクトを新規作成または開いてください。ここでは、HelloPSDというプロジェクトで進めます。

📗ターミナルで以下のコマンドを実行し、SivPSDとpsd_sdkをサブモジュールとして追加します。

git submodule add https://github.com/sashi0034/SivPSD
git submodule update --init --recursive
  • gitを用いない場合は、代わりにSivPSDをダウンロードしてプロジェクト直下に配置してください。

📝 SivPSDは現在v0.6.12のSiv3Dが設定されています。ご自身のプロジェクトのバージョンと違う場合は手動またはスクリプトを実行してSivPSDで使うSiv3Dのバージョンを変更してください。例えば、v0.6.10に変更する場合は以下のように実行すれば簡単に変更できます (しかし、バージョンによっては engine 以下や Resource.rc 等を手動で修正する必要があります)

cd .\SivPSD\SivPSD\
python .\s3d_switch.py 0_6_10

📗 SivPSDをソリューションに追加します。ソリューションエクスプローラーのルートから右クリックをして、追加 / 既存のプロジェクトを選択します。プロジェクトルートからSivPSD/SivPSD/SivPSD.vcproj を探して追加します。

📝 同様の手順で SivPSD/psd_sdk/build/VS2022/Psd.vcxproj も追加します。

📗 プロジェクトからSivPSDを参照します。自身のプロジェクトを右クリックして、追加 / 参照 をクリックします。SivPSDにチェックマークを入れてOKを押してください。

🎉以上で導入は終わりです。

(かなり手間のかかる作業になるので、時間のあるときに psd_sdk と一緒にヘッダオンリにまとめて簡略化したいところです...)

使い方

基本的なコード

📝 以下のようPSDを読み込んで使用できます。

PSDImporter psdImporter{U"path/to/file.psd"}; // 読み込み
if (const auto e = psdImporter.getCriticalError()) throw e; // エラー
PSDObject psdObject = psdImporter.getObject(); // 読み込んだデータの取得
while (System::Update())
{
    psdObject.draw(); // PSDを描画
}

📗 psdObject.layers[index] でレイヤー単体にもアクセスが可能です。以下にサンプルコードを掲載するのでご参考ください。イラスト miko15.psdSivPSD/Test/App/psd に配置しました。

gist.github.com

実行結果

非同期処理にして実行

📝 このPSDImporterですが、以下のようにコンストラクタで細かな設定が可能です。

  • storeTarget でレイヤーを Image として格納、DynamicTexture として格納するかの設定が可能です。

  • storeTarget で読み込み時に用いるスレッド数を決められます。適切なスレッド数を決めることで高速な読み込みが実現できます。

  • asyncStartでバックグラウンドとして非同期実行ができます。

PSDImporter psdImporter{
    {
        .filepath = U"psd/miko15.psd",
        .storeTarget = StoreTarget::MipmapTexture,
        .maxThreads = 4,
        .asyncStart= true
    }
};

📗 非同期処理にしたサンプルコードは以下をご確認ください。

gist.github.com

PSD形式の注意について

今回実装したライブラリではレイヤーのマスク処理やクリッピング機能が実装されていません。

また、カラーモードやカラーチャンネルについてもRGB、8ビット/チャンネルと制限しました。

CLIP STUDIO PAINTでイラストを作成するときは基本的にこのカラーモードやカラーチャンネルになっているようです。

Live2D Cubismのマニュアルを確認しますとこのような細かい制約が確認できます。

今回の開発においてもこの規則に則っていくことにしました。

🤖レイヤーの統合作業は大変だと思います。こちらは蛇足になりますが、今回次のようなクリスタで使えるレイヤー自動統合スクリプトを作ってみました。ぜひご活用ください。

C#でdefer

C#でdefer

IDisposableとusingで再現。

    public class ScopedDefer : IDisposable
    {
        private readonly Action action;

        public ScopedDefer(Action action)
        {
            this.action = action;
        }

        public void Dispose()
        {
            action();
        }
    }

使い方

stack++;
using var _ = new ScopedDefer(() => stack--);