NYOットやろうぜ

~独田地獄斎が贈るPlayStation研究序説~

                     NNNNNN                           
                     NNNNNNNNNNNN                     
                     NNNNNNNNNNNNNNNN                 
                     NNNNNNNNNNNNNNNNNNN              
                     NNNNNNNNN   NNNNNNNNN            
                     NNNNNNNNN    NNNNNNNNN           
                     NNNNNNNNN    NNNNNNNNN           
                     NNNNNNNNN    NNNNNNNNNN          
                     NNNNNNNNN    NNNNNNNNNN          
                     NNNNNNNNN    NNNNNNNNNN          
                     NNNNNNNNN    NNNNNNNNNN          
                     NNNNNNNNN    NNNNNNNNN           
                     NNNNNNNNN    NNNNNNNN            
                   OONNNNNNNNN    NNNNNNN             
              YYYOOOONNNNNNNNN                        
          YYYYYYYYYYYNNNNNNNNN     OOOOOOOOOOOO       
      YYYYYYYYYYYYYYYNNNNNNNNNYYOOOOOOOOOOOOOOOOOOO   
 OOOOOOYYYYYYYYY     NNNNNNNNNYYYYYYYOOO     OOOOOOOOO
OOOOOOOOOOO        YYNNNNNNNNNYYYYY        OOOOOOOOOOO
OOOOOOOOO     OOOYYYYNNNNNNNNN        YYYYYYYYYOOOOOO 
   OOOOOOOOOOOOOOOOOONNNNNNNNN   YYYYYYYYYYYYYYY      
       OOOOOOOOOOOO  NNNNNNNNNOYYYYYYYYYYYY           
                     NNNNNNNNNOOOOOOYYY               
                          NNNNOOOO                    

はじめに

本ページは未来のPlayStationエミュレータ開発者への情報提供を目的とする。 ただし、ここでコーディングの講義をするつもりはない。 なぜならコーディングの観点からエミュレータ特有と呼べるものは何もないからだ。 動的コンパイルは珍しいかもしれないが、これはエミュレータの本質ではない。 換言すると、この程度のコーディングに躓くようではプログラマとして終っている。 基礎からやり直した方がいいんじゃないか。


解析環境

PlayStation本体

SCPH-7000 (SONY)

制御マシン

いわゆる自作DOS/V

通信デバイス

PS X-TERMINATOR (Future Console Design)

通信デバイスファームウェア

caetla 0.34 (K-Communications)

通信プログラム

caetools (K-Communications)
directio (K-Communications)

解析プログラム作成

GNU Binary Utilities 2.8.1 mipsel-generic-elf/ecoff (こぺる)
GNU C/C++ Compiler 2.7.2.3 mipsel-generic-elf/ecoff (こぺる)
Borland C++Compiler 5.5 (Borland)


参考文献

「ネットやろうぜ」ユーザガイドホームページ

http://www.scei.co.jp/Net/guide/user/

PADUA PLAYSTATION RESOURCE

http://psx.rules.org/psxrul2.shtml

ただし、これらには多くの誤記があるので鵜呑みにしないこと。 正規開発マニュアルから抜粋したと思われる部分にすら誤記がある (正規マニュアルにも誤記はある)。 開発者ならば最終的には自分でウラをとること。 幸い、PlayStationにはその現実的な手段があるのだから。

東芝のTX39のドキュメント

http://www.semicon.toshiba.co.jp/product/micro/index.html


アーキテクチャ

ブロック図

メモリマップ

表. メモリマップ
アドレス 内容
0x00000000~0x001FFFFF メインメモリ(命令キャッシュ有効)
0x00200000~0x003FFFFF メインメモリ ミラー(命令キャッシュ有効)
0x00400000~0x005FFFFF メインメモリ ミラー(命令キャッシュ有効)
0x00600000~0x007FFFFF メインメモリ ミラー(命令キャッシュ有効)
0x00800000~
0x1F000000~ PIO
0x1F0?????~
0x1F800000~0x1F8003FF スクラッチパッド
0x1F000400~
0x1F801000~ I/Oポート(いわゆるメモリマップドI/O)
0x1F80????~
0x1FC00000~0x1FC7FFFF OS ROM
0x1F800000~
0x80000000~0x801FFFFF メインメモリ ミラー(命令キャッシュ有効)
0x80200000~0x803FFFFF メインメモリ ミラー(命令キャッシュ有効)
0x80400000~0x805FFFFF メインメモリ ミラー(命令キャッシュ有効)
0x80600000~0x807FFFFF メインメモリ ミラー(命令キャッシュ有効)
0x80800000~
0x9FC00000~0x9FC7FFFF OS ROM ミラー
0x9FC80000~
0xA0000000~0xA01FFFFF メインメモリ ミラー(命令キャッシュ無効)
0xA0200000~0xA03FFFFF メインメモリ ミラー(命令キャッシュ無効)
0xA0400000~0xA05FFFFF メインメモリ ミラー(命令キャッシュ無効)
0xA0600000~0xA07FFFFF メインメモリ ミラー(命令キャッシュ無効)
0xA0800000~
0xBFC00000~0xBFC7FFFF OS ROM ミラー
0xBFC80000~
0xFFFE0130~0xFFFE0133 SwapCache?
0xFFFE0134~

レジスタマップ

