LoginSignup
96
84

More than 3 years have passed since last update.

RaspberryPiのVideoCore IV(GPU)を使ってOS無し(ベアメタル)からポリゴン出して遊ぶ

Last updated at Posted at 2016-12-14

■初めに■

※この記事は Raspberry Pi Advent Calendar 2016の15日目です。
http://www.adventar.org/calendars/1341

こんにちは。gyaboと申します。

ラズベリーパイ初代を結構前に買ってから、ちょうど一年前位にラズベリーパイで遅刻しますメール出したりで使っていたのですが、禁止されたので別の方面で使ってやろうと思ってました。
みんな物理的な方面で有効活用されていて面白いのですが、勉強がてら、もうちょっと別なことをします。

■何をするか? → ベアメタルでブートさせて、ポリゴン出して遊びます■

ベアメタル(OSが何も搭載されていない状態)のRaspberryPIで、搭載されているGPU VideocoreIVの機能のV3Dを使って
HDMI経由で表示されたディスプレイにポリゴン出して遊びます。

ポリゴンといっても、頂点流し込み、テクスチャマッピング、ゆるいシェーダアセンブラ書いたら終わりにします。

主に前準備、起動、V3Dの初期化、フレームバッファへのアクセス、binning, rendering, V3D回りのお作法回り、
落とし穴、有効なツールのご紹介などができればと思っています。
勉強中なので間違いなどがありましたら教えてください。

■調査期間■

あんまり覚えていないのですが、1.5 - 2.0月くらいで集中して遊んでました。
今年の3~4月くらいです。あとはちょくちょく。調べ貯めていたものです。

■これまで■

RaspberryPIはOSイメージを適切にddしてあげればサクッと立ち上がってあとはterminalなりで
適当につないでやりたいことをさせる、というのが手早く、賢い使い方だと思っています。
使うOSイメージでOpenGLES2相当のAPIが叩けるので別に要らないじゃん、って思ったのですが
ベアメタルからV3Dを直接叩いて遊んでる方が海外にいらして面白いと思い、遊んでみようと思ったのです。
また日本語の記事が極めて少ないので勉強したことを書きたいと思いました。

RaspberryPIのブートはざっくりいうとVideocoreIVが最初にSDIFを初期化してSDカードのファイルシステムを解析から読み込みを行います(仕様とバイナリのみ提供されていて起動には必須)

その後RAM側にイメージが転送されてARM側が起動します。つまり、オンボードのRAMの必要最低限の初期化もすでに終わってる状態です。
あとは何しようがやりたい放題です。

で、「タイルアーキテクチャで動作するGPU」の下回りのレジスタを間接的に叩いて描画までの流れの学習用としてはうってつけだと思います。当然ほかの小さいガジェットボードでGPU搭載されていて直接たたけるのがあればできそうですが、
まあRaspberryPIでやっても変わらないじゃろう。

■必要な知識や手技■

  • コマンドラインから何かするのに抵抗が無い事
  • ARMのアセンブラが若干読み書きできること
  • 基本的な挙動を知っていること
  • SDLとかでフレームバッファにアクセスして絵を出したことがある
  • 英語がちょっと読める
  • 組み込み系の下回りでプログラムで何か動かしたことがある

尚、ARM側はGPU側を中心に制御するために存在するようなものなのでARM側でなんでもできるというわけではないです。

■前提■

  • RaspberryPI 2で遊びます。
  • RaspberryPI -> rpiと略記することがあります。統一していない場合は申し訳ない。
  • 作り置きのサンプルがあるのでそれを元に解説します
  • VPM(頂点を蓄えてQPUでを使う)回りはしません。つまりVertexShaderは扱いません
  • FragmentShader相当のQPUアセンブラを書きます
  • ですます混ざってます。すみません
  • 後半に出てくるControlList回りは説明がいまいちかも

■予算■

RaspberryPI本体とほかの適当なボード、キット込みで11,000円程度を見込んでおきます。

■基本資料■

  • 公式のRaspberryPIのSoCの基本的な周辺機器のレジスタ構成が記載されている仕様書。

レジスタのアドレスはRaspberryPI1, 2で異なること、ほかに機能が大量にあるのに記載されていないこと(端的に言って不十分)を理解して読む必要があります。

http://www.broadcom.com/docs/support/videocore/VideoCoreIV-AG100-R.pdf
※最新のリンクを取り込みました。 @matobaaさんありがとうございます。
https://www.broadcom.com/support/download-search?pg=Legacy+Products&pf=Legacy+Broadcom&pn=BCM21553&pa=Reference+Manual&po=&dk=

RaspberryPIに使われているGPUの仕様書です。公式PDFと記載した場合はこのVideoCoreIVの仕様書をさします。
先の周辺機器のPDFと合わせて読むと理解が深まります。
テーマは絵を出すことなので、コードを書くときはこの仕様書とgithubのサンプルが中心になります。

■情報を集める■

幸いなことにいろいろな人がBareMetalで動かしています。

https://github.com/phire/hackdriver
絵を出したいというテーマで調べたら最初に見つけたもの。userland層からコマンドを発行してフレームバッファにポリゴンを書き出すソースで、
かなりシリアルに書かれてるので読む分には良いです。

