5. アプリケーションの初期化と状態遷移のハンドリング

この章では、アプリケーションが最初に行わなければならない初期化処理とスリープなどの状態遷移への対処について説明します。

5.1. エントリ関数までの初期化処理

アプリケーションのエントリ関数が呼び出されるまでに行われている処理については、SDK に付属されている「システムプログラミングガイド」を参照してください。

5.2. エントリ関数

アプリケーションのエントリ関数は nnMain() で定義されています。

エントリ関数で最初に行うのは、アプリケーションで使用するライブラリの初期化です。主なライブラリの初期化については、次の節以降で説明します。

エントリ関数を抜けるとアプリケーションが終了します。エントリ関数内でメインループを構成し、アプリケーションが終了するまでエントリ関数から抜けないようにしてください。

5.3. APPLET ライブラリの初期化

APPLET ライブラリは、単にライブラリアプレットを利用するためだけでなく、HOME ボタンや電源ボタンが押されたときへの対処や蓋を閉じてスリープ状態へ移行するときへの対処を実装するために必要なライブラリです。APPLET ライブラリの初期化は、エントリ関数に実行が移るまでに行われています。アプリケーションで行わなければならない初期化処理は、APPLET ライブラリの各機能を有効にするために nn::applet::Enable() を呼び出すことです。

コード 5-1. APPLET ライブラリの初期化
void nn::applet::Enable(bool isSleepEnabled = true);

スリープ関連のコールバック設定が完了してから呼び出してください。isSleepEnabled には、呼び出しの時点でスリープ関連のコールバックを有効にするかどうかを指定します。

この関数を呼び出す前後では、スリープ関連のハンドリングを慎重に行う必要があります。スリープ関連のハンドリングについては、「5.3.3. スリープへの対処」を参照してください。

注意:

nn::applet::Enable()nngxInitialize()nn::dsp::Initialize()nn::snd::Initialize() よりも前に呼び出さなければなりません。

アプリケーションの起動中に電源ボタンが押されるなど、Enable() が呼び出されるまでに発生した要因で、アプリケーションの起動直後に終了させなければならない場合があります。そのため、この関数を呼び出した直後は、「5.3.1. アプリケーション終了要求への対処」に従ってアプリケーション終了要求に対処してください。その際、終了要求が来ていないことを確認するまで nngxInitialize() は呼び出さないでください。

以降の項では、アプリケーションの実装に必須となる、アプリケーション終了要求、HOME ボタン、スリープ(蓋閉じ)、電源ボタンへの対処とアプリケーションでのハンドリング例について説明します。対処が必要となる状態の変化は、すべてアプレットマネージャを介してアプリケーションに通知されます。

注意:

アプリケーション終了要求や電源ボタンへの対処を行わなかった場合、電源ボタンが一定時間以上押し続けられたときにアプリケーションが強制終了させられます。アプリケーションはこれらの状態変化に対応し、正常に処理が行われるように実装しなければなりません。

図 5-1. アプレットマネージャからの通知

アプリケーション アプレットマネージャ IsExpectedToProcessHomeButton()がtrueを返す IsExpectedToProcessPowerButton()がtrueを返す IsExpectedToCloseApplication()がtrueを返す スリープ問い合わせコールバックが呼び出される スリープキャンセルコールバックが呼び出される スリープ復帰コールバックが呼び出される 承諾/保留/拒否を返す HOMEボタンが押された 電源ボタンが押された 起動中に終了要件を満たしていた(ロゴ表示中に電源ボタンが押されたなど) HOMEメニューで「おわる」を選択 ほかのアプリケーションを起動 バッテリー切れ 蓋を閉じた スリープ前に蓋を開けた(スリープを保留中、問い合わせへの回答前) 問い合わせへの回答でスリープ拒否が返された スリープ中に蓋を開けた

5.3.1. アプリケーション終了要求への対処

アプレットマネージャは、アプリケーションを中断して表示している HOME メニューでの「おわる」の選択、ほかのアプリケーションの起動などが要因でアプリケーションが終了しなければならない状態にあるかどうかを nn::applet::IsExpectedToCloseApplication() で通知しますので、アプリケーションはこの関数を定期的(フレームごとなど)に呼び出さなければなりません。この関数が true を返したときは、速やかにアプリケーションを終了させる必要があります。また、描画の権限がない状態になる可能性があることに注意してください。

アプリケーションは独自の終了処理(4 秒以内)を行ったあとに nn::applet::CloseApplication() を呼び出してアプリケーションを終了させてください。描画の権限がない状態ではグラフィックス関連の処理が実行されませんので、nngxInitialize()nngxWaitCmdlistDone() を呼び出すと処理がブロックされてしまいます。また、コマンドリクエストの終了割り込みも発生しませんので、これらの完了を待つことなく終了処理が行われるように実装してください。

nngxFinalize() は描画の権限がない状態でも呼び出すことができ、nngx 関数や gl 関数で確保したディスプレイバッファなどが自動的に解放されます。

コード 5-2. アプリケーションの終了に使用する関数
bool nn::applet::IsExpectedToCloseApplication(void);
nn::Result nn::applet::CloseApplication(const u8* pParam=NULL, 
                size_t paramSize=0, nn::Handle handle=NN_APPLET_HANDLE_NONE);

IsExpectedToCloseApplication() は定期的に呼び出す以外にも、HOME メニューやライブラリアプレットからの復帰直後にも呼び出して、復帰待ちの間にアプリケーションを終了する要因が発生していないかどうかを確認しなければなりません。また、アプリケーションのロード中に終了要求が来た場合に備えて、APPLET ライブラリの初期化直後でも確認してください。オートセーブなど、アプリケーションの終了時に行いたい処理も、この関数が true を返したときに行ってください。

5.3.1.1. 終了処理の注意事項

CTR-SDK では、アプリケーションが任意のタイミングで nn::applet::CloseApplication() を呼び出しても、確保していたリソース類の解放が行われるように設計されていますが、その検証が十分に行われていません。安全にアプリケーションを終了するためにも、CloseApplication() を呼び出すまでに、以下の対応を踏まえて終了処理を実装してください。

補足:

これらの対応が不要となるように、今後のリリースで修正する予定です。

必須

nn::os::Alarm オブジェクトを作成していた場合は、Alarm オブジェクトのメンバ関数 Cancel()Finalize() を順番に呼び出してください。

nn::os::Timer オブジェクトを作成していた場合は、Timer オブジェクトのメンバ関数 Stop()Finalize() を順番に呼び出してください。

FS と UDS ライブラリは処理を止め、ライブラリの終了処理を行ってください。特に FS ライブラリでは、使用している各クラスの Finalize() を呼び出してください。NW4C などのように SDK 以外のパッケージでも、FS ライブラリを使用しているクラスについては確実に終了処理を行うようにしてください。

推奨

初期化した、すべてのライブラリの終了処理を行ってください。

5.3.2. HOME ボタンへの対処

HOME ボタンが押された場合、アプレットマネージャはアプリケーションが HOME メニューを起動しなければならない状態にあるかどうかを nn::applet::IsExpectedToProcessHomeButton() で通知しますので、アプリケーションはこの関数を定期的(フレームごとなど)に呼び出さなければなりません。この関数が true を返したときは、HOME メニューの起動を行う必要があります。アプリケーションはデバイスなどの動作を停止させ、すぐに HOME メニューを起動させてください。ただし、中断できない処理のために動作を停止できない場合は、後述する「HOME ボタン禁止アイコン」を表示して HOME メニューの起動を中止することができます。中止した場合は、nn::applet::ClearHomeButtonState() で HOME ボタンが押されたことを無効にしなければ、IsExpectedToProcessHomeButton()true を返し続けることに注意してください。

コード 5-3. HOME メニューの起動に使用する関数
bool nn::applet::IsExpectedToProcessHomeButton(void);
bool nn::applet::ProcessHomeButton(void);
nn::applet::AppletWakeupState nn::applet::WaitForStarting(
        nn::applet::AppletId* pSenderId=NULL, u8* pParam=NULL, 
        size_t paramSize=0, s32* pReadLen=NULL, nn::Handle *pHandle=NULL, 
        nn::fnd::TimeSpan timeout=NN_APPLET_WAIT_INFINITE);
void nn::applet::ClearHomeButtonState(void);
void nn::applet::ProcessHomeButtonAndWait();

HOME メニューを起動するために必要な処理は nn::applet::ProcessHomeButton() を呼び出すだけで行われます。返り値が false の場合は HOME メニューからの復帰待ちを行う必要がありませんが、返り値が true のときは直後に nn::applet::WaitForStarting() を呼び出して HOME メニューからの復帰を待ってください。復帰するまで、キー入力の取得や描画などのデバイスの使用に制限がかかります。また、WaitForStarting() を呼び出したスレッドのみが停止することに注意してください。

優先度 16 もしくはより高い優先度(数値としては小さい)のスレッドが、HOME メニューへの遷移後も CPU を占有して動作するような実装にはしないでください。そのようなスレッドが存在した状態でゲームメモを起動すると、ゲームメモの起動中にフリーズします。なお、HOME メニューの表示中にアプリケーションのスレッドが動作し続けていると HOME メニュー等のパフォーマンスが低下しますので、HOME メニューへ遷移するときにはアプリケーションのすべてのスレッドを停止させることを推奨します。

nn::applet::ProcessHomeButtonAndWait()ProcessHomeButton() の実行および待ち受けとスリープのハンドリングを行うラッパー関数です。

注意:

ProcessHomeButton() を呼び出すまでに、nngxWaitCmdlistDone() を呼び出すもしくはそれと同等の処理を行って GPU の描画コマンドの実行を確実に終了させておく必要があります。

補足:

ProcessHomeButton() を呼び出すことができるのは、GX ライブラリが使用可能である期間(nngxInitialize() の完了から nngxFinalize() を呼び出すまでの間)だけです。

ProcessHomeButton() を呼び出す前にディスプレイバッファの設定とバッファスワップを行い、nngxStartLcdDisplay() を呼び出して LCD 出力を開始してください。LCD 出力が開始されていないと、黒画面のまま HOME メニューが起動する可能性があります。また、ディスプレイバッファの設定とバッファスワップを行っていない場合は、画面表示が不定なものになる可能性があります。

表 5-1. HOME メニュー表示中のデバイスへの対処

デバイス

HOME メニュー表示中のアプリケーションの対処

GPU/LCD

描画を止め、ディスプレイバッファを更新しないでください。また、HOME メニューが表示されるまでにグラフィックスの処理を完了させ、GPU が停止している状態でなければなりません。

デジタルボタンスライドパッド

特に対処は必要ありません。アプリケーションで取得する入力値に入力は反映されません。

タッチパネル

特に対処は必要ありません。アプリケーションで取得するサンプリング値に入力は反映されず、無効なサンプリング値(0)が返されます。

加速度センサー

特に対処は必要ありません。入力値を取得することができます。

ジャイロセンサー

入力値を取得することができます。復帰後にキャリブレーションを行わない場合は、HOME メニューの表示中も入力値を取得して補正が効いた状態にしておく必要があります。

サウンド

