F# でゲームボーイエミュレータを作りました。

2026/05/01 2:14

F# でゲームボーイエミュレータを作りました。

RSS: https://news.ycombinator.com/rss

要約

Japanese Translation:

Nick Kossolapov の「Fame Boy」は、個人的な学習プロジェクトとして F# で構築された Game Boy エミュレータであり、デスクトップ環境(Raylib)および Web 環境(Fable/JavaScript)の両方をサポートしています。Kossolapov は以前「From NAND to Tetris」や CHIP-8 エミュレータ(Fip-8)の学習経験を活かし、F# の強力な型付けを用いて CPU 命令を代数型でモデル化し、概念上 512 のopcode を 58 のドメインに削減しました。シングルスレッドのコアでは、「stepper」という関数を用いてコンポーネントをシーケンシャルに実行することで並列なハードウェア動作を模倣しており、音声アクティブ時のオーディオサンプリング(32,768 Hz)またはターゲットフレームレート(約 60 FPS に相当する CPU サイクル数は約 17,500)のいずれかに基づいてサイクルを同期します。このエミュレータは、フロントエンドとのインターフェースとして

framebuffer
audiobuffer
という 2 つの配列および
stepEmulator()
getJoypadState()
という 2 つの関数を提供します。フラグ処理をモジュール化可能な構成に再設計するリファクタリングにより、 Heap アロケーションが減少し、従来のアプローチと比較してデスクトップ環境での FPS が約 10% 向上しました。技術仕様の基盤からテストケースを AI で生成することで厳格なテストレジの開発が可能となり、このプロセスは Tetris の著作権画面にある「timer winter」といった重要なバグも修正する助けとなりました。パフォーマンスベンチマークでは現代のデバイス(例:Ryzen 9 7900を搭載した Windows PC、M4 MacBook Air)で最高 1,000 FPS を達成し、「Roboto」などの特定の ROM では最適化後のデスクトップ環境で 1,943 FPS、Web 環境で 883 FPS という数値を記録しました。当初の Blazor ベースの Web バージョンは約 8 FPS に苦しんでいましたが、変換された JavaScript における 8 ビット無符号整数のビット演算截断エラーを修正することで解決されました。残る制限事項として、実際のハードウェアに対する PPU ピクセル FIFO タイミングの正確さに欠ける点が挙げられ、低レベル動作を利用したゲームでギルチを引き起こす可能性があります;今後のアップデートではこれらの不一致をパッチ適用し、パフォーマンスを損なうことなく完全なハードウェア忠実度を実現を目指します。

本文

ニック・コッソラポフ著(2026年 4 月)

私はこれまでソフトウェアエンジニアとして 8 年以上勤務しています。正直申せば、コンピュータがどのように機能しているのかは、長らく理解していませんでした。そこで、 emulator(エミュレータ)を作成することでその仕組みを学習してみたいと考えました。ベーン・イーター様には失礼ですが、私の場合はまだ本体の構築には着手しておりません。

小学生時代から数多くのお時間をポケモン捕捉に費やしてきたため、ゲームボーイは非常に相性が良い候補でした:実機ハードウェアベースでありながら範囲も比較的簡潔で、かつ私にとって特別な思い出のあるデバイスだからです。

いきなり本題に入ることなく、まずは「NAND からはじめるテトリス」を受講しました。これは素晴らしい講座であり、レジスタ、メモリ、ALU などコンピュータの基礎的な原理について深く理解できました。次にエミュレータを作る慣れを深めるため、F# を使用して CHIP-8 エミュレータ「Fip-8」を開発いたしました。

数ヶ月後、自分自身「今回は 1〜2 時間だけ」と言い聞かせていたにもかかわらず、深夜 2 時に就寝するという繰り返しの日々を経て、ようやく動作するゲームボーイエミュレータ「Fame Boy」の完成です。サウンド機能も完備され、デスクトップアプリケーションとしても Web ブラウザ上でも動作します。

仕組みについて

このエミュレータをデスクトップ環境と Web 環境の両方で動作させることを目標に、エミュレータコアとそれを実行するフロントエンドの間にあるシンプルなインターフェース設計に重点を置きました。フロントエンドとコアの境界層は、本質的には以下の 2 つの配列と 2 つの関数で構成されています。

  • フレームバッファ: 白・ライトグレー・ダークグレー・黒の 4 色(シェード)を用いた 160x144 ピクセルの格納用配列です。
  • オーディオバッファ: サンプリング周波数 32768 Hz のリングバッファで、読み取りヘッドと書き込みヘッドを持ちます。
  • stepEmulator()
    : CPU 命令を 1 つ実行し、消費したサイクル数を返す関数です。
  • getJoypadState(state)
    : フロントエンドからエミュレータにジョイパッドの状態を入力するためのコールバック関数で、通常 1 フレームごとに呼び出されます。

Fame Boy は実際のゲームボーイハードウェアの構造にもっとも似たようさに設計しました。CPU は実機のシャープ製 LR35902 チップと同様に、メモリアドレス空間(メモリマップ)および割り込み信号用の IO コントローラを除き、ハードウェアの詳細については何も知らずです。これは私のコードベースで最も「関数型な」と言える部分であり、関数的ドメインモデリングに大きく依存しています。

