Skip to content

Instantly share code, notes, and snippets.

@akiyan
Last active July 13, 2026 02:24
Show Gist options
  • Select an option

  • Save akiyan/f01687cfc704c607f87955c05fe185c7 to your computer and use it in GitHub Desktop.

Select an option

Save akiyan/f01687cfc704c607f87955c05fe185c7 to your computer and use it in GitHub Desktop.
SEGA-CD Tile Texture Reuse Codec — hardware playback timeline (dual-68000 execution & sync)

SEGA-CD Tile Texture Reuse Codec — 実機再生タイムライン(68000 二基)

SEGA-CD(メガCD)の 2 つの 68000 CPU が、実機で MOVIE.DAT ストリームを再生する とき 実際に何を実行し、どう同期し、どのメモリをどれだけ使うか を実行時の視点で まとめたもの。対象は Tile Texture Reuse Codec のプレイヤー boot/movieplay_sp.s(Sub CPU)と boot/movieplay_ip.s(Main CPU)。 メモリ数値は H40フル(320×224 / 40×28=1120セル / 15fps)の実測ビルドに基づく。

この設計は、3 つの厳しいハード制約を 同時に 満たすために存在する:

  1. CD は連続読みを維持ROM_READN 1 回、シークし直さない (再シークはストリームが許せる帯域よりはるかに高くつく)。
  2. VDP の VRAM 転送は垂直帰線(VBLANK)中しか DMA できない — アクティブ表示に はみ出した DMA は、厳密なハード/エミュで画面を壊す。
  3. 2 つの CPU が 1 つの Word RAM を共有 — 同じバンクを同時には触れないので、 フレームバッファを両者で ping-pong させる。

答えは producer/consumer パイプライン: Sub CPU が裏バンクへ 1 フレームを生成する間、 Main CPU が表バンクから前フレームを消費し、フレーム境界でバンクを受け渡す。


登場するハードウェア(メモリ容量つき)

 ┌──────────────┐  セクタ    ┌───────────────┐   復号    ┌──────────────┐
 │  CDドライブ  │─(CDC DMA)─▶│  Sub CPU (SP) │──(pop)──▶│  Word RAM     │
 │  連続        │            │  68000        │           │  1M / 1M      │
 │  ROM_READN   │            │  生成側       │           │ 各バンク128KB │
 └──────────────┘            └─┬────┬────┬──┘           └──────┬───────┘
                    apply/RING  │    │    │ 887B/frame          │ swap
                                ▼    │    ▼               (MEMMODE bit0)
                    ┌──────────────┐ │ ┌────────────────┐       ▼
                    │  PRG-RAM     │ │ │ RF5C164 PCM    │ ┌──────────────┐
                    │  512KB (SP)  │ │ │ wave-RAMリング  │ │  Main CPU(MD)│
                    │ ┌──────────┐ │ │ │ 音声を再生     │ │  68000       │
                    │ │RING 貯水池│ │ │ └────────────────┘ │  消費側      │
                    │ │420KB     │ │ │                     └──────┬───────┘
                    │ │(タンク)  │ │ │                            │ VBLANK内DMA
                    │ └──────────┘ │ │                     ┌──────▼───────┐
                    │ apply 34KB   │ │                     │  Main-RAM    │
                    │ routing 8KB  │ │                     │  64KB (MD)   │
                    └──────────────┘ │                     │ DMA_STAGE24KB│
                                     └─(Sub が両方に給餌)   │ →VDP→VRAM→画面│
                                                            └──────────────┘
  • Sub CPU(SP) — CD セクタを吸い上げ、1 フレーム分の control ブロックを復号し、 結果のネームテーブル+新規タイルパターンを Word-RAM の 裏バンク へ書き、PCM 音声 リングを補充する。連続読み中は PRG-RAM へバースト書込みしない(payload は CDC_TRN で DMA し、PRG は読むだけ=連続読み中も安全)。
  • PRG-RAM(Sub CPU 側・512KB) — Sub CPU のワーク RAM。中心は RING(貯水池=タンク・ 420KB): 先読みした冷たい(新規)タイルパターンを溜めておき、CD 供給が一時的に細っても 枯渇しないための緩衝。ほかに CDC 着地+control 展開の apply(34KB)、フレーム→セクタの routing(8KB)、プログラムとスタック。
  • Main CPU(MD) — Word-RAM の 表バンク を読み、新規タイルパターンを VRAM へ DMA(VBLANK 内のみ・予算制)、表示を flip、デバッグ HUD を描画、フレームハンドシェイク を主導する。
  • Main-RAM(Main CPU 側・64KB) — Genesis 68000 のワーク RAM。中心は DMA_STAGE(24KB): VDP の DMA 源は必ず Main-RAM なので、Word-RAM から読んだ 1 フレーム分のタイルをここへ整列 してから VRAM へ流す。ほかに shadow[1120](セル→エントリ表・2.2KB)、ラン表、スタック。
  • Word RAM(1M/1M・各バンク 128KB) — 共有ダブルバッファ。一方が「表」(Main が読む)、 他方が「裏」(Sub が書く)。MEMMODE bit0 で所有権を swap。
  • RF5C164 PCM — 自走の音声。Sub CPU がハード再生カーソルの少し先を保つように wave-RAM リングを補充する。