https://github.com/PeterLemon/RaspberryPI
ベアメタルで何から何まであるすごい。repo見るとN64のベアメタルまである。クレイジーっす。
このrepoの方は本家forumsにいろいろ投下しています。flatassemblerの超高性能マクロアセンブラで作成している模様。
[参考] https://flatassembler.net/

https://github.com/ICTeam28/PiFox
もうこれで十分なんじゃね?というスターフォックスっぽいdemo.
サウンドはDMA転送 + PWMにて実装、描画はソフトウェアレンダリングです。
入力周りはGPIOでとってきています。正直このくらいできればなんでもできそうな気がしてくる。

https://github.com/Microsoft/graphics-driver-samples
天下のMicrosoftのDirectX11相当APIのVideoCoreIVの実装。事実上低レベル層のグラフィックドライバになります。
APIは、DirectX11っぽい感じになってるけど実装できていない箇所は結構ある。
が、VideoCoreIVで扱えるテクスチャの形式のT-FORMATの実装だったり、シェーダアセンブラ(QPU)があったりなど、
割とかゆいところに手が届くソースを見ることができる。素晴らしい。今はあまりメンテナンスされていない感じ。

■RaspberryPIがBootする仕組み■

以下が詳しいです。

https://www.RaspberryPI.org/forums/viewtopic.php?t=72260
最初はGPU側のFirmwareが全部主導権を握っていることに注意することと、
SDRAMの初期化だったりディスプレイの初期化なども全部やってくれます。
気を付けなければならないのは、config.txtでブートの挙動が大きく変わるので以下を見ながら注意。
http://elinux.org/RPiconfig
※本家のドキュメンテーションが正義。ただ見づらいので↑がよろしそう。

[参考 : Raspberry Pi boot process]
https://www.raspberrypi.org/forums/viewtopic.php?f=63&t=6685

[参考 : Raspberry Pi2(Linux Kernel)のブートシーケンスを読む(その1) アーキテクチャ依存部]
http://qiita.com/Nao1215/items/b8f866b4ede757cdaa73

■開発するためのツール■

@コンパイラ
launchpad.net/gcc-arm-embeddedを使うことにします。インストールしたらbin回りにpathを通しておくこと。
https://launchpad.net/gcc-arm-embedded

@uartからUSBに変換してシリアル通信できる変換接続ボードと適当なケーブル

FTDI USBシリアル変換アダプター(5V/3.3V切り替え機能付き)
https://www.switch-science.com/catalog/1032/

これを使ってTeraTermとやり取りします。これが無いとデバッグが大変にしんどいです。
あたりまえだけどこの治具は絶対に5VにDIPを倒さないで使うようにします。rpi側のGPIOが3.3Vだからです。
焼けます。1枚焼きました私。

最初Lチカで何とかデバッグしてたけどしんどくなったりつらいバグが大量に出たりしたので、上の板を素直に使います。
接続方法は以下の電子書籍が極めて親切で具合が良いです。

BareMetalで遊ぶ Raspberry Pi - 達人出版会
http://tatsu-zine.com/books/raspi-bm

また接続する際はチクチクさすケーブルがどうしても必要になるので、以下のセットなどを探すか秋月電子で頼む。
私は以下を注文した。とても安いしお得。
https://www.amazon.co.jp/dp/B01AUN1JYW
※これ原価割れしているのではと思うくらい安い。

@TeraTerm
https://ttssh2.osdn.jp/
おなじみ定番ターミナルソフト。COMポートとつながるなら何でもいいです。
RaspberryPI側からPC側に文字列送信してデバッグするときに必要。いろいろな使い方ができる。

@SDカード(rpi2ならmicroSDカード)
何をやるかに依りますが、1GByteないし4GByteあれば十分。
FAT32でフォーマットして中にfirmwareと自分で作ったkernel.imgをコピーして開発してくことになります。

@カードリーダ
できるだけ安くて、カードの抜き差しで簡単に壊れないタフなものを使うこと(重要)

@HDMIケーブル
グラフィック出したいので、HDMIケーブルが必要。音声回りは特に何も必要ないので、音でなくても大丈夫。

■RPI1,2の違いについて■

GPIO回りとコアとレジスタのベースアドレスが違うので本家参照してください。本ページで開発に使うのは、uart-functionを使うことくらい。
ここのサイトが詳しい。
http://elinux.org/RPi_Serial_Connection

■ とりあえずfirmwareをブートさせる■

以下の手順でブートさせる。

- SDカードをフォーマットする
- 本家サイトからブートローダのバイナリ(start.elf, bootcode.bin)を拾ってきて、SDカードのルートにコピーする。
- config.txtを作成してSDカードのルートにコピーする。
- HDMIケーブルとrpiを接続して適当なHDMIモニタに接続する。
- microUSBケーブルと適当なUSBケーブルをつないでrpiの電源を入れる。
- うまくいけば何か表示される

私が作成したconfig.txtは以下です。

config.txt
kernel=image.bin
hdmi_drive=2
hdmi_group=2
hdmi_mode=4
disable_pvt=1
disable_overscan=1
force_turbo=1
fake_vsync_isr=1

config.txtは以下の意味を持っています

