【R-CHR】Rust + egui でファミコン用スプライトエディタを作った話 その1 概要とアーキテクチャ
2026.09.22

どもです。
ちょっと前の事になりますが、Rust でファミコン(NES)用のスプライト(CHR)エディタを作った話でもつらつらと記事を書いていこうかと思います。
名前は R-CHR。Rust で書いた CHR エディタなので、そのまんまの名前となります。

リポジトリはこちら。
ファミコンのグラフィックいじりといえば YY-CHR が定番中の定番でして、自分もずっとお世話になっていたのですが、「描画ツールがもう少し欲しい」「PNG をもっと雑に放り込みたい」「Mac でもネイティブに動いてほしい」といった欲が出てきまして。だったら自分で作ってしまえ、と。
ということで、今回から数回に分けて、この R-CHR の技術的な中身を順を追って解説していく形になります。ファミコンのグラフィック形式の話から、egui での UI 構築、描画アルゴリズム、PNG インポート、macOS ネイティブ対応、GitHub Actions でのリリースまで、一通り触れていく予定です。
連載の予定
- 概要とアーキテクチャ(この記事)
- NES の CHR フォーマット(2BPP)と iNES ヘッダ、パレット
- egui で組む 3 パネル UI とバンクビュー
- ドットエディタの基礎とアンドゥ設計
- 図形ツール(Bresenham・楕円・塗りつぶし・スタンプ)
- PNG インポートと 3 つのマッピング戦略
- macOS ネイティブメニュー・多言語対応・GitHub Actions でのリリース
R-CHR でできること
まずはどんなものかをざっくりと。