表. レジスタマップ
アドレス レジスタ名(仮) 内容
0x1F801000 ? 拡張メモリ領域指定?
0x1F801004 ? I/Oポート終端指定?
0x1F801008 ? ?
0x1F80100C ? ?
0x1F801010 ? ?
0x1F801014 ? SPU DMAのウェイト?
0x1F801018 ? OTC DMAのウェイト?
0x1F80101C ? ?
0x1F801020 ? CD DMAのウェイト?



0x1F801040 COMA_DATA COMAデータ
0x1F801044 COMA_STAT COMAステータス
0x1F801048 COMA_MODE COMAモード
0x1F80104A COMA_CTRL COMAコントロール
0x1F80104E COMA_BAUD COMAボー
0x1F801050 SIO_DATA SIOデータ
0x1F801054 SIO_STAT SIOステータス
0x1F801058 SIO_MODE SIOモード
0x1F80105A SIO_CTRL SIOコントロール
0x1F80105E SIO_BAUD SIOボー
0x1F801060 RAM_SIZE 実効メモリサイズ



0x1F801070 I_STAT 割り込みステータス
0x1F801074 I_MASK 割り込みマスク



0x1F801080 D0_MADR DMACチャネル0(MDECin)メモリアドレス
0x1F801084 D0_BCR DMACチャネル0(MDECin)ブロックカウント
0x1F801088 D0_CHCR DMACチャネル0(MDECin)チャネルコントロール



0x1F801090 D1_MADR DMACチャネル1(MDECout)メモリアドレス
0x1F801094 D1_BCR DMACチャネル1(MDECout)ブロックカウント
0x1F801098 D1_CHCR DMACチャネル1(MDECout)チャネルコントロール



0x1F8010A0 D2_MADR DMACチャネル2(GPU)メモリアドレス
0x1F8010A4 D2_BCR DMACチャネル2(GPU)ブロックカウント
0x1F8010A8 D2_CHCR DMACチャネル2(GPU)チャネルコントロール



0x1F8010B0 D3_MADR DMACチャネル3(CD)メモリアドレス
0x1F8010B4 D3_BCR DMACチャネル3(CD)ブロックカウント
0x1F8010B8 D3_CHCR DMACチャネル3(CD)チャネルコントロール



0x1F8010C0 D4_MADR DMACチャネル4(SPU)メモリアドレス
0x1F8010C4 D4_BCR DMACチャネル4(SPU)ブロックカウント
0x1F8010C8 D4_CHCR DMACチャネル4(SPU)チャネルコントロール



0x1F8010D0 D5_MADR DMACチャネル5(PIO)メモリアドレス
0x1F8010D4 D5_BCR DMACチャネル5(PIO)ブロックカウント
0x1F8010D8 D5_CHCR DMACチャネル5(PIO)チャネルコントロール



0x1F8010E0 D6_MADR DMACチャネル6(OTC)メモリアドレス
0x1F8010E4 D6_BCR DMACチャネル6(OTC)ブロックカウント
0x1F8010E8 D6_CHCR DMACチャネル6(OTC)チャネルコントロール



0x1F8010F0 D_PCR DMAC優先度コントロール
0x1F8010F4 D_ICR DMAC割り込みコントロール



0x1F801100 T0_COUNT タイマ0(システム/ピクセル)カウント
0x1F801104 T0_MODE タイマ0(システム/ピクセル)モード
0x1F801108 T0_COMP タイマ0(システム/ピクセル)コンペア



0x1F801110 T1_COUNT タイマ1(システム/水平同期)カウント
0x1F801114 T1_MODE タイマ1(システム/水平同期)モード
0x1F801118 T1_COMP タイマ1(システム/水平同期)コンペア



0x1F801120 T2_COUNT タイマ2(システム/8分周)カウント
0x1F801124 T2_MODE タイマ2(システム/8分周)モード
0x1F801128 T2_COMP タイマ2(システム/8分周)コンペア



0x1F801800 CD_REG0 CDレジスタ0
0x1F801801 CD_REG1 CDレジスタ1
0x1F801802 CD_REG2 CDレジスタ2
0x1F801803 CD_REG3 CDレジスタ3



0x1F801810 GPU_DATA GPUデータ/コマンド
0x1F801814 GPU_CTRL GPUステータス/コントロール



0x1F801820 MDEC_DATA MDECデータ/コマンド
0x1F801824 MDEC_CTRL MDECステータス/コントロール



0x1F801C00 SPU_REG00 SPUレジスタ0
0x1F801C02 SPU_REG01 SPUレジスタ1
中略 中略 中略
0x1F801DFC SPU_REGFE SPUレジスタ254
0x1F801DFE SPU_REGFF SPUレジスタ255



0x1F802041 ? ディップスイッチ?




CPU(中央処理装置)

PlayStationのCPUはMIPSのR3000をベースとしたカスタムCPUである。 動作周波数は33868800Hzと言われ、エンディアンはリトルである。 若干の違いはあるが、それは青本も同じなので、 基本的な理解は東芝のTX39のドキュメントで十分である。

コード表

COP1,COP3,TLB系は存在しない。 サービスコール内のatofでCOP1命令が使われているがatof自体が呼ばれた試しがない。 おそらく試作機ではデバグ処理のために存在するが、結局オミットされたのだろう。

