KITAQ 开发系统
用户手册 2026.09

KITAQ SERIES • 参考版

KITAQFC

FC C 语言编程

从第一行代码,到运行、观察和复测。
源码快照:2026 年 9 月 14 日 · 简体中文版: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

各操作系统的示例构建

PowerShell 示例构建使用根目录的 Windows 程序。在 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 构建脚本后,项目设置会将可执行文件复制到仓库根目录。BINARY_BUILD.json 记录编译器源文件、可执行文件的哈希及该构建的验证结果。过长的诊断行只显示200个字符和省略号。

使用29个输入比较了当前C#构建和先前发布版。接受的15个输入生成完全相同的ROM字节,另外14个输入产生相同的拒绝诊断。另将两个ROM各在KUROSAKI中运行4帧,分别检查实体分配、回调、槽位复用,以及有符号运算、KQBody3D 布局和音频队列常量。报告列出18字节RAM的预期值和实际值。这些检查不涉及音频播放或全部API;先前的回归测试结果在 BINARY_BUILD.json 中单独标注。

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

源代码示例与运行验证记录

1. KITAQFC 与 GB 编译器的关系

KITAQFC 使用 KITAQGB 的前端,为 NES/FC 的 6502 系列 CPU 生成代码。它不把 GB ROM 转换成 NES ROM;程序仍需针对目标机器的画面、声音、内存和 mapper 编写。

KITAQFC 支持结构体复制、普通函数调用、for、while 和 do-while。do-while 先执行循环体至少一次,再检查条件;continue 进入末尾条件判断,break 退出循环。switch 选择 0..255 范围内的 case 常量,没有匹配时执行 default,选择表达式只求值一次。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 程序。在 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 队列、等待 NMI,并恢复滚动。通过 PPUADDR 传输会影响内部滚动状态,遗漏恢复可能使文字偏向边缘。请熟悉“关闭显示、准备素材、开启显示、与 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 演示循环。不要加入 CGB 寄存器或 GB 专用内建函数。

表达式结果与求值

比较运算符 ==、!=、<、<=、>、>= 和逻辑运算符 !、&&、|| 以 0 表示假、1 表示真。结果可存入 u16、传参、返回或参加算术。例如 score = 500 + (lives != 0); 在还有生命时得到 501,否则为 500。

&& 左操作数为零时跳过右操作数;|| 左操作数非零时跳过右操作数。在 pointer != 0 && pointer->active != 0 中,空指针会阻止成员访问。16 位值的两个字节都参加真值判断,因此 256 为真。按位 & 和 | 不短路。

++value 返回更新后的值,value++ 返回原值,也可操作数组元素、解引用指针及结构体成员。buffer[index()]++ 只调用一次 index()。对于 u16 *p,p++ 前进两字节到下一个元素,(*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 前进两字节,指向所选数组的第二个元素,不复制数组。

condition ? yes : no 先计算条件,再只计算选中的分支。条件和分支可含可变移位数,例如 on = (pattern & (0x80 >> bit)) != 0 ? 4 : 2; 根据所选位得到四或二。被 && 或 || 跳过的右操作数中的函数也不会调用。for 循环中的 continue 先执行一次更新表达式,再重算条件;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(); }

这些片段展示至少更新一次的循环,以及为当前状态选择处理函数的分支。请在程序中定义 update、condition、state 和各状态处理函数。完整循环示例见 fc_control.c;完整场景管理 ROM 见库的帧与场景示例。

注册 scene、entity 和 system 回调时,请匹配声明要求的参数和返回类型。库会在相应更新、绘制或帧等待操作中调用已注册处理函数。遵守各 API 的 ROM 存储体及映射要求,不要假定递归或可变参数具有桌面级支持。

6. 内存与 PPU

NES CPU 内部 RAM 位于 0x0000~0x07FF;0x0800 以上的镜像不是额外 RAM。6502 栈使用第 1 页,OAM 影子缓冲和队列还会保留其他区域。--nes-local-ram=START:LENGTH、--nes-temp-ram=START:LENGTH 是需要检查映射文件的高级设置。

PPU 有独立地址空间。CHR 提供图案,名称表安排图块,属性表选择调色板组,调色板保存颜色编号。背景属性通常作用于 16×16 像素区域,与 GB 图块属性不同。

7. NMI 与 VRAM 队列

NMI 是与显示帧边界相关的中断。显示期间直接大量写 PPU 可能破坏画面。初始化应在关闭显示时直接完成;平时更新使用 __vramq_put、__vramq_copy、__vramq_fill 并提交队列。

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

检查队列容量和源数据寿命。默认 NMI 负责执行队列。自定义 __nes_nmi 时,须保留所需队列处理、OAM 工作与寄存器保护。

8. Mapper 与 ROM 布局

选项常见起步用途
nrom小型固定 ROM 教学程序
uxrom / cnrom / axrom简单 PRG 或 CHR 切换
mmc1 / mmc3 / mmc5大程序及 mapper 专用功能
vrc6 / vrc7 / fme7分页及相应扩展功能
fds磁盘镜像输出

这些是编译器选项,不是硬件或模拟器功能完成度表。--board=surom512 选择特定 MMC1 电路板配置;仅填充文件使其达到 512 KiB,不能建立这种配置。请配合 KUROSAKI 的电路板审查。

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 布局和跨 bank 调用的电路板要求。更改 mapper 或镜像方式后,除生成 ROM 外,还要测试启动、滚动和数据切换。

普通的 ROM 数组下标读取和 C 指针引用会使这些数组放入公共存储体 0,除非 #pragma fixed_bank 明确固定了位置。这样,调用方位于可切换存储体时,数据仍然可见。显式远访问应在同一次调用中传入数组名及同名的 __bankof,例如 __farpeek8(__bankof(table), table)。仅查询存储体编号,不会请求公共存储体放置。固定存储体数据和汇编内引用仍要求自行维护正确映射;这是基于语法的放置检查,不是指针流分析。公共存储体仍有容量限制。

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 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 的 mapper 设置,二者必须匹配。外设应分别验证输入值、连接状态以及对普通手柄路径的影响。

10. 诊断与构建结果

KQ 诊断以及 symfind、src2asm、romdiff 等开发命令与 GB 类似,但继承的 GB 帮助选项未必都对应已实现的 NES 功能。FC 字典单独从 FC 源码与头文件收集。

关闭显示的初始化也可能产生直接 PPU 操作警告,例如 KQ2421。不要为消除警告而破坏安全初始化;应检查显示时机与运行日志。“没有错误”和“没有警告”是两种不同结果。

11. 参考示例:使用生成式 AI 开发游戏

填写需求后,将完整提示词交给 AI。内容涵盖实现、模拟器测试、SARAKURA 分析以及修复后的复测。

· 下载 Markdown

填写需求后,将本文完整交给 AI。命令假定 kitaqgb、kitaqfc、kokura、kurosaki、sarakura、kitaq-docs 仓库与 game-gb 或 game-fc 项目位于同一父目录。请从该父目录运行,并按实际环境调整路径。

需求

  • 游戏名称:<填写>
  • 类型与核心玩法:<填写>
  • 操作方式及成功、失败条件:<填写>
  • 必需的界面、关卡、敌人和道具:<填写>
  • 画面风格、背景音乐和音效:<填写,并注明所提供素材的路径>
  • 存档、通信、外设及其他要求:<填写,或无>
  • 项目目录:<填写>
  • 再分发要求:<例如,自编代码和原创素材可按 MIT 许可公开>
  • 目标:<NES/红白机卡带或 FDS>
  • Mapper、ROM 大小与镜像方式:<指定,或根据需求选择>
  • 视频制式与性能目标:<例如 NTSC,每秒更新游戏逻辑 60 次>

请执行的任务

请使用 KITAQFC 及其库实现游戏。使用 KUROSAKI 运行和调试,使用 SARAKURA 整理诊断并比较修复前后的结果。

不断重复以下过程,直到满足验收标准:明确规格 → 实现一个小改动 → 构建 → 输入操作并观察 → 调查原因 → 修复 → 在相同条件下复测。不能以写出计划、提供代码或编译成功作为完成依据。

确认环境和验收标准

  1. 阅读工作目录的说明、各工具的 README、HTML 手册,以及所用库的头文件和实现。记录可执行文件路径及版本或 SHA-256;以实际 --help 输出核对命令,以源码核对 API。
  2. 为输入、画面、声音、游戏进程和更新频率制定可判断的验收标准。例如,按下并松开 START 后开始游戏;碰撞扣除一条生命;暂停时指定声音静音,恢复后继续播放。
  3. 只就重要歧义提问,常规、可撤销的实现决策请自主推进。不得擅自降低需求或验收标准。
  4. 先用一个随附的小示例走通编译器、模拟器和 SARAKURA。它只能证明工具之间能衔接,不能代表所需游戏已经完成。

先实现一个可玩的最小流程

  • 小型游戏先考虑 NROM;若规模或分库需求需要,再选择 MMC3 等 Mapper。不能仅凭名称认定支持情况,应通过 inspect-rom、mapper-info、audit-board 和实现检查所需卡带板功能。
  • 规划 PRG/CHR 大小、CHR-ROM 或 CHR-RAM、镜像、固定库、中断向量和存档 RAM。优化或更改分库后,核对头部与实际布局。--nes-local-ram 使用 CPU 内部 RAM $0000–$07FF;避免与零页、栈、OAM 缓冲区及运行时、库占用区域重叠。
  • 使用 KITAQFC 的 C 方言、FC 库和 void main(void)。不要假定 GB API 兼容;有些头文件条目只有声明,需检查实现位置并纳入所需 .c 文件。
  • 考虑 PPU 寄存器、NMI、OAM DMA、每扫描线精灵上限、滚动、镜像、属性表和 APU/DMC 行为。队列剩余容量不等于 PPU VRAM 容量,应限制每次 NMI 的工作量。
  • 将提供的原创 ascii.c 字体转换为 FC CHR,检查 CHR、调色板、名称表和属性数据。FDS 的磁盘访问、存档和 BIOS 条件需单独验证,不可套用卡带的启动条件。
  • 先连通启动、标题界面、可控制角色、成功或失败及重新开始,再扩充内容。
  • 保留可编辑的图形、音乐、音效源文件及生成步骤,确认构建实际读取了导出数据。
  • 源码注释用英文,进度报告用简体中文。SARAKURA 的标准报告保持英文。

将每次构建与运行对应起来

使用 out/iter-001 等目录区分每轮输出。记录命令、退出码及源码、素材、工具、ROM、元数据的哈希。构建失败后,不得运行遗留的旧 ROM。映射文件、源码映射和调试信息必须与 ROM 来自同一次构建。

下面是无输入的 NROM 检查示例。请准备 main.c、所需库实现和 CHR 文件,并选择合适的 Mapper。未经格式核对,不要把编译器构建元数据传给 KUROSAKI 的 --kitaqfc-debug。

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 帧只是初步检查。在认定游戏正常之前,必须加入有先后顺序的游玩场景。

复现有先后顺序的输入

  • --pad1 和 --pad2 使用 NES 原始位掩码:A=1、B=2、SELECT=4、START=8、UP=16、DOWN=32、LEFT=64、RIGHT=128。不要与库中的 BTN_* 值混淆。
  • run --pad1 是固定输入。连续操作应使用回放,并区分按下、按住和松开。阅读 Replay、ReplayFrame 定义。公开 CLI 的 replay-record 记录的是无输入状态,不是玩家的交互操作。
  • 自行核对 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 准备对应回放。公开 CLI 的 replay-run 没有 --emit-diagnostics 选项。不要编造选项,也不要将 CPU 跟踪当作诊断事件传入。若要用 SARAKURA 分析顺序输入场景,应在项目内编写测试驱动程序,使用公开 kurosaki-core 的 RunOptions.replay_frames 和 diagnostic_events_from_trace_and_report。以相同 ROM、回放和帧条件运行,从该次执行的跟踪与诊断报告生成 JSONL。检查跟踪配置、保留范围,并将驱动程序的画面和观测结果与 CLI 回放对照。不得把无输入运行的诊断当作游玩场景的证据。缺少所需环境时,应将这项验证明确列为未完成。

检查画面、声音、状态和性能

  • 保存输入场景,区分按下、按住和松开。覆盖规格中的全部路径:启动、开始、移动、动作、碰撞、滚动、关卡切换、游戏结束、重新开始、暂停,以及适用的存档和通信。
  • 保存关键帧 PNG、输入、运行报告、诊断 JSONL、WAV 和必要的状态、内存观测。检查实际到达帧数与停止原因。真正打开图像查看;一张截图不能证明运动或输入响应。将计数器、坐标和状态变化与预期值对照,同时检查画面边缘、图块和属性边界及精灵密集场景。
  • 检查音乐、音效、同时发声、断音、暂停和恢复。仅生成 WAV 不能证明声音正确。无法试听时,应区分已完成的波形、数值检查与尚未确认的听感。
  • 测量高负载场景的目标 CPU 工作量、游戏更新及传输量,FC 还需计入 NMI 工作。宿主机上模拟器的运行速度不等于游戏更新频率,也不是实机速度证明。使用 --allow-unimplemented 后能继续运行,不代表未实现功能已受支持。

分析、修复并复测

  • 向 SARAKURA 提交被测 ROM 的构建元数据和该次运行的诊断 JSONL。CPU 跟踪或普通运行报告不能替代它。--frames 指定分析条件;SARAKURA 不执行 ROM,也不自动修改源码。
  • 阅读 report.html、ai_diagnostics.json、repair_prompt.md、retest_plan.json,与复现步骤、画面、声音和源码核对。区分推测的源码位置、原因与已确认事实,并区分正常等待循环与卡死。逐项判断警告,记录未支持事件和分析范围限制。不要通过过滤警告或缩短测试来获得通过结果。
  • 把问题缩减为最小复现,修复原因后重新构建。若根因在编译器或模拟器,应与游戏代码问题分离,并为工具修复补充回归验证。
  • 复测时保持输入、随机种子、机型和视频制式、Mapper、观测帧和诊断设置一致。新 ROM 使用对应元数据;代码或 RAM 布局变化后,不得盲目复用即时存档。
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

诊断差异应与操作、画面和声音的验收结果结合使用。相同失败反复出现时,应重审证据与假设,不要无依据地继续修改。

完成条件与交付内容

用交付源码和设置构建最终 ROM,再执行全部必需场景。仅使用无敌状态、自动测试输入或另一种 Mapper,不能证明最终版本的正常游玩。提供需求与测试对应表,说明剩余警告的原因,明确未验证、未支持项目。未做实机测试时标注“实机未验证”。

交付源码、工具和库的标识信息、可编辑素材、可复现的构建与测试脚本、ROM、最终验证证据,以及说明安装、操作和已知限制的 README。按需附上回放和测试驱动程序。仅在明确授权范围内发布或向外部发送文件。验证后删除不必要的中间构建和临时跟踪,但保留源码、素材、最终成果及必要的回归证据。

若环境或权限阻碍必需检查,应报告准确的复现步骤和所需操作,不得标记为完成。

源码位置

编译器源码和项目文件位于仓库内的同名子目录,可执行文件位于仓库根目录,库文件位于 lib/。项目路径和构建要求请参阅目录结构说明。

完整示例程序

程序、公共头文件、提供的 ascii.c 及转换素材均已附带。m_ 开头的名称是本手册的辅助函数。请将构建命令与预期结果、验证记录一起阅读,并从各并列仓库的父目录执行命令。

批量构建脚本 / 结果与截图

显示所提供的字符集 fc_font.c

用途: 查看所提供原创字体包含哪些字母、数字和标点,以及如何绘制。

工作原理: m_init 准备显示和字体。m_text 绘制每一行;m_put 通过字符码 34 绘制双引号。

操作与预期结果: 应看到 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。前导零来自三位十进制格式。

第一次显示文字:画面显示 HELLO WORLD 和 042。前导零来自三位十进制格式。
显示示例:画面显示 HELLO WORLD 和 042。前导零来自三位十进制格式。

下载源码 / 验证状态

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

用途: 学习保留超出八位范围的中间结果。

工作原理: (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。

工作原理: for 循环将 0 至 7 相加但跳过 3,得到 25。while 循环推进到 32;随后 if 在匹配该值时赋值 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

用途: 学习组织数值,并通过函数和指针访问。

工作原理: 程序复制含 (10,20) 的 Point,再通过指针将 1 至 4 写入数组。将函数结果 30、数组值 1、2、4 和常量 5 相加。

操作与预期结果: 总和显示为 042。请跟踪结构体复制、ptr[i] 写入和函数返回值。

函数、数组、结构体与指针:总和显示为 042。请跟踪结构体复制、ptr[i] 写入和函数返回值。
显示示例:总和显示为 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

用途: 将标志打包到字节中,并将缓冲区复制到独立存储空间。

工作原理: __memset 清除四个字节。程序设置第 3 位、翻转第 1 位,再用 __memcpy 复制全部四字节。

操作与预期结果: 目标首字节显示为 010:第 3 位贡献 8,第 1 位贡献 2。

位标志与缓冲区复制:目标首字节显示为 010,第 3 位贡献 8,第 1 位贡献 2。
显示示例:目标首字节显示为 010:第 3 位贡献 8,第 1 位贡献 2。

下载源码 / 验证状态

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、乘法以及转回整数。

工作原理: fix_from_int 将 3 和 4 转为 Q8.8,fix_mul 将它们相乘,fix_to_int 再将乘积转回整数。

操作与预期结果: 012 表示 3 × 4 = 12。这是转换后的整数,不是按 256 缩放的内部值。

Q8.8 定点乘法:012 表示 3 × 4 = 12。这是转换后的整数,不是按 256 缩放的内部值。
显示示例:012 表示 3 × 4 = 12。这是转换后的整数,不是按 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 重置对象池。entity_create 在 (20,30) 创建类型 1;程序检查失败 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

用途: 将有名称的游戏数值存入诊断缓冲区,与画面显示分开。

工作原理: 程序初始化调试,设置帧标签 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

用途: 学习用环形缓冲区保存位置轨迹。

工作原理: chain_init 关联可容纳四个点的数组,随后 chain_push_head 记录 (20,30)。

操作与预期结果: 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 两次,再显示 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

用途: 学习选择 ID 以区分标题画面、游戏过程等场景。

工作原理: 程序注册两个零初始化的 SceneDef 条目,通过 scene_change 选择 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

用途: 将亚像素位移保留为余数,整像素部分进位到位置。

工作原理: 从 X=40 开始,循环八次,每次加 2/8 像素。右移提取整数像素,掩码保留余数。

操作与预期结果: 总位移为两像素,因此画面将 X=42 显示为 042。此手工积分示例处理正向位移;负向位移需要对应的借位处理。

累积 Q5.3 小数位移:总位移为两像素,因此画面将 X=42 显示为 042。此手工积分示例处理正向位移;负向位移需要对应的借位处理。
显示示例:总位移为两像素,因此画面将 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,并将其作为水平位置。

操作与预期结果: 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 将图块 52 排入名称表地址 0x2103 的更新队列,__vramq_commit 提交更新。该地址对应图块坐标 (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 停止。仅凭显示标记不能验证声音输出。

播放 APU 脉冲音:显示 042 时持续播放脉冲音。调用 nes_apu_silence_all 停止。仅凭显示标记不能验证声音输出。
显示示例:显示 042 时持续播放脉冲音。调用 nes_apu_silence_all 停止。仅凭显示标记不能验证声音输出。

下载源码 / 验证状态

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 个条目。展开可查看语法、参数、源码说明和示例。调用片段依赖外围初始化,不能单独构建 ROM。参数传递示例只说明如何把准备好的值交给 API,不代表已完成设备级验证。

audio_vblank.h — 通过 VBlank IRQ 播放音乐

由 NMI 推进的四声道音乐库。使用顺序为初始化、设置曲目并预填队列、启用 NMI 播放,然后在主循环中持续 refill。NMI 只读取 RAM 队列,只要及时补充,少量 RAM 就能支持长音频流。每个声道都可独立使用 HOLD/STOP。

每条记录包含五字节,顺序为 等待时间, CH1, CH2, CH3, CH4。等待时间 1~255 表示本次声道状态持续的 NMI 次数;零表示结束播放,其余四字节被忽略。CH1~CH3 的音符值 0~71 对应 C2~B7,其中 24=C4。CH4 的 0~15 选择长序列噪声的周期编号,16~31 选择短序列模式。所有声道均使用 255=NES_AUDIO_HOLD 保持状态、254=NES_AUDIO_STOP 停止发声。

这是面向 NTSC 的四声道驱动:CH1 为脉冲 1,CH2 为脉冲 2,CH3 为三角波,CH4 为噪声,对应索引 0~3。不支持 DMC 或扩展音源。不要同时运行 audio.c 音效函数等直接改写同一 APU 的代码。本库没有 PAL 音高表或速度补偿。

当前等待时间耗尽却没有下一条 BGM 记录时,停止 BGM 发声;数据补齐后在随后的 NMI 中恢复。独立音效仍继续推进。欠载计数统计进入缺数据状态的次数,而非空队列持续的帧数。仅补入 HOLD 不会恢复停止前的音符。通常应每帧调用 refill,并在执行耗时工作前为队列准备足够的播放时长。

fds.h — FDS 磁盘操作

__fds_available

报告编译目标是否为 FDS,例如用于共享游戏代码显示目标专用信息。

语法

c
u8 __fds_available(void);

返回值

FDS 构建返回 1,否则返回 0。这不是运行时外设检测。

使用条件与限制

此查询也适用于 NROM 等卡带目标。把 FDS 专用 API 放在 if (__fds_available()) 内,不会免除该 API 的目标检查;请相应组织目标专用源码。

截图使用 KUROSAKI 将磁盘启动文件直接载入 RAM 的方式。本示例不调用 BIOS 例程,不验证文件传送、换盘或硬件启动。空测试固件不是替代 BIOS。示例用 NMI 更新画面,并禁用可屏蔽 IRQ;这些查询不需要 IRQ。

构建示例生成原生布局的 .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 位为零时返回 1,为一时返回 0。

使用条件与限制

使用 --mapper=fds 构建。卡带映射器目标调用除 __fds_available 以外的 FDS 专用函数时,会以 KQFC2420 拒绝编译。

检查使用报告磁盘存在状态的 $4032 第 0 位。不检查第 1 位的驱动器就绪状态、文件是否存在或加载是否完成。这里的“ready”不能理解为可以开始传送。

KUROSAKI 的 FDS 控制器模型尚不完整。本示例观测到存在值 1、保护掩码 0、状态 0;截图不能证明弹出磁盘、写保护介质或 CRC 错误的行为。

截图使用 KUROSAKI 将磁盘启动文件直接载入 RAM 的方式。本示例不调用 BIOS 例程,不验证文件传送、换盘或硬件启动。空测试固件不是替代 BIOS。示例用 NMI 更新画面,并禁用可屏蔽 IRQ;这些查询不需要 IRQ。

构建示例生成原生布局的 .fds,不附加认证画面启动文件。此配置用于检查 KUROSAKI 直接启动;硬件磁盘的启动流程需另行验证。

NESdev:FDS 硬件寄存器

使用示例

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

读取 $4030 的 FDS 硬件状态寄存器。这不是上一次磁盘操作保存的错误码。

语法

c
u8 __fds_error(void);

返回值

返回完整的八位 $4030 值。处理 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;这些查询不需要 IRQ。

构建示例生成原生布局的 .fds,不附加认证画面启动文件。此配置用于检查 KUROSAKI 直接启动;硬件磁盘的启动流程需另行验证。

NESdev:FDS 硬件寄存器

使用示例

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,不是规范化布尔值。硬件第 2 位表示写保护或磁盘已弹出。

使用条件与限制

使用 --mapper=fds 构建。卡带映射器目标调用除 __fds_available 以外的 FDS 专用函数时,会以 KQFC2420 拒绝编译。

KUROSAKI 的 FDS 控制器模型尚不完整。本示例观测到存在值 1、保护掩码 0、状态 0;截图不能证明弹出磁盘、写保护介质或 CRC 错误的行为。

检查保护状态请使用 (__fds_side() != 0)。判断保存是否成功,还需结合磁盘存在状态和 BIOS 操作返回值。

截图使用 KUROSAKI 将磁盘启动文件直接载入 RAM 的方式。本示例不调用 BIOS 例程,不验证文件传送、换盘或硬件启动。空测试固件不是替代 BIOS。示例用 NMI 更新画面,并禁用可屏蔽 IRQ;这些查询不需要 IRQ。

构建示例生成原生布局的 .fds,不附加认证画面启动文件。此配置用于检查 KUROSAKI 直接启动;硬件磁盘的启动流程需另行验证。

NESdev:FDS 硬件寄存器

使用示例

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;这些查询不需要 IRQ。

构建示例生成原生布局的 .fds,不附加认证画面启动文件。此配置用于检查 KUROSAKI 直接启动;硬件磁盘的启动流程需另行验证。

NESdev:FDS 硬件寄存器

使用示例

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;这些查询不需要 IRQ。

构建示例生成原生布局的 .fds,不附加认证画面启动文件。此配置用于检查 KUROSAKI 直接启动;硬件磁盘的启动流程需另行验证。

NESdev:FDS 硬件寄存器

使用示例

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。大小为零的记录仍视为存在。

语法

c
u8 __fds_file_exists(u8 id);

参数

id

要查询的文件 ID。元数据使用 0..254;255 保留作结束标记。ID 不等于文件编号、文件名或目标地址。

返回值

编译后的表包含该 ID 时返回 1,否则返回 0。不保证插入的磁盘包含该文件。

使用条件与限制

使用 --mapper=fds 构建。卡带映射器目标调用除 __fds_available 以外的 FDS 专用函数时,会以 KQFC2420 拒绝编译。

搜索由 --fds-meta=... 或 --fds-manifest=... 编译的表,以及自动生成的覆盖记录。不读取磁盘目录。即使文件已打包到磁盘中,没有表记录也无法查到。手工记录和生成记录合计最多为 42 条。

大小是元数据中声明的值,不一定等于源文件推导长度、裁剪后覆盖数据长度,或换入磁盘的内容。若用这些查询限制数据访问范围,请准确声明大小。

将 fds_metadata.json 及其两个 .bin 文件放在 C 源文件旁。ID 16 包含独立编写的 513 字节序列:0..255 重复两遍,最后为零。ID 17 的有效载荷为空。两者都是非启动 PRG 文件(类型 0),本示例不加载它们。相对源路径以清单所在目录为基准解析。

截图使用 KUROSAKI 将磁盘启动文件直接载入 RAM 的方式。本示例不调用 BIOS 例程,不验证文件传送、换盘或硬件启动。空测试固件不是替代 BIOS。示例用 NMI 更新画面,并禁用可屏蔽 IRQ;这些查询不需要 IRQ。

构建示例生成原生布局的 .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 存在、大小为 513 字节;ID 17 存在、大小为零;ID 18 不存在、大小为零。TABLE VALUES MATCH: PASS 表示读取值与此组合一致。仅凭大小为零无法区分空记录与不存在的 ID。

__fds_file_exists: METADATA-FDS-DEFAULT — 表中显示 ID 16 存在、大小为 513 字节;ID 17 存在、大小为零;ID 18 不存在、大小为零。TABLE VALUES MATCH: PASS 表示读取值与此组合一致。仅凭大小为零无法区分空记录与不存在的 ID。
METADATA-FDS-DEFAULT — 表中显示 ID 16 存在、大小为 513 字节;ID 17 存在、大小为零;ID 18 不存在、大小为零。TABLE VALUES MATCH: PASS 表示读取值与此组合一致。仅凭大小为零无法区分空记录与不存在的 ID。

完整示例程序

构建命令

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 保留作结束标记。ID 不等于文件编号、文件名或目标地址。

返回值

声明的无符号 16 位大小,范围为 0..65535。ID 不存在时也返回零;需要区分时,请使用 __fds_file_exists()。

使用条件与限制

使用 --mapper=fds 构建。卡带映射器目标调用除 __fds_available 以外的 FDS 专用函数时,会以 KQFC2420 拒绝编译。

搜索由 --fds-meta=... 或 --fds-manifest=... 编译的表,以及自动生成的覆盖记录。不读取磁盘目录。即使文件已打包到磁盘中,没有表记录也无法查到。手工记录和生成记录合计最多为 42 条。

大小是元数据中声明的值,不一定等于源文件推导长度、裁剪后覆盖数据长度,或换入磁盘的内容。若用这些查询限制数据访问范围,请准确声明大小。

将 fds_metadata.json 及其两个 .bin 文件放在 C 源文件旁。ID 16 包含独立编写的 513 字节序列:0..255 重复两遍,最后为零。ID 17 的有效载荷为空。两者都是非启动 PRG 文件(类型 0),本示例不加载它们。相对源路径以清单所在目录为基准解析。

截图使用 KUROSAKI 将磁盘启动文件直接载入 RAM 的方式。本示例不调用 BIOS 例程,不验证文件传送、换盘或硬件启动。空测试固件不是替代 BIOS。示例用 NMI 更新画面,并禁用可屏蔽 IRQ;这些查询不需要 IRQ。

构建示例生成原生布局的 .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 存在、大小为 513 字节;ID 17 存在、大小为零;ID 18 不存在、大小为零。TABLE VALUES MATCH: PASS 表示读取值与此组合一致。仅凭大小为零无法区分空记录与不存在的 ID。

__fds_file_size: METADATA-FDS-DEFAULT — 表中显示 ID 16 存在、大小为 513 字节;ID 17 存在、大小为零;ID 18 不存在、大小为零。TABLE VALUES MATCH: PASS 表示读取值与此组合一致。仅凭大小为零无法区分空记录与不存在的 ID。
METADATA-FDS-DEFAULT — 表中显示 ID 16 存在、大小为 513 字节;ID 17 存在、大小为零;ID 18 不存在、大小为零。TABLE VALUES MATCH: PASS 表示读取值与此组合一致。仅凭大小为零无法区分空记录与不存在的 ID。

完整示例程序

构建命令

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。它与文件的物理位置不同。ID 255、不存在的 ID 和启动文件都会被拒绝。

dst

PRG 文件的目标地址,空间须容纳打包后的完整长度。传入零则保留数据在文件头指定的地址,不进行复制。CHR/名称表 PPU 文件只接受零;BIOS 会传送到原生 PPU 目标地址。

返回值

成功返回零。非零 BIOS 错误原样返回。BIOS 名义上成功但并未恰好加载一个文件时,返回 64 ($40)。ID、缓冲区或文件类型/目标组合无效时返回 255 ($FF)。

使用条件与限制

配合 --mapper=fds --fds-layout=fds32 使用。--fds-meta 清单定义文件 ID、加载地址、类型、是否为启动文件及排列顺序。传送地址和大小来自构建生成的磁盘镜像;本 API 不会检查任意插入的磁盘。

BIOS 先将数据加载到磁盘文件头记录的地址,本辅助函数再复制到 dst。它会选择复制方向以保护重叠数据,但暂存区域也必须预留。传送失败时不复制到 dst。调用 BIOS 后,驻留存储体记录设为 255,表示未知。

每个 CPU 缓冲区必须完整位于 $0200–$07FF 或 $6000–$DFFF 其中一个范围内。跨地址回绕的范围及 I/O 寄存器目标会被拒绝。调用方仍须避免缓冲区与代码、OAM、其他变量或编译器临时存储重叠。示例使用存储体 0 放置公共代码,$8000 作为暂存区,$8500 和 $8600 作为四字节缓冲区。

这些是同步 BIOS 调用。请从公共代码调用,以免传送覆盖正在执行的代码或返回地址;不要从中断处理程序调用。磁盘 I/O 期间须满足 BIOS 前提:停止渲染和 NMI,将 PPU 地址增量设为一,并禁用非磁盘 IRQ 源。不保证传送期间游戏或音乐能持续运行。

截图采用独立编写的 BIOS 调用约定测试替身,用于检查编译后的 CPU 代码、缓冲区内容、保存/读回行为及每个显示像素。它不验证真实 BIOS、实体磁盘写入、CRC 或断电恢复。仅在 KUROSAKI 中普通运行 FDS,不能证明具有相同的保存行为。

请将示例与同目录的 fds_file_io_manifest.json、fds_file_io_payload.bin 一起使用。下方检查程序从独立编写的源代码生成临时测试替身,完成后删除。不包含商业 BIOS;该替身也不是供普通游戏使用的 BIOS。

FDS BIOS 调用约定

使用示例

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 的四字节数据的首尾值。示例将数据改为 7、14、21、28,保存后重新加载到 $8600,因此 SAVED FIRST / LAST 显示 007 和 028。STAGING 8000 / SIZE 4 表示磁盘记录的加载地址及文件长度。独立测试还检查 ID 73 实际保存到第四个文件,即从零开始的位置 3。

__fds_load_file: DEFAULT — LOAD、SAVE、RELOAD STATUS 和 BYTE CHECK ERRORS 均显示 000。INITIAL FIRST/LAST 显示 011 和 044,对应加载到 $8500 的四字节数据的首尾值。示例将数据改为 7、14、21、28,保存后重新加载到 $8600,因此 SAVED FIRST / LAST 显示 007 和 028。STAGING 8000 / SIZE 4 表示磁盘记录的加载地址及文件长度。独立测试还检查 ID 73 实际保存到第四个文件,即从零开始的位置 3。
DEFAULT — LOAD、SAVE、RELOAD STATUS 和 BYTE CHECK ERRORS 均显示 000。INITIAL FIRST/LAST 显示 011 和 044,对应加载到 $8500 的四字节数据的首尾值。示例将数据改为 7、14、21、28,保存后重新加载到 $8600,因此 SAVED FIRST / LAST 显示 007 和 028。STAGING 8000 / SIZE 4 表示磁盘记录的加载地址及文件长度。独立测试还检查 ID 73 实际保存到第四个文件,即从零开始的位置 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

用 CPU 缓冲区的字节覆盖磁盘面末尾预留的固定大小 PRG 存档槽。文件保留其 ID、名称和以后加载时使用的地址。

语法

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

参数

id

生成磁盘中一个非启动文件的 ID,范围为 0..254;示例使用 73。它与文件的物理位置不同。ID 255、不存在的 ID 和启动文件都会被拒绝。

src

CPU RAM 中数据的起始位置,须有 len 个可读字节。本 API 不直接保存 VRAM。

len

要保存的字节数,必须等于打包文件长度。若游戏数据较少,请在调用前填充存档缓冲区未使用的部分。

返回值

成功返回零,否则返回 BIOS 错误,例如写保护或写入校验失败。ID 无效、文件为启动文件或非 PRG 文件、不是磁盘面最后一个文件、长度不匹配或 CPU 范围无效时,不调用 BIOS,直接返回 255 ($FF)。

使用条件与限制

配合 --mapper=fds --fds-layout=fds32 使用。--fds-meta 清单定义文件 ID、加载地址、类型、是否为启动文件及排列顺序。传送地址和大小来自构建生成的磁盘镜像;本 API 不会检查任意插入的磁盘。

BIOS WriteFile 会在逻辑上隐藏写入位置之后的文件。因此,本 API 只接受每个磁盘面最后一个非启动 PRG 文件。不会追加文件,也不会调整槽位大小。在最终布局中,槽位必须位于末尾;生成的启动辅助文件和自动覆盖文件也计入布局。

编译器区分文件 ID、文件头编号和物理顺序。示例的 ID 为 73,清单编号为 200,但传给 BIOS 的位置是 3。磁盘编号和面编号也来自最终解析的布局。不要用于布局已独立于清单发生变化的磁盘。

每个 CPU 缓冲区必须完整位于 $0200–$07FF 或 $6000–$DFFF 其中一个范围内。跨地址回绕的范围及 I/O 寄存器目标会被拒绝。调用方仍须避免缓冲区与代码、OAM、其他变量或编译器临时存储重叠。示例使用存储体 0 放置公共代码,$8000 作为暂存区,$8500 和 $8600 作为四字节缓冲区。

这些是同步 BIOS 调用。请从公共代码调用,以免传送覆盖正在执行的代码或返回地址;不要从中断处理程序调用。磁盘 I/O 期间须满足 BIOS 前提:停止渲染和 NMI,将 PPU 地址增量设为一,并禁用非磁盘 IRQ 源。不保证传送期间游戏或音乐能持续运行。

截图采用独立编写的 BIOS 调用约定测试替身,用于检查编译后的 CPU 代码、缓冲区内容、保存/读回行为及每个显示像素。它不验证真实 BIOS、实体磁盘写入、CRC 或断电恢复。仅在 KUROSAKI 中普通运行 FDS,不能证明具有相同的保存行为。

请将示例与同目录的 fds_file_io_manifest.json、fds_file_io_payload.bin 一起使用。下方检查程序从独立编写的源代码生成临时测试替身,完成后删除。不包含商业 BIOS;该替身也不是供普通游戏使用的 BIOS。

FDS BIOS 调用约定

使用示例

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 的四字节数据的首尾值。示例将数据改为 7、14、21、28,保存后重新加载到 $8600,因此 SAVED FIRST / LAST 显示 007 和 028。STAGING 8000 / SIZE 4 表示磁盘记录的加载地址及文件长度。独立测试还检查 ID 73 实际保存到第四个文件,即从零开始的位置 3。

__fds_save_file: DEFAULT — LOAD、SAVE、RELOAD STATUS 和 BYTE CHECK ERRORS 均显示 000。INITIAL FIRST/LAST 显示 011 和 044,对应加载到 $8500 的四字节数据的首尾值。示例将数据改为 7、14、21、28,保存后重新加载到 $8600,因此 SAVED FIRST / LAST 显示 007 和 028。STAGING 8000 / SIZE 4 表示磁盘记录的加载地址及文件长度。独立测试还检查 ID 73 实际保存到第四个文件,即从零开始的位置 3。
DEFAULT — LOAD、SAVE、RELOAD STATUS 和 BYTE CHECK ERRORS 均显示 000。INITIAL FIRST/LAST 显示 011 和 044,对应加载到 $8500 的四字节数据的首尾值。示例将数据改为 7、14、21、28,保存后重新加载到 $8600,因此 SAVED FIRST / LAST 显示 007 和 028。STAGING 8000 / SIZE 4 表示磁盘记录的加载地址及文件长度。独立测试还检查 ID 73 实际保存到第四个文件,即从零开始的位置 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);

返回值

八位软件驻留记录;标准启动后立即为 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;这些查询不需要 IRQ。

构建示例生成原生布局的 .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,不会使该存储体在启动时驻留。使用 --fds-no-overlay-table 的验证构建中,只有最后一个值变为零。

__fds_current_bank: RESIDENCY-FDS-DEFAULT — CURRENT BANK 为 1,BANK 1 RESIDENT 为 1,BANK 2 RESIDENT 为 0,OVERLAY FUNCTIONS 为 2。将两个函数打包在存储体 2,不会使该存储体在启动时驻留。使用 --fds-no-overlay-table 的验证构建中,只有最后一个值变为零。
RESIDENCY-FDS-DEFAULT — CURRENT BANK 为 1,BANK 1 RESIDENT 为 1,BANK 2 RESIDENT 为 0,OVERLAY FUNCTIONS 为 2。将两个函数打包在存储体 2,不会使该存储体在启动时驻留。使用 --fds-no-overlay-table 的验证构建中,只有最后一个值变为零。
__fds_current_bank: RESIDENCY-FDS-TABLE-OFF-DEFAULT — CURRENT BANK 为 1,BANK 1 RESIDENT 为 1,BANK 2 RESIDENT 为 0,OVERLAY FUNCTIONS 为 2。将两个函数打包在存储体 2,不会使该存储体在启动时驻留。使用 --fds-no-overlay-table 的验证构建中,只有最后一个值变为零。
RESIDENCY-FDS-TABLE-OFF-DEFAULT — CURRENT BANK 为 1,BANK 1 RESIDENT 为 1,BANK 2 RESIDENT 为 0,OVERLAY FUNCTIONS 为 2。将两个函数打包在存储体 2,不会使该存储体在启动时驻留。使用 --fds-no-overlay-table 的验证构建中,只有最后一个值变为零。

完整示例程序

构建命令

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) 调用。