kernel=image.bin    #ブートローダが読み込むファイル名をimage.binに変更。単にkernel.imgという名前が嫌だったので。別に無くてもいいです。 
hdmi_drive=2        #HDMI mode。サウンドがサポートされているなら出力まで行う(baremetalだとたぶん1でもOK。HDMIに音声出力する口が今のところ無い)
hdmi_group=2        #DMTモード。HDMIのPCディスプレイに接続するのでとりあえず。CEAでも動く。
hdmi_mode=4         #640x480 60Hzの解像度に設定。解像度は作りたいもの、趣味の問題。今回いろいろ計算した結果640x480x32 60Hzでなんか作ろうということに。
disable_pvt=1       #RAMのリフレッシュレートの500ms縛りを無効にする。趣味の問題。
disable_overscan=1  #ディスプレイのオーバースキャンモードを無効にする。
force_turbo=1       #GPU, SDRAM, CPUを全速力のクロックモードにする。
fake_vsync_isr=1    #この後使うはずの、フレームバッファに描いた垂直同期をとるために必要になる。

config.txtは割と曲者で、ブートの挙動やレジスタの挙動に影響を与えるので注意が必要になります。
ベアメタルの場合、基本は必要な事以外は記載しないことが重要。つまらんことで時間を使うことになります。
あとは他のサイトにあるstart.elf, bootcode.binは絶対に使わないことです。
古いだけじゃなく普通に起動しない場合があります。
まだコードを書いていませんが、config.txtに記載してあるimage.binを生成してベアメタルしていくことになります。

起動しない場合

LEDランプがチカチカする。チカチカパターンは以下のサイトが詳しいです。

3 flashes: loader.bin not found
4 flashes: loader.bin not launched
5 flashes: start.elf not found
6 flashes: start.elf not launched
7 flashes: kernel.img not found

Firmware since 20th October 2012 no longer requires loader.bin, and the flashes mean:
3 flashes: start.elf not found
4 flashes: start.elf not launched
7 flashes: kernel.img not found
8 flashes: SDRAM not recognised. You need newer bootcode.bin/start.elf firmware.

上記でRaspberryPIの起動失敗要因が大体わかります。

[LEDのチカチカについて若干補足]

ブートローダが起動するとどうやらGPU側でvcos(VideoCoreOS?)というのが立ち上がった状態になって、
rpiのメインのコアからはmailboxという仕組みで通信をすることになります(後述)
mailboxの仕様の中で、HDMIに関する仕様が存在するが、変なconfig.txtのパラメータを渡すと、8 flashesの状態になる。
推測の域を出ないけど、vcosのuserlandで以下のコードを見ることができます。

つまりvcos側がabortになった場合でも8 flashesが発生する可能があります。
たいして原因わからんな。

■ Bare Metal : ブートコードを書く■

まず、基本となるベースのサンプルダウンロードは ⇒ rpi2_boot1.zip

firmwareは、正常に起動したら直近のブートのために以下を実行します。

  • kernel.img(image.bin)をSDカードから読み込んで
  • SDRAMの0x8000に書き込み、PCをJump

開始アドレスはconfig.txtでも変更することができますが、0x8000にしておいても別に十分じゃろう。

私が書いた必要なブートコードはメモリマップを記載するリンカコードと、
起動直後にスタックレジスタ回りなりを初期化して、Cソースにジャンプするものを書いています。
全部アセンブラで書く必要はないです。
また、winのバッチでbin作成するのでmakefile書く必要もない。batファイルだけで十分です。
書きたい人は書いて便利にするといいじゃろう。

シリアルボードと、USBがうまくつながってるなら、以下の通りメッセージが出ます。

たーみなる

ボーレートは115200に設定しているので、TeraTerm側も設定しておきましょう。
シリアルでデバッグができるようになればいかようにでもなるので絶対に最初にやっておきましょう。デバッグの効率が格段に違います。
また、ELFを生成してメモリマップ吐いて確認しておくとうっかりミスを防ぐことができます。ここまでできたら後は根気の問題です。

私の開発では、以下の繰り返しをして開発しています。

・ソースを書く。
・コンパイルしてバイナリを生成。
・SDカードにコピーする
・SDカードをrpiに差し込んでUSBで電源を入れる
・シリアルと画面を確認する
・最初に戻る

私は上記を適当なWindowsのバッチファイルを書いて対応しています。
make自体はでかくないので数秒で終わる。あとはSDカードをカードリーダーに接続してコピー(これもドライブ固定でバッチたたくだけでOKにしておく)して、
rpiにさしこんで~の手順を何度も行えばOKです。

通常クロス開発ならエミュレータやICE(という古いのもある)使いますが、
今rpiでやりたいことはV3D機能を使って画像を出すことです。で、個人で手に入るようなクロス開発環境がありません。
なので実機でひたすらトライアンドエラーを繰り返すことになります。幸いrpiはブート回りが堅牢にできているので、焼きミスったりしても
SDカードを宜しくフォーマットすれば復帰するのでダメージはかなり小さいです。

もちろん詳しい人はシリアルからバイナリを書き込む単純なコマンド(ダウンローダ)を自作して、
CPUレジスタを全部リセットして、SDRAM側に展開してジャンプするの繰り返しで、物理操作なしで開発のイテレーションを高速化することも可能です。
ここは趣味と開発速度とのトレードオフとなります。ダウンローダは設計が重要なので、適当に作るとさっくり動作してくれない認識です。
が、作るのが得意な方はぜひとも自作してみてください。