表. フィールドの名称
31 30 29 28 27 26 25 24 23 22 21 20 19 18 17 16 15 14 13 12 11 10 09 08 07 06 05 04 03 02 01 00
op rs rt rd sa funct

表. op
op 0x00 0x01 0x02 0x03 0x04 0x05 0x06 0x07
0x00 SPECIAL BCOND J JAL BEQ BNE BLEZ BGTZ
0x08 ADDI ADDIU SLTI SLTIU ANDI ORI XORI LUI
0x10 COP0
COP2




0x18







0x20 LB LH LWL LW LBU LHU LWR
0x28 SB SH SWL SW

SWR
0x30

LWC2




0x38

SWC2





表. funct(op=SPECIAL)
funct 0x00 0x01 0x02 0x03 0x04 0x05 0x06 0x07
0x00 SLL
SRL SRA SLLV
SRLV SRAV
0x08 JR JALR

SYSCALL BREAK

0x10 MFHI MTHI MFLO MTLO



0x18 MULT MULTU DIV DIVU



0x20 ADD ADDU SUB SUBU AND OR XOR NOR
0x28

SLT SLTU



0x30







0x38








表. rs(op=BCOND)
rs 0x00 0x01 0x02 0x03 0x04 0x05 0x06 0x07
0x00 BLTZ BGEZ





0x08







0x10 BLTZAL BGEZAL





0x18








表. rs(op=COP0)
rs 0x00 0x01 0x02 0x03 0x04 0x05 0x06 0x07
0x00 MFC0


MTC0


0x08







0x10 CP0
0x18

表. funct(op=COP0,rs=CP0)
funct 0x00 0x01 0x02 0x03 0x04 0x05 0x06 0x07
0x00







0x08







0x10 RFE






0x18







0x20







0x28







0x30







0x38








表. rs(op=COP2)
rs 0x00 0x01 0x02 0x03 0x04 0x05 0x06 0x07
0x00 MFC2
CFC2
MTC2
CTC2
0x08







0x10 CP2
0x18

表. funct(op=COP2,rs=CP2)
funct 0x00 0x01 0x02 0x03 0x04 0x05 0x06 0x07
0x00
RTPS



NCLIP
0x08



OP


0x10 DPCS INTPL MVMVA NCDS CDP
NCDT
0x18


NCCS CC
NCS
0x20 NCT






0x28 SQR DCPL DPCT

AVSZ3 AVSZ4
0x30 RTPT






0x38




GPF GPL NCCT
rs+b20をコードと一対一に対応させるのは、ただの慣習である。

I(命令)キャッシュ

ネットやろうぜの記述には重要部であるほど誤りがあるので、注意が必要である。 まずメモリ空間とキャッシュ(バースト転送)の有効/無効の関係を示した表において メモリ空間を取り違えている。また

Iキャッシュが有効な論理メモリ空間上の命令コードは通常の約5倍の速度でCPUに読み込まれます。 また、一旦読み込まれた命令コードはCPU内のIキャッシュメモリに保存されますので、 再実行時にはメインメモリへのアクセスなしに実行されます。

という文章もおかしい。 後半の文章があるから、前半はキャッシュミスした状態での実行速度と考えるしかない。 ならば理論的に24/9倍速以上にはならない。

スクラッチパッド Dキャッシュ

アドレス0x1F800000にマッピングされた、ストールの発生しないRAMを PlayStationの「方言」としてDキャッシュと呼んでいるが、 これは一般的な意味のDキャッシュではない。 一般的な意味のDキャッシュはPlayStationには存在しない。


GTE(Geometry Transformation Engine グラフィックデータ生成プロセッサ)

逆数テーブル

RTPS,RTPT命令において、投影変換係数

(int) (h * 65536 / Sz + 0.5)

が求められるが、実際は除算ではなく、逆数テーブルを用いた積算

(h * iSz + (1 << (N - 1))) >> N

が行われる。iSzは0x10000~0x1FFFFの範囲であり、小数部16ビットの浮動小数に相当する。 iSzは必ずしも

(65536 << N) / Sz

にはなっていないので、ブルートフォースアタックによる探索が必要になる。

Sz:32768~65535(N=16)について結果を示す。 Sz:0~32767については、Nを減らすだけなので割愛する。


エミュレート法

動的コンパイル法の一例

コード表から分かるようにR3000は単純な命令だけで構成されており、既にマイクロコードであるとも言える。 従って、一コードに対して一個のコンパイル済みデータを予め用意し、実行時に変数部分を書き換えるだけでも 実用に耐える動的コンパイルが可能である。例えばSRAV,SRLVでは、

/*
 *
 */
uchar __SRAV[] = {
    0x8B, 0x0D, '\\', '\\', '\\', '\\', /* 00 mov ecx,[GPR[rs]] 02 &GPR[rs] */
    0xA1, '\\', '\\', '\\', '\\',       /* 06 mov eax,[GPR[rt]] 07 &GPR[rt] */
    0xD3, 0xF8,                         /* 0B sar eax,cl */
    0xA3, '\\', '\\', '\\', '\\'        /* 0D mov [GPR[rd]],eax 0E &GPR[rd] */
};


/*
 *
 */
