Gaming Life

一日24時間、ゲームは10時間

WindowsのC++でウィンドウアプリケーションを書く際にWinMain()を隠匿する方法の一例

序文

OpenSiv3Dなど、ユーザーが使いやすいように考えて作られたC++Windowsアプリケーション開発用フレームワークは、複雑なシグネチャを持つ WinMain() を隠してくれる。

# include <Siv3D.hpp>

// WinMain()を書く必要がない
// int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,
//                    LPSTR lpCmdLine, int nCmdShow)
void Main()
{
    Print << U"Hello, Siv3D!";

    while (System::Update())
    {
    }
}

私もこうしたWinMainの隠匿をしてみたいなーと思い、実際に実装してみた。

最も簡単な実装

FrameworkMain.cpp

#include <Windows.h>

void Main();

int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,
                   LPSTR lpCmdLine, int nCmdShow)
{
    OutputDebugStringA("winmain() start!\n");
    Main();
    return 0;
}

UserMain.cpp

#include <Windows.h>

void Main() {
    MessageBoxA(nullptr, "Demo", "Demo Message", MB_OK);
}

上記のコードのように、フレームワーク側(.lib)で、宣言だけ用意して未実装な void Main() 関数を用意し、それをフレームワーク側に記述した WinMain() で呼び出す。これにより、ユーザーは WinMain() の存在に気を払うことなく、Main() 関数さえ実装することができる。

以上で目的は達成できたのだが、これではあまりに薄いので、もう少し凝った実装をしてみた。

クラスのメンバ変数をエントリポイントのように扱う

ここでは以下の要件を満たすものを実装する。

  1. フレームワークはAppBaseという名のクラスを用意する
  2. AppBaseは virtual int Init() という純粋仮想関数をもつ。
  3. ユーザーはAppBaseをpublic継承したアプリケーション用クラスを作成できる
  4. ユーザーは継承先で virtual int Init() 関数をエントリポイントのように扱って実装できる
  5. ユーザーは、これらの機能を最小限の労力で使用できる。

AppBase.h(フレームワーク実装)

#pragma once
#include <windows.h>

class AppBase* GetWinPtr();

class AppBase {
private:
    friend AppBase* GetWinPtr();
    static AppBase* m_App;

public:
    AppBase();
    virtual int Init() = 0;
};

AppBase.cpp(フレームワーク実装)

#include "AppBase.h"

AppBase::AppBase() { m_App = this; }

AppBase* AppBase::m_App = nullptr;

AppBase* GetWinPtr() { return AppBase::m_App; }

int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,
                   LPSTR lpCmdLine, int nCmdShow)
{
    OutputDebugStringA("winmain() start!\n");
    AppBase* g_App = nullptr;
    g_App = GetWinPtr();
    if (g_App == nullptr) {
        return -1;
    }
    return g_App->Init();
}

MyApp.cpp(ユーザー実装)

#include "AppBase.h"

class MyApp : public AppBase {
public:
    virtual int Init() {
        MessageBoxA(nullptr, "Demo", "Demo Message", MB_OK);
        return 0;
    }
};

MyApp app;

解説

ユーザーは、グローバルに自身がAppBaseクラスを継承して作成したクラスのインスタンスを作成するだけで、 Init() 関数をエントリポイントのように扱って実装できる。仕組みとしては以下の通り。

AppBaseはstatic変数として、自身の型のポインタ変数を持つ。このポインタは、AppBaseのfriend関数である GetWinPtr() から取得できる。

フレームワーク上では、 WinMain() において GetWinPtr() 経由でAppBase型のポインタから Init() メンバ関数を呼び出す。しかし、この時点ではそのポインタの実体がどの型を表しているか分からない。

int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,
                   LPSTR lpCmdLine, int nCmdShow)
{
    OutputDebugStringA("winmain() start!\n");
    AppBase* g_App = nullptr;
    g_App = GetWinPtr();
    if (g_App == nullptr) {
        return -1;
    }
    return g_App->Init();
}

そこでユーザーは、AppBase型を継承したクラスであるMyApp型のインスタンスを、グローバルで作成する。グローバルで定義した変数は、エントリポイント、つまり WinMain() より早く用意される。

これにより、ユーザーは、AppBase継承クラスの Init() 関数を、エントリポイントのように扱って実装することができる。