トラブル

  • @シリアルポートが動作しない
    レジスタの設定が間違ってるので見直すこと。

  • @焦げ臭い、焼いてしまった
    火事です。注意しましょう。シリアル変換ボードのレベル変換が正しくないとボードを焼いてしまう。RPI側は3.3Vであることに注意。
    変換ボードのディップは絶対に注意しながら設定すること。

  • @ターミナルにごみしかでない
    2つ原因候補がありそう。
    (1) レジスタ設定が合っていない -> レジスタ設定を見直すこと
    (2) ターミナル側のボーレートが合っていない -> ターミナル側のボーレートを期待している値にしてみる(サンプルは115200)

こんな感じで。
ぼーれーと

  • @起動すらしない

start.elf, bootcode.bin, config.txtがない可能性と、フォーマットがうまくいっていない可能性もある。
最初から手順を実行してみましょう。

■mailboxについて■

mailboxと呼ばれるGPUとのやり取りに使われる仕組みがあります。
フレームバッファのアドレス、テクスチャマッピングに使うVRAMの確保などは
すべてこのmailboxで取得したデータを使います。

アクセス方法は以下に記載されています。レジスタアクセスでやり取りします。
https://github.com/raspberrypi/firmware/wiki/Accessing-mailboxes

Bootで使ったソースのmailbox.cに具体的な方法が記載しました。よろしければ参考にしてみてください。
以下の手順でアクセスします。

  • 16byte-alignされた適当な領域に上記のサイトに記載されているコマンドを書き込み
  • ARMのアドレスからVCのアドレスへゲタを履かせたポインタを作成して
  • mailboxの書き込みキューアドレスに書き込んで
  • mailboxのレスポンス(結果)を待つ

mailboxとのあらゆるデータのやり取りは上記を繰り返します。

■フレームバッファのアドレスを取得して絵を出す■

GPU側と通信して、フレームバッファを取得します。
フレームバッファの取得は本家サイトが参考になります。

Mailbox-framebuffer-interface
https://github.com/RaspberryPI/firmware/wiki/Mailbox-framebuffer-interface

特にフレームバッファの場合、すでに固定で機能が割り振られているので一発でフレームバッファが取れます。


実は適当にfirmwareリリースを見るとこの機能はdeprecatedらしいですが、下位互換性のために仕組みを残しているようです。
ソース失念。比較のために上記機能とframebufferを別個取る方法で速度や手間を見ましたが、上記の一発取得の方が良さそうです。楽。

サンプルソースではクラシカルなXORテクスチャを計算して表示しています。
簡単にするために以下のI/Fをmailboxのサンプルソースに書いてます。

mailbox_fb_init : フレームバッファ初期化
mailbox_fb_ptr : フレームバッファの先頭ポインタ(VideoCore側)取得

以下の通り絵が出ていると思います。

和菓子っぽい

ここまで来れば割といろいろできます。

■ポリゴン出すための概要■

説明に使うソース -> http://gyabo.sakura.ne.jp/prog/RPI2_TRIANGLE_DEMO5.zip
さて、下準備ができましたので、VideoCore IV(GPU)でテクスチャ付きポリゴンを出すための手順を整理します。
以下の手順となります。

(1) QPUを有効にする(そうしないとV3Dのレジスタが見えない)
(2) フレームバッファを取得する(前述)
(3) V3D_L2CACTLでL2キャッシュを有効にする ※単にポリゴンだけ出したいなら不要
(4) テクスチャを作成して転送 ※単にポリゴンだけ出したいなら不要

while {
  (5) binningのコントロールリストを設定(タイル数、Viewport設定、MSAAx4の設定、頂点バッファの設定など)
  (6) renderingのコントロールリストを設定(レンダリング対象の領域へbinningで得られた情報をどのタイルに割り当てるかのリンクリストを作成)
  (7) binning処理発行(binningのコントロールリストを処理)
  (8) rendering処理発行(renderingのコントロールリストを処理)
  (9) 公式提供のfake_isrでvsyncを取る
}


(7), (8)は遅延させて実行することも可能ですが、簡便のためにこの手順にしています。

サンプルソースでは上記の面倒処理を、mailbox.hと、v3d.hのヘッダに詰めてます。
まだかなり生書きの状態で見づらいかもしれませんが、PDFと併せて参考にしてみてください。

1つ1つ解説します。

■(1) QPUを有効にする(そうしないとV3Dのレジスタが見えない)■

サンプルの関数 : mailbox_qpu_enable を使ってください。


//https://github.com/phire/hackdriver/blob/master/test.cpp
uint32_t mailbox_qpu_enable() {
    int i  = 0;
    p[i++] = 0;          // size
    p[i++] = 0x00000000; // process request
    p[i++] = SET_CLOCK_RATE; // (the tag id)
    p[i++] = 0x00000008; // (the tag id)
    p[i++] = 0x00000008; // (the tag id)
    p[i++] = 5;          // V3D
    p[i++] = 250 * 1000 * 1000;
    p[i++] = ENABLE_QPU;    // (the tag id)
    p[i++] = 4;          // (size of the buffer)
    p[i++] = 4;          // (size of the data)
    p[i++] = 1;
    p[i++] = 0x00000000; // end tag
    p[0]   = i * sizeof(uint32_t); // actual size

    do {
        mailbox_write(MAILBOX_ARM_TO_VC, ArmToVc((void *)p));
        mailbox_read(MAILBOX_ARM_TO_VC);
        if(p[1] == 0x80000000) {
            break;
        }
        uart_debug_puts("mailbox_qpu_enable FAILED p[1]=\n", p[1]);
    } while(1);
    uart_debug_puts("mailbox_qpu_enable OK \n", p[1]);
    return p[1];

}