uchar __SRLV[] = {
    0x8B, 0x0D, '\\', '\\', '\\', '\\', /* 00 mov ecx,[GPR[rs]] 02 &GPR[rs] */
    0xA1, '\\', '\\', '\\', '\\',       /* 06 mov eax,[GPR[rt]] 07 &GPR[rt] */
    0xD3, 0xE8,                         /* 0B shr eax,cl */
    0xA3, '\\', '\\', '\\', '\\'        /* 0D mov [GPR[rd]],eax 0E &GPR[rd] */
};

の様なデータを予め用意し、実行時に

/*
 *
 */
#define _rs_    ((opcode >> 21) & 31)
#define _rt_    ((opcode >> 16) & 31)
#define _rd_    ((opcode >> 11) & 31)


/*
 *
 */
#define CPU_SET_SRAV()                                    \
    {                                                     \
        *(ulong *) (&__SRAV[0x02]) = (ulong) & GPR[_rs_]; \
        *(ulong *) (&__SRAV[0x07]) = (ulong) & GPR[_rt_]; \
        *(ulong *) (&__SRAV[0x0E]) = (ulong) & GPR[_rd_]; \
        CPU_OPCODE                 = __SRAV;              \
        CPU_OPSIZE                 = sizeof(__SRAV);      \
    }


/*
 *
 */
#define CPU_SET_SRLV()                                    \
    {                                                     \
        *(ulong *) (&__SRLV[0x02]) = (ulong) & GPR[_rs_]; \
        *(ulong *) (&__SRLV[0x07]) = (ulong) & GPR[_rt_]; \
        *(ulong *) (&__SRLV[0x0E]) = (ulong) & GPR[_rd_]; \
        CPU_OPCODE                 = __SRLV;              \
        CPU_OPSIZE                 = sizeof(__SRLV);      \
    }

の様に変数部分を書き換える。後はこれを連結コピーして関数を作成し、出来上がった関数を実行する。

注意事項

巷で誤解があるようなので、注意を喚起すると、 実行毎に新たな関数を作成していたのでは、インタプリタよりも遅くなる。 出来上がった関数を管理・再利用することこそが、動的コンパイルの肝である。 動的コンパイルとは可変長データを対象としたキャッシュ機構を構築することに等しい。

コンパイル済データの作成法の一例

アセンブラで直接記述する以外に、以下の様にCで記述し、 ファイル出力、逆アセンブルするのも一つの手である。

/*
 *
 */
#define _rs_    1               /* 変数部分なので適当 */
#define _rt_    2               /* 変数部分なので適当 */
#define _rd_    3               /* 変数部分なので適当 */


/*
 *
 */
long GPR[32];


/*
 *
 */
void __SRAV(void)
{
    GPR[_rd_] = GPR[_rt_] >> GPR[_rs_];
}


/*
 *
 */
void ____SRAV(void)
{
}


/*
 *
 */
void __SRLV(void)
{
    GPR[_rd_] = ((ulong) GPR[_rt_]) >> GPR[_rs_];
}


/*
 *
 */
void ____SRLV(void)
{
}


/*
 *
 */
void SaveToFile(char *lpFileName, void *lpStart, void *lpEnd)
{
    FILE *fp;

    if (NULL == (fp = fopen(lpFileName, "wb"))) {
        return;
    }
    fwrite(lpStart, (ulong) lpEnd - (ulong) lpStart, 1, fp);
    fclose(fp);
}


/*
 *
 */
int main(void)
{
    SaveToFile("SRAV.X86", __SRAV, ____SRAV);
    SaveToFile("SRLV.X86", __SRLV, ____SRLV);
    return(0);
}

INTC(割り込みコントローラ)

I_STATレジスタ

ロードした場合

15 14 13 12 11 10 09 08 07 06 05 04 03 02 01 00

S
S
P
U
S
S
I
O
S
C
O
M
A
S
T
M
R
2
S
T
M
R
1
S
T
M
R
0
S
D
M
A
C
S
C
D
S
G
P
U
S
V
S
Y
N
C

フィールド 内容
SVSYNC VSYNCの割り込みステータス
0:VSYNCから割り込み要求がない
1:VSYNCから割り込み要求があった
SGPU~SSPU SVSYNCと同様

ストアした場合

15 14 13 12 11 10 09 08 07 06 05 04 03 02 01 00

C
S
P
U
C
S
I
O
C
C
O
M
A
C
T
M
R
2
C
T
M
R
1
C
T
M
R
0
C
D
M
A
C
C
C
D
C
G
P
U
C
V
S
Y
N
C

フィールド 内容
CVSYNC VSYNCの割り込みクリア
0→SVSYNCを0にする
CGPU~CSPU CVSYNCと同様

I_MASKレジスタ

ロードした場合

ストアした値が返る

ストアした場合

15 14 13 12 11 10 09 08 07 06 05 04 03 02 01 00

M
S
P
U
M
S
I
O
M
C
O
M
A
M
T
M
R
2
M
T
M
R
1
M
T
M
R
0
M
D
M
A
C
M
C
D
M
G
P
U
M
V
S
Y
N
C

フィールド 内容
MVSYNC VSYNCの割り込みマスク
MVSYNCとSVSYNCがともに1である限り、INTCはCPUに割り込みを要求し続ける。
MGPU~MSPU MVSYNCと同様

DMAC(DMAコントローラ)

準備中


タイマ

タイマは設定によりシステムクロックではなくピクセル表示や水平同期によりカウントアップさせることができる。