SND ライブラリの関数はメインスレッドおよびサウンドスレッドから呼び出されることを想定して設計されています。これらのスレッドからの呼び出しを行う場合は、HOME ボタン遷移時に注意することはありません。ただし、上記以外のスレッドから関数を呼び出す場合は、アプリケーションが中断されている状態で呼び出さないように、適切な対処が必要です。

なお、サウンドが停止するタイミングを制御することができませんので、ムービーのようにグラフィックスなどとの同期が必要な場合は再生状態を管理するなどの対処が必要です。

カメラ

特に対処は必要ありません。HOME メニューの起動までにカメラの終了処理を行っていない場合、カメラを使用する HOME メニューの機能は動作しません。

マイク

特に対処は必要ありません。

無線通信

ローカル通信以外であれば、特に対処は必要ありません。ただし、ブラウザなどを起動して無線通信を HOME メニューの表示中に行った場合、アプリケーションで実施していた通信が切断された状態で復帰します。

ローカル通信は継続することができます。ただし、HOME メニューの表示中に蓋が閉じられることを考慮する必要があり、蓋が閉じられたときにスリープする場合、ローカル通信を終了させていないとローカル通信の状態がエラーに遷移します。HOME メニューの起動までにローカル通信を終了させていない場合、ローカル通信を使用する HOME メニューの機能は動作しません。

NFC

NFP ライブラリを使用している場合は、HOME メニュー、及びアプレットに遷移する前に nn::nfp::Finalize() を呼び出して、 NFP ライブラリの終了させてください。

詳細は「3DS プログラミングマニュアル - NFP 編」を参照してください。

HOME メニューから復帰した直後、アプリケーションが nngxSwapBuffers() を呼び出してディスプレイバッファを切り替えるまで、画面には HOME メニューへの遷移時にキャプチャされた画像が表示されています。ここで注意しなければならないのは、メインメモリや VRAM の内容は保護されていますが、GPU のレジスタ設定をアプリケーションで再設定しなければならないことです。頂点ロードアレイの設定やテクスチャユニットの設定だけでなく、フレームバッファやシェーダバイナリ、参照テーブルといった、すべてのレジスタ設定を再設定してください。

補足:

レジスタ設定を直接書き換えていなければ、nngxUpdateState(NN_GX_STATE_ALL) でレジスタ設定を復元することができます。

HOME ボタン押下の検知

HOME ボタン押下の検知は、nn::applet::SetHomeButtonCallback() で登録したコールバック関数への通知でも確認することができますが、基本的に nn::applet::IsExpectedToProcessHomeButton() を定期的に調べるように実装してください。

コード 5-4. HOME ボタンのコールバック関数の登録
void nn::applet::SetHomeButtonCallback(
        nn::applet::AppletHomeButtonCallback callback, uptr arg=0);
typedef bool (*nn::applet::AppletHomeButtonCallback)(
        uptr arg, bool isActive, nn::applet::CTR::HomeButtonState state);

コールバック関数の登録は nn::applet::SetHomeButtonCallback() で行います。登録されたコールバック関数が呼び出されたとき、引数 arg には SetHomeButtonCallback() の引数 arg に指定した値が格納されています。isActive には、アプリケーションが動作中であるかどうかが格納され、state には HOME ボタンの状態が格納されています。

HOME ボタンの状態は nn::applet::HomeButtonState 列挙子で定義されています。

表 5-2. HOME ボタンの状態

定義

説明

HOME_BUTTON_NONE

HOME ボタンは押されていない。

HOME_BUTTON_SINGLE_PRESSED

HOME ボタンがクリックされた。(200 ms 以上押し続けた)

HOME ボタンの状態は nn::applet::GetHomeButtonState() を呼び出すことでも確認することができます。押されたことが検知されると、HOME ボタンの状態は nn::applet::ClearHomeButtonState() が呼び出されるまで保持されます。ClearHomeButtonState() を呼び出すと、HOME ボタンの状態は HOME_BUTTON_NONE に戻ります。

コード 5-5. HOME ボタンの状態の確認
nn::applet::AppletHomeButtonState nn::applet::GetHomeButtonState(void);

コールバック関数で返す値によって、HOME ボタン押下の検出を有効にするか、無効にするかをアプリケーションで決定し、nn::applet::GetHomeButtonState() で取得する HOME ボタンの状態に反映するかどうかを制御することができます。true を返すと反映され、false を返すと反映されません。通常は isActive を返すように実装してください。コールバック関数内ではフラグの制御などの軽い処理のみを実装し、HOME メニューの起動はコールバック関数内で行わないでください。

5.3.2.1. HOME メニューの起動までにできること

HOME ボタンが押されたことを検知してから HOME メニューを起動するまでの 0.5 秒以内であれば、アクションゲームで一時停止をかける、オートセーブを行うなどの処理を行うことができます。制限時間のあるパズルゲームなど、HOME メニューの背景にゲームの静止画像を表示したくない場合は、このタイミングで描画処理を行って画面を隠すことができますが、なるべくそのまま静止画像を表示するようにしてください。
アプリケーションの中断中に HOME メニューの上画面に表示されるキャプチャイメージを作る場合などでは、nn::applet::IsExpectedToProcessHomeButton()true を返したことを確認してから描画処理を行いますが、nn::applet::ProcessHomeButton() で HOME メニューを表示する際には、GPU の処理が停止(グラフィックス処理を完了)している必要があります。実行中のグラフィックスコマンドがすべて完了するまで待つには、nngxWaitCmdlistDone() を呼び出します。また、LCD に表示される絵を確実に更新するためには、そのあとに nngxWaitVSync() を呼び出してください。

ユーザーが HOME メニューでアプリケーションを終了する可能性を考慮して、HOME メニューを起動する際には、HOME メニューで終了されても問題がないように実装してください。通常はアプリケーションに制御が戻ってから終了処理を行うことができますが、HOME メニュー表示中にスリープした状態でバッテリー切れになると終了処理を行えないことに注意してください。

5.3.2.2. HOME ボタン禁止アイコンの表示

セーブなどの中断できない処理を行っている最中に HOME ボタンが押されたことを検知したとき、その処理によってすぐに HOME メニューを表示できない場合は、HOME ボタン禁止アイコンを下画面の中央に表示し、HOME メニューの起動を中止することができます。
HOME ボタン禁止アイコンは、以下の仕様に従って実装してください。

  • アイコン:CTR-SDK に付属している画像ファイル(HomeNixSign_Targa.tga)
  • 表示位置:下画面の中央
  • フェードイン:0.083 秒(60 フレーム換算で 5 フレーム)
  • 表示時間:1 秒間(60 フレーム換算で 60 フレーム)
  • フェードアウト:0.333 秒(60 フレーム換算で 20 フレーム)

表示演出中に HOME ボタンが押された場合は演出をそのまま続けてください。例えば、フェードアウト中に押されたときはフェードアウトの演出を続け、フェードインの演出に戻る必要はありません。また、表示演出中に HOME メニューの起動が可能になったとしても、HOME メニューを起動してはいけません。

5.3.2.3. スクリーンショットの投稿を禁止する

HOME メニューへの遷移時に作成されるキャプチャイメージを、ほかのアプリケーション(Miiverse などのシステムアプレット)で投稿できないようにすることができます。

コード 5-6. スクリーンショットの投稿の許可設定および設定値の取得
nn::Result nn::applet::SetScreenCapturePostPermission(
    const nn::applet::ScreenCapturePostPermission permission);
nn::Result nn::applet::GetScreenCapturePostPermission(
    nn::applet::ScreenCapturePostPermission* pPermission);

SetScreenCapturePostPermission() で投稿の可否を指定することができます。引数 permission に指定可能な値は SCREEN_CAPTURE_POST_ENABLE または SCREEN_CAPTURE_POST_DISABLE の 2 種類です。そのほかの値を指定することは禁止されています。なお、nn::applet::RestartApplication() でアプリケーションを再起動すると初期値(表 5-3 参照)に戻ります。

現在の設定は GetScreenCapturePosetPermission() で取得することができます。

表 5-3. スクリーンショットの投稿の許可設定および設定値の取得で使用する値

定義

説明

SCREEN_CAPTURE_POST_NO_SETTING

(指定禁止)設定の初期値で、投稿が許可された状態です。

SCREEN_CAPTURE_POST_NO_SETTING_OLD_SDK

(指定禁止)旧バージョンの SDK でリリースされたアプリケーションで取得される初期値で、投稿が許可された状態です。

カメラ使用時はスクリーンショットの投稿が禁止されます。ここでいうカメラ使用時とは、CAMERA ライブラリの nn::camera::Initialize() で初期化してから、nn::camera::Finalize() で終了するまでの期間を指します。

SCREEN_CAPTURE_POST_ENABLE

投稿が許可された状態です。

SCREEN_CAPTURE_POST_DISABLE

投稿が禁止された状態です。

補足:

CTR-SDK 7.x より前の SDK でリリースされたアプリケーションで、Miiverse へのスクリーンショットの投稿を禁止したい場合は弊社窓口までお問い合わせください。

5.3.3. スリープへの対処

3DS でも、DS と同様にアプリケーションの動作中に蓋が閉じられると、スリープ状態へ遷移するかどうかをアプリケーションで決定することができます。ただし 3DS では、HOME メニューを表示しているときなど、アプリケーションが中断中であってもスリープ状態への遷移が行われる可能性があります。

下図は、スリープの状態遷移を、関連するユーザーの操作、3DS の状態、コールバック関数の関係で示したものです。この項では、これらのコールバック関数を軸にスリープへの対処方法を説明し、そのあとにスリープに関連する情報を提供します。

図 5-2. スリープの状態遷移

スリープ問い合わせ スリープキャンセル スリープ復帰 スリープ状態 蓋を閉じる 蓋を開ける スリープを承諾する回答保留中 スリープを拒否する問い合わせへの回答を保留する スリープを承諾する 保留したあと、スリープ状態に遷移するかどうかが確定する前に蓋を開ける スリープを拒否する ユーザーの操作 3DSの状態 コールバック関数

5.3.3.1. スリープ問い合わせコールバック

アプリケーションの動作中に蓋が閉じられると、アプレットマネージャはアプリケーションに蓋閉じによるスリープ状態への移行を問い合わせます。問い合わせは登録されたコールバック関数に通知されます。

コード 5-7. スリープ問い合わせコールバック関数の登録
void nn::applet::SetSleepQueryCallback(
                nn::applet::AppletSleepQueryCallback callback, uptr arg=0);
typedef nn::applet::AppletQueryReply 
                (*nn::applet::AppletSleepQueryCallback)(uptr arg);

コールバック関数の登録は nn::applet::SetSleepQueryCallback() で行います。登録されたコールバック関数が呼び出されたとき、引数 arg には SetSleepQueryCallback() の引数 arg に指定した値が格納されています。

問い合わせに対し、アプリケーションはコールバック関数の返り値ですぐにスリープ状態へ移行するかどうかをアプレットマネージャに通知します。

表 5-4. スリープ問い合わせコールバック関数の返り値に指定可能な値

定義

説明

REPLY_REJECT

スリープ状態への移行を拒否します。

REPLY_ACCEPT

スリープ状態への移行を承諾します。

REPLY_LATER

スリープ状態への移行を保留します。

