Skip to content

Instantly share code, notes, and snippets.

@akiyan
Created July 26, 2026 23:59
Show Gist options
  • Select an option

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

Select an option

Save akiyan/6b9c4078e6c6e30718eb534a41f9d66c to your computer and use it in GitHub Desktop.
SEGA-CD FMV H40再生FAIL修正で得た知見(e139/p97)

SEGA-CD FMV H40再生FAIL修正で得た知見

対象: Tile Texture Reuse Codec(TTRC)
期間: 2026-07-26
最終main: encoder e139 / player p97 / eed9d704262820d2ef940b57a87487aaab193659
全編認定録画: 30fps profileはe139/p96、15fps profileはe139/p97

要約

H40の代表4プロファイルを、解像度、fps、cold cap、フィルタ、palette、ditherなどの目標画質設定を下げずに、DEBUG HUDでFAILしない状態へ戻した。

最終的な主原因は、単純なPrgBuf枯渇でも、Main-CPUのrun数でも、ADPCM decode単体の遅さでもなかった。旧encoderは、ある配送slotがその時点までにCD 1xで物理的に読める量を超えても、後続の軽いslotでpadを減らし、映画全体では配送量の帳尻を合わせられるscheduleを許していた。しかし、一度余分に費やした実時間は後から取り戻せない。その表示遅延でcontinuous CD producerがplayback consumerより先行し、有限のAPPLY control queueがguardへ達し、control sectorをpumpできなくなった後にMSF gap/reseekが発生していた。

修正は場当たり的にcold capや画質を下げるものではない。controlとPrg payloadの全prefixを、routing上限だけでなく、fpsから決まる累積CD 1x時間にも収め、有限PrgBufのexact scheduleでrate_lead_peak = 0を必須にした。これにより、全4プロファイルが元の品質設定とcold capのままPASSした。

対象と最終結果

Profile 認定版 出力 cadence cold cap 実測最大cold 最大coldのframe数 HUD最終結果
H40 Bad Apple e139/p96 320x224 30fps / N=2 180 180 1,330 PASS: S00 D00 R00 M01 J04
H40 Sonic Jam OP e139/p96 288x200 30fps / N=2 190 190 142 PASS: S00 D00 R00 M01 J00
H40 Machi OP e139/p97 320x152 15fps / N=4 480 480 8 PASS: S00 D00 R00 M03 J00
H40 Machi ED e139/p97 320x224 15fps / N=4 380 380 399 PASS: S00 D00 R00 M03 J02

各認定版で、full sim/pack、startup込みのlossless DEBUG recording、first-loop全frame HUD OCR、timeline、hudline、mixlineを確認した。under=0rate_lead peak/end=0/0であり、目標品質設定は変更していない。最終main p97では4 profileすべてのDEBUG discを再pack/buildし、player memory contractも再確認した。

1. 最初に守るべき不変条件を明確にする

今回許容したのはencoder内部の判断やsim結果の多少の変化であり、目標品質の低下ではない。

変更しなかったもの:

  • 解像度とfps
  • cold cap
  • crop、fit、resize filter
  • palette方式、segmented palette、dither
  • active tile数やcodecの品質目標
  • 各profileが意図するprefetch方式

この区別が重要だった。「sim結果を一切変えない」とすると正しいschedule修正まで妨げる。一方、「再生できればよい」としてcold capや解像度を下げると、原因を隠すだけになる。

2. cold capを下げ続けない

初期のBad Apple e135/p92では、coldを下げてもFAILは消えなかった。

cold cap HUD
190 FAIL: S=0x31, R=0x10
180 FAIL: S=0x1D, R=0x08
170 FAIL: S=0x0B, R=0x03

負荷を下げるほど症状は減ったが、production baselineの180でもFAILし、170でも残った。この時点でcap探索を止めた判断は正しかった。症状が連続的に減ることは、capが根本原因である証拠ではない。壊れた容量・期限モデルでも、入力負荷を下げれば発症頻度は減る。

一般化すると、既知の品質点で以前通っていたものが通らなくなった場合、まず回帰として扱う。品質ノブを動かすのは原因修正後の上限調査であり、回帰修正の代用ではない。

3. Main-CPUとSub-CPUの仕事を混同しない