フレームハンドシェイク(誰が誰に合図するか+各フェーズのメモリ)

通信は Gate Array の 2 つのメールボックス: COMCMD0(Main → Sub)と COMSTAT0(Sub → Main)、加えて診断用の COMSTAT1

信号 レジスタ 意味
CMD_STREAM 0x50 COMCMD0 Main: 先頭からストリーム開始
CMD_SWAP 0x51 COMCMD0 Main: 表バンクの処理完了 — swap して
STAT_READY 0x8003 COMSTAT0 Sub: バンクswap済み、次フレームが表に乗った
STAT_END 0x8004 COMSTAT0 Sub: 映画終端(Main は 15 秒静止しループ)
desync / bad-magic COMSTAT1 Sub: 診断カウンタ
sequenceDiagram
    participant M as Main CPU (MD)<br/>Main-RAM 64KB
    participant S as Sub CPU (SP)
    participant P as PRG-RAM<br/>RING 420KB
    participant W as Word RAM<br/>バンク 128KB
    Note over M,W: 起動
    M->>S: CMD_STREAM (0x50)
    S->>P: prebuffer で RING を先読み充填
    Note over P: RING 使用 ~380KB / 420KB (90%)
    S->>S: ROM_READN(連続読み1回), frame 0 を裏バンクへ構築
    S-->>M: STAT_READY (0x8003)
    S->>W: swap → frame 0 を表へ
    loop 各 movie frame N
        par Main が frame N を消費
            M->>W: 表を読む(ネームテーブル+新規パターン)
            M->>M: タイルを DMA_STAGE へ整列, VRAMへDMA(VBLANK予算内)
            Note over M: DMA_STAGE 使用 ≤24KB (peak 45%) / 平常 ~9KB(17%)
            M->>M: CRAM入替? + flip(同一VBLANK内で原子的)
        and Sub が frame N+1 を生成
            S->>P: apply でセクタ着地 + RING からパターンpop
            Note over P: apply 34KB / RING 380KB(90%)
            S->>W: 裏バンクへ書込(セル+パターン)
            Note over W: 裏バンク出力窓 平常 ~9KB(7%) / keyframe ~37KB(29%)
            S->>S: 887 B PCM を給餌
        end
        M->>S: CMD_SWAP (0x51)
        S->>W: バンクswap (MEMMODE bit0)
        S-->>M: STAT_READY
        Note over M,W: 表/裏の役割が反転, N+1 へ
    end
    S-->>M: STAT_END(映画終端) → 15秒静止 → 再び CMD_STREAM
Loading