蓋が閉じられた状態で無線通信を継続するときやサウンドの再生を継続するときには REPLY_REJECT を返してください。すぐにスリープ状態へ移行するときは REPLY_ACCEPT を返してもよいのですが、グラフィックスの処理などが停止しても問題ないタイミングでスリープ状態に移行するとは限りません。ほかの処理を停止してからスリープ状態に移行させるには、REPLY_LATER を返してスリープ状態への移行を一旦保留してください。ただし、スリープ状態に移行しても問題のないタイミングになったときは、REPLY_ACCEPT を引数に nn::applet::ReplySleepQuery() を速やかに呼び出し、イベントクラスなどでスリープからの復帰まで待機しなければなりません。

REPLY_LATER を返した時点で、LCD の電源がオフになるなどスリープに備えた状態になっています。また、アプレットマネージャも HOME ボタンや電源ボタンの処理が行われない半停止状態になるため、ほかの処理の停止を行う場合、処理時間はできる限り短くしてください。ただし、後述するスリープキャンセルコールバックで保留中の蓋開けを適切にハンドリングしている場合は、数十秒程度であれば保留状態を続けてもかまいません。スリープキャンセルコールバックでハンドリングしていない場合、保留状態が 4 秒以下であればガイドラインに違反していないと判断します。4 秒以上スリープを禁止したい場合は、後述する REPLY_REJECTnn::applet::EnableSleep() を組み合わせた、スリープ問い合わせコールバックの再発生を利用してください。

注意:

ローカル通信の初期化処理中(nn::uds::Initialize() の実行中)はスリープ状態への移行を承諾しないでください。無線通信の状態遷移のバッティングにより、正常に動作しない可能性があります。また、ローカル通信を終了させずにスリープ状態に移行した場合、UDS ライブラリは強制的にローカル通信を切断してエラー状態に遷移します。この状態になると、終了処理が行われるまで、一部の関数を除いて UDS ライブラリの関数はエラーを返すようになります。

SND ライブラリの関数をメインスレッドおよびサウンドスレッドとは別のスレッドで呼び出している場合は、REPLY_ACCEPT でスリープ状態への移行を承諾したあと、スリープ状態から復帰するまで SND ライブラリの関数を呼び出すことがないよう、適切に制御する必要があります。

なお、スリープ状態へ移行する間にサウンドスレッドの処理が停滞するなどして、通常 5 ms 周期で行われている nn::snd::WaitForDspSync()nn::snd::SendParameterToDsp() の呼び出し周期が 100 ms を超えると、スリープ状態から復帰したときにサウンドスレッドが停止したままになることがあります。このスレッド停止はデバッグビルドでのみ、まれに起こることが報告されています。

スリープ中にバッテリー切れになった場合はスリープから復帰せず、終了処理を行うことができません。また、スリープ中にゲームカードが抜ける場合もありますので、スリープ中に異常終了しても問題がないように実装してください。

なお、スリープ問い合わせコールバック関数をアプリケーションで登録していない場合は、REPLY_REJECT を返すコールバック関数を登録している状態と同じです。

アプリケーション中断中のコールバック

スリープ問い合わせコールバック関数は、HOME メニューの表示中など、アプリケーションが動作中でなくても呼び出されます。アプリケーションが動作中であるかどうかは、nn::applet::IsActive() で調べることができます。スリープ問い合わせコールバック関数内でアプリケーション中断中と判定された場合は、REPLY_ACCEPT を返してください。

コード 5-8. アプリケーション動作中の判定
bool nn::applet::IsActive(void);

返り値が true ならばアプリケーションは動作中、false ならば中断中です。

アプリケーションの動作が中断されている状態は Inactive 状態(動作している状態は Active 状態)といい、HOME メニューやライブラリアプレットが表示されている間のように、アプリケーションがその表示のための関数の呼び出しと終了待ちをしているスレッド以外は動作を継続することができます。また、アプリケーションの起動中、nn::applet::Enable() を呼び出すまでの間も、スリープ関連のコールバックは発生しませんが、Inactive 状態となります。

Inactive 状態では、スリープ問い合わせコールバックのほかにも、VSync のコールバックなども通常通り発生します。しかし、Inactive 状態ではメインスレッドが止まるように実装されているのが普通ですので、スリープ問い合わせコールバックを適切にハンドリングできず、意図しないデッドロック状態に陥る場合があります。例えば、メインスレッドでスリープのハンドリングを行っている場合、Inactive 状態で REPLY_LATER を返してしまうと、メインスレッドが止まっているためにスリープ状態への移行を承諾する機会が得られず、アプリケーションの状態が停滞してしまいます。

通信中などで常に REPLY_REJECT を返す場合を除き、HOME メニューの表示などで Inactive 状態になる前に FS ライブラリでのアクセスを止め、Inactive 状態でのスリープ問い合わせには常に REPLY_ACCEPT を返すような実装を推奨します。なお、Inactive 状態でも動作するスレッドを作成する場合、適切にスリープのハンドリングを行っているならば Active 状態と同様の処理でも問題はありません。

スリープ対応の制御

nn::applet::EnableSleep()nn::applet::DisableSleep() で、アプリケーションがスリープ状態への遷移に対応するかどうかを制御することができます。

コード 5-9. スリープ対応の制御
void nn::applet::EnableSleep(
                        bool isSleepCheck=nn::applet::SLEEP_IF_SHELL_CLOSED);
void nn::applet::DisableSleep(
                        bool isReplyReject=nn::applet::REPLY_REJECT_IF_LATER);

nn::applet::EnableSleep() はスリープ問い合わせコールバック関数で指定した返り値を有効にし、スリープ状態への移行シーケンスに移れるようにします。isSleepCheckSLEEP_IF_SHELL_CLOSED を指定した場合は、関数を呼び出した時点での蓋の状態を確認し、蓋が閉じられていればスリープ問い合わせコールバック関数が呼び出されます。isSleepCheckNO_SHELL_CHECK を指定した場合は、蓋の状態の確認とコールバック関数の呼び出しが行われないだけで、それ以外の動作に違いはありません。ちなみに、nn::applet::Enable(true) でスリープに関するコールバックを有効にしたときは、関数内で EnableSleep(NO_SHELL_CHECK) が実行されています。

isSleepCheck SLEEP_IF_SHELL_CLOSED を指定して nn::applet::EnableSleep() を呼び出すと、蓋が閉じられているときにはスリープ問い合わせコールバックが発生することを利用して、スリープ問い合わせコールバック関数で REPLY_LATER ではなく REPLY_REJECT を返すと、スリープを拒否している間に必要な処理を行って、スリープ状態への遷移途中での蓋開けを考慮する必要がなくなります。つまり、スリープ問い合わせコールバックで REPLY_REJECT を返すと、それから蓋を閉じている間はスリープ問い合わせコールバックが発生しませんが、スリープ状態へ移行できるようになったときに EnableSleep(SLEEP_IF_SHELL_CLOSED) を呼び出すことで、まだ蓋が閉じられている場合に再びスリープ問い合わせコールバックを発生させることができます。なお、DisableSleep() を呼んでいなくても、EnableSleep() を任意のタイミングで呼び出しても構いません。ただし、アプレットマネージャの状態によっては関数の呼び出しから戻るまでに数ミリ秒以上かかる可能性がありますので、毎フレーム呼び出すことは推奨しません。

注意:

アプリケーションの起動中に蓋が閉じられていると、nn::applet::Enable() を呼び出すまでにスリープ問い合わせに対して REPLY_REJECT を返しているとみなされます。スリープに対応するアプリケーションは、起動中に蓋が閉じられていてもスリープ問い合わせコールバックが再発生するように、Enable() のあとに EnableSleep(SLEEP_IF_SHELL_CLOSED) を呼び出してください。

nn::applet::DisableSleep() はスリープ問い合わせコールバック関数で指定した返り値を無効にし、どのような返り値を指定しても REPLY_REJECT を返した状態にします。isReplyRejectREPLY_REJECT_IF_LATER を指定したときは、さらに nn::applet::ReplySleepQuery(REPLY_REJECT) を実行し、すでにスリープ状態へ移行しようとしていた場合にそれをキャンセルします。isReplyRejectNO_REPLY_REJECT を指定したときは、ReplySleepQuery(REPLY_REJECT) を実行しないだけで、それ以外の動作に違いはありません。

DisableSleep() が呼び出されたあとでも、蓋が閉じられたときはスリープ問い合わせコールバックが発生しますが、コールバック関数の返り値は無視され、常に REPLY_REJECT が返されたと見なされます。ただし、アプリケーションが中断中(nn::applet::IsActive()false を返す状態)に呼び出されたスリープ問い合わせコールバックの返り値は有効となる(REPLY_REJECT 以外も返る)ことに注意してください。また、DisableSleep() は内部にカウンタを持っていませんので、複数回呼び出された場合でも、一度の EnableSleep() の呼び出しでコールバック関数の返り値が有効になります。

HOME メニューの表示(nn::applet::ProcessHomeButton())や電源メニューの表示(nn::applet::ProcessPowerButton())、ライブラリアプレットの起動など、アプリケーションが中断されるような関数を呼び出す場合は、必ずその前に DisableSleep() でスリープ問い合わせを拒否し、復帰したあとで EnableSleep() を呼び出してください。

注意:

アプリケーションの終了処理を行う前に DisableSleep(REPLY_REJECT_IF_LATER) を実行しなければ、終了処理の途中で蓋が閉じられると、スリープ状態で停止したままになる可能性があります。

通知状態の確認と保留への回答

アプリケーションでスリープ状態への遷移を保留しているかどうかは、nn::applet::IsExpectedToReplySleepQuery() で確認することができます。この関数が true を返したときは、アプリケーションはスリープ状態へ遷移しても構わないかどうかを判断し、保留していた回答を nn::applet::ReplySleepQuery() で確定しなければなりません。

スリープ状態の遷移をハンドリングする場合は、IsExpectedToReplySleepQuery() を定期的(フレームごとなど)に呼び出し、決められたタイミングでスリープ状態に遷移するような実装を推奨します。

コード 5-10. スリープ関連の通知状態の確認と保留への回答
bool nn::applet::IsExpectedToReplySleepQuery(void);
void nn::applet::ReplySleepQuery(nn::applet::AppletQueryReply reply);

5.3.3.2. スリープ復帰コールバック

蓋が開けられてスリープから復帰するときの通知はコールバック関数で受け取ります。

コード 5-11. スリープ復帰コールバック関数の登録
void nn::applet::SetAwakeCallback(nn::applet::AppletAwakeCallback callback, 
                                  uptr arg=0);
typedef void (*nn::applet::AppletAwakeCallback)(uptr arg);

コールバック関数の登録は nn::applet::SetAwakeCallback() で行います。登録されたコールバック関数が呼び出されたとき、引数 arg には SetAwakeCallback() の引数 arg に指定した値が格納されています。

スリープ復帰コールバック関数内では、スリープからの復帰を待機しているイベントクラスをシグナル状態にするなどの簡単な処理のみを行ってください。このコールバック関数は、スリープ問い合わせコールバック関数で REPLY_REJECT を返すなど、蓋が閉じられてもスリープ状態に移行しない場合はすぐに呼び出されます。そのため、蓋が開けられたかどうかを、このコールバック関数が呼び出されることで判断することはできません。