補足

この実装は、WindowsGUIフレームワークとしてMicrosoftが提供する、MFCMicrosoft Foundation Class)の実装とほとんど同じらしい。

参考

In a typical MFC application we never write WinMain() function so how is it possible to compile and link a windows program with out WinMain()?(http://www.equestionanswers.com/vcpp/mfc-application-winmain().php)

winmainの隠蔽工作

PIXでシェーダデバッグする際はwindowsの開発者モードを有効にしなければならない

この記事は、ai_9684_dctソロ Advent Calendar 2020 7日目 の記事です。

PIX on Windowsとは

PIXとは、Microsoftが開発したDirectX12製ゲームのパフォーマンス測定及びシェーダデバッグのための、プロファイラである。DirectX12を使った開発をする際は、ぜひ使いたいソフトの一つ。

遭遇したエラー

PIXを使って、シェーダデバッグをしようとしたところ、以下のエラーでシェーダデバッグが起動出来なかった。

「An Error has occured. This feature requres developer mode to be enabled on the analysis device. Type "developer mode" into the start menu to find the correct setting」

f:id:ai_gaminglife:20201207200737p:plain

エラーの回避方法

エラーメッセージの指示どおり、windows10の開発者モードを有効にすれば、エラーを回避できる。

windows 10の [設定] → [更新とセキュリティ] → [開発者向け]の画面で、[開発者用モード]をオフからオンに切り替える。(この時PCの再起動は必要ない)

f:id:ai_gaminglife:20201207200748p:plain

これにより、PIXでDirectX12のシェーダデバッグが可能となる。

UE4のコンソール変数の命名規則とsgコマンドの役割

この記事は、ai_9684_dctソロ Advent Calendar 2020 5日目 の記事です。

UE4には r.ScreenPercentage や、 r.Streaming.PoolSize だったり、多くの便利なコンソール変数が用意されている。こういったコンソール変数には、命名規則がある。

ConsoleManager.cpp に、コンソール変数の命名規則が書かれている。

// r.      Renderer / 3D Engine / graphical feature
// RHI.    Low level RHI (rendering platform) specific
// a.     Animation
// s.     Sound / Music
// n.      Network
// ai.     Artificial intelligence
// i.      Input e.g. mouse/keyboard
// p.      Physics
// t.      Timer
// log.       Logging system
// con.       Console (in game  or editor) 
// g.      Game specific
// Compat.
// FX.     Particle effects
// sg.     scalability group (used by scalability system, ini load/save or using SCALABILITY console command)

この中で、 sg. から始まるコマンドは少々他と違う扱いになっている。

sg.コマンドについて

sgとは、Scalability Groupの略で、sgコマンド類は、ある設定ファイルに基づいて、他のパラメータをまとめて制御する役割を持つ。例えば自分の環境だと、 sg.PostProcessQuality を3から1に変更すると、

LogConfig: Applying CVar settings from Section [PostProcessQuality@1] File [C:/Users/detec/Documents/Unreal Projects/ActionRPG/Saved/Config/Windows/Scalability.ini]
LogConfig: Setting CVar [[r.MotionBlurQuality:3]]
LogConfig: Setting CVar [[r.AmbientOcclusionMipLevelFactor:1.0]]
LogConfig: Setting CVar [[r.AmbientOcclusionMaxQuality:60]]
LogConfig: Setting CVar [[r.AmbientOcclusionLevels:-1]]
LogConfig: Setting CVar [[r.AmbientOcclusionRadiusScale:1.5]]
LogConfig: Setting CVar [[r.DepthOfFieldQuality:1]]
LogConfig: Setting CVar [[r.RenderTargetPoolMin:350]]
LogConfig: Setting CVar [[r.LensFlareQuality:0]]
LogConfig: Setting CVar [[r.SceneColorFringeQuality:0]]
LogConfig: Setting CVar [[r.EyeAdaptationQuality:0]]
LogConfig: Setting CVar [[r.BloomQuality:4]]
LogConfig: Setting CVar [[r.FastBlurThreshold:2]]
LogConfig: Setting CVar [[r.Upscale.Quality:2]]
LogConfig: Setting CVar [[r.Tonemapper.GrainQuantization:0]]
LogConfig: Setting CVar [[r.LightShaftQuality:0]]
LogConfig: Setting CVar [[r.Filter.SizeScale:0.7]]
LogConfig: Setting CVar [[r.Tonemapper.Quality:2]]
LogConfig: Setting CVar [[r.DOF.Gather.AccumulatorQuality:0        ; lower gathering accumulator quality]]
LogConfig: Setting CVar [[r.DOF.Gather.PostfilterMethod:2          ; Max3x3 postfilering method]]
LogConfig: Setting CVar [[r.DOF.Gather.EnableBokehSettings:0       ; no bokeh simulation when gathering]]
LogConfig: Setting CVar [[r.DOF.Gather.RingCount:3                 ; low number of samples when gathering]]
LogConfig: Setting CVar [[r.DOF.Scatter.ForegroundCompositing:0    ; no foreground scattering]]
LogConfig: Setting CVar [[r.DOF.Scatter.BackgroundCompositing:0    ; no foreground scattering]]
LogConfig: Setting CVar [[r.DOF.Recombine.Quality:0                ; no slight out of focus]]
LogConfig: Setting CVar [[r.DOF.TemporalAAQuality:0                ; faster temporal accumulation]]
LogConfig: Setting CVar [[r.DOF.Kernel.MaxForegroundRadius:0.006   ; required because low gathering and no scattering and not looking great at 1080p]]
LogConfig: Setting CVar [[r.DOF.Kernel.MaxBackgroundRadius:0.006   ; required because low gathering and no scattering and not looking great at 1080p]]
sg.PostProcessQuality = "1"

この様にポストプロセス関連のコンソール変数がまとめて変更される。

Scalability Groupの設定は、Engine/Configフォルダに格納されている。BaseScalability.iniに記述されている。ここに記載されている値を変更すれば、例えば、 sg.PostProcessQuality を1にさげても、 r.BloomQuality は下げてほしくない……みたいな調整ができる。

参考サイト

docs.unrealengine.com

Pythonのデコレータ(風)をC++で実装してみる

本記事はai_9684_dctソロ Advent Calendar 2020 3日目の記事です。

Pythonには「デコレータ」という、関数に追加の機能を修飾するシンタックスシュガーが存在する。

def decorator_sample(func):
    def wrapper(*args, **kwargs):
        print(f'Arguments: {args}')
        print(f'Keyword arguments: {kwargs}')
        result = func(*args, **kwargs)
        print(f'Result: {result}')
        return result
    return wrapper

@decorator_sample
@decorator_sample
def add_int2(a: int, b: int):
    return a + b

add_int2(12, 41)

# 出力結果
Arguments: (12, 41)
Keyword arguments: {}
Arguments: (12, 41)
Keyword arguments: {}
Result: 53
Result: 53

デコレータは、親切なライブラリだとサポートしていることが多い印象。

今回はPythonのデコレータっぽいものをC++で実装してみた。

C++17によるデコレータの実装

といってもPythonのように @decorator とつけるだけで修飾するには、言語機能レベルでサポートしなければならない。そこまでのことは出来ないので、デコレートしたい関数オブジェクトを受け取る関数を作って、そこでデコレートするという実装にした。

今回の実装にはこちらのサイトを参考にした。

C++のPython関数デコレータに相当するものは何ですか?

#include <iostream>

template <class... Ts>
void print_all(std::ostream& os, const std::string& separator, Ts const&... args) {
    ((os << args << separator), ... );
}

template <class T>
auto decorator(T&& func) {
    auto wrapper = [func = std::forward<T>(func)](auto&&... args){
        std::cout << "arguments: ";
        print_all(std::cout, ", " , args...);
        std::cout << "\n";
        auto result = func(std::forward<decltype(args)>(args)...);
        std::cout << "Result: " << result << '\n';
        return result;
    };
    return wrapper;
}

int add_int2(int a, int b) { return a + b; }

int main() {
    auto decorated = decorator(decorator(add_int2));
    decorated(12, 29);
}

// 出力結果
// arguments: 12, 29, 
// arguments: 12, 29, 
// Result: 41
// Result: 41

[C++] gcc HEAD 11.0.0 20201106 (experimental) - Wandbox

(ちょっと)文法解説

デコレータへの関数及びその引数の受け渡しには完全転送を利用している。

template <class T>
auto decorator(T&& func) { // ユニバーサル参照で仮引数を宣言して
    auto wrapper = [func = std::forward<T>(func)](auto&&... args){ // std::forwardで完全転送
// 以下省略
}

完全転送についてはこの辺りの資料が参考になる。

marycore.jp

デコレートする関数に渡される実引数を列挙するための print_all 関数には、C++17以降でサポートされている畳み込み式(fold expression)を利用している。そのため、C++14以前のコンパイラでは動作しない。

cpprefjp.github.io

私的ゲーム開発技術情報集め ver. 2020年12月

本記事はai_9684_dctソロ Advent Calendar 2020 1日目の記事です。

まだ大学生だった1年前、下記の記事のような、私流の技術情報収集の手段をまとめた記事を書いた。今年、ゲームエンジニアとして就職して以降、当時の情報収集手段とは少し変わってきたので、今年もこの内容で記事を書くことにした。

ai-gaminglife.hatenablog.com

Google検索

疑問に思うことがあればまずはGoogleで検索。プログラムの深い部分を調べたい時は、検索言語を英語に変えると良い。

余談だが、「英語の記事は理解できないから絶対読まない」なんてプログラマは、自身の価値を大きく毀損している。今の時代、たとえ自分で英語が読めなくても、DeepL翻訳やGoogle翻訳があるので何とかなる。特にアプリ版DeepL翻訳は、Ctrl + Cを2回押すだけでそれなりの精度で自動翻訳してくれるのでオススメ。

書籍

信頼と安心の情報源。書籍の情報が全て正しい、というわけではないが、インターネット上で無料で手に入る情報よりは確実に精度が高い。体系立った情報を手に入れたいなら、値は張るが、まず最初に書籍を当たったほうが良いと思う。

これまで、私は技術書は本屋かAmazonで買って読むだけのものだったが、最近もう一つ技術書の付き合い方が増えた。図書館だ。今年、私は横浜に引っ越したのだが、夏頃から最低でも月に1度は横浜市立中央図書館に足を運ぶようになった。中央図書館には、最新の技術書や古典的な名著など沢山の技術書が揃っていることに気づいたからだ。あなたがもし都会に住んでいるのなら、是非一度は近所の大きな図書館に足を運んでみてほしい。沢山の技術書にタダで触れることができる。

Twitter

自分がよく使うフレームワークやアプリなどの第一人者、今風にいうとエヴァチェリストを数人フォローしておけば、最新の情報がすぐ手に入る。一年前はTweetDeckでそういった人のツイートを全て追っかける……ということをよくやっていたが、最近は公式TwitterクライアントでTLを見ることが多くなったので、そうしたストーカーまがいの行為はやってない。

作業中、困ったことがあればGoogle検索に加え、Twitterでも検索するというのは今も続けている。それで業務中助けられた事が何度かあった。

Inoreader(RSSリーダー

今年に入って、Inoreaderを使った情報収集を始めた。これがなかなか良い。これまで取りこぼしていた情報を集めることができるようになった。最近RSSを使う人は減っていると聞くが、私には性に合った。

以下に、今購読しているサイトで、特に有用だと思うサイトを挙げる。

プログラミング関連

IT News

はてなブックマーク

はてなブログを使っておきながら言うのもなんだが、今からはてブを見るというのはオススメできない。はてブの技術カテゴリは、有用な情報を集める事ができるが、Web系の技術記事ばかりがトレンドに上がるし、雑音(ノイズ)が多すぎる。はてなブックマークという場所がこの一年で更に政治臭が強くなったので、その辺り耐性のない人は、使わないほうがいい。

なお私は今更はてブやめれなくなったので今後も使います。

NotionのWebクリッピング

最強のAll in Oneメモツールには、Webクリッピング機能が備わっている。ChromeFirefox拡張機能ですぐにNotion上にWebサイトをクリップできる。私は、上記のサービス・ニュースサイトで集めた情報は、ほぼすべてNotionのWeb クリッピングで収集している。

まとめ

今年はQiitaの対抗サイトとしてZennが登場するなど、技術トレンドを追う手段は強烈な速さで移り変わっている。情報過多のこの時代、どれだけノイズを排してほしい情報を得るかが大切だと思っている。今後も、トレンドに合わせて、情報収集の手段を適宜アップデートしていくことになるだろう。

VS 16.7以前では一時オブジェクトのアドレスを&演算子で取得してもエラーが出なかったらしい

Visual Studio 2019のバージョンを16.8に上げてから、DirectX12のコードをビルドしてみたら、これまでコンパイルできていたものができなくなっていた。そのコードがこれ。

hr = m_device->CreateCommittedResource(
            &CD3DX12_HEAP_PROPERTIES(D3D12_HEAP_TYPE_DEFAULT), // ここでC2102エラー
            D3D12_HEAP_FLAG_NONE,
            &depthBufferDesc,
            D3D12_RESOURCE_STATE_DEPTH_WRITE,
            &depthClearValue,
            IID_PPV_ARGS(&m_depthBuffer)
        );

docs.microsoft.com

C2102: '&'に左辺値がありません。

エラーの原因は、一時オブジェクト(右辺値)である CD3DX12_HEAP_PROPERTY のアドレスを、&演算子で取得しようとしたこと。よく考えれば、一時オブジェクトのアドレスを取得なんてしてはいけないことだと思う。なので、試しに他のコンパイラで似たようなコードを書いてみた。

#include <iostream>

struct A { int a, b, c, d; };

A make_A() { return A{}; }

void func(A*) {}

int main() {
    func(&(A{}));
    func(&make_A());
}

wandbox.org

gccでもclangでも、このコードはコンパイル出来なかった。右辺値のアドレスを取ろうとするのは駄目みたい。

で、MSVCのオンラインコンパイルも可能な、Compiler Explorerでも同じコードをコンパイルしてみた。すると、MSVCだけコンパイルが通ることが確認できた。

godbolt.org

どうもVisual Studioが間違っていて、バージョン16.8からコンパイルが通るようになったみたい。

ちなみに手元で試した限り、Visual Studio 2017では、このコードのコンパイルが通った。

UE4でNVIDIA NSight Graphicsを使ったGPUプロファイリングをしてみる

本記事は、「Online Game-Tech LT 2020」のLT発表の補足資料として用意しました。登壇一日前で時間がない中で本記事を執筆しているため、必要最低限の説明だけにとどまっていることをご容赦ください。時間があればそのうち修正します。

connpass.com

NVIDIA NSight Graphicsとは

NVIDIAが開発したグラフィックアプリケーション用のプロファイラ。RenderDocと比べて、GPUの低レイヤの調査に向いている。

セットアップ

今回は、UE4で作成したゲームパッケージでプロファイルするのではなく、エディタ上でプロファイルする。製品版に近い状態でプロファイルしたい場合は、Developmentパッケージを作成し、それをプロファイルするのがよい。

公式サイトからNVIDIA NSight Graphicsをダウンロードする(以下NSightと表記)。 developer.nvidia.com

プロファイルする時は、必ず管理者権限で実行すること。そうしないと警告メッセージが出て、詳細な調査ができない。

起動するとこのようなウィンドウが表示される。「Create New Project」を選択し、プロファイルのための新しいプロジェクトを作成する。 f:id:ai_gaminglife:20201031210430j:plain

プロセス立ち上げのための設定ウィンドウが立ち上がったら、「Application Executable」にUE4Editor.exeのパスを、「Commnad Line Arguments」にプロファイルしたいuprojectのパスと-game引数を記述する。なお、私の環境ではなぜか「-dx12」引数をつけないとプロファイルできなかった。そんなことをしなくても普通は出来るはずなんだけど……もし「-game」だけで起動しない方がいれば試してみてください。

UE4Editor.exeはデフォルトで、「C:\Program Files\Epic Games\UE_4.「バージョン番号」\Engine\Binaries\Win64\UE4Editor.exe」のパスにある。

f:id:ai_gaminglife:20201031211100p:plain

「Launch Frame Debugger」を押すとUE4Editorが起動する。プロファイルしたいシーンに移動したら、NSightウィンドウで「Capture for Live Analysis」ボタンをクリックする。すると、下図のような画面が立ち上がる。これを使ってフレーム単位の詳細なプロファイリングを行っていく。

f:id:ai_gaminglife:20201031211938j:plain

実際のNSightを使ってのプロファイリング手順は「Unreal Fest 2019」講演が詳しいので、そちらを参考のこと。

www.slideshare.net