さて、前回 MZ-80K2 のエミュはほぼ動いたのですが、めちゃめちゃ遅いという問題がは発生。
画面描画にメスを入れる
★ カラーを 32bitから16bit にして描画コストを半分にする。
エミュレートする対象がMZ-80Kなので、カラーが 32bit というのはかなりオーバースペックであることに気づいたので、16bitモードに変更。これに伴って、 ARGB (LongWord) で記述していたコードを RGB565 (Word) にすべて書き換え。
//==============================================================================
// 指定した X,Y (40x25) にキャラを表示 (RGB565形式バージョン)
//==============================================================================
procedure DrawCharToSurface(X, Y: Integer; CharCode: Byte);
var
Line, Bit: Integer;
Data: Byte;
PixelAddr: PWord;
BaseAddr: PtrUInt;
begin
// 1文字目の先頭アドレス(1ピクセル=2バイト)
BaseAddr := FBInfo.PixelAddr + Y * FBInfo.Pitch + X * 2;
for Line := 0 to 7 do
begin
Data := FontRom[CharCode * 8 + Line];
for Bit := 0 to 7 do
begin
PixelAddr := PWord(BaseAddr + Line * FBInfo.Pitch + Bit * 2);
if (Data and (128 shr Bit)) <> 0 then
PixelAddr^ := FG // FG は Word 型(RGB565値)
else
PixelAddr^ := BG; // BG も Word 型
end;
end;
end;
こんな感じで、すべて Word型に。 RGB565変換は、下記のようなfunctionを作成。
//==============================================================================
// RGB565 形式に変換する関数
//==============================================================================
function RGB565(R, G, B: Byte): Word;
begin
Result := ((R shr 3) shl 11) or ((G shr 2) shl 5) or (B shr 3);
end;
これは、ARDUINO や ESP32 などのSBCで、外部LEDに出力するときに RGB565仕様のLEDモニタが多いので役立ちそう。あまり考えずに RGB を 255,255,255 で指定して、自動変換。
★ フレーム描画を間引き
禁断かどうかわかりませんが、フレーム描画を4回に1回だけ描画して、フレーム描画をすっとばす。
//==============================================================================
// Draw Screen Thread Execeute
//==============================================================================
procedure TDrawScreenThread.Execute;
var
FStartVSync, FEndVSync, FVSyncTime: QWord;
FrameCount: Integer;
begin
FrameCount := 0;
while not Terminated do
begin
FStartVSync := GetTickCount64;
Inc(FrameCount);
if hw80.v_gate then
begin
if (FrameCount mod 4 = 0) then
update_scrn;
end
else
begin
// 描画間引きに関係なく、背景は毎回クリア
ClearGameAreaFast16(BG);
end;
FEndVSync := GetTickCount64;
FVSyncTime := FEndVSync - FStartVSync;
if SYNCTIME > FVSyncTime then
Sleep(SYNCTIME - FVSyncTime)
else
Sleep(1);
end;
end;
update_scrn の呼び出し頻度を変えている。
★ Thread の優先順位を設定
画面描画をLOWEST にして、 Z80のThreadをHIGHEST に設定
結果は?
少し早くなったが、まだまだぜんぜん。なかなかなやましい。
画面描画処理をストップしてZ80だけ動かして計測してみた
そもそも Z80 部分は十分なスピードで動いているのか? という疑問があったので、計測用のソースに書き換えてテスト
//==============================================================================
// Z80Thread Execute
//==============================================================================
procedure TZ80Thread.Execute;
var
i: Integer;
T1, T2: QWord;
Hz: Double;
begin
// 描画がない状態で、Z80処理速度を測定
T1 := GetTickCount64;
for i := 1 to 1000000 do
z80.Z80_Execute;
T2 := GetTickCount64;
// ミリ秒 → 秒へ変換して Hz 計算
if (T2 > T1) then
begin
Hz := 1000000 / ((T2 - T1) / 1000);
ConsoleWindowWriteLn(Console1, 'Z80: ' + FloatToStrF(Hz / 1000000, ffFixed, 5, 3) + ' MHz');
end
else
ConsoleWindowWriteLn(Console1, '計測エラー');
// スレッド終了
Terminate;
end;
1,000,000個の命令を処理して、何ヘルツで動いているか計測してみた。

ガーン! ショック。テスト結果は 1.727MHz。実機の 2MHz に達していないではないか!
そりゃ、描画処理も入れたら遅いわけだ。。
ボトルネックは何んなのか考える
ChatGPT に相談してみた。
そしたら、以下のような指摘が帰ってきました。(致命的!!) と言ってます。
⚠️ 問題点①:ExecMethod2 の文字列分岐(致命的)
ExecMethod2(op, ‘main’, opcode); // ← これが非常に遅い
文字列で分岐しています:
if kind = ‘main’ then
op.opc_main[opcode]()
else if kind = ‘cb’ then …
✅ 改善策:直接呼び出しに置き換える
op.opc_main[opcode](); // ← これで高速化(関数ポインタ直呼び)
💡 25%以上の速度向上が見込めます。
先生了解しました。直接呼出しにしてみます。

す、すごい 倍速になりました。
やはり、Z80などのエミュレーションは、とにかく繰り返し処理なので、繰り返しの中のオーバーヘッドをなくすってことですね。
昔のマイコンプログラムを思い出す。
現代のゆるーい感じのコーディングのぬるま湯に浸かりすぎてました。反省。
まだまだ、改善できそうなので、次回に続く。