注意:

スリープ状態に移行すると LCD 表示が OFF になります。そのため、スリープ復帰後に画面表示の準備ができた段階で nn::gx::StartLcdDisplay() を呼び出さなければ画面に何も表示されなくなります。ただし、画面の準備ができていない状態や、HOME メニューの表示中、ライブラリアプレットの実行中にスリープしてから復帰した状態で nn::gx::StartLcdDisplay() を呼び出すと、画面表示が一瞬乱れてしまう可能性があります。その場合は画面表示の準備が完了し、正常に表示できる状態になってから呼び出してください。

5.3.3.3. スリープキャンセルコールバック

蓋が閉じられてから、アプリケーションがスリープ状態への移行を行うまでに蓋が開けられた(スリープキャンセル)場合に呼び出されるコールバック関数を nn::applet::SetSleepCanceledCallback() で登録することができます。

コード 5-12. スリープキャンセルコールバック関数の登録
void nn::applet::SetSleepCanceledCallback(
                nn::applet::AppletSleepCanceledCallback callback, uptr arg=0);
typedef void (*nn::applet::AppletSleepCanceledCallback)(uptr arg);

このコールバック関数は、蓋が閉じられてからスリープ状態に移行するまでにデータをセーブするといった、時間のかかる処理を行っている間に、蓋が開けられたことを検知してスリープの処理を中断する場合などに利用します。

このコールバック関数内でなにもしなければ、スリープ状態への移行を保留したままになります。つまり、明示的に ReplySleepQuery(REPLY_REJECT) を呼び出さないと、スリープはキャンセルされたことになりません。また、このコールバック関数が呼び出された場合でも、スリープ復帰コールバック関数が呼び出されることに注意してください。

すぐにスリープ状態に移行する場合や処理にかかる時間が短い場合は、このコールバック関数を登録する必要はありません。このような場合は、スリープキャンセルコールバックの対応を無理に行わないことも検討してください。

通常、REPLY_LATER を返した場合にのみスリープキャンセルコールバックが発生します。ただし、高速に蓋の開閉を行った場合は、REPLY_ACCEPTREPLY_REJECT を返したあとにスリープキャンセルコールバックが発生する可能性があります。スリープ問い合わせコールバックへの回答前に蓋開けが発生するケースは、高速な蓋の開閉を行わないと再現しないため、デバッグには困難が伴う可能性があります。

なお、スリープ関連のコールバックが同時に、並行して呼び出されることはありません。スリープキャンセルコールバックは、スリープ問い合わせコールバックとスリープ復帰コールバックの間に 0 または 1 回発生することが保証されています。

補足:

nn::applet::SetSleepCanceledCallback() の関数リファレンスに実装サンプルが掲載されています。

5.3.3.4. スリープ中に禁止されている処理

補足:

現在、アプリケーションを実装する上でスリープ中に禁止されている処理はありません。

5.3.3.5. 蓋を閉じたときのデバイスの動作状態

スリープ問い合わせコールバックの返り値や nn::applet::ReplySleepQuery() の引数に REPLY_ACCEPT を指定するか REPLY_REJECT を指定するかで、蓋が閉じられた状態での動作が異なるデバイスが存在します。

表 5-5. 蓋を閉じたときのデバイスの動作状態

デバイス

REPLY_ACCEPT

REPLY_REJECT

LCD

ディスプレイバッファの更新および VSync 同期が停止し、LCD 表示が OFF になります。

ディスプレイバッファの更新が停止し、バックライトのみ OFF になります。

デジタルボタン

一切の入力を受け付けません。

CTR は L と R ボタンのみ入力を受け付けます。SNAKE は L / R / ZL / ZR ボタンの入力を受け付けます。

タッチパネル

サンプリング値を取得することができません。

無効なサンプリング値(0)を返します。

加速度センサー

歩数計として動作します。入力値を取得することができません。

歩数計として動作します。入力値を取得することができます。

ジャイロセンサー

停止します。

入力値を取得することができます。

サウンド

停止します。

通常はスピーカーからの出力を停止し、強制的にヘッドホンから出力されますが、スピーカーからも出力できるように設定可能です。

カメラ

停止します。

停止します。

マイク

停止します。

停止します。

無線通信

停止します。ローカル通信は事前に停止させておくことを推奨します。

停止しません。

赤外線通信 停止します。スリープ前に切断処理を実行し、完了まで待機することを推奨します。 停止しません。
NFC 停止します。 停止します。

メインスレッドを含め、アプリケーションで作成したスレッドはスリープ状態に移行すると停止します。スリープ状態に移行していなかった場合、スレッドの動作は継続していますがデバイス自体が停止しているとイベントなどの通知がなくなります。

注意:

ニンテンドーDS/DSi とは異なり、通常、蓋を閉じた状態ではスピーカーからサウンドが出力されないことに注意してください。スピーカーから出力されるようにするには、「10.5.7. 蓋閉じによるスリープを拒否した際のサウンド出力」を参照してください。

5.3.4. 電源ボタンへの対処

160 ミリ秒以上電源ボタンが押された場合、アプレットマネージャはアプリケーションで電源ボタンが押されたことに対応しなければならないかどうかを nn::applet::IsExpectedToProcessPowerButton() で通知しますので、アプリケーションはこの関数を定期的(フレームごとなど)に呼び出さなければなりません。この関数が true を返したときは、速やかに nn::applet::ProcessPowerButton() を呼び出してください。

コード 5-13. 電源ボタンへの対応に使用する関数
bool nn::applet::IsExpectedToProcessPowerButton(void);
bool nn::applet::ProcessPowerButton(void);
void nn::applet::ProcessPowerButtonAndWait();

nn::applet::ProcessPowerButton() を呼び出すと、「電源メニュー」を表示するために、システムに制御が移ります。電源メニューが表示されることにより、終了時にアプリケーションが特別な画面を表示する必要がありません。この関数を呼び出した直後に、アプリケーションは終了処理の開始が許可されるのを nn::applet::WaitForStarting() で待ち受ける必要があります。WaitForStarting() から処理が戻ったあとは、nn::applet::IsCloseApplication()true を返す状態になっていますので、「5.3.1. アプリケーション終了要求への対処」を参照してアプリケーションの終了処理を行ってください。

nn::applet::ProcessPowerButtonAndWait()ProcessPowerButton() の実行および待ち受けとスリープのハンドリングを行うラッパー関数です。

補足:

ProcessPowerButton() を呼び出すことができるのは、GX ライブラリが使用可能である期間(nngxInitialize() の完了から nngxFinalize() を呼び出すまでの間)だけです。

ProcessPowerButton() を呼び出す前にディスプレイバッファの設定とバッファスワップを行い、nngxStartLcdDisplay() を呼び出して LCD 出力を開始してください。LCD 出力が開始されていないと、黒画面のまま電源メニューが起動する可能性があります。また、ディスプレイバッファの設定とバッファスワップを行っていない場合は、画面表示が不定なものになる可能性があります。

5.3.5. アプリケーションのハンドリング例

スリープや HOME ボタンのハンドリングは、以下に挙げるポイントを踏まえて実装してください。

  • メインループなど、定期的に呼ばれる場所で、以下の判定を行ってください。
    • nn::applet::IsExpectedToProcessHomeButton() の判定
      true が返されたときは、nn::applet::ProcessHomeButton() を呼び出す
      (HOME メニューを表示したくないもしくはできない場合は、ガイドラインに従って HOME メニュー禁止アイコンを表示する)
    • nn::applet::IsExpectedToProcessPowerButton() の判定
      true が返されたときは、nn::applet::ProcessPowerButton() を呼び出す
    • nn::applet::IsExpectedToCloseApplication() の判定
      true が返されたときは、アプリケーションの終了処理を行う
  • nn::applet::ProcessHomeButton()nn::applet::ProcessPowerButton() を呼び出す前に、描画コマンドの実行を完了させておいてください。実行中の描画コマンドは nngxWaitCmdlistDone() で完了を待つことができます。
  • nn::applet::ProcessHomeButton()nn::applet::ProcessPowerButton() を呼び出したあとは、必ず nn::applet::WaitForStarting() を呼び出してください。
  • nn::applet::Enable()nn::applet::WaitForStarting() から制御が戻ってきたときは、必ず nn::applet::IsExpectedToCloseApplication() の判定を行ってください。

スリープや HOME ボタンのハンドリングのコード例を以下に示します。

コード 5-14. ハンドリングのコード例
nn::os::LightEvent sAwakeEvent(true);
nn::os::LightEvent sTransitionEvent(true);
nn::os::CriticalSection sFileSystemCS(WithInitialize);

// スリープ問い合わせコールバック
nn::applet::AppletQueryReply mySleepQueryCallback(uptr arg)
{
    sAwakeEvent.ClearSignal();
    return (nn::applet::IsActive() ? REPLY_LATER : REPLY_ACCEPT);
}

// スリープ復帰コールバック
void myAwakeCallback(uptr arg)
{
    sAwakeEvent.Signal();
}

// スリープ
void sleepApplication()
{
    // スリープ中にFSライブラリによるアクセスが起こらないようにロックする
    // ロックできなかった場合は次のフレームに再試行する
    if (sFileSystemCS.TryEnter())
    {
        _app_prepareToSleep();
        nn::applet::ReplySleepQuery(REPLY_ACCEPT);
        sAwakeEvent.Wait();
        _app_recoverFormSleep();
        sFileSystemCS.Leave();
        nn::gx::StartLcdDisplay();
    }
}

// アプリケーション終了
void exitApplication()
{
    _app_finalize();
    nn::applet::CloseApplication();
}