.nes(iNES 形式)/.bin(生の CHR バイナリ)/.zip(中の.nesを自動展開)を開いて編集・保存- CHR データ全体を 16 タイル幅の縦長テクスチャとして連続スクロール表示
- 8 / 16 / 32 / 64 / 128px の 5 段階で編集単位(フォーカスサイズ)を切り替え
- ペン・パターンペン・直線・矩形(枠 / 塗り / パターン)・楕円(枠 / 塗り)・塗りつぶし・スタンプの 10 種類の描画ツール
- 1 ストローク = 1 アンドゥ単位の Undo(最大 100 ステップ)
- タイルブロックのコピー & ペースト
- インデックスカラー / フルカラー / 透過 PNG のインポート(3 種類のマッピング戦略)
- YY-CHR 互換の
.pal/.datパレットファイル読み書き - macOS はネイティブメニューバー、日本語 / 英語の UI 切り替え
YY-CHR の基本機能は押さえつつ、描画周りと画像の取り込みを厚くした、という感じですね。
技術スタック
言語は Rust(edition 2024)、GUI は egui / eframe を採用しています。
Cargo.toml の依存はこんな感じです。
[package]
name = "R-CHR"
version = "0.1.0"
edition = "2024"
[dependencies]
eframe = "0.31"
egui = "0.31"
egui_extras = { version = "0.31", features = ["svg"] }
rfd = "0.15"
zip = { version = "2", default-features = false, features = ["deflate"] }
image = { version = "0.25", default-features = false, features = ["png", "bmp"] }
png = "0.17"
[target.'cfg(target_os = "macos")'.dependencies]
muda = "0.15"
objc2-app-kit = { version = "0.2", default-features = false, features = ["NSAlert", "NSAppearance", "NSApplication"] }
objc2-foundation = { version = "0.2", default-features = false, features = ["NSThread"] }
| クレート | 用途 |
|---|---|
eframe / egui |
ネイティブウィンドウとイベントループ、即時モード GUI |
egui_extras |
SVG アイコンの読み込み(svg feature) |
rfd |
OS ネイティブのファイルダイアログ(開く / 保存) |
zip |
ZIP 内の .nes を取り出す |
image |
PNG / BMP を RGBA に展開する。アイコン読み込みにも使用 |
png |
PNG の PLTE / tRNS チャンクなど低レベル情報にアクセスする |
muda |
macOS のネイティブ NSMenu 構築(macOS のみ) |
objc2-app-kit / objc2-foundation |
NSAppearance(ダークモード)や NSAlert の呼び出し(macOS のみ) |
なぜ egui なのか
Rust の GUI ライブラリはいくつか選択肢がありますが、今回は egui にしました。
理由はシンプルで、即時モード(immediate mode)GUI なので、ドットエディタのような「毎フレーム状態から絵を描き直す」タイプのアプリと相性がとても良いからです。ウィジェットツリーを保持して差分更新する、みたいなことを考えなくて良い。update() の中で「今の状態」をそのまま描けば、それが画面になります。
あとは単一バイナリで Windows / macOS / Linux に配れるという点も大きいですね。ここは後の回で GitHub Actions のビルドと合わせて触れます。
ちなみに、macOS のメニューバーだけは egui のメニューではなくネイティブ NSMenu を使っています。この辺りの「egui で足りないところをどう埋めたか」は最終回で。
全体アーキテクチャ
構造はこんな形になっています。
┌──────────────────────────────────────────────────────────┐
│ eframe(ネイティブウィンドウ・OpenGL / Metal) │
│ ┌────────────────────────────────────────────────────┐ │
│ │ egui(即時モード GUI レンダリング) │ │
│ │ ┌──────────────────────────────────────────────┐ │ │
│ │ │ editor::app::RChrApp(アプリ状態) │ │ │
│ │ │ update() ─ 毎フレーム呼ばれる UI 構築 │ │ │
│ │ └──────────────────────────────────────────────┘ │ │
│ └────────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────┘
│ │
io モジュール model モジュール
├── chr.rs └── palette.rs
├── nes.rs
└── png.rs
[macOS のみ]
native_menu.rs ──── muda(NSMenu)
└── objc2-app-kit(NSAppearance)
モジュールは大きく 3 層に分かれています。
| 層 | 責務 | 依存先 |
|---|---|---|
model |
NES パレットのデータ表現(MasterPalette / DatPalette) |
なし |
io |
CHR のデコード / エンコード、iNES パース、PNG 変換 | model |
editor |
egui の UI と、アプリ状態 RChrApp |
io, model |
依存の方向は常に editor → io → model の一方通行で、モデル層は egui にも他のモジュールにも依存しません。CHR の 2BPP 変換や PNG のマッピングといった「純粋なデータ処理」が UI から切り離されているので、この後の記事でもその順番で解説していけるという寸法です。
ディレクトリ構成
src/
main.rs — エントリポイント & ウィンドウ設定
native_menu.rs — macOS ネイティブメニュー(muda)
editor/
app.rs — アプリ状態 & メインループ(eframe::App)
bank_view.rs — バンクビュー(CHR 全体表示 & タイル選択)
clipboard.rs — タイルのコピー & ペースト
dot_editor.rs — ドットエディタ & 10 種類の描画ツール
file_ops.rs — 開く / 保存 / 新規作成
i18n.rs — 日本語 / 英語の UI 文字列
info_panel.rs — 右パネル(パレット・描画色・情報表示)
keyboard.rs — キーボードショートカット
png_import.rs — PNG インポートダイアログ
setup.rs — フォント設定
theme.rs — 色・フォント・サイズ定数
mac/
menu_bar.rs — egui メニューバー(macOS 以外)
menu_events.rs — macOS ネイティブメニューのイベント処理
io/
chr.rs — CHR のデコード / エンコード / レンダリング
nes.rs — iNES パーサ / RomData 型
png.rs — PNG / BMP インポート(マッピング & CHR 書き込み)
model/
palette.rs — NES パレット(MasterPalette / DatPalette)
assets/
rchr.pal — デフォルトのマスターパレット(YY-CHR 互換 .pal)
rchr.dat — デフォルトのパレットセット(YY-CHR 互換 .dat)
rchr.bin — 起動時に表示する CHR データ(R-CHR ロゴ)
nes.pal — NES 標準 64 色パレット(リセット用)
icons/ — ツールバー用 SVG アイコン
fonts/ — Noto Sans JP(日本語表示用)
editor/ 配下は「app.rs を薄いオーケストレーターに保ち、機能ごとに impl RChrApp ブロックを別ファイルへ分割する」という方針で書いています。Rust は同じ構造体の impl を複数ファイルに分けて書けるので、app.rs には構造体定義と update() の骨格だけを置き、バンクビューは bank_view.rs、ドットエディタは dot_editor.rs、といった具合に責務ごとに分けています。
最初は 1 ファイルにベタ書きだったのですが、描画ツールを増やしていくうちに 1,700 行を超えてきて、いかぁーん、となって分割した経緯があります。。
画面レイアウト
画面は左から順に 3 つのパネルで構成されています。
| パネル | 位置 | 幅 |
|---|---|---|
| バンクビュー | 左 | 最低 410px。ウィンドウ幅に合わせて整数倍で拡大 |
| ドットエディタ | 中央 | リサイズ可能。最低 180px |
| 情報パネル | 右 | 245px 固定 |
バンクビューで編集したいタイルをクリックすると、ドットエディタにそのブロックが拡大表示され、右の情報パネルでパレットや描画色を選ぶ、という流れです。この辺りの egui での組み方は第 3 回で。
ビルドと起動
Rust 1.80 以降があればビルドできます。依存ライブラリは Cargo が勝手に取ってきてくれます。
git clone https://github.com/retrodig/r-chr.git cd r-chr cargo run --release
起動すると assets/rchr.bin に埋め込んである R-CHR ロゴ入りの CHR データが表示されます。ウィンドウに .nes ファイルをドラッグ & ドロップすれば、そのまま中身の CHR-ROM が開きます。
ビルド済みのバイナリは GitHub の Releases に Windows / macOS / Linux 向けを置いてありますので、Rust 環境が無い方はそちらからどうぞ。
まとめ
- R-CHR は Rust + egui で作った NES 用 CHR タイルエディタ
- YY-CHR の基本機能に加えて、10 種類の描画ツール、PNG インポート、連続スクロール表示などを搭載
- 構成は
editor → io → modelの 3 層で、データ処理は UI から分離 - 機能ごとに
impl RChrAppをファイル分割し、app.rsは薄く保つ
次回は、そもそもファミコンの絵がどういうバイト列で格納されているのか、という CHR の 2BPP フォーマットと iNES ヘッダ、パレットの話になります。ここが分かると、あとの描画ツールや PNG インポートの実装がすんなり読めるようになるので、まずはデータの話から。
ではではぁ。
またまたぁ。