返回值

目标函数的八位结果。若加载失败但能恢复先前存储体,则不调用目标,而是返回加载错误字节。请设计返回值约定,使错误与正常结果可区分。

使用条件与限制

配合 --mapper=fds --fds-layout=fds32 使用。存储体 0 的公共代码位于 $A000–$DFFF。启动时存储体 1 占据 $6000–$9FFF;存储体 2 及以上作为覆盖内容加载到同一个 16 KiB 窗口。可用 #pragma fixed_bank 2 等指令放置覆盖代码或数据。

存储体 1 从位于 $6000 的独立 16,384 字节启动 PRG 文件恢复。不提供清单时,默认文件为:ID 0 对应存储体 1,ID 1 对应公共代码,ID 2 对应 CHR。自定义清单必须将启动 PRG 拆分为位于 $6000 和 $A000 的两个 16,384 字节文件。存储体 2 及以上的文件 ID 为 --fds-overlay-start-id(默认 32)+ 存储体编号 − 2。

跨存储体调用会保存先前存储体,并在返回前恢复。嵌套调用遵循相同规则。传送部分失败后也会尝试恢复。若恢复本身失败,执行会停止在公共代码中,而不是返回已被覆盖的代码。公共存储体目标和同存储体调用会避免不必要的传送。

BIOS 调用是同步的;本 API 不会在加载期间保持游戏主循环每帧更新。请从公共存储体中的普通控制代码调用,不要从中断处理程序调用。画面/音频连续性以及与真实 BIOS 的共存情况,需要另行验证。

这些截图采用独立编写的 LoadFiles 调用约定测试替身,检查编译后的 CPU 代码、内存传送及每个显示像素。不验证真实 BIOS、磁盘传送时序、CRC 或硬件启动。KUROSAKI 的 FDS 控制器模型尚不完整;不使用该替身的普通运行,不能证明相同的传送结果。

下方第一条编译命令构建可下载示例。Python 检查程序分别启用和关闭优化,构建全部五个教学示例,再在 KUROSAKI 中检查。它根据 scripts/fds_abi_fixture.py 创建并删除临时替身;这些检查无需外部 BIOS。该替身不是普通游戏用的 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 ABI 替身时,RETURN VALUE 为 042;CALLBACK COUNT 和 BANK EXPR COUNT 均为 001;BANK IN CALLBACK 为 002;BANK AFTER RETURN 为 001。函数和存储体表达式各执行一次,调用存储体 2 后恢复了存储体 1。这不是真实磁盘传送测试的截图。

__fds_farcall: FARCALL-DEFAULT — 使用独立编写的 LoadFiles ABI 替身时,RETURN VALUE 为 042;CALLBACK COUNT 和 BANK EXPR COUNT 均为 001;BANK IN CALLBACK 为 002;BANK AFTER RETURN 为 001。函数和存储体表达式各执行一次,调用存储体 2 后恢复了存储体 1。这不是真实磁盘传送测试的截图。
FARCALL-DEFAULT — 使用独立编写的 LoadFiles ABI 替身时,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。

使用条件与限制

使用 --mapper=fds 构建。卡带映射器目标调用除 __fds_available 以外的 FDS 专用函数时,会以 KQFC2420 拒绝编译。

标准 FDS 启动将驻留记录初始化为 1。__fds_load_bank() 加载成功后更新该记录。通过 __fds_load_file() 或 __fds_load_overlay() 进行的原始加载,以及失败的存储体传送,都会将记录设为 255,表示未知。不检查内存内容;直接修改窗口后,不要依赖此记录。自定义复位例程须自行初始化。

截图使用 KUROSAKI 将磁盘启动文件直接载入 RAM 的方式。本示例不调用 BIOS 例程,不验证文件传送、换盘或硬件启动。空测试固件不是替代 BIOS。示例用 NMI 更新画面,并禁用可屏蔽 IRQ;这些查询不需要 IRQ。

构建示例生成原生布局的 .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,不会使该存储体在启动时驻留。使用 --fds-no-overlay-table 的验证构建中,只有最后一个值变为零。

__fds_is_bank_resident: RESIDENCY-FDS-DEFAULT — CURRENT BANK 为 1,BANK 1 RESIDENT 为 1,BANK 2 RESIDENT 为 0,OVERLAY FUNCTIONS 为 2。将两个函数打包在存储体 2,不会使该存储体在启动时驻留。使用 --fds-no-overlay-table 的验证构建中,只有最后一个值变为零。
RESIDENCY-FDS-DEFAULT — CURRENT BANK 为 1,BANK 1 RESIDENT 为 1,BANK 2 RESIDENT 为 0,OVERLAY FUNCTIONS 为 2。将两个函数打包在存储体 2,不会使该存储体在启动时驻留。使用 --fds-no-overlay-table 的验证构建中,只有最后一个值变为零。
__fds_is_bank_resident: RESIDENCY-FDS-TABLE-OFF-DEFAULT — CURRENT BANK 为 1,BANK 1 RESIDENT 为 1,BANK 2 RESIDENT 为 0,OVERLAY FUNCTIONS 为 2。将两个函数打包在存储体 2,不会使该存储体在启动时驻留。使用 --fds-no-overlay-table 的验证构建中,只有最后一个值变为零。
RESIDENCY-FDS-TABLE-OFF-DEFAULT — CURRENT BANK 为 1,BANK 1 RESIDENT 为 1,BANK 2 RESIDENT 为 0,OVERLAY FUNCTIONS 为 2。将两个函数打包在存储体 2,不会使该存储体在启动时驻留。使用 --fds-no-overlay-table 的验证构建中,只有最后一个值变为零。

完整示例程序

构建命令

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 或 255、文件 ID 溢出,或存储体 1 缺少恢复文件时返回 255。BIOS 传送失败时返回其错误字节。传送前的参数错误保留驻留记录;BIOS 传送失败则将其设为 255,表示未知。

使用条件与限制

配合 --mapper=fds --fds-layout=fds32 使用。存储体 0 的公共代码位于 $A000–$DFFF。启动时存储体 1 占据 $6000–$9FFF;存储体 2 及以上作为覆盖内容加载到同一个 16 KiB 窗口。可用 #pragma fixed_bank 2 等指令放置覆盖代码或数据。

存储体 1 从位于 $6000 的独立 16,384 字节启动 PRG 文件恢复。不提供清单时,默认文件为:ID 0 对应存储体 1,ID 1 对应公共代码,ID 2 对应 CHR。自定义清单必须将启动 PRG 拆分为位于 $6000 和 $A000 的两个 16,384 字节文件。存储体 2 及以上的文件 ID 为 --fds-overlay-start-id(默认 32)+ 存储体编号 − 2。

默认启用驻留检查;已记录为驻留的存储体直接返回零,不读取磁盘。--fds-no-overlay-guard 会关闭 __fds_load_bank() 中这一快捷路径。加载成功后记录请求的存储体。该记录不是对内存内容的验证。

从 $6000–$9FFF 窗口中的代码直接加载其他存储体,可能覆盖当前函数或其返回地址。示例将调用方及显示代码放在 #pragma fixed_bank 0。若要调用另一存储体中的函数并返回,请使用 __fds_overlay_farcall() 或普通的带类型函数调用。

BIOS 调用是同步的;本 API 不会在加载期间保持游戏主循环每帧更新。请从公共存储体中的普通控制代码调用,不要从中断处理程序调用。画面/音频连续性以及与真实 BIOS 的共存情况,需要另行验证。

这些截图采用独立编写的 LoadFiles 调用约定测试替身,检查编译后的 CPU 代码、内存传送及每个显示像素。不验证真实 BIOS、磁盘传送时序、CRC 或硬件启动。KUROSAKI 的 FDS 控制器模型尚不完整;不使用该替身的普通运行,不能证明相同的传送结果。

下方第一条编译命令构建可下载示例。Python 检查程序分别启用和关闭优化,构建全部五个教学示例,再在 KUROSAKI 中检查。它根据 scripts/fds_abi_fixture.py 创建并删除临时替身;这些检查无需外部 BIOS。该替身不是普通游戏用的 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 ABI 替身时,LOAD BANK 2 STATUS、REQUIRE SAME BANK 和 RESTORE BANK 1 均为 000。FIRST / LAST BYTE 显示所传送四字节数据的首尾值 011 和 044;恢复后 RESIDENT BANK 为 001。无效存储体 0 和 255 均返回 255。请求相同存储体不会额外加载:检查到的文件请求依次为 32 和 0。

__fds_load_bank: BANK_LOAD-DEFAULT — 使用独立编写的 LoadFiles ABI 替身时,LOAD BANK 2 STATUS、REQUIRE SAME BANK 和 RESTORE BANK 1 均为 000。FIRST / LAST BYTE 显示所传送四字节数据的首尾值 011 和 044;恢复后 RESIDENT BANK 为 001。无效存储体 0 和 255 均返回 255。请求相同存储体不会额外加载:检查到的文件请求依次为 32 和 0。
BANK_LOAD-DEFAULT — 使用独立编写的 LoadFiles ABI 替身时,LOAD BANK 2 STATUS、REQUIRE SAME BANK 和 RESTORE BANK 1 均为 000。FIRST / LAST BYTE 显示所传送四字节数据的首尾值 011 和 044;恢复后 RESIDENT BANK 为 001。无效存储体 0 和 255 均返回 255。请求相同存储体不会额外加载:检查到的文件请求依次为 32 和 0。

完整示例程序

构建命令

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 文件列表的结束标记。默认布局中,ID 32 包含存储体 2。

返回值

BIOS 错误字节,零表示成功。无论成功或失败,驻留记录均设为 255,表示未知;仅凭文件 ID 无法建立逻辑存储体映射。

使用条件与限制

配合 --mapper=fds --fds-layout=fds32 使用。存储体 0 的公共代码位于 $A000–$DFFF。启动时存储体 1 占据 $6000–$9FFF;存储体 2 及以上作为覆盖内容加载到同一个 16 KiB 窗口。可用 #pragma fixed_bank 2 等指令放置覆盖代码或数据。

构造包含所请求 ID 和结束标记 255 的列表。请检查清单中的目标地址,避免文件覆盖公共代码或正在执行的代码。逻辑覆盖加载优先使用 __fds_load_bank(),由它管理驻留状态。

从 $6000–$9FFF 窗口中的代码直接加载其他存储体,可能覆盖当前函数或其返回地址。示例将调用方及显示代码放在 #pragma fixed_bank 0。若要调用另一存储体中的函数并返回,请使用 __fds_overlay_farcall() 或普通的带类型函数调用。

BIOS 调用是同步的;本 API 不会在加载期间保持游戏主循环每帧更新。请从公共存储体中的普通控制代码调用,不要从中断处理程序调用。画面/音频连续性以及与真实 BIOS 的共存情况,需要另行验证。

这些截图采用独立编写的 LoadFiles 调用约定测试替身,检查编译后的 CPU 代码、内存传送及每个显示像素。不验证真实 BIOS、磁盘传送时序、CRC 或硬件启动。KUROSAKI 的 FDS 控制器模型尚不完整;不使用该替身的普通运行,不能证明相同的传送结果。

下方第一条编译命令构建可下载示例。Python 检查程序分别启用和关闭优化,构建全部五个教学示例,再在 KUROSAKI 中检查。它根据 scripts/fds_abi_fixture.py 创建并删除临时替身;这些检查无需外部 BIOS。该替身不是普通游戏用的 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 ABI 替身时,FILE 32 LOAD STATUS 为 000,传送数据的首尾字节为 011 和 044。原始加载后,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 ABI 替身时,FILE 32 LOAD STATUS 为 000,传送数据的首尾字节为 011 和 044。原始加载后,UNKNOWN BANK RECORD 为 255,IS BANK 1 RESIDENT 为 000。REQUIRE BANK 1 成功返回 000,随后 RESIDENT AFTER RELOAD 为 001。这区分了按文件 ID 加载与管理逻辑存储体。
RAW_OVERLAY-DEFAULT — 使用独立编写的 LoadFiles ABI 替身时,FILE 32 LOAD STATUS 为 000,传送数据的首尾字节为 011 和 044。原始加载后,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) 调用。

返回值

目标函数的八位结果。若加载失败但能恢复先前存储体,则不调用目标,而是返回加载错误字节。请设计返回值约定,使错误与正常结果可区分。

使用条件与限制

配合 --mapper=fds --fds-layout=fds32 使用。存储体 0 的公共代码位于 $A000–$DFFF。启动时存储体 1 占据 $6000–$9FFF;存储体 2 及以上作为覆盖内容加载到同一个 16 KiB 窗口。可用 #pragma fixed_bank 2 等指令放置覆盖代码或数据。

存储体 1 从位于 $6000 的独立 16,384 字节启动 PRG 文件恢复。不提供清单时,默认文件为:ID 0 对应存储体 1,ID 1 对应公共代码,ID 2 对应 CHR。自定义清单必须将启动 PRG 拆分为位于 $6000 和 $A000 的两个 16,384 字节文件。存储体 2 及以上的文件 ID 为 --fds-overlay-start-id(默认 32)+ 存储体编号 − 2。

跨存储体调用会保存先前存储体,并在返回前恢复。嵌套调用遵循相同规则。传送部分失败后也会尝试恢复。若恢复本身失败,执行会停止在公共代码中,而不是返回已被覆盖的代码。公共存储体目标和同存储体调用会避免不必要的传送。

BIOS 调用是同步的;本 API 不会在加载期间保持游戏主循环每帧更新。请从公共存储体中的普通控制代码调用,不要从中断处理程序调用。画面/音频连续性以及与真实 BIOS 的共存情况,需要另行验证。

这些截图采用独立编写的 LoadFiles 调用约定测试替身,检查编译后的 CPU 代码、内存传送及每个显示像素。不验证真实 BIOS、磁盘传送时序、CRC 或硬件启动。KUROSAKI 的 FDS 控制器模型尚不完整;不使用该替身的普通运行,不能证明相同的传送结果。

下方第一条编译命令构建可下载示例。Python 检查程序分别启用和关闭优化,构建全部五个教学示例,再在 KUROSAKI 中检查。它根据 scripts/fds_abi_fixture.py 创建并删除临时替身;这些检查无需外部 BIOS。该替身不是普通游戏用的 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 ABI 替身时,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 ABI 替身时,RETURN VALUE 为 042;CALLBACK COUNT 和 BANK EXPR COUNT 均为 001;BANK IN CALLBACK 为 002;BANK AFTER RETURN 为 001。函数和存储体表达式各执行一次,调用存储体 2 后恢复了存储体 1。这不是真实磁盘传送测试的截图。
OVERLAY_CALL-DEFAULT — 使用独立编写的 LoadFiles ABI 替身时,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 时为零。超过 255 项会以 KQFC2511 拒绝。

使用条件与限制

使用 --mapper=fds 构建。卡带映射器目标调用除 __fds_available 以外的 FDS 专用函数时,会以 KQFC2420 拒绝编译。

代码生成阶段创建零占位值;原生 FDS 链接在确定存储体和地址后,将其替换为实际表。示例在存储体 2 打包两个函数,但不调用它们。--fds-layout=legacy 的初始数据不代表覆盖函数总数。

截图使用 KUROSAKI 将磁盘启动文件直接载入 RAM 的方式。本示例不调用 BIOS 例程,不验证文件传送、换盘或硬件启动。空测试固件不是替代 BIOS。示例用 NMI 更新画面,并禁用可屏蔽 IRQ;这些查询不需要 IRQ。

构建示例生成原生布局的 .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,不会使该存储体在启动时驻留。使用 --fds-no-overlay-table 的验证构建中,只有最后一个值变为零。

__fds_overlay_function_count: RESIDENCY-FDS-DEFAULT — CURRENT BANK 为 1,BANK 1 RESIDENT 为 1,BANK 2 RESIDENT 为 0,OVERLAY FUNCTIONS 为 2。将两个函数打包在存储体 2,不会使该存储体在启动时驻留。使用 --fds-no-overlay-table 的验证构建中,只有最后一个值变为零。
RESIDENCY-FDS-DEFAULT — CURRENT BANK 为 1,BANK 1 RESIDENT 为 1,BANK 2 RESIDENT 为 0,OVERLAY FUNCTIONS 为 2。将两个函数打包在存储体 2,不会使该存储体在启动时驻留。使用 --fds-no-overlay-table 的验证构建中,只有最后一个值变为零。
__fds_overlay_function_count: RESIDENCY-FDS-TABLE-OFF-DEFAULT — CURRENT BANK 为 1,BANK 1 RESIDENT 为 1,BANK 2 RESIDENT 为 0,OVERLAY FUNCTIONS 为 2。将两个函数打包在存储体 2,不会使该存储体在启动时驻留。使用 --fds-no-overlay-table 的验证构建中,只有最后一个值变为零。
RESIDENCY-FDS-TABLE-OFF-DEFAULT — CURRENT BANK 为 1,BANK 1 RESIDENT 为 1,BANK 2 RESIDENT 为 0,OVERLAY FUNCTIONS 为 2。将两个函数打包在存储体 2,不会使该存储体在启动时驻留。使用 --fds-no-overlay-table 的验证构建中,只有最后一个值变为零。

完整示例程序

构建命令

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 或 255、文件 ID 溢出,或存储体 1 缺少恢复文件时返回 255。BIOS 传送失败时返回其错误字节。传送前的参数错误保留驻留记录;BIOS 传送失败则将其设为 255,表示未知。

使用条件与限制

配合 --mapper=fds --fds-layout=fds32 使用。存储体 0 的公共代码位于 $A000–$DFFF。启动时存储体 1 占据 $6000–$9FFF;存储体 2 及以上作为覆盖内容加载到同一个 16 KiB 窗口。可用 #pragma fixed_bank 2 等指令放置覆盖代码或数据。

有效存储体与记录一致时返回零;否则调用 __fds_load_bank()。即使用 --fds-no-overlay-guard,最初的比较仍会执行。存储体 255 永远不算驻留,即使记录本身为 255。

存储体 1 从位于 $6000 的独立 16,384 字节启动 PRG 文件恢复。不提供清单时,默认文件为:ID 0 对应存储体 1,ID 1 对应公共代码,ID 2 对应 CHR。自定义清单必须将启动 PRG 拆分为位于 $6000 和 $A000 的两个 16,384 字节文件。存储体 2 及以上的文件 ID 为 --fds-overlay-start-id(默认 32)+ 存储体编号 − 2。

从 $6000–$9FFF 窗口中的代码直接加载其他存储体,可能覆盖当前函数或其返回地址。示例将调用方及显示代码放在 #pragma fixed_bank 0。若要调用另一存储体中的函数并返回,请使用 __fds_overlay_farcall() 或普通的带类型函数调用。

BIOS 调用是同步的;本 API 不会在加载期间保持游戏主循环每帧更新。请从公共存储体中的普通控制代码调用,不要从中断处理程序调用。画面/音频连续性以及与真实 BIOS 的共存情况,需要另行验证。

这些截图采用独立编写的 LoadFiles 调用约定测试替身,检查编译后的 CPU 代码、内存传送及每个显示像素。不验证真实 BIOS、磁盘传送时序、CRC 或硬件启动。KUROSAKI 的 FDS 控制器模型尚不完整;不使用该替身的普通运行,不能证明相同的传送结果。

下方第一条编译命令构建可下载示例。Python 检查程序分别启用和关闭优化,构建全部五个教学示例,再在 KUROSAKI 中检查。它根据 scripts/fds_abi_fixture.py 创建并删除临时替身;这些检查无需外部 BIOS。该替身不是普通游戏用的 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 ABI 替身时,LOAD BANK 2 STATUS、REQUIRE SAME BANK 和 RESTORE BANK 1 均为 000。FIRST / LAST BYTE 显示所传送四字节数据的首尾值 011 和 044;恢复后 RESIDENT BANK 为 001。无效存储体 0 和 255 均返回 255。请求相同存储体不会额外加载:检查到的文件请求依次为 32 和 0。

__fds_require_bank: BANK_LOAD-DEFAULT — 使用独立编写的 LoadFiles ABI 替身时,LOAD BANK 2 STATUS、REQUIRE SAME BANK 和 RESTORE BANK 1 均为 000。FIRST / LAST BYTE 显示所传送四字节数据的首尾值 011 和 044;恢复后 RESIDENT BANK 为 001。无效存储体 0 和 255 均返回 255。请求相同存储体不会额外加载:检查到的文件请求依次为 32 和 0。
BANK_LOAD-DEFAULT — 使用独立编写的 LoadFiles ABI 替身时,LOAD BANK 2 STATUS、REQUIRE SAME BANK 和 RESTORE BANK 1 均为 000。FIRST / LAST BYTE 显示所传送四字节数据的首尾值 011 和 044;恢复后 RESIDENT BANK 为 001。无效存储体 0 和 255 均返回 255。请求相同存储体不会额外加载:检查到的文件请求依次为 32 和 0。

完整示例程序

构建命令

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 镜像。本例直接操作音源,不执行磁盘 I/O。录音使用 KUROSAKI 的 RAM 直接启动方式和音源模型。测试使用的空固件区域不提供 BIOS 功能,也不随示例分发。正常磁盘启动和实机运行需另行准备适当的启动环境。

__fds_env_set