表. カウントタイミング
タイプ 画面幅 [A] [B]システムクロック/水平同期 システムクロック/ピクセル表示 システムクロック/垂直同期
非インターレース インターレース
偶数フィールド 奇数フィールド
NTSC 256 566107.5005 [A]/263 [A]/89683=[B]/341 [B]*263 [B]*263 [B]*262
320 [A]/112038=[B]/426
368 [A]/128081=[B]/487
512 [A]/179366=[B]/682
640 [A]/224339=[B]/853
PAL 256 674399.5367 [A]/314 [A]/106760=[B]/340 [B]*314 [B]*313 [B]*312
320 [A]/133764=[B]/426
368 [A]/152604=[B]/486
512 [A]/213834=[B]/681
640 [A]/267214=[B]/851

メインメモリ(Main Memory or RAM 主記憶装置)

準備中


OS ROM(or BIOS 基本操作システム記憶装置)

準備中


MDEC(Motion Decoder データ伸長エンジン)

準備中


PIO(拡張パラレルポート)

準備中


SIO(or COMB 拡張シリアルポート)

準備中


GPU(グラフィック描画処理プロセッサ)

GPU_DATAレジスタ

ロードした場合

基本的には内部のリザルトレジスタの値が返るが、 StoreImageコマンド実行中の場合は、その結果が返る。

リザルトレジスタには、 GPU制御系コマンドの結果や StoreImageコマンドの最初の結果が格納される。 途中の結果は格納されない。

ストアした場合

データは内部の多目的FIFOに格納され、 必要な数のデータが貯まると、 GPU描画系コマンドが実行される。

GPU_CTRLレジスタ

ロードした場合

31 30 29 28 27 26 25 24 23 22 21 20 19 18 17 16 15 14 13 12 11 10 09 08 07 06 05 04 03 02 01 00
o
d
e
t
m
o
d
e
i
s
e
m
p
t
y
d
a
t
a
r
d
y
i
s
i
d
l
e
d
m
a
r
d
y
r
e
s
e
r
v
e
d
i
s
s
t
o
p
i
s
i
n
t
e
r
i
s
r
g
b
2
4
i
s
p
a
l
v
r
e
s
h
r
e
s
1
h
r
e
s
2
r
e
s
e
r
v
e
d
u
n
k
n
o
w
n
r
e
s
e
r
v
e
d
p
b
c
p
b
w
d
f
e
d
t
d
t
p
a
b
r
t
y
t
x

フィールド 内容
ode ビデオ出力の走査フィールドがフレームバッファ上の奇数ラインかどうか。 よってインターレースでは数百水平同期毎、非インターレースでは1水平同期毎(指定ブランク区間は0のまま)に変化する。
tmode 転送モード
isempty FIFO(パケットバッファ)が空かどうか
datardy データレディ
isidle アイドル状態である
dmardy DMAレディ
isstop ビデオ出力を停止している
isinter インターレースで出力している(垂直解像度を倍密度にする場合は必須)
isrgb24 rgb24ビットモードである
ispal PALモードである
vres 垂直解像度
hres1 水平解像度1
hres2 水平解像度2(水平解像度1は無効になる)
unknown 不明(オン/オフ操作は可能)
pbc 優先度ビットコンペアを行う
pbw 優先度ビットを書き込む
dfe インターレースであっても、 非インターレースと同様、 走査フィールドに関係なく描画する
dtd ディザ処理を行う
tp テクスチャのビットモード
abr 半透明率
ty テクスチャページのY座標
tx テクスチャページのX座標

ストアした場合

GPU制御系コマンドが実行される。


SPU(サウンド処理プロセッサ)

ブロック図

ボイス

全24チャネルの内蔵音源のこと。各チャネルは独立に 「波形データ(ADPCMまたは共通ノイズ)×エンベロープ×ボイス音量(左右独立)」 の三者の掛け合わせを出力する。

エンベロープ

波形データにかけるバイアスのこと。

表. ADSRレート
レート[*] 実効レート
分母 分子(増加) 分子(減少)
0~47 1 (7 - (RATE & 3)) << (11 - (RATE >> 2)) (-8 + (RATE & 3)) << (11 - (RATE >> 2))
48~ 1 << ((RATE >> 2) - 11) 7 - (RATE & 3) -8 + (RATE & 3)
[*]ディケイレート、リリースレートは1/4で指定されるので4倍して考える

表. ADSRモード
モード 内容
線形増加 分母サンプリング時間毎にエンベロープレベルに分子(増加)を加算
線形減少 分母サンプリング時間毎にエンベロープレベルに分子(減少)を加算
指数増加 基本的には線形増加と同じだがエンベロープレベルが0x6000以上になるとレートが8増加
指数減少 分母サンプリング時間毎にエンベロープレベルに ((分子(減少) * レベル) >> 15)を加算

表. ADSRステータス
ステータス 開始条件 終了条件 飽和下限 飽和上限
初期化 キーオンする アタックレートに依存した初期化時間[*]が経過する 0 0
アタック 初期化が終了する エンベロープレベルが32767以上になる
32767
ディケイ アタックが終了する エンベロープレベルがサステインレベル未満になる 0
サステイン ディケイが終了する キーオフする 0 32767
リリース キーオフする エンベロープレベルが0以下になる -1
停止 ・リリースが終了する
・ADPCMデコーダが強制停止する
キーオンする 0 0
[*]アタックレート0~47では5サンプリング時間で、 それ以上は徐々に短くなる。 これはADPCMデコードのフィルタ処理に起因する、 残響が消えるための時間である。 レートが大きくなると、アタックの方で0になる時間が増えるから、 その分短くて良いのである。