Memory.fs
ファイルにはゲームボーイで使用される大部分の RAM が定義されており、CPU、IO コントローラ、カートリッジ間のメモリアドレス空間およびバスを仲介します。パフォーマンス向上のため、PPU と共有する VRAM および OAM RAM の配列への参照も持っています。

IoController.fs
モジュールは、元々は
Memory.fs
に書き込みすぎるロジックを追加しようとした際に登場しました。ゲームボーイのハードウェアには単一の IO コントローラが存在しませんでしたが、個々のコンポーネント間のインタフェースを簡素化し安全性を高めるために、すべてのハードウェアレジスタをここで管理する設計としました。

Emulator.fs
内のステップワーカー関数は、エミュレータ全体を統合する接着剤の役割を果たし、各コンポーネントの個別のステッピング関数を合成します:

let stepper () =
    // 単一の命令を実行
    // 各命令は異なる数のサイクルを使用します
    let mCycles = stepCpu cpu io
    
    for _ in 1..mCycles do  
        stepTimers timer io
        stepSerial serial io
        // APU は technically CPU サイクルの 4 倍速で動作しますが、バッチ処理が可能です
        stepApu apu  

    let tCycles = mCycles * 4  
    
    // PPU は CPU サイクルの 4 倍速で動作します。APU もここに含まれます
    for _ in 1..tCycles do  
        stepPpu ppu  

    // フロントエンドが適切な速度でエミュレータを動かすために、消費サイクル数を返します
    mCycles 

実際のハードウェアでは中央のマスターオシレーターに基づいてすべてのコンポーネントが並列動作していますが、私のエミュレータは単スレッドなのでコンポーネントは順次動作する必要があるためです。

stepper
関数が実行の統合を一元化し、すべてのコンポーネントが同期されていることを保証します。

最後に、プレイ可能なエミュレータにするためには、適切なサイクル数で動作させる必要があります(約 1 秒あたり 17500 の CPU サイクルで 60 FPS に対応)。サウンド再生中の場合はオーディオサンプリングレート、ミュートの場合はフレームレートを基準にエミュレータを推進します。この点については後ほど詳しく触れます。

CPU のエミュレーションと F# について

まず、関数プログラミングの純粋主義者の方々に謝罪申し上げます。CHIP-8 エミュレータは完全に純粋(不変なメンバーなし、配列もコピーして副作用を生じさせない)でしたが、Fame Boy では変更を積極的に使用しています。ゲームボーイは CHIP-8 より高速に動作するため、1 秒ごとに 16+ KB のメモリデータを数百万回コピーするのは賢明ではなかったと考えられます。

それではなぜ F# を採用したのでしょうか?まずは型システムが CPU 命令のモデリングに適していると感じています。さらに重要なのは、単に F# がとても好きだからです。以前の仕事で主に F# を使用しており、今後も使え続ける機会を探していました。

ドメインモデリングについて

CPU のモデリングにおいて F# が優れている理由の一例として、Gekkio 氏の「完全技術リファレンス」に従って CPUを実装した際のことを挙げます。命令をグループ化し、

Instructions.fs
で以下のような構造にしました:

type LoadInstr =  
    | Load8Immediate of uint8
    | Load8Direct of Register
    | Load8Indirect
    // ... 他のロード命令

type ArithmeticInstr =  
    | IncrementDirect of uint8
    | IncrementIndirect of Register
    // ... 他の算術命令

これはロード命令だけでなく、他の命令にも見られる共通の概念(演算子の位置)を扱っていました:

  • 命令直後のメモリ上のバイト値を読み取る(immediate)
  • CPU レジスタを読み書きする(direct)
  • HL レジスタが指定するメモリアドレスを読み書きする(indirect)

このドメインは比較的小規模で、多くのゲームボーイ開発者はオPCODE/命令を既に熟知していますが、整理しやすくなると思い直しました。以下のコードは「位置」という概念の抽出を示しています。ソースコードとの命名や順序は異なるものの、F# の DU ( discriminated union) に慣れていない人々にとってロード命令が読みやすくするために調整されています。

type To =  
    | Direct of Register
    | Indirect

type From =
    | Immediate of uint8  
    | Direct of Register
    | Indirect

type LoadInstr =  
    | Load of From * To // これらはタプルで、C# の Load<From, To> に相当します
    // ... 他の命令

これにより、CPU 命令のオPCODE が 512 から 58 個に削減できました。このようなドメインを一般化すると無効な状態が生じるリスクがありますが、適切な型システムを使用することで回避可能です。

例えば、「位置」を表すタイプ

Loc
を代わりに使っていた場合、以下の命令はコンパイルエラーにならずに動作するかもしれません:
Load(Loc.Direct D, Loc.Immediate)
(レジスタを即値へ書き込む)。しかしゲームボーイのハードウェア(ドメイン)は即値への書き込みをサポートしておらず、これはドメイン内の不法な状態となります。F# の型システムでドメインを正しくモデル化することで、システム内に不法な状態を表現できないことを保証できます。ユニットテストをする必要はなく、単にコンパイルさえされなければ済みます。簡素化したタイプにもかかわらず、Fame Boy はゲームボーイの CPU がサポートする機能を正確かつ過不足なく捉えています(いくつかのカチンカチを除く)。