将三个原始参数依次写入 $4080、$4084 和 $408A。

语法

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

参数

speed

尽管参数名为 speed,实际写入的是 $4080 原始值:第 7 位选择直接音量,第 6 位选择递增包络。

gain

写入 $4084 的调制包络原始值。

mode

写入 $408A 的包络总速度。

返回值

无返回值。修改音源寄存器或驱动状态。

使用条件与限制

使用 --mapper=fds 生成 .fds 镜像。本例直接操作音源,不执行磁盘 I/O。录音使用 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 点三角波先以约 440 Hz 播放,暂停后以约 660 Hz 播放,随后停止。第二个音使用 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

向 $4082/$4083 写入 12 位频率,并清除 $4083 的高位控制标志。

语法

c
void __fds_freq_set(u16 freq);

参数

freq

FDS 频率寄存器值 0~4095,高位控制标志会被清零。

返回值

无返回值。修改音源寄存器或驱动状态。

使用条件与限制

使用 --mapper=fds 生成 .fds 镜像。本例直接操作音源,不执行磁盘 I/O。录音使用 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 点三角波先以约 440 Hz 播放,暂停后以约 660 Hz 播放,随后停止。第二个音使用 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

依次向调制表写入端口 $4088 发送 32 个字节。

语法

c
void __fds_mod_load(const u8* mod32);

参数

mod32

写入 FDS 调制表的 32 字节,每项为 0~7 的控制码。载入前应停止调制。

返回值

无返回值。修改音源寄存器或驱动状态。

使用条件与限制

使用 --mapper=fds 生成 .fds 镜像。本例直接操作音源,不执行磁盘 I/O。录音使用 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 点三角波先以约 440 Hz 播放,暂停后以约 660 Hz 播放,随后停止。第二个音使用 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 清零。

语法

c
void __fds_sound_enable(void);

返回值

无返回值。修改音源寄存器或驱动状态。

使用条件与限制

使用 --mapper=fds 生成 .fds 镜像。本例直接操作音源,不执行磁盘 I/O。录音使用 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 点三角波先以约 440 Hz 播放,暂停后以约 660 Hz 播放,随后停止。第二个音使用 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

将音量低六位与 0x80 组合后写入 $4080,设置直接音量。

语法

c
void __fds_volume_set(u8 vol);

参数

vol

FDS 直接音量 0~63。仅保留低六位,因此 64 会回绕为零。

返回值

无返回值。修改音源寄存器或驱动状态。

使用条件与限制

使用 --mapper=fds 生成 .fds 镜像。本例直接操作音源,不执行磁盘 I/O。录音使用 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 点三角波先以约 440 Hz 播放,暂停后以约 660 Hz 播放,随后停止。第二个音使用 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 设回零。

语法

c
void __fds_wave_load(const u8* wave64);

参数

wave64

FDS 波形的 64 字节数组,每项为 0~63 的六位幅度值。不检查空指针。

返回值

无返回值。修改音源寄存器或驱动状态。

使用条件与限制

使用 --mapper=fds 生成 .fds 镜像。本例直接操作音源,不执行磁盘 I/O。录音使用 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 点三角波先以约 440 Hz 播放,暂停后以约 660 Hz 播放,随后停止。第二个音使用 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 与音效的等待时间,并根据当前声道归属更新 APU。

语法

c
void __nes_audio_vblank_tick(void);

返回值

无返回值。

使用条件与限制

包含 audio_vblank.h,并将 audio_vblank.c 加入构建一次。编译器会从默认或自定义 NMI 处理程序中调用一次本库钩子。必须启用 PPU NMI;主循环无需调用音乐更新函数。

这是面向 NTSC 的四声道驱动:CH1 为脉冲 1,CH2 为脉冲 2,CH3 为三角波,CH4 为噪声,对应索引 0~3。不支持 DMC 或扩展音源。不要同时运行 audio.c 音效函数等直接改写同一 APU 的代码。本库没有 PAL 音高表或速度补偿。

应用程序不要直接调用。链接音乐库后,编译器会将本钩子接入 NMI。钩子保存 A/X/Y,不使用共享表达式计算临时区。钩子与音高表放在固定 ROM 中,NMI 内不会切换 ROM 存储体。这些保护只覆盖音乐钩子自身;自定义 NMI 处理程序仍须保存其余处理可能改动的状态。

每条记录包含五字节,顺序为 等待时间, CH1, CH2, CH3, CH4。等待时间 1~255 表示本次声道状态持续的 NMI 次数;零表示结束播放,其余四字节被忽略。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();

这是一段调用示例。放入完整程序前,请满足所述初始化和缓冲区要求。

预期结果

示例播放一次原创的四声道短曲。主循环连续 90 帧进行整数计算而不补充队列时,音乐仍会继续。画面上的 PHRASE COMPLETE / SILENT 和 UNDERRUNS 0 / MATH OK 表示播放结束、没有欠载且计算结果一致。请通过附带录音辨认脉冲旋律与伴奏、三角波低音和噪声;结尾应为静音。截图仅表示控制结果,声音效果需通过录音确认。

__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) 写入整个打包属性字节。X/Y 是图块坐标,用于确定一个 4×4 图块属性区域。四个两位调色板字段都会被替换,不是仅合并选定象限。

语法

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

参数

x / y

图块坐标,不是像素坐标。可见的 32×30 图块地图应使用 x=0..31、y=0..29;每个图块为 8×8 像素。字节参数不会被限制在范围内,无效坐标可能写到预期图块区域之外。

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 还是 32。镜像和地址回绕由 PPU 处理;CHR-ROM 不可写。访问结束后、开始渲染前,请设置所需的滚动状态。

NESdev:PPU 寄存器

NESdev:PPU 滚动

使用示例

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 竖条和八行三角形,第二行只有左半竖条。(16,48) 处是绿色实心 64×32 矩形,(144,48) 处的红色 64×32 区域由横八、纵四个三角形图块组成。

y=96 处,x=16、24、32 的蓝色标记分别为正方形、左半竖条和三角形;x=80 为红色左半竖条,x=144 为绿色三角形。这些是背景图块。使用独立调色板的精灵位于 y=144:x=16 是红色 8×8 正方形,x=80 是蓝色左半 4×8 竖条,x=144 是绿色八行三角形。最终滚动为 (0,0)。

图像检查 16,384 个像素的预期形状和颜色,包括精灵颜色。另有 31 个状态测试,覆盖寄存器替换、状态与锁存器副作用、滚动保存值、0/255 字节传输、增量 1/32、地图边缘、矩形零尺寸、表选择、完整属性字节及两种调色板目标。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) 写入一个完整属性字节。该字节中的所有调色板象限都会被覆盖。表选择遵循卡带当前镜像。

语法

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

参数

nt

逻辑名称表选择值,只使用最低两位(nt & 3)。例如 4 选择表 0,7 选择表 3。不会分配独立存储空间,也不改变映射器镜像。

x / y

图块坐标,不是像素坐标。可见的 32×30 图块地图应使用 x=0..31、y=0..29;每个图块为 8×8 像素。字节参数不会被限制在范围内,无效坐标可能写到预期图块区域之外。

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 还是 32。镜像和地址回绕由 PPU 处理;CHR-ROM 不可写。访问结束后、开始渲染前,请设置所需的滚动状态。

NESdev:PPU 寄存器

NESdev:PPU 滚动

使用示例

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 竖条和八行三角形,第二行只有左半竖条。(16,48) 处是绿色实心 64×32 矩形,(144,48) 处的红色 64×32 区域由横八、纵四个三角形图块组成。

y=96 处,x=16、24、32 的蓝色标记分别为正方形、左半竖条和三角形;x=80 为红色左半竖条,x=144 为绿色三角形。这些是背景图块。使用独立调色板的精灵位于 y=144:x=16 是红色 8×8 正方形,x=80 是蓝色左半 4×8 竖条,x=144 是绿色八行三角形。最终滚动为 (0,0)。

图像检查 16,384 个像素的预期形状和颜色,包括精灵颜色。另有 31 个状态测试,覆盖寄存器替换、状态与锁存器副作用、滚动保存值、0/255 字节传输、增量 1/32、地图边缘、矩形零尺寸、表选择、完整属性字节及两种调色板目标。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。这验证布局元数据;运行时存储体行为由独立的读取和回调检查验证。

完整程序演示如何用存储体编号和近地址确定数据、如何跨存储体调用回调,以及如何区分库记录与实际映射。所有数值均为十六进制。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 各执行一次回调。

__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

通过编译器针对 mapper 的处理选择逻辑 PRG 存储体,并更新其内部状态。调用后保持新的映射,但不更新 bank 库单独保存的记录。

语法

c
void __bankswitch(u8 bank);

参数

bank

无符号 8 位 ROM 存储体编号。调用时应与函数布局一致,读取时用于选择源映射。请使用目标卡带支持的范围。

返回值

无(void)。

使用条件与限制

持续生效的存储体切换不能使当前代码或返回地址失去映射。请像示例一样,将调用点、所用的 bank_switch 本体及后续代码放在公共或固定存储体 0。

库记录初值为 0,不会查询卡带启动时的映射。请用 bank_switch 初始化。直接调用切换内置函数或写 mapper 不会更新这份独立记录,因此 bank_get_current 无法验证实际硬件映射。

FC 切换编号是编译器的逻辑编号。0 表示公共 ROM,不是切换目标;辅助代码将 0 归一为存储体 1。SUROM512 仅允许 1–30:直接传给切换内置函数的越界常量会导致构建错误;无效运行时值会回退到 1 并设置 mapper 故障状态。公共 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。

完整程序演示如何用存储体编号和近地址确定数据、如何跨存储体调用回调,以及如何区分库记录与实际映射。所有数值均为十六进制。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 各执行一次回调。

__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,不清除所在字节的其他位。适用于移除某个已记录条件,例如临时状态。重复清除同一位,结果仍为 0。

语法

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

参数

base

指向 CPU 可访问内存中标志数组首字节的指针。本调用不分配或初始化数组。设置、清除和翻转操作要求可写 RAM;测试操作只读取数组。

bit

从零开始的 u16 位索引,范围为 0–65535;不是字节偏移,也不是位掩码。索引 0 选择字节 0 的掩码 0x01;索引 7 选择字节 0 的 0x80;索引 8 选择字节 1 的 0x01。一般来说,字节偏移为 index / 8,掩码为 1 << (index % 8)。对于 N 字节数组,要求 index < 8*N,且 CPU 地址范围有效。

返回值

无(void)。

使用条件与限制

调用方必须提供足够的存储空间。不提供数组长度参数、边界裁剪、存储体切换或自动图形上传。较大的位编号可能访问分配范围以外的字节。应保持内存映射稳定;不要用这些 RAM 标志辅助函数操作读写有副作用的硬件寄存器。

设置、清除和翻转操作都会先读取字节、修改所选位,再写回字节。这个过程不是原子更新。请协调主循环与中断处理程序中的写入,避免在读写之间对同一字节其他位的修改被覆盖。

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,即二进制 10100010、十进制 162;CLEAR BIT2 行应显示 162。再次清除第 2 位不会改变结果。两次调用都保留其他已存储的标志。

FC 画面各行的预期十进制值为: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,将 0x80 改为 0x82,而不修改字节 0。截取的 FC 画面使用黑底蓝字。

程序检查每个显示的状态变化,并显示失败数。设置、清除和翻转操作本身不返回值;对应行显示操作后的 RAM 字节。另外的模拟器检查在每个平台上执行 136 个案例,比较带保护区域的缓冲区全部 520 字节,覆盖跨字节及跨 256 字节边界的常量和运行时索引,以及全部八种非零位测试掩码。这些是 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,同时保留同一字节中的其他七位。例如,可用于标记任务已完成或物品已收集。对已经为 1 的位再次设置,不会改变它;这不是翻转操作。

语法

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

参数

base

指向 CPU 可访问内存中标志数组首字节的指针。本调用不分配或初始化数组。设置、清除和翻转操作要求可写 RAM;测试操作只读取数组。

bit

从零开始的 u16 位索引,范围为 0–65535;不是字节偏移,也不是位掩码。索引 0 选择字节 0 的掩码 0x01;索引 7 选择字节 0 的 0x80;索引 8 选择字节 1 的 0x01。一般来说,字节偏移为 index / 8,掩码为 1 << (index % 8)。对于 N 字节数组,要求 index < 8*N,且 CPU 地址范围有效。

返回值

无(void)。

使用条件与限制

调用方必须提供足够的存储空间。不提供数组长度参数、边界裁剪、存储体切换或自动图形上传。较大的位编号可能访问分配范围以外的字节。应保持内存映射稳定;不要用这些 RAM 标志辅助函数操作读写有副作用的硬件寄存器。

设置、清除和翻转操作都会先读取字节、修改所选位,再写回字节。这个过程不是原子更新。请协调主循环与中断处理程序中的写入,避免在读写之间对同一字节其他位的修改被覆盖。

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,十进制为 166。SET BIT1 行应显示 166。示例再次设置该位,并验证字节仍为 0xA6,说明重复设置不会翻转标志。

FC 画面各行的预期十进制值为: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,将 0x80 改为 0x82,而不修改字节 0。截取的 FC 画面使用黑底蓝字。

程序检查每个显示的状态变化,并显示失败数。设置、清除和翻转操作本身不返回值;对应行显示操作后的 RAM 字节。另外的模拟器检查在每个平台上执行 136 个案例,比较带保护区域的缓冲区全部 520 字节,覆盖跨字节及跨 256 字节边界的常量和运行时索引,以及全部八种非零位测试掩码。这些是 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

从紧凑排列的位数组中读取一个标志,不修改数组。这样,每个 RAM 字节可以容纳八个独立的开关标志。返回结果表示所选位是否已设置;具体数值表示方式取决于目标平台。

语法

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

参数

base

指向 CPU 可访问内存中标志数组首字节的指针。本调用不分配或初始化数组。设置、清除和翻转操作要求可写 RAM;测试操作只读取数组。

bit

从零开始的 u16 位索引,范围为 0–65535;不是字节偏移,也不是位掩码。索引 0 选择字节 0 的掩码 0x01;索引 7 选择字节 0 的 0x80;索引 8 选择字节 1 的 0x01。一般来说,字节偏移为 index / 8,掩码为 1 << (index % 8)。对于 N 字节数组,要求 index < 8*N,且 CPU 地址范围有效。

返回值

u8:所选位为零时返回 0,否则返回对应掩码,即 1、2、4、8、16、32、64 或 128。结果为 base[bit >> 3] & (1 << (bit & 7))。例如,测试已置位的第 7 位会返回 128。请用 result != 0 判断是否置位;只与 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,即二进制 10100100。测试第 1 位返回 0。测试第 7 位时,GB 返回 1,FC 返回 128;示例还将该非零结果转换为两平台共用的开关标志,并检查原字节未被修改。请查看下图的 TEST OFF 和 TEST ON。

FC 画面各行的预期十进制值为: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,将 0x80 改为 0x82,而不修改字节 0。截取的 FC 画面使用黑底蓝字。

程序检查每个显示的状态变化,并显示失败数。设置、清除和翻转操作本身不返回值;对应行显示操作后的 RAM 字节。另外的模拟器检查在每个平台上执行 136 个案例,比较带保护区域的缓冲区全部 520 字节,覆盖跨字节及跨 256 字节边界的常量和运行时索引,以及全部八种非零位测试掩码。这些是 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。其余位保持不变。若两次调用之间没有其他代码修改该位,对同一位调用两次会恢复原始状态。

语法

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

参数

base

指向 CPU 可访问内存中标志数组首字节的指针。本调用不分配或初始化数组。设置、清除和翻转操作要求可写 RAM;测试操作只读取数组。

bit

从零开始的 u16 位索引,范围为 0–65535;不是字节偏移,也不是位掩码。索引 0 选择字节 0 的掩码 0x01;索引 7 选择字节 0 的 0x80;索引 8 选择字节 1 的 0x01。一般来说,字节偏移为 index / 8,掩码为 1 << (index % 8)。对于 N 字节数组,要求 index < 8*N,且 CPU 地址范围有效。

返回值

无(void)。

使用条件与限制

调用方必须提供足够的存储空间。不提供数组长度参数、边界裁剪、存储体切换或自动图形上传。较大的位编号可能访问分配范围以外的字节。应保持内存映射稳定;不要用这些 RAM 标志辅助函数操作读写有副作用的硬件寄存器。

设置、清除和翻转操作都会先读取字节、修改所选位,再写回字节。这个过程不是原子更新。请协调主循环与中断处理程序中的写入,避免在读写之间对同一字节其他位的修改被覆盖。

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,即十进制 34,因此 TOGGLE BIT7 显示 034。再次翻转会恢复为 0xA2,RESTORE 显示 162。这两个结果展示了两个方向的翻转,同时保留其他位。

FC 画面各行的预期十进制值为: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,将 0x80 改为 0x82,而不修改字节 0。截取的 FC 画面使用黑底蓝字。

程序检查每个显示的状态变化,并显示失败数。设置、清除和翻转操作本身不返回值;对应行显示操作后的 RAM 字节。另外的模拟器检查在每个平台上执行 136 个案例,比较带保护区域的缓冲区全部 520 字节,覆盖跨字节及跨 256 字节边界的常量和运行时索引,以及全部八种非零位测试掩码。这些是 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 中是用于选择 2 KiB 窗口的 1 KiB 存储体编号:最低位被忽略,因此 2 和 3 都选择编号 2、3 这一对 1 KiB 存储体。可用编号取决于 CHR ROM 容量;API 不检查范围。

返回值

无返回值。

使用条件与限制

内建函数声明位于 intrinsics.h。使用 nes_mapper_irq_* 宏时包含 nes_game.h。构建时通过 --mapper 选择板卡。

向 MMC3 存储体选择寄存器写入 0 或 1,同时会清除 PRG 模式位 6 和 CHR 反转位 7。不要与要求这些位保持置位的 PRG/CHR 布局混用。渲染期间切换会改变后续图案读取。示例在关闭渲染时检查,并在显示结果前恢复字体存储体。

这三个内建函数支持 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:前三次显式启用各允许一次中断;禁用后计数不变。每个阶段等待六帧。

__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。它通过 PPU 读取独立编写的标记字节,证明三个名称都切换整个 8 KiB 图案区域。

__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 中是用于选择 2 KiB 窗口的 1 KiB 存储体编号:最低位被忽略,因此 2 和 3 都选择编号 2、3 这一对 1 KiB 存储体。可用编号取决于 CHR ROM 容量;API 不检查范围。

返回值

无返回值。

使用条件与限制

内建函数声明位于 intrinsics.h。使用 nes_mapper_irq_* 宏时包含 nes_game.h。构建时通过 --mapper 选择板卡。

向 MMC3 存储体选择寄存器写入 0 或 1,同时会清除 PRG 模式位 6 和 CHR 反转位 7。不要与要求这些位保持置位的 PRG/CHR 布局混用。渲染期间切换会改变后续图案读取。示例在关闭渲染时检查,并在显示结果前恢复字体存储体。

这三个内建函数支持 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:前三次显式启用各允许一次中断;禁用后计数不变。每个阶段等待六帧。

__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。它通过 PPU 读取独立编写的标记字节,证明三个名称都切换整个 8 KiB 图案区域。

__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 没有独立的第二窗口,因此改变整个 8 KiB 区域。

语法

c
void __chr_bank_set1(u8 bank);

参数

bank

CNROM 中这是 8 KiB CHR 存储体编号。MMC3 中是用于选择 2 KiB 窗口的 1 KiB 存储体编号:最低位被忽略,因此 2 和 3 都选择编号 2、3 这一对 1 KiB 存储体。可用编号取决于 CHR ROM 容量;API 不检查范围。

返回值

无返回值。

使用条件与限制

内建函数声明位于 intrinsics.h。使用 nes_mapper_irq_* 宏时包含 nes_game.h。构建时通过 --mapper 选择板卡。

向 MMC3 存储体选择寄存器写入 0 或 1,同时会清除 PRG 模式位 6 和 CHR 反转位 7。不要与要求这些位保持置位的 PRG/CHR 布局混用。渲染期间切换会改变后续图案读取。示例在关闭渲染时检查,并在显示结果前恢复字体存储体。

这三个内建函数支持 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:前三次显式启用各允许一次中断;禁用后计数不变。每个阶段等待六帧。

__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。它通过 PPU 读取独立编写的标记字节,证明三个名称都切换整个 8 KiB 图案区域。

__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/NES 图块。16 指字节数,不是位数;此调用不会把图像上传到显示硬件。

语法

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

参数

dst

指向目标区域第一个可写字节的指针。必须预先准备足够的连续空间;函数不会自动分配缓冲区。

src

指向源区域第一个可读字节的指针。复制期间,整个源区域必须保持可访问且内容不变。

返回值

无(void)。

使用条件与限制

源区域和目标区域应互不重叠。这些函数按地址递增方向复制,不提供 memmove 的重叠安全保证;写入尚未读取的源区域会破坏后续数据。

操作完成后才返回调用方。函数不会切换 ROM/RAM 存储体,也不会把范围自动裁剪到缓冲区边界。请保持映射稳定、指定有效范围,并协调与使用同一缓冲区的中断处理程序之间的访问。

在 FC 上,dst 是 CPU 地址,不是 PPU 地址。复制到 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 从偏移量 1 开始复制 16 字节的规律数据。memory_check 会比较整个源数组和目标数组;COPY16 行应显示 016。缓冲区首字节留作保护区,并检查复制范围之后的字节也未改变。

完整程序在每次操作前初始化两个 264 字节缓冲区,随后检查源和目标的每一个字节,以及前后不应改变的保护区域。正确显示为 COPY WORD 257、FILL WORD 257、COPY BYTE 255、FILL BYTE 255、COPY16 016、COPY32 032、ZERO CALLS 002、FAILED 000。WORD/BYTE 表示长度的类型,不是复制元素的大小。这些数字来自示例的检查过程,并非这些无返回值函数的返回值。

截图中,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 字节图块图案。它按地址递增方向逐字节复制,不是一次 32 位读取,也不会切换存储体。

语法

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

参数

dst

指向目标区域第一个可写字节的指针。必须预先准备足够的连续空间;函数不会自动分配缓冲区。

src

指向源区域第一个可读字节的指针。复制期间,整个源区域必须保持可访问且内容不变。

返回值

无(void)。

使用条件与限制

源区域和目标区域应互不重叠。这些函数按地址递增方向复制,不提供 memmove 的重叠安全保证;写入尚未读取的源区域会破坏后续数据。

操作完成后才返回调用方。函数不会切换 ROM/RAM 存储体,也不会把范围自动裁剪到缓冲区边界。请保持映射稳定、指定有效范围,并协调与使用同一缓冲区的中断处理程序之间的访问。

在 FC 上,dst 是 CPU 地址,不是 PPU 地址。复制到 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 从偏移量 1 开始复制 32 字节的规律数据。memory_check 会比较整个源数组和目标数组;COPY32 行应显示 032。缓冲区首字节留作保护区,并检查复制范围之后的字节也未改变。

完整程序在每次操作前初始化两个 264 字节缓冲区,随后检查源和目标的每一个字节,以及前后不应改变的保护区域。正确显示为 COPY WORD 257、FILL WORD 257、COPY BYTE 255、FILL BYTE 255、COPY16 016、COPY32 032、ZERO CALLS 002、FAILED 000。WORD/BYTE 表示长度的类型,不是复制元素的大小。这些数字来自示例的检查过程,并非这些无返回值函数的返回值。

截图中,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

计算无符号两项点积。

语法

c
u16 __dot2_q8_8(u16 a0, u16 a1, u8 b0, u8 b1);

参数

a0

无符号字,范围为 0..65535。

a1

无符号字,范围为 0..65535。

b0

无符号系数,范围为 0..255。

b1

无符号系数,范围为 0..255。

返回值

返回 a0*b0+a1*b1 的低 16 位。

使用条件与限制

在 FC 上,名称含 q8_8/q1_7 的函数仍使用原始整数乘积,不执行定点缩放的右移。Q1.7 函数是对应有符号运算的别名。单位由调用方处理;有符号结果以 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 位值显示;负数采用二进制补码位模式。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

计算无符号三项点积。

语法

c
u16 __dot3_q8_8(u16 a0, u16 a1, u16 a2, u8 b0, u8 b1, u8 b2);

参数

a0

无符号字,范围为 0..65535。

a1

无符号字,范围为 0..65535。

a2

无符号字,范围为 0..65535。

b0

无符号系数,范围为 0..255。

b1

无符号系数,范围为 0..255。

b2

无符号系数,范围为 0..255。

返回值

返回 a0*b0+a1*b1+a2*b2 的低 16 位。

使用条件与限制

在 FC 上,名称含 q8_8/q1_7 的函数仍使用原始整数乘积,不执行定点缩放的右移。Q1.7 函数是对应有符号运算的别名。单位由调用方处理;有符号结果以 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 位值显示;负数采用二进制补码位模式。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信号,返回相同的八位结果,副作用也相同。此名称用于表明扩展输入用途,不会启用其他设备模式,也不会合并D0与D1。

语法

c
u8 __exp_pad_read1(void);

返回值

NES 原始位序的 8 位按住掩码。置位的每一位代表一个按下按钮,0 表示没有按钮。不是归一化的布尔值。

使用条件与限制

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线上提供兼容八位串行手柄信号的外设。函数不会识别已连接的设备,也没有实现所有扩展设备协议。每次调用都会重新锁存两个控制器的数据流,因此应完成一次完整读取后再开始下一次。结果为新采集的状态,不会更新input_*或nes_pad1_*缓存。

公开版KUROSAKI的控制器模型仅返回D0和固定的位6。因此即使按住普通控制器按钮,该模拟器中的D1与麦克风位也始终为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,因为相应信号线未被模拟。采集后显示值保持不变。在具备适合的外设及其信号线实现的环境中,扩展和麦克风数值取决于实际输入;应按上述位布局解读,而不是期待这些零值。

公开版KUROSAKI的控制器模型仅返回D0和固定的位6。因此即使按住普通控制器按钮,该模拟器中的D1与麦克风位也始终为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信号,返回相同的八位结果,副作用也相同。此名称用于表明扩展输入用途,不会启用其他设备模式,也不会合并D0与D1。

语法

c
u8 __exp_pad_read2(void);

返回值

NES 原始位序的 8 位按住掩码。置位的每一位代表一个按下按钮,0 表示没有按钮。不是归一化的布尔值。

使用条件与限制

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线上提供兼容八位串行手柄信号的外设。函数不会识别已连接的设备,也没有实现所有扩展设备协议。每次调用都会重新锁存两个控制器的数据流,因此应完成一次完整读取后再开始下一次。结果为新采集的状态,不会更新input_*或nes_pad1_*缓存。

公开版KUROSAKI的控制器模型仅返回D0和固定的位6。因此即使按住普通控制器按钮,该模拟器中的D1与麦克风位也始终为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,因为相应信号线未被模拟。采集后显示值保持不变。在具备适合的外设及其信号线实现的环境中,扩展和麦克风数值取决于实际输入;应按上述位布局解读,而不是期待这些零值。

公开版KUROSAKI的控制器模型仅返回D0和固定的位6。因此即使按住普通控制器按钮,该模拟器中的D1与麦克风位也始终为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

目标可写内存,容量必须能容纳实际复制的全部字节。除零字节传送外,不会检查null或无效目标地址。

bank

选择源ROM映射的8位存储体编号,须符合卡带与编译器的存储体布局;此参数与源地址分别指定。

src

所选ROM映射中源字节的16位CPU地址。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复制两个图案,共32字节。程序逐字节比较,检查存储体1已恢复,并确认零长度复制不会改变0x55哨兵值。

预期图像中,红色一行从像素坐标(16,32)开始,占72×8像素。依次重复8×8三角形、边框厚1像素的8×8空心正方形、填满左侧4列的图块,共三组。三角形从左上角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 并设置 mapper 故障状态。公共 ROM 应直接读取,不要请求切换到存储体 0。

使用示例

c
__farcall(3,callback);
bank_expect(callback_count==2 && callback_byte==0xD3,16);

这是一段调用示例。放入完整程序前,请满足所述初始化和缓冲区要求。

预期结果

在完整程序的此处,__farcall 使 callback_count 达到 2。回调从存储体 3 读取 D3。随后通过近指针读取 home2 并确认 B2,验证返回时已恢复存储体 2。

完整程序演示如何用存储体编号和近地址确定数据、如何跨存储体调用回调,以及如何区分库记录与实际映射。所有数值均为十六进制。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 各执行一次回调。

__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

目标可写内存,容量必须能容纳实际复制的全部字节。除零字节传送外,不会检查null或无效目标地址。

bank

选择源ROM映射的8位存储体编号,须符合卡带与编译器的存储体布局;此参数与源地址分别指定。

src

所选ROM映射中源字节的16位CPU地址。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 为零时,不改变任何目标字节,但仍会选择和恢复存储体。函数无返回值。

__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复制两个图案,共32字节。程序逐字节比较,检查存储体1已恢复,并确认零长度复制不会改变0x55哨兵值。

预期图像中,红色一行从像素坐标(16,32)开始,占72×8像素。依次重复8×8三角形、边框厚1像素的8×8空心正方形、填满左侧4列的图块,共三组。三角形从左上角1像素逐渐扩展到底行8像素。

像素坐标 (16,64) 处的蓝色三角形和方框轮廓展示 __farmemcpy 复制的字节。下方 (16,96) 的绿色图形来自 asset_load_raw。每组在黑底上占据 16×8 像素;复制的字节上传到 CHR RAM 后显示。

预期状态为FAILED CHECKS 000和FIRST FAILURE 00。验证运行300帧,比较文字、图形掩码和颜色是否符合预期。这是模拟器证据,不是实体硬件测试。