// メインループ
void nnMain()
{
    // イベントがシグナル状態のときは通常動作
    sAwakeEvent.Signal();
    sTransitionEvent.Signal();
    // コールバックの設定
    nn::applet::SetSleepQueryCallback(mySleepQueryCallback);
    nn::applet::SetAwakeCallback(myAwakeCallback);
    nn::applet::Enable(true);
    // アプリケーションロード中の終了要求に対応
    if (nn::applet::IsExpectedToCloseApplication())
    {
        exitApplication();
    }
    // アプリケーションロード中に蓋が閉じられていた場合に対応
    nn::applet::EnableSleep(SLEEP_IF_SHELL_CLOSED);

    while (true)
    {
        _app_exec();

        // スリープ問い合わせへの返答
        if (nn::applet::IsExpectedToReplySleepQuery())
        {
            if (_app_isRejectSleep())
            {
                // スリープできない状況ならば拒否を通知する
                nn::applet::ReplySleepQuery(REPLY_REJECT);
            } else {
                sleepApplication();
            }
        }
        // アプリケーションの終了要求
        if (nn::applet::IsExpectedToCloseApplication())
        {
            exitApplication();
        }
        // HOME ボタン
        if (nn::applet::IsExpectedToProcessHomeButton())
        {
            if (_app_isSuppressedHomeButton())
            {
                _app_drawSuppressedHomeButtonIcon();
                nn::applet::ClearHomeButtonState();
            } else {
                // HOME メニューやライブラリアプレットなどの起動のように、
                // メインスレッドを止めるような状態遷移の場合は、
                // イベントを非シグナル状態にする
                sTransitionEvent.ClearSignal();
                // HOME メニュー表示中にスリープしたときに、
                // FSライブラリによるアクセスが起こらないようにロックする
                // ロックできなかった場合は次のフレームに再試行する
                if (sFileSystemCS.TryEnter())
                {
                    _app_prepareToHomeButton();
                    nn::applet::ProcessHomeButtonAndWait();
                    sFileSystemCS.Leave();
                    if (nn::applet::IsExpectedToCloseApplication())
                    {
                        exitApplication();
                    }
                    sTransitionEvent.Signal();
                    // GPU のレジスタ設定を戻す必要があります
                    _app_recoverGpuState();
                }
            }
        }
        // 電源ボタン
        if (nn::applet::IsExpectedToProcessPowerButton())
        {
            // FSライブラリによるアクセスが起こらないようにロックする
            // ロックできなかった場合は次のフレームに再試行する
            if (sFileSystemCS.TryEnter())
            {
                nn::applet::ProcessPowerButtonAndWait();
                sFileSystemCS.Leave();
                if (nn::applet::IsExpectedToCloseApplication())
                {
                    exitApplication();
                }
                // GPU のレジスタ設定を戻す必要があります
                _app_recoverGpuState();
            }
        }
    }
}
// メインスレッド外でFSライブラリによるアクセスを行う場合は
// 以下のようにロックしてからアクセスする
{
    // スリープ状態の場合、起きるまでスレッドを止める
    sAwakeEvent.Wait();
    // メインスレッドに制御が戻ってくるまでスレッドを止める
    s_TransitionEvent.Wait();
    {
        os::CriticalSection::ScopedLock lock(sFileSystemCS);
        //
        // ここに FS にアクセスする処理を書く
        //
    }
}

5.3.6. アプリケーションの再起動

nn::applet::RestartApplication() を呼び出すことで、HOME メニューに戻ることなくアプリケーションを再起動することができます。再起動後のアプリケーションに渡すパラメータを指定することができ、指定されたパラメータは nn::applet::GetStartupArgument() で取得することができます。

コード 5-15. アプリケーションの再起動に使用する関数
nn::Result nn::applet::RestartApplication(
                const void* pParam = NULL, 
                size_t paramSize = NN_APPLET_PARAM_BUF_SIZE);
bool nn::applet::GetStartupArgument(
                void* pParam, 
                size_t paramSize = NN_APPLET_PARAM_BUF_SIZE);

pParam にはパラメータのバイト列を、paramSize にはパラメータのバイトサイズを指定します。バイト列の最大長は NN_APPLET_PARAM_BUF_SIZE です。

通常、nn::applet::RestartApplication() は制御を戻さずにアプリケーションを再起動させます。この関数によって再起動したかどうかは、nn::applet::GetStartupArgument()true を返すかどうか(渡したパラメータを取得できたかどうか)で判断することができます。

5.3.7. 本体設定へのジャンプ

アプリケーションから本体設定のインターネット設定、ペアレンタルコントロール、データ管理の各画面へとジャンプすることができます。

コード 5-16. 本体設定へのジャンプ
nn::Result nn::applet::JumpToInternetSetting(void);
nn::Result nn::applet::JumpToParentalControls(
                nn::applet::AppletParentalControlsScene scene =
                nn::applet::CTR::PARENTAL_CONTROLS_TOP);
nn::Result nn::applet::JumpToDataManagement(
                nn::applet::AppletDataManagementScene scene =
                nn::applet::CTR::DATA_MANAGEMENT_STREETPASS);
bool nn::applet::IsFromMset(nn::applet::AppletMsetScene* pScene = NULL);

インターネット設定へは nn::applet::JumpToInternetSetting()、ペアレンタルコントロールへは nn::applet::JumpToParentalControls()、データ管理へは nn::applet::JumpToDataManagement() でそれぞれジャンプすることができます。アプリケーションを一旦終了させてから本体設定にジャンプしますので、事前に終了処理を行ってください。これらの関数によるジャンプに失敗したときはフェイタルエラーとなります。また、関数の成否に関わらず、アプリケーションに制御は戻りません。

ペアレンタルコントロールには複数の設定項目があり、引数でどの項目にジャンプするかが選択可能です。

表 5-6. ジャンプ先の画面の指定

定義

ジャンプ先の画面

PARENTAL_CONTROLS_TOP

ペアレンタルコントロールのトップ画面

PARENTAL_CONTROLS_COPPACS

ペアレンタルコントロールの COPPACS の認証処理画面

DATA_MANAGEMENT_STREETPASS

データ管理画面のすれちがい通信設定画面

上記の関数でジャンプした本体設定を終了すると、アプリケーションが再起動されます。本体設定を終了したあとの再起動では、nn::applet::IsFromMset()true を返します。本体設定のどのシーンにジャンプしていたのかを判別する場合は、引数 pScene にジャンプ先シーンを格納する変数を指定します。IsFromMset()nn::applet::Enable() よりもあとに呼び出してください。

5.3.8. 起動時に取得可能な初期パラメータ

アプリケーションから再起動した場合や本体設定にジャンプして再起動された場合など、アプリケーションがどのような経緯で起動されたのかを取得する関数には、以下のものがあります。

表 5-7. 起動時の初期パラメータを取得する関数

関数

取得可能な初期パラメータ

nn::applet::GetStartupArgument()

nn::applet::RestartApplication() で指定したパラメータ

nn::applet::IsFromMset()

アプリケーションからジャンプしていた本体設定の画面

nn::news::IsFromNewsList()

おしらせの種類や指定されたパラメータ。おしらせリストで「ソフトを起動」を選んだ場合に通知される

nn::frineds::IsFromFriendList()

フレンドのフレンドキー。フレンドリストで「プレイ中のソフトに参加する」を選んだ場合に通知される

5.3.9. ニンテンドーeショップへのジャンプ

アプリケーションからニンテンドーeショップのページにジャンプすることができます。

コード 5-17. ニンテンドーeショップのインストール確認
bool nn::applet::IsEShopAvailable();

本体更新の適用状態によっては、ユーザーの 3DS 本体にニンテンドーeショップがインストールされていない場合があります。必ず nn::applet::IsEshopAvailable() でニンテンドーeショップがインストール済みであることを確認してからジャンプしてください。

補足:

ニンテンドーeショップが本体に存在しなかった場合は、インターネットで本体更新を行う必要があることをユーザーに通知することをおすすめします。

例えば、「ニンテンドーeショップを使用できません。ニンテンドーeショップを使用するには、インターネットで本体更新をしてください。」といったメッセージを表示してください。

補足:

ダウンロードアプリの場合はインストール状況の確認は不要です。

コード 5-18. ニンテンドーeショップへのジャンプ(詳細ページへのジャンプ)
nn::Result nn::applet::JumpToEShopTitlePage(bit32 uniqueId);

ジャンプ後に表示されるページは、引数 uniqueId で指定されたユニーク ID を持つタイトルの詳細ページです。なお、指定されたタイトルが検索公開タイトル(タイトル名の検索で表示されるタイトル)でなければ、そのページは表示されません。また、引数 uniqueId にニンテンドーeショップに登録されていないタイトルのユニーク IDや不正な値を指定した場合は、ニンテンドーeショップ上でエラーが表示されます。

コード 5-19. ニンテンドーeショップへのジャンプ(パッチページへのジャンプ)
nn::Result nn::applet::JumpToEShopPatchPage(bit32 uniqueId);

認証サーバやアカウントサーバから、 nn::act::ResultApplicationUpdateRequired などのアプリケーションの更新を必要とするエラーが返ってきた場合に、ニンテンドーeショップのタイトルのパッチページへ誘導するために使用します。

ジャンプ後に表示されるページは、引数 uniqueId で指定されたユニーク ID を持つタイトルのパッチページです。

アプリケーションの更新をリマスターで運用している場合は、ニンテンドーeショップにタイトルのパッチページが存在しません。その場合、タイトルが存在しないエラーが表示され、ニンテンドーeショップ起動画面に遷移します。リマスターで運用している場合は、 nn::applet::JumpToEShopTitlePage() を使用してください。

ニンテンドーeショップにジャンプするこれらの関数は、呼ばれた時にアプリケーションが終了しますので、事前に終了処理を行ってください。なお、ニンテンドーeショップを終了してもアプリケーションは再起動されません。

補足:

本関数を使用してジャンプを行う場合には、ジャンプ先のタイトルを OMAS に申請してください。申請についての詳細は、ガイドラインの「eコマース」を参照してください。

5.3.10. 電子説明書へのジャンプ

HOME メニューの「説明書」をタッチしたときに起動する電子説明書に、アプリケーションからジャンプすることができます。

コード 5-20. 電子説明書へのジャンプ
void nn::applet::JumpToManual();

ジャンプ後、アプリケーションの取扱説明書が表示されます。

この関数はアプリケーションを中断してジャンプしますので、呼び出した後は nn::applet::WaitForStarting() で HOME メニューからの復帰を待ってください。電子説明書が終了すると、HOME メニューがアプリケーションを復帰させます。

5.4. FS ライブラリの初期化

多くの場合、メディア上のファイルにアクセスするために FS ライブラリの初期化を次に行うことになるでしょう。FS ライブラリの初期化は nn::fs::Initialize() を呼び出して行います。メディア上のファイルへのアクセスには、必ず FS ライブラリで用意されているクラスを使用してください。

5.5. GX ライブラリの初期化

LCD への表示や 3D グラフィックスの描画を行うには GX ライブラリを使用します。GX ライブラリの初期化は nngxInitialize() を呼び出して行います。初期化にはライブラリからのメモリ領域確保要求と解放要求を処理する関数の指定が必要になります。

nngxInitialize() を呼び出したあとは、グラフィックスコマンドの実行のためのコマンドリストオブジェクトの作成や、LCD に表示するためのディスプレイバッファやレンダーバッファの確保などを行います。

3DS では、図 5-3 のように LCD の配置されている方向や解像度が NITRO/TWL と異なっている点に注意が必要です。

図 5-3. LCD の配置と解像度

Upper LCD Lower LCD 256 192 NITRO/TWL 240 320 240 400 3DS

ここでは、サンプルプログラムでの実装を例に説明します。使用されている関数や設定の詳細については「3DS プログラミングマニュアル - グラフィックス基本編」や SDK の関数リファレンスを参照してください。また、立体視表示については「3DS プログラミングマニュアル - グラフィックス応用編」を参照してください。

5.5.1. ライブラリの初期化

nngxInitialize() の呼び出しには、ライブラリからのメモリ確保と解放の要求を受け付けるアロケータとデアロケータの指定が必要です。

コード 5-21. GX ライブラリの初期化
void SetupNngxLibrary(const uptr fcramAddress, const size_t memorySize)
{
    InitializeMemoryManager(fcramAddress, memorySize);

    if (nngxInitialize(GetAllocator, GetDeallocator) == GL_FALSE)
    {
        NN_PANIC("nngxInitialize() failed.\n");
    }
}

5.5.1.1. アロケータ

グラフィックス処理で必要となるテクスチャイメージデータやレンダーバッファなど、GPU がアクセスするメモリ領域は nngxInitialize() で指定したアロケータを介して確保されます。

