M1 の成果:アセット層
M1 では、ベースラインのアセット層を実装し検証しました。その内容は、Ren'Py の RPA アーカイブを置き換える暗号化コンテナ、ランタイムのローダーフック、Ren'Py に同梱された Python の内部で動作する暗号バックエンドです。検証には Ren'Py 8.5.2 を使用し、すべてのアセット種別とストリーミングの経路をカバーするため、SDK のサンプルプロジェクト the_question と、数千のアセットからなるギガバイト級の配布用ビルドの両方で実行しました。
対象プラットフォームは Windows で、開発と検証のホストは macOS/arm64 です。Windows のランタイム経路(CNG)は、M4 で別途実装し検証しています。
実証された内容
すべてのアセット種別が、エンジン内部の実際の renpy.loader を通じてバイト単位で同一に復号されることを、各元ファイルの SHA-256 と比較して確認しました。
lint はコンテナからすべての .rpyc と翻訳ファイルを解析するため、Ren'Py のスクリプト層は完全に読み込まれます。保護後のディスク上には、平文のアーカイブ、.rpyc、.rpy は残らず、残るのは renmori.rmc と renmori.rpe、そして実行時に生成される cache/ のみです。
既存ツールは失敗する
unrpaはコンテナを受け付けない。コンテナは RPA アーカイブではないため、バージョンの自動検出に失敗する- ディスク上には
unrpycがデコンパイルできる.rpycや.rpyが残っていない - ギガバイト級のコンテナに対してシグネチャによるカービングを行っても、複数バイトのマジック(
PNG、RENPY RPC2、OggS、RIFF、OTTO、zip)は 1 つも見つからず、統計的に予想される頻度で 3 バイトの偶然の一致があるのみで、そのいずれからもデータは復元できない。コンテナはランダムなデータと区別がつかない
パフォーマンス:ベースラインのリファレンス読み込み処理
Python によるリファレンスの読み込み処理(ctypes → librenpython 内の OpenSSL EVP、64 KB のチャンクごとに AES-256-GCM)を通じた逐次復号のスループットは、Ren'Py 内で数 MB のアセットに対して計測したところ、約 74 MB/s でした。これは、数 MB/s しか必要としないリアルタイムの動画と音声のデコード速度を大きく上回ります。小さなアセットはバッファの経路を通り、その後は Ren'Py 自身の C バッファ RWops に渡されるため、読み込みごとの GIL の競合はそれ以上発生しません。
これらの数値はリファレンスの読み込み処理によるものであり、製品版のネイティブ復号器ではチャンクごとの Python 呼び出しのオーバーヘッドがなくなります。実際のプレイ中におけるフレーム時間の知覚的な比較(保護前後)は、ベンチマーク用の環境で別途追跡しています。
確立されたアーキテクチャ上の重要な事実
- フックポイント:
file_open_callbacks(先頭に挿入)とscandirfiles_callbacks(ファイルの検出とlower_map)。.rpe拡張はindex_files()より前に読み込まれる(ローダーフックを参照) - サードパーティの依存関係なしで Ren'Py 内で AES を利用:macOS/Linux では
librenpythonがエクスポートする OpenSSL EVP(sys.executableと同じ場所にある)、Windows では CNG を使用。_ctypesは同梱される 3 つのランタイムすべてに組み込まれている to_rwopsの再オープンの罠を回避:ストリーミング用のオブジェクトは.nameと.rawのいずれも公開しない
既知の課題と次のステップ
renpy.exists()(ディスクのみを対象とするtransfn)は、コンテナに格納されたファイルに対して False を返す。必要とするプロジェクトがあればすぐに適用できるパッチを用意済み- ベースラインのプレースホルダー鍵は
autorun.pyに埋め込まれている。実行時の鍵導出はクローズドのコンポーネントが担う - ベースラインのスクリプト層は依然として標準の
.rpycを配布している(保存時は暗号化)。独自の AST 形式はクローズドの多態化層が担う - ウィンドウ表示での知覚的なパフォーマンスのベンチマーク(フレーム時間)は、ベンチマーク用の環境で追跡している