を見てください。公式には全く書いてないのですが、上記処理が無いとQPUのレジスタに全くRead/Writeできません。
これは起動直後一回やればOKです。尚、ソフトリセット直後だとRead/Writeできるのですが、
電源を完全に切った後だとNGである事がわかっています。起動毎にやりましょう。

20200613追記
Raspberry PI4では電源を入れる方法が変わっています。
上記では電源が入らずレジスタreadの結果が0xDEADBEEFとなり見えません。
rpi4の場合は上記ではなく、以下の通り、PM側からGRAV3DのcoreをresetしてからASB経由で電源を入れてください。
usleepしているのは差し当たってなので実際にはstatusbit確認して電源ONを確認してください。


//ref:https://github.com/raspberrypi/linux/blob/75f1d14cee8cfb1964cbda21a30cb030a632c3c2/drivers/soc/bcm/bcm2835-power.c#L283
//ref:https://elinux.org/BCM2835_registers#PM_GRAFX
#define PM_MAGIC         (0x5A000000)
#define PM_GRAFX         (0xFE10010C) //SUBSYSTEM_BASE 0xFE000000
#define PM_GRAFX_V3DRSTN (0x40)
#define ASB_RPIVID_BASE  (SUBSYSTEM_BASE + 0xC11000)
#define ASB_RPIVID_M     (ASB_RPIVID_BASE + 0x8)
#define ASB_RPIVID_S     (ASB_RPIVID_BASE + 0xC)
#define ASB_RPIVID_MAGIC (0x5A000000)

void
v3d_power_on()
{

    uint32_t reg = PM_GRAFX;
    IO_WRITE(reg, PM_MAGIC | IO_READ(reg) | PM_GRAFX_V3DRSTN);
    usleep(1000);
    IO_WRITE(ASB_RPIVID_S, ASB_RPIVID_MAGIC | (IO_READ(ASB_RPIVID_S) & ~1));
    usleep(1000);
    IO_WRITE(ASB_RPIVID_M, ASB_RPIVID_MAGIC | (IO_READ(ASB_RPIVID_M) & ~1));
    usleep(1000);
}

rpi4の電源を入れるだけのサンプルは以下に用意しています。参考にしてみてください。
https://github.com/kumaashi/RaspberryPI/tree/master/RPI4/v3d_power_on

■(2) フレームバッファを取得する■

前述の通りmailbox経由でフレームバッファの先頭アドレスを取ります。
サンプルの関数 : mailbox_fb_initを参考に以下に記載します。


int32_t mailbox_fb_init(uint32_t w, uint32_t h) {
    volatile mailbox_fb *fb = mailbox_fb_getaddr();
    fb->width   = w;
    fb->height  = h;
    fb->vwidth  = fb->width;
    fb->vheight = fb->height;
    fb->pitch   = 0;
    fb->depth   = 32;
    fb->x       = 0;
    fb->y       = 0;
    fb->pointer = 0;
    fb->size    = 0;

    int count = 0;
    do {
        mailbox_write(MAILBOX_FRAMEBUFFER, ArmToVc((void *)fb));
        mailbox_read(MAILBOX_FRAMEBUFFER);
        if(fb->pointer) {
            break;
        }
        count++;
        usleep(0x1000);
        uart_debug_puts("MAILBOX_FRAMEBUFFER count=\n", count);
    } while(1);
    is_fb_init = 1;
    return 0;
}

■V3D_L2CACTLでL2キャッシュを有効にする■

テクスチャを使うために有効化します。理由ですが、公式PDFの
Section 2: Architecture Overview -> Figure 1: VideoCore® IV 3D System Block Diagramを見てください。
L2 CACHE経由でTMUにデータが各QPUに渡されます。概要は以下抜粋。

There is nominally one Texture and Memory lookup Unit (TMU) per slice, but texturing performance can be scaled by adding TMUs.
Due to the use of multiple slices, the same texture will appear in more than one TMU. Each texture unit has a small L1 cache.
There is an L2 cache (L2C) shared between all texture units

■(4) テクスチャを作成してプロパティを設定してメモリに格納■

以下の手順でテクスチャを作成、メモリに格納します

  • テクスチャを仕様に従って作成
  • mailbox経由でテクスチャ用のメモリを取得(ドキュメントだとhandleという4byteのアドレスが取れる)
  • アドレスをVideoCoreアドレスからARM側のアドレスに変換
  • shaderに渡すuniformのアドレスをV3Dに設定
  • テクスチャのMINFILTER, MAGFILTERのON/OFF設定

◆テクスチャのメモリフォーマット

さて、まずテクスチャですが、公式PDFのSection 11: Texture Memory Formatによると、microtileという単位で扱われます。
※図を抜粋

image

このmicrotileを決まった順序で並べた1kByte分のフォーマットが以下です(T-FORMATといいます。PDF抜粋)
※図を抜粋