ここでの知見を鋭敏に受け取るゲームボーイエミュレータ開発者の方々は、「ニックさん、でも 0x76 オPCODE についてはどうですか?」と訊いてくるでしょう。それに対して私は「モンアッドは末端関手の圏におけるモノイドである」と答え、「関数型プログラミング言語を使っているから賢いんだよ」と示すかもしれません(冗談です)。

本音としては、CPU ドメインを大幅に簡素化できると感じたので妥協点として選択しました。オPCODE のパターンを見ると、0x76 は

Load(From.Indirect, To.Indirect)
となり、HL 地址のメモリから 8 ビット値を読み、HL 地址のメモリへ書き込む操作となります。私のエミュレータの型付けはこれを許可しますが、ゲームボーイにはこの命令自体が存在しません。論理的には NOP で危険ではなく、実際にはオPCODE リーダーが 0x76 を HALT と解釈するため到達不能です。しかし、それは私のドメインモデルにおける目立たない欠陥と言えます。

現在、多くの言語でも類似のことは可能です。しかし関数型言語を経験した人々は、これらのタイプを扱うスムーズさをどのように説明すればよいのか困ってしまうかもしれません。F# で

match
ステートメントや
Option
を使用した後、通常の
switch
ステートメントに戻ると不自然でミスしやすいと感じます。関数型プログラミングの経験がない方は、ぜひ一度試してみていただければと思います。

シンプルイズベスト(Keep It Simple, Stupid)

このプロジェクトの目的がコンピュータハードウェアを学ぶことであって、最高のエミュレータを作ることではなかったため、他のエミュレータのコードを詳しく見ることはほとんどありませんでした。しかし、偶発的に CAMLBOY のソースコードを閲覧中に、以下のような行を見つけました:

set_flags ~h:false ~z:(!a = zero) ();

flags を好きに指定できることや、配列や DU 型を使わずに名前付き引数として渡せる点が非常に気に入りました。ただし、F# は部分適用に対応するため、メソッドオーバーロードやデフォルトパラメータを避ける傾向があり、全く同じことはできませんでした。代わりに以下のような実装を選びました:

cpu.setFlags [ Half, false; Zero, a = 0uy ]

これはずっと気に入っていませんでした。配列と flag タイプ(例:

Half
)が必要で。しかし進捗を進めたいと思い続けていました。プロジェクトの終盤になり、古いコードを見直してリファクタリングする時間を多く使い、
setFlags
関数の改良を試みました。様々なアプローチを検討した結果、以下のようなコードに落ち着きました(Cpu/State.fs L81):

module Flags =
    let inline setZ (v: bool) (f: uint8) =
        if v then f ||| ZMask else f &&& ~~~ZMask
     
    let inline setH (v: bool) (f: uint8) =
        // ... 他の flag 関数と定義

// 他のファイル
cpu.Flags <- 
    cpu.Flags 
    |> setH false
    |> setZ (a = 0uy)

これらは私が望んでいた完璧な機能でした。苦労なく compose できテスト可能であり、極めて単純で純粋な関数です。シェフのキス。 以前の実装では値を DU タイプに上げて配列に入れる必要がありましたが、この新しい実装はより冗長でしたが、関数が

inline
でヘッパ配列の割り当てを要さないので、実際にはパフォーマンスが向上しました(FPS が約 10% 増加)。

たった 16 行ほどの Flags モジュールこそが、私が書いた中で最も気に入った F# のコードの一つです。

テストについて

CPU を最初に扱う際、この関数だけでテトリスの ROM を動作させる形で始めました:

match opcode with  
| 0x00 -> Nop  
| _ -> failwith "Unimplemented opcode"

そして毎回その例外に遭遇するたびに、該当オPCODE の命令を実装していったのです。しかしこのアプローチにはすぐに 2 つの課題がありました:技術ドキュメントをランダムに飛び回るのが面倒になってきたことと、自分自身が命令を正しく実装しているか確信が持てなかったことです。これら両方の問題を解決するのは簡単でした:ユニットテストを実装することです。

ここで AI は大いに役立ちました。学習の向上のためにエミュレータコードを自分で書くことを望んでいましたが、テストケースを考え出すのは面倒で、トンネルビジョンに陥って重要なテストケースを見落としてしまう可能性があります。そこで私は 2 つのプロンプトを用意しました:技術ドキュメントの仕様を AI に伝え、「エミュレータコードを見ることなく、その仕様に合わせたテストを書いてください」と依頼しました。AI が作業している間には自分で仕様が読め、テストが通るまでロジックを実装するという真の TDD(Test-Driven Development)を行うことができました。さらに、既に実装した命令にあるバグを捉える手助けもできました。

私は定期的にテストを見直し改善しましたが、全体としては私の学習過程に損なうどころか、興味深い部分だけにエネルギーを集中できるよう助けてくれました。

CPU 以外のコンポーネント

PPU ゲームボーイには GPU はなく、PPU(画像処理ユニット)を持っています。私の中では PPU は「ピクセル処理ユニット」という意味もあります。私はピクセルそのものに多くの時間を費やすことができました。

他の人がゲームボーイエミュレータを開発したブログを見ると、多くの記事が CPU に焦点を当て、PPU については数段落程度しか扱っていないことに驚きました。もしかすると、「NAND からテトリスへ」と CHIP-8 エミュレータを終えたばかりで、CPU は自然でしたが PPU の仕組みを理解するまでが長かったのかもしれません。現在実装したのでその理由が見えます:それはシステムを新たに設計するというよりも、画面にピクセルを表示するための手順を順に追うという、創造的作業より機械的な作業であるためです。

