KITAQ 开发系统
用户手册 2026.09

KITAQ SERIES • 参考版

KITAQGB

Game Boy 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,或使用完整路径运行。

OSkitaqgb
Windows x64kitaqgb.exe
Linux x64 (musl)bin/linux-x86_64/kitaqgb
Linux x64 (GNU)bin/linux-gnu-x86_64/kitaqgb
macOS ARM64bin/macos-arm64/kitaqgb
macOS Intelbin/macos-x86_64/kitaqgb
macOS Universalbin/macos-universal/kitaqgb

kitaqgb · 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
kitaqgb game.c -I lib -o game.gb --cart=romonly --romsize=32k

1. 认识 KITAQGB

KITAQGB 是从 Zachtronics 的 NORCAL 发展而来的 GB/CGB C 语言编译器。

NORCAL的名称源于北加利福尼亚(Northern California)。受这种以地域命名的思路启发,作者以自己出生并长大的北九州市为基础,将项目命名为KITAQGB。KITAQ + GB把日本福冈县北九州市的昵称北九(キタキュー,Kitakyū)与Game Boy结合起来。KITAQ读作日语“キタキュー”;英语读音提示为 kee-tah-KYOO,音标为 /ˌkiːtɑːˈkjuː/。末尾的Q与英语字母Q同音。KITAQGB中的G和B分别按字母名称发音,整个名称读作 kee-tah-KYOO jee bee。

KITAQGB这个名称包含两层含义。Kernel-Informed Toolchain for AI-Quality Game Boy Development表达了工具链应理解目标硬件、同时支持程序员和生成式AI的目标。

另一层含义是 Kids' Imagination Transformed into Actual Quests in Game Boy Forests,意为“把孩子的想象,化为Game Boy森林里的真正冒险的工具”。这寄托了一份创作愿望:让小小的点子、涂鸦和借助AI制作的原型,变成真正可以游玩的冒险。

它读取 C 文件,生成 CPU 指令,再封装成主机可读取的 ROM。它的语言、库和调用约定与桌面 C 编译器并不完全相同。

GB 使用 8 位 CPU,内存有限。画面主要由 8×8 像素的图块组成;精灵是可独立定位的小图像。文字同样需要图块,不能假定系统自带通用文字显示。本例采用作者的 ascii.c,按 GB 的 ASCII 顺序重新排列,但不改变字形位图;FC 版则转换为 NES CHR 布局。

2. 环境准备与编译器构建

使用 Rust 1.85 或更高版本,可为 Windows、Linux、macOS ARM 和 macOS Intel 构建编译器及全部辅助工具。原生程序运行不需要 .NET,素材处理工具也不需要 Python 或 Pillow。

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

Windows 程序位于仓库根目录;Linux 和 macOS 程序位于下表所列的 bin/ 平台目录。请保留 lib/ 和许可声明。在 Linux/macOS 上下载后执行 chmod +x,并将目录加入 PATH,或使用完整路径运行。

OSkitaqgb
Windows x64kitaqgb.exe
Linux x64 (musl)bin/linux-x86_64/kitaqgb
Linux x64 (GNU)bin/linux-gnu-x86_64/kitaqgb
macOS ARM64bin/macos-arm64/kitaqgb
macOS Intelbin/macos-x86_64/kitaqgb
macOS Universalbin/macos-universal/kitaqgb
powershell
.\scripts\build.ps1
.\kitaqgb.exe --help
sh
sh scripts/build.sh

PowerShell 示例构建使用根目录的 Windows 程序。在 Linux/macOS 上使用原生编译器并传入相同的 C 输入和选项,或使用 PowerShell 7。CHR 图形与 C 源码是不同输入;font.chr 保留原示例字体。

3. 第一个程序

samples/gb_hello.c 使用 gb_common.h 中的画面和文字辅助函数。请保留配套头文件与字体文件。#include 用于读入其他文件的声明或定义。

c
#include "gb_common.h"
void main() {
    m_init();
    m_text(2, 5, "HELLO WORLD");
    m_number(42);
    while (1) { m_wait(); }
}
powershell
.\kitaqgb\kitaqgb.exe .\kitaq-docs\samples\gb_hello.c -I .\kitaq-docs\samples -o .\out\hello.gb --profile=dev --rst-disable --stack-bank=fixed
.\kokura\kokura-cli.exe .\out\hello.gb --run-frames 120 --png .\out\hello.png --dump-report .\out\hello.json

预期画面显示 HELLO WORLD 和 042。以 m_ 开头的名称是公共示例头文件中定义的教学辅助函数,不是 KITAQGB 标准命令。编译器内建操作包括 __wait_vblank、__vram_copy 等。

4. 基本语法与类型

语句以 ; 结尾,多条语句用 { } 分组。// 开始行注释,/* ... */ 表示块注释,名称区分大小写。入口请写成 void main()。本 GB 版本中的 void main(void) 会产生语法错误,不要原样照搬 FC 的入口声明。

类型含义与范围
u8无符号 8 位整数,0~255
s8有符号 8 位整数,-128~127
u16无符号 16 位整数,0~65535
s16有符号 16 位整数,-32768~32767
void无返回值
T*指向 T 类型数据的指针

入门时建议使用这些短类型名。不要假定 int、long、float、double 和标准头文件与桌面环境相同。本版 char 是无符号 8 位数据;需要负数时请明确使用 s8 或 s16。

c
u8 lives = 3;
u16 score;
score = (u16)200 + 100;

(u16) 是强制类型转换。小类型已经溢出的值,即使再赋给大变量,也无法找回丢失的位;应在计算前扩大操作数的类型。小数单位的移动可使用定点数库。

5. 表达式与运算符

类别运算符示例或含义
算术+ - * / %n / 10 求整数商,n % 10 求余数
比较== != < <= > >=lives == 0 判断相等
逻辑! && / ||非、两个条件都成立、任一条件成立
位运算& / | / ^ ~ << >>按钮掩码及其他位集合
赋值= += -= 等x += 1 更新数值
自增自减++ --i++ 增加 1
条件选择condition ? A : B根据条件选择一个值
指针&variable / *p取得地址或访问指向的数据

请区分 = 和 ==。复杂表达式用括号写清意图,不要把过多函数调用和副作用挤在一条语句里。避免除以零和数组越界。sizeof 返回字节数,offsetof 返回结构体成员的偏移。

表达式结果与求值

比较运算符 ==、!=、<、<=、>、>= 和逻辑运算符 !、&&、|| 以 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 中则进入条件判断。

6. 分支与循环

c
if (lives == 0) { game_over = 1; }
else { score = score + 10; }
for (i = 0; i < 4; i++) { table[i] = i; }
while (running != 0) { update_game(); }
do { count++; } while (count < 3);

break 离开循环或 switch,continue 进入下一轮循环,return 结束函数。需要从一个 switch case 继续执行下一个时,请明确写出 fallthrough;;隐式贯穿会触发诊断。完整程序见 gb_control.c。

7. 函数、数组与结构体

c
u8 add(u8 a, u8 b) { return (u8)(a + b); }
typedef struct { u8 x; u8 y; } Point;
Point player;
u8 tiles[4];

数组下标从 0 开始,四元素数组的合法下标为 0~3。player.x 访问成员,pointer->x 通过指针访问成员。解析器接受结构体、联合体和枚举,但布局取决于类型以及 __packed / __aligned 属性。与硬件或二进制格式交换数据前,应检查 sizeof。

默认 Legacy ABI 将参数和局部存储放在固定位置。不要假定具备桌面式递归或中断重入能力。__stackcall 和 --abi=stack 是高级调用约定选项;混用约定时应检查 ABI 报告并验证执行。--abi=stack 不支持通过函数指针调用回调。请按构建命令使用默认 Legacy ABI,构建完整 system 和 scene 回调示例。

8. 多文件与预处理器

类型、常量、声明放在头文件,函数体放在 .c 文件。使用 #pragma once 或头文件保护宏避免重复包含。条件编译支持 #define、#undef、#if、#ifdef、#ifndef、#elif、#else、#endif。

powershell
.\kitaqgb\kitaqgb.exe main.c game.c .\kitaqgb\lib\input.c -I .\kitaqgb\lib -o .\out\game.gb --profile=dev

-I 添加头文件搜索目录。包含头文件并不等于链接实现,还要在构建命令中列出所需 .c 文件。按需选择源文件;把全部库文件无差别加入,可能造成寄存器定义或中断处理程序重复。

9. ROM、内存与分页

ROM 存代码和常量,WRAM 存变量,VRAM 存图像,OAM 存精灵描述。切换 bank 会改变 CPU 地址映射到哪一块实际内存。单独一个 16 位指针无法标识另一 bank 的数据。

c
#pragma bank 1
__prg_rom u8 level_data[] = { 1, 2, 3, 4 };

__prg_rom 将数据放入 ROM;__location(0xFF40) 等属性指定固定地址,__wram / __hram 指定内存区域。使用 #pragma bank 或 #pragma fixed_bank 时,请检查映射文件,确保中断使用的代码和数据始终可访问。

powershell
.\kitaqgb\kitaqgb.exe main.c -o .\out\game.gbc --cart=mbc5 --romsize=256k --ramsize=32k --cgb=cgb --rom-title=MYGAME --stack-bank=fixed --emit-ai-metadata=.\out\build.json

--cgb=dmg 声明 DMG 软件,--cgb=cgb 声明双模式软件,--cgb=cgb_only 声明 CGB 专用软件。双模式游戏必须先识别硬件,再使用 CGB 专用功能。ROM 头标志不会替你实现游戏内部的双模式逻辑。

10. 安全更新图像

VBlank 是显示帧之间的空白期。时机不当的 VRAM/OAM 写入可能导致画面损坏或更新丢失。初始化素材应在关闭显示时加载,常规更新应使用安全内建函数或 VRAM 队列。带 _unsafe、_fast 的函数需要调用方保证安全传输时段。

OAM DMA 缓冲区须按 256 字节边界对齐。GB 的 __oam_dma 接收源地址;FC 的同名内建函数没有参数,二者不能混用。

11. 构建选项与输出文件

-o 选择输出,-O0 / -O1 选择优化级别,--profile=dev|release|test 选择设置组合。--no-disasm 关闭反汇编输出;--debug-out=...、--trace-out=... 指定调查资料的位置。根据快速构建或详细排查的需要选择输出量。

powershell
.\kitaqgb\kitaqgb.exe main.c -I .\kitaqgb\lib -o .\out\game.gb --profile=dev --diag-json=.\out\compile.json --emit-ai-metadata=.\out\build.json --debug-out=.\out\debug

.map 记录名称和布局,.funcsizes.txt 记录函数大小,.dbg2.json / .source_map.txt 关联执行位置与源码,.build_report.json 汇总构建结果。明确生成 --emit-ai-metadata JSON,可使 SARAKURA 的输入关系更清楚。

12. 从第一个错误读起

先读首个错误的文件名、行号和 KQ 诊断编号;后续错误可能只是第一个语法错误的连锁结果。未定义符号应检查声明、实现以及是否加入构建。ROM 超出容量时,检查素材大小、函数大小和 bank 布局。

powershell
.\kitaqgb\kitaqgb.exe kqhelp KQ1002
.\kitaqgb\kitaqgb.exe symfind main --map=.\out\game.map
.\kitaqgb\kitaqgb.exe romdiff .\out\before.gb .\out\after.gb

13. 内联汇编

__asm { ... } 使用 KITAQGB 的指令名称,不保证接受其他 GB 汇编器的任意源码。内部名称包括 LD_A_IMM 等。使用前须了解参数、返回值、需保留的寄存器和栈行为。附录列出了指令写法与操作数格式。

c
void nop_example() {
    __asm { NOP }
}

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

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

· 下载 Markdown

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

需求

  • 游戏名称:<填写>
  • 类型与核心玩法:<填写>
  • 操作方式及成功、失败条件:<填写>
  • 必需的界面、关卡、敌人和道具:<填写>
  • 画面风格、背景音乐和音效:<填写,并注明所提供素材的路径>
  • 存档、通信、外设及其他要求:<填写,或无>
  • 项目目录:<填写>
  • 再分发要求:<例如,自编代码和原创素材可按 MIT 许可公开>
  • 目标机型:<初代 Game Boy / GB 与 CGB 双兼容 / 仅 CGB>
  • 性能目标:<例如,正常游玩时每秒更新游戏逻辑 60 次;注明高负载场景的可接受表现>

请执行的任务

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

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

确认环境和验收标准

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

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

  • 使用 KITAQGB 的 C 方言和 void main()。不要假定桌面 C 或 GBDK API 可以直接使用。除声明外,还要把所需 .c 实现纳入构建;检查初始化顺序、单位、符号、范围、缓冲区生命周期和 ROM 分库。
  • 规划 VRAM/OAM 更新、VBlank、中断、栈、ROM/WRAM 分库和图块、精灵数量限制。传输队列的总容量、剩余容量与物理 VRAM 的容量、空闲空间不是同一概念。
  • DMG 游戏不得依赖 CGB 专用功能。双兼容游戏必须分别测试两种硬件模式。
  • 字母、数字和符号使用所提供的原创 ascii.c 字体,并检查字符与图块的对应关系。
  • 先连通启动、标题界面、可控制角色、成功或失败及重新开始,再扩充内容。
  • 保留可编辑的图形、音乐、音效源文件及生成步骤,确认构建实际读取了导出数据。
  • 源码注释用英文,进度报告用简体中文。SARAKURA 的标准报告保持英文。

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

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

下面是基本的 DMG 检查示例。请准备 main.c 和所需库实现文件,并按游戏调整选项和输入序列。

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

# Include all additional implementation units required by the game.
& '.\kitaqgb\kitaqgb.exe' '.\game-gb\src\main.c' `
  -I '.\kitaqgb\lib' -o "$iteration\game.gb" `
  --profile=dev --rst-disable --stack-bank=fixed --no-disasm `
  "--emit-ai-metadata=$iteration\build.json"
if ($LASTEXITCODE -ne 0) { throw 'Build failed; inspect the build log.' }

# This sequence presses START once, with released intervals on both sides.
& '.\kokura\kokura-cli.exe' "$iteration\game.gb" `
  --hardware dmg --run-frames 300 `
  --input-seq 'NONE:60;START:1;NONE:239' `
  --png "$iteration\frame.png" --record-wav "$iteration\audio.wav" `
  --dump-report "$iteration\run.json" `
  --emit-diagnostics "$iteration\events.jsonl"
if ($LASTEXITCODE -ne 0) { throw 'Emulator run failed; inspect the run log.' }

& '.\sarakura\sarakura.exe' gb 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.' }

--hardware dmg 选择初代 GB。测试 CGB 或双兼容时,需匹配 ROM 头部和模拟器机型设置。示例输入在两个松开区间之间按一次 START。运行 300 帧不能代表完成整个游戏的测试。

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

  • 保存输入场景,区分按下、按住和松开。覆盖规格中的全部路径:启动、开始、移动、动作、碰撞、滚动、关卡切换、游戏结束、重新开始、暂停,以及适用的存档和通信。
  • 保存关键帧 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-gb\out\iter-001\analysis' `
  --current '.\game-gb\out\iter-002\analysis' `
  --out '.\game-gb\out\delta.json' --markdown '.\game-gb\out\delta.md' `
  --fail-on-new error --fail-on-regression error --enforce

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

完成条件与交付内容

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

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

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

源码位置

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

完整示例程序

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

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

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

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

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

操作与预期结果: 应看到 26 个大写字母、26 个小写字母、10 个数字及 30 个标点符号。检查每个字形是否清晰,无需输入。

显示所提供的字符集:应看到 26 个大写字母、26 个小写字母、10 个数字及 30 个标点符号。检查每个字形是否清晰,无需输入。
显示示例:应看到 26 个大写字母、26 个小写字母、10 个数字及 30 个标点符号。检查每个字形是否清晰,无需输入。

下载源码 / 验证状态

powershell
.\kitaqgb\kitaqgb.exe  kitaq-docs/samples/gb_font.c -I kitaqgb/lib -I kitaq-docs/samples -o out/gb_font.gb --profile=dev --rst-disable --stack-bank=fixed
c
// Display the letters, digits and punctuation from the supplied ascii.c font.
// Expected: Uppercase, lowercase, numbers and all 30 supplied punctuation glyphs
#include "gb_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() {
    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();  }
}
第一次显示文字 gb_hello.c

用途: 学习启动 ROM、在背景绘制文字和数值的基本步骤。

工作原理: m_init 后,m_text 绘制问候语,m_number 格式化 42。最后的循环等待各帧,同时保持画面可见。

操作与预期结果: 画面显示 HELLO WORLD 和 042。前导零来自三位十进制格式。

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

下载源码 / 验证状态

powershell
.\kitaqgb\kitaqgb.exe  kitaq-docs/samples/gb_hello.c -I kitaqgb/lib -I kitaq-docs/samples -o out/gb_hello.gb --profile=dev --rst-disable --stack-bank=fixed
c
// Display a first message on screen.
// Expected: HELLO WORLD and 042
#include "gb_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() {
    m_init();
    m_text(2,3,"HELLO");
    m_text(2,5,"HELLO WORLD");
    m_number(42);
    while (1) { m_wait();  }
}
使用更宽中间值的算术 gb_arithmetic.c

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

工作原理: (u16)200 + 100 得到 300。程序将其除以 10,再转为 u8 显示。注意:扩大类型的强制转换发生在加法之前。

操作与预期结果: 030 表示计算结果 30。

使用更宽中间值的算术:030 表示计算结果 30。
显示示例:030 表示计算结果 30。

下载源码 / 验证状态

powershell
.\kitaqgb\kitaqgb.exe  kitaq-docs/samples/gb_arithmetic.c -I kitaqgb/lib -I kitaq-docs/samples -o out/gb_arithmetic.gb --profile=dev --rst-disable --stack-bank=fixed
c
// Variables, arithmetic and type conversion.
// Expected: 030
#include "gb_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() {
    m_init();
    m_text(2,3,"ARITHMETIC");
    u16 sum;
    sum = (u16)200 + 100;
    m_number((u8)(sum / 10));
    while (1) { m_wait();  }
}
分支与循环 gb_control.c

用途: 在小型计算中尝试 for、continue、while、do/while 和 switch。

工作原理: 将 0 至 7 相加但跳过 3,得到 25。while 循环推进到 30,do/while 推进到 32,switch 选择 case 32 并赋值 42。

操作与预期结果: 042 是这些循环和分支的最终结果。

分支与循环:042 是这些循环和分支的最终结果。
显示示例:042 是这些循环和分支的最终结果。

下载源码 / 验证状态

powershell
.\kitaqgb\kitaqgb.exe  kitaq-docs/samples/gb_control.c -I kitaqgb/lib -I kitaq-docs/samples -o out/gb_control.gb --profile=dev --rst-disable --stack-bank=fixed
c
// Conditional branches and loops.
// Expected: 042
#include "gb_common.h"


// Exercise loop control and a final branch, then display the selected value 42.
void main() {
    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<30) { sum++; }
    do { sum++; } while (sum<32);
    switch (sum) { case 32: sum=42; break; default: sum=0; break; }
    m_number(sum);
    while (1) { m_wait();  }
}
函数、数组、结构体与指针 gb_aggregate.c

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

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

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

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

下载源码 / 验证状态

powershell
.\kitaqgb\kitaqgb.exe  kitaq-docs/samples/gb_aggregate.c -I kitaqgb/lib -I kitaq-docs/samples -o out/gb_aggregate.gb --profile=dev --rst-disable --stack-bank=fixed
c
// Functions, arrays, structures and pointers.
// Expected: 042
#include "gb_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() {
    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();  }
}
位标志与缓冲区复制 gb_bits_memory.c

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

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

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

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

下载源码 / 验证状态

powershell
.\kitaqgb\kitaqgb.exe  kitaq-docs/samples/gb_bits_memory.c -I kitaqgb/lib -I kitaq-docs/samples -o out/gb_bits_memory.gb --profile=dev --rst-disable --stack-bank=fixed
c
// Bit operations and memory copying.
// Expected: 010
#include "gb_common.h"
void __memset(void* dst,u8 value,u16 len);
void __memcpy(void* dst,const void* src,u16 len);
void __bit_set(u8* base,u16 bit);
void __bit_toggle(u8* base,u16 bit);

// Clear the source bytes, set bits 3 and 1, copy the buffer, and display 8 + 2 = 10.
void main() {
    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();  }
}
统计新按下的按键 gb_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
.\kitaqgb\kitaqgb.exe kitaqgb/lib/input.c kitaq-docs/samples/gb_input.c -I kitaqgb/lib -I kitaq-docs/samples -o out/gb_input.gb --profile=dev --rst-disable --stack-bank=fixed
c
// Distinguish a new button press from a held button.
// Expected: 000 initially; A increments once per press
#include "gb_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() {
    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 定点乘法 gb_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
.\kitaqgb\kitaqgb.exe kitaqgb/lib/fixed.c kitaq-docs/samples/gb_fixed.c -I kitaqgb/lib -I kitaq-docs/samples -o out/gb_fixed.gb --profile=dev --rst-disable --stack-bank=fixed
c
// Calculate with signed Q8.8 fixed-point values.
// Expected: 012
#include "gb_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() {
    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();  }
}
从固定对象池分配 gb_entity.c

用途: 从固定槽位集合创建对象,无需动态分配。

工作原理: entity_init 重置对象池。entity_create 在 (20,30) 创建类型 1;程序检查失败 ID 0xFF 后,才将 vx 设为 2。

操作与预期结果: 001 是活动对象数,不是 ID 或坐标。本示例不更新或绘制对象,因此不会出现移动角色。

从固定对象池分配:001 是活动对象数,不是 ID 或坐标。本示例不更新或绘制对象,因此不会出现移动角色。
显示示例:001 是活动对象数,不是 ID 或坐标。本示例不更新或绘制对象,因此不会出现移动角色。

下载源码 / 验证状态

powershell
.\kitaqgb\kitaqgb.exe kitaqgb/lib/entity.c kitaq-docs/samples/gb_entity.c -I kitaqgb/lib -I kitaq-docs/samples -o out/gb_entity.gb --profile=dev --rst-disable --stack-bank=fixed
c
// Allocate an entity from a fixed-capacity object pool.
// Expected: 001
#include "gb_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() {
    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 中记录诊断值 gb_debug.c

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

工作原理: 程序初始化调试,设置帧标签 1,并以 HP 为名记录值 42。

操作与预期结果: 001 是跟踪条目数。HP=42 保存在诊断缓冲区中;仅凭画面不能查看或验证该有效载荷。

在 RAM 中记录诊断值:001 是跟踪条目数。HP=42 保存在诊断缓冲区中;仅凭画面不能查看或验证该有效载荷。
显示示例:001 是跟踪条目数。HP=42 保存在诊断缓冲区中;仅凭画面不能查看或验证该有效载荷。

下载源码 / 验证状态

powershell
.\kitaqgb\kitaqgb.exe kitaqgb/lib/debug.c kitaq-docs/samples/gb_debug.c -I kitaqgb/lib -I kitaq-docs/samples -o out/gb_debug.gb --profile=dev --rst-disable --stack-bank=fixed
c
// Record diagnostic values in the debug trace.
// Expected: 001; trace log contains HP=42
#include "gb_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() {
    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();  }
}
保存位置历史 gb_chain.c

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

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

操作与预期结果: 042 是固定完成标记,不是坐标或计数。检查 trail 和 points 可确认保存的历史。本程序不绘制跟随身体。

保存位置历史:042 是固定完成标记,不是坐标或计数。检查 trail 和 points 可确认保存的历史。本程序不绘制跟随身体。
显示示例:042 是固定完成标记,不是坐标或计数。检查 trail 和 points 可确认保存的历史。本程序不绘制跟随身体。

下载源码 / 验证状态

powershell
.\kitaqgb\kitaqgb.exe kitaqgb/lib/chain.c kitaq-docs/samples/gb_chain.c -I kitaqgb/lib -I kitaq-docs/samples -o out/gb_chain.gb --profile=dev --rst-disable --stack-bank=fixed
c
// Store a position history in a ring buffer.
// Expected: 042; history contains (20,30)
#include "gb_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() {
    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();  }
}
读取库的帧计数器 gb_system.c

用途: 观察计数式帧等待如何影响库帧计数器。

工作原理: system_init 后,程序调用 system_wait_vblank 两次,再显示 system_get_frame8。

操作与预期结果: 结果为 002。空闲循环使用手册辅助函数 m_wait,因此显示值不会持续增加。

读取库的帧计数器:结果为 002。空闲循环使用手册辅助函数 m_wait,因此显示值不会持续增加。
显示示例:结果为 002。空闲循环使用手册辅助函数 m_wait,因此显示值不会持续增加。

下载源码 / 验证状态

powershell
.\kitaqgb\kitaqgb.exe kitaqgb/lib/system.c kitaq-docs/samples/gb_system.c -I kitaqgb/lib -I kitaq-docs/samples -o out/gb_system.gb --profile=dev --rst-disable --stack-bank=fixed
c
// Count completed frame waits.
// Expected: 002
#include "gb_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() {
    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 gb_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
.\kitaqgb\kitaqgb.exe kitaqgb/lib/scene.c kitaq-docs/samples/gb_scene.c -I kitaqgb/lib -I kitaq-docs/samples -o out/gb_scene.gb --profile=dev --rst-disable --stack-bank=fixed
c
// Switch the current scene ID.
// Expected: 001; no callbacks registered
#include "gb_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() {
    m_init();
    m_text(2,3,"SCENE");
    scene_init(manual_scenes,2); scene_change(1);
    m_number(scene_get_current());
    while (1) { m_wait();  }
}
将世界坐标转换为屏幕坐标 gb_camera.c

用途: 减去摄像机位置,求世界坐标对应的屏幕位置。

工作原理: Camera_Set 接收 Q8.8 值 2560,即 X=10 像素。随后 Camera_WorldToScreenX 转换世界 X=52。

操作与预期结果: 052 减去 010 得到显示的 042。这是坐标计算,不会滚动背景。

将世界坐标转换为屏幕坐标:052 减去 010 得到显示的 042。这是坐标计算,不会滚动背景。
显示示例:052 减去 010 得到显示的 042。这是坐标计算,不会滚动背景。

下载源码 / 验证状态

powershell
.\kitaqgb\kitaqgb.exe kitaqgb/lib/scroll.c kitaqgb/lib/camera.c kitaq-docs/samples/gb_camera.c -I kitaqgb/lib -I kitaq-docs/samples -o out/gb_camera.gb --profile=dev --rst-disable --stack-bank=fixed
c
// Convert world coordinates to screen coordinates.
// Expected: 042: world x=52 minus camera x=10
#include "gb_common.h"
#include "camera.h"
Camera8_8 cam;
// Set the camera X coordinate to 10 pixels in Q8.8 (10 * 256), then subtract
// it from world X = 52 to display screen X = 42.
void main() {
    m_init();
    m_text(2,3,"CAMERA");
    Camera_Init(&cam); Camera_Set(&cam,2560,0);
    m_number((u8)Camera_WorldToScreenX(&cam,52));
    while (1) { m_wait();  }
}
将 RLE 解码到 RAM gb_rle.c

用途: 尝试用紧凑的计数/数值形式表示重复字节。

工作原理: 打包字节 {3,42,0} 表示三个 42,后接计数为零的结束标记。rle_decode 写入三字节目标空间。

操作与预期结果: 最后一个字节 out[2] 显示为 042。三个解码字节预期都为 42。

将 RLE 解码到 RAM:最后一个字节 out[2] 显示为 042。三个解码字节预期都为 42。
显示示例:最后一个字节 out[2] 显示为 042。三个解码字节预期都为 42。

下载源码 / 验证状态

powershell
.\kitaqgb\kitaqgb.exe kitaqgb/lib/rle.c kitaq-docs/samples/gb_rle.c -I kitaqgb/lib -I kitaq-docs/samples -o out/gb_rle.gb --profile=dev --rst-disable --stack-bank=fixed
c
// Decode run-length encoded data.
// Expected: 042; three bytes each equal to 42
#include "gb_common.h"
#include "rpg.h"
__prg_rom u8 packed[] = {3,42,0};
// Decode count/value pair (3,42), terminated by a zero count, into a
// three-byte buffer and display the final decoded byte.
void main() {
    m_init();
    m_text(2,3,"RLE");
    u8 out[3]; rle_decode(out,packed); m_number(out[2]);
    while (1) { m_wait();  }
}
读写棋盘格子 gb_board.c

用途: 学习棋盘游戏中网格坐标与存储位置的对应关系。

工作原理: 16 字节数组保存 4 × 4 棋盘。程序先清空,在格子 (2,1) 写入 42,再读取同一格。

操作与预期结果: 042 是读回值。程序不绘制 4 × 4 棋盘本身。

读写棋盘格子:042 是读回值。程序不绘制 4 × 4 棋盘本身。
显示示例:042 是读回值。程序不绘制 4 × 4 棋盘本身。

下载源码 / 验证状态

powershell
.\kitaqgb\kitaqgb.exe kitaqgb/lib/slg_board.c kitaq-docs/samples/gb_board.c -I kitaqgb/lib -I kitaq-docs/samples -o out/gb_board.gb --profile=dev --rst-disable --stack-bank=fixed
c
// Read and write cells in a game board.
// Expected: 042
#include "gb_common.h"
#include "slg.h"
SLGBoard board; u8 cells[16];
// Create a cleared 4-by-4 board in caller-owned storage, write 42 at (2,1),
// and display the value read back from the same cell.
void main() {
    m_init();
    m_text(2,3,"BOARD");
    slg_board_init(&board,4,4,cells); slg_board_clear(&board,0);
    slg_board_set(&board,2,1,42); m_number(slg_board_get(&board,2,1));
    while (1) { m_wait();  }
}
将方框推进一个物理步长 gb_physics.c

用途: 初始化世界和方框物体,再根据速度更新位置。

工作原理: 物体起始于 (40,40),速度 (2,0),重力为零。程序调用 kq2d_step 一次。

操作与预期结果: 042 是得到的 X 坐标,本示例以整数像素为单位。不绘制方框形状或连续动画。

将方框推进一个物理步长:042 是得到的 X 坐标,本示例以整数像素为单位。不绘制方框形状或连续动画。
显示示例:042 是得到的 X 坐标,本示例以整数像素为单位。不绘制方框形状或连续动画。

下载源码 / 验证状态

powershell
.\kitaqgb\kitaqgb.exe kitaqgb/lib/fixed.c kitaqgb/lib/physics2d.c kitaq-docs/samples/gb_physics.c -I kitaqgb/lib -I kitaq-docs/samples -o out/gb_physics.gb --profile=dev --rst-disable --stack-bank=fixed
c
// Advance a rectangular body through one physics step.
// Expected: 042: integer pixel coordinates
#include "gb_common.h"
#include "physics2d.h"
KQWorld2D world; KQBody2D boxes[1];
// Create one box at X = 40, set horizontal velocity 2 and zero gravity,
// then display its X coordinate after a single physics step.
void main() {
    m_init();
    m_text(2,3,"PHYSICS");
    kq2d_world_init(&world,boxes,1);
    kq2d_body_init(&boxes[0],40,40,4,4);
    kq2d_body_set_velocity(&boxes[0],2,0);
    world.gravity_x=0; world.gravity_y=0;
    kq2d_step(&world); m_number((u8)boxes[0].x);
    while (1) { m_wait();  }
}
带存储体参数读取 ROM 数据 gb_bank.c

用途: 学习将 ROM 数据复制到 RAM 的参数。

工作原理: far_data_read 使用存储体参数 0、长度 2,将两个字节 7 和 42 复制到 copied。

操作与预期结果: 目标第二字节显示为 042。本示例读取固定 ROM,不演示不同可切换存储体之间的传送。

带存储体参数读取 ROM 数据:目标第二字节显示为 042。本示例读取固定 ROM,不演示不同可切换存储体之间的传送。
显示示例:目标第二字节显示为 042。本示例读取固定 ROM,不演示不同可切换存储体之间的传送。

下载源码 / 验证状态

powershell
.\kitaqgb\kitaqgb.exe kitaqgb/lib/bank.c kitaq-docs/samples/gb_bank.c -I kitaqgb/lib -I kitaq-docs/samples -o out/gb_bank.gb --profile=dev --rst-disable --stack-bank=fixed
c
// Read data using an explicit ROM bank.
// Expected: 042; explicit bank argument 0
#include "gb_common.h"
#include "bank.h"
__prg_rom u8 values[] = {7,42}; u8 copied[2];
// Copy two ROM bytes using bank argument zero and display the second byte.
// This introduces the API; it does not test switching between distinct payload banks.
void main() {
    m_init();
    m_text(2,3,"BANK");
    far_data_read(0,values,copied,2); m_number(copied[1]);
    while (1) { m_wait();  }
}
按资源 ID 加载数据 gb_asset.c

用途: 注册资源位置和长度,再按 ID 取回字节。

工作原理: 资源 0 的类型为 RAW,存储体为 0,指向 {7,42},长度为 2。asset_load_raw 将其复制到两字节 RAM 缓冲区。

操作与预期结果: 第二个字节显示为 042。这演示资源查找和复制,不是图像渲染。

按资源 ID 加载数据:第二个字节显示为 042。这演示资源查找和复制,不是图像渲染。
显示示例:第二个字节显示为 042。这演示资源查找和复制,不是图像渲染。

下载源码 / 验证状态

powershell
.\kitaqgb\kitaqgb.exe kitaqgb/lib/asset.c kitaq-docs/samples/gb_asset.c -I kitaqgb/lib -I kitaq-docs/samples -o out/gb_asset.gb --profile=dev --rst-disable --stack-bank=fixed
c
// Load asset bytes into RAM by asset ID.
// Expected: 042
#include "gb_common.h"
#include "asset.h"
AssetDesc table[1]; __prg_rom u8 values[] = {7,42}; u8 copied[2];
// Register a two-byte raw asset, copy it into RAM and display its second byte.
// This example uses bank zero and a destination sized for the complete payload.
void main() {
    m_init();
    m_text(2,3,"ASSET");
    table[0].type=ASSET_TYPE_RAW; table[0].bank=0;
    table[0].ptr=values; table[0].len=2;
    asset_set_table(0,table,1); asset_load_raw(0,copied,2);
    m_number(copied[1]);
    while (1) { m_wait();  }
}
分配一颗子弹 gb_danmaku.c

用途: 初始化子弹池,并查看生成后的占用数量。

工作原理: danmaku_reset 后,程序调用 danmaku_spawn(40,40,16,0),并显示 dm_count。

操作与预期结果: 001 是已分配子弹数。这里没有子弹更新循环或背景合成器,因此不绘制移动子弹。

分配一颗子弹:001 是已分配子弹数。这里没有子弹更新循环或背景合成器,因此不绘制移动子弹。
显示示例:001 是已分配子弹数。这里没有子弹更新循环或背景合成器,因此不绘制移动子弹。

下载源码 / 验证状态

powershell
.\kitaqgb\kitaqgb.exe kitaqgb/lib/danmaku.c kitaq-docs/samples/gb_danmaku.c -I kitaqgb/lib -I kitaq-docs/samples -o out/gb_danmaku.gb --profile=dev --rst-disable --stack-bank=fixed
c
// Allocate a bullet and inspect the pool count.
// Expected: 001; numeric pool example, no bullet BG compositor
#include "gb_common.h"
#include "danmaku.h"

// Reset the bullet pool, spawn one bullet and display the active count.
// This numeric example does not install the background bullet compositor.
void main() {
    m_init();
    m_text(2,3,"DANMAKU");
    danmaku_reset(); danmaku_spawn(40,40,16,0);
    m_number(dm_count);
    while (1) { m_wait();  }
}
设置 CGB 背景调色板 gb_cgb_palette.c

用途: 使用 Game Boy Color 的 RGB 分量选择背景和文字颜色。

工作原理: 程序在 VBlank 期间关闭 LCD,将调色板 0 的颜色 0 设为白色 (31,31,31),颜色 3 设为紫色 (12,0,22),然后开启显示。

操作与预期结果: 在 CGB 模式运行,可看到白底紫色文字和 042。DMG 单色输出无法展示这些颜色。

设置 CGB 背景调色板:在 CGB 模式运行,可看到白底紫色文字和 042。DMG 单色输出无法展示这些颜色。
显示示例:在 CGB 模式运行,可看到白底紫色文字和 042。DMG 单色输出无法展示这些颜色。

下载源码 / 验证状态

powershell
.\kitaqgb\kitaqgb.exe kitaqgb/lib/cgb_palette.c kitaq-docs/samples/gb_cgb_palette.c -I kitaqgb/lib -I kitaq-docs/samples -o out/gb_cgb_palette.gb --profile=dev --rst-disable --stack-bank=fixed --cgb=cgb
c
// Set Game Boy Color palette entries.
// Expected: 042 in purple on white; CGB mode
#include "gb_common.h"
#include "cgb_palette.h"

// Set palette-zero background colors to white and purple, then display 42.
// Run the ROM in CGB mode to observe the color result.
void main() {
    m_init();
    m_text(2,3,"CGB PALETTE");
    // Disable the LCD during VBlank so both palette bytes remain accessible.
    m_wait(); M_LCDC = 0;
    cgb_bg_rgb(0,0,31,31,31); cgb_bg_rgb(0,3,12,0,22);
    M_LCDC = 0x91;
    m_number(42);
    while (1) { m_wait();  }
}
水平滚动背景 gb_scroll.c

用途: 改变视点位置,无需重绘背景内容。

工作原理: 每帧递增 phase,并传给 Scroll_SetBg。八位位置在 255 之后回绕。

操作与预期结果: SCROLL 一词向左移动。请观察随时间变化的位置;单张截图不能展示移动。

水平滚动背景:SCROLL 一词向左移动。请观察随时间变化的位置;单张截图不能展示移动。
显示示例:SCROLL 一词向左移动。请观察随时间变化的位置;单张截图不能展示移动。

下载源码 / 验证状态

powershell
.\kitaqgb\kitaqgb.exe kitaqgb/lib/scroll.c kitaq-docs/samples/gb_scroll.c -I kitaqgb/lib -I kitaq-docs/samples -o out/gb_scroll.gb --profile=dev --rst-disable --stack-bank=fixed
c
// Scroll the background horizontally.
// Expected: title moves left as SCX increases
#include "gb_common.h"
#include "scroll.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() {
    m_init();
    m_text(2,3,"SCROLL");
    Scroll_SetBg(0,0);
    while (1) { m_wait(); phase++; Scroll_SetBg(phase,0); }
}
将图块更新加入队列 gb_vram_queue.c

用途: 将绘图请求与实际应用它的 VRAM 传送分离。

工作原理: vram_queue_bg_tile 将图块坐标 (3,8) 处的图块 52 加入队列,vram_flush 执行传送。所提供字体中,图块 52 是数字 4。

操作与预期结果: 应在背景图块坐标 (3,8) 看到一个 4。这是图块更新,不是数值计数器。

将图块更新加入队列:应在背景图块坐标 (3,8) 看到一个 4。这是图块更新,不是数值计数器。
显示示例:应在背景图块坐标 (3,8) 看到一个 4。这是图块更新,不是数值计数器。

下载源码 / 验证状态

powershell
.\kitaqgb\kitaqgb.exe kitaqgb/lib/vram.c kitaq-docs/samples/gb_vram_queue.c -I kitaqgb/lib -I kitaq-docs/samples -o out/gb_vram_queue.gb --profile=dev --rst-disable --stack-bank=fixed
c
// Queue a VRAM update and flush it at VBlank.
// Expected: 4 at (3,8)
#include "gb_common.h"
#include "vram.h"

// Queue font tile 52 (the digit 4) at tile coordinate (3,8), then wait for
// VBlank and execute the queued transfer.
void main() {
    m_init();
    m_text(2,3,"VRAM QUEUE");
    vram_init();
    vram_queue_bg_tile(3,8,52); vram_flush();
    while (1) { m_wait();  }
}
通过 OAM DMA 显示精灵 gb_sprite.c

用途: 分配并显示一个独立于背景的精灵。

工作原理: 新初始化的对象池提供槽位 0。程序选择图块 65,设置位置 (70,80),启用 OBJ 显示,并每帧传送影子 OAM。

操作与预期结果: 指定精灵位置出现静止字母 A。游戏中应在使用返回 ID 前检查分配失败。

通过 OAM DMA 显示精灵:指定精灵位置出现静止字母 A。游戏中应在使用返回 ID 前检查分配失败。
显示示例:指定精灵位置出现静止字母 A。游戏中应在使用返回 ID 前检查分配失败。

下载源码 / 验证状态

powershell
.\kitaqgb\kitaqgb.exe kitaqgb/lib/sprite.c kitaq-docs/samples/gb_sprite.c -I kitaqgb/lib -I kitaq-docs/samples -o out/gb_sprite.gb --profile=dev --rst-disable --stack-bank=fixed
c
// Display a sprite through the OAM DMA path.
// Expected: letter A at sprite position
#include "gb_common.h"
#include "sprite.h"

// Allocate the first sprite, choose font tile 65 (A), set its position and
// enable sprite display. Flush shadow OAM after each frame wait.
void main() {
    m_init();
    m_text(2,3,"SPRITE");
    // The freshly reset pool guarantees slot zero for this first allocation.
    // In a running game, check the allocation-failure sentinel before using the returned ID.
    sprite_init(); sprite_set_tile(sprite_alloc(),65);
    sprite_set_pos(0,70,80); M_LCDC=(u8)(M_LCDC|2);
    while (1) { m_wait(); sprite_flush_oam_now(); }
}
生成可复现的随机值 gb_rng.c

用途: 通过种子复现伪随机序列。

工作原理: rng_seed(1234) 初始化生成器,再调用一次 rng_range(10)。结果不包含上限 10。

操作与预期结果: 画面显示 000 至 009 中的一个值。以相同种子重新启动同一程序会复现该值;它不会每帧变化。

生成可复现的随机值:画面显示 000 至 009 中的一个值。以相同种子重新启动同一程序会复现该值;它不会每帧变化。
显示示例:画面显示 000 至 009 中的一个值。以相同种子重新启动同一程序会复现该值;它不会每帧变化。

下载源码 / 验证状态

powershell
.\kitaqgb\kitaqgb.exe kitaqgb/lib/rng.c kitaq-docs/samples/gb_rng.c -I kitaqgb/lib -I kitaq-docs/samples -o out/gb_rng.gb --profile=dev --rst-disable --stack-bank=fixed
c
// Generate a repeatable pseudorandom value from a seed.
// Expected: value 000 through 009; same seed repeats
#include "gb_common.h"
#include "rpg.h"

// Seed the generator with 1234 and display one value in [0,10).
// Restarting with the same seed reproduces the sequence.
void main() {
    m_init();
    m_text(2,3,"RNG");
    rng_seed(1234); m_number(rng_range(10));
    while (1) { m_wait();  }
}
保存游戏状态标志 gb_flags.c

用途: 用标志编号表示事件是否完成等布尔状态。

工作原理: 程序清除标志 12,再设置并通过 flag_get 读回。

操作与预期结果: 001 表示标志 12 为真。本示例操作 RAM 标志,不会在断电后保留。

保存游戏状态标志:001 表示标志 12 为真。本示例操作 RAM 标志,不会在断电后保留。
显示示例:001 表示标志 12 为真。本示例操作 RAM 标志,不会在断电后保留。

下载源码 / 验证状态

powershell
.\kitaqgb\kitaqgb.exe kitaqgb/lib/flags.c kitaq-docs/samples/gb_flags.c -I kitaqgb/lib -I kitaq-docs/samples -o out/gb_flags.gb --profile=dev --rst-disable --stack-bank=fixed
c
// Read and write RAM-backed game-state flags.
// Expected: 001
#include "gb_common.h"
#include "rpg.h"

// Clear and set flag 12, then display its Boolean value. This does not write
// a save file or demonstrate persistence across power cycles.
void main() {
    m_init();
    m_text(2,3,"FLAGS");
    flag_clear(12); flag_set(12); m_number(flag_get(12));
    while (1) { m_wait();  }
}
将圆形推进一个物理步长 gb_circle.c

用途: 设置圆形物体,并根据速度更新位置。

工作原理: 活动圆形半径为 4,位置 (40,40),速度 (2,0)。重力为零,linear_damping_q8 为 255;kq2dc_step 执行一次。

操作与预期结果: 042 是得到的整数 X 坐标。本示例不绘制圆形轮廓或碰撞动画。

将圆形推进一个物理步长:042 是得到的整数 X 坐标。本示例不绘制圆形轮廓或碰撞动画。
显示示例:042 是得到的整数 X 坐标。本示例不绘制圆形轮廓或碰撞动画。

下载源码 / 验证状态

powershell
.\kitaqgb\kitaqgb.exe kitaqgb/lib/physics2d_circle.c kitaq-docs/samples/gb_circle.c -I kitaqgb/lib -I kitaq-docs/samples -o out/gb_circle.gb --profile=dev --rst-disable --stack-bank=fixed
c
// Advance a circular body through one physics step.
// Expected: 042 after one step; this example uses integer pixels
#include "gb_common.h"
#include "physics2d_circle.h"
KQCircleBody2D balls[1]; KQCircleWorld2D world;
// Activate one circle at X = 40 with horizontal velocity 2 and zero gravity.
// Advance one step and display its resulting integer-pixel X coordinate.
void main() {
    m_init();
    m_text(2,3,"CIRCLE");
    kq2dc_world_init(&world,balls,1);
    balls[0].active=1; balls[0].x=40; balls[0].y=40;
    balls[0].radius=4; balls[0].inv_mass_q8=256;
    balls[0].vx=2; balls[0].vy=0;
    world.gravity_x=0; world.gravity_y=0; world.linear_damping_q8=255;
    kq2dc_step(&world); m_number((u8)balls[0].x);
    while (1) { m_wait();  }
}
每帧更新音效 gb_sound.c

用途: 启动音效流,并通过定期帧更新推进播放。

工作原理: Audio_Init 后,Audio_PlaySFX 在通道 ID 1 上启动 tone。主循环每帧调用一次 Audio_Update。

操作与预期结果: 播放短脉冲音效,同时保持显示 042。该数值是固定标记,不是音量或播放位置。请通过音频输出或录音判断声音。

每帧更新音效:播放短脉冲音效,同时保持显示 042。该数值是固定标记,不是音量或播放位置。请通过音频输出或录音判断声音。
显示示例:播放短脉冲音效,同时保持显示 042。该数值是固定标记,不是音量或播放位置。请通过音频输出或录音判断声音。

下载源码 / 验证状态

powershell
.\kitaqgb\kitaqgb.exe kitaqgb/lib/audio_hwregs_gb.c kitaqgb/lib/audio.c kitaq-docs/samples/gb_sound.c -I kitaqgb/lib -I kitaq-docs/samples -o out/gb_sound.gb --profile=dev --rst-disable --stack-bank=fixed
c
// Play a Game Boy pulse-channel sound effect.
// Expected: 042 and a short pulse tone
#include "gb_common.h"
#include "audio.h"
__prg_rom u8 tone[] = { 24,0xF0,24,0xE0,24,0xC0,24,0xA0,24,0x80,24,0x60,0 };
// Start a pulse-channel effect and service Audio_Update once per frame
// so the effect stream advances while 42 remains on screen.
void main() {
    m_init();
    m_text(2,3,"SOUND");
    Audio_Init(); Audio_PlaySFX(tone,1); m_number(42);
    while (1) { m_wait(); Audio_Update(); }
}

编译器内建函数字典

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

asset.h — 素材 ID 与数据描述符

素材库通过8位ID查找ROM存储体中的素材。请包含asset.c;FC版还需要bank.c。紧凑排列的AssetDesc占6字节,依次保存1字节类型、1字节素材存储体编号、16位地址和以字节计的长度。描述符表与素材数据可以位于不同的存储体。

查找前请先登记表。登记信息是共享全局状态,不要在查找过程中同时修改。库会检查ID和asset_get的输出指针,但不会验证已登记的表、素材地址或卡带内容是否正确。

查找RAW或TILES素材,用__vram_copy传送全部desc.len字节。调用前必须选中素材数据所在的ROM存储体;GB版直接使用desc.ptr,传送时不会选择desc.bank。

__farmemcpy

临时切换ROM存储体,按地址递增顺序从src向dst复制len字节,然后恢复编译器保存的ROM存储体。在此平台上,__farmemcpy与__far_memcpy使用相同的实现。

语法

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

参数

dst

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

bank

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

src

指向所选ROM存储体中可读字节的近指针,指针本身不含存储体编号。

len

字节数,范围0–65535。为0时不复制内存,但仍会执行存储体选择和恢复。

返回值

无(void)。

使用条件与限制

整个源范围必须在预期ROM映射中可读,整个目标范围必须可写。复制不会在存储体窗口边界自动切换到下一存储体,不提供memmove式的重叠处理,也不控制VRAM安全写入时机。写VRAM请使用图形传送API。

示例将调用代码放在固定存储体0中,防止切换源存储体后正在执行的代码不可见。传送为同步操作。不要在传送中通过中断再次执行切换存储体的复制,也不要认为此辅助函数能保证整个程序的中断安全。

GB辅助例程根据编译器保存的ROM存储体状态恢复映射。请用__bankswitch维护该状态;直接写映射器不会更新它。HRAM中的返回存储体保存位置是共享的,嵌套传送覆盖此位置会造成风险。

使用示例

c
__farmemcpy(buffer,payload_bank,payload_pointer,32);
expect(*asset_bank_marker==0xA1,27); // Source bank 2 was selected only for the copy.
for(i=0;i<32;i++) expect(buffer[(__safe_index u8)i]==expected_shape_byte(i),28);

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

预期结果

在初始选中存储体1的状态下,从素材存储体2复制两个图案,共32字节。程序逐字节比较,检查存储体1已恢复,并确认零长度复制不会改变0x55哨兵值。

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

GB中,(16,64)的蓝色三角形和方框显示__farmemcpy结果,(16,96)的绿色一对显示__far_memcpy结果。每对占16×8像素。CGB显示上述颜色,DMG在白底上以黑色显示相同形状。

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

__farmemcpy: DMG
DMG
__farmemcpy: CGB
CGB

完整示例程序

构建命令

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqgb\kitaqgb.exe .\kitaq-docs\samples\api-examples\gb\asset_banks.c -I .\kitaqgb\lib -I .\kitaq-docs\samples -o .\out\asset_banks.gb --profile=dev --rst-disable --stack-bank=fixed --cgb=cgb --cart=mbc5 --romsize=64k --no-cache --no-disasm
实现与源码

kitaqgb/lib/asset.h:17

__vram_copy

__vram_copy是编译器中__vram_memcpy的别名。长度同样以字节计,使用相同的即时CPU传输及LCD访问规则。

立即按地址递增顺序将src的len字节复制到dst,完成后才返回,不将操作加入队列。源地址和目标地址每次增加1字节,不处理图块映射表行跨度或图块格式转换。

语法

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

参数

dst

目标首字节地址,通常位于VRAM 0x8000–0x9FFF。整个范围必须有效。CGB使用当前VRAM存储体;映射表区域的存储体0存放图块编号,存储体1存放属性。这些函数不根据LCDC选择映射表,不在行末换行,也不分配VRAM。

src

源字节指针,在整个调用期间都必须有效且映射正确。数据立即读取,不为后续flush保留,也不会自动切换源存储体。请使用不重叠的范围;这种向前复制不提供memmove语义。

len

连续字节数,范围0至65535,不是图块数。整个范围应位于有效源数据和可写目标区域内;API不检查边界,也不防止十六位地址回绕。

返回值

无(void)。

使用条件与限制

LCD开启时,每次写入前执行DI并等待STAT模式0或1,写入后执行EI。LCD关闭时使用普通CPU循环。调用可能跨越多帧,不保留进入时的中断禁用状态,也不设置DMA寄存器。长度0不传输数据。

使用示例

c
__vram_copy(0x9862,strip,3); // The copy alias uses the same CPU transfer.

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

预期结果

本例用__vram_copy在图块坐标(2,3)、像素坐标(16,24)写入3字节映射表数据。24×8区域应从左到右显示[1,2,3]图案,相邻单元保持空白。完整程序将每种受测复制或填充放在独立的一行。

附带的自制图形中,图块1为8×8实心正方形,2只填左侧四列,3为左对齐的三角形,每行加宽1像素,从1像素增至8像素。DMG显示白底黑图形;CGB按操作区分颜色,文字保持黑色。

像素(16,88)的正方形应在安全版和无等待版的零长度调用后保留。LCD开启时,安全复制和填充在(16,96)和(32,96)添加正方形。FAILED CHECKS应为000。模拟器测试同时比较绘制像素和计数值。本例CGB使用存储体0;此处未测试其他存储体或实机。

CGB模式下,从MEMCPY到FILL UNSAFE各行依次循环使用红、蓝、绿。ZERO COUNT正方形为红色;LCD ON左侧正方形为蓝色,右侧为绿色,文字保持黑色。测试还检查每个绘制像素是否为指定颜色。

__vram_copy: DMG
DMG
__vram_copy: CGB
CGB

完整示例程序

构建命令

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqgb\kitaqgb.exe .\kitaq-docs\samples\api-examples\gb\vram_memory_shapes.c -I .\kitaqgb\lib -I .\kitaq-docs\samples -o .\out\vram_memory_shapes.gb --profile=dev --rst-disable --stack-bank=fixed --cgb=cgb --no-cache --no-disasm
实现与源码

kitaqgb/lib/asset.h:18

bank.h — 考虑 bank 的代码与数据访问

ROM 存储体是映射到 CPU 地址空间的一块卡带存储区域。不同存储体可以出现在同一个 CPU 地址,因此仅靠 16 位指针无法确定跨存储体数据。BankPtr 是紧凑的三字节描述符,包含一字节存储体编号和两字节指针。使用 farptr_make 构造它,再通过 farptr_read8、farptr_read16 或 farptr_read 访问数据。

GB 示例使用 MBC5 卡带:固定存储体 0 的代码始终可通过 $0000–$3FFF 访问,存储体 1、2、3 则映射到 $4000–$7FFF。请将 bank_switch 及调用后继续执行的代码放在固定 ROM 中。查询库保存的编号前,应先用 bank_switch 初始化记录。GB 的 far_call 接受回调指针,而 __farcall 要求函数名。

__farcall_ptr

调用指定存储体中运行时地址所指向的无参数回调。固定 ROM 辅助代码将每次调用的原存储体压入 CPU 栈,并在返回时恢复。此内置函数不返回值。

语法

c
void __farcall_ptr(u8 bank, const void* func);

参数

bank

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

func

无参数回调的有效可执行地址,可由函数名或运行时指针提供。运行时不会检查空指针或错误的存储体与地址组合。

返回值

无(void)。

使用条件与限制

这些调用不传递用户参数,也不会自动使回调可在中断中重入。请正确管理回调自身的数据、栈和中断使用。恢复存储体的调用路径已测试嵌套和重复调用;此示例不覆盖实机或任意中断交错。

这些 GB 存储体接口只携带 8 位编号,不设置 MBC5 的第 9 位。仍须遵守卡带规则,包括 mapper 对编号 0 的处理。示例明确使用 MBC5 存储体 1–3;不要混用未记录的 mapper 直接写入。

使用示例

c
requested_bank=3;requested_callback=callback;
__farcall_ptr(requested_bank,requested_callback); // Runtime bank and address.
bank_expect(callback_count==3 && callback_byte==0xD3,17);

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

预期结果

在完整程序的此处,__farcall_ptr 使 callback_count 达到 3。回调从存储体 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 为 03:far_call、__farcall 和 __farcall_ptr 各执行一次回调。

__farcall_ptr: DMG
DMG
__farcall_ptr: CGB
CGB

完整示例程序

构建命令

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqgb\kitaqgb.exe .\kitaq-docs\samples\api-examples\gb\banked_data_calls.c -I .\kitaqgb\lib -I .\kitaq-docs\samples -o .\out\banked_data_calls.gb --profile=dev --rst-disable --stack-bank=fixed --cgb=cgb --cart=mbc5 --romsize=64k --no-cache --no-disasm
实现与源码

kitaqgb/lib/bank.h:15

kitaqgb/kitaqgb/CodeGenerator.cs:8335

csharp
if (funcName == "__farcall_ptr")
            {
                if (args.Length != 2) Error(expr, "__farcall_ptr(bank, func) expects 2 arguments");
                CType callbackType = TypeOf(args[1]);
                if (callbackType.IsPointer) callbackType = callbackType.Subtype;
                if (callbackType != null && callbackType.IsFunction &&
                    (callbackType.ParamTypes ?? Array.Empty<CType>()).Length != 0)
                    Error(expr, "__farcall_ptr requires a callback with no parameters");
                CompileIntoA(args[0]);
                EmitAsm("PUSH_AF");
                CompileIntoHL(args[1]);
                EmitAsm("POP_AF");
                UseFarCallPointerBank0Helper = true;
                EmitAsm("CALL", new AsmOperand("__kq_farcall_pointer_bank0", AddressMode.Absolute));
                RecordCallEdge(expr, "<indirect-far>", -1, "farcall_pointer", viaThunk: true,
                    viaFarcall: true, actualArgSizes: new int[0], expectedArgSizes: new int[0]);
                return;
            }
__farpeek16

临时选择存储体,从 addr 和 addr+1 读取小端序 16 位值,然后恢复原存储体。这是 far_data_read16 使用的内置函数。

语法

c
u16 __farpeek16(u8 bank, const void* addr);

参数

bank

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

addr

指向所选ROM存储体中可读字节的近指针,指针本身不含存储体编号。

返回值

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

使用条件与限制

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

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

这些 GB 存储体接口只携带 8 位编号,不设置 MBC5 的第 9 位。仍须遵守卡带规则,包括 mapper 对编号 0 的处理。示例明确使用 MBC5 存储体 1–3;不要混用未记录的 mapper 直接写入。

使用示例

c
bank_expect(__farpeek16(3,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 为 03:far_call、__farcall 和 __farcall_ptr 各执行一次回调。

__farpeek16: DMG
DMG
__farpeek16: CGB
CGB

完整示例程序

构建命令

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqgb\kitaqgb.exe .\kitaq-docs\samples\api-examples\gb\banked_data_calls.c -I .\kitaqgb\lib -I .\kitaq-docs\samples -o .\out\banked_data_calls.gb --profile=dev --rst-disable --stack-bank=fixed --cgb=cgb --cart=mbc5 --romsize=64k --no-cache --no-disasm
实现与源码

kitaqgb/lib/bank.h:13

kitaqgb/kitaqgb/CodeGenerator.cs:8295

csharp
if (funcName == "__farpeek16")
            {
                if (args.Length != 2) Error(expr, "__farpeek16(bank, addr) expects 2 arguments");

                CompileIntoA(args[0]); EmitPushAAsWord(); // bank
                CompileIntoHL(args[1]); EmitAsm("PUSH_HL"); // src
                EmitAsm("ADD_SP_IMM", new AsmOperand(-2, AddressMode.Relative)); // scratch
                // At SP: scratch, saved address, saved bank; each occupies two bytes.

                EmitAsm("LD_HL_SP_IMM", new AsmOperand(0, AddressMode.Relative));
                EmitAsm("PUSH_HL"); // dst = &scratch

                EmitAsm("LD_HL_SP_IMM", new AsmOperand(4, AddressMode.Relative));
                EmitAsm("LD_A_HL");
                EmitAsm("LD_E_A");
                EmitAsm("INC_HL");
                EmitAsm("LD_A_HL");
                EmitAsm("LD_D_A");

                EmitAsm("LD_HL_SP_IMM", new AsmOperand(6, AddressMode.Relative));
                EmitAsm("LD_A_HL"); // bank

                EmitAsm("POP_HL"); // dst
                EmitAsm("LD_BC_IMM", new AsmOperand(2, AddressMode.Immediate16));
                EmitCallFarMemcpyHelper();

                EmitAsm("LD_HL_SP_IMM", new AsmOperand(0, AddressMode.Relative));
                EmitAsm("LD_A_HL");
                EmitAsm("LD_E_A");
                EmitAsm("INC_HL");
                EmitAsm("LD_A_HL");
                EmitAsm("LD_D_A");
                EmitAsm("LD_H_D");
                EmitAsm("LD_L_E");
                EmitAsm("ADD_SP_IMM", new AsmOperand(6, AddressMode.Relative));
                LastCallReturnsHL = true;
                return;
            }
__farpeek8

临时选择指定存储体,从 CPU 地址读取一个字节后恢复原存储体。这是 far_data_read8 使用的内置函数。

语法

c
u8 __farpeek8(u8 bank, const void* addr);

参数

bank

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

addr

指向所选ROM存储体中可读字节的近指针,指针本身不含存储体编号。

返回值

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

使用条件与限制

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

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

这些 GB 存储体接口只携带 8 位编号,不设置 MBC5 的第 9 位。仍须遵守卡带规则,包括 mapper 对编号 0 的处理。示例明确使用 MBC5 存储体 1–3;不要混用未记录的 mapper 直接写入。

使用示例

c
bank_expect(__farpeek8(3,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 为 03:far_call、__farcall 和 __farcall_ptr 各执行一次回调。

__farpeek8: DMG
DMG
__farpeek8: CGB
CGB

完整示例程序

构建命令

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqgb\kitaqgb.exe .\kitaq-docs\samples\api-examples\gb\banked_data_calls.c -I .\kitaqgb\lib -I .\kitaq-docs\samples -o .\out\banked_data_calls.gb --profile=dev --rst-disable --stack-bank=fixed --cgb=cgb --cart=mbc5 --romsize=64k --no-cache --no-disasm
实现与源码

kitaqgb/lib/bank.h:12

kitaqgb/kitaqgb/CodeGenerator.cs:8262

csharp
if (funcName == "__farpeek8")
            {
                if (args.Length != 2) Error(expr, "__farpeek8(bank, addr) expects 2 arguments");

                CompileIntoA(args[0]); EmitPushAAsWord(); // bank
                CompileIntoHL(args[1]); EmitAsm("PUSH_HL"); // src
                EmitAsm("ADD_SP_IMM", new AsmOperand(-2, AddressMode.Relative)); // scratch
                // At SP: scratch, saved address, saved bank; each occupies two bytes.

                EmitAsm("LD_HL_SP_IMM", new AsmOperand(0, AddressMode.Relative));
                EmitAsm("PUSH_HL"); // dst = &scratch

                EmitAsm("LD_HL_SP_IMM", new AsmOperand(4, AddressMode.Relative));
                EmitAsm("LD_A_HL");
                EmitAsm("LD_E_A");
                EmitAsm("INC_HL");
                EmitAsm("LD_A_HL");
                EmitAsm("LD_D_A");

                EmitAsm("LD_HL_SP_IMM", new AsmOperand(6, AddressMode.Relative));
                EmitAsm("LD_A_HL"); // bank

                EmitAsm("POP_HL"); // dst
                EmitAsm("LD_BC_IMM", new AsmOperand(1, AddressMode.Immediate16));
                EmitCallFarMemcpyHelper();

                EmitAsm("LD_HL_SP_IMM", new AsmOperand(0, AddressMode.Relative));
                EmitAsm("LD_A_HL");
                EmitAsm("ADD_SP_IMM", new AsmOperand(6, AddressMode.Relative));
                return;
            }

cgb_palette.h — CGB 背景与 OBJ 调色板

cgb_palette.h和cgb_palette.c提供RGB15打包及CGB调色板即时更新。背景/窗口用BG函数,精灵用OBJ函数。单色及RGB分量函数修改一色,范围函数写入连续颜色字,整组调色板函数写入4个字。raw范围函数使用绝对颜色槽位,寄存器内置函数使用字节位置。通过地图属性或OAM另行指定调色板归属。请像完整示例一样,只将实现包含到程序中一次。

调色板操作和带模式保护的寄存器写入在DMG模式下不写入任何内容。这里的safe指CGB模式检查,不代表等待PPU可访问或禁止中断。请在LCD关闭时设置,或在可访问的PPU阶段内完成足够短的更新。函数不等待VBlank,也不保护多字节更新免受中断影响。

背景/窗口和OBJ调色板属于独立存储区。库的颜色写入会启用索引自动递增,并保留修改后的索引寄存器。OBJ颜色编号0虽可存储和读取,但绘制精灵时仍为透明。

__cgb_is_cgb

返回编译器的CGB模式布尔值。明确指定--cgb=dmg时折叠为0,--cgb=cgb_only时为1;兼容两种模式的--cgb=cgb使用运行时检测。它表示所选执行模式,不是对实体机全部功能的检测。

语法

c
u8 __cgb_is_cgb();

返回值

u8:DMG模式为0,CGB模式为1。

使用示例

c
color_mode=__cgb_is_cgb(); // 0 in DMG mode, 1 in CGB mode for this dual-mode ROM.
if (color_mode>1) failures++;

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

预期结果

本程序演示同一图块图案如何分别使用BG和OBJ的调色板。每个三角形为8×8像素,从顶行左侧1个像素逐行扩展到底行8个像素。在x=16、32、48、64、80、96、112处,y=24的BG行依次为红、蓝、绿、红、蓝、绿、红;y=64的OBJ行依次为蓝、绿、红、蓝、绿、红、蓝。各列依次使用单色、RGB分量、索引范围、整组调色板、绝对范围、标准寄存器名及寄存器别名写入。

y=88处的7个8×8格依次显示:红色三角形(x=16)、存储体1中线宽1像素的蓝色方框(32)、水平翻转的绿色三角形(48)、垂直翻转的红色三角形(64)、双向翻转的蓝色三角形(80)、位于红色精灵前方且颜色0区域透出红色的绿色三角形(96),以及关闭BG优先级后覆盖三角形的实心红色精灵(112)。坐标以屏幕左上角为原点,单位为像素。空白处为白色,文字为黑色。

FAILED CHECKS 000表示RGB掩码、含输入2的布尔标志、调色板索引回绕、跨调色板写入、在槽位31截断、null/零长度不写入,以及LCD关闭时的颜色读回检查均通过。测试4种组合:双模式ROM分别在DMG和CGB运行、DMG专用ROM在DMG运行、CGB专用ROM在CGB运行。DMG下CGB MODE为0,三角形均为黑色,存储体及翻转属性不影响显示,最下行最后两格为黑色方块。CGB测试中CGB MODE为1。未测试实机及LCD显示期间的调色板访问冲突。

__cgb_is_cgb: --cgb=cgb / DMG
--cgb=cgb / DMG
__cgb_is_cgb: --cgb=cgb / CGB
--cgb=cgb / CGB
__cgb_is_cgb: --cgb=dmg / DMG
--cgb=dmg / DMG
__cgb_is_cgb: --cgb=cgb_only / CGB
--cgb=cgb_only / CGB

完整示例程序

构建命令

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqgb\kitaqgb.exe .\kitaq-docs\samples\api-examples\gb\cgb_palette_attributes.c -I .\kitaqgb\lib -I .\kitaq-docs\samples -o .\out\cgb_palette_attributes.gb --profile=dev --rst-disable --stack-bank=fixed --cgb=cgb --no-cache --no-disasm
实现与源码

kitaqgb/lib/cgb_palette.h:8

声明注释(原文)
Safe here means CGB hardware gating. Palette data writes still require an accessible
PPU phase (for example LCD off or a sufficiently short VBlank update).
__cgb_safe_set_bcpd

向CGB的背景/窗口调色板数据寄存器BCPD/BGPD($FF69)写入一个字节。__cgb_safe_set_bgpd是同一寄存器的别名。对应的索引寄存器选择字节位置;完整RGB15颜色需要低、高两个字节。

语法

c
void __cgb_safe_set_bcpd(u8 value);

参数

value

一个字节,0~255。依次写RGB15的低字节和高字节;先启用索引自动递增,或逐次明确选择位置。数据写入本身不会启用自动递增。

返回值

无(void)。

使用条件与限制

调色板操作和带模式保护的寄存器写入在DMG模式下不写入任何内容。这里的safe指CGB模式检查,不代表等待PPU可访问或禁止中断。请在LCD关闭时设置,或在可访问的PPU阶段内完成足够短的更新。函数不等待VBlank,也不保护多字节更新免受中断影响。

使用示例

c
__cgb_safe_set_bcpd(0xE0); __cgb_safe_set_bcpd(0x03); // Green word 0x03E0, low then high.
expect_color(0,27,992);

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

预期结果

本程序演示同一图块图案如何分别使用BG和OBJ的调色板。每个三角形为8×8像素,从顶行左侧1个像素逐行扩展到底行8个像素。在x=16、32、48、64、80、96、112处,y=24的BG行依次为红、蓝、绿、红、蓝、绿、红;y=64的OBJ行依次为蓝、绿、红、蓝、绿、红、蓝。各列依次使用单色、RGB分量、索引范围、整组调色板、绝对范围、标准寄存器名及寄存器别名写入。

y=88处的7个8×8格依次显示:红色三角形(x=16)、存储体1中线宽1像素的蓝色方框(32)、水平翻转的绿色三角形(48)、垂直翻转的红色三角形(64)、双向翻转的蓝色三角形(80)、位于红色精灵前方且颜色0区域透出红色的绿色三角形(96),以及关闭BG优先级后覆盖三角形的实心红色精灵(112)。坐标以屏幕左上角为原点,单位为像素。空白处为白色,文字为黑色。

FAILED CHECKS 000表示RGB掩码、含输入2的布尔标志、调色板索引回绕、跨调色板写入、在槽位31截断、null/零长度不写入,以及LCD关闭时的颜色读回检查均通过。测试4种组合:双模式ROM分别在DMG和CGB运行、DMG专用ROM在DMG运行、CGB专用ROM在CGB运行。DMG下CGB MODE为0,三角形均为黑色,存储体及翻转属性不影响显示,最下行最后两格为黑色方块。CGB测试中CGB MODE为1。未测试实机及LCD显示期间的调色板访问冲突。

__cgb_safe_set_bcpd: --cgb=cgb / DMG
--cgb=cgb / DMG
__cgb_safe_set_bcpd: --cgb=cgb / CGB
--cgb=cgb / CGB
__cgb_safe_set_bcpd: --cgb=dmg / DMG
--cgb=dmg / DMG
__cgb_safe_set_bcpd: --cgb=cgb_only / CGB
--cgb=cgb_only / CGB

完整示例程序

构建命令

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqgb\kitaqgb.exe .\kitaq-docs\samples\api-examples\gb\cgb_palette_attributes.c -I .\kitaqgb\lib -I .\kitaq-docs\samples -o .\out\cgb_palette_attributes.gb --profile=dev --rst-disable --stack-bank=fixed --cgb=cgb --no-cache --no-disasm
实现与源码

kitaqgb/lib/cgb_palette.h:10

__cgb_safe_set_bcps

写入CGB的背景/窗口调色板字节索引寄存器BCPS/BGPI($FF68)。__cgb_safe_set_bgpi操作同一寄存器。它选择下一次数据寄存器访问的位置,本身不写入颜色。

语法

c
void __cgb_safe_set_bcps(u8 value);

参数

value

位0~5指定字节索引0~63;位7为1时,数据写入后自动递增。位6未使用。颜色槽位n从字节2n开始。索引在63之后回到0,直接操作寄存器时须自行控制范围。

返回值

无(void)。

使用条件与限制

调色板操作和带模式保护的寄存器写入在DMG模式下不写入任何内容。这里的safe指CGB模式检查,不代表等待PPU可访问或禁止中断。请在LCD关闭时设置,或在可访问的PPU阶段内完成足够短的更新。函数不等待VBlank,也不保护多字节更新免受中断影响。

使用示例

c
__cgb_safe_set_bcps(0xB6); // BG byte 54, with auto-increment enabled (slot 27 low byte).

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

预期结果

本程序演示同一图块图案如何分别使用BG和OBJ的调色板。每个三角形为8×8像素,从顶行左侧1个像素逐行扩展到底行8个像素。在x=16、32、48、64、80、96、112处,y=24的BG行依次为红、蓝、绿、红、蓝、绿、红;y=64的OBJ行依次为蓝、绿、红、蓝、绿、红、蓝。各列依次使用单色、RGB分量、索引范围、整组调色板、绝对范围、标准寄存器名及寄存器别名写入。

y=88处的7个8×8格依次显示:红色三角形(x=16)、存储体1中线宽1像素的蓝色方框(32)、水平翻转的绿色三角形(48)、垂直翻转的红色三角形(64)、双向翻转的蓝色三角形(80)、位于红色精灵前方且颜色0区域透出红色的绿色三角形(96),以及关闭BG优先级后覆盖三角形的实心红色精灵(112)。坐标以屏幕左上角为原点,单位为像素。空白处为白色,文字为黑色。

FAILED CHECKS 000表示RGB掩码、含输入2的布尔标志、调色板索引回绕、跨调色板写入、在槽位31截断、null/零长度不写入,以及LCD关闭时的颜色读回检查均通过。测试4种组合:双模式ROM分别在DMG和CGB运行、DMG专用ROM在DMG运行、CGB专用ROM在CGB运行。DMG下CGB MODE为0,三角形均为黑色,存储体及翻转属性不影响显示,最下行最后两格为黑色方块。CGB测试中CGB MODE为1。未测试实机及LCD显示期间的调色板访问冲突。

__cgb_safe_set_bcps: --cgb=cgb / DMG
--cgb=cgb / DMG
__cgb_safe_set_bcps: --cgb=cgb / CGB
--cgb=cgb / CGB
__cgb_safe_set_bcps: --cgb=dmg / DMG
--cgb=dmg / DMG
__cgb_safe_set_bcps: --cgb=cgb_only / CGB
--cgb=cgb_only / CGB

完整示例程序

构建命令

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqgb\kitaqgb.exe .\kitaq-docs\samples\api-examples\gb\cgb_palette_attributes.c -I .\kitaqgb\lib -I .\kitaq-docs\samples -o .\out\cgb_palette_attributes.gb --profile=dev --rst-disable --stack-bank=fixed --cgb=cgb --no-cache --no-disasm
实现与源码

kitaqgb/lib/cgb_palette.h:9

__cgb_safe_set_ocpd

向CGB的精灵(OBJ)调色板数据寄存器OCPD/OBPD($FF6B)写入一个字节。__cgb_safe_set_obpd是同一寄存器的别名。对应的索引寄存器选择字节位置;完整RGB15颜色需要低、高两个字节。

语法

c
void __cgb_safe_set_ocpd(u8 value);

参数

value

一个字节,0~255。依次写RGB15的低字节和高字节;先启用索引自动递增,或逐次明确选择位置。数据写入本身不会启用自动递增。

返回值

无(void)。

使用条件与限制

调色板操作和带模式保护的寄存器写入在DMG模式下不写入任何内容。这里的safe指CGB模式检查,不代表等待PPU可访问或禁止中断。请在LCD关闭时设置,或在可访问的PPU阶段内完成足够短的更新。函数不等待VBlank,也不保护多字节更新免受中断影响。

使用示例

c
__cgb_safe_set_ocpd(31); __cgb_safe_set_ocpd(0); // Red sprite color.
expect_color(1,27,31);

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

预期结果

本程序演示同一图块图案如何分别使用BG和OBJ的调色板。每个三角形为8×8像素,从顶行左侧1个像素逐行扩展到底行8个像素。在x=16、32、48、64、80、96、112处,y=24的BG行依次为红、蓝、绿、红、蓝、绿、红;y=64的OBJ行依次为蓝、绿、红、蓝、绿、红、蓝。各列依次使用单色、RGB分量、索引范围、整组调色板、绝对范围、标准寄存器名及寄存器别名写入。

y=88处的7个8×8格依次显示:红色三角形(x=16)、存储体1中线宽1像素的蓝色方框(32)、水平翻转的绿色三角形(48)、垂直翻转的红色三角形(64)、双向翻转的蓝色三角形(80)、位于红色精灵前方且颜色0区域透出红色的绿色三角形(96),以及关闭BG优先级后覆盖三角形的实心红色精灵(112)。坐标以屏幕左上角为原点,单位为像素。空白处为白色,文字为黑色。

FAILED CHECKS 000表示RGB掩码、含输入2的布尔标志、调色板索引回绕、跨调色板写入、在槽位31截断、null/零长度不写入,以及LCD关闭时的颜色读回检查均通过。测试4种组合:双模式ROM分别在DMG和CGB运行、DMG专用ROM在DMG运行、CGB专用ROM在CGB运行。DMG下CGB MODE为0,三角形均为黑色,存储体及翻转属性不影响显示,最下行最后两格为黑色方块。CGB测试中CGB MODE为1。未测试实机及LCD显示期间的调色板访问冲突。

__cgb_safe_set_ocpd: --cgb=cgb / DMG
--cgb=cgb / DMG
__cgb_safe_set_ocpd: --cgb=cgb / CGB
--cgb=cgb / CGB
__cgb_safe_set_ocpd: --cgb=dmg / DMG
--cgb=dmg / DMG
__cgb_safe_set_ocpd: --cgb=cgb_only / CGB
--cgb=cgb_only / CGB

完整示例程序

构建命令

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqgb\kitaqgb.exe .\kitaq-docs\samples\api-examples\gb\cgb_palette_attributes.c -I .\kitaqgb\lib -I .\kitaq-docs\samples -o .\out\cgb_palette_attributes.gb --profile=dev --rst-disable --stack-bank=fixed --cgb=cgb --no-cache --no-disasm
实现与源码

kitaqgb/lib/cgb_palette.h:12

__cgb_safe_set_ocps

写入CGB的精灵(OBJ)调色板字节索引寄存器OCPS/OBPI($FF6A)。__cgb_safe_set_obpi操作同一寄存器。它选择下一次数据寄存器访问的位置,本身不写入颜色。

语法

c
void __cgb_safe_set_ocps(u8 value);

参数

value

位0~5指定字节索引0~63;位7为1时,数据写入后自动递增。位6未使用。颜色槽位n从字节2n开始。索引在63之后回到0,直接操作寄存器时须自行控制范围。

返回值

无(void)。

使用条件与限制

调色板操作和带模式保护的寄存器写入在DMG模式下不写入任何内容。这里的safe指CGB模式检查,不代表等待PPU可访问或禁止中断。请在LCD关闭时设置,或在可访问的PPU阶段内完成足够短的更新。函数不等待VBlank,也不保护多字节更新免受中断影响。

使用示例

c
__cgb_safe_set_ocps(0xB6); // OBJ byte 54; this does not select a BG color.

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

预期结果

本程序演示同一图块图案如何分别使用BG和OBJ的调色板。每个三角形为8×8像素,从顶行左侧1个像素逐行扩展到底行8个像素。在x=16、32、48、64、80、96、112处,y=24的BG行依次为红、蓝、绿、红、蓝、绿、红;y=64的OBJ行依次为蓝、绿、红、蓝、绿、红、蓝。各列依次使用单色、RGB分量、索引范围、整组调色板、绝对范围、标准寄存器名及寄存器别名写入。

y=88处的7个8×8格依次显示:红色三角形(x=16)、存储体1中线宽1像素的蓝色方框(32)、水平翻转的绿色三角形(48)、垂直翻转的红色三角形(64)、双向翻转的蓝色三角形(80)、位于红色精灵前方且颜色0区域透出红色的绿色三角形(96),以及关闭BG优先级后覆盖三角形的实心红色精灵(112)。坐标以屏幕左上角为原点,单位为像素。空白处为白色,文字为黑色。

FAILED CHECKS 000表示RGB掩码、含输入2的布尔标志、调色板索引回绕、跨调色板写入、在槽位31截断、null/零长度不写入,以及LCD关闭时的颜色读回检查均通过。测试4种组合:双模式ROM分别在DMG和CGB运行、DMG专用ROM在DMG运行、CGB专用ROM在CGB运行。DMG下CGB MODE为0,三角形均为黑色,存储体及翻转属性不影响显示,最下行最后两格为黑色方块。CGB测试中CGB MODE为1。未测试实机及LCD显示期间的调色板访问冲突。

__cgb_safe_set_ocps: --cgb=cgb / DMG
--cgb=cgb / DMG
__cgb_safe_set_ocps: --cgb=cgb / CGB
--cgb=cgb / CGB
__cgb_safe_set_ocps: --cgb=dmg / DMG
--cgb=dmg / DMG
__cgb_safe_set_ocps: --cgb=cgb_only / CGB
--cgb=cgb_only / CGB

完整示例程序

构建命令

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqgb\kitaqgb.exe .\kitaq-docs\samples\api-examples\gb\cgb_palette_attributes.c -I .\kitaqgb\lib -I .\kitaq-docs\samples -o .\out\cgb_palette_attributes.gb --profile=dev --rst-disable --stack-bank=fixed --cgb=cgb --no-cache --no-disasm
实现与源码

kitaqgb/lib/cgb_palette.h:11

cgb_tile.h — CGB 图块编号与属性

cgb_tile.h 集中了图块地图内建函数的声明,以及构造 CGB 图块属性的宏。图块编号选择图案;属性选择调色板、图案所在的 VRAM 存储体、翻转方式和背景优先级。设置属性不会改变图案的像素。

__settile 系列访问地图 0x9800;__settileat 使用明确指定的地图基地址;__settilebg 使用 LCDC 选择的背景地图;__settilewin 使用 LCDC 选择的窗口地图。attr 形式只写属性,cgb 形式同时写图块编号和属性。bulk 形式在给定地址传送连续字节。请根据目标位置和要写入的数据选择函数。

使用 KQ_CGB_ATTR_PAL 等宏设置单个属性字段,或用 KQ_CGB_ATTR 组合各字段。bank 字段选择存放图案的 VRAM 存储体,而不是 ROM 存储体。普通图块编号写入使用当前选择的 VRAM 存储体,因此执行图块/属性组合操作前,应先选择存储体零。在 DMG 上,仅属性操作不产生效果;组合操作仍会写入图块编号。

编译器会生成 __ 内建函数的实现,无需链接 cgb_tile.c。普通写入路径会等待 VRAM 可访问,但部分路径也会改变中断启用状态。_unsafe 和 _fast 形式省略等待:仅在调用方保证 VRAM 可写时使用,例如 LCD 关闭期间。请查看每个条目的中断、存储体和传送长度条件。

__cgb_safe_set_vbk

在CGB上通过VBK($FF4F)选择CPU访问的VRAM存储体。只写入value & 1:偶数选0,奇数选1。影响CPU对$8000–$9FFF的访问,不是地图属性选择的图块数据存储体。不会自动恢复原存储体。

语法

c
void __cgb_safe_set_vbk(u8 value);

参数

value

在CGB上通过VBK($FF4F)选择CPU访问的VRAM存储体。只写入value & 1:偶数选0,奇数选1。影响CPU对$8000–$9FFF的访问,不是地图属性选择的图块数据存储体。不会自动恢复原存储体。

返回值

无(void)。

使用条件与限制

调色板操作和带模式保护的寄存器写入在DMG模式下不写入任何内容。这里的safe指CGB模式检查,不代表等待PPU可访问或禁止中断。请在LCD关闭时设置,或在可访问的PPU阶段内完成足够短的更新。函数不等待VBlank,也不保护多字节更新免受中断影响。

使用示例

c
if (color_mode) {
    __cgb_safe_set_vbk(3); // Only bit zero is kept: CPU accesses now use bank 1.
    if ((P_VBK&1)!=1) failures++;
    __vram_copy(0x8010,shape1,16);
    __cgb_safe_set_vbk(2); // Bit zero is clear, so return to bank 0.
    if ((P_VBK&1)!=0) failures++;
}

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

预期结果

本程序演示同一图块图案如何分别使用BG和OBJ的调色板。每个三角形为8×8像素,从顶行左侧1个像素逐行扩展到底行8个像素。在x=16、32、48、64、80、96、112处,y=24的BG行依次为红、蓝、绿、红、蓝、绿、红;y=64的OBJ行依次为蓝、绿、红、蓝、绿、红、蓝。各列依次使用单色、RGB分量、索引范围、整组调色板、绝对范围、标准寄存器名及寄存器别名写入。

y=88处的7个8×8格依次显示:红色三角形(x=16)、存储体1中线宽1像素的蓝色方框(32)、水平翻转的绿色三角形(48)、垂直翻转的红色三角形(64)、双向翻转的蓝色三角形(80)、位于红色精灵前方且颜色0区域透出红色的绿色三角形(96),以及关闭BG优先级后覆盖三角形的实心红色精灵(112)。坐标以屏幕左上角为原点,单位为像素。空白处为白色,文字为黑色。

FAILED CHECKS 000表示RGB掩码、含输入2的布尔标志、调色板索引回绕、跨调色板写入、在槽位31截断、null/零长度不写入,以及LCD关闭时的颜色读回检查均通过。测试4种组合:双模式ROM分别在DMG和CGB运行、DMG专用ROM在DMG运行、CGB专用ROM在CGB运行。DMG下CGB MODE为0,三角形均为黑色,存储体及翻转属性不影响显示,最下行最后两格为黑色方块。CGB测试中CGB MODE为1。未测试实机及LCD显示期间的调色板访问冲突。

__cgb_safe_set_vbk: --cgb=cgb / DMG
--cgb=cgb / DMG
__cgb_safe_set_vbk: --cgb=cgb / CGB
--cgb=cgb / CGB
__cgb_safe_set_vbk: --cgb=dmg / DMG
--cgb=dmg / DMG
__cgb_safe_set_vbk: --cgb=cgb_only / CGB
--cgb=cgb_only / CGB

完整示例程序

构建命令

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqgb\kitaqgb.exe .\kitaq-docs\samples\api-examples\gb\cgb_palette_attributes.c -I .\kitaqgb\lib -I .\kitaq-docs\samples -o .\out\cgb_palette_attributes.gb --profile=dev --rst-disable --stack-bank=fixed --cgb=cgb --no-cache --no-disasm
实现与源码

kitaqgb/lib/cgb_tile.h:28

声明注释(原文)
Write the VRAM bank selection only on CGB. Safe here means hardware-mode guarded, not PPU-timed.
__settile

向图块地图的一个单元写入图块编号。这是在选择已有图块图像,不会修改像素数据。

目标地址始终为 0x9800 + 32*y + x,不随 LCDC 的背景或窗口地图选择而改变。

必要时等待 VRAM 可写。LCD 开启时的路径会屏蔽中断,并可能在结束后启用中断;不保证保留调用前的中断状态,也不保证在一次 VBlank 内完成。

语法

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

参数

x, y

以 8×8 图块为单位的坐标,各为 0~31。整个写入范围必须位于 32×32 地图内;不会自动裁剪坐标。

tile

0~255 的图块编号。LCDC 位 4 决定编号如何寻址图块图像数据;这不是 VRAM 字节地址。

返回值

无(void)。

使用条件与限制

图块编号写入当前选中的 VRAM 库。CGB 上应选择编号所在的库 0;此调用不会代为切换库。

示例使用 gb_common.h。m_init() 将附带 ASCII 字体加载到与字符编码相同的图块编号。m_wait(); M_LCDC=0; 在 VBlank 中关闭 LCD,以便进行无需等待的传输。最后的 LCDC 值选择显示地图。

使用示例

c
// __settile
// Write one tile-number byte to the tile map; this selects an existing tile image, not
// new pixel data. The destination is always `0x9800 + 32*y + x`, regardless of which
// maps LCDC selects for the background and window.
// Expected: FAILED CHECKS 000, with the written tile data in the selected map.
#include "gb_tile_color_example.h"

void main() {
    u8 failures;
    failures = 0;
    tile_color_example_begin();
    __settile(2, 3, 'A');
    if (tile_example_read(0x9862, 0) != 'A') { failures++; }
    if (tile_example_read(0x9C62, 0) != 0) { failures++; }
    tile_example_finish(failures, 0);
}

预期结果

所选地图显示写入的字符或填充区域。程序读回目标图块编号,并在 CGB 上读回属性进行比较;FAILED CHECKS 000 表示比较全部通过。两张地图初始均为 0,调用前将背景/窗口地图选为 0x9C00,因此能区分固定地图与所选地图的写入。示例在 LCD 关闭时写入,不测量 LCD 开启时的时序。

在 CGB 上,示例字符和结果文字以白底蓝字显示。初始化时将调色板 0 的绘制颜色设为蓝色,不修改图块映射的属性。在 DMG 上,相同字符显示为白底黑字。

预期图像:以下图块排列从图块坐标 (x=2, y=3)、像素坐标 (16,24) 开始显示。每个字符占一个 8×8 图块。按从上到下、从左到右阅读,周围图块保持空白。

text
A
__settile: DMG
DMG
__settile: CGB
CGB

完整示例程序

构建命令

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqgb\kitaqgb.exe .\kitaq-docs\samples\api-examples\gb\__settile.c -I .\kitaqgb\lib -I .\kitaq-docs\samples -o .\out\__settile.gb --profile=dev --rst-disable --stack-bank=fixed --cgb=cgb --no-disasm
实现与源码

kitaqgb/lib/cgb_tile.h:31

声明注释(原文)
Write a tile number to map 9800, waiting for VRAM access when necessary.
__settile_bulk

从 src 向 VRAM 字节地址 dest 复制连续的 count 个图块编号字节。数量以字节计,不是图块图像数;不调整行跨度。

必要时等待 VRAM 可写。LCD 开启时的路径会屏蔽中断,并可能在结束后启用中断;不保证保留调用前的中断状态,也不保证在一次 VBlank 内完成。

语法

c
void __settile_bulk(u16 dest, const u8* src, u8 count);

参数

dest

起始 VRAM 字节地址。地图使用 0x9800–0x9FFF 范围,整个传输应位于目标地图内。

src

位于 RAM 或当前可见 ROM bank 中的连续字节。图块流保存图块编号,属性流保存 CGB 属性字节。函数立即复制,不保留指针。

count

字节数,范围 0~255。为 0 时不复制;源数据至少要有这么多可读字节。

返回值

无(void)。

使用条件与限制

图块编号写入当前选中的 VRAM 库。CGB 上应选择编号所在的库 0;此调用不会代为切换库。

复制结束前,必须确保源缓冲区及其 ROM 库始终可读。无返回值。

示例使用 gb_common.h。m_init() 将附带 ASCII 字体加载到与字符编码相同的图块编号。m_wait(); M_LCDC=0; 在 VBlank 中关闭 LCD,以便进行无需等待的传输。最后的 LCDC 值选择显示地图。

使用示例

c
// __settile_bulk
// Copy `count` consecutive tile-number bytes from `src` to the VRAM byte address `dest`.
// The count is in bytes, not tile-image units; no row-stride adjustment is made. Waits
// for writable VRAM when necessary. The LCD-on path uses interrupt masking and can
// enable interrupts afterward; it is not a promise to preserve the caller's interrupt
// state or to finish within one VBlank.
// Expected: FAILED CHECKS 000, with the written tile data in the selected map.
#include "gb_tile_color_example.h"
const u8 example_tiles[] = {'A','B','C'};
const u8 example_attrs[] = {1,2,3};

void main() {
    u8 failures;
    failures = 0;
    tile_color_example_begin();
    __settile_bulk(0x9862, example_tiles, 3);
    if (tile_example_read(0x9862, 0) != 'A') { failures++; }
    if (tile_example_read(0x9863, 0) != 'B') { failures++; }
    if (tile_example_read(0x9864, 0) != 'C') { failures++; }
    if (tile_example_read(0x9865, 0) != 0) { failures++; }
    tile_example_finish(failures, 0);
}

预期结果

所选地图显示写入的字符或填充区域。程序读回目标图块编号,并在 CGB 上读回属性进行比较;FAILED CHECKS 000 表示比较全部通过。两张地图初始均为 0,调用前将背景/窗口地图选为 0x9C00,因此能区分固定地图与所选地图的写入。示例在 LCD 关闭时写入,不测量 LCD 开启时的时序。

在 CGB 上,示例字符和结果文字以白底蓝字显示。初始化时将调色板 0 的绘制颜色设为蓝色,不修改图块映射的属性。在 DMG 上,相同字符显示为白底黑字。

预期图像:以下图块排列从图块坐标 (x=2, y=3)、像素坐标 (16,24) 开始显示。每个字符占一个 8×8 图块。按从上到下、从左到右阅读,周围图块保持空白。

text
ABC
__settile_bulk: DMG
DMG
__settile_bulk: CGB
CGB

完整示例程序

构建命令

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqgb\kitaqgb.exe .\kitaq-docs\samples\api-examples\gb\__settile_bulk.c -I .\kitaqgb\lib -I .\kitaq-docs\samples -o .\out\__settile_bulk.gb --profile=dev --rst-disable --stack-bank=fixed --cgb=cgb --no-disasm
实现与源码

kitaqgb/lib/cgb_tile.h:48

声明注释(原文)
Copy count sequential bytes to dest using the current VRAM bank and access-checked transfer.
__settile_bulk_fast

从 src 向 VRAM 字节地址 dest 复制连续的 count 个图块编号字节。数量以字节计,不是图块图像数;不调整行跨度。

不等待 VRAM 可写。只能在 LCD 关闭时,或由调用方确保整个传输都处于可写时间段时使用。

语法

c
void __settile_bulk_fast(u16 dest, const u8* src, u8 count);

参数

dest

起始 VRAM 字节地址。地图使用 0x9800–0x9FFF 范围,整个传输应位于目标地图内。

src

位于 RAM 或当前可见 ROM bank 中的连续字节。图块流保存图块编号,属性流保存 CGB 属性字节。函数立即复制,不保留指针。

count

字节数,范围 0~255。为 0 时不复制;源数据至少要有这么多可读字节。

返回值

无(void)。

使用条件与限制

图块编号写入当前选中的 VRAM 库。CGB 上应选择编号所在的库 0;此调用不会代为切换库。

复制结束前,必须确保源缓冲区及其 ROM 库始终可读。无返回值。

示例使用 gb_common.h。m_init() 将附带 ASCII 字体加载到与字符编码相同的图块编号。m_wait(); M_LCDC=0; 在 VBlank 中关闭 LCD,以便进行无需等待的传输。最后的 LCDC 值选择显示地图。

使用示例

c
// __settile_bulk_fast
// Copy `count` consecutive tile-number bytes from `src` to the VRAM byte address `dest`.
// The count is in bytes, not tile-image units; no row-stride adjustment is made. Does
// not wait for VRAM access. Call only while the LCD is off or within a caller-managed
// writable interval with enough time for the whole transfer.
// Expected: FAILED CHECKS 000, with the written tile data in the selected map.
#include "gb_tile_color_example.h"
const u8 example_tiles[] = {'A','B','C'};
const u8 example_attrs[] = {1,2,3};

void main() {
    u8 failures;
    failures = 0;
    tile_color_example_begin();
    __settile_bulk_fast(0x9862, example_tiles, 3);
    if (tile_example_read(0x9862, 0) != 'A') { failures++; }
    if (tile_example_read(0x9863, 0) != 'B') { failures++; }
    if (tile_example_read(0x9864, 0) != 'C') { failures++; }
    if (tile_example_read(0x9865, 0) != 0) { failures++; }
    tile_example_finish(failures, 0);
}

预期结果

所选地图显示写入的字符或填充区域。程序读回目标图块编号,并在 CGB 上读回属性进行比较;FAILED CHECKS 000 表示比较全部通过。两张地图初始均为 0,调用前将背景/窗口地图选为 0x9C00,因此能区分固定地图与所选地图的写入。示例在 LCD 关闭时写入,不测量 LCD 开启时的时序。

在 CGB 上,示例字符和结果文字以白底蓝字显示。初始化时将调色板 0 的绘制颜色设为蓝色,不修改图块映射的属性。在 DMG 上,相同字符显示为白底黑字。

预期图像:以下图块排列从图块坐标 (x=2, y=3)、像素坐标 (16,24) 开始显示。每个字符占一个 8×8 图块。按从上到下、从左到右阅读,周围图块保持空白。

text
ABC
__settile_bulk_fast: DMG
DMG
__settile_bulk_fast: CGB
CGB

完整示例程序

构建命令

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqgb\kitaqgb.exe .\kitaq-docs\samples\api-examples\gb\__settile_bulk_fast.c -I .\kitaqgb\lib -I .\kitaq-docs\samples -o .\out\__settile_bulk_fast.gb --profile=dev --rst-disable --stack-bank=fixed --cgb=cgb --no-disasm
实现与源码

kitaqgb/lib/cgb_tile.h:50

声明注释(原文)
Copy count sequential bytes to dest without VRAM access waits; retain source-bank visibility.
__settile_unsafe

向图块地图的一个单元写入图块编号。这是在选择已有图块图像,不会修改像素数据。

目标地址始终为 0x9800 + 32*y + x,不随 LCDC 的背景或窗口地图选择而改变。

不等待 VRAM 可写。只能在 LCD 关闭时,或由调用方确保整个传输都处于可写时间段时使用。

语法

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

参数

x, y

以 8×8 图块为单位的坐标,各为 0~31。整个写入范围必须位于 32×32 地图内;不会自动裁剪坐标。

tile

0~255 的图块编号。LCDC 位 4 决定编号如何寻址图块图像数据;这不是 VRAM 字节地址。

返回值

无(void)。

使用条件与限制

图块编号写入当前选中的 VRAM 库。CGB 上应选择编号所在的库 0;此调用不会代为切换库。

示例使用 gb_common.h。m_init() 将附带 ASCII 字体加载到与字符编码相同的图块编号。m_wait(); M_LCDC=0; 在 VBlank 中关闭 LCD,以便进行无需等待的传输。最后的 LCDC 值选择显示地图。

使用示例

c
// __settile_unsafe
// Write one tile-number byte to the tile map; this selects an existing tile image, not
// new pixel data. The destination is always `0x9800 + 32*y + x`, regardless of which
// maps LCDC selects for the background and window.
// Expected: FAILED CHECKS 000, with the written tile data in the selected map.
#include "gb_tile_color_example.h"

void main() {
    u8 failures;
    failures = 0;
    tile_color_example_begin();
    __settile_unsafe(2, 3, 'A');
    if (tile_example_read(0x9862, 0) != 'A') { failures++; }
    if (tile_example_read(0x9C62, 0) != 0) { failures++; }
    tile_example_finish(failures, 0);
}

预期结果

所选地图显示写入的字符或填充区域。程序读回目标图块编号,并在 CGB 上读回属性进行比较;FAILED CHECKS 000 表示比较全部通过。两张地图初始均为 0,调用前将背景/窗口地图选为 0x9C00,因此能区分固定地图与所选地图的写入。示例在 LCD 关闭时写入,不测量 LCD 开启时的时序。

在 CGB 上,示例字符和结果文字以白底蓝字显示。初始化时将调色板 0 的绘制颜色设为蓝色,不修改图块映射的属性。在 DMG 上,相同字符显示为白底黑字。

预期图像:以下图块排列从图块坐标 (x=2, y=3)、像素坐标 (16,24) 开始显示。每个字符占一个 8×8 图块。按从上到下、从左到右阅读,周围图块保持空白。

text
A
__settile_unsafe: DMG
DMG
__settile_unsafe: CGB
CGB

完整示例程序

构建命令

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqgb\kitaqgb.exe .\kitaq-docs\samples\api-examples\gb\__settile_unsafe.c -I .\kitaqgb\lib -I .\kitaq-docs\samples -o .\out\__settile_unsafe.gb --profile=dev --rst-disable --stack-bank=fixed --cgb=cgb --no-disasm
实现与源码

kitaqgb/lib/cgb_tile.h:33

声明注释(原文)
Write a tile number to map 9800 without waiting for writable VRAM.
__settileat

向图块地图的一个单元写入图块编号。这是在选择已有图块图像,不会修改像素数据。

用 base 指定地图起始地址 0x9800 或 0x9C00;字节地址为 base + 32*y + x。

必要时等待 VRAM 可写。LCD 开启时的路径会屏蔽中断,并可能在结束后启用中断;不保证保留调用前的中断状态,也不保证在一次 VBlank 内完成。

语法

c
void __settileat(u16 base, u8 x, u8 y, u8 tile);

参数

base

用 base 指定地图起始地址 0x9800 或 0x9C00;字节地址为 base + 32*y + x。

x, y

以 8×8 图块为单位的坐标,各为 0~31。整个写入范围必须位于 32×32 地图内;不会自动裁剪坐标。

tile

0~255 的图块编号。LCDC 位 4 决定编号如何寻址图块图像数据;这不是 VRAM 字节地址。

返回值

无(void)。

使用条件与限制

图块编号写入当前选中的 VRAM 库。CGB 上应选择编号所在的库 0;此调用不会代为切换库。

示例使用 gb_common.h。m_init() 将附带 ASCII 字体加载到与字符编码相同的图块编号。m_wait(); M_LCDC=0; 在 VBlank 中关闭 LCD,以便进行无需等待的传输。最后的 LCDC 值选择显示地图。

使用示例

c
// __settileat
// Write one tile-number byte to the tile map; this selects an existing tile image, not
// new pixel data. `base` explicitly selects the map (`0x9800` or `0x9C00`); the byte
// address is `base + 32*y + x`.
// Expected: FAILED CHECKS 000, with the written tile data in the selected map.
#include "gb_tile_color_example.h"

void main() {
    u8 failures;
    failures = 0;
    tile_color_example_begin();
    __settileat(0x9C00, 2, 3, 'A');
    if (tile_example_read(0x9C62, 0) != 'A') { failures++; }
    if (tile_example_read(0x9862, 0) != 0) { failures++; }
    tile_example_finish(failures, 1);
}

预期结果

所选地图显示写入的字符或填充区域。程序读回目标图块编号,并在 CGB 上读回属性进行比较;FAILED CHECKS 000 表示比较全部通过。两张地图初始均为 0,调用前将背景/窗口地图选为 0x9C00,因此能区分固定地图与所选地图的写入。示例在 LCD 关闭时写入,不测量 LCD 开启时的时序。

在 CGB 上,示例字符和结果文字以白底蓝字显示。初始化时将调色板 0 的绘制颜色设为蓝色,不修改图块映射的属性。在 DMG 上,相同字符显示为白底黑字。

预期图像:以下图块排列从图块坐标 (x=2, y=3)、像素坐标 (16,24) 开始显示。每个字符占一个 8×8 图块。按从上到下、从左到右阅读,周围图块保持空白。

text
A
__settileat: DMG
DMG
__settileat: CGB
CGB

完整示例程序

构建命令

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqgb\kitaqgb.exe .\kitaq-docs\samples\api-examples\gb\__settileat.c -I .\kitaqgb\lib -I .\kitaq-docs\samples -o .\out\__settileat.gb --profile=dev --rst-disable --stack-bank=fixed --cgb=cgb --no-disasm
实现与源码

kitaqgb/lib/cgb_tile.h:35

声明注释(原文)
Write at base+32*y+x with VRAM access checks; supply a valid tile-map base.
__settileat_unsafe

向图块地图的一个单元写入图块编号。这是在选择已有图块图像,不会修改像素数据。

用 base 指定地图起始地址 0x9800 或 0x9C00;字节地址为 base + 32*y + x。

不等待 VRAM 可写。只能在 LCD 关闭时,或由调用方确保整个传输都处于可写时间段时使用。

语法

c
void __settileat_unsafe(u16 base, u8 x, u8 y, u8 tile);

参数

base

用 base 指定地图起始地址 0x9800 或 0x9C00;字节地址为 base + 32*y + x。

x, y

以 8×8 图块为单位的坐标,各为 0~31。整个写入范围必须位于 32×32 地图内;不会自动裁剪坐标。

tile

0~255 的图块编号。LCDC 位 4 决定编号如何寻址图块图像数据;这不是 VRAM 字节地址。

返回值

无(void)。

使用条件与限制

图块编号写入当前选中的 VRAM 库。CGB 上应选择编号所在的库 0;此调用不会代为切换库。

示例使用 gb_common.h。m_init() 将附带 ASCII 字体加载到与字符编码相同的图块编号。m_wait(); M_LCDC=0; 在 VBlank 中关闭 LCD,以便进行无需等待的传输。最后的 LCDC 值选择显示地图。

使用示例

c
// __settileat_unsafe
// Write one tile-number byte to the tile map; this selects an existing tile image, not
// new pixel data. `base` explicitly selects the map (`0x9800` or `0x9C00`); the byte
// address is `base + 32*y + x`.
// Expected: FAILED CHECKS 000, with the written tile data in the selected map.
#include "gb_tile_color_example.h"

void main() {
    u8 failures;
    failures = 0;
    tile_color_example_begin();
    __settileat_unsafe(0x9C00, 2, 3, 'A');
    if (tile_example_read(0x9C62, 0) != 'A') { failures++; }
    if (tile_example_read(0x9862, 0) != 0) { failures++; }
    tile_example_finish(failures, 1);
}

预期结果

所选地图显示写入的字符或填充区域。程序读回目标图块编号,并在 CGB 上读回属性进行比较;FAILED CHECKS 000 表示比较全部通过。两张地图初始均为 0,调用前将背景/窗口地图选为 0x9C00,因此能区分固定地图与所选地图的写入。示例在 LCD 关闭时写入,不测量 LCD 开启时的时序。

在 CGB 上,示例字符和结果文字以白底蓝字显示。初始化时将调色板 0 的绘制颜色设为蓝色,不修改图块映射的属性。在 DMG 上,相同字符显示为白底黑字。

预期图像:以下图块排列从图块坐标 (x=2, y=3)、像素坐标 (16,24) 开始显示。每个字符占一个 8×8 图块。按从上到下、从左到右阅读,周围图块保持空白。

text
A
__settileat_unsafe: DMG
DMG
__settileat_unsafe: CGB
CGB

完整示例程序

构建命令

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqgb\kitaqgb.exe .\kitaq-docs\samples\api-examples\gb\__settileat_unsafe.c -I .\kitaqgb\lib -I .\kitaq-docs\samples -o .\out\__settileat_unsafe.gb --profile=dev --rst-disable --stack-bank=fixed --cgb=cgb --no-disasm
实现与源码

kitaqgb/lib/cgb_tile.h:37

声明注释(原文)
Use an explicit map base without waiting for VRAM access.
__settileatattr

只修改 CGB 属性字节,保留图块编号。属性指定调色板、图块图像库、水平或垂直翻转和优先级。

用 base 指定地图起始地址 0x9800 或 0x9C00;字节地址为 base + 32*y + x。

必要时等待 VRAM 可写。LCD 开启时的路径会屏蔽中断,并可能在结束后启用中断;不保证保留调用前的中断状态,也不保证在一次 VBlank 内完成。

语法

c
void __settileatattr(u16 base, u8 x, u8 y, u8 attr);

参数

base

用 base 指定地图起始地址 0x9800 或 0x9C00;字节地址为 base + 32*y + x。

x, y

以 8×8 图块为单位的坐标,各为 0~31。整个写入范围必须位于 32×32 地图内;不会自动裁剪坐标。

attr

CGB 属性字节:位 0~2 选择调色板 0~7,位 3 选择图像数据 bank,位 5、6 分别控制水平和垂直翻转,位 7 设置背景/窗口优先级。未使用的位 4 应为 0。

返回值

无(void)。

使用条件与限制

CGB 上切到 VRAM 库 1 写入属性,结束时置 VBK=0。DMG 上不执行属性写入。

示例使用 gb_common.h。m_init() 将附带 ASCII 字体加载到与字符编码相同的图块编号。m_wait(); M_LCDC=0; 在 VBlank 中关闭 LCD,以便进行无需等待的传输。最后的 LCDC 值选择显示地图。

使用示例

c
// __settileatattr
// Write only the CGB attribute byte; leave the tile number unchanged. Attribute bits
// select palette, tile-data bank, flips and priority. `base` explicitly selects the map
// (`0x9800` or `0x9C00`); the byte address is `base + 32*y + x`.
// Expected: FAILED CHECKS 000, with the written tile data in the selected map.
#include "gb_tile_example.h"

void main() {
    u8 failures;
    failures = 0;
    tile_example_begin();
    __settileat(0x9C00, 2, 3, 'A');
    __settileatattr(0x9C00, 2, 3, KQ_CGB_ATTR(2, 0, 0, 0, 0));
    if (tile_example_read(0x9C62, 0) != 'A') { failures++; }
    if (tile_example_read(0x9862, 0) != 0) { failures++; }
    if (__cgb_is_cgb() && tile_example_read(0x9C62, 1) != 2) { failures++; }
    if (__cgb_is_cgb() && tile_example_read(0x9862, 1) != 0) { failures++; }
    tile_example_finish(failures, 1);
}

预期结果

所选地图显示写入的字符或填充区域。程序读回目标图块编号,并在 CGB 上读回属性进行比较;FAILED CHECKS 000 表示比较全部通过。两张地图初始均为 0,调用前将背景/窗口地图选为 0x9C00,因此能区分固定地图与所选地图的写入。示例在 LCD 关闭时写入,不测量 LCD 开启时的时序。

CGB 上的 A 为蓝色:属性值 2 选择示例设置的蓝色调色板。DMG 不应用 CGB 属性,因此同一字符为黑色。

预期图像:以下图块排列从图块坐标 (x=2, y=3)、像素坐标 (16,24) 开始显示。每个字符占一个 8×8 图块。按从上到下、从左到右阅读,周围图块保持空白。

text
A
__settileatattr: DMG
DMG
__settileatattr: CGB
CGB

完整示例程序

构建命令

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqgb\kitaqgb.exe .\kitaq-docs\samples\api-examples\gb\__settileatattr.c -I .\kitaqgb\lib -I .\kitaq-docs\samples -o .\out\__settileatattr.gb --profile=dev --rst-disable --stack-bank=fixed --cgb=cgb --no-disasm
实现与源码

kitaqgb/lib/cgb_tile.h:62

声明注释(原文)
Write attributes in bank one at the explicit map base on CGB, then leave VBK zero; skip on DMG. Use the access-checked write path.
__settileatattr_unsafe

只修改 CGB 属性字节,保留图块编号。属性指定调色板、图块图像库、水平或垂直翻转和优先级。

用 base 指定地图起始地址 0x9800 或 0x9C00;字节地址为 base + 32*y + x。

不等待 VRAM 可写。只能在 LCD 关闭时,或由调用方确保整个传输都处于可写时间段时使用。

语法

c
void __settileatattr_unsafe(u16 base, u8 x, u8 y, u8 attr);

参数

base

用 base 指定地图起始地址 0x9800 或 0x9C00;字节地址为 base + 32*y + x。

x, y

以 8×8 图块为单位的坐标,各为 0~31。整个写入范围必须位于 32×32 地图内;不会自动裁剪坐标。

attr

CGB 属性字节:位 0~2 选择调色板 0~7,位 3 选择图像数据 bank,位 5、6 分别控制水平和垂直翻转,位 7 设置背景/窗口优先级。未使用的位 4 应为 0。

返回值

无(void)。

使用条件与限制

CGB 上切到 VRAM 库 1 写入属性,结束时置 VBK=0。DMG 上不执行属性写入。

示例使用 gb_common.h。m_init() 将附带 ASCII 字体加载到与字符编码相同的图块编号。m_wait(); M_LCDC=0; 在 VBlank 中关闭 LCD,以便进行无需等待的传输。最后的 LCDC 值选择显示地图。

使用示例

c
// __settileatattr_unsafe
// Write only the CGB attribute byte; leave the tile number unchanged. Attribute bits
// select palette, tile-data bank, flips and priority. `base` explicitly selects the map
// (`0x9800` or `0x9C00`); the byte address is `base + 32*y + x`.
// Expected: FAILED CHECKS 000, with the written tile data in the selected map.
#include "gb_tile_example.h"

void main() {
    u8 failures;
    failures = 0;
    tile_example_begin();
    __settileat(0x9C00, 2, 3, 'A');
    __settileatattr_unsafe(0x9C00, 2, 3, KQ_CGB_ATTR(2, 0, 0, 0, 0));
    if (tile_example_read(0x9C62, 0) != 'A') { failures++; }
    if (tile_example_read(0x9862, 0) != 0) { failures++; }
    if (__cgb_is_cgb() && tile_example_read(0x9C62, 1) != 2) { failures++; }
    if (__cgb_is_cgb() && tile_example_read(0x9862, 1) != 0) { failures++; }
    tile_example_finish(failures, 1);
}

预期结果

所选地图显示写入的字符或填充区域。程序读回目标图块编号,并在 CGB 上读回属性进行比较;FAILED CHECKS 000 表示比较全部通过。两张地图初始均为 0,调用前将背景/窗口地图选为 0x9C00,因此能区分固定地图与所选地图的写入。示例在 LCD 关闭时写入,不测量 LCD 开启时的时序。

CGB 上的 A 为蓝色:属性值 2 选择示例设置的蓝色调色板。DMG 不应用 CGB 属性,因此同一字符为黑色。

预期图像:以下图块排列从图块坐标 (x=2, y=3)、像素坐标 (16,24) 开始显示。每个字符占一个 8×8 图块。按从上到下、从左到右阅读,周围图块保持空白。

text
A
__settileatattr_unsafe: DMG
DMG
__settileatattr_unsafe: CGB
CGB

完整示例程序

构建命令

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqgb\kitaqgb.exe .\kitaq-docs\samples\api-examples\gb\__settileatattr_unsafe.c -I .\kitaqgb\lib -I .\kitaq-docs\samples -o .\out\__settileatattr_unsafe.gb --profile=dev --rst-disable --stack-bank=fixed --cgb=cgb --no-disasm
实现与源码

kitaqgb/lib/cgb_tile.h:64

声明注释(原文)
Write attributes in bank one at the explicit map base on CGB, then leave VBK zero; skip on DMG. No VRAM access waits are inserted.
__settileatcgb

依次写入图块编号和 CGB 属性。DMG 只写编号。CGB 调用前应选择 VRAM 库 0;属性写入库 1,结束时为 VBK=0。

用 base 指定地图起始地址 0x9800 或 0x9C00;字节地址为 base + 32*y + x。

必要时等待 VRAM 可写。LCD 开启时的路径会屏蔽中断,并可能在结束后启用中断;不保证保留调用前的中断状态,也不保证在一次 VBlank 内完成。

语法

c
void __settileatcgb(u16 base, u8 x, u8 y, u8 tile, u8 attr);

参数

base

用 base 指定地图起始地址 0x9800 或 0x9C00;字节地址为 base + 32*y + x。

x, y

以 8×8 图块为单位的坐标,各为 0~31。整个写入范围必须位于 32×32 地图内;不会自动裁剪坐标。

tile

0~255 的图块编号。LCDC 位 4 决定编号如何寻址图块图像数据;这不是 VRAM 字节地址。

attr

CGB 属性字节:位 0~2 选择调色板 0~7,位 3 选择图像数据 bank,位 5、6 分别控制水平和垂直翻转,位 7 设置背景/窗口优先级。未使用的位 4 应为 0。

返回值

无(void)。

使用条件与限制

示例使用 gb_common.h。m_init() 将附带 ASCII 字体加载到与字符编码相同的图块编号。m_wait(); M_LCDC=0; 在 VBlank 中关闭 LCD,以便进行无需等待的传输。最后的 LCDC 值选择显示地图。

使用示例

c
// __settileatcgb
// Write a tile number and its CGB attribute. On DMG, only the tile-number write is
// performed. On CGB, enter with VRAM bank 0 selected; attributes use bank 1 and the call
// finishes with `VBK=0`. `base` explicitly selects the map (`0x9800` or `0x9C00`); the
// byte address is `base + 32*y + x`.
// Expected: FAILED CHECKS 000, with the written tile data in the selected map.
#include "gb_tile_example.h"

void main() {
    u8 failures;
    failures = 0;
    tile_example_begin();
    __settileatcgb(0x9C00, 2, 3, 'A', KQ_CGB_ATTR(2, 0, 0, 0, 0));
    if (tile_example_read(0x9C62, 0) != 'A') { failures++; }
    if (tile_example_read(0x9862, 0) != 0) { failures++; }
    if (__cgb_is_cgb() && tile_example_read(0x9C62, 1) != 2) { failures++; }
    if (__cgb_is_cgb() && tile_example_read(0x9862, 1) != 0) { failures++; }
    tile_example_finish(failures, 1);
}

预期结果

所选地图显示写入的字符或填充区域。程序读回目标图块编号,并在 CGB 上读回属性进行比较;FAILED CHECKS 000 表示比较全部通过。两张地图初始均为 0,调用前将背景/窗口地图选为 0x9C00,因此能区分固定地图与所选地图的写入。示例在 LCD 关闭时写入,不测量 LCD 开启时的时序。

CGB 上的 A 为蓝色:属性值 2 选择示例设置的蓝色调色板。DMG 不应用 CGB 属性,因此同一字符为黑色。

预期图像:以下图块排列从图块坐标 (x=2, y=3)、像素坐标 (16,24) 开始显示。每个字符占一个 8×8 图块。按从上到下、从左到右阅读,周围图块保持空白。

text
A
__settileatcgb: DMG
DMG
__settileatcgb: CGB
CGB

完整示例程序

构建命令

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqgb\kitaqgb.exe .\kitaq-docs\samples\api-examples\gb\__settileatcgb.c -I .\kitaqgb\lib -I .\kitaq-docs\samples -o .\out\__settileatcgb.gb --profile=dev --rst-disable --stack-bank=fixed --cgb=cgb --no-disasm
实现与源码

kitaqgb/lib/cgb_tile.h:66

声明注释(原文)
Write tile and CGB attributes at the explicit map base; DMG receives only the tile write. Use the access-checked write path.
__settileatcgb_unsafe

依次写入图块编号和 CGB 属性。DMG 只写编号。CGB 调用前应选择 VRAM 库 0;属性写入库 1,结束时为 VBK=0。

用 base 指定地图起始地址 0x9800 或 0x9C00;字节地址为 base + 32*y + x。

不等待 VRAM 可写。只能在 LCD 关闭时,或由调用方确保整个传输都处于可写时间段时使用。

语法

c
void __settileatcgb_unsafe(u16 base, u8 x, u8 y, u8 tile, u8 attr);

参数

base

用 base 指定地图起始地址 0x9800 或 0x9C00;字节地址为 base + 32*y + x。

x, y

以 8×8 图块为单位的坐标,各为 0~31。整个写入范围必须位于 32×32 地图内;不会自动裁剪坐标。

tile

0~255 的图块编号。LCDC 位 4 决定编号如何寻址图块图像数据;这不是 VRAM 字节地址。

attr

CGB 属性字节:位 0~2 选择调色板 0~7,位 3 选择图像数据 bank,位 5、6 分别控制水平和垂直翻转,位 7 设置背景/窗口优先级。未使用的位 4 应为 0。

返回值

无(void)。

使用条件与限制

示例使用 gb_common.h。m_init() 将附带 ASCII 字体加载到与字符编码相同的图块编号。m_wait(); M_LCDC=0; 在 VBlank 中关闭 LCD,以便进行无需等待的传输。最后的 LCDC 值选择显示地图。

使用示例

c
// __settileatcgb_unsafe
// Write a tile number and its CGB attribute. On DMG, only the tile-number write is
// performed. On CGB, enter with VRAM bank 0 selected; attributes use bank 1 and the call
// finishes with `VBK=0`. `base` explicitly selects the map (`0x9800` or `0x9C00`); the
// byte address is `base + 32*y + x`.
// Expected: FAILED CHECKS 000, with the written tile data in the selected map.
#include "gb_tile_example.h"

void main() {
    u8 failures;
    failures = 0;
    tile_example_begin();
    __settileatcgb_unsafe(0x9C00, 2, 3, 'A', KQ_CGB_ATTR(2, 0, 0, 0, 0));
    if (tile_example_read(0x9C62, 0) != 'A') { failures++; }
    if (tile_example_read(0x9862, 0) != 0) { failures++; }
    if (__cgb_is_cgb() && tile_example_read(0x9C62, 1) != 2) { failures++; }
    if (__cgb_is_cgb() && tile_example_read(0x9862, 1) != 0) { failures++; }
    tile_example_finish(failures, 1);
}

预期结果

所选地图显示写入的字符或填充区域。程序读回目标图块编号,并在 CGB 上读回属性进行比较;FAILED CHECKS 000 表示比较全部通过。两张地图初始均为 0,调用前将背景/窗口地图选为 0x9C00,因此能区分固定地图与所选地图的写入。示例在 LCD 关闭时写入,不测量 LCD 开启时的时序。

CGB 上的 A 为蓝色:属性值 2 选择示例设置的蓝色调色板。DMG 不应用 CGB 属性,因此同一字符为黑色。

预期图像:以下图块排列从图块坐标 (x=2, y=3)、像素坐标 (16,24) 开始显示。每个字符占一个 8×8 图块。按从上到下、从左到右阅读,周围图块保持空白。

text
A
__settileatcgb_unsafe: DMG
DMG
__settileatcgb_unsafe: CGB
CGB

完整示例程序

构建命令

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqgb\kitaqgb.exe .\kitaq-docs\samples\api-examples\gb\__settileatcgb_unsafe.c -I .\kitaqgb\lib -I .\kitaq-docs\samples -o .\out\__settileatcgb_unsafe.gb --profile=dev --rst-disable --stack-bank=fixed --cgb=cgb --no-disasm
实现与源码

kitaqgb/lib/cgb_tile.h:68

声明注释(原文)
Write tile and CGB attributes at the explicit map base; DMG receives only the tile write. No VRAM access waits are inserted.
__settileattr

只修改 CGB 属性字节,保留图块编号。属性指定调色板、图块图像库、水平或垂直翻转和优先级。

目标地址始终为 0x9800 + 32*y + x,不随 LCDC 的背景或窗口地图选择而改变。

必要时等待 VRAM 可写。LCD 开启时的路径会屏蔽中断,并可能在结束后启用中断;不保证保留调用前的中断状态,也不保证在一次 VBlank 内完成。

语法

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

参数

x, y

以 8×8 图块为单位的坐标,各为 0~31。整个写入范围必须位于 32×32 地图内;不会自动裁剪坐标。

attr

CGB 属性字节:位 0~2 选择调色板 0~7,位 3 选择图像数据 bank,位 5、6 分别控制水平和垂直翻转,位 7 设置背景/窗口优先级。未使用的位 4 应为 0。

返回值

无(void)。

使用条件与限制

CGB 上切到 VRAM 库 1 写入属性,结束时置 VBK=0。DMG 上不执行属性写入。

示例使用 gb_common.h。m_init() 将附带 ASCII 字体加载到与字符编码相同的图块编号。m_wait(); M_LCDC=0; 在 VBlank 中关闭 LCD,以便进行无需等待的传输。最后的 LCDC 值选择显示地图。

使用示例

c
// __settileattr
// Write only the CGB attribute byte; leave the tile number unchanged. Attribute bits
// select palette, tile-data bank, flips and priority. The destination is always `0x9800
// + 32*y + x`, regardless of which maps LCDC selects for the background and window.
// Expected: FAILED CHECKS 000, with the written tile data in the selected map.
#include "gb_tile_example.h"

void main() {
    u8 failures;
    failures = 0;
    tile_example_begin();
    __settileat(0x9800, 2, 3, 'A');
    __settileattr(2, 3, KQ_CGB_ATTR(2, 0, 0, 0, 0));
    if (tile_example_read(0x9862, 0) != 'A') { failures++; }
    if (tile_example_read(0x9C62, 0) != 0) { failures++; }
    if (__cgb_is_cgb() && tile_example_read(0x9862, 1) != 2) { failures++; }
    if (__cgb_is_cgb() && tile_example_read(0x9C62, 1) != 0) { failures++; }
    tile_example_finish(failures, 0);
}

预期结果

所选地图显示写入的字符或填充区域。程序读回目标图块编号,并在 CGB 上读回属性进行比较;FAILED CHECKS 000 表示比较全部通过。两张地图初始均为 0,调用前将背景/窗口地图选为 0x9C00,因此能区分固定地图与所选地图的写入。示例在 LCD 关闭时写入,不测量 LCD 开启时的时序。

CGB 上的 A 为蓝色:属性值 2 选择示例设置的蓝色调色板。DMG 不应用 CGB 属性,因此同一字符为黑色。

预期图像:以下图块排列从图块坐标 (x=2, y=3)、像素坐标 (16,24) 开始显示。每个字符占一个 8×8 图块。按从上到下、从左到右阅读,周围图块保持空白。

text
A
__settileattr: DMG
DMG
__settileattr: CGB
CGB

完整示例程序

构建命令

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqgb\kitaqgb.exe .\kitaq-docs\samples\api-examples\gb\__settileattr.c -I .\kitaqgb\lib -I .\kitaq-docs\samples -o .\out\__settileattr.gb --profile=dev --rst-disable --stack-bank=fixed --cgb=cgb --no-disasm
实现与源码

kitaqgb/lib/cgb_tile.h:53

声明注释(原文)
Write attributes in bank one at map 9800 on CGB, then leave VBK zero; skip on DMG. Use the access-checked write path.
__settileattr_bulk

将 src 中连续的 count 个 CGB 属性字节复制到 VRAM 库 1 的 dest。图块编号不变;dest 是字节地址,不是坐标对。

必要时等待 VRAM 可写。LCD 开启时的路径会屏蔽中断,并可能在结束后启用中断;不保证保留调用前的中断状态,也不保证在一次 VBlank 内完成。

语法

c
void __settileattr_bulk(u16 dest, const u8* src, u8 count);

参数

dest

起始 VRAM 字节地址。地图使用 0x9800–0x9FFF 范围,整个传输应位于目标地图内。

src

位于 RAM 或当前可见 ROM bank 中的连续字节。图块流保存图块编号,属性流保存 CGB 属性字节。函数立即复制,不保留指针。

count

字节数,范围 0~255。为 0 时不复制;源数据至少要有这么多可读字节。

返回值

无(void)。

使用条件与限制

CGB 上切到 VRAM 库 1 写入属性,结束时置 VBK=0。DMG 上不执行属性写入。

复制结束前,必须确保源缓冲区及其 ROM 库始终可读。无返回值。

示例使用 gb_common.h。m_init() 将附带 ASCII 字体加载到与字符编码相同的图块编号。m_wait(); M_LCDC=0; 在 VBlank 中关闭 LCD,以便进行无需等待的传输。最后的 LCDC 值选择显示地图。

使用示例

c
// __settileattr_bulk
// Copy `count` consecutive CGB attribute bytes from `src` to `dest` in VRAM bank 1. Tile
// numbers are untouched; the destination is a byte address, not an `(x,y)` pair. Waits
// for writable VRAM when necessary. The LCD-on path uses interrupt masking and can
// enable interrupts afterward; it is not a promise to preserve the caller's interrupt
// state or to finish within one VBlank.
// Expected: FAILED CHECKS 000, with the written tile data in the selected map.
#include "gb_tile_example.h"
const u8 example_tiles[] = {'A','B','C'};
const u8 example_attrs[] = {1,2,3};

void main() {
    u8 failures;
    failures = 0;
    tile_example_begin();
    __settile_bulk(0x9862, example_tiles, 3);
    __settileattr_bulk(0x9862, example_attrs, 3);
    if (tile_example_read(0x9862, 0) != 'A') { failures++; }
    if (tile_example_read(0x9863, 0) != 'B') { failures++; }
    if (tile_example_read(0x9864, 0) != 'C') { failures++; }
    if (tile_example_read(0x9865, 0) != 0) { failures++; }
    if (__cgb_is_cgb() && tile_example_read(0x9862, 1) != 1) { failures++; }
    if (__cgb_is_cgb() && tile_example_read(0x9864, 1) != 3) { failures++; }
    tile_example_finish(failures, 0);
}

预期结果

所选地图显示写入的字符或填充区域。程序读回目标图块编号,并在 CGB 上读回属性进行比较;FAILED CHECKS 000 表示比较全部通过。两张地图初始均为 0,调用前将背景/窗口地图选为 0x9C00,因此能区分固定地图与所选地图的写入。示例在 LCD 关闭时写入,不测量 LCD 开启时的时序。

CGB 上,属性字节 1,2,3 分别选择三个预设调色板,使 A 为红色、B 为蓝色、C 为绿色。DMG 上三个字符均为黑色。

预期图像:以下图块排列从图块坐标 (x=2, y=3)、像素坐标 (16,24) 开始显示。每个字符占一个 8×8 图块。按从上到下、从左到右阅读,周围图块保持空白。

text
ABC
__settileattr_bulk: DMG
DMG
__settileattr_bulk: CGB
CGB

完整示例程序

构建命令

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqgb\kitaqgb.exe .\kitaq-docs\samples\api-examples\gb\__settileattr_bulk.c -I .\kitaqgb\lib -I .\kitaq-docs\samples -o .\out\__settileattr_bulk.gb --profile=dev --rst-disable --stack-bank=fixed --cgb=cgb --no-disasm
实现与源码

kitaqgb/lib/cgb_tile.h:89

声明注释(原文)
Copy count attributes to dest in bank one on CGB, then leave VBK zero; skip on DMG. Transfers use access checks.
__settileattr_bulk_fast

将 src 中连续的 count 个 CGB 属性字节复制到 VRAM 库 1 的 dest。图块编号不变;dest 是字节地址,不是坐标对。

不等待 VRAM 可写。只能在 LCD 关闭时,或由调用方确保整个传输都处于可写时间段时使用。

语法

c
void __settileattr_bulk_fast(u16 dest, const u8* src, u8 count);

参数

dest

起始 VRAM 字节地址。地图使用 0x9800–0x9FFF 范围,整个传输应位于目标地图内。

src

位于 RAM 或当前可见 ROM bank 中的连续字节。图块流保存图块编号,属性流保存 CGB 属性字节。函数立即复制,不保留指针。

count

字节数,范围 0~255。为 0 时不复制;源数据至少要有这么多可读字节。

返回值

无(void)。

使用条件与限制

CGB 上切到 VRAM 库 1 写入属性,结束时置 VBK=0。DMG 上不执行属性写入。

复制结束前,必须确保源缓冲区及其 ROM 库始终可读。无返回值。

示例使用 gb_common.h。m_init() 将附带 ASCII 字体加载到与字符编码相同的图块编号。m_wait(); M_LCDC=0; 在 VBlank 中关闭 LCD,以便进行无需等待的传输。最后的 LCDC 值选择显示地图。

使用示例

c
// __settileattr_bulk_fast
// Copy `count` consecutive CGB attribute bytes from `src` to `dest` in VRAM bank 1. Tile
// numbers are untouched; the destination is a byte address, not an `(x,y)` pair. Does
// not wait for VRAM access. Call only while the LCD is off or within a caller-managed
// writable interval with enough time for the whole transfer.
// Expected: FAILED CHECKS 000, with the written tile data in the selected map.
#include "gb_tile_example.h"
const u8 example_tiles[] = {'A','B','C'};
const u8 example_attrs[] = {1,2,3};

void main() {
    u8 failures;
    failures = 0;
    tile_example_begin();
    __settile_bulk(0x9862, example_tiles, 3);
    __settileattr_bulk_fast(0x9862, example_attrs, 3);
    if (tile_example_read(0x9862, 0) != 'A') { failures++; }
    if (tile_example_read(0x9863, 0) != 'B') { failures++; }
    if (tile_example_read(0x9864, 0) != 'C') { failures++; }
    if (tile_example_read(0x9865, 0) != 0) { failures++; }
    if (__cgb_is_cgb() && tile_example_read(0x9862, 1) != 1) { failures++; }
    if (__cgb_is_cgb() && tile_example_read(0x9864, 1) != 3) { failures++; }
    tile_example_finish(failures, 0);
}

预期结果

所选地图显示写入的字符或填充区域。程序读回目标图块编号,并在 CGB 上读回属性进行比较;FAILED CHECKS 000 表示比较全部通过。两张地图初始均为 0,调用前将背景/窗口地图选为 0x9C00,因此能区分固定地图与所选地图的写入。示例在 LCD 关闭时写入,不测量 LCD 开启时的时序。

CGB 上,属性字节 1,2,3 分别选择三个预设调色板,使 A 为红色、B 为蓝色、C 为绿色。DMG 上三个字符均为黑色。

预期图像:以下图块排列从图块坐标 (x=2, y=3)、像素坐标 (16,24) 开始显示。每个字符占一个 8×8 图块。按从上到下、从左到右阅读,周围图块保持空白。

text
ABC
__settileattr_bulk_fast: DMG
DMG
__settileattr_bulk_fast: CGB
CGB

完整示例程序

构建命令

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqgb\kitaqgb.exe .\kitaq-docs\samples\api-examples\gb\__settileattr_bulk_fast.c -I .\kitaqgb\lib -I .\kitaq-docs\samples -o .\out\__settileattr_bulk_fast.gb --profile=dev --rst-disable --stack-bank=fixed --cgb=cgb --no-disasm
实现与源码

kitaqgb/lib/cgb_tile.h:91

声明注释(原文)
Copy count attributes to dest in bank one on CGB, then leave VBK zero; skip on DMG. The caller supplies writable VRAM timing.
__settileattr_unsafe

只修改 CGB 属性字节,保留图块编号。属性指定调色板、图块图像库、水平或垂直翻转和优先级。

目标地址始终为 0x9800 + 32*y + x,不随 LCDC 的背景或窗口地图选择而改变。

不等待 VRAM 可写。只能在 LCD 关闭时,或由调用方确保整个传输都处于可写时间段时使用。

语法

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

参数

x, y

以 8×8 图块为单位的坐标,各为 0~31。整个写入范围必须位于 32×32 地图内;不会自动裁剪坐标。

attr

CGB 属性字节:位 0~2 选择调色板 0~7,位 3 选择图像数据 bank,位 5、6 分别控制水平和垂直翻转,位 7 设置背景/窗口优先级。未使用的位 4 应为 0。

返回值

无(void)。

使用条件与限制

CGB 上切到 VRAM 库 1 写入属性,结束时置 VBK=0。DMG 上不执行属性写入。

示例使用 gb_common.h。m_init() 将附带 ASCII 字体加载到与字符编码相同的图块编号。m_wait(); M_LCDC=0; 在 VBlank 中关闭 LCD,以便进行无需等待的传输。最后的 LCDC 值选择显示地图。

使用示例

c
// __settileattr_unsafe
// Write only the CGB attribute byte; leave the tile number unchanged. Attribute bits
// select palette, tile-data bank, flips and priority. The destination is always `0x9800
// + 32*y + x`, regardless of which maps LCDC selects for the background and window.
// Expected: FAILED CHECKS 000, with the written tile data in the selected map.
#include "gb_tile_example.h"

void main() {
    u8 failures;
    failures = 0;
    tile_example_begin();
    __settileat(0x9800, 2, 3, 'A');
    __settileattr_unsafe(2, 3, KQ_CGB_ATTR(2, 0, 0, 0, 0));
    if (tile_example_read(0x9862, 0) != 'A') { failures++; }
    if (tile_example_read(0x9C62, 0) != 0) { failures++; }
    if (__cgb_is_cgb() && tile_example_read(0x9862, 1) != 2) { failures++; }
    if (__cgb_is_cgb() && tile_example_read(0x9C62, 1) != 0) { failures++; }
    tile_example_finish(failures, 0);
}

预期结果

所选地图显示写入的字符或填充区域。程序读回目标图块编号,并在 CGB 上读回属性进行比较;FAILED CHECKS 000 表示比较全部通过。两张地图初始均为 0,调用前将背景/窗口地图选为 0x9C00,因此能区分固定地图与所选地图的写入。示例在 LCD 关闭时写入,不测量 LCD 开启时的时序。

CGB 上的 A 为蓝色:属性值 2 选择示例设置的蓝色调色板。DMG 不应用 CGB 属性,因此同一字符为黑色。

预期图像:以下图块排列从图块坐标 (x=2, y=3)、像素坐标 (16,24) 开始显示。每个字符占一个 8×8 图块。按从上到下、从左到右阅读,周围图块保持空白。

text
A
__settileattr_unsafe: DMG
DMG
__settileattr_unsafe: CGB
CGB

完整示例程序

构建命令

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqgb\kitaqgb.exe .\kitaq-docs\samples\api-examples\gb\__settileattr_unsafe.c -I .\kitaqgb\lib -I .\kitaq-docs\samples -o .\out\__settileattr_unsafe.gb --profile=dev --rst-disable --stack-bank=fixed --cgb=cgb --no-disasm
实现与源码

kitaqgb/lib/cgb_tile.h:55

声明注释(原文)
Write attributes in bank one at map 9800 on CGB, then leave VBK zero; skip on DMG. No VRAM access waits are inserted.
__settilebg

向图块地图的一个单元写入图块编号。这是在选择已有图块图像,不会修改像素数据。

写入 LCDC 位 3 当前选择的背景地图:0 对应 0x9800,1 对应 0x9C00,再加上 32*y + x。

必要时等待 VRAM 可写。LCD 开启时的路径会屏蔽中断,并可能在结束后启用中断;不保证保留调用前的中断状态,也不保证在一次 VBlank 内完成。

语法

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

参数

x, y

以 8×8 图块为单位的坐标,各为 0~31。整个写入范围必须位于 32×32 地图内;不会自动裁剪坐标。

tile

0~255 的图块编号。LCDC 位 4 决定编号如何寻址图块图像数据;这不是 VRAM 字节地址。

返回值

无(void)。

使用条件与限制

图块编号写入当前选中的 VRAM 库。CGB 上应选择编号所在的库 0;此调用不会代为切换库。

示例使用 gb_common.h。m_init() 将附带 ASCII 字体加载到与字符编码相同的图块编号。m_wait(); M_LCDC=0; 在 VBlank 中关闭 LCD,以便进行无需等待的传输。最后的 LCDC 值选择显示地图。

使用示例

c
// __settilebg
// Write one tile-number byte to the tile map; this selects an existing tile image, not
// new pixel data. LCDC bit 3 selects the background map: zero uses `0x9800`, one uses
// `0x9C00`. The destination is that base plus `32*y + x`.
// Expected: FAILED CHECKS 000, with the written tile data in the selected map.
#include "gb_tile_color_example.h"

void main() {
    u8 failures;
    failures = 0;
    tile_color_example_begin();
    __settilebg(2, 3, 'A');
    if (tile_example_read(0x9C62, 0) != 'A') { failures++; }
    if (tile_example_read(0x9862, 0) != 0) { failures++; }
    tile_example_finish(failures, 1);
}

预期结果

所选地图显示写入的字符或填充区域。程序读回目标图块编号,并在 CGB 上读回属性进行比较;FAILED CHECKS 000 表示比较全部通过。两张地图初始均为 0,调用前将背景/窗口地图选为 0x9C00,因此能区分固定地图与所选地图的写入。示例在 LCD 关闭时写入,不测量 LCD 开启时的时序。

在 CGB 上,示例字符和结果文字以白底蓝字显示。初始化时将调色板 0 的绘制颜色设为蓝色,不修改图块映射的属性。在 DMG 上,相同字符显示为白底黑字。

预期图像:以下图块排列从图块坐标 (x=2, y=3)、像素坐标 (16,24) 开始显示。每个字符占一个 8×8 图块。按从上到下、从左到右阅读,周围图块保持空白。

text
A
__settilebg: DMG
DMG
__settilebg: CGB
CGB

完整示例程序

构建命令

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqgb\kitaqgb.exe .\kitaq-docs\samples\api-examples\gb\__settilebg.c -I .\kitaqgb\lib -I .\kitaq-docs\samples -o .\out\__settilebg.gb --profile=dev --rst-disable --stack-bank=fixed --cgb=cgb --no-disasm
实现与源码

kitaqgb/lib/cgb_tile.h:43

声明注释(原文)
Select the background map from LCDC bit 3, then write with access checks.
__settilebg_unsafe

向图块地图的一个单元写入图块编号。这是在选择已有图块图像,不会修改像素数据。

写入 LCDC 位 3 当前选择的背景地图:0 对应 0x9800,1 对应 0x9C00,再加上 32*y + x。

不等待 VRAM 可写。只能在 LCD 关闭时,或由调用方确保整个传输都处于可写时间段时使用。

语法

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

参数

x, y

以 8×8 图块为单位的坐标,各为 0~31。整个写入范围必须位于 32×32 地图内;不会自动裁剪坐标。

tile

0~255 的图块编号。LCDC 位 4 决定编号如何寻址图块图像数据;这不是 VRAM 字节地址。

返回值

无(void)。

使用条件与限制

图块编号写入当前选中的 VRAM 库。CGB 上应选择编号所在的库 0;此调用不会代为切换库。

示例使用 gb_common.h。m_init() 将附带 ASCII 字体加载到与字符编码相同的图块编号。m_wait(); M_LCDC=0; 在 VBlank 中关闭 LCD,以便进行无需等待的传输。最后的 LCDC 值选择显示地图。

使用示例

c
// __settilebg_unsafe
// Write one tile-number byte to the tile map; this selects an existing tile image, not
// new pixel data. LCDC bit 3 selects the background map: zero uses `0x9800`, one uses
// `0x9C00`. The destination is that base plus `32*y + x`.
// Expected: FAILED CHECKS 000, with the written tile data in the selected map.
#include "gb_tile_color_example.h"

void main() {
    u8 failures;
    failures = 0;
    tile_color_example_begin();
    __settilebg_unsafe(2, 3, 'A');
    if (tile_example_read(0x9C62, 0) != 'A') { failures++; }
    if (tile_example_read(0x9862, 0) != 0) { failures++; }
    tile_example_finish(failures, 1);
}

预期结果

所选地图显示写入的字符或填充区域。程序读回目标图块编号,并在 CGB 上读回属性进行比较;FAILED CHECKS 000 表示比较全部通过。两张地图初始均为 0,调用前将背景/窗口地图选为 0x9C00,因此能区分固定地图与所选地图的写入。示例在 LCD 关闭时写入,不测量 LCD 开启时的时序。

在 CGB 上,示例字符和结果文字以白底蓝字显示。初始化时将调色板 0 的绘制颜色设为蓝色,不修改图块映射的属性。在 DMG 上,相同字符显示为白底黑字。

预期图像:以下图块排列从图块坐标 (x=2, y=3)、像素坐标 (16,24) 开始显示。每个字符占一个 8×8 图块。按从上到下、从左到右阅读,周围图块保持空白。

text
A
__settilebg_unsafe: DMG
DMG
__settilebg_unsafe: CGB
CGB

完整示例程序

构建命令

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqgb\kitaqgb.exe .\kitaq-docs\samples\api-examples\gb\__settilebg_unsafe.c -I .\kitaqgb\lib -I .\kitaq-docs\samples -o .\out\__settilebg_unsafe.gb --profile=dev --rst-disable --stack-bank=fixed --cgb=cgb --no-disasm
实现与源码

kitaqgb/lib/cgb_tile.h:45

声明注释(原文)
Select the LCDC background map and write without access waits.
__settilebgattr

只修改 CGB 属性字节,保留图块编号。属性指定调色板、图块图像库、水平或垂直翻转和优先级。

写入 LCDC 位 3 当前选择的背景地图:0 对应 0x9800,1 对应 0x9C00,再加上 32*y + x。

必要时等待 VRAM 可写。LCD 开启时的路径会屏蔽中断,并可能在结束后启用中断;不保证保留调用前的中断状态,也不保证在一次 VBlank 内完成。

语法

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

参数

x, y

以 8×8 图块为单位的坐标,各为 0~31。整个写入范围必须位于 32×32 地图内;不会自动裁剪坐标。

attr

CGB 属性字节:位 0~2 选择调色板 0~7,位 3 选择图像数据 bank,位 5、6 分别控制水平和垂直翻转,位 7 设置背景/窗口优先级。未使用的位 4 应为 0。

返回值

无(void)。

使用条件与限制

CGB 上切到 VRAM 库 1 写入属性,结束时置 VBK=0。DMG 上不执行属性写入。

示例使用 gb_common.h。m_init() 将附带 ASCII 字体加载到与字符编码相同的图块编号。m_wait(); M_LCDC=0; 在 VBlank 中关闭 LCD,以便进行无需等待的传输。最后的 LCDC 值选择显示地图。

使用示例

c
// __settilebgattr
// Write only the CGB attribute byte; leave the tile number unchanged. Attribute bits
// select palette, tile-data bank, flips and priority. LCDC bit 3 selects the background
// map: zero uses `0x9800`, one uses `0x9C00`. The destination is that base plus `32*y +
// x`.
// Expected: FAILED CHECKS 000, with the written tile data in the selected map.
#include "gb_tile_example.h"

void main() {
    u8 failures;
    failures = 0;
    tile_example_begin();
    __settileat(0x9C00, 2, 3, 'A');
    __settilebgattr(2, 3, KQ_CGB_ATTR(2, 0, 0, 0, 0));
    if (tile_example_read(0x9C62, 0) != 'A') { failures++; }
    if (tile_example_read(0x9862, 0) != 0) { failures++; }
    if (__cgb_is_cgb() && tile_example_read(0x9C62, 1) != 2) { failures++; }
    if (__cgb_is_cgb() && tile_example_read(0x9862, 1) != 0) { failures++; }
    tile_example_finish(failures, 1);
}

预期结果

所选地图显示写入的字符或填充区域。程序读回目标图块编号,并在 CGB 上读回属性进行比较;FAILED CHECKS 000 表示比较全部通过。两张地图初始均为 0,调用前将背景/窗口地图选为 0x9C00,因此能区分固定地图与所选地图的写入。示例在 LCD 关闭时写入,不测量 LCD 开启时的时序。

CGB 上的 A 为蓝色:属性值 2 选择示例设置的蓝色调色板。DMG 不应用 CGB 属性,因此同一字符为黑色。

预期图像:以下图块排列从图块坐标 (x=2, y=3)、像素坐标 (16,24) 开始显示。每个字符占一个 8×8 图块。按从上到下、从左到右阅读,周围图块保持空白。

text
A
__settilebgattr: DMG
DMG
__settilebgattr: CGB
CGB

完整示例程序

构建命令

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqgb\kitaqgb.exe .\kitaq-docs\samples\api-examples\gb\__settilebgattr.c -I .\kitaqgb\lib -I .\kitaq-docs\samples -o .\out\__settilebgattr.gb --profile=dev --rst-disable --stack-bank=fixed --cgb=cgb --no-disasm
实现与源码

kitaqgb/lib/cgb_tile.h:80

声明注释(原文)
Write attributes in bank one at the LCDC background map on CGB, then leave VBK zero; skip on DMG. Use the access-checked write path.
__settilebgattr_unsafe

只修改 CGB 属性字节,保留图块编号。属性指定调色板、图块图像库、水平或垂直翻转和优先级。

写入 LCDC 位 3 当前选择的背景地图:0 对应 0x9800,1 对应 0x9C00,再加上 32*y + x。

不等待 VRAM 可写。只能在 LCD 关闭时,或由调用方确保整个传输都处于可写时间段时使用。

语法

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

参数

x, y

以 8×8 图块为单位的坐标,各为 0~31。整个写入范围必须位于 32×32 地图内;不会自动裁剪坐标。

attr

CGB 属性字节:位 0~2 选择调色板 0~7,位 3 选择图像数据 bank,位 5、6 分别控制水平和垂直翻转,位 7 设置背景/窗口优先级。未使用的位 4 应为 0。

返回值

无(void)。

使用条件与限制

CGB 上切到 VRAM 库 1 写入属性,结束时置 VBK=0。DMG 上不执行属性写入。

示例使用 gb_common.h。m_init() 将附带 ASCII 字体加载到与字符编码相同的图块编号。m_wait(); M_LCDC=0; 在 VBlank 中关闭 LCD,以便进行无需等待的传输。最后的 LCDC 值选择显示地图。

使用示例

c
// __settilebgattr_unsafe
// Write only the CGB attribute byte; leave the tile number unchanged. Attribute bits
// select palette, tile-data bank, flips and priority. LCDC bit 3 selects the background
// map: zero uses `0x9800`, one uses `0x9C00`. The destination is that base plus `32*y +
// x`.
// Expected: FAILED CHECKS 000, with the written tile data in the selected map.
#include "gb_tile_example.h"

void main() {
    u8 failures;
    failures = 0;
    tile_example_begin();
    __settileat(0x9C00, 2, 3, 'A');
    __settilebgattr_unsafe(2, 3, KQ_CGB_ATTR(2, 0, 0, 0, 0));
    if (tile_example_read(0x9C62, 0) != 'A') { failures++; }
    if (tile_example_read(0x9862, 0) != 0) { failures++; }
    if (__cgb_is_cgb() && tile_example_read(0x9C62, 1) != 2) { failures++; }
    if (__cgb_is_cgb() && tile_example_read(0x9862, 1) != 0) { failures++; }
    tile_example_finish(failures, 1);
}

预期结果

所选地图显示写入的字符或填充区域。程序读回目标图块编号,并在 CGB 上读回属性进行比较;FAILED CHECKS 000 表示比较全部通过。两张地图初始均为 0,调用前将背景/窗口地图选为 0x9C00,因此能区分固定地图与所选地图的写入。示例在 LCD 关闭时写入,不测量 LCD 开启时的时序。

CGB 上的 A 为蓝色:属性值 2 选择示例设置的蓝色调色板。DMG 不应用 CGB 属性,因此同一字符为黑色。

预期图像:以下图块排列从图块坐标 (x=2, y=3)、像素坐标 (16,24) 开始显示。每个字符占一个 8×8 图块。按从上到下、从左到右阅读,周围图块保持空白。

text
A
__settilebgattr_unsafe: DMG
DMG
__settilebgattr_unsafe: CGB
CGB

完整示例程序

构建命令

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqgb\kitaqgb.exe .\kitaq-docs\samples\api-examples\gb\__settilebgattr_unsafe.c -I .\kitaqgb\lib -I .\kitaq-docs\samples -o .\out\__settilebgattr_unsafe.gb --profile=dev --rst-disable --stack-bank=fixed --cgb=cgb --no-disasm
实现与源码

kitaqgb/lib/cgb_tile.h:82

声明注释(原文)
Write attributes in bank one at the LCDC background map on CGB, then leave VBK zero; skip on DMG. No VRAM access waits are inserted.
__settilebgcgb

依次写入图块编号和 CGB 属性。DMG 只写编号。CGB 调用前应选择 VRAM 库 0;属性写入库 1,结束时为 VBK=0。

写入 LCDC 位 3 当前选择的背景地图:0 对应 0x9800,1 对应 0x9C00,再加上 32*y + x。

必要时等待 VRAM 可写。LCD 开启时的路径会屏蔽中断,并可能在结束后启用中断;不保证保留调用前的中断状态,也不保证在一次 VBlank 内完成。

语法

c
void __settilebgcgb(u8 x, u8 y, u8 tile, u8 attr);

参数

x, y

以 8×8 图块为单位的坐标,各为 0~31。整个写入范围必须位于 32×32 地图内;不会自动裁剪坐标。

tile

0~255 的图块编号。LCDC 位 4 决定编号如何寻址图块图像数据;这不是 VRAM 字节地址。

attr

CGB 属性字节:位 0~2 选择调色板 0~7,位 3 选择图像数据 bank,位 5、6 分别控制水平和垂直翻转,位 7 设置背景/窗口优先级。未使用的位 4 应为 0。

返回值

无(void)。

使用条件与限制

示例使用 gb_common.h。m_init() 将附带 ASCII 字体加载到与字符编码相同的图块编号。m_wait(); M_LCDC=0; 在 VBlank 中关闭 LCD,以便进行无需等待的传输。最后的 LCDC 值选择显示地图。

使用示例

c
// __settilebgcgb
// Write a tile number and its CGB attribute. On DMG, only the tile-number write is
// performed. On CGB, enter with VRAM bank 0 selected; attributes use bank 1 and the call
// finishes with `VBK=0`. LCDC bit 3 selects the background map: zero uses `0x9800`, one
// uses `0x9C00`. The destination is that base plus `32*y + x`.
// Expected: FAILED CHECKS 000, with the written tile data in the selected map.
#include "gb_tile_example.h"

void main() {
    u8 failures;
    failures = 0;
    tile_example_begin();
    __settilebgcgb(2, 3, 'A', KQ_CGB_ATTR(2, 0, 0, 0, 0));
    if (tile_example_read(0x9C62, 0) != 'A') { failures++; }
    if (tile_example_read(0x9862, 0) != 0) { failures++; }
    if (__cgb_is_cgb() && tile_example_read(0x9C62, 1) != 2) { failures++; }
    if (__cgb_is_cgb() && tile_example_read(0x9862, 1) != 0) { failures++; }
    tile_example_finish(failures, 1);
}

预期结果

所选地图显示写入的字符或填充区域。程序读回目标图块编号,并在 CGB 上读回属性进行比较;FAILED CHECKS 000 表示比较全部通过。两张地图初始均为 0,调用前将背景/窗口地图选为 0x9C00,因此能区分固定地图与所选地图的写入。示例在 LCD 关闭时写入,不测量 LCD 开启时的时序。

CGB 上的 A 为蓝色:属性值 2 选择示例设置的蓝色调色板。DMG 不应用 CGB 属性,因此同一字符为黑色。

预期图像:以下图块排列从图块坐标 (x=2, y=3)、像素坐标 (16,24) 开始显示。每个字符占一个 8×8 图块。按从上到下、从左到右阅读,周围图块保持空白。

text
A
__settilebgcgb: DMG
DMG
__settilebgcgb: CGB
CGB

完整示例程序

构建命令

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqgb\kitaqgb.exe .\kitaq-docs\samples\api-examples\gb\__settilebgcgb.c -I .\kitaqgb\lib -I .\kitaq-docs\samples -o .\out\__settilebgcgb.gb --profile=dev --rst-disable --stack-bank=fixed --cgb=cgb --no-disasm
实现与源码

kitaqgb/lib/cgb_tile.h:84

声明注释(原文)
Write tile and CGB attributes at the LCDC background map; DMG receives only the tile write. Use the access-checked write path.
__settilebgcgb_unsafe

依次写入图块编号和 CGB 属性。DMG 只写编号。CGB 调用前应选择 VRAM 库 0;属性写入库 1,结束时为 VBK=0。

写入 LCDC 位 3 当前选择的背景地图:0 对应 0x9800,1 对应 0x9C00,再加上 32*y + x。

不等待 VRAM 可写。只能在 LCD 关闭时,或由调用方确保整个传输都处于可写时间段时使用。

语法

c
void __settilebgcgb_unsafe(u8 x, u8 y, u8 tile, u8 attr);

参数

x, y

以 8×8 图块为单位的坐标,各为 0~31。整个写入范围必须位于 32×32 地图内;不会自动裁剪坐标。

tile

0~255 的图块编号。LCDC 位 4 决定编号如何寻址图块图像数据;这不是 VRAM 字节地址。

attr

CGB 属性字节:位 0~2 选择调色板 0~7,位 3 选择图像数据 bank,位 5、6 分别控制水平和垂直翻转,位 7 设置背景/窗口优先级。未使用的位 4 应为 0。

返回值

无(void)。

使用条件与限制

示例使用 gb_common.h。m_init() 将附带 ASCII 字体加载到与字符编码相同的图块编号。m_wait(); M_LCDC=0; 在 VBlank 中关闭 LCD,以便进行无需等待的传输。最后的 LCDC 值选择显示地图。

使用示例

c
// __settilebgcgb_unsafe
// Write a tile number and its CGB attribute. On DMG, only the tile-number write is
// performed. On CGB, enter with VRAM bank 0 selected; attributes use bank 1 and the call
// finishes with `VBK=0`. LCDC bit 3 selects the background map: zero uses `0x9800`, one
// uses `0x9C00`. The destination is that base plus `32*y + x`.
// Expected: FAILED CHECKS 000, with the written tile data in the selected map.
#include "gb_tile_example.h"

void main() {
    u8 failures;
    failures = 0;
    tile_example_begin();
    __settilebgcgb_unsafe(2, 3, 'A', KQ_CGB_ATTR(2, 0, 0, 0, 0));
    if (tile_example_read(0x9C62, 0) != 'A') { failures++; }
    if (tile_example_read(0x9862, 0) != 0) { failures++; }
    if (__cgb_is_cgb() && tile_example_read(0x9C62, 1) != 2) { failures++; }
    if (__cgb_is_cgb() && tile_example_read(0x9862, 1) != 0) { failures++; }
    tile_example_finish(failures, 1);
}

预期结果

所选地图显示写入的字符或填充区域。程序读回目标图块编号,并在 CGB 上读回属性进行比较;FAILED CHECKS 000 表示比较全部通过。两张地图初始均为 0,调用前将背景/窗口地图选为 0x9C00,因此能区分固定地图与所选地图的写入。示例在 LCD 关闭时写入,不测量 LCD 开启时的时序。

CGB 上的 A 为蓝色:属性值 2 选择示例设置的蓝色调色板。DMG 不应用 CGB 属性,因此同一字符为黑色。

预期图像:以下图块排列从图块坐标 (x=2, y=3)、像素坐标 (16,24) 开始显示。每个字符占一个 8×8 图块。按从上到下、从左到右阅读,周围图块保持空白。

text
A
__settilebgcgb_unsafe: DMG
DMG
__settilebgcgb_unsafe: CGB
CGB

完整示例程序

构建命令

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqgb\kitaqgb.exe .\kitaq-docs\samples\api-examples\gb\__settilebgcgb_unsafe.c -I .\kitaqgb\lib -I .\kitaq-docs\samples -o .\out\__settilebgcgb_unsafe.gb --profile=dev --rst-disable --stack-bank=fixed --cgb=cgb --no-disasm
实现与源码

kitaqgb/lib/cgb_tile.h:86

声明注释(原文)
Write tile and CGB attributes at the LCDC background map; DMG receives only the tile write. No VRAM access waits are inserted.
__settilecgb

依次写入图块编号和 CGB 属性。DMG 只写编号。CGB 调用前应选择 VRAM 库 0;属性写入库 1,结束时为 VBK=0。

目标地址始终为 0x9800 + 32*y + x,不随 LCDC 的背景或窗口地图选择而改变。

必要时等待 VRAM 可写。LCD 开启时的路径会屏蔽中断,并可能在结束后启用中断;不保证保留调用前的中断状态,也不保证在一次 VBlank 内完成。

语法

c
void __settilecgb(u8 x, u8 y, u8 tile, u8 attr);

参数

x, y

以 8×8 图块为单位的坐标,各为 0~31。整个写入范围必须位于 32×32 地图内;不会自动裁剪坐标。

tile

0~255 的图块编号。LCDC 位 4 决定编号如何寻址图块图像数据;这不是 VRAM 字节地址。

attr

CGB 属性字节:位 0~2 选择调色板 0~7,位 3 选择图像数据 bank,位 5、6 分别控制水平和垂直翻转,位 7 设置背景/窗口优先级。未使用的位 4 应为 0。

返回值

无(void)。

使用条件与限制

示例使用 gb_common.h。m_init() 将附带 ASCII 字体加载到与字符编码相同的图块编号。m_wait(); M_LCDC=0; 在 VBlank 中关闭 LCD,以便进行无需等待的传输。最后的 LCDC 值选择显示地图。

使用示例

c
// __settilecgb
// Write a tile number and its CGB attribute. On DMG, only the tile-number write is
// performed. On CGB, enter with VRAM bank 0 selected; attributes use bank 1 and the call
// finishes with `VBK=0`. The destination is always `0x9800 + 32*y + x`, regardless of
// which maps LCDC selects for the background and window.
// Expected: FAILED CHECKS 000, with the written tile data in the selected map.
#include "gb_tile_example.h"

void main() {
    u8 failures;
    failures = 0;
    tile_example_begin();
    __settilecgb(2, 3, 'A', KQ_CGB_ATTR(2, 0, 0, 0, 0));
    if (tile_example_read(0x9862, 0) != 'A') { failures++; }
    if (tile_example_read(0x9C62, 0) != 0) { failures++; }
    if (__cgb_is_cgb() && tile_example_read(0x9862, 1) != 2) { failures++; }
    if (__cgb_is_cgb() && tile_example_read(0x9C62, 1) != 0) { failures++; }
    tile_example_finish(failures, 0);
}

预期结果

所选地图显示写入的字符或填充区域。程序读回目标图块编号,并在 CGB 上读回属性进行比较;FAILED CHECKS 000 表示比较全部通过。两张地图初始均为 0,调用前将背景/窗口地图选为 0x9C00,因此能区分固定地图与所选地图的写入。示例在 LCD 关闭时写入,不测量 LCD 开启时的时序。

CGB 上的 A 为蓝色:属性值 2 选择示例设置的蓝色调色板。DMG 不应用 CGB 属性,因此同一字符为黑色。

预期图像:以下图块排列从图块坐标 (x=2, y=3)、像素坐标 (16,24) 开始显示。每个字符占一个 8×8 图块。按从上到下、从左到右阅读,周围图块保持空白。

text
A
__settilecgb: DMG
DMG
__settilecgb: CGB
CGB

完整示例程序

构建命令

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqgb\kitaqgb.exe .\kitaq-docs\samples\api-examples\gb\__settilecgb.c -I .\kitaqgb\lib -I .\kitaq-docs\samples -o .\out\__settilecgb.gb --profile=dev --rst-disable --stack-bank=fixed --cgb=cgb --no-disasm
实现与源码

kitaqgb/lib/cgb_tile.h:57

声明注释(原文)
Write tile and CGB attributes at map 9800; DMG receives only the tile write. Use the access-checked write path.
__settilecgb_bulk

分别从 tile_src 和 attr_src 复制 count 个编号与属性字节,目标均从 dest 开始。两者是独立数组,不是交错存储的字节对。

必要时等待 VRAM 可写。LCD 开启时的路径会屏蔽中断,并可能在结束后启用中断;不保证保留调用前的中断状态,也不保证在一次 VBlank 内完成。

语法

c
void __settilecgb_bulk(u16 dest, const u8* tile_src, const u8* attr_src, u8 count);

参数

dest

起始 VRAM 字节地址。地图使用 0x9800–0x9FFF 范围,整个传输应位于目标地图内。

tile_src, attr_src

位于 RAM 或当前可见 ROM bank 中的连续字节。图块流保存图块编号,属性流保存 CGB 属性字节。函数立即复制,不保留指针。

count

字节数,范围 0~255。为 0 时不复制;源数据至少要有这么多可读字节。

返回值

无(void)。

使用条件与限制

依次写入图块编号和 CGB 属性。DMG 只写编号。CGB 调用前应选择 VRAM 库 0;属性写入库 1,结束时为 VBK=0。

复制结束前,必须确保源缓冲区及其 ROM 库始终可读。无返回值。

示例使用 gb_common.h。m_init() 将附带 ASCII 字体加载到与字符编码相同的图块编号。m_wait(); M_LCDC=0; 在 VBlank 中关闭 LCD,以便进行无需等待的传输。最后的 LCDC 值选择显示地图。

使用示例

c
// __settilecgb_bulk
// Copy `count` tile numbers from `tile_src`, then `count` attributes from `attr_src`, to
// the same map addresses starting at `dest`. These are separate byte arrays, not
// interleaved pairs. Waits for writable VRAM when necessary. The LCD-on path uses
// interrupt masking and can enable interrupts afterward; it is not a promise to preserve
// the caller's interrupt state or to finish within one VBlank.
// Expected: FAILED CHECKS 000, with the written tile data in the selected map.
#include "gb_tile_example.h"
const u8 example_tiles[] = {'A','B','C'};
const u8 example_attrs[] = {1,2,3};

void main() {
    u8 failures;
    failures = 0;
    tile_example_begin();
    __settilecgb_bulk(0x9862, example_tiles, example_attrs, 3);
    if (tile_example_read(0x9862, 0) != 'A') { failures++; }
    if (tile_example_read(0x9863, 0) != 'B') { failures++; }
    if (tile_example_read(0x9864, 0) != 'C') { failures++; }
    if (tile_example_read(0x9865, 0) != 0) { failures++; }
    if (__cgb_is_cgb() && tile_example_read(0x9862, 1) != 1) { failures++; }
    if (__cgb_is_cgb() && tile_example_read(0x9864, 1) != 3) { failures++; }
    tile_example_finish(failures, 0);
}

预期结果

所选地图显示写入的字符或填充区域。程序读回目标图块编号,并在 CGB 上读回属性进行比较;FAILED CHECKS 000 表示比较全部通过。两张地图初始均为 0,调用前将背景/窗口地图选为 0x9C00,因此能区分固定地图与所选地图的写入。示例在 LCD 关闭时写入,不测量 LCD 开启时的时序。

CGB 上,属性字节 1,2,3 分别选择三个预设调色板,使 A 为红色、B 为蓝色、C 为绿色。DMG 上三个字符均为黑色。

预期图像:以下图块排列从图块坐标 (x=2, y=3)、像素坐标 (16,24) 开始显示。每个字符占一个 8×8 图块。按从上到下、从左到右阅读,周围图块保持空白。

text
ABC
__settilecgb_bulk: DMG
DMG
__settilecgb_bulk: CGB
CGB

完整示例程序

构建命令

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqgb\kitaqgb.exe .\kitaq-docs\samples\api-examples\gb\__settilecgb_bulk.c -I .\kitaqgb\lib -I .\kitaq-docs\samples -o .\out\__settilecgb_bulk.gb --profile=dev --rst-disable --stack-bank=fixed --cgb=cgb --no-disasm
实现与源码

kitaqgb/lib/cgb_tile.h:93

声明注释(原文)
Copy tile and attribute streams to dest; DMG receives only tiles. Keep both sources readable. Transfers use access checks.
__settilecgb_bulk_fast

分别从 tile_src 和 attr_src 复制 count 个编号与属性字节,目标均从 dest 开始。两者是独立数组,不是交错存储的字节对。

不等待 VRAM 可写。只能在 LCD 关闭时,或由调用方确保整个传输都处于可写时间段时使用。

语法

c
void __settilecgb_bulk_fast(u16 dest, const u8* tile_src, const u8* attr_src, u8 count);

参数

dest

起始 VRAM 字节地址。地图使用 0x9800–0x9FFF 范围,整个传输应位于目标地图内。

tile_src, attr_src

位于 RAM 或当前可见 ROM bank 中的连续字节。图块流保存图块编号,属性流保存 CGB 属性字节。函数立即复制,不保留指针。

count

字节数,范围 0~255。为 0 时不复制;源数据至少要有这么多可读字节。

返回值

无(void)。

使用条件与限制

依次写入图块编号和 CGB 属性。DMG 只写编号。CGB 调用前应选择 VRAM 库 0;属性写入库 1,结束时为 VBK=0。

复制结束前,必须确保源缓冲区及其 ROM 库始终可读。无返回值。

示例使用 gb_common.h。m_init() 将附带 ASCII 字体加载到与字符编码相同的图块编号。m_wait(); M_LCDC=0; 在 VBlank 中关闭 LCD,以便进行无需等待的传输。最后的 LCDC 值选择显示地图。

使用示例

c
// __settilecgb_bulk_fast
// Copy `count` tile numbers from `tile_src`, then `count` attributes from `attr_src`, to
// the same map addresses starting at `dest`. These are separate byte arrays, not
// interleaved pairs. Does not wait for VRAM access. Call only while the LCD is off or
// within a caller-managed writable interval with enough time for the whole transfer.
// Expected: FAILED CHECKS 000, with the written tile data in the selected map.
#include "gb_tile_example.h"
const u8 example_tiles[] = {'A','B','C'};
const u8 example_attrs[] = {1,2,3};

void main() {
    u8 failures;
    failures = 0;
    tile_example_begin();
    __settilecgb_bulk_fast(0x9862, example_tiles, example_attrs, 3);
    if (tile_example_read(0x9862, 0) != 'A') { failures++; }
    if (tile_example_read(0x9863, 0) != 'B') { failures++; }
    if (tile_example_read(0x9864, 0) != 'C') { failures++; }
    if (tile_example_read(0x9865, 0) != 0) { failures++; }
    if (__cgb_is_cgb() && tile_example_read(0x9862, 1) != 1) { failures++; }
    if (__cgb_is_cgb() && tile_example_read(0x9864, 1) != 3) { failures++; }
    tile_example_finish(failures, 0);
}

预期结果

所选地图显示写入的字符或填充区域。程序读回目标图块编号,并在 CGB 上读回属性进行比较;FAILED CHECKS 000 表示比较全部通过。两张地图初始均为 0,调用前将背景/窗口地图选为 0x9C00,因此能区分固定地图与所选地图的写入。示例在 LCD 关闭时写入,不测量 LCD 开启时的时序。

CGB 上,属性字节 1,2,3 分别选择三个预设调色板,使 A 为红色、B 为蓝色、C 为绿色。DMG 上三个字符均为黑色。

预期图像:以下图块排列从图块坐标 (x=2, y=3)、像素坐标 (16,24) 开始显示。每个字符占一个 8×8 图块。按从上到下、从左到右阅读,周围图块保持空白。

text
ABC
__settilecgb_bulk_fast: DMG
DMG
__settilecgb_bulk_fast: CGB
CGB

完整示例程序

构建命令

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqgb\kitaqgb.exe .\kitaq-docs\samples\api-examples\gb\__settilecgb_bulk_fast.c -I .\kitaqgb\lib -I .\kitaq-docs\samples -o .\out\__settilecgb_bulk_fast.gb --profile=dev --rst-disable --stack-bank=fixed --cgb=cgb --no-disasm
实现与源码

kitaqgb/lib/cgb_tile.h:95

声明注释(原文)
Copy tile and attribute streams to dest; DMG receives only tiles. Keep both sources readable. The caller supplies writable VRAM timing.
__settilecgb_unsafe

依次写入图块编号和 CGB 属性。DMG 只写编号。CGB 调用前应选择 VRAM 库 0;属性写入库 1,结束时为 VBK=0。

目标地址始终为 0x9800 + 32*y + x,不随 LCDC 的背景或窗口地图选择而改变。

不等待 VRAM 可写。只能在 LCD 关闭时,或由调用方确保整个传输都处于可写时间段时使用。

语法

c
void __settilecgb_unsafe(u8 x, u8 y, u8 tile, u8 attr);

参数

x, y

以 8×8 图块为单位的坐标,各为 0~31。整个写入范围必须位于 32×32 地图内;不会自动裁剪坐标。

tile

0~255 的图块编号。LCDC 位 4 决定编号如何寻址图块图像数据;这不是 VRAM 字节地址。

attr

CGB 属性字节:位 0~2 选择调色板 0~7,位 3 选择图像数据 bank,位 5、6 分别控制水平和垂直翻转,位 7 设置背景/窗口优先级。未使用的位 4 应为 0。

返回值

无(void)。

使用条件与限制

示例使用 gb_common.h。m_init() 将附带 ASCII 字体加载到与字符编码相同的图块编号。m_wait(); M_LCDC=0; 在 VBlank 中关闭 LCD,以便进行无需等待的传输。最后的 LCDC 值选择显示地图。

使用示例

c
// __settilecgb_unsafe
// Write a tile number and its CGB attribute. On DMG, only the tile-number write is
// performed. On CGB, enter with VRAM bank 0 selected; attributes use bank 1 and the call
// finishes with `VBK=0`. The destination is always `0x9800 + 32*y + x`, regardless of
// which maps LCDC selects for the background and window.
// Expected: FAILED CHECKS 000, with the written tile data in the selected map.
#include "gb_tile_example.h"

void main() {
    u8 failures;
    failures = 0;
    tile_example_begin();
    __settilecgb_unsafe(2, 3, 'A', KQ_CGB_ATTR(2, 0, 0, 0, 0));
    if (tile_example_read(0x9862, 0) != 'A') { failures++; }
    if (tile_example_read(0x9C62, 0) != 0) { failures++; }
    if (__cgb_is_cgb() && tile_example_read(0x9862, 1) != 2) { failures++; }
    if (__cgb_is_cgb() && tile_example_read(0x9C62, 1) != 0) { failures++; }
    tile_example_finish(failures, 0);
}

预期结果

所选地图显示写入的字符或填充区域。程序读回目标图块编号,并在 CGB 上读回属性进行比较;FAILED CHECKS 000 表示比较全部通过。两张地图初始均为 0,调用前将背景/窗口地图选为 0x9C00,因此能区分固定地图与所选地图的写入。示例在 LCD 关闭时写入,不测量 LCD 开启时的时序。

CGB 上的 A 为蓝色:属性值 2 选择示例设置的蓝色调色板。DMG 不应用 CGB 属性,因此同一字符为黑色。

预期图像:以下图块排列从图块坐标 (x=2, y=3)、像素坐标 (16,24) 开始显示。每个字符占一个 8×8 图块。按从上到下、从左到右阅读,周围图块保持空白。

text
A
__settilecgb_unsafe: DMG
DMG
__settilecgb_unsafe: CGB
CGB

完整示例程序

构建命令

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqgb\kitaqgb.exe .\kitaq-docs\samples\api-examples\gb\__settilecgb_unsafe.c -I .\kitaqgb\lib -I .\kitaq-docs\samples -o .\out\__settilecgb_unsafe.gb --profile=dev --rst-disable --stack-bank=fixed --cgb=cgb --no-disasm
实现与源码

kitaqgb/lib/cgb_tile.h:59

声明注释(原文)
Write tile and CGB attributes at map 9800; DMG receives only the tile write. No VRAM access waits are inserted.
__settilewin

向图块地图的一个单元写入图块编号。这是在选择已有图块图像,不会修改像素数据。

写入 LCDC 位 6 选择的窗口地图:0 为 0x9800,1 为 0x9C00。x,y 是地图坐标,不是窗口在屏幕上的位置。

必要时等待 VRAM 可写。LCD 开启时的路径会屏蔽中断,并可能在结束后启用中断;不保证保留调用前的中断状态,也不保证在一次 VBlank 内完成。

语法

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

参数

x, y

以 8×8 图块为单位的坐标,各为 0~31。整个写入范围必须位于 32×32 地图内;不会自动裁剪坐标。

tile

0~255 的图块编号。LCDC 位 4 决定编号如何寻址图块图像数据;这不是 VRAM 字节地址。

返回值

无(void)。

使用条件与限制

图块编号写入当前选中的 VRAM 库。CGB 上应选择编号所在的库 0;此调用不会代为切换库。

示例使用 gb_common.h。m_init() 将附带 ASCII 字体加载到与字符编码相同的图块编号。m_wait(); M_LCDC=0; 在 VBlank 中关闭 LCD,以便进行无需等待的传输。最后的 LCDC 值选择显示地图。

使用示例

c
// __settilewin
// Write one tile-number byte to the tile map; this selects an existing tile image, not
// new pixel data. LCDC bit 6 selects the window map: zero uses `0x9800`, one uses
// `0x9C00`. These are map coordinates, not the window's screen position.
// Expected: FAILED CHECKS 000, with the written tile data in the selected map.
#include "gb_tile_color_example.h"

void main() {
    u8 failures;
    failures = 0;
    tile_color_example_begin();
    __settilewin(2, 3, 'A');
    if (tile_example_read(0x9C62, 0) != 'A') { failures++; }
    if (tile_example_read(0x9862, 0) != 0) { failures++; }
    tile_example_finish(failures, 1);
}

预期结果

所选地图显示写入的字符或填充区域。程序读回目标图块编号,并在 CGB 上读回属性进行比较;FAILED CHECKS 000 表示比较全部通过。两张地图初始均为 0,调用前将背景/窗口地图选为 0x9C00,因此能区分固定地图与所选地图的写入。示例在 LCD 关闭时写入,不测量 LCD 开启时的时序。

在 CGB 上,示例字符和结果文字以白底蓝字显示。初始化时将调色板 0 的绘制颜色设为蓝色,不修改图块映射的属性。在 DMG 上,相同字符显示为白底黑字。

预期图像:以下图块排列从图块坐标 (x=2, y=3)、像素坐标 (16,24) 开始显示。每个字符占一个 8×8 图块。按从上到下、从左到右阅读,周围图块保持空白。

text
A
__settilewin: DMG
DMG
__settilewin: CGB
CGB

完整示例程序

构建命令

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqgb\kitaqgb.exe .\kitaq-docs\samples\api-examples\gb\__settilewin.c -I .\kitaqgb\lib -I .\kitaq-docs\samples -o .\out\__settilewin.gb --profile=dev --rst-disable --stack-bank=fixed --cgb=cgb --no-disasm
实现与源码

kitaqgb/lib/cgb_tile.h:39

声明注释(原文)
Select the window map from LCDC bit 6, then write a tile with access checks.
__settilewin_unsafe

向图块地图的一个单元写入图块编号。这是在选择已有图块图像,不会修改像素数据。

写入 LCDC 位 6 选择的窗口地图:0 为 0x9800,1 为 0x9C00。x,y 是地图坐标,不是窗口在屏幕上的位置。

不等待 VRAM 可写。只能在 LCD 关闭时,或由调用方确保整个传输都处于可写时间段时使用。

语法

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

参数

x, y

以 8×8 图块为单位的坐标,各为 0~31。整个写入范围必须位于 32×32 地图内;不会自动裁剪坐标。

tile

0~255 的图块编号。LCDC 位 4 决定编号如何寻址图块图像数据;这不是 VRAM 字节地址。

返回值

无(void)。

使用条件与限制

图块编号写入当前选中的 VRAM 库。CGB 上应选择编号所在的库 0;此调用不会代为切换库。

示例使用 gb_common.h。m_init() 将附带 ASCII 字体加载到与字符编码相同的图块编号。m_wait(); M_LCDC=0; 在 VBlank 中关闭 LCD,以便进行无需等待的传输。最后的 LCDC 值选择显示地图。

使用示例

c
// __settilewin_unsafe
// Write one tile-number byte to the tile map; this selects an existing tile image, not
// new pixel data. LCDC bit 6 selects the window map: zero uses `0x9800`, one uses
// `0x9C00`. These are map coordinates, not the window's screen position.
// Expected: FAILED CHECKS 000, with the written tile data in the selected map.
#include "gb_tile_color_example.h"

void main() {
    u8 failures;
    failures = 0;
    tile_color_example_begin();
    __settilewin_unsafe(2, 3, 'A');
    if (tile_example_read(0x9C62, 0) != 'A') { failures++; }
    if (tile_example_read(0x9862, 0) != 0) { failures++; }
    tile_example_finish(failures, 1);
}

预期结果

所选地图显示写入的字符或填充区域。程序读回目标图块编号,并在 CGB 上读回属性进行比较;FAILED CHECKS 000 表示比较全部通过。两张地图初始均为 0,调用前将背景/窗口地图选为 0x9C00,因此能区分固定地图与所选地图的写入。示例在 LCD 关闭时写入,不测量 LCD 开启时的时序。

在 CGB 上,示例字符和结果文字以白底蓝字显示。初始化时将调色板 0 的绘制颜色设为蓝色,不修改图块映射的属性。在 DMG 上,相同字符显示为白底黑字。

预期图像:以下图块排列从图块坐标 (x=2, y=3)、像素坐标 (16,24) 开始显示。每个字符占一个 8×8 图块。按从上到下、从左到右阅读,周围图块保持空白。

text
A
__settilewin_unsafe: DMG
DMG
__settilewin_unsafe: CGB
CGB

完整示例程序

构建命令

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqgb\kitaqgb.exe .\kitaq-docs\samples\api-examples\gb\__settilewin_unsafe.c -I .\kitaqgb\lib -I .\kitaq-docs\samples -o .\out\__settilewin_unsafe.gb --profile=dev --rst-disable --stack-bank=fixed --cgb=cgb --no-disasm
实现与源码

kitaqgb/lib/cgb_tile.h:41

声明注释(原文)
Select the LCDC window map and write without access waits.
__settilewinattr

只修改 CGB 属性字节,保留图块编号。属性指定调色板、图块图像库、水平或垂直翻转和优先级。

写入 LCDC 位 6 选择的窗口地图:0 为 0x9800,1 为 0x9C00。x,y 是地图坐标,不是窗口在屏幕上的位置。

必要时等待 VRAM 可写。LCD 开启时的路径会屏蔽中断,并可能在结束后启用中断;不保证保留调用前的中断状态,也不保证在一次 VBlank 内完成。

语法

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

参数

x, y

以 8×8 图块为单位的坐标,各为 0~31。整个写入范围必须位于 32×32 地图内;不会自动裁剪坐标。

attr

CGB 属性字节:位 0~2 选择调色板 0~7,位 3 选择图像数据 bank,位 5、6 分别控制水平和垂直翻转,位 7 设置背景/窗口优先级。未使用的位 4 应为 0。

返回值

无(void)。

使用条件与限制

CGB 上切到 VRAM 库 1 写入属性,结束时置 VBK=0。DMG 上不执行属性写入。

示例使用 gb_common.h。m_init() 将附带 ASCII 字体加载到与字符编码相同的图块编号。m_wait(); M_LCDC=0; 在 VBlank 中关闭 LCD,以便进行无需等待的传输。最后的 LCDC 值选择显示地图。

使用示例

c
// __settilewinattr
// Write only the CGB attribute byte; leave the tile number unchanged. Attribute bits
// select palette, tile-data bank, flips and priority. LCDC bit 6 selects the window map:
// zero uses `0x9800`, one uses `0x9C00`. These are map coordinates, not the window's
// screen position.
// Expected: FAILED CHECKS 000, with the written tile data in the selected map.
#include "gb_tile_example.h"

void main() {
    u8 failures;
    failures = 0;
    tile_example_begin();
    __settileat(0x9C00, 2, 3, 'A');
    __settilewinattr(2, 3, KQ_CGB_ATTR(2, 0, 0, 0, 0));
    if (tile_example_read(0x9C62, 0) != 'A') { failures++; }
    if (tile_example_read(0x9862, 0) != 0) { failures++; }
    if (__cgb_is_cgb() && tile_example_read(0x9C62, 1) != 2) { failures++; }
    if (__cgb_is_cgb() && tile_example_read(0x9862, 1) != 0) { failures++; }
    tile_example_finish(failures, 1);
}

预期结果

所选地图显示写入的字符或填充区域。程序读回目标图块编号,并在 CGB 上读回属性进行比较;FAILED CHECKS 000 表示比较全部通过。两张地图初始均为 0,调用前将背景/窗口地图选为 0x9C00,因此能区分固定地图与所选地图的写入。示例在 LCD 关闭时写入,不测量 LCD 开启时的时序。

CGB 上的 A 为蓝色:属性值 2 选择示例设置的蓝色调色板。DMG 不应用 CGB 属性,因此同一字符为黑色。

预期图像:以下图块排列从图块坐标 (x=2, y=3)、像素坐标 (16,24) 开始显示。每个字符占一个 8×8 图块。按从上到下、从左到右阅读,周围图块保持空白。

text
A
__settilewinattr: DMG
DMG
__settilewinattr: CGB
CGB

完整示例程序

构建命令

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqgb\kitaqgb.exe .\kitaq-docs\samples\api-examples\gb\__settilewinattr.c -I .\kitaqgb\lib -I .\kitaq-docs\samples -o .\out\__settilewinattr.gb --profile=dev --rst-disable --stack-bank=fixed --cgb=cgb --no-disasm
实现与源码

kitaqgb/lib/cgb_tile.h:71

声明注释(原文)
Write attributes in bank one at the LCDC window map on CGB, then leave VBK zero; skip on DMG. Use the access-checked write path.
__settilewinattr_unsafe

只修改 CGB 属性字节,保留图块编号。属性指定调色板、图块图像库、水平或垂直翻转和优先级。

写入 LCDC 位 6 选择的窗口地图:0 为 0x9800,1 为 0x9C00。x,y 是地图坐标,不是窗口在屏幕上的位置。

不等待 VRAM 可写。只能在 LCD 关闭时,或由调用方确保整个传输都处于可写时间段时使用。

语法

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

参数

x, y

以 8×8 图块为单位的坐标,各为 0~31。整个写入范围必须位于 32×32 地图内;不会自动裁剪坐标。

attr

CGB 属性字节:位 0~2 选择调色板 0~7,位 3 选择图像数据 bank,位 5、6 分别控制水平和垂直翻转,位 7 设置背景/窗口优先级。未使用的位 4 应为 0。

返回值

无(void)。

使用条件与限制

CGB 上切到 VRAM 库 1 写入属性,结束时置 VBK=0。DMG 上不执行属性写入。

示例使用 gb_common.h。m_init() 将附带 ASCII 字体加载到与字符编码相同的图块编号。m_wait(); M_LCDC=0; 在 VBlank 中关闭 LCD,以便进行无需等待的传输。最后的 LCDC 值选择显示地图。

使用示例

c
// __settilewinattr_unsafe
// Write only the CGB attribute byte; leave the tile number unchanged. Attribute bits
// select palette, tile-data bank, flips and priority. LCDC bit 6 selects the window map:
// zero uses `0x9800`, one uses `0x9C00`. These are map coordinates, not the window's
// screen position.
// Expected: FAILED CHECKS 000, with the written tile data in the selected map.
#include "gb_tile_example.h"

void main() {
    u8 failures;
    failures = 0;
    tile_example_begin();
    __settileat(0x9C00, 2, 3, 'A');
    __settilewinattr_unsafe(2, 3, KQ_CGB_ATTR(2, 0, 0, 0, 0));
    if (tile_example_read(0x9C62, 0) != 'A') { failures++; }
    if (tile_example_read(0x9862, 0) != 0) { failures++; }
    if (__cgb_is_cgb() && tile_example_read(0x9C62, 1) != 2) { failures++; }
    if (__cgb_is_cgb() && tile_example_read(0x9862, 1) != 0) { failures++; }
    tile_example_finish(failures, 1);
}

预期结果

所选地图显示写入的字符或填充区域。程序读回目标图块编号,并在 CGB 上读回属性进行比较;FAILED CHECKS 000 表示比较全部通过。两张地图初始均为 0,调用前将背景/窗口地图选为 0x9C00,因此能区分固定地图与所选地图的写入。示例在 LCD 关闭时写入,不测量 LCD 开启时的时序。

CGB 上的 A 为蓝色:属性值 2 选择示例设置的蓝色调色板。DMG 不应用 CGB 属性,因此同一字符为黑色。

预期图像:以下图块排列从图块坐标 (x=2, y=3)、像素坐标 (16,24) 开始显示。每个字符占一个 8×8 图块。按从上到下、从左到右阅读,周围图块保持空白。

text
A
__settilewinattr_unsafe: DMG
DMG
__settilewinattr_unsafe: CGB
CGB

完整示例程序

构建命令

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqgb\kitaqgb.exe .\kitaq-docs\samples\api-examples\gb\__settilewinattr_unsafe.c -I .\kitaqgb\lib -I .\kitaq-docs\samples -o .\out\__settilewinattr_unsafe.gb --profile=dev --rst-disable --stack-bank=fixed --cgb=cgb --no-disasm
实现与源码

kitaqgb/lib/cgb_tile.h:73

声明注释(原文)
Write attributes in bank one at the LCDC window map on CGB, then leave VBK zero; skip on DMG. No VRAM access waits are inserted.
__settilewincgb

依次写入图块编号和 CGB 属性。DMG 只写编号。CGB 调用前应选择 VRAM 库 0;属性写入库 1,结束时为 VBK=0。

写入 LCDC 位 6 选择的窗口地图:0 为 0x9800,1 为 0x9C00。x,y 是地图坐标,不是窗口在屏幕上的位置。

必要时等待 VRAM 可写。LCD 开启时的路径会屏蔽中断,并可能在结束后启用中断;不保证保留调用前的中断状态,也不保证在一次 VBlank 内完成。

语法

c
void __settilewincgb(u8 x, u8 y, u8 tile, u8 attr);

参数

x, y

以 8×8 图块为单位的坐标,各为 0~31。整个写入范围必须位于 32×32 地图内;不会自动裁剪坐标。

tile

0~255 的图块编号。LCDC 位 4 决定编号如何寻址图块图像数据;这不是 VRAM 字节地址。

attr

CGB 属性字节:位 0~2 选择调色板 0~7,位 3 选择图像数据 bank,位 5、6 分别控制水平和垂直翻转,位 7 设置背景/窗口优先级。未使用的位 4 应为 0。

返回值

无(void)。

使用条件与限制

示例使用 gb_common.h。m_init() 将附带 ASCII 字体加载到与字符编码相同的图块编号。m_wait(); M_LCDC=0; 在 VBlank 中关闭 LCD,以便进行无需等待的传输。最后的 LCDC 值选择显示地图。

使用示例

c
// __settilewincgb
// Write a tile number and its CGB attribute. On DMG, only the tile-number write is
// performed. On CGB, enter with VRAM bank 0 selected; attributes use bank 1 and the call
// finishes with `VBK=0`. LCDC bit 6 selects the window map: zero uses `0x9800`, one uses
// `0x9C00`. These are map coordinates, not the window's screen position.
// Expected: FAILED CHECKS 000, with the written tile data in the selected map.
#include "gb_tile_example.h"

void main() {
    u8 failures;
    failures = 0;
    tile_example_begin();
    __settilewincgb(2, 3, 'A', KQ_CGB_ATTR(2, 0, 0, 0, 0));
    if (tile_example_read(0x9C62, 0) != 'A') { failures++; }
    if (tile_example_read(0x9862, 0) != 0) { failures++; }
    if (__cgb_is_cgb() && tile_example_read(0x9C62, 1) != 2) { failures++; }
    if (__cgb_is_cgb() && tile_example_read(0x9862, 1) != 0) { failures++; }
    tile_example_finish(failures, 1);
}

预期结果

所选地图显示写入的字符或填充区域。程序读回目标图块编号,并在 CGB 上读回属性进行比较;FAILED CHECKS 000 表示比较全部通过。两张地图初始均为 0,调用前将背景/窗口地图选为 0x9C00,因此能区分固定地图与所选地图的写入。示例在 LCD 关闭时写入,不测量 LCD 开启时的时序。

CGB 上的 A 为蓝色:属性值 2 选择示例设置的蓝色调色板。DMG 不应用 CGB 属性,因此同一字符为黑色。

预期图像:以下图块排列从图块坐标 (x=2, y=3)、像素坐标 (16,24) 开始显示。每个字符占一个 8×8 图块。按从上到下、从左到右阅读,周围图块保持空白。

text
A
__settilewincgb: DMG
DMG
__settilewincgb: CGB
CGB

完整示例程序

构建命令

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqgb\kitaqgb.exe .\kitaq-docs\samples\api-examples\gb\__settilewincgb.c -I .\kitaqgb\lib -I .\kitaq-docs\samples -o .\out\__settilewincgb.gb --profile=dev --rst-disable --stack-bank=fixed --cgb=cgb --no-disasm
实现与源码

kitaqgb/lib/cgb_tile.h:75

声明注释(原文)
Write tile and CGB attributes at the LCDC window map; DMG receives only the tile write. Use the access-checked write path.
__settilewincgb_unsafe

依次写入图块编号和 CGB 属性。DMG 只写编号。CGB 调用前应选择 VRAM 库 0;属性写入库 1,结束时为 VBK=0。

写入 LCDC 位 6 选择的窗口地图:0 为 0x9800,1 为 0x9C00。x,y 是地图坐标,不是窗口在屏幕上的位置。

不等待 VRAM 可写。只能在 LCD 关闭时,或由调用方确保整个传输都处于可写时间段时使用。

语法

c
void __settilewincgb_unsafe(u8 x, u8 y, u8 tile, u8 attr);

参数

x, y

以 8×8 图块为单位的坐标,各为 0~31。整个写入范围必须位于 32×32 地图内;不会自动裁剪坐标。

tile

0~255 的图块编号。LCDC 位 4 决定编号如何寻址图块图像数据;这不是 VRAM 字节地址。

attr

CGB 属性字节:位 0~2 选择调色板 0~7,位 3 选择图像数据 bank,位 5、6 分别控制水平和垂直翻转,位 7 设置背景/窗口优先级。未使用的位 4 应为 0。

返回值

无(void)。

使用条件与限制

示例使用 gb_common.h。m_init() 将附带 ASCII 字体加载到与字符编码相同的图块编号。m_wait(); M_LCDC=0; 在 VBlank 中关闭 LCD,以便进行无需等待的传输。最后的 LCDC 值选择显示地图。

使用示例

c
// __settilewincgb_unsafe
// Write a tile number and its CGB attribute. On DMG, only the tile-number write is
// performed. On CGB, enter with VRAM bank 0 selected; attributes use bank 1 and the call
// finishes with `VBK=0`. LCDC bit 6 selects the window map: zero uses `0x9800`, one uses
// `0x9C00`. These are map coordinates, not the window's screen position.
// Expected: FAILED CHECKS 000, with the written tile data in the selected map.
#include "gb_tile_example.h"

void main() {
    u8 failures;
    failures = 0;
    tile_example_begin();
    __settilewincgb_unsafe(2, 3, 'A', KQ_CGB_ATTR(2, 0, 0, 0, 0));
    if (tile_example_read(0x9C62, 0) != 'A') { failures++; }
    if (tile_example_read(0x9862, 0) != 0) { failures++; }
    if (__cgb_is_cgb() && tile_example_read(0x9C62, 1) != 2) { failures++; }
    if (__cgb_is_cgb() && tile_example_read(0x9862, 1) != 0) { failures++; }
    tile_example_finish(failures, 1);
}

预期结果

所选地图显示写入的字符或填充区域。程序读回目标图块编号,并在 CGB 上读回属性进行比较;FAILED CHECKS 000 表示比较全部通过。两张地图初始均为 0,调用前将背景/窗口地图选为 0x9C00,因此能区分固定地图与所选地图的写入。示例在 LCD 关闭时写入,不测量 LCD 开启时的时序。

CGB 上的 A 为蓝色:属性值 2 选择示例设置的蓝色调色板。DMG 不应用 CGB 属性,因此同一字符为黑色。

预期图像:以下图块排列从图块坐标 (x=2, y=3)、像素坐标 (16,24) 开始显示。每个字符占一个 8×8 图块。按从上到下、从左到右阅读,周围图块保持空白。

text
A
__settilewincgb_unsafe: DMG
DMG
__settilewincgb_unsafe: CGB
CGB

完整示例程序

构建命令

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqgb\kitaqgb.exe .\kitaq-docs\samples\api-examples\gb\__settilewincgb_unsafe.c -I .\kitaqgb\lib -I .\kitaq-docs\samples -o .\out\__settilewincgb_unsafe.gb --profile=dev --rst-disable --stack-bank=fixed --cgb=cgb --no-disasm
实现与源码

kitaqgb/lib/cgb_tile.h:77

声明注释(原文)
Write tile and CGB attributes at the LCDC window map; DMG receives only the tile write. No VRAM access waits are inserted.

input.h — 按钮状态、按下、松开与重复

连续移动用 input_down,单次动作用 input_pressed,松开时停止用 input_released,菜单重复操作用 input_repeat。初始化一次,每个输入周期更新一次,然后按需查询。btn_down、btn_pressed、btn_released、btn_repeat 分别是这四个查询函数的别名。

通用掩码为 BTN_RIGHT=0x01、BTN_LEFT=0x02、BTN_UP=0x04、BTN_DOWN=0x08、BTN_A=0x10、BTN_B=0x20、BTN_SELECT=0x40、BTN_START=0x80。位为 1 表示按住。GB 和 FC 的 input_* 均使用此格式;FC 原始手柄函数的位序不同。

重复触发按更新次数计时,不是毫秒。新按下时立即触发。默认 INPUT_REPEAT_DELAY=18、INPUT_REPEAT_RATE=5 时,19 次更新后再次触发,此后每 6 次更新触发一次。各按钮独立计数。修改设置时,编译 input.c 也必须使用相同宏值;只修改调用方的头文件不会改变库的设置。

__readpadex

读取全部按钮并与传入的上次按住掩码比较。低字节存当前掩码,高字节存新按下的位 current & ~previous。不在内部保存上次状态,也不返回松开或重复触发掩码。

语法

c
u16 __readpadex(u8 prev_keys);

参数

prev_keys

上次 __readpad() 的返回值,或上次 __readpadex() 结果的低字节。首次传 0,然后保存新的低字节供下次调用使用。

返回值

返回 u16:(u8)result 为当前掩码,(u8)(result >> 8) 为新按下掩码。必须存入 16 位变量,否则会丢失新按下的位。

使用条件与限制

通用掩码为 BTN_RIGHT=0x01、BTN_LEFT=0x02、BTN_UP=0x04、BTN_DOWN=0x08、BTN_A=0x10、BTN_B=0x20、BTN_SELECT=0x40、BTN_START=0x80。位为 1 表示按住。GB 和 FC 的 input_* 均使用此格式;FC 原始手柄函数的位序不同。

使用示例

c
packed_first=__readpadex(0); // First sample: current 0x11, new presses 0x11.
current_keys=(u8)packed_first;
press_keys=(u8)(packed_first>>8);
packed_held=__readpadex(current_keys); // Held without a change: new presses are 0.

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

预期结果

此程序用于比较三种按钮掩码格式,以及将当前状态和新按下状态打包的返回值。同时按住A和右。ALL KEYS为17,DIRECTIONS和BUTTONS LOW各为1。前次掩码为0时,EX CURRENT和EX NEW PRESS均为17。将当前掩码传给下一次读取后,HELD PRESS为0。FAILED CHECKS应为0。采集到的数值会保留在屏幕上,松开按钮也不会清除。

__readpadex: DMG
DMG
__readpadex: CGB
CGB

完整示例程序

构建命令

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqgb\kitaqgb.exe .\kitaq-docs\samples\api-examples\gb\input_raw.c -I .\kitaqgb\lib -I .\kitaq-docs\samples -o .\out\input_raw.gb --profile=dev --rst-disable --stack-bank=fixed --cgb=cgb --no-disasm --no-cache
实现与源码

kitaqgb/lib/input.h:28

intrinsics — 编译器内建函数

本节汇集由 KITAQGB 代码生成器直接处理的内建函数。intrinsics 是文档分类,不表示要包含名为 intrinsics.h 的共用 GB 头文件。请使用各完整示例展示的头文件和初始化方式。游戏无需自行定义内建函数实现。

有些内建函数使用构建时信息,另一些则在游戏运行时访问硬件或内存。例如,__bankof(symbol) 解析指定对象或函数被分配的存储体,__bankswitch(bank) 则在运行时改变可见 ROM 映射。传参时请区分存储体编号、CPU 地址和指针。

名称以 __ 开头,不保证进行边界检查、等待 VRAM 访问或可安全用于中断。例如,__vram_memcpy_unsafe 的调用方必须保证当前处于 VRAM 可写时段。选择操作前,请核对各条目的单位、有效范围、存储体映射和中断要求。

__bankof

获取具名数据对象或函数被分配的逻辑 ROM 存储体编号。它解析的是布局信息,不读取当前映射的存储体,也不解引用指针。

语法

c
u8 bank = __bankof(symbol);

此调用格式中的参数名用于解释编译器内置功能,不是需要另行添加的 C 声明。

参数

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 为 03:far_call、__farcall 和 __farcall_ptr 各执行一次回调。

__bankof: DMG
DMG
__bankof: CGB
CGB

完整示例程序

构建命令

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqgb\kitaqgb.exe .\kitaq-docs\samples\api-examples\gb\banked_data_calls.c -I .\kitaqgb\lib -I .\kitaq-docs\samples -o .\out\banked_data_calls.gb --profile=dev --rst-disable --stack-bank=fixed --cgb=cgb --cart=mbc5 --romsize=64k --no-cache --no-disasm
实现与源码

kitaqgb/kitaqgb/CodeGenerator.cs:432

kitaqgb/kitaqgb/CodeGenerator.cs:7360

csharp
if (funcName == "__bankof")
            {
                if (args.Length != 1) Program.Error("__bankof(symbol) expects 1 argument");
                if (!args[0].Match(Tag.Name, out string symName)) Program.Error("__bankof(symbol) requires a symbol name argument");
                if (TryGetKnownRomBankForSymbol(symName, out int knownBank))
                {
                    EmitAsm("LD_A_IMM", new AsmOperand(knownBank & 0xFF, AddressMode.Immediate));
                    return;
                }
                EmitAsm("LD_A_IMM", new AsmOperand(symName, ImmediateModifier.Bank));
                return;
            }
__bankswitch

向卡带地址 $2000 写入存储体编号的低字节,并更新编译器的存储体状态。调用后保持该选择;不会更新 bank_get_current 使用的记录。

语法

c
void __bankswitch(u8 bank);

参数

bank

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

返回值

无(void)。

使用条件与限制

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

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

这些 GB 存储体接口只携带 8 位编号,不设置 MBC5 的第 9 位。仍须遵守卡带规则,包括 mapper 对编号 0 的处理。示例明确使用 MBC5 存储体 1–3;不要混用未记录的 mapper 直接写入。

使用示例

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 为 03:far_call、__farcall 和 __farcall_ptr 各执行一次回调。

__bankswitch: DMG
DMG
__bankswitch: CGB
CGB

完整示例程序

构建命令

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqgb\kitaqgb.exe .\kitaq-docs\samples\api-examples\gb\banked_data_calls.c -I .\kitaqgb\lib -I .\kitaq-docs\samples -o .\out\banked_data_calls.gb --profile=dev --rst-disable --stack-bank=fixed --cgb=cgb --cart=mbc5 --romsize=64k --no-cache --no-disasm
实现与源码

kitaqgb/lib/bank.h:11

kitaqgb/kitaqgb/CodeGenerator.cs:7268

csharp
if (funcName == "__bankswitch")
            {
                if (args.Length != 1) Program.Error("__bankswitch(bank) expects 1 argument");
                CompileIntoA(args[0]);
                if (CurrentFunctionBank == 0)
                {
                    EmitAsm("LD_MEM_A", new AsmOperand(0x2000, AddressMode.Absolute));
                    EmitStoreA(RomBankVar);
                }
                else
                {
                    UseBankSwitchBank0Helper = true;
                    EmitAsm("CALL", new AsmOperand("__kq_bankswitch_bank0", AddressMode.Absolute));
                }
                return;
            }
__cgb_safe_set_bgpd

向CGB的背景/窗口调色板数据寄存器BCPD/BGPD($FF69)写入一个字节。__cgb_safe_set_bcpd是同一寄存器的别名。对应的索引寄存器选择字节位置;完整RGB15颜色需要低、高两个字节。

语法

c
__cgb_safe_set_bgpd(/* 書式は実装と呼び出し例を参照 */);

此调用格式中的参数名用于解释编译器内置功能,不是需要另行添加的 C 声明。

参数

value

一个字节,0~255。依次写RGB15的低字节和高字节;先启用索引自动递增,或逐次明确选择位置。数据写入本身不会启用自动递增。

返回值

无(void)。

使用条件与限制

调色板操作和带模式保护的寄存器写入在DMG模式下不写入任何内容。这里的safe指CGB模式检查,不代表等待PPU可访问或禁止中断。请在LCD关闭时设置,或在可访问的PPU阶段内完成足够短的更新。函数不等待VBlank,也不保护多字节更新免受中断影响。

使用示例

c
__cgb_safe_set_bgpd(31); __cgb_safe_set_bgpd(0); // Alias of BCPD: red at slot 31.
expect_color(0,31,31);

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

预期结果

本程序演示同一图块图案如何分别使用BG和OBJ的调色板。每个三角形为8×8像素,从顶行左侧1个像素逐行扩展到底行8个像素。在x=16、32、48、64、80、96、112处,y=24的BG行依次为红、蓝、绿、红、蓝、绿、红;y=64的OBJ行依次为蓝、绿、红、蓝、绿、红、蓝。各列依次使用单色、RGB分量、索引范围、整组调色板、绝对范围、标准寄存器名及寄存器别名写入。

y=88处的7个8×8格依次显示:红色三角形(x=16)、存储体1中线宽1像素的蓝色方框(32)、水平翻转的绿色三角形(48)、垂直翻转的红色三角形(64)、双向翻转的蓝色三角形(80)、位于红色精灵前方且颜色0区域透出红色的绿色三角形(96),以及关闭BG优先级后覆盖三角形的实心红色精灵(112)。坐标以屏幕左上角为原点,单位为像素。空白处为白色,文字为黑色。

FAILED CHECKS 000表示RGB掩码、含输入2的布尔标志、调色板索引回绕、跨调色板写入、在槽位31截断、null/零长度不写入,以及LCD关闭时的颜色读回检查均通过。测试4种组合:双模式ROM分别在DMG和CGB运行、DMG专用ROM在DMG运行、CGB专用ROM在CGB运行。DMG下CGB MODE为0,三角形均为黑色,存储体及翻转属性不影响显示,最下行最后两格为黑色方块。CGB测试中CGB MODE为1。未测试实机及LCD显示期间的调色板访问冲突。

__cgb_safe_set_bgpd: --cgb=cgb / DMG
--cgb=cgb / DMG
__cgb_safe_set_bgpd: --cgb=cgb / CGB
--cgb=cgb / CGB
__cgb_safe_set_bgpd: --cgb=dmg / DMG
--cgb=dmg / DMG
__cgb_safe_set_bgpd: --cgb=cgb_only / CGB
--cgb=cgb_only / CGB

完整示例程序

构建命令

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqgb\kitaqgb.exe .\kitaq-docs\samples\api-examples\gb\cgb_palette_attributes.c -I .\kitaqgb\lib -I .\kitaq-docs\samples -o .\out\cgb_palette_attributes.gb --profile=dev --rst-disable --stack-bank=fixed --cgb=cgb --no-cache --no-disasm
实现与源码

kitaqgb/kitaqgb/CodeGenerator.cs:1865

__cgb_safe_set_bgpi

写入CGB的背景/窗口调色板字节索引寄存器BCPS/BGPI($FF68)。__cgb_safe_set_bcps操作同一寄存器。它选择下一次数据寄存器访问的位置,本身不写入颜色。

语法

c
__cgb_safe_set_bgpi(/* 書式は実装と呼び出し例を参照 */);

此调用格式中的参数名用于解释编译器内置功能,不是需要另行添加的 C 声明。

参数

value

位0~5指定字节索引0~63;位7为1时,数据写入后自动递增。位6未使用。颜色槽位n从字节2n开始。索引在63之后回到0,直接操作寄存器时须自行控制范围。

返回值

无(void)。

使用条件与限制

调色板操作和带模式保护的寄存器写入在DMG模式下不写入任何内容。这里的safe指CGB模式检查,不代表等待PPU可访问或禁止中断。请在LCD关闭时设置,或在可访问的PPU阶段内完成足够短的更新。函数不等待VBlank,也不保护多字节更新免受中断影响。

使用示例

c
__cgb_safe_set_bgpi(0xBE); // Alias of BCPS: BG byte 62, auto-increment enabled.

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

预期结果

本程序演示同一图块图案如何分别使用BG和OBJ的调色板。每个三角形为8×8像素,从顶行左侧1个像素逐行扩展到底行8个像素。在x=16、32、48、64、80、96、112处,y=24的BG行依次为红、蓝、绿、红、蓝、绿、红;y=64的OBJ行依次为蓝、绿、红、蓝、绿、红、蓝。各列依次使用单色、RGB分量、索引范围、整组调色板、绝对范围、标准寄存器名及寄存器别名写入。

y=88处的7个8×8格依次显示:红色三角形(x=16)、存储体1中线宽1像素的蓝色方框(32)、水平翻转的绿色三角形(48)、垂直翻转的红色三角形(64)、双向翻转的蓝色三角形(80)、位于红色精灵前方且颜色0区域透出红色的绿色三角形(96),以及关闭BG优先级后覆盖三角形的实心红色精灵(112)。坐标以屏幕左上角为原点,单位为像素。空白处为白色,文字为黑色。

FAILED CHECKS 000表示RGB掩码、含输入2的布尔标志、调色板索引回绕、跨调色板写入、在槽位31截断、null/零长度不写入,以及LCD关闭时的颜色读回检查均通过。测试4种组合:双模式ROM分别在DMG和CGB运行、DMG专用ROM在DMG运行、CGB专用ROM在CGB运行。DMG下CGB MODE为0,三角形均为黑色,存储体及翻转属性不影响显示,最下行最后两格为黑色方块。CGB测试中CGB MODE为1。未测试实机及LCD显示期间的调色板访问冲突。

__cgb_safe_set_bgpi: --cgb=cgb / DMG
--cgb=cgb / DMG
__cgb_safe_set_bgpi: --cgb=cgb / CGB
--cgb=cgb / CGB
__cgb_safe_set_bgpi: --cgb=dmg / DMG
--cgb=dmg / DMG
__cgb_safe_set_bgpi: --cgb=cgb_only / CGB
--cgb=cgb_only / CGB

完整示例程序

构建命令

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqgb\kitaqgb.exe .\kitaq-docs\samples\api-examples\gb\cgb_palette_attributes.c -I .\kitaqgb\lib -I .\kitaq-docs\samples -o .\out\cgb_palette_attributes.gb --profile=dev --rst-disable --stack-bank=fixed --cgb=cgb --no-cache --no-disasm
实现与源码

kitaqgb/kitaqgb/CodeGenerator.cs:1863

__cgb_safe_set_hdma1

写入HDMA1($FF51),设置CGB DMA源地址的高字节。只配置源地址,不复制数据或启动传输;低字节由HDMA2设置。

语法

c
void __cgb_safe_set_hdma1(u8 value);

此调用格式中的参数名用于解释编译器内置功能,不是需要另行添加的 C 声明。

参数

value

寄存器值,类型u8,范围0~255。位含义见该寄存器说明;传入的是一个字节,不是指针或字节数量。

返回值

无(void)。

使用条件与限制

编译器以CGB模式检查保护写入,DMG模式下不写入。safe仅指此模式保护,不保存或恢复原寄存器值。

每块16字节。新传输可指定1~128块(16~2048字节),因此控制值0表示16字节,不是空传输。GDMA在完成前暂停CPU。HBlank DMA在每个可见行的HBlank传输一块,可跨帧继续;调用方须在复用数据前确认完成。

GDMA应在LCD关闭或足够长的可写阶段使用。HBlank DMA须在LCD运行且非STAT模式0时启动;示例从VBlank启动。活动期间不要改变源存储体、目标VBK或源数据。HALT也会暂停HBlank DMA。这些设置函数不额外等待、不保护中断,也不检查源/目标范围。

须另读$FF55获取状态:位7为0表示活动,$FF表示完成。取消后位7为1,位0~6保留剩余块数减1;已复制的字节不会恢复。仅在传输活动时发出取消命令,否则将位7写为0会启动GDMA。整个传输须位于有效源内存及目标VRAM存储体内,不要依赖地址溢出。

这些HDMA寄存器设置函数控制CGB硬件DMA。__vram_copy_dma是CPU复制循环的另一编译器别名,不会因为名称中有dma就使用此DMA控制器。

使用示例

c
__cgb_safe_set_hdma1(0xC6); // Source high byte: fixed-WRAM buffer at $C600.

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

预期结果

完整程序使用位于$C600、按16字节对齐的缓冲区比较三条传输路径。GDMA将32字节复制到$8010:在屏幕(16,24)显示从左上1像素扩展到底行8像素的红色8×8三角形,(24,24)显示红色1像素宽方框。HBlank DMA将32字节复制到$8030:在(16,48)显示蓝色4×8左半块,(24,48)显示蓝色8×8正方形。这些是实际传输的图块图案,而非数字占位。

CANCELED行是CPU预先写入的绿色4×8半块(16,72)和绿色8×8正方形(24,72)。针对这些图块的两块HBlank传输在同一个VBlank内启动并取消,不传输第一块。$FF55应为$81,一帧后图案仍不变。DMG下前两行留白,仅这两个保持检查图形显示为黑色。文字为白底黑字。FAILED CHECKS 000还检查复制字节及完成状态。已在KOKURA测试双模式DMG/CGB及CGB专用执行;未测试部分传输后取消、双速或实机。

__cgb_safe_set_hdma1: --cgb=cgb / DMG
--cgb=cgb / DMG
__cgb_safe_set_hdma1: --cgb=cgb / CGB
--cgb=cgb / CGB
__cgb_safe_set_hdma1: --cgb=cgb_only / CGB
--cgb=cgb_only / CGB

完整示例程序

构建命令

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqgb\kitaqgb.exe .\kitaq-docs\samples\api-examples\gb\cgb_dma_shapes.c -I .\kitaqgb\lib -I .\kitaq-docs\samples -o .\out\cgb_dma_shapes.gb --profile=dev --rst-disable --stack-bank=fixed --cgb=cgb --no-cache --no-disasm
实现与源码

kitaqgb/kitaqgb/CodeGenerator.cs:1871

__cgb_safe_set_hdma2

写入HDMA2($FF52),设置DMA源地址的低字节。硬件忽略位0~3,向下对齐到16字节边界。例如HDMA1=$C6、HDMA2=$0F时源地址为$C600。不启动DMA。

语法

c
void __cgb_safe_set_hdma2(u8 value);

此调用格式中的参数名用于解释编译器内置功能,不是需要另行添加的 C 声明。

参数

value

寄存器值,类型u8,范围0~255。位含义见该寄存器说明;传入的是一个字节,不是指针或字节数量。

返回值

无(void)。

使用条件与限制

编译器以CGB模式检查保护写入,DMG模式下不写入。safe仅指此模式保护,不保存或恢复原寄存器值。

每块16字节。新传输可指定1~128块(16~2048字节),因此控制值0表示16字节,不是空传输。GDMA在完成前暂停CPU。HBlank DMA在每个可见行的HBlank传输一块,可跨帧继续;调用方须在复用数据前确认完成。

GDMA应在LCD关闭或足够长的可写阶段使用。HBlank DMA须在LCD运行且非STAT模式0时启动;示例从VBlank启动。活动期间不要改变源存储体、目标VBK或源数据。HALT也会暂停HBlank DMA。这些设置函数不额外等待、不保护中断,也不检查源/目标范围。

须另读$FF55获取状态:位7为0表示活动,$FF表示完成。取消后位7为1,位0~6保留剩余块数减1;已复制的字节不会恢复。仅在传输活动时发出取消命令,否则将位7写为0会启动GDMA。整个传输须位于有效源内存及目标VRAM存储体内,不要依赖地址溢出。

这些HDMA寄存器设置函数控制CGB硬件DMA。__vram_copy_dma是CPU复制循环的另一编译器别名,不会因为名称中有dma就使用此DMA控制器。

使用示例

c
__cgb_safe_set_hdma2(0x0F); // Low four bits are ignored by hardware: source remains $C600.

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

预期结果

完整程序使用位于$C600、按16字节对齐的缓冲区比较三条传输路径。GDMA将32字节复制到$8010:在屏幕(16,24)显示从左上1像素扩展到底行8像素的红色8×8三角形,(24,24)显示红色1像素宽方框。HBlank DMA将32字节复制到$8030:在(16,48)显示蓝色4×8左半块,(24,48)显示蓝色8×8正方形。这些是实际传输的图块图案,而非数字占位。

CANCELED行是CPU预先写入的绿色4×8半块(16,72)和绿色8×8正方形(24,72)。针对这些图块的两块HBlank传输在同一个VBlank内启动并取消,不传输第一块。$FF55应为$81,一帧后图案仍不变。DMG下前两行留白,仅这两个保持检查图形显示为黑色。文字为白底黑字。FAILED CHECKS 000还检查复制字节及完成状态。已在KOKURA测试双模式DMG/CGB及CGB专用执行;未测试部分传输后取消、双速或实机。

__cgb_safe_set_hdma2: --cgb=cgb / DMG
--cgb=cgb / DMG
__cgb_safe_set_hdma2: --cgb=cgb / CGB
--cgb=cgb / CGB
__cgb_safe_set_hdma2: --cgb=cgb_only / CGB
--cgb=cgb_only / CGB

完整示例程序

构建命令

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqgb\kitaqgb.exe .\kitaq-docs\samples\api-examples\gb\cgb_dma_shapes.c -I .\kitaqgb\lib -I .\kitaq-docs\samples -o .\out\cgb_dma_shapes.gb --profile=dev --rst-disable --stack-bank=fixed --cgb=cgb --no-cache --no-disasm
实现与源码

kitaqgb/kitaqgb/CodeGenerator.cs:1872

__cgb_safe_set_hdma3

写入HDMA3($FF53),设置VRAM中DMA目标地址的高位部分。仅位0~4有效,目标始终位于$8000–$9FFF。与HDMA4配合使用;不选择VRAM存储体,也不启动DMA。

语法

c
void __cgb_safe_set_hdma3(u8 value);

此调用格式中的参数名用于解释编译器内置功能,不是需要另行添加的 C 声明。

参数

value

寄存器值,类型u8,范围0~255。位含义见该寄存器说明;传入的是一个字节,不是指针或字节数量。

返回值

无(void)。

使用条件与限制

编译器以CGB模式检查保护写入,DMG模式下不写入。safe仅指此模式保护,不保存或恢复原寄存器值。

每块16字节。新传输可指定1~128块(16~2048字节),因此控制值0表示16字节,不是空传输。GDMA在完成前暂停CPU。HBlank DMA在每个可见行的HBlank传输一块,可跨帧继续;调用方须在复用数据前确认完成。

GDMA应在LCD关闭或足够长的可写阶段使用。HBlank DMA须在LCD运行且非STAT模式0时启动;示例从VBlank启动。活动期间不要改变源存储体、目标VBK或源数据。HALT也会暂停HBlank DMA。这些设置函数不额外等待、不保护中断,也不检查源/目标范围。

须另读$FF55获取状态:位7为0表示活动,$FF表示完成。取消后位7为1,位0~6保留剩余块数减1;已复制的字节不会恢复。仅在传输活动时发出取消命令,否则将位7写为0会启动GDMA。整个传输须位于有效源内存及目标VRAM存储体内,不要依赖地址溢出。

这些HDMA寄存器设置函数控制CGB硬件DMA。__vram_copy_dma是CPU复制循环的另一编译器别名,不会因为名称中有dma就使用此DMA控制器。

使用示例

c
__cgb_safe_set_hdma3(0xE0); // Only low five bits select the high VRAM offset.

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

预期结果

完整程序使用位于$C600、按16字节对齐的缓冲区比较三条传输路径。GDMA将32字节复制到$8010:在屏幕(16,24)显示从左上1像素扩展到底行8像素的红色8×8三角形,(24,24)显示红色1像素宽方框。HBlank DMA将32字节复制到$8030:在(16,48)显示蓝色4×8左半块,(24,48)显示蓝色8×8正方形。这些是实际传输的图块图案,而非数字占位。

CANCELED行是CPU预先写入的绿色4×8半块(16,72)和绿色8×8正方形(24,72)。针对这些图块的两块HBlank传输在同一个VBlank内启动并取消,不传输第一块。$FF55应为$81,一帧后图案仍不变。DMG下前两行留白,仅这两个保持检查图形显示为黑色。文字为白底黑字。FAILED CHECKS 000还检查复制字节及完成状态。已在KOKURA测试双模式DMG/CGB及CGB专用执行;未测试部分传输后取消、双速或实机。

__cgb_safe_set_hdma3: --cgb=cgb / DMG
--cgb=cgb / DMG
__cgb_safe_set_hdma3: --cgb=cgb / CGB
--cgb=cgb / CGB
__cgb_safe_set_hdma3: --cgb=cgb_only / CGB
--cgb=cgb_only / CGB

完整示例程序

构建命令

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqgb\kitaqgb.exe .\kitaq-docs\samples\api-examples\gb\cgb_dma_shapes.c -I .\kitaqgb\lib -I .\kitaq-docs\samples -o .\out\cgb_dma_shapes.gb --profile=dev --rst-disable --stack-bank=fixed --cgb=cgb --no-cache --no-disasm
实现与源码

kitaqgb/kitaqgb/CodeGenerator.cs:1873

__cgb_safe_set_hdma4

写入HDMA4($FF54),设置VRAM目标地址的低字节。位0~3被忽略,向下对齐到16字节边界。HDMA3=$E0、HDMA4=$1F选择$8010。这是地址设置,不是传输长度。

语法

c
void __cgb_safe_set_hdma4(u8 value);

此调用格式中的参数名用于解释编译器内置功能,不是需要另行添加的 C 声明。

参数

value

寄存器值,类型u8,范围0~255。位含义见该寄存器说明;传入的是一个字节,不是指针或字节数量。

返回值

无(void)。

使用条件与限制

编译器以CGB模式检查保护写入,DMG模式下不写入。safe仅指此模式保护,不保存或恢复原寄存器值。

每块16字节。新传输可指定1~128块(16~2048字节),因此控制值0表示16字节,不是空传输。GDMA在完成前暂停CPU。HBlank DMA在每个可见行的HBlank传输一块,可跨帧继续;调用方须在复用数据前确认完成。

GDMA应在LCD关闭或足够长的可写阶段使用。HBlank DMA须在LCD运行且非STAT模式0时启动;示例从VBlank启动。活动期间不要改变源存储体、目标VBK或源数据。HALT也会暂停HBlank DMA。这些设置函数不额外等待、不保护中断,也不检查源/目标范围。

须另读$FF55获取状态:位7为0表示活动,$FF表示完成。取消后位7为1,位0~6保留剩余块数减1;已复制的字节不会恢复。仅在传输活动时发出取消命令,否则将位7写为0会启动GDMA。整个传输须位于有效源内存及目标VRAM存储体内,不要依赖地址溢出。

这些HDMA寄存器设置函数控制CGB硬件DMA。__vram_copy_dma是CPU复制循环的另一编译器别名,不会因为名称中有dma就使用此DMA控制器。

使用示例

c
__cgb_safe_set_hdma4(0x1F); // Destination aligns to $8010, not $801F.

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

预期结果

完整程序使用位于$C600、按16字节对齐的缓冲区比较三条传输路径。GDMA将32字节复制到$8010:在屏幕(16,24)显示从左上1像素扩展到底行8像素的红色8×8三角形,(24,24)显示红色1像素宽方框。HBlank DMA将32字节复制到$8030:在(16,48)显示蓝色4×8左半块,(24,48)显示蓝色8×8正方形。这些是实际传输的图块图案,而非数字占位。

CANCELED行是CPU预先写入的绿色4×8半块(16,72)和绿色8×8正方形(24,72)。针对这些图块的两块HBlank传输在同一个VBlank内启动并取消,不传输第一块。$FF55应为$81,一帧后图案仍不变。DMG下前两行留白,仅这两个保持检查图形显示为黑色。文字为白底黑字。FAILED CHECKS 000还检查复制字节及完成状态。已在KOKURA测试双模式DMG/CGB及CGB专用执行;未测试部分传输后取消、双速或实机。

__cgb_safe_set_hdma4: --cgb=cgb / DMG
--cgb=cgb / DMG
__cgb_safe_set_hdma4: --cgb=cgb / CGB
--cgb=cgb / CGB
__cgb_safe_set_hdma4: --cgb=cgb_only / CGB
--cgb=cgb_only / CGB

完整示例程序

构建命令

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqgb\kitaqgb.exe .\kitaq-docs\samples\api-examples\gb\cgb_dma_shapes.c -I .\kitaqgb\lib -I .\kitaq-docs\samples -o .\out\cgb_dma_shapes.gb --profile=dev --rst-disable --stack-bank=fixed --cgb=cgb --no-cache --no-disasm
实现与源码

kitaqgb/kitaqgb/CodeGenerator.cs:1874

__cgb_safe_set_hdma5

写入HDMA5($FF55)以启动DMA或取消活动中的HBlank传输。新传输的位0~6编码“块数减1”,位7选择GDMA(0)或HBlank DMA(1)。HBlank传输活动期间,将位7写为0会取消剩余工作,不会启动新的GDMA。

语法

c
void __cgb_safe_set_hdma5(u8 value);

此调用格式中的参数名用于解释编译器内置功能,不是需要另行添加的 C 声明。

参数

value

寄存器值,类型u8,范围0~255。位含义见该寄存器说明;传入的是一个字节,不是指针或字节数量。

返回值

无(void)。

使用条件与限制

编译器以CGB模式检查保护写入,DMG模式下不写入。safe仅指此模式保护,不保存或恢复原寄存器值。

每块16字节。新传输可指定1~128块(16~2048字节),因此控制值0表示16字节,不是空传输。GDMA在完成前暂停CPU。HBlank DMA在每个可见行的HBlank传输一块,可跨帧继续;调用方须在复用数据前确认完成。

GDMA应在LCD关闭或足够长的可写阶段使用。HBlank DMA须在LCD运行且非STAT模式0时启动;示例从VBlank启动。活动期间不要改变源存储体、目标VBK或源数据。HALT也会暂停HBlank DMA。这些设置函数不额外等待、不保护中断,也不检查源/目标范围。

须另读$FF55获取状态:位7为0表示活动,$FF表示完成。取消后位7为1,位0~6保留剩余块数减1;已复制的字节不会恢复。仅在传输活动时发出取消命令,否则将位7写为0会启动GDMA。整个传输须位于有效源内存及目标VRAM存储体内,不要依赖地址溢出。

这些HDMA寄存器设置函数控制CGB硬件DMA。__vram_copy_dma是CPU复制循环的另一编译器别名,不会因为名称中有dma就使用此DMA控制器。

使用示例

c
__cgb_safe_set_hdma5(1); // LCD is off: GDMA, (1+1)*16 = 32 bytes.
if (color_mode && D_CONTROL!=255) failures++;

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

预期结果

完整程序使用位于$C600、按16字节对齐的缓冲区比较三条传输路径。GDMA将32字节复制到$8010:在屏幕(16,24)显示从左上1像素扩展到底行8像素的红色8×8三角形,(24,24)显示红色1像素宽方框。HBlank DMA将32字节复制到$8030:在(16,48)显示蓝色4×8左半块,(24,48)显示蓝色8×8正方形。这些是实际传输的图块图案,而非数字占位。

CANCELED行是CPU预先写入的绿色4×8半块(16,72)和绿色8×8正方形(24,72)。针对这些图块的两块HBlank传输在同一个VBlank内启动并取消,不传输第一块。$FF55应为$81,一帧后图案仍不变。DMG下前两行留白,仅这两个保持检查图形显示为黑色。文字为白底黑字。FAILED CHECKS 000还检查复制字节及完成状态。已在KOKURA测试双模式DMG/CGB及CGB专用执行;未测试部分传输后取消、双速或实机。

__cgb_safe_set_hdma5: --cgb=cgb / DMG
--cgb=cgb / DMG
__cgb_safe_set_hdma5: --cgb=cgb / CGB
--cgb=cgb / CGB
__cgb_safe_set_hdma5: --cgb=cgb_only / CGB
--cgb=cgb_only / CGB

完整示例程序

构建命令

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqgb\kitaqgb.exe .\kitaq-docs\samples\api-examples\gb\cgb_dma_shapes.c -I .\kitaqgb\lib -I .\kitaq-docs\samples -o .\out\cgb_dma_shapes.gb --profile=dev --rst-disable --stack-bank=fixed --cgb=cgb --no-cache --no-disasm
实现与源码

kitaqgb/kitaqgb/CodeGenerator.cs:1875

__cgb_safe_set_obpd

向CGB的精灵(OBJ)调色板数据寄存器OCPD/OBPD($FF6B)写入一个字节。__cgb_safe_set_ocpd是同一寄存器的别名。对应的索引寄存器选择字节位置;完整RGB15颜色需要低、高两个字节。

语法

c
__cgb_safe_set_obpd(/* 書式は実装と呼び出し例を参照 */);

此调用格式中的参数名用于解释编译器内置功能,不是需要另行添加的 C 声明。

参数

value

一个字节,0~255。依次写RGB15的低字节和高字节;先启用索引自动递增,或逐次明确选择位置。数据写入本身不会启用自动递增。

返回值

无(void)。

使用条件与限制

调色板操作和带模式保护的寄存器写入在DMG模式下不写入任何内容。这里的safe指CGB模式检查,不代表等待PPU可访问或禁止中断。请在LCD关闭时设置,或在可访问的PPU阶段内完成足够短的更新。函数不等待VBlank,也不保护多字节更新免受中断影响。

使用示例

c
__cgb_safe_set_obpd(0); __cgb_safe_set_obpd(0x7C); // Alias of OCPD: blue at slot 31.
expect_color(1,31,31744);

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

预期结果

本程序演示同一图块图案如何分别使用BG和OBJ的调色板。每个三角形为8×8像素,从顶行左侧1个像素逐行扩展到底行8个像素。在x=16、32、48、64、80、96、112处,y=24的BG行依次为红、蓝、绿、红、蓝、绿、红;y=64的OBJ行依次为蓝、绿、红、蓝、绿、红、蓝。各列依次使用单色、RGB分量、索引范围、整组调色板、绝对范围、标准寄存器名及寄存器别名写入。

y=88处的7个8×8格依次显示:红色三角形(x=16)、存储体1中线宽1像素的蓝色方框(32)、水平翻转的绿色三角形(48)、垂直翻转的红色三角形(64)、双向翻转的蓝色三角形(80)、位于红色精灵前方且颜色0区域透出红色的绿色三角形(96),以及关闭BG优先级后覆盖三角形的实心红色精灵(112)。坐标以屏幕左上角为原点,单位为像素。空白处为白色,文字为黑色。

FAILED CHECKS 000表示RGB掩码、含输入2的布尔标志、调色板索引回绕、跨调色板写入、在槽位31截断、null/零长度不写入,以及LCD关闭时的颜色读回检查均通过。测试4种组合:双模式ROM分别在DMG和CGB运行、DMG专用ROM在DMG运行、CGB专用ROM在CGB运行。DMG下CGB MODE为0,三角形均为黑色,存储体及翻转属性不影响显示,最下行最后两格为黑色方块。CGB测试中CGB MODE为1。未测试实机及LCD显示期间的调色板访问冲突。

__cgb_safe_set_obpd: --cgb=cgb / DMG
--cgb=cgb / DMG
__cgb_safe_set_obpd: --cgb=cgb / CGB
--cgb=cgb / CGB
__cgb_safe_set_obpd: --cgb=dmg / DMG
--cgb=dmg / DMG
__cgb_safe_set_obpd: --cgb=cgb_only / CGB
--cgb=cgb_only / CGB

完整示例程序

构建命令

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqgb\kitaqgb.exe .\kitaq-docs\samples\api-examples\gb\cgb_palette_attributes.c -I .\kitaqgb\lib -I .\kitaq-docs\samples -o .\out\cgb_palette_attributes.gb --profile=dev --rst-disable --stack-bank=fixed --cgb=cgb --no-cache --no-disasm
实现与源码

kitaqgb/kitaqgb/CodeGenerator.cs:1869

__cgb_safe_set_obpi

写入CGB的精灵(OBJ)调色板字节索引寄存器OCPS/OBPI($FF6A)。__cgb_safe_set_ocps操作同一寄存器。它选择下一次数据寄存器访问的位置,本身不写入颜色。

语法

c
__cgb_safe_set_obpi(/* 書式は実装と呼び出し例を参照 */);

此调用格式中的参数名用于解释编译器内置功能,不是需要另行添加的 C 声明。

参数

value

位0~5指定字节索引0~63;位7为1时,数据写入后自动递增。位6未使用。颜色槽位n从字节2n开始。索引在63之后回到0,直接操作寄存器时须自行控制范围。

返回值

无(void)。

使用条件与限制

调色板操作和带模式保护的寄存器写入在DMG模式下不写入任何内容。这里的safe指CGB模式检查,不代表等待PPU可访问或禁止中断。请在LCD关闭时设置,或在可访问的PPU阶段内完成足够短的更新。函数不等待VBlank,也不保护多字节更新免受中断影响。

使用示例

c
__cgb_safe_set_obpi(0xBE); // Alias of OCPS: select the OBJ store's last word.

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

预期结果

本程序演示同一图块图案如何分别使用BG和OBJ的调色板。每个三角形为8×8像素,从顶行左侧1个像素逐行扩展到底行8个像素。在x=16、32、48、64、80、96、112处,y=24的BG行依次为红、蓝、绿、红、蓝、绿、红;y=64的OBJ行依次为蓝、绿、红、蓝、绿、红、蓝。各列依次使用单色、RGB分量、索引范围、整组调色板、绝对范围、标准寄存器名及寄存器别名写入。

y=88处的7个8×8格依次显示:红色三角形(x=16)、存储体1中线宽1像素的蓝色方框(32)、水平翻转的绿色三角形(48)、垂直翻转的红色三角形(64)、双向翻转的蓝色三角形(80)、位于红色精灵前方且颜色0区域透出红色的绿色三角形(96),以及关闭BG优先级后覆盖三角形的实心红色精灵(112)。坐标以屏幕左上角为原点,单位为像素。空白处为白色,文字为黑色。

FAILED CHECKS 000表示RGB掩码、含输入2的布尔标志、调色板索引回绕、跨调色板写入、在槽位31截断、null/零长度不写入,以及LCD关闭时的颜色读回检查均通过。测试4种组合:双模式ROM分别在DMG和CGB运行、DMG专用ROM在DMG运行、CGB专用ROM在CGB运行。DMG下CGB MODE为0,三角形均为黑色,存储体及翻转属性不影响显示,最下行最后两格为黑色方块。CGB测试中CGB MODE为1。未测试实机及LCD显示期间的调色板访问冲突。

__cgb_safe_set_obpi: --cgb=cgb / DMG
--cgb=cgb / DMG
__cgb_safe_set_obpi: --cgb=cgb / CGB
--cgb=cgb / CGB
__cgb_safe_set_obpi: --cgb=dmg / DMG
--cgb=dmg / DMG
__cgb_safe_set_obpi: --cgb=cgb_only / CGB
--cgb=cgb_only / CGB

完整示例程序

构建命令

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqgb\kitaqgb.exe .\kitaq-docs\samples\api-examples\gb\cgb_palette_attributes.c -I .\kitaqgb\lib -I .\kitaq-docs\samples -o .\out\cgb_palette_attributes.gb --profile=dev --rst-disable --stack-bank=fixed --cgb=cgb --no-cache --no-disasm
实现与源码

kitaqgb/kitaqgb/CodeGenerator.cs:1867

__cgb_safe_set_svbk

在CGB上直接写WRAM选择寄存器SVBK($FF70)。硬件使用位0~2,原始值0和1都将存储体1映射到$D000–$DFFF,例如8也映射为1。与__svbk_set不同,允许双模式构建,但不返回原存储体编号。

语法

c
void __cgb_safe_set_svbk(u8 value);

此调用格式中的参数名用于解释编译器内置功能,不是需要另行添加的 C 声明。

参数

value

寄存器值,类型u8,范围0~255。位含义见该寄存器说明;传入的是一个字节,不是指针或字节数量。

返回值

无(void)。

使用条件与限制

编译器以CGB模式检查保护写入,DMG模式下不写入。safe仅指此模式保护,不保存或恢复原寄存器值。

切换存储体影响$D000–$DFFF的所有访问,固定WRAM的$C000–$CFFF保持不变。切换期间所需的栈和状态须放在该窗口之外。示例使用--stack-bank=fixed并明确将状态放入固定WRAM。须自行协调中断与使用存储体的DMA;这些API不建立临界区。

使用示例

c
__cgb_safe_set_svbk(0); // Raw selector 0 maps effective bank 1; this API returns void.
if ((W_SELECTOR&7)!=0) failures++;

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

预期结果

CGB专用完整程序在WRAM存储体2的$D100保存1,在存储体3的同一地址保存2,再读回两者并恢复到1。读值分别选择(16,24)的红色8×8正方形与(16,48)的蓝色1像素宽8×8方框。FAILED CHECKS 000检查原始选择值归一化、存储体独立性,以及用常量、变量、表达式、函数返回值切换时的原编号返回。另有6个故意无效的构建测试验证DMG/双模式的KQ2103及常量0、8的KQ2102。运行验证使用KOKURA;未测试实机及中断并发切换。

__cgb_safe_set_svbk: --cgb=cgb_only / CGB
--cgb=cgb_only / CGB

完整示例程序

构建命令

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqgb\kitaqgb.exe .\kitaq-docs\samples\api-examples\gb\cgb_wram_banks.c -I .\kitaqgb\lib -I .\kitaq-docs\samples -o .\out\cgb_wram_banks.gb --profile=dev --rst-disable --stack-bank=fixed --cgb=cgb_only --no-cache --no-disasm
实现与源码

kitaqgb/kitaqgb/CodeGenerator.cs:1862

__critical_enter

递增软件嵌套深度并执行 DI。

语法

c
u8 __critical_enter();

返回值

以 u8 令牌返回先前的嵌套深度:最外层为零,第一层嵌套为一。

使用条件与限制

退出时须按相反顺序传入令牌,并避免嵌套深度达到 256 后回绕。不会保存先前硬件 IME 状态;若必须保留原有的 DI 状态,不要使用这一对函数。

使用示例

c
*((u8*)0xFFFF)=0;outer=__critical_enter();inner=__critical_enter();result[7]=outer;result[8]=inner;

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

预期结果

学习地图选择、滚动、嵌套和扫描行等待。基地址为 38912/39936;嵌套令牌为 0/1;观测到的目标 LY 值为 80/144。同时验证 LCD 关闭时等待函数会返回。

__critical_enter: DMG
DMG
__critical_enter: CGB
CGB

完整示例程序

构建命令

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqgb\kitaqgb.exe .\kitaq-docs\samples\api-examples\gb\batch3_display.c -I .\kitaqgb\lib -I .\kitaq-docs\samples -o .\out\batch3_display.gb --no-cache --no-disasm --profile=dev --rst-disable --stack-bank=fixed --cgb=cgb --cart=mbc5 --romsize=64k --ramsize=none
实现与源码

kitaqgb/kitaqgb/CodeGenerator.cs:7970

kitaqgb/kitaqgb/CodeGenerator.cs:7970

csharp
if (funcName == "__critical_enter")
            {
                if (args.Length != 0) Error(expr, "__critical_enter() expects 0 arguments");

                EmitLoadA(CriticalDepthVar);
                EmitAsm("LD_B_A"); // token = old depth
                EmitAsm("INC_A");
                EmitStoreA(CriticalDepthVar);
                EmitAsm("DI");
                EmitAsm("LD_A_B");
                return;
            }
__critical_leave

递减嵌套深度;令牌为最外层的零时执行 EI。

语法

c
void __critical_leave(u8 token);

参数

token

由配对的 __critical_enter 返回的令牌。

返回值

无返回值。

使用条件与限制

令牌非零时不执行 EI。随意传入零,可能在其他作用域仍有效时启用中断;请正确配对调用。

使用示例

c
__critical_leave(inner);__critical_leave(outer);

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

预期结果

学习地图选择、滚动、嵌套和扫描行等待。基地址为 38912/39936;嵌套令牌为 0/1;观测到的目标 LY 值为 80/144。同时验证 LCD 关闭时等待函数会返回。

__critical_leave: DMG
DMG
__critical_leave: CGB
CGB

完整示例程序

构建命令

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqgb\kitaqgb.exe .\kitaq-docs\samples\api-examples\gb\batch3_display.c -I .\kitaqgb\lib -I .\kitaq-docs\samples -o .\out\batch3_display.gb --no-cache --no-disasm --profile=dev --rst-disable --stack-bank=fixed --cgb=cgb --cart=mbc5 --romsize=64k --ramsize=none
实现与源码

kitaqgb/kitaqgb/CodeGenerator.cs:7984

kitaqgb/kitaqgb/CodeGenerator.cs:7984

csharp
if (funcName == "__critical_leave")
            {
                if (args.Length != 1) Error(expr, "__critical_leave(token) expects 1 argument");

                CompileIntoA(args[0]);
                EmitAsm("LD_B_A"); // token

                AsmOperand clAfterDec = MakeUniqueLabel("critleave_afterdec");
                AsmOperand clDone = MakeUniqueLabel("critleave_done");

                EmitLoadA(CriticalDepthVar);
                EmitAsm("OR_A");
                EmitAsm("JR_Z", clAfterDec);
                EmitAsm("DEC_A");
                EmitStoreA(CriticalDepthVar);
                EmitLabel(clAfterDec);

                // Re-enable only for outermost token (token==0).
                EmitAsm("LD_A_B");
                EmitAsm("OR_A");
                EmitAsm("JR_NZ", clDone);
                EmitAsm("EI");
                EmitLabel(clDone);
                return;
            }
__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。

返回值

每项按 floor(a*b/256) 计算,再将各项相加;返回低 16 位。

使用条件与限制

GB 的乘积除以 256;有符号结果向零截断。Q1.7 变体将该截断结果乘以二,可能与精确除以 128 的结果相差一。各项分别舍入;最终加法按 16 位回绕。

使用示例

c
// Compare literal coefficients with values read from RAM.
result[36]=__dot2_q8_8(513,769,64,127);
v0=513;v1=769;v2=64;v3=127;
result[37]=__dot2_q8_8(v0,v1,v2,v3);

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

预期结果

比较字面量输入与 RAM 中存储的输入。代表性结果以无符号 16 位值显示;负数采用二进制补码位模式。GB 会对定点乘积进行缩放,FC 则返回整数乘积,因此相同输入不一定得到相同结果。

这段代码保存以下 16 位值:result[36]=509, result[37]=509。使用字面量输入和对应 RAM 输入的计算结果一致。屏幕显示代表性数值;result 数组保存全部结果。

__dot2_q8_8: DMG
DMG
__dot2_q8_8: CGB
CGB

完整示例程序

构建命令

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqgb\kitaqgb.exe .\kitaq-docs\samples\api-examples\gb\batch3_math.c -I .\kitaqgb\lib -I .\kitaq-docs\samples -o .\out\batch3_math.gb --no-cache --no-disasm --profile=dev --rst-disable --stack-bank=fixed --cgb=cgb --cart=mbc5 --romsize=64k --ramsize=none
实现与源码

kitaqgb/kitaqgb/CodeGenerator.cs:11575

kitaqgb/kitaqgb/CodeGenerator.cs:11575

csharp
if (funcName == "__dot2_q8_8")
{
    if (args.Length != 4) Error(expr, "__dot2_q8_8 requires 4 arguments");
    LastCallReturnsHL = true;

    // Fast paths when coefficients are compile-time constants.
    // Special-case: 0 / power-of-two coefficients (u8) => shift instead of mul.
    if (TryGetU8Const(args[2], out int ax0) && (ax0 == 0) && TryGetU8Const(args[3], out int ay0) && (ay0 == 0))
    {
        CompileDiscard(args[0]);
        CompileDiscard(args[1]);
        EmitAsm("XOR_A");
        EmitAsm("LD_H_A");
        EmitAsm("LD_L_A");
        return;
    }
    if (TryGetU8Const(args[2], out int axOnly) && (axOnly == 0))
    {
        CompileDiscard(args[0]);
        // only y*ay
        if (!TryEmitDot2Pow2Term(args[1], args[3], signed: false, q1_7: false, out _))
        {
            CompileIntoHL(args[1]);
            EmitAsm("PUSH_HL");
            CompileIntoA(args[3]);
            EmitAsm("LD_C_A");
            EmitAsm("POP_HL");
            EmitMul16x8Shr8_HL("__dot2_y");
        }
        return;
    }
    if (TryGetU8Const(args[3], out int ayOnly) && (ayOnly == 0))
    {
        // only x*ax
        if (!TryEmitDot2Pow2Term(args[0], args[2], signed: false, q1_7: false, out _))
        {
            CompileIntoHL(args[0]);
            EmitAsm("PUSH_HL");
            CompileIntoA(args[2]);
            EmitAsm("LD_C_A");
            EmitAsm("POP_HL");
            EmitMul16x8Shr8_HL("__dot2_x");
        }
        // Preserve the scaled result while evaluating the zero-product input.
        EmitAsm("PUSH_HL");
        CompileDiscard(args[1]);
        EmitAsm("POP_HL");
        return;
    }

    // x*ax
    if (!TryEmitDot2Pow2Term(args[0], args[2], signed: false, q1_7: false, out _))
    {
        CompileIntoHL(args[0]);
        EmitAsm("PUSH_HL");
        CompileIntoA(args[2]);
        EmitAsm("LD_C_A");
        EmitAsm("POP_HL");
        EmitMul16x8Shr8_HL("__dot2_x");
    }
    EmitAsm("PUSH_HL");

    // y*ay
    if (!TryEmitDot2Pow2Term(args[1], args[3], signed: false, q1_7: false, out _))
    {
        CompileIntoHL(args[1]);
        EmitAsm("PUSH_HL");
        CompileIntoA(args[3]);
        EmitAsm("LD_C_A");
        EmitAsm("POP_HL");
        EmitMul16x8Shr8_HL("__dot2_y");
    }
    EmitAsm("POP_DE");
    EmitAsm("ADD_HL_DE");
    return;
}
__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。

返回值

每项按 floor(a*b/256) 计算,再将各项相加;返回低 16 位。

使用条件与限制

GB 的乘积除以 256;有符号结果向零截断。Q1.7 变体将该截断结果乘以二,可能与精确除以 128 的结果相差一。各项分别舍入;最终加法按 16 位回绕。

使用示例

c
// Compare literal coefficients with values read from RAM.
result[6]=__dot3_q8_8(513,769,1025,64,127,3);
v0=513;v1=769;v2=1025;v3=64;v4=127;v5=3;
result[7]=__dot3_q8_8(v0,v1,v2,v3,v4,v5);

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

预期结果

比较字面量输入与 RAM 中存储的输入。代表性结果以无符号 16 位值显示;负数采用二进制补码位模式。GB 会对定点乘积进行缩放,FC 则返回整数乘积,因此相同输入不一定得到相同结果。

这段代码保存以下 16 位值:result[6]=521, result[7]=521。使用字面量输入和对应 RAM 输入的计算结果一致。屏幕显示代表性数值;result 数组保存全部结果。

__dot3_q8_8: DMG
DMG
__dot3_q8_8: CGB
CGB

完整示例程序

构建命令

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqgb\kitaqgb.exe .\kitaq-docs\samples\api-examples\gb\batch3_math.c -I .\kitaqgb\lib -I .\kitaq-docs\samples -o .\out\batch3_math.gb --no-cache --no-disasm --profile=dev --rst-disable --stack-bank=fixed --cgb=cgb --cart=mbc5 --romsize=64k --ramsize=none
实现与源码

kitaqgb/kitaqgb/CodeGenerator.cs:11550

kitaqgb/kitaqgb/CodeGenerator.cs:11550

csharp
if (funcName == "__dot3_q8_8")
            {
                if (args.Length != 6) Error(expr, "__dot3_q8_8 requires 6 arguments");
                LastCallReturnsHL = true;

                // x*ax
                EmitDotTermToHL(args[0], args[3], signed: false, q1_7: false, labelPrefix: "__dot3_x");
                EmitAsm("PUSH_HL");

                // y*ay
                EmitDotTermToHL(args[1], args[4], signed: false, q1_7: false, labelPrefix: "__dot3_y");
                EmitAsm("POP_DE");
                EmitAsm("ADD_HL_DE");
                EmitAsm("PUSH_HL");

                // z*az
                EmitDotTermToHL(args[2], args[5], signed: false, q1_7: false, labelPrefix: "__dot3_z");
                EmitAsm("POP_DE");
                EmitAsm("ADD_HL_DE");
                return;
            }
__farcall

使用指定存储体编号调用不带参数的具名函数。需要切换时,由固定 ROM 中的辅助代码在函数返回后恢复调用方的存储体。

语法

c
__farcall(bank, function_name);

此调用格式中的参数名用于解释编译器内置功能,不是需要另行添加的 C 声明。

参数

bank

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

func

已声明的无参数函数名。此具名调用内置函数不接受保存函数地址的变量。bank 参数不会作为回调参数传入。

返回值

结果类型由被调用函数决定。void 回调不返回值;u8 和 u16 回调返回相应标量值。展示的示例使用 void 回调。

使用条件与限制

这些调用不传递用户参数,也不会自动使回调可在中断中重入。请正确管理回调自身的数据、栈和中断使用。恢复存储体的调用路径已测试嵌套和重复调用;此示例不覆盖实机或任意中断交错。

这些 GB 存储体接口只携带 8 位编号,不设置 MBC5 的第 9 位。仍须遵守卡带规则,包括 mapper 对编号 0 的处理。示例明确使用 MBC5 存储体 1–3;不要混用未记录的 mapper 直接写入。

使用示例

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 为 03:far_call、__farcall 和 __farcall_ptr 各执行一次回调。

__farcall: DMG
DMG
__farcall: CGB
CGB

完整示例程序

构建命令

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqgb\kitaqgb.exe .\kitaq-docs\samples\api-examples\gb\banked_data_calls.c -I .\kitaqgb\lib -I .\kitaq-docs\samples -o .\out\banked_data_calls.gb --profile=dev --rst-disable --stack-bank=fixed --cgb=cgb --cart=mbc5 --romsize=64k --no-cache --no-disasm
实现与源码

kitaqgb/kitaqgb/CodeGenerator.cs:7288

kitaqgb/kitaqgb/CodeGenerator.cs:7288

csharp
if (funcName == "__farcall")
            {
                if (args.Length != 2) Program.Error("__farcall(bank, func) expects 2 arguments");
                if (!args[1].Match(Tag.Name, out string targetName)) Program.Error("__farcall(bank, func) requires 2nd argument to be a function name");

                // A named far call supplies no arguments. Reject data symbols
                // and callbacks that would otherwise read stale argument storage.
                CFunctionInfo targetInfo;
                if (!Functions.TryGetValue(targetName, out targetInfo))
                {
                    Error(expr, "__farcall requires a declared function name");
                    return;
                }
                if ((targetInfo.Parameters ?? Array.Empty<FieldInfo>()).Length != 0)
                {
                    Error(expr, "__farcall requires a callback with no parameters");
                    return;
                }
                Expr bankExpr = FoldConstants(args[0]);
                int targetBank = -1;
                if (targetInfo != null)
                {
                    targetBank = targetInfo.RomBank;
                    PrepareStructReturnDestination(expr, targetInfo.ReturnType, targetInfo, targetName);
                    if (targetInfo.ReturnType != null)
                        LastCallReturnsHL = ReturnsInHL(expr, targetInfo.ReturnType);
                }

                // A known bank permits direct dispatch when safe, otherwise request a fixed-bank thunk.
                if (TryResolveBankExprToConstU8(args[0], out int bankLiteral))
                {
                    int optimizedBank = bankLiteral & 0xFF;
                    bool farcallDirectOK = IsDirectCallBankSafe(optimizedBank, CurrentFunctionBank);
                    if (!farcallDirectOK)
                    {
                        string farcallThunkName = EnsureBank0Thunk(
                            targetName,
                            optimizedBank,
                            isStackCall: targetInfo != null && targetInfo.IsStackCall,
                            stackArgBytes: 0);
                        EmitAsm("CALL", new AsmOperand(farcallThunkName, AddressMode.Absolute));
                        RecordCallEdge(expr, targetName, optimizedBank, "farcall_bank_thunk", viaThunk: true, viaFarcall: true, actualArgSizes: new int[0], expectedArgSizes: new int[0]);
                    }
                    else
                    {
                        if (Program.CheckBankCalls && !InUnsafe && optimizedBank != 0)
                        {
                            AsmOperand ok = MakeUniqueLabel("kq_bank_ok");
                            EmitLoadA(RomBankVar);
                            EmitAsm("CP_IMM", new AsmOperand(optimizedBank & 0xFF, AddressMode.Immediate));
                            EmitAsm("JP_Z", ok);
                            EmitAsm("JP", EnsureCheckTrap());
                            EmitLabel(ok);
                        }

                        EmitAsm("CALL", new AsmOperand(targetName, AddressMode.Absolute));
                        RecordCallEdge(expr, targetName, optimizedBank, "farcall_direct", viaThunk: false, viaFarcall: true, actualArgSizes: new int[0], expectedArgSizes: new int[0]);
                    }
                    return;
                }

                // A = requested bank argument (runtime value)
                CompileIntoA(bankExpr);

                // Always dispatch via bank0 helper thunk.
                string thunkName = EnsureBank0RuntimeFarcallThunk(targetName);
                EmitAsm("CALL", new AsmOperand(thunkName, AddressMode.Absolute));
                RecordCallEdge(expr, targetName, targetBank, "farcall_intrinsic", viaThunk: true, viaFarcall: true, actualArgSizes: new int[0], expectedArgSizes: new int[0]);
                return;
            }
__getwinmapbase

读取 LCDC 的第 6 位,确定窗口图块地图基地址。

语法

c
u16 __getwinmapbase();

返回值

返回 $9800 或 $9C00,即图块地图基地址,而不是图块图形地址。

使用示例

c
M_LCDC=0;result[2]=__getwinmapbase();M_LCDC=0x40;result[3]=__getwinmapbase();

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

预期结果

学习地图选择、滚动、嵌套和扫描行等待。基地址为 38912/39936;嵌套令牌为 0/1;观测到的目标 LY 值为 80/144。同时验证 LCD 关闭时等待函数会返回。

__getwinmapbase: DMG
DMG
__getwinmapbase: CGB
CGB

完整示例程序

构建命令

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqgb\kitaqgb.exe .\kitaq-docs\samples\api-examples\gb\batch3_display.c -I .\kitaqgb\lib -I .\kitaq-docs\samples -o .\out\batch3_display.gb --no-cache --no-disasm --profile=dev --rst-disable --stack-bank=fixed --cgb=cgb --cart=mbc5 --romsize=64k --ramsize=none
实现与源码

kitaqgb/kitaqgb/CodeGenerator.cs:10642

kitaqgb/kitaqgb/CodeGenerator.cs:10642

csharp
if (funcName == "__getwinmapbase")
{
    if (args.Length != 0) Error(expr, "__getwinmapbase requires 0 arguments");

    AsmOperand win_base0 = MakeUniqueLabel("getwinbase_base0");
    AsmOperand win_done  = MakeUniqueLabel("getwinbase_done");

    EmitAsm("LDH_A_MEM", new AsmOperand(0x40, AddressMode.HighMem)); // LCDC
    EmitAsm("AND_IMM", new AsmOperand(0x40, AddressMode.Immediate)); // bit6 (Window tile map display select)
    EmitAsm("JR_Z", win_base0);
    EmitAsm("LD_HL_IMM", new AsmOperand(0x9C00, AddressMode.Immediate16));
    EmitAsm("JR", win_done);
    EmitLabel(win_base0);
    EmitAsm("LD_HL_IMM", new AsmOperand(0x9800, AddressMode.Immediate16));
    EmitLabel(win_done);
    LastCallReturnsHL = true;
    return;
}
__mac16

计算无符号乘加。

语法

c
u16 __mac16(u16 acc, u16 a, u8 b);

参数

acc

要加上的 16 位累加器值。

a

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

b

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

返回值

每项按 floor(a*b/256) 计算,再加到 acc 上;返回低 16 位。

使用条件与限制

GB 的乘积除以 256;有符号结果向零截断。Q1.7 变体将该截断结果乘以二,可能与精确除以 128 的结果相差一。各项分别舍入;最终加法按 16 位回绕。

使用示例

c
// Compare literal coefficients with values read from RAM.
result[34]=__mac16(65500,513,127);
v0=65500;v1=513;v2=127;
result[35]=__mac16(v0,v1,v2);

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

预期结果

比较字面量输入与 RAM 中存储的输入。代表性结果以无符号 16 位值显示;负数采用二进制补码位模式。GB 会对定点乘积进行缩放,FC 则返回整数乘积,因此相同输入不一定得到相同结果。

这段代码保存以下 16 位值:result[34]=218, result[35]=218。使用字面量输入和对应 RAM 输入的计算结果一致。屏幕显示代表性数值;result 数组保存全部结果。

__mac16: DMG
DMG
__mac16: CGB
CGB

完整示例程序

构建命令

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqgb\kitaqgb.exe .\kitaq-docs\samples\api-examples\gb\batch3_math.c -I .\kitaqgb\lib -I .\kitaq-docs\samples -o .\out\batch3_math.gb --no-cache --no-disasm --profile=dev --rst-disable --stack-bank=fixed --cgb=cgb --cart=mbc5 --romsize=64k --ramsize=none
实现与源码

kitaqgb/kitaqgb/CodeGenerator.cs:11499

kitaqgb/kitaqgb/CodeGenerator.cs:11499

csharp
if (funcName == "__mac16")
            {
                if (args.Length != 3) Error(expr, "__mac16 requires 3 arguments");

                // mul = (a*b)>>8
                EmitDotTermToHL(args[1], args[2], signed: false, q1_7: false, labelPrefix: "__mac16_mul");

                // acc + mul
                EmitAsm("PUSH_HL");
                CompileIntoHL(args[0]);
                EmitAsm("POP_DE");
                EmitAsm("ADD_HL_DE");
                LastCallReturnsHL = true;
                return;
            }
__mul16x8

计算无符号乘法。

语法

c
u16 __mul16x8(u16 a, u8 b);

参数

a

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

b

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

返回值

每项按 floor(a*b/256) 计算;返回低 16 位。

使用条件与限制

GB 的乘积除以 256;有符号结果向零截断。Q1.7 变体将该截断结果乘以二,可能与精确除以 128 的结果相差一。各项分别舍入;最终加法按 16 位回绕。

使用示例

c
// Compare literal coefficients with values read from RAM.
result[24]=__mul16x8(513,127);
v0=513;v1=127;
result[25]=__mul16x8(v0,v1);

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

预期结果

比较字面量输入与 RAM 中存储的输入。代表性结果以无符号 16 位值显示;负数采用二进制补码位模式。GB 会对定点乘积进行缩放,FC 则返回整数乘积,因此相同输入不一定得到相同结果。

这段代码保存以下 16 位值:result[24]=254, result[25]=254。使用字面量输入和对应 RAM 输入的计算结果一致。屏幕显示代表性数值;result 数组保存全部结果。

__mul16x8: DMG
DMG
__mul16x8: CGB
CGB

完整示例程序

构建命令

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqgb\kitaqgb.exe .\kitaq-docs\samples\api-examples\gb\batch3_math.c -I .\kitaqgb\lib -I .\kitaq-docs\samples -o .\out\batch3_math.gb --no-cache --no-disasm --profile=dev --rst-disable --stack-bank=fixed --cgb=cgb --cart=mbc5 --romsize=64k --ramsize=none
实现与源码

kitaqgb/kitaqgb/CodeGenerator.cs:11467

kitaqgb/kitaqgb/CodeGenerator.cs:11467

csharp
if (funcName == "__mul16x8")
            {
                if (args.Length != 2) Error(expr, "__mul16x8 requires 2 arguments");

                EmitDotTermToHL(args[0], args[1], signed: false, q1_7: false, labelPrefix: "__mul16x8");
                LastCallReturnsHL = true;
                return;
            }
__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[4]=__mul8x8_hi(255,255);
v0=255;v1=255;
result[5]=__mul8x8_hi(v0,v1);

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

预期结果

比较字面量输入与 RAM 中存储的输入。代表性结果以无符号 16 位值显示;负数采用二进制补码位模式。GB 会对定点乘积进行缩放,FC 则返回整数乘积,因此相同输入不一定得到相同结果。

这段代码保存以下 16 位值:result[4]=254, result[5]=254。使用字面量输入和对应 RAM 输入的计算结果一致。屏幕显示代表性数值;result 数组保存全部结果。

__mul8x8_hi: DMG
DMG
__mul8x8_hi: CGB
CGB

完整示例程序

构建命令

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqgb\kitaqgb.exe .\kitaq-docs\samples\api-examples\gb\batch3_math.c -I .\kitaqgb\lib -I .\kitaq-docs\samples -o .\out\batch3_math.gb --no-cache --no-disasm --profile=dev --rst-disable --stack-bank=fixed --cgb=cgb --cart=mbc5 --romsize=64k --ramsize=none
实现与源码

kitaqgb/kitaqgb/CodeGenerator.cs:11452

kitaqgb/kitaqgb/CodeGenerator.cs:11452

csharp
if (funcName == "__mul8x8_hi")
            {
                if (args.Length != 2) Error(expr, "__mul8x8_hi requires 2 arguments");

                CompileIntoA(args[0]);
                EmitAsm("PUSH_AF");
                CompileIntoA(args[1]);
                EmitAsm("LD_C_A");
                EmitAsm("POP_AF");
                EmitAsm("LD_E_A");
                EmitMul8x8ToHL_EC("__mul8x8_hi");
                EmitAsm("LD_A_H");
                return;
            }
__padrep

从mask指定的按钮中选择一个操作进行连发。存在符合条件的新按下位时,选择最低有效位并立即返回。否则,只要当前操作仍在掩码范围内且仍被按住,就保持该操作。该操作结束后,选择剩余按住位中的最低位,但不立即产生脉冲。没有符合条件的按住位时清空运行状态。

语法

c
u8 __padrep(u8* state, u8 keys, u8 trigger, u8 mask);

此调用格式中的参数名用于解释编译器内置功能,不是需要另行添加的 C 声明。

参数

state

指向连续5字节可写RAM的指针,例如u8 state[5]。布局为[0]当前按钮位、[1]初始等待时间、[2]连发间隔、[3]初始等待经过计数、[4]连发经过计数。每个独立的连发处理都应使用单独的状态,并在使用前初始化。不支持空指针、少于5字节的区域,或在方向专用处理与通用处理之间共用状态。

keys

使用GB BTN_*布局的当前完整按住掩码。传入__readpad的结果、__readpadex的低字节或input_current的结果。连发函数本身不读取硬件。

trigger

本次更新中新按下的位,布局与keys相同,通常为keys & ~previous_keys或__readpadex的高字节。只能包含同时存在于keys中的位。每次都传入按住掩码会使每次调用都被误认为初次按下,重新开始等待。

mask

用|组合允许参与连发的GB按钮位。0会禁用所有操作并清空运行状态。多个位可提供多个候选,但每次调用只返回选中的一个位,不返回组合脉冲掩码。

返回值

没有操作脉冲时返回0,否则返回1, 2, 4, 8, 16, 32, 64, 128之一。非零结果是按钮位,不是归一化布尔值,也不是连发次数。

使用条件与限制

通用掩码为 BTN_RIGHT=0x01、BTN_LEFT=0x02、BTN_UP=0x04、BTN_DOWN=0x08、BTN_A=0x10、BTN_B=0x20、BTN_SELECT=0x40、BTN_START=0x80。位为 1 表示按住。GB 和 FC 的 input_* 均使用此格式;FC 原始手柄函数的位序不同。

初始等待时间,范围0至255,单位为连发函数的调用次数。正值N表示初次按下脉冲之后恰好经过N次调用产生首次长按连发。为0时跳过初始等待相等分支,由连发间隔决定首次长按脉冲的时间。单位不是毫秒。

后续连发间隔,范围0至255次调用。正值N表示首次连发之后,每N次有效按住更新产生一次脉冲。0表示初始等待结束后每次有效更新都产生脉冲,因此在该阶段0和1均为每次更新一次。等待时间和间隔均为0时,每次按住更新都会产生脉冲。

每个预定输入周期调用一次。每次调用都会修改状态,不会等待VBlank。没有新按下位而接管已按住的按钮时,该次调用返回0并开始新的等待。这组函数每个状态只处理一个当前操作;input_repeat则为八个按钮分别维护独立计时器。

使用示例

c
result=__padrep(actions,keys,trigger,48); // A|B: select A, the lower bit.
    if (result!=0) action_pulses++;

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

预期结果

此程序把预设按钮状态送入四个独立状态,演示连发具体发生在哪次调用。等待时间3、间隔2时,编号0至7的八次调用会在0、3、5、7产生脉冲。因此RIGHT PULSES、DOWN PULSES、ACTION PULSES和ALIAS PULSES均显示004。通用处理同时接收A和B,选择A(16)。程序还检查初始化、重置、双方向优先级、松开、接管已按住的方向、时间值0和表达式参数。CHECKS应为057,FAILED CHECKS应为000。这是使用固定输入的状态处理示例,不会等待你实际按下按钮。

__padrep: DMG
DMG
__padrep: CGB
CGB

完整示例程序

构建命令

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqgb\kitaqgb.exe .\kitaq-docs\samples\api-examples\gb\pad_repeat_state.c -I .\kitaq-docs\samples -I .\kitaqgb\lib -o .\out\pad_repeat_state.gb --profile=dev --rst-disable --stack-bank=fixed --cgb=cgb --no-cache --no-disasm
实现与源码

kitaqgb/kitaqgb/CodeGenerator.cs:11249

csharp
{
    if (args.Length != 4) Error(expr, "__padrep requires 4 arguments (state*, keys, trigger, mask)");

    AsmOperand pm_done        = MakeUniqueLabel("padrepM_done");
    AsmOperand pm_ret0        = MakeUniqueLabel("padrepM_ret0");
    AsmOperand pm_no_trig     = MakeUniqueLabel("padrepM_notrig");
    AsmOperand pm_reset_all   = MakeUniqueLabel("padrepM_reset");
    AsmOperand pm_dir0        = MakeUniqueLabel("padrepM_dir0");
    AsmOperand pm_have_dir    = MakeUniqueLabel("padrepM_havedir");
    AsmOperand pm_first_rep   = MakeUniqueLabel("padrepM_firstrep");
    AsmOperand pm_ret_dir     = MakeUniqueLabel("padrepM_retdir");
    AsmOperand pm_skip_dasinc = MakeUniqueLabel("padrepM_skipdasinc");
    AsmOperand pm_skip_arrinc = MakeUniqueLabel("padrepM_skiparrinc");

    // Evaluate args safely:
    // HL = state*, B = keys, C = trigger, E = mask
    CompileIntoHL(args[0]);
    EmitAsm("PUSH_HL");
    CompileIntoA(args[1]); EmitAsm("LD_B_A"); // keys
    CompileIntoA(args[2]); EmitAsm("LD_C_A"); // trigger
    CompileIntoA(args[3]); EmitAsm("LD_E_A"); // mask
    EmitAsm("POP_HL");

    // trigMasked = trigger & mask -> D
    EmitAsm("LD_A_C");
    EmitAsm("AND_E");
    EmitAsm("LD_D_A");
    EmitAsm("OR_A");
    EmitAsm("JP_Z", pm_no_trig);

    // select = lowbit(trigMasked) = x & -x
    EmitAsm("LD_A_D");
    EmitAsm("CPL");
    EmitAsm("INC_A");
    EmitAsm("AND_D");
    EmitAsm("LD_C_A"); // save selected in C

    // dirMask=selected, dasCnt=0, arrCnt=0
    EmitAsm("LD_A_C");
    EmitAsm("LD_HL_A"); // [0]=selected
    EmitAsm("PUSH_HL");
    EmitAsm("INC_HL"); Emi
__padrep_down

仅针对下方向推进一次连发状态。下键刚按下时立即返回8并重新开始计数。持续按住下键时按状态中的时间设置产生后续脉冲,松开后清空运行状态。其他按钮不会触发此连发处理。

语法

c
u8 __padrep_down(u8* state, u8 keys, u8 trigger);

此调用格式中的参数名用于解释编译器内置功能,不是需要另行添加的 C 声明。

参数

state

指向连续5字节可写RAM的指针,例如u8 state[5]。布局为[0]当前按钮位、[1]初始等待时间、[2]连发间隔、[3]初始等待经过计数、[4]连发经过计数。每个独立的连发处理都应使用单独的状态,并在使用前初始化。不支持空指针、少于5字节的区域,或在方向专用处理与通用处理之间共用状态。

keys

使用GB BTN_*布局的当前完整按住掩码。传入__readpad的结果、__readpadex的低字节或input_current的结果。连发函数本身不读取硬件。

trigger

本次更新中新按下的位,布局与keys相同,通常为keys & ~previous_keys或__readpadex的高字节。只能包含同时存在于keys中的位。每次都传入按住掩码会使每次调用都被误认为初次按下,重新开始等待。

返回值

没有操作脉冲时返回0,否则返回8之一。非零结果是按钮位,不是归一化布尔值,也不是连发次数。

使用条件与限制

通用掩码为 BTN_RIGHT=0x01、BTN_LEFT=0x02、BTN_UP=0x04、BTN_DOWN=0x08、BTN_A=0x10、BTN_B=0x20、BTN_SELECT=0x40、BTN_START=0x80。位为 1 表示按住。GB 和 FC 的 input_* 均使用此格式;FC 原始手柄函数的位序不同。

初始等待时间,范围0至255,单位为连发函数的调用次数。正值N表示初次按下脉冲之后恰好经过N次调用产生首次长按连发。为0时跳过初始等待相等分支,由连发间隔决定首次长按脉冲的时间。单位不是毫秒。

后续连发间隔,范围0至255次调用。正值N表示首次连发之后,每N次有效按住更新产生一次脉冲。0表示初始等待结束后每次有效更新都产生脉冲,因此在该阶段0和1均为每次更新一次。等待时间和间隔均为0时,每次按住更新都会产生脉冲。

每个预定输入周期调用一次。每次调用都会修改状态,不会等待VBlank。没有新按下位而接管已按住的按钮时,该次调用返回0并开始新的等待。这组函数每个状态只处理一个当前操作;input_repeat则为八个按钮分别维护独立计时器。

使用示例

c
result=__padrep_down(down,keys,trigger);
    if (result!=0) down_pulses++;

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

预期结果

此程序把预设按钮状态送入四个独立状态,演示连发具体发生在哪次调用。等待时间3、间隔2时,编号0至7的八次调用会在0、3、5、7产生脉冲。因此RIGHT PULSES、DOWN PULSES、ACTION PULSES和ALIAS PULSES均显示004。通用处理同时接收A和B,选择A(16)。程序还检查初始化、重置、双方向优先级、松开、接管已按住的方向、时间值0和表达式参数。CHECKS应为057,FAILED CHECKS应为000。这是使用固定输入的状态处理示例,不会等待你实际按下按钮。

__padrep_down: DMG
DMG
__padrep_down: CGB
CGB

完整示例程序

构建命令

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqgb\kitaqgb.exe .\kitaq-docs\samples\api-examples\gb\pad_repeat_state.c -I .\kitaq-docs\samples -I .\kitaqgb\lib -o .\out\pad_repeat_state.gb --profile=dev --rst-disable --stack-bank=fixed --cgb=cgb --no-cache --no-disasm
实现与源码

kitaqgb/kitaqgb/CodeGenerator.cs:11069

csharp
{
    if (args.Length != 3) Error(expr, "__padrep_down requires 3 arguments (state*, keys, trigger)");

    AsmOperand pd_done        = MakeUniqueLabel("padrepD_done");
    AsmOperand pd_ret0        = MakeUniqueLabel("padrepD_ret0");
    AsmOperand pd_no_trig     = MakeUniqueLabel("padrepD_notrig");
    AsmOperand pd_dir0        = MakeUniqueLabel("padrepD_dir0");
    AsmOperand pd_have_dir    = MakeUniqueLabel("padrepD_havedir");
    AsmOperand pd_first_rep   = MakeUniqueLabel("padrepD_firstrep");
    AsmOperand pd_ret_dir     = MakeUniqueLabel("padrepD_retdir");
    AsmOperand pd_skip_dasinc = MakeUniqueLabel("padrepD_skipdasinc");
    AsmOperand pd_skip_arrinc = MakeUniqueLabel("padrepD_skiparrinc");
    AsmOperand pd_reset_all   = MakeUniqueLabel("padrepD_reset");

    // Evaluate args safely:
    // HL = state*, B = keys, C = trigger
    CompileIntoHL(args[0]);
    EmitAsm("PUSH_HL");
    CompileIntoA(args[1]); EmitAsm("LD_B_A"); // keys
    CompileIntoA(args[2]); EmitAsm("LD_C_A"); // trigger
    EmitAsm("POP_HL");

    // --- Trigger Down? (bit3 = 0x08) ---
    EmitAsm("LD_A_C");
    EmitAsm("AND_IMM", new AsmOperand(0x08, AddressMode.Immediate));
    EmitAsm("JP_Z", pd_no_trig);

    // dirMask=0x08, dasCnt=0, arrCnt=0
    EmitAsm("LD_A_IMM", new AsmOperand(0x08, AddressMode.Immediate));
    EmitAsm("LD_HL_A"); // [0]=0x08
    EmitAsm("PUSH_HL");
    EmitAsm("INC_HL"); EmitAsm("INC_HL"); EmitAsm("INC_HL"); // -> [3]
    EmitAsm("XOR_A");
    EmitAsm("LD_HL_A"); // dasCnt=0
    EmitAsm("INC_HL");
    EmitAsm("LD_HL_A"); // arrCnt=0
    EmitAsm("POP_HL");

    EmitAsm
__padrep_init

初始化调用方提供的5字节连发状态:清空当前操作和两个计数器,保存指定的初始等待时间与连发间隔。不读取按钮,也不产生连发脉冲。

语法

c
void __padrep_init(u8* state, u8 das, u8 arr);

此调用格式中的参数名用于解释编译器内置功能,不是需要另行添加的 C 声明。

参数

state

指向连续5字节可写RAM的指针,例如u8 state[5]。布局为[0]当前按钮位、[1]初始等待时间、[2]连发间隔、[3]初始等待经过计数、[4]连发经过计数。每个独立的连发处理都应使用单独的状态,并在使用前初始化。不支持空指针、少于5字节的区域,或在方向专用处理与通用处理之间共用状态。

das

初始等待时间,范围0至255,单位为连发函数的调用次数。正值N表示初次按下脉冲之后恰好经过N次调用产生首次长按连发。为0时跳过初始等待相等分支,由连发间隔决定首次长按脉冲的时间。单位不是毫秒。

arr

后续连发间隔,范围0至255次调用。正值N表示首次连发之后,每N次有效按住更新产生一次脉冲。0表示初始等待结束后每次有效更新都产生脉冲,因此在该阶段0和1均为每次更新一次。等待时间和间隔均为0时,每次按住更新都会产生脉冲。

返回值

无(void)。

使用示例

c
__padrep_init(lr,3,2);
// Active action and counters are zero; delay and interval are retained.
expect(lr[0],0); expect(lr[1],3); expect(lr[2],2);
expect(lr[3],0); expect(lr[4],0);

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

预期结果

此程序把预设按钮状态送入四个独立状态,演示连发具体发生在哪次调用。等待时间3、间隔2时,编号0至7的八次调用会在0、3、5、7产生脉冲。因此RIGHT PULSES、DOWN PULSES、ACTION PULSES和ALIAS PULSES均显示004。通用处理同时接收A和B,选择A(16)。程序还检查初始化、重置、双方向优先级、松开、接管已按住的方向、时间值0和表达式参数。CHECKS应为057,FAILED CHECKS应为000。这是使用固定输入的状态处理示例,不会等待你实际按下按钮。

__padrep_init: DMG
DMG
__padrep_init: CGB
CGB

完整示例程序

构建命令

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqgb\kitaqgb.exe .\kitaq-docs\samples\api-examples\gb\pad_repeat_state.c -I .\kitaq-docs\samples -I .\kitaqgb\lib -o .\out\pad_repeat_state.gb --profile=dev --rst-disable --stack-bank=fixed --cgb=cgb --no-cache --no-disasm
实现与源码

kitaqgb/kitaqgb/CodeGenerator.cs:10808

csharp
{
    if (args.Length != 3) Error(expr, "__padrep_init requires 3 arguments (state*, das, arr)");

    // Evaluate args with register safety
    CompileIntoHL(args[0]);
    EmitAsm("PUSH_HL");
    CompileIntoA(args[1]); EmitAsm("LD_B_A"); // das
    CompileIntoA(args[2]); EmitAsm("LD_C_A"); // arr
    EmitAsm("POP_HL");

    // dirMask = 0; dasCnt=0; arrCnt=0; store params
    EmitAsm("XOR_A");
    EmitAsm("LD_HL_A"); // [0]=0
    EmitAsm("INC_HL");
    EmitAsm("LD_A_B");
    EmitAsm("LD_HL_A"); // [1]=das
    EmitAsm("INC_HL");
    EmitAsm("LD_A_C");
    EmitAsm("LD_HL_A"); // [2]=arr
    EmitAsm("INC_HL");
    EmitAsm("XOR_A");
    EmitAsm("LD_HL_A"); // [3]=dasCnt
    EmitAsm("INC_HL");
    EmitAsm("LD_HL_A"); // [4]=arrCnt
    return;
}

// 2.21 __padrep_reset(state_ptr) -> void
// Resets runtime counters for PadRepeatState while keeping das/arr params:
// dirMask=0, dasCnt=0, arrCnt=0
// Treated as a compiler intrinsic (no C definition required).
if (funcName == "__padrep_reset")
{
    if (args.Length != 1) Error(expr, "__padrep_reset requires 1 argument (state*)");

    CompileIntoHL(args[0]);

    EmitAsm("XOR_A");
    EmitAsm("LD_HL_A"); // [0]=0 (dirMask)

    // [3]=0, [4]=0
    EmitAsm("PUSH_HL");
    EmitAsm("INC_HL"); EmitAsm("INC_HL"); EmitAsm("INC_HL"); // -> [3]
    EmitAsm("LD_HL_A");
    EmitAsm("INC_HL"); // -> [4]
    EmitAsm("LD_HL_A");
    EmitAsm("POP_HL");
    return;
}

// __padrep_lr(state*, keys, trigger) returns Right (1), Left (2), or zero.
// New triggers take priority, with Right selected first when both trigger bits are set.
// Without a new
__padrep_lr

将左或右其中一个操作的连发状态推进一次。右键刚按下时立即返回1,否则左键刚按下时返回2。两者同时刚按下时优先选择右。没有新按下事件且同时按住两个方向时返回0并暂停计数器;两个方向均未按住时清空运行状态。

语法

c
u8 __padrep_lr(u8* state, u8 keys, u8 trigger);

此调用格式中的参数名用于解释编译器内置功能,不是需要另行添加的 C 声明。

参数

state

指向连续5字节可写RAM的指针,例如u8 state[5]。布局为[0]当前按钮位、[1]初始等待时间、[2]连发间隔、[3]初始等待经过计数、[4]连发经过计数。每个独立的连发处理都应使用单独的状态,并在使用前初始化。不支持空指针、少于5字节的区域,或在方向专用处理与通用处理之间共用状态。

keys

使用GB BTN_*布局的当前完整按住掩码。传入__readpad的结果、__readpadex的低字节或input_current的结果。连发函数本身不读取硬件。

trigger

本次更新中新按下的位,布局与keys相同,通常为keys & ~previous_keys或__readpadex的高字节。只能包含同时存在于keys中的位。每次都传入按住掩码会使每次调用都被误认为初次按下,重新开始等待。

返回值

没有操作脉冲时返回0,否则返回1, 2之一。非零结果是按钮位,不是归一化布尔值,也不是连发次数。

使用条件与限制

通用掩码为 BTN_RIGHT=0x01、BTN_LEFT=0x02、BTN_UP=0x04、BTN_DOWN=0x08、BTN_A=0x10、BTN_B=0x20、BTN_SELECT=0x40、BTN_START=0x80。位为 1 表示按住。GB 和 FC 的 input_* 均使用此格式;FC 原始手柄函数的位序不同。

初始等待时间,范围0至255,单位为连发函数的调用次数。正值N表示初次按下脉冲之后恰好经过N次调用产生首次长按连发。为0时跳过初始等待相等分支,由连发间隔决定首次长按脉冲的时间。单位不是毫秒。

后续连发间隔,范围0至255次调用。正值N表示首次连发之后,每N次有效按住更新产生一次脉冲。0表示初始等待结束后每次有效更新都产生脉冲,因此在该阶段0和1均为每次更新一次。等待时间和间隔均为0时,每次按住更新都会产生脉冲。

每个预定输入周期调用一次。每次调用都会修改状态,不会等待VBlank。没有新按下位而接管已按住的按钮时,该次调用返回0并开始新的等待。这组函数每个状态只处理一个当前操作;input_repeat则为八个按钮分别维护独立计时器。

使用示例

c
result=__padrep_lr(lr,keys,trigger);
    if (result!=0) pulses++;

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

预期结果

此程序把预设按钮状态送入四个独立状态,演示连发具体发生在哪次调用。等待时间3、间隔2时,编号0至7的八次调用会在0、3、5、7产生脉冲。因此RIGHT PULSES、DOWN PULSES、ACTION PULSES和ALIAS PULSES均显示004。通用处理同时接收A和B,选择A(16)。程序还检查初始化、重置、双方向优先级、松开、接管已按住的方向、时间值0和表达式参数。CHECKS应为057,FAILED CHECKS应为000。这是使用固定输入的状态处理示例,不会等待你实际按下按钮。

__padrep_lr: DMG
DMG
__padrep_lr: CGB
CGB

完整示例程序

构建命令

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqgb\kitaqgb.exe .\kitaq-docs\samples\api-examples\gb\pad_repeat_state.c -I .\kitaq-docs\samples -I .\kitaqgb\lib -o .\out\pad_repeat_state.gb --profile=dev --rst-disable --stack-bank=fixed --cgb=cgb --no-cache --no-disasm
实现与源码

kitaqgb/kitaqgb/CodeGenerator.cs:10864

csharp
{
    if (args.Length != 3) Error(expr, "__padrep_lr requires 3 arguments (state*, keys, trigger)");

    AsmOperand pr_done        = MakeUniqueLabel("padrep_done");
    AsmOperand pr_ret0        = MakeUniqueLabel("padrep_ret0");
    AsmOperand pr_check_left  = MakeUniqueLabel("padrep_chkL");
    AsmOperand pr_no_trig     = MakeUniqueLabel("padrep_notrig");
    AsmOperand pr_reset_all   = MakeUniqueLabel("padrep_reset");
    AsmOperand pr_dir0        = MakeUniqueLabel("padrep_dir0");
    AsmOperand pr_have_dir    = MakeUniqueLabel("padrep_havedir");
    AsmOperand pr_first_rep   = MakeUniqueLabel("padrep_firstrep");
    AsmOperand pr_ret_dir     = MakeUniqueLabel("padrep_retdir");
    AsmOperand pr_skip_dasinc = MakeUniqueLabel("padrep_skipdasinc");
    AsmOperand pr_skip_arrinc = MakeUniqueLabel("padrep_skiparrinc");

    // Evaluate args safely:
    // HL = state*, B = keys, C = trigger
    CompileIntoHL(args[0]);
    EmitAsm("PUSH_HL");
    CompileIntoA(args[1]); EmitAsm("LD_B_A"); // keys
    CompileIntoA(args[2]); EmitAsm("LD_C_A"); // trigger
    EmitAsm("POP_HL");

    // --- Trigger Right? (bit0) ---
    EmitAsm("LD_A_C");
    EmitAsm("AND_IMM", new AsmOperand(0x01, AddressMode.Immediate));
    EmitAsm("JP_Z", pr_check_left);

    // dirMask=0x01, dasCnt=0, arrCnt=0
    EmitAsm("LD_A_IMM", new AsmOperand(0x01, AddressMode.Immediate));
    EmitAsm("LD_HL_A"); // [0]=0x01
    EmitAsm("PUSH_HL");
    EmitAsm("INC_HL"); EmitAsm("INC_HL"); EmitAsm("INC_HL"); // -> [3]
    EmitAsm("XOR_A");
    EmitAsm("LD_HL_A"); // dasCnt=0
    EmitAsm("INC_HL");
    EmitAsm("LD_HL_A")
__padrep_mask

__padrep_mask是__padrep的别名,两者进入相同的编译器实现。单个操作的选择方式、时间规则、状态布局和返回值均相同。较明确的名称有助于在调用位置看出这是通过掩码选择操作的连发处理。

从mask指定的按钮中选择一个操作进行连发。存在符合条件的新按下位时,选择最低有效位并立即返回。否则,只要当前操作仍在掩码范围内且仍被按住,就保持该操作。该操作结束后,选择剩余按住位中的最低位,但不立即产生脉冲。没有符合条件的按住位时清空运行状态。

语法

c
u8 __padrep_mask(u8* state, u8 keys, u8 trigger, u8 mask);

此调用格式中的参数名用于解释编译器内置功能,不是需要另行添加的 C 声明。

参数

state

指向连续5字节可写RAM的指针,例如u8 state[5]。布局为[0]当前按钮位、[1]初始等待时间、[2]连发间隔、[3]初始等待经过计数、[4]连发经过计数。每个独立的连发处理都应使用单独的状态,并在使用前初始化。不支持空指针、少于5字节的区域,或在方向专用处理与通用处理之间共用状态。

keys

使用GB BTN_*布局的当前完整按住掩码。传入__readpad的结果、__readpadex的低字节或input_current的结果。连发函数本身不读取硬件。

trigger

本次更新中新按下的位,布局与keys相同,通常为keys & ~previous_keys或__readpadex的高字节。只能包含同时存在于keys中的位。每次都传入按住掩码会使每次调用都被误认为初次按下,重新开始等待。

mask

用|组合允许参与连发的GB按钮位。0会禁用所有操作并清空运行状态。多个位可提供多个候选,但每次调用只返回选中的一个位,不返回组合脉冲掩码。

返回值

没有操作脉冲时返回0,否则返回1, 2, 4, 8, 16, 32, 64, 128之一。非零结果是按钮位,不是归一化布尔值,也不是连发次数。

使用条件与限制

通用掩码为 BTN_RIGHT=0x01、BTN_LEFT=0x02、BTN_UP=0x04、BTN_DOWN=0x08、BTN_A=0x10、BTN_B=0x20、BTN_SELECT=0x40、BTN_START=0x80。位为 1 表示按住。GB 和 FC 的 input_* 均使用此格式;FC 原始手柄函数的位序不同。

初始等待时间,范围0至255,单位为连发函数的调用次数。正值N表示初次按下脉冲之后恰好经过N次调用产生首次长按连发。为0时跳过初始等待相等分支,由连发间隔决定首次长按脉冲的时间。单位不是毫秒。

后续连发间隔,范围0至255次调用。正值N表示首次连发之后,每N次有效按住更新产生一次脉冲。0表示初始等待结束后每次有效更新都产生脉冲,因此在该阶段0和1均为每次更新一次。等待时间和间隔均为0时,每次按住更新都会产生脉冲。

每个预定输入周期调用一次。每次调用都会修改状态,不会等待VBlank。没有新按下位而接管已按住的按钮时,该次调用返回0并开始新的等待。这组函数每个状态只处理一个当前操作;input_repeat则为八个按钮分别维护独立计时器。

使用示例

c
result=__padrep_mask(alias_state,keys,trigger,48);
    if (result!=0) alias_pulses++;

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

预期结果

此程序把预设按钮状态送入四个独立状态,演示连发具体发生在哪次调用。等待时间3、间隔2时,编号0至7的八次调用会在0、3、5、7产生脉冲。因此RIGHT PULSES、DOWN PULSES、ACTION PULSES和ALIAS PULSES均显示004。通用处理同时接收A和B,选择A(16)。程序还检查初始化、重置、双方向优先级、松开、接管已按住的方向、时间值0和表达式参数。CHECKS应为057,FAILED CHECKS应为000。这是使用固定输入的状态处理示例,不会等待你实际按下按钮。

__padrep_mask: DMG
DMG
__padrep_mask: CGB
CGB

完整示例程序

构建命令

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqgb\kitaqgb.exe .\kitaq-docs\samples\api-examples\gb\pad_repeat_state.c -I .\kitaq-docs\samples -I .\kitaqgb\lib -o .\out\pad_repeat_state.gb --profile=dev --rst-disable --stack-bank=fixed --cgb=cgb --no-cache --no-disasm
实现与源码

kitaqgb/kitaqgb/CodeGenerator.cs:11249

csharp
{
    if (args.Length != 4) Error(expr, "__padrep requires 4 arguments (state*, keys, trigger, mask)");

    AsmOperand pm_done        = MakeUniqueLabel("padrepM_done");
    AsmOperand pm_ret0        = MakeUniqueLabel("padrepM_ret0");
    AsmOperand pm_no_trig     = MakeUniqueLabel("padrepM_notrig");
    AsmOperand pm_reset_all   = MakeUniqueLabel("padrepM_reset");
    AsmOperand pm_dir0        = MakeUniqueLabel("padrepM_dir0");
    AsmOperand pm_have_dir    = MakeUniqueLabel("padrepM_havedir");
    AsmOperand pm_first_rep   = MakeUniqueLabel("padrepM_firstrep");
    AsmOperand pm_ret_dir     = MakeUniqueLabel("padrepM_retdir");
    AsmOperand pm_skip_dasinc = MakeUniqueLabel("padrepM_skipdasinc");
    AsmOperand pm_skip_arrinc = MakeUniqueLabel("padrepM_skiparrinc");

    // Evaluate args safely:
    // HL = state*, B = keys, C = trigger, E = mask
    CompileIntoHL(args[0]);
    EmitAsm("PUSH_HL");
    CompileIntoA(args[1]); EmitAsm("LD_B_A"); // keys
    CompileIntoA(args[2]); EmitAsm("LD_C_A"); // trigger
    CompileIntoA(args[3]); EmitAsm("LD_E_A"); // mask
    EmitAsm("POP_HL");

    // trigMasked = trigger & mask -> D
    EmitAsm("LD_A_C");
    EmitAsm("AND_E");
    EmitAsm("LD_D_A");
    EmitAsm("OR_A");
    EmitAsm("JP_Z", pm_no_trig);

    // select = lowbit(trigMasked) = x & -x
    EmitAsm("LD_A_D");
    EmitAsm("CPL");
    EmitAsm("INC_A");
    EmitAsm("AND_D");
    EmitAsm("LD_C_A"); // save selected in C

    // dirMask=selected, dasCnt=0, arrCnt=0
    EmitAsm("LD_A_C");
    EmitAsm("LD_HL_A"); // [0]=selected
    EmitAsm("PUSH_HL");
    EmitAsm("INC_HL"); Emi
__padrep_reset

保留已配置的等待时间和间隔,取消当前操作并清空两个经过时间计数器。可在关闭菜单或切换输入目标时使用,避免把之前长按的连发进度带入下一次交互。

语法

c
void __padrep_reset(u8* state);

此调用格式中的参数名用于解释编译器内置功能,不是需要另行添加的 C 声明。

参数

state

指向连续5字节可写RAM的指针,例如u8 state[5]。布局为[0]当前按钮位、[1]初始等待时间、[2]连发间隔、[3]初始等待经过计数、[4]连发经过计数。每个独立的连发处理都应使用单独的状态,并在使用前初始化。不支持空指针、少于5字节的区域,或在方向专用处理与通用处理之间共用状态。

返回值

无(void)。

使用示例

c
__padrep_reset(lr);
expect(lr[0],0); expect(lr[1],3); expect(lr[2],2);
expect(lr[3],0); expect(lr[4],0);

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

预期结果

此程序把预设按钮状态送入四个独立状态,演示连发具体发生在哪次调用。等待时间3、间隔2时,编号0至7的八次调用会在0、3、5、7产生脉冲。因此RIGHT PULSES、DOWN PULSES、ACTION PULSES和ALIAS PULSES均显示004。通用处理同时接收A和B,选择A(16)。程序还检查初始化、重置、双方向优先级、松开、接管已按住的方向、时间值0和表达式参数。CHECKS应为057,FAILED CHECKS应为000。这是使用固定输入的状态处理示例,不会等待你实际按下按钮。

__padrep_reset: DMG
DMG
__padrep_reset: CGB
CGB

完整示例程序

构建命令

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqgb\kitaqgb.exe .\kitaq-docs\samples\api-examples\gb\pad_repeat_state.c -I .\kitaq-docs\samples -I .\kitaqgb\lib -o .\out\pad_repeat_state.gb --profile=dev --rst-disable --stack-bank=fixed --cgb=cgb --no-cache --no-disasm
实现与源码

kitaqgb/kitaqgb/CodeGenerator.cs:10840

csharp
{
    if (args.Length != 1) Error(expr, "__padrep_reset requires 1 argument (state*)");

    CompileIntoHL(args[0]);

    EmitAsm("XOR_A");
    EmitAsm("LD_HL_A"); // [0]=0 (dirMask)

    // [3]=0, [4]=0
    EmitAsm("PUSH_HL");
    EmitAsm("INC_HL"); EmitAsm("INC_HL"); EmitAsm("INC_HL"); // -> [3]
    EmitAsm("LD_HL_A");
    EmitAsm("INC_HL"); // -> [4]
    EmitAsm("LD_HL_A");
    EmitAsm("POP_HL");
    return;
}

// __padrep_lr(state*, keys, trigger) returns Right (1), Left (2), or zero.
// New triggers take priority, with Right selected first when both trigger bits are set.
// Without a new trigger, simultaneous Left/Right holds suppress movement without resetting counters.
// A single held direction is adopted without immediate movement when no direction is active.
// Subsequent updates use the five-byte state's initial delay and repeat interval; ARR=0 repeats each update.
if (funcName == "__padrep_lr")
{
    if (args.Length != 3) Error(expr, "__padrep_lr requires 3 arguments (state*, keys, trigger)");

    AsmOperand pr_done        = MakeUniqueLabel("padrep_done");
    AsmOperand pr_ret0        = MakeUniqueLabel("padrep_ret0");
    AsmOperand pr_check_left  = MakeUniqueLabel("padrep_chkL");
    AsmOperand pr_no_trig     = MakeUniqueLabel("padrep_notrig");
    AsmOperand pr_reset_all   = MakeUniqueLabel("padrep_reset");
    AsmOperand pr_dir0        = MakeUniqueLabel("padrep_dir0");
    AsmOperand pr_have_dir    = MakeUniqueLabel("padrep_havedir");
    AsmOperand pr_first_rep   = MakeUniqueLabel("padrep_firstrep");
    AsmOperand pr_ret_dir     = MakeUniqueLabel("padrep
__readpad

从 JOYP 读取方向键和操作按钮,合并为通用 8 位按钮掩码。依次选择各组并读取两次以等待信号稳定,最后将组选择位恢复为 0x30。

语法

c
u8 __readpad();

此调用格式中的参数名用于解释编译器内置功能,不是需要另行添加的 C 声明。

返回值

通用 BTN_* 格式的 8 位掩码。多个按钮可以同时置位;0 表示没有按钮。

使用条件与限制

通用掩码为 BTN_RIGHT=0x01、BTN_LEFT=0x02、BTN_UP=0x04、BTN_DOWN=0x08、BTN_A=0x10、BTN_B=0x20、BTN_SELECT=0x40、BTN_START=0x80。位为 1 表示按住。GB 和 FC 的 input_* 均使用此格式;FC 原始手柄函数的位序不同。

此调用重新读取硬件状态,不更新 input_* 或 nes_pad1_* 的缓存,也不生成按下、松开或重复事件。需要检测变化时,请保存读取结果,并与自己保存的上次掩码比较。

使用示例

c
all_keys=__readpad(); // A + Right: 0x11, decimal 17.

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

预期结果

此程序用于比较三种按钮掩码格式,以及将当前状态和新按下状态打包的返回值。同时按住A和右。ALL KEYS为17,DIRECTIONS和BUTTONS LOW各为1。前次掩码为0时,EX CURRENT和EX NEW PRESS均为17。将当前掩码传给下一次读取后,HELD PRESS为0。FAILED CHECKS应为0。采集到的数值会保留在屏幕上,松开按钮也不会清除。

__readpad: DMG
DMG
__readpad: CGB
CGB

完整示例程序

构建命令

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqgb\kitaqgb.exe .\kitaq-docs\samples\api-examples\gb\input_raw.c -I .\kitaqgb\lib -I .\kitaq-docs\samples -o .\out\input_raw.gb --profile=dev --rst-disable --stack-bank=fixed --cgb=cgb --no-disasm --no-cache
实现与源码

kitaqgb/kitaqgb/CodeGenerator.cs:10669

csharp
{
    if (args.Length != 0) Error(expr, "__readpad requires 0 arguments");

    AsmOperand p1 = new AsmOperand(0x00, AddressMode.HighMem); // P1 / JOYP

    // Directions (P1=0x20)
    EmitAsm("LD_A_IMM", new AsmOperand(0x20, AddressMode.Immediate));
    EmitAsm("LDH_MEM_A", p1);
    EmitAsm("LDH_A_MEM", p1); // dummy reads for settle
    EmitAsm("LDH_A_MEM", p1);
    EmitAsm("CPL");
    EmitAsm("AND_IMM", new AsmOperand(0x0F, AddressMode.Immediate));
    EmitAsm("LD_C_A"); // dpad in C

    // Buttons (P1=0x10)
    EmitAsm("LD_A_IMM", new AsmOperand(0x10, AddressMode.Immediate));
    EmitAsm("LDH_MEM_A", p1);
    EmitAsm("LDH_A_MEM", p1);
    EmitAsm("LDH_A_MEM", p1);
    EmitAsm("CPL");
    EmitAsm("AND_IMM", new AsmOperand(0x0F, AddressMode.Immediate));
    // shift into upper nibble (<<4)
    EmitAsm("RLCA"); EmitAsm("RLCA"); EmitAsm("RLCA"); EmitAsm("RLCA");
    EmitAsm("OR_C");

    // Restore P1 selection (0x30) without losing return value
    EmitAsm("LD_C_A");
    EmitAsm("LD_A_IMM", new AsmOperand(0x30, AddressMode.Immediate));
    EmitAsm("LDH_MEM_A", p1);
    EmitAsm("LD_A_C");
    return;
}

// 2.17 __readpaddir() -> u8 in A (bits0..3 only)
// Returns dpad nibble (Right,Left,Up,Down) with 1 when pressed.
// Treated as a compiler intrinsic (no C definition required).
if (funcName == "__readpaddir")
{
    if (args.Length != 0) Error(expr, "__readpaddir requires 0 arguments");

    AsmOperand p1 = new AsmOperand(0x00, AddressMode.HighMem); // P1 / JOYP

    EmitAsm("LD_A_IMM", new AsmOperand(0x20, AddressMode.Immediate));
    EmitAsm("LDH_MEM_A", p1);
    EmitAsm
__readpadbtn

只读取 JOYP 操作按钮组,返回低四位:A=1、B=2、Select=4、Start=8。它们不同于占高四位的通用 BTN_A~BTN_START。读取后将 JOYP 选择恢复为 0x30。

语法

c
u8 __readpadbtn();

此调用格式中的参数名用于解释编译器内置功能,不是需要另行添加的 C 声明。

返回值

按上述位序返回 0~15 的掩码。可能同时置多个位;应使用 result & bit 检查某个按钮,而不是判断整个结果是否等于 1。

使用条件与限制

此调用重新读取硬件状态,不更新 input_* 或 nes_pad1_* 的缓存,也不生成按下、松开或重复事件。需要检测变化时,请保存读取结果,并与自己保存的上次掩码比较。

使用示例

c
buttons=__readpadbtn(); // A: 1, not BTN_A (16).

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

预期结果

此程序用于比较三种按钮掩码格式,以及将当前状态和新按下状态打包的返回值。同时按住A和右。ALL KEYS为17,DIRECTIONS和BUTTONS LOW各为1。前次掩码为0时,EX CURRENT和EX NEW PRESS均为17。将当前掩码传给下一次读取后,HELD PRESS为0。FAILED CHECKS应为0。采集到的数值会保留在屏幕上,松开按钮也不会清除。

__readpadbtn: DMG
DMG
__readpadbtn: CGB
CGB

完整示例程序

构建命令

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqgb\kitaqgb.exe .\kitaq-docs\samples\api-examples\gb\input_raw.c -I .\kitaqgb\lib -I .\kitaq-docs\samples -o .\out\input_raw.gb --profile=dev --rst-disable --stack-bank=fixed --cgb=cgb --no-disasm --no-cache
实现与源码

kitaqgb/kitaqgb/CodeGenerator.cs:10729

csharp
{
    if (args.Length != 0) Error(expr, "__readpadbtn requires 0 arguments");

    AsmOperand p1 = new AsmOperand(0x00, AddressMode.HighMem); // P1 / JOYP

    EmitAsm("LD_A_IMM", new AsmOperand(0x10, AddressMode.Immediate));
    EmitAsm("LDH_MEM_A", p1);
    EmitAsm("LDH_A_MEM", p1);
    EmitAsm("LDH_A_MEM", p1);
    EmitAsm("CPL");
    EmitAsm("AND_IMM", new AsmOperand(0x0F, AddressMode.Immediate));

    EmitAsm("LD_C_A");
    EmitAsm("LD_A_IMM", new AsmOperand(0x30, AddressMode.Immediate));
    EmitAsm("LDH_MEM_A", p1);
    EmitAsm("LD_A_C");
    return;
}

// 2.19 __readpadex(prev_u8) -> u16 in HL
// Returns: L = keys (same encoding as __readpad), H = trigger (newly pressed edge: keys & ~prev).
// This mirrors the common pattern:
// prev = keys; keys = __readpad(); trigger = keys & ~prev;
// Treated as a compiler intrinsic (no C definition required).
if (funcName == "__readpadex")
{
    if (args.Length != 1) Error(expr, "__readpadex requires 1 argument (prev_keys)");

    // prev -> B
    CompileIntoA(args[0]);
    EmitAsm("LD_B_A");

    AsmOperand p1 = new AsmOperand(0x00, AddressMode.HighMem); // P1 / JOYP

    // Directions (P1=0x20)
    EmitAsm("LD_A_IMM", new AsmOperand(0x20, AddressMode.Immediate));
    EmitAsm("LDH_MEM_A", p1);
    EmitAsm("LDH_A_MEM", p1);
    EmitAsm("LDH_A_MEM", p1);
    EmitAsm("CPL");
    EmitAsm("AND_IMM", new AsmOperand(0x0F, AddressMode.Immediate));
    EmitAsm("LD_C_A"); // dpad in C

    // Buttons (P1=0x10)
    EmitAsm("LD_A_IMM", new AsmOperand(0x10, AddressMode.Immediate));
    EmitAsm("LDH_MEM_A", p1);
    EmitAsm("LDH_A_MEM", p1)
__readpaddir

只读取 JOYP 的方向键组。结果的位 0~3 为右=1、左=2、上=4、下=8,位 4~7 为 0。不包含操作按钮,读取后将 JOYP 选择恢复为 0x30。

语法

c
u8 __readpaddir();

此调用格式中的参数名用于解释编译器内置功能,不是需要另行添加的 C 声明。

返回值

按上述位序返回 0~15 的掩码。可能同时置多个位;应使用 result & bit 检查某个按钮,而不是判断整个结果是否等于 1。

使用条件与限制

此调用重新读取硬件状态,不更新 input_* 或 nes_pad1_* 的缓存,也不生成按下、松开或重复事件。需要检测变化时,请保存读取结果,并与自己保存的上次掩码比较。

使用示例

c
directions=__readpaddir(); // Right: 1. The held A button is not included.

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

预期结果

此程序用于比较三种按钮掩码格式,以及将当前状态和新按下状态打包的返回值。同时按住A和右。ALL KEYS为17,DIRECTIONS和BUTTONS LOW各为1。前次掩码为0时,EX CURRENT和EX NEW PRESS均为17。将当前掩码传给下一次读取后,HELD PRESS为0。FAILED CHECKS应为0。采集到的数值会保留在屏幕上,松开按钮也不会清除。

__readpaddir: DMG
DMG
__readpaddir: CGB
CGB

完整示例程序

构建命令

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqgb\kitaqgb.exe .\kitaq-docs\samples\api-examples\gb\input_raw.c -I .\kitaqgb\lib -I .\kitaq-docs\samples -o .\out\input_raw.gb --profile=dev --rst-disable --stack-bank=fixed --cgb=cgb --no-disasm --no-cache
实现与源码

kitaqgb/kitaqgb/CodeGenerator.cs:10706

csharp
{
    if (args.Length != 0) Error(expr, "__readpaddir requires 0 arguments");

    AsmOperand p1 = new AsmOperand(0x00, AddressMode.HighMem); // P1 / JOYP

    EmitAsm("LD_A_IMM", new AsmOperand(0x20, AddressMode.Immediate));
    EmitAsm("LDH_MEM_A", p1);
    EmitAsm("LDH_A_MEM", p1);
    EmitAsm("LDH_A_MEM", p1);
    EmitAsm("CPL");
    EmitAsm("AND_IMM", new AsmOperand(0x0F, AddressMode.Immediate));

    EmitAsm("LD_C_A");
    EmitAsm("LD_A_IMM", new AsmOperand(0x30, AddressMode.Immediate));
    EmitAsm("LDH_MEM_A", p1);
    EmitAsm("LD_A_C");
    return;
}

// 2.18 __readpadbtn() -> u8 in A (bits0..3 only)
// Returns buttons nibble (A,B,Select,Start) with 1 when pressed.
// Treated as a compiler intrinsic (no C definition required).
if (funcName == "__readpadbtn")
{
    if (args.Length != 0) Error(expr, "__readpadbtn requires 0 arguments");

    AsmOperand p1 = new AsmOperand(0x00, AddressMode.HighMem); // P1 / JOYP

    EmitAsm("LD_A_IMM", new AsmOperand(0x10, AddressMode.Immediate));
    EmitAsm("LDH_MEM_A", p1);
    EmitAsm("LDH_A_MEM", p1);
    EmitAsm("LDH_A_MEM", p1);
    EmitAsm("CPL");
    EmitAsm("AND_IMM", new AsmOperand(0x0F, AddressMode.Immediate));

    EmitAsm("LD_C_A");
    EmitAsm("LD_A_IMM", new AsmOperand(0x30, AddressMode.Immediate));
    EmitAsm("LDH_MEM_A", p1);
    EmitAsm("LD_A_C");
    return;
}

// 2.19 __readpadex(prev_u8) -> u16 in HL
// Returns: L = keys (same encoding as __readpad), H = trigger (newly pressed edge: keys & ~prev).
// This mirrors the common pattern:
// prev = keys; keys = __readpad(); trigger = keys & ~prev;
// Treated as a c
__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。

返回值

每项按 2*trunc(a*b/256) 计算,再将各项相加;返回低 16 位。

使用条件与限制

GB 的乘积除以 256;有符号结果向零截断。Q1.7 变体将该截断结果乘以二,可能与精确除以 128 的结果相差一。各项分别舍入;最终加法按 16 位回绕。

使用示例

c
// Compare literal coefficients with values read from RAM.
result[20]=__sdot2_q1_7(-513,769,-64,127);
v0=-513;v1=769;v2=-64;v3=127;
result[21]=__sdot2_q1_7(v0,v1,v2,v3);
// Reverse coefficient signs and verify negative products.
result[22]=__sdot2_q1_7(-513,769,64,-127);
v0=-513;v1=769;v2=64;v3=-127;
result[23]=__sdot2_q1_7(v0,v1,v2,v3);

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

预期结果

比较字面量输入与 RAM 中存储的输入。代表性结果以无符号 16 位值显示;负数采用二进制补码位模式。GB 会对定点乘积进行缩放,FC 则返回整数乘积,因此相同输入不一定得到相同结果。

这段代码保存以下 16 位值:result[20]=1018, result[21]=1018, result[22]=64518, result[23]=64518。使用字面量输入和对应 RAM 输入的计算结果一致。屏幕显示代表性数值;result 数组保存全部结果。

__sdot2_q1_7: DMG
DMG
__sdot2_q1_7: CGB
CGB

完整示例程序

构建命令

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqgb\kitaqgb.exe .\kitaq-docs\samples\api-examples\gb\batch3_math.c -I .\kitaqgb\lib -I .\kitaq-docs\samples -o .\out\batch3_math.gb --no-cache --no-disasm --profile=dev --rst-disable --stack-bank=fixed --cgb=cgb --cart=mbc5 --romsize=64k --ramsize=none
实现与源码

kitaqgb/kitaqgb/CodeGenerator.cs:11732

kitaqgb/kitaqgb/CodeGenerator.cs:11732

csharp
if (funcName == "__sdot2_q1_7")
{
    if (args.Length != 4) Error(expr, "__sdot2_q1_7 requires 4 arguments");
    LastCallReturnsHL = true;

    // x*ax
    if (!TryEmitDot2Pow2Term(args[0], args[2], signed: true, q1_7: true, out _))
    {
        CompileIntoHL(args[0]);
        EmitAsm("PUSH_HL");
        CompileIntoA(args[2]);
        EmitAsm("LD_C_A");
        EmitAsm("POP_HL");
        EmitSMul16x8Shr8_HL("__sdot2_q1_7_x");
        EmitAsm("ADD_HL_HL");
    }
    EmitAsm("PUSH_HL");

    // y*ay
    if (!TryEmitDot2Pow2Term(args[1], args[3], signed: true, q1_7: true, out _))
    {
        CompileIntoHL(args[1]);
        EmitAsm("PUSH_HL");
        CompileIntoA(args[3]);
        EmitAsm("LD_C_A");
        EmitAsm("POP_HL");
        EmitSMul16x8Shr8_HL("__sdot2_q1_7_y");
        EmitAsm("ADD_HL_HL");
    }
    EmitAsm("POP_DE");
    EmitAsm("ADD_HL_DE");
    return;
}
__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。

返回值

每项按 trunc(a*b/256) 计算,再将各项相加;返回低 16 位。

使用条件与限制

GB 的乘积除以 256;有符号结果向零截断。Q1.7 变体将该截断结果乘以二,可能与精确除以 128 的结果相差一。各项分别舍入;最终加法按 16 位回绕。

使用示例

c
// Compare literal coefficients with values read from RAM.
result[30]=__sdot2_q8_8(-513,769,-64,127);
v0=-513;v1=769;v2=-64;v3=127;
result[31]=__sdot2_q8_8(v0,v1,v2,v3);
// Reverse coefficient signs and verify negative products.
result[32]=__sdot2_q8_8(-513,769,64,-127);
v0=-513;v1=769;v2=64;v3=-127;
result[33]=__sdot2_q8_8(v0,v1,v2,v3);

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

预期结果

比较字面量输入与 RAM 中存储的输入。代表性结果以无符号 16 位值显示;负数采用二进制补码位模式。GB 会对定点乘积进行缩放,FC 则返回整数乘积,因此相同输入不一定得到相同结果。

这段代码保存以下 16 位值:result[30]=509, result[31]=509, result[32]=65027, result[33]=65027。使用字面量输入和对应 RAM 输入的计算结果一致。屏幕显示代表性数值;result 数组保存全部结果。

__sdot2_q8_8: DMG
DMG
__sdot2_q8_8: CGB
CGB

完整示例程序

构建命令

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqgb\kitaqgb.exe .\kitaq-docs\samples\api-examples\gb\batch3_math.c -I .\kitaqgb\lib -I .\kitaq-docs\samples -o .\out\batch3_math.gb --no-cache --no-disasm --profile=dev --rst-disable --stack-bank=fixed --cgb=cgb --cart=mbc5 --romsize=64k --ramsize=none
实现与源码

kitaqgb/kitaqgb/CodeGenerator.cs:11654

kitaqgb/kitaqgb/CodeGenerator.cs:11654

csharp
if (funcName == "__sdot2_q8_8")
{
    if (args.Length != 4) Error(expr, "__sdot2_q8_8 requires 4 arguments");
    LastCallReturnsHL = true;

    // Fast paths when coefficients are compile-time constants.
    // Special-case: 0 / power-of-two coefficients (s8 two's complement) => arithmetic shift instead of mul.
    if (TryGetS8Const(args[2], out int sax0) && (sax0 == 0) && TryGetS8Const(args[3], out int say0) && (say0 == 0))
    {
        CompileDiscard(args[0]);
        CompileDiscard(args[1]);
        EmitAsm("XOR_A");
        EmitAsm("LD_H_A");
        EmitAsm("LD_L_A");
        return;
    }
    if (TryGetS8Const(args[2], out int saxOnly) && (saxOnly == 0))
    {
        CompileDiscard(args[0]);
        // only y*ay
        if (!TryEmitDot2Pow2Term(args[1], args[3], signed: true, q1_7: false, out _))
        {
            CompileIntoHL(args[1]);
            EmitAsm("PUSH_HL");
            CompileIntoA(args[3]);
            EmitAsm("LD_C_A");
            EmitAsm("POP_HL");
            EmitSMul16x8Shr8_HL("__sdot2_y");
        }
        return;
    }
    if (TryGetS8Const(args[3], out int sayOnly) && (sayOnly == 0))
    {
        // only x*ax
        if (!TryEmitDot2Pow2Term(args[0], args[2], signed: true, q1_7: false, out _))
        {
            CompileIntoHL(args[0]);
            EmitAsm("PUSH_HL");
            CompileIntoA(args[2]);
            EmitAsm("LD_C_A");
            EmitAsm("POP_HL");
            EmitSMul16x8Shr8_HL("__sdot2_x");
        }
        // Preserve the scaled result while evaluating the zero-product input.
        EmitAsm("PUSH_HL");
        CompileDiscard(args[1]);
        EmitAsm("POP_HL");
        return;
    }

    // x*ax
    if (!TryEmitDot2Pow2Term(args[0], args[2], signed: true, q1_7: false, out _))
    {
        CompileIntoHL(args[0]);
        EmitAsm("PUSH_HL");
        CompileIntoA(args[2]);
        EmitAsm("LD_C_A");
        EmitAsm("POP_HL");
        EmitSMul16x8Shr8_HL("__sdot2_x");
    }
    EmitAsm("PUSH_HL");

    // y*ay
    if (!TryEmitDot2Pow2Term(args[1], args[3], signed: true, q1_7: false, out _))
    {
        CompileIntoHL(args[1]);
        EmitAsm("PUSH_HL");
        CompileIntoA(args[3]);
        EmitAsm("LD_C_A");
        EmitAsm("POP_HL");
        EmitSMul16x8Shr8_HL("__sdot2_y");
    }
    EmitAsm("POP_DE");
    EmitAsm("ADD_HL_DE");
    return;
}
__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。

返回值

每项按 2*trunc(a*b/256) 计算,再将各项相加;返回低 16 位。

使用条件与限制

GB 的乘积除以 256;有符号结果向零截断。Q1.7 变体将该截断结果乘以二,可能与精确除以 128 的结果相差一。各项分别舍入;最终加法按 16 位回绕。

使用示例

c
// Compare literal coefficients with values read from RAM.
result[16]=__sdot3_q1_7(-513,769,-1025,-64,127,-3);
v0=-513;v1=769;v2=-1025;v3=-64;v4=127;v5=-3;
result[17]=__sdot3_q1_7(v0,v1,v2,v3,v4,v5);
// Reverse coefficient signs and verify negative products.
result[18]=__sdot3_q1_7(-513,769,-1025,64,-127,3);
v0=-513;v1=769;v2=-1025;v3=64;v4=-127;v5=3;
result[19]=__sdot3_q1_7(v0,v1,v2,v3,v4,v5);

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

预期结果

比较字面量输入与 RAM 中存储的输入。代表性结果以无符号 16 位值显示;负数采用二进制补码位模式。GB 会对定点乘积进行缩放,FC 则返回整数乘积,因此相同输入不一定得到相同结果。

这段代码保存以下 16 位值:result[16]=1042, result[17]=1042, result[18]=64494, result[19]=64494。使用字面量输入和对应 RAM 输入的计算结果一致。屏幕显示代表性数值;result 数组保存全部结果。

__sdot3_q1_7: DMG
DMG
__sdot3_q1_7: CGB
CGB

完整示例程序

构建命令

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqgb\kitaqgb.exe .\kitaq-docs\samples\api-examples\gb\batch3_math.c -I .\kitaqgb\lib -I .\kitaq-docs\samples -o .\out\batch3_math.gb --no-cache --no-disasm --profile=dev --rst-disable --stack-bank=fixed --cgb=cgb --cart=mbc5 --romsize=64k --ramsize=none
实现与源码

kitaqgb/kitaqgb/CodeGenerator.cs:11790

kitaqgb/kitaqgb/CodeGenerator.cs:11790

csharp
if (funcName == "__sdot3_q1_7")
            {
                if (args.Length != 6) Error(expr, "__sdot3_q1_7 requires 6 arguments");
                LastCallReturnsHL = true;

                // x*ax
                EmitDotTermToHL(args[0], args[3], signed: true, q1_7: true, labelPrefix: "__sdot3_q1_7_x");
                EmitAsm("PUSH_HL");

                // y*ay
                EmitDotTermToHL(args[1], args[4], signed: true, q1_7: true, labelPrefix: "__sdot3_q1_7_y");
                EmitAsm("POP_DE");
                EmitAsm("ADD_HL_DE");
                EmitAsm("PUSH_HL");

                // z*az
                EmitDotTermToHL(args[2], args[5], signed: true, q1_7: true, labelPrefix: "__sdot3_q1_7_z");
                EmitAsm("POP_DE");
                EmitAsm("ADD_HL_DE");
                return;
            }
__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。

返回值

每项按 trunc(a*b/256) 计算,再将各项相加;返回低 16 位。

使用条件与限制

GB 的乘积除以 256;有符号结果向零截断。Q1.7 变体将该截断结果乘以二,可能与精确除以 128 的结果相差一。各项分别舍入;最终加法按 16 位回绕。

使用示例

c
// Compare literal coefficients with values read from RAM.
result[8]=__sdot3_q8_8(-513,769,-1025,-64,127,-3);
v0=-513;v1=769;v2=-1025;v3=-64;v4=127;v5=-3;
result[9]=__sdot3_q8_8(v0,v1,v2,v3,v4,v5);
// Reverse coefficient signs and verify negative products.
result[10]=__sdot3_q8_8(-513,769,-1025,64,-127,3);
v0=-513;v1=769;v2=-1025;v3=64;v4=-127;v5=3;
result[11]=__sdot3_q8_8(v0,v1,v2,v3,v4,v5);

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

预期结果

比较字面量输入与 RAM 中存储的输入。代表性结果以无符号 16 位值显示;负数采用二进制补码位模式。GB 会对定点乘积进行缩放,FC 则返回整数乘积,因此相同输入不一定得到相同结果。

这段代码保存以下 16 位值:result[8]=521, result[9]=521, result[10]=65015, result[11]=65015。使用字面量输入和对应 RAM 输入的计算结果一致。屏幕显示代表性数值;result 数组保存全部结果。

__sdot3_q8_8: DMG
DMG
__sdot3_q8_8: CGB
CGB

完整示例程序

构建命令

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqgb\kitaqgb.exe .\kitaq-docs\samples\api-examples\gb\batch3_math.c -I .\kitaqgb\lib -I .\kitaq-docs\samples -o .\out\batch3_math.gb --no-cache --no-disasm --profile=dev --rst-disable --stack-bank=fixed --cgb=cgb --cart=mbc5 --romsize=64k --ramsize=none
实现与源码

kitaqgb/kitaqgb/CodeGenerator.cs:11767

kitaqgb/kitaqgb/CodeGenerator.cs:11767

csharp
if (funcName == "__sdot3_q8_8")
            {
                if (args.Length != 6) Error(expr, "__sdot3_q8_8 requires 6 arguments");
                LastCallReturnsHL = true;

                // x*ax
                EmitDotTermToHL(args[0], args[3], signed: true, q1_7: false, labelPrefix: "__sdot3_x");
                EmitAsm("PUSH_HL");

                // y*ay
                EmitDotTermToHL(args[1], args[4], signed: true, q1_7: false, labelPrefix: "__sdot3_y");
                EmitAsm("POP_DE");
                EmitAsm("ADD_HL_DE");
                EmitAsm("PUSH_HL");

                // z*az
                EmitDotTermToHL(args[2], args[5], signed: true, q1_7: false, labelPrefix: "__sdot3_z");
                EmitAsm("POP_DE");
                EmitAsm("ADD_HL_DE");
                return;
            }
__settile_fast

向图块地图的一个单元写入图块编号。这是在选择已有图块图像,不会修改像素数据。

目标地址始终为 0x9800 + 32*y + x,不随 LCDC 的背景或窗口地图选择而改变。

不等待 VRAM 可写。只能在 LCD 关闭时,或由调用方确保整个传输都处于可写时间段时使用。

语法

c
__settile_fast(x, y, tile);

此调用格式中的参数名用于解释编译器内置功能,不是需要另行添加的 C 声明。

参数

x, y

以 8×8 图块为单位的坐标,各为 0~31。整个写入范围必须位于 32×32 地图内;不会自动裁剪坐标。

tile

0~255 的图块编号。LCDC 位 4 决定编号如何寻址图块图像数据;这不是 VRAM 字节地址。

返回值

无(void)。

使用条件与限制

图块编号写入当前选中的 VRAM 库。CGB 上应选择编号所在的库 0;此调用不会代为切换库。

示例使用 gb_common.h。m_init() 将附带 ASCII 字体加载到与字符编码相同的图块编号。m_wait(); M_LCDC=0; 在 VBlank 中关闭 LCD,以便进行无需等待的传输。最后的 LCDC 值选择显示地图。

使用示例

c
// __settile_fast
// Write one tile-number byte to the tile map; this selects an existing tile image, not
// new pixel data. The destination is always `0x9800 + 32*y + x`, regardless of which
// maps LCDC selects for the background and window.
// Expected: FAILED CHECKS 000, with the written tile data in the selected map.
#include "gb_tile_color_example.h"
void __settile_fast(u8 x, u8 y, u8 tile);

void main() {
    u8 failures;
    failures = 0;
    tile_color_example_begin();
    __settile_fast(2, 3, 'A');
    if (tile_example_read(0x9862, 0) != 'A') { failures++; }
    if (tile_example_read(0x9C62, 0) != 0) { failures++; }
    tile_example_finish(failures, 0);
}

预期结果

所选地图显示写入的字符或填充区域。程序读回目标图块编号,并在 CGB 上读回属性进行比较;FAILED CHECKS 000 表示比较全部通过。两张地图初始均为 0,调用前将背景/窗口地图选为 0x9C00,因此能区分固定地图与所选地图的写入。示例在 LCD 关闭时写入,不测量 LCD 开启时的时序。

在 CGB 上,示例字符和结果文字以白底蓝字显示。初始化时将调色板 0 的绘制颜色设为蓝色,不修改图块映射的属性。在 DMG 上,相同字符显示为白底黑字。

预期图像:以下图块排列从图块坐标 (x=2, y=3)、像素坐标 (16,24) 开始显示。每个字符占一个 8×8 图块。按从上到下、从左到右阅读,周围图块保持空白。

text
A
__settile_fast: DMG
DMG
__settile_fast: CGB
CGB

完整示例程序

构建命令

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqgb\kitaqgb.exe .\kitaq-docs\samples\api-examples\gb\__settile_fast.c -I .\kitaqgb\lib -I .\kitaq-docs\samples -o .\out\__settile_fast.gb --profile=dev --rst-disable --stack-bank=fixed --cgb=cgb --no-disasm
实现与源码

kitaqgb/kitaqgb/CodeGenerator.cs:9321

csharp
{
                if (args.Length != 3) Error(expr, "__settile_unsafe requires 3 arguments");

                // Evaluate args left-to-right (matches normal call semantics)
                // Preserve x,y while evaluating tile.
                CompileIntoA(args[0]);
                EmitAsm("LD_L_A");
                EmitAsm("LD_H_IMM", new AsmOperand(0, AddressMode.Immediate));
                EmitAsm("PUSH_HL"); // x

                CompileIntoA(args[1]);
                EmitAsm("LD_L_A");
                EmitAsm("LD_H_IMM", new AsmOperand(0, AddressMode.Immediate));
                EmitAsm("PUSH_HL"); // y

                CompileIntoA(args[2]);
                EmitAsm("LD_C_A"); // tile

                // Restore y -> A, x -> B
                EmitAsm("POP_HL");
                EmitAsm("LD_A_L");
                EmitAsm("LD_D_A"); // y temp

                EmitAsm("POP_HL");
                EmitAsm("LD_A_L");
                EmitAsm("LD_B_A"); // x

                EmitAsm("LD_A_D"); // y

                UsedSetTileFastCore = true;
                EmitAsm("CALL", new AsmOperand("__settile_fast_core", AddressMode.Absolute));
                return;
            }

            // 2.6b __settile_bulk(dest_u16, src_u16, count_u8)
            // v9: bulk tile-map copy (LCD ON safe with wait+DI/EI inside core).
            if (funcName == "__settile_bulk")
            {
                if (args.Length != 3) Error(expr, "__settile_bulk requires 3 arguments");

                Expr bulkCountExpr = FoldConstants(args[2]);

                // dest
__settilebg16_buf

在 RAM 图块地图缓冲区写入一个 16×16 像素单元。四个 8×8 图块编号上排为 tile,tile+1,下排为 tile+2,tile+3,超过 255 后回绕。不向 VRAM 传输。

语法

c
__settilebg16_buf(buf, x, y, tile);

此调用格式中的参数名用于解释编译器内置功能,不是需要另行添加的 C 声明。

参数

buf

每图块行 32 字节的 RAM 缓冲区。完整 32×32 地图需要 1024 字节。左上角偏移为 64*y + 2*x,四个写入位置相对此处为 0,1,32,33。

x, y

以 16×16 像素单元为单位的坐标;完整 32×32 图块缓冲区中各为 0~15。与单图块写入函数使用的 8×8 图块坐标不同。

tile

0~255 的图块编号。LCDC 位 4 决定编号如何寻址图块图像数据;这不是 VRAM 字节地址。

返回值

无(void)。

使用条件与限制

示例使用 gb_common.h。m_init() 将附带 ASCII 字体加载到与字符编码相同的图块编号。m_wait(); M_LCDC=0; 在 VBlank 中关闭 LCD,以便进行无需等待的传输。最后的 LCDC 值选择显示地图。

使用示例

c
// __settilebg16_buf
// Write a 16×16-pixel cell into a RAM tile-map buffer: the four 8×8 tile numbers are
// `tile,tile+1` on top and `tile+2,tile+3` below. Values wrap at 256. This does not
// transfer anything to VRAM.
// Expected: FAILED CHECKS 000, with the written tile data in the selected map.
#include "gb_tile_color_example.h"
u8 example_tiles[1024];
u8 example_attrs[1024];
void __memset(void* dest, u8 value, u16 len);
void __settilebg16_buf(u8* buf, u8 x, u8 y, u8 tile);
void __settilebg16cgb_buf(u8* tilebuf, u8* attrbuf, u8 x, u8 y, u8 tile, u8 attr);
void __settilebg16_flush(const u8* buf, u8 y, u8 x, u8 count);
void __settilebg16cgb_flush(const u8* tilebuf, const u8* attrbuf, u8 y, u8 x, u8 count);

void main() {
    u8 failures;
    failures = 0;
    tile_color_example_begin();
    __memset(example_tiles, 0, 1024);
    __memset(example_attrs, 0, 1024);
    __settilebg16_buf(example_tiles, 1, 1, 'A');
    // The buffer operation must not write to VRAM yet.
    if (tile_example_read(0x9C42, 0) != 0) { failures++; }
    // Transfer the prepared cell so its four characters can be seen.
    __settilebg16_flush(example_tiles, 1, 2, 2);
    if (tile_example_read(0x9C42, 0) != 'A') { failures++; }
    if (tile_example_read(0x9C43, 0) != 'B') { failures++; }
    if (tile_example_read(0x9C62, 0) != 'C') { failures++; }
    if (tile_example_read(0x9C63, 0) != 'D') { failures++; }
    if (example_tiles[66] != 'A') { failures++; }
    if (example_tiles[99] != 'D') { failures++; }
    if (tile_example_read(0x9C44, 0) != 0) { failures++; }
    tile_example_finish(failures, 1);
}

预期结果

所选地图显示写入的字符或填充区域。程序读回目标图块编号,并在 CGB 上读回属性进行比较;FAILED CHECKS 000 表示比较全部通过。两张地图初始均为 0,调用前将背景/窗口地图选为 0x9C00,因此能区分固定地图与所选地图的写入。示例在 LCD 关闭时写入,不测量 LCD 开启时的时序。

图块坐标 (2,2) 处显示上排 AB、下排 CD,即单元坐标 (1,1) 的一个 16×16 像素单元。缓冲区写入示例先确认 RAM 准备操作未改变 VRAM,再传输;传输函数示例则先逐字节设置 RAM 数据,再执行传输。

在 CGB 上,示例字符和结果文字以白底蓝字显示。初始化时将调色板 0 的绘制颜色设为蓝色,不修改图块映射的属性。在 DMG 上,相同字符显示为白底黑字。

预期图像:以下图块排列从图块坐标 (x=2, y=2)、像素坐标 (16,16) 开始显示。每个字符占一个 8×8 图块。按从上到下、从左到右阅读,周围图块保持空白。

text
AB
CD
__settilebg16_buf: DMG
DMG
__settilebg16_buf: CGB
CGB

完整示例程序

构建命令

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqgb\kitaqgb.exe .\kitaq-docs\samples\api-examples\gb\__settilebg16_buf.c -I .\kitaqgb\lib -I .\kitaq-docs\samples -o .\out\__settilebg16_buf.gb --profile=dev --rst-disable --stack-bank=fixed --cgb=cgb --no-disasm
实现与源码

kitaqgb/kitaqgb/CodeGenerator.cs:10564

csharp
{
                if (args.Length != 4) Error(expr, "__settilebg16_buf requires 4 arguments");
                EmitTile16BufferedWriteIntrinsic(args[0], args[1], args[2], args[3], incrementQuad: true, labelPrefix: "settilebg16buf");
                return;
            }

            // Update tile and attribute RAM buffers separately, repeating the attribute across the cell.
            if (funcName == "__settilebg16cgb_buf")
            {
                if (args.Length != 6) Error(expr, "__settilebg16cgb_buf requires 6 arguments");
                EmitTile16BufferedWriteIntrinsic(args[0], args[2], args[3], args[4], incrementQuad: true, labelPrefix: "settilebg16cgbbuf_tile");
                EmitTile16BufferedWriteIntrinsic(args[1], args[2], args[3], args[5], incrementQuad: false, labelPrefix: "settilebg16cgbbuf_attr");
                return;
            }

            // Flush both tile rows belonging to the requested buffered cell row.
            if (funcName == "__settilebg16_flush")
            {
                if (args.Length != 4) Error(expr, "__settilebg16_flush requires 4 arguments");
                EmitTile16FlushRowsIntrinsic(args[0], args[1], args[2], args[3], cgbAttr: false, labelPrefix: "settilebg16flush");
                return;
            }

            // Flush the tile buffer first and copy attributes only when CGB hardware is available.
            if (funcName == "__settilebg16cgb_flush")
            {
                if (args.Length != 5) Error(expr, "__settilebg16cgb_flush requires 5 arguments");
                EmitTile16FlushRowsIn
__settilebg16_flush

将 RAM 缓冲区中相邻的两行图块传输到 LCDC 所选背景地图。源偏移为 64*y + x,从该行及其下方相隔 32 字节的一行各复制 count 字节。

必要时等待 VRAM 可写。LCD 开启时的路径会屏蔽中断,并可能在结束后启用中断;不保证保留调用前的中断状态,也不保证在一次 VBlank 内完成。

语法

c
__settilebg16_flush(buf, y, x, count);

此调用格式中的参数名用于解释编译器内置功能,不是需要另行添加的 C 声明。

参数

buf

可读的图块地图缓冲区,行间距为 32 字节,完整地图占 1024 字节。双缓冲区形式接收布局相同的独立图块编号和属性数组,二者不交错存储。

y, x, count

参数顺序为 y,x,count。y 是 16 像素单元行(0~15),x 和 count 以 8 像素图块计。须满足 x+count <= 32。传输单元 (2,3) 时使用 y=3,x=4,count=2。

返回值

无(void)。

使用条件与限制

图块编号写入当前选中的 VRAM 库。CGB 上应选择编号所在的库 0;此调用不会代为切换库。

示例使用 gb_common.h。m_init() 将附带 ASCII 字体加载到与字符编码相同的图块编号。m_wait(); M_LCDC=0; 在 VBlank 中关闭 LCD,以便进行无需等待的传输。最后的 LCDC 值选择显示地图。

使用示例

c
// __settilebg16_flush
// Transfer two adjacent tile rows from the RAM buffer to the LCDC-selected BG map. The
// source offset is `64*y + x`; `count` bytes are copied from that row and from the row
// 32 bytes below it. Waits for writable VRAM when necessary. The LCD-on path uses
// interrupt masking and can enable interrupts afterward; it is not a promise to preserve
// the caller's interrupt state or to finish within one VBlank.
// Expected: FAILED CHECKS 000, with the written tile data in the selected map.
#include "gb_tile_color_example.h"
u8 example_tiles[1024];
u8 example_attrs[1024];
void __memset(void* dest, u8 value, u16 len);
void __settilebg16_buf(u8* buf, u8 x, u8 y, u8 tile);
void __settilebg16cgb_buf(u8* tilebuf, u8* attrbuf, u8 x, u8 y, u8 tile, u8 attr);
void __settilebg16_flush(const u8* buf, u8 y, u8 x, u8 count);
void __settilebg16cgb_flush(const u8* tilebuf, const u8* attrbuf, u8 y, u8 x, u8 count);

void main() {
    u8 failures;
    failures = 0;
    tile_color_example_begin();
    __memset(example_tiles, 0, 1024);
    __memset(example_attrs, 0, 1024);
    example_tiles[66]='A'; example_tiles[67]='B';
    example_tiles[98]='C'; example_tiles[99]='D';
    __settilebg16_flush(example_tiles, 1, 2, 2);
    if (tile_example_read(0x9C42, 0) != 'A') { failures++; }
    if (tile_example_read(0x9C43, 0) != 'B') { failures++; }
    if (tile_example_read(0x9C62, 0) != 'C') { failures++; }
    if (tile_example_read(0x9C63, 0) != 'D') { failures++; }
    if (example_tiles[66] != 'A') { failures++; }
    if (example_tiles[99] != 'D') { failures++; }
    if (tile_example_read(0x9C44, 0) != 0) { failures++; }
    tile_example_finish(failures, 1);
}

预期结果

所选地图显示写入的字符或填充区域。程序读回目标图块编号,并在 CGB 上读回属性进行比较;FAILED CHECKS 000 表示比较全部通过。两张地图初始均为 0,调用前将背景/窗口地图选为 0x9C00,因此能区分固定地图与所选地图的写入。示例在 LCD 关闭时写入,不测量 LCD 开启时的时序。

图块坐标 (2,2) 处显示上排 AB、下排 CD,即单元坐标 (1,1) 的一个 16×16 像素单元。缓冲区写入示例先确认 RAM 准备操作未改变 VRAM,再传输;传输函数示例则先逐字节设置 RAM 数据,再执行传输。

在 CGB 上,示例字符和结果文字以白底蓝字显示。初始化时将调色板 0 的绘制颜色设为蓝色,不修改图块映射的属性。在 DMG 上,相同字符显示为白底黑字。

预期图像:以下图块排列从图块坐标 (x=2, y=2)、像素坐标 (16,16) 开始显示。每个字符占一个 8×8 图块。按从上到下、从左到右阅读,周围图块保持空白。

text
AB
CD
__settilebg16_flush: DMG
DMG
__settilebg16_flush: CGB
CGB

完整示例程序

构建命令

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqgb\kitaqgb.exe .\kitaq-docs\samples\api-examples\gb\__settilebg16_flush.c -I .\kitaqgb\lib -I .\kitaq-docs\samples -o .\out\__settilebg16_flush.gb --profile=dev --rst-disable --stack-bank=fixed --cgb=cgb --no-disasm
实现与源码

kitaqgb/kitaqgb/CodeGenerator.cs:10581

csharp
{
                if (args.Length != 4) Error(expr, "__settilebg16_flush requires 4 arguments");
                EmitTile16FlushRowsIntrinsic(args[0], args[1], args[2], args[3], cgbAttr: false, labelPrefix: "settilebg16flush");
                return;
            }

            // Flush the tile buffer first and copy attributes only when CGB hardware is available.
            if (funcName == "__settilebg16cgb_flush")
            {
                if (args.Length != 5) Error(expr, "__settilebg16cgb_flush requires 5 arguments");
                EmitTile16FlushRowsIntrinsic(args[0], args[2], args[3], args[4], cgbAttr: false, labelPrefix: "settilebg16cgbflush_tile");
                if (IsKnownCgbRuntimeTrue())
                {
                    EmitSetVbkUnchecked(1);
                    EmitTile16FlushRowsIntrinsic(args[1], args[2], args[3], args[4], cgbAttr: true, labelPrefix: "settilebg16cgbflush_attr");
                }
                else
                {
                    AsmOperand skip = MakeUniqueLabel("settilebg16cgbflush_skip");
                    AsmOperand done = MakeUniqueLabel("settilebg16cgbflush_done");
                    EmitRuntimeCgbCheckCall();
                    EmitAsm("OR_A");
                    // Two unrolled row copies can exceed the relative-branch range.
                    EmitAsm("JP_Z", skip);
                    EmitSetVbkUnchecked(1);
                    EmitTile16FlushRowsIntrinsic(args[1], args[2], args[3], args[4], cgbAttr: true, labelPrefix: "settilebg16cgbflush_attr");
                    EmitAsm("JR", done);
__settilebg16cgb_buf

在 RAM 图块地图缓冲区写入一个 16×16 像素单元。四个 8×8 图块编号上排为 tile,tile+1,下排为 tile+2,tile+3,超过 255 后回绕。不向 VRAM 传输。

还会在独立属性缓冲区的对应四个单元写入相同的 attr。两个缓冲区均为普通 RAM,因此 DMG 也会执行此准备操作。

语法

c
__settilebg16cgb_buf(tilebuf, attrbuf, x, y, tile, attr);

此调用格式中的参数名用于解释编译器内置功能,不是需要另行添加的 C 声明。

参数

tilebuf, attrbuf

每图块行 32 字节的 RAM 缓冲区。完整 32×32 地图需要 1024 字节。左上角偏移为 64*y + 2*x,四个写入位置相对此处为 0,1,32,33。

x, y

以 16×16 像素单元为单位的坐标;完整 32×32 图块缓冲区中各为 0~15。与单图块写入函数使用的 8×8 图块坐标不同。

tile

0~255 的图块编号。LCDC 位 4 决定编号如何寻址图块图像数据;这不是 VRAM 字节地址。

attr

CGB 属性字节:位 0~2 选择调色板 0~7,位 3 选择图像数据 bank,位 5、6 分别控制水平和垂直翻转,位 7 设置背景/窗口优先级。未使用的位 4 应为 0。

返回值

无(void)。

使用条件与限制

示例使用 gb_common.h。m_init() 将附带 ASCII 字体加载到与字符编码相同的图块编号。m_wait(); M_LCDC=0; 在 VBlank 中关闭 LCD,以便进行无需等待的传输。最后的 LCDC 值选择显示地图。

使用示例

c
// __settilebg16cgb_buf
// Write a 16×16-pixel cell into a RAM tile-map buffer: the four 8×8 tile numbers are
// `tile,tile+1` on top and `tile+2,tile+3` below. Values wrap at 256. This does not
// transfer anything to VRAM. Also write the same `attr` byte to all four corresponding
// cells in the separate attribute buffer. Both buffers are ordinary RAM, so this
// preparation also runs on DMG.
// Expected: FAILED CHECKS 000, with the written tile data in the selected map.
#include "gb_tile_example.h"
u8 example_tiles[1024];
u8 example_attrs[1024];
void __memset(void* dest, u8 value, u16 len);
void __settilebg16_buf(u8* buf, u8 x, u8 y, u8 tile);
void __settilebg16cgb_buf(u8* tilebuf, u8* attrbuf, u8 x, u8 y, u8 tile, u8 attr);
void __settilebg16_flush(const u8* buf, u8 y, u8 x, u8 count);
void __settilebg16cgb_flush(const u8* tilebuf, const u8* attrbuf, u8 y, u8 x, u8 count);

void main() {
    u8 failures;
    failures = 0;
    tile_example_begin();
    __memset(example_tiles, 0, 1024);
    __memset(example_attrs, 0, 1024);
    __settilebg16cgb_buf(example_tiles, example_attrs, 1, 1, 'A', 2);
    // The buffer operation must not write to VRAM yet.
    if (tile_example_read(0x9C42, 0) != 0) { failures++; }
    // Transfer the prepared cell so its four characters can be seen.
    __settilebg16cgb_flush(example_tiles, example_attrs, 1, 2, 2);
    if (tile_example_read(0x9C42, 0) != 'A') { failures++; }
    if (tile_example_read(0x9C43, 0) != 'B') { failures++; }
    if (tile_example_read(0x9C62, 0) != 'C') { failures++; }
    if (tile_example_read(0x9C63, 0) != 'D') { failures++; }
    if (example_tiles[66] != 'A') { failures++; }
    if (example_tiles[99] != 'D') { failures++; }
    if (tile_example_read(0x9C44, 0) != 0) { failures++; }
    if (example_attrs[66] != 2) { failures++; }
    if (example_attrs[99] != 2) { failures++; }
    if (__cgb_is_cgb() && tile_example_read(0x9C42, 1) != 2) { failures++; }
    if (__cgb_is_cgb() && tile_example_read(0x9C63, 1) != 2) { failures++; }
    tile_example_finish(failures, 1);
}

预期结果

所选地图显示写入的字符或填充区域。程序读回目标图块编号,并在 CGB 上读回属性进行比较;FAILED CHECKS 000 表示比较全部通过。两张地图初始均为 0,调用前将背景/窗口地图选为 0x9C00,因此能区分固定地图与所选地图的写入。示例在 LCD 关闭时写入,不测量 LCD 开启时的时序。

图块坐标 (2,2) 处显示上排 AB、下排 CD,即单元坐标 (1,1) 的一个 16×16 像素单元。缓冲区写入示例先确认 RAM 准备操作未改变 VRAM,再传输;传输函数示例则先逐字节设置 RAM 数据,再执行传输。

CGB 上四个单元的属性均为 2,因此四个字符均为蓝色;DMG 上为黑色。

预期图像:以下图块排列从图块坐标 (x=2, y=2)、像素坐标 (16,16) 开始显示。每个字符占一个 8×8 图块。按从上到下、从左到右阅读,周围图块保持空白。

text
AB
CD
__settilebg16cgb_buf: DMG
DMG
__settilebg16cgb_buf: CGB
CGB

完整示例程序

构建命令

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqgb\kitaqgb.exe .\kitaq-docs\samples\api-examples\gb\__settilebg16cgb_buf.c -I .\kitaqgb\lib -I .\kitaq-docs\samples -o .\out\__settilebg16cgb_buf.gb --profile=dev --rst-disable --stack-bank=fixed --cgb=cgb --no-disasm
实现与源码

kitaqgb/kitaqgb/CodeGenerator.cs:10572

csharp
{
                if (args.Length != 6) Error(expr, "__settilebg16cgb_buf requires 6 arguments");
                EmitTile16BufferedWriteIntrinsic(args[0], args[2], args[3], args[4], incrementQuad: true, labelPrefix: "settilebg16cgbbuf_tile");
                EmitTile16BufferedWriteIntrinsic(args[1], args[2], args[3], args[5], incrementQuad: false, labelPrefix: "settilebg16cgbbuf_attr");
                return;
            }

            // Flush both tile rows belonging to the requested buffered cell row.
            if (funcName == "__settilebg16_flush")
            {
                if (args.Length != 4) Error(expr, "__settilebg16_flush requires 4 arguments");
                EmitTile16FlushRowsIntrinsic(args[0], args[1], args[2], args[3], cgbAttr: false, labelPrefix: "settilebg16flush");
                return;
            }

            // Flush the tile buffer first and copy attributes only when CGB hardware is available.
            if (funcName == "__settilebg16cgb_flush")
            {
                if (args.Length != 5) Error(expr, "__settilebg16cgb_flush requires 5 arguments");
                EmitTile16FlushRowsIntrinsic(args[0], args[2], args[3], args[4], cgbAttr: false, labelPrefix: "settilebg16cgbflush_tile");
                if (IsKnownCgbRuntimeTrue())
                {
                    EmitSetVbkUnchecked(1);
                    EmitTile16FlushRowsIntrinsic(args[1], args[2], args[3], args[4], cgbAttr: true, labelPrefix: "settilebg16cgbflush_attr");
                }
                else
                {
                    AsmOperand ski
__settilebg16cgb_flush

将 RAM 缓冲区中相邻的两行图块传输到 LCDC 所选背景地图。源偏移为 64*y + x,从该行及其下方相隔 32 字节的一行各复制 count 字节。

依次写入图块编号和 CGB 属性。DMG 只写编号。CGB 调用前应选择 VRAM 库 0;属性写入库 1,结束时为 VBK=0。

必要时等待 VRAM 可写。LCD 开启时的路径会屏蔽中断,并可能在结束后启用中断;不保证保留调用前的中断状态,也不保证在一次 VBlank 内完成。

语法

c
__settilebg16cgb_flush(tilebuf, attrbuf, y, x, count);

此调用格式中的参数名用于解释编译器内置功能,不是需要另行添加的 C 声明。

参数

tilebuf, attrbuf

可读的图块地图缓冲区,行间距为 32 字节,完整地图占 1024 字节。双缓冲区形式接收布局相同的独立图块编号和属性数组,二者不交错存储。

y, x, count

参数顺序为 y,x,count。y 是 16 像素单元行(0~15),x 和 count 以 8 像素图块计。须满足 x+count <= 32。传输单元 (2,3) 时使用 y=3,x=4,count=2。

返回值

无(void)。

使用条件与限制

示例使用 gb_common.h。m_init() 将附带 ASCII 字体加载到与字符编码相同的图块编号。m_wait(); M_LCDC=0; 在 VBlank 中关闭 LCD,以便进行无需等待的传输。最后的 LCDC 值选择显示地图。

使用示例

c
// __settilebg16cgb_flush
// Transfer two adjacent tile rows from the RAM buffer to the LCDC-selected BG map. The
// source offset is `64*y + x`; `count` bytes are copied from that row and from the row
// 32 bytes below it. Write a tile number and its CGB attribute. On DMG, only the tile-
// number write is performed. On CGB, enter with VRAM bank 0 selected; attributes use
// bank 1 and the call finishes with `VBK=0`.
// Expected: FAILED CHECKS 000, with the written tile data in the selected map.
#include "gb_tile_example.h"
u8 example_tiles[1024];
u8 example_attrs[1024];
void __memset(void* dest, u8 value, u16 len);
void __settilebg16_buf(u8* buf, u8 x, u8 y, u8 tile);
void __settilebg16cgb_buf(u8* tilebuf, u8* attrbuf, u8 x, u8 y, u8 tile, u8 attr);
void __settilebg16_flush(const u8* buf, u8 y, u8 x, u8 count);
void __settilebg16cgb_flush(const u8* tilebuf, const u8* attrbuf, u8 y, u8 x, u8 count);

void main() {
    u8 failures;
    failures = 0;
    tile_example_begin();
    __memset(example_tiles, 0, 1024);
    __memset(example_attrs, 0, 1024);
    example_tiles[66]='A'; example_tiles[67]='B';
    example_tiles[98]='C'; example_tiles[99]='D';
    example_attrs[66]=2; example_attrs[67]=2;
    example_attrs[98]=2; example_attrs[99]=2;
    __settilebg16cgb_flush(example_tiles, example_attrs, 1, 2, 2);
    if (tile_example_read(0x9C42, 0) != 'A') { failures++; }
    if (tile_example_read(0x9C43, 0) != 'B') { failures++; }
    if (tile_example_read(0x9C62, 0) != 'C') { failures++; }
    if (tile_example_read(0x9C63, 0) != 'D') { failures++; }
    if (example_tiles[66] != 'A') { failures++; }
    if (example_tiles[99] != 'D') { failures++; }
    if (tile_example_read(0x9C44, 0) != 0) { failures++; }
    if (example_attrs[66] != 2) { failures++; }
    if (example_attrs[99] != 2) { failures++; }
    if (__cgb_is_cgb() && tile_example_read(0x9C42, 1) != 2) { failures++; }
    if (__cgb_is_cgb() && tile_example_read(0x9C63, 1) != 2) { failures++; }
    tile_example_finish(failures, 1);
}

预期结果

所选地图显示写入的字符或填充区域。程序读回目标图块编号,并在 CGB 上读回属性进行比较;FAILED CHECKS 000 表示比较全部通过。两张地图初始均为 0,调用前将背景/窗口地图选为 0x9C00,因此能区分固定地图与所选地图的写入。示例在 LCD 关闭时写入,不测量 LCD 开启时的时序。

图块坐标 (2,2) 处显示上排 AB、下排 CD,即单元坐标 (1,1) 的一个 16×16 像素单元。缓冲区写入示例先确认 RAM 准备操作未改变 VRAM,再传输;传输函数示例则先逐字节设置 RAM 数据,再执行传输。

CGB 上四个单元的属性均为 2,因此四个字符均为蓝色;DMG 上为黑色。

预期图像:以下图块排列从图块坐标 (x=2, y=2)、像素坐标 (16,16) 开始显示。每个字符占一个 8×8 图块。按从上到下、从左到右阅读,周围图块保持空白。

text
AB
CD
__settilebg16cgb_flush: DMG
DMG
__settilebg16cgb_flush: CGB
CGB

完整示例程序

构建命令

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqgb\kitaqgb.exe .\kitaq-docs\samples\api-examples\gb\__settilebg16cgb_flush.c -I .\kitaqgb\lib -I .\kitaq-docs\samples -o .\out\__settilebg16cgb_flush.gb --profile=dev --rst-disable --stack-bank=fixed --cgb=cgb --no-disasm
实现与源码

kitaqgb/kitaqgb/CodeGenerator.cs:10589

csharp
{
                if (args.Length != 5) Error(expr, "__settilebg16cgb_flush requires 5 arguments");
                EmitTile16FlushRowsIntrinsic(args[0], args[2], args[3], args[4], cgbAttr: false, labelPrefix: "settilebg16cgbflush_tile");
                if (IsKnownCgbRuntimeTrue())
                {
                    EmitSetVbkUnchecked(1);
                    EmitTile16FlushRowsIntrinsic(args[1], args[2], args[3], args[4], cgbAttr: true, labelPrefix: "settilebg16cgbflush_attr");
                }
                else
                {
                    AsmOperand skip = MakeUniqueLabel("settilebg16cgbflush_skip");
                    AsmOperand done = MakeUniqueLabel("settilebg16cgbflush_done");
                    EmitRuntimeCgbCheckCall();
                    EmitAsm("OR_A");
                    // Two unrolled row copies can exceed the relative-branch range.
                    EmitAsm("JP_Z", skip);
                    EmitSetVbkUnchecked(1);
                    EmitTile16FlushRowsIntrinsic(args[1], args[2], args[3], args[4], cgbAttr: true, labelPrefix: "settilebg16cgbflush_attr");
                    EmitAsm("JR", done);
                    EmitLabel(skip);
                    EmitLabel(done);
                }
                return;
            }

            // 2.14 __getbgmapbase() -> u16 in HL
// Returns the BG tilemap base address selected by LCDC bit3:
// bit3=0 => 0x9800, bit3=1 => 0x9C00
// Treated as a compiler intrinsic (no C definition required).
if (funcName == "__getbgmapbase")
{
    if (args.Length != 0) Error(expr, "__getb
__smac16

计算有符号乘加。

语法

c
u16 __smac16(u16 acc, u16 a, u8 b);

参数

acc

要加上的 16 位累加器值。

a

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

b

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

返回值

每项按 trunc(a*b/256) 计算,再加到 acc 上;返回低 16 位。

使用条件与限制

GB 的乘积除以 256;有符号结果向零截断。Q1.7 变体将该截断结果乘以二,可能与精确除以 128 的结果相差一。各项分别舍入;最终加法按 16 位回绕。

使用示例

c
// Compare literal coefficients with values read from RAM.
result[26]=__smac16(65500,-513,-64);
v0=65500;v1=-513;v2=-64;
result[27]=__smac16(v0,v1,v2);
// Reverse coefficient signs and verify negative products.
result[28]=__smac16(65500,-513,64);
v0=65500;v1=-513;v2=64;
result[29]=__smac16(v0,v1,v2);

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

预期结果

比较字面量输入与 RAM 中存储的输入。代表性结果以无符号 16 位值显示;负数采用二进制补码位模式。GB 会对定点乘积进行缩放,FC 则返回整数乘积,因此相同输入不一定得到相同结果。

这段代码保存以下 16 位值:result[26]=92, result[27]=92, result[28]=65372, result[29]=65372。使用字面量输入和对应 RAM 输入的计算结果一致。屏幕显示代表性数值;result 数组保存全部结果。

__smac16: DMG
DMG
__smac16: CGB
CGB

完整示例程序

构建命令

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqgb\kitaqgb.exe .\kitaq-docs\samples\api-examples\gb\batch3_math.c -I .\kitaqgb\lib -I .\kitaq-docs\samples -o .\out\batch3_math.gb --no-cache --no-disasm --profile=dev --rst-disable --stack-bank=fixed --cgb=cgb --cart=mbc5 --romsize=64k --ramsize=none
实现与源码

kitaqgb/kitaqgb/CodeGenerator.cs:11516

kitaqgb/kitaqgb/CodeGenerator.cs:11516

csharp
if (funcName == "__smac16")
            {
                if (args.Length != 3) Error(expr, "__smac16 requires 3 arguments");

                // mul = (a*b)>>8 (signed)
                EmitDotTermToHL(args[1], args[2], signed: true, q1_7: false, labelPrefix: "__smac16_mul");

                // acc + mul
                EmitAsm("PUSH_HL");
                CompileIntoHL(args[0]);
                EmitAsm("POP_DE");
                EmitAsm("ADD_HL_DE");
                LastCallReturnsHL = true;
                return;
            }
__smac16_q1_7

计算有符号乘加。

语法

c
u16 __smac16_q1_7(u16 acc, u16 a, u8 b);

参数

acc

要加上的 16 位累加器值。

a

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

b

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

返回值

每项按 2*trunc(a*b/256) 计算,再加到 acc 上;返回低 16 位。

使用条件与限制

GB 的乘积除以 256;有符号结果向零截断。Q1.7 变体将该截断结果乘以二,可能与精确除以 128 的结果相差一。各项分别舍入;最终加法按 16 位回绕。

使用示例

c
// Compare literal coefficients with values read from RAM.
result[0]=__smac16_q1_7(65500,-513,-64);
v0=65500;v1=-513;v2=-64;
result[1]=__smac16_q1_7(v0,v1,v2);
// Reverse coefficient signs and verify negative products.
result[2]=__smac16_q1_7(65500,-513,64);
v0=65500;v1=-513;v2=64;
result[3]=__smac16_q1_7(v0,v1,v2);

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

预期结果

比较字面量输入与 RAM 中存储的输入。代表性结果以无符号 16 位值显示;负数采用二进制补码位模式。GB 会对定点乘积进行缩放,FC 则返回整数乘积,因此相同输入不一定得到相同结果。

这段代码保存以下 16 位值:result[0]=220, result[1]=220, result[2]=65244, result[3]=65244。使用字面量输入和对应 RAM 输入的计算结果一致。屏幕显示代表性数值;result 数组保存全部结果。

__smac16_q1_7: DMG
DMG
__smac16_q1_7: CGB
CGB

完整示例程序

构建命令

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqgb\kitaqgb.exe .\kitaq-docs\samples\api-examples\gb\batch3_math.c -I .\kitaqgb\lib -I .\kitaq-docs\samples -o .\out\batch3_math.gb --no-cache --no-disasm --profile=dev --rst-disable --stack-bank=fixed --cgb=cgb --cart=mbc5 --romsize=64k --ramsize=none
实现与源码

kitaqgb/kitaqgb/CodeGenerator.cs:11533

kitaqgb/kitaqgb/CodeGenerator.cs:11533

csharp
if (funcName == "__smac16_q1_7")
            {
                if (args.Length != 3) Error(expr, "__smac16_q1_7 requires 3 arguments");

                // mul = (a*b)>>7 (approx via >>8 then *2)
                EmitDotTermToHL(args[1], args[2], signed: true, q1_7: true, labelPrefix: "__smac16_q1_7_mul");

                // acc + mul
                EmitAsm("PUSH_HL");
                CompileIntoHL(args[0]);
                EmitAsm("POP_DE");
                EmitAsm("ADD_HL_DE");
                LastCallReturnsHL = true;
                return;
            }
__smul16x8_q1_7

计算有符号乘法。

语法

c
u16 __smul16x8_q1_7(u16 a, u8 b);

参数

a

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

b

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

返回值

每项按 2*trunc(a*b/256) 计算;返回低 16 位。

使用条件与限制

GB 的乘积除以 256;有符号结果向零截断。Q1.7 变体将该截断结果乘以二,可能与精确除以 128 的结果相差一。各项分别舍入;最终加法按 16 位回绕。

使用示例

c
// Compare literal coefficients with values read from RAM.
result[12]=__smul16x8_q1_7(-513,-64);
v0=-513;v1=-64;
result[13]=__smul16x8_q1_7(v0,v1);
// Reverse coefficient signs and verify negative products.
result[14]=__smul16x8_q1_7(-513,64);
v0=-513;v1=64;
result[15]=__smul16x8_q1_7(v0,v1);

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

预期结果

比较字面量输入与 RAM 中存储的输入。代表性结果以无符号 16 位值显示;负数采用二进制补码位模式。GB 会对定点乘积进行缩放,FC 则返回整数乘积,因此相同输入不一定得到相同结果。

这段代码保存以下 16 位值:result[12]=256, result[13]=256, result[14]=65280, result[15]=65280。使用字面量输入和对应 RAM 输入的计算结果一致。屏幕显示代表性数值;result 数组保存全部结果。

__smul16x8_q1_7: DMG
DMG
__smul16x8_q1_7: CGB
CGB

完整示例程序

构建命令

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqgb\kitaqgb.exe .\kitaq-docs\samples\api-examples\gb\batch3_math.c -I .\kitaqgb\lib -I .\kitaq-docs\samples -o .\out\batch3_math.gb --no-cache --no-disasm --profile=dev --rst-disable --stack-bank=fixed --cgb=cgb --cart=mbc5 --romsize=64k --ramsize=none
实现与源码

kitaqgb/kitaqgb/CodeGenerator.cs:11490

kitaqgb/kitaqgb/CodeGenerator.cs:11490

csharp
if (funcName == "__smul16x8_q1_7")
            {
                if (args.Length != 2) Error(expr, "__smul16x8_q1_7 requires 2 arguments");

                EmitDotTermToHL(args[0], args[1], signed: true, q1_7: true, labelPrefix: "__smul16x8_q1_7");
                LastCallReturnsHL = true;
                return;
            }
__sram_read8

从指定 CPU 地址读取一个 SRAM 字节。

语法

c
u8 __sram_read8(u16 address);

参数

address

CPU 地址,范围为 $A000..$BFFF。

返回值

直接返回字节,不检查 RAM 是否存在或数据是否有效。

使用条件与限制

访问前向 $0000 写入 0x0A 以启用 RAM,访问后写入零以禁用 RAM。不选择存储体,也不验证地址;RAM 存储体由调用方配置。不管理存档头部或校验和。

使用示例

c
*((u8*)0x4000)=0;result[12]=__sram_read8(0xA006);result[19]=*((u8*)0xA006);

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

预期结果

保存三个字节 7/42/255,然后测试长度不匹配、无效槽位和校验和损坏。加载成功时,中间字节为 42;读取失败时,目标值保持为 77;清空后验证返回 0。

__sram_read8: DMG
DMG
__sram_read8: CGB
CGB

完整示例程序

构建命令

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqgb\kitaqgb.exe .\kitaq-docs\samples\api-examples\gb\batch3_save.c -I .\kitaqgb\lib -I .\kitaq-docs\samples -o .\out\batch3_save.gb --no-cache --no-disasm --profile=dev --rst-disable --stack-bank=fixed --cgb=cgb --cart=mbc5 --romsize=64k --ramsize=8k
实现与源码

kitaqgb/kitaqgb/CodeGenerator.cs:8825

kitaqgb/kitaqgb/CodeGenerator.cs:8825

csharp
if (funcName == "__sram_read8")
            {
                if (args.Length != 1) Error(expr, "__sram_read8(addr) expects 1 argument");

                CompileIntoHL(args[0]);
                EmitAsm("PUSH_HL");

                EmitAsm("LD_A_IMM", new AsmOperand(0x0A, AddressMode.Immediate));
                EmitAsm("LD_MEM_A", new AsmOperand(0x0000, AddressMode.Absolute));

                EmitAsm("POP_HL");
                EmitAsm("LD_A_HL");
                EmitAsm("LD_B_A");

                EmitAsm("LD_A_IMM", new AsmOperand(0x00, AddressMode.Immediate));
                EmitAsm("LD_MEM_A", new AsmOperand(0x0000, AddressMode.Absolute));
                EmitAsm("LD_A_B");
                return;
            }
__sram_write8

向指定 CPU 地址写入一个 SRAM 字节。

语法

c
void __sram_write8(u16 address, u8 value);

参数

address

CPU 地址,范围为 $A000..$BFFF。

value

字节值,范围为 0..255。

返回值

无返回值。

使用条件与限制

访问前向 $0000 写入 0x0A 以启用 RAM,访问后写入零以禁用 RAM。不选择存储体,也不验证地址;RAM 存储体由调用方配置。不管理存档头部或校验和。

使用示例

c
__sram_write8(0xA006,8);result[20]=*((u8*)0xA006);result[13]=save_exists(0);readback[0]=77;result[14]=save_read(0,readback,3);result[15]=readback[0];

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

预期结果

保存三个字节 7/42/255,然后测试长度不匹配、无效槽位和校验和损坏。加载成功时,中间字节为 42;读取失败时,目标值保持为 77;清空后验证返回 0。

__sram_write8: DMG
DMG
__sram_write8: CGB
CGB

完整示例程序

构建命令

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqgb\kitaqgb.exe .\kitaq-docs\samples\api-examples\gb\batch3_save.c -I .\kitaqgb\lib -I .\kitaq-docs\samples -o .\out\batch3_save.gb --no-cache --no-disasm --profile=dev --rst-disable --stack-bank=fixed --cgb=cgb --cart=mbc5 --romsize=64k --ramsize=8k
实现与源码

kitaqgb/kitaqgb/CodeGenerator.cs:8846

kitaqgb/kitaqgb/CodeGenerator.cs:8846

csharp
if (funcName == "__sram_write8")
            {
                if (args.Length != 2) Error(expr, "__sram_write8(addr, value) expects 2 arguments");

                CompileIntoHL(args[0]);
                EmitAsm("PUSH_HL");
                CompileIntoA(args[1]);
                EmitAsm("LD_C_A");

                EmitAsm("LD_A_IMM", new AsmOperand(0x0A, AddressMode.Immediate));
                EmitAsm("LD_MEM_A", new AsmOperand(0x0000, AddressMode.Absolute));

                EmitAsm("POP_HL");
                EmitAsm("LD_A_C");
                EmitAsm("LD_HL_A");

                EmitAsm("LD_A_IMM", new AsmOperand(0x00, AddressMode.Immediate));
                EmitAsm("LD_MEM_A", new AsmOperand(0x0000, AddressMode.Absolute));
                return;
            }
__svbk_get

读取映射到$D000–$DFFF的有效WRAM存储体编号。读取SVBK位0~2,并将原始选择值0归一化为1。返回的是存储体编号,不是RAM容量、地址或指针。

语法

c
u8 __svbk_get();

此调用格式中的参数名用于解释编译器内置功能,不是需要另行添加的 C 声明。

返回值

当前有效存储体编号,u8,1~7;原始选择值0返回为1。

使用条件与限制

__svbk_get和__svbk_set要求--cgb=cgb_only或#pragma rom_cgb cgb_only。DMG及双模式构建会报KQ2103,即使运行时条件会跳过调用。只有在不同接口约定适用时,才使用带保护的原始设置函数。

切换存储体影响$D000–$DFFF的所有访问,固定WRAM的$C000–$CFFF保持不变。切换期间所需的栈和状态须放在该窗口之外。示例使用--stack-bank=fixed并明确将状态放入固定WRAM。须自行协调中断与使用存储体的DMA;这些API不建立临界区。

使用示例

c
if (__svbk_get()!=1) failures++; // Normalize raw selector zero to effective bank 1.

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

预期结果

CGB专用完整程序在WRAM存储体2的$D100保存1,在存储体3的同一地址保存2,再读回两者并恢复到1。读值分别选择(16,24)的红色8×8正方形与(16,48)的蓝色1像素宽8×8方框。FAILED CHECKS 000检查原始选择值归一化、存储体独立性,以及用常量、变量、表达式、函数返回值切换时的原编号返回。另有6个故意无效的构建测试验证DMG/双模式的KQ2103及常量0、8的KQ2102。运行验证使用KOKURA;未测试实机及中断并发切换。

__svbk_get: --cgb=cgb_only / CGB
--cgb=cgb_only / CGB

完整示例程序

构建命令

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqgb\kitaqgb.exe .\kitaq-docs\samples\api-examples\gb\cgb_wram_banks.c -I .\kitaqgb\lib -I .\kitaq-docs\samples -o .\out\cgb_wram_banks.gb --profile=dev --rst-disable --stack-bank=fixed --cgb=cgb_only --no-cache --no-disasm
实现与源码

kitaqgb/kitaqgb/CodeGenerator.cs:7387

csharp
{
                if (args.Length != 0) Error(expr, "__svbk_get() expects 0 arguments");
                if (!Program.IsCgbOnlyTargetRequested())
                    Error(expr, ErrorCode.BankedWramRequiresCgbOnly, "__svbk_get() requires #pragma rom_cgb cgb_only or --cgb=cgb_only");
                EmitReadNormalizedSvbkIntoA();
                return;
            }

            // Select a WRAM bank and return the prior normalized bank; diagnose out-of-range constant requests.
            if (funcName == "__svbk_set")
            {
                if (args.Length != 1) Error(expr, "__svbk_set(bank_u8) expects 1 argument");
                if (!Program.IsCgbOnlyTargetRequested())
                    Error(expr, ErrorCode.BankedWramRequiresCgbOnly, "__svbk_set() requires #pragma rom_cgb cgb_only or --cgb=cgb_only");

                Expr bankExpr = FoldConstants(args[0]);
                if (bankExpr.Match(Tag.Integer, out int bankLiteral) && (bankLiteral < 1 || bankLiteral > 7))
                    Error(expr, ErrorCode.InvalidWramXBank, "__svbk_set() bank must be in range 1..7 (got {0})", bankLiteral);

                EmitReadNormalizedSvbkIntoA();
                EmitAsm("LD_B_A");
                CompileIntoA(bankExpr);
                EmitAsm("LDH_MEM_A", new AsmOperand(0x70, AddressMode.HighMem));
                EmitAsm("LD_A_B");
                return;
            }

            // __cgb_safe_set_* (safe CGB-only register write wrappers)
            if (TryGetCgbSafeRegister(funcName, out int cgbOffset, out string cgbReg))
            {
__svbk_set

切换映射到$D000–$DFFF的WRAM存储体,并返回切换前的有效编号。如需恢复调用方的存储体,请保存返回值。不会复制或清空内存内容,也不会自动恢复存储体。

语法

c
u8 __svbk_set(u8 bank);

此调用格式中的参数名用于解释编译器内置功能,不是需要另行添加的 C 声明。

参数

bank

请求的存储体编号为u8,范围1~7。越界常量产生KQ2102。该内置函数不检查动态值范围;应在调用前验证,不要依赖寄存器掩码。

返回值

切换前的有效存储体编号,u8,1~7。返回原存储体,不是新选择的存储体。

使用条件与限制

__svbk_get和__svbk_set要求--cgb=cgb_only或#pragma rom_cgb cgb_only。DMG及双模式构建会报KQ2103,即使运行时条件会跳过调用。只有在不同接口约定适用时,才使用带保护的原始设置函数。

切换存储体影响$D000–$DFFF的所有访问,固定WRAM的$C000–$CFFF保持不变。切换期间所需的栈和状态须放在该窗口之外。示例使用--stack-bank=fixed并明确将状态放入固定WRAM。须自行协调中断与使用存储体的DMA;这些API不建立临界区。

使用示例

c
previous_bank=__svbk_set(requested_bank);
if(previous_bank!=1 || __svbk_get()!=2) failures++;
*((u8*)0xD100)=1; // This byte belongs to bank 2.
previous_bank=__svbk_set(next_bank()); // Dynamic argument evaluates to 3.
if(previous_bank!=2 || __svbk_get()!=3) failures++;
*((u8*)0xD100)=2; // Same CPU address, different physical RAM.

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

预期结果

CGB专用完整程序在WRAM存储体2的$D100保存1,在存储体3的同一地址保存2,再读回两者并恢复到1。读值分别选择(16,24)的红色8×8正方形与(16,48)的蓝色1像素宽8×8方框。FAILED CHECKS 000检查原始选择值归一化、存储体独立性,以及用常量、变量、表达式、函数返回值切换时的原编号返回。另有6个故意无效的构建测试验证DMG/双模式的KQ2103及常量0、8的KQ2102。运行验证使用KOKURA;未测试实机及中断并发切换。

__svbk_set: --cgb=cgb_only / CGB
--cgb=cgb_only / CGB

完整示例程序

构建命令

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqgb\kitaqgb.exe .\kitaq-docs\samples\api-examples\gb\cgb_wram_banks.c -I .\kitaqgb\lib -I .\kitaq-docs\samples -o .\out\cgb_wram_banks.gb --profile=dev --rst-disable --stack-bank=fixed --cgb=cgb_only --no-cache --no-disasm
实现与源码

kitaqgb/kitaqgb/CodeGenerator.cs:7397

csharp
{
                if (args.Length != 1) Error(expr, "__svbk_set(bank_u8) expects 1 argument");
                if (!Program.IsCgbOnlyTargetRequested())
                    Error(expr, ErrorCode.BankedWramRequiresCgbOnly, "__svbk_set() requires #pragma rom_cgb cgb_only or --cgb=cgb_only");

                Expr bankExpr = FoldConstants(args[0]);
                if (bankExpr.Match(Tag.Integer, out int bankLiteral) && (bankLiteral < 1 || bankLiteral > 7))
                    Error(expr, ErrorCode.InvalidWramXBank, "__svbk_set() bank must be in range 1..7 (got {0})", bankLiteral);

                EmitReadNormalizedSvbkIntoA();
                EmitAsm("LD_B_A");
                CompileIntoA(bankExpr);
                EmitAsm("LDH_MEM_A", new AsmOperand(0x70, AddressMode.HighMem));
                EmitAsm("LD_A_B");
                return;
            }

            // __cgb_safe_set_* (safe CGB-only register write wrappers)
            if (TryGetCgbSafeRegister(funcName, out int cgbOffset, out string cgbReg))
            {
                if (args.Length != 1) Program.Error(funcName + "(value_u8) expects 1 argument");

                if (IsKnownCgbRuntimeTrue())
                {
                    CompileIntoA(args[0]);
                    if (cgbOffset == 0x4F)
                        EmitAsm("AND_IMM", new AsmOperand(0x01, AddressMode.Immediate));
                    EmitAsm("LDH_MEM_A", new AsmOperand(cgbOffset, AddressMode.HighMem));
                    CgbGuardedWriteCount++;
                    CgbGuardedRegisters.Add(cgbReg);
                    return;
__vram_memcpy_unsafe

立即按地址递增顺序将src的len字节复制到dst,完成后才返回,不将操作加入队列。源地址和目标地址每次增加1字节,不处理图块映射表行跨度或图块格式转换。

语法

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

此调用格式中的参数名用于解释编译器内置功能,不是需要另行添加的 C 声明。

参数

dst

目标首字节地址,通常位于VRAM 0x8000–0x9FFF。整个范围必须有效。CGB使用当前VRAM存储体;映射表区域的存储体0存放图块编号,存储体1存放属性。这些函数不根据LCDC选择映射表,不在行末换行,也不分配VRAM。

src

源字节指针,在整个调用期间都必须有效且映射正确。数据立即读取,不为后续flush保留,也不会自动切换源存储体。请使用不重叠的范围;这种向前复制不提供memmove语义。

len

连续字节数,范围0至65535,不是图块数。整个范围应位于有效源数据和可写目标区域内;API不检查边界,也不防止十六位地址回绕。

返回值

无(void)。

使用条件与限制

跳过全部LCD/STAT等待,直接用CPU写入。应关闭LCD,或确保整个传输都在可控的可写时段内完成。仅进入VBlank并不保证任意长度的传输安全。此内置函数不会为传输生成DI或EI,也不裁剪或检查范围。

使用示例

c
// The LCD is off throughout these unchecked writes.
__vram_memcpy_unsafe(0x98C2,strip,computed_length());

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

预期结果

本例用__vram_memcpy_unsafe在图块坐标(2,6)、像素坐标(16,48)写入3字节映射表数据。24×8区域应从左到右显示[1,2,3]图案,相邻单元保持空白。完整程序将每种受测复制或填充放在独立的一行。

附带的自制图形中,图块1为8×8实心正方形,2只填左侧四列,3为左对齐的三角形,每行加宽1像素,从1像素增至8像素。DMG显示白底黑图形;CGB按操作区分颜色,文字保持黑色。

像素(16,88)的正方形应在安全版和无等待版的零长度调用后保留。LCD开启时,安全复制和填充在(16,96)和(32,96)添加正方形。FAILED CHECKS应为000。模拟器测试同时比较绘制像素和计数值。本例CGB使用存储体0;此处未测试其他存储体或实机。

CGB模式下,从MEMCPY到FILL UNSAFE各行依次循环使用红、蓝、绿。ZERO COUNT正方形为红色;LCD ON左侧正方形为蓝色,右侧为绿色,文字保持黑色。测试还检查每个绘制像素是否为指定颜色。

__vram_memcpy_unsafe: DMG
DMG
__vram_memcpy_unsafe: CGB
CGB

完整示例程序

构建命令

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqgb\kitaqgb.exe .\kitaq-docs\samples\api-examples\gb\vram_memory_shapes.c -I .\kitaqgb\lib -I .\kitaq-docs\samples -o .\out\vram_memory_shapes.gb --profile=dev --rst-disable --stack-bank=fixed --cgb=cgb --no-cache --no-disasm
实现与源码

kitaqgb/kitaqgb/CodeGenerator.cs:8104

csharp
{
                if (args.Length != 3) Error(expr, "__vram_memcpy_unsafe(dst, src, len) expects 3 arguments");

                CompileIntoHL(args[0]); EmitAsm("PUSH_HL"); // dst
                CompileIntoHL(args[1]); EmitAsm("PUSH_HL"); // src
                // Use the same count specialization without emitting LCD-mode waits.
                Expr vramCopyUnsafeLen = FoldConstants(args[2]);
                EmitLengthExprIntoBC(vramCopyUnsafeLen);
                EmitAsm("POP_DE"); // src
                EmitAsm("POP_HL"); // dst

                if (vramCopyUnsafeLen.Match(Tag.Integer, out int vramCopyUnsafeLenConst) &&
                    TryEmitVramMemcpyConstCount(false, vramCopyUnsafeLenConst, "vramcpyu_small"))
                    return;

                AsmOperand vcuLoop = MakeUniqueLabel("vramcpyu_loop");
                AsmOperand vcuDone = MakeUniqueLabel("vramcpyu_done");
                EmitLabel(vcuLoop);
                EmitAsm("LD_A_B");
                EmitAsm("OR_C");
                EmitAsm("JR_Z", vcuDone);
                EmitAsm("LD_A_DE");
                EmitAsm("LDI_HL_A");
                EmitAsm("INC_DE");
                EmitAsm("DEC_BC");
                EmitAsm("JR", vcuLoop);
                EmitLabel(vcuDone);
                return;
            }

            // __vram_memset(dst_ptr, value_u8, len_u16)
            // Safe VRAM fill: if LCD is on, each byte write is gated by STAT mode 0/1.
            if (funcName == "__vram_memset")
            {
                if (args.Length != 3) Error(expr, "__vram_memset(dst, value, l
__vram_memset_unsafe

立即用单字节value填充从dst开始的连续len字节。重复的是1字节,不是整个图块图案。填充映射表字节会重复图块编号;填充图案字节则会改变编码后的像素位平面。

语法

c
void __vram_memset_unsafe(u16 dst, u8 value, u16 len);

此调用格式中的参数名用于解释编译器内置功能,不是需要另行添加的 C 声明。

参数

dst

目标首字节地址,通常位于VRAM 0x8000–0x9FFF。整个范围必须有效。CGB使用当前VRAM存储体;映射表区域的存储体0存放图块编号,存储体1存放属性。这些函数不根据LCDC选择映射表,不在行末换行,也不分配VRAM。

value

重复写入0至255的单字节。显示含义取决于目标是图块索引、像素位平面还是其他字节结构。

len

连续字节数,范围0至65535,不是图块数。整个范围应位于有效源数据和可写目标区域内;API不检查边界,也不防止十六位地址回绕。

返回值

无(void)。

使用条件与限制

跳过全部LCD/STAT等待,直接用CPU写入。应关闭LCD,或确保整个传输都在可控的可写时段内完成。仅进入VBlank并不保证任意长度的传输安全。此内置函数不会为传输生成DI或EI,也不裁剪或检查范围。

使用示例

c
__vram_memset_unsafe(0x9942,1,computed_length()); // Safe here because the LCD is off.

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

预期结果

本例用__vram_memset_unsafe将图块坐标(2,10)、像素坐标(16,80)开始的3个映射表字节填为图块编号1。应得到24×8实心横条,相邻单元为空白。长度由变量或函数返回值计算;三个字节仍须全部写入值1。

附带的自制图形中,图块1为8×8实心正方形,2只填左侧四列,3为左对齐的三角形,每行加宽1像素,从1像素增至8像素。DMG显示白底黑图形;CGB按操作区分颜色,文字保持黑色。

像素(16,88)的正方形应在安全版和无等待版的零长度调用后保留。LCD开启时,安全复制和填充在(16,96)和(32,96)添加正方形。FAILED CHECKS应为000。模拟器测试同时比较绘制像素和计数值。本例CGB使用存储体0;此处未测试其他存储体或实机。

CGB模式下,从MEMCPY到FILL UNSAFE各行依次循环使用红、蓝、绿。ZERO COUNT正方形为红色;LCD ON左侧正方形为蓝色,右侧为绿色,文字保持黑色。测试还检查每个绘制像素是否为指定颜色。

__vram_memset_unsafe: DMG
DMG
__vram_memset_unsafe: CGB
CGB

完整示例程序

构建命令

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqgb\kitaqgb.exe .\kitaq-docs\samples\api-examples\gb\vram_memory_shapes.c -I .\kitaqgb\lib -I .\kitaq-docs\samples -o .\out\vram_memory_shapes.gb --profile=dev --rst-disable --stack-bank=fixed --cgb=cgb --no-cache --no-disasm
实现与源码

kitaqgb/kitaqgb/CodeGenerator.cs:8196

csharp
{
                if (args.Length != 3) Error(expr, "__vram_memset_unsafe(dst, value, len) expects 3 arguments");

                CompileIntoHL(args[0]); EmitAsm("PUSH_HL"); // dst
                CompileIntoA(args[1]); EmitAsm("LD_D_A"); // value
                // Specialize a constant unsafe fill or fall back to its unchecked byte loop.
                Expr vramSetUnsafeLen = FoldConstants(args[2]);
                EmitLengthExprIntoBC(vramSetUnsafeLen);
                EmitAsm("POP_HL"); // dst

                if (vramSetUnsafeLen.Match(Tag.Integer, out int vramSetUnsafeLenConst) &&
                    TryEmitVramMemsetConstCount(false, vramSetUnsafeLenConst, "vramsetu_small"))
                    return;

                AsmOperand vsuLoop = MakeUniqueLabel("vramsetu_loop");
                AsmOperand vsuDone = MakeUniqueLabel("vramsetu_done");
                EmitLabel(vsuLoop);
                EmitAsm("LD_A_B");
                EmitAsm("OR_C");
                EmitAsm("JR_Z", vsuDone);
                EmitAsm("LD_A_D");
                EmitAsm("LDI_HL_A");
                EmitAsm("DEC_BC");
                EmitAsm("JR", vsuLoop);
                EmitLabel(vsuDone);
                return;
            }

            // __far_memcpy(dst_ptr, bank_u8, src_ptr, len_u16)
            // Copies from switchable ROM bank via a bank0 helper and restores original bank.
            if (funcName == "__far_memcpy")
            {
                if (args.Length != 4) Error(expr, "__far_memcpy(dst, bank, src, len) expects 4 arguments");

                CompileIntoHL(
__wait_ly

LCD 运行时,等待 LY 等于目标行号。

语法

c
void __wait_ly(u8 ly);

参数

ly

扫描行号,范围为 0..153;大于 153 时,只要 LCD 仍在运行,等待就不会结束。

返回值

无返回值。

使用条件与限制

正好处于目标行时立即返回;若已错过目标行,则等到下一帧。LCD 关闭时也立即返回。

使用示例

c
M_LCDC=0x91;__wait_ly(80);result[9]=*((u8*)0xFF44);

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

预期结果

学习地图选择、滚动、嵌套和扫描行等待。基地址为 38912/39936;嵌套令牌为 0/1;观测到的目标 LY 值为 80/144。同时验证 LCD 关闭时等待函数会返回。

__wait_ly: DMG
DMG
__wait_ly: CGB
CGB

完整示例程序

构建命令

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqgb\kitaqgb.exe .\kitaq-docs\samples\api-examples\gb\batch3_display.c -I .\kitaqgb\lib -I .\kitaq-docs\samples -o .\out\batch3_display.gb --no-cache --no-disasm --profile=dev --rst-disable --stack-bank=fixed --cgb=cgb --cart=mbc5 --romsize=64k --ramsize=none
实现与源码

kitaqgb/kitaqgb/CodeGenerator.cs:7483

kitaqgb/kitaqgb/CodeGenerator.cs:7483

csharp
if (funcName == "__wait_ly")
            {
                if (args.Length != 1) Error(expr, "__wait_ly(target) expects 1 argument");

                CompileIntoA(args[0]);
                EmitAsm("LD_B_A");

                AsmOperand wlyLoop = MakeUniqueLabel("waitly_loop");
                AsmOperand wlyDone = MakeUniqueLabel("waitly_done");

                // If LCD is off, LY is not meaningful; return.
                EmitAsm("LDH_A_MEM", new AsmOperand(0x40, AddressMode.HighMem)); // LCDC
                EmitAsm("AND_IMM", new AsmOperand(0x80, AddressMode.Immediate));
                EmitAsm("JR_Z", wlyDone);

                EmitLabel(wlyLoop);
                EmitAsm("LDH_A_MEM", new AsmOperand(0x44, AddressMode.HighMem)); // LY
                EmitAsm("CP_B");
                EmitAsm("JR_NZ", wlyLoop);
                EmitLabel(wlyDone);
                return;
            }

physics2d.h — 2D 运动与碰撞

提供矩形积分、轴对齐包围盒接触及可选表面响应求解。刚体使用中心和半尺寸,active=0 排除该槽位,inv_mass_q8<=0 表示静态。KQRect 则使用左上角和宽高。矩形对接触不使用刚体恢复系数字段,表面响应使用 KQSurface2D 系数。

位置、半径、半尺寸、速度和重力须采用一致的整数单位。一次 step 对应一次模拟更新,示例以一单位表示一像素。函数不会自动将位置转换为 Q8.8,也不负责绘制。

world 仅引用数组,刚体存储由调用方分配、持有并保持有效。初始化 world 不会初始化数组中的元素。

__smul16x8

将有符号 16 位值乘以有符号 8 位系数,再除以 256,向零截断。

语法

c
s16 __smul16x8(s16 a, s8 b);

参数

a

待缩放的有符号 16 位数值。

b

范围 −128~127 的有符号字节系数。

返回值

返回有符号 16 位缩放乘积。

使用条件与限制

系数范围 −128~127,以 256 表示 1.0。不论符号如何,乘积都向零截断,例如 −257×64/256 返回 −64。

使用示例

c
result[13]=__smul16x8(-257,64);

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

预期结果

左侧为初始状态,右侧为一次更新后。红色 16×16 矩形中心从 (24,24) 以速度 (4,16) 到达 (28,36),碰到蓝色地板后 Y 速度为零。面板原点为屏幕 (8,48) 和 (88,48),DMG 中所有标记为黑色。RAM 检查还覆盖初始化、设置函数、积分、矩形/点查询和带符号缩放。

__smul16x8: DMG — 左侧为初始状态,右侧为一次更新后。红色 16×16 矩形中心从 (24,24) 以速度 (4,16) 到达 (28,36),碰到蓝色地板后 Y 速度为零。面板原点为屏幕 (8,48) 和 (88,48),DMG 中所有标记为黑色。RAM 检查还覆盖初始化、设置函数、积分、矩形/点查询和带符号缩放。
DMG — 左侧为初始状态,右侧为一次更新后。红色 16×16 矩形中心从 (24,24) 以速度 (4,16) 到达 (28,36),碰到蓝色地板后 Y 速度为零。面板原点为屏幕 (8,48) 和 (88,48),DMG 中所有标记为黑色。RAM 检查还覆盖初始化、设置函数、积分、矩形/点查询和带符号缩放。
__smul16x8: CGB — 左侧为初始状态,右侧为一次更新后。红色 16×16 矩形中心从 (24,24) 以速度 (4,16) 到达 (28,36),碰到蓝色地板后 Y 速度为零。面板原点为屏幕 (8,48) 和 (88,48),DMG 中所有标记为黑色。RAM 检查还覆盖初始化、设置函数、积分、矩形/点查询和带符号缩放。
CGB — 左侧为初始状态,右侧为一次更新后。红色 16×16 矩形中心从 (24,24) 以速度 (4,16) 到达 (28,36),碰到蓝色地板后 Y 速度为零。面板原点为屏幕 (8,48) 和 (88,48),DMG 中所有标记为黑色。RAM 检查还覆盖初始化、设置函数、积分、矩形/点查询和带符号缩放。

完整示例程序

构建命令

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqgb\kitaqgb.exe .\kitaq-docs\samples\api-examples\gb\physics_body2d.c -I .\kitaqgb\lib -I .\kitaq-docs\samples -o .\out\physics_body2d.gb --profile=dev --rst-disable --stack-bank=fixed --cgb=cgb --cart=mbc5 --romsize=128k --no-cache --no-disasm
实现与源码

kitaqgb/lib/physics2d.h:9

声明注释(原文)
Multiply a signed word by a signed byte; divide by 256, truncating toward zero.

kitaqgb/kitaqgb/CodeGenerator.cs:11478

csharp
if (funcName == "__smul16x8")
            {
                if (args.Length != 2) Error(expr, "__smul16x8 requires 2 arguments");

                EmitDotTermToHL(args[0], args[1], signed: true, q1_7: false, labelPrefix: "__smul16x8");
                LastCallReturnsHL = true;
                return;
            }

rpg.h — 随机数、文字、地图、保存及 RPG/ADV/SLG 功能

绝对文本辅助函数直接绘制背景地图。text_window 打开带框区域,text_clear_rect 填充区域,text_print_xy 写入图块字节而不解码控制码。text_print_u8、text_print_u16、text_print_s16 将数值转为十进制,只使用必要格数。替换长度不同的值时,请预留并清除完整字段。

rpg.h 声明的 RNG 函数由 lib/rng.c 实现。它们共享编译器的 GB 生成器:rng_seed 重置状态,rng8/rng_next8 消耗一个字节,rng16/rng_next16 消耗两个字节;rand_range/rng_range、rng_chance 和 weighted_choice 将取值用于游戏选择。调用这些库函数时,请将 rng.c 一起编译。要复现测试,请固定种子和调用顺序;基于取模的选择不保证精确概率。

RLE 使用数量/数值字节对,以数量零结束。rle_decode 从普通指针读取并展开到 RAM;rle_decode_far 从带 ROM 存储体编号的指针读取并展开到 RAM;__rle_decode_vram 则根据 LCD 访问时机,将跨存储体输入直接展开到 VRAM。RAM 函数实现在 lib/rle.c 中,直接 VRAM 操作由编译器生成。三者均返回输出字节数,要求数据有效且目标空间充足。

rpg.h 中声明的游戏状态存储由 lib/flags.c 实现。三个标志函数操作 2048 个独立布尔位;quest_state 和 quest_set_state 操作 64 个独立字节,用于游戏定义的阶段。使用时须编译 flags.c,并将 ID 保持在限制内;若进度需断电保存,请使用独立存档系统。

窗口式会话共用一个活动矩形、光标和字符速度设置。text_open 初始化区域,text_close 擦除区域,text_set_speed 设置每字符的 VBlank 等待次数。text_print 和 text_print_far 解释控制流;text_choice 运行阻塞式垂直选项列表。请先加载图块、安排区域尺寸,并设计游戏如何处理这些阻塞调用。

提供普通地图/元图块绘制、脚本、SRAM 存档和寻路功能。普通格子为 8×8 像素,元图块为 16×16。加载/绘制、移动范围掩码和攻击范围掩码分别操作;游戏负责协调图形加载与事件执行。

搜索使用约 4 KiB 的共享工作区,不可重入。地图最多为 1024 格,探索距离最多为 254。距离 0xFF 表示尚未到达,因此无法表示 255 步的路径。代价和掩码输出各需 width*height 个可写字节。

默认有四个 512 字节的槽位,每个含六字节头部及最多 506 字节的有效载荷。示例使用 MBC5 和 8 KiB SRAM。调用结束后 RAM 处于禁用状态,不恢复先前的 RAM 存储体或启用状态。

菜单既支持阻塞选择,也支持由游戏循环分别驱动输入和绘制。物品菜单显示名称与数量。

__bit_clear

将所选标志清为 0,不清除所在字节的其他位。适用于移除某个已记录条件,例如临时状态。重复清除同一位,结果仍为 0。

语法

c
void __bit_clear(u8* p, u16 bit_index);

参数

p

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

bit_index

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

返回值

无(void)。

使用条件与限制

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

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

使用示例

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 位不会改变结果。两次调用都保留其他已存储的标志。

GB 画面各行的预期十进制值为:TEST OFF 000、TEST ON 001、SET BIT1 166、CLEAR BIT2 162、TOGGLE BIT7 034、RESTORE 162、NEXT BYTE 130、FAILED 000。NEXT BYTE 来自设置第二个字节中的索引 9,将 0x80 改为 0x82,而不修改字节 0。DMG 使用白底黑字;CGB 使用白底蓝字。

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

__bit_clear: DMG
DMG
__bit_clear: CGB
CGB

完整示例程序

构建命令

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqgb\kitaqgb.exe .\kitaq-docs\samples\api-examples\gb\bit_intrinsics.c -I .\kitaqgb\lib -I .\kitaq-docs\samples -o .\out\bit_intrinsics.gb --profile=dev --rst-disable --stack-bank=fixed --cgb=cgb --no-cache --no-disasm
实现与源码

kitaqgb/lib/rpg.h:187

kitaqgb/kitaqgb/CodeGenerator.cs:8582

csharp
if (funcName == "__bit_test" || funcName == "__bit_set" || funcName == "__bit_clear" || funcName == "__bit_toggle")
            {
                if (args.Length != 2) Error(expr, funcName + "(ptr, bit_index) expects 2 arguments");

                CompileIntoHL(args[0]); EmitAsm("PUSH_HL"); // ptr
                CompileIntoHL(args[1]); EmitAsm("PUSH_HL"); // bit_index

                EmitAsm("LD_HL_SP_IMM", new AsmOperand(0, AddressMode.Relative));
                EmitAsm("LD_A_HL");
                EmitAsm("AND_IMM", new AsmOperand(7, AddressMode.Immediate));
                EmitAsm("LD_B_A"); // bit number 0..7

                EmitAsm("LD_HL_SP_IMM", new AsmOperand(0, AddressMode.Relative));
                EmitAsm("LD_A_HL");
                EmitAsm("LD_E_A");
                EmitAsm("INC_HL");
                EmitAsm("LD_A_HL");
                EmitAsm("LD_D_A");
                EmitAsm("LD_H_D");
                EmitAsm("LD_L_E");
                EmitShiftRightLogicalHL(3);
                EmitAsm("PUSH_HL"); // byte offset

                EmitAsm("LD_HL_SP_IMM", new AsmOperand(4, AddressMode.Relative));
                EmitAsm("LD_A_HL");
                EmitAsm("LD_E_A");
                EmitAsm("INC_HL");
                EmitAsm("LD_A_HL");
                EmitAsm("LD_D_A");

                EmitAsm("POP_HL");
                EmitAsm("ADD_HL_DE"); // HL = ptr + (bit_index >> 3)

                AsmOperand maskReady = MakeUniqueLabel("bit_mask_ready");
                AsmOperand maskLoop = MakeUniqueLabel("bit_mask_loop");
                EmitAsm("LD_A_B");
                EmitAsm("OR_A");
                EmitAsm("LD_A_IMM", new AsmOperand(1, AddressMode.Immediate));
                EmitAsm("JR_Z", maskReady);
                EmitLabel(maskLoop);
                EmitAsm("ADD_A");
                EmitAsm("DEC_B");
                EmitAsm("JR_NZ", maskLoop);
                EmitLabel(maskReady);
                EmitAsm("LD_B_A"); // mask

                if (funcName == "__bit_test")
                {
                    AsmOperand retZero = MakeUniqueLabel("bit_test_zero");
                    AsmOperand done = MakeUniqueLabel("bit_test_done");
                    EmitAsm("LD_A_HL");
                    EmitAsm("AND_B");
                    EmitAsm("JR_Z", retZero);
                    EmitAsm("LD_A_IMM", new AsmOperand(1, AddressMode.Immediate));
                    EmitAsm("JR", done);
                    EmitLabel(retZero);
                    EmitAsm("XOR_A");
                    EmitLabel(done);
                    EmitAsm("ADD_SP_IMM", new AsmOperand(4, AddressMode.Relative));
                    return;
                }

                if (funcName == "__bit_set")
                {
                    EmitAsm("LD_A_HL");
                    EmitAsm("OR_B");
                    EmitAsm("LD_HL_A");
                    EmitAsm("ADD_SP_IMM", new AsmOperand(4, AddressMode.Relative));
                    return;
                }

                if (funcName == "__bit_clear")
                {
                    EmitAsm("LD_A_B");
                    EmitAsm("CPL");
                    EmitAsm("LD_B_A");
                    EmitAsm("LD_A_HL");
                    EmitAsm("AND_B");
                    EmitAsm("LD_HL_A");
                    EmitAsm("ADD_SP_IMM", new AsmOperand(4, AddressMode.Relative));
                    return;
                }

                EmitAsm("LD_A_HL");
                EmitAsm("XOR_B");
                EmitAsm("LD_HL_A");
                EmitAsm("ADD_SP_IMM", new AsmOperand(4, AddressMode.Relative));
                return;
            }
__bit_set

将所选标志设为 1,同时保留同一字节中的其他七位。例如,可用于标记任务已完成或物品已收集。对已经为 1 的位再次设置,不会改变它;这不是翻转操作。

语法

c
void __bit_set(u8* p, u16 bit_index);

参数

p

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

bit_index

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

返回值

无(void)。

使用条件与限制

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

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

使用示例

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,说明重复设置不会翻转标志。

GB 画面各行的预期十进制值为:TEST OFF 000、TEST ON 001、SET BIT1 166、CLEAR BIT2 162、TOGGLE BIT7 034、RESTORE 162、NEXT BYTE 130、FAILED 000。NEXT BYTE 来自设置第二个字节中的索引 9,将 0x80 改为 0x82,而不修改字节 0。DMG 使用白底黑字;CGB 使用白底蓝字。

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

__bit_set: DMG
DMG
__bit_set: CGB
CGB

完整示例程序

构建命令

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqgb\kitaqgb.exe .\kitaq-docs\samples\api-examples\gb\bit_intrinsics.c -I .\kitaqgb\lib -I .\kitaq-docs\samples -o .\out\bit_intrinsics.gb --profile=dev --rst-disable --stack-bank=fixed --cgb=cgb --no-cache --no-disasm
实现与源码

kitaqgb/lib/rpg.h:186

kitaqgb/kitaqgb/CodeGenerator.cs:8582

csharp
if (funcName == "__bit_test" || funcName == "__bit_set" || funcName == "__bit_clear" || funcName == "__bit_toggle")
            {
                if (args.Length != 2) Error(expr, funcName + "(ptr, bit_index) expects 2 arguments");

                CompileIntoHL(args[0]); EmitAsm("PUSH_HL"); // ptr
                CompileIntoHL(args[1]); EmitAsm("PUSH_HL"); // bit_index

                EmitAsm("LD_HL_SP_IMM", new AsmOperand(0, AddressMode.Relative));
                EmitAsm("LD_A_HL");
                EmitAsm("AND_IMM", new AsmOperand(7, AddressMode.Immediate));
                EmitAsm("LD_B_A"); // bit number 0..7

                EmitAsm("LD_HL_SP_IMM", new AsmOperand(0, AddressMode.Relative));
                EmitAsm("LD_A_HL");
                EmitAsm("LD_E_A");
                EmitAsm("INC_HL");
                EmitAsm("LD_A_HL");
                EmitAsm("LD_D_A");
                EmitAsm("LD_H_D");
                EmitAsm("LD_L_E");
                EmitShiftRightLogicalHL(3);
                EmitAsm("PUSH_HL"); // byte offset

                EmitAsm("LD_HL_SP_IMM", new AsmOperand(4, AddressMode.Relative));
                EmitAsm("LD_A_HL");
                EmitAsm("LD_E_A");
                EmitAsm("INC_HL");
                EmitAsm("LD_A_HL");
                EmitAsm("LD_D_A");

                EmitAsm("POP_HL");
                EmitAsm("ADD_HL_DE"); // HL = ptr + (bit_index >> 3)

                AsmOperand maskReady = MakeUniqueLabel("bit_mask_ready");
                AsmOperand maskLoop = MakeUniqueLabel("bit_mask_loop");
                EmitAsm("LD_A_B");
                EmitAsm("OR_A");
                EmitAsm("LD_A_IMM", new AsmOperand(1, AddressMode.Immediate));
                EmitAsm("JR_Z", maskReady);
                EmitLabel(maskLoop);
                EmitAsm("ADD_A");
                EmitAsm("DEC_B");
                EmitAsm("JR_NZ", maskLoop);
                EmitLabel(maskReady);
                EmitAsm("LD_B_A"); // mask

                if (funcName == "__bit_test")
                {
                    AsmOperand retZero = MakeUniqueLabel("bit_test_zero");
                    AsmOperand done = MakeUniqueLabel("bit_test_done");
                    EmitAsm("LD_A_HL");
                    EmitAsm("AND_B");
                    EmitAsm("JR_Z", retZero);
                    EmitAsm("LD_A_IMM", new AsmOperand(1, AddressMode.Immediate));
                    EmitAsm("JR", done);
                    EmitLabel(retZero);
                    EmitAsm("XOR_A");
                    EmitLabel(done);
                    EmitAsm("ADD_SP_IMM", new AsmOperand(4, AddressMode.Relative));
                    return;
                }

                if (funcName == "__bit_set")
                {
                    EmitAsm("LD_A_HL");
                    EmitAsm("OR_B");
                    EmitAsm("LD_HL_A");
                    EmitAsm("ADD_SP_IMM", new AsmOperand(4, AddressMode.Relative));
                    return;
                }

                if (funcName == "__bit_clear")
                {
                    EmitAsm("LD_A_B");
                    EmitAsm("CPL");
                    EmitAsm("LD_B_A");
                    EmitAsm("LD_A_HL");
                    EmitAsm("AND_B");
                    EmitAsm("LD_HL_A");
                    EmitAsm("ADD_SP_IMM", new AsmOperand(4, AddressMode.Relative));
                    return;
                }

                EmitAsm("LD_A_HL");
                EmitAsm("XOR_B");
                EmitAsm("LD_HL_A");
                EmitAsm("ADD_SP_IMM", new AsmOperand(4, AddressMode.Relative));
                return;
            }
__bit_test

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

语法

c
u8 __bit_test(const u8* p, u16 bit_index);

参数

p

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

bit_index

从零开始的 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。GB 实现会将掩码运算结果规范化为这两个值。

使用条件与限制

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

使用示例

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。

GB 画面各行的预期十进制值为:TEST OFF 000、TEST ON 001、SET BIT1 166、CLEAR BIT2 162、TOGGLE BIT7 034、RESTORE 162、NEXT BYTE 130、FAILED 000。NEXT BYTE 来自设置第二个字节中的索引 9,将 0x80 改为 0x82,而不修改字节 0。DMG 使用白底黑字;CGB 使用白底蓝字。

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

__bit_test: DMG
DMG
__bit_test: CGB
CGB

完整示例程序

构建命令

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqgb\kitaqgb.exe .\kitaq-docs\samples\api-examples\gb\bit_intrinsics.c -I .\kitaqgb\lib -I .\kitaq-docs\samples -o .\out\bit_intrinsics.gb --profile=dev --rst-disable --stack-bank=fixed --cgb=cgb --no-cache --no-disasm
实现与源码

kitaqgb/lib/rpg.h:185

kitaqgb/kitaqgb/CodeGenerator.cs:8582

csharp
if (funcName == "__bit_test" || funcName == "__bit_set" || funcName == "__bit_clear" || funcName == "__bit_toggle")
            {
                if (args.Length != 2) Error(expr, funcName + "(ptr, bit_index) expects 2 arguments");

                CompileIntoHL(args[0]); EmitAsm("PUSH_HL"); // ptr
                CompileIntoHL(args[1]); EmitAsm("PUSH_HL"); // bit_index

                EmitAsm("LD_HL_SP_IMM", new AsmOperand(0, AddressMode.Relative));
                EmitAsm("LD_A_HL");
                EmitAsm("AND_IMM", new AsmOperand(7, AddressMode.Immediate));
                EmitAsm("LD_B_A"); // bit number 0..7

                EmitAsm("LD_HL_SP_IMM", new AsmOperand(0, AddressMode.Relative));
                EmitAsm("LD_A_HL");
                EmitAsm("LD_E_A");
                EmitAsm("INC_HL");
                EmitAsm("LD_A_HL");
                EmitAsm("LD_D_A");
                EmitAsm("LD_H_D");
                EmitAsm("LD_L_E");
                EmitShiftRightLogicalHL(3);
                EmitAsm("PUSH_HL"); // byte offset

                EmitAsm("LD_HL_SP_IMM", new AsmOperand(4, AddressMode.Relative));
                EmitAsm("LD_A_HL");
                EmitAsm("LD_E_A");
                EmitAsm("INC_HL");
                EmitAsm("LD_A_HL");
                EmitAsm("LD_D_A");

                EmitAsm("POP_HL");
                EmitAsm("ADD_HL_DE"); // HL = ptr + (bit_index >> 3)

                AsmOperand maskReady = MakeUniqueLabel("bit_mask_ready");
                AsmOperand maskLoop = MakeUniqueLabel("bit_mask_loop");
                EmitAsm("LD_A_B");
                EmitAsm("OR_A");
                EmitAsm("LD_A_IMM", new AsmOperand(1, AddressMode.Immediate));
                EmitAsm("JR_Z", maskReady);
                EmitLabel(maskLoop);
                EmitAsm("ADD_A");
                EmitAsm("DEC_B");
                EmitAsm("JR_NZ", maskLoop);
                EmitLabel(maskReady);
                EmitAsm("LD_B_A"); // mask

                if (funcName == "__bit_test")
                {
                    AsmOperand retZero = MakeUniqueLabel("bit_test_zero");
                    AsmOperand done = MakeUniqueLabel("bit_test_done");
                    EmitAsm("LD_A_HL");
                    EmitAsm("AND_B");
                    EmitAsm("JR_Z", retZero);
                    EmitAsm("LD_A_IMM", new AsmOperand(1, AddressMode.Immediate));
                    EmitAsm("JR", done);
                    EmitLabel(retZero);
                    EmitAsm("XOR_A");
                    EmitLabel(done);
                    EmitAsm("ADD_SP_IMM", new AsmOperand(4, AddressMode.Relative));
                    return;
                }

                if (funcName == "__bit_set")
                {
                    EmitAsm("LD_A_HL");
                    EmitAsm("OR_B");
                    EmitAsm("LD_HL_A");
                    EmitAsm("ADD_SP_IMM", new AsmOperand(4, AddressMode.Relative));
                    return;
                }

                if (funcName == "__bit_clear")
                {
                    EmitAsm("LD_A_B");
                    EmitAsm("CPL");
                    EmitAsm("LD_B_A");
                    EmitAsm("LD_A_HL");
                    EmitAsm("AND_B");
                    EmitAsm("LD_HL_A");
                    EmitAsm("ADD_SP_IMM", new AsmOperand(4, AddressMode.Relative));
                    return;
                }

                EmitAsm("LD_A_HL");
                EmitAsm("XOR_B");
                EmitAsm("LD_HL_A");
                EmitAsm("ADD_SP_IMM", new AsmOperand(4, AddressMode.Relative));
                return;
            }
__bit_toggle

翻转所选标志:0 变为 1,1 变为 0。其余位保持不变。若两次调用之间没有其他代码修改该位,对同一位调用两次会恢复原始状态。

语法

c
void __bit_toggle(u8* p, u16 bit_index);

参数

p

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

bit_index

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

返回值

无(void)。

使用条件与限制

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

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

使用示例

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。这两个结果展示了两个方向的翻转,同时保留其他位。

GB 画面各行的预期十进制值为:TEST OFF 000、TEST ON 001、SET BIT1 166、CLEAR BIT2 162、TOGGLE BIT7 034、RESTORE 162、NEXT BYTE 130、FAILED 000。NEXT BYTE 来自设置第二个字节中的索引 9,将 0x80 改为 0x82,而不修改字节 0。DMG 使用白底黑字;CGB 使用白底蓝字。

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

__bit_toggle: DMG
DMG
__bit_toggle: CGB
CGB

完整示例程序

构建命令

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqgb\kitaqgb.exe .\kitaq-docs\samples\api-examples\gb\bit_intrinsics.c -I .\kitaqgb\lib -I .\kitaq-docs\samples -o .\out\bit_intrinsics.gb --profile=dev --rst-disable --stack-bank=fixed --cgb=cgb --no-cache --no-disasm
实现与源码

kitaqgb/lib/rpg.h:188

kitaqgb/kitaqgb/CodeGenerator.cs:8582

csharp
if (funcName == "__bit_test" || funcName == "__bit_set" || funcName == "__bit_clear" || funcName == "__bit_toggle")
            {
                if (args.Length != 2) Error(expr, funcName + "(ptr, bit_index) expects 2 arguments");

                CompileIntoHL(args[0]); EmitAsm("PUSH_HL"); // ptr
                CompileIntoHL(args[1]); EmitAsm("PUSH_HL"); // bit_index

                EmitAsm("LD_HL_SP_IMM", new AsmOperand(0, AddressMode.Relative));
                EmitAsm("LD_A_HL");
                EmitAsm("AND_IMM", new AsmOperand(7, AddressMode.Immediate));
                EmitAsm("LD_B_A"); // bit number 0..7

                EmitAsm("LD_HL_SP_IMM", new AsmOperand(0, AddressMode.Relative));
                EmitAsm("LD_A_HL");
                EmitAsm("LD_E_A");
                EmitAsm("INC_HL");
                EmitAsm("LD_A_HL");
                EmitAsm("LD_D_A");
                EmitAsm("LD_H_D");
                EmitAsm("LD_L_E");
                EmitShiftRightLogicalHL(3);
                EmitAsm("PUSH_HL"); // byte offset

                EmitAsm("LD_HL_SP_IMM", new AsmOperand(4, AddressMode.Relative));
                EmitAsm("LD_A_HL");
                EmitAsm("LD_E_A");
                EmitAsm("INC_HL");
                EmitAsm("LD_A_HL");
                EmitAsm("LD_D_A");

                EmitAsm("POP_HL");
                EmitAsm("ADD_HL_DE"); // HL = ptr + (bit_index >> 3)

                AsmOperand maskReady = MakeUniqueLabel("bit_mask_ready");
                AsmOperand maskLoop = MakeUniqueLabel("bit_mask_loop");
                EmitAsm("LD_A_B");
                EmitAsm("OR_A");
                EmitAsm("LD_A_IMM", new AsmOperand(1, AddressMode.Immediate));
                EmitAsm("JR_Z", maskReady);
                EmitLabel(maskLoop);
                EmitAsm("ADD_A");
                EmitAsm("DEC_B");
                EmitAsm("JR_NZ", maskLoop);
                EmitLabel(maskReady);
                EmitAsm("LD_B_A"); // mask

                if (funcName == "__bit_test")
                {
                    AsmOperand retZero = MakeUniqueLabel("bit_test_zero");
                    AsmOperand done = MakeUniqueLabel("bit_test_done");
                    EmitAsm("LD_A_HL");
                    EmitAsm("AND_B");
                    EmitAsm("JR_Z", retZero);
                    EmitAsm("LD_A_IMM", new AsmOperand(1, AddressMode.Immediate));
                    EmitAsm("JR", done);
                    EmitLabel(retZero);
                    EmitAsm("XOR_A");
                    EmitLabel(done);
                    EmitAsm("ADD_SP_IMM", new AsmOperand(4, AddressMode.Relative));
                    return;
                }

                if (funcName == "__bit_set")
                {
                    EmitAsm("LD_A_HL");
                    EmitAsm("OR_B");
                    EmitAsm("LD_HL_A");
                    EmitAsm("ADD_SP_IMM", new AsmOperand(4, AddressMode.Relative));
                    return;
                }

                if (funcName == "__bit_clear")
                {
                    EmitAsm("LD_A_B");
                    EmitAsm("CPL");
                    EmitAsm("LD_B_A");
                    EmitAsm("LD_A_HL");
                    EmitAsm("AND_B");
                    EmitAsm("LD_HL_A");
                    EmitAsm("ADD_SP_IMM", new AsmOperand(4, AddressMode.Relative));
                    return;
                }

                EmitAsm("LD_A_HL");
                EmitAsm("XOR_B");
                EmitAsm("LD_HL_A");
                EmitAsm("ADD_SP_IMM", new AsmOperand(4, AddressMode.Relative));
                return;
            }
__copy16

无需长度参数,从 src 向 dst 恰好复制 16 字节。例如,可复制 CPU 可访问内存中的一条 16 字节记录,或一个 GB/NES 图块。16 指字节数,不是位数;此调用不会把图像上传到显示硬件。

语法

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

参数

dst

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

src

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

返回值

无(void)。

使用条件与限制

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

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

在 GB 上,这些函数执行普通的 CPU 内存访问,不会等待 VRAM/OAM 的安全访问时机、启动 DMA,或选择 CGB 的 VRAM/WRAM 存储体。这些操作应使用专用图形传输 API。

使用示例

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: DMG
DMG
__copy16: CGB
CGB

完整示例程序

构建命令

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqgb\kitaqgb.exe .\kitaq-docs\samples\api-examples\gb\memory_intrinsics.c -I .\kitaqgb\lib -I .\kitaq-docs\samples -o .\out\memory_intrinsics.gb --profile=dev --rst-disable --stack-bank=fixed --cgb=cgb --no-cache --no-disasm
实现与源码

kitaqgb/lib/rpg.h:177

kitaqgb/kitaqgb/CodeGenerator.cs:8247

csharp
if (funcName == "__copy16" || funcName == "__copy32")
            {
                if (args.Length != 2) Error(expr, funcName + "(dst, src) expects 2 arguments");

                int copyLen = (funcName == "__copy16") ? 16 : 32;
                CompileIntoHL(args[0]); EmitAsm("PUSH_HL"); // dst
                CompileIntoHL(args[1]); EmitAsm("PUSH_HL"); // src
                EmitAsm("LD_BC_IMM", new AsmOperand(copyLen, AddressMode.Immediate16));
                EmitAsm("POP_DE"); // src
                EmitAsm("POP_HL"); // dst
                EmitRamMemcpyLoop(funcName.TrimStart('_'));
                return;
            }

void EmitRamMemcpyLoop(string labelPrefix)
    {
        AsmOperand loop = MakeUniqueLabel(labelPrefix + "_loop");
        AsmOperand done = MakeUniqueLabel(labelPrefix + "_done");

        EmitLabel(loop);
        EmitAsm("LD_A_B");
        EmitAsm("OR_C");
        EmitAsm("JR_Z", done);
        EmitAsm("LD_A_DE");
        EmitAsm("LDI_HL_A");
        EmitAsm("INC_DE");
        EmitAsm("DEC_BC");
        EmitAsm("JR", loop);
        EmitLabel(done);
    }

// kitaqgb/kitaqgb/Lowerer.cs:2145
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(void* dst, const void* src);

参数

dst

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

src

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

返回值

无(void)。

使用条件与限制

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

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

在 GB 上,这些函数执行普通的 CPU 内存访问,不会等待 VRAM/OAM 的安全访问时机、启动 DMA,或选择 CGB 的 VRAM/WRAM 存储体。这些操作应使用专用图形传输 API。

使用示例

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: DMG
DMG
__copy32: CGB
CGB

完整示例程序

构建命令

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqgb\kitaqgb.exe .\kitaq-docs\samples\api-examples\gb\memory_intrinsics.c -I .\kitaqgb\lib -I .\kitaq-docs\samples -o .\out\memory_intrinsics.gb --profile=dev --rst-disable --stack-bank=fixed --cgb=cgb --no-cache --no-disasm
实现与源码

kitaqgb/lib/rpg.h:178

kitaqgb/kitaqgb/CodeGenerator.cs:8247

csharp
if (funcName == "__copy16" || funcName == "__copy32")
            {
                if (args.Length != 2) Error(expr, funcName + "(dst, src) expects 2 arguments");

                int copyLen = (funcName == "__copy16") ? 16 : 32;
                CompileIntoHL(args[0]); EmitAsm("PUSH_HL"); // dst
                CompileIntoHL(args[1]); EmitAsm("PUSH_HL"); // src
                EmitAsm("LD_BC_IMM", new AsmOperand(copyLen, AddressMode.Immediate16));
                EmitAsm("POP_DE"); // src
                EmitAsm("POP_HL"); // dst
                EmitRamMemcpyLoop(funcName.TrimStart('_'));
                return;
            }

void EmitRamMemcpyLoop(string labelPrefix)
    {
        AsmOperand loop = MakeUniqueLabel(labelPrefix + "_loop");
        AsmOperand done = MakeUniqueLabel(labelPrefix + "_done");

        EmitLabel(loop);
        EmitAsm("LD_A_B");
        EmitAsm("OR_C");
        EmitAsm("JR_Z", done);
        EmitAsm("LD_A_DE");
        EmitAsm("LDI_HL_A");
        EmitAsm("INC_DE");
        EmitAsm("DEC_BC");
        EmitAsm("JR", loop);
        EmitLabel(done);
    }

// kitaqgb/kitaqgb/Lowerer.cs:2145
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);
        }
__far_memcpy

临时切换ROM存储体,按地址递增顺序从src向dst复制len字节,然后恢复编译器保存的ROM存储体。在此平台上,__farmemcpy与__far_memcpy使用相同的实现。

语法

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

参数

dst

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

bank

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

src

指向所选ROM存储体中可读字节的近指针,指针本身不含存储体编号。

len

字节数,范围0–65535。为0时不复制内存,但仍会执行存储体选择和恢复。

返回值

无(void)。

使用条件与限制

整个源范围必须在预期ROM映射中可读,整个目标范围必须可写。复制不会在存储体窗口边界自动切换到下一存储体,不提供memmove式的重叠处理,也不控制VRAM安全写入时机。写VRAM请使用图形传送API。

示例将调用代码放在固定存储体0中,防止切换源存储体后正在执行的代码不可见。传送为同步操作。不要在传送中通过中断再次执行切换存储体的复制,也不要认为此辅助函数能保证整个程序的中断安全。

GB辅助例程根据编译器保存的ROM存储体状态恢复映射。请用__bankswitch维护该状态;直接写映射器不会更新它。HRAM中的返回存储体保存位置是共享的,嵌套传送覆盖此位置会造成风险。

使用示例

c
__far_memcpy(buffer,2,asset_shapes,32); // Same backend as __farmemcpy.
expect(*asset_bank_marker==0xA1,29);
for(i=0;i<32;i++) expect(buffer[(__safe_index u8)i]==expected_shape_byte(i),32);
buffer[32]=0x55;
__far_memcpy(buffer+32,2,asset_shapes,0); // Zero count writes no bytes and restores the bank.
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像素。

GB中,(16,64)的蓝色三角形和方框显示__farmemcpy结果,(16,96)的绿色一对显示__far_memcpy结果。每对占16×8像素。CGB显示上述颜色,DMG在白底上以黑色显示相同形状。

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

__far_memcpy: DMG
DMG
__far_memcpy: CGB
CGB

完整示例程序

构建命令

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqgb\kitaqgb.exe .\kitaq-docs\samples\api-examples\gb\asset_banks.c -I .\kitaqgb\lib -I .\kitaq-docs\samples -o .\out\asset_banks.gb --profile=dev --rst-disable --stack-bank=fixed --cgb=cgb --cart=mbc5 --romsize=64k --no-cache --no-disasm
实现与源码

kitaqgb/lib/rpg.h:148

__fill_tilemap

__fill_tilemap是编译器中__vram_memset的别名。长度同样以字节计,使用相同的即时CPU传输及LCD访问规则。

立即用单字节value填充从dst开始的连续len字节。重复的是1字节,不是整个图块图案。填充映射表字节会重复图块编号;填充图案字节则会改变编码后的像素位平面。

语法

c
void __fill_tilemap(u16 dst, u8 value, u16 len);

参数

dst

目标首字节地址,通常位于VRAM 0x8000–0x9FFF。整个范围必须有效。CGB使用当前VRAM存储体;映射表区域的存储体0存放图块编号,存储体1存放属性。这些函数不根据LCDC选择映射表,不在行末换行,也不分配VRAM。

value

重复写入0至255的单字节。显示含义取决于目标是图块索引、像素位平面还是其他字节结构。

len

连续字节数,范围0至65535,不是图块数。整个范围应位于有效源数据和可写目标区域内;API不检查边界,也不防止十六位地址回绕。

返回值

无(void)。

使用条件与限制

必须明确指定起始地址和字节数。此别名既不选择背景映射表,也不自动填充整张表。填充完整32×32图块编号表时,应选择VRAM存储体0,指定表基址和1024字节。

LCD开启时,每次写入前执行DI并等待STAT模式0或1,写入后执行EI。LCD关闭时使用普通CPU循环。调用可能跨越多帧,不保留进入时的中断禁用状态,也不设置DMA寄存器。长度0不传输数据。

使用示例

c
__fill_tilemap(0x9922,1,length_base+length_extra); // Explicit address and byte count, not a whole-map clear.

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

预期结果

本例用__fill_tilemap将图块坐标(2,9)、像素坐标(16,72)开始的3个映射表字节填为图块编号1。应得到24×8实心横条,相邻单元为空白。长度由变量或函数返回值计算;三个字节仍须全部写入值1。

附带的自制图形中,图块1为8×8实心正方形,2只填左侧四列,3为左对齐的三角形,每行加宽1像素,从1像素增至8像素。DMG显示白底黑图形;CGB按操作区分颜色,文字保持黑色。

像素(16,88)的正方形应在安全版和无等待版的零长度调用后保留。LCD开启时,安全复制和填充在(16,96)和(32,96)添加正方形。FAILED CHECKS应为000。模拟器测试同时比较绘制像素和计数值。本例CGB使用存储体0;此处未测试其他存储体或实机。

CGB模式下,从MEMCPY到FILL UNSAFE各行依次循环使用红、蓝、绿。ZERO COUNT正方形为红色;LCD ON左侧正方形为蓝色,右侧为绿色,文字保持黑色。测试还检查每个绘制像素是否为指定颜色。

__fill_tilemap: DMG
DMG
__fill_tilemap: CGB
CGB

完整示例程序

构建命令

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqgb\kitaqgb.exe .\kitaq-docs\samples\api-examples\gb\vram_memory_shapes.c -I .\kitaqgb\lib -I .\kitaq-docs\samples -o .\out\vram_memory_shapes.gb --profile=dev --rst-disable --stack-bank=fixed --cgb=cgb --no-cache --no-disasm
实现与源码

kitaqgb/lib/rpg.h:157

__getbgmapbase

读取 LCDC 的第 3 位,确定背景图块地图基地址。

语法

c
u16 __getbgmapbase();

返回值

返回 $9800 或 $9C00,即图块地图基地址,而不是图块图形地址。

使用示例

c
M_LCDC=0;result[0]=__getbgmapbase();M_LCDC=8;result[1]=__getbgmapbase();

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

预期结果

学习地图选择、滚动、嵌套和扫描行等待。基地址为 38912/39936;嵌套令牌为 0/1;观测到的目标 LY 值为 80/144。同时验证 LCD 关闭时等待函数会返回。

__getbgmapbase: DMG
DMG
__getbgmapbase: CGB
CGB

完整示例程序

构建命令

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqgb\kitaqgb.exe .\kitaq-docs\samples\api-examples\gb\batch3_display.c -I .\kitaqgb\lib -I .\kitaq-docs\samples -o .\out\batch3_display.gb --no-cache --no-disasm --profile=dev --rst-disable --stack-bank=fixed --cgb=cgb --cart=mbc5 --romsize=64k --ramsize=none
实现与源码

kitaqgb/lib/rpg.h:140

kitaqgb/kitaqgb/CodeGenerator.cs:10619

csharp
if (funcName == "__getbgmapbase")
{
    if (args.Length != 0) Error(expr, "__getbgmapbase requires 0 arguments");

    AsmOperand gb_base0 = MakeUniqueLabel("getbgbase_base0");
    AsmOperand gb_done  = MakeUniqueLabel("getbgbase_done");

    EmitAsm("LDH_A_MEM", new AsmOperand(0x40, AddressMode.HighMem)); // LCDC
    EmitAsm("AND_IMM", new AsmOperand(0x08, AddressMode.Immediate)); // bit3 (BG tile map display select)
    EmitAsm("JR_Z", gb_base0);
    EmitAsm("LD_HL_IMM", new AsmOperand(0x9C00, AddressMode.Immediate16));
    EmitAsm("JR", gb_done);
    EmitLabel(gb_base0);
    EmitAsm("LD_HL_IMM", new AsmOperand(0x9800, AddressMode.Immediate16));
    EmitLabel(gb_done);
    LastCallReturnsHL = true;
    return;
}
__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: DMG
DMG
__manhattan: CGB
CGB

完整示例程序

构建命令

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqgb\kitaqgb.exe .\kitaq-docs\samples\api-examples\gb\batch3_coordinates.c -I .\kitaqgb\lib -I .\kitaq-docs\samples -o .\out\batch3_coordinates.gb --no-cache --no-disasm --profile=dev --rst-disable --stack-bank=fixed --cgb=cgb --cart=mbc5 --romsize=64k --ramsize=none
实现与源码

kitaqgb/lib/rpg.h:183

kitaqgb/kitaqgb/CodeGenerator.cs:8516

csharp
if (funcName == "__manhattan")
            {
                if (args.Length != 4) Error(expr, "__manhattan(x1, y1, x2, y2) expects 4 arguments");

                Expr[] foldedManhattanArgs = args.Select(FoldConstants).ToArray();
                if (foldedManhattanArgs[0].Match(Tag.Integer, out int constX1) &&
                    foldedManhattanArgs[1].Match(Tag.Integer, out int constY1) &&
                    foldedManhattanArgs[2].Match(Tag.Integer, out int constX2) &&
                    foldedManhattanArgs[3].Match(Tag.Integer, out int constY2))
                {
                    int dx = Math.Abs((constX1 & 0xFF) - (constX2 & 0xFF));
                    int dy = Math.Abs((constY1 & 0xFF) - (constY2 & 0xFF));
                    EmitAsm("LD_A_IMM", new AsmOperand((dx + dy) & 0xFF, AddressMode.Immediate));
                    return;
                }

                EmitPushExprAsWord(args[0]); // x1
                EmitPushExprAsWord(args[1]); // y1
                EmitPushExprAsWord(args[2]); // x2
                EmitPushExprAsWord(args[3]); // y2

                AsmOperand dxGe = MakeUniqueLabel("manhattan_dx_ge");
                AsmOperand dxDone = MakeUniqueLabel("manhattan_dx_done");
                AsmOperand dyGe = MakeUniqueLabel("manhattan_dy_ge");
                AsmOperand dyDone = MakeUniqueLabel("manhattan_dy_done");

                EmitAsm("LD_HL_SP_IMM", new AsmOperand(6, AddressMode.Relative));
                EmitAsm("LD_A_HL");
                EmitAsm("LD_B_A"); // x1
                EmitAsm("LD_HL_SP_IMM", new AsmOperand(2, AddressMode.Relative));
                EmitAsm("LD_A_HL");
                EmitAsm("LD_C_A"); // x2
                EmitAsm("LD_A_B");
                EmitAsm("CP_C");
                EmitAsm("JR_NC", dxGe);
                EmitAsm("LD_A_C");
                EmitAsm("SUB_B");
                EmitAsm("JR", dxDone);
                EmitLabel(dxGe);
                EmitAsm("LD_A_B");
                EmitAsm("SUB_C");
                EmitLabel(dxDone);
                EmitAsm("LD_B_A"); // dx

                EmitAsm("LD_HL_SP_IMM", new AsmOperand(4, AddressMode.Relative));
                EmitAsm("LD_A_HL");
                EmitAsm("LD_C_A"); // y1
                EmitAsm("LD_HL_SP_IMM", new AsmOperand(0, AddressMode.Relative));
                EmitAsm("LD_A_HL");
                EmitAsm("LD_D_A"); // y2
                EmitAsm("LD_A_C");
                EmitAsm("CP_D");
                EmitAsm("JR_NC", dyGe);
                EmitAsm("LD_A_D");
                EmitAsm("SUB_C");
                EmitAsm("JR", dyDone);
                EmitLabel(dyGe);
                EmitAsm("LD_A_C");
                EmitAsm("SUB_D");
                EmitLabel(dyDone);
                EmitAsm("ADD_B");
                EmitAsm("ADD_SP_IMM", new AsmOperand(8, AddressMode.Relative));
                return;
            }
__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: DMG
DMG
__map_index: CGB
CGB

完整示例程序

构建命令

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqgb\kitaqgb.exe .\kitaq-docs\samples\api-examples\gb\batch3_coordinates.c -I .\kitaqgb\lib -I .\kitaq-docs\samples -o .\out\batch3_coordinates.gb --no-cache --no-disasm --profile=dev --rst-disable --stack-bank=fixed --cgb=cgb --cart=mbc5 --romsize=64k --ramsize=none
实现与源码

kitaqgb/lib/rpg.h:181

kitaqgb/kitaqgb/CodeGenerator.cs:8365

csharp
if (funcName == "__map_index")
            {
                if (args.Length != 3) Error(expr, "__map_index(x, y, width) expects 3 arguments");

                Expr xExpr = FoldConstants(args[0]);
                Expr yExpr = FoldConstants(args[1]);
                Expr widthExpr = FoldConstants(args[2]);
                // Apply the width parameter's byte conversion before choosing a shift specialization.
                if (widthExpr.Match(Tag.Integer, out int byteWidth))
                    widthExpr = Expr.Make(Tag.Integer, byteWidth & 0xFF).WithSource(widthExpr.Source);
                if (xExpr.Match(Tag.Integer, out int fullXConst) &&
                    yExpr.Match(Tag.Integer, out int fullYConst) &&
                    widthExpr.Match(Tag.Integer, out int fullWidthConst))
                {
                    // Apply the byte argument widths before calculating the word-sized constant index.
                    int fullIndexConst = (((fullYConst & 0xFF) * (fullWidthConst & 0xFF)) + (fullXConst & 0xFF)) & 0xFFFF;
                    EmitAsm("LD_HL_IMM", new AsmOperand(fullIndexConst, AddressMode.Immediate16));
                    LastCallReturnsHL = true;
                    return;
                }

                if (widthExpr.Match(Tag.Integer, out int widthConst) && widthConst > 0 && IsPow2(widthConst))
                {
                    CompileIntoA(yExpr);
                    EmitAsm("LD_L_A");
                    EmitAsm("LD_H_IMM", new AsmOperand(0, AddressMode.Immediate));
                    int sh = Log2Pow2(widthConst);
                    for (int i = 0; i < sh; i++) EmitAsm("ADD_HL_HL");

                    if (xExpr.Match(Tag.Integer, out int xConst) && xConst == 0)
                    {
                        LastCallReturnsHL = true;
                        return;
                    }

                    CompileIntoA(xExpr);
                    EmitAsm("LD_E_A");
                    EmitAsm("LD_D_IMM", new AsmOperand(0, AddressMode.Immediate));
                    EmitAsm("ADD_HL_DE");
                    LastCallReturnsHL = true;
                    return;
                }

                CompileIntoA(args[0]); EmitPushAAsWord(); // x
                CompileIntoA(args[1]); EmitPushAAsWord(); // y
                CompileIntoA(args[2]); EmitPushAAsWord(); // width

                EmitAsm("LD_HL_SP_IMM", new AsmOperand(0, AddressMode.Relative));
                EmitAsm("LD_A_HL");
                EmitAsm("LD_C_A"); // width

                EmitAsm("LD_HL_SP_IMM", new AsmOperand(2, AddressMode.Relative));
                EmitAsm("LD_A_HL");
                EmitAsm("LD_E_A"); // y

                EmitMul8x8ToHL_EC("__map_index");

                // Loading the saved X argument uses HL, so preserve the multiplication result first.
                EmitAsm("PUSH_HL");
                EmitAsm("LD_HL_SP_IMM", new AsmOperand(6, AddressMode.Relative));
                EmitAsm("LD_A_HL");
                EmitAsm("LD_E_A");
                EmitAsm("LD_D_IMM", new AsmOperand(0, AddressMode.Immediate));
                EmitAsm("POP_HL");
                EmitAsm("ADD_HL_DE");
                EmitAsm("ADD_SP_IMM", new AsmOperand(6, AddressMode.Relative));
                LastCallReturnsHL = true;
                return;
            }
__memcpy

从 src 向 dst 复制指定字节数。长度使用 16 位数值,可用于复制超过 255 字节的数据,例如 RAM 中的整张表。读写按地址递增顺序进行。

语法

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

参数

dst

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

src

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

count

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

返回值

无(void)。

使用条件与限制

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

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

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

在 GB 上,这些函数执行普通的 CPU 内存访问,不会等待 VRAM/OAM 的安全访问时机、启动 DMA,或选择 CGB 的 VRAM/WRAM 存储体。这些操作应使用专用图形传输 API。

使用示例

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: DMG
DMG
__memcpy: CGB
CGB

完整示例程序

构建命令

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqgb\kitaqgb.exe .\kitaq-docs\samples\api-examples\gb\memory_intrinsics.c -I .\kitaqgb\lib -I .\kitaq-docs\samples -o .\out\memory_intrinsics.gb --profile=dev --rst-disable --stack-bank=fixed --cgb=cgb --no-cache --no-disasm
实现与源码

kitaqgb/lib/rpg.h:142

kitaqgb/kitaqgb/CodeGenerator.cs:9084

csharp
if (funcName == "__memcpy" || funcName == "__memcpy_small")
            {
                // Select byte or word count semantics before optional fixed-array bounds diagnostics.
                bool useU8Count = (funcName == "__memcpy_small");
                if (args.Length != 3) { Error(expr, "__memcpy requires 3 arguments"); return; }

                bool doCheck = Program.CheckMemCopy && !InUnsafe;
                bool hasDst = false, hasSrc = false;
                int dstBytes = 0, srcBytes = 0;
                string dstName = null, srcName = null;
                if (doCheck)
                {
                    hasDst = TryGetFixedArrayByteLength(args[0], out dstBytes, out dstName);
                    hasSrc = TryGetFixedArrayByteLength(args[1], out srcBytes, out srcName);
                }

                bool isConstCountAny = args[2].Match(Tag.Integer, out int constCountAny);
                int normalizedConstCount = constCountAny;
                if (isConstCountAny && useU8Count)
                    normalizedConstCount &= 0xFF;

                if (doCheck && isConstCountAny)
                {
                    if (hasDst && normalizedConstCount >= 0 && normalizedConstCount > dstBytes)
                    {
                        Program.Warning(args[2].Source, "__memcpy count {0} exceeds fixed dst array '{1}' size {2} bytes (-Zcheck)", normalizedConstCount, dstName, dstBytes);
                        EmitAsm("JP", EnsureCheckTrap());
                        return;
                    }
                    if (hasSrc && normalizedConstCount >= 0 && normalizedConstCount > srcBytes)
                    {
                        Program.Warning(args[2].Source, "__memcpy count {0} exceeds fixed src array '{1}' size {2} bytes (-Zcheck)", normalizedConstCount, srcName, srcBytes);
                        EmitAsm("JP", EnsureCheckTrap());
                        return;
                    }
                }

                if (isConstCountAny && normalizedConstCount >= 0 && normalizedConstCount <= 16)
                {
                    if (normalizedConstCount == 0) return;

                    CompileIntoHL(args[0]);
                    EmitAsm("PUSH_HL");
                    CompileIntoHL(args[1]);
                    EmitAsm("PUSH_HL");
                    EmitAsm("POP_DE");
                    EmitAsm("POP_HL");

                    for (int k = 0; k < normalizedConstCount; k++)
                    {
                        EmitAsm("LD_A_DE");
                        EmitAsm("LDI_HL_A");
                        EmitAsm("INC_DE");
                    }
                    return;
                }

                if (isConstCountAny && normalizedConstCount > 16 && normalizedConstCount <= 255)
                {
                    CompileIntoHL(args[0]);
                    EmitAsm("PUSH_HL");
                    CompileIntoHL(args[1]);
                    EmitAsm("PUSH_HL");
                    EmitAsm("POP_DE");
                    EmitAsm("POP_HL");

                    if (normalizedConstCount <= 64)
                    {
                        int blocks8 = normalizedConstCount / 8;
                        int rem8 = normalizedConstCount % 8;
                        if (blocks8 > 0)
                        {
                            EmitAsm("LD_B_IMM", new AsmOperand(blocks8, AddressMode.Immediate));
                            // Advance source and destination through eight-byte groups, then emit the remaining constant bytes.
                            AsmOperand loop8 = MakeUniqueLabel("memcpy8_loop");
                            EmitLabel(loop8);
                            for (int k = 0; k < 8; k++)
                            {
                                EmitAsm("LD_A_DE");
                                EmitAsm("LDI_HL_A");
                                EmitAsm("INC_DE");
                            }
                            EmitAsm("DEC_B");
                            EmitAsm("JP_NZ", loop8);
                        }
                        for (int k = 0; k < rem8; k++)
                        {
                            EmitAsm("LD_A_DE");
                            EmitAsm("LDI_HL_A");
                            EmitAsm("INC_DE");
                        }
                        return;
                    }

                    int blocks16 = normalizedConstCount / 16;
                    int rem16 = normalizedConstCount % 16;
                    if (blocks16 > 0)
                    {
                        EmitAsm("LD_B_IMM", new AsmOperand(blocks16, AddressMode.Immediate));
                        // Use sixteen-byte groups while keeping the residual copy length known at compile time.
                        AsmOperand loop16 = MakeUniqueLabel("memcpy16_loop");
                        EmitLabel(loop16);
                        for (int k = 0; k < 16; k++)
                        {
                            EmitAsm("LD_A_DE");
                            EmitAsm("LDI_HL_A");
                            EmitAsm("INC_DE");
                        }
                        EmitAsm("DEC_B");
                        EmitAsm("JP_NZ", loop16);
                    }
                    for (int k = 0; k < rem16; k++)
                    {
                        EmitAsm("LD_A_DE");
                        EmitAsm("LDI_HL_A");
                        EmitAsm("INC_DE");
                    }
                    return;
                }

                if (useU8Count)
                {
                    CompileIntoHL(args[0]);
                    EmitAsm("PUSH_HL");

                    CompileIntoHL(args[1]);
                    EmitAsm("PUSH_HL");

                    CompileIntoA(args[2]);
                    EmitAsm("LD_B_A");
                    if (doCheck)
                    {
                        if (hasDst) EmitU8LeqConstOrTrapInA(dstBytes);
                        if (hasSrc) EmitU8LeqConstOrTrapInA(srcBytes);
                    }

                    AsmOperand doneLabel = MakeUniqueLabel("memcpy_small_done");
                    AsmOperand smallLoopLabel = MakeUniqueLabel("memcpy_small_loop");
                    EmitAsm("OR_A");
                    EmitAsm("POP_DE");
                    EmitAsm("POP_HL");
                    EmitAsm("JP_Z", doneLabel);

                    EmitLabel(smallLoopLabel);
                    EmitAsm("LD_A_DE");
                    EmitAsm("LDI_HL_A");
                    EmitAsm("INC_DE");
                    EmitAsm("DEC_B");
                    EmitAsm("JP_NZ", smallLoopLabel);
                    EmitLabel(doneLabel);
                    return;
                }

                CompileIntoHL(args[0]);
                EmitAsm("PUSH_HL");

                CompileIntoHL(args[1]);
                EmitAsm("PUSH_HL");

                CompileIntoHL(args[2]);
                if (doCheck && !isConstCountAny)
                {
                    if (hasDst) EmitU16LeqConstOrTrap(dstBytes);
                    if (hasSrc) EmitU16LeqConstOrTrap(srcBytes);
                }
                EmitAsm("LD_B_H");
                EmitAsm("LD_C_L");

                EmitAsm("POP_DE");
                EmitAsm("POP_HL");

                AsmOperand loopLabel = MakeUniqueLabel("memcpy_loop");
                AsmOperand wordDoneLabel = MakeUniqueLabel("memcpy_done");
                // A zero word count must skip the first write as well as the loop.
                EmitAsm("LD_A_B");
                EmitAsm("OR_C");
                EmitAsm("JP_Z", wordDoneLabel);
                EmitLabel(loopLabel);

                EmitAsm("LD_A_DE");
                EmitAsm("LDI_HL_A");
                EmitAsm("INC_DE");

                EmitAsm("DEC_BC");
                EmitAsm("LD_A_B");
                EmitAsm("OR_C");
                EmitAsm("JP_NZ", loopLabel);
                EmitLabel(wordDoneLabel);

                return;
            }

// kitaqgb/kitaqgb/Lowerer.cs:2145
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(void* dst, const void* src, u8 len);

参数

dst

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

src

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

len

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

返回值

无(void)。

使用条件与限制

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

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

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

在 GB 上,这些函数执行普通的 CPU 内存访问,不会等待 VRAM/OAM 的安全访问时机、启动 DMA,或选择 CGB 的 VRAM/WRAM 存储体。这些操作应使用专用图形传输 API。

使用示例

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: DMG
DMG
__memcpy_small: CGB
CGB

完整示例程序

构建命令

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqgb\kitaqgb.exe .\kitaq-docs\samples\api-examples\gb\memory_intrinsics.c -I .\kitaqgb\lib -I .\kitaq-docs\samples -o .\out\memory_intrinsics.gb --profile=dev --rst-disable --stack-bank=fixed --cgb=cgb --no-cache --no-disasm
实现与源码

kitaqgb/lib/rpg.h:175

kitaqgb/kitaqgb/CodeGenerator.cs:9084

csharp
if (funcName == "__memcpy" || funcName == "__memcpy_small")
            {
                // Select byte or word count semantics before optional fixed-array bounds diagnostics.
                bool useU8Count = (funcName == "__memcpy_small");
                if (args.Length != 3) { Error(expr, "__memcpy requires 3 arguments"); return; }

                bool doCheck = Program.CheckMemCopy && !InUnsafe;
                bool hasDst = false, hasSrc = false;
                int dstBytes = 0, srcBytes = 0;
                string dstName = null, srcName = null;
                if (doCheck)
                {
                    hasDst = TryGetFixedArrayByteLength(args[0], out dstBytes, out dstName);
                    hasSrc = TryGetFixedArrayByteLength(args[1], out srcBytes, out srcName);
                }

                bool isConstCountAny = args[2].Match(Tag.Integer, out int constCountAny);
                int normalizedConstCount = constCountAny;
                if (isConstCountAny && useU8Count)
                    normalizedConstCount &= 0xFF;

                if (doCheck && isConstCountAny)
                {
                    if (hasDst && normalizedConstCount >= 0 && normalizedConstCount > dstBytes)
                    {
                        Program.Warning(args[2].Source, "__memcpy count {0} exceeds fixed dst array '{1}' size {2} bytes (-Zcheck)", normalizedConstCount, dstName, dstBytes);
                        EmitAsm("JP", EnsureCheckTrap());
                        return;
                    }
                    if (hasSrc && normalizedConstCount >= 0 && normalizedConstCount > srcBytes)
                    {
                        Program.Warning(args[2].Source, "__memcpy count {0} exceeds fixed src array '{1}' size {2} bytes (-Zcheck)", normalizedConstCount, srcName, srcBytes);
                        EmitAsm("JP", EnsureCheckTrap());
                        return;
                    }
                }

                if (isConstCountAny && normalizedConstCount >= 0 && normalizedConstCount <= 16)
                {
                    if (normalizedConstCount == 0) return;

                    CompileIntoHL(args[0]);
                    EmitAsm("PUSH_HL");
                    CompileIntoHL(args[1]);
                    EmitAsm("PUSH_HL");
                    EmitAsm("POP_DE");
                    EmitAsm("POP_HL");

                    for (int k = 0; k < normalizedConstCount; k++)
                    {
                        EmitAsm("LD_A_DE");
                        EmitAsm("LDI_HL_A");
                        EmitAsm("INC_DE");
                    }
                    return;
                }

                if (isConstCountAny && normalizedConstCount > 16 && normalizedConstCount <= 255)
                {
                    CompileIntoHL(args[0]);
                    EmitAsm("PUSH_HL");
                    CompileIntoHL(args[1]);
                    EmitAsm("PUSH_HL");
                    EmitAsm("POP_DE");
                    EmitAsm("POP_HL");

                    if (normalizedConstCount <= 64)
                    {
                        int blocks8 = normalizedConstCount / 8;
                        int rem8 = normalizedConstCount % 8;
                        if (blocks8 > 0)
                        {
                            EmitAsm("LD_B_IMM", new AsmOperand(blocks8, AddressMode.Immediate));
                            // Advance source and destination through eight-byte groups, then emit the remaining constant bytes.
                            AsmOperand loop8 = MakeUniqueLabel("memcpy8_loop");
                            EmitLabel(loop8);
                            for (int k = 0; k < 8; k++)
                            {
                                EmitAsm("LD_A_DE");
                                EmitAsm("LDI_HL_A");
                                EmitAsm("INC_DE");
                            }
                            EmitAsm("DEC_B");
                            EmitAsm("JP_NZ", loop8);
                        }
                        for (int k = 0; k < rem8; k++)
                        {
                            EmitAsm("LD_A_DE");
                            EmitAsm("LDI_HL_A");
                            EmitAsm("INC_DE");
                        }
                        return;
                    }

                    int blocks16 = normalizedConstCount / 16;
                    int rem16 = normalizedConstCount % 16;
                    if (blocks16 > 0)
                    {
                        EmitAsm("LD_B_IMM", new AsmOperand(blocks16, AddressMode.Immediate));
                        // Use sixteen-byte groups while keeping the residual copy length known at compile time.
                        AsmOperand loop16 = MakeUniqueLabel("memcpy16_loop");
                        EmitLabel(loop16);
                        for (int k = 0; k < 16; k++)
                        {
                            EmitAsm("LD_A_DE");
                            EmitAsm("LDI_HL_A");
                            EmitAsm("INC_DE");
                        }
                        EmitAsm("DEC_B");
                        EmitAsm("JP_NZ", loop16);
                    }
                    for (int k = 0; k < rem16; k++)
                    {
                        EmitAsm("LD_A_DE");
                        EmitAsm("LDI_HL_A");
                        EmitAsm("INC_DE");
                    }
                    return;
                }

                if (useU8Count)
                {
                    CompileIntoHL(args[0]);
                    EmitAsm("PUSH_HL");

                    CompileIntoHL(args[1]);
                    EmitAsm("PUSH_HL");

                    CompileIntoA(args[2]);
                    EmitAsm("LD_B_A");
                    if (doCheck)
                    {
                        if (hasDst) EmitU8LeqConstOrTrapInA(dstBytes);
                        if (hasSrc) EmitU8LeqConstOrTrapInA(srcBytes);
                    }

                    AsmOperand doneLabel = MakeUniqueLabel("memcpy_small_done");
                    AsmOperand smallLoopLabel = MakeUniqueLabel("memcpy_small_loop");
                    EmitAsm("OR_A");
                    EmitAsm("POP_DE");
                    EmitAsm("POP_HL");
                    EmitAsm("JP_Z", doneLabel);

                    EmitLabel(smallLoopLabel);
                    EmitAsm("LD_A_DE");
                    EmitAsm("LDI_HL_A");
                    EmitAsm("INC_DE");
                    EmitAsm("DEC_B");
                    EmitAsm("JP_NZ", smallLoopLabel);
                    EmitLabel(doneLabel);
                    return;
                }

                CompileIntoHL(args[0]);
                EmitAsm("PUSH_HL");

                CompileIntoHL(args[1]);
                EmitAsm("PUSH_HL");

                CompileIntoHL(args[2]);
                if (doCheck && !isConstCountAny)
                {
                    if (hasDst) EmitU16LeqConstOrTrap(dstBytes);
                    if (hasSrc) EmitU16LeqConstOrTrap(srcBytes);
                }
                EmitAsm("LD_B_H");
                EmitAsm("LD_C_L");

                EmitAsm("POP_DE");
                EmitAsm("POP_HL");

                AsmOperand loopLabel = MakeUniqueLabel("memcpy_loop");
                AsmOperand wordDoneLabel = MakeUniqueLabel("memcpy_done");
                // A zero word count must skip the first write as well as the loop.
                EmitAsm("LD_A_B");
                EmitAsm("OR_C");
                EmitAsm("JP_Z", wordDoneLabel);
                EmitLabel(loopLabel);

                EmitAsm("LD_A_DE");
                EmitAsm("LDI_HL_A");
                EmitAsm("INC_DE");

                EmitAsm("DEC_BC");
                EmitAsm("LD_A_B");
                EmitAsm("OR_C");
                EmitAsm("JP_NZ", loopLabel);
                EmitLabel(wordDoneLabel);

                return;
            }

// kitaqgb/kitaqgb/Lowerer.cs:2145
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(void* dst, u8 value, u16 count);

参数

dst

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

value

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

count

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

返回值

无(void)。

使用条件与限制

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

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

在 GB 上,这些函数执行普通的 CPU 内存访问,不会等待 VRAM/OAM 的安全访问时机、启动 DMA,或选择 CGB 的 VRAM/WRAM 存储体。这些操作应使用专用图形传输 API。

使用示例

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: DMG
DMG
__memset: CGB
CGB

完整示例程序

构建命令

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqgb\kitaqgb.exe .\kitaq-docs\samples\api-examples\gb\memory_intrinsics.c -I .\kitaqgb\lib -I .\kitaq-docs\samples -o .\out\memory_intrinsics.gb --profile=dev --rst-disable --stack-bank=fixed --cgb=cgb --no-cache --no-disasm
实现与源码

kitaqgb/lib/rpg.h:141

kitaqgb/kitaqgb/CodeGenerator.cs:8926

csharp
if (funcName == "__memset" || funcName == "__memset_small")
            {
                // The small API truncates its count to a byte; the general API uses a word count.
                bool useU8Count = (funcName == "__memset_small");
                if (args.Length != 3) { Error(expr, "__memset requires 3 arguments"); return; }

                // -Zcheck: fixed-array range check for memset length
                bool doCheck = Program.CheckMemCopy && !InUnsafe;
                bool hasDst = false;
                int dstBytes = 0;
                string dstName = null;
                if (doCheck)
                    hasDst = TryGetFixedArrayByteLength(args[0], out dstBytes, out dstName);

                bool isConstCountAny = args[2].Match(Tag.Integer, out int constCountAny);
                int normalizedConstCount = constCountAny;
                if (isConstCountAny && useU8Count)
                    normalizedConstCount &= 0xFF;

                if (doCheck && hasDst && isConstCountAny && normalizedConstCount >= 0 && normalizedConstCount > dstBytes)
                {
                    Program.Warning(args[2].Source, "__memset count {0} exceeds fixed array '{1}' size {2} bytes (-Zcheck)", normalizedConstCount, dstName, dstBytes);
                    EmitAsm("JP", EnsureCheckTrap());
                    return;
                }

                // Size-specialized memset for small constant counts (<=16): unrolled stores.
                if (isConstCountAny && normalizedConstCount >= 0 && normalizedConstCount <= 16)
                {
                    if (normalizedConstCount == 0) return;

                    CompileIntoHL(args[0]);
                    EmitAsm("PUSH_HL");
                    CompileIntoA(args[1]);
                    EmitAsm("POP_HL");

                    for (int k = 0; k < normalizedConstCount; k++)
                        EmitAsm("LDI_HL_A");
                    return;
                }

                // Size-specialized memset for constant counts beyond 16
                if (isConstCountAny && normalizedConstCount >= 0)
                {
                    if (normalizedConstCount == 0) return;

                    if (normalizedConstCount > 16 && normalizedConstCount <= 64)
                    {
                        int blocks8 = normalizedConstCount / 8;
                        int rem = normalizedConstCount % 8;

                        CompileIntoHL(args[0]);
                        EmitAsm("PUSH_HL");
                        CompileIntoA(args[1]);
                        EmitAsm("POP_HL");

                        if (blocks8 > 0)
                        {
                            EmitAsm("LD_B_IMM", new AsmOperand(blocks8, AddressMode.Immediate));
                            // Emit groups of eight stores followed by the constant remainder for medium fills.
                            AsmOperand loop8 = MakeUniqueLabel("memset8_loop");
                            EmitLabel(loop8);
                            for (int k = 0; k < 8; k++) EmitAsm("LDI_HL_A");
                            EmitAsm("DEC_B");
                            EmitAsm("JP_NZ", loop8);
                        }
                        for (int k = 0; k < rem; k++) EmitAsm("LDI_HL_A");
                        return;
                    }

                    if (normalizedConstCount <= 255)
                    {
                        int blocks16 = normalizedConstCount / 16;
                        int rem = normalizedConstCount % 16;

                        CompileIntoHL(args[0]);
                        EmitAsm("PUSH_HL");
                        CompileIntoA(args[1]);
                        EmitAsm("POP_HL");

                        if (blocks16 > 0)
                        {
                            EmitAsm("LD_B_IMM", new AsmOperand(blocks16, AddressMode.Immediate));
                            // Use sixteen-store groups for larger byte-range constant fills.
                            AsmOperand loop16 = MakeUniqueLabel("memset16_loop");
                            EmitLabel(loop16);
                            for (int k = 0; k < 16; k++) EmitAsm("LDI_HL_A");
                            EmitAsm("DEC_B");
                            EmitAsm("JP_NZ", loop16);
                        }
                        for (int k = 0; k < rem; k++) EmitAsm("LDI_HL_A");
                        return;
                    }
                }

                if (useU8Count)
                {
                    CompileIntoHL(args[0]);
                    EmitAsm("PUSH_HL");

                    CompileIntoA(args[1]);
                    EmitAsm("LD_D_A");

                    CompileIntoA(args[2]);
                    EmitAsm("LD_B_A");
                    if (doCheck && hasDst)
                        EmitU8LeqConstOrTrapInA(dstBytes);

                    AsmOperand doneLabel = MakeUniqueLabel("memset_small_done");
                    AsmOperand smallLoopLabel = MakeUniqueLabel("memset_small_loop");
                    EmitAsm("OR_A");
                    EmitAsm("POP_HL");
                    EmitAsm("JP_Z", doneLabel);
                    EmitAsm("LD_A_D");
                    EmitLabel(smallLoopLabel);
                    EmitAsm("LDI_HL_A");
                    EmitAsm("DEC_B");
                    EmitAsm("JP_NZ", smallLoopLabel);
                    EmitLabel(doneLabel);
                    return;
                }

                CompileIntoHL(args[0]);
                EmitAsm("PUSH_HL");

                CompileIntoHL(args[2]);
                if (doCheck && hasDst && !isConstCountAny)
                    EmitU16LeqConstOrTrap(dstBytes);
                EmitAsm("LD_B_H");
                EmitAsm("LD_C_L");

                CompileIntoA(args[1]);
                EmitAsm("LD_D_A");

                EmitAsm("POP_HL");

                AsmOperand loopLabel = MakeUniqueLabel("memset_loop");
                AsmOperand wordDoneLabel = MakeUniqueLabel("memset_done");
                // A zero word count must skip the first write as well as the loop.
                EmitAsm("LD_A_B");
                EmitAsm("OR_C");
                EmitAsm("JP_Z", wordDoneLabel);
                EmitLabel(loopLabel);

                EmitAsm("LD_A_D");
                EmitAsm("LDI_HL_A");

                EmitAsm("DEC_BC");
                EmitAsm("LD_A_B");
                EmitAsm("OR_C");
                EmitAsm("JP_NZ", loopLabel);
                EmitLabel(wordDoneLabel);

                return;
            }

// kitaqgb/kitaqgb/Lowerer.cs:2145
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(void* dst, u8 value, u8 len);

参数

dst

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

value

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

len

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

返回值

无(void)。

使用条件与限制

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

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

在 GB 上,这些函数执行普通的 CPU 内存访问,不会等待 VRAM/OAM 的安全访问时机、启动 DMA,或选择 CGB 的 VRAM/WRAM 存储体。这些操作应使用专用图形传输 API。

使用示例

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: DMG
DMG
__memset_small: CGB
CGB

完整示例程序

构建命令

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqgb\kitaqgb.exe .\kitaq-docs\samples\api-examples\gb\memory_intrinsics.c -I .\kitaqgb\lib -I .\kitaq-docs\samples -o .\out\memory_intrinsics.gb --profile=dev --rst-disable --stack-bank=fixed --cgb=cgb --no-cache --no-disasm
实现与源码

kitaqgb/lib/rpg.h:176

kitaqgb/kitaqgb/CodeGenerator.cs:8926

csharp
if (funcName == "__memset" || funcName == "__memset_small")
            {
                // The small API truncates its count to a byte; the general API uses a word count.
                bool useU8Count = (funcName == "__memset_small");
                if (args.Length != 3) { Error(expr, "__memset requires 3 arguments"); return; }

                // -Zcheck: fixed-array range check for memset length
                bool doCheck = Program.CheckMemCopy && !InUnsafe;
                bool hasDst = false;
                int dstBytes = 0;
                string dstName = null;
                if (doCheck)
                    hasDst = TryGetFixedArrayByteLength(args[0], out dstBytes, out dstName);

                bool isConstCountAny = args[2].Match(Tag.Integer, out int constCountAny);
                int normalizedConstCount = constCountAny;
                if (isConstCountAny && useU8Count)
                    normalizedConstCount &= 0xFF;

                if (doCheck && hasDst && isConstCountAny && normalizedConstCount >= 0 && normalizedConstCount > dstBytes)
                {
                    Program.Warning(args[2].Source, "__memset count {0} exceeds fixed array '{1}' size {2} bytes (-Zcheck)", normalizedConstCount, dstName, dstBytes);
                    EmitAsm("JP", EnsureCheckTrap());
                    return;
                }

                // Size-specialized memset for small constant counts (<=16): unrolled stores.
                if (isConstCountAny && normalizedConstCount >= 0 && normalizedConstCount <= 16)
                {
                    if (normalizedConstCount == 0) return;

                    CompileIntoHL(args[0]);
                    EmitAsm("PUSH_HL");
                    CompileIntoA(args[1]);
                    EmitAsm("POP_HL");

                    for (int k = 0; k < normalizedConstCount; k++)
                        EmitAsm("LDI_HL_A");
                    return;
                }

                // Size-specialized memset for constant counts beyond 16
                if (isConstCountAny && normalizedConstCount >= 0)
                {
                    if (normalizedConstCount == 0) return;

                    if (normalizedConstCount > 16 && normalizedConstCount <= 64)
                    {
                        int blocks8 = normalizedConstCount / 8;
                        int rem = normalizedConstCount % 8;

                        CompileIntoHL(args[0]);
                        EmitAsm("PUSH_HL");
                        CompileIntoA(args[1]);
                        EmitAsm("POP_HL");

                        if (blocks8 > 0)
                        {
                            EmitAsm("LD_B_IMM", new AsmOperand(blocks8, AddressMode.Immediate));
                            // Emit groups of eight stores followed by the constant remainder for medium fills.
                            AsmOperand loop8 = MakeUniqueLabel("memset8_loop");
                            EmitLabel(loop8);
                            for (int k = 0; k < 8; k++) EmitAsm("LDI_HL_A");
                            EmitAsm("DEC_B");
                            EmitAsm("JP_NZ", loop8);
                        }
                        for (int k = 0; k < rem; k++) EmitAsm("LDI_HL_A");
                        return;
                    }

                    if (normalizedConstCount <= 255)
                    {
                        int blocks16 = normalizedConstCount / 16;
                        int rem = normalizedConstCount % 16;

                        CompileIntoHL(args[0]);
                        EmitAsm("PUSH_HL");
                        CompileIntoA(args[1]);
                        EmitAsm("POP_HL");

                        if (blocks16 > 0)
                        {
                            EmitAsm("LD_B_IMM", new AsmOperand(blocks16, AddressMode.Immediate));
                            // Use sixteen-store groups for larger byte-range constant fills.
                            AsmOperand loop16 = MakeUniqueLabel("memset16_loop");
                            EmitLabel(loop16);
                            for (int k = 0; k < 16; k++) EmitAsm("LDI_HL_A");
                            EmitAsm("DEC_B");
                            EmitAsm("JP_NZ", loop16);
                        }
                        for (int k = 0; k < rem; k++) EmitAsm("LDI_HL_A");
                        return;
                    }
                }

                if (useU8Count)
                {
                    CompileIntoHL(args[0]);
                    EmitAsm("PUSH_HL");

                    CompileIntoA(args[1]);
                    EmitAsm("LD_D_A");

                    CompileIntoA(args[2]);
                    EmitAsm("LD_B_A");
                    if (doCheck && hasDst)
                        EmitU8LeqConstOrTrapInA(dstBytes);

                    AsmOperand doneLabel = MakeUniqueLabel("memset_small_done");
                    AsmOperand smallLoopLabel = MakeUniqueLabel("memset_small_loop");
                    EmitAsm("OR_A");
                    EmitAsm("POP_HL");
                    EmitAsm("JP_Z", doneLabel);
                    EmitAsm("LD_A_D");
                    EmitLabel(smallLoopLabel);
                    EmitAsm("LDI_HL_A");
                    EmitAsm("DEC_B");
                    EmitAsm("JP_NZ", smallLoopLabel);
                    EmitLabel(doneLabel);
                    return;
                }

                CompileIntoHL(args[0]);
                EmitAsm("PUSH_HL");

                CompileIntoHL(args[2]);
                if (doCheck && hasDst && !isConstCountAny)
                    EmitU16LeqConstOrTrap(dstBytes);
                EmitAsm("LD_B_H");
                EmitAsm("LD_C_L");

                CompileIntoA(args[1]);
                EmitAsm("LD_D_A");

                EmitAsm("POP_HL");

                AsmOperand loopLabel = MakeUniqueLabel("memset_loop");
                AsmOperand wordDoneLabel = MakeUniqueLabel("memset_done");
                // A zero word count must skip the first write as well as the loop.
                EmitAsm("LD_A_B");
                EmitAsm("OR_C");
                EmitAsm("JP_Z", wordDoneLabel);
                EmitLabel(loopLabel);

                EmitAsm("LD_A_D");
                EmitAsm("LDI_HL_A");

                EmitAsm("DEC_BC");
                EmitAsm("LD_A_B");
                EmitAsm("OR_C");
                EmitAsm("JP_NZ", loopLabel);
                EmitLabel(wordDoneLabel);

                return;
            }

// kitaqgb/kitaqgb/Lowerer.cs:2145
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);
        }
__rle_decode_vram

将带存储体编号的数量/数值流直接展开到 VRAM,不使用中间 RAM 输出缓冲。编译器生成循环,读取每组数据,通过考虑 LCD 访问时机的写入函数填充,最后返回输出字节数。适合载入压缩图块或按字节存储的背景图数据。

语法

c
u16 __rle_decode_vram(u16 dst, u8 bank, const void* src);

参数

dst

首个输出字节的 16 位 CPU 地址,范围为 0x8000~0x9FFF。须预留连续空间,末尾不得超过 0x9FFF。例如,在无符号图块编号布局中,0x8860 是图块 134。函数不分配图块,也不检查目标范围。

bank

u8 类型的源 ROM 存储体编号。编号及 CPU 地址必须与卡带映射器和素材布局一致。每次读取编码字节后会恢复存储体选择。此参数不选择目标 VRAM 存储体,也不能表示 MBC5 的九位存储体编号。

src

指定存储体中压缩流的 CPU 指针。函数会递增地址,但存储体编号不变。包括结束标记在内的完整数据必须位于可读映射范围中;越过 0x7FFF 不会自动转到下一个 ROM 存储体。

返回值

以 u16 返回写入的字节数。首个数量为零时不输出数据,返回 0;结束标记不会被复制。展开长度须能用 u16 表示。返回值没有独立错误码,也不是图块数量。

使用条件与限制

此格式面向字节,不是文本。例如 {3, 0x12, 2, 0xA5, 0} 展开为五字节:12 12 12 A5 A5,这里的输出以十六进制表示。非零数量之后的数值零属于普通数据,不是结束标记。

接口没有压缩长度、输出容量或错误数据参数。生成或载入素材时应验证格式,提供数量零的结束标记,并预先计算展开长度。无效或未终止的数据可能导致源读取越界和目标写入越界。

LCD 关闭时直接写入;开启时,每个输出字节都等待可写的 LCD 模式,并用 DI/EI 包围等待和写入。长数据可能跨越多个帧,本调用是同步执行,不是排队传输。LCD 开启路径会在每字节后启用中断,不保留调用前禁用中断的状态。

CGB 写入当前 VBK 选择的存储体,并保持该选择不变。示例图块数据应选择 VBK 0。即使输出选择 VBK 1,bank 参数仍表示 ROM 输入存储体。解码器不管理 CGB 调色板或背景图属性。

读取带存储体编号的数据期间,不要让中断程序改变 ROM 映射或重用远地址复制辅助函数,因为这些操作共享存储体管理状态。普通指针解码函数及源数据必须处于兼容的映射中,切换到函数所在存储体时不能使源指针失效。

rle_decode 和 rle_decode_far 在 rpg.h 中声明,在 lib/rle.c 中实现;游戏应只编译一次实现。完整示例在 #pragma bank 0 后包含 rle.c,使调用方和解码器都位于公共存储体;对此示例不要再次把 rle.c 作为独立输入。__rle_decode_vram 内建函数本身无需库实现文件。

使用示例

c
vram_count=__rle_decode_vram(0x8860,2,banked_rle); // Expand directly to 48 bytes of tile data.
if(*rle_bank_marker!=0xA1)failures++;

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

预期结果

本例将存储体 2 的数据直接展开到 VRAM 地址 0x8860,不使用 RAM 输出缓冲。DIRECT VRAM 应显示 048。最下排在 CGB 上为蓝色,三个图案应与通过 RAM 上传的两排一致。教学程序在 LCD 关闭时上传。

每排从左到右分别为 8×8 实心方块、单像素宽边框的 8×8 空心方框,以及占图块左半边的 4×8 实心矩形。图块列为 2、5、8,对应像素 X=16、40、64;三排起始 Y 为 32、72、112。DMG 使用黑色图形,CGB 三排依次为红、绿、蓝。FAILED 必须为 000。数值表示字节数:三个 16 字节 GB 图块共需展开 48 字节。

现有验证矩阵包含 42 个用例,覆盖空流、单字节数据段、数值零、255 字节数据段、跨目标页的 510 字节输出,以及跨源页的编码。检查全部 528 个观测目标/保护字节、输出长度和恢复后的 ROM 映射。直接 VRAM 用例包括 DMG、CGB 的 LCD 开/关及 CGB VBK 1。两个教学画面在模拟器运行 240 帧后检查。验证不涵盖无效素材、中断并发调用或实机。

__rle_decode_vram: DMG
DMG
__rle_decode_vram: CGB
CGB

完整示例程序

构建命令

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqgb\kitaqgb.exe .\kitaq-docs\samples\api-examples\gb\rle_shapes.c -I .\kitaqgb\lib -I .\kitaq-docs\samples -o .\out\rle_shapes.gb --profile=dev --rst-disable --stack-bank=fixed --cgb=cgb --cart=mbc5 --romsize=64k --no-cache --no-disasm
实现与源码

kitaqgb/lib/rpg.h:191

声明注释(原文)
Compiler intrinsic, separate from the RAM rle_decode helpers below; it is not a bounded-stream API.

kitaqgb/kitaqgb/CodeGenerator.cs:8673

csharp
if (funcName == "__rle_decode_vram")
            {
                if (args.Length != 3) Error(expr, "__rle_decode_vram(dst, bank, src) expects 3 arguments");

                CompileIntoHL(args[0]); EmitAsm("PUSH_HL"); // dst
                CompileIntoA(args[1]); EmitPushAAsWord(); // bank
                CompileIntoHL(args[2]); EmitAsm("PUSH_HL"); // src
                EmitAsm("ADD_SP_IMM", new AsmOperand(-4, AddressMode.Relative)); // total(2) + scratch(2)

                EmitAsm("LD_HL_SP_IMM", new AsmOperand(0, AddressMode.Relative));
                EmitAsm("XOR_A");
                EmitAsm("LD_HL_A");
                EmitAsm("INC_HL");
                EmitAsm("LD_HL_A");

                AsmOperand loop = MakeUniqueLabel("rlevram_loop");
                // Keep total, scratch, source, bank and destination in stack slots across repeated far-memory calls.
                AsmOperand countDone = MakeUniqueLabel("rlevram_count_inc_done");
                AsmOperand valueDone = MakeUniqueLabel("rlevram_value_inc_done");
                AsmOperand finished = MakeUniqueLabel("rlevram_finished");

                EmitLabel(loop);

                EmitAsm("LD_HL_SP_IMM", new AsmOperand(2, AddressMode.Relative));
                EmitAsm("PUSH_HL"); // &scratch

                EmitAsm("LD_HL_SP_IMM", new AsmOperand(6, AddressMode.Relative));
                EmitAsm("LD_A_HL");
                EmitAsm("LD_E_A");
                EmitAsm("INC_HL");
                EmitAsm("LD_A_HL");
                EmitAsm("LD_D_A"); // src

                EmitAsm("LD_HL_SP_IMM", new AsmOperand(8, AddressMode.Relative));
                EmitAsm("LD_A_HL"); // bank

                EmitAsm("POP_HL"); // &scratch
                EmitAsm("LD_BC_IMM", new AsmOperand(1, AddressMode.Immediate16));
                EmitCallFarMemcpyHelper();

                EmitAsm("LD_HL_SP_IMM", new AsmOperand(2, AddressMode.Relative));
                EmitAsm("LD_A_HL"); // count
                EmitAsm("OR_A");
                EmitAsm("JP_Z", finished);
                EmitAsm("LD_HL_SP_IMM", new AsmOperand(3, AddressMode.Relative));
                EmitAsm("LD_HL_A"); // save count in scratch+1

                EmitAsm("LD_HL_SP_IMM", new AsmOperand(4, AddressMode.Relative));
                EmitAsm("LD_A_HL");
                EmitAsm("ADD_A_IMM", new AsmOperand(1, AddressMode.Immediate));
                EmitAsm("LD_HL_A");
                EmitAsm("JR_NC", countDone);
                EmitAsm("INC_HL");
                EmitAsm("LD_A_HL");
                EmitAsm("ADC_IMM", new AsmOperand(0, AddressMode.Immediate));
                EmitAsm("LD_HL_A");
                EmitLabel(countDone);

                EmitAsm("LD_HL_SP_IMM", new AsmOperand(2, AddressMode.Relative));
                EmitAsm("PUSH_HL"); // &scratch

                EmitAsm("LD_HL_SP_IMM", new AsmOperand(6, AddressMode.Relative));
                EmitAsm("LD_A_HL");
                EmitAsm("LD_E_A");
                EmitAsm("INC_HL");
                EmitAsm("LD_A_HL");
                EmitAsm("LD_D_A"); // src

                EmitAsm("LD_HL_SP_IMM", new AsmOperand(8, AddressMode.Relative));
                EmitAsm("LD_A_HL"); // bank

                EmitAsm("POP_HL"); // &scratch
                EmitAsm("LD_BC_IMM", new AsmOperand(1, AddressMode.Immediate16));
                EmitCallFarMemcpyHelper();

                EmitAsm("LD_HL_SP_IMM", new AsmOperand(2, AddressMode.Relative));
                EmitAsm("LD_A_HL");
                EmitAsm("LD_D_A"); // value

                EmitAsm("LD_HL_SP_IMM", new AsmOperand(3, AddressMode.Relative));
                EmitAsm("LD_A_HL");
                EmitAsm("LD_C_A");
                EmitAsm("LD_B_IMM", new AsmOperand(0, AddressMode.Immediate)); // count

                EmitAsm("LD_HL_SP_IMM", new AsmOperand(8, AddressMode.Relative));
                EmitAsm("LD_A_HL");
                EmitAsm("LD_E_A");
                EmitAsm("INC_HL");
                EmitAsm("LD_A_HL");
                EmitAsm("LD_D_A");
                EmitAsm("LD_H_D");
                EmitAsm("LD_L_E"); // dst
                // Loading dst used D for its high byte. Recover the run value
                // from stack scratch while preserving the completed HL pointer.
                EmitAsm("PUSH_HL");
                EmitAsm("LD_HL_SP_IMM", new AsmOperand(4, AddressMode.Relative));
                EmitAsm("LD_A_HL");
                EmitAsm("LD_D_A");
                EmitAsm("POP_HL");

                // Use the LCD-aware fill loop for each decoded run, then store its advanced destination pointer.
                EmitVramMemsetLoop(true, "rlevram_fill");

                EmitAsm("PUSH_HL");
                EmitAsm("LD_HL_SP_IMM", new AsmOperand(10, AddressMode.Relative));
                EmitAsm("POP_DE");
                EmitAsm("LD_A_E");
                EmitAsm("LD_HL_A");
                EmitAsm("INC_HL");
                EmitAsm("LD_A_D");
                EmitAsm("LD_HL_A"); // save dst

                EmitAsm("LD_HL_SP_IMM", new AsmOperand(3, AddressMode.Relative));
                EmitAsm("LD_A_HL");
                EmitAsm("LD_B_A"); // count
                EmitAsm("LD_HL_SP_IMM", new AsmOperand(0, AddressMode.Relative));
                EmitAsm("LD_A_HL");
                EmitAsm("ADD_B");
                EmitAsm("LD_HL_A");
                EmitAsm("INC_HL");
                EmitAsm("LD_A_HL");
                EmitAsm("ADC_IMM", new AsmOperand(0, AddressMode.Immediate));
                EmitAsm("LD_HL_A"); // total += count

                EmitAsm("LD_HL_SP_IMM", new AsmOperand(4, AddressMode.Relative));
                EmitAsm("LD_A_HL");
                EmitAsm("ADD_A_IMM", new AsmOperand(1, AddressMode.Immediate));
                EmitAsm("LD_HL_A");
                EmitAsm("JR_NC", valueDone);
                EmitAsm("INC_HL");
                EmitAsm("LD_A_HL");
                EmitAsm("ADC_IMM", new AsmOperand(0, AddressMode.Immediate));
                EmitAsm("LD_HL_A");
                EmitLabel(valueDone);

                EmitAsm("JP", loop);

                EmitLabel(finished);
                EmitAsm("LD_HL_SP_IMM", new AsmOperand(0, AddressMode.Relative));
                EmitAsm("LD_A_HL");
                EmitAsm("LD_E_A");
                EmitAsm("INC_HL");
                EmitAsm("LD_A_HL");
                EmitAsm("LD_D_A");
                EmitAsm("LD_H_D");
                EmitAsm("LD_L_E");
                EmitAsm("ADD_SP_IMM", new AsmOperand(10, AddressMode.Relative));
                LastCallReturnsHL = true;
                return;
            }
__rng8

将共享 GB 伪随机生成器推进一次,返回新的高字节。无需链接 rng.c 封装函数、只需一个字节时,可用此内建函数。

语法

c
u8 __rng8();

返回值

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

使用条件与限制

GB 的一步运算先给 16 位状态加 0x5D17,溢出回绕;再将更新后的低字节与高字节异或,随后将该低字节加到循环左移一位的高字节上。保存更新后的两个字节,并返回高字节。

接受零状态。下一次取字节时,GB 生成器先替换为 0x5AA5 再推进,因此 __rng_seed(0) 后调用 __rng8() 返回 122。反复设置种子只会重新开始序列,不会增加随机性。

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

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

使用库函数时,包含 rpg.h,并将 lib/rng.c 与游戏一起编译。__rng_seed 和 __rng8 内建函数由编译器生成;只使用这两个函数的程序无需 rng.c 目标代码。

使用示例

c
__rng_seed(0x1234);
first=__rng8();  // 2: the first draw from this seed on GB.
second=__rng8(); // 34: a second draw advances the same shared state.

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

预期结果

示例展示每次取值都会推进序列。设置种子 0x1234 后,前两次 __rng8 显示 FIRST 00002、SECOND 00034。重复设置种子后从 00002 重新开始。

完整程序在白底上打印十进制结果:DMG 为黑字,CGB 为蓝字。FAILED 必须为 00000。图像展示该示例的实际模拟器输出。

验证将每个案例的 32 个输出及随后八次字节取值与独立宿主模型比较:GB 93 个案例、FC 18 个案例,覆盖常量/运行时种子及 GB 范围、概率和权重边界。教学画面在模拟器运行 240 帧后检查。未测试实体硬件或统计质量。

__rng8: DMG
DMG
__rng8: CGB
CGB

完整示例程序

构建命令

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqgb\kitaqgb.exe .\kitaq-docs\samples\api-examples\gb\rng_values.c .\kitaqgb\lib\rng.c -I .\kitaqgb\lib -I .\kitaq-docs\samples -o .\out\rng_values.gb --profile=dev --rst-disable --stack-bank=fixed --cgb=cgb --no-cache --no-disasm
实现与源码

kitaqgb/lib/rpg.h:149

kitaqgb/kitaqgb/CodeGenerator.cs:8881

csharp
if (funcName == "__rng8")
            {
                if (args.Length != 0) Error(expr, "__rng8() expects 0 arguments");

                EmitLoadA(RngStateLoVar);
                EmitAsm("LD_B_A");
                EmitLoadA(RngStateHiVar);
                EmitAsm("LD_C_A");

                // Zero state fallback for deterministic startup.
                AsmOperand rngSeeded = MakeUniqueLabel("rng_seeded");
                EmitAsm("LD_A_B");
                EmitAsm("OR_C");
                EmitAsm("JR_NZ", rngSeeded);
                EmitAsm("LD_A_IMM", new AsmOperand(0xA5, AddressMode.Immediate));
                EmitAsm("LD_B_A");
                EmitAsm("LD_A_IMM", new AsmOperand(0x5A, AddressMode.Immediate));
                EmitAsm("LD_C_A");
                EmitLabel(rngSeeded);

                // 16-bit mixed add/xor/rotate step.
                EmitAsm("LD_A_B");
                EmitAsm("ADD_A_IMM", new AsmOperand(0x17, AddressMode.Immediate));
                EmitAsm("LD_B_A");
                EmitAsm("LD_A_C");
                EmitAsm("ADC_IMM", new AsmOperand(0x5D, AddressMode.Immediate));
                EmitAsm("LD_C_A");
                EmitAsm("LD_A_B");
                EmitAsm("XOR_C");
                EmitAsm("LD_B_A");
                EmitAsm("LD_A_C");
                EmitAsm("RLCA");
                EmitAsm("ADD_B");
                EmitAsm("LD_C_A");

                EmitAsm("LD_A_B");
                EmitStoreA(RngStateLoVar);
                EmitAsm("LD_A_C");
                EmitStoreA(RngStateHiVar);
                EmitAsm("LD_A_C");
                return;
            }
__rng_seed

替换共享 GB 生成器的 16 位状态。调试时若需重放相同序列,可使用固定种子。设置种子本身不生成随机值。

语法

c
void __rng_seed(u16 seed);

参数

seed

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

返回值

无(void)。

使用条件与限制

接受零状态。下一次取字节时,GB 生成器先替换为 0x5AA5 再推进,因此 __rng_seed(0) 后调用 __rng8() 返回 122。反复设置种子只会重新开始序列,不会增加随机性。

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

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

使用库函数时,包含 rpg.h,并将 lib/rng.c 与游戏一起编译。__rng_seed 和 __rng8 内建函数由编译器生成;只使用这两个函数的程序无需 rng.c 目标代码。

使用示例

c
__rng_seed(0x1234);
repeated=__rng8(); // Reseeding reproduces the first byte, 2.

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

预期结果

示例演示重置测试序列。种子为 0x1234 时,FIRST 和 RESEEDED 均显示 00002;SECOND 显示 00034。库的种子函数重置同一状态,因此 RNG8 也显示 00002。

完整程序在白底上打印十进制结果:DMG 为黑字,CGB 为蓝字。FAILED 必须为 00000。图像展示该示例的实际模拟器输出。

验证将每个案例的 32 个输出及随后八次字节取值与独立宿主模型比较:GB 93 个案例、FC 18 个案例,覆盖常量/运行时种子及 GB 范围、概率和权重边界。教学画面在模拟器运行 240 帧后检查。未测试实体硬件或统计质量。

__rng_seed: DMG
DMG
__rng_seed: CGB
CGB

完整示例程序

构建命令

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqgb\kitaqgb.exe .\kitaq-docs\samples\api-examples\gb\rng_values.c .\kitaqgb\lib\rng.c -I .\kitaqgb\lib -I .\kitaq-docs\samples -o .\out\rng_values.gb --profile=dev --rst-disable --stack-bank=fixed --cgb=cgb --no-cache --no-disasm
实现与源码

kitaqgb/lib/rpg.h:150

kitaqgb/kitaqgb/CodeGenerator.cs:8868

csharp
if (funcName == "__rng_seed")
            {
                if (args.Length != 1) Error(expr, "__rng_seed(seed) expects 1 argument");

                CompileIntoHL(args[0]);
                EmitAsm("LD_A_L");
                EmitStoreA(RngStateLoVar);
                EmitAsm("LD_A_H");
                EmitStoreA(RngStateHiVar);
                return;
            }
__scroll_bg_set

立即写入背景 SCX/SCY,并同步软件维护的当前值与待提交值。

语法

c
void __scroll_bg_set(u8 scx, u8 scy);

参数

scx

水平滚动量,范围为 0..255 像素。

scy

垂直滚动量,范围为 0..255 像素。

返回值

无返回值。

使用条件与限制

清除背景待更新标记。在帧中途写入会影响后续扫描;通常应在 VBlank 期间调用。

使用示例

c
__scroll_bg_set(19,27);result[5]=*((u8*)0xFF43);result[6]=*((u8*)0xFF42);__scroll_bg_set(0,0);

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

预期结果

学习地图选择、滚动、嵌套和扫描行等待。基地址为 38912/39936;嵌套令牌为 0/1;观测到的目标 LY 值为 80/144。同时验证 LCD 关闭时等待函数会返回。

__scroll_bg_set: DMG
DMG
__scroll_bg_set: CGB
CGB

完整示例程序

构建命令

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqgb\kitaqgb.exe .\kitaq-docs\samples\api-examples\gb\batch3_display.c -I .\kitaqgb\lib -I .\kitaq-docs\samples -o .\out\batch3_display.gb --no-cache --no-disasm --profile=dev --rst-disable --stack-bank=fixed --cgb=cgb --cart=mbc5 --romsize=64k --ramsize=none
实现与源码

kitaqgb/lib/rpg.h:151

kitaqgb/kitaqgb/CodeGenerator.cs:7507

csharp
if (funcName == "__scroll_bg_set")
            {
                if (args.Length != 2) Error(expr, "__scroll_bg_set(scx, scy) expects 2 arguments");
                EnsureScrollState();

                CompileIntoA(args[0]);
                EmitPushAAsWord();
                CompileIntoA(args[1]);
                EmitAsm("LD_B_A");
                EmitAsm("POP_HL");

                EmitAsm("LD_A_L");
                EmitStoreA(ScrollBgXCurVar);
                EmitStoreA(ScrollBgXNextVar);
                EmitAsm("LDH_MEM_A", new AsmOperand(0x43, AddressMode.HighMem)); // SCX

                EmitAsm("LD_A_B");
                EmitStoreA(ScrollBgYCurVar);
                EmitStoreA(ScrollBgYNextVar);
                EmitAsm("LDH_MEM_A", new AsmOperand(0x42, AddressMode.HighMem)); // SCY

                EmitLoadA(ScrollDirtyVar);
                EmitAsm("AND_IMM", new AsmOperand(0xFF ^ ScrollDirtyBgMask, AddressMode.Immediate));
                EmitStoreA(ScrollDirtyVar);
                return;
            }
__settile_col

将 src 中连续的 len 个图块编号,从 LCDC 所选背景地图的 (x,y) 起向下复制。源地址每次增加一字节,目标地址增加 32 字节。

必要时等待 VRAM 可写。LCD 开启时的路径会屏蔽中断,并可能在结束后启用中断;不保证保留调用前的中断状态,也不保证在一次 VBlank 内完成。

语法

c
void __settile_col(u8 x, u8 y, const u8* src, u8 len);

参数

x, y

以 8×8 图块为单位的坐标,各为 0~31。整个写入范围必须位于 32×32 地图内;不会自动裁剪坐标。

src

位于 RAM 或当前可见 ROM bank 中的连续字节。图块流保存图块编号,属性流保存 CGB 属性字节。函数立即复制,不保留指针。

len

字节数,范围 0~255。为 0 时不复制;源数据至少要有这么多可读字节。

返回值

无(void)。

使用条件与限制

图块编号写入当前选中的 VRAM 库。CGB 上应选择编号所在的库 0;此调用不会代为切换库。

横向复制应满足 x+len <= 32,纵向复制应满足 y+len <= 32。不会在地图边缘自动回绕坐标。

示例使用 gb_common.h。m_init() 将附带 ASCII 字体加载到与字符编码相同的图块编号。m_wait(); M_LCDC=0; 在 VBlank 中关闭 LCD,以便进行无需等待的传输。最后的 LCDC 值选择显示地图。

使用示例

c
// __settile_col
// Copy `len` successive tile numbers from `src` vertically, starting at `(x,y)` in the
// LCDC-selected BG map. The source advances one byte and the destination 32 bytes per
// tile. Waits for writable VRAM when necessary. The LCD-on path uses interrupt masking
// and can enable interrupts afterward; it is not a promise to preserve the caller's
// interrupt state or to finish within one VBlank.
// Expected: FAILED CHECKS 000, with the written tile data in the selected map.
#include "gb_tile_color_example.h"
const u8 example_tiles[] = {'A','B','C'};

void main() {
    u8 failures;
    failures = 0;
    tile_color_example_begin();
    __settile_col(2, 3, example_tiles, 3);
    if (tile_example_read(0x9C62, 0) != 'A') { failures++; }
    if (tile_example_read(0x9CA2, 0) != 'C') { failures++; }
    if (tile_example_read(0x9C63, 0) != 0) { failures++; }
    tile_example_finish(failures, 1);
}

预期结果

所选地图显示写入的字符或填充区域。程序读回目标图块编号,并在 CGB 上读回属性进行比较;FAILED CHECKS 000 表示比较全部通过。两张地图初始均为 0,调用前将背景/窗口地图选为 0x9C00,因此能区分固定地图与所选地图的写入。示例在 LCD 关闭时写入,不测量 LCD 开启时的时序。

在 CGB 上,示例字符和结果文字以白底蓝字显示。初始化时将调色板 0 的绘制颜色设为蓝色,不修改图块映射的属性。在 DMG 上,相同字符显示为白底黑字。

预期图像:以下图块排列从图块坐标 (x=2, y=3)、像素坐标 (16,24) 开始显示。每个字符占一个 8×8 图块。按从上到下、从左到右阅读,周围图块保持空白。

text
A
B
C
__settile_col: DMG
DMG
__settile_col: CGB
CGB

完整示例程序

构建命令

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqgb\kitaqgb.exe .\kitaq-docs\samples\api-examples\gb\__settile_col.c -I .\kitaqgb\lib -I .\kitaq-docs\samples -o .\out\__settile_col.gb --profile=dev --rst-disable --stack-bank=fixed --cgb=cgb --no-disasm
实现与源码

kitaqgb/lib/rpg.h:156

__settile_rect

用同一个图块编号填充 LCDC 所选背景地图中宽 w、高 h 的矩形。行间距为 32 字节;这是重复铺放同一图块,而非复制图像数组。

必要时等待 VRAM 可写。LCD 开启时的路径会屏蔽中断,并可能在结束后启用中断;不保证保留调用前的中断状态,也不保证在一次 VBlank 内完成。

语法

c
void __settile_rect(u8 x, u8 y, u8 w, u8 h, u8 tile);

参数

x, y

以 8×8 图块为单位的坐标,各为 0~31。整个写入范围必须位于 32×32 地图内;不会自动裁剪坐标。

w, h

以 8×8 图块为单位的宽和高。须满足 x+w <= 32、y+h <= 32。任一维为 0 时不写入。

tile

0~255 的图块编号。LCDC 位 4 决定编号如何寻址图块图像数据;这不是 VRAM 字节地址。

返回值

无(void)。

使用条件与限制

图块编号写入当前选中的 VRAM 库。CGB 上应选择编号所在的库 0;此调用不会代为切换库。

示例使用 gb_common.h。m_init() 将附带 ASCII 字体加载到与字符编码相同的图块编号。m_wait(); M_LCDC=0; 在 VBlank 中关闭 LCD,以便进行无需等待的传输。最后的 LCDC 值选择显示地图。

使用示例

c
// __settile_rect
// Fill a `w` by `h` rectangle in the LCDC-selected BG map with one tile number. Rows are
// 32 bytes apart; this repeats a tile, rather than copying an image array. Waits for
// writable VRAM when necessary. The LCD-on path uses interrupt masking and can enable
// interrupts afterward; it is not a promise to preserve the caller's interrupt state or
// to finish within one VBlank.
// Expected: FAILED CHECKS 000, with the written tile data in the selected map.
#include "gb_tile_color_example.h"

void main() {
    u8 failures;
    failures = 0;
    tile_color_example_begin();
    __settile_rect(2, 3, 3, 2, 'A');
    if (tile_example_read(0x9C62, 0) != 'A') { failures++; }
    if (tile_example_read(0x9C84, 0) != 'A') { failures++; }
    if (tile_example_read(0x9C65, 0) != 0) { failures++; }
    tile_example_finish(failures, 1);
}

预期结果

所选地图显示写入的字符或填充区域。程序读回目标图块编号,并在 CGB 上读回属性进行比较;FAILED CHECKS 000 表示比较全部通过。两张地图初始均为 0,调用前将背景/窗口地图选为 0x9C00,因此能区分固定地图与所选地图的写入。示例在 LCD 关闭时写入,不测量 LCD 开启时的时序。

在 CGB 上,示例字符和结果文字以白底蓝字显示。初始化时将调色板 0 的绘制颜色设为蓝色,不修改图块映射的属性。在 DMG 上,相同字符显示为白底黑字。

预期图像:以下图块排列从图块坐标 (x=2, y=3)、像素坐标 (16,24) 开始显示。每个字符占一个 8×8 图块。按从上到下、从左到右阅读,周围图块保持空白。

text
AAA
AAA
__settile_rect: DMG
DMG
__settile_rect: CGB
CGB

完整示例程序

构建命令

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqgb\kitaqgb.exe .\kitaq-docs\samples\api-examples\gb\__settile_rect.c -I .\kitaqgb\lib -I .\kitaq-docs\samples -o .\out\__settile_rect.gb --profile=dev --rst-disable --stack-bank=fixed --cgb=cgb --no-disasm
实现与源码

kitaqgb/lib/rpg.h:154

__settile_row

将 src 中连续的 len 个图块编号,从 LCDC 所选背景地图的 (x,y) 起向右复制。每复制一个图块,源和目标地址各增加一字节。

必要时等待 VRAM 可写。LCD 开启时的路径会屏蔽中断,并可能在结束后启用中断;不保证保留调用前的中断状态,也不保证在一次 VBlank 内完成。

语法

c
void __settile_row(u8 x, u8 y, const u8* src, u8 len);

参数

x, y

以 8×8 图块为单位的坐标,各为 0~31。整个写入范围必须位于 32×32 地图内;不会自动裁剪坐标。

src

位于 RAM 或当前可见 ROM bank 中的连续字节。图块流保存图块编号,属性流保存 CGB 属性字节。函数立即复制,不保留指针。

len

字节数,范围 0~255。为 0 时不复制;源数据至少要有这么多可读字节。

返回值

无(void)。

使用条件与限制

图块编号写入当前选中的 VRAM 库。CGB 上应选择编号所在的库 0;此调用不会代为切换库。

横向复制应满足 x+len <= 32,纵向复制应满足 y+len <= 32。不会在地图边缘自动回绕坐标。

示例使用 gb_common.h。m_init() 将附带 ASCII 字体加载到与字符编码相同的图块编号。m_wait(); M_LCDC=0; 在 VBlank 中关闭 LCD,以便进行无需等待的传输。最后的 LCDC 值选择显示地图。

使用示例

c
// __settile_row
// Copy `len` successive tile numbers from `src` horizontally, starting at `(x,y)` in the
// LCDC-selected BG map. Source and destination advance by one byte per tile. Waits for
// writable VRAM when necessary. The LCD-on path uses interrupt masking and can enable
// interrupts afterward; it is not a promise to preserve the caller's interrupt state or
// to finish within one VBlank.
// Expected: FAILED CHECKS 000, with the written tile data in the selected map.
#include "gb_tile_color_example.h"
const u8 example_tiles[] = {'A','B','C'};

void main() {
    u8 failures;
    failures = 0;
    tile_color_example_begin();
    __settile_row(2, 3, example_tiles, 3);
    if (tile_example_read(0x9C62, 0) != 'A') { failures++; }
    if (tile_example_read(0x9C64, 0) != 'C') { failures++; }
    if (tile_example_read(0x9C82, 0) != 0) { failures++; }
    tile_example_finish(failures, 1);
}

预期结果

所选地图显示写入的字符或填充区域。程序读回目标图块编号,并在 CGB 上读回属性进行比较;FAILED CHECKS 000 表示比较全部通过。两张地图初始均为 0,调用前将背景/窗口地图选为 0x9C00,因此能区分固定地图与所选地图的写入。示例在 LCD 关闭时写入,不测量 LCD 开启时的时序。

在 CGB 上,示例字符和结果文字以白底蓝字显示。初始化时将调色板 0 的绘制颜色设为蓝色,不修改图块映射的属性。在 DMG 上,相同字符显示为白底黑字。

预期图像:以下图块排列从图块坐标 (x=2, y=3)、像素坐标 (16,24) 开始显示。每个字符占一个 8×8 图块。按从上到下、从左到右阅读,周围图块保持空白。

text
ABC
__settile_row: DMG
DMG
__settile_row: CGB
CGB

完整示例程序

构建命令

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqgb\kitaqgb.exe .\kitaq-docs\samples\api-examples\gb\__settile_row.c -I .\kitaqgb\lib -I .\kitaq-docs\samples -o .\out\__settile_row.gb --profile=dev --rst-disable --stack-bank=fixed --cgb=cgb --no-disasm
实现与源码

kitaqgb/lib/rpg.h:155

__settile_xy

向图块地图的一个单元写入图块编号。这是在选择已有图块图像,不会修改像素数据。

写入 LCDC 位 3 当前选择的背景地图:0 对应 0x9800,1 对应 0x9C00,再加上 32*y + x。

必要时等待 VRAM 可写。LCD 开启时的路径会屏蔽中断,并可能在结束后启用中断;不保证保留调用前的中断状态,也不保证在一次 VBlank 内完成。

语法

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

参数

x, y

以 8×8 图块为单位的坐标,各为 0~31。整个写入范围必须位于 32×32 地图内;不会自动裁剪坐标。

tile

0~255 的图块编号。LCDC 位 4 决定编号如何寻址图块图像数据;这不是 VRAM 字节地址。

返回值

无(void)。

使用条件与限制

图块编号写入当前选中的 VRAM 库。CGB 上应选择编号所在的库 0;此调用不会代为切换库。

示例使用 gb_common.h。m_init() 将附带 ASCII 字体加载到与字符编码相同的图块编号。m_wait(); M_LCDC=0; 在 VBlank 中关闭 LCD,以便进行无需等待的传输。最后的 LCDC 值选择显示地图。

使用示例

c
// __settile_xy
// Write one tile-number byte to the tile map; this selects an existing tile image, not
// new pixel data. LCDC bit 3 selects the background map: zero uses `0x9800`, one uses
// `0x9C00`. The destination is that base plus `32*y + x`.
// Expected: FAILED CHECKS 000, with the written tile data in the selected map.
#include "gb_tile_color_example.h"

void main() {
    u8 failures;
    failures = 0;
    tile_color_example_begin();
    __settile_xy(2, 3, 'A');
    if (tile_example_read(0x9C62, 0) != 'A') { failures++; }
    if (tile_example_read(0x9862, 0) != 0) { failures++; }
    tile_example_finish(failures, 1);
}

预期结果

所选地图显示写入的字符或填充区域。程序读回目标图块编号,并在 CGB 上读回属性进行比较;FAILED CHECKS 000 表示比较全部通过。两张地图初始均为 0,调用前将背景/窗口地图选为 0x9C00,因此能区分固定地图与所选地图的写入。示例在 LCD 关闭时写入,不测量 LCD 开启时的时序。

在 CGB 上,示例字符和结果文字以白底蓝字显示。初始化时将调色板 0 的绘制颜色设为蓝色,不修改图块映射的属性。在 DMG 上,相同字符显示为白底黑字。

预期图像:以下图块排列从图块坐标 (x=2, y=3)、像素坐标 (16,24) 开始显示。每个字符占一个 8×8 图块。按从上到下、从左到右阅读,周围图块保持空白。

text
A
__settile_xy: DMG
DMG
__settile_xy: CGB
CGB

完整示例程序

构建命令

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqgb\kitaqgb.exe .\kitaq-docs\samples\api-examples\gb\__settile_xy.c -I .\kitaqgb\lib -I .\kitaq-docs\samples -o .\out\__settile_xy.gb --profile=dev --rst-disable --stack-bank=fixed --cgb=cgb --no-disasm
实现与源码

kitaqgb/lib/rpg.h:153

__settilemap_rect

将紧密排列的 w*h 个图块编号复制到 base 指定地图中以 (x,y) 为起点的矩形。源数据每行 w 字节,目标行间距为 32 字节。

必要时等待 VRAM 可写。LCD 开启时的路径会屏蔽中断,并可能在结束后启用中断;不保证保留调用前的中断状态,也不保证在一次 VBlank 内完成。

语法

c
void __settilemap_rect(u16 base, u8 x, u8 y, u8 w, u8 h, const u8* src);

参数

base

用 base 指定地图起始地址 0x9800 或 0x9C00;字节地址为 base + 32*y + x。

x, y

以 8×8 图块为单位的坐标,各为 0~31。整个写入范围必须位于 32×32 地图内;不会自动裁剪坐标。

w, h

以 8×8 图块为单位的宽和高。须满足 x+w <= 32、y+h <= 32。任一维为 0 时不写入。

src

位于 RAM 或当前可见 ROM bank 中的连续字节。图块流保存图块编号,属性流保存 CGB 属性字节。函数立即复制,不保留指针。

返回值

无(void)。

使用条件与限制

图块编号写入当前选中的 VRAM 库。CGB 上应选择编号所在的库 0;此调用不会代为切换库。

示例使用 gb_common.h。m_init() 将附带 ASCII 字体加载到与字符编码相同的图块编号。m_wait(); M_LCDC=0; 在 VBlank 中关闭 LCD,以便进行无需等待的传输。最后的 LCDC 值选择显示地图。

使用示例

c
// __settilemap_rect
// Copy a tightly packed `w*h` array of tile numbers to a rectangle at `(x,y)` in the map
// selected by `base`. Source rows have `w` bytes; destination rows have a 32-byte
// stride. Waits for writable VRAM when necessary. The LCD-on path uses interrupt masking
// and can enable interrupts afterward; it is not a promise to preserve the caller's
// interrupt state or to finish within one VBlank.
// Expected: FAILED CHECKS 000, with the written tile data in the selected map.
#include "gb_tile_color_example.h"
const u8 example_tiles[] = {'A','B','C','D','E','F'};

void main() {
    u8 failures;
    failures = 0;
    tile_color_example_begin();
    __settilemap_rect(0x9C00, 2, 3, 3, 2, example_tiles);
    if (tile_example_read(0x9C62, 0) != 'A') { failures++; }
    if (tile_example_read(0x9C84, 0) != 'F') { failures++; }
    if (tile_example_read(0x9C65, 0) != 0) { failures++; }
    tile_example_finish(failures, 1);
}

预期结果

所选地图显示写入的字符或填充区域。程序读回目标图块编号,并在 CGB 上读回属性进行比较;FAILED CHECKS 000 表示比较全部通过。两张地图初始均为 0,调用前将背景/窗口地图选为 0x9C00,因此能区分固定地图与所选地图的写入。示例在 LCD 关闭时写入,不测量 LCD 开启时的时序。

在 CGB 上,示例字符和结果文字以白底蓝字显示。初始化时将调色板 0 的绘制颜色设为蓝色,不修改图块映射的属性。在 DMG 上,相同字符显示为白底黑字。

预期图像:以下图块排列从图块坐标 (x=2, y=3)、像素坐标 (16,24) 开始显示。每个字符占一个 8×8 图块。按从上到下、从左到右阅读,周围图块保持空白。

text
ABC
DEF
__settilemap_rect: DMG
DMG
__settilemap_rect: CGB
CGB

完整示例程序

构建命令

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqgb\kitaqgb.exe .\kitaq-docs\samples\api-examples\gb\__settilemap_rect.c -I .\kitaqgb\lib -I .\kitaq-docs\samples -o .\out\__settilemap_rect.gb --profile=dev --rst-disable --stack-bank=fixed --cgb=cgb --no-disasm
实现与源码

kitaqgb/lib/rpg.h:158

__tile_addr

计算宽度为 32 个图块的地图中一个格子的 CPU 地址。

语法

c
u16 __tile_addr(u16 base, u8 x, u8 y);

参数

base

图块地图基地址,通常为 $9800/$9C00。

x

列坐标,通常为 0..31。

y

行坐标,通常为 0..31。

返回值

返回 (base + y*32 + x) 对 65536 取模的结果,不访问内存。

使用示例

c
result[4]=__tile_addr(0x9800,3,2);

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

预期结果

学习地图选择、滚动、嵌套和扫描行等待。基地址为 38912/39936;嵌套令牌为 0/1;观测到的目标 LY 值为 80/144。同时验证 LCD 关闭时等待函数会返回。

__tile_addr: DMG
DMG
__tile_addr: CGB
CGB

完整示例程序

构建命令

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqgb\kitaqgb.exe .\kitaq-docs\samples\api-examples\gb\batch3_display.c -I .\kitaqgb\lib -I .\kitaq-docs\samples -o .\out\batch3_display.gb --no-cache --no-disasm --profile=dev --rst-disable --stack-bank=fixed --cgb=cgb --cart=mbc5 --romsize=64k --ramsize=none
实现与源码

kitaqgb/lib/rpg.h:180

kitaqgb/kitaqgb/CodeGenerator.cs:8355

csharp
if (funcName == "__tile_addr")
            {
                if (args.Length != 3) Error(expr, "__tile_addr(base, x, y) expects 3 arguments");

                EmitTileMapAddressIntoHL(args[0], args[1], args[2]);
                LastCallReturnsHL = true;
                return;
            }
__vram_copy_dma

__vram_copy_dma是编译器中__vram_memcpy的别名。长度同样以字节计,使用相同的即时CPU传输及LCD访问规则。

立即按地址递增顺序将src的len字节复制到dst,完成后才返回,不将操作加入队列。源地址和目标地址每次增加1字节,不处理图块映射表行跨度或图块格式转换。

语法

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

参数

dst

目标首字节地址,通常位于VRAM 0x8000–0x9FFF。整个范围必须有效。CGB使用当前VRAM存储体;映射表区域的存储体0存放图块编号,存储体1存放属性。这些函数不根据LCDC选择映射表,不在行末换行,也不分配VRAM。

src

源字节指针,在整个调用期间都必须有效且映射正确。数据立即读取,不为后续flush保留,也不会自动切换源存储体。请使用不重叠的范围;这种向前复制不提供memmove语义。

len

连续字节数,范围0至65535,不是图块数。整个范围应位于有效源数据和可写目标区域内;API不检查边界,也不防止十六位地址回绕。

返回值

无(void)。

使用条件与限制

此名称不会选择DMA引擎,也不会把工作安排到之后的HBlank。调用者会等待完成。LCD活动时,相同的CPU轮询接受模式0(HBlank)或模式1(VBlank),不配置CGB HDMA。

LCD开启时,每次写入前执行DI并等待STAT模式0或1,写入后执行EI。LCD关闭时使用普通CPU循环。调用可能跨越多帧,不保留进入时的中断禁用状态,也不设置DMA寄存器。长度0不传输数据。

使用示例

c
__vram_copy_dma(0x98A2,strip,3); // This alias also uses CPU writes, not a DMA engine.

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

预期结果

本例用__vram_copy_dma在图块坐标(2,5)、像素坐标(16,40)写入3字节映射表数据。24×8区域应从左到右显示[1,2,3]图案,相邻单元保持空白。完整程序将每种受测复制或填充放在独立的一行。

附带的自制图形中,图块1为8×8实心正方形,2只填左侧四列,3为左对齐的三角形,每行加宽1像素,从1像素增至8像素。DMG显示白底黑图形;CGB按操作区分颜色,文字保持黑色。

像素(16,88)的正方形应在安全版和无等待版的零长度调用后保留。LCD开启时,安全复制和填充在(16,96)和(32,96)添加正方形。FAILED CHECKS应为000。模拟器测试同时比较绘制像素和计数值。本例CGB使用存储体0;此处未测试其他存储体或实机。

CGB模式下,从MEMCPY到FILL UNSAFE各行依次循环使用红、蓝、绿。ZERO COUNT正方形为红色;LCD ON左侧正方形为蓝色,右侧为绿色,文字保持黑色。测试还检查每个绘制像素是否为指定颜色。

__vram_copy_dma: DMG
DMG
__vram_copy_dma: CGB
CGB

完整示例程序

构建命令

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqgb\kitaqgb.exe .\kitaq-docs\samples\api-examples\gb\vram_memory_shapes.c -I .\kitaqgb\lib -I .\kitaq-docs\samples -o .\out\vram_memory_shapes.gb --profile=dev --rst-disable --stack-bank=fixed --cgb=cgb --no-cache --no-disasm
实现与源码

kitaqgb/lib/rpg.h:163

__vram_copy_hblank

__vram_copy_hblank是编译器中__vram_memcpy的别名。长度同样以字节计,使用相同的即时CPU传输及LCD访问规则。

立即按地址递增顺序将src的len字节复制到dst,完成后才返回,不将操作加入队列。源地址和目标地址每次增加1字节,不处理图块映射表行跨度或图块格式转换。

语法

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

参数

dst

目标首字节地址,通常位于VRAM 0x8000–0x9FFF。整个范围必须有效。CGB使用当前VRAM存储体;映射表区域的存储体0存放图块编号,存储体1存放属性。这些函数不根据LCDC选择映射表,不在行末换行,也不分配VRAM。

src

源字节指针,在整个调用期间都必须有效且映射正确。数据立即读取,不为后续flush保留,也不会自动切换源存储体。请使用不重叠的范围;这种向前复制不提供memmove语义。

len

连续字节数,范围0至65535,不是图块数。整个范围应位于有效源数据和可写目标区域内;API不检查边界,也不防止十六位地址回绕。

返回值

无(void)。

使用条件与限制

此名称不会选择DMA引擎,也不会把工作安排到之后的HBlank。调用者会等待完成。LCD活动时,相同的CPU轮询接受模式0(HBlank)或模式1(VBlank),不配置CGB HDMA。

LCD开启时,每次写入前执行DI并等待STAT模式0或1,写入后执行EI。LCD关闭时使用普通CPU循环。调用可能跨越多帧,不保留进入时的中断禁用状态,也不设置DMA寄存器。长度0不传输数据。

使用示例

c
__vram_copy_hblank(0x9882,strip,3); // Immediate safe copy; no scheduled HBlank DMA.

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

预期结果

本例用__vram_copy_hblank在图块坐标(2,4)、像素坐标(16,32)写入3字节映射表数据。24×8区域应从左到右显示[1,2,3]图案,相邻单元保持空白。完整程序将每种受测复制或填充放在独立的一行。

附带的自制图形中,图块1为8×8实心正方形,2只填左侧四列,3为左对齐的三角形,每行加宽1像素,从1像素增至8像素。DMG显示白底黑图形;CGB按操作区分颜色,文字保持黑色。

像素(16,88)的正方形应在安全版和无等待版的零长度调用后保留。LCD开启时,安全复制和填充在(16,96)和(32,96)添加正方形。FAILED CHECKS应为000。模拟器测试同时比较绘制像素和计数值。本例CGB使用存储体0;此处未测试其他存储体或实机。

CGB模式下,从MEMCPY到FILL UNSAFE各行依次循环使用红、蓝、绿。ZERO COUNT正方形为红色;LCD ON左侧正方形为蓝色,右侧为绿色,文字保持黑色。测试还检查每个绘制像素是否为指定颜色。

__vram_copy_hblank: DMG
DMG
__vram_copy_hblank: CGB
CGB

完整示例程序

构建命令

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqgb\kitaqgb.exe .\kitaq-docs\samples\api-examples\gb\vram_memory_shapes.c -I .\kitaqgb\lib -I .\kitaq-docs\samples -o .\out\vram_memory_shapes.gb --profile=dev --rst-disable --stack-bank=fixed --cgb=cgb --no-cache --no-disasm
实现与源码

kitaqgb/lib/rpg.h:162

__vram_fill

__vram_fill是编译器中__vram_memset的别名。长度同样以字节计,使用相同的即时CPU传输及LCD访问规则。

立即用单字节value填充从dst开始的连续len字节。重复的是1字节,不是整个图块图案。填充映射表字节会重复图块编号;填充图案字节则会改变编码后的像素位平面。

语法

c
void __vram_fill(u16 dst, u8 value, u16 len);

参数

dst

目标首字节地址,通常位于VRAM 0x8000–0x9FFF。整个范围必须有效。CGB使用当前VRAM存储体;映射表区域的存储体0存放图块编号,存储体1存放属性。这些函数不根据LCDC选择映射表,不在行末换行,也不分配VRAM。

value

重复写入0至255的单字节。显示含义取决于目标是图块索引、像素位平面还是其他字节结构。

len

连续字节数,范围0至65535,不是图块数。整个范围应位于有效源数据和可写目标区域内;API不检查边界,也不防止十六位地址回绕。

返回值

无(void)。

使用条件与限制

LCD开启时,每次写入前执行DI并等待STAT模式0或1,写入后执行EI。LCD关闭时使用普通CPU循环。调用可能跨越多帧,不保留进入时的中断禁用状态,也不设置DMA寄存器。长度0不传输数据。

使用示例

c
__vram_fill(0x9902,1,computed_length());

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

预期结果

本例用__vram_fill将图块坐标(2,8)、像素坐标(16,64)开始的3个映射表字节填为图块编号1。应得到24×8实心横条,相邻单元为空白。长度由变量或函数返回值计算;三个字节仍须全部写入值1。

附带的自制图形中,图块1为8×8实心正方形,2只填左侧四列,3为左对齐的三角形,每行加宽1像素,从1像素增至8像素。DMG显示白底黑图形;CGB按操作区分颜色,文字保持黑色。

像素(16,88)的正方形应在安全版和无等待版的零长度调用后保留。LCD开启时,安全复制和填充在(16,96)和(32,96)添加正方形。FAILED CHECKS应为000。模拟器测试同时比较绘制像素和计数值。本例CGB使用存储体0;此处未测试其他存储体或实机。

CGB模式下,从MEMCPY到FILL UNSAFE各行依次循环使用红、蓝、绿。ZERO COUNT正方形为红色;LCD ON左侧正方形为蓝色,右侧为绿色,文字保持黑色。测试还检查每个绘制像素是否为指定颜色。

__vram_fill: DMG
DMG
__vram_fill: CGB
CGB

完整示例程序

构建命令

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqgb\kitaqgb.exe .\kitaq-docs\samples\api-examples\gb\vram_memory_shapes.c -I .\kitaqgb\lib -I .\kitaq-docs\samples -o .\out\vram_memory_shapes.gb --profile=dev --rst-disable --stack-bank=fixed --cgb=cgb --no-cache --no-disasm
实现与源码

kitaqgb/lib/rpg.h:161

__vram_memcpy

立即按地址递增顺序将src的len字节复制到dst,完成后才返回,不将操作加入队列。源地址和目标地址每次增加1字节,不处理图块映射表行跨度或图块格式转换。

语法

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

参数

dst

目标首字节地址,通常位于VRAM 0x8000–0x9FFF。整个范围必须有效。CGB使用当前VRAM存储体;映射表区域的存储体0存放图块编号,存储体1存放属性。这些函数不根据LCDC选择映射表,不在行末换行,也不分配VRAM。

src

源字节指针,在整个调用期间都必须有效且映射正确。数据立即读取,不为后续flush保留,也不会自动切换源存储体。请使用不重叠的范围;这种向前复制不提供memmove语义。

len

连续字节数,范围0至65535,不是图块数。整个范围应位于有效源数据和可写目标区域内;API不检查边界,也不防止十六位地址回绕。

返回值

无(void)。

使用条件与限制

LCD开启时,每次写入前执行DI并等待STAT模式0或1,写入后执行EI。LCD关闭时使用普通CPU循环。调用可能跨越多帧,不保留进入时的中断禁用状态,也不设置DMA寄存器。长度0不传输数据。

使用示例

c
__vram_memcpy(0x9842,strip,length_base+length_extra); // Three map bytes at tile (2,2).

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

预期结果

本例用__vram_memcpy在图块坐标(2,2)、像素坐标(16,16)写入3字节映射表数据。24×8区域应从左到右显示[1,2,3]图案,相邻单元保持空白。完整程序将每种受测复制或填充放在独立的一行。

附带的自制图形中,图块1为8×8实心正方形,2只填左侧四列,3为左对齐的三角形,每行加宽1像素,从1像素增至8像素。DMG显示白底黑图形;CGB按操作区分颜色,文字保持黑色。

像素(16,88)的正方形应在安全版和无等待版的零长度调用后保留。LCD开启时,安全复制和填充在(16,96)和(32,96)添加正方形。FAILED CHECKS应为000。模拟器测试同时比较绘制像素和计数值。本例CGB使用存储体0;此处未测试其他存储体或实机。

CGB模式下,从MEMCPY到FILL UNSAFE各行依次循环使用红、蓝、绿。ZERO COUNT正方形为红色;LCD ON左侧正方形为蓝色,右侧为绿色,文字保持黑色。测试还检查每个绘制像素是否为指定颜色。

__vram_memcpy: DMG
DMG
__vram_memcpy: CGB
CGB

完整示例程序

构建命令

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqgb\kitaqgb.exe .\kitaq-docs\samples\api-examples\gb\vram_memory_shapes.c -I .\kitaqgb\lib -I .\kitaq-docs\samples -o .\out\vram_memory_shapes.gb --profile=dev --rst-disable --stack-bank=fixed --cgb=cgb --no-cache --no-disasm
实现与源码

kitaqgb/lib/rpg.h:146

__vram_memset

立即用单字节value填充从dst开始的连续len字节。重复的是1字节,不是整个图块图案。填充映射表字节会重复图块编号;填充图案字节则会改变编码后的像素位平面。

语法

c
void __vram_memset(u16 dst, u8 value, u16 len);

参数

dst

目标首字节地址,通常位于VRAM 0x8000–0x9FFF。整个范围必须有效。CGB使用当前VRAM存储体;映射表区域的存储体0存放图块编号,存储体1存放属性。这些函数不根据LCDC选择映射表,不在行末换行,也不分配VRAM。

value

重复写入0至255的单字节。显示含义取决于目标是图块索引、像素位平面还是其他字节结构。

len

连续字节数,范围0至65535,不是图块数。整个范围应位于有效源数据和可写目标区域内;API不检查边界,也不防止十六位地址回绕。

返回值

无(void)。

使用条件与限制

LCD开启时,每次写入前执行DI并等待STAT模式0或1,写入后执行EI。LCD关闭时使用普通CPU循环。调用可能跨越多帧,不保留进入时的中断禁用状态,也不设置DMA寄存器。长度0不传输数据。

使用示例

c
__vram_memset(0x98E2,1,length_base+length_extra); // Three solid tiles, not three tile patterns.

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

预期结果

本例用__vram_memset将图块坐标(2,7)、像素坐标(16,56)开始的3个映射表字节填为图块编号1。应得到24×8实心横条,相邻单元为空白。长度由变量或函数返回值计算;三个字节仍须全部写入值1。

附带的自制图形中,图块1为8×8实心正方形,2只填左侧四列,3为左对齐的三角形,每行加宽1像素,从1像素增至8像素。DMG显示白底黑图形;CGB按操作区分颜色,文字保持黑色。

像素(16,88)的正方形应在安全版和无等待版的零长度调用后保留。LCD开启时,安全复制和填充在(16,96)和(32,96)添加正方形。FAILED CHECKS应为000。模拟器测试同时比较绘制像素和计数值。本例CGB使用存储体0;此处未测试其他存储体或实机。

CGB模式下,从MEMCPY到FILL UNSAFE各行依次循环使用红、蓝、绿。ZERO COUNT正方形为红色;LCD ON左侧正方形为蓝色,右侧为绿色,文字保持黑色。测试还检查每个绘制像素是否为指定颜色。

__vram_memset: DMG
DMG
__vram_memset: CGB
CGB

完整示例程序

构建命令

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqgb\kitaqgb.exe .\kitaq-docs\samples\api-examples\gb\vram_memory_shapes.c -I .\kitaqgb\lib -I .\kitaq-docs\samples -o .\out\vram_memory_shapes.gb --profile=dev --rst-disable --stack-bank=fixed --cgb=cgb --no-cache --no-disasm
实现与源码

kitaqgb/lib/rpg.h:147

__wait_vblank

LCD 运行时,先等当前 VBlank 结束,再等到下一次 LY≥144。

语法

c
void __wait_vblank();

返回值

无返回值。

使用条件与限制

LCD 关闭时立即返回。通过轮询 LY 等待,不启用中断,也不递增库的帧计数器。

使用示例

c
__wait_vblank();result[10]=*((u8*)0xFF44);M_LCDC=0;__wait_vblank();__wait_ly(200);result[11]=1;

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

预期结果

学习地图选择、滚动、嵌套和扫描行等待。基地址为 38912/39936;嵌套令牌为 0/1;观测到的目标 LY 值为 80/144。同时验证 LCD 关闭时等待函数会返回。

__wait_vblank: DMG
DMG
__wait_vblank: CGB
CGB

完整示例程序

构建命令

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqgb\kitaqgb.exe .\kitaq-docs\samples\api-examples\gb\batch3_display.c -I .\kitaqgb\lib -I .\kitaq-docs\samples -o .\out\batch3_display.gb --no-cache --no-disasm --profile=dev --rst-disable --stack-bank=fixed --cgb=cgb --cart=mbc5 --romsize=64k --ramsize=none
实现与源码

kitaqgb/lib/rpg.h:138

kitaqgb/kitaqgb/CodeGenerator.cs:7451

csharp
if (funcName == "__wait_vblank")
            {
                if (args.Length != 0) Error(expr, "__wait_vblank() expects 0 arguments");

                AsmOperand wbRet = MakeUniqueLabel("waitvb_ret");
                AsmOperand wbExitVblank = MakeUniqueLabel("waitvb_exit");
                AsmOperand wbEnterVblank = MakeUniqueLabel("waitvb_enter");

                // If LCD is off, return immediately.
                EmitAsm("LDH_A_MEM", new AsmOperand(0x40, AddressMode.HighMem)); // LCDC
                EmitAsm("AND_IMM", new AsmOperand(0x80, AddressMode.Immediate));
                EmitAsm("JR_Z", wbRet);

                // while (LY >= 144) {}
                EmitLabel(wbExitVblank);
                EmitAsm("LDH_A_MEM", new AsmOperand(0x44, AddressMode.HighMem)); // LY
                EmitAsm("CP_IMM", new AsmOperand(144, AddressMode.Immediate));
                EmitAsm("JR_C", wbEnterVblank);
                EmitAsm("JR", wbExitVblank);

                // while (LY < 144) {}
                EmitLabel(wbEnterVblank);
                EmitAsm("LDH_A_MEM", new AsmOperand(0x44, AddressMode.HighMem)); // LY
                EmitAsm("CP_IMM", new AsmOperand(144, AddressMode.Immediate));
                EmitAsm("JR_C", wbEnterVblank);

                EmitLabel(wbRet);
                return;
            }
__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: DMG
DMG
__xy_in_rect: CGB
CGB

完整示例程序

构建命令

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqgb\kitaqgb.exe .\kitaq-docs\samples\api-examples\gb\batch3_coordinates.c -I .\kitaqgb\lib -I .\kitaq-docs\samples -o .\out\batch3_coordinates.gb --no-cache --no-disasm --profile=dev --rst-disable --stack-bank=fixed --cgb=cgb --cart=mbc5 --romsize=64k --ramsize=none
实现与源码

kitaqgb/lib/rpg.h:182

kitaqgb/kitaqgb/CodeGenerator.cs:8436

csharp
if (funcName == "__xy_in_rect")
            {
                if (args.Length != 6) Error(expr, "__xy_in_rect(x, y, rx, ry, rw, rh) expects 6 arguments");

                Expr[] foldedRectArgs = args.Select(FoldConstants).ToArray();
                if (foldedRectArgs[0].Match(Tag.Integer, out int constX) &&
                    foldedRectArgs[1].Match(Tag.Integer, out int constY) &&
                    foldedRectArgs[2].Match(Tag.Integer, out int constRx) &&
                    foldedRectArgs[3].Match(Tag.Integer, out int constRy) &&
                    foldedRectArgs[4].Match(Tag.Integer, out int constRw) &&
                    foldedRectArgs[5].Match(Tag.Integer, out int constRh))
                {
                    int x = constX & 0xFF;
                    int y = constY & 0xFF;
                    int rx = constRx & 0xFF;
                    int ry = constRy & 0xFF;
                    int rw = constRw & 0xFF;
                    int rh = constRh & 0xFF;
                    bool inside = x >= rx && y >= ry && (x - rx) < rw && (y - ry) < rh;
                    if (inside) EmitAsm("LD_A_IMM", new AsmOperand(1, AddressMode.Immediate));
                    else EmitAsm("XOR_A");
                    return;
                }

                EmitPushExprAsWord(args[0]); // x
                EmitPushExprAsWord(args[1]); // y
                EmitPushExprAsWord(args[2]); // rx
                EmitPushExprAsWord(args[3]); // ry
                EmitPushExprAsWord(args[4]); // rw
                EmitPushExprAsWord(args[5]); // rh

                AsmOperand ret0 = MakeUniqueLabel("xyrect_ret0");
                AsmOperand done = MakeUniqueLabel("xyrect_done");

                EmitAsm("LD_HL_SP_IMM", new AsmOperand(10, AddressMode.Relative));
                EmitAsm("LD_A_HL");
                EmitAsm("LD_B_A"); // x
                EmitAsm("LD_HL_SP_IMM", new AsmOperand(6, AddressMode.Relative));
                EmitAsm("LD_A_HL");
                EmitAsm("LD_C_A"); // rx
                EmitAsm("LD_A_B");
                EmitAsm("CP_C");
                EmitAsm("JR_C", ret0);
                EmitAsm("SUB_C");
                EmitAsm("LD_B_A");
                EmitAsm("LD_HL_SP_IMM", new AsmOperand(2, AddressMode.Relative));
                EmitAsm("LD_A_HL");
                EmitAsm("LD_C_A"); // rw
                EmitAsm("LD_A_B");
                EmitAsm("CP_C");
                EmitAsm("JR_NC", ret0);

                EmitAsm("LD_HL_SP_IMM", new AsmOperand(8, AddressMode.Relative));
                EmitAsm("LD_A_HL");
                EmitAsm("LD_B_A"); // y
                EmitAsm("LD_HL_SP_IMM", new AsmOperand(4, AddressMode.Relative));
                EmitAsm("LD_A_HL");
                EmitAsm("LD_C_A"); // ry
                EmitAsm("LD_A_B");
                EmitAsm("CP_C");
                EmitAsm("JR_C", ret0);
                EmitAsm("SUB_C");
                EmitAsm("LD_B_A");
                EmitAsm("LD_HL_SP_IMM", new AsmOperand(0, AddressMode.Relative));
                EmitAsm("LD_A_HL");
                EmitAsm("LD_C_A"); // rh
                EmitAsm("LD_A_B");
                EmitAsm("CP_C");
                EmitAsm("JR_NC", ret0);

                EmitAsm("LD_A_IMM", new AsmOperand(1, AddressMode.Immediate));
                EmitAsm("JR", done);
                EmitLabel(ret0);
                EmitAsm("XOR_A");
                EmitLabel(done);
                EmitAsm("ADD_SP_IMM", new AsmOperand(12, AddressMode.Relative));
                return;
            }

scroll.h — 背景、窗口与分屏滚动

控制背景与窗口的立即滚动、缓冲滚动及最多八个扫描行分段事件。请区分读取函数维护的当前值、实时硬件寄存器值和待提交值。

__scroll_bg_add

将有符号位移加到库记录的背景坐标上,并立即应用。

语法

c
void __scroll_bg_add(s8 dx, s8 dy);

参数

dx

有符号水平位移,单位为像素,范围为 −128 至 127。

dy

有符号垂直位移,单位为像素,范围为 −128 至 127。

返回值

无返回值,本条说明受影响状态及生效时机。

使用条件与限制

不执行等待。立即设置或增量操作会将受影响的整对待提交坐标同步为当前值,并清除其脏标记。即使只立即设置一个轴,同一对中另一个轴的待提交值也会被取消。结果按模 256 回绕。增量操作以库记录的当前值为起点,而不是待提交值或光栅处理临时写入寄存器的值。

使用示例

c
__scroll_bg_add(-20,3);result[22]=M_SCX;result[23]=M_SCY;

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

预期结果

学习立即值与待提交值的区别,以及有符号增量的回绕。背景 X 在提交前为 250,提交后为 16;WX 从 23 变为 39。背景 X 加上 −20 得到 252;WY 加上 −40 得到 248。可视化示例还展示了背景矩形和独立的窗口边框。

__scroll_bg_add: DMG
DMG
__scroll_bg_add: CGB
CGB

完整示例程序

构建命令

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqgb\kitaqgb.exe .\kitaq-docs\samples\api-examples\gb\batch2_scroll_intrinsics.c -I .\kitaqgb\lib -I .\kitaq-docs\samples -o .\out\batch2_scroll_intrinsics.gb --no-cache --no-disasm --profile=dev --rst-disable --stack-bank=fixed --cgb=cgb --cart=mbc5 --romsize=64k

使用示例

c
Camera_Init(&view);
Camera_Set(&view,2048,4096);
Camera_ApplyBg(&view);
Scroll_SetWindow(103,80);
Scroll_WindowShow();

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

预期结果

摄像机位于 (8,16) 时,红色 32×24 矩形显示在屏幕坐标 (24,16)。窗口在 (96,80) 显示蓝色 64×64 边框,在 (112,96) 显示绿色 16×16 方块。在 DMG 上,所有图形均为黑色。

__scroll_bg_add: DMG
DMG
__scroll_bg_add: CGB
CGB

完整示例程序

构建命令

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqgb\kitaqgb.exe .\kitaq-docs\samples\api-examples\gb\batch2_camera_view.c -I .\kitaqgb\lib -I .\kitaq-docs\samples -o .\out\batch2_camera_view.gb --no-cache --no-disasm --profile=dev --rst-disable --stack-bank=fixed --cgb=cgb --cart=mbc5 --romsize=64k
实现与源码

kitaqgb/lib/scroll.h:37

kitaqgb/kitaqgb/CodeGenerator.cs:7835

csharp
if (funcName == "__scroll_bg_add")
            {
                if (args.Length != 2) Error(expr, "__scroll_bg_add(dx, dy) expects 2 arguments");
                EnsureScrollState();

                CompileIntoA(args[0]);
                EmitPushAAsWord();
                CompileIntoA(args[1]);
                EmitAsm("LD_C_A");
                EmitAsm("POP_HL");
                EmitAsm("LD_B_L");

                EmitLoadA(ScrollBgXCurVar);
                EmitAsm("ADD_B");
                EmitStoreA(ScrollBgXCurVar);
                EmitStoreA(ScrollBgXNextVar);
                EmitAsm("LDH_MEM_A", new AsmOperand(0x43, AddressMode.HighMem)); // SCX

                EmitLoadA(ScrollBgYCurVar);
                EmitAsm("ADD_C");
                EmitStoreA(ScrollBgYCurVar);
                EmitStoreA(ScrollBgYNextVar);
                EmitAsm("LDH_MEM_A", new AsmOperand(0x42, AddressMode.HighMem)); // SCY

                EmitLoadA(ScrollDirtyVar);
                EmitAsm("AND_IMM", new AsmOperand(0xFF ^ ScrollDirtyBgMask, AddressMode.Immediate));
                EmitStoreA(ScrollDirtyVar);
                return;
            }
__scroll_bg_set_buffered

暂存背景的两个滚动坐标,等待后续提交。

语法

c
void __scroll_bg_set_buffered(u8 scx, u8 scy);

参数

scx

背景水平滚动量,范围为 0–255 像素;增大该值会使背景向左移动。

scy

背景垂直滚动量,范围为 0–255 像素;增大该值会使背景向上移动。

返回值

无返回值,本条说明受影响状态及生效时机。

使用条件与限制

不改变库记录的当前值或寄存器。请调用 __scroll_flush 或 Scroll_Flush 应用暂存值;暂存操作本身不等待 VBlank。

使用示例

c
__scroll_bg_set_buffered(16,24);result[14]=M_SCX;result[15]=__scroll_bg_x_get();

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

预期结果

学习立即值与待提交值的区别,以及有符号增量的回绕。背景 X 在提交前为 250,提交后为 16;WX 从 23 变为 39。背景 X 加上 −20 得到 252;WY 加上 −40 得到 248。可视化示例还展示了背景矩形和独立的窗口边框。

__scroll_bg_set_buffered: DMG
DMG
__scroll_bg_set_buffered: CGB
CGB

完整示例程序

构建命令

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqgb\kitaqgb.exe .\kitaq-docs\samples\api-examples\gb\batch2_scroll_intrinsics.c -I .\kitaqgb\lib -I .\kitaq-docs\samples -o .\out\batch2_scroll_intrinsics.gb --no-cache --no-disasm --profile=dev --rst-disable --stack-bank=fixed --cgb=cgb --cart=mbc5 --romsize=64k

使用示例

c
Camera_Init(&view);
Camera_Set(&view,2048,4096);
Camera_ApplyBg(&view);
Scroll_SetWindow(103,80);
Scroll_WindowShow();

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

预期结果

摄像机位于 (8,16) 时,红色 32×24 矩形显示在屏幕坐标 (24,16)。窗口在 (96,80) 显示蓝色 64×64 边框,在 (112,96) 显示绿色 16×16 方块。在 DMG 上,所有图形均为黑色。

__scroll_bg_set_buffered: DMG
DMG
__scroll_bg_set_buffered: CGB
CGB

完整示例程序

构建命令

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqgb\kitaqgb.exe .\kitaq-docs\samples\api-examples\gb\batch2_camera_view.c -I .\kitaqgb\lib -I .\kitaq-docs\samples -o .\out\batch2_camera_view.gb --no-cache --no-disasm --profile=dev --rst-disable --stack-bank=fixed --cgb=cgb --cart=mbc5 --romsize=64k
实现与源码

kitaqgb/lib/scroll.h:22

kitaqgb/kitaqgb/CodeGenerator.cs:7671

csharp
if (funcName == "__scroll_bg_set_buffered")
            {
                if (args.Length != 2) Error(expr, "__scroll_bg_set_buffered(scx, scy) expects 2 arguments");
                EnsureScrollState();

                CompileIntoA(args[0]);
                EmitPushAAsWord();
                CompileIntoA(args[1]);
                EmitAsm("LD_B_A");
                EmitAsm("POP_HL");

                EmitAsm("LD_A_L");
                EmitStoreA(ScrollBgXNextVar);
                EmitAsm("LD_A_B");
                EmitStoreA(ScrollBgYNextVar);
                EmitLoadA(ScrollDirtyVar);
                EmitAsm("OR_IMM", new AsmOperand(ScrollDirtyBgMask, AddressMode.Immediate));
                EmitStoreA(ScrollDirtyVar);
                return;
            }
__scroll_bg_x_get

读取库记录的当前背景水平滚动量。

语法

c
u8 __scroll_bg_x_get();

返回值

返回库记录的当前字节值,范围为 0–255。WX 包含硬件所需的七像素偏移。

使用条件与限制

这不是直接读取硬件寄存器。返回值不包含待提交的缓冲值、直接写入寄存器的值,也不包含分段扫描处理中临时应用的值。

使用示例

c
result[6]=__scroll_bg_x_get();

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

预期结果

学习立即值与待提交值的区别,以及有符号增量的回绕。背景 X 在提交前为 250,提交后为 16;WX 从 23 变为 39。背景 X 加上 −20 得到 252;WY 加上 −40 得到 248。可视化示例还展示了背景矩形和独立的窗口边框。

__scroll_bg_x_get: DMG
DMG
__scroll_bg_x_get: CGB
CGB

完整示例程序

构建命令

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqgb\kitaqgb.exe .\kitaq-docs\samples\api-examples\gb\batch2_scroll_intrinsics.c -I .\kitaqgb\lib -I .\kitaq-docs\samples -o .\out\batch2_scroll_intrinsics.gb --no-cache --no-disasm --profile=dev --rst-disable --stack-bank=fixed --cgb=cgb --cart=mbc5 --romsize=64k

使用示例

c
Camera_Init(&view);
Camera_Set(&view,2048,4096);
Camera_ApplyBg(&view);
Scroll_SetWindow(103,80);
Scroll_WindowShow();

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

预期结果

摄像机位于 (8,16) 时,红色 32×24 矩形显示在屏幕坐标 (24,16)。窗口在 (96,80) 显示蓝色 64×64 边框,在 (112,96) 显示绿色 16×16 方块。在 DMG 上,所有图形均为黑色。

__scroll_bg_x_get: DMG
DMG
__scroll_bg_x_get: CGB
CGB

完整示例程序

构建命令

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqgb\kitaqgb.exe .\kitaq-docs\samples\api-examples\gb\batch2_camera_view.c -I .\kitaqgb\lib -I .\kitaq-docs\samples -o .\out\batch2_camera_view.gb --no-cache --no-disasm --profile=dev --rst-disable --stack-bank=fixed --cgb=cgb --cart=mbc5 --romsize=64k
实现与源码

kitaqgb/lib/scroll.h:13

kitaqgb/kitaqgb/CodeGenerator.cs:7571

csharp
if (funcName == "__scroll_bg_x_get")
            {
                if (args.Length != 0) Error(expr, "__scroll_bg_x_get() expects 0 arguments");
                EnsureScrollState();
                EmitLoadA(ScrollBgXCurVar);
                return;
            }
__scroll_bg_x_set

立即设置背景的水平滚动位置。

语法

c
void __scroll_bg_x_set(u8 scx);

参数

scx

背景水平滚动量,范围为 0–255 像素;增大该值会使背景向左移动。

返回值

无返回值,本条说明受影响状态及生效时机。

使用条件与限制

不执行等待。立即设置或增量操作会将受影响的整对待提交坐标同步为当前值,并清除其脏标记。即使只立即设置一个轴,同一对中另一个轴的待提交值也会被取消。

使用示例

c
__scroll_bg_x_set(250);result[4]=M_SCX;

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

预期结果

学习立即值与待提交值的区别,以及有符号增量的回绕。背景 X 在提交前为 250,提交后为 16;WX 从 23 变为 39。背景 X 加上 −20 得到 252;WY 加上 −40 得到 248。可视化示例还展示了背景矩形和独立的窗口边框。

__scroll_bg_x_set: DMG
DMG
__scroll_bg_x_set: CGB
CGB

完整示例程序

构建命令

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqgb\kitaqgb.exe .\kitaq-docs\samples\api-examples\gb\batch2_scroll_intrinsics.c -I .\kitaqgb\lib -I .\kitaq-docs\samples -o .\out\batch2_scroll_intrinsics.gb --no-cache --no-disasm --profile=dev --rst-disable --stack-bank=fixed --cgb=cgb --cart=mbc5 --romsize=64k

使用示例

c
Camera_Init(&view);
Camera_Set(&view,2048,4096);
Camera_ApplyBg(&view);
Scroll_SetWindow(103,80);
Scroll_WindowShow();

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

预期结果

摄像机位于 (8,16) 时,红色 32×24 矩形显示在屏幕坐标 (24,16)。窗口在 (96,80) 显示蓝色 64×64 边框,在 (112,96) 显示绿色 16×16 方块。在 DMG 上,所有图形均为黑色。

__scroll_bg_x_set: DMG
DMG
__scroll_bg_x_set: CGB
CGB

完整示例程序

构建命令

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqgb\kitaqgb.exe .\kitaq-docs\samples\api-examples\gb\batch2_camera_view.c -I .\kitaqgb\lib -I .\kitaq-docs\samples -o .\out\batch2_camera_view.gb --no-cache --no-disasm --profile=dev --rst-disable --stack-bank=fixed --cgb=cgb --cart=mbc5 --romsize=64k
实现与源码

kitaqgb/lib/scroll.h:11

kitaqgb/kitaqgb/CodeGenerator.cs:7535

csharp
if (funcName == "__scroll_bg_x_set")
            {
                if (args.Length != 1) Error(expr, "__scroll_bg_x_set(scx) expects 1 argument");
                EnsureScrollState();

                CompileIntoA(args[0]);
                EmitStoreA(ScrollBgXCurVar);
                EmitStoreA(ScrollBgXNextVar);
                EmitAsm("LDH_MEM_A", new AsmOperand(0x43, AddressMode.HighMem)); // SCX
                EmitLoadA(ScrollBgYCurVar);
                EmitStoreA(ScrollBgYNextVar);
                EmitLoadA(ScrollDirtyVar);
                EmitAsm("AND_IMM", new AsmOperand(0xFF ^ ScrollDirtyBgMask, AddressMode.Immediate));
                EmitStoreA(ScrollDirtyVar);
                return;
            }
__scroll_bg_x_set_buffered

仅暂存背景 X 坐标。

语法

c
void __scroll_bg_x_set_buffered(u8 scx);

参数

scx

背景水平滚动量,范围为 0–255 像素;增大该值会使背景向左移动。

返回值

无返回值,本条说明受影响状态及生效时机。

使用条件与限制

保留另一个轴的待提交值。在提交之前,当前值和寄存器均不改变;请先初始化整对坐标。

使用示例

c
__scroll_bg_x_set_buffered(32);

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

预期结果

学习立即值与待提交值的区别,以及有符号增量的回绕。背景 X 在提交前为 250,提交后为 16;WX 从 23 变为 39。背景 X 加上 −20 得到 252;WY 加上 −40 得到 248。可视化示例还展示了背景矩形和独立的窗口边框。

__scroll_bg_x_set_buffered: DMG
DMG
__scroll_bg_x_set_buffered: CGB
CGB

完整示例程序

构建命令

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqgb\kitaqgb.exe .\kitaq-docs\samples\api-examples\gb\batch2_scroll_intrinsics.c -I .\kitaqgb\lib -I .\kitaq-docs\samples -o .\out\batch2_scroll_intrinsics.gb --no-cache --no-disasm --profile=dev --rst-disable --stack-bank=fixed --cgb=cgb --cart=mbc5 --romsize=64k

使用示例

c
Camera_Init(&view);
Camera_Set(&view,2048,4096);
Camera_ApplyBg(&view);
Scroll_SetWindow(103,80);
Scroll_WindowShow();

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

预期结果

摄像机位于 (8,16) 时,红色 32×24 矩形显示在屏幕坐标 (24,16)。窗口在 (96,80) 显示蓝色 64×64 边框,在 (112,96) 显示绿色 16×16 方块。在 DMG 上,所有图形均为黑色。

__scroll_bg_x_set_buffered: DMG
DMG
__scroll_bg_x_set_buffered: CGB
CGB

完整示例程序

构建命令

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqgb\kitaqgb.exe .\kitaq-docs\samples\api-examples\gb\batch2_camera_view.c -I .\kitaqgb\lib -I .\kitaq-docs\samples -o .\out\batch2_camera_view.gb --no-cache --no-disasm --profile=dev --rst-disable --stack-bank=fixed --cgb=cgb --cart=mbc5 --romsize=64k
实现与源码

kitaqgb/lib/scroll.h:23

kitaqgb/kitaqgb/CodeGenerator.cs:7693

csharp
if (funcName == "__scroll_bg_x_set_buffered")
            {
                if (args.Length != 1) Error(expr, "__scroll_bg_x_set_buffered(scx) expects 1 argument");
                EnsureScrollState();

                CompileIntoA(args[0]);
                EmitStoreA(ScrollBgXNextVar);
                EmitLoadA(ScrollDirtyVar);
                EmitAsm("OR_IMM", new AsmOperand(ScrollDirtyBgMask, AddressMode.Immediate));
                EmitStoreA(ScrollDirtyVar);
                return;
            }
__scroll_bg_y_get

读取库记录的当前背景垂直滚动量。

语法

c
u8 __scroll_bg_y_get();

返回值

返回库记录的当前字节值,范围为 0–255。WX 包含硬件所需的七像素偏移。

使用条件与限制

这不是直接读取硬件寄存器。返回值不包含待提交的缓冲值、直接写入寄存器的值,也不包含分段扫描处理中临时应用的值。

使用示例

c
result[7]=__scroll_bg_y_get();

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

预期结果

学习立即值与待提交值的区别,以及有符号增量的回绕。背景 X 在提交前为 250,提交后为 16;WX 从 23 变为 39。背景 X 加上 −20 得到 252;WY 加上 −40 得到 248。可视化示例还展示了背景矩形和独立的窗口边框。

__scroll_bg_y_get: DMG
DMG
__scroll_bg_y_get: CGB
CGB

完整示例程序

构建命令

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqgb\kitaqgb.exe .\kitaq-docs\samples\api-examples\gb\batch2_scroll_intrinsics.c -I .\kitaqgb\lib -I .\kitaq-docs\samples -o .\out\batch2_scroll_intrinsics.gb --no-cache --no-disasm --profile=dev --rst-disable --stack-bank=fixed --cgb=cgb --cart=mbc5 --romsize=64k

使用示例

c
Camera_Init(&view);
Camera_Set(&view,2048,4096);
Camera_ApplyBg(&view);
Scroll_SetWindow(103,80);
Scroll_WindowShow();

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

预期结果

摄像机位于 (8,16) 时,红色 32×24 矩形显示在屏幕坐标 (24,16)。窗口在 (96,80) 显示蓝色 64×64 边框,在 (112,96) 显示绿色 16×16 方块。在 DMG 上,所有图形均为黑色。

__scroll_bg_y_get: DMG
DMG
__scroll_bg_y_get: CGB
CGB

完整示例程序

构建命令

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqgb\kitaqgb.exe .\kitaq-docs\samples\api-examples\gb\batch2_camera_view.c -I .\kitaqgb\lib -I .\kitaq-docs\samples -o .\out\batch2_camera_view.gb --no-cache --no-disasm --profile=dev --rst-disable --stack-bank=fixed --cgb=cgb --cart=mbc5 --romsize=64k
实现与源码

kitaqgb/lib/scroll.h:14

kitaqgb/kitaqgb/CodeGenerator.cs:7580

csharp
if (funcName == "__scroll_bg_y_get")
            {
                if (args.Length != 0) Error(expr, "__scroll_bg_y_get() expects 0 arguments");
                EnsureScrollState();
                EmitLoadA(ScrollBgYCurVar);
                return;
            }
__scroll_bg_y_set

立即设置背景的垂直滚动位置。

语法

c
void __scroll_bg_y_set(u8 scy);

参数

scy

背景垂直滚动量,范围为 0–255 像素;增大该值会使背景向上移动。

返回值

无返回值,本条说明受影响状态及生效时机。

使用条件与限制

不执行等待。立即设置或增量操作会将受影响的整对待提交坐标同步为当前值,并清除其脏标记。即使只立即设置一个轴,同一对中另一个轴的待提交值也会被取消。

使用示例

c
__scroll_bg_y_set(2);result[5]=M_SCY;

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

预期结果

学习立即值与待提交值的区别,以及有符号增量的回绕。背景 X 在提交前为 250,提交后为 16;WX 从 23 变为 39。背景 X 加上 −20 得到 252;WY 加上 −40 得到 248。可视化示例还展示了背景矩形和独立的窗口边框。

__scroll_bg_y_set: DMG
DMG
__scroll_bg_y_set: CGB
CGB

完整示例程序

构建命令

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqgb\kitaqgb.exe .\kitaq-docs\samples\api-examples\gb\batch2_scroll_intrinsics.c -I .\kitaqgb\lib -I .\kitaq-docs\samples -o .\out\batch2_scroll_intrinsics.gb --no-cache --no-disasm --profile=dev --rst-disable --stack-bank=fixed --cgb=cgb --cart=mbc5 --romsize=64k

使用示例

c
Camera_Init(&view);
Camera_Set(&view,2048,4096);
Camera_ApplyBg(&view);
Scroll_SetWindow(103,80);
Scroll_WindowShow();

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

预期结果

摄像机位于 (8,16) 时,红色 32×24 矩形显示在屏幕坐标 (24,16)。窗口在 (96,80) 显示蓝色 64×64 边框,在 (112,96) 显示绿色 16×16 方块。在 DMG 上,所有图形均为黑色。

__scroll_bg_y_set: DMG
DMG
__scroll_bg_y_set: CGB
CGB

完整示例程序

构建命令

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqgb\kitaqgb.exe .\kitaq-docs\samples\api-examples\gb\batch2_camera_view.c -I .\kitaqgb\lib -I .\kitaq-docs\samples -o .\out\batch2_camera_view.gb --no-cache --no-disasm --profile=dev --rst-disable --stack-bank=fixed --cgb=cgb --cart=mbc5 --romsize=64k
实现与源码

kitaqgb/lib/scroll.h:12

kitaqgb/kitaqgb/CodeGenerator.cs:7553

csharp
if (funcName == "__scroll_bg_y_set")
            {
                if (args.Length != 1) Error(expr, "__scroll_bg_y_set(scy) expects 1 argument");
                EnsureScrollState();

                CompileIntoA(args[0]);
                EmitStoreA(ScrollBgYCurVar);
                EmitStoreA(ScrollBgYNextVar);
                EmitAsm("LDH_MEM_A", new AsmOperand(0x42, AddressMode.HighMem)); // SCY
                EmitLoadA(ScrollBgXCurVar);
                EmitStoreA(ScrollBgXNextVar);
                EmitLoadA(ScrollDirtyVar);
                EmitAsm("AND_IMM", new AsmOperand(0xFF ^ ScrollDirtyBgMask, AddressMode.Immediate));
                EmitStoreA(ScrollDirtyVar);
                return;
            }
__scroll_bg_y_set_buffered

仅暂存背景 Y 坐标。

语法

c
void __scroll_bg_y_set_buffered(u8 scy);

参数

scy

背景垂直滚动量,范围为 0–255 像素;增大该值会使背景向上移动。

返回值

无返回值,本条说明受影响状态及生效时机。

使用条件与限制

保留另一个轴的待提交值。在提交之前,当前值和寄存器均不改变;请先初始化整对坐标。

使用示例

c
__scroll_bg_y_set_buffered(40);

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

预期结果

学习立即值与待提交值的区别,以及有符号增量的回绕。背景 X 在提交前为 250,提交后为 16;WX 从 23 变为 39。背景 X 加上 −20 得到 252;WY 加上 −40 得到 248。可视化示例还展示了背景矩形和独立的窗口边框。

__scroll_bg_y_set_buffered: DMG
DMG
__scroll_bg_y_set_buffered: CGB
CGB

完整示例程序

构建命令

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqgb\kitaqgb.exe .\kitaq-docs\samples\api-examples\gb\batch2_scroll_intrinsics.c -I .\kitaqgb\lib -I .\kitaq-docs\samples -o .\out\batch2_scroll_intrinsics.gb --no-cache --no-disasm --profile=dev --rst-disable --stack-bank=fixed --cgb=cgb --cart=mbc5 --romsize=64k

使用示例

c
Camera_Init(&view);
Camera_Set(&view,2048,4096);
Camera_ApplyBg(&view);
Scroll_SetWindow(103,80);
Scroll_WindowShow();

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

预期结果

摄像机位于 (8,16) 时,红色 32×24 矩形显示在屏幕坐标 (24,16)。窗口在 (96,80) 显示蓝色 64×64 边框,在 (112,96) 显示绿色 16×16 方块。在 DMG 上,所有图形均为黑色。

__scroll_bg_y_set_buffered: DMG
DMG
__scroll_bg_y_set_buffered: CGB
CGB

完整示例程序

构建命令

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqgb\kitaqgb.exe .\kitaq-docs\samples\api-examples\gb\batch2_camera_view.c -I .\kitaqgb\lib -I .\kitaq-docs\samples -o .\out\batch2_camera_view.gb --no-cache --no-disasm --profile=dev --rst-disable --stack-bank=fixed --cgb=cgb --cart=mbc5 --romsize=64k
实现与源码

kitaqgb/lib/scroll.h:24

kitaqgb/kitaqgb/CodeGenerator.cs:7707

csharp
if (funcName == "__scroll_bg_y_set_buffered")
            {
                if (args.Length != 1) Error(expr, "__scroll_bg_y_set_buffered(scy) expects 1 argument");
                EnsureScrollState();

                CompileIntoA(args[0]);
                EmitStoreA(ScrollBgYNextVar);
                EmitLoadA(ScrollDirtyVar);
                EmitAsm("OR_IMM", new AsmOperand(ScrollDirtyBgMask, AddressMode.Immediate));
                EmitStoreA(ScrollDirtyVar);
                return;
            }
__scroll_flush

将带脏标记的背景或窗口坐标应用到库记录的状态和硬件,然后清除待提交标记。

语法

c
void __scroll_flush();

返回值

无返回值,本条说明受影响状态及生效时机。

使用条件与限制

不处理没有脏标记的坐标对。不会屏蔽中断,也不等待 VBlank;请由调用方安排所需的执行时机。

使用示例

c
__scroll_flush();result[18]=M_SCX;result[19]=M_SCY;result[20]=M_WX;result[21]=M_WY;

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

预期结果

学习立即值与待提交值的区别,以及有符号增量的回绕。背景 X 在提交前为 250,提交后为 16;WX 从 23 变为 39。背景 X 加上 −20 得到 252;WY 加上 −40 得到 248。可视化示例还展示了背景矩形和独立的窗口边框。

__scroll_flush: DMG
DMG
__scroll_flush: CGB
CGB

完整示例程序

构建命令

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqgb\kitaqgb.exe .\kitaq-docs\samples\api-examples\gb\batch2_scroll_intrinsics.c -I .\kitaqgb\lib -I .\kitaq-docs\samples -o .\out\batch2_scroll_intrinsics.gb --no-cache --no-disasm --profile=dev --rst-disable --stack-bank=fixed --cgb=cgb --cart=mbc5 --romsize=64k

使用示例

c
Camera_Init(&view);
Camera_Set(&view,2048,4096);
Camera_ApplyBg(&view);
Scroll_SetWindow(103,80);
Scroll_WindowShow();

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

预期结果

摄像机位于 (8,16) 时,红色 32×24 矩形显示在屏幕坐标 (24,16)。窗口在 (96,80) 显示蓝色 64×64 边框,在 (112,96) 显示绿色 16×16 方块。在 DMG 上,所有图形均为黑色。

__scroll_flush: DMG
DMG
__scroll_flush: CGB
CGB

完整示例程序

构建命令

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqgb\kitaqgb.exe .\kitaq-docs\samples\api-examples\gb\batch2_camera_view.c -I .\kitaqgb\lib -I .\kitaq-docs\samples -o .\out\batch2_camera_view.gb --no-cache --no-disasm --profile=dev --rst-disable --stack-bank=fixed --cgb=cgb --cart=mbc5 --romsize=64k
实现与源码

kitaqgb/lib/scroll.h:28

kitaqgb/kitaqgb/CodeGenerator.cs:7771

csharp
if (funcName == "__scroll_flush")
            {
                if (args.Length != 0) Error(expr, "__scroll_flush() expects 0 arguments");
                EnsureScrollState();

                AsmOperand flushSkipBg = MakeUniqueLabel("scroll_flush_skip_bg");
                AsmOperand flushSkipWin = MakeUniqueLabel("scroll_flush_skip_win");

                EmitLoadA(ScrollDirtyVar);
                EmitAsm("AND_IMM", new AsmOperand(ScrollDirtyBgMask, AddressMode.Immediate));
                EmitAsm("JR_Z", flushSkipBg);
                EmitLoadA(ScrollBgXNextVar);
                EmitStoreA(ScrollBgXCurVar);
                EmitAsm("LDH_MEM_A", new AsmOperand(0x43, AddressMode.HighMem)); // SCX
                EmitLoadA(ScrollBgYNextVar);
                EmitStoreA(ScrollBgYCurVar);
                EmitAsm("LDH_MEM_A", new AsmOperand(0x42, AddressMode.HighMem)); // SCY
                EmitLabel(flushSkipBg);

                EmitLoadA(ScrollDirtyVar);
                EmitAsm("AND_IMM", new AsmOperand(ScrollDirtyWinMask, AddressMode.Immediate));
                EmitAsm("JR_Z", flushSkipWin);
                EmitLoadA(ScrollWinXNextVar);
                EmitStoreA(ScrollWinXCurVar);
                EmitAsm("LDH_MEM_A", new AsmOperand(0x4B, AddressMode.HighMem)); // WX
                EmitLoadA(ScrollWinYNextVar);
                EmitStoreA(ScrollWinYCurVar);
                EmitAsm("LDH_MEM_A", new AsmOperand(0x4A, AddressMode.HighMem)); // WY
                EmitLabel(flushSkipWin);

                EmitAsm("XOR_A");
                EmitStoreA(ScrollDirtyVar);
                return;
            }
__scroll_split_commit

保存窗口的基础可见性状态,并启用分段事件表中的第一个扫描行事件。

语法

c
void __scroll_split_commit();

返回值

无返回值,本条说明受影响状态及生效时机。

使用条件与限制

共享事件表最多容纳八项。追加操作既不应用事件,也不排序;超出的项会被忽略。请在安全的帧边界按 reset→push→commit 的顺序重建事件表,并为相邻事件留出足够间隔,使处理程序能及时结束。由于中断延迟,计划触发的扫描行不一定是第一个受影响像素所在的行。事件表非空时,会在 IE 中启用 VBlank/STAT 并执行 EI。空表会关闭 LYC 中断源,但不恢复 IE 或 CPU 中断状态。

使用示例

c
__scroll_split_commit();

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

预期结果

将屏幕分为三个滚动带,使背景 X 按 0→8→16 变化。三个 24×16 矩形分别出现在屏幕坐标 (32,16)、(24,80) 和 (16,112)。事件安排在 LY=64 和 104。矩形在 DMG 上为黑色,在 CGB 上为红色。

__scroll_split_commit: DMG
DMG
__scroll_split_commit: CGB
CGB

完整示例程序

构建命令

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqgb\kitaqgb.exe .\kitaq-docs\samples\api-examples\gb\batch2_split_intrinsics.c -I .\kitaqgb\lib -I .\kitaq-docs\samples -o .\out\batch2_split_intrinsics.gb --no-cache --no-disasm --profile=dev --rst-disable --stack-bank=fixed --cgb=cgb --cart=mbc5 --romsize=64k
实现与源码

kitaqgb/lib/scroll.h:33

kitaqgb/kitaqgb/CodeGenerator.cs:7959

csharp
if (funcName == "__scroll_split_commit")
            {
                if (args.Length != 0) Error(expr, "__scroll_split_commit() expects 0 arguments");
                EnsureScrollState();
                UseScrollSplitHelpers = true;
                EmitAsm("CALL", new AsmOperand("__kq_scroll_split_commit_core", AddressMode.Absolute));
                return;
            }

void AppendScrollHelpersIfUsed(List<Expr> rawLines)
    {
        bool needScrollHelpers = UseScrollSplitHelpers;
        if (!needScrollHelpers)
        {
            foreach (var e in rawLines)
            {
                string m;
                AsmOperand o;
                if (!e.Match(Tag.Asm, out m, out o) || o == null || !o.Base.HasValue) continue;
                string target = o.Base.Value;
                if (target == "__kq_scroll_split_reset_core" ||
                    target == "__kq_scroll_split_push_core" ||
                    target == "__kq_scroll_split_commit_core")
                {
                    needScrollHelpers = true;
                    break;
                }
            }
        }

        if (!needScrollHelpers) return;

        EnsureScrollState();

        var injected = new List<Expr>();
        injected.Add(Expr.Make(Tag.Comment, "[KITAQGB] injected scroll split helpers"));

        // Reset the split count/index and disable STAT coincidence interrupts; existing table bytes are left in place.
        string resetDone = "__kq_scroll_split_reset_done";
        injected.Add(Expr.Make(Tag.Function, "__kq_scroll_split_reset_core"));
        injected.Add(Expr.MakeAsm("XOR_A"));
        injected.Add(Expr.MakeAsm("LD_MEM_A", ScrollSplitCountVar));
        injected.Add(Expr.MakeAsm("LD_MEM_A", ScrollSplitEnabledVar));
        injected.Add(Expr.MakeAsm("LD_MEM_A", ScrollSplitIndexVar));
        injected.Add(Expr.MakeAsm("LDH_A_MEM", new AsmOperand(0x41, AddressMode.HighMem))); // STAT
        injected.Add(Expr.MakeAsm("AND_IMM", new AsmOperand(0xBF, AddressMode.Immediate)));
        injected.Add(Expr.MakeAsm("LDH_MEM_A", new AsmOperand(0x41, AddressMode.HighMem)));
        injected.Add(Expr.Make(Tag.Label, resetDone));
        injected.Add(Expr.MakeAsm("RET"));

        // Append staged LY, SCX, SCY, WX, WY and masked flags; silently ignore entries beyond the eight-slot capacity.
        string pushHaveRoom = "__kq_scroll_split_push_have_room";
        injected.Add(Expr.Make(Tag.Function, "__kq_scroll_split_push_core"));
        injected.Add(Expr.MakeAsm("LD_A_MEM", ScrollSplitCountVar));
        injected.Add(Expr.MakeAsm("CP_IMM", new AsmOperand(ScrollSplitMaxEntries, AddressMode.Immediate)));
        injected.Add(Expr.MakeAsm("JR_C", new AsmOperand(pushHaveRoom, AddressMode.Absolute)));
        injected.Add(Expr.MakeAsm("RET"));
        injected.Add(Expr.Make(Tag.Label, pushHaveRoom));
        injected.Add(Expr.MakeAsm("LD_E_A"));
        injected.Add(Expr.MakeAsm("LD_D_IMM", new AsmOperand(0, AddressMode.Immediate)));
        injected.Add(Expr.MakeAsm("LD_L_A"));
        injected.Add(Expr.MakeAsm("LD_H_IMM", new AsmOperand(0, AddressMode.Immediate)));
        injected.Add(Expr.MakeAsm("ADD_HL_HL"));
        injected.Add(Expr.MakeAsm("ADD_HL_DE"));
        injected.Add(Expr.MakeAsm("ADD_HL_HL"));
        injected.Add(Expr.MakeAsm("LD_DE_IMM", new AsmOperand(ScrollSplitTableAddr, AddressMode.Immediate)));
        injected.Add(Expr.MakeAsm("ADD_HL_DE"));
        injected.Add(Expr.MakeAsm("LD_A_MEM", ScrollSplitTmpLyVar));
        injected.Add(Expr.MakeAsm("LDI_HL_A"));
        injected.Add(Expr.MakeAsm("LD_A_MEM", ScrollSplitTmpScxVar));
        injected.Add(Expr.MakeAsm("LDI_HL_A"));
        injected.Add(Expr.MakeAsm("LD_A_MEM", ScrollSplitTmpScyVar));
        injected.Add(Expr.MakeAsm("LDI_HL_A"));
        injected.Add(Expr.MakeAsm("LD_A_MEM", ScrollSplitTmpWxVar));
        injected.Add(Expr.MakeAsm("LDI_HL_A"));
        injected.Add(Expr.MakeAsm("LD_A_MEM", ScrollSplitTmpWyVar));
        injected.Add(Expr.MakeAsm("LDI_HL_A"));
        injected.Add(Expr.MakeAsm("LD_A_MEM", ScrollSplitTmpFlagsVar));
        injected.Add(Expr.MakeAsm("AND_IMM", new AsmOperand(ScrollSplitFlagMask, AddressMode.Immediate)));
        injected.Add(Expr.MakeAsm("LDI_HL_A"));
        injected.Add(Expr.MakeAsm("LD_A_MEM", ScrollSplitCountVar));
        injected.Add(Expr.MakeAsm("INC_A"));
        injected.Add(Expr.MakeAsm("LD_MEM_A", ScrollSplitCountVar));
        injected.Add(Expr.MakeAsm("RET"));

        // Schedule entry A by programming LYC and STAT bit 6, or disable that bit when A is outside the current count.
        string schedDisable = "__kq_scroll_schedule_next_disable";
        string schedDisabled = "__kq_scroll_schedule_next_disabled";
        string schedHaveTarget = "__kq_scroll_schedule_next_have_target";
        injected.Add(Expr.Make(Tag.Function, "__kq_scroll_schedule_next"));
        injected.Add(Expr.MakeAsm("LD_B_A"));
        injected.Add(Expr.MakeAsm("LD_A_MEM", ScrollSplitCountVar));
        injected.Add(Expr.MakeAsm("CP_B"));
        injected.Add(Expr.MakeAsm("JR_C", new AsmOperand(schedDisable, AddressMode.Absolute)));
        injected.Add(Expr.MakeAsm("JR_Z", new AsmOperand(schedDisable, AddressMode.Absolute)));
        injected.Add(Expr.Make(Tag.Label, schedHaveTarget));
        injected.Add(Expr.MakeAsm("LD_A_B"));
        injected.Add(Expr.MakeAsm("LD_E_A"));
        injected.Add(Expr.MakeAsm("LD_D_IMM", new AsmOperand(0, AddressMode.Immediate)));
        injected.Add(Expr.MakeAsm("LD_L_A"));
        injected.Add(Expr.MakeAsm("LD_H_IMM", new AsmOperand(0, AddressMode.Immediate)));
        injected.Add(Expr.MakeAsm("ADD_HL_HL"));
        injected.Add(Expr.MakeAsm("ADD_HL_DE"));
        injected.Add(Expr.MakeAsm("ADD_HL_HL"));
        injected.Add(Expr.MakeAsm("LD_DE_IMM", new AsmOperand(ScrollSplitTableAddr, AddressMode.Immediate)));
        injected.Add(Expr.MakeAsm("ADD_HL_DE"));
        injected.Add(Expr.MakeAsm("LD_A_HL"));
        injected.Add(Expr.MakeAsm("LDH_MEM_A", new AsmOperand(0x45, AddressMode.HighMem))); // LYC
        injected.Add(Expr.MakeAsm("LDH_A_MEM", new AsmOperand(0x41, AddressMode.HighMem))); // STAT
        injected.Add(Expr.MakeAsm("OR_IMM", new AsmOperand(0x40, AddressMode.Immediate)));
        injected.Add(Expr.MakeAsm("LDH_MEM_A", new AsmOperand(0x41, AddressMode.HighMem)));
        injected.Add(Expr.MakeAsm("RET"));
        injected.Add(Expr.Make(Tag.Label, schedDisable));
        // Do not rewrite a disabled STAT source: on DMG even writing zero
        // can raise a fresh request and repeatedly re-enter the STAT handler.
        injected.Add(Expr.MakeAsm("LDH_A_MEM", new AsmOperand(0x41, AddressMode.HighMem)));
        injected.Add(Expr.MakeAsm("AND_IMM", new AsmOperand(0x40, AddressMode.Immediate)));
        injected.Add(Expr.MakeAsm("JR_Z", new AsmOperand(schedDisabled, AddressMode.Absolute)));
        injected.Add(Expr.MakeAsm("LDH_A_MEM", new AsmOperand(0x41, AddressMode.HighMem))); // STAT
        injected.Add(Expr.MakeAsm("AND_IMM", new AsmOperand(0xBF, AddressMode.Immediate)));
        injected.Add(Expr.MakeAsm("LDH_MEM_A", new AsmOperand(0x41, AddressMode.HighMem)));
        injected.Add(Expr.Make(Tag.Label, schedDisabled));
        injected.Add(Expr.MakeAsm("RET"));

        // Apply the fields selected by an entry's flags. Show follows hide when both are set; palette color zero reuses SCX/SCY bytes.
        string applySkipBg = "__kq_scroll_apply_entry_skip_bg";
        string applySkipWin = "__kq_scroll_apply_entry_skip_win";
        string applySkipHide = "__kq_scroll_apply_entry_skip_hide";
        string applySkipBgColor0 = "__kq_scroll_apply_entry_skip_bg_color0";
        string applyWaitPalette = "__kq_scroll_apply_entry_wait_palette";
        string applyDone = "__kq_scroll_apply_entry_done";
        injected.Add(Expr.Make(Tag.Function, "__kq_scroll_apply_entry"));
        injected.Add(Expr.MakeAsm("INC_HL")); // scx
        injected.Add(Expr.MakeAsm("LD_A_HL"));
        injected.Add(Expr.MakeAsm("LD_B_A"));
        injected.Add(Expr.MakeAsm("INC_HL")); // scy
        injected.Add(Expr.MakeAsm("LD_A_HL"));
        injected.Add(Expr.MakeAsm("LD_C_A"));
        injected.Add(Expr.MakeAsm("INC_HL")); // wx
        injected.Add(Expr.MakeAsm("LD_A_HL"));
        injected.Add(Expr.MakeAsm("LD_D_A"));
        injected.Add(Expr.MakeAsm("INC_HL")); // wy
        injected.Add(Expr.MakeAsm("LD_A_HL"));
        injected.Add(Expr.MakeAsm("LD_E_A"));
        injected.Add(Expr.MakeAsm("INC_HL")); // flags
        injected.Add(Expr.MakeAsm("LD_A_HL"));
        injected.Add(Expr.MakeAsm("AND_IMM", new AsmOperand(ScrollSplitFlagMask, AddressMode.Immediate)));
        injected.Add(Expr.MakeAsm("LD_H_A"));
        injected.Add(Expr.MakeAsm("LD_A_H"));
        injected.Add(Expr.MakeAsm("AND_IMM", new AsmOperand(ScrollSplitFlagUseBg, AddressMode.Immediate)));
        injected.Add(Expr.MakeAsm("JR_Z", new AsmOperand(applySkipBg, AddressMode.Absolute)));
        injected.Add(Expr.MakeAsm("LD_A_B"));
        injected.Add(Expr.MakeAsm("LDH_MEM_A", new AsmOperand(0x43, AddressMode.HighMem))); // SCX
        injected.Add(Expr.MakeAsm("LD_A_C"));
        injected.Add(Expr.MakeAsm("LDH_MEM_A", new AsmOperand(0x42, AddressMode.HighMem))); // SCY
        injected.Add(Expr.Make(Tag.Label, applySkipBg));
        injected.Add(Expr.MakeAsm("LD_A_H"));
        injected.Add(Expr.MakeAsm("AND_IMM", new AsmOperand(ScrollSplitFlagUseWin, AddressMode.Immediate)));
        injected.Add(Expr.MakeAsm("JR_Z", new AsmOperand(applySkipWin, AddressMode.Absolute)));
        injected.Add(Expr.MakeAsm("LD_A_D"));
        injected.Add(Expr.MakeAsm("LDH_MEM_A", new AsmOperand(0x4B, AddressMode.HighMem))); // WX
        injected.Add(Expr.MakeAsm("LD_A_E"));
        injected.Add(Expr.MakeAsm("LDH_MEM_A", new AsmOperand(0x4A, AddressMode.HighMem))); // WY
        injected.Add(Expr.Make(Tag.Label, applySkipWin));
        injected.Add(Expr.MakeAsm("LD_A_H"));
        injected.Add(Expr.MakeAsm("AND_IMM", new AsmOperand(ScrollSplitFlagWinHide, AddressMode.Immediate)));
        injected.Add(Expr.MakeAsm("JR_Z", new AsmOperand(applySkipHide, AddressMode.Absolute)));
        injected.Add(Expr.MakeAsm("LDH_A_MEM", new AsmOperand(0x40, AddressMode.HighMem))); // LCDC
        injected.Add(Expr.MakeAsm("AND_IMM", new AsmOperand(0xDF, AddressMode.Immediate)));
        injected.Add(Expr.MakeAsm("LDH_MEM_A", new AsmOperand(0x40, AddressMode.HighMem)));
        injected.Add(Expr.Make(Tag.Label, applySkipHide));
        injected.Add(Expr.MakeAsm("LD_A_H"));
        injected.Add(Expr.MakeAsm("AND_IMM", new AsmOperand(ScrollSplitFlagWinShow, AddressMode.Immediate)));
        injected.Add(Expr.MakeAsm("JR_Z", new AsmOperand(applySkipBgColor0, AddressMode.Absolute)));
        injected.Add(Expr.MakeAsm("LDH_A_MEM", new AsmOperand(0x40, AddressMode.HighMem))); // LCDC
        injected.Add(Expr.MakeAsm("OR_IMM", new AsmOperand(0x20, AddressMode.Immediate)));
        injected.Add(Expr.MakeAsm("LDH_MEM_A", new AsmOperand(0x40, AddressMode.HighMem)));
        injected.Add(Expr.Make(Tag.Label, applySkipBgColor0));
        injected.Add(Expr.MakeAsm("LD_A_H"));
        injected.Add(Expr.MakeAsm("AND_IMM", new AsmOperand(ScrollSplitFlagBgColor0, AddressMode.Immediate)));
        injected.Add(Expr.MakeAsm("JR_Z", new AsmOperand(applyDone, AddressMode.Absolute)));
        // CGB palette data writes are blocked during pixel transfer. Wait through
        // OAM search as well, so both color bytes fit in the following HBlank.
        // The LY-zero entry runs in VBlank and does not wait here.
        injected.Add(Expr.Make(Tag.Label, applyWaitPalette));
        injected.Add(Expr.MakeAsm("LDH_A_MEM", new AsmOperand(0x41, AddressMode.HighMem)));
        injected.Add(Expr.MakeAsm("AND_IMM", new AsmOperand(0x02, AddressMode.Immediate)));
        injected.Add(Expr.MakeAsm("JR_NZ", new AsmOperand(applyWaitPalette, AddressMode.Absolute)));
        injected.Add(Expr.MakeAsm("LD_A_IMM", new AsmOperand(0x80, AddressMode.Immediate)));
        injected.Add(Expr.MakeAsm("LDH_MEM_A", new AsmOperand(0x68, AddressMode.HighMem))); // BCPS: palette 0, color 0, auto increment
        injected.Add(Expr.MakeAsm("LD_A_B"));
        injected.Add(Expr.MakeAsm("LDH_MEM_A", new AsmOperand(0x69, AddressMode.HighMem))); // BCPD low
        injected.Add(Expr.MakeAsm("LD_A_C"));
        injected.Add(Expr.MakeAsm("LDH_MEM_A", new AsmOperand(0x69, AddressMode.HighMem))); // BCPD high
        injected.Add(Expr.Make(Tag.Label, applyDone));
        injected.Add(Expr.MakeAsm("RET"));

        // Capture base window visibility and arm a nonempty table at index zero, enabling VBlank/STAT interrupts and issuing EI.
        string commitDisable = "__kq_scroll_split_commit_disable";
        injected.Add(Expr.Make(Tag.Function, "__kq_scroll_split_commit_core"));
        injected.Add(Expr.MakeAsm("LDH_A_MEM", new AsmOperand(0x40, AddressMode.HighMem))); // LCDC
        injected.Add(Expr.MakeAsm("AND_IMM", new AsmOperand(0x20, AddressMode.Immediate)));
        injected.Add(Expr.MakeAsm("LD_MEM_A", ScrollWinVisibleVar));
        injected.Add(Expr.MakeAsm("LD_A_MEM", ScrollSplitCountVar));
        injected.Add(Expr.MakeAsm("OR_A"));
        injected.Add(Expr.MakeAsm("JR_Z", new AsmOperand(commitDisable, AddressMode.Absolute)));
        injected.Add(Expr.MakeAsm("LD_A_IMM", new AsmOperand(1, AddressMode.Immediate)));
        injected.Add(Expr.MakeAsm("LD_MEM_A", ScrollSplitEnabledVar));
        injected.Add(Expr.MakeAsm("XOR_A"));
        injected.Add(Expr.MakeAsm("LD_MEM_A", ScrollSplitIndexVar));
        injected.Add(Expr.MakeAsm("CALL", new AsmOperand("__kq_scroll_schedule_next", AddressMode.Absolute)));
        injected.Add(Expr.MakeAsm("LD_A_MEM", new AsmOperand(0xFFFF, AddressMode.Absolute))); // IE
        injected.Add(Expr.MakeAsm("OR_IMM", new AsmOperand(0x03, AddressMode.Immediate))); // VBlank | STAT
        injected.Add(Expr.MakeAsm("LD_MEM_A", new AsmOperand(0xFFFF, AddressMode.Absolute)));
        injected.Add(Expr.MakeAsm("EI"));
        injected.Add(Expr.MakeAsm("RET"));
        injected.Add(Expr.Make(Tag.Label, commitDisable));
        injected.Add(Expr.MakeAsm("XOR_A"));
        injected.Add(Expr.MakeAsm("LD_MEM_A", ScrollSplitEnabledVar));
        injected.Add(Expr.MakeAsm("LD_MEM_A", ScrollSplitIndexVar));
        injected.Add(Expr.MakeAsm("CALL", new AsmOperand("__kq_scroll_schedule_next", AddressMode.Absolute)));
        injected.Add(Expr.MakeAsm("RET"));

        // Save registers/SVBK, select WRAM bank 1, restore base scroll state and restart the split sequence for the frame.
        // A first entry at LY zero is applied immediately before scheduling the following entry.
        string vbDisable = "__kq_vblank_disable";
        string vbFirstNonZero = "__kq_vblank_first_nonzero";
        string vbDone = "__kq_vblank_done";
        injected.Add(Expr.Make(Tag.Function, "__kq_vblank_vector"));
        injected.Add(Expr.MakeAsm("PUSH_AF"));
        injected.Add(Expr.MakeAsm("PUSH_BC"));
        injected.Add(Expr.MakeAsm("PUSH_DE"));
        injected.Add(Expr.MakeAsm("PUSH_HL"));
        injected.Add(Expr.MakeAsm("LDH_A_MEM", new AsmOperand(0x70, AddressMode.HighMem))); // SVBK
        injected.Add(Expr.MakeAsm("PUSH_AF"));
        injected.Add(Expr.MakeAsm("LD_A_IMM", new AsmOperand(1, AddressMode.Immediate)));
        injected.Add(Expr.MakeAsm("LDH_MEM_A", new AsmOperand(0x70, AddressMode.HighMem)));
        injected.Add(Expr.MakeAsm("LD_A_IMM", new AsmOperand(1, AddressMode.Immediate)));
        // Set the existing fixed-address frame-ready flag used by the VBlank path.
        injected.Add(Expr.MakeAsm("LD_MEM_A", new AsmOperand(0xC29C, AddressMode.Absolute)));
        injected.Add(Expr.MakeAsm("LD_A_MEM", ScrollBgXCurVar));
        injected.Add(Expr.MakeAsm("LDH_MEM_A", new AsmOperand(0x43, AddressMode.HighMem))); // SCX
        injected.Add(Expr.MakeAsm("LD_A_MEM", ScrollBgYCurVar));
        injected.Add(Expr.MakeAsm("LDH_MEM_A", new AsmOperand(0x42, AddressMode.HighMem))); // SCY
        injected.Add(Expr.MakeAsm("LD_A_MEM", ScrollWinXCurVar));
        injected.Add(Expr.MakeAsm("LDH_MEM_A", new AsmOperand(0x4B, AddressMode.HighMem))); // WX
        injected.Add(Expr.MakeAsm("LD_A_MEM", ScrollWinYCurVar));
        injected.Add(Expr.MakeAsm("LDH_MEM_A", new AsmOperand(0x4A, AddressMode.HighMem))); // WY
        injected.Add(Expr.MakeAsm("LDH_A_MEM", new AsmOperand(0x40, AddressMode.HighMem))); // LCDC
        injected.Add(Expr.MakeAsm("AND_IMM", new AsmOperand(0xDF, AddressMode.Immediate)));
        injected.Add(Expr.MakeAsm("LD_B_A"));
        injected.Add(Expr.MakeAsm("LD_A_MEM", ScrollWinVisibleVar));
        injected.Add(Expr.MakeAsm("OR_B"));
        injected.Add(Expr.MakeAsm("LDH_MEM_A", new AsmOperand(0x40, AddressMode.HighMem)));
        injected.Add(Expr.MakeAsm("LD_A_MEM", ScrollSplitEnabledVar));
        injected.Add(Expr.MakeAsm("OR_A"));
        injected.Add(Expr.MakeAsm("JR_Z", new AsmOperand(vbDisable, AddressMode.Absolute)));
        injected.Add(Expr.MakeAsm("LD_A_MEM", ScrollSplitCountVar));
        injected.Add(Expr.MakeAsm("OR_A"));
        injected.Add(Expr.MakeAsm("JR_Z", new AsmOperand(vbDisable, AddressMode.Absolute)));
        injected.Add(Expr.MakeAsm("XOR_A"));
        injected.Add(Expr.MakeAsm("LD_MEM_A", ScrollSplitIndexVar));
        injected.Add(Expr.MakeAsm("LD_A_MEM", new AsmOperand(ScrollSplitTableAddr, AddressMode.Absolute)));
        injected.Add(Expr.MakeAsm("OR_A"));
        injected.Add(Expr.MakeAsm("JR_NZ", new AsmOperand(vbFirstNonZero, AddressMode.Absolute)));
        injected.Add(Expr.MakeAsm("LD_HL_IMM", new AsmOperand(ScrollSplitTableAddr, AddressMode.Immediate)));
        injected.Add(Expr.MakeAsm("CALL", new AsmOperand("__kq_scroll_apply_entry", AddressMode.Absolute)));
        injected.Add(Expr.MakeAsm("LD_A_IMM", new AsmOperand(1, AddressMode.Immediate)));
        injected.Add(Expr.MakeAsm("LD_MEM_A", ScrollSplitIndexVar));
        injected.Add(Expr.MakeAsm("CALL", new AsmOperand("__kq_scroll_schedule_next", AddressMode.Absolute)));
        injected.Add(Expr.MakeAsm("JR", new AsmOperand(vbDone, AddressMode.Absolute)));
        injected.Add(Expr.Make(Tag.Label, vbFirstNonZero));
        injected.Add(Expr.MakeAsm("XOR_A"));
        injected.Add(Expr.MakeAsm("CALL", new AsmOperand("__kq_scroll_schedule_next", AddressMode.Absolute)));
        injected.Add(Expr.MakeAsm("JR", new AsmOperand(vbDone, AddressMode.Absolute)));
        injected.Add(Expr.Make(Tag.Label, vbDisable));
        injected.Add(Expr.MakeAsm("CALL", new AsmOperand("__kq_scroll_schedule_next", AddressMode.Absolute)));
        injected.Add(Expr.Make(Tag.Label, vbDone));
        injected.Add(Expr.MakeAsm("POP_AF"));
        injected.Add(Expr.MakeAsm("LDH_MEM_A", new AsmOperand(0x70, AddressMode.HighMem))); // restore SVBK
        injected.Add(Expr.MakeAsm("POP_HL"));
        injected.Add(Expr.MakeAsm("POP_DE"));
        injected.Add(Expr.MakeAsm("POP_BC"));
        injected.Add(Expr.MakeAsm("POP_AF"));
        injected.Add(Expr.MakeAsm("RETI"));

        // Save registers/SVBK, apply the current entry and advance its index, or disable further coincidence interrupts at the end.
        string statDisable = "__kq_stat_disable";
        string statDone = "__kq_stat_done";
        injected.Add(Expr.Make(Tag.Function, "__kq_stat_vector"));
        injected.Add(Expr.MakeAsm("PUSH_AF"));
        injected.Add(Expr.MakeAsm("PUSH_BC"));
        injected.Add(Expr.MakeAsm("PUSH_DE"));
        injected.Add(Expr.MakeAsm("PUSH_HL"));
        injected.Add(Expr.MakeAsm("LDH_A_MEM", new AsmOperand(0x70, AddressMode.HighMem))); // SVBK
        injected.Add(Expr.MakeAsm("PUSH_AF"));
        injected.Add(Expr.MakeAsm("LD_A_IMM", new AsmOperand(1, AddressMode.Immediate)));
        injected.Add(Expr.MakeAsm("LDH_MEM_A", new AsmOperand(0x70, AddressMode.HighMem)));
        injected.Add(Expr.MakeAsm("LD_A_MEM", ScrollSplitEnabledVar));
        injected.Add(Expr.MakeAsm("OR_A"));
        injected.Add(Expr.MakeAsm("JR_Z", new AsmOperand(statDisable, AddressMode.Absolute)));
        injected.Add(Expr.MakeAsm("LD_A_MEM", ScrollSplitIndexVar));
        injected.Add(Expr.MakeAsm("LD_B_A"));
        injected.Add(Expr.MakeAsm("LD_A_MEM", ScrollSplitCountVar));
        injected.Add(Expr.MakeAsm("CP_B"));
        injected.Add(Expr.MakeAsm("JR_C", new AsmOperand(statDisable, AddressMode.Absolute)));
        injected.Add(Expr.MakeAsm("JR_Z", new AsmOperand(statDisable, AddressMode.Absolute)));
        injected.Add(Expr.MakeAsm("LD_A_B"));
        injected.Add(Expr.MakeAsm("LD_E_A"));
        injected.Add(Expr.MakeAsm("LD_D_IMM", new AsmOperand(0, AddressMode.Immediate)));
        injected.Add(Expr.MakeAsm("LD_L_A"));
        injected.Add(Expr.MakeAsm("LD_H_IMM", new AsmOperand(0, AddressMode.Immediate)));
        injected.Add(Expr.MakeAsm("ADD_HL_HL"));
        injected.Add(Expr.MakeAsm("ADD_HL_DE"));
        injected.Add(Expr.MakeAsm("ADD_HL_HL"));
        injected.Add(Expr.MakeAsm("LD_DE_IMM", new AsmOperand(ScrollSplitTableAddr, AddressMode.Immediate)));
        injected.Add(Expr.MakeAsm("ADD_HL_DE"));
        // DMG STAT writes can request an interrupt unrelated to LYC. Consume
        // this entry only on its scheduled line; leave the index intact otherwise.
        injected.Add(Expr.MakeAsm("LD_A_HL"));
        injected.Add(Expr.MakeAsm("LD_B_A"));
        injected.Add(Expr.MakeAsm("LDH_A_MEM", new AsmOperand(0x44, AddressMode.HighMem)));
        injected.Add(Expr.MakeAsm("CP_B"));
        injected.Add(Expr.MakeAsm("JR_NZ", new AsmOperand(statDone, AddressMode.Absolute)));
        injected.Add(Expr.MakeAsm("CALL", new AsmOperand("__kq_scroll_apply_entry", AddressMode.Absolute)));
        injected.Add(Expr.MakeAsm("LD_A_MEM", ScrollSplitIndexVar));
        injected.Add(Expr.MakeAsm("INC_A"));
        injected.Add(Expr.MakeAsm("LD_MEM_A", ScrollSplitIndexVar));
        injected.Add(Expr.MakeAsm("CALL", new AsmOperand("__kq_scroll_schedule_next", AddressMode.Absolute)));
        injected.Add(Expr.MakeAsm("JR", new AsmOperand(statDone, AddressMode.Absolute)));
        injected.Add(Expr.Make(Tag.Label, statDisable));
        // Acknowledge an already-disabled source without writing STAT again.
        injected.Add(Expr.MakeAsm("LDH_A_MEM", new AsmOperand(0x41, AddressMode.HighMem)));
        injected.Add(Expr.MakeAsm("AND_IMM", new AsmOperand(0x40, AddressMode.Immediate)));
        injected.Add(Expr.MakeAsm("JR_Z", new AsmOperand(statDone, AddressMode.Absolute)));
        injected.Add(Expr.MakeAsm("LDH_A_MEM", new AsmOperand(0x41, AddressMode.HighMem))); // STAT
        injected.Add(Expr.MakeAsm("AND_IMM", new AsmOperand(0xBF, AddressMode.Immediate)));
        injected.Add(Expr.MakeAsm("LDH_MEM_A", new AsmOperand(0x41, AddressMode.HighMem)));
        injected.Add(Expr.Make(Tag.Label, statDone));
        injected.Add(Expr.MakeAsm("POP_AF"));
        injected.Add(Expr.MakeAsm("LDH_MEM_A", new AsmOperand(0x70, AddressMode.HighMem))); // restore SVBK
        injected.Add(Expr.MakeAsm("POP_HL"));
        injected.Add(Expr.MakeAsm("POP_DE"));
        injected.Add(Expr.MakeAsm("POP_BC"));
        injected.Add(Expr.MakeAsm("POP_AF"));
        injected.Add(Expr.MakeAsm("RETI"));

        int insertAt = 0;
        while (insertAt < rawLines.Count && rawLines[insertAt].MatchTag(Tag.RstMap)) insertAt++;
        rawLines.InsertRange(insertAt, injected);
    }
__scroll_split_push

追加一个分段事件,在指定扫描行事件触发时改变背景滚动位置。

语法

c
void __scroll_split_push(u8 ly, u8 scx, u8 scy);

参数

ly

扫描行事件编号;可见区域使用 0–143,并按升序追加。若第一项的行号为零,则会在 VBlank 期间应用。

scx

背景水平滚动量,范围为 0–255 像素;增大该值会使背景向左移动。

scy

背景垂直滚动量,范围为 0–255 像素;增大该值会使背景向上移动。

返回值

无返回值,本条说明受影响状态及生效时机。

使用条件与限制

共享事件表最多容纳八项。追加操作既不应用事件,也不排序;超出的项会被忽略。请在安全的帧边界按 reset→push→commit 的顺序重建事件表,并为相邻事件留出足够间隔,使处理程序能及时结束。由于中断延迟,计划触发的扫描行不一定是第一个受影响像素所在的行。

使用示例

c
__scroll_split_push(64,8,0);

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

预期结果

将屏幕分为三个滚动带,使背景 X 按 0→8→16 变化。三个 24×16 矩形分别出现在屏幕坐标 (32,16)、(24,80) 和 (16,112)。事件安排在 LY=64 和 104。矩形在 DMG 上为黑色,在 CGB 上为红色。

__scroll_split_push: DMG
DMG
__scroll_split_push: CGB
CGB

完整示例程序

构建命令

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqgb\kitaqgb.exe .\kitaq-docs\samples\api-examples\gb\batch2_split_intrinsics.c -I .\kitaqgb\lib -I .\kitaq-docs\samples -o .\out\batch2_split_intrinsics.gb --no-cache --no-disasm --profile=dev --rst-disable --stack-bank=fixed --cgb=cgb --cart=mbc5 --romsize=64k
实现与源码

kitaqgb/lib/scroll.h:31

kitaqgb/kitaqgb/CodeGenerator.cs:7907

csharp
if (funcName == "__scroll_split_push")
            {
                if (args.Length != 3) Error(expr, "__scroll_split_push(ly, scx, scy) expects 3 arguments");
                EnsureScrollState();
                UseScrollSplitHelpers = true;

                CompileIntoA(args[0]);
                EmitStoreA(ScrollSplitTmpLyVar);
                CompileIntoA(args[1]);
                EmitStoreA(ScrollSplitTmpScxVar);
                CompileIntoA(args[2]);
                EmitStoreA(ScrollSplitTmpScyVar);
                EmitAsm("XOR_A");
                EmitStoreA(ScrollSplitTmpWxVar);
                EmitStoreA(ScrollSplitTmpWyVar);
                EmitAsm("LD_A_IMM", new AsmOperand(ScrollSplitFlagUseBg, AddressMode.Immediate));
                EmitStoreA(ScrollSplitTmpFlagsVar);
                EmitAsm("CALL", new AsmOperand("__kq_scroll_split_push_core", AddressMode.Absolute));
                return;
            }

void AppendScrollHelpersIfUsed(List<Expr> rawLines)
    {
        bool needScrollHelpers = UseScrollSplitHelpers;
        if (!needScrollHelpers)
        {
            foreach (var e in rawLines)
            {
                string m;
                AsmOperand o;
                if (!e.Match(Tag.Asm, out m, out o) || o == null || !o.Base.HasValue) continue;
                string target = o.Base.Value;
                if (target == "__kq_scroll_split_reset_core" ||
                    target == "__kq_scroll_split_push_core" ||
                    target == "__kq_scroll_split_commit_core")
                {
                    needScrollHelpers = true;
                    break;
                }
            }
        }

        if (!needScrollHelpers) return;

        EnsureScrollState();

        var injected = new List<Expr>();
        injected.Add(Expr.Make(Tag.Comment, "[KITAQGB] injected scroll split helpers"));

        // Reset the split count/index and disable STAT coincidence interrupts; existing table bytes are left in place.
        string resetDone = "__kq_scroll_split_reset_done";
        injected.Add(Expr.Make(Tag.Function, "__kq_scroll_split_reset_core"));
        injected.Add(Expr.MakeAsm("XOR_A"));
        injected.Add(Expr.MakeAsm("LD_MEM_A", ScrollSplitCountVar));
        injected.Add(Expr.MakeAsm("LD_MEM_A", ScrollSplitEnabledVar));
        injected.Add(Expr.MakeAsm("LD_MEM_A", ScrollSplitIndexVar));
        injected.Add(Expr.MakeAsm("LDH_A_MEM", new AsmOperand(0x41, AddressMode.HighMem))); // STAT
        injected.Add(Expr.MakeAsm("AND_IMM", new AsmOperand(0xBF, AddressMode.Immediate)));
        injected.Add(Expr.MakeAsm("LDH_MEM_A", new AsmOperand(0x41, AddressMode.HighMem)));
        injected.Add(Expr.Make(Tag.Label, resetDone));
        injected.Add(Expr.MakeAsm("RET"));

        // Append staged LY, SCX, SCY, WX, WY and masked flags; silently ignore entries beyond the eight-slot capacity.
        string pushHaveRoom = "__kq_scroll_split_push_have_room";
        injected.Add(Expr.Make(Tag.Function, "__kq_scroll_split_push_core"));
        injected.Add(Expr.MakeAsm("LD_A_MEM", ScrollSplitCountVar));
        injected.Add(Expr.MakeAsm("CP_IMM", new AsmOperand(ScrollSplitMaxEntries, AddressMode.Immediate)));
        injected.Add(Expr.MakeAsm("JR_C", new AsmOperand(pushHaveRoom, AddressMode.Absolute)));
        injected.Add(Expr.MakeAsm("RET"));
        injected.Add(Expr.Make(Tag.Label, pushHaveRoom));
        injected.Add(Expr.MakeAsm("LD_E_A"));
        injected.Add(Expr.MakeAsm("LD_D_IMM", new AsmOperand(0, AddressMode.Immediate)));
        injected.Add(Expr.MakeAsm("LD_L_A"));
        injected.Add(Expr.MakeAsm("LD_H_IMM", new AsmOperand(0, AddressMode.Immediate)));
        injected.Add(Expr.MakeAsm("ADD_HL_HL"));
        injected.Add(Expr.MakeAsm("ADD_HL_DE"));
        injected.Add(Expr.MakeAsm("ADD_HL_HL"));
        injected.Add(Expr.MakeAsm("LD_DE_IMM", new AsmOperand(ScrollSplitTableAddr, AddressMode.Immediate)));
        injected.Add(Expr.MakeAsm("ADD_HL_DE"));
        injected.Add(Expr.MakeAsm("LD_A_MEM", ScrollSplitTmpLyVar));
        injected.Add(Expr.MakeAsm("LDI_HL_A"));
        injected.Add(Expr.MakeAsm("LD_A_MEM", ScrollSplitTmpScxVar));
        injected.Add(Expr.MakeAsm("LDI_HL_A"));
        injected.Add(Expr.MakeAsm("LD_A_MEM", ScrollSplitTmpScyVar));
        injected.Add(Expr.MakeAsm("LDI_HL_A"));
        injected.Add(Expr.MakeAsm("LD_A_MEM", ScrollSplitTmpWxVar));
        injected.Add(Expr.MakeAsm("LDI_HL_A"));
        injected.Add(Expr.MakeAsm("LD_A_MEM", ScrollSplitTmpWyVar));
        injected.Add(Expr.MakeAsm("LDI_HL_A"));
        injected.Add(Expr.MakeAsm("LD_A_MEM", ScrollSplitTmpFlagsVar));
        injected.Add(Expr.MakeAsm("AND_IMM", new AsmOperand(ScrollSplitFlagMask, AddressMode.Immediate)));
        injected.Add(Expr.MakeAsm("LDI_HL_A"));
        injected.Add(Expr.MakeAsm("LD_A_MEM", ScrollSplitCountVar));
        injected.Add(Expr.MakeAsm("INC_A"));
        injected.Add(Expr.MakeAsm("LD_MEM_A", ScrollSplitCountVar));
        injected.Add(Expr.MakeAsm("RET"));

        // Schedule entry A by programming LYC and STAT bit 6, or disable that bit when A is outside the current count.
        string schedDisable = "__kq_scroll_schedule_next_disable";
        string schedDisabled = "__kq_scroll_schedule_next_disabled";
        string schedHaveTarget = "__kq_scroll_schedule_next_have_target";
        injected.Add(Expr.Make(Tag.Function, "__kq_scroll_schedule_next"));
        injected.Add(Expr.MakeAsm("LD_B_A"));
        injected.Add(Expr.MakeAsm("LD_A_MEM", ScrollSplitCountVar));
        injected.Add(Expr.MakeAsm("CP_B"));
        injected.Add(Expr.MakeAsm("JR_C", new AsmOperand(schedDisable, AddressMode.Absolute)));
        injected.Add(Expr.MakeAsm("JR_Z", new AsmOperand(schedDisable, AddressMode.Absolute)));
        injected.Add(Expr.Make(Tag.Label, schedHaveTarget));
        injected.Add(Expr.MakeAsm("LD_A_B"));
        injected.Add(Expr.MakeAsm("LD_E_A"));
        injected.Add(Expr.MakeAsm("LD_D_IMM", new AsmOperand(0, AddressMode.Immediate)));
        injected.Add(Expr.MakeAsm("LD_L_A"));
        injected.Add(Expr.MakeAsm("LD_H_IMM", new AsmOperand(0, AddressMode.Immediate)));
        injected.Add(Expr.MakeAsm("ADD_HL_HL"));
        injected.Add(Expr.MakeAsm("ADD_HL_DE"));
        injected.Add(Expr.MakeAsm("ADD_HL_HL"));
        injected.Add(Expr.MakeAsm("LD_DE_IMM", new AsmOperand(ScrollSplitTableAddr, AddressMode.Immediate)));
        injected.Add(Expr.MakeAsm("ADD_HL_DE"));
        injected.Add(Expr.MakeAsm("LD_A_HL"));
        injected.Add(Expr.MakeAsm("LDH_MEM_A", new AsmOperand(0x45, AddressMode.HighMem))); // LYC
        injected.Add(Expr.MakeAsm("LDH_A_MEM", new AsmOperand(0x41, AddressMode.HighMem))); // STAT
        injected.Add(Expr.MakeAsm("OR_IMM", new AsmOperand(0x40, AddressMode.Immediate)));
        injected.Add(Expr.MakeAsm("LDH_MEM_A", new AsmOperand(0x41, AddressMode.HighMem)));
        injected.Add(Expr.MakeAsm("RET"));
        injected.Add(Expr.Make(Tag.Label, schedDisable));
        // Do not rewrite a disabled STAT source: on DMG even writing zero
        // can raise a fresh request and repeatedly re-enter the STAT handler.
        injected.Add(Expr.MakeAsm("LDH_A_MEM", new AsmOperand(0x41, AddressMode.HighMem)));
        injected.Add(Expr.MakeAsm("AND_IMM", new AsmOperand(0x40, AddressMode.Immediate)));
        injected.Add(Expr.MakeAsm("JR_Z", new AsmOperand(schedDisabled, AddressMode.Absolute)));
        injected.Add(Expr.MakeAsm("LDH_A_MEM", new AsmOperand(0x41, AddressMode.HighMem))); // STAT
        injected.Add(Expr.MakeAsm("AND_IMM", new AsmOperand(0xBF, AddressMode.Immediate)));
        injected.Add(Expr.MakeAsm("LDH_MEM_A", new AsmOperand(0x41, AddressMode.HighMem)));
        injected.Add(Expr.Make(Tag.Label, schedDisabled));
        injected.Add(Expr.MakeAsm("RET"));

        // Apply the fields selected by an entry's flags. Show follows hide when both are set; palette color zero reuses SCX/SCY bytes.
        string applySkipBg = "__kq_scroll_apply_entry_skip_bg";
        string applySkipWin = "__kq_scroll_apply_entry_skip_win";
        string applySkipHide = "__kq_scroll_apply_entry_skip_hide";
        string applySkipBgColor0 = "__kq_scroll_apply_entry_skip_bg_color0";
        string applyWaitPalette = "__kq_scroll_apply_entry_wait_palette";
        string applyDone = "__kq_scroll_apply_entry_done";
        injected.Add(Expr.Make(Tag.Function, "__kq_scroll_apply_entry"));
        injected.Add(Expr.MakeAsm("INC_HL")); // scx
        injected.Add(Expr.MakeAsm("LD_A_HL"));
        injected.Add(Expr.MakeAsm("LD_B_A"));
        injected.Add(Expr.MakeAsm("INC_HL")); // scy
        injected.Add(Expr.MakeAsm("LD_A_HL"));
        injected.Add(Expr.MakeAsm("LD_C_A"));
        injected.Add(Expr.MakeAsm("INC_HL")); // wx
        injected.Add(Expr.MakeAsm("LD_A_HL"));
        injected.Add(Expr.MakeAsm("LD_D_A"));
        injected.Add(Expr.MakeAsm("INC_HL")); // wy
        injected.Add(Expr.MakeAsm("LD_A_HL"));
        injected.Add(Expr.MakeAsm("LD_E_A"));
        injected.Add(Expr.MakeAsm("INC_HL")); // flags
        injected.Add(Expr.MakeAsm("LD_A_HL"));
        injected.Add(Expr.MakeAsm("AND_IMM", new AsmOperand(ScrollSplitFlagMask, AddressMode.Immediate)));
        injected.Add(Expr.MakeAsm("LD_H_A"));
        injected.Add(Expr.MakeAsm("LD_A_H"));
        injected.Add(Expr.MakeAsm("AND_IMM", new AsmOperand(ScrollSplitFlagUseBg, AddressMode.Immediate)));
        injected.Add(Expr.MakeAsm("JR_Z", new AsmOperand(applySkipBg, AddressMode.Absolute)));
        injected.Add(Expr.MakeAsm("LD_A_B"));
        injected.Add(Expr.MakeAsm("LDH_MEM_A", new AsmOperand(0x43, AddressMode.HighMem))); // SCX
        injected.Add(Expr.MakeAsm("LD_A_C"));
        injected.Add(Expr.MakeAsm("LDH_MEM_A", new AsmOperand(0x42, AddressMode.HighMem))); // SCY
        injected.Add(Expr.Make(Tag.Label, applySkipBg));
        injected.Add(Expr.MakeAsm("LD_A_H"));
        injected.Add(Expr.MakeAsm("AND_IMM", new AsmOperand(ScrollSplitFlagUseWin, AddressMode.Immediate)));
        injected.Add(Expr.MakeAsm("JR_Z", new AsmOperand(applySkipWin, AddressMode.Absolute)));
        injected.Add(Expr.MakeAsm("LD_A_D"));
        injected.Add(Expr.MakeAsm("LDH_MEM_A", new AsmOperand(0x4B, AddressMode.HighMem))); // WX
        injected.Add(Expr.MakeAsm("LD_A_E"));
        injected.Add(Expr.MakeAsm("LDH_MEM_A", new AsmOperand(0x4A, AddressMode.HighMem))); // WY
        injected.Add(Expr.Make(Tag.Label, applySkipWin));
        injected.Add(Expr.MakeAsm("LD_A_H"));
        injected.Add(Expr.MakeAsm("AND_IMM", new AsmOperand(ScrollSplitFlagWinHide, AddressMode.Immediate)));
        injected.Add(Expr.MakeAsm("JR_Z", new AsmOperand(applySkipHide, AddressMode.Absolute)));
        injected.Add(Expr.MakeAsm("LDH_A_MEM", new AsmOperand(0x40, AddressMode.HighMem))); // LCDC
        injected.Add(Expr.MakeAsm("AND_IMM", new AsmOperand(0xDF, AddressMode.Immediate)));
        injected.Add(Expr.MakeAsm("LDH_MEM_A", new AsmOperand(0x40, AddressMode.HighMem)));
        injected.Add(Expr.Make(Tag.Label, applySkipHide));
        injected.Add(Expr.MakeAsm("LD_A_H"));
        injected.Add(Expr.MakeAsm("AND_IMM", new AsmOperand(ScrollSplitFlagWinShow, AddressMode.Immediate)));
        injected.Add(Expr.MakeAsm("JR_Z", new AsmOperand(applySkipBgColor0, AddressMode.Absolute)));
        injected.Add(Expr.MakeAsm("LDH_A_MEM", new AsmOperand(0x40, AddressMode.HighMem))); // LCDC
        injected.Add(Expr.MakeAsm("OR_IMM", new AsmOperand(0x20, AddressMode.Immediate)));
        injected.Add(Expr.MakeAsm("LDH_MEM_A", new AsmOperand(0x40, AddressMode.HighMem)));
        injected.Add(Expr.Make(Tag.Label, applySkipBgColor0));
        injected.Add(Expr.MakeAsm("LD_A_H"));
        injected.Add(Expr.MakeAsm("AND_IMM", new AsmOperand(ScrollSplitFlagBgColor0, AddressMode.Immediate)));
        injected.Add(Expr.MakeAsm("JR_Z", new AsmOperand(applyDone, AddressMode.Absolute)));
        // CGB palette data writes are blocked during pixel transfer. Wait through
        // OAM search as well, so both color bytes fit in the following HBlank.
        // The LY-zero entry runs in VBlank and does not wait here.
        injected.Add(Expr.Make(Tag.Label, applyWaitPalette));
        injected.Add(Expr.MakeAsm("LDH_A_MEM", new AsmOperand(0x41, AddressMode.HighMem)));
        injected.Add(Expr.MakeAsm("AND_IMM", new AsmOperand(0x02, AddressMode.Immediate)));
        injected.Add(Expr.MakeAsm("JR_NZ", new AsmOperand(applyWaitPalette, AddressMode.Absolute)));
        injected.Add(Expr.MakeAsm("LD_A_IMM", new AsmOperand(0x80, AddressMode.Immediate)));
        injected.Add(Expr.MakeAsm("LDH_MEM_A", new AsmOperand(0x68, AddressMode.HighMem))); // BCPS: palette 0, color 0, auto increment
        injected.Add(Expr.MakeAsm("LD_A_B"));
        injected.Add(Expr.MakeAsm("LDH_MEM_A", new AsmOperand(0x69, AddressMode.HighMem))); // BCPD low
        injected.Add(Expr.MakeAsm("LD_A_C"));
        injected.Add(Expr.MakeAsm("LDH_MEM_A", new AsmOperand(0x69, AddressMode.HighMem))); // BCPD high
        injected.Add(Expr.Make(Tag.Label, applyDone));
        injected.Add(Expr.MakeAsm("RET"));

        // Capture base window visibility and arm a nonempty table at index zero, enabling VBlank/STAT interrupts and issuing EI.
        string commitDisable = "__kq_scroll_split_commit_disable";
        injected.Add(Expr.Make(Tag.Function, "__kq_scroll_split_commit_core"));
        injected.Add(Expr.MakeAsm("LDH_A_MEM", new AsmOperand(0x40, AddressMode.HighMem))); // LCDC
        injected.Add(Expr.MakeAsm("AND_IMM", new AsmOperand(0x20, AddressMode.Immediate)));
        injected.Add(Expr.MakeAsm("LD_MEM_A", ScrollWinVisibleVar));
        injected.Add(Expr.MakeAsm("LD_A_MEM", ScrollSplitCountVar));
        injected.Add(Expr.MakeAsm("OR_A"));
        injected.Add(Expr.MakeAsm("JR_Z", new AsmOperand(commitDisable, AddressMode.Absolute)));
        injected.Add(Expr.MakeAsm("LD_A_IMM", new AsmOperand(1, AddressMode.Immediate)));
        injected.Add(Expr.MakeAsm("LD_MEM_A", ScrollSplitEnabledVar));
        injected.Add(Expr.MakeAsm("XOR_A"));
        injected.Add(Expr.MakeAsm("LD_MEM_A", ScrollSplitIndexVar));
        injected.Add(Expr.MakeAsm("CALL", new AsmOperand("__kq_scroll_schedule_next", AddressMode.Absolute)));
        injected.Add(Expr.MakeAsm("LD_A_MEM", new AsmOperand(0xFFFF, AddressMode.Absolute))); // IE
        injected.Add(Expr.MakeAsm("OR_IMM", new AsmOperand(0x03, AddressMode.Immediate))); // VBlank | STAT
        injected.Add(Expr.MakeAsm("LD_MEM_A", new AsmOperand(0xFFFF, AddressMode.Absolute)));
        injected.Add(Expr.MakeAsm("EI"));
        injected.Add(Expr.MakeAsm("RET"));
        injected.Add(Expr.Make(Tag.Label, commitDisable));
        injected.Add(Expr.MakeAsm("XOR_A"));
        injected.Add(Expr.MakeAsm("LD_MEM_A", ScrollSplitEnabledVar));
        injected.Add(Expr.MakeAsm("LD_MEM_A", ScrollSplitIndexVar));
        injected.Add(Expr.MakeAsm("CALL", new AsmOperand("__kq_scroll_schedule_next", AddressMode.Absolute)));
        injected.Add(Expr.MakeAsm("RET"));

        // Save registers/SVBK, select WRAM bank 1, restore base scroll state and restart the split sequence for the frame.
        // A first entry at LY zero is applied immediately before scheduling the following entry.
        string vbDisable = "__kq_vblank_disable";
        string vbFirstNonZero = "__kq_vblank_first_nonzero";
        string vbDone = "__kq_vblank_done";
        injected.Add(Expr.Make(Tag.Function, "__kq_vblank_vector"));
        injected.Add(Expr.MakeAsm("PUSH_AF"));
        injected.Add(Expr.MakeAsm("PUSH_BC"));
        injected.Add(Expr.MakeAsm("PUSH_DE"));
        injected.Add(Expr.MakeAsm("PUSH_HL"));
        injected.Add(Expr.MakeAsm("LDH_A_MEM", new AsmOperand(0x70, AddressMode.HighMem))); // SVBK
        injected.Add(Expr.MakeAsm("PUSH_AF"));
        injected.Add(Expr.MakeAsm("LD_A_IMM", new AsmOperand(1, AddressMode.Immediate)));
        injected.Add(Expr.MakeAsm("LDH_MEM_A", new AsmOperand(0x70, AddressMode.HighMem)));
        injected.Add(Expr.MakeAsm("LD_A_IMM", new AsmOperand(1, AddressMode.Immediate)));
        // Set the existing fixed-address frame-ready flag used by the VBlank path.
        injected.Add(Expr.MakeAsm("LD_MEM_A", new AsmOperand(0xC29C, AddressMode.Absolute)));
        injected.Add(Expr.MakeAsm("LD_A_MEM", ScrollBgXCurVar));
        injected.Add(Expr.MakeAsm("LDH_MEM_A", new AsmOperand(0x43, AddressMode.HighMem))); // SCX
        injected.Add(Expr.MakeAsm("LD_A_MEM", ScrollBgYCurVar));
        injected.Add(Expr.MakeAsm("LDH_MEM_A", new AsmOperand(0x42, AddressMode.HighMem))); // SCY
        injected.Add(Expr.MakeAsm("LD_A_MEM", ScrollWinXCurVar));
        injected.Add(Expr.MakeAsm("LDH_MEM_A", new AsmOperand(0x4B, AddressMode.HighMem))); // WX
        injected.Add(Expr.MakeAsm("LD_A_MEM", ScrollWinYCurVar));
        injected.Add(Expr.MakeAsm("LDH_MEM_A", new AsmOperand(0x4A, AddressMode.HighMem))); // WY
        injected.Add(Expr.MakeAsm("LDH_A_MEM", new AsmOperand(0x40, AddressMode.HighMem))); // LCDC
        injected.Add(Expr.MakeAsm("AND_IMM", new AsmOperand(0xDF, AddressMode.Immediate)));
        injected.Add(Expr.MakeAsm("LD_B_A"));
        injected.Add(Expr.MakeAsm("LD_A_MEM", ScrollWinVisibleVar));
        injected.Add(Expr.MakeAsm("OR_B"));
        injected.Add(Expr.MakeAsm("LDH_MEM_A", new AsmOperand(0x40, AddressMode.HighMem)));
        injected.Add(Expr.MakeAsm("LD_A_MEM", ScrollSplitEnabledVar));
        injected.Add(Expr.MakeAsm("OR_A"));
        injected.Add(Expr.MakeAsm("JR_Z", new AsmOperand(vbDisable, AddressMode.Absolute)));
        injected.Add(Expr.MakeAsm("LD_A_MEM", ScrollSplitCountVar));
        injected.Add(Expr.MakeAsm("OR_A"));
        injected.Add(Expr.MakeAsm("JR_Z", new AsmOperand(vbDisable, AddressMode.Absolute)));
        injected.Add(Expr.MakeAsm("XOR_A"));
        injected.Add(Expr.MakeAsm("LD_MEM_A", ScrollSplitIndexVar));
        injected.Add(Expr.MakeAsm("LD_A_MEM", new AsmOperand(ScrollSplitTableAddr, AddressMode.Absolute)));
        injected.Add(Expr.MakeAsm("OR_A"));
        injected.Add(Expr.MakeAsm("JR_NZ", new AsmOperand(vbFirstNonZero, AddressMode.Absolute)));
        injected.Add(Expr.MakeAsm("LD_HL_IMM", new AsmOperand(ScrollSplitTableAddr, AddressMode.Immediate)));
        injected.Add(Expr.MakeAsm("CALL", new AsmOperand("__kq_scroll_apply_entry", AddressMode.Absolute)));
        injected.Add(Expr.MakeAsm("LD_A_IMM", new AsmOperand(1, AddressMode.Immediate)));
        injected.Add(Expr.MakeAsm("LD_MEM_A", ScrollSplitIndexVar));
        injected.Add(Expr.MakeAsm("CALL", new AsmOperand("__kq_scroll_schedule_next", AddressMode.Absolute)));
        injected.Add(Expr.MakeAsm("JR", new AsmOperand(vbDone, AddressMode.Absolute)));
        injected.Add(Expr.Make(Tag.Label, vbFirstNonZero));
        injected.Add(Expr.MakeAsm("XOR_A"));
        injected.Add(Expr.MakeAsm("CALL", new AsmOperand("__kq_scroll_schedule_next", AddressMode.Absolute)));
        injected.Add(Expr.MakeAsm("JR", new AsmOperand(vbDone, AddressMode.Absolute)));
        injected.Add(Expr.Make(Tag.Label, vbDisable));
        injected.Add(Expr.MakeAsm("CALL", new AsmOperand("__kq_scroll_schedule_next", AddressMode.Absolute)));
        injected.Add(Expr.Make(Tag.Label, vbDone));
        injected.Add(Expr.MakeAsm("POP_AF"));
        injected.Add(Expr.MakeAsm("LDH_MEM_A", new AsmOperand(0x70, AddressMode.HighMem))); // restore SVBK
        injected.Add(Expr.MakeAsm("POP_HL"));
        injected.Add(Expr.MakeAsm("POP_DE"));
        injected.Add(Expr.MakeAsm("POP_BC"));
        injected.Add(Expr.MakeAsm("POP_AF"));
        injected.Add(Expr.MakeAsm("RETI"));

        // Save registers/SVBK, apply the current entry and advance its index, or disable further coincidence interrupts at the end.
        string statDisable = "__kq_stat_disable";
        string statDone = "__kq_stat_done";
        injected.Add(Expr.Make(Tag.Function, "__kq_stat_vector"));
        injected.Add(Expr.MakeAsm("PUSH_AF"));
        injected.Add(Expr.MakeAsm("PUSH_BC"));
        injected.Add(Expr.MakeAsm("PUSH_DE"));
        injected.Add(Expr.MakeAsm("PUSH_HL"));
        injected.Add(Expr.MakeAsm("LDH_A_MEM", new AsmOperand(0x70, AddressMode.HighMem))); // SVBK
        injected.Add(Expr.MakeAsm("PUSH_AF"));
        injected.Add(Expr.MakeAsm("LD_A_IMM", new AsmOperand(1, AddressMode.Immediate)));
        injected.Add(Expr.MakeAsm("LDH_MEM_A", new AsmOperand(0x70, AddressMode.HighMem)));
        injected.Add(Expr.MakeAsm("LD_A_MEM", ScrollSplitEnabledVar));
        injected.Add(Expr.MakeAsm("OR_A"));
        injected.Add(Expr.MakeAsm("JR_Z", new AsmOperand(statDisable, AddressMode.Absolute)));
        injected.Add(Expr.MakeAsm("LD_A_MEM", ScrollSplitIndexVar));
        injected.Add(Expr.MakeAsm("LD_B_A"));
        injected.Add(Expr.MakeAsm("LD_A_MEM", ScrollSplitCountVar));
        injected.Add(Expr.MakeAsm("CP_B"));
        injected.Add(Expr.MakeAsm("JR_C", new AsmOperand(statDisable, AddressMode.Absolute)));
        injected.Add(Expr.MakeAsm("JR_Z", new AsmOperand(statDisable, AddressMode.Absolute)));
        injected.Add(Expr.MakeAsm("LD_A_B"));
        injected.Add(Expr.MakeAsm("LD_E_A"));
        injected.Add(Expr.MakeAsm("LD_D_IMM", new AsmOperand(0, AddressMode.Immediate)));
        injected.Add(Expr.MakeAsm("LD_L_A"));
        injected.Add(Expr.MakeAsm("LD_H_IMM", new AsmOperand(0, AddressMode.Immediate)));
        injected.Add(Expr.MakeAsm("ADD_HL_HL"));
        injected.Add(Expr.MakeAsm("ADD_HL_DE"));
        injected.Add(Expr.MakeAsm("ADD_HL_HL"));
        injected.Add(Expr.MakeAsm("LD_DE_IMM", new AsmOperand(ScrollSplitTableAddr, AddressMode.Immediate)));
        injected.Add(Expr.MakeAsm("ADD_HL_DE"));
        // DMG STAT writes can request an interrupt unrelated to LYC. Consume
        // this entry only on its scheduled line; leave the index intact otherwise.
        injected.Add(Expr.MakeAsm("LD_A_HL"));
        injected.Add(Expr.MakeAsm("LD_B_A"));
        injected.Add(Expr.MakeAsm("LDH_A_MEM", new AsmOperand(0x44, AddressMode.HighMem)));
        injected.Add(Expr.MakeAsm("CP_B"));
        injected.Add(Expr.MakeAsm("JR_NZ", new AsmOperand(statDone, AddressMode.Absolute)));
        injected.Add(Expr.MakeAsm("CALL", new AsmOperand("__kq_scroll_apply_entry", AddressMode.Absolute)));
        injected.Add(Expr.MakeAsm("LD_A_MEM", ScrollSplitIndexVar));
        injected.Add(Expr.MakeAsm("INC_A"));
        injected.Add(Expr.MakeAsm("LD_MEM_A", ScrollSplitIndexVar));
        injected.Add(Expr.MakeAsm("CALL", new AsmOperand("__kq_scroll_schedule_next", AddressMode.Absolute)));
        injected.Add(Expr.MakeAsm("JR", new AsmOperand(statDone, AddressMode.Absolute)));
        injected.Add(Expr.Make(Tag.Label, statDisable));
        // Acknowledge an already-disabled source without writing STAT again.
        injected.Add(Expr.MakeAsm("LDH_A_MEM", new AsmOperand(0x41, AddressMode.HighMem)));
        injected.Add(Expr.MakeAsm("AND_IMM", new AsmOperand(0x40, AddressMode.Immediate)));
        injected.Add(Expr.MakeAsm("JR_Z", new AsmOperand(statDone, AddressMode.Absolute)));
        injected.Add(Expr.MakeAsm("LDH_A_MEM", new AsmOperand(0x41, AddressMode.HighMem))); // STAT
        injected.Add(Expr.MakeAsm("AND_IMM", new AsmOperand(0xBF, AddressMode.Immediate)));
        injected.Add(Expr.MakeAsm("LDH_MEM_A", new AsmOperand(0x41, AddressMode.HighMem)));
        injected.Add(Expr.Make(Tag.Label, statDone));
        injected.Add(Expr.MakeAsm("POP_AF"));
        injected.Add(Expr.MakeAsm("LDH_MEM_A", new AsmOperand(0x70, AddressMode.HighMem))); // restore SVBK
        injected.Add(Expr.MakeAsm("POP_HL"));
        injected.Add(Expr.MakeAsm("POP_DE"));
        injected.Add(Expr.MakeAsm("POP_BC"));
        injected.Add(Expr.MakeAsm("POP_AF"));
        injected.Add(Expr.MakeAsm("RETI"));

        int insertAt = 0;
        while (insertAt < rawLines.Count && rawLines[insertAt].MatchTag(Tag.RstMap)) insertAt++;
        rawLines.InsertRange(insertAt, injected);
    }
__scroll_split_push_ex

追加一个分段事件,通过标志位选择背景、窗口、可见性或背景颜色零的操作。

语法

c
void __scroll_split_push_ex(u8 ly, u8 scx, u8 scy, u8 wx, u8 wy, u8 flags);

参数

ly

扫描行事件编号;可见区域使用 0–143,并按升序追加。若第一项的行号为零,则会在 VBlank 期间应用。

scx

背景水平滚动量,范围为 0–255 像素;增大该值会使背景向左移动。

scy

背景垂直滚动量,范围为 0–255 像素;增大该值会使背景向上移动。

wx

原始 WX 字节值,范围为 0–255:在所需屏幕 X 坐标上加 7;窗口左缘位于屏幕左侧时使用 7。

wy

窗口顶端的屏幕 Y 坐标,范围为 0–255 像素;可见行范围为 0–143。

flags

操作标志位:背景=1、窗口位置=2、显示=4、隐藏=8、CGB 背景调色板 0 的颜色 0=16。未知位会被丢弃。颜色零模式将 scx/scy 分别用作 RGB15 的低字节和高字节。

返回值

无返回值,本条说明受影响状态及生效时机。

使用条件与限制

共享事件表最多容纳八项。追加操作既不应用事件,也不排序;超出的项会被忽略。请在安全的帧边界按 reset→push→commit 的顺序重建事件表,并为相邻事件留出足够间隔,使处理程序能及时结束。由于中断延迟,计划触发的扫描行不一定是第一个受影响像素所在的行。显示和隐藏标志同时置位时,以显示为准。颜色零操作仅适用于 CGB;若与背景移动同时使用,两项操作会共用 scx/scy 字节。

使用示例

c
__scroll_split_push_ex(104,16,0,7,0,KQ_SCROLL_SPLIT_USE_BG|KQ_SCROLL_SPLIT_WIN_HIDE);

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

预期结果

将屏幕分为三个滚动带,使背景 X 按 0→8→16 变化。三个 24×16 矩形分别出现在屏幕坐标 (32,16)、(24,80) 和 (16,112)。事件安排在 LY=64 和 104。矩形在 DMG 上为黑色,在 CGB 上为红色。

__scroll_split_push_ex: DMG
DMG
__scroll_split_push_ex: CGB
CGB

完整示例程序

构建命令

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqgb\kitaqgb.exe .\kitaq-docs\samples\api-examples\gb\batch2_split_intrinsics.c -I .\kitaqgb\lib -I .\kitaq-docs\samples -o .\out\batch2_split_intrinsics.gb --no-cache --no-disasm --profile=dev --rst-disable --stack-bank=fixed --cgb=cgb --cart=mbc5 --romsize=64k
实现与源码

kitaqgb/lib/scroll.h:32

kitaqgb/kitaqgb/CodeGenerator.cs:7929

csharp
if (funcName == "__scroll_split_push_ex")
            {
                if (args.Length != 6) Error(expr, "__scroll_split_push_ex(ly, scx, scy, wx, wy, flags) expects 6 arguments");
                EnsureScrollState();
                UseScrollSplitHelpers = true;

                if (TryGetU8Const(args[5], out int splitFlagsConst))
                {
                    int unknownBits = splitFlagsConst & ~ScrollSplitFlagMask;
                    if (unknownBits != 0)
                        Warning(args[5], string.Format("__scroll_split_push_ex() ignores unknown flag bits 0x{0:X2}", unknownBits));
                }

                CompileIntoA(args[0]);
                EmitStoreA(ScrollSplitTmpLyVar);
                CompileIntoA(args[1]);
                EmitStoreA(ScrollSplitTmpScxVar);
                CompileIntoA(args[2]);
                EmitStoreA(ScrollSplitTmpScyVar);
                CompileIntoA(args[3]);
                EmitStoreA(ScrollSplitTmpWxVar);
                CompileIntoA(args[4]);
                EmitStoreA(ScrollSplitTmpWyVar);
                CompileIntoA(args[5]);
                EmitStoreA(ScrollSplitTmpFlagsVar);
                EmitAsm("CALL", new AsmOperand("__kq_scroll_split_push_core", AddressMode.Absolute));
                return;
            }

void AppendScrollHelpersIfUsed(List<Expr> rawLines)
    {
        bool needScrollHelpers = UseScrollSplitHelpers;
        if (!needScrollHelpers)
        {
            foreach (var e in rawLines)
            {
                string m;
                AsmOperand o;
                if (!e.Match(Tag.Asm, out m, out o) || o == null || !o.Base.HasValue) continue;
                string target = o.Base.Value;
                if (target == "__kq_scroll_split_reset_core" ||
                    target == "__kq_scroll_split_push_core" ||
                    target == "__kq_scroll_split_commit_core")
                {
                    needScrollHelpers = true;
                    break;
                }
            }
        }

        if (!needScrollHelpers) return;

        EnsureScrollState();

        var injected = new List<Expr>();
        injected.Add(Expr.Make(Tag.Comment, "[KITAQGB] injected scroll split helpers"));

        // Reset the split count/index and disable STAT coincidence interrupts; existing table bytes are left in place.
        string resetDone = "__kq_scroll_split_reset_done";
        injected.Add(Expr.Make(Tag.Function, "__kq_scroll_split_reset_core"));
        injected.Add(Expr.MakeAsm("XOR_A"));
        injected.Add(Expr.MakeAsm("LD_MEM_A", ScrollSplitCountVar));
        injected.Add(Expr.MakeAsm("LD_MEM_A", ScrollSplitEnabledVar));
        injected.Add(Expr.MakeAsm("LD_MEM_A", ScrollSplitIndexVar));
        injected.Add(Expr.MakeAsm("LDH_A_MEM", new AsmOperand(0x41, AddressMode.HighMem))); // STAT
        injected.Add(Expr.MakeAsm("AND_IMM", new AsmOperand(0xBF, AddressMode.Immediate)));
        injected.Add(Expr.MakeAsm("LDH_MEM_A", new AsmOperand(0x41, AddressMode.HighMem)));
        injected.Add(Expr.Make(Tag.Label, resetDone));
        injected.Add(Expr.MakeAsm("RET"));

        // Append staged LY, SCX, SCY, WX, WY and masked flags; silently ignore entries beyond the eight-slot capacity.
        string pushHaveRoom = "__kq_scroll_split_push_have_room";
        injected.Add(Expr.Make(Tag.Function, "__kq_scroll_split_push_core"));
        injected.Add(Expr.MakeAsm("LD_A_MEM", ScrollSplitCountVar));
        injected.Add(Expr.MakeAsm("CP_IMM", new AsmOperand(ScrollSplitMaxEntries, AddressMode.Immediate)));
        injected.Add(Expr.MakeAsm("JR_C", new AsmOperand(pushHaveRoom, AddressMode.Absolute)));
        injected.Add(Expr.MakeAsm("RET"));
        injected.Add(Expr.Make(Tag.Label, pushHaveRoom));
        injected.Add(Expr.MakeAsm("LD_E_A"));
        injected.Add(Expr.MakeAsm("LD_D_IMM", new AsmOperand(0, AddressMode.Immediate)));
        injected.Add(Expr.MakeAsm("LD_L_A"));
        injected.Add(Expr.MakeAsm("LD_H_IMM", new AsmOperand(0, AddressMode.Immediate)));
        injected.Add(Expr.MakeAsm("ADD_HL_HL"));
        injected.Add(Expr.MakeAsm("ADD_HL_DE"));
        injected.Add(Expr.MakeAsm("ADD_HL_HL"));
        injected.Add(Expr.MakeAsm("LD_DE_IMM", new AsmOperand(ScrollSplitTableAddr, AddressMode.Immediate)));
        injected.Add(Expr.MakeAsm("ADD_HL_DE"));
        injected.Add(Expr.MakeAsm("LD_A_MEM", ScrollSplitTmpLyVar));
        injected.Add(Expr.MakeAsm("LDI_HL_A"));
        injected.Add(Expr.MakeAsm("LD_A_MEM", ScrollSplitTmpScxVar));
        injected.Add(Expr.MakeAsm("LDI_HL_A"));
        injected.Add(Expr.MakeAsm("LD_A_MEM", ScrollSplitTmpScyVar));
        injected.Add(Expr.MakeAsm("LDI_HL_A"));
        injected.Add(Expr.MakeAsm("LD_A_MEM", ScrollSplitTmpWxVar));
        injected.Add(Expr.MakeAsm("LDI_HL_A"));
        injected.Add(Expr.MakeAsm("LD_A_MEM", ScrollSplitTmpWyVar));
        injected.Add(Expr.MakeAsm("LDI_HL_A"));
        injected.Add(Expr.MakeAsm("LD_A_MEM", ScrollSplitTmpFlagsVar));
        injected.Add(Expr.MakeAsm("AND_IMM", new AsmOperand(ScrollSplitFlagMask, AddressMode.Immediate)));
        injected.Add(Expr.MakeAsm("LDI_HL_A"));
        injected.Add(Expr.MakeAsm("LD_A_MEM", ScrollSplitCountVar));
        injected.Add(Expr.MakeAsm("INC_A"));
        injected.Add(Expr.MakeAsm("LD_MEM_A", ScrollSplitCountVar));
        injected.Add(Expr.MakeAsm("RET"));

        // Schedule entry A by programming LYC and STAT bit 6, or disable that bit when A is outside the current count.
        string schedDisable = "__kq_scroll_schedule_next_disable";
        string schedDisabled = "__kq_scroll_schedule_next_disabled";
        string schedHaveTarget = "__kq_scroll_schedule_next_have_target";
        injected.Add(Expr.Make(Tag.Function, "__kq_scroll_schedule_next"));
        injected.Add(Expr.MakeAsm("LD_B_A"));
        injected.Add(Expr.MakeAsm("LD_A_MEM", ScrollSplitCountVar));
        injected.Add(Expr.MakeAsm("CP_B"));
        injected.Add(Expr.MakeAsm("JR_C", new AsmOperand(schedDisable, AddressMode.Absolute)));
        injected.Add(Expr.MakeAsm("JR_Z", new AsmOperand(schedDisable, AddressMode.Absolute)));
        injected.Add(Expr.Make(Tag.Label, schedHaveTarget));
        injected.Add(Expr.MakeAsm("LD_A_B"));
        injected.Add(Expr.MakeAsm("LD_E_A"));
        injected.Add(Expr.MakeAsm("LD_D_IMM", new AsmOperand(0, AddressMode.Immediate)));
        injected.Add(Expr.MakeAsm("LD_L_A"));
        injected.Add(Expr.MakeAsm("LD_H_IMM", new AsmOperand(0, AddressMode.Immediate)));
        injected.Add(Expr.MakeAsm("ADD_HL_HL"));
        injected.Add(Expr.MakeAsm("ADD_HL_DE"));
        injected.Add(Expr.MakeAsm("ADD_HL_HL"));
        injected.Add(Expr.MakeAsm("LD_DE_IMM", new AsmOperand(ScrollSplitTableAddr, AddressMode.Immediate)));
        injected.Add(Expr.MakeAsm("ADD_HL_DE"));
        injected.Add(Expr.MakeAsm("LD_A_HL"));
        injected.Add(Expr.MakeAsm("LDH_MEM_A", new AsmOperand(0x45, AddressMode.HighMem))); // LYC
        injected.Add(Expr.MakeAsm("LDH_A_MEM", new AsmOperand(0x41, AddressMode.HighMem))); // STAT
        injected.Add(Expr.MakeAsm("OR_IMM", new AsmOperand(0x40, AddressMode.Immediate)));
        injected.Add(Expr.MakeAsm("LDH_MEM_A", new AsmOperand(0x41, AddressMode.HighMem)));
        injected.Add(Expr.MakeAsm("RET"));
        injected.Add(Expr.Make(Tag.Label, schedDisable));
        // Do not rewrite a disabled STAT source: on DMG even writing zero
        // can raise a fresh request and repeatedly re-enter the STAT handler.
        injected.Add(Expr.MakeAsm("LDH_A_MEM", new AsmOperand(0x41, AddressMode.HighMem)));
        injected.Add(Expr.MakeAsm("AND_IMM", new AsmOperand(0x40, AddressMode.Immediate)));
        injected.Add(Expr.MakeAsm("JR_Z", new AsmOperand(schedDisabled, AddressMode.Absolute)));
        injected.Add(Expr.MakeAsm("LDH_A_MEM", new AsmOperand(0x41, AddressMode.HighMem))); // STAT
        injected.Add(Expr.MakeAsm("AND_IMM", new AsmOperand(0xBF, AddressMode.Immediate)));
        injected.Add(Expr.MakeAsm("LDH_MEM_A", new AsmOperand(0x41, AddressMode.HighMem)));
        injected.Add(Expr.Make(Tag.Label, schedDisabled));
        injected.Add(Expr.MakeAsm("RET"));

        // Apply the fields selected by an entry's flags. Show follows hide when both are set; palette color zero reuses SCX/SCY bytes.
        string applySkipBg = "__kq_scroll_apply_entry_skip_bg";
        string applySkipWin = "__kq_scroll_apply_entry_skip_win";
        string applySkipHide = "__kq_scroll_apply_entry_skip_hide";
        string applySkipBgColor0 = "__kq_scroll_apply_entry_skip_bg_color0";
        string applyWaitPalette = "__kq_scroll_apply_entry_wait_palette";
        string applyDone = "__kq_scroll_apply_entry_done";
        injected.Add(Expr.Make(Tag.Function, "__kq_scroll_apply_entry"));
        injected.Add(Expr.MakeAsm("INC_HL")); // scx
        injected.Add(Expr.MakeAsm("LD_A_HL"));
        injected.Add(Expr.MakeAsm("LD_B_A"));
        injected.Add(Expr.MakeAsm("INC_HL")); // scy
        injected.Add(Expr.MakeAsm("LD_A_HL"));
        injected.Add(Expr.MakeAsm("LD_C_A"));
        injected.Add(Expr.MakeAsm("INC_HL")); // wx
        injected.Add(Expr.MakeAsm("LD_A_HL"));
        injected.Add(Expr.MakeAsm("LD_D_A"));
        injected.Add(Expr.MakeAsm("INC_HL")); // wy
        injected.Add(Expr.MakeAsm("LD_A_HL"));
        injected.Add(Expr.MakeAsm("LD_E_A"));
        injected.Add(Expr.MakeAsm("INC_HL")); // flags
        injected.Add(Expr.MakeAsm("LD_A_HL"));
        injected.Add(Expr.MakeAsm("AND_IMM", new AsmOperand(ScrollSplitFlagMask, AddressMode.Immediate)));
        injected.Add(Expr.MakeAsm("LD_H_A"));
        injected.Add(Expr.MakeAsm("LD_A_H"));
        injected.Add(Expr.MakeAsm("AND_IMM", new AsmOperand(ScrollSplitFlagUseBg, AddressMode.Immediate)));
        injected.Add(Expr.MakeAsm("JR_Z", new AsmOperand(applySkipBg, AddressMode.Absolute)));
        injected.Add(Expr.MakeAsm("LD_A_B"));
        injected.Add(Expr.MakeAsm("LDH_MEM_A", new AsmOperand(0x43, AddressMode.HighMem))); // SCX
        injected.Add(Expr.MakeAsm("LD_A_C"));
        injected.Add(Expr.MakeAsm("LDH_MEM_A", new AsmOperand(0x42, AddressMode.HighMem))); // SCY
        injected.Add(Expr.Make(Tag.Label, applySkipBg));
        injected.Add(Expr.MakeAsm("LD_A_H"));
        injected.Add(Expr.MakeAsm("AND_IMM", new AsmOperand(ScrollSplitFlagUseWin, AddressMode.Immediate)));
        injected.Add(Expr.MakeAsm("JR_Z", new AsmOperand(applySkipWin, AddressMode.Absolute)));
        injected.Add(Expr.MakeAsm("LD_A_D"));
        injected.Add(Expr.MakeAsm("LDH_MEM_A", new AsmOperand(0x4B, AddressMode.HighMem))); // WX
        injected.Add(Expr.MakeAsm("LD_A_E"));
        injected.Add(Expr.MakeAsm("LDH_MEM_A", new AsmOperand(0x4A, AddressMode.HighMem))); // WY
        injected.Add(Expr.Make(Tag.Label, applySkipWin));
        injected.Add(Expr.MakeAsm("LD_A_H"));
        injected.Add(Expr.MakeAsm("AND_IMM", new AsmOperand(ScrollSplitFlagWinHide, AddressMode.Immediate)));
        injected.Add(Expr.MakeAsm("JR_Z", new AsmOperand(applySkipHide, AddressMode.Absolute)));
        injected.Add(Expr.MakeAsm("LDH_A_MEM", new AsmOperand(0x40, AddressMode.HighMem))); // LCDC
        injected.Add(Expr.MakeAsm("AND_IMM", new AsmOperand(0xDF, AddressMode.Immediate)));
        injected.Add(Expr.MakeAsm("LDH_MEM_A", new AsmOperand(0x40, AddressMode.HighMem)));
        injected.Add(Expr.Make(Tag.Label, applySkipHide));
        injected.Add(Expr.MakeAsm("LD_A_H"));
        injected.Add(Expr.MakeAsm("AND_IMM", new AsmOperand(ScrollSplitFlagWinShow, AddressMode.Immediate)));
        injected.Add(Expr.MakeAsm("JR_Z", new AsmOperand(applySkipBgColor0, AddressMode.Absolute)));
        injected.Add(Expr.MakeAsm("LDH_A_MEM", new AsmOperand(0x40, AddressMode.HighMem))); // LCDC
        injected.Add(Expr.MakeAsm("OR_IMM", new AsmOperand(0x20, AddressMode.Immediate)));
        injected.Add(Expr.MakeAsm("LDH_MEM_A", new AsmOperand(0x40, AddressMode.HighMem)));
        injected.Add(Expr.Make(Tag.Label, applySkipBgColor0));
        injected.Add(Expr.MakeAsm("LD_A_H"));
        injected.Add(Expr.MakeAsm("AND_IMM", new AsmOperand(ScrollSplitFlagBgColor0, AddressMode.Immediate)));
        injected.Add(Expr.MakeAsm("JR_Z", new AsmOperand(applyDone, AddressMode.Absolute)));
        // CGB palette data writes are blocked during pixel transfer. Wait through
        // OAM search as well, so both color bytes fit in the following HBlank.
        // The LY-zero entry runs in VBlank and does not wait here.
        injected.Add(Expr.Make(Tag.Label, applyWaitPalette));
        injected.Add(Expr.MakeAsm("LDH_A_MEM", new AsmOperand(0x41, AddressMode.HighMem)));
        injected.Add(Expr.MakeAsm("AND_IMM", new AsmOperand(0x02, AddressMode.Immediate)));
        injected.Add(Expr.MakeAsm("JR_NZ", new AsmOperand(applyWaitPalette, AddressMode.Absolute)));
        injected.Add(Expr.MakeAsm("LD_A_IMM", new AsmOperand(0x80, AddressMode.Immediate)));
        injected.Add(Expr.MakeAsm("LDH_MEM_A", new AsmOperand(0x68, AddressMode.HighMem))); // BCPS: palette 0, color 0, auto increment
        injected.Add(Expr.MakeAsm("LD_A_B"));
        injected.Add(Expr.MakeAsm("LDH_MEM_A", new AsmOperand(0x69, AddressMode.HighMem))); // BCPD low
        injected.Add(Expr.MakeAsm("LD_A_C"));
        injected.Add(Expr.MakeAsm("LDH_MEM_A", new AsmOperand(0x69, AddressMode.HighMem))); // BCPD high
        injected.Add(Expr.Make(Tag.Label, applyDone));
        injected.Add(Expr.MakeAsm("RET"));

        // Capture base window visibility and arm a nonempty table at index zero, enabling VBlank/STAT interrupts and issuing EI.
        string commitDisable = "__kq_scroll_split_commit_disable";
        injected.Add(Expr.Make(Tag.Function, "__kq_scroll_split_commit_core"));
        injected.Add(Expr.MakeAsm("LDH_A_MEM", new AsmOperand(0x40, AddressMode.HighMem))); // LCDC
        injected.Add(Expr.MakeAsm("AND_IMM", new AsmOperand(0x20, AddressMode.Immediate)));
        injected.Add(Expr.MakeAsm("LD_MEM_A", ScrollWinVisibleVar));
        injected.Add(Expr.MakeAsm("LD_A_MEM", ScrollSplitCountVar));
        injected.Add(Expr.MakeAsm("OR_A"));
        injected.Add(Expr.MakeAsm("JR_Z", new AsmOperand(commitDisable, AddressMode.Absolute)));
        injected.Add(Expr.MakeAsm("LD_A_IMM", new AsmOperand(1, AddressMode.Immediate)));
        injected.Add(Expr.MakeAsm("LD_MEM_A", ScrollSplitEnabledVar));
        injected.Add(Expr.MakeAsm("XOR_A"));
        injected.Add(Expr.MakeAsm("LD_MEM_A", ScrollSplitIndexVar));
        injected.Add(Expr.MakeAsm("CALL", new AsmOperand("__kq_scroll_schedule_next", AddressMode.Absolute)));
        injected.Add(Expr.MakeAsm("LD_A_MEM", new AsmOperand(0xFFFF, AddressMode.Absolute))); // IE
        injected.Add(Expr.MakeAsm("OR_IMM", new AsmOperand(0x03, AddressMode.Immediate))); // VBlank | STAT
        injected.Add(Expr.MakeAsm("LD_MEM_A", new AsmOperand(0xFFFF, AddressMode.Absolute)));
        injected.Add(Expr.MakeAsm("EI"));
        injected.Add(Expr.MakeAsm("RET"));
        injected.Add(Expr.Make(Tag.Label, commitDisable));
        injected.Add(Expr.MakeAsm("XOR_A"));
        injected.Add(Expr.MakeAsm("LD_MEM_A", ScrollSplitEnabledVar));
        injected.Add(Expr.MakeAsm("LD_MEM_A", ScrollSplitIndexVar));
        injected.Add(Expr.MakeAsm("CALL", new AsmOperand("__kq_scroll_schedule_next", AddressMode.Absolute)));
        injected.Add(Expr.MakeAsm("RET"));

        // Save registers/SVBK, select WRAM bank 1, restore base scroll state and restart the split sequence for the frame.
        // A first entry at LY zero is applied immediately before scheduling the following entry.
        string vbDisable = "__kq_vblank_disable";
        string vbFirstNonZero = "__kq_vblank_first_nonzero";
        string vbDone = "__kq_vblank_done";
        injected.Add(Expr.Make(Tag.Function, "__kq_vblank_vector"));
        injected.Add(Expr.MakeAsm("PUSH_AF"));
        injected.Add(Expr.MakeAsm("PUSH_BC"));
        injected.Add(Expr.MakeAsm("PUSH_DE"));
        injected.Add(Expr.MakeAsm("PUSH_HL"));
        injected.Add(Expr.MakeAsm("LDH_A_MEM", new AsmOperand(0x70, AddressMode.HighMem))); // SVBK
        injected.Add(Expr.MakeAsm("PUSH_AF"));
        injected.Add(Expr.MakeAsm("LD_A_IMM", new AsmOperand(1, AddressMode.Immediate)));
        injected.Add(Expr.MakeAsm("LDH_MEM_A", new AsmOperand(0x70, AddressMode.HighMem)));
        injected.Add(Expr.MakeAsm("LD_A_IMM", new AsmOperand(1, AddressMode.Immediate)));
        // Set the existing fixed-address frame-ready flag used by the VBlank path.
        injected.Add(Expr.MakeAsm("LD_MEM_A", new AsmOperand(0xC29C, AddressMode.Absolute)));
        injected.Add(Expr.MakeAsm("LD_A_MEM", ScrollBgXCurVar));
        injected.Add(Expr.MakeAsm("LDH_MEM_A", new AsmOperand(0x43, AddressMode.HighMem))); // SCX
        injected.Add(Expr.MakeAsm("LD_A_MEM", ScrollBgYCurVar));
        injected.Add(Expr.MakeAsm("LDH_MEM_A", new AsmOperand(0x42, AddressMode.HighMem))); // SCY
        injected.Add(Expr.MakeAsm("LD_A_MEM", ScrollWinXCurVar));
        injected.Add(Expr.MakeAsm("LDH_MEM_A", new AsmOperand(0x4B, AddressMode.HighMem))); // WX
        injected.Add(Expr.MakeAsm("LD_A_MEM", ScrollWinYCurVar));
        injected.Add(Expr.MakeAsm("LDH_MEM_A", new AsmOperand(0x4A, AddressMode.HighMem))); // WY
        injected.Add(Expr.MakeAsm("LDH_A_MEM", new AsmOperand(0x40, AddressMode.HighMem))); // LCDC
        injected.Add(Expr.MakeAsm("AND_IMM", new AsmOperand(0xDF, AddressMode.Immediate)));
        injected.Add(Expr.MakeAsm("LD_B_A"));
        injected.Add(Expr.MakeAsm("LD_A_MEM", ScrollWinVisibleVar));
        injected.Add(Expr.MakeAsm("OR_B"));
        injected.Add(Expr.MakeAsm("LDH_MEM_A", new AsmOperand(0x40, AddressMode.HighMem)));
        injected.Add(Expr.MakeAsm("LD_A_MEM", ScrollSplitEnabledVar));
        injected.Add(Expr.MakeAsm("OR_A"));
        injected.Add(Expr.MakeAsm("JR_Z", new AsmOperand(vbDisable, AddressMode.Absolute)));
        injected.Add(Expr.MakeAsm("LD_A_MEM", ScrollSplitCountVar));
        injected.Add(Expr.MakeAsm("OR_A"));
        injected.Add(Expr.MakeAsm("JR_Z", new AsmOperand(vbDisable, AddressMode.Absolute)));
        injected.Add(Expr.MakeAsm("XOR_A"));
        injected.Add(Expr.MakeAsm("LD_MEM_A", ScrollSplitIndexVar));
        injected.Add(Expr.MakeAsm("LD_A_MEM", new AsmOperand(ScrollSplitTableAddr, AddressMode.Absolute)));
        injected.Add(Expr.MakeAsm("OR_A"));
        injected.Add(Expr.MakeAsm("JR_NZ", new AsmOperand(vbFirstNonZero, AddressMode.Absolute)));
        injected.Add(Expr.MakeAsm("LD_HL_IMM", new AsmOperand(ScrollSplitTableAddr, AddressMode.Immediate)));
        injected.Add(Expr.MakeAsm("CALL", new AsmOperand("__kq_scroll_apply_entry", AddressMode.Absolute)));
        injected.Add(Expr.MakeAsm("LD_A_IMM", new AsmOperand(1, AddressMode.Immediate)));
        injected.Add(Expr.MakeAsm("LD_MEM_A", ScrollSplitIndexVar));
        injected.Add(Expr.MakeAsm("CALL", new AsmOperand("__kq_scroll_schedule_next", AddressMode.Absolute)));
        injected.Add(Expr.MakeAsm("JR", new AsmOperand(vbDone, AddressMode.Absolute)));
        injected.Add(Expr.Make(Tag.Label, vbFirstNonZero));
        injected.Add(Expr.MakeAsm("XOR_A"));
        injected.Add(Expr.MakeAsm("CALL", new AsmOperand("__kq_scroll_schedule_next", AddressMode.Absolute)));
        injected.Add(Expr.MakeAsm("JR", new AsmOperand(vbDone, AddressMode.Absolute)));
        injected.Add(Expr.Make(Tag.Label, vbDisable));
        injected.Add(Expr.MakeAsm("CALL", new AsmOperand("__kq_scroll_schedule_next", AddressMode.Absolute)));
        injected.Add(Expr.Make(Tag.Label, vbDone));
        injected.Add(Expr.MakeAsm("POP_AF"));
        injected.Add(Expr.MakeAsm("LDH_MEM_A", new AsmOperand(0x70, AddressMode.HighMem))); // restore SVBK
        injected.Add(Expr.MakeAsm("POP_HL"));
        injected.Add(Expr.MakeAsm("POP_DE"));
        injected.Add(Expr.MakeAsm("POP_BC"));
        injected.Add(Expr.MakeAsm("POP_AF"));
        injected.Add(Expr.MakeAsm("RETI"));

        // Save registers/SVBK, apply the current entry and advance its index, or disable further coincidence interrupts at the end.
        string statDisable = "__kq_stat_disable";
        string statDone = "__kq_stat_done";
        injected.Add(Expr.Make(Tag.Function, "__kq_stat_vector"));
        injected.Add(Expr.MakeAsm("PUSH_AF"));
        injected.Add(Expr.MakeAsm("PUSH_BC"));
        injected.Add(Expr.MakeAsm("PUSH_DE"));
        injected.Add(Expr.MakeAsm("PUSH_HL"));
        injected.Add(Expr.MakeAsm("LDH_A_MEM", new AsmOperand(0x70, AddressMode.HighMem))); // SVBK
        injected.Add(Expr.MakeAsm("PUSH_AF"));
        injected.Add(Expr.MakeAsm("LD_A_IMM", new AsmOperand(1, AddressMode.Immediate)));
        injected.Add(Expr.MakeAsm("LDH_MEM_A", new AsmOperand(0x70, AddressMode.HighMem)));
        injected.Add(Expr.MakeAsm("LD_A_MEM", ScrollSplitEnabledVar));
        injected.Add(Expr.MakeAsm("OR_A"));
        injected.Add(Expr.MakeAsm("JR_Z", new AsmOperand(statDisable, AddressMode.Absolute)));
        injected.Add(Expr.MakeAsm("LD_A_MEM", ScrollSplitIndexVar));
        injected.Add(Expr.MakeAsm("LD_B_A"));
        injected.Add(Expr.MakeAsm("LD_A_MEM", ScrollSplitCountVar));
        injected.Add(Expr.MakeAsm("CP_B"));
        injected.Add(Expr.MakeAsm("JR_C", new AsmOperand(statDisable, AddressMode.Absolute)));
        injected.Add(Expr.MakeAsm("JR_Z", new AsmOperand(statDisable, AddressMode.Absolute)));
        injected.Add(Expr.MakeAsm("LD_A_B"));
        injected.Add(Expr.MakeAsm("LD_E_A"));
        injected.Add(Expr.MakeAsm("LD_D_IMM", new AsmOperand(0, AddressMode.Immediate)));
        injected.Add(Expr.MakeAsm("LD_L_A"));
        injected.Add(Expr.MakeAsm("LD_H_IMM", new AsmOperand(0, AddressMode.Immediate)));
        injected.Add(Expr.MakeAsm("ADD_HL_HL"));
        injected.Add(Expr.MakeAsm("ADD_HL_DE"));
        injected.Add(Expr.MakeAsm("ADD_HL_HL"));
        injected.Add(Expr.MakeAsm("LD_DE_IMM", new AsmOperand(ScrollSplitTableAddr, AddressMode.Immediate)));
        injected.Add(Expr.MakeAsm("ADD_HL_DE"));
        // DMG STAT writes can request an interrupt unrelated to LYC. Consume
        // this entry only on its scheduled line; leave the index intact otherwise.
        injected.Add(Expr.MakeAsm("LD_A_HL"));
        injected.Add(Expr.MakeAsm("LD_B_A"));
        injected.Add(Expr.MakeAsm("LDH_A_MEM", new AsmOperand(0x44, AddressMode.HighMem)));
        injected.Add(Expr.MakeAsm("CP_B"));
        injected.Add(Expr.MakeAsm("JR_NZ", new AsmOperand(statDone, AddressMode.Absolute)));
        injected.Add(Expr.MakeAsm("CALL", new AsmOperand("__kq_scroll_apply_entry", AddressMode.Absolute)));
        injected.Add(Expr.MakeAsm("LD_A_MEM", ScrollSplitIndexVar));
        injected.Add(Expr.MakeAsm("INC_A"));
        injected.Add(Expr.MakeAsm("LD_MEM_A", ScrollSplitIndexVar));
        injected.Add(Expr.MakeAsm("CALL", new AsmOperand("__kq_scroll_schedule_next", AddressMode.Absolute)));
        injected.Add(Expr.MakeAsm("JR", new AsmOperand(statDone, AddressMode.Absolute)));
        injected.Add(Expr.Make(Tag.Label, statDisable));
        // Acknowledge an already-disabled source without writing STAT again.
        injected.Add(Expr.MakeAsm("LDH_A_MEM", new AsmOperand(0x41, AddressMode.HighMem)));
        injected.Add(Expr.MakeAsm("AND_IMM", new AsmOperand(0x40, AddressMode.Immediate)));
        injected.Add(Expr.MakeAsm("JR_Z", new AsmOperand(statDone, AddressMode.Absolute)));
        injected.Add(Expr.MakeAsm("LDH_A_MEM", new AsmOperand(0x41, AddressMode.HighMem))); // STAT
        injected.Add(Expr.MakeAsm("AND_IMM", new AsmOperand(0xBF, AddressMode.Immediate)));
        injected.Add(Expr.MakeAsm("LDH_MEM_A", new AsmOperand(0x41, AddressMode.HighMem)));
        injected.Add(Expr.Make(Tag.Label, statDone));
        injected.Add(Expr.MakeAsm("POP_AF"));
        injected.Add(Expr.MakeAsm("LDH_MEM_A", new AsmOperand(0x70, AddressMode.HighMem))); // restore SVBK
        injected.Add(Expr.MakeAsm("POP_HL"));
        injected.Add(Expr.MakeAsm("POP_DE"));
        injected.Add(Expr.MakeAsm("POP_BC"));
        injected.Add(Expr.MakeAsm("POP_AF"));
        injected.Add(Expr.MakeAsm("RETI"));

        int insertAt = 0;
        while (insertAt < rawLines.Count && rawLines[insertAt].MatchTag(Tag.RstMap)) insertAt++;
        rawLines.InsertRange(insertAt, injected);
    }
__scroll_split_reset

清零分段事件数量及索引,关闭分段处理,并清除 STAT 中的 LYC 中断使能位。

语法

c
void __scroll_split_reset();

返回值

无返回值,本条说明受影响状态及生效时机。

使用条件与限制

共享事件表最多容纳八项。追加操作既不应用事件,也不排序;超出的项会被忽略。请在安全的帧边界按 reset→push→commit 的顺序重建事件表,并为相邻事件留出足够间隔,使处理程序能及时结束。由于中断延迟,计划触发的扫描行不一定是第一个受影响像素所在的行。本操作不会擦除表中的字节、清除 IE 或关闭 CPU 中断。

使用示例

c
__scroll_split_reset();

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

预期结果

将屏幕分为三个滚动带,使背景 X 按 0→8→16 变化。三个 24×16 矩形分别出现在屏幕坐标 (32,16)、(24,80) 和 (16,112)。事件安排在 LY=64 和 104。矩形在 DMG 上为黑色,在 CGB 上为红色。

__scroll_split_reset: DMG
DMG
__scroll_split_reset: CGB
CGB

完整示例程序

构建命令

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqgb\kitaqgb.exe .\kitaq-docs\samples\api-examples\gb\batch2_split_intrinsics.c -I .\kitaqgb\lib -I .\kitaq-docs\samples -o .\out\batch2_split_intrinsics.gb --no-cache --no-disasm --profile=dev --rst-disable --stack-bank=fixed --cgb=cgb --cart=mbc5 --romsize=64k
实现与源码

kitaqgb/lib/scroll.h:30

kitaqgb/kitaqgb/CodeGenerator.cs:7897

csharp
if (funcName == "__scroll_split_reset")
            {
                if (args.Length != 0) Error(expr, "__scroll_split_reset() expects 0 arguments");
                EnsureScrollState();
                UseScrollSplitHelpers = true;
                EmitAsm("CALL", new AsmOperand("__kq_scroll_split_reset_core", AddressMode.Absolute));
                return;
            }

void AppendScrollHelpersIfUsed(List<Expr> rawLines)
    {
        bool needScrollHelpers = UseScrollSplitHelpers;
        if (!needScrollHelpers)
        {
            foreach (var e in rawLines)
            {
                string m;
                AsmOperand o;
                if (!e.Match(Tag.Asm, out m, out o) || o == null || !o.Base.HasValue) continue;
                string target = o.Base.Value;
                if (target == "__kq_scroll_split_reset_core" ||
                    target == "__kq_scroll_split_push_core" ||
                    target == "__kq_scroll_split_commit_core")
                {
                    needScrollHelpers = true;
                    break;
                }
            }
        }

        if (!needScrollHelpers) return;

        EnsureScrollState();

        var injected = new List<Expr>();
        injected.Add(Expr.Make(Tag.Comment, "[KITAQGB] injected scroll split helpers"));

        // Reset the split count/index and disable STAT coincidence interrupts; existing table bytes are left in place.
        string resetDone = "__kq_scroll_split_reset_done";
        injected.Add(Expr.Make(Tag.Function, "__kq_scroll_split_reset_core"));
        injected.Add(Expr.MakeAsm("XOR_A"));
        injected.Add(Expr.MakeAsm("LD_MEM_A", ScrollSplitCountVar));
        injected.Add(Expr.MakeAsm("LD_MEM_A", ScrollSplitEnabledVar));
        injected.Add(Expr.MakeAsm("LD_MEM_A", ScrollSplitIndexVar));
        injected.Add(Expr.MakeAsm("LDH_A_MEM", new AsmOperand(0x41, AddressMode.HighMem))); // STAT
        injected.Add(Expr.MakeAsm("AND_IMM", new AsmOperand(0xBF, AddressMode.Immediate)));
        injected.Add(Expr.MakeAsm("LDH_MEM_A", new AsmOperand(0x41, AddressMode.HighMem)));
        injected.Add(Expr.Make(Tag.Label, resetDone));
        injected.Add(Expr.MakeAsm("RET"));

        // Append staged LY, SCX, SCY, WX, WY and masked flags; silently ignore entries beyond the eight-slot capacity.
        string pushHaveRoom = "__kq_scroll_split_push_have_room";
        injected.Add(Expr.Make(Tag.Function, "__kq_scroll_split_push_core"));
        injected.Add(Expr.MakeAsm("LD_A_MEM", ScrollSplitCountVar));
        injected.Add(Expr.MakeAsm("CP_IMM", new AsmOperand(ScrollSplitMaxEntries, AddressMode.Immediate)));
        injected.Add(Expr.MakeAsm("JR_C", new AsmOperand(pushHaveRoom, AddressMode.Absolute)));
        injected.Add(Expr.MakeAsm("RET"));
        injected.Add(Expr.Make(Tag.Label, pushHaveRoom));
        injected.Add(Expr.MakeAsm("LD_E_A"));
        injected.Add(Expr.MakeAsm("LD_D_IMM", new AsmOperand(0, AddressMode.Immediate)));
        injected.Add(Expr.MakeAsm("LD_L_A"));
        injected.Add(Expr.MakeAsm("LD_H_IMM", new AsmOperand(0, AddressMode.Immediate)));
        injected.Add(Expr.MakeAsm("ADD_HL_HL"));
        injected.Add(Expr.MakeAsm("ADD_HL_DE"));
        injected.Add(Expr.MakeAsm("ADD_HL_HL"));
        injected.Add(Expr.MakeAsm("LD_DE_IMM", new AsmOperand(ScrollSplitTableAddr, AddressMode.Immediate)));
        injected.Add(Expr.MakeAsm("ADD_HL_DE"));
        injected.Add(Expr.MakeAsm("LD_A_MEM", ScrollSplitTmpLyVar));
        injected.Add(Expr.MakeAsm("LDI_HL_A"));
        injected.Add(Expr.MakeAsm("LD_A_MEM", ScrollSplitTmpScxVar));
        injected.Add(Expr.MakeAsm("LDI_HL_A"));
        injected.Add(Expr.MakeAsm("LD_A_MEM", ScrollSplitTmpScyVar));
        injected.Add(Expr.MakeAsm("LDI_HL_A"));
        injected.Add(Expr.MakeAsm("LD_A_MEM", ScrollSplitTmpWxVar));
        injected.Add(Expr.MakeAsm("LDI_HL_A"));
        injected.Add(Expr.MakeAsm("LD_A_MEM", ScrollSplitTmpWyVar));
        injected.Add(Expr.MakeAsm("LDI_HL_A"));
        injected.Add(Expr.MakeAsm("LD_A_MEM", ScrollSplitTmpFlagsVar));
        injected.Add(Expr.MakeAsm("AND_IMM", new AsmOperand(ScrollSplitFlagMask, AddressMode.Immediate)));
        injected.Add(Expr.MakeAsm("LDI_HL_A"));
        injected.Add(Expr.MakeAsm("LD_A_MEM", ScrollSplitCountVar));
        injected.Add(Expr.MakeAsm("INC_A"));
        injected.Add(Expr.MakeAsm("LD_MEM_A", ScrollSplitCountVar));
        injected.Add(Expr.MakeAsm("RET"));

        // Schedule entry A by programming LYC and STAT bit 6, or disable that bit when A is outside the current count.
        string schedDisable = "__kq_scroll_schedule_next_disable";
        string schedDisabled = "__kq_scroll_schedule_next_disabled";
        string schedHaveTarget = "__kq_scroll_schedule_next_have_target";
        injected.Add(Expr.Make(Tag.Function, "__kq_scroll_schedule_next"));
        injected.Add(Expr.MakeAsm("LD_B_A"));
        injected.Add(Expr.MakeAsm("LD_A_MEM", ScrollSplitCountVar));
        injected.Add(Expr.MakeAsm("CP_B"));
        injected.Add(Expr.MakeAsm("JR_C", new AsmOperand(schedDisable, AddressMode.Absolute)));
        injected.Add(Expr.MakeAsm("JR_Z", new AsmOperand(schedDisable, AddressMode.Absolute)));
        injected.Add(Expr.Make(Tag.Label, schedHaveTarget));
        injected.Add(Expr.MakeAsm("LD_A_B"));
        injected.Add(Expr.MakeAsm("LD_E_A"));
        injected.Add(Expr.MakeAsm("LD_D_IMM", new AsmOperand(0, AddressMode.Immediate)));
        injected.Add(Expr.MakeAsm("LD_L_A"));
        injected.Add(Expr.MakeAsm("LD_H_IMM", new AsmOperand(0, AddressMode.Immediate)));
        injected.Add(Expr.MakeAsm("ADD_HL_HL"));
        injected.Add(Expr.MakeAsm("ADD_HL_DE"));
        injected.Add(Expr.MakeAsm("ADD_HL_HL"));
        injected.Add(Expr.MakeAsm("LD_DE_IMM", new AsmOperand(ScrollSplitTableAddr, AddressMode.Immediate)));
        injected.Add(Expr.MakeAsm("ADD_HL_DE"));
        injected.Add(Expr.MakeAsm("LD_A_HL"));
        injected.Add(Expr.MakeAsm("LDH_MEM_A", new AsmOperand(0x45, AddressMode.HighMem))); // LYC
        injected.Add(Expr.MakeAsm("LDH_A_MEM", new AsmOperand(0x41, AddressMode.HighMem))); // STAT
        injected.Add(Expr.MakeAsm("OR_IMM", new AsmOperand(0x40, AddressMode.Immediate)));
        injected.Add(Expr.MakeAsm("LDH_MEM_A", new AsmOperand(0x41, AddressMode.HighMem)));
        injected.Add(Expr.MakeAsm("RET"));
        injected.Add(Expr.Make(Tag.Label, schedDisable));
        // Do not rewrite a disabled STAT source: on DMG even writing zero
        // can raise a fresh request and repeatedly re-enter the STAT handler.
        injected.Add(Expr.MakeAsm("LDH_A_MEM", new AsmOperand(0x41, AddressMode.HighMem)));
        injected.Add(Expr.MakeAsm("AND_IMM", new AsmOperand(0x40, AddressMode.Immediate)));
        injected.Add(Expr.MakeAsm("JR_Z", new AsmOperand(schedDisabled, AddressMode.Absolute)));
        injected.Add(Expr.MakeAsm("LDH_A_MEM", new AsmOperand(0x41, AddressMode.HighMem))); // STAT
        injected.Add(Expr.MakeAsm("AND_IMM", new AsmOperand(0xBF, AddressMode.Immediate)));
        injected.Add(Expr.MakeAsm("LDH_MEM_A", new AsmOperand(0x41, AddressMode.HighMem)));
        injected.Add(Expr.Make(Tag.Label, schedDisabled));
        injected.Add(Expr.MakeAsm("RET"));

        // Apply the fields selected by an entry's flags. Show follows hide when both are set; palette color zero reuses SCX/SCY bytes.
        string applySkipBg = "__kq_scroll_apply_entry_skip_bg";
        string applySkipWin = "__kq_scroll_apply_entry_skip_win";
        string applySkipHide = "__kq_scroll_apply_entry_skip_hide";
        string applySkipBgColor0 = "__kq_scroll_apply_entry_skip_bg_color0";
        string applyWaitPalette = "__kq_scroll_apply_entry_wait_palette";
        string applyDone = "__kq_scroll_apply_entry_done";
        injected.Add(Expr.Make(Tag.Function, "__kq_scroll_apply_entry"));
        injected.Add(Expr.MakeAsm("INC_HL")); // scx
        injected.Add(Expr.MakeAsm("LD_A_HL"));
        injected.Add(Expr.MakeAsm("LD_B_A"));
        injected.Add(Expr.MakeAsm("INC_HL")); // scy
        injected.Add(Expr.MakeAsm("LD_A_HL"));
        injected.Add(Expr.MakeAsm("LD_C_A"));
        injected.Add(Expr.MakeAsm("INC_HL")); // wx
        injected.Add(Expr.MakeAsm("LD_A_HL"));
        injected.Add(Expr.MakeAsm("LD_D_A"));
        injected.Add(Expr.MakeAsm("INC_HL")); // wy
        injected.Add(Expr.MakeAsm("LD_A_HL"));
        injected.Add(Expr.MakeAsm("LD_E_A"));
        injected.Add(Expr.MakeAsm("INC_HL")); // flags
        injected.Add(Expr.MakeAsm("LD_A_HL"));
        injected.Add(Expr.MakeAsm("AND_IMM", new AsmOperand(ScrollSplitFlagMask, AddressMode.Immediate)));
        injected.Add(Expr.MakeAsm("LD_H_A"));
        injected.Add(Expr.MakeAsm("LD_A_H"));
        injected.Add(Expr.MakeAsm("AND_IMM", new AsmOperand(ScrollSplitFlagUseBg, AddressMode.Immediate)));
        injected.Add(Expr.MakeAsm("JR_Z", new AsmOperand(applySkipBg, AddressMode.Absolute)));
        injected.Add(Expr.MakeAsm("LD_A_B"));
        injected.Add(Expr.MakeAsm("LDH_MEM_A", new AsmOperand(0x43, AddressMode.HighMem))); // SCX
        injected.Add(Expr.MakeAsm("LD_A_C"));
        injected.Add(Expr.MakeAsm("LDH_MEM_A", new AsmOperand(0x42, AddressMode.HighMem))); // SCY
        injected.Add(Expr.Make(Tag.Label, applySkipBg));
        injected.Add(Expr.MakeAsm("LD_A_H"));
        injected.Add(Expr.MakeAsm("AND_IMM", new AsmOperand(ScrollSplitFlagUseWin, AddressMode.Immediate)));
        injected.Add(Expr.MakeAsm("JR_Z", new AsmOperand(applySkipWin, AddressMode.Absolute)));
        injected.Add(Expr.MakeAsm("LD_A_D"));
        injected.Add(Expr.MakeAsm("LDH_MEM_A", new AsmOperand(0x4B, AddressMode.HighMem))); // WX
        injected.Add(Expr.MakeAsm("LD_A_E"));
        injected.Add(Expr.MakeAsm("LDH_MEM_A", new AsmOperand(0x4A, AddressMode.HighMem))); // WY
        injected.Add(Expr.Make(Tag.Label, applySkipWin));
        injected.Add(Expr.MakeAsm("LD_A_H"));
        injected.Add(Expr.MakeAsm("AND_IMM", new AsmOperand(ScrollSplitFlagWinHide, AddressMode.Immediate)));
        injected.Add(Expr.MakeAsm("JR_Z", new AsmOperand(applySkipHide, AddressMode.Absolute)));
        injected.Add(Expr.MakeAsm("LDH_A_MEM", new AsmOperand(0x40, AddressMode.HighMem))); // LCDC
        injected.Add(Expr.MakeAsm("AND_IMM", new AsmOperand(0xDF, AddressMode.Immediate)));
        injected.Add(Expr.MakeAsm("LDH_MEM_A", new AsmOperand(0x40, AddressMode.HighMem)));
        injected.Add(Expr.Make(Tag.Label, applySkipHide));
        injected.Add(Expr.MakeAsm("LD_A_H"));
        injected.Add(Expr.MakeAsm("AND_IMM", new AsmOperand(ScrollSplitFlagWinShow, AddressMode.Immediate)));
        injected.Add(Expr.MakeAsm("JR_Z", new AsmOperand(applySkipBgColor0, AddressMode.Absolute)));
        injected.Add(Expr.MakeAsm("LDH_A_MEM", new AsmOperand(0x40, AddressMode.HighMem))); // LCDC
        injected.Add(Expr.MakeAsm("OR_IMM", new AsmOperand(0x20, AddressMode.Immediate)));
        injected.Add(Expr.MakeAsm("LDH_MEM_A", new AsmOperand(0x40, AddressMode.HighMem)));
        injected.Add(Expr.Make(Tag.Label, applySkipBgColor0));
        injected.Add(Expr.MakeAsm("LD_A_H"));
        injected.Add(Expr.MakeAsm("AND_IMM", new AsmOperand(ScrollSplitFlagBgColor0, AddressMode.Immediate)));
        injected.Add(Expr.MakeAsm("JR_Z", new AsmOperand(applyDone, AddressMode.Absolute)));
        // CGB palette data writes are blocked during pixel transfer. Wait through
        // OAM search as well, so both color bytes fit in the following HBlank.
        // The LY-zero entry runs in VBlank and does not wait here.
        injected.Add(Expr.Make(Tag.Label, applyWaitPalette));
        injected.Add(Expr.MakeAsm("LDH_A_MEM", new AsmOperand(0x41, AddressMode.HighMem)));
        injected.Add(Expr.MakeAsm("AND_IMM", new AsmOperand(0x02, AddressMode.Immediate)));
        injected.Add(Expr.MakeAsm("JR_NZ", new AsmOperand(applyWaitPalette, AddressMode.Absolute)));
        injected.Add(Expr.MakeAsm("LD_A_IMM", new AsmOperand(0x80, AddressMode.Immediate)));
        injected.Add(Expr.MakeAsm("LDH_MEM_A", new AsmOperand(0x68, AddressMode.HighMem))); // BCPS: palette 0, color 0, auto increment
        injected.Add(Expr.MakeAsm("LD_A_B"));
        injected.Add(Expr.MakeAsm("LDH_MEM_A", new AsmOperand(0x69, AddressMode.HighMem))); // BCPD low
        injected.Add(Expr.MakeAsm("LD_A_C"));
        injected.Add(Expr.MakeAsm("LDH_MEM_A", new AsmOperand(0x69, AddressMode.HighMem))); // BCPD high
        injected.Add(Expr.Make(Tag.Label, applyDone));
        injected.Add(Expr.MakeAsm("RET"));

        // Capture base window visibility and arm a nonempty table at index zero, enabling VBlank/STAT interrupts and issuing EI.
        string commitDisable = "__kq_scroll_split_commit_disable";
        injected.Add(Expr.Make(Tag.Function, "__kq_scroll_split_commit_core"));
        injected.Add(Expr.MakeAsm("LDH_A_MEM", new AsmOperand(0x40, AddressMode.HighMem))); // LCDC
        injected.Add(Expr.MakeAsm("AND_IMM", new AsmOperand(0x20, AddressMode.Immediate)));
        injected.Add(Expr.MakeAsm("LD_MEM_A", ScrollWinVisibleVar));
        injected.Add(Expr.MakeAsm("LD_A_MEM", ScrollSplitCountVar));
        injected.Add(Expr.MakeAsm("OR_A"));
        injected.Add(Expr.MakeAsm("JR_Z", new AsmOperand(commitDisable, AddressMode.Absolute)));
        injected.Add(Expr.MakeAsm("LD_A_IMM", new AsmOperand(1, AddressMode.Immediate)));
        injected.Add(Expr.MakeAsm("LD_MEM_A", ScrollSplitEnabledVar));
        injected.Add(Expr.MakeAsm("XOR_A"));
        injected.Add(Expr.MakeAsm("LD_MEM_A", ScrollSplitIndexVar));
        injected.Add(Expr.MakeAsm("CALL", new AsmOperand("__kq_scroll_schedule_next", AddressMode.Absolute)));
        injected.Add(Expr.MakeAsm("LD_A_MEM", new AsmOperand(0xFFFF, AddressMode.Absolute))); // IE
        injected.Add(Expr.MakeAsm("OR_IMM", new AsmOperand(0x03, AddressMode.Immediate))); // VBlank | STAT
        injected.Add(Expr.MakeAsm("LD_MEM_A", new AsmOperand(0xFFFF, AddressMode.Absolute)));
        injected.Add(Expr.MakeAsm("EI"));
        injected.Add(Expr.MakeAsm("RET"));
        injected.Add(Expr.Make(Tag.Label, commitDisable));
        injected.Add(Expr.MakeAsm("XOR_A"));
        injected.Add(Expr.MakeAsm("LD_MEM_A", ScrollSplitEnabledVar));
        injected.Add(Expr.MakeAsm("LD_MEM_A", ScrollSplitIndexVar));
        injected.Add(Expr.MakeAsm("CALL", new AsmOperand("__kq_scroll_schedule_next", AddressMode.Absolute)));
        injected.Add(Expr.MakeAsm("RET"));

        // Save registers/SVBK, select WRAM bank 1, restore base scroll state and restart the split sequence for the frame.
        // A first entry at LY zero is applied immediately before scheduling the following entry.
        string vbDisable = "__kq_vblank_disable";
        string vbFirstNonZero = "__kq_vblank_first_nonzero";
        string vbDone = "__kq_vblank_done";
        injected.Add(Expr.Make(Tag.Function, "__kq_vblank_vector"));
        injected.Add(Expr.MakeAsm("PUSH_AF"));
        injected.Add(Expr.MakeAsm("PUSH_BC"));
        injected.Add(Expr.MakeAsm("PUSH_DE"));
        injected.Add(Expr.MakeAsm("PUSH_HL"));
        injected.Add(Expr.MakeAsm("LDH_A_MEM", new AsmOperand(0x70, AddressMode.HighMem))); // SVBK
        injected.Add(Expr.MakeAsm("PUSH_AF"));
        injected.Add(Expr.MakeAsm("LD_A_IMM", new AsmOperand(1, AddressMode.Immediate)));
        injected.Add(Expr.MakeAsm("LDH_MEM_A", new AsmOperand(0x70, AddressMode.HighMem)));
        injected.Add(Expr.MakeAsm("LD_A_IMM", new AsmOperand(1, AddressMode.Immediate)));
        // Set the existing fixed-address frame-ready flag used by the VBlank path.
        injected.Add(Expr.MakeAsm("LD_MEM_A", new AsmOperand(0xC29C, AddressMode.Absolute)));
        injected.Add(Expr.MakeAsm("LD_A_MEM", ScrollBgXCurVar));
        injected.Add(Expr.MakeAsm("LDH_MEM_A", new AsmOperand(0x43, AddressMode.HighMem))); // SCX
        injected.Add(Expr.MakeAsm("LD_A_MEM", ScrollBgYCurVar));
        injected.Add(Expr.MakeAsm("LDH_MEM_A", new AsmOperand(0x42, AddressMode.HighMem))); // SCY
        injected.Add(Expr.MakeAsm("LD_A_MEM", ScrollWinXCurVar));
        injected.Add(Expr.MakeAsm("LDH_MEM_A", new AsmOperand(0x4B, AddressMode.HighMem))); // WX
        injected.Add(Expr.MakeAsm("LD_A_MEM", ScrollWinYCurVar));
        injected.Add(Expr.MakeAsm("LDH_MEM_A", new AsmOperand(0x4A, AddressMode.HighMem))); // WY
        injected.Add(Expr.MakeAsm("LDH_A_MEM", new AsmOperand(0x40, AddressMode.HighMem))); // LCDC
        injected.Add(Expr.MakeAsm("AND_IMM", new AsmOperand(0xDF, AddressMode.Immediate)));
        injected.Add(Expr.MakeAsm("LD_B_A"));
        injected.Add(Expr.MakeAsm("LD_A_MEM", ScrollWinVisibleVar));
        injected.Add(Expr.MakeAsm("OR_B"));
        injected.Add(Expr.MakeAsm("LDH_MEM_A", new AsmOperand(0x40, AddressMode.HighMem)));
        injected.Add(Expr.MakeAsm("LD_A_MEM", ScrollSplitEnabledVar));
        injected.Add(Expr.MakeAsm("OR_A"));
        injected.Add(Expr.MakeAsm("JR_Z", new AsmOperand(vbDisable, AddressMode.Absolute)));
        injected.Add(Expr.MakeAsm("LD_A_MEM", ScrollSplitCountVar));
        injected.Add(Expr.MakeAsm("OR_A"));
        injected.Add(Expr.MakeAsm("JR_Z", new AsmOperand(vbDisable, AddressMode.Absolute)));
        injected.Add(Expr.MakeAsm("XOR_A"));
        injected.Add(Expr.MakeAsm("LD_MEM_A", ScrollSplitIndexVar));
        injected.Add(Expr.MakeAsm("LD_A_MEM", new AsmOperand(ScrollSplitTableAddr, AddressMode.Absolute)));
        injected.Add(Expr.MakeAsm("OR_A"));
        injected.Add(Expr.MakeAsm("JR_NZ", new AsmOperand(vbFirstNonZero, AddressMode.Absolute)));
        injected.Add(Expr.MakeAsm("LD_HL_IMM", new AsmOperand(ScrollSplitTableAddr, AddressMode.Immediate)));
        injected.Add(Expr.MakeAsm("CALL", new AsmOperand("__kq_scroll_apply_entry", AddressMode.Absolute)));
        injected.Add(Expr.MakeAsm("LD_A_IMM", new AsmOperand(1, AddressMode.Immediate)));
        injected.Add(Expr.MakeAsm("LD_MEM_A", ScrollSplitIndexVar));
        injected.Add(Expr.MakeAsm("CALL", new AsmOperand("__kq_scroll_schedule_next", AddressMode.Absolute)));
        injected.Add(Expr.MakeAsm("JR", new AsmOperand(vbDone, AddressMode.Absolute)));
        injected.Add(Expr.Make(Tag.Label, vbFirstNonZero));
        injected.Add(Expr.MakeAsm("XOR_A"));
        injected.Add(Expr.MakeAsm("CALL", new AsmOperand("__kq_scroll_schedule_next", AddressMode.Absolute)));
        injected.Add(Expr.MakeAsm("JR", new AsmOperand(vbDone, AddressMode.Absolute)));
        injected.Add(Expr.Make(Tag.Label, vbDisable));
        injected.Add(Expr.MakeAsm("CALL", new AsmOperand("__kq_scroll_schedule_next", AddressMode.Absolute)));
        injected.Add(Expr.Make(Tag.Label, vbDone));
        injected.Add(Expr.MakeAsm("POP_AF"));
        injected.Add(Expr.MakeAsm("LDH_MEM_A", new AsmOperand(0x70, AddressMode.HighMem))); // restore SVBK
        injected.Add(Expr.MakeAsm("POP_HL"));
        injected.Add(Expr.MakeAsm("POP_DE"));
        injected.Add(Expr.MakeAsm("POP_BC"));
        injected.Add(Expr.MakeAsm("POP_AF"));
        injected.Add(Expr.MakeAsm("RETI"));

        // Save registers/SVBK, apply the current entry and advance its index, or disable further coincidence interrupts at the end.
        string statDisable = "__kq_stat_disable";
        string statDone = "__kq_stat_done";
        injected.Add(Expr.Make(Tag.Function, "__kq_stat_vector"));
        injected.Add(Expr.MakeAsm("PUSH_AF"));
        injected.Add(Expr.MakeAsm("PUSH_BC"));
        injected.Add(Expr.MakeAsm("PUSH_DE"));
        injected.Add(Expr.MakeAsm("PUSH_HL"));
        injected.Add(Expr.MakeAsm("LDH_A_MEM", new AsmOperand(0x70, AddressMode.HighMem))); // SVBK
        injected.Add(Expr.MakeAsm("PUSH_AF"));
        injected.Add(Expr.MakeAsm("LD_A_IMM", new AsmOperand(1, AddressMode.Immediate)));
        injected.Add(Expr.MakeAsm("LDH_MEM_A", new AsmOperand(0x70, AddressMode.HighMem)));
        injected.Add(Expr.MakeAsm("LD_A_MEM", ScrollSplitEnabledVar));
        injected.Add(Expr.MakeAsm("OR_A"));
        injected.Add(Expr.MakeAsm("JR_Z", new AsmOperand(statDisable, AddressMode.Absolute)));
        injected.Add(Expr.MakeAsm("LD_A_MEM", ScrollSplitIndexVar));
        injected.Add(Expr.MakeAsm("LD_B_A"));
        injected.Add(Expr.MakeAsm("LD_A_MEM", ScrollSplitCountVar));
        injected.Add(Expr.MakeAsm("CP_B"));
        injected.Add(Expr.MakeAsm("JR_C", new AsmOperand(statDisable, AddressMode.Absolute)));
        injected.Add(Expr.MakeAsm("JR_Z", new AsmOperand(statDisable, AddressMode.Absolute)));
        injected.Add(Expr.MakeAsm("LD_A_B"));
        injected.Add(Expr.MakeAsm("LD_E_A"));
        injected.Add(Expr.MakeAsm("LD_D_IMM", new AsmOperand(0, AddressMode.Immediate)));
        injected.Add(Expr.MakeAsm("LD_L_A"));
        injected.Add(Expr.MakeAsm("LD_H_IMM", new AsmOperand(0, AddressMode.Immediate)));
        injected.Add(Expr.MakeAsm("ADD_HL_HL"));
        injected.Add(Expr.MakeAsm("ADD_HL_DE"));
        injected.Add(Expr.MakeAsm("ADD_HL_HL"));
        injected.Add(Expr.MakeAsm("LD_DE_IMM", new AsmOperand(ScrollSplitTableAddr, AddressMode.Immediate)));
        injected.Add(Expr.MakeAsm("ADD_HL_DE"));
        // DMG STAT writes can request an interrupt unrelated to LYC. Consume
        // this entry only on its scheduled line; leave the index intact otherwise.
        injected.Add(Expr.MakeAsm("LD_A_HL"));
        injected.Add(Expr.MakeAsm("LD_B_A"));
        injected.Add(Expr.MakeAsm("LDH_A_MEM", new AsmOperand(0x44, AddressMode.HighMem)));
        injected.Add(Expr.MakeAsm("CP_B"));
        injected.Add(Expr.MakeAsm("JR_NZ", new AsmOperand(statDone, AddressMode.Absolute)));
        injected.Add(Expr.MakeAsm("CALL", new AsmOperand("__kq_scroll_apply_entry", AddressMode.Absolute)));
        injected.Add(Expr.MakeAsm("LD_A_MEM", ScrollSplitIndexVar));
        injected.Add(Expr.MakeAsm("INC_A"));
        injected.Add(Expr.MakeAsm("LD_MEM_A", ScrollSplitIndexVar));
        injected.Add(Expr.MakeAsm("CALL", new AsmOperand("__kq_scroll_schedule_next", AddressMode.Absolute)));
        injected.Add(Expr.MakeAsm("JR", new AsmOperand(statDone, AddressMode.Absolute)));
        injected.Add(Expr.Make(Tag.Label, statDisable));
        // Acknowledge an already-disabled source without writing STAT again.
        injected.Add(Expr.MakeAsm("LDH_A_MEM", new AsmOperand(0x41, AddressMode.HighMem)));
        injected.Add(Expr.MakeAsm("AND_IMM", new AsmOperand(0x40, AddressMode.Immediate)));
        injected.Add(Expr.MakeAsm("JR_Z", new AsmOperand(statDone, AddressMode.Absolute)));
        injected.Add(Expr.MakeAsm("LDH_A_MEM", new AsmOperand(0x41, AddressMode.HighMem))); // STAT
        injected.Add(Expr.MakeAsm("AND_IMM", new AsmOperand(0xBF, AddressMode.Immediate)));
        injected.Add(Expr.MakeAsm("LDH_MEM_A", new AsmOperand(0x41, AddressMode.HighMem)));
        injected.Add(Expr.Make(Tag.Label, statDone));
        injected.Add(Expr.MakeAsm("POP_AF"));
        injected.Add(Expr.MakeAsm("LDH_MEM_A", new AsmOperand(0x70, AddressMode.HighMem))); // restore SVBK
        injected.Add(Expr.MakeAsm("POP_HL"));
        injected.Add(Expr.MakeAsm("POP_DE"));
        injected.Add(Expr.MakeAsm("POP_BC"));
        injected.Add(Expr.MakeAsm("POP_AF"));
        injected.Add(Expr.MakeAsm("RETI"));

        int insertAt = 0;
        while (insertAt < rawLines.Count && rawLines[insertAt].MatchTag(Tag.RstMap)) insertAt++;
        rawLines.InsertRange(insertAt, injected);
    }
__scroll_win_add

将有符号位移加到库记录的窗口坐标上,并立即应用。

语法

c
void __scroll_win_add(s8 dx, s8 dy);

参数

dx

有符号水平位移,单位为像素,范围为 −128 至 127。

dy

有符号垂直位移,单位为像素,范围为 −128 至 127。

返回值

无返回值,本条说明受影响状态及生效时机。

使用条件与限制

不执行等待。立即设置或增量操作会将受影响的整对待提交坐标同步为当前值,并清除其脏标记。即使只立即设置一个轴,同一对中另一个轴的待提交值也会被取消。结果按模 256 回绕。增量操作以库记录的当前值为起点,而不是待提交值或光栅处理临时写入寄存器的值。

使用示例

c
__scroll_win_add(4,-40);result[24]=M_WX;result[25]=M_WY;

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

预期结果

学习立即值与待提交值的区别,以及有符号增量的回绕。背景 X 在提交前为 250,提交后为 16;WX 从 23 变为 39。背景 X 加上 −20 得到 252;WY 加上 −40 得到 248。可视化示例还展示了背景矩形和独立的窗口边框。

__scroll_win_add: DMG
DMG
__scroll_win_add: CGB
CGB

完整示例程序

构建命令

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqgb\kitaqgb.exe .\kitaq-docs\samples\api-examples\gb\batch2_scroll_intrinsics.c -I .\kitaqgb\lib -I .\kitaq-docs\samples -o .\out\batch2_scroll_intrinsics.gb --no-cache --no-disasm --profile=dev --rst-disable --stack-bank=fixed --cgb=cgb --cart=mbc5 --romsize=64k

使用示例

c
Camera_Init(&view);
Camera_Set(&view,2048,4096);
Camera_ApplyBg(&view);
Scroll_SetWindow(103,80);
Scroll_WindowShow();

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

预期结果

摄像机位于 (8,16) 时,红色 32×24 矩形显示在屏幕坐标 (24,16)。窗口在 (96,80) 显示蓝色 64×64 边框,在 (112,96) 显示绿色 16×16 方块。在 DMG 上,所有图形均为黑色。

__scroll_win_add: DMG
DMG
__scroll_win_add: CGB
CGB

完整示例程序

构建命令

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqgb\kitaqgb.exe .\kitaq-docs\samples\api-examples\gb\batch2_camera_view.c -I .\kitaqgb\lib -I .\kitaq-docs\samples -o .\out\batch2_camera_view.gb --no-cache --no-disasm --profile=dev --rst-disable --stack-bank=fixed --cgb=cgb --cart=mbc5 --romsize=64k
实现与源码

kitaqgb/lib/scroll.h:38

kitaqgb/kitaqgb/CodeGenerator.cs:7866

csharp
if (funcName == "__scroll_win_add")
            {
                if (args.Length != 2) Error(expr, "__scroll_win_add(dx, dy) expects 2 arguments");
                EnsureScrollState();

                CompileIntoA(args[0]);
                EmitPushAAsWord();
                CompileIntoA(args[1]);
                EmitAsm("LD_C_A");
                EmitAsm("POP_HL");
                EmitAsm("LD_B_L");

                EmitLoadA(ScrollWinXCurVar);
                EmitAsm("ADD_B");
                EmitStoreA(ScrollWinXCurVar);
                EmitStoreA(ScrollWinXNextVar);
                EmitAsm("LDH_MEM_A", new AsmOperand(0x4B, AddressMode.HighMem)); // WX

                EmitLoadA(ScrollWinYCurVar);
                EmitAsm("ADD_C");
                EmitStoreA(ScrollWinYCurVar);
                EmitStoreA(ScrollWinYNextVar);
                EmitAsm("LDH_MEM_A", new AsmOperand(0x4A, AddressMode.HighMem)); // WY

                EmitLoadA(ScrollDirtyVar);
                EmitAsm("AND_IMM", new AsmOperand(0xFF ^ ScrollDirtyWinMask, AddressMode.Immediate));
                EmitStoreA(ScrollDirtyVar);
                return;
            }
__scroll_win_hide

清除 LCDC 的窗口显示位,并将库记录的窗口状态设为隐藏。

语法

c
void __scroll_win_hide();

返回值

无返回值,本条说明受影响状态及生效时机。

使用条件与限制

不改变坐标或地图选择。实际显示还要求 LCD 及背景/窗口功能已启用,并且窗口位于可见区域内。

使用示例

c
__scroll_win_hide();result[27]=M_LCDC&32;

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

预期结果

学习立即值与待提交值的区别,以及有符号增量的回绕。背景 X 在提交前为 250,提交后为 16;WX 从 23 变为 39。背景 X 加上 −20 得到 252;WY 加上 −40 得到 248。可视化示例还展示了背景矩形和独立的窗口边框。

__scroll_win_hide: DMG
DMG
__scroll_win_hide: CGB
CGB

完整示例程序

构建命令

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqgb\kitaqgb.exe .\kitaq-docs\samples\api-examples\gb\batch2_scroll_intrinsics.c -I .\kitaqgb\lib -I .\kitaq-docs\samples -o .\out\batch2_scroll_intrinsics.gb --no-cache --no-disasm --profile=dev --rst-disable --stack-bank=fixed --cgb=cgb --cart=mbc5 --romsize=64k

使用示例

c
Camera_Init(&view);
Camera_Set(&view,2048,4096);
Camera_ApplyBg(&view);
Scroll_SetWindow(103,80);
Scroll_WindowShow();

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

预期结果

摄像机位于 (8,16) 时,红色 32×24 矩形显示在屏幕坐标 (24,16)。窗口在 (96,80) 显示蓝色 64×64 边框,在 (112,96) 显示绿色 16×16 方块。在 DMG 上,所有图形均为黑色。

__scroll_win_hide: DMG
DMG
__scroll_win_hide: CGB
CGB

完整示例程序

构建命令

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqgb\kitaqgb.exe .\kitaq-docs\samples\api-examples\gb\batch2_camera_view.c -I .\kitaqgb\lib -I .\kitaq-docs\samples -o .\out\batch2_camera_view.gb --no-cache --no-disasm --profile=dev --rst-disable --stack-bank=fixed --cgb=cgb --cart=mbc5 --romsize=64k
实现与源码

kitaqgb/lib/scroll.h:36

kitaqgb/kitaqgb/CodeGenerator.cs:7821

csharp
if (funcName == "__scroll_win_hide")
            {
                if (args.Length != 0) Error(expr, "__scroll_win_hide() expects 0 arguments");
                EnsureScrollState();

                EmitAsm("LDH_A_MEM", new AsmOperand(0x40, AddressMode.HighMem)); // LCDC
                EmitAsm("AND_IMM", new AsmOperand(0xDF, AddressMode.Immediate));
                EmitAsm("LDH_MEM_A", new AsmOperand(0x40, AddressMode.HighMem));
                EmitAsm("XOR_A");
                EmitStoreA(ScrollWinVisibleVar);
                return;
            }
__scroll_win_set

立即设置窗口的 WX 和 WY。

语法

c
void __scroll_win_set(u8 wx, u8 wy);

参数

wx

原始 WX 字节值,范围为 0–255:在所需屏幕 X 坐标上加 7;窗口左缘位于屏幕左侧时使用 7。

wy

窗口顶端的屏幕 Y 坐标,范围为 0–255 像素;可见行范围为 0–143。

返回值

无返回值,本条说明受影响状态及生效时机。

使用条件与限制

不执行等待。立即设置或增量操作会将受影响的整对待提交坐标同步为当前值,并清除其脏标记。即使只立即设置一个轴,同一对中另一个轴的待提交值也会被取消。

使用示例

c
__scroll_win_set(15,8);result[8]=M_WX;result[9]=M_WY;

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

预期结果

学习立即值与待提交值的区别,以及有符号增量的回绕。背景 X 在提交前为 250,提交后为 16;WX 从 23 变为 39。背景 X 加上 −20 得到 252;WY 加上 −40 得到 248。可视化示例还展示了背景矩形和独立的窗口边框。

__scroll_win_set: DMG
DMG
__scroll_win_set: CGB
CGB

完整示例程序

构建命令

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqgb\kitaqgb.exe .\kitaq-docs\samples\api-examples\gb\batch2_scroll_intrinsics.c -I .\kitaqgb\lib -I .\kitaq-docs\samples -o .\out\batch2_scroll_intrinsics.gb --no-cache --no-disasm --profile=dev --rst-disable --stack-bank=fixed --cgb=cgb --cart=mbc5 --romsize=64k

使用示例

c
Camera_Init(&view);
Camera_Set(&view,2048,4096);
Camera_ApplyBg(&view);
Scroll_SetWindow(103,80);
Scroll_WindowShow();

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

预期结果

摄像机位于 (8,16) 时,红色 32×24 矩形显示在屏幕坐标 (24,16)。窗口在 (96,80) 显示蓝色 64×64 边框,在 (112,96) 显示绿色 16×16 方块。在 DMG 上,所有图形均为黑色。

__scroll_win_set: DMG
DMG
__scroll_win_set: CGB
CGB

完整示例程序

构建命令

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqgb\kitaqgb.exe .\kitaq-docs\samples\api-examples\gb\batch2_camera_view.c -I .\kitaqgb\lib -I .\kitaq-docs\samples -o .\out\batch2_camera_view.gb --no-cache --no-disasm --profile=dev --rst-disable --stack-bank=fixed --cgb=cgb --cart=mbc5 --romsize=64k
实现与源码

kitaqgb/lib/scroll.h:16

kitaqgb/kitaqgb/CodeGenerator.cs:7589

csharp
if (funcName == "__scroll_win_set")
            {
                if (args.Length != 2) Error(expr, "__scroll_win_set(wx, wy) expects 2 arguments");
                EnsureScrollState();

                CompileIntoA(args[0]);
                EmitPushAAsWord();
                CompileIntoA(args[1]);
                EmitAsm("LD_B_A");
                EmitAsm("POP_HL");

                EmitAsm("LD_A_L");
                EmitStoreA(ScrollWinXCurVar);
                EmitStoreA(ScrollWinXNextVar);
                EmitAsm("LDH_MEM_A", new AsmOperand(0x4B, AddressMode.HighMem)); // WX

                EmitAsm("LD_A_B");
                EmitStoreA(ScrollWinYCurVar);
                EmitStoreA(ScrollWinYNextVar);
                EmitAsm("LDH_MEM_A", new AsmOperand(0x4A, AddressMode.HighMem)); // WY

                EmitLoadA(ScrollDirtyVar);
                EmitAsm("AND_IMM", new AsmOperand(0xFF ^ ScrollDirtyWinMask, AddressMode.Immediate));
                EmitStoreA(ScrollDirtyVar);
                return;
            }
__scroll_win_set_buffered

暂存窗口的两个坐标,等待后续提交。

语法

c
void __scroll_win_set_buffered(u8 wx, u8 wy);

参数

wx

原始 WX 字节值,范围为 0–255:在所需屏幕 X 坐标上加 7;窗口左缘位于屏幕左侧时使用 7。

wy

窗口顶端的屏幕 Y 坐标,范围为 0–255 像素;可见行范围为 0–143。

返回值

无返回值,本条说明受影响状态及生效时机。

使用条件与限制

不改变库记录的当前值或寄存器。请调用 __scroll_flush 或 Scroll_Flush 应用暂存值;暂存操作本身不等待 VBlank。

使用示例

c
__scroll_win_set_buffered(39,32);result[16]=M_WX;result[17]=__scroll_win_x_get();

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

预期结果

学习立即值与待提交值的区别,以及有符号增量的回绕。背景 X 在提交前为 250,提交后为 16;WX 从 23 变为 39。背景 X 加上 −20 得到 252;WY 加上 −40 得到 248。可视化示例还展示了背景矩形和独立的窗口边框。

__scroll_win_set_buffered: DMG
DMG
__scroll_win_set_buffered: CGB
CGB

完整示例程序

构建命令

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqgb\kitaqgb.exe .\kitaq-docs\samples\api-examples\gb\batch2_scroll_intrinsics.c -I .\kitaqgb\lib -I .\kitaq-docs\samples -o .\out\batch2_scroll_intrinsics.gb --no-cache --no-disasm --profile=dev --rst-disable --stack-bank=fixed --cgb=cgb --cart=mbc5 --romsize=64k

使用示例

c
Camera_Init(&view);
Camera_Set(&view,2048,4096);
Camera_ApplyBg(&view);
Scroll_SetWindow(103,80);
Scroll_WindowShow();

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

预期结果

摄像机位于 (8,16) 时,红色 32×24 矩形显示在屏幕坐标 (24,16)。窗口在 (96,80) 显示蓝色 64×64 边框,在 (112,96) 显示绿色 16×16 方块。在 DMG 上,所有图形均为黑色。

__scroll_win_set_buffered: DMG
DMG
__scroll_win_set_buffered: CGB
CGB

完整示例程序

构建命令

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqgb\kitaqgb.exe .\kitaq-docs\samples\api-examples\gb\batch2_camera_view.c -I .\kitaqgb\lib -I .\kitaq-docs\samples -o .\out\batch2_camera_view.gb --no-cache --no-disasm --profile=dev --rst-disable --stack-bank=fixed --cgb=cgb --cart=mbc5 --romsize=64k
实现与源码

kitaqgb/lib/scroll.h:25

kitaqgb/kitaqgb/CodeGenerator.cs:7721

csharp
if (funcName == "__scroll_win_set_buffered")
            {
                if (args.Length != 2) Error(expr, "__scroll_win_set_buffered(wx, wy) expects 2 arguments");
                EnsureScrollState();

                CompileIntoA(args[0]);
                EmitPushAAsWord();
                CompileIntoA(args[1]);
                EmitAsm("LD_B_A");
                EmitAsm("POP_HL");

                EmitAsm("LD_A_L");
                EmitStoreA(ScrollWinXNextVar);
                EmitAsm("LD_A_B");
                EmitStoreA(ScrollWinYNextVar);
                EmitLoadA(ScrollDirtyVar);
                EmitAsm("OR_IMM", new AsmOperand(ScrollDirtyWinMask, AddressMode.Immediate));
                EmitStoreA(ScrollDirtyVar);
                return;
            }
__scroll_win_show

置位 LCDC 的窗口显示位,并将库记录的窗口状态设为可见。

语法

c
void __scroll_win_show();

返回值

无返回值,本条说明受影响状态及生效时机。

使用条件与限制

不改变坐标或地图选择。实际显示还要求 LCD 及背景/窗口功能已启用,并且窗口位于可见区域内。

使用示例

c
__scroll_win_show();result[26]=M_LCDC&32;

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

预期结果

学习立即值与待提交值的区别,以及有符号增量的回绕。背景 X 在提交前为 250,提交后为 16;WX 从 23 变为 39。背景 X 加上 −20 得到 252;WY 加上 −40 得到 248。可视化示例还展示了背景矩形和独立的窗口边框。

__scroll_win_show: DMG
DMG
__scroll_win_show: CGB
CGB

完整示例程序

构建命令

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqgb\kitaqgb.exe .\kitaq-docs\samples\api-examples\gb\batch2_scroll_intrinsics.c -I .\kitaqgb\lib -I .\kitaq-docs\samples -o .\out\batch2_scroll_intrinsics.gb --no-cache --no-disasm --profile=dev --rst-disable --stack-bank=fixed --cgb=cgb --cart=mbc5 --romsize=64k

使用示例

c
Camera_Init(&view);
Camera_Set(&view,2048,4096);
Camera_ApplyBg(&view);
Scroll_SetWindow(103,80);
Scroll_WindowShow();

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

预期结果

摄像机位于 (8,16) 时,红色 32×24 矩形显示在屏幕坐标 (24,16)。窗口在 (96,80) 显示蓝色 64×64 边框,在 (112,96) 显示绿色 16×16 方块。在 DMG 上,所有图形均为黑色。

__scroll_win_show: DMG
DMG
__scroll_win_show: CGB
CGB

完整示例程序

构建命令

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqgb\kitaqgb.exe .\kitaq-docs\samples\api-examples\gb\batch2_camera_view.c -I .\kitaqgb\lib -I .\kitaq-docs\samples -o .\out\batch2_camera_view.gb --no-cache --no-disasm --profile=dev --rst-disable --stack-bank=fixed --cgb=cgb --cart=mbc5 --romsize=64k
实现与源码

kitaqgb/lib/scroll.h:35

kitaqgb/kitaqgb/CodeGenerator.cs:7807

csharp
if (funcName == "__scroll_win_show")
            {
                if (args.Length != 0) Error(expr, "__scroll_win_show() expects 0 arguments");
                EnsureScrollState();

                EmitAsm("LDH_A_MEM", new AsmOperand(0x40, AddressMode.HighMem)); // LCDC
                EmitAsm("OR_IMM", new AsmOperand(0x20, AddressMode.Immediate));
                EmitAsm("LDH_MEM_A", new AsmOperand(0x40, AddressMode.HighMem));
                EmitAsm("LD_A_IMM", new AsmOperand(0x20, AddressMode.Immediate));
                EmitStoreA(ScrollWinVisibleVar);
                return;
            }
__scroll_win_x_get

读取库记录的当前窗口 WX 值。

语法

c
u8 __scroll_win_x_get();

返回值

返回库记录的当前字节值,范围为 0–255。WX 包含硬件所需的七像素偏移。

使用条件与限制

这不是直接读取硬件寄存器。返回值不包含待提交的缓冲值、直接写入寄存器的值,也不包含分段扫描处理中临时应用的值。

使用示例

c
result[12]=__scroll_win_x_get();

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

预期结果

学习立即值与待提交值的区别,以及有符号增量的回绕。背景 X 在提交前为 250,提交后为 16;WX 从 23 变为 39。背景 X 加上 −20 得到 252;WY 加上 −40 得到 248。可视化示例还展示了背景矩形和独立的窗口边框。

__scroll_win_x_get: DMG
DMG
__scroll_win_x_get: CGB
CGB

完整示例程序

构建命令

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqgb\kitaqgb.exe .\kitaq-docs\samples\api-examples\gb\batch2_scroll_intrinsics.c -I .\kitaqgb\lib -I .\kitaq-docs\samples -o .\out\batch2_scroll_intrinsics.gb --no-cache --no-disasm --profile=dev --rst-disable --stack-bank=fixed --cgb=cgb --cart=mbc5 --romsize=64k

使用示例

c
Camera_Init(&view);
Camera_Set(&view,2048,4096);
Camera_ApplyBg(&view);
Scroll_SetWindow(103,80);
Scroll_WindowShow();

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

预期结果

摄像机位于 (8,16) 时,红色 32×24 矩形显示在屏幕坐标 (24,16)。窗口在 (96,80) 显示蓝色 64×64 边框,在 (112,96) 显示绿色 16×16 方块。在 DMG 上,所有图形均为黑色。

__scroll_win_x_get: DMG
DMG
__scroll_win_x_get: CGB
CGB

完整示例程序

构建命令

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqgb\kitaqgb.exe .\kitaq-docs\samples\api-examples\gb\batch2_camera_view.c -I .\kitaqgb\lib -I .\kitaq-docs\samples -o .\out\batch2_camera_view.gb --no-cache --no-disasm --profile=dev --rst-disable --stack-bank=fixed --cgb=cgb --cart=mbc5 --romsize=64k
实现与源码

kitaqgb/lib/scroll.h:19

kitaqgb/kitaqgb/CodeGenerator.cs:7653

csharp
if (funcName == "__scroll_win_x_get")
            {
                if (args.Length != 0) Error(expr, "__scroll_win_x_get() expects 0 arguments");
                EnsureScrollState();
                EmitLoadA(ScrollWinXCurVar);
                return;
            }
__scroll_win_x_set

立即设置窗口的原始 WX 坐标,不改变 WY。

语法

c
void __scroll_win_x_set(u8 wx);

参数

wx

原始 WX 字节值,范围为 0–255:在所需屏幕 X 坐标上加 7;窗口左缘位于屏幕左侧时使用 7。

返回值

无返回值,本条说明受影响状态及生效时机。

使用条件与限制

不执行等待。立即设置或增量操作会将受影响的整对待提交坐标同步为当前值,并清除其脏标记。即使只立即设置一个轴,同一对中另一个轴的待提交值也会被取消。

使用示例

c
__scroll_win_x_set(23);result[10]=M_WX;

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

预期结果

学习立即值与待提交值的区别,以及有符号增量的回绕。背景 X 在提交前为 250,提交后为 16;WX 从 23 变为 39。背景 X 加上 −20 得到 252;WY 加上 −40 得到 248。可视化示例还展示了背景矩形和独立的窗口边框。

__scroll_win_x_set: DMG
DMG
__scroll_win_x_set: CGB
CGB

完整示例程序

构建命令

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqgb\kitaqgb.exe .\kitaq-docs\samples\api-examples\gb\batch2_scroll_intrinsics.c -I .\kitaqgb\lib -I .\kitaq-docs\samples -o .\out\batch2_scroll_intrinsics.gb --no-cache --no-disasm --profile=dev --rst-disable --stack-bank=fixed --cgb=cgb --cart=mbc5 --romsize=64k

使用示例

c
Camera_Init(&view);
Camera_Set(&view,2048,4096);
Camera_ApplyBg(&view);
Scroll_SetWindow(103,80);
Scroll_WindowShow();

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

预期结果

摄像机位于 (8,16) 时,红色 32×24 矩形显示在屏幕坐标 (24,16)。窗口在 (96,80) 显示蓝色 64×64 边框,在 (112,96) 显示绿色 16×16 方块。在 DMG 上,所有图形均为黑色。

__scroll_win_x_set: DMG
DMG
__scroll_win_x_set: CGB
CGB

完整示例程序

构建命令

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqgb\kitaqgb.exe .\kitaq-docs\samples\api-examples\gb\batch2_camera_view.c -I .\kitaqgb\lib -I .\kitaq-docs\samples -o .\out\batch2_camera_view.gb --no-cache --no-disasm --profile=dev --rst-disable --stack-bank=fixed --cgb=cgb --cart=mbc5 --romsize=64k
实现与源码

kitaqgb/lib/scroll.h:17

kitaqgb/kitaqgb/CodeGenerator.cs:7617

csharp
if (funcName == "__scroll_win_x_set")
            {
                if (args.Length != 1) Error(expr, "__scroll_win_x_set(wx) expects 1 argument");
                EnsureScrollState();

                CompileIntoA(args[0]);
                EmitStoreA(ScrollWinXCurVar);
                EmitStoreA(ScrollWinXNextVar);
                EmitAsm("LDH_MEM_A", new AsmOperand(0x4B, AddressMode.HighMem)); // WX
                EmitLoadA(ScrollWinYCurVar);
                EmitStoreA(ScrollWinYNextVar);
                EmitLoadA(ScrollDirtyVar);
                EmitAsm("AND_IMM", new AsmOperand(0xFF ^ ScrollDirtyWinMask, AddressMode.Immediate));
                EmitStoreA(ScrollDirtyVar);
                return;
            }
__scroll_win_x_set_buffered

仅暂存窗口 WX 坐标。

语法

c
void __scroll_win_x_set_buffered(u8 wx);

参数

wx

原始 WX 字节值,范围为 0–255:在所需屏幕 X 坐标上加 7;窗口左缘位于屏幕左侧时使用 7。

返回值

无返回值,本条说明受影响状态及生效时机。

使用条件与限制

保留另一个轴的待提交值。在提交之前,当前值和寄存器均不改变;请先初始化整对坐标。

使用示例

c
__scroll_win_x_set_buffered(55);

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

预期结果

学习立即值与待提交值的区别,以及有符号增量的回绕。背景 X 在提交前为 250,提交后为 16;WX 从 23 变为 39。背景 X 加上 −20 得到 252;WY 加上 −40 得到 248。可视化示例还展示了背景矩形和独立的窗口边框。

__scroll_win_x_set_buffered: DMG
DMG
__scroll_win_x_set_buffered: CGB
CGB

完整示例程序

构建命令

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqgb\kitaqgb.exe .\kitaq-docs\samples\api-examples\gb\batch2_scroll_intrinsics.c -I .\kitaqgb\lib -I .\kitaq-docs\samples -o .\out\batch2_scroll_intrinsics.gb --no-cache --no-disasm --profile=dev --rst-disable --stack-bank=fixed --cgb=cgb --cart=mbc5 --romsize=64k

使用示例

c
Camera_Init(&view);
Camera_Set(&view,2048,4096);
Camera_ApplyBg(&view);
Scroll_SetWindow(103,80);
Scroll_WindowShow();

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

预期结果

摄像机位于 (8,16) 时,红色 32×24 矩形显示在屏幕坐标 (24,16)。窗口在 (96,80) 显示蓝色 64×64 边框,在 (112,96) 显示绿色 16×16 方块。在 DMG 上,所有图形均为黑色。

__scroll_win_x_set_buffered: DMG
DMG
__scroll_win_x_set_buffered: CGB
CGB

完整示例程序

构建命令

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqgb\kitaqgb.exe .\kitaq-docs\samples\api-examples\gb\batch2_camera_view.c -I .\kitaqgb\lib -I .\kitaq-docs\samples -o .\out\batch2_camera_view.gb --no-cache --no-disasm --profile=dev --rst-disable --stack-bank=fixed --cgb=cgb --cart=mbc5 --romsize=64k
实现与源码

kitaqgb/lib/scroll.h:26

kitaqgb/kitaqgb/CodeGenerator.cs:7743

csharp
if (funcName == "__scroll_win_x_set_buffered")
            {
                if (args.Length != 1) Error(expr, "__scroll_win_x_set_buffered(wx) expects 1 argument");
                EnsureScrollState();

                CompileIntoA(args[0]);
                EmitStoreA(ScrollWinXNextVar);
                EmitLoadA(ScrollDirtyVar);
                EmitAsm("OR_IMM", new AsmOperand(ScrollDirtyWinMask, AddressMode.Immediate));
                EmitStoreA(ScrollDirtyVar);
                return;
            }
__scroll_win_y_get

读取库记录的当前窗口 WY 值。

语法

c
u8 __scroll_win_y_get();

返回值

返回库记录的当前字节值,范围为 0–255。WX 包含硬件所需的七像素偏移。

使用条件与限制

这不是直接读取硬件寄存器。返回值不包含待提交的缓冲值、直接写入寄存器的值,也不包含分段扫描处理中临时应用的值。

使用示例

c
result[13]=__scroll_win_y_get();

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

预期结果

学习立即值与待提交值的区别,以及有符号增量的回绕。背景 X 在提交前为 250,提交后为 16;WX 从 23 变为 39。背景 X 加上 −20 得到 252;WY 加上 −40 得到 248。可视化示例还展示了背景矩形和独立的窗口边框。

__scroll_win_y_get: DMG
DMG
__scroll_win_y_get: CGB
CGB

完整示例程序

构建命令

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqgb\kitaqgb.exe .\kitaq-docs\samples\api-examples\gb\batch2_scroll_intrinsics.c -I .\kitaqgb\lib -I .\kitaq-docs\samples -o .\out\batch2_scroll_intrinsics.gb --no-cache --no-disasm --profile=dev --rst-disable --stack-bank=fixed --cgb=cgb --cart=mbc5 --romsize=64k

使用示例

c
Camera_Init(&view);
Camera_Set(&view,2048,4096);
Camera_ApplyBg(&view);
Scroll_SetWindow(103,80);
Scroll_WindowShow();

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

预期结果

摄像机位于 (8,16) 时,红色 32×24 矩形显示在屏幕坐标 (24,16)。窗口在 (96,80) 显示蓝色 64×64 边框,在 (112,96) 显示绿色 16×16 方块。在 DMG 上,所有图形均为黑色。

__scroll_win_y_get: DMG
DMG
__scroll_win_y_get: CGB
CGB

完整示例程序

构建命令

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqgb\kitaqgb.exe .\kitaq-docs\samples\api-examples\gb\batch2_camera_view.c -I .\kitaqgb\lib -I .\kitaq-docs\samples -o .\out\batch2_camera_view.gb --no-cache --no-disasm --profile=dev --rst-disable --stack-bank=fixed --cgb=cgb --cart=mbc5 --romsize=64k
实现与源码

kitaqgb/lib/scroll.h:20

kitaqgb/kitaqgb/CodeGenerator.cs:7662

csharp
if (funcName == "__scroll_win_y_get")
            {
                if (args.Length != 0) Error(expr, "__scroll_win_y_get() expects 0 arguments");
                EnsureScrollState();
                EmitLoadA(ScrollWinYCurVar);
                return;
            }
__scroll_win_y_set

立即设置窗口的 WY 坐标,不改变 WX。

语法

c
void __scroll_win_y_set(u8 wy);

参数

wy

窗口顶端的屏幕 Y 坐标,范围为 0–255 像素;可见行范围为 0–143。

返回值

无返回值,本条说明受影响状态及生效时机。

使用条件与限制

不执行等待。立即设置或增量操作会将受影响的整对待提交坐标同步为当前值,并清除其脏标记。即使只立即设置一个轴,同一对中另一个轴的待提交值也会被取消。

使用示例

c
__scroll_win_y_set(16);result[11]=M_WY;

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

预期结果

学习立即值与待提交值的区别,以及有符号增量的回绕。背景 X 在提交前为 250,提交后为 16;WX 从 23 变为 39。背景 X 加上 −20 得到 252;WY 加上 −40 得到 248。可视化示例还展示了背景矩形和独立的窗口边框。

__scroll_win_y_set: DMG
DMG
__scroll_win_y_set: CGB
CGB

完整示例程序

构建命令

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqgb\kitaqgb.exe .\kitaq-docs\samples\api-examples\gb\batch2_scroll_intrinsics.c -I .\kitaqgb\lib -I .\kitaq-docs\samples -o .\out\batch2_scroll_intrinsics.gb --no-cache --no-disasm --profile=dev --rst-disable --stack-bank=fixed --cgb=cgb --cart=mbc5 --romsize=64k

使用示例

c
Camera_Init(&view);
Camera_Set(&view,2048,4096);
Camera_ApplyBg(&view);
Scroll_SetWindow(103,80);
Scroll_WindowShow();

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

预期结果

摄像机位于 (8,16) 时,红色 32×24 矩形显示在屏幕坐标 (24,16)。窗口在 (96,80) 显示蓝色 64×64 边框,在 (112,96) 显示绿色 16×16 方块。在 DMG 上,所有图形均为黑色。

__scroll_win_y_set: DMG
DMG
__scroll_win_y_set: CGB
CGB

完整示例程序

构建命令

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqgb\kitaqgb.exe .\kitaq-docs\samples\api-examples\gb\batch2_camera_view.c -I .\kitaqgb\lib -I .\kitaq-docs\samples -o .\out\batch2_camera_view.gb --no-cache --no-disasm --profile=dev --rst-disable --stack-bank=fixed --cgb=cgb --cart=mbc5 --romsize=64k
实现与源码

kitaqgb/lib/scroll.h:18

kitaqgb/kitaqgb/CodeGenerator.cs:7635

csharp
if (funcName == "__scroll_win_y_set")
            {
                if (args.Length != 1) Error(expr, "__scroll_win_y_set(wy) expects 1 argument");
                EnsureScrollState();

                CompileIntoA(args[0]);
                EmitStoreA(ScrollWinYCurVar);
                EmitStoreA(ScrollWinYNextVar);
                EmitAsm("LDH_MEM_A", new AsmOperand(0x4A, AddressMode.HighMem)); // WY
                EmitLoadA(ScrollWinXCurVar);
                EmitStoreA(ScrollWinXNextVar);
                EmitLoadA(ScrollDirtyVar);
                EmitAsm("AND_IMM", new AsmOperand(0xFF ^ ScrollDirtyWinMask, AddressMode.Immediate));
                EmitStoreA(ScrollDirtyVar);
                return;
            }
__scroll_win_y_set_buffered

仅暂存窗口 WY 坐标。

语法

c
void __scroll_win_y_set_buffered(u8 wy);

参数

wy

窗口顶端的屏幕 Y 坐标,范围为 0–255 像素;可见行范围为 0–143。

返回值

无返回值,本条说明受影响状态及生效时机。

使用条件与限制

保留另一个轴的待提交值。在提交之前,当前值和寄存器均不改变;请先初始化整对坐标。

使用示例

c
__scroll_win_y_set_buffered(48);

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

预期结果

学习立即值与待提交值的区别,以及有符号增量的回绕。背景 X 在提交前为 250,提交后为 16;WX 从 23 变为 39。背景 X 加上 −20 得到 252;WY 加上 −40 得到 248。可视化示例还展示了背景矩形和独立的窗口边框。

__scroll_win_y_set_buffered: DMG
DMG
__scroll_win_y_set_buffered: CGB
CGB

完整示例程序

构建命令

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqgb\kitaqgb.exe .\kitaq-docs\samples\api-examples\gb\batch2_scroll_intrinsics.c -I .\kitaqgb\lib -I .\kitaq-docs\samples -o .\out\batch2_scroll_intrinsics.gb --no-cache --no-disasm --profile=dev --rst-disable --stack-bank=fixed --cgb=cgb --cart=mbc5 --romsize=64k

使用示例

c
Camera_Init(&view);
Camera_Set(&view,2048,4096);
Camera_ApplyBg(&view);
Scroll_SetWindow(103,80);
Scroll_WindowShow();

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

预期结果

摄像机位于 (8,16) 时,红色 32×24 矩形显示在屏幕坐标 (24,16)。窗口在 (96,80) 显示蓝色 64×64 边框,在 (112,96) 显示绿色 16×16 方块。在 DMG 上,所有图形均为黑色。

__scroll_win_y_set_buffered: DMG
DMG
__scroll_win_y_set_buffered: CGB
CGB

完整示例程序

构建命令

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqgb\kitaqgb.exe .\kitaq-docs\samples\api-examples\gb\batch2_camera_view.c -I .\kitaqgb\lib -I .\kitaq-docs\samples -o .\out\batch2_camera_view.gb --no-cache --no-disasm --profile=dev --rst-disable --stack-bank=fixed --cgb=cgb --cart=mbc5 --romsize=64k
实现与源码

kitaqgb/lib/scroll.h:27

kitaqgb/kitaqgb/CodeGenerator.cs:7757

csharp
if (funcName == "__scroll_win_y_set_buffered")
            {
                if (args.Length != 1) Error(expr, "__scroll_win_y_set_buffered(wy) expects 1 argument");
                EnsureScrollState();

                CompileIntoA(args[0]);
                EmitStoreA(ScrollWinYNextVar);
                EmitLoadA(ScrollDirtyVar);
                EmitAsm("OR_IMM", new AsmOperand(ScrollDirtyWinMask, AddressMode.Immediate));
                EmitStoreA(ScrollDirtyVar);
                return;
            }

sprite.h — OBJ 分配、定位、组合精灵与动画

精灵是可移动的硬件图形,通常为 8×8 或 8×16 像素,由对象属性存储器 OAM 的条目描述。先分配槽位,在工作副本中设置位置、图块和属性,再进行传输。元精灵将多个槽位组合成一个对象。GB 有 40 个槽位,每条扫描线最多 10 个;FC 有 64 个,每条扫描线最多 8 个。

GB 属性:位 4 选择 DMG 的 OBP1;位 5/6 控制 X/Y 翻转;位 7 将精灵置于非零背景色编号之后。CGB 的位 0..2 选择对象调色板,位 3 选择图块数据 VRAM 存储体;DMG 调色板选择位不用于原生 CGB 颜色。

示例使用完整的 GB 精灵库。定义 KQ_SPRITE_MINIMAL_RUNTIME 后,sprite.c 不再包含 sprite_free、sprite_set_pos、sprite_count_used、两个扫描线查询函数、metasprite_draw 和 anim_update。需要这些函数时请勿定义该宏。

__oam_dma

将内存页中的160字节传送到硬件OAM(0xFE00–0xFE9F),替换全部40个精灵条目。函数在传送完成后返回。

语法

c
void __oam_dma(u16 src_ptr);

参数

src_ptr

源字节地址。只使用高字节:传入0xC480会读取0xC400–0xC49F。请在256字节边界处准备160字节可读数据,并使用硬件支持的源页(0x00–0xDF)。所需的ROM或RAM存储体必须已映射。编译器不会检查地址或对齐情况。

返回值

无(void)。

使用条件与限制

每个条目依次包含Y、X、图块编号和属性,共4字节。屏幕坐标为(X−8, Y−16)。将未使用条目的Y设为0即可隐藏它。此内置函数只复制原始数据,不分配精灵,也不更新精灵库的计数器或影子缓冲区。

请在VBlank期间或LCD关闭时调用。函数本身不会等待VBlank,也不会改变LCD设置。硬件传送需要160个CPU机器周期,此外还需加上函数准备和返回的开销。

包含此调用的程序会为传送例程保留8字节HRAM。每次调用都会初始化例程,并在DMA工作期间于HRAM中等待。中断源会被暂时屏蔽,但IE和IME的启用状态保持不变。中断服务会延迟到传送完成后执行。

使用示例

c
// raw_oam is a 160-byte array at address 0xC400.
m_wait();
__oam_dma((u16)raw_oam);

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

预期结果

示例直接构造完整的OAM页:在(88,32)显示8×8右箭头,在(88,56)显示其水平镜像,在(88,80)显示8×8方块。CGB使用红色箭头边缘、绿色箭杆、蓝色箭头尖端细节和绿色方块;DMG使用灰阶。其他槽位全部隐藏。验证会在模拟器的两种硬件模式下检查实际图像像素;尚未进行实机测试。

__oam_dma: DMG
DMG
__oam_dma: CGB
CGB

完整示例程序

构建命令

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqgb\kitaqgb.exe .\kitaq-docs\samples\api-examples\gb\oam_dma.c -I .\kitaqgb\lib -I .\kitaq-docs\samples -o .\out\oam_dma.gb --cgb=cgb --profile=dev --rst-disable --stack-bank=fixed --no-cache --no-disasm
实现与源码

kitaqgb/lib/sprite.h:46

csharp
// Copy one 160-byte, page-aligned OAM source and return after DMA completes.
            if (funcName == "__oam_dma")
            {
                if (args.Length != 1) Error(expr, "__oam_dma(src_ptr) expects 1 argument");

                CompileIntoHL(args[0]); // Evaluate once; hardware uses only the high byte.
                // Mask interrupt sources without changing IME, including calls made inside an ISR.
                EmitAsm("LD_A_MEM", new AsmOperand(0xFFFF, AddressMode.Absolute));
                EmitAsm("PUSH_AF");
                EmitAsm("XOR_A");
                EmitAsm("LD_MEM_A", new AsmOperand(0xFFFF, AddressMode.Absolute));
                // LDH [FF46],A; LD B,40; DEC B; JR NZ,-3; RET.
                // Initialize on every call, so no startup copy or readiness flag is needed.
                byte[] dmaStub = { 0xE0, 0x46, 0x06, 0x28, 0x05, 0x20, 0xFD, 0xC9 };
                for (int i = 0; i < dmaStub.Length; i++)
                {
                    EmitAsm("LD_A_IMM", new AsmOperand(dmaStub[i], AddressMode.Immediate));
                    EmitAsm("LDH_MEM_A", new AsmOperand((OamDmaStubAddr + i) & 0xFF, AddressMode.HighMem));
                }
                EmitAsm("LD_A_H");
                // CALL pushes before DMA starts; RET reads the stack only after the wait.
                EmitAsm("CALL", new AsmOperand(OamDmaStubAddr, AddressMode.Absolute));
                EmitAsm("POP_AF");
                EmitAsm("LD_MEM_A", new AsmOperand(0xFFFF, AddressMode.Absolute));
                return;
            }

命令语法参考

下方帮助取自本源码版本,可查询可选参数、默认值和允许值。请将示例输入文件名换成自己的文件。实际工具响应保留原文。

kitaqgb
help
KITAQGB native Rust compiler

Commands:
  disasm ROM -o dis.s
  header ROM -o patched.gb [--title=NAME --cgb=dmg|cgb|cgb_only ...]
  assemble-ir INPUT.assembly.json -o output.gb [header options]
  tokenize SOURCE.c ... -I INCLUDE_DIR -o tokens.json
  parse SOURCE.c ... -I INCLUDE_DIR -o ast.json
  lower SOURCE.c ... -I INCLUDE_DIR -o lowered.json
  emit-ir SOURCE.c ... -I INCLUDE_DIR -o raw.assembly.json
  compile SOURCE.c ... -I INCLUDE_DIR -o output.gb [-O0|-O1]
  optimize-ir INPUT.assembly.json -o optimized.json [-O0|-O1]
  strip-ir INPUT.assembly.json -o stripped.json [--strip-keep=LABELS]

C compilation, aggregates, banked calls and hardware intrinsics are implemented and covered by differential tests. Utility commands: kqhelp, symfind, src2asm, romdiff, template, snippet, conventions, fixhint, irsum, attrviz, recipe, test.

Development and recovery:
  SOURCE.c ... --watch [--watch-max-builds=N]
  devserver SOURCE.c ... [--once --poll-ms=N --debounce-ms=N --max-builds=N]
  recipe [--history=FILE --out=FILE --script=FILE --last=N --stdout]
  SOURCE.c ... --minimize[=FILE] [--minimize-out=FILE --minimize-work=DIR --minimize-quick --minimize-trace=final|all]
  SOURCE.c ... --minimize-on-fail --repro-pack[=DIR]
  SOURCE.c ... --debug-output --disasm-changed[=GIT_BASE]
Watch loops stop with Ctrl+C; the Rust API also accepts a CancellationToken. See PORT_STATUS.md for executed verification and compatibility limits.

开发命令语法

这些用法取自 Program.DebugTools.cs 与 Program.VibeTools.cs。ROM 比较、符号搜索和模板属于独立子命令,与普通 C 编译不同。

usage
usage: kitaqgb kqhelp <KQxxxx>
usage
usage: kitaqgb symfind <pattern> [--map=<.map>] [--dbg=<.dbg>] [--regex] [--ignore-case] [--out=<file>|-]
usage
usage: kitaqgb src2asm <source.c:line> [--disasm=<dis.s>] [--funcsizes=<.funcsizes.txt>] [--context=N] [--out=<file>|-]
usage
usage: kitaqgb romdiff <old.gb> <new.gb> [--old-func=<.funcsizes.txt>] [--new-func=<.funcsizes.txt>] [--old-map=<.map>] [--new-map=<.map>] [--out=<file>|-]
usage
usage: kitaqgb template [out.c] [--out=<file.c>] [--cgb] [--overwrite] [--no-build-script] [--no-readme]
usage
usage: kitaqgb conventions [--out=<file>|--stdout]
usage
usage: kitaqgb snippet list | get <id> [--out=<file>|-] | emit <category|all> [--out=<file>|-]
usage
usage: kitaqgb fixhint [diag.json|stderr.log] [--in=<file>] [--out=<file>|-] [--max=N]
usage
usage: kitaqgb irsum [--ir=<syntax_tree_lowered.txt>] [--asm=<assembly_code.txt>] [--top=N] [--out=<file>|-]
usage
usage: kitaqgb devserver [--poll-ms=N] [--debounce-ms=N] [--once] [--max-builds=N] [--deps=<file>] <compile args...>
usage
usage: kitaqgb recipe [--history=<file>] [--last=N] [--out=<file>] [--script=<file>] [--stdout]

解析器识别的长选项

选项名称取自 src/cli.rs,包含兼容和调查选项。是否接收数值,应查看帮助和对应处理程序。

--abi--abi-diff-report--abi-verify--attach--auto-minimize--bank-sim--cache--cart--cgb--cgb-consistency--cross-bank-report--debug-out--debug-output--deps-out--dest--diag-json--disasm--disasm-changed--disasm-changed-out--emit-ai-metadata--emit-path-manifest--farcall-suggest--fast--fast-build--func-size-report--header-logo--help--hotspot-report--include-dir--machine-readable--max-errors--minimize-on-fail--no-banner--no-cache--no-disasm--no-header-logo--no-rst--opt-diff--permissive--profile--ramsize--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--stack-bank--stack-reserve--stack-top--strict--trace--trace-out--verify-cgb-symbols--version--vlist--vlist-out

附录:汇编指令索引

此表从 src/asm_info.rs 提取,说明编译器内部指令名称与操作数格式。包含分支目标或内存地址的条目是语法示例,并非独立程序。寄存器保留和标志变化请查阅调用约定与代码生成实现。

opcode指令格式语法示例
00NOPIMPNOP
F3DIIMPDI
FBEIIMPEI
76HALTIMPHALT
10STOPIMM8STOP #1
C9RETIMPRET
C0RET_NZIMPRET_NZ
C8RET_ZIMPRET_Z
D0RET_NCIMPRET_NC
D8RET_CIMPRET_C
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
46LD_B_HLIMPLD_B_HL
4ELD_C_HLIMPLD_C_HL
56LD_D_HLIMPLD_D_HL
5ELD_E_HLIMPLD_E_HL
66LD_H_HLIMPLD_H_HL
6ELD_L_HLIMPLD_L_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