__farmemcpy: SUROM512 — 像素坐标 (16,64) 处的蓝色三角形和方框轮廓展示 __farmemcpy 复制的字节。下方 (16,96) 的绿色图形来自 asset_load_raw。每组在黑底上占据 16×8 像素;复制的字节上传到 CHR RAM 后显示。
SUROM512 — 像素坐标 (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映射中源字节的16位CPU地址。FC版声明使用u16,因此素材指针应通过(u16)pointer转换。

返回值

以 u16(0–65535)返回读到的小端序字。

使用条件与限制

目标字节或字的全部内容必须在选定 CPU 映射中可读。这些读取不会检查指针、裁剪地址,也不会在窗口边界自动切换到下一个 ROM 存储体。读取一个字需要连续两个有效字节。

远程读取和复制是同步操作,会使用编译器共享工作区。操作期间不要让中断执行另一笔存储体传输并覆盖同一工作区。GB 复制辅助代码还使用一个共享 HRAM 位置保存返回存储体。示例从固定代码执行,不设置竞争的存储体传输中断。

FC 切换编号是编译器的逻辑编号。0 表示公共 ROM,不是切换目标;辅助代码将 0 归一为存储体 1。SUROM512 仅允许 1–30:直接传给切换内置函数的越界常量会导致构建错误;无效运行时值会回退到 1 并设置 mapper 故障状态。公共 ROM 应直接读取,不要请求切换到存储体 0。

使用示例

c
bank_expect(__farpeek16(3,(u16)(payload+1))==0x5C7A,14);

这是一段调用示例。放入完整程序前,请满足所述初始化和缓冲区要求。

预期结果

此代码片段使用 __farpeek16 读取存储体 3 中的 payload+1 并检查 5C7A。完整程序还通过近指针读取 home2,确认已恢复存储体 2 的映射。

完整程序演示如何用存储体编号和近地址确定数据、如何跨存储体调用回调,以及如何区分库记录与实际映射。所有数值均为十六进制。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 各执行一次回调。

__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 地址读取一个字节后恢复原存储体。这是 far_data_read8 使用的内置函数。

语法

c
u8 __farpeek8(u8 bank, u16 addr);

参数

bank

无符号 8 位 ROM 存储体编号。调用时应与函数布局一致,读取时用于选择源映射。请使用目标卡带支持的范围。

addr

所选ROM映射中源字节的16位CPU地址。FC版声明使用u16,因此素材指针应通过(u16)pointer转换。

返回值

以 u8(0–255)返回读到的字节。

使用条件与限制

目标字节或字的全部内容必须在选定 CPU 映射中可读。这些读取不会检查指针、裁剪地址,也不会在窗口边界自动切换到下一个 ROM 存储体。读取一个字需要连续两个有效字节。

远程读取和复制是同步操作,会使用编译器共享工作区。操作期间不要让中断执行另一笔存储体传输并覆盖同一工作区。GB 复制辅助代码还使用一个共享 HRAM 位置保存返回存储体。示例从固定代码执行,不设置竞争的存储体传输中断。

FC 切换编号是编译器的逻辑编号。0 表示公共 ROM,不是切换目标;辅助代码将 0 归一为存储体 1。SUROM512 仅允许 1–30:直接传给切换内置函数的越界常量会导致构建错误;无效运行时值会回退到 1 并设置 mapper 故障状态。公共 ROM 应直接读取,不要请求切换到存储体 0。

使用示例

c
bank_expect(__farpeek8(3,(u16)payload)==0xD3,13);

这是一段调用示例。放入完整程序前,请满足所述初始化和缓冲区要求。

预期结果

此代码片段使用 __farpeek8 读取存储体 3 中的 payload 并检查 D3。完整程序还通过近指针读取 home2,确认已恢复存储体 2 的映射。

完整程序演示如何用存储体编号和近地址确定数据、如何跨存储体调用回调,以及如何区分库记录与实际映射。所有数值均为十六进制。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 各执行一次回调。

__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,且关闭矩阵后读为零时返回 1,否则返回 0。这是基于信号模式的启发式判断,不是设备识别码。

使用条件与限制

这些 API 直接访问 $4016/$4017。不要在同一端口交错执行中断驱动的手柄、键盘或串行操作;控制器移位位置也可能受影响。示例在采集输入时关闭 NMI,再在初始化显示时启用。

向 $4016 写入 5 以复位到第 0 行,再通过列选择位从高到低的跳变推进各行。除周围指令外,生成代码在复位后加入 12 个 CPU 周期的 NOP,在选择列后加入 50 个周期。退出时写入零以关闭矩阵。

独立回归检查根据实际执行的 CPU 总线轨迹,核对行 0..9、两列、九行扫描、稳定延迟及最终端口状态。不会注入实体按键信号。

第 9 行没有按键。任一条件失败就立即返回零。KUROSAKI 第一次读取就返回零,因此检测失败。

这些图像来自在 KUROSAKI 中原样运行可下载 ROM。当前输入总线模型支持手柄,但不模拟键盘、光枪或串行适配器信号。外部输入位读为零,因此截图不能验证这些设备。实体按键、光学感应和通信仍未经测试。

NESdev:Family BASIC 键盘

使用示例

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;九组 COL0/COL1 均为 00。A 位于第 6 行、第 0 列的第 4 位,但检测失败会阻止将原始零报告为按下。表格是启动时的一次采集结果。

__fkb_detect: KEYBOARD-DEFAULT — 本次 KUROSAKI 运行中,DEVICE DETECT、ROW 6 COL 0、A KEY - GUARDED 均为 000;九组 COL0/COL1 均为 00。A 位于第 6 行、第 0 列的第 4 位,但检测失败会阻止将原始零报告为按下。表格是启动时的一次采集结果。
KEYBOARD-DEFAULT — 本次 KUROSAKI 运行中,DEVICE DETECT、ROW 6 COL 0、A KEY - GUARDED 均为 000;九组 COL0/COL1 均为 00。A 位于第 6 行、第 0 列的第 4 位,但检测失败会阻止将原始零报告为按下。表格是启动时的一次采集结果。

完整示例程序

构建命令

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

只需要某一组时,读取指定行及列组,无需 18 字节缓冲区。

语法

c
u8 __fkb_read_row_col(u8 row, u8 col);

参数

row

按键行使用 0..8,无按键的检测行使用 9。不生成运行时范围检查。

col

0 或 1。只使用最低位:偶数选择组 0,奇数选择组 1。

返回值

返回 $4017 & 0x1E,不将位向低位压缩。

使用条件与限制

这些 API 直接访问 $4016/$4017。不要在同一端口交错执行中断驱动的手柄、键盘或串行操作;控制器移位位置也可能受影响。示例在采集输入时关闭 NMI,再在初始化显示时启用。

按键组使用原始位 1..4:按下会清除对应位,空闲组为 0x1E。它们既不是 ASCII 文本,也不是一表示按下的位图。未连接设备也可能读为零;没有检查设备存在状态时,不要将其解释为全部按键都被按下。

向 $4016 写入 5 以复位到第 0 行,再通过列选择位从高到低的跳变推进各行。除周围指令外,生成代码在复位后加入 12 个 CPU 周期的 NOP,在选择列后加入 50 个周期。退出时写入零以关闭矩阵。

独立回归检查根据实际执行的 CPU 总线轨迹,核对行 0..9、两列、九行扫描、稳定延迟及最终端口状态。不会注入实体按键信号。

每次调用都复位到第 0 行并重新推进,不复用上次调用的行位置。

这些图像来自在 KUROSAKI 中原样运行可下载 ROM。当前输入总线模型支持手柄,但不模拟键盘、光枪或串行适配器信号。外部输入位读为零,因此截图不能验证这些设备。实体按键、光学感应和通信仍未经测试。

NESdev:Family BASIC 键盘

使用示例

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;九组 COL0/COL1 均为 00。A 位于第 6 行、第 0 列的第 4 位,但检测失败会阻止将原始零报告为按下。表格是启动时的一次采集结果。

__fkb_read_row_col: KEYBOARD-DEFAULT — 本次 KUROSAKI 运行中,DEVICE DETECT、ROW 6 COL 0、A KEY - GUARDED 均为 000;九组 COL0/COL1 均为 00。A 位于第 6 行、第 0 列的第 4 位,但检测失败会阻止将原始零报告为按下。表格是启动时的一次采集结果。
KEYBOARD-DEFAULT — 本次 KUROSAKI 运行中,DEVICE DETECT、ROW 6 COL 0、A KEY - GUARDED 均为 000;九组 COL0/COL1 均为 00。A 位于第 6 行、第 0 列的第 4 位,但检测失败会阻止将原始零报告为按下。表格是启动时的一次采集结果。

完整示例程序

构建命令

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

将全部九行、每行两组列扫描到 18 字节中,使一次矩阵采集即可检查多个按键。

语法

c
void __fkb_scan(u8* dst18);

参数

dst18

至少 18 字节的可写存储空间。元素 row*2+col 接收该组原始值。不检查 null 或长度。

返回值

无返回值。

使用条件与限制

这些 API 直接访问 $4016/$4017。不要在同一端口交错执行中断驱动的手柄、键盘或串行操作;控制器移位位置也可能受影响。示例在采集输入时关闭 NMI,再在初始化显示时启用。

按键组使用原始位 1..4:按下会清除对应位,空闲组为 0x1E。它们既不是 ASCII 文本,也不是一表示按下的位图。未连接设备也可能读为零;没有检查设备存在状态时,不要将其解释为全部按键都被按下。

向 $4016 写入 5 以复位到第 0 行,再通过列选择位从高到低的跳变推进各行。除周围指令外,生成代码在复位后加入 12 个 CPU 周期的 NOP,在选择列后加入 50 个周期。退出时写入零以关闭矩阵。

独立回归检查根据实际执行的 CPU 总线轨迹,核对行 0..9、两列、九行扫描、稳定延迟及最终端口状态。不会注入实体按键信号。

每行先读取列 0,再读取列 1。这是顺序扫描,不是所有按键的同步快照。某组为零不会提前结束扫描。

这些图像来自在 KUROSAKI 中原样运行可下载 ROM。当前输入总线模型支持手柄,但不模拟键盘、光枪或串行适配器信号。外部输入位读为零,因此截图不能验证这些设备。实体按键、光学感应和通信仍未经测试。

NESdev:Family BASIC 键盘

使用示例

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;九组 COL0/COL1 均为 00。A 位于第 6 行、第 0 列的第 4 位,但检测失败会阻止将原始零报告为按下。表格是启动时的一次采集结果。

__fkb_scan: KEYBOARD-DEFAULT — 本次 KUROSAKI 运行中,DEVICE DETECT、ROW 6 COL 0、A KEY - GUARDED 均为 000;九组 COL0/COL1 均为 00。A 位于第 6 行、第 0 列的第 4 位,但检测失败会阻止将原始零报告为按下。表格是启动时的一次采集结果。
KEYBOARD-DEFAULT — 本次 KUROSAKI 运行中,DEVICE DETECT、ROW 6 COL 0、A KEY - GUARDED 均为 000;九组 COL0/COL1 均为 00。A 位于第 6 行、第 0 列的第 4 位,但检测失败会阻止将原始零报告为按下。表格是启动时的一次采集结果。

完整示例程序

构建命令

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 中断

使用示例

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。

请让模拟器运行至少 360 帧,以完成等待 256 次的演示。即使 IRQ 被屏蔽,NMI DELTA 3 WAITS 也应为 003;字节循环后,NMI DELTA 256 WAITS 应为 000。NMI OFF DELTA 也应为 000。程序不链接 runtime.c,使用默认处理程序。另有 21 项状态检查,涵盖状态位恢复,以及禁用 NMI 或使用空自定义处理程序时在限定运行时间内的等待行为。已验证 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 中断

使用示例

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。

请让模拟器运行至少 360 帧,以完成等待 256 次的演示。即使 IRQ 被屏蔽,NMI DELTA 3 WAITS 也应为 003;字节循环后,NMI DELTA 256 WAITS 应为 000。NMI OFF DELTA 也应为 000。程序不链接 runtime.c,使用默认处理程序。另有 21 项状态检查,涵盖状态位恢复,以及禁用 NMI 或使用空自定义处理程序时在限定运行时间内的等待行为。已验证 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 允许 IRQ。PHP 压栈时还会将第 4、5 位置 1。

返回值

无(void)。

使用条件与限制

I 位只控制普通 IRQ,NMI 仍可在此处理区间内发生。屏蔽 IRQ 不会清除设备的待处理请求,也不能保护与 NMI 共享的数据。请另行准备中断来源及处理程序。此示例不测试产生 IRQ 的外设。

NESdev: NMI

NESdev:CPU 中断

使用示例

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。

请让模拟器运行至少 360 帧,以完成等待 256 次的演示。即使 IRQ 被屏蔽,NMI DELTA 3 WAITS 也应为 003;字节循环后,NMI DELTA 256 WAITS 应为 000。NMI OFF DELTA 也应为 000。程序不链接 runtime.c,使用默认处理程序。另有 21 项状态检查,涵盖状态位恢复,以及禁用 NMI 或使用空自定义处理程序时在限定运行时间内的等待行为。已验证 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 允许 IRQ。PHP 压栈时还会将第 4、5 位置 1。

使用条件与限制

I 位只控制普通 IRQ,NMI 仍可在此处理区间内发生。屏蔽 IRQ 不会清除设备的待处理请求,也不能保护与 NMI 共享的数据。请另行准备中断来源及处理程序。此示例不测试产生 IRQ 的外设。

NESdev: NMI

NESdev:CPU 中断

使用示例

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。

请让模拟器运行至少 360 帧,以完成等待 256 次的演示。即使 IRQ 被屏蔽,NMI DELTA 3 WAITS 也应为 003;字节循环后,NMI DELTA 256 WAITS 应为 000。NMI OFF DELTA 也应为 000。程序不链接 runtime.c,使用默认处理程序。另有 21 项状态检查,涵盖状态位恢复,以及禁用 NMI 或使用空自定义处理程序时在限定运行时间内的等待行为。已验证 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,不是绝对屏幕 Y 坐标。重载时机及符合条件的 PPU A12 跳变决定触发时刻。零是有效值,不表示“禁用中断”。

返回值

无返回值。

使用条件与限制

内建函数声明位于 intrinsics.h。使用 nes_mapper_irq_* 宏时包含 nes_game.h。构建时通过 --mapper 选择板卡。

仅适用于 MMC3。须提供 __nes_irq 处理程序、符合条件的 PPU A12 跳变、映射器 IRQ 使能,以及 CPU __irq_enable()。示例开启渲染,背景图案位于 $0000,精灵图案位于 $1000。本示例不验证实体硬件上精确到某一行的同步。

MMC3 的确认和禁用操作都写入 $E000,既清除当前请求,也禁用后续请求。要再次触发,请在适当时刻重新调用 __mapper_irq_enable()。两种操作均不改变 CPU 中断屏蔽或 NMI 使能。

编译器为自定义中断处理程序生成 RTI,但处理程序必须保存它会修改的 A/X/Y 寄存器和共享临时存储。示例保存 A/X/Y,只执行字节递增和无参数确认。若添加 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:前三次显式启用各允许一次中断;禁用后计数不变。每个阶段等待六帧。

__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”并不意味着音频处理。分开调用会分别采样,如果期间信号发生变化,结果可能不同。

从$4016的位2读取红白机第二个控制器麦克风对应的数字信号。辅助例程读取一次寄存器,用0x04掩码筛选,不反转,并将结果归一化为0或1。它不会锁存控制器、录制音频、测量音量或识别语音。

语法

c
u8 __joypad2p_voice(void);

返回值

$4016 & 0x04非零时恰好返回1,否则返回0。这是采样到的数字电平,不是按钮掩码、振幅、频率或说出的词数。

使用条件与限制

麦克风虽位于第二个控制器上,此辅助例程访问的却是$4016,而不是$4017。选通信号为低时,读取寄存器还可能推进控制器的串行数据流,因此不要在其他控制器的八位读取循环中插入麦克风读取。不会更新输入历史缓存。

公开版KUROSAKI的控制器模型仅返回D0和固定的位6。因此即使按住普通控制器按钮,该模拟器中的D1与麦克风位也始终为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,因为相应信号线未被模拟。采集后显示值保持不变。在具备适合的外设及其信号线实现的环境中,扩展和麦克风数值取决于实际输入;应按上述位布局解读,而不是期待这些零值。

公开版KUROSAKI的控制器模型仅返回D0和固定的位6。因此即使按住普通控制器按钮,该模拟器中的D1与麦克风位也始终为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

无符号字,范围为 0..65535。

b

无符号系数,范围为 0..255。

返回值

返回 acc+a*b 的低 16 位。

使用条件与限制

在 FC 上,名称含 q8_8/q1_7 的函数仍使用原始整数乘积,不执行定点缩放的右移。Q1.7 函数是对应有符号运算的别名。单位由调用方处理;有符号结果以 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 位值显示;负数采用二进制补码位模式。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

将两点 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| 的低字节;距离超过 255 时会回绕。

使用示例

c
result[5]=__manhattan(2,3,9,11);result[6]=__manhattan(0,0,255,255);

这是一段调用示例。放入完整程序前,请满足所述初始化和缓冲区要求。

预期结果

测试内部、边界、外部点和零宽度: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

将格子坐标转换为行优先数组索引。

语法

c
u16 __map_index(u8 x, u8 y, u8 width);

参数

x

列坐标,范围为 0..255,通常应小于 width。

y

行坐标,范围为 0..255。

width

每行的格子数,范围为 0..255。

返回值

以 u16 返回 y*width+x;不检查坐标有效性。

使用示例

c
result[7]=__map_index(4,3,20);result[8]=__map_index(255,255,255);

这是一段调用示例。放入完整程序前,请满足所述初始化和缓冲区要求。

预期结果

测试内部、边界、外部点和零宽度: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);

返回值

八位映射器编号: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:前三次显式启用各允许一次中断;禁用后计数不变。每个阶段等待六帧。

__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 跳变、映射器 IRQ 使能,以及 CPU __irq_enable()。示例开启渲染,背景图案位于 $0000,精灵图案位于 $1000。本示例不验证实体硬件上精确到某一行的同步。

MMC3 的确认和禁用操作都写入 $E000,既清除当前请求,也禁用后续请求。要再次触发,请在适当时刻重新调用 __mapper_irq_enable()。两种操作均不改变 CPU 中断屏蔽或 NMI 使能。

编译器为自定义中断处理程序生成 RTI,但处理程序必须保存它会修改的 A/X/Y 寄存器和共享临时存储。示例保存 A/X/Y,只执行字节递增和无参数确认。若添加 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:前三次显式启用各允许一次中断;禁用后计数不变。每个阶段等待六帧。

__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 跳变、映射器 IRQ 使能,以及 CPU __irq_enable()。示例开启渲染,背景图案位于 $0000,精灵图案位于 $1000。本示例不验证实体硬件上精确到某一行的同步。

MMC3 的确认和禁用操作都写入 $E000,既清除当前请求,也禁用后续请求。要再次触发,请在适当时刻重新调用 __mapper_irq_enable()。两种操作均不改变 CPU 中断屏蔽或 NMI 使能。

编译器为自定义中断处理程序生成 RTI,但处理程序必须保存它会修改的 A/X/Y 寄存器和共享临时存储。示例保存 A/X/Y,只执行字节递增和无参数确认。若添加 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:前三次显式启用各允许一次中断;禁用后计数不变。每个阶段等待六帧。

__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 跳变、映射器 IRQ 使能,以及 CPU __irq_enable()。示例开启渲染,背景图案位于 $0000,精灵图案位于 $1000。本示例不验证实体硬件上精确到某一行的同步。

MMC3 的确认和禁用操作都写入 $E000,既清除当前请求,也禁用后续请求。要再次触发,请在适当时刻重新调用 __mapper_irq_enable()。两种操作均不改变 CPU 中断屏蔽或 NMI 使能。

编译器为自定义中断处理程序生成 RTI,但处理程序必须保存它会修改的 A/X/Y 寄存器和共享临时存储。示例保存 A/X/Y,只执行字节递增和无参数确认。若添加 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:前三次显式启用各允许一次中断;禁用后计数不变。每个阶段等待六帧。

__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

向 $C000 写入 MMC3 IRQ 锁存值,并通过 $C001 请求下一次重载。启用中断是独立操作。

语法

c
void __mapper_irq_set(u8 scanline);

参数

scanline

八位计数器锁存值,范围为 0..255,不是绝对屏幕 Y 坐标。重载时机及符合条件的 PPU A12 跳变决定触发时刻。零是有效值,不表示“禁用中断”。

返回值

无返回值。

使用条件与限制

内建函数声明位于 intrinsics.h。使用 nes_mapper_irq_* 宏时包含 nes_game.h。构建时通过 --mapper 选择板卡。

仅适用于 MMC3。须提供 __nes_irq 处理程序、符合条件的 PPU A12 跳变、映射器 IRQ 使能,以及 CPU __irq_enable()。示例开启渲染,背景图案位于 $0000,精灵图案位于 $1000。本示例不验证实体硬件上精确到某一行的同步。

MMC3 的确认和禁用操作都写入 $E000,既清除当前请求,也禁用后续请求。要再次触发,请在适当时刻重新调用 __mapper_irq_enable()。两种操作均不改变 CPU 中断屏蔽或 NMI 使能。

编译器为自定义中断处理程序生成 RTI,但处理程序必须保存它会修改的 A/X/Y 寄存器和共享临时存储。示例保存 A/X/Y,只执行字节递增和无参数确认。若添加 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:前三次显式启用各允许一次中断;禁用后计数不变。每个阶段等待六帧。

__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 位数值,可用于复制超过 255 字节的数据,例如 RAM 中的整张表。读写按地址递增顺序进行。

语法

c
void __memcpy(u8* dst, const u8* src, u16 len);

参数

dst

指向目标区域第一个可写字节的指针。必须预先准备足够的连续空间;函数不会自动分配缓冲区。

src

指向源区域第一个可读字节的指针。复制期间,整个源区域必须保持可访问且内容不变。

len

以 u16 指定的字节数(0~65535),不是元素个数。类型的最大值不代表目标机器能够提供同样大小的有效缓冲区。整个范围必须位于当前内存映射之内。

返回值

无(void)。

使用条件与限制

长度为 0 时不传输任何字节,但参数表达式中的副作用仍会发生。示例把两个返回指针的函数作为参数,并确认即使复制长度为 0,这两个函数也都会被调用。

源区域和目标区域应互不重叠。这些函数按地址递增方向复制,不提供 memmove 的重叠安全保证;写入尚未读取的源区域会破坏后续数据。

操作完成后才返回调用方。函数不会切换 ROM/RAM 存储体,也不会把范围自动裁剪到缓冲区边界。请保持映射稳定、指定有效范围,并协调与使用同一缓冲区的中断处理程序之间的访问。

在 FC 上,dst 是 CPU 地址,不是 PPU 地址。复制到 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 从偏移量 1 开始复制 257 字节的规律数据。memory_check 会比较整个源数组和目标数组;COPY WORD 行应显示 257。缓冲区首字节留作保护区,并检查复制范围之后的字节也未改变。

完整程序在每次操作前初始化两个 264 字节缓冲区,随后检查源和目标的每一个字节,以及前后不应改变的保护区域。正确显示为 COPY WORD 257、FILL WORD 257、COPY BYTE 255、FILL BYTE 255、COPY16 016、COPY32 032、ZERO CALLS 002、FAILED 000。WORD/BYTE 表示长度的类型,不是复制元素的大小。这些数字来自示例的检查过程,并非这些无返回值函数的返回值。

截图中,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 字节的数据块。传输长度能用一个字节表示时可选用此形式。_small 表示长度的位宽,并不保证特定的运行时间或生成代码大小。

语法

c
void __memcpy_small(u8* dst, const u8* src, u8 len);

参数

dst

指向目标区域第一个可写字节的指针。必须预先准备足够的连续空间;函数不会自动分配缓冲区。

src

指向源区域第一个可读字节的指针。复制期间,整个源区域必须保持可访问且内容不变。

len

以 u8 指定的字节数,范围为 0~255。不要传入 256 来表示完整的 256 字节块,这个参数的类型无法表示该长度。256 字节及以上请使用名称不带 _small 的对应函数。

返回值

无(void)。

使用条件与限制

长度为 0 时不传输任何字节,但参数表达式中的副作用仍会发生。示例把两个返回指针的函数作为参数,并确认即使复制长度为 0,这两个函数也都会被调用。

源区域和目标区域应互不重叠。这些函数按地址递增方向复制,不提供 memmove 的重叠安全保证;写入尚未读取的源区域会破坏后续数据。

操作完成后才返回调用方。函数不会切换 ROM/RAM 存储体,也不会把范围自动裁剪到缓冲区边界。请保持映射稳定、指定有效范围,并协调与使用同一缓冲区的中断处理程序之间的访问。

在 FC 上,dst 是 CPU 地址,不是 PPU 地址。复制到 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 从偏移量 1 开始复制 255 字节的规律数据。memory_check 会比较整个源数组和目标数组;COPY BYTE 行应显示 255。缓冲区首字节留作保护区,并检查复制范围之后的字节也未改变。

完整程序在每次操作前初始化两个 264 字节缓冲区,随后检查源和目标的每一个字节,以及前后不应改变的保护区域。正确显示为 COPY WORD 257、FILL WORD 257、COPY BYTE 255、FILL BYTE 255、COPY16 016、COPY32 032、ZERO CALLS 002、FAILED 000。WORD/BYTE 表示长度的类型,不是复制元素的大小。这些数字来自示例的检查过程,并非这些无返回值函数的返回值。

截图中,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

用同一个单字节值填充目标区域。长度为 16 位,适合清空较大的缓冲区或初始化表。例如,填充值为 0x5A 时写入 5A 5A 5A…,并不是重复写入一个多字节整数。

语法

c
void __memset(u8* dst, u8 value, u16 len);

参数

dst

指向目标区域第一个可写字节的指针。必须预先准备足够的连续空间;函数不会自动分配缓冲区。

value

在每个目标地址重复写入的单字节值,范围为 0~255。用 0 可清零该区域。如需把 u16 数组初始化为非零数值,应逐个元素赋值。

len

以 u16 指定的字节数(0~65535),不是元素个数。类型的最大值不代表目标机器能够提供同样大小的有效缓冲区。整个范围必须位于当前内存映射之内。

返回值

无(void)。

使用条件与限制

长度为 0 时不传输任何字节,但参数表达式中的副作用仍会发生。示例把两个返回指针的函数作为参数,并确认即使复制长度为 0,这两个函数也都会被调用。

操作完成后才返回调用方。函数不会切换 ROM/RAM 存储体,也不会把范围自动裁剪到缓冲区边界。请保持映射稳定、指定有效范围,并协调与使用同一缓冲区的中断处理程序之间的访问。

在 FC 上,dst 是 CPU 地址,不是 PPU 地址。复制到 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 字节缓冲区,随后检查源和目标的每一个字节,以及前后不应改变的保护区域。正确显示为 COPY WORD 257、FILL WORD 257、COPY BYTE 255、FILL BYTE 255、COPY16 016、COPY32 032、ZERO CALLS 002、FAILED 000。WORD/BYTE 表示长度的类型,不是复制元素的大小。这些数字来自示例的检查过程,并非这些无返回值函数的返回值。

截图中,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 个字节填为同一个单字节值。适合初始化小型工作缓冲区。所需长度超过 255 字节时,请使用 __memset。

语法

c
void __memset_small(u8* dst, u8 value, u8 len);

参数

dst

指向目标区域第一个可写字节的指针。必须预先准备足够的连续空间;函数不会自动分配缓冲区。

value

在每个目标地址重复写入的单字节值,范围为 0~255。用 0 可清零该区域。如需把 u16 数组初始化为非零数值,应逐个元素赋值。

len

以 u8 指定的字节数,范围为 0~255。不要传入 256 来表示完整的 256 字节块,这个参数的类型无法表示该长度。256 字节及以上请使用名称不带 _small 的对应函数。

返回值

无(void)。

使用条件与限制

长度为 0 时不传输任何字节,但参数表达式中的副作用仍会发生。示例把两个返回指针的函数作为参数,并确认即使复制长度为 0,这两个函数也都会被调用。

操作完成后才返回调用方。函数不会切换 ROM/RAM 存储体,也不会把范围自动裁剪到缓冲区边界。请保持映射稳定、指定有效范围,并协调与使用同一缓冲区的中断处理程序之间的访问。

在 FC 上,dst 是 CPU 地址,不是 PPU 地址。复制到 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 字节缓冲区,随后检查源和目标的每一个字节,以及前后不应改变的保护区域。正确显示为 COPY WORD 257、FILL WORD 257、COPY BYTE 255、FILL BYTE 255、COPY16 016、COPY32 032、ZERO CALLS 002、FAILED 000。WORD/BYTE 表示长度的类型,不是复制元素的大小。这些数字来自示例的检查过程,并非这些无返回值函数的返回值。

截图中,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页影子缓冲区的连续条目中。每个部件将字节大小的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

以像素为单位的横向位置,使用无符号字节表示,不是图块坐标。

base_y

直接写入 OAM 的 Y 字节。要把顶部放在第 1 条扫描线或更下方,应传入顶部坐标减一;239..255 位于 240 行可见区域之外。

metasprite

指向可读四字节记录的指针,记录格式为dx, dy, tile, attr。下一个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)显示一对箭头,第二个水平翻转。NEXT 07表示写完槽位5和6后的下一位置。箭头边缘为红色、箭杆为绿色,尖端细节为蓝色。验证会逐图比较8,704个像素并检查传送后的OAM字节;尚未进行实机测试。

TRANSFER在(144,128)显示箭头。程序用__oam_dma直接传送第02页,其中包含各项精灵操作准备好的全部字段。

__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

从$4016的位2读取红白机第二个控制器麦克风对应的数字信号。辅助例程读取一次寄存器,用0x04掩码筛选,不反转,并将结果归一化为0或1。它不会锁存控制器、录制音频、测量音量或识别语音。

语法

c
u8 __mic_read2p(void);

返回值

$4016 & 0x04非零时恰好返回1,否则返回0。这是采样到的数字电平,不是按钮掩码、振幅、频率或说出的词数。

使用条件与限制

麦克风虽位于第二个控制器上,此辅助例程访问的却是$4016,而不是$4017。选通信号为低时,读取寄存器还可能推进控制器的串行数据流,因此不要在其他控制器的八位读取循环中插入麦克风读取。不会更新输入历史缓存。

公开版KUROSAKI的控制器模型仅返回D0和固定的位6。因此即使按住普通控制器按钮,该模拟器中的D1与麦克风位也始终为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,因为相应信号线未被模拟。采集后显示值保持不变。在具备适合的外设及其信号线实现的环境中,扩展和麦克风数值取决于实际输入;应按上述位布局解读,而不是期待这些零值。

公开版KUROSAKI的控制器模型仅返回D0和固定的位6。因此即使按住普通控制器按钮,该模拟器中的D1与麦克风位也始终为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。调用方负责按时间安排后续时钟发送。

语法

c
void __midi_clock(void);

返回值

无返回值。修改音源寄存器或驱动状态。

使用条件与限制

需要外部电路,将 $4016 的 D0 接到发送端、$4017 的 D4 接到接收端。标准手柄本身不能进行 MIDI 通信,这些调用也不会使内置 APU 发声。传输期间应停止 DMC 播放和 DMA、屏蔽 NMI 与 IRQ,并避免其他代码访问同一端口或共享临时区。NTSC 下每位 57 个周期约为 31,400 bps,比标准 31,250 bps 高约 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,内置音源不发声。发送的十六进制字节依次为 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,不操作内部音乐驱动。