アロケータには 4 つの引数が渡されます。

第 1 引数(確保先のメモリ空間)

第 1 引数はメモリ領域を確保するメモリ空間の指定で、GLenum 型で受け取ります。受け取った値によって、確保先のメモリ空間が異なります。

NN_GX_MEM_FCRAM が渡された場合、メモリ領域はメインメモリから確保するのですが、その領域はデバイスメモリから確保されなければなりません。デバイスメモリとは、周辺デバイスとメインプロセスの双方からアクセスする際に OS がアドレスの整合性を保証しているメインメモリの一部です。デバイスメモリの詳細や確保の方法については、「3.1.2. デバイスメモリ」を参照してください。

NN_GX_MEM_VRAMA または NN_GX_MEM_VRAMB が渡された場合、メモリ領域の確保先は、前者が VRAM-A、後者が VRAM-B となります。それぞれの VRAM の先頭アドレスとサイズは nngxGetVramStartAddr()nngxGetVramSize() に VRAM-A ならば NN_GX_MEM_VRAMA、VRAM-B ならば NN_GX_MEM_VRAMB を渡して呼び出すことで取得することができます。

第 2 引数(メモリ領域の用途)

第 2 引数はメモリ領域の用途指定で、GLenum 型で受け取ります。受け取った値によって、確保するメモリ領域のアライメントが異なります。

NN_GX_MEM_TEXTURE が渡された場合、メモリ領域はテクスチャイメージデータとして使用されます。アライメントはフォーマットに関係なく 128 Byte です。

NN_GX_MEM_VERTEXBUFFER が渡された場合、メモリ領域は頂点バッファとして使用されます。アライメントは格納するデータ型によって 1、2、4 Byte と異なりますが、データ型まではアロケータに渡されないため、最大の 4 Byte でメモリ領域を確保するように実装することを推奨します。

NN_GX_MEM_RENDERBUFFER が渡された場合、メモリ領域はレンダーバッファ(カラー、デプス、ステンシル)として使用されます。アライメントは 1 ピクセルあたりのビット数(16、24、32)によって 32、96、64 Byte と異なりますが、フォーマットまではアロケータに渡されません。そのため、アプリケーションで使用するレンダーバッファのフォーマットを固定するか、アライメントを最小公倍数の 192 Byte にしてメモリ領域を確保するように実装してください。

NN_GX_MEM_DISPLAYBUFFER が渡された場合、メモリ領域はディスプレイバッファとして使用されます。アライメントはフォーマットに関係なく 16 Byte です。VRAM に確保する場合は最後尾から 1.5 MByte には確保しないでください。

NN_GX_MEM_COMMANDBUFFER が渡された場合、メモリ領域はコマンドリストとして使用されます。アライメントは 16 Byte です。

NN_GX_MEM_SYSTEM が渡された場合、メモリ領域はライブラリのシステム領域として使用されます。アライメントは確保サイズによって 1、2、4 Byte と異なりますが、実装を簡単にするためにも、最大の 4 Byte でメモリを確保するように実装することを推奨します。

第 3 引数(オブジェクトの名前)

第 3 引数はオブジェクトの名前(ID)で、GLuint 型で受け取ります。第 2 引数が NN_GX_MEM_SYSTEM 以外である場合に渡され、メモリを管理する際の一情報として使用します。

第 4 引数(メモリ領域のサイズ)

第 4 引数は確保するメモリ領域のサイズで、GLsizei 型で受け取ります。指定されたサイズのメモリ領域を確保してください。

 

アプリケーションはこれらの引数から、適切なアライメントとサイズでメモリ領域を確保し、その先頭アドレスをライブラリに void* 型で返します。4 つの引数と確保したメモリ領域の先頭アドレスを組にして記憶しておくなど、後述するデアロケータでのメモリ領域の解放も含めて、メモリの管理はアプリケーションで行わなければなりません。

5.5.1.2. デアロケータ

アロケータで確保したメモリ領域が解放される際に、nngxInitialize() で指定したデアロケータが呼び出されます。デアロケータに渡される引数は 4 つで、第 1 から第 3 引数までは確保時にアロケータに渡された引数と同じ値が渡され、第 4 引数にはメモリ領域の先頭アドレスが渡されます。

アプリケーションはこれらの引数からアロケータで確保したメモリ領域を特定し、解放しなければなりません。

5.5.2. コマンドリストオブジェクトの作成

nngxInitialize() の呼び出しが完了したら、次はグラフィックス処理で呼び出される gl、nngx 関数の実行に必要となるコマンドリストオブジェクトを作成しなければなりません。

まず、nngxGenCmdlists() でコマンドリストのオブジェクトを作成し、nngxBindCmdlist() でカレントのコマンドリストに指定したあと、nngxCmdlistStorage() で 3D コマンドバッファとコマンドリクエストを蓄積するためのメモリ領域を確保します。

コード 5-22. コマンドリストオブジェクトの作成
void CreateCmdList()
{
    nngxGenCmdlists(1, &m_CmdList);
    nngxBindCmdlist(m_CmdList);
    nngxCmdlistStorage(256*1024, 128);
    nngxSetCmdlistParameteri(NN_GX_CMDLIST_RUN_MODE, NN_GX_CMDLIST_SERIAL_RUN);
}

このコード例では 3D コマンドバッファが 256 KByte、コマンドリクエストが最大 128 個蓄積可能なコマンドリストを 1 つだけ確保していますが、複数のコマンドリストを確保し、フレーム単位などで切り替えて使用することもできます。その際、3D コマンドを蓄積した順にコマンドリストを実行しなければならないことに注意しなければなりません。

5.5.3. ディスプレイバッファ、レンダーバッファの確保

LCD 表示に必要なディスプレイバッファと、レンダーターゲットとなるレンダーバッファを確保します。

フレームバッファを確保するには、フレームバッファオブジェクトにカラーバッファ、デプスバッファ、ステンシルバッファの各レンダーバッファを関連付ける必要があります。カラーバッファにはカラー情報が、デプスバッファにはデプス情報が、ステンシルバッファにはステンシル情報がレンダリング時に書き込まれます。ステンシルバッファを利用する場合は、デプスバッファとバッファを共有しなければならないことに注意してください。

上画面と下画面でフォーマットが同じ、かつ平行してレンダリングする必要がない場合は、メモリリソースを節約するためにもフレームバッファオブジェクトとレンダーバッファは上画面用と下画面用で共有することを推奨します。その際、バッファの幅と高さは両方の画面が収まるような設定を使用してください。

コード 5-23. レンダーバッファの確保
void CreateRenderbuffers(
        GLenum format, GLsizei width, GLsizei height)
{
    glGenFramebuffers(1, m_FrameBufferObject);
    glGenRenderbuffers(2, m_RenderBuffer);

    glBindRenderbuffer(GL_RENDERBUFFER, m_RenderBuffer[0]);
    glRenderbufferStorage(GL_RENDERBUFFER | NN_GX_MEM_VRAMA, format, 
        width, height);
    glBindFramebuffer(GL_FRAMEBUFFER, m_FrameBufferObject);
    glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, 
        GL_RENDERBUFFER, m_RenderBuffer[0]);
    glBindRenderbuffer(GL_RENDERBUFFER, m_RenderBuffer[1]);
    glRenderbufferStorage(GL_RENDERBUFFER | NN_GX_MEM_VRAMB, 
        GL_DEPTH24_STENCIL8_EXT, width, height);
    glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, 
        GL_RENDERBUFFER, m_RenderBuffer[1]);
}

このコード例ではカラーバッファとデプス・ステンシルバッファを VRAM-A と VRAM-B に分けて確保しています。

通常のフレームバッファアーキテクチャではカラーバッファの内容をそのまま表示することができますが、3DS のカラーバッファのフォーマットはブロックフォーマットであるため、リニアフォーマットに変換しなければ LCD に表示することができません。そのため、3DS ではディスプレイバッファをカラーバッファと LCD の間に挟み、カラーバッファからディスプレイバッファへのコピーとフォーマット変換を行います。その際、ハードウェアによるアンチエイリアスと上下反転を行うことができます。

初期化の段階ではディスプレイバッファの確保のみを行います。

コード 5-24. ディスプレイバッファの確保
void CreateDisplaybuffers(
        GLenum format0, GLsizei width0, GLsizei height0, GLenum area0,
        GLenum format1, GLsizei width1, GLsizei height1, GLenum area1)
{
    // 上画面(DISPLAY0)
    nngxActiveDisplay(NN_GX_DISPLAY0);
    nngxGenDisplaybuffers(2, m_Display0Buffers);
    nngxBindDisplaybuffer(m_Display0Buffers[0]);
    nngxDisplaybufferStorage(format0, width0, height0, area0);
    nngxBindDisplaybuffer(m_Display0Buffers[1]);
    nngxDisplaybufferStorage(format0, width0, height0, area0);
    nngxDisplayEnv(0, 0);
    // 下画面(DISPLAY1)
    nngxActiveDisplay(NN_GX_DISPLAY1);
    nngxGenDisplaybuffers(2, m_Display1Buffers);
    nngxBindDisplaybuffer(m_Display1Buffers[0]);
    nngxDisplaybufferStorage(format1, width1, height1, area1);
    nngxBindDisplaybuffer(m_Display1Buffers[1]);
    nngxDisplaybufferStorage(format1, width1, height1, area1);
    nngxDisplayEnv(0, 0);
}

このコード例では LCD ごとにディスプレイバッファを 2 つ用意してマルチバッファリングを行っています。マルチバッファリングで複数確保しなければならないのはディスプレイバッファだけです。

ディスプレイバッファはメインメモリ(デバイスメモリ)上に確保することができます。ディスプレイバッファのフォーマットとカラーバッファのフォーマットで、ピクセル当たりに必要なビット数がディスプレイバッファ>カラーバッファとなる組み合わせはカラーバッファからのコピー時にエラーが発生します。

LCD 表示に必要な初期化処理は以上です。各種バッファの確保が行われるように、この時点で一度 nngxRunCmdlist() を呼び出しておいてください。

5.6. メモリ管理

アプリケーションに割り当てられたヒープメモリ、デバイスメモリ、VRAM はアプリケーション自身で管理しなければなりません。CTR-SDK では、アプリケーションでメモリを管理するためのクラスを用意しています。

5.6.1. メモリブロック

メモリブロックは NITRO/TWL や Revolution ではアリーナと呼ばれていた機能に相当し、ある程度の大きさを持つ領域を確保したあと、後述する FND ライブラリのヒープクラスなどでメモリを切り出して使用することに適しています。FND ライブラリで定義されているヒープクラスは、メモリブロックを引数にインスタンスを生成することもできます。

メモリブロックは 4096 Byte 単位で確保されます。通常、アプリケーションに割り当てられたヒープメモリから、デバイスメモリに確保できない作業メモリやスタックなどを確保するために利用します。また、メモリブロックを利用することで、アプリケーションの実装を簡略化できるライブラリも存在します。

メモリブロックの確保は nn::os::MemoryBlock クラスやスタック用の nn::os::StackMemoryBlock クラス、nn::os::StackMemory クラスのインスタンスを生成して行います。これらのインスタンスを生成する前に nn::os::InitializeMemoryBlock() を呼び出してメモリブロックとして使用するメモリ領域を指定しなければなりません。