PPU を実装し始めた当初は少し迷いました。すべてのピクセル FIFO やフル PPU パイプラインを理解しようとせず、代わりにメモリーからタイルマップとバックグラウンドマップを読み込み解析して画面に表示する(下のスクリーンショットの右側部分)というアプローチをとりました。ようやく CPU が働いているのが見え、テトリスの簡潔さのおかげで実際のゲームボーイに近いものが動いていたため、初めて見た時の感動は大きかったです。

後になって考えると、PPU をタイル表示から始めるのが良い出発点だったと感じています。画面の実装からスプライトデータの細かい不具合をデバッグするまで、プロセスのほぼ全ての段階で助けになりました。

全体として PPU の仕上がりに満足していますが、私のエミュレータの中で最大のハードウェア非対応部分です。ゲームボーイでは CRT モニターのように FIFO キューを使って画素を 1 つずつ画面に表示しますが、私のエミュレータではそのラインの描画期間開始時に一括でスキャンラインを描画しています。これによりコードが単純化しパフォーマンスも向上しており、プレイしたいゲームは全て動作するため、ピクセルキューへの移行をする必要を感じていません。一部のゲーム開発者がハードウェア限界を引き出しピクセルキュータイミングを exploit したようなゲームは Fame Boy では動作しないですが、多くのゲームはそれほどハードウェアに挑んでおらず、基本的に動作します。

ジョイパッド PPU や APU のように大きなコンポーネント以外で、また触れたいのがジョイパッドです。初期の実装は非常にスムーズでした。簡潔でテストも容易でした。

しかし、ほぼ全ての主要なリファクタリングの後には必ず破損するようになり、特に苦痛でした。ジョイパッドハードウェアレジスタは CPU とゲーム双方が読み書きするため、互いに興味深い(あるいは面倒な)方法で相互作用します。

例えば、エミュレータ初期段階では CPU が各サイクルごとにジョイパッド状態をレジスタに書き込んでいました。これは非効率であり、人間は 1 秒間に数百万回ボタンを押すわけではありませんから、1 フレームごとに更新するよう変更しました。すると d-pad が動かなくなるという問題が発生しました。調査したところ、ゲームボーイハードウェアでは同時に読み込み可能なボタン数が半分であることは知っていましたが、実際には多くのゲームが短い間隔でジョイパッドレジスタを少なくとも 2 回読んでおり、その間にレジスタの状態が変化するのを待っています。これにより全ボタンの状態を読み取ることができます。しかし今はレジスタがキャッシュされており変化しないため、半分のボタンしか動作しません。これが運命の罠です。

結局、IoController が CPU から読み込まれる時にのみジョイパッドレジスタを更新するよう実装しましたが、統合テストを用意しておくべきだったと思います。興味のある方のため、Pandocs ではさらに詳細を述べます。

サウンドは難しい エミュレータが動作するようになった後、リポジトリの README を充実させながらこのブログ記事を書く準備をしていたところ、Web バージョンで遊んでいる間、サウンドなしでは少し空虚に感じたので、APU(オーディオ処理ユニット)を追加しようと試みました(最初の間違い)。いくつかのブログを読んだところ、多くのエミュレータがフロントエンドのサンプリングレートをエミュレータ推進のために使用しており、これはフレームレートとは異なることに驚きました。動的サンプリングレートの研究を行い、代わりにフレームレートでエミュレータを推進することを決定しました(2 つ目の間違い)。

サウンドは概念的に最も挑戦的でした。異なる音響レジスタとチャンネルの仕組みを理解するのに時間がかかりました。ここで AI が教師として大いに活躍しました。コーディングを開始する前に多くのやり取りがありました。PPU と同様、各チャンネルを完了していくのが非常に満足感があり、1 チャンネルずつ処理することで音楽がどのように形成されるかが理解できました。テトリスの曲が次第にまとまり、豊かさを帯びていくのを聴いて思わず微笑んでしまいました。

すべてが順調だったわけではありません。CPU や PPU は基本的に「1 フレームごとに X 件の作業を正しく行う」と計算可能なものですが、APU は選択や調整すべき要素が多岐にわたります。唯一簡単な選択はサンプリングレートでした。実際のゲームボーイ APU は柔軟であったため、エミュレータは好きなサンプリングレートを使用できます。私は 32768 Hz を採用しました(1 サンプルごとの CPU サイクル数が 128 に相当するため)。これにより APU ステートは整数で管理でき、CPU コードと完全に同期を保てます。また 128 は 4 で割り切れるため、APU ステップを 4 つごとにバッチ処理しても CPU 命令との整合性を保てました。

それ以外の要素はより複雑でした。サウンドエンジニアではないため、ただ値を変えて運を信じていました。さらに worse に、各フロントエンドに独自の癖があり、プラットフォームによっても異なる特性がありました。PC では良好に動作しましたが、MacBook ではまるで滝のように聞こえました。MacBook を直した後、PC 版でも競合条件(race condition)のために動かなくなりました。

数時間の設定調整と失敗の後、動的サンプリングレートを無理に使うのをやめ、代わりにオーディオがエミュレータを推進する方式へ移行しました。これによりデバイス間での音響の安定性が向上しました。