image

えらいめんどくさいですが、仕様上TMUからフェッチする画像ピクセルデータは上記のフォーマットでGPUに渡さなければなりません。
このフォーマットじゃないと意図したピクセルの並びになりません。

私は面倒だったのでLUTを使って256x256に限定してテーブルを作成して画像を流し込むことにしました。
テーブル作成はmicrosoftのサンプルソースの以下の仕組みを改造して作りました。

次にテクスチャメモリ確保の話です。詳しい話は公式PDFの「Texture and Memory Lookup Unit Setup」に記載されています。

サンプルソースの : mailbox_allocate_memoryで行います。注意があって、mem_align=4kbyte単位で確保してください。
※公式PDF「Texture Base Pointer (in multiples of 4Kbytes)」より

サンプルでは256 x 256 x 4のテクスチャを渡すので、4096 x 64 byte分のメモリが必要です。
併せて、PrimitiveDraw時に渡すuniformアドレスで、かつtextureとして使用する場合、先頭32Byte x 2の領域は
Texture Config Parameter0, 1に使用されます。
(※すみません、この辺りもっと読み込めればよかったので指摘ください)
上記を踏まえてバイナリデータを構築します。サンプルソース抜粋は以下です。


void V3DSetupTexture(V3DContext *ctx, uint32_t num, uint32_t width, uint32_t height, void *data) {
    const uint32_t mem_align = 4096;

    V3DTextureInfo *p = ctx->texture_info[num];
    p->handle = mailbox_allocate_memory(4 * (width * height) + mem_align, mem_align, 0);
    if(p->handle) {
        p->addr = mailbox_lock_memory(p->handle);
        p->uniform_param = p->addr;
        p->addr = VcToArm(p->addr);
        p->width = width;
        p->height = height;

        uint32_t *dest = (uint32_t  *)(p->addr + mem_align); //メモリアライン1つ分離れた箇所からテクスチャが始まるようにする。これは適当。
        uint32_t *src = (uint32_t  *)data;
        uint32_t *param = (uint32_t *)p->addr;
        param[0] = 0;
        param[0] |= p->addr + mem_align; //メモリアライン1つ分離れた箇所からテクスチャが始まるようにする。これは適当。

        param[1] = 0;
        param[1] |= ( (width ) << 8);
        param[1] |= ( (height) << 20);
        /*
        param[1] |= (1 << 7); //MAGFILTER無効になる
        param[1] |= (1 << 4); //MINFILTER無効になる
        */
        param[2]  = 0;
        param[3]  = 0;

        const uint16_t *table = GetLTFormat256x256(); //LUTを使ってRAWをT-FORMATに変換
        for(int i = 0 ; i < width * height; i++) {
            uint32_t offset = table[i];
            dest[i] = src[offset];
        }

        mailbox_unlock_memory(p->handle);
    }
}

※一番面倒だったところです…

■(5) binningのコントロールリストを設定■

※ここから説明が頓珍漢になるかもしれません。すみません。

タイルアーキテクチャGPUで有名なのはPowerVRシリーズがあります。
https://imgtec.com/powervr/graphics/architecture/

詳細はWikiに譲ります。
https://ja.wikipedia.org/wiki/PowerVR

binningは、画面をタイルと呼ばれる小さい面で分割してレンダリングするための前準備を行うphaseです。
renderingはbinningで作成した情報を用いて実際にレンダリング(RAMにピクセルを書き出し)するphaseです。

VideoCoreIVにおいてbinning, renderingを行うためにはV3DのControlListと呼ばれるものを構築します。
ControlListはControlCodeと呼ばれるコマンドのバイナリ列で構築されています。
このコマンドをV3Dのbinning実行用レジスタと、rendering実行用レジスタに設定して実行することで
コマンド列で指定されたRAMの先にレンダリング結果が書き出される、という仕組みになっています。
カンの良い方は気づいたかもしれませんが、RAMの先 == フレームバッファにしてあげれば画面上にレンダリングできる、というわけです。

公式PDFのSection 9: Control Listsに詳細なコマンドが記載されています。非常に多いのでここではマルゴト記載はしません。

サンプルソースでは必要な前処理は以下の関数に全部詰めました。

v3d.c : V3DBeginScene
v3d.c : V3DSetDrawPrimitive
v3d.c : V3DEndScene
v3d.c : V3DControlListSetupRendering ※renderingで解説します

内部では公式PDFのControlListsのテーブルから値を決定し、binningに必要なコマンドを並べて
後でレジスタに渡す適当なメモリの先頭アドレスから徐々に詰めていきます。上記を並べて実行すると以下を行います。

・binningであることを示すコマンド書き込み

・binningするためのタイルサイズ、タイル数、タイルのWidth, Height決定、binningの結果を転送するアドレスを指定

・binning開始

・クリッピング設定(Viewportの設定、Viewportのオフセット)

・プリミティブのCCW設定

・MSAA有効化

・binningステート初期化

・作成したポリゴンの頂点、シェーダの情報(V3DShaderInfoFmt)を設定

・描画するポリゴンの頂点フォーマットを設定(サンプルではglDrawArrayで描画するのと一緒にしています)

・binningのControlListのFLUSH : 必須らしい

・binningのControlListの終了 : NOP処理で終わる