コード 5-25. メモリブロックとして使用するメモリ領域の指定
void nn::os::InitializeMemoryBlock(uptr begin, size_t size);

begin にはメモリ領域の先頭アドレス、size にはメモリ領域のサイズを指定します。どちらも nn::os::MEMORY_BLOCK_UNITSIZE ( 4096 Byte ) のアライメントでなければなりません。

メモリブロックは nn::os::MemoryBlock クラス(スタック用は nn::os::StackMemoryBlock クラス)のインスタンスを確保したいメモリ領域のサイズで生成するか、空のインスタンスを生成したあとに Initialize() を呼び出して初期化することで確保されます。サイズの指定は 4096 Byte 単位で行わなければなりません。

確保したメモリブロックの先頭アドレスは GetAddress() で、サイズは GetSize() で取得することができます。また、スタック用には、スレッドに直接渡すことができるように GetStackBottom()GetStackSize() のインターフェースが用意されています。スタック用にはありませんが、SetReadOnly()IsReadOnly() で読み込み専用属性の設定と取得を行うことができます。

不要になったメモリブロックはメンバ関数の Finalize() を呼び出すことで明示的に解放することができます。

補足:

nn::os::SetupHeapForMemoryBlock() は一部のサンプルデモで利用しているために残されている関数です。メモリブロックを利用するために呼び出すことは推奨しません。

nn::os::MemoryBlock クラスと nn::os::StackMemoryBlock クラスは、今後の更新で互換性のない変更が行われる可能性がありますので、アプリケーションでの使用を推奨しません。

5.6.2. フレームヒープ

フレームヒープは、初期化時に指定されたメモリ領域から、指定サイズのメモリ領域を切り出して使用するメモリ管理クラスです。アライメントの指定ではバイト境界のほかに、その符号によってメモリ領域をヒープの先頭から確保するのか、末尾から確保するのかを選択することができます。ヒープの先頭からメモリ領域を確保している場合に限り、最後に確保したメモリ領域のサイズを変更することができます。

確保したメモリ領域は個別に解放することができず、すべてのメモリ領域か、先頭から確保したもの、末尾から確保したもののように一括して解放することになります。

フレームヒープのインスタンスは、ロック操作を指定するクラステンプレート(nn::fnd::FrameHeapTemplate)、ロック操作をしない nn::fnd::FrameHeap、スレッドセーフ(ロック操作は nn::os::CriticalSection)な nn::fnd::ThreadSafeFrameHeap のいずれかで行うことができます。

5.6.3. ユニットヒープ

ユニットヒープは、初期化時に指定されたメモリ領域から、一定サイズのメモリ領域(ユニット)を切り出して使用するメモリ管理クラスです。アライメントの指定は初期化時に行い、デフォルトでは 4 Byte が指定されます。連続して確保したメモリ領域が、連続するメモリ領域から確保されるとは限りません。
確保したメモリ領域は個別に解放することができます。確保していたメモリ領域を一括で解放する場合は、Invalidate() を呼び出したあとに Finalize() を呼び出して、ヒープの終了処理を行います。そのヒープを続けて利用する場合には Initialize() を呼び出して再構築してください。

ユニットヒープのインスタンスは、ロック操作を指定するクラステンプレート(nn::fnd::UnitHeapTemplate)、ロック操作をしない nn::fnd::UnitHeap、スレッドセーフ(ロック操作は nn::os::CriticalSection)な nn::fnd::ThreadSafeUnitHeap のいずれかで行うことができます。

5.6.4. 拡張ヒープ

拡張ヒープは、初期化時に指定されたメモリ領域から、指定サイズのメモリ領域を切り出して使用するメモリ管理クラスです。アライメントの指定ではバイト境界のほかに、空いているメモリ領域をヒープの先頭から探して確保するのか、末尾から探して確保するのかを符号で選択することができます。また、確保したメモリ領域のサイズを変更することができます。

確保したメモリ領域は個別に解放することができます。確保と解放を繰り返すと、GetTotalFreeSize() で取得した空き容量より小さなサイズでもメモリ領域を確保できなくなる可能性があります。これはヒープ内に指定されたサイズの連続したメモリ領域がないためです。GetAllocatableSize() であれば、連続するメモリ領域として確保することのできる最大サイズを取得することができます。

拡張ヒープのインスタンスは、ロック操作を指定するクラステンプレート(nn::fnd::ExpHeapTemplate)、ロック操作をしない nn::fnd::ExpHeap、スレッドセーフ(ロック操作は nn::os::CriticalSection)な nn::fnd::ThreadSafeExpHeap のいずれかで行うことができます。

5.7. DLL 機能(RO ライブラリ)

DLL 機能とは、動的にメモリに読み込んだモジュールを使用する機能です。この機能を利用することで、アプリケーションのコード部分が使用するメモリ量を削減したり、アプリケーション起動までの時間を短縮したりすることができます。

CTR-SDK では、DLL 機能を利用するためのライブラリとして RO ライブラリを提供します。

5.7.1. DLL 機能に関する用語

CTR-SDK で提供されている DLL 機能に関する用語を解説します。これらの用語の意味や用いられ方は、一般的なものと異なる場合があります。

モジュール

実行コードを意味のある単位でまとめたものです。

静的モジュール

エントリ関数(nnMain())が含まれ、起動時にロードされるモジュールです。静的モジュールのサイズを小さくすることで起動時間を短縮することができます。

動的モジュール

動的にロードして実行することのできるモジュールです。機能ごとに分けられた動的モジュールを入れ替えることでメモリ消費量を削減することができます。

シンボル

変数や関数のモジュール内での位置を示す情報です。

参照(インポート)

ほかのモジュール内のシンボルで示された変数や関数を使用すること、およびその宣言です。

解決

参照するシンボルが利用可能な状態になることです。

公開(エクスポート)

シンボルで示された変数や関数を、ほかのモジュールで使用できるようにすること、およびその宣言です。

公開種別

シンボルをどのような形式で公開するかを示します。名前、インデックス、オフセットの 3 形式あります。

5.7.2. RO ライブラリの特徴と制限

一般的な DLL 機能の実装と比較して、RO ライブラリには以下のような特徴があります。

  • 3 種類の公開種別
    シンボルを解決するための方式として、オフセット、インデックス、名前の 3 つの形式のいずれか 1 つを使用することができます。どの形式を使用するかはシンボルごとに選択できますが、CTR-SDK のビルドシステムではモジュール単位での選択のみをサポートしています。
    公開種別の指定方法については「CTR-SDK ビルドシステムマニュアル(DLL 編)」または「ビルドシステム構築ガイド(DLL 編)」を参照してください。
  • モジュール間の参照を自動で解決
    モジュールをロードするだけでモジュール間の参照を自動的に解決し、関数や変数が利用可能となります。公開種別が名前またはインデックスのシンボルであれば、シンボルのポインタを手動で取得することもできます。
  • モジュールのメモリへのロードはアプリケーションで行う
    動的モジュールのメモリ上への読み込みや配置の調整はアプリケーションで行う必要があります。
  • C++ で記述されたコードに対応
    C++ で記述されたコードを動的モジュールにすることができます。また、C++ のシンボルをモジュール間で参照/解決することができます。ただし、公開種別が名前のシンボルのポインタを手動で取得する場合はマングルされた名前を使用する必要があります。
  • モジュールの境界を越えた C++ 例外の受け渡し
    あるモジュールで throw された例外を、異なるモジュールで catch することができます。

RO ライブラリには以下の上限および制約があります。

  • 1 つのモジュールが名前で公開できるシンボルは最大 65535 個です。
  • シンボルを名前で公開する場合の最大の名前長は 8192 文字です。
  • 動的モジュールに C の標準関数やそれに類するものを含めることはできません。
  • 動的モジュールで 8 Byte を超えるアライメントを指定した静的な変数/定数を定義した場合、そのアライメント制約は無視されます。
  • 同時にロードできる動的モジュールの上限数は 64 個です。
  • weak シンボルなど C/C++ 言語非標準の機能を使用した場合、意図した動作にならない場合があります。

スタティックライブラリを動的モジュールとして使用する場合は、ライブラリの作成者に動的モジュールとして使用可能かどうかを確認してください。

注意:

SDK で提供されているスタティックライブラリを動的モジュールとして使用することは禁止されています。

同じスタティックライブラリを複数の動的モジュールに組み込まないでください。グローバル変数などが複数存在するなど、ライブラリの意図しない動作による不具合が発生する可能性があります。

5.7.3. RO ライブラリで使用するファイル

RO ライブラリでは DLL 機能を実現するために、以下のファイル形式で作成されたデータを使用しています。

表 5-8. RO ライブラリで使用するファイル

ファイル形式(拡張子)

説明

crs

静的モジュールの参照/公開情報が格納されているファイルです。実行コードは含まれていません。なお、静的モジュールはアプリケーションに 1 つしか存在しないため、crs ファイルも 1 つしか存在しません。

crr

1 つ以上の動的モジュールの管理情報が格納されているファイルです。ROM アーカイブ内の特定のディレクトリ(/.crr/)直下に配置されていなければなりません。

cro

動的モジュールの実行コードと参照/公開情報が格納されているファイルです。

各ファイルの関係を図にすると以下のようになります。

図 5-4. RO ライブラリで使用するファイルの関係

静的モジュール crs crr cro

通常のアプリケーションでは、アプリケーションで使用するすべての cro の情報を含んだ crr を作成し、crr を 1 つだけ使用すれば十分です。crr が使用するメモリも最適化したい場合や追加プログラムの配信を行う場合は、複数の crr を使用します。

補足:

各ファイルの作成方法については「CTR-SDK ビルドシステムマニュアル(DLL 編)」または「ビルドシステム構築ガイド(DLL 編)」を参照してください。

5.7.4. 公開と参照

ヘッダファイルが共有されている場合、モジュール間での関数の呼び出しや変数の参照は通常のソースコードと同様に記述するだけです。シンボルが公開されているかどうかを特に意識する必要はありません。

ヘッダファイルが共有されていない場合、ほかのモジュールの関数を呼び出したり変数を参照したりするには、モジュールのソースコードで明示的に公開と参照を宣言する必要があります。

明示的な公開と参照の宣言には、以下の定義をソースコードに記述します。

表 5-9. 明示的な公開と参照の宣言で使用する定義

定義

説明

NN_DLL_EXPORT

ほかのモジュールに公開する関数や変数の宣言に記述します。

NN_DLL_IMPORT

ほかのモジュールで公開されている関数や変数の宣言に記述します。

コード 5-26. 明示的な公開と参照の宣言のコード例
// 明示的な公開の宣言のコード例
NN_DLL_EXPORT int g_Variable;
NN_DLL_EXPORT void Function()
{
    // (略)
}

// 明示的な参照の宣言のコード例
extern NN_DLL_IMPORT int g_Variable;
extern NN_DLL_IMPORT void Function();
補足:

公開を明示的に宣言したシンボルは必ず公開され、モジュール内で使用されていなくてもデッドストリップされません。

5.7.5. 公開種別による違い

シンボルの公開種別によって、以下のような違いがあります。