メモリマップと使用量(H40フル 320×224 / 40×28 / 15fps)

3 つの独立した RAM がある。PRG-RAM(Sub 専用・512KB)、Word-RAM の 1M バンク (共有・128KB を 2 面)、Main-RAM(Main 専用・64KB)。各面の確保レイアウトと、 H40フルでの実使用(率 & KB):

PRG-RAM — Sub CPU ワーク RAM — 512 KB

領域 確保 用途 H40フル使用
program / BIOS 予約 ~48 KB SP プログラム+起動時 ISO_BUF 固定
RING(貯水池=タンク) 420 KB 冷たいタイルパターンの先読みリング 380 KB(12160枚×32B)= リングの90% / PRGの74%
routing 8 KB フレーム→セクタ経路表 ~7.8 KB(3998フレーム×2B)
apply 34 KB CDC 着地+control 線形化 ≤34 KB
stack 2 KB SP スタック
PRG 合計 512 KB ライブピーク ~472 KB ≈ 92%

Word-RAM 1M バンク — 共有ダブルバッファ — 128 KB ×2

Sub が「裏」バンクへ 1 フレームの出力を書き、swap 後に Main が「表」として読む。同じ レイアウトの面が 2 つ。1 面あたり:

領域 オフセット 確保 H40フル使用
palette(PALW+CRAM) +0x0000 130 B 130 B(区間切替時のみ)
loads(冷パターン列) +0x0084 ≤37.9 KB keyframe 全面 ~35 KB(1120枚)/ 平常 ~7 KB(~223枚)
updates(ネームテーブル差分) +0x9802 ≤21.7 KB ≤2.2 KB(最大1120セル×2B)
diag / header +0xAF00 ~256 B 固定
SP scratch(CTRL/PAD) +0x10000 ~8 KB ≤2.2 KB(control 線形化)
1M バンク使用(出力窓) ~44 KB 確保 平常 ~9 KB(7%)/ keyframe ~37 KB(29%)— 128KB中

Main-RAM — Main CPU(Genesis 68000)ワーク RAM — 64 KB

領域 オフセット 確保 H40フル使用
program + shadow + vars 0xFF0000 8 KB shadow 1120×2=2.2 KB +プログラム
DMA_STAGE(タイル整列) 0xFF2000 24 KB ≤24 KB(~768枚, VDP DMA 源)/ 平常 ~9 KB
RUN_TABLE 0xFF8000 8 KB ~0.5 KB(~128ラン×4B)
DBG_STAGE + heap 0xFFA000 ~23 KB 小(フォント色替え等)
stack 0xFFFD00
Main-RAM 使用(ワーキングセット) 64 KB 全域に配置 平常 ~11 KB(17%)/ peak ~29 KB(45%)— 64KB中

使用率をひと目で(H40フル)

pie showData title PRG-RAM 512KB 使用率
    "使用 472KB (92%)" : 472
    "空き 40KB (8%)" : 40
Loading
pie showData title Word-RAM 1Mバンク 128KB 使用率(keyframe)
    "出力窓 使用 37KB (29%)" : 37
    "空き 91KB (71%)" : 91
Loading
pie showData title Main-RAM 64KB 使用率(peak)
    "使用 29KB (45%)" : 29
    "空き 35KB (55%)" : 35
Loading

要点: **PRG-RAM は RING(先読み貯水池)でほぼ満杯(92%)**=ここが一番のメモリ消費源。 一方 1M バンクと Main-RAM は余裕がある(平常 1 桁 %、keyframe/peak でも 3〜5 割)。 これは意図通りで、CD 供給のジッタを PRG の大きなタンクで吸収し、フレーム毎に触る共有 バッファ(1M)と整列バッファ(Main-RAM)は小さく速く保っている。

データが流れる先(どのメモリへ何が入るか)