途中でslot-locality、Hamiltonian path探索、run数削減も候補に上がったが、runを適用するのはMain-CPUである。今回疑っていたのはSub-CPU側のCD pump、audio、control/payload copy、handoffだったため、run数最適化は直接の対策ではない。

この切り分けは重要である。

  • Main-CPU: VRAM更新、run適用、表示側の処理
  • Sub-CPU: CD sector pump、stream分配、ADPCM decode/wave write、Word RAM handoff
  • Encoder: 将来の配送slot、control/payload量、有限bufferを事前に計画

見かけ上「CPU負荷」と呼べても、どのCPUがどのmemory domainで行う仕事かを確定しない限り、最適化案の評価はできない。

4. Sub-CPU最適化は有効でも、根本原因とは限らない

PCM_DEC_BUFのPRG-RAM移動

PCM_DEC_BUF 1,536 bytesをWord RAMから安全なSub PRG-RAMへ移し、同一streamでplayer-only A/Bを行った。

Sonicでは非デグレを確認し、J最大値が20 KiBから19 KiB、Mainから見たSub handoff待ちWの平均が約0.37 scanline減った。一方、ADPCM phase AはHUD分解能内で不変だった。Bad AppleのFAILは解消せず、Sは減らなかった。

したがって、この移動は共有Word RAM trafficを減らす正しい最適化だが、Bad AppleのFAIL原因を直接解いたものではない。

ADPCM hot tableのPRG-RAM移動

next-index table 2,848 bytesとnibble LUT 256 bytesをSub PRG-RAMへ移した。Sonicは全2,714 frameを完走し、HUD gateはPASSした。しかしAの改善は約0.123 ms単位のHUDでは測定できなかったため、5,696-byte signed-delta tableを動かすStage 2には進まなかった。

ここから得た原則:

  • 理論上access数が減っても、実測可能な効果がなければ次段を自動で積み重ねない。
  • 正しさ、非デグレ、効果量は別々に判定する。
  • PrgBufを減らす施策は、その価値が測定できてから行う。

関連issue:

5. Cは「悪さ」ではなく、CPUが待てた時間でもある

HUDのCはCD待ちを示すdiagnosticであり、画像破綻やdeadline missそのものではない。処理が早く終われば、次のdataを待つ時間が増えてCが大きくなることもある。

そのためCをgate判定から外し、代わりにmin / mean / median / maxを常に報告するようにした。Aも同じ4統計を出す。

今回の判定原則:

  • S/D/R/M/Jなど、再生破綻や期限違反へ直接つながる値をgateに使う。
  • Cは原因分析用に保存するが、単独でFAILにしない。
  • C増加を「改善」とも「悪化」とも即断せず、S/D/R、映像、cadence、J、A、Wと合わせて読む。

6. PrgBufがゼロに見えても、まず測る

whole-movie timelineのPrgBufは固定scaleなので、数KiB残っていても視覚上ゼロに見える。そこでplayerのlive signed balance QをHUDへ追加した。

Bad Apple p95/p96で確認した値:

  • player live Q最小: 64 patterns、2 KiB
  • packer model最小: 32 patterns、1 KiB
  • literal zero、logical underflow: なし

PrgBuf低水位とSには時間的な相関があったが、最初のSが発生した時点ではmodel PrgBufは約77.5 KiBまで回復していた。したがって、「PrgBufがゼロになったからSが起きた」という説明は否定された。

低水位を起点に調査したこと自体は有効だったが、低水位は直接原因ではなく、同じ重い区間で生じたcadence遅延を見つける入口だった。

7. HUDを増やして因果関係を分解する

追加した主なdiagnostic:

  • Q: live signed PrgBuf balance
  • G: SubがCDC pump opportunity外にいた最長interval
  • B: APPLY 30 KiB guardによりcontrol pumpを拒否したframe
  • K: MSF-gap recoveryに由来するSの累積数

Bad Apple診断では:

  • G min/mean/median/max = 253 / 275.896 / 276 / 284 tick
  • S付近にGの異常spikeなし
  • Bは12 frame
  • B at f2172 -> S at f2173を含め、B直後のS transitionを10件確認
  • K最終値とS最終値が一致
  • CDC_TRN retry exhaustionは0

これにより、「Subがpump path外で長時間止まった」という仮説は否定された。一方で、APPLY guardがcontrol sectorを拒否し、その後MSF gap/reseekへ進む因果関係が見えた。