表 5-10. 公開種別による違い

項目

名前

インデックス

オフセット

参照情報のサイズ

公開情報のサイズ

0

モジュールのロードに要する時間

ポインタの手動取得

不可

参照するモジュールを先に作成

不可

不可

ビルドステップ

通常

通常

名前>インデックス>オフセットの順に高機能となりますが、ファイルサイズや処理時間といったコストがかかります。

通常はオフセットを選択し、シンボルのポインタを手動で取得する場合やライブラリのように動的モジュールを使用する場合は、必要に応じてインデックスや名前を選択します。

補足:

静的モジュールのシンボルについても公開種別の指定を行うことができます。

5.7.6. 動的モジュールの特別な関数

動的モジュールのソースコード内に特定の名前の関数が公開されている場合、モジュールの初期化処理などのタイミングで、RO ライブラリがその関数を呼び出します。

コード 5-27. 特別な関数
extern "C" NN_DLL_EXPORT void nnroProlog();
extern "C" NN_DLL_EXPORT void nnroEpilog();
extern "C" NN_DLL_EXPORT void nnroUnresolved();

nnroProlog() は、DoInitialize() によるモジュールの初期化処理がすべて完了したあとに呼び出されます。モジュール固有の初期化処理を実装することができます。

nnroEpilog() は、DoFinalize() によるモジュールの終了処理がすべて完了したあとに呼び出されます。モジュール固有の終了処理を実装することができます。

nnroUnresolved() は、モジュールから外部のシンボルを使用したときに、そのシンボルが未解決だった場合に呼び出されます。未解決状態のシンボルを呼び出したことを検知する処理を実装することができます。

これらの関数の実装は必要に応じてアプリケーションで行ってください。

補足:

nnroUnresolved() は静的モジュールでも使用可能ですが、参照が解決されていたシンボルが未解決になった場合には機能しないという制約があります。

5.7.7. 基本の処理フロー

ここでは、動的モジュールを使用する際の基本の処理フローを順を追って説明します。

5.7.7.1. RO ライブラリの初期化

RO ライブラリの初期化は nn::ro::Initialize() で行います。初期化には crs ファイルが必要ですので、事前にメモリに読み込んでおかなければなりません。なお、crs ファイルの内容を格納するバッファは、デバイスメモリ以外のメモリ領域に確保し、その先頭アドレスが nn::ro::RS_ALIGNMENT ( 4096 Byte ) のアライメント、バッファサイズが nn::ro::RS_UNITSIZE ( 4096 Byte ) の倍数でなければならないことに注意してください。

crs ファイルが格納されているバッファは RO ライブラリの管理下に置かれるため、RO ライブラリの終了処理を行うまで、内容を書き換えたり、バッファを解放したりしないでください。

5.7.7.2. 管理情報の登録

動的モジュールを使用する前に、それを管理する crr ファイルを読み込み、nn::ro::RegisterList() で管理情報の登録を行わなければなりません。なお、crr ファイルの内容を格納するバッファは、デバイスメモリ以外のメモリ領域に確保し、その先頭アドレスが nn::ro::RR_ALIGNMENT ( 4096 Byte ) のアライメント、バッファサイズが nn::ro::RR_UNITSIZE ( 4096 Byte ) の倍数でなければならないことに注意してください。

crr ファイルが格納されているバッファは RO ライブラリの管理下に置かれるため、nn::ro::RegisterList() で返された nn::ro::RegistrationList クラスのポインタで Unregister() を呼び出して管理情報の登録を解除するまで、内容を書き換えたり、バッファを解放したりしないでください。

5.7.7.3. 動的モジュールのロード

動的モジュールのロードは nn::ro::LoadModule() で行います。cro ファイルはアプリケーションで事前にメモリに読み込んでおかなければなりません。なお、cro ファイルの内容を格納するバッファは、デバイスメモリ以外のメモリ領域に確保し、その先頭アドレスが nn::ro::RO_ALIGNMENT_LOAD_MODULE ( 4096 Byte ) のアライメント、バッファサイズが nn::ro::RO_UNITSIZE_LOAD_MODULE ( 4096 Byte ) の倍数でなければならないことに注意してください。

ほかにも .data/.bss セクションとして使用されるバッファを用意する必要があります。このバッファは先頭アドレスが nn::ro::BUFFER_ALIGNMENT ( 8 Byte ) のアライメントでなければなりません。バッファのサイズは、nn::ro::GetSizeInfo() に cro ファイルの内容(先頭から REQUIRED_SIZE_FOR_GET_SIZE_INFO バイト)を渡して取得した、nn::ro::SizeInfo 構造体の bufferSize メンバの値以上でなければなりません。なお、SizeInfo 構造体にはロード後に使用可能となるメモリ領域の情報も格納されています。ロード後に解放されるメモリ領域のサイズが bufferSize よりも大きいならば、このバッファをそのメモリ領域から捻出することができます。

引数 fixLevel での指定によって、動的モジュールの機能やロード後に解放されるメモリ領域が以下のように異なります。指定を省略した場合は FIX_LEVEL_1 が指定されたものとして処理されます。

表 5-11. fixLevel の指定による動的モジュールの動作の違い

項目

FIX_LEVEL_0

FIX_LEVEL_1

FIX_LEVEL_2

FIX_LEVEL_3

アンロード後の再利用

不可

不可

不可

以降にロードされるモジュールに含まれているシンボルとの自動リンク

不可

不可

以降にロードされるモジュールと、このモジュールに含まれているシンボルとの自動リンク

不可

このモジュールに含まれているシンボルのアドレス取得

不可

現在までにロードされているモジュールに含まれているシンボルとの自動リンク

ライブラリの管理下となる領域の最終アドレス

fix0End

fix1End

fix2End

fix3End

ロード後に解放されるメモリ領域のサイズは FIX_LEVEL_0FIX_LEVEL_1FIX_LEVEL_2FIX_LEVEL_3 となります。また、シンボルの公開種別が名前であれば、解放されるメモリ領域のサイズはほかの公開種別よりも大きくなります。

引数 doRegister には通常 true を指定して自動リンク対象にし、ほかの動的モジュールがロードされたときに、ロード時点で未解決だったシンボルへの参照が自動的に解決されるようにします。false を指定した場合、これまでにロードされている動的モジュールに含まれているシンボルへの参照は解決されますが、ほかの動的モジュールがロードされたときに自動的に解決されません。この場合、nn::ro::LoadModule() で返される nn::ro::Module クラスのポインタで Link() を呼び出し、手動で解決する必要があります。

5.7.7.4. 動的モジュールの使用開始

動的モジュールに含まれている関数や変数を利用する前に、nn::ro::LoadModule() で返された nn::ro::Module クラスのポインタで DoInitialize() を呼び出し、動的モジュール内のグローバルオブジェクトの構築や nnroProlog() に実装された初期化処理を実行する必要があります。

初期化処理でほかの動的モジュールのグローバルオブジェクトを参照していなければ、DoInitialize() はモジュールのロード直後に呼び出すことができますが、相互にグローバルオブジェクトを参照している場合はロードまでで処理を止めておき、すべての動的モジュールをロードしたあとに DoInitialize() を呼び出してください。

動的モジュールの名前を Module クラスの GetName() で取得することができます。この関数で返される名前はビルド時に指定したモジュール名(CTR-SDK のビルドシステムであれば TARGET_MODULE で指定した名前)と同じです。また、ロードされている動的モジュールを nn::ro::FindModule() で検索する際の名前にも使用します。

動的モジュールから外部への参照がすべて解決済みであるかどうかは、Module::IsAllSymbolResolved() で判断することができます。なお、動的モジュールへの参照を含めて、そのモジュールの解決済みの参照をすべて未解決状態にするには Module::Unlink() を呼び出します。参照を再度解決させる場合は Module::Link() を呼び出します。

シンボルのポインタの手動取得には、nn::ro::GetPointer() ですべてのモジュールのシンボルから名前で検索する方法と、nn::ro::Module::GetPointer() で特定の動的モジュールに含まれているシンボルから名前またはインデックスで検索する方法があります。また、nn::ro::GetAddress() では、名前で検索したシンボルのアドレスを取得することができます。

5.7.7.5. 動的モジュールの使用終了

動的モジュールが不要になり、そのモジュールが使用していたメモリ領域を解放するには、まず Module クラスの DoFinalize() で動的モジュール内のグローバルオブジェクトの破棄や nnroEpilog() に実装された終了処理を実行する必要があります。そして終了処理が完了したあとに、Module クラスの Unload() で動的モジュールをアンロードします。

アンロードによって動的モジュールはロード前の状態に戻る(ライブラリの管理下からメモリを解放し、参照をすべて未解決状態にする)ため、cro ファイルの内容を格納していたバッファを解放することができます。

アンロードした動的モジュールを再度使用する場合は、基本的に cro ファイルの読み込みからやり直す必要があります。ただし、引数 fixLevelFIX_LEVEL_0FIX_LEVEL_NONE)を指定してロードしていた場合は、ある条件を満たせば cro ファイルを読み込み直すことなく、使用していたバッファなどを nn::ro::LoadModule() でそのまま再利用することができます。再利用の条件は、静的変数の初期値を使用しない、もしくは nnroProlog() ですべての静的変数を初期化することです。これは、アンロードを実行しても動的モジュールを使用したときに書き換えられた静的変数がロード前の状態に戻らないためです。

5.7.7.6. 管理情報の登録解除

管理情報で使用していたメモリ領域を解放するには、nn::ro::RegisterList() で返された nn::ro::RegistrationList クラスへのポインタで Unregister() を呼び出し、管理情報の登録を解除する必要があります。

管理情報の登録が解除されると、crr ファイルの内容を格納していたバッファがライブラリの管理下から解放されます。なお登録解除後は、その crr ファイルが管理している動的モジュールを新たにロードできなくなりますが、ロード済みの動的モジュールに対する処理は正常に行うことができます。

5.7.7.7. RO ライブラリの終了処理

RO ライブラリの終了処理は nn::ro::Finalize() で行います。

nn::ro::Finalize() を呼び出したあとは、crs ファイルの内容を格納していたバッファだけでなく、crr ファイルや cro ファイルを格納していたバッファなど、RO ライブラリで使用していたメモリのすべてを解放することができるようになります。

5.7.8. 動的モジュールの列挙、探索

nn::ro::Module::Enumerate() で、自動リンクでロードされたかどうかに関係なく、すべての動的モジュールを列挙することができます。Enumerate() の引数には、nn::ro::Module::EnumerateCallback を継承したクラスを渡します。動的モジュールごとに、動的モジュールへのポインタを引数に operator() が呼び出されます。

nn::ro::Module::Find() で動的モジュールを探索することができます。指定された文字列と名前が一致する動的モジュールが見つかればモジュールへのポインタを返し、見つからなければ NULL を返します。ただし、探索の対象となるのは、自動リンクでロードされた動的モジュールのみです。

5.7.9. 動的モジュールが使用しているメモリ領域の情報

動的モジュールが使用しているメモリ領域の情報を、nn::ro::Module::GetRegionInfo() で取得することができます。メモリ領域の情報は、引数 pri に渡した nn::ro::RegionInfo 構造体に格納されます。