ADPCM波形

ADPCMデコーダが圧縮データをデコードして得たサンプリング波形のこと。 重要なのはADPCMデコーダが(リバーブプロセスも)常時稼動していることだろう。 停止したように見えて、それはエンベロープを強制停止させた結果にすぎない。 つまり、常に割り込みが発生する可能性がある。 しかしながら、常時稼動のエミュレーションは、高負荷のワリに見返りが小さい。 モラトリアム期間を設けるのが現実解であろう。

表. ループフラグ
フィールド 内容
0x04 カレントブロックの先頭をループアドレスに設定する。
0x02 エンベロープを強制停止しない。
0x01 カレントブロックを演奏後、ループアドレスにジャンプする。 0x02が設定されていないと、エンベロープを強制停止する。

組み合わせとしては以下の5通りとなる(ループアドレスが強制的に設定された場合は、この限りではない)。

表. ループフラグの組み合わせ
ループフラグ 内容
0x06,0x04 カレントブロックの先頭をループアドレスに設定する。
0x03 カレントブロック演奏後、ループアドレスにジャンプする。
0x07 上2つの組み合わせにより、カレントブロックを延々と演奏する。
0x01 ループアドレスにジャンプするが、エンベロープを停止するので無音になる。
0x05 カレントブロックを延々と演奏するが、エンベロープを停止するので無音になる。

ADPCM波形のフィルタ処理

図中、赤丸で示した点が波形データで、黒の破線が実際に出力される値である。 黒の実線は波形データと比較するために平行移動したものである。 赤の破線は参考として波形データに対しスプライン曲線を引いたものである。 重要なのは中間点以外も全く同じ処理がされているということであり、 出力は基本的に波形データを通過しないということである。 多くの人が想像するであろう、スプライン曲線とは、かなり違った形になる。

ちなみにg(x)=g(INT(x * 256)/256)であり、 4096個のテーブルデータとして実装していると思われる。 16ビット固定小数で実装したところ(1/32768)ほど値が異なることがあるので、 おそらくシフト値を有する、実質浮動小数になっていると推測される。

共通ノイズ波形

ADPCM波形の替わりに用いるランダム波形のこと。 チャネル毎にどちらかを選択できるが、 チャネル毎に異なるノイズ波形を得るといったことは出来ない。

アルゴリズム

/*
 * レベル変化時の計算式
 */
char Addition[64] = {
    1, 0, 0, 1, 0, 1, 1, 0,
    1, 0, 0, 1, 0, 1, 1, 0,
    1, 0, 0, 1, 0, 1, 1, 0,
    1, 0, 0, 1, 0, 1, 1, 0,
    0, 1, 1, 0, 1, 0, 0, 1,
    0, 1, 1, 0, 1, 0, 0, 1,
    0, 1, 1, 0, 1, 0, 0, 1,
    0, 1, 1, 0, 1, 0, 0, 1
};

NoiseLevel = (short) (NoiseLevel + NoiseLevel + Addition[(NoiseLevel >> 10) & 63]);


/*
 * レベル変化の周期
 */
Freq = 0x8000 >> (NoiseClock >> 2);


/*
 * 周期が半減する頻度
 */
Half = ((NoiseClock & 3) * 2) / (4 + (NoiseClock & 3));

リバーブ

アルゴリズム

/*
 * PlayStation Reverberation Algorithm (C)Dr.Hell, 2005
 * 厳密には、左右の処理のタイミングは1サンプリング時間ずれており、
 * 各々は2サンプリング時間毎に実行される
 */