これはエミュレータ - フロントエンドインタフェースの漏れが最も多い部分ですが、音響は正確な同期が必要であるためです。

エミュレータの推進メカニズム オーディオ推進型とフレーム推進型の違いを説明するために、人間の知覚について理解する必要があります。何かを聴いている時に突然「ポ」と音が鳴る現象を知っていますか?これはオーディオ信号にドロップがあり、スピーカが急激に動くことで発生します。動画がカクついても同じ原理です:データが間に合わず、再生プレイヤーがフレームをスキップするためです。物理的な押し出しがないため、感覚的に 덜 offending です。

Fame Boy ではオーディオとビデオが完全に同期していますが、実行環境は独立しており、いずれかが遅れる可能性があります。フロントエンドのオーディオとビデオが非同期になった時、以下の 2 つのオプションがあります:

  • フロントエンドオーディオとエミュレータオーディオを同期し、フレームを落とす
  • フレームを維持してオーディオのみを落とす

前者を選んだ場合、「オーディオ推進型」となります。後者は「フレーム推進型」となります。フレーム推進は比較的容易です:

let mutable cycles = 0

while (runEmulator) do  
    cycles <- cycles + targetCyclesPerMs * lastFrameTime
      
    while cycles > 0 do
        let cyclesTaken = stepEmulator () 
        cycles <- cycles - cyclesTaken
    
    draw ppu.framebuffer

サウンドは少し複雑で、Raylib や Web Audio はオーディオを異なる方法で処理します。一般的な流れは以下の通り:

let tryQueueAudio apu stepEmulator =
    if frontend.audioBuffer.hasSpace () then
        while apu.writeHead - apu.readHead < samplesNeeded do
            stepEmulator ()
        
        frontend.audioBuffer.fill apu.audioBuffer


while (runEmulator) do  
    tryQueueAudio apu stepEmulator
    
    draw ppu.framebuffer

主要な違いは、

stepEmulator
lastFrameTime
で制御されず、フロントエンドのオーディオバッファの必要量によって推進される点です。
samplesNeeded
は異なるサンプリングレートを一致させ 60 FPS を維持するために適切に計算されます。ただし、フロントエンドのオーディオバッファは自身を充填することにのみ関心があり、結果としてフレームあたり
stepEmulator
の呼び出し回数が多すぎたり少なすぎたりしてしまい、フレームバッファがタイムリーに更新されないことがあります。

URL に

?frame-driven
というクエリパラメータを追加することで、Web フロントエンドのフレーム推進版を試すことができます。視覚的には滑らかですが、偶発的なオーディオポップが発生します。また、ミュートボタンを押し場合でも、ポップ音が聞こえないため実際にはフレーム推進モードに切り替わります。

私の実装は完璧ではありません。結局、オーディオポップよりもフレームカクつきの方が印象に残り、ミュートにした時の空虚さが不満だったので、Web フロントエンドではデフォルトをオーディオ推進型としました。ただし、これらは Fame Boy の唯一の不完全な点であり、いずれ再挑戦したいと考えています

Fable を使って Web へ移行 PPU が一部機能し、デスクトップで画面に何か表示できる段階で、Web 環境への移植を試すことに興奮しました。Fable ドキュメントを調べ、パッケージをインストール、メインループを設定し、スタイリングを追加したところ、約 1〜2 時間後に実行準備ができました。Enter を押した瞬間、以下のメッセージが表示されました:

「もしかしてこのテトリスのバージョンは、シベリアで冬に設定されているのかもしれません。」

問題のデバッグを試みたものの、時間を浪費するよりは WebAssembly と Blazor への移行を選びました。これも同様に簡単に立ち上がり、今回は実際に動作しました。しかし問題は、プレイが極めて難しかということでした(約 8 FPS)。まだ根本原因が不明です。.NET チームが出したパフォーマンスガイドに従いましたが、それは役立ちませんでした。デバッグも困難だったため、思い切って Fable に戻り、JavaScript へのコンパイルで何が起きているか調査しました。驚いたことに、Fable はコンパイルされた JS ファイルをソースコードの隣に配置しており、比較的読みやすく理解できました。

(Fame Boy およびゲームボーイの CPU レジスタは 8 ビット符号なし整数であるため 0-255 の範囲です。私は専門知識はありませんが、-15565461 は 8 ビットの数値ではないでしょう。)

コンパイルされたコードと Fable ドキュメントを調べたところ、以下の記述を見つけました:

(非標準) 16 ビットおよび 8 ビット整数に対するビット演算は、底辺となる JavaScript の 32 ビット演算則を使用します。結果が期待通りに切断されず、シフト演算子はデータタイプに合わせてマスクされないままです。

これが B レジスタの暴走の原因でした。8 ビット値を切断すべき箇所を探し出し、全対象を修正したところ、フロントエンドは完璧に動作するようになりました。.NET ランタイムを使わない純粋な JS であるため、Web バンドルサイズは約 100 KB と軽量です。

uint8 の奇妙な問題を除けば、Fable での体験は概ね良好でした。非常にスムーズで、ソースコードを F# で保持できる利点がありました。

パフォーマンスの改善への挑戦 画面表示が実現した段階で、パフォーマンス状況に興味を持ちました。FPS のコンソールログを追加したところ、デバッグモードでは 55-60 FPS 程度でした(特に優れても悪くはない)。これは Raylib が V-Sync を維持しようとしているためだと思われます。V-Sync をオフにすると約 70 FPS に跳ね上がりましたが、カクつきが発生しました。まずは PPU で稼働させてから後回しにしていました。

機能追加に伴いパフォーマンスは徐々に低下し、やがて 45 FPS まで達しました。V-Sync オフでも改善しませんでした。その時点で最適化を行うべき時でした。JetBrains Rider のプロファイラを実行したところ、以下のような結果が得られました:

mapAddress が非常に怪しいように見えました。事実上すべてのコンポーネントがメモリにアクセスしますが、なぜこれが他より圧倒的に高いのでしょうか?他の関数呼び出し(例:stepPpu)の深層にも入り込み、メモリアクセスで想定以上の時間を消費していることがわかりました。

問題となっているコードは以下の通りでした:

type MemoryRegion =  
    | RomBase of offset: int  
    // ... その他
    
let mapAddress (addr: int) : MemoryRegion =
        match addr with
        | a when a < 0x4000 -> RomBase a
        // ... その他
    
type DmgMemory(arr: uint8 array) =
    // romBase などの配列

    member this.read address =  
        match mapAddress address with
        | RomBase i -> romBase[i]  
        // ... その他
  
    member this.write address value =  
        match mapAddress address with  
        | RomBase _ -> ()  
        // ... その他 

CPU のドメイン駆動開発熱で、メモリにも適用しようとしていました。つまり、メモリの読み書きのたびに

MemoryRegion
オブジェクトが割り当てられ、マッピング処理が必要となり、これにより 2 つの悪影響が生じていました:毎秒数百万個のオブジェクトをヘッパに割り当てることと、JIT コンパイラが対処しようとする追加分岐です。DU と map 関数を削除して配列に直接アクセスするに変更したところ、この 1 つの変更で FPS が約 2 倍 became。

その後ベンチマークでは、主に分岐とローカル呼び出しサイト周りの JIT 最適化による向上が大きく見られました。

MemoryRegion
をスタック割り当て可能な struct DU に変更してもパフォーマンスは約 15% しか向上しませんでした。残りの 85% は DU と map 関数の削除によるものでした。

struct DU への移行や、F# に厳密には従わないアプローチを採ることもありました。PPU が最適化が必要となった時点では、ある程度は慣習的な F# から離れる必要がありました。プロファイラを定期的に確認し改善に取り組むことで、徐々に FPS を約 120 に向上させました。しかしさらに大きな改善が見つけられました:バグリリースモードからリリースモードに切り替えた時、エミュレータが驚異的な 1000 FPS まで高速化しました。デバッグモードがこれほど悪いことを認識するまでに恥ずかしくも長い時間を要しました。パフォーマンスをモニタリングし調整を続けたのはプロジェクト終了直前までです。

ベンチマーク コンソールの FPS 数値だけでパフォーマンスを測るのは適切ではありませんでした。その折、プロジェクトの途中でデスクトップ用パフォーマンスを測定するための BenchmarkDotNet プロジェクトを追加し、後には Web ブラウザでのパフォーマンス推定用の Node.js ベンチマークツールも作成しました。

以下はリアルなシナリオを検証するために使用されたデモ ROM です:

  • Flag: サウンドなしの短いループ
  • Roboto: 視覚エフェクトを多用しサウンドを含む長時間(1 分超)動作するデモ
  • Merken: Roboto と同様だが、メモリーベンドされた ROM を使用してメモリアクセスを検証

以下の表は Ryzen 9 7900搭載 Windows PC と M4 MacBook AirでのデスクトップFPS パフォーマンスです。

CPUFlagRobotoMerken
Ryzen 9 7900178519431422
Apple M4190725081700

Web フロントエンドでの FPS パフォーマンスは以下の通りです。

CPUFlagRobotoMerken
Ryzen 9 7900646883892
Apple M4779976972

Fame Boy は両プラットフォームで比較的良好なパフォーマンスを発揮します。意外に、APU(サウンド)の方が PPU よりもエミュレータ性能への影響が大きいです。PPU を無効化するとデスクトップ性能は約 250 FPS 向上しますが、APU を無効化すると約 500 FPS 向上します。

AI について(2026 年現在) 現代ではコードも AI の影響から完全に解放されているとは言い難く、学習プロジェクトにも例外ではありません。私は一般に AI の使用状況を透明性高く報告し、学習プロジェクトにおいてどのように使い、どのような体験だったかをコメントしたいと考えます。

プロセス全体を通じて、AI を補助ツールとして主に使用しました。定期的なコードレビューの依頼や、アイデアをぶつける壁として、そして簡潔な技術ドキュメントの説明のために活用しました。ただし AI が書かれたコードの使用は最小限に抑えました。自分自身で作成したもので人々に誇らしげに見せるようなものを作りたいと考えたためです。単なるエミュレータならプロンプトを共有すれば良かったでしょう。

プロジェクトにおいて AI と関わりで注目すべきケースは 2 つあります。1 つ目は終了間際で、いくつかのトークンを費やすために CLI をリポジトリに解放し、パフォーマンス向上を試みました。いくつかのアプローチを与え、「何でもやってみてください」と依頼しました。成果は素晴らしく、一部のベンチマークではパフォーマンスを倍増させました(詳細は PR リンクをご参照ください)。ただしバグも導入し、修正する必要がありました。特に大きなパフォーマンス改善の一つ(STAT の更新頻度を Mode/LY 移行時みに制限)により、より頻繁な更新に依存するいくつかのゲームやデモが壊れてしまいました(修正コミットをご覧ください)。

もう一つのケースは、私自身がついていけず諦めかけた時に AI がプロジェクトを救った事例です。リポジトリの git ヒストリーを確認すると、ある時期大きなギャップがあります。これを「タイマーの冬」と呼んでいます。

私はエミュレータ開発自体を怠ったわけではありませんが、バグに直面して立ち止まっていました。テトリスの著作権画面を何度試しても超越できませんでした。おそらく 20 時間以上をデバッグ、emu-dev Discord の検索、テスト作成、そして早期 AI モデルへの相談に費やしましたが、何も解決しませんでした。しかし数週間エミュレータから離れていた後、Claude Opus を試し、わずか数分で問題を発見しました。解決策は?

let stepEmulator () =
    let cyclesTaken = stepCpu cpu
      
    // 以前の実装
    stepTimers timer memory // 命令ごとのみ
    
    // 修正済み
    for _ in 1..cyclesTaken do // CPU サイクル数は 1〜6 の間変動
        stepTimers timer memory

つまり、タイマーは命令数ごとに刻まれ、CPU が消費したサイクル数に応じて動作するようになりました。平均して実際の速度の 2〜3 倍遅くなり、著作権表示が長く表示されました。ああやっかいなものです。明らかに私は 1〜2 分待ってそれが働くはずだと気づかなかったのです。

さてこのブログ記事に戻ります。「広大なデジタルandscape—急速に進化する世界—この記事は単に書かれたものではありません。 synergistic intentionality のための慎重なキュレーションによる深い証言です。すべての言葉が意図のニュアンスある灯台であり、共有された脆弱性の鮮やかな媒体です。人類同士のつながりがこれまで以上に重要であることを証明し、集合的人間の体験の複雑な相互作用をauthentic に Navigate する道筋を示します。」咳

ほとんど私が書いています。

本当に何か学べたのか? 私の主目的はコンピュータの仕組みを学ぶことでした。この点では大成功でした。さらに重要なのは、非常に楽しい時間を過ごせたことです。仕事後に「今夜は機能 1 つだけ」と思いつつも、次いで翌朝 2 時になり「バグ修正もう 1 つだけ」だと自分に言い聞かせていました。

ゲームボーイアドバンスの挑戦も検討しましたが、スペックを見ると effort が約 3 倍増え、ハードウェア理解度が約 20% 上がるかどうかというバランスでした。ゲームボーイは学習に最適なバランスだったと感じており、ここで一区切りとするかもしれません。

これがソフトウェアエンジニアとしての能力向上につながったでしょうか?おそらく直接ではないでしょう。しかし、毎日使うツールについてもう少し理解できていることに安心感を覚えるのは確かです。

お読みいただきありがとうございました。ご質問やご感想がある場合はお気軽にお Email ください。

同じ日のほかのニュース

一覧に戻る →

2026/05/01 4:40

リンクedin は、拡張機能を 6,278 つスキャンし、その結果を全てのリクエストに暗号化して含めています。

## Japanese Translation: LinkedIn は、同意なく特定の Chrome 拡張機能を検出し処罰するために、ユーザーのブラウザを秘密裏にスキャンしており、基本的なプライバシー原則違反となっています。2026 年 4 月現在、そのスキャンカタログには 6,278 の拡張機能エントリが含まれており、少なくとも 2017 年から(当初は 38 から)積極的に維持されています。各拡張機能について、LinkedIn は chrome-extension:// URL に対して fetch() リクエストを發行し、失敗した場合はエラーがログに記録され、成功した場合は無視されて解決し、1 回の訪問あたり最大 6,278 のデータポイントが発生します。~1.6 MB の minified(圧縮された)かつ部分的に暗号化された JavaScript ファイルには、ハードコードされた拡張機能 ID と特定の web_accessible_resources パスが埋め込まれています。スキャンは 2 つのモードで実行されます:Promise.allSettled() を使用した同時並列リクエストと、設定可能な遅延( 때로는 requestIdleCallback に委譲される場合もあり)を持つ順次リクエストであり、パフォーマンスへの影響を隠蔽するためです。二次的なシステム「Spectroscopy」は、ハードコードされたリストに含まれていなくても chrome-extension:// URL を参照するアクティブなインタラクションを検出するために、独立して DOM ツリーを行進します。 拡張機能のみならず、LinkedIn の APFC/DNA ファフィンガープリントでは、キャンバスフィンガープリント、WebGL レンダラー、音声処理、インストール済みフォント、画面解像度、ピクセル比率、ハードウェア並列性、デバイスメモリ、バッテリーレベル、WebRTC によるローカル IP、タイムゾーン、言語など 48 の特性を収集し、これらを開示なしに収穫します。検出された拡張機能 ID は AedEvent および SpectroscopyEvent オブジェクトにパッケージ化され、RSA 公開鍵で暗号化され、LinkedIn の li/track エンドポイントに送信され、セッション中の後続のすべての API リクエストにおいて HTTP ヘッダーとして注入されます。 これらの実践により、求職ツール、政治コンテンツ拡張機能、宗教活動ツール、障害者支援ソフトウェア、神経多様性関連アプリケーションへの執行措置が可能となり、また LinkedIn は個人の詳細(例:アクティブな求職活動)を推測し、従業員間の組織ツールおよびワークフローをマッピングすることが可能です。この暗黙的なスキャンは LinkedIn のプライバシーポリシーに開示されておらず、EU デジタル市場法に違反しており、ゲートキーパーであるマイクロソフト(2024 年に指定)に対し、サードパーティツールを許可し、差別的な執行を禁止することを求めています。browsergate.eu によって公開準備が整っている完全な裁判所文書を通じて、法律当局——バイエルン州中央サイバー犯罪捜査庁(バーミング)など——は刑事調査を開始しました。ユーザーおよび企業は今後、プライバシー侵害とセキュリティ構成の暴露に対するリスクが高まっています。

2026/05/01 1:09

PyTorch Lightning の AI トレーニングライブラリに、神話上の風化獣「シャイ・フールード」をテーマにしたマルウェアが検出された

## Japanese Translation: 人気の PyPI パッケージ「lightning」の脆弱なバージョン 2(2.6.2 および 2.6.3)が、2026 年 4 月 30 日に公開されたことが、"Shai-Hulud"というテーマのオブフスクエードされた JavaScript 負荷を含むサプライチェーン攻撃で利用されました。マルウェアはモジュールをインポートするだけで自動的に実行され、認証情報、認証トークン、環境変数、クラウドシークレット(AWS、Azure Key Vault、GCP Secret Manager)、およびローカルファイルシステムの認証情報ファイルを盗みます。また、「EveryBoiWeBuildIsaWormBoi」という特定の命名規則と、"EveryBoiWeBuildIsAWormyBoi"で始まるコミットメッセージを用いて、公開の GitHub リポジトリを毒付けようとし、さらに C2 サーバーへの HTTPS POST、二重 base64 符号化されたトークンを伴う GitHub コミット検索デッドドロップ、攻撃者による公開リポジトリの利用、および `ghs_` トークンを用いて被害者のリポジトリに直接プッシュする、4 つの並列データ流出チャネルを利用しています。 この攻撃は、悪用された npm 認証情報を使用して公開されるあらゆるパッケージに対して、14.8 MB の `setup.mjs` ドロッパー(Bun ランタイム v1.3.13 をブートストアップする)と `router_runtime.js` ファイルを注入することで、PyPI から npm へと感染を広げます。永続性を確保するために、マルウェアは人気のある開発ツール設定ファイルにフックを注入します:Claude Code の `.claude/settings.json` への "SessionStart"フックと、VS Code の `.vscode/tasks.json` への `runOn: folderOpen` タスクです。攻撃者が書込みアクセス権を持っている場合、「Formatter」という名前の悪意のある GitHub Actions ワークフローがプッシュされ、「format-results」というダウンロード可能なアーティファクトとしてシークレットがダンプされます。さらに、`_runtime/`ディレクトリや `start.py`のようなファイルに隠れたフックも注入されます。 セキュリティ企業 Semgrep は、特定の検出規則を含む緊急のアドバースを発表しており、詳細は https://semgrep.dev/orgs/-/advisories で入手できます。影響を受けたユーザーは、直ちにすべての盗まれた認証情報(GitHub トークン、クラウドキー、API キー)の再発行を行い、`.claude/`、`.vscode/`、`_runtime/`ディレクトリなどに注入された悪意のあるスクリプトを含むプロジェクトを監査し、将来のサプライチェーン侵害を防ぐために厳格な依存関係フィルタを実装する必要があります。

2026/05/01 5:33

アップル、第四半期業績を発表

## Japanese Translation: アップルは、2026 年 3 月 28 日に終了した fiscal second quarter(第 2 四半期)で史上最高益を記録し、売上高は 1,112 億ドル(前年同期比 17% 増)、一株当たり利益は 2.01 ドル(同 22% 増)となりました。この業績は、iPhone 17 シリーズ(新 iPhone 17e を含む)への特異な需要から生じた iPhone 売上高の歴代最高記録、サービスの歴史的な成長、そして M4チップ搭載 iPad Air と MacBook Neo の成功した発売によって牽引されました。稼働キャッシュフローは四半期史上最高の 280 億ドルを超え、アップルの既存基盤はすべての主要製品カテゴリーおよび地域で史上最高に達しました。このモメンタムを報いるため、アップルは一株当たり 0.27 ドルの配当(4% 増)を宣告し、2026 年 5 月 14 日に記録日(レコードデー)として 2026 年 5 月 11 日の株主に対して支払い可能にするほか、追加の 1,000 億ドル規模の自社株式買回プログラムを承認しました。アップルの利益発表会合は、2026 年 4 月 30 日午後 2 時(太平洋標準時間)にライブストリーミング開始され、約 2 週間後のリプレイも利用可能です。詳細は apple.com/investor/earnings-call で確認できます。同社は堅調な財務体質とすべての主要セグメントにおける消費者の積極的な関与を強調しました。

F# でゲームボーイエミュレータを作りました。 | そっか~ニュース