flowchart LR
    CD["CDドライブ<br/>連続 ROM_READN"] -->|セクタ CDC DMA| APPLY["PRG apply<br/>34KB"]
    APPLY --> RING[("PRG RING 貯水池<br/>420KB / 使用 380KB=90%")]
    RING -->|パターン pop| WB["Word-RAM 裏バンク<br/>128KB / 使用 9〜37KB"]
    WB -->|CMD_SWAP| WF["Word-RAM 表バンク<br/>128KB"]
    WF -->|タイル copy| STAGE["Main-RAM DMA_STAGE<br/>24KB / peak 使用 24KB"]
    STAGE -->|VBLANK DMA| VRAM["VDP VRAM → 画面"]
    RING -.887B/frame.-> PCM["RF5C164 PCM"]
Loading

フェーズ別メモリ使用量(H40フル)

フェーズ 主に使うメモリ H40フル使用(率 / KB)
① CD 吸上(SP) PRG apply + RING 補充 RING 380 / 420 KB(90%)、apply ≤34 KB
② 復号・展開(SP) Word-RAM 裏バンク(出力窓) 平常 9 / 128 KB(7%) / keyframe 37 / 128 KB(29%)
③ Main 読取+整列(MD) Main-RAM DMA_STAGE 平常 ~9 / 64 KB(14%) / peak 24 / 64 KB(38%)
④ VBLANK DMA(MD) DMA_STAGE → VRAM(排出) 上の stage をそのまま VRAM へ流す
⑤ CRAM+flip(MD) Main-RAM shadow[1120] 2.2 / 64 KB(3%)

1 movie frame 期間を並べて見る

ソース本来のレート(例: 15 fps NTSC)では、1 movie frame は 60Hz フィールド 4 枚 = VBLANK 窓 4 回 ぶん。以下はすべてこの 1 期間の中で、2 CPU に並列で起きる。 時間は 左→右。右端の [mem] は各レーンがそのフェーズで触るメモリの H40フル使用量。

              ┌──────────────── movie frame 期間 (≈66.7 ms @15fps = 4フィールド) ────────────────┐
 フィールド   │  active走査    ░VB░│  active走査    ░VB░│  active走査    ░VB░│  active走査    ░VB░│
 /VDP         │  (ここでDMA不可)   │  DMA窓 ~1.3ms      │  DMA窓             │ CRAM+flip @頭     │
              ├────────────────────┼────────────────────┼────────────────────┼────────────────────┤
 CD  (SP)     │══ ROM_READN: セクタが流れ込み続ける, ドライブは再シークしない ═══════════════════▶  [PRG apply 34KB]
              │   (pump_poll が余裕+セクタ準備の時だけ CDC → PRGリング/applyリング へ吸上)          [RING 380/420KB=90%]
              ├────────────────────┴────────────────────┴────────────────────┴────────────────────┤
 Sub CPU (SP) │ frame N+1 の5セクタ吸上 → control復号 → WORD-RAM *裏* へ展開                       │  [Word裏 出力窓]
              │ ├ payload: 32B 新規タイルパターン → PRGリング (CDC_TRN DMA)                        │  [平常 9/128KB=7%]
              │ ├ control: bitmap + ネームテーブルentry → 裏バンクのセル                           │  [key 37/128KB=29%]
              │ └ 887B PCM → RF5C164 wave リング     ······ そして: CMD_SWAP を待つ ······─────────▶│swap│READY
              ├─────────────────────────────────────────────────────────────────────────────────────┤
 Word RAM     │  表 = frame N   (Mainが読/DMA)  ┃  裏 = frame N+1  (Subが書) ────┈┈ swap ┈┈▶ 役割反転  [各128KB×2]
              ├─────────────────────────────────────────────────────────────────────────────────────┤
 Main CPU(MD) │ 表を読む: DMA_STAGE へ整列 │ ▓STAGE→VRAM DMA▓ │ ▓DMA継続▓ │ CRAM+flip │→ CMD_SWAP     [DMA_STAGE]
              │ (Pass1: CPUコピー, active中も可)  │ (Pass2: VBLANK内のみ, ≤予算/field)          │  ▲原子的  [≤24/64KB=38%]
              ├─────────────────────────────────────────────────────────────────────────────────────┤
 PCM(RF5C164) │▶▶▶ wave-RAMリングをハードが連続再生(映像と独立) ▶▶▶                                   │  [wave-RAM ~別領域]
              │    Sub は書込ポインタをハード再生カーソルの SYNC_MIN..SYNC_MAX 先に保つ               │
              └─────────────────────────────────────────────────────────────────────────────────────┘
                    2 CPU が出会うのは破線の所だけ: CMD_SWAP / STAT_READY バリア