mAPF1    = *((ushort *) 0x1F801DC0) * 4;
mAPF2    = *((ushort *) 0x1F801DC2) * 4;
gIIR     = *((short *) 0x1F801DC4) / 32768.0;
gCOMB1   = *((short *) 0x1F801DC6) / 32768.0;
gCOMB2   = *((short *) 0x1F801DC8) / 32768.0;
gCOMB3   = *((short *) 0x1F801DCA) / 32768.0;
gCOMB4   = *((short *) 0x1F801DCC) / 32768.0;
gWALL    = *((short *) 0x1F801DCE) / 32768.0;
gAPF1    = *((short *) 0x1F801DD0) / 32768.0;
gAPF2    = *((short *) 0x1F801DD2) / 32768.0;
z0_Lsame = *((ushort *) 0x1F801DD4) * 4;
z0_Rsame = *((ushort *) 0x1F801DD6) * 4;
m1_Lcomb = *((ushort *) 0x1F801DD8) * 4;
m1_Rcomb = *((ushort *) 0x1F801DDA) * 4;
m2_Lcomb = *((ushort *) 0x1F801DDC) * 4;
m2_Rcomb = *((ushort *) 0x1F801DDE) * 4;
zm_Lsame = *((ushort *) 0x1F801DE0) * 4;
zm_Rsame = *((ushort *) 0x1F801DE2) * 4;
z0_Ldiff = *((ushort *) 0x1F801DE4) * 4;
z0_Rdiff = *((ushort *) 0x1F801DE6) * 4;
m3_Lcomb = *((ushort *) 0x1F801DE8) * 4;
m3_Rcomb = *((ushort *) 0x1F801DEA) * 4;
m4_Lcomb = *((ushort *) 0x1F801DEC) * 4;
m4_Rcomb = *((ushort *) 0x1F801DEE) * 4;
zm_Ldiff = *((ushort *) 0x1F801DF0) * 4;
zm_Rdiff = *((ushort *) 0x1F801DF2) * 4;
z0_Lapf1 = *((ushort *) 0x1F801DF4) * 4;
z0_Rapf1 = *((ushort *) 0x1F801DF6) * 4;
z0_Lapf2 = *((ushort *) 0x1F801DF8) * 4;
z0_Rapf2 = *((ushort *) 0x1F801DFA) * 4;
gLIN     = *((short *) 0x1F801DFC) / 32768.0;
gRIN     = *((short *) 0x1F801DFE) / 32768.0;
z1_Lsame = z0_Lsame - 1;
z1_Rsame = z0_Rsame - 1;
z1_Ldiff = z0_Ldiff - 1;
z1_Rdiff = z0_Rdiff - 1;
zm_Lapf1 = z0_Lapf1 - mAPF1;
zm_Rapf1 = z0_Rapf1 - mAPF1;
zm_Lapf2 = z0_Lapf2 - mAPF2;
zm_Rapf2 = z0_Rapf2 - mAPF2;
for (;;) {
    /*
     * LoadFromLowPassFilterは35もしくは39タップのFIRフィルタ
     * 最外周の係数が0でも結果は同じなので、35か39かは特定できず
     */
    L_in = gLIN * LoadFromLowPassFilterL();
    R_in = gRIN * LoadFromLowPassFilterR();


    /*
     * Left -> Wall -> Left Reflection
     */
    L_temp = ReadReverbWork(zm_Lsame);
    R_temp = ReadReverbWork(zm_Rsame);
    L_same = L_in + gWALL * L_temp;
    R_same = R_in + gWALL * R_temp;
    L_temp = ReadReverbWork(z1_Lsame);
    R_temp = ReadReverbWork(z1_Rsame);
    L_same = L_temp + gIIR * (L_same - L_temp);
    R_same = R_temp + gIIR * (R_same - R_temp);


    /*
     * Left -> Wall -> Right Reflection
     */
    L_temp = ReadReverbWork(zm_Rdiff);
    R_temp = ReadReverbWork(zm_Ldiff);
    L_diff = L_in + gWALL * L_temp;
    R_diff = R_in + gWALL * R_temp;
    L_temp = ReadReverbWork(z1_Ldiff);
    R_temp = ReadReverbWork(z1_Rdiff);
    L_diff = L_temp + gIIR * (L_diff - L_temp);
    R_diff = R_temp + gIIR * (R_diff - R_temp);


    /*
     * Early Echo(Comb Filter)
     */
    L_in = gCOMB1 * ReadReverbWork(m1_Lcomb) + gCOMB2 *ReadReverbWork(m2_Lcomb) + gCOMB3 *ReadReverbWork(m3_Lcomb) + gCOMB4 *ReadReverbWork(m4_Lcomb);
    R_in = gCOMB1 * ReadReverbWork(m1_Rcomb) + gCOMB2 *ReadReverbWork(m2_Rcomb) + gCOMB3 *ReadReverbWork(m3_Rcomb) + gCOMB4 *ReadReverbWork(m4_Rcomb);


    /*
     * Late Reverb(Two All Pass Filters)
     */
    L_temp = ReadReverbWork(zm_Lapf1);
    R_temp = ReadReverbWork(zm_Rapf1);
    L_apf1 = L_in - gAPF1 * L_temp;
    R_apf1 = R_in - gAPF1 * R_temp;
    L_in   = L_temp + gAPF1 * L_apf1;
    R_in   = R_temp + gAPF1 * R_apf1;
    L_temp = ReadReverbWork(zm_Lapf2);
    R_temp = ReadReverbWork(zm_Rapf2);
    L_apf2 = L_in - gAPF2 * L_temp;
    R_apf2 = R_in - gAPF2 * R_temp;
    L_in   = L_temp + gAPF2 * L_apf2;
    R_in   = R_temp + gAPF2 * R_apf2;


    /*
     * Output
     */
    SetOutputL(L_in);
    SetOutputR(R_in);


    /*
     * Write Buffer
     */
    WriteReverbWork(z0_Lsame, L_same);
    WriteReverbWork(z0_Rsame, R_same);
    WriteReverbWork(z0_Ldiff, L_diff);
    WriteReverbWork(z0_Rdiff, R_diff);
    WriteReverbWork(z0_Lapf1, L_apf1);
    WriteReverbWork(z0_Rapf1, R_apf1);
    WriteReverbWork(z0_Lapf2, L_apf2);
    WriteReverbWork(z0_Rapf2, R_apf2);


    /*
     * Update Circular Buffer
     */
    UpdateReverbWork();
}

CD(コンパクトディスク読込み装置)

CDコマンド

表. CDコマンドの引数
番号 コマンド名 1 2 3
0x00 Sync 0


0x01 Nop 0


0x02 Setloc 3 AMin ASec AFrac
0x03 Play 0/1 Track

0x04 Forward 0