・V3DControlListSetupRendering : 次で解説します

関数を実行すると上記が行われますが、まだ描画は行われません。binningをするための前準備だけです。

■(6) renderingのコントロールリストを設定■

binning同様、rendering用のControlListを構築します。

サンプルだと関数 : V3DControlListSetupRenderingに全部詰めました。
実行すると、以下のControlListを構築します。

・renderingであることを示すコマンド書き込み

・ClearColorの色を決定

・フレームバッファのWidth, Height, アドレスを指定

・binningで書き出されているはずのアドレスをタイル毎に設定する(丁寧にアドレス計算を行う)

・renderingのControlListの終了 : NOP処理で終わる

ここで初めてどのタイルがどのbinning情報を使用するかが決定されます。

まだ描画は行われません。renderingをするための前準備だけです。

■(7) binning処理発行(binningのコントロールリストを処理)■

サンプルだと関数 : V3DControlPresentBinningに全部詰めました。
やっていることは以下の通りです。

・レジスタV3D_BFCに1を書き込んでflushカウンタをリセットする

・レジスタV3D_CT0CAにbinningのControlListの先頭アドレスを渡す

・レジスタV3D_CT0EAにbinningのControlListの終了アドレスを渡す(この時点でbinningが開始される)

・V3D_BFCをポーリングして、flushされたか監視する

終わらない場合(何かbinningの設定が間違ってるなど)を想定して適当なカウンタを設けて、
デバッグできるような仕組みを詰めています。
終了すると、「(5) binningのコントロールリストを設定」で設定したbinningの書き出し先のアドレスに
rendering用のデータが書き込まれます。

■(8) rendering処理発行(renderingのコントロールリストを処理)■

まだ続きます。
サンプルだと関数 : V3DControlPresentRenderingに全部詰めました。
やっていることは以下の通りです。

・レジスタV3D_RFCに1を書き込んでflushカウンタをリセットする

・レジスタV3D_CT1CAにbinningのControlListの先頭アドレスを渡す

・レジスタV3D_CT1EAにbinningのControlListの終了アドレスを渡す(この時点でrenderingが開始される)

・V3D_RFCをポーリングして、flushされたか監視する

終わらない場合(何かrenderingの設定が間違ってるなど)を想定して適当なカウンタを設けて、
デバッグできるような仕組みを詰めています。
ご参考までに。


この処理が終わって初めて目的のRAMにピクセルとポリゴンがレンダリングされます。

が、まだvsyncが残っています。

■(9) 公式提供のfake_isrでvsyncを取る■

vsync、つまり垂直同期をとります。

[参考]
https://github.com/raspberrypi/firmware/issues/67

公式のコメントを参考にvsync用のコードを作成しました。以下の通りとなります。


void fake_vsync(void) {
    //NEED config.txt -> fake_vsync_isr=1
    IO_WRITE(IRQ_GPU_ENABLE2, IRQ_GPU_FAKE_ISR);
    IO_WRITE(SMI_CS, 0);
    while( (IO_READ(IRQ_GPU_PENDING2) & IRQ_GPU_FAKE_ISR) == 0 );
}

これでフレームバッファの垂直同期ができます。
レジスタはARM側であることに注意してください。

なお、firmwareは新しいのにしてください。config.txtのfake_vsync_isr=1を有効にするためです。
初期のfirmwareだとこの機能が無かったようです。

これで初めて1フレーム描画できます。お疲れ様でした。

■その他 : 頂点フォーマットについて■

公式PDFのVPM Vertex Data Formatsに記載があります。
※資料抜粋

image

X座標とY座標は固定小数点の符号付き整数です。12bitが実数部、4bitが小数部で扱います(12.4)
それ以外は全部32bit浮動小数点フォーマットです。

慣れている方にはなんだと思うかもしれませんが、ディスプレイ上の(100, 100)の頂点を表すためには
x = 100 << 4;
y = 100 << 4;
とします。上記の座標をXs, Ysに渡します。

なお、ARMで浮動小数点を使う場合VFPを有効にしなければなりません。
サンプルではboot.sでレジスタの設定を行いましたので、参考までに。

■その他 : QPUのアセンブラ(事実上のシェーダ)について■

QPUのアセンブラについては仕様が公式PDFのQPU Instruction Encodingに記載されています。
この辺は非公式ながら強力なアセンブラがあるのでご紹介します。

nodejsで動作します。使い方は以下の通りです。

node qpuasm.js ファイル

あとは公式を参照してください。

アセンブラパイプラインの最適化は以下の記事が詳しいです。
http://qiita.com/9_ties/items/15ab7fa198991a61a3a9

今回のポリゴン描画に使用するためのシンプルなアセンブラを以下に記載します。


# Assemble with qpuasm.js from github.com/hermanhermitage/videocoreiv-qpu

.global entry