HUDは最初から完璧な設計でなくてよい。ただし、仮説を一つずつ否定できるfieldを追加し、追加した値が再生制御を変えないことが重要である。

8. 決定的な原因は「平均は合うが、期限には間に合わないschedule」

SEGA-CDのCD 1xは75 sectors/sである。

  • 30fps、N=2では、1 game frameあたり約2.5 sector
  • 15fps、N=4では、1 game frameあたり約5 sector

旧plannerはrouting上の最大slot量を主なcapacityとして使い、あるslotがそのframeまでのCD 1x時間を超えても、後続slotのpadを減らして最終的なrate debtを0に戻せればfeasibleとしていた。

これはbyte accountingとしては合っていても、deadline accountingとして誤りである。例えば30fpsで5 sector読むslotは約2 frame分のCD時間を使う。次のslotを軽くして映画全体の平均を1xへ戻しても、先に失った表示時間は戻らない。

実際の連鎖:

  1. 重いslotが、その時点のCD 1x cadence allowanceを超える。
  2. fixed-cadence表示が遅れ、同じgame frameが余分なscanoutで表示される。
  3. CD pumpはcontinuousなので、producerがplayback control consumerより先行する。
  4. APPLY queueが30 KiB guardへ達する。
  5. 次のcontrol sectorをpumpできない。
  6. MSF gapを検出し、reseekがS/Rとして現れる。

最初のmodel低水位区間f1789..2142では、通常N=2より77 scanout余分に表示されていた。これはbufferの見た目ではなく、失われた実時間の直接証拠だった。

修正した構築条件

全prefixについて、control sector数とPrg payload sector数の合計を次の小さい方へ収める。

  1. routing tableが許す累積sector数
  2. fps/cadenceから、その時点までにCD 1xで物理的に読める累積sector数

さらに、有限PrgBufのexact scheduleについて:

  • under = 0
  • control/payload deadlineを満たす
  • ring peakが物理容量以下
  • rate_lead_peak = 0

を必須にした。以前のrate_lead_end = 0だけでは不十分である。

この修正はCPU作業量を推測するmodelではない。物理CDがdeadlineまでに配送できるsector数という、直接検証可能で保守しやすい制約である。CPU作業量を初期scheduleへ組み込む案は、正確なmodelが必要なので別課題として保留した。

修正commit:

9. APPLY容量を増やす前にscheduleを直す

診断途中では、APPLYを増やしてPrgBufを減らす案も合理的に見えた。実際、有限queueのheadroomを増やせば発症を遅らせられる。

しかし今回は、producer leadの起点が不正な配送scheduleだった。ここでAPPLYだけを増やすと、誤ったscheduleをより大きなbufferで吸収することになる。必要容量の計算漏れを後付けbufferで隠してはいけない。

判断順序:

  1. そのslot/prefixは物理deadlineまでに配送可能か。
  2. producer/consumerの正しい最大leadはいくつか。
  3. 正しいscheduleでも有限queueに必要なheadroomが足りないか。
  4. 3が証明された場合だけ、PrgBufとの再配分を検討する。

10. 15fpsと30fpsは同じ原則、異なる実体

fps別に必要なresource量は異なる。

cadence control block上限 PCM samples/frame PrgBuf delivery cap live jitter
30fps / N=2 372 B 736 398 KiB 20 KiB
15fps / N=4 740 B 1,472 378 KiB 40 KiB

15fpsは1 frameあたりのaudio/control量が大きく、low-rate CDC poll pathとrouting条件も異なる。30fps buildが通ったから15fpsも通るとは限らない。

一方、実装を「15fpsだけの例外」にしてはいけない。resource量、jitter、Supply、routing、buffer ceilingはfpsから導出し、15/24/30fpsを同じsource of truthで構築する。

11. Sub imageの4 KiBは絶対的な命令上限ではない

4 KiBはMega-CD BIOSのSub-CPU命令全体に固有の絶対上限ではない。現行boot imageがdata track先頭16 sector、32 KiB内でSP sourceを0x7000..0x7FFFへ置く構成のため、BIOS boot moduleとして4 KiBになっている。

最初にboot moduleへextensionを単純連結した方式は、Sonicのframe 499で確定的に停止した。また0x08600..0x097FFはstartup BIOS read後も保持される領域ではなかった。