语法

c
void __midi_continue(void);

返回值

无返回值。修改音源寄存器或驱动状态。

使用条件与限制

需要外部电路,将 $4016 的 D0 接到发送端、$4017 的 D4 接到接收端。标准手柄本身不能进行 MIDI 通信,这些调用也不会使内置 APU 发声。传输期间应停止 DMC 播放和 DMA、屏蔽 NMI 与 IRQ,并避免其他代码访问同一端口或共享临时区。NTSC 下每位 57 个周期约为 31,400 bps,比标准 31,250 bps 高约 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,内置音源不发声。发送的十六进制字节依次为 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

发送 Control Change:0xB0 | (ch & 0x0F)、控制器编号、数值。

语法

c
void __midi_control_change(u8 ch, u8 cc, u8 value);

参数

ch

MIDI 声道 0~15,将低四位写入状态字节。零对应通常显示的 MIDI 声道 1。

cc

控制器编号 0~127,7 通常表示声道音量。

value

控制器数值 0~127,不会自动清除高位。

返回值

无返回值。修改音源寄存器或驱动状态。

使用条件与限制

需要外部电路,将 $4016 的 D0 接到发送端、$4017 的 D4 接到接收端。标准手柄本身不能进行 MIDI 通信,这些调用也不会使内置 APU 发声。传输期间应停止 DMC 播放和 DMA、屏蔽 NMI 与 IRQ,并避免其他代码访问同一端口或共享临时区。NTSC 下每位 57 个周期约为 31,400 bps,比标准 31,250 bps 高约 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,内置音源不发声。发送的十六进制字节依次为 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

等待接收输入变为低电平,然后按规定时间间隔采样八位数据。

语法

c
u8 __midi_in_byte(void);

返回值

返回收到的 u8。没有超时、帧格式检查或接收缓冲。输入一直为高电平时会无限等待,一直为低电平时会读到零。

使用条件与限制

需要外部电路,将 $4016 的 D0 接到发送端、$4017 的 D4 接到接收端。标准手柄本身不能进行 MIDI 通信,这些调用也不会使内置 APU 发声。传输期间应停止 DMC 播放和 DMA、屏蔽 NMI 与 IRQ,并避免其他代码访问同一端口或共享临时区。NTSC 下每位 57 个周期约为 31,400 bps,比标准 31,250 bps 高约 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,内置音源不发声。发送的十六进制字节依次为 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

发送三个字节的 Note Off:0x80 | (ch & 0x0F)、note、velocity。

语法

c
void __midi_note_off(u8 ch, u8 note, u8 velocity);

参数

ch

MIDI 声道 0~15,将低四位写入状态字节。零对应通常显示的 MIDI 声道 1。

note

MIDI 音符编号 0~127,69 对应 A4。辅助函数不自动截取低七位,调用方须保证范围有效。

velocity

力度 0~127,不会自动限制为七位。

返回值

无返回值。修改音源寄存器或驱动状态。

使用条件与限制

需要外部电路,将 $4016 的 D0 接到发送端、$4017 的 D4 接到接收端。标准手柄本身不能进行 MIDI 通信,这些调用也不会使内置 APU 发声。传输期间应停止 DMC 播放和 DMA、屏蔽 NMI 与 IRQ,并避免其他代码访问同一端口或共享临时区。NTSC 下每位 57 个周期约为 31,400 bps,比标准 31,250 bps 高约 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,内置音源不发声。发送的十六进制字节依次为 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

发送三个字节的 Note On:0x90 | (ch & 0x0F)、note、velocity。

语法

c
void __midi_note_on(u8 ch, u8 note, u8 velocity);

参数

ch

MIDI 声道 0~15,将低四位写入状态字节。零对应通常显示的 MIDI 声道 1。

note

MIDI 音符编号 0~127,69 对应 A4。辅助函数不自动截取低七位,调用方须保证范围有效。

velocity

力度 0~127,不会自动限制为七位。

返回值

无返回值。修改音源寄存器或驱动状态。

使用条件与限制

需要外部电路,将 $4016 的 D0 接到发送端、$4017 的 D4 接到接收端。标准手柄本身不能进行 MIDI 通信,这些调用也不会使内置 APU 发声。传输期间应停止 DMC 播放和 DMA、屏蔽 NMI 与 IRQ,并避免其他代码访问同一端口或共享临时区。NTSC 下每位 57 个周期约为 31,400 bps,比标准 31,250 bps 高约 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,内置音源不发声。发送的十六进制字节依次为 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

同步发送一个 8N1 字节:低电平起始位、从最低位开始的八个数据位、高电平停止位。

语法

c
void __midi_out_byte(u8 byte);

参数

byte

要发送的原始字节 0~255,可以是状态字节或数据字节。

返回值

无返回值。修改音源寄存器或驱动状态。

使用条件与限制

需要外部电路,将 $4016 的 D0 接到发送端、$4017 的 D4 接到接收端。标准手柄本身不能进行 MIDI 通信,这些调用也不会使内置 APU 发声。传输期间应停止 DMC 播放和 DMA、屏蔽 NMI 与 IRQ,并避免其他代码访问同一端口或共享临时区。NTSC 下每位 57 个周期约为 31,400 bps,比标准 31,250 bps 高约 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,内置音源不发声。发送的十六进制字节依次为 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

发送 Program Change:先发送 0xC0 | (ch & 0x0F),再发送程序编号。

语法

c
void __midi_program_change(u8 ch, u8 program);

参数

ch

MIDI 声道 0~15,将低四位写入状态字节。零对应通常显示的 MIDI 声道 1。

program

音色程序编号 0~127,具体乐器对应关系由接收设备决定。

返回值

无返回值。修改音源寄存器或驱动状态。

使用条件与限制

需要外部电路,将 $4016 的 D0 接到发送端、$4017 的 D4 接到接收端。标准手柄本身不能进行 MIDI 通信,这些调用也不会使内置 APU 发声。传输期间应停止 DMC 播放和 DMA、屏蔽 NMI 与 IRQ,并避免其他代码访问同一端口或共享临时区。NTSC 下每位 57 个周期约为 31,400 bps,比标准 31,250 bps 高约 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,内置音源不发声。发送的十六进制字节依次为 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,不改变游戏内部曲目指针。

语法

c
void __midi_start(void);

返回值

无返回值。修改音源寄存器或驱动状态。

使用条件与限制

需要外部电路,将 $4016 的 D0 接到发送端、$4017 的 D4 接到接收端。标准手柄本身不能进行 MIDI 通信,这些调用也不会使内置 APU 发声。传输期间应停止 DMC 播放和 DMA、屏蔽 NMI 与 IRQ,并避免其他代码访问同一端口或共享临时区。NTSC 下每位 57 个周期约为 31,400 bps,比标准 31,250 bps 高约 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,内置音源不发声。发送的十六进制字节依次为 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,不直接关闭 APU,也不直接停止之前已发送的音符。

语法

c
void __midi_stop(void);

返回值

无返回值。修改音源寄存器或驱动状态。

使用条件与限制

需要外部电路,将 $4016 的 D0 接到发送端、$4017 的 D4 接到接收端。标准手柄本身不能进行 MIDI 通信,这些调用也不会使内置 APU 发声。传输期间应停止 DMC 播放和 DMA、屏蔽 NMI 与 IRQ,并避免其他代码访问同一端口或共享临时区。NTSC 下每位 57 个周期约为 31,400 bps,比标准 31,250 bps 高约 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,内置音源不发声。发送的十六进制字节依次为 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

将四个逻辑名称表区域映射到主机的两页屏幕 RAM,用于滚动布局或单屏页面选择。

语法

c
void __mirroring_set(u8 mode);

参数

mode

模式编号随板卡而异。MMC1/SUROM:0=低页单屏、1=高页单屏、2=垂直、3=水平。MMC3:0=垂直、1=水平。MMC5:0=水平、1=垂直、2=低页单屏、3=高页单屏。AxROM:0=低页单屏、1=高页单屏。FME7:0=垂直、1=水平、2=低页单屏、3=高页单屏。MMC3/AxROM 使用最低位,其他使用低两位。

返回值

无返回值。

使用条件与限制

内建函数声明位于 intrinsics.h。使用 nes_mapper_irq_* 宏时包含 nes_game.h。构建时通过 --mapper 选择板卡。

垂直镜像使四个区域映射为 A B A B;水平镜像为 A A B B。低页单屏为 A A A A,高页单屏为 B B B B。这改变内存映射,不翻转或复制背景图形。不支持的板卡(包括固定镜像的 NROM)会在构建时被拒绝。

MMC1 写入控制值 $0C|mode,选择固定的高 16 KiB PRG 窗口和 8 KiB CHR 模式。SUROM 还更新控制影子值。AxROM 保留运行时 PRG 存储体值,只改变屏幕位。若还直接写映射器寄存器,请保持其与运行时存储体状态一致。

MMC5 中,API 向 $5105 写入 $50/$44/$00/$55。这些布局只使用主机的两页 CIRAM,不包括 ExRAM 和填充模式。KUROSAKI 支持全部十六种纯 CIRAM 布局;仍不支持 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,然后读取四个逻辑区域。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

无符号字,范围为 0..65535。

b

无符号系数,范围为 0..255。

返回值

返回 a*b 的低 16 位。

使用条件与限制

在 FC 上,名称含 q8_8/q1_7 的函数仍使用原始整数乘积,不执行定点缩放的右移。Q1.7 函数是对应有符号运算的别名。单位由调用方处理;有符号结果以 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 位值显示;负数采用二进制补码位模式。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

将两个无符号字节相乘,返回乘积的高字节。

语法

c
u8 __mul8x8_hi(u8 a, u8 b);

参数

a

无符号字节,范围为 0..255。

b

无符号字节,范围为 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 位值显示;负数采用二进制补码位模式。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 的一个图块字节。设置地址前会复位锁存器。不会写入图块图形或调色板属性,也不裁剪坐标。

语法

c
void __nametable_put(u8 x, u8 y, u8 tile);

参数

x / y

图块坐标,不是像素坐标。可见的 32×30 图块地图应使用 x=0..31、y=0..29;每个图块为 8×8 像素。字节参数不会被限制在范围内,无效坐标可能写到预期图块区域之外。

tile

PPUCTRL 所选背景图案表中的图块编号 0..255。它选择已有 CHR 图形,不是像素颜色或源数据指针。

返回值

无(void)。

使用条件与限制

直接访问 PPU 时,必须关闭渲染,或确保整个操作处于安全的 VBlank 时间内。这些函数不等待、不排队、不屏蔽 NMI,也不恢复滚动。调用期间应防止中断改变共享锁存器或地址。PPUCTRL 第 2 位决定每个数据字节使目标地址增加 1 还是 32。镜像和地址回绕由 PPU 处理;CHR-ROM 不可写。访问结束后、开始渲染前,请设置所需的滚动状态。

NESdev:PPU 寄存器

NESdev:PPU 滚动

使用示例

c
__nametable_put(2,12,1); // Blue solid marker at (16,96).

这是一段调用示例。放入完整程序前,请满足所述初始化和缓冲区要求。

预期结果

此程序用于学习寄存器设置、图块与属性直接写入、滚动,以及背景和精灵调色板的分别加载。(0,16) 处蓝色条带为 256×16 像素:首行重复实心 8×8 图块、左半 4×8 竖条和八行三角形,第二行只有左半竖条。(16,48) 处是绿色实心 64×32 矩形,(144,48) 处的红色 64×32 区域由横八、纵四个三角形图块组成。

y=96 处,x=16、24、32 的蓝色标记分别为正方形、左半竖条和三角形;x=80 为红色左半竖条,x=144 为绿色三角形。这些是背景图块。使用独立调色板的精灵位于 y=144:x=16 是红色 8×8 正方形,x=80 是蓝色左半 4×8 竖条,x=144 是绿色八行三角形。最终滚动为 (0,0)。

图像检查 16,384 个像素的预期形状和颜色,包括精灵颜色。另有 31 个状态测试,覆盖寄存器替换、状态与锁存器副作用、滚动保存值、0/255 字节传输、增量 1/32、地图边缘、矩形零尺寸、表选择、完整属性字节及两种调色板目标。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 写入一个图块。写入地址对之前复位锁存器。卡带镜像可能让不同的逻辑表编号指向同一物理内存。

语法

c
void __nametable_put_nt(u8 nt, u8 x, u8 y, u8 tile);

参数

nt

逻辑名称表选择值,只使用最低两位(nt & 3)。例如 4 选择表 0,7 选择表 3。不会分配独立存储空间,也不改变映射器镜像。

x / y

图块坐标,不是像素坐标。可见的 32×30 图块地图应使用 x=0..31、y=0..29;每个图块为 8×8 像素。字节参数不会被限制在范围内,无效坐标可能写到预期图块区域之外。

tile

PPUCTRL 所选背景图案表中的图块编号 0..255。它选择已有 CHR 图形,不是像素颜色或源数据指针。

返回值

无(void)。

使用条件与限制

直接访问 PPU 时,必须关闭渲染,或确保整个操作处于安全的 VBlank 时间内。这些函数不等待、不排队、不屏蔽 NMI,也不恢复滚动。调用期间应防止中断改变共享锁存器或地址。PPUCTRL 第 2 位决定每个数据字节使目标地址增加 1 还是 32。镜像和地址回绕由 PPU 处理;CHR-ROM 不可写。访问结束后、开始渲染前,请设置所需的滚动状态。

NESdev:PPU 寄存器

NESdev:PPU 滚动

使用示例

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 竖条和八行三角形,第二行只有左半竖条。(16,48) 处是绿色实心 64×32 矩形,(144,48) 处的红色 64×32 区域由横八、纵四个三角形图块组成。

y=96 处,x=16、24、32 的蓝色标记分别为正方形、左半竖条和三角形;x=80 为红色左半竖条,x=144 为绿色三角形。这些是背景图块。使用独立调色板的精灵位于 y=144:x=16 是红色 8×8 正方形,x=80 是蓝色左半 4×8 竖条,x=144 是绿色八行三角形。最终滚动为 (0,0)。

图像检查 16,384 个像素的预期形状和颜色,包括精灵颜色。另有 31 个状态测试,覆盖寄存器替换、状态与锁存器副作用、滚动保存值、0/255 字节传输、增量 1/32、地图边缘、矩形零尺寸、表选择、完整属性字节及两种调色板目标。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 的图块区域。每行复位锁存器并设置地址,写入 w 个 tile,再将下一行起点前移 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;每个图块为 8×8 像素。字节参数不会被限制在范围内,无效坐标可能写到预期图块区域之外。

w / h

以图块为单位的宽度和高度,各为无符号字节。请保持 x+w<=32、y+h<=30。高度为 0 时不访问 PPU;宽度为 0 时不写图块字节,但仍逐行复位锁存器并设置 h 个行地址。水平行应使用 PPU 增量 1。

tile

PPUCTRL 所选背景图案表中的图块编号 0..255。它选择已有 CHR 图形,不是像素颜色或源数据指针。

返回值

无(void)。

使用条件与限制

直接访问 PPU 时,必须关闭渲染,或确保整个操作处于安全的 VBlank 时间内。这些函数不等待、不排队、不屏蔽 NMI,也不恢复滚动。调用期间应防止中断改变共享锁存器或地址。PPUCTRL 第 2 位决定每个数据字节使目标地址增加 1 还是 32。镜像和地址回绕由 PPU 处理;CHR-ROM 不可写。访问结束后、开始渲染前,请设置所需的滚动状态。

NESdev:PPU 寄存器

NESdev:PPU 滚动

使用示例

c
__nametable_rect(2,6,8,4,1); // Solid green rectangle at (16,48), 64x32 pixels.

这是一段调用示例。放入完整程序前,请满足所述初始化和缓冲区要求。

预期结果

此程序用于学习寄存器设置、图块与属性直接写入、滚动,以及背景和精灵调色板的分别加载。(0,16) 处蓝色条带为 256×16 像素:首行重复实心 8×8 图块、左半 4×8 竖条和八行三角形,第二行只有左半竖条。(16,48) 处是绿色实心 64×32 矩形,(144,48) 处的红色 64×32 区域由横八、纵四个三角形图块组成。

y=96 处,x=16、24、32 的蓝色标记分别为正方形、左半竖条和三角形;x=80 为红色左半竖条,x=144 为绿色三角形。这些是背景图块。使用独立调色板的精灵位于 y=144:x=16 是红色 8×8 正方形,x=80 是蓝色左半 4×8 竖条,x=144 是绿色八行三角形。最终滚动为 (0,0)。

图像检查 16,384 个像素的预期形状和颜色,包括精灵颜色。另有 31 个状态测试,覆盖寄存器替换、状态与锁存器副作用、滚动保存值、0/255 字节传输、增量 1/32、地图边缘、矩形零尺寸、表选择、完整属性字节及两种调色板目标。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 中,每行填充 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

逻辑名称表选择值,只使用最低两位(nt & 3)。例如 4 选择表 0,7 选择表 3。不会分配独立存储空间,也不改变映射器镜像。

x / y

图块坐标,不是像素坐标。可见的 32×30 图块地图应使用 x=0..31、y=0..29;每个图块为 8×8 像素。字节参数不会被限制在范围内,无效坐标可能写到预期图块区域之外。

w / h

以图块为单位的宽度和高度,各为无符号字节。请保持 x+w<=32、y+h<=30。高度为 0 时不访问 PPU;宽度为 0 时不写图块字节,但仍逐行复位锁存器并设置 h 个行地址。水平行应使用 PPU 增量 1。

tile

PPUCTRL 所选背景图案表中的图块编号 0..255。它选择已有 CHR 图形,不是像素颜色或源数据指针。

返回值

无(void)。

使用条件与限制

直接访问 PPU 时,必须关闭渲染,或确保整个操作处于安全的 VBlank 时间内。这些函数不等待、不排队、不屏蔽 NMI,也不恢复滚动。调用期间应防止中断改变共享锁存器或地址。PPUCTRL 第 2 位决定每个数据字节使目标地址增加 1 还是 32。镜像和地址回绕由 PPU 处理;CHR-ROM 不可写。访问结束后、开始渲染前,请设置所需的滚动状态。

NESdev:PPU 寄存器

NESdev:PPU 滚动

使用示例

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 竖条和八行三角形,第二行只有左半竖条。(16,48) 处是绿色实心 64×32 矩形,(144,48) 处的红色 64×32 区域由横八、纵四个三角形图块组成。

y=96 处,x=16、24、32 的蓝色标记分别为正方形、左半竖条和三角形;x=80 为红色左半竖条,x=144 为绿色三角形。这些是背景图块。使用独立调色板的精灵位于 y=144:x=16 是红色 8×8 正方形,x=80 是蓝色左半 4×8 竖条,x=144 是绿色八行三角形。最终滚动为 (0,0)。

图像检查 16,384 个像素的预期形状和颜色,包括精灵颜色。另有 31 个状态测试,覆盖寄存器替换、状态与锁存器副作用、滚动保存值、0/255 字节传输、增量 1/32、地图边缘、矩形零尺寸、表选择、完整属性字节及两种调色板目标。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 中断

使用示例

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。

请让模拟器运行至少 360 帧,以完成等待 256 次的演示。即使 IRQ 被屏蔽,NMI DELTA 3 WAITS 也应为 003;字节循环后,NMI DELTA 256 WAITS 应为 000。NMI OFF DELTA 也应为 000。程序不链接 runtime.c,使用默认处理程序。另有 21 项状态检查,涵盖状态位恢复,以及禁用 NMI 或使用空自定义处理程序时在限定运行时间内的等待行为。已验证 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 位置 1,再写入 $2000,以启用 PPU 产生的 NMI。保留保存值中的其他位及 CPU 的 IRQ 屏蔽状态。此函数不会开启背景或精灵绘制。

语法

c
void __nmi_enable(void);

返回值

无(void)。

使用条件与限制

这些函数使用 PPUCTRL 的保存值,不读取硬件当前值;直接写入 $2000 不会更新保存值。VBlank 标志已置位时,启用 NMI 可能立即触发它。禁用 PPU 的生成机制无法取消 CPU 已锁存的请求。

NESdev: NMI

NESdev:CPU 中断

使用示例

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。

请让模拟器运行至少 360 帧,以完成等待 256 次的演示。即使 IRQ 被屏蔽,NMI DELTA 3 WAITS 也应为 003;字节循环后,NMI DELTA 256 WAITS 应为 000。NMI OFF DELTA 也应为 000。程序不链接 runtime.c,使用默认处理程序。另有 21 项状态检查,涵盖状态位恢复,以及禁用 NMI 或使用空自定义处理程序时在限定运行时间内的等待行为。已验证 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,返回值却是一个字节的计数值,而非布尔就绪标志。与保存的值比较,即可判断默认 NMI 处理程序是否有新的进展。

语法

c
u8 __nmi_ready(void);

返回值

0~255 的无符号字节,由默认 NMI 处理程序递增并按模 256 循环。0 也是有效值。经过 256 次递增后,当前值再次等于保存值;间隔很长的轮询无法无限计数。

使用条件与限制

此计数器属于编译器的默认 NMI 处理程序。定义 __nes_nmi 会替换它。runtime.c 使用独立的 nes_nmi_counter 和 nes_wait_nmi。空的自定义处理程序不会递增编译器计数器,因此 __nmi_wait 不会返回。请启用 NMI,并在 NMI 处理程序之外调用等待函数。

NESdev: NMI

NESdev:CPU 中断

使用示例

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。

请让模拟器运行至少 360 帧,以完成等待 256 次的演示。即使 IRQ 被屏蔽,NMI DELTA 3 WAITS 也应为 003;字节循环后,NMI DELTA 256 WAITS 应为 000。NMI OFF DELTA 也应为 000。程序不链接 runtime.c,使用默认处理程序。另有 21 项状态检查,涵盖状态位恢复,以及禁用 NMI 或使用空自定义处理程序时在限定运行时间内的等待行为。已验证 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 中断

使用示例

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。

请让模拟器运行至少 360 帧,以完成等待 256 次的演示。即使 IRQ 被屏蔽,NMI DELTA 3 WAITS 也应为 003;字节循环后,NMI DELTA 256 WAITS 应为 000。NMI OFF DELTA 也应为 000。程序不链接 runtime.c,使用默认处理程序。另有 21 项状态检查,涵盖状态位恢复,以及禁用 NMI 或使用空自定义处理程序时在限定运行时间内的等待行为。已验证 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)显示一对箭头,第二个水平翻转。NEXT 07表示写完槽位5和6后的下一位置。箭头边缘为红色、箭杆为绿色,尖端细节为蓝色。验证会逐图比较8,704个像素并检查传送后的OAM字节;尚未进行实机测试。

TRANSFER在(144,128)显示箭头。程序用__oam_dma直接传送第02页,其中包含各项精灵操作准备好的全部字段。

__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,从第0个条目开始写入。CPU在传送期间暂停,完成后继续执行。

语法

c
void __oam_dma(void);

返回值

无(void)。

使用条件与限制

请在VBlank期间或关闭渲染时调用;两个DMA内置函数都不会等待VBlank,并会在传送前将OAMADDR设为0。硬件DMA通常使CPU暂停513或514周期,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)显示一对箭头,第二个水平翻转。NEXT 07表示写完槽位5和6后的下一位置。箭头边缘为红色、箭杆为绿色,尖端细节为蓝色。验证会逐图比较8,704个像素并检查传送后的OAM字节;尚未进行实机测试。

TRANSFER在(144,128)显示箭头。程序用__oam_dma直接传送第02页,其中包含各项精灵操作准备好的全部字段。

__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,从第0个条目开始写入。这样可直接使用单独准备的OAM页,无需先复制到第02页。源数据不会被修改。

语法

c
void __oam_dma_page(u8 page);

参数

page

源CPU地址的高字节:传送0x0400–0x04FF时应传入4,而不是0x0400。请提供完整的256字节可读页面,并保持所需存储体已映射。避免使用含I/O寄存器的页面,因为读取可能产生副作用。函数不检查源地址。

返回值

无(void)。

使用条件与限制

请在VBlank期间或关闭渲染时调用;两个DMA内置函数都不会等待VBlank,并会在传送前将OAMADDR设为0。硬件DMA通常使CPU暂停513或514周期,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)显示一对箭头,第二个水平翻转。NEXT 07表示写完槽位5和6后的下一位置。箭头边缘为红色、箭杆为绿色,尖端细节为蓝色。验证会逐图比较8,704个像素并检查传送后的OAM字节;尚未进行实机测试。

此变体将准备好的第02页复制到0x0400处的独立数组,只把条目7的图块改为129。调用__oam_dma_page(4)后,TRANSFER旁的(144,128)应显示绿色方块,而第02页仍保留箭头图块128。其他行与默认页示例相同。验证会将全部256个硬件OAM字节与第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 手柄快照的位 0~3(pad & 0x0F):A、B、Select 和 Start。只过滤已读取的数值,不访问控制器硬件。

语法

c
u8 __pad_buttons(u8 pad);

参数

pad

已读取的 8 位 NES 手柄值,例如 __pad_read1_safe() 的结果。传入数值本身,不是指针、端口号或 input_* 的通用掩码。

返回值

按上述位序返回 0~15 的掩码。可能同时置多个位;应使用 result & bit 检查某个按钮,而不是判断整个结果是否等于 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_* 掩码混用。

使用示例

c
buttons=__pad_buttons(raw1); // Low nibble of 129: 1, the A button.

这是一段调用示例。放入完整程序前,请满足所述初始化和缓冲区要求。

预期结果

此程序比较两个控制器端口、普通读取与一致性检查读取、掩码筛选和简写宏。在控制器1上同时按A和右,在控制器2上同时按B和左。RAW P1、SAFE P1、ALIAS P1为129;P2的三个数值为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 手柄快照的位 4~7(pad & 0xF0):上、下、左、右。位位置保持不变,因此仅右键返回 128,而不是 8。不读取控制器。

语法

c
u8 __pad_dirs(u8 pad);

参数

pad

已读取的 8 位 NES 手柄值,例如 __pad_read1_safe() 的结果。传入数值本身,不是指针、端口号或 input_* 的通用掩码。

返回值

传入值与 0xF0 按位与,结果为 0、16、32、…、240。方向位仍位于位 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.

这是一段调用示例。放入完整程序前,请满足所述初始化和缓冲区要求。

预期结果

此程序比较两个控制器端口、普通读取与一致性检查读取、掩码筛选和简写宏。在控制器1上同时按A和右,在控制器2上同时按B和左。RAW P1、SAFE P1、ALIAS P1为129;P2的三个数值为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 的八位数据。返回一次 NES 格式的按住状态,不重试,也不计算按下或松开事件。

语法

c
u8 __pad_read1(void);

返回值

NES 原始位序的 8 位按住掩码。置位的每一位代表一个按下按钮,0 表示没有按钮。不是归一化的布尔值。

使用条件与限制

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.

这是一段调用示例。放入完整程序前,请满足所述初始化和缓冲区要求。

预期结果

此程序比较两个控制器端口、普通读取与一致性检查读取、掩码筛选和简写宏。在控制器1上同时按A和右,在控制器2上同时按B和左。RAW P1、SAFE P1、ALIAS P1为129;P2的三个数值为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串行读取八位。向$4016依次写入1和0以锁存控制器状态,读取位1八次,并依次放入结果的位0至7。这是对端口1的D1通道进行一次读取;不会读取普通控制器的D0通道,也不会为检查一致性而重试。

语法

c
u8 __pad_read1_d1(void);

返回值

NES 原始位序的 8 位按住掩码。置位的每一位代表一个按下按钮,0 表示没有按钮。不是归一化的布尔值。

使用条件与限制

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线上提供兼容八位串行手柄信号的外设。函数不会识别已连接的设备,也没有实现所有扩展设备协议。每次调用都会重新锁存两个控制器的数据流,因此应完成一次完整读取后再开始下一次。结果为新采集的状态,不会更新input_*或nes_pad1_*缓存。

公开版KUROSAKI的控制器模型仅返回D0和固定的位6。因此即使按住普通控制器按钮,该模拟器中的D1与麦克风位也始终为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,因为相应信号线未被模拟。采集后显示值保持不变。在具备适合的外设及其信号线实现的环境中,扩展和麦克风数值取决于实际输入;应按上述位布局解读,而不是期待这些零值。

公开版KUROSAKI的控制器模型仅返回D0和固定的位6。因此即使按住普通控制器按钮,该模拟器中的D1与麦克风位也始终为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() 读取控制器 1 的全部按钮,两次结果一致时返回 NES 格式掩码。返回的是按住状态,不是新按下事件。

语法

c
u8 __pad_read1_safe(void);

返回值

NES 原始位序的 8 位按住掩码。置位的每一位代表一个按下按钮,0 表示没有按钮。不是归一化的布尔值。

使用条件与限制

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 版本成对读取全部按钮,直到两次掩码相同。每次读取都会选通 $4016,使两个控制器的串行读取位置回到开头。重试次数没有上限。此一致性检查并不意味着已验证所有 DMC 和外设组合。

使用示例

c
safe1=__pad_read1_safe(); // Repeat paired controller-1 reads until they agree.

这是一段调用示例。放入完整程序前,请满足所述初始化和缓冲区要求。

预期结果

此程序比较两个控制器端口、普通读取与一致性检查读取、掩码筛选和简写宏。在控制器1上同时按A和右,在控制器2上同时按B和左。RAW P1、SAFE P1、ALIAS P1为129;P2的三个数值为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 的八位数据。返回一次 NES 格式的按住状态,不重试,也不计算按下或松开事件。

语法

c
u8 __pad_read2(void);

返回值

NES 原始位序的 8 位按住掩码。置位的每一位代表一个按下按钮,0 表示没有按钮。不是归一化的布尔值。

使用条件与限制

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.

这是一段调用示例。放入完整程序前,请满足所述初始化和缓冲区要求。

预期结果

此程序比较两个控制器端口、普通读取与一致性检查读取、掩码筛选和简写宏。在控制器1上同时按A和右,在控制器2上同时按B和左。RAW P1、SAFE P1、ALIAS P1为129;P2的三个数值为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串行读取八位。向$4016依次写入1和0以锁存控制器状态,读取位1八次,并依次放入结果的位0至7。这是对端口2的D1通道进行一次读取;不会读取普通控制器的D0通道,也不会为检查一致性而重试。

语法

c
u8 __pad_read2_d1(void);

返回值

NES 原始位序的 8 位按住掩码。置位的每一位代表一个按下按钮,0 表示没有按钮。不是归一化的布尔值。

使用条件与限制

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线上提供兼容八位串行手柄信号的外设。函数不会识别已连接的设备,也没有实现所有扩展设备协议。每次调用都会重新锁存两个控制器的数据流,因此应完成一次完整读取后再开始下一次。结果为新采集的状态,不会更新input_*或nes_pad1_*缓存。

公开版KUROSAKI的控制器模型仅返回D0和固定的位6。因此即使按住普通控制器按钮,该模拟器中的D1与麦克风位也始终为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,因为相应信号线未被模拟。采集后显示值保持不变。在具备适合的外设及其信号线实现的环境中,扩展和麦克风数值取决于实际输入;应按上述位布局解读,而不是期待这些零值。

公开版KUROSAKI的控制器模型仅返回D0和固定的位6。因此即使按住普通控制器按钮,该模拟器中的D1与麦克风位也始终为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 的全部按钮,两次结果一致时返回 NES 格式掩码。返回的是按住状态,不是新按下事件。

语法

c
u8 __pad_read2_safe(void);

返回值

NES 原始位序的 8 位按住掩码。置位的每一位代表一个按下按钮,0 表示没有按钮。不是归一化的布尔值。

使用条件与限制

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 版本成对读取全部按钮,直到两次掩码相同。每次读取都会选通 $4016,使两个控制器的串行读取位置回到开头。重试次数没有上限。此一致性检查并不意味着已验证所有 DMC 和外设组合。

使用示例

c
safe2=__pad_read2_safe(); // The same consistency check on controller 2.

这是一段调用示例。放入完整程序前,请满足所述初始化和缓冲区要求。

预期结果

此程序比较两个控制器端口、普通读取与一致性检查读取、掩码筛选和简写宏。在控制器1上同时按A和右,在控制器2上同时按B和左。RAW P1、SAFE P1、ALIAS P1为129;P2的三个数值为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,通过 PPUDATA 写入源数据的 16 字节,供四组背景调色板使用。这是立即传输,不改变名称表属性中的调色板选择,也不加载图块图形。

语法

c
void __palette_bg_load(const u8* pal16);

参数

pal16

CPU 指针,指向 16 个可读的 NES 调色板代码字节。这些是调色板代码,不是 RGB 三元组。同步调用返回前应保持源存储体映射;不会切换存储体或检查长度。请使用 PPU 增量 1,以写入预期的 16 个调色板地址。

返回值

无(void)。

使用条件与限制

PPU 地址 0x3F10/14/18/1C 与 0x3F00/04/08/0C 共享存储位置。因此加载精灵调色板也会写入这些背景项。编译器原样发送字节,不验证颜色选择、不合并调色板,也不保留共享项。

直接访问 PPU 时,必须关闭渲染,或确保整个操作处于安全的 VBlank 时间内。这些函数不等待、不排队、不屏蔽 NMI,也不恢复滚动。调用期间应防止中断改变共享锁存器或地址。PPUCTRL 第 2 位决定每个数据字节使目标地址增加 1 还是 32。镜像和地址回绕由 PPU 处理;CHR-ROM 不可写。访问结束后、开始渲染前,请设置所需的滚动状态。

NESdev:PPU 寄存器

NESdev:PPU 滚动

使用示例

c
__palette_bg_load(scene_palette); // Four background palettes, 16 bytes total.

这是一段调用示例。放入完整程序前,请满足所述初始化和缓冲区要求。

预期结果

此程序用于学习寄存器设置、图块与属性直接写入、滚动,以及背景和精灵调色板的分别加载。(0,16) 处蓝色条带为 256×16 像素:首行重复实心 8×8 图块、左半 4×8 竖条和八行三角形,第二行只有左半竖条。(16,48) 处是绿色实心 64×32 矩形,(144,48) 处的红色 64×32 区域由横八、纵四个三角形图块组成。

y=96 处,x=16、24、32 的蓝色标记分别为正方形、左半竖条和三角形;x=80 为红色左半竖条,x=144 为绿色三角形。这些是背景图块。使用独立调色板的精灵位于 y=144:x=16 是红色 8×8 正方形,x=80 是蓝色左半 4×8 竖条,x=144 是绿色八行三角形。最终滚动为 (0,0)。

图像检查 16,384 个像素的预期形状和颜色,包括精灵颜色。另有 31 个状态测试,覆盖寄存器替换、状态与锁存器副作用、滚动保存值、0/255 字节传输、增量 1/32、地图边缘、矩形零尺寸、表选择、完整属性字节及两种调色板目标。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,通过 PPUDATA 写入源数据的 16 字节,供四组精灵调色板使用。精灵属性位决定显示哪组调色板。此调用不修改 OAM,也不传输精灵图块图形。

语法

c
void __palette_sp_load(const u8* pal16);

参数

pal16

CPU 指针,指向 16 个可读的 NES 调色板代码字节。这些是调色板代码,不是 RGB 三元组。同步调用返回前应保持源存储体映射;不会切换存储体或检查长度。请使用 PPU 增量 1,以写入预期的 16 个调色板地址。

返回值

无(void)。

使用条件与限制

PPU 地址 0x3F10/14/18/1C 与 0x3F00/04/08/0C 共享存储位置。因此加载精灵调色板也会写入这些背景项。编译器原样发送字节,不验证颜色选择、不合并调色板,也不保留共享项。

直接访问 PPU 时,必须关闭渲染,或确保整个操作处于安全的 VBlank 时间内。这些函数不等待、不排队、不屏蔽 NMI,也不恢复滚动。调用期间应防止中断改变共享锁存器或地址。PPUCTRL 第 2 位决定每个数据字节使目标地址增加 1 还是 32。镜像和地址回绕由 PPU 处理;CHR-ROM 不可写。访问结束后、开始渲染前,请设置所需的滚动状态。

NESdev:PPU 寄存器

NESdev:PPU 滚动

使用示例

c
__palette_sp_load(scene_palette); // Four sprite palettes, 16 bytes total.

这是一段调用示例。放入完整程序前,请满足所述初始化和缓冲区要求。

预期结果

此程序用于学习寄存器设置、图块与属性直接写入、滚动,以及背景和精灵调色板的分别加载。(0,16) 处蓝色条带为 256×16 像素:首行重复实心 8×8 图块、左半 4×8 竖条和八行三角形,第二行只有左半竖条。(16,48) 处是绿色实心 64×32 矩形,(144,48) 处的红色 64×32 区域由横八、纵四个三角形图块组成。

y=96 处,x=16、24、32 的蓝色标记分别为正方形、左半竖条和三角形;x=80 为红色左半竖条,x=144 为绿色三角形。这些是背景图块。使用独立调色板的精灵位于 y=144:x=16 是红色 8×8 正方形,x=80 是蓝色左半 4×8 竖条,x=144 是绿色八行三角形。最终滚动为 (0,0)。

图像检查 16,384 个像素的预期形状和颜色,包括精灵颜色。另有 31 个状态测试,覆盖寄存器替换、状态与锁存器副作用、滚动保存值、0/255 字节传输、增量 1/32、地图边缘、矩形零尺寸、表选择、完整属性字节及两种调色板目标。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

PPU字节地址(0x0000–0x3FFF),不是CPU指针。目标必须可写;改写图案需要CHR RAM,不能写入CHR ROM。PPU映射与镜像规则照常适用。连续复制和填充应将PPUCTRL地址增量设为1。在安全的PPU写入时段执行,然后恢复所需的滚动状态;执行器会写入PPUADDR和PPUDATA,不会恢复滚动设置。

返回值

无(void)。

使用条件与限制

直接访问 PPU 时,必须关闭渲染,或确保整个操作处于安全的 VBlank 时间内。这些函数不等待、不排队、不屏蔽 NMI,也不恢复滚动。调用期间应防止中断改变共享锁存器或地址。PPUCTRL 第 2 位决定每个数据字节使目标地址增加 1 还是 32。镜像和地址回绕由 PPU 处理;CHR-ROM 不可写。访问结束后、开始渲染前,请设置所需的滚动状态。

NESdev:PPU 寄存器

NESdev:PPU 滚动

使用示例

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 竖条和八行三角形,第二行只有左半竖条。(16,48) 处是绿色实心 64×32 矩形,(144,48) 处的红色 64×32 区域由横八、纵四个三角形图块组成。

y=96 处,x=16、24、32 的蓝色标记分别为正方形、左半竖条和三角形;x=80 为红色左半竖条,x=144 为绿色三角形。这些是背景图块。使用独立调色板的精灵位于 y=144:x=16 是红色 8×8 正方形,x=80 是蓝色左半 4×8 竖条,x=144 是绿色八行三角形。最终滚动为 (0,0)。

图像检查 16,384 个像素的预期形状和颜色,包括精灵颜色。另有 31 个状态测试,覆盖寄存器替换、状态与锁存器副作用、滚动保存值、0/255 字节传输、增量 1/32、地图边缘、矩形零尺寸、表选择、完整属性字节及两种调色板目标。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);

这是一段调用示例。放入完整程序前,请满足所述初始化和缓冲区要求。

预期结果

可运行替代方案先关闭渲染,设置调色板并清空图块和属性,再绘制此测试图像。蓝色实心图块、左半竖条与三角形的重复图案从 (0,16) 开始,绿色实心区域从 (0,96) 开始。每块区域为 256×64 像素,但只传输 255 个图块字节,因此最后的 8×8 单元 (248,72) 和 (248,152) 保持黑色。y=208 处的红色图形分别是 x=16 的 8×8 正方形、x=224 的左半 4×8 竖条、x=240 的八行三角形。FAILED CHECKS 为 0。独立状态测试还确认两个控制寄存器、32 字节调色板传输,以及名称表全部 1,024 字节的清空。直接调用四个声明时,预期结果是链接失败。

__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 及其软件保存值,控制 NMI 启用、PPU 地址增量、图案表选择、精灵尺寸和基础名称表。不会修改 PPUMASK,也不会保留未指定的位。

语法

c
void __ppu_ctrl_set(u8 value);

参数

value

无符号字节 0..255,按原值写入。这是完整的寄存器或数据字节,不是布尔值或位编号。

返回值

无(void)。

NESdev:PPU 寄存器

NESdev:PPU 滚动

使用示例

c
__ppu_ctrl_set(0x80); // NMI enabled, increment 1, nametable/pattern tables 0.

这是一段调用示例。放入完整程序前,请满足所述初始化和缓冲区要求。

预期结果

此程序用于学习寄存器设置、图块与属性直接写入、滚动,以及背景和精灵调色板的分别加载。(0,16) 处蓝色条带为 256×16 像素:首行重复实心 8×8 图块、左半 4×8 竖条和八行三角形,第二行只有左半竖条。(16,48) 处是绿色实心 64×32 矩形,(144,48) 处的红色 64×32 区域由横八、纵四个三角形图块组成。

y=96 处,x=16、24、32 的蓝色标记分别为正方形、左半竖条和三角形;x=80 为红色左半竖条,x=144 为绿色三角形。这些是背景图块。使用独立调色板的精灵位于 y=144:x=16 是红色 8×8 正方形,x=80 是蓝色左半 4×8 竖条,x=144 是绿色八行三角形。最终滚动为 (0,0)。

图像检查 16,384 个像素的预期形状和颜色,包括精灵颜色。另有 31 个状态测试,覆盖寄存器替换、状态与锁存器副作用、滚动保存值、0/255 字节传输、增量 1/32、地图边缘、矩形零尺寸、表选择、完整属性字节及两种调色板目标。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

通过 PPUDATA 向当前 PPU 地址写入一次 value,之后地址按 PPUCTRL 设置增加 1 或 32。此调用不设置地址、不复位锁存器,也不将传输排入队列。

语法

c
void __ppu_data(u8 value);

参数

value

无符号字节 0..255,按原值写入。这是完整的寄存器或数据字节,不是布尔值或位编号。

返回值

无(void)。

使用条件与限制

直接访问 PPU 时,必须关闭渲染,或确保整个操作处于安全的 VBlank 时间内。这些函数不等待、不排队、不屏蔽 NMI,也不恢复滚动。调用期间应防止中断改变共享锁存器或地址。PPUCTRL 第 2 位决定每个数据字节使目标地址增加 1 还是 32。镜像和地址回绕由 PPU 处理;CHR-ROM 不可写。访问结束后、开始渲染前,请设置所需的滚动状态。

NESdev:PPU 寄存器

NESdev:PPU 滚动

使用示例

c
__ppu_data(3); // Green triangle at (144,96); destination advances by one.

这是一段调用示例。放入完整程序前,请满足所述初始化和缓冲区要求。

预期结果

此程序用于学习寄存器设置、图块与属性直接写入、滚动,以及背景和精灵调色板的分别加载。(0,16) 处蓝色条带为 256×16 像素:首行重复实心 8×8 图块、左半 4×8 竖条和八行三角形,第二行只有左半竖条。(16,48) 处是绿色实心 64×32 矩形,(144,48) 处的红色 64×32 区域由横八、纵四个三角形图块组成。

y=96 处,x=16、24、32 的蓝色标记分别为正方形、左半竖条和三角形;x=80 为红色左半竖条,x=144 为绿色三角形。这些是背景图块。使用独立调色板的精灵位于 y=144:x=16 是红色 8×8 正方形,x=80 是蓝色左半 4×8 竖条,x=144 是绿色八行三角形。最终滚动为 (0,0)。

图像检查 16,384 个像素的预期形状和颜色,包括精灵颜色。另有 31 个状态测试,覆盖寄存器替换、状态与锁存器副作用、滚动保存值、0/255 字节传输、增量 1/32、地图边缘、矩形零尺寸、表选择、完整属性字节及两种调色板目标。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);

这是一段调用示例。放入完整程序前,请满足所述初始化和缓冲区要求。

预期结果

可运行替代方案先关闭渲染,设置调色板并清空图块和属性,再绘制此测试图像。蓝色实心图块、左半竖条与三角形的重复图案从 (0,16) 开始,绿色实心区域从 (0,96) 开始。每块区域为 256×64 像素,但只传输 255 个图块字节,因此最后的 8×8 单元 (248,72) 和 (248,152) 保持黑色。y=208 处的红色图形分别是 x=16 的 8×8 正方形、x=224 的左半 4×8 竖条、x=240 的八行三角形。FAILED CHECKS 为 0。独立状态测试还确认两个控制寄存器、32 字节调色板传输,以及名称表全部 1,024 字节的清空。直接调用四个声明时,预期结果是链接失败。

__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 字节及其软件保存值替换为 value。请明确选择渲染、左边缘显示、灰度和颜色强调;函数不会与原掩码合并位,也不修改 PPUCTRL。

语法

c
void __ppu_mask_set(u8 value);

参数

value

无符号字节 0..255,按原值写入。这是完整的寄存器或数据字节,不是布尔值或位编号。

返回值

无(void)。

NESdev:PPU 寄存器

NESdev:PPU 滚动

使用示例

c
__ppu_mask_set(0x0A); // Background enabled, including its leftmost eight pixels.

这是一段调用示例。放入完整程序前,请满足所述初始化和缓冲区要求。

预期结果

此程序用于学习寄存器设置、图块与属性直接写入、滚动,以及背景和精灵调色板的分别加载。(0,16) 处蓝色条带为 256×16 像素:首行重复实心 8×8 图块、左半 4×8 竖条和八行三角形,第二行只有左半竖条。(16,48) 处是绿色实心 64×32 矩形,(144,48) 处的红色 64×32 区域由横八、纵四个三角形图块组成。

y=96 处,x=16、24、32 的蓝色标记分别为正方形、左半竖条和三角形;x=80 为红色左半竖条,x=144 为绿色三角形。这些是背景图块。使用独立调色板的精灵位于 y=144:x=16 是红色 8×8 正方形,x=80 是蓝色左半 4×8 竖条,x=144 是绿色八行三角形。最终滚动为 (0,0)。

图像检查 16,384 个像素的预期形状和颜色,包括精灵颜色。另有 31 个状态测试,覆盖寄存器替换、状态与锁存器副作用、滚动保存值、0/255 字节传输、增量 1/32、地图边缘、矩形零尺寸、表选择、完整属性字节及两种调色板目标。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 及其软件保存值写入零,停止背景和精灵渲染。包括灰度和颜色强调在内的所有掩码位都会清零。PPUCTRL 与 NMI 启用状态不变;准备共享传输状态时,请按需另行控制 NMI。

语法

c
void __ppu_off(void);

返回值

无(void)。

NESdev:PPU 寄存器

NESdev:PPU 滚动

使用示例

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 竖条和八行三角形,第二行只有左半竖条。(16,48) 处是绿色实心 64×32 矩形,(144,48) 处的红色 64×32 区域由横八、纵四个三角形图块组成。

y=96 处,x=16、24、32 的蓝色标记分别为正方形、左半竖条和三角形;x=80 为红色左半竖条,x=144 为绿色三角形。这些是背景图块。使用独立调色板的精灵位于 y=144:x=16 是红色 8×8 正方形,x=80 是蓝色左半 4×8 竖条,x=144 是绿色八行三角形。最终滚动为 (0,0)。

图像检查 16,384 个像素的预期形状和颜色,包括精灵颜色。另有 31 个状态测试,覆盖寄存器替换、状态与锁存器副作用、滚动保存值、0/255 字节传输、增量 1/32、地图边缘、矩形零尺寸、表选择、完整属性字节及两种调色板目标。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,启用背景与精灵渲染,包括最左侧八个像素。灰度和颜色强调设置也会被覆盖;不修改 PPUCTRL,也不启用 NMI。

语法

c
void __ppu_on(void);

返回值

无(void)。

NESdev:PPU 寄存器

NESdev:PPU 滚动

使用示例

c
__ppu_on(); // Replace mask with 0x1E: enable sprites and background at both edges.

这是一段调用示例。放入完整程序前,请满足所述初始化和缓冲区要求。

预期结果

此程序用于学习寄存器设置、图块与属性直接写入、滚动,以及背景和精灵调色板的分别加载。(0,16) 处蓝色条带为 256×16 像素:首行重复实心 8×8 图块、左半 4×8 竖条和八行三角形,第二行只有左半竖条。(16,48) 处是绿色实心 64×32 矩形,(144,48) 处的红色 64×32 区域由横八、纵四个三角形图块组成。

y=96 处,x=16、24、32 的蓝色标记分别为正方形、左半竖条和三角形;x=80 为红色左半竖条,x=144 为绿色三角形。这些是背景图块。使用独立调色板的精灵位于 y=144:x=16 是红色 8×8 正方形,x=80 是蓝色左半 4×8 竖条,x=144 是绿色八行三角形。最终滚动为 (0,0)。

图像检查 16,384 个像素的预期形状和颜色,包括精灵颜色。另有 31 个状态测试,覆盖寄存器替换、状态与锁存器副作用、滚动保存值、0/255 字节传输、增量 1/32、地图边缘、矩形零尺寸、表选择、完整属性字节及两种调色板目标。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,返回本次读取清除 VBlank 标志之前观察到的字节,同时复位滚动与地址共用的写入锁存器。这是有副作用的硬件状态读取,不是缓存的帧计数器,也不是 NMI 完成检查。

语法

c
u8 __ppu_read_status(void);

返回值

原始状态字节:第 7 位为 VBlank,第 6 位为精灵零命中,第 5 位为精灵溢出。应使用掩码测试所需标志,不要将整个字节当作布尔值或帧编号。低位不是可跨环境使用的应用状态值。

使用条件与限制

读取 PPUSTATUS 可能影响 NMI 时序,以及后续对 PPUSCROLL 和 PPUADDR 的写入。不要把它插入其他例程尚未写完的寄存器字节对之间。这些调用不会等待 VBlank,也不会完成待处理传输队列。

NESdev:PPU 寄存器

NESdev:PPU 滚动

使用示例

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 竖条和八行三角形,第二行只有左半竖条。(16,48) 处是绿色实心 64×32 矩形,(144,48) 处的红色 64×32 区域由横八、纵四个三角形图块组成。

y=96 处,x=16、24、32 的蓝色标记分别为正方形、左半竖条和三角形;x=80 为红色左半竖条,x=144 为绿色三角形。这些是背景图块。使用独立调色板的精灵位于 y=144:x=16 是红色 8×8 正方形,x=80 是蓝色左半 4×8 竖条,x=144 是绿色八行三角形。最终滚动为 (0,0)。

图像检查 16,384 个像素的预期形状和颜色,包括精灵颜色。另有 31 个状态测试,覆盖寄存器替换、状态与锁存器副作用、滚动保存值、0/255 字节传输、增量 1/32、地图边缘、矩形零尺寸、表选择、完整属性字节及两种调色板目标。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 相同的 mapper 对应 PRG 存储体切换。编号检查规则相同,不会自动恢复之前的映射。

语法

c
void __prg_bank_set(u8 bank);

参数

bank

无符号 8 位 ROM 存储体编号。调用时应与函数布局一致,读取时用于选择源映射。请使用目标卡带支持的范围。

返回值

无(void)。

使用条件与限制

持续生效的存储体切换不能使当前代码或返回地址失去映射。请像示例一样,将调用点、所用的 bank_switch 本体及后续代码放在公共或固定存储体 0。

库记录初值为 0,不会查询卡带启动时的映射。请用 bank_switch 初始化。直接调用切换内置函数或写 mapper 不会更新这份独立记录,因此 bank_get_current 无法验证实际硬件映射。

FC 切换编号是编译器的逻辑编号。0 表示公共 ROM,不是切换目标;辅助代码将 0 归一为存储体 1。SUROM512 仅允许 1–30:直接传给切换内置函数的越界常量会导致构建错误;无效运行时值会回退到 1 并设置 mapper 故障状态。公共 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。

完整程序演示如何用存储体编号和近地址确定数据、如何跨存储体调用回调,以及如何区分库记录与实际映射。所有数值均为十六进制。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 各执行一次回调。

__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

将共享的 16 位 FC Galois LFSR 推进一步,返回更新后低字节与高字节的异或值。LFSR 是通过异或计算反馈的移位寄存器,因此固定种子产生可复现的字节序列。

语法

c
u8 __rng8(void);

返回值

共享生成器推进一次后的 u8 结果,范围为 0–255。这描述类型和边界,不表示每个字节等概率。

使用条件与限制

FC 的一步运算将 16 位状态右移一位。如果原最低位为 1,则将移位后的状态与 0xB400 异或。返回字节为 low_byte XOR high_byte,不是原始低字节。

两个状态字节均为零时,下一次取值会先替换为 0xA55A 再移位。因此 __rng_seed(0) 后调用 __rng8() 返回 255。设置种子时保存的是零;替换发生在取值调用中。

这些调用共享持久状态。复现要求相同目标、种子和调用顺序,包括其他辅助函数发起的调用。应在同一执行上下文中设置种子和取值;这些检查不证明可从中断处理程序安全并发使用。

GB 和 FC 使用不同算法,不保证相同种子产生相同序列。它们是确定性的游戏用生成器,不是密码学随机源;测试检查实现行为,不评估统计质量或周期。

使用示例

c
__rng_seed(0x1234);
first=__rng8();second=__rng8();third=__rng8(); // FC sequence: 19, 137, 240.

这是一段调用示例。放入完整程序前,请满足所述初始化和缓冲区要求。

预期结果

示例读取三个连续 FC 字节。种子 0x1234 产生 FIRST 00019、SECOND 00137、THIRD 00240。这些值表示调用顺序,不是频率或随机性评分。

完整 NROM 程序在黑底上打印蓝色十进制结果。FAILED 必须为 00000。文字在关闭渲染时上传,然后启用结果画面。

验证将每个案例的 32 个输出及随后八次字节取值与独立宿主模型比较: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 生成器的两个状态字节。复用种子可使测试或回放从伪随机序列同一位置开始;本调用不推进生成器。

语法

c
void __rng_seed(u16 seed);

参数

seed

无符号 16 位种子,范围为 0–65535。两个字节原样保存。在希望复现的游戏取值序列开始前,选择并记录种子。

返回值

无(void)。

使用条件与限制

两个状态字节均为零时,下一次取值会先替换为 0xA55A 再移位。因此 __rng_seed(0) 后调用 __rng8() 返回 255。设置种子时保存的是零;替换发生在取值调用中。

这些调用共享持久状态。复现要求相同目标、种子和调用顺序,包括其他辅助函数发起的调用。应在同一执行上下文中设置种子和取值;这些检查不证明可从中断处理程序安全并发使用。

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 可复现的起始序列和零种子规则。种子为 0x1234 时,FIRST 和 RESEEDED 均显示 00019。种子设为零后,ZERO SEED 显示 00255。

完整 NROM 程序在黑底上打印蓝色十进制结果。FAILED 必须为 00000。文字在关闭渲染时上传,然后启用结果画面。

验证将每个案例的 32 个输出及随后八次字节取值与独立宿主模型比较: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 计数器变化一次。返回后所选掩码继续生效。

语法

c
void __rob_flash(u8 bright);

参数

bright

零选择关闭,任意非零字节(包括 255)选择开启。这不是亮度级别。

返回值

无返回值。完成指定等待后返回调用方。

使用条件与限制

包含 intrinsics.h。默认 NMI 处理程序必须运行并推进其计数器。禁用 NMI 或自定义处理程序不维护该计数器时,等待会一直阻塞。没有超时。

亮阶段将 PPUMASK 及其软件影子值设为 0x1E;暗阶段均设为零。不恢复先前掩码。这会同时改变背景、精灵和左缘渲染,也会替换灰度及颜色强调设置。

仅启用渲染不会自动产生白屏。请提供明亮图块和调色板,并为暗阶段设置暗色底色。示例使用纯白图块和黑色底色,使操作期间整个屏幕闪烁。

等待次数按 NMI 计数器变化计算。进入到首次 NMI 的间隔可能不足一帧。应从前台代码调用,不要从中断调用。其他例程改变渲染掩码可能扰乱模式。

这些是低层显示掩码及等待原语。不生成机器人命令前导、设备专用命令序列或接收确认。验证覆盖 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 对应四个白格再接两个黑格;一位 0 对应两个白格再接四个黑格。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 对应四个白格再接两个黑格;一位 0 对应两个白格再接四个黑格。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 对应四个白格再接两个黑格;一位 0 对应两个白格再接四个黑格。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

先执行指定的亮阶段等待,再执行暗阶段等待,产生一个脉冲。

语法

c
void __rob_pulse(u8 on_frames, u8 off_frames);

参数

on_frames

亮阶段等待次数,范围为 0..255。零跳过亮阶段。

off_frames

暗阶段等待次数,范围为 0..255。零跳过暗阶段。合计最多 510 次等待。

返回值

无返回值。完成指定等待后返回调用方。

使用条件与限制

两个次数都为零时,不改变掩码或时序。只要有暗阶段等待,最终掩码为零;只有亮阶段的脉冲最终为 0x1E。

包含 intrinsics.h。默认 NMI 处理程序必须运行并推进其计数器。禁用 NMI 或自定义处理程序不维护该计数器时,等待会一直阻塞。没有超时。

亮阶段将 PPUMASK 及其软件影子值设为 0x1E;暗阶段均设为零。不恢复先前掩码。这会同时改变背景、精灵和左缘渲染,也会替换灰度及颜色强调设置。

仅启用渲染不会自动产生白屏。请提供明亮图块和调色板,并为暗阶段设置暗色底色。示例使用纯白图块和黑色底色,使操作期间整个屏幕闪烁。

等待次数按 NMI 计数器变化计算。进入到首次 NMI 的间隔可能不足一帧。应从前台代码调用,不要从中断调用。其他例程改变渲染掩码可能扰乱模式。

这些是低层显示掩码及等待原语。不生成机器人命令前导、设备专用命令序列或接收确认。验证覆盖 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 对应四个白格再接两个黑格;一位 0 对应两个白格再接四个黑格。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 对应四个白格再接两个黑格;一位 0 对应两个白格再接四个黑格。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 对应四个白格再接两个黑格;一位 0 对应两个白格再接四个黑格。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 使用四次亮等待和两次暗等待;0 使用两次亮等待和四次暗等待。总计 48 次 NMI 等待。

语法

c
void __rob_send_byte(u8 pattern);

参数

pattern

八位模式,范围为 0..255。0xA5 的顺序为 1,0,1,0,0,1,0,1。不是通道 ID 或机器人动作编号。

返回值

无返回值。完成指定等待后返回调用方。

使用条件与限制

最后一位也以暗阶段等待结束,使 PPUMASK 及其影子值为零。恰好发送八位,不添加起始或结束标记。

包含 intrinsics.h。默认 NMI 处理程序必须运行并推进其计数器。禁用 NMI 或自定义处理程序不维护该计数器时,等待会一直阻塞。没有超时。

亮阶段将 PPUMASK 及其软件影子值设为 0x1E;暗阶段均设为零。不恢复先前掩码。这会同时改变背景、精灵和左缘渲染,也会替换灰度及颜色强调设置。

仅启用渲染不会自动产生白屏。请提供明亮图块和调色板,并为暗阶段设置暗色底色。示例使用纯白图块和黑色底色,使操作期间整个屏幕闪烁。

等待次数按 NMI 计数器变化计算。进入到首次 NMI 的间隔可能不足一帧。应从前台代码调用,不要从中断调用。其他例程改变渲染掩码可能扰乱模式。

这些是低层显示掩码及等待原语。不生成机器人命令前导、设备专用命令序列或接收确认。验证覆盖 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 对应四个白格再接两个黑格;一位 0 对应两个白格再接四个黑格。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 对应四个白格再接两个黑格;一位 0 对应两个白格再接四个黑格。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 对应四个白格再接两个黑格;一位 0 对应两个白格再接四个黑格。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 标志,但不会将滚动坐标写为零,也不改变保存的 X/Y 值。

语法

c
void __scroll_latch_reset(void);

返回值

无(void)。

使用条件与限制

读取 PPUSTATUS 可能影响 NMI 时序,以及后续对 PPUSCROLL 和 PPUADDR 的写入。不要把它插入其他例程尚未写完的寄存器字节对之间。这些调用不会等待 VBlank,也不会完成待处理传输队列。

NESdev:PPU 寄存器

NESdev:PPU 滚动

使用示例

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 竖条和八行三角形,第二行只有左半竖条。(16,48) 处是绿色实心 64×32 矩形,(144,48) 处的红色 64×32 区域由横八、纵四个三角形图块组成。

y=96 处,x=16、24、32 的蓝色标记分别为正方形、左半竖条和三角形;x=80 为红色左半竖条,x=144 为绿色三角形。这些是背景图块。使用独立调色板的精灵位于 y=144:x=16 是红色 8×8 正方形,x=80 是蓝色左半 4×8 竖条,x=144 是绿色八行三角形。最终滚动为 (0,0)。

图像检查 16,384 个像素的预期形状和颜色,包括精灵颜色。另有 31 个状态测试,覆盖寄存器替换、状态与锁存器副作用、滚动保存值、0/255 字节传输、增量 1/32、地图边缘、矩形零尺寸、表选择、完整属性字节及两种调色板目标。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 复位共享锁存器,再按 X、Y 顺序写入 PPUSCROLL。它直接更新滚动,不会排队执行相机移动、等待 NMI 或选择基础名称表。

语法

c
void __scroll_set(u8 x, u8 y);

参数

x / y

偏移是无符号字节的像素值。X 使用 0..255,Y 通常使用 0..239。Y 的 240..255 也会原样写入,但会触发 PPU 特殊行行为,并非普通可见地图滚动。基础名称表位由 PPUCTRL 提供。直接访问 VRAM 后、开始渲染前,应恢复所需的滚动;函数不会替你安排时序。

返回值

无(void)。

使用条件与限制

读取 PPUSTATUS 可能影响 NMI 时序,以及后续对 PPUSCROLL 和 PPUADDR 的写入。不要把它插入其他例程尚未写完的寄存器字节对之间。这些调用不会等待 VBlank,也不会完成待处理传输队列。

NESdev:PPU 寄存器

NESdev:PPU 滚动

使用示例

c
__scroll_set(19,27); // Save both coordinates and write the two-scroll-byte pair.

这是一段调用示例。放入完整程序前,请满足所述初始化和缓冲区要求。

预期结果

此程序用于学习寄存器设置、图块与属性直接写入、滚动,以及背景和精灵调色板的分别加载。(0,16) 处蓝色条带为 256×16 像素:首行重复实心 8×8 图块、左半 4×8 竖条和八行三角形,第二行只有左半竖条。(16,48) 处是绿色实心 64×32 矩形,(144,48) 处的红色 64×32 区域由横八、纵四个三角形图块组成。

y=96 处,x=16、24、32 的蓝色标记分别为正方形、左半竖条和三角形;x=80 为红色左半竖条,x=144 为绿色三角形。这些是背景图块。使用独立调色板的精灵位于 y=144:x=16 是红色 8×8 正方形,x=80 是蓝色左半 4×8 竖条,x=144 是绿色八行三角形。最终滚动为 (0,0)。

图像检查 16,384 个像素的预期形状和颜色,包括精灵颜色。另有 31 个状态测试,覆盖寄存器替换、状态与锁存器副作用、滚动保存值、0/255 字节传输、增量 1/32、地图边缘、矩形零尺寸、表选择、完整属性字节及两种调色板目标。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

偏移是无符号字节的像素值。X 使用 0..255,Y 通常使用 0..239。Y 的 240..255 也会原样写入,但会触发 PPU 特殊行行为,并非普通可见地图滚动。基础名称表位由 PPUCTRL 提供。直接访问 VRAM 后、开始渲染前,应恢复所需的滚动;函数不会替你安排时序。

返回值

无(void)。

使用条件与限制

读取 PPUSTATUS 可能影响 NMI 时序,以及后续对 PPUSCROLL 和 PPUADDR 的写入。不要把它插入其他例程尚未写完的寄存器字节对之间。这些调用不会等待 VBlank,也不会完成待处理传输队列。

NESdev:PPU 寄存器

NESdev:PPU 滚动

使用示例

c
__scroll_x_set(0); // Keeps saved Y=27 while changing X to zero.

这是一段调用示例。放入完整程序前,请满足所述初始化和缓冲区要求。

预期结果

此程序用于学习寄存器设置、图块与属性直接写入、滚动,以及背景和精灵调色板的分别加载。(0,16) 处蓝色条带为 256×16 像素:首行重复实心 8×8 图块、左半 4×8 竖条和八行三角形,第二行只有左半竖条。(16,48) 处是绿色实心 64×32 矩形,(144,48) 处的红色 64×32 区域由横八、纵四个三角形图块组成。

y=96 处,x=16、24、32 的蓝色标记分别为正方形、左半竖条和三角形;x=80 为红色左半竖条,x=144 为绿色三角形。这些是背景图块。使用独立调色板的精灵位于 y=144:x=16 是红色 8×8 正方形,x=80 是蓝色左半 4×8 竖条,x=144 是绿色八行三角形。最终滚动为 (0,0)。

图像检查 16,384 个像素的预期形状和颜色,包括精灵颜色。另有 31 个状态测试,覆盖寄存器替换、状态与锁存器副作用、滚动保存值、0/255 字节传输、增量 1/32、地图边缘、矩形零尺寸、表选择、完整属性字节及两种调色板目标。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,也会写入两个滚动字节。X 使用运行时保存值。

语法

c
void __scroll_y_set(u8 y);

参数

y

偏移是无符号字节的像素值。X 使用 0..255,Y 通常使用 0..239。Y 的 240..255 也会原样写入,但会触发 PPU 特殊行行为,并非普通可见地图滚动。基础名称表位由 PPUCTRL 提供。直接访问 VRAM 后、开始渲染前,应恢复所需的滚动;函数不会替你安排时序。

返回值

无(void)。

使用条件与限制

读取 PPUSTATUS 可能影响 NMI 时序,以及后续对 PPUSCROLL 和 PPUADDR 的写入。不要把它插入其他例程尚未写完的寄存器字节对之间。这些调用不会等待 VBlank,也不会完成待处理传输队列。

NESdev:PPU 寄存器

NESdev:PPU 滚动

使用示例

c
__scroll_y_set(0); // Keeps saved X=0; the scene is now at the origin.

这是一段调用示例。放入完整程序前,请满足所述初始化和缓冲区要求。

预期结果

此程序用于学习寄存器设置、图块与属性直接写入、滚动,以及背景和精灵调色板的分别加载。(0,16) 处蓝色条带为 256×16 像素:首行重复实心 8×8 图块、左半 4×8 竖条和八行三角形,第二行只有左半竖条。(16,48) 处是绿色实心 64×32 矩形,(144,48) 处的红色 64×32 区域由横八、纵四个三角形图块组成。

y=96 处,x=16、24、32 的蓝色标记分别为正方形、左半竖条和三角形;x=80 为红色左半竖条,x=144 为绿色三角形。这些是背景图块。使用独立调色板的精灵位于 y=144:x=16 是红色 8×8 正方形,x=80 是蓝色左半 4×8 竖条,x=144 是绿色八行三角形。最终滚动为 (0,0)。

图像检查 16,384 个像素的预期形状和颜色,包括精灵颜色。另有 31 个状态测试,覆盖寄存器替换、状态与锁存器副作用、滚动保存值、0/255 字节传输、增量 1/32、地图边缘、矩形零尺寸、表选择、完整属性字节及两种调色板目标。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

计算有符号两项点积。

语法

c
u16 __sdot2_q1_7(u16 a0, u16 a1, u8 b0, u8 b1);

参数

a0

有符号 16 位位模式,表示 −32768..32767。

a1

有符号 16 位位模式,表示 −32768..32767。

b0

有符号字节位模式,表示系数 −128..127。

b1

有符号字节位模式,表示系数 −128..127。

返回值

返回 a0*b0+a1*b1 的低 16 位。

使用条件与限制

在 FC 上,名称含 q8_8/q1_7 的函数仍使用原始整数乘积,不执行定点缩放的右移。Q1.7 函数是对应有符号运算的别名。单位由调用方处理;有符号结果以 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 位值显示;负数采用二进制补码位模式。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

计算有符号两项点积。

语法

c
u16 __sdot2_q8_8(u16 a0, u16 a1, u8 b0, u8 b1);

参数

a0

有符号 16 位位模式,表示 −32768..32767。

a1

有符号 16 位位模式,表示 −32768..32767。

b0

有符号字节位模式,表示系数 −128..127。

b1

有符号字节位模式,表示系数 −128..127。

返回值

返回 a0*b0+a1*b1 的低 16 位。

使用条件与限制

在 FC 上,名称含 q8_8/q1_7 的函数仍使用原始整数乘积,不执行定点缩放的右移。Q1.7 函数是对应有符号运算的别名。单位由调用方处理;有符号结果以 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 位值显示;负数采用二进制补码位模式。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

计算有符号三项点积。

语法

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

有符号字节位模式,表示系数 −128..127。

b1

有符号字节位模式,表示系数 −128..127。

b2

有符号字节位模式,表示系数 −128..127。

返回值

返回 a0*b0+a1*b1+a2*b2 的低 16 位。

使用条件与限制

在 FC 上,名称含 q8_8/q1_7 的函数仍使用原始整数乘积,不执行定点缩放的右移。Q1.7 函数是对应有符号运算的别名。单位由调用方处理;有符号结果以 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 位值显示;负数采用二进制补码位模式。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

计算有符号三项点积。

语法

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

有符号字节位模式,表示系数 −128..127。

b1

有符号字节位模式,表示系数 −128..127。

b2

有符号字节位模式,表示系数 −128..127。

返回值

返回 a0*b0+a1*b1+a2*b2 的低 16 位。

使用条件与限制

在 FC 上,名称含 q8_8/q1_7 的函数仍使用原始整数乘积,不执行定点缩放的右移。Q1.7 函数是对应有符号运算的别名。单位由调用方处理;有符号结果以 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 位值显示;负数采用二进制补码位模式。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,否则返回 0。这不是数据就绪或通信错误状态。

使用条件与限制

这些 API 直接访问 $4016/$4017。不要在同一端口交错执行中断驱动的手柄、键盘或串行操作;控制器移位位置也可能受影响。示例在采集输入时关闭 NMI,再在初始化显示时启用。

这些低层原语假定外部适配器将 OUT0 映射为 TX、$4017 D4 映射为 RX。不提供位间隔、波特率时钟、起止位、字节组装或接收等待。其用途不同于 __midi_out() 等按字节操作的辅助函数。

调用方须在合适时刻采样。既不等待数据,也不组装多位,且不改变 $4016 输出。

位转换还通过自行编写的测试 ROM 进行单元测试,将寄存器读取重定向到 RAM 测试值。覆盖 00/08/10/18 组合,并设置无关位。该单元测试与原样运行示例及真实外设行为是分开的。

这些图像来自在 KUROSAKI 中原样运行可下载 ROM。当前输入总线模型支持手柄,但不模拟键盘、光枪或串行适配器信号。外部输入位读为零,因此截图不能验证这些设备。实体按键、光学感应和通信仍未经测试。

使用示例

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。此列解释最低位映射,不是电气读回。CPU 轨迹确认这些写入及最后一次写入一。RX SAMPLE 为 000,因为 KUROSAKI 不模拟接收线。

__serial_rx_bit: SERIAL-DEFAULT — VALUE 为 000/001/002/003/254/255 时,EXPECTED OUT0 分别为 0/1/0/1/0/1。此列解释最低位映射,不是电气读回。CPU 轨迹确认这些写入及最后一次写入一。RX SAMPLE 为 000,因为 KUROSAKI 不模拟接收线。
SERIAL-DEFAULT — VALUE 为 000/001/002/003/254/255 时,EXPECTED OUT0 分别为 0/1/0/1/0/1。此列解释最低位映射,不是电气读回。CPU 轨迹确认这些写入及最后一次写入一。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

通过 $4016 将参数最低位写到 OUT0,为外部串行适配器提供单比特输出操作。

语法

c
void __serial_tx_bit(u8 bit);

参数

bit

输出值。只使用 bit & 1:2 输出零,255 输出一。

返回值

无返回值。

使用条件与限制

这些 API 直接访问 $4016/$4017。不要在同一端口交错执行中断驱动的手柄、键盘或串行操作;控制器移位位置也可能受影响。示例在采集输入时关闭 NMI,再在初始化显示时启用。

这些低层原语假定外部适配器将 OUT0 映射为 TX、$4017 D4 映射为 RX。不提供位间隔、波特率时钟、起止位、字节组装或接收等待。其用途不同于 __midi_out() 等按字节操作的辅助函数。

向整个 $4016 写入零或一,清除 OUT1/OUT2。不插入保持延迟。OUT0 控制普通手柄锁存,因此此操作也改变手柄读取状态。

这些图像来自在 KUROSAKI 中原样运行可下载 ROM。当前输入总线模型支持手柄,但不模拟键盘、光枪或串行适配器信号。外部输入位读为零,因此截图不能验证这些设备。实体按键、光学感应和通信仍未经测试。

使用示例

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。此列解释最低位映射,不是电气读回。CPU 轨迹确认这些写入及最后一次写入一。RX SAMPLE 为 000,因为 KUROSAKI 不模拟接收线。

__serial_tx_bit: SERIAL-DEFAULT — VALUE 为 000/001/002/003/254/255 时,EXPECTED OUT0 分别为 0/1/0/1/0/1。此列解释最低位映射,不是电气读回。CPU 轨迹确认这些写入及最后一次写入一。RX SAMPLE 为 000,因为 KUROSAKI 不模拟接收线。
SERIAL-DEFAULT — VALUE 为 000/001/002/003/254/255 时,EXPECTED OUT0 分别为 0/1/0/1/0/1。此列解释最低位映射,不是电气读回。CPU 轨迹确认这些写入及最后一次写入一。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

有符号字节位模式,表示系数 −128..127。

返回值

返回 acc+a*b 的低 16 位。

使用条件与限制

在 FC 上,名称含 q8_8/q1_7 的函数仍使用原始整数乘积,不执行定点缩放的右移。Q1.7 函数是对应有符号运算的别名。单位由调用方处理;有符号结果以 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 位值显示;负数采用二进制补码位模式。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

有符号字节位模式,表示系数 −128..127。

返回值

返回 acc+a*b 的低 16 位。

使用条件与限制

在 FC 上,名称含 q8_8/q1_7 的函数仍使用原始整数乘积,不执行定点缩放的右移。Q1.7 函数是对应有符号运算的别名。单位由调用方处理;有符号结果以 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 位值显示;负数采用二进制补码位模式。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

有符号字节位模式,表示系数 −128..127。

返回值

返回 a*b 的低 16 位。

使用条件与限制

在 FC 上,名称含 q8_8/q1_7 的函数仍使用原始整数乘积,不执行定点缩放的右移。Q1.7 函数是对应有符号运算的别名。单位由调用方处理;有符号结果以 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 位值显示;负数采用二进制补码位模式。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

有符号字节位模式,表示系数 −128..127。

返回值

返回 a*b 的低 16 位。

使用条件与限制

在 FC 上,名称含 q8_8/q1_7 的函数仍使用原始整数乘积,不执行定点缩放的右移。Q1.7 函数是对应有符号运算的别名。单位由调用方处理;有符号结果以 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 位值显示;负数采用二进制补码位模式。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

等待新的精灵零命中,然后写入滚动 X/Y 以实现水平分割,使顶部保持对齐、下部背景移位。

语法

c
void __split_scroll_sprite0(u8 x, u8 y);

参数

x

水平滚动值,范围为 0..255,不是对象的屏幕 X 坐标。示例用 32 将同一背景的下半部分向左移 32 像素。

y

临时垂直滚动设置,范围为 0..255,通常可见范围为 0..239。本辅助函数写 $2005,因此不会在渲染期间立即改变当前垂直位置。水平分割示例传入零。

返回值

无返回值。

使用条件与限制

让 OAM 精灵 0 的不透明像素与背景不透明像素重叠,并启用两层。置于背景后方的优先级标志仍允许命中。最左八像素要求相应左缘层使能;X=255 处重叠不能触发命中。

轮询 PPUSTATUS ($2002) 第 6 位,先等其清零,再等其置位。调用时若已命中,会等待下一帧的新命中。读取不确认或清除该标志;PPU 在预渲染期间清除。

没有超时或失败返回。精灵 0 隐藏、背景透明、渲染关闭或其他无重叠配置都会使调用阻塞。调用前须保证命中。不要求 CPU IRQ 使能,也不会启用 NMI。示例为独立的帧等待启用了 NMI。

读取 $2002 还影响共享 PPU 地址/滚动写入锁存器及 VBlank 标志。不要把需要两次写入的 $2005/$2006 操作分放在本调用前后。请从前台使用,不要从 NMI/IRQ 处理程序调用。

命中后,读取 $2002,再向 $2005 依次写 X 和 Y。精细 X 立即改变;粗略 X 在下一次水平重载生效。Y 通常在下一次预渲染重载生效,除非先被其他滚动写入替换。这不是立即任意重定位 X/Y 的光栅操作。

轮询及指令执行会延迟写入。KUROSAKI 按扫描行渲染,因此截图不能证明硬件上精确到像素时钟的边界。示例将变化放在 Y=96..103 的白色带内部,使边界不确定性远离蓝色和红色条形。

共享头文件 fc_sprite0_scene.h 准备原创蓝/红条形及白色横带。精灵 0 位于屏幕 (16,96),藏在背景后方;原始 OAM Y 为 95,因为显示 Y=OAM Y+1。每帧在 VBlank 期间先将顶部滚动恢复为零,再等待命中。

NESdev:PPU 滚动

NESdev:PPU 编程参考

使用示例

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。验证检查每帧完成一次,以及每个图像像素的位置和颜色。

__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。验证检查每帧完成一次,以及每个图像像素的位置和颜色。
SPLIT-DEFAULT — 白色带上方,蓝条位于 X=64..95,红条位于 X=160..191。其下方从 Y=104 起,蓝条移至 X=32..63,红条移至 X=128..159:只有下部左移 32 像素。横带位于 Y=96..103,条形延伸至 Y=207。验证检查每帧完成一次,以及每个图像像素的位置和颜色。

完整示例程序

构建命令

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

等待新的精灵零/背景重叠,使前台工作与已知屏幕区域同步,不改变滚动值。

语法

c
void __sprite0_wait_hit(void);

返回值

无返回值。

使用条件与限制

让 OAM 精灵 0 的不透明像素与背景不透明像素重叠,并启用两层。置于背景后方的优先级标志仍允许命中。最左八像素要求相应左缘层使能;X=255 处重叠不能触发命中。

轮询 PPUSTATUS ($2002) 第 6 位,先等其清零,再等其置位。调用时若已命中,会等待下一帧的新命中。读取不确认或清除该标志;PPU 在预渲染期间清除。

没有超时或失败返回。精灵 0 隐藏、背景透明、渲染关闭或其他无重叠配置都会使调用阻塞。调用前须保证命中。不要求 CPU IRQ 使能,也不会启用 NMI。示例为独立的帧等待启用了 NMI。

读取 $2002 还影响共享 PPU 地址/滚动写入锁存器及 VBlank 标志。不要把需要两次写入的 $2005/$2006 操作分放在本调用前后。请从前台使用,不要从 NMI/IRQ 处理程序调用。

轮询及指令执行会延迟写入。KUROSAKI 按扫描行渲染,因此截图不能证明硬件上精确到像素时钟的边界。示例将变化放在 Y=96..103 的白色带内部,使边界不确定性远离蓝色和红色条形。

共享头文件 fc_sprite0_scene.h 准备原创蓝/红条形及白色横带。精灵 0 位于屏幕 (16,96),藏在背景后方;原始 OAM Y 为 95,因为显示 Y=OAM Y+1。每帧在 VBlank 期间先将顶部滚动恢复为零,再等待命中。

NESdev:PPU 滚动

NESdev:PPU 编程参考

使用示例

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

替换一个影子精灵的完整属性字节,可在不改变图块或位置的情况下设置调色板、背景优先级以及水平或垂直翻转。

语法

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)显示一对箭头,第二个水平翻转。NEXT 07表示写完槽位5和6后的下一位置。箭头边缘为红色、箭杆为绿色,尖端细节为蓝色。验证会逐图比较8,704个像素并检查传送后的OAM字节;尚未进行实机测试。

TRANSFER在(144,128)显示箭头。程序用__oam_dma直接传送第02页,其中包含各项精灵操作准备好的全部字段。

__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

将一个影子精灵的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)显示一对箭头,第二个水平翻转。NEXT 07表示写完槽位5和6后的下一位置。箭头边缘为红色、箭杆为绿色,尖端细节为蓝色。验证会逐图比较8,704个像素并检查传送后的OAM字节;尚未进行实机测试。

TRANSFER在(144,128)显示箭头。程序用__oam_dma直接传送第02页,其中包含各项精灵操作准备好的全部字段。

__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

只修改一个影子精灵的X和Y字节,保留图块编号与属性。因此可在不改变调色板或翻转状态的情况下移动同一图形。

语法

c
void __sprite_move(u8 index, u8 x, u8 y);

参数

index

精灵条目编号,范围0–63,不是字节偏移。实现使用8位游标将其乘以4;越界值会按64取模,可能覆盖其他条目。不会检查条目是否已分配。

x

以像素为单位的横向位置,使用无符号字节表示,不是图块坐标。

y

直接写入 OAM 的 Y 字节。要把顶部放在第 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)显示一对箭头,第二个水平翻转。NEXT 07表示写完槽位5和6后的下一位置。箭头边缘为红色、箭杆为绿色,尖端细节为蓝色。验证会逐图比较8,704个像素并检查传送后的OAM字节;尚未进行实机测试。

TRANSFER在(144,128)显示箭头。程序用__oam_dma直接传送第02页,其中包含各项精灵操作准备好的全部字段。

__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

将一个精灵的全部四个字段写入第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

以像素为单位的横向位置,使用无符号字节表示,不是图块坐标。

y

直接写入 OAM 的 Y 字节。要把顶部放在第 1 条扫描线或更下方,应传入顶部坐标减一;239..255 位于 240 行可见区域之外。

tile

8 位图块编号。须先加载图形。8×16 精灵仍遵循硬件规则,函数不会自动调整成成对图块编号。

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)显示一对箭头,第二个水平翻转。NEXT 07表示写完槽位5和6后的下一位置。箭头边缘为红色、箭杆为绿色,尖端细节为蓝色。验证会逐图比较8,704个像素并检查传送后的OAM字节;尚未进行实机测试。

TRANSFER在(144,128)显示箭头。程序用__oam_dma直接传送第02页,其中包含各项精灵操作准备好的全部字段。

__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

只替换一个影子精灵的图块编号字节,保留位置与属性。此操作选择已有图案,不会向CHR内存上传图像。

语法

c
void __sprite_tile(u8 index, u8 tile);

参数

index

精灵条目编号,范围0–63,不是字节偏移。实现使用8位游标将其乘以4;越界值会按64取模,可能覆盖其他条目。不会检查条目是否已分配。

tile

8 位图块编号。须先加载图形。8×16 精灵仍遵循硬件规则,函数不会自动调整成成对图块编号。

返回值

无(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)显示一对箭头,第二个水平翻转。NEXT 07表示写完槽位5和6后的下一位置。箭头边缘为红色、箭杆为绿色,尖端细节为蓝色。验证会逐图比较8,704个像素并检查传送后的OAM字节;尚未进行实机测试。

TRANSFER在(144,128)显示箭头。程序用__oam_dma直接传送第02页,其中包含各项精灵操作准备好的全部字段。

__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 后,将同一个字节值 value 向 PPUDATA 写入 len 次。这是返回前完成的直接填充操作,不会解释图块尺寸、属性或矩形边界。

语法

c
void __vram_fill(u16 ppu_addr, u8 value, u8 len);

参数

ppu_addr

PPU字节地址(0x0000–0x3FFF),不是CPU指针。目标必须可写;改写图案需要CHR RAM,不能写入CHR ROM。PPU映射与镜像规则照常适用。连续复制和填充应将PPUCTRL地址增量设为1。在安全的PPU写入时段执行,然后恢复所需的滚动状态;执行器会写入PPUADDR和PPUDATA,不会恢复滚动设置。

value

重复写入0至255的单字节。显示含义取决于目标是图块索引、像素位平面还是其他字节结构。

len

字节数 0..255。0 仍会设置 PPU 地址并复位锁存器,但不写数据。不检查目标容量、不裁剪、不切换存储体,也不延后传输。

返回值

无(void)。

使用条件与限制

直接访问 PPU 时,必须关闭渲染,或确保整个操作处于安全的 VBlank 时间内。这些函数不等待、不排队、不屏蔽 NMI,也不恢复滚动。调用期间应防止中断改变共享锁存器或地址。PPUCTRL 第 2 位决定每个数据字节使目标地址增加 1 还是 32。镜像和地址回绕由 PPU 处理;CHR-ROM 不可写。访问结束后、开始渲染前,请设置所需的滚动状态。

NESdev:PPU 寄存器

NESdev:PPU 滚动

使用示例

c
__vram_fill(0x2060,2,32); // A second blue row of 4-pixel-wide left halves.

这是一段调用示例。放入完整程序前,请满足所述初始化和缓冲区要求。

预期结果

此程序用于学习寄存器设置、图块与属性直接写入、滚动,以及背景和精灵调色板的分别加载。(0,16) 处蓝色条带为 256×16 像素:首行重复实心 8×8 图块、左半 4×8 竖条和八行三角形,第二行只有左半竖条。(16,48) 处是绿色实心 64×32 矩形,(144,48) 处的红色 64×32 区域由横八、纵四个三角形图块组成。

y=96 处,x=16、24、32 的蓝色标记分别为正方形、左半竖条和三角形;x=80 为红色左半竖条,x=144 为绿色三角形。这些是背景图块。使用独立调色板的精灵位于 y=144:x=16 是红色 8×8 正方形,x=80 是蓝色左半 4×8 竖条,x=144 是绿色八行三角形。最终滚动为 (0,0)。

图像检查 16,384 个像素的预期形状和颜色,包括精灵颜色。另有 31 个状态测试,覆盖寄存器替换、状态与锁存器副作用、滚动保存值、0/255 字节传输、增量 1/32、地图边缘、矩形零尺寸、表选择、完整属性字节及两种调色板目标。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。计数为八位,编译器使用索引读取源数据。不为稍后执行保留指针,返回后修改源数据不会改变 VRAM。

语法

c
void __vram_write(u16 ppu_addr, const u8* src, u8 len);

参数

ppu_addr

PPU字节地址(0x0000–0x3FFF),不是CPU指针。目标必须可写;改写图案需要CHR RAM,不能写入CHR ROM。PPU映射与镜像规则照常适用。连续复制和填充应将PPUCTRL地址增量设为1。在安全的PPU写入时段执行,然后恢复所需的滚动状态;执行器会写入PPUADDR和PPUDATA,不会恢复滚动设置。

src

CPU 指针,必须能读取至少指定数量的字节。同步传输返回前,请保持源数据映射和内容稳定。函数不会切换存储体、在队列中保留指针或检查源长度;计数为 0 时不会解引用指针。

len

字节数 0..255。0 仍会设置 PPU 地址并复位锁存器,但不写数据。不检查目标容量、不裁剪、不切换存储体,也不延后传输。

返回值

无(void)。

使用条件与限制

直接访问 PPU 时,必须关闭渲染,或确保整个操作处于安全的 VBlank 时间内。这些函数不等待、不排队、不屏蔽 NMI,也不恢复滚动。调用期间应防止中断改变共享锁存器或地址。PPUCTRL 第 2 位决定每个数据字节使目标地址增加 1 还是 32。镜像和地址回绕由 PPU 处理;CHR-ROM 不可写。访问结束后、开始渲染前,请设置所需的滚动状态。

NESdev:PPU 寄存器

NESdev:PPU 滚动

使用示例

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 竖条和八行三角形,第二行只有左半竖条。(16,48) 处是绿色实心 64×32 矩形,(144,48) 处的红色 64×32 区域由横八、纵四个三角形图块组成。

y=96 处,x=16、24、32 的蓝色标记分别为正方形、左半竖条和三角形;x=80 为红色左半竖条,x=144 为绿色三角形。这些是背景图块。使用独立调色板的精灵位于 y=144:x=16 是红色 8×8 正方形,x=80 是蓝色左半 4×8 竖条,x=144 是绿色八行三角形。最终滚动为 (0,0)。

图像检查 16,384 个像素的预期形状和颜色,包括精灵颜色。另有 31 个状态测试,覆盖寄存器替换、状态与锁存器副作用、滚动保存值、0/255 字节传输、增量 1/32、地图边缘、矩形零尺寸、表选择、完整属性字节及两种调色板目标。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);

返回值

以此处说明的平台单位表示的无符号八位值。读取不会修改或执行队列。

使用条件与限制

FC以固定128字节内置缓冲区中的编码命令字节数计量。单图块写入占4字节,填充记录占5字节,指针复制记录占6字节,均含元数据。就绪队列可能由NMI执行,应在提交前检查和构建,并协调生产与执行阶段。

这些内置函数操作的命令缓冲区与lib/vram.c相同。runtime.c中的nes_vram_queue_*管理另一个记录格式不同的队列。清除或查询其中一个,不会清除或测量另一个。

应协调队列构建与执行阶段。不要在中断执行队列时追加、直接修改管理变量或意外覆盖保留的源数据。这些API不提供锁或双缓冲。容量指命令缓冲区,不是硬件VRAM空闲字节数。

使用示例

c
capacity=__vramq_capacity();
if (capacity!=128) failures++;

这是一段调用示例。放入完整程序前,请满足所述初始化和缓冲区要求。

预期结果

完整程序演示队列容量、保留的指针、两种独立重置,以及直接执行与NMI执行的区别。预期显示CAPACITY 128、ENCODED BYTES 030、AFTER EXEC 000、OVERFLOW SEEN 001和FAILED CHECKS 000。6条记录占30字节,却执行512次字节写入。OVERFLOW SEEN保留之前故意触发拒绝时的值,不表示最终溢出标志仍置位。

蓝色复制区域从像素(0,16)开始,绿色填充区域从(0,88)开始。每块包含7个完整的256像素宽图块行,以及第8行的248像素,共255个图块。最后一个8×8单元保持黑色。复制重复实心、左半填充和三角形的8×8图案;因入队后修改了源数据,首图块应为三角形。填充在调用方变量改变后仍保持实心。测试同时检查形状和颜色。

在y=160处,x=16应为红色实心正方形,x=224为红色左半图块,x=240为红色实心正方形。第一个验证零长度复制和填充不会改写;中间验证默认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字节内置缓冲区中的编码命令字节数计量。单图块写入占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.

这是一段调用示例。放入完整程序前,请满足所述初始化和缓冲区要求。

预期结果

完整程序演示队列容量、保留的指针、两种独立重置,以及直接执行与NMI执行的区别。预期显示CAPACITY 128、ENCODED BYTES 030、AFTER EXEC 000、OVERFLOW SEEN 001和FAILED CHECKS 000。6条记录占30字节,却执行512次字节写入。OVERFLOW SEEN保留之前故意触发拒绝时的值,不表示最终溢出标志仍置位。

蓝色复制区域从像素(0,16)开始,绿色填充区域从(0,88)开始。每块包含7个完整的256像素宽图块行,以及第8行的248像素,共255个图块。最后一个8×8单元保持黑色。复制重复实心、左半填充和三角形的8×8图案;因入队后修改了源数据,首图块应为三角形。填充在调用方变量改变后仍保持实心。测试同时检查形状和颜色。

在y=160处,x=16应为红色实心正方形,x=224为红色左半图块,x=240为红色实心正方形。第一个验证零长度复制和填充不会改写;中间验证默认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字节内置缓冲区中的编码命令字节数计量。单图块写入占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.

这是一段调用示例。放入完整程序前,请满足所述初始化和缓冲区要求。

预期结果

完整程序演示队列容量、保留的指针、两种独立重置,以及直接执行与NMI执行的区别。预期显示CAPACITY 128、ENCODED BYTES 030、AFTER EXEC 000、OVERFLOW SEEN 001和FAILED CHECKS 000。6条记录占30字节,却执行512次字节写入。OVERFLOW SEEN保留之前故意触发拒绝时的值,不表示最终溢出标志仍置位。

蓝色复制区域从像素(0,16)开始,绿色填充区域从(0,88)开始。每块包含7个完整的256像素宽图块行,以及第8行的248像素,共255个图块。最后一个8×8单元保持黑色。复制重复实心、左半填充和三角形的8×8图案;因入队后修改了源数据,首图块应为三角形。填充在调用方变量改变后仍保持实心。测试同时检查形状和颜色。

在y=160处,x=16应为红色实心正方形,x=224为红色左半图块,x=240为红色实心正方形。第一个验证零长度复制和填充不会改写;中间验证默认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字节内置缓冲区中的编码命令字节数计量。单图块写入占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++;

这是一段调用示例。放入完整程序前,请满足所述初始化和缓冲区要求。

预期结果

完整程序演示队列容量、保留的指针、两种独立重置,以及直接执行与NMI执行的区别。预期显示CAPACITY 128、ENCODED BYTES 030、AFTER EXEC 000、OVERFLOW SEEN 001和FAILED CHECKS 000。6条记录占30字节,却执行512次字节写入。OVERFLOW SEEN保留之前故意触发拒绝时的值,不表示最终溢出标志仍置位。

蓝色复制区域从像素(0,16)开始,绿色填充区域从(0,88)开始。每块包含7个完整的256像素宽图块行,以及第8行的248像素,共255个图块。最后一个8×8单元保持黑色。复制重复实心、左半填充和三角形的8×8图案;因入队后修改了源数据,首图块应为三角形。填充在调用方变量改变后仍保持实心。测试同时检查形状和颜色。

在y=160处,x=16应为红色实心正方形,x=224为红色左半图块,x=240为红色实心正方形。第一个验证零长度复制和填充不会改写;中间验证默认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

追加一条6字节记录,在执行时将src的len字节复制到ppu_addr。队列保留源指针而非数据本身。即使复制255字节,也只占6字节命令缓冲区。源数据按地址递增顺序读取;目标连续写入需选择PPU增量1。

语法

c
void __vramq_copy(u16 ppu_addr, const u8* src, u8 len);

参数

ppu_addr

PPU字节地址(0x0000–0x3FFF),不是CPU指针。目标必须可写;改写图案需要CHR RAM,不能写入CHR ROM。PPU映射与镜像规则照常适用。连续复制和填充应将PPUCTRL地址增量设为1。在安全的PPU写入时段执行,然后恢复所需的滚动状态;执行器会写入PPUADDR和PPUDATA,不会恢复滚动设置。

src

指向可读字节的近指针,保留到执行时。整个执行期间必须保持数据有效且其内存或ROM银行可访问;不保存银行号或数据副本。非空传输需要有效源数据,不可使用生命周期已结束的局部数组。

len

字节数,范围0至255。一条记录处理全部长度,不拆成127字节的块。0仍占用复制或填充记录;执行时会设置PPU地址,但不向PPUDATA写入字节。256超出此参数范围。

返回值

无(void)。

使用条件与限制

追加函数不返回成功值。完整记录放不下时,将__vramq_overflow()置1,保留原有记录和长度。空间恰好足够时可以加入。旧溢出标志不会阻止能放下的新记录,因此判断新批次前应先处理或清除标志。连续多次追加不构成事务。

FC以固定128字节内置缓冲区中的编码命令字节数计量。单图块写入占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.

这是一段调用示例。放入完整程序前,请满足所述初始化和缓冲区要求。

预期结果

完整程序演示队列容量、保留的指针、两种独立重置,以及直接执行与NMI执行的区别。预期显示CAPACITY 128、ENCODED BYTES 030、AFTER EXEC 000、OVERFLOW SEEN 001和FAILED CHECKS 000。6条记录占30字节,却执行512次字节写入。OVERFLOW SEEN保留之前故意触发拒绝时的值,不表示最终溢出标志仍置位。

蓝色复制区域从像素(0,16)开始,绿色填充区域从(0,88)开始。每块包含7个完整的256像素宽图块行,以及第8行的248像素,共255个图块。最后一个8×8单元保持黑色。复制重复实心、左半填充和三角形的8×8图案;因入队后修改了源数据,首图块应为三角形。填充在调用方变量改变后仍保持实心。测试同时检查形状和颜色。

在y=160处,x=16应为红色实心正方形,x=224为红色左半图块,x=240为红色实心正方形。第一个验证零长度复制和填充不会改写;中间验证默认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)。

使用条件与限制

PPU字节地址(0x0000–0x3FFF),不是CPU指针。目标必须可写;改写图案需要CHR RAM,不能写入CHR ROM。PPU映射与镜像规则照常适用。连续复制和填充应将PPUCTRL地址增量设为1。在安全的PPU写入时段执行,然后恢复所需的滚动状态;执行器会写入PPUADDR和PPUDATA,不会恢复滚动设置。

FC以固定128字节内置缓冲区中的编码命令字节数计量。单图块写入占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++;

这是一段调用示例。放入完整程序前,请满足所述初始化和缓冲区要求。

预期结果

完整程序演示队列容量、保留的指针、两种独立重置,以及直接执行与NMI执行的区别。预期显示CAPACITY 128、ENCODED BYTES 030、AFTER EXEC 000、OVERFLOW SEEN 001和FAILED CHECKS 000。6条记录占30字节,却执行512次字节写入。OVERFLOW SEEN保留之前故意触发拒绝时的值,不表示最终溢出标志仍置位。

蓝色复制区域从像素(0,16)开始,绿色填充区域从(0,88)开始。每块包含7个完整的256像素宽图块行,以及第8行的248像素,共255个图块。最后一个8×8单元保持黑色。复制重复实心、左半填充和三角形的8×8图案;因入队后修改了源数据,首图块应为三角形。填充在调用方变量改变后仍保持实心。测试同时检查形状和颜色。

在y=160处,x=16应为红色实心正方形,x=224为红色左半图块,x=240为红色实心正方形。第一个验证零长度复制和填充不会改写;中间验证默认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

追加一条5字节记录,将单字节value向PPU重复写入len次。值存储在记录中,之后修改调用方变量不会改变已入队的填充值。重复的是单字节,不是多字节图块图案。

语法

c
void __vramq_fill(u16 ppu_addr, u8 value, u8 len);

参数

ppu_addr

PPU字节地址(0x0000–0x3FFF),不是CPU指针。目标必须可写;改写图案需要CHR RAM,不能写入CHR ROM。PPU映射与镜像规则照常适用。连续复制和填充应将PPUCTRL地址增量设为1。在安全的PPU写入时段执行,然后恢复所需的滚动状态;执行器会写入PPUADDR和PPUDATA,不会恢复滚动设置。

value

重复写入0至255的单字节。显示含义取决于目标是图块索引、像素位平面还是其他字节结构。

len

字节数,范围0至255。一条记录处理全部长度,不拆成127字节的块。0仍占用复制或填充记录;执行时会设置PPU地址,但不向PPUDATA写入字节。256超出此参数范围。

返回值

无(void)。

使用条件与限制

追加函数不返回成功值。完整记录放不下时,将__vramq_overflow()置1,保留原有记录和长度。空间恰好足够时可以加入。旧溢出标志不会阻止能放下的新记录,因此判断新批次前应先处理或清除标志。连续多次追加不构成事务。

FC以固定128字节内置缓冲区中的编码命令字节数计量。单图块写入占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.

这是一段调用示例。放入完整程序前,请满足所述初始化和缓冲区要求。

预期结果

完整程序演示队列容量、保留的指针、两种独立重置,以及直接执行与NMI执行的区别。预期显示CAPACITY 128、ENCODED BYTES 030、AFTER EXEC 000、OVERFLOW SEEN 001和FAILED CHECKS 000。6条记录占30字节,却执行512次字节写入。OVERFLOW SEEN保留之前故意触发拒绝时的值,不表示最终溢出标志仍置位。

蓝色复制区域从像素(0,16)开始,绿色填充区域从(0,88)开始。每块包含7个完整的256像素宽图块行,以及第8行的248像素,共255个图块。最后一个8×8单元保持黑色。复制重复实心、左半填充和三角形的8×8图案;因入队后修改了源数据,首图块应为三角形。填充在调用方变量改变后仍保持实心。测试同时检查形状和颜色。

在y=160处,x=16应为红色实心正方形,x=224为红色左半图块,x=240为红色实心正方形。第一个验证零长度复制和填充不会改写;中间验证默认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);

返回值

以此处说明的平台单位表示的无符号八位值。读取不会修改或执行队列。

使用条件与限制

FC以固定128字节内置缓冲区中的编码命令字节数计量。单图块写入占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.

这是一段调用示例。放入完整程序前,请满足所述初始化和缓冲区要求。

预期结果

完整程序演示队列容量、保留的指针、两种独立重置,以及直接执行与NMI执行的区别。预期显示CAPACITY 128、ENCODED BYTES 030、AFTER EXEC 000、OVERFLOW SEEN 001和FAILED CHECKS 000。6条记录占30字节,却执行512次字节写入。OVERFLOW SEEN保留之前故意触发拒绝时的值,不表示最终溢出标志仍置位。

蓝色复制区域从像素(0,16)开始,绿色填充区域从(0,88)开始。每块包含7个完整的256像素宽图块行,以及第8行的248像素,共255个图块。最后一个8×8单元保持黑色。复制重复实心、左半填充和三角形的8×8图案;因入队后修改了源数据,首图块应为三角形。填充在调用方变量改变后仍保持实心。测试同时检查形状和颜色。

在y=160处,x=16应为红色实心正方形,x=224为红色左半图块,x=240为红色实心正方形。第一个验证零长度复制和填充不会改写;中间验证默认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字节内置缓冲区中的编码命令字节数计量。单图块写入占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++;

这是一段调用示例。放入完整程序前,请满足所述初始化和缓冲区要求。

预期结果

完整程序演示队列容量、保留的指针、两种独立重置,以及直接执行与NMI执行的区别。预期显示CAPACITY 128、ENCODED BYTES 030、AFTER EXEC 000、OVERFLOW SEEN 001和FAILED CHECKS 000。6条记录占30字节,却执行512次字节写入。OVERFLOW SEEN保留之前故意触发拒绝时的值,不表示最终溢出标志仍置位。

蓝色复制区域从像素(0,16)开始,绿色填充区域从(0,88)开始。每块包含7个完整的256像素宽图块行,以及第8行的248像素,共255个图块。最后一个8×8单元保持黑色。复制重复实心、左半填充和三角形的8×8图案;因入队后修改了源数据,首图块应为三角形。填充在调用方变量改变后仍保持实心。测试同时检查形状和颜色。

在y=160处,x=16应为红色实心正方形,x=224为红色左半图块,x=240为红色实心正方形。第一个验证零长度复制和填充不会改写;中间验证默认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地址和单字节值追加为4字节记录。值立即复制进队列,执行时才写入PPU。可用于更新名称表中的一个图块,也可指定其他可写的PPU字节。

语法

c
void __vramq_put(u16 ppu_addr, u8 value);

参数

ppu_addr

PPU字节地址(0x0000–0x3FFF),不是CPU指针。目标必须可写;改写图案需要CHR RAM,不能写入CHR ROM。PPU映射与镜像规则照常适用。连续复制和填充应将PPUCTRL地址增量设为1。在安全的PPU写入时段执行,然后恢复所需的滚动状态;执行器会写入PPUADDR和PPUDATA,不会恢复滚动设置。

value

重复写入0至255的单字节。显示含义取决于目标是图块索引、像素位平面还是其他字节结构。

返回值

无(void)。

使用条件与限制

追加函数不返回成功值。完整记录放不下时,将__vramq_overflow()置1,保留原有记录和长度。空间恰好足够时可以加入。旧溢出标志不会阻止能放下的新记录,因此判断新批次前应先处理或清除标志。连续多次追加不构成事务。

FC以固定128字节内置缓冲区中的编码命令字节数计量。单图块写入占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).

这是一段调用示例。放入完整程序前,请满足所述初始化和缓冲区要求。

预期结果

完整程序演示队列容量、保留的指针、两种独立重置,以及直接执行与NMI执行的区别。预期显示CAPACITY 128、ENCODED BYTES 030、AFTER EXEC 000、OVERFLOW SEEN 001和FAILED CHECKS 000。6条记录占30字节,却执行512次字节写入。OVERFLOW SEEN保留之前故意触发拒绝时的值,不表示最终溢出标志仍置位。

蓝色复制区域从像素(0,16)开始,绿色填充区域从(0,88)开始。每块包含7个完整的256像素宽图块行,以及第8行的248像素,共255个图块。最后一个8×8单元保持黑色。复制重复实心、左半填充和三角形的8×8图案;因入队后修改了源数据,首图块应为三角形。填充在调用方变量改变后仍保持实心。测试同时检查形状和颜色。

在y=160处,x=16应为红色实心正方形,x=224为红色左半图块,x=240为红色实心正方形。第一个验证零长度复制和填充不会改写;中间验证默认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。宽度或高度为零时视为空矩形。

使用条件与限制

使用坐标差进行判断;右边界超过 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);

这是一段调用示例。放入完整程序前,请满足所述初始化和缓冲区要求。

预期结果

测试内部、边界、外部点和零宽度: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 位为零时返回 1,否则返回 0。不读取端口 1。

使用条件与限制

这些 API 直接访问 $4016/$4017。不要在同一端口交错执行中断驱动的手柄、键盘或串行操作;控制器移位位置也可能受影响。示例在采集输入时关闭 NMI,再在初始化显示时启用。

读取瞬时信号。不执行选通、不等待帧、不检测边沿、不确定瞄准位置,也不判断命中。原始读取与扳机/光线读取分别在各自时刻访问寄存器,因此信号变化时结果可能不同。

光线输入低电平有效:第 3 位为零时返回一。此转换不检测光枪是否连接;手柄或未连接输入也可能返回一。在屏幕上绘制目标,不会使该函数变成命中检测 API。

位转换还通过自行编写的测试 ROM 进行单元测试,将寄存器读取重定向到 RAM 测试值。覆盖 00/08/10/18 组合,并设置无关位。该单元测试与原样运行示例及真实外设行为是分开的。

这些图像来自在 KUROSAKI 中原样运行可下载 ROM。当前输入总线模型支持手柄,但不模拟键盘、光枪或串行适配器信号。外部输入位读为零,因此截图不能验证这些设备。实体按键、光学感应和通信仍未经测试。

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 读为零,不是检测到屏幕亮度。画面还注明 LIGHT 1 IS NOT A HIT TEST。

__zapper_light: ZAPPER-DEFAULT — KUROSAKI 中,两个端口均显示 RAW=00、TRIGGER=000、LIGHT=001。无后缀调用与端口 2 相同,显示 000/001。LIGHT=001 来自未模拟的输入位 3 读为零,不是检测到屏幕亮度。画面还注明 LIGHT 1 IS NOT A HIT TEST。
ZAPPER-DEFAULT — KUROSAKI 中,两个端口均显示 RAW=00、TRIGGER=000、LIGHT=001。无后缀调用与端口 2 相同,显示 000/001。LIGHT=001 来自未模拟的输入位 3 读为零,不是检测到屏幕亮度。画面还注明 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) 的光线感应线,将低电平有效输入转换为一表示感光。

语法

c
u8 __zapper_light1(void);

返回值

$4016 第 3 位为零时返回 1,为一时返回 0。

使用条件与限制

这些 API 直接访问 $4016/$4017。不要在同一端口交错执行中断驱动的手柄、键盘或串行操作;控制器移位位置也可能受影响。示例在采集输入时关闭 NMI,再在初始化显示时启用。

读取瞬时信号。不执行选通、不等待帧、不检测边沿、不确定瞄准位置,也不判断命中。原始读取与扳机/光线读取分别在各自时刻访问寄存器,因此信号变化时结果可能不同。

光线输入低电平有效:第 3 位为零时返回一。此转换不检测光枪是否连接;手柄或未连接输入也可能返回一。在屏幕上绘制目标,不会使该函数变成命中检测 API。

位转换还通过自行编写的测试 ROM 进行单元测试,将寄存器读取重定向到 RAM 测试值。覆盖 00/08/10/18 组合,并设置无关位。该单元测试与原样运行示例及真实外设行为是分开的。

这些图像来自在 KUROSAKI 中原样运行可下载 ROM。当前输入总线模型支持手柄,但不模拟键盘、光枪或串行适配器信号。外部输入位读为零,因此截图不能验证这些设备。实体按键、光学感应和通信仍未经测试。

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 读为零,不是检测到屏幕亮度。画面还注明 LIGHT 1 IS NOT A HIT TEST。

__zapper_light1: ZAPPER-DEFAULT — KUROSAKI 中,两个端口均显示 RAW=00、TRIGGER=000、LIGHT=001。无后缀调用与端口 2 相同,显示 000/001。LIGHT=001 来自未模拟的输入位 3 读为零,不是检测到屏幕亮度。画面还注明 LIGHT 1 IS NOT A HIT TEST。
ZAPPER-DEFAULT — KUROSAKI 中,两个端口均显示 RAW=00、TRIGGER=000、LIGHT=001。无后缀调用与端口 2 相同,显示 000/001。LIGHT=001 来自未模拟的输入位 3 读为零,不是检测到屏幕亮度。画面还注明 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) 的光线感应线,将低电平有效输入转换为一表示感光。

语法

c
u8 __zapper_light2(void);

返回值

$4017 第 3 位为零时返回 1,为一时返回 0。

使用条件与限制

这些 API 直接访问 $4016/$4017。不要在同一端口交错执行中断驱动的手柄、键盘或串行操作;控制器移位位置也可能受影响。示例在采集输入时关闭 NMI,再在初始化显示时启用。

读取瞬时信号。不执行选通、不等待帧、不检测边沿、不确定瞄准位置,也不判断命中。原始读取与扳机/光线读取分别在各自时刻访问寄存器,因此信号变化时结果可能不同。

光线输入低电平有效:第 3 位为零时返回一。此转换不检测光枪是否连接;手柄或未连接输入也可能返回一。在屏幕上绘制目标,不会使该函数变成命中检测 API。

位转换还通过自行编写的测试 ROM 进行单元测试,将寄存器读取重定向到 RAM 测试值。覆盖 00/08/10/18 组合,并设置无关位。该单元测试与原样运行示例及真实外设行为是分开的。

这些图像来自在 KUROSAKI 中原样运行可下载 ROM。当前输入总线模型支持手柄,但不模拟键盘、光枪或串行适配器信号。外部输入位读为零,因此截图不能验证这些设备。实体按键、光学感应和通信仍未经测试。

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 读为零,不是检测到屏幕亮度。画面还注明 LIGHT 1 IS NOT A HIT TEST。

__zapper_light2: ZAPPER-DEFAULT — KUROSAKI 中,两个端口均显示 RAW=00、TRIGGER=000、LIGHT=001。无后缀调用与端口 2 相同,显示 000/001。LIGHT=001 来自未模拟的输入位 3 读为零,不是检测到屏幕亮度。画面还注明 LIGHT 1 IS NOT A HIT TEST。
ZAPPER-DEFAULT — KUROSAKI 中,两个端口均显示 RAW=00、TRIGGER=000、LIGHT=001。无后缀调用与端口 2 相同,显示 000/001。LIGHT=001 来自未模拟的输入位 3 读为零,不是检测到屏幕亮度。画面还注明 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) 的两个光枪位。

语法

c
u8 __zapper_raw1(void);

返回值

返回 $4016 & 0x18,即 0x00、0x08、0x10 或 0x18。第 4 位为扳机线,第 3 位为原始无光信号。

使用条件与限制

这些 API 直接访问 $4016/$4017。不要在同一端口交错执行中断驱动的手柄、键盘或串行操作;控制器移位位置也可能受影响。示例在采集输入时关闭 NMI,再在初始化显示时启用。

读取瞬时信号。不执行选通、不等待帧、不检测边沿、不确定瞄准位置,也不判断命中。原始读取与扳机/光线读取分别在各自时刻访问寄存器,因此信号变化时结果可能不同。

光线输入低电平有效:第 3 位为零时返回一。此转换不检测光枪是否连接;手柄或未连接输入也可能返回一。在屏幕上绘制目标,不会使该函数变成命中检测 API。

位转换还通过自行编写的测试 ROM 进行单元测试,将寄存器读取重定向到 RAM 测试值。覆盖 00/08/10/18 组合,并设置无关位。该单元测试与原样运行示例及真实外设行为是分开的。

这些图像来自在 KUROSAKI 中原样运行可下载 ROM。当前输入总线模型支持手柄,但不模拟键盘、光枪或串行适配器信号。外部输入位读为零,因此截图不能验证这些设备。实体按键、光学感应和通信仍未经测试。

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 读为零,不是检测到屏幕亮度。画面还注明 LIGHT 1 IS NOT A HIT TEST。

__zapper_raw1: ZAPPER-DEFAULT — KUROSAKI 中,两个端口均显示 RAW=00、TRIGGER=000、LIGHT=001。无后缀调用与端口 2 相同,显示 000/001。LIGHT=001 来自未模拟的输入位 3 读为零,不是检测到屏幕亮度。画面还注明 LIGHT 1 IS NOT A HIT TEST。
ZAPPER-DEFAULT — KUROSAKI 中,两个端口均显示 RAW=00、TRIGGER=000、LIGHT=001。无后缀调用与端口 2 相同,显示 000/001。LIGHT=001 来自未模拟的输入位 3 读为零,不是检测到屏幕亮度。画面还注明 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) 的两个光枪位。

语法

c
u8 __zapper_raw2(void);

返回值

返回 $4017 & 0x18,即 0x00、0x08、0x10 或 0x18。第 4 位为扳机线,第 3 位为原始无光信号。

使用条件与限制

这些 API 直接访问 $4016/$4017。不要在同一端口交错执行中断驱动的手柄、键盘或串行操作;控制器移位位置也可能受影响。示例在采集输入时关闭 NMI,再在初始化显示时启用。

读取瞬时信号。不执行选通、不等待帧、不检测边沿、不确定瞄准位置,也不判断命中。原始读取与扳机/光线读取分别在各自时刻访问寄存器,因此信号变化时结果可能不同。

光线输入低电平有效:第 3 位为零时返回一。此转换不检测光枪是否连接;手柄或未连接输入也可能返回一。在屏幕上绘制目标,不会使该函数变成命中检测 API。

位转换还通过自行编写的测试 ROM 进行单元测试,将寄存器读取重定向到 RAM 测试值。覆盖 00/08/10/18 组合,并设置无关位。该单元测试与原样运行示例及真实外设行为是分开的。

这些图像来自在 KUROSAKI 中原样运行可下载 ROM。当前输入总线模型支持手柄,但不模拟键盘、光枪或串行适配器信号。外部输入位读为零,因此截图不能验证这些设备。实体按键、光学感应和通信仍未经测试。

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 读为零,不是检测到屏幕亮度。画面还注明 LIGHT 1 IS NOT A HIT TEST。

__zapper_raw2: ZAPPER-DEFAULT — KUROSAKI 中,两个端口均显示 RAW=00、TRIGGER=000、LIGHT=001。无后缀调用与端口 2 相同,显示 000/001。LIGHT=001 来自未模拟的输入位 3 读为零,不是检测到屏幕亮度。画面还注明 LIGHT 1 IS NOT A HIT TEST。
ZAPPER-DEFAULT — KUROSAKI 中,两个端口均显示 RAW=00、TRIGGER=000、LIGHT=001。无后缀调用与端口 2 相同,显示 000/001。LIGHT=001 来自未模拟的输入位 3 读为零,不是检测到屏幕亮度。画面还注明 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 位。不会自动切换到端口 1。

使用条件与限制

这些 API 直接访问 $4016/$4017。不要在同一端口交错执行中断驱动的手柄、键盘或串行操作;控制器移位位置也可能受影响。示例在采集输入时关闭 NMI,再在初始化显示时启用。

读取瞬时信号。不执行选通、不等待帧、不检测边沿、不确定瞄准位置,也不判断命中。原始读取与扳机/光线读取分别在各自时刻访问寄存器,因此信号变化时结果可能不同。

结果是扳机信号线的逻辑状态。不保证扳机一直完全扣下,开关行为也取决于设备。要产生游戏开火事件,请与上一次读值比较,检测所需跳变。

位转换还通过自行编写的测试 ROM 进行单元测试,将寄存器读取重定向到 RAM 测试值。覆盖 00/08/10/18 组合,并设置无关位。该单元测试与原样运行示例及真实外设行为是分开的。

这些图像来自在 KUROSAKI 中原样运行可下载 ROM。当前输入总线模型支持手柄,但不模拟键盘、光枪或串行适配器信号。外部输入位读为零,因此截图不能验证这些设备。实体按键、光学感应和通信仍未经测试。

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 读为零,不是检测到屏幕亮度。画面还注明 LIGHT 1 IS NOT A HIT TEST。

__zapper_trigger: ZAPPER-DEFAULT — KUROSAKI 中,两个端口均显示 RAW=00、TRIGGER=000、LIGHT=001。无后缀调用与端口 2 相同,显示 000/001。LIGHT=001 来自未模拟的输入位 3 读为零,不是检测到屏幕亮度。画面还注明 LIGHT 1 IS NOT A HIT TEST。
ZAPPER-DEFAULT — KUROSAKI 中,两个端口均显示 RAW=00、TRIGGER=000、LIGHT=001。无后缀调用与端口 2 相同,显示 000/001。LIGHT=001 来自未模拟的输入位 3 读为零,不是检测到屏幕亮度。画面还注明 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) 的扳机线,并规范化为布尔值。

语法

c
u8 __zapper_trigger1(void);

返回值

$4016 第 4 位为一时返回 1,否则返回 0。

使用条件与限制

这些 API 直接访问 $4016/$4017。不要在同一端口交错执行中断驱动的手柄、键盘或串行操作;控制器移位位置也可能受影响。示例在采集输入时关闭 NMI,再在初始化显示时启用。

读取瞬时信号。不执行选通、不等待帧、不检测边沿、不确定瞄准位置,也不判断命中。原始读取与扳机/光线读取分别在各自时刻访问寄存器,因此信号变化时结果可能不同。

结果是扳机信号线的逻辑状态。不保证扳机一直完全扣下,开关行为也取决于设备。要产生游戏开火事件,请与上一次读值比较,检测所需跳变。

位转换还通过自行编写的测试 ROM 进行单元测试,将寄存器读取重定向到 RAM 测试值。覆盖 00/08/10/18 组合,并设置无关位。该单元测试与原样运行示例及真实外设行为是分开的。

这些图像来自在 KUROSAKI 中原样运行可下载 ROM。当前输入总线模型支持手柄,但不模拟键盘、光枪或串行适配器信号。外部输入位读为零,因此截图不能验证这些设备。实体按键、光学感应和通信仍未经测试。

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 读为零,不是检测到屏幕亮度。画面还注明 LIGHT 1 IS NOT A HIT TEST。

__zapper_trigger1: ZAPPER-DEFAULT — KUROSAKI 中,两个端口均显示 RAW=00、TRIGGER=000、LIGHT=001。无后缀调用与端口 2 相同,显示 000/001。LIGHT=001 来自未模拟的输入位 3 读为零,不是检测到屏幕亮度。画面还注明 LIGHT 1 IS NOT A HIT TEST。
ZAPPER-DEFAULT — KUROSAKI 中,两个端口均显示 RAW=00、TRIGGER=000、LIGHT=001。无后缀调用与端口 2 相同,显示 000/001。LIGHT=001 来自未模拟的输入位 3 读为零,不是检测到屏幕亮度。画面还注明 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) 的扳机线,并规范化为布尔值。

语法

c
u8 __zapper_trigger2(void);

返回值

$4017 第 4 位为一时返回 1,否则返回 0。

使用条件与限制

这些 API 直接访问 $4016/$4017。不要在同一端口交错执行中断驱动的手柄、键盘或串行操作;控制器移位位置也可能受影响。示例在采集输入时关闭 NMI,再在初始化显示时启用。

读取瞬时信号。不执行选通、不等待帧、不检测边沿、不确定瞄准位置,也不判断命中。原始读取与扳机/光线读取分别在各自时刻访问寄存器,因此信号变化时结果可能不同。

结果是扳机信号线的逻辑状态。不保证扳机一直完全扣下,开关行为也取决于设备。要产生游戏开火事件,请与上一次读值比较,检测所需跳变。

位转换还通过自行编写的测试 ROM 进行单元测试,将寄存器读取重定向到 RAM 测试值。覆盖 00/08/10/18 组合,并设置无关位。该单元测试与原样运行示例及真实外设行为是分开的。

这些图像来自在 KUROSAKI 中原样运行可下载 ROM。当前输入总线模型支持手柄,但不模拟键盘、光枪或串行适配器信号。外部输入位读为零,因此截图不能验证这些设备。实体按键、光学感应和通信仍未经测试。

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 读为零,不是检测到屏幕亮度。画面还注明 LIGHT 1 IS NOT A HIT TEST。

__zapper_trigger2: ZAPPER-DEFAULT — KUROSAKI 中,两个端口均显示 RAW=00、TRIGGER=000、LIGHT=001。无后缀调用与端口 2 相同,显示 000/001。LIGHT=001 来自未模拟的输入位 3 读为零,不是检测到屏幕亮度。画面还注明 LIGHT 1 IS NOT A HIT TEST。
ZAPPER-DEFAULT — KUROSAKI 中,两个端口均显示 RAW=00、TRIGGER=000、LIGHT=001。无后缀调用与端口 2 相同,显示 000/001。LIGHT=001 来自未模拟的输入位 3 读为零,不是检测到屏幕亮度。画面还注明 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.

开发命令语法

这些用法取自 Program.DebugTools.cs 与 Program.VibeTools.cs。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