0x05 Backward 0


0x06 ReadN 0


0x07 Standby 0


0x08 Stop 0


0x09 Pause 0


0x0A Reset 0


0x0B Mute 0


0x0C Demute 0


0x0D Setfilter 2 File Chan
0x0E Setmode 1 Mode

0x0F Getparam 0


0x10 GetlocL 0


0x11 GetlocP 0


0x12 ReadT 1 0x01

0x13 GetTN 0


0x14 GetTD 1 Track

0x15 SeekL 0


0x16 SeekP 0


0x17 Setclock



0x18 Getclock



0x19 Test 1 Num

0x1A Id 0


0x1B ReadS 0


0x1C Init 0


0x1D




0x1E ReadTOC 0


0x1F





表. CDコマンドの結果(一次)
番号 コマンド名 所要
クロック
σ% 割り
込み
1 2 3 4 5 6 7 8
0x00 Sync

0x03 1 Stat






0x01 Nop 30547 23 0x03 1 Stat






0x02 Setloc 31557 7 0x03 1 Stat






0x03 Play 51444
0x03 1 Stat






0x04 Forward 193250 12 0x03 1 Stat






0x05 Backward 125119 0 0x03 1 Stat






0x06 ReadN 75701 7 0x03 1 Stat






0x07 Standby 74753 5 0x03 1 Stat






0x08 Stop 43503 13 0x03 1 Stat






0x09 Pause 27648 24 0x03 1 Stat






0x0A Reset 80295 12 0x03 1 Stat






0x0B Mute 36114 41 0x03 1 Stat






0x0C Demute 32768 10 0x03 1 Stat






0x0D Setfilter 39105 48 0x03 1 Stat






0x0E Setmode 41082 52 0x03 1 Stat






0x0F Getparam 40163 25 0x03 5 Stat Mode 0x00 File Chan


0x10 GetlocL 43335 20 0x03 8 セクタの13番目~20番目のデータ(つまりRead,SeekLが必要)
0x11 GetlocP 37937 17 0x03 8 Track Index Min Sec Frac AMin ASec AFrac
0x12 ReadT 98762 13 0x03 1 Stat






0x13 GetTN 40833
0x03 3 Stat Begin End




0x14 GetTD 39091
0x03 3 Stat AMin ASec




0x15 SeekL 41771 25 0x03 1 Stat






0x16 SeekP 62041 40 0x03 1 Stat






0x17 Setclock











0x18 Getclock











0x19 Test(0x20) 40864 14 0x03 4 Year Mon Day Ver



0x1A Id 30980 22 0x03 1 Stat






0x1B ReadS 81925 2 0x03 1 Stat






0x1C Init 25847 29 0x03 1 Stat






0x1D












0x1E ReadTOC 73292 10 0x03 1 Stat






0x1F













表. CDコマンドの結果(二次以降)
番号 コマンド名 所要
クロック
σ% 割り
込み
1 2 3 4 5 6 7 8
0x00 Sync











0x01 Nop











0x02 Setloc











0x03 Play

0x01 8 Stat Track Index A/Min A/Sec A/Frac LvHi LvLo
0x04 Forward











0x05 Backward











0x06 ReadN

0x01 1 Stat






0x07 Standby 51429486 55 0x02 1 Stat






0x08 Stop 12767194 2 0x02 1 Stat






0x09 Pause 3665794 0 0x02 1 Stat






0x0A Reset 3656479 6 0x02 1 Stat






0x0B Mute











0x0C Demute











0x0D Setfilter











0x0E Setmode











0x0F Getparam











0x10 GetlocL











0x11 GetlocP











0x12 ReadT 57701665 0 0x02 1 Stat






0x13 GetTN











0x14 GetTD











0x15 SeekL

0x02 1 Stat






0x16 SeekP

0x02 1 Stat






0x17 Setclock











0x18 Getclock











0x19 Test(0x20)











0x1A Id 19658 1 0x02 8 0x02 0x00 0x20 0x00 'S' 'C' 'E' 'I'
0x1B ReadS

0x01 1 Stat






0x1C Init











0x1D












0x1E ReadTOC 31330364 14 0x02 1 Stat






0x1F













表. コマンドの補足事項
コマンド名 補足事項
Play Setmodeで0x04を立てると、AFrac(BCD)の下位4ビットが0の時にレポートを返す。上位が奇数の時は位置情報がトラック相対になるが、その際フラグとしてSec(BCD)の0x80が立つ。
Reset 位置もホームポジションに戻る。
ReadT そろそろ公表してもよかろう。こちらが本当の意味でのTOC更新コマンドである。引数は0x01以外上手くいかない。
ReadTOC そろそろ公表してもよかろう。TOC更新はディスク入れ替えや電源ON時にシステムが自動でやるか、上記のReadTで行う。 こいつは例のプロテクト情報を読みにいくだけでTOCの更新は行わない。実機能としてはReadMAGICとでも呼ぶべきもの。これがどういうことを意味するか...
GetTD 引数を0x00にすると全トラックの合計になる。
Init 承認後も暫く時間をあける必要がある。

通信デバイス(Comms Device or COMA)

準備中


独自解析プログラム群

準備中


エミゅってしまうま

実践結果としてのエミュレータ「エミゅってしまうま(略称XEBRA/ARBEX)」を公開する。


inserted by FC2 system