citro3dを学ぶ #1 GPUのShader命令

完全に備忘録です。#1とありますが順番になっているわけではありません。内容はバラバラです。そもそも#2をかくかどうかすらわかりません。
今回はShader命令について書いていきます。オペランドの指定の仕方などはある程度はわかることを前提とします。



目次:

DP4

dp4 r0, r1, r2

一番出てくる。Dot Product 4-componentの略。四次元ベクトルの内積を計算します。ちなみにDot Productは内積という意味。
例: r0 = r1 ・r2

DP3

dp3 r0, r1, r2

dp4の三次元バージョン。三次元ベクトルの内積を計算する。
例: r0 = r1 ・r2

MOV

mov r0, r1

みんな大好きARMでお馴染みのmov命令!値コピー。
例: r0 = r1

RCP

rcp r0, r1

最初の要素の逆数を計算し、各要素に格納する。
例: r0[0 ~ n] = 1/r1.x

RSQ

rsq r0, r1

最初の要素の平方根の逆数を計算し、各要素に格納する。
例: r0[0 ~ n] =1/√r1.x

MUL

mul r0, r1, r2

ベクトルの各要素ごとで乗算をする。
例: r1(1, 2, 3, 4) * r2(0.1, 0.2, 0.3, 0.4) = r0(0.1, 0.4, 0.9, 1.6)

ADD

add r0, r1, r2

ベクトルの各要素ごとで加算をする。
例: r1(1, 2, 3, 4) + r2(0.1, 0.2, 0.3, 0.4) = r0(1.1, 2.2, 3.3, 4.4)

MAX

max r0, r1, r2

ベクトルの各要素ごとで最大値をとる。
例: r1(1.2, 4.9, 3.0, 2.5), r2(1.8, 3.4, 0.3, 2.5) -> r0(1.8, 4.9, 3.0, 2.5)

MIN

min r0, r1, r2

ベクトルの各要素ごとで最小値をとる。
例: r1(1.2, 0.9, 0, 0.5), r2(1.8, 3.4, 0.3, 0.5) -> r0(1.2, 0.3, 0, 0.5)

MAD

mad r0, r1, r2, r3

2つのベクトルを乗算して、4つ目のベクトルを加算する。
例: r0 = r1 * r2 + r3

おまけ

VSCodeを使っている場合は、pica200という拡張機能をインストールすることで、Shaderファイルが見やすくなるのでおすすめです。



よく使うものだけを紹介したので全てではありませんが、これだけでも理解すればだいぶ読めると思います。
数学の知識が少しいりますね。それでは。

マルチスレッドとイベント マイナーすぎる3DS備忘録

3DSでマルチスレッディングを行う際に使用するEventについて書いていきます。



イベントの作成

Result svcCreateEvent(Handle* event, ResetType reset_type);

引数にポインタを渡して作成したイベントのハンドルを取得しましょう。ResetTypeについてはこちらをご覧ください。



イベントを待つ

Result svcWaitSynchronization(Handle handle, s64 nanoseconds);

handleには作成したイベントのハンドルを渡しましょう。nanosecondはタイムアウトです。タイムアウトした場合、0x09401BFEが返ってきます。



シグナルの送信

Result svcSignalEvent(Handle handle);

handleはお察しの通りイベントのハンドルです。イベントにシグナルします。



イベントのクリア

Result svcClearEvent(Handle handle);

handleはイベントのハンドルです。ResetTypeをRESET_STICKYにした場合に使用します。RESET_STICKYだとイベントをクリアしないとシグナルがずっと送信されたままになります。クリアするともう一度イベントを待つことができます。




#include <stdio.h>
#include <3ds.h>

// イベントのハンドル
Handle eventHandle;

// スレッドのフラグ
bool runThread = true;

void threadMain(void *arg) {
    while(runThread) {
        // イベントを待つ
        svcWaitSynchronization(eventHandle, U64_MAX);

        printf("Hello World!\n");
    }
}