要点: バリアはフレームに 1 回で、VBLANK ごとではない。 バリアの間は 2 CPU が 逆バンクで完全に独立して走る。Main の DMA は VBLANK に収まるよう刻まれ、Sub の復号+ CD 吸上が残りの実時間を埋める。


各レーンの詳細

Sub CPU(SP)— 生成側(movieplay_sp.s

process_frame:                       1回の呼び出し = 1フレームを裏バンクへ復号
  ├─ pf_pump   : このフレームの5セクタが届くまで pump1 で CDC を吸上 (→ apply 34KB)
  ├─ seq check : control の frame_seq が期待index と一致するか
  │              不一致 → desync: そのフレームを破棄, 前コマ維持(0更新), 回数を計上
  ├─ fetch_control : 可変長 control ブロックを PRG→Word-RAM scratch へコピー
  └─ expand_frame  : bitmap を走査 → 立ったセル毎にネームテーブルentry(base+slot)を書く
                     coldセルはリング(貯水池420KB)から32Bパターンをそのスロットへpop。
                     その後887B PCM給餌
stream_loop:
  process_frame → CMD_SWAP を待つ間 (pump_poll)*
                → bchg #0,MEMMODE+1 (バンクswap) → swap_settle → STAT_READY
  • pump_poll が安全弁。 ループの至る所で—Main を待つ間でも—呼ばれ、CDC セクタが 滞留して落ちないようにする。PRG リングと apply リングに空きがあり かつ 実際にセクタが 準備できている(CDC_STAT)時だけ吸うので、バッファを溢れさせない。
  • RING(貯水池)がジッタ吸収の要。 H40フルでは 12160 枚(380KB)まで先読みを溜め、 CD 供給が一時的に足りないフレームでも枯渇せずパターンを供給する(それでも供給が続かなければ starved フレームになる)。
  • 連続読み中、Sub CPU は PRG-RAM へバースト書込みしない(固まる)。payload は CDC_TRN DMA で PRG に届き、Sub はパターン pop 時に PRG を 読む だけ。出力は Word RAM(書込安全)。

Main CPU(MD)— 消費側(movieplay_ip.s

build_frame:
  ├─ Pass1 (active中も可): 表バンクを読み、タイルDMAのラン表を作り、必要分を DMA_STAGE(24KB) へ整列
  │                        (遅いCPU作業をVBLANK外でやる。VDP DMAの源は必ずMain-RAM)
  ├─ Pass2 (VBLANK内のみ): DMA_STAGE の新規タイルパターンを Main-RAM → VRAM へ DMA。
  │        d7 = md_vbudget 語の予算/フィールド (H32=2800, H40=3400, dmabench実測)。
  │        1フィールドの予算を超えるフレームは分割し次VBLANKへ継続=DMAはactiveに入らない。
  └─ bf_flip (新しいVBLANKの頭で, その1フィールド内に原子的):
         render_dbg(HUDを裏へ) → 区間が変われば CRAM入替 → do_flip(VDPが新フレーム表示)。
         shadow[1120](セル→エントリ表)は Main-RAM 常駐で差分適用に使う。
play_loop:
  build_frame → swap_or_end: CMD_SWAP送出, STAT_READY待ち (または STAT_END)
  • タイル転送は DMA(VDP が自走し CPU を空ける)だが、VBLANK 内のみ・モード別語予算まで。 これが最重要のタイミング規則: VBLANK 超過が画面を壊す元凶。
  • DMA_STAGE(24KB)が Main-RAM 最大の消費。VDP の DMA 源は必ず Main-RAM なので、 Word-RAM から読んだ 1 フレーム分のタイル(H40フルで最大 ~768 枚)をここへ整列してから流す。
  • CRAM(パレット)入替と flip は 同一 VBLANK で行う。 別々だと一瞬「旧フレーム×新 パレット」が見える。

Word-RAM の ping-pong

        frame N              frame N+1             frame N+2
      ┌─────────┐          ┌─────────┐           ┌─────────┐
BankA │ Main 読 │  ─swap→  │ Sub 書  │   ─swap→   │ Main 読 │  …   各面 128KB
BankB │ Sub 書  │          │ Main 読 │            │ Sub 書  │      出力窓 9〜37KB使用
      └─────────┘          └─────────┘           └─────────┘
   MEMMODE bit0 が CMD_SWAP 毎にトグル; Sub が書き終えたバンクが次フレームで Main の読みバンクに。

音声のデカップリング

RF5C164 は自分のクロックで wave-RAM リングを再生する。Sub CPU はフレーム毎に 887 バイト (13.3 kHz mono 8bit PCM)をそのリングへ書き、書込ポインタをハード再生カーソルの少し先に 置き、映像ジッタでズレたら SYNC_MIN … SYNC_MAX の窓へ戻す。だから音声タイミングは PCM チップの再生カーソルに紐づき、映像のフレームバリアには 依存しない — 遅い映像フレームが あっても音声はクリックしない。


同期が保たれる理由(不変条件)

  • バリアはフレームに1回。 CMD_SWAP → STAT_READY が唯一のランデブー。間は 2 CPU が 逆バンクで自分のペースで走る。
  • 連続CD, 機会的に吸上。 読みは再開しない; pump_poll が全ての待ちで CDC を吸うので セクタを落とさない。CD 供給のジッタは PRG の RING(貯水池 380/420KB) が吸収する。
  • DMAはVBLANKに封じ, モード別に予算化。 大きいフレームは連続VBLANKへ分割、activeへ 入らない。DMA 源は Main-RAM の DMA_STAGE(≤24KB) に整列済み。
  • パレット+flipは同一VBLANK内で原子的。
  • 音声はPCM再生カーソルに乗る, 映像バリアと独立。

破綻する時(とガード)

症状 原因 プレイヤーのガード
フレームが裂ける/化ける DMAがVBLANKを超えactiveへ モード別語予算, フィールド跨ぎ分割
画面全体が崩壊 CDCセクタ落ち→ストリームdesync frame_seq チェック→フレーム破棄, 前コマ維持, 計上
runaway read/クラッシュ コンパクトブロックのcount破損 残数へクランプ, 0を終端扱い
音声クリック/途切れ wave リング underrun / lead 誤り 書込ptrを再生カーソルの SYNC_MIN…SYNC_MAX に保つ
一瞬フリーズ/コマ落ち CD供給が続かず RING 枯渇 RING(貯水池420KB)で吸収; それでも尽きれば starved フレーム
新規タイルに暗い破線 Word-RAM源DMAが先頭ワードを落とす dst[0]をCPU書き, 残りを src+2 → dst+2 でDMA

出典: boot/movieplay_sp.s(Sub)と boot/movieplay_ip.s(Main)。メモリ数値は H40フル (out/movieplay/MOVIE.DAT = 40×28 / 1120セル / pool 1400 / ring_peak 12160枚 = 380KB)の 実測ビルドに基づく。モードは H32(256×224)と H40(320×224)が同じパイプラインを共有し、 モード別値は VBLANK の DMA 語予算だけ。フレームレートはソース本来のレートなので、1 movie frame あたりの VBLANK 窓数 = 60 / fps。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment