KITAQ DEVELOPMENT SYSTEM
USER'S MANUAL 2026.09

KITAQ SERIES • REFERENCE EDITION

KITAQFC

ファミコン Cプログラミング

初めての一行から、実行・観測・再テストまで。
ローカルソース採取:2026年9月14日

Rust製コンパイラと補助ツール

Rust 1.85以降で、Windows・Linux・macOS ARM・macOS Intel用のコンパイラと全補助ツールをビルドできます。ネイティブ実行ファイルの動作に.NETは不要です。素材処理用の補助ツールもPythonやPillowを必要としません。

Windows用実行ファイルはリポジトリ直下、Linux・macOS用は下表のbin/配下にあります。lib/とライセンス表記を一緒に保持してください。Linux/macOSでは取得した実行ファイルにchmod +xで実行権限を付け、配置先をPATHに追加するかフルパスで実行します。

OSkitaqfc
Windows x64kitaqfc.exe
Linux x64bin/Linux-X64/kitaqfc
macOS ARM64bin/macOS-ARM64/kitaqfc
macOS Intelbin/macOS-X64/kitaqfc

kitaqfc · Rust製コンパイラと補助ツール · GitHub Actions

Windows・Linux・macOS ARM・macOS Intelでネイティブビルドと実行検証が成功しています。KITAQGBは各環境48件のテストと395件の補助ツール検証、KITAQFCは55件と401件が成功しました。Rust 1.85でも確認済みです。PUBLIC_DISTRIBUTION.jsonに配置済みバイナリのハッシュと検証の出典を記録しています。公開GitHub Actionsでもこのソースを独立してビルド・検証します。

保存済みの比較データでROMのバイト列、診断と補助ツールの形式を検証しています。以前のC#版によるエミュレータ検証は、そのソース指紋に対応する過去の記録として残しています。これだけでRust版の全API、実機動作、FDSのBIOS経由のゲーム起動を保証するものではありません。元のPNG変換スクリプトは入手できないため仕様から再実装し、元スクリプトとのバイト一致は確認できません。

--attachは互換性のため受理しますが、ネイティブ実行ファイルへマネージドデバッガを接続しません。PC側のデバッグにはネイティブデバッガを使用してください。

KITAQGB · SHA-256 · KITAQFC · SHA-256

各OSでのサンプルビルド

PowerShellのサンプルビルドは直下のWindows用EXEを使用します。Linux/macOSでは同じC入力とオプションをネイティブコンパイラに渡すか、PowerShell 7を使用します。CHR画像とCソースは別の入力です。font.chrには元のサンプルフォントを保持しています。

sh
kitaqfc game.c -I lib -o game.nes --mapper=nrom

コンパイラとライブラリの現行配布構成 — 2026年10月4日

kitaqfc.exe と lib は同じチェックアウトの組み合わせを使ってください。親ディレクトリから下記の Release ビルドスクリプトを実行すると、プロジェクト設定により EXE がリポジトリ直下へコピーされます。BINARY_BUILD.json にコンパイラのソース、EXE のハッシュ、そのビルドの検証結果を記録しています。長い診断行の表示は200文字と省略記号に制限されます。

現行C#ビルドと以前の配布版を29入力で比較しました。受理した15入力の生成ROMはバイト単位で一致し、受理しなかった14入力のエラー診断も一致しました。さらに2本のROMをKUROSAKIで4フレーム実行し、一方でエンティティの割り当て、コールバック、スロット再利用を、他方で符号付き演算、KQBody3D の配置、オーディオキュー定数を確認しました。検証記録にはRAMの期待値18バイトと実際の値を収録しています。この検証は音声再生や全APIを対象としておらず、以前の回帰試験結果は BINARY_BUILD.json で区別しています。

.\kitaqfc\scripts\build.ps1
.\kitaqfc\kitaqfc.exe --help

ソース例と実行検証記録

1 KITAQFCとGB版の違い

KITAQFCはKITAQGBのフロントエンドを利用し、NES/Famicomの6502系CPUへコードを出すコンパイラです。GB用ROMをNESへ変換するツールではありません。画面、音、メモリ、マッパーに合わせてプログラムを作ります。

構造体のコピー、通常の関数呼び出し、for、while、do-whileを使用できます。do-whileは本体を少なくとも1回実行してから条件を判定します。continueは末尾の条件判定へ進み、breakはループを抜けます。switchでは0~255のcase定数で分岐し、caseに一致しなければdefaultを実行します。判定式は1回だけ評価します。breakは最も内側のループまたはswitchを抜け、switch内のcontinueは外側のループへ進みます。

2 準備とビルド

Rust 1.85以降で、Windows・Linux・macOS ARM・macOS Intel用のコンパイラと全補助ツールをビルドできます。ネイティブ実行ファイルの動作に.NETは不要です。素材処理用の補助ツールもPythonやPillowを必要としません。

sh
cd kitaqfc
cargo test --locked --tests
cargo build --locked --release

Windows用実行ファイルはリポジトリ直下、Linux・macOS用は下表のbin/配下にあります。lib/とライセンス表記を一緒に保持してください。Linux/macOSでは取得した実行ファイルにchmod +xで実行権限を付け、配置先をPATHに追加するかフルパスで実行します。

OSkitaqfc
Windows x64kitaqfc.exe
Linux x64bin/Linux-X64/kitaqfc
macOS ARM64bin/macOS-ARM64/kitaqfc
macOS Intelbin/macOS-X64/kitaqfc
powershell
.\scripts\build.ps1
.\kitaqfc.exe --help
sh
sh scripts/build.sh

PowerShellのサンプルビルドは直下のWindows用EXEを使用します。Linux/macOSでは同じC入力とオプションをネイティブコンパイラに渡すか、PowerShell 7を使用します。CHR画像とCソースは別の入力です。font.chrには元のサンプルフォントを保持しています。

3 最初のプログラム

c
#include "fc_common.h"
void main(void) {
    m_init();
    m_text(2, 5, "HELLO WORLD");
    m_number(42);
    while (1) { m_wait(); }
}
powershell
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\fc_hello.c -I .\kitaq-docs\samples -I .\kitaqfc\lib --mapper=nrom --nes-chr=.\kitaq-docs\samples\font.chr -o .\out\hello.nes --kurosaki-metadata=.\out\hello.debug.json

HELLO WORLDと042を表示します。共通ヘッダーの m_wait はVRAMキューをcommitしてNMIを待ち、スクロールを復元します。PPUADDRへの転送でスクロール内部状態が変化するため、復元を抜かすと文字が画面端へずれる場合があります。画面OFF→素材準備→描画ON→NMI同期の流れを覚えてください。

4 言語入門

GB巻の文と式の説明が共通の出発点です。FCでは unsigned char / unsigned short が使え、core.h に u8 / u16 / s8 / s16 の短い名前が定義されています。fc.h は集約ヘッダーです。引数なし関数は void main(void) の形式を使えます。

c
#include "core.h"
u8 clamp_score(u8 n) {
    if (n > 99) return 99;
    return n;
}

整数は8ビットまたは16ビットの範囲で扱います。配列の添字は0からです。関数・ポインタ・構造体を使う実例は fc_aggregate.c、算術は fc_arithmetic.c、繰り返しは fc_control.c にあります。GB用のCGBレジスターやGB専用組み込み命令を混ぜないでください。

式の結果と評価回数

比較演算子 ==、!=、<、<=、>、>= と論理演算子 !、&&、|| は、偽なら0、真なら1を返します。結果を u16 変数へ保存したり、引数・戻り値・算術式に使えます。例えば score = 500 + (lives != 0); は、残機があれば501、なければ500になります。

&& は左辺が0なら右辺を評価せず、|| は左辺が0以外なら右辺を評価しません。pointer != 0 && pointer->active != 0 では、ポインタが0のとき右辺へアクセスしません。16ビット値は上位バイトも含めて判定するため、256も真です。ビット演算子 &、| には短絡評価がありません。

++value は更新後、value++ は更新前の値を返します。配列要素、ポインタの参照先、構造体メンバーにも使えます。buffer[index()]++ は index() を1回呼びます。u16 *p に対する p++ は次の要素へ2バイト進み、(*p)++ は指している値を増やします。

sizeof(array) は配列全体のバイト数、sizeof(pointer) は2です。例えば u16 values[9]; なら sizeof(values) は18です。ROM配列・ローカル配列・構造体内の配列も対象になります。sizeof(function()) は戻り値の型の大きさを調べるだけで、関数を呼びません。

関数宣言と定義では、引数の型と順番を合わせます。引数名は一致しなくてもよく、関数本体では定義側の名前を使います。例えば u8 next(u8 input); に対して u8 next(u8 value) { return (u8)(value + 1); } と定義できます。引数やローカル変数は同名のグローバル変数を隠します。

?:で配列や文字列を選ぶと、結果は選んだ要素型へのポインタになります。show(ready ? "READY" : "WAIT");のように直接引数へ渡せます。u16配列aとbを選ぶ(ready ? a : b) + 1は、選択先の2番目の要素へ2バイト進みます。配列そのものをコピーする操作ではありません。

condition ? yes : no は条件を評価してから、選ばれた側だけを評価します。条件や選択肢には変数を使ったシフト式も書けます。例えば on = (pattern & (0x80 >> bit)) != 0 ? 4 : 2; は対象ビットに応じて4か2を選びます。&&、||で飛ばされる右辺の関数呼び出しも実行されません。ループ内のcontinueでは、forの更新式を1回実行してから条件を再評価します。while・do ... whileでは条件へ進みます。

5 ループと状態による分岐

c
do {
    update();
} while (condition);

// Dispatch a state with if/else.
if (state == 0) { title_update(); }
else if (state == 1) { game_update(); }
else { pause_update(); }

この断片は、更新を少なくとも1回実行するループと、状態に応じた処理の振り分けを示しています。update、condition、state、各状態の関数は、自分のプログラムで定義してください。完全なループのROM例はfc_control.c、シーン管理のROM例はライブラリのフレーム・シーンサンプルを参照してください。

scene、entity、systemのコールバックには、宣言された引数と戻り値の型に合う関数を登録してください。各ライブラリは対応する更新・描画・フレーム待ちの処理から登録関数を呼び出します。関数のあるROMバンクと呼び出し時の配置条件は、各APIの説明に従ってください。再帰や可変長引数をPC用Cと同じように使えるとは限りません。

6 メモリとPPU

NESのCPU内部RAMは0x0000~0x07FFです。0x0800以降のミラーを別RAMのように配置しません。スタックは6502のページ1、OAMシャドウやキューにも予約領域があります。--nes-local-ram=START:LENGTH / --nes-temp-ram=START:LENGTH はマップを調べて使う上級設定です。

PPUのアドレス空間はCPUのメモリとは別です。CHRは絵のパターン、ネームテーブルはどのタイルをどこへ置くか、属性テーブルは色の選択、パレットは色番号です。背景の属性は通常16×16画素単位なので、GBのタイル属性と同じ操作にはなりません。

7 NMIとVRAMキュー

NMIは画面の区切りに来る割り込みです。描画中にPPUのメモリを直接大量更新すると表示が壊れます。起動時の描画OFF中は直接初期化し、通常更新では __vramq_put / __vramq_copy / __vramq_fill とcommitを使います。

c
__vramq_put(0x2000 + 8 * 32 + 3, '4');
__vramq_commit();
__nmi_wait();
__scroll_set(0, 0);

キューの容量と転送元の寿命を確認します。デフォルトNMIはキュー処理を行います。独自 __nes_nmi を定義する場合は、必要なキュー実行・OAM・レジスター保存等を引き継ぎます。

8 マッパーとROM構成

指定最初に考える用途
nrom小さな固定ROMの教材
uxrom / cnrom / axrom単純なPRGまたはCHR切り替え
mmc1 / mmc3 / mmc5大きなゲームとマッパー固有機能
vrc6 / vrc7 / fme7バンク制御と対応する拡張機能
fdsディスク形式の出力経路

この表はコンパイラの選択肢です。各エミュレータや実機での完成度一覧ではありません。--board=surom512 はMMC1の特定基板構成を明示するためにあり、単にファイルを512KiBにすれば同じになるわけではありません。KUROSAKIのboard auditと組み合わせます。

powershell
.\kitaqfc\kitaqfc.exe main.c --mapper=mmc3 --nes-chr=tiles.chr --mirroring=vertical -o .\out\game.nes --kurosaki-metadata=.\out\game.debug.json --emit-ai-metadata=.\out\build.json

--battery / --no-battery、CHR容量、PRG配置、バンク呼び出しは対象基板の条件を確認します。マッパーやミラーリングを変更したら、ROM生成だけでなく起動・スクロール・データ切り替えまで再確認します。

ROM配列を通常の添字やCポインターで参照すると、その配列は共通バンク0へ配置されます。ただし、#pragma fixed_bank で明示した配置は保持します。これにより、呼び出し元が切替バンクで動いていてもデータを参照できます。明示的なfarアクセスには、__farpeek8(__bankof(table), table) のように、同じ呼び出しの引数で配列名とその配列の __bankof を組み合わせます。バンク番号を問い合わせるだけでは共通バンクへの配置を要求しません。fixed_bankを指定したデータやアセンブリ中の参照は、呼び出し側で正しいマッピングを維持してください。この判定は構文に基づく配置処理で、ポインターの流れ全体を解析するものではありません。共通バンクの容量制限も適用されます。

8.1 描画を書き換えるCHR RAM

--nes-chr-ram は、実行中にパターンを書き換える8 KiBのCHR RAMを使うカートリッジROMを生成します。ROMファイルへCHRデータは格納しないため、プログラムが初期化時にタイルをPPUへ書き込みます。wire3d.c はこの方式を使います。

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\wire3d_projection.c -I .\kitaqfc\lib --mapper=nrom --nes-chr-ram -o .\out\wire3d_projection.nes

--nes-chr=tiles.chr または --chr-rom=tiles.chr は、用意したパターンをCHR ROMとして組み込む指定です。--nes-chr-ram と同時には指定できません。CNROMの選択とも併用できません。対象マッパー・基板が必要なCHR RAMを備えることを確認してください。

CHRの指定をどれも省略すると、コンパイラは空の8 KiB CHR ROMを組み込みます。実行中にパターンを書き込むサンプルでは、--nes-chr-ram を明示してください。CHR RAMはCPU側のPRG RAMとは別のメモリーで、ワイヤーフレームのステージ用PRG RAMも別途必要です。

9 FDS・拡張音源・機器

FDSはディスクのファイル配置、起動方式、オーバーレイ、保存などを含む別の作業です。fds_manifest_sample.json とFDS関連ヘッダーを参照します。ディスクイメージを入力するエミュレータに、必要なBIOSや起動条件がある場合はその利用者自身の環境で準備します。本書の配布物にBIOSは含めません。

VRC6/VRC7音源を呼ぶだけでは、ROM側のマッパー設定は変わりません。選んだマッパーと実際に使う音源を一致させます。周辺機器は入力値が読めること、接続状態、通常のパッド入力への影響を別々に試します。

10 エラーとビルド結果

KQ番号を使う診断体系や symfind / src2asm / romdiff などの開発補助コマンドはGB版と似ています。ただしGB由来のヘルプに残る選択肢すべてがNES機能として実装済みとは限りません。FC巻の辞典はFC側のソース・ヘッダーで分けています。

KQ2421のようなPPU直接操作の警告は、画面OFFの初期化コードでも出ることがあります。警告を消すためだけに安全な初期化を崩さず、描画時刻と実行ログを確認します。エラー0と警告0を区別します。

11 参考例:生成AIを使ったゲーム開発

依頼内容を記入して、プロンプト全文を生成AIに渡してください。実装、エミュレータ検証、SARAKURA解析、修正後の再検証まで含みます。

· Markdown版をダウンロード

以下の「依頼内容」を記入し、このファイル全体を生成AIに渡してください。 コマンドは、kitaqfc、kurosaki、sarakura、kitaq-docs、game-fc が同じ親フォルダーにある配置を前提にしています。既存環境では実際のパスを使ってください。

依頼内容

  • ゲーム名:〈記入〉
  • ジャンル・遊びの中心となる仕組み:〈記入〉
  • プレイヤーが行う操作と、成功・失敗条件:〈記入〉
  • 必須の画面・ステージ・敵・アイテム:〈記入〉
  • 見た目、BGM、効果音:〈記入。資料がある場合はファイルも指定〉
  • 対象:〈NES/ファミコンのカートリッジ/FDS〉
  • マッパー・ROM規模・ミラーリング:〈指定。未定なら要件から選定する〉
  • 映像方式・性能目標:〈例:NTSC、通常時に毎秒60回のゲーム更新〉
  • 保存・周辺機器・その他の要件:〈記入。不要なら「なし」〉
  • プロジェクトの保存先:〈例:game-fc〉
  • 再配布条件:〈例:自作コードと素材をMITで公開できる状態にする〉

あなたに実行してほしいこと

KITAQFCと付属ライブラリで、上記のゲームを実装してください。 デバッグと実行検証にはKUROSAKI、診断の整理と修正前後の比較にはSARAKURAを使います。 「仕様を具体化 → 小さく実装 → ビルド → 操作して観測 → 原因を調べる → 修正 → 同条件で再検証」を、受け入れ条件を満たすまで繰り返してください。計画、コードの提示、コンパイル成功だけで完了にしないでください。

1. 環境と受け入れ条件を確定する

  1. 作業先の指示、各ツールのREADME、対象言語のHTMLマニュアル、使用するライブラリのヘッダーと実装を読んでください。実行ファイルの場所・バージョンまたはSHA-256を記録し、コマンドとAPIは実際の --help とソースで確認してください。
  2. 操作、画面、音、進行、更新頻度について、合格・不合格を判断できる受け入れ条件を書いてください。「STARTを押して離すと開始する」「被弾で残機が1減る」「ゲームオーバー後に通常の操作で再開できる」など、確認可能な形にしてください。
  3. 重要な仕様の曖昧さだけを確認し、通常の可逆な実装判断は自律的に進めてください。仕様や合格基準を勝手に弱めないでください。
  4. まず付属の小さなサンプルでコンパイラ・KUROSAKI・SARAKURAの接続を確認してください。これを依頼されたゲームの完成と扱わないでください。

2. マッパーと資源の構成を決める

  • 小さな構成ではNROMを検討し、規模やバンク切り替えの要件がある場合はMMC3などを選んでください。マッパー名だけで対応を判断せず、KUROSAKIの inspect-rom、mapper-info、audit-board と実装で必要な機能を確認してください。
  • PRG/CHR容量、CHR-ROMまたはCHR-RAM、ミラーリング、固定バンク、割り込みベクター、保存RAMを計画してください。最適化やバンク変更の後も、ヘッダーと実際の配置を照合してください。
  • --nes-local-ram を使う場合、対象はCPU内蔵RAM $0000–$07FF の範囲です。ゼロページ、スタック、OAMの作業領域、ライブラリ用領域との重複を確認してください。
  • VRAM転送キューの空き容量と、PPUのVRAM自体の容量を混同しないでください。1回のNMIで処理できる更新量も確認してください。
  • FDSではディスクアクセス、保存、BIOSなどの条件を別に検証し、通常のカートリッジと同じ起動条件だと仮定しないでください。

3. 小さく遊べる単位で実装する

  • 最初に「起動・タイトル・操作可能なプレイヤー・成功または失敗・再開」をつなぎ、その後に内容を増やしてください。
  • KITAQFCのC方言とFC用ライブラリを使ってください。GB版のライブラリやAPIがそのまま使えると仮定しないでください。エントリーポイントは void main(void) を使ってください。
  • 関数は宣言だけでなく対応する実装を確認し、必要な .c をビルド対象に含めてください。付属ヘッダーに宣言のみの項目があることにも注意してください。
  • PPUレジスター、NMI、OAM DMA、スプライトの走査線制限、スクロールとミラーリング、属性テーブル、APU/DMCの条件を踏まえて実装してください。
  • 英数字・記号には、提供された自作 ascii.c から変換したFC用フォントを使ってください。CHR、パレット、ネームテーブル、属性データと編集可能な元データを保存し、ビルドが実際にそれらを読み込むことを確認してください。
  • ソースのコメントは英語、作業報告は日本語で記述してください。SARAKURAの標準レポートは英語のまま利用してください。

4. 各反復でビルドと実行を結び付ける

game-fc/out/iter-001 のように出力先を分け、実行コマンド、終了コード、ROM・メタデータ・素材・ソース・ツールのハッシュを記録してください。 ビルド失敗後に残っている別のROMを実行しないでください。ビルド情報とデバッグ情報は同じROMに対応するものを使ってください。KITAQFCのビルドメタデータを、形式を確かめずにKUROSAKIの --kitaqfc-debug へ流用しないでください。

以下は親フォルダーから実行する、NROM・入力なしの動作確認例です。main.c、追加のライブラリ、CHRファイル、マッパーは実装に合わせて用意してください。

powershell
$iteration = '.\game-fc\out\iter-001'
New-Item -ItemType Directory -Force $iteration | Out-Null

# Include all additional implementation units required by the game.
& '.\kitaqfc\kitaqfc.exe' '.\game-fc\src\main.c' `
  -I '.\kitaqfc\lib' -o "$iteration\game.nes" `
  --mapper=nrom '--nes-chr=.\game-fc\assets\game.chr' --no-disasm `
  "--kurosaki-metadata=$iteration\build.json"
if ($LASTEXITCODE -ne 0) { throw 'Build failed; inspect the build log.' }

& '.\kurosaki\kurosaki.exe' run "$iteration\game.nes" `
  --frames 300 --pad1 0 --png "$iteration\frame.png" `
  --json "$iteration\run.json" --emit-diagnostics "$iteration\events.jsonl"
if ($LASTEXITCODE -ne 0) { throw 'Emulator run failed; inspect the run log.' }

& '.\sarakura\sarakura.exe' fc analyze `
  --metadata "$iteration\build.json" --events "$iteration\events.jsonl" `
  --frames 300 --out "$iteration\analysis" --fail-on error
if ($LASTEXITCODE -ne 0) { throw 'Inspect the analysis report and fix the cause.' }

300フレームの無入力実行はゲーム全体の検証ではありません。次の操作シナリオを必ず追加してください。

5. 操作シナリオを再現して検証する

  • KUROSAKIの --pad1 / --pad2 は生のNESビットマスクです。A=1、B=2、SELECT=4、START=8、UP=16、DOWN=32、LEFT=64、RIGHT=128です。ライブラリの BTN_* の値と混同しないでください。
  • run --pad1 は固定入力です。開始、移動、アクション、ポーズ、再開などの順序付き入力にはリプレイを用意し、押下・保持・解放を区別してください。
  • replay-record は公開CLIでは無入力の記録を作るコマンドです。人が操作した入力の録画だと扱わないでください。Replay / ReplayFrame の形式を読み、必要な入力列を作成してください。
  • 実行前にROMのSHA-256、リプレイの対象、フレーム範囲を自分で照合してください。replay-run --verify は期待ハッシュが存在する場合の比較であり、操作結果やROMの対応を包括的に検査するものではありません。テストを通すために期待値を実測値で無条件に上書きしないでください。
powershell
& '.\kurosaki\kurosaki.exe' replay-run `
  '.\game-fc\out\iter-001\game.nes' '.\game-fc\tests\start-and-play.replay.json' `
  --frames 900 --json '.\game-fc\out\iter-001\play.json' `
  --png '.\game-fc\out\iter-001\play.png' `
  --wav '.\game-fc\out\iter-001\play.wav'

リプレイファイルはそのビルドのROMに対応させて準備してください。修正前後で入力の意味と条件を揃え、ROMごとに適切な識別情報を記録してください。

順序付き入力とSARAKURAの接続: 公開CLIの replay-run には --emit-diagnostics がありません。存在しないオプションを追加して呼び出したり、CPUトレースを診断イベントとして渡したりしないでください。入力シナリオをSARAKURAで解析する際は、公開 kurosaki-core の RunOptions.replay_frames と diagnostic_events_from_trace_and_report を利用する検証ハーネスをプロジェクト配下に用意してください。同じROM・リプレイ・フレーム条件で実行し、その実行のトレースと診断レポートからJSONLを生成します。トレース設定・保存範囲を確認し、ハーネスとCLIリプレイの画面・観測結果も照合してください。無入力実行の診断を、操作シナリオの検証結果として流用しないでください。必要な環境がなく実施できない場合は未完了項目として報告してください。

6. 画面・音・状態・性能を確認する

  • 起動、開始、通常プレイ、衝突、スクロール、ステージ遷移、失敗、再開、ポーズ、保存など、仕様にある経路を通してください。1枚のPNGだけで動作を確認したことにしないでください。
  • 代表フレームと境界条件のPNG、入力、実行レポート、診断JSONL、WAV、必要に応じた状態・メモリ観測を保存してください。要求したフレームに到達したか、停止理由は何かも確認してください。
  • 画像は実際に開き、タイルや属性の境界、画面端、スクロール境界、スプライトが密集する場面を確認してください。残機、スコア、座標、状態遷移は期待値とも照合してください。
  • BGM、効果音、同時発音、途切れ、ポーズ・再開を確認してください。WAVの生成だけで音を正しいと判断しないでください。試聴できない場合は、実施した波形・数値検査と未確認事項を分けて報告してください。
  • 重い場面のNMI内処理、CPU負荷、転送量、ゲーム更新回数を測定してください。ホスト上のエミュレータ速度を実機やゲーム内の更新頻度と同一視しないでください。
  • --allow-unimplemented によって処理を続行できても、その機能を対応済みと判断しないでください。

7. SARAKURAで原因を絞り、同条件で再検証する

  • --metadata は対象ROMのビルド情報、--events は対象実行の診断JSONLです。--frames は解析条件の指定であり、SARAKURAがROMを実行する命令ではありません。
  • report.html、ai_diagnostics.json、repair_prompt.md、retest_plan.json を読み、診断を再現手順・画面・音・ソースと照合してください。ソース対応や原因の推定を確認済みの事実として扱わないでください。
  • 正常な待機と停止不具合を区別してください。警告をフィルターで隠したり、試験を短くしたりして合格にしないでください。未対応のイベントや解析範囲も記録してください。
  • 原因を最小再現例で絞り、必要な箇所を修正し、ビルドから再実行してください。ツール側が原因の場合はゲーム側の問題と切り分け、ツールの修正に回帰試験を付けてください。
  • 修正前後の入力、乱数種、マッパー、映像方式、観測フレーム、診断条件を揃えて比較してください。コードやRAM配置が変わったROMに、保存状態を無条件で流用しないでください。
powershell
& '.\sarakura\sarakura.exe' baseline-delta `
  --baseline '.\game-fc\out\iter-001\analysis' `
  --current '.\game-fc\out\iter-002\analysis' `
  --out '.\game-fc\out\delta.json' --markdown '.\game-fc\out\delta.md' `
  --fail-on-new error --fail-on-regression error --enforce

差分診断は、操作・表示・音の合格判定と併用してください。同じ失敗を繰り返す場合はログと仮説を見直してください。

8. 完了条件と納品

完成版のROMと同じソース・設定で、すべての必須シナリオを再実行してください。検証用の無敵状態や別のマッパーだけで完成版を検証済みにしないでください。 必須要件と試験の対応表、残る警告の理由、未対応・未確認事項を明記してください。実機で試していない場合は「実機未確認」と記載してください。

納品物は、ソース、使用ライブラリとツールの識別情報、編集可能な素材、ビルド・検証スクリプト、ROM、リプレイ・検証ハーネス、最終検証の証拠、起動方法・操作・既知の制限を記したREADMEです。 公開・外部送信は明示された範囲で行ってください。不要な中間ビルドや一時トレースは確認後に削除し、ソース、素材、最終成果物、必要な回帰証拠を保存してください。 環境や権限などの障害で必須検証を実施できない場合は、完了とせず、再現手順と必要な対応を具体的に報告してください。

完全なサンプルプログラム

各プログラムの本体、共通ヘッダー、指定ascii.cとその変換素材を同梱しています。説明中のm_*は本書専用ヘルパーです。ビルドと操作・期待結果を一組として参照してください。

一括ビルド用PowerShell / 実行結果・検証画像

英数字・記号の一覧表示 fc_font.c

目的:同梱の自作フォントで使える文字と、その表示方法を確認します。

処理の流れ:m_initで画面とフォントを準備し、m_textで文字列を行ごとに表示します。二重引用符は文字コード34をm_putへ渡して描きます。

操作と期待する結果:大文字26字、小文字26字、数字10字、記号30字が並びます。各文字が欠けずに表示されるか確認してください。操作は不要です。

英数字・記号の一覧表示: 大文字26字、小文字26字、数字10字、記号30字が並びます。各文字が欠けずに表示されるか確認してください。操作は不要です。
表示例:大文字26字、小文字26字、数字10字、記号30字が並びます。各文字が欠けずに表示されるか確認してください。操作は不要です。

ソースを保存 / 検証状況

powershell
.\kitaqfc\kitaqfc.exe  kitaq-docs/samples/fc_font.c -I kitaqfc/lib -I kitaq-docs/samples -o out/fc_font.nes --mapper=nrom --nes-chr=kitaq-docs/samples/font.chr
c
// Display the letters, digits and punctuation from the supplied ascii.c font.
// Expected: Uppercase, lowercase, numbers and all 30 supplied punctuation glyphs
#include "fc_common.h"


// Lay out every supplied character category, yielding between rows. The final
// m_put uses character code 34 to display the double quote separately.
void main(void) {
    m_init();
    m_text(2,3,"FONT");
    m_text(2,5,"ABCDEFGHIJKLMNOP"); m_wait();
    m_text(2,6,"QRSTUVWXYZ"); m_wait();
    m_text(2,8,"0123456789"); m_wait();
    m_text(2,10,"abcdefghijklmnop"); m_wait();
    m_text(2,11,"qrstuvwxyz"); m_wait();
    m_text(2,13,"!#$%&'()*+,-./"); m_wait();
    m_text(2,14,":;<=>?@[]^_`{}~"); m_wait();
    m_put(16,13,34); m_wait();
    while (1) { m_wait();  }
}
最初の文字表示 fc_hello.c

目的:ROMを起動し、文字列と数値を背景に表示する最小の流れを学びます。

処理の流れ:m_initの後にm_textで挨拶を描き、m_numberに42を渡します。最後のループは画面を保ったままフレームを待ちます。

操作と期待する結果:HELLO WORLDと042が表示されます。042は3桁にそろえた十進数42です。

最初の文字表示: HELLO WORLDと042が表示されます。042は3桁にそろえた十進数42です。
表示例:HELLO WORLDと042が表示されます。042は3桁にそろえた十進数42です。

ソースを保存 / 検証状況

powershell
.\kitaqfc\kitaqfc.exe  kitaq-docs/samples/fc_hello.c -I kitaqfc/lib -I kitaq-docs/samples -o out/fc_hello.nes --mapper=nrom --nes-chr=kitaq-docs/samples/font.chr
c
// Display a first message on screen.
// Expected: HELLO WORLD and 042
#include "fc_common.h"


// Initialize the display/font, write HELLO WORLD and a three-digit 042,
// then wait for frames so the result remains visible.
void main(void) {
    m_init();
    m_text(2,3,"HELLO");
    m_text(2,5,"HELLO WORLD");
    m_number(42);
    while (1) { m_wait();  }
}
桁あふれを避ける算術 fc_arithmetic.c

目的:8ビットに収まらない途中結果を16ビットで扱う方法を学びます。

処理の流れ:(u16)200 + 100で300を作り、10で割ってから表示用にu8へ変換します。足し算の前の型変換に注目してください。

操作と期待する結果:030、つまり計算結果30が表示されます。

桁あふれを避ける算術: 030、つまり計算結果30が表示されます。
表示例:030、つまり計算結果30が表示されます。

ソースを保存 / 検証状況

powershell
.\kitaqfc\kitaqfc.exe  kitaq-docs/samples/fc_arithmetic.c -I kitaqfc/lib -I kitaq-docs/samples -o out/fc_arithmetic.nes --mapper=nrom --nes-chr=kitaq-docs/samples/font.chr
c
// Variables, arithmetic and type conversion.
// Expected: 030
#include "fc_common.h"


// Widen before adding 200 + 100, divide the 16-bit sum, and display 30.
// The cast demonstrates avoiding an 8-bit intermediate result.
void main(void) {
    m_init();
    m_text(2,3,"ARITHMETIC");
    u16 sum;
    sum = (u16)200 + 100;
    m_number((u8)(sum / 10));
    while (1) { m_wait();  }
}
条件分岐と繰り返し fc_control.c

目的:for、continue、while、if/elseを小さな計算で試します。

処理の流れ:0~7のうち3を飛ばして合計25を作り、whileで32まで増やします。ifで32との一致を判定し、成功側で42を代入します。

操作と期待する結果:042が表示されます。このプログラムで使う分岐はif/elseです。

条件分岐と繰り返し: 042が表示されます。このプログラムで使う分岐はif/elseです。
表示例:042が表示されます。このプログラムで使う分岐はif/elseです。

ソースを保存 / 検証状況

powershell
.\kitaqfc\kitaqfc.exe  kitaq-docs/samples/fc_control.c -I kitaqfc/lib -I kitaq-docs/samples -o out/fc_control.nes --mapper=nrom --nes-chr=kitaq-docs/samples/font.chr
c
// Conditional branches and loops.
// Expected: 042
#include "fc_common.h"


// Exercise loop control and a final branch, then display the selected value 42.
void main(void) {
    m_init();
    m_text(2,3,"CONTROL");
    u8 i; u8 sum; sum=0;
    for (i=0;i<8;i++) { if (i==3) continue; sum=(u8)(sum+i); }
    while (sum<32) { sum++; }
    if (sum==32) sum=42; else sum=0;
    m_number(sum);
    while (1) { m_wait();  }
}
関数・配列・構造体・ポインタ fc_aggregate.c

目的:複数の値をまとめ、関数やポインタを通して扱う方法を学びます。

処理の流れ:Pointの(10,20)を別の構造体へコピーし、ポインタから配列に1~4を書きます。addの戻り値30に配列の1、2、4と定数5を加えます。

操作と期待する結果:合計42が042と表示されます。構造体コピー、ptr[i]による書き込み、関数の戻り値を順に追ってください。

関数・配列・構造体・ポインタ: 合計42が042と表示されます。構造体コピー、ptr[i]による書き込み、関数の戻り値を順に追ってください。
表示例:合計42が042と表示されます。構造体コピー、ptr[i]による書き込み、関数の戻り値を順に追ってください。

ソースを保存 / 検証状況

powershell
.\kitaqfc\kitaqfc.exe  kitaq-docs/samples/fc_aggregate.c -I kitaqfc/lib -I kitaq-docs/samples -o out/fc_aggregate.nes --mapper=nrom --nes-chr=kitaq-docs/samples/font.chr
c
// Functions, arrays, structures and pointers.
// Expected: 042
#include "fc_common.h"

typedef struct { u8 x; u8 y; } Point;
// Add two byte values and explicitly narrow the returned result to u8.
u8 add(u8 a,u8 b) { return (u8)(a+b); }
// Copy a structure, fill an array through a pointer, and combine selected values
// through add() to produce 42. Keep the final screen visible in the idle loop.
void main(void) {
    m_init();
    m_text(2,3,"AGGREGATE");
    Point a; Point b; u8 i; u8 data[4]; u8* ptr;
    a.x=10; a.y=20; b=a; ptr=data;
    for(i=0;i<4;i++) ptr[i]=(u8)(i+1);
    m_number((u8)(add(b.x,b.y)+ptr[0]+ptr[1]+ptr[3]+5));
    while (1) { m_wait();  }
}
ビット操作とバッファのコピー fc_bits_memory.c

目的:複数のフラグを1バイトにまとめ、バッファを別の領域へコピーします。

処理の流れ:__memsetで4バイトを0にし、ビット3を立て、ビット1を反転します。__memcpyで4バイトすべてを別配列にコピーします。

操作と期待する結果:コピー先の先頭バイトが010になります。ビット3の8とビット1の2を合わせた十進数10です。

ビット操作とバッファのコピー: コピー先の先頭バイトが010になります。ビット3の8とビット1の2を合わせた十進数10です。
表示例:コピー先の先頭バイトが010になります。ビット3の8とビット1の2を合わせた十進数10です。

ソースを保存 / 検証状況

powershell
.\kitaqfc\kitaqfc.exe  kitaq-docs/samples/fc_bits_memory.c -I kitaqfc/lib -I kitaq-docs/samples -o out/fc_bits_memory.nes --mapper=nrom --nes-chr=kitaq-docs/samples/font.chr
c
// Bit operations and memory copying.
// Expected: 010
#include "fc_common.h"


// Clear the source bytes, set bits 3 and 1, copy the buffer, and display 8 + 2 = 10.
void main(void) {
    m_init();
    m_text(2,3,"BITS MEMORY");
    u8 data[4]; u8 copy[4];
    __memset(data,0,4); __bit_set(data,3); __bit_toggle(data,1);
    __memcpy(copy,data,4); m_number(copy[0]);
    while (1) { m_wait();  }
}
押した瞬間だけを数える fc_input.c

目的:ボタンを押し続ける状態と、新しく押した瞬間を区別します。

処理の流れ:毎フレームinput_updateを呼び、input_pressed(BTN_A)が真のときだけカウンターを増やします。

操作と期待する結果:最初は000です。Aを押すと001になり、押し続けても増えません。離して押し直すと002になります。255の次は000に戻ります。

押した瞬間だけを数える: 最初は000です。Aを押すと001になり、押し続けても増えません。離して押し直すと002になります。255の次は000に戻ります。
表示例:最初は000です。Aを押すと001になり、押し続けても増えません。離して押し直すと002になります。255の次は000に戻ります。

ソースを保存 / 検証状況

powershell
.\kitaqfc\kitaqfc.exe kitaqfc/lib/input.c kitaq-docs/samples/fc_input.c -I kitaqfc/lib -I kitaq-docs/samples -o out/fc_input.nes --mapper=nrom --nes-chr=kitaq-docs/samples/font.chr
c
// Distinguish a new button press from a held button.
// Expected: 000 initially; A increments once per press
#include "fc_common.h"
#include "input.h"
u8 manual_count;
// Sample buttons once per frame and increment only on a new A press.
// Holding A does not repeatedly increment; the byte-sized counter wraps after 255.
void main(void) {
    m_init();
    m_text(2,3,"INPUT");
    input_init();
    m_number(0);
    while (1) { m_wait(); input_update(); if(input_pressed(BTN_A)) { manual_count++; m_number(manual_count); } }
}
Q8.8固定小数点の掛け算 fc_fixed.c

目的:整数とQ8.8固定小数点の相互変換、および掛け算の基本を学びます。

処理の流れ:3と4をfix_from_intでQ8.8へ変換し、fix_mulで掛け、fix_to_intで整数へ戻します。

操作と期待する結果:3×4の結果12が012と表示されます。画面に出るのは内部の256倍された値ではなく、整数へ戻した値です。

Q8.8固定小数点の掛け算: 3×4の結果12が012と表示されます。画面に出るのは内部の256倍された値ではなく、整数へ戻した値です。
表示例:3×4の結果12が012と表示されます。画面に出るのは内部の256倍された値ではなく、整数へ戻した値です。

ソースを保存 / 検証状況

powershell
.\kitaqfc\kitaqfc.exe kitaqfc/lib/fixed.c kitaq-docs/samples/fc_fixed.c -I kitaqfc/lib -I kitaq-docs/samples -o out/fc_fixed.nes --mapper=nrom --nes-chr=kitaq-docs/samples/font.chr
c
// Calculate with signed Q8.8 fixed-point values.
// Expected: 012
#include "fc_common.h"
#include "fixed.h"

// Convert 3 and 4 to Q8.8, multiply them, convert the result back to an
// integer, and display 12.
void main(void) {
    m_init();
    m_text(2,3,"FIXED");
    fix8 a; fix8 b; a=fix_from_int(3); b=fix_from_int(4);
    m_number((u8)fix_to_int(fix_mul(a,b)));
    while (1) { m_wait();  }
}
固定長プールからの生成 fc_entity.c

目的:動的メモリ確保を使わず、決まった個数の領域からオブジェクトを作ります。

処理の流れ:entity_initで初期化し、種類1のオブジェクトを(20,30)に作ります。IDが失敗値0xFFでないことを確認してvxへ2を設定します。

操作と期待する結果:001は使用中オブジェクトの個数です。IDや座標ではありません。この例は更新・描画を行わないので、動くキャラクターは表示されません。

固定長プールからの生成: 001は使用中オブジェクトの個数です。IDや座標ではありません。この例は更新・描画を行わないので、動くキャラクターは表示されません。
表示例:001は使用中オブジェクトの個数です。IDや座標ではありません。この例は更新・描画を行わないので、動くキャラクターは表示されません。

ソースを保存 / 検証状況

powershell
.\kitaqfc\kitaqfc.exe kitaqfc/lib/entity.c kitaq-docs/samples/fc_entity.c -I kitaqfc/lib -I kitaq-docs/samples -o out/fc_entity.nes --mapper=nrom --nes-chr=kitaq-docs/samples/font.chr
c
// Allocate an entity from a fixed-capacity object pool.
// Expected: 001
#include "fc_common.h"
#include "entity.h"

// Create one entity and check the 0xFF allocation-failure sentinel before
// accessing its velocity. Display the pool occupancy, not the entity ID.
void main(void) {
    m_init();
    m_text(2,3,"ENTITY");
    u8 id; entity_init(); id=entity_create(1,20,30);
    if(id!=0xFF) entity_get(id)->vx=2; m_number(entity_count_active());
    while (1) { m_wait();  }
}
診断値をRAMへ記録する fc_debug.c

目的:画面表示とは別に、ゲーム中の変数を診断用バッファへ残します。

処理の流れ:debug_initで初期化し、フレームタグ1を設定してHPという名前で42を記録します。

操作と期待する結果:画面の001は記録件数です。HP=42という値そのものは画面ではなく診断バッファにあります。画面だけでは記録内容までは確認できません。

診断値をRAMへ記録する: 画面の001は記録件数です。HP=42という値そのものは画面ではなく診断バッファにあります。画面だけでは記録内容までは確認できません。
表示例:画面の001は記録件数です。HP=42という値そのものは画面ではなく診断バッファにあります。画面だけでは記録内容までは確認できません。

ソースを保存 / 検証状況

powershell
.\kitaqfc\kitaqfc.exe kitaqfc/lib/debug.c kitaq-docs/samples/fc_debug.c -I kitaqfc/lib -I kitaq-docs/samples -o out/fc_debug.nes --mapper=nrom --nes-chr=kitaq-docs/samples/font.chr
c
// Record diagnostic values in the debug trace.
// Expected: 001; trace log contains HP=42
#include "fc_common.h"
#include "debug.h"

// Record HP = 42 with frame tag 1 and display the number of trace entries.
// The trace buffer contains the value; the screen displays the count, not HP.
void main(void) {
    m_init();
    m_text(2,3,"DEBUG");
    debug_init(); debug_set_frame(1); debug_trace_u8("HP",42);
    m_number(debug_get_trace_count());
    while (1) { m_wait();  }
}
座標履歴のリングバッファ fc_chain.c

目的:キャラクターの軌跡などに使う座標履歴の格納方法を学びます。

処理の流れ:4点分の配列をchain_initに渡し、chain_push_headで(20,30)を1点追加します。

操作と期待する結果:042は処理後に表示する固定の目印です。保存した座標や件数を表していません。履歴の内容を確かめるにはtrailとpointsを観察します。追従する胴体の描画例ではありません。

座標履歴のリングバッファ: 042は処理後に表示する固定の目印です。保存した座標や件数を表していません。履歴の内容を確かめるにはtrailとpointsを観察します。追従する胴体の描画例ではありません。
表示例:042は処理後に表示する固定の目印です。保存した座標や件数を表していません。履歴の内容を確かめるにはtrailとpointsを観察します。追従する胴体の描画例ではありません。

ソースを保存 / 検証状況

powershell
.\kitaqfc\kitaqfc.exe kitaqfc/lib/chain.c kitaq-docs/samples/fc_chain.c -I kitaqfc/lib -I kitaq-docs/samples -o out/fc_chain.nes --mapper=nrom --nes-chr=kitaq-docs/samples/font.chr
c
// Store a position history in a ring buffer.
// Expected: 042; history contains (20,30)
#include "fc_common.h"
#include "chain.h"
Chain trail; ChainPoint points[4];
// Attach four history slots and record one point. The displayed 42 is a fixed
// completion marker; inspect the chain to verify the stored (20,30) point.
void main(void) {
    m_init();
    m_text(2,3,"CHAIN");
    chain_init(&trail,points,4); chain_push_head(&trail,20,30);
    m_number(42);
    while (1) { m_wait();  }
}
ライブラリのフレームカウンター fc_system.c

目的:フレーム待機とライブラリが管理する経過フレーム数の関係を確認します。

処理の流れ:system_initの後にsystem_wait_vblankを2回呼び、system_get_frame8の値を表示します。

操作と期待する結果:002が表示されます。その後のループは本書のm_waitを使うため、この表示値は増え続けません。

ライブラリのフレームカウンター: 002が表示されます。その後のループは本書のm_waitを使うため、この表示値は増え続けません。
表示例:002が表示されます。その後のループは本書のm_waitを使うため、この表示値は増え続けません。

ソースを保存 / 検証状況

powershell
.\kitaqfc\kitaqfc.exe kitaqfc/lib/system.c kitaq-docs/samples/fc_system.c -I kitaqfc/lib -I kitaq-docs/samples -o out/fc_system.nes --mapper=nrom --nes-chr=kitaq-docs/samples/font.chr
c
// Count completed frame waits.
// Expected: 002
#include "fc_common.h"
#include "system.h"

// Reset the library frame count, perform two counted waits, and display 2.
// Subsequent m_wait calls leave that library counter unchanged.
void main(void) {
    m_init();
    m_text(2,3,"SYSTEM");
    system_init(); system_wait_vblank(); system_wait_vblank();
    m_number(system_get_frame8());
    while (1) { m_wait();  }
}
現在のシーンIDを選ぶ fc_scene.c

目的:タイトル画面やゲーム本編などを区別するシーン番号の選択方法を学びます。

処理の流れ:0で初期化された2件のSceneDefを登録し、scene_change(1)でID 1を選択します。

操作と期待する結果:001はscene_get_currentが返す現在のIDです。この例にはシーンのコールバックや画面切り替えの描画は登録していません。

現在のシーンIDを選ぶ: 001はscene_get_currentが返す現在のIDです。この例にはシーンのコールバックや画面切り替えの描画は登録していません。
表示例:001はscene_get_currentが返す現在のIDです。この例にはシーンのコールバックや画面切り替えの描画は登録していません。

ソースを保存 / 検証状況

powershell
.\kitaqfc\kitaqfc.exe kitaqfc/lib/runtime.c kitaqfc/lib/scene.c kitaq-docs/samples/fc_scene.c -I kitaqfc/lib -I kitaq-docs/samples -o out/fc_scene.nes --mapper=nrom --nes-chr=kitaq-docs/samples/font.chr
c
// The scene streaming module shares runtime.c queue symbols, even though this demo only changes IDs.
#define MANUAL_FC_RUNTIME 1
// Switch the current scene ID.
// Expected: 001; no callbacks registered
#include "fc_common.h"
#include "scene.h"
SceneDef manual_scenes[2];
// Register two zero-initialized scene entries, select ID 1 and display it.
// No callbacks are registered in this example.
void main(void) {
    m_init();
    m_text(2,3,"SCENE");
    scene_init(manual_scenes,2); scene_change(1);
    m_number(scene_get_current());
    while (1) { m_wait();  }
}
Q5.3で端数の移動を積み重ねる fc_subpixel.c

目的:1ピクセル未満の移動を端数として保持し、整数座標へ繰り上げる方法を学びます。

処理の流れ:X=40から、2/8ピクセルの移動を8回加算します。右シフトで整数部分を取り出し、マスクで端数を残します。

操作と期待する結果:合計2ピクセル進み、X=42が042と表示されます。これは正方向の手動積算例です。負方向には対応する繰り下げ処理が必要です。

Q5.3で端数の移動を積み重ねる: 合計2ピクセル進み、X=42が042と表示されます。これは正方向の手動積算例です。負方向には対応する繰り下げ処理が必要です。
表示例:合計2ピクセル進み、X=42が042と表示されます。これは正方向の手動積算例です。負方向には対応する繰り下げ処理が必要です。

ソースを保存 / 検証状況

powershell
.\kitaqfc\kitaqfc.exe  kitaq-docs/samples/fc_subpixel.c -I kitaqfc/lib -I kitaq-docs/samples -o out/fc_subpixel.nes --mapper=nrom --nes-chr=kitaq-docs/samples/font.chr
c
// Accumulate Q5.3 eighth-pixel motion into an integer coordinate.
// Expected: 042; explicit game-side integration using header constants
#include "fc_common.h"
#include "physics2d.h"

// Integrate eight increments of 2/8 pixel. Carry whole pixels into X and
// retain the fractional remainder with the supplied mask, moving X from 40 to 42.
void main(void) {
    m_init();
    m_text(2,3,"SUBPIXEL");
    KQ2DPosition x; KQ2DFraction frac; KQ2DSpeed speed; u8 i;
    // This example demonstrates positive motion only; signed negative motion needs a matching carry/borrow path.
    x=40; frac=0; speed=2;
    for(i=0;i<8;i++) {
        frac=(u8)(frac+speed);
        x=(u8)(x+(frac>>KQ2D_SUBPIXEL_BITS));
        frac=(u8)(frac & KQ2D_SUBPIXEL_MASK);
    }
    m_number(x);
    while (1) { m_wait();  }
}
NES背景を横へスクロールする fc_scroll.c

目的:PPUのスクロール位置を更新して背景を動かします。

処理の流れ:__scroll_set(0,0)で開始し、フレームごとにphaseを1増やして横位置へ設定します。

操作と期待する結果:SCROLLという文字の横位置が変わります。phaseは255から0へ戻ります。複数のネームテーブルを使う広いマップの例ではありません。

NES背景を横へスクロールする: SCROLLという文字の横位置が変わります。phaseは255から0へ戻ります。複数のネームテーブルを使う広いマップの例ではありません。
表示例:SCROLLという文字の横位置が変わります。phaseは255から0へ戻ります。複数のネームテーブルを使う広いマップの例ではありません。

ソースを保存 / 検証状況

powershell
.\kitaqfc\kitaqfc.exe  kitaq-docs/samples/fc_scroll.c -I kitaqfc/lib -I kitaq-docs/samples -o out/fc_scroll.nes --mapper=nrom --nes-chr=kitaq-docs/samples/font.chr
c
// Scroll the NES background horizontally.
// Expected: title scrolls horizontally
#include "fc_common.h"

u8 phase;
// Advance an 8-bit horizontal scroll position once per frame, wrapping naturally
// after 255. Background motion demonstrates the changing scroll register.
void main(void) {
    m_init();
    m_text(2,3,"SCROLL");
    __scroll_set(0,0);
    while (1) { m_wait(); phase++; __scroll_set(phase,0); }
}
NMIでタイル更新を反映する fc_vram_queue.c

目的:メイン処理からPPU更新をキューへ送り、NMI側で反映する流れを学びます。

処理の流れ:__vramq_putでネームテーブルの0x2103へタイル52を予約し、__vramq_commitで送信します。0x2103はタイル座標(3,8)です。

操作と期待する結果:背景の(3,8)に数字4が表示されます。番号52は同梱フォントの数字4に対応します。

NMIでタイル更新を反映する: 背景の(3,8)に数字4が表示されます。番号52は同梱フォントの数字4に対応します。
表示例:背景の(3,8)に数字4が表示されます。番号52は同梱フォントの数字4に対応します。

ソースを保存 / 検証状況

powershell
.\kitaqfc\kitaqfc.exe  kitaq-docs/samples/fc_vram_queue.c -I kitaqfc/lib -I kitaq-docs/samples -o out/fc_vram_queue.nes --mapper=nrom --nes-chr=kitaq-docs/samples/font.chr
c
// Submit a VRAM update for the NMI handler.
// Expected: 4 at (3,8)
#include "fc_common.h"


// Queue tile 52 at nametable address 0x2103, corresponding to (3,8), and
// commit the queue so the NMI update path displays the digit 4.
void main(void) {
    m_init();
    m_text(2,3,"VRAM QUEUE");
    __vramq_put(0x2103,52); __vramq_commit();
    while (1) { m_wait();  }
}
NESのOAMスプライト fc_sprite.c

目的:スプライト用の色とOAM情報を準備して、背景とは別の画像を表示します。

処理の流れ:スプライトパレットを読み込み、スロット0へ位置(70,80)、タイル65、属性0を設定します。表示を有効にしてOAM DMAを繰り返します。

操作と期待する結果:文字Aのスプライトが固定位置に表示されます。動く画像ではなく、OAM設定と転送の基本例です。

NESのOAMスプライト: 文字Aのスプライトが固定位置に表示されます。動く画像ではなく、OAM設定と転送の基本例です。
表示例:文字Aのスプライトが固定位置に表示されます。動く画像ではなく、OAM設定と転送の基本例です。

ソースを保存 / 検証状況

powershell
.\kitaqfc\kitaqfc.exe  kitaq-docs/samples/fc_sprite.c -I kitaqfc/lib -I kitaq-docs/samples -o out/fc_sprite.nes --mapper=nrom --nes-chr=kitaq-docs/samples/font.chr
c
// Display a sprite using NES OAM.
// Expected: A sprite on screen
#include "fc_common.h"


// Load the sprite palette, assign font tile 65 to OAM slot zero, enable
// background/sprite display, and transfer OAM after each frame wait.
void main(void) {
    m_init();
    m_text(2,3,"SPRITE");
    __palette_sp_load(manual_pal); __sprite_set(0,70,80,65,0);
    __ppu_mask_set(0x1E);
    while (1) { m_wait(); __oam_dma(); }
}
APUのパルス音を鳴らす fc_sound.c

目的:NES本体のパルスチャンネルへ音量・タイマー・長さを設定します。

処理の流れ:nes_apu_initの後にnes_sfx_square1(0xBF,400,20)を呼びます。0xBFは一定音量と長さカウンター停止を指定するため、音は持続します。20は長さテーブルの番号で、20フレームという意味ではありません。

操作と期待する結果:042を表示しながらパルス音が鳴り続けます。音を止めるにはnes_apu_silence_allを呼びます。042自体は発音確認の結果ではありません。

APUのパルス音を鳴らす: 042を表示しながらパルス音が鳴り続けます。音を止めるにはnes_apu_silence_allを呼びます。042自体は発音確認の結果ではありません。
表示例:042を表示しながらパルス音が鳴り続けます。音を止めるにはnes_apu_silence_allを呼びます。042自体は発音確認の結果ではありません。

ソースを保存 / 検証状況

powershell
.\kitaqfc\kitaqfc.exe kitaqfc/lib/audio.c kitaq-docs/samples/fc_sound.c -I kitaqfc/lib -I kitaq-docs/samples -o out/fc_sound.nes --mapper=nrom --nes-chr=kitaq-docs/samples/font.chr
c
// Play a pulse-channel effect on the built-in NES APU.
// Expected: 042 and a pulse tone
#include "fc_common.h"
#include "audio.h"

// Initialize the APU, trigger pulse channel 1 with the chosen timer/length
// values, and leave the screen displaying 42 while the hardware plays the tone.
void main(void) {
    m_init();
    m_text(2,3,"SOUND");
    // Control BF selects constant volume and halts the length counter, so this tone sustains.
    // Call nes_apu_silence_all when the game should stop it; 20 is a length-table index, not frames.
    nes_apu_init(); nes_sfx_square1(0xBF,400,20);
    m_number(42);
    while (1) { m_wait();  }
}

組み込み命令辞典

全161項目。各項目では、機能、引数、戻り値、制約、使用例と期待する結果を説明します。

audio_vblank.h — VBlank IRQによる音楽再生

NMIで進む4音源の音楽ライブラリです。初期化→曲の設定と初回補充→NMIを有効にして再生→前景でrefillの順で使います。NMIはRAMキューを読むだけなので、補充を続ければ長い曲も小さなRAM使用量で再生できます。各音源は独立したHOLD/STOPを持ちます。

1レコードは5バイトで、待機時間, CH1, CH2, CH3, CH4の順です。待機時間1~255は、その音を維持するNMI回数です。0は終端で、残り4バイトは無視します。CH1~CH3の音階0~71はC2~B7(24=C4)。CH4の0~15は通常ノイズの周期番号、16~31は短周期モードです。各音源の255=NES_AUDIO_HOLDは変更せず、254=NES_AUDIO_STOPは停止します。

NTSC向けの4音源ドライバーです。CH1=パルス1、CH2=パルス2、CH3=三角波、CH4=ノイズで、チャンネル添字は0~3です。DMCと拡張音源は再生しません。audio.cの効果音関数など、同じAPUを直接書き換える処理とは同時に使わないでください。PAL用の音程表やテンポ補正はありません。

待機時間が尽きて次のBGMレコードがなければBGMの音を停止し、補充後のNMIで再開します。独立したSFXは進み続けます。補充不足は空の状態へ移った回数として数えます。HOLDだけを補充しても停止前のBGM音は復元しません。通常は毎フレームrefillを呼び、重い処理の前に十分な待機時間をキューへ入れてください。

fds.h — FDSディスク操作

__fds_available

ビルド対象がFDSかどうかを返します。共通のゲームコードで対象別の案内を表示する場合に使います。

書式

c
u8 __fds_available(void);

戻り値

FDS向けなら1、それ以外なら0。実行時に周辺機器を検出する処理ではありません。

使用条件と制限

この関数はNROMなどのカートリッジ向けにも使用できます。ただし、if (__fds_available())の中にFDS専用APIを書いても、そのAPIの対象制限を回避できるわけではありません。対象別のビルドではソースの構成も分けてください。

掲載画像はKUROSAKIでディスクの起動ファイルをRAMへ直接配置して確認したものです。このサンプルはBIOSを呼ばず、ファイルの読み書き、ディスク交換、実機での起動を検証していません。検証用の空のファームウェアはBIOSの代用品ではありません。サンプルは画面更新用NMIを使い、不要なマスク可能IRQを無効にしています。

ビルド例ではBIOSの起動画面用の追加ファイルを付けず、ネイティブFDS配置の.fdsを作ります。KUROSAKIの直接起動で確認するための構成です。実機用ディスクの起動手順は、これとは別に確認してください。

使用例

c
void main(void) {
    // These BIOS-free queries need NMI for display, but no maskable IRQ.
    __irq_disable();
    fds_result[0] = __fds_available();
    m_init();
    m_text(2,2,"FDS BUILD SELECTION"); m_wait();
    m_text(2,5,"FDS=1 / CARTRIDGE=0"); m_wait();
    m_number(fds_result[0]); m_wait();
    while (1) { m_wait(); }
}

呼び出し部分のコード例です。完全なプログラムに組み込む際は、記載した初期化とバッファの条件を満たしてください。

期待する結果

FDS向けでは001、NROM向けでは000を表示します。同じCソースでもビルド時の対象によって値が変わり、ディスクの有無を調べているわけではないことを示します。

__fds_available: AVAILABLE-FDS-DEFAULT — FDS向けでは001、NROM向けでは000を表示します。同じCソースでもビルド時の対象によって値が変わり、ディスクの有無を調べているわけではないことを示します。
AVAILABLE-FDS-DEFAULT — FDS向けでは001、NROM向けでは000を表示します。同じCソースでもビルド時の対象によって値が変わり、ディスクの有無を調べているわけではないことを示します。
__fds_available: AVAILABLE-NROM-DEFAULT — FDS向けでは001、NROM向けでは000を表示します。同じCソースでもビルド時の対象によって値が変わり、ディスクの有無を調べているわけではないことを示します。
AVAILABLE-NROM-DEFAULT — FDS向けでは001、NROM向けでは000を表示します。同じCソースでもビルド時の対象によって値が変わり、ディスクの有無を調べているわけではないことを示します。

使用例の完全なプログラム

ビルドコマンド

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\fds_available.c -I .\kitaqfc\lib -I .\kitaq-docs\samples -o .\out\fds_available.fds --mapper=fds --fds-layout=fds32 --fds-no-license-bypass --nes-chr=.\kitaq-docs\samples\font.chr --no-cache --no-disasm
# Cartridge comparison
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\fds_available.c -I .\kitaqfc\lib -I .\kitaq-docs\samples -o .\out\fds_available.nes --mapper=nrom --nes-chr=.\kitaq-docs\samples\font.chr --no-cache --no-disasm
実装とソース

kitaqfc/lib/fds.h:7

宣言の注記(原文)
Return the compile-time FDS mapper selection as 0 or 1; this is not a hardware probe.

kitaqfc/kitaqfc/CodeGenerator.cs:5146

csharp
if (funcName == "__fds_available")
            {
                if (!Need(0)) return true;
                EmitAsm("LDA", Imm(isFds ? 1 : 0));
                return true;
            }
__fds_disk_ready

ディスクが挿入されているかを、その場で調べて返します。挿入待ちの画面で毎フレーム呼ぶなど、処理を止めずに存在を確認できます。

書式

c
u8 __fds_disk_ready(void);

戻り値

$4032のビット0が0なら1、1なら0を返します。

使用条件と制限

--mapper=fdsでビルドしてください。__fds_available以外のFDS専用関数をカートリッジ向けに呼ぶと、KQFC2420でビルドを拒否します。

判定対象は$4032のビット0(ディスクの有無)です。ビット1の転送準備状態、ファイルの存在、ロード完了は確認しません。名前のreadyを「読み書きを開始できる」と解釈しないでください。

KUROSAKIのFDSコントローラーは簡易モデルです。この例では挿入状態が1、書き込み禁止マスクが0、ステータスが0になりますが、未挿入・保護付きディスク・CRC異常への応答はこの画面で確認したことにはなりません。

掲載画像はKUROSAKIでディスクの起動ファイルをRAMへ直接配置して確認したものです。このサンプルはBIOSを呼ばず、ファイルの読み書き、ディスク交換、実機での起動を検証していません。検証用の空のファームウェアはBIOSの代用品ではありません。サンプルは画面更新用NMIを使い、不要なマスク可能IRQを無効にしています。

ビルド例ではBIOSの起動画面用の追加ファイルを付けず、ネイティブFDS配置の.fdsを作ります。KUROSAKIの直接起動で確認するための構成です。実機用ディスクの起動手順は、これとは別に確認してください。

NESdev: FDS hardware registers

使用例

c
void main(void) {
    // These BIOS-free queries need NMI for display, but no maskable IRQ.
    __irq_disable();
    fds_result[0] = __fds_disk_ready();
    fds_result[1] = __fds_side();
    fds_result[2] = __fds_error();
    fds_result[3] = 0;
    // Never enter an unbounded wait without first checking disk presence.
    if (fds_result[0]) {
        __fds_wait_ready(); fds_result[3]++;
        __fds_wait_insert(); fds_result[3]++;
    }
    m_init();
    m_text(2,2,"FDS REGISTER STATUS"); m_wait();
    m_text(2,5,"PRESENT / PROTECT / STATUS"); m_wait();
    m_put(2,7,'0'+fds_result[0]);
    m_put(12,7,'0'+fds_result[1]);
    m_put(22,7,'0'+fds_result[2]); m_wait();
    m_text(2,10,"COMPLETED PRESENCE WAITS"); m_wait();
    m_put(2,12,'0'+fds_result[3]); m_wait();
    m_text(2,15,"NO FILE TRANSFER TESTED"); m_wait();
    while (1) { m_wait(); }
}

呼び出し部分のコード例です。完全なプログラムに組み込む際は、記載した初期化とバッファの条件を満たしてください。

期待する結果

PRESENT / PROTECT / STATUSの下に1 / 0 / 0、COMPLETED PRESENCE WAITSの下に2が出ます。二つの待機関数が戻ったことを示し、ファイル転送の成功表示ではありません。最下部にもNO FILE TRANSFER TESTEDと表示します。

__fds_disk_ready: STATUS-FDS-DEFAULT — PRESENT / PROTECT / STATUSの下に1 / 0 / 0、COMPLETED PRESENCE WAITSの下に2が出ます。二つの待機関数が戻ったことを示し、ファイル転送の成功表示ではありません。最下部にもNO FILE TRANSFER TESTEDと表示します。
STATUS-FDS-DEFAULT — PRESENT / PROTECT / STATUSの下に1 / 0 / 0、COMPLETED PRESENCE WAITSの下に2が出ます。二つの待機関数が戻ったことを示し、ファイル転送の成功表示ではありません。最下部にもNO FILE TRANSFER TESTEDと表示します。

使用例の完全なプログラム

ビルドコマンド

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\fds_status.c -I .\kitaqfc\lib -I .\kitaq-docs\samples -o .\out\fds_status.fds --mapper=fds --fds-layout=fds32 --fds-no-license-bypass --nes-chr=.\kitaq-docs\samples\font.chr --no-cache --no-disasm
実装とソース

kitaqfc/lib/fds.h:9

宣言の注記(原文)
Return the inverted disk-absent bit from 4032 as 0 or 1; this checks presence, not transfer completion.

kitaqfc/kitaqfc/CodeGenerator.cs:5160

csharp
case "__fds_disk_ready":
                    if (!Need(0)) return true;
                    EmitAsm("LDA", Mem(0x4032));
                    EmitAsm("AND", Imm(0x01));
                    EmitAsm("EOR", Imm(0x01));
                    return true;
                // Expose status bit 2 as the raw mask value 0 or 4, rather than a normalized Boolean.
__fds_error

FDSのハードウェアステータスレジスター$4030を読みます。ディスク操作の前回エラーを保存しておく変数ではありません。

書式

c
u8 __fds_error(void);

戻り値

$4030の8ビット値をそのまま返します。BIOSのエラーコードは__fds_load_file()や__fds_save_file()の戻り値を保存して扱ってください。

使用条件と制限

--mapper=fdsでビルドしてください。__fds_available以外のFDS専用関数をカートリッジ向けに呼ぶと、KQFC2420でビルドを拒否します。

KUROSAKIのFDSコントローラーは簡易モデルです。この例では挿入状態が1、書き込み禁止マスクが0、ステータスが0になりますが、未挿入・保護付きディスク・CRC異常への応答はこの画面で確認したことにはなりません。

ステータスの読み取りは割り込み要求の確認応答にもなります。別の割り込み処理が同じ状態を使う場合、無計画にポーリングしないでください。KUROSAKIでは、この読み取りで保留中のFDS IRQフラグが解除されます。

掲載画像はKUROSAKIでディスクの起動ファイルをRAMへ直接配置して確認したものです。このサンプルはBIOSを呼ばず、ファイルの読み書き、ディスク交換、実機での起動を検証していません。検証用の空のファームウェアはBIOSの代用品ではありません。サンプルは画面更新用NMIを使い、不要なマスク可能IRQを無効にしています。

ビルド例ではBIOSの起動画面用の追加ファイルを付けず、ネイティブFDS配置の.fdsを作ります。KUROSAKIの直接起動で確認するための構成です。実機用ディスクの起動手順は、これとは別に確認してください。

NESdev: FDS hardware registers

使用例

c
void main(void) {
    // These BIOS-free queries need NMI for display, but no maskable IRQ.
    __irq_disable();
    fds_result[0] = __fds_disk_ready();
    fds_result[1] = __fds_side();
    fds_result[2] = __fds_error();
    fds_result[3] = 0;
    // Never enter an unbounded wait without first checking disk presence.
    if (fds_result[0]) {
        __fds_wait_ready(); fds_result[3]++;
        __fds_wait_insert(); fds_result[3]++;
    }
    m_init();
    m_text(2,2,"FDS REGISTER STATUS"); m_wait();
    m_text(2,5,"PRESENT / PROTECT / STATUS"); m_wait();
    m_put(2,7,'0'+fds_result[0]);
    m_put(12,7,'0'+fds_result[1]);
    m_put(22,7,'0'+fds_result[2]); m_wait();
    m_text(2,10,"COMPLETED PRESENCE WAITS"); m_wait();
    m_put(2,12,'0'+fds_result[3]); m_wait();
    m_text(2,15,"NO FILE TRANSFER TESTED"); m_wait();
    while (1) { m_wait(); }
}

呼び出し部分のコード例です。完全なプログラムに組み込む際は、記載した初期化とバッファの条件を満たしてください。

期待する結果

PRESENT / PROTECT / STATUSの下に1 / 0 / 0、COMPLETED PRESENCE WAITSの下に2が出ます。二つの待機関数が戻ったことを示し、ファイル転送の成功表示ではありません。最下部にもNO FILE TRANSFER TESTEDと表示します。

__fds_error: STATUS-FDS-DEFAULT — PRESENT / PROTECT / STATUSの下に1 / 0 / 0、COMPLETED PRESENCE WAITSの下に2が出ます。二つの待機関数が戻ったことを示し、ファイル転送の成功表示ではありません。最下部にもNO FILE TRANSFER TESTEDと表示します。
STATUS-FDS-DEFAULT — PRESENT / PROTECT / STATUSの下に1 / 0 / 0、COMPLETED PRESENCE WAITSの下に2が出ます。二つの待機関数が戻ったことを示し、ファイル転送の成功表示ではありません。最下部にもNO FILE TRANSFER TESTEDと表示します。

使用例の完全なプログラム

ビルドコマンド

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\fds_status.c -I .\kitaqfc\lib -I .\kitaq-docs\samples -o .\out\fds_status.fds --mapper=fds --fds-layout=fds32 --fds-no-license-bypass --nes-chr=.\kitaq-docs\samples\font.chr --no-cache --no-disasm
実装とソース

kitaqfc/lib/fds.h:13

宣言の注記(原文)
Read the entire 4030 status register; this is not a stored BIOS error code.

kitaqfc/kitaqfc/CodeGenerator.cs:5173

csharp
case "__fds_error":
                    if (!Need(0)) return true;
                    EmitAsm("LDA", Mem(0x4030));
                    return true;
__fds_side

$4032の書き込み禁止ビットをマスクして返します。この関数はディスクのA面/B面を識別しません。

書式

c
u8 __fds_side(void);

戻り値

$4032 & 0x04。0または4であり、0/1の真偽値ではありません。ハードウェア上のビット2は書き込み禁止または未挿入を示します。

使用条件と制限

--mapper=fdsでビルドしてください。__fds_available以外のFDS専用関数をカートリッジ向けに呼ぶと、KQFC2420でビルドを拒否します。

KUROSAKIのFDSコントローラーは簡易モデルです。この例では挿入状態が1、書き込み禁止マスクが0、ステータスが0になりますが、未挿入・保護付きディスク・CRC異常への応答はこの画面で確認したことにはなりません。

保存処理で使うなら(__fds_side() != 0)と比較します。ただし、保存可能かどうかはディスクの存在やBIOSの戻り値も含めて判断してください。

掲載画像はKUROSAKIでディスクの起動ファイルをRAMへ直接配置して確認したものです。このサンプルはBIOSを呼ばず、ファイルの読み書き、ディスク交換、実機での起動を検証していません。検証用の空のファームウェアはBIOSの代用品ではありません。サンプルは画面更新用NMIを使い、不要なマスク可能IRQを無効にしています。

ビルド例ではBIOSの起動画面用の追加ファイルを付けず、ネイティブFDS配置の.fdsを作ります。KUROSAKIの直接起動で確認するための構成です。実機用ディスクの起動手順は、これとは別に確認してください。

NESdev: FDS hardware registers

使用例

c
void main(void) {
    // These BIOS-free queries need NMI for display, but no maskable IRQ.
    __irq_disable();
    fds_result[0] = __fds_disk_ready();
    fds_result[1] = __fds_side();
    fds_result[2] = __fds_error();
    fds_result[3] = 0;
    // Never enter an unbounded wait without first checking disk presence.
    if (fds_result[0]) {
        __fds_wait_ready(); fds_result[3]++;
        __fds_wait_insert(); fds_result[3]++;
    }
    m_init();
    m_text(2,2,"FDS REGISTER STATUS"); m_wait();
    m_text(2,5,"PRESENT / PROTECT / STATUS"); m_wait();
    m_put(2,7,'0'+fds_result[0]);
    m_put(12,7,'0'+fds_result[1]);
    m_put(22,7,'0'+fds_result[2]); m_wait();
    m_text(2,10,"COMPLETED PRESENCE WAITS"); m_wait();
    m_put(2,12,'0'+fds_result[3]); m_wait();
    m_text(2,15,"NO FILE TRANSFER TESTED"); m_wait();
    while (1) { m_wait(); }
}

呼び出し部分のコード例です。完全なプログラムに組み込む際は、記載した初期化とバッファの条件を満たしてください。

期待する結果

PRESENT / PROTECT / STATUSの下に1 / 0 / 0、COMPLETED PRESENCE WAITSの下に2が出ます。二つの待機関数が戻ったことを示し、ファイル転送の成功表示ではありません。最下部にもNO FILE TRANSFER TESTEDと表示します。

__fds_side: STATUS-FDS-DEFAULT — PRESENT / PROTECT / STATUSの下に1 / 0 / 0、COMPLETED PRESENCE WAITSの下に2が出ます。二つの待機関数が戻ったことを示し、ファイル転送の成功表示ではありません。最下部にもNO FILE TRANSFER TESTEDと表示します。
STATUS-FDS-DEFAULT — PRESENT / PROTECT / STATUSの下に1 / 0 / 0、COMPLETED PRESENCE WAITSの下に2が出ます。二つの待機関数が戻ったことを示し、ファイル転送の成功表示ではありません。最下部にもNO FILE TRANSFER TESTEDと表示します。

使用例の完全なプログラム

ビルドコマンド

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\fds_status.c -I .\kitaqfc\lib -I .\kitaq-docs\samples -o .\out\fds_status.fds --mapper=fds --fds-layout=fds32 --fds-no-license-bypass --nes-chr=.\kitaq-docs\samples\font.chr --no-cache --no-disasm
実装とソース

kitaqfc/lib/fds.h:11

宣言の注記(原文)
Return raw status mask 4032 & 04, either 0 or 4. This is not a numbered disk-side identifier.

kitaqfc/kitaqfc/CodeGenerator.cs:5167

csharp
case "__fds_side":
                    if (!Need(0)) return true;
                    EmitAsm("LDA", Mem(0x4032));
                    EmitAsm("AND", Imm(0x04));
                    return true;
                // Read status register 4030 directly; the caller receives its complete byte value.
__fds_wait_insert

ディスク挿入待ちを表す呼び出し名です。__fds_wait_ready()と同じ存在判定ループを呼びます。

書式

c
void __fds_wait_insert(void);

戻り値

戻り値はありません。

使用条件と制限

--mapper=fdsでビルドしてください。__fds_available以外のFDS専用関数をカートリッジ向けに呼ぶと、KQFC2420でビルドを拒否します。

判定対象は$4032のビット0(ディスクの有無)です。ビット1の転送準備状態、ファイルの存在、ロード完了は確認しません。名前のreadyを「読み書きを開始できる」と解釈しないでください。

タイムアウトはありません。ディスクが入っていなければ戻らず、画面更新や入力処理を続けたい場合は__fds_disk_ready()を各フレームで調べてください。サンプルは先に存在を確認し、入っている場合だけ待機関数を呼びます。挿入済みなら、新たな抜き差しは待ちません。

KUROSAKIのFDSコントローラーは簡易モデルです。この例では挿入状態が1、書き込み禁止マスクが0、ステータスが0になりますが、未挿入・保護付きディスク・CRC異常への応答はこの画面で確認したことにはなりません。

掲載画像はKUROSAKIでディスクの起動ファイルをRAMへ直接配置して確認したものです。このサンプルはBIOSを呼ばず、ファイルの読み書き、ディスク交換、実機での起動を検証していません。検証用の空のファームウェアはBIOSの代用品ではありません。サンプルは画面更新用NMIを使い、不要なマスク可能IRQを無効にしています。

ビルド例ではBIOSの起動画面用の追加ファイルを付けず、ネイティブFDS配置の.fdsを作ります。KUROSAKIの直接起動で確認するための構成です。実機用ディスクの起動手順は、これとは別に確認してください。

NESdev: FDS hardware registers

使用例

c
void main(void) {
    // These BIOS-free queries need NMI for display, but no maskable IRQ.
    __irq_disable();
    fds_result[0] = __fds_disk_ready();
    fds_result[1] = __fds_side();
    fds_result[2] = __fds_error();
    fds_result[3] = 0;
    // Never enter an unbounded wait without first checking disk presence.
    if (fds_result[0]) {
        __fds_wait_ready(); fds_result[3]++;
        __fds_wait_insert(); fds_result[3]++;
    }
    m_init();
    m_text(2,2,"FDS REGISTER STATUS"); m_wait();
    m_text(2,5,"PRESENT / PROTECT / STATUS"); m_wait();
    m_put(2,7,'0'+fds_result[0]);
    m_put(12,7,'0'+fds_result[1]);
    m_put(22,7,'0'+fds_result[2]); m_wait();
    m_text(2,10,"COMPLETED PRESENCE WAITS"); m_wait();
    m_put(2,12,'0'+fds_result[3]); m_wait();
    m_text(2,15,"NO FILE TRANSFER TESTED"); m_wait();
    while (1) { m_wait(); }
}

呼び出し部分のコード例です。完全なプログラムに組み込む際は、記載した初期化とバッファの条件を満たしてください。

期待する結果

PRESENT / PROTECT / STATUSの下に1 / 0 / 0、COMPLETED PRESENCE WAITSの下に2が出ます。二つの待機関数が戻ったことを示し、ファイル転送の成功表示ではありません。最下部にもNO FILE TRANSFER TESTEDと表示します。

__fds_wait_insert: STATUS-FDS-DEFAULT — PRESENT / PROTECT / STATUSの下に1 / 0 / 0、COMPLETED PRESENCE WAITSの下に2が出ます。二つの待機関数が戻ったことを示し、ファイル転送の成功表示ではありません。最下部にもNO FILE TRANSFER TESTEDと表示します。
STATUS-FDS-DEFAULT — PRESENT / PROTECT / STATUSの下に1 / 0 / 0、COMPLETED PRESENCE WAITSの下に2が出ます。二つの待機関数が戻ったことを示し、ファイル転送の成功表示ではありません。最下部にもNO FILE TRANSFER TESTEDと表示します。

使用例の完全なプログラム

ビルドコマンド

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\fds_status.c -I .\kitaqfc\lib -I .\kitaq-docs\samples -o .\out\fds_status.fds --mapper=fds --fds-layout=fds32 --fds-no-license-bypass --nes-chr=.\kitaq-docs\samples\font.chr --no-cache --no-disasm
実装とソース

kitaqfc/lib/fds.h:17

宣言の注記(原文)
Alias the same disk-presence wait as __fds_wait_ready; the call can block indefinitely.

kitaqfc/kitaqfc/CodeGenerator.cs:9148

csharp
EmitHelperStart("__fds_wait_ready");
            string loop = NewGeneratedLabel("fds_wait_ready_loop");
            _assembly.Add(Expr.Make(Tag.Label, loop));
            EmitAsm("LDA", Mem(0x4032));
            EmitAsm("AND", Imm(0x01));
            EmitAsm("BNE", Rel(loop));
            EmitAsm("RTS");
__fds_wait_ready

ディスクが挿入されるまでCPUで待ちます。

書式

c
void __fds_wait_ready(void);

戻り値

戻り値はありません。

使用条件と制限

--mapper=fdsでビルドしてください。__fds_available以外のFDS専用関数をカートリッジ向けに呼ぶと、KQFC2420でビルドを拒否します。

判定対象は$4032のビット0(ディスクの有無)です。ビット1の転送準備状態、ファイルの存在、ロード完了は確認しません。名前のreadyを「読み書きを開始できる」と解釈しないでください。

タイムアウトはありません。ディスクが入っていなければ戻らず、画面更新や入力処理を続けたい場合は__fds_disk_ready()を各フレームで調べてください。サンプルは先に存在を確認し、入っている場合だけ待機関数を呼びます。挿入済みなら、新たな抜き差しは待ちません。

KUROSAKIのFDSコントローラーは簡易モデルです。この例では挿入状態が1、書き込み禁止マスクが0、ステータスが0になりますが、未挿入・保護付きディスク・CRC異常への応答はこの画面で確認したことにはなりません。

掲載画像はKUROSAKIでディスクの起動ファイルをRAMへ直接配置して確認したものです。このサンプルはBIOSを呼ばず、ファイルの読み書き、ディスク交換、実機での起動を検証していません。検証用の空のファームウェアはBIOSの代用品ではありません。サンプルは画面更新用NMIを使い、不要なマスク可能IRQを無効にしています。

ビルド例ではBIOSの起動画面用の追加ファイルを付けず、ネイティブFDS配置の.fdsを作ります。KUROSAKIの直接起動で確認するための構成です。実機用ディスクの起動手順は、これとは別に確認してください。

NESdev: FDS hardware registers

使用例

c
void main(void) {
    // These BIOS-free queries need NMI for display, but no maskable IRQ.
    __irq_disable();
    fds_result[0] = __fds_disk_ready();
    fds_result[1] = __fds_side();
    fds_result[2] = __fds_error();
    fds_result[3] = 0;
    // Never enter an unbounded wait without first checking disk presence.
    if (fds_result[0]) {
        __fds_wait_ready(); fds_result[3]++;
        __fds_wait_insert(); fds_result[3]++;
    }
    m_init();
    m_text(2,2,"FDS REGISTER STATUS"); m_wait();
    m_text(2,5,"PRESENT / PROTECT / STATUS"); m_wait();
    m_put(2,7,'0'+fds_result[0]);
    m_put(12,7,'0'+fds_result[1]);
    m_put(22,7,'0'+fds_result[2]); m_wait();
    m_text(2,10,"COMPLETED PRESENCE WAITS"); m_wait();
    m_put(2,12,'0'+fds_result[3]); m_wait();
    m_text(2,15,"NO FILE TRANSFER TESTED"); m_wait();
    while (1) { m_wait(); }
}

呼び出し部分のコード例です。完全なプログラムに組み込む際は、記載した初期化とバッファの条件を満たしてください。

期待する結果

PRESENT / PROTECT / STATUSの下に1 / 0 / 0、COMPLETED PRESENCE WAITSの下に2が出ます。二つの待機関数が戻ったことを示し、ファイル転送の成功表示ではありません。最下部にもNO FILE TRANSFER TESTEDと表示します。

__fds_wait_ready: STATUS-FDS-DEFAULT — PRESENT / PROTECT / STATUSの下に1 / 0 / 0、COMPLETED PRESENCE WAITSの下に2が出ます。二つの待機関数が戻ったことを示し、ファイル転送の成功表示ではありません。最下部にもNO FILE TRANSFER TESTEDと表示します。
STATUS-FDS-DEFAULT — PRESENT / PROTECT / STATUSの下に1 / 0 / 0、COMPLETED PRESENCE WAITSの下に2が出ます。二つの待機関数が戻ったことを示し、ファイル転送の成功表示ではありません。最下部にもNO FILE TRANSFER TESTEDと表示します。

使用例の完全なプログラム

ビルドコマンド

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\fds_status.c -I .\kitaqfc\lib -I .\kitaq-docs\samples -o .\out\fds_status.fds --mapper=fds --fds-layout=fds32 --fds-no-license-bypass --nes-chr=.\kitaq-docs\samples\font.chr --no-cache --no-disasm
実装とソース

kitaqfc/lib/fds.h:15

宣言の注記(原文)
Poll disk presence until the absent bit clears. There is no timeout or cancellation path.

kitaqfc/kitaqfc/CodeGenerator.cs:9148

csharp
EmitHelperStart("__fds_wait_ready");
            string loop = NewGeneratedLabel("fds_wait_ready_loop");
            _assembly.Add(Expr.Make(Tag.Label, loop));
            EmitAsm("LDA", Mem(0x4032));
            EmitAsm("AND", Imm(0x01));
            EmitAsm("BNE", Rel(loop));
            EmitAsm("RTS");

fds_file.h — FDSファイルのロード

__fds_file_exists

指定IDのレコードが、コンパイル済みのファイル管理表にあるかを調べます。サイズ0のレコードも「存在する」と判定します。

書式

c
u8 __fds_file_exists(u8 id);

引数

id

検索するファイルID。管理表では0~254を使い、255は終端用です。ファイル番号、ファイル名、ロード先アドレスとは別の値です。

戻り値

管理表にあれば1、なければ0。挿入中ディスクの内容についての保証ではありません。

使用条件と制限

--mapper=fdsでビルドしてください。__fds_available以外のFDS専用関数をカートリッジ向けに呼ぶと、KQFC2420でビルドを拒否します。

--fds-meta=...または--fds-manifest=...で指定した管理表と、自動生成したオーバーレイのレコードを検索します。ディスクのディレクトリは読みません。管理表にレコードがないファイルは、ディスクに収録されていても見つかりません。手動レコードと自動レコードの合計は42件までです。

返すサイズは管理表の宣言値です。sourceファイルから推定した実際の長さ、オーバーレイ末尾の切り詰め後の長さ、交換したディスクの内容と一致するとは限りません。実データの範囲確認には正しいサイズを管理表へ指定してください。

サンプルのfds_metadata.jsonと二つの.binファイルはCソースと同じフォルダーに置きます。ID 16の独自データは0~255を2回と末尾0の513バイト、ID 17のデータは空です。両方ともPRG形式(type 0)の非起動ファイルとして収録し、この例ではロードしません。sourceの相対パスは管理表のあるフォルダーを基準に解決します。

掲載画像はKUROSAKIでディスクの起動ファイルをRAMへ直接配置して確認したものです。このサンプルはBIOSを呼ばず、ファイルの読み書き、ディスク交換、実機での起動を検証していません。検証用の空のファームウェアはBIOSの代用品ではありません。サンプルは画面更新用NMIを使い、不要なマスク可能IRQを無効にしています。

ビルド例ではBIOSの起動画面用の追加ファイルを付けず、ネイティブFDS配置の.fdsを作ります。KUROSAKIの直接起動で確認するための構成です。実機用ディスクの起動手順は、これとは別に確認してください。

使用例

c
void main(void) {
    // These BIOS-free queries need NMI for display, but no maskable IRQ.
    __irq_disable();
    fds_result[0] = __fds_file_exists(16);
    fds_sizes[0] = __fds_file_size(16);
    fds_result[1] = __fds_file_exists(17);
    fds_sizes[1] = __fds_file_size(17);
    fds_result[2] = __fds_file_exists(18);
    fds_sizes[2] = __fds_file_size(18);
    m_init();
    m_text(2,2,"FDS COMPILED FILE TABLE"); m_wait();
    m_text(2,5,"ID    EXISTS    BYTES"); m_wait();
    m_text(2,7,"16      1        513"); m_wait();
    m_text(2,9,"17      1          0"); m_wait();
    m_text(2,11,"18      0          0"); m_wait();
    if (fds_result[0]==1 && fds_sizes[0]==513 &&
        fds_result[1]==1 && fds_sizes[1]==0 &&
        fds_result[2]==0 && fds_sizes[2]==0) { m_text(2,14,"TABLE VALUES MATCH: PASS"); }
    else { m_text(2,14,"TABLE VALUES MATCH: FAIL"); }
    m_wait();
    m_text(2,17,"NO DISK DIRECTORY SCAN"); m_wait();
    while (1) { m_wait(); }
}

呼び出し部分のコード例です。完全なプログラムに組み込む際は、記載した初期化とバッファの条件を満たしてください。

期待する結果

表のID 16は存在1・513バイト、ID 17は存在1・0バイト、ID 18は存在0・0バイトです。TABLE VALUES MATCH: PASSは取得値をこの組み合わせと比較した結果です。サイズ0だけでは「空のレコード」と「該当なし」を区別できないことを示します。

__fds_file_exists: METADATA-FDS-DEFAULT — 表のID 16は存在1・513バイト、ID 17は存在1・0バイト、ID 18は存在0・0バイトです。TABLE VALUES MATCH: PASSは取得値をこの組み合わせと比較した結果です。サイズ0だけでは「空のレコード」と「該当なし」を区別できないことを示します。
METADATA-FDS-DEFAULT — 表のID 16は存在1・513バイト、ID 17は存在1・0バイト、ID 18は存在0・0バイトです。TABLE VALUES MATCH: PASSは取得値をこの組み合わせと比較した結果です。サイズ0だけでは「空のレコード」と「該当なし」を区別できないことを示します。

使用例の完全なプログラム

ビルドコマンド

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\fds_metadata.c -I .\kitaqfc\lib -I .\kitaq-docs\samples -o .\out\fds_metadata.fds --mapper=fds --fds-layout=fds32 --fds-no-license-bypass --nes-chr=.\kitaq-docs\samples\font.chr --no-cache --no-disasm --fds-meta=.\kitaq-docs\samples\api-examples\fc\fds_metadata.json
実装とソース

kitaqfc/lib/fds_file.h:18

宣言の注記(原文)
Query the compiled --fds-meta/--fds-manifest table, not the inserted disk; return 0 or 1.

kitaqfc/kitaqfc/CodeGenerator.cs:9481

csharp
EmitHelperStart("__fds_file_exists");
            EmitFdsMetadataSearch(foundReturnsSize: false);

            // Return the compiled metadata size, using zero for an unmatched ID.
__fds_file_size

指定IDに対して管理表へ登録されたバイト数を返します。ロード予定の資産の宣言サイズを調べる用途です。

書式

c
u16 __fds_file_size(u8 id);

引数

id

検索するファイルID。管理表では0~254を使い、255は終端用です。ファイル番号、ファイル名、ロード先アドレスとは別の値です。

戻り値

登録された16ビット符号なしサイズ(0~65535)。該当IDがない場合も0です。必要に応じて__fds_file_exists()を併用します。

使用条件と制限

--mapper=fdsでビルドしてください。__fds_available以外のFDS専用関数をカートリッジ向けに呼ぶと、KQFC2420でビルドを拒否します。

--fds-meta=...または--fds-manifest=...で指定した管理表と、自動生成したオーバーレイのレコードを検索します。ディスクのディレクトリは読みません。管理表にレコードがないファイルは、ディスクに収録されていても見つかりません。手動レコードと自動レコードの合計は42件までです。

返すサイズは管理表の宣言値です。sourceファイルから推定した実際の長さ、オーバーレイ末尾の切り詰め後の長さ、交換したディスクの内容と一致するとは限りません。実データの範囲確認には正しいサイズを管理表へ指定してください。

サンプルのfds_metadata.jsonと二つの.binファイルはCソースと同じフォルダーに置きます。ID 16の独自データは0~255を2回と末尾0の513バイト、ID 17のデータは空です。両方ともPRG形式(type 0)の非起動ファイルとして収録し、この例ではロードしません。sourceの相対パスは管理表のあるフォルダーを基準に解決します。

掲載画像はKUROSAKIでディスクの起動ファイルをRAMへ直接配置して確認したものです。このサンプルはBIOSを呼ばず、ファイルの読み書き、ディスク交換、実機での起動を検証していません。検証用の空のファームウェアはBIOSの代用品ではありません。サンプルは画面更新用NMIを使い、不要なマスク可能IRQを無効にしています。

ビルド例ではBIOSの起動画面用の追加ファイルを付けず、ネイティブFDS配置の.fdsを作ります。KUROSAKIの直接起動で確認するための構成です。実機用ディスクの起動手順は、これとは別に確認してください。

使用例

c
void main(void) {
    // These BIOS-free queries need NMI for display, but no maskable IRQ.
    __irq_disable();
    fds_result[0] = __fds_file_exists(16);
    fds_sizes[0] = __fds_file_size(16);
    fds_result[1] = __fds_file_exists(17);
    fds_sizes[1] = __fds_file_size(17);
    fds_result[2] = __fds_file_exists(18);
    fds_sizes[2] = __fds_file_size(18);
    m_init();
    m_text(2,2,"FDS COMPILED FILE TABLE"); m_wait();
    m_text(2,5,"ID    EXISTS    BYTES"); m_wait();
    m_text(2,7,"16      1        513"); m_wait();
    m_text(2,9,"17      1          0"); m_wait();
    m_text(2,11,"18      0          0"); m_wait();
    if (fds_result[0]==1 && fds_sizes[0]==513 &&
        fds_result[1]==1 && fds_sizes[1]==0 &&
        fds_result[2]==0 && fds_sizes[2]==0) { m_text(2,14,"TABLE VALUES MATCH: PASS"); }
    else { m_text(2,14,"TABLE VALUES MATCH: FAIL"); }
    m_wait();
    m_text(2,17,"NO DISK DIRECTORY SCAN"); m_wait();
    while (1) { m_wait(); }
}

呼び出し部分のコード例です。完全なプログラムに組み込む際は、記載した初期化とバッファの条件を満たしてください。

期待する結果

表のID 16は存在1・513バイト、ID 17は存在1・0バイト、ID 18は存在0・0バイトです。TABLE VALUES MATCH: PASSは取得値をこの組み合わせと比較した結果です。サイズ0だけでは「空のレコード」と「該当なし」を区別できないことを示します。

__fds_file_size: METADATA-FDS-DEFAULT — 表のID 16は存在1・513バイト、ID 17は存在1・0バイト、ID 18は存在0・0バイトです。TABLE VALUES MATCH: PASSは取得値をこの組み合わせと比較した結果です。サイズ0だけでは「空のレコード」と「該当なし」を区別できないことを示します。
METADATA-FDS-DEFAULT — 表のID 16は存在1・513バイト、ID 17は存在1・0バイト、ID 18は存在0・0バイトです。TABLE VALUES MATCH: PASSは取得値をこの組み合わせと比較した結果です。サイズ0だけでは「空のレコード」と「該当なし」を区別できないことを示します。

使用例の完全なプログラム

ビルドコマンド

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\fds_metadata.c -I .\kitaqfc\lib -I .\kitaq-docs\samples -o .\out\fds_metadata.fds --mapper=fds --fds-layout=fds32 --fds-no-license-bypass --nes-chr=.\kitaq-docs\samples\font.chr --no-cache --no-disasm --fds-meta=.\kitaq-docs\samples\api-examples\fc\fds_metadata.json
実装とソース

kitaqfc/lib/fds_file.h:20

宣言の注記(原文)
Return the compiled metadata size, or zero if id is absent. No disk directory is read.

kitaqfc/kitaqfc/CodeGenerator.cs:9485

csharp
EmitHelperStart("__fds_file_size");
            EmitFdsMetadataSearch(foundReturnsSize: true);
__fds_load_file

ファイルをBIOSで読み込み、PRGデータを指定したCPUバッファへコピーします。セーブデータやステージデータを、ゲーム側で確保した領域へ取り込むための関数です。

書式

c
u8 __fds_load_file(u8 id, u8* dst);

引数

id

生成するディスクに含まれる、起動時に読まないファイルのID(0~254)。サンプルは73です。ディスク上で何番目に並ぶかを表す番号とは異なります。255、見つからないID、起動用ファイルは拒否します。

dst

PRGファイルのコピー先。実際のファイル長以上の領域が必要です。0を渡すとコピーせず、ファイルヘッダーのロード先に残します。CHR・ネームテーブルのPPUファイルは0のみ指定でき、BIOSが本来のPPU宛先へ転送します。

戻り値

成功は0。BIOSの非ゼロエラーはそのまま返します。BIOSが成功を返しても転送数が1でなければ64($40)、無効なID・バッファ・ファイル種別と宛先の組み合わせは255($FF)です。

使用条件と制限

--mapper=fds --fds-layout=fds32用です。--fds-metaでファイルのID、ロード先、種類、起動時に読むかどうか、配置順を指定します。転送先とサイズは、実際に生成したディスクの情報から決まります。挿入した任意のディスクの内容を調べるAPIではありません。

BIOSのロード先はディスクのヘッダーで決まるため、先にその領域へ読み込み、その後dstへコピーします。二つの領域が重なってもコピー方向を選んで内容を保持しますが、一時ロード先自体の確保も必要です。転送失敗時はコピーを行いません。BIOS呼び出し後はバンク常駐記録を255(不明)にします。

CPUバッファは、全体が$0200~$07FFまたは$6000~$DFFFの一方に収まる必要があります。範囲の折り返しやI/Oレジスタへの転送は拒否します。ただし、コード・OAM・他の変数・コンパイラの作業領域と重ならないことは呼び出し側で保証してください。サンプルは共通コードをバンク0に置き、$8000を一時ロード先、$8500と$8600を4バイトのバッファとして使います。

同期式のBIOS呼び出しです。実行中のコードや復帰先をロードで上書きしないよう共通バンクから呼んでください。割り込み内からは呼べません。ディスクI/O中は描画とNMIを停止し、PPUのアドレス増分を1にし、ディスク以外のIRQ源を停止するなど、BIOSの前提を満たす必要があります。転送中にゲームや音楽の処理が継続することは保証しません。

掲載画像は独自作成のBIOS呼び出し規約テストで、コンパイル後のCPU処理、バッファ内容、保存後の再読み込み、全画素を確認したものです。実際のBIOSや物理ディスクの書き込み、CRC、電源断時の復旧を確認した画像ではありません。KUROSAKIの通常のFDS実行だけでは同じ保存処理を検証できません。

サンプル本体に加え、同じフォルダーのfds_file_io_manifest.jsonとfds_file_io_payload.binを使います。下記の検証コマンドは、テスト用処理を独自のソースから一時生成して実行後に削除します。市販BIOSを配布物に含めたり、通常のゲーム用BIOSとしてこの検証処理を使ったりするものではありません。

FDS BIOS calling convention

使用例

c
#pragma fixed_bank 0
#include "fc_fds_scene.h"
void main(void) {
    u8 i;
    u8* data=(u8*)0x8500;
    u8* again=(u8*)0x8600;
    __irq_disable();__ppu_ctrl_set(0);__ppu_mask_set(0);
    // Keep rendering/NMI and non-disk IRQ sources disabled during disk I/O.
    *((u8*)0xFF)=0;*((u8*)0x4010)=0;*((u8*)0x4017)=0x40;*((u8*)0x4022)=0;
    fds_result[0]=__fds_load_file(73,data);
    fds_result[1]=data[0];fds_result[2]=data[3];
    for(i=0;i<4;i=i+1){data[i]=(i+1)*7;}
    fds_result[3]=__fds_save_file(73,data,4);
    fds_result[4]=__fds_load_file(73,again);
    fds_result[5]=again[0];fds_result[6]=again[3];
    // Check every returned byte, not only the displayed endpoints.
    for(i=0;i<4;i=i+1){if(again[i]!=(i+1)*7){fds_result[7]=1;}}
    fds_result[15]=165;
    m_init();m_text(2,2,"FDS LOAD / SAVE BUFFER");m_wait();
    m_text(2,5,"LOAD TO 8500 STATUS");fds_number(25,5,fds_result[0]);m_wait();
    m_text(2,7,"INITIAL FIRST/LAST");fds_number(21,7,fds_result[1]);fds_number(25,7,fds_result[2]);m_wait();
    m_text(2,9,"SAVE FILE 73 STATUS");fds_number(25,9,fds_result[3]);m_wait();
    m_text(2,11,"RELOAD TO 8600 STATUS");fds_number(25,11,fds_result[4]);m_wait();
    m_text(2,13,"SAVED FIRST / LAST");fds_number(21,13,fds_result[5]);fds_number(25,13,fds_result[6]);m_wait();
    m_text(2,15,"BYTE CHECK ERRORS");fds_number(25,15,fds_result[7]);m_wait();
    m_text(2,18,"STAGING 8000 / SIZE 4");m_wait();
    while(1){m_wait();}
}

呼び出し部分のコード例です。完全なプログラムに組み込む際は、記載した初期化とバッファの条件を満たしてください。

期待する結果

LOAD・SAVE・RELOADのSTATUSとBYTE CHECK ERRORSはすべて000です。INITIAL FIRST/LASTの011と044は、$8500へ読み込んだ4バイトの最初と最後です。内容を7・14・21・28へ変えて保存し、別の$8600へ読み直すため、SAVED FIRST / LASTは007と028になります。STAGING 8000 / SIZE 4はディスク側のロード先とファイル長です。独自テストでは、ID 73を実際の4番目のファイル(0起点の位置3)へ保存したことも確認しています。

__fds_load_file: DEFAULT — LOAD・SAVE・RELOADのSTATUSとBYTE CHECK ERRORSはすべて000です。INITIAL FIRST/LASTの011と044は、$8500へ読み込んだ4バイトの最初と最後です。内容を7・14・21・28へ変えて保存し、別の$8600へ読み直すため、SAVED FIRST / LASTは007と028になります。STAGING 8000 / SIZE 4はディスク側のロード先とファイル長です。独自テストでは、ID 73を実際の4番目のファイル(0起点の位置3)へ保存したことも確認しています。
DEFAULT — LOAD・SAVE・RELOADのSTATUSとBYTE CHECK ERRORSはすべて000です。INITIAL FIRST/LASTの011と044は、$8500へ読み込んだ4バイトの最初と最後です。内容を7・14・21・28へ変えて保存し、別の$8600へ読み直すため、SAVED FIRST / LASTは007と028になります。STAGING 8000 / SIZE 4はディスク側のロード先とファイル長です。独自テストでは、ID 73を実際の4番目のファイル(0起点の位置3)へ保存したことも確認しています。

使用例の完全なプログラム

fds_file_io_manifest.json

fds_file_io_payload.bin

ビルドコマンド

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\fds_file_io.c -I .\kitaqfc\lib -I .\kitaq-docs\samples -o .\out\fds_file_io.fds --mapper=fds --fds-layout=fds32 --fds-no-license-bypass --fds-meta=.\kitaq-docs\samples\api-examples\fc\fds_file_io_manifest.json --nes-chr=.\kitaq-docs\samples\font.chr --no-cache --no-disasm
python .\kitaq-docs\tools\check_fds_file_examples.py
実装とソース

kitaqfc/lib/fds_file.h:11

宣言の注記(原文)
Native FDS32: load a non-boot file at its disk-header address, then memmove
PRG bytes to dst. Null dst keeps the native destination (also for PPU files).
Both CPU spans must fit $0200-$07FF or $6000-$DFFF and be owned by the caller.
Return 0, a BIOS error, $40 for a missing transfer, or $FF for invalid input.
Call from common code with disk-I/O interrupt/display prerequisites satisfied.

kitaqfc/kitaqfc/CodeGenerator.cs:9223

csharp
EmitHelperStart("__fds_load_file");
            // Load at the on-disk staging address, then memmove PRG data to non-null dst.
            EmitAsm("JSR", Abs("__kq_fds_find_io"));
            EmitAsm("CMP", Imm(0));
            EmitAsm("BNE", Rel("__kq_fio_load_invalid"));
            EmitAsm("LDA", Mem(_runtimeFdsFileHeaderAddress + 19));
            EmitAsm("BMI", Rel("__kq_fio_load_invalid"));
            EmitAsm("LDA", Mem(CallArgBase + 1));
            EmitAsm("STA", Mem(_runtimeFdsFileHeaderAddress + 14));
            EmitAsm("LDA", Mem(CallArgBase + 2));
            EmitAsm("STA", Mem(_runtimeFdsFileHeaderAddress + 15));
            EmitAsm("LDA", Mem(_runtimeFdsFileHeaderAddress + 23));
            EmitAsm("STA", Mem(CallArgBase + 10));
            EmitAsm("LDA", Mem(_runtimeFdsFileHeaderAddress + 24));
            EmitAsm("STA", Mem(CallArgBase + 11));
            EmitAsm("LDA", Mem(_runtimeFdsFileHeaderAddress + 20));
            EmitAsm("BNE", Rel("__kq_fio_load_ppu"));
            EmitAsm("LDA", Mem(_runtimeFdsFileHeaderAddress + 21));
            EmitAsm("STA", Mem(CallArgBase + 8));
            EmitAsm("LDA", Mem(_runtimeFdsFileHeaderAddress + 22));
            EmitAsm("STA", Mem(CallArgBase + 9));
            EmitAsm("JSR", Abs("__kq_fds_check_span"));
            EmitAsm("CMP", Imm(0));
            EmitAsm("BNE", Rel("__kq_fio_load_invalid"));
            EmitAsm("JMP", Abs("__kq_fio_load_dst"));
            _assembly.Add(Expr.Make(Tag.Label, "__kq_fio_load_ppu"));
            // CHR/nametable files have no CPU buffer; null dst selects their native PPU destination.
            EmitAsm("LDA", Mem(_runtimeFdsFileHeaderAddress + 14));
            EmitAsm("ORA", Mem(_runtimeFdsFileHeaderAddress + 15));
            EmitAsm("BNE", Rel("__kq_fio_load_invalid"));
            EmitAsm("JMP", Abs("__kq_fio_load_bios"));
            _assembly.Add(Expr.Make(Tag.Label, "__kq_fio_load_dst"));
            EmitAsm("LDA", Mem(_runtimeFdsFileHeaderAddress + 14));
            EmitAsm("ORA", Mem(_runtimeFdsFileHeaderAddress + 15));
            EmitAsm("BEQ", Rel("__kq_fio_load_bios"));
            EmitAsm("LDA", Mem(_runtimeFdsFileHeaderAddress + 14));
            EmitAsm("STA", Mem(CallArgBase + 8));
            EmitAsm("LDA", Mem(_runtimeFdsFileHeaderAddress + 15));
            EmitAsm("STA", Mem(CallArgBase + 9));
            EmitAsm("JSR", Abs("__kq_fds_check_span"));
            EmitAsm("CMP", Imm(0));
            EmitAsm("BNE", Rel("__kq_fio_load_invalid"));
            _assembly.Add(Expr.Make(Tag.Label, "__kq_fio_load_bios"));
            EmitAsm("LDA", Mem(_runtimeFdsFileHeaderAddress + 17));
            EmitAsm("STA", Mem(_runtimeFdsLoadListAddress));
            EmitAsm("LDA", Imm(0xFF));
            EmitAsm("STA", Mem(_runtimeFdsLoadListAddress + 1));
            EmitAsm("JSR", Mem(0xE1F8));
            _assembly.Add(Expr.Make(Tag.Word, string.Format("${0:X4}", _runtimeFdsFileHeaderAddress + 33)));
            _assembly.Add(Expr.Make(Tag.Word, string.Format("${0:X4}", _runtimeFdsLoadListAddress)));
            EmitAsm("TAX");
            EmitAsm("LDA", Imm(0xFF));
            EmitAsm("STA", Mem(_runtimeFdsResidentBankAddress));
            EmitAsm("STA", Mem(_runtimeCurrentBankAddress));
            EmitAsm("TXA");
            EmitAsm("BNE", Rel("__kq_fio_load_return"));
            EmitAsm("CPY", Imm(1));
            EmitAsm("BEQ", Rel("__kq_fio_load_count_ok"));
            EmitAsm("LDA", Imm(0x40));
            EmitAsm("RTS");
            _assembly.Add(Expr.Make(Tag.Label, "__kq_fio_load_count_ok"));
            EmitAsm("LDA", Mem(_runtimeFdsFileHeaderAddress + 14));
            EmitAsm("ORA", Mem(_runtimeFdsFileHeaderAddress + 15));
            EmitAsm("BEQ", Rel("__kq_fio_load_ok"));
            EmitAsm("LDA", Mem(_runtimeFdsFileHeaderAddress + 21));
            EmitAsm("STA", Mem(CallArgBase + 0));
            EmitAsm("LDA", Mem(_runtimeFdsFileHeaderAddress + 14));
            EmitAsm("STA", Mem(CallArgBase + 2));
            EmitAsm("LDA", Mem(_runtimeFdsFileHeaderAddress + 23));
            EmitAsm("STA", Mem(CallArgBase + 4));
            EmitAsm("LDA", Mem(_runtimeFdsFileHeaderAddress + 22));
            EmitAsm("STA", Mem(CallArgBase + 1));
            EmitAsm("LDA", Mem(_runtimeFdsFileHeaderAddress + 15));
            EmitAsm("STA", Mem(CallArgBase + 3));
            EmitAsm("LDA", Mem(_runtimeFdsFileHeaderAddress + 24));
            EmitAsm("STA", Mem(CallArgBase + 5));
            EmitAsm("LDA", Mem(CallArgBase + 4));
            EmitAsm("ORA", Mem(CallArgBase + 5));
            EmitAsm("BEQ", Rel("__kq_fio_load_ok"));
            // Copy backwards when destination starts above source, including overlapping buffers.
            EmitAsm("LDA", Mem(CallArgBase + 3));
            EmitAsm("CMP", Mem(CallArgBase + 1));
            EmitAsm("BCC", Rel("__kq_fio_forward"));
            EmitAsm("BNE", Rel("__kq_fio_back_setup"));
            EmitAsm("LDA", Mem(CallArgBase + 2));
            EmitAsm("CMP", Mem(CallArgBase + 0));
            EmitAsm("BCC", Rel("__kq_fio_forward"));
            EmitAsm("BEQ", Rel("__kq_fio_load_ok"));
            _assembly.Add(Expr.Make(Tag.Label, "__kq_fio_back_setup"));
            EmitAsm("CLC");
            EmitAsm("LDA", Mem(CallArgBase + 0));
            EmitAsm("ADC", Mem(CallArgBase + 4));
            EmitAsm("STA", Mem(CallArgBase + 0));
            EmitAsm("LDA", Mem(CallArgBase + 1));
            EmitAsm("ADC", Mem(CallArgBase + 5));
            EmitAsm("STA", Mem(CallArgBase + 1));
            EmitAsm("CLC");
            EmitAsm("LDA", Mem(CallArgBase + 2));
            EmitAsm("ADC", Mem(CallArgBase + 4));
            EmitAsm("STA", Mem(CallArgBase + 2));
            EmitAsm("LDA", Mem(CallArgBase + 3));
            EmitAsm("ADC", Mem(CallArgBase + 5));
            EmitAsm("STA", Mem(CallArgBase + 3));
            _assembly.Add(Expr.Make(Tag.Label, "__kq_fio_backward"));
            EmitAsm("LDA", Mem(CallArgBase + 0));
            EmitAsm("BNE", Rel("__kq_fio_back_dec0"));
            EmitAsm("DEC", Mem(CallArgBase + 1));
            _assembly.Add(Expr.Make(Tag.Label, "__kq_fio_back_dec0"));
            EmitAsm("DEC", Mem(CallArgBase + 0));
            EmitAsm("LDA", Mem(CallArgBase + 2));
            EmitAsm("BNE", Rel("__kq_fio_back_dec2"));
            EmitAsm("DEC", Mem(CallArgBase + 3));
            _assembly.Add(Expr.Make(Tag.Label, "__kq_fio_back_dec2"));
            EmitAsm("DEC", Mem(CallArgBase + 2));
            EmitAsm("LDY", Imm(0));
            EmitAsm("LDA", IndY(CallArgBase));
            EmitAsm("STA", IndY(CallArgBase + 2));
            EmitAsm("JSR", Abs("__kq_fds_copy_count"));
            EmitAsm("BNE", Rel("__kq_fio_backward"));
            EmitAsm("JMP", Abs("__kq_fio_load_ok"));
            _assembly.Add(Expr.Make(Tag.Label, "__kq_fio_forward"));
            EmitAsm("LDY", Imm(0));
            EmitAsm("LDA", IndY(CallArgBase));
            EmitAsm("STA", IndY(CallArgBase + 2));
            EmitAsm("INC", Mem(CallArgBase + 0));
            EmitAsm("BNE", Rel("__kq_fio_forward_inc0"));
            EmitAsm("INC", Mem(CallArgBase + 1));
            _assembly.Add(Expr.Make(Tag.Label, "__kq_fio_forward_inc0"));
            EmitAsm("INC", Mem(CallArgBase + 2));
            EmitAsm("BNE", Rel("__kq_fio_forward_inc2"));
            EmitAsm("INC", Mem(CallArgBase + 3));
            _assembly.Add(Expr.Make(Tag.Label, "__kq_fio_forward_inc2"));
            EmitAsm("JSR", Abs("__kq_fds_copy_count"));
            EmitAsm("BNE", Rel("__kq_fio_forward"));
            _assembly.Add(Expr.Make(Tag.Label, "__kq_fio_load_ok"));
            EmitAsm("LDA", Imm(0));
            _assembly.Add(Expr.Make(Tag.Label, "__kq_fio_load_return"));
            EmitAsm("RTS");
            _assembly.Add(Expr.Make(Tag.Label, "__kq_fio_load_invalid"));
            EmitAsm("LDA", Imm(0xFF));
            EmitAsm("RTS");
__fds_save_file

あらかじめディスクの最後に確保した固定長のPRGセーブ領域を、CPUバッファの内容で上書きします。ファイルのID・名前・次回のロード先を保持します。

書式

c
u8 __fds_save_file(u8 id, const u8* src, u16 len);

引数

id

生成するディスクに含まれる、起動時に読まないファイルのID(0~254)。サンプルは73です。ディスク上で何番目に並ぶかを表す番号とは異なります。255、見つからないID、起動用ファイルは拒否します。

src

保存するデータのCPU RAM上の先頭。lenバイトを読み出せる領域を渡します。VRAMを直接保存するAPIではありません。

len

保存するバイト数。生成済みファイルの実際の長さと一致させてください。データが短いときは、保存用バッファの残りをゲーム側で埋めます。

戻り値

成功は0、BIOSが返す書き込み保護・検証失敗などのエラーはその値です。無効なID、起動用またはPRG以外のファイル、同じ面で最後に並ばないファイル、長さの不一致、無効なCPU範囲は255($FF)を返し、BIOSを呼びません。

使用条件と制限

--mapper=fds --fds-layout=fds32用です。--fds-metaでファイルのID、ロード先、種類、起動時に読むかどうか、配置順を指定します。転送先とサイズは、実際に生成したディスクの情報から決まります。挿入した任意のディスクの内容を調べるAPIではありません。

BIOSのWriteFileは書き込む位置より後のファイルを論理的に隠すため、このAPIでは各面の最後にある非起動PRGファイルだけを保存対象にします。新規ファイルの追加や任意の長さへの変更は行いません。起動補助ファイルや自動オーバーレイも含め、最終配置で最後になっている必要があります。

コンパイラはファイルID、ヘッダーのnumber、物理的な並び順を区別します。サンプルのIDは73、管理表のnumberは200ですが、BIOSへ渡す位置は3です。ディスク番号・面番号も配置から設定します。マニフェストと異なる構成に改変したディスクでは使わないでください。

CPUバッファは、全体が$0200~$07FFまたは$6000~$DFFFの一方に収まる必要があります。範囲の折り返しやI/Oレジスタへの転送は拒否します。ただし、コード・OAM・他の変数・コンパイラの作業領域と重ならないことは呼び出し側で保証してください。サンプルは共通コードをバンク0に置き、$8000を一時ロード先、$8500と$8600を4バイトのバッファとして使います。

同期式のBIOS呼び出しです。実行中のコードや復帰先をロードで上書きしないよう共通バンクから呼んでください。割り込み内からは呼べません。ディスクI/O中は描画とNMIを停止し、PPUのアドレス増分を1にし、ディスク以外のIRQ源を停止するなど、BIOSの前提を満たす必要があります。転送中にゲームや音楽の処理が継続することは保証しません。

掲載画像は独自作成のBIOS呼び出し規約テストで、コンパイル後のCPU処理、バッファ内容、保存後の再読み込み、全画素を確認したものです。実際のBIOSや物理ディスクの書き込み、CRC、電源断時の復旧を確認した画像ではありません。KUROSAKIの通常のFDS実行だけでは同じ保存処理を検証できません。

サンプル本体に加え、同じフォルダーのfds_file_io_manifest.jsonとfds_file_io_payload.binを使います。下記の検証コマンドは、テスト用処理を独自のソースから一時生成して実行後に削除します。市販BIOSを配布物に含めたり、通常のゲーム用BIOSとしてこの検証処理を使ったりするものではありません。

FDS BIOS calling convention

使用例

c
#pragma fixed_bank 0
#include "fc_fds_scene.h"
void main(void) {
    u8 i;
    u8* data=(u8*)0x8500;
    u8* again=(u8*)0x8600;
    __irq_disable();__ppu_ctrl_set(0);__ppu_mask_set(0);
    // Keep rendering/NMI and non-disk IRQ sources disabled during disk I/O.
    *((u8*)0xFF)=0;*((u8*)0x4010)=0;*((u8*)0x4017)=0x40;*((u8*)0x4022)=0;
    fds_result[0]=__fds_load_file(73,data);
    fds_result[1]=data[0];fds_result[2]=data[3];
    for(i=0;i<4;i=i+1){data[i]=(i+1)*7;}
    fds_result[3]=__fds_save_file(73,data,4);
    fds_result[4]=__fds_load_file(73,again);
    fds_result[5]=again[0];fds_result[6]=again[3];
    // Check every returned byte, not only the displayed endpoints.
    for(i=0;i<4;i=i+1){if(again[i]!=(i+1)*7){fds_result[7]=1;}}
    fds_result[15]=165;
    m_init();m_text(2,2,"FDS LOAD / SAVE BUFFER");m_wait();
    m_text(2,5,"LOAD TO 8500 STATUS");fds_number(25,5,fds_result[0]);m_wait();
    m_text(2,7,"INITIAL FIRST/LAST");fds_number(21,7,fds_result[1]);fds_number(25,7,fds_result[2]);m_wait();
    m_text(2,9,"SAVE FILE 73 STATUS");fds_number(25,9,fds_result[3]);m_wait();
    m_text(2,11,"RELOAD TO 8600 STATUS");fds_number(25,11,fds_result[4]);m_wait();
    m_text(2,13,"SAVED FIRST / LAST");fds_number(21,13,fds_result[5]);fds_number(25,13,fds_result[6]);m_wait();
    m_text(2,15,"BYTE CHECK ERRORS");fds_number(25,15,fds_result[7]);m_wait();
    m_text(2,18,"STAGING 8000 / SIZE 4");m_wait();
    while(1){m_wait();}
}

呼び出し部分のコード例です。完全なプログラムに組み込む際は、記載した初期化とバッファの条件を満たしてください。

期待する結果

LOAD・SAVE・RELOADのSTATUSとBYTE CHECK ERRORSはすべて000です。INITIAL FIRST/LASTの011と044は、$8500へ読み込んだ4バイトの最初と最後です。内容を7・14・21・28へ変えて保存し、別の$8600へ読み直すため、SAVED FIRST / LASTは007と028になります。STAGING 8000 / SIZE 4はディスク側のロード先とファイル長です。独自テストでは、ID 73を実際の4番目のファイル(0起点の位置3)へ保存したことも確認しています。

__fds_save_file: DEFAULT — LOAD・SAVE・RELOADのSTATUSとBYTE CHECK ERRORSはすべて000です。INITIAL FIRST/LASTの011と044は、$8500へ読み込んだ4バイトの最初と最後です。内容を7・14・21・28へ変えて保存し、別の$8600へ読み直すため、SAVED FIRST / LASTは007と028になります。STAGING 8000 / SIZE 4はディスク側のロード先とファイル長です。独自テストでは、ID 73を実際の4番目のファイル(0起点の位置3)へ保存したことも確認しています。
DEFAULT — LOAD・SAVE・RELOADのSTATUSとBYTE CHECK ERRORSはすべて000です。INITIAL FIRST/LASTの011と044は、$8500へ読み込んだ4バイトの最初と最後です。内容を7・14・21・28へ変えて保存し、別の$8600へ読み直すため、SAVED FIRST / LASTは007と028になります。STAGING 8000 / SIZE 4はディスク側のロード先とファイル長です。独自テストでは、ID 73を実際の4番目のファイル(0起点の位置3)へ保存したことも確認しています。

使用例の完全なプログラム

fds_file_io_manifest.json

fds_file_io_payload.bin

ビルドコマンド

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\fds_file_io.c -I .\kitaqfc\lib -I .\kitaq-docs\samples -o .\out\fds_file_io.fds --mapper=fds --fds-layout=fds32 --fds-no-license-bypass --fds-meta=.\kitaq-docs\samples\api-examples\fc\fds_file_io_manifest.json --nes-chr=.\kitaq-docs\samples\font.chr --no-cache --no-disasm
python .\kitaq-docs\tools\check_fds_file_examples.py
実装とソース

kitaqfc/lib/fds_file.h:16

宣言の注記(原文)
Overwrite the final non-boot PRG slot on its side, preserving its ID/name/load
address. len must equal its packaged size; src must be a valid CPU RAM span.
The resolved physical ordinal selects the slot, independently of file ID.
Return 0, a BIOS error, or $FF before any write for an invalid slot/span/size.

kitaqfc/kitaqfc/CodeGenerator.cs:9488

csharp
EmitHelperStart("__fds_save_file");
            // Only a non-boot, final, fixed-size PRG slot can be overwritten without hiding later files.
            EmitAsm("JSR", Abs("__kq_fds_find_io"));
            EmitAsm("CMP", Imm(0));
            EmitAsm("BNE", Rel("__kq_fio_save_invalid"));
            EmitAsm("LDA", Mem(_runtimeFdsFileHeaderAddress + 19));
            EmitAsm("AND", Imm(0xC0));
            EmitAsm("CMP", Imm(0x40));
            EmitAsm("BNE", Rel("__kq_fio_save_invalid"));
            EmitAsm("LDA", Mem(_runtimeFdsFileHeaderAddress + 20));
            EmitAsm("BNE", Rel("__kq_fio_save_invalid"));
            EmitAsm("LDA", Mem(CallArgBase + 3));
            EmitAsm("CMP", Mem(_runtimeFdsFileHeaderAddress + 23));
            EmitAsm("BNE", Rel("__kq_fio_save_invalid"));
            EmitAsm("STA", Mem(CallArgBase + 10));
            EmitAsm("LDA", Mem(CallArgBase + 1));
            EmitAsm("STA", Mem(CallArgBase + 8));
            EmitAsm("LDA", Mem(CallArgBase + 4));
            EmitAsm("CMP", Mem(_runtimeFdsFileHeaderAddress + 24));
            EmitAsm("BNE", Rel("__kq_fio_save_invalid"));
            EmitAsm("STA", Mem(CallArgBase + 11));
            EmitAsm("LDA", Mem(CallArgBase + 2));
            EmitAsm("STA", Mem(CallArgBase + 9));
            EmitAsm("JSR", Abs("__kq_fds_check_span"));
            EmitAsm("CMP", Imm(0));
            EmitAsm("BNE", Rel("__kq_fio_save_invalid"));
            EmitAsm("LDA", Mem(_runtimeFdsFileHeaderAddress + 17));
            EmitAsm("STA", Mem(_runtimeFdsFileHeaderAddress + 0));
            EmitAsm("LDA", Mem(_runtimeFdsFileHeaderAddress + 25));
            EmitAsm("STA", Mem(_runtimeFdsFileHeaderAddress + 1));
            EmitAsm("LDA", Mem(_runtimeFdsFileHeaderAddress + 26));
            EmitAsm("STA", Mem(_runtimeFdsFileHeaderAddress + 2));
            EmitAsm("LDA", Mem(_runtimeFdsFileHeaderAddress + 27));
            EmitAsm("STA", Mem(_runtimeFdsFileHeaderAddress + 3));
            EmitAsm("LDA", Mem(_runtimeFdsFileHeaderAddress + 28));
            EmitAsm("STA", Mem(_runtimeFdsFileHeaderAddress + 4));
            EmitAsm("LDA", Mem(_runtimeFdsFileHeaderAddress + 29));
            EmitAsm("STA", Mem(_runtimeFdsFileHeaderAddress + 5));
            EmitAsm("LDA", Mem(_runtimeFdsFileHeaderAddress + 30));
            EmitAsm("STA", Mem(_runtimeFdsFileHeaderAddress + 6));
            EmitAsm("LDA", Mem(_runtimeFdsFileHeaderAddress + 31));
            EmitAsm("STA", Mem(_runtimeFdsFileHeaderAddress + 7));
            EmitAsm("LDA", Mem(_runtimeFdsFileHeaderAddress + 32));
            EmitAsm("STA", Mem(_runtimeFdsFileHeaderAddress + 8));
            EmitAsm("LDA", Mem(_runtimeFdsFileHeaderAddress + 21));
            EmitAsm("STA", Mem(_runtimeFdsFileHeaderAddress + 9));
            EmitAsm("LDA", Mem(CallArgBase + 3));
            EmitAsm("STA", Mem(_runtimeFdsFileHeaderAddress + 11));
            EmitAsm("LDA", Mem(CallArgBase + 1));
            EmitAsm("STA", Mem(_runtimeFdsFileHeaderAddress + 14));
            EmitAsm("LDA", Mem(_runtimeFdsFileHeaderAddress + 22));
            EmitAsm("STA", Mem(_runtimeFdsFileHeaderAddress + 10));
            EmitAsm("LDA", Mem(CallArgBase + 4));
            EmitAsm("STA", Mem(_runtimeFdsFileHeaderAddress + 12));
            EmitAsm("LDA", Mem(CallArgBase + 2));
            EmitAsm("STA", Mem(_runtimeFdsFileHeaderAddress + 15));
            EmitAsm("LDA", Imm(0));
            EmitAsm("STA", Mem(_runtimeFdsFileHeaderAddress + 13));
            EmitAsm("STA", Mem(_runtimeFdsFileHeaderAddress + 16));
            EmitAsm("LDA", Mem(_runtimeFdsFileHeaderAddress + 18));
            EmitAsm("JSR", Mem(0xE239));
            _assembly.Add(Expr.Make(Tag.Word, string.Format("${0:X4}", _runtimeFdsFileHeaderAddress + 33)));
            _assembly.Add(Expr.Make(Tag.Word, string.Format("${0:X4}", _runtimeFdsFileHeaderAddress)));
            EmitAsm("RTS");
            _assembly.Add(Expr.Make(Tag.Label, "__kq_fio_save_invalid"));
            EmitAsm("LDA", Imm(0xFF));
            EmitAsm("RTS");

            // Enable wave-RAM writes, copy 64 source bytes, then clear the wave-write control register.

fds_overlay.h — FDSのオーバーレイコード

__fds_current_bank

現在ロード済みとして記録されている論理バンク番号を返します。ディスクから読み直す操作は行いません。

書式

c
u8 __fds_current_bank(void);

戻り値

ソフトウェアが保持する8ビットの常駐バンク番号。標準起動直後は1、不明な状態では255です。

使用条件と制限

--mapper=fdsでビルドしてください。__fds_available以外のFDS専用関数をカートリッジ向けに呼ぶと、KQFC2420でビルドを拒否します。

FDSの標準起動処理は常駐記録を1で初期化します。__fds_load_bank()はロード成功時に記録を更新します。__fds_load_file()または__fds_load_overlay()による直接ロード、およびバンク転送の失敗では記録が255(不明)になります。メモリ内容の照合は行わないため、直接メモリを書き換える場合は記録に依存しないでください。独自のリセット処理を用意する場合は初期化も自分で管理します。

掲載画像はKUROSAKIでディスクの起動ファイルをRAMへ直接配置して確認したものです。このサンプルはBIOSを呼ばず、ファイルの読み書き、ディスク交換、実機での起動を検証していません。検証用の空のファームウェアはBIOSの代用品ではありません。サンプルは画面更新用NMIを使い、不要なマスク可能IRQを無効にしています。

ビルド例ではBIOSの起動画面用の追加ファイルを付けず、ネイティブFDS配置の.fdsを作ります。KUROSAKIの直接起動で確認するための構成です。実機用ディスクの起動手順は、これとは別に確認してください。

使用例

c
void main(void) {
    // These BIOS-free queries need NMI for display, but no maskable IRQ.
    __irq_disable();
    request_overlay=0;
    fds_result[0] = __fds_current_bank();
    fds_result[1] = __fds_is_bank_resident(1);
    fds_result[2] = __fds_is_bank_resident(2);
    fds_result[3] = __fds_overlay_function_count();
    if (request_overlay) {
        fds_result[4]=lesson_overlay_a(); fds_result[5]=lesson_overlay_b();
    }
    m_init();
    m_text(2,2,"FDS OVERLAY RECORDS"); m_wait();
    m_text(2,5,"CURRENT BANK"); m_put(25,5,'0'+fds_result[0]); m_wait();
    m_text(2,7,"BANK 1 RESIDENT"); m_put(25,7,'0'+fds_result[1]); m_wait();
    m_text(2,9,"BANK 2 RESIDENT"); m_put(25,9,'0'+fds_result[2]); m_wait();
    m_text(2,11,"OVERLAY FUNCTIONS"); m_put(25,11,'0'+fds_result[3]); m_wait();
    m_text(2,14,"NO OVERLAY LOAD TESTED"); m_wait();
    while (1) { m_wait(); }
}

呼び出し部分のコード例です。完全なプログラムに組み込む際は、記載した初期化とバッファの条件を満たしてください。

期待する結果

CURRENT BANKは1、BANK 1 RESIDENTは1、BANK 2 RESIDENTは0、OVERLAY FUNCTIONSは2です。バンク2に二つの関数を収録していても、起動直後にバンク2がロード済みになるわけではありません。--fds-no-overlay-tableを付けた検証では最後の値だけ0になります。

__fds_current_bank: RESIDENCY-FDS-DEFAULT — CURRENT BANKは1、BANK 1 RESIDENTは1、BANK 2 RESIDENTは0、OVERLAY FUNCTIONSは2です。バンク2に二つの関数を収録していても、起動直後にバンク2がロード済みになるわけではありません。--fds-no-overlay-tableを付けた検証では最後の値だけ0になります。
RESIDENCY-FDS-DEFAULT — CURRENT BANKは1、BANK 1 RESIDENTは1、BANK 2 RESIDENTは0、OVERLAY FUNCTIONSは2です。バンク2に二つの関数を収録していても、起動直後にバンク2がロード済みになるわけではありません。--fds-no-overlay-tableを付けた検証では最後の値だけ0になります。
__fds_current_bank: RESIDENCY-FDS-TABLE-OFF-DEFAULT — CURRENT BANKは1、BANK 1 RESIDENTは1、BANK 2 RESIDENTは0、OVERLAY FUNCTIONSは2です。バンク2に二つの関数を収録していても、起動直後にバンク2がロード済みになるわけではありません。--fds-no-overlay-tableを付けた検証では最後の値だけ0になります。
RESIDENCY-FDS-TABLE-OFF-DEFAULT — CURRENT BANKは1、BANK 1 RESIDENTは1、BANK 2 RESIDENTは0、OVERLAY FUNCTIONSは2です。バンク2に二つの関数を収録していても、起動直後にバンク2がロード済みになるわけではありません。--fds-no-overlay-tableを付けた検証では最後の値だけ0になります。

使用例の完全なプログラム

ビルドコマンド

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\fds_residency.c -I .\kitaqfc\lib -I .\kitaq-docs\samples -o .\out\fds_residency.fds --mapper=fds --fds-layout=fds32 --fds-no-license-bypass --nes-chr=.\kitaq-docs\samples\font.chr --no-cache --no-disasm
実装とソース

kitaqfc/lib/fds_overlay.h:16

宣言の注記(原文)
Return the software resident-bank byte, or FF after a failed bank transfer.
This is not a checksum of overlay memory.

kitaqfc/kitaqfc/CodeGenerator.cs:5222

csharp
case "__fds_current_bank":
                    if (!Need(0)) return true;
                    EmitAsm("LDA", Mem(_runtimeFdsResidentBankAddress));
                    return true;
__fds_farcall

FDS向けの名前で引数なしの遠隔関数呼び出しを行います。ロードと復元の動作は__fds_overlay_farcall()と同じです。

書式

c
u8 __fds_farcall(u8 bank, u16 func);

引数

bank

バンクを表す式。式は呼び出し前に一度だけ評価しますが、実際にロードするバンクはfuncのリンク時の配置から決まります。任意の番号へ同じアドレスを振り向ける機能ではありません。__bankof(func)など、対象と一致する値を渡してください。

func

宣言済みで引数を取らない関数の名前。サンプルはu8 overlay_answer(void)を渡します。数値アドレス、実行時の関数ポインター、引数を必要とする関数は受け付けません。引数を渡す場合は通常のtarget(a, b)形式を使います。

戻り値

対象関数が返した8ビット値。ロードに失敗して元のバンクを復元できた場合は、対象を呼ばずロードエラーのバイトを返します。そのため、関数の結果とエラーを区別する設計が必要です。

使用条件と制限

--mapper=fds --fds-layout=fds32向けです。バンク0は$A000~$DFFFの共通コード、バンク1は起動時の$6000~$9FFF、バンク2以降は同じ16 KiB窓へ読み込むオーバーレイです。バンク2以降は#pragma fixed_bank 2などで配置します。

バンク1は$6000・16384バイトの独立した起動PRGファイルから復元します。管理表を省略した標準構成ではID 0がバンク1、ID 1が共通コード、ID 2がCHRです。独自の管理表を使う場合は起動PRGを$6000と$A000の16384バイトずつに分けてください。バンク2以降のファイルIDは--fds-overlay-start-id(既定32)+ バンク番号 - 2です。

異なるバンクを呼ぶ場合は、呼び出し前のバンクを保存し、対象の実行後に復元してから戻ります。入れ子の呼び出しも同じ規則です。転送途中の失敗でも復元を試みます。復元そのものが失敗した場合は、上書きされたコードへ戻らず共通コード内で停止します。共通コード内の対象や同一バンクの対象には不要な転送を行いません。

BIOS呼び出しは同期処理です。読み込み中も毎フレームの処理が進むAPIではありません。割り込みハンドラーから呼ばず、共通コードに置いた通常の制御処理から使ってください。表示・音声の継続や実機でのBIOSとの共存は別途確認が必要です。

掲載画像は、独自に作成したLoadFilesの呼び出し規約テスト用処理で、コンパイルしたコード・メモリ転送・画面の全画素を確認したものです。実際のBIOS、ディスク転送タイミング、CRC、実機起動を検証した画像ではありません。KUROSAKIのFDSコントローラーは簡易モデルで、この検証用処理なしの通常実行だけでは同じ転送結果を再現できません。

下記コマンドの最初の行で配布サンプルをビルドします。Pythonの検証コマンドは5本のサンプルを最適化あり・なしでビルドし、KUROSAKIで確認します。検証用処理はscripts/fds_abi_fixture.pyから一時生成して削除するため、外部BIOSを用意する必要はありません。通常のゲームでこの検証用処理を使うものではありません。

使用例

c
#pragma fixed_bank 0
#include "fc_fds_scene.h"
u8 choose_overlay(void){fds_result[2]++;return 2;}
#pragma fixed_bank 2
u8 overlay_answer(void){fds_result[1]++;fds_result[3]=__fds_current_bank();return 42;}
#pragma fixed_bank 0
void main(void) {
    __irq_disable();fds_result[1]=0;fds_result[2]=0;
    fds_result[0]=__fds_farcall(choose_overlay(),overlay_answer);
    fds_result[4]=__fds_current_bank();fds_result[15]=165;
    fds_call_screen();while(1){m_wait();}
}

呼び出し部分のコード例です。完全なプログラムに組み込む際は、記載した初期化とバッファの条件を満たしてください。

期待する結果

独自のLoadFiles呼び出し規約テストでは、RETURN VALUEが042、CALLBACK COUNTとBANK EXPR COUNTが各001、BANK IN CALLBACKが002、BANK AFTER RETURNが001になります。対象とバンク式が一度ずつ実行され、バンク2で関数を呼んだ後にバンク1へ復元したことを表します。実際のディスク転送の検証画像ではありません。

__fds_farcall: FARCALL-DEFAULT — 独自のLoadFiles呼び出し規約テストでは、RETURN VALUEが042、CALLBACK COUNTとBANK EXPR COUNTが各001、BANK IN CALLBACKが002、BANK AFTER RETURNが001になります。対象とバンク式が一度ずつ実行され、バンク2で関数を呼んだ後にバンク1へ復元したことを表します。実際のディスク転送の検証画像ではありません。
FARCALL-DEFAULT — 独自のLoadFiles呼び出し規約テストでは、RETURN VALUEが042、CALLBACK COUNTとBANK EXPR COUNTが各001、BANK IN CALLBACKが002、BANK AFTER RETURNが001になります。対象とバンク式が一度ずつ実行され、バンク2で関数を呼んだ後にバンク1へ復元したことを表します。実際のディスク転送の検証画像ではありません。

使用例の完全なプログラム

ビルドコマンド

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\fds_farcall.c -I .\kitaqfc\lib -I .\kitaq-docs\samples -o .\out\fds_farcall.fds --mapper=fds --fds-layout=fds32 --fds-no-license-bypass --fds-overlay-trim --nes-chr=.\kitaq-docs\samples\font.chr --no-cache --no-disasm
python .\kitaq-docs\tools\check_fds_load_examples.py
実装とソース

kitaqfc/lib/fds_overlay.h:25

宣言の注記(原文)
Same named, zero-argument dispatch as __fds_overlay_farcall; evaluate bank once.

kitaqfc/kitaqfc/CodeGenerator.cs:4074

csharp
if (string.Equals(funcName, "__fds_overlay_farcall", StringComparison.Ordinal) ||
                string.Equals(funcName, "__fds_farcall", StringComparison.Ordinal))
            {
                // FDS overlay dispatch requires the configured FDS RAM layout and a named function target.
                Expr[] farArgs = args ?? Array.Empty<Expr>();
                if (farArgs.Length != 2)
                {
                    Program.Error("error KQFC2512: {0}(bank, func) expects exactly 2 arguments.", funcName);
                    return;
                }
                string targetName;
                if (!farArgs[1].Match(Tag.Name, out targetName))
                {
                    Program.Error("error KQFC2513: {0}(bank, func) requires the 2nd argument to be a function name.", funcName);
                    return;
                }
                if (!Program.NesMapperProfile.HasFds || !Program.FdsPrgRamLayoutEnabled)
                {
                    Program.Error("error KQFC2514: {0} requires --mapper=fds with --fds-layout=fds32.", funcName);
                    return;
                }
                // Like ordinary __farcall, this entry supplies no callback arguments.
                // Validate before emitting code so a data symbol cannot become a JSR target.
                if (!_functionParameters.TryGetValue(targetName, out FieldInfo[] fdsParameters))
                {
                    Program.Error("error KQFC2610: {0} requires a declared function name; use an ordinary typed call for a function pointer.", funcName);
                    return;
                }
                if (fdsParameters.Length != 0)
                {
                    Program.Error("error KQFC2610: {0} requires a callback with no parameters.", funcName);
                    return;
                }
                // Evaluate the bank expression exactly once, even for a safe direct call.
                // The named target's linked placement determines the dispatched bank.
                EmitLoadValue(farArgs[0], ctx, 1);
                int callerBank = ctx == null ? 0 : ctx.Bank;
                int calleeBank = GetFunctionBank(targetName);
                if (ShouldUseFdsOverlayThunk(callerBank, calleeBank))
                {
                    string directFdsThunkName = EnsureFdsOverlayThunk(targetName, calleeBank);
                    EmitAsm("JSR", Abs(directFdsThunkName));
                    RecordCallEdge(targetName, calleeBank, "fds_overlay_thunk", viaThunk: true, viaFarcall: true);
                    return;
                }
                if (calleeBank == 0 || calleeBank == callerBank)
                {
                    EmitAsm("JSR", Abs(targetName));
                    RecordCallEdge(targetName, calleeBank, "direct", viaThunk: false, viaFarcall: true);
                    return;
                }
                string fdsThunkName = EnsureFdsOverlayThunk(targetName, calleeBank);
                EmitAsm("JSR", Abs(fdsThunkName));
                RecordCallEdge(targetName, calleeBank, "fds_overlay_thunk", viaThunk: true, viaFarcall: true);
                return;
            }
__fds_is_bank_resident

指定バンクが常駐記録と一致するかを調べます。ロードが必要か判断する前に使えますが、この関数自体はロードしません。

書式

c
u8 __fds_is_bank_resident(u8 bank);

引数

bank

比較する論理バンク番号。標準起動時の記録は1、自動オーバーレイは2以降です。0は共通コードのバンクであり、この常駐記録の初期値ではありません。

戻り値

有効な番号が記録と一致すれば1、それ以外は0。255(不明)は記録が255でも0です。

使用条件と制限

--mapper=fdsでビルドしてください。__fds_available以外のFDS専用関数をカートリッジ向けに呼ぶと、KQFC2420でビルドを拒否します。

FDSの標準起動処理は常駐記録を1で初期化します。__fds_load_bank()はロード成功時に記録を更新します。__fds_load_file()または__fds_load_overlay()による直接ロード、およびバンク転送の失敗では記録が255(不明)になります。メモリ内容の照合は行わないため、直接メモリを書き換える場合は記録に依存しないでください。独自のリセット処理を用意する場合は初期化も自分で管理します。

掲載画像はKUROSAKIでディスクの起動ファイルをRAMへ直接配置して確認したものです。このサンプルはBIOSを呼ばず、ファイルの読み書き、ディスク交換、実機での起動を検証していません。検証用の空のファームウェアはBIOSの代用品ではありません。サンプルは画面更新用NMIを使い、不要なマスク可能IRQを無効にしています。

ビルド例ではBIOSの起動画面用の追加ファイルを付けず、ネイティブFDS配置の.fdsを作ります。KUROSAKIの直接起動で確認するための構成です。実機用ディスクの起動手順は、これとは別に確認してください。

使用例

c
void main(void) {
    // These BIOS-free queries need NMI for display, but no maskable IRQ.
    __irq_disable();
    request_overlay=0;
    fds_result[0] = __fds_current_bank();
    fds_result[1] = __fds_is_bank_resident(1);
    fds_result[2] = __fds_is_bank_resident(2);
    fds_result[3] = __fds_overlay_function_count();
    if (request_overlay) {
        fds_result[4]=lesson_overlay_a(); fds_result[5]=lesson_overlay_b();
    }
    m_init();
    m_text(2,2,"FDS OVERLAY RECORDS"); m_wait();
    m_text(2,5,"CURRENT BANK"); m_put(25,5,'0'+fds_result[0]); m_wait();
    m_text(2,7,"BANK 1 RESIDENT"); m_put(25,7,'0'+fds_result[1]); m_wait();
    m_text(2,9,"BANK 2 RESIDENT"); m_put(25,9,'0'+fds_result[2]); m_wait();
    m_text(2,11,"OVERLAY FUNCTIONS"); m_put(25,11,'0'+fds_result[3]); m_wait();
    m_text(2,14,"NO OVERLAY LOAD TESTED"); m_wait();
    while (1) { m_wait(); }
}

呼び出し部分のコード例です。完全なプログラムに組み込む際は、記載した初期化とバッファの条件を満たしてください。

期待する結果

CURRENT BANKは1、BANK 1 RESIDENTは1、BANK 2 RESIDENTは0、OVERLAY FUNCTIONSは2です。バンク2に二つの関数を収録していても、起動直後にバンク2がロード済みになるわけではありません。--fds-no-overlay-tableを付けた検証では最後の値だけ0になります。

__fds_is_bank_resident: RESIDENCY-FDS-DEFAULT — CURRENT BANKは1、BANK 1 RESIDENTは1、BANK 2 RESIDENTは0、OVERLAY FUNCTIONSは2です。バンク2に二つの関数を収録していても、起動直後にバンク2がロード済みになるわけではありません。--fds-no-overlay-tableを付けた検証では最後の値だけ0になります。
RESIDENCY-FDS-DEFAULT — CURRENT BANKは1、BANK 1 RESIDENTは1、BANK 2 RESIDENTは0、OVERLAY FUNCTIONSは2です。バンク2に二つの関数を収録していても、起動直後にバンク2がロード済みになるわけではありません。--fds-no-overlay-tableを付けた検証では最後の値だけ0になります。
__fds_is_bank_resident: RESIDENCY-FDS-TABLE-OFF-DEFAULT — CURRENT BANKは1、BANK 1 RESIDENTは1、BANK 2 RESIDENTは0、OVERLAY FUNCTIONSは2です。バンク2に二つの関数を収録していても、起動直後にバンク2がロード済みになるわけではありません。--fds-no-overlay-tableを付けた検証では最後の値だけ0になります。
RESIDENCY-FDS-TABLE-OFF-DEFAULT — CURRENT BANKは1、BANK 1 RESIDENTは1、BANK 2 RESIDENTは0、OVERLAY FUNCTIONSは2です。バンク2に二つの関数を収録していても、起動直後にバンク2がロード済みになるわけではありません。--fds-no-overlay-tableを付けた検証では最後の値だけ0になります。

使用例の完全なプログラム

ビルドコマンド

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\fds_residency.c -I .\kitaqfc\lib -I .\kitaq-docs\samples -o .\out\fds_residency.fds --mapper=fds --fds-layout=fds32 --fds-no-license-bypass --nes-chr=.\kitaq-docs\samples\font.chr --no-cache --no-disasm
実装とソース

kitaqfc/lib/fds_overlay.h:18

宣言の注記(原文)
Compare a valid bank with the software resident-bank byte; FF (unknown) never matches.

kitaqfc/kitaqfc/CodeGenerator.cs:9465

csharp
EmitHelperStart("__fds_is_bank_resident");
            string fdsResidentYes = NewGeneratedLabel("fds_resident_yes");
            string fdsResidentNo = NewGeneratedLabel("fds_resident_no");
            EmitAsm("LDA", Mem(CallArgBase));
            EmitAsm("CMP", Imm(0xFF));
            EmitAsm("BEQ", Rel(fdsResidentNo));
            EmitAsm("CMP", Mem(_runtimeFdsResidentBankAddress));
            EmitAsm("BEQ", Rel(fdsResidentYes));
            _assembly.Add(Expr.Make(Tag.Label, fdsResidentNo));
            EmitAsm("LDA", Imm(0));
            EmitAsm("RTS");
            _assembly.Add(Expr.Make(Tag.Label, fdsResidentYes));
            EmitAsm("LDA", Imm(1));
            EmitAsm("RTS");

            // Query file existence in the compiled metadata table.
__fds_load_bank

論理バンクをファイルIDへ変換し、$6000~$9FFFのオーバーレイ窓へ読み込みます。読み込み後、そのバンクを常駐中として記録します。

書式

c
u8 __fds_load_bank(u8 bank);

引数

bank

ロードする論理バンク番号1~254。0は共通コードなのでロード対象ではなく、255は不明状態の予約値です。バンク2以降では、計算したファイルIDも0~254に収まる必要があります。

戻り値

成功時は0。番号0・255、ファイルIDの桁あふれ、復元用ファイルがないバンク1などは255。BIOS転送が失敗した場合はそのエラーバイトです。転送前の引数エラーでは常駐記録を変更せず、BIOS転送失敗時は255(不明)にします。

使用条件と制限

--mapper=fds --fds-layout=fds32向けです。バンク0は$A000~$DFFFの共通コード、バンク1は起動時の$6000~$9FFF、バンク2以降は同じ16 KiB窓へ読み込むオーバーレイです。バンク2以降は#pragma fixed_bank 2などで配置します。

バンク1は$6000・16384バイトの独立した起動PRGファイルから復元します。管理表を省略した標準構成ではID 0がバンク1、ID 1が共通コード、ID 2がCHRです。独自の管理表を使う場合は起動PRGを$6000と$A000の16384バイトずつに分けてください。バンク2以降のファイルIDは--fds-overlay-start-id(既定32)+ バンク番号 - 2です。

既定の常駐ガードが有効で、指定バンクがすでに記録されていれば、ディスクを読まず0を返します。--fds-no-overlay-guardは__fds_load_bank()のこの省略処理を無効にします。ロード成功後はバンク番号を記録します。記録はメモリの検査結果ではありません。

ロード元と同じ$6000~$9FFF窓に置いた関数から直接バンクを切り替えると、自分のコードや復帰先を上書きします。このサンプルは呼び出し元と表示処理を#pragma fixed_bank 0へ置きます。別バンクの関数を呼んで戻る用途には__fds_overlay_farcall()または通常の型付き関数呼び出しを使います。

BIOS呼び出しは同期処理です。読み込み中も毎フレームの処理が進むAPIではありません。割り込みハンドラーから呼ばず、共通コードに置いた通常の制御処理から使ってください。表示・音声の継続や実機でのBIOSとの共存は別途確認が必要です。

掲載画像は、独自に作成したLoadFilesの呼び出し規約テスト用処理で、コンパイルしたコード・メモリ転送・画面の全画素を確認したものです。実際のBIOS、ディスク転送タイミング、CRC、実機起動を検証した画像ではありません。KUROSAKIのFDSコントローラーは簡易モデルで、この検証用処理なしの通常実行だけでは同じ転送結果を再現できません。

下記コマンドの最初の行で配布サンプルをビルドします。Pythonの検証コマンドは5本のサンプルを最適化あり・なしでビルドし、KUROSAKIで確認します。検証用処理はscripts/fds_abi_fixture.pyから一時生成して削除するため、外部BIOSを用意する必要はありません。通常のゲームでこの検証用処理を使うものではありません。

使用例

c
#pragma fixed_bank 0
#include "fc_fds_scene.h"
#pragma fixed_bank 2
__prg_rom u8 bank_payload[4]={11,22,33,44};
#pragma fixed_bank 0
void main(void) {
    __irq_disable();
    fds_result[0]=__fds_load_bank(2);
    fds_result[1]=__fds_current_bank();
    fds_result[2]=__fds_require_bank(2);
    fds_result[3]=bank_payload[0];fds_result[4]=bank_payload[3];
    fds_result[5]=nes_fds_load_bank(1);
    fds_result[6]=__fds_current_bank();
    fds_result[7]=__fds_require_bank(1);
    fds_result[8]=__fds_load_bank(0);
    fds_result[9]=__fds_load_bank(255);
    fds_result[15]=165;
    m_init();m_text(2,2,"FDS DATA WINDOW");m_wait();
    m_text(2,5,"LOAD BANK 2 STATUS");fds_number(25,5,fds_result[0]);m_wait();
    m_text(2,7,"FIRST / LAST BYTE");fds_number(21,7,fds_result[3]);fds_number(25,7,fds_result[4]);m_wait();
    m_text(2,9,"REQUIRE SAME BANK");fds_number(25,9,fds_result[2]);m_wait();
    m_text(2,11,"RESTORE BANK 1");fds_number(25,11,fds_result[5]);m_wait();
    m_text(2,13,"RESIDENT BANK");fds_number(25,13,fds_result[6]);m_wait();
    m_text(2,16,"INVALID BANKS 0 / 255");m_wait();
    fds_number(2,18,fds_result[8]);fds_number(8,18,fds_result[9]);m_wait();
    while(1){m_wait();}
}

呼び出し部分のコード例です。完全なプログラムに組み込む際は、記載した初期化とバッファの条件を満たしてください。

期待する結果

独自のLoadFiles呼び出し規約テストでは、LOAD BANK 2 STATUS・REQUIRE SAME BANK・RESTORE BANK 1が000です。FIRST / LAST BYTEの011と044は実際に転送した4バイトの両端、RESIDENT BANKの001は復元後の番号です。無効なバンク0と255はそれぞれ255を返します。同一バンクのrequireは追加のロードを起こさず、検証したファイル読み込み要求は32→0の2回です。

__fds_load_bank: BANK_LOAD-DEFAULT — 独自のLoadFiles呼び出し規約テストでは、LOAD BANK 2 STATUS・REQUIRE SAME BANK・RESTORE BANK 1が000です。FIRST / LAST BYTEの011と044は実際に転送した4バイトの両端、RESIDENT BANKの001は復元後の番号です。無効なバンク0と255はそれぞれ255を返します。同一バンクのrequireは追加のロードを起こさず、検証したファイル読み込み要求は32→0の2回です。
BANK_LOAD-DEFAULT — 独自のLoadFiles呼び出し規約テストでは、LOAD BANK 2 STATUS・REQUIRE SAME BANK・RESTORE BANK 1が000です。FIRST / LAST BYTEの011と044は実際に転送した4バイトの両端、RESIDENT BANKの001は復元後の番号です。無効なバンク0と255はそれぞれ255を返します。同一バンクのrequireは追加のロードを起こさず、検証したファイル読み込み要求は32→0の2回です。

使用例の完全なプログラム

ビルドコマンド

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\fds_bank_load.c -I .\kitaqfc\lib -I .\kitaq-docs\samples -o .\out\fds_bank_load.fds --mapper=fds --fds-layout=fds32 --fds-no-license-bypass --fds-overlay-trim --nes-chr=.\kitaq-docs\samples\font.chr --no-cache --no-disasm
python .\kitaq-docs\tools\check_fds_load_examples.py
実装とソース

kitaqfc/lib/fds_overlay.h:11

宣言の注記(原文)
Load logical bank 1 from its separate boot file, or bank 2+ from overlay-start-id+(bank-2).
Return zero on success, FF for invalid/unavailable mappings, or the BIOS error.
A failed BIOS transfer marks residency FF (unknown); a matching valid guard can skip loading.

kitaqfc/kitaqfc/CodeGenerator.cs:9394

csharp
EmitHelperStart("__fds_load_bank");
            string fdsLoadBankLoad = NewGeneratedLabel("fds_load_bank_load");
            string fdsLoadBankOverlay = NewGeneratedLabel("fds_load_bank_overlay");
            string fdsLoadBankCall = NewGeneratedLabel("fds_load_bank_call");
            string fdsLoadBankDone = NewGeneratedLabel("fds_load_bank_done");
            string fdsLoadBankPopInvalid = NewGeneratedLabel("fds_load_bank_pop_invalid");
            string fdsLoadBankInvalid = NewGeneratedLabel("fds_load_bank_invalid");
            EmitAsm("LDA", Mem(CallArgBase));
            EmitAsm("CMP", Imm(1));
            EmitAsm("BCC", Rel(fdsLoadBankInvalid));
            EmitAsm("CMP", Imm(0xFF));
            EmitAsm("BEQ", Rel(fdsLoadBankInvalid));
            if (Program.FdsOverlayResidencyGuardEnabled)
            {
                EmitAsm("CMP", Mem(_runtimeFdsResidentBankAddress));
                EmitAsm("BNE", Rel(fdsLoadBankLoad));
                EmitAsm("LDA", Imm(0));
                EmitAsm("RTS");
                _assembly.Add(Expr.Make(Tag.Label, fdsLoadBankLoad));
            }
            EmitAsm("PHA");
            EmitAsm("CMP", Imm(1));
            EmitAsm("BNE", Rel(fdsLoadBankOverlay));
            EmitAsm("LDA", Abs("__kq_fds_boot_bank_file_id"));
            EmitAsm("CMP", Imm(0xFF));
            EmitAsm("BEQ", Rel(fdsLoadBankPopInvalid));
            EmitAsm("JMP", Abs(fdsLoadBankCall));
            _assembly.Add(Expr.Make(Tag.Label, fdsLoadBankOverlay));
            EmitAsm("SEC");
            EmitAsm("SBC", Imm(2));
            EmitAsm("CLC");
            EmitAsm("ADC", Imm(Program.FdsOverlayStartId & 0xFF));
            EmitAsm("BCS", Rel(fdsLoadBankPopInvalid));
            EmitAsm("CMP", Imm(0xFF));
            EmitAsm("BEQ", Rel(fdsLoadBankPopInvalid));
            _assembly.Add(Expr.Make(Tag.Label, fdsLoadBankCall));
            EmitAsm("STA", Mem(CallArgBase));
            EmitAsm("JSR", Abs("__fds_load_overlay"));
            EmitAsm("TAX");
            EmitAsm("PLA");
            EmitAsm("CPX", Imm(0));
            EmitAsm("BEQ", Rel(fdsLoadBankDone));
            // A failed BIOS transfer may have overwritten part of the window.
            // Mark it unknown so a subsequent restore cannot be skipped by the guard.
            EmitAsm("LDA", Imm(0xFF));
            _assembly.Add(Expr.Make(Tag.Label, fdsLoadBankDone));
            EmitAsm("STA", Mem(_runtimeFdsResidentBankAddress));
            EmitAsm("STA", Mem(_runtimeCurrentBankAddress));
            EmitAsm("TXA");
            EmitAsm("RTS");
            _assembly.Add(Expr.Make(Tag.Label, fdsLoadBankPopInvalid));
            EmitAsm("PLA");
            _assembly.Add(Expr.Make(Tag.Label, fdsLoadBankInvalid));
            EmitAsm("LDA", Imm(0xFF));
            EmitAsm("RTS");

            // Return success for the recorded resident bank, otherwise dispatch a bank load.
__fds_load_overlay

論理バンク番号ではなくディスクのファイルIDを指定してBIOSのLoadFilesを呼びます。ロード先はそのファイルのヘッダーで決まります。

書式

c
u8 __fds_load_overlay(u8 id);

引数

id

ディスク上のファイルID(0~254)。255はBIOSのファイルリスト終端に使うため渡さないでください。既定構成でバンク2を読むIDは32です。

戻り値

BIOSが返したエラーバイト(0が成功)。転送後の常駐記録は成功・失敗とも255(不明)です。ファイルIDだけでは論理バンクとの対応を確定しません。

使用条件と制限

--mapper=fds --fds-layout=fds32向けです。バンク0は$A000~$DFFFの共通コード、バンク1は起動時の$6000~$9FFF、バンク2以降は同じ16 KiB窓へ読み込むオーバーレイです。バンク2以降は#pragma fixed_bank 2などで配置します。

指定したファイルだけを読むため、要求IDと終端255のリストを作ります。ファイルが共通コードや実行中のコードを上書きしないよう、管理表のロード先を確認してください。論理バンクとして使う場合は__fds_load_bank()が記録管理を行います。

ロード元と同じ$6000~$9FFF窓に置いた関数から直接バンクを切り替えると、自分のコードや復帰先を上書きします。このサンプルは呼び出し元と表示処理を#pragma fixed_bank 0へ置きます。別バンクの関数を呼んで戻る用途には__fds_overlay_farcall()または通常の型付き関数呼び出しを使います。

BIOS呼び出しは同期処理です。読み込み中も毎フレームの処理が進むAPIではありません。割り込みハンドラーから呼ばず、共通コードに置いた通常の制御処理から使ってください。表示・音声の継続や実機でのBIOSとの共存は別途確認が必要です。

掲載画像は、独自に作成したLoadFilesの呼び出し規約テスト用処理で、コンパイルしたコード・メモリ転送・画面の全画素を確認したものです。実際のBIOS、ディスク転送タイミング、CRC、実機起動を検証した画像ではありません。KUROSAKIのFDSコントローラーは簡易モデルで、この検証用処理なしの通常実行だけでは同じ転送結果を再現できません。

下記コマンドの最初の行で配布サンプルをビルドします。Pythonの検証コマンドは5本のサンプルを最適化あり・なしでビルドし、KUROSAKIで確認します。検証用処理はscripts/fds_abi_fixture.pyから一時生成して削除するため、外部BIOSを用意する必要はありません。通常のゲームでこの検証用処理を使うものではありません。

使用例

c
#pragma fixed_bank 0
#include "fc_fds_scene.h"
#pragma fixed_bank 2
__prg_rom u8 raw_payload[4]={11,22,33,44};
#pragma fixed_bank 0
void main(void) {
    __irq_disable();
    fds_result[0]=__fds_load_overlay(32);
    fds_result[1]=__fds_current_bank();
    fds_result[2]=__fds_is_bank_resident(1);
    fds_result[3]=raw_payload[0];fds_result[4]=raw_payload[3];
    fds_result[5]=__fds_require_bank(1);
    fds_result[6]=__fds_current_bank();
    fds_result[15]=165;
    m_init();m_text(2,2,"FDS RAW FILE-ID LOAD");m_wait();
    m_text(2,5,"FILE 32 LOAD STATUS");fds_number(25,5,fds_result[0]);m_wait();
    m_text(2,7,"FIRST / LAST BYTE");fds_number(21,7,fds_result[3]);fds_number(25,7,fds_result[4]);m_wait();
    m_text(2,9,"UNKNOWN BANK RECORD");fds_number(25,9,fds_result[1]);m_wait();
    m_text(2,11,"IS BANK 1 RESIDENT");fds_number(25,11,fds_result[2]);m_wait();
    m_text(2,13,"REQUIRE BANK 1");fds_number(25,13,fds_result[5]);m_wait();
    m_text(2,15,"RESIDENT AFTER RELOAD");fds_number(25,15,fds_result[6]);m_wait();
    while(1){m_wait();}
}

呼び出し部分のコード例です。完全なプログラムに組み込む際は、記載した初期化とバッファの条件を満たしてください。

期待する結果

独自のLoadFiles呼び出し規約テストでは、FILE 32 LOAD STATUSが000、転送したデータの両端が011と044です。RAWロード直後のUNKNOWN BANK RECORDは255、IS BANK 1 RESIDENTは000になります。REQUIRE BANK 1が000で成功した後はRESIDENT AFTER RELOADが001です。ファイルIDで読む操作と、論理バンクを管理する操作の違いを示します。

__fds_load_overlay: RAW_OVERLAY-DEFAULT — 独自のLoadFiles呼び出し規約テストでは、FILE 32 LOAD STATUSが000、転送したデータの両端が011と044です。RAWロード直後のUNKNOWN BANK RECORDは255、IS BANK 1 RESIDENTは000になります。REQUIRE BANK 1が000で成功した後はRESIDENT AFTER RELOADが001です。ファイルIDで読む操作と、論理バンクを管理する操作の違いを示します。
RAW_OVERLAY-DEFAULT — 独自のLoadFiles呼び出し規約テストでは、FILE 32 LOAD STATUSが000、転送したデータの両端が011と044です。RAWロード直後のUNKNOWN BANK RECORDは255、IS BANK 1 RESIDENTは000になります。REQUIRE BANK 1が000で成功した後はRESIDENT AFTER RELOADが001です。ファイルIDで読む操作と、論理バンクを管理する操作の違いを示します。

使用例の完全なプログラム

ビルドコマンド

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\fds_raw_overlay.c -I .\kitaqfc\lib -I .\kitaq-docs\samples -o .\out\fds_raw_overlay.fds --mapper=fds --fds-layout=fds32 --fds-no-license-bypass --fds-overlay-trim --nes-chr=.\kitaq-docs\samples\font.chr --no-cache --no-disasm
python .\kitaq-docs\tools\check_fds_load_examples.py
実装とソース

kitaqfc/lib/fds_overlay.h:7

宣言の注記(原文)
Load disk file id through BIOS, return its error byte and mark bank residency FF (unknown).

kitaqfc/kitaqfc/CodeGenerator.cs:9375

csharp
EmitHelperStart("__fds_load_overlay");
            EmitAsm("LDA", Mem(CallArgBase));
            EmitAsm("STA", Mem(_runtimeFdsLoadListAddress));
            EmitAsm("LDA", Imm(0xFF));
            EmitAsm("STA", Mem(_runtimeFdsLoadListAddress + 1));
            EmitAsm("JSR", Mem(0xE1F8));
            _assembly.Add(Expr.Make(Tag.Word, "__kq_fds_disk_id_wildcard"));
            _assembly.Add(Expr.Make(Tag.Word, string.Format("${0:X4}", _runtimeFdsLoadListAddress)));
            // A raw file load can replace any part of the switchable window.
            // Only the bank-aware wrapper may establish a known resident bank.
            EmitAsm("TAX");
            EmitAsm("LDA", Imm(0xFF));
            EmitAsm("STA", Mem(_runtimeFdsResidentBankAddress));
            EmitAsm("STA", Mem(_runtimeCurrentBankAddress));
            EmitAsm("TXA");
            EmitAsm("RTS");

            // Restore bank 1 from its separate boot file; map banks 2+ to overlay
            // IDs. Keep the requested bank on the CPU stack across the BIOS call.
__fds_overlay_farcall

オーバーレイに配置した引数なし関数を呼び、呼び出し前のバンクを復元して戻ります。

書式

c
u8 __fds_overlay_farcall(u8 bank, u16 func);

引数

bank

バンクを表す式。式は呼び出し前に一度だけ評価しますが、実際にロードするバンクはfuncのリンク時の配置から決まります。任意の番号へ同じアドレスを振り向ける機能ではありません。__bankof(func)など、対象と一致する値を渡してください。

func

宣言済みで引数を取らない関数の名前。サンプルはu8 overlay_answer(void)を渡します。数値アドレス、実行時の関数ポインター、引数を必要とする関数は受け付けません。引数を渡す場合は通常のtarget(a, b)形式を使います。

戻り値

対象関数が返した8ビット値。ロードに失敗して元のバンクを復元できた場合は、対象を呼ばずロードエラーのバイトを返します。そのため、関数の結果とエラーを区別する設計が必要です。

使用条件と制限

--mapper=fds --fds-layout=fds32向けです。バンク0は$A000~$DFFFの共通コード、バンク1は起動時の$6000~$9FFF、バンク2以降は同じ16 KiB窓へ読み込むオーバーレイです。バンク2以降は#pragma fixed_bank 2などで配置します。

バンク1は$6000・16384バイトの独立した起動PRGファイルから復元します。管理表を省略した標準構成ではID 0がバンク1、ID 1が共通コード、ID 2がCHRです。独自の管理表を使う場合は起動PRGを$6000と$A000の16384バイトずつに分けてください。バンク2以降のファイルIDは--fds-overlay-start-id(既定32)+ バンク番号 - 2です。

異なるバンクを呼ぶ場合は、呼び出し前のバンクを保存し、対象の実行後に復元してから戻ります。入れ子の呼び出しも同じ規則です。転送途中の失敗でも復元を試みます。復元そのものが失敗した場合は、上書きされたコードへ戻らず共通コード内で停止します。共通コード内の対象や同一バンクの対象には不要な転送を行いません。

BIOS呼び出しは同期処理です。読み込み中も毎フレームの処理が進むAPIではありません。割り込みハンドラーから呼ばず、共通コードに置いた通常の制御処理から使ってください。表示・音声の継続や実機でのBIOSとの共存は別途確認が必要です。

掲載画像は、独自に作成したLoadFilesの呼び出し規約テスト用処理で、コンパイルしたコード・メモリ転送・画面の全画素を確認したものです。実際のBIOS、ディスク転送タイミング、CRC、実機起動を検証した画像ではありません。KUROSAKIのFDSコントローラーは簡易モデルで、この検証用処理なしの通常実行だけでは同じ転送結果を再現できません。

下記コマンドの最初の行で配布サンプルをビルドします。Pythonの検証コマンドは5本のサンプルを最適化あり・なしでビルドし、KUROSAKIで確認します。検証用処理はscripts/fds_abi_fixture.pyから一時生成して削除するため、外部BIOSを用意する必要はありません。通常のゲームでこの検証用処理を使うものではありません。

使用例

c
#pragma fixed_bank 0
#include "fc_fds_scene.h"
u8 choose_overlay(void){fds_result[2]++;return 2;}
#pragma fixed_bank 2
u8 overlay_answer(void){fds_result[1]++;fds_result[3]=__fds_current_bank();return 42;}
#pragma fixed_bank 0
void main(void) {
    __irq_disable();fds_result[1]=0;fds_result[2]=0;
    fds_result[0]=__fds_overlay_farcall(choose_overlay(),overlay_answer);
    fds_result[4]=__fds_current_bank();fds_result[15]=165;
    fds_call_screen();while(1){m_wait();}
}

呼び出し部分のコード例です。完全なプログラムに組み込む際は、記載した初期化とバッファの条件を満たしてください。

期待する結果

独自のLoadFiles呼び出し規約テストでは、RETURN VALUEが042、CALLBACK COUNTとBANK EXPR COUNTが各001、BANK IN CALLBACKが002、BANK AFTER RETURNが001になります。対象とバンク式が一度ずつ実行され、バンク2で関数を呼んだ後にバンク1へ復元したことを表します。実際のディスク転送の検証画像ではありません。

__fds_overlay_farcall: OVERLAY_CALL-DEFAULT — 独自のLoadFiles呼び出し規約テストでは、RETURN VALUEが042、CALLBACK COUNTとBANK EXPR COUNTが各001、BANK IN CALLBACKが002、BANK AFTER RETURNが001になります。対象とバンク式が一度ずつ実行され、バンク2で関数を呼んだ後にバンク1へ復元したことを表します。実際のディスク転送の検証画像ではありません。
OVERLAY_CALL-DEFAULT — 独自のLoadFiles呼び出し規約テストでは、RETURN VALUEが042、CALLBACK COUNTとBANK EXPR COUNTが各001、BANK IN CALLBACKが002、BANK AFTER RETURNが001になります。対象とバンク式が一度ずつ実行され、バンク2で関数を呼んだ後にバンク1へ復元したことを表します。実際のディスク転送の検証画像ではありません。

使用例の完全なプログラム

ビルドコマンド

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\fds_overlay_call.c -I .\kitaqfc\lib -I .\kitaq-docs\samples -o .\out\fds_overlay_call.fds --mapper=fds --fds-layout=fds32 --fds-no-license-bypass --fds-overlay-trim --nes-chr=.\kitaq-docs\samples\font.chr --no-cache --no-disasm
python .\kitaq-docs\tools\check_fds_load_examples.py
実装とソース

kitaqfc/lib/fds_overlay.h:23

宣言の注記(原文)
Evaluate bank once and call the named zero-argument function using its linked bank.
Restore the caller window before returning; restoration failure stops in common code.

kitaqfc/kitaqfc/CodeGenerator.cs:4074

csharp
if (string.Equals(funcName, "__fds_overlay_farcall", StringComparison.Ordinal) ||
                string.Equals(funcName, "__fds_farcall", StringComparison.Ordinal))
            {
                // FDS overlay dispatch requires the configured FDS RAM layout and a named function target.
                Expr[] farArgs = args ?? Array.Empty<Expr>();
                if (farArgs.Length != 2)
                {
                    Program.Error("error KQFC2512: {0}(bank, func) expects exactly 2 arguments.", funcName);
                    return;
                }
                string targetName;
                if (!farArgs[1].Match(Tag.Name, out targetName))
                {
                    Program.Error("error KQFC2513: {0}(bank, func) requires the 2nd argument to be a function name.", funcName);
                    return;
                }
                if (!Program.NesMapperProfile.HasFds || !Program.FdsPrgRamLayoutEnabled)
                {
                    Program.Error("error KQFC2514: {0} requires --mapper=fds with --fds-layout=fds32.", funcName);
                    return;
                }
                // Like ordinary __farcall, this entry supplies no callback arguments.
                // Validate before emitting code so a data symbol cannot become a JSR target.
                if (!_functionParameters.TryGetValue(targetName, out FieldInfo[] fdsParameters))
                {
                    Program.Error("error KQFC2610: {0} requires a declared function name; use an ordinary typed call for a function pointer.", funcName);
                    return;
                }
                if (fdsParameters.Length != 0)
                {
                    Program.Error("error KQFC2610: {0} requires a callback with no parameters.", funcName);
                    return;
                }
                // Evaluate the bank expression exactly once, even for a safe direct call.
                // The named target's linked placement determines the dispatched bank.
                EmitLoadValue(farArgs[0], ctx, 1);
                int callerBank = ctx == null ? 0 : ctx.Bank;
                int calleeBank = GetFunctionBank(targetName);
                if (ShouldUseFdsOverlayThunk(callerBank, calleeBank))
                {
                    string directFdsThunkName = EnsureFdsOverlayThunk(targetName, calleeBank);
                    EmitAsm("JSR", Abs(directFdsThunkName));
                    RecordCallEdge(targetName, calleeBank, "fds_overlay_thunk", viaThunk: true, viaFarcall: true);
                    return;
                }
                if (calleeBank == 0 || calleeBank == callerBank)
                {
                    EmitAsm("JSR", Abs(targetName));
                    RecordCallEdge(targetName, calleeBank, "direct", viaThunk: false, viaFarcall: true);
                    return;
                }
                string fdsThunkName = EnsureFdsOverlayThunk(targetName, calleeBank);
                EmitAsm("JSR", Abs(fdsThunkName));
                RecordCallEdge(targetName, calleeBank, "fds_overlay_thunk", viaThunk: true, viaFarcall: true);
                return;
            }
__fds_overlay_function_count

リンク時にオーバーレイバンク(2以降)へ配置された関数の件数を返します。ロード済みの件数やファイル数ではありません。

書式

c
u8 __fds_overlay_function_count(void);

戻り値

関数表の先頭バイト(0~255)。該当関数がない場合や--fds-no-overlay-tableを指定した場合は0です。255件を超える表はKQFC2511で拒否します。

使用条件と制限

--mapper=fdsでビルドしてください。__fds_available以外のFDS専用関数をカートリッジ向けに呼ぶと、KQFC2420でビルドを拒否します。

コード生成では0の仮データを置き、ネイティブFDS配置のリンク時に、バンク・アドレスが確定した関数表へ置き換えます。サンプルの二つの関数はバンク2へ収録されますが呼びません。--fds-layout=legacyの初期データだけを見て関数数を判断しないでください。

掲載画像はKUROSAKIでディスクの起動ファイルをRAMへ直接配置して確認したものです。このサンプルはBIOSを呼ばず、ファイルの読み書き、ディスク交換、実機での起動を検証していません。検証用の空のファームウェアはBIOSの代用品ではありません。サンプルは画面更新用NMIを使い、不要なマスク可能IRQを無効にしています。

ビルド例ではBIOSの起動画面用の追加ファイルを付けず、ネイティブFDS配置の.fdsを作ります。KUROSAKIの直接起動で確認するための構成です。実機用ディスクの起動手順は、これとは別に確認してください。

使用例

c
void main(void) {
    // These BIOS-free queries need NMI for display, but no maskable IRQ.
    __irq_disable();
    request_overlay=0;
    fds_result[0] = __fds_current_bank();
    fds_result[1] = __fds_is_bank_resident(1);
    fds_result[2] = __fds_is_bank_resident(2);
    fds_result[3] = __fds_overlay_function_count();
    if (request_overlay) {
        fds_result[4]=lesson_overlay_a(); fds_result[5]=lesson_overlay_b();
    }
    m_init();
    m_text(2,2,"FDS OVERLAY RECORDS"); m_wait();
    m_text(2,5,"CURRENT BANK"); m_put(25,5,'0'+fds_result[0]); m_wait();
    m_text(2,7,"BANK 1 RESIDENT"); m_put(25,7,'0'+fds_result[1]); m_wait();
    m_text(2,9,"BANK 2 RESIDENT"); m_put(25,9,'0'+fds_result[2]); m_wait();
    m_text(2,11,"OVERLAY FUNCTIONS"); m_put(25,11,'0'+fds_result[3]); m_wait();
    m_text(2,14,"NO OVERLAY LOAD TESTED"); m_wait();
    while (1) { m_wait(); }
}

呼び出し部分のコード例です。完全なプログラムに組み込む際は、記載した初期化とバッファの条件を満たしてください。

期待する結果

CURRENT BANKは1、BANK 1 RESIDENTは1、BANK 2 RESIDENTは0、OVERLAY FUNCTIONSは2です。バンク2に二つの関数を収録していても、起動直後にバンク2がロード済みになるわけではありません。--fds-no-overlay-tableを付けた検証では最後の値だけ0になります。

__fds_overlay_function_count: RESIDENCY-FDS-DEFAULT — CURRENT BANKは1、BANK 1 RESIDENTは1、BANK 2 RESIDENTは0、OVERLAY FUNCTIONSは2です。バンク2に二つの関数を収録していても、起動直後にバンク2がロード済みになるわけではありません。--fds-no-overlay-tableを付けた検証では最後の値だけ0になります。
RESIDENCY-FDS-DEFAULT — CURRENT BANKは1、BANK 1 RESIDENTは1、BANK 2 RESIDENTは0、OVERLAY FUNCTIONSは2です。バンク2に二つの関数を収録していても、起動直後にバンク2がロード済みになるわけではありません。--fds-no-overlay-tableを付けた検証では最後の値だけ0になります。
__fds_overlay_function_count: RESIDENCY-FDS-TABLE-OFF-DEFAULT — CURRENT BANKは1、BANK 1 RESIDENTは1、BANK 2 RESIDENTは0、OVERLAY FUNCTIONSは2です。バンク2に二つの関数を収録していても、起動直後にバンク2がロード済みになるわけではありません。--fds-no-overlay-tableを付けた検証では最後の値だけ0になります。
RESIDENCY-FDS-TABLE-OFF-DEFAULT — CURRENT BANKは1、BANK 1 RESIDENTは1、BANK 2 RESIDENTは0、OVERLAY FUNCTIONSは2です。バンク2に二つの関数を収録していても、起動直後にバンク2がロード済みになるわけではありません。--fds-no-overlay-tableを付けた検証では最後の値だけ0になります。

使用例の完全なプログラム

ビルドコマンド

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\fds_residency.c -I .\kitaqfc\lib -I .\kitaq-docs\samples -o .\out\fds_residency.fds --mapper=fds --fds-layout=fds32 --fds-no-license-bypass --nes-chr=.\kitaq-docs\samples\font.chr --no-cache --no-disasm
実装とソース

kitaqfc/lib/fds_overlay.h:20

宣言の注記(原文)
Return the linker-generated overlay-function count; zero if the table is disabled.

kitaqfc/kitaqfc/CodeGenerator.cs:5229

csharp
case "__fds_overlay_function_count":
                    if (!Need(0)) return true;
                    EmitAsm("LDA", Abs("__kq_fds_overlay_function_table"));
                    return true;
__fds_require_bank

指定バンクが常駐していなければ読み込みます。資産を使う前に呼び、すでに利用可能な場合の不要な転送を省くための関数です。

書式

c
u8 __fds_require_bank(u8 bank);

引数

bank

ロードする論理バンク番号1~254。0は共通コードなのでロード対象ではなく、255は不明状態の予約値です。バンク2以降では、計算したファイルIDも0~254に収まる必要があります。

戻り値

成功時は0。番号0・255、ファイルIDの桁あふれ、復元用ファイルがないバンク1などは255。BIOS転送が失敗した場合はそのエラーバイトです。転送前の引数エラーでは常駐記録を変更せず、BIOS転送失敗時は255(不明)にします。

使用条件と制限

--mapper=fds --fds-layout=fds32向けです。バンク0は$A000~$DFFFの共通コード、バンク1は起動時の$6000~$9FFF、バンク2以降は同じ16 KiB窓へ読み込むオーバーレイです。バンク2以降は#pragma fixed_bank 2などで配置します。

有効な番号が常駐記録と一致すれば0を返し、一致しなければ__fds_load_bank()を呼びます。この最初の比較は--fds-no-overlay-guardでも行います。255は常駐記録が255でも成功扱いにしません。

バンク1は$6000・16384バイトの独立した起動PRGファイルから復元します。管理表を省略した標準構成ではID 0がバンク1、ID 1が共通コード、ID 2がCHRです。独自の管理表を使う場合は起動PRGを$6000と$A000の16384バイトずつに分けてください。バンク2以降のファイルIDは--fds-overlay-start-id(既定32)+ バンク番号 - 2です。

ロード元と同じ$6000~$9FFF窓に置いた関数から直接バンクを切り替えると、自分のコードや復帰先を上書きします。このサンプルは呼び出し元と表示処理を#pragma fixed_bank 0へ置きます。別バンクの関数を呼んで戻る用途には__fds_overlay_farcall()または通常の型付き関数呼び出しを使います。

BIOS呼び出しは同期処理です。読み込み中も毎フレームの処理が進むAPIではありません。割り込みハンドラーから呼ばず、共通コードに置いた通常の制御処理から使ってください。表示・音声の継続や実機でのBIOSとの共存は別途確認が必要です。

掲載画像は、独自に作成したLoadFilesの呼び出し規約テスト用処理で、コンパイルしたコード・メモリ転送・画面の全画素を確認したものです。実際のBIOS、ディスク転送タイミング、CRC、実機起動を検証した画像ではありません。KUROSAKIのFDSコントローラーは簡易モデルで、この検証用処理なしの通常実行だけでは同じ転送結果を再現できません。

下記コマンドの最初の行で配布サンプルをビルドします。Pythonの検証コマンドは5本のサンプルを最適化あり・なしでビルドし、KUROSAKIで確認します。検証用処理はscripts/fds_abi_fixture.pyから一時生成して削除するため、外部BIOSを用意する必要はありません。通常のゲームでこの検証用処理を使うものではありません。

使用例

c
#pragma fixed_bank 0
#include "fc_fds_scene.h"
#pragma fixed_bank 2
__prg_rom u8 bank_payload[4]={11,22,33,44};
#pragma fixed_bank 0
void main(void) {
    __irq_disable();
    fds_result[0]=__fds_load_bank(2);
    fds_result[1]=__fds_current_bank();
    fds_result[2]=__fds_require_bank(2);
    fds_result[3]=bank_payload[0];fds_result[4]=bank_payload[3];
    fds_result[5]=nes_fds_load_bank(1);
    fds_result[6]=__fds_current_bank();
    fds_result[7]=__fds_require_bank(1);
    fds_result[8]=__fds_load_bank(0);
    fds_result[9]=__fds_load_bank(255);
    fds_result[15]=165;
    m_init();m_text(2,2,"FDS DATA WINDOW");m_wait();
    m_text(2,5,"LOAD BANK 2 STATUS");fds_number(25,5,fds_result[0]);m_wait();
    m_text(2,7,"FIRST / LAST BYTE");fds_number(21,7,fds_result[3]);fds_number(25,7,fds_result[4]);m_wait();
    m_text(2,9,"REQUIRE SAME BANK");fds_number(25,9,fds_result[2]);m_wait();
    m_text(2,11,"RESTORE BANK 1");fds_number(25,11,fds_result[5]);m_wait();
    m_text(2,13,"RESIDENT BANK");fds_number(25,13,fds_result[6]);m_wait();
    m_text(2,16,"INVALID BANKS 0 / 255");m_wait();
    fds_number(2,18,fds_result[8]);fds_number(8,18,fds_result[9]);m_wait();
    while(1){m_wait();}
}

呼び出し部分のコード例です。完全なプログラムに組み込む際は、記載した初期化とバッファの条件を満たしてください。

期待する結果

独自のLoadFiles呼び出し規約テストでは、LOAD BANK 2 STATUS・REQUIRE SAME BANK・RESTORE BANK 1が000です。FIRST / LAST BYTEの011と044は実際に転送した4バイトの両端、RESIDENT BANKの001は復元後の番号です。無効なバンク0と255はそれぞれ255を返します。同一バンクのrequireは追加のロードを起こさず、検証したファイル読み込み要求は32→0の2回です。

__fds_require_bank: BANK_LOAD-DEFAULT — 独自のLoadFiles呼び出し規約テストでは、LOAD BANK 2 STATUS・REQUIRE SAME BANK・RESTORE BANK 1が000です。FIRST / LAST BYTEの011と044は実際に転送した4バイトの両端、RESIDENT BANKの001は復元後の番号です。無効なバンク0と255はそれぞれ255を返します。同一バンクのrequireは追加のロードを起こさず、検証したファイル読み込み要求は32→0の2回です。
BANK_LOAD-DEFAULT — 独自のLoadFiles呼び出し規約テストでは、LOAD BANK 2 STATUS・REQUIRE SAME BANK・RESTORE BANK 1が000です。FIRST / LAST BYTEの011と044は実際に転送した4バイトの両端、RESIDENT BANKの001は復元後の番号です。無効なバンク0と255はそれぞれ255を返します。同一バンクのrequireは追加のロードを起こさず、検証したファイル読み込み要求は32→0の2回です。

使用例の完全なプログラム

ビルドコマンド

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\fds_bank_load.c -I .\kitaqfc\lib -I .\kitaq-docs\samples -o .\out\fds_bank_load.fds --mapper=fds --fds-layout=fds32 --fds-no-license-bypass --fds-overlay-trim --nes-chr=.\kitaq-docs\samples\font.chr --no-cache --no-disasm
python .\kitaq-docs\tools\check_fds_load_examples.py
実装とソース

kitaqfc/lib/fds_overlay.h:13

宣言の注記(原文)
Return zero when the software residency record matches, otherwise call __fds_load_bank.

kitaqfc/kitaqfc/CodeGenerator.cs:9451

csharp
EmitHelperStart("__fds_require_bank");
            string fdsRequireLoad = NewGeneratedLabel("fds_require_load");
            EmitAsm("LDA", Mem(CallArgBase));
            EmitAsm("CMP", Imm(0xFF));
            EmitAsm("BEQ", Rel(fdsRequireLoad));
            EmitAsm("CMP", Mem(_runtimeFdsResidentBankAddress));
            EmitAsm("BNE", Rel(fdsRequireLoad));
            EmitAsm("LDA", Imm(0));
            EmitAsm("RTS");
            _assembly.Add(Expr.Make(Tag.Label, fdsRequireLoad));
            EmitAsm("JSR", Abs("__fds_load_bank"));
            EmitAsm("RTS");

            // Compare the requested bank with the software residency record and return zero or one.

fds_sound.h — FDS波形音源

64段階の波形、変調テーブル、周波数、音量、エンベロープを操作する組み込み関数の宣言です。実装はKITAQFCコンパイラが生成します。

--mapper=fdsで.fdsを生成します。サンプルはディスク入出力を使わず音源を操作します。録音はKUROSAKIのRAM直接起動と音源モデルで確認しました。検証用の空のファームウェア領域はBIOS機能を提供せず、配布物にも含みません。通常のディスク起動や実機では別途適切な起動環境が必要です。

__fds_env_set

3引数を加工せず$4080、$4084、$408Aへ順番に書きます。

書式

c
void __fds_env_set(u8 speed, u8 gain, u8 mode);

引数

speed

名前にかかわらず$4080へ書く生値です。bit7で直接音量、bit6で増加方向を指定します。

gain

$4084へ書く生の変調エンベロープ値です。

mode

$408Aへ書くエンベロープのマスター速度です。

戻り値

戻り値はありません。音源レジスターまたはドライバーの状態を変更します。

使用条件と制限

--mapper=fdsで.fdsを生成します。サンプルはディスク入出力を使わず音源を操作します。録音はKUROSAKIのRAM直接起動と音源モデルで確認しました。検証用の空のファームウェア領域はBIOS機能を提供せず、配布物にも含みません。通常のディスク起動や実機では別途適切な起動環境が必要です。

使用例

c
// Mapper 20 sound lesson: original 64-step triangle and two sustained pitches.
#include "fc_sound_example.h"
#include "fds_sound.h"
#include "nes_game.h"
__location(0x4087) u8 mod_control;
__location(0x4085) u8 mod_counter;
u8 triangle[64]; u8 modulation[32];
void main(void) {
    u8 i;
    sound_begin("FDS WAVE AND VOLUME");
    for(i=0;i<32;i++) { triangle[i]=(u8)(i*2); triangle[63-i]=(u8)(i*2); modulation[i]=0; }
    __fds_sound_enable();
    // Halt/reset modulation before writing the 32 values (each writes two slots).
    mod_control=0x80; mod_counter=0;
    __fds_mod_load(modulation);
    __fds_wave_load(triangle);
    sound_result[0]=*((u8*)0x4040); sound_result[1]=*((u8*)0x405F);
    __fds_freq_set(1031);
    __fds_volume_set(24);
    sound_wait(60);
    __fds_volume_set(0); sound_wait(30);
    // This macro forwards the same 64-byte load. Use raw envelope registers.
    nes_fds_wave_load(triangle);
    __fds_env_set(0x98,0x80,0);
    __fds_freq_set(1545);
    sound_wait(60);
    __fds_volume_set(0); sound_wait(30);
    sound_end();
}

期待する結果

独自の64段階三角波で約440Hz、無音、約660Hz、無音の順です。2回目はnes_fds_wave_loadと生のエンベロープ設定を使います。これは音源モデルと直接起動の検証であり、BIOS経由のディスク起動の検証ではありません。

__fds_env_set: NTSC
NTSC
NTSC — 確認した音声

使用例の完全なプログラム

ビルドコマンド

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\sound_fds.c -I .\kitaqfc\lib -I .\kitaq-docs\samples -o .\out\fc-fds.fds --mapper=fds --nes-chr=.\kitaq-docs\samples\font.chr --no-cache --no-disasm
実装とソース

kitaqfc/lib/fds_sound.h:18

宣言の注記(原文)
Write the three raw arguments to 4080, 4084 and 408A in that order.
The legacy parameter names do not describe three independently packed envelope fields.

kitaqfc/kitaqfc/CodeGenerator.cs:5207

csharp
case "__fds_env_set":
                    return EmitKitaqfcHelperCall(funcName, args, ctx, new[] {1,1,1}, "__fds_env_set");
                // File helpers use byte identifiers and word memory addresses/lengths.

EmitHelperStart("__fds_env_set");
            EmitAsm("LDA", Mem(CallArgBase)); EmitAsm("STA", Mem(0x4080));
            EmitAsm("LDA", Mem(CallArgBase + 1)); EmitAsm("STA", Mem(0x4084));
            EmitAsm("LDA", Mem(CallArgBase + 2)); EmitAsm("STA", Mem(0x408A));
            EmitAsm("RTS");
__fds_freq_set

12ビット周波数を$4082・$4083へ書き、$4083の上位制御ビットを消します。

書式

c
void __fds_freq_set(u16 freq);

引数

freq

FDS周波数レジスター値0~4095。上位制御ビットは0になります。

戻り値

戻り値はありません。音源レジスターまたはドライバーの状態を変更します。

使用条件と制限

--mapper=fdsで.fdsを生成します。サンプルはディスク入出力を使わず音源を操作します。録音はKUROSAKIのRAM直接起動と音源モデルで確認しました。検証用の空のファームウェア領域はBIOS機能を提供せず、配布物にも含みません。通常のディスク起動や実機では別途適切な起動環境が必要です。

使用例

c
// Mapper 20 sound lesson: original 64-step triangle and two sustained pitches.
#include "fc_sound_example.h"
#include "fds_sound.h"
#include "nes_game.h"
__location(0x4087) u8 mod_control;
__location(0x4085) u8 mod_counter;
u8 triangle[64]; u8 modulation[32];
void main(void) {
    u8 i;
    sound_begin("FDS WAVE AND VOLUME");
    for(i=0;i<32;i++) { triangle[i]=(u8)(i*2); triangle[63-i]=(u8)(i*2); modulation[i]=0; }
    __fds_sound_enable();
    // Halt/reset modulation before writing the 32 values (each writes two slots).
    mod_control=0x80; mod_counter=0;
    __fds_mod_load(modulation);
    __fds_wave_load(triangle);
    sound_result[0]=*((u8*)0x4040); sound_result[1]=*((u8*)0x405F);
    __fds_freq_set(1031);
    __fds_volume_set(24);
    sound_wait(60);
    __fds_volume_set(0); sound_wait(30);
    // This macro forwards the same 64-byte load. Use raw envelope registers.
    nes_fds_wave_load(triangle);
    __fds_env_set(0x98,0x80,0);
    __fds_freq_set(1545);
    sound_wait(60);
    __fds_volume_set(0); sound_wait(30);
    sound_end();
}

期待する結果

独自の64段階三角波で約440Hz、無音、約660Hz、無音の順です。2回目はnes_fds_wave_loadと生のエンベロープ設定を使います。これは音源モデルと直接起動の検証であり、BIOS経由のディスク起動の検証ではありません。

__fds_freq_set: NTSC
NTSC
NTSC — 確認した音声

使用例の完全なプログラム

ビルドコマンド

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\sound_fds.c -I .\kitaqfc\lib -I .\kitaq-docs\samples -o .\out\fc-fds.fds --mapper=fds --nes-chr=.\kitaq-docs\samples\font.chr --no-cache --no-disasm
実装とソース

kitaqfc/lib/fds_sound.h:13

宣言の注記(原文)
Write a 12-bit frequency to 4082/4083; upper control bits in 4083 are cleared.

kitaqfc/kitaqfc/CodeGenerator.cs:5197

csharp
case "__fds_freq_set":
                    return EmitKitaqfcHelperCall(funcName, args, ctx, new[] {2}, "__fds_freq_set");
                // Clamp the direct volume field to six bits and set the fixed-volume control bit.

EmitHelperStart("__fds_freq_set");
            EmitAsm("LDA", Mem(CallArgBase)); EmitAsm("STA", Mem(0x4082));
            EmitAsm("LDA", Mem(CallArgBase + 1)); EmitAsm("AND", Imm(0x0F)); EmitAsm("STA", Mem(0x4083));
            EmitAsm("RTS");
__fds_mod_load

32バイトを変調テーブルの書き込みポート$4088へ順に送ります。

書式

c
void __fds_mod_load(const u8* mod32);

引数

mod32

FDS変調テーブルへ書く32バイト。各値は0~7の制御コードです。書き込み前に変調を停止します。

戻り値

戻り値はありません。音源レジスターまたはドライバーの状態を変更します。

使用条件と制限

--mapper=fdsで.fdsを生成します。サンプルはディスク入出力を使わず音源を操作します。録音はKUROSAKIのRAM直接起動と音源モデルで確認しました。検証用の空のファームウェア領域はBIOS機能を提供せず、配布物にも含みません。通常のディスク起動や実機では別途適切な起動環境が必要です。

使用例

c
// Mapper 20 sound lesson: original 64-step triangle and two sustained pitches.
#include "fc_sound_example.h"
#include "fds_sound.h"
#include "nes_game.h"
__location(0x4087) u8 mod_control;
__location(0x4085) u8 mod_counter;
u8 triangle[64]; u8 modulation[32];
void main(void) {
    u8 i;
    sound_begin("FDS WAVE AND VOLUME");
    for(i=0;i<32;i++) { triangle[i]=(u8)(i*2); triangle[63-i]=(u8)(i*2); modulation[i]=0; }
    __fds_sound_enable();
    // Halt/reset modulation before writing the 32 values (each writes two slots).
    mod_control=0x80; mod_counter=0;
    __fds_mod_load(modulation);
    __fds_wave_load(triangle);
    sound_result[0]=*((u8*)0x4040); sound_result[1]=*((u8*)0x405F);
    __fds_freq_set(1031);
    __fds_volume_set(24);
    sound_wait(60);
    __fds_volume_set(0); sound_wait(30);
    // This macro forwards the same 64-byte load. Use raw envelope registers.
    nes_fds_wave_load(triangle);
    __fds_env_set(0x98,0x80,0);
    __fds_freq_set(1545);
    sound_wait(60);
    __fds_volume_set(0); sound_wait(30);
    sound_end();
}

期待する結果

独自の64段階三角波で約440Hz、無音、約660Hz、無音の順です。2回目はnes_fds_wave_loadと生のエンベロープ設定を使います。これは音源モデルと直接起動の検証であり、BIOS経由のディスク起動の検証ではありません。

__fds_mod_load: NTSC
NTSC
NTSC — 確認した音声

使用例の完全なプログラム

ビルドコマンド

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\sound_fds.c -I .\kitaqfc\lib -I .\kitaq-docs\samples -o .\out\fc-fds.fds --mapper=fds --nes-chr=.\kitaq-docs\samples\font.chr --no-cache --no-disasm
実装とソース

kitaqfc/lib/fds_sound.h:11

宣言の注記(原文)
Stream 32 readable bytes to 4088; the caller prepares modulation state and write conditions.

kitaqfc/kitaqfc/CodeGenerator.cs:5195

csharp
case "__fds_mod_load":
                    return EmitKitaqfcHelperCall(funcName, args, ctx, new[] {2}, "__fds_mod_load");

EmitHelperStart("__fds_mod_load");
            EmitAsm("LDY", Imm(0));
            string modLoop = NewGeneratedLabel("fds_mod_loop");
            _assembly.Add(Expr.Make(Tag.Label, modLoop));
            EmitAsm("LDA", IndY(CallArgBase));
            EmitAsm("STA", Mem(0x4088));
            EmitAsm("INY"); EmitAsm("CPY", Imm(32)); EmitAsm("BNE", Rel(modLoop));
            EmitAsm("RTS");
__fds_sound_enable

ディスク・音源ゲート$4023へ0x83を書き、波形制御$4089と速度$408Aを0にします。

書式

c
void __fds_sound_enable(void);

戻り値

戻り値はありません。音源レジスターまたはドライバーの状態を変更します。

使用条件と制限

--mapper=fdsで.fdsを生成します。サンプルはディスク入出力を使わず音源を操作します。録音はKUROSAKIのRAM直接起動と音源モデルで確認しました。検証用の空のファームウェア領域はBIOS機能を提供せず、配布物にも含みません。通常のディスク起動や実機では別途適切な起動環境が必要です。

使用例

c
// Mapper 20 sound lesson: original 64-step triangle and two sustained pitches.
#include "fc_sound_example.h"
#include "fds_sound.h"
#include "nes_game.h"
__location(0x4087) u8 mod_control;
__location(0x4085) u8 mod_counter;
u8 triangle[64]; u8 modulation[32];
void main(void) {
    u8 i;
    sound_begin("FDS WAVE AND VOLUME");
    for(i=0;i<32;i++) { triangle[i]=(u8)(i*2); triangle[63-i]=(u8)(i*2); modulation[i]=0; }
    __fds_sound_enable();
    // Halt/reset modulation before writing the 32 values (each writes two slots).
    mod_control=0x80; mod_counter=0;
    __fds_mod_load(modulation);
    __fds_wave_load(triangle);
    sound_result[0]=*((u8*)0x4040); sound_result[1]=*((u8*)0x405F);
    __fds_freq_set(1031);
    __fds_volume_set(24);
    sound_wait(60);
    __fds_volume_set(0); sound_wait(30);
    // This macro forwards the same 64-byte load. Use raw envelope registers.
    nes_fds_wave_load(triangle);
    __fds_env_set(0x98,0x80,0);
    __fds_freq_set(1545);
    sound_wait(60);
    __fds_volume_set(0); sound_wait(30);
    sound_end();
}

期待する結果

独自の64段階三角波で約440Hz、無音、約660Hz、無音の順です。2回目はnes_fds_wave_loadと生のエンベロープ設定を使います。これは音源モデルと直接起動の検証であり、BIOS経由のディスク起動の検証ではありません。

__fds_sound_enable: NTSC
NTSC
NTSC — 確認した音声

使用例の完全なプログラム

ビルドコマンド

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\sound_fds.c -I .\kitaqfc\lib -I .\kitaq-docs\samples -o .\out\fc-fds.fds --mapper=fds --nes-chr=.\kitaq-docs\samples\font.chr --no-cache --no-disasm
実装とソース

kitaqfc/lib/fds_sound.h:7

宣言の注記(原文)
Write 83 to 4023 and zero to 4089/408A to initialize disk/sound register gates.

kitaqfc/kitaqfc/CodeGenerator.cs:5183

csharp
case "__fds_sound_enable":
                    if (!Need(0)) return true;
                    EmitAsm("LDA", Imm(0x83));
                    EmitAsm("STA", Mem(0x4023));
                    EmitAsm("LDA", Imm(0x00));
                    EmitAsm("STA", Mem(0x4089));
                    EmitAsm("LDA", Imm(0x00));
                    EmitAsm("STA", Mem(0x408A));
                    return true;
                // Wave, modulation and frequency operations pass word arguments to their dedicated helpers.
__fds_volume_set

音量の下位6ビットに0x80を加えて$4080へ書き、直接音量を設定します。

書式

c
void __fds_volume_set(u8 vol);

引数

vol

FDSの直接音量0~63。下位6ビットを使うため64は0に折り返します。

戻り値

戻り値はありません。音源レジスターまたはドライバーの状態を変更します。

使用条件と制限

--mapper=fdsで.fdsを生成します。サンプルはディスク入出力を使わず音源を操作します。録音はKUROSAKIのRAM直接起動と音源モデルで確認しました。検証用の空のファームウェア領域はBIOS機能を提供せず、配布物にも含みません。通常のディスク起動や実機では別途適切な起動環境が必要です。

使用例

c
// Mapper 20 sound lesson: original 64-step triangle and two sustained pitches.
#include "fc_sound_example.h"
#include "fds_sound.h"
#include "nes_game.h"
__location(0x4087) u8 mod_control;
__location(0x4085) u8 mod_counter;
u8 triangle[64]; u8 modulation[32];
void main(void) {
    u8 i;
    sound_begin("FDS WAVE AND VOLUME");
    for(i=0;i<32;i++) { triangle[i]=(u8)(i*2); triangle[63-i]=(u8)(i*2); modulation[i]=0; }
    __fds_sound_enable();
    // Halt/reset modulation before writing the 32 values (each writes two slots).
    mod_control=0x80; mod_counter=0;
    __fds_mod_load(modulation);
    __fds_wave_load(triangle);
    sound_result[0]=*((u8*)0x4040); sound_result[1]=*((u8*)0x405F);
    __fds_freq_set(1031);
    __fds_volume_set(24);
    sound_wait(60);
    __fds_volume_set(0); sound_wait(30);
    // This macro forwards the same 64-byte load. Use raw envelope registers.
    nes_fds_wave_load(triangle);
    __fds_env_set(0x98,0x80,0);
    __fds_freq_set(1545);
    sound_wait(60);
    __fds_volume_set(0); sound_wait(30);
    sound_end();
}

期待する結果

独自の64段階三角波で約440Hz、無音、約660Hz、無音の順です。2回目はnes_fds_wave_loadと生のエンベロープ設定を使います。これは音源モデルと直接起動の検証であり、BIOS経由のディスク起動の検証ではありません。

__fds_volume_set: NTSC
NTSC
NTSC — 確認した音声

使用例の完全なプログラム

ビルドコマンド

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\sound_fds.c -I .\kitaqfc\lib -I .\kitaq-docs\samples -o .\out\fc-fds.fds --mapper=fds --nes-chr=.\kitaq-docs\samples\font.chr --no-cache --no-disasm
実装とソース

kitaqfc/lib/fds_sound.h:15

宣言の注記(原文)
Write (volume & 3F) | 80 to 4080 for direct volume control; masking wraps, rather than clamps.

kitaqfc/kitaqfc/CodeGenerator.cs:5200

csharp
case "__fds_volume_set":
                    if (!Need(1)) return true;
                    Load1(args[0]);
                    EmitAsm("AND", Imm(0x3F));
                    EmitAsm("ORA", Imm(0x80));
                    EmitAsm("STA", Mem(0x4080));
                    return true;
__fds_wave_load

波形書き込みを有効にして64バイトを$4040~$407Fへコピーし、$4089を0へ戻します。

書式

c
void __fds_wave_load(const u8* wave64);

引数

wave64

FDS波形の64バイト配列。各値は0~63の6ビット振幅です。NULLは検査しません。

戻り値

戻り値はありません。音源レジスターまたはドライバーの状態を変更します。

使用条件と制限

--mapper=fdsで.fdsを生成します。サンプルはディスク入出力を使わず音源を操作します。録音はKUROSAKIのRAM直接起動と音源モデルで確認しました。検証用の空のファームウェア領域はBIOS機能を提供せず、配布物にも含みません。通常のディスク起動や実機では別途適切な起動環境が必要です。

使用例

c
// Mapper 20 sound lesson: original 64-step triangle and two sustained pitches.
#include "fc_sound_example.h"
#include "fds_sound.h"
#include "nes_game.h"
__location(0x4087) u8 mod_control;
__location(0x4085) u8 mod_counter;
u8 triangle[64]; u8 modulation[32];
void main(void) {
    u8 i;
    sound_begin("FDS WAVE AND VOLUME");
    for(i=0;i<32;i++) { triangle[i]=(u8)(i*2); triangle[63-i]=(u8)(i*2); modulation[i]=0; }
    __fds_sound_enable();
    // Halt/reset modulation before writing the 32 values (each writes two slots).
    mod_control=0x80; mod_counter=0;
    __fds_mod_load(modulation);
    __fds_wave_load(triangle);
    sound_result[0]=*((u8*)0x4040); sound_result[1]=*((u8*)0x405F);
    __fds_freq_set(1031);
    __fds_volume_set(24);
    sound_wait(60);
    __fds_volume_set(0); sound_wait(30);
    // This macro forwards the same 64-byte load. Use raw envelope registers.
    nes_fds_wave_load(triangle);
    __fds_env_set(0x98,0x80,0);
    __fds_freq_set(1545);
    sound_wait(60);
    __fds_volume_set(0); sound_wait(30);
    sound_end();
}

期待する結果

独自の64段階三角波で約440Hz、無音、約660Hz、無音の順です。2回目はnes_fds_wave_loadと生のエンベロープ設定を使います。これは音源モデルと直接起動の検証であり、BIOS経由のディスク起動の検証ではありません。

__fds_wave_load: NTSC
NTSC
NTSC — 確認した音声

使用例の完全なプログラム

ビルドコマンド

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\sound_fds.c -I .\kitaqfc\lib -I .\kitaq-docs\samples -o .\out\fc-fds.fds --mapper=fds --nes-chr=.\kitaq-docs\samples\font.chr --no-cache --no-disasm
実装とソース

kitaqfc/lib/fds_sound.h:9

宣言の注記(原文)
Copy exactly 64 readable bytes to wave RAM with wave writes enabled, then set 4089 to zero.

kitaqfc/kitaqfc/CodeGenerator.cs:5193

csharp
case "__fds_wave_load":
                    return EmitKitaqfcHelperCall(funcName, args, ctx, new[] {2}, "__fds_wave_load");

EmitHelperStart("__fds_wave_load");
            EmitAsm("LDA", Imm(0x80)); EmitAsm("STA", Mem(0x4089));
            EmitAsm("LDY", Imm(0));
            string waveLoop = NewGeneratedLabel("fds_wave_loop");
            _assembly.Add(Expr.Make(Tag.Label, waveLoop));
            EmitAsm("LDA", IndY(CallArgBase));
            EmitAsm("STA", new AsmOperand(0x4040, AddressMode.AbsoluteY));
            EmitAsm("INY"); EmitAsm("CPY", Imm(64)); EmitAsm("BNE", Rel(waveLoop));
            EmitAsm("LDA", Imm(0x00)); EmitAsm("STA", Mem(0x4089));
            EmitAsm("RTS");

intrinsics.h — コンパイラ組み込み命令

__nes_audio_vblank_tick

NMIでBGMとSFXの待機時間を進め、現在の音源所有権に従ってAPUへ反映するフックです。

書式

c
void __nes_audio_vblank_tick(void);

戻り値

戻り値はありません。

使用条件と制限

audio_vblank.hを読み込み、audio_vblank.cを1回だけビルドへ加えます。コンパイラは、このライブラリのフックを既定または独自のNMIハンドラーから1回ずつ呼びます。PPUのNMIを有効にしてください。音楽の更新を前景ループから呼ぶ必要はありません。

NTSC向けの4音源ドライバーです。CH1=パルス1、CH2=パルス2、CH3=三角波、CH4=ノイズで、チャンネル添字は0~3です。DMCと拡張音源は再生しません。audio.cの効果音関数など、同じAPUを直接書き換える処理とは同時に使わないでください。PAL用の音程表やテンポ補正はありません。

アプリケーションから直接呼ばないでください。音楽ライブラリをリンクするとコンパイラがNMIへ接続します。処理はA/X/Yを保存し、共通の式計算用ワーク領域を使いません。フックと音程表は固定ROMへ配置され、NMI中にROMバンクを切り替えません。これは音楽フック自身の保存処理であり、独自NMIハンドラーの他の処理に必要な保存を肩代わりするものではありません。

1レコードは5バイトで、待機時間, CH1, CH2, CH3, CH4の順です。待機時間1~255は、その音を維持するNMI回数です。0は終端で、残り4バイトは無視します。CH1~CH3の音階0~71はC2~B7(24=C4)。CH4の0~15は通常ノイズの周期番号、16~31は短周期モードです。各音源の255=NES_AUDIO_HOLDは変更せず、254=NES_AUDIO_STOPは停止します。

使用例

c
// The linked audio_vblank.c supplies __nes_audio_vblank_tick().
    // The compiler calls that hook once per NMI. Do not call it in this loop.
    __nmi_wait();

呼び出し部分のコード例です。完全なプログラムに組み込む際は、記載した初期化とバッファの条件を満たしてください。

期待する結果

サンプルは独自の4音源フレーズを1回再生します。前景処理が90フレーム補充を止めて整数計算をしている間も音楽が進みます。画面の「PHRASE COMPLETE / SILENT」「UNDERRUNS 0 / MATH OK」は、終了・補充不足0・計算結果一致を表します。添付の音声でパルスの旋律と伴奏、三角波の低音、ノイズを確認できます。終了後は無音です。画像は制御結果の表示であり、音の確認には音声を使ってください。

__nes_audio_vblank_tick: NROM
NROM
NROM — 確認した音声

使用例の完全なプログラム

ビルドコマンド

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaqfc\lib\audio_vblank.c .\kitaq-docs\samples\api-examples\fc\audio_vblank.c -I .\kitaqfc\lib -I .\kitaq-docs\samples --mapper=nrom --nes-chr=.\kitaq-docs\samples\font.chr -o .\out\audio_vblank.nes --no-cache --no-disasm
実装とソース

kitaqfc/lib/audio_vblank.h:70

宣言の注記(原文)
NMI hook: invoke exactly once per NMI. With a custom handler or a compiler
without automatic audio-hook linking, call it there. Never call from main
or call it again if the compiler already inserts the hook.
Saves A/X/Y, uses no compiler scratch bytes and reads only RAM/fixed ROM.

kitaqfc/lib/audio_vblank.c:150

c
void __nes_audio_vblank_tick(void){
    __asm {
        PHA
        TXA
        PHA
        TYA
        PHA
        LDA nav_paused
        BEQ nav_tick_begin
        JMP nav_tick_exit
nav_tick_begin:
        LDA #0
        STA nav_dirty
        LDA nav_refresh
        BEQ nav_tick_music
        LDA #0
        STA nav_refresh
        LDA #15
        STA nav_dirty
// Advance BGM even when an effect temporarily owns its output channel.
nav_tick_music:
        LDA nav_active
        BNE nav_tick_active
        JMP nav_tick_sfx
nav_tick_active:
        LDA nav_delay
        BEQ nav_tick_next
        DEC nav_delay
        BEQ nav_tick_next
        JMP nav_tick_sfx
nav_tick_next:
        LDX nav_tail
        CPX nav_head
        BNE nav_tick_record
        LDA nav_starved
        BNE nav_tick_empty_done
        INC nav_starved
        LDA nav_underruns
        CMP #255
        BEQ nav_tick_empty_done
        INC nav_underruns
nav_tick_empty_done:
        JSR nav_silence_music
        JMP nav_tick_sfx
nav_tick_record:
        LDA #0
        STA nav_starved
        LDA nav_queue,X
        BNE nav_tick_timed
        STA nav_active
        JSR nav_silence_music
        JMP nav_tick_release
nav_tick_timed:
        STA nav_delay
        LDY #0
// Remember the new BGM notes but do not retrigger channels owned by SFX.
nav_tick_copy:
        INX
        LDA nav_queue,X
        CMP #255
        BEQ nav_tick_copy_next
        STA nav_notes,Y
        LDA nav_bits,Y
        AND nav_previous_mask
        BNE nav_tick_copy_next
        LDA nav_bits,Y
        ORA nav_dirty
        STA nav_dirty
nav_tick_copy_next:
        INY
        CPY #4
        BNE nav_tick_copy
nav_tick_release:
        LDA nav_tail
        CLC
        ADC #5
        CMP #40
        BNE nav_tick_publish
        LDA #0
nav_tick_publish:
        STA nav_tail
// The SFX has its own delay; completion releases its mask automatically.
nav_tick_sfx:
        LDA nav_sfx_active
        BEQ nav_tick_select
        LDA nav_sfx_delay
        BEQ nav_sfx_next
        DEC nav_sfx_delay
        BNE nav_tick_select
nav_sfx_next:
        LDX nav_sfx_cursor
        CPX nav_sfx_length
        BCC nav_sfx_record
        LDA #0
        STA nav_sfx_active
        JMP nav_tick_select
nav_sfx_record:
        LDA nav_sfx_queue,X
        STA nav_sfx_delay
        LDY #0
nav_sfx_copy:
        INX
        LDA nav_sfx_queue,X
        CMP #255
        BEQ nav_sfx_copy_next
        STA nav_sfx_notes,Y
        LDA nav_bits,Y
        AND nav_sfx_mask
        ORA nav_dirty
        STA nav_dirty
nav_sfx_copy_next:
        INY
        CPY #4
        BNE nav_sfx_copy
        INX
        STX nav_sfx_cursor
nav_tick_select:
        LDA #0
        STA nav_effective_mask
        LDA nav_sfx_active
        BEQ nav_tick_restore
        LDA nav_sfx_mask
        STA nav_effective_mask
// A changed ownership bit forces the newly selected note onto the APU.
nav_tick_restore:
        LDA nav_effective_mask
        EOR nav_previous_mask
        ORA nav_dirty
        STA nav_dirty
        LDA nav_effective_mask
        STA nav_previous_mask
        LDA nav_dirty
        AND #1
        BNE nav_apply_0
        JMP nav_done_0
nav_apply_0:
        LDA nav_effective_mask
        AND #1
        BEQ nav_bg_0
        LDA nav_sfx_notes+0
        JMP nav_value_0
nav_bg_0:
        LDA nav_notes+0
nav_value_0:
        CMP #254
        BNE nav_note_0
        LDA nav_enabled
        AND #254
        STA nav_enabled
        STA $4015
        JMP nav_done_0
nav_note_0:
        TAY
        LDA nav_enabled
        ORA #1
        STA nav_enabled
        STA $4015
        LDA nav_timbre+0
        STA $4000
        LDA nav_pulse_lo,Y
        STA $4002
        LDA nav_pulse_hi,Y
        STA $4003
nav_done_0:
        LDA nav_dirty
        AND #2
        BNE nav_apply_1
        JMP nav_done_1
nav_apply_1:
        LDA nav_effective_mask
        AND #2
        BEQ nav_bg_1
        LDA nav_sfx_notes+1
        JMP nav_value_1
nav_bg_1:
        LDA nav_notes+1
nav_value_1:
        CMP #254
        BNE nav_note_1
        LDA nav_enabled
        AND #253
        STA nav_enabled
        STA $4015
        JMP nav_done_1
nav_note_1:
        TAY
        LDA nav_enabled
        ORA #2
        STA nav_enabled
        STA $4015
        LDA nav_timbre+1
        STA $4004
        LDA nav_pulse_lo,Y
        STA $4006
        LDA nav_pulse_hi,Y
        STA $4007
nav_done_1:
        LDA nav_dirty
        AND #4
        BNE nav_apply_2
        JMP nav_done_2
nav_apply_2:
        LDA nav_effective_mask
        AND #4
        BEQ nav_bg_2
        LDA nav_sfx_notes+2
        JMP nav_value_2
nav_bg_2:
        LDA nav_notes+2
nav_value_2:
        CMP #254
        BNE nav_note_2
        LDA nav_enabled
        AND #251
        STA nav_enabled
        STA $4015
        JMP nav_done_2
nav_note_2:
        TAY
        LDA nav_enabled
        ORA #4
        STA nav_enabled
        STA $4015
        LDA nav_timbre+2
        STA $4008
        LDA nav_triangle_lo,Y
        STA $400A
        LDA nav_triangle_hi,Y
        STA $400B
nav_done_2:
        LDA nav_dirty
        AND #8
        BNE nav_apply_3
        JMP nav_done_3
nav_apply_3:
        LDA nav_effective_mask
        AND #8
        BEQ nav_bg_3
        LDA nav_sfx_notes+3
        JMP nav_value_3
nav_bg_3:
        LDA nav_notes+3
nav_value_3:
        CMP #254
        BNE nav_note_3
        LDA nav_enabled
        AND #247
        STA nav_enabled
        STA $4015
        JMP nav_done_3
nav_note_3:
        TAY
        LDA nav_enabled
        ORA #8
        STA nav_enabled
        STA $4015
        LDA nav_timbre+3
        STA $400C
        TYA
        AND #16
        BEQ nav_noise_long
        TYA
        AND #15
        ORA #128
        JMP nav_noise_period
nav_noise_long:
        TYA
nav_noise_period:
        STA $400E
        // A one-shot envelope must outlive its slowest 16-step decay.
        // Length index 1 gives 254 half-frame ticks; index 0 cuts it at 10.
        LDA nav_timbre+3
        AND #16
        BNE nav_noise_constant_length
        LDA #8
        JMP nav_noise_trigger
nav_noise_constant_length:
        LDA #0
nav_noise_trigger:
        STA $400F
nav_done_3:
nav_tick_exit:
        PLA
        TAY
        PLA
        TAX
        PLA
        RTS
// Internal subroutine: stop BGM channels without disturbing an active effect.
// End/underrun forgets the BGM notes without cutting an independent SFX.
nav_silence_music:
        LDA #254
        STA nav_notes+0
        STA nav_notes+1
        STA nav_notes+2
        STA nav_notes+3
        LDA nav_previous_mask
        EOR #15
        ORA nav_dirty
        STA nav_dirty
        RTS
    }
}
__attr_set

ネームテーブル0の0x23C0 + (y>>2)*8 + (x>>2)へ、まとめられた属性1バイト全体を書きます。X・Yはタイル座標で、4×4タイルの属性領域を特定します。2ビットずつの4個のパレット指定をすべて置き換えるため、選んだ四分区画だけを合成する処理ではありません。

書式

c
void __attr_set(u8 x, u8 y, u8 attr);

引数

x / y

ピクセルではなくタイル座標です。32×30タイルの表示領域ではxを0..31、yを0..29に収めます。1タイルは8×8ピクセルです。1バイトの引数を範囲内へ補正しないため、範囲外の座標では意図したタイル領域の外へ書く場合があります。

attr

まとめられた属性バイトです。ビット0–1が左上2×2タイル、2–3が右上、4–5が左下、6–7が右下を指定し、各区画でパレット0–3を選びます。0x55、0xAA、0xFFなら四区画すべてをそれぞれパレット1、2、3にします。

戻り値

ありません(void)。

使用条件と制限

PPU直接アクセスは、描画停止中か、処理全体が安全なVBlank時間内に収まる条件で行います。これらの関数は待機・キュー登録・NMI抑止・スクロール復元を行いません。呼び出し中に割り込みで共有ラッチやアドレスを変更しないでください。PPUCTRLのビット2により、書き込み先は1バイトごとに1または32進みます。ミラーリングとアドレスの周回はPPUの動作に従い、CHR-ROMへは書き込めません。アクセス後、描画を始める前に意図したスクロール状態を設定してください。

NESdev: PPU registers

NESdev: PPU scrolling

使用例

c
for(i=2;i<12;i=(u8)(i+4)) {
__attr_set(i,6,0xFF);__attr_set(i,10,0xFF); // All quadrants select green.
}

呼び出し部分のコード例です。完全なプログラムに組み込む際は、記載した初期化とバッファの条件を満たしてください。

期待する結果

このプログラムではレジスター設定、タイル・属性の直接書き込み、スクロール、背景・スプライトそれぞれのパレット転送を学びます。(0,16)の青い帯は256×16ピクセルです。上段は8×8の塗りつぶし、左半分4×8の棒、8行の三角形を繰り返し、下段は左半分の棒だけを並べます。(16,48)に緑の塗りつぶし64×32、(144,48)に三角形タイルを横8個・縦4個並べた赤い64×32の領域が出ます。

y=96では、x=16・24・32に青い正方形・左半分の棒・三角形、x=80に赤い左半分の棒、x=144に緑の三角形が表示されます。これらは背景タイルです。別パレットを使うスプライトはy=144に並び、x=16が赤い8×8の正方形、x=80が青い左半分4×8の棒、x=144が緑の8行の三角形です。最終スクロールは(0,0)です。

画像ではスプライトの色も含め、16,384ピクセルの形と色を照合します。別の31ケースで、レジスターの置き換え、ステータス・ラッチの副作用、スクロール保存値、0・255バイト転送、増分1・32、マップ端、矩形の寸法0、テーブル選択、属性1バイト全体、背景・スプライト両方のパレットを検証しています。Y=255はレジスターへ書く値としての確認で、正しいスクロール画像の検証ではありません。検証環境はKUROSAKIのNROMで、実機は未検証です。

__attr_set: NROM
NROM

使用例の完全なプログラム

ビルドコマンド

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\ppu_intrinsics.c -I .\kitaqfc\lib -I .\kitaq-docs\samples --nes-chr=.\kitaq-docs\samples\api-examples\fc\vram_shapes.chr -o .\out\ppu_intrinsics.nes --no-cache --no-disasm
実装とソース

kitaqfc/lib/intrinsics.h:134

宣言の注記(原文)
Write the entire packed attribute byte at 23C0+(y>>2)*8+(x>>2).
Use tile coordinates below 32; this does not merge a single palette quadrant.

kitaqfc/kitaqfc/CodeGenerator.cs:4824

csharp
case "__attr_set": return EmitKitaqfcHelperCall(funcName, args, ctx, new[] {1,1,1}, funcName);

void EmitHelperAttrSet()
        {
            EmitHelperStart("__attr_set");
            // args: x,y,attr. Attribute address = $23C0 + (y>>2)*8 + (x>>2), nametable 0.
            EmitAsm("LDA", Mem(CallArgBase + 1));
            EmitAsm("LSR"); EmitAsm("LSR"); EmitAsm("ASL"); EmitAsm("ASL"); EmitAsm("ASL");
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("LDA", Mem(CallArgBase)); EmitAsm("LSR"); EmitAsm("LSR"); EmitAsm("CLC"); EmitAsm("ADC", Mem(_runtimeIntrinsicTmp0Address)); EmitAsm("STA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("LDA", Mem(0x2002)); EmitAsm("LDA", Imm(0x23)); EmitAsm("STA", Mem(0x2006));
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp0Address)); EmitAsm("CLC"); EmitAsm("ADC", Imm(0xC0)); EmitAsm("STA", Mem(0x2006));
            EmitAsm("LDA", Mem(CallArgBase + 2)); EmitAsm("STA", Mem(0x2007));
            EmitAsm("RTS");
        }
__attr_set_nt

論理ネームテーブルnt & 3を選び、PPUラッチをリセットして、0x23C0 + (nt & 3)*0x400 + (y>>2)*8 + (x>>2)へ属性1バイト全体を書きます。そのバイトのパレット四分区画をすべて上書きします。テーブルの対応はカートリッジの現在のミラーリングに従います。

書式

c
void __attr_set_nt(u8 nt, u8 x, u8 y, u8 attr);

引数

nt

論理ネームテーブルの選択値です。下位2ビットだけを使うため(nt & 3)、たとえば4はテーブル0、7はテーブル3を選びます。独立した保存領域の確保やマッパーのミラーリング変更は行いません。

x / y

ピクセルではなくタイル座標です。32×30タイルの表示領域ではxを0..31、yを0..29に収めます。1タイルは8×8ピクセルです。1バイトの引数を範囲内へ補正しないため、範囲外の座標では意図したタイル領域の外へ書く場合があります。

attr

まとめられた属性バイトです。ビット0–1が左上2×2タイル、2–3が右上、4–5が左下、6–7が右下を指定し、各区画でパレット0–3を選びます。0x55、0xAA、0xFFなら四区画すべてをそれぞれパレット1、2、3にします。

戻り値

ありません(void)。

使用条件と制限

PPU直接アクセスは、描画停止中か、処理全体が安全なVBlank時間内に収まる条件で行います。これらの関数は待機・キュー登録・NMI抑止・スクロール復元を行いません。呼び出し中に割り込みで共有ラッチやアドレスを変更しないでください。PPUCTRLのビット2により、書き込み先は1バイトごとに1または32進みます。ミラーリングとアドレスの周回はPPUの動作に従い、CHR-ROMへは書き込めません。アクセス後、描画を始める前に意図したスクロール状態を設定してください。

NESdev: PPU registers

NESdev: PPU scrolling

使用例

c
for(i=18;i<28;i=(u8)(i+4)) {
__attr_set_nt(4,i,6,0x55);__attr_set_nt(4,i,10,0x55); // nt&3=0; red.
}

呼び出し部分のコード例です。完全なプログラムに組み込む際は、記載した初期化とバッファの条件を満たしてください。

期待する結果

このプログラムではレジスター設定、タイル・属性の直接書き込み、スクロール、背景・スプライトそれぞれのパレット転送を学びます。(0,16)の青い帯は256×16ピクセルです。上段は8×8の塗りつぶし、左半分4×8の棒、8行の三角形を繰り返し、下段は左半分の棒だけを並べます。(16,48)に緑の塗りつぶし64×32、(144,48)に三角形タイルを横8個・縦4個並べた赤い64×32の領域が出ます。

y=96では、x=16・24・32に青い正方形・左半分の棒・三角形、x=80に赤い左半分の棒、x=144に緑の三角形が表示されます。これらは背景タイルです。別パレットを使うスプライトはy=144に並び、x=16が赤い8×8の正方形、x=80が青い左半分4×8の棒、x=144が緑の8行の三角形です。最終スクロールは(0,0)です。

画像ではスプライトの色も含め、16,384ピクセルの形と色を照合します。別の31ケースで、レジスターの置き換え、ステータス・ラッチの副作用、スクロール保存値、0・255バイト転送、増分1・32、マップ端、矩形の寸法0、テーブル選択、属性1バイト全体、背景・スプライト両方のパレットを検証しています。Y=255はレジスターへ書く値としての確認で、正しいスクロール画像の検証ではありません。検証環境はKUROSAKIのNROMで、実機は未検証です。

__attr_set_nt: NROM
NROM

使用例の完全なプログラム

ビルドコマンド

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\ppu_intrinsics.c -I .\kitaqfc\lib -I .\kitaq-docs\samples --nes-chr=.\kitaq-docs\samples\api-examples\fc\vram_shapes.chr -o .\out\ppu_intrinsics.nes --no-cache --no-disasm
実装とソース

kitaqfc/lib/intrinsics.h:136

宣言の注記(原文)
Write a whole packed attribute byte in nametable nt&3; other quadrant fields are overwritten.

kitaqfc/kitaqfc/CodeGenerator.cs:4825

csharp
case "__attr_set_nt": return EmitKitaqfcHelperCall(funcName, args, ctx, new[] {1,1,1,1}, funcName);
                // Palette loaders receive a word pointer; their fixed data layout is handled by the runtime helper.

void EmitHelperAttrSetNt()
        {
            EmitHelperStart("__attr_set_nt");
            // args: nt,x,y,attr. Attribute address = $23C0 + nt*$400 + (y>>2)*8 + (x>>2).
            EmitAsm("LDA", Mem(CallArgBase));
            EmitAsm("AND", Imm(3));
            EmitAsm("ASL");
            EmitAsm("ASL");
            EmitAsm("CLC");
            EmitAsm("ADC", Imm(0x23));
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp1Address));
            EmitAsm("LDA", Mem(CallArgBase + 2));
            EmitAsm("LSR"); EmitAsm("LSR"); EmitAsm("ASL"); EmitAsm("ASL"); EmitAsm("ASL");
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("LDA", Mem(CallArgBase + 1)); EmitAsm("LSR"); EmitAsm("LSR"); EmitAsm("CLC"); EmitAsm("ADC", Mem(_runtimeIntrinsicTmp0Address)); EmitAsm("STA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("LDA", Mem(0x2002)); EmitAsm("LDA", Mem(_runtimeIntrinsicTmp1Address)); EmitAsm("STA", Mem(0x2006));
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp0Address)); EmitAsm("CLC"); EmitAsm("ADC", Imm(0xC0)); EmitAsm("STA", Mem(0x2006));
            EmitAsm("LDA", Mem(CallArgBase + 3)); EmitAsm("STA", Mem(0x2007));
            EmitAsm("RTS");
        }
__bankof

名前で指定したデータまたは関数に割り当てられた論理ROMバンク番号を取得します。配置情報を解決するもので、現在選ばれているバンクの取得やポインター参照ではありません。

書式

c
u8 bank = __bankof(symbol);

引数

symbol

コンパイラまたはリンカーが配置を把握しているROMデータや関数の名前です。ポインター変数を指定しても参照先の意味にはなりません。実行時の式はシンボル名として渡せません。

戻り値

u8のバンク番号です。0が共通ROMを表す場合がありますが、すべての対象で切り替え先として有効な番号とは限りません。

使用例

c
bank_expect(__bankof(payload)==3,3);
bank_expect(__bankof(callback)==3,4);

呼び出し部分のコード例です。完全なプログラムに組み込む際は、記載した初期化とバッファの条件を満たしてください。

期待する結果

サンプルではpayloadとcallbackが両方ともバンク3に配置されていることを確認します。これは配置情報の検証であり、実行時のバンク動作は別の読み出しとコールバック検査で確認します。

完成プログラムでは、バンク番号と近アドレスによるデータの指定、バンクをまたぐコールバック、ライブラリの記録と実際のマッピングの区別を学べます。数値はすべて16進数です。BANK INとBANK OUTは02、BYTE HEXはD3、WORD HEXは5C7A、FAILED CHECKSとFIRST FAILUREはどちらも00が期待値です。CGBとFCは青字、DMGは黒字で表示します。

CALL COUNTは02です。far_callマクロと__farcallがそれぞれ1回コールバックを実行します。

__bankof: MMC3
MMC3

使用例の完全なプログラム

ビルドコマンド

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\banked_data_calls.c -I .\kitaqfc\lib -I .\kitaq-docs\samples -o .\out\banked_data_calls.nes --mapper=mmc3 --nes-chr=.\kitaq-docs\samples\font.chr --no-cache --no-disasm
実装とソース

kitaqfc/lib/intrinsics.h:77

宣言の注記(原文)
PRG bank
Resolve a symbol or function placement to its byte bank number at compilation time.

kitaqfc/kitaqfc/CodeGenerator.cs:6906

csharp
if (expr.MatchAny(Tag.Call, out funcExpr, out args) &&
                funcExpr != null &&
                funcExpr.Match(Tag.Name, out name) &&
                string.Equals(name, "__bankof", StringComparison.Ordinal) &&
                args != null &&
                args.Length == 1 &&
                args[0] != null &&
                args[0].Match(Tag.Name, out string bankedSymbol))
            {
                // Resolve __bankof from placement metadata and keep the byte-sized bank value.
                value = ResolveSymbolBank(bankedSymbol) & 0xFF;
                return true;
            }
__bankswitch

コンパイラのマッパー別処理で論理PRGバンクを選び、内部のバンク状態を更新します。マッピングの変更は呼び出し後も続き、bankライブラリが別に保持する記録は更新しません。

書式

c
void __bankswitch(u8 bank);

引数

bank

符号なし8ビットのROMバンク番号です。呼び出しでは関数の配置と一致させ、読み出しでは転送元のマッピングを指定します。対象カートリッジで有効な範囲を使用してください。

戻り値

ありません(void)。

使用条件と制限

切り替えを維持する操作では、実行中のコードや戻り先を見えなくしてはいけません。サンプルのように、この呼び出し、使用するbank_switch本体、その後に続くコードを共通・固定バンク0に置いてください。

ライブラリの記録は0で始まり、カートリッジの起動時のマッピングを調べるものではありません。bank_switchで初期化してください。組み込み関数による直接切り替えやマッパーへの直接書き込みではこの記録を更新しないため、bank_get_currentで実際のハードウェア状態を検証することはできません。

FCの切り替え番号はコンパイラの論理バンク番号です。0は共通ROMを示し、切り替え先ではありません。補助処理は0をバンク1として扱います。SUROM512の有効範囲は1〜30で、切り替え組み込み関数へ直接渡す範囲外の定数はビルドエラーです。範囲外の実行時値では1を選び、マッパー異常状態を設定します。共通ROMは、バンク0への切り替えを要求せず直接読んでください。

使用例

c
__bankswitch(1);
bank_expect(home1[0]==0xA1,19);
bank_expect(bank_get_current()==2,20); // Intrinsics bypass the library record.
__bankswitch(2);

呼び出し部分のコード例です。完全なプログラムに組み込む際は、記載した初期化とバッファの条件を満たしてください。

期待する結果

最初のbank_switch(2)でバンク2を記録して選びます。その後の直接切り替えではバンク1を選び、home1からA1を読みますが、bank_get_currentの記録は2です。戻すとhome2から再びB2を読めます。

完成プログラムでは、バンク番号と近アドレスによるデータの指定、バンクをまたぐコールバック、ライブラリの記録と実際のマッピングの区別を学べます。数値はすべて16進数です。BANK INとBANK OUTは02、BYTE HEXはD3、WORD HEXは5C7A、FAILED CHECKSとFIRST FAILUREはどちらも00が期待値です。CGBとFCは青字、DMGは黒字で表示します。

CALL COUNTは02です。far_callマクロと__farcallがそれぞれ1回コールバックを実行します。

__bankswitch: MMC3
MMC3

使用例の完全なプログラム

ビルドコマンド

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\banked_data_calls.c -I .\kitaqfc\lib -I .\kitaq-docs\samples -o .\out\banked_data_calls.nes --mapper=mmc3 --nes-chr=.\kitaq-docs\samples\font.chr --no-cache --no-disasm
実装とソース

kitaqfc/lib/intrinsics.h:79

宣言の注記(原文)
Select the mapper-specific PRG bank; the common bank and legal bank range depend on the target board.

kitaqfc/kitaqfc/CodeGenerator.cs:4746

csharp
case "__prg_bank_set":
                case "__bankswitch":
                    if (!Need(1)) return true;
                    if (Program.NesMapperProfile.IsSurom512 && TryEvaluateConstant(args[0], out int constantBank) && (constantBank < 1 || constantBank > 30))
                    {
                        Program.Error("error KQFC2603 KQFC-SUROM-BANK-RANGE: {0} constant bank {1} is outside 1..30; common bank 0 is not a switch target.", funcName, constantBank);
                        return true;
                    }
                    _mapperWritingFunctions.Add(_currentFunctionName);
                    Load1(args[0]); EmitAsm("JSR", Abs("__kq_prg_set_bank_a")); return true;
__bit_clear

指定したフラグだけを0にします。そのフラグを含むバイト全体を消去するわけではありません。一時的な状態フラグなど、記録した条件を1つだけ解除する際に使います。同じビットを再び解除しても0のままです。

書式

c
void __bit_clear(u8* base, u16 bit);

引数

base

CPUからアクセスできるフラグ配列の先頭バイトへのポインターです。この呼び出しで配列の確保や初期化は行いません。設定・解除・反転には書き込み可能なRAMを使ってください。判定処理は読み取りだけを行います。

bit

0から始まるu16のビット番号(0〜65535)です。バイト単位のオフセットやビットマスクではありません。番号0は0番目のバイトの0x01、番号7は同じバイトの0x80、番号8は1番目のバイトの0x01を選びます。一般に、バイト位置はindex / 8、マスクは1 << (index % 8)です。Nバイトの配列ならindex < 8*Nを満たし、計算後のCPUアドレスも有効な範囲に収まる必要があります。

戻り値

ありません(void)。

使用条件と制限

必要な記憶領域は呼び出し側で用意します。配列長を渡す引数、範囲外の切り詰め、バンク切り替え、画面への自動転送はありません。大きなビット番号を渡すと、確保した配列の外を読み書きすることがあります。処理中はメモリーマップを維持してください。読み書きに副作用があるハードウェアレジスターを、RAM上のフラグと同じつもりで操作しないでください。

設定・解除・反転は、1バイトを読み取り、対象ビットを変更してから同じバイトへ書き戻します。この一連の処理は不可分ではありません。メインループと割り込み処理の書き込みを調整し、途中で同じバイトの別のビットに加えた変更が失われないようにしてください。

FC版は、ビット番号が定数なら処理を呼び出し位置へ展開する場合があり、それ以外は生成した補助処理を呼びます。どちらもビット番号の扱いと判定時のマスク値は同じです。補助処理は引数・作業領域を共有するため、別の補助処理の実行中にNMI・IRQから再び呼び出さないでください。

使用例

c
__bit_clear(bit_flags,2);
bit_clear_value=bit_flags[0]; // A6 & FB = A2, decimal 162.
__bit_clear(bit_flags,2);     // Clearing it twice leaves it clear.

呼び出し部分のコード例です。完全なプログラムに組み込む際は、記載した初期化とバッファの条件を満たしてください。

期待する結果

全体プログラムでは、この処理の直前に0番目のバイトが0xA6になっています。ビット2を解除すると0xA2(2進数10100010、10進数162)になり、CLEAR BIT2の行に162を表示します。再びビット2を解除しても値は変わらず、他のフラグは両方の呼び出しで維持されます。

FC版の正常時の表示は、10進数でTEST OFF 000、TEST ON 128、SET BIT1 166、CLEAR BIT2 162、TOGGLE BIT7 034、RESTORE 162、NEXT BYTE 130、FAILED 000です。NEXT BYTEはビット番号9を設定した結果で、0番目のバイトを変えずに1番目のバイトを0x80から0x82へ変更しています。掲載したFC版の画面は黒地に青字です。

プログラムは表示する各状態変化を照合し、不一致の数を表示します。設定・解除・反転の関数そのものには戻り値がなく、対応する行は操作後のRAMの値です。別途、両機種それぞれ136ケースで保護領域を含む520バイト全体をエミュレーター上で比較し、定数・実行時のビット番号、バイト境界・256バイト境界、判定結果の非ゼロのマスク8種類を確認しています。RAMの状態を検証する例であり、図形描画の例ではありません。実機と、割り込み処理による同時書き込みは未検証です。

__bit_clear: NROM
NROM

使用例の完全なプログラム

ビルドコマンド

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\bit_intrinsics.c -I .\kitaqfc\lib -I .\kitaq-docs\samples -o .\out\bit_intrinsics.nes --mapper=nrom --nes-chr=.\kitaq-docs\samples\font.chr --no-cache --no-disasm
実装とソース

kitaqfc/lib/intrinsics.h:45

宣言の注記(原文)
Clear the indexed bit in caller storage; no bounds check or interrupt exclusion is inserted.

kitaqfc/kitaqfc/CodeGenerator.cs:4640

csharp
case "__bit_clear":
                    if (TryEmitBitIntrinsicInline(funcName, args, ctx)) return true;
                    return EmitKitaqfcHelperCall(funcName, args, ctx, new[] {2,2}, "__bit_clear");

bool TryEmitBitIntrinsicInline(string funcName, Expr[] args, FunctionContext ctx)
        {
            args = args ?? Array.Empty<Expr>();
            if (args.Length != 2) return false;
            if (!TryEvaluateConstant(args[1], out int bit) || bit < 0) return false;
            // Split the bit number into a byte displacement and an in-byte mask.
            int byteOffset = bit >> 3;
            if (byteOffset > 255) return false;
            int mask = 1 << (bit & 7);
            int[] ptr = AcquireTemps(2);
            try
            {
                EmitLoadValue(args[0], ctx, 2);
                EmitAsm("STA", Mem(ptr[0]));
                EmitAsm("STX", Mem(ptr[1]));
                EmitAsm("LDY", Imm(byteOffset));
                EmitAsm("LDA", IndY(ptr[0]));
                switch (funcName)
                {
                    case "__bit_test":
                        EmitAsm("AND", Imm(mask));
                        break;
                    case "__bit_set":
                        EmitAsm("ORA", Imm(mask));
                        EmitAsm("STA", IndY(ptr[0]));
                        break;
                    case "__bit_clear":
                        EmitAsm("AND", Imm((~mask) & 0xFF));
                        EmitAsm("STA", IndY(ptr[0]));
                        break;
                    case "__bit_toggle":
                        EmitAsm("EOR", Imm(mask));
                        EmitAsm("STA", IndY(ptr[0]));
                        break;
                    default:
                        return false;
                }
            }
            finally { ReleaseTemps(ptr); }
            return true;
        }

void EmitBitHelper(string name, string op)
        {
            EmitHelperStart(name);
            EmitAsm("LDA", Imm(1));
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("LDA", Mem(CallArgBase + 2));
            EmitAsm("AND", Imm(7));
            EmitAsm("TAX");
            string maskDone = NewGeneratedLabel("bit_mask_done");
            string maskLoop = NewGeneratedLabel("bit_mask_loop");
            EmitAsm("BEQ", Rel(maskDone));
            _assembly.Add(Expr.Make(Tag.Label, maskLoop));
            EmitAsm("ASL", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("DEX");
            EmitAsm("BNE", Rel(maskLoop));
            _assembly.Add(Expr.Make(Tag.Label, maskDone));
            EmitAsm("LDA", Mem(CallArgBase + 2));
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp1Address));
            EmitAsm("LDA", Mem(CallArgBase + 3));
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp2Address));
            for (int i = 0; i < 3; i++) { EmitAsm("LSR", Mem(_runtimeIntrinsicTmp2Address)); EmitAsm("ROR", Mem(_runtimeIntrinsicTmp1Address)); }
            // Fold the high displacement into the pointer; indirect-Y adds the low byte and its carry.
            EmitAsm("LDA", Mem(CallArgBase + 1));
            EmitAsm("CLC");
            EmitAsm("ADC", Mem(_runtimeIntrinsicTmp2Address));
            EmitAsm("STA", Mem(CallArgBase + 1));
            EmitAsm("LDY", Mem(_runtimeIntrinsicTmp1Address));
            EmitAsm("LDA", IndY(CallArgBase));
            switch (op)
            {
                case "test":
                    EmitAsm("AND", Mem(_runtimeIntrinsicTmp0Address));
                    EmitAsm("RTS");
                    break;
                case "set":
                    EmitAsm("ORA", Mem(_runtimeIntrinsicTmp0Address));
                    EmitAsm("STA", IndY(CallArgBase));
                    EmitAsm("RTS");
                    break;
                case "clear":
                    EmitAsm("LDA", Mem(_runtimeIntrinsicTmp0Address));
                    EmitAsm("EOR", Imm(0xFF));
                    EmitAsm("STA", Mem(_runtimeIntrinsicTmp2Address));
                    EmitAsm("LDA", IndY(CallArgBase));
                    EmitAsm("AND", Mem(_runtimeIntrinsicTmp2Address));
                    EmitAsm("STA", IndY(CallArgBase));
                    EmitAsm("RTS");
                    break;
                case "toggle":
                    EmitAsm("EOR", Mem(_runtimeIntrinsicTmp0Address));
                    EmitAsm("STA", IndY(CallArgBase));
                    EmitAsm("RTS");
                    break;
            }
        }
__bit_set

指定したフラグを1にし、同じバイト内の他の7ビットは維持します。例えば、クエストの完了状態やアイテムの取得状態を記録できます。すでに1のビットを再び指定しても状態は変わりません。オン・オフを反転する操作ではありません。

書式

c
void __bit_set(u8* base, u16 bit);

引数

base

CPUからアクセスできるフラグ配列の先頭バイトへのポインターです。この呼び出しで配列の確保や初期化は行いません。設定・解除・反転には書き込み可能なRAMを使ってください。判定処理は読み取りだけを行います。

bit

0から始まるu16のビット番号(0〜65535)です。バイト単位のオフセットやビットマスクではありません。番号0は0番目のバイトの0x01、番号7は同じバイトの0x80、番号8は1番目のバイトの0x01を選びます。一般に、バイト位置はindex / 8、マスクは1 << (index % 8)です。Nバイトの配列ならindex < 8*Nを満たし、計算後のCPUアドレスも有効な範囲に収まる必要があります。

戻り値

ありません(void)。

使用条件と制限

必要な記憶領域は呼び出し側で用意します。配列長を渡す引数、範囲外の切り詰め、バンク切り替え、画面への自動転送はありません。大きなビット番号を渡すと、確保した配列の外を読み書きすることがあります。処理中はメモリーマップを維持してください。読み書きに副作用があるハードウェアレジスターを、RAM上のフラグと同じつもりで操作しないでください。

設定・解除・反転は、1バイトを読み取り、対象ビットを変更してから同じバイトへ書き戻します。この一連の処理は不可分ではありません。メインループと割り込み処理の書き込みを調整し、途中で同じバイトの別のビットに加えた変更が失われないようにしてください。

FC版は、ビット番号が定数なら処理を呼び出し位置へ展開する場合があり、それ以外は生成した補助処理を呼びます。どちらもビット番号の扱いと判定時のマスク値は同じです。補助処理は引数・作業領域を共有するため、別の補助処理の実行中にNMI・IRQから再び呼び出さないでください。

使用例

c
__bit_set(bit_flags,1);
bit_set_value=bit_flags[0]; // A4 | 02 = A6, decimal 166.
__bit_set(bit_flags,1);     // Setting it twice leaves it set.

呼び出し部分のコード例です。完全なプログラムに組み込む際は、記載した初期化とバッファの条件を満たしてください。

期待する結果

0xA4のビット1を設定します。10100100 | 00000010は10100110、つまり0xA6(10進数166)になります。SET BIT1の行が166なら正常です。サンプルでは同じビットをもう一度設定し、0xA6のままであることも確認します。繰り返し設定してもフラグが反転しないことを示す例です。

FC版の正常時の表示は、10進数でTEST OFF 000、TEST ON 128、SET BIT1 166、CLEAR BIT2 162、TOGGLE BIT7 034、RESTORE 162、NEXT BYTE 130、FAILED 000です。NEXT BYTEはビット番号9を設定した結果で、0番目のバイトを変えずに1番目のバイトを0x80から0x82へ変更しています。掲載したFC版の画面は黒地に青字です。

プログラムは表示する各状態変化を照合し、不一致の数を表示します。設定・解除・反転の関数そのものには戻り値がなく、対応する行は操作後のRAMの値です。別途、両機種それぞれ136ケースで保護領域を含む520バイト全体をエミュレーター上で比較し、定数・実行時のビット番号、バイト境界・256バイト境界、判定結果の非ゼロのマスク8種類を確認しています。RAMの状態を検証する例であり、図形描画の例ではありません。実機と、割り込み処理による同時書き込みは未検証です。

__bit_set: NROM
NROM

使用例の完全なプログラム

ビルドコマンド

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\bit_intrinsics.c -I .\kitaqfc\lib -I .\kitaq-docs\samples -o .\out\bit_intrinsics.nes --mapper=nrom --nes-chr=.\kitaq-docs\samples\font.chr --no-cache --no-disasm
実装とソース

kitaqfc/lib/intrinsics.h:43

宣言の注記(原文)
Set the indexed bit in caller storage without changing the other seven bits in that byte.

kitaqfc/kitaqfc/CodeGenerator.cs:4637

csharp
case "__bit_set":
                    if (TryEmitBitIntrinsicInline(funcName, args, ctx)) return true;
                    return EmitKitaqfcHelperCall(funcName, args, ctx, new[] {2,2}, "__bit_set");

bool TryEmitBitIntrinsicInline(string funcName, Expr[] args, FunctionContext ctx)
        {
            args = args ?? Array.Empty<Expr>();
            if (args.Length != 2) return false;
            if (!TryEvaluateConstant(args[1], out int bit) || bit < 0) return false;
            // Split the bit number into a byte displacement and an in-byte mask.
            int byteOffset = bit >> 3;
            if (byteOffset > 255) return false;
            int mask = 1 << (bit & 7);
            int[] ptr = AcquireTemps(2);
            try
            {
                EmitLoadValue(args[0], ctx, 2);
                EmitAsm("STA", Mem(ptr[0]));
                EmitAsm("STX", Mem(ptr[1]));
                EmitAsm("LDY", Imm(byteOffset));
                EmitAsm("LDA", IndY(ptr[0]));
                switch (funcName)
                {
                    case "__bit_test":
                        EmitAsm("AND", Imm(mask));
                        break;
                    case "__bit_set":
                        EmitAsm("ORA", Imm(mask));
                        EmitAsm("STA", IndY(ptr[0]));
                        break;
                    case "__bit_clear":
                        EmitAsm("AND", Imm((~mask) & 0xFF));
                        EmitAsm("STA", IndY(ptr[0]));
                        break;
                    case "__bit_toggle":
                        EmitAsm("EOR", Imm(mask));
                        EmitAsm("STA", IndY(ptr[0]));
                        break;
                    default:
                        return false;
                }
            }
            finally { ReleaseTemps(ptr); }
            return true;
        }

void EmitBitHelper(string name, string op)
        {
            EmitHelperStart(name);
            EmitAsm("LDA", Imm(1));
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("LDA", Mem(CallArgBase + 2));
            EmitAsm("AND", Imm(7));
            EmitAsm("TAX");
            string maskDone = NewGeneratedLabel("bit_mask_done");
            string maskLoop = NewGeneratedLabel("bit_mask_loop");
            EmitAsm("BEQ", Rel(maskDone));
            _assembly.Add(Expr.Make(Tag.Label, maskLoop));
            EmitAsm("ASL", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("DEX");
            EmitAsm("BNE", Rel(maskLoop));
            _assembly.Add(Expr.Make(Tag.Label, maskDone));
            EmitAsm("LDA", Mem(CallArgBase + 2));
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp1Address));
            EmitAsm("LDA", Mem(CallArgBase + 3));
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp2Address));
            for (int i = 0; i < 3; i++) { EmitAsm("LSR", Mem(_runtimeIntrinsicTmp2Address)); EmitAsm("ROR", Mem(_runtimeIntrinsicTmp1Address)); }
            // Fold the high displacement into the pointer; indirect-Y adds the low byte and its carry.
            EmitAsm("LDA", Mem(CallArgBase + 1));
            EmitAsm("CLC");
            EmitAsm("ADC", Mem(_runtimeIntrinsicTmp2Address));
            EmitAsm("STA", Mem(CallArgBase + 1));
            EmitAsm("LDY", Mem(_runtimeIntrinsicTmp1Address));
            EmitAsm("LDA", IndY(CallArgBase));
            switch (op)
            {
                case "test":
                    EmitAsm("AND", Mem(_runtimeIntrinsicTmp0Address));
                    EmitAsm("RTS");
                    break;
                case "set":
                    EmitAsm("ORA", Mem(_runtimeIntrinsicTmp0Address));
                    EmitAsm("STA", IndY(CallArgBase));
                    EmitAsm("RTS");
                    break;
                case "clear":
                    EmitAsm("LDA", Mem(_runtimeIntrinsicTmp0Address));
                    EmitAsm("EOR", Imm(0xFF));
                    EmitAsm("STA", Mem(_runtimeIntrinsicTmp2Address));
                    EmitAsm("LDA", IndY(CallArgBase));
                    EmitAsm("AND", Mem(_runtimeIntrinsicTmp2Address));
                    EmitAsm("STA", IndY(CallArgBase));
                    EmitAsm("RTS");
                    break;
                case "toggle":
                    EmitAsm("EOR", Mem(_runtimeIntrinsicTmp0Address));
                    EmitAsm("STA", IndY(CallArgBase));
                    EmitAsm("RTS");
                    break;
            }
        }
__bit_test

複数のフラグを各ビットに詰めた配列から、1つのフラグを読み取ります。配列の内容は変更しません。RAMの1バイトで8個のオン・オフ状態を保持できます。結果は対象ビットが0か1かを示しますが、返される数値の形式はGB版とFC版で異なります。

書式

c
u8 __bit_test(u8* base, u16 bit);

引数

base

CPUからアクセスできるフラグ配列の先頭バイトへのポインターです。この呼び出しで配列の確保や初期化は行いません。設定・解除・反転には書き込み可能なRAMを使ってください。判定処理は読み取りだけを行います。

bit

0から始まるu16のビット番号(0〜65535)です。バイト単位のオフセットやビットマスクではありません。番号0は0番目のバイトの0x01、番号7は同じバイトの0x80、番号8は1番目のバイトの0x01を選びます。一般に、バイト位置はindex / 8、マスクは1 << (index % 8)です。Nバイトの配列ならindex < 8*Nを満たし、計算後のCPUアドレスも有効な範囲に収まる必要があります。

戻り値

u8で、ビットが0なら0、1ならそのビットのマスク値である1・2・4・8・16・32・64・128のいずれかを返します。計算はbase[bit >> 3] & (1 << (bit & 7))です。例えば、1になっているビット7を調べると128を返します。有効かどうかはresult != 0で判定してください。1とだけ比較すると、他の位置のビットが1でも見落とします。

使用条件と制限

必要な記憶領域は呼び出し側で用意します。配列長を渡す引数、範囲外の切り詰め、バンク切り替え、画面への自動転送はありません。大きなビット番号を渡すと、確保した配列の外を読み書きすることがあります。処理中はメモリーマップを維持してください。読み書きに副作用があるハードウェアレジスターを、RAM上のフラグと同じつもりで操作しないでください。

FC版は、ビット番号が定数なら処理を呼び出し位置へ展開する場合があり、それ以外は生成した補助処理を呼びます。どちらもビット番号の扱いと判定時のマスク値は同じです。補助処理は引数・作業領域を共有するため、別の補助処理の実行中にNMI・IRQから再び呼び出さないでください。

使用例

c
bit_off=__bit_test(bit_flags,1); // Bit 1 of A4 is clear: zero.
bit_on=__bit_test(bit_flags,7);  // Bit 7 is set: GB returns 1; FC returns 128.
bit_is_on=0;
if(bit_on!=0)bit_is_on=1; // The same nonzero test works on both targets.

呼び出し部分のコード例です。完全なプログラムに組み込む際は、記載した初期化とバッファの条件を満たしてください。

期待する結果

0番目のバイトを0xA4(2進数10100100)にして始めます。ビット1を調べると0を返します。ビット7ではGB版が1、FC版が128を返します。サンプルはその結果が0でないことから共通のオン・オフ値も求め、元のバイトが変わっていないことを照合します。下の画像のTEST OFF・TEST ONを確認してください。

FC版の正常時の表示は、10進数でTEST OFF 000、TEST ON 128、SET BIT1 166、CLEAR BIT2 162、TOGGLE BIT7 034、RESTORE 162、NEXT BYTE 130、FAILED 000です。NEXT BYTEはビット番号9を設定した結果で、0番目のバイトを変えずに1番目のバイトを0x80から0x82へ変更しています。掲載したFC版の画面は黒地に青字です。

プログラムは表示する各状態変化を照合し、不一致の数を表示します。設定・解除・反転の関数そのものには戻り値がなく、対応する行は操作後のRAMの値です。別途、両機種それぞれ136ケースで保護領域を含む520バイト全体をエミュレーター上で比較し、定数・実行時のビット番号、バイト境界・256バイト境界、判定結果の非ゼロのマスク8種類を確認しています。RAMの状態を検証する例であり、図形描画の例ではありません。実機と、割り込み処理による同時書き込みは未検証です。

__bit_test: NROM
NROM

使用例の完全なプログラム

ビルドコマンド

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\bit_intrinsics.c -I .\kitaqfc\lib -I .\kitaq-docs\samples -o .\out\bit_intrinsics.nes --mapper=nrom --nes-chr=.\kitaq-docs\samples\font.chr --no-cache --no-disasm
実装とソース

kitaqfc/lib/intrinsics.h:41

宣言の注記(原文)
Return base[bit>>3] & (1<<(bit&7)): zero or the bit mask, not a normalized Boolean.

kitaqfc/kitaqfc/CodeGenerator.cs:4634

csharp
case "__bit_test":
                    if (TryEmitBitIntrinsicInline(funcName, args, ctx)) return true;
                    return EmitKitaqfcHelperCall(funcName, args, ctx, new[] {2,2}, "__bit_test");

bool TryEmitBitIntrinsicInline(string funcName, Expr[] args, FunctionContext ctx)
        {
            args = args ?? Array.Empty<Expr>();
            if (args.Length != 2) return false;
            if (!TryEvaluateConstant(args[1], out int bit) || bit < 0) return false;
            // Split the bit number into a byte displacement and an in-byte mask.
            int byteOffset = bit >> 3;
            if (byteOffset > 255) return false;
            int mask = 1 << (bit & 7);
            int[] ptr = AcquireTemps(2);
            try
            {
                EmitLoadValue(args[0], ctx, 2);
                EmitAsm("STA", Mem(ptr[0]));
                EmitAsm("STX", Mem(ptr[1]));
                EmitAsm("LDY", Imm(byteOffset));
                EmitAsm("LDA", IndY(ptr[0]));
                switch (funcName)
                {
                    case "__bit_test":
                        EmitAsm("AND", Imm(mask));
                        break;
                    case "__bit_set":
                        EmitAsm("ORA", Imm(mask));
                        EmitAsm("STA", IndY(ptr[0]));
                        break;
                    case "__bit_clear":
                        EmitAsm("AND", Imm((~mask) & 0xFF));
                        EmitAsm("STA", IndY(ptr[0]));
                        break;
                    case "__bit_toggle":
                        EmitAsm("EOR", Imm(mask));
                        EmitAsm("STA", IndY(ptr[0]));
                        break;
                    default:
                        return false;
                }
            }
            finally { ReleaseTemps(ptr); }
            return true;
        }

void EmitBitHelper(string name, string op)
        {
            EmitHelperStart(name);
            EmitAsm("LDA", Imm(1));
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("LDA", Mem(CallArgBase + 2));
            EmitAsm("AND", Imm(7));
            EmitAsm("TAX");
            string maskDone = NewGeneratedLabel("bit_mask_done");
            string maskLoop = NewGeneratedLabel("bit_mask_loop");
            EmitAsm("BEQ", Rel(maskDone));
            _assembly.Add(Expr.Make(Tag.Label, maskLoop));
            EmitAsm("ASL", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("DEX");
            EmitAsm("BNE", Rel(maskLoop));
            _assembly.Add(Expr.Make(Tag.Label, maskDone));
            EmitAsm("LDA", Mem(CallArgBase + 2));
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp1Address));
            EmitAsm("LDA", Mem(CallArgBase + 3));
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp2Address));
            for (int i = 0; i < 3; i++) { EmitAsm("LSR", Mem(_runtimeIntrinsicTmp2Address)); EmitAsm("ROR", Mem(_runtimeIntrinsicTmp1Address)); }
            // Fold the high displacement into the pointer; indirect-Y adds the low byte and its carry.
            EmitAsm("LDA", Mem(CallArgBase + 1));
            EmitAsm("CLC");
            EmitAsm("ADC", Mem(_runtimeIntrinsicTmp2Address));
            EmitAsm("STA", Mem(CallArgBase + 1));
            EmitAsm("LDY", Mem(_runtimeIntrinsicTmp1Address));
            EmitAsm("LDA", IndY(CallArgBase));
            switch (op)
            {
                case "test":
                    EmitAsm("AND", Mem(_runtimeIntrinsicTmp0Address));
                    EmitAsm("RTS");
                    break;
                case "set":
                    EmitAsm("ORA", Mem(_runtimeIntrinsicTmp0Address));
                    EmitAsm("STA", IndY(CallArgBase));
                    EmitAsm("RTS");
                    break;
                case "clear":
                    EmitAsm("LDA", Mem(_runtimeIntrinsicTmp0Address));
                    EmitAsm("EOR", Imm(0xFF));
                    EmitAsm("STA", Mem(_runtimeIntrinsicTmp2Address));
                    EmitAsm("LDA", IndY(CallArgBase));
                    EmitAsm("AND", Mem(_runtimeIntrinsicTmp2Address));
                    EmitAsm("STA", IndY(CallArgBase));
                    EmitAsm("RTS");
                    break;
                case "toggle":
                    EmitAsm("EOR", Mem(_runtimeIntrinsicTmp0Address));
                    EmitAsm("STA", IndY(CallArgBase));
                    EmitAsm("RTS");
                    break;
            }
        }
__bit_toggle

指定したフラグを反転し、0なら1、1なら0にします。他のビットは維持します。途中で他の処理が書き換えなければ、同じビットを2回反転すると元の状態に戻ります。

書式

c
void __bit_toggle(u8* base, u16 bit);

引数

base

CPUからアクセスできるフラグ配列の先頭バイトへのポインターです。この呼び出しで配列の確保や初期化は行いません。設定・解除・反転には書き込み可能なRAMを使ってください。判定処理は読み取りだけを行います。

bit

0から始まるu16のビット番号(0〜65535)です。バイト単位のオフセットやビットマスクではありません。番号0は0番目のバイトの0x01、番号7は同じバイトの0x80、番号8は1番目のバイトの0x01を選びます。一般に、バイト位置はindex / 8、マスクは1 << (index % 8)です。Nバイトの配列ならindex < 8*Nを満たし、計算後のCPUアドレスも有効な範囲に収まる必要があります。

戻り値

ありません(void)。

使用条件と制限

必要な記憶領域は呼び出し側で用意します。配列長を渡す引数、範囲外の切り詰め、バンク切り替え、画面への自動転送はありません。大きなビット番号を渡すと、確保した配列の外を読み書きすることがあります。処理中はメモリーマップを維持してください。読み書きに副作用があるハードウェアレジスターを、RAM上のフラグと同じつもりで操作しないでください。

設定・解除・反転は、1バイトを読み取り、対象ビットを変更してから同じバイトへ書き戻します。この一連の処理は不可分ではありません。メインループと割り込み処理の書き込みを調整し、途中で同じバイトの別のビットに加えた変更が失われないようにしてください。

FC版は、ビット番号が定数なら処理を呼び出し位置へ展開する場合があり、それ以外は生成した補助処理を呼びます。どちらもビット番号の扱いと判定時のマスク値は同じです。補助処理は引数・作業領域を共有するため、別の補助処理の実行中にNMI・IRQから再び呼び出さないでください。

使用例

c
__bit_toggle(bit_flags,7);
bit_toggle_value=bit_flags[0]; // A2 ^ 80 = 22, decimal 34.
__bit_toggle(bit_flags,7);
bit_restore_value=bit_flags[0]; // Two toggles restore A2.

呼び出し部分のコード例です。完全なプログラムに組み込む際は、記載した初期化とバッファの条件を満たしてください。

期待する結果

全体プログラムでは、この処理を0xA2から始めます。ビット7を反転すると0x22(10進数34)になり、TOGGLE BIT7は034になります。もう一度反転すると0xA2へ戻り、RESTOREは162になります。2つの表示から、他のビットを保ったまま両方向に反転できることを確認できます。

FC版の正常時の表示は、10進数でTEST OFF 000、TEST ON 128、SET BIT1 166、CLEAR BIT2 162、TOGGLE BIT7 034、RESTORE 162、NEXT BYTE 130、FAILED 000です。NEXT BYTEはビット番号9を設定した結果で、0番目のバイトを変えずに1番目のバイトを0x80から0x82へ変更しています。掲載したFC版の画面は黒地に青字です。

プログラムは表示する各状態変化を照合し、不一致の数を表示します。設定・解除・反転の関数そのものには戻り値がなく、対応する行は操作後のRAMの値です。別途、両機種それぞれ136ケースで保護領域を含む520バイト全体をエミュレーター上で比較し、定数・実行時のビット番号、バイト境界・256バイト境界、判定結果の非ゼロのマスク8種類を確認しています。RAMの状態を検証する例であり、図形描画の例ではありません。実機と、割り込み処理による同時書き込みは未検証です。

__bit_toggle: NROM
NROM

使用例の完全なプログラム

ビルドコマンド

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\bit_intrinsics.c -I .\kitaqfc\lib -I .\kitaq-docs\samples -o .\out\bit_intrinsics.nes --mapper=nrom --nes-chr=.\kitaq-docs\samples\font.chr --no-cache --no-disasm
実装とソース

kitaqfc/lib/intrinsics.h:47

宣言の注記(原文)
XOR the indexed bit in caller storage; coordinate concurrent writers to avoid lost updates.

kitaqfc/kitaqfc/CodeGenerator.cs:4643

csharp
case "__bit_toggle":
                    if (TryEmitBitIntrinsicInline(funcName, args, ctx)) return true;
                    return EmitKitaqfcHelperCall(funcName, args, ctx, new[] {2,2}, "__bit_toggle");
                // Arithmetic variants share argument packing here; signed/fixed-point arithmetic is implemented by their named helpers.

bool TryEmitBitIntrinsicInline(string funcName, Expr[] args, FunctionContext ctx)
        {
            args = args ?? Array.Empty<Expr>();
            if (args.Length != 2) return false;
            if (!TryEvaluateConstant(args[1], out int bit) || bit < 0) return false;
            // Split the bit number into a byte displacement and an in-byte mask.
            int byteOffset = bit >> 3;
            if (byteOffset > 255) return false;
            int mask = 1 << (bit & 7);
            int[] ptr = AcquireTemps(2);
            try
            {
                EmitLoadValue(args[0], ctx, 2);
                EmitAsm("STA", Mem(ptr[0]));
                EmitAsm("STX", Mem(ptr[1]));
                EmitAsm("LDY", Imm(byteOffset));
                EmitAsm("LDA", IndY(ptr[0]));
                switch (funcName)
                {
                    case "__bit_test":
                        EmitAsm("AND", Imm(mask));
                        break;
                    case "__bit_set":
                        EmitAsm("ORA", Imm(mask));
                        EmitAsm("STA", IndY(ptr[0]));
                        break;
                    case "__bit_clear":
                        EmitAsm("AND", Imm((~mask) & 0xFF));
                        EmitAsm("STA", IndY(ptr[0]));
                        break;
                    case "__bit_toggle":
                        EmitAsm("EOR", Imm(mask));
                        EmitAsm("STA", IndY(ptr[0]));
                        break;
                    default:
                        return false;
                }
            }
            finally { ReleaseTemps(ptr); }
            return true;
        }

void EmitBitHelper(string name, string op)
        {
            EmitHelperStart(name);
            EmitAsm("LDA", Imm(1));
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("LDA", Mem(CallArgBase + 2));
            EmitAsm("AND", Imm(7));
            EmitAsm("TAX");
            string maskDone = NewGeneratedLabel("bit_mask_done");
            string maskLoop = NewGeneratedLabel("bit_mask_loop");
            EmitAsm("BEQ", Rel(maskDone));
            _assembly.Add(Expr.Make(Tag.Label, maskLoop));
            EmitAsm("ASL", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("DEX");
            EmitAsm("BNE", Rel(maskLoop));
            _assembly.Add(Expr.Make(Tag.Label, maskDone));
            EmitAsm("LDA", Mem(CallArgBase + 2));
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp1Address));
            EmitAsm("LDA", Mem(CallArgBase + 3));
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp2Address));
            for (int i = 0; i < 3; i++) { EmitAsm("LSR", Mem(_runtimeIntrinsicTmp2Address)); EmitAsm("ROR", Mem(_runtimeIntrinsicTmp1Address)); }
            // Fold the high displacement into the pointer; indirect-Y adds the low byte and its carry.
            EmitAsm("LDA", Mem(CallArgBase + 1));
            EmitAsm("CLC");
            EmitAsm("ADC", Mem(_runtimeIntrinsicTmp2Address));
            EmitAsm("STA", Mem(CallArgBase + 1));
            EmitAsm("LDY", Mem(_runtimeIntrinsicTmp1Address));
            EmitAsm("LDA", IndY(CallArgBase));
            switch (op)
            {
                case "test":
                    EmitAsm("AND", Mem(_runtimeIntrinsicTmp0Address));
                    EmitAsm("RTS");
                    break;
                case "set":
                    EmitAsm("ORA", Mem(_runtimeIntrinsicTmp0Address));
                    EmitAsm("STA", IndY(CallArgBase));
                    EmitAsm("RTS");
                    break;
                case "clear":
                    EmitAsm("LDA", Mem(_runtimeIntrinsicTmp0Address));
                    EmitAsm("EOR", Imm(0xFF));
                    EmitAsm("STA", Mem(_runtimeIntrinsicTmp2Address));
                    EmitAsm("LDA", IndY(CallArgBase));
                    EmitAsm("AND", Mem(_runtimeIntrinsicTmp2Address));
                    EmitAsm("STA", IndY(CallArgBase));
                    EmitAsm("RTS");
                    break;
                case "toggle":
                    EmitAsm("EOR", Mem(_runtimeIntrinsicTmp0Address));
                    EmitAsm("STA", IndY(CallArgBase));
                    EmitAsm("RTS");
                    break;
            }
        }
__chr_bank_set

CHRバンクを選択します。MMC3ではR0を使い、PPU $0000~$07FFの2 KiBを変更します。CNROMでは$0000~$1FFFの8 KiB全体を変更します。

書式

c
void __chr_bank_set(u8 bank);

引数

bank

CNROMでは8 KiBのCHRバンク番号です。MMC3では1 KiB単位の番号で、2 KiBの窓を切り替えます。MMC3の最下位ビットは無視されるため、2と3はいずれも1 KiBバンク2・3を選びます。利用できるバンク数はCHR ROMの容量に依存し、APIは範囲検査しません。

戻り値

戻り値はありません。

使用条件と制限

組み込み関数の宣言はintrinsics.hにあります。nes_mapper_irq_*のマクロにはnes_game.hを読み込みます。マッパーはビルド時の--mapperで選びます。

MMC3のバンク選択レジスターへ0または1を書き込むため、PRGモードビット6とCHR反転ビット7も0になります。別のモードを前提にしたPRG/CHR配置とは組み合わせないでください。描画中の変更は、その時点から参照する絵柄を変えます。サンプルは描画を止めて調べ、文字を表示する前にフォントのバンクへ戻します。

この3つの組み込み関数はCNROMとMMC3に対応します。その他のマッパーではコンパイルエラーになります。SUROMではCHRが外側PRGバンク制御も兼ねるため、これらのCHR操作を使用できません。

使用例

c
__chr_bank_set(2);
    mapper_result[1] = mapper_read(0x03FF);
    mapper_result[2] = mapper_read(0x07FF);

呼び出し部分のコード例です。完全なプログラムに組み込む際は、記載した初期化とバッファの条件を満たしてください。

期待する結果

MMC3のサンプルはCHRのマーカー値19/20、21/22、23/24を表示します。CIRAMは縦配置でA B A B、横配置でA A B Bになります。IRQ回数は01→02→03→03です。最初の3回は明示的な再有効化で1回ずつ増え、無効化後は増えません。各段階で6フレーム待って確認します。

__chr_bank_set: MMC3-DEFAULT
MMC3-DEFAULT

使用例の完全なプログラム

ビルドコマンド

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\mapper_controls.c -I .\kitaqfc\lib -I .\kitaq-docs\samples -o .\out\mapper_controls.nes --mapper=mmc3 --nes-chr=.\kitaq-docs\samples\mapper_banks.chr --no-cache --no-disasm

使用例

c
__chr_bank_set(2); chr_result[0]=chr_marker();
    __chr_bank_set0(1); chr_result[1]=chr_marker();
    __chr_bank_set1(3); chr_result[2]=chr_marker();

呼び出し部分のコード例です。完全なプログラムに組み込む際は、記載した初期化とバッファの条件を満たしてください。

期待する結果

CNROMのサンプルでは、SET 2、SET0 1、SET1 3のマーカー値が33、25、41になります。すべて8 KiB全体を切り替えることを、各バンク内の独自マーカーをPPUから読み戻して示します。

__chr_bank_set: CNROM-DEFAULT
CNROM-DEFAULT

使用例の完全なプログラム

ビルドコマンド

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\mapper_chr_cnrom.c -I .\kitaqfc\lib -I .\kitaq-docs\samples -o .\out\mapper_chr_cnrom.nes --mapper=cnrom --nes-chr=.\kitaq-docs\samples\mapper_banks.chr --no-cache --no-disasm
実装とソース

kitaqfc/lib/intrinsics.h:316

宣言の注記(原文)
Select a CNROM bank or MMC3 register 0 bank; unsupported mapper profiles produce a compile error.
__chr_bank_set0

MMC3のR0を選択し、PPU $0000~$07FFの2 KiBを変更します。__chr_bank_setと同じ動作です。CNROMでは8 KiB全体を変更します。

書式

c
void __chr_bank_set0(u8 bank);

引数

bank

CNROMでは8 KiBのCHRバンク番号です。MMC3では1 KiB単位の番号で、2 KiBの窓を切り替えます。MMC3の最下位ビットは無視されるため、2と3はいずれも1 KiBバンク2・3を選びます。利用できるバンク数はCHR ROMの容量に依存し、APIは範囲検査しません。

戻り値

戻り値はありません。

使用条件と制限

組み込み関数の宣言はintrinsics.hにあります。nes_mapper_irq_*のマクロにはnes_game.hを読み込みます。マッパーはビルド時の--mapperで選びます。

MMC3のバンク選択レジスターへ0または1を書き込むため、PRGモードビット6とCHR反転ビット7も0になります。別のモードを前提にしたPRG/CHR配置とは組み合わせないでください。描画中の変更は、その時点から参照する絵柄を変えます。サンプルは描画を止めて調べ、文字を表示する前にフォントのバンクへ戻します。

この3つの組み込み関数はCNROMとMMC3に対応します。その他のマッパーではコンパイルエラーになります。SUROMではCHRが外側PRGバンク制御も兼ねるため、これらのCHR操作を使用できません。

使用例

c
__chr_bank_set0(4);
    mapper_result[3] = mapper_read(0x03FF);
    mapper_result[4] = mapper_read(0x07FF);

呼び出し部分のコード例です。完全なプログラムに組み込む際は、記載した初期化とバッファの条件を満たしてください。

期待する結果

MMC3のサンプルはCHRのマーカー値19/20、21/22、23/24を表示します。CIRAMは縦配置でA B A B、横配置でA A B Bになります。IRQ回数は01→02→03→03です。最初の3回は明示的な再有効化で1回ずつ増え、無効化後は増えません。各段階で6フレーム待って確認します。

__chr_bank_set0: MMC3-DEFAULT
MMC3-DEFAULT

使用例の完全なプログラム

ビルドコマンド

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\mapper_controls.c -I .\kitaqfc\lib -I .\kitaq-docs\samples -o .\out\mapper_controls.nes --mapper=mmc3 --nes-chr=.\kitaq-docs\samples\mapper_banks.chr --no-cache --no-disasm

使用例

c
__chr_bank_set(2); chr_result[0]=chr_marker();
    __chr_bank_set0(1); chr_result[1]=chr_marker();
    __chr_bank_set1(3); chr_result[2]=chr_marker();

呼び出し部分のコード例です。完全なプログラムに組み込む際は、記載した初期化とバッファの条件を満たしてください。

期待する結果

CNROMのサンプルでは、SET 2、SET0 1、SET1 3のマーカー値が33、25、41になります。すべて8 KiB全体を切り替えることを、各バンク内の独自マーカーをPPUから読み戻して示します。

__chr_bank_set0: CNROM-DEFAULT
CNROM-DEFAULT

使用例の完全なプログラム

ビルドコマンド

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\mapper_chr_cnrom.c -I .\kitaqfc\lib -I .\kitaq-docs\samples -o .\out\mapper_chr_cnrom.nes --mapper=cnrom --nes-chr=.\kitaq-docs\samples\mapper_banks.chr --no-cache --no-disasm
実装とソース

kitaqfc/lib/intrinsics.h:318

宣言の注記(原文)
Use the same CNROM/MMC3 register 0 selection as __chr_bank_set.
__chr_bank_set1

MMC3のR1を選択し、PPU $0800~$0FFFの2 KiBを変更します。R0の窓は変更しません。CNROMでは独立した第2窓はなく、8 KiB全体を変更します。

書式

c
void __chr_bank_set1(u8 bank);

引数

bank

CNROMでは8 KiBのCHRバンク番号です。MMC3では1 KiB単位の番号で、2 KiBの窓を切り替えます。MMC3の最下位ビットは無視されるため、2と3はいずれも1 KiBバンク2・3を選びます。利用できるバンク数はCHR ROMの容量に依存し、APIは範囲検査しません。

戻り値

戻り値はありません。

使用条件と制限

組み込み関数の宣言はintrinsics.hにあります。nes_mapper_irq_*のマクロにはnes_game.hを読み込みます。マッパーはビルド時の--mapperで選びます。

MMC3のバンク選択レジスターへ0または1を書き込むため、PRGモードビット6とCHR反転ビット7も0になります。別のモードを前提にしたPRG/CHR配置とは組み合わせないでください。描画中の変更は、その時点から参照する絵柄を変えます。サンプルは描画を止めて調べ、文字を表示する前にフォントのバンクへ戻します。

この3つの組み込み関数はCNROMとMMC3に対応します。その他のマッパーではコンパイルエラーになります。SUROMではCHRが外側PRGバンク制御も兼ねるため、これらのCHR操作を使用できません。

使用例

c
__chr_bank_set1(6);
    mapper_result[5] = mapper_read(0x0BFF);
    mapper_result[6] = mapper_read(0x0FFF);

呼び出し部分のコード例です。完全なプログラムに組み込む際は、記載した初期化とバッファの条件を満たしてください。

期待する結果

MMC3のサンプルはCHRのマーカー値19/20、21/22、23/24を表示します。CIRAMは縦配置でA B A B、横配置でA A B Bになります。IRQ回数は01→02→03→03です。最初の3回は明示的な再有効化で1回ずつ増え、無効化後は増えません。各段階で6フレーム待って確認します。

__chr_bank_set1: MMC3-DEFAULT
MMC3-DEFAULT

使用例の完全なプログラム

ビルドコマンド

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\mapper_controls.c -I .\kitaqfc\lib -I .\kitaq-docs\samples -o .\out\mapper_controls.nes --mapper=mmc3 --nes-chr=.\kitaq-docs\samples\mapper_banks.chr --no-cache --no-disasm

使用例

c
__chr_bank_set(2); chr_result[0]=chr_marker();
    __chr_bank_set0(1); chr_result[1]=chr_marker();
    __chr_bank_set1(3); chr_result[2]=chr_marker();

呼び出し部分のコード例です。完全なプログラムに組み込む際は、記載した初期化とバッファの条件を満たしてください。

期待する結果

CNROMのサンプルでは、SET 2、SET0 1、SET1 3のマーカー値が33、25、41になります。すべて8 KiB全体を切り替えることを、各バンク内の独自マーカーをPPUから読み戻して示します。

__chr_bank_set1: CNROM-DEFAULT
CNROM-DEFAULT

使用例の完全なプログラム

ビルドコマンド

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\mapper_chr_cnrom.c -I .\kitaqfc\lib -I .\kitaq-docs\samples -o .\out\mapper_chr_cnrom.nes --mapper=cnrom --nes-chr=.\kitaq-docs\samples\mapper_banks.chr --no-cache --no-disasm
実装とソース

kitaqfc/lib/intrinsics.h:320

宣言の注記(原文)
Select MMC3 register 1, or the same full CNROM bank register; this is mapper-specific banking.
__copy16

長さの引数を渡さず、srcからdstへ正確に16バイトをコピーします。CPUからアクセスできるメモリーに置いた16バイトのレコードやGB・FCのタイル1枚分のコピーなどに使えます。16はビット数ではなくバイト数です。画面への画像転送を行う関数ではありません。

書式

c
void __copy16(u8* dst, const u8* src);

引数

dst

書き込み先の先頭バイトへのポインターです。処理全体が収まる連続領域を確保してください。バッファを自動確保する機能はありません。

src

読み取り元の先頭バイトへのポインターです。コピー中は、範囲全体を読み取り可能な状態で保持し、内容を変更しないでください。

戻り値

ありません(void)。

使用条件と制限

コピー元とコピー先には重ならない領域を使ってください。これらの処理は小さいアドレスからコピーし、重なりを考慮するmemmoveの動作は提供しません。まだ読んでいないコピー元へ書き込むと、後続のデータが壊れます。

処理が完了してから呼び出し元へ戻ります。ROM・RAMのバンク切り替えや、バッファ境界での範囲の切り詰めは行いません。メモリーの割り当てを安定させ、有効な範囲を指定し、同じバッファを使う割り込み処理とのアクセスを調整してください。

FC版のdstはPPUアドレスではなくCPUアドレスです。CPU側のRAMへコピーしてもCHR・ネームテーブル・パレットは更新されません。転送にはPPU・VRAM用APIを使います。生成される補助処理は引数・作業領域を共有するため、呼び出し中にNMI・IRQから再び呼び出さないでください。

使用例

c
memory_reset();
__copy16(memory_destination+1,memory_source+1);
memory_copy_16=memory_check(16,0); // Exactly 16 bytes, not 16 bits.

呼び出し部分のコード例です。完全なプログラムに組み込む際は、記載した初期化とバッファの条件を満たしてください。

期待する結果

この例では__copy16で、規則的な値を入れた16バイトをオフセット1からコピーします。memory_checkでコピー元・先の配列全体を照合し、COPY16の行に016が表示されることを確認します。先頭を1バイト空けて保護領域とし、転送末尾より後のバイトも変わっていないことを調べます。

全体プログラムは処理ごとに264バイトのバッファ2つを初期化し、コピー元・先の全バイトと、変更してはいけない前後の保護領域を照合します。正常時の表示はCOPY WORD 257、FILL WORD 257、COPY BYTE 255、FILL BYTE 255、COPY16 016、COPY32 032、ZERO CALLS 002、FAILED 000です。WORD・BYTEは長さの型を表し、1回にコピーする要素の大きさではありません。数字はサンプルの照合処理が求めた値で、戻り値のない各関数が返した値ではありません。

掲載画像はDMGが白地に黒字、CGBが白地に青字、FCが黒地に青字です。RAM操作を確認する例であり、文字は検査結果を表示するためのものです。図形描画関数の検証ではありません。別途、両機種それぞれ88ケースをエミュレーターで照合し、定数・実行時の長さ0・1・16・17・32・33・64・65・255バイトと、16ビット長の256・257バイトを確認しています。実機では未検証です。

__copy16: NROM
NROM

使用例の完全なプログラム

ビルドコマンド

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\memory_intrinsics.c -I .\kitaqfc\lib -I .\kitaq-docs\samples -o .\out\memory_intrinsics.nes --mapper=nrom --nes-chr=.\kitaq-docs\samples\font.chr --no-cache --no-disasm
実装とソース

kitaqfc/lib/intrinsics.h:27

宣言の注記(原文)
Copy exactly 16 bytes forward; both ranges must be valid and suitably non-overlapping.

kitaqfc/kitaqfc/CodeGenerator.cs:4620

csharp
case "__copy16":
                    if (!Need(2)) return true;
                    EmitFixedMemcpy(args[0], args[1], 16, ctx); return true;

void EmitFixedMemcpy(Expr dstExpr, Expr srcExpr, int len, FunctionContext ctx)
        {
            int[] ptr = AcquireTemps(4);
            try
            {
                EmitLoadValue(dstExpr, ctx, 2);
                EmitAsm("STA", Mem(ptr[0]));
                EmitAsm("STX", Mem(ptr[1]));
                EmitLoadValue(srcExpr, ctx, 2);
                EmitAsm("STA", Mem(ptr[2]));
                EmitAsm("STX", Mem(ptr[3]));
                for (int i = 0; i < len; i++)
                {
                    EmitAsm("LDY", Imm(i));
                    EmitAsm("LDA", IndY(ptr[2]));
                    EmitAsm("STA", IndY(ptr[0]));
                }
            }
            finally { ReleaseTemps(ptr); }
        }

// kitaqfc/kitaqfc/Lowerer.cs:2128
if (expr.MatchAny(Tag.Call, out callFunc, out callArgs))
        {
            string knownCallName;
            if (callFunc != null && callFunc.Match(Tag.Name, out knownCallName) &&
                knownCallName == "__cgb_is_cgb" &&
                callArgs != null && callArgs.Length == 0 &&
                // A resolved target configuration can replace the zero-argument CGB query with a literal.
                Program.TryGetKnownCgbRuntimeValue(out int knownCgbValue))
            {
                return Expr.Make(Tag.Integer, knownCgbValue).WithSource(expr.Source);
            }

            Expr newFunc = LowerExpr(ctx, callFunc, false, prefix);

            // Optimization (post-A3): when we must spill a non-atomic argument into a temp,
            // prefer a u8 temp if the *parameter type* is u8-like.
            // This preserves semantics (implicit truncation to u8) while avoiding needless
            // u16 temporaries created by integer promotions.
            CType[] paramTypes = null;
            string __fname = null;
            if (newFunc.Match(Tag.Name, out __fname))
            {
                paramTypes = ctx.FindFuncParams(__fname);
                if (paramTypes == null)
                {
                    CType fnTypeFromName;
                    if (TryGetCallableFunctionType(ctx.FindTypeOfName(__fname), out fnTypeFromName))
                        paramTypes = fnTypeFromName.ParamTypes;
                }
            }
            else
            {
                CType fnTypeFromExpr;
                if (TryGetCallableFunctionType(InferType(ctx, newFunc), out fnTypeFromExpr))
                    paramTypes = fnTypeFromExpr.ParamTypes;
            }

            Expr[] newArgs = new Expr[callArgs.Length];
            for (int i = 0; i < callArgs.Length; i++)
            {
                Expr a = LowerExpr(ctx, callArgs[i], allowExtract, prefix);

                bool preserveConstantArgIntrinsic = false;
                if (a.MatchAny(Tag.Call, out Expr argFunc, out Expr[] _argCallArgs) &&
                    argFunc.Match(Tag.Name, out string argFuncName))
                {
                    preserveConstantArgIntrinsic = (argFuncName == "__bankof" || argFuncName == "__cgb_is_cgb");
                }
                // Keep evaluable constants and special intrinsic/bank operands visible to code generation instead of spilling them.
                bool preserveCompileTimeConstantArg = ctx.Global.TryEvalConstInt(a, out _);
                bool preserveFarcallBankArg = (__fname == "__farcall" && i == 0);

                if (allowExtract && !preserveFarcallBankArg && !preserveConstantArgIntrinsic && !preserveCompileTimeConstantArg && !IsAtom(a))
                {
                    // If we know the callee's parameter type, use it to choose a smaller temp.
                    // Example: (u8)(row*32) passed to a u8 param should spill as u8, not u16.
                    CType spillAs = null;
                    if (paramTypes != null && i < paramTypes.Length)
                    {
                        CType pt = paramTypes[i];
                        if (IsU8Like(pt)) spillAs = CType.UInt8;
                    }
                    if (spillAs == null) spillAs = InferType(ctx, a);
                    if (!IsAggregateType(spillAs) && SizeOfType(spillAs) <= 2)
                        a = SpillToTemp(ctx, a, spillAs, prefix);
                }
                newArgs[i] = a;
            }

            // Lint: const discard for argument passing
            // Warn when passing a const-qualified pointer (or const array decay) to a non-const pointer parameter.
            // Suppress when the argument has an explicit cast, or inside __unsafe.
            if (!ctx.InUnsafe && paramTypes != null)
            {
                for (int i = 0; i < newArgs.Length && i < paramTypes.Length; i++)
                {
                    CType pt = paramTypes[i];
                    if (pt == null) continue;
                    if (!IsPointerToNonConst(pt)) continue;
                    if (HasTopLevelCast(newArgs[i])) continue;

                    CType at = InferType(ctx, newArgs[i]);
                    if (at != null && (IsPointerToConst(at) || IsArrayOfConst(at)))
                    {
                        string fn = __fname ?? "<call>";
                        WarnConstDiscard(expr, $"argument {i} to '{fn}'");
                        break; // keep it quiet: one warning per call is enough
                    }
                }
            }

            if (paramTypes != null)
            {
                string fn = __fname ?? "<call>";
                for (int i = 0; i < newArgs.Length && i < paramTypes.Length; i++)
                {
                    FilePosition argPos = newArgs[i].Source;
                    if (string.IsNullOrEmpty(argPos.Filename) || argPos.Filename == "<unknown>")
                        argPos = expr.Source;
                    WarnImplicitNarrowingIfNeeded(ctx, newArgs[i], InferType(ctx, newArgs[i]), paramTypes[i], argPos,
                        "argument " + i + " to '" + fn + "'");
                }
            }

            // Lint: __enum_strict argument passing.
            if (paramTypes != null)
            {
                string fn = __fname ?? "<call>";
                for (int i = 0; i < newArgs.Length && i < paramTypes.Length; i++)
                {
                    CType pt = paramTypes[i];
                    if (pt == null) continue;

                    CType at = InferType(ctx, newArgs[i]);
                    if (at == null) continue;

                    if (HasTopLevelCast(newArgs[i])) continue;

                    if (IsStrictEnum(pt))
                    {
                        if (!IsSameEnum(pt, at))
                        {
                            Program.Warning(expr.Source, ErrorCode.EnumStrictMix,
                                "argument {0} to '{1}' expects __enum_strict {2}, got {3}; cast explicitly to silence",
                                i, fn, pt.Show(), at.Show());
                        }
                    }
                    else if (IsPlainInteger(pt) && IsStrictEnum(at))
                    {
                        Program.Warning(expr.Source, ErrorCode.EnumStrictMix,
                            "argument {0} to '{1}' passes __enum_strict {2} to integer parameter {3}; cast explicitly to silence",
                            i, fn, at.Show(), pt.Show());
                    }
                }
            }

            if (!ctx.InUnsafe)
                RestrictAliasLint(ctx, newFunc, newArgs, expr.Source);

            // __slice(ptr,len) helper: represent a "slice" (ptr+len) in the IR.
            // Semantics: evaluate both arguments, result type is the pointer/array (same as arg0).
            // Bounds checks are injected only with -Zcheck (see Program.CheckSliceBounds).
            // Note: keep the intrinsic name conservative to avoid collisions ("slice" is allowed only
            // when there is no declared function named "slice").
            if (newFunc.Match(Tag.Name, out string __sfname))
            {
                bool isSliceIntrinsic = (__sfname == "__slice") || (__sfname == "slice" && ctx.FindFuncParams("slice") == null);
                if (isSliceIntrinsic)
                {
                    if (newArgs.Length != 2)
                    {
                        Program.Error(expr.Source, ErrorCode.ParseError, "{0}: expected 2 arguments: __slice(ptr,len)", __sfname);
                    }
                    else
                    {
                        CType pty = InferType(ctx, newArgs[0]);
                        if (pty == null || !(pty.IsPointer || pty.IsArray))
                            Program.Error(expr.Source, ErrorCode.ExpectedType, "{0}: first argument must be a pointer or array, got: {1}", __sfname, (pty == null ? "<unknown>" : pty.Show()));

                        CType lty = InferType(ctx, newArgs[1]);
                        if (lty == null || !lty.IsInteger)
                            Program.Error(expr.Source, ErrorCode.ExpectedType, "{0}: second argument must be an integer length, got: {1}", __sfname, (lty == null ? "<unknown>" : lty.Show()));
                    }

                    return Expr.Make(Tag.Slice, newArgs[0], newArgs[1]).WithSource(expr.Source);
                }
            }

            return MakeCall(newFunc, newArgs).WithSource(expr.Source);
        }
__copy32

長さの引数を渡さず、srcからdstへ正確に32バイトをコピーします。CPUからアクセスできるメモリー上の32バイト固定長レコードや、連続した16バイトのタイルパターン2枚分などに使います。小さいアドレスからのバイトコピーであり、32ビット値の一括読み込みやバンクをまたぐ転送ではありません。

書式

c
void __copy32(u8* dst, const u8* src);

引数

dst

書き込み先の先頭バイトへのポインターです。処理全体が収まる連続領域を確保してください。バッファを自動確保する機能はありません。

src

読み取り元の先頭バイトへのポインターです。コピー中は、範囲全体を読み取り可能な状態で保持し、内容を変更しないでください。

戻り値

ありません(void)。

使用条件と制限

コピー元とコピー先には重ならない領域を使ってください。これらの処理は小さいアドレスからコピーし、重なりを考慮するmemmoveの動作は提供しません。まだ読んでいないコピー元へ書き込むと、後続のデータが壊れます。

処理が完了してから呼び出し元へ戻ります。ROM・RAMのバンク切り替えや、バッファ境界での範囲の切り詰めは行いません。メモリーの割り当てを安定させ、有効な範囲を指定し、同じバッファを使う割り込み処理とのアクセスを調整してください。

FC版のdstはPPUアドレスではなくCPUアドレスです。CPU側のRAMへコピーしてもCHR・ネームテーブル・パレットは更新されません。転送にはPPU・VRAM用APIを使います。生成される補助処理は引数・作業領域を共有するため、呼び出し中にNMI・IRQから再び呼び出さないでください。

使用例

c
memory_reset();
__copy32(memory_destination+1,memory_source+1);
memory_copy_32=memory_check(32,0);

呼び出し部分のコード例です。完全なプログラムに組み込む際は、記載した初期化とバッファの条件を満たしてください。

期待する結果

この例では__copy32で、規則的な値を入れた32バイトをオフセット1からコピーします。memory_checkでコピー元・先の配列全体を照合し、COPY32の行に032が表示されることを確認します。先頭を1バイト空けて保護領域とし、転送末尾より後のバイトも変わっていないことを調べます。

全体プログラムは処理ごとに264バイトのバッファ2つを初期化し、コピー元・先の全バイトと、変更してはいけない前後の保護領域を照合します。正常時の表示はCOPY WORD 257、FILL WORD 257、COPY BYTE 255、FILL BYTE 255、COPY16 016、COPY32 032、ZERO CALLS 002、FAILED 000です。WORD・BYTEは長さの型を表し、1回にコピーする要素の大きさではありません。数字はサンプルの照合処理が求めた値で、戻り値のない各関数が返した値ではありません。

掲載画像はDMGが白地に黒字、CGBが白地に青字、FCが黒地に青字です。RAM操作を確認する例であり、文字は検査結果を表示するためのものです。図形描画関数の検証ではありません。別途、両機種それぞれ88ケースをエミュレーターで照合し、定数・実行時の長さ0・1・16・17・32・33・64・65・255バイトと、16ビット長の256・257バイトを確認しています。実機では未検証です。

__copy32: NROM
NROM

使用例の完全なプログラム

ビルドコマンド

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\memory_intrinsics.c -I .\kitaqfc\lib -I .\kitaq-docs\samples -o .\out\memory_intrinsics.nes --mapper=nrom --nes-chr=.\kitaq-docs\samples\font.chr --no-cache --no-disasm
実装とソース

kitaqfc/lib/intrinsics.h:29

宣言の注記(原文)
Copy exactly 32 bytes forward; bank selection and source lifetime belong to the caller.

kitaqfc/kitaqfc/CodeGenerator.cs:4623

csharp
case "__copy32":
                    if (!Need(2)) return true;
                    EmitFixedMemcpy(args[0], args[1], 32, ctx); return true;
                // Geometry helpers receive byte coordinates; the size array describes ABI packing, not result width.

void EmitFixedMemcpy(Expr dstExpr, Expr srcExpr, int len, FunctionContext ctx)
        {
            int[] ptr = AcquireTemps(4);
            try
            {
                EmitLoadValue(dstExpr, ctx, 2);
                EmitAsm("STA", Mem(ptr[0]));
                EmitAsm("STX", Mem(ptr[1]));
                EmitLoadValue(srcExpr, ctx, 2);
                EmitAsm("STA", Mem(ptr[2]));
                EmitAsm("STX", Mem(ptr[3]));
                for (int i = 0; i < len; i++)
                {
                    EmitAsm("LDY", Imm(i));
                    EmitAsm("LDA", IndY(ptr[2]));
                    EmitAsm("STA", IndY(ptr[0]));
                }
            }
            finally { ReleaseTemps(ptr); }
        }

// kitaqfc/kitaqfc/Lowerer.cs:2128
if (expr.MatchAny(Tag.Call, out callFunc, out callArgs))
        {
            string knownCallName;
            if (callFunc != null && callFunc.Match(Tag.Name, out knownCallName) &&
                knownCallName == "__cgb_is_cgb" &&
                callArgs != null && callArgs.Length == 0 &&
                // A resolved target configuration can replace the zero-argument CGB query with a literal.
                Program.TryGetKnownCgbRuntimeValue(out int knownCgbValue))
            {
                return Expr.Make(Tag.Integer, knownCgbValue).WithSource(expr.Source);
            }

            Expr newFunc = LowerExpr(ctx, callFunc, false, prefix);

            // Optimization (post-A3): when we must spill a non-atomic argument into a temp,
            // prefer a u8 temp if the *parameter type* is u8-like.
            // This preserves semantics (implicit truncation to u8) while avoiding needless
            // u16 temporaries created by integer promotions.
            CType[] paramTypes = null;
            string __fname = null;
            if (newFunc.Match(Tag.Name, out __fname))
            {
                paramTypes = ctx.FindFuncParams(__fname);
                if (paramTypes == null)
                {
                    CType fnTypeFromName;
                    if (TryGetCallableFunctionType(ctx.FindTypeOfName(__fname), out fnTypeFromName))
                        paramTypes = fnTypeFromName.ParamTypes;
                }
            }
            else
            {
                CType fnTypeFromExpr;
                if (TryGetCallableFunctionType(InferType(ctx, newFunc), out fnTypeFromExpr))
                    paramTypes = fnTypeFromExpr.ParamTypes;
            }

            Expr[] newArgs = new Expr[callArgs.Length];
            for (int i = 0; i < callArgs.Length; i++)
            {
                Expr a = LowerExpr(ctx, callArgs[i], allowExtract, prefix);

                bool preserveConstantArgIntrinsic = false;
                if (a.MatchAny(Tag.Call, out Expr argFunc, out Expr[] _argCallArgs) &&
                    argFunc.Match(Tag.Name, out string argFuncName))
                {
                    preserveConstantArgIntrinsic = (argFuncName == "__bankof" || argFuncName == "__cgb_is_cgb");
                }
                // Keep evaluable constants and special intrinsic/bank operands visible to code generation instead of spilling them.
                bool preserveCompileTimeConstantArg = ctx.Global.TryEvalConstInt(a, out _);
                bool preserveFarcallBankArg = (__fname == "__farcall" && i == 0);

                if (allowExtract && !preserveFarcallBankArg && !preserveConstantArgIntrinsic && !preserveCompileTimeConstantArg && !IsAtom(a))
                {
                    // If we know the callee's parameter type, use it to choose a smaller temp.
                    // Example: (u8)(row*32) passed to a u8 param should spill as u8, not u16.
                    CType spillAs = null;
                    if (paramTypes != null && i < paramTypes.Length)
                    {
                        CType pt = paramTypes[i];
                        if (IsU8Like(pt)) spillAs = CType.UInt8;
                    }
                    if (spillAs == null) spillAs = InferType(ctx, a);
                    if (!IsAggregateType(spillAs) && SizeOfType(spillAs) <= 2)
                        a = SpillToTemp(ctx, a, spillAs, prefix);
                }
                newArgs[i] = a;
            }

            // Lint: const discard for argument passing
            // Warn when passing a const-qualified pointer (or const array decay) to a non-const pointer parameter.
            // Suppress when the argument has an explicit cast, or inside __unsafe.
            if (!ctx.InUnsafe && paramTypes != null)
            {
                for (int i = 0; i < newArgs.Length && i < paramTypes.Length; i++)
                {
                    CType pt = paramTypes[i];
                    if (pt == null) continue;
                    if (!IsPointerToNonConst(pt)) continue;
                    if (HasTopLevelCast(newArgs[i])) continue;

                    CType at = InferType(ctx, newArgs[i]);
                    if (at != null && (IsPointerToConst(at) || IsArrayOfConst(at)))
                    {
                        string fn = __fname ?? "<call>";
                        WarnConstDiscard(expr, $"argument {i} to '{fn}'");
                        break; // keep it quiet: one warning per call is enough
                    }
                }
            }

            if (paramTypes != null)
            {
                string fn = __fname ?? "<call>";
                for (int i = 0; i < newArgs.Length && i < paramTypes.Length; i++)
                {
                    FilePosition argPos = newArgs[i].Source;
                    if (string.IsNullOrEmpty(argPos.Filename) || argPos.Filename == "<unknown>")
                        argPos = expr.Source;
                    WarnImplicitNarrowingIfNeeded(ctx, newArgs[i], InferType(ctx, newArgs[i]), paramTypes[i], argPos,
                        "argument " + i + " to '" + fn + "'");
                }
            }

            // Lint: __enum_strict argument passing.
            if (paramTypes != null)
            {
                string fn = __fname ?? "<call>";
                for (int i = 0; i < newArgs.Length && i < paramTypes.Length; i++)
                {
                    CType pt = paramTypes[i];
                    if (pt == null) continue;

                    CType at = InferType(ctx, newArgs[i]);
                    if (at == null) continue;

                    if (HasTopLevelCast(newArgs[i])) continue;

                    if (IsStrictEnum(pt))
                    {
                        if (!IsSameEnum(pt, at))
                        {
                            Program.Warning(expr.Source, ErrorCode.EnumStrictMix,
                                "argument {0} to '{1}' expects __enum_strict {2}, got {3}; cast explicitly to silence",
                                i, fn, pt.Show(), at.Show());
                        }
                    }
                    else if (IsPlainInteger(pt) && IsStrictEnum(at))
                    {
                        Program.Warning(expr.Source, ErrorCode.EnumStrictMix,
                            "argument {0} to '{1}' passes __enum_strict {2} to integer parameter {3}; cast explicitly to silence",
                            i, fn, at.Show(), pt.Show());
                    }
                }
            }

            if (!ctx.InUnsafe)
                RestrictAliasLint(ctx, newFunc, newArgs, expr.Source);

            // __slice(ptr,len) helper: represent a "slice" (ptr+len) in the IR.
            // Semantics: evaluate both arguments, result type is the pointer/array (same as arg0).
            // Bounds checks are injected only with -Zcheck (see Program.CheckSliceBounds).
            // Note: keep the intrinsic name conservative to avoid collisions ("slice" is allowed only
            // when there is no declared function named "slice").
            if (newFunc.Match(Tag.Name, out string __sfname))
            {
                bool isSliceIntrinsic = (__sfname == "__slice") || (__sfname == "slice" && ctx.FindFuncParams("slice") == null);
                if (isSliceIntrinsic)
                {
                    if (newArgs.Length != 2)
                    {
                        Program.Error(expr.Source, ErrorCode.ParseError, "{0}: expected 2 arguments: __slice(ptr,len)", __sfname);
                    }
                    else
                    {
                        CType pty = InferType(ctx, newArgs[0]);
                        if (pty == null || !(pty.IsPointer || pty.IsArray))
                            Program.Error(expr.Source, ErrorCode.ExpectedType, "{0}: first argument must be a pointer or array, got: {1}", __sfname, (pty == null ? "<unknown>" : pty.Show()));

                        CType lty = InferType(ctx, newArgs[1]);
                        if (lty == null || !lty.IsInteger)
                            Program.Error(expr.Source, ErrorCode.ExpectedType, "{0}: second argument must be an integer length, got: {1}", __sfname, (lty == null ? "<unknown>" : lty.Show()));
                    }

                    return Expr.Make(Tag.Slice, newArgs[0], newArgs[1]).WithSource(expr.Source);
                }
            }

            return MakeCall(newFunc, newArgs).WithSource(expr.Source);
        }
__dot2_q8_8

符号なし2項の内積を計算します。

書式

c
u16 __dot2_q8_8(u16 a0, u16 a1, u8 b0, u8 b1);

引数

a0

符号なし16ビット値0~65535。

a1

符号なし16ビット値0~65535。

b0

符号なし8ビット係数0~255。

b1

符号なし8ビット係数0~255。

戻り値

a0*b0+a1*b1の下位16ビットです。

使用条件と制限

FCでは名前にq8_8/q1_7があっても生の整数積を使い、固定小数点の縮小シフトは行いません。Q1.7の3関数は対応する符号付き関数への別名です。単位変換は呼び出し側で行います。符号付き結果もu16のビット列として返します。

使用例

c
// Compare literal coefficients with values read from RAM.
result[22]=__dot2_q8_8(513,769,64,127);
v0=513;v1=769;v2=64;v3=127;
result[23]=__dot2_q8_8(v0,v1,v2,v3);

呼び出し部分のコード例です。完全なプログラムに組み込む際は、記載した初期化とバッファの条件を満たしてください。

期待する結果

固定値とRAM変数で同じ計算を行う例です。画面には代表値を符号なし16ビットで表示し、負数は2の補数の値になります。GBは固定小数点の縮小を行い、FCは整数積を返すため、同じ入力でも両機種の結果は異なります。

この抜粋で保存する16ビット値は result[22]=64959, result[23]=64959 です。固定値とRAM変数を使った計算は、それぞれ同じ値になります。画面は代表値を表示し、全結果はresult配列に保存します。

__dot2_q8_8: NROM
NROM

使用例の完全なプログラム

ビルドコマンド

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\batch3_math.c -I .\kitaqfc\lib -I .\kitaq-docs\samples -o .\out\batch3_math.nes --no-cache --no-disasm --mapper=nrom --nes-chr=.\kitaq-docs\samples\font.chr
実装とソース

kitaqfc/lib/intrinsics.h:63

宣言の注記(原文)
Return a0*b0+a1*b1 modulo 65536. The FC backend inserts no Q8.8 rescaling or rounding.

kitaqfc/kitaqfc/CodeGenerator.cs:4654

csharp
case "__dot2_q8_8": return EmitKitaqfcHelperCall(funcName, args, ctx, new[] {2,2,1,1}, funcName);

void EmitHelperMath()
        {
            // Consume eight multiplier bits and accumulate the low product word in shared temporaries, returning A/X.
            EmitHelperStart("__mul16x8");
            EmitAsm("LDA", Imm(0));
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp1Address));
            EmitAsm("LDX", Imm(8));
            string loop = NewGeneratedLabel("mul16x8_loop");
            string skip = NewGeneratedLabel("mul16x8_skip_add");
            _assembly.Add(Expr.Make(Tag.Label, loop));
            EmitAsm("LSR", Mem(CallArgBase + 2));
            EmitAsm("BCC", Rel(skip));
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("CLC");
            EmitAsm("ADC", Mem(CallArgBase));
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp1Address));
            EmitAsm("ADC", Mem(CallArgBase + 1));
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp1Address));
            _assembly.Add(Expr.Make(Tag.Label, skip));
            EmitAsm("ASL", Mem(CallArgBase));
            EmitAsm("ROL", Mem(CallArgBase + 1));
            EmitAsm("DEX");
            EmitAsm("BNE", Rel(loop));
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("LDX", Mem(_runtimeIntrinsicTmp1Address));
            EmitAsm("RTS");

            // Zero-extend the byte multiplicand, reuse word multiplication and return its high byte.
            EmitHelperStart("__mul8x8_hi");
            EmitAsm("LDA", Mem(CallArgBase + 1));
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp2Address));
            EmitAsm("LDA", Imm(0));
            EmitAsm("STA", Mem(CallArgBase + 1));
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp2Address));
            EmitAsm("STA", Mem(CallArgBase + 2));
            EmitAsm("JSR", Abs("__mul16x8"));
            EmitAsm("TXA");
            EmitAsm("RTS");

            // Convert signed operands to magnitudes, multiply, then restore the product sign modulo 65536.
            EmitHelperStart("__smul16x8");
            EmitAsm("LDA", Imm(0));
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp2Address));
            string aPos = NewGeneratedLabel("smul_a_positive");
            EmitAsm("LDA", Mem(CallArgBase + 1));
            EmitAsm("AND", Imm(0x80));
            EmitAsm("BEQ", Rel(aPos));
            EmitAsm("LDA", Mem(CallArgBase)); EmitAsm("EOR", Imm(0xFF)); EmitAsm("CLC"); EmitAsm("ADC", Imm(1)); EmitAsm("STA", Mem(CallArgBase));
            EmitAsm("LDA", Mem(CallArgBase + 1)); EmitAsm("EOR", Imm(0xFF)); EmitAsm("ADC", Imm(0)); EmitAsm("STA", Mem(CallArgBase + 1));
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp2Address)); EmitAsm("EOR", Imm(1)); EmitAsm("STA", Mem(_runtimeIntrinsicTmp2Address));
            _assembly.Add(Expr.Make(Tag.Label, aPos));
            string bPos = NewGeneratedLabel("smul_b_positive");
            EmitAsm("LDA", Mem(CallArgBase + 2));
            EmitAsm("AND", Imm(0x80));
            EmitAsm("BEQ", Rel(bPos));
            EmitAsm("LDA", Mem(CallArgBase + 2)); EmitAsm("EOR", Imm(0xFF)); EmitAsm("CLC"); EmitAsm("ADC", Imm(1)); EmitAsm("STA", Mem(CallArgBase + 2));
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp2Address)); EmitAsm("EOR", Imm(1)); EmitAsm("STA", Mem(_runtimeIntrinsicTmp2Address));
            _assembly.Add(Expr.Make(Tag.Label, bPos));
            EmitAsm("JSR", Abs("__mul16x8"));
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("STX", Mem(_runtimeIntrinsicTmp1Address));
            string sdone = NewGeneratedLabel("smul_done");
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp2Address));
            EmitAsm("BEQ", Rel(sdone));
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp0Address)); EmitAsm("EOR", Imm(0xFF)); EmitAsm("CLC"); EmitAsm("ADC", Imm(1)); EmitAsm("STA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp1Address)); EmitAsm("EOR", Imm(0xFF)); EmitAsm("ADC", Imm(0)); EmitAsm("STA", Mem(_runtimeIntrinsicTmp1Address));
            _assembly.Add(Expr.Make(Tag.Label, sdone));
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("LDX", Mem(_runtimeIntrinsicTmp1Address));
            EmitAsm("RTS");

            EmitMacHelper("__mac16", "__mul16x8");
            EmitMacHelper("__smac16", "__smul16x8");
            EmitDot2Helper("__dot2_q8_8", "__mul16x8");
            EmitDot3Helper("__dot3_q8_8", "__mul16x8");
            EmitDot2Helper("__sdot2_q8_8", "__smul16x8");
            EmitDot3Helper("__sdot3_q8_8", "__smul16x8");
        }
__dot3_q8_8

符号なし3項の内積を計算します。

書式

c
u16 __dot3_q8_8(u16 a0, u16 a1, u16 a2, u8 b0, u8 b1, u8 b2);

引数

a0

符号なし16ビット値0~65535。

a1

符号なし16ビット値0~65535。

a2

符号なし16ビット値0~65535。

b0

符号なし8ビット係数0~255。

b1

符号なし8ビット係数0~255。

b2

符号なし8ビット係数0~255。

戻り値

a0*b0+a1*b1+a2*b2の下位16ビットです。

使用条件と制限

FCでは名前にq8_8/q1_7があっても生の整数積を使い、固定小数点の縮小シフトは行いません。Q1.7の3関数は対応する符号付き関数への別名です。単位変換は呼び出し側で行います。符号付き結果もu16のビット列として返します。

使用例

c
// Compare literal coefficients with values read from RAM.
result[24]=__dot3_q8_8(513,769,1025,64,127,3);
v0=513;v1=769;v2=1025;v3=64;v4=127;v5=3;
result[25]=__dot3_q8_8(v0,v1,v2,v3,v4,v5);

呼び出し部分のコード例です。完全なプログラムに組み込む際は、記載した初期化とバッファの条件を満たしてください。

期待する結果

固定値とRAM変数で同じ計算を行う例です。画面には代表値を符号なし16ビットで表示し、負数は2の補数の値になります。GBは固定小数点の縮小を行い、FCは整数積を返すため、同じ入力でも両機種の結果は異なります。

この抜粋で保存する16ビット値は result[24]=2498, result[25]=2498 です。固定値とRAM変数を使った計算は、それぞれ同じ値になります。画面は代表値を表示し、全結果はresult配列に保存します。

__dot3_q8_8: NROM
NROM

使用例の完全なプログラム

ビルドコマンド

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\batch3_math.c -I .\kitaqfc\lib -I .\kitaq-docs\samples -o .\out\batch3_math.nes --no-cache --no-disasm --mapper=nrom --nes-chr=.\kitaq-docs\samples\font.chr
実装とソース

kitaqfc/lib/intrinsics.h:65

宣言の注記(原文)
Sum three unsigned word-by-byte products modulo 65536 without a final scaling shift.

kitaqfc/kitaqfc/CodeGenerator.cs:4655

csharp
case "__dot3_q8_8": return EmitKitaqfcHelperCall(funcName, args, ctx, new[] {2,2,2,1,1,1}, funcName);

void EmitHelperMath()
        {
            // Consume eight multiplier bits and accumulate the low product word in shared temporaries, returning A/X.
            EmitHelperStart("__mul16x8");
            EmitAsm("LDA", Imm(0));
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp1Address));
            EmitAsm("LDX", Imm(8));
            string loop = NewGeneratedLabel("mul16x8_loop");
            string skip = NewGeneratedLabel("mul16x8_skip_add");
            _assembly.Add(Expr.Make(Tag.Label, loop));
            EmitAsm("LSR", Mem(CallArgBase + 2));
            EmitAsm("BCC", Rel(skip));
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("CLC");
            EmitAsm("ADC", Mem(CallArgBase));
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp1Address));
            EmitAsm("ADC", Mem(CallArgBase + 1));
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp1Address));
            _assembly.Add(Expr.Make(Tag.Label, skip));
            EmitAsm("ASL", Mem(CallArgBase));
            EmitAsm("ROL", Mem(CallArgBase + 1));
            EmitAsm("DEX");
            EmitAsm("BNE", Rel(loop));
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("LDX", Mem(_runtimeIntrinsicTmp1Address));
            EmitAsm("RTS");

            // Zero-extend the byte multiplicand, reuse word multiplication and return its high byte.
            EmitHelperStart("__mul8x8_hi");
            EmitAsm("LDA", Mem(CallArgBase + 1));
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp2Address));
            EmitAsm("LDA", Imm(0));
            EmitAsm("STA", Mem(CallArgBase + 1));
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp2Address));
            EmitAsm("STA", Mem(CallArgBase + 2));
            EmitAsm("JSR", Abs("__mul16x8"));
            EmitAsm("TXA");
            EmitAsm("RTS");

            // Convert signed operands to magnitudes, multiply, then restore the product sign modulo 65536.
            EmitHelperStart("__smul16x8");
            EmitAsm("LDA", Imm(0));
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp2Address));
            string aPos = NewGeneratedLabel("smul_a_positive");
            EmitAsm("LDA", Mem(CallArgBase + 1));
            EmitAsm("AND", Imm(0x80));
            EmitAsm("BEQ", Rel(aPos));
            EmitAsm("LDA", Mem(CallArgBase)); EmitAsm("EOR", Imm(0xFF)); EmitAsm("CLC"); EmitAsm("ADC", Imm(1)); EmitAsm("STA", Mem(CallArgBase));
            EmitAsm("LDA", Mem(CallArgBase + 1)); EmitAsm("EOR", Imm(0xFF)); EmitAsm("ADC", Imm(0)); EmitAsm("STA", Mem(CallArgBase + 1));
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp2Address)); EmitAsm("EOR", Imm(1)); EmitAsm("STA", Mem(_runtimeIntrinsicTmp2Address));
            _assembly.Add(Expr.Make(Tag.Label, aPos));
            string bPos = NewGeneratedLabel("smul_b_positive");
            EmitAsm("LDA", Mem(CallArgBase + 2));
            EmitAsm("AND", Imm(0x80));
            EmitAsm("BEQ", Rel(bPos));
            EmitAsm("LDA", Mem(CallArgBase + 2)); EmitAsm("EOR", Imm(0xFF)); EmitAsm("CLC"); EmitAsm("ADC", Imm(1)); EmitAsm("STA", Mem(CallArgBase + 2));
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp2Address)); EmitAsm("EOR", Imm(1)); EmitAsm("STA", Mem(_runtimeIntrinsicTmp2Address));
            _assembly.Add(Expr.Make(Tag.Label, bPos));
            EmitAsm("JSR", Abs("__mul16x8"));
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("STX", Mem(_runtimeIntrinsicTmp1Address));
            string sdone = NewGeneratedLabel("smul_done");
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp2Address));
            EmitAsm("BEQ", Rel(sdone));
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp0Address)); EmitAsm("EOR", Imm(0xFF)); EmitAsm("CLC"); EmitAsm("ADC", Imm(1)); EmitAsm("STA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp1Address)); EmitAsm("EOR", Imm(0xFF)); EmitAsm("ADC", Imm(0)); EmitAsm("STA", Mem(_runtimeIntrinsicTmp1Address));
            _assembly.Add(Expr.Make(Tag.Label, sdone));
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("LDX", Mem(_runtimeIntrinsicTmp1Address));
            EmitAsm("RTS");

            EmitMacHelper("__mac16", "__mul16x8");
            EmitMacHelper("__smac16", "__smul16x8");
            EmitDot2Helper("__dot2_q8_8", "__mul16x8");
            EmitDot3Helper("__dot3_q8_8", "__mul16x8");
            EmitDot2Helper("__sdot2_q8_8", "__smul16x8");
            EmitDot3Helper("__sdot3_q8_8", "__smul16x8");
        }
__exp_pad_read1

__exp_pad_read1は__pad_read1_d1と同じコンパイラ内ヘルパーを呼びます。$4016の拡張入力D1を、同じラッチ手順で読み取り、8ビットの結果を返します。副作用も同じです。拡張入力用であることを示す名前であり、別の機器モードを有効にしたりD0とD1を合成したりするものではありません。

書式

c
u8 __exp_pad_read1(void);

戻り値

NESの生のビット順による8ビットの押下中マスクです。各ビット1が押されているボタンを表し、0はなしです。0/1に正規化された真偽値ではありません。

使用条件と制限

NESの生の形式は A=0x01、B=0x02、SELECT=0x04、START=0x08、UP=0x10、DOWN=0x20、LEFT=0x40、RIGHT=0x80 です。ビット1が押下中です。nes_pad_* と生の __pad_* の結果にはこの値を使い、input.h の共通 BTN_* マスクとは混同しないでください。

選択したD1に互換性のある8ビットのシリアルパッド信号を出す周辺機器で使います。接続機器の識別や、すべての拡張機器の通信方式への対応は行いません。各呼び出しで両コントローラーの読み取り列をラッチし直すため、1つの読み取りを完了させてから次を始めてください。結果は新たに取得した状態で、input_*やnes_pad1_*の保存済み状態は更新しません。

公開版KUROSAKIのコントローラーモデルが返すのはD0と固定のビット6だけです。そのため通常のボタンを押していても、同エミュレーターではD1とマイクのビットは常に0になります。付属の検証で確認したのは、ビルド、通常ボタン入力、この0の信号表示です。周辺機器からの0以外の信号、電気的タイミング、実機での互換性は未確認です。ここで0が表示されても、接続機器が動作する証明や、実機でも読み取れないという証明にはなりません。

使用例

c
expansion_first=__exp_pad_read1(); // Same helper as __pad_read1_d1.

呼び出し部分のコード例です。完全なプログラムに組み込む際は、記載した初期化とバッファの条件を満たしてください。

期待する結果

通常入力のD0、拡張入力のD1、マイクのデジタルレベルを比較するプログラムです。状態が取得されるまでコントローラー1のAとコントローラー2のBを押します。NORMAL P1とNORMAL P2は001、002になります。付属のKUROSAKI実行結果では、D1 PORT 1、D1 PORT 2、EXP ALIAS 1、EXP ALIAS 2、MIC LEVEL、MIC DIRECTはすべて000です。対応する信号線がエミュレートされていないためです。取得後の表示は固定されます。適切な周辺機器と信号線の実装がある環境では実際の入力で値が変わるため、この0を期待値とせず、上記のビット配置に従って読み取ってください。

公開版KUROSAKIのコントローラーモデルが返すのはD0と固定のビット6だけです。そのため通常のボタンを押していても、同エミュレーターではD1とマイクのビットは常に0になります。付属の検証で確認したのは、ビルド、通常ボタン入力、この0の信号表示です。周辺機器からの0以外の信号、電気的タイミング、実機での互換性は未確認です。ここで0が表示されても、接続機器が動作する証明や、実機でも読み取れないという証明にはなりません。

__exp_pad_read1: NROM
NROM

使用例の完全なプログラム

ビルドコマンド

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\expansion_input.c -I .\kitaq-docs\samples -I .\kitaqfc\lib -o .\out\expansion_input.nes --mapper=nrom --nes-chr=.\kitaq-docs\samples\font.chr --no-cache --no-disasm
実装とソース

kitaqfc/lib/intrinsics.h:227

宣言の注記(原文)
Alias the port-1 D1 controller reader; the attached device must use the expected serial protocol.
__exp_pad_read2

__exp_pad_read2は__pad_read2_d1と同じコンパイラ内ヘルパーを呼びます。$4017の拡張入力D1を、同じラッチ手順で読み取り、8ビットの結果を返します。副作用も同じです。拡張入力用であることを示す名前であり、別の機器モードを有効にしたりD0とD1を合成したりするものではありません。

書式

c
u8 __exp_pad_read2(void);

戻り値

NESの生のビット順による8ビットの押下中マスクです。各ビット1が押されているボタンを表し、0はなしです。0/1に正規化された真偽値ではありません。

使用条件と制限

NESの生の形式は A=0x01、B=0x02、SELECT=0x04、START=0x08、UP=0x10、DOWN=0x20、LEFT=0x40、RIGHT=0x80 です。ビット1が押下中です。nes_pad_* と生の __pad_* の結果にはこの値を使い、input.h の共通 BTN_* マスクとは混同しないでください。

選択したD1に互換性のある8ビットのシリアルパッド信号を出す周辺機器で使います。接続機器の識別や、すべての拡張機器の通信方式への対応は行いません。各呼び出しで両コントローラーの読み取り列をラッチし直すため、1つの読み取りを完了させてから次を始めてください。結果は新たに取得した状態で、input_*やnes_pad1_*の保存済み状態は更新しません。

公開版KUROSAKIのコントローラーモデルが返すのはD0と固定のビット6だけです。そのため通常のボタンを押していても、同エミュレーターではD1とマイクのビットは常に0になります。付属の検証で確認したのは、ビルド、通常ボタン入力、この0の信号表示です。周辺機器からの0以外の信号、電気的タイミング、実機での互換性は未確認です。ここで0が表示されても、接続機器が動作する証明や、実機でも読み取れないという証明にはなりません。

使用例

c
expansion_second=__exp_pad_read2(); // Same helper as __pad_read2_d1.

呼び出し部分のコード例です。完全なプログラムに組み込む際は、記載した初期化とバッファの条件を満たしてください。

期待する結果

通常入力のD0、拡張入力のD1、マイクのデジタルレベルを比較するプログラムです。状態が取得されるまでコントローラー1のAとコントローラー2のBを押します。NORMAL P1とNORMAL P2は001、002になります。付属のKUROSAKI実行結果では、D1 PORT 1、D1 PORT 2、EXP ALIAS 1、EXP ALIAS 2、MIC LEVEL、MIC DIRECTはすべて000です。対応する信号線がエミュレートされていないためです。取得後の表示は固定されます。適切な周辺機器と信号線の実装がある環境では実際の入力で値が変わるため、この0を期待値とせず、上記のビット配置に従って読み取ってください。

公開版KUROSAKIのコントローラーモデルが返すのはD0と固定のビット6だけです。そのため通常のボタンを押していても、同エミュレーターではD1とマイクのビットは常に0になります。付属の検証で確認したのは、ビルド、通常ボタン入力、この0の信号表示です。周辺機器からの0以外の信号、電気的タイミング、実機での互換性は未確認です。ここで0が表示されても、接続機器が動作する証明や、実機でも読み取れないという証明にはなりません。

__exp_pad_read2: NROM
NROM

使用例の完全なプログラム

ビルドコマンド

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\expansion_input.c -I .\kitaq-docs\samples -I .\kitaqfc\lib -o .\out\expansion_input.nes --mapper=nrom --nes-chr=.\kitaq-docs\samples\font.chr --no-cache --no-disasm
実装とソース

kitaqfc/lib/intrinsics.h:229

宣言の注記(原文)
Alias the port-2 D1 controller reader; this is not an expansion-device discovery operation.
__far_memcpy

ROMバンクを一時的に切り替え、srcからdstへアドレスの小さい方からlenバイトをコピーし、コンパイラが保持していたROMバンクへ戻します。この機種では__farmemcpyと__far_memcpyは同じ実装を使います。

書式

c
void __far_memcpy(u8* dst, u8 bank, u16 src, u16 len);

引数

dst

書き込み先のメモリです。実際のコピー量を収められる領域を確保してください。0バイト転送以外では、ヌルポインタや無効な書き込み先を検出する処理はありません。

bank

コピー元ROMのバンクを選ぶ8ビット値です。カートリッジとコンパイラのバンク配置に対応する番号を使います。コピー元アドレスとは別に指定します。

src

選んだROM配置でのコピー元CPUアドレスを16ビット値で指定します。FC版の宣言はu16なので、素材ポインタは(u16)pointerの形で変換します。

len

バイト数を0~65535で指定します。0ならメモリはコピーしませんが、バンクの選択と復帰の処理は実行します。

戻り値

ありません(void)。

使用条件と制限

コピー元全体が目的のROM配置で読め、コピー先全体が書き込み可能な範囲になるようにしてください。バンク窓の端で次のバンクへ進む処理、memmoveのような重なりへの対応、VRAMの安全な書き込み制御はありません。VRAMには描画用の転送APIを使います。

サンプルの呼び出し側コードは固定バンク0に配置し、コピー元の切り替えで実行中のコードが見えなくなることを防ぎます。転送は同期処理です。転送中に割り込み側から別のバンク切り替えコピーを実行したり、この関数だけでアプリ全体の割り込み安全性が確保されると考えたりしないでください。

使用例

c
__far_memcpy(buffer,payload_bank,(u16)payload_pointer,32);
expect(*asset_bank_marker==0xA1 && bank_get_current()==1,27);
for(i=0;i<32;i++) expect(buffer[(__safe_index u8)i]==expected_shape_byte(i),28);
buffer[32]=0x55;
__far_memcpy(buffer+32,2,(u16)asset_shapes,0);
expect(buffer[32]==0x55 && *asset_bank_marker==0xA1,30);

呼び出し部分のコード例です。完全なプログラムに組み込む際は、記載した初期化とバッファの条件を満たしてください。

期待する結果

バンク1を選択した状態から、素材のバンク2にある絵柄2枚、32バイトをコピーします。コピーした全バイト、バンク1への復帰、長さ0のコピーで確認値0x55が変わらないことを検査します。

期待画像では、赤い横一列がピクセル座標(16,32)から始まり、72×8ピクセルを占めます。8×8の三角形、1ピクセル枠の8×8の正方形、左4列を塗ったタイルの順に、3回繰り返します。三角形は左上の1ピクセルから最下段の8ピクセルまで広がります。

FC版では、(16,64)の青い三角形と枠が__far_memcpyの結果、(16,96)の緑の組がasset_load_rawの後にCHRへ転送した結果です。黒地の各組は16×8ピクセルです。数値だけでなく、コピーした絵柄のバイト列が正しいことを目で確認できます。

期待する表示はFAILED CHECKS 000とFIRST FAILURE 00です。検証では300フレーム実行し、文字、図形の形、色を期待値と比較します。これはエミュレータ上の検証であり、実機試験ではありません。

__far_memcpy: SUROM512
SUROM512

使用例の完全なプログラム

ビルドコマンド

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\asset_banks.c -I .\kitaqfc\lib -I .\kitaq-docs\samples -o .\out\asset_banks.nes --mapper=mmc1 --board=surom512 --no-cache --no-disasm
実装とソース

kitaqfc/lib/intrinsics.h:86

宣言の注記(原文)
Switch to the source bank, copy len bytes forward and restore the previous bank.
Keep destination RAM and helper code accessible throughout; overlapping copies are not memmove.
__farcall

引数のない名前付き関数を呼び出します。FCコンパイラは関数の配置バンクから、直接呼び出すか、バンク復元付きの補助処理を使うか決めます。bank引数は評価しますが、実際の選択先は関数の配置で決まります。

書式

c
__farcall(bank, function_name);

引数

bank

符号なし8ビットのROMバンク番号です。呼び出しでは関数の配置と一致させ、読み出しでは転送元のマッピングを指定します。対象カートリッジで有効な範囲を使用してください。

func

宣言済みの、引数のない関数名です。この名前指定の組み込み呼び出しには、関数アドレスを格納した変数を渡せません。bank引数をコールバックの引数として渡すこともありません。

戻り値

戻り値の型は呼び出した関数に従います。voidのコールバックは値を返さず、u8・u16のコールバックはその値を返します。掲載サンプルはvoidのコールバックを使います。

使用条件と制限

これらの呼び出しは、ユーザー引数を渡したり、コールバックを割り込みに対して再入可能にしたりするものではありません。コールバック自身のデータ・スタック・割り込みの使い方を適切に管理してください。バンク復帰は入れ子呼び出しと反復呼び出しで検証していますが、この例で実機や任意の割り込みタイミングを検証したわけではありません。

FCの切り替え番号はコンパイラの論理バンク番号です。0は共通ROMを示し、切り替え先ではありません。補助処理は0をバンク1として扱います。SUROM512の有効範囲は1〜30で、切り替え組み込み関数へ直接渡す範囲外の定数はビルドエラーです。範囲外の実行時値では1を選び、マッパー異常状態を設定します。共通ROMは、バンク0への切り替えを要求せず直接読んでください。

使用例

c
__farcall(3,callback);
bank_expect(callback_count==2 && callback_byte==0xD3,16);

呼び出し部分のコード例です。完全なプログラムに組み込む際は、記載した初期化とバッファの条件を満たしてください。

期待する結果

完成プログラムのこの位置では、__farcallによってcallback_countが2になります。コールバックはバンク3からD3を読みます。その後にhome2を近ポインターで読んでB2を確認し、バンク2が選ばれた状態で復帰したことを確かめます。

完成プログラムでは、バンク番号と近アドレスによるデータの指定、バンクをまたぐコールバック、ライブラリの記録と実際のマッピングの区別を学べます。数値はすべて16進数です。BANK INとBANK OUTは02、BYTE HEXはD3、WORD HEXは5C7A、FAILED CHECKSとFIRST FAILUREはどちらも00が期待値です。CGBとFCは青字、DMGは黒字で表示します。

CALL COUNTは02です。far_callマクロと__farcallがそれぞれ1回コールバックを実行します。

__farcall: MMC3
MMC3

使用例の完全なプログラム

ビルドコマンド

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\banked_data_calls.c -I .\kitaqfc\lib -I .\kitaq-docs\samples -o .\out\banked_data_calls.nes --mapper=mmc3 --nes-chr=.\kitaq-docs\samples\font.chr --no-cache --no-disasm
実装とソース

kitaqfc/lib/intrinsics.h:83

宣言の注記(原文)
Call a declared zero-argument function; placement selects its bank and its declared type determines the result.

kitaqfc/kitaqfc/CodeGenerator.cs:4131

csharp
if (string.Equals(funcName, "__farcall", StringComparison.Ordinal))
            {
                Expr[] farArgs = args ?? Array.Empty<Expr>();
                if (farArgs.Length != 2)
                {
                    Program.Error("error KQ0000: __farcall(bank, func) expects exactly 2 arguments.");
                    return;
                }

                string targetName;
                if (!farArgs[1].Match(Tag.Name, out targetName))
                {
                    Program.Error("error KQ0000: __farcall(bank, func) requires the 2nd argument to be a function name.");
                    return;
                }
                // This intrinsic supplies no callback arguments. A variable name
                // is not a static function target, even when its value is a pointer.
                if (!_functionParameters.TryGetValue(targetName, out FieldInfo[] farParameters))
                {
                    Program.Error("error KQFC2610: __farcall requires a declared function name; use an ordinary typed call for a function pointer.");
                    return;
                }
                if (farParameters.Length != 0)
                {
                    Program.Error("error KQFC2610: __farcall requires a callback with no parameters.");
                    return;
                }
                if (Program.NesMapperProfile.IsSurom512 &&
                    TryEvaluateConstant(farArgs[0], out int constantFarBank) &&
                    (constantFarBank < 1 || constantFarBank > 30))
                {
                    Program.Error("error KQFC2603 KQFC-SUROM-BANK-RANGE: __farcall constant bank {0} is outside 1..30; common bank 0 is not a switch target.", constantFarBank);
                    return;
                }

                // Evaluate the requested far-call bank byte; the following dispatch uses the target function bank metadata.
                EmitLoadValue(farArgs[0], ctx, 1);

                int callerBank = ctx == null ? 0 : ctx.Bank;
                int calleeBank = GetFunctionBank(targetName);
                if (ShouldUseFdsOverlayThunk(callerBank, calleeBank))
                {
                    string fdsThunkName = EnsureFdsOverlayThunk(targetName, calleeBank);
                    EmitAsm("JSR", Abs(fdsThunkName));
                    RecordCallEdge(targetName, calleeBank, "fds_overlay_thunk", viaThunk: true, viaFarcall: true);
                    return;
                }
                bool bankedPrg = Program.NesMapperProfile.SupportsPrgBanking || Program.NesMapperProfile.UsesDuplicatedCommonBank;
                bool directSafe = !bankedPrg || calleeBank == 0 || calleeBank == callerBank;
                if (directSafe)
                {
                    EmitAsm("JSR", Abs(targetName));
                    RecordCallEdge(targetName, calleeBank, "direct", viaThunk: false, viaFarcall: true);
                    return;
                }

                string thunkName = EnsureBankThunk(targetName, calleeBank);
                EmitAsm("JSR", Abs(thunkName));
                RecordCallEdge(targetName, calleeBank, "bank_thunk", viaThunk: true, viaFarcall: true);
                return;
            }
__farmemcpy

__farmemcpyはFCの__far_memcpyを別の綴りで呼ぶ組み込み関数です。引数の順序、バンクの復元、コピーする内容は同じで、どちらを選んでも転送方式や速度は変わりません。サンプルではこの綴りを使って、ROMの絵柄をRAMへ読み出します。

ROMバンクを一時的に切り替え、srcからdstへアドレスの小さい方からlenバイトをコピーし、コンパイラが保持していたROMバンクへ戻します。この機種では__farmemcpyと__far_memcpyは同じ実装を使います。

書式

c
void __farmemcpy(u8* dst, u8 bank, u16 src, u16 len);

引数

dst

書き込み先のメモリです。実際のコピー量を収められる領域を確保してください。0バイト転送以外では、ヌルポインタや無効な書き込み先を検出する処理はありません。

bank

コピー元ROMのバンクを選ぶ8ビット値です。カートリッジとコンパイラのバンク配置に対応する番号を使います。コピー元アドレスとは別に指定します。

src

選んだROM配置でのコピー元CPUアドレスを16ビット値で指定します。FC版の宣言はu16なので、素材ポインタは(u16)pointerの形で変換します。

len

バイト数を0~65535で指定します。0ならメモリはコピーしませんが、バンクの選択と復帰の処理は実行します。

戻り値

ありません(void)。

使用条件と制限

この綴りはコンパイラに組み込み登録されています。intrinsics.hに別の宣言がなくても呼び出せます。引数は順にRAMポインター、8ビットのバンク番号、16ビットのCPUアドレス、16ビットのバイト数です。srcはROMファイル先頭からのオフセットではありません。

コピー元全体が目的のROM配置で読め、コピー先全体が書き込み可能な範囲になるようにしてください。バンク窓の端で次のバンクへ進む処理、memmoveのような重なりへの対応、VRAMの安全な書き込み制御はありません。VRAMには描画用の転送APIを使います。

サンプルの呼び出し側コードは固定バンク0に配置し、コピー元の切り替えで実行中のコードが見えなくなることを防ぎます。転送は同期処理です。転送中に割り込み側から別のバンク切り替えコピーを実行したり、この関数だけでアプリ全体の割り込み安全性が確保されると考えたりしないでください。

lenが0ならコピー先のバイトは変更されませんが、バンク選択と復元の処理は実行されます。戻り値はありません。

__far_memcpy

使用例

c
__farmemcpy(buffer,payload_bank,(u16)payload_pointer,32);
expect(*asset_bank_marker==0xA1 && bank_get_current()==1,27);
for(i=0;i<32;i++) expect(buffer[(__safe_index u8)i]==expected_shape_byte(i),28);
buffer[32]=0x55;
__farmemcpy(buffer+32,2,(u16)asset_shapes,0);
expect(buffer[32]==0x55 && *asset_bank_marker==0xA1,30);

呼び出し部分のコード例です。完全なプログラムに組み込む際は、記載した初期化とバッファの条件を満たしてください。

期待する結果

バンク1を選択した状態から、素材のバンク2にある絵柄2枚、32バイトをコピーします。コピーした全バイト、バンク1への復帰、長さ0のコピーで確認値0x55が変わらないことを検査します。

期待画像では、赤い横一列がピクセル座標(16,32)から始まり、72×8ピクセルを占めます。8×8の三角形、1ピクセル枠の8×8の正方形、左4列を塗ったタイルの順に、3回繰り返します。三角形は左上の1ピクセルから最下段の8ピクセルまで広がります。

FC画面の(16,64)にある青い三角形と正方形の枠は__farmemcpyでコピーした絵柄です。その下の(16,96)にある緑の組はasset_load_rawの結果です。黒地の各組は16×8ピクセルで、コピー後にCHR RAMへ転送して表示します。

期待する表示はFAILED CHECKS 000とFIRST FAILURE 00です。検証では300フレーム実行し、文字、図形の形、色を期待値と比較します。これはエミュレータ上の検証であり、実機試験ではありません。

__farmemcpy: SUROM512 — FC画面の(16,64)にある青い三角形と正方形の枠は__farmemcpyでコピーした絵柄です。その下の(16,96)にある緑の組はasset_load_rawの結果です。黒地の各組は16×8ピクセルで、コピー後にCHR RAMへ転送して表示します。
SUROM512 — FC画面の(16,64)にある青い三角形と正方形の枠は__farmemcpyでコピーした絵柄です。その下の(16,96)にある緑の組はasset_load_rawの結果です。黒地の各組は16×8ピクセルで、コピー後にCHR RAMへ転送して表示します。

使用例の完全なプログラム

ビルドコマンド

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\far_copy_alias.c -I .\kitaqfc\lib -I .\kitaq-docs\samples -o .\out\far_copy_alias.nes --mapper=mmc1 --board=surom512 --no-cache --no-disasm
実装とソース

kitaqfc/kitaqfc/CodeGenerator.cs:264

kitaqfc/kitaqfc/CodeGenerator.cs:4668

csharp
case "__far_memcpy":
                case "__farmemcpy":
                    return EmitKitaqfcHelperCall(funcName, args, ctx, new[] {2,1,2,2}, "__far_memcpy");
__farpeek16

バンクを一時的に選び、addrとaddr+1からリトルエンディアンの16ビット値を読んで復帰します。far_data_read16が利用する組み込み関数です。

書式

c
u16 __farpeek16(u8 bank, u16 addr);

引数

bank

符号なし8ビットのROMバンク番号です。呼び出しでは関数の配置と一致させ、読み出しでは転送元のマッピングを指定します。対象カートリッジで有効な範囲を使用してください。

addr

選んだROM配置でのコピー元CPUアドレスを16ビット値で指定します。FC版の宣言はu16なので、素材ポインタは(u16)pointerの形で変換します。

戻り値

読み出したリトルエンディアンのワードをu16(0〜65535)で返します。

使用条件と制限

対象のバイトまたはワード全体を、選択したCPUマッピングで読める必要があります。ポインターの検査、アドレスの切り詰め、ウィンドウ境界での次バンクへの自動切り替えは行いません。ワード読み出しには連続する有効な2バイトが必要です。

遠方読み出しとコピーは同期処理で、コンパイラの共有作業領域を使います。処理中に割り込みから別のバンク転送を実行し、同じ作業領域を上書きしないでください。GBのコピー補助処理は、復帰バンクの保存にもHRAMの共有領域を1か所使います。例では競合する転送割り込みを設けず、固定コードから実行します。

FCの切り替え番号はコンパイラの論理バンク番号です。0は共通ROMを示し、切り替え先ではありません。補助処理は0をバンク1として扱います。SUROM512の有効範囲は1〜30で、切り替え組み込み関数へ直接渡す範囲外の定数はビルドエラーです。範囲外の実行時値では1を選び、マッパー異常状態を設定します。共通ROMは、バンク0への切り替えを要求せず直接読んでください。

使用例

c
bank_expect(__farpeek16(3,(u16)(payload+1))==0x5C7A,14);

呼び出し部分のコード例です。完全なプログラムに組み込む際は、記載した初期化とバッファの条件を満たしてください。

期待する結果

この断片では__farpeek16でバンク3のpayload+1を読み、5C7Aを確認します。完成プログラムでは、home2の近ポインター読み出しでもバンク2への復帰を確認します。

完成プログラムでは、バンク番号と近アドレスによるデータの指定、バンクをまたぐコールバック、ライブラリの記録と実際のマッピングの区別を学べます。数値はすべて16進数です。BANK INとBANK OUTは02、BYTE HEXはD3、WORD HEXは5C7A、FAILED CHECKSとFIRST FAILUREはどちらも00が期待値です。CGBとFCは青字、DMGは黒字で表示します。

CALL COUNTは02です。far_callマクロと__farcallがそれぞれ1回コールバックを実行します。

__farpeek16: MMC3
MMC3

使用例の完全なプログラム

ビルドコマンド

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\banked_data_calls.c -I .\kitaqfc\lib -I .\kitaq-docs\samples -o .\out\banked_data_calls.nes --mapper=mmc3 --nes-chr=.\kitaq-docs\samples\font.chr --no-cache --no-disasm
実装とソース

kitaqfc/lib/intrinsics.h:90

宣言の注記(原文)
Temporarily select bank and read a little-endian word, then restore the previous mapping.

kitaqfc/kitaqfc/CodeGenerator.cs:8698

csharp
EmitHelperStart("__farpeek16");
            // args: bank, addr16. Return A=lo, X=hi.
            EmitAsm("LDA", Mem(_runtimeCurrentBankAddress));
            EmitAsm("PHA");
            EmitAsm("LDA", Mem(CallArgBase));
            EmitAsm("JSR", Abs("__kq_prg_set_bank_a"));
            EmitAsm("LDY", Imm(0));
            EmitAsm("LDA", IndY(CallArgBase + 1));
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("INY");
            EmitAsm("LDA", IndY(CallArgBase + 1));
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp1Address));
            EmitAsm("PLA");
            EmitAsm("JSR", Abs("__kq_prg_set_bank_a"));
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("LDX", Mem(_runtimeIntrinsicTmp1Address));
            EmitAsm("RTS");

            // Consume the word count while advancing source/destination pointers; zero count still restores the saved bank.
__farpeek8

指定バンクを一時的に選び、CPUアドレスから1バイト読んで元のバンクに戻します。far_data_read8が利用する組み込み関数です。

書式

c
u8 __farpeek8(u8 bank, u16 addr);

引数

bank

符号なし8ビットのROMバンク番号です。呼び出しでは関数の配置と一致させ、読み出しでは転送元のマッピングを指定します。対象カートリッジで有効な範囲を使用してください。

addr

選んだROM配置でのコピー元CPUアドレスを16ビット値で指定します。FC版の宣言はu16なので、素材ポインタは(u16)pointerの形で変換します。

戻り値

読み出したバイトをu8(0〜255)で返します。

使用条件と制限

対象のバイトまたはワード全体を、選択したCPUマッピングで読める必要があります。ポインターの検査、アドレスの切り詰め、ウィンドウ境界での次バンクへの自動切り替えは行いません。ワード読み出しには連続する有効な2バイトが必要です。

遠方読み出しとコピーは同期処理で、コンパイラの共有作業領域を使います。処理中に割り込みから別のバンク転送を実行し、同じ作業領域を上書きしないでください。GBのコピー補助処理は、復帰バンクの保存にもHRAMの共有領域を1か所使います。例では競合する転送割り込みを設けず、固定コードから実行します。

FCの切り替え番号はコンパイラの論理バンク番号です。0は共通ROMを示し、切り替え先ではありません。補助処理は0をバンク1として扱います。SUROM512の有効範囲は1〜30で、切り替え組み込み関数へ直接渡す範囲外の定数はビルドエラーです。範囲外の実行時値では1を選び、マッパー異常状態を設定します。共通ROMは、バンク0への切り替えを要求せず直接読んでください。

使用例

c
bank_expect(__farpeek8(3,(u16)payload)==0xD3,13);

呼び出し部分のコード例です。完全なプログラムに組み込む際は、記載した初期化とバッファの条件を満たしてください。

期待する結果

この断片では__farpeek8でバンク3のpayloadを読み、D3を確認します。完成プログラムでは、home2の近ポインター読み出しでもバンク2への復帰を確認します。

完成プログラムでは、バンク番号と近アドレスによるデータの指定、バンクをまたぐコールバック、ライブラリの記録と実際のマッピングの区別を学べます。数値はすべて16進数です。BANK INとBANK OUTは02、BYTE HEXはD3、WORD HEXは5C7A、FAILED CHECKSとFIRST FAILUREはどちらも00が期待値です。CGBとFCは青字、DMGは黒字で表示します。

CALL COUNTは02です。far_callマクロと__farcallがそれぞれ1回コールバックを実行します。

__farpeek8: MMC3
MMC3

使用例の完全なプログラム

ビルドコマンド

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\banked_data_calls.c -I .\kitaqfc\lib -I .\kitaq-docs\samples -o .\out\banked_data_calls.nes --mapper=mmc3 --nes-chr=.\kitaq-docs\samples\font.chr --no-cache --no-disasm
実装とソース

kitaqfc/lib/intrinsics.h:88

宣言の注記(原文)
Temporarily select bank, read one byte at addr and restore the prior bank mapping.

kitaqfc/kitaqfc/CodeGenerator.cs:8683

csharp
EmitHelperStart("__farpeek8");
            // args: bank, addr16. Return A.
            EmitAsm("LDA", Mem(_runtimeCurrentBankAddress));
            EmitAsm("PHA");
            EmitAsm("LDA", Mem(CallArgBase));
            EmitAsm("JSR", Abs("__kq_prg_set_bank_a"));
            EmitAsm("LDY", Imm(0));
            EmitAsm("LDA", IndY(CallArgBase + 1));
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("PLA");
            EmitAsm("JSR", Abs("__kq_prg_set_bank_a"));
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("RTS");

            // Retain both fetched bytes across bank restoration and return the word in A/X.
__fkb_detect

キーボードの検出用パターンを調べます。キーの押下を調べる前の接続判定に使います。

書式

c
u8 __fkb_detect(void);

戻り値

行9・列0が0x1Eで、行列を無効にした後の値が0なら1。それ以外は0です。機器識別IDではなく、信号パターンによる推定です。

使用条件と制限

これらは$4016/$4017を直接操作します。通常のパッド、キーボード、シリアル送信など、同じポートを使う処理を割り込みで混在させないでください。パッドのシリアル読み取り位置にも影響します。サンプルは入力の採取中だけNMIを止め、表示を初期化してから再開します。

$4016へ5を書いて行0にリセットし、列選択ビットを1→0に切り替えて行を進めます。リセット後は12 CPUサイクル分、列選択後は50サイクル分のNOP待機を入れます(周囲の命令時間は別)。最後は0を書いてキーボード行列を無効化します。

別の回帰検証で、行0~9・列0/1の選択、9行の一括走査、待ち時間、最後のポート値を、実行したCPUのバス記録から確認しています。キーの電気信号を入力した検証ではありません。

行9にはキーがありません。途中で条件を満たさなければ0で戻ります。KUROSAKIでは最初の読み取りが0のため検出しません。

掲載画像は配布サンプルを変更せずにKUROSAKIで実行した結果です。現在のKUROSAKIはパッドの入力モデルを持ちますが、キーボード・光線銃・シリアルアダプターの信号はモデル化していません。外部入力ビットが0になるため、画像は周辺機器の動作確認ではありません。実機のキー入力、受光、通信は未検証です。

NESdev: Family BASIC Keyboard

使用例

c
#include "fc_ports_scene.h"
void main(void){
    u8 row;
    __irq_disable();__ppu_ctrl_set(0);port_phase=1;
    port_result[0]=__fkb_detect();
    __fkb_scan(key_matrix);
    port_result[1]=__fkb_read_row_col(6,0);
    port_result[2]=0;
    if(port_result[0] && !(port_result[1]&16))port_result[2]=1;
    port_phase=2;port_result[31]=165;
    m_init();m_text(2,2,"KEYBOARD STARTUP SNAPSHOT");m_wait();
    m_text(2,4,"DEVICE DETECT");port_number(25,4,port_result[0]);m_wait();
    m_text(2,6,"ROW 6 COL 0");port_number(25,6,port_result[1]);m_wait();
    m_text(2,8,"A KEY - GUARDED");port_number(25,8,port_result[2]);m_wait();
    m_text(2,10,"ROW   COL0  COL1  (HEX)");m_wait();
    for(row=0;row<9;row++){
        m_put(3,12+row,'0'+row);port_hex(9,12+row,key_matrix[row*2]);port_hex(15,12+row,key_matrix[row*2+1]);m_wait();
    }
    m_text(2,23,"RAW MATRIX - NOT ASCII");m_wait();
    while(1){m_wait();}
}

呼び出し部分のコード例です。完全なプログラムに組み込む際は、記載した初期化とバッファの条件を満たしてください。

期待する結果

このKUROSAKI実行ではDEVICE DETECT、ROW 6 COL 0、A KEY - GUARDEDがすべて000、9行のCOL0/COL1がすべて00です。Aは行6・列0のビット4ですが、未検出時は生値0を押下と扱わず、表示も000に保ちます。表は起動時の一度の採取結果です。

__fkb_detect: KEYBOARD-DEFAULT — このKUROSAKI実行ではDEVICE DETECT、ROW 6 COL 0、A KEY - GUARDEDがすべて000、9行のCOL0/COL1がすべて00です。Aは行6・列0のビット4ですが、未検出時は生値0を押下と扱わず、表示も000に保ちます。表は起動時の一度の採取結果です。
KEYBOARD-DEFAULT — このKUROSAKI実行ではDEVICE DETECT、ROW 6 COL 0、A KEY - GUARDEDがすべて000、9行のCOL0/COL1がすべて00です。Aは行6・列0のビット4ですが、未検出時は生値0を押下と扱わず、表示も000に保ちます。表は起動時の一度の採取結果です。

使用例の完全なプログラム

ビルドコマンド

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\peripheral_keyboard.c -I .\kitaqfc\lib -I .\kitaq-docs\samples -o .\out\peripheral_keyboard.nes --mapper=nrom --nes-chr=.\kitaq-docs\samples\font.chr --no-cache --no-disasm
.\kurosaki\kurosaki.exe run .\out\peripheral_keyboard.nes --frames 120 --png .\out\peripheral_keyboard.png
実装とソース

kitaqfc/lib/intrinsics.h:259

宣言の注記(原文)
Family BASIC Keyboard. __fkb_scan() writes 18 bytes: 9 rows x 2 columns,
each byte containing raw $4017 & 0x1E for that row/column.

Probe selected-row and disabled-matrix bit patterns and return 0 or 1; this is a heuristic detection.

kitaqfc/kitaqfc/CodeGenerator.cs:7842

csharp
EmitHelperStart("__fkb_detect");
            EmitAsm("LDA", Imm(0x09));
            EmitAsm("STA", Mem(CallArgBase));
            EmitAsm("LDA", Imm(0));
            EmitAsm("STA", Mem(CallArgBase + 1));
            EmitAsm("JSR", Abs("__fkb_read_row_col"));
            EmitAsm("CMP", Imm(0x1E));
            string no = NewGeneratedLabel("fkb_no");
            EmitAsm("BNE", Rel(no));
            EmitAsm("LDA", Imm(0));
            EmitAsm("STA", Mem(0x4016));
            EmitAsm("LDA", Mem(0x4017));
            EmitAsm("AND", Imm(0x1E));
            EmitAsm("BNE", Rel(no));
            EmitAsm("LDA", Imm(1));
            EmitAsm("RTS");
            _assembly.Add(Expr.Make(Tag.Label, no));
            EmitAsm("LDA", Imm(0));
            EmitAsm("RTS");
__fkb_read_row_col

指定した1行・1列群だけを読みます。必要なキー群が決まっている場合に、18バイトのバッファを使わず取得できます。

書式

c
u8 __fkb_read_row_col(u8 row, u8 col);

引数

row

キーは0~8、検出用のキーなし行は9です。範囲外を検査するコードは生成しません。

col

0または1。実装では下位1ビットだけを使うため、偶数は列0、奇数は列1になります。

戻り値

$4017 & 0x1E。ビットを詰め直さず、そのまま返します。

使用条件と制限

これらは$4016/$4017を直接操作します。通常のパッド、キーボード、シリアル送信など、同じポートを使う処理を割り込みで混在させないでください。パッドのシリアル読み取り位置にも影響します。サンプルは入力の採取中だけNMIを止め、表示を初期化してから再開します。

キー状態はビット1~4の生値で、押すと対応ビットが0になります。無押下は0x1Eです。ASCII文字列でも、押下したキーを1にしたビットマップでもありません。未接続でも0になる場合があるため、接続判定なしに0を「全部押された」と解釈しないでください。

$4016へ5を書いて行0にリセットし、列選択ビットを1→0に切り替えて行を進めます。リセット後は12 CPUサイクル分、列選択後は50サイクル分のNOP待機を入れます(周囲の命令時間は別)。最後は0を書いてキーボード行列を無効化します。

別の回帰検証で、行0~9・列0/1の選択、9行の一括走査、待ち時間、最後のポート値を、実行したCPUのバス記録から確認しています。キーの電気信号を入力した検証ではありません。

毎回行0から指定行まで進め直します。前の呼び出しの行位置を再利用するAPIではありません。

掲載画像は配布サンプルを変更せずにKUROSAKIで実行した結果です。現在のKUROSAKIはパッドの入力モデルを持ちますが、キーボード・光線銃・シリアルアダプターの信号はモデル化していません。外部入力ビットが0になるため、画像は周辺機器の動作確認ではありません。実機のキー入力、受光、通信は未検証です。

NESdev: Family BASIC Keyboard

使用例

c
#include "fc_ports_scene.h"
void main(void){
    u8 row;
    __irq_disable();__ppu_ctrl_set(0);port_phase=1;
    port_result[0]=__fkb_detect();
    __fkb_scan(key_matrix);
    port_result[1]=__fkb_read_row_col(6,0);
    port_result[2]=0;
    if(port_result[0] && !(port_result[1]&16))port_result[2]=1;
    port_phase=2;port_result[31]=165;
    m_init();m_text(2,2,"KEYBOARD STARTUP SNAPSHOT");m_wait();
    m_text(2,4,"DEVICE DETECT");port_number(25,4,port_result[0]);m_wait();
    m_text(2,6,"ROW 6 COL 0");port_number(25,6,port_result[1]);m_wait();
    m_text(2,8,"A KEY - GUARDED");port_number(25,8,port_result[2]);m_wait();
    m_text(2,10,"ROW   COL0  COL1  (HEX)");m_wait();
    for(row=0;row<9;row++){
        m_put(3,12+row,'0'+row);port_hex(9,12+row,key_matrix[row*2]);port_hex(15,12+row,key_matrix[row*2+1]);m_wait();
    }
    m_text(2,23,"RAW MATRIX - NOT ASCII");m_wait();
    while(1){m_wait();}
}

呼び出し部分のコード例です。完全なプログラムに組み込む際は、記載した初期化とバッファの条件を満たしてください。

期待する結果

このKUROSAKI実行ではDEVICE DETECT、ROW 6 COL 0、A KEY - GUARDEDがすべて000、9行のCOL0/COL1がすべて00です。Aは行6・列0のビット4ですが、未検出時は生値0を押下と扱わず、表示も000に保ちます。表は起動時の一度の採取結果です。

__fkb_read_row_col: KEYBOARD-DEFAULT — このKUROSAKI実行ではDEVICE DETECT、ROW 6 COL 0、A KEY - GUARDEDがすべて000、9行のCOL0/COL1がすべて00です。Aは行6・列0のビット4ですが、未検出時は生値0を押下と扱わず、表示も000に保ちます。表は起動時の一度の採取結果です。
KEYBOARD-DEFAULT — このKUROSAKI実行ではDEVICE DETECT、ROW 6 COL 0、A KEY - GUARDEDがすべて000、9行のCOL0/COL1がすべて00です。Aは行6・列0のビット4ですが、未検出時は生値0を押下と扱わず、表示も000に保ちます。表は起動時の一度の採取結果です。

使用例の完全なプログラム

ビルドコマンド

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\peripheral_keyboard.c -I .\kitaqfc\lib -I .\kitaq-docs\samples -o .\out\peripheral_keyboard.nes --mapper=nrom --nes-chr=.\kitaq-docs\samples\font.chr --no-cache --no-disasm
.\kurosaki\kurosaki.exe run .\out\peripheral_keyboard.nes --frames 120 --png .\out\peripheral_keyboard.png
実装とソース

kitaqfc/lib/intrinsics.h:264

宣言の注記(原文)
Select a matrix row and the low bit of col, then return raw 4017 & 1E.
Use rows 0..8 for keys; row 9 is used by detection. No row-range validation is emitted.

kitaqfc/kitaqfc/CodeGenerator.cs:7802

csharp
EmitHelperStart("__fkb_read_row_col");
            EmitAsm("LDA", Imm(0x05));
            EmitAsm("STA", Mem(0x4016));
            EmitKeyboardDelayShort();
            // Establish row zero, then generate one high-to-low column edge
            // per requested advance. Repeated writes of 04 do not clock the counter.
            EmitAsm("LDA", Imm(0x04));
            EmitAsm("STA", Mem(0x4016));
            EmitKeyboardDelayLong();
            EmitAsm("LDX", Mem(CallArgBase));
            string selectedRow = NewGeneratedLabel("fkb_selected_row");
            EmitAsm("BEQ", Rel(selectedRow));
            string adv = NewGeneratedLabel("fkb_adv_row");
            _assembly.Add(Expr.Make(Tag.Label, adv));
            EmitAsm("LDA", Imm(0x06));
            EmitAsm("STA", Mem(0x4016));
            EmitKeyboardDelayLong();
            EmitAsm("LDA", Imm(0x04));
            EmitAsm("STA", Mem(0x4016));
            EmitKeyboardDelayLong();
            EmitAsm("DEX");
            EmitAsm("BNE", Rel(adv));
            _assembly.Add(Expr.Make(Tag.Label, selectedRow));
            EmitAsm("LDA", Mem(CallArgBase + 1));
            EmitAsm("AND", Imm(1));
            string readNow = NewGeneratedLabel("fkb_read_now");
            EmitAsm("BEQ", Rel(readNow));
            EmitAsm("LDA", Imm(0x06));
            EmitAsm("STA", Mem(0x4016));
            EmitKeyboardDelayLong();
            _assembly.Add(Expr.Make(Tag.Label, readNow));
            EmitAsm("LDA", Mem(0x4017));
            EmitAsm("AND", Imm(0x1E));
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("LDA", Imm(0));
            EmitAsm("STA", Mem(0x4016));
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("RTS");
__fkb_scan

9行×2列群のキーボード行列をまとめて読み、18バイトへ保存します。複数キーを一度の走査で扱うための入口です。

書式

c
void __fkb_scan(u8* dst18);

引数

dst18

18バイト以上の書き込み可能な領域。添字row*2+colにその列群の生値を保存します。NULLや長さの検査はありません。

戻り値

戻り値はありません。

使用条件と制限

これらは$4016/$4017を直接操作します。通常のパッド、キーボード、シリアル送信など、同じポートを使う処理を割り込みで混在させないでください。パッドのシリアル読み取り位置にも影響します。サンプルは入力の採取中だけNMIを止め、表示を初期化してから再開します。

キー状態はビット1~4の生値で、押すと対応ビットが0になります。無押下は0x1Eです。ASCII文字列でも、押下したキーを1にしたビットマップでもありません。未接続でも0になる場合があるため、接続判定なしに0を「全部押された」と解釈しないでください。

$4016へ5を書いて行0にリセットし、列選択ビットを1→0に切り替えて行を進めます。リセット後は12 CPUサイクル分、列選択後は50サイクル分のNOP待機を入れます(周囲の命令時間は別)。最後は0を書いてキーボード行列を無効化します。

別の回帰検証で、行0~9・列0/1の選択、9行の一括走査、待ち時間、最後のポート値を、実行したCPUのバス記録から確認しています。キーの電気信号を入力した検証ではありません。

各行の列0、列1の順に読みます。逐次走査なので、全キーを同一の瞬間に採取する機能ではありません。キーが0の列でも走査を途中で打ち切りません。

掲載画像は配布サンプルを変更せずにKUROSAKIで実行した結果です。現在のKUROSAKIはパッドの入力モデルを持ちますが、キーボード・光線銃・シリアルアダプターの信号はモデル化していません。外部入力ビットが0になるため、画像は周辺機器の動作確認ではありません。実機のキー入力、受光、通信は未検証です。

NESdev: Family BASIC Keyboard

使用例

c
#include "fc_ports_scene.h"
void main(void){
    u8 row;
    __irq_disable();__ppu_ctrl_set(0);port_phase=1;
    port_result[0]=__fkb_detect();
    __fkb_scan(key_matrix);
    port_result[1]=__fkb_read_row_col(6,0);
    port_result[2]=0;
    if(port_result[0] && !(port_result[1]&16))port_result[2]=1;
    port_phase=2;port_result[31]=165;
    m_init();m_text(2,2,"KEYBOARD STARTUP SNAPSHOT");m_wait();
    m_text(2,4,"DEVICE DETECT");port_number(25,4,port_result[0]);m_wait();
    m_text(2,6,"ROW 6 COL 0");port_number(25,6,port_result[1]);m_wait();
    m_text(2,8,"A KEY - GUARDED");port_number(25,8,port_result[2]);m_wait();
    m_text(2,10,"ROW   COL0  COL1  (HEX)");m_wait();
    for(row=0;row<9;row++){
        m_put(3,12+row,'0'+row);port_hex(9,12+row,key_matrix[row*2]);port_hex(15,12+row,key_matrix[row*2+1]);m_wait();
    }
    m_text(2,23,"RAW MATRIX - NOT ASCII");m_wait();
    while(1){m_wait();}
}

呼び出し部分のコード例です。完全なプログラムに組み込む際は、記載した初期化とバッファの条件を満たしてください。

期待する結果

このKUROSAKI実行ではDEVICE DETECT、ROW 6 COL 0、A KEY - GUARDEDがすべて000、9行のCOL0/COL1がすべて00です。Aは行6・列0のビット4ですが、未検出時は生値0を押下と扱わず、表示も000に保ちます。表は起動時の一度の採取結果です。

__fkb_scan: KEYBOARD-DEFAULT — このKUROSAKI実行ではDEVICE DETECT、ROW 6 COL 0、A KEY - GUARDEDがすべて000、9行のCOL0/COL1がすべて00です。Aは行6・列0のビット4ですが、未検出時は生値0を押下と扱わず、表示も000に保ちます。表は起動時の一度の採取結果です。
KEYBOARD-DEFAULT — このKUROSAKI実行ではDEVICE DETECT、ROW 6 COL 0、A KEY - GUARDEDがすべて000、9行のCOL0/COL1がすべて00です。Aは行6・列0のビット4ですが、未検出時は生値0を押下と扱わず、表示も000に保ちます。表は起動時の一度の採取結果です。

使用例の完全なプログラム

ビルドコマンド

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\peripheral_keyboard.c -I .\kitaqfc\lib -I .\kitaq-docs\samples -o .\out\peripheral_keyboard.nes --mapper=nrom --nes-chr=.\kitaq-docs\samples\font.chr --no-cache --no-disasm
.\kurosaki\kurosaki.exe run .\out\peripheral_keyboard.nes --frames 120 --png .\out\peripheral_keyboard.png
実装とソース

kitaqfc/lib/intrinsics.h:261

宣言の注記(原文)
Write 18 raw masked samples (4017 & 1E), two per row, to caller storage; reset 4016 afterward.

kitaqfc/kitaqfc/CodeGenerator.cs:7773

csharp
EmitHelperStart("__fkb_scan");
            EmitAsm("LDA", Imm(0x05));
            EmitAsm("STA", Mem(0x4016));
            EmitKeyboardDelayShort();
            EmitAsm("LDY", Imm(0));
            EmitAsm("LDX", Imm(9));
            string rowLoop = NewGeneratedLabel("fkb_scan_row");
            _assembly.Add(Expr.Make(Tag.Label, rowLoop));
            EmitAsm("LDA", Imm(0x04));
            EmitAsm("STA", Mem(0x4016));
            EmitKeyboardDelayLong();
            EmitAsm("LDA", Mem(0x4017));
            EmitAsm("AND", Imm(0x1E));
            EmitAsm("STA", IndY(CallArgBase));
            EmitAsm("INY");
            EmitAsm("LDA", Imm(0x06));
            EmitAsm("STA", Mem(0x4016));
            EmitKeyboardDelayLong();
            EmitAsm("LDA", Mem(0x4017));
            EmitAsm("AND", Imm(0x1E));
            EmitAsm("STA", IndY(CallArgBase));
            EmitAsm("INY");
            EmitAsm("DEX");
            EmitAsm("BNE", Rel(rowLoop));
            EmitAsm("LDA", Imm(0));
            EmitAsm("STA", Mem(0x4016));
            EmitAsm("RTS");
__irq_disable

SEI命令でCPUの状態レジスターのIビットを立て、通常のIRQ要求をマスクします。IRQによる割り込みを避けたい処理区間で使います。NMIの停止、APUの消音、IRQ発生源の確認応答、元のマスク状態の保存は行いません。

書式

c
void __irq_disable(void);

戻り値

ありません(void)。

使用条件と制限

Iビットが制御するのは通常のIRQだけで、処理区間の途中でもNMIは発生します。IRQを禁止してもデバイスの保留要求は解除されず、NMIと共有するデータも保護されません。発生源とハンドラーは別途準備してください。このサンプルではIRQを発生させる周辺機器は試験しません。

NESdev: NMI

NESdev: CPU interrupts

使用例

c
__irq_disable();
disabled=(__irq_save()&4)>>2; // The CPU I bit must be 1.

呼び出し部分のコード例です。完全なプログラムに組み込む際は、記載した初期化とバッファの条件を満たしてください。

期待する結果

CPUのIRQマスクとPPUのNMI制御の違いを調べるプログラムです。黒背景の青文字で、IRQ DISABLED Iは001、IRQ ENABLED Iは000、SAVED SECTION Iは001、RESTORED Iは000になります。表示するのは状態バイト全体ではなく、取り出したIビットです。FAILED CHECKSは000が期待値です。

256回待つ実演が終わるように、エミュレータを360フレーム以上実行してください。IRQ禁止中でもNMI DELTA 3 WAITSは003になります。NMI DELTA 256 WAITSは1バイトが一周するため000、NMI OFF DELTAも000です。runtime.cをリンクせず標準ハンドラーを使います。別の状態検証では、状態ビットの復元やNMI無効時・空の独自ハンドラー使用時の待機を含む21ケースを確認しています。KUROSAKIのNROM環境での検証で、実機は未検証です。

__irq_disable: NROM
NROM

使用例の完全なプログラム

ビルドコマンド

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\interrupt_intrinsics.c -I .\kitaqfc\lib -I .\kitaq-docs\samples --nes-chr=.\kitaq-docs\samples\font.chr -o .\out\interrupt_intrinsics.nes --no-cache --no-disasm
実装とソース

kitaqfc/lib/intrinsics.h:176

宣言の注記(原文)
Emit SEI to mask CPU IRQs; NMI is unaffected.

kitaqfc/kitaqfc/CodeGenerator.cs:4733

csharp
case "__irq_disable": if (!Need(0)) return true; EmitAsm("SEI"); return true;
__irq_enable

CLI命令でCPUの状態レジスターのIビットを落とし、通常のIRQ要求を受け付けられる状態にします。マッパーやAPUの割り込み発生源を設定する処理は行いません。発生源を有効にする前に、対応するハンドラーと割り込み要求を解除する処理を用意してください。

書式

c
void __irq_enable(void);

戻り値

ありません(void)。

使用条件と制限

Iビットが制御するのは通常のIRQだけで、処理区間の途中でもNMIは発生します。IRQを禁止してもデバイスの保留要求は解除されず、NMIと共有するデータも保護されません。発生源とハンドラーは別途準備してください。このサンプルではIRQを発生させる周辺機器は試験しません。

NESdev: NMI

NESdev: CPU interrupts

使用例

c
__irq_enable();
enabled=(__irq_save()&4)>>2; // Save returns the old I bit: 0, then masks IRQ.

呼び出し部分のコード例です。完全なプログラムに組み込む際は、記載した初期化とバッファの条件を満たしてください。

期待する結果

CPUのIRQマスクとPPUのNMI制御の違いを調べるプログラムです。黒背景の青文字で、IRQ DISABLED Iは001、IRQ ENABLED Iは000、SAVED SECTION Iは001、RESTORED Iは000になります。表示するのは状態バイト全体ではなく、取り出したIビットです。FAILED CHECKSは000が期待値です。

256回待つ実演が終わるように、エミュレータを360フレーム以上実行してください。IRQ禁止中でもNMI DELTA 3 WAITSは003になります。NMI DELTA 256 WAITSは1バイトが一周するため000、NMI OFF DELTAも000です。runtime.cをリンクせず標準ハンドラーを使います。別の状態検証では、状態ビットの復元やNMI無効時・空の独自ハンドラー使用時の待機を含む21ケースを確認しています。KUROSAKIのNROM環境での検証で、実機は未検証です。

__irq_enable: NROM
NROM

使用例の完全なプログラム

ビルドコマンド

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\interrupt_intrinsics.c -I .\kitaqfc\lib -I .\kitaq-docs\samples --nes-chr=.\kitaq-docs\samples\font.chr -o .\out\interrupt_intrinsics.nes --no-cache --no-disasm
実装とソース

kitaqfc/lib/intrinsics.h:178

宣言の注記(原文)
Emit CLI to unmask CPU IRQs; the caller must have a valid interrupt handler.

kitaqfc/kitaqfc/CodeGenerator.cs:4734

csharp
case "__irq_enable": if (!Need(0)) return true; EmitAsm("CLI"); return true;
                // Return the saved processor status in A after masking IRQs; restore consumes a status byte, not a Boolean.
__irq_restore

引数の状態バイトを評価してPHA命令で積み、PLP命令でCPUの状態を復元します。常にIRQを許可するのではなく、保存したIRQマスクと演算フラグを戻します。その後に実行する通常のC式で演算フラグが変わることはあります。

書式

c
void __irq_restore(u8 state);

引数

state

__irq_saveが保存する8ビットのCPU状態値です。__irq_restoreへ渡すため全ビットを保持し、真偽値に変換しないでください。ビット2がIで、1はIRQ禁止、0は許可です。PHPが積む形式ではビット4と5も立ちます。

戻り値

ありません(void)。

使用条件と制限

Iビットが制御するのは通常のIRQだけで、処理区間の途中でもNMIは発生します。IRQを禁止してもデバイスの保留要求は解除されず、NMIと共有するデータも保護されません。発生源とハンドラーは別途準備してください。このサンプルではIRQを発生させる周辺機器は試験しません。

NESdev: NMI

NESdev: CPU interrupts

使用例

c
__irq_restore(saved_state); // Restore the caller's original IRQ mask.
restored=(__irq_save()&4)>>2; // Original I was 0; this probe masks it again.

呼び出し部分のコード例です。完全なプログラムに組み込む際は、記載した初期化とバッファの条件を満たしてください。

期待する結果

CPUのIRQマスクとPPUのNMI制御の違いを調べるプログラムです。黒背景の青文字で、IRQ DISABLED Iは001、IRQ ENABLED Iは000、SAVED SECTION Iは001、RESTORED Iは000になります。表示するのは状態バイト全体ではなく、取り出したIビットです。FAILED CHECKSは000が期待値です。

256回待つ実演が終わるように、エミュレータを360フレーム以上実行してください。IRQ禁止中でもNMI DELTA 3 WAITSは003になります。NMI DELTA 256 WAITSは1バイトが一周するため000、NMI OFF DELTAも000です。runtime.cをリンクせず標準ハンドラーを使います。別の状態検証では、状態ビットの復元やNMI無効時・空の独自ハンドラー使用時の待機を含む21ケースを確認しています。KUROSAKIのNROM環境での検証で、実機は未検証です。

__irq_restore: NROM
NROM

使用例の完全なプログラム

ビルドコマンド

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\interrupt_intrinsics.c -I .\kitaqfc\lib -I .\kitaq-docs\samples --nes-chr=.\kitaq-docs\samples\font.chr -o .\out\interrupt_intrinsics.nes --no-cache --no-disasm
実装とソース

kitaqfc/lib/intrinsics.h:182

宣言の注記(原文)
Restore processor status with PLP, including arithmetic flags and the saved IRQ mask.

kitaqfc/kitaqfc/CodeGenerator.cs:4737

csharp
case "__irq_restore": if (!Need(1)) return true; Load1(args[0]); EmitAsm("PHA"); EmitAsm("PLP"); return true;
__irq_save

PHP命令でCPUの状態バイトを保存した後、SEI命令でIRQをマスクし、保存したバイトをPLA命令で取り出して返します。保存値のIビットはマスクする前の状態です。__irq_restoreと組み合わせると、処理区間を入れ子にしても、元からIRQ禁止だった状態を維持できます。

書式

c
u8 __irq_save(void);

戻り値

__irq_saveが保存する8ビットのCPU状態値です。__irq_restoreへ渡すため全ビットを保持し、真偽値に変換しないでください。ビット2がIで、1はIRQ禁止、0は許可です。PHPが積む形式ではビット4と5も立ちます。

使用条件と制限

Iビットが制御するのは通常のIRQだけで、処理区間の途中でもNMIは発生します。IRQを禁止してもデバイスの保留要求は解除されず、NMIと共有するデータも保護されません。発生源とハンドラーは別途準備してください。このサンプルではIRQを発生させる周辺機器は試験しません。

NESdev: NMI

NESdev: CPU interrupts

使用例

c
saved_state=__irq_save(); // Save all processor-status bits, then set I.
inside=(__irq_save()&4)>>2; // IRQ remains masked inside this section.

呼び出し部分のコード例です。完全なプログラムに組み込む際は、記載した初期化とバッファの条件を満たしてください。

期待する結果

CPUのIRQマスクとPPUのNMI制御の違いを調べるプログラムです。黒背景の青文字で、IRQ DISABLED Iは001、IRQ ENABLED Iは000、SAVED SECTION Iは001、RESTORED Iは000になります。表示するのは状態バイト全体ではなく、取り出したIビットです。FAILED CHECKSは000が期待値です。

256回待つ実演が終わるように、エミュレータを360フレーム以上実行してください。IRQ禁止中でもNMI DELTA 3 WAITSは003になります。NMI DELTA 256 WAITSは1バイトが一周するため000、NMI OFF DELTAも000です。runtime.cをリンクせず標準ハンドラーを使います。別の状態検証では、状態ビットの復元やNMI無効時・空の独自ハンドラー使用時の待機を含む21ケースを確認しています。KUROSAKIのNROM環境での検証で、実機は未検証です。

__irq_save: NROM
NROM

使用例の完全なプログラム

ビルドコマンド

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\interrupt_intrinsics.c -I .\kitaqfc\lib -I .\kitaq-docs\samples --nes-chr=.\kitaq-docs\samples\font.chr -o .\out\interrupt_intrinsics.nes --no-cache --no-disasm
実装とソース

kitaqfc/lib/intrinsics.h:180

宣言の注記(原文)
Return the processor-status byte captured before SEI. Pass that byte to __irq_restore.

kitaqfc/kitaqfc/CodeGenerator.cs:4736

csharp
case "__irq_save": if (!Need(0)) return true; EmitAsm("PHP"); EmitAsm("SEI"); EmitAsm("PLA"); return true;
__irq_scanline_set

__mapper_irq_setと同じMMC3のラッチ設定・再ロード要求です。関数名のscanlineは絶対的な画面の行番号を保証しません。

書式

c
void __irq_scanline_set(u8 line);

引数

line

0~255の8ビットカウンターラッチ値です。画面上の絶対Y座標ではありません。再ロードのタイミングとPPUのA12変化によって発生位置が決まります。0も書き込めますが、割り込み停止の意味ではありません。

戻り値

戻り値はありません。

使用条件と制限

組み込み関数の宣言はintrinsics.hにあります。nes_mapper_irq_*のマクロにはnes_game.hを読み込みます。マッパーはビルド時の--mapperで選びます。

MMC3専用です。__nes_irqハンドラー、PPUのA12変化、マッパー側の有効化、CPU側の__irq_enable()が必要です。サンプルは背景を$0000、スプライトを$1000のパターンテーブルにして描画を有効にしています。実機での特定ラインへの同期精度は、このサンプルの検証範囲に含みません。

MMC3のACKと無効化はどちらも$E000への書き込みです。要求を解除すると同時に、次の要求も無効になります。繰り返し使う場合は必要なタイミングで__mapper_irq_enable()を呼んでください。CPUの割り込みマスクやNMIの有効状態は変更しません。

独自ハンドラーの末尾にはコンパイラがRTIを生成しますが、A/X/Yや共有一時領域はハンドラー側で必要な分を保存・復元します。サンプルのハンドラーはA/X/Yを保存し、バイトの加算と引数なしのACKだけを行います。別のC関数を追加する場合は、その関数が触る共有領域も確認してください。

使用例

c
irq_phase=2;
    __irq_scanline_set(7); __mapper_irq_enable();
    for (i=0;i<6;i++) { m_wait(); }
    mapper_result[17]=irq_count;

呼び出し部分のコード例です。完全なプログラムに組み込む際は、記載した初期化とバッファの条件を満たしてください。

期待する結果

MMC3のサンプルはCHRのマーカー値19/20、21/22、23/24を表示します。CIRAMは縦配置でA B A B、横配置でA A B Bになります。IRQ回数は01→02→03→03です。最初の3回は明示的な再有効化で1回ずつ増え、無効化後は増えません。各段階で6フレーム待って確認します。

__irq_scanline_set: MMC3-DEFAULT
MMC3-DEFAULT

使用例の完全なプログラム

ビルドコマンド

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\mapper_controls.c -I .\kitaqfc\lib -I .\kitaq-docs\samples -o .\out\mapper_controls.nes --mapper=mmc3 --nes-chr=.\kitaq-docs\samples\mapper_banks.chr --no-cache --no-disasm
実装とソース

kitaqfc/lib/intrinsics.h:324

宣言の注記(原文)
Write the MMC3 IRQ latch and reload registers. This does not enable IRQ delivery.
__joypad2p_voice

__joypad2p_voiceは__mic_read2pのヘルパーを呼ぶ別名です。どちらも同じデジタルマイクビットを読みます。名前の「voice」は音声処理を意味しません。別々に呼ぶとそれぞれの時点で読み取るため、その間に信号が変化すれば値が異なることがあります。

ファミコンの2番目のコントローラーのマイクに対応するデジタル信号を、$4016のビット2から読みます。レジスターを1回読み、0x04でマスクして、反転せず0か1に正規化します。コントローラーのラッチ、音声の録音、音量測定、音声認識は行いません。

書式

c
u8 __joypad2p_voice(void);

戻り値

$4016 & 0x04が0以外なら正確に1、それ以外は0です。読み取ったデジタルレベルであり、ボタンマスク、振幅、周波数、発話した単語数ではありません。

使用条件と制限

マイクは2番目のコントローラーにありますが、このヘルパーが読むのは$4017ではなく$4016です。ストローブが低いときのレジスター読み取りはコントローラーのシリアル読み取り列も進めることがあるため、別のコントローラーの8ビット読み取りループの途中にマイクの読み取りを挟まないでください。入力履歴の保存済み状態は更新しません。

公開版KUROSAKIのコントローラーモデルが返すのはD0と固定のビット6だけです。そのため通常のボタンを押していても、同エミュレーターではD1とマイクのビットは常に0になります。付属の検証で確認したのは、ビルド、通常ボタン入力、この0の信号表示です。周辺機器からの0以外の信号、電気的タイミング、実機での互換性は未確認です。ここで0が表示されても、接続機器が動作する証明や、実機でも読み取れないという証明にはなりません。

使用例

c
voice=__joypad2p_voice();

呼び出し部分のコード例です。完全なプログラムに組み込む際は、記載した初期化とバッファの条件を満たしてください。

期待する結果

通常入力のD0、拡張入力のD1、マイクのデジタルレベルを比較するプログラムです。状態が取得されるまでコントローラー1のAとコントローラー2のBを押します。NORMAL P1とNORMAL P2は001、002になります。付属のKUROSAKI実行結果では、D1 PORT 1、D1 PORT 2、EXP ALIAS 1、EXP ALIAS 2、MIC LEVEL、MIC DIRECTはすべて000です。対応する信号線がエミュレートされていないためです。取得後の表示は固定されます。適切な周辺機器と信号線の実装がある環境では実際の入力で値が変わるため、この0を期待値とせず、上記のビット配置に従って読み取ってください。

公開版KUROSAKIのコントローラーモデルが返すのはD0と固定のビット6だけです。そのため通常のボタンを押していても、同エミュレーターではD1とマイクのビットは常に0になります。付属の検証で確認したのは、ビルド、通常ボタン入力、この0の信号表示です。周辺機器からの0以外の信号、電気的タイミング、実機での互換性は未確認です。ここで0が表示されても、接続機器が動作する証明や、実機でも読み取れないという証明にはなりません。

__joypad2p_voice: NROM
NROM

使用例の完全なプログラム

ビルドコマンド

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\expansion_input.c -I .\kitaq-docs\samples -I .\kitaqfc\lib -o .\out\expansion_input.nes --mapper=nrom --nes-chr=.\kitaq-docs\samples\font.chr --no-cache --no-disasm
実装とソース

kitaqfc/lib/intrinsics.h:231

宣言の注記(原文)
Return controller-port 4016 bit 2 normalized to 0 or 1; this is a microphone level bit, not PCM audio.
__mac16

符号なし積和を計算します。

書式

c
u16 __mac16(u16 acc, u16 a, u8 b);

引数

acc

加算する16ビットの累積値。

a

符号なし16ビット値0~65535。

b

符号なし8ビット係数0~255。

戻り値

acc+a*bの下位16ビットです。

使用条件と制限

FCでは名前にq8_8/q1_7があっても生の整数積を使い、固定小数点の縮小シフトは行いません。Q1.7の3関数は対応する符号付き関数への別名です。単位変換は呼び出し側で行います。符号付き結果もu16のビット列として返します。

使用例

c
// Compare literal coefficients with values read from RAM.
result[8]=__mac16(65500,513,127);
v0=65500;v1=513;v2=127;
result[9]=__mac16(v0,v1,v2);

呼び出し部分のコード例です。完全なプログラムに組み込む際は、記載した初期化とバッファの条件を満たしてください。

期待する結果

固定値とRAM変数で同じ計算を行う例です。画面には代表値を符号なし16ビットで表示し、負数は2の補数の値になります。GBは固定小数点の縮小を行い、FCは整数積を返すため、同じ入力でも両機種の結果は異なります。

この抜粋で保存する16ビット値は result[8]=65115, result[9]=65115 です。固定値とRAM変数を使った計算は、それぞれ同じ値になります。画面は代表値を表示し、全結果はresult配列に保存します。

__mac16: NROM
NROM

使用例の完全なプログラム

ビルドコマンド

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\batch3_math.c -I .\kitaqfc\lib -I .\kitaq-docs\samples -o .\out\batch3_math.nes --no-cache --no-disasm --mapper=nrom --nes-chr=.\kitaq-docs\samples\font.chr
実装とソース

kitaqfc/lib/intrinsics.h:55

宣言の注記(原文)
Return acc+a*b modulo 65536 using unsigned multiplication.

kitaqfc/kitaqfc/CodeGenerator.cs:4650

csharp
case "__mac16": return EmitKitaqfcHelperCall(funcName, args, ctx, new[] {2,2,1}, funcName);

void EmitHelperMath()
        {
            // Consume eight multiplier bits and accumulate the low product word in shared temporaries, returning A/X.
            EmitHelperStart("__mul16x8");
            EmitAsm("LDA", Imm(0));
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp1Address));
            EmitAsm("LDX", Imm(8));
            string loop = NewGeneratedLabel("mul16x8_loop");
            string skip = NewGeneratedLabel("mul16x8_skip_add");
            _assembly.Add(Expr.Make(Tag.Label, loop));
            EmitAsm("LSR", Mem(CallArgBase + 2));
            EmitAsm("BCC", Rel(skip));
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("CLC");
            EmitAsm("ADC", Mem(CallArgBase));
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp1Address));
            EmitAsm("ADC", Mem(CallArgBase + 1));
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp1Address));
            _assembly.Add(Expr.Make(Tag.Label, skip));
            EmitAsm("ASL", Mem(CallArgBase));
            EmitAsm("ROL", Mem(CallArgBase + 1));
            EmitAsm("DEX");
            EmitAsm("BNE", Rel(loop));
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("LDX", Mem(_runtimeIntrinsicTmp1Address));
            EmitAsm("RTS");

            // Zero-extend the byte multiplicand, reuse word multiplication and return its high byte.
            EmitHelperStart("__mul8x8_hi");
            EmitAsm("LDA", Mem(CallArgBase + 1));
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp2Address));
            EmitAsm("LDA", Imm(0));
            EmitAsm("STA", Mem(CallArgBase + 1));
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp2Address));
            EmitAsm("STA", Mem(CallArgBase + 2));
            EmitAsm("JSR", Abs("__mul16x8"));
            EmitAsm("TXA");
            EmitAsm("RTS");

            // Convert signed operands to magnitudes, multiply, then restore the product sign modulo 65536.
            EmitHelperStart("__smul16x8");
            EmitAsm("LDA", Imm(0));
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp2Address));
            string aPos = NewGeneratedLabel("smul_a_positive");
            EmitAsm("LDA", Mem(CallArgBase + 1));
            EmitAsm("AND", Imm(0x80));
            EmitAsm("BEQ", Rel(aPos));
            EmitAsm("LDA", Mem(CallArgBase)); EmitAsm("EOR", Imm(0xFF)); EmitAsm("CLC"); EmitAsm("ADC", Imm(1)); EmitAsm("STA", Mem(CallArgBase));
            EmitAsm("LDA", Mem(CallArgBase + 1)); EmitAsm("EOR", Imm(0xFF)); EmitAsm("ADC", Imm(0)); EmitAsm("STA", Mem(CallArgBase + 1));
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp2Address)); EmitAsm("EOR", Imm(1)); EmitAsm("STA", Mem(_runtimeIntrinsicTmp2Address));
            _assembly.Add(Expr.Make(Tag.Label, aPos));
            string bPos = NewGeneratedLabel("smul_b_positive");
            EmitAsm("LDA", Mem(CallArgBase + 2));
            EmitAsm("AND", Imm(0x80));
            EmitAsm("BEQ", Rel(bPos));
            EmitAsm("LDA", Mem(CallArgBase + 2)); EmitAsm("EOR", Imm(0xFF)); EmitAsm("CLC"); EmitAsm("ADC", Imm(1)); EmitAsm("STA", Mem(CallArgBase + 2));
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp2Address)); EmitAsm("EOR", Imm(1)); EmitAsm("STA", Mem(_runtimeIntrinsicTmp2Address));
            _assembly.Add(Expr.Make(Tag.Label, bPos));
            EmitAsm("JSR", Abs("__mul16x8"));
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("STX", Mem(_runtimeIntrinsicTmp1Address));
            string sdone = NewGeneratedLabel("smul_done");
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp2Address));
            EmitAsm("BEQ", Rel(sdone));
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp0Address)); EmitAsm("EOR", Imm(0xFF)); EmitAsm("CLC"); EmitAsm("ADC", Imm(1)); EmitAsm("STA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp1Address)); EmitAsm("EOR", Imm(0xFF)); EmitAsm("ADC", Imm(0)); EmitAsm("STA", Mem(_runtimeIntrinsicTmp1Address));
            _assembly.Add(Expr.Make(Tag.Label, sdone));
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("LDX", Mem(_runtimeIntrinsicTmp1Address));
            EmitAsm("RTS");

            EmitMacHelper("__mac16", "__mul16x8");
            EmitMacHelper("__smac16", "__smul16x8");
            EmitDot2Helper("__dot2_q8_8", "__mul16x8");
            EmitDot3Helper("__dot3_q8_8", "__mul16x8");
            EmitDot2Helper("__sdot2_q8_8", "__smul16x8");
            EmitDot3Helper("__sdot3_q8_8", "__smul16x8");
        }
__manhattan

2点のX差の絶対値とY差の絶対値を足します。

書式

c
u8 __manhattan(u8 x1, u8 y1, u8 x2, u8 y2);

引数

x1

点1のX座標0~255。

y1

点1のY座標0~255。

x2

点2のX座標0~255。

y2

点2のY座標0~255。

戻り値

|x1−x2|+|y1−y2|の下位8ビット。255を超える距離は周回します。

使用例

c
result[5]=__manhattan(2,3,9,11);result[6]=__manhattan(0,0,255,255);

呼び出し部分のコード例です。完全なプログラムに組み込む際は、記載した初期化とバッファの条件を満たしてください。

期待する結果

矩形の内部・端・外側・幅0を試します。INSIDE=1、RIGHT EDGE=0、LEFT EDGE=0、HIGH X=1です。距離15、距離の周回254、配列位置64と65280を確認します。

__manhattan: NROM
NROM

使用例の完全なプログラム

ビルドコマンド

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\batch3_coordinates.c -I .\kitaqfc\lib -I .\kitaq-docs\samples -o .\out\batch3_coordinates.nes --no-cache --no-disasm --mapper=nrom --nes-chr=.\kitaq-docs\samples\font.chr
実装とソース

kitaqfc/lib/intrinsics.h:33

宣言の注記(原文)
Return abs(x1-x2)+abs(y1-y2) modulo 256; large distances are not saturated.

kitaqfc/kitaqfc/CodeGenerator.cs:4629

csharp
case "__manhattan":
                    return EmitKitaqfcHelperCall(funcName, args, ctx, new[] {1,1,1,1}, "__manhattan");

void EmitHelperGeometryAndRng()
        {
            EmitHelperStart("__xy_in_rect");
            string xyFalse = NewGeneratedLabel("xy_in_rect_false");
            string xyDone = NewGeneratedLabel("xy_in_rect_done");
            // return (x-rx < rw) && (y-ry < rh), using unsigned subtract to avoid rx+rw overflow.
            EmitAsm("LDA", Mem(CallArgBase));
            EmitAsm("SEC");
            EmitAsm("SBC", Mem(CallArgBase + 2));
            EmitAsm("BCC", Rel(xyFalse));
            EmitAsm("CMP", Mem(CallArgBase + 4));
            EmitAsm("BCS", Rel(xyFalse));
            EmitAsm("LDA", Mem(CallArgBase + 1));
            EmitAsm("SEC");
            EmitAsm("SBC", Mem(CallArgBase + 3));
            EmitAsm("BCC", Rel(xyFalse));
            EmitAsm("CMP", Mem(CallArgBase + 5));
            EmitAsm("BCS", Rel(xyFalse));
            EmitAsm("LDA", Imm(1));
            EmitAsm("JMP", Abs(xyDone));
            _assembly.Add(Expr.Make(Tag.Label, xyFalse));
            EmitAsm("LDA", Imm(0));
            _assembly.Add(Expr.Make(Tag.Label, xyDone));
            EmitAsm("RTS");

            EmitHelperStart("__manhattan");
            // dx = abs(x1-x2)
            EmitAsm("LDA", Mem(CallArgBase));
            EmitAsm("SEC");
            EmitAsm("SBC", Mem(CallArgBase + 2));
            string mdxOk = NewGeneratedLabel("manhattan_dx_ok");
            EmitAsm("BCS", Rel(mdxOk));
            EmitAsm("EOR", Imm(0xFF));
            EmitAsm("CLC");
            EmitAsm("ADC", Imm(1));
            _assembly.Add(Expr.Make(Tag.Label, mdxOk));
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp0Address));
            // dy = abs(y1-y2), then add dx.
            EmitAsm("LDA", Mem(CallArgBase + 1));
            EmitAsm("SEC");
            EmitAsm("SBC", Mem(CallArgBase + 3));
            string mdyOk = NewGeneratedLabel("manhattan_dy_ok");
            EmitAsm("BCS", Rel(mdyOk));
            EmitAsm("EOR", Imm(0xFF));
            EmitAsm("CLC");
            EmitAsm("ADC", Imm(1));
            _assembly.Add(Expr.Make(Tag.Label, mdyOk));
            EmitAsm("CLC");
            EmitAsm("ADC", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("RTS");

            // Save X-coordinate separately while multiplying width by Y, then add X with carry into the high byte.
            EmitHelperStart("__map_index");
            // args: x, y, width. Return y * width + x as u16.
            EmitAsm("LDA", Mem(CallArgBase));      // x
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp2Address));
            EmitAsm("LDA", Mem(CallArgBase + 1));  // y
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp1Address));
            EmitAsm("LDA", Mem(CallArgBase + 2));  // width as u16 multiplicand
            EmitAsm("STA", Mem(CallArgBase));
            EmitAsm("LDA", Imm(0));
            EmitAsm("STA", Mem(CallArgBase + 1));
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp1Address));
            EmitAsm("STA", Mem(CallArgBase + 2));
            EmitAsm("JSR", Abs("__mul16x8"));
            EmitAsm("CLC");
            EmitAsm("ADC", Mem(_runtimeIntrinsicTmp2Address));
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("TXA");
            EmitAsm("ADC", Imm(0));
            EmitAsm("TAX");
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("RTS");

            // Replace both bytes of the persistent generator state with the supplied seed.
            EmitHelperStart("__rng_seed");
            EmitAsm("LDA", Mem(CallArgBase));
            EmitAsm("STA", Mem(_runtimeRngLoAddress));
            EmitAsm("LDA", Mem(CallArgBase + 1));
            EmitAsm("STA", Mem(_runtimeRngHiAddress));
            EmitAsm("RTS");

            EmitHelperStart("__rng8");
            // 16-bit Galois LFSR: if low bit set, shift right then xor $B400.
            EmitAsm("LDA", Mem(_runtimeRngLoAddress));
            EmitAsm("ORA", Mem(_runtimeRngHiAddress));
            // Replace the all-zero LFSR state with a deterministic nonzero seed before advancing.
            string seeded = NewGeneratedLabel("rng_seeded");
            EmitAsm("BNE", Rel(seeded));
            EmitAsm("LDA", Imm(0x5A));
            EmitAsm("STA", Mem(_runtimeRngLoAddress));
            EmitAsm("LDA", Imm(0xA5));
            EmitAsm("STA", Mem(_runtimeRngHiAddress));
            _assembly.Add(Expr.Make(Tag.Label, seeded));
            EmitAsm("LDA", Mem(_runtimeRngLoAddress));
            EmitAsm("AND", Imm(1));
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("LSR", Mem(_runtimeRngHiAddress));
            EmitAsm("ROR", Mem(_runtimeRngLoAddress));
            string rngNoXor = NewGeneratedLabel("rng_no_xor");
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("BEQ", Rel(rngNoXor));
            EmitAsm("LDA", Mem(_runtimeRngHiAddress));
            EmitAsm("EOR", Imm(0xB4));
            EmitAsm("STA", Mem(_runtimeRngHiAddress));
            _assembly.Add(Expr.Make(Tag.Label, rngNoXor));
            EmitAsm("LDA", Mem(_runtimeRngLoAddress));
            EmitAsm("EOR", Mem(_runtimeRngHiAddress));
            EmitAsm("RTS");
        }
__map_index

行優先配列のセル位置を1次元インデックスへ変換します。

書式

c
u16 __map_index(u8 x, u8 y, u8 width);

引数

x

列0~255。通常はwidth未満です。

y

行0~255。

width

1行のセル数0~255。

戻り値

y*width+xをu16で返します。座標の範囲検査はありません。

使用例

c
result[7]=__map_index(4,3,20);result[8]=__map_index(255,255,255);

呼び出し部分のコード例です。完全なプログラムに組み込む際は、記載した初期化とバッファの条件を満たしてください。

期待する結果

矩形の内部・端・外側・幅0を試します。INSIDE=1、RIGHT EDGE=0、LEFT EDGE=0、HIGH X=1です。距離15、距離の周回254、配列位置64と65280を確認します。

__map_index: NROM
NROM

使用例の完全なプログラム

ビルドコマンド

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\batch3_coordinates.c -I .\kitaqfc\lib -I .\kitaq-docs\samples -o .\out\batch3_coordinates.nes --no-cache --no-disasm --mapper=nrom --nes-chr=.\kitaq-docs\samples\font.chr
実装とソース

kitaqfc/lib/intrinsics.h:35

宣言の注記(原文)
Return y*width+x as a word without checking coordinates against map dimensions.

kitaqfc/kitaqfc/CodeGenerator.cs:4631

csharp
case "__map_index":
                    if (TryEmitMapIndexInline(args, ctx)) return true;
                    return EmitKitaqfcHelperCall(funcName, args, ctx, new[] {1,1,1}, "__map_index");

void EmitHelperGeometryAndRng()
        {
            EmitHelperStart("__xy_in_rect");
            string xyFalse = NewGeneratedLabel("xy_in_rect_false");
            string xyDone = NewGeneratedLabel("xy_in_rect_done");
            // return (x-rx < rw) && (y-ry < rh), using unsigned subtract to avoid rx+rw overflow.
            EmitAsm("LDA", Mem(CallArgBase));
            EmitAsm("SEC");
            EmitAsm("SBC", Mem(CallArgBase + 2));
            EmitAsm("BCC", Rel(xyFalse));
            EmitAsm("CMP", Mem(CallArgBase + 4));
            EmitAsm("BCS", Rel(xyFalse));
            EmitAsm("LDA", Mem(CallArgBase + 1));
            EmitAsm("SEC");
            EmitAsm("SBC", Mem(CallArgBase + 3));
            EmitAsm("BCC", Rel(xyFalse));
            EmitAsm("CMP", Mem(CallArgBase + 5));
            EmitAsm("BCS", Rel(xyFalse));
            EmitAsm("LDA", Imm(1));
            EmitAsm("JMP", Abs(xyDone));
            _assembly.Add(Expr.Make(Tag.Label, xyFalse));
            EmitAsm("LDA", Imm(0));
            _assembly.Add(Expr.Make(Tag.Label, xyDone));
            EmitAsm("RTS");

            EmitHelperStart("__manhattan");
            // dx = abs(x1-x2)
            EmitAsm("LDA", Mem(CallArgBase));
            EmitAsm("SEC");
            EmitAsm("SBC", Mem(CallArgBase + 2));
            string mdxOk = NewGeneratedLabel("manhattan_dx_ok");
            EmitAsm("BCS", Rel(mdxOk));
            EmitAsm("EOR", Imm(0xFF));
            EmitAsm("CLC");
            EmitAsm("ADC", Imm(1));
            _assembly.Add(Expr.Make(Tag.Label, mdxOk));
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp0Address));
            // dy = abs(y1-y2), then add dx.
            EmitAsm("LDA", Mem(CallArgBase + 1));
            EmitAsm("SEC");
            EmitAsm("SBC", Mem(CallArgBase + 3));
            string mdyOk = NewGeneratedLabel("manhattan_dy_ok");
            EmitAsm("BCS", Rel(mdyOk));
            EmitAsm("EOR", Imm(0xFF));
            EmitAsm("CLC");
            EmitAsm("ADC", Imm(1));
            _assembly.Add(Expr.Make(Tag.Label, mdyOk));
            EmitAsm("CLC");
            EmitAsm("ADC", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("RTS");

            // Save X-coordinate separately while multiplying width by Y, then add X with carry into the high byte.
            EmitHelperStart("__map_index");
            // args: x, y, width. Return y * width + x as u16.
            EmitAsm("LDA", Mem(CallArgBase));      // x
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp2Address));
            EmitAsm("LDA", Mem(CallArgBase + 1));  // y
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp1Address));
            EmitAsm("LDA", Mem(CallArgBase + 2));  // width as u16 multiplicand
            EmitAsm("STA", Mem(CallArgBase));
            EmitAsm("LDA", Imm(0));
            EmitAsm("STA", Mem(CallArgBase + 1));
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp1Address));
            EmitAsm("STA", Mem(CallArgBase + 2));
            EmitAsm("JSR", Abs("__mul16x8"));
            EmitAsm("CLC");
            EmitAsm("ADC", Mem(_runtimeIntrinsicTmp2Address));
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("TXA");
            EmitAsm("ADC", Imm(0));
            EmitAsm("TAX");
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("RTS");

            // Replace both bytes of the persistent generator state with the supplied seed.
            EmitHelperStart("__rng_seed");
            EmitAsm("LDA", Mem(CallArgBase));
            EmitAsm("STA", Mem(_runtimeRngLoAddress));
            EmitAsm("LDA", Mem(CallArgBase + 1));
            EmitAsm("STA", Mem(_runtimeRngHiAddress));
            EmitAsm("RTS");

            EmitHelperStart("__rng8");
            // 16-bit Galois LFSR: if low bit set, shift right then xor $B400.
            EmitAsm("LDA", Mem(_runtimeRngLoAddress));
            EmitAsm("ORA", Mem(_runtimeRngHiAddress));
            // Replace the all-zero LFSR state with a deterministic nonzero seed before advancing.
            string seeded = NewGeneratedLabel("rng_seeded");
            EmitAsm("BNE", Rel(seeded));
            EmitAsm("LDA", Imm(0x5A));
            EmitAsm("STA", Mem(_runtimeRngLoAddress));
            EmitAsm("LDA", Imm(0xA5));
            EmitAsm("STA", Mem(_runtimeRngHiAddress));
            _assembly.Add(Expr.Make(Tag.Label, seeded));
            EmitAsm("LDA", Mem(_runtimeRngLoAddress));
            EmitAsm("AND", Imm(1));
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("LSR", Mem(_runtimeRngHiAddress));
            EmitAsm("ROR", Mem(_runtimeRngLoAddress));
            string rngNoXor = NewGeneratedLabel("rng_no_xor");
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("BEQ", Rel(rngNoXor));
            EmitAsm("LDA", Mem(_runtimeRngHiAddress));
            EmitAsm("EOR", Imm(0xB4));
            EmitAsm("STA", Mem(_runtimeRngHiAddress));
            _assembly.Add(Expr.Make(Tag.Label, rngNoXor));
            EmitAsm("LDA", Mem(_runtimeRngLoAddress));
            EmitAsm("EOR", Mem(_runtimeRngHiAddress));
            EmitAsm("RTS");
        }
__mapper_id

ビルド時に選択したマッパー番号を返します。マッパー別の初期化や表示に使えます。

書式

c
u8 __mapper_id(void);

戻り値

8ビットの番号です。NROM=0、MMC1=1、UxROM=2、CNROM=3、MMC3=4、MMC5=5、AxROM=7、VRC6=24、FME7=69、VRC7=85。実行中の基板を調べる処理ではなく、コンパイラが定数を生成します。

使用条件と制限

組み込み関数の宣言はintrinsics.hにあります。nes_mapper_irq_*のマクロにはnes_game.hを読み込みます。マッパーはビルド時の--mapperで選びます。

使用例

c
mapper_result[0] = __mapper_id();

呼び出し部分のコード例です。完全なプログラムに組み込む際は、記載した初期化とバッファの条件を満たしてください。

期待する結果

MMC3のサンプルはCHRのマーカー値19/20、21/22、23/24を表示します。CIRAMは縦配置でA B A B、横配置でA A B Bになります。IRQ回数は01→02→03→03です。最初の3回は明示的な再有効化で1回ずつ増え、無効化後は増えません。各段階で6フレーム待って確認します。

__mapper_id: MMC3-DEFAULT
MMC3-DEFAULT

使用例の完全なプログラム

ビルドコマンド

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\mapper_controls.c -I .\kitaqfc\lib -I .\kitaq-docs\samples -o .\out\mapper_controls.nes --mapper=mmc3 --nes-chr=.\kitaq-docs\samples\mapper_banks.chr --no-cache --no-disasm
実装とソース

kitaqfc/lib/intrinsics.h:314

宣言の注記(原文)
Mapper
Return the low byte of the mapper number selected for this build; no cartridge probe is performed.
__mapper_irq_ack

IRQハンドラーでMMC3の要求を解除します。$E000へ書くため、この呼び出しは次のIRQ要求も無効にします。

書式

c
void __mapper_irq_ack(void);

戻り値

戻り値はありません。

使用条件と制限

組み込み関数の宣言はintrinsics.hにあります。nes_mapper_irq_*のマクロにはnes_game.hを読み込みます。マッパーはビルド時の--mapperで選びます。

MMC3専用です。__nes_irqハンドラー、PPUのA12変化、マッパー側の有効化、CPU側の__irq_enable()が必要です。サンプルは背景を$0000、スプライトを$1000のパターンテーブルにして描画を有効にしています。実機での特定ラインへの同期精度は、このサンプルの検証範囲に含みません。

MMC3のACKと無効化はどちらも$E000への書き込みです。要求を解除すると同時に、次の要求も無効になります。繰り返し使う場合は必要なタイミングで__mapper_irq_enable()を呼んでください。CPUの割り込みマスクやNMIの有効状態は変更しません。

独自ハンドラーの末尾にはコンパイラがRTIを生成しますが、A/X/Yや共有一時領域はハンドラー側で必要な分を保存・復元します。サンプルのハンドラーはA/X/Yを保存し、バイトの加算と引数なしのACKだけを行います。別のC関数を追加する場合は、その関数が触る共有領域も確認してください。

使用例

c
void __nes_irq(void) {
    __asm {
        PHA
        TXA
        PHA
        TYA
        PHA
    }
    irq_count++;
    if (irq_phase == 1) { nes_mapper_irq_ack(); }
    else { __mapper_irq_ack(); }
    __asm {
        PLA
        TAY
        PLA
        TAX
        PLA
    }
}

呼び出し部分のコード例です。完全なプログラムに組み込む際は、記載した初期化とバッファの条件を満たしてください。

期待する結果

MMC3のサンプルはCHRのマーカー値19/20、21/22、23/24を表示します。CIRAMは縦配置でA B A B、横配置でA A B Bになります。IRQ回数は01→02→03→03です。最初の3回は明示的な再有効化で1回ずつ増え、無効化後は増えません。各段階で6フレーム待って確認します。

__mapper_irq_ack: MMC3-DEFAULT
MMC3-DEFAULT

使用例の完全なプログラム

ビルドコマンド

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\mapper_controls.c -I .\kitaqfc\lib -I .\kitaq-docs\samples -o .\out\mapper_controls.nes --mapper=mmc3 --nes-chr=.\kitaq-docs\samples\mapper_banks.chr --no-cache --no-disasm
実装とソース

kitaqfc/lib/intrinsics.h:332

宣言の注記(原文)
Use the same E000 write as disable; explicitly re-enable when another interrupt is needed.
__mapper_irq_disable

$E000へ書き、MMC3のIRQ要求を解除して、以後の要求を無効にします。

書式

c
void __mapper_irq_disable(void);

戻り値

戻り値はありません。

使用条件と制限

組み込み関数の宣言はintrinsics.hにあります。nes_mapper_irq_*のマクロにはnes_game.hを読み込みます。マッパーはビルド時の--mapperで選びます。

MMC3専用です。__nes_irqハンドラー、PPUのA12変化、マッパー側の有効化、CPU側の__irq_enable()が必要です。サンプルは背景を$0000、スプライトを$1000のパターンテーブルにして描画を有効にしています。実機での特定ラインへの同期精度は、このサンプルの検証範囲に含みません。

MMC3のACKと無効化はどちらも$E000への書き込みです。要求を解除すると同時に、次の要求も無効になります。繰り返し使う場合は必要なタイミングで__mapper_irq_enable()を呼んでください。CPUの割り込みマスクやNMIの有効状態は変更しません。

独自ハンドラーの末尾にはコンパイラがRTIを生成しますが、A/X/Yや共有一時領域はハンドラー側で必要な分を保存・復元します。サンプルのハンドラーはA/X/Yを保存し、バイトの加算と引数なしのACKだけを行います。別のC関数を追加する場合は、その関数が触る共有領域も確認してください。

使用例

c
__irq_disable(); __mapper_irq_disable();

呼び出し部分のコード例です。完全なプログラムに組み込む際は、記載した初期化とバッファの条件を満たしてください。

期待する結果

MMC3のサンプルはCHRのマーカー値19/20、21/22、23/24を表示します。CIRAMは縦配置でA B A B、横配置でA A B Bになります。IRQ回数は01→02→03→03です。最初の3回は明示的な再有効化で1回ずつ増え、無効化後は増えません。各段階で6フレーム待って確認します。

__mapper_irq_disable: MMC3-DEFAULT
MMC3-DEFAULT

使用例の完全なプログラム

ビルドコマンド

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\mapper_controls.c -I .\kitaqfc\lib -I .\kitaq-docs\samples -o .\out\mapper_controls.nes --mapper=mmc3 --nes-chr=.\kitaq-docs\samples\mapper_banks.chr --no-cache --no-disasm
実装とソース

kitaqfc/lib/intrinsics.h:330

宣言の注記(原文)
Write MMC3 E000 to disable and acknowledge mapper IRQs.
__mapper_irq_enable

$E001へ書き、MMC3がカウンター条件に従ってIRQを要求できるようにします。ラッチ値や再ロード要求は設定しません。

書式

c
void __mapper_irq_enable(void);

戻り値

戻り値はありません。

使用条件と制限

組み込み関数の宣言はintrinsics.hにあります。nes_mapper_irq_*のマクロにはnes_game.hを読み込みます。マッパーはビルド時の--mapperで選びます。

MMC3専用です。__nes_irqハンドラー、PPUのA12変化、マッパー側の有効化、CPU側の__irq_enable()が必要です。サンプルは背景を$0000、スプライトを$1000のパターンテーブルにして描画を有効にしています。実機での特定ラインへの同期精度は、このサンプルの検証範囲に含みません。

MMC3のACKと無効化はどちらも$E000への書き込みです。要求を解除すると同時に、次の要求も無効になります。繰り返し使う場合は必要なタイミングで__mapper_irq_enable()を呼んでください。CPUの割り込みマスクやNMIの有効状態は変更しません。

独自ハンドラーの末尾にはコンパイラがRTIを生成しますが、A/X/Yや共有一時領域はハンドラー側で必要な分を保存・復元します。サンプルのハンドラーはA/X/Yを保存し、バイトの加算と引数なしのACKだけを行います。別のC関数を追加する場合は、その関数が触る共有領域も確認してください。

使用例

c
__mapper_irq_set(37); __mapper_irq_enable(); __irq_enable();
    for (i=0;i<6;i++) { m_wait(); }
    mapper_result[15]=irq_count;

呼び出し部分のコード例です。完全なプログラムに組み込む際は、記載した初期化とバッファの条件を満たしてください。

期待する結果

MMC3のサンプルはCHRのマーカー値19/20、21/22、23/24を表示します。CIRAMは縦配置でA B A B、横配置でA A B Bになります。IRQ回数は01→02→03→03です。最初の3回は明示的な再有効化で1回ずつ増え、無効化後は増えません。各段階で6フレーム待って確認します。

__mapper_irq_enable: MMC3-DEFAULT
MMC3-DEFAULT

使用例の完全なプログラム

ビルドコマンド

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\mapper_controls.c -I .\kitaqfc\lib -I .\kitaq-docs\samples -o .\out\mapper_controls.nes --mapper=mmc3 --nes-chr=.\kitaq-docs\samples\mapper_banks.chr --no-cache --no-disasm
実装とソース

kitaqfc/lib/intrinsics.h:328

宣言の注記(原文)
Write MMC3 E001. The CPU IRQ mask and a valid handler are separate caller responsibilities.
__mapper_irq_set

MMC3のIRQラッチを$C000へ設定し、$C001への書き込みで次の再ロードを要求します。割り込みの有効化は別操作です。

書式

c
void __mapper_irq_set(u8 scanline);

引数

scanline

0~255の8ビットカウンターラッチ値です。画面上の絶対Y座標ではありません。再ロードのタイミングとPPUのA12変化によって発生位置が決まります。0も書き込めますが、割り込み停止の意味ではありません。

戻り値

戻り値はありません。

使用条件と制限

組み込み関数の宣言はintrinsics.hにあります。nes_mapper_irq_*のマクロにはnes_game.hを読み込みます。マッパーはビルド時の--mapperで選びます。

MMC3専用です。__nes_irqハンドラー、PPUのA12変化、マッパー側の有効化、CPU側の__irq_enable()が必要です。サンプルは背景を$0000、スプライトを$1000のパターンテーブルにして描画を有効にしています。実機での特定ラインへの同期精度は、このサンプルの検証範囲に含みません。

MMC3のACKと無効化はどちらも$E000への書き込みです。要求を解除すると同時に、次の要求も無効になります。繰り返し使う場合は必要なタイミングで__mapper_irq_enable()を呼んでください。CPUの割り込みマスクやNMIの有効状態は変更しません。

独自ハンドラーの末尾にはコンパイラがRTIを生成しますが、A/X/Yや共有一時領域はハンドラー側で必要な分を保存・復元します。サンプルのハンドラーはA/X/Yを保存し、バイトの加算と引数なしのACKだけを行います。別のC関数を追加する場合は、その関数が触る共有領域も確認してください。

使用例

c
__mapper_irq_set(37); __mapper_irq_enable(); __irq_enable();
    for (i=0;i<6;i++) { m_wait(); }
    mapper_result[15]=irq_count;

呼び出し部分のコード例です。完全なプログラムに組み込む際は、記載した初期化とバッファの条件を満たしてください。

期待する結果

MMC3のサンプルはCHRのマーカー値19/20、21/22、23/24を表示します。CIRAMは縦配置でA B A B、横配置でA A B Bになります。IRQ回数は01→02→03→03です。最初の3回は明示的な再有効化で1回ずつ増え、無効化後は増えません。各段階で6フレーム待って確認します。

__mapper_irq_set: MMC3-DEFAULT
MMC3-DEFAULT

使用例の完全なプログラム

ビルドコマンド

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\mapper_controls.c -I .\kitaqfc\lib -I .\kitaq-docs\samples -o .\out\mapper_controls.nes --mapper=mmc3 --nes-chr=.\kitaq-docs\samples\mapper_banks.chr --no-cache --no-disasm
実装とソース

kitaqfc/lib/intrinsics.h:326

宣言の注記(原文)
Alias the MMC3 IRQ latch/reload operation; other mapper profiles are rejected.
__memcpy

srcからdstへ指定したバイト数をコピーします。長さは16ビットなので、RAM上の表全体など、255バイトを超える転送にも使えます。アドレスの小さい方から順に読み書きします。

書式

c
void __memcpy(u8* dst, const u8* src, u16 len);

引数

dst

書き込み先の先頭バイトへのポインターです。処理全体が収まる連続領域を確保してください。バッファを自動確保する機能はありません。

src

読み取り元の先頭バイトへのポインターです。コピー中は、範囲全体を読み取り可能な状態で保持し、内容を変更しないでください。

len

u16で指定するバイト数(0〜65535)です。要素数ではありません。型の最大値は、対象機にその大きさのバッファを確保できることを意味しません。範囲全体を現在のメモリーマップ内に収めてください。

戻り値

ありません(void)。

使用条件と制限

長さ0ではバイトの転送を行いません。ただし引数式の副作用は実行されます。サンプルではポインターを返す2つの関数を引数に渡し、長さ0でも両方が呼ばれたことを確認します。

コピー元とコピー先には重ならない領域を使ってください。これらの処理は小さいアドレスからコピーし、重なりを考慮するmemmoveの動作は提供しません。まだ読んでいないコピー元へ書き込むと、後続のデータが壊れます。

処理が完了してから呼び出し元へ戻ります。ROM・RAMのバンク切り替えや、バッファ境界での範囲の切り詰めは行いません。メモリーの割り当てを安定させ、有効な範囲を指定し、同じバッファを使う割り込み処理とのアクセスを調整してください。

FC版のdstはPPUアドレスではなくCPUアドレスです。CPU側のRAMへコピーしてもCHR・ネームテーブル・パレットは更新されません。転送にはPPU・VRAM用APIを使います。生成される補助処理は引数・作業領域を共有するため、呼び出し中にNMI・IRQから再び呼び出さないでください。

使用例

c
memory_reset();
__memcpy(memory_destination+1,memory_source+1,257);
memory_copy_word=memory_check(257,0); // 257 copied bytes; guards unchanged.

呼び出し部分のコード例です。完全なプログラムに組み込む際は、記載した初期化とバッファの条件を満たしてください。

期待する結果

この例では__memcpyで、規則的な値を入れた257バイトをオフセット1からコピーします。memory_checkでコピー元・先の配列全体を照合し、COPY WORDの行に257が表示されることを確認します。先頭を1バイト空けて保護領域とし、転送末尾より後のバイトも変わっていないことを調べます。

全体プログラムは処理ごとに264バイトのバッファ2つを初期化し、コピー元・先の全バイトと、変更してはいけない前後の保護領域を照合します。正常時の表示はCOPY WORD 257、FILL WORD 257、COPY BYTE 255、FILL BYTE 255、COPY16 016、COPY32 032、ZERO CALLS 002、FAILED 000です。WORD・BYTEは長さの型を表し、1回にコピーする要素の大きさではありません。数字はサンプルの照合処理が求めた値で、戻り値のない各関数が返した値ではありません。

掲載画像はDMGが白地に黒字、CGBが白地に青字、FCが黒地に青字です。RAM操作を確認する例であり、文字は検査結果を表示するためのものです。図形描画関数の検証ではありません。別途、両機種それぞれ88ケースをエミュレーターで照合し、定数・実行時の長さ0・1・16・17・32・33・64・65・255バイトと、16ビット長の256・257バイトを確認しています。実機では未検証です。

__memcpy: NROM
NROM

使用例の完全なプログラム

ビルドコマンド

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\memory_intrinsics.c -I .\kitaqfc\lib -I .\kitaq-docs\samples -o .\out\memory_intrinsics.nes --mapper=nrom --nes-chr=.\kitaq-docs\samples\font.chr --no-cache --no-disasm
実装とソース

kitaqfc/lib/intrinsics.h:19

宣言の注記(原文)
memory / bit / math
Copy len bytes forward between readable/writable CPU ranges; overlapping copies are not memmove.
Zero length performs no memory transfer; argument side effects are evaluated before expansion.

kitaqfc/kitaqfc/CodeGenerator.cs:4607

csharp
case "__memcpy":
                    if (TryEmitMemcpyOrMemsetInline(funcName, args, ctx, isSet: false, lenIndex: 2, lenSize: 2)) return true;
                    return EmitKitaqfcHelperCall(funcName, args, ctx, new[] {2,2,2}, "__memcpy");

bool TryEmitMemcpyOrMemsetInline(string funcName, Expr[] args, FunctionContext ctx, bool isSet, int lenIndex, int lenSize)
        {
            args = args ?? Array.Empty<Expr>();
            int expected = isSet ? 3 : 3;
            if (args.Length != expected) return false;
            if (!TryEvaluateConstant(args[lenIndex], out int len)) return false;
            if (len < 0 || len > 32) return false;
            int valueConst = 0;
            if (isSet && !TryEvaluateConstant(args[1], out valueConst)) return false;
            // A zero-length expansion emits no address or fill evaluation in this helper.
            if (len == 0) return true;
            if (isSet)
            {
                int[] ptr = AcquireTemps(2);
                try
                {
                    EmitLoadValue(args[0], ctx, 2);
                    EmitAsm("STA", Mem(ptr[0]));
                    EmitAsm("STX", Mem(ptr[1]));
                    for (int i = 0; i < len; i++)
                    {
                        EmitAsm("LDY", Imm(i));
                        EmitAsm("LDA", Imm(valueConst));
                        EmitAsm("STA", IndY(ptr[0]));
                    }
                }
                finally { ReleaseTemps(ptr); }
                return true;
            }
            EmitFixedMemcpy(args[0], args[1], len, ctx);
            return true;
        }

void EmitHelperMemoryAndBit()
        {
            // Promote the byte count to a word by clearing its high byte before entering memcpy.
            EmitHelperStart("__memcpy_small");
            EmitAsm("LDA", Imm(0));
            EmitAsm("STA", Mem(CallArgBase + 5));
            EmitAsm("JMP", Abs("__memcpy"));

            // Advance both argument pointers and decrement the word count in place; overlapping copies are not reversed.
            EmitHelperStart("__memcpy");
            string mloop = NewGeneratedLabel("memcpy_loop");
            string mdone = NewGeneratedLabel("memcpy_done");
            EmitAsm("LDA", Mem(CallArgBase + 4));
            EmitAsm("ORA", Mem(CallArgBase + 5));
            EmitAsm("BEQ", Rel(mdone));
            _assembly.Add(Expr.Make(Tag.Label, mloop));
            EmitAsm("LDY", Imm(0));
            EmitAsm("LDA", IndY(CallArgBase + 2));
            EmitAsm("STA", IndY(CallArgBase));
            EmitAsm("INC", Mem(CallArgBase + 2));
            string ms1 = NewGeneratedLabel("memcpy_src_page_ok");
            EmitAsm("BNE", Rel(ms1));
            EmitAsm("INC", Mem(CallArgBase + 3));
            _assembly.Add(Expr.Make(Tag.Label, ms1));
            EmitAsm("INC", Mem(CallArgBase));
            string md1 = NewGeneratedLabel("memcpy_dst_page_ok");
            EmitAsm("BNE", Rel(md1));
            EmitAsm("INC", Mem(CallArgBase + 1));
            _assembly.Add(Expr.Make(Tag.Label, md1));
            EmitAsm("LDA", Mem(CallArgBase + 4));
            string mlononzero = NewGeneratedLabel("memcpy_lenlo_nonzero");
            EmitAsm("BNE", Rel(mlononzero));
            EmitAsm("DEC", Mem(CallArgBase + 5));
            _assembly.Add(Expr.Make(Tag.Label, mlononzero));
            EmitAsm("DEC", Mem(CallArgBase + 4));
            EmitAsm("LDA", Mem(CallArgBase + 4));
            EmitAsm("ORA", Mem(CallArgBase + 5));
            EmitAsm("BNE", Rel(mloop));
            _assembly.Add(Expr.Make(Tag.Label, mdone));
            EmitAsm("RTS");

            // Clear the high count byte for the small-fill wrapper.
            EmitHelperStart("__memset_small");
            EmitAsm("LDA", Imm(0));
            EmitAsm("STA", Mem(CallArgBase + 4));
            EmitAsm("JMP", Abs("__memset"));

            // Advance the destination pointer while consuming the word count in the shared argument area.
            EmitHelperStart("__memset");
            string sloop = NewGeneratedLabel("memset_loop");
            string sdone = NewGeneratedLabel("memset_done");
            EmitAsm("LDA", Mem(CallArgBase + 3));
            EmitAsm("ORA", Mem(CallArgBase + 4));
            EmitAsm("BEQ", Rel(sdone));
            _assembly.Add(Expr.Make(Tag.Label, sloop));
            EmitAsm("LDY", Imm(0));
            EmitAsm("LDA", Mem(CallArgBase + 2));
            EmitAsm("STA", IndY(CallArgBase));
            EmitAsm("INC", Mem(CallArgBase));
            string sd1 = NewGeneratedLabel("memset_dst_page_ok");
            EmitAsm("BNE", Rel(sd1));
            EmitAsm("INC", Mem(CallArgBase + 1));
            _assembly.Add(Expr.Make(Tag.Label, sd1));
            EmitAsm("LDA", Mem(CallArgBase + 3));
            string slononzero = NewGeneratedLabel("memset_lenlo_nonzero");
            EmitAsm("BNE", Rel(slononzero));
            EmitAsm("DEC", Mem(CallArgBase + 4));
            _assembly.Add(Expr.Make(Tag.Label, slononzero));
            EmitAsm("DEC", Mem(CallArgBase + 3));
            EmitAsm("LDA", Mem(CallArgBase + 3));
            EmitAsm("ORA", Mem(CallArgBase + 4));
            EmitAsm("BNE", Rel(sloop));
            _assembly.Add(Expr.Make(Tag.Label, sdone));
            EmitAsm("RTS");
        }

void EmitFixedMemcpy(Expr dstExpr, Expr srcExpr, int len, FunctionContext ctx)
        {
            int[] ptr = AcquireTemps(4);
            try
            {
                EmitLoadValue(dstExpr, ctx, 2);
                EmitAsm("STA", Mem(ptr[0]));
                EmitAsm("STX", Mem(ptr[1]));
                EmitLoadValue(srcExpr, ctx, 2);
                EmitAsm("STA", Mem(ptr[2]));
                EmitAsm("STX", Mem(ptr[3]));
                for (int i = 0; i < len; i++)
                {
                    EmitAsm("LDY", Imm(i));
                    EmitAsm("LDA", IndY(ptr[2]));
                    EmitAsm("STA", IndY(ptr[0]));
                }
            }
            finally { ReleaseTemps(ptr); }
        }

// kitaqfc/kitaqfc/Lowerer.cs:2128
if (expr.MatchAny(Tag.Call, out callFunc, out callArgs))
        {
            string knownCallName;
            if (callFunc != null && callFunc.Match(Tag.Name, out knownCallName) &&
                knownCallName == "__cgb_is_cgb" &&
                callArgs != null && callArgs.Length == 0 &&
                // A resolved target configuration can replace the zero-argument CGB query with a literal.
                Program.TryGetKnownCgbRuntimeValue(out int knownCgbValue))
            {
                return Expr.Make(Tag.Integer, knownCgbValue).WithSource(expr.Source);
            }

            Expr newFunc = LowerExpr(ctx, callFunc, false, prefix);

            // Optimization (post-A3): when we must spill a non-atomic argument into a temp,
            // prefer a u8 temp if the *parameter type* is u8-like.
            // This preserves semantics (implicit truncation to u8) while avoiding needless
            // u16 temporaries created by integer promotions.
            CType[] paramTypes = null;
            string __fname = null;
            if (newFunc.Match(Tag.Name, out __fname))
            {
                paramTypes = ctx.FindFuncParams(__fname);
                if (paramTypes == null)
                {
                    CType fnTypeFromName;
                    if (TryGetCallableFunctionType(ctx.FindTypeOfName(__fname), out fnTypeFromName))
                        paramTypes = fnTypeFromName.ParamTypes;
                }
            }
            else
            {
                CType fnTypeFromExpr;
                if (TryGetCallableFunctionType(InferType(ctx, newFunc), out fnTypeFromExpr))
                    paramTypes = fnTypeFromExpr.ParamTypes;
            }

            Expr[] newArgs = new Expr[callArgs.Length];
            for (int i = 0; i < callArgs.Length; i++)
            {
                Expr a = LowerExpr(ctx, callArgs[i], allowExtract, prefix);

                bool preserveConstantArgIntrinsic = false;
                if (a.MatchAny(Tag.Call, out Expr argFunc, out Expr[] _argCallArgs) &&
                    argFunc.Match(Tag.Name, out string argFuncName))
                {
                    preserveConstantArgIntrinsic = (argFuncName == "__bankof" || argFuncName == "__cgb_is_cgb");
                }
                // Keep evaluable constants and special intrinsic/bank operands visible to code generation instead of spilling them.
                bool preserveCompileTimeConstantArg = ctx.Global.TryEvalConstInt(a, out _);
                bool preserveFarcallBankArg = (__fname == "__farcall" && i == 0);

                if (allowExtract && !preserveFarcallBankArg && !preserveConstantArgIntrinsic && !preserveCompileTimeConstantArg && !IsAtom(a))
                {
                    // If we know the callee's parameter type, use it to choose a smaller temp.
                    // Example: (u8)(row*32) passed to a u8 param should spill as u8, not u16.
                    CType spillAs = null;
                    if (paramTypes != null && i < paramTypes.Length)
                    {
                        CType pt = paramTypes[i];
                        if (IsU8Like(pt)) spillAs = CType.UInt8;
                    }
                    if (spillAs == null) spillAs = InferType(ctx, a);
                    if (!IsAggregateType(spillAs) && SizeOfType(spillAs) <= 2)
                        a = SpillToTemp(ctx, a, spillAs, prefix);
                }
                newArgs[i] = a;
            }

            // Lint: const discard for argument passing
            // Warn when passing a const-qualified pointer (or const array decay) to a non-const pointer parameter.
            // Suppress when the argument has an explicit cast, or inside __unsafe.
            if (!ctx.InUnsafe && paramTypes != null)
            {
                for (int i = 0; i < newArgs.Length && i < paramTypes.Length; i++)
                {
                    CType pt = paramTypes[i];
                    if (pt == null) continue;
                    if (!IsPointerToNonConst(pt)) continue;
                    if (HasTopLevelCast(newArgs[i])) continue;

                    CType at = InferType(ctx, newArgs[i]);
                    if (at != null && (IsPointerToConst(at) || IsArrayOfConst(at)))
                    {
                        string fn = __fname ?? "<call>";
                        WarnConstDiscard(expr, $"argument {i} to '{fn}'");
                        break; // keep it quiet: one warning per call is enough
                    }
                }
            }

            if (paramTypes != null)
            {
                string fn = __fname ?? "<call>";
                for (int i = 0; i < newArgs.Length && i < paramTypes.Length; i++)
                {
                    FilePosition argPos = newArgs[i].Source;
                    if (string.IsNullOrEmpty(argPos.Filename) || argPos.Filename == "<unknown>")
                        argPos = expr.Source;
                    WarnImplicitNarrowingIfNeeded(ctx, newArgs[i], InferType(ctx, newArgs[i]), paramTypes[i], argPos,
                        "argument " + i + " to '" + fn + "'");
                }
            }

            // Lint: __enum_strict argument passing.
            if (paramTypes != null)
            {
                string fn = __fname ?? "<call>";
                for (int i = 0; i < newArgs.Length && i < paramTypes.Length; i++)
                {
                    CType pt = paramTypes[i];
                    if (pt == null) continue;

                    CType at = InferType(ctx, newArgs[i]);
                    if (at == null) continue;

                    if (HasTopLevelCast(newArgs[i])) continue;

                    if (IsStrictEnum(pt))
                    {
                        if (!IsSameEnum(pt, at))
                        {
                            Program.Warning(expr.Source, ErrorCode.EnumStrictMix,
                                "argument {0} to '{1}' expects __enum_strict {2}, got {3}; cast explicitly to silence",
                                i, fn, pt.Show(), at.Show());
                        }
                    }
                    else if (IsPlainInteger(pt) && IsStrictEnum(at))
                    {
                        Program.Warning(expr.Source, ErrorCode.EnumStrictMix,
                            "argument {0} to '{1}' passes __enum_strict {2} to integer parameter {3}; cast explicitly to silence",
                            i, fn, at.Show(), pt.Show());
                    }
                }
            }

            if (!ctx.InUnsafe)
                RestrictAliasLint(ctx, newFunc, newArgs, expr.Source);

            // __slice(ptr,len) helper: represent a "slice" (ptr+len) in the IR.
            // Semantics: evaluate both arguments, result type is the pointer/array (same as arg0).
            // Bounds checks are injected only with -Zcheck (see Program.CheckSliceBounds).
            // Note: keep the intrinsic name conservative to avoid collisions ("slice" is allowed only
            // when there is no declared function named "slice").
            if (newFunc.Match(Tag.Name, out string __sfname))
            {
                bool isSliceIntrinsic = (__sfname == "__slice") || (__sfname == "slice" && ctx.FindFuncParams("slice") == null);
                if (isSliceIntrinsic)
                {
                    if (newArgs.Length != 2)
                    {
                        Program.Error(expr.Source, ErrorCode.ParseError, "{0}: expected 2 arguments: __slice(ptr,len)", __sfname);
                    }
                    else
                    {
                        CType pty = InferType(ctx, newArgs[0]);
                        if (pty == null || !(pty.IsPointer || pty.IsArray))
                            Program.Error(expr.Source, ErrorCode.ExpectedType, "{0}: first argument must be a pointer or array, got: {1}", __sfname, (pty == null ? "<unknown>" : pty.Show()));

                        CType lty = InferType(ctx, newArgs[1]);
                        if (lty == null || !lty.IsInteger)
                            Program.Error(expr.Source, ErrorCode.ExpectedType, "{0}: second argument must be an integer length, got: {1}", __sfname, (lty == null ? "<unknown>" : lty.Show()));
                    }

                    return Expr.Make(Tag.Slice, newArgs[0], newArgs[1]).WithSource(expr.Source);
                }
            }

            return MakeCall(newFunc, newArgs).WithSource(expr.Source);
        }
__memcpy_small

8ビットの長さを使い、最大255バイトのブロックをコピーします。転送長が1バイトに収まる場合に使います。_smallは長さの型を表しており、特定の実行時間や生成コード量を保証する名前ではありません。

書式

c
void __memcpy_small(u8* dst, const u8* src, u8 len);

引数

dst

書き込み先の先頭バイトへのポインターです。処理全体が収まる連続領域を確保してください。バッファを自動確保する機能はありません。

src

読み取り元の先頭バイトへのポインターです。コピー中は、範囲全体を読み取り可能な状態で保持し、内容を変更しないでください。

len

u8で指定する0〜255のバイト数です。256バイト全体を表すつもりで256を渡してはいけません。この引数の型に収まりません。256バイト以上には_smallの付かない関数を使ってください。

戻り値

ありません(void)。

使用条件と制限

長さ0ではバイトの転送を行いません。ただし引数式の副作用は実行されます。サンプルではポインターを返す2つの関数を引数に渡し、長さ0でも両方が呼ばれたことを確認します。

コピー元とコピー先には重ならない領域を使ってください。これらの処理は小さいアドレスからコピーし、重なりを考慮するmemmoveの動作は提供しません。まだ読んでいないコピー元へ書き込むと、後続のデータが壊れます。

処理が完了してから呼び出し元へ戻ります。ROM・RAMのバンク切り替えや、バッファ境界での範囲の切り詰めは行いません。メモリーの割り当てを安定させ、有効な範囲を指定し、同じバッファを使う割り込み処理とのアクセスを調整してください。

FC版のdstはPPUアドレスではなくCPUアドレスです。CPU側のRAMへコピーしてもCHR・ネームテーブル・パレットは更新されません。転送にはPPU・VRAM用APIを使います。生成される補助処理は引数・作業領域を共有するため、呼び出し中にNMI・IRQから再び呼び出さないでください。

使用例

c
memory_reset();
__memcpy_small(memory_destination+1,memory_source+1,255);
memory_copy_byte=memory_check(255,0); // Maximum representable byte count.

呼び出し部分のコード例です。完全なプログラムに組み込む際は、記載した初期化とバッファの条件を満たしてください。

期待する結果

この例では__memcpy_smallで、規則的な値を入れた255バイトをオフセット1からコピーします。memory_checkでコピー元・先の配列全体を照合し、COPY BYTEの行に255が表示されることを確認します。先頭を1バイト空けて保護領域とし、転送末尾より後のバイトも変わっていないことを調べます。

全体プログラムは処理ごとに264バイトのバッファ2つを初期化し、コピー元・先の全バイトと、変更してはいけない前後の保護領域を照合します。正常時の表示はCOPY WORD 257、FILL WORD 257、COPY BYTE 255、FILL BYTE 255、COPY16 016、COPY32 032、ZERO CALLS 002、FAILED 000です。WORD・BYTEは長さの型を表し、1回にコピーする要素の大きさではありません。数字はサンプルの照合処理が求めた値で、戻り値のない各関数が返した値ではありません。

掲載画像はDMGが白地に黒字、CGBが白地に青字、FCが黒地に青字です。RAM操作を確認する例であり、文字は検査結果を表示するためのものです。図形描画関数の検証ではありません。別途、両機種それぞれ88ケースをエミュレーターで照合し、定数・実行時の長さ0・1・16・17・32・33・64・65・255バイトと、16ビット長の256・257バイトを確認しています。実機では未検証です。

__memcpy_small: NROM
NROM

使用例の完全なプログラム

ビルドコマンド

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\memory_intrinsics.c -I .\kitaqfc\lib -I .\kitaq-docs\samples -o .\out\memory_intrinsics.nes --mapper=nrom --nes-chr=.\kitaq-docs\samples\font.chr --no-cache --no-disasm
実装とソース

kitaqfc/lib/intrinsics.h:23

宣言の注記(原文)
Forward copy with an 8-bit length, zero through 255; the dynamic helper promotes it to a word count.

kitaqfc/kitaqfc/CodeGenerator.cs:4610

csharp
case "__memcpy_small":
                    if (TryEmitMemcpyOrMemsetInline(funcName, args, ctx, isSet: false, lenIndex: 2, lenSize: 1)) return true;
                    return EmitKitaqfcHelperCall(funcName, args, ctx, new[] {2,2,1}, "__memcpy_small");

bool TryEmitMemcpyOrMemsetInline(string funcName, Expr[] args, FunctionContext ctx, bool isSet, int lenIndex, int lenSize)
        {
            args = args ?? Array.Empty<Expr>();
            int expected = isSet ? 3 : 3;
            if (args.Length != expected) return false;
            if (!TryEvaluateConstant(args[lenIndex], out int len)) return false;
            if (len < 0 || len > 32) return false;
            int valueConst = 0;
            if (isSet && !TryEvaluateConstant(args[1], out valueConst)) return false;
            // A zero-length expansion emits no address or fill evaluation in this helper.
            if (len == 0) return true;
            if (isSet)
            {
                int[] ptr = AcquireTemps(2);
                try
                {
                    EmitLoadValue(args[0], ctx, 2);
                    EmitAsm("STA", Mem(ptr[0]));
                    EmitAsm("STX", Mem(ptr[1]));
                    for (int i = 0; i < len; i++)
                    {
                        EmitAsm("LDY", Imm(i));
                        EmitAsm("LDA", Imm(valueConst));
                        EmitAsm("STA", IndY(ptr[0]));
                    }
                }
                finally { ReleaseTemps(ptr); }
                return true;
            }
            EmitFixedMemcpy(args[0], args[1], len, ctx);
            return true;
        }

void EmitHelperMemoryAndBit()
        {
            // Promote the byte count to a word by clearing its high byte before entering memcpy.
            EmitHelperStart("__memcpy_small");
            EmitAsm("LDA", Imm(0));
            EmitAsm("STA", Mem(CallArgBase + 5));
            EmitAsm("JMP", Abs("__memcpy"));

            // Advance both argument pointers and decrement the word count in place; overlapping copies are not reversed.
            EmitHelperStart("__memcpy");
            string mloop = NewGeneratedLabel("memcpy_loop");
            string mdone = NewGeneratedLabel("memcpy_done");
            EmitAsm("LDA", Mem(CallArgBase + 4));
            EmitAsm("ORA", Mem(CallArgBase + 5));
            EmitAsm("BEQ", Rel(mdone));
            _assembly.Add(Expr.Make(Tag.Label, mloop));
            EmitAsm("LDY", Imm(0));
            EmitAsm("LDA", IndY(CallArgBase + 2));
            EmitAsm("STA", IndY(CallArgBase));
            EmitAsm("INC", Mem(CallArgBase + 2));
            string ms1 = NewGeneratedLabel("memcpy_src_page_ok");
            EmitAsm("BNE", Rel(ms1));
            EmitAsm("INC", Mem(CallArgBase + 3));
            _assembly.Add(Expr.Make(Tag.Label, ms1));
            EmitAsm("INC", Mem(CallArgBase));
            string md1 = NewGeneratedLabel("memcpy_dst_page_ok");
            EmitAsm("BNE", Rel(md1));
            EmitAsm("INC", Mem(CallArgBase + 1));
            _assembly.Add(Expr.Make(Tag.Label, md1));
            EmitAsm("LDA", Mem(CallArgBase + 4));
            string mlononzero = NewGeneratedLabel("memcpy_lenlo_nonzero");
            EmitAsm("BNE", Rel(mlononzero));
            EmitAsm("DEC", Mem(CallArgBase + 5));
            _assembly.Add(Expr.Make(Tag.Label, mlononzero));
            EmitAsm("DEC", Mem(CallArgBase + 4));
            EmitAsm("LDA", Mem(CallArgBase + 4));
            EmitAsm("ORA", Mem(CallArgBase + 5));
            EmitAsm("BNE", Rel(mloop));
            _assembly.Add(Expr.Make(Tag.Label, mdone));
            EmitAsm("RTS");

            // Clear the high count byte for the small-fill wrapper.
            EmitHelperStart("__memset_small");
            EmitAsm("LDA", Imm(0));
            EmitAsm("STA", Mem(CallArgBase + 4));
            EmitAsm("JMP", Abs("__memset"));

            // Advance the destination pointer while consuming the word count in the shared argument area.
            EmitHelperStart("__memset");
            string sloop = NewGeneratedLabel("memset_loop");
            string sdone = NewGeneratedLabel("memset_done");
            EmitAsm("LDA", Mem(CallArgBase + 3));
            EmitAsm("ORA", Mem(CallArgBase + 4));
            EmitAsm("BEQ", Rel(sdone));
            _assembly.Add(Expr.Make(Tag.Label, sloop));
            EmitAsm("LDY", Imm(0));
            EmitAsm("LDA", Mem(CallArgBase + 2));
            EmitAsm("STA", IndY(CallArgBase));
            EmitAsm("INC", Mem(CallArgBase));
            string sd1 = NewGeneratedLabel("memset_dst_page_ok");
            EmitAsm("BNE", Rel(sd1));
            EmitAsm("INC", Mem(CallArgBase + 1));
            _assembly.Add(Expr.Make(Tag.Label, sd1));
            EmitAsm("LDA", Mem(CallArgBase + 3));
            string slononzero = NewGeneratedLabel("memset_lenlo_nonzero");
            EmitAsm("BNE", Rel(slononzero));
            EmitAsm("DEC", Mem(CallArgBase + 4));
            _assembly.Add(Expr.Make(Tag.Label, slononzero));
            EmitAsm("DEC", Mem(CallArgBase + 3));
            EmitAsm("LDA", Mem(CallArgBase + 3));
            EmitAsm("ORA", Mem(CallArgBase + 4));
            EmitAsm("BNE", Rel(sloop));
            _assembly.Add(Expr.Make(Tag.Label, sdone));
            EmitAsm("RTS");
        }

void EmitFixedMemcpy(Expr dstExpr, Expr srcExpr, int len, FunctionContext ctx)
        {
            int[] ptr = AcquireTemps(4);
            try
            {
                EmitLoadValue(dstExpr, ctx, 2);
                EmitAsm("STA", Mem(ptr[0]));
                EmitAsm("STX", Mem(ptr[1]));
                EmitLoadValue(srcExpr, ctx, 2);
                EmitAsm("STA", Mem(ptr[2]));
                EmitAsm("STX", Mem(ptr[3]));
                for (int i = 0; i < len; i++)
                {
                    EmitAsm("LDY", Imm(i));
                    EmitAsm("LDA", IndY(ptr[2]));
                    EmitAsm("STA", IndY(ptr[0]));
                }
            }
            finally { ReleaseTemps(ptr); }
        }

// kitaqfc/kitaqfc/Lowerer.cs:2128
if (expr.MatchAny(Tag.Call, out callFunc, out callArgs))
        {
            string knownCallName;
            if (callFunc != null && callFunc.Match(Tag.Name, out knownCallName) &&
                knownCallName == "__cgb_is_cgb" &&
                callArgs != null && callArgs.Length == 0 &&
                // A resolved target configuration can replace the zero-argument CGB query with a literal.
                Program.TryGetKnownCgbRuntimeValue(out int knownCgbValue))
            {
                return Expr.Make(Tag.Integer, knownCgbValue).WithSource(expr.Source);
            }

            Expr newFunc = LowerExpr(ctx, callFunc, false, prefix);

            // Optimization (post-A3): when we must spill a non-atomic argument into a temp,
            // prefer a u8 temp if the *parameter type* is u8-like.
            // This preserves semantics (implicit truncation to u8) while avoiding needless
            // u16 temporaries created by integer promotions.
            CType[] paramTypes = null;
            string __fname = null;
            if (newFunc.Match(Tag.Name, out __fname))
            {
                paramTypes = ctx.FindFuncParams(__fname);
                if (paramTypes == null)
                {
                    CType fnTypeFromName;
                    if (TryGetCallableFunctionType(ctx.FindTypeOfName(__fname), out fnTypeFromName))
                        paramTypes = fnTypeFromName.ParamTypes;
                }
            }
            else
            {
                CType fnTypeFromExpr;
                if (TryGetCallableFunctionType(InferType(ctx, newFunc), out fnTypeFromExpr))
                    paramTypes = fnTypeFromExpr.ParamTypes;
            }

            Expr[] newArgs = new Expr[callArgs.Length];
            for (int i = 0; i < callArgs.Length; i++)
            {
                Expr a = LowerExpr(ctx, callArgs[i], allowExtract, prefix);

                bool preserveConstantArgIntrinsic = false;
                if (a.MatchAny(Tag.Call, out Expr argFunc, out Expr[] _argCallArgs) &&
                    argFunc.Match(Tag.Name, out string argFuncName))
                {
                    preserveConstantArgIntrinsic = (argFuncName == "__bankof" || argFuncName == "__cgb_is_cgb");
                }
                // Keep evaluable constants and special intrinsic/bank operands visible to code generation instead of spilling them.
                bool preserveCompileTimeConstantArg = ctx.Global.TryEvalConstInt(a, out _);
                bool preserveFarcallBankArg = (__fname == "__farcall" && i == 0);

                if (allowExtract && !preserveFarcallBankArg && !preserveConstantArgIntrinsic && !preserveCompileTimeConstantArg && !IsAtom(a))
                {
                    // If we know the callee's parameter type, use it to choose a smaller temp.
                    // Example: (u8)(row*32) passed to a u8 param should spill as u8, not u16.
                    CType spillAs = null;
                    if (paramTypes != null && i < paramTypes.Length)
                    {
                        CType pt = paramTypes[i];
                        if (IsU8Like(pt)) spillAs = CType.UInt8;
                    }
                    if (spillAs == null) spillAs = InferType(ctx, a);
                    if (!IsAggregateType(spillAs) && SizeOfType(spillAs) <= 2)
                        a = SpillToTemp(ctx, a, spillAs, prefix);
                }
                newArgs[i] = a;
            }

            // Lint: const discard for argument passing
            // Warn when passing a const-qualified pointer (or const array decay) to a non-const pointer parameter.
            // Suppress when the argument has an explicit cast, or inside __unsafe.
            if (!ctx.InUnsafe && paramTypes != null)
            {
                for (int i = 0; i < newArgs.Length && i < paramTypes.Length; i++)
                {
                    CType pt = paramTypes[i];
                    if (pt == null) continue;
                    if (!IsPointerToNonConst(pt)) continue;
                    if (HasTopLevelCast(newArgs[i])) continue;

                    CType at = InferType(ctx, newArgs[i]);
                    if (at != null && (IsPointerToConst(at) || IsArrayOfConst(at)))
                    {
                        string fn = __fname ?? "<call>";
                        WarnConstDiscard(expr, $"argument {i} to '{fn}'");
                        break; // keep it quiet: one warning per call is enough
                    }
                }
            }

            if (paramTypes != null)
            {
                string fn = __fname ?? "<call>";
                for (int i = 0; i < newArgs.Length && i < paramTypes.Length; i++)
                {
                    FilePosition argPos = newArgs[i].Source;
                    if (string.IsNullOrEmpty(argPos.Filename) || argPos.Filename == "<unknown>")
                        argPos = expr.Source;
                    WarnImplicitNarrowingIfNeeded(ctx, newArgs[i], InferType(ctx, newArgs[i]), paramTypes[i], argPos,
                        "argument " + i + " to '" + fn + "'");
                }
            }

            // Lint: __enum_strict argument passing.
            if (paramTypes != null)
            {
                string fn = __fname ?? "<call>";
                for (int i = 0; i < newArgs.Length && i < paramTypes.Length; i++)
                {
                    CType pt = paramTypes[i];
                    if (pt == null) continue;

                    CType at = InferType(ctx, newArgs[i]);
                    if (at == null) continue;

                    if (HasTopLevelCast(newArgs[i])) continue;

                    if (IsStrictEnum(pt))
                    {
                        if (!IsSameEnum(pt, at))
                        {
                            Program.Warning(expr.Source, ErrorCode.EnumStrictMix,
                                "argument {0} to '{1}' expects __enum_strict {2}, got {3}; cast explicitly to silence",
                                i, fn, pt.Show(), at.Show());
                        }
                    }
                    else if (IsPlainInteger(pt) && IsStrictEnum(at))
                    {
                        Program.Warning(expr.Source, ErrorCode.EnumStrictMix,
                            "argument {0} to '{1}' passes __enum_strict {2} to integer parameter {3}; cast explicitly to silence",
                            i, fn, at.Show(), pt.Show());
                    }
                }
            }

            if (!ctx.InUnsafe)
                RestrictAliasLint(ctx, newFunc, newArgs, expr.Source);

            // __slice(ptr,len) helper: represent a "slice" (ptr+len) in the IR.
            // Semantics: evaluate both arguments, result type is the pointer/array (same as arg0).
            // Bounds checks are injected only with -Zcheck (see Program.CheckSliceBounds).
            // Note: keep the intrinsic name conservative to avoid collisions ("slice" is allowed only
            // when there is no declared function named "slice").
            if (newFunc.Match(Tag.Name, out string __sfname))
            {
                bool isSliceIntrinsic = (__sfname == "__slice") || (__sfname == "slice" && ctx.FindFuncParams("slice") == null);
                if (isSliceIntrinsic)
                {
                    if (newArgs.Length != 2)
                    {
                        Program.Error(expr.Source, ErrorCode.ParseError, "{0}: expected 2 arguments: __slice(ptr,len)", __sfname);
                    }
                    else
                    {
                        CType pty = InferType(ctx, newArgs[0]);
                        if (pty == null || !(pty.IsPointer || pty.IsArray))
                            Program.Error(expr.Source, ErrorCode.ExpectedType, "{0}: first argument must be a pointer or array, got: {1}", __sfname, (pty == null ? "<unknown>" : pty.Show()));

                        CType lty = InferType(ctx, newArgs[1]);
                        if (lty == null || !lty.IsInteger)
                            Program.Error(expr.Source, ErrorCode.ExpectedType, "{0}: second argument must be an integer length, got: {1}", __sfname, (lty == null ? "<unknown>" : lty.Show()));
                    }

                    return Expr.Make(Tag.Slice, newArgs[0], newArgs[1]).WithSource(expr.Source);
                }
            }

            return MakeCall(newFunc, newArgs).WithSource(expr.Source);
        }
__memset

転送先の範囲を同じ1バイトの値で埋めます。長さは16ビットで、大きなバッファの消去や表の初期化に使えます。例えば0x5Aなら5A 5A 5A…を書き込みます。複数バイトの整数を繰り返す操作ではありません。

書式

c
void __memset(u8* dst, u8 value, u16 len);

引数

dst

書き込み先の先頭バイトへのポインターです。処理全体が収まる連続領域を確保してください。バッファを自動確保する機能はありません。

value

各アドレスに繰り返し書く0〜255の1バイト値です。0なら範囲をゼロで埋めます。u16配列をゼロ以外の数値で初期化したい場合は、各要素に代入してください。

len

u16で指定するバイト数(0〜65535)です。要素数ではありません。型の最大値は、対象機にその大きさのバッファを確保できることを意味しません。範囲全体を現在のメモリーマップ内に収めてください。

戻り値

ありません(void)。

使用条件と制限

長さ0ではバイトの転送を行いません。ただし引数式の副作用は実行されます。サンプルではポインターを返す2つの関数を引数に渡し、長さ0でも両方が呼ばれたことを確認します。

処理が完了してから呼び出し元へ戻ります。ROM・RAMのバンク切り替えや、バッファ境界での範囲の切り詰めは行いません。メモリーの割り当てを安定させ、有効な範囲を指定し、同じバッファを使う割り込み処理とのアクセスを調整してください。

FC版のdstはPPUアドレスではなくCPUアドレスです。CPU側のRAMへコピーしてもCHR・ネームテーブル・パレットは更新されません。転送にはPPU・VRAM用APIを使います。生成される補助処理は引数・作業領域を共有するため、呼び出し中にNMI・IRQから再び呼び出さないでください。

使用例

c
memory_reset();
__memset(memory_destination+1,0x5A,257);
memory_fill_word=memory_check(257,1); // Repeat one byte, not a 16-bit word.

呼び出し部分のコード例です。完全なプログラムに組み込む際は、記載した初期化とバッファの条件を満たしてください。

期待する結果

この例では__memsetで、オフセット1からの257バイトを0x5Aで埋めます。memory_checkは対象範囲の全バイトが0x5Aで、前後の保護領域が0xCCのままであり、別のコピー元バッファも変わっていないことを調べます。FILL WORDの行に257が表示されれば正常です。

全体プログラムは処理ごとに264バイトのバッファ2つを初期化し、コピー元・先の全バイトと、変更してはいけない前後の保護領域を照合します。正常時の表示はCOPY WORD 257、FILL WORD 257、COPY BYTE 255、FILL BYTE 255、COPY16 016、COPY32 032、ZERO CALLS 002、FAILED 000です。WORD・BYTEは長さの型を表し、1回にコピーする要素の大きさではありません。数字はサンプルの照合処理が求めた値で、戻り値のない各関数が返した値ではありません。

掲載画像はDMGが白地に黒字、CGBが白地に青字、FCが黒地に青字です。RAM操作を確認する例であり、文字は検査結果を表示するためのものです。図形描画関数の検証ではありません。別途、両機種それぞれ88ケースをエミュレーターで照合し、定数・実行時の長さ0・1・16・17・32・33・64・65・255バイトと、16ビット長の256・257バイトを確認しています。実機では未検証です。

__memset: NROM
NROM

使用例の完全なプログラム

ビルドコマンド

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\memory_intrinsics.c -I .\kitaqfc\lib -I .\kitaq-docs\samples -o .\out\memory_intrinsics.nes --mapper=nrom --nes-chr=.\kitaq-docs\samples\font.chr --no-cache --no-disasm
実装とソース

kitaqfc/lib/intrinsics.h:21

宣言の注記(原文)
Fill len CPU bytes with value. Zero length still evaluates argument side effects.

kitaqfc/kitaqfc/CodeGenerator.cs:4613

csharp
case "__memset":
                    if (TryEmitMemcpyOrMemsetInline(funcName, args, ctx, isSet: true, lenIndex: 2, lenSize: 2)) return true;
                    return EmitKitaqfcHelperCall(funcName, args, ctx, new[] {2,1,2}, "__memset");

bool TryEmitMemcpyOrMemsetInline(string funcName, Expr[] args, FunctionContext ctx, bool isSet, int lenIndex, int lenSize)
        {
            args = args ?? Array.Empty<Expr>();
            int expected = isSet ? 3 : 3;
            if (args.Length != expected) return false;
            if (!TryEvaluateConstant(args[lenIndex], out int len)) return false;
            if (len < 0 || len > 32) return false;
            int valueConst = 0;
            if (isSet && !TryEvaluateConstant(args[1], out valueConst)) return false;
            // A zero-length expansion emits no address or fill evaluation in this helper.
            if (len == 0) return true;
            if (isSet)
            {
                int[] ptr = AcquireTemps(2);
                try
                {
                    EmitLoadValue(args[0], ctx, 2);
                    EmitAsm("STA", Mem(ptr[0]));
                    EmitAsm("STX", Mem(ptr[1]));
                    for (int i = 0; i < len; i++)
                    {
                        EmitAsm("LDY", Imm(i));
                        EmitAsm("LDA", Imm(valueConst));
                        EmitAsm("STA", IndY(ptr[0]));
                    }
                }
                finally { ReleaseTemps(ptr); }
                return true;
            }
            EmitFixedMemcpy(args[0], args[1], len, ctx);
            return true;
        }

void EmitHelperMemoryAndBit()
        {
            // Promote the byte count to a word by clearing its high byte before entering memcpy.
            EmitHelperStart("__memcpy_small");
            EmitAsm("LDA", Imm(0));
            EmitAsm("STA", Mem(CallArgBase + 5));
            EmitAsm("JMP", Abs("__memcpy"));

            // Advance both argument pointers and decrement the word count in place; overlapping copies are not reversed.
            EmitHelperStart("__memcpy");
            string mloop = NewGeneratedLabel("memcpy_loop");
            string mdone = NewGeneratedLabel("memcpy_done");
            EmitAsm("LDA", Mem(CallArgBase + 4));
            EmitAsm("ORA", Mem(CallArgBase + 5));
            EmitAsm("BEQ", Rel(mdone));
            _assembly.Add(Expr.Make(Tag.Label, mloop));
            EmitAsm("LDY", Imm(0));
            EmitAsm("LDA", IndY(CallArgBase + 2));
            EmitAsm("STA", IndY(CallArgBase));
            EmitAsm("INC", Mem(CallArgBase + 2));
            string ms1 = NewGeneratedLabel("memcpy_src_page_ok");
            EmitAsm("BNE", Rel(ms1));
            EmitAsm("INC", Mem(CallArgBase + 3));
            _assembly.Add(Expr.Make(Tag.Label, ms1));
            EmitAsm("INC", Mem(CallArgBase));
            string md1 = NewGeneratedLabel("memcpy_dst_page_ok");
            EmitAsm("BNE", Rel(md1));
            EmitAsm("INC", Mem(CallArgBase + 1));
            _assembly.Add(Expr.Make(Tag.Label, md1));
            EmitAsm("LDA", Mem(CallArgBase + 4));
            string mlononzero = NewGeneratedLabel("memcpy_lenlo_nonzero");
            EmitAsm("BNE", Rel(mlononzero));
            EmitAsm("DEC", Mem(CallArgBase + 5));
            _assembly.Add(Expr.Make(Tag.Label, mlononzero));
            EmitAsm("DEC", Mem(CallArgBase + 4));
            EmitAsm("LDA", Mem(CallArgBase + 4));
            EmitAsm("ORA", Mem(CallArgBase + 5));
            EmitAsm("BNE", Rel(mloop));
            _assembly.Add(Expr.Make(Tag.Label, mdone));
            EmitAsm("RTS");

            // Clear the high count byte for the small-fill wrapper.
            EmitHelperStart("__memset_small");
            EmitAsm("LDA", Imm(0));
            EmitAsm("STA", Mem(CallArgBase + 4));
            EmitAsm("JMP", Abs("__memset"));

            // Advance the destination pointer while consuming the word count in the shared argument area.
            EmitHelperStart("__memset");
            string sloop = NewGeneratedLabel("memset_loop");
            string sdone = NewGeneratedLabel("memset_done");
            EmitAsm("LDA", Mem(CallArgBase + 3));
            EmitAsm("ORA", Mem(CallArgBase + 4));
            EmitAsm("BEQ", Rel(sdone));
            _assembly.Add(Expr.Make(Tag.Label, sloop));
            EmitAsm("LDY", Imm(0));
            EmitAsm("LDA", Mem(CallArgBase + 2));
            EmitAsm("STA", IndY(CallArgBase));
            EmitAsm("INC", Mem(CallArgBase));
            string sd1 = NewGeneratedLabel("memset_dst_page_ok");
            EmitAsm("BNE", Rel(sd1));
            EmitAsm("INC", Mem(CallArgBase + 1));
            _assembly.Add(Expr.Make(Tag.Label, sd1));
            EmitAsm("LDA", Mem(CallArgBase + 3));
            string slononzero = NewGeneratedLabel("memset_lenlo_nonzero");
            EmitAsm("BNE", Rel(slononzero));
            EmitAsm("DEC", Mem(CallArgBase + 4));
            _assembly.Add(Expr.Make(Tag.Label, slononzero));
            EmitAsm("DEC", Mem(CallArgBase + 3));
            EmitAsm("LDA", Mem(CallArgBase + 3));
            EmitAsm("ORA", Mem(CallArgBase + 4));
            EmitAsm("BNE", Rel(sloop));
            _assembly.Add(Expr.Make(Tag.Label, sdone));
            EmitAsm("RTS");
        }

void EmitFixedMemcpy(Expr dstExpr, Expr srcExpr, int len, FunctionContext ctx)
        {
            int[] ptr = AcquireTemps(4);
            try
            {
                EmitLoadValue(dstExpr, ctx, 2);
                EmitAsm("STA", Mem(ptr[0]));
                EmitAsm("STX", Mem(ptr[1]));
                EmitLoadValue(srcExpr, ctx, 2);
                EmitAsm("STA", Mem(ptr[2]));
                EmitAsm("STX", Mem(ptr[3]));
                for (int i = 0; i < len; i++)
                {
                    EmitAsm("LDY", Imm(i));
                    EmitAsm("LDA", IndY(ptr[2]));
                    EmitAsm("STA", IndY(ptr[0]));
                }
            }
            finally { ReleaseTemps(ptr); }
        }

// kitaqfc/kitaqfc/Lowerer.cs:2128
if (expr.MatchAny(Tag.Call, out callFunc, out callArgs))
        {
            string knownCallName;
            if (callFunc != null && callFunc.Match(Tag.Name, out knownCallName) &&
                knownCallName == "__cgb_is_cgb" &&
                callArgs != null && callArgs.Length == 0 &&
                // A resolved target configuration can replace the zero-argument CGB query with a literal.
                Program.TryGetKnownCgbRuntimeValue(out int knownCgbValue))
            {
                return Expr.Make(Tag.Integer, knownCgbValue).WithSource(expr.Source);
            }

            Expr newFunc = LowerExpr(ctx, callFunc, false, prefix);

            // Optimization (post-A3): when we must spill a non-atomic argument into a temp,
            // prefer a u8 temp if the *parameter type* is u8-like.
            // This preserves semantics (implicit truncation to u8) while avoiding needless
            // u16 temporaries created by integer promotions.
            CType[] paramTypes = null;
            string __fname = null;
            if (newFunc.Match(Tag.Name, out __fname))
            {
                paramTypes = ctx.FindFuncParams(__fname);
                if (paramTypes == null)
                {
                    CType fnTypeFromName;
                    if (TryGetCallableFunctionType(ctx.FindTypeOfName(__fname), out fnTypeFromName))
                        paramTypes = fnTypeFromName.ParamTypes;
                }
            }
            else
            {
                CType fnTypeFromExpr;
                if (TryGetCallableFunctionType(InferType(ctx, newFunc), out fnTypeFromExpr))
                    paramTypes = fnTypeFromExpr.ParamTypes;
            }

            Expr[] newArgs = new Expr[callArgs.Length];
            for (int i = 0; i < callArgs.Length; i++)
            {
                Expr a = LowerExpr(ctx, callArgs[i], allowExtract, prefix);

                bool preserveConstantArgIntrinsic = false;
                if (a.MatchAny(Tag.Call, out Expr argFunc, out Expr[] _argCallArgs) &&
                    argFunc.Match(Tag.Name, out string argFuncName))
                {
                    preserveConstantArgIntrinsic = (argFuncName == "__bankof" || argFuncName == "__cgb_is_cgb");
                }
                // Keep evaluable constants and special intrinsic/bank operands visible to code generation instead of spilling them.
                bool preserveCompileTimeConstantArg = ctx.Global.TryEvalConstInt(a, out _);
                bool preserveFarcallBankArg = (__fname == "__farcall" && i == 0);

                if (allowExtract && !preserveFarcallBankArg && !preserveConstantArgIntrinsic && !preserveCompileTimeConstantArg && !IsAtom(a))
                {
                    // If we know the callee's parameter type, use it to choose a smaller temp.
                    // Example: (u8)(row*32) passed to a u8 param should spill as u8, not u16.
                    CType spillAs = null;
                    if (paramTypes != null && i < paramTypes.Length)
                    {
                        CType pt = paramTypes[i];
                        if (IsU8Like(pt)) spillAs = CType.UInt8;
                    }
                    if (spillAs == null) spillAs = InferType(ctx, a);
                    if (!IsAggregateType(spillAs) && SizeOfType(spillAs) <= 2)
                        a = SpillToTemp(ctx, a, spillAs, prefix);
                }
                newArgs[i] = a;
            }

            // Lint: const discard for argument passing
            // Warn when passing a const-qualified pointer (or const array decay) to a non-const pointer parameter.
            // Suppress when the argument has an explicit cast, or inside __unsafe.
            if (!ctx.InUnsafe && paramTypes != null)
            {
                for (int i = 0; i < newArgs.Length && i < paramTypes.Length; i++)
                {
                    CType pt = paramTypes[i];
                    if (pt == null) continue;
                    if (!IsPointerToNonConst(pt)) continue;
                    if (HasTopLevelCast(newArgs[i])) continue;

                    CType at = InferType(ctx, newArgs[i]);
                    if (at != null && (IsPointerToConst(at) || IsArrayOfConst(at)))
                    {
                        string fn = __fname ?? "<call>";
                        WarnConstDiscard(expr, $"argument {i} to '{fn}'");
                        break; // keep it quiet: one warning per call is enough
                    }
                }
            }

            if (paramTypes != null)
            {
                string fn = __fname ?? "<call>";
                for (int i = 0; i < newArgs.Length && i < paramTypes.Length; i++)
                {
                    FilePosition argPos = newArgs[i].Source;
                    if (string.IsNullOrEmpty(argPos.Filename) || argPos.Filename == "<unknown>")
                        argPos = expr.Source;
                    WarnImplicitNarrowingIfNeeded(ctx, newArgs[i], InferType(ctx, newArgs[i]), paramTypes[i], argPos,
                        "argument " + i + " to '" + fn + "'");
                }
            }

            // Lint: __enum_strict argument passing.
            if (paramTypes != null)
            {
                string fn = __fname ?? "<call>";
                for (int i = 0; i < newArgs.Length && i < paramTypes.Length; i++)
                {
                    CType pt = paramTypes[i];
                    if (pt == null) continue;

                    CType at = InferType(ctx, newArgs[i]);
                    if (at == null) continue;

                    if (HasTopLevelCast(newArgs[i])) continue;

                    if (IsStrictEnum(pt))
                    {
                        if (!IsSameEnum(pt, at))
                        {
                            Program.Warning(expr.Source, ErrorCode.EnumStrictMix,
                                "argument {0} to '{1}' expects __enum_strict {2}, got {3}; cast explicitly to silence",
                                i, fn, pt.Show(), at.Show());
                        }
                    }
                    else if (IsPlainInteger(pt) && IsStrictEnum(at))
                    {
                        Program.Warning(expr.Source, ErrorCode.EnumStrictMix,
                            "argument {0} to '{1}' passes __enum_strict {2} to integer parameter {3}; cast explicitly to silence",
                            i, fn, at.Show(), pt.Show());
                    }
                }
            }

            if (!ctx.InUnsafe)
                RestrictAliasLint(ctx, newFunc, newArgs, expr.Source);

            // __slice(ptr,len) helper: represent a "slice" (ptr+len) in the IR.
            // Semantics: evaluate both arguments, result type is the pointer/array (same as arg0).
            // Bounds checks are injected only with -Zcheck (see Program.CheckSliceBounds).
            // Note: keep the intrinsic name conservative to avoid collisions ("slice" is allowed only
            // when there is no declared function named "slice").
            if (newFunc.Match(Tag.Name, out string __sfname))
            {
                bool isSliceIntrinsic = (__sfname == "__slice") || (__sfname == "slice" && ctx.FindFuncParams("slice") == null);
                if (isSliceIntrinsic)
                {
                    if (newArgs.Length != 2)
                    {
                        Program.Error(expr.Source, ErrorCode.ParseError, "{0}: expected 2 arguments: __slice(ptr,len)", __sfname);
                    }
                    else
                    {
                        CType pty = InferType(ctx, newArgs[0]);
                        if (pty == null || !(pty.IsPointer || pty.IsArray))
                            Program.Error(expr.Source, ErrorCode.ExpectedType, "{0}: first argument must be a pointer or array, got: {1}", __sfname, (pty == null ? "<unknown>" : pty.Show()));

                        CType lty = InferType(ctx, newArgs[1]);
                        if (lty == null || !lty.IsInteger)
                            Program.Error(expr.Source, ErrorCode.ExpectedType, "{0}: second argument must be an integer length, got: {1}", __sfname, (lty == null ? "<unknown>" : lty.Show()));
                    }

                    return Expr.Make(Tag.Slice, newArgs[0], newArgs[1]).WithSource(expr.Source);
                }
            }

            return MakeCall(newFunc, newArgs).WithSource(expr.Source);
        }
__memset_small

8ビットの長さを使い、連続する最大255バイトを同じ1バイトの値で埋めます。小さな作業バッファの初期化に使えます。必要な長さが255バイトを超える場合は__memsetを選びます。

書式

c
void __memset_small(u8* dst, u8 value, u8 len);

引数

dst

書き込み先の先頭バイトへのポインターです。処理全体が収まる連続領域を確保してください。バッファを自動確保する機能はありません。

value

各アドレスに繰り返し書く0〜255の1バイト値です。0なら範囲をゼロで埋めます。u16配列をゼロ以外の数値で初期化したい場合は、各要素に代入してください。

len

u8で指定する0〜255のバイト数です。256バイト全体を表すつもりで256を渡してはいけません。この引数の型に収まりません。256バイト以上には_smallの付かない関数を使ってください。

戻り値

ありません(void)。

使用条件と制限

長さ0ではバイトの転送を行いません。ただし引数式の副作用は実行されます。サンプルではポインターを返す2つの関数を引数に渡し、長さ0でも両方が呼ばれたことを確認します。

処理が完了してから呼び出し元へ戻ります。ROM・RAMのバンク切り替えや、バッファ境界での範囲の切り詰めは行いません。メモリーの割り当てを安定させ、有効な範囲を指定し、同じバッファを使う割り込み処理とのアクセスを調整してください。

FC版のdstはPPUアドレスではなくCPUアドレスです。CPU側のRAMへコピーしてもCHR・ネームテーブル・パレットは更新されません。転送にはPPU・VRAM用APIを使います。生成される補助処理は引数・作業領域を共有するため、呼び出し中にNMI・IRQから再び呼び出さないでください。

使用例

c
memory_reset();
__memset_small(memory_destination+1,0x5A,255);
memory_fill_byte=memory_check(255,1);

呼び出し部分のコード例です。完全なプログラムに組み込む際は、記載した初期化とバッファの条件を満たしてください。

期待する結果

この例では__memset_smallで、オフセット1からの255バイトを0x5Aで埋めます。memory_checkは対象範囲の全バイトが0x5Aで、前後の保護領域が0xCCのままであり、別のコピー元バッファも変わっていないことを調べます。FILL BYTEの行に255が表示されれば正常です。

全体プログラムは処理ごとに264バイトのバッファ2つを初期化し、コピー元・先の全バイトと、変更してはいけない前後の保護領域を照合します。正常時の表示はCOPY WORD 257、FILL WORD 257、COPY BYTE 255、FILL BYTE 255、COPY16 016、COPY32 032、ZERO CALLS 002、FAILED 000です。WORD・BYTEは長さの型を表し、1回にコピーする要素の大きさではありません。数字はサンプルの照合処理が求めた値で、戻り値のない各関数が返した値ではありません。

掲載画像はDMGが白地に黒字、CGBが白地に青字、FCが黒地に青字です。RAM操作を確認する例であり、文字は検査結果を表示するためのものです。図形描画関数の検証ではありません。別途、両機種それぞれ88ケースをエミュレーターで照合し、定数・実行時の長さ0・1・16・17・32・33・64・65・255バイトと、16ビット長の256・257バイトを確認しています。実機では未検証です。

__memset_small: NROM
NROM

使用例の完全なプログラム

ビルドコマンド

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\memory_intrinsics.c -I .\kitaqfc\lib -I .\kitaq-docs\samples -o .\out\memory_intrinsics.nes --mapper=nrom --nes-chr=.\kitaq-docs\samples\font.chr --no-cache --no-disasm
実装とソース

kitaqfc/lib/intrinsics.h:25

宣言の注記(原文)
Fill zero through 255 CPU bytes; this is ordinary RAM access, not a PPU transfer.

kitaqfc/kitaqfc/CodeGenerator.cs:4616

csharp
case "__memset_small":
                    if (TryEmitMemcpyOrMemsetInline(funcName, args, ctx, isSet: true, lenIndex: 2, lenSize: 1)) return true;
                    return EmitKitaqfcHelperCall(funcName, args, ctx, new[] {2,1,1}, "__memset_small");
                // These fixed-size copies bypass the variable-length helper argument area.

bool TryEmitMemcpyOrMemsetInline(string funcName, Expr[] args, FunctionContext ctx, bool isSet, int lenIndex, int lenSize)
        {
            args = args ?? Array.Empty<Expr>();
            int expected = isSet ? 3 : 3;
            if (args.Length != expected) return false;
            if (!TryEvaluateConstant(args[lenIndex], out int len)) return false;
            if (len < 0 || len > 32) return false;
            int valueConst = 0;
            if (isSet && !TryEvaluateConstant(args[1], out valueConst)) return false;
            // A zero-length expansion emits no address or fill evaluation in this helper.
            if (len == 0) return true;
            if (isSet)
            {
                int[] ptr = AcquireTemps(2);
                try
                {
                    EmitLoadValue(args[0], ctx, 2);
                    EmitAsm("STA", Mem(ptr[0]));
                    EmitAsm("STX", Mem(ptr[1]));
                    for (int i = 0; i < len; i++)
                    {
                        EmitAsm("LDY", Imm(i));
                        EmitAsm("LDA", Imm(valueConst));
                        EmitAsm("STA", IndY(ptr[0]));
                    }
                }
                finally { ReleaseTemps(ptr); }
                return true;
            }
            EmitFixedMemcpy(args[0], args[1], len, ctx);
            return true;
        }

void EmitHelperMemoryAndBit()
        {
            // Promote the byte count to a word by clearing its high byte before entering memcpy.
            EmitHelperStart("__memcpy_small");
            EmitAsm("LDA", Imm(0));
            EmitAsm("STA", Mem(CallArgBase + 5));
            EmitAsm("JMP", Abs("__memcpy"));

            // Advance both argument pointers and decrement the word count in place; overlapping copies are not reversed.
            EmitHelperStart("__memcpy");
            string mloop = NewGeneratedLabel("memcpy_loop");
            string mdone = NewGeneratedLabel("memcpy_done");
            EmitAsm("LDA", Mem(CallArgBase + 4));
            EmitAsm("ORA", Mem(CallArgBase + 5));
            EmitAsm("BEQ", Rel(mdone));
            _assembly.Add(Expr.Make(Tag.Label, mloop));
            EmitAsm("LDY", Imm(0));
            EmitAsm("LDA", IndY(CallArgBase + 2));
            EmitAsm("STA", IndY(CallArgBase));
            EmitAsm("INC", Mem(CallArgBase + 2));
            string ms1 = NewGeneratedLabel("memcpy_src_page_ok");
            EmitAsm("BNE", Rel(ms1));
            EmitAsm("INC", Mem(CallArgBase + 3));
            _assembly.Add(Expr.Make(Tag.Label, ms1));
            EmitAsm("INC", Mem(CallArgBase));
            string md1 = NewGeneratedLabel("memcpy_dst_page_ok");
            EmitAsm("BNE", Rel(md1));
            EmitAsm("INC", Mem(CallArgBase + 1));
            _assembly.Add(Expr.Make(Tag.Label, md1));
            EmitAsm("LDA", Mem(CallArgBase + 4));
            string mlononzero = NewGeneratedLabel("memcpy_lenlo_nonzero");
            EmitAsm("BNE", Rel(mlononzero));
            EmitAsm("DEC", Mem(CallArgBase + 5));
            _assembly.Add(Expr.Make(Tag.Label, mlononzero));
            EmitAsm("DEC", Mem(CallArgBase + 4));
            EmitAsm("LDA", Mem(CallArgBase + 4));
            EmitAsm("ORA", Mem(CallArgBase + 5));
            EmitAsm("BNE", Rel(mloop));
            _assembly.Add(Expr.Make(Tag.Label, mdone));
            EmitAsm("RTS");

            // Clear the high count byte for the small-fill wrapper.
            EmitHelperStart("__memset_small");
            EmitAsm("LDA", Imm(0));
            EmitAsm("STA", Mem(CallArgBase + 4));
            EmitAsm("JMP", Abs("__memset"));

            // Advance the destination pointer while consuming the word count in the shared argument area.
            EmitHelperStart("__memset");
            string sloop = NewGeneratedLabel("memset_loop");
            string sdone = NewGeneratedLabel("memset_done");
            EmitAsm("LDA", Mem(CallArgBase + 3));
            EmitAsm("ORA", Mem(CallArgBase + 4));
            EmitAsm("BEQ", Rel(sdone));
            _assembly.Add(Expr.Make(Tag.Label, sloop));
            EmitAsm("LDY", Imm(0));
            EmitAsm("LDA", Mem(CallArgBase + 2));
            EmitAsm("STA", IndY(CallArgBase));
            EmitAsm("INC", Mem(CallArgBase));
            string sd1 = NewGeneratedLabel("memset_dst_page_ok");
            EmitAsm("BNE", Rel(sd1));
            EmitAsm("INC", Mem(CallArgBase + 1));
            _assembly.Add(Expr.Make(Tag.Label, sd1));
            EmitAsm("LDA", Mem(CallArgBase + 3));
            string slononzero = NewGeneratedLabel("memset_lenlo_nonzero");
            EmitAsm("BNE", Rel(slononzero));
            EmitAsm("DEC", Mem(CallArgBase + 4));
            _assembly.Add(Expr.Make(Tag.Label, slononzero));
            EmitAsm("DEC", Mem(CallArgBase + 3));
            EmitAsm("LDA", Mem(CallArgBase + 3));
            EmitAsm("ORA", Mem(CallArgBase + 4));
            EmitAsm("BNE", Rel(sloop));
            _assembly.Add(Expr.Make(Tag.Label, sdone));
            EmitAsm("RTS");
        }

void EmitFixedMemcpy(Expr dstExpr, Expr srcExpr, int len, FunctionContext ctx)
        {
            int[] ptr = AcquireTemps(4);
            try
            {
                EmitLoadValue(dstExpr, ctx, 2);
                EmitAsm("STA", Mem(ptr[0]));
                EmitAsm("STX", Mem(ptr[1]));
                EmitLoadValue(srcExpr, ctx, 2);
                EmitAsm("STA", Mem(ptr[2]));
                EmitAsm("STX", Mem(ptr[3]));
                for (int i = 0; i < len; i++)
                {
                    EmitAsm("LDY", Imm(i));
                    EmitAsm("LDA", IndY(ptr[2]));
                    EmitAsm("STA", IndY(ptr[0]));
                }
            }
            finally { ReleaseTemps(ptr); }
        }

// kitaqfc/kitaqfc/Lowerer.cs:2128
if (expr.MatchAny(Tag.Call, out callFunc, out callArgs))
        {
            string knownCallName;
            if (callFunc != null && callFunc.Match(Tag.Name, out knownCallName) &&
                knownCallName == "__cgb_is_cgb" &&
                callArgs != null && callArgs.Length == 0 &&
                // A resolved target configuration can replace the zero-argument CGB query with a literal.
                Program.TryGetKnownCgbRuntimeValue(out int knownCgbValue))
            {
                return Expr.Make(Tag.Integer, knownCgbValue).WithSource(expr.Source);
            }

            Expr newFunc = LowerExpr(ctx, callFunc, false, prefix);

            // Optimization (post-A3): when we must spill a non-atomic argument into a temp,
            // prefer a u8 temp if the *parameter type* is u8-like.
            // This preserves semantics (implicit truncation to u8) while avoiding needless
            // u16 temporaries created by integer promotions.
            CType[] paramTypes = null;
            string __fname = null;
            if (newFunc.Match(Tag.Name, out __fname))
            {
                paramTypes = ctx.FindFuncParams(__fname);
                if (paramTypes == null)
                {
                    CType fnTypeFromName;
                    if (TryGetCallableFunctionType(ctx.FindTypeOfName(__fname), out fnTypeFromName))
                        paramTypes = fnTypeFromName.ParamTypes;
                }
            }
            else
            {
                CType fnTypeFromExpr;
                if (TryGetCallableFunctionType(InferType(ctx, newFunc), out fnTypeFromExpr))
                    paramTypes = fnTypeFromExpr.ParamTypes;
            }

            Expr[] newArgs = new Expr[callArgs.Length];
            for (int i = 0; i < callArgs.Length; i++)
            {
                Expr a = LowerExpr(ctx, callArgs[i], allowExtract, prefix);

                bool preserveConstantArgIntrinsic = false;
                if (a.MatchAny(Tag.Call, out Expr argFunc, out Expr[] _argCallArgs) &&
                    argFunc.Match(Tag.Name, out string argFuncName))
                {
                    preserveConstantArgIntrinsic = (argFuncName == "__bankof" || argFuncName == "__cgb_is_cgb");
                }
                // Keep evaluable constants and special intrinsic/bank operands visible to code generation instead of spilling them.
                bool preserveCompileTimeConstantArg = ctx.Global.TryEvalConstInt(a, out _);
                bool preserveFarcallBankArg = (__fname == "__farcall" && i == 0);

                if (allowExtract && !preserveFarcallBankArg && !preserveConstantArgIntrinsic && !preserveCompileTimeConstantArg && !IsAtom(a))
                {
                    // If we know the callee's parameter type, use it to choose a smaller temp.
                    // Example: (u8)(row*32) passed to a u8 param should spill as u8, not u16.
                    CType spillAs = null;
                    if (paramTypes != null && i < paramTypes.Length)
                    {
                        CType pt = paramTypes[i];
                        if (IsU8Like(pt)) spillAs = CType.UInt8;
                    }
                    if (spillAs == null) spillAs = InferType(ctx, a);
                    if (!IsAggregateType(spillAs) && SizeOfType(spillAs) <= 2)
                        a = SpillToTemp(ctx, a, spillAs, prefix);
                }
                newArgs[i] = a;
            }

            // Lint: const discard for argument passing
            // Warn when passing a const-qualified pointer (or const array decay) to a non-const pointer parameter.
            // Suppress when the argument has an explicit cast, or inside __unsafe.
            if (!ctx.InUnsafe && paramTypes != null)
            {
                for (int i = 0; i < newArgs.Length && i < paramTypes.Length; i++)
                {
                    CType pt = paramTypes[i];
                    if (pt == null) continue;
                    if (!IsPointerToNonConst(pt)) continue;
                    if (HasTopLevelCast(newArgs[i])) continue;

                    CType at = InferType(ctx, newArgs[i]);
                    if (at != null && (IsPointerToConst(at) || IsArrayOfConst(at)))
                    {
                        string fn = __fname ?? "<call>";
                        WarnConstDiscard(expr, $"argument {i} to '{fn}'");
                        break; // keep it quiet: one warning per call is enough
                    }
                }
            }

            if (paramTypes != null)
            {
                string fn = __fname ?? "<call>";
                for (int i = 0; i < newArgs.Length && i < paramTypes.Length; i++)
                {
                    FilePosition argPos = newArgs[i].Source;
                    if (string.IsNullOrEmpty(argPos.Filename) || argPos.Filename == "<unknown>")
                        argPos = expr.Source;
                    WarnImplicitNarrowingIfNeeded(ctx, newArgs[i], InferType(ctx, newArgs[i]), paramTypes[i], argPos,
                        "argument " + i + " to '" + fn + "'");
                }
            }

            // Lint: __enum_strict argument passing.
            if (paramTypes != null)
            {
                string fn = __fname ?? "<call>";
                for (int i = 0; i < newArgs.Length && i < paramTypes.Length; i++)
                {
                    CType pt = paramTypes[i];
                    if (pt == null) continue;

                    CType at = InferType(ctx, newArgs[i]);
                    if (at == null) continue;

                    if (HasTopLevelCast(newArgs[i])) continue;

                    if (IsStrictEnum(pt))
                    {
                        if (!IsSameEnum(pt, at))
                        {
                            Program.Warning(expr.Source, ErrorCode.EnumStrictMix,
                                "argument {0} to '{1}' expects __enum_strict {2}, got {3}; cast explicitly to silence",
                                i, fn, pt.Show(), at.Show());
                        }
                    }
                    else if (IsPlainInteger(pt) && IsStrictEnum(at))
                    {
                        Program.Warning(expr.Source, ErrorCode.EnumStrictMix,
                            "argument {0} to '{1}' passes __enum_strict {2} to integer parameter {3}; cast explicitly to silence",
                            i, fn, at.Show(), pt.Show());
                    }
                }
            }

            if (!ctx.InUnsafe)
                RestrictAliasLint(ctx, newFunc, newArgs, expr.Source);

            // __slice(ptr,len) helper: represent a "slice" (ptr+len) in the IR.
            // Semantics: evaluate both arguments, result type is the pointer/array (same as arg0).
            // Bounds checks are injected only with -Zcheck (see Program.CheckSliceBounds).
            // Note: keep the intrinsic name conservative to avoid collisions ("slice" is allowed only
            // when there is no declared function named "slice").
            if (newFunc.Match(Tag.Name, out string __sfname))
            {
                bool isSliceIntrinsic = (__sfname == "__slice") || (__sfname == "slice" && ctx.FindFuncParams("slice") == null);
                if (isSliceIntrinsic)
                {
                    if (newArgs.Length != 2)
                    {
                        Program.Error(expr.Source, ErrorCode.ParseError, "{0}: expected 2 arguments: __slice(ptr,len)", __sfname);
                    }
                    else
                    {
                        CType pty = InferType(ctx, newArgs[0]);
                        if (pty == null || !(pty.IsPointer || pty.IsArray))
                            Program.Error(expr.Source, ErrorCode.ExpectedType, "{0}: first argument must be a pointer or array, got: {1}", __sfname, (pty == null ? "<unknown>" : pty.Show()));

                        CType lty = InferType(ctx, newArgs[1]);
                        if (lty == null || !lty.IsInteger)
                            Program.Error(expr.Source, ErrorCode.ExpectedType, "{0}: second argument must be an integer length, got: {1}", __sfname, (lty == null ? "<unknown>" : lty.Show()));
                    }

                    return Expr.Make(Tag.Slice, newArgs[0], newArgs[1]).WithSource(expr.Source);
                }
            }

            return MakeCall(newFunc, newArgs).WithSource(expr.Source);
        }
__metasprite_draw

終端付きの部品データ列を読み、ページ02のシャドーバッファの連続した項目へ展開します。各部品の1バイトのX・Yオフセットを基準位置へ加え、部品ごとのタイルと属性を設定します。戻り値は次のOAM番号です。

書式

c
u8 __metasprite_draw(u8 oam_index, u8 base_x, u8 base_y, const u8* metasprite);

引数

oam_index

スプライトの項目番号0〜63です。バイト単位のオフセットではありません。内部では8ビットのカーソルで4倍するため、範囲外の値は64を周期に折り返し、別の項目を上書きします。確保済みかどうかの検査は行いません。

base_x

ピクセル単位の横位置を符号なし8ビット値で渡します。タイル座標ではありません。

base_y

OAMに格納するY値です。画面の1行目以降に上端を置く場合は表示上端−1を渡します。239〜255は240行の表示範囲外です。

metasprite

dx, dy, tile, attrの4バイトを1組とする読み出し可能なデータ列へのポインタです。次のdxの位置に0xFFがあれば終了し、その後のバイトは不要です。オフセットは256を周期に加算します。0xFEは−2、dyの0xFFは−1として使えますが、dxの0xFFは終端専用なので−1を表せません。現在選択中のメモリでポインタが有効である必要があります。

戻り値

次のOAM項目番号で、(oam_index + 書き込んだ部品数) modulo 64です。描いた部品数ではなく、次の書き込み位置を返します。空のデータ列ではoam_index modulo 64です。

使用条件と制限

これらの操作はCPU RAMの0x0200–0x02FFを書き換えます。ハードウェアOAMへ即座に反映されるわけではありません。ページを完成させてから__oam_dmaで転送してください。スプライトライブラリの確保フラグ・座標配列・使用数は更新しません。準備途中のページを割り込み処理で変更・転送しないようにします。

部品数・画面外クリッピング・容量の検査はありません。必ず終端を付け、部品数を64−oam_index以下にしてください。座標は256を周期に折り返し、63番の次は0番へ書き込みます。空のデータ列は何も変更しません。この形式は、スプライトライブラリのmetasprite_drawで使う個数指定のMetaSpritePart配列とは異なります。

使用例

c
fc_oam_next=__metasprite_draw(5,136,95,fc_oam_pair); // Returns 7 after two entries.

呼び出し部分のコード例です。完全なプログラムに組み込む際は、記載した初期化とバッファの条件を満たしてください。

期待する結果

操作ごとに見出し付きの行を分けたサンプルです。SETとMOVEは(144,16)・(144,32)の8×8右向き矢印、TILEは(144,48)の緑の四角、ATTRは(144,64)の上下左右を反転した矢印です。HIDEの(144,80)とCLEARの(144,112)には何も表示されません。METAは(136,96)と(144,96)の2個組で、2個目を左右反転します。NEXT 07は5・6番へ書き込んだ後の次の位置です。矢印の輪郭は赤、軸は緑、先端の一部は青です。検証では画像ごとに8,704ピクセルと転送後のOAMデータを照合します。実機では未検証です。

TRANSFERの(144,128)には矢印を表示します。このプログラムは、個別のスプライト操作で設定した全項目を含むページ02を__oam_dmaで直接転送します。

__metasprite_draw: PAGE02
PAGE02

使用例の完全なプログラム

ビルドコマンド

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\oam_intrinsics.c -I .\kitaqfc\lib -I .\kitaq-docs\samples --nes-chr=.\kitaq-docs\samples\sprite_example.chr -o .\out\oam_intrinsics.nes --no-cache --no-disasm
実装とソース

kitaqfc/lib/intrinsics.h:203

宣言の注記(原文)
Expand dx,dy,tile,attr records into page-02 shadow OAM until dx=FF; return the next index.
Coordinates and the byte OAM cursor wrap; keep the terminated stream within the available slots.
csharp
EmitHelperStart("__metasprite_draw");
            // args: oam_index, base_x, base_y, metasprite_ptr16.
            // stream: dx, dy, tile, attr ... dx=$FF terminates. Returns next OAM index in A.
            EmitAsm("LDA", Mem(CallArgBase));
            EmitAsm("ASL");
            EmitAsm("ASL");
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp2Address)); // dst byte offset
            string msLoop = NewGeneratedLabel("metasprite_loop");
            string msDone = NewGeneratedLabel("metasprite_done");
            _assembly.Add(Expr.Make(Tag.Label, msLoop));
            EmitAsm("LDY", Imm(0));
            EmitAsm("LDA", IndY(CallArgBase + 3));
            EmitAsm("CMP", Imm(0xFF));
            EmitAsm("BEQ", Rel(msDone));
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp0Address)); // dx
            EmitAsm("LDX", Mem(_runtimeIntrinsicTmp2Address));
            EmitAsm("INY");
            EmitAsm("LDA", IndY(CallArgBase + 3));
            EmitAsm("CLC");
            EmitAsm("ADC", Mem(CallArgBase + 2));
            EmitAsm("STA", new AsmOperand(OamRamBase + 0, AddressMode.AbsoluteX));
            EmitAsm("INY");
            EmitAsm("LDA", IndY(CallArgBase + 3));
            EmitAsm("STA", new AsmOperand(OamRamBase + 1, AddressMode.AbsoluteX));
            EmitAsm("INY");
            EmitAsm("LDA", IndY(CallArgBase + 3));
            EmitAsm("STA", new AsmOperand(OamRamBase + 2, AddressMode.AbsoluteX));
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("CLC");
            EmitAsm("ADC", Mem(CallArgBase + 1));
            EmitAsm("STA", new AsmOperand(OamRamBase + 3, AddressMode.AbsoluteX));
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp2Address));
            EmitAsm("CLC");
            EmitAsm("ADC", Imm(4));
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp2Address));
            EmitAsm("LDA", Mem(CallArgBase + 3));
            EmitAsm("CLC");
            EmitAsm("ADC", Imm(4));
            EmitAsm("STA", Mem(CallArgBase + 3));
            // Propagate stream-pointer page carry after consuming the complete four-byte record.
            string msPtrOk = NewGeneratedLabel("metasprite_ptr_ok");
            EmitAsm("BCC", Rel(msPtrOk));
            EmitAsm("INC", Mem(CallArgBase + 4));
            _assembly.Add(Expr.Make(Tag.Label, msPtrOk));
            EmitAsm("JMP", Abs(msLoop));
            _assembly.Add(Expr.Make(Tag.Label, msDone));
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp2Address));
            EmitAsm("LSR");
            EmitAsm("LSR");
            EmitAsm("RTS");
__mic_read2p

ファミコンの2番目のコントローラーのマイクに対応するデジタル信号を、$4016のビット2から読みます。レジスターを1回読み、0x04でマスクして、反転せず0か1に正規化します。コントローラーのラッチ、音声の録音、音量測定、音声認識は行いません。

書式

c
u8 __mic_read2p(void);

戻り値

$4016 & 0x04が0以外なら正確に1、それ以外は0です。読み取ったデジタルレベルであり、ボタンマスク、振幅、周波数、発話した単語数ではありません。

使用条件と制限

マイクは2番目のコントローラーにありますが、このヘルパーが読むのは$4017ではなく$4016です。ストローブが低いときのレジスター読み取りはコントローラーのシリアル読み取り列も進めることがあるため、別のコントローラーの8ビット読み取りループの途中にマイクの読み取りを挟まないでください。入力履歴の保存済み状態は更新しません。

公開版KUROSAKIのコントローラーモデルが返すのはD0と固定のビット6だけです。そのため通常のボタンを押していても、同エミュレーターではD1とマイクのビットは常に0になります。付属の検証で確認したのは、ビルド、通常ボタン入力、この0の信号表示です。周辺機器からの0以外の信号、電気的タイミング、実機での互換性は未確認です。ここで0が表示されても、接続機器が動作する証明や、実機でも読み取れないという証明にはなりません。

使用例

c
mic_direct=__mic_read2p(); // A separate sample of the same digital signal.

呼び出し部分のコード例です。完全なプログラムに組み込む際は、記載した初期化とバッファの条件を満たしてください。

期待する結果

通常入力のD0、拡張入力のD1、マイクのデジタルレベルを比較するプログラムです。状態が取得されるまでコントローラー1のAとコントローラー2のBを押します。NORMAL P1とNORMAL P2は001、002になります。付属のKUROSAKI実行結果では、D1 PORT 1、D1 PORT 2、EXP ALIAS 1、EXP ALIAS 2、MIC LEVEL、MIC DIRECTはすべて000です。対応する信号線がエミュレートされていないためです。取得後の表示は固定されます。適切な周辺機器と信号線の実装がある環境では実際の入力で値が変わるため、この0を期待値とせず、上記のビット配置に従って読み取ってください。

公開版KUROSAKIのコントローラーモデルが返すのはD0と固定のビット6だけです。そのため通常のボタンを押していても、同エミュレーターではD1とマイクのビットは常に0になります。付属の検証で確認したのは、ビルド、通常ボタン入力、この0の信号表示です。周辺機器からの0以外の信号、電気的タイミング、実機での互換性は未確認です。ここで0が表示されても、接続機器が動作する証明や、実機でも読み取れないという証明にはなりません。

__mic_read2p: NROM
NROM

使用例の完全なプログラム

ビルドコマンド

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\expansion_input.c -I .\kitaq-docs\samples -I .\kitaqfc\lib -o .\out\expansion_input.nes --mapper=nrom --nes-chr=.\kitaq-docs\samples\font.chr --no-cache --no-disasm
実装とソース

kitaqfc/lib/intrinsics.h:233

宣言の注記(原文)
Alias the same normalized microphone input bit as __joypad2p_voice.
__midi_clock

タイミングクロックF8を1回送信します。一定間隔で繰り返す処理は呼び出し側で行います。

書式

c
void __midi_clock(void);

戻り値

戻り値はありません。音源レジスターまたはドライバーの状態を変更します。

使用条件と制限

外付け回路が$4016のD0を送信、$4017のD4を受信へ接続する構成です。標準コントローラーだけではMIDI通信できず、内蔵APUも鳴りません。処理中はDMC再生・DMAとNMI・IRQを止め、同じポートや共有作業領域をほかの処理から触らないでください。NTSCの57サイクル/bitは約31,400bps(標準31,250bpsに対して約+0.5%)です。PAL向けのタイミング調整、実アダプターとの往復通信は未検証です。

使用例

c
// Controller-port byte encoding. This does not synthesize sound inside the FC.
#define SOUND_PORT_ONLY
#include "fc_sound_example.h"
void main(void) {
    sound_begin("MIDI PORT BYTES");
    // sound_begin has disabled DMC DMA and all internal APU voices. A compatible external circuit is required.
    // Stop NMI and mask IRQ so neither can split a software-timed serial frame.
    __ppu_ctrl_set(0); __irq_disable();
    __midi_out_byte(0x7E);
    __midi_program_change(2,5);
    __midi_control_change(2,7,100);
    __midi_note_on(2,69,96);
    __midi_note_off(2,69,0);
    __midi_clock(); __midi_start(); __midi_continue(); __midi_stop();
    // The emulator's unconnected input bit is low. Reading returns zero here.
    // A real input held high would block waiting for its start bit, without timeout.
    sound_result[0]=__midi_in_byte();
    sound_result[1]=1;
    __ppu_ctrl_set(0x80); __irq_enable();
    sound_end();
}

期待する結果

画面はEXTERNAL PORT DATAと表示し、内蔵音源は鳴りません。送信列(16進数)は7E、C2 05、B2 07 64、92 45 60、82 45 00、F8 FA FB FCです。NTSCの送信間隔57サイクル、受信データの読み取り間隔57サイクルを検査します。受信データはエミュレータの未接続入力による00で、実際の外部機器との通信成功を意味しません。

__midi_clock: NTSC
NTSC

使用例の完全なプログラム

ビルドコマンド

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\sound_midi.c -I .\kitaqfc\lib -I .\kitaq-docs\samples -o .\out\fc-midi.nes --mapper=nrom --nes-chr=.\kitaq-docs\samples\font.chr --no-cache --no-disasm
実装とソース

kitaqfc/lib/intrinsics.h:304

宣言の注記(原文)
Transmit the single realtime byte F8 synchronously; this does not schedule future ticks.

kitaqfc/kitaqfc/CodeGenerator.cs:4803

csharp
case "__midi_clock": if (!Need(0)) return true; EmitAsm("LDA", Imm(0xF8)); EmitAsm("JSR", Abs("__kq_midi_out_a")); return true;

EmitHelperStart("__kq_midi_out_a");
            bool midiScratchInZeroPage = _runtimeIntrinsicTmp0Address <= 0xFF;
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("LDA", Imm(0));
            EmitAsm("STA", Mem(0x4016));
            // Unroll so neither branch cost nor payload bits change the baud.
            // Each data edge: 44 NOP cycles + LSR zp (5) + LDA/ADC (4) + STA (4).
            for (int bit = 0; bit < 8; bit++)
            {
                for (int delay = 0; delay < (midiScratchInZeroPage ? 22 : 20); delay++) EmitAsm("NOP");
                if (!midiScratchInZeroPage) EmitAsm("BIT", Mem(CallArgBase));
                EmitAsm("LSR", Mem(_runtimeIntrinsicTmp0Address));
                EmitAsm("LDA", Imm(0));
                EmitAsm("ADC", Imm(0));
                EmitAsm("STA", Mem(0x4016));
            }
            // 51 delay cycles plus LDA/STA (6) place the stop bit 57 cycles later.
            for (int delay = 0; delay < 24; delay++) EmitAsm("NOP");
            EmitAsm("BIT", Mem(CallArgBase));
            EmitAsm("LDA", Imm(1));
            EmitAsm("STA", Mem(0x4016));
            EmitMidiBitDelay(); // Hold stop high for at least one full bit cell.
            EmitAsm("RTS");

            EmitHelperStart("__midi_out_byte");
            EmitAsm("LDA", Mem(CallArgBase));
            EmitAsm("JSR", Abs("__kq_midi_out_a"));
            EmitAsm("RTS");

            // Poll in nine-cycle steps, then sample near data-bit centers.
            // IRQ/NMI masking belongs to the caller; no timeout/framing status is returned.
            EmitHelperStart("__midi_in_byte");
            EmitAsm("LDA", Imm(0));
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp0Address));
            string waitStart = NewGeneratedLabel("midi_wait_start");
            _assembly.Add(Expr.Make(Tag.Label, waitStart));
            EmitAsm("LDA", Mem(0x4017));
            EmitAsm("AND", Imm(0x10));
            EmitAsm("BNE", Rel(waitStart));
            // 81 cycles from the start-detect read to the first data read.
            // Polling uncertainty is at most nine cycles within a 57-cycle cell.
            for (int delay = 0; delay < 35; delay++) EmitAsm("NOP");
            EmitAsm("BIT", Mem(CallArgBase));
            for (int bit = 0; bit < 8; bit++)
            {
                EmitAsm("LDA", Mem(0x4017));
                EmitAsm("AND", Imm(0x10));
                EmitAsm("CMP", Imm(0x10));
                EmitAsm("ROR", Mem(_runtimeIntrinsicTmp0Address));
                for (int delay = 0; delay < (midiScratchInZeroPage ? 22 : 20); delay++) EmitAsm("NOP");
                if (!midiScratchInZeroPage) EmitAsm("BIT", Mem(CallArgBase));
            }
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("RTS");

            // Compose the channelized note-on status, then send note and velocity arguments.
__midi_continue

リアルタイムContinueのFBを1回送ります。内部の音楽ドライバーは操作しません。

書式

c
void __midi_continue(void);

戻り値

戻り値はありません。音源レジスターまたはドライバーの状態を変更します。

使用条件と制限

外付け回路が$4016のD0を送信、$4017のD4を受信へ接続する構成です。標準コントローラーだけではMIDI通信できず、内蔵APUも鳴りません。処理中はDMC再生・DMAとNMI・IRQを止め、同じポートや共有作業領域をほかの処理から触らないでください。NTSCの57サイクル/bitは約31,400bps(標準31,250bpsに対して約+0.5%)です。PAL向けのタイミング調整、実アダプターとの往復通信は未検証です。

使用例

c
// Controller-port byte encoding. This does not synthesize sound inside the FC.
#define SOUND_PORT_ONLY
#include "fc_sound_example.h"
void main(void) {
    sound_begin("MIDI PORT BYTES");
    // sound_begin has disabled DMC DMA and all internal APU voices. A compatible external circuit is required.
    // Stop NMI and mask IRQ so neither can split a software-timed serial frame.
    __ppu_ctrl_set(0); __irq_disable();
    __midi_out_byte(0x7E);
    __midi_program_change(2,5);
    __midi_control_change(2,7,100);
    __midi_note_on(2,69,96);
    __midi_note_off(2,69,0);
    __midi_clock(); __midi_start(); __midi_continue(); __midi_stop();
    // The emulator's unconnected input bit is low. Reading returns zero here.
    // A real input held high would block waiting for its start bit, without timeout.
    sound_result[0]=__midi_in_byte();
    sound_result[1]=1;
    __ppu_ctrl_set(0x80); __irq_enable();
    sound_end();
}

期待する結果

画面はEXTERNAL PORT DATAと表示し、内蔵音源は鳴りません。送信列(16進数)は7E、C2 05、B2 07 64、92 45 60、82 45 00、F8 FA FB FCです。NTSCの送信間隔57サイクル、受信データの読み取り間隔57サイクルを検査します。受信データはエミュレータの未接続入力による00で、実際の外部機器との通信成功を意味しません。

__midi_continue: NTSC
NTSC

使用例の完全なプログラム

ビルドコマンド

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\sound_midi.c -I .\kitaqfc\lib -I .\kitaq-docs\samples -o .\out\fc-midi.nes --mapper=nrom --nes-chr=.\kitaq-docs\samples\font.chr --no-cache --no-disasm
実装とソース

kitaqfc/lib/intrinsics.h:308

宣言の注記(原文)
Transmit realtime Continue (FB) through the serial adapter path.

kitaqfc/kitaqfc/CodeGenerator.cs:4805

csharp
case "__midi_continue": if (!Need(0)) return true; EmitAsm("LDA", Imm(0xFB)); EmitAsm("JSR", Abs("__kq_midi_out_a")); return true;

EmitHelperStart("__kq_midi_out_a");
            bool midiScratchInZeroPage = _runtimeIntrinsicTmp0Address <= 0xFF;
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("LDA", Imm(0));
            EmitAsm("STA", Mem(0x4016));
            // Unroll so neither branch cost nor payload bits change the baud.
            // Each data edge: 44 NOP cycles + LSR zp (5) + LDA/ADC (4) + STA (4).
            for (int bit = 0; bit < 8; bit++)
            {
                for (int delay = 0; delay < (midiScratchInZeroPage ? 22 : 20); delay++) EmitAsm("NOP");
                if (!midiScratchInZeroPage) EmitAsm("BIT", Mem(CallArgBase));
                EmitAsm("LSR", Mem(_runtimeIntrinsicTmp0Address));
                EmitAsm("LDA", Imm(0));
                EmitAsm("ADC", Imm(0));
                EmitAsm("STA", Mem(0x4016));
            }
            // 51 delay cycles plus LDA/STA (6) place the stop bit 57 cycles later.
            for (int delay = 0; delay < 24; delay++) EmitAsm("NOP");
            EmitAsm("BIT", Mem(CallArgBase));
            EmitAsm("LDA", Imm(1));
            EmitAsm("STA", Mem(0x4016));
            EmitMidiBitDelay(); // Hold stop high for at least one full bit cell.
            EmitAsm("RTS");

            EmitHelperStart("__midi_out_byte");
            EmitAsm("LDA", Mem(CallArgBase));
            EmitAsm("JSR", Abs("__kq_midi_out_a"));
            EmitAsm("RTS");

            // Poll in nine-cycle steps, then sample near data-bit centers.
            // IRQ/NMI masking belongs to the caller; no timeout/framing status is returned.
            EmitHelperStart("__midi_in_byte");
            EmitAsm("LDA", Imm(0));
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp0Address));
            string waitStart = NewGeneratedLabel("midi_wait_start");
            _assembly.Add(Expr.Make(Tag.Label, waitStart));
            EmitAsm("LDA", Mem(0x4017));
            EmitAsm("AND", Imm(0x10));
            EmitAsm("BNE", Rel(waitStart));
            // 81 cycles from the start-detect read to the first data read.
            // Polling uncertainty is at most nine cycles within a 57-cycle cell.
            for (int delay = 0; delay < 35; delay++) EmitAsm("NOP");
            EmitAsm("BIT", Mem(CallArgBase));
            for (int bit = 0; bit < 8; bit++)
            {
                EmitAsm("LDA", Mem(0x4017));
                EmitAsm("AND", Imm(0x10));
                EmitAsm("CMP", Imm(0x10));
                EmitAsm("ROR", Mem(_runtimeIntrinsicTmp0Address));
                for (int delay = 0; delay < (midiScratchInZeroPage ? 22 : 20); delay++) EmitAsm("NOP");
                if (!midiScratchInZeroPage) EmitAsm("BIT", Mem(CallArgBase));
            }
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("RTS");

            // Compose the channelized note-on status, then send note and velocity arguments.
__midi_control_change

0xB0 | (ch & 0x0F)、コントローラー番号、値の3バイトを送ります。

書式

c
void __midi_control_change(u8 ch, u8 cc, u8 value);

引数

ch

MIDIチャンネル0~15。下位4ビットをステータスへ入れます。0が一般的な表示のチャンネル1です。

cc

コントローラー番号0~127。7は一般的なチャンネル音量です。

value

コントローラーの値0~127。上位ビットは自動で消しません。

戻り値

戻り値はありません。音源レジスターまたはドライバーの状態を変更します。

使用条件と制限

外付け回路が$4016のD0を送信、$4017のD4を受信へ接続する構成です。標準コントローラーだけではMIDI通信できず、内蔵APUも鳴りません。処理中はDMC再生・DMAとNMI・IRQを止め、同じポートや共有作業領域をほかの処理から触らないでください。NTSCの57サイクル/bitは約31,400bps(標準31,250bpsに対して約+0.5%)です。PAL向けのタイミング調整、実アダプターとの往復通信は未検証です。

使用例

c
// Controller-port byte encoding. This does not synthesize sound inside the FC.
#define SOUND_PORT_ONLY
#include "fc_sound_example.h"
void main(void) {
    sound_begin("MIDI PORT BYTES");
    // sound_begin has disabled DMC DMA and all internal APU voices. A compatible external circuit is required.
    // Stop NMI and mask IRQ so neither can split a software-timed serial frame.
    __ppu_ctrl_set(0); __irq_disable();
    __midi_out_byte(0x7E);
    __midi_program_change(2,5);
    __midi_control_change(2,7,100);
    __midi_note_on(2,69,96);
    __midi_note_off(2,69,0);
    __midi_clock(); __midi_start(); __midi_continue(); __midi_stop();
    // The emulator's unconnected input bit is low. Reading returns zero here.
    // A real input held high would block waiting for its start bit, without timeout.
    sound_result[0]=__midi_in_byte();
    sound_result[1]=1;
    __ppu_ctrl_set(0x80); __irq_enable();
    sound_end();
}

期待する結果

画面はEXTERNAL PORT DATAと表示し、内蔵音源は鳴りません。送信列(16進数)は7E、C2 05、B2 07 64、92 45 60、82 45 00、F8 FA FB FCです。NTSCの送信間隔57サイクル、受信データの読み取り間隔57サイクルを検査します。受信データはエミュレータの未接続入力による00で、実際の外部機器との通信成功を意味しません。

__midi_control_change: NTSC
NTSC

使用例の完全なプログラム

ビルドコマンド

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\sound_midi.c -I .\kitaqfc\lib -I .\kitaq-docs\samples -o .\out\fc-midi.nes --mapper=nrom --nes-chr=.\kitaq-docs\samples\font.chr --no-cache --no-disasm
実装とソース

kitaqfc/lib/intrinsics.h:300

宣言の注記(原文)
Send B0|(ch&0F), controller and value; both data arguments must be valid 7-bit values.

kitaqfc/kitaqfc/CodeGenerator.cs:4800

csharp
case "__midi_control_change": return EmitKitaqfcHelperCall(funcName, args, ctx, new[] {1,1,1}, funcName);

EmitHelperStart("__midi_control_change");
            EmitAsm("LDA", Mem(CallArgBase));
            EmitAsm("AND", Imm(0x0F));
            EmitAsm("ORA", Imm(0xB0));
            EmitAsm("JSR", Abs("__kq_midi_out_a"));
            EmitAsm("LDA", Mem(CallArgBase + 1));
            EmitAsm("JSR", Abs("__kq_midi_out_a"));
            EmitAsm("LDA", Mem(CallArgBase + 2));
            EmitAsm("JSR", Abs("__kq_midi_out_a"));
            EmitAsm("RTS");

EmitHelperStart("__kq_midi_out_a");
            bool midiScratchInZeroPage = _runtimeIntrinsicTmp0Address <= 0xFF;
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("LDA", Imm(0));
            EmitAsm("STA", Mem(0x4016));
            // Unroll so neither branch cost nor payload bits change the baud.
            // Each data edge: 44 NOP cycles + LSR zp (5) + LDA/ADC (4) + STA (4).
            for (int bit = 0; bit < 8; bit++)
            {
                for (int delay = 0; delay < (midiScratchInZeroPage ? 22 : 20); delay++) EmitAsm("NOP");
                if (!midiScratchInZeroPage) EmitAsm("BIT", Mem(CallArgBase));
                EmitAsm("LSR", Mem(_runtimeIntrinsicTmp0Address));
                EmitAsm("LDA", Imm(0));
                EmitAsm("ADC", Imm(0));
                EmitAsm("STA", Mem(0x4016));
            }
            // 51 delay cycles plus LDA/STA (6) place the stop bit 57 cycles later.
            for (int delay = 0; delay < 24; delay++) EmitAsm("NOP");
            EmitAsm("BIT", Mem(CallArgBase));
            EmitAsm("LDA", Imm(1));
            EmitAsm("STA", Mem(0x4016));
            EmitMidiBitDelay(); // Hold stop high for at least one full bit cell.
            EmitAsm("RTS");

            EmitHelperStart("__midi_out_byte");
            EmitAsm("LDA", Mem(CallArgBase));
            EmitAsm("JSR", Abs("__kq_midi_out_a"));
            EmitAsm("RTS");

            // Poll in nine-cycle steps, then sample near data-bit centers.
            // IRQ/NMI masking belongs to the caller; no timeout/framing status is returned.
            EmitHelperStart("__midi_in_byte");
            EmitAsm("LDA", Imm(0));
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp0Address));
            string waitStart = NewGeneratedLabel("midi_wait_start");
            _assembly.Add(Expr.Make(Tag.Label, waitStart));
            EmitAsm("LDA", Mem(0x4017));
            EmitAsm("AND", Imm(0x10));
            EmitAsm("BNE", Rel(waitStart));
            // 81 cycles from the start-detect read to the first data read.
            // Polling uncertainty is at most nine cycles within a 57-cycle cell.
            for (int delay = 0; delay < 35; delay++) EmitAsm("NOP");
            EmitAsm("BIT", Mem(CallArgBase));
            for (int bit = 0; bit < 8; bit++)
            {
                EmitAsm("LDA", Mem(0x4017));
                EmitAsm("AND", Imm(0x10));
                EmitAsm("CMP", Imm(0x10));
                EmitAsm("ROR", Mem(_runtimeIntrinsicTmp0Address));
                for (int delay = 0; delay < (midiScratchInZeroPage ? 22 : 20); delay++) EmitAsm("NOP");
                if (!midiScratchInZeroPage) EmitAsm("BIT", Mem(CallArgBase));
            }
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("RTS");

            // Compose the channelized note-on status, then send note and velocity arguments.
__midi_in_byte

受信入力が0になるまで待ち、8ビットを時間間隔を空けて読み取ります。

書式

c
u8 __midi_in_byte(void);

戻り値

読み取ったu8を返します。タイムアウト・フレーミング検査・受信バッファはありません。入力が高いままだと戻らず、低いままだと0を読み取ります。

使用条件と制限

外付け回路が$4016のD0を送信、$4017のD4を受信へ接続する構成です。標準コントローラーだけではMIDI通信できず、内蔵APUも鳴りません。処理中はDMC再生・DMAとNMI・IRQを止め、同じポートや共有作業領域をほかの処理から触らないでください。NTSCの57サイクル/bitは約31,400bps(標準31,250bpsに対して約+0.5%)です。PAL向けのタイミング調整、実アダプターとの往復通信は未検証です。

使用例

c
// Controller-port byte encoding. This does not synthesize sound inside the FC.
#define SOUND_PORT_ONLY
#include "fc_sound_example.h"
void main(void) {
    sound_begin("MIDI PORT BYTES");
    // sound_begin has disabled DMC DMA and all internal APU voices. A compatible external circuit is required.
    // Stop NMI and mask IRQ so neither can split a software-timed serial frame.
    __ppu_ctrl_set(0); __irq_disable();
    __midi_out_byte(0x7E);
    __midi_program_change(2,5);
    __midi_control_change(2,7,100);
    __midi_note_on(2,69,96);
    __midi_note_off(2,69,0);
    __midi_clock(); __midi_start(); __midi_continue(); __midi_stop();
    // The emulator's unconnected input bit is low. Reading returns zero here.
    // A real input held high would block waiting for its start bit, without timeout.
    sound_result[0]=__midi_in_byte();
    sound_result[1]=1;
    __ppu_ctrl_set(0x80); __irq_enable();
    sound_end();
}

期待する結果

画面はEXTERNAL PORT DATAと表示し、内蔵音源は鳴りません。送信列(16進数)は7E、C2 05、B2 07 64、92 45 60、82 45 00、F8 FA FB FCです。NTSCの送信間隔57サイクル、受信データの読み取り間隔57サイクルを検査します。受信データはエミュレータの未接続入力による00で、実際の外部機器との通信成功を意味しません。

__midi_in_byte: NTSC
NTSC

使用例の完全なプログラム

ビルドコマンド

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\sound_midi.c -I .\kitaqfc\lib -I .\kitaq-docs\samples -o .\out\fc-midi.nes --mapper=nrom --nes-chr=.\kitaq-docs\samples\font.chr --no-cache --no-disasm
実装とソース

kitaqfc/lib/intrinsics.h:294

宣言の注記(原文)
Block for a low start signal and sample eight bits. No timeout, framing check or receive buffer is provided.

kitaqfc/kitaqfc/CodeGenerator.cs:4796

csharp
case "__midi_in_byte": if (!Need(0)) return true; EmitAsm("JSR", Abs("__midi_in_byte")); return true;
                // Pack MIDI message fields as bytes; real-time commands below load their fixed status byte directly.

EmitHelperStart("__midi_in_byte");
            EmitAsm("LDA", Imm(0));
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp0Address));
            string waitStart = NewGeneratedLabel("midi_wait_start");
            _assembly.Add(Expr.Make(Tag.Label, waitStart));
            EmitAsm("LDA", Mem(0x4017));
            EmitAsm("AND", Imm(0x10));
            EmitAsm("BNE", Rel(waitStart));
            // 81 cycles from the start-detect read to the first data read.
            // Polling uncertainty is at most nine cycles within a 57-cycle cell.
            for (int delay = 0; delay < 35; delay++) EmitAsm("NOP");
            EmitAsm("BIT", Mem(CallArgBase));
            for (int bit = 0; bit < 8; bit++)
            {
                EmitAsm("LDA", Mem(0x4017));
                EmitAsm("AND", Imm(0x10));
                EmitAsm("CMP", Imm(0x10));
                EmitAsm("ROR", Mem(_runtimeIntrinsicTmp0Address));
                for (int delay = 0; delay < (midiScratchInZeroPage ? 22 : 20); delay++) EmitAsm("NOP");
                if (!midiScratchInZeroPage) EmitAsm("BIT", Mem(CallArgBase));
            }
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("RTS");

EmitHelperStart("__kq_midi_out_a");
            bool midiScratchInZeroPage = _runtimeIntrinsicTmp0Address <= 0xFF;
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("LDA", Imm(0));
            EmitAsm("STA", Mem(0x4016));
            // Unroll so neither branch cost nor payload bits change the baud.
            // Each data edge: 44 NOP cycles + LSR zp (5) + LDA/ADC (4) + STA (4).
            for (int bit = 0; bit < 8; bit++)
            {
                for (int delay = 0; delay < (midiScratchInZeroPage ? 22 : 20); delay++) EmitAsm("NOP");
                if (!midiScratchInZeroPage) EmitAsm("BIT", Mem(CallArgBase));
                EmitAsm("LSR", Mem(_runtimeIntrinsicTmp0Address));
                EmitAsm("LDA", Imm(0));
                EmitAsm("ADC", Imm(0));
                EmitAsm("STA", Mem(0x4016));
            }
            // 51 delay cycles plus LDA/STA (6) place the stop bit 57 cycles later.
            for (int delay = 0; delay < 24; delay++) EmitAsm("NOP");
            EmitAsm("BIT", Mem(CallArgBase));
            EmitAsm("LDA", Imm(1));
            EmitAsm("STA", Mem(0x4016));
            EmitMidiBitDelay(); // Hold stop high for at least one full bit cell.
            EmitAsm("RTS");

            EmitHelperStart("__midi_out_byte");
            EmitAsm("LDA", Mem(CallArgBase));
            EmitAsm("JSR", Abs("__kq_midi_out_a"));
            EmitAsm("RTS");

            // Poll in nine-cycle steps, then sample near data-bit centers.
            // IRQ/NMI masking belongs to the caller; no timeout/framing status is returned.
            EmitHelperStart("__midi_in_byte");
            EmitAsm("LDA", Imm(0));
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp0Address));
            string waitStart = NewGeneratedLabel("midi_wait_start");
            _assembly.Add(Expr.Make(Tag.Label, waitStart));
            EmitAsm("LDA", Mem(0x4017));
            EmitAsm("AND", Imm(0x10));
            EmitAsm("BNE", Rel(waitStart));
            // 81 cycles from the start-detect read to the first data read.
            // Polling uncertainty is at most nine cycles within a 57-cycle cell.
            for (int delay = 0; delay < 35; delay++) EmitAsm("NOP");
            EmitAsm("BIT", Mem(CallArgBase));
            for (int bit = 0; bit < 8; bit++)
            {
                EmitAsm("LDA", Mem(0x4017));
                EmitAsm("AND", Imm(0x10));
                EmitAsm("CMP", Imm(0x10));
                EmitAsm("ROR", Mem(_runtimeIntrinsicTmp0Address));
                for (int delay = 0; delay < (midiScratchInZeroPage ? 22 : 20); delay++) EmitAsm("NOP");
                if (!midiScratchInZeroPage) EmitAsm("BIT", Mem(CallArgBase));
            }
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("RTS");

            // Compose the channelized note-on status, then send note and velocity arguments.
__midi_note_off

0x80 | (ch & 0x0F)、note、velocityの3バイトでノートオフを送ります。

書式

c
void __midi_note_off(u8 ch, u8 note, u8 velocity);

引数

ch

MIDIチャンネル0~15。下位4ビットをステータスへ入れます。0が一般的な表示のチャンネル1です。

note

MIDIノート番号0~127。69はA4です。値を7ビットへ制限しないため、範囲は呼び出し側で保証します。

velocity

ベロシティ0~127。7ビットに自動制限しません。

戻り値

戻り値はありません。音源レジスターまたはドライバーの状態を変更します。

使用条件と制限

外付け回路が$4016のD0を送信、$4017のD4を受信へ接続する構成です。標準コントローラーだけではMIDI通信できず、内蔵APUも鳴りません。処理中はDMC再生・DMAとNMI・IRQを止め、同じポートや共有作業領域をほかの処理から触らないでください。NTSCの57サイクル/bitは約31,400bps(標準31,250bpsに対して約+0.5%)です。PAL向けのタイミング調整、実アダプターとの往復通信は未検証です。

使用例

c
// Controller-port byte encoding. This does not synthesize sound inside the FC.
#define SOUND_PORT_ONLY
#include "fc_sound_example.h"
void main(void) {
    sound_begin("MIDI PORT BYTES");
    // sound_begin has disabled DMC DMA and all internal APU voices. A compatible external circuit is required.
    // Stop NMI and mask IRQ so neither can split a software-timed serial frame.
    __ppu_ctrl_set(0); __irq_disable();
    __midi_out_byte(0x7E);
    __midi_program_change(2,5);
    __midi_control_change(2,7,100);
    __midi_note_on(2,69,96);
    __midi_note_off(2,69,0);
    __midi_clock(); __midi_start(); __midi_continue(); __midi_stop();
    // The emulator's unconnected input bit is low. Reading returns zero here.
    // A real input held high would block waiting for its start bit, without timeout.
    sound_result[0]=__midi_in_byte();
    sound_result[1]=1;
    __ppu_ctrl_set(0x80); __irq_enable();
    sound_end();
}

期待する結果

画面はEXTERNAL PORT DATAと表示し、内蔵音源は鳴りません。送信列(16進数)は7E、C2 05、B2 07 64、92 45 60、82 45 00、F8 FA FB FCです。NTSCの送信間隔57サイクル、受信データの読み取り間隔57サイクルを検査します。受信データはエミュレータの未接続入力による00で、実際の外部機器との通信成功を意味しません。

__midi_note_off: NTSC
NTSC

使用例の完全なプログラム

ビルドコマンド

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\sound_midi.c -I .\kitaqfc\lib -I .\kitaq-docs\samples -o .\out\fc-midi.nes --mapper=nrom --nes-chr=.\kitaq-docs\samples\font.chr --no-cache --no-disasm
実装とソース

kitaqfc/lib/intrinsics.h:298

宣言の注記(原文)
Send 80|(ch&0F), note and velocity through the same synchronous serial path.

kitaqfc/kitaqfc/CodeGenerator.cs:4799

csharp
case "__midi_note_off": return EmitKitaqfcHelperCall(funcName, args, ctx, new[] {1,1,1}, funcName);

EmitHelperStart("__midi_note_off");
            EmitAsm("LDA", Mem(CallArgBase));
            EmitAsm("AND", Imm(0x0F));
            EmitAsm("ORA", Imm(0x80));
            EmitAsm("JSR", Abs("__kq_midi_out_a"));
            EmitAsm("LDA", Mem(CallArgBase + 1));
            EmitAsm("JSR", Abs("__kq_midi_out_a"));
            EmitAsm("LDA", Mem(CallArgBase + 2));
            EmitAsm("JSR", Abs("__kq_midi_out_a"));
            EmitAsm("RTS");

EmitHelperStart("__kq_midi_out_a");
            bool midiScratchInZeroPage = _runtimeIntrinsicTmp0Address <= 0xFF;
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("LDA", Imm(0));
            EmitAsm("STA", Mem(0x4016));
            // Unroll so neither branch cost nor payload bits change the baud.
            // Each data edge: 44 NOP cycles + LSR zp (5) + LDA/ADC (4) + STA (4).
            for (int bit = 0; bit < 8; bit++)
            {
                for (int delay = 0; delay < (midiScratchInZeroPage ? 22 : 20); delay++) EmitAsm("NOP");
                if (!midiScratchInZeroPage) EmitAsm("BIT", Mem(CallArgBase));
                EmitAsm("LSR", Mem(_runtimeIntrinsicTmp0Address));
                EmitAsm("LDA", Imm(0));
                EmitAsm("ADC", Imm(0));
                EmitAsm("STA", Mem(0x4016));
            }
            // 51 delay cycles plus LDA/STA (6) place the stop bit 57 cycles later.
            for (int delay = 0; delay < 24; delay++) EmitAsm("NOP");
            EmitAsm("BIT", Mem(CallArgBase));
            EmitAsm("LDA", Imm(1));
            EmitAsm("STA", Mem(0x4016));
            EmitMidiBitDelay(); // Hold stop high for at least one full bit cell.
            EmitAsm("RTS");

            EmitHelperStart("__midi_out_byte");
            EmitAsm("LDA", Mem(CallArgBase));
            EmitAsm("JSR", Abs("__kq_midi_out_a"));
            EmitAsm("RTS");

            // Poll in nine-cycle steps, then sample near data-bit centers.
            // IRQ/NMI masking belongs to the caller; no timeout/framing status is returned.
            EmitHelperStart("__midi_in_byte");
            EmitAsm("LDA", Imm(0));
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp0Address));
            string waitStart = NewGeneratedLabel("midi_wait_start");
            _assembly.Add(Expr.Make(Tag.Label, waitStart));
            EmitAsm("LDA", Mem(0x4017));
            EmitAsm("AND", Imm(0x10));
            EmitAsm("BNE", Rel(waitStart));
            // 81 cycles from the start-detect read to the first data read.
            // Polling uncertainty is at most nine cycles within a 57-cycle cell.
            for (int delay = 0; delay < 35; delay++) EmitAsm("NOP");
            EmitAsm("BIT", Mem(CallArgBase));
            for (int bit = 0; bit < 8; bit++)
            {
                EmitAsm("LDA", Mem(0x4017));
                EmitAsm("AND", Imm(0x10));
                EmitAsm("CMP", Imm(0x10));
                EmitAsm("ROR", Mem(_runtimeIntrinsicTmp0Address));
                for (int delay = 0; delay < (midiScratchInZeroPage ? 22 : 20); delay++) EmitAsm("NOP");
                if (!midiScratchInZeroPage) EmitAsm("BIT", Mem(CallArgBase));
            }
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("RTS");

            // Compose the channelized note-on status, then send note and velocity arguments.
__midi_note_on

0x90 | (ch & 0x0F)、note、velocityの3バイトでノートオンを送ります。

書式

c
void __midi_note_on(u8 ch, u8 note, u8 velocity);

引数

ch

MIDIチャンネル0~15。下位4ビットをステータスへ入れます。0が一般的な表示のチャンネル1です。

note

MIDIノート番号0~127。69はA4です。値を7ビットへ制限しないため、範囲は呼び出し側で保証します。

velocity

ベロシティ0~127。7ビットに自動制限しません。

戻り値

戻り値はありません。音源レジスターまたはドライバーの状態を変更します。

使用条件と制限

外付け回路が$4016のD0を送信、$4017のD4を受信へ接続する構成です。標準コントローラーだけではMIDI通信できず、内蔵APUも鳴りません。処理中はDMC再生・DMAとNMI・IRQを止め、同じポートや共有作業領域をほかの処理から触らないでください。NTSCの57サイクル/bitは約31,400bps(標準31,250bpsに対して約+0.5%)です。PAL向けのタイミング調整、実アダプターとの往復通信は未検証です。

使用例

c
// Controller-port byte encoding. This does not synthesize sound inside the FC.
#define SOUND_PORT_ONLY
#include "fc_sound_example.h"
void main(void) {
    sound_begin("MIDI PORT BYTES");
    // sound_begin has disabled DMC DMA and all internal APU voices. A compatible external circuit is required.
    // Stop NMI and mask IRQ so neither can split a software-timed serial frame.
    __ppu_ctrl_set(0); __irq_disable();
    __midi_out_byte(0x7E);
    __midi_program_change(2,5);
    __midi_control_change(2,7,100);
    __midi_note_on(2,69,96);
    __midi_note_off(2,69,0);
    __midi_clock(); __midi_start(); __midi_continue(); __midi_stop();
    // The emulator's unconnected input bit is low. Reading returns zero here.
    // A real input held high would block waiting for its start bit, without timeout.
    sound_result[0]=__midi_in_byte();
    sound_result[1]=1;
    __ppu_ctrl_set(0x80); __irq_enable();
    sound_end();
}

期待する結果

画面はEXTERNAL PORT DATAと表示し、内蔵音源は鳴りません。送信列(16進数)は7E、C2 05、B2 07 64、92 45 60、82 45 00、F8 FA FB FCです。NTSCの送信間隔57サイクル、受信データの読み取り間隔57サイクルを検査します。受信データはエミュレータの未接続入力による00で、実際の外部機器との通信成功を意味しません。

__midi_note_on: NTSC
NTSC

使用例の完全なプログラム

ビルドコマンド

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\sound_midi.c -I .\kitaqfc\lib -I .\kitaq-docs\samples -o .\out\fc-midi.nes --mapper=nrom --nes-chr=.\kitaq-docs\samples\font.chr --no-cache --no-disasm
実装とソース

kitaqfc/lib/intrinsics.h:296

宣言の注記(原文)
Send 90|(ch&0F), note and velocity. Supply 7-bit data bytes; they are not masked by this helper.

kitaqfc/kitaqfc/CodeGenerator.cs:4798

csharp
case "__midi_note_on": return EmitKitaqfcHelperCall(funcName, args, ctx, new[] {1,1,1}, funcName);

EmitHelperStart("__midi_note_on");
            EmitAsm("LDA", Mem(CallArgBase));
            EmitAsm("AND", Imm(0x0F));
            EmitAsm("ORA", Imm(0x90));
            EmitAsm("JSR", Abs("__kq_midi_out_a"));
            EmitAsm("LDA", Mem(CallArgBase + 1));
            EmitAsm("JSR", Abs("__kq_midi_out_a"));
            EmitAsm("LDA", Mem(CallArgBase + 2));
            EmitAsm("JSR", Abs("__kq_midi_out_a"));
            EmitAsm("RTS");

EmitHelperStart("__kq_midi_out_a");
            bool midiScratchInZeroPage = _runtimeIntrinsicTmp0Address <= 0xFF;
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("LDA", Imm(0));
            EmitAsm("STA", Mem(0x4016));
            // Unroll so neither branch cost nor payload bits change the baud.
            // Each data edge: 44 NOP cycles + LSR zp (5) + LDA/ADC (4) + STA (4).
            for (int bit = 0; bit < 8; bit++)
            {
                for (int delay = 0; delay < (midiScratchInZeroPage ? 22 : 20); delay++) EmitAsm("NOP");
                if (!midiScratchInZeroPage) EmitAsm("BIT", Mem(CallArgBase));
                EmitAsm("LSR", Mem(_runtimeIntrinsicTmp0Address));
                EmitAsm("LDA", Imm(0));
                EmitAsm("ADC", Imm(0));
                EmitAsm("STA", Mem(0x4016));
            }
            // 51 delay cycles plus LDA/STA (6) place the stop bit 57 cycles later.
            for (int delay = 0; delay < 24; delay++) EmitAsm("NOP");
            EmitAsm("BIT", Mem(CallArgBase));
            EmitAsm("LDA", Imm(1));
            EmitAsm("STA", Mem(0x4016));
            EmitMidiBitDelay(); // Hold stop high for at least one full bit cell.
            EmitAsm("RTS");

            EmitHelperStart("__midi_out_byte");
            EmitAsm("LDA", Mem(CallArgBase));
            EmitAsm("JSR", Abs("__kq_midi_out_a"));
            EmitAsm("RTS");

            // Poll in nine-cycle steps, then sample near data-bit centers.
            // IRQ/NMI masking belongs to the caller; no timeout/framing status is returned.
            EmitHelperStart("__midi_in_byte");
            EmitAsm("LDA", Imm(0));
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp0Address));
            string waitStart = NewGeneratedLabel("midi_wait_start");
            _assembly.Add(Expr.Make(Tag.Label, waitStart));
            EmitAsm("LDA", Mem(0x4017));
            EmitAsm("AND", Imm(0x10));
            EmitAsm("BNE", Rel(waitStart));
            // 81 cycles from the start-detect read to the first data read.
            // Polling uncertainty is at most nine cycles within a 57-cycle cell.
            for (int delay = 0; delay < 35; delay++) EmitAsm("NOP");
            EmitAsm("BIT", Mem(CallArgBase));
            for (int bit = 0; bit < 8; bit++)
            {
                EmitAsm("LDA", Mem(0x4017));
                EmitAsm("AND", Imm(0x10));
                EmitAsm("CMP", Imm(0x10));
                EmitAsm("ROR", Mem(_runtimeIntrinsicTmp0Address));
                for (int delay = 0; delay < (midiScratchInZeroPage ? 22 : 20); delay++) EmitAsm("NOP");
                if (!midiScratchInZeroPage) EmitAsm("BIT", Mem(CallArgBase));
            }
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("RTS");

            // Compose the channelized note-on status, then send note and velocity arguments.
__midi_out_byte

スタート0、下位ビットから8データビット、ストップ1の順で1バイトを同期送信します。

書式

c
void __midi_out_byte(u8 byte);

引数

byte

送信する1バイト。生のステータスやデータを含め、0~255をそのまま送ります。

戻り値

戻り値はありません。音源レジスターまたはドライバーの状態を変更します。

使用条件と制限

外付け回路が$4016のD0を送信、$4017のD4を受信へ接続する構成です。標準コントローラーだけではMIDI通信できず、内蔵APUも鳴りません。処理中はDMC再生・DMAとNMI・IRQを止め、同じポートや共有作業領域をほかの処理から触らないでください。NTSCの57サイクル/bitは約31,400bps(標準31,250bpsに対して約+0.5%)です。PAL向けのタイミング調整、実アダプターとの往復通信は未検証です。

使用例

c
// Controller-port byte encoding. This does not synthesize sound inside the FC.
#define SOUND_PORT_ONLY
#include "fc_sound_example.h"
void main(void) {
    sound_begin("MIDI PORT BYTES");
    // sound_begin has disabled DMC DMA and all internal APU voices. A compatible external circuit is required.
    // Stop NMI and mask IRQ so neither can split a software-timed serial frame.
    __ppu_ctrl_set(0); __irq_disable();
    __midi_out_byte(0x7E);
    __midi_program_change(2,5);
    __midi_control_change(2,7,100);
    __midi_note_on(2,69,96);
    __midi_note_off(2,69,0);
    __midi_clock(); __midi_start(); __midi_continue(); __midi_stop();
    // The emulator's unconnected input bit is low. Reading returns zero here.
    // A real input held high would block waiting for its start bit, without timeout.
    sound_result[0]=__midi_in_byte();
    sound_result[1]=1;
    __ppu_ctrl_set(0x80); __irq_enable();
    sound_end();
}

期待する結果

画面はEXTERNAL PORT DATAと表示し、内蔵音源は鳴りません。送信列(16進数)は7E、C2 05、B2 07 64、92 45 60、82 45 00、F8 FA FB FCです。NTSCの送信間隔57サイクル、受信データの読み取り間隔57サイクルを検査します。受信データはエミュレータの未接続入力による00で、実際の外部機器との通信成功を意味しません。

__midi_out_byte: NTSC
NTSC

使用例の完全なプログラム

ビルドコマンド

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\sound_midi.c -I .\kitaqfc\lib -I .\kitaq-docs\samples -o .\out\fc-midi.nes --mapper=nrom --nes-chr=.\kitaq-docs\samples\font.chr --no-cache --no-disasm
実装とソース

kitaqfc/lib/intrinsics.h:292

宣言の注記(原文)
Send start-low, eight least-significant-first bits and stop-high in 57-cycle
NTSC bit cells (about 31400 bps). Stop DMC DMA and mask IRQ/NMI during transfers;
PAL timing and electrical adapter compatibility are not established here.

kitaqfc/kitaqfc/CodeGenerator.cs:4795

csharp
case "__midi_out_byte": return EmitKitaqfcHelperCall(funcName, args, ctx, new[] {1}, funcName);

EmitHelperStart("__midi_out_byte");
            EmitAsm("LDA", Mem(CallArgBase));
            EmitAsm("JSR", Abs("__kq_midi_out_a"));
            EmitAsm("RTS");

EmitHelperStart("__kq_midi_out_a");
            bool midiScratchInZeroPage = _runtimeIntrinsicTmp0Address <= 0xFF;
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("LDA", Imm(0));
            EmitAsm("STA", Mem(0x4016));
            // Unroll so neither branch cost nor payload bits change the baud.
            // Each data edge: 44 NOP cycles + LSR zp (5) + LDA/ADC (4) + STA (4).
            for (int bit = 0; bit < 8; bit++)
            {
                for (int delay = 0; delay < (midiScratchInZeroPage ? 22 : 20); delay++) EmitAsm("NOP");
                if (!midiScratchInZeroPage) EmitAsm("BIT", Mem(CallArgBase));
                EmitAsm("LSR", Mem(_runtimeIntrinsicTmp0Address));
                EmitAsm("LDA", Imm(0));
                EmitAsm("ADC", Imm(0));
                EmitAsm("STA", Mem(0x4016));
            }
            // 51 delay cycles plus LDA/STA (6) place the stop bit 57 cycles later.
            for (int delay = 0; delay < 24; delay++) EmitAsm("NOP");
            EmitAsm("BIT", Mem(CallArgBase));
            EmitAsm("LDA", Imm(1));
            EmitAsm("STA", Mem(0x4016));
            EmitMidiBitDelay(); // Hold stop high for at least one full bit cell.
            EmitAsm("RTS");

            EmitHelperStart("__midi_out_byte");
            EmitAsm("LDA", Mem(CallArgBase));
            EmitAsm("JSR", Abs("__kq_midi_out_a"));
            EmitAsm("RTS");

            // Poll in nine-cycle steps, then sample near data-bit centers.
            // IRQ/NMI masking belongs to the caller; no timeout/framing status is returned.
            EmitHelperStart("__midi_in_byte");
            EmitAsm("LDA", Imm(0));
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp0Address));
            string waitStart = NewGeneratedLabel("midi_wait_start");
            _assembly.Add(Expr.Make(Tag.Label, waitStart));
            EmitAsm("LDA", Mem(0x4017));
            EmitAsm("AND", Imm(0x10));
            EmitAsm("BNE", Rel(waitStart));
            // 81 cycles from the start-detect read to the first data read.
            // Polling uncertainty is at most nine cycles within a 57-cycle cell.
            for (int delay = 0; delay < 35; delay++) EmitAsm("NOP");
            EmitAsm("BIT", Mem(CallArgBase));
            for (int bit = 0; bit < 8; bit++)
            {
                EmitAsm("LDA", Mem(0x4017));
                EmitAsm("AND", Imm(0x10));
                EmitAsm("CMP", Imm(0x10));
                EmitAsm("ROR", Mem(_runtimeIntrinsicTmp0Address));
                for (int delay = 0; delay < (midiScratchInZeroPage ? 22 : 20); delay++) EmitAsm("NOP");
                if (!midiScratchInZeroPage) EmitAsm("BIT", Mem(CallArgBase));
            }
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("RTS");

            // Compose the channelized note-on status, then send note and velocity arguments.
__midi_program_change

0xC0 | (ch & 0x0F)とプログラム番号の2バイトを送ります。

書式

c
void __midi_program_change(u8 ch, u8 program);

引数

ch

MIDIチャンネル0~15。下位4ビットをステータスへ入れます。0が一般的な表示のチャンネル1です。

program

プログラム番号0~127。受信機によって楽器への対応が異なります。

戻り値

戻り値はありません。音源レジスターまたはドライバーの状態を変更します。

使用条件と制限

外付け回路が$4016のD0を送信、$4017のD4を受信へ接続する構成です。標準コントローラーだけではMIDI通信できず、内蔵APUも鳴りません。処理中はDMC再生・DMAとNMI・IRQを止め、同じポートや共有作業領域をほかの処理から触らないでください。NTSCの57サイクル/bitは約31,400bps(標準31,250bpsに対して約+0.5%)です。PAL向けのタイミング調整、実アダプターとの往復通信は未検証です。

使用例

c
// Controller-port byte encoding. This does not synthesize sound inside the FC.
#define SOUND_PORT_ONLY
#include "fc_sound_example.h"
void main(void) {
    sound_begin("MIDI PORT BYTES");
    // sound_begin has disabled DMC DMA and all internal APU voices. A compatible external circuit is required.
    // Stop NMI and mask IRQ so neither can split a software-timed serial frame.
    __ppu_ctrl_set(0); __irq_disable();
    __midi_out_byte(0x7E);
    __midi_program_change(2,5);
    __midi_control_change(2,7,100);
    __midi_note_on(2,69,96);
    __midi_note_off(2,69,0);
    __midi_clock(); __midi_start(); __midi_continue(); __midi_stop();
    // The emulator's unconnected input bit is low. Reading returns zero here.
    // A real input held high would block waiting for its start bit, without timeout.
    sound_result[0]=__midi_in_byte();
    sound_result[1]=1;
    __ppu_ctrl_set(0x80); __irq_enable();
    sound_end();
}

期待する結果

画面はEXTERNAL PORT DATAと表示し、内蔵音源は鳴りません。送信列(16進数)は7E、C2 05、B2 07 64、92 45 60、82 45 00、F8 FA FB FCです。NTSCの送信間隔57サイクル、受信データの読み取り間隔57サイクルを検査します。受信データはエミュレータの未接続入力による00で、実際の外部機器との通信成功を意味しません。

__midi_program_change: NTSC
NTSC

使用例の完全なプログラム

ビルドコマンド

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\sound_midi.c -I .\kitaqfc\lib -I .\kitaq-docs\samples -o .\out\fc-midi.nes --mapper=nrom --nes-chr=.\kitaq-docs\samples\font.chr --no-cache --no-disasm
実装とソース

kitaqfc/lib/intrinsics.h:302

宣言の注記(原文)
Send C0|(ch&0F) followed by the caller-supplied 7-bit program value.

kitaqfc/kitaqfc/CodeGenerator.cs:4801

csharp
case "__midi_program_change": return EmitKitaqfcHelperCall(funcName, args, ctx, new[] {1,1}, funcName);
                // Real-time MIDI commands send a single status byte through the common output routine.

EmitHelperStart("__midi_program_change");
            EmitAsm("LDA", Mem(CallArgBase));
            EmitAsm("AND", Imm(0x0F));
            EmitAsm("ORA", Imm(0xC0));
            EmitAsm("JSR", Abs("__kq_midi_out_a"));
            EmitAsm("LDA", Mem(CallArgBase + 1));
            EmitAsm("JSR", Abs("__kq_midi_out_a"));
            EmitAsm("RTS");

EmitHelperStart("__kq_midi_out_a");
            bool midiScratchInZeroPage = _runtimeIntrinsicTmp0Address <= 0xFF;
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("LDA", Imm(0));
            EmitAsm("STA", Mem(0x4016));
            // Unroll so neither branch cost nor payload bits change the baud.
            // Each data edge: 44 NOP cycles + LSR zp (5) + LDA/ADC (4) + STA (4).
            for (int bit = 0; bit < 8; bit++)
            {
                for (int delay = 0; delay < (midiScratchInZeroPage ? 22 : 20); delay++) EmitAsm("NOP");
                if (!midiScratchInZeroPage) EmitAsm("BIT", Mem(CallArgBase));
                EmitAsm("LSR", Mem(_runtimeIntrinsicTmp0Address));
                EmitAsm("LDA", Imm(0));
                EmitAsm("ADC", Imm(0));
                EmitAsm("STA", Mem(0x4016));
            }
            // 51 delay cycles plus LDA/STA (6) place the stop bit 57 cycles later.
            for (int delay = 0; delay < 24; delay++) EmitAsm("NOP");
            EmitAsm("BIT", Mem(CallArgBase));
            EmitAsm("LDA", Imm(1));
            EmitAsm("STA", Mem(0x4016));
            EmitMidiBitDelay(); // Hold stop high for at least one full bit cell.
            EmitAsm("RTS");

            EmitHelperStart("__midi_out_byte");
            EmitAsm("LDA", Mem(CallArgBase));
            EmitAsm("JSR", Abs("__kq_midi_out_a"));
            EmitAsm("RTS");

            // Poll in nine-cycle steps, then sample near data-bit centers.
            // IRQ/NMI masking belongs to the caller; no timeout/framing status is returned.
            EmitHelperStart("__midi_in_byte");
            EmitAsm("LDA", Imm(0));
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp0Address));
            string waitStart = NewGeneratedLabel("midi_wait_start");
            _assembly.Add(Expr.Make(Tag.Label, waitStart));
            EmitAsm("LDA", Mem(0x4017));
            EmitAsm("AND", Imm(0x10));
            EmitAsm("BNE", Rel(waitStart));
            // 81 cycles from the start-detect read to the first data read.
            // Polling uncertainty is at most nine cycles within a 57-cycle cell.
            for (int delay = 0; delay < 35; delay++) EmitAsm("NOP");
            EmitAsm("BIT", Mem(CallArgBase));
            for (int bit = 0; bit < 8; bit++)
            {
                EmitAsm("LDA", Mem(0x4017));
                EmitAsm("AND", Imm(0x10));
                EmitAsm("CMP", Imm(0x10));
                EmitAsm("ROR", Mem(_runtimeIntrinsicTmp0Address));
                for (int delay = 0; delay < (midiScratchInZeroPage ? 22 : 20); delay++) EmitAsm("NOP");
                if (!midiScratchInZeroPage) EmitAsm("BIT", Mem(CallArgBase));
            }
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("RTS");

            // Compose the channelized note-on status, then send note and velocity arguments.
__midi_start

リアルタイムStartのFAを1回送ります。ゲーム内の曲ポインタは変更しません。

書式

c
void __midi_start(void);

戻り値

戻り値はありません。音源レジスターまたはドライバーの状態を変更します。

使用条件と制限

外付け回路が$4016のD0を送信、$4017のD4を受信へ接続する構成です。標準コントローラーだけではMIDI通信できず、内蔵APUも鳴りません。処理中はDMC再生・DMAとNMI・IRQを止め、同じポートや共有作業領域をほかの処理から触らないでください。NTSCの57サイクル/bitは約31,400bps(標準31,250bpsに対して約+0.5%)です。PAL向けのタイミング調整、実アダプターとの往復通信は未検証です。

使用例

c
// Controller-port byte encoding. This does not synthesize sound inside the FC.
#define SOUND_PORT_ONLY
#include "fc_sound_example.h"
void main(void) {
    sound_begin("MIDI PORT BYTES");
    // sound_begin has disabled DMC DMA and all internal APU voices. A compatible external circuit is required.
    // Stop NMI and mask IRQ so neither can split a software-timed serial frame.
    __ppu_ctrl_set(0); __irq_disable();
    __midi_out_byte(0x7E);
    __midi_program_change(2,5);
    __midi_control_change(2,7,100);
    __midi_note_on(2,69,96);
    __midi_note_off(2,69,0);
    __midi_clock(); __midi_start(); __midi_continue(); __midi_stop();
    // The emulator's unconnected input bit is low. Reading returns zero here.
    // A real input held high would block waiting for its start bit, without timeout.
    sound_result[0]=__midi_in_byte();
    sound_result[1]=1;
    __ppu_ctrl_set(0x80); __irq_enable();
    sound_end();
}

期待する結果

画面はEXTERNAL PORT DATAと表示し、内蔵音源は鳴りません。送信列(16進数)は7E、C2 05、B2 07 64、92 45 60、82 45 00、F8 FA FB FCです。NTSCの送信間隔57サイクル、受信データの読み取り間隔57サイクルを検査します。受信データはエミュレータの未接続入力による00で、実際の外部機器との通信成功を意味しません。

__midi_start: NTSC
NTSC

使用例の完全なプログラム

ビルドコマンド

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\sound_midi.c -I .\kitaqfc\lib -I .\kitaq-docs\samples -o .\out\fc-midi.nes --mapper=nrom --nes-chr=.\kitaq-docs\samples\font.chr --no-cache --no-disasm
実装とソース

kitaqfc/lib/intrinsics.h:306

宣言の注記(原文)
Transmit realtime Start (FA) through the serial adapter path.

kitaqfc/kitaqfc/CodeGenerator.cs:4804

csharp
case "__midi_start": if (!Need(0)) return true; EmitAsm("LDA", Imm(0xFA)); EmitAsm("JSR", Abs("__kq_midi_out_a")); return true;

EmitHelperStart("__kq_midi_out_a");
            bool midiScratchInZeroPage = _runtimeIntrinsicTmp0Address <= 0xFF;
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("LDA", Imm(0));
            EmitAsm("STA", Mem(0x4016));
            // Unroll so neither branch cost nor payload bits change the baud.
            // Each data edge: 44 NOP cycles + LSR zp (5) + LDA/ADC (4) + STA (4).
            for (int bit = 0; bit < 8; bit++)
            {
                for (int delay = 0; delay < (midiScratchInZeroPage ? 22 : 20); delay++) EmitAsm("NOP");
                if (!midiScratchInZeroPage) EmitAsm("BIT", Mem(CallArgBase));
                EmitAsm("LSR", Mem(_runtimeIntrinsicTmp0Address));
                EmitAsm("LDA", Imm(0));
                EmitAsm("ADC", Imm(0));
                EmitAsm("STA", Mem(0x4016));
            }
            // 51 delay cycles plus LDA/STA (6) place the stop bit 57 cycles later.
            for (int delay = 0; delay < 24; delay++) EmitAsm("NOP");
            EmitAsm("BIT", Mem(CallArgBase));
            EmitAsm("LDA", Imm(1));
            EmitAsm("STA", Mem(0x4016));
            EmitMidiBitDelay(); // Hold stop high for at least one full bit cell.
            EmitAsm("RTS");

            EmitHelperStart("__midi_out_byte");
            EmitAsm("LDA", Mem(CallArgBase));
            EmitAsm("JSR", Abs("__kq_midi_out_a"));
            EmitAsm("RTS");

            // Poll in nine-cycle steps, then sample near data-bit centers.
            // IRQ/NMI masking belongs to the caller; no timeout/framing status is returned.
            EmitHelperStart("__midi_in_byte");
            EmitAsm("LDA", Imm(0));
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp0Address));
            string waitStart = NewGeneratedLabel("midi_wait_start");
            _assembly.Add(Expr.Make(Tag.Label, waitStart));
            EmitAsm("LDA", Mem(0x4017));
            EmitAsm("AND", Imm(0x10));
            EmitAsm("BNE", Rel(waitStart));
            // 81 cycles from the start-detect read to the first data read.
            // Polling uncertainty is at most nine cycles within a 57-cycle cell.
            for (int delay = 0; delay < 35; delay++) EmitAsm("NOP");
            EmitAsm("BIT", Mem(CallArgBase));
            for (int bit = 0; bit < 8; bit++)
            {
                EmitAsm("LDA", Mem(0x4017));
                EmitAsm("AND", Imm(0x10));
                EmitAsm("CMP", Imm(0x10));
                EmitAsm("ROR", Mem(_runtimeIntrinsicTmp0Address));
                for (int delay = 0; delay < (midiScratchInZeroPage ? 22 : 20); delay++) EmitAsm("NOP");
                if (!midiScratchInZeroPage) EmitAsm("BIT", Mem(CallArgBase));
            }
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("RTS");

            // Compose the channelized note-on status, then send note and velocity arguments.
__midi_stop

リアルタイムStopのFCを1回送ります。内蔵APUや送信済みノートを直接止める関数ではありません。

書式

c
void __midi_stop(void);

戻り値

戻り値はありません。音源レジスターまたはドライバーの状態を変更します。

使用条件と制限

外付け回路が$4016のD0を送信、$4017のD4を受信へ接続する構成です。標準コントローラーだけではMIDI通信できず、内蔵APUも鳴りません。処理中はDMC再生・DMAとNMI・IRQを止め、同じポートや共有作業領域をほかの処理から触らないでください。NTSCの57サイクル/bitは約31,400bps(標準31,250bpsに対して約+0.5%)です。PAL向けのタイミング調整、実アダプターとの往復通信は未検証です。

使用例

c
// Controller-port byte encoding. This does not synthesize sound inside the FC.
#define SOUND_PORT_ONLY
#include "fc_sound_example.h"
void main(void) {
    sound_begin("MIDI PORT BYTES");
    // sound_begin has disabled DMC DMA and all internal APU voices. A compatible external circuit is required.
    // Stop NMI and mask IRQ so neither can split a software-timed serial frame.
    __ppu_ctrl_set(0); __irq_disable();
    __midi_out_byte(0x7E);
    __midi_program_change(2,5);
    __midi_control_change(2,7,100);
    __midi_note_on(2,69,96);
    __midi_note_off(2,69,0);
    __midi_clock(); __midi_start(); __midi_continue(); __midi_stop();
    // The emulator's unconnected input bit is low. Reading returns zero here.
    // A real input held high would block waiting for its start bit, without timeout.
    sound_result[0]=__midi_in_byte();
    sound_result[1]=1;
    __ppu_ctrl_set(0x80); __irq_enable();
    sound_end();
}

期待する結果

画面はEXTERNAL PORT DATAと表示し、内蔵音源は鳴りません。送信列(16進数)は7E、C2 05、B2 07 64、92 45 60、82 45 00、F8 FA FB FCです。NTSCの送信間隔57サイクル、受信データの読み取り間隔57サイクルを検査します。受信データはエミュレータの未接続入力による00で、実際の外部機器との通信成功を意味しません。

__midi_stop: NTSC
NTSC

使用例の完全なプログラム

ビルドコマンド

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\sound_midi.c -I .\kitaqfc\lib -I .\kitaq-docs\samples -o .\out\fc-midi.nes --mapper=nrom --nes-chr=.\kitaq-docs\samples\font.chr --no-cache --no-disasm
実装とソース

kitaqfc/lib/intrinsics.h:310

宣言の注記(原文)
Transmit realtime Stop (FC) through the serial adapter path.

kitaqfc/kitaqfc/CodeGenerator.cs:4806

csharp
case "__midi_stop": if (!Need(0)) return true; EmitAsm("LDA", Imm(0xFC)); EmitAsm("JSR", Abs("__kq_midi_out_a")); return true;
                // Sprite helpers modify the runtime OAM representation; DMA remains a separate operation.

EmitHelperStart("__kq_midi_out_a");
            bool midiScratchInZeroPage = _runtimeIntrinsicTmp0Address <= 0xFF;
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("LDA", Imm(0));
            EmitAsm("STA", Mem(0x4016));
            // Unroll so neither branch cost nor payload bits change the baud.
            // Each data edge: 44 NOP cycles + LSR zp (5) + LDA/ADC (4) + STA (4).
            for (int bit = 0; bit < 8; bit++)
            {
                for (int delay = 0; delay < (midiScratchInZeroPage ? 22 : 20); delay++) EmitAsm("NOP");
                if (!midiScratchInZeroPage) EmitAsm("BIT", Mem(CallArgBase));
                EmitAsm("LSR", Mem(_runtimeIntrinsicTmp0Address));
                EmitAsm("LDA", Imm(0));
                EmitAsm("ADC", Imm(0));
                EmitAsm("STA", Mem(0x4016));
            }
            // 51 delay cycles plus LDA/STA (6) place the stop bit 57 cycles later.
            for (int delay = 0; delay < 24; delay++) EmitAsm("NOP");
            EmitAsm("BIT", Mem(CallArgBase));
            EmitAsm("LDA", Imm(1));
            EmitAsm("STA", Mem(0x4016));
            EmitMidiBitDelay(); // Hold stop high for at least one full bit cell.
            EmitAsm("RTS");

            EmitHelperStart("__midi_out_byte");
            EmitAsm("LDA", Mem(CallArgBase));
            EmitAsm("JSR", Abs("__kq_midi_out_a"));
            EmitAsm("RTS");

            // Poll in nine-cycle steps, then sample near data-bit centers.
            // IRQ/NMI masking belongs to the caller; no timeout/framing status is returned.
            EmitHelperStart("__midi_in_byte");
            EmitAsm("LDA", Imm(0));
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp0Address));
            string waitStart = NewGeneratedLabel("midi_wait_start");
            _assembly.Add(Expr.Make(Tag.Label, waitStart));
            EmitAsm("LDA", Mem(0x4017));
            EmitAsm("AND", Imm(0x10));
            EmitAsm("BNE", Rel(waitStart));
            // 81 cycles from the start-detect read to the first data read.
            // Polling uncertainty is at most nine cycles within a 57-cycle cell.
            for (int delay = 0; delay < 35; delay++) EmitAsm("NOP");
            EmitAsm("BIT", Mem(CallArgBase));
            for (int bit = 0; bit < 8; bit++)
            {
                EmitAsm("LDA", Mem(0x4017));
                EmitAsm("AND", Imm(0x10));
                EmitAsm("CMP", Imm(0x10));
                EmitAsm("ROR", Mem(_runtimeIntrinsicTmp0Address));
                for (int delay = 0; delay < (midiScratchInZeroPage ? 22 : 20); delay++) EmitAsm("NOP");
                if (!midiScratchInZeroPage) EmitAsm("BIT", Mem(CallArgBase));
            }
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("RTS");

            // Compose the channelized note-on status, then send note and velocity arguments.
__mirroring_set

論理ネームテーブル4領域を、本体の2ページの画面RAMへ割り当てます。スクロール用の共有配置や1画面の切り替えに使います。

書式

c
void __mirroring_set(u8 mode);

引数

mode

モード番号は基板ごとに異なります。MMC1/SUROM: 0=下側1画面、1=上側1画面、2=縦、3=横。MMC3: 0=縦、1=横。MMC5: 0=横、1=縦、2=下側1画面、3=上側1画面。AxROM: 0=下側1画面、1=上側1画面。FME7: 0=縦、1=横、2=下側1画面、3=上側1画面。MMC3/AxROMは下位1ビット、その他は下位2ビットを使います。

戻り値

戻り値はありません。

使用条件と制限

組み込み関数の宣言はintrinsics.hにあります。nes_mapper_irq_*のマクロにはnes_game.hを読み込みます。マッパーはビルド時の--mapperで選びます。

縦ミラーリングは4領域の共有順がA B A B、横ミラーリングはA A B Bです。下側1画面はA A A A、上側1画面はB B B Bです。これはメモリの割り当てであり、背景画像の反転やコピーは行いません。固定ミラーリングのNROMなど、このAPIに対応しない基板はビルド時に拒否します。

MMC1は制御値$0C|modeを書き込み、PRGを上側16 KiB固定、CHRを8 KiBモードにします。SUROMも制御シャドーを更新します。AxROMではランタイムのPRGバンク値を保ちながら画面選択ビットを変えます。直接レジスターを書く処理と併用する場合は、ランタイムのバンク状態と一致させてください。

MMC5では$5105に$50/$44/$00/$55を書きます。いずれも本体CIRAMの2ページだけを使う配置です。ExRAMや塗りつぶしモードはこのAPIの対象ではありません。KUROSAKIはCIRAMだけを使う16通りの配置に対応しますが、MMC5のExRAM・塗りつぶし描画は未対応です。

サンプルの--mapper=mmc3をmmc1、mmc5、axrom、fme7へ変更して各基板の結果を確認できます。MMC5/FME7ではフォント用のCHRバンクを基板のレジスターで初期化しています。

使用例

c
k=1;
    for (i=0;i<4;i++) {
        __mirroring_set(i);
        for (j=0;j<4;j++) { mirror_result[k]=mirror_read(0x2000+(u16)j*1024); k++; }
    }

呼び出し部分のコード例です。完全なプログラムに組み込む際は、記載した初期化とバッファの条件を満たしてください。

期待する結果

サンプルは物理RAMの下側に17、上側に34を書き、4つの論理領域を読み戻します。Aは下側、Bは上側です。同じ文字の領域は同じRAMを共有するため、一方への書き込みが他方からも見えます。画面の列は$2000、$2400、$2800、$2C00、行はモード0~3です。読み戻し後に文字を表示しています。

__mirroring_set: MMC1-DEFAULT
MMC1-DEFAULT
__mirroring_set: MMC3-DEFAULT
MMC3-DEFAULT
__mirroring_set: MMC5-DEFAULT
MMC5-DEFAULT
__mirroring_set: AXROM-DEFAULT
AXROM-DEFAULT
__mirroring_set: FME7-DEFAULT
FME7-DEFAULT

使用例の完全なプログラム

ビルドコマンド

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\mapper_mirroring.c -I .\kitaqfc\lib -I .\kitaq-docs\samples -o .\out\mapper_mirroring.nes --mapper=mmc3 --nes-chr=.\kitaq-docs\samples\font.chr --no-cache --no-disasm
実装とソース

kitaqfc/lib/intrinsics.h:322

宣言の注記(原文)
Emit the selected mapper mirroring operation; available modes and writable control depend on the board.
__mul16x8

符号なし乗算を計算します。

書式

c
u16 __mul16x8(u16 a, u8 b);

引数

a

符号なし16ビット値0~65535。

b

符号なし8ビット係数0~255。

戻り値

a*bの下位16ビットです。

使用条件と制限

FCでは名前にq8_8/q1_7があっても生の整数積を使い、固定小数点の縮小シフトは行いません。Q1.7の3関数は対応する符号付き関数への別名です。単位変換は呼び出し側で行います。符号付き結果もu16のビット列として返します。

使用例

c
// Compare literal coefficients with values read from RAM.
result[2]=__mul16x8(513,127);
v0=513;v1=127;
result[3]=__mul16x8(v0,v1);

呼び出し部分のコード例です。完全なプログラムに組み込む際は、記載した初期化とバッファの条件を満たしてください。

期待する結果

固定値とRAM変数で同じ計算を行う例です。画面には代表値を符号なし16ビットで表示し、負数は2の補数の値になります。GBは固定小数点の縮小を行い、FCは整数積を返すため、同じ入力でも両機種の結果は異なります。

この抜粋で保存する16ビット値は result[2]=65151, result[3]=65151 です。固定値とRAM変数を使った計算は、それぞれ同じ値になります。画面は代表値を表示し、全結果はresult配列に保存します。

__mul16x8: NROM
NROM

使用例の完全なプログラム

ビルドコマンド

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\batch3_math.c -I .\kitaqfc\lib -I .\kitaq-docs\samples -o .\out\batch3_math.nes --no-cache --no-disasm --mapper=nrom --nes-chr=.\kitaq-docs\samples\font.chr
実装とソース

kitaqfc/lib/intrinsics.h:51

宣言の注記(原文)
Return the low word of the unsigned product; overflow wraps without saturation.

kitaqfc/kitaqfc/CodeGenerator.cs:4647

csharp
case "__mul16x8": return EmitKitaqfcHelperCall(funcName, args, ctx, new[] {2,1}, funcName);

void EmitHelperMath()
        {
            // Consume eight multiplier bits and accumulate the low product word in shared temporaries, returning A/X.
            EmitHelperStart("__mul16x8");
            EmitAsm("LDA", Imm(0));
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp1Address));
            EmitAsm("LDX", Imm(8));
            string loop = NewGeneratedLabel("mul16x8_loop");
            string skip = NewGeneratedLabel("mul16x8_skip_add");
            _assembly.Add(Expr.Make(Tag.Label, loop));
            EmitAsm("LSR", Mem(CallArgBase + 2));
            EmitAsm("BCC", Rel(skip));
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("CLC");
            EmitAsm("ADC", Mem(CallArgBase));
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp1Address));
            EmitAsm("ADC", Mem(CallArgBase + 1));
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp1Address));
            _assembly.Add(Expr.Make(Tag.Label, skip));
            EmitAsm("ASL", Mem(CallArgBase));
            EmitAsm("ROL", Mem(CallArgBase + 1));
            EmitAsm("DEX");
            EmitAsm("BNE", Rel(loop));
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("LDX", Mem(_runtimeIntrinsicTmp1Address));
            EmitAsm("RTS");

            // Zero-extend the byte multiplicand, reuse word multiplication and return its high byte.
            EmitHelperStart("__mul8x8_hi");
            EmitAsm("LDA", Mem(CallArgBase + 1));
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp2Address));
            EmitAsm("LDA", Imm(0));
            EmitAsm("STA", Mem(CallArgBase + 1));
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp2Address));
            EmitAsm("STA", Mem(CallArgBase + 2));
            EmitAsm("JSR", Abs("__mul16x8"));
            EmitAsm("TXA");
            EmitAsm("RTS");

            // Convert signed operands to magnitudes, multiply, then restore the product sign modulo 65536.
            EmitHelperStart("__smul16x8");
            EmitAsm("LDA", Imm(0));
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp2Address));
            string aPos = NewGeneratedLabel("smul_a_positive");
            EmitAsm("LDA", Mem(CallArgBase + 1));
            EmitAsm("AND", Imm(0x80));
            EmitAsm("BEQ", Rel(aPos));
            EmitAsm("LDA", Mem(CallArgBase)); EmitAsm("EOR", Imm(0xFF)); EmitAsm("CLC"); EmitAsm("ADC", Imm(1)); EmitAsm("STA", Mem(CallArgBase));
            EmitAsm("LDA", Mem(CallArgBase + 1)); EmitAsm("EOR", Imm(0xFF)); EmitAsm("ADC", Imm(0)); EmitAsm("STA", Mem(CallArgBase + 1));
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp2Address)); EmitAsm("EOR", Imm(1)); EmitAsm("STA", Mem(_runtimeIntrinsicTmp2Address));
            _assembly.Add(Expr.Make(Tag.Label, aPos));
            string bPos = NewGeneratedLabel("smul_b_positive");
            EmitAsm("LDA", Mem(CallArgBase + 2));
            EmitAsm("AND", Imm(0x80));
            EmitAsm("BEQ", Rel(bPos));
            EmitAsm("LDA", Mem(CallArgBase + 2)); EmitAsm("EOR", Imm(0xFF)); EmitAsm("CLC"); EmitAsm("ADC", Imm(1)); EmitAsm("STA", Mem(CallArgBase + 2));
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp2Address)); EmitAsm("EOR", Imm(1)); EmitAsm("STA", Mem(_runtimeIntrinsicTmp2Address));
            _assembly.Add(Expr.Make(Tag.Label, bPos));
            EmitAsm("JSR", Abs("__mul16x8"));
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("STX", Mem(_runtimeIntrinsicTmp1Address));
            string sdone = NewGeneratedLabel("smul_done");
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp2Address));
            EmitAsm("BEQ", Rel(sdone));
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp0Address)); EmitAsm("EOR", Imm(0xFF)); EmitAsm("CLC"); EmitAsm("ADC", Imm(1)); EmitAsm("STA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp1Address)); EmitAsm("EOR", Imm(0xFF)); EmitAsm("ADC", Imm(0)); EmitAsm("STA", Mem(_runtimeIntrinsicTmp1Address));
            _assembly.Add(Expr.Make(Tag.Label, sdone));
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("LDX", Mem(_runtimeIntrinsicTmp1Address));
            EmitAsm("RTS");

            EmitMacHelper("__mac16", "__mul16x8");
            EmitMacHelper("__smac16", "__smul16x8");
            EmitDot2Helper("__dot2_q8_8", "__mul16x8");
            EmitDot3Helper("__dot3_q8_8", "__mul16x8");
            EmitDot2Helper("__sdot2_q8_8", "__smul16x8");
            EmitDot3Helper("__sdot3_q8_8", "__smul16x8");
        }
__mul8x8_hi

8ビット同士を掛け、積の上位8ビットを取り出します。

書式

c
u8 __mul8x8_hi(u8 a, u8 b);

引数

a

符号なし8ビット値0~255。

b

符号なし8ビット値0~255。

戻り値

floor(a*b/256)、0~254。

使用例

c
// Compare literal coefficients with values read from RAM.
result[0]=__mul8x8_hi(255,255);
v0=255;v1=255;
result[1]=__mul8x8_hi(v0,v1);

呼び出し部分のコード例です。完全なプログラムに組み込む際は、記載した初期化とバッファの条件を満たしてください。

期待する結果

固定値とRAM変数で同じ計算を行う例です。画面には代表値を符号なし16ビットで表示し、負数は2の補数の値になります。GBは固定小数点の縮小を行い、FCは整数積を返すため、同じ入力でも両機種の結果は異なります。

この抜粋で保存する16ビット値は result[0]=254, result[1]=254 です。固定値とRAM変数を使った計算は、それぞれ同じ値になります。画面は代表値を表示し、全結果はresult配列に保存します。

__mul8x8_hi: NROM
NROM

使用例の完全なプログラム

ビルドコマンド

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\batch3_math.c -I .\kitaqfc\lib -I .\kitaq-docs\samples -o .\out\batch3_math.nes --no-cache --no-disasm --mapper=nrom --nes-chr=.\kitaq-docs\samples\font.chr
実装とソース

kitaqfc/lib/intrinsics.h:49

宣言の注記(原文)
Return the high byte of the unsigned 8-by-8 product, equivalent to floor(a*b/256).

kitaqfc/kitaqfc/CodeGenerator.cs:4649

csharp
case "__mul8x8_hi": return EmitKitaqfcHelperCall(funcName, args, ctx, new[] {1,1}, funcName);

void EmitHelperMath()
        {
            // Consume eight multiplier bits and accumulate the low product word in shared temporaries, returning A/X.
            EmitHelperStart("__mul16x8");
            EmitAsm("LDA", Imm(0));
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp1Address));
            EmitAsm("LDX", Imm(8));
            string loop = NewGeneratedLabel("mul16x8_loop");
            string skip = NewGeneratedLabel("mul16x8_skip_add");
            _assembly.Add(Expr.Make(Tag.Label, loop));
            EmitAsm("LSR", Mem(CallArgBase + 2));
            EmitAsm("BCC", Rel(skip));
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("CLC");
            EmitAsm("ADC", Mem(CallArgBase));
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp1Address));
            EmitAsm("ADC", Mem(CallArgBase + 1));
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp1Address));
            _assembly.Add(Expr.Make(Tag.Label, skip));
            EmitAsm("ASL", Mem(CallArgBase));
            EmitAsm("ROL", Mem(CallArgBase + 1));
            EmitAsm("DEX");
            EmitAsm("BNE", Rel(loop));
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("LDX", Mem(_runtimeIntrinsicTmp1Address));
            EmitAsm("RTS");

            // Zero-extend the byte multiplicand, reuse word multiplication and return its high byte.
            EmitHelperStart("__mul8x8_hi");
            EmitAsm("LDA", Mem(CallArgBase + 1));
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp2Address));
            EmitAsm("LDA", Imm(0));
            EmitAsm("STA", Mem(CallArgBase + 1));
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp2Address));
            EmitAsm("STA", Mem(CallArgBase + 2));
            EmitAsm("JSR", Abs("__mul16x8"));
            EmitAsm("TXA");
            EmitAsm("RTS");

            // Convert signed operands to magnitudes, multiply, then restore the product sign modulo 65536.
            EmitHelperStart("__smul16x8");
            EmitAsm("LDA", Imm(0));
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp2Address));
            string aPos = NewGeneratedLabel("smul_a_positive");
            EmitAsm("LDA", Mem(CallArgBase + 1));
            EmitAsm("AND", Imm(0x80));
            EmitAsm("BEQ", Rel(aPos));
            EmitAsm("LDA", Mem(CallArgBase)); EmitAsm("EOR", Imm(0xFF)); EmitAsm("CLC"); EmitAsm("ADC", Imm(1)); EmitAsm("STA", Mem(CallArgBase));
            EmitAsm("LDA", Mem(CallArgBase + 1)); EmitAsm("EOR", Imm(0xFF)); EmitAsm("ADC", Imm(0)); EmitAsm("STA", Mem(CallArgBase + 1));
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp2Address)); EmitAsm("EOR", Imm(1)); EmitAsm("STA", Mem(_runtimeIntrinsicTmp2Address));
            _assembly.Add(Expr.Make(Tag.Label, aPos));
            string bPos = NewGeneratedLabel("smul_b_positive");
            EmitAsm("LDA", Mem(CallArgBase + 2));
            EmitAsm("AND", Imm(0x80));
            EmitAsm("BEQ", Rel(bPos));
            EmitAsm("LDA", Mem(CallArgBase + 2)); EmitAsm("EOR", Imm(0xFF)); EmitAsm("CLC"); EmitAsm("ADC", Imm(1)); EmitAsm("STA", Mem(CallArgBase + 2));
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp2Address)); EmitAsm("EOR", Imm(1)); EmitAsm("STA", Mem(_runtimeIntrinsicTmp2Address));
            _assembly.Add(Expr.Make(Tag.Label, bPos));
            EmitAsm("JSR", Abs("__mul16x8"));
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("STX", Mem(_runtimeIntrinsicTmp1Address));
            string sdone = NewGeneratedLabel("smul_done");
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp2Address));
            EmitAsm("BEQ", Rel(sdone));
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp0Address)); EmitAsm("EOR", Imm(0xFF)); EmitAsm("CLC"); EmitAsm("ADC", Imm(1)); EmitAsm("STA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp1Address)); EmitAsm("EOR", Imm(0xFF)); EmitAsm("ADC", Imm(0)); EmitAsm("STA", Mem(_runtimeIntrinsicTmp1Address));
            _assembly.Add(Expr.Make(Tag.Label, sdone));
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("LDX", Mem(_runtimeIntrinsicTmp1Address));
            EmitAsm("RTS");

            EmitMacHelper("__mac16", "__mul16x8");
            EmitMacHelper("__smac16", "__smul16x8");
            EmitDot2Helper("__dot2_q8_8", "__mul16x8");
            EmitDot3Helper("__dot3_q8_8", "__mul16x8");
            EmitDot2Helper("__sdot2_q8_8", "__smul16x8");
            EmitDot3Helper("__sdot3_q8_8", "__smul16x8");
        }
__nametable_put

PPUアドレス0x2000 + y*32 + xへ、ネームテーブル0のタイル番号を1バイト直接書きます。アドレス設定前にラッチをリセットします。タイルの絵柄やパレット属性は書かず、座標の切り詰めも行いません。

書式

c
void __nametable_put(u8 x, u8 y, u8 tile);

引数

x / y

ピクセルではなくタイル座標です。32×30タイルの表示領域ではxを0..31、yを0..29に収めます。1タイルは8×8ピクセルです。1バイトの引数を範囲内へ補正しないため、範囲外の座標では意図したタイル領域の外へ書く場合があります。

tile

PPUCTRLで選んだ背景パターンテーブル内のタイル番号0..255です。既存のCHR画像を選ぶ値で、ピクセルの色や転送元ポインターではありません。

戻り値

ありません(void)。

使用条件と制限

PPU直接アクセスは、描画停止中か、処理全体が安全なVBlank時間内に収まる条件で行います。これらの関数は待機・キュー登録・NMI抑止・スクロール復元を行いません。呼び出し中に割り込みで共有ラッチやアドレスを変更しないでください。PPUCTRLのビット2により、書き込み先は1バイトごとに1または32進みます。ミラーリングとアドレスの周回はPPUの動作に従い、CHR-ROMへは書き込めません。アクセス後、描画を始める前に意図したスクロール状態を設定してください。

NESdev: PPU registers

NESdev: PPU scrolling

使用例

c
__nametable_put(2,12,1); // Blue solid marker at (16,96).

呼び出し部分のコード例です。完全なプログラムに組み込む際は、記載した初期化とバッファの条件を満たしてください。

期待する結果

このプログラムではレジスター設定、タイル・属性の直接書き込み、スクロール、背景・スプライトそれぞれのパレット転送を学びます。(0,16)の青い帯は256×16ピクセルです。上段は8×8の塗りつぶし、左半分4×8の棒、8行の三角形を繰り返し、下段は左半分の棒だけを並べます。(16,48)に緑の塗りつぶし64×32、(144,48)に三角形タイルを横8個・縦4個並べた赤い64×32の領域が出ます。

y=96では、x=16・24・32に青い正方形・左半分の棒・三角形、x=80に赤い左半分の棒、x=144に緑の三角形が表示されます。これらは背景タイルです。別パレットを使うスプライトはy=144に並び、x=16が赤い8×8の正方形、x=80が青い左半分4×8の棒、x=144が緑の8行の三角形です。最終スクロールは(0,0)です。

画像ではスプライトの色も含め、16,384ピクセルの形と色を照合します。別の31ケースで、レジスターの置き換え、ステータス・ラッチの副作用、スクロール保存値、0・255バイト転送、増分1・32、マップ端、矩形の寸法0、テーブル選択、属性1バイト全体、背景・スプライト両方のパレットを検証しています。Y=255はレジスターへ書く値としての確認で、正しいスクロール画像の検証ではありません。検証環境はKUROSAKIのNROMで、実機は未検証です。

__nametable_put: NROM
NROM

使用例の完全なプログラム

ビルドコマンド

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\ppu_intrinsics.c -I .\kitaqfc\lib -I .\kitaq-docs\samples --nes-chr=.\kitaq-docs\samples\api-examples\fc\vram_shapes.chr -o .\out\ppu_intrinsics.nes --no-cache --no-disasm
実装とソース

kitaqfc/lib/intrinsics.h:124

宣言の注記(原文)
Write one tile directly at 2000+y*32+x; use x<32, y<30 for visible nametable cells.

kitaqfc/kitaqfc/CodeGenerator.cs:4820

csharp
case "__nametable_put": return EmitKitaqfcHelperCall(funcName, args, ctx, new[] {1,1,1}, funcName);

void EmitHelperNametablePut()
        {
            EmitHelperStart("__nametable_put");
            // args: x, y, tile. PPU address = $2000 + y * 32 + x.
            EmitAsm("LDA", Mem(CallArgBase + 1));
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("LDA", Imm(0));
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp1Address));
            for (int i = 0; i < 5; i++) { EmitAsm("ASL", Mem(_runtimeIntrinsicTmp0Address)); EmitAsm("ROL", Mem(_runtimeIntrinsicTmp1Address)); }
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("CLC");
            EmitAsm("ADC", Mem(CallArgBase));
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp1Address));
            EmitAsm("ADC", Imm(0x20));
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp1Address));
            EmitAsm("LDA", Mem(0x2002));
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp1Address));
            EmitAsm("STA", Mem(0x2006));
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("STA", Mem(0x2006));
            EmitAsm("LDA", Mem(CallArgBase + 2));
            EmitAsm("STA", Mem(0x2007));
            EmitAsm("RTS");
        }
__nametable_put_nt

論理ネームテーブルnt & 3を選び、0x2000 + (nt & 3)*0x400 + y*32 + xへタイル番号を1個書きます。アドレスの2バイトを書く前にラッチをリセットします。カートリッジのミラーリングにより、異なる論理テーブル番号が同じ物理メモリーを指す場合があります。

書式

c
void __nametable_put_nt(u8 nt, u8 x, u8 y, u8 tile);

引数

nt

論理ネームテーブルの選択値です。下位2ビットだけを使うため(nt & 3)、たとえば4はテーブル0、7はテーブル3を選びます。独立した保存領域の確保やマッパーのミラーリング変更は行いません。

x / y

ピクセルではなくタイル座標です。32×30タイルの表示領域ではxを0..31、yを0..29に収めます。1タイルは8×8ピクセルです。1バイトの引数を範囲内へ補正しないため、範囲外の座標では意図したタイル領域の外へ書く場合があります。

tile

PPUCTRLで選んだ背景パターンテーブル内のタイル番号0..255です。既存のCHR画像を選ぶ値で、ピクセルの色や転送元ポインターではありません。

戻り値

ありません(void)。

使用条件と制限

PPU直接アクセスは、描画停止中か、処理全体が安全なVBlank時間内に収まる条件で行います。これらの関数は待機・キュー登録・NMI抑止・スクロール復元を行いません。呼び出し中に割り込みで共有ラッチやアドレスを変更しないでください。PPUCTRLのビット2により、書き込み先は1バイトごとに1または32進みます。ミラーリングとアドレスの周回はPPUの動作に従い、CHR-ROMへは書き込めません。アクセス後、描画を始める前に意図したスクロール状態を設定してください。

NESdev: PPU registers

NESdev: PPU scrolling

使用例

c
__nametable_put_nt(4,10,12,2); // Red left-half marker at (80,96), table 0.

呼び出し部分のコード例です。完全なプログラムに組み込む際は、記載した初期化とバッファの条件を満たしてください。

期待する結果

このプログラムではレジスター設定、タイル・属性の直接書き込み、スクロール、背景・スプライトそれぞれのパレット転送を学びます。(0,16)の青い帯は256×16ピクセルです。上段は8×8の塗りつぶし、左半分4×8の棒、8行の三角形を繰り返し、下段は左半分の棒だけを並べます。(16,48)に緑の塗りつぶし64×32、(144,48)に三角形タイルを横8個・縦4個並べた赤い64×32の領域が出ます。

y=96では、x=16・24・32に青い正方形・左半分の棒・三角形、x=80に赤い左半分の棒、x=144に緑の三角形が表示されます。これらは背景タイルです。別パレットを使うスプライトはy=144に並び、x=16が赤い8×8の正方形、x=80が青い左半分4×8の棒、x=144が緑の8行の三角形です。最終スクロールは(0,0)です。

画像ではスプライトの色も含め、16,384ピクセルの形と色を照合します。別の31ケースで、レジスターの置き換え、ステータス・ラッチの副作用、スクロール保存値、0・255バイト転送、増分1・32、マップ端、矩形の寸法0、テーブル選択、属性1バイト全体、背景・スプライト両方のパレットを検証しています。Y=255はレジスターへ書く値としての確認で、正しいスクロール画像の検証ではありません。検証環境はKUROSAKIのNROMで、実機は未検証です。

__nametable_put_nt: NROM
NROM

使用例の完全なプログラム

ビルドコマンド

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\ppu_intrinsics.c -I .\kitaqfc\lib -I .\kitaq-docs\samples --nes-chr=.\kitaq-docs\samples\api-examples\fc\vram_shapes.chr -o .\out\ppu_intrinsics.nes --no-cache --no-disasm
実装とソース

kitaqfc/lib/intrinsics.h:126

宣言の注記(原文)
Write one tile in nametable nt&3. Mirroring can alias logical nametables; coordinates are unchecked.

kitaqfc/kitaqfc/CodeGenerator.cs:4821

csharp
case "__nametable_put_nt": return EmitKitaqfcHelperCall(funcName, args, ctx, new[] {1,1,1,1}, funcName);

void EmitHelperNametablePutNt()
        {
            EmitHelperStart("__nametable_put_nt");
            // args: nt,x,y,tile. PPU address = $2000 + nt*$400 + y*32 + x.
            EmitAsm("LDA", Mem(CallArgBase));
            EmitAsm("AND", Imm(3));
            EmitAsm("ASL");
            EmitAsm("ASL");
            EmitAsm("CLC");
            EmitAsm("ADC", Imm(0x20));
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp2Address));
            EmitAsm("LDA", Mem(CallArgBase + 2));
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("LDA", Imm(0));
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp1Address));
            for (int i = 0; i < 5; i++) { EmitAsm("ASL", Mem(_runtimeIntrinsicTmp0Address)); EmitAsm("ROL", Mem(_runtimeIntrinsicTmp1Address)); }
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("CLC");
            EmitAsm("ADC", Mem(CallArgBase + 1));
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp1Address));
            EmitAsm("ADC", Mem(_runtimeIntrinsicTmp2Address));
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp1Address));
            EmitAsm("LDA", Mem(0x2002));
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp1Address));
            EmitAsm("STA", Mem(0x2006));
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("STA", Mem(0x2006));
            EmitAsm("LDA", Mem(CallArgBase + 3));
            EmitAsm("STA", Mem(0x2007));
            EmitAsm("RTS");
        }
__nametable_rect

ネームテーブル0の(x,y)を起点に、幅w・高さhのタイル番号を埋めます。各行でラッチとアドレスを設定し、tileをw個書いて、次の行の先頭を32バイト先へ進めます。パレット属性は変更しません。

書式

c
void __nametable_rect(u8 x, u8 y, u8 w, u8 h, u8 tile);

引数

x / y

ピクセルではなくタイル座標です。32×30タイルの表示領域ではxを0..31、yを0..29に収めます。1タイルは8×8ピクセルです。1バイトの引数を範囲内へ補正しないため、範囲外の座標では意図したタイル領域の外へ書く場合があります。

w / h

タイル単位の幅と高さで、どちらも符号なし1バイトです。x+w<=32、y+h<=30に収めてください。高さ0ならPPUにアクセスしません。幅0ならタイル番号は書きませんが、h行それぞれのラッチリセットとアドレス設定は行います。横に並ぶ行にはPPU増分1を使います。

tile

PPUCTRLで選んだ背景パターンテーブル内のタイル番号0..255です。既存のCHR画像を選ぶ値で、ピクセルの色や転送元ポインターではありません。

戻り値

ありません(void)。

使用条件と制限

PPU直接アクセスは、描画停止中か、処理全体が安全なVBlank時間内に収まる条件で行います。これらの関数は待機・キュー登録・NMI抑止・スクロール復元を行いません。呼び出し中に割り込みで共有ラッチやアドレスを変更しないでください。PPUCTRLのビット2により、書き込み先は1バイトごとに1または32進みます。ミラーリングとアドレスの周回はPPUの動作に従い、CHR-ROMへは書き込めません。アクセス後、描画を始める前に意図したスクロール状態を設定してください。

NESdev: PPU registers

NESdev: PPU scrolling

使用例

c
__nametable_rect(2,6,8,4,1); // Solid green rectangle at (16,48), 64x32 pixels.

呼び出し部分のコード例です。完全なプログラムに組み込む際は、記載した初期化とバッファの条件を満たしてください。

期待する結果

このプログラムではレジスター設定、タイル・属性の直接書き込み、スクロール、背景・スプライトそれぞれのパレット転送を学びます。(0,16)の青い帯は256×16ピクセルです。上段は8×8の塗りつぶし、左半分4×8の棒、8行の三角形を繰り返し、下段は左半分の棒だけを並べます。(16,48)に緑の塗りつぶし64×32、(144,48)に三角形タイルを横8個・縦4個並べた赤い64×32の領域が出ます。

y=96では、x=16・24・32に青い正方形・左半分の棒・三角形、x=80に赤い左半分の棒、x=144に緑の三角形が表示されます。これらは背景タイルです。別パレットを使うスプライトはy=144に並び、x=16が赤い8×8の正方形、x=80が青い左半分4×8の棒、x=144が緑の8行の三角形です。最終スクロールは(0,0)です。

画像ではスプライトの色も含め、16,384ピクセルの形と色を照合します。別の31ケースで、レジスターの置き換え、ステータス・ラッチの副作用、スクロール保存値、0・255バイト転送、増分1・32、マップ端、矩形の寸法0、テーブル選択、属性1バイト全体、背景・スプライト両方のパレットを検証しています。Y=255はレジスターへ書く値としての確認で、正しいスクロール画像の検証ではありません。検証環境はKUROSAKIのNROMで、実機は未検証です。

__nametable_rect: NROM
NROM

使用例の完全なプログラム

ビルドコマンド

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\ppu_intrinsics.c -I .\kitaqfc\lib -I .\kitaq-docs\samples --nes-chr=.\kitaq-docs\samples\api-examples\fc\vram_shapes.chr -o .\out\ppu_intrinsics.nes --no-cache --no-disasm
実装とソース

kitaqfc/lib/intrinsics.h:129

宣言の注記(原文)
Fill rows of nametable zero directly, advancing each row start by 32 bytes.
Keep the rectangle in bounds and use PPU increment one; this does not enqueue or clip.

kitaqfc/kitaqfc/CodeGenerator.cs:4822

csharp
case "__nametable_rect": return EmitKitaqfcHelperCall(funcName, args, ctx, new[] {1,1,1,1,1}, funcName);

void EmitHelperNametableRect()
        {
            EmitHelperStart("__nametable_rect");
            // args: x,y,w,h,tile; writes tile-filled rectangle to nametable 0.
            EmitAsm("LDA", Mem(CallArgBase + 1));
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("LDA", Imm(0));
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp1Address));
            for (int i = 0; i < 5; i++) { EmitAsm("ASL", Mem(_runtimeIntrinsicTmp0Address)); EmitAsm("ROL", Mem(_runtimeIntrinsicTmp1Address)); }
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp0Address)); EmitAsm("CLC"); EmitAsm("ADC", Mem(CallArgBase)); EmitAsm("STA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp1Address)); EmitAsm("ADC", Imm(0x20)); EmitAsm("STA", Mem(_runtimeIntrinsicTmp1Address));
            EmitAsm("LDA", Mem(CallArgBase + 3)); EmitAsm("STA", Mem(_runtimeIntrinsicTmp2Address));
            string rowLoop = NewGeneratedLabel("nt_rect_row");
            string done = NewGeneratedLabel("nt_rect_done");
            _assembly.Add(Expr.Make(Tag.Label, rowLoop));
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp2Address)); EmitAsm("BEQ", Rel(done));
            EmitAsm("LDA", Mem(0x2002)); EmitAsm("LDA", Mem(_runtimeIntrinsicTmp1Address)); EmitAsm("STA", Mem(0x2006)); EmitAsm("LDA", Mem(_runtimeIntrinsicTmp0Address)); EmitAsm("STA", Mem(0x2006));
            EmitAsm("LDY", Imm(0));
            string colLoop = NewGeneratedLabel("nt_rect_col");
            string rowDone = NewGeneratedLabel("nt_rect_row_done");
            _assembly.Add(Expr.Make(Tag.Label, colLoop));
            EmitAsm("CPY", Mem(CallArgBase + 2)); EmitAsm("BEQ", Rel(rowDone));
            EmitAsm("LDA", Mem(CallArgBase + 4)); EmitAsm("STA", Mem(0x2007)); EmitAsm("INY"); EmitAsm("BNE", Rel(colLoop));
            _assembly.Add(Expr.Make(Tag.Label, rowDone));
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp0Address)); EmitAsm("CLC"); EmitAsm("ADC", Imm(32)); EmitAsm("STA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("BCC", Rel(rowLoop + "_nohi"));
            EmitAsm("INC", Mem(_runtimeIntrinsicTmp1Address));
            _assembly.Add(Expr.Make(Tag.Label, rowLoop + "_nohi"));
            EmitAsm("DEC", Mem(_runtimeIntrinsicTmp2Address));
            EmitAsm("JMP", Abs(rowLoop));
            _assembly.Add(Expr.Make(Tag.Label, done));
            EmitAsm("RTS");
        }
__nametable_rect_nt

論理ネームテーブルnt & 3で、1行wタイルをh行埋めます。開始アドレスは0x2000 + (nt & 3)*0x400 + y*32 + xで、行の先頭を32ずつ進めます。書くのはタイル番号だけです。属性更新、切り詰め、ミラーリング設定は行いません。

書式

c
void __nametable_rect_nt(u8 nt, u8 x, u8 y, u8 w, u8 h, u8 tile);

引数

nt

論理ネームテーブルの選択値です。下位2ビットだけを使うため(nt & 3)、たとえば4はテーブル0、7はテーブル3を選びます。独立した保存領域の確保やマッパーのミラーリング変更は行いません。

x / y

ピクセルではなくタイル座標です。32×30タイルの表示領域ではxを0..31、yを0..29に収めます。1タイルは8×8ピクセルです。1バイトの引数を範囲内へ補正しないため、範囲外の座標では意図したタイル領域の外へ書く場合があります。

w / h

タイル単位の幅と高さで、どちらも符号なし1バイトです。x+w<=32、y+h<=30に収めてください。高さ0ならPPUにアクセスしません。幅0ならタイル番号は書きませんが、h行それぞれのラッチリセットとアドレス設定は行います。横に並ぶ行にはPPU増分1を使います。

tile

PPUCTRLで選んだ背景パターンテーブル内のタイル番号0..255です。既存のCHR画像を選ぶ値で、ピクセルの色や転送元ポインターではありません。

戻り値

ありません(void)。

使用条件と制限

PPU直接アクセスは、描画停止中か、処理全体が安全なVBlank時間内に収まる条件で行います。これらの関数は待機・キュー登録・NMI抑止・スクロール復元を行いません。呼び出し中に割り込みで共有ラッチやアドレスを変更しないでください。PPUCTRLのビット2により、書き込み先は1バイトごとに1または32進みます。ミラーリングとアドレスの周回はPPUの動作に従い、CHR-ROMへは書き込めません。アクセス後、描画を始める前に意図したスクロール状態を設定してください。

NESdev: PPU registers

NESdev: PPU scrolling

使用例

c
__nametable_rect_nt(4,18,6,8,4,3); // nt&3=0; red triangle tiles at (144,48).

呼び出し部分のコード例です。完全なプログラムに組み込む際は、記載した初期化とバッファの条件を満たしてください。

期待する結果

このプログラムではレジスター設定、タイル・属性の直接書き込み、スクロール、背景・スプライトそれぞれのパレット転送を学びます。(0,16)の青い帯は256×16ピクセルです。上段は8×8の塗りつぶし、左半分4×8の棒、8行の三角形を繰り返し、下段は左半分の棒だけを並べます。(16,48)に緑の塗りつぶし64×32、(144,48)に三角形タイルを横8個・縦4個並べた赤い64×32の領域が出ます。

y=96では、x=16・24・32に青い正方形・左半分の棒・三角形、x=80に赤い左半分の棒、x=144に緑の三角形が表示されます。これらは背景タイルです。別パレットを使うスプライトはy=144に並び、x=16が赤い8×8の正方形、x=80が青い左半分4×8の棒、x=144が緑の8行の三角形です。最終スクロールは(0,0)です。

画像ではスプライトの色も含め、16,384ピクセルの形と色を照合します。別の31ケースで、レジスターの置き換え、ステータス・ラッチの副作用、スクロール保存値、0・255バイト転送、増分1・32、マップ端、矩形の寸法0、テーブル選択、属性1バイト全体、背景・スプライト両方のパレットを検証しています。Y=255はレジスターへ書く値としての確認で、正しいスクロール画像の検証ではありません。検証環境はKUROSAKIのNROMで、実機は未検証です。

__nametable_rect_nt: NROM
NROM

使用例の完全なプログラム

ビルドコマンド

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\ppu_intrinsics.c -I .\kitaqfc\lib -I .\kitaq-docs\samples --nes-chr=.\kitaq-docs\samples\api-examples\fc\vram_shapes.chr -o .\out\ppu_intrinsics.nes --no-cache --no-disasm
実装とソース

kitaqfc/lib/intrinsics.h:131

宣言の注記(原文)
Fill a rectangle in nametable nt&3 with the same unchecked, direct-write row loop.

kitaqfc/kitaqfc/CodeGenerator.cs:4823

csharp
case "__nametable_rect_nt": return EmitKitaqfcHelperCall(funcName, args, ctx, new[] {1,1,1,1,1,1}, funcName);

void EmitHelperNametableRectNt()
        {
            EmitHelperStart("__nametable_rect_nt");
            // args: nt,x,y,w,h,tile.
            EmitAsm("LDA", Mem(CallArgBase));
            EmitAsm("AND", Imm(3));
            EmitAsm("ASL");
            EmitAsm("ASL");
            EmitAsm("CLC");
            EmitAsm("ADC", Imm(0x20));
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp2Address));
            EmitAsm("LDA", Mem(CallArgBase + 2));
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("LDA", Imm(0));
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp1Address));
            for (int i = 0; i < 5; i++) { EmitAsm("ASL", Mem(_runtimeIntrinsicTmp0Address)); EmitAsm("ROL", Mem(_runtimeIntrinsicTmp1Address)); }
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp0Address)); EmitAsm("CLC"); EmitAsm("ADC", Mem(CallArgBase + 1)); EmitAsm("STA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp1Address)); EmitAsm("ADC", Mem(_runtimeIntrinsicTmp2Address)); EmitAsm("STA", Mem(_runtimeIntrinsicTmp1Address));
            EmitAsm("LDA", Mem(CallArgBase + 4)); EmitAsm("STA", Mem(_runtimeIntrinsicTmp2Address));
            string rowLoop = NewGeneratedLabel("nt_rect_nt_row");
            string done = NewGeneratedLabel("nt_rect_nt_done");
            _assembly.Add(Expr.Make(Tag.Label, rowLoop));
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp2Address)); EmitAsm("BEQ", Rel(done));
            EmitAsm("LDA", Mem(0x2002)); EmitAsm("LDA", Mem(_runtimeIntrinsicTmp1Address)); EmitAsm("STA", Mem(0x2006)); EmitAsm("LDA", Mem(_runtimeIntrinsicTmp0Address)); EmitAsm("STA", Mem(0x2006));
            EmitAsm("LDY", Imm(0));
            string colLoop = NewGeneratedLabel("nt_rect_nt_col");
            string rowDone = NewGeneratedLabel("nt_rect_nt_row_done");
            _assembly.Add(Expr.Make(Tag.Label, colLoop));
            EmitAsm("CPY", Mem(CallArgBase + 3)); EmitAsm("BEQ", Rel(rowDone));
            EmitAsm("LDA", Mem(CallArgBase + 5)); EmitAsm("STA", Mem(0x2007)); EmitAsm("INY"); EmitAsm("BNE", Rel(colLoop));
            _assembly.Add(Expr.Make(Tag.Label, rowDone));
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp0Address)); EmitAsm("CLC"); EmitAsm("ADC", Imm(32)); EmitAsm("STA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("BCC", Rel(rowLoop + "_nohi"));
            EmitAsm("INC", Mem(_runtimeIntrinsicTmp1Address));
            _assembly.Add(Expr.Make(Tag.Label, rowLoop + "_nohi"));
            EmitAsm("DEC", Mem(_runtimeIntrinsicTmp2Address));
            EmitAsm("JMP", Abs(rowLoop));
            _assembly.Add(Expr.Make(Tag.Label, done));
            EmitAsm("RTS");
        }
__nmi_disable

コンパイラが保持するPPUCTRLの値のビット7を落とし、$2000へ書き込んでPPUからのNMI発生を無効にします。他のビットは維持します。CPUのIRQマスクや描画の有効・無効は変更しません。

書式

c
void __nmi_disable(void);

戻り値

ありません(void)。

使用条件と制限

有効化・無効化にはPPUCTRLの保持値を使い、ハードウェアから読み戻しません。$2000へ直接書いても保持値は更新されません。VBlankフラグが立っていると、有効化直後にNMIが発生する場合があります。PPU側を無効にしても、CPUが既に取り込んだ要求は取り消せません。

NESdev: NMI

NESdev: CPU interrupts

使用例

c
__nmi_disable();
before_count=__nmi_ready();
for(i=0;i<2000;i++)frozen=__nmi_ready();
frozen=(u8)(frozen-before_count); // No default-handler advances while off.

呼び出し部分のコード例です。完全なプログラムに組み込む際は、記載した初期化とバッファの条件を満たしてください。

期待する結果

CPUのIRQマスクとPPUのNMI制御の違いを調べるプログラムです。黒背景の青文字で、IRQ DISABLED Iは001、IRQ ENABLED Iは000、SAVED SECTION Iは001、RESTORED Iは000になります。表示するのは状態バイト全体ではなく、取り出したIビットです。FAILED CHECKSは000が期待値です。

256回待つ実演が終わるように、エミュレータを360フレーム以上実行してください。IRQ禁止中でもNMI DELTA 3 WAITSは003になります。NMI DELTA 256 WAITSは1バイトが一周するため000、NMI OFF DELTAも000です。runtime.cをリンクせず標準ハンドラーを使います。別の状態検証では、状態ビットの復元やNMI無効時・空の独自ハンドラー使用時の待機を含む21ケースを確認しています。KUROSAKIのNROM環境での検証で、実機は未検証です。

__nmi_disable: NROM
NROM

使用例の完全なプログラム

ビルドコマンド

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\interrupt_intrinsics.c -I .\kitaqfc\lib -I .\kitaq-docs\samples --nes-chr=.\kitaq-docs\samples\font.chr -o .\out\interrupt_intrinsics.nes --no-cache --no-disasm
実装とソース

kitaqfc/lib/intrinsics.h:174

宣言の注記(原文)
Clear bit 7 in the PPUCTRL shadow and write it to hardware; CPU IRQ masking is separate.

kitaqfc/kitaqfc/CodeGenerator.cs:4730

csharp
case "__nmi_disable":
                    if (!Need(0)) return true;
                    EmitAsm("LDA", Mem(_runtimePpuCtrlShadowAddress)); EmitAsm("AND", Imm(0x7F)); EmitAsm("STA", Mem(_runtimePpuCtrlShadowAddress)); EmitAsm("STA", Mem(0x2000)); return true;
__nmi_enable

コンパイラが保持するPPUCTRLの値のビット7を立て、$2000へ書き込み、PPUからのNMIを有効にします。保持値の他のビットとCPUのIRQマスクは維持します。背景やスプライトの描画を有効にする関数ではありません。

書式

c
void __nmi_enable(void);

戻り値

ありません(void)。

使用条件と制限

有効化・無効化にはPPUCTRLの保持値を使い、ハードウェアから読み戻しません。$2000へ直接書いても保持値は更新されません。VBlankフラグが立っていると、有効化直後にNMIが発生する場合があります。PPU側を無効にしても、CPUが既に取り込んだ要求は取り消せません。

NESdev: NMI

NESdev: CPU interrupts

使用例

c
__nmi_enable(); // Preserve the PPUCTRL shadow's other bits; CPU I stays set.
__nmi_wait(); // The default NMI handler still runs with IRQ masked.

呼び出し部分のコード例です。完全なプログラムに組み込む際は、記載した初期化とバッファの条件を満たしてください。

期待する結果

CPUのIRQマスクとPPUのNMI制御の違いを調べるプログラムです。黒背景の青文字で、IRQ DISABLED Iは001、IRQ ENABLED Iは000、SAVED SECTION Iは001、RESTORED Iは000になります。表示するのは状態バイト全体ではなく、取り出したIビットです。FAILED CHECKSは000が期待値です。

256回待つ実演が終わるように、エミュレータを360フレーム以上実行してください。IRQ禁止中でもNMI DELTA 3 WAITSは003になります。NMI DELTA 256 WAITSは1バイトが一周するため000、NMI OFF DELTAも000です。runtime.cをリンクせず標準ハンドラーを使います。別の状態検証では、状態ビットの復元やNMI無効時・空の独自ハンドラー使用時の待機を含む21ケースを確認しています。KUROSAKIのNROM環境での検証で、実機は未検証です。

__nmi_enable: NROM
NROM

使用例の完全なプログラム

ビルドコマンド

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\interrupt_intrinsics.c -I .\kitaqfc\lib -I .\kitaq-docs\samples --nes-chr=.\kitaq-docs\samples\font.chr -o .\out\interrupt_intrinsics.nes --no-cache --no-disasm
実装とソース

kitaqfc/lib/intrinsics.h:172

宣言の注記(原文)
Set bit 7 in the PPUCTRL shadow and write it to hardware, preserving the other shadow bits.

kitaqfc/kitaqfc/CodeGenerator.cs:4727

csharp
case "__nmi_enable":
                    if (!Need(0)) return true;
                    EmitAsm("LDA", Mem(_runtimePpuCtrlShadowAddress)); EmitAsm("ORA", Imm(0x80)); EmitAsm("STA", Mem(_runtimePpuCtrlShadowAddress)); EmitAsm("STA", Mem(0x2000)); return true;
__nmi_ready

コンパイラ側の現在のNMIカウンターを、待機もクリアもせずに読み取ります。名前にreadyとありますが、返すのは真偽値ではなくカウンターの1バイトです。保存した値と比較すると、標準NMIハンドラーの処理が進んだかを調べられます。

書式

c
u8 __nmi_ready(void);

戻り値

0~255の符号なし1バイトで、標準NMIハンドラーで増加し、256で一周します。0も有効です。256回進むと保存値と現在値が再び一致するため、間隔を空けた読み取りで無制限の経過回数を数えることはできません。

使用条件と制限

このカウンターはコンパイラの標準NMIハンドラー用です。__nes_nmiを定義するとハンドラーが置き換わります。runtime.cは独自のnes_nmi_counterとnes_wait_nmiを使います。空の独自ハンドラーではコンパイラ側のカウンターが進まず、__nmi_waitは戻りません。NMIを有効にし、待機はNMIハンドラーの外から呼んでください。

NESdev: NMI

NESdev: CPU interrupts

使用例

c
before_count=__nmi_ready(); // A wrapping byte, not a Boolean ready flag.

呼び出し部分のコード例です。完全なプログラムに組み込む際は、記載した初期化とバッファの条件を満たしてください。

期待する結果

CPUのIRQマスクとPPUのNMI制御の違いを調べるプログラムです。黒背景の青文字で、IRQ DISABLED Iは001、IRQ ENABLED Iは000、SAVED SECTION Iは001、RESTORED Iは000になります。表示するのは状態バイト全体ではなく、取り出したIビットです。FAILED CHECKSは000が期待値です。

256回待つ実演が終わるように、エミュレータを360フレーム以上実行してください。IRQ禁止中でもNMI DELTA 3 WAITSは003になります。NMI DELTA 256 WAITSは1バイトが一周するため000、NMI OFF DELTAも000です。runtime.cをリンクせず標準ハンドラーを使います。別の状態検証では、状態ビットの復元やNMI無効時・空の独自ハンドラー使用時の待機を含む21ケースを確認しています。KUROSAKIのNROM環境での検証で、実機は未検証です。

__nmi_ready: NROM
NROM

使用例の完全なプログラム

ビルドコマンド

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\interrupt_intrinsics.c -I .\kitaqfc\lib -I .\kitaq-docs\samples --nes-chr=.\kitaq-docs\samples\font.chr -o .\out\interrupt_intrinsics.nes --no-cache --no-disasm
実装とソース

kitaqfc/lib/intrinsics.h:170

宣言の注記(原文)
Return the current wrapping NMI counter byte, not a Boolean ready flag.

kitaqfc/kitaqfc/CodeGenerator.cs:4740

csharp
case "__nmi_ready": if (!Need(0)) return true; EmitAsm("LDA", Mem(_runtimeNmiCounterAddress)); return true;
                // Reset OAMADDR and transfer page 02; the page-selecting variant evaluates its own byte argument.

// The default NMI executes the queued VRAM work and increments the runtime frame counter before RTI.
            if (!_functions.ContainsKey("__nes_nmi"))
            {
                EmitPlacement(0, true);
                _assembly.Add(Expr.Make(Tag.Function, "__kq_nmi_default"));
                // NMI may interrupt argument evaluation, a pointer dispatch or an ordinary expression.
                // Preserve registers and every shared byte touched by the queue executor; RTI restores P.
                EmitAsm("PHA"); EmitAsm("TXA"); EmitAsm("PHA"); EmitAsm("TYA"); EmitAsm("PHA");
                int[] queueScratch = { CallArgBase, CallArgBase + 1, CallArgBase + 2,
                    _runtimeIntrinsicTmp0Address, _runtimeIntrinsicTmp1Address, _runtimeIntrinsicTmp2Address };
                foreach (int address in queueScratch) { EmitAsm("LDA", Mem(address)); EmitAsm("PHA"); }
                // The optional library hook preserves its own registers and uses no
                // compiler scratch. It consumes only RAM records and fixed ROM tables.
                if (_functions.ContainsKey("__nes_audio_vblank_tick"))
                    EmitAsm("JSR", Abs("__nes_audio_vblank_tick"));
                EmitAsm("JSR", Abs("__vramq_exec"));
                EmitAsm("INC", Mem(_runtimeNmiCounterAddress));
                foreach (int address in queueScratch.Reverse()) { EmitAsm("PLA"); EmitAsm("STA", Mem(address)); }
                EmitAsm("PLA"); EmitAsm("TAY"); EmitAsm("PLA"); EmitAsm("TAX"); EmitAsm("PLA");
                EmitAsm("RTI");
            }
__nmi_wait

呼び出し時にコンパイラ側のNMIカウンターを読み、その値が変わるまでループで待ちます。標準NMIハンドラーでは、コミット済みVRAMキューの処理を含む、その後のハンドラー完了を待ちます。NMIの有効化、キューのコミット、タイムアウト処理は行いません。

書式

c
void __nmi_wait(void);

戻り値

ありません(void)。

使用条件と制限

このカウンターはコンパイラの標準NMIハンドラー用です。__nes_nmiを定義するとハンドラーが置き換わります。runtime.cは独自のnes_nmi_counterとnes_wait_nmiを使います。空の独自ハンドラーではコンパイラ側のカウンターが進まず、__nmi_waitは戻りません。NMIを有効にし、待機はNMIハンドラーの外から呼んでください。

NESdev: NMI

NESdev: CPU interrupts

使用例

c
__nmi_wait();__nmi_wait();__nmi_wait();
delta=(u8)(__nmi_ready()-before_count); // Three completed default NMIs.

呼び出し部分のコード例です。完全なプログラムに組み込む際は、記載した初期化とバッファの条件を満たしてください。

期待する結果

CPUのIRQマスクとPPUのNMI制御の違いを調べるプログラムです。黒背景の青文字で、IRQ DISABLED Iは001、IRQ ENABLED Iは000、SAVED SECTION Iは001、RESTORED Iは000になります。表示するのは状態バイト全体ではなく、取り出したIビットです。FAILED CHECKSは000が期待値です。

256回待つ実演が終わるように、エミュレータを360フレーム以上実行してください。IRQ禁止中でもNMI DELTA 3 WAITSは003になります。NMI DELTA 256 WAITSは1バイトが一周するため000、NMI OFF DELTAも000です。runtime.cをリンクせず標準ハンドラーを使います。別の状態検証では、状態ビットの復元やNMI無効時・空の独自ハンドラー使用時の待機を含む21ケースを確認しています。KUROSAKIのNROM環境での検証で、実機は未検証です。

__nmi_wait: NROM
NROM

使用例の完全なプログラム

ビルドコマンド

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\interrupt_intrinsics.c -I .\kitaqfc\lib -I .\kitaq-docs\samples --nes-chr=.\kitaq-docs\samples\font.chr -o .\out\interrupt_intrinsics.nes --no-cache --no-disasm
実装とソース

kitaqfc/lib/intrinsics.h:168

宣言の注記(原文)
NMI / IRQ
Spin until the runtime NMI counter changes; the configured handler must advance it. No timeout is emitted.

kitaqfc/kitaqfc/CodeGenerator.cs:4738

csharp
case "__nmi_wait": if (!Need(0)) return true; EmitAsm("JSR", Abs("__nmi_wait")); return true;
                // Return the current NMI counter byte; callers decide how to compare it with their previous value.

void EmitHelperNmiWait()
        {
            EmitHelperStart("__nmi_wait");
            EmitAsm("LDA", Mem(_runtimeNmiCounterAddress));
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp0Address));
            string loop = NewGeneratedLabel("nmi_wait_loop");
            _assembly.Add(Expr.Make(Tag.Label, loop));
            EmitAsm("LDA", Mem(_runtimeNmiCounterAddress));
            EmitAsm("CMP", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("BEQ", Rel(loop));
            EmitAsm("RTS");
        }

// The default NMI executes the queued VRAM work and increments the runtime frame counter before RTI.
            if (!_functions.ContainsKey("__nes_nmi"))
            {
                EmitPlacement(0, true);
                _assembly.Add(Expr.Make(Tag.Function, "__kq_nmi_default"));
                // NMI may interrupt argument evaluation, a pointer dispatch or an ordinary expression.
                // Preserve registers and every shared byte touched by the queue executor; RTI restores P.
                EmitAsm("PHA"); EmitAsm("TXA"); EmitAsm("PHA"); EmitAsm("TYA"); EmitAsm("PHA");
                int[] queueScratch = { CallArgBase, CallArgBase + 1, CallArgBase + 2,
                    _runtimeIntrinsicTmp0Address, _runtimeIntrinsicTmp1Address, _runtimeIntrinsicTmp2Address };
                foreach (int address in queueScratch) { EmitAsm("LDA", Mem(address)); EmitAsm("PHA"); }
                // The optional library hook preserves its own registers and uses no
                // compiler scratch. It consumes only RAM records and fixed ROM tables.
                if (_functions.ContainsKey("__nes_audio_vblank_tick"))
                    EmitAsm("JSR", Abs("__nes_audio_vblank_tick"));
                EmitAsm("JSR", Abs("__vramq_exec"));
                EmitAsm("INC", Mem(_runtimeNmiCounterAddress));
                foreach (int address in queueScratch.Reverse()) { EmitAsm("PLA"); EmitAsm("STA", Mem(address)); }
                EmitAsm("PLA"); EmitAsm("TAY"); EmitAsm("PLA"); EmitAsm("TAX"); EmitAsm("PLA");
                EmitAsm("RTI");
            }
__oam_clear

ページ02のシャドーバッファにある64個のスプライトすべてのYを0xF0にして非表示にします。タイル番号・属性・X座標は変更しません。

書式

c
void __oam_clear(void);

戻り値

ありません(void)。

使用条件と制限

これらの操作はCPU RAMの0x0200–0x02FFを書き換えます。ハードウェアOAMへ即座に反映されるわけではありません。ページを完成させてから__oam_dmaで転送してください。スプライトライブラリの確保フラグ・座標配列・使用数は更新しません。準備途中のページを割り込み処理で変更・転送しないようにします。

使用例

c
__sprite_set(63,144,111,128,0); // Seed the CLEAR row with a visible entry.
__oam_clear(); // Set every Y byte to 240; tile, attributes and X are retained.

呼び出し部分のコード例です。完全なプログラムに組み込む際は、記載した初期化とバッファの条件を満たしてください。

期待する結果

操作ごとに見出し付きの行を分けたサンプルです。SETとMOVEは(144,16)・(144,32)の8×8右向き矢印、TILEは(144,48)の緑の四角、ATTRは(144,64)の上下左右を反転した矢印です。HIDEの(144,80)とCLEARの(144,112)には何も表示されません。METAは(136,96)と(144,96)の2個組で、2個目を左右反転します。NEXT 07は5・6番へ書き込んだ後の次の位置です。矢印の輪郭は赤、軸は緑、先端の一部は青です。検証では画像ごとに8,704ピクセルと転送後のOAMデータを照合します。実機では未検証です。

TRANSFERの(144,128)には矢印を表示します。このプログラムは、個別のスプライト操作で設定した全項目を含むページ02を__oam_dmaで直接転送します。

__oam_clear: PAGE02
PAGE02

使用例の完全なプログラム

ビルドコマンド

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\oam_intrinsics.c -I .\kitaqfc\lib -I .\kitaq-docs\samples --nes-chr=.\kitaq-docs\samples\sprite_example.chr -o .\out\oam_intrinsics.nes --no-cache --no-disasm
実装とソース

kitaqfc/lib/intrinsics.h:190

宣言の注記(原文)
Hide all 64 page-02 shadow sprites by writing Y=F0; hardware OAM changes only after DMA.
csharp
EmitHelperStart("__oam_clear");
            EmitAsm("LDX", Imm(0));
            EmitAsm("LDA", Imm(0xF0));
            string loop = NewGeneratedLabel("oam_clear_loop");
            _assembly.Add(Expr.Make(Tag.Label, loop));
            EmitAsm("STA", new AsmOperand(0x0200, AddressMode.AbsoluteX));
            EmitAsm("INX"); EmitAsm("INX"); EmitAsm("INX"); EmitAsm("INX");
            EmitAsm("BNE", Rel(loop));
            EmitAsm("RTS");
__oam_dma

0x0200–0x02FFのシャドーページ全体256バイトを、ハードウェアOAMの先頭へ転送します。転送中はCPUが停止し、完了後に処理が再開します。

書式

c
void __oam_dma(void);

戻り値

ありません(void)。

使用条件と制限

VBlank中、または描画を停止して呼び出してください。どちらのDMA組み込み関数もVBlank待ちは行わず、転送前にOAMADDRを0にします。実機のDMAは通常513または514サイクルCPUを停止し、DMC動作によって延長されることがあります。KUROSAKIは513サイクルで近似するため、画像検証だけでは実機の正確なタイミングは確認できません。コンパイラは呼び出し元の名前で判定するので、明示的にVBlankを待っていてもKQ2421が出る場合があります。

使用例

c
m_wait();__oam_dma(); // Transfer all 256 bytes from page 02 at VBlank.

呼び出し部分のコード例です。完全なプログラムに組み込む際は、記載した初期化とバッファの条件を満たしてください。

期待する結果

操作ごとに見出し付きの行を分けたサンプルです。SETとMOVEは(144,16)・(144,32)の8×8右向き矢印、TILEは(144,48)の緑の四角、ATTRは(144,64)の上下左右を反転した矢印です。HIDEの(144,80)とCLEARの(144,112)には何も表示されません。METAは(136,96)と(144,96)の2個組で、2個目を左右反転します。NEXT 07は5・6番へ書き込んだ後の次の位置です。矢印の輪郭は赤、軸は緑、先端の一部は青です。検証では画像ごとに8,704ピクセルと転送後のOAMデータを照合します。実機では未検証です。

TRANSFERの(144,128)には矢印を表示します。このプログラムは、個別のスプライト操作で設定した全項目を含むページ02を__oam_dmaで直接転送します。

__oam_dma: PAGE02
PAGE02

使用例の完全なプログラム

ビルドコマンド

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\oam_intrinsics.c -I .\kitaqfc\lib -I .\kitaq-docs\samples --nes-chr=.\kitaq-docs\samples\sprite_example.chr -o .\out\oam_intrinsics.nes --no-cache --no-disasm
実装とソース

kitaqfc/lib/intrinsics.h:186

宣言の注記(原文)
OAM / input
Set OAMADDR to zero and start DMA from CPU page 02; arrange safe PPU timing in the caller.
csharp
case "__oam_dma": if (!Need(0)) return true; EmitAsm("LDA", Imm(0)); EmitAsm("STA", Mem(0x2003)); EmitAsm("LDA", Imm(0x02)); EmitAsm("STA", Mem(0x4014)); return true;
__oam_dma_page

選択したCPUメモリページの256バイトを、ハードウェアOAMの先頭へ転送します。別に用意したOAMページを、ページ02へコピーせずに使えます。転送元は変更しません。

書式

c
void __oam_dma_page(u8 page);

引数

page

転送元CPUアドレスの上位バイトです。0x0400–0x04FFなら0x0400ではなく4を渡します。256バイトすべてが読み出せるページを用意し、必要なバンクを選択しておいてください。読み出しに副作用があるI/Oレジスタを含むページは避けます。転送元の検査は行いません。

戻り値

ありません(void)。

使用条件と制限

VBlank中、または描画を停止して呼び出してください。どちらのDMA組み込み関数もVBlank待ちは行わず、転送前にOAMADDRを0にします。実機のDMAは通常513または514サイクルCPUを停止し、DMC動作によって延長されることがあります。KUROSAKIは513サイクルで近似するため、画像検証だけでは実機の正確なタイミングは確認できません。コンパイラは呼び出し元の名前で判定するので、明示的にVBlankを待っていてもKQ2421が出る場合があります。

使用例

c
m_wait();__oam_dma_page(4); // The argument is a page number, not 0x0400.

呼び出し部分のコード例です。完全なプログラムに組み込む際は、記載した初期化とバッファの条件を満たしてください。

期待する結果

操作ごとに見出し付きの行を分けたサンプルです。SETとMOVEは(144,16)・(144,32)の8×8右向き矢印、TILEは(144,48)の緑の四角、ATTRは(144,64)の上下左右を反転した矢印です。HIDEの(144,80)とCLEARの(144,112)には何も表示されません。METAは(136,96)と(144,96)の2個組で、2個目を左右反転します。NEXT 07は5・6番へ書き込んだ後の次の位置です。矢印の輪郭は赤、軸は緑、先端の一部は青です。検証では画像ごとに8,704ピクセルと転送後のOAMデータを照合します。実機では未検証です。

この例では準備済みのページ02を0x0400の別配列へコピーし、7番のタイルだけを129に変更します。__oam_dma_page(4)により、TRANSFERの(144,128)には緑の四角を表示しますが、ページ02には矢印のタイル128が残ります。他の行は標準ページの例と同じです。検証ではハードウェアOAM全256バイトをページ04と照合し、ページ02が変更されていないことも確認します。

__oam_dma_page: PAGE04
PAGE04

使用例の完全なプログラム

ビルドコマンド

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\oam_page.c -I .\kitaqfc\lib -I .\kitaq-docs\samples --nes-chr=.\kitaq-docs\samples\sprite_example.chr -o .\out\oam_page.nes --no-cache --no-disasm
実装とソース

kitaqfc/lib/intrinsics.h:188

宣言の注記(原文)
Set OAMADDR to zero and DMA 256 bytes from page<<8. The source page must remain readable.
csharp
case "__oam_dma_page": if (!Need(1)) return true; EmitAsm("LDA", Imm(0)); EmitAsm("STA", Mem(0x2003)); Load1(args[0]); EmitAsm("STA", Mem(0x4014)); return true;
__pad_buttons

渡されたNES形式の値のbit 0~3(pad & 0x0F)を残し、A・B・Select・Startだけを取り出します。読込済みの値を加工するだけで、コントローラにはアクセスしません。

書式

c
u8 __pad_buttons(u8 pad);

引数

pad

__pad_read1_safe() の戻り値など、読込済みの8ビットNESパッド値です。ポインタ、ポート番号、input_* の共通形式のマスクではなく、その値自体を渡します。

戻り値

上記のビット順による0~15のマスクです。複数のビットが立つ場合があるため、全体が1かどうかではなく result & bit で特定のボタンを調べます。

使用条件と制限

NESの生の形式は A=0x01、B=0x02、SELECT=0x04、START=0x08、UP=0x10、DOWN=0x20、LEFT=0x40、RIGHT=0x80 です。ビット1が押下中です。nes_pad_* と生の __pad_* の結果にはこの値を使い、input.h の共通 BTN_* マスクとは混同しないでください。

使用例

c
buttons=__pad_buttons(raw1); // Low nibble of 129: 1, the A button.

呼び出し部分のコード例です。完全なプログラムに組み込む際は、記載した初期化とバッファの条件を満たしてください。

期待する結果

2つのコントローラーポート、通常読み取りと一致確認付き読み取り、マスク抽出、短縮マクロを比較するプログラムです。コントローラー1ではAと右、コントローラー2ではBと左を同時に押します。RAW P1、SAFE P1、ALIAS P1は129、P2の3項目は66になります。BUTTONS P1は1、DIRS P1は128です。方向の抽出では上位ビットの位置を保ちます。FAILED CHECKSは0が正常です。取得した値は画面に残ります。

__pad_buttons: NROM
NROM

使用例の完全なプログラム

ビルドコマンド

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\input_raw.c -I .\kitaqfc\lib -I .\kitaq-docs\samples -o .\out\input_raw.nes --mapper=nrom --nes-chr=.\kitaq-docs\samples\font.chr --no-disasm --no-cache
実装とソース

kitaqfc/lib/intrinsics.h:213

宣言の注記(原文)
Return pad & 0F for A/B/Select/Start; this masks a supplied snapshot and performs no polling.
__pad_dirs

渡されたNES形式の値のbit 4~7(pad & 0xF0)を残し、上・下・左・右だけを取り出します。ビットは元の位置のままなので、右だけなら8ではなく128です。コントローラの読込は行いません。

書式

c
u8 __pad_dirs(u8 pad);

引数

pad

__pad_read1_safe() の戻り値など、読込済みの8ビットNESパッド値です。ポインタ、ポート番号、input_* の共通形式のマスクではなく、その値自体を渡します。

戻り値

渡された値と 0xF0 のANDで、0、16、32、…、240のいずれかです。方向ビットはbit 4~7の位置を保ちます。

使用条件と制限

NESの生の形式は A=0x01、B=0x02、SELECT=0x04、START=0x08、UP=0x10、DOWN=0x20、LEFT=0x40、RIGHT=0x80 です。ビット1が押下中です。nes_pad_* と生の __pad_* の結果にはこの値を使い、input.h の共通 BTN_* マスクとは混同しないでください。

使用例

c
directions=__pad_dirs(raw1); // High nibble retained in place: 128, not 8.

呼び出し部分のコード例です。完全なプログラムに組み込む際は、記載した初期化とバッファの条件を満たしてください。

期待する結果

2つのコントローラーポート、通常読み取りと一致確認付き読み取り、マスク抽出、短縮マクロを比較するプログラムです。コントローラー1ではAと右、コントローラー2ではBと左を同時に押します。RAW P1、SAFE P1、ALIAS P1は129、P2の3項目は66になります。BUTTONS P1は1、DIRS P1は128です。方向の抽出では上位ビットの位置を保ちます。FAILED CHECKSは0が正常です。取得した値は画面に残ります。

__pad_dirs: NROM
NROM

使用例の完全なプログラム

ビルドコマンド

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\input_raw.c -I .\kitaqfc\lib -I .\kitaq-docs\samples -o .\out\input_raw.nes --mapper=nrom --nes-chr=.\kitaq-docs\samples\font.chr --no-disasm --no-cache
実装とソース

kitaqfc/lib/intrinsics.h:215

宣言の注記(原文)
Return pad & F0 in the original bit positions; this does not normalize directions to bits 0..3.
__pad_read1

$4016 に1→0を書いて両コントローラの状態をラッチし、$4016 のD0からコントローラ1の8ビットをシリアル読込します。NES形式の押下中マスクを1回読み取って返します。再試行や押下・解放の変化の計算は行いません。

書式

c
u8 __pad_read1(void);

戻り値

NESの生のビット順による8ビットの押下中マスクです。各ビット1が押されているボタンを表し、0はなしです。0/1に正規化された真偽値ではありません。

使用条件と制限

NESの生の形式は A=0x01、B=0x02、SELECT=0x04、START=0x08、UP=0x10、DOWN=0x20、LEFT=0x40、RIGHT=0x80 です。ビット1が押下中です。nes_pad_* と生の __pad_* の結果にはこの値を使い、input.h の共通 BTN_* マスクとは混同しないでください。

この呼び出しはハードウェアの状態を新たに読みます。input_* や nes_pad1_* の保存状態は更新せず、押下・解放イベントやリピートも生成しません。必要なら読み取った値を保存し、自分で保持した前回マスクと比較して変化を求めてください。

使用例

c
raw1=__pad_read1(); // Controller 1: A (1) | Right (128) = 129.

呼び出し部分のコード例です。完全なプログラムに組み込む際は、記載した初期化とバッファの条件を満たしてください。

期待する結果

2つのコントローラーポート、通常読み取りと一致確認付き読み取り、マスク抽出、短縮マクロを比較するプログラムです。コントローラー1ではAと右、コントローラー2ではBと左を同時に押します。RAW P1、SAFE P1、ALIAS P1は129、P2の3項目は66になります。BUTTONS P1は1、DIRS P1は128です。方向の抽出では上位ビットの位置を保ちます。FAILED CHECKSは0が正常です。取得した値は画面に残ります。

__pad_read1: NROM
NROM

使用例の完全なプログラム

ビルドコマンド

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\input_raw.c -I .\kitaqfc\lib -I .\kitaq-docs\samples -o .\out\input_raw.nes --mapper=nrom --nes-chr=.\kitaq-docs\samples\font.chr --no-disasm --no-cache
実装とソース

kitaqfc/lib/intrinsics.h:205

宣言の注記(原文)
Latch and sample eight D0 bits from port 1; bit order is A,B,Select,Start,Up,Down,Left,Right.
__pad_read1_d1

$4016の拡張入力D1から8ビットをシリアル読み取りします。$4016に1、0の順に書いてコントローラーの状態をラッチし、ビット1を8回読み、順に結果のビット0〜7へ格納します。ポート1のD1を1回読み取る処理です。通常のコントローラーが使うD0の読み取りや、一致確認のための再試行は行いません。

書式

c
u8 __pad_read1_d1(void);

戻り値

NESの生のビット順による8ビットの押下中マスクです。各ビット1が押されているボタンを表し、0はなしです。0/1に正規化された真偽値ではありません。

使用条件と制限

NESの生の形式は A=0x01、B=0x02、SELECT=0x04、START=0x08、UP=0x10、DOWN=0x20、LEFT=0x40、RIGHT=0x80 です。ビット1が押下中です。nes_pad_* と生の __pad_* の結果にはこの値を使い、input.h の共通 BTN_* マスクとは混同しないでください。

選択したD1に互換性のある8ビットのシリアルパッド信号を出す周辺機器で使います。接続機器の識別や、すべての拡張機器の通信方式への対応は行いません。各呼び出しで両コントローラーの読み取り列をラッチし直すため、1つの読み取りを完了させてから次を始めてください。結果は新たに取得した状態で、input_*やnes_pad1_*の保存済み状態は更新しません。

公開版KUROSAKIのコントローラーモデルが返すのはD0と固定のビット6だけです。そのため通常のボタンを押していても、同エミュレーターではD1とマイクのビットは常に0になります。付属の検証で確認したのは、ビルド、通常ボタン入力、この0の信号表示です。周辺機器からの0以外の信号、電気的タイミング、実機での互換性は未確認です。ここで0が表示されても、接続機器が動作する証明や、実機でも読み取れないという証明にはなりません。

使用例

c
d1_first=__pad_read1_d1();

呼び出し部分のコード例です。完全なプログラムに組み込む際は、記載した初期化とバッファの条件を満たしてください。

期待する結果

通常入力のD0、拡張入力のD1、マイクのデジタルレベルを比較するプログラムです。状態が取得されるまでコントローラー1のAとコントローラー2のBを押します。NORMAL P1とNORMAL P2は001、002になります。付属のKUROSAKI実行結果では、D1 PORT 1、D1 PORT 2、EXP ALIAS 1、EXP ALIAS 2、MIC LEVEL、MIC DIRECTはすべて000です。対応する信号線がエミュレートされていないためです。取得後の表示は固定されます。適切な周辺機器と信号線の実装がある環境では実際の入力で値が変わるため、この0を期待値とせず、上記のビット配置に従って読み取ってください。

公開版KUROSAKIのコントローラーモデルが返すのはD0と固定のビット6だけです。そのため通常のボタンを押していても、同エミュレーターではD1とマイクのビットは常に0になります。付属の検証で確認したのは、ビルド、通常ボタン入力、この0の信号表示です。周辺機器からの0以外の信号、電気的タイミング、実機での互換性は未確認です。ここで0が表示されても、接続機器が動作する証明や、実機でも読み取れないという証明にはなりません。

__pad_read1_d1: NROM
NROM

使用例の完全なプログラム

ビルドコマンド

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\expansion_input.c -I .\kitaq-docs\samples -I .\kitaqfc\lib -o .\out\expansion_input.nes --mapper=nrom --nes-chr=.\kitaq-docs\samples\font.chr --no-cache --no-disasm
実装とソース

kitaqfc/lib/intrinsics.h:223

宣言の注記(原文)
Famicom/NES expansion input devices.
D1 readers are for expansion-port controllers/joysticks that expose a
standard 8-bit joypad stream on data line D1 instead of D0.
__joypad2p_voice() / __mic_read2p() return the Famicom 2P microphone bit.

Latch and read an eight-button stream from port-1 data line D1 rather than D0.
__pad_read1_safe

__pad_read1() による全ボタンの読込を2回ずつ繰り返し、コントローラ1の二つの読込値が一致したらNES形式のマスクを返します。新たな押下イベントではなく、押下中の状態です。

書式

c
u8 __pad_read1_safe(void);

戻り値

NESの生のビット順による8ビットの押下中マスクです。各ビット1が押されているボタンを表し、0はなしです。0/1に正規化された真偽値ではありません。

使用条件と制限

NESの生の形式は A=0x01、B=0x02、SELECT=0x04、START=0x08、UP=0x10、DOWN=0x20、LEFT=0x40、RIGHT=0x80 です。ビット1が押下中です。nes_pad_* と生の __pad_* の結果にはこの値を使い、input.h の共通 BTN_* マスクとは混同しないでください。

この呼び出しはハードウェアの状態を新たに読みます。input_* や nes_pad1_* の保存状態は更新せず、押下・解放イベントやリピートも生成しません。必要なら読み取った値を保存し、自分で保持した前回マスクと比較して変化を求めてください。

safe版は全ボタンを2回ずつ読み、二つのマスクが一致するまで繰り返します。各読込で $4016 をストローブし、両コントローラのシリアル読込位置を先頭へ戻します。再試行回数に上限はありません。この一致確認だけで、すべてのDMC・周辺機器との組み合わせを保証するわけではありません。

使用例

c
safe1=__pad_read1_safe(); // Repeat paired controller-1 reads until they agree.

呼び出し部分のコード例です。完全なプログラムに組み込む際は、記載した初期化とバッファの条件を満たしてください。

期待する結果

2つのコントローラーポート、通常読み取りと一致確認付き読み取り、マスク抽出、短縮マクロを比較するプログラムです。コントローラー1ではAと右、コントローラー2ではBと左を同時に押します。RAW P1、SAFE P1、ALIAS P1は129、P2の3項目は66になります。BUTTONS P1は1、DIRS P1は128です。方向の抽出では上位ビットの位置を保ちます。FAILED CHECKSは0が正常です。取得した値は画面に残ります。

__pad_read1_safe: NROM
NROM

使用例の完全なプログラム

ビルドコマンド

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\input_raw.c -I .\kitaqfc\lib -I .\kitaq-docs\samples -o .\out\input_raw.nes --mapper=nrom --nes-chr=.\kitaq-docs\samples\font.chr --no-disasm --no-cache
実装とソース

kitaqfc/lib/intrinsics.h:209

宣言の注記(原文)
Repeat pairs of port-1 polls until both bytes match; the retry loop has no fixed limit.
__pad_read2

$4016 に1→0を書いて両コントローラの状態をラッチし、$4017 のD0からコントローラ2の8ビットをシリアル読込します。NES形式の押下中マスクを1回読み取って返します。再試行や押下・解放の変化の計算は行いません。

書式

c
u8 __pad_read2(void);

戻り値

NESの生のビット順による8ビットの押下中マスクです。各ビット1が押されているボタンを表し、0はなしです。0/1に正規化された真偽値ではありません。

使用条件と制限

NESの生の形式は A=0x01、B=0x02、SELECT=0x04、START=0x08、UP=0x10、DOWN=0x20、LEFT=0x40、RIGHT=0x80 です。ビット1が押下中です。nes_pad_* と生の __pad_* の結果にはこの値を使い、input.h の共通 BTN_* マスクとは混同しないでください。

この呼び出しはハードウェアの状態を新たに読みます。input_* や nes_pad1_* の保存状態は更新せず、押下・解放イベントやリピートも生成しません。必要なら読み取った値を保存し、自分で保持した前回マスクと比較して変化を求めてください。

使用例

c
raw2=__pad_read2(); // Controller 2: B (2) | Left (64) = 66.

呼び出し部分のコード例です。完全なプログラムに組み込む際は、記載した初期化とバッファの条件を満たしてください。

期待する結果

2つのコントローラーポート、通常読み取りと一致確認付き読み取り、マスク抽出、短縮マクロを比較するプログラムです。コントローラー1ではAと右、コントローラー2ではBと左を同時に押します。RAW P1、SAFE P1、ALIAS P1は129、P2の3項目は66になります。BUTTONS P1は1、DIRS P1は128です。方向の抽出では上位ビットの位置を保ちます。FAILED CHECKSは0が正常です。取得した値は画面に残ります。

__pad_read2: NROM
NROM

使用例の完全なプログラム

ビルドコマンド

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\input_raw.c -I .\kitaqfc\lib -I .\kitaq-docs\samples -o .\out\input_raw.nes --mapper=nrom --nes-chr=.\kitaq-docs\samples\font.chr --no-disasm --no-cache
実装とソース

kitaqfc/lib/intrinsics.h:207

宣言の注記(原文)
Latch and sample eight D0 bits from port 2 in the same NES button order.
__pad_read2_d1

$4017の拡張入力D1から8ビットをシリアル読み取りします。$4016に1、0の順に書いてコントローラーの状態をラッチし、ビット1を8回読み、順に結果のビット0〜7へ格納します。ポート2のD1を1回読み取る処理です。通常のコントローラーが使うD0の読み取りや、一致確認のための再試行は行いません。

書式

c
u8 __pad_read2_d1(void);

戻り値

NESの生のビット順による8ビットの押下中マスクです。各ビット1が押されているボタンを表し、0はなしです。0/1に正規化された真偽値ではありません。

使用条件と制限

NESの生の形式は A=0x01、B=0x02、SELECT=0x04、START=0x08、UP=0x10、DOWN=0x20、LEFT=0x40、RIGHT=0x80 です。ビット1が押下中です。nes_pad_* と生の __pad_* の結果にはこの値を使い、input.h の共通 BTN_* マスクとは混同しないでください。

選択したD1に互換性のある8ビットのシリアルパッド信号を出す周辺機器で使います。接続機器の識別や、すべての拡張機器の通信方式への対応は行いません。各呼び出しで両コントローラーの読み取り列をラッチし直すため、1つの読み取りを完了させてから次を始めてください。結果は新たに取得した状態で、input_*やnes_pad1_*の保存済み状態は更新しません。

公開版KUROSAKIのコントローラーモデルが返すのはD0と固定のビット6だけです。そのため通常のボタンを押していても、同エミュレーターではD1とマイクのビットは常に0になります。付属の検証で確認したのは、ビルド、通常ボタン入力、この0の信号表示です。周辺機器からの0以外の信号、電気的タイミング、実機での互換性は未確認です。ここで0が表示されても、接続機器が動作する証明や、実機でも読み取れないという証明にはなりません。

使用例

c
d1_second=__pad_read2_d1();

呼び出し部分のコード例です。完全なプログラムに組み込む際は、記載した初期化とバッファの条件を満たしてください。

期待する結果

通常入力のD0、拡張入力のD1、マイクのデジタルレベルを比較するプログラムです。状態が取得されるまでコントローラー1のAとコントローラー2のBを押します。NORMAL P1とNORMAL P2は001、002になります。付属のKUROSAKI実行結果では、D1 PORT 1、D1 PORT 2、EXP ALIAS 1、EXP ALIAS 2、MIC LEVEL、MIC DIRECTはすべて000です。対応する信号線がエミュレートされていないためです。取得後の表示は固定されます。適切な周辺機器と信号線の実装がある環境では実際の入力で値が変わるため、この0を期待値とせず、上記のビット配置に従って読み取ってください。

公開版KUROSAKIのコントローラーモデルが返すのはD0と固定のビット6だけです。そのため通常のボタンを押していても、同エミュレーターではD1とマイクのビットは常に0になります。付属の検証で確認したのは、ビルド、通常ボタン入力、この0の信号表示です。周辺機器からの0以外の信号、電気的タイミング、実機での互換性は未確認です。ここで0が表示されても、接続機器が動作する証明や、実機でも読み取れないという証明にはなりません。

__pad_read2_d1: NROM
NROM

使用例の完全なプログラム

ビルドコマンド

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\expansion_input.c -I .\kitaq-docs\samples -I .\kitaqfc\lib -o .\out\expansion_input.nes --mapper=nrom --nes-chr=.\kitaq-docs\samples\font.chr --no-cache --no-disasm
実装とソース

kitaqfc/lib/intrinsics.h:225

宣言の注記(原文)
Latch and read an eight-button stream from port-2 data line D1 rather than D0.
__pad_read2_safe

__pad_read2() による全ボタンの読込を2回ずつ繰り返し、コントローラ2の二つの読込値が一致したらNES形式のマスクを返します。新たな押下イベントではなく、押下中の状態です。

書式

c
u8 __pad_read2_safe(void);

戻り値

NESの生のビット順による8ビットの押下中マスクです。各ビット1が押されているボタンを表し、0はなしです。0/1に正規化された真偽値ではありません。

使用条件と制限

NESの生の形式は A=0x01、B=0x02、SELECT=0x04、START=0x08、UP=0x10、DOWN=0x20、LEFT=0x40、RIGHT=0x80 です。ビット1が押下中です。nes_pad_* と生の __pad_* の結果にはこの値を使い、input.h の共通 BTN_* マスクとは混同しないでください。

この呼び出しはハードウェアの状態を新たに読みます。input_* や nes_pad1_* の保存状態は更新せず、押下・解放イベントやリピートも生成しません。必要なら読み取った値を保存し、自分で保持した前回マスクと比較して変化を求めてください。

safe版は全ボタンを2回ずつ読み、二つのマスクが一致するまで繰り返します。各読込で $4016 をストローブし、両コントローラのシリアル読込位置を先頭へ戻します。再試行回数に上限はありません。この一致確認だけで、すべてのDMC・周辺機器との組み合わせを保証するわけではありません。

使用例

c
safe2=__pad_read2_safe(); // The same consistency check on controller 2.

呼び出し部分のコード例です。完全なプログラムに組み込む際は、記載した初期化とバッファの条件を満たしてください。

期待する結果

2つのコントローラーポート、通常読み取りと一致確認付き読み取り、マスク抽出、短縮マクロを比較するプログラムです。コントローラー1ではAと右、コントローラー2ではBと左を同時に押します。RAW P1、SAFE P1、ALIAS P1は129、P2の3項目は66になります。BUTTONS P1は1、DIRS P1は128です。方向の抽出では上位ビットの位置を保ちます。FAILED CHECKSは0が正常です。取得した値は画面に残ります。

__pad_read2_safe: NROM
NROM

使用例の完全なプログラム

ビルドコマンド

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\input_raw.c -I .\kitaqfc\lib -I .\kitaq-docs\samples -o .\out\input_raw.nes --mapper=nrom --nes-chr=.\kitaq-docs\samples\font.chr --no-disasm --no-cache
実装とソース

kitaqfc/lib/intrinsics.h:211

宣言の注記(原文)
Repeat pairs of port-2 polls until both bytes match; this does not populate pad.c globals.
__palette_bg_load

PPUラッチをリセットし、アドレスを0x3F00にして、4組の背景パレット用に転送元の16バイトをPPUDATAへ書きます。即時転送であり、ネームテーブルの属性によるパレット選択やタイル画像の読み込みは行いません。

書式

c
void __palette_bg_load(const u8* pal16);

引数

pal16

NESのパレットコード16バイトを読めるCPU側ポインターです。RGBの3成分ではなく、パレットコードを渡します。同期呼び出しが戻るまで転送元のバンクを保ってください。バンク切り替えや長さ確認は行いません。意図した16個のパレットアドレスへ書くため、PPU増分1を使います。

戻り値

ありません(void)。

使用条件と制限

PPUアドレス0x3F10/14/18/1Cは0x3F00/04/08/0Cと同じ保存場所です。スプライトパレットを読み込むと、これらの背景エントリーも書き換えます。コンパイラは指定バイトをそのまま送り、色の妥当性確認、パレットの合成、共有エントリーの保存は行いません。

PPU直接アクセスは、描画停止中か、処理全体が安全なVBlank時間内に収まる条件で行います。これらの関数は待機・キュー登録・NMI抑止・スクロール復元を行いません。呼び出し中に割り込みで共有ラッチやアドレスを変更しないでください。PPUCTRLのビット2により、書き込み先は1バイトごとに1または32進みます。ミラーリングとアドレスの周回はPPUの動作に従い、CHR-ROMへは書き込めません。アクセス後、描画を始める前に意図したスクロール状態を設定してください。

NESdev: PPU registers

NESdev: PPU scrolling

使用例

c
__palette_bg_load(scene_palette); // Four background palettes, 16 bytes total.

呼び出し部分のコード例です。完全なプログラムに組み込む際は、記載した初期化とバッファの条件を満たしてください。

期待する結果

このプログラムではレジスター設定、タイル・属性の直接書き込み、スクロール、背景・スプライトそれぞれのパレット転送を学びます。(0,16)の青い帯は256×16ピクセルです。上段は8×8の塗りつぶし、左半分4×8の棒、8行の三角形を繰り返し、下段は左半分の棒だけを並べます。(16,48)に緑の塗りつぶし64×32、(144,48)に三角形タイルを横8個・縦4個並べた赤い64×32の領域が出ます。

y=96では、x=16・24・32に青い正方形・左半分の棒・三角形、x=80に赤い左半分の棒、x=144に緑の三角形が表示されます。これらは背景タイルです。別パレットを使うスプライトはy=144に並び、x=16が赤い8×8の正方形、x=80が青い左半分4×8の棒、x=144が緑の8行の三角形です。最終スクロールは(0,0)です。

画像ではスプライトの色も含め、16,384ピクセルの形と色を照合します。別の31ケースで、レジスターの置き換え、ステータス・ラッチの副作用、スクロール保存値、0・255バイト転送、増分1・32、マップ端、矩形の寸法0、テーブル選択、属性1バイト全体、背景・スプライト両方のパレットを検証しています。Y=255はレジスターへ書く値としての確認で、正しいスクロール画像の検証ではありません。検証環境はKUROSAKIのNROMで、実機は未検証です。

__palette_bg_load: NROM
NROM

使用例の完全なプログラム

ビルドコマンド

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\ppu_intrinsics.c -I .\kitaqfc\lib -I .\kitaq-docs\samples --nes-chr=.\kitaq-docs\samples\api-examples\fc\vram_shapes.chr -o .\out\ppu_intrinsics.nes --no-cache --no-disasm
実装とソース

kitaqfc/lib/intrinsics.h:138

宣言の注記(原文)
Stream exactly 16 source bytes to 3F00. Arrange PPU access timing and increment one.

kitaqfc/kitaqfc/CodeGenerator.cs:4827

csharp
case "__palette_bg_load": return EmitKitaqfcHelperCall(funcName, args, ctx, new[] {2}, funcName);

void EmitHelperPalette(string name, int ppuAddress)
        {
            EmitHelperStart(name);
            EmitAsm("LDA", Mem(0x2002));
            EmitAsm("LDA", Imm((ppuAddress >> 8) & 0xFF));
            EmitAsm("STA", Mem(0x2006));
            EmitAsm("LDA", Imm(ppuAddress & 0xFF));
            EmitAsm("STA", Mem(0x2006));
            EmitAsm("LDY", Imm(0));
            string loop = NewGeneratedLabel("pal_loop");
            _assembly.Add(Expr.Make(Tag.Label, loop));
            EmitAsm("LDA", IndY(CallArgBase));
            EmitAsm("STA", Mem(0x2007));
            EmitAsm("INY");
            EmitAsm("CPY", Imm(16));
            EmitAsm("BNE", Rel(loop));
            EmitAsm("RTS");
        }

EmitHelperPalette("__palette_bg_load", 0x3F00);
__palette_sp_load

PPUラッチをリセットし、アドレスを0x3F10にして、4組のスプライトパレット用に転送元の16バイトをPPUDATAへ書きます。表示に使うパレットはスプライト属性のビットで選びます。この呼び出しはOAMの変更やスプライトのタイル画像転送を行いません。

書式

c
void __palette_sp_load(const u8* pal16);

引数

pal16

NESのパレットコード16バイトを読めるCPU側ポインターです。RGBの3成分ではなく、パレットコードを渡します。同期呼び出しが戻るまで転送元のバンクを保ってください。バンク切り替えや長さ確認は行いません。意図した16個のパレットアドレスへ書くため、PPU増分1を使います。

戻り値

ありません(void)。

使用条件と制限

PPUアドレス0x3F10/14/18/1Cは0x3F00/04/08/0Cと同じ保存場所です。スプライトパレットを読み込むと、これらの背景エントリーも書き換えます。コンパイラは指定バイトをそのまま送り、色の妥当性確認、パレットの合成、共有エントリーの保存は行いません。

PPU直接アクセスは、描画停止中か、処理全体が安全なVBlank時間内に収まる条件で行います。これらの関数は待機・キュー登録・NMI抑止・スクロール復元を行いません。呼び出し中に割り込みで共有ラッチやアドレスを変更しないでください。PPUCTRLのビット2により、書き込み先は1バイトごとに1または32進みます。ミラーリングとアドレスの周回はPPUの動作に従い、CHR-ROMへは書き込めません。アクセス後、描画を始める前に意図したスクロール状態を設定してください。

NESdev: PPU registers

NESdev: PPU scrolling

使用例

c
__palette_sp_load(scene_palette); // Four sprite palettes, 16 bytes total.

呼び出し部分のコード例です。完全なプログラムに組み込む際は、記載した初期化とバッファの条件を満たしてください。

期待する結果

このプログラムではレジスター設定、タイル・属性の直接書き込み、スクロール、背景・スプライトそれぞれのパレット転送を学びます。(0,16)の青い帯は256×16ピクセルです。上段は8×8の塗りつぶし、左半分4×8の棒、8行の三角形を繰り返し、下段は左半分の棒だけを並べます。(16,48)に緑の塗りつぶし64×32、(144,48)に三角形タイルを横8個・縦4個並べた赤い64×32の領域が出ます。

y=96では、x=16・24・32に青い正方形・左半分の棒・三角形、x=80に赤い左半分の棒、x=144に緑の三角形が表示されます。これらは背景タイルです。別パレットを使うスプライトはy=144に並び、x=16が赤い8×8の正方形、x=80が青い左半分4×8の棒、x=144が緑の8行の三角形です。最終スクロールは(0,0)です。

画像ではスプライトの色も含め、16,384ピクセルの形と色を照合します。別の31ケースで、レジスターの置き換え、ステータス・ラッチの副作用、スクロール保存値、0・255バイト転送、増分1・32、マップ端、矩形の寸法0、テーブル選択、属性1バイト全体、背景・スプライト両方のパレットを検証しています。Y=255はレジスターへ書く値としての確認で、正しいスクロール画像の検証ではありません。検証環境はKUROSAKIのNROMで、実機は未検証です。

__palette_sp_load: NROM
NROM

使用例の完全なプログラム

ビルドコマンド

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\ppu_intrinsics.c -I .\kitaqfc\lib -I .\kitaq-docs\samples --nes-chr=.\kitaq-docs\samples\api-examples\fc\vram_shapes.chr -o .\out\ppu_intrinsics.nes --no-cache --no-disasm
実装とソース

kitaqfc/lib/intrinsics.h:140

宣言の注記(原文)
Stream exactly 16 source bytes to 3F10; hardware palette aliases also affect background entries.

kitaqfc/kitaqfc/CodeGenerator.cs:4828

csharp
case "__palette_sp_load": return EmitKitaqfcHelperCall(funcName, args, ctx, new[] {2}, funcName);
                // Clear queued length and readiness; the overflow indicator has a separate reset intrinsic.

void EmitHelperPalette(string name, int ppuAddress)
        {
            EmitHelperStart(name);
            EmitAsm("LDA", Mem(0x2002));
            EmitAsm("LDA", Imm((ppuAddress >> 8) & 0xFF));
            EmitAsm("STA", Mem(0x2006));
            EmitAsm("LDA", Imm(ppuAddress & 0xFF));
            EmitAsm("STA", Mem(0x2006));
            EmitAsm("LDY", Imm(0));
            string loop = NewGeneratedLabel("pal_loop");
            _assembly.Add(Expr.Make(Tag.Label, loop));
            EmitAsm("LDA", IndY(CallArgBase));
            EmitAsm("STA", Mem(0x2007));
            EmitAsm("INY");
            EmitAsm("CPY", Imm(16));
            EmitAsm("BNE", Rel(loop));
            EmitAsm("RTS");
        }

EmitHelperPalette("__palette_sp_load", 0x3F10);
__ppu_addr

ppu_addrを一度評価し、PPUSTATUSを読んで共有書き込みラッチをリセットしてから、上位・下位アドレスをPPUADDRへ書きます。後続のPPUDATAアクセス先を設定するだけで、データそのものは書きません。PPUのアドレスマスクとミラーリングが適用されます。

書式

c
void __ppu_addr(u16 ppu_addr);

引数

ppu_addr

CPUポインターではなく、PPUのバイトアドレス(0x0000~0x3FFF)です。転送先は書き込み可能でなければなりません。パターンの変更にはCHR ROMではなくCHR RAMが必要です。PPUのマッピングとミラーリングが適用されます。連続したコピーや埋め込みではPPUCTRLの増分を1にしてください。安全にPPUへ書ける時期に実行し、その後で必要なスクロール状態を設定してください。実行処理はPPUADDRとPPUDATAを使い、スクロール設定を復元しません。

戻り値

ありません(void)。

使用条件と制限

PPU直接アクセスは、描画停止中か、処理全体が安全なVBlank時間内に収まる条件で行います。これらの関数は待機・キュー登録・NMI抑止・スクロール復元を行いません。呼び出し中に割り込みで共有ラッチやアドレスを変更しないでください。PPUCTRLのビット2により、書き込み先は1バイトごとに1または32進みます。ミラーリングとアドレスの周回はPPUの動作に従い、CHR-ROMへは書き込めません。アクセス後、描画を始める前に意図したスクロール状態を設定してください。

NESdev: PPU registers

NESdev: PPU scrolling

使用例

c
address_port=0x21;
__ppu_addr(0x2192); // Reset latch and set the cell at tile (18,12).

呼び出し部分のコード例です。完全なプログラムに組み込む際は、記載した初期化とバッファの条件を満たしてください。

期待する結果

このプログラムではレジスター設定、タイル・属性の直接書き込み、スクロール、背景・スプライトそれぞれのパレット転送を学びます。(0,16)の青い帯は256×16ピクセルです。上段は8×8の塗りつぶし、左半分4×8の棒、8行の三角形を繰り返し、下段は左半分の棒だけを並べます。(16,48)に緑の塗りつぶし64×32、(144,48)に三角形タイルを横8個・縦4個並べた赤い64×32の領域が出ます。

y=96では、x=16・24・32に青い正方形・左半分の棒・三角形、x=80に赤い左半分の棒、x=144に緑の三角形が表示されます。これらは背景タイルです。別パレットを使うスプライトはy=144に並び、x=16が赤い8×8の正方形、x=80が青い左半分4×8の棒、x=144が緑の8行の三角形です。最終スクロールは(0,0)です。

画像ではスプライトの色も含め、16,384ピクセルの形と色を照合します。別の31ケースで、レジスターの置き換え、ステータス・ラッチの副作用、スクロール保存値、0・255バイト転送、増分1・32、マップ端、矩形の寸法0、テーブル選択、属性1バイト全体、背景・スプライト両方のパレットを検証しています。Y=255はレジスターへ書く値としての確認で、正しいスクロール画像の検証ではありません。検証環境はKUROSAKIのNROMで、実機は未検証です。

__ppu_addr: NROM
NROM

使用例の完全なプログラム

ビルドコマンド

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\ppu_intrinsics.c -I .\kitaqfc\lib -I .\kitaq-docs\samples --nes-chr=.\kitaq-docs\samples\api-examples\fc\vram_shapes.chr -o .\out\ppu_intrinsics.nes --no-cache --no-disasm
実装とソース

kitaqfc/lib/intrinsics.h:106

宣言の注記(原文)
Set the PPU address through the compiler helper; address writes affect the shared scroll/address latch.

kitaqfc/kitaqfc/CodeGenerator.cs:4698

csharp
case "__ppu_addr":
                    if (!Need(1)) return true;
                    PpuAddr(args[0]); return true;

void PpuAddr(Expr e)
            {
                int[] t = AcquireTemps(2);
                try
                {
                    EmitLoadValue(e, ctx, 2);
                    EmitAsm("STA", Mem(t[0]));
                    EmitAsm("STX", Mem(t[1]));
                    EmitAsm("LDA", Mem(0x2002));
                    EmitAsm("LDA", Mem(t[1]));
                    EmitAsm("STA", Mem(0x2006));
                    EmitAsm("LDA", Mem(t[0]));
                    EmitAsm("STA", Mem(0x2006));
                }
                finally { ReleaseTemps(t); }
            }
__ppu_ctrl_get

PPUCTRLのソフトウェア側の保持値を返します。NMIの有効設定、パターンテーブル、アドレス増分などを保存して一時設定後に戻す用途です。

書式

c
u8 __ppu_ctrl_get(void);

戻り値

最後にライブラリまたは組み込み関数が設定した制御値をu8で返します。PPUSTATUSを読みません。

使用条件と制限

PPUCTRLとPPUMASKは書き込み専用です。この関数はハードウェアを読み戻すものではありません。$2000/$2001への直接書き込みや独自のアセンブリ操作は保持値に反映されません。設定は対応する__ppu_*_setなどを使って揃えてください。読み出しでラッチや割り込み状態は変わりません。

使用例

c
__ppu_ctrl_set(4);
    if(__ppu_ctrl_get()!=4)failures=1; // Inspect the software shadow, not PPUSTATUS.
    __ppu_ctrl_set(0);

呼び出し部分のコード例です。完全なプログラムに組み込む際は、記載した初期化とバッファの条件を満たしてください。

期待する結果

4つのライブラリ関数で描画停止、両パレットの転送、ネームテーブル全体の初期化、描画開始を行います。描く前にパレットと初期化した面を読み戻して確認します。y=16に青い正方形・棒・三角形のコピー、y=96に緑の埋め込み、y=208に赤い正方形・棒・三角形を表示します。大きい領域は各255タイルで、最後の1マスは黒です。FAILED CHECKSは0です。

__ppu_ctrl_get: ALTERNATIVES
ALTERNATIVES

使用例の完全なプログラム

ビルドコマンド

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\ppu_implemented.c -I .\kitaqfc\lib -I .\kitaq-docs\samples -o .\out\ppu_implemented.nes --mapper=nrom --nes-chr=.\kitaq-docs\samples\api-examples\fc\vram_shapes.chr --no-cache --no-disasm
実装とソース

kitaqfc/lib/intrinsics.h:103

宣言の注記(原文)
Read the software PPUCTRL/PPUMASK shadows; use the setters for coherent state.
Raw writes to $2000/$2001 bypass these shadows. No PPU hardware read occurs.

kitaqfc/kitaqfc/CodeGenerator.cs:4691

csharp
case "__ppu_ctrl_get":
                    if (!Need(0)) return true;
                    EmitAsm("LDA", Mem(_runtimePpuCtrlShadowAddress)); return true;
__ppu_ctrl_set

PPUCTRLとソフトウェア側の保存値を指定した1バイトに置き換えます。NMIの有効化、PPUアドレスの増分、パターンテーブル、スプライトサイズ、基準ネームテーブルを制御します。PPUMASKは変更せず、指定しなかったビットを保存する処理もありません。

書式

c
void __ppu_ctrl_set(u8 value);

引数

value

指定どおりに書く符号なし1バイト0..255です。真偽値やビット番号ではなく、レジスターまたはデータの1バイト全体を渡します。

戻り値

ありません(void)。

NESdev: PPU registers

NESdev: PPU scrolling

使用例

c
__ppu_ctrl_set(0x80); // NMI enabled, increment 1, nametable/pattern tables 0.

呼び出し部分のコード例です。完全なプログラムに組み込む際は、記載した初期化とバッファの条件を満たしてください。

期待する結果

このプログラムではレジスター設定、タイル・属性の直接書き込み、スクロール、背景・スプライトそれぞれのパレット転送を学びます。(0,16)の青い帯は256×16ピクセルです。上段は8×8の塗りつぶし、左半分4×8の棒、8行の三角形を繰り返し、下段は左半分の棒だけを並べます。(16,48)に緑の塗りつぶし64×32、(144,48)に三角形タイルを横8個・縦4個並べた赤い64×32の領域が出ます。

y=96では、x=16・24・32に青い正方形・左半分の棒・三角形、x=80に赤い左半分の棒、x=144に緑の三角形が表示されます。これらは背景タイルです。別パレットを使うスプライトはy=144に並び、x=16が赤い8×8の正方形、x=80が青い左半分4×8の棒、x=144が緑の8行の三角形です。最終スクロールは(0,0)です。

画像ではスプライトの色も含め、16,384ピクセルの形と色を照合します。別の31ケースで、レジスターの置き換え、ステータス・ラッチの副作用、スクロール保存値、0・255バイト転送、増分1・32、マップ端、矩形の寸法0、テーブル選択、属性1バイト全体、背景・スプライト両方のパレットを検証しています。Y=255はレジスターへ書く値としての確認で、正しいスクロール画像の検証ではありません。検証環境はKUROSAKIのNROMで、実機は未検証です。

__ppu_ctrl_set: NROM
NROM

使用例の完全なプログラム

ビルドコマンド

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\ppu_intrinsics.c -I .\kitaqfc\lib -I .\kitaq-docs\samples --nes-chr=.\kitaq-docs\samples\api-examples\fc\vram_shapes.chr -o .\out\ppu_intrinsics.nes --no-cache --no-disasm
実装とソース

kitaqfc/lib/intrinsics.h:100

宣言の注記(原文)
Replace PPUCTRL, including NMI enable, pattern-table choices and address increment mode.

kitaqfc/kitaqfc/CodeGenerator.cs:4687

csharp
case "__ppu_ctrl_set":
                    if (!Need(1)) return true;
                    Store1(args[0], _runtimePpuCtrlShadowAddress); EmitAsm("STA", Mem(0x2000)); return true;
                // Read software state: the corresponding hardware registers are write-only.
__ppu_data

現在のPPUアドレスへ、PPUDATAを通じてvalueを1回書きます。その後PPUアドレスはPPUCTRLの設定に従い1または32進みます。この呼び出しはアドレス設定、ラッチのリセット、転送キューへの登録を行いません。

書式

c
void __ppu_data(u8 value);

引数

value

指定どおりに書く符号なし1バイト0..255です。真偽値やビット番号ではなく、レジスターまたはデータの1バイト全体を渡します。

戻り値

ありません(void)。

使用条件と制限

PPU直接アクセスは、描画停止中か、処理全体が安全なVBlank時間内に収まる条件で行います。これらの関数は待機・キュー登録・NMI抑止・スクロール復元を行いません。呼び出し中に割り込みで共有ラッチやアドレスを変更しないでください。PPUCTRLのビット2により、書き込み先は1バイトごとに1または32進みます。ミラーリングとアドレスの周回はPPUの動作に従い、CHR-ROMへは書き込めません。アクセス後、描画を始める前に意図したスクロール状態を設定してください。

NESdev: PPU registers

NESdev: PPU scrolling

使用例

c
__ppu_data(3); // Green triangle at (144,96); destination advances by one.

呼び出し部分のコード例です。完全なプログラムに組み込む際は、記載した初期化とバッファの条件を満たしてください。

期待する結果

このプログラムではレジスター設定、タイル・属性の直接書き込み、スクロール、背景・スプライトそれぞれのパレット転送を学びます。(0,16)の青い帯は256×16ピクセルです。上段は8×8の塗りつぶし、左半分4×8の棒、8行の三角形を繰り返し、下段は左半分の棒だけを並べます。(16,48)に緑の塗りつぶし64×32、(144,48)に三角形タイルを横8個・縦4個並べた赤い64×32の領域が出ます。

y=96では、x=16・24・32に青い正方形・左半分の棒・三角形、x=80に赤い左半分の棒、x=144に緑の三角形が表示されます。これらは背景タイルです。別パレットを使うスプライトはy=144に並び、x=16が赤い8×8の正方形、x=80が青い左半分4×8の棒、x=144が緑の8行の三角形です。最終スクロールは(0,0)です。

画像ではスプライトの色も含め、16,384ピクセルの形と色を照合します。別の31ケースで、レジスターの置き換え、ステータス・ラッチの副作用、スクロール保存値、0・255バイト転送、増分1・32、マップ端、矩形の寸法0、テーブル選択、属性1バイト全体、背景・スプライト両方のパレットを検証しています。Y=255はレジスターへ書く値としての確認で、正しいスクロール画像の検証ではありません。検証環境はKUROSAKIのNROMで、実機は未検証です。

__ppu_data: NROM
NROM

使用例の完全なプログラム

ビルドコマンド

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\ppu_intrinsics.c -I .\kitaqfc\lib -I .\kitaq-docs\samples --nes-chr=.\kitaq-docs\samples\api-examples\fc\vram_shapes.chr -o .\out\ppu_intrinsics.nes --no-cache --no-disasm
実装とソース

kitaqfc/lib/intrinsics.h:108

宣言の注記(原文)
Write one PPUDATA byte using the current PPU address and increment mode.

kitaqfc/kitaqfc/CodeGenerator.cs:4701

csharp
case "__ppu_data":
                    if (!Need(1)) return true;
                    Load1(args[0]); EmitAsm("STA", Mem(0x2007)); return true;
                // Reading PPUSTATUS also has hardware side effects; this is not a side-effect-free status cache.
__ppu_mask_get

PPUMASKのソフトウェア側の保持値を返します。背景・スプライトの描画、左端表示、グレースケール、色強調の設定を保存して戻す用途です。

書式

c
u8 __ppu_mask_get(void);

戻り値

最後にライブラリまたは組み込み関数が設定した制御値をu8で返します。PPUSTATUSを読みません。

使用条件と制限

PPUCTRLとPPUMASKは書き込み専用です。この関数はハードウェアを読み戻すものではありません。$2000/$2001への直接書き込みや独自のアセンブリ操作は保持値に反映されません。設定は対応する__ppu_*_setなどを使って揃えてください。読み出しでラッチや割り込み状態は変わりません。

使用例

c
__ppu_mask_set(0xE7);
    if(__ppu_mask_get()!=0xE7)failures=1;
    __ppu_mask_set(0);

呼び出し部分のコード例です。完全なプログラムに組み込む際は、記載した初期化とバッファの条件を満たしてください。

期待する結果

4つのライブラリ関数で描画停止、両パレットの転送、ネームテーブル全体の初期化、描画開始を行います。描く前にパレットと初期化した面を読み戻して確認します。y=16に青い正方形・棒・三角形のコピー、y=96に緑の埋め込み、y=208に赤い正方形・棒・三角形を表示します。大きい領域は各255タイルで、最後の1マスは黒です。FAILED CHECKSは0です。

__ppu_mask_get: ALTERNATIVES
ALTERNATIVES

使用例の完全なプログラム

ビルドコマンド

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\ppu_implemented.c -I .\kitaqfc\lib -I .\kitaq-docs\samples -o .\out\ppu_implemented.nes --mapper=nrom --nes-chr=.\kitaq-docs\samples\api-examples\fc\vram_shapes.chr --no-cache --no-disasm
実装とソース

kitaqfc/lib/intrinsics.h:104

kitaqfc/kitaqfc/CodeGenerator.cs:4694

csharp
case "__ppu_mask_get":
                    if (!Need(0)) return true;
                    EmitAsm("LDA", Mem(_runtimePpuMaskShadowAddress)); return true;
                // Direct PPU address/data access does not wait for a safe rendering interval here.
__ppu_mask_set

PPUMASKの全8ビットとソフトウェア側の保存値をvalueに置き換えます。描画、左端の表示、グレースケール、色強調を明示的に選びます。直前のマスクとのビット合成やPPUCTRLの変更は行いません。

書式

c
void __ppu_mask_set(u8 value);

引数

value

指定どおりに書く符号なし1バイト0..255です。真偽値やビット番号ではなく、レジスターまたはデータの1バイト全体を渡します。

戻り値

ありません(void)。

NESdev: PPU registers

NESdev: PPU scrolling

使用例

c
__ppu_mask_set(0x0A); // Background enabled, including its leftmost eight pixels.

呼び出し部分のコード例です。完全なプログラムに組み込む際は、記載した初期化とバッファの条件を満たしてください。

期待する結果

このプログラムではレジスター設定、タイル・属性の直接書き込み、スクロール、背景・スプライトそれぞれのパレット転送を学びます。(0,16)の青い帯は256×16ピクセルです。上段は8×8の塗りつぶし、左半分4×8の棒、8行の三角形を繰り返し、下段は左半分の棒だけを並べます。(16,48)に緑の塗りつぶし64×32、(144,48)に三角形タイルを横8個・縦4個並べた赤い64×32の領域が出ます。

y=96では、x=16・24・32に青い正方形・左半分の棒・三角形、x=80に赤い左半分の棒、x=144に緑の三角形が表示されます。これらは背景タイルです。別パレットを使うスプライトはy=144に並び、x=16が赤い8×8の正方形、x=80が青い左半分4×8の棒、x=144が緑の8行の三角形です。最終スクロールは(0,0)です。

画像ではスプライトの色も含め、16,384ピクセルの形と色を照合します。別の31ケースで、レジスターの置き換え、ステータス・ラッチの副作用、スクロール保存値、0・255バイト転送、増分1・32、マップ端、矩形の寸法0、テーブル選択、属性1バイト全体、背景・スプライト両方のパレットを検証しています。Y=255はレジスターへ書く値としての確認で、正しいスクロール画像の検証ではありません。検証環境はKUROSAKIのNROMで、実機は未検証です。

__ppu_mask_set: NROM
NROM

使用例の完全なプログラム

ビルドコマンド

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\ppu_intrinsics.c -I .\kitaqfc\lib -I .\kitaq-docs\samples --nes-chr=.\kitaq-docs\samples\api-examples\fc\vram_shapes.chr -o .\out\ppu_intrinsics.nes --no-cache --no-disasm
実装とソース

kitaqfc/lib/intrinsics.h:98

宣言の注記(原文)
Replace PPUMASK with the supplied byte; the caller chooses rendering, clipping and emphasis bits.

kitaqfc/kitaqfc/CodeGenerator.cs:4683

csharp
case "__ppu_mask_set":
                    if (!Need(1)) return true;
                    Store1(args[0], _runtimePpuMaskShadowAddress); EmitAsm("STA", Mem(0x2001)); return true;
                // Update the PPUCTRL shadow before writing the hardware register.
__ppu_off

PPUMASKとソフトウェア側の保存値へ0を書き、背景とスプライトの描画を停止します。グレースケールや色強調も含め、マスクの全ビットを消します。PPUCTRLとNMIの有効状態は変わらないため、共有転送データの準備では必要に応じて別途NMIを制御してください。

書式

c
void __ppu_off(void);

戻り値

ありません(void)。

NESdev: PPU registers

NESdev: PPU scrolling

使用例

c
__ppu_off(); // PPUMASK and its software shadow become zero; NMI is unchanged.
__ppu_ctrl_set(0); // Disable NMI separately while preparing the whole scene.

呼び出し部分のコード例です。完全なプログラムに組み込む際は、記載した初期化とバッファの条件を満たしてください。

期待する結果

このプログラムではレジスター設定、タイル・属性の直接書き込み、スクロール、背景・スプライトそれぞれのパレット転送を学びます。(0,16)の青い帯は256×16ピクセルです。上段は8×8の塗りつぶし、左半分4×8の棒、8行の三角形を繰り返し、下段は左半分の棒だけを並べます。(16,48)に緑の塗りつぶし64×32、(144,48)に三角形タイルを横8個・縦4個並べた赤い64×32の領域が出ます。

y=96では、x=16・24・32に青い正方形・左半分の棒・三角形、x=80に赤い左半分の棒、x=144に緑の三角形が表示されます。これらは背景タイルです。別パレットを使うスプライトはy=144に並び、x=16が赤い8×8の正方形、x=80が青い左半分4×8の棒、x=144が緑の8行の三角形です。最終スクロールは(0,0)です。

画像ではスプライトの色も含め、16,384ピクセルの形と色を照合します。別の31ケースで、レジスターの置き換え、ステータス・ラッチの副作用、スクロール保存値、0・255バイト転送、増分1・32、マップ端、矩形の寸法0、テーブル選択、属性1バイト全体、背景・スプライト両方のパレットを検証しています。Y=255はレジスターへ書く値としての確認で、正しいスクロール画像の検証ではありません。検証環境はKUROSAKIのNROMで、実機は未検証です。

__ppu_off: NROM
NROM

使用例の完全なプログラム

ビルドコマンド

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\ppu_intrinsics.c -I .\kitaqfc\lib -I .\kitaq-docs\samples --nes-chr=.\kitaq-docs\samples\api-examples\fc\vram_shapes.chr -o .\out\ppu_intrinsics.nes --no-cache --no-disasm
実装とソース

kitaqfc/lib/intrinsics.h:96

宣言の注記(原文)
Invoke the compiler rendering-disable helper before bulk direct PPU updates.

kitaqfc/kitaqfc/CodeGenerator.cs:4679

csharp
case "__ppu_off":
                    if (!Need(0)) return true;
                    EmitAsm("LDA", Imm(0)); EmitAsm("STA", Mem(_runtimePpuMaskShadowAddress)); EmitAsm("STA", Mem(0x2001)); return true;
                // Keep the supplied mask available for later runtime code that restores PPU state.
__ppu_on

PPUMASKとソフトウェア側の保存値を0x1Eに置き換え、左端8ピクセルを含めて背景とスプライトの描画を有効にします。グレースケール・色強調の設定も上書きします。PPUCTRLの変更やNMIの有効化は行いません。

書式

c
void __ppu_on(void);

戻り値

ありません(void)。

NESdev: PPU registers

NESdev: PPU scrolling

使用例

c
__ppu_on(); // Replace mask with 0x1E: enable sprites and background at both edges.

呼び出し部分のコード例です。完全なプログラムに組み込む際は、記載した初期化とバッファの条件を満たしてください。

期待する結果

このプログラムではレジスター設定、タイル・属性の直接書き込み、スクロール、背景・スプライトそれぞれのパレット転送を学びます。(0,16)の青い帯は256×16ピクセルです。上段は8×8の塗りつぶし、左半分4×8の棒、8行の三角形を繰り返し、下段は左半分の棒だけを並べます。(16,48)に緑の塗りつぶし64×32、(144,48)に三角形タイルを横8個・縦4個並べた赤い64×32の領域が出ます。

y=96では、x=16・24・32に青い正方形・左半分の棒・三角形、x=80に赤い左半分の棒、x=144に緑の三角形が表示されます。これらは背景タイルです。別パレットを使うスプライトはy=144に並び、x=16が赤い8×8の正方形、x=80が青い左半分4×8の棒、x=144が緑の8行の三角形です。最終スクロールは(0,0)です。

画像ではスプライトの色も含め、16,384ピクセルの形と色を照合します。別の31ケースで、レジスターの置き換え、ステータス・ラッチの副作用、スクロール保存値、0・255バイト転送、増分1・32、マップ端、矩形の寸法0、テーブル選択、属性1バイト全体、背景・スプライト両方のパレットを検証しています。Y=255はレジスターへ書く値としての確認で、正しいスクロール画像の検証ではありません。検証環境はKUROSAKIのNROMで、実機は未検証です。

__ppu_on: NROM
NROM

使用例の完全なプログラム

ビルドコマンド

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\ppu_intrinsics.c -I .\kitaqfc\lib -I .\kitaq-docs\samples --nes-chr=.\kitaq-docs\samples\api-examples\fc\vram_shapes.chr -o .\out\ppu_intrinsics.nes --no-cache --no-disasm
実装とソース

kitaqfc/lib/intrinsics.h:94

宣言の注記(原文)
PRG bank
Resolve a symbol or function placement to its byte bank number at compilation time.
u8 __bankof(u16 symbol_or_function);
Select the mapper-specific PRG bank; the common bank and legal bank range depend on the target board.
void __bankswitch(u8 bank);
Use the same mapper PRG-switch helper as __bankswitch; retain executable code in a valid mapping.
void __prg_bank_set(u8 bank);
Call a declared zero-argument function; placement selects its bank and its declared type determines the result.
u8 __farcall(u8 bank, u16 func);
Switch to the source bank, copy len bytes forward and restore the previous bank.
Keep destination RAM and helper code accessible throughout; overlapping copies are not memmove.
void __far_memcpy(u8* dst, u8 bank, u16 src, u16 len);
Temporarily select bank, read one byte at addr and restore the prior bank mapping.
u8 __farpeek8(u8 bank, u16 addr);
Temporarily select bank and read a little-endian word, then restore the previous mapping.
u16 __farpeek16(u8 bank, u16 addr);

 PPU / VRAM
Invoke the compiler rendering-enable helper; this header contains declarations, not C fallback implementations.

kitaqfc/kitaqfc/CodeGenerator.cs:4676

csharp
case "__ppu_on":
                    if (!Need(0)) return true;
                    EmitAsm("LDA", Imm(0x1E)); EmitAsm("STA", Mem(_runtimePpuMaskShadowAddress)); EmitAsm("STA", Mem(0x2001)); return true;
__ppu_read_status

PPUSTATUSを1回読み、読み取りによってVBlankフラグが消える前の値を返します。スクロールとアドレス共用の書き込みラッチもリセットします。副作用のあるハードウェアステータスの読み取りであり、保存済みフレームカウンターやNMI処理の完了確認ではありません。

書式

c
u8 __ppu_read_status(void);

戻り値

生のステータス1バイトです。ビット7がVBlank、ビット6がスプライト0ヒット、ビット5がスプライトオーバーフローです。ビット0~4にはPPUのI/Oラッチの下位ビットが残るため、0とは限りません。VBlankは(status & 0x80) != 0で判定してください。1バイト全体が0でないことはVBlankを意味しません。下位ビットをアプリケーションの状態やフレーム数として使わないでください。

使用条件と制限

PPUSTATUSの読み取りはNMIのタイミングと、その後のPPUSCROLL・PPUADDR両方への書き込みに影響します。別の処理が2バイトの途中まで書いている場所へ挟まないでください。これらの呼び出しはVBlankを待たず、未処理の転送キューも完了させません。

NESdev: PPU registers

NESdev: PPU scrolling

使用例

c
address_port=0x21;
status_snapshot=__ppu_read_status(); // Also resets the shared write latch.
address_port=0x21;address_port=0x84;data_port=3; // Blue triangle at (32,96).

呼び出し部分のコード例です。完全なプログラムに組み込む際は、記載した初期化とバッファの条件を満たしてください。

期待する結果

このプログラムではレジスター設定、タイル・属性の直接書き込み、スクロール、背景・スプライトそれぞれのパレット転送を学びます。(0,16)の青い帯は256×16ピクセルです。上段は8×8の塗りつぶし、左半分4×8の棒、8行の三角形を繰り返し、下段は左半分の棒だけを並べます。(16,48)に緑の塗りつぶし64×32、(144,48)に三角形タイルを横8個・縦4個並べた赤い64×32の領域が出ます。

y=96では、x=16・24・32に青い正方形・左半分の棒・三角形、x=80に赤い左半分の棒、x=144に緑の三角形が表示されます。これらは背景タイルです。別パレットを使うスプライトはy=144に並び、x=16が赤い8×8の正方形、x=80が青い左半分4×8の棒、x=144が緑の8行の三角形です。最終スクロールは(0,0)です。

画像ではスプライトの色も含め、16,384ピクセルの形と色を照合します。別の31ケースで、レジスターの置き換え、ステータス・ラッチの副作用、スクロール保存値、0・255バイト転送、増分1・32、マップ端、矩形の寸法0、テーブル選択、属性1バイト全体、背景・スプライト両方のパレットを検証しています。Y=255はレジスターへ書く値としての確認で、正しいスクロール画像の検証ではありません。検証環境はKUROSAKIのNROMで、実機は未検証です。

__ppu_read_status: NROM
NROM

使用例の完全なプログラム

ビルドコマンド

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\ppu_intrinsics.c -I .\kitaqfc\lib -I .\kitaq-docs\samples --nes-chr=.\kitaq-docs\samples\api-examples\fc\vram_shapes.chr -o .\out\ppu_intrinsics.nes --no-cache --no-disasm
実装とソース

kitaqfc/lib/intrinsics.h:110

宣言の注記(原文)
Read PPUSTATUS; the hardware clears VBlank status and resets the shared write latch.

kitaqfc/kitaqfc/CodeGenerator.cs:4705

csharp
case "__ppu_read_status":
                    if (!Need(0)) return true;
                    EmitAsm("LDA", Mem(0x2002)); return true;
__prg_bank_set

__bankswitchと同じ、マッパーに対応したPRGバンク切り替えを行います。バンク番号の検査も同じで、元のマッピングへ自動では戻しません。

書式

c
void __prg_bank_set(u8 bank);

引数

bank

符号なし8ビットのROMバンク番号です。呼び出しでは関数の配置と一致させ、読み出しでは転送元のマッピングを指定します。対象カートリッジで有効な範囲を使用してください。

戻り値

ありません(void)。

使用条件と制限

切り替えを維持する操作では、実行中のコードや戻り先を見えなくしてはいけません。サンプルのように、この呼び出し、使用するbank_switch本体、その後に続くコードを共通・固定バンク0に置いてください。

ライブラリの記録は0で始まり、カートリッジの起動時のマッピングを調べるものではありません。bank_switchで初期化してください。組み込み関数による直接切り替えやマッパーへの直接書き込みではこの記録を更新しないため、bank_get_currentで実際のハードウェア状態を検証することはできません。

FCの切り替え番号はコンパイラの論理バンク番号です。0は共通ROMを示し、切り替え先ではありません。補助処理は0をバンク1として扱います。SUROM512の有効範囲は1〜30で、切り替え組み込み関数へ直接渡す範囲外の定数はビルドエラーです。範囲外の実行時値では1を選び、マッパー異常状態を設定します。共通ROMは、バンク0への切り替えを要求せず直接読んでください。

使用例

c
__prg_bank_set(1);bank_expect(home1[0]==0xA1,21);
__prg_bank_set(2); // Same mapper helper as __bankswitch.

呼び出し部分のコード例です。完全なプログラムに組み込む際は、記載した初期化とバッファの条件を満たしてください。

期待する結果

最初のbank_switch(2)でバンク2を記録して選びます。その後の直接切り替えではバンク1を選び、home1からA1を読みますが、bank_get_currentの記録は2です。戻すとhome2から再びB2を読めます。

完成プログラムでは、バンク番号と近アドレスによるデータの指定、バンクをまたぐコールバック、ライブラリの記録と実際のマッピングの区別を学べます。数値はすべて16進数です。BANK INとBANK OUTは02、BYTE HEXはD3、WORD HEXは5C7A、FAILED CHECKSとFIRST FAILUREはどちらも00が期待値です。CGBとFCは青字、DMGは黒字で表示します。

CALL COUNTは02です。far_callマクロと__farcallがそれぞれ1回コールバックを実行します。

__prg_bank_set: MMC3
MMC3

使用例の完全なプログラム

ビルドコマンド

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\banked_data_calls.c -I .\kitaqfc\lib -I .\kitaq-docs\samples -o .\out\banked_data_calls.nes --mapper=mmc3 --nes-chr=.\kitaq-docs\samples\font.chr --no-cache --no-disasm
実装とソース

kitaqfc/lib/intrinsics.h:81

宣言の注記(原文)
Use the same mapper PRG-switch helper as __bankswitch; retain executable code in a valid mapping.

kitaqfc/kitaqfc/CodeGenerator.cs:4746

csharp
case "__prg_bank_set":
                case "__bankswitch":
                    if (!Need(1)) return true;
                    if (Program.NesMapperProfile.IsSurom512 && TryEvaluateConstant(args[0], out int constantBank) && (constantBank < 1 || constantBank > 30))
                    {
                        Program.Error("error KQFC2603 KQFC-SUROM-BANK-RANGE: {0} constant bank {1} is outside 1..30; common bank 0 is not a switch target.", funcName, constantBank);
                        return true;
                    }
                    _mapperWritingFunctions.Add(_currentFunctionName);
                    Load1(args[0]); EmitAsm("JSR", Abs("__kq_prg_set_bank_a")); return true;
__rng8

共有するFCの16ビットGalois LFSRを1回進め、更新後の下位バイトと上位バイトの排他的論理和を返します。LFSRはシフトと排他的論理和で状態を更新する方式で、固定したシードからは同じバイト列が得られます。

書式

c
u8 __rng8(void);

戻り値

共有生成器を1回進めて得たu8の値(0〜255)です。これは型と値の範囲を示しており、各バイト値が同じ確率で出るという意味ではありません。

使用条件と制限

FCの更新では16ビット状態を1ビット右にシフトします。更新前の最下位ビットが1なら、シフト後の状態と0xB400の排他的論理和を取ります。戻り値は下位バイトそのものではなく、下位バイトと上位バイトの排他的論理和です。

状態の2バイトがともに0なら、次の取得時に0xA55Aへ置き換えてからシフトします。このため__rng_seed(0)の直後の__rng8()は255を返します。シード設定は0を保存し、置き換えは取得側で行います。

各呼び出しは持続する状態を共有します。再現には、同じ対象機種・シード・呼び出し順が必要で、ほかの補助関数による取得もその順番に含まれます。シード設定と取得は一つの実行箇所にまとめてください。今回の確認は、割り込み処理と並行して安全に使えることを保証するものではありません。

GBとFCは異なるアルゴリズムを使い、同じシードでも同じ列になることを保証しません。ゲーム処理向けの決定的な生成器であり、暗号用の乱数源ではありません。検証対象は実装の動作で、統計的な品質や周期ではありません。

使用例

c
__rng_seed(0x1234);
first=__rng8();second=__rng8();third=__rng8(); // FC sequence: 19, 137, 240.

呼び出し部分のコード例です。完全なプログラムに組み込む際は、記載した初期化とバッファの条件を満たしてください。

期待する結果

FCから連続する3バイトを取得するサンプルです。シード0x1234でFIRSTは00019、SECONDは00137、THIRDは00240になります。表示は呼び出し順の結果であり、頻度や乱数品質の点数ではありません。

完全なNROMプログラムでは、黒地に青文字で10進数の結果を表示します。FAILEDは00000になる必要があります。描画を無効にした状態で文字を転送してから、結果画面を有効にします。

各条件で32個の出力と、その後の8回のバイト取得を独立したホスト側モデルと照合しました。GBは93条件、FCは18条件で、定数・実行時のシード指定や、GBの範囲・確率・重みの境界条件を含みます。サンプル画面はエミュレータで240フレーム後に確認しました。実機や統計的な品質は未検証です。

__rng8: NROM
NROM

使用例の完全なプログラム

ビルドコマンド

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\rng_core.c -I .\kitaqfc\lib -I .\kitaq-docs\samples -o .\out\rng_core.nes --mapper=nrom --nes-chr=.\kitaq-docs\samples\font.chr --no-cache --no-disasm
実装とソース

kitaqfc/lib/intrinsics.h:39

宣言の注記(原文)
Advance the shared 16-bit LFSR and return the XOR of its two bytes; zero state is reseeded to A55A.

kitaqfc/kitaqfc/CodeGenerator.cs:4663

csharp
case "__rng8":
                    if (!Need(0)) return true;
                    EmitAsm("JSR", Abs("__rng8"));
                    return true;
                // Far-memory helpers pack the address, bank and length arguments according to their explicit byte/word signatures.

EmitHelperStart("__rng8");
            // 16-bit Galois LFSR: if low bit set, shift right then xor $B400.
            EmitAsm("LDA", Mem(_runtimeRngLoAddress));
            EmitAsm("ORA", Mem(_runtimeRngHiAddress));
            // Replace the all-zero LFSR state with a deterministic nonzero seed before advancing.
            string seeded = NewGeneratedLabel("rng_seeded");
            EmitAsm("BNE", Rel(seeded));
            EmitAsm("LDA", Imm(0x5A));
            EmitAsm("STA", Mem(_runtimeRngLoAddress));
            EmitAsm("LDA", Imm(0xA5));
            EmitAsm("STA", Mem(_runtimeRngHiAddress));
            _assembly.Add(Expr.Make(Tag.Label, seeded));
            EmitAsm("LDA", Mem(_runtimeRngLoAddress));
            EmitAsm("AND", Imm(1));
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("LSR", Mem(_runtimeRngHiAddress));
            EmitAsm("ROR", Mem(_runtimeRngLoAddress));
            string rngNoXor = NewGeneratedLabel("rng_no_xor");
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("BEQ", Rel(rngNoXor));
            EmitAsm("LDA", Mem(_runtimeRngHiAddress));
            EmitAsm("EOR", Imm(0xB4));
            EmitAsm("STA", Mem(_runtimeRngHiAddress));
            _assembly.Add(Expr.Make(Tag.Label, rngNoXor));
            EmitAsm("LDA", Mem(_runtimeRngLoAddress));
            EmitAsm("EOR", Mem(_runtimeRngHiAddress));
            EmitAsm("RTS");
__rng_seed

FCの乱数生成器の状態を構成する2バイトを設定します。同じシードを使うと、テストやリプレイを同じ疑似乱数列の開始点から実行できます。この呼び出しでは生成器を進めません。

書式

c
void __rng_seed(u16 seed);

引数

seed

0〜65535の符号なし16ビットのシードです。指定した2バイトをそのまま保存します。再現したいゲーム処理の乱数取得を始める前に設定し、使用した値を記録してください。

戻り値

ありません(void)。

使用条件と制限

状態の2バイトがともに0なら、次の取得時に0xA55Aへ置き換えてからシフトします。このため__rng_seed(0)の直後の__rng8()は255を返します。シード設定は0を保存し、置き換えは取得側で行います。

各呼び出しは持続する状態を共有します。再現には、同じ対象機種・シード・呼び出し順が必要で、ほかの補助関数による取得もその順番に含まれます。シード設定と取得は一つの実行箇所にまとめてください。今回の確認は、割り込み処理と並行して安全に使えることを保証するものではありません。

GBとFCは異なるアルゴリズムを使い、同じシードでも同じ列になることを保証しません。ゲーム処理向けの決定的な生成器であり、暗号用の乱数源ではありません。検証対象は実装の動作で、統計的な品質や周期ではありません。

使用例

c
__rng_seed(0x1234);
repeated=__rng8(); // The first byte is 19 again.
__rng_seed(0);
zero=__rng8(); // The next draw uses the FC fallback state and returns 255.

呼び出し部分のコード例です。完全なプログラムに組み込む際は、記載した初期化とバッファの条件を満たしてください。

期待する結果

FCの列の再現と、シード0の扱いを確かめるサンプルです。シード0x1234ではFIRSTとRESEEDEDがともに00019になります。0を設定した後のZERO SEEDは00255です。

完全なNROMプログラムでは、黒地に青文字で10進数の結果を表示します。FAILEDは00000になる必要があります。描画を無効にした状態で文字を転送してから、結果画面を有効にします。

各条件で32個の出力と、その後の8回のバイト取得を独立したホスト側モデルと照合しました。GBは93条件、FCは18条件で、定数・実行時のシード指定や、GBの範囲・確率・重みの境界条件を含みます。サンプル画面はエミュレータで240フレーム後に確認しました。実機や統計的な品質は未検証です。

__rng_seed: NROM
NROM

使用例の完全なプログラム

ビルドコマンド

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\rng_core.c -I .\kitaqfc\lib -I .\kitaq-docs\samples -o .\out\rng_core.nes --mapper=nrom --nes-chr=.\kitaq-docs\samples\font.chr --no-cache --no-disasm
実装とソース

kitaqfc/lib/intrinsics.h:37

宣言の注記(原文)
Replace both persistent state bytes. Reusing a seed reproduces the sequence; this is not cryptographic randomness.

kitaqfc/kitaqfc/CodeGenerator.cs:4661

csharp
case "__rng_seed":
                    return EmitKitaqfcHelperCall(funcName, args, ctx, new[] {2}, "__rng_seed");

EmitHelperStart("__rng_seed");
            EmitAsm("LDA", Mem(CallArgBase));
            EmitAsm("STA", Mem(_runtimeRngLoAddress));
            EmitAsm("LDA", Mem(CallArgBase + 1));
            EmitAsm("STA", Mem(_runtimeRngHiAddress));
            EmitAsm("RTS");
__rob_flash

描画を点灯側または消灯側へ設定し、NMIカウンターが1回変わるまで待ちます。設定したマスクは戻った後も維持します。

書式

c
void __rob_flash(u8 bright);

引数

bright

0なら消灯、0以外なら点灯です。1だけでなく255なども点灯として扱います。輝度の段階指定ではありません。

戻り値

戻り値はありません。指定した待機が終わると呼び出し元へ戻ります。

使用条件と制限

intrinsics.hを読み込みます。既定のNMIハンドラーが動き、内部のNMIカウンターが更新される状態で使ってください。NMIが無効、または独自ハンドラーがカウンターを更新しない場合、待機は終わりません。タイムアウトはありません。

点灯側はPPUMASKとその保持値を0x1E、消灯側は0にします。以前の描画マスクは復元しません。背景・スプライト・左端描画をまとめて切り替え、グレースケールや色強調の指定も上書きします。

描画を有効にするだけでは白い画面になりません。明るいタイルとパレット、消灯中に見える暗い背景色を呼び出し側で用意します。サンプルは白い単色タイルと黒い背景色を使い、処理中に画面全体を点滅させます。

待機回数はNMIカウンターの変化数です。呼び出し時点から最初のNMIまでは1フレーム未満になる場合があります。前景処理から使い、割り込み内から呼ばないでください。別処理による描画マスクの変更は点滅を乱します。

画面の明暗と待機のための低水準部品です。ロボット用コマンドの前置きや機種別の命令列、受信確認は生成しません。検証はKUROSAKI上のPPUMASK書き込み順序・NMI回数・画像を対象とし、実機ロボットの受信は確認していません。

使用例

c
start=__nmi_ready();
    __rob_flash(1); // Enable rendering and wait for one NMI-counter change.
    rob_result[0]=(u8)(__nmi_ready()-start);
    __rob_flash(0); // Disable rendering; the black backdrop remains visible.

呼び出し部分のコード例です。完全なプログラムに組み込む際は、記載した初期化とバッファの条件を満たしてください。

期待する結果

実行後、FLASH WAITS 1、PULSE 3 ON / 2 OFF = 5、A5 BYTE WAITS 48と表示します。下の図は0xA5のビット列で、上段が1・0・1・0、下段が0・1・0・1です。1区画は1回の待機、白は描画有効、黒は無効を表します。ビット1は白4区画+黒2区画、ビット0は白2区画+黒4区画です。FINAL MASK 0とTIMING CHECKS OKが完了を示します。

__rob_flash: NTSC — 実行後、FLASH WAITS 1、PULSE 3 ON / 2 OFF = 5、A5 BYTE WAITS 48と表示します。下の図は0xA5のビット列で、上段が1・0・1・0、下段が0・1・0・1です。1区画は1回の待機、白は描画有効、黒は無効を表します。ビット1は白4区画+黒2区画、ビット0は白2区画+黒4区画です。FINAL MASK 0とTIMING CHECKS OKが完了を示します。
NTSC — 実行後、FLASH WAITS 1、PULSE 3 ON / 2 OFF = 5、A5 BYTE WAITS 48と表示します。下の図は0xA5のビット列で、上段が1・0・1・0、下段が0・1・0・1です。1区画は1回の待機、白は描画有効、黒は無効を表します。ビット1は白4区画+黒2区画、ビット0は白2区画+黒4区画です。FINAL MASK 0とTIMING CHECKS OKが完了を示します。

使用例の完全なプログラム

ビルドコマンド

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\rob_timing.c -I .\kitaqfc\lib -I .\kitaq-docs\samples --mapper=nrom --nes-chr=.\kitaq-docs\samples\api-examples\fc\vram_shapes.chr -o .\out\rob_timing.nes --no-cache --no-disasm
実装とソース

kitaqfc/lib/intrinsics.h:272

宣言の注記(原文)
Set PPUMASK and its shadow to 1E or 00, then wait for an NMI-counter change.
The caller supplies image/palette brightness; rendering enable alone does not create a white screen.

kitaqfc/kitaqfc/CodeGenerator.cs:7867

csharp
EmitHelperStart("__rob_flash");
            EmitAsm("LDA", Mem(CallArgBase));
            string black = NewGeneratedLabel("rob_black");
            EmitAsm("BEQ", Rel(black));
            EmitAsm("LDA", Imm(0x1E));
            EmitAsm("STA", Mem(_runtimePpuMaskShadowAddress));
            EmitAsm("STA", Mem(0x2001));
            EmitAsm("JSR", Abs("__nmi_wait"));
            EmitAsm("RTS");
            _assembly.Add(Expr.Make(Tag.Label, black));
            EmitAsm("LDA", Imm(0x00));
            EmitAsm("STA", Mem(_runtimePpuMaskShadowAddress));
            EmitAsm("STA", Mem(0x2001));
            EmitAsm("JSR", Abs("__nmi_wait"));
            EmitAsm("RTS");

            // Emit the requested number of enabled frames followed by the requested disabled frames.
__rob_pulse

指定回数の点灯待機を行い、続けて指定回数の消灯待機を行います。1組のパルスを作る関数です。

書式

c
void __rob_pulse(u8 on_frames, u8 off_frames);

引数

on_frames

点灯側の待機回数0~255。0なら点灯側を実行しません。

off_frames

消灯側の待機回数0~255。0なら消灯側を実行しません。合計は最大510回です。

戻り値

戻り値はありません。指定した待機が終わると呼び出し元へ戻ります。

使用条件と制限

両方0ならマスクも待機も変更しません。消灯回数が1以上なら終了時のマスクは0、点灯だけなら0x1Eです。

intrinsics.hを読み込みます。既定のNMIハンドラーが動き、内部のNMIカウンターが更新される状態で使ってください。NMIが無効、または独自ハンドラーがカウンターを更新しない場合、待機は終わりません。タイムアウトはありません。

点灯側はPPUMASKとその保持値を0x1E、消灯側は0にします。以前の描画マスクは復元しません。背景・スプライト・左端描画をまとめて切り替え、グレースケールや色強調の指定も上書きします。

描画を有効にするだけでは白い画面になりません。明るいタイルとパレット、消灯中に見える暗い背景色を呼び出し側で用意します。サンプルは白い単色タイルと黒い背景色を使い、処理中に画面全体を点滅させます。

待機回数はNMIカウンターの変化数です。呼び出し時点から最初のNMIまでは1フレーム未満になる場合があります。前景処理から使い、割り込み内から呼ばないでください。別処理による描画マスクの変更は点滅を乱します。

画面の明暗と待機のための低水準部品です。ロボット用コマンドの前置きや機種別の命令列、受信確認は生成しません。検証はKUROSAKI上のPPUMASK書き込み順序・NMI回数・画像を対象とし、実機ロボットの受信は確認していません。

使用例

c
start=__nmi_ready();
    __rob_pulse(3,2); // Three enabled waits, then two disabled waits.
    rob_result[1]=(u8)(__nmi_ready()-start);

呼び出し部分のコード例です。完全なプログラムに組み込む際は、記載した初期化とバッファの条件を満たしてください。

期待する結果

実行後、FLASH WAITS 1、PULSE 3 ON / 2 OFF = 5、A5 BYTE WAITS 48と表示します。下の図は0xA5のビット列で、上段が1・0・1・0、下段が0・1・0・1です。1区画は1回の待機、白は描画有効、黒は無効を表します。ビット1は白4区画+黒2区画、ビット0は白2区画+黒4区画です。FINAL MASK 0とTIMING CHECKS OKが完了を示します。

__rob_pulse: NTSC — 実行後、FLASH WAITS 1、PULSE 3 ON / 2 OFF = 5、A5 BYTE WAITS 48と表示します。下の図は0xA5のビット列で、上段が1・0・1・0、下段が0・1・0・1です。1区画は1回の待機、白は描画有効、黒は無効を表します。ビット1は白4区画+黒2区画、ビット0は白2区画+黒4区画です。FINAL MASK 0とTIMING CHECKS OKが完了を示します。
NTSC — 実行後、FLASH WAITS 1、PULSE 3 ON / 2 OFF = 5、A5 BYTE WAITS 48と表示します。下の図は0xA5のビット列で、上段が1・0・1・0、下段が0・1・0・1です。1区画は1回の待機、白は描画有効、黒は無効を表します。ビット1は白4区画+黒2区画、ビット0は白2区画+黒4区画です。FINAL MASK 0とTIMING CHECKS OKが完了を示します。

使用例の完全なプログラム

ビルドコマンド

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\rob_timing.c -I .\kitaqfc\lib -I .\kitaq-docs\samples --mapper=nrom --nes-chr=.\kitaq-docs\samples\api-examples\fc\vram_shapes.chr -o .\out\rob_timing.nes --no-cache --no-disasm
実装とソース

kitaqfc/lib/intrinsics.h:274

宣言の注記(原文)
Run on_frames enabled-mask waits followed by off_frames disabled-mask waits; NMI must advance.

kitaqfc/kitaqfc/CodeGenerator.cs:7884

csharp
EmitHelperStart("__rob_pulse");
            EmitAsm("LDX", Mem(CallArgBase));
            string onLoop = NewGeneratedLabel("rob_on_loop");
            string offStart = NewGeneratedLabel("rob_off_start");
            _assembly.Add(Expr.Make(Tag.Label, onLoop));
            EmitAsm("CPX", Imm(0));
            EmitAsm("BEQ", Rel(offStart));
            EmitAsm("LDA", Imm(1));
            EmitAsm("STA", Mem(CallArgBase));
            EmitAsm("JSR", Abs("__rob_flash"));
            EmitAsm("DEX");
            EmitAsm("BNE", Rel(onLoop));
            _assembly.Add(Expr.Make(Tag.Label, offStart));
            EmitAsm("LDX", Mem(CallArgBase + 1));
            string offLoop = NewGeneratedLabel("rob_off_loop");
            string pulseDone = NewGeneratedLabel("rob_pulse_done");
            _assembly.Add(Expr.Make(Tag.Label, offLoop));
            EmitAsm("CPX", Imm(0));
            EmitAsm("BEQ", Rel(pulseDone));
            EmitAsm("LDA", Imm(0));
            EmitAsm("STA", Mem(CallArgBase));
            EmitAsm("JSR", Abs("__rob_flash"));
            EmitAsm("DEX");
            EmitAsm("BNE", Rel(offLoop));
            _assembly.Add(Expr.Make(Tag.Label, pulseDone));
            EmitAsm("RTS");

            // Choose a four/two or two/four frame pulse pattern for each current high bit.
__rob_send_byte

1バイトを最上位ビットから8ビットの点滅パターンに変換します。1は点灯4回・消灯2回、0は点灯2回・消灯4回の待機で表し、合計48回のNMI待機を行います。

書式

c
void __rob_send_byte(u8 pattern);

引数

pattern

送る8ビットのパターン0~255です。0xA5なら1,0,1,0,0,1,0,1の順になります。チャンネル番号やロボットの操作番号ではありません。

戻り値

戻り値はありません。指定した待機が終わると呼び出し元へ戻ります。

使用条件と制限

最後のビットも必ず消灯待機で終わり、終了時のPPUMASKと保持値は0です。固定長の8ビットだけを送り、開始記号や終端記号は加えません。

intrinsics.hを読み込みます。既定のNMIハンドラーが動き、内部のNMIカウンターが更新される状態で使ってください。NMIが無効、または独自ハンドラーがカウンターを更新しない場合、待機は終わりません。タイムアウトはありません。

点灯側はPPUMASKとその保持値を0x1E、消灯側は0にします。以前の描画マスクは復元しません。背景・スプライト・左端描画をまとめて切り替え、グレースケールや色強調の指定も上書きします。

描画を有効にするだけでは白い画面になりません。明るいタイルとパレット、消灯中に見える暗い背景色を呼び出し側で用意します。サンプルは白い単色タイルと黒い背景色を使い、処理中に画面全体を点滅させます。

待機回数はNMIカウンターの変化数です。呼び出し時点から最初のNMIまでは1フレーム未満になる場合があります。前景処理から使い、割り込み内から呼ばないでください。別処理による描画マスクの変更は点滅を乱します。

画面の明暗と待機のための低水準部品です。ロボット用コマンドの前置きや機種別の命令列、受信確認は生成しません。検証はKUROSAKI上のPPUMASK書き込み順序・NMI回数・画像を対象とし、実機ロボットの受信は確認していません。

使用例

c
start=__nmi_ready();
    __rob_send_byte(0xA5); // 10100101, most-significant bit first; 48 waits.
    rob_result[2]=(u8)(__nmi_ready()-start);
    rob_result[3]=__ppu_mask_get(); // Zero: the sequence ends with rendering off.

呼び出し部分のコード例です。完全なプログラムに組み込む際は、記載した初期化とバッファの条件を満たしてください。

期待する結果

実行後、FLASH WAITS 1、PULSE 3 ON / 2 OFF = 5、A5 BYTE WAITS 48と表示します。下の図は0xA5のビット列で、上段が1・0・1・0、下段が0・1・0・1です。1区画は1回の待機、白は描画有効、黒は無効を表します。ビット1は白4区画+黒2区画、ビット0は白2区画+黒4区画です。FINAL MASK 0とTIMING CHECKS OKが完了を示します。

__rob_send_byte: NTSC — 実行後、FLASH WAITS 1、PULSE 3 ON / 2 OFF = 5、A5 BYTE WAITS 48と表示します。下の図は0xA5のビット列で、上段が1・0・1・0、下段が0・1・0・1です。1区画は1回の待機、白は描画有効、黒は無効を表します。ビット1は白4区画+黒2区画、ビット0は白2区画+黒4区画です。FINAL MASK 0とTIMING CHECKS OKが完了を示します。
NTSC — 実行後、FLASH WAITS 1、PULSE 3 ON / 2 OFF = 5、A5 BYTE WAITS 48と表示します。下の図は0xA5のビット列で、上段が1・0・1・0、下段が0・1・0・1です。1区画は1回の待機、白は描画有効、黒は無効を表します。ビット1は白4区画+黒2区画、ビット0は白2区画+黒4区画です。FINAL MASK 0とTIMING CHECKS OKが完了を示します。

使用例の完全なプログラム

ビルドコマンド

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\rob_timing.c -I .\kitaqfc\lib -I .\kitaq-docs\samples --mapper=nrom --nes-chr=.\kitaq-docs\samples\api-examples\fc\vram_shapes.chr -o .\out\rob_timing.nes --no-cache --no-disasm
実装とソース

kitaqfc/lib/intrinsics.h:278

宣言の注記(原文)
Send eight bits MSB first: one uses 4 on/2 off waits; zero uses 2 on/4 off.
Requires an advancing runtime NMI counter; 48 waits in total, final PPUMASK=0.
This raw pattern does not add a robot command preamble or validate reception.

kitaqfc/kitaqfc/CodeGenerator.cs:7912

csharp
EmitHelperStart("__rob_send_byte");
            EmitAsm("LDA", Mem(CallArgBase));
            EmitAsm("LDX", Imm(8));
            string bitLoop = NewGeneratedLabel("rob_bit_loop");
            string bitZero = NewGeneratedLabel("rob_bit_zero");
            string bitNext = NewGeneratedLabel("rob_bit_next");
            _assembly.Add(Expr.Make(Tag.Label, bitLoop));
            // Preserve the remaining payload and outer count on the CPU stack.
            // Pulse consumes X; its NMI wait also uses the shared scratch byte.
            // ASL selects the next MSB in carry, which TXA/PHA leave unchanged.
            EmitAsm("ASL");
            EmitAsm("PHA");
            EmitAsm("TXA");
            EmitAsm("PHA");
            EmitAsm("BCC", Rel(bitZero));
            EmitAsm("LDA", Imm(4));
            EmitAsm("STA", Mem(CallArgBase));
            EmitAsm("LDA", Imm(2));
            EmitAsm("STA", Mem(CallArgBase + 1));
            EmitAsm("JSR", Abs("__rob_pulse"));
            EmitAsm("JMP", Abs(bitNext));
            _assembly.Add(Expr.Make(Tag.Label, bitZero));
            EmitAsm("LDA", Imm(2));
            EmitAsm("STA", Mem(CallArgBase));
            EmitAsm("LDA", Imm(4));
            EmitAsm("STA", Mem(CallArgBase + 1));
            EmitAsm("JSR", Abs("__rob_pulse"));
            _assembly.Add(Expr.Make(Tag.Label, bitNext));
            EmitAsm("PLA");
            EmitAsm("TAX");
            EmitAsm("PLA");
            EmitAsm("DEX");
            EmitAsm("BNE", Rel(bitLoop));
            EmitAsm("RTS");
        }
__scroll_latch_reset

PPUSTATUSを読み、戻り値を捨てることでPPUのスクロール・アドレス共用書き込みラッチをリセットします。VBlankフラグも消えます。スクロール座標へ0を書いたり、保存済みのX・Yを変更したりする処理ではありません。

書式

c
void __scroll_latch_reset(void);

戻り値

ありません(void)。

使用条件と制限

PPUSTATUSの読み取りはNMIのタイミングと、その後のPPUSCROLL・PPUADDR両方への書き込みに影響します。別の処理が2バイトの途中まで書いている場所へ挟まないでください。これらの呼び出しはVBlankを待たず、未処理の転送キューも完了させません。

NESdev: PPU registers

NESdev: PPU scrolling

使用例

c
address_port=0x21; // Leave the shared latch halfway through an address pair.
__scroll_latch_reset();
address_port=0x21;address_port=0x83;data_port=2; // Blue left-half marker.

呼び出し部分のコード例です。完全なプログラムに組み込む際は、記載した初期化とバッファの条件を満たしてください。

期待する結果

このプログラムではレジスター設定、タイル・属性の直接書き込み、スクロール、背景・スプライトそれぞれのパレット転送を学びます。(0,16)の青い帯は256×16ピクセルです。上段は8×8の塗りつぶし、左半分4×8の棒、8行の三角形を繰り返し、下段は左半分の棒だけを並べます。(16,48)に緑の塗りつぶし64×32、(144,48)に三角形タイルを横8個・縦4個並べた赤い64×32の領域が出ます。

y=96では、x=16・24・32に青い正方形・左半分の棒・三角形、x=80に赤い左半分の棒、x=144に緑の三角形が表示されます。これらは背景タイルです。別パレットを使うスプライトはy=144に並び、x=16が赤い8×8の正方形、x=80が青い左半分4×8の棒、x=144が緑の8行の三角形です。最終スクロールは(0,0)です。

画像ではスプライトの色も含め、16,384ピクセルの形と色を照合します。別の31ケースで、レジスターの置き換え、ステータス・ラッチの副作用、スクロール保存値、0・255バイト転送、増分1・32、マップ端、矩形の寸法0、テーブル選択、属性1バイト全体、背景・スプライト両方のパレットを検証しています。Y=255はレジスターへ書く値としての確認で、正しいスクロール画像の検証ではありません。検証環境はKUROSAKIのNROMで、実機は未検証です。

__scroll_latch_reset: NROM
NROM

使用例の完全なプログラム

ビルドコマンド

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\ppu_intrinsics.c -I .\kitaqfc\lib -I .\kitaq-docs\samples --nes-chr=.\kitaq-docs\samples\api-examples\fc\vram_shapes.chr -o .\out\ppu_intrinsics.nes --no-cache --no-disasm
実装とソース

kitaqfc/lib/intrinsics.h:112

宣言の注記(原文)
Read PPUSTATUS to reset the scroll/address write latch; the read also clears VBlank status.

kitaqfc/kitaqfc/CodeGenerator.cs:4708

csharp
case "__scroll_latch_reset":
                    if (!Need(0)) return true;
                    EmitAsm("LDA", Mem(0x2002)); return true;
                // Store both scroll components, reset the write latch, then emit the X/Y pair.
__scroll_set

ピクセル単位のX・Yをランタイムの保存領域へ記録し、PPUSTATUSを読んで共有ラッチをリセットしてから、PPUSCROLLへX、Yの順に書きます。スクロールを直接更新する処理で、カメラ移動の予約、NMI待機、基準ネームテーブルの選択は行いません。

書式

c
void __scroll_set(u8 x, u8 y);

引数

x / y

オフセットは符号なし1バイトのピクセル値です。Xは0..255、通常のYは0..239を使います。Yの240..255もそのまま書きますが、PPUでは特殊な行の扱いとなり、通常の表示マップのスクロールとは異なります。基準ネームテーブルのビットはPPUCTRLで指定します。VRAMへ直接アクセスした後、描画前に意図したスクロールを復元してください。この関数は実行タイミングを予約しません。

戻り値

ありません(void)。

使用条件と制限

PPUSTATUSの読み取りはNMIのタイミングと、その後のPPUSCROLL・PPUADDR両方への書き込みに影響します。別の処理が2バイトの途中まで書いている場所へ挟まないでください。これらの呼び出しはVBlankを待たず、未処理の転送キューも完了させません。

NESdev: PPU registers

NESdev: PPU scrolling

使用例

c
__scroll_set(19,27); // Save both coordinates and write the two-scroll-byte pair.

呼び出し部分のコード例です。完全なプログラムに組み込む際は、記載した初期化とバッファの条件を満たしてください。

期待する結果

このプログラムではレジスター設定、タイル・属性の直接書き込み、スクロール、背景・スプライトそれぞれのパレット転送を学びます。(0,16)の青い帯は256×16ピクセルです。上段は8×8の塗りつぶし、左半分4×8の棒、8行の三角形を繰り返し、下段は左半分の棒だけを並べます。(16,48)に緑の塗りつぶし64×32、(144,48)に三角形タイルを横8個・縦4個並べた赤い64×32の領域が出ます。

y=96では、x=16・24・32に青い正方形・左半分の棒・三角形、x=80に赤い左半分の棒、x=144に緑の三角形が表示されます。これらは背景タイルです。別パレットを使うスプライトはy=144に並び、x=16が赤い8×8の正方形、x=80が青い左半分4×8の棒、x=144が緑の8行の三角形です。最終スクロールは(0,0)です。

画像ではスプライトの色も含め、16,384ピクセルの形と色を照合します。別の31ケースで、レジスターの置き換え、ステータス・ラッチの副作用、スクロール保存値、0・255バイト転送、増分1・32、マップ端、矩形の寸法0、テーブル選択、属性1バイト全体、背景・スプライト両方のパレットを検証しています。Y=255はレジスターへ書く値としての確認で、正しいスクロール画像の検証ではありません。検証環境はKUROSAKIのNROMで、実機は未検証です。

__scroll_set: NROM
NROM

使用例の完全なプログラム

ビルドコマンド

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\ppu_intrinsics.c -I .\kitaqfc\lib -I .\kitaq-docs\samples --nes-chr=.\kitaq-docs\samples\api-examples\fc\vram_shapes.chr -o .\out\ppu_intrinsics.nes --no-cache --no-disasm
実装とソース

kitaqfc/lib/intrinsics.h:114

宣言の注記(原文)
Save X/Y in runtime shadows, reset the PPU latch and write both scroll bytes immediately.

kitaqfc/kitaqfc/CodeGenerator.cs:4712

csharp
case "__scroll_set":
                    if (!Need(2)) return true;
                    Store1(args[0], _runtimeScrollXAddress); Store1(args[1], _runtimeScrollYAddress);
                    EmitAsm("LDA", Mem(0x2002)); EmitAsm("LDA", Mem(_runtimeScrollXAddress)); EmitAsm("STA", Mem(0x2005)); EmitAsm("LDA", Mem(_runtimeScrollYAddress)); EmitAsm("STA", Mem(0x2005)); return true;
                // Changing X rewrites both latch bytes, using the retained Y component.
__scroll_x_set

保存済みのXを置き換え、ステータス読み取りで共有ラッチをリセットし、新しいXと保存済みのYをPPUSCROLLへ書きます。YはPPUから読み戻すのではなくソフトウェアの保存値です。レジスターへの直接書き込みでは、この保存値は更新されません。

書式

c
void __scroll_x_set(u8 x);

引数

x

オフセットは符号なし1バイトのピクセル値です。Xは0..255、通常のYは0..239を使います。Yの240..255もそのまま書きますが、PPUでは特殊な行の扱いとなり、通常の表示マップのスクロールとは異なります。基準ネームテーブルのビットはPPUCTRLで指定します。VRAMへ直接アクセスした後、描画前に意図したスクロールを復元してください。この関数は実行タイミングを予約しません。

戻り値

ありません(void)。

使用条件と制限

PPUSTATUSの読み取りはNMIのタイミングと、その後のPPUSCROLL・PPUADDR両方への書き込みに影響します。別の処理が2バイトの途中まで書いている場所へ挟まないでください。これらの呼び出しはVBlankを待たず、未処理の転送キューも完了させません。

NESdev: PPU registers

NESdev: PPU scrolling

使用例

c
__scroll_x_set(0); // Keeps saved Y=27 while changing X to zero.

呼び出し部分のコード例です。完全なプログラムに組み込む際は、記載した初期化とバッファの条件を満たしてください。

期待する結果

このプログラムではレジスター設定、タイル・属性の直接書き込み、スクロール、背景・スプライトそれぞれのパレット転送を学びます。(0,16)の青い帯は256×16ピクセルです。上段は8×8の塗りつぶし、左半分4×8の棒、8行の三角形を繰り返し、下段は左半分の棒だけを並べます。(16,48)に緑の塗りつぶし64×32、(144,48)に三角形タイルを横8個・縦4個並べた赤い64×32の領域が出ます。

y=96では、x=16・24・32に青い正方形・左半分の棒・三角形、x=80に赤い左半分の棒、x=144に緑の三角形が表示されます。これらは背景タイルです。別パレットを使うスプライトはy=144に並び、x=16が赤い8×8の正方形、x=80が青い左半分4×8の棒、x=144が緑の8行の三角形です。最終スクロールは(0,0)です。

画像ではスプライトの色も含め、16,384ピクセルの形と色を照合します。別の31ケースで、レジスターの置き換え、ステータス・ラッチの副作用、スクロール保存値、0・255バイト転送、増分1・32、マップ端、矩形の寸法0、テーブル選択、属性1バイト全体、背景・スプライト両方のパレットを検証しています。Y=255はレジスターへ書く値としての確認で、正しいスクロール画像の検証ではありません。検証環境はKUROSAKIのNROMで、実機は未検証です。

__scroll_x_set: NROM
NROM

使用例の完全なプログラム

ビルドコマンド

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\ppu_intrinsics.c -I .\kitaqfc\lib -I .\kitaq-docs\samples --nes-chr=.\kitaq-docs\samples\api-examples\fc\vram_shapes.chr -o .\out\ppu_intrinsics.nes --no-cache --no-disasm
実装とソース

kitaqfc/lib/intrinsics.h:116

宣言の注記(原文)
Replace saved X and write an X/Y pair using the retained Y shadow.

kitaqfc/kitaqfc/CodeGenerator.cs:4717

csharp
case "__scroll_x_set":
                    if (!Need(1)) return true;
                    Store1(args[0], _runtimeScrollXAddress);
                    EmitAsm("LDA", Mem(0x2002)); EmitAsm("LDA", Mem(_runtimeScrollXAddress)); EmitAsm("STA", Mem(0x2005)); EmitAsm("LDA", Mem(_runtimeScrollYAddress)); EmitAsm("STA", Mem(0x2005)); return true;
                // Changing Y rewrites both latch bytes, using the retained X component.
__scroll_y_set

保存済みのYを置き換え、ステータス読み取りで共有ラッチをリセットし、保存済みのXと新しいYをPPUSCROLLへ書きます。変更する値がYだけでも、スクロールの2バイトを両方書きます。Xにはランタイムの保存値を使います。

書式

c
void __scroll_y_set(u8 y);

引数

y

オフセットは符号なし1バイトのピクセル値です。Xは0..255、通常のYは0..239を使います。Yの240..255もそのまま書きますが、PPUでは特殊な行の扱いとなり、通常の表示マップのスクロールとは異なります。基準ネームテーブルのビットはPPUCTRLで指定します。VRAMへ直接アクセスした後、描画前に意図したスクロールを復元してください。この関数は実行タイミングを予約しません。

戻り値

ありません(void)。

使用条件と制限

PPUSTATUSの読み取りはNMIのタイミングと、その後のPPUSCROLL・PPUADDR両方への書き込みに影響します。別の処理が2バイトの途中まで書いている場所へ挟まないでください。これらの呼び出しはVBlankを待たず、未処理の転送キューも完了させません。

NESdev: PPU registers

NESdev: PPU scrolling

使用例

c
__scroll_y_set(0); // Keeps saved X=0; the scene is now at the origin.

呼び出し部分のコード例です。完全なプログラムに組み込む際は、記載した初期化とバッファの条件を満たしてください。

期待する結果

このプログラムではレジスター設定、タイル・属性の直接書き込み、スクロール、背景・スプライトそれぞれのパレット転送を学びます。(0,16)の青い帯は256×16ピクセルです。上段は8×8の塗りつぶし、左半分4×8の棒、8行の三角形を繰り返し、下段は左半分の棒だけを並べます。(16,48)に緑の塗りつぶし64×32、(144,48)に三角形タイルを横8個・縦4個並べた赤い64×32の領域が出ます。

y=96では、x=16・24・32に青い正方形・左半分の棒・三角形、x=80に赤い左半分の棒、x=144に緑の三角形が表示されます。これらは背景タイルです。別パレットを使うスプライトはy=144に並び、x=16が赤い8×8の正方形、x=80が青い左半分4×8の棒、x=144が緑の8行の三角形です。最終スクロールは(0,0)です。

画像ではスプライトの色も含め、16,384ピクセルの形と色を照合します。別の31ケースで、レジスターの置き換え、ステータス・ラッチの副作用、スクロール保存値、0・255バイト転送、増分1・32、マップ端、矩形の寸法0、テーブル選択、属性1バイト全体、背景・スプライト両方のパレットを検証しています。Y=255はレジスターへ書く値としての確認で、正しいスクロール画像の検証ではありません。検証環境はKUROSAKIのNROMで、実機は未検証です。

__scroll_y_set: NROM
NROM

使用例の完全なプログラム

ビルドコマンド

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\ppu_intrinsics.c -I .\kitaqfc\lib -I .\kitaq-docs\samples --nes-chr=.\kitaq-docs\samples\api-examples\fc\vram_shapes.chr -o .\out\ppu_intrinsics.nes --no-cache --no-disasm
実装とソース

kitaqfc/lib/intrinsics.h:118

宣言の注記(原文)
Replace saved Y and write an X/Y pair using the retained X shadow.

kitaqfc/kitaqfc/CodeGenerator.cs:4722

csharp
case "__scroll_y_set":
                    if (!Need(1)) return true;
                    Store1(args[0], _runtimeScrollYAddress);
                    EmitAsm("LDA", Mem(0x2002)); EmitAsm("LDA", Mem(_runtimeScrollXAddress)); EmitAsm("STA", Mem(0x2005)); EmitAsm("LDA", Mem(_runtimeScrollYAddress)); EmitAsm("STA", Mem(0x2005)); return true;
                // Modify only the NMI-enable bit in the PPUCTRL shadow and publish the new register value.
__sdot2_q1_7

符号付き2項の内積を計算します。

書式

c
u16 __sdot2_q1_7(u16 a0, u16 a1, u8 b0, u8 b1);

引数

a0

符号付き16ビットのビット列。数値は−32768~32767。

a1

符号付き16ビットのビット列。数値は−32768~32767。

b0

符号付き8ビットのビット列。係数は−128~127。

b1

符号付き8ビットのビット列。係数は−128~127。

戻り値

a0*b0+a1*b1の下位16ビットです。

使用条件と制限

FCでは名前にq8_8/q1_7があっても生の整数積を使い、固定小数点の縮小シフトは行いません。Q1.7の3関数は対応する符号付き関数への別名です。単位変換は呼び出し側で行います。符号付き結果もu16のビット列として返します。

使用例

c
// Compare literal coefficients with values read from RAM.
result[34]=__sdot2_q1_7(-513,769,-64,127);
v0=-513;v1=769;v2=-64;v3=127;
result[35]=__sdot2_q1_7(v0,v1,v2,v3);
// Reverse coefficient signs and verify negative products.
result[36]=__sdot2_q1_7(-513,769,64,-127);
v0=-513;v1=769;v2=64;v3=-127;
result[37]=__sdot2_q1_7(v0,v1,v2,v3);

呼び出し部分のコード例です。完全なプログラムに組み込む際は、記載した初期化とバッファの条件を満たしてください。

期待する結果

固定値とRAM変数で同じ計算を行う例です。画面には代表値を符号なし16ビットで表示し、負数は2の補数の値になります。GBは固定小数点の縮小を行い、FCは整数積を返すため、同じ入力でも両機種の結果は異なります。

この抜粋で保存する16ビット値は result[34]=64959, result[35]=64959, result[36]=577, result[37]=577 です。固定値とRAM変数を使った計算は、それぞれ同じ値になります。画面は代表値を表示し、全結果はresult配列に保存します。

__sdot2_q1_7: NROM
NROM

使用例の完全なプログラム

ビルドコマンド

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\batch3_math.c -I .\kitaqfc\lib -I .\kitaq-docs\samples -o .\out\batch3_math.nes --no-cache --no-disasm --mapper=nrom --nes-chr=.\kitaq-docs\samples\font.chr
実装とソース

kitaqfc/lib/intrinsics.h:71

宣言の注記(原文)
FC alias of __sdot2_q8_8: two raw signed-pattern products, summed without rescaling.

kitaqfc/kitaqfc/CodeGenerator.cs:4658

csharp
case "__sdot2_q1_7": return EmitKitaqfcHelperCall(funcName, args, ctx, new[] {2,2,1,1}, funcName);

void EmitHelperMath()
        {
            // Consume eight multiplier bits and accumulate the low product word in shared temporaries, returning A/X.
            EmitHelperStart("__mul16x8");
            EmitAsm("LDA", Imm(0));
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp1Address));
            EmitAsm("LDX", Imm(8));
            string loop = NewGeneratedLabel("mul16x8_loop");
            string skip = NewGeneratedLabel("mul16x8_skip_add");
            _assembly.Add(Expr.Make(Tag.Label, loop));
            EmitAsm("LSR", Mem(CallArgBase + 2));
            EmitAsm("BCC", Rel(skip));
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("CLC");
            EmitAsm("ADC", Mem(CallArgBase));
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp1Address));
            EmitAsm("ADC", Mem(CallArgBase + 1));
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp1Address));
            _assembly.Add(Expr.Make(Tag.Label, skip));
            EmitAsm("ASL", Mem(CallArgBase));
            EmitAsm("ROL", Mem(CallArgBase + 1));
            EmitAsm("DEX");
            EmitAsm("BNE", Rel(loop));
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("LDX", Mem(_runtimeIntrinsicTmp1Address));
            EmitAsm("RTS");

            // Zero-extend the byte multiplicand, reuse word multiplication and return its high byte.
            EmitHelperStart("__mul8x8_hi");
            EmitAsm("LDA", Mem(CallArgBase + 1));
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp2Address));
            EmitAsm("LDA", Imm(0));
            EmitAsm("STA", Mem(CallArgBase + 1));
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp2Address));
            EmitAsm("STA", Mem(CallArgBase + 2));
            EmitAsm("JSR", Abs("__mul16x8"));
            EmitAsm("TXA");
            EmitAsm("RTS");

            // Convert signed operands to magnitudes, multiply, then restore the product sign modulo 65536.
            EmitHelperStart("__smul16x8");
            EmitAsm("LDA", Imm(0));
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp2Address));
            string aPos = NewGeneratedLabel("smul_a_positive");
            EmitAsm("LDA", Mem(CallArgBase + 1));
            EmitAsm("AND", Imm(0x80));
            EmitAsm("BEQ", Rel(aPos));
            EmitAsm("LDA", Mem(CallArgBase)); EmitAsm("EOR", Imm(0xFF)); EmitAsm("CLC"); EmitAsm("ADC", Imm(1)); EmitAsm("STA", Mem(CallArgBase));
            EmitAsm("LDA", Mem(CallArgBase + 1)); EmitAsm("EOR", Imm(0xFF)); EmitAsm("ADC", Imm(0)); EmitAsm("STA", Mem(CallArgBase + 1));
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp2Address)); EmitAsm("EOR", Imm(1)); EmitAsm("STA", Mem(_runtimeIntrinsicTmp2Address));
            _assembly.Add(Expr.Make(Tag.Label, aPos));
            string bPos = NewGeneratedLabel("smul_b_positive");
            EmitAsm("LDA", Mem(CallArgBase + 2));
            EmitAsm("AND", Imm(0x80));
            EmitAsm("BEQ", Rel(bPos));
            EmitAsm("LDA", Mem(CallArgBase + 2)); EmitAsm("EOR", Imm(0xFF)); EmitAsm("CLC"); EmitAsm("ADC", Imm(1)); EmitAsm("STA", Mem(CallArgBase + 2));
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp2Address)); EmitAsm("EOR", Imm(1)); EmitAsm("STA", Mem(_runtimeIntrinsicTmp2Address));
            _assembly.Add(Expr.Make(Tag.Label, bPos));
            EmitAsm("JSR", Abs("__mul16x8"));
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("STX", Mem(_runtimeIntrinsicTmp1Address));
            string sdone = NewGeneratedLabel("smul_done");
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp2Address));
            EmitAsm("BEQ", Rel(sdone));
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp0Address)); EmitAsm("EOR", Imm(0xFF)); EmitAsm("CLC"); EmitAsm("ADC", Imm(1)); EmitAsm("STA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp1Address)); EmitAsm("EOR", Imm(0xFF)); EmitAsm("ADC", Imm(0)); EmitAsm("STA", Mem(_runtimeIntrinsicTmp1Address));
            _assembly.Add(Expr.Make(Tag.Label, sdone));
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("LDX", Mem(_runtimeIntrinsicTmp1Address));
            EmitAsm("RTS");

            EmitMacHelper("__mac16", "__mul16x8");
            EmitMacHelper("__smac16", "__smul16x8");
            EmitDot2Helper("__dot2_q8_8", "__mul16x8");
            EmitDot3Helper("__dot3_q8_8", "__mul16x8");
            EmitDot2Helper("__sdot2_q8_8", "__smul16x8");
            EmitDot3Helper("__sdot3_q8_8", "__smul16x8");
        }
__sdot2_q8_8

符号付き2項の内積を計算します。

書式

c
u16 __sdot2_q8_8(u16 a0, u16 a1, u8 b0, u8 b1);

引数

a0

符号付き16ビットのビット列。数値は−32768~32767。

a1

符号付き16ビットのビット列。数値は−32768~32767。

b0

符号付き8ビットのビット列。係数は−128~127。

b1

符号付き8ビットのビット列。係数は−128~127。

戻り値

a0*b0+a1*b1の下位16ビットです。

使用条件と制限

FCでは名前にq8_8/q1_7があっても生の整数積を使い、固定小数点の縮小シフトは行いません。Q1.7の3関数は対応する符号付き関数への別名です。単位変換は呼び出し側で行います。符号付き結果もu16のビット列として返します。

使用例

c
// Compare literal coefficients with values read from RAM.
result[26]=__sdot2_q8_8(-513,769,-64,127);
v0=-513;v1=769;v2=-64;v3=127;
result[27]=__sdot2_q8_8(v0,v1,v2,v3);
// Reverse coefficient signs and verify negative products.
result[28]=__sdot2_q8_8(-513,769,64,-127);
v0=-513;v1=769;v2=64;v3=-127;
result[29]=__sdot2_q8_8(v0,v1,v2,v3);

呼び出し部分のコード例です。完全なプログラムに組み込む際は、記載した初期化とバッファの条件を満たしてください。

期待する結果

固定値とRAM変数で同じ計算を行う例です。画面には代表値を符号なし16ビットで表示し、負数は2の補数の値になります。GBは固定小数点の縮小を行い、FCは整数積を返すため、同じ入力でも両機種の結果は異なります。

この抜粋で保存する16ビット値は result[26]=64959, result[27]=64959, result[28]=577, result[29]=577 です。固定値とRAM変数を使った計算は、それぞれ同じ値になります。画面は代表値を表示し、全結果はresult配列に保存します。

__sdot2_q8_8: NROM
NROM

使用例の完全なプログラム

ビルドコマンド

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\batch3_math.c -I .\kitaqfc\lib -I .\kitaq-docs\samples -o .\out\batch3_math.nes --no-cache --no-disasm --mapper=nrom --nes-chr=.\kitaq-docs\samples\font.chr
実装とソース

kitaqfc/lib/intrinsics.h:67

宣言の注記(原文)
Sum two signed-pattern products modulo 65536; no Q8.8 scaling shift is generated.

kitaqfc/kitaqfc/CodeGenerator.cs:4656

csharp
case "__sdot2_q8_8": return EmitKitaqfcHelperCall(funcName, args, ctx, new[] {2,2,1,1}, funcName);

void EmitHelperMath()
        {
            // Consume eight multiplier bits and accumulate the low product word in shared temporaries, returning A/X.
            EmitHelperStart("__mul16x8");
            EmitAsm("LDA", Imm(0));
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp1Address));
            EmitAsm("LDX", Imm(8));
            string loop = NewGeneratedLabel("mul16x8_loop");
            string skip = NewGeneratedLabel("mul16x8_skip_add");
            _assembly.Add(Expr.Make(Tag.Label, loop));
            EmitAsm("LSR", Mem(CallArgBase + 2));
            EmitAsm("BCC", Rel(skip));
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("CLC");
            EmitAsm("ADC", Mem(CallArgBase));
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp1Address));
            EmitAsm("ADC", Mem(CallArgBase + 1));
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp1Address));
            _assembly.Add(Expr.Make(Tag.Label, skip));
            EmitAsm("ASL", Mem(CallArgBase));
            EmitAsm("ROL", Mem(CallArgBase + 1));
            EmitAsm("DEX");
            EmitAsm("BNE", Rel(loop));
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("LDX", Mem(_runtimeIntrinsicTmp1Address));
            EmitAsm("RTS");

            // Zero-extend the byte multiplicand, reuse word multiplication and return its high byte.
            EmitHelperStart("__mul8x8_hi");
            EmitAsm("LDA", Mem(CallArgBase + 1));
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp2Address));
            EmitAsm("LDA", Imm(0));
            EmitAsm("STA", Mem(CallArgBase + 1));
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp2Address));
            EmitAsm("STA", Mem(CallArgBase + 2));
            EmitAsm("JSR", Abs("__mul16x8"));
            EmitAsm("TXA");
            EmitAsm("RTS");

            // Convert signed operands to magnitudes, multiply, then restore the product sign modulo 65536.
            EmitHelperStart("__smul16x8");
            EmitAsm("LDA", Imm(0));
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp2Address));
            string aPos = NewGeneratedLabel("smul_a_positive");
            EmitAsm("LDA", Mem(CallArgBase + 1));
            EmitAsm("AND", Imm(0x80));
            EmitAsm("BEQ", Rel(aPos));
            EmitAsm("LDA", Mem(CallArgBase)); EmitAsm("EOR", Imm(0xFF)); EmitAsm("CLC"); EmitAsm("ADC", Imm(1)); EmitAsm("STA", Mem(CallArgBase));
            EmitAsm("LDA", Mem(CallArgBase + 1)); EmitAsm("EOR", Imm(0xFF)); EmitAsm("ADC", Imm(0)); EmitAsm("STA", Mem(CallArgBase + 1));
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp2Address)); EmitAsm("EOR", Imm(1)); EmitAsm("STA", Mem(_runtimeIntrinsicTmp2Address));
            _assembly.Add(Expr.Make(Tag.Label, aPos));
            string bPos = NewGeneratedLabel("smul_b_positive");
            EmitAsm("LDA", Mem(CallArgBase + 2));
            EmitAsm("AND", Imm(0x80));
            EmitAsm("BEQ", Rel(bPos));
            EmitAsm("LDA", Mem(CallArgBase + 2)); EmitAsm("EOR", Imm(0xFF)); EmitAsm("CLC"); EmitAsm("ADC", Imm(1)); EmitAsm("STA", Mem(CallArgBase + 2));
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp2Address)); EmitAsm("EOR", Imm(1)); EmitAsm("STA", Mem(_runtimeIntrinsicTmp2Address));
            _assembly.Add(Expr.Make(Tag.Label, bPos));
            EmitAsm("JSR", Abs("__mul16x8"));
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("STX", Mem(_runtimeIntrinsicTmp1Address));
            string sdone = NewGeneratedLabel("smul_done");
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp2Address));
            EmitAsm("BEQ", Rel(sdone));
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp0Address)); EmitAsm("EOR", Imm(0xFF)); EmitAsm("CLC"); EmitAsm("ADC", Imm(1)); EmitAsm("STA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp1Address)); EmitAsm("EOR", Imm(0xFF)); EmitAsm("ADC", Imm(0)); EmitAsm("STA", Mem(_runtimeIntrinsicTmp1Address));
            _assembly.Add(Expr.Make(Tag.Label, sdone));
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("LDX", Mem(_runtimeIntrinsicTmp1Address));
            EmitAsm("RTS");

            EmitMacHelper("__mac16", "__mul16x8");
            EmitMacHelper("__smac16", "__smul16x8");
            EmitDot2Helper("__dot2_q8_8", "__mul16x8");
            EmitDot3Helper("__dot3_q8_8", "__mul16x8");
            EmitDot2Helper("__sdot2_q8_8", "__smul16x8");
            EmitDot3Helper("__sdot3_q8_8", "__smul16x8");
        }
__sdot3_q1_7

符号付き3項の内積を計算します。

書式

c
u16 __sdot3_q1_7(u16 a0, u16 a1, u16 a2, u8 b0, u8 b1, u8 b2);

引数

a0

符号付き16ビットのビット列。数値は−32768~32767。

a1

符号付き16ビットのビット列。数値は−32768~32767。

a2

符号付き16ビットのビット列。数値は−32768~32767。

b0

符号付き8ビットのビット列。係数は−128~127。

b1

符号付き8ビットのビット列。係数は−128~127。

b2

符号付き8ビットのビット列。係数は−128~127。

戻り値

a0*b0+a1*b1+a2*b2の下位16ビットです。

使用条件と制限

FCでは名前にq8_8/q1_7があっても生の整数積を使い、固定小数点の縮小シフトは行いません。Q1.7の3関数は対応する符号付き関数への別名です。単位変換は呼び出し側で行います。符号付き結果もu16のビット列として返します。

使用例

c
// Compare literal coefficients with values read from RAM.
result[38]=__sdot3_q1_7(-513,769,-1025,-64,127,-3);
v0=-513;v1=769;v2=-1025;v3=-64;v4=127;v5=-3;
result[39]=__sdot3_q1_7(v0,v1,v2,v3,v4,v5);
// Reverse coefficient signs and verify negative products.
result[40]=__sdot3_q1_7(-513,769,-1025,64,-127,3);
v0=-513;v1=769;v2=-1025;v3=64;v4=-127;v5=3;
result[41]=__sdot3_q1_7(v0,v1,v2,v3,v4,v5);

呼び出し部分のコード例です。完全なプログラムに組み込む際は、記載した初期化とバッファの条件を満たしてください。

期待する結果

固定値とRAM変数で同じ計算を行う例です。画面には代表値を符号なし16ビットで表示し、負数は2の補数の値になります。GBは固定小数点の縮小を行い、FCは整数積を返すため、同じ入力でも両機種の結果は異なります。

この抜粋で保存する16ビット値は result[38]=2498, result[39]=2498, result[40]=63038, result[41]=63038 です。固定値とRAM変数を使った計算は、それぞれ同じ値になります。画面は代表値を表示し、全結果はresult配列に保存します。

__sdot3_q1_7: NROM
NROM

使用例の完全なプログラム

ビルドコマンド

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\batch3_math.c -I .\kitaqfc\lib -I .\kitaq-docs\samples -o .\out\batch3_math.nes --no-cache --no-disasm --mapper=nrom --nes-chr=.\kitaq-docs\samples\font.chr
実装とソース

kitaqfc/lib/intrinsics.h:73

宣言の注記(原文)
FC alias of __sdot3_q8_8: three raw signed-pattern products, summed without rescaling.

kitaqfc/kitaqfc/CodeGenerator.cs:4659

csharp
case "__sdot3_q1_7": return EmitKitaqfcHelperCall(funcName, args, ctx, new[] {2,2,2,1,1,1}, funcName);
                // Pass a word seed to the RNG helper; the zero-argument generator is called directly below.

void EmitHelperMath()
        {
            // Consume eight multiplier bits and accumulate the low product word in shared temporaries, returning A/X.
            EmitHelperStart("__mul16x8");
            EmitAsm("LDA", Imm(0));
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp1Address));
            EmitAsm("LDX", Imm(8));
            string loop = NewGeneratedLabel("mul16x8_loop");
            string skip = NewGeneratedLabel("mul16x8_skip_add");
            _assembly.Add(Expr.Make(Tag.Label, loop));
            EmitAsm("LSR", Mem(CallArgBase + 2));
            EmitAsm("BCC", Rel(skip));
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("CLC");
            EmitAsm("ADC", Mem(CallArgBase));
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp1Address));
            EmitAsm("ADC", Mem(CallArgBase + 1));
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp1Address));
            _assembly.Add(Expr.Make(Tag.Label, skip));
            EmitAsm("ASL", Mem(CallArgBase));
            EmitAsm("ROL", Mem(CallArgBase + 1));
            EmitAsm("DEX");
            EmitAsm("BNE", Rel(loop));
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("LDX", Mem(_runtimeIntrinsicTmp1Address));
            EmitAsm("RTS");

            // Zero-extend the byte multiplicand, reuse word multiplication and return its high byte.
            EmitHelperStart("__mul8x8_hi");
            EmitAsm("LDA", Mem(CallArgBase + 1));
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp2Address));
            EmitAsm("LDA", Imm(0));
            EmitAsm("STA", Mem(CallArgBase + 1));
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp2Address));
            EmitAsm("STA", Mem(CallArgBase + 2));
            EmitAsm("JSR", Abs("__mul16x8"));
            EmitAsm("TXA");
            EmitAsm("RTS");

            // Convert signed operands to magnitudes, multiply, then restore the product sign modulo 65536.
            EmitHelperStart("__smul16x8");
            EmitAsm("LDA", Imm(0));
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp2Address));
            string aPos = NewGeneratedLabel("smul_a_positive");
            EmitAsm("LDA", Mem(CallArgBase + 1));
            EmitAsm("AND", Imm(0x80));
            EmitAsm("BEQ", Rel(aPos));
            EmitAsm("LDA", Mem(CallArgBase)); EmitAsm("EOR", Imm(0xFF)); EmitAsm("CLC"); EmitAsm("ADC", Imm(1)); EmitAsm("STA", Mem(CallArgBase));
            EmitAsm("LDA", Mem(CallArgBase + 1)); EmitAsm("EOR", Imm(0xFF)); EmitAsm("ADC", Imm(0)); EmitAsm("STA", Mem(CallArgBase + 1));
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp2Address)); EmitAsm("EOR", Imm(1)); EmitAsm("STA", Mem(_runtimeIntrinsicTmp2Address));
            _assembly.Add(Expr.Make(Tag.Label, aPos));
            string bPos = NewGeneratedLabel("smul_b_positive");
            EmitAsm("LDA", Mem(CallArgBase + 2));
            EmitAsm("AND", Imm(0x80));
            EmitAsm("BEQ", Rel(bPos));
            EmitAsm("LDA", Mem(CallArgBase + 2)); EmitAsm("EOR", Imm(0xFF)); EmitAsm("CLC"); EmitAsm("ADC", Imm(1)); EmitAsm("STA", Mem(CallArgBase + 2));
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp2Address)); EmitAsm("EOR", Imm(1)); EmitAsm("STA", Mem(_runtimeIntrinsicTmp2Address));
            _assembly.Add(Expr.Make(Tag.Label, bPos));
            EmitAsm("JSR", Abs("__mul16x8"));
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("STX", Mem(_runtimeIntrinsicTmp1Address));
            string sdone = NewGeneratedLabel("smul_done");
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp2Address));
            EmitAsm("BEQ", Rel(sdone));
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp0Address)); EmitAsm("EOR", Imm(0xFF)); EmitAsm("CLC"); EmitAsm("ADC", Imm(1)); EmitAsm("STA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp1Address)); EmitAsm("EOR", Imm(0xFF)); EmitAsm("ADC", Imm(0)); EmitAsm("STA", Mem(_runtimeIntrinsicTmp1Address));
            _assembly.Add(Expr.Make(Tag.Label, sdone));
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("LDX", Mem(_runtimeIntrinsicTmp1Address));
            EmitAsm("RTS");

            EmitMacHelper("__mac16", "__mul16x8");
            EmitMacHelper("__smac16", "__smul16x8");
            EmitDot2Helper("__dot2_q8_8", "__mul16x8");
            EmitDot3Helper("__dot3_q8_8", "__mul16x8");
            EmitDot2Helper("__sdot2_q8_8", "__smul16x8");
            EmitDot3Helper("__sdot3_q8_8", "__smul16x8");
        }
__sdot3_q8_8

符号付き3項の内積を計算します。

書式

c
u16 __sdot3_q8_8(u16 a0, u16 a1, u16 a2, u8 b0, u8 b1, u8 b2);

引数

a0

符号付き16ビットのビット列。数値は−32768~32767。

a1

符号付き16ビットのビット列。数値は−32768~32767。

a2

符号付き16ビットのビット列。数値は−32768~32767。

b0

符号付き8ビットのビット列。係数は−128~127。

b1

符号付き8ビットのビット列。係数は−128~127。

b2

符号付き8ビットのビット列。係数は−128~127。

戻り値

a0*b0+a1*b1+a2*b2の下位16ビットです。

使用条件と制限

FCでは名前にq8_8/q1_7があっても生の整数積を使い、固定小数点の縮小シフトは行いません。Q1.7の3関数は対応する符号付き関数への別名です。単位変換は呼び出し側で行います。符号付き結果もu16のビット列として返します。

使用例

c
// Compare literal coefficients with values read from RAM.
result[30]=__sdot3_q8_8(-513,769,-1025,-64,127,-3);
v0=-513;v1=769;v2=-1025;v3=-64;v4=127;v5=-3;
result[31]=__sdot3_q8_8(v0,v1,v2,v3,v4,v5);
// Reverse coefficient signs and verify negative products.
result[32]=__sdot3_q8_8(-513,769,-1025,64,-127,3);
v0=-513;v1=769;v2=-1025;v3=64;v4=-127;v5=3;
result[33]=__sdot3_q8_8(v0,v1,v2,v3,v4,v5);

呼び出し部分のコード例です。完全なプログラムに組み込む際は、記載した初期化とバッファの条件を満たしてください。

期待する結果

固定値とRAM変数で同じ計算を行う例です。画面には代表値を符号なし16ビットで表示し、負数は2の補数の値になります。GBは固定小数点の縮小を行い、FCは整数積を返すため、同じ入力でも両機種の結果は異なります。

この抜粋で保存する16ビット値は result[30]=2498, result[31]=2498, result[32]=63038, result[33]=63038 です。固定値とRAM変数を使った計算は、それぞれ同じ値になります。画面は代表値を表示し、全結果はresult配列に保存します。

__sdot3_q8_8: NROM
NROM

使用例の完全なプログラム

ビルドコマンド

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\batch3_math.c -I .\kitaqfc\lib -I .\kitaq-docs\samples -o .\out\batch3_math.nes --no-cache --no-disasm --mapper=nrom --nes-chr=.\kitaq-docs\samples\font.chr
実装とソース

kitaqfc/lib/intrinsics.h:69

宣言の注記(原文)
Sum three signed-pattern products modulo 65536; choose operand units explicitly in the caller.

kitaqfc/kitaqfc/CodeGenerator.cs:4657

csharp
case "__sdot3_q8_8": return EmitKitaqfcHelperCall(funcName, args, ctx, new[] {2,2,2,1,1,1}, funcName);

void EmitHelperMath()
        {
            // Consume eight multiplier bits and accumulate the low product word in shared temporaries, returning A/X.
            EmitHelperStart("__mul16x8");
            EmitAsm("LDA", Imm(0));
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp1Address));
            EmitAsm("LDX", Imm(8));
            string loop = NewGeneratedLabel("mul16x8_loop");
            string skip = NewGeneratedLabel("mul16x8_skip_add");
            _assembly.Add(Expr.Make(Tag.Label, loop));
            EmitAsm("LSR", Mem(CallArgBase + 2));
            EmitAsm("BCC", Rel(skip));
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("CLC");
            EmitAsm("ADC", Mem(CallArgBase));
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp1Address));
            EmitAsm("ADC", Mem(CallArgBase + 1));
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp1Address));
            _assembly.Add(Expr.Make(Tag.Label, skip));
            EmitAsm("ASL", Mem(CallArgBase));
            EmitAsm("ROL", Mem(CallArgBase + 1));
            EmitAsm("DEX");
            EmitAsm("BNE", Rel(loop));
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("LDX", Mem(_runtimeIntrinsicTmp1Address));
            EmitAsm("RTS");

            // Zero-extend the byte multiplicand, reuse word multiplication and return its high byte.
            EmitHelperStart("__mul8x8_hi");
            EmitAsm("LDA", Mem(CallArgBase + 1));
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp2Address));
            EmitAsm("LDA", Imm(0));
            EmitAsm("STA", Mem(CallArgBase + 1));
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp2Address));
            EmitAsm("STA", Mem(CallArgBase + 2));
            EmitAsm("JSR", Abs("__mul16x8"));
            EmitAsm("TXA");
            EmitAsm("RTS");

            // Convert signed operands to magnitudes, multiply, then restore the product sign modulo 65536.
            EmitHelperStart("__smul16x8");
            EmitAsm("LDA", Imm(0));
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp2Address));
            string aPos = NewGeneratedLabel("smul_a_positive");
            EmitAsm("LDA", Mem(CallArgBase + 1));
            EmitAsm("AND", Imm(0x80));
            EmitAsm("BEQ", Rel(aPos));
            EmitAsm("LDA", Mem(CallArgBase)); EmitAsm("EOR", Imm(0xFF)); EmitAsm("CLC"); EmitAsm("ADC", Imm(1)); EmitAsm("STA", Mem(CallArgBase));
            EmitAsm("LDA", Mem(CallArgBase + 1)); EmitAsm("EOR", Imm(0xFF)); EmitAsm("ADC", Imm(0)); EmitAsm("STA", Mem(CallArgBase + 1));
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp2Address)); EmitAsm("EOR", Imm(1)); EmitAsm("STA", Mem(_runtimeIntrinsicTmp2Address));
            _assembly.Add(Expr.Make(Tag.Label, aPos));
            string bPos = NewGeneratedLabel("smul_b_positive");
            EmitAsm("LDA", Mem(CallArgBase + 2));
            EmitAsm("AND", Imm(0x80));
            EmitAsm("BEQ", Rel(bPos));
            EmitAsm("LDA", Mem(CallArgBase + 2)); EmitAsm("EOR", Imm(0xFF)); EmitAsm("CLC"); EmitAsm("ADC", Imm(1)); EmitAsm("STA", Mem(CallArgBase + 2));
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp2Address)); EmitAsm("EOR", Imm(1)); EmitAsm("STA", Mem(_runtimeIntrinsicTmp2Address));
            _assembly.Add(Expr.Make(Tag.Label, bPos));
            EmitAsm("JSR", Abs("__mul16x8"));
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("STX", Mem(_runtimeIntrinsicTmp1Address));
            string sdone = NewGeneratedLabel("smul_done");
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp2Address));
            EmitAsm("BEQ", Rel(sdone));
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp0Address)); EmitAsm("EOR", Imm(0xFF)); EmitAsm("CLC"); EmitAsm("ADC", Imm(1)); EmitAsm("STA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp1Address)); EmitAsm("EOR", Imm(0xFF)); EmitAsm("ADC", Imm(0)); EmitAsm("STA", Mem(_runtimeIntrinsicTmp1Address));
            _assembly.Add(Expr.Make(Tag.Label, sdone));
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("LDX", Mem(_runtimeIntrinsicTmp1Address));
            EmitAsm("RTS");

            EmitMacHelper("__mac16", "__mul16x8");
            EmitMacHelper("__smac16", "__smul16x8");
            EmitDot2Helper("__dot2_q8_8", "__mul16x8");
            EmitDot3Helper("__dot3_q8_8", "__mul16x8");
            EmitDot2Helper("__sdot2_q8_8", "__smul16x8");
            EmitDot3Helper("__sdot3_q8_8", "__smul16x8");
        }
__serial_rx_bit

外部アダプターの受信線として使う$4017のビット4を、その場で一度読み取ります。

書式

c
u8 __serial_rx_bit(void);

戻り値

ビット4が1なら1、それ以外は0。データ到着の有無や通信エラーを表す状態コードではありません。

使用条件と制限

これらは$4016/$4017を直接操作します。通常のパッド、キーボード、シリアル送信など、同じポートを使う処理を割り込みで混在させないでください。パッドのシリアル読み取り位置にも影響します。サンプルは入力の採取中だけNMIを止め、表示を初期化してから再開します。

外部アダプターのOUT0→TX、$4017のD4→RXという配線規約を前提にした低水準処理です。ビットの待ち時間、ボーレート、開始・停止ビット、バイト化、受信待ちを自動では追加しません。__midi_out()などのバイト単位処理とは役割が異なります。

相手の送信タイミングに合わせて呼ぶ責任は呼び出し側にあります。受信待ちや複数ビットの復元を行わず、$4016への出力も変更しません。

ビット変換は、独自の検証ROMでレジスター読み取り先だけをRAMの模擬値へ置き換え、00/08/10/18の組み合わせと無関係な上位ビットを入力して確認しました。この単体検証は、配布サンプルの通常実行および周辺機器の実動作とは別です。

掲載画像は配布サンプルを変更せずにKUROSAKIで実行した結果です。現在のKUROSAKIはパッドの入力モデルを持ちますが、キーボード・光線銃・シリアルアダプターの信号はモデル化していません。外部入力ビットが0になるため、画像は周辺機器の動作確認ではありません。実機のキー入力、受光、通信は未検証です。

使用例

c
#include "fc_ports_scene.h"
__prg_rom u8 tx_values[6]={0,1,2,3,254,255};
void main(void){
    u8 i;
    __irq_disable();__ppu_ctrl_set(0);port_phase=1;
    for(i=0;i<6;i++)__serial_tx_bit(tx_values[i]);
    __serial_tx_bit(1); // Leave OUT0 high; no byte framing or baud clock is added.
    port_result[0]=__serial_rx_bit();port_phase=2;port_result[31]=165;
    m_init();m_text(2,2,"SERIAL BIT PRIMITIVES");m_wait();
    m_text(2,5,"VALUE       EXPECTED OUT0");m_wait();
    for(i=0;i<6;i++){port_number(3,7+i*2,tx_values[i]);m_put(23,7+i*2,'0'+(tx_values[i]&1));m_wait();}
    m_text(2,20,"RX SAMPLE");port_number(23,20,port_result[0]);m_wait();
    m_text(2,23,"NO BYTE FRAMING OR CLOCK");m_wait();
    while(1){m_wait();}
}

呼び出し部分のコード例です。完全なプログラムに組み込む際は、記載した初期化とバッファの条件を満たしてください。

期待する結果

VALUEの000/001/002/003/254/255に対し、EXPECTED OUT0は0/1/0/1/0/1です。これは下位1ビットの意味を示す説明欄で、端子を読み返した値ではありません。CPUトレースでもこの順の書き込みと最後の1を確認しました。RX SAMPLE=000はKUROSAKIで受信線をモデル化していないためです。

__serial_rx_bit: SERIAL-DEFAULT — VALUEの000/001/002/003/254/255に対し、EXPECTED OUT0は0/1/0/1/0/1です。これは下位1ビットの意味を示す説明欄で、端子を読み返した値ではありません。CPUトレースでもこの順の書き込みと最後の1を確認しました。RX SAMPLE=000はKUROSAKIで受信線をモデル化していないためです。
SERIAL-DEFAULT — VALUEの000/001/002/003/254/255に対し、EXPECTED OUT0は0/1/0/1/0/1です。これは下位1ビットの意味を示す説明欄で、端子を読み返した値ではありません。CPUトレースでもこの順の書き込みと最後の1を確認しました。RX SAMPLE=000はKUROSAKIで受信線をモデル化していないためです。

使用例の完全なプログラム

ビルドコマンド

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\peripheral_serial.c -I .\kitaqfc\lib -I .\kitaq-docs\samples -o .\out\peripheral_serial.nes --mapper=nrom --nes-chr=.\kitaq-docs\samples\font.chr --no-cache --no-disasm
.\kurosaki\kurosaki.exe run .\out\peripheral_serial.nes --frames 120 --png .\out\peripheral_serial.png
実装とソース

kitaqfc/lib/intrinsics.h:288

宣言の注記(原文)
Read 4017 bit 4 and normalize it to 0 or 1; an external serial adapter supplies the signal.

kitaqfc/kitaqfc/CodeGenerator.cs:7965

csharp
EmitHelperStart("__serial_rx_bit");
            EmitAsm("LDA", Mem(0x4017));
            EmitAsm("AND", Imm(0x10));
            string rx0 = NewGeneratedLabel("serial_rx0");
            string rxd = NewGeneratedLabel("serial_rxdone");
            EmitAsm("BEQ", Rel(rx0));
            EmitAsm("LDA", Imm(1));
            EmitAsm("JMP", Abs(rxd));
            _assembly.Add(Expr.Make(Tag.Label, rx0));
            EmitAsm("LDA", Imm(0));
            _assembly.Add(Expr.Make(Tag.Label, rxd));
            EmitAsm("RTS");
__serial_tx_bit

引数の下位1ビットを$4016のOUT0へ書きます。外部シリアルアダプター用の1ビット出力操作です。

書式

c
void __serial_tx_bit(u8 bit);

引数

bit

0/1として送りたい値。使用するのはbit & 1だけなので、2は0、255は1を送ります。

戻り値

戻り値はありません。

使用条件と制限

これらは$4016/$4017を直接操作します。通常のパッド、キーボード、シリアル送信など、同じポートを使う処理を割り込みで混在させないでください。パッドのシリアル読み取り位置にも影響します。サンプルは入力の採取中だけNMIを止め、表示を初期化してから再開します。

外部アダプターのOUT0→TX、$4017のD4→RXという配線規約を前提にした低水準処理です。ビットの待ち時間、ボーレート、開始・停止ビット、バイト化、受信待ちを自動では追加しません。__midi_out()などのバイト単位処理とは役割が異なります。

$4016へ0または1を丸ごと書くため、OUT1/OUT2は0になります。送ったビットを保持するための待機処理はありません。通常のパッドではOUT0はラッチ制御なので、この操作はパッドの読み出し状態も変更します。

掲載画像は配布サンプルを変更せずにKUROSAKIで実行した結果です。現在のKUROSAKIはパッドの入力モデルを持ちますが、キーボード・光線銃・シリアルアダプターの信号はモデル化していません。外部入力ビットが0になるため、画像は周辺機器の動作確認ではありません。実機のキー入力、受光、通信は未検証です。

使用例

c
#include "fc_ports_scene.h"
__prg_rom u8 tx_values[6]={0,1,2,3,254,255};
void main(void){
    u8 i;
    __irq_disable();__ppu_ctrl_set(0);port_phase=1;
    for(i=0;i<6;i++)__serial_tx_bit(tx_values[i]);
    __serial_tx_bit(1); // Leave OUT0 high; no byte framing or baud clock is added.
    port_result[0]=__serial_rx_bit();port_phase=2;port_result[31]=165;
    m_init();m_text(2,2,"SERIAL BIT PRIMITIVES");m_wait();
    m_text(2,5,"VALUE       EXPECTED OUT0");m_wait();
    for(i=0;i<6;i++){port_number(3,7+i*2,tx_values[i]);m_put(23,7+i*2,'0'+(tx_values[i]&1));m_wait();}
    m_text(2,20,"RX SAMPLE");port_number(23,20,port_result[0]);m_wait();
    m_text(2,23,"NO BYTE FRAMING OR CLOCK");m_wait();
    while(1){m_wait();}
}

呼び出し部分のコード例です。完全なプログラムに組み込む際は、記載した初期化とバッファの条件を満たしてください。

期待する結果

VALUEの000/001/002/003/254/255に対し、EXPECTED OUT0は0/1/0/1/0/1です。これは下位1ビットの意味を示す説明欄で、端子を読み返した値ではありません。CPUトレースでもこの順の書き込みと最後の1を確認しました。RX SAMPLE=000はKUROSAKIで受信線をモデル化していないためです。

__serial_tx_bit: SERIAL-DEFAULT — VALUEの000/001/002/003/254/255に対し、EXPECTED OUT0は0/1/0/1/0/1です。これは下位1ビットの意味を示す説明欄で、端子を読み返した値ではありません。CPUトレースでもこの順の書き込みと最後の1を確認しました。RX SAMPLE=000はKUROSAKIで受信線をモデル化していないためです。
SERIAL-DEFAULT — VALUEの000/001/002/003/254/255に対し、EXPECTED OUT0は0/1/0/1/0/1です。これは下位1ビットの意味を示す説明欄で、端子を読み返した値ではありません。CPUトレースでもこの順の書き込みと最後の1を確認しました。RX SAMPLE=000はKUROSAKIで受信線をモデル化していないためです。

使用例の完全なプログラム

ビルドコマンド

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\peripheral_serial.c -I .\kitaqfc\lib -I .\kitaq-docs\samples -o .\out\peripheral_serial.nes --mapper=nrom --nes-chr=.\kitaq-docs\samples\font.chr --no-cache --no-disasm
.\kurosaki\kurosaki.exe run .\out\peripheral_serial.nes --frames 120 --png .\out\peripheral_serial.png
実装とソース

kitaqfc/lib/intrinsics.h:286

宣言の注記(原文)
Expansion-port TTL serial / MIDI adapter support.
The low-level serial layer assumes an external adapter that maps OUT0 to TX
and D4 on $4017 to RX. MIDI helpers bit-bang 8N1 bytes at an approximate
31250 bps timing suitable for adapter bring-up and simple music sync.

Write bit & 1 to 4016, replacing the port output value; coordinate with other port users.

kitaqfc/kitaqfc/CodeGenerator.cs:7958

csharp
EmitHelperStart("__serial_tx_bit");
            EmitAsm("LDA", Mem(CallArgBase));
            EmitAsm("AND", Imm(1));
            EmitAsm("STA", Mem(0x4016));
            EmitAsm("RTS");
__smac16

符号付き積和を計算します。

書式

c
u16 __smac16(u16 acc, u16 a, u8 b);

引数

acc

加算する16ビットの累積値。

a

符号付き16ビットのビット列。数値は−32768~32767。

b

符号付き8ビットのビット列。係数は−128~127。

戻り値

acc+a*bの下位16ビットです。

使用条件と制限

FCでは名前にq8_8/q1_7があっても生の整数積を使い、固定小数点の縮小シフトは行いません。Q1.7の3関数は対応する符号付き関数への別名です。単位変換は呼び出し側で行います。符号付き結果もu16のビット列として返します。

使用例

c
// Compare literal coefficients with values read from RAM.
result[10]=__smac16(65500,-513,-64);
v0=65500;v1=-513;v2=-64;
result[11]=__smac16(v0,v1,v2);
// Reverse coefficient signs and verify negative products.
result[12]=__smac16(65500,-513,64);
v0=65500;v1=-513;v2=64;
result[13]=__smac16(v0,v1,v2);

呼び出し部分のコード例です。完全なプログラムに組み込む際は、記載した初期化とバッファの条件を満たしてください。

期待する結果

固定値とRAM変数で同じ計算を行う例です。画面には代表値を符号なし16ビットで表示し、負数は2の補数の値になります。GBは固定小数点の縮小を行い、FCは整数積を返すため、同じ入力でも両機種の結果は異なります。

この抜粋で保存する16ビット値は result[10]=32796, result[11]=32796, result[12]=32668, result[13]=32668 です。固定値とRAM変数を使った計算は、それぞれ同じ値になります。画面は代表値を表示し、全結果はresult配列に保存します。

__smac16: NROM
NROM

使用例の完全なプログラム

ビルドコマンド

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\batch3_math.c -I .\kitaqfc\lib -I .\kitaq-docs\samples -o .\out\batch3_math.nes --no-cache --no-disasm --mapper=nrom --nes-chr=.\kitaq-docs\samples\font.chr
実装とソース

kitaqfc/lib/intrinsics.h:57

宣言の注記(原文)
Add the signed-pattern product to acc modulo 65536; the API carries the result as u16 bits.

kitaqfc/kitaqfc/CodeGenerator.cs:4651

csharp
case "__smac16": return EmitKitaqfcHelperCall(funcName, args, ctx, new[] {2,2,1}, funcName);

void EmitHelperMath()
        {
            // Consume eight multiplier bits and accumulate the low product word in shared temporaries, returning A/X.
            EmitHelperStart("__mul16x8");
            EmitAsm("LDA", Imm(0));
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp1Address));
            EmitAsm("LDX", Imm(8));
            string loop = NewGeneratedLabel("mul16x8_loop");
            string skip = NewGeneratedLabel("mul16x8_skip_add");
            _assembly.Add(Expr.Make(Tag.Label, loop));
            EmitAsm("LSR", Mem(CallArgBase + 2));
            EmitAsm("BCC", Rel(skip));
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("CLC");
            EmitAsm("ADC", Mem(CallArgBase));
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp1Address));
            EmitAsm("ADC", Mem(CallArgBase + 1));
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp1Address));
            _assembly.Add(Expr.Make(Tag.Label, skip));
            EmitAsm("ASL", Mem(CallArgBase));
            EmitAsm("ROL", Mem(CallArgBase + 1));
            EmitAsm("DEX");
            EmitAsm("BNE", Rel(loop));
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("LDX", Mem(_runtimeIntrinsicTmp1Address));
            EmitAsm("RTS");

            // Zero-extend the byte multiplicand, reuse word multiplication and return its high byte.
            EmitHelperStart("__mul8x8_hi");
            EmitAsm("LDA", Mem(CallArgBase + 1));
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp2Address));
            EmitAsm("LDA", Imm(0));
            EmitAsm("STA", Mem(CallArgBase + 1));
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp2Address));
            EmitAsm("STA", Mem(CallArgBase + 2));
            EmitAsm("JSR", Abs("__mul16x8"));
            EmitAsm("TXA");
            EmitAsm("RTS");

            // Convert signed operands to magnitudes, multiply, then restore the product sign modulo 65536.
            EmitHelperStart("__smul16x8");
            EmitAsm("LDA", Imm(0));
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp2Address));
            string aPos = NewGeneratedLabel("smul_a_positive");
            EmitAsm("LDA", Mem(CallArgBase + 1));
            EmitAsm("AND", Imm(0x80));
            EmitAsm("BEQ", Rel(aPos));
            EmitAsm("LDA", Mem(CallArgBase)); EmitAsm("EOR", Imm(0xFF)); EmitAsm("CLC"); EmitAsm("ADC", Imm(1)); EmitAsm("STA", Mem(CallArgBase));
            EmitAsm("LDA", Mem(CallArgBase + 1)); EmitAsm("EOR", Imm(0xFF)); EmitAsm("ADC", Imm(0)); EmitAsm("STA", Mem(CallArgBase + 1));
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp2Address)); EmitAsm("EOR", Imm(1)); EmitAsm("STA", Mem(_runtimeIntrinsicTmp2Address));
            _assembly.Add(Expr.Make(Tag.Label, aPos));
            string bPos = NewGeneratedLabel("smul_b_positive");
            EmitAsm("LDA", Mem(CallArgBase + 2));
            EmitAsm("AND", Imm(0x80));
            EmitAsm("BEQ", Rel(bPos));
            EmitAsm("LDA", Mem(CallArgBase + 2)); EmitAsm("EOR", Imm(0xFF)); EmitAsm("CLC"); EmitAsm("ADC", Imm(1)); EmitAsm("STA", Mem(CallArgBase + 2));
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp2Address)); EmitAsm("EOR", Imm(1)); EmitAsm("STA", Mem(_runtimeIntrinsicTmp2Address));
            _assembly.Add(Expr.Make(Tag.Label, bPos));
            EmitAsm("JSR", Abs("__mul16x8"));
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("STX", Mem(_runtimeIntrinsicTmp1Address));
            string sdone = NewGeneratedLabel("smul_done");
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp2Address));
            EmitAsm("BEQ", Rel(sdone));
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp0Address)); EmitAsm("EOR", Imm(0xFF)); EmitAsm("CLC"); EmitAsm("ADC", Imm(1)); EmitAsm("STA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp1Address)); EmitAsm("EOR", Imm(0xFF)); EmitAsm("ADC", Imm(0)); EmitAsm("STA", Mem(_runtimeIntrinsicTmp1Address));
            _assembly.Add(Expr.Make(Tag.Label, sdone));
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("LDX", Mem(_runtimeIntrinsicTmp1Address));
            EmitAsm("RTS");

            EmitMacHelper("__mac16", "__mul16x8");
            EmitMacHelper("__smac16", "__smul16x8");
            EmitDot2Helper("__dot2_q8_8", "__mul16x8");
            EmitDot3Helper("__dot3_q8_8", "__mul16x8");
            EmitDot2Helper("__sdot2_q8_8", "__smul16x8");
            EmitDot3Helper("__sdot3_q8_8", "__smul16x8");
        }
__smac16_q1_7

符号付き積和を計算します。

書式

c
u16 __smac16_q1_7(u16 acc, u16 a, u8 b);

引数

acc

加算する16ビットの累積値。

a

符号付き16ビットのビット列。数値は−32768~32767。

b

符号付き8ビットのビット列。係数は−128~127。

戻り値

acc+a*bの下位16ビットです。

使用条件と制限

FCでは名前にq8_8/q1_7があっても生の整数積を使い、固定小数点の縮小シフトは行いません。Q1.7の3関数は対応する符号付き関数への別名です。単位変換は呼び出し側で行います。符号付き結果もu16のビット列として返します。

使用例

c
// Compare literal coefficients with values read from RAM.
result[18]=__smac16_q1_7(65500,-513,-64);
v0=65500;v1=-513;v2=-64;
result[19]=__smac16_q1_7(v0,v1,v2);
// Reverse coefficient signs and verify negative products.
result[20]=__smac16_q1_7(65500,-513,64);
v0=65500;v1=-513;v2=64;
result[21]=__smac16_q1_7(v0,v1,v2);

呼び出し部分のコード例です。完全なプログラムに組み込む際は、記載した初期化とバッファの条件を満たしてください。

期待する結果

固定値とRAM変数で同じ計算を行う例です。画面には代表値を符号なし16ビットで表示し、負数は2の補数の値になります。GBは固定小数点の縮小を行い、FCは整数積を返すため、同じ入力でも両機種の結果は異なります。

この抜粋で保存する16ビット値は result[18]=32796, result[19]=32796, result[20]=32668, result[21]=32668 です。固定値とRAM変数を使った計算は、それぞれ同じ値になります。画面は代表値を表示し、全結果はresult配列に保存します。

__smac16_q1_7: NROM
NROM

使用例の完全なプログラム

ビルドコマンド

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\batch3_math.c -I .\kitaqfc\lib -I .\kitaq-docs\samples -o .\out\batch3_math.nes --no-cache --no-disasm --mapper=nrom --nes-chr=.\kitaq-docs\samples\font.chr
実装とソース

kitaqfc/lib/intrinsics.h:61

宣言の注記(原文)
FC alias of __smac16: accumulate a raw signed-pattern product modulo 65536.

kitaqfc/kitaqfc/CodeGenerator.cs:4653

csharp
case "__smac16_q1_7": return EmitKitaqfcHelperCall(funcName, args, ctx, new[] {2,2,1}, funcName);

void EmitHelperMath()
        {
            // Consume eight multiplier bits and accumulate the low product word in shared temporaries, returning A/X.
            EmitHelperStart("__mul16x8");
            EmitAsm("LDA", Imm(0));
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp1Address));
            EmitAsm("LDX", Imm(8));
            string loop = NewGeneratedLabel("mul16x8_loop");
            string skip = NewGeneratedLabel("mul16x8_skip_add");
            _assembly.Add(Expr.Make(Tag.Label, loop));
            EmitAsm("LSR", Mem(CallArgBase + 2));
            EmitAsm("BCC", Rel(skip));
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("CLC");
            EmitAsm("ADC", Mem(CallArgBase));
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp1Address));
            EmitAsm("ADC", Mem(CallArgBase + 1));
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp1Address));
            _assembly.Add(Expr.Make(Tag.Label, skip));
            EmitAsm("ASL", Mem(CallArgBase));
            EmitAsm("ROL", Mem(CallArgBase + 1));
            EmitAsm("DEX");
            EmitAsm("BNE", Rel(loop));
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("LDX", Mem(_runtimeIntrinsicTmp1Address));
            EmitAsm("RTS");

            // Zero-extend the byte multiplicand, reuse word multiplication and return its high byte.
            EmitHelperStart("__mul8x8_hi");
            EmitAsm("LDA", Mem(CallArgBase + 1));
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp2Address));
            EmitAsm("LDA", Imm(0));
            EmitAsm("STA", Mem(CallArgBase + 1));
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp2Address));
            EmitAsm("STA", Mem(CallArgBase + 2));
            EmitAsm("JSR", Abs("__mul16x8"));
            EmitAsm("TXA");
            EmitAsm("RTS");

            // Convert signed operands to magnitudes, multiply, then restore the product sign modulo 65536.
            EmitHelperStart("__smul16x8");
            EmitAsm("LDA", Imm(0));
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp2Address));
            string aPos = NewGeneratedLabel("smul_a_positive");
            EmitAsm("LDA", Mem(CallArgBase + 1));
            EmitAsm("AND", Imm(0x80));
            EmitAsm("BEQ", Rel(aPos));
            EmitAsm("LDA", Mem(CallArgBase)); EmitAsm("EOR", Imm(0xFF)); EmitAsm("CLC"); EmitAsm("ADC", Imm(1)); EmitAsm("STA", Mem(CallArgBase));
            EmitAsm("LDA", Mem(CallArgBase + 1)); EmitAsm("EOR", Imm(0xFF)); EmitAsm("ADC", Imm(0)); EmitAsm("STA", Mem(CallArgBase + 1));
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp2Address)); EmitAsm("EOR", Imm(1)); EmitAsm("STA", Mem(_runtimeIntrinsicTmp2Address));
            _assembly.Add(Expr.Make(Tag.Label, aPos));
            string bPos = NewGeneratedLabel("smul_b_positive");
            EmitAsm("LDA", Mem(CallArgBase + 2));
            EmitAsm("AND", Imm(0x80));
            EmitAsm("BEQ", Rel(bPos));
            EmitAsm("LDA", Mem(CallArgBase + 2)); EmitAsm("EOR", Imm(0xFF)); EmitAsm("CLC"); EmitAsm("ADC", Imm(1)); EmitAsm("STA", Mem(CallArgBase + 2));
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp2Address)); EmitAsm("EOR", Imm(1)); EmitAsm("STA", Mem(_runtimeIntrinsicTmp2Address));
            _assembly.Add(Expr.Make(Tag.Label, bPos));
            EmitAsm("JSR", Abs("__mul16x8"));
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("STX", Mem(_runtimeIntrinsicTmp1Address));
            string sdone = NewGeneratedLabel("smul_done");
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp2Address));
            EmitAsm("BEQ", Rel(sdone));
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp0Address)); EmitAsm("EOR", Imm(0xFF)); EmitAsm("CLC"); EmitAsm("ADC", Imm(1)); EmitAsm("STA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp1Address)); EmitAsm("EOR", Imm(0xFF)); EmitAsm("ADC", Imm(0)); EmitAsm("STA", Mem(_runtimeIntrinsicTmp1Address));
            _assembly.Add(Expr.Make(Tag.Label, sdone));
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("LDX", Mem(_runtimeIntrinsicTmp1Address));
            EmitAsm("RTS");

            EmitMacHelper("__mac16", "__mul16x8");
            EmitMacHelper("__smac16", "__smul16x8");
            EmitDot2Helper("__dot2_q8_8", "__mul16x8");
            EmitDot3Helper("__dot3_q8_8", "__mul16x8");
            EmitDot2Helper("__sdot2_q8_8", "__smul16x8");
            EmitDot3Helper("__sdot3_q8_8", "__smul16x8");
        }
__smul16x8

符号付き乗算を計算します。

書式

c
u16 __smul16x8(u16 a, u8 b);

引数

a

符号付き16ビットのビット列。数値は−32768~32767。

b

符号付き8ビットのビット列。係数は−128~127。

戻り値

a*bの下位16ビットです。

使用条件と制限

FCでは名前にq8_8/q1_7があっても生の整数積を使い、固定小数点の縮小シフトは行いません。Q1.7の3関数は対応する符号付き関数への別名です。単位変換は呼び出し側で行います。符号付き結果もu16のビット列として返します。

使用例

c
// Compare literal coefficients with values read from RAM.
result[4]=__smul16x8(-513,-64);
v0=-513;v1=-64;
result[5]=__smul16x8(v0,v1);
// Reverse coefficient signs and verify negative products.
result[6]=__smul16x8(-513,64);
v0=-513;v1=64;
result[7]=__smul16x8(v0,v1);

呼び出し部分のコード例です。完全なプログラムに組み込む際は、記載した初期化とバッファの条件を満たしてください。

期待する結果

固定値とRAM変数で同じ計算を行う例です。画面には代表値を符号なし16ビットで表示し、負数は2の補数の値になります。GBは固定小数点の縮小を行い、FCは整数積を返すため、同じ入力でも両機種の結果は異なります。

この抜粋で保存する16ビット値は result[4]=32832, result[5]=32832, result[6]=32704, result[7]=32704 です。固定値とRAM変数を使った計算は、それぞれ同じ値になります。画面は代表値を表示し、全結果はresult配列に保存します。

__smul16x8: NROM
NROM

使用例の完全なプログラム

ビルドコマンド

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\batch3_math.c -I .\kitaqfc\lib -I .\kitaq-docs\samples -o .\out\batch3_math.nes --no-cache --no-disasm --mapper=nrom --nes-chr=.\kitaq-docs\samples\font.chr
実装とソース

kitaqfc/lib/intrinsics.h:53

宣言の注記(原文)
Interpret a and b as signed two's-complement bit patterns and return the low word of their product.

kitaqfc/kitaqfc/CodeGenerator.cs:4648

csharp
case "__smul16x8": return EmitKitaqfcHelperCall(funcName, args, ctx, new[] {2,1}, funcName);

void EmitHelperMath()
        {
            // Consume eight multiplier bits and accumulate the low product word in shared temporaries, returning A/X.
            EmitHelperStart("__mul16x8");
            EmitAsm("LDA", Imm(0));
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp1Address));
            EmitAsm("LDX", Imm(8));
            string loop = NewGeneratedLabel("mul16x8_loop");
            string skip = NewGeneratedLabel("mul16x8_skip_add");
            _assembly.Add(Expr.Make(Tag.Label, loop));
            EmitAsm("LSR", Mem(CallArgBase + 2));
            EmitAsm("BCC", Rel(skip));
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("CLC");
            EmitAsm("ADC", Mem(CallArgBase));
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp1Address));
            EmitAsm("ADC", Mem(CallArgBase + 1));
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp1Address));
            _assembly.Add(Expr.Make(Tag.Label, skip));
            EmitAsm("ASL", Mem(CallArgBase));
            EmitAsm("ROL", Mem(CallArgBase + 1));
            EmitAsm("DEX");
            EmitAsm("BNE", Rel(loop));
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("LDX", Mem(_runtimeIntrinsicTmp1Address));
            EmitAsm("RTS");

            // Zero-extend the byte multiplicand, reuse word multiplication and return its high byte.
            EmitHelperStart("__mul8x8_hi");
            EmitAsm("LDA", Mem(CallArgBase + 1));
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp2Address));
            EmitAsm("LDA", Imm(0));
            EmitAsm("STA", Mem(CallArgBase + 1));
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp2Address));
            EmitAsm("STA", Mem(CallArgBase + 2));
            EmitAsm("JSR", Abs("__mul16x8"));
            EmitAsm("TXA");
            EmitAsm("RTS");

            // Convert signed operands to magnitudes, multiply, then restore the product sign modulo 65536.
            EmitHelperStart("__smul16x8");
            EmitAsm("LDA", Imm(0));
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp2Address));
            string aPos = NewGeneratedLabel("smul_a_positive");
            EmitAsm("LDA", Mem(CallArgBase + 1));
            EmitAsm("AND", Imm(0x80));
            EmitAsm("BEQ", Rel(aPos));
            EmitAsm("LDA", Mem(CallArgBase)); EmitAsm("EOR", Imm(0xFF)); EmitAsm("CLC"); EmitAsm("ADC", Imm(1)); EmitAsm("STA", Mem(CallArgBase));
            EmitAsm("LDA", Mem(CallArgBase + 1)); EmitAsm("EOR", Imm(0xFF)); EmitAsm("ADC", Imm(0)); EmitAsm("STA", Mem(CallArgBase + 1));
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp2Address)); EmitAsm("EOR", Imm(1)); EmitAsm("STA", Mem(_runtimeIntrinsicTmp2Address));
            _assembly.Add(Expr.Make(Tag.Label, aPos));
            string bPos = NewGeneratedLabel("smul_b_positive");
            EmitAsm("LDA", Mem(CallArgBase + 2));
            EmitAsm("AND", Imm(0x80));
            EmitAsm("BEQ", Rel(bPos));
            EmitAsm("LDA", Mem(CallArgBase + 2)); EmitAsm("EOR", Imm(0xFF)); EmitAsm("CLC"); EmitAsm("ADC", Imm(1)); EmitAsm("STA", Mem(CallArgBase + 2));
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp2Address)); EmitAsm("EOR", Imm(1)); EmitAsm("STA", Mem(_runtimeIntrinsicTmp2Address));
            _assembly.Add(Expr.Make(Tag.Label, bPos));
            EmitAsm("JSR", Abs("__mul16x8"));
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("STX", Mem(_runtimeIntrinsicTmp1Address));
            string sdone = NewGeneratedLabel("smul_done");
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp2Address));
            EmitAsm("BEQ", Rel(sdone));
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp0Address)); EmitAsm("EOR", Imm(0xFF)); EmitAsm("CLC"); EmitAsm("ADC", Imm(1)); EmitAsm("STA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp1Address)); EmitAsm("EOR", Imm(0xFF)); EmitAsm("ADC", Imm(0)); EmitAsm("STA", Mem(_runtimeIntrinsicTmp1Address));
            _assembly.Add(Expr.Make(Tag.Label, sdone));
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("LDX", Mem(_runtimeIntrinsicTmp1Address));
            EmitAsm("RTS");

            EmitMacHelper("__mac16", "__mul16x8");
            EmitMacHelper("__smac16", "__smul16x8");
            EmitDot2Helper("__dot2_q8_8", "__mul16x8");
            EmitDot3Helper("__dot3_q8_8", "__mul16x8");
            EmitDot2Helper("__sdot2_q8_8", "__smul16x8");
            EmitDot3Helper("__sdot3_q8_8", "__smul16x8");
        }
__smul16x8_q1_7

符号付き乗算を計算します。

書式

c
u16 __smul16x8_q1_7(u16 a, u8 b);

引数

a

符号付き16ビットのビット列。数値は−32768~32767。

b

符号付き8ビットのビット列。係数は−128~127。

戻り値

a*bの下位16ビットです。

使用条件と制限

FCでは名前にq8_8/q1_7があっても生の整数積を使い、固定小数点の縮小シフトは行いません。Q1.7の3関数は対応する符号付き関数への別名です。単位変換は呼び出し側で行います。符号付き結果もu16のビット列として返します。

使用例

c
// Compare literal coefficients with values read from RAM.
result[14]=__smul16x8_q1_7(-513,-64);
v0=-513;v1=-64;
result[15]=__smul16x8_q1_7(v0,v1);
// Reverse coefficient signs and verify negative products.
result[16]=__smul16x8_q1_7(-513,64);
v0=-513;v1=64;
result[17]=__smul16x8_q1_7(v0,v1);

呼び出し部分のコード例です。完全なプログラムに組み込む際は、記載した初期化とバッファの条件を満たしてください。

期待する結果

固定値とRAM変数で同じ計算を行う例です。画面には代表値を符号なし16ビットで表示し、負数は2の補数の値になります。GBは固定小数点の縮小を行い、FCは整数積を返すため、同じ入力でも両機種の結果は異なります。

この抜粋で保存する16ビット値は result[14]=32832, result[15]=32832, result[16]=32704, result[17]=32704 です。固定値とRAM変数を使った計算は、それぞれ同じ値になります。画面は代表値を表示し、全結果はresult配列に保存します。

__smul16x8_q1_7: NROM
NROM

使用例の完全なプログラム

ビルドコマンド

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\batch3_math.c -I .\kitaqfc\lib -I .\kitaq-docs\samples -o .\out\batch3_math.nes --no-cache --no-disasm --mapper=nrom --nes-chr=.\kitaq-docs\samples\font.chr
実装とソース

kitaqfc/lib/intrinsics.h:59

宣言の注記(原文)
FC alias of __smul16x8: signed-pattern raw product, with no Q1.7 scaling shift.

kitaqfc/kitaqfc/CodeGenerator.cs:4652

csharp
case "__smul16x8_q1_7": return EmitKitaqfcHelperCall(funcName, args, ctx, new[] {2,1}, funcName);

void EmitHelperMath()
        {
            // Consume eight multiplier bits and accumulate the low product word in shared temporaries, returning A/X.
            EmitHelperStart("__mul16x8");
            EmitAsm("LDA", Imm(0));
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp1Address));
            EmitAsm("LDX", Imm(8));
            string loop = NewGeneratedLabel("mul16x8_loop");
            string skip = NewGeneratedLabel("mul16x8_skip_add");
            _assembly.Add(Expr.Make(Tag.Label, loop));
            EmitAsm("LSR", Mem(CallArgBase + 2));
            EmitAsm("BCC", Rel(skip));
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("CLC");
            EmitAsm("ADC", Mem(CallArgBase));
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp1Address));
            EmitAsm("ADC", Mem(CallArgBase + 1));
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp1Address));
            _assembly.Add(Expr.Make(Tag.Label, skip));
            EmitAsm("ASL", Mem(CallArgBase));
            EmitAsm("ROL", Mem(CallArgBase + 1));
            EmitAsm("DEX");
            EmitAsm("BNE", Rel(loop));
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("LDX", Mem(_runtimeIntrinsicTmp1Address));
            EmitAsm("RTS");

            // Zero-extend the byte multiplicand, reuse word multiplication and return its high byte.
            EmitHelperStart("__mul8x8_hi");
            EmitAsm("LDA", Mem(CallArgBase + 1));
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp2Address));
            EmitAsm("LDA", Imm(0));
            EmitAsm("STA", Mem(CallArgBase + 1));
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp2Address));
            EmitAsm("STA", Mem(CallArgBase + 2));
            EmitAsm("JSR", Abs("__mul16x8"));
            EmitAsm("TXA");
            EmitAsm("RTS");

            // Convert signed operands to magnitudes, multiply, then restore the product sign modulo 65536.
            EmitHelperStart("__smul16x8");
            EmitAsm("LDA", Imm(0));
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp2Address));
            string aPos = NewGeneratedLabel("smul_a_positive");
            EmitAsm("LDA", Mem(CallArgBase + 1));
            EmitAsm("AND", Imm(0x80));
            EmitAsm("BEQ", Rel(aPos));
            EmitAsm("LDA", Mem(CallArgBase)); EmitAsm("EOR", Imm(0xFF)); EmitAsm("CLC"); EmitAsm("ADC", Imm(1)); EmitAsm("STA", Mem(CallArgBase));
            EmitAsm("LDA", Mem(CallArgBase + 1)); EmitAsm("EOR", Imm(0xFF)); EmitAsm("ADC", Imm(0)); EmitAsm("STA", Mem(CallArgBase + 1));
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp2Address)); EmitAsm("EOR", Imm(1)); EmitAsm("STA", Mem(_runtimeIntrinsicTmp2Address));
            _assembly.Add(Expr.Make(Tag.Label, aPos));
            string bPos = NewGeneratedLabel("smul_b_positive");
            EmitAsm("LDA", Mem(CallArgBase + 2));
            EmitAsm("AND", Imm(0x80));
            EmitAsm("BEQ", Rel(bPos));
            EmitAsm("LDA", Mem(CallArgBase + 2)); EmitAsm("EOR", Imm(0xFF)); EmitAsm("CLC"); EmitAsm("ADC", Imm(1)); EmitAsm("STA", Mem(CallArgBase + 2));
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp2Address)); EmitAsm("EOR", Imm(1)); EmitAsm("STA", Mem(_runtimeIntrinsicTmp2Address));
            _assembly.Add(Expr.Make(Tag.Label, bPos));
            EmitAsm("JSR", Abs("__mul16x8"));
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("STX", Mem(_runtimeIntrinsicTmp1Address));
            string sdone = NewGeneratedLabel("smul_done");
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp2Address));
            EmitAsm("BEQ", Rel(sdone));
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp0Address)); EmitAsm("EOR", Imm(0xFF)); EmitAsm("CLC"); EmitAsm("ADC", Imm(1)); EmitAsm("STA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp1Address)); EmitAsm("EOR", Imm(0xFF)); EmitAsm("ADC", Imm(0)); EmitAsm("STA", Mem(_runtimeIntrinsicTmp1Address));
            _assembly.Add(Expr.Make(Tag.Label, sdone));
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("LDX", Mem(_runtimeIntrinsicTmp1Address));
            EmitAsm("RTS");

            EmitMacHelper("__mac16", "__mul16x8");
            EmitMacHelper("__smac16", "__smul16x8");
            EmitDot2Helper("__dot2_q8_8", "__mul16x8");
            EmitDot3Helper("__dot3_q8_8", "__mul16x8");
            EmitDot2Helper("__sdot2_q8_8", "__smul16x8");
            EmitDot3Helper("__sdot3_q8_8", "__smul16x8");
        }
__split_scroll_sprite0

0番スプライトの新しいヒットを待ち、横分割用にスクロールX/Yを書き込みます。上部を固定して下部の背景だけ横へずらす用途に使えます。

書式

c
void __split_scroll_sprite0(u8 x, u8 y);

引数

x

横スクロール値0~255。画面内の物体のX座標ではありません。サンプルでは32を指定し、同じ背景の下部を左へ32ピクセル移動して表示します。

y

縦スクロールの一時設定値0~255。通常の表示範囲は0~239です。このヘルパーは$2005へ書くため、描画中の縦位置を直ちに変更する機能ではありません。横分割のサンプルでは0を渡します。

戻り値

戻り値はありません。

使用条件と制限

OAMの0番スプライトと背景の不透明なピクセルを重ねます。背景・スプライトの描画を両方有効にしてください。スプライトの「背景の後ろ」属性を付けてもヒットします。左端8ピクセルは該当する表示許可が必要で、X=255の重なりはヒットしません。

PPUSTATUS($2002)のビット6が0になるまで待ち、続いて1になるまで待ちます。呼び出した時点ですでにヒット済みなら、次のフレームの新しいヒットを待ちます。フラグは読み取りでは解除されず、PPUが次のフレームの準備中に解除します。

タイムアウトも失敗の戻り値もありません。0番スプライトが非表示、背景が透明、描画が無効など、重なりが発生しない設定では戻りません。呼ぶ前にヒット条件を保証してください。CPUのIRQ許可は不要で、この関数自身はNMIを有効化しません。サンプルのフレーム待機にはNMIを有効にしています。

$2002を読むので、PPUのアドレス/スクロール共有ラッチとVBlankフラグにも影響します。途中まで書いた$2005/$2006の2バイト操作を、この呼び出しを挟んで続けないでください。NMI/IRQハンドラーから呼ばず、前景ループで使います。

ヒット後に$2002を読み、$2005へX、Yの順に書きます。細かいX位置はすぐに変わり、タイル単位のX位置は次の水平再ロードで反映されます。Yは通常、次のプリレンダーで反映されますが、それまでに別のスクロール設定で上書きされることがあります。任意のX/Yへ即座に飛ばすラスタースクロール関数としては使わないでください。

書き込み位置にはポーリングと命令実行の遅れがあります。KUROSAKIは走査線単位で描画するため、実機のドット単位の境界位置をこの画像だけで保証しません。サンプルではY=96~103の白い帯の中に切り替えを収め、境界付近の誤差が青・赤の帯へ出ない構成にしています。

共有ヘッダーfc_sprite0_scene.hが独自の青・赤の帯と白い横帯を準備します。0番スプライトは画面(16,96)に置き、背景の後ろへ隠しています。OAMのYには表示位置−1の95を指定します。各フレームのVBlankで上部のスクロールを0へ戻してから、ヒットを待ちます。

NESdev: PPU scrolling

NESdev: PPU programmer reference

使用例

c
void main(void) {
    s0_prepare();
    while (1) {
        s0_frame();
        __split_scroll_sprite0(32,0);
        s0_completed++;
    }
}

呼び出し部分のコード例です。完全なプログラムに組み込む際は、記載した初期化とバッファの条件を満たしてください。

期待する結果

白い横帯より上では青がX=64~95、赤がX=160~191です。Y=104以降の下部では青がX=32~63、赤がX=128~159になり、下部だけ左へ32ピクセルずれます。横帯はY=96~103、縦帯の下端はY=207です。毎フレーム1回ずつ処理が完了することと、画像全体の色・位置を検証しています。

__split_scroll_sprite0: SPLIT-DEFAULT — 白い横帯より上では青がX=64~95、赤がX=160~191です。Y=104以降の下部では青がX=32~63、赤がX=128~159になり、下部だけ左へ32ピクセルずれます。横帯はY=96~103、縦帯の下端はY=207です。毎フレーム1回ずつ処理が完了することと、画像全体の色・位置を検証しています。
SPLIT-DEFAULT — 白い横帯より上では青がX=64~95、赤がX=160~191です。Y=104以降の下部では青がX=32~63、赤がX=128~159になり、下部だけ左へ32ピクセルずれます。横帯はY=96~103、縦帯の下端はY=207です。毎フレーム1回ずつ処理が完了することと、画像全体の色・位置を検証しています。

使用例の完全なプログラム

ビルドコマンド

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\sprite0_split.c -I .\kitaqfc\lib -I .\kitaq-docs\samples -o .\out\sprite0_split.nes --mapper=nrom --nes-chr=.\kitaq-docs\samples\sprite0_scene.chr --no-cache --no-disasm
実装とソース

kitaqfc/lib/intrinsics.h:336

宣言の注記(原文)
Wait for a new sprite-zero hit, reset the PPU latch and write X/Y scroll; requires a visible hit setup.
__sprite0_wait_hit

0番スプライトと背景が新しく重なるまで待ちます。画面内の既知の位置に処理を同期させるための待機で、スクロール値は変更しません。

書式

c
void __sprite0_wait_hit(void);

戻り値

戻り値はありません。

使用条件と制限

OAMの0番スプライトと背景の不透明なピクセルを重ねます。背景・スプライトの描画を両方有効にしてください。スプライトの「背景の後ろ」属性を付けてもヒットします。左端8ピクセルは該当する表示許可が必要で、X=255の重なりはヒットしません。

PPUSTATUS($2002)のビット6が0になるまで待ち、続いて1になるまで待ちます。呼び出した時点ですでにヒット済みなら、次のフレームの新しいヒットを待ちます。フラグは読み取りでは解除されず、PPUが次のフレームの準備中に解除します。

タイムアウトも失敗の戻り値もありません。0番スプライトが非表示、背景が透明、描画が無効など、重なりが発生しない設定では戻りません。呼ぶ前にヒット条件を保証してください。CPUのIRQ許可は不要で、この関数自身はNMIを有効化しません。サンプルのフレーム待機にはNMIを有効にしています。

$2002を読むので、PPUのアドレス/スクロール共有ラッチとVBlankフラグにも影響します。途中まで書いた$2005/$2006の2バイト操作を、この呼び出しを挟んで続けないでください。NMI/IRQハンドラーから呼ばず、前景ループで使います。

書き込み位置にはポーリングと命令実行の遅れがあります。KUROSAKIは走査線単位で描画するため、実機のドット単位の境界位置をこの画像だけで保証しません。サンプルではY=96~103の白い帯の中に切り替えを収め、境界付近の誤差が青・赤の帯へ出ない構成にしています。

共有ヘッダーfc_sprite0_scene.hが独自の青・赤の帯と白い横帯を準備します。0番スプライトは画面(16,96)に置き、背景の後ろへ隠しています。OAMのYには表示位置−1の95を指定します。各フレームのVBlankで上部のスクロールを0へ戻してから、ヒットを待ちます。

NESdev: PPU scrolling

NESdev: PPU programmer reference

使用例

c
void main(void) {
    s0_prepare();
    while (1) {
        s0_frame();
        __sprite0_wait_hit();
        s0_completed++;
    }
}

呼び出し部分のコード例です。完全なプログラムに組み込む際は、記載した初期化とバッファの条件を満たしてください。

期待する結果

青い縦帯はX=64~95、赤い縦帯はX=160~191にあり、白い横帯の上下で位置は変わりません。待つだけではスクロールを変更しないことを示します。120フレーム時点で116回、121フレームで117回の待機完了を確認し、次のフレームにも進み続けることを検証しています。

__sprite0_wait_hit: WAIT-DEFAULT — 青い縦帯はX=64~95、赤い縦帯はX=160~191にあり、白い横帯の上下で位置は変わりません。待つだけではスクロールを変更しないことを示します。120フレーム時点で116回、121フレームで117回の待機完了を確認し、次のフレームにも進み続けることを検証しています。
WAIT-DEFAULT — 青い縦帯はX=64~95、赤い縦帯はX=160~191にあり、白い横帯の上下で位置は変わりません。待つだけではスクロールを変更しないことを示します。120フレーム時点で116回、121フレームで117回の待機完了を確認し、次のフレームにも進み続けることを検証しています。

使用例の完全なプログラム

ビルドコマンド

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\sprite0_wait.c -I .\kitaqfc\lib -I .\kitaq-docs\samples -o .\out\sprite0_wait.nes --mapper=nrom --nes-chr=.\kitaq-docs\samples\sprite0_scene.chr --no-cache --no-disasm
実装とソース

kitaqfc/lib/intrinsics.h:334

宣言の注記(原文)
Wait for the current hit flag to clear, then for a new hit. No timeout is provided.
__sprite_attr

シャドーバッファ内の1個のスプライトの属性バイト全体を置き換えます。タイルや位置を変えずに、パレット・背景との優先順位・左右反転・上下反転を設定できます。

書式

c
void __sprite_attr(u8 index, u8 attr);

引数

index

スプライトの項目番号0〜63です。バイト単位のオフセットではありません。内部では8ビットのカーソルで4倍するため、範囲外の値は64を周期に折り返し、別の項目を上書きします。確保済みかどうかの検査は行いません。

attr

属性バイト全体です。ビット0〜1でスプライトパレット0〜3、ビット5で不透明な背景の後ろに表示する指定、ビット6で左右反転、ビット7で上下反転を設定します。ビット2〜4は0にしてください。フラグはビットORで組み合わせ、渡したバイトで属性全体を置き換えます。

戻り値

ありません(void)。

使用条件と制限

これらの操作はCPU RAMの0x0200–0x02FFを書き換えます。ハードウェアOAMへ即座に反映されるわけではありません。ページを完成させてから__oam_dmaで転送してください。スプライトライブラリの確保フラグ・座標配列・使用数は更新しません。準備途中のページを割り込み処理で変更・転送しないようにします。

使用例

c
__sprite_attr(3,0xC0); // Flip the arrow horizontally and vertically.

呼び出し部分のコード例です。完全なプログラムに組み込む際は、記載した初期化とバッファの条件を満たしてください。

期待する結果

操作ごとに見出し付きの行を分けたサンプルです。SETとMOVEは(144,16)・(144,32)の8×8右向き矢印、TILEは(144,48)の緑の四角、ATTRは(144,64)の上下左右を反転した矢印です。HIDEの(144,80)とCLEARの(144,112)には何も表示されません。METAは(136,96)と(144,96)の2個組で、2個目を左右反転します。NEXT 07は5・6番へ書き込んだ後の次の位置です。矢印の輪郭は赤、軸は緑、先端の一部は青です。検証では画像ごとに8,704ピクセルと転送後のOAMデータを照合します。実機では未検証です。

TRANSFERの(144,128)には矢印を表示します。このプログラムは、個別のスプライト操作で設定した全項目を含むページ02を__oam_dmaで直接転送します。

__sprite_attr: PAGE02
PAGE02

使用例の完全なプログラム

ビルドコマンド

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\oam_intrinsics.c -I .\kitaqfc\lib -I .\kitaq-docs\samples --nes-chr=.\kitaq-docs\samples\sprite_example.chr -o .\out\oam_intrinsics.nes --no-cache --no-disasm
実装とソース

kitaqfc/lib/intrinsics.h:198

宣言の注記(原文)
Replace the selected shadow attribute byte, including palette, priority and flip bits.
csharp
EmitHelperStart("__sprite_attr");
            EmitSpriteIndexToX();
            EmitAsm("LDA", Mem(CallArgBase + 1)); EmitAsm("STA", new AsmOperand(OamRamBase + 2, AddressMode.AbsoluteX));
            EmitAsm("RTS");
__sprite_hide

1個のシャドースプライトのYを0xF0にして非表示にします。X・タイル・属性は残るので再利用できます。スロットの解放やスプライトライブラリの使用数の変更は行いません。

書式

c
void __sprite_hide(u8 index);

引数

index

スプライトの項目番号0〜63です。バイト単位のオフセットではありません。内部では8ビットのカーソルで4倍するため、範囲外の値は64を周期に折り返し、別の項目を上書きします。確保済みかどうかの検査は行いません。

戻り値

ありません(void)。

使用条件と制限

これらの操作はCPU RAMの0x0200–0x02FFを書き換えます。ハードウェアOAMへ即座に反映されるわけではありません。ページを完成させてから__oam_dmaで転送してください。スプライトライブラリの確保フラグ・座標配列・使用数は更新しません。準備途中のページを割り込み処理で変更・転送しないようにします。

使用例

c
__sprite_hide(4); // The HIDE row remains empty after DMA.

呼び出し部分のコード例です。完全なプログラムに組み込む際は、記載した初期化とバッファの条件を満たしてください。

期待する結果

操作ごとに見出し付きの行を分けたサンプルです。SETとMOVEは(144,16)・(144,32)の8×8右向き矢印、TILEは(144,48)の緑の四角、ATTRは(144,64)の上下左右を反転した矢印です。HIDEの(144,80)とCLEARの(144,112)には何も表示されません。METAは(136,96)と(144,96)の2個組で、2個目を左右反転します。NEXT 07は5・6番へ書き込んだ後の次の位置です。矢印の輪郭は赤、軸は緑、先端の一部は青です。検証では画像ごとに8,704ピクセルと転送後のOAMデータを照合します。実機では未検証です。

TRANSFERの(144,128)には矢印を表示します。このプログラムは、個別のスプライト操作で設定した全項目を含むページ02を__oam_dmaで直接転送します。

__sprite_hide: PAGE02
PAGE02

使用例の完全なプログラム

ビルドコマンド

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\oam_intrinsics.c -I .\kitaqfc\lib -I .\kitaq-docs\samples --nes-chr=.\kitaq-docs\samples\sprite_example.chr -o .\out\oam_intrinsics.nes --no-cache --no-disasm
実装とソース

kitaqfc/lib/intrinsics.h:200

宣言の注記(原文)
Write F0 to the selected shadow Y byte; issue DMA to make the change visible.
csharp
EmitHelperStart("__sprite_hide");
            EmitSpriteIndexToX();
            EmitAsm("LDA", Imm(0xF0)); EmitAsm("STA", new AsmOperand(OamRamBase + 0, AddressMode.AbsoluteX));
            EmitAsm("RTS");
__sprite_move

シャドーバッファ内の1個のスプライトのX・Yだけを変更します。タイル番号と属性を保持するため、パレットや反転状態を変えずに同じ絵を移動できます。

書式

c
void __sprite_move(u8 index, u8 x, u8 y);

引数

index

スプライトの項目番号0〜63です。バイト単位のオフセットではありません。内部では8ビットのカーソルで4倍するため、範囲外の値は64を周期に折り返し、別の項目を上書きします。確保済みかどうかの検査は行いません。

x

ピクセル単位の横位置を符号なし8ビット値で渡します。タイル座標ではありません。

y

OAMに格納するY値です。画面の1行目以降に上端を置く場合は表示上端−1を渡します。239〜255は240行の表示範囲外です。

戻り値

ありません(void)。

使用条件と制限

これらの操作はCPU RAMの0x0200–0x02FFを書き換えます。ハードウェアOAMへ即座に反映されるわけではありません。ページを完成させてから__oam_dmaで転送してください。スプライトライブラリの確保フラグ・座標配列・使用数は更新しません。準備途中のページを割り込み処理で変更・転送しないようにします。

使用例

c
__sprite_move(1,144,31); // Move to (144,32), retaining tile 128 and attributes 0.

呼び出し部分のコード例です。完全なプログラムに組み込む際は、記載した初期化とバッファの条件を満たしてください。

期待する結果

操作ごとに見出し付きの行を分けたサンプルです。SETとMOVEは(144,16)・(144,32)の8×8右向き矢印、TILEは(144,48)の緑の四角、ATTRは(144,64)の上下左右を反転した矢印です。HIDEの(144,80)とCLEARの(144,112)には何も表示されません。METAは(136,96)と(144,96)の2個組で、2個目を左右反転します。NEXT 07は5・6番へ書き込んだ後の次の位置です。矢印の輪郭は赤、軸は緑、先端の一部は青です。検証では画像ごとに8,704ピクセルと転送後のOAMデータを照合します。実機では未検証です。

TRANSFERの(144,128)には矢印を表示します。このプログラムは、個別のスプライト操作で設定した全項目を含むページ02を__oam_dmaで直接転送します。

__sprite_move: PAGE02
PAGE02

使用例の完全なプログラム

ビルドコマンド

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\oam_intrinsics.c -I .\kitaqfc\lib -I .\kitaq-docs\samples --nes-chr=.\kitaq-docs\samples\sprite_example.chr -o .\out\oam_intrinsics.nes --no-cache --no-disasm
実装とソース

kitaqfc/lib/intrinsics.h:194

宣言の注記(原文)
Replace only shadow X/Y for index 0..63; tile and attribute bytes remain unchanged.
csharp
EmitHelperStart("__sprite_move");
            EmitSpriteIndexToX();
            EmitAsm("LDA", Mem(CallArgBase + 2)); EmitAsm("STA", new AsmOperand(OamRamBase + 0, AddressMode.AbsoluteX));
            EmitAsm("LDA", Mem(CallArgBase + 1)); EmitAsm("STA", new AsmOperand(OamRamBase + 3, AddressMode.AbsoluteX));
            EmitAsm("RTS");
__sprite_set

1個のスプライトの4項目をページ02のシャドーバッファへ書き込みます。引数の順序は番号・X・Y・タイル・属性ですが、格納順はY・タイル・属性・Xです。

書式

c
void __sprite_set(u8 index, u8 x, u8 y, u8 tile, u8 attr);

引数

index

スプライトの項目番号0〜63です。バイト単位のオフセットではありません。内部では8ビットのカーソルで4倍するため、範囲外の値は64を周期に折り返し、別の項目を上書きします。確保済みかどうかの検査は行いません。

x

ピクセル単位の横位置を符号なし8ビット値で渡します。タイル座標ではありません。

y

OAMに格納するY値です。画面の1行目以降に上端を置く場合は表示上端−1を渡します。239〜255は240行の表示範囲外です。

tile

8ビットのタイル番号です。画像を先に読み込んでください。8×16スプライトのハードウェア規則はそのまま適用され、2枚組に合わせた番号補正は行いません。

attr

属性バイト全体です。ビット0〜1でスプライトパレット0〜3、ビット5で不透明な背景の後ろに表示する指定、ビット6で左右反転、ビット7で上下反転を設定します。ビット2〜4は0にしてください。フラグはビットORで組み合わせ、渡したバイトで属性全体を置き換えます。

戻り値

ありません(void)。

使用条件と制限

これらの操作はCPU RAMの0x0200–0x02FFを書き換えます。ハードウェアOAMへ即座に反映されるわけではありません。ページを完成させてから__oam_dmaで転送してください。スプライトライブラリの確保フラグ・座標配列・使用数は更新しません。準備途中のページを割り込み処理で変更・転送しないようにします。

使用例

c
__sprite_set(0,144,15,128,0); // An 8x8 arrow at visible position (144,16).

呼び出し部分のコード例です。完全なプログラムに組み込む際は、記載した初期化とバッファの条件を満たしてください。

期待する結果

操作ごとに見出し付きの行を分けたサンプルです。SETとMOVEは(144,16)・(144,32)の8×8右向き矢印、TILEは(144,48)の緑の四角、ATTRは(144,64)の上下左右を反転した矢印です。HIDEの(144,80)とCLEARの(144,112)には何も表示されません。METAは(136,96)と(144,96)の2個組で、2個目を左右反転します。NEXT 07は5・6番へ書き込んだ後の次の位置です。矢印の輪郭は赤、軸は緑、先端の一部は青です。検証では画像ごとに8,704ピクセルと転送後のOAMデータを照合します。実機では未検証です。

TRANSFERの(144,128)には矢印を表示します。このプログラムは、個別のスプライト操作で設定した全項目を含むページ02を__oam_dmaで直接転送します。

__sprite_set: PAGE02
PAGE02

使用例の完全なプログラム

ビルドコマンド

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\oam_intrinsics.c -I .\kitaqfc\lib -I .\kitaq-docs\samples --nes-chr=.\kitaq-docs\samples\sprite_example.chr -o .\out\oam_intrinsics.nes --no-cache --no-disasm
実装とソース

kitaqfc/lib/intrinsics.h:192

宣言の注記(原文)
Write Y, tile, attributes and X to one shadow entry. Use indices 0..63; coordinates are raw OAM bytes.
csharp
EmitHelperStart("__sprite_set");
            EmitSpriteIndexToX();
            EmitAsm("LDA", Mem(CallArgBase + 2)); EmitAsm("STA", new AsmOperand(OamRamBase + 0, AddressMode.AbsoluteX));
            EmitAsm("LDA", Mem(CallArgBase + 3)); EmitAsm("STA", new AsmOperand(OamRamBase + 1, AddressMode.AbsoluteX));
            EmitAsm("LDA", Mem(CallArgBase + 4)); EmitAsm("STA", new AsmOperand(OamRamBase + 2, AddressMode.AbsoluteX));
            EmitAsm("LDA", Mem(CallArgBase + 1)); EmitAsm("STA", new AsmOperand(OamRamBase + 3, AddressMode.AbsoluteX));
            EmitAsm("RTS");
__sprite_tile

シャドーバッファ内の1個のスプライトのタイル番号だけを置き換えます。位置と属性は保持します。既存のパターンを選択する処理であり、CHRメモリへの画像転送は行いません。

書式

c
void __sprite_tile(u8 index, u8 tile);

引数

index

スプライトの項目番号0〜63です。バイト単位のオフセットではありません。内部では8ビットのカーソルで4倍するため、範囲外の値は64を周期に折り返し、別の項目を上書きします。確保済みかどうかの検査は行いません。

tile

8ビットのタイル番号です。画像を先に読み込んでください。8×16スプライトのハードウェア規則はそのまま適用され、2枚組に合わせた番号補正は行いません。

戻り値

ありません(void)。

使用条件と制限

これらの操作はCPU RAMの0x0200–0x02FFを書き換えます。ハードウェアOAMへ即座に反映されるわけではありません。ページを完成させてから__oam_dmaで転送してください。スプライトライブラリの確保フラグ・座標配列・使用数は更新しません。準備途中のページを割り込み処理で変更・転送しないようにします。

使用例

c
__sprite_tile(2,129); // Select the green square at (144,48).

呼び出し部分のコード例です。完全なプログラムに組み込む際は、記載した初期化とバッファの条件を満たしてください。

期待する結果

操作ごとに見出し付きの行を分けたサンプルです。SETとMOVEは(144,16)・(144,32)の8×8右向き矢印、TILEは(144,48)の緑の四角、ATTRは(144,64)の上下左右を反転した矢印です。HIDEの(144,80)とCLEARの(144,112)には何も表示されません。METAは(136,96)と(144,96)の2個組で、2個目を左右反転します。NEXT 07は5・6番へ書き込んだ後の次の位置です。矢印の輪郭は赤、軸は緑、先端の一部は青です。検証では画像ごとに8,704ピクセルと転送後のOAMデータを照合します。実機では未検証です。

TRANSFERの(144,128)には矢印を表示します。このプログラムは、個別のスプライト操作で設定した全項目を含むページ02を__oam_dmaで直接転送します。

__sprite_tile: PAGE02
PAGE02

使用例の完全なプログラム

ビルドコマンド

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\oam_intrinsics.c -I .\kitaqfc\lib -I .\kitaq-docs\samples --nes-chr=.\kitaq-docs\samples\sprite_example.chr -o .\out\oam_intrinsics.nes --no-cache --no-disasm
実装とソース

kitaqfc/lib/intrinsics.h:196

宣言の注記(原文)
Replace the selected shadow tile byte; this does not upload tile patterns.
csharp
EmitHelperStart("__sprite_tile");
            EmitSpriteIndexToX();
            EmitAsm("LDA", Mem(CallArgBase + 1)); EmitAsm("STA", new AsmOperand(OamRamBase + 1, AddressMode.AbsoluteX));
            EmitAsm("RTS");
__vram_fill

ppu_addrを設定し、同じ1バイト値valueをPPUDATAへlen回書きます。関数が戻るまでに完了する直接の埋め込み処理です。タイルの寸法・属性・矩形の境界は解釈しません。

書式

c
void __vram_fill(u16 ppu_addr, u8 value, u8 len);

引数

ppu_addr

CPUポインターではなく、PPUのバイトアドレス(0x0000~0x3FFF)です。転送先は書き込み可能でなければなりません。パターンの変更にはCHR ROMではなくCHR RAMが必要です。PPUのマッピングとミラーリングが適用されます。連続したコピーや埋め込みではPPUCTRLの増分を1にしてください。安全にPPUへ書ける時期に実行し、その後で必要なスクロール状態を設定してください。実行処理はPPUADDRとPPUDATAを使い、スクロール設定を復元しません。

value

0〜255の1バイトを繰り返し書きます。画面での意味は、転送先がタイル番号、画素のビットプレーン、その他のバイト列のどれかによって変わります。

len

バイト数0..255です。0でもPPUアドレス設定とラッチのリセットは行いますが、データは書きません。転送先の容量確認、切り詰め、バンク切り替え、遅延転送はありません。

戻り値

ありません(void)。

使用条件と制限

PPU直接アクセスは、描画停止中か、処理全体が安全なVBlank時間内に収まる条件で行います。これらの関数は待機・キュー登録・NMI抑止・スクロール復元を行いません。呼び出し中に割り込みで共有ラッチやアドレスを変更しないでください。PPUCTRLのビット2により、書き込み先は1バイトごとに1または32進みます。ミラーリングとアドレスの周回はPPUの動作に従い、CHR-ROMへは書き込めません。アクセス後、描画を始める前に意図したスクロール状態を設定してください。

NESdev: PPU registers

NESdev: PPU scrolling

使用例

c
__vram_fill(0x2060,2,32); // A second blue row of 4-pixel-wide left halves.

呼び出し部分のコード例です。完全なプログラムに組み込む際は、記載した初期化とバッファの条件を満たしてください。

期待する結果

このプログラムではレジスター設定、タイル・属性の直接書き込み、スクロール、背景・スプライトそれぞれのパレット転送を学びます。(0,16)の青い帯は256×16ピクセルです。上段は8×8の塗りつぶし、左半分4×8の棒、8行の三角形を繰り返し、下段は左半分の棒だけを並べます。(16,48)に緑の塗りつぶし64×32、(144,48)に三角形タイルを横8個・縦4個並べた赤い64×32の領域が出ます。

y=96では、x=16・24・32に青い正方形・左半分の棒・三角形、x=80に赤い左半分の棒、x=144に緑の三角形が表示されます。これらは背景タイルです。別パレットを使うスプライトはy=144に並び、x=16が赤い8×8の正方形、x=80が青い左半分4×8の棒、x=144が緑の8行の三角形です。最終スクロールは(0,0)です。

画像ではスプライトの色も含め、16,384ピクセルの形と色を照合します。別の31ケースで、レジスターの置き換え、ステータス・ラッチの副作用、スクロール保存値、0・255バイト転送、増分1・32、マップ端、矩形の寸法0、テーブル選択、属性1バイト全体、背景・スプライト両方のパレットを検証しています。Y=255はレジスターへ書く値としての確認で、正しいスクロール画像の検証ではありません。検証環境はKUROSAKIのNROMで、実機は未検証です。

__vram_fill: NROM
NROM

使用例の完全なプログラム

ビルドコマンド

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\ppu_intrinsics.c -I .\kitaqfc\lib -I .\kitaq-docs\samples --nes-chr=.\kitaq-docs\samples\api-examples\fc\vram_shapes.chr -o .\out\ppu_intrinsics.nes --no-cache --no-disasm
実装とソース

kitaqfc/lib/intrinsics.h:122

宣言の注記(原文)
Write repeated bytes directly; this is not an enqueue operation.

kitaqfc/kitaqfc/CodeGenerator.cs:4818

csharp
case "__vram_fill": return EmitKitaqfcHelperCall(funcName, args, ctx, new[] {2,1,1}, funcName);
                // Nametable and attribute helpers receive byte coordinates, values and optional table selectors.

void EmitHelperVramFill()
        {
            EmitHelperStart("__vram_fill");
            EmitPpuAddrFromCallArg();
            EmitAsm("LDX", Mem(CallArgBase + 3));
            string loop = NewGeneratedLabel("vram_fill_loop");
            string done = NewGeneratedLabel("vram_fill_done");
            EmitAsm("BEQ", Rel(done));
            _assembly.Add(Expr.Make(Tag.Label, loop));
            EmitAsm("LDA", Mem(CallArgBase + 2));
            EmitAsm("STA", Mem(0x2007));
            EmitAsm("DEX");
            EmitAsm("BNE", Rel(loop));
            _assembly.Add(Expr.Make(Tag.Label, done));
            EmitAsm("RTS");
        }
__vram_write

PPUラッチをリセットしてppu_addrを設定し、転送元の連続したlenバイトをPPUDATAへ同期転送します。個数は8ビットで、コンパイラは添字付きの読み取りを行います。後で実行するためのポインター保持はなく、戻った後に転送元を変更してもVRAMは変わりません。

書式

c
void __vram_write(u16 ppu_addr, const u8* src, u8 len);

引数

ppu_addr

CPUポインターではなく、PPUのバイトアドレス(0x0000~0x3FFF)です。転送先は書き込み可能でなければなりません。パターンの変更にはCHR ROMではなくCHR RAMが必要です。PPUのマッピングとミラーリングが適用されます。連続したコピーや埋め込みではPPUCTRLの増分を1にしてください。安全にPPUへ書ける時期に実行し、その後で必要なスクロール状態を設定してください。実行処理はPPUADDRとPPUDATAを使い、スクロール設定を復元しません。

src

指定したバイト数を読み出せるCPU側のポインターです。同期転送が戻るまで、元データが見えるバンクと内容を保ってください。バンク切り替え・キューへのポインター保持・転送元の長さ確認は行いません。長さ0ならポインターを参照しません。

len

バイト数0..255です。0でもPPUアドレス設定とラッチのリセットは行いますが、データは書きません。転送先の容量確認、切り詰め、バンク切り替え、遅延転送はありません。

戻り値

ありません(void)。

使用条件と制限

PPU直接アクセスは、描画停止中か、処理全体が安全なVBlank時間内に収まる条件で行います。これらの関数は待機・キュー登録・NMI抑止・スクロール復元を行いません。呼び出し中に割り込みで共有ラッチやアドレスを変更しないでください。PPUCTRLのビット2により、書き込み先は1バイトごとに1または32進みます。ミラーリングとアドレスの周回はPPUの動作に従い、CHR-ROMへは書き込めません。アクセス後、描画を始める前に意図したスクロール状態を設定してください。

NESdev: PPU registers

NESdev: PPU scrolling

使用例

c
__vram_write(0x2040,pattern,32); // Blue row: solid tile, left half, triangle.
pattern[0]=3; // The completed transfer keeps its first solid tile.

呼び出し部分のコード例です。完全なプログラムに組み込む際は、記載した初期化とバッファの条件を満たしてください。

期待する結果

このプログラムではレジスター設定、タイル・属性の直接書き込み、スクロール、背景・スプライトそれぞれのパレット転送を学びます。(0,16)の青い帯は256×16ピクセルです。上段は8×8の塗りつぶし、左半分4×8の棒、8行の三角形を繰り返し、下段は左半分の棒だけを並べます。(16,48)に緑の塗りつぶし64×32、(144,48)に三角形タイルを横8個・縦4個並べた赤い64×32の領域が出ます。

y=96では、x=16・24・32に青い正方形・左半分の棒・三角形、x=80に赤い左半分の棒、x=144に緑の三角形が表示されます。これらは背景タイルです。別パレットを使うスプライトはy=144に並び、x=16が赤い8×8の正方形、x=80が青い左半分4×8の棒、x=144が緑の8行の三角形です。最終スクロールは(0,0)です。

画像ではスプライトの色も含め、16,384ピクセルの形と色を照合します。別の31ケースで、レジスターの置き換え、ステータス・ラッチの副作用、スクロール保存値、0・255バイト転送、増分1・32、マップ端、矩形の寸法0、テーブル選択、属性1バイト全体、背景・スプライト両方のパレットを検証しています。Y=255はレジスターへ書く値としての確認で、正しいスクロール画像の検証ではありません。検証環境はKUROSAKIのNROMで、実機は未検証です。

__vram_write: NROM
NROM

使用例の完全なプログラム

ビルドコマンド

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\ppu_intrinsics.c -I .\kitaqfc\lib -I .\kitaq-docs\samples --nes-chr=.\kitaq-docs\samples\api-examples\fc\vram_shapes.chr -o .\out\ppu_intrinsics.nes --no-cache --no-disasm
実装とソース

kitaqfc/lib/intrinsics.h:120

宣言の注記(原文)
Write a byte-counted source range directly; arrange PPU access timing and source-bank visibility.

kitaqfc/kitaqfc/CodeGenerator.cs:4817

csharp
case "__vram_write": return EmitKitaqfcHelperCall(funcName, args, ctx, new[] {2,2,1}, funcName);

void EmitHelperVramWrite()
        {
            EmitHelperStart("__vram_write");
            EmitPpuAddrFromCallArg();
            EmitAsm("LDY", Imm(0));
            string loop = NewGeneratedLabel("vram_write_loop");
            string done = NewGeneratedLabel("vram_write_done");
            _assembly.Add(Expr.Make(Tag.Label, loop));
            EmitAsm("CPY", Mem(CallArgBase + 4));
            EmitAsm("BEQ", Rel(done));
            EmitAsm("LDA", IndY(CallArgBase + 2));
            EmitAsm("STA", Mem(0x2007));
            EmitAsm("INY");
            EmitAsm("BNE", Rel(loop));
            _assembly.Add(Expr.Make(Tag.Label, done));
            EmitAsm("RTS");
        }
__vramq_capacity

現在の使用量にかかわらず、コマンドバッファーの総容量を返します。

書式

c
u8 __vramq_capacity(void);

戻り値

ここで説明した各機種の単位による符号なし8ビット値です。読み取るだけではキューを変更・実行しません。

使用条件と制限

FCの単位は固定128バイトの組み込みバッファーに格納する、コマンド記録のバイト数です。タイル1個の書き込みは4バイト、埋める記録は5バイト、ポインターを使うコピー記録は6バイトで、管理情報も含みます。準備済みキューはNMIで消費されるため、コミット前に使用量の確認と登録を行い、登録側と実行側を調整してください。

これらの組み込み関数は、lib/vram.cと同じコマンドバッファーを操作します。runtime.cのnes_vram_queue_*関数は、記録形式の異なる別のキューを管理します。一方のクリアや容量取得が、もう一方にも適用されるわけではありません。

キューの登録と実行は、互いに重ならないよう管理してください。割り込みで実行している途中の追加、管理変数の直接変更、保存した転送元データの意図しない上書きは避けてください。これらのAPIはロックやダブルバッファーを提供しません。容量はコマンドバッファーの容量であり、ハードウェアVRAMの空きバイト数ではありません。

使用例

c
capacity=__vramq_capacity();
if (capacity!=128) failures++;

呼び出し部分のコード例です。完全なプログラムに組み込む際は、記載した初期化とバッファの条件を満たしてください。

期待する結果

完成プログラムでは、キュー容量、保持されるポインター、2種類のリセット、直接実行とNMI実行の違いを学べます。期待値はCAPACITY 128、ENCODED BYTES 030、AFTER EXEC 000、OVERFLOW SEEN 001、FAILED CHECKS 000です。6つの記録が30バイトを使い、512バイト分の書き込みを実行します。OVERFLOW SEENは意図的な登録拒否時に保存した値であり、終了時も通知が立っているという意味ではありません。

青いコピー領域はピクセル座標(0,16)、緑の埋め込み領域は(0,88)から始まります。それぞれ、幅256ピクセルのタイル行7行と、8行目の248ピクセルを描き、合計255タイルになります。最後の8×8セルは黒いままです。コピーは8×8の塗りつぶし・左半分・三角形を繰り返しますが、登録後に転送元を変えるため、先頭は三角形です。埋め込みは呼び出し元の変数を変えても塗りつぶしを保ちます。形と色の両方を検証します。

y=160では、x=16に赤い塗りつぶしの正方形、x=224に赤い左半分のタイル、x=240に赤い塗りつぶしの正方形が現れます。最初の正方形は長さ0のコピーと埋め込みで消えず、中央は標準NMIでの実行、最後は単発書き込みを確認するものです。取り消したセル(248,160)は黒いままです。長い転送は描画とNMIを止めて実行し、最後の小さな書き込みだけをNMIに任せます。これはNROMのエミュレータ検証であり、実機検証ではありません。

__vramq_capacity: NROM
NROM

使用例の完全なプログラム

ビルドコマンド

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\vramq_intrinsics.c -I .\kitaqfc\lib -I .\kitaq-docs\samples -o .\out\vramq_intrinsics.nes --mapper=nrom --nes-chr=.\kitaq-docs\samples\api-examples\fc\vram_shapes.chr --no-cache --no-disasm
実装とソース

kitaqfc/lib/intrinsics.h:164

宣言の注記(原文)
Return total encoded command-buffer capacity; this is independent of PPU VRAM capacity.
__vramq_clear

組み込みキューの実行待ち記録をすべて破棄し、長さと準備完了フラグを0にします。VRAMへの書き込みや、オーバーフロー通知の解除は行いません。通知を解除するには__vramq_clear_overflowを別に呼びます。

書式

c
void __vramq_clear(void);

戻り値

ありません(void)。

使用条件と制限

FCの単位は固定128バイトの組み込みバッファーに格納する、コマンド記録のバイト数です。タイル1個の書き込みは4バイト、埋める記録は5バイト、ポインターを使うコピー記録は6バイトで、管理情報も含みます。準備済みキューはNMIで消費されるため、コミット前に使用量の確認と登録を行い、登録側と実行側を調整してください。

これらの組み込み関数は、lib/vram.cと同じコマンドバッファーを操作します。runtime.cのnes_vram_queue_*関数は、記録形式の異なる別のキューを管理します。一方のクリアや容量取得が、もう一方にも適用されるわけではありません。

キューの登録と実行は、互いに重ならないよう管理してください。割り込みで実行している途中の追加、管理変数の直接変更、保存した転送元データの意図しない上書きは避けてください。これらのAPIはロックやダブルバッファーを提供しません。容量はコマンドバッファーの容量であり、ハードウェアVRAMの空きバイト数ではありません。

使用例

c
__vramq_clear();
if (__vramq_len()!=0 || __vramq_overflow()!=1) failures++;
// Clearing queued work deliberately leaves the overflow latch set.

呼び出し部分のコード例です。完全なプログラムに組み込む際は、記載した初期化とバッファの条件を満たしてください。

期待する結果

完成プログラムでは、キュー容量、保持されるポインター、2種類のリセット、直接実行とNMI実行の違いを学べます。期待値はCAPACITY 128、ENCODED BYTES 030、AFTER EXEC 000、OVERFLOW SEEN 001、FAILED CHECKS 000です。6つの記録が30バイトを使い、512バイト分の書き込みを実行します。OVERFLOW SEENは意図的な登録拒否時に保存した値であり、終了時も通知が立っているという意味ではありません。

青いコピー領域はピクセル座標(0,16)、緑の埋め込み領域は(0,88)から始まります。それぞれ、幅256ピクセルのタイル行7行と、8行目の248ピクセルを描き、合計255タイルになります。最後の8×8セルは黒いままです。コピーは8×8の塗りつぶし・左半分・三角形を繰り返しますが、登録後に転送元を変えるため、先頭は三角形です。埋め込みは呼び出し元の変数を変えても塗りつぶしを保ちます。形と色の両方を検証します。

y=160では、x=16に赤い塗りつぶしの正方形、x=224に赤い左半分のタイル、x=240に赤い塗りつぶしの正方形が現れます。最初の正方形は長さ0のコピーと埋め込みで消えず、中央は標準NMIでの実行、最後は単発書き込みを確認するものです。取り消したセル(248,160)は黒いままです。長い転送は描画とNMIを止めて実行し、最後の小さな書き込みだけをNMIに任せます。これはNROMのエミュレータ検証であり、実機検証ではありません。

__vramq_clear: NROM
NROM

使用例の完全なプログラム

ビルドコマンド

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\vramq_intrinsics.c -I .\kitaqfc\lib -I .\kitaq-docs\samples -o .\out\vramq_intrinsics.nes --mapper=nrom --nes-chr=.\kitaq-docs\samples\api-examples\fc\vram_shapes.chr --no-cache --no-disasm
実装とソース

kitaqfc/lib/intrinsics.h:146

宣言の注記(原文)
NMI VRAM transfer queue. Queue entries are consumed by __vramq_exec(),
and the default NMI handler calls __vramq_exec() automatically.

Reset pending intrinsic-queue records; this is separate from runtime.c nes_vram_queue_clear.
__vramq_clear_overflow

オーバーフロー通知だけを0に戻します。実行待ちの記録、長さ、準備完了フラグは変更しません。容量不足の通知を解除する操作であり、拒否された記録の再登録や、それ以前に成功した登録の取り消しは行いません。

書式

c
void __vramq_clear_overflow(void);

戻り値

ありません(void)。

使用条件と制限

FCの単位は固定128バイトの組み込みバッファーに格納する、コマンド記録のバイト数です。タイル1個の書き込みは4バイト、埋める記録は5バイト、ポインターを使うコピー記録は6バイトで、管理情報も含みます。準備済みキューはNMIで消費されるため、コミット前に使用量の確認と登録を行い、登録側と実行側を調整してください。

これらの組み込み関数は、lib/vram.cと同じコマンドバッファーを操作します。runtime.cのnes_vram_queue_*関数は、記録形式の異なる別のキューを管理します。一方のクリアや容量取得が、もう一方にも適用されるわけではありません。

キューの登録と実行は、互いに重ならないよう管理してください。割り込みで実行している途中の追加、管理変数の直接変更、保存した転送元データの意図しない上書きは避けてください。これらのAPIはロックやダブルバッファーを提供しません。容量はコマンドバッファーの容量であり、ハードウェアVRAMの空きバイト数ではありません。

使用例

c
__vramq_clear_overflow();
if (__vramq_overflow()!=0 || __vramq_len()!=4) failures++;
// Resetting the latch deliberately keeps this pending put record.

呼び出し部分のコード例です。完全なプログラムに組み込む際は、記載した初期化とバッファの条件を満たしてください。

期待する結果

完成プログラムでは、キュー容量、保持されるポインター、2種類のリセット、直接実行とNMI実行の違いを学べます。期待値はCAPACITY 128、ENCODED BYTES 030、AFTER EXEC 000、OVERFLOW SEEN 001、FAILED CHECKS 000です。6つの記録が30バイトを使い、512バイト分の書き込みを実行します。OVERFLOW SEENは意図的な登録拒否時に保存した値であり、終了時も通知が立っているという意味ではありません。

青いコピー領域はピクセル座標(0,16)、緑の埋め込み領域は(0,88)から始まります。それぞれ、幅256ピクセルのタイル行7行と、8行目の248ピクセルを描き、合計255タイルになります。最後の8×8セルは黒いままです。コピーは8×8の塗りつぶし・左半分・三角形を繰り返しますが、登録後に転送元を変えるため、先頭は三角形です。埋め込みは呼び出し元の変数を変えても塗りつぶしを保ちます。形と色の両方を検証します。

y=160では、x=16に赤い塗りつぶしの正方形、x=224に赤い左半分のタイル、x=240に赤い塗りつぶしの正方形が現れます。最初の正方形は長さ0のコピーと埋め込みで消えず、中央は標準NMIでの実行、最後は単発書き込みを確認するものです。取り消したセル(248,160)は黒いままです。長い転送は描画とNMIを止めて実行し、最後の小さな書き込みだけをNMIに任せます。これはNROMのエミュレータ検証であり、実機検証ではありません。

__vramq_clear_overflow: NROM
NROM

使用例の完全なプログラム

ビルドコマンド

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\vramq_intrinsics.c -I .\kitaqfc\lib -I .\kitaq-docs\samples -o .\out\vramq_intrinsics.nes --mapper=nrom --nes-chr=.\kitaq-docs\samples\api-examples\fc\vram_shapes.chr --no-cache --no-disasm
実装とソース

kitaqfc/lib/intrinsics.h:162

宣言の注記(原文)
Clear the overflow latch explicitly after handling the failed submission.
__vramq_commit

実行側がキューを処理できるよう、準備完了フラグを1にします。転送データのコピー、PPUへの書き込み、NMI待ちは行いません。コミット後は、実行が終わるまでキューと保持した転送元を変更しないでください。NMIが有効なら、標準ハンドラーが非同期に処理する場合があります。

書式

c
void __vramq_commit(void);

戻り値

ありません(void)。

使用条件と制限

FCの単位は固定128バイトの組み込みバッファーに格納する、コマンド記録のバイト数です。タイル1個の書き込みは4バイト、埋める記録は5バイト、ポインターを使うコピー記録は6バイトで、管理情報も含みます。準備済みキューはNMIで消費されるため、コミット前に使用量の確認と登録を行い、登録側と実行側を調整してください。

これらの組み込み関数は、lib/vram.cと同じコマンドバッファーを操作します。runtime.cのnes_vram_queue_*関数は、記録形式の異なる別のキューを管理します。一方のクリアや容量取得が、もう一方にも適用されるわけではありません。

キューの登録と実行は、互いに重ならないよう管理してください。割り込みで実行している途中の追加、管理変数の直接変更、保存した転送元データの意図しない上書きは避けてください。これらのAPIはロックやダブルバッファーを提供しません。容量はコマンドバッファーの容量であり、ハードウェアVRAMの空きバイト数ではありません。

使用例

c
__vramq_commit(); // NMI is disabled here: this only marks the queue ready.
if (__vramq_len()!=30) failures++;

呼び出し部分のコード例です。完全なプログラムに組み込む際は、記載した初期化とバッファの条件を満たしてください。

期待する結果

完成プログラムでは、キュー容量、保持されるポインター、2種類のリセット、直接実行とNMI実行の違いを学べます。期待値はCAPACITY 128、ENCODED BYTES 030、AFTER EXEC 000、OVERFLOW SEEN 001、FAILED CHECKS 000です。6つの記録が30バイトを使い、512バイト分の書き込みを実行します。OVERFLOW SEENは意図的な登録拒否時に保存した値であり、終了時も通知が立っているという意味ではありません。

青いコピー領域はピクセル座標(0,16)、緑の埋め込み領域は(0,88)から始まります。それぞれ、幅256ピクセルのタイル行7行と、8行目の248ピクセルを描き、合計255タイルになります。最後の8×8セルは黒いままです。コピーは8×8の塗りつぶし・左半分・三角形を繰り返しますが、登録後に転送元を変えるため、先頭は三角形です。埋め込みは呼び出し元の変数を変えても塗りつぶしを保ちます。形と色の両方を検証します。

y=160では、x=16に赤い塗りつぶしの正方形、x=224に赤い左半分のタイル、x=240に赤い塗りつぶしの正方形が現れます。最初の正方形は長さ0のコピーと埋め込みで消えず、中央は標準NMIでの実行、最後は単発書き込みを確認するものです。取り消したセル(248,160)は黒いままです。長い転送は描画とNMIを止めて実行し、最後の小さな書き込みだけをNMIに任せます。これはNROMのエミュレータ検証であり、実機検証ではありません。

__vramq_commit: NROM
NROM

使用例の完全なプログラム

ビルドコマンド

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\vramq_intrinsics.c -I .\kitaqfc\lib -I .\kitaq-docs\samples -o .\out\vramq_intrinsics.nes --mapper=nrom --nes-chr=.\kitaq-docs\samples\api-examples\fc\vram_shapes.chr --no-cache --no-disasm
実装とソース

kitaqfc/lib/intrinsics.h:154

宣言の注記(原文)
Publish the pending records to the configured consumer. Do not mutate them while consumption can run.
__vramq_copy

実行時にsrcからppu_addrへlenバイトをコピーする、6バイトの記録を1つ追加します。キューに保存するのは転送データではなく転送元ポインターです。255バイトのコピーでもコマンドバッファーの使用量は6バイトです。転送元のアドレスが増える順に読み、転送先を連続させるにはPPUの増分を1にします。

書式

c
void __vramq_copy(u16 ppu_addr, const u8* src, u8 len);

引数

ppu_addr

CPUポインターではなく、PPUのバイトアドレス(0x0000~0x3FFF)です。転送先は書き込み可能でなければなりません。パターンの変更にはCHR ROMではなくCHR RAMが必要です。PPUのマッピングとミラーリングが適用されます。連続したコピーや埋め込みではPPUCTRLの増分を1にしてください。安全にPPUへ書ける時期に実行し、その後で必要なスクロール状態を設定してください。実行処理はPPUADDRとPPUDATAを使い、スクロール設定を復元しません。

src

実行時まで保存する、読み取り可能なバイト列への近距離ポインターです。実行が終わるまで必要な領域を有効に保ち、そのメモリー・ROMバンクを読み取り可能にしてください。バンク番号やデータのコピーは保存しません。長さがある転送には有効な元データが必要です。有効期間が終わったローカル配列を使わないでください。

len

バイト数で、範囲は0~255です。1つの記録で全体を扱い、127バイトずつには分割しません。0でもコピーや埋め込みの記録を使います。実行時にはPPUアドレスを設定しますが、PPUDATAへの書き込みは行いません。256はこの引数の範囲外です。

戻り値

ありません(void)。

使用条件と制限

追加処理は成否の値を返しません。記録全体が収まらない場合は__vramq_overflow()を1にし、既存の記録と長さは保ちます。空き容量と記録サイズが等しい場合は登録できます。以前の通知が残っていても、空きがあれば追加できるため、新しい一連の登録を判断する前に通知を処理・解除してください。複数回の追加をまとめて取り消す仕組みはありません。

FCの単位は固定128バイトの組み込みバッファーに格納する、コマンド記録のバイト数です。タイル1個の書き込みは4バイト、埋める記録は5バイト、ポインターを使うコピー記録は6バイトで、管理情報も含みます。準備済みキューはNMIで消費されるため、コミット前に使用量の確認と登録を行い、登録側と実行側を調整してください。

これらの組み込み関数は、lib/vram.cと同じコマンドバッファーを操作します。runtime.cのnes_vram_queue_*関数は、記録形式の異なる別のキューを管理します。一方のクリアや容量取得が、もう一方にも適用されるわけではありません。

キューの登録と実行は、互いに重ならないよう管理してください。割り込みで実行している途中の追加、管理変数の直接変更、保存した転送元データの意図しない上書きは避けてください。これらのAPIはロックやダブルバッファーを提供しません。容量はコマンドバッファーの容量であり、ハードウェアVRAMの空きバイト数ではありません。

使用例

c
__vramq_copy(0x2040,source_bytes,255);
source_bytes[0]=3; // The executor must see this change through its retained pointer.

呼び出し部分のコード例です。完全なプログラムに組み込む際は、記載した初期化とバッファの条件を満たしてください。

期待する結果

完成プログラムでは、キュー容量、保持されるポインター、2種類のリセット、直接実行とNMI実行の違いを学べます。期待値はCAPACITY 128、ENCODED BYTES 030、AFTER EXEC 000、OVERFLOW SEEN 001、FAILED CHECKS 000です。6つの記録が30バイトを使い、512バイト分の書き込みを実行します。OVERFLOW SEENは意図的な登録拒否時に保存した値であり、終了時も通知が立っているという意味ではありません。

青いコピー領域はピクセル座標(0,16)、緑の埋め込み領域は(0,88)から始まります。それぞれ、幅256ピクセルのタイル行7行と、8行目の248ピクセルを描き、合計255タイルになります。最後の8×8セルは黒いままです。コピーは8×8の塗りつぶし・左半分・三角形を繰り返しますが、登録後に転送元を変えるため、先頭は三角形です。埋め込みは呼び出し元の変数を変えても塗りつぶしを保ちます。形と色の両方を検証します。

y=160では、x=16に赤い塗りつぶしの正方形、x=224に赤い左半分のタイル、x=240に赤い塗りつぶしの正方形が現れます。最初の正方形は長さ0のコピーと埋め込みで消えず、中央は標準NMIでの実行、最後は単発書き込みを確認するものです。取り消したセル(248,160)は黒いままです。長い転送は描画とNMIを止めて実行し、最後の小さな書き込みだけをNMIに任せます。これはNROMのエミュレータ検証であり、実機検証ではありません。

__vramq_copy: NROM
NROM

使用例の完全なプログラム

ビルドコマンド

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\vramq_intrinsics.c -I .\kitaqfc\lib -I .\kitaq-docs\samples -o .\out\vramq_intrinsics.nes --mapper=nrom --nes-chr=.\kitaq-docs\samples\api-examples\fc\vram_shapes.chr --no-cache --no-disasm
実装とソース

kitaqfc/lib/intrinsics.h:150

宣言の注記(原文)
Retain the source address for later consumption; keep its storage and bank mapping valid.
__vramq_exec

コミット済みのキューを、記録順に直ちに実行します。準備完了でなければキューを変更せずに戻ります。実行後は長さと準備完了フラグを消しますが、オーバーフロー通知は残します。安全なPPUアクセス時期の待機、転送量の制限、スクロールの復元は行わないため、呼び出し側で管理してください。

書式

c
void __vramq_exec(void);

戻り値

ありません(void)。

使用条件と制限

CPUポインターではなく、PPUのバイトアドレス(0x0000~0x3FFF)です。転送先は書き込み可能でなければなりません。パターンの変更にはCHR ROMではなくCHR RAMが必要です。PPUのマッピングとミラーリングが適用されます。連続したコピーや埋め込みではPPUCTRLの増分を1にしてください。安全にPPUへ書ける時期に実行し、その後で必要なスクロール状態を設定してください。実行処理はPPUADDRとPPUDATAを使い、スクロール設定を復元しません。

FCの単位は固定128バイトの組み込みバッファーに格納する、コマンド記録のバイト数です。タイル1個の書き込みは4バイト、埋める記録は5バイト、ポインターを使うコピー記録は6バイトで、管理情報も含みます。準備済みキューはNMIで消費されるため、コミット前に使用量の確認と登録を行い、登録側と実行側を調整してください。

これらの組み込み関数は、lib/vram.cと同じコマンドバッファーを操作します。runtime.cのnes_vram_queue_*関数は、記録形式の異なる別のキューを管理します。一方のクリアや容量取得が、もう一方にも適用されるわけではありません。

キューの登録と実行は、互いに重ならないよう管理してください。割り込みで実行している途中の追加、管理変数の直接変更、保存した転送元データの意図しない上書きは避けてください。これらのAPIはロックやダブルバッファーを提供しません。容量はコマンドバッファーの容量であり、ハードウェアVRAMの空きバイト数ではありません。

使用例

c
// Rendering is stopped and the PPU increment is 1.
__vramq_exec();
after_exec=__vramq_len();
if (after_exec!=0 || __vramq_overflow()!=0) failures++;

呼び出し部分のコード例です。完全なプログラムに組み込む際は、記載した初期化とバッファの条件を満たしてください。

期待する結果

完成プログラムでは、キュー容量、保持されるポインター、2種類のリセット、直接実行とNMI実行の違いを学べます。期待値はCAPACITY 128、ENCODED BYTES 030、AFTER EXEC 000、OVERFLOW SEEN 001、FAILED CHECKS 000です。6つの記録が30バイトを使い、512バイト分の書き込みを実行します。OVERFLOW SEENは意図的な登録拒否時に保存した値であり、終了時も通知が立っているという意味ではありません。

青いコピー領域はピクセル座標(0,16)、緑の埋め込み領域は(0,88)から始まります。それぞれ、幅256ピクセルのタイル行7行と、8行目の248ピクセルを描き、合計255タイルになります。最後の8×8セルは黒いままです。コピーは8×8の塗りつぶし・左半分・三角形を繰り返しますが、登録後に転送元を変えるため、先頭は三角形です。埋め込みは呼び出し元の変数を変えても塗りつぶしを保ちます。形と色の両方を検証します。

y=160では、x=16に赤い塗りつぶしの正方形、x=224に赤い左半分のタイル、x=240に赤い塗りつぶしの正方形が現れます。最初の正方形は長さ0のコピーと埋め込みで消えず、中央は標準NMIでの実行、最後は単発書き込みを確認するものです。取り消したセル(248,160)は黒いままです。長い転送は描画とNMIを止めて実行し、最後の小さな書き込みだけをNMIに任せます。これはNROMのエミュレータ検証であり、実機検証ではありません。

__vramq_exec: NROM
NROM

使用例の完全なプログラム

ビルドコマンド

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\vramq_intrinsics.c -I .\kitaqfc\lib -I .\kitaq-docs\samples -o .\out\vramq_intrinsics.nes --mapper=nrom --nes-chr=.\kitaq-docs\samples\api-examples\fc\vram_shapes.chr --no-cache --no-disasm
実装とソース

kitaqfc/lib/intrinsics.h:156

宣言の注記(原文)
Execute a ready queue directly; committing and safe PPU timing are caller responsibilities.
__vramq_fill

1バイトのvalueをPPUへlen回書く、5バイトの記録を1つ追加します。値は記録内へ保存されるため、呼び出し元の変数を後で変えても登録済みの値は変わりません。繰り返すのは1バイトであり、複数バイトのタイルパターンではありません。

書式

c
void __vramq_fill(u16 ppu_addr, u8 value, u8 len);

引数

ppu_addr

CPUポインターではなく、PPUのバイトアドレス(0x0000~0x3FFF)です。転送先は書き込み可能でなければなりません。パターンの変更にはCHR ROMではなくCHR RAMが必要です。PPUのマッピングとミラーリングが適用されます。連続したコピーや埋め込みではPPUCTRLの増分を1にしてください。安全にPPUへ書ける時期に実行し、その後で必要なスクロール状態を設定してください。実行処理はPPUADDRとPPUDATAを使い、スクロール設定を復元しません。

value

0〜255の1バイトを繰り返し書きます。画面での意味は、転送先がタイル番号、画素のビットプレーン、その他のバイト列のどれかによって変わります。

len

バイト数で、範囲は0~255です。1つの記録で全体を扱い、127バイトずつには分割しません。0でもコピーや埋め込みの記録を使います。実行時にはPPUアドレスを設定しますが、PPUDATAへの書き込みは行いません。256はこの引数の範囲外です。

戻り値

ありません(void)。

使用条件と制限

追加処理は成否の値を返しません。記録全体が収まらない場合は__vramq_overflow()を1にし、既存の記録と長さは保ちます。空き容量と記録サイズが等しい場合は登録できます。以前の通知が残っていても、空きがあれば追加できるため、新しい一連の登録を判断する前に通知を処理・解除してください。複数回の追加をまとめて取り消す仕組みはありません。

FCの単位は固定128バイトの組み込みバッファーに格納する、コマンド記録のバイト数です。タイル1個の書き込みは4バイト、埋める記録は5バイト、ポインターを使うコピー記録は6バイトで、管理情報も含みます。準備済みキューはNMIで消費されるため、コミット前に使用量の確認と登録を行い、登録側と実行側を調整してください。

これらの組み込み関数は、lib/vram.cと同じコマンドバッファーを操作します。runtime.cのnes_vram_queue_*関数は、記録形式の異なる別のキューを管理します。一方のクリアや容量取得が、もう一方にも適用されるわけではありません。

キューの登録と実行は、互いに重ならないよう管理してください。割り込みで実行している途中の追加、管理変数の直接変更、保存した転送元データの意図しない上書きは避けてください。これらのAPIはロックやダブルバッファーを提供しません。容量はコマンドバッファーの容量であり、ハードウェアVRAMの空きバイト数ではありません。

使用例

c
__vramq_fill(0x2160,fill_value,255); // Seven complete rows and 31 tiles in the eighth.
fill_value=3; // The queued value stays 1, so the green block remains solid.

呼び出し部分のコード例です。完全なプログラムに組み込む際は、記載した初期化とバッファの条件を満たしてください。

期待する結果

完成プログラムでは、キュー容量、保持されるポインター、2種類のリセット、直接実行とNMI実行の違いを学べます。期待値はCAPACITY 128、ENCODED BYTES 030、AFTER EXEC 000、OVERFLOW SEEN 001、FAILED CHECKS 000です。6つの記録が30バイトを使い、512バイト分の書き込みを実行します。OVERFLOW SEENは意図的な登録拒否時に保存した値であり、終了時も通知が立っているという意味ではありません。

青いコピー領域はピクセル座標(0,16)、緑の埋め込み領域は(0,88)から始まります。それぞれ、幅256ピクセルのタイル行7行と、8行目の248ピクセルを描き、合計255タイルになります。最後の8×8セルは黒いままです。コピーは8×8の塗りつぶし・左半分・三角形を繰り返しますが、登録後に転送元を変えるため、先頭は三角形です。埋め込みは呼び出し元の変数を変えても塗りつぶしを保ちます。形と色の両方を検証します。

y=160では、x=16に赤い塗りつぶしの正方形、x=224に赤い左半分のタイル、x=240に赤い塗りつぶしの正方形が現れます。最初の正方形は長さ0のコピーと埋め込みで消えず、中央は標準NMIでの実行、最後は単発書き込みを確認するものです。取り消したセル(248,160)は黒いままです。長い転送は描画とNMIを止めて実行し、最後の小さな書き込みだけをNMIに任せます。これはNROMのエミュレータ検証であり、実機検証ではありません。

__vramq_fill: NROM
NROM

使用例の完全なプログラム

ビルドコマンド

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\vramq_intrinsics.c -I .\kitaqfc\lib -I .\kitaq-docs\samples -o .\out\vramq_intrinsics.nes --mapper=nrom --nes-chr=.\kitaq-docs\samples\api-examples\fc\vram_shapes.chr --no-cache --no-disasm
実装とソース

kitaqfc/lib/intrinsics.h:152

宣言の注記(原文)
Append a repeated-byte update; the source value is stored in the record.
__vramq_len

実行待ちのコマンドが現在占めている、コマンドバッファーの使用量を返します。ビデオメモリーへ書き込むデータのバイト数ではありません。

書式

c
u8 __vramq_len(void);

戻り値

ここで説明した各機種の単位による符号なし8ビット値です。読み取るだけではキューを変更・実行しません。

使用条件と制限

FCの単位は固定128バイトの組み込みバッファーに格納する、コマンド記録のバイト数です。タイル1個の書き込みは4バイト、埋める記録は5バイト、ポインターを使うコピー記録は6バイトで、管理情報も含みます。準備済みキューはNMIで消費されるため、コミット前に使用量の確認と登録を行い、登録側と実行側を調整してください。

これらの組み込み関数は、lib/vram.cと同じコマンドバッファーを操作します。runtime.cのnes_vram_queue_*関数は、記録形式の異なる別のキューを管理します。一方のクリアや容量取得が、もう一方にも適用されるわけではありません。

キューの登録と実行は、互いに重ならないよう管理してください。割り込みで実行している途中の追加、管理変数の直接変更、保存した転送元データの意図しない上書きは避けてください。これらのAPIはロックやダブルバッファーを提供しません。容量はコマンドバッファーの容量であり、ハードウェアVRAMの空きバイト数ではありません。

使用例

c
encoded=__vramq_len();
if (encoded!=30 || capacity-encoded!=98) failures++;
// 4+6+5+6+5+4 encoded bytes describe 512 actual byte writes.

呼び出し部分のコード例です。完全なプログラムに組み込む際は、記載した初期化とバッファの条件を満たしてください。

期待する結果

完成プログラムでは、キュー容量、保持されるポインター、2種類のリセット、直接実行とNMI実行の違いを学べます。期待値はCAPACITY 128、ENCODED BYTES 030、AFTER EXEC 000、OVERFLOW SEEN 001、FAILED CHECKS 000です。6つの記録が30バイトを使い、512バイト分の書き込みを実行します。OVERFLOW SEENは意図的な登録拒否時に保存した値であり、終了時も通知が立っているという意味ではありません。

青いコピー領域はピクセル座標(0,16)、緑の埋め込み領域は(0,88)から始まります。それぞれ、幅256ピクセルのタイル行7行と、8行目の248ピクセルを描き、合計255タイルになります。最後の8×8セルは黒いままです。コピーは8×8の塗りつぶし・左半分・三角形を繰り返しますが、登録後に転送元を変えるため、先頭は三角形です。埋め込みは呼び出し元の変数を変えても塗りつぶしを保ちます。形と色の両方を検証します。

y=160では、x=16に赤い塗りつぶしの正方形、x=224に赤い左半分のタイル、x=240に赤い塗りつぶしの正方形が現れます。最初の正方形は長さ0のコピーと埋め込みで消えず、中央は標準NMIでの実行、最後は単発書き込みを確認するものです。取り消したセル(248,160)は黒いままです。長い転送は描画とNMIを止めて実行し、最後の小さな書き込みだけをNMIに任せます。これはNROMのエミュレータ検証であり、実機検証ではありません。

__vramq_len: NROM
NROM

使用例の完全なプログラム

ビルドコマンド

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\vramq_intrinsics.c -I .\kitaqfc\lib -I .\kitaq-docs\samples -o .\out\vramq_intrinsics.nes --mapper=nrom --nes-chr=.\kitaq-docs\samples\api-examples\fc\vram_shapes.chr --no-cache --no-disasm
実装とソース

kitaqfc/lib/intrinsics.h:158

宣言の注記(原文)
Return encoded bytes currently occupied, not transfer payload bytes or command count.
__vramq_overflow

キューあふれフラグを消さずに読み取ります。過去の登録失敗が記録されたままの場合があります。

書式

c
u8 __vramq_overflow(void);

戻り値

オーバーフローの記録がなければ0です。登録を拒否すると1になり、明示的にリセットするまで残ります。キューを実行してもこの記録は消えません。

使用条件と制限

FCの単位は固定128バイトの組み込みバッファーに格納する、コマンド記録のバイト数です。タイル1個の書き込みは4バイト、埋める記録は5バイト、ポインターを使うコピー記録は6バイトで、管理情報も含みます。準備済みキューはNMIで消費されるため、コミット前に使用量の確認と登録を行い、登録側と実行側を調整してください。

これらの組み込み関数は、lib/vram.cと同じコマンドバッファーを操作します。runtime.cのnes_vram_queue_*関数は、記録形式の異なる別のキューを管理します。一方のクリアや容量取得が、もう一方にも適用されるわけではありません。

キューの登録と実行は、互いに重ならないよう管理してください。割り込みで実行している途中の追加、管理変数の直接変更、保存した転送元データの意図しない上書きは避けてください。これらのAPIはロックやダブルバッファーを提供しません。容量はコマンドバッファーの容量であり、ハードウェアVRAMの空きバイト数ではありません。

使用例

c
overflow_seen=__vramq_overflow();
if (overflow_seen!=1 || __vramq_overflow()!=1) failures++;

呼び出し部分のコード例です。完全なプログラムに組み込む際は、記載した初期化とバッファの条件を満たしてください。

期待する結果

完成プログラムでは、キュー容量、保持されるポインター、2種類のリセット、直接実行とNMI実行の違いを学べます。期待値はCAPACITY 128、ENCODED BYTES 030、AFTER EXEC 000、OVERFLOW SEEN 001、FAILED CHECKS 000です。6つの記録が30バイトを使い、512バイト分の書き込みを実行します。OVERFLOW SEENは意図的な登録拒否時に保存した値であり、終了時も通知が立っているという意味ではありません。

青いコピー領域はピクセル座標(0,16)、緑の埋め込み領域は(0,88)から始まります。それぞれ、幅256ピクセルのタイル行7行と、8行目の248ピクセルを描き、合計255タイルになります。最後の8×8セルは黒いままです。コピーは8×8の塗りつぶし・左半分・三角形を繰り返しますが、登録後に転送元を変えるため、先頭は三角形です。埋め込みは呼び出し元の変数を変えても塗りつぶしを保ちます。形と色の両方を検証します。

y=160では、x=16に赤い塗りつぶしの正方形、x=224に赤い左半分のタイル、x=240に赤い塗りつぶしの正方形が現れます。最初の正方形は長さ0のコピーと埋め込みで消えず、中央は標準NMIでの実行、最後は単発書き込みを確認するものです。取り消したセル(248,160)は黒いままです。長い転送は描画とNMIを止めて実行し、最後の小さな書き込みだけをNMIに任せます。これはNROMのエミュレータ検証であり、実機検証ではありません。

__vramq_overflow: NROM
NROM

使用例の完全なプログラム

ビルドコマンド

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\vramq_intrinsics.c -I .\kitaqfc\lib -I .\kitaq-docs\samples -o .\out\vramq_intrinsics.nes --mapper=nrom --nes-chr=.\kitaq-docs\samples\api-examples\fc\vram_shapes.chr --no-cache --no-disasm
実装とソース

kitaqfc/lib/intrinsics.h:160

宣言の注記(原文)
Read the latched capacity failure; reading does not reset it.
__vramq_put

PPUアドレスと1バイトの値を、4バイトの記録として追加します。値はその場でキューへコピーされ、実行時にPPUへ書かれます。ネームテーブルのタイル1個の更新に使えるほか、書き込み可能な他のPPUバイトも指定できます。

書式

c
void __vramq_put(u16 ppu_addr, u8 value);

引数

ppu_addr

CPUポインターではなく、PPUのバイトアドレス(0x0000~0x3FFF)です。転送先は書き込み可能でなければなりません。パターンの変更にはCHR ROMではなくCHR RAMが必要です。PPUのマッピングとミラーリングが適用されます。連続したコピーや埋め込みではPPUCTRLの増分を1にしてください。安全にPPUへ書ける時期に実行し、その後で必要なスクロール状態を設定してください。実行処理はPPUADDRとPPUDATAを使い、スクロール設定を復元しません。

value

0〜255の1バイトを繰り返し書きます。画面での意味は、転送先がタイル番号、画素のビットプレーン、その他のバイト列のどれかによって変わります。

戻り値

ありません(void)。

使用条件と制限

追加処理は成否の値を返しません。記録全体が収まらない場合は__vramq_overflow()を1にし、既存の記録と長さは保ちます。空き容量と記録サイズが等しい場合は登録できます。以前の通知が残っていても、空きがあれば追加できるため、新しい一連の登録を判断する前に通知を処理・解除してください。複数回の追加をまとめて取り消す仕組みはありません。

FCの単位は固定128バイトの組み込みバッファーに格納する、コマンド記録のバイト数です。タイル1個の書き込みは4バイト、埋める記録は5バイト、ポインターを使うコピー記録は6バイトで、管理情報も含みます。準備済みキューはNMIで消費されるため、コミット前に使用量の確認と登録を行い、登録側と実行側を調整してください。

これらの組み込み関数は、lib/vram.cと同じコマンドバッファーを操作します。runtime.cのnes_vram_queue_*関数は、記録形式の異なる別のキューを管理します。一方のクリアや容量取得が、もう一方にも適用されるわけではありません。

キューの登録と実行は、互いに重ならないよう管理してください。割り込みで実行している途中の追加、管理変数の直接変更、保存した転送元データの意図しない上書きは避けてください。これらのAPIはロックやダブルバッファーを提供しません。容量はコマンドバッファーの容量であり、ハードウェアVRAMの空きバイト数ではありません。

使用例

c
__vramq_put(0x2282,1); // Red 8x8 sentinel at pixel (16,160).

呼び出し部分のコード例です。完全なプログラムに組み込む際は、記載した初期化とバッファの条件を満たしてください。

期待する結果

完成プログラムでは、キュー容量、保持されるポインター、2種類のリセット、直接実行とNMI実行の違いを学べます。期待値はCAPACITY 128、ENCODED BYTES 030、AFTER EXEC 000、OVERFLOW SEEN 001、FAILED CHECKS 000です。6つの記録が30バイトを使い、512バイト分の書き込みを実行します。OVERFLOW SEENは意図的な登録拒否時に保存した値であり、終了時も通知が立っているという意味ではありません。

青いコピー領域はピクセル座標(0,16)、緑の埋め込み領域は(0,88)から始まります。それぞれ、幅256ピクセルのタイル行7行と、8行目の248ピクセルを描き、合計255タイルになります。最後の8×8セルは黒いままです。コピーは8×8の塗りつぶし・左半分・三角形を繰り返しますが、登録後に転送元を変えるため、先頭は三角形です。埋め込みは呼び出し元の変数を変えても塗りつぶしを保ちます。形と色の両方を検証します。

y=160では、x=16に赤い塗りつぶしの正方形、x=224に赤い左半分のタイル、x=240に赤い塗りつぶしの正方形が現れます。最初の正方形は長さ0のコピーと埋め込みで消えず、中央は標準NMIでの実行、最後は単発書き込みを確認するものです。取り消したセル(248,160)は黒いままです。長い転送は描画とNMIを止めて実行し、最後の小さな書き込みだけをNMIに任せます。これはNROMのエミュレータ検証であり、実機検証ではありません。

__vramq_put: NROM
NROM

使用例の完全なプログラム

ビルドコマンド

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\vramq_intrinsics.c -I .\kitaqfc\lib -I .\kitaq-docs\samples -o .\out\vramq_intrinsics.nes --mapper=nrom --nes-chr=.\kitaq-docs\samples\api-examples\fc\vram_shapes.chr --no-cache --no-disasm
実装とソース

kitaqfc/lib/intrinsics.h:148

宣言の注記(原文)
Append one address/value update; use the overflow latch to detect capacity failure.
__xy_in_rect

点が左上を含み右端・下端を含まない矩形内にあるか調べます。

書式

c
u8 __xy_in_rect(u8 x, u8 y, u8 rx, u8 ry, u8 rw, u8 rh);

引数

x

点のX座標0~255。

y

点のY座標0~255。

rx

矩形左端0~255。

ry

矩形上端0~255。

rw

幅0~255。

rh

高さ0~255。

戻り値

内部なら1、それ以外は0。幅または高さ0は空です。

使用条件と制限

差分で比較するのでrx+rwが255を超えても左端へ周回しません。

使用例

c
result[0]=__xy_in_rect(10,20,10,20,3,2);result[1]=__xy_in_rect(13,20,10,20,3,2);result[2]=__xy_in_rect(9,20,10,20,3,2);result[3]=__xy_in_rect(255,1,250,0,10,2);result[4]=__xy_in_rect(10,20,10,20,0,2);

呼び出し部分のコード例です。完全なプログラムに組み込む際は、記載した初期化とバッファの条件を満たしてください。

期待する結果

矩形の内部・端・外側・幅0を試します。INSIDE=1、RIGHT EDGE=0、LEFT EDGE=0、HIGH X=1です。距離15、距離の周回254、配列位置64と65280を確認します。

__xy_in_rect: NROM
NROM

使用例の完全なプログラム

ビルドコマンド

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\batch3_coordinates.c -I .\kitaqfc\lib -I .\kitaq-docs\samples -o .\out\batch3_coordinates.nes --no-cache --no-disasm --mapper=nrom --nes-chr=.\kitaq-docs\samples\font.chr
実装とソース

kitaqfc/lib/intrinsics.h:31

宣言の注記(原文)
Return 0 or 1 for half-open bounds. Zero extents are empty; coordinates do not wrap across 255.

kitaqfc/kitaqfc/CodeGenerator.cs:4627

csharp
case "__xy_in_rect":
                    return EmitKitaqfcHelperCall(funcName, args, ctx, new[] {1,1,1,1,1,1}, "__xy_in_rect");

void EmitHelperGeometryAndRng()
        {
            EmitHelperStart("__xy_in_rect");
            string xyFalse = NewGeneratedLabel("xy_in_rect_false");
            string xyDone = NewGeneratedLabel("xy_in_rect_done");
            // return (x-rx < rw) && (y-ry < rh), using unsigned subtract to avoid rx+rw overflow.
            EmitAsm("LDA", Mem(CallArgBase));
            EmitAsm("SEC");
            EmitAsm("SBC", Mem(CallArgBase + 2));
            EmitAsm("BCC", Rel(xyFalse));
            EmitAsm("CMP", Mem(CallArgBase + 4));
            EmitAsm("BCS", Rel(xyFalse));
            EmitAsm("LDA", Mem(CallArgBase + 1));
            EmitAsm("SEC");
            EmitAsm("SBC", Mem(CallArgBase + 3));
            EmitAsm("BCC", Rel(xyFalse));
            EmitAsm("CMP", Mem(CallArgBase + 5));
            EmitAsm("BCS", Rel(xyFalse));
            EmitAsm("LDA", Imm(1));
            EmitAsm("JMP", Abs(xyDone));
            _assembly.Add(Expr.Make(Tag.Label, xyFalse));
            EmitAsm("LDA", Imm(0));
            _assembly.Add(Expr.Make(Tag.Label, xyDone));
            EmitAsm("RTS");

            EmitHelperStart("__manhattan");
            // dx = abs(x1-x2)
            EmitAsm("LDA", Mem(CallArgBase));
            EmitAsm("SEC");
            EmitAsm("SBC", Mem(CallArgBase + 2));
            string mdxOk = NewGeneratedLabel("manhattan_dx_ok");
            EmitAsm("BCS", Rel(mdxOk));
            EmitAsm("EOR", Imm(0xFF));
            EmitAsm("CLC");
            EmitAsm("ADC", Imm(1));
            _assembly.Add(Expr.Make(Tag.Label, mdxOk));
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp0Address));
            // dy = abs(y1-y2), then add dx.
            EmitAsm("LDA", Mem(CallArgBase + 1));
            EmitAsm("SEC");
            EmitAsm("SBC", Mem(CallArgBase + 3));
            string mdyOk = NewGeneratedLabel("manhattan_dy_ok");
            EmitAsm("BCS", Rel(mdyOk));
            EmitAsm("EOR", Imm(0xFF));
            EmitAsm("CLC");
            EmitAsm("ADC", Imm(1));
            _assembly.Add(Expr.Make(Tag.Label, mdyOk));
            EmitAsm("CLC");
            EmitAsm("ADC", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("RTS");

            // Save X-coordinate separately while multiplying width by Y, then add X with carry into the high byte.
            EmitHelperStart("__map_index");
            // args: x, y, width. Return y * width + x as u16.
            EmitAsm("LDA", Mem(CallArgBase));      // x
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp2Address));
            EmitAsm("LDA", Mem(CallArgBase + 1));  // y
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp1Address));
            EmitAsm("LDA", Mem(CallArgBase + 2));  // width as u16 multiplicand
            EmitAsm("STA", Mem(CallArgBase));
            EmitAsm("LDA", Imm(0));
            EmitAsm("STA", Mem(CallArgBase + 1));
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp1Address));
            EmitAsm("STA", Mem(CallArgBase + 2));
            EmitAsm("JSR", Abs("__mul16x8"));
            EmitAsm("CLC");
            EmitAsm("ADC", Mem(_runtimeIntrinsicTmp2Address));
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("TXA");
            EmitAsm("ADC", Imm(0));
            EmitAsm("TAX");
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("RTS");

            // Replace both bytes of the persistent generator state with the supplied seed.
            EmitHelperStart("__rng_seed");
            EmitAsm("LDA", Mem(CallArgBase));
            EmitAsm("STA", Mem(_runtimeRngLoAddress));
            EmitAsm("LDA", Mem(CallArgBase + 1));
            EmitAsm("STA", Mem(_runtimeRngHiAddress));
            EmitAsm("RTS");

            EmitHelperStart("__rng8");
            // 16-bit Galois LFSR: if low bit set, shift right then xor $B400.
            EmitAsm("LDA", Mem(_runtimeRngLoAddress));
            EmitAsm("ORA", Mem(_runtimeRngHiAddress));
            // Replace the all-zero LFSR state with a deterministic nonzero seed before advancing.
            string seeded = NewGeneratedLabel("rng_seeded");
            EmitAsm("BNE", Rel(seeded));
            EmitAsm("LDA", Imm(0x5A));
            EmitAsm("STA", Mem(_runtimeRngLoAddress));
            EmitAsm("LDA", Imm(0xA5));
            EmitAsm("STA", Mem(_runtimeRngHiAddress));
            _assembly.Add(Expr.Make(Tag.Label, seeded));
            EmitAsm("LDA", Mem(_runtimeRngLoAddress));
            EmitAsm("AND", Imm(1));
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("LSR", Mem(_runtimeRngHiAddress));
            EmitAsm("ROR", Mem(_runtimeRngLoAddress));
            string rngNoXor = NewGeneratedLabel("rng_no_xor");
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("BEQ", Rel(rngNoXor));
            EmitAsm("LDA", Mem(_runtimeRngHiAddress));
            EmitAsm("EOR", Imm(0xB4));
            EmitAsm("STA", Mem(_runtimeRngHiAddress));
            _assembly.Add(Expr.Make(Tag.Label, rngNoXor));
            EmitAsm("LDA", Mem(_runtimeRngLoAddress));
            EmitAsm("EOR", Mem(_runtimeRngHiAddress));
            EmitAsm("RTS");
        }
__zapper_light

ポート2を既定とする受光読み取り名です。__zapper_light2()と同じ処理を呼びます。

書式

c
u8 __zapper_light(void);

戻り値

$4017のビット3が0なら1、それ以外は0。ポート1の値は読みません。

使用条件と制限

これらは$4016/$4017を直接操作します。通常のパッド、キーボード、シリアル送信など、同じポートを使う処理を割り込みで混在させないでください。パッドのシリアル読み取り位置にも影響します。サンプルは入力の採取中だけNMIを止め、表示を初期化してから再開します。

呼び出した瞬間の信号を読みます。ストローブ、フレーム待ち、前回との差分判定、照準座標や命中判定は行いません。rawとtrigger/lightを別々に呼ぶと、それぞれ別の時刻にレジスターを読むため、信号が変化中なら一致しない場合があります。

受光線はアクティブローです。ビット3が0なので戻り値1になる、という処理であり、装置の存在を検出した結果ではありません。通常のパッドや未接続の入力でも1になり得ます。画面に的を描くだけで命中を判定するAPIではありません。

ビット変換は、独自の検証ROMでレジスター読み取り先だけをRAMの模擬値へ置き換え、00/08/10/18の組み合わせと無関係な上位ビットを入力して確認しました。この単体検証は、配布サンプルの通常実行および周辺機器の実動作とは別です。

掲載画像は配布サンプルを変更せずにKUROSAKIで実行した結果です。現在のKUROSAKIはパッドの入力モデルを持ちますが、キーボード・光線銃・シリアルアダプターの信号はモデル化していません。外部入力ビットが0になるため、画像は周辺機器の動作確認ではありません。実機のキー入力、受光、通信は未検証です。

NESdev: Zapper

使用例

c
#include "fc_ports_scene.h"
void main(void){
    __irq_disable();__ppu_ctrl_set(0);port_phase=1;
    port_result[0]=__zapper_raw1();port_result[1]=__zapper_trigger1();port_result[2]=__zapper_light1();
    port_result[3]=__zapper_raw2();port_result[4]=__zapper_trigger2();port_result[5]=__zapper_light2();
    port_result[6]=__zapper_trigger();port_result[7]=__zapper_light();
    port_phase=2;port_result[31]=165;
    m_init();m_text(2,2,"LIGHT-GUN INPUT SNAPSHOT");m_wait();
    m_text(2,5,"PORT   RAW  TRIGGER  LIGHT");m_wait();
    m_put(3,8,'1');port_hex(9,8,port_result[0]);port_number(15,8,port_result[1]);port_number(24,8,port_result[2]);m_wait();
    m_put(3,10,'2');port_hex(9,10,port_result[3]);port_number(15,10,port_result[4]);port_number(24,10,port_result[5]);m_wait();
    m_text(2,13,"DEFAULT = PORT 2");m_wait();
    m_text(2,15,"TRIGGER");port_number(15,15,port_result[6]);m_wait();
    m_text(2,17,"LIGHT");port_number(15,17,port_result[7]);m_wait();
    m_text(2,21,"LIGHT 1 IS NOT A HIT TEST");m_wait();
    while(1){m_wait();}
}

呼び出し部分のコード例です。完全なプログラムに組み込む際は、記載した初期化とバッファの条件を満たしてください。

期待する結果

KUROSAKIでは両ポートともRAW=00、TRIGGER=000、LIGHT=001です。番号なし関数もポート2と同じ000/001になります。LIGHT=001は未モデル化の入力ビット3が0だからで、画面の光を検知した証拠ではありません。画面下にも「LIGHT 1 IS NOT A HIT TEST」と表示します。

__zapper_light: ZAPPER-DEFAULT — KUROSAKIでは両ポートともRAW=00、TRIGGER=000、LIGHT=001です。番号なし関数もポート2と同じ000/001になります。LIGHT=001は未モデル化の入力ビット3が0だからで、画面の光を検知した証拠ではありません。画面下にも「LIGHT 1 IS NOT A HIT TEST」と表示します。
ZAPPER-DEFAULT — KUROSAKIでは両ポートともRAW=00、TRIGGER=000、LIGHT=001です。番号なし関数もポート2と同じ000/001になります。LIGHT=001は未モデル化の入力ビット3が0だからで、画面の光を検知した証拠ではありません。画面下にも「LIGHT 1 IS NOT A HIT TEST」と表示します。

使用例の完全なプログラム

ビルドコマンド

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\peripheral_zapper.c -I .\kitaqfc\lib -I .\kitaq-docs\samples -o .\out\peripheral_zapper.nes --mapper=nrom --nes-chr=.\kitaq-docs\samples\font.chr --no-cache --no-disasm
.\kurosaki\kurosaki.exe run .\out\peripheral_zapper.nes --frames 120 --png .\out\peripheral_zapper.png
実装とソース

kitaqfc/lib/intrinsics.h:253

宣言の注記(原文)
Default to the port-2 normalized light reader.

kitaqfc/kitaqfc/CodeGenerator.cs:7753

csharp
EmitHelperStart("__zapper_light2");
            EmitNormalizeBoolFromMask(0x4017, 0x08, true);
        void EmitNormalizeBoolFromMask(int port, int mask, bool invert)
        {
            string zero = NewGeneratedLabel("bool_zero");
            string done = NewGeneratedLabel("bool_done");
            EmitAsm("LDA", Mem(port));
            EmitAsm("AND", Imm(mask));
            if (invert) EmitAsm("BNE", Rel(zero)); else EmitAsm("BEQ", Rel(zero));
            EmitAsm("LDA", Imm(1));
            EmitAsm("JMP", Abs(done));
            _assembly.Add(Expr.Make(Tag.Label, zero));
            EmitAsm("LDA", Imm(0));
            _assembly.Add(Expr.Make(Tag.Label, done));
            EmitAsm("RTS");
        }
__zapper_light1

ポート1($4016)の受光線を、受光なら1という表記へ変換して読みます。

書式

c
u8 __zapper_light1(void);

戻り値

$4016のビット3が0なら1、1なら0です。

使用条件と制限

これらは$4016/$4017を直接操作します。通常のパッド、キーボード、シリアル送信など、同じポートを使う処理を割り込みで混在させないでください。パッドのシリアル読み取り位置にも影響します。サンプルは入力の採取中だけNMIを止め、表示を初期化してから再開します。

呼び出した瞬間の信号を読みます。ストローブ、フレーム待ち、前回との差分判定、照準座標や命中判定は行いません。rawとtrigger/lightを別々に呼ぶと、それぞれ別の時刻にレジスターを読むため、信号が変化中なら一致しない場合があります。

受光線はアクティブローです。ビット3が0なので戻り値1になる、という処理であり、装置の存在を検出した結果ではありません。通常のパッドや未接続の入力でも1になり得ます。画面に的を描くだけで命中を判定するAPIではありません。

ビット変換は、独自の検証ROMでレジスター読み取り先だけをRAMの模擬値へ置き換え、00/08/10/18の組み合わせと無関係な上位ビットを入力して確認しました。この単体検証は、配布サンプルの通常実行および周辺機器の実動作とは別です。

掲載画像は配布サンプルを変更せずにKUROSAKIで実行した結果です。現在のKUROSAKIはパッドの入力モデルを持ちますが、キーボード・光線銃・シリアルアダプターの信号はモデル化していません。外部入力ビットが0になるため、画像は周辺機器の動作確認ではありません。実機のキー入力、受光、通信は未検証です。

NESdev: Zapper

使用例

c
#include "fc_ports_scene.h"
void main(void){
    __irq_disable();__ppu_ctrl_set(0);port_phase=1;
    port_result[0]=__zapper_raw1();port_result[1]=__zapper_trigger1();port_result[2]=__zapper_light1();
    port_result[3]=__zapper_raw2();port_result[4]=__zapper_trigger2();port_result[5]=__zapper_light2();
    port_result[6]=__zapper_trigger();port_result[7]=__zapper_light();
    port_phase=2;port_result[31]=165;
    m_init();m_text(2,2,"LIGHT-GUN INPUT SNAPSHOT");m_wait();
    m_text(2,5,"PORT   RAW  TRIGGER  LIGHT");m_wait();
    m_put(3,8,'1');port_hex(9,8,port_result[0]);port_number(15,8,port_result[1]);port_number(24,8,port_result[2]);m_wait();
    m_put(3,10,'2');port_hex(9,10,port_result[3]);port_number(15,10,port_result[4]);port_number(24,10,port_result[5]);m_wait();
    m_text(2,13,"DEFAULT = PORT 2");m_wait();
    m_text(2,15,"TRIGGER");port_number(15,15,port_result[6]);m_wait();
    m_text(2,17,"LIGHT");port_number(15,17,port_result[7]);m_wait();
    m_text(2,21,"LIGHT 1 IS NOT A HIT TEST");m_wait();
    while(1){m_wait();}
}

呼び出し部分のコード例です。完全なプログラムに組み込む際は、記載した初期化とバッファの条件を満たしてください。

期待する結果

KUROSAKIでは両ポートともRAW=00、TRIGGER=000、LIGHT=001です。番号なし関数もポート2と同じ000/001になります。LIGHT=001は未モデル化の入力ビット3が0だからで、画面の光を検知した証拠ではありません。画面下にも「LIGHT 1 IS NOT A HIT TEST」と表示します。

__zapper_light1: ZAPPER-DEFAULT — KUROSAKIでは両ポートともRAW=00、TRIGGER=000、LIGHT=001です。番号なし関数もポート2と同じ000/001になります。LIGHT=001は未モデル化の入力ビット3が0だからで、画面の光を検知した証拠ではありません。画面下にも「LIGHT 1 IS NOT A HIT TEST」と表示します。
ZAPPER-DEFAULT — KUROSAKIでは両ポートともRAW=00、TRIGGER=000、LIGHT=001です。番号なし関数もポート2と同じ000/001になります。LIGHT=001は未モデル化の入力ビット3が0だからで、画面の光を検知した証拠ではありません。画面下にも「LIGHT 1 IS NOT A HIT TEST」と表示します。

使用例の完全なプログラム

ビルドコマンド

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\peripheral_zapper.c -I .\kitaqfc\lib -I .\kitaq-docs\samples -o .\out\peripheral_zapper.nes --mapper=nrom --nes-chr=.\kitaq-docs\samples\font.chr --no-cache --no-disasm
.\kurosaki\kurosaki.exe run .\out\peripheral_zapper.nes --frames 120 --png .\out\peripheral_zapper.png
実装とソース

kitaqfc/lib/intrinsics.h:247

宣言の注記(原文)
Invert port-1 bit 3 and normalize: one means light detected.

kitaqfc/kitaqfc/CodeGenerator.cs:7751

csharp
EmitHelperStart("__zapper_light1");
            EmitNormalizeBoolFromMask(0x4016, 0x08, true);
        void EmitNormalizeBoolFromMask(int port, int mask, bool invert)
        {
            string zero = NewGeneratedLabel("bool_zero");
            string done = NewGeneratedLabel("bool_done");
            EmitAsm("LDA", Mem(port));
            EmitAsm("AND", Imm(mask));
            if (invert) EmitAsm("BNE", Rel(zero)); else EmitAsm("BEQ", Rel(zero));
            EmitAsm("LDA", Imm(1));
            EmitAsm("JMP", Abs(done));
            _assembly.Add(Expr.Make(Tag.Label, zero));
            EmitAsm("LDA", Imm(0));
            _assembly.Add(Expr.Make(Tag.Label, done));
            EmitAsm("RTS");
        }
__zapper_light2

ポート2($4017)の受光線を、受光なら1という表記へ変換して読みます。

書式

c
u8 __zapper_light2(void);

戻り値

$4017のビット3が0なら1、1なら0です。

使用条件と制限

これらは$4016/$4017を直接操作します。通常のパッド、キーボード、シリアル送信など、同じポートを使う処理を割り込みで混在させないでください。パッドのシリアル読み取り位置にも影響します。サンプルは入力の採取中だけNMIを止め、表示を初期化してから再開します。

呼び出した瞬間の信号を読みます。ストローブ、フレーム待ち、前回との差分判定、照準座標や命中判定は行いません。rawとtrigger/lightを別々に呼ぶと、それぞれ別の時刻にレジスターを読むため、信号が変化中なら一致しない場合があります。

受光線はアクティブローです。ビット3が0なので戻り値1になる、という処理であり、装置の存在を検出した結果ではありません。通常のパッドや未接続の入力でも1になり得ます。画面に的を描くだけで命中を判定するAPIではありません。

ビット変換は、独自の検証ROMでレジスター読み取り先だけをRAMの模擬値へ置き換え、00/08/10/18の組み合わせと無関係な上位ビットを入力して確認しました。この単体検証は、配布サンプルの通常実行および周辺機器の実動作とは別です。

掲載画像は配布サンプルを変更せずにKUROSAKIで実行した結果です。現在のKUROSAKIはパッドの入力モデルを持ちますが、キーボード・光線銃・シリアルアダプターの信号はモデル化していません。外部入力ビットが0になるため、画像は周辺機器の動作確認ではありません。実機のキー入力、受光、通信は未検証です。

NESdev: Zapper

使用例

c
#include "fc_ports_scene.h"
void main(void){
    __irq_disable();__ppu_ctrl_set(0);port_phase=1;
    port_result[0]=__zapper_raw1();port_result[1]=__zapper_trigger1();port_result[2]=__zapper_light1();
    port_result[3]=__zapper_raw2();port_result[4]=__zapper_trigger2();port_result[5]=__zapper_light2();
    port_result[6]=__zapper_trigger();port_result[7]=__zapper_light();
    port_phase=2;port_result[31]=165;
    m_init();m_text(2,2,"LIGHT-GUN INPUT SNAPSHOT");m_wait();
    m_text(2,5,"PORT   RAW  TRIGGER  LIGHT");m_wait();
    m_put(3,8,'1');port_hex(9,8,port_result[0]);port_number(15,8,port_result[1]);port_number(24,8,port_result[2]);m_wait();
    m_put(3,10,'2');port_hex(9,10,port_result[3]);port_number(15,10,port_result[4]);port_number(24,10,port_result[5]);m_wait();
    m_text(2,13,"DEFAULT = PORT 2");m_wait();
    m_text(2,15,"TRIGGER");port_number(15,15,port_result[6]);m_wait();
    m_text(2,17,"LIGHT");port_number(15,17,port_result[7]);m_wait();
    m_text(2,21,"LIGHT 1 IS NOT A HIT TEST");m_wait();
    while(1){m_wait();}
}

呼び出し部分のコード例です。完全なプログラムに組み込む際は、記載した初期化とバッファの条件を満たしてください。

期待する結果

KUROSAKIでは両ポートともRAW=00、TRIGGER=000、LIGHT=001です。番号なし関数もポート2と同じ000/001になります。LIGHT=001は未モデル化の入力ビット3が0だからで、画面の光を検知した証拠ではありません。画面下にも「LIGHT 1 IS NOT A HIT TEST」と表示します。

__zapper_light2: ZAPPER-DEFAULT — KUROSAKIでは両ポートともRAW=00、TRIGGER=000、LIGHT=001です。番号なし関数もポート2と同じ000/001になります。LIGHT=001は未モデル化の入力ビット3が0だからで、画面の光を検知した証拠ではありません。画面下にも「LIGHT 1 IS NOT A HIT TEST」と表示します。
ZAPPER-DEFAULT — KUROSAKIでは両ポートともRAW=00、TRIGGER=000、LIGHT=001です。番号なし関数もポート2と同じ000/001になります。LIGHT=001は未モデル化の入力ビット3が0だからで、画面の光を検知した証拠ではありません。画面下にも「LIGHT 1 IS NOT A HIT TEST」と表示します。

使用例の完全なプログラム

ビルドコマンド

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\peripheral_zapper.c -I .\kitaqfc\lib -I .\kitaq-docs\samples -o .\out\peripheral_zapper.nes --mapper=nrom --nes-chr=.\kitaq-docs\samples\font.chr --no-cache --no-disasm
.\kurosaki\kurosaki.exe run .\out\peripheral_zapper.nes --frames 120 --png .\out\peripheral_zapper.png
実装とソース

kitaqfc/lib/intrinsics.h:249

宣言の注記(原文)
Invert port-2 bit 3 and normalize: one means light detected.

kitaqfc/kitaqfc/CodeGenerator.cs:7753

csharp
EmitHelperStart("__zapper_light2");
            EmitNormalizeBoolFromMask(0x4017, 0x08, true);
        void EmitNormalizeBoolFromMask(int port, int mask, bool invert)
        {
            string zero = NewGeneratedLabel("bool_zero");
            string done = NewGeneratedLabel("bool_done");
            EmitAsm("LDA", Mem(port));
            EmitAsm("AND", Imm(mask));
            if (invert) EmitAsm("BNE", Rel(zero)); else EmitAsm("BEQ", Rel(zero));
            EmitAsm("LDA", Imm(1));
            EmitAsm("JMP", Abs(done));
            _assembly.Add(Expr.Make(Tag.Label, zero));
            EmitAsm("LDA", Imm(0));
            _assembly.Add(Expr.Make(Tag.Label, done));
            EmitAsm("RTS");
        }
__zapper_raw1

ポート1($4016)の光線銃用2ビットをまとめて取得します。同じ読み取り時点のトリガー線と受光線を自分で分けたい場合に使います。

書式

c
u8 __zapper_raw1(void);

戻り値

$4016 & 0x18。0x00・0x08・0x10・0x18のいずれかです。ビット4がトリガー、ビット3が非受光を表す生の信号です。

使用条件と制限

これらは$4016/$4017を直接操作します。通常のパッド、キーボード、シリアル送信など、同じポートを使う処理を割り込みで混在させないでください。パッドのシリアル読み取り位置にも影響します。サンプルは入力の採取中だけNMIを止め、表示を初期化してから再開します。

呼び出した瞬間の信号を読みます。ストローブ、フレーム待ち、前回との差分判定、照準座標や命中判定は行いません。rawとtrigger/lightを別々に呼ぶと、それぞれ別の時刻にレジスターを読むため、信号が変化中なら一致しない場合があります。

受光線はアクティブローです。ビット3が0なので戻り値1になる、という処理であり、装置の存在を検出した結果ではありません。通常のパッドや未接続の入力でも1になり得ます。画面に的を描くだけで命中を判定するAPIではありません。

ビット変換は、独自の検証ROMでレジスター読み取り先だけをRAMの模擬値へ置き換え、00/08/10/18の組み合わせと無関係な上位ビットを入力して確認しました。この単体検証は、配布サンプルの通常実行および周辺機器の実動作とは別です。

掲載画像は配布サンプルを変更せずにKUROSAKIで実行した結果です。現在のKUROSAKIはパッドの入力モデルを持ちますが、キーボード・光線銃・シリアルアダプターの信号はモデル化していません。外部入力ビットが0になるため、画像は周辺機器の動作確認ではありません。実機のキー入力、受光、通信は未検証です。

NESdev: Zapper

使用例

c
#include "fc_ports_scene.h"
void main(void){
    __irq_disable();__ppu_ctrl_set(0);port_phase=1;
    port_result[0]=__zapper_raw1();port_result[1]=__zapper_trigger1();port_result[2]=__zapper_light1();
    port_result[3]=__zapper_raw2();port_result[4]=__zapper_trigger2();port_result[5]=__zapper_light2();
    port_result[6]=__zapper_trigger();port_result[7]=__zapper_light();
    port_phase=2;port_result[31]=165;
    m_init();m_text(2,2,"LIGHT-GUN INPUT SNAPSHOT");m_wait();
    m_text(2,5,"PORT   RAW  TRIGGER  LIGHT");m_wait();
    m_put(3,8,'1');port_hex(9,8,port_result[0]);port_number(15,8,port_result[1]);port_number(24,8,port_result[2]);m_wait();
    m_put(3,10,'2');port_hex(9,10,port_result[3]);port_number(15,10,port_result[4]);port_number(24,10,port_result[5]);m_wait();
    m_text(2,13,"DEFAULT = PORT 2");m_wait();
    m_text(2,15,"TRIGGER");port_number(15,15,port_result[6]);m_wait();
    m_text(2,17,"LIGHT");port_number(15,17,port_result[7]);m_wait();
    m_text(2,21,"LIGHT 1 IS NOT A HIT TEST");m_wait();
    while(1){m_wait();}
}

呼び出し部分のコード例です。完全なプログラムに組み込む際は、記載した初期化とバッファの条件を満たしてください。

期待する結果

KUROSAKIでは両ポートともRAW=00、TRIGGER=000、LIGHT=001です。番号なし関数もポート2と同じ000/001になります。LIGHT=001は未モデル化の入力ビット3が0だからで、画面の光を検知した証拠ではありません。画面下にも「LIGHT 1 IS NOT A HIT TEST」と表示します。

__zapper_raw1: ZAPPER-DEFAULT — KUROSAKIでは両ポートともRAW=00、TRIGGER=000、LIGHT=001です。番号なし関数もポート2と同じ000/001になります。LIGHT=001は未モデル化の入力ビット3が0だからで、画面の光を検知した証拠ではありません。画面下にも「LIGHT 1 IS NOT A HIT TEST」と表示します。
ZAPPER-DEFAULT — KUROSAKIでは両ポートともRAW=00、TRIGGER=000、LIGHT=001です。番号なし関数もポート2と同じ000/001になります。LIGHT=001は未モデル化の入力ビット3が0だからで、画面の光を検知した証拠ではありません。画面下にも「LIGHT 1 IS NOT A HIT TEST」と表示します。

使用例の完全なプログラム

ビルドコマンド

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\peripheral_zapper.c -I .\kitaqfc\lib -I .\kitaq-docs\samples -o .\out\peripheral_zapper.nes --mapper=nrom --nes-chr=.\kitaq-docs\samples\font.chr --no-cache --no-disasm
.\kurosaki\kurosaki.exe run .\out\peripheral_zapper.nes --frames 120 --png .\out\peripheral_zapper.png
実装とソース

kitaqfc/lib/intrinsics.h:239

宣言の注記(原文)
Zapper / light gun. The no-suffix names default to the usual port-2/Famicom
expansion-port mapping. __zapper_light*() returns 1 when light is detected.

Return raw 4016 & 18, retaining trigger and active-low light bits.

kitaqfc/kitaqfc/CodeGenerator.cs:4775

csharp
case "__zapper_raw1": if (!Need(0)) return true; EmitAsm("LDA", Mem(0x4016)); EmitAsm("AND", Imm(0x18)); return true;
__zapper_raw2

ポート2($4017)の光線銃用2ビットをまとめて取得します。同じ読み取り時点のトリガー線と受光線を自分で分けたい場合に使います。

書式

c
u8 __zapper_raw2(void);

戻り値

$4017 & 0x18。0x00・0x08・0x10・0x18のいずれかです。ビット4がトリガー、ビット3が非受光を表す生の信号です。

使用条件と制限

これらは$4016/$4017を直接操作します。通常のパッド、キーボード、シリアル送信など、同じポートを使う処理を割り込みで混在させないでください。パッドのシリアル読み取り位置にも影響します。サンプルは入力の採取中だけNMIを止め、表示を初期化してから再開します。

呼び出した瞬間の信号を読みます。ストローブ、フレーム待ち、前回との差分判定、照準座標や命中判定は行いません。rawとtrigger/lightを別々に呼ぶと、それぞれ別の時刻にレジスターを読むため、信号が変化中なら一致しない場合があります。

受光線はアクティブローです。ビット3が0なので戻り値1になる、という処理であり、装置の存在を検出した結果ではありません。通常のパッドや未接続の入力でも1になり得ます。画面に的を描くだけで命中を判定するAPIではありません。

ビット変換は、独自の検証ROMでレジスター読み取り先だけをRAMの模擬値へ置き換え、00/08/10/18の組み合わせと無関係な上位ビットを入力して確認しました。この単体検証は、配布サンプルの通常実行および周辺機器の実動作とは別です。

掲載画像は配布サンプルを変更せずにKUROSAKIで実行した結果です。現在のKUROSAKIはパッドの入力モデルを持ちますが、キーボード・光線銃・シリアルアダプターの信号はモデル化していません。外部入力ビットが0になるため、画像は周辺機器の動作確認ではありません。実機のキー入力、受光、通信は未検証です。

NESdev: Zapper

使用例

c
#include "fc_ports_scene.h"
void main(void){
    __irq_disable();__ppu_ctrl_set(0);port_phase=1;
    port_result[0]=__zapper_raw1();port_result[1]=__zapper_trigger1();port_result[2]=__zapper_light1();
    port_result[3]=__zapper_raw2();port_result[4]=__zapper_trigger2();port_result[5]=__zapper_light2();
    port_result[6]=__zapper_trigger();port_result[7]=__zapper_light();
    port_phase=2;port_result[31]=165;
    m_init();m_text(2,2,"LIGHT-GUN INPUT SNAPSHOT");m_wait();
    m_text(2,5,"PORT   RAW  TRIGGER  LIGHT");m_wait();
    m_put(3,8,'1');port_hex(9,8,port_result[0]);port_number(15,8,port_result[1]);port_number(24,8,port_result[2]);m_wait();
    m_put(3,10,'2');port_hex(9,10,port_result[3]);port_number(15,10,port_result[4]);port_number(24,10,port_result[5]);m_wait();
    m_text(2,13,"DEFAULT = PORT 2");m_wait();
    m_text(2,15,"TRIGGER");port_number(15,15,port_result[6]);m_wait();
    m_text(2,17,"LIGHT");port_number(15,17,port_result[7]);m_wait();
    m_text(2,21,"LIGHT 1 IS NOT A HIT TEST");m_wait();
    while(1){m_wait();}
}

呼び出し部分のコード例です。完全なプログラムに組み込む際は、記載した初期化とバッファの条件を満たしてください。

期待する結果

KUROSAKIでは両ポートともRAW=00、TRIGGER=000、LIGHT=001です。番号なし関数もポート2と同じ000/001になります。LIGHT=001は未モデル化の入力ビット3が0だからで、画面の光を検知した証拠ではありません。画面下にも「LIGHT 1 IS NOT A HIT TEST」と表示します。

__zapper_raw2: ZAPPER-DEFAULT — KUROSAKIでは両ポートともRAW=00、TRIGGER=000、LIGHT=001です。番号なし関数もポート2と同じ000/001になります。LIGHT=001は未モデル化の入力ビット3が0だからで、画面の光を検知した証拠ではありません。画面下にも「LIGHT 1 IS NOT A HIT TEST」と表示します。
ZAPPER-DEFAULT — KUROSAKIでは両ポートともRAW=00、TRIGGER=000、LIGHT=001です。番号なし関数もポート2と同じ000/001になります。LIGHT=001は未モデル化の入力ビット3が0だからで、画面の光を検知した証拠ではありません。画面下にも「LIGHT 1 IS NOT A HIT TEST」と表示します。

使用例の完全なプログラム

ビルドコマンド

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\peripheral_zapper.c -I .\kitaqfc\lib -I .\kitaq-docs\samples -o .\out\peripheral_zapper.nes --mapper=nrom --nes-chr=.\kitaq-docs\samples\font.chr --no-cache --no-disasm
.\kurosaki\kurosaki.exe run .\out\peripheral_zapper.nes --frames 120 --png .\out\peripheral_zapper.png
実装とソース

kitaqfc/lib/intrinsics.h:241

宣言の注記(原文)
Return raw 4017 & 18 for the second port.

kitaqfc/kitaqfc/CodeGenerator.cs:4776

csharp
case "__zapper_raw2": if (!Need(0)) return true; EmitAsm("LDA", Mem(0x4017)); EmitAsm("AND", Imm(0x18)); return true;
__zapper_trigger

ポート2を既定とするトリガー読み取り名です。__zapper_trigger2()と同じ処理を呼びます。

書式

c
u8 __zapper_trigger(void);

戻り値

$4017のビット4を0/1にした値。ポート1への自動切り替えはありません。

使用条件と制限

これらは$4016/$4017を直接操作します。通常のパッド、キーボード、シリアル送信など、同じポートを使う処理を割り込みで混在させないでください。パッドのシリアル読み取り位置にも影響します。サンプルは入力の採取中だけNMIを止め、表示を初期化してから再開します。

呼び出した瞬間の信号を読みます。ストローブ、フレーム待ち、前回との差分判定、照準座標や命中判定は行いません。rawとtrigger/lightを別々に呼ぶと、それぞれ別の時刻にレジスターを読むため、信号が変化中なら一致しない場合があります。

戻り値はトリガー線の論理状態です。「最後まで引き続けている」を保証する値ではなく、装置のスイッチ特性にも依存します。ゲームの発射イベントには、必要な方向の変化を前回値と比較して扱います。

ビット変換は、独自の検証ROMでレジスター読み取り先だけをRAMの模擬値へ置き換え、00/08/10/18の組み合わせと無関係な上位ビットを入力して確認しました。この単体検証は、配布サンプルの通常実行および周辺機器の実動作とは別です。

掲載画像は配布サンプルを変更せずにKUROSAKIで実行した結果です。現在のKUROSAKIはパッドの入力モデルを持ちますが、キーボード・光線銃・シリアルアダプターの信号はモデル化していません。外部入力ビットが0になるため、画像は周辺機器の動作確認ではありません。実機のキー入力、受光、通信は未検証です。

NESdev: Zapper

使用例

c
#include "fc_ports_scene.h"
void main(void){
    __irq_disable();__ppu_ctrl_set(0);port_phase=1;
    port_result[0]=__zapper_raw1();port_result[1]=__zapper_trigger1();port_result[2]=__zapper_light1();
    port_result[3]=__zapper_raw2();port_result[4]=__zapper_trigger2();port_result[5]=__zapper_light2();
    port_result[6]=__zapper_trigger();port_result[7]=__zapper_light();
    port_phase=2;port_result[31]=165;
    m_init();m_text(2,2,"LIGHT-GUN INPUT SNAPSHOT");m_wait();
    m_text(2,5,"PORT   RAW  TRIGGER  LIGHT");m_wait();
    m_put(3,8,'1');port_hex(9,8,port_result[0]);port_number(15,8,port_result[1]);port_number(24,8,port_result[2]);m_wait();
    m_put(3,10,'2');port_hex(9,10,port_result[3]);port_number(15,10,port_result[4]);port_number(24,10,port_result[5]);m_wait();
    m_text(2,13,"DEFAULT = PORT 2");m_wait();
    m_text(2,15,"TRIGGER");port_number(15,15,port_result[6]);m_wait();
    m_text(2,17,"LIGHT");port_number(15,17,port_result[7]);m_wait();
    m_text(2,21,"LIGHT 1 IS NOT A HIT TEST");m_wait();
    while(1){m_wait();}
}

呼び出し部分のコード例です。完全なプログラムに組み込む際は、記載した初期化とバッファの条件を満たしてください。

期待する結果

KUROSAKIでは両ポートともRAW=00、TRIGGER=000、LIGHT=001です。番号なし関数もポート2と同じ000/001になります。LIGHT=001は未モデル化の入力ビット3が0だからで、画面の光を検知した証拠ではありません。画面下にも「LIGHT 1 IS NOT A HIT TEST」と表示します。

__zapper_trigger: ZAPPER-DEFAULT — KUROSAKIでは両ポートともRAW=00、TRIGGER=000、LIGHT=001です。番号なし関数もポート2と同じ000/001になります。LIGHT=001は未モデル化の入力ビット3が0だからで、画面の光を検知した証拠ではありません。画面下にも「LIGHT 1 IS NOT A HIT TEST」と表示します。
ZAPPER-DEFAULT — KUROSAKIでは両ポートともRAW=00、TRIGGER=000、LIGHT=001です。番号なし関数もポート2と同じ000/001になります。LIGHT=001は未モデル化の入力ビット3が0だからで、画面の光を検知した証拠ではありません。画面下にも「LIGHT 1 IS NOT A HIT TEST」と表示します。

使用例の完全なプログラム

ビルドコマンド

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\peripheral_zapper.c -I .\kitaqfc\lib -I .\kitaq-docs\samples -o .\out\peripheral_zapper.nes --mapper=nrom --nes-chr=.\kitaq-docs\samples\font.chr --no-cache --no-disasm
.\kurosaki\kurosaki.exe run .\out\peripheral_zapper.nes --frames 120 --png .\out\peripheral_zapper.png
実装とソース

kitaqfc/lib/intrinsics.h:251

宣言の注記(原文)
Default to the port-2 normalized trigger reader.

kitaqfc/kitaqfc/CodeGenerator.cs:7749

csharp
EmitHelperStart("__zapper_trigger2");
            EmitNormalizeBoolFromMask(0x4017, 0x10, false);
        void EmitNormalizeBoolFromMask(int port, int mask, bool invert)
        {
            string zero = NewGeneratedLabel("bool_zero");
            string done = NewGeneratedLabel("bool_done");
            EmitAsm("LDA", Mem(port));
            EmitAsm("AND", Imm(mask));
            if (invert) EmitAsm("BNE", Rel(zero)); else EmitAsm("BEQ", Rel(zero));
            EmitAsm("LDA", Imm(1));
            EmitAsm("JMP", Abs(done));
            _assembly.Add(Expr.Make(Tag.Label, zero));
            EmitAsm("LDA", Imm(0));
            _assembly.Add(Expr.Make(Tag.Label, done));
            EmitAsm("RTS");
        }
__zapper_trigger1

ポート1($4016)のトリガー線を0/1へ正規化して読みます。

書式

c
u8 __zapper_trigger1(void);

戻り値

$4016のビット4が1なら1、0なら0です。

使用条件と制限

これらは$4016/$4017を直接操作します。通常のパッド、キーボード、シリアル送信など、同じポートを使う処理を割り込みで混在させないでください。パッドのシリアル読み取り位置にも影響します。サンプルは入力の採取中だけNMIを止め、表示を初期化してから再開します。

呼び出した瞬間の信号を読みます。ストローブ、フレーム待ち、前回との差分判定、照準座標や命中判定は行いません。rawとtrigger/lightを別々に呼ぶと、それぞれ別の時刻にレジスターを読むため、信号が変化中なら一致しない場合があります。

戻り値はトリガー線の論理状態です。「最後まで引き続けている」を保証する値ではなく、装置のスイッチ特性にも依存します。ゲームの発射イベントには、必要な方向の変化を前回値と比較して扱います。

ビット変換は、独自の検証ROMでレジスター読み取り先だけをRAMの模擬値へ置き換え、00/08/10/18の組み合わせと無関係な上位ビットを入力して確認しました。この単体検証は、配布サンプルの通常実行および周辺機器の実動作とは別です。

掲載画像は配布サンプルを変更せずにKUROSAKIで実行した結果です。現在のKUROSAKIはパッドの入力モデルを持ちますが、キーボード・光線銃・シリアルアダプターの信号はモデル化していません。外部入力ビットが0になるため、画像は周辺機器の動作確認ではありません。実機のキー入力、受光、通信は未検証です。

NESdev: Zapper

使用例

c
#include "fc_ports_scene.h"
void main(void){
    __irq_disable();__ppu_ctrl_set(0);port_phase=1;
    port_result[0]=__zapper_raw1();port_result[1]=__zapper_trigger1();port_result[2]=__zapper_light1();
    port_result[3]=__zapper_raw2();port_result[4]=__zapper_trigger2();port_result[5]=__zapper_light2();
    port_result[6]=__zapper_trigger();port_result[7]=__zapper_light();
    port_phase=2;port_result[31]=165;
    m_init();m_text(2,2,"LIGHT-GUN INPUT SNAPSHOT");m_wait();
    m_text(2,5,"PORT   RAW  TRIGGER  LIGHT");m_wait();
    m_put(3,8,'1');port_hex(9,8,port_result[0]);port_number(15,8,port_result[1]);port_number(24,8,port_result[2]);m_wait();
    m_put(3,10,'2');port_hex(9,10,port_result[3]);port_number(15,10,port_result[4]);port_number(24,10,port_result[5]);m_wait();
    m_text(2,13,"DEFAULT = PORT 2");m_wait();
    m_text(2,15,"TRIGGER");port_number(15,15,port_result[6]);m_wait();
    m_text(2,17,"LIGHT");port_number(15,17,port_result[7]);m_wait();
    m_text(2,21,"LIGHT 1 IS NOT A HIT TEST");m_wait();
    while(1){m_wait();}
}

呼び出し部分のコード例です。完全なプログラムに組み込む際は、記載した初期化とバッファの条件を満たしてください。

期待する結果

KUROSAKIでは両ポートともRAW=00、TRIGGER=000、LIGHT=001です。番号なし関数もポート2と同じ000/001になります。LIGHT=001は未モデル化の入力ビット3が0だからで、画面の光を検知した証拠ではありません。画面下にも「LIGHT 1 IS NOT A HIT TEST」と表示します。

__zapper_trigger1: ZAPPER-DEFAULT — KUROSAKIでは両ポートともRAW=00、TRIGGER=000、LIGHT=001です。番号なし関数もポート2と同じ000/001になります。LIGHT=001は未モデル化の入力ビット3が0だからで、画面の光を検知した証拠ではありません。画面下にも「LIGHT 1 IS NOT A HIT TEST」と表示します。
ZAPPER-DEFAULT — KUROSAKIでは両ポートともRAW=00、TRIGGER=000、LIGHT=001です。番号なし関数もポート2と同じ000/001になります。LIGHT=001は未モデル化の入力ビット3が0だからで、画面の光を検知した証拠ではありません。画面下にも「LIGHT 1 IS NOT A HIT TEST」と表示します。

使用例の完全なプログラム

ビルドコマンド

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\peripheral_zapper.c -I .\kitaqfc\lib -I .\kitaq-docs\samples -o .\out\peripheral_zapper.nes --mapper=nrom --nes-chr=.\kitaq-docs\samples\font.chr --no-cache --no-disasm
.\kurosaki\kurosaki.exe run .\out\peripheral_zapper.nes --frames 120 --png .\out\peripheral_zapper.png
実装とソース

kitaqfc/lib/intrinsics.h:243

宣言の注記(原文)
Return port-1 trigger bit 4 normalized to 0 or 1.

kitaqfc/kitaqfc/CodeGenerator.cs:7747

csharp
EmitHelperStart("__zapper_trigger1");
            EmitNormalizeBoolFromMask(0x4016, 0x10, false);
        void EmitNormalizeBoolFromMask(int port, int mask, bool invert)
        {
            string zero = NewGeneratedLabel("bool_zero");
            string done = NewGeneratedLabel("bool_done");
            EmitAsm("LDA", Mem(port));
            EmitAsm("AND", Imm(mask));
            if (invert) EmitAsm("BNE", Rel(zero)); else EmitAsm("BEQ", Rel(zero));
            EmitAsm("LDA", Imm(1));
            EmitAsm("JMP", Abs(done));
            _assembly.Add(Expr.Make(Tag.Label, zero));
            EmitAsm("LDA", Imm(0));
            _assembly.Add(Expr.Make(Tag.Label, done));
            EmitAsm("RTS");
        }
__zapper_trigger2

ポート2($4017)のトリガー線を0/1へ正規化して読みます。

書式

c
u8 __zapper_trigger2(void);

戻り値

$4017のビット4が1なら1、0なら0です。

使用条件と制限

これらは$4016/$4017を直接操作します。通常のパッド、キーボード、シリアル送信など、同じポートを使う処理を割り込みで混在させないでください。パッドのシリアル読み取り位置にも影響します。サンプルは入力の採取中だけNMIを止め、表示を初期化してから再開します。

呼び出した瞬間の信号を読みます。ストローブ、フレーム待ち、前回との差分判定、照準座標や命中判定は行いません。rawとtrigger/lightを別々に呼ぶと、それぞれ別の時刻にレジスターを読むため、信号が変化中なら一致しない場合があります。

戻り値はトリガー線の論理状態です。「最後まで引き続けている」を保証する値ではなく、装置のスイッチ特性にも依存します。ゲームの発射イベントには、必要な方向の変化を前回値と比較して扱います。

ビット変換は、独自の検証ROMでレジスター読み取り先だけをRAMの模擬値へ置き換え、00/08/10/18の組み合わせと無関係な上位ビットを入力して確認しました。この単体検証は、配布サンプルの通常実行および周辺機器の実動作とは別です。

掲載画像は配布サンプルを変更せずにKUROSAKIで実行した結果です。現在のKUROSAKIはパッドの入力モデルを持ちますが、キーボード・光線銃・シリアルアダプターの信号はモデル化していません。外部入力ビットが0になるため、画像は周辺機器の動作確認ではありません。実機のキー入力、受光、通信は未検証です。

NESdev: Zapper

使用例

c
#include "fc_ports_scene.h"
void main(void){
    __irq_disable();__ppu_ctrl_set(0);port_phase=1;
    port_result[0]=__zapper_raw1();port_result[1]=__zapper_trigger1();port_result[2]=__zapper_light1();
    port_result[3]=__zapper_raw2();port_result[4]=__zapper_trigger2();port_result[5]=__zapper_light2();
    port_result[6]=__zapper_trigger();port_result[7]=__zapper_light();
    port_phase=2;port_result[31]=165;
    m_init();m_text(2,2,"LIGHT-GUN INPUT SNAPSHOT");m_wait();
    m_text(2,5,"PORT   RAW  TRIGGER  LIGHT");m_wait();
    m_put(3,8,'1');port_hex(9,8,port_result[0]);port_number(15,8,port_result[1]);port_number(24,8,port_result[2]);m_wait();
    m_put(3,10,'2');port_hex(9,10,port_result[3]);port_number(15,10,port_result[4]);port_number(24,10,port_result[5]);m_wait();
    m_text(2,13,"DEFAULT = PORT 2");m_wait();
    m_text(2,15,"TRIGGER");port_number(15,15,port_result[6]);m_wait();
    m_text(2,17,"LIGHT");port_number(15,17,port_result[7]);m_wait();
    m_text(2,21,"LIGHT 1 IS NOT A HIT TEST");m_wait();
    while(1){m_wait();}
}

呼び出し部分のコード例です。完全なプログラムに組み込む際は、記載した初期化とバッファの条件を満たしてください。

期待する結果

KUROSAKIでは両ポートともRAW=00、TRIGGER=000、LIGHT=001です。番号なし関数もポート2と同じ000/001になります。LIGHT=001は未モデル化の入力ビット3が0だからで、画面の光を検知した証拠ではありません。画面下にも「LIGHT 1 IS NOT A HIT TEST」と表示します。

__zapper_trigger2: ZAPPER-DEFAULT — KUROSAKIでは両ポートともRAW=00、TRIGGER=000、LIGHT=001です。番号なし関数もポート2と同じ000/001になります。LIGHT=001は未モデル化の入力ビット3が0だからで、画面の光を検知した証拠ではありません。画面下にも「LIGHT 1 IS NOT A HIT TEST」と表示します。
ZAPPER-DEFAULT — KUROSAKIでは両ポートともRAW=00、TRIGGER=000、LIGHT=001です。番号なし関数もポート2と同じ000/001になります。LIGHT=001は未モデル化の入力ビット3が0だからで、画面の光を検知した証拠ではありません。画面下にも「LIGHT 1 IS NOT A HIT TEST」と表示します。

使用例の完全なプログラム

ビルドコマンド

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\peripheral_zapper.c -I .\kitaqfc\lib -I .\kitaq-docs\samples -o .\out\peripheral_zapper.nes --mapper=nrom --nes-chr=.\kitaq-docs\samples\font.chr --no-cache --no-disasm
.\kurosaki\kurosaki.exe run .\out\peripheral_zapper.nes --frames 120 --png .\out\peripheral_zapper.png
実装とソース

kitaqfc/lib/intrinsics.h:245

宣言の注記(原文)
Return port-2 trigger bit 4 normalized to 0 or 1.

kitaqfc/kitaqfc/CodeGenerator.cs:7749

csharp
EmitHelperStart("__zapper_trigger2");
            EmitNormalizeBoolFromMask(0x4017, 0x10, false);
        void EmitNormalizeBoolFromMask(int port, int mask, bool invert)
        {
            string zero = NewGeneratedLabel("bool_zero");
            string done = NewGeneratedLabel("bool_done");
            EmitAsm("LDA", Mem(port));
            EmitAsm("AND", Imm(mask));
            if (invert) EmitAsm("BNE", Rel(zero)); else EmitAsm("BEQ", Rel(zero));
            EmitAsm("LDA", Imm(1));
            EmitAsm("JMP", Abs(done));
            _assembly.Add(Expr.Make(Tag.Label, zero));
            EmitAsm("LDA", Imm(0));
            _assembly.Add(Expr.Make(Tag.Label, done));
            EmitAsm("RTS");
        }

コマンド書式辞典

下記のヘルプは本版で実行して採取しました。省略可能な引数、既定値、列挙値は採取結果に従ってください。説明用コマンドの入力ファイルは自分の出力名に合わせます。

kitaqfc
help
KITAQFC native Rust compiler

Build a NES ROM:
  kitaqfc SOURCE.c ... -I INCLUDE --mapper=nrom -o GAME.nes
  kitaqfc SOURCE.c ... -I lib --mapper=mmc3 --nes-chr=tiles.chr -O2 -o GAME.nes

Build a Famicom Disk System image:
  kitaqfc SOURCE.c ... --mapper=fds --fds-layout=fds32 -o GAME.fds
  --fds-meta=FILE --fds-meta-out[=FILE] --fds-out[=FILE]
  --fds-auto-overlay / --fds-no-auto-overlay
  --fds-overlay-farcall / --fds-no-overlay-farcall
  --fds-overlay-guard / --fds-no-overlay-guard
  --fds-overlay-trim --fds-overlay-prefix=NAME --fds-overlay-start-id=N
  --fds-overlay-table / --fds-no-overlay-table
  --fds-header / --fds-raw --fds-game-code=CODE
  --fds-license-bypass / --fds-no-license-bypass

Cartridge and optimization:
  --target=nes|fc|famicom|6502|2a03
  --mapper=nrom|uxrom|cnrom|axrom|mmc1|mmc3|mmc5|vrc6|vrc7|fme7|fds|surom512
  --board=auto|generic|surom512 --mirroring=horizontal|vertical|four_screen
  --battery / --no-battery --nes-chr-ram --nes-chr=FILE
  -O0 / -O1 / -O2 --nes-opt=all|off
  --abi=legacy|stack|fastcall --reg-cc-v2 / --no-reg-cc-v2
  --zp-alloc --static-frame --small-inline --loop-lowering
  --library-lto-lite --mapper-aware-placement --kurosaki-profile=FILE
  --nes-local-ram=START:LENGTH --nes-temp-ram=START:LENGTH
  -Zcheck -Zcheck-bounds -Zconst-scalar-in-rom

Diagnostics and reports:
  --strict / --permissive --max-errors=N --diag-json[=FILE|-]
  --profile=dev|release|test --cache / --no-cache --deps-out[=FILE]
  --debug-output[=DIR] --trace[=tokens,ast,ir,asm] --trace-out=DIR
  --no-debug-output --no-trace --fast-build --emit-path-manifest=FILE
  --kurosaki-metadata[=FILE] --nes-opt-report[=FILE]
  --func-size-report --hotspot-report --cross-bank-report --farcall-suggest
  --abi-verify --abi-diff-report --bank-sim --opt-diff --rst-report --repro-check
  Report switches accept =FILE; otherwise reports are placed beside the ROM.

Frontend and typed assembly:
  tokenize SOURCE.c ... -I INCLUDE -o tokens.json
  parse SOURCE.c ... -I INCLUDE -o ast.json
  lower SOURCE.c ... -I INCLUDE -o lowered.json
  emit-ir SOURCE.c ... -I INCLUDE -o assembly.json
  assemble-ir ASSEMBLY.json --mapper=nrom -o ROM.nes

Utilities and development:
  kqhelp, symfind, src2asm, romdiff, template, snippet,
  conventions, fixhint, irsum, attrviz, recipe, test
  SOURCE.c ... --watch [--watch-max-builds=N]
  devserver SOURCE.c ... [--once --poll-ms=N --debounce-ms=N --max-builds=N]
  SOURCE.c ... --repro-pack[=DIR] --auto-minimize
  SOURCE.c ... --minimize[=FILE] --minimize-work=DIR --minimize-out=FILE

Legacy ROM-header and RST flags retain the C# NES backend's behavior.
NES disassembly and managed debugger attachment do not run in that backend.

開発補助コマンドの書式

以下は現在のsrc/debug_tools.rs / src/vibe_tools.rsにある書式です。ROM差分、シンボル探索、テンプレートなどは通常のCコンパイルと別のサブコマンドです。

usage
usage: kitaqfc kqhelp <KQxxxx>
usage
usage: kitaqfc symfind <pattern> [--map=<.map>] [--dbg=<.dbg>] [--regex] [--ignore-case] [--out=<file>|-]
usage
usage: kitaqfc src2asm <source.c:line> [--disasm=<dis.s>] [--funcsizes=<.funcsizes.txt>] [--context=N] [--out=<file>|-]
usage
usage: kitaqfc romdiff <old.nes> <new.nes> [--old-func=<.funcsizes.txt>] [--new-func=<.funcsizes.txt>] [--old-map=<.map>] [--new-map=<.map>] [--out=<file>|-]
usage
usage: kitaqfc template [out.c] [--out=<file.c>] [--overwrite] [--no-build-script] [--no-readme]
usage
usage: kitaqfc conventions [--out=<file>|--stdout]
usage
usage: kitaqfc snippet list | get <id> [--out=<file>|-] | emit <category|all> [--out=<file>|-]
usage
usage: kitaqfc fixhint [diag.json|stderr.log] [--in=<file>] [--out=<file>|-] [--max=N]
usage
usage: kitaqfc irsum [--ir=<syntax_tree_lowered.txt>] [--asm=<assembly_code.txt>] [--top=N] [--out=<file>|-]
usage
usage: kitaqfc devserver [--poll-ms=N] [--debounce-ms=N] [--once] [--max-builds=N] [--deps=<file>] <compile args...>
usage
usage: kitaqfc recipe [--history=<file>] [--last=N] [--out=<file>] [--script=<file>] [--stdout]

パーサーが認識する長いオプション索引

完全な綴りを現在のsrc/cli.rsから採取。ここには互換・調査用の指定も含みます。値を必要とするかはヘルプと対応する処理を参照してください。

--abi--abi-diff-report--abi-verify--attach--auto-inline-small--auto-minimize--bank-sim--battery--board--cache--cart--cgb--cgb-consistency--chr-rom--container--cross-bank-report--debug-out--debug-output--deps-out--dest--diag-json--disasm--disasm-changed--disasm-changed-out--emit-ai-metadata--farcall-suggest--fast--fast-build--fastcall-v2--fds-approval-bypass--fds-auto-overlay--fds-auto-overlays--fds-builder-manifest--fds-bypass-license--fds-game-code--fds-header--fds-image--fds-layout--fds-license-bypass--fds-manifest--fds-meta--fds-meta-out--fds-metadata--fds-metadata-out--fds-no-approval-bypass--fds-no-auto-overlay--fds-no-auto-overlays--fds-no-header--fds-no-license-bypass--fds-no-overlay-farcall--fds-no-overlay-guard--fds-no-overlay-table--fds-no-residency-guard--fds-out--fds-overlay-farcall--fds-overlay-guard--fds-overlay-id-base--fds-overlay-no-trim--fds-overlay-prefix--fds-overlay-start-id--fds-overlay-table--fds-overlay-trim--fds-raw--fds-residency-guard--func-size-report--help--hotspot-report--include-dir--kurosaki-meta--kurosaki-metadata--kurosaki-out--kurosaki-profile--library-lto-lite--loop-lowering--lto-lite--mapper--mapper-aware-placement--max-errors--minimize-on-fail--mirroring--nes-chr--nes-chr-ram--nes-local-ram--nes-opt--nes-opt-report--nes-temp-ram--no-auto-inline-small--no-battery--no-cache--no-disasm--no-fastcall-v2--no-library-lto-lite--no-loop-lowering--no-lto-lite--no-mapper-aware-placement--no-reg-cc-v2--no-rst--no-small-inline--no-static-frame--no-whole-program-zp--no-zp-alloc--opt-diff--output-format--permissive--profile--ramsize--reg-cc-v2--repro-check--repro-pack--rom-header--rom-title--romsize--rst--rst-disable--rst-enable--rst-exclude--rst-max-calls--rst-max-vectors--rst-off--rst-on--rst-report--rst-safe--rst-speed-safe--rst-unsafe--rst-use-38--sgb--small-inline--static-frame--strict--target--trace--trace-out--verify-cgb-symbols--version--vlist--vlist-out--whole-program-zp--zp-alloc

付録:アセンブリ命令索引

src/machine/opcodes.rsの命令表を採取しています。これはコンパイラ内部の綴り・オペランド形式の索引です。分岐先やメモリアドレスを持つ行は書式例であり、単独で実行するプログラムではありません。レジスターの保持やフラグ変化は呼び出し規約とコード生成を参照してください。

opcode命令名形式書式例
00NOPIMPNOP
F3DIIMPDI
FBEIIMPEI
76HALTIMPHALT
10STOPIMM8STOP #1
C9RETIMPRET
D9RETIIMPRETI
C3JPABSJP 0xC000
E9JP_HLIMPJP_HL
18JRRELJR +target
20JR_NZRELJR_NZ +target
28JR_ZRELJR_Z +target
30JR_NCRELJR_NC +target
38JR_CRELJR_C +target
CDCALLABSCALL 0xC000
C4CALL_NZABSCALL_NZ 0xC000
CCCALL_ZABSCALL_Z 0xC000
D4CALL_NCABSCALL_NC 0xC000
DCCALL_CABSCALL_C 0xC000
C2JP_NZABSJP_NZ 0xC000
CAJP_ZABSJP_Z 0xC000
D2JP_NCABSJP_NC 0xC000
DAJP_CABSJP_C 0xC000
C7RST_00IMPRST_00
CFRST_08IMPRST_08
D7RST_10IMPRST_10
DFRST_18IMPRST_18
E7RST_20IMPRST_20
EFRST_28IMPRST_28
F7RST_30IMPRST_30
FFRST_38IMPRST_38
3ELD_A_IMMIMM8LD_A_IMM #1
06LD_B_IMMIMM8LD_B_IMM #1
0ELD_C_IMMIMM8LD_C_IMM #1
16LD_D_IMMIMM8LD_D_IMM #1
1ELD_E_IMMIMM8LD_E_IMM #1
26LD_H_IMMIMM8LD_H_IMM #1
2ELD_L_IMMIMM8LD_L_IMM #1
7FLD_A_AIMPLD_A_A
78LD_A_BIMPLD_A_B
79LD_A_CIMPLD_A_C
7ALD_A_DIMPLD_A_D
7BLD_A_EIMPLD_A_E
7CLD_A_HIMPLD_A_H
7DLD_A_LIMPLD_A_L
47LD_B_AIMPLD_B_A
40LD_B_BIMPLD_B_B
41LD_B_CIMPLD_B_C
42LD_B_DIMPLD_B_D
43LD_B_EIMPLD_B_E
44LD_B_HIMPLD_B_H
45LD_B_LIMPLD_B_L
4FLD_C_AIMPLD_C_A
48LD_C_BIMPLD_C_B
49LD_C_CIMPLD_C_C
4ALD_C_DIMPLD_C_D
4BLD_C_EIMPLD_C_E
4CLD_C_HIMPLD_C_H
4DLD_C_LIMPLD_C_L
57LD_D_AIMPLD_D_A
50LD_D_BIMPLD_D_B
51LD_D_CIMPLD_D_C
52LD_D_DIMPLD_D_D
53LD_D_EIMPLD_D_E
54LD_D_HIMPLD_D_H
55LD_D_LIMPLD_D_L
5FLD_E_AIMPLD_E_A
58LD_E_BIMPLD_E_B
59LD_E_CIMPLD_E_C
5ALD_E_DIMPLD_E_D
5BLD_E_EIMPLD_E_E
5CLD_E_HIMPLD_E_H
5DLD_E_LIMPLD_E_L
67LD_H_AIMPLD_H_A
60LD_H_BIMPLD_H_B
61LD_H_CIMPLD_H_C
62LD_H_DIMPLD_H_D
63LD_H_EIMPLD_H_E
64LD_H_HIMPLD_H_H
65LD_H_LIMPLD_H_L
6FLD_L_AIMPLD_L_A
68LD_L_BIMPLD_L_B
69LD_L_CIMPLD_L_C
6ALD_L_DIMPLD_L_D
6BLD_L_EIMPLD_L_E
6CLD_L_HIMPLD_L_H
6DLD_L_LIMPLD_L_L
EALD_MEM_AABSLD_MEM_A 0xC000
FALD_A_MEMABSLD_A_MEM 0xC000
E0LDH_MEM_ALDHLDH_MEM_A 0x40
F0LDH_A_MEMLDHLDH_A_MEM 0x40
E2LD_C_MEM_AIMPLD_C_MEM_A
F2LD_A_MEM_CIMPLD_A_MEM_C
77LD_HL_AIMPLD_HL_A
7ELD_A_HLIMPLD_A_HL
70LD_HL_BIMPLD_HL_B
71LD_HL_CIMPLD_HL_C
72LD_HL_DIMPLD_HL_D
73LD_HL_EIMPLD_HL_E
74LD_HL_HIMPLD_HL_H
75LD_HL_LIMPLD_HL_L
36LD_HL_REF_IMMIMM8LD_HL_REF_IMM #1
12LD_DE_AIMPLD_DE_A
1ALD_A_DEIMPLD_A_DE
02LD_BC_AIMPLD_BC_A
0ALD_A_BCIMPLD_A_BC
22LDI_HL_AIMPLDI_HL_A
2ALDI_A_HLIMPLDI_A_HL
32LDD_HL_AIMPLDD_HL_A
3ALDD_A_HLIMPLDD_A_HL
01LD_BC_IMMIMM16LD_BC_IMM #0xC000
11LD_DE_IMMIMM16LD_DE_IMM #0xC000
21LD_HL_IMMIMM16LD_HL_IMM #0xC000
31LD_SP_IMMIMM16LD_SP_IMM #0xC000
F9LD_SP_HLIMPLD_SP_HL
F8LD_HL_SP_IMMRELLD_HL_SP_IMM +target
E8ADD_SP_IMMRELADD_SP_IMM +target
08LD_MEM_SPIMM16LD_MEM_SP #0xC000
C5PUSH_BCIMPPUSH_BC
C1POP_BCIMPPOP_BC
D5PUSH_DEIMPPUSH_DE
D1POP_DEIMPPOP_DE
E5PUSH_HLIMPPUSH_HL
E1POP_HLIMPPOP_HL
F5PUSH_AFIMPPUSH_AF
F1POP_AFIMPPOP_AF
3CINC_AIMPINC_A
04INC_BIMPINC_B
0CINC_CIMPINC_C
14INC_DIMPINC_D
1CINC_EIMPINC_E
24INC_HIMPINC_H
2CINC_LIMPINC_L
34INC_HL_REFIMPINC_HL_REF
3DDEC_AIMPDEC_A
05DEC_BIMPDEC_B
0DDEC_CIMPDEC_C
15DEC_DIMPDEC_D
1DDEC_EIMPDEC_E
25DEC_HIMPDEC_H
2DDEC_LIMPDEC_L
35DEC_HL_REFIMPDEC_HL_REF
87ADD_AIMPADD_A
80ADD_BIMPADD_B
81ADD_CIMPADD_C
82ADD_DIMPADD_D
83ADD_EIMPADD_E
84ADD_HIMPADD_H
85ADD_LIMPADD_L
86ADD_HL_REFIMPADD_HL_REF
C6ADD_A_IMMIMM8ADD_A_IMM #1
8FADC_AIMPADC_A
88ADC_BIMPADC_B
89ADC_CIMPADC_C
8AADC_DIMPADC_D
8BADC_EIMPADC_E
8CADC_HIMPADC_H
8DADC_LIMPADC_L
8EADC_HL_REFIMPADC_HL_REF
CEADC_IMMIMM8ADC_IMM #1
97SUB_AIMPSUB_A
90SUB_BIMPSUB_B
91SUB_CIMPSUB_C
92SUB_DIMPSUB_D
93SUB_EIMPSUB_E
94SUB_HIMPSUB_H
95SUB_LIMPSUB_L
96SUB_HL_REFIMPSUB_HL_REF
D6SUB_IMMIMM8SUB_IMM #1
9FSBC_AIMPSBC_A
98SBC_BIMPSBC_B
99SBC_CIMPSBC_C
9ASBC_DIMPSBC_D
9BSBC_EIMPSBC_E
9CSBC_HIMPSBC_H
9DSBC_LIMPSBC_L
9ESBC_HL_REFIMPSBC_HL_REF
DESBC_IMMIMM8SBC_IMM #1
A7AND_AIMPAND_A
A0AND_BIMPAND_B
A1AND_CIMPAND_C
A2AND_DIMPAND_D
A3AND_EIMPAND_E
A4AND_HIMPAND_H
A5AND_LIMPAND_L
A6AND_HL_REFIMPAND_HL_REF
E6AND_IMMIMM8AND_IMM #1
B7OR_AIMPOR_A
B0OR_BIMPOR_B
B1OR_CIMPOR_C
B2OR_DIMPOR_D
B3OR_EIMPOR_E
B4OR_HIMPOR_H
B5OR_LIMPOR_L
B6OR_HL_REFIMPOR_HL_REF
F6OR_IMMIMM8OR_IMM #1
AFXOR_AIMPXOR_A
A8XOR_BIMPXOR_B
A9XOR_CIMPXOR_C
AAXOR_DIMPXOR_D
ABXOR_EIMPXOR_E
ACXOR_HIMPXOR_H
ADXOR_LIMPXOR_L
AEXOR_HL_REFIMPXOR_HL_REF
EEXOR_IMMIMM8XOR_IMM #1
BFCP_AIMPCP_A
B8CP_BIMPCP_B
B9CP_CIMPCP_C
BACP_DIMPCP_D
BBCP_EIMPCP_E
BCCP_HIMPCP_H
BDCP_LIMPCP_L
BECP_HL_REFIMPCP_HL_REF
FECP_IMMIMM8CP_IMM #1
03INC_BCIMPINC_BC
0BDEC_BCIMPDEC_BC
13INC_DEIMPINC_DE
1BDEC_DEIMPDEC_DE
23INC_HLIMPINC_HL
2BDEC_HLIMPDEC_HL
33INC_SPIMPINC_SP
3BDEC_SPIMPDEC_SP
09ADD_HL_BCIMPADD_HL_BC
19ADD_HL_DEIMPADD_HL_DE
29ADD_HL_HLIMPADD_HL_HL
39ADD_HL_SPIMPADD_HL_SP
07RLCAIMPRLCA
17RLAIMPRLA
0FRRCAIMPRRCA
1FRRAIMPRRA
CBPREFIX_CBIMPPREFIX_CB
2FCPLIMPCPL
3FCCFIMPCCF
37SCFIMPSCF
27DAAIMPDAA