int main(void)
{
    // 画面を初期化
    gfxInitDefault();
    consoleInit(GFX_TOP, NULL);

    // イベントを作成
    svcCreateEvent(&eventHandle, RESET_ONESHOT);

    // スレッドを作成
    Thread threadHandle = threadCreate(threadMain, 0, 0x1000, 0x3f, -2, true);

    while (aptMainLoop())
    {
        // キー入力を更新
        hidScanInput();
        u32 key = hidKeysDown();

        if (key & KEY_START)
            break;

        // Aボタンが押されたら
        if (key & KEY_A)
        {
            // イベントにシグナルを送信
            svcSignalEvent(eventHandle);
        }

        // 画面を更新
        gfxFlushBuffers();
        gfxSwapBuffers();
        gspWaitForVBlank();
    }

    // スレッドのフラグを更新
    runThread = false;

    // スレッドはイベントを待機中なので、シグナルを送りスレッドを動かせる
    svcSignalEvent(eventHandle);

    // スレッドの終了を待つ
    threadJoin(threadHandle, U64_MAX);

    // イベントハンドルを閉じる
    svcCloseHandle(eventHandle);

    gfxExit();
    return 0;
}

Aボタンを押したらシグナルを送信し、待機スレッドはシグナルを検知したらHello World!を出力します。
例にある通り、使わなくなったイベントはsvcCloseHandleでハンドルを閉じましょう。



終わりに
前回よりはわかりやすいと思います。わからないことがあれば気軽にどうぞ!

EventのResetTypeについて マイナーすぎる3DS備忘録

ResetTypeとはlibctruで定義されているEventやTimerのリセットに使用される列挙型です。Eventのリセットに使用されるのは、

  • RESET_ONESHOT
  • RESET_STICKY

の2つです。今回はこの2つの違いについて書いていきます。


RESET_ONESHOT
RESET_ONESHOTは、Signalが送信され、Waitしているスレッドが動き出したとき自動でSignalがクリアされるのが特徴です。一般的に2つのスレッド(Signalするスレッド, Waitするスレッド)間で使用するときに用いられます。


RESET_STICKY
RESET_STICKYはRESET_ONESHOTと違い、Signalが自動でクリアされることはありません。2つ以上のスレッドが同時にその他のスレッドのSignalを待っているときに使用されます。自動でクリアされないのでクリアのし忘れに注意してください。



先ほどの説明をもう少しわかりやすく説明します。


例として3つのスレッドA, B, CとイベントXがあるとします。
スレッドA, Bは共にイベントXを待っています。

  • イベントXがRESET_ONESHOTでリセットされている時

この時、スレッドCがイベントXにSignalを送信すると、スレッドA, BのどちらかのみがWait状態から起動し、もう片方のスレッドはWaitし続けます。もう一度イベントXにSignalを送信するともう片方のスレッドも起動します。

  • イベントXがRESET_STICKYでリセットされている時

この時、スレッドCがイベントXにSignalを送信すると、スレッドA, Bの両方が起動します。もう一度イベントXを使用する場合はイベントXをクリアする必要があります。


いかがでしたでしょうか。今回は少し難しい内容になってしまいました。RESET_ONESHOTは1つのスレッド、RESET_STICKYは複数のスレッドに効果があるという認識で大丈夫だと思います。
それでは。

3DSにおけるPortとServiceの違い マイナーすぎる3DS備忘録

3DSにはsvcSendSyncRequestというプロセス間通信に使用する関数があります。この関数ではセッションハンドルを用いて他プロセスに処理を頼んだり、情報を取得したりすることができます。このセッションハンドルは、svcConnectToPortまたはsrvGetSessionHandleを用いて取得することが出来ます。svcConnectToPortはSVC(システムコール)で、srvGetSessionHandleはServiceManagerによって提供される関数です。
では、PortとServiceの違いとは何なのでしょうか。

とても簡単に説明すると、ServiceManagerによって管理されているPortをServiceと呼ぶのです。
ServiceManagerはPortを管理するプログラムであり、ServiceManagerに登録されたPortはServiceManagerを介してのみアクセスできます。
ServiceManagerに登録されていないPortの例としては、"srv:"(ServiceManagerそのもの)や"plg:ldr"(Luma3DSのみ)などです。よく使われる"fs:USER"や"hid:USER"などは全てサービスになります。

いかがでしたでしょうか。自分的にはこの違いがずっとわからずモヤモヤしてましたがわかるとなるほどってなりました。

以上

GSPGPU_FlushDataCacheについてのメモ マイナーすぎる3DS備忘録

GSPGPUってなんかいいですよね。
そんな中の関数の1つ!GSPGPU_FlushDataCache!これの正体はただ
svcFlushProcessDataCacheを呼び出すだけなんです!通りで引数が一緒な訳ですね。

終わり!

ciaファイルからのromfs抽出と展開