entry:
        #Red成分をVaryingで作る
        mov  r0, vary   ; nop #varyingレジスタはQueueになっているので読みだすたびにvarying成分が取れる
        fadd r0, r0, r5 ; nop; sbwait 
        nop             ; mov  r3.8a, r0

        #Green成分をVaryingで作る
        mov  r0, vary   ; nop
        fadd r0, r0, r5 ; nop
        nop             ; mov  r3.8b, r0

        #Blue成分をVaryingで作る
        mov  r0, vary   ; nop
        fadd r0, r0, r5 ; nop
        nop             ; mov  r3.8c, r0

        #Alpha成分をVaryingで作る
        mov  r0, vary   ; nop
        fadd r0, r0, r5 ; nop
        nop             ; mov  r3.8d, r0

        #UV座標をuniformから取得
        mov  r2, 0.0    ; nop
        nop             ; mov  unif_addr, r2
        mov  r2, 0.0    ; nop
        nop             ; mov  unif_addr_rel, r2

        #実はここまでの計算は読み飛ばし。必要に応じてtlbcに入れて色を出す

        #r0にポリゴンのU座標の補間係数をかけてU座標を算出して格納しておく
        fmul r0, vary, ra15
        fadd r0, r0, r5 ; nop

        #r1にポリゴンのV座標の補間係数をかけてV座標を算出して格納しておく        
        fmul r1, vary, ra15
        fadd r1, r1, r5 ; nop

        mov  t0t, r1    ; nop #V座標をテクスチャユニットに設定
        mov  t0s, r0    ; nop #U座標をテクスチャユニットに設定
        nop             ; nop ; ldtmu0 #UVデータがr4に格納されるまで待つ
        nop             ; nop ; sbwait

        mov  tlbc, r4   ; nop; thrend # タイルカラーバッファにr4のテクスチャ色を詰めてQPUスレッド終了
        nop
        nop             ; nop; sbdone # タイルバッファへの操作終了シグナルを投げる

FragmentShaderのGLSLだと以下のような感じでしょうか。


varying vertex_uv;
void main() {
  vec4 texcolor = texture2D(vertex_uv);
  gl_FragColor = vec4(texcolor);
}

という感じで一苦労してようやく色が出ます。

r5のレジスタなどがどういう意味しているのかは本家PDFのQPU Core Pipelineを穴が開くほどみてください。

image

※抜粋

■その他 : 苦労したところ■

デバッグです。シリアルがなかったころはフレームバッファの一部に
bitmapでダンプ結果を表示しながらやってました。
だんだんめんどくさくなってきて結局シリアルボードを注文してuart初期化して使いました。

■その他 : 参考動画■

参考動画ではナンが好きなので、ナンを大量に出しています。
https://ja.wikipedia.org/wiki/%E3%83%8A%E3%83%B3

サンプルソースをコマンドラインからm.bat実行後、add.batの転送先のドライブをSDカードにすれば
SDカードにimageを転送することができます。その後、rpi2に刺してHDMIディスプレイにさして電源ONで
ナンを躍らせることができます。

■所感とか感想とか■

正直、俺は何をやってるんだッッッ!という感覚に後半襲われてしばらく手を休めてました。
アルファブレンドまでQPU側でやらせたらあとはどうとでもなりそうだなとふんだからです。

まあ仕方ないですね。結構苦労した割にはまだまだ先という感じです。
GLのgear.c相当を移植して、うまく動作するならベンチしてみたいですね。
https://www.opengl.org/archives/resources/code/samples/glut_examples/mesademos/gears.c

正直まだやりこみ不足で、もっと面白いことできるんじゃないかなとか思っています。
特にベアメタルからGPGPUさせるときももうちょっと凝ったことができそうです。

今回こだわったもうちょっと深い動機としては、もともと旧職場で新しいチップのドライバ(GPU関連ではなくストレージ関連です)のモックを作成するのが仕事だったので、久々にドライバ周りの仕様書から動くモック作ってみたいな~と思い立ったのもあります。
あいかわらず石周りの仕様書は不親切だけどレジスタ周りは叩いて反応を見てなんとかなるもんですね。

あと、実際にAPIレベルに落とし込むには全然役不足です。仕様書を見る限り、直接ControlListをいじるとか、
もっと上位層からた叩かれることを意識すると、今のAPIのようにするのは論外です。
まあもうちょいと整理整頓しないと見せられないと思ったのですがとりあえず動くものを出します。

やるなら一度RAM側に中間コマンドを落とし込んでそのあとContorlListを構築する履歴を残し、
何かのタイミングでControlListを全部構築したりなど、凝ったことをしないとOpenGLほぼ等価な実装を実現するには難しい認識です。この辺はコンシューマ周りのドライバ書いたことある方なら当たり前のことかもしれませんが。
グラフィックスAPI周りの互換性をとるのがいかに難しいか分かった気がします。
きちんとしたフォーマルな仕様がJEDECやらSDAやらSCSIやらUSBみたいに存在しないので。

まあここからはAPI <--- HW中間層とHWの癖とエラッタを見抜きながら作るセンスが要求されそうです。
ここからが本当は楽しいところなんですが…(マゾです)

■今後■

  • neon周りとかソースに入ってるくせにまだラフの段階で全然動かせてないので動かす。まあ来年やります
  • VPMでVertexShader動かしてみたいですが、取り回しが難しそうです
  • PWMと合わせてなんか作りたいですね

■終わりに■

最後までみていただきありがとうございました。
間違ってる、もうちょっと説明を詳しくなどありましたらガンガン指摘もらえると助かります。


誤字とか随時修正します。

ナン pic.twitter.com/iHUjR5Di6X

— 野菜 (@gyabo) 2016年12月14日
96
84
5

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
96
84