採用した方式:

  1. boot時だけ必要な追加命令をHEADER preload内の既存paddingへ格納する。
  2. streaming開始前に安全なSub PRG-RAMへcopyする。
  3. size、hash、load address、exec address、entry point、memory overlapをbuild-time検証する。
  4. BIOS boot module自体は4,096 bytes以内に保つ。

p94では88-byteのADPCM table copy extensionをこの方式へ移した。p97では同じ基盤へboot-onlyのrouting/ring/APPLY初期化を追加し、15fps diagnostic buildを次のように収めた。

  • resident Sub image: 4,128 Bから4,080 B
  • preload extension: 176 Bから216 B
  • HEADER sector数: 増加なし
  • PrgBuf、APPLY、WordBuf、sim判断: 変更なし

修正commit:

12. hudlineだけでなくmixlineを必須にする

HUDだけでは「playerで何が起きたか」は見えるが、「encoderがそのframeに何を計画したか」が見えない。timelineだけでは、実再生がどこで遅れたかが見えない。

今回からfull DEBUG recordingごとに次を恒久化した。

  1. timelineを生成・公開
  2. hudlineを生成・公開
  3. 同じwhole-movie frame axisへ合わせたmixlineを生成・公開
  4. layout JSONとGist receiptを録画の隣へ保存

mixlineにより、model PrgBuf低水位、実Q、extra scanout、B、Sの順序を一枚で追えるようになった。原因調査では、単一meterの最大値より時間軸上の前後関係が重要である。

13. 最終証拠

Bad Apple H40 cold 180

Sonic Jam OP H40 cold 190

Machi OP H40 cold 480

  • 2,293 frames、cold max 480、該当8 frames
  • S00 / D00 / R00 / M03 / J00
  • C: 0 / 0.000 / 0 / 0
  • A: 182 / 207.196 / 188 / 242
  • G: 361 / 377.244 / 377 / 378
  • timeline
  • hudline
  • mixline

Machi ED H40 cold 380

  • 3,998 frames、cold max 380、該当399 frames
  • S00 / D00 / R00 / M03 / J02
  • C: 0 / 0.001 / 0 / 1
  • A: 182 / 206.065 / 188 / 242
  • G: 361 / 377.225 / 377 / 378
  • timeline
  • hudline
  • mixline

完了issue:

14. 今後の診断手順

同種の再生破綻では、次の順序を崩さない。

  1. Data layer
    実際のHEADER.DATBODY.DATを全frame走査し、writer/reader契約をbyte単位で確認する。

  2. Logic layer
    Python referenceで表示状態、pattern source、control/payload順、ring推移を照合する。

  3. Physical deadline
    最終平均だけでなく、全prefixがその時点のCD 1x時間、DMA時間、queue容量へ収まるか確認する。

  4. CPU ownership
    Main、Sub、encoderのどこが仕事を所有しているかを確定する。別CPUの最適化を混ぜない。

  5. Assembly and memory
    build binaryを逆アセンブルし、address、size、hash、bank ownership、overlapを確認する。

  6. HUD evidence
    仮説を否定できるfieldを追加し、full first-loop TSV、timeline、hudline、mixlineで前後関係を見る。

  7. Cross-cadence regression
    30fpsだけでなく15fps、必要なら24fpsも、各々の実resource量でbuild・full playbackする。

  8. Modelを疑う
    simが成立するのに実再生だけが繰り返し壊れる場合、hardwareを品質ノブでsimへ合わせるのではなく、sim/packerが物理deadlineを正しく表しているか問い直す。

結論

今回最も大きかった教訓は、「容量が足りる」と「期限までに届く」は別であることだった。映画全体でCD 1x平均に収まり、PrgBufがunderflowせず、最終rate debtが0でも、途中の1 slotが使った実時間は取り戻せない。

また、HUDで見えた相関をすぐ原因と決めず、Q、G、B、Kを追加して仮説を否定したことで、PrgBuf枯渇という見かけから、APPLY back-pressure、さらにその起点であるfixed-cadence配送制約の欠落まで辿れた。

品質を下げずに直すには、playerを速くすることだけでなく、encoderが最初からhardwareへ配送可能なscheduleだけを作ることが必要である。

Project: https://github.com/akiyan/segacd-fmv-ttrc

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