romfsとは、ゲームソフトにおける音楽やモデルのデータなどさまざまなものが入っているファイルである。ciaファイルにはromfsの他にもう1つ、exefsというものがあるが今回は触れない。romfsの抽出、展開はよくHackingToolkit9DSなどを用いて行われることが多い。しかし、HackingToolkit9DSでは稀にromfsの展開が上手くいかないことがある。実際に私もこの状況に遭遇したので、今回はHackingToolkit9DSを使用しないromfsの展開方法を紹介する。

前置きが長くなったが、実際にromfsを抽出していこう。

必要なもの

復号化済みのcia
・ctrtool
・3dstool

ciaの復号化はGodMode9にて行うことができる。

1.ciaからcontentsを抽出

ctrtool --contents=contents ファイル名.cia

ciaファイルがあるフォルダ上で上記のコマンドを実行してcontentsを抽出する。ファイル名にはciaの名前を入力。このコマンドを実行すると、ファイルが複数出てくることもあるが、今回重要になるのは、contents.0000.XXXXXXXX(XXXXXXXXには特定の数)という形をとる名前のファイルだけである。

2.contentsからromfs.binを抽出

3dstool -xvtf cxi contentsファイル --romfs romfs.bin

を実行し、romfs.binを抽出する。contentsファイルの部分には先程説明したファイルの名前を入力。するとromfs.binが出てくる。

3.romfs.binを展開

3dstool -xvtf romfs romfs.bin --romfs-dir romfs

を実行し、romfs.binを展開する。するとromfsフォルダに展開したものが入っている。

これでciaファイルからromfsを抽出し、展開することができる。HackingToolkit9DSでできなかった場合は是非試してみるといいだろう。

C++をよく理解してCTRPFを作ろう

C++をあまり理解せずにCTRPFを作ってる人がやりがちなことをまとめてみました。

あるある

謎の変数

u32 a;やfloat l;などはやめましょう。わかりにくかったり、重複する可能性があります。

バラバラな命名

1つのCTRPFのソースコード内にスネークケース(set_moneyなど),キャメルケース(skipStoryなど),パスカルケース(LoadUserInfoなど)などが混合しているものをたまに見かけます。関数名や変数名などで分けられているのであれば構いませんが、関数名だけでバラバラにするのはよくありません。libctrpfは基本パスカルケースで書かれているのでそれに準じて命名しましょう。

Process::Write地獄

アドレスや値がバラバラな場合は構いませんが、
Process::Write32(0xC3D0BEC, 0xFFFFFFFF);
Process::Write32(0xC3D0BF0, 0xFFFFFFFF);
Process::Write32(0xC3D0BF4, 0xFFFFFFFF);
Process::Write32(0xC3D0BF8, 0xFFFFFFFF);
Process::Write32(0xC3D0BFC, 0xFFFFFFFF);
などのコードはループで書けるのでなるべくループで書きましょう。
見た目も悪くソースコードの量も多くなってしまいます。

謎のoffset

Process::Write系で、offsetを使ってるコードをよく見ます。本当に必要か考えた上、使用しましょう。また、offsetをグローバル変数にするのはやめましょう。

謎の0x

for文やProcess::Write系でたまに見るのですが、0x0とかはやめましょう。

Keyboardのif地獄

int choice = Keyboard("", {"", "", …}).Open();

if(choice == 0)
{

}
if(choice == 1)
{

}
などは汚いのでswitch文を使うことをおすすめします。breakを忘れないよう注意してください。また、配列を使うことによって処理を省略出来る場合もあるので省略できる場合はそうしましょう。

キモイ命名

Game_coinなど、大文字と小文字と_が混ざった命名はやめまょう。

goto乱用

gotoは便利ですが、可読性が著しく低下するので乱用はやめた方がよいです。多重ループから抜ける場合のみに使用するようにしましょう。

ローマ字だらけ

変数名や関数名がローマ字だらけなのも可読性が低下する原因です。英語が苦手でもなるべく英語を使うようにしましょう。

ガッタガタのインデント

どこかからコピペした際にインデントがズレることがよくありますが、放置するのは絶対やめましょう。これは可読性が低下するどころか読む気すら失せてしまいます。

番外編

黒魔術

ZN18CTRPluginFramework16PluginMenuSearch13_RenderBottomEv(search);
if(_ZN18CTRPluginFramework6ButtonclEv(cancelBtn))
_ZN18CTRPluginFramework16PluginMenuSearch18_cancelBtn_OnClickEv(search);
このようなコードのことです。
これ面白いし大好きなんですが、ほんとソースコードが汚くなるのでやめまょう。


以上のことを避けることでよいコードが書けるのではないでしょうか。他にもあれば是非教えてください。