KITAQ DEVELOPMENT SYSTEM
USER'S MANUAL 2026.09

KITAQ SERIES • REFERENCE EDITION

KITAQFC

Famicom C programming

From your first line to execution, observation and retesting.
Source snapshot: September 14, 2026 · English edition: September 14, 2026

Native Rust compiler and helper tools

Build the compiler and all helper tools on Windows, Linux, macOS ARM or macOS Intel with Rust 1.85 or later. The native executables run without .NET; production asset tools also run without Python or Pillow.

Windows executables are at the repository root. Linux and macOS executables are under bin/ in the platform folders listed below. Keep lib/ and license notices with the tools. On Linux/macOS, run chmod +x on the downloaded executables and add their folder to PATH, or invoke them by their full path.

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

kitaqfc · Native Rust compiler and helper tools · GitHub Actions

Native builds and executable checks passed on Windows, Linux, macOS ARM and macOS Intel. KITAQGB passed 48 tests and 395 helper checks per platform; KITAQFC passed 55 tests and 401 helper checks. Rust 1.85 was also tested. PUBLIC_DISTRIBUTION.json records the installed binary hashes and validation provenance. The public GitHub Actions workflows rebuild and test this source independently.

Frozen reference outputs test ROM bytes, diagnostics and helper formats. Earlier C# emulator evidence remains historical evidence with its original source fingerprints. It does not automatically prove every Rust API, real hardware or complete FDS BIOS/game startup. The unavailable original PNG conversion script was reconstructed from its specification; byte parity with that missing script cannot be claimed.

--attach is accepted for compatibility and does not attach a managed debugger to the native executable. Use a native debugger for host debugging.

KITAQGB · SHA-256 · KITAQFC · SHA-256

Portable sample builds

PowerShell build scripts use the root Windows executable. For Linux/macOS use the native compiler command with the same C inputs and options, or PowerShell 7. Graphics CHR and C source are separate inputs; font.chr keeps the original sample font.

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

Current compiler and library distribution — 4 October 2026

Use kitaqfc.exe and lib from the same checkout. From the parent directory, run the Release build script below; the project copies the executable to the repository root. BINARY_BUILD.json records the compiler source hashes, executable hash and checks for that build. Long diagnostic lines are limited to 200 characters followed by an ellipsis.

The current C# build was compared with the previous distribution using 29 inputs. Fifteen accepted inputs produced identical ROM bytes; the other fourteen inputs produced matching rejection diagnostics. Two additional ROMs ran for four frames in KUROSAKI: one checked entity allocation, callback dispatch and slot reuse; the other checked signed arithmetic, the KQBody3D layout and audio queue constants. The report records 18 expected RAM bytes and the actual values. These cases do not test audio playback or every library API; prior regression results are identified separately in BINARY_BUILD.json.

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

Source fixtures and execution evidence

1. KITAQFC and the GB compiler

KITAQFC uses the KITAQGB front end to generate code for the NES/Famicom's 6502-family CPU. It does not convert a GB ROM into an NES ROM. Write software for the target's display, sound, memory and mapper.

KITAQFC supports structure copies, ordinary function calls, for, while and do-while. A do-while loop runs its body at least once before testing the condition. continue proceeds to that final test; break leaves the loop. A switch selects a case constant in 0..255 or the default body when no case matches. Its selector is evaluated once. break exits the innermost loop or switch; continue inside a switch advances the enclosing loop.

2. Requirements and build

Build the compiler and all helper tools on Windows, Linux, macOS ARM or macOS Intel with Rust 1.85 or later. The native executables run without .NET; production asset tools also run without Python or Pillow.

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

Windows executables are at the repository root. Linux and macOS executables are under bin/ in the platform folders listed below. Keep lib/ and license notices with the tools. On Linux/macOS, run chmod +x on the downloaded executables and add their folder to PATH, or invoke them by their full path.

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

PowerShell build scripts use the root Windows executable. For Linux/macOS use the native compiler command with the same C inputs and options, or PowerShell 7. Graphics CHR and C source are separate inputs; font.chr keeps the original sample font.

3. Your first program

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

The sample displays HELLO WORLD and 042. Its m_wait helper commits the VRAM queue, waits for NMI and restores scrolling. Transfers through PPUADDR affect internal scroll state; omitting restoration can shift the text toward an edge. Learn the sequence: display off, prepare assets, display on, synchronize with NMI.

4. Language introduction

The GB volume's statements and expressions provide a common starting point. FC accepts unsigned char and unsigned short; core.h defines u8, u16, s8 and s16. fc.h is an umbrella header. No-argument functions can use void main(void) here.

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

Use integers within their 8-bit or 16-bit ranges. Array indexes start at zero. fc_aggregate.c demonstrates functions, pointers and structures; fc_arithmetic.c demonstrates arithmetic; fc_control.c demonstrates loops. Do not include CGB registers or GB-only intrinsics in an FC program.

Expression results and evaluation

The comparison operators ==, !=, <, <=, >, >= and logical operators !, &&, || return 0 for false and 1 for true. You can store the result in a u16, pass or return it, or use it in arithmetic. For example, score = 500 + (lives != 0); produces 501 when a life remains and 500 otherwise.

&& skips its right operand when the left operand is zero. || skips its right operand when the left operand is nonzero. In pointer != 0 && pointer->active != 0, a null pointer prevents the member access. Both bytes of a 16-bit value participate in the truth test, so 256 is true. Bitwise & and | do not short-circuit.

++value returns the updated value; value++ returns the original value. These operators also accept array elements, dereferenced pointers and structure members. buffer[index()]++ calls index() once. For a u16 *p, p++ advances two bytes to the next element, while (*p)++ increments the pointed-to value.

sizeof(array) gives the entire array's size in bytes; sizeof(pointer) is 2. For u16 values[9];, sizeof(values) is 18. This includes ROM arrays, local arrays and array members. sizeof(function()) inspects the return type without calling the function.

A function declaration and definition must agree on parameter types and order. Their parameter names may differ; the body uses the definition's names. For example, u8 next(u8 input); can be defined as u8 next(u8 value) { return (u8)(value + 1); }. Parameters and local variables hide globals with the same name.

Selecting arrays or strings with ?: produces a pointer to the selected element type. You can pass it directly, as in show(ready ? "READY" : "WAIT");. For u16 arrays a and b, (ready ? a : b) + 1 advances two bytes to the second element of the selected array. This does not copy the array.

condition ? yes : no evaluates its condition and then only the selected arm. The condition and arms may contain shifts with variable counts. For example, on = (pattern & (0x80 >> bit)) != 0 ? 4 : 2; selects four or two for the chosen bit. Function calls in a right operand skipped by && or || are also skipped. A continue in a for loop runs the update expression once before reevaluating the condition; in while and do ... while, it advances to the condition.

5. Loops and state dispatch

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

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

These fragments show a loop that updates at least once and a branch that selects a handler for the current state. Define update, condition, state and the state handlers in your program. See fc_control.c for a complete loop example and the library's frame and scene sample for a complete scene-management ROM.

Register scene, entity and system callbacks with the argument and return types required by their declarations. The libraries invoke registered handlers from the corresponding update, drawing or frame-wait operations. Follow each API's ROM-bank and mapping requirements. Do not assume desktop-style support for recursion or variadic functions.

6. Memory and the PPU

NES internal CPU RAM occupies 0x0000-0x07FF. Its mirrors above 0x0800 are not additional RAM. The 6502 stack occupies page 1, while OAM shadows and queues reserve other regions. --nes-local-ram=START:LENGTH and --nes-temp-ram=START:LENGTH are advanced settings that require map inspection.

PPU addresses form a separate address space. CHR supplies patterns, nametables position tiles, attribute tables choose palette groups, and palettes contain color codes. Background attributes normally apply to 16-by-16-pixel regions, so they do not behave like GB tile attributes.

7. NMI and the VRAM queue

NMI is the interrupt associated with the display frame boundary. Large direct PPU writes during rendering can corrupt the screen. Initialize directly while rendering is off; use __vramq_put, __vramq_copy, __vramq_fill and commit for ordinary updates.

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

Check queue capacity and source lifetimes. The default NMI handles the queue. A custom __nes_nmi must retain the required queue execution, OAM work and register preservation.

8. Mappers and ROM layout

SelectionTypical starting use
nromSmall fixed-ROM lessons
uxrom / cnrom / axromSimple PRG or CHR switching
mmc1 / mmc3 / mmc5Larger programs and mapper-specific features
vrc6 / vrc7 / fme7Banking and corresponding expansion features
fdsDisk-image output

These are compiler selections, not a hardware or emulator completeness table. --board=surom512 selects a particular MMC1 board arrangement; merely padding a file to 512 KiB does not establish that arrangement. Use KUROSAKI's board audit alongside it.

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

Check board requirements for --battery / --no-battery, CHR capacity, PRG placement and banked calls. After changing a mapper or mirroring mode, test startup, scrolling and data switching as well as ROM generation.

Ordinary indexed reads and C pointer references to ROM arrays place those arrays in common bank 0 unless #pragma fixed_bank explicitly fixes their placement. This keeps the data visible when a caller runs in a switchable bank. For explicit far access, pass the bare array name with __bankof of that same name in the same call, for example __farpeek8(__bankof(table), table). A bank-number query alone does not request common-bank placement. Explicit fixed-bank data and references inside assembly require you to maintain the correct mapping; this rule is a syntax-based placement check, not pointer-flow analysis. Common-bank capacity still applies.

8.1 CHR RAM for writable graphics

--nes-chr-ram builds a cartridge ROM that uses 8 KiB of writable CHR RAM. No CHR data is stored in the ROM file, so the program must upload tile patterns to the PPU during initialization. The wire3d.c renderer uses this mode.

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

--nes-chr=tiles.chr or --chr-rom=tiles.chr embeds prepared patterns as CHR ROM. Neither can be combined with --nes-chr-ram. The CNROM profile also rejects CHR RAM mode. Check that the target mapper and board provide the required CHR RAM.

Omitting all CHR options embeds a blank 8 KiB CHR ROM. Explicitly select --nes-chr-ram for examples that upload patterns at runtime. CHR RAM is separate from CPU-side PRG RAM; the wireframe staging buffer also needs its own PRG RAM allocation.

9. FDS, expansion sound and peripherals

FDS involves disk-file placement, startup, overlays and saving. Consult fds_manifest_sample.json and the FDS headers. Prepare any required BIOS in your own runtime environment; the public distribution contains no BIOS.

Calling a VRC6 or VRC7 sound operation does not change the ROM's mapper setting. Match the mapper to the expansion sound being used. For peripherals, separately test readable input, connection state and effects on the ordinary controller path.

10. Diagnostics and build results

KQ diagnostics and developer commands such as symfind, src2asm and romdiff resemble their GB counterparts. Some inherited GB help choices may not represent implemented NES features. The FC dictionary is collected separately from FC source and headers.

Warnings such as KQ2421 for direct PPU operations can appear even in display-off initialization. Do not disrupt safe initialization merely to eliminate a warning: inspect rendering timing and execution logs. Zero errors and zero warnings are different outcomes.

11. Reference example: developing a game with AI

Fill in the requirements, then give the complete prompt to your AI assistant. It covers implementation, emulator testing, SARAKURA analysis and retesting.

· Download Markdown

Fill in the requirements and give this entire document to the AI assistant. Commands assume sibling repositories named kitaqgb, kitaqfc, kokura, kurosaki, sarakura and kitaq-docs, with your game-gb or game-fc project beside them. Run commands from their parent directory; adapt paths to the actual environment.

Requirements

  • Game title: <fill in>
  • Genre and core gameplay: <fill in>
  • Controls, success and failure conditions: <fill in>
  • Required screens, stages, enemies and items: <fill in>
  • Visual style, music and sound effects: <fill in; identify any supplied assets>
  • Saving, communication, peripherals and other requirements: <fill in, or none>
  • Project directory: <fill in>
  • Redistribution requirements: <for example, original code and assets suitable for MIT publication>
  • Target: <NES/Famicom cartridge or FDS>
  • Mapper, ROM size and mirroring: <specify, or select from the requirements>
  • Video standard and performance: <for example, NTSC and 60 gameplay updates per second>

Task

Implement the game with KITAQFC and its libraries. Use KUROSAKI for execution and debugging, and SARAKURA to organize diagnostics and compare results before and after a fix.

Repeat this cycle until the acceptance criteria are met: make the specification concrete → implement a small change → build → apply inputs and observe → investigate the cause → fix → retest under the same conditions. A plan, a code listing or a successful compilation is not completion.

Establish the environment and acceptance criteria

  1. Read workspace instructions, tool READMEs, HTML manuals, and the headers and implementations of the libraries you will use. Record executable paths and versions or SHA-256 hashes. Verify commands against actual --help output and APIs against source.
  2. Define measurable acceptance criteria for inputs, images, audio, progression and update frequency. Examples: pressing and releasing START begins the game; a collision removes one life; pausing silences the intended audio and resuming restores playback.
  3. Ask only about material ambiguities. Make ordinary reversible implementation decisions autonomously. Do not weaken requirements or acceptance criteria.
  4. First run a small supplied sample through the compiler, emulator and SARAKURA. This checks the tool connection, not completion of the requested game.

Implement a small playable slice

  • Start with NROM for a small game; choose MMC3 or another mapper when banking or size requires it. Verify required board features using inspect-rom, mapper-info, audit-board and the implementation, rather than relying on the mapper name alone.
  • Plan PRG/CHR size, CHR-ROM or CHR-RAM, mirroring, fixed banks, interrupt vectors and save RAM. Check headers against actual placement after optimizations or banking changes. --nes-local-ram uses internal CPU RAM in $0000–$07FF; avoid overlaps with zero page, stack, OAM buffers and runtime/library storage.
  • Use the KITAQFC C dialect, FC libraries and void main(void). Do not assume GB APIs are compatible. Check implementation providers and include the required .c files; some header entries are declarations only.
  • Account for PPU registers, NMI, OAM DMA, per-scanline sprite limits, scrolling, mirroring, attribute tables and APU/DMC behavior. Transfer-queue free space is not PPU VRAM capacity; budget the work done per NMI.
  • Convert the supplied original ascii.c font to FC CHR for letters, digits and symbols. Verify CHR, palettes, nametables and attributes. For FDS, separately verify disk access, saving and BIOS requirements; do not assume cartridge boot conditions.
  • First connect boot, title, a controllable player, success or failure, and restart. Then expand the game.
  • Keep editable graphics, music and sound-effect sources and their generation steps. Verify that the build actually consumes their exports.
  • Write source comments in English and progress reports in English. Keep SARAKURA’s standard reports in English.

Connect each build to its execution

Use a separate output directory for each iteration, such as out/iter-001. Record commands, exit codes, and hashes of source, assets, tools, ROM and metadata. Never run an older ROM after a failed build. Maps, source maps and debug information must come from the same build as the ROM.

The following is an NROM check with no input. Supply main.c, required library implementation units and the CHR file; select the appropriate mapper. Do not pass compiler build metadata to KUROSAKI’s --kitaqfc-debug without first checking its required format.

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

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

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

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

A 300-frame run with no input is only an initial check. Add ordered gameplay scenarios before claiming that the game works.

Reproduce ordered input

  • --pad1 and --pad2 use raw NES bitmasks: A=1, B=2, SELECT=4, START=8, UP=16, DOWN=32, LEFT=64, RIGHT=128. Do not confuse these with library BTN_* values.
  • run --pad1 applies fixed input. For sequences of actions, create a replay that distinguishes presses, holds and releases. Inspect the Replay and ReplayFrame definitions. The public CLI’s replay-record records neutral input, not a person’s interactive play.
  • Check ROM SHA-256, replay identity and frame range yourself. replay-run --verify compares an expected final hash only when one is provided; it does not comprehensively validate gameplay or ROM identity. Never overwrite expected results with observed results merely to make a test pass.
powershell
& '.\kurosaki\kurosaki.exe' replay-run `
  '.\game-fc\out\iter-001\game.nes' '.\game-fc\tests\start-and-play.replay.json' `
  --frames 900 --json '.\game-fc\out\iter-001\play.json' `
  --png '.\game-fc\out\iter-001\play.png' `
  --wav '.\game-fc\out\iter-001\play.wav'

Prepare the replay for the ROM being tested. The public CLI’s replay-run has no --emit-diagnostics option. Do not invent that option or pass CPU trace data as diagnostic events. To analyze ordered-input scenarios with SARAKURA, create a project-local test harness using public kurosaki-core APIs: RunOptions.replay_frames and diagnostic_events_from_trace_and_report. Run the same ROM, replay and frame conditions; generate diagnostic JSONL from that execution’s trace and diagnostic report. Check trace configuration and retained ranges, and compare the harness’s images and observations with CLI replay results. Do not present no-input diagnostics as evidence for a gameplay scenario. If the required environment is unavailable, report this verification as incomplete.

Check images, audio, state and performance

  • Save input scenarios with distinct presses, holds and releases. Exercise every specified path: boot, start, movement, actions, collisions, scrolling, stage changes, game over, restart, pause and saving or communication where applicable.
  • Preserve PNGs at relevant frames, input data, execution reports, diagnostic JSONL, WAVs and any necessary state or memory observations. Check the reached frame count and stop reason. Actually open the images; one screenshot cannot establish motion or input response. Compare counters, positions and state changes with expected values. Check screen edges, tile/attribute boundaries and crowded sprite scenes.
  • Check music, effects, simultaneous playback, dropouts, pause and resume. A generated WAV alone does not establish correct sound. If listening is unavailable, distinguish the waveform/numerical checks performed from unverified audible qualities.
  • Measure heavy scenes, target CPU/update workload and transfers; on FC, include NMI work. Host emulator throughput is not game update frequency or proof of hardware speed. Continuing with --allow-unimplemented, where available, does not demonstrate support for the missing feature.

Analyze, repair and retest

  • Feed SARAKURA the build metadata for the tested ROM and diagnostic JSONL from the tested execution. A CPU trace or ordinary run report is not a substitute. --frames specifies analysis conditions; SARAKURA does not execute the ROM or automatically edit the source.
  • Read report.html, ai_diagnostics.json, repair_prompt.md and retest_plan.json. Compare diagnoses with reproduction steps, images, audio and source. Distinguish inferred source locations or causes from verified facts, and normal waiting loops from hangs. Assess warnings individually and record unsupported events or analysis limits. Do not hide warnings with filters or shorten tests to obtain a passing result.
  • Reduce failures to minimal reproductions, fix their causes and rebuild. If the compiler or emulator is responsible, isolate its defect from game code and add regression verification for the tool fix.
  • Retest with matching input, random seed, hardware/video mode, mapper, observed frames and diagnostic settings. Use new metadata for each new ROM; do not blindly reuse save states after code or RAM layout changes.
powershell
& '.\sarakura\sarakura.exe' baseline-delta `
  --baseline '.\game-fc\out\iter-001\analysis' `
  --current '.\game-fc\out\iter-002\analysis' `
  --out '.\game-fc\out\delta.json' --markdown '.\game-fc\out\delta.md' `
  --fail-on-new error --fail-on-regression error --enforce

Use diagnostic differences alongside gameplay, graphics and audio acceptance checks. If the same failure repeats, revisit the evidence and hypothesis instead of continuing arbitrary changes.

Completion and deliverables

Rerun all required scenarios against the final ROM built from the delivered source and settings. Invincibility, automatic test input or another mapper alone does not verify normal play in the final build. Provide a requirement-to-test table, reasons for remaining warnings and explicit unverified or unsupported items. State “not tested on physical hardware” when applicable.

Deliver source, tool/library identities, editable assets, reproducible build and test scripts, the ROM, final verification evidence, and a README covering setup, controls and known limits. Include replay data and a test harness where needed. Publish or send files externally only within explicitly authorized scope. Delete unnecessary intermediate builds and temporary traces after verification, preserving source, assets, final deliverables and needed regression evidence.

If environment or permission constraints prevent a required check, report the exact reproduction steps and required action. Do not mark the work complete.

Source locations

The compiler sources and project file are in the repository’s same-named subdirectory. The executable is at the repository root, and the libraries are in lib/. See the directory layout for project paths and build requirements.

Complete sample programs

Each program, the shared headers, the supplied ascii.c and its converted assets are included. Names beginning with m_ are helpers written for these manuals. Read each build command together with its expected result and verification record. Run commands from the parent of the sibling repository folders.

Batch build script / Results and screenshots

Display the supplied character set fc_font.c

Purpose: See which letters, digits and punctuation marks the supplied original font contains and how to draw them.

How it works: m_init prepares the display and font. m_text draws each row; m_put draws the double quote using character code 34.

Controls and expected result: Look for 26 uppercase letters, 26 lowercase letters, 10 digits and 30 punctuation marks. Check that every glyph is legible. No input is required.

Display the supplied character set: Look for 26 uppercase letters, 26 lowercase letters, 10 digits and 30 punctuation marks. Check that every glyph is legible. No input is required.
Display example: Look for 26 uppercase letters, 26 lowercase letters, 10 digits and 30 punctuation marks. Check that every glyph is legible. No input is required.

Download source / Verification status

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


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

Purpose: Learn the basic sequence for starting a ROM and drawing text and a number on the background.

How it works: After m_init, m_text draws the greeting and m_number formats 42. The final loop waits for frames while leaving the screen visible.

Controls and expected result: The screen shows HELLO WORLD and 042. The leading zero is three-digit decimal formatting.

Your first text display: The screen shows HELLO WORLD and 042. The leading zero is three-digit decimal formatting.
Display example: The screen shows HELLO WORLD and 042. The leading zero is three-digit decimal formatting.

Download source / Verification status

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


// Initialize the display/font, write HELLO WORLD and a three-digit 042,
// then wait for frames so the result remains visible.
void main(void) {
    m_init();
    m_text(2,3,"HELLO");
    m_text(2,5,"HELLO WORLD");
    m_number(42);
    while (1) { m_wait();  }
}
Arithmetic with a wider intermediate fc_arithmetic.c

Purpose: Learn to keep an intermediate result that does not fit in eight bits.

How it works: (u16)200 + 100 produces 300. The program divides it by 10, then converts the result to u8 for display. Notice that the widening cast precedes the addition.

Controls and expected result: 030 represents the calculated result, 30.

Arithmetic with a wider intermediate: 030 represents the calculated result, 30.
Display example: 030 represents the calculated result, 30.

Download source / Verification status

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


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

Purpose: Try for, continue, while and if/else in a small calculation.

How it works: The for loop skips 3 while adding 0 through 7, producing 25. The while loop advances to 32; the following if assigns 42 when that value matches.

Controls and expected result: 042 is the final result. This program uses an if/else branch.

Branches and loops: 042 is the final result. This program uses an if/else branch.
Display example: 042 is the final result. This program uses an if/else branch.

Download source / Verification status

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


// Exercise loop control and a final branch, then display the selected value 42.
void main(void) {
    m_init();
    m_text(2,3,"CONTROL");
    u8 i; u8 sum; sum=0;
    for (i=0;i<8;i++) { if (i==3) continue; sum=(u8)(sum+i); }
    while (sum<32) { sum++; }
    if (sum==32) sum=42; else sum=0;
    m_number(sum);
    while (1) { m_wait();  }
}
Functions, arrays, structures and pointers fc_aggregate.c

Purpose: Learn to group values and access them through functions and pointers.

How it works: The program copies a Point containing (10,20), then writes 1 through 4 into an array through a pointer. It adds the function result 30 to array values 1, 2 and 4 and the constant 5.

Controls and expected result: The sum is displayed as 042. Follow the structure copy, the ptr[i] writes and the function return value.

Functions, arrays, structures and pointers: The sum is displayed as 042. Follow the structure copy, the ptr[i] writes and the function return value.
Display example: The sum is displayed as 042. Follow the structure copy, the ptr[i] writes and the function return value.

Download source / Verification status

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

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

Purpose: Pack flags into a byte and copy a buffer into separate storage.

How it works: __memset clears four bytes. The program sets bit 3, toggles bit 1, then copies all four bytes with __memcpy.

Controls and expected result: The first destination byte is displayed as 010: bit 3 contributes 8 and bit 1 contributes 2.

Bit flags and buffer copying: The first destination byte is displayed as 010: bit 3 contributes 8 and bit 1 contributes 2.
Display example: The first destination byte is displayed as 010: bit 3 contributes 8 and bit 1 contributes 2.

Download source / Verification status

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


// Clear the source bytes, set bits 3 and 1, copy the buffer, and display 8 + 2 = 10.
void main(void) {
    m_init();
    m_text(2,3,"BITS MEMORY");
    u8 data[4]; u8 copy[4];
    __memset(data,0,4); __bit_set(data,3); __bit_toggle(data,1);
    __memcpy(copy,data,4); m_number(copy[0]);
    while (1) { m_wait();  }
}
Count new button presses fc_input.c

Purpose: Distinguish a newly pressed button from one that remains held.

How it works: Each frame calls input_update. The counter advances only when input_pressed(BTN_A) is true.

Controls and expected result: The initial count is 000. Press A to get 001; holding it does not add more. Release and press again for 002. The byte counter wraps from 255 to 000.

Count new button presses: The initial count is 000. Press A to get 001; holding it does not add more. Release and press again for 002. The byte counter wraps from 255 to 000.
Display example: The initial count is 000. Press A to get 001; holding it does not add more. Release and press again for 002. The byte counter wraps from 255 to 000.

Download source / Verification status

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

Purpose: Learn integer-to-Q8.8 conversion, multiplication and conversion back to an integer.

How it works: fix_from_int converts 3 and 4 to Q8.8. fix_mul multiplies them, and fix_to_int converts the product back.

Controls and expected result: 012 means 3 × 4 = 12. It is the converted integer, not the internal value scaled by 256.

Multiply Q8.8 fixed-point values: 012 means 3 × 4 = 12. It is the converted integer, not the internal value scaled by 256.
Display example: 012 means 3 × 4 = 12. It is the converted integer, not the internal value scaled by 256.

Download source / Verification status

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

// Convert 3 and 4 to Q8.8, multiply them, convert the result back to an
// integer, and display 12.
void main(void) {
    m_init();
    m_text(2,3,"FIXED");
    fix8 a; fix8 b; a=fix_from_int(3); b=fix_from_int(4);
    m_number((u8)fix_to_int(fix_mul(a,b)));
    while (1) { m_wait();  }
}
Allocate from a fixed object pool fc_entity.c

Purpose: Create an object from a fixed set of slots without dynamic allocation.

How it works: entity_init resets the pool. entity_create creates type 1 at (20,30); the program checks for the failure ID 0xFF before setting vx to 2.

Controls and expected result: 001 is the active object count, not an ID or coordinate. This example does not update or draw the object, so no moving character appears.

Allocate from a fixed object pool: 001 is the active object count, not an ID or coordinate. This example does not update or draw the object, so no moving character appears.
Display example: 001 is the active object count, not an ID or coordinate. This example does not update or draw the object, so no moving character appears.

Download source / Verification status

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

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

Purpose: Keep a named game value in a diagnostic buffer separately from the screen display.

How it works: The program initializes debugging, sets frame tag 1 and records the value 42 under the name HP.

Controls and expected result: 001 is the number of trace entries. HP=42 is stored in the diagnostic buffer; the screen alone does not show or verify that payload.

Record a diagnostic value in RAM: 001 is the number of trace entries. HP=42 is stored in the diagnostic buffer; the screen alone does not show or verify that payload.
Display example: 001 is the number of trace entries. HP=42 is stored in the diagnostic buffer; the screen alone does not show or verify that payload.

Download source / Verification status

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

// Record HP = 42 with frame tag 1 and display the number of trace entries.
// The trace buffer contains the value; the screen displays the count, not HP.
void main(void) {
    m_init();
    m_text(2,3,"DEBUG");
    debug_init(); debug_set_frame(1); debug_trace_u8("HP",42);
    m_number(debug_get_trace_count());
    while (1) { m_wait();  }
}
Store a position history fc_chain.c

Purpose: Learn how to store a trail of positions in a ring buffer.

How it works: chain_init attaches an array with room for four points. chain_push_head then records (20,30).

Controls and expected result: 042 is a fixed completion marker, not a coordinate or count. Inspect trail and points to check the stored history. This program does not draw a following body.

Store a position history: 042 is a fixed completion marker, not a coordinate or count. Inspect trail and points to check the stored history. This program does not draw a following body.
Display example: 042 is a fixed completion marker, not a coordinate or count. Inspect trail and points to check the stored history. This program does not draw a following body.

Download source / Verification status

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

Purpose: See how counted frame waits affect the library frame counter.

How it works: After system_init, the program calls system_wait_vblank twice and displays system_get_frame8.

Controls and expected result: The result is 002. The idle loop uses the manual helper m_wait, so the displayed value does not keep increasing.

Read the library frame counter: The result is 002. The idle loop uses the manual helper m_wait, so the displayed value does not keep increasing.
Display example: The result is 002. The idle loop uses the manual helper m_wait, so the displayed value does not keep increasing.

Download source / Verification status

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

// Reset the library frame count, perform two counted waits, and display 2.
// Subsequent m_wait calls leave that library counter unchanged.
void main(void) {
    m_init();
    m_text(2,3,"SYSTEM");
    system_init(); system_wait_vblank(); system_wait_vblank();
    m_number(system_get_frame8());
    while (1) { m_wait();  }
}
Select the current scene ID fc_scene.c

Purpose: Learn to select an ID that can distinguish scenes such as a title screen and gameplay.

How it works: The program registers two zero-initialized SceneDef entries and selects ID 1 with scene_change.

Controls and expected result: 001 is the current ID returned by scene_get_current. No scene callbacks or transition drawing are registered in this example.

Select the current scene ID: 001 is the current ID returned by scene_get_current. No scene callbacks or transition drawing are registered in this example.
Display example: 001 is the current ID returned by scene_get_current. No scene callbacks or transition drawing are registered in this example.

Download source / Verification status

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

Purpose: Keep subpixel motion as a remainder and carry whole pixels into the position.

How it works: Starting at X=40, the loop adds 2/8 pixel eight times. A right shift extracts whole pixels; a mask retains the remainder.

Controls and expected result: The total motion is two pixels, so the screen shows X=42 as 042. This manual integration example handles positive motion; negative motion needs a matching borrow path.

Accumulate fractional Q5.3 motion: The total motion is two pixels, so the screen shows X=42 as 042. This manual integration example handles positive motion; negative motion needs a matching borrow path.
Display example: The total motion is two pixels, so the screen shows X=42 as 042. This manual integration example handles positive motion; negative motion needs a matching borrow path.

Download source / Verification status

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

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

Purpose: Move the background by updating the PPU scroll position.

How it works: Starting at __scroll_set(0,0), the program increments phase once per frame and uses it as the horizontal position.

Controls and expected result: The word SCROLL changes horizontal position. phase wraps from 255 to 0. This is not a wide-map example spanning multiple nametables.

Scroll the NES background: The word SCROLL changes horizontal position. phase wraps from 255 to 0. This is not a wide-map example spanning multiple nametables.
Display example: The word SCROLL changes horizontal position. phase wraps from 255 to 0. This is not a wide-map example spanning multiple nametables.

Download source / Verification status

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

u8 phase;
// Advance an 8-bit horizontal scroll position once per frame, wrapping naturally
// after 255. Background motion demonstrates the changing scroll register.
void main(void) {
    m_init();
    m_text(2,3,"SCROLL");
    __scroll_set(0,0);
    while (1) { m_wait(); phase++; __scroll_set(phase,0); }
}
Apply a tile update through NMI fc_vram_queue.c

Purpose: Submit a PPU update from the main program for the NMI path to apply.

How it works: __vramq_put queues tile 52 at nametable address 0x2103, and __vramq_commit submits it. That address corresponds to tile coordinate (3,8).

Controls and expected result: A single 4 appears at background coordinate (3,8). Tile 52 is the digit 4 in the supplied font.

Apply a tile update through NMI: A single 4 appears at background coordinate (3,8). Tile 52 is the digit 4 in the supplied font.
Display example: A single 4 appears at background coordinate (3,8). Tile 52 is the digit 4 in the supplied font.

Download source / Verification status

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


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

Purpose: Prepare sprite colors and OAM data to draw an image independently of the background.

How it works: The program loads the sprite palette, sets slot 0 to position (70,80), tile 65 and attributes 0, enables rendering and repeats OAM DMA.

Controls and expected result: A stationary letter A appears as a sprite. The example demonstrates OAM setup and transfer, not animation.

Display an NES OAM sprite: A stationary letter A appears as a sprite. The example demonstrates OAM setup and transfer, not animation.
Display example: A stationary letter A appears as a sprite. The example demonstrates OAM setup and transfer, not animation.

Download source / Verification status

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


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

Purpose: Set the built-in NES pulse channel control, timer and length values.

How it works: After nes_apu_init, the program calls nes_sfx_square1(0xBF,400,20). Control 0xBF selects constant volume and halts the length counter, so the tone sustains. The value 20 is a length-table index, not 20 frames.

Controls and expected result: A sustained pulse tone plays while 042 is displayed. Call nes_apu_silence_all to stop it. The displayed marker alone does not verify sound output.

Play an APU pulse tone: A sustained pulse tone plays while 042 is displayed. Call nes_apu_silence_all to stop it. The displayed marker alone does not verify sound output.
Display example: A sustained pulse tone plays while 042 is displayed. Call nes_apu_silence_all to stop it. The displayed marker alone does not verify sound output.

Download source / Verification status

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

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

Compiler intrinsic dictionary

161 entries. Each entry explains the operation, arguments, return value, constraints, usage example and expected result.

audio_vblank.h — Music playback through the VBlank IRQ

Four-channel music advances in NMI. Initialize, attach and prefill a song, enable NMI for playback, then refill from the foreground. NMI reads a RAM queue, letting a small amount of RAM serve a long stream as long as it is refilled. Each channel has independent HOLD/STOP commands.

A record contains five bytes: delay, CH1, CH2, CH3, CH4. Delay 1..255 is the number of NMI ticks for which the new state lasts. Zero terminates playback and ignores the other four bytes. Tonal notes 0..71 are C2..B7 (24=C4). Noise values 0..15 select a long-mode period, and 16..31 select short mode. For any channel, 255=NES_AUDIO_HOLD leaves it unchanged; 254=NES_AUDIO_STOP silences it.

This four-channel driver is tuned for NTSC: CH1=pulse 1, CH2=pulse 2, CH3=triangle, CH4=noise, indexed 0..3. It does not play DMC or expansion audio. Do not run it alongside direct APU writers such as the effects in audio.c. There is no PAL pitch table or tempo correction.

When the delay expires without another BGM record, stop the BGM notes and resume at an NMI after data arrives. An independent SFX continues. Count entries into starvation, not every empty frame. Enqueueing only HOLD values does not restore the old BGM notes. Normally refill each frame and queue enough playing time before lengthy work.

fds.h — FDS disk operations

__fds_available

Report whether the compilation target is FDS, for example when displaying target-specific information in shared game code.

Syntax

c
u8 __fds_available(void);

Return value

1 for an FDS build, otherwise 0. This is not runtime peripheral detection.

Conditions and limits

This query is valid for cartridge targets such as NROM. Placing an FDS-only API inside if (__fds_available()) does not exempt that API from target checks; arrange target-specific source accordingly.

The capture uses KUROSAKI’s direct loading of disk boot files into RAM. This sample calls no BIOS routine and does not verify file transfers, disk swapping or boot on hardware. The empty test firmware is not a replacement BIOS. The sample uses NMI for display updates and disables maskable IRQs, which these queries do not need.

The build example produces a native-layout .fds without the extra approval-screen boot files. This configuration is for checking KUROSAKI direct boot; a hardware disk’s boot procedure needs separate validation.

Usage example

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

This is a calling fragment. Follow the stated initialization and buffer requirements before using it in a complete program.

Expected result

The FDS build displays 001; the NROM build displays 000. The same C source returns a different value according to the compilation target, independently of whether a disk is inserted.

__fds_available: AVAILABLE-FDS-DEFAULT — The FDS build displays 001; the NROM build displays 000. The same C source returns a different value according to the compilation target, independently of whether a disk is inserted.
AVAILABLE-FDS-DEFAULT — The FDS build displays 001; the NROM build displays 000. The same C source returns a different value according to the compilation target, independently of whether a disk is inserted.
__fds_available: AVAILABLE-NROM-DEFAULT — The FDS build displays 001; the NROM build displays 000. The same C source returns a different value according to the compilation target, independently of whether a disk is inserted.
AVAILABLE-NROM-DEFAULT — The FDS build displays 001; the NROM build displays 000. The same C source returns a different value according to the compilation target, independently of whether a disk is inserted.

Complete example program

Build command

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

kitaqfc/lib/fds.h:7

Declaration notes (original source)
Return the compile-time FDS mapper selection as 0 or 1; this is not a hardware probe.

kitaqfc/kitaqfc/CodeGenerator.cs:5146

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

Check disk presence immediately, allowing an insertion prompt to poll once per frame without blocking.

Syntax

c
u8 __fds_disk_ready(void);

Return value

Returns 1 when $4032 bit 0 is clear and 0 when it is set.

Conditions and limits

Build with --mapper=fds. Calling an FDS-only function other than __fds_available for a cartridge mapper is rejected with KQFC2420.

The test uses $4032 bit 0, which reports disk presence. It does not check bit 1’s drive readiness, file existence or load completion. The word “ready” here must not be taken as permission to start a transfer.

KUROSAKI uses a partial FDS controller model. This example observes presence 1, protection mask 0 and status 0; the capture does not establish behavior for ejection, protected media or CRC errors.

The capture uses KUROSAKI’s direct loading of disk boot files into RAM. This sample calls no BIOS routine and does not verify file transfers, disk swapping or boot on hardware. The empty test firmware is not a replacement BIOS. The sample uses NMI for display updates and disables maskable IRQs, which these queries do not need.

The build example produces a native-layout .fds without the extra approval-screen boot files. This configuration is for checking KUROSAKI direct boot; a hardware disk’s boot procedure needs separate validation.

NESdev: FDS hardware registers

Usage example

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

This is a calling fragment. Follow the stated initialization and buffer requirements before using it in a complete program.

Expected result

Under PRESENT / PROTECT / STATUS the screen shows 1 / 0 / 0, followed by 2 under COMPLETED PRESENCE WAITS. Both presence waits returned; this is not a file-transfer success indicator. NO FILE TRANSFER TESTED is shown below.

__fds_disk_ready: STATUS-FDS-DEFAULT — Under PRESENT / PROTECT / STATUS the screen shows 1 / 0 / 0, followed by 2 under COMPLETED PRESENCE WAITS. Both presence waits returned; this is not a file-transfer success indicator. NO FILE TRANSFER TESTED is shown below.
STATUS-FDS-DEFAULT — Under PRESENT / PROTECT / STATUS the screen shows 1 / 0 / 0, followed by 2 under COMPLETED PRESENCE WAITS. Both presence waits returned; this is not a file-transfer success indicator. NO FILE TRANSFER TESTED is shown below.

Complete example program

Build command

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

kitaqfc/lib/fds.h:9

Declaration notes (original source)
Return the inverted disk-absent bit from 4032 as 0 or 1; this checks presence, not transfer completion.

kitaqfc/kitaqfc/CodeGenerator.cs:5160

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

Read the FDS hardware status register at $4030. This is not a stored error from the last disk operation.

Syntax

c
u8 __fds_error(void);

Return value

Returns the entire eight-bit $4030 value. Preserve the return value of __fds_load_file() or __fds_save_file() when handling BIOS error codes.

Conditions and limits

Build with --mapper=fds. Calling an FDS-only function other than __fds_available for a cartridge mapper is rejected with KQFC2420.

KUROSAKI uses a partial FDS controller model. This example observes presence 1, protection mask 0 and status 0; the capture does not establish behavior for ejection, protected media or CRC errors.

Reading status also acknowledges interrupt requests. Do not poll indiscriminately if another handler relies on the same status. KUROSAKI clears its pending FDS IRQ flag on this read.

The capture uses KUROSAKI’s direct loading of disk boot files into RAM. This sample calls no BIOS routine and does not verify file transfers, disk swapping or boot on hardware. The empty test firmware is not a replacement BIOS. The sample uses NMI for display updates and disables maskable IRQs, which these queries do not need.

The build example produces a native-layout .fds without the extra approval-screen boot files. This configuration is for checking KUROSAKI direct boot; a hardware disk’s boot procedure needs separate validation.

NESdev: FDS hardware registers

Usage example

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

This is a calling fragment. Follow the stated initialization and buffer requirements before using it in a complete program.

Expected result

Under PRESENT / PROTECT / STATUS the screen shows 1 / 0 / 0, followed by 2 under COMPLETED PRESENCE WAITS. Both presence waits returned; this is not a file-transfer success indicator. NO FILE TRANSFER TESTED is shown below.

__fds_error: STATUS-FDS-DEFAULT — Under PRESENT / PROTECT / STATUS the screen shows 1 / 0 / 0, followed by 2 under COMPLETED PRESENCE WAITS. Both presence waits returned; this is not a file-transfer success indicator. NO FILE TRANSFER TESTED is shown below.
STATUS-FDS-DEFAULT — Under PRESENT / PROTECT / STATUS the screen shows 1 / 0 / 0, followed by 2 under COMPLETED PRESENCE WAITS. Both presence waits returned; this is not a file-transfer success indicator. NO FILE TRANSFER TESTED is shown below.

Complete example program

Build command

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

kitaqfc/lib/fds.h:13

Declaration notes (original source)
Read the entire 4030 status register; this is not a stored BIOS error code.

kitaqfc/kitaqfc/CodeGenerator.cs:5173

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

Return the masked write-protection bit from $4032. This function does not identify disk side A or B.

Syntax

c
u8 __fds_side(void);

Return value

Returns $4032 & 0x04: either 0 or 4, not a normalized Boolean. Hardware bit 2 indicates write protection or an ejected disk.

Conditions and limits

Build with --mapper=fds. Calling an FDS-only function other than __fds_available for a cartridge mapper is rejected with KQFC2420.

KUROSAKI uses a partial FDS controller model. This example observes presence 1, protection mask 0 and status 0; the capture does not establish behavior for ejection, protected media or CRC errors.

For a protection test, use (__fds_side() != 0). Disk presence and the BIOS operation’s return value are also needed to decide whether saving succeeds.

The capture uses KUROSAKI’s direct loading of disk boot files into RAM. This sample calls no BIOS routine and does not verify file transfers, disk swapping or boot on hardware. The empty test firmware is not a replacement BIOS. The sample uses NMI for display updates and disables maskable IRQs, which these queries do not need.

The build example produces a native-layout .fds without the extra approval-screen boot files. This configuration is for checking KUROSAKI direct boot; a hardware disk’s boot procedure needs separate validation.

NESdev: FDS hardware registers

Usage example

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

This is a calling fragment. Follow the stated initialization and buffer requirements before using it in a complete program.

Expected result

Under PRESENT / PROTECT / STATUS the screen shows 1 / 0 / 0, followed by 2 under COMPLETED PRESENCE WAITS. Both presence waits returned; this is not a file-transfer success indicator. NO FILE TRANSFER TESTED is shown below.

__fds_side: STATUS-FDS-DEFAULT — Under PRESENT / PROTECT / STATUS the screen shows 1 / 0 / 0, followed by 2 under COMPLETED PRESENCE WAITS. Both presence waits returned; this is not a file-transfer success indicator. NO FILE TRANSFER TESTED is shown below.
STATUS-FDS-DEFAULT — Under PRESENT / PROTECT / STATUS the screen shows 1 / 0 / 0, followed by 2 under COMPLETED PRESENCE WAITS. Both presence waits returned; this is not a file-transfer success indicator. NO FILE TRANSFER TESTED is shown below.

Complete example program

Build command

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

kitaqfc/lib/fds.h:11

Declaration notes (original source)
Return raw status mask 4032 & 04, either 0 or 4. This is not a numbered disk-side identifier.

kitaqfc/kitaqfc/CodeGenerator.cs:5167

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

Express a disk-insertion wait. Calls the same presence-polling loop as __fds_wait_ready().

Syntax

c
void __fds_wait_insert(void);

Return value

No return value.

Conditions and limits

Build with --mapper=fds. Calling an FDS-only function other than __fds_available for a cartridge mapper is rejected with KQFC2420.

The test uses $4032 bit 0, which reports disk presence. It does not check bit 1’s drive readiness, file existence or load completion. The word “ready” here must not be taken as permission to start a transfer.

There is no timeout. An absent disk leaves the call blocked. Poll __fds_disk_ready() once per frame if the game must keep updating its display or accepting input. The sample checks presence before entering either wait. An already-inserted disk satisfies the wait without a new removal/insertion cycle.

KUROSAKI uses a partial FDS controller model. This example observes presence 1, protection mask 0 and status 0; the capture does not establish behavior for ejection, protected media or CRC errors.

The capture uses KUROSAKI’s direct loading of disk boot files into RAM. This sample calls no BIOS routine and does not verify file transfers, disk swapping or boot on hardware. The empty test firmware is not a replacement BIOS. The sample uses NMI for display updates and disables maskable IRQs, which these queries do not need.

The build example produces a native-layout .fds without the extra approval-screen boot files. This configuration is for checking KUROSAKI direct boot; a hardware disk’s boot procedure needs separate validation.

NESdev: FDS hardware registers

Usage example

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

This is a calling fragment. Follow the stated initialization and buffer requirements before using it in a complete program.

Expected result

Under PRESENT / PROTECT / STATUS the screen shows 1 / 0 / 0, followed by 2 under COMPLETED PRESENCE WAITS. Both presence waits returned; this is not a file-transfer success indicator. NO FILE TRANSFER TESTED is shown below.

__fds_wait_insert: STATUS-FDS-DEFAULT — Under PRESENT / PROTECT / STATUS the screen shows 1 / 0 / 0, followed by 2 under COMPLETED PRESENCE WAITS. Both presence waits returned; this is not a file-transfer success indicator. NO FILE TRANSFER TESTED is shown below.
STATUS-FDS-DEFAULT — Under PRESENT / PROTECT / STATUS the screen shows 1 / 0 / 0, followed by 2 under COMPLETED PRESENCE WAITS. Both presence waits returned; this is not a file-transfer success indicator. NO FILE TRANSFER TESTED is shown below.

Complete example program

Build command

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

kitaqfc/lib/fds.h:17

Declaration notes (original source)
Alias the same disk-presence wait as __fds_wait_ready; the call can block indefinitely.

kitaqfc/kitaqfc/CodeGenerator.cs:9148

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

Wait in a CPU polling loop until a disk is present.

Syntax

c
void __fds_wait_ready(void);

Return value

No return value.

Conditions and limits

Build with --mapper=fds. Calling an FDS-only function other than __fds_available for a cartridge mapper is rejected with KQFC2420.

The test uses $4032 bit 0, which reports disk presence. It does not check bit 1’s drive readiness, file existence or load completion. The word “ready” here must not be taken as permission to start a transfer.

There is no timeout. An absent disk leaves the call blocked. Poll __fds_disk_ready() once per frame if the game must keep updating its display or accepting input. The sample checks presence before entering either wait. An already-inserted disk satisfies the wait without a new removal/insertion cycle.

KUROSAKI uses a partial FDS controller model. This example observes presence 1, protection mask 0 and status 0; the capture does not establish behavior for ejection, protected media or CRC errors.

The capture uses KUROSAKI’s direct loading of disk boot files into RAM. This sample calls no BIOS routine and does not verify file transfers, disk swapping or boot on hardware. The empty test firmware is not a replacement BIOS. The sample uses NMI for display updates and disables maskable IRQs, which these queries do not need.

The build example produces a native-layout .fds without the extra approval-screen boot files. This configuration is for checking KUROSAKI direct boot; a hardware disk’s boot procedure needs separate validation.

NESdev: FDS hardware registers

Usage example

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

This is a calling fragment. Follow the stated initialization and buffer requirements before using it in a complete program.

Expected result

Under PRESENT / PROTECT / STATUS the screen shows 1 / 0 / 0, followed by 2 under COMPLETED PRESENCE WAITS. Both presence waits returned; this is not a file-transfer success indicator. NO FILE TRANSFER TESTED is shown below.

__fds_wait_ready: STATUS-FDS-DEFAULT — Under PRESENT / PROTECT / STATUS the screen shows 1 / 0 / 0, followed by 2 under COMPLETED PRESENCE WAITS. Both presence waits returned; this is not a file-transfer success indicator. NO FILE TRANSFER TESTED is shown below.
STATUS-FDS-DEFAULT — Under PRESENT / PROTECT / STATUS the screen shows 1 / 0 / 0, followed by 2 under COMPLETED PRESENCE WAITS. Both presence waits returned; this is not a file-transfer success indicator. NO FILE TRANSFER TESTED is shown below.

Complete example program

Build command

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

kitaqfc/lib/fds.h:15

Declaration notes (original source)
Poll disk presence until the absent bit clears. There is no timeout or cancellation path.

kitaqfc/kitaqfc/CodeGenerator.cs:9148

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

fds_file.h — Loading FDS files

__fds_file_exists

Check whether the compiled file metadata contains the requested ID. A zero-size record still counts as present.

Syntax

c
u8 __fds_file_exists(u8 id);

Parameters

id

File ID to look up. Metadata uses IDs 0..254; 255 is reserved as the terminator. An ID is distinct from a file number, filename or destination address.

Return value

1 when the compiled table contains the ID, otherwise 0. This does not guarantee that the inserted disk contains that file.

Conditions and limits

Build with --mapper=fds. Calling an FDS-only function other than __fds_available for a cartridge mapper is rejected with KQFC2420.

Searches the compiled table from --fds-meta=... or --fds-manifest=..., together with generated overlay records. It does not read a disk directory. A file without a table record is not found even if it is packaged on disk. Manual and generated records together are limited to 42.

The size is the metadata’s declared value. It need not equal the inferred length of a source file, a trimmed overlay’s payload length or the contents of a swapped disk. Declare accurate sizes when using these queries to bound data access.

Keep fds_metadata.json and its two .bin files beside the C source. ID 16 contains an original 513-byte sequence: 0..255 twice, then zero. ID 17 has an empty payload. Both are non-boot PRG files (type 0), and this lesson does not load them. Relative source paths are resolved from the manifest’s directory.

The capture uses KUROSAKI’s direct loading of disk boot files into RAM. This sample calls no BIOS routine and does not verify file transfers, disk swapping or boot on hardware. The empty test firmware is not a replacement BIOS. The sample uses NMI for display updates and disables maskable IRQs, which these queries do not need.

The build example produces a native-layout .fds without the extra approval-screen boot files. This configuration is for checking KUROSAKI direct boot; a hardware disk’s boot procedure needs separate validation.

Usage example

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

This is a calling fragment. Follow the stated initialization and buffer requirements before using it in a complete program.

Expected result

The table shows ID 16 as present with 513 bytes, ID 17 as present with zero bytes, and ID 18 as absent with zero bytes. TABLE VALUES MATCH: PASS compares the retrieved values with this combination. A size of zero alone cannot distinguish an empty record from a missing ID.

__fds_file_exists: METADATA-FDS-DEFAULT — The table shows ID 16 as present with 513 bytes, ID 17 as present with zero bytes, and ID 18 as absent with zero bytes. TABLE VALUES MATCH: PASS compares the retrieved values with this combination. A size of zero alone cannot distinguish an empty record from a missing ID.
METADATA-FDS-DEFAULT — The table shows ID 16 as present with 513 bytes, ID 17 as present with zero bytes, and ID 18 as absent with zero bytes. TABLE VALUES MATCH: PASS compares the retrieved values with this combination. A size of zero alone cannot distinguish an empty record from a missing ID.

Complete example program

Build command

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

kitaqfc/lib/fds_file.h:18

Declaration notes (original source)
Query the compiled --fds-meta/--fds-manifest table, not the inserted disk; return 0 or 1.

kitaqfc/kitaqfc/CodeGenerator.cs:9481

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

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

Return the byte size declared for an ID in compiled metadata, for example when inspecting an asset’s planned load size.

Syntax

c
u16 __fds_file_size(u8 id);

Parameters

id

File ID to look up. Metadata uses IDs 0..254; 255 is reserved as the terminator. An ID is distinct from a file number, filename or destination address.

Return value

The declared unsigned 16-bit size, 0..65535. Missing IDs also return zero; use __fds_file_exists() when that distinction matters.

Conditions and limits

Build with --mapper=fds. Calling an FDS-only function other than __fds_available for a cartridge mapper is rejected with KQFC2420.

Searches the compiled table from --fds-meta=... or --fds-manifest=..., together with generated overlay records. It does not read a disk directory. A file without a table record is not found even if it is packaged on disk. Manual and generated records together are limited to 42.

The size is the metadata’s declared value. It need not equal the inferred length of a source file, a trimmed overlay’s payload length or the contents of a swapped disk. Declare accurate sizes when using these queries to bound data access.

Keep fds_metadata.json and its two .bin files beside the C source. ID 16 contains an original 513-byte sequence: 0..255 twice, then zero. ID 17 has an empty payload. Both are non-boot PRG files (type 0), and this lesson does not load them. Relative source paths are resolved from the manifest’s directory.

The capture uses KUROSAKI’s direct loading of disk boot files into RAM. This sample calls no BIOS routine and does not verify file transfers, disk swapping or boot on hardware. The empty test firmware is not a replacement BIOS. The sample uses NMI for display updates and disables maskable IRQs, which these queries do not need.

The build example produces a native-layout .fds without the extra approval-screen boot files. This configuration is for checking KUROSAKI direct boot; a hardware disk’s boot procedure needs separate validation.

Usage example

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

This is a calling fragment. Follow the stated initialization and buffer requirements before using it in a complete program.

Expected result

The table shows ID 16 as present with 513 bytes, ID 17 as present with zero bytes, and ID 18 as absent with zero bytes. TABLE VALUES MATCH: PASS compares the retrieved values with this combination. A size of zero alone cannot distinguish an empty record from a missing ID.

__fds_file_size: METADATA-FDS-DEFAULT — The table shows ID 16 as present with 513 bytes, ID 17 as present with zero bytes, and ID 18 as absent with zero bytes. TABLE VALUES MATCH: PASS compares the retrieved values with this combination. A size of zero alone cannot distinguish an empty record from a missing ID.
METADATA-FDS-DEFAULT — The table shows ID 16 as present with 513 bytes, ID 17 as present with zero bytes, and ID 18 as absent with zero bytes. TABLE VALUES MATCH: PASS compares the retrieved values with this combination. A size of zero alone cannot distinguish an empty record from a missing ID.

Complete example program

Build command

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

kitaqfc/lib/fds_file.h:20

Declaration notes (original source)
Return the compiled metadata size, or zero if id is absent. No disk directory is read.

kitaqfc/kitaqfc/CodeGenerator.cs:9485

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

Read a file through the BIOS and copy its PRG data into a CPU buffer. Use it to bring save data or level assets into memory reserved by the game.

Syntax

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

Parameters

id

ID of a non-boot file included in the generated disk, 0..254; the sample uses 73. This is distinct from the file’s physical position. ID 255, absent IDs and boot files are rejected.

dst

Destination for a PRG file, with space for its complete packaged length. Pass zero to leave the data at its header-defined destination without copying. For CHR/nametable PPU files, only zero is accepted; the BIOS transfers to the native PPU destination.

Return value

Zero on success. Nonzero BIOS errors are returned unchanged. A nominally successful BIOS call that did not load exactly one file returns 64 ($40). Invalid IDs, buffers or file-type/destination combinations return 255 ($FF).

Conditions and limits

Use with --mapper=fds --fds-layout=fds32. The --fds-meta manifest defines file IDs, load addresses, types, boot status and ordering. Transfer addresses and sizes come from the disk image produced by the build; this API does not inspect an arbitrary inserted disk.

The BIOS first loads into the address stored in the disk header, then this helper copies to dst. It selects the copy direction to preserve overlapping data, but the staging area must also be reserved. A failed transfer does not copy to dst. After a BIOS call, bank residency is marked 255 (unknown).

Each CPU buffer must fit entirely within either $0200–$07FF or $6000–$DFFF. Wrapping spans and I/O-register destinations are rejected. The caller must still keep these buffers clear of code, OAM, other variables and compiler scratch storage. The sample uses bank 0 for common code, $8000 for staging, and four-byte buffers at $8500 and $8600.

These are synchronous BIOS calls. Call from common code so the transfer cannot overwrite the executing code or its return address; do not call from an interrupt handler. Satisfy BIOS prerequisites during disk I/O: stop rendering and NMI, use a PPU address increment of one, and disable non-disk IRQ sources. Continued gameplay or music during the transfer is not guaranteed.

The capture uses an original BIOS calling-convention fixture to check compiled CPU code, buffer contents, save/readback behavior and every display pixel. It does not verify the real BIOS, physical disk writes, CRC or recovery after power loss. Ordinary FDS execution in KUROSAKI alone does not establish the same save behavior.

Use the sample together with fds_file_io_manifest.json and fds_file_io_payload.bin from the same folder. The checker below generates a temporary fixture from original source and removes it afterward. No commercial BIOS is included, and this fixture is not a BIOS for normal games.

FDS BIOS calling convention

Usage example

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

This is a calling fragment. Follow the stated initialization and buffer requirements before using it in a complete program.

Expected result

LOAD, SAVE and RELOAD STATUS and BYTE CHECK ERRORS all show 000. INITIAL FIRST/LAST shows 011 and 044, the endpoints of the four bytes loaded into $8500. The sample changes them to 7, 14, 21 and 28, saves them, then reloads into $8600, so SAVED FIRST / LAST shows 007 and 028. STAGING 8000 / SIZE 4 identifies the disk-side load address and file length. The independent test also checks that ID 73 is saved at the actual fourth file, zero-based position 3.

__fds_load_file: DEFAULT — LOAD, SAVE and RELOAD STATUS and BYTE CHECK ERRORS all show 000. INITIAL FIRST/LAST shows 011 and 044, the endpoints of the four bytes loaded into $8500. The sample changes them to 7, 14, 21 and 28, saves them, then reloads into $8600, so SAVED FIRST / LAST shows 007 and 028. STAGING 8000 / SIZE 4 identifies the disk-side load address and file length. The independent test also checks that ID 73 is saved at the actual fourth file, zero-based position 3.
DEFAULT — LOAD, SAVE and RELOAD STATUS and BYTE CHECK ERRORS all show 000. INITIAL FIRST/LAST shows 011 and 044, the endpoints of the four bytes loaded into $8500. The sample changes them to 7, 14, 21 and 28, saves them, then reloads into $8600, so SAVED FIRST / LAST shows 007 and 028. STAGING 8000 / SIZE 4 identifies the disk-side load address and file length. The independent test also checks that ID 73 is saved at the actual fourth file, zero-based position 3.

Complete example program

fds_file_io_manifest.json

fds_file_io_payload.bin

Build command

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

kitaqfc/lib/fds_file.h:11

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

kitaqfc/kitaqfc/CodeGenerator.cs:9223

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

Overwrite a fixed-size PRG save slot reserved at the end of a disk side with bytes from a CPU buffer. The file keeps its ID, name and future load address.

Syntax

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

Parameters

id

ID of a non-boot file included in the generated disk, 0..254; the sample uses 73. This is distinct from the file’s physical position. ID 255, absent IDs and boot files are rejected.

src

Start of the data in CPU RAM, readable for len bytes. This API does not save VRAM directly.

len

Number of bytes to save; it must equal the packaged file length. If the game has less data, fill the unused part of its save buffer before calling.

Return value

Zero on success, otherwise the BIOS error such as write protection or write-verification failure. Returns 255 ($FF) without calling the BIOS for an invalid ID, a boot or non-PRG file, a file that is not last on its side, a length mismatch or an invalid CPU span.

Conditions and limits

Use with --mapper=fds --fds-layout=fds32. The --fds-meta manifest defines file IDs, load addresses, types, boot status and ordering. Transfer addresses and sizes come from the disk image produced by the build; this API does not inspect an arbitrary inserted disk.

BIOS WriteFile logically hides files following the write position. This API therefore accepts only the final non-boot PRG file on each side. It does not append a new file or resize a slot. The slot must be last in the final layout, including generated boot helpers and automatic overlays.

The compiler distinguishes file ID, the header number and physical order. The sample has ID 73 and manifest number 200, but passes position 3 to the BIOS. Disk and side numbers also come from the resolved layout. Do not use this with a disk whose layout has been changed independently of the manifest.

Each CPU buffer must fit entirely within either $0200–$07FF or $6000–$DFFF. Wrapping spans and I/O-register destinations are rejected. The caller must still keep these buffers clear of code, OAM, other variables and compiler scratch storage. The sample uses bank 0 for common code, $8000 for staging, and four-byte buffers at $8500 and $8600.

These are synchronous BIOS calls. Call from common code so the transfer cannot overwrite the executing code or its return address; do not call from an interrupt handler. Satisfy BIOS prerequisites during disk I/O: stop rendering and NMI, use a PPU address increment of one, and disable non-disk IRQ sources. Continued gameplay or music during the transfer is not guaranteed.

The capture uses an original BIOS calling-convention fixture to check compiled CPU code, buffer contents, save/readback behavior and every display pixel. It does not verify the real BIOS, physical disk writes, CRC or recovery after power loss. Ordinary FDS execution in KUROSAKI alone does not establish the same save behavior.

Use the sample together with fds_file_io_manifest.json and fds_file_io_payload.bin from the same folder. The checker below generates a temporary fixture from original source and removes it afterward. No commercial BIOS is included, and this fixture is not a BIOS for normal games.

FDS BIOS calling convention

Usage example

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

This is a calling fragment. Follow the stated initialization and buffer requirements before using it in a complete program.

Expected result

LOAD, SAVE and RELOAD STATUS and BYTE CHECK ERRORS all show 000. INITIAL FIRST/LAST shows 011 and 044, the endpoints of the four bytes loaded into $8500. The sample changes them to 7, 14, 21 and 28, saves them, then reloads into $8600, so SAVED FIRST / LAST shows 007 and 028. STAGING 8000 / SIZE 4 identifies the disk-side load address and file length. The independent test also checks that ID 73 is saved at the actual fourth file, zero-based position 3.

__fds_save_file: DEFAULT — LOAD, SAVE and RELOAD STATUS and BYTE CHECK ERRORS all show 000. INITIAL FIRST/LAST shows 011 and 044, the endpoints of the four bytes loaded into $8500. The sample changes them to 7, 14, 21 and 28, saves them, then reloads into $8600, so SAVED FIRST / LAST shows 007 and 028. STAGING 8000 / SIZE 4 identifies the disk-side load address and file length. The independent test also checks that ID 73 is saved at the actual fourth file, zero-based position 3.
DEFAULT — LOAD, SAVE and RELOAD STATUS and BYTE CHECK ERRORS all show 000. INITIAL FIRST/LAST shows 011 and 044, the endpoints of the four bytes loaded into $8500. The sample changes them to 7, 14, 21 and 28, saves them, then reloads into $8600, so SAVED FIRST / LAST shows 007 and 028. STAGING 8000 / SIZE 4 identifies the disk-side load address and file length. The independent test also checks that ID 73 is saved at the actual fourth file, zero-based position 3.

Complete example program

fds_file_io_manifest.json

fds_file_io_payload.bin

Build command

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

kitaqfc/lib/fds_file.h:16

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

kitaqfc/kitaqfc/CodeGenerator.cs:9488

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

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

fds_overlay.h — FDS overlay code

__fds_current_bank

Return the logical bank recorded as resident, without reading anything from disk.

Syntax

c
u8 __fds_current_bank(void);

Return value

The eight-bit software residency record; 1 immediately after standard startup, or 255 when unknown.

Conditions and limits

Build with --mapper=fds. Calling an FDS-only function other than __fds_available for a cartridge mapper is rejected with KQFC2420.

The standard FDS startup initializes the residency record to 1. __fds_load_bank() updates it after a successful load. Raw loads through __fds_load_file() or __fds_load_overlay(), and failed bank transfers, set the record to 255 (unknown). No memory contents are checked; do not rely on the record after modifying the window directly. A custom reset routine must manage initialization itself.

The capture uses KUROSAKI’s direct loading of disk boot files into RAM. This sample calls no BIOS routine and does not verify file transfers, disk swapping or boot on hardware. The empty test firmware is not a replacement BIOS. The sample uses NMI for display updates and disables maskable IRQs, which these queries do not need.

The build example produces a native-layout .fds without the extra approval-screen boot files. This configuration is for checking KUROSAKI direct boot; a hardware disk’s boot procedure needs separate validation.

Usage example

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

This is a calling fragment. Follow the stated initialization and buffer requirements before using it in a complete program.

Expected result

CURRENT BANK is 1, BANK 1 RESIDENT is 1, BANK 2 RESIDENT is 0 and OVERLAY FUNCTIONS is 2. Packaging two functions in bank 2 does not make that bank resident at startup. In the verification build with --fds-no-overlay-table, only the last value changes to zero.

__fds_current_bank: RESIDENCY-FDS-DEFAULT — CURRENT BANK is 1, BANK 1 RESIDENT is 1, BANK 2 RESIDENT is 0 and OVERLAY FUNCTIONS is 2. Packaging two functions in bank 2 does not make that bank resident at startup. In the verification build with --fds-no-overlay-table, only the last value changes to zero.
RESIDENCY-FDS-DEFAULT — CURRENT BANK is 1, BANK 1 RESIDENT is 1, BANK 2 RESIDENT is 0 and OVERLAY FUNCTIONS is 2. Packaging two functions in bank 2 does not make that bank resident at startup. In the verification build with --fds-no-overlay-table, only the last value changes to zero.
__fds_current_bank: RESIDENCY-FDS-TABLE-OFF-DEFAULT — CURRENT BANK is 1, BANK 1 RESIDENT is 1, BANK 2 RESIDENT is 0 and OVERLAY FUNCTIONS is 2. Packaging two functions in bank 2 does not make that bank resident at startup. In the verification build with --fds-no-overlay-table, only the last value changes to zero.
RESIDENCY-FDS-TABLE-OFF-DEFAULT — CURRENT BANK is 1, BANK 1 RESIDENT is 1, BANK 2 RESIDENT is 0 and OVERLAY FUNCTIONS is 2. Packaging two functions in bank 2 does not make that bank resident at startup. In the verification build with --fds-no-overlay-table, only the last value changes to zero.

Complete example program

Build command

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

kitaqfc/lib/fds_overlay.h:16

Declaration notes (original source)
Return the software resident-bank byte, or FF after a failed bank transfer.
This is not a checksum of overlay memory.

kitaqfc/kitaqfc/CodeGenerator.cs:5222

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

Make a parameterless far call using the FDS-specific spelling. Loading and restoration behave exactly as in __fds_overlay_farcall().

Syntax

c
u8 __fds_farcall(u8 bank, u16 func);

Parameters

bank

An expression representing the bank. It is evaluated once before the call, but the bank actually loaded comes from func’s linked placement. This does not dispatch an address into an arbitrary runtime-selected bank. Pass a matching value, such as __bankof(func).

func

Name of a declared function with no parameters. The sample passes u8 overlay_answer(void). Numeric addresses, runtime function pointers and functions requiring arguments are rejected. Use an ordinary target(a, b) call when arguments are needed.

Return value

The target function’s eight-bit result. If loading fails and the previous bank can be restored, the target is not called and the load error byte is returned instead. Design the result convention so an error can be distinguished from a normal result.

Conditions and limits

Use with --mapper=fds --fds-layout=fds32. Bank 0 is common code at $A000–$DFFF. Bank 1 occupies $6000–$9FFF at startup; banks 2 and above are overlays loaded into that same 16 KiB window. Place overlay code or data with directives such as #pragma fixed_bank 2.

Bank 1 is restored from a separate 16,384-byte boot PRG file at $6000. Without a manifest, the default files are ID 0 for bank 1, ID 1 for common code and ID 2 for CHR. A custom manifest must split the boot PRG into 16,384-byte files at $6000 and $A000. For banks 2 and above, the file ID is --fds-overlay-start-id (32 by default) + bank number - 2.

For a different-bank call, the previous bank is saved and restored before returning. Nested calls follow the same rule. Restoration is also attempted after a partially failed transfer. If restoration itself fails, execution stops in common code rather than returning to overwritten code. Common-bank targets and same-bank calls avoid unnecessary transfers.

BIOS calls are synchronous; this API does not keep the game loop advancing once per frame during a load. Use it from ordinary control code in the common bank, not from an interrupt handler. Display/audio continuity and coexistence with the real BIOS require separate verification.

These captures use an original LoadFiles calling-convention fixture to check compiled CPU code, memory transfers and every display pixel. They do not verify the real BIOS, disk-transfer timing, CRC or hardware boot. KUROSAKI has a partial FDS controller model; ordinary execution without this fixture does not establish the same transfer results.

The first compiler command below builds the downloadable sample. The Python checker builds all five lessons with and without optimization, then checks them in KUROSAKI. It creates and removes a temporary fixture from scripts/fds_abi_fixture.py; no external BIOS is needed for these checks. The fixture is not a BIOS for normal games.

Usage example

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

This is a calling fragment. Follow the stated initialization and buffer requirements before using it in a complete program.

Expected result

With the original LoadFiles ABI fixture, RETURN VALUE is 042; CALLBACK COUNT and BANK EXPR COUNT are both 001; BANK IN CALLBACK is 002; BANK AFTER RETURN is 001. The function and bank expression ran once each, and bank 1 was restored after calling bank 2. This is not a capture of a real disk-transfer test.

__fds_farcall: FARCALL-DEFAULT — With the original LoadFiles ABI fixture, RETURN VALUE is 042; CALLBACK COUNT and BANK EXPR COUNT are both 001; BANK IN CALLBACK is 002; BANK AFTER RETURN is 001. The function and bank expression ran once each, and bank 1 was restored after calling bank 2. This is not a capture of a real disk-transfer test.
FARCALL-DEFAULT — With the original LoadFiles ABI fixture, RETURN VALUE is 042; CALLBACK COUNT and BANK EXPR COUNT are both 001; BANK IN CALLBACK is 002; BANK AFTER RETURN is 001. The function and bank expression ran once each, and bank 1 was restored after calling bank 2. This is not a capture of a real disk-transfer test.

Complete example program

Build command

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

kitaqfc/lib/fds_overlay.h:25

Declaration notes (original source)
Same named, zero-argument dispatch as __fds_overlay_farcall; evaluate bank once.

kitaqfc/kitaqfc/CodeGenerator.cs:4074

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

Compare a requested bank with the residency record. It can help decide whether a load is needed, but does not perform one.

Syntax

c
u8 __fds_is_bank_resident(u8 bank);

Parameters

bank

Logical bank number to compare. Standard startup records 1; generated overlays use 2 and above. Bank 0 holds common code and is not the startup value of this residency record.

Return value

1 if a valid bank matches the record, otherwise 0. Bank 255 (unknown) always returns zero, even when the record is 255.

Conditions and limits

Build with --mapper=fds. Calling an FDS-only function other than __fds_available for a cartridge mapper is rejected with KQFC2420.

The standard FDS startup initializes the residency record to 1. __fds_load_bank() updates it after a successful load. Raw loads through __fds_load_file() or __fds_load_overlay(), and failed bank transfers, set the record to 255 (unknown). No memory contents are checked; do not rely on the record after modifying the window directly. A custom reset routine must manage initialization itself.

The capture uses KUROSAKI’s direct loading of disk boot files into RAM. This sample calls no BIOS routine and does not verify file transfers, disk swapping or boot on hardware. The empty test firmware is not a replacement BIOS. The sample uses NMI for display updates and disables maskable IRQs, which these queries do not need.

The build example produces a native-layout .fds without the extra approval-screen boot files. This configuration is for checking KUROSAKI direct boot; a hardware disk’s boot procedure needs separate validation.

Usage example

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

This is a calling fragment. Follow the stated initialization and buffer requirements before using it in a complete program.

Expected result

CURRENT BANK is 1, BANK 1 RESIDENT is 1, BANK 2 RESIDENT is 0 and OVERLAY FUNCTIONS is 2. Packaging two functions in bank 2 does not make that bank resident at startup. In the verification build with --fds-no-overlay-table, only the last value changes to zero.

__fds_is_bank_resident: RESIDENCY-FDS-DEFAULT — CURRENT BANK is 1, BANK 1 RESIDENT is 1, BANK 2 RESIDENT is 0 and OVERLAY FUNCTIONS is 2. Packaging two functions in bank 2 does not make that bank resident at startup. In the verification build with --fds-no-overlay-table, only the last value changes to zero.
RESIDENCY-FDS-DEFAULT — CURRENT BANK is 1, BANK 1 RESIDENT is 1, BANK 2 RESIDENT is 0 and OVERLAY FUNCTIONS is 2. Packaging two functions in bank 2 does not make that bank resident at startup. In the verification build with --fds-no-overlay-table, only the last value changes to zero.
__fds_is_bank_resident: RESIDENCY-FDS-TABLE-OFF-DEFAULT — CURRENT BANK is 1, BANK 1 RESIDENT is 1, BANK 2 RESIDENT is 0 and OVERLAY FUNCTIONS is 2. Packaging two functions in bank 2 does not make that bank resident at startup. In the verification build with --fds-no-overlay-table, only the last value changes to zero.
RESIDENCY-FDS-TABLE-OFF-DEFAULT — CURRENT BANK is 1, BANK 1 RESIDENT is 1, BANK 2 RESIDENT is 0 and OVERLAY FUNCTIONS is 2. Packaging two functions in bank 2 does not make that bank resident at startup. In the verification build with --fds-no-overlay-table, only the last value changes to zero.

Complete example program

Build command

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

kitaqfc/lib/fds_overlay.h:18

Declaration notes (original source)
Compare a valid bank with the software resident-bank byte; FF (unknown) never matches.

kitaqfc/kitaqfc/CodeGenerator.cs:9465

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

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

Translate a logical bank into its file ID and load the $6000–$9FFF overlay window, recording that bank as resident afterward.

Syntax

c
u8 __fds_load_bank(u8 bank);

Parameters

bank

Logical bank to load, 1..254. Bank 0 is common code, not a loadable window; 255 is reserved for unknown residency. For bank 2 and above, the calculated file ID must also fit in 0..254.

Return value

Zero on success. Returns 255 for bank 0 or 255, a file-ID overflow, or bank 1 without a restore file. A failed BIOS transfer returns its error byte. Argument failures before a transfer preserve the residency record; failed BIOS transfers mark it 255 (unknown).

Conditions and limits

Use with --mapper=fds --fds-layout=fds32. Bank 0 is common code at $A000–$DFFF. Bank 1 occupies $6000–$9FFF at startup; banks 2 and above are overlays loaded into that same 16 KiB window. Place overlay code or data with directives such as #pragma fixed_bank 2.

Bank 1 is restored from a separate 16,384-byte boot PRG file at $6000. Without a manifest, the default files are ID 0 for bank 1, ID 1 for common code and ID 2 for CHR. A custom manifest must split the boot PRG into 16,384-byte files at $6000 and $A000. For banks 2 and above, the file ID is --fds-overlay-start-id (32 by default) + bank number - 2.

With the default residency guard, an already-recorded bank returns zero without a disk read. --fds-no-overlay-guard disables this shortcut in __fds_load_bank(). A successful load records the requested bank. The record is not a validation of memory contents.

Loading a different bank directly from code in the $6000–$9FFF window can overwrite the executing function or its return address. This sample places the caller and display code in #pragma fixed_bank 0. To call a function in another bank and return, use __fds_overlay_farcall() or an ordinary typed function call.

BIOS calls are synchronous; this API does not keep the game loop advancing once per frame during a load. Use it from ordinary control code in the common bank, not from an interrupt handler. Display/audio continuity and coexistence with the real BIOS require separate verification.

These captures use an original LoadFiles calling-convention fixture to check compiled CPU code, memory transfers and every display pixel. They do not verify the real BIOS, disk-transfer timing, CRC or hardware boot. KUROSAKI has a partial FDS controller model; ordinary execution without this fixture does not establish the same transfer results.

The first compiler command below builds the downloadable sample. The Python checker builds all five lessons with and without optimization, then checks them in KUROSAKI. It creates and removes a temporary fixture from scripts/fds_abi_fixture.py; no external BIOS is needed for these checks. The fixture is not a BIOS for normal games.

Usage example

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

This is a calling fragment. Follow the stated initialization and buffer requirements before using it in a complete program.

Expected result

With the original LoadFiles ABI fixture, LOAD BANK 2 STATUS, REQUIRE SAME BANK and RESTORE BANK 1 are all 000. FIRST / LAST BYTE shows 011 and 044 from the transferred four-byte payload; RESIDENT BANK is 001 after restoration. Invalid banks 0 and 255 each return 255. Requiring the same bank issues no extra load: the checked file requests are 32 then 0.

__fds_load_bank: BANK_LOAD-DEFAULT — With the original LoadFiles ABI fixture, LOAD BANK 2 STATUS, REQUIRE SAME BANK and RESTORE BANK 1 are all 000. FIRST / LAST BYTE shows 011 and 044 from the transferred four-byte payload; RESIDENT BANK is 001 after restoration. Invalid banks 0 and 255 each return 255. Requiring the same bank issues no extra load: the checked file requests are 32 then 0.
BANK_LOAD-DEFAULT — With the original LoadFiles ABI fixture, LOAD BANK 2 STATUS, REQUIRE SAME BANK and RESTORE BANK 1 are all 000. FIRST / LAST BYTE shows 011 and 044 from the transferred four-byte payload; RESIDENT BANK is 001 after restoration. Invalid banks 0 and 255 each return 255. Requiring the same bank issues no extra load: the checked file requests are 32 then 0.

Complete example program

Build command

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

kitaqfc/lib/fds_overlay.h:11

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

kitaqfc/kitaqfc/CodeGenerator.cs:9394

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

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

Call BIOS LoadFiles using a disk file ID rather than a logical bank number. The file header determines the destination.

Syntax

c
u8 __fds_load_overlay(u8 id);

Parameters

id

Disk file ID, 0..254. Do not pass 255: it terminates the BIOS file list. In the default layout, ID 32 contains bank 2.

Return value

The BIOS error byte, with zero indicating success. After either success or failure, residency is marked 255 (unknown); a file ID alone does not establish a logical-bank mapping.

Conditions and limits

Use with --mapper=fds --fds-layout=fds32. Bank 0 is common code at $A000–$DFFF. Bank 1 occupies $6000–$9FFF at startup; banks 2 and above are overlays loaded into that same 16 KiB window. Place overlay code or data with directives such as #pragma fixed_bank 2.

Builds a list containing the requested ID and terminator 255. Check the manifest destination so the file cannot overwrite common or executing code. Prefer __fds_load_bank() for logical overlays; it manages residency.

Loading a different bank directly from code in the $6000–$9FFF window can overwrite the executing function or its return address. This sample places the caller and display code in #pragma fixed_bank 0. To call a function in another bank and return, use __fds_overlay_farcall() or an ordinary typed function call.

BIOS calls are synchronous; this API does not keep the game loop advancing once per frame during a load. Use it from ordinary control code in the common bank, not from an interrupt handler. Display/audio continuity and coexistence with the real BIOS require separate verification.

These captures use an original LoadFiles calling-convention fixture to check compiled CPU code, memory transfers and every display pixel. They do not verify the real BIOS, disk-transfer timing, CRC or hardware boot. KUROSAKI has a partial FDS controller model; ordinary execution without this fixture does not establish the same transfer results.

The first compiler command below builds the downloadable sample. The Python checker builds all five lessons with and without optimization, then checks them in KUROSAKI. It creates and removes a temporary fixture from scripts/fds_abi_fixture.py; no external BIOS is needed for these checks. The fixture is not a BIOS for normal games.

Usage example

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

This is a calling fragment. Follow the stated initialization and buffer requirements before using it in a complete program.

Expected result

With the original LoadFiles ABI fixture, FILE 32 LOAD STATUS is 000 and the transferred payload’s end bytes are 011 and 044. After the raw load, UNKNOWN BANK RECORD is 255 and IS BANK 1 RESIDENT is 000. REQUIRE BANK 1 succeeds with 000, then RESIDENT AFTER RELOAD is 001. This distinguishes loading by file ID from managing a logical bank.

__fds_load_overlay: RAW_OVERLAY-DEFAULT — With the original LoadFiles ABI fixture, FILE 32 LOAD STATUS is 000 and the transferred payload’s end bytes are 011 and 044. After the raw load, UNKNOWN BANK RECORD is 255 and IS BANK 1 RESIDENT is 000. REQUIRE BANK 1 succeeds with 000, then RESIDENT AFTER RELOAD is 001. This distinguishes loading by file ID from managing a logical bank.
RAW_OVERLAY-DEFAULT — With the original LoadFiles ABI fixture, FILE 32 LOAD STATUS is 000 and the transferred payload’s end bytes are 011 and 044. After the raw load, UNKNOWN BANK RECORD is 255 and IS BANK 1 RESIDENT is 000. REQUIRE BANK 1 succeeds with 000, then RESIDENT AFTER RELOAD is 001. This distinguishes loading by file ID from managing a logical bank.

Complete example program

Build command

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

kitaqfc/lib/fds_overlay.h:7

Declaration notes (original source)
Load disk file id through BIOS, return its error byte and mark bank residency FF (unknown).

kitaqfc/kitaqfc/CodeGenerator.cs:9375

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

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

Call a parameterless function in an overlay and restore the previous bank before returning.

Syntax

c
u8 __fds_overlay_farcall(u8 bank, u16 func);

Parameters

bank

An expression representing the bank. It is evaluated once before the call, but the bank actually loaded comes from func’s linked placement. This does not dispatch an address into an arbitrary runtime-selected bank. Pass a matching value, such as __bankof(func).

func

Name of a declared function with no parameters. The sample passes u8 overlay_answer(void). Numeric addresses, runtime function pointers and functions requiring arguments are rejected. Use an ordinary target(a, b) call when arguments are needed.

Return value

The target function’s eight-bit result. If loading fails and the previous bank can be restored, the target is not called and the load error byte is returned instead. Design the result convention so an error can be distinguished from a normal result.

Conditions and limits

Use with --mapper=fds --fds-layout=fds32. Bank 0 is common code at $A000–$DFFF. Bank 1 occupies $6000–$9FFF at startup; banks 2 and above are overlays loaded into that same 16 KiB window. Place overlay code or data with directives such as #pragma fixed_bank 2.

Bank 1 is restored from a separate 16,384-byte boot PRG file at $6000. Without a manifest, the default files are ID 0 for bank 1, ID 1 for common code and ID 2 for CHR. A custom manifest must split the boot PRG into 16,384-byte files at $6000 and $A000. For banks 2 and above, the file ID is --fds-overlay-start-id (32 by default) + bank number - 2.

For a different-bank call, the previous bank is saved and restored before returning. Nested calls follow the same rule. Restoration is also attempted after a partially failed transfer. If restoration itself fails, execution stops in common code rather than returning to overwritten code. Common-bank targets and same-bank calls avoid unnecessary transfers.

BIOS calls are synchronous; this API does not keep the game loop advancing once per frame during a load. Use it from ordinary control code in the common bank, not from an interrupt handler. Display/audio continuity and coexistence with the real BIOS require separate verification.

These captures use an original LoadFiles calling-convention fixture to check compiled CPU code, memory transfers and every display pixel. They do not verify the real BIOS, disk-transfer timing, CRC or hardware boot. KUROSAKI has a partial FDS controller model; ordinary execution without this fixture does not establish the same transfer results.

The first compiler command below builds the downloadable sample. The Python checker builds all five lessons with and without optimization, then checks them in KUROSAKI. It creates and removes a temporary fixture from scripts/fds_abi_fixture.py; no external BIOS is needed for these checks. The fixture is not a BIOS for normal games.

Usage example

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

This is a calling fragment. Follow the stated initialization and buffer requirements before using it in a complete program.

Expected result

With the original LoadFiles ABI fixture, RETURN VALUE is 042; CALLBACK COUNT and BANK EXPR COUNT are both 001; BANK IN CALLBACK is 002; BANK AFTER RETURN is 001. The function and bank expression ran once each, and bank 1 was restored after calling bank 2. This is not a capture of a real disk-transfer test.

__fds_overlay_farcall: OVERLAY_CALL-DEFAULT — With the original LoadFiles ABI fixture, RETURN VALUE is 042; CALLBACK COUNT and BANK EXPR COUNT are both 001; BANK IN CALLBACK is 002; BANK AFTER RETURN is 001. The function and bank expression ran once each, and bank 1 was restored after calling bank 2. This is not a capture of a real disk-transfer test.
OVERLAY_CALL-DEFAULT — With the original LoadFiles ABI fixture, RETURN VALUE is 042; CALLBACK COUNT and BANK EXPR COUNT are both 001; BANK IN CALLBACK is 002; BANK AFTER RETURN is 001. The function and bank expression ran once each, and bank 1 was restored after calling bank 2. This is not a capture of a real disk-transfer test.

Complete example program

Build command

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

kitaqfc/lib/fds_overlay.h:23

Declaration notes (original source)
Evaluate bank once and call the named zero-argument function using its linked bank.
Restore the caller window before returning; restoration failure stops in common code.

kitaqfc/kitaqfc/CodeGenerator.cs:4074

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

Return the number of functions placed in overlay banks 2 and above by the linker. This is neither a resident-function count nor a file count.

Syntax

c
u8 __fds_overlay_function_count(void);

Return value

The table’s leading count byte, 0..255. Zero when there are no overlay functions or --fds-no-overlay-table is used. More than 255 entries are rejected with KQFC2511.

Conditions and limits

Build with --mapper=fds. Calling an FDS-only function other than __fds_available for a cartridge mapper is rejected with KQFC2420.

Code generation creates a zero placeholder; native FDS linking replaces it with the table after bank/address placement. The sample packages two functions in bank 2 without calling them. The initial data in --fds-layout=legacy is not a census of overlay functions.

The capture uses KUROSAKI’s direct loading of disk boot files into RAM. This sample calls no BIOS routine and does not verify file transfers, disk swapping or boot on hardware. The empty test firmware is not a replacement BIOS. The sample uses NMI for display updates and disables maskable IRQs, which these queries do not need.

The build example produces a native-layout .fds without the extra approval-screen boot files. This configuration is for checking KUROSAKI direct boot; a hardware disk’s boot procedure needs separate validation.

Usage example

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

This is a calling fragment. Follow the stated initialization and buffer requirements before using it in a complete program.

Expected result

CURRENT BANK is 1, BANK 1 RESIDENT is 1, BANK 2 RESIDENT is 0 and OVERLAY FUNCTIONS is 2. Packaging two functions in bank 2 does not make that bank resident at startup. In the verification build with --fds-no-overlay-table, only the last value changes to zero.

__fds_overlay_function_count: RESIDENCY-FDS-DEFAULT — CURRENT BANK is 1, BANK 1 RESIDENT is 1, BANK 2 RESIDENT is 0 and OVERLAY FUNCTIONS is 2. Packaging two functions in bank 2 does not make that bank resident at startup. In the verification build with --fds-no-overlay-table, only the last value changes to zero.
RESIDENCY-FDS-DEFAULT — CURRENT BANK is 1, BANK 1 RESIDENT is 1, BANK 2 RESIDENT is 0 and OVERLAY FUNCTIONS is 2. Packaging two functions in bank 2 does not make that bank resident at startup. In the verification build with --fds-no-overlay-table, only the last value changes to zero.
__fds_overlay_function_count: RESIDENCY-FDS-TABLE-OFF-DEFAULT — CURRENT BANK is 1, BANK 1 RESIDENT is 1, BANK 2 RESIDENT is 0 and OVERLAY FUNCTIONS is 2. Packaging two functions in bank 2 does not make that bank resident at startup. In the verification build with --fds-no-overlay-table, only the last value changes to zero.
RESIDENCY-FDS-TABLE-OFF-DEFAULT — CURRENT BANK is 1, BANK 1 RESIDENT is 1, BANK 2 RESIDENT is 0 and OVERLAY FUNCTIONS is 2. Packaging two functions in bank 2 does not make that bank resident at startup. In the verification build with --fds-no-overlay-table, only the last value changes to zero.

Complete example program

Build command

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

kitaqfc/lib/fds_overlay.h:20

Declaration notes (original source)
Return the linker-generated overlay-function count; zero if the table is disabled.

kitaqfc/kitaqfc/CodeGenerator.cs:5229

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

Load a bank only if it is not already recorded as resident. Call before accessing an asset to avoid an unnecessary transfer when it is already available.

Syntax

c
u8 __fds_require_bank(u8 bank);

Parameters

bank

Logical bank to load, 1..254. Bank 0 is common code, not a loadable window; 255 is reserved for unknown residency. For bank 2 and above, the calculated file ID must also fit in 0..254.

Return value

Zero on success. Returns 255 for bank 0 or 255, a file-ID overflow, or bank 1 without a restore file. A failed BIOS transfer returns its error byte. Argument failures before a transfer preserve the residency record; failed BIOS transfers mark it 255 (unknown).

Conditions and limits

Use with --mapper=fds --fds-layout=fds32. Bank 0 is common code at $A000–$DFFF. Bank 1 occupies $6000–$9FFF at startup; banks 2 and above are overlays loaded into that same 16 KiB window. Place overlay code or data with directives such as #pragma fixed_bank 2.

A valid bank matching the record returns zero; otherwise this calls __fds_load_bank(). The initial comparison remains active with --fds-no-overlay-guard. Bank 255 never counts as resident, even when the record is 255.

Bank 1 is restored from a separate 16,384-byte boot PRG file at $6000. Without a manifest, the default files are ID 0 for bank 1, ID 1 for common code and ID 2 for CHR. A custom manifest must split the boot PRG into 16,384-byte files at $6000 and $A000. For banks 2 and above, the file ID is --fds-overlay-start-id (32 by default) + bank number - 2.

Loading a different bank directly from code in the $6000–$9FFF window can overwrite the executing function or its return address. This sample places the caller and display code in #pragma fixed_bank 0. To call a function in another bank and return, use __fds_overlay_farcall() or an ordinary typed function call.

BIOS calls are synchronous; this API does not keep the game loop advancing once per frame during a load. Use it from ordinary control code in the common bank, not from an interrupt handler. Display/audio continuity and coexistence with the real BIOS require separate verification.

These captures use an original LoadFiles calling-convention fixture to check compiled CPU code, memory transfers and every display pixel. They do not verify the real BIOS, disk-transfer timing, CRC or hardware boot. KUROSAKI has a partial FDS controller model; ordinary execution without this fixture does not establish the same transfer results.

The first compiler command below builds the downloadable sample. The Python checker builds all five lessons with and without optimization, then checks them in KUROSAKI. It creates and removes a temporary fixture from scripts/fds_abi_fixture.py; no external BIOS is needed for these checks. The fixture is not a BIOS for normal games.

Usage example

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

This is a calling fragment. Follow the stated initialization and buffer requirements before using it in a complete program.

Expected result

With the original LoadFiles ABI fixture, LOAD BANK 2 STATUS, REQUIRE SAME BANK and RESTORE BANK 1 are all 000. FIRST / LAST BYTE shows 011 and 044 from the transferred four-byte payload; RESIDENT BANK is 001 after restoration. Invalid banks 0 and 255 each return 255. Requiring the same bank issues no extra load: the checked file requests are 32 then 0.

__fds_require_bank: BANK_LOAD-DEFAULT — With the original LoadFiles ABI fixture, LOAD BANK 2 STATUS, REQUIRE SAME BANK and RESTORE BANK 1 are all 000. FIRST / LAST BYTE shows 011 and 044 from the transferred four-byte payload; RESIDENT BANK is 001 after restoration. Invalid banks 0 and 255 each return 255. Requiring the same bank issues no extra load: the checked file requests are 32 then 0.
BANK_LOAD-DEFAULT — With the original LoadFiles ABI fixture, LOAD BANK 2 STATUS, REQUIRE SAME BANK and RESTORE BANK 1 are all 000. FIRST / LAST BYTE shows 011 and 044 from the transferred four-byte payload; RESIDENT BANK is 001 after restoration. Invalid banks 0 and 255 each return 255. Requiring the same bank issues no extra load: the checked file requests are 32 then 0.

Complete example program

Build command

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

kitaqfc/lib/fds_overlay.h:13

Declaration notes (original source)
Return zero when the software residency record matches, otherwise call __fds_load_bank.

kitaqfc/kitaqfc/CodeGenerator.cs:9451

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

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

fds_sound.h — FDS wavetable audio

Declares compiler intrinsics for the 64-step waveform, modulation table, pitch, volume and envelopes. KITAQFC generates their implementations.

Build a .fds image with --mapper=fds. This lesson operates sound without disk I/O. The recording uses KUROSAKI’s direct RAM boot and audio model. Its empty test firmware backing implements no BIOS routines and is not distributed. Normal disk boot and physical hardware require an appropriate boot environment.

__fds_env_set

Write the three raw arguments to $4080, $4084 and $408A, in that order.

Syntax

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

Parameters

speed

Despite the parameter name, this is the raw $4080 value: bit 7 direct volume, bit 6 increasing envelope.

gain

Raw modulation-envelope value written to $4084.

mode

Envelope master speed written to $408A.

Return value

No return value. Changes sound registers or driver state.

Conditions and limits

Build a .fds image with --mapper=fds. This lesson operates sound without disk I/O. The recording uses KUROSAKI’s direct RAM boot and audio model. Its empty test firmware backing implements no BIOS routines and is not distributed. Normal disk boot and physical hardware require an appropriate boot environment.

Usage example

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

Expected result

An original 64-step triangle plays at about 440 Hz, pauses, plays at about 660 Hz, then stops. The second note uses nes_fds_wave_load and raw envelope settings. This verifies the audio model and direct boot, not BIOS-driven disk startup.

__fds_env_set: NTSC
NTSC
NTSC — Captured audio

Complete example program

Build command

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\sound_fds.c -I .\kitaqfc\lib -I .\kitaq-docs\samples -o .\out\fc-fds.fds --mapper=fds --nes-chr=.\kitaq-docs\samples\font.chr --no-cache --no-disasm
Implementation and source

kitaqfc/lib/fds_sound.h:18

Declaration notes (original source)
Write the three raw arguments to 4080, 4084 and 408A in that order.
The legacy parameter names do not describe three independently packed envelope fields.

kitaqfc/kitaqfc/CodeGenerator.cs:5207

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

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

Write the 12-bit frequency to $4082/$4083 and clear $4083’s upper control bits.

Syntax

c
void __fds_freq_set(u16 freq);

Parameters

freq

FDS frequency register value 0..4095. Upper control bits are cleared.

Return value

No return value. Changes sound registers or driver state.

Conditions and limits

Build a .fds image with --mapper=fds. This lesson operates sound without disk I/O. The recording uses KUROSAKI’s direct RAM boot and audio model. Its empty test firmware backing implements no BIOS routines and is not distributed. Normal disk boot and physical hardware require an appropriate boot environment.

Usage example

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

Expected result

An original 64-step triangle plays at about 440 Hz, pauses, plays at about 660 Hz, then stops. The second note uses nes_fds_wave_load and raw envelope settings. This verifies the audio model and direct boot, not BIOS-driven disk startup.

__fds_freq_set: NTSC
NTSC
NTSC — Captured audio

Complete example program

Build command

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\sound_fds.c -I .\kitaqfc\lib -I .\kitaq-docs\samples -o .\out\fc-fds.fds --mapper=fds --nes-chr=.\kitaq-docs\samples\font.chr --no-cache --no-disasm
Implementation and source

kitaqfc/lib/fds_sound.h:13

Declaration notes (original source)
Write a 12-bit frequency to 4082/4083; upper control bits in 4083 are cleared.

kitaqfc/kitaqfc/CodeGenerator.cs:5197

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

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

Send 32 bytes in order to modulation-table write port $4088.

Syntax

c
void __fds_mod_load(const u8* mod32);

Parameters

mod32

Thirty-two bytes for the FDS modulation table, each a control code 0..7. Halt modulation before loading.

Return value

No return value. Changes sound registers or driver state.

Conditions and limits

Build a .fds image with --mapper=fds. This lesson operates sound without disk I/O. The recording uses KUROSAKI’s direct RAM boot and audio model. Its empty test firmware backing implements no BIOS routines and is not distributed. Normal disk boot and physical hardware require an appropriate boot environment.

Usage example

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

Expected result

An original 64-step triangle plays at about 440 Hz, pauses, plays at about 660 Hz, then stops. The second note uses nes_fds_wave_load and raw envelope settings. This verifies the audio model and direct boot, not BIOS-driven disk startup.

__fds_mod_load: NTSC
NTSC
NTSC — Captured audio

Complete example program

Build command

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\sound_fds.c -I .\kitaqfc\lib -I .\kitaq-docs\samples -o .\out\fc-fds.fds --mapper=fds --nes-chr=.\kitaq-docs\samples\font.chr --no-cache --no-disasm
Implementation and source

kitaqfc/lib/fds_sound.h:11

Declaration notes (original source)
Stream 32 readable bytes to 4088; the caller prepares modulation state and write conditions.

kitaqfc/kitaqfc/CodeGenerator.cs:5195

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

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

Write 0x83 to disk/sound gate $4023 and zero wave control $4089 and speed $408A.

Syntax

c
void __fds_sound_enable(void);

Return value

No return value. Changes sound registers or driver state.

Conditions and limits

Build a .fds image with --mapper=fds. This lesson operates sound without disk I/O. The recording uses KUROSAKI’s direct RAM boot and audio model. Its empty test firmware backing implements no BIOS routines and is not distributed. Normal disk boot and physical hardware require an appropriate boot environment.

Usage example

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

Expected result

An original 64-step triangle plays at about 440 Hz, pauses, plays at about 660 Hz, then stops. The second note uses nes_fds_wave_load and raw envelope settings. This verifies the audio model and direct boot, not BIOS-driven disk startup.

__fds_sound_enable: NTSC
NTSC
NTSC — Captured audio

Complete example program

Build command

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\sound_fds.c -I .\kitaqfc\lib -I .\kitaq-docs\samples -o .\out\fc-fds.fds --mapper=fds --nes-chr=.\kitaq-docs\samples\font.chr --no-cache --no-disasm
Implementation and source

kitaqfc/lib/fds_sound.h:7

Declaration notes (original source)
Write 83 to 4023 and zero to 4089/408A to initialize disk/sound register gates.

kitaqfc/kitaqfc/CodeGenerator.cs:5183

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

Write the low six volume bits with 0x80 to $4080 for direct volume control.

Syntax

c
void __fds_volume_set(u8 vol);

Parameters

vol

FDS direct volume 0..63; masking to six bits makes 64 wrap to zero.

Return value

No return value. Changes sound registers or driver state.

Conditions and limits

Build a .fds image with --mapper=fds. This lesson operates sound without disk I/O. The recording uses KUROSAKI’s direct RAM boot and audio model. Its empty test firmware backing implements no BIOS routines and is not distributed. Normal disk boot and physical hardware require an appropriate boot environment.

Usage example

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

Expected result

An original 64-step triangle plays at about 440 Hz, pauses, plays at about 660 Hz, then stops. The second note uses nes_fds_wave_load and raw envelope settings. This verifies the audio model and direct boot, not BIOS-driven disk startup.

__fds_volume_set: NTSC
NTSC
NTSC — Captured audio

Complete example program

Build command

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\sound_fds.c -I .\kitaqfc\lib -I .\kitaq-docs\samples -o .\out\fc-fds.fds --mapper=fds --nes-chr=.\kitaq-docs\samples\font.chr --no-cache --no-disasm
Implementation and source

kitaqfc/lib/fds_sound.h:15

Declaration notes (original source)
Write (volume & 3F) | 80 to 4080 for direct volume control; masking wraps, rather than clamps.

kitaqfc/kitaqfc/CodeGenerator.cs:5200

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

Enable wave writes, copy 64 bytes to $4040..$407F and reset $4089 to zero.

Syntax

c
void __fds_wave_load(const u8* wave64);

Parameters

wave64

Sixty-four FDS wave bytes, each a 6-bit amplitude 0..63. NULL is not checked.

Return value

No return value. Changes sound registers or driver state.

Conditions and limits

Build a .fds image with --mapper=fds. This lesson operates sound without disk I/O. The recording uses KUROSAKI’s direct RAM boot and audio model. Its empty test firmware backing implements no BIOS routines and is not distributed. Normal disk boot and physical hardware require an appropriate boot environment.

Usage example

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

Expected result

An original 64-step triangle plays at about 440 Hz, pauses, plays at about 660 Hz, then stops. The second note uses nes_fds_wave_load and raw envelope settings. This verifies the audio model and direct boot, not BIOS-driven disk startup.

__fds_wave_load: NTSC
NTSC
NTSC — Captured audio

Complete example program

Build command

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\sound_fds.c -I .\kitaqfc\lib -I .\kitaq-docs\samples -o .\out\fc-fds.fds --mapper=fds --nes-chr=.\kitaq-docs\samples\font.chr --no-cache --no-disasm
Implementation and source

kitaqfc/lib/fds_sound.h:9

Declaration notes (original source)
Copy exactly 64 readable bytes to wave RAM with wave writes enabled, then set 4089 to zero.

kitaqfc/kitaqfc/CodeGenerator.cs:5193

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

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

intrinsics.h — Compiler intrinsics

__nes_audio_vblank_tick

NMI hook that advances BGM and SFX delays and updates the APU according to current channel ownership.

Syntax

c
void __nes_audio_vblank_tick(void);

Return value

No return value.

Conditions and limits

Include audio_vblank.h and add audio_vblank.c to the build exactly once. The compiler calls its hook once from either the default or a custom NMI handler. Enable PPU NMI. The foreground loop does not call a music update routine.

This four-channel driver is tuned for NTSC: CH1=pulse 1, CH2=pulse 2, CH3=triangle, CH4=noise, indexed 0..3. It does not play DMC or expansion audio. Do not run it alongside direct APU writers such as the effects in audio.c. There is no PAL pitch table or tempo correction.

Do not call this directly from application code. Linking the music library connects it to NMI through the compiler. The hook saves A/X/Y and does not use shared expression scratch. The hook and pitch tables reside in fixed ROM; it never switches ROM banks in NMI. Its preservation applies to the music hook itself; a custom NMI handler remains responsible for preserving state used by its other work.

A record contains five bytes: delay, CH1, CH2, CH3, CH4. Delay 1..255 is the number of NMI ticks for which the new state lasts. Zero terminates playback and ignores the other four bytes. Tonal notes 0..71 are C2..B7 (24=C4). Noise values 0..15 select a long-mode period, and 16..31 select short mode. For any channel, 255=NES_AUDIO_HOLD leaves it unchanged; 254=NES_AUDIO_STOP silences it.

Usage example

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

This is a calling fragment. Follow the stated initialization and buffer requirements before using it in a complete program.

Expected result

The sample plays an original four-channel phrase once. Music continues while the foreground performs integer arithmetic without refilling for 90 frames. “PHRASE COMPLETE / SILENT” and “UNDERRUNS 0 / MATH OK” mean playback finished, no queue starvation occurred and arithmetic results matched. Listen to the captured audio for the pulse melody and accompaniment, triangle bass and noise. The end is silent. The image reports control results; use the audio to assess sound.

__nes_audio_vblank_tick: NROM
NROM
NROM — Captured audio

Complete example program

Build command

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaqfc\lib\audio_vblank.c .\kitaq-docs\samples\api-examples\fc\audio_vblank.c -I .\kitaqfc\lib -I .\kitaq-docs\samples --mapper=nrom --nes-chr=.\kitaq-docs\samples\font.chr -o .\out\audio_vblank.nes --no-cache --no-disasm
Implementation and source

kitaqfc/lib/audio_vblank.h:70

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

kitaqfc/lib/audio_vblank.c:150

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

Write the entire packed attribute byte at 0x23C0 + (y>>2)*8 + (x>>2) in nametable 0. X/Y are tile coordinates identifying a 4×4-tile attribute area. All four two-bit palette fields are replaced; this does not merge one selected quadrant.

Syntax

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

Parameters

x / y

Tile coordinates, not pixels. Keep x in 0..31 and y in 0..29 for the visible 32×30 tile map. Each tile covers 8×8 pixels. The byte arguments are not clamped; invalid coordinates can write outside the intended tile area.

attr

Packed attribute byte: bits 0–1 select the upper-left 2×2 tiles, 2–3 upper-right, 4–5 lower-left and 6–7 lower-right. Each field selects palette 0–3. Values 0x55, 0xAA and 0xFF select palette 1, 2 or 3 for all four quadrants.

Return value

None (void).

Conditions and limits

Direct PPU access requires rendering to be off or the entire operation to fit a safe VBlank interval. These functions do not wait, queue, mask NMI or restore scroll. Prevent an interrupt from changing the shared latch/address during the call. PPUCTRL bit 2 selects a destination step of 1 or 32 per data byte. The PPU handles mirroring and address wrapping; CHR-ROM cannot be written. Set the intended scroll state after access and before rendering.

NESdev: PPU registers

NESdev: PPU scrolling

Usage example

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

This is a calling fragment. Follow the stated initialization and buffer requirements before using it in a complete program.

Expected result

This program teaches register setup, direct tile/attribute writes, scrolling and separate background/sprite palette loads. The blue band at (0,16) measures 256×16 pixels: its first row repeats a solid 8×8 tile, a left-half 4×8 bar and an eight-row triangle; its second row contains only left-half bars. A solid green 64×32 rectangle starts at (16,48). A red 64×32 region at (144,48) contains an 8-by-4 grid of triangle tiles.

At y=96, blue markers at x=16, 24 and 32 are respectively a square, left-half bar and triangle; a red left-half bar is at x=80 and a green triangle at x=144. These are background tiles. At y=144, the separately colored sprites are a red 8×8 square at x=16, a blue left-half 4×8 bar at x=80 and a green eight-row triangle at x=144. Final scrolling is (0,0).

The image checks 16,384 pixels for the expected shapes and colors, including sprite colors. Separate state tests cover 31 cases: register replacement, status/latch effects, saved scroll coordinates, zero/255-byte transfers, increments 1/32, map edges, zero rectangle dimensions, table selectors, whole-byte attributes and both palette targets. Values of Y=255 are checked as register writes, not as correct scrolling artwork. Evidence is from KUROSAKI with NROM; real hardware is not tested.

__attr_set: NROM
NROM

Complete example program

Build command

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\ppu_intrinsics.c -I .\kitaqfc\lib -I .\kitaq-docs\samples --nes-chr=.\kitaq-docs\samples\api-examples\fc\vram_shapes.chr -o .\out\ppu_intrinsics.nes --no-cache --no-disasm
Implementation and source

kitaqfc/lib/intrinsics.h:134

Declaration notes (original source)
Write the entire packed attribute byte at 23C0+(y>>2)*8+(x>>2).
Use tile coordinates below 32; this does not merge a single palette quadrant.

kitaqfc/kitaqfc/CodeGenerator.cs:4824

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

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

Select logical nametable nt & 3, reset the PPU latch and write one complete attribute byte at 0x23C0 + (nt & 3)*0x400 + (y>>2)*8 + (x>>2). All palette quadrants in that byte are overwritten. Table selection follows the cartridge's current mirroring.

Syntax

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

Parameters

nt

Logical nametable selector. Only the lowest two bits are used (nt & 3); for example, 4 selects table 0 and 7 selects table 3. This does not allocate independent storage or change mapper mirroring.

x / y

Tile coordinates, not pixels. Keep x in 0..31 and y in 0..29 for the visible 32×30 tile map. Each tile covers 8×8 pixels. The byte arguments are not clamped; invalid coordinates can write outside the intended tile area.

attr

Packed attribute byte: bits 0–1 select the upper-left 2×2 tiles, 2–3 upper-right, 4–5 lower-left and 6–7 lower-right. Each field selects palette 0–3. Values 0x55, 0xAA and 0xFF select palette 1, 2 or 3 for all four quadrants.

Return value

None (void).

Conditions and limits

Direct PPU access requires rendering to be off or the entire operation to fit a safe VBlank interval. These functions do not wait, queue, mask NMI or restore scroll. Prevent an interrupt from changing the shared latch/address during the call. PPUCTRL bit 2 selects a destination step of 1 or 32 per data byte. The PPU handles mirroring and address wrapping; CHR-ROM cannot be written. Set the intended scroll state after access and before rendering.

NESdev: PPU registers

NESdev: PPU scrolling

Usage example

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

This is a calling fragment. Follow the stated initialization and buffer requirements before using it in a complete program.

Expected result

This program teaches register setup, direct tile/attribute writes, scrolling and separate background/sprite palette loads. The blue band at (0,16) measures 256×16 pixels: its first row repeats a solid 8×8 tile, a left-half 4×8 bar and an eight-row triangle; its second row contains only left-half bars. A solid green 64×32 rectangle starts at (16,48). A red 64×32 region at (144,48) contains an 8-by-4 grid of triangle tiles.

At y=96, blue markers at x=16, 24 and 32 are respectively a square, left-half bar and triangle; a red left-half bar is at x=80 and a green triangle at x=144. These are background tiles. At y=144, the separately colored sprites are a red 8×8 square at x=16, a blue left-half 4×8 bar at x=80 and a green eight-row triangle at x=144. Final scrolling is (0,0).

The image checks 16,384 pixels for the expected shapes and colors, including sprite colors. Separate state tests cover 31 cases: register replacement, status/latch effects, saved scroll coordinates, zero/255-byte transfers, increments 1/32, map edges, zero rectangle dimensions, table selectors, whole-byte attributes and both palette targets. Values of Y=255 are checked as register writes, not as correct scrolling artwork. Evidence is from KUROSAKI with NROM; real hardware is not tested.

__attr_set_nt: NROM
NROM

Complete example program

Build command

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\ppu_intrinsics.c -I .\kitaqfc\lib -I .\kitaq-docs\samples --nes-chr=.\kitaq-docs\samples\api-examples\fc\vram_shapes.chr -o .\out\ppu_intrinsics.nes --no-cache --no-disasm
Implementation and source

kitaqfc/lib/intrinsics.h:136

Declaration notes (original source)
Write a whole packed attribute byte in nametable nt&3; other quadrant fields are overwritten.

kitaqfc/kitaqfc/CodeGenerator.cs:4825

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

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

Obtain the logical ROM-bank number assigned to a named data object or function. This resolves placement information; it does not inspect the currently mapped bank or dereference a pointer.

Syntax

c
u8 bank = __bankof(symbol);

Parameters

symbol

The name of a ROM data object or function whose placement is known to the compiler or linker. A pointer variable does not mean the object it points to; runtime expressions are not accepted as symbol names.

Return value

A u8 bank number. Zero can identify common ROM; it is not necessarily a valid switch-bank selector on every target.

Usage example

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

This is a calling fragment. Follow the stated initialization and buffer requirements before using it in a complete program.

Expected result

The sample checks that both payload and callback are placed in bank 3. This proves placement metadata; separate reads and callback checks prove the runtime bank behavior.

The complete program teaches how a bank number and near address identify data, how callbacks cross banks, and how to distinguish the library's record from the actual mapping. All displayed numbers are hexadecimal. BANK IN and BANK OUT must show 02, BYTE HEX D3, WORD HEX 5C7A, and both FAILED CHECKS and FIRST FAILURE 00. CGB and FC display blue text; DMG displays black text.

CALL COUNT is 02: the far_call macro and __farcall each invoke the callback once.

__bankof: MMC3
MMC3

Complete example program

Build command

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

kitaqfc/lib/intrinsics.h:77

Declaration notes (original source)
PRG bank
Resolve a symbol or function placement to its byte bank number at compilation time.

kitaqfc/kitaqfc/CodeGenerator.cs:6906

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

Select a logical PRG bank through the compiler's mapper-specific helper and update its internal bank state. This changes the mapping persistently, without updating the bank library's separate record.

Syntax

c
void __bankswitch(u8 bank);

Parameters

bank

An unsigned 8-bit ROM-bank number. For a call, it must agree with the function's placement; for a read, it selects the source mapping. Use the target cartridge's valid range.

Return value

None (void).

Conditions and limits

A permanent bank change must not unmap the executing code or its return address. Put this call, bank_switch itself when used, and the code that continues afterwards in common/fixed bank 0, as the sample does.

The library record starts at zero and does not discover the cartridge's startup mapping. Initialize it with bank_switch. Direct intrinsics and raw mapper writes do not update this separate record; bank_get_current therefore cannot verify the actual hardware mapping.

FC switch-bank numbers are logical compiler numbers. Zero denotes common ROM, not a switch target. The helper normalizes zero to bank 1. On SUROM512, only 1–30 are valid: an out-of-range constant passed directly to the switching intrinsic is a build error; an invalid runtime value falls back to 1 and sets the mapper-fault state. Read common ROM directly instead of requesting a bank-zero switch.

Usage example

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

This is a calling fragment. Follow the stated initialization and buffer requirements before using it in a complete program.

Expected result

The initial bank_switch(2) records and selects bank 2. Later, direct switching selects bank 1 and reads A1 from home1 while bank_get_current still reports 2; switching back makes home2 read B2 again.

The complete program teaches how a bank number and near address identify data, how callbacks cross banks, and how to distinguish the library's record from the actual mapping. All displayed numbers are hexadecimal. BANK IN and BANK OUT must show 02, BYTE HEX D3, WORD HEX 5C7A, and both FAILED CHECKS and FIRST FAILURE 00. CGB and FC display blue text; DMG displays black text.

CALL COUNT is 02: the far_call macro and __farcall each invoke the callback once.

__bankswitch: MMC3
MMC3

Complete example program

Build command

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

kitaqfc/lib/intrinsics.h:79

Declaration notes (original source)
Select the mapper-specific PRG bank; the common bank and legal bank range depend on the target board.

kitaqfc/kitaqfc/CodeGenerator.cs:4746

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

Clear the selected flag to 0 without clearing the rest of its byte. Use this to remove one stored condition, such as a temporary status flag. Clearing the same bit again still leaves it at 0.

Syntax

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

Parameters

base

Pointer to the first byte of the flag array in CPU-accessible memory. The array is not allocated or initialized by this call. Use writable RAM for set, clear and toggle operations; the test operation only reads it.

bit

Zero-based bit index as u16 (0–65535), not a byte offset or a bit mask. Index 0 selects mask 0x01 in byte 0; index 7 selects 0x80 in byte 0; index 8 selects 0x01 in byte 1. In general, the byte offset is index / 8 and the mask is 1 << (index % 8). For an N-byte array, require index < 8*N and a valid CPU address range.

Return value

None (void).

Conditions and limits

The caller must provide enough storage. There is no array-length argument, bounds clipping, bank switching or automatic graphics upload. A large bit number can select a byte well beyond the allocated array. Keep the memory mapping stable; do not use these RAM-flag helpers as a shortcut for hardware registers whose reads or writes have side effects.

Set, clear and toggle read a byte, modify the selected bit, then write the byte back. This sequence is not an atomic update. Coordinate writers in the main loop and interrupt handlers so that an intervening update to another bit in the same byte is not lost.

FC code may inline a constant index or call a generated helper. Both forms use the same bit numbering and test-result mask. The generated helpers share argument and scratch storage, so do not re-enter them from NMI/IRQ during another helper call.

Usage example

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.

This is a calling fragment. Follow the stated initialization and buffer requirements before using it in a complete program.

Expected result

The complete program reaches this step with byte 0 equal to 0xA6. Clearing bit 2 changes it to 0xA2 (binary 10100010, decimal 162); the CLEAR BIT2 row must show 162. Clearing bit 2 again leaves the result unchanged. The other stored flags survive both calls.

Expected FC rows, in decimal: TEST OFF 000, TEST ON 128, SET BIT1 166, CLEAR BIT2 162, TOGGLE BIT7 034, RESTORE 162, NEXT BYTE 130, FAILED 000. NEXT BYTE comes from setting index 9 in the second byte, changing 0x80 to 0x82 without modifying byte 0. The captured FC screen uses blue text on black.

The program checks every displayed transition and shows the number of failures. Set, clear and toggle themselves return no value; their rows display the resulting RAM byte. Separate emulator checks compare all 520 bytes of guarded buffers in 136 cases per platform, including constant and runtime indices across byte and 256-byte boundaries and all eight possible nonzero bit-test masks. These are RAM-state checks, not a drawing demonstration. Physical hardware and concurrent interrupt writers have not been tested.

__bit_clear: NROM
NROM

Complete example program

Build command

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\bit_intrinsics.c -I .\kitaqfc\lib -I .\kitaq-docs\samples -o .\out\bit_intrinsics.nes --mapper=nrom --nes-chr=.\kitaq-docs\samples\font.chr --no-cache --no-disasm
Implementation and source

kitaqfc/lib/intrinsics.h:45

Declaration notes (original source)
Clear the indexed bit in caller storage; no bounds check or interrupt exclusion is inserted.

kitaqfc/kitaqfc/CodeGenerator.cs:4640

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

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

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

Set the selected flag to 1 while preserving the other seven bits in the same byte. For example, mark a quest as completed or an item as collected. Setting a bit that is already 1 leaves it unchanged; this is not a toggle.

Syntax

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

Parameters

base

Pointer to the first byte of the flag array in CPU-accessible memory. The array is not allocated or initialized by this call. Use writable RAM for set, clear and toggle operations; the test operation only reads it.

bit

Zero-based bit index as u16 (0–65535), not a byte offset or a bit mask. Index 0 selects mask 0x01 in byte 0; index 7 selects 0x80 in byte 0; index 8 selects 0x01 in byte 1. In general, the byte offset is index / 8 and the mask is 1 << (index % 8). For an N-byte array, require index < 8*N and a valid CPU address range.

Return value

None (void).

Conditions and limits

The caller must provide enough storage. There is no array-length argument, bounds clipping, bank switching or automatic graphics upload. A large bit number can select a byte well beyond the allocated array. Keep the memory mapping stable; do not use these RAM-flag helpers as a shortcut for hardware registers whose reads or writes have side effects.

Set, clear and toggle read a byte, modify the selected bit, then write the byte back. This sequence is not an atomic update. Coordinate writers in the main loop and interrupt handlers so that an intervening update to another bit in the same byte is not lost.

FC code may inline a constant index or call a generated helper. Both forms use the same bit numbering and test-result mask. The generated helpers share argument and scratch storage, so do not re-enter them from NMI/IRQ during another helper call.

Usage example

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.

This is a calling fragment. Follow the stated initialization and buffer requirements before using it in a complete program.

Expected result

Set bit 1 of 0xA4: 10100100 | 00000010 becomes 10100110, or 0xA6 (decimal 166). The SET BIT1 row must show 166. The sample sets the bit a second time and verifies that the byte remains 0xA6, demonstrating that repeated setting does not invert a flag.

Expected FC rows, in decimal: TEST OFF 000, TEST ON 128, SET BIT1 166, CLEAR BIT2 162, TOGGLE BIT7 034, RESTORE 162, NEXT BYTE 130, FAILED 000. NEXT BYTE comes from setting index 9 in the second byte, changing 0x80 to 0x82 without modifying byte 0. The captured FC screen uses blue text on black.

The program checks every displayed transition and shows the number of failures. Set, clear and toggle themselves return no value; their rows display the resulting RAM byte. Separate emulator checks compare all 520 bytes of guarded buffers in 136 cases per platform, including constant and runtime indices across byte and 256-byte boundaries and all eight possible nonzero bit-test masks. These are RAM-state checks, not a drawing demonstration. Physical hardware and concurrent interrupt writers have not been tested.

__bit_set: NROM
NROM

Complete example program

Build command

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\bit_intrinsics.c -I .\kitaqfc\lib -I .\kitaq-docs\samples -o .\out\bit_intrinsics.nes --mapper=nrom --nes-chr=.\kitaq-docs\samples\font.chr --no-cache --no-disasm
Implementation and source

kitaqfc/lib/intrinsics.h:43

Declaration notes (original source)
Set the indexed bit in caller storage without changing the other seven bits in that byte.

kitaqfc/kitaqfc/CodeGenerator.cs:4637

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

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

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

Read one flag from a packed array of bits without modifying the array. This lets eight independent on/off flags share each byte of RAM. The result tells you whether the selected bit is clear or set; its numeric representation depends on the target.

Syntax

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

Parameters

base

Pointer to the first byte of the flag array in CPU-accessible memory. The array is not allocated or initialized by this call. Use writable RAM for set, clear and toggle operations; the test operation only reads it.

bit

Zero-based bit index as u16 (0–65535), not a byte offset or a bit mask. Index 0 selects mask 0x01 in byte 0; index 7 selects 0x80 in byte 0; index 8 selects 0x01 in byte 1. In general, the byte offset is index / 8 and the mask is 1 << (index % 8). For an N-byte array, require index < 8*N and a valid CPU address range.

Return value

u8: 0 when the bit is clear, otherwise its mask: 1, 2, 4, 8, 16, 32, 64 or 128. The result is base[bit >> 3] & (1 << (bit & 7)). For example, testing a set bit 7 returns 128. Use result != 0 to test whether the flag is set; comparing only with 1 would miss flags in other bit positions.

Conditions and limits

The caller must provide enough storage. There is no array-length argument, bounds clipping, bank switching or automatic graphics upload. A large bit number can select a byte well beyond the allocated array. Keep the memory mapping stable; do not use these RAM-flag helpers as a shortcut for hardware registers whose reads or writes have side effects.

FC code may inline a constant index or call a generated helper. Both forms use the same bit numbering and test-result mask. The generated helpers share argument and scratch storage, so do not re-enter them from NMI/IRQ during another helper call.

Usage example

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.

This is a calling fragment. Follow the stated initialization and buffer requirements before using it in a complete program.

Expected result

Start with byte 0 equal to 0xA4 (binary 10100100). Testing bit 1 returns 0. Testing bit 7 returns 1 on GB or 128 on FC; the sample also converts that nonzero result to a shared on/off flag and checks that the original byte is unchanged. Look at TEST OFF and TEST ON in the screen below.

Expected FC rows, in decimal: TEST OFF 000, TEST ON 128, SET BIT1 166, CLEAR BIT2 162, TOGGLE BIT7 034, RESTORE 162, NEXT BYTE 130, FAILED 000. NEXT BYTE comes from setting index 9 in the second byte, changing 0x80 to 0x82 without modifying byte 0. The captured FC screen uses blue text on black.

The program checks every displayed transition and shows the number of failures. Set, clear and toggle themselves return no value; their rows display the resulting RAM byte. Separate emulator checks compare all 520 bytes of guarded buffers in 136 cases per platform, including constant and runtime indices across byte and 256-byte boundaries and all eight possible nonzero bit-test masks. These are RAM-state checks, not a drawing demonstration. Physical hardware and concurrent interrupt writers have not been tested.

__bit_test: NROM
NROM

Complete example program

Build command

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\bit_intrinsics.c -I .\kitaqfc\lib -I .\kitaq-docs\samples -o .\out\bit_intrinsics.nes --mapper=nrom --nes-chr=.\kitaq-docs\samples\font.chr --no-cache --no-disasm
Implementation and source

kitaqfc/lib/intrinsics.h:41

Declaration notes (original source)
Return base[bit>>3] & (1<<(bit&7)): zero or the bit mask, not a normalized Boolean.

kitaqfc/kitaqfc/CodeGenerator.cs:4634

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

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

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

Invert the selected flag: 0 becomes 1 and 1 becomes 0. The remaining bits are preserved. Two calls on the same bit restore its original state, provided nothing else writes that bit between the calls.

Syntax

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

Parameters

base

Pointer to the first byte of the flag array in CPU-accessible memory. The array is not allocated or initialized by this call. Use writable RAM for set, clear and toggle operations; the test operation only reads it.

bit

Zero-based bit index as u16 (0–65535), not a byte offset or a bit mask. Index 0 selects mask 0x01 in byte 0; index 7 selects 0x80 in byte 0; index 8 selects 0x01 in byte 1. In general, the byte offset is index / 8 and the mask is 1 << (index % 8). For an N-byte array, require index < 8*N and a valid CPU address range.

Return value

None (void).

Conditions and limits

The caller must provide enough storage. There is no array-length argument, bounds clipping, bank switching or automatic graphics upload. A large bit number can select a byte well beyond the allocated array. Keep the memory mapping stable; do not use these RAM-flag helpers as a shortcut for hardware registers whose reads or writes have side effects.

Set, clear and toggle read a byte, modify the selected bit, then write the byte back. This sequence is not an atomic update. Coordinate writers in the main loop and interrupt handlers so that an intervening update to another bit in the same byte is not lost.

FC code may inline a constant index or call a generated helper. Both forms use the same bit numbering and test-result mask. The generated helpers share argument and scratch storage, so do not re-enter them from NMI/IRQ during another helper call.

Usage example

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.

This is a calling fragment. Follow the stated initialization and buffer requirements before using it in a complete program.

Expected result

The complete program enters this step with 0xA2. Toggling bit 7 produces 0x22 (decimal 34), so TOGGLE BIT7 shows 034. Toggling it again restores 0xA2, and RESTORE shows 162. These two observations demonstrate both directions of the inversion while preserving the other bits.

Expected FC rows, in decimal: TEST OFF 000, TEST ON 128, SET BIT1 166, CLEAR BIT2 162, TOGGLE BIT7 034, RESTORE 162, NEXT BYTE 130, FAILED 000. NEXT BYTE comes from setting index 9 in the second byte, changing 0x80 to 0x82 without modifying byte 0. The captured FC screen uses blue text on black.

The program checks every displayed transition and shows the number of failures. Set, clear and toggle themselves return no value; their rows display the resulting RAM byte. Separate emulator checks compare all 520 bytes of guarded buffers in 136 cases per platform, including constant and runtime indices across byte and 256-byte boundaries and all eight possible nonzero bit-test masks. These are RAM-state checks, not a drawing demonstration. Physical hardware and concurrent interrupt writers have not been tested.

__bit_toggle: NROM
NROM

Complete example program

Build command

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\bit_intrinsics.c -I .\kitaqfc\lib -I .\kitaq-docs\samples -o .\out\bit_intrinsics.nes --mapper=nrom --nes-chr=.\kitaq-docs\samples\font.chr --no-cache --no-disasm
Implementation and source

kitaqfc/lib/intrinsics.h:47

Declaration notes (original source)
XOR the indexed bit in caller storage; coordinate concurrent writers to avoid lost updates.

kitaqfc/kitaqfc/CodeGenerator.cs:4643

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

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

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

Select a CHR bank. On MMC3 this selects R0, changing the 2 KiB window at PPU $0000–$07FF. On CNROM it changes the full 8 KiB region at $0000–$1FFF.

Syntax

c
void __chr_bank_set(u8 bank);

Parameters

bank

On CNROM this is an 8 KiB CHR bank number. On MMC3 it is a 1 KiB bank number selecting a 2 KiB window: the low bit is ignored, so 2 and 3 both select the pair of 1 KiB banks 2 and 3. Available banks depend on CHR ROM capacity; the API performs no range check.

Return value

No return value.

Conditions and limits

Intrinsic declarations are in intrinsics.h. Include nes_game.h for the nes_mapper_irq_* macros. Select the board at build time with --mapper.

Writing 0 or 1 to the MMC3 bank-select register also clears PRG mode bit 6 and CHR inversion bit 7. Do not combine this with a PRG/CHR layout that requires those bits to remain set. Switching during rendering changes subsequent pattern fetches. The sample checks with rendering off and restores the font banks before displaying results.

These three intrinsics support CNROM and MMC3. Other boards are rejected at compilation. SUROM also uses CHR control for outer PRG selection and does not allow these CHR operations.

Usage example

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

This is a calling fragment. Follow the stated initialization and buffer requirements before using it in a complete program.

Expected result

The MMC3 sample displays CHR markers 19/20, 21/22 and 23/24. CIRAM reads show A B A B for vertical mirroring and A A B B for horizontal mirroring. IRQ counts are 01→02→03→03: each of the first three explicit enables permits one interrupt; the count stays unchanged after disable. Each stage waits six frames.

__chr_bank_set: MMC3-DEFAULT
MMC3-DEFAULT

Complete example program

Build command

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

Usage example

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

This is a calling fragment. Follow the stated initialization and buffer requirements before using it in a complete program.

Expected result

The CNROM lesson shows markers 33, 25 and 41 after SET 2, SET0 1 and SET1 3. It reads original marker bytes through the PPU to demonstrate that all three names switch the entire 8 KiB pattern region.

__chr_bank_set: CNROM-DEFAULT
CNROM-DEFAULT

Complete example program

Build command

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

kitaqfc/lib/intrinsics.h:316

Declaration notes (original source)
Select a CNROM bank or MMC3 register 0 bank; unsupported mapper profiles produce a compile error.
__chr_bank_set0

Select MMC3 R0 and change the 2 KiB window at PPU $0000–$07FF. This is equivalent to __chr_bank_set. On CNROM it changes the full 8 KiB region.

Syntax

c
void __chr_bank_set0(u8 bank);

Parameters

bank

On CNROM this is an 8 KiB CHR bank number. On MMC3 it is a 1 KiB bank number selecting a 2 KiB window: the low bit is ignored, so 2 and 3 both select the pair of 1 KiB banks 2 and 3. Available banks depend on CHR ROM capacity; the API performs no range check.

Return value

No return value.

Conditions and limits

Intrinsic declarations are in intrinsics.h. Include nes_game.h for the nes_mapper_irq_* macros. Select the board at build time with --mapper.

Writing 0 or 1 to the MMC3 bank-select register also clears PRG mode bit 6 and CHR inversion bit 7. Do not combine this with a PRG/CHR layout that requires those bits to remain set. Switching during rendering changes subsequent pattern fetches. The sample checks with rendering off and restores the font banks before displaying results.

These three intrinsics support CNROM and MMC3. Other boards are rejected at compilation. SUROM also uses CHR control for outer PRG selection and does not allow these CHR operations.

Usage example

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

This is a calling fragment. Follow the stated initialization and buffer requirements before using it in a complete program.

Expected result

The MMC3 sample displays CHR markers 19/20, 21/22 and 23/24. CIRAM reads show A B A B for vertical mirroring and A A B B for horizontal mirroring. IRQ counts are 01→02→03→03: each of the first three explicit enables permits one interrupt; the count stays unchanged after disable. Each stage waits six frames.

__chr_bank_set0: MMC3-DEFAULT
MMC3-DEFAULT

Complete example program

Build command

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

Usage example

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

This is a calling fragment. Follow the stated initialization and buffer requirements before using it in a complete program.

Expected result

The CNROM lesson shows markers 33, 25 and 41 after SET 2, SET0 1 and SET1 3. It reads original marker bytes through the PPU to demonstrate that all three names switch the entire 8 KiB pattern region.

__chr_bank_set0: CNROM-DEFAULT
CNROM-DEFAULT

Complete example program

Build command

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

kitaqfc/lib/intrinsics.h:318

Declaration notes (original source)
Use the same CNROM/MMC3 register 0 selection as __chr_bank_set.
__chr_bank_set1

Select MMC3 R1 and change the 2 KiB window at PPU $0800–$0FFF, leaving R0 unchanged. CNROM has no independent second window; it changes the full 8 KiB region.

Syntax

c
void __chr_bank_set1(u8 bank);

Parameters

bank

On CNROM this is an 8 KiB CHR bank number. On MMC3 it is a 1 KiB bank number selecting a 2 KiB window: the low bit is ignored, so 2 and 3 both select the pair of 1 KiB banks 2 and 3. Available banks depend on CHR ROM capacity; the API performs no range check.

Return value

No return value.

Conditions and limits

Intrinsic declarations are in intrinsics.h. Include nes_game.h for the nes_mapper_irq_* macros. Select the board at build time with --mapper.

Writing 0 or 1 to the MMC3 bank-select register also clears PRG mode bit 6 and CHR inversion bit 7. Do not combine this with a PRG/CHR layout that requires those bits to remain set. Switching during rendering changes subsequent pattern fetches. The sample checks with rendering off and restores the font banks before displaying results.

These three intrinsics support CNROM and MMC3. Other boards are rejected at compilation. SUROM also uses CHR control for outer PRG selection and does not allow these CHR operations.

Usage example

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

This is a calling fragment. Follow the stated initialization and buffer requirements before using it in a complete program.

Expected result

The MMC3 sample displays CHR markers 19/20, 21/22 and 23/24. CIRAM reads show A B A B for vertical mirroring and A A B B for horizontal mirroring. IRQ counts are 01→02→03→03: each of the first three explicit enables permits one interrupt; the count stays unchanged after disable. Each stage waits six frames.

__chr_bank_set1: MMC3-DEFAULT
MMC3-DEFAULT

Complete example program

Build command

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

Usage example

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

This is a calling fragment. Follow the stated initialization and buffer requirements before using it in a complete program.

Expected result

The CNROM lesson shows markers 33, 25 and 41 after SET 2, SET0 1 and SET1 3. It reads original marker bytes through the PPU to demonstrate that all three names switch the entire 8 KiB pattern region.

__chr_bank_set1: CNROM-DEFAULT
CNROM-DEFAULT

Complete example program

Build command

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

kitaqfc/lib/intrinsics.h:320

Declaration notes (original source)
Select MMC3 register 1, or the same full CNROM bank register; this is mapper-specific banking.
__copy16

Copy exactly 16 bytes from src to dst, with no length argument. For example, copy one 16-byte record or one Game Boy/NES tile stored in CPU-accessible memory. The number 16 means bytes, not bits; this call does not upload graphics to the display.

Syntax

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

Parameters

dst

Pointer to the first writable destination byte. Reserve enough consecutive bytes for the complete operation; no buffer is allocated for you.

src

Pointer to the first readable source byte. The entire range must remain accessible and unchanged throughout the copy.

Return value

None (void).

Conditions and limits

Use separate, non-overlapping source and destination ranges. These routines copy forward and do not provide overlap-safe memmove behavior; writing into the unread part of the source can corrupt later bytes.

The operation finishes before the caller continues. It neither switches ROM/RAM banks nor clips a range to a buffer boundary. Keep mappings stable, use valid memory ranges and coordinate access with interrupt handlers that use the same buffers.

On FC, dst is a CPU address, not a PPU address. Copying to CPU RAM does not update CHR, nametables or palettes; use PPU/VRAM APIs to upload that data. Generated helpers share argument/scratch storage, so do not re-enter them from NMI/IRQ while another call is in progress.

Usage example

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

This is a calling fragment. Follow the stated initialization and buffer requirements before using it in a complete program.

Expected result

This example uses __copy16 to copy 16 patterned bytes starting at offset 1. memory_check compares the entire source and destination arrays; the COPY16 row must show 016. Starting inside the buffer leaves a leading guard byte, and bytes after the payload must also remain unchanged.

The complete program resets two 264-byte buffers before each operation, then checks every source byte, every destination byte and the untouched guard regions. The expected rows are COPY WORD 257, FILL WORD 257, COPY BYTE 255, FILL BYTE 255, COPY16 016, COPY32 032, ZERO CALLS 002 and FAILED 000. WORD/BYTE describe the count width, not the size of each copied item. The numbers are produced by the sample's checks, not return values from these void functions.

The captured screens show black lettering on white for DMG, blue on white for CGB and blue on black for FC. This example verifies RAM operations; the lettering presents the results rather than demonstrating a drawing primitive. Separate emulator checks cover 88 memory cases on each platform, including constant and runtime lengths at 0, 1, 16, 17, 32, 33, 64, 65 and 255 bytes, plus 256 and 257 for the word-count forms. Physical hardware has not been tested.

__copy16: NROM
NROM

Complete example program

Build command

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\memory_intrinsics.c -I .\kitaqfc\lib -I .\kitaq-docs\samples -o .\out\memory_intrinsics.nes --mapper=nrom --nes-chr=.\kitaq-docs\samples\font.chr --no-cache --no-disasm
Implementation and source

kitaqfc/lib/intrinsics.h:27

Declaration notes (original source)
Copy exactly 16 bytes forward; both ranges must be valid and suitably non-overlapping.

kitaqfc/kitaqfc/CodeGenerator.cs:4620

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

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

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

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

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

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

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

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

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

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

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

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

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

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

                    if (IsStrictEnum(pt))
                    {
                        if (!IsSameEnum(pt, at))
                        {
                            Program.Warning(expr.Source, ErrorCode.EnumStrictMix,
                                "argument {0} to '{1}' expects __enum_strict {2}, got {3}; cast explicitly to silence",
                                i, fn, pt.Show(), at.Show());
                        }
                    }
                    else if (IsPlainInteger(pt) && IsStrictEnum(at))
                    {
                        Program.Warning(expr.Source, ErrorCode.EnumStrictMix,
                            "argument {0} to '{1}' passes __enum_strict {2} to integer parameter {3}; cast explicitly to silence",
                            i, fn, at.Show(), pt.Show());
                    }
                }
            }

            if (!ctx.InUnsafe)
                RestrictAliasLint(ctx, newFunc, newArgs, expr.Source);

            // __slice(ptr,len) helper: represent a "slice" (ptr+len) in the IR.
            // Semantics: evaluate both arguments, result type is the pointer/array (same as arg0).
            // Bounds checks are injected only with -Zcheck (see Program.CheckSliceBounds).
            // Note: keep the intrinsic name conservative to avoid collisions ("slice" is allowed only
            // when there is no declared function named "slice").
            if (newFunc.Match(Tag.Name, out string __sfname))
            {
                bool isSliceIntrinsic = (__sfname == "__slice") || (__sfname == "slice" && ctx.FindFuncParams("slice") == null);
                if (isSliceIntrinsic)
                {
                    if (newArgs.Length != 2)
                    {
                        Program.Error(expr.Source, ErrorCode.ParseError, "{0}: expected 2 arguments: __slice(ptr,len)", __sfname);
                    }
                    else
                    {
                        CType pty = InferType(ctx, newArgs[0]);
                        if (pty == null || !(pty.IsPointer || pty.IsArray))
                            Program.Error(expr.Source, ErrorCode.ExpectedType, "{0}: first argument must be a pointer or array, got: {1}", __sfname, (pty == null ? "<unknown>" : pty.Show()));

                        CType lty = InferType(ctx, newArgs[1]);
                        if (lty == null || !lty.IsInteger)
                            Program.Error(expr.Source, ErrorCode.ExpectedType, "{0}: second argument must be an integer length, got: {1}", __sfname, (lty == null ? "<unknown>" : lty.Show()));
                    }

                    return Expr.Make(Tag.Slice, newArgs[0], newArgs[1]).WithSource(expr.Source);
                }
            }

            return MakeCall(newFunc, newArgs).WithSource(expr.Source);
        }
__copy32

Copy exactly 32 bytes from src to dst, with no length argument. Use it for a fixed 32-byte record or two adjacent 16-byte tile patterns in CPU-accessible memory. It performs a forward byte copy, not a single 32-bit load or a banked transfer.

Syntax

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

Parameters

dst

Pointer to the first writable destination byte. Reserve enough consecutive bytes for the complete operation; no buffer is allocated for you.

src

Pointer to the first readable source byte. The entire range must remain accessible and unchanged throughout the copy.

Return value

None (void).

Conditions and limits

Use separate, non-overlapping source and destination ranges. These routines copy forward and do not provide overlap-safe memmove behavior; writing into the unread part of the source can corrupt later bytes.

The operation finishes before the caller continues. It neither switches ROM/RAM banks nor clips a range to a buffer boundary. Keep mappings stable, use valid memory ranges and coordinate access with interrupt handlers that use the same buffers.

On FC, dst is a CPU address, not a PPU address. Copying to CPU RAM does not update CHR, nametables or palettes; use PPU/VRAM APIs to upload that data. Generated helpers share argument/scratch storage, so do not re-enter them from NMI/IRQ while another call is in progress.

Usage example

c
memory_reset();
__copy32(memory_destination+1,memory_source+1);
memory_copy_32=memory_check(32,0);

This is a calling fragment. Follow the stated initialization and buffer requirements before using it in a complete program.

Expected result

This example uses __copy32 to copy 32 patterned bytes starting at offset 1. memory_check compares the entire source and destination arrays; the COPY32 row must show 032. Starting inside the buffer leaves a leading guard byte, and bytes after the payload must also remain unchanged.

The complete program resets two 264-byte buffers before each operation, then checks every source byte, every destination byte and the untouched guard regions. The expected rows are COPY WORD 257, FILL WORD 257, COPY BYTE 255, FILL BYTE 255, COPY16 016, COPY32 032, ZERO CALLS 002 and FAILED 000. WORD/BYTE describe the count width, not the size of each copied item. The numbers are produced by the sample's checks, not return values from these void functions.

The captured screens show black lettering on white for DMG, blue on white for CGB and blue on black for FC. This example verifies RAM operations; the lettering presents the results rather than demonstrating a drawing primitive. Separate emulator checks cover 88 memory cases on each platform, including constant and runtime lengths at 0, 1, 16, 17, 32, 33, 64, 65 and 255 bytes, plus 256 and 257 for the word-count forms. Physical hardware has not been tested.

__copy32: NROM
NROM

Complete example program

Build command

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\memory_intrinsics.c -I .\kitaqfc\lib -I .\kitaq-docs\samples -o .\out\memory_intrinsics.nes --mapper=nrom --nes-chr=.\kitaq-docs\samples\font.chr --no-cache --no-disasm
Implementation and source

kitaqfc/lib/intrinsics.h:29

Declaration notes (original source)
Copy exactly 32 bytes forward; bank selection and source lifetime belong to the caller.

kitaqfc/kitaqfc/CodeGenerator.cs:4623

csharp
case "__copy32":
                    if (!Need(2)) return true;
                    EmitFixedMemcpy(args[0], args[1], 32, ctx); return true;
                // Geometry helpers receive byte coordinates; the size array describes ABI packing, not result width.

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

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

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

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

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

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

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

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

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

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

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

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

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

                    if (IsStrictEnum(pt))
                    {
                        if (!IsSameEnum(pt, at))
                        {
                            Program.Warning(expr.Source, ErrorCode.EnumStrictMix,
                                "argument {0} to '{1}' expects __enum_strict {2}, got {3}; cast explicitly to silence",
                                i, fn, pt.Show(), at.Show());
                        }
                    }
                    else if (IsPlainInteger(pt) && IsStrictEnum(at))
                    {
                        Program.Warning(expr.Source, ErrorCode.EnumStrictMix,
                            "argument {0} to '{1}' passes __enum_strict {2} to integer parameter {3}; cast explicitly to silence",
                            i, fn, at.Show(), pt.Show());
                    }
                }
            }

            if (!ctx.InUnsafe)
                RestrictAliasLint(ctx, newFunc, newArgs, expr.Source);

            // __slice(ptr,len) helper: represent a "slice" (ptr+len) in the IR.
            // Semantics: evaluate both arguments, result type is the pointer/array (same as arg0).
            // Bounds checks are injected only with -Zcheck (see Program.CheckSliceBounds).
            // Note: keep the intrinsic name conservative to avoid collisions ("slice" is allowed only
            // when there is no declared function named "slice").
            if (newFunc.Match(Tag.Name, out string __sfname))
            {
                bool isSliceIntrinsic = (__sfname == "__slice") || (__sfname == "slice" && ctx.FindFuncParams("slice") == null);
                if (isSliceIntrinsic)
                {
                    if (newArgs.Length != 2)
                    {
                        Program.Error(expr.Source, ErrorCode.ParseError, "{0}: expected 2 arguments: __slice(ptr,len)", __sfname);
                    }
                    else
                    {
                        CType pty = InferType(ctx, newArgs[0]);
                        if (pty == null || !(pty.IsPointer || pty.IsArray))
                            Program.Error(expr.Source, ErrorCode.ExpectedType, "{0}: first argument must be a pointer or array, got: {1}", __sfname, (pty == null ? "<unknown>" : pty.Show()));

                        CType lty = InferType(ctx, newArgs[1]);
                        if (lty == null || !lty.IsInteger)
                            Program.Error(expr.Source, ErrorCode.ExpectedType, "{0}: second argument must be an integer length, got: {1}", __sfname, (lty == null ? "<unknown>" : lty.Show()));
                    }

                    return Expr.Make(Tag.Slice, newArgs[0], newArgs[1]).WithSource(expr.Source);
                }
            }

            return MakeCall(newFunc, newArgs).WithSource(expr.Source);
        }
__dot2_q8_8

Compute a unsigned two-term dot product.

Syntax

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

Parameters

a0

Unsigned word, 0..65535.

a1

Unsigned word, 0..65535.

b0

Unsigned coefficient, 0..255.

b1

Unsigned coefficient, 0..255.

Return value

Returns the low 16 bits of a0*b0+a1*b1.

Conditions and limits

On FC, q8_8/q1_7 names still use raw integer products without a fixed-point downshift. The Q1.7 functions alias their corresponding signed operations. The caller handles units; signed results are returned as u16 bit patterns.

Usage example

c
// Compare literal coefficients with values read from RAM.
result[22]=__dot2_q8_8(513,769,64,127);
v0=513;v1=769;v2=64;v3=127;
result[23]=__dot2_q8_8(v0,v1,v2,v3);

This is a calling fragment. Follow the stated initialization and buffer requirements before using it in a complete program.

Expected result

Compare literal inputs with values stored in RAM. Representative results appear as unsigned 16-bit values; negatives use two’s-complement bit patterns. GB rescales fixed-point products, while FC returns integer products, so identical inputs need not give identical results.

The excerpt stores these 16-bit values: result[22]=64959, result[23]=64959. Each literal-input calculation matches its RAM-input counterpart. The screen shows representative values; the result array holds every result.

__dot2_q8_8: NROM
NROM

Complete example program

Build command

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\batch3_math.c -I .\kitaqfc\lib -I .\kitaq-docs\samples -o .\out\batch3_math.nes --no-cache --no-disasm --mapper=nrom --nes-chr=.\kitaq-docs\samples\font.chr
Implementation and source

kitaqfc/lib/intrinsics.h:63

Declaration notes (original source)
Return a0*b0+a1*b1 modulo 65536. The FC backend inserts no Q8.8 rescaling or rounding.

kitaqfc/kitaqfc/CodeGenerator.cs:4654

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

void EmitHelperMath()
        {
            // Consume eight multiplier bits and accumulate the low product word in shared temporaries, returning A/X.
            EmitHelperStart("__mul16x8");
            EmitAsm("LDA", Imm(0));
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp1Address));
            EmitAsm("LDX", Imm(8));
            string loop = NewGeneratedLabel("mul16x8_loop");
            string skip = NewGeneratedLabel("mul16x8_skip_add");
            _assembly.Add(Expr.Make(Tag.Label, loop));
            EmitAsm("LSR", Mem(CallArgBase + 2));
            EmitAsm("BCC", Rel(skip));
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("CLC");
            EmitAsm("ADC", Mem(CallArgBase));
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp1Address));
            EmitAsm("ADC", Mem(CallArgBase + 1));
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp1Address));
            _assembly.Add(Expr.Make(Tag.Label, skip));
            EmitAsm("ASL", Mem(CallArgBase));
            EmitAsm("ROL", Mem(CallArgBase + 1));
            EmitAsm("DEX");
            EmitAsm("BNE", Rel(loop));
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("LDX", Mem(_runtimeIntrinsicTmp1Address));
            EmitAsm("RTS");

            // Zero-extend the byte multiplicand, reuse word multiplication and return its high byte.
            EmitHelperStart("__mul8x8_hi");
            EmitAsm("LDA", Mem(CallArgBase + 1));
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp2Address));
            EmitAsm("LDA", Imm(0));
            EmitAsm("STA", Mem(CallArgBase + 1));
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp2Address));
            EmitAsm("STA", Mem(CallArgBase + 2));
            EmitAsm("JSR", Abs("__mul16x8"));
            EmitAsm("TXA");
            EmitAsm("RTS");

            // Convert signed operands to magnitudes, multiply, then restore the product sign modulo 65536.
            EmitHelperStart("__smul16x8");
            EmitAsm("LDA", Imm(0));
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp2Address));
            string aPos = NewGeneratedLabel("smul_a_positive");
            EmitAsm("LDA", Mem(CallArgBase + 1));
            EmitAsm("AND", Imm(0x80));
            EmitAsm("BEQ", Rel(aPos));
            EmitAsm("LDA", Mem(CallArgBase)); EmitAsm("EOR", Imm(0xFF)); EmitAsm("CLC"); EmitAsm("ADC", Imm(1)); EmitAsm("STA", Mem(CallArgBase));
            EmitAsm("LDA", Mem(CallArgBase + 1)); EmitAsm("EOR", Imm(0xFF)); EmitAsm("ADC", Imm(0)); EmitAsm("STA", Mem(CallArgBase + 1));
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp2Address)); EmitAsm("EOR", Imm(1)); EmitAsm("STA", Mem(_runtimeIntrinsicTmp2Address));
            _assembly.Add(Expr.Make(Tag.Label, aPos));
            string bPos = NewGeneratedLabel("smul_b_positive");
            EmitAsm("LDA", Mem(CallArgBase + 2));
            EmitAsm("AND", Imm(0x80));
            EmitAsm("BEQ", Rel(bPos));
            EmitAsm("LDA", Mem(CallArgBase + 2)); EmitAsm("EOR", Imm(0xFF)); EmitAsm("CLC"); EmitAsm("ADC", Imm(1)); EmitAsm("STA", Mem(CallArgBase + 2));
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp2Address)); EmitAsm("EOR", Imm(1)); EmitAsm("STA", Mem(_runtimeIntrinsicTmp2Address));
            _assembly.Add(Expr.Make(Tag.Label, bPos));
            EmitAsm("JSR", Abs("__mul16x8"));
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("STX", Mem(_runtimeIntrinsicTmp1Address));
            string sdone = NewGeneratedLabel("smul_done");
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp2Address));
            EmitAsm("BEQ", Rel(sdone));
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp0Address)); EmitAsm("EOR", Imm(0xFF)); EmitAsm("CLC"); EmitAsm("ADC", Imm(1)); EmitAsm("STA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp1Address)); EmitAsm("EOR", Imm(0xFF)); EmitAsm("ADC", Imm(0)); EmitAsm("STA", Mem(_runtimeIntrinsicTmp1Address));
            _assembly.Add(Expr.Make(Tag.Label, sdone));
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("LDX", Mem(_runtimeIntrinsicTmp1Address));
            EmitAsm("RTS");

            EmitMacHelper("__mac16", "__mul16x8");
            EmitMacHelper("__smac16", "__smul16x8");
            EmitDot2Helper("__dot2_q8_8", "__mul16x8");
            EmitDot3Helper("__dot3_q8_8", "__mul16x8");
            EmitDot2Helper("__sdot2_q8_8", "__smul16x8");
            EmitDot3Helper("__sdot3_q8_8", "__smul16x8");
        }
__dot3_q8_8

Compute a unsigned three-term dot product.

Syntax

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

Parameters

a0

Unsigned word, 0..65535.

a1

Unsigned word, 0..65535.

a2

Unsigned word, 0..65535.

b0

Unsigned coefficient, 0..255.

b1

Unsigned coefficient, 0..255.

b2

Unsigned coefficient, 0..255.

Return value

Returns the low 16 bits of a0*b0+a1*b1+a2*b2.

Conditions and limits

On FC, q8_8/q1_7 names still use raw integer products without a fixed-point downshift. The Q1.7 functions alias their corresponding signed operations. The caller handles units; signed results are returned as u16 bit patterns.

Usage example

c
// Compare literal coefficients with values read from RAM.
result[24]=__dot3_q8_8(513,769,1025,64,127,3);
v0=513;v1=769;v2=1025;v3=64;v4=127;v5=3;
result[25]=__dot3_q8_8(v0,v1,v2,v3,v4,v5);

This is a calling fragment. Follow the stated initialization and buffer requirements before using it in a complete program.

Expected result

Compare literal inputs with values stored in RAM. Representative results appear as unsigned 16-bit values; negatives use two’s-complement bit patterns. GB rescales fixed-point products, while FC returns integer products, so identical inputs need not give identical results.

The excerpt stores these 16-bit values: result[24]=2498, result[25]=2498. Each literal-input calculation matches its RAM-input counterpart. The screen shows representative values; the result array holds every result.

__dot3_q8_8: NROM
NROM

Complete example program

Build command

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\batch3_math.c -I .\kitaqfc\lib -I .\kitaq-docs\samples -o .\out\batch3_math.nes --no-cache --no-disasm --mapper=nrom --nes-chr=.\kitaq-docs\samples\font.chr
Implementation and source

kitaqfc/lib/intrinsics.h:65

Declaration notes (original source)
Sum three unsigned word-by-byte products modulo 65536 without a final scaling shift.

kitaqfc/kitaqfc/CodeGenerator.cs:4655

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

void EmitHelperMath()
        {
            // Consume eight multiplier bits and accumulate the low product word in shared temporaries, returning A/X.
            EmitHelperStart("__mul16x8");
            EmitAsm("LDA", Imm(0));
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp1Address));
            EmitAsm("LDX", Imm(8));
            string loop = NewGeneratedLabel("mul16x8_loop");
            string skip = NewGeneratedLabel("mul16x8_skip_add");
            _assembly.Add(Expr.Make(Tag.Label, loop));
            EmitAsm("LSR", Mem(CallArgBase + 2));
            EmitAsm("BCC", Rel(skip));
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("CLC");
            EmitAsm("ADC", Mem(CallArgBase));
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp1Address));
            EmitAsm("ADC", Mem(CallArgBase + 1));
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp1Address));
            _assembly.Add(Expr.Make(Tag.Label, skip));
            EmitAsm("ASL", Mem(CallArgBase));
            EmitAsm("ROL", Mem(CallArgBase + 1));
            EmitAsm("DEX");
            EmitAsm("BNE", Rel(loop));
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("LDX", Mem(_runtimeIntrinsicTmp1Address));
            EmitAsm("RTS");

            // Zero-extend the byte multiplicand, reuse word multiplication and return its high byte.
            EmitHelperStart("__mul8x8_hi");
            EmitAsm("LDA", Mem(CallArgBase + 1));
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp2Address));
            EmitAsm("LDA", Imm(0));
            EmitAsm("STA", Mem(CallArgBase + 1));
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp2Address));
            EmitAsm("STA", Mem(CallArgBase + 2));
            EmitAsm("JSR", Abs("__mul16x8"));
            EmitAsm("TXA");
            EmitAsm("RTS");

            // Convert signed operands to magnitudes, multiply, then restore the product sign modulo 65536.
            EmitHelperStart("__smul16x8");
            EmitAsm("LDA", Imm(0));
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp2Address));
            string aPos = NewGeneratedLabel("smul_a_positive");
            EmitAsm("LDA", Mem(CallArgBase + 1));
            EmitAsm("AND", Imm(0x80));
            EmitAsm("BEQ", Rel(aPos));
            EmitAsm("LDA", Mem(CallArgBase)); EmitAsm("EOR", Imm(0xFF)); EmitAsm("CLC"); EmitAsm("ADC", Imm(1)); EmitAsm("STA", Mem(CallArgBase));
            EmitAsm("LDA", Mem(CallArgBase + 1)); EmitAsm("EOR", Imm(0xFF)); EmitAsm("ADC", Imm(0)); EmitAsm("STA", Mem(CallArgBase + 1));
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp2Address)); EmitAsm("EOR", Imm(1)); EmitAsm("STA", Mem(_runtimeIntrinsicTmp2Address));
            _assembly.Add(Expr.Make(Tag.Label, aPos));
            string bPos = NewGeneratedLabel("smul_b_positive");
            EmitAsm("LDA", Mem(CallArgBase + 2));
            EmitAsm("AND", Imm(0x80));
            EmitAsm("BEQ", Rel(bPos));
            EmitAsm("LDA", Mem(CallArgBase + 2)); EmitAsm("EOR", Imm(0xFF)); EmitAsm("CLC"); EmitAsm("ADC", Imm(1)); EmitAsm("STA", Mem(CallArgBase + 2));
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp2Address)); EmitAsm("EOR", Imm(1)); EmitAsm("STA", Mem(_runtimeIntrinsicTmp2Address));
            _assembly.Add(Expr.Make(Tag.Label, bPos));
            EmitAsm("JSR", Abs("__mul16x8"));
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("STX", Mem(_runtimeIntrinsicTmp1Address));
            string sdone = NewGeneratedLabel("smul_done");
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp2Address));
            EmitAsm("BEQ", Rel(sdone));
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp0Address)); EmitAsm("EOR", Imm(0xFF)); EmitAsm("CLC"); EmitAsm("ADC", Imm(1)); EmitAsm("STA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp1Address)); EmitAsm("EOR", Imm(0xFF)); EmitAsm("ADC", Imm(0)); EmitAsm("STA", Mem(_runtimeIntrinsicTmp1Address));
            _assembly.Add(Expr.Make(Tag.Label, sdone));
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("LDX", Mem(_runtimeIntrinsicTmp1Address));
            EmitAsm("RTS");

            EmitMacHelper("__mac16", "__mul16x8");
            EmitMacHelper("__smac16", "__smul16x8");
            EmitDot2Helper("__dot2_q8_8", "__mul16x8");
            EmitDot3Helper("__dot3_q8_8", "__mul16x8");
            EmitDot2Helper("__sdot2_q8_8", "__smul16x8");
            EmitDot3Helper("__sdot3_q8_8", "__smul16x8");
        }
__exp_pad_read1

__exp_pad_read1 calls exactly the same compiler helper as __pad_read1_d1. It reads the expansion D1 signal at $4016 with the same latch sequence, eight-bit result and side effects. This name identifies the expansion-input purpose; it does not enable another device mode or combine D0 and D1.

Syntax

c
u8 __exp_pad_read1(void);

Return value

An 8-bit held-button mask in NES raw order. Each set bit identifies a pressed button; 0 means none. It is not a normalized Boolean.

Conditions and limits

NES raw layout: A=0x01, B=0x02, SELECT=0x04, START=0x08, UP=0x10, DOWN=0x20, LEFT=0x40, RIGHT=0x80. A set bit means pressed. Use these values with nes_pad_* and raw __pad_* results, not the shared BTN_* masks from input.h.

Use a peripheral that supplies a compatible eight-bit serial pad stream on the selected D1 line. The function does not detect the attached device or implement every expansion-device protocol. Each call strobes both controller streams; finish a complete read before starting another. Results are fresh snapshots and do not update the input_* or nes_pad1_* caches.

The published KUROSAKI controller model returns only D0 plus fixed bit 6. D1 and the microphone bit are therefore always 0 in that emulator, even while ordinary controller buttons are held. The supplied run verifies compilation, ordinary-button input and this zero-signal observation. Nonzero peripheral signals, electrical timing and real-device compatibility remain unverified; a zero here is not proof that an attached device works or that these functions cannot read it on hardware.

Usage example

c
expansion_first=__exp_pad_read1(); // Same helper as __pad_read1_d1.

This is a calling fragment. Follow the stated initialization and buffer requirements before using it in a complete program.

Expected result

This program contrasts ordinary D0 input with expansion D1 and the microphone's digital level. Hold A on controller 1 and B on controller 2 until the snapshot is captured. NORMAL P1 and NORMAL P2 display 001 and 002. In the supplied KUROSAKI run, D1 PORT 1, D1 PORT 2, EXP ALIAS 1, EXP ALIAS 2, MIC LEVEL and MIC DIRECT all display 000 because those signal lines are not emulated. The displayed values then stay fixed. With suitable peripherals and an implementation of their signal lines, the expansion and microphone values depend on the actual inputs; consult the bit layouts above rather than expecting these zeros.

The published KUROSAKI controller model returns only D0 plus fixed bit 6. D1 and the microphone bit are therefore always 0 in that emulator, even while ordinary controller buttons are held. The supplied run verifies compilation, ordinary-button input and this zero-signal observation. Nonzero peripheral signals, electrical timing and real-device compatibility remain unverified; a zero here is not proof that an attached device works or that these functions cannot read it on hardware.

__exp_pad_read1: NROM
NROM

Complete example program

Build command

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\expansion_input.c -I .\kitaq-docs\samples -I .\kitaqfc\lib -o .\out\expansion_input.nes --mapper=nrom --nes-chr=.\kitaq-docs\samples\font.chr --no-cache --no-disasm
Implementation and source

kitaqfc/lib/intrinsics.h:227

Declaration notes (original source)
Alias the port-1 D1 controller reader; the attached device must use the expected serial protocol.
__exp_pad_read2

__exp_pad_read2 calls exactly the same compiler helper as __pad_read2_d1. It reads the expansion D1 signal at $4017 with the same latch sequence, eight-bit result and side effects. This name identifies the expansion-input purpose; it does not enable another device mode or combine D0 and D1.

Syntax

c
u8 __exp_pad_read2(void);

Return value

An 8-bit held-button mask in NES raw order. Each set bit identifies a pressed button; 0 means none. It is not a normalized Boolean.

Conditions and limits

NES raw layout: A=0x01, B=0x02, SELECT=0x04, START=0x08, UP=0x10, DOWN=0x20, LEFT=0x40, RIGHT=0x80. A set bit means pressed. Use these values with nes_pad_* and raw __pad_* results, not the shared BTN_* masks from input.h.

Use a peripheral that supplies a compatible eight-bit serial pad stream on the selected D1 line. The function does not detect the attached device or implement every expansion-device protocol. Each call strobes both controller streams; finish a complete read before starting another. Results are fresh snapshots and do not update the input_* or nes_pad1_* caches.

The published KUROSAKI controller model returns only D0 plus fixed bit 6. D1 and the microphone bit are therefore always 0 in that emulator, even while ordinary controller buttons are held. The supplied run verifies compilation, ordinary-button input and this zero-signal observation. Nonzero peripheral signals, electrical timing and real-device compatibility remain unverified; a zero here is not proof that an attached device works or that these functions cannot read it on hardware.

Usage example

c
expansion_second=__exp_pad_read2(); // Same helper as __pad_read2_d1.

This is a calling fragment. Follow the stated initialization and buffer requirements before using it in a complete program.

Expected result

This program contrasts ordinary D0 input with expansion D1 and the microphone's digital level. Hold A on controller 1 and B on controller 2 until the snapshot is captured. NORMAL P1 and NORMAL P2 display 001 and 002. In the supplied KUROSAKI run, D1 PORT 1, D1 PORT 2, EXP ALIAS 1, EXP ALIAS 2, MIC LEVEL and MIC DIRECT all display 000 because those signal lines are not emulated. The displayed values then stay fixed. With suitable peripherals and an implementation of their signal lines, the expansion and microphone values depend on the actual inputs; consult the bit layouts above rather than expecting these zeros.

The published KUROSAKI controller model returns only D0 plus fixed bit 6. D1 and the microphone bit are therefore always 0 in that emulator, even while ordinary controller buttons are held. The supplied run verifies compilation, ordinary-button input and this zero-signal observation. Nonzero peripheral signals, electrical timing and real-device compatibility remain unverified; a zero here is not proof that an attached device works or that these functions cannot read it on hardware.

__exp_pad_read2: NROM
NROM

Complete example program

Build command

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\expansion_input.c -I .\kitaq-docs\samples -I .\kitaqfc\lib -o .\out\expansion_input.nes --mapper=nrom --nes-chr=.\kitaq-docs\samples\font.chr --no-cache --no-disasm
Implementation and source

kitaqfc/lib/intrinsics.h:229

Declaration notes (original source)
Alias the port-2 D1 controller reader; this is not an expansion-device discovery operation.
__far_memcpy

Temporarily select a ROM bank, copy len bytes from src to dst in increasing address order, then restore the compiler’s saved ROM bank. __farmemcpy and __far_memcpy use the same implementation on this platform.

Syntax

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

Parameters

dst

Destination in writable memory. Reserve enough space for every byte copied. Except for a zero-byte transfer, a null or invalid destination is not checked.

bank

8-bit bank selector for the source ROM mapping. Use a bank supported by the cartridge and the compiler’s bank layout; this argument is separate from the source address.

src

16-bit CPU address of source bytes in the selected ROM mapping. The FC declaration takes u16; cast a resource pointer with (u16)pointer.

len

Byte count, 0–65535. Zero performs no memory copy, but the bank-selection and restoration steps still run.

Return value

None (void).

Conditions and limits

Keep the full source range in the intended ROM mapping and the full destination range in writable memory. Copying does not select the next bank at a window boundary, handle overlapping ranges like memmove, or provide VRAM-safe writes. Use the graphics transfer APIs for VRAM.

The samples place calling code in fixed bank 0 so source-bank changes cannot unmap the code being executed. These transfers are synchronous. Do not interrupt a transfer with another bank-changing copy or assume the helper makes a whole application interrupt-safe.

Usage example

c
__far_memcpy(buffer,payload_bank,(u16)payload_pointer,32);
expect(*asset_bank_marker==0xA1 && bank_get_current()==1,27);
for(i=0;i<32;i++) expect(buffer[(__safe_index u8)i]==expected_shape_byte(i),28);
buffer[32]=0x55;
__far_memcpy(buffer+32,2,(u16)asset_shapes,0);
expect(buffer[32]==0x55 && *asset_bank_marker==0xA1,30);

This is a calling fragment. Follow the stated initialization and buffer requirements before using it in a complete program.

Expected result

Copy two patterns, 32 bytes, from payload bank 2 while bank 1 is initially selected. The program compares every copied byte, checks that bank 1 is restored and verifies that a zero-length copy leaves a 0x55 sentinel unchanged.

Expected image: a red row starts at pixel (16,32) and spans 72×8 pixels. It repeats an 8×8 triangle, an 8×8 square outline with a one-pixel border, and a tile with its left four columns filled, three times. The triangle grows from one pixel at the top left to eight pixels on its bottom row.

On FC, a blue triangle and outline at (16,64) show __far_memcpy; a green pair at (16,96) shows asset_load_raw followed by a CHR upload. Each pair occupies 16×8 pixels on a black background. These rows demonstrate copied image bytes, not merely numeric status values.

Expected status: FAILED CHECKS 000 and FIRST FAILURE 00. The verification runs 300 frames and compares text, shape masks and colors against these expectations. This is emulator evidence, not a physical-hardware test.

__far_memcpy: SUROM512
SUROM512

Complete example program

Build command

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\asset_banks.c -I .\kitaqfc\lib -I .\kitaq-docs\samples -o .\out\asset_banks.nes --mapper=mmc1 --board=surom512 --no-cache --no-disasm
Implementation and source

kitaqfc/lib/intrinsics.h:86

Declaration notes (original source)
Switch to the source bank, copy len bytes forward and restore the previous bank.
Keep destination RAM and helper code accessible throughout; overlapping copies are not memmove.
__farcall

Call a named function with no arguments. The FC compiler uses the function's assigned bank to choose a direct call or a bank-restoring helper. It evaluates the bank argument, but the target's placement determines the selected bank.

Syntax

c
__farcall(bank, function_name);

Parameters

bank

An unsigned 8-bit ROM-bank number. For a call, it must agree with the function's placement; for a read, it selects the source mapping. Use the target cartridge's valid range.

func

The declared name of a function with no parameters. A variable holding a function address is not accepted by this named-call intrinsic. The bank argument is not forwarded as a callback parameter.

Return value

The called function's result type determines the result. A void callback yields no value; u8 and u16 callbacks return their scalar value. The displayed sample uses void callbacks.

Conditions and limits

These calls do not forward user arguments or make the callback interrupt-reentrant. Keep the callback's own data, stack and interrupt use valid. The bank-restoring call paths are tested with nested calls and repeated calls; physical hardware and arbitrary interrupt interleavings are not covered by this example.

FC switch-bank numbers are logical compiler numbers. Zero denotes common ROM, not a switch target. The helper normalizes zero to bank 1. On SUROM512, only 1–30 are valid: an out-of-range constant passed directly to the switching intrinsic is a build error; an invalid runtime value falls back to 1 and sets the mapper-fault state. Read common ROM directly instead of requesting a bank-zero switch.

Usage example

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

This is a calling fragment. Follow the stated initialization and buffer requirements before using it in a complete program.

Expected result

At this point in the complete program, __farcall raises callback_count to 2. The callback reads D3 from bank 3. A later near read of B2 from home2 verifies that execution returned with bank 2 mapped.

The complete program teaches how a bank number and near address identify data, how callbacks cross banks, and how to distinguish the library's record from the actual mapping. All displayed numbers are hexadecimal. BANK IN and BANK OUT must show 02, BYTE HEX D3, WORD HEX 5C7A, and both FAILED CHECKS and FIRST FAILURE 00. CGB and FC display blue text; DMG displays black text.

CALL COUNT is 02: the far_call macro and __farcall each invoke the callback once.

__farcall: MMC3
MMC3

Complete example program

Build command

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

kitaqfc/lib/intrinsics.h:83

Declaration notes (original source)
Call a declared zero-argument function; placement selects its bank and its declared type determines the result.

kitaqfc/kitaqfc/CodeGenerator.cs:4131

csharp
if (string.Equals(funcName, "__farcall", StringComparison.Ordinal))
            {
                Expr[] farArgs = args ?? Array.Empty<Expr>();
                if (farArgs.Length != 2)
                {
                    Program.Error("error KQ0000: __farcall(bank, func) expects exactly 2 arguments.");
                    return;
                }

                string targetName;
                if (!farArgs[1].Match(Tag.Name, out targetName))
                {
                    Program.Error("error KQ0000: __farcall(bank, func) requires the 2nd argument to be a function name.");
                    return;
                }
                // This intrinsic supplies no callback arguments. A variable name
                // is not a static function target, even when its value is a pointer.
                if (!_functionParameters.TryGetValue(targetName, out FieldInfo[] farParameters))
                {
                    Program.Error("error KQFC2610: __farcall requires a declared function name; use an ordinary typed call for a function pointer.");
                    return;
                }
                if (farParameters.Length != 0)
                {
                    Program.Error("error KQFC2610: __farcall requires a callback with no parameters.");
                    return;
                }
                if (Program.NesMapperProfile.IsSurom512 &&
                    TryEvaluateConstant(farArgs[0], out int constantFarBank) &&
                    (constantFarBank < 1 || constantFarBank > 30))
                {
                    Program.Error("error KQFC2603 KQFC-SUROM-BANK-RANGE: __farcall constant bank {0} is outside 1..30; common bank 0 is not a switch target.", constantFarBank);
                    return;
                }

                // Evaluate the requested far-call bank byte; the following dispatch uses the target function bank metadata.
                EmitLoadValue(farArgs[0], ctx, 1);

                int callerBank = ctx == null ? 0 : ctx.Bank;
                int calleeBank = GetFunctionBank(targetName);
                if (ShouldUseFdsOverlayThunk(callerBank, calleeBank))
                {
                    string fdsThunkName = EnsureFdsOverlayThunk(targetName, calleeBank);
                    EmitAsm("JSR", Abs(fdsThunkName));
                    RecordCallEdge(targetName, calleeBank, "fds_overlay_thunk", viaThunk: true, viaFarcall: true);
                    return;
                }
                bool bankedPrg = Program.NesMapperProfile.SupportsPrgBanking || Program.NesMapperProfile.UsesDuplicatedCommonBank;
                bool directSafe = !bankedPrg || calleeBank == 0 || calleeBank == callerBank;
                if (directSafe)
                {
                    EmitAsm("JSR", Abs(targetName));
                    RecordCallEdge(targetName, calleeBank, "direct", viaThunk: false, viaFarcall: true);
                    return;
                }

                string thunkName = EnsureBankThunk(targetName, calleeBank);
                EmitAsm("JSR", Abs(thunkName));
                RecordCallEdge(targetName, calleeBank, "bank_thunk", viaThunk: true, viaFarcall: true);
                return;
            }
__farmemcpy

__farmemcpy is an alternative spelling of the FC intrinsic __far_memcpy. It takes the same arguments and uses the same copy and bank-restoration routine; choosing this spelling does not select a different transfer method or speed. The example uses it to read tile patterns from ROM into RAM.

Temporarily select a ROM bank, copy len bytes from src to dst in increasing address order, then restore the compiler’s saved ROM bank. __farmemcpy and __far_memcpy use the same implementation on this platform.

Syntax

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

Parameters

dst

Destination in writable memory. Reserve enough space for every byte copied. Except for a zero-byte transfer, a null or invalid destination is not checked.

bank

8-bit bank selector for the source ROM mapping. Use a bank supported by the cartridge and the compiler’s bank layout; this argument is separate from the source address.

src

16-bit CPU address of source bytes in the selected ROM mapping. The FC declaration takes u16; cast a resource pointer with (u16)pointer.

len

Byte count, 0–65535. Zero performs no memory copy, but the bank-selection and restoration steps still run.

Return value

None (void).

Conditions and limits

The compiler registers this spelling directly, so it can be called without a separate declaration in intrinsics.h. Arguments are a RAM pointer, an 8-bit bank number, a 16-bit CPU address and a 16-bit byte count, in that order. src is not an offset from the start of the ROM file.

Keep the full source range in the intended ROM mapping and the full destination range in writable memory. Copying does not select the next bank at a window boundary, handle overlapping ranges like memmove, or provide VRAM-safe writes. Use the graphics transfer APIs for VRAM.

The samples place calling code in fixed bank 0 so source-bank changes cannot unmap the code being executed. These transfers are synchronous. Do not interrupt a transfer with another bank-changing copy or assume the helper makes a whole application interrupt-safe.

When len is zero, no destination bytes are changed, but bank selection and restoration still take place. The function returns no value.

__far_memcpy

Usage example

c
__farmemcpy(buffer,payload_bank,(u16)payload_pointer,32);
expect(*asset_bank_marker==0xA1 && bank_get_current()==1,27);
for(i=0;i<32;i++) expect(buffer[(__safe_index u8)i]==expected_shape_byte(i),28);
buffer[32]=0x55;
__farmemcpy(buffer+32,2,(u16)asset_shapes,0);
expect(buffer[32]==0x55 && *asset_bank_marker==0xA1,30);

This is a calling fragment. Follow the stated initialization and buffer requirements before using it in a complete program.

Expected result

Copy two patterns, 32 bytes, from payload bank 2 while bank 1 is initially selected. The program compares every copied byte, checks that bank 1 is restored and verifies that a zero-length copy leaves a 0x55 sentinel unchanged.

Expected image: a red row starts at pixel (16,32) and spans 72×8 pixels. It repeats an 8×8 triangle, an 8×8 square outline with a one-pixel border, and a tile with its left four columns filled, three times. The triangle grows from one pixel at the top left to eight pixels on its bottom row.

The blue triangle and square outline at pixel (16,64) show the bytes copied by __farmemcpy. The green pair below at (16,96) comes from asset_load_raw. Each pair occupies 16×8 pixels on black; the copied bytes are uploaded to CHR RAM for display.

Expected status: FAILED CHECKS 000 and FIRST FAILURE 00. The verification runs 300 frames and compares text, shape masks and colors against these expectations. This is emulator evidence, not a physical-hardware test.

__farmemcpy: SUROM512 — The blue triangle and square outline at pixel (16,64) show the bytes copied by __farmemcpy. The green pair below at (16,96) comes from asset_load_raw. Each pair occupies 16×8 pixels on black; the copied bytes are uploaded to CHR RAM for display.
SUROM512 — The blue triangle and square outline at pixel (16,64) show the bytes copied by __farmemcpy. The green pair below at (16,96) comes from asset_load_raw. Each pair occupies 16×8 pixels on black; the copied bytes are uploaded to CHR RAM for display.

Complete example program

Build command

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\far_copy_alias.c -I .\kitaqfc\lib -I .\kitaq-docs\samples -o .\out\far_copy_alias.nes --mapper=mmc1 --board=surom512 --no-cache --no-disasm
Implementation and source

kitaqfc/kitaqfc/CodeGenerator.cs:264

kitaqfc/kitaqfc/CodeGenerator.cs:4668

csharp
case "__far_memcpy":
                case "__farmemcpy":
                    return EmitKitaqfcHelperCall(funcName, args, ctx, new[] {2,1,2,2}, "__far_memcpy");
__farpeek16

Read a little-endian 16-bit value at addr and addr+1 under a temporary bank selection, then restore the bank. This is the intrinsic used by far_data_read16.

Syntax

c
u16 __farpeek16(u8 bank, u16 addr);

Parameters

bank

An unsigned 8-bit ROM-bank number. For a call, it must agree with the function's placement; for a read, it selects the source mapping. Use the target cartridge's valid range.

addr

16-bit CPU address of source bytes in the selected ROM mapping. The FC declaration takes u16; cast a resource pointer with (u16)pointer.

Return value

The little-endian word read, as u16 (0–65535).

Conditions and limits

The complete byte or word must be readable in the selected CPU mapping. These reads do not validate pointers, clip addresses or advance automatically to another ROM bank at a window boundary. A word read needs two valid consecutive bytes.

Far reads and copies are synchronous and use shared compiler scratch. Do not let an interrupt run another banked transfer over the same scratch while one is active. On GB, the copy helper also uses one shared HRAM slot for the saved bank. The sample performs these operations from fixed code with no competing banked-transfer handler.

FC switch-bank numbers are logical compiler numbers. Zero denotes common ROM, not a switch target. The helper normalizes zero to bank 1. On SUROM512, only 1–30 are valid: an out-of-range constant passed directly to the switching intrinsic is a build error; an invalid runtime value falls back to 1 and sets the mapper-fault state. Read common ROM directly instead of requesting a bank-zero switch.

Usage example

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

This is a calling fragment. Follow the stated initialization and buffer requirements before using it in a complete program.

Expected result

This excerpt uses __farpeek16 to read payload+1 in bank 3 and checks 5C7A. In the full program, the near read from home2 also confirms that the bank-2 mapping is restored.

The complete program teaches how a bank number and near address identify data, how callbacks cross banks, and how to distinguish the library's record from the actual mapping. All displayed numbers are hexadecimal. BANK IN and BANK OUT must show 02, BYTE HEX D3, WORD HEX 5C7A, and both FAILED CHECKS and FIRST FAILURE 00. CGB and FC display blue text; DMG displays black text.

CALL COUNT is 02: the far_call macro and __farcall each invoke the callback once.

__farpeek16: MMC3
MMC3

Complete example program

Build command

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

kitaqfc/lib/intrinsics.h:90

Declaration notes (original source)
Temporarily select bank and read a little-endian word, then restore the previous mapping.

kitaqfc/kitaqfc/CodeGenerator.cs:8698

csharp
EmitHelperStart("__farpeek16");
            // args: bank, addr16. Return A=lo, X=hi.
            EmitAsm("LDA", Mem(_runtimeCurrentBankAddress));
            EmitAsm("PHA");
            EmitAsm("LDA", Mem(CallArgBase));
            EmitAsm("JSR", Abs("__kq_prg_set_bank_a"));
            EmitAsm("LDY", Imm(0));
            EmitAsm("LDA", IndY(CallArgBase + 1));
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("INY");
            EmitAsm("LDA", IndY(CallArgBase + 1));
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp1Address));
            EmitAsm("PLA");
            EmitAsm("JSR", Abs("__kq_prg_set_bank_a"));
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("LDX", Mem(_runtimeIntrinsicTmp1Address));
            EmitAsm("RTS");

            // Consume the word count while advancing source/destination pointers; zero count still restores the saved bank.
__farpeek8

Temporarily select the supplied bank, read one byte at the CPU address and restore the previous bank. This is the intrinsic used by far_data_read8.

Syntax

c
u8 __farpeek8(u8 bank, u16 addr);

Parameters

bank

An unsigned 8-bit ROM-bank number. For a call, it must agree with the function's placement; for a read, it selects the source mapping. Use the target cartridge's valid range.

addr

16-bit CPU address of source bytes in the selected ROM mapping. The FC declaration takes u16; cast a resource pointer with (u16)pointer.

Return value

The byte read, as u8 (0–255).

Conditions and limits

The complete byte or word must be readable in the selected CPU mapping. These reads do not validate pointers, clip addresses or advance automatically to another ROM bank at a window boundary. A word read needs two valid consecutive bytes.

Far reads and copies are synchronous and use shared compiler scratch. Do not let an interrupt run another banked transfer over the same scratch while one is active. On GB, the copy helper also uses one shared HRAM slot for the saved bank. The sample performs these operations from fixed code with no competing banked-transfer handler.

FC switch-bank numbers are logical compiler numbers. Zero denotes common ROM, not a switch target. The helper normalizes zero to bank 1. On SUROM512, only 1–30 are valid: an out-of-range constant passed directly to the switching intrinsic is a build error; an invalid runtime value falls back to 1 and sets the mapper-fault state. Read common ROM directly instead of requesting a bank-zero switch.

Usage example

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

This is a calling fragment. Follow the stated initialization and buffer requirements before using it in a complete program.

Expected result

This excerpt uses __farpeek8 to read payload in bank 3 and checks D3. In the full program, the near read from home2 also confirms that the bank-2 mapping is restored.

The complete program teaches how a bank number and near address identify data, how callbacks cross banks, and how to distinguish the library's record from the actual mapping. All displayed numbers are hexadecimal. BANK IN and BANK OUT must show 02, BYTE HEX D3, WORD HEX 5C7A, and both FAILED CHECKS and FIRST FAILURE 00. CGB and FC display blue text; DMG displays black text.

CALL COUNT is 02: the far_call macro and __farcall each invoke the callback once.

__farpeek8: MMC3
MMC3

Complete example program

Build command

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

kitaqfc/lib/intrinsics.h:88

Declaration notes (original source)
Temporarily select bank, read one byte at addr and restore the prior bank mapping.

kitaqfc/kitaqfc/CodeGenerator.cs:8683

csharp
EmitHelperStart("__farpeek8");
            // args: bank, addr16. Return A.
            EmitAsm("LDA", Mem(_runtimeCurrentBankAddress));
            EmitAsm("PHA");
            EmitAsm("LDA", Mem(CallArgBase));
            EmitAsm("JSR", Abs("__kq_prg_set_bank_a"));
            EmitAsm("LDY", Imm(0));
            EmitAsm("LDA", IndY(CallArgBase + 1));
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("PLA");
            EmitAsm("JSR", Abs("__kq_prg_set_bank_a"));
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("RTS");

            // Retain both fetched bytes across bank restoration and return the word in A/X.
__fkb_detect

Probe the keyboard detection pattern before interpreting key presses.

Syntax

c
u8 __fkb_detect(void);

Return value

Returns 1 if row 9, column 0 reads 0x1E and the disabled matrix reads zero; otherwise 0. This is a signal-pattern heuristic, not a device identification code.

Conditions and limits

These APIs access $4016/$4017 directly. Do not interleave interrupt-driven gamepad, keyboard or serial operations on the same ports. Controller shift positions can also be affected. The sample disables NMI while capturing input, then enables it when initializing the display.

Writes 5 to $4016 to reset row 0, then advances rows using high-to-low transitions of the column-select bit. The generated code includes 12 CPU cycles of NOPs after reset and 50 after column selection, in addition to surrounding instructions. It writes zero on exit to disable the matrix.

A separate regression checks rows 0..9, both columns, the nine-row scan, settling delays and final port state from executed CPU bus traces. It does not inject physical key signals.

Row 9 has no keys. A failed condition returns zero immediately. KUROSAKI returns zero on the first read, so detection fails there.

These images run the downloadable ROMs unchanged in KUROSAKI. Its current input bus models gamepads but not keyboard, light-gun or serial-adapter signals. The external input bits read as zero, so the captures do not verify those devices. Physical key input, optical sensing and communication remain untested.

NESdev: Family BASIC Keyboard

Usage example

c
#include "fc_ports_scene.h"
void main(void){
    u8 row;
    __irq_disable();__ppu_ctrl_set(0);port_phase=1;
    port_result[0]=__fkb_detect();
    __fkb_scan(key_matrix);
    port_result[1]=__fkb_read_row_col(6,0);
    port_result[2]=0;
    if(port_result[0] && !(port_result[1]&16))port_result[2]=1;
    port_phase=2;port_result[31]=165;
    m_init();m_text(2,2,"KEYBOARD STARTUP SNAPSHOT");m_wait();
    m_text(2,4,"DEVICE DETECT");port_number(25,4,port_result[0]);m_wait();
    m_text(2,6,"ROW 6 COL 0");port_number(25,6,port_result[1]);m_wait();
    m_text(2,8,"A KEY - GUARDED");port_number(25,8,port_result[2]);m_wait();
    m_text(2,10,"ROW   COL0  COL1  (HEX)");m_wait();
    for(row=0;row<9;row++){
        m_put(3,12+row,'0'+row);port_hex(9,12+row,key_matrix[row*2]);port_hex(15,12+row,key_matrix[row*2+1]);m_wait();
    }
    m_text(2,23,"RAW MATRIX - NOT ASCII");m_wait();
    while(1){m_wait();}
}

This is a calling fragment. Follow the stated initialization and buffer requirements before using it in a complete program.

Expected result

In this KUROSAKI run, DEVICE DETECT, ROW 6 COL 0 and A KEY - GUARDED are all 000; all nine COL0/COL1 pairs are 00. A is row 6, column 0, bit 4, but failed detection prevents the raw zero from being reported as a key press. The table is a single startup capture.

__fkb_detect: KEYBOARD-DEFAULT — In this KUROSAKI run, DEVICE DETECT, ROW 6 COL 0 and A KEY - GUARDED are all 000; all nine COL0/COL1 pairs are 00. A is row 6, column 0, bit 4, but failed detection prevents the raw zero from being reported as a key press. The table is a single startup capture.
KEYBOARD-DEFAULT — In this KUROSAKI run, DEVICE DETECT, ROW 6 COL 0 and A KEY - GUARDED are all 000; all nine COL0/COL1 pairs are 00. A is row 6, column 0, bit 4, but failed detection prevents the raw zero from being reported as a key press. The table is a single startup capture.

Complete example program

Build command

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\peripheral_keyboard.c -I .\kitaqfc\lib -I .\kitaq-docs\samples -o .\out\peripheral_keyboard.nes --mapper=nrom --nes-chr=.\kitaq-docs\samples\font.chr --no-cache --no-disasm
.\kurosaki\kurosaki.exe run .\out\peripheral_keyboard.nes --frames 120 --png .\out\peripheral_keyboard.png
Implementation and source

kitaqfc/lib/intrinsics.h:259

Declaration notes (original source)
Family BASIC Keyboard. __fkb_scan() writes 18 bytes: 9 rows x 2 columns,
each byte containing raw $4017 & 0x1E for that row/column.

Probe selected-row and disabled-matrix bit patterns and return 0 or 1; this is a heuristic detection.

kitaqfc/kitaqfc/CodeGenerator.cs:7842

csharp
EmitHelperStart("__fkb_detect");
            EmitAsm("LDA", Imm(0x09));
            EmitAsm("STA", Mem(CallArgBase));
            EmitAsm("LDA", Imm(0));
            EmitAsm("STA", Mem(CallArgBase + 1));
            EmitAsm("JSR", Abs("__fkb_read_row_col"));
            EmitAsm("CMP", Imm(0x1E));
            string no = NewGeneratedLabel("fkb_no");
            EmitAsm("BNE", Rel(no));
            EmitAsm("LDA", Imm(0));
            EmitAsm("STA", Mem(0x4016));
            EmitAsm("LDA", Mem(0x4017));
            EmitAsm("AND", Imm(0x1E));
            EmitAsm("BNE", Rel(no));
            EmitAsm("LDA", Imm(1));
            EmitAsm("RTS");
            _assembly.Add(Expr.Make(Tag.Label, no));
            EmitAsm("LDA", Imm(0));
            EmitAsm("RTS");
__fkb_read_row_col

Read one selected row and column group without an 18-byte buffer when only a particular group is needed.

Syntax

c
u8 __fkb_read_row_col(u8 row, u8 col);

Parameters

row

Use 0..8 for key rows or 9 for the keyless detection row. No runtime range check is generated.

col

0 or 1. Only the low bit is used: even values select group 0 and odd values group 1.

Return value

Returns $4017 & 0x1E without packing the bits down.

Conditions and limits

These APIs access $4016/$4017 directly. Do not interleave interrupt-driven gamepad, keyboard or serial operations on the same ports. Controller shift positions can also be affected. The sample disables NMI while capturing input, then enables it when initializing the display.

Key groups are raw bits 1..4: a pressed key clears its bit, and an idle group is 0x1E. They are neither ASCII text nor a bitmap with one meaning pressed. A disconnected device can also read zero; do not interpret that as every key being held without checking presence.

Writes 5 to $4016 to reset row 0, then advances rows using high-to-low transitions of the column-select bit. The generated code includes 12 CPU cycles of NOPs after reset and 50 after column selection, in addition to surrounding instructions. It writes zero on exit to disable the matrix.

A separate regression checks rows 0..9, both columns, the nine-row scan, settling delays and final port state from executed CPU bus traces. It does not inject physical key signals.

Resets to row 0 and advances afresh for every call; it does not reuse the previous call’s row position.

These images run the downloadable ROMs unchanged in KUROSAKI. Its current input bus models gamepads but not keyboard, light-gun or serial-adapter signals. The external input bits read as zero, so the captures do not verify those devices. Physical key input, optical sensing and communication remain untested.

NESdev: Family BASIC Keyboard

Usage example

c
#include "fc_ports_scene.h"
void main(void){
    u8 row;
    __irq_disable();__ppu_ctrl_set(0);port_phase=1;
    port_result[0]=__fkb_detect();
    __fkb_scan(key_matrix);
    port_result[1]=__fkb_read_row_col(6,0);
    port_result[2]=0;
    if(port_result[0] && !(port_result[1]&16))port_result[2]=1;
    port_phase=2;port_result[31]=165;
    m_init();m_text(2,2,"KEYBOARD STARTUP SNAPSHOT");m_wait();
    m_text(2,4,"DEVICE DETECT");port_number(25,4,port_result[0]);m_wait();
    m_text(2,6,"ROW 6 COL 0");port_number(25,6,port_result[1]);m_wait();
    m_text(2,8,"A KEY - GUARDED");port_number(25,8,port_result[2]);m_wait();
    m_text(2,10,"ROW   COL0  COL1  (HEX)");m_wait();
    for(row=0;row<9;row++){
        m_put(3,12+row,'0'+row);port_hex(9,12+row,key_matrix[row*2]);port_hex(15,12+row,key_matrix[row*2+1]);m_wait();
    }
    m_text(2,23,"RAW MATRIX - NOT ASCII");m_wait();
    while(1){m_wait();}
}

This is a calling fragment. Follow the stated initialization and buffer requirements before using it in a complete program.

Expected result

In this KUROSAKI run, DEVICE DETECT, ROW 6 COL 0 and A KEY - GUARDED are all 000; all nine COL0/COL1 pairs are 00. A is row 6, column 0, bit 4, but failed detection prevents the raw zero from being reported as a key press. The table is a single startup capture.

__fkb_read_row_col: KEYBOARD-DEFAULT — In this KUROSAKI run, DEVICE DETECT, ROW 6 COL 0 and A KEY - GUARDED are all 000; all nine COL0/COL1 pairs are 00. A is row 6, column 0, bit 4, but failed detection prevents the raw zero from being reported as a key press. The table is a single startup capture.
KEYBOARD-DEFAULT — In this KUROSAKI run, DEVICE DETECT, ROW 6 COL 0 and A KEY - GUARDED are all 000; all nine COL0/COL1 pairs are 00. A is row 6, column 0, bit 4, but failed detection prevents the raw zero from being reported as a key press. The table is a single startup capture.

Complete example program

Build command

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\peripheral_keyboard.c -I .\kitaqfc\lib -I .\kitaq-docs\samples -o .\out\peripheral_keyboard.nes --mapper=nrom --nes-chr=.\kitaq-docs\samples\font.chr --no-cache --no-disasm
.\kurosaki\kurosaki.exe run .\out\peripheral_keyboard.nes --frames 120 --png .\out\peripheral_keyboard.png
Implementation and source

kitaqfc/lib/intrinsics.h:264

Declaration notes (original source)
Select a matrix row and the low bit of col, then return raw 4017 & 1E.
Use rows 0..8 for keys; row 9 is used by detection. No row-range validation is emitted.

kitaqfc/kitaqfc/CodeGenerator.cs:7802

csharp
EmitHelperStart("__fkb_read_row_col");
            EmitAsm("LDA", Imm(0x05));
            EmitAsm("STA", Mem(0x4016));
            EmitKeyboardDelayShort();
            // Establish row zero, then generate one high-to-low column edge
            // per requested advance. Repeated writes of 04 do not clock the counter.
            EmitAsm("LDA", Imm(0x04));
            EmitAsm("STA", Mem(0x4016));
            EmitKeyboardDelayLong();
            EmitAsm("LDX", Mem(CallArgBase));
            string selectedRow = NewGeneratedLabel("fkb_selected_row");
            EmitAsm("BEQ", Rel(selectedRow));
            string adv = NewGeneratedLabel("fkb_adv_row");
            _assembly.Add(Expr.Make(Tag.Label, adv));
            EmitAsm("LDA", Imm(0x06));
            EmitAsm("STA", Mem(0x4016));
            EmitKeyboardDelayLong();
            EmitAsm("LDA", Imm(0x04));
            EmitAsm("STA", Mem(0x4016));
            EmitKeyboardDelayLong();
            EmitAsm("DEX");
            EmitAsm("BNE", Rel(adv));
            _assembly.Add(Expr.Make(Tag.Label, selectedRow));
            EmitAsm("LDA", Mem(CallArgBase + 1));
            EmitAsm("AND", Imm(1));
            string readNow = NewGeneratedLabel("fkb_read_now");
            EmitAsm("BEQ", Rel(readNow));
            EmitAsm("LDA", Imm(0x06));
            EmitAsm("STA", Mem(0x4016));
            EmitKeyboardDelayLong();
            _assembly.Add(Expr.Make(Tag.Label, readNow));
            EmitAsm("LDA", Mem(0x4017));
            EmitAsm("AND", Imm(0x1E));
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("LDA", Imm(0));
            EmitAsm("STA", Mem(0x4016));
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("RTS");
__fkb_scan

Scan all nine rows and two column groups into 18 bytes, allowing multiple keys to be inspected from one matrix capture.

Syntax

c
void __fkb_scan(u8* dst18);

Parameters

dst18

Writable storage of at least 18 bytes. Element row*2+col receives that group’s raw value. No null or length check is performed.

Return value

No return value.

Conditions and limits

These APIs access $4016/$4017 directly. Do not interleave interrupt-driven gamepad, keyboard or serial operations on the same ports. Controller shift positions can also be affected. The sample disables NMI while capturing input, then enables it when initializing the display.

Key groups are raw bits 1..4: a pressed key clears its bit, and an idle group is 0x1E. They are neither ASCII text nor a bitmap with one meaning pressed. A disconnected device can also read zero; do not interpret that as every key being held without checking presence.

Writes 5 to $4016 to reset row 0, then advances rows using high-to-low transitions of the column-select bit. The generated code includes 12 CPU cycles of NOPs after reset and 50 after column selection, in addition to surrounding instructions. It writes zero on exit to disable the matrix.

A separate regression checks rows 0..9, both columns, the nine-row scan, settling delays and final port state from executed CPU bus traces. It does not inject physical key signals.

Reads column 0 then column 1 for each row. This is sequential scanning, not a simultaneous snapshot of every key. A zero-valued group does not terminate the scan early.

These images run the downloadable ROMs unchanged in KUROSAKI. Its current input bus models gamepads but not keyboard, light-gun or serial-adapter signals. The external input bits read as zero, so the captures do not verify those devices. Physical key input, optical sensing and communication remain untested.

NESdev: Family BASIC Keyboard

Usage example

c
#include "fc_ports_scene.h"
void main(void){
    u8 row;
    __irq_disable();__ppu_ctrl_set(0);port_phase=1;
    port_result[0]=__fkb_detect();
    __fkb_scan(key_matrix);
    port_result[1]=__fkb_read_row_col(6,0);
    port_result[2]=0;
    if(port_result[0] && !(port_result[1]&16))port_result[2]=1;
    port_phase=2;port_result[31]=165;
    m_init();m_text(2,2,"KEYBOARD STARTUP SNAPSHOT");m_wait();
    m_text(2,4,"DEVICE DETECT");port_number(25,4,port_result[0]);m_wait();
    m_text(2,6,"ROW 6 COL 0");port_number(25,6,port_result[1]);m_wait();
    m_text(2,8,"A KEY - GUARDED");port_number(25,8,port_result[2]);m_wait();
    m_text(2,10,"ROW   COL0  COL1  (HEX)");m_wait();
    for(row=0;row<9;row++){
        m_put(3,12+row,'0'+row);port_hex(9,12+row,key_matrix[row*2]);port_hex(15,12+row,key_matrix[row*2+1]);m_wait();
    }
    m_text(2,23,"RAW MATRIX - NOT ASCII");m_wait();
    while(1){m_wait();}
}

This is a calling fragment. Follow the stated initialization and buffer requirements before using it in a complete program.

Expected result

In this KUROSAKI run, DEVICE DETECT, ROW 6 COL 0 and A KEY - GUARDED are all 000; all nine COL0/COL1 pairs are 00. A is row 6, column 0, bit 4, but failed detection prevents the raw zero from being reported as a key press. The table is a single startup capture.

__fkb_scan: KEYBOARD-DEFAULT — In this KUROSAKI run, DEVICE DETECT, ROW 6 COL 0 and A KEY - GUARDED are all 000; all nine COL0/COL1 pairs are 00. A is row 6, column 0, bit 4, but failed detection prevents the raw zero from being reported as a key press. The table is a single startup capture.
KEYBOARD-DEFAULT — In this KUROSAKI run, DEVICE DETECT, ROW 6 COL 0 and A KEY - GUARDED are all 000; all nine COL0/COL1 pairs are 00. A is row 6, column 0, bit 4, but failed detection prevents the raw zero from being reported as a key press. The table is a single startup capture.

Complete example program

Build command

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\peripheral_keyboard.c -I .\kitaqfc\lib -I .\kitaq-docs\samples -o .\out\peripheral_keyboard.nes --mapper=nrom --nes-chr=.\kitaq-docs\samples\font.chr --no-cache --no-disasm
.\kurosaki\kurosaki.exe run .\out\peripheral_keyboard.nes --frames 120 --png .\out\peripheral_keyboard.png
Implementation and source

kitaqfc/lib/intrinsics.h:261

Declaration notes (original source)
Write 18 raw masked samples (4017 & 1E), two per row, to caller storage; reset 4016 afterward.

kitaqfc/kitaqfc/CodeGenerator.cs:7773

csharp
EmitHelperStart("__fkb_scan");
            EmitAsm("LDA", Imm(0x05));
            EmitAsm("STA", Mem(0x4016));
            EmitKeyboardDelayShort();
            EmitAsm("LDY", Imm(0));
            EmitAsm("LDX", Imm(9));
            string rowLoop = NewGeneratedLabel("fkb_scan_row");
            _assembly.Add(Expr.Make(Tag.Label, rowLoop));
            EmitAsm("LDA", Imm(0x04));
            EmitAsm("STA", Mem(0x4016));
            EmitKeyboardDelayLong();
            EmitAsm("LDA", Mem(0x4017));
            EmitAsm("AND", Imm(0x1E));
            EmitAsm("STA", IndY(CallArgBase));
            EmitAsm("INY");
            EmitAsm("LDA", Imm(0x06));
            EmitAsm("STA", Mem(0x4016));
            EmitKeyboardDelayLong();
            EmitAsm("LDA", Mem(0x4017));
            EmitAsm("AND", Imm(0x1E));
            EmitAsm("STA", IndY(CallArgBase));
            EmitAsm("INY");
            EmitAsm("DEX");
            EmitAsm("BNE", Rel(rowLoop));
            EmitAsm("LDA", Imm(0));
            EmitAsm("STA", Mem(0x4016));
            EmitAsm("RTS");
__irq_disable

Emit SEI to set the CPU status register's I bit and mask ordinary IRQ requests. Use this for an IRQ-critical section. It does not disable NMI, silence the APU, acknowledge an IRQ source or save the previous mask.

Syntax

c
void __irq_disable(void);

Return value

None (void).

Conditions and limits

The I bit only controls ordinary IRQ: NMI can still interrupt this critical section. Masking IRQ neither acknowledges a pending device request nor protects data shared with NMI. Prepare sources and handlers separately. The sample does not test an IRQ-generating peripheral.

NESdev: NMI

NESdev: CPU interrupts

Usage example

c
__irq_disable();
disabled=(__irq_save()&4)>>2; // The CPU I bit must be 1.

This is a calling fragment. Follow the stated initialization and buffer requirements before using it in a complete program.

Expected result

The program shows how the CPU IRQ mask differs from PPU NMI control. On the black screen, blue text must show IRQ DISABLED I = 001, IRQ ENABLED I = 000, SAVED SECTION I = 001 and RESTORED I = 000. Each value is the extracted I bit, not the whole status byte. FAILED CHECKS must be 000.

Run for at least 360 emulator frames so the 256-wait demonstration can finish. NMI DELTA 3 WAITS must be 003 even with IRQ masked; NMI DELTA 256 WAITS must be 000 because the byte wraps. NMI OFF DELTA must be 000. This program uses the default handler without runtime.c. Separate state checks cover 21 cases, including status-bit restoration and bounded waits with NMI disabled or an empty custom handler. KUROSAKI/NROM is tested; physical hardware is not.

__irq_disable: NROM
NROM

Complete example program

Build command

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\interrupt_intrinsics.c -I .\kitaqfc\lib -I .\kitaq-docs\samples --nes-chr=.\kitaq-docs\samples\font.chr -o .\out\interrupt_intrinsics.nes --no-cache --no-disasm
Implementation and source

kitaqfc/lib/intrinsics.h:176

Declaration notes (original source)
Emit SEI to mask CPU IRQs; NMI is unaffected.

kitaqfc/kitaqfc/CodeGenerator.cs:4733

csharp
case "__irq_disable": if (!Need(0)) return true; EmitAsm("SEI"); return true;
__irq_enable

Emit CLI to clear the CPU status register's I bit and allow ordinary IRQ requests to be serviced. This does not configure a mapper or APU interrupt source. Set up the intended handler and its source-acknowledgment logic before allowing that source to interrupt.

Syntax

c
void __irq_enable(void);

Return value

None (void).

Conditions and limits

The I bit only controls ordinary IRQ: NMI can still interrupt this critical section. Masking IRQ neither acknowledges a pending device request nor protects data shared with NMI. Prepare sources and handlers separately. The sample does not test an IRQ-generating peripheral.

NESdev: NMI

NESdev: CPU interrupts

Usage example

c
__irq_enable();
enabled=(__irq_save()&4)>>2; // Save returns the old I bit: 0, then masks IRQ.

This is a calling fragment. Follow the stated initialization and buffer requirements before using it in a complete program.

Expected result

The program shows how the CPU IRQ mask differs from PPU NMI control. On the black screen, blue text must show IRQ DISABLED I = 001, IRQ ENABLED I = 000, SAVED SECTION I = 001 and RESTORED I = 000. Each value is the extracted I bit, not the whole status byte. FAILED CHECKS must be 000.

Run for at least 360 emulator frames so the 256-wait demonstration can finish. NMI DELTA 3 WAITS must be 003 even with IRQ masked; NMI DELTA 256 WAITS must be 000 because the byte wraps. NMI OFF DELTA must be 000. This program uses the default handler without runtime.c. Separate state checks cover 21 cases, including status-bit restoration and bounded waits with NMI disabled or an empty custom handler. KUROSAKI/NROM is tested; physical hardware is not.

__irq_enable: NROM
NROM

Complete example program

Build command

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\interrupt_intrinsics.c -I .\kitaqfc\lib -I .\kitaq-docs\samples --nes-chr=.\kitaq-docs\samples\font.chr -o .\out\interrupt_intrinsics.nes --no-cache --no-disasm
Implementation and source

kitaqfc/lib/intrinsics.h:178

Declaration notes (original source)
Emit CLI to unmask CPU IRQs; the caller must have a valid interrupt handler.

kitaqfc/kitaqfc/CodeGenerator.cs:4734

csharp
case "__irq_enable": if (!Need(0)) return true; EmitAsm("CLI"); return true;
                // Return the saved processor status in A after masking IRQs; restore consumes a status byte, not a Boolean.
__irq_restore

Evaluate the supplied status byte, push it with PHA and restore CPU status with PLP. This restores the saved IRQ mask and arithmetic flags, rather than always enabling IRQ. Ordinary C expressions executed afterward may change arithmetic flags again.

Syntax

c
void __irq_restore(u8 state);

Parameters

state

The full 8-bit processor-status value captured by __irq_save. Preserve it for __irq_restore; do not convert it to a Boolean. Bit 2 is I: 1 masks IRQ, 0 allows it. PHP's stacked representation sets bits 4 and 5.

Return value

None (void).

Conditions and limits

The I bit only controls ordinary IRQ: NMI can still interrupt this critical section. Masking IRQ neither acknowledges a pending device request nor protects data shared with NMI. Prepare sources and handlers separately. The sample does not test an IRQ-generating peripheral.

NESdev: NMI

NESdev: CPU interrupts

Usage example

c
__irq_restore(saved_state); // Restore the caller's original IRQ mask.
restored=(__irq_save()&4)>>2; // Original I was 0; this probe masks it again.

This is a calling fragment. Follow the stated initialization and buffer requirements before using it in a complete program.

Expected result

The program shows how the CPU IRQ mask differs from PPU NMI control. On the black screen, blue text must show IRQ DISABLED I = 001, IRQ ENABLED I = 000, SAVED SECTION I = 001 and RESTORED I = 000. Each value is the extracted I bit, not the whole status byte. FAILED CHECKS must be 000.

Run for at least 360 emulator frames so the 256-wait demonstration can finish. NMI DELTA 3 WAITS must be 003 even with IRQ masked; NMI DELTA 256 WAITS must be 000 because the byte wraps. NMI OFF DELTA must be 000. This program uses the default handler without runtime.c. Separate state checks cover 21 cases, including status-bit restoration and bounded waits with NMI disabled or an empty custom handler. KUROSAKI/NROM is tested; physical hardware is not.

__irq_restore: NROM
NROM

Complete example program

Build command

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\interrupt_intrinsics.c -I .\kitaqfc\lib -I .\kitaq-docs\samples --nes-chr=.\kitaq-docs\samples\font.chr -o .\out\interrupt_intrinsics.nes --no-cache --no-disasm
Implementation and source

kitaqfc/lib/intrinsics.h:182

Declaration notes (original source)
Restore processor status with PLP, including arithmetic flags and the saved IRQ mask.

kitaqfc/kitaqfc/CodeGenerator.cs:4737

csharp
case "__irq_restore": if (!Need(1)) return true; Load1(args[0]); EmitAsm("PHA"); EmitAsm("PLP"); return true;
__irq_save

Capture the CPU status byte with PHP, then mask IRQ using SEI and return the captured byte through PLA. The saved I bit describes the state before masking. Pair this with __irq_restore so a nested critical section preserves an already disabled IRQ mask.

Syntax

c
u8 __irq_save(void);

Return value

The full 8-bit processor-status value captured by __irq_save. Preserve it for __irq_restore; do not convert it to a Boolean. Bit 2 is I: 1 masks IRQ, 0 allows it. PHP's stacked representation sets bits 4 and 5.

Conditions and limits

The I bit only controls ordinary IRQ: NMI can still interrupt this critical section. Masking IRQ neither acknowledges a pending device request nor protects data shared with NMI. Prepare sources and handlers separately. The sample does not test an IRQ-generating peripheral.

NESdev: NMI

NESdev: CPU interrupts

Usage example

c
saved_state=__irq_save(); // Save all processor-status bits, then set I.
inside=(__irq_save()&4)>>2; // IRQ remains masked inside this section.

This is a calling fragment. Follow the stated initialization and buffer requirements before using it in a complete program.

Expected result

The program shows how the CPU IRQ mask differs from PPU NMI control. On the black screen, blue text must show IRQ DISABLED I = 001, IRQ ENABLED I = 000, SAVED SECTION I = 001 and RESTORED I = 000. Each value is the extracted I bit, not the whole status byte. FAILED CHECKS must be 000.

Run for at least 360 emulator frames so the 256-wait demonstration can finish. NMI DELTA 3 WAITS must be 003 even with IRQ masked; NMI DELTA 256 WAITS must be 000 because the byte wraps. NMI OFF DELTA must be 000. This program uses the default handler without runtime.c. Separate state checks cover 21 cases, including status-bit restoration and bounded waits with NMI disabled or an empty custom handler. KUROSAKI/NROM is tested; physical hardware is not.

__irq_save: NROM
NROM

Complete example program

Build command

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\interrupt_intrinsics.c -I .\kitaqfc\lib -I .\kitaq-docs\samples --nes-chr=.\kitaq-docs\samples\font.chr -o .\out\interrupt_intrinsics.nes --no-cache --no-disasm
Implementation and source

kitaqfc/lib/intrinsics.h:180

Declaration notes (original source)
Return the processor-status byte captured before SEI. Pass that byte to __irq_restore.

kitaqfc/kitaqfc/CodeGenerator.cs:4736

csharp
case "__irq_save": if (!Need(0)) return true; EmitAsm("PHP"); EmitAsm("SEI"); EmitAsm("PLA"); return true;
__irq_scanline_set

Alias of the MMC3 latch/reload operation in __mapper_irq_set. The word scanline does not guarantee an absolute screen row.

Syntax

c
void __irq_scanline_set(u8 line);

Parameters

line

An 8-bit counter latch value, 0..255, not an absolute screen Y coordinate. Reload timing and qualified PPU A12 transitions determine when it fires. Zero is valid and does not mean “disable interrupts.”

Return value

No return value.

Conditions and limits

Intrinsic declarations are in intrinsics.h. Include nes_game.h for the nes_mapper_irq_* macros. Select the board at build time with --mapper.

MMC3 only. Supply a __nes_irq handler, qualified PPU A12 transitions, mapper IRQ enable and CPU __irq_enable(). The sample enables rendering with background patterns at $0000 and sprite patterns at $1000. This lesson does not verify synchronization to an exact line on physical hardware.

MMC3 acknowledgment and disable both write $E000. This clears the current request and disables subsequent requests. Call __mapper_irq_enable() again at the intended time to arm another interrupt. Neither operation changes the CPU interrupt mask or NMI enable.

The compiler emits RTI for a custom interrupt handler, but the handler must preserve any A/X/Y registers and shared scratch that it changes. The sample saves A/X/Y and uses only a byte increment and argument-free acknowledgment. If you add C calls, also inspect the shared storage they use.

Usage example

c
irq_phase=2;
    __irq_scanline_set(7); __mapper_irq_enable();
    for (i=0;i<6;i++) { m_wait(); }
    mapper_result[17]=irq_count;

This is a calling fragment. Follow the stated initialization and buffer requirements before using it in a complete program.

Expected result

The MMC3 sample displays CHR markers 19/20, 21/22 and 23/24. CIRAM reads show A B A B for vertical mirroring and A A B B for horizontal mirroring. IRQ counts are 01→02→03→03: each of the first three explicit enables permits one interrupt; the count stays unchanged after disable. Each stage waits six frames.

__irq_scanline_set: MMC3-DEFAULT
MMC3-DEFAULT

Complete example program

Build command

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

kitaqfc/lib/intrinsics.h:324

Declaration notes (original source)
Write the MMC3 IRQ latch and reload registers. This does not enable IRQ delivery.
__joypad2p_voice

__joypad2p_voice is another entry name for the __mic_read2p helper. Both read the same digital microphone bit. The word “voice” does not imply audio processing; separate calls take separate samples and can differ if the signal changes between them.

Read the digital signal for the second Famicom controller's microphone from $4016 bit 2. The helper reads the register once, masks 0x04 and normalizes the result to 0 or 1 without inversion. It does not latch controllers, record audio, measure volume or recognize speech.

Syntax

c
u8 __joypad2p_voice(void);

Return value

Exactly 1 when $4016 & 0x04 is nonzero, otherwise 0. This is the sampled digital level, not a button mask, amplitude, frequency or count of spoken words.

Conditions and limits

Although the microphone belongs to controller 2, this helper accesses $4016, not $4017. A register read can also advance the controller's serial stream while strobe is low, so do not insert microphone reads inside another controller's eight-bit read loop. No input-history cache is updated.

The published KUROSAKI controller model returns only D0 plus fixed bit 6. D1 and the microphone bit are therefore always 0 in that emulator, even while ordinary controller buttons are held. The supplied run verifies compilation, ordinary-button input and this zero-signal observation. Nonzero peripheral signals, electrical timing and real-device compatibility remain unverified; a zero here is not proof that an attached device works or that these functions cannot read it on hardware.

Usage example

c
voice=__joypad2p_voice();

This is a calling fragment. Follow the stated initialization and buffer requirements before using it in a complete program.

Expected result

This program contrasts ordinary D0 input with expansion D1 and the microphone's digital level. Hold A on controller 1 and B on controller 2 until the snapshot is captured. NORMAL P1 and NORMAL P2 display 001 and 002. In the supplied KUROSAKI run, D1 PORT 1, D1 PORT 2, EXP ALIAS 1, EXP ALIAS 2, MIC LEVEL and MIC DIRECT all display 000 because those signal lines are not emulated. The displayed values then stay fixed. With suitable peripherals and an implementation of their signal lines, the expansion and microphone values depend on the actual inputs; consult the bit layouts above rather than expecting these zeros.

The published KUROSAKI controller model returns only D0 plus fixed bit 6. D1 and the microphone bit are therefore always 0 in that emulator, even while ordinary controller buttons are held. The supplied run verifies compilation, ordinary-button input and this zero-signal observation. Nonzero peripheral signals, electrical timing and real-device compatibility remain unverified; a zero here is not proof that an attached device works or that these functions cannot read it on hardware.

__joypad2p_voice: NROM
NROM

Complete example program

Build command

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\expansion_input.c -I .\kitaq-docs\samples -I .\kitaqfc\lib -o .\out\expansion_input.nes --mapper=nrom --nes-chr=.\kitaq-docs\samples\font.chr --no-cache --no-disasm
Implementation and source

kitaqfc/lib/intrinsics.h:231

Declaration notes (original source)
Return controller-port 4016 bit 2 normalized to 0 or 1; this is a microphone level bit, not PCM audio.
__mac16

Compute a unsigned multiply-accumulate.

Syntax

c
u16 __mac16(u16 acc, u16 a, u8 b);

Parameters

acc

16-bit accumulator to add.

a

Unsigned word, 0..65535.

b

Unsigned coefficient, 0..255.

Return value

Returns the low 16 bits of acc+a*b.

Conditions and limits

On FC, q8_8/q1_7 names still use raw integer products without a fixed-point downshift. The Q1.7 functions alias their corresponding signed operations. The caller handles units; signed results are returned as u16 bit patterns.

Usage example

c
// Compare literal coefficients with values read from RAM.
result[8]=__mac16(65500,513,127);
v0=65500;v1=513;v2=127;
result[9]=__mac16(v0,v1,v2);

This is a calling fragment. Follow the stated initialization and buffer requirements before using it in a complete program.

Expected result

Compare literal inputs with values stored in RAM. Representative results appear as unsigned 16-bit values; negatives use two’s-complement bit patterns. GB rescales fixed-point products, while FC returns integer products, so identical inputs need not give identical results.

The excerpt stores these 16-bit values: result[8]=65115, result[9]=65115. Each literal-input calculation matches its RAM-input counterpart. The screen shows representative values; the result array holds every result.

__mac16: NROM
NROM

Complete example program

Build command

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\batch3_math.c -I .\kitaqfc\lib -I .\kitaq-docs\samples -o .\out\batch3_math.nes --no-cache --no-disasm --mapper=nrom --nes-chr=.\kitaq-docs\samples\font.chr
Implementation and source

kitaqfc/lib/intrinsics.h:55

Declaration notes (original source)
Return acc+a*b modulo 65536 using unsigned multiplication.

kitaqfc/kitaqfc/CodeGenerator.cs:4650

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

void EmitHelperMath()
        {
            // Consume eight multiplier bits and accumulate the low product word in shared temporaries, returning A/X.
            EmitHelperStart("__mul16x8");
            EmitAsm("LDA", Imm(0));
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp1Address));
            EmitAsm("LDX", Imm(8));
            string loop = NewGeneratedLabel("mul16x8_loop");
            string skip = NewGeneratedLabel("mul16x8_skip_add");
            _assembly.Add(Expr.Make(Tag.Label, loop));
            EmitAsm("LSR", Mem(CallArgBase + 2));
            EmitAsm("BCC", Rel(skip));
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("CLC");
            EmitAsm("ADC", Mem(CallArgBase));
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp1Address));
            EmitAsm("ADC", Mem(CallArgBase + 1));
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp1Address));
            _assembly.Add(Expr.Make(Tag.Label, skip));
            EmitAsm("ASL", Mem(CallArgBase));
            EmitAsm("ROL", Mem(CallArgBase + 1));
            EmitAsm("DEX");
            EmitAsm("BNE", Rel(loop));
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("LDX", Mem(_runtimeIntrinsicTmp1Address));
            EmitAsm("RTS");

            // Zero-extend the byte multiplicand, reuse word multiplication and return its high byte.
            EmitHelperStart("__mul8x8_hi");
            EmitAsm("LDA", Mem(CallArgBase + 1));
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp2Address));
            EmitAsm("LDA", Imm(0));
            EmitAsm("STA", Mem(CallArgBase + 1));
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp2Address));
            EmitAsm("STA", Mem(CallArgBase + 2));
            EmitAsm("JSR", Abs("__mul16x8"));
            EmitAsm("TXA");
            EmitAsm("RTS");

            // Convert signed operands to magnitudes, multiply, then restore the product sign modulo 65536.
            EmitHelperStart("__smul16x8");
            EmitAsm("LDA", Imm(0));
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp2Address));
            string aPos = NewGeneratedLabel("smul_a_positive");
            EmitAsm("LDA", Mem(CallArgBase + 1));
            EmitAsm("AND", Imm(0x80));
            EmitAsm("BEQ", Rel(aPos));
            EmitAsm("LDA", Mem(CallArgBase)); EmitAsm("EOR", Imm(0xFF)); EmitAsm("CLC"); EmitAsm("ADC", Imm(1)); EmitAsm("STA", Mem(CallArgBase));
            EmitAsm("LDA", Mem(CallArgBase + 1)); EmitAsm("EOR", Imm(0xFF)); EmitAsm("ADC", Imm(0)); EmitAsm("STA", Mem(CallArgBase + 1));
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp2Address)); EmitAsm("EOR", Imm(1)); EmitAsm("STA", Mem(_runtimeIntrinsicTmp2Address));
            _assembly.Add(Expr.Make(Tag.Label, aPos));
            string bPos = NewGeneratedLabel("smul_b_positive");
            EmitAsm("LDA", Mem(CallArgBase + 2));
            EmitAsm("AND", Imm(0x80));
            EmitAsm("BEQ", Rel(bPos));
            EmitAsm("LDA", Mem(CallArgBase + 2)); EmitAsm("EOR", Imm(0xFF)); EmitAsm("CLC"); EmitAsm("ADC", Imm(1)); EmitAsm("STA", Mem(CallArgBase + 2));
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp2Address)); EmitAsm("EOR", Imm(1)); EmitAsm("STA", Mem(_runtimeIntrinsicTmp2Address));
            _assembly.Add(Expr.Make(Tag.Label, bPos));
            EmitAsm("JSR", Abs("__mul16x8"));
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("STX", Mem(_runtimeIntrinsicTmp1Address));
            string sdone = NewGeneratedLabel("smul_done");
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp2Address));
            EmitAsm("BEQ", Rel(sdone));
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp0Address)); EmitAsm("EOR", Imm(0xFF)); EmitAsm("CLC"); EmitAsm("ADC", Imm(1)); EmitAsm("STA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp1Address)); EmitAsm("EOR", Imm(0xFF)); EmitAsm("ADC", Imm(0)); EmitAsm("STA", Mem(_runtimeIntrinsicTmp1Address));
            _assembly.Add(Expr.Make(Tag.Label, sdone));
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("LDX", Mem(_runtimeIntrinsicTmp1Address));
            EmitAsm("RTS");

            EmitMacHelper("__mac16", "__mul16x8");
            EmitMacHelper("__smac16", "__smul16x8");
            EmitDot2Helper("__dot2_q8_8", "__mul16x8");
            EmitDot3Helper("__dot3_q8_8", "__mul16x8");
            EmitDot2Helper("__sdot2_q8_8", "__smul16x8");
            EmitDot3Helper("__sdot3_q8_8", "__smul16x8");
        }
__manhattan

Add the absolute X and Y differences between two points.

Syntax

c
u8 __manhattan(u8 x1, u8 y1, u8 x2, u8 y2);

Parameters

x1

Point 1 X coordinate, 0..255.

y1

Point 1 Y coordinate, 0..255.

x2

Point 2 X coordinate, 0..255.

y2

Point 2 Y coordinate, 0..255.

Return value

Returns the low byte of |x1−x2|+|y1−y2|; distances above 255 wrap.

Usage example

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

This is a calling fragment. Follow the stated initialization and buffer requirements before using it in a complete program.

Expected result

Exercise the interior, boundaries, outside point and zero width: INSIDE=1, RIGHT EDGE=0, LEFT EDGE=0, HIGH X=1. Verify distance 15, wrapped distance 254 and array indices 64/65280.

__manhattan: NROM
NROM

Complete example program

Build command

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\batch3_coordinates.c -I .\kitaqfc\lib -I .\kitaq-docs\samples -o .\out\batch3_coordinates.nes --no-cache --no-disasm --mapper=nrom --nes-chr=.\kitaq-docs\samples\font.chr
Implementation and source

kitaqfc/lib/intrinsics.h:33

Declaration notes (original source)
Return abs(x1-x2)+abs(y1-y2) modulo 256; large distances are not saturated.

kitaqfc/kitaqfc/CodeGenerator.cs:4629

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

void EmitHelperGeometryAndRng()
        {
            EmitHelperStart("__xy_in_rect");
            string xyFalse = NewGeneratedLabel("xy_in_rect_false");
            string xyDone = NewGeneratedLabel("xy_in_rect_done");
            // return (x-rx < rw) && (y-ry < rh), using unsigned subtract to avoid rx+rw overflow.
            EmitAsm("LDA", Mem(CallArgBase));
            EmitAsm("SEC");
            EmitAsm("SBC", Mem(CallArgBase + 2));
            EmitAsm("BCC", Rel(xyFalse));
            EmitAsm("CMP", Mem(CallArgBase + 4));
            EmitAsm("BCS", Rel(xyFalse));
            EmitAsm("LDA", Mem(CallArgBase + 1));
            EmitAsm("SEC");
            EmitAsm("SBC", Mem(CallArgBase + 3));
            EmitAsm("BCC", Rel(xyFalse));
            EmitAsm("CMP", Mem(CallArgBase + 5));
            EmitAsm("BCS", Rel(xyFalse));
            EmitAsm("LDA", Imm(1));
            EmitAsm("JMP", Abs(xyDone));
            _assembly.Add(Expr.Make(Tag.Label, xyFalse));
            EmitAsm("LDA", Imm(0));
            _assembly.Add(Expr.Make(Tag.Label, xyDone));
            EmitAsm("RTS");

            EmitHelperStart("__manhattan");
            // dx = abs(x1-x2)
            EmitAsm("LDA", Mem(CallArgBase));
            EmitAsm("SEC");
            EmitAsm("SBC", Mem(CallArgBase + 2));
            string mdxOk = NewGeneratedLabel("manhattan_dx_ok");
            EmitAsm("BCS", Rel(mdxOk));
            EmitAsm("EOR", Imm(0xFF));
            EmitAsm("CLC");
            EmitAsm("ADC", Imm(1));
            _assembly.Add(Expr.Make(Tag.Label, mdxOk));
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp0Address));
            // dy = abs(y1-y2), then add dx.
            EmitAsm("LDA", Mem(CallArgBase + 1));
            EmitAsm("SEC");
            EmitAsm("SBC", Mem(CallArgBase + 3));
            string mdyOk = NewGeneratedLabel("manhattan_dy_ok");
            EmitAsm("BCS", Rel(mdyOk));
            EmitAsm("EOR", Imm(0xFF));
            EmitAsm("CLC");
            EmitAsm("ADC", Imm(1));
            _assembly.Add(Expr.Make(Tag.Label, mdyOk));
            EmitAsm("CLC");
            EmitAsm("ADC", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("RTS");

            // Save X-coordinate separately while multiplying width by Y, then add X with carry into the high byte.
            EmitHelperStart("__map_index");
            // args: x, y, width. Return y * width + x as u16.
            EmitAsm("LDA", Mem(CallArgBase));      // x
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp2Address));
            EmitAsm("LDA", Mem(CallArgBase + 1));  // y
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp1Address));
            EmitAsm("LDA", Mem(CallArgBase + 2));  // width as u16 multiplicand
            EmitAsm("STA", Mem(CallArgBase));
            EmitAsm("LDA", Imm(0));
            EmitAsm("STA", Mem(CallArgBase + 1));
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp1Address));
            EmitAsm("STA", Mem(CallArgBase + 2));
            EmitAsm("JSR", Abs("__mul16x8"));
            EmitAsm("CLC");
            EmitAsm("ADC", Mem(_runtimeIntrinsicTmp2Address));
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("TXA");
            EmitAsm("ADC", Imm(0));
            EmitAsm("TAX");
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("RTS");

            // Replace both bytes of the persistent generator state with the supplied seed.
            EmitHelperStart("__rng_seed");
            EmitAsm("LDA", Mem(CallArgBase));
            EmitAsm("STA", Mem(_runtimeRngLoAddress));
            EmitAsm("LDA", Mem(CallArgBase + 1));
            EmitAsm("STA", Mem(_runtimeRngHiAddress));
            EmitAsm("RTS");

            EmitHelperStart("__rng8");
            // 16-bit Galois LFSR: if low bit set, shift right then xor $B400.
            EmitAsm("LDA", Mem(_runtimeRngLoAddress));
            EmitAsm("ORA", Mem(_runtimeRngHiAddress));
            // Replace the all-zero LFSR state with a deterministic nonzero seed before advancing.
            string seeded = NewGeneratedLabel("rng_seeded");
            EmitAsm("BNE", Rel(seeded));
            EmitAsm("LDA", Imm(0x5A));
            EmitAsm("STA", Mem(_runtimeRngLoAddress));
            EmitAsm("LDA", Imm(0xA5));
            EmitAsm("STA", Mem(_runtimeRngHiAddress));
            _assembly.Add(Expr.Make(Tag.Label, seeded));
            EmitAsm("LDA", Mem(_runtimeRngLoAddress));
            EmitAsm("AND", Imm(1));
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("LSR", Mem(_runtimeRngHiAddress));
            EmitAsm("ROR", Mem(_runtimeRngLoAddress));
            string rngNoXor = NewGeneratedLabel("rng_no_xor");
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("BEQ", Rel(rngNoXor));
            EmitAsm("LDA", Mem(_runtimeRngHiAddress));
            EmitAsm("EOR", Imm(0xB4));
            EmitAsm("STA", Mem(_runtimeRngHiAddress));
            _assembly.Add(Expr.Make(Tag.Label, rngNoXor));
            EmitAsm("LDA", Mem(_runtimeRngLoAddress));
            EmitAsm("EOR", Mem(_runtimeRngHiAddress));
            EmitAsm("RTS");
        }
__map_index

Convert a cell coordinate to a row-major array index.

Syntax

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

Parameters

x

Column 0..255, normally less than width.

y

Row 0..255.

width

Cells per row, 0..255.

Return value

Returns y*width+x as u16; does not validate coordinates.

Usage example

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

This is a calling fragment. Follow the stated initialization and buffer requirements before using it in a complete program.

Expected result

Exercise the interior, boundaries, outside point and zero width: INSIDE=1, RIGHT EDGE=0, LEFT EDGE=0, HIGH X=1. Verify distance 15, wrapped distance 254 and array indices 64/65280.

__map_index: NROM
NROM

Complete example program

Build command

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\batch3_coordinates.c -I .\kitaqfc\lib -I .\kitaq-docs\samples -o .\out\batch3_coordinates.nes --no-cache --no-disasm --mapper=nrom --nes-chr=.\kitaq-docs\samples\font.chr
Implementation and source

kitaqfc/lib/intrinsics.h:35

Declaration notes (original source)
Return y*width+x as a word without checking coordinates against map dimensions.

kitaqfc/kitaqfc/CodeGenerator.cs:4631

csharp
case "__map_index":
                    if (TryEmitMapIndexInline(args, ctx)) return true;
                    return EmitKitaqfcHelperCall(funcName, args, ctx, new[] {1,1,1}, "__map_index");

void EmitHelperGeometryAndRng()
        {
            EmitHelperStart("__xy_in_rect");
            string xyFalse = NewGeneratedLabel("xy_in_rect_false");
            string xyDone = NewGeneratedLabel("xy_in_rect_done");
            // return (x-rx < rw) && (y-ry < rh), using unsigned subtract to avoid rx+rw overflow.
            EmitAsm("LDA", Mem(CallArgBase));
            EmitAsm("SEC");
            EmitAsm("SBC", Mem(CallArgBase + 2));
            EmitAsm("BCC", Rel(xyFalse));
            EmitAsm("CMP", Mem(CallArgBase + 4));
            EmitAsm("BCS", Rel(xyFalse));
            EmitAsm("LDA", Mem(CallArgBase + 1));
            EmitAsm("SEC");
            EmitAsm("SBC", Mem(CallArgBase + 3));
            EmitAsm("BCC", Rel(xyFalse));
            EmitAsm("CMP", Mem(CallArgBase + 5));
            EmitAsm("BCS", Rel(xyFalse));
            EmitAsm("LDA", Imm(1));
            EmitAsm("JMP", Abs(xyDone));
            _assembly.Add(Expr.Make(Tag.Label, xyFalse));
            EmitAsm("LDA", Imm(0));
            _assembly.Add(Expr.Make(Tag.Label, xyDone));
            EmitAsm("RTS");

            EmitHelperStart("__manhattan");
            // dx = abs(x1-x2)
            EmitAsm("LDA", Mem(CallArgBase));
            EmitAsm("SEC");
            EmitAsm("SBC", Mem(CallArgBase + 2));
            string mdxOk = NewGeneratedLabel("manhattan_dx_ok");
            EmitAsm("BCS", Rel(mdxOk));
            EmitAsm("EOR", Imm(0xFF));
            EmitAsm("CLC");
            EmitAsm("ADC", Imm(1));
            _assembly.Add(Expr.Make(Tag.Label, mdxOk));
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp0Address));
            // dy = abs(y1-y2), then add dx.
            EmitAsm("LDA", Mem(CallArgBase + 1));
            EmitAsm("SEC");
            EmitAsm("SBC", Mem(CallArgBase + 3));
            string mdyOk = NewGeneratedLabel("manhattan_dy_ok");
            EmitAsm("BCS", Rel(mdyOk));
            EmitAsm("EOR", Imm(0xFF));
            EmitAsm("CLC");
            EmitAsm("ADC", Imm(1));
            _assembly.Add(Expr.Make(Tag.Label, mdyOk));
            EmitAsm("CLC");
            EmitAsm("ADC", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("RTS");

            // Save X-coordinate separately while multiplying width by Y, then add X with carry into the high byte.
            EmitHelperStart("__map_index");
            // args: x, y, width. Return y * width + x as u16.
            EmitAsm("LDA", Mem(CallArgBase));      // x
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp2Address));
            EmitAsm("LDA", Mem(CallArgBase + 1));  // y
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp1Address));
            EmitAsm("LDA", Mem(CallArgBase + 2));  // width as u16 multiplicand
            EmitAsm("STA", Mem(CallArgBase));
            EmitAsm("LDA", Imm(0));
            EmitAsm("STA", Mem(CallArgBase + 1));
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp1Address));
            EmitAsm("STA", Mem(CallArgBase + 2));
            EmitAsm("JSR", Abs("__mul16x8"));
            EmitAsm("CLC");
            EmitAsm("ADC", Mem(_runtimeIntrinsicTmp2Address));
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("TXA");
            EmitAsm("ADC", Imm(0));
            EmitAsm("TAX");
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("RTS");

            // Replace both bytes of the persistent generator state with the supplied seed.
            EmitHelperStart("__rng_seed");
            EmitAsm("LDA", Mem(CallArgBase));
            EmitAsm("STA", Mem(_runtimeRngLoAddress));
            EmitAsm("LDA", Mem(CallArgBase + 1));
            EmitAsm("STA", Mem(_runtimeRngHiAddress));
            EmitAsm("RTS");

            EmitHelperStart("__rng8");
            // 16-bit Galois LFSR: if low bit set, shift right then xor $B400.
            EmitAsm("LDA", Mem(_runtimeRngLoAddress));
            EmitAsm("ORA", Mem(_runtimeRngHiAddress));
            // Replace the all-zero LFSR state with a deterministic nonzero seed before advancing.
            string seeded = NewGeneratedLabel("rng_seeded");
            EmitAsm("BNE", Rel(seeded));
            EmitAsm("LDA", Imm(0x5A));
            EmitAsm("STA", Mem(_runtimeRngLoAddress));
            EmitAsm("LDA", Imm(0xA5));
            EmitAsm("STA", Mem(_runtimeRngHiAddress));
            _assembly.Add(Expr.Make(Tag.Label, seeded));
            EmitAsm("LDA", Mem(_runtimeRngLoAddress));
            EmitAsm("AND", Imm(1));
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("LSR", Mem(_runtimeRngHiAddress));
            EmitAsm("ROR", Mem(_runtimeRngLoAddress));
            string rngNoXor = NewGeneratedLabel("rng_no_xor");
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("BEQ", Rel(rngNoXor));
            EmitAsm("LDA", Mem(_runtimeRngHiAddress));
            EmitAsm("EOR", Imm(0xB4));
            EmitAsm("STA", Mem(_runtimeRngHiAddress));
            _assembly.Add(Expr.Make(Tag.Label, rngNoXor));
            EmitAsm("LDA", Mem(_runtimeRngLoAddress));
            EmitAsm("EOR", Mem(_runtimeRngHiAddress));
            EmitAsm("RTS");
        }
__mapper_id

Return the mapper number selected at build time, for board-specific initialization or display.

Syntax

c
u8 __mapper_id(void);

Return value

An 8-bit mapper number: NROM=0, MMC1=1, UxROM=2, CNROM=3, MMC3=4, MMC5=5, AxROM=7, VRC6=24, FME7=69 and VRC7=85. The compiler emits a constant; this does not probe the running hardware.

Conditions and limits

Intrinsic declarations are in intrinsics.h. Include nes_game.h for the nes_mapper_irq_* macros. Select the board at build time with --mapper.

Usage example

c
mapper_result[0] = __mapper_id();

This is a calling fragment. Follow the stated initialization and buffer requirements before using it in a complete program.

Expected result

The MMC3 sample displays CHR markers 19/20, 21/22 and 23/24. CIRAM reads show A B A B for vertical mirroring and A A B B for horizontal mirroring. IRQ counts are 01→02→03→03: each of the first three explicit enables permits one interrupt; the count stays unchanged after disable. Each stage waits six frames.

__mapper_id: MMC3-DEFAULT
MMC3-DEFAULT

Complete example program

Build command

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

kitaqfc/lib/intrinsics.h:314

Declaration notes (original source)
Mapper
Return the low byte of the mapper number selected for this build; no cartridge probe is performed.
__mapper_irq_ack

Acknowledge MMC3 in the IRQ handler. Because this writes $E000, it also disables subsequent IRQ requests.

Syntax

c
void __mapper_irq_ack(void);

Return value

No return value.

Conditions and limits

Intrinsic declarations are in intrinsics.h. Include nes_game.h for the nes_mapper_irq_* macros. Select the board at build time with --mapper.

MMC3 only. Supply a __nes_irq handler, qualified PPU A12 transitions, mapper IRQ enable and CPU __irq_enable(). The sample enables rendering with background patterns at $0000 and sprite patterns at $1000. This lesson does not verify synchronization to an exact line on physical hardware.

MMC3 acknowledgment and disable both write $E000. This clears the current request and disables subsequent requests. Call __mapper_irq_enable() again at the intended time to arm another interrupt. Neither operation changes the CPU interrupt mask or NMI enable.

The compiler emits RTI for a custom interrupt handler, but the handler must preserve any A/X/Y registers and shared scratch that it changes. The sample saves A/X/Y and uses only a byte increment and argument-free acknowledgment. If you add C calls, also inspect the shared storage they use.

Usage example

c
void __nes_irq(void) {
    __asm {
        PHA
        TXA
        PHA
        TYA
        PHA
    }
    irq_count++;
    if (irq_phase == 1) { nes_mapper_irq_ack(); }
    else { __mapper_irq_ack(); }
    __asm {
        PLA
        TAY
        PLA
        TAX
        PLA
    }
}

This is a calling fragment. Follow the stated initialization and buffer requirements before using it in a complete program.

Expected result

The MMC3 sample displays CHR markers 19/20, 21/22 and 23/24. CIRAM reads show A B A B for vertical mirroring and A A B B for horizontal mirroring. IRQ counts are 01→02→03→03: each of the first three explicit enables permits one interrupt; the count stays unchanged after disable. Each stage waits six frames.

__mapper_irq_ack: MMC3-DEFAULT
MMC3-DEFAULT

Complete example program

Build command

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

kitaqfc/lib/intrinsics.h:332

Declaration notes (original source)
Use the same E000 write as disable; explicitly re-enable when another interrupt is needed.
__mapper_irq_disable

Write $E000 to clear the MMC3 IRQ request and disable subsequent requests.

Syntax

c
void __mapper_irq_disable(void);

Return value

No return value.

Conditions and limits

Intrinsic declarations are in intrinsics.h. Include nes_game.h for the nes_mapper_irq_* macros. Select the board at build time with --mapper.

MMC3 only. Supply a __nes_irq handler, qualified PPU A12 transitions, mapper IRQ enable and CPU __irq_enable(). The sample enables rendering with background patterns at $0000 and sprite patterns at $1000. This lesson does not verify synchronization to an exact line on physical hardware.

MMC3 acknowledgment and disable both write $E000. This clears the current request and disables subsequent requests. Call __mapper_irq_enable() again at the intended time to arm another interrupt. Neither operation changes the CPU interrupt mask or NMI enable.

The compiler emits RTI for a custom interrupt handler, but the handler must preserve any A/X/Y registers and shared scratch that it changes. The sample saves A/X/Y and uses only a byte increment and argument-free acknowledgment. If you add C calls, also inspect the shared storage they use.

Usage example

c
__irq_disable(); __mapper_irq_disable();

This is a calling fragment. Follow the stated initialization and buffer requirements before using it in a complete program.

Expected result

The MMC3 sample displays CHR markers 19/20, 21/22 and 23/24. CIRAM reads show A B A B for vertical mirroring and A A B B for horizontal mirroring. IRQ counts are 01→02→03→03: each of the first three explicit enables permits one interrupt; the count stays unchanged after disable. Each stage waits six frames.

__mapper_irq_disable: MMC3-DEFAULT
MMC3-DEFAULT

Complete example program

Build command

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

kitaqfc/lib/intrinsics.h:330

Declaration notes (original source)
Write MMC3 E000 to disable and acknowledge mapper IRQs.
__mapper_irq_enable

Write $E001 to let MMC3 assert IRQ when its counter condition is met. This sets neither the latch nor a reload request.

Syntax

c
void __mapper_irq_enable(void);

Return value

No return value.

Conditions and limits

Intrinsic declarations are in intrinsics.h. Include nes_game.h for the nes_mapper_irq_* macros. Select the board at build time with --mapper.

MMC3 only. Supply a __nes_irq handler, qualified PPU A12 transitions, mapper IRQ enable and CPU __irq_enable(). The sample enables rendering with background patterns at $0000 and sprite patterns at $1000. This lesson does not verify synchronization to an exact line on physical hardware.

MMC3 acknowledgment and disable both write $E000. This clears the current request and disables subsequent requests. Call __mapper_irq_enable() again at the intended time to arm another interrupt. Neither operation changes the CPU interrupt mask or NMI enable.

The compiler emits RTI for a custom interrupt handler, but the handler must preserve any A/X/Y registers and shared scratch that it changes. The sample saves A/X/Y and uses only a byte increment and argument-free acknowledgment. If you add C calls, also inspect the shared storage they use.

Usage example

c
__mapper_irq_set(37); __mapper_irq_enable(); __irq_enable();
    for (i=0;i<6;i++) { m_wait(); }
    mapper_result[15]=irq_count;

This is a calling fragment. Follow the stated initialization and buffer requirements before using it in a complete program.

Expected result

The MMC3 sample displays CHR markers 19/20, 21/22 and 23/24. CIRAM reads show A B A B for vertical mirroring and A A B B for horizontal mirroring. IRQ counts are 01→02→03→03: each of the first three explicit enables permits one interrupt; the count stays unchanged after disable. Each stage waits six frames.

__mapper_irq_enable: MMC3-DEFAULT
MMC3-DEFAULT

Complete example program

Build command

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

kitaqfc/lib/intrinsics.h:328

Declaration notes (original source)
Write MMC3 E001. The CPU IRQ mask and a valid handler are separate caller responsibilities.
__mapper_irq_set

Write the MMC3 IRQ latch at $C000 and request its next reload at $C001. Enabling interrupts is a separate operation.

Syntax

c
void __mapper_irq_set(u8 scanline);

Parameters

scanline

An 8-bit counter latch value, 0..255, not an absolute screen Y coordinate. Reload timing and qualified PPU A12 transitions determine when it fires. Zero is valid and does not mean “disable interrupts.”

Return value

No return value.

Conditions and limits

Intrinsic declarations are in intrinsics.h. Include nes_game.h for the nes_mapper_irq_* macros. Select the board at build time with --mapper.

MMC3 only. Supply a __nes_irq handler, qualified PPU A12 transitions, mapper IRQ enable and CPU __irq_enable(). The sample enables rendering with background patterns at $0000 and sprite patterns at $1000. This lesson does not verify synchronization to an exact line on physical hardware.

MMC3 acknowledgment and disable both write $E000. This clears the current request and disables subsequent requests. Call __mapper_irq_enable() again at the intended time to arm another interrupt. Neither operation changes the CPU interrupt mask or NMI enable.

The compiler emits RTI for a custom interrupt handler, but the handler must preserve any A/X/Y registers and shared scratch that it changes. The sample saves A/X/Y and uses only a byte increment and argument-free acknowledgment. If you add C calls, also inspect the shared storage they use.

Usage example

c
__mapper_irq_set(37); __mapper_irq_enable(); __irq_enable();
    for (i=0;i<6;i++) { m_wait(); }
    mapper_result[15]=irq_count;

This is a calling fragment. Follow the stated initialization and buffer requirements before using it in a complete program.

Expected result

The MMC3 sample displays CHR markers 19/20, 21/22 and 23/24. CIRAM reads show A B A B for vertical mirroring and A A B B for horizontal mirroring. IRQ counts are 01→02→03→03: each of the first three explicit enables permits one interrupt; the count stays unchanged after disable. Each stage waits six frames.

__mapper_irq_set: MMC3-DEFAULT
MMC3-DEFAULT

Complete example program

Build command

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

kitaqfc/lib/intrinsics.h:326

Declaration notes (original source)
Alias the MMC3 IRQ latch/reload operation; other mapper profiles are rejected.
__memcpy

Copy a caller-specified number of bytes from src to dst. The 16-bit count supports transfers longer than 255 bytes, such as copying a complete RAM table. Bytes are read and written in ascending address order.

Syntax

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

Parameters

dst

Pointer to the first writable destination byte. Reserve enough consecutive bytes for the complete operation; no buffer is allocated for you.

src

Pointer to the first readable source byte. The entire range must remain accessible and unchanged throughout the copy.

len

Number of bytes as u16 (0–65535), not an element count. The type's maximum is not a promise that such a large valid buffer exists on the target machine. Keep the whole range within the selected memory mapping.

Return value

None (void).

Conditions and limits

A zero count transfers no bytes. Side effects in argument expressions still occur: the example calls two pointer-returning functions and observes both calls even though the copy length is zero.

Use separate, non-overlapping source and destination ranges. These routines copy forward and do not provide overlap-safe memmove behavior; writing into the unread part of the source can corrupt later bytes.

The operation finishes before the caller continues. It neither switches ROM/RAM banks nor clips a range to a buffer boundary. Keep mappings stable, use valid memory ranges and coordinate access with interrupt handlers that use the same buffers.

On FC, dst is a CPU address, not a PPU address. Copying to CPU RAM does not update CHR, nametables or palettes; use PPU/VRAM APIs to upload that data. Generated helpers share argument/scratch storage, so do not re-enter them from NMI/IRQ while another call is in progress.

Usage example

c
memory_reset();
__memcpy(memory_destination+1,memory_source+1,257);
memory_copy_word=memory_check(257,0); // 257 copied bytes; guards unchanged.

This is a calling fragment. Follow the stated initialization and buffer requirements before using it in a complete program.

Expected result

This example uses __memcpy to copy 257 patterned bytes starting at offset 1. memory_check compares the entire source and destination arrays; the COPY WORD row must show 257. Starting inside the buffer leaves a leading guard byte, and bytes after the payload must also remain unchanged.

The complete program resets two 264-byte buffers before each operation, then checks every source byte, every destination byte and the untouched guard regions. The expected rows are COPY WORD 257, FILL WORD 257, COPY BYTE 255, FILL BYTE 255, COPY16 016, COPY32 032, ZERO CALLS 002 and FAILED 000. WORD/BYTE describe the count width, not the size of each copied item. The numbers are produced by the sample's checks, not return values from these void functions.

The captured screens show black lettering on white for DMG, blue on white for CGB and blue on black for FC. This example verifies RAM operations; the lettering presents the results rather than demonstrating a drawing primitive. Separate emulator checks cover 88 memory cases on each platform, including constant and runtime lengths at 0, 1, 16, 17, 32, 33, 64, 65 and 255 bytes, plus 256 and 257 for the word-count forms. Physical hardware has not been tested.

__memcpy: NROM
NROM

Complete example program

Build command

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\memory_intrinsics.c -I .\kitaqfc\lib -I .\kitaq-docs\samples -o .\out\memory_intrinsics.nes --mapper=nrom --nes-chr=.\kitaq-docs\samples\font.chr --no-cache --no-disasm
Implementation and source

kitaqfc/lib/intrinsics.h:19

Declaration notes (original source)
memory / bit / math
Copy len bytes forward between readable/writable CPU ranges; overlapping copies are not memmove.
Zero length performs no memory transfer; argument side effects are evaluated before expansion.

kitaqfc/kitaqfc/CodeGenerator.cs:4607

csharp
case "__memcpy":
                    if (TryEmitMemcpyOrMemsetInline(funcName, args, ctx, isSet: false, lenIndex: 2, lenSize: 2)) return true;
                    return EmitKitaqfcHelperCall(funcName, args, ctx, new[] {2,2,2}, "__memcpy");

bool TryEmitMemcpyOrMemsetInline(string funcName, Expr[] args, FunctionContext ctx, bool isSet, int lenIndex, int lenSize)
        {
            args = args ?? Array.Empty<Expr>();
            int expected = isSet ? 3 : 3;
            if (args.Length != expected) return false;
            if (!TryEvaluateConstant(args[lenIndex], out int len)) return false;
            if (len < 0 || len > 32) return false;
            int valueConst = 0;
            if (isSet && !TryEvaluateConstant(args[1], out valueConst)) return false;
            // A zero-length expansion emits no address or fill evaluation in this helper.
            if (len == 0) return true;
            if (isSet)
            {
                int[] ptr = AcquireTemps(2);
                try
                {
                    EmitLoadValue(args[0], ctx, 2);
                    EmitAsm("STA", Mem(ptr[0]));
                    EmitAsm("STX", Mem(ptr[1]));
                    for (int i = 0; i < len; i++)
                    {
                        EmitAsm("LDY", Imm(i));
                        EmitAsm("LDA", Imm(valueConst));
                        EmitAsm("STA", IndY(ptr[0]));
                    }
                }
                finally { ReleaseTemps(ptr); }
                return true;
            }
            EmitFixedMemcpy(args[0], args[1], len, ctx);
            return true;
        }

void EmitHelperMemoryAndBit()
        {
            // Promote the byte count to a word by clearing its high byte before entering memcpy.
            EmitHelperStart("__memcpy_small");
            EmitAsm("LDA", Imm(0));
            EmitAsm("STA", Mem(CallArgBase + 5));
            EmitAsm("JMP", Abs("__memcpy"));

            // Advance both argument pointers and decrement the word count in place; overlapping copies are not reversed.
            EmitHelperStart("__memcpy");
            string mloop = NewGeneratedLabel("memcpy_loop");
            string mdone = NewGeneratedLabel("memcpy_done");
            EmitAsm("LDA", Mem(CallArgBase + 4));
            EmitAsm("ORA", Mem(CallArgBase + 5));
            EmitAsm("BEQ", Rel(mdone));
            _assembly.Add(Expr.Make(Tag.Label, mloop));
            EmitAsm("LDY", Imm(0));
            EmitAsm("LDA", IndY(CallArgBase + 2));
            EmitAsm("STA", IndY(CallArgBase));
            EmitAsm("INC", Mem(CallArgBase + 2));
            string ms1 = NewGeneratedLabel("memcpy_src_page_ok");
            EmitAsm("BNE", Rel(ms1));
            EmitAsm("INC", Mem(CallArgBase + 3));
            _assembly.Add(Expr.Make(Tag.Label, ms1));
            EmitAsm("INC", Mem(CallArgBase));
            string md1 = NewGeneratedLabel("memcpy_dst_page_ok");
            EmitAsm("BNE", Rel(md1));
            EmitAsm("INC", Mem(CallArgBase + 1));
            _assembly.Add(Expr.Make(Tag.Label, md1));
            EmitAsm("LDA", Mem(CallArgBase + 4));
            string mlononzero = NewGeneratedLabel("memcpy_lenlo_nonzero");
            EmitAsm("BNE", Rel(mlononzero));
            EmitAsm("DEC", Mem(CallArgBase + 5));
            _assembly.Add(Expr.Make(Tag.Label, mlononzero));
            EmitAsm("DEC", Mem(CallArgBase + 4));
            EmitAsm("LDA", Mem(CallArgBase + 4));
            EmitAsm("ORA", Mem(CallArgBase + 5));
            EmitAsm("BNE", Rel(mloop));
            _assembly.Add(Expr.Make(Tag.Label, mdone));
            EmitAsm("RTS");

            // Clear the high count byte for the small-fill wrapper.
            EmitHelperStart("__memset_small");
            EmitAsm("LDA", Imm(0));
            EmitAsm("STA", Mem(CallArgBase + 4));
            EmitAsm("JMP", Abs("__memset"));

            // Advance the destination pointer while consuming the word count in the shared argument area.
            EmitHelperStart("__memset");
            string sloop = NewGeneratedLabel("memset_loop");
            string sdone = NewGeneratedLabel("memset_done");
            EmitAsm("LDA", Mem(CallArgBase + 3));
            EmitAsm("ORA", Mem(CallArgBase + 4));
            EmitAsm("BEQ", Rel(sdone));
            _assembly.Add(Expr.Make(Tag.Label, sloop));
            EmitAsm("LDY", Imm(0));
            EmitAsm("LDA", Mem(CallArgBase + 2));
            EmitAsm("STA", IndY(CallArgBase));
            EmitAsm("INC", Mem(CallArgBase));
            string sd1 = NewGeneratedLabel("memset_dst_page_ok");
            EmitAsm("BNE", Rel(sd1));
            EmitAsm("INC", Mem(CallArgBase + 1));
            _assembly.Add(Expr.Make(Tag.Label, sd1));
            EmitAsm("LDA", Mem(CallArgBase + 3));
            string slononzero = NewGeneratedLabel("memset_lenlo_nonzero");
            EmitAsm("BNE", Rel(slononzero));
            EmitAsm("DEC", Mem(CallArgBase + 4));
            _assembly.Add(Expr.Make(Tag.Label, slononzero));
            EmitAsm("DEC", Mem(CallArgBase + 3));
            EmitAsm("LDA", Mem(CallArgBase + 3));
            EmitAsm("ORA", Mem(CallArgBase + 4));
            EmitAsm("BNE", Rel(sloop));
            _assembly.Add(Expr.Make(Tag.Label, sdone));
            EmitAsm("RTS");
        }

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

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

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

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

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

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

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

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

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

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

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

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

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

                    if (IsStrictEnum(pt))
                    {
                        if (!IsSameEnum(pt, at))
                        {
                            Program.Warning(expr.Source, ErrorCode.EnumStrictMix,
                                "argument {0} to '{1}' expects __enum_strict {2}, got {3}; cast explicitly to silence",
                                i, fn, pt.Show(), at.Show());
                        }
                    }
                    else if (IsPlainInteger(pt) && IsStrictEnum(at))
                    {
                        Program.Warning(expr.Source, ErrorCode.EnumStrictMix,
                            "argument {0} to '{1}' passes __enum_strict {2} to integer parameter {3}; cast explicitly to silence",
                            i, fn, at.Show(), pt.Show());
                    }
                }
            }

            if (!ctx.InUnsafe)
                RestrictAliasLint(ctx, newFunc, newArgs, expr.Source);

            // __slice(ptr,len) helper: represent a "slice" (ptr+len) in the IR.
            // Semantics: evaluate both arguments, result type is the pointer/array (same as arg0).
            // Bounds checks are injected only with -Zcheck (see Program.CheckSliceBounds).
            // Note: keep the intrinsic name conservative to avoid collisions ("slice" is allowed only
            // when there is no declared function named "slice").
            if (newFunc.Match(Tag.Name, out string __sfname))
            {
                bool isSliceIntrinsic = (__sfname == "__slice") || (__sfname == "slice" && ctx.FindFuncParams("slice") == null);
                if (isSliceIntrinsic)
                {
                    if (newArgs.Length != 2)
                    {
                        Program.Error(expr.Source, ErrorCode.ParseError, "{0}: expected 2 arguments: __slice(ptr,len)", __sfname);
                    }
                    else
                    {
                        CType pty = InferType(ctx, newArgs[0]);
                        if (pty == null || !(pty.IsPointer || pty.IsArray))
                            Program.Error(expr.Source, ErrorCode.ExpectedType, "{0}: first argument must be a pointer or array, got: {1}", __sfname, (pty == null ? "<unknown>" : pty.Show()));

                        CType lty = InferType(ctx, newArgs[1]);
                        if (lty == null || !lty.IsInteger)
                            Program.Error(expr.Source, ErrorCode.ExpectedType, "{0}: second argument must be an integer length, got: {1}", __sfname, (lty == null ? "<unknown>" : lty.Show()));
                    }

                    return Expr.Make(Tag.Slice, newArgs[0], newArgs[1]).WithSource(expr.Source);
                }
            }

            return MakeCall(newFunc, newArgs).WithSource(expr.Source);
        }
__memcpy_small

Copy a block of at most 255 bytes using an 8-bit count. Use this form when the transfer length fits in a byte. _small describes the count width; it is not a promise of a particular execution time or generated code size.

Syntax

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

Parameters

dst

Pointer to the first writable destination byte. Reserve enough consecutive bytes for the complete operation; no buffer is allocated for you.

src

Pointer to the first readable source byte. The entire range must remain accessible and unchanged throughout the copy.

len

Number of bytes as u8: 0–255. Do not pass 256 to mean a full 256-byte block; that length does not fit this parameter. Use the corresponding function without _small for 256 bytes or more.

Return value

None (void).

Conditions and limits

A zero count transfers no bytes. Side effects in argument expressions still occur: the example calls two pointer-returning functions and observes both calls even though the copy length is zero.

Use separate, non-overlapping source and destination ranges. These routines copy forward and do not provide overlap-safe memmove behavior; writing into the unread part of the source can corrupt later bytes.

The operation finishes before the caller continues. It neither switches ROM/RAM banks nor clips a range to a buffer boundary. Keep mappings stable, use valid memory ranges and coordinate access with interrupt handlers that use the same buffers.

On FC, dst is a CPU address, not a PPU address. Copying to CPU RAM does not update CHR, nametables or palettes; use PPU/VRAM APIs to upload that data. Generated helpers share argument/scratch storage, so do not re-enter them from NMI/IRQ while another call is in progress.

Usage example

c
memory_reset();
__memcpy_small(memory_destination+1,memory_source+1,255);
memory_copy_byte=memory_check(255,0); // Maximum representable byte count.

This is a calling fragment. Follow the stated initialization and buffer requirements before using it in a complete program.

Expected result

This example uses __memcpy_small to copy 255 patterned bytes starting at offset 1. memory_check compares the entire source and destination arrays; the COPY BYTE row must show 255. Starting inside the buffer leaves a leading guard byte, and bytes after the payload must also remain unchanged.

The complete program resets two 264-byte buffers before each operation, then checks every source byte, every destination byte and the untouched guard regions. The expected rows are COPY WORD 257, FILL WORD 257, COPY BYTE 255, FILL BYTE 255, COPY16 016, COPY32 032, ZERO CALLS 002 and FAILED 000. WORD/BYTE describe the count width, not the size of each copied item. The numbers are produced by the sample's checks, not return values from these void functions.

The captured screens show black lettering on white for DMG, blue on white for CGB and blue on black for FC. This example verifies RAM operations; the lettering presents the results rather than demonstrating a drawing primitive. Separate emulator checks cover 88 memory cases on each platform, including constant and runtime lengths at 0, 1, 16, 17, 32, 33, 64, 65 and 255 bytes, plus 256 and 257 for the word-count forms. Physical hardware has not been tested.

__memcpy_small: NROM
NROM

Complete example program

Build command

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\memory_intrinsics.c -I .\kitaqfc\lib -I .\kitaq-docs\samples -o .\out\memory_intrinsics.nes --mapper=nrom --nes-chr=.\kitaq-docs\samples\font.chr --no-cache --no-disasm
Implementation and source

kitaqfc/lib/intrinsics.h:23

Declaration notes (original source)
Forward copy with an 8-bit length, zero through 255; the dynamic helper promotes it to a word count.

kitaqfc/kitaqfc/CodeGenerator.cs:4610

csharp
case "__memcpy_small":
                    if (TryEmitMemcpyOrMemsetInline(funcName, args, ctx, isSet: false, lenIndex: 2, lenSize: 1)) return true;
                    return EmitKitaqfcHelperCall(funcName, args, ctx, new[] {2,2,1}, "__memcpy_small");

bool TryEmitMemcpyOrMemsetInline(string funcName, Expr[] args, FunctionContext ctx, bool isSet, int lenIndex, int lenSize)
        {
            args = args ?? Array.Empty<Expr>();
            int expected = isSet ? 3 : 3;
            if (args.Length != expected) return false;
            if (!TryEvaluateConstant(args[lenIndex], out int len)) return false;
            if (len < 0 || len > 32) return false;
            int valueConst = 0;
            if (isSet && !TryEvaluateConstant(args[1], out valueConst)) return false;
            // A zero-length expansion emits no address or fill evaluation in this helper.
            if (len == 0) return true;
            if (isSet)
            {
                int[] ptr = AcquireTemps(2);
                try
                {
                    EmitLoadValue(args[0], ctx, 2);
                    EmitAsm("STA", Mem(ptr[0]));
                    EmitAsm("STX", Mem(ptr[1]));
                    for (int i = 0; i < len; i++)
                    {
                        EmitAsm("LDY", Imm(i));
                        EmitAsm("LDA", Imm(valueConst));
                        EmitAsm("STA", IndY(ptr[0]));
                    }
                }
                finally { ReleaseTemps(ptr); }
                return true;
            }
            EmitFixedMemcpy(args[0], args[1], len, ctx);
            return true;
        }

void EmitHelperMemoryAndBit()
        {
            // Promote the byte count to a word by clearing its high byte before entering memcpy.
            EmitHelperStart("__memcpy_small");
            EmitAsm("LDA", Imm(0));
            EmitAsm("STA", Mem(CallArgBase + 5));
            EmitAsm("JMP", Abs("__memcpy"));

            // Advance both argument pointers and decrement the word count in place; overlapping copies are not reversed.
            EmitHelperStart("__memcpy");
            string mloop = NewGeneratedLabel("memcpy_loop");
            string mdone = NewGeneratedLabel("memcpy_done");
            EmitAsm("LDA", Mem(CallArgBase + 4));
            EmitAsm("ORA", Mem(CallArgBase + 5));
            EmitAsm("BEQ", Rel(mdone));
            _assembly.Add(Expr.Make(Tag.Label, mloop));
            EmitAsm("LDY", Imm(0));
            EmitAsm("LDA", IndY(CallArgBase + 2));
            EmitAsm("STA", IndY(CallArgBase));
            EmitAsm("INC", Mem(CallArgBase + 2));
            string ms1 = NewGeneratedLabel("memcpy_src_page_ok");
            EmitAsm("BNE", Rel(ms1));
            EmitAsm("INC", Mem(CallArgBase + 3));
            _assembly.Add(Expr.Make(Tag.Label, ms1));
            EmitAsm("INC", Mem(CallArgBase));
            string md1 = NewGeneratedLabel("memcpy_dst_page_ok");
            EmitAsm("BNE", Rel(md1));
            EmitAsm("INC", Mem(CallArgBase + 1));
            _assembly.Add(Expr.Make(Tag.Label, md1));
            EmitAsm("LDA", Mem(CallArgBase + 4));
            string mlononzero = NewGeneratedLabel("memcpy_lenlo_nonzero");
            EmitAsm("BNE", Rel(mlononzero));
            EmitAsm("DEC", Mem(CallArgBase + 5));
            _assembly.Add(Expr.Make(Tag.Label, mlononzero));
            EmitAsm("DEC", Mem(CallArgBase + 4));
            EmitAsm("LDA", Mem(CallArgBase + 4));
            EmitAsm("ORA", Mem(CallArgBase + 5));
            EmitAsm("BNE", Rel(mloop));
            _assembly.Add(Expr.Make(Tag.Label, mdone));
            EmitAsm("RTS");

            // Clear the high count byte for the small-fill wrapper.
            EmitHelperStart("__memset_small");
            EmitAsm("LDA", Imm(0));
            EmitAsm("STA", Mem(CallArgBase + 4));
            EmitAsm("JMP", Abs("__memset"));

            // Advance the destination pointer while consuming the word count in the shared argument area.
            EmitHelperStart("__memset");
            string sloop = NewGeneratedLabel("memset_loop");
            string sdone = NewGeneratedLabel("memset_done");
            EmitAsm("LDA", Mem(CallArgBase + 3));
            EmitAsm("ORA", Mem(CallArgBase + 4));
            EmitAsm("BEQ", Rel(sdone));
            _assembly.Add(Expr.Make(Tag.Label, sloop));
            EmitAsm("LDY", Imm(0));
            EmitAsm("LDA", Mem(CallArgBase + 2));
            EmitAsm("STA", IndY(CallArgBase));
            EmitAsm("INC", Mem(CallArgBase));
            string sd1 = NewGeneratedLabel("memset_dst_page_ok");
            EmitAsm("BNE", Rel(sd1));
            EmitAsm("INC", Mem(CallArgBase + 1));
            _assembly.Add(Expr.Make(Tag.Label, sd1));
            EmitAsm("LDA", Mem(CallArgBase + 3));
            string slononzero = NewGeneratedLabel("memset_lenlo_nonzero");
            EmitAsm("BNE", Rel(slononzero));
            EmitAsm("DEC", Mem(CallArgBase + 4));
            _assembly.Add(Expr.Make(Tag.Label, slononzero));
            EmitAsm("DEC", Mem(CallArgBase + 3));
            EmitAsm("LDA", Mem(CallArgBase + 3));
            EmitAsm("ORA", Mem(CallArgBase + 4));
            EmitAsm("BNE", Rel(sloop));
            _assembly.Add(Expr.Make(Tag.Label, sdone));
            EmitAsm("RTS");
        }

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

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

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

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

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

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

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

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

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

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

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

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

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

                    if (IsStrictEnum(pt))
                    {
                        if (!IsSameEnum(pt, at))
                        {
                            Program.Warning(expr.Source, ErrorCode.EnumStrictMix,
                                "argument {0} to '{1}' expects __enum_strict {2}, got {3}; cast explicitly to silence",
                                i, fn, pt.Show(), at.Show());
                        }
                    }
                    else if (IsPlainInteger(pt) && IsStrictEnum(at))
                    {
                        Program.Warning(expr.Source, ErrorCode.EnumStrictMix,
                            "argument {0} to '{1}' passes __enum_strict {2} to integer parameter {3}; cast explicitly to silence",
                            i, fn, at.Show(), pt.Show());
                    }
                }
            }

            if (!ctx.InUnsafe)
                RestrictAliasLint(ctx, newFunc, newArgs, expr.Source);

            // __slice(ptr,len) helper: represent a "slice" (ptr+len) in the IR.
            // Semantics: evaluate both arguments, result type is the pointer/array (same as arg0).
            // Bounds checks are injected only with -Zcheck (see Program.CheckSliceBounds).
            // Note: keep the intrinsic name conservative to avoid collisions ("slice" is allowed only
            // when there is no declared function named "slice").
            if (newFunc.Match(Tag.Name, out string __sfname))
            {
                bool isSliceIntrinsic = (__sfname == "__slice") || (__sfname == "slice" && ctx.FindFuncParams("slice") == null);
                if (isSliceIntrinsic)
                {
                    if (newArgs.Length != 2)
                    {
                        Program.Error(expr.Source, ErrorCode.ParseError, "{0}: expected 2 arguments: __slice(ptr,len)", __sfname);
                    }
                    else
                    {
                        CType pty = InferType(ctx, newArgs[0]);
                        if (pty == null || !(pty.IsPointer || pty.IsArray))
                            Program.Error(expr.Source, ErrorCode.ExpectedType, "{0}: first argument must be a pointer or array, got: {1}", __sfname, (pty == null ? "<unknown>" : pty.Show()));

                        CType lty = InferType(ctx, newArgs[1]);
                        if (lty == null || !lty.IsInteger)
                            Program.Error(expr.Source, ErrorCode.ExpectedType, "{0}: second argument must be an integer length, got: {1}", __sfname, (lty == null ? "<unknown>" : lty.Show()));
                    }

                    return Expr.Make(Tag.Slice, newArgs[0], newArgs[1]).WithSource(expr.Source);
                }
            }

            return MakeCall(newFunc, newArgs).WithSource(expr.Source);
        }
__memset

Write the same byte value throughout a destination range. The 16-bit count is useful for clearing a large buffer or initializing a table. For example, filling with 0x5A writes 5A 5A 5A…; it does not repeat a multibyte integer.

Syntax

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

Parameters

dst

Pointer to the first writable destination byte. Reserve enough consecutive bytes for the complete operation; no buffer is allocated for you.

value

The byte repeated at every destination address: 0–255. Use 0 to clear the range. To initialize a u16 array to a nonzero numeric value, assign its elements individually instead.

len

Number of bytes as u16 (0–65535), not an element count. The type's maximum is not a promise that such a large valid buffer exists on the target machine. Keep the whole range within the selected memory mapping.

Return value

None (void).

Conditions and limits

A zero count transfers no bytes. Side effects in argument expressions still occur: the example calls two pointer-returning functions and observes both calls even though the copy length is zero.

The operation finishes before the caller continues. It neither switches ROM/RAM banks nor clips a range to a buffer boundary. Keep mappings stable, use valid memory ranges and coordinate access with interrupt handlers that use the same buffers.

On FC, dst is a CPU address, not a PPU address. Copying to CPU RAM does not update CHR, nametables or palettes; use PPU/VRAM APIs to upload that data. Generated helpers share argument/scratch storage, so do not re-enter them from NMI/IRQ while another call is in progress.

Usage example

c
memory_reset();
__memset(memory_destination+1,0x5A,257);
memory_fill_word=memory_check(257,1); // Repeat one byte, not a 16-bit word.

This is a calling fragment. Follow the stated initialization and buffer requirements before using it in a complete program.

Expected result

This example uses __memset to fill 257 bytes at offset 1 with 0x5A. memory_check requires every byte in that range to equal 0x5A, while the guard bytes stay 0xCC and the separate source buffer remains untouched. The FILL WORD row must show 257.

The complete program resets two 264-byte buffers before each operation, then checks every source byte, every destination byte and the untouched guard regions. The expected rows are COPY WORD 257, FILL WORD 257, COPY BYTE 255, FILL BYTE 255, COPY16 016, COPY32 032, ZERO CALLS 002 and FAILED 000. WORD/BYTE describe the count width, not the size of each copied item. The numbers are produced by the sample's checks, not return values from these void functions.

The captured screens show black lettering on white for DMG, blue on white for CGB and blue on black for FC. This example verifies RAM operations; the lettering presents the results rather than demonstrating a drawing primitive. Separate emulator checks cover 88 memory cases on each platform, including constant and runtime lengths at 0, 1, 16, 17, 32, 33, 64, 65 and 255 bytes, plus 256 and 257 for the word-count forms. Physical hardware has not been tested.

__memset: NROM
NROM

Complete example program

Build command

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\memory_intrinsics.c -I .\kitaqfc\lib -I .\kitaq-docs\samples -o .\out\memory_intrinsics.nes --mapper=nrom --nes-chr=.\kitaq-docs\samples\font.chr --no-cache --no-disasm
Implementation and source

kitaqfc/lib/intrinsics.h:21

Declaration notes (original source)
Fill len CPU bytes with value. Zero length still evaluates argument side effects.

kitaqfc/kitaqfc/CodeGenerator.cs:4613

csharp
case "__memset":
                    if (TryEmitMemcpyOrMemsetInline(funcName, args, ctx, isSet: true, lenIndex: 2, lenSize: 2)) return true;
                    return EmitKitaqfcHelperCall(funcName, args, ctx, new[] {2,1,2}, "__memset");

bool TryEmitMemcpyOrMemsetInline(string funcName, Expr[] args, FunctionContext ctx, bool isSet, int lenIndex, int lenSize)
        {
            args = args ?? Array.Empty<Expr>();
            int expected = isSet ? 3 : 3;
            if (args.Length != expected) return false;
            if (!TryEvaluateConstant(args[lenIndex], out int len)) return false;
            if (len < 0 || len > 32) return false;
            int valueConst = 0;
            if (isSet && !TryEvaluateConstant(args[1], out valueConst)) return false;
            // A zero-length expansion emits no address or fill evaluation in this helper.
            if (len == 0) return true;
            if (isSet)
            {
                int[] ptr = AcquireTemps(2);
                try
                {
                    EmitLoadValue(args[0], ctx, 2);
                    EmitAsm("STA", Mem(ptr[0]));
                    EmitAsm("STX", Mem(ptr[1]));
                    for (int i = 0; i < len; i++)
                    {
                        EmitAsm("LDY", Imm(i));
                        EmitAsm("LDA", Imm(valueConst));
                        EmitAsm("STA", IndY(ptr[0]));
                    }
                }
                finally { ReleaseTemps(ptr); }
                return true;
            }
            EmitFixedMemcpy(args[0], args[1], len, ctx);
            return true;
        }

void EmitHelperMemoryAndBit()
        {
            // Promote the byte count to a word by clearing its high byte before entering memcpy.
            EmitHelperStart("__memcpy_small");
            EmitAsm("LDA", Imm(0));
            EmitAsm("STA", Mem(CallArgBase + 5));
            EmitAsm("JMP", Abs("__memcpy"));

            // Advance both argument pointers and decrement the word count in place; overlapping copies are not reversed.
            EmitHelperStart("__memcpy");
            string mloop = NewGeneratedLabel("memcpy_loop");
            string mdone = NewGeneratedLabel("memcpy_done");
            EmitAsm("LDA", Mem(CallArgBase + 4));
            EmitAsm("ORA", Mem(CallArgBase + 5));
            EmitAsm("BEQ", Rel(mdone));
            _assembly.Add(Expr.Make(Tag.Label, mloop));
            EmitAsm("LDY", Imm(0));
            EmitAsm("LDA", IndY(CallArgBase + 2));
            EmitAsm("STA", IndY(CallArgBase));
            EmitAsm("INC", Mem(CallArgBase + 2));
            string ms1 = NewGeneratedLabel("memcpy_src_page_ok");
            EmitAsm("BNE", Rel(ms1));
            EmitAsm("INC", Mem(CallArgBase + 3));
            _assembly.Add(Expr.Make(Tag.Label, ms1));
            EmitAsm("INC", Mem(CallArgBase));
            string md1 = NewGeneratedLabel("memcpy_dst_page_ok");
            EmitAsm("BNE", Rel(md1));
            EmitAsm("INC", Mem(CallArgBase + 1));
            _assembly.Add(Expr.Make(Tag.Label, md1));
            EmitAsm("LDA", Mem(CallArgBase + 4));
            string mlononzero = NewGeneratedLabel("memcpy_lenlo_nonzero");
            EmitAsm("BNE", Rel(mlononzero));
            EmitAsm("DEC", Mem(CallArgBase + 5));
            _assembly.Add(Expr.Make(Tag.Label, mlononzero));
            EmitAsm("DEC", Mem(CallArgBase + 4));
            EmitAsm("LDA", Mem(CallArgBase + 4));
            EmitAsm("ORA", Mem(CallArgBase + 5));
            EmitAsm("BNE", Rel(mloop));
            _assembly.Add(Expr.Make(Tag.Label, mdone));
            EmitAsm("RTS");

            // Clear the high count byte for the small-fill wrapper.
            EmitHelperStart("__memset_small");
            EmitAsm("LDA", Imm(0));
            EmitAsm("STA", Mem(CallArgBase + 4));
            EmitAsm("JMP", Abs("__memset"));

            // Advance the destination pointer while consuming the word count in the shared argument area.
            EmitHelperStart("__memset");
            string sloop = NewGeneratedLabel("memset_loop");
            string sdone = NewGeneratedLabel("memset_done");
            EmitAsm("LDA", Mem(CallArgBase + 3));
            EmitAsm("ORA", Mem(CallArgBase + 4));
            EmitAsm("BEQ", Rel(sdone));
            _assembly.Add(Expr.Make(Tag.Label, sloop));
            EmitAsm("LDY", Imm(0));
            EmitAsm("LDA", Mem(CallArgBase + 2));
            EmitAsm("STA", IndY(CallArgBase));
            EmitAsm("INC", Mem(CallArgBase));
            string sd1 = NewGeneratedLabel("memset_dst_page_ok");
            EmitAsm("BNE", Rel(sd1));
            EmitAsm("INC", Mem(CallArgBase + 1));
            _assembly.Add(Expr.Make(Tag.Label, sd1));
            EmitAsm("LDA", Mem(CallArgBase + 3));
            string slononzero = NewGeneratedLabel("memset_lenlo_nonzero");
            EmitAsm("BNE", Rel(slononzero));
            EmitAsm("DEC", Mem(CallArgBase + 4));
            _assembly.Add(Expr.Make(Tag.Label, slononzero));
            EmitAsm("DEC", Mem(CallArgBase + 3));
            EmitAsm("LDA", Mem(CallArgBase + 3));
            EmitAsm("ORA", Mem(CallArgBase + 4));
            EmitAsm("BNE", Rel(sloop));
            _assembly.Add(Expr.Make(Tag.Label, sdone));
            EmitAsm("RTS");
        }

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

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

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

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

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

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

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

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

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

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

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

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

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

                    if (IsStrictEnum(pt))
                    {
                        if (!IsSameEnum(pt, at))
                        {
                            Program.Warning(expr.Source, ErrorCode.EnumStrictMix,
                                "argument {0} to '{1}' expects __enum_strict {2}, got {3}; cast explicitly to silence",
                                i, fn, pt.Show(), at.Show());
                        }
                    }
                    else if (IsPlainInteger(pt) && IsStrictEnum(at))
                    {
                        Program.Warning(expr.Source, ErrorCode.EnumStrictMix,
                            "argument {0} to '{1}' passes __enum_strict {2} to integer parameter {3}; cast explicitly to silence",
                            i, fn, at.Show(), pt.Show());
                    }
                }
            }

            if (!ctx.InUnsafe)
                RestrictAliasLint(ctx, newFunc, newArgs, expr.Source);

            // __slice(ptr,len) helper: represent a "slice" (ptr+len) in the IR.
            // Semantics: evaluate both arguments, result type is the pointer/array (same as arg0).
            // Bounds checks are injected only with -Zcheck (see Program.CheckSliceBounds).
            // Note: keep the intrinsic name conservative to avoid collisions ("slice" is allowed only
            // when there is no declared function named "slice").
            if (newFunc.Match(Tag.Name, out string __sfname))
            {
                bool isSliceIntrinsic = (__sfname == "__slice") || (__sfname == "slice" && ctx.FindFuncParams("slice") == null);
                if (isSliceIntrinsic)
                {
                    if (newArgs.Length != 2)
                    {
                        Program.Error(expr.Source, ErrorCode.ParseError, "{0}: expected 2 arguments: __slice(ptr,len)", __sfname);
                    }
                    else
                    {
                        CType pty = InferType(ctx, newArgs[0]);
                        if (pty == null || !(pty.IsPointer || pty.IsArray))
                            Program.Error(expr.Source, ErrorCode.ExpectedType, "{0}: first argument must be a pointer or array, got: {1}", __sfname, (pty == null ? "<unknown>" : pty.Show()));

                        CType lty = InferType(ctx, newArgs[1]);
                        if (lty == null || !lty.IsInteger)
                            Program.Error(expr.Source, ErrorCode.ExpectedType, "{0}: second argument must be an integer length, got: {1}", __sfname, (lty == null ? "<unknown>" : lty.Show()));
                    }

                    return Expr.Make(Tag.Slice, newArgs[0], newArgs[1]).WithSource(expr.Source);
                }
            }

            return MakeCall(newFunc, newArgs).WithSource(expr.Source);
        }
__memset_small

Fill up to 255 consecutive bytes with one byte value, using an 8-bit count. This is suitable for initializing a small working buffer. Choose __memset when the required length exceeds 255 bytes.

Syntax

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

Parameters

dst

Pointer to the first writable destination byte. Reserve enough consecutive bytes for the complete operation; no buffer is allocated for you.

value

The byte repeated at every destination address: 0–255. Use 0 to clear the range. To initialize a u16 array to a nonzero numeric value, assign its elements individually instead.

len

Number of bytes as u8: 0–255. Do not pass 256 to mean a full 256-byte block; that length does not fit this parameter. Use the corresponding function without _small for 256 bytes or more.

Return value

None (void).

Conditions and limits

A zero count transfers no bytes. Side effects in argument expressions still occur: the example calls two pointer-returning functions and observes both calls even though the copy length is zero.

The operation finishes before the caller continues. It neither switches ROM/RAM banks nor clips a range to a buffer boundary. Keep mappings stable, use valid memory ranges and coordinate access with interrupt handlers that use the same buffers.

On FC, dst is a CPU address, not a PPU address. Copying to CPU RAM does not update CHR, nametables or palettes; use PPU/VRAM APIs to upload that data. Generated helpers share argument/scratch storage, so do not re-enter them from NMI/IRQ while another call is in progress.

Usage example

c
memory_reset();
__memset_small(memory_destination+1,0x5A,255);
memory_fill_byte=memory_check(255,1);

This is a calling fragment. Follow the stated initialization and buffer requirements before using it in a complete program.

Expected result

This example uses __memset_small to fill 255 bytes at offset 1 with 0x5A. memory_check requires every byte in that range to equal 0x5A, while the guard bytes stay 0xCC and the separate source buffer remains untouched. The FILL BYTE row must show 255.

The complete program resets two 264-byte buffers before each operation, then checks every source byte, every destination byte and the untouched guard regions. The expected rows are COPY WORD 257, FILL WORD 257, COPY BYTE 255, FILL BYTE 255, COPY16 016, COPY32 032, ZERO CALLS 002 and FAILED 000. WORD/BYTE describe the count width, not the size of each copied item. The numbers are produced by the sample's checks, not return values from these void functions.

The captured screens show black lettering on white for DMG, blue on white for CGB and blue on black for FC. This example verifies RAM operations; the lettering presents the results rather than demonstrating a drawing primitive. Separate emulator checks cover 88 memory cases on each platform, including constant and runtime lengths at 0, 1, 16, 17, 32, 33, 64, 65 and 255 bytes, plus 256 and 257 for the word-count forms. Physical hardware has not been tested.

__memset_small: NROM
NROM

Complete example program

Build command

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\memory_intrinsics.c -I .\kitaqfc\lib -I .\kitaq-docs\samples -o .\out\memory_intrinsics.nes --mapper=nrom --nes-chr=.\kitaq-docs\samples\font.chr --no-cache --no-disasm
Implementation and source

kitaqfc/lib/intrinsics.h:25

Declaration notes (original source)
Fill zero through 255 CPU bytes; this is ordinary RAM access, not a PPU transfer.

kitaqfc/kitaqfc/CodeGenerator.cs:4616

csharp
case "__memset_small":
                    if (TryEmitMemcpyOrMemsetInline(funcName, args, ctx, isSet: true, lenIndex: 2, lenSize: 1)) return true;
                    return EmitKitaqfcHelperCall(funcName, args, ctx, new[] {2,1,1}, "__memset_small");
                // These fixed-size copies bypass the variable-length helper argument area.

bool TryEmitMemcpyOrMemsetInline(string funcName, Expr[] args, FunctionContext ctx, bool isSet, int lenIndex, int lenSize)
        {
            args = args ?? Array.Empty<Expr>();
            int expected = isSet ? 3 : 3;
            if (args.Length != expected) return false;
            if (!TryEvaluateConstant(args[lenIndex], out int len)) return false;
            if (len < 0 || len > 32) return false;
            int valueConst = 0;
            if (isSet && !TryEvaluateConstant(args[1], out valueConst)) return false;
            // A zero-length expansion emits no address or fill evaluation in this helper.
            if (len == 0) return true;
            if (isSet)
            {
                int[] ptr = AcquireTemps(2);
                try
                {
                    EmitLoadValue(args[0], ctx, 2);
                    EmitAsm("STA", Mem(ptr[0]));
                    EmitAsm("STX", Mem(ptr[1]));
                    for (int i = 0; i < len; i++)
                    {
                        EmitAsm("LDY", Imm(i));
                        EmitAsm("LDA", Imm(valueConst));
                        EmitAsm("STA", IndY(ptr[0]));
                    }
                }
                finally { ReleaseTemps(ptr); }
                return true;
            }
            EmitFixedMemcpy(args[0], args[1], len, ctx);
            return true;
        }

void EmitHelperMemoryAndBit()
        {
            // Promote the byte count to a word by clearing its high byte before entering memcpy.
            EmitHelperStart("__memcpy_small");
            EmitAsm("LDA", Imm(0));
            EmitAsm("STA", Mem(CallArgBase + 5));
            EmitAsm("JMP", Abs("__memcpy"));

            // Advance both argument pointers and decrement the word count in place; overlapping copies are not reversed.
            EmitHelperStart("__memcpy");
            string mloop = NewGeneratedLabel("memcpy_loop");
            string mdone = NewGeneratedLabel("memcpy_done");
            EmitAsm("LDA", Mem(CallArgBase + 4));
            EmitAsm("ORA", Mem(CallArgBase + 5));
            EmitAsm("BEQ", Rel(mdone));
            _assembly.Add(Expr.Make(Tag.Label, mloop));
            EmitAsm("LDY", Imm(0));
            EmitAsm("LDA", IndY(CallArgBase + 2));
            EmitAsm("STA", IndY(CallArgBase));
            EmitAsm("INC", Mem(CallArgBase + 2));
            string ms1 = NewGeneratedLabel("memcpy_src_page_ok");
            EmitAsm("BNE", Rel(ms1));
            EmitAsm("INC", Mem(CallArgBase + 3));
            _assembly.Add(Expr.Make(Tag.Label, ms1));
            EmitAsm("INC", Mem(CallArgBase));
            string md1 = NewGeneratedLabel("memcpy_dst_page_ok");
            EmitAsm("BNE", Rel(md1));
            EmitAsm("INC", Mem(CallArgBase + 1));
            _assembly.Add(Expr.Make(Tag.Label, md1));
            EmitAsm("LDA", Mem(CallArgBase + 4));
            string mlononzero = NewGeneratedLabel("memcpy_lenlo_nonzero");
            EmitAsm("BNE", Rel(mlononzero));
            EmitAsm("DEC", Mem(CallArgBase + 5));
            _assembly.Add(Expr.Make(Tag.Label, mlononzero));
            EmitAsm("DEC", Mem(CallArgBase + 4));
            EmitAsm("LDA", Mem(CallArgBase + 4));
            EmitAsm("ORA", Mem(CallArgBase + 5));
            EmitAsm("BNE", Rel(mloop));
            _assembly.Add(Expr.Make(Tag.Label, mdone));
            EmitAsm("RTS");

            // Clear the high count byte for the small-fill wrapper.
            EmitHelperStart("__memset_small");
            EmitAsm("LDA", Imm(0));
            EmitAsm("STA", Mem(CallArgBase + 4));
            EmitAsm("JMP", Abs("__memset"));

            // Advance the destination pointer while consuming the word count in the shared argument area.
            EmitHelperStart("__memset");
            string sloop = NewGeneratedLabel("memset_loop");
            string sdone = NewGeneratedLabel("memset_done");
            EmitAsm("LDA", Mem(CallArgBase + 3));
            EmitAsm("ORA", Mem(CallArgBase + 4));
            EmitAsm("BEQ", Rel(sdone));
            _assembly.Add(Expr.Make(Tag.Label, sloop));
            EmitAsm("LDY", Imm(0));
            EmitAsm("LDA", Mem(CallArgBase + 2));
            EmitAsm("STA", IndY(CallArgBase));
            EmitAsm("INC", Mem(CallArgBase));
            string sd1 = NewGeneratedLabel("memset_dst_page_ok");
            EmitAsm("BNE", Rel(sd1));
            EmitAsm("INC", Mem(CallArgBase + 1));
            _assembly.Add(Expr.Make(Tag.Label, sd1));
            EmitAsm("LDA", Mem(CallArgBase + 3));
            string slononzero = NewGeneratedLabel("memset_lenlo_nonzero");
            EmitAsm("BNE", Rel(slononzero));
            EmitAsm("DEC", Mem(CallArgBase + 4));
            _assembly.Add(Expr.Make(Tag.Label, slononzero));
            EmitAsm("DEC", Mem(CallArgBase + 3));
            EmitAsm("LDA", Mem(CallArgBase + 3));
            EmitAsm("ORA", Mem(CallArgBase + 4));
            EmitAsm("BNE", Rel(sloop));
            _assembly.Add(Expr.Make(Tag.Label, sdone));
            EmitAsm("RTS");
        }

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

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

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

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

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

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

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

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

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

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

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

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

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

                    if (IsStrictEnum(pt))
                    {
                        if (!IsSameEnum(pt, at))
                        {
                            Program.Warning(expr.Source, ErrorCode.EnumStrictMix,
                                "argument {0} to '{1}' expects __enum_strict {2}, got {3}; cast explicitly to silence",
                                i, fn, pt.Show(), at.Show());
                        }
                    }
                    else if (IsPlainInteger(pt) && IsStrictEnum(at))
                    {
                        Program.Warning(expr.Source, ErrorCode.EnumStrictMix,
                            "argument {0} to '{1}' passes __enum_strict {2} to integer parameter {3}; cast explicitly to silence",
                            i, fn, at.Show(), pt.Show());
                    }
                }
            }

            if (!ctx.InUnsafe)
                RestrictAliasLint(ctx, newFunc, newArgs, expr.Source);

            // __slice(ptr,len) helper: represent a "slice" (ptr+len) in the IR.
            // Semantics: evaluate both arguments, result type is the pointer/array (same as arg0).
            // Bounds checks are injected only with -Zcheck (see Program.CheckSliceBounds).
            // Note: keep the intrinsic name conservative to avoid collisions ("slice" is allowed only
            // when there is no declared function named "slice").
            if (newFunc.Match(Tag.Name, out string __sfname))
            {
                bool isSliceIntrinsic = (__sfname == "__slice") || (__sfname == "slice" && ctx.FindFuncParams("slice") == null);
                if (isSliceIntrinsic)
                {
                    if (newArgs.Length != 2)
                    {
                        Program.Error(expr.Source, ErrorCode.ParseError, "{0}: expected 2 arguments: __slice(ptr,len)", __sfname);
                    }
                    else
                    {
                        CType pty = InferType(ctx, newArgs[0]);
                        if (pty == null || !(pty.IsPointer || pty.IsArray))
                            Program.Error(expr.Source, ErrorCode.ExpectedType, "{0}: first argument must be a pointer or array, got: {1}", __sfname, (pty == null ? "<unknown>" : pty.Show()));

                        CType lty = InferType(ctx, newArgs[1]);
                        if (lty == null || !lty.IsInteger)
                            Program.Error(expr.Source, ErrorCode.ExpectedType, "{0}: second argument must be an integer length, got: {1}", __sfname, (lty == null ? "<unknown>" : lty.Show()));
                    }

                    return Expr.Make(Tag.Slice, newArgs[0], newArgs[1]).WithSource(expr.Source);
                }
            }

            return MakeCall(newFunc, newArgs).WithSource(expr.Source);
        }
__metasprite_draw

Expand a terminated stream of sprite parts into consecutive entries in the page-02 shadow buffer. Each part adds byte-sized X/Y offsets to the base position and supplies its own tile and attributes. Return the next OAM index.

Syntax

c
u8 __metasprite_draw(u8 oam_index, u8 base_x, u8 base_y, const u8* metasprite);

Parameters

oam_index

Sprite entry number, 0–63, not a byte offset. The implementation multiplies it by four using an 8-bit cursor; out-of-range values wrap modulo 64 and can overwrite another entry. No allocation check is performed.

base_x

Horizontal position in pixels, stored as an unsigned byte. This is a pixel coordinate, not a tile coordinate.

base_y

Raw OAM Y byte. Use visible_top-1 for a visible top below scanline zero; values 239..255 are outside the visible 240-line area.

metasprite

Pointer to readable four-byte records: dx, dy, tile, attr. A byte 0xFF in the next dx position ends the stream; no other bytes are needed after it. Offsets add modulo 256: 0xFE represents −2 and dy=0xFF can represent −1, but dx=0xFF is reserved and cannot represent −1. The pointer must remain valid in the currently mapped memory.

Return value

Next OAM entry number, (oam_index + parts_written) modulo 64. This is a cursor, not the number of parts drawn. For an empty stream it is oam_index modulo 64.

Conditions and limits

These operations edit CPU RAM at 0x0200–0x02FF; they do not immediately change hardware OAM. Transfer the completed page with __oam_dma. They do not maintain the sprite library’s allocation flags, coordinate arrays or active count. Keep interrupt handlers from editing or transferring a partially prepared page.

There is no part count, clipping or capacity check. Include a terminator and use at most 64−oam_index parts. Both coordinates wrap modulo 256; after slot 63 the write cursor wraps to slot 0. An empty stream changes no entries. This stream format differs from the counted MetaSpritePart array used by the sprite library’s metasprite_draw.

Usage example

c
fc_oam_next=__metasprite_draw(5,136,95,fc_oam_pair); // Returns 7 after two entries.

This is a calling fragment. Follow the stated initialization and buffer requirements before using it in a complete program.

Expected result

The example separates each operation into a labeled row. SET and MOVE show 8×8 right arrows at (144,16) and (144,32); TILE shows a green square at (144,48); ATTR shows the arrow flipped both ways at (144,64). HIDE at (144,80) and CLEAR at (144,112) must stay empty. META shows a pair at (136,96) and (144,96), with the second arrow mirrored horizontally. NEXT 07 is the next write position after writing slots 5 and 6. Arrows have red edges, green shafts and blue tip details. Verification compares 8,704 pixels per image and checks the transferred OAM bytes; physical hardware is untested.

TRANSFER shows an arrow at (144,128). This program sends page 02 directly with __oam_dma, including every field prepared by the individual sprite operations.

__metasprite_draw: PAGE02
PAGE02

Complete example program

Build command

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\oam_intrinsics.c -I .\kitaqfc\lib -I .\kitaq-docs\samples --nes-chr=.\kitaq-docs\samples\sprite_example.chr -o .\out\oam_intrinsics.nes --no-cache --no-disasm
Implementation and source

kitaqfc/lib/intrinsics.h:203

Declaration notes (original source)
Expand dx,dy,tile,attr records into page-02 shadow OAM until dx=FF; return the next index.
Coordinates and the byte OAM cursor wrap; keep the terminated stream within the available slots.
csharp
EmitHelperStart("__metasprite_draw");
            // args: oam_index, base_x, base_y, metasprite_ptr16.
            // stream: dx, dy, tile, attr ... dx=$FF terminates. Returns next OAM index in A.
            EmitAsm("LDA", Mem(CallArgBase));
            EmitAsm("ASL");
            EmitAsm("ASL");
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp2Address)); // dst byte offset
            string msLoop = NewGeneratedLabel("metasprite_loop");
            string msDone = NewGeneratedLabel("metasprite_done");
            _assembly.Add(Expr.Make(Tag.Label, msLoop));
            EmitAsm("LDY", Imm(0));
            EmitAsm("LDA", IndY(CallArgBase + 3));
            EmitAsm("CMP", Imm(0xFF));
            EmitAsm("BEQ", Rel(msDone));
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp0Address)); // dx
            EmitAsm("LDX", Mem(_runtimeIntrinsicTmp2Address));
            EmitAsm("INY");
            EmitAsm("LDA", IndY(CallArgBase + 3));
            EmitAsm("CLC");
            EmitAsm("ADC", Mem(CallArgBase + 2));
            EmitAsm("STA", new AsmOperand(OamRamBase + 0, AddressMode.AbsoluteX));
            EmitAsm("INY");
            EmitAsm("LDA", IndY(CallArgBase + 3));
            EmitAsm("STA", new AsmOperand(OamRamBase + 1, AddressMode.AbsoluteX));
            EmitAsm("INY");
            EmitAsm("LDA", IndY(CallArgBase + 3));
            EmitAsm("STA", new AsmOperand(OamRamBase + 2, AddressMode.AbsoluteX));
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("CLC");
            EmitAsm("ADC", Mem(CallArgBase + 1));
            EmitAsm("STA", new AsmOperand(OamRamBase + 3, AddressMode.AbsoluteX));
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp2Address));
            EmitAsm("CLC");
            EmitAsm("ADC", Imm(4));
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp2Address));
            EmitAsm("LDA", Mem(CallArgBase + 3));
            EmitAsm("CLC");
            EmitAsm("ADC", Imm(4));
            EmitAsm("STA", Mem(CallArgBase + 3));
            // Propagate stream-pointer page carry after consuming the complete four-byte record.
            string msPtrOk = NewGeneratedLabel("metasprite_ptr_ok");
            EmitAsm("BCC", Rel(msPtrOk));
            EmitAsm("INC", Mem(CallArgBase + 4));
            _assembly.Add(Expr.Make(Tag.Label, msPtrOk));
            EmitAsm("JMP", Abs(msLoop));
            _assembly.Add(Expr.Make(Tag.Label, msDone));
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp2Address));
            EmitAsm("LSR");
            EmitAsm("LSR");
            EmitAsm("RTS");
__mic_read2p

Read the digital signal for the second Famicom controller's microphone from $4016 bit 2. The helper reads the register once, masks 0x04 and normalizes the result to 0 or 1 without inversion. It does not latch controllers, record audio, measure volume or recognize speech.

Syntax

c
u8 __mic_read2p(void);

Return value

Exactly 1 when $4016 & 0x04 is nonzero, otherwise 0. This is the sampled digital level, not a button mask, amplitude, frequency or count of spoken words.

Conditions and limits

Although the microphone belongs to controller 2, this helper accesses $4016, not $4017. A register read can also advance the controller's serial stream while strobe is low, so do not insert microphone reads inside another controller's eight-bit read loop. No input-history cache is updated.

The published KUROSAKI controller model returns only D0 plus fixed bit 6. D1 and the microphone bit are therefore always 0 in that emulator, even while ordinary controller buttons are held. The supplied run verifies compilation, ordinary-button input and this zero-signal observation. Nonzero peripheral signals, electrical timing and real-device compatibility remain unverified; a zero here is not proof that an attached device works or that these functions cannot read it on hardware.

Usage example

c
mic_direct=__mic_read2p(); // A separate sample of the same digital signal.

This is a calling fragment. Follow the stated initialization and buffer requirements before using it in a complete program.

Expected result

This program contrasts ordinary D0 input with expansion D1 and the microphone's digital level. Hold A on controller 1 and B on controller 2 until the snapshot is captured. NORMAL P1 and NORMAL P2 display 001 and 002. In the supplied KUROSAKI run, D1 PORT 1, D1 PORT 2, EXP ALIAS 1, EXP ALIAS 2, MIC LEVEL and MIC DIRECT all display 000 because those signal lines are not emulated. The displayed values then stay fixed. With suitable peripherals and an implementation of their signal lines, the expansion and microphone values depend on the actual inputs; consult the bit layouts above rather than expecting these zeros.

The published KUROSAKI controller model returns only D0 plus fixed bit 6. D1 and the microphone bit are therefore always 0 in that emulator, even while ordinary controller buttons are held. The supplied run verifies compilation, ordinary-button input and this zero-signal observation. Nonzero peripheral signals, electrical timing and real-device compatibility remain unverified; a zero here is not proof that an attached device works or that these functions cannot read it on hardware.

__mic_read2p: NROM
NROM

Complete example program

Build command

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\expansion_input.c -I .\kitaq-docs\samples -I .\kitaqfc\lib -o .\out\expansion_input.nes --mapper=nrom --nes-chr=.\kitaq-docs\samples\font.chr --no-cache --no-disasm
Implementation and source

kitaqfc/lib/intrinsics.h:233

Declaration notes (original source)
Alias the same normalized microphone input bit as __joypad2p_voice.
__midi_clock

Send one timing-clock byte F8. The caller schedules repeated ticks.

Syntax

c
void __midi_clock(void);

Return value

No return value. Changes sound registers or driver state.

Conditions and limits

Requires an external circuit connecting $4016 D0 to TX and $4017 D4 to RX. A standard controller does not provide MIDI and these calls do not play the internal APU. Stop DMC playback/DMA and mask NMI and IRQ during transfers; avoid concurrent port/shared-scratch access. NTSC’s 57 cycles/bit give about 31,400 bps, roughly +0.5% from 31,250. PAL timing and round-trip communication with a physical adapter are unverified.

Usage example

c
// Controller-port byte encoding. This does not synthesize sound inside the FC.
#define SOUND_PORT_ONLY
#include "fc_sound_example.h"
void main(void) {
    sound_begin("MIDI PORT BYTES");
    // sound_begin has disabled DMC DMA and all internal APU voices. A compatible external circuit is required.
    // Stop NMI and mask IRQ so neither can split a software-timed serial frame.
    __ppu_ctrl_set(0); __irq_disable();
    __midi_out_byte(0x7E);
    __midi_program_change(2,5);
    __midi_control_change(2,7,100);
    __midi_note_on(2,69,96);
    __midi_note_off(2,69,0);
    __midi_clock(); __midi_start(); __midi_continue(); __midi_stop();
    // The emulator's unconnected input bit is low. Reading returns zero here.
    // A real input held high would block waiting for its start bit, without timeout.
    sound_result[0]=__midi_in_byte();
    sound_result[1]=1;
    __ppu_ctrl_set(0x80); __irq_enable();
    sound_end();
}

Expected result

The screen says EXTERNAL PORT DATA and no internal voice sounds. Transmitted bytes in hexadecimal are 7E; C2 05; B2 07 64; 92 45 60; 82 45 00; F8 FA FB FC. The check requires 57-cycle NTSC transmit cells and 57-cycle data-read spacing. Received 00 comes from the emulator’s unconnected input and does not prove communication with an external device.

__midi_clock: NTSC
NTSC

Complete example program

Build command

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\sound_midi.c -I .\kitaqfc\lib -I .\kitaq-docs\samples -o .\out\fc-midi.nes --mapper=nrom --nes-chr=.\kitaq-docs\samples\font.chr --no-cache --no-disasm
Implementation and source

kitaqfc/lib/intrinsics.h:304

Declaration notes (original source)
Transmit the single realtime byte F8 synchronously; this does not schedule future ticks.

kitaqfc/kitaqfc/CodeGenerator.cs:4803

csharp
case "__midi_clock": if (!Need(0)) return true; EmitAsm("LDA", Imm(0xF8)); EmitAsm("JSR", Abs("__kq_midi_out_a")); return true;

EmitHelperStart("__kq_midi_out_a");
            bool midiScratchInZeroPage = _runtimeIntrinsicTmp0Address <= 0xFF;
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("LDA", Imm(0));
            EmitAsm("STA", Mem(0x4016));
            // Unroll so neither branch cost nor payload bits change the baud.
            // Each data edge: 44 NOP cycles + LSR zp (5) + LDA/ADC (4) + STA (4).
            for (int bit = 0; bit < 8; bit++)
            {
                for (int delay = 0; delay < (midiScratchInZeroPage ? 22 : 20); delay++) EmitAsm("NOP");
                if (!midiScratchInZeroPage) EmitAsm("BIT", Mem(CallArgBase));
                EmitAsm("LSR", Mem(_runtimeIntrinsicTmp0Address));
                EmitAsm("LDA", Imm(0));
                EmitAsm("ADC", Imm(0));
                EmitAsm("STA", Mem(0x4016));
            }
            // 51 delay cycles plus LDA/STA (6) place the stop bit 57 cycles later.
            for (int delay = 0; delay < 24; delay++) EmitAsm("NOP");
            EmitAsm("BIT", Mem(CallArgBase));
            EmitAsm("LDA", Imm(1));
            EmitAsm("STA", Mem(0x4016));
            EmitMidiBitDelay(); // Hold stop high for at least one full bit cell.
            EmitAsm("RTS");

            EmitHelperStart("__midi_out_byte");
            EmitAsm("LDA", Mem(CallArgBase));
            EmitAsm("JSR", Abs("__kq_midi_out_a"));
            EmitAsm("RTS");

            // Poll in nine-cycle steps, then sample near data-bit centers.
            // IRQ/NMI masking belongs to the caller; no timeout/framing status is returned.
            EmitHelperStart("__midi_in_byte");
            EmitAsm("LDA", Imm(0));
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp0Address));
            string waitStart = NewGeneratedLabel("midi_wait_start");
            _assembly.Add(Expr.Make(Tag.Label, waitStart));
            EmitAsm("LDA", Mem(0x4017));
            EmitAsm("AND", Imm(0x10));
            EmitAsm("BNE", Rel(waitStart));
            // 81 cycles from the start-detect read to the first data read.
            // Polling uncertainty is at most nine cycles within a 57-cycle cell.
            for (int delay = 0; delay < 35; delay++) EmitAsm("NOP");
            EmitAsm("BIT", Mem(CallArgBase));
            for (int bit = 0; bit < 8; bit++)
            {
                EmitAsm("LDA", Mem(0x4017));
                EmitAsm("AND", Imm(0x10));
                EmitAsm("CMP", Imm(0x10));
                EmitAsm("ROR", Mem(_runtimeIntrinsicTmp0Address));
                for (int delay = 0; delay < (midiScratchInZeroPage ? 22 : 20); delay++) EmitAsm("NOP");
                if (!midiScratchInZeroPage) EmitAsm("BIT", Mem(CallArgBase));
            }
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("RTS");

            // Compose the channelized note-on status, then send note and velocity arguments.
__midi_continue

Send one realtime Continue byte FB without operating the internal music driver.

Syntax

c
void __midi_continue(void);

Return value

No return value. Changes sound registers or driver state.

Conditions and limits

Requires an external circuit connecting $4016 D0 to TX and $4017 D4 to RX. A standard controller does not provide MIDI and these calls do not play the internal APU. Stop DMC playback/DMA and mask NMI and IRQ during transfers; avoid concurrent port/shared-scratch access. NTSC’s 57 cycles/bit give about 31,400 bps, roughly +0.5% from 31,250. PAL timing and round-trip communication with a physical adapter are unverified.

Usage example

c
// Controller-port byte encoding. This does not synthesize sound inside the FC.
#define SOUND_PORT_ONLY
#include "fc_sound_example.h"
void main(void) {
    sound_begin("MIDI PORT BYTES");
    // sound_begin has disabled DMC DMA and all internal APU voices. A compatible external circuit is required.
    // Stop NMI and mask IRQ so neither can split a software-timed serial frame.
    __ppu_ctrl_set(0); __irq_disable();
    __midi_out_byte(0x7E);
    __midi_program_change(2,5);
    __midi_control_change(2,7,100);
    __midi_note_on(2,69,96);
    __midi_note_off(2,69,0);
    __midi_clock(); __midi_start(); __midi_continue(); __midi_stop();
    // The emulator's unconnected input bit is low. Reading returns zero here.
    // A real input held high would block waiting for its start bit, without timeout.
    sound_result[0]=__midi_in_byte();
    sound_result[1]=1;
    __ppu_ctrl_set(0x80); __irq_enable();
    sound_end();
}

Expected result

The screen says EXTERNAL PORT DATA and no internal voice sounds. Transmitted bytes in hexadecimal are 7E; C2 05; B2 07 64; 92 45 60; 82 45 00; F8 FA FB FC. The check requires 57-cycle NTSC transmit cells and 57-cycle data-read spacing. Received 00 comes from the emulator’s unconnected input and does not prove communication with an external device.

__midi_continue: NTSC
NTSC

Complete example program

Build command

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\sound_midi.c -I .\kitaqfc\lib -I .\kitaq-docs\samples -o .\out\fc-midi.nes --mapper=nrom --nes-chr=.\kitaq-docs\samples\font.chr --no-cache --no-disasm
Implementation and source

kitaqfc/lib/intrinsics.h:308

Declaration notes (original source)
Transmit realtime Continue (FB) through the serial adapter path.

kitaqfc/kitaqfc/CodeGenerator.cs:4805

csharp
case "__midi_continue": if (!Need(0)) return true; EmitAsm("LDA", Imm(0xFB)); EmitAsm("JSR", Abs("__kq_midi_out_a")); return true;

EmitHelperStart("__kq_midi_out_a");
            bool midiScratchInZeroPage = _runtimeIntrinsicTmp0Address <= 0xFF;
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("LDA", Imm(0));
            EmitAsm("STA", Mem(0x4016));
            // Unroll so neither branch cost nor payload bits change the baud.
            // Each data edge: 44 NOP cycles + LSR zp (5) + LDA/ADC (4) + STA (4).
            for (int bit = 0; bit < 8; bit++)
            {
                for (int delay = 0; delay < (midiScratchInZeroPage ? 22 : 20); delay++) EmitAsm("NOP");
                if (!midiScratchInZeroPage) EmitAsm("BIT", Mem(CallArgBase));
                EmitAsm("LSR", Mem(_runtimeIntrinsicTmp0Address));
                EmitAsm("LDA", Imm(0));
                EmitAsm("ADC", Imm(0));
                EmitAsm("STA", Mem(0x4016));
            }
            // 51 delay cycles plus LDA/STA (6) place the stop bit 57 cycles later.
            for (int delay = 0; delay < 24; delay++) EmitAsm("NOP");
            EmitAsm("BIT", Mem(CallArgBase));
            EmitAsm("LDA", Imm(1));
            EmitAsm("STA", Mem(0x4016));
            EmitMidiBitDelay(); // Hold stop high for at least one full bit cell.
            EmitAsm("RTS");

            EmitHelperStart("__midi_out_byte");
            EmitAsm("LDA", Mem(CallArgBase));
            EmitAsm("JSR", Abs("__kq_midi_out_a"));
            EmitAsm("RTS");

            // Poll in nine-cycle steps, then sample near data-bit centers.
            // IRQ/NMI masking belongs to the caller; no timeout/framing status is returned.
            EmitHelperStart("__midi_in_byte");
            EmitAsm("LDA", Imm(0));
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp0Address));
            string waitStart = NewGeneratedLabel("midi_wait_start");
            _assembly.Add(Expr.Make(Tag.Label, waitStart));
            EmitAsm("LDA", Mem(0x4017));
            EmitAsm("AND", Imm(0x10));
            EmitAsm("BNE", Rel(waitStart));
            // 81 cycles from the start-detect read to the first data read.
            // Polling uncertainty is at most nine cycles within a 57-cycle cell.
            for (int delay = 0; delay < 35; delay++) EmitAsm("NOP");
            EmitAsm("BIT", Mem(CallArgBase));
            for (int bit = 0; bit < 8; bit++)
            {
                EmitAsm("LDA", Mem(0x4017));
                EmitAsm("AND", Imm(0x10));
                EmitAsm("CMP", Imm(0x10));
                EmitAsm("ROR", Mem(_runtimeIntrinsicTmp0Address));
                for (int delay = 0; delay < (midiScratchInZeroPage ? 22 : 20); delay++) EmitAsm("NOP");
                if (!midiScratchInZeroPage) EmitAsm("BIT", Mem(CallArgBase));
            }
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("RTS");

            // Compose the channelized note-on status, then send note and velocity arguments.
__midi_control_change

Send control change as 0xB0 | (ch & 0x0F), controller number and value.

Syntax

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

Parameters

ch

MIDI channel 0..15; the low nibble enters the status byte. Zero corresponds to displayed MIDI channel 1.

cc

Controller number 0..127; 7 is channel volume.

value

Controller value 0..127; upper bits are not automatically cleared.

Return value

No return value. Changes sound registers or driver state.

Conditions and limits

Requires an external circuit connecting $4016 D0 to TX and $4017 D4 to RX. A standard controller does not provide MIDI and these calls do not play the internal APU. Stop DMC playback/DMA and mask NMI and IRQ during transfers; avoid concurrent port/shared-scratch access. NTSC’s 57 cycles/bit give about 31,400 bps, roughly +0.5% from 31,250. PAL timing and round-trip communication with a physical adapter are unverified.

Usage example

c
// Controller-port byte encoding. This does not synthesize sound inside the FC.
#define SOUND_PORT_ONLY
#include "fc_sound_example.h"
void main(void) {
    sound_begin("MIDI PORT BYTES");
    // sound_begin has disabled DMC DMA and all internal APU voices. A compatible external circuit is required.
    // Stop NMI and mask IRQ so neither can split a software-timed serial frame.
    __ppu_ctrl_set(0); __irq_disable();
    __midi_out_byte(0x7E);
    __midi_program_change(2,5);
    __midi_control_change(2,7,100);
    __midi_note_on(2,69,96);
    __midi_note_off(2,69,0);
    __midi_clock(); __midi_start(); __midi_continue(); __midi_stop();
    // The emulator's unconnected input bit is low. Reading returns zero here.
    // A real input held high would block waiting for its start bit, without timeout.
    sound_result[0]=__midi_in_byte();
    sound_result[1]=1;
    __ppu_ctrl_set(0x80); __irq_enable();
    sound_end();
}

Expected result

The screen says EXTERNAL PORT DATA and no internal voice sounds. Transmitted bytes in hexadecimal are 7E; C2 05; B2 07 64; 92 45 60; 82 45 00; F8 FA FB FC. The check requires 57-cycle NTSC transmit cells and 57-cycle data-read spacing. Received 00 comes from the emulator’s unconnected input and does not prove communication with an external device.

__midi_control_change: NTSC
NTSC

Complete example program

Build command

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\sound_midi.c -I .\kitaqfc\lib -I .\kitaq-docs\samples -o .\out\fc-midi.nes --mapper=nrom --nes-chr=.\kitaq-docs\samples\font.chr --no-cache --no-disasm
Implementation and source

kitaqfc/lib/intrinsics.h:300

Declaration notes (original source)
Send B0|(ch&0F), controller and value; both data arguments must be valid 7-bit values.

kitaqfc/kitaqfc/CodeGenerator.cs:4800

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

EmitHelperStart("__midi_control_change");
            EmitAsm("LDA", Mem(CallArgBase));
            EmitAsm("AND", Imm(0x0F));
            EmitAsm("ORA", Imm(0xB0));
            EmitAsm("JSR", Abs("__kq_midi_out_a"));
            EmitAsm("LDA", Mem(CallArgBase + 1));
            EmitAsm("JSR", Abs("__kq_midi_out_a"));
            EmitAsm("LDA", Mem(CallArgBase + 2));
            EmitAsm("JSR", Abs("__kq_midi_out_a"));
            EmitAsm("RTS");

EmitHelperStart("__kq_midi_out_a");
            bool midiScratchInZeroPage = _runtimeIntrinsicTmp0Address <= 0xFF;
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("LDA", Imm(0));
            EmitAsm("STA", Mem(0x4016));
            // Unroll so neither branch cost nor payload bits change the baud.
            // Each data edge: 44 NOP cycles + LSR zp (5) + LDA/ADC (4) + STA (4).
            for (int bit = 0; bit < 8; bit++)
            {
                for (int delay = 0; delay < (midiScratchInZeroPage ? 22 : 20); delay++) EmitAsm("NOP");
                if (!midiScratchInZeroPage) EmitAsm("BIT", Mem(CallArgBase));
                EmitAsm("LSR", Mem(_runtimeIntrinsicTmp0Address));
                EmitAsm("LDA", Imm(0));
                EmitAsm("ADC", Imm(0));
                EmitAsm("STA", Mem(0x4016));
            }
            // 51 delay cycles plus LDA/STA (6) place the stop bit 57 cycles later.
            for (int delay = 0; delay < 24; delay++) EmitAsm("NOP");
            EmitAsm("BIT", Mem(CallArgBase));
            EmitAsm("LDA", Imm(1));
            EmitAsm("STA", Mem(0x4016));
            EmitMidiBitDelay(); // Hold stop high for at least one full bit cell.
            EmitAsm("RTS");

            EmitHelperStart("__midi_out_byte");
            EmitAsm("LDA", Mem(CallArgBase));
            EmitAsm("JSR", Abs("__kq_midi_out_a"));
            EmitAsm("RTS");

            // Poll in nine-cycle steps, then sample near data-bit centers.
            // IRQ/NMI masking belongs to the caller; no timeout/framing status is returned.
            EmitHelperStart("__midi_in_byte");
            EmitAsm("LDA", Imm(0));
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp0Address));
            string waitStart = NewGeneratedLabel("midi_wait_start");
            _assembly.Add(Expr.Make(Tag.Label, waitStart));
            EmitAsm("LDA", Mem(0x4017));
            EmitAsm("AND", Imm(0x10));
            EmitAsm("BNE", Rel(waitStart));
            // 81 cycles from the start-detect read to the first data read.
            // Polling uncertainty is at most nine cycles within a 57-cycle cell.
            for (int delay = 0; delay < 35; delay++) EmitAsm("NOP");
            EmitAsm("BIT", Mem(CallArgBase));
            for (int bit = 0; bit < 8; bit++)
            {
                EmitAsm("LDA", Mem(0x4017));
                EmitAsm("AND", Imm(0x10));
                EmitAsm("CMP", Imm(0x10));
                EmitAsm("ROR", Mem(_runtimeIntrinsicTmp0Address));
                for (int delay = 0; delay < (midiScratchInZeroPage ? 22 : 20); delay++) EmitAsm("NOP");
                if (!midiScratchInZeroPage) EmitAsm("BIT", Mem(CallArgBase));
            }
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("RTS");

            // Compose the channelized note-on status, then send note and velocity arguments.
__midi_in_byte

Wait for a low receive input, then sample eight bits at timed intervals.

Syntax

c
u8 __midi_in_byte(void);

Return value

Returns the received u8. There is no timeout, framing validation or receive buffer. An input held high blocks indefinitely; one held low reads as zero.

Conditions and limits

Requires an external circuit connecting $4016 D0 to TX and $4017 D4 to RX. A standard controller does not provide MIDI and these calls do not play the internal APU. Stop DMC playback/DMA and mask NMI and IRQ during transfers; avoid concurrent port/shared-scratch access. NTSC’s 57 cycles/bit give about 31,400 bps, roughly +0.5% from 31,250. PAL timing and round-trip communication with a physical adapter are unverified.

Usage example

c
// Controller-port byte encoding. This does not synthesize sound inside the FC.
#define SOUND_PORT_ONLY
#include "fc_sound_example.h"
void main(void) {
    sound_begin("MIDI PORT BYTES");
    // sound_begin has disabled DMC DMA and all internal APU voices. A compatible external circuit is required.
    // Stop NMI and mask IRQ so neither can split a software-timed serial frame.
    __ppu_ctrl_set(0); __irq_disable();
    __midi_out_byte(0x7E);
    __midi_program_change(2,5);
    __midi_control_change(2,7,100);
    __midi_note_on(2,69,96);
    __midi_note_off(2,69,0);
    __midi_clock(); __midi_start(); __midi_continue(); __midi_stop();
    // The emulator's unconnected input bit is low. Reading returns zero here.
    // A real input held high would block waiting for its start bit, without timeout.
    sound_result[0]=__midi_in_byte();
    sound_result[1]=1;
    __ppu_ctrl_set(0x80); __irq_enable();
    sound_end();
}

Expected result

The screen says EXTERNAL PORT DATA and no internal voice sounds. Transmitted bytes in hexadecimal are 7E; C2 05; B2 07 64; 92 45 60; 82 45 00; F8 FA FB FC. The check requires 57-cycle NTSC transmit cells and 57-cycle data-read spacing. Received 00 comes from the emulator’s unconnected input and does not prove communication with an external device.

__midi_in_byte: NTSC
NTSC

Complete example program

Build command

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\sound_midi.c -I .\kitaqfc\lib -I .\kitaq-docs\samples -o .\out\fc-midi.nes --mapper=nrom --nes-chr=.\kitaq-docs\samples\font.chr --no-cache --no-disasm
Implementation and source

kitaqfc/lib/intrinsics.h:294

Declaration notes (original source)
Block for a low start signal and sample eight bits. No timeout, framing check or receive buffer is provided.

kitaqfc/kitaqfc/CodeGenerator.cs:4796

csharp
case "__midi_in_byte": if (!Need(0)) return true; EmitAsm("JSR", Abs("__midi_in_byte")); return true;
                // Pack MIDI message fields as bytes; real-time commands below load their fixed status byte directly.

EmitHelperStart("__midi_in_byte");
            EmitAsm("LDA", Imm(0));
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp0Address));
            string waitStart = NewGeneratedLabel("midi_wait_start");
            _assembly.Add(Expr.Make(Tag.Label, waitStart));
            EmitAsm("LDA", Mem(0x4017));
            EmitAsm("AND", Imm(0x10));
            EmitAsm("BNE", Rel(waitStart));
            // 81 cycles from the start-detect read to the first data read.
            // Polling uncertainty is at most nine cycles within a 57-cycle cell.
            for (int delay = 0; delay < 35; delay++) EmitAsm("NOP");
            EmitAsm("BIT", Mem(CallArgBase));
            for (int bit = 0; bit < 8; bit++)
            {
                EmitAsm("LDA", Mem(0x4017));
                EmitAsm("AND", Imm(0x10));
                EmitAsm("CMP", Imm(0x10));
                EmitAsm("ROR", Mem(_runtimeIntrinsicTmp0Address));
                for (int delay = 0; delay < (midiScratchInZeroPage ? 22 : 20); delay++) EmitAsm("NOP");
                if (!midiScratchInZeroPage) EmitAsm("BIT", Mem(CallArgBase));
            }
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("RTS");

EmitHelperStart("__kq_midi_out_a");
            bool midiScratchInZeroPage = _runtimeIntrinsicTmp0Address <= 0xFF;
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("LDA", Imm(0));
            EmitAsm("STA", Mem(0x4016));
            // Unroll so neither branch cost nor payload bits change the baud.
            // Each data edge: 44 NOP cycles + LSR zp (5) + LDA/ADC (4) + STA (4).
            for (int bit = 0; bit < 8; bit++)
            {
                for (int delay = 0; delay < (midiScratchInZeroPage ? 22 : 20); delay++) EmitAsm("NOP");
                if (!midiScratchInZeroPage) EmitAsm("BIT", Mem(CallArgBase));
                EmitAsm("LSR", Mem(_runtimeIntrinsicTmp0Address));
                EmitAsm("LDA", Imm(0));
                EmitAsm("ADC", Imm(0));
                EmitAsm("STA", Mem(0x4016));
            }
            // 51 delay cycles plus LDA/STA (6) place the stop bit 57 cycles later.
            for (int delay = 0; delay < 24; delay++) EmitAsm("NOP");
            EmitAsm("BIT", Mem(CallArgBase));
            EmitAsm("LDA", Imm(1));
            EmitAsm("STA", Mem(0x4016));
            EmitMidiBitDelay(); // Hold stop high for at least one full bit cell.
            EmitAsm("RTS");

            EmitHelperStart("__midi_out_byte");
            EmitAsm("LDA", Mem(CallArgBase));
            EmitAsm("JSR", Abs("__kq_midi_out_a"));
            EmitAsm("RTS");

            // Poll in nine-cycle steps, then sample near data-bit centers.
            // IRQ/NMI masking belongs to the caller; no timeout/framing status is returned.
            EmitHelperStart("__midi_in_byte");
            EmitAsm("LDA", Imm(0));
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp0Address));
            string waitStart = NewGeneratedLabel("midi_wait_start");
            _assembly.Add(Expr.Make(Tag.Label, waitStart));
            EmitAsm("LDA", Mem(0x4017));
            EmitAsm("AND", Imm(0x10));
            EmitAsm("BNE", Rel(waitStart));
            // 81 cycles from the start-detect read to the first data read.
            // Polling uncertainty is at most nine cycles within a 57-cycle cell.
            for (int delay = 0; delay < 35; delay++) EmitAsm("NOP");
            EmitAsm("BIT", Mem(CallArgBase));
            for (int bit = 0; bit < 8; bit++)
            {
                EmitAsm("LDA", Mem(0x4017));
                EmitAsm("AND", Imm(0x10));
                EmitAsm("CMP", Imm(0x10));
                EmitAsm("ROR", Mem(_runtimeIntrinsicTmp0Address));
                for (int delay = 0; delay < (midiScratchInZeroPage ? 22 : 20); delay++) EmitAsm("NOP");
                if (!midiScratchInZeroPage) EmitAsm("BIT", Mem(CallArgBase));
            }
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("RTS");

            // Compose the channelized note-on status, then send note and velocity arguments.
__midi_note_off

Send note-off as three bytes: 0x80 | (ch & 0x0F), note, velocity.

Syntax

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

Parameters

ch

MIDI channel 0..15; the low nibble enters the status byte. Zero corresponds to displayed MIDI channel 1.

note

MIDI note 0..127; 69 is A4. The helper does not mask to seven bits, so the caller enforces the range.

velocity

Velocity 0..127; not automatically masked to seven bits.

Return value

No return value. Changes sound registers or driver state.

Conditions and limits

Requires an external circuit connecting $4016 D0 to TX and $4017 D4 to RX. A standard controller does not provide MIDI and these calls do not play the internal APU. Stop DMC playback/DMA and mask NMI and IRQ during transfers; avoid concurrent port/shared-scratch access. NTSC’s 57 cycles/bit give about 31,400 bps, roughly +0.5% from 31,250. PAL timing and round-trip communication with a physical adapter are unverified.

Usage example

c
// Controller-port byte encoding. This does not synthesize sound inside the FC.
#define SOUND_PORT_ONLY
#include "fc_sound_example.h"
void main(void) {
    sound_begin("MIDI PORT BYTES");
    // sound_begin has disabled DMC DMA and all internal APU voices. A compatible external circuit is required.
    // Stop NMI and mask IRQ so neither can split a software-timed serial frame.
    __ppu_ctrl_set(0); __irq_disable();
    __midi_out_byte(0x7E);
    __midi_program_change(2,5);
    __midi_control_change(2,7,100);
    __midi_note_on(2,69,96);
    __midi_note_off(2,69,0);
    __midi_clock(); __midi_start(); __midi_continue(); __midi_stop();
    // The emulator's unconnected input bit is low. Reading returns zero here.
    // A real input held high would block waiting for its start bit, without timeout.
    sound_result[0]=__midi_in_byte();
    sound_result[1]=1;
    __ppu_ctrl_set(0x80); __irq_enable();
    sound_end();
}

Expected result

The screen says EXTERNAL PORT DATA and no internal voice sounds. Transmitted bytes in hexadecimal are 7E; C2 05; B2 07 64; 92 45 60; 82 45 00; F8 FA FB FC. The check requires 57-cycle NTSC transmit cells and 57-cycle data-read spacing. Received 00 comes from the emulator’s unconnected input and does not prove communication with an external device.

__midi_note_off: NTSC
NTSC

Complete example program

Build command

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\sound_midi.c -I .\kitaqfc\lib -I .\kitaq-docs\samples -o .\out\fc-midi.nes --mapper=nrom --nes-chr=.\kitaq-docs\samples\font.chr --no-cache --no-disasm
Implementation and source

kitaqfc/lib/intrinsics.h:298

Declaration notes (original source)
Send 80|(ch&0F), note and velocity through the same synchronous serial path.

kitaqfc/kitaqfc/CodeGenerator.cs:4799

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

EmitHelperStart("__midi_note_off");
            EmitAsm("LDA", Mem(CallArgBase));
            EmitAsm("AND", Imm(0x0F));
            EmitAsm("ORA", Imm(0x80));
            EmitAsm("JSR", Abs("__kq_midi_out_a"));
            EmitAsm("LDA", Mem(CallArgBase + 1));
            EmitAsm("JSR", Abs("__kq_midi_out_a"));
            EmitAsm("LDA", Mem(CallArgBase + 2));
            EmitAsm("JSR", Abs("__kq_midi_out_a"));
            EmitAsm("RTS");

EmitHelperStart("__kq_midi_out_a");
            bool midiScratchInZeroPage = _runtimeIntrinsicTmp0Address <= 0xFF;
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("LDA", Imm(0));
            EmitAsm("STA", Mem(0x4016));
            // Unroll so neither branch cost nor payload bits change the baud.
            // Each data edge: 44 NOP cycles + LSR zp (5) + LDA/ADC (4) + STA (4).
            for (int bit = 0; bit < 8; bit++)
            {
                for (int delay = 0; delay < (midiScratchInZeroPage ? 22 : 20); delay++) EmitAsm("NOP");
                if (!midiScratchInZeroPage) EmitAsm("BIT", Mem(CallArgBase));
                EmitAsm("LSR", Mem(_runtimeIntrinsicTmp0Address));
                EmitAsm("LDA", Imm(0));
                EmitAsm("ADC", Imm(0));
                EmitAsm("STA", Mem(0x4016));
            }
            // 51 delay cycles plus LDA/STA (6) place the stop bit 57 cycles later.
            for (int delay = 0; delay < 24; delay++) EmitAsm("NOP");
            EmitAsm("BIT", Mem(CallArgBase));
            EmitAsm("LDA", Imm(1));
            EmitAsm("STA", Mem(0x4016));
            EmitMidiBitDelay(); // Hold stop high for at least one full bit cell.
            EmitAsm("RTS");

            EmitHelperStart("__midi_out_byte");
            EmitAsm("LDA", Mem(CallArgBase));
            EmitAsm("JSR", Abs("__kq_midi_out_a"));
            EmitAsm("RTS");

            // Poll in nine-cycle steps, then sample near data-bit centers.
            // IRQ/NMI masking belongs to the caller; no timeout/framing status is returned.
            EmitHelperStart("__midi_in_byte");
            EmitAsm("LDA", Imm(0));
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp0Address));
            string waitStart = NewGeneratedLabel("midi_wait_start");
            _assembly.Add(Expr.Make(Tag.Label, waitStart));
            EmitAsm("LDA", Mem(0x4017));
            EmitAsm("AND", Imm(0x10));
            EmitAsm("BNE", Rel(waitStart));
            // 81 cycles from the start-detect read to the first data read.
            // Polling uncertainty is at most nine cycles within a 57-cycle cell.
            for (int delay = 0; delay < 35; delay++) EmitAsm("NOP");
            EmitAsm("BIT", Mem(CallArgBase));
            for (int bit = 0; bit < 8; bit++)
            {
                EmitAsm("LDA", Mem(0x4017));
                EmitAsm("AND", Imm(0x10));
                EmitAsm("CMP", Imm(0x10));
                EmitAsm("ROR", Mem(_runtimeIntrinsicTmp0Address));
                for (int delay = 0; delay < (midiScratchInZeroPage ? 22 : 20); delay++) EmitAsm("NOP");
                if (!midiScratchInZeroPage) EmitAsm("BIT", Mem(CallArgBase));
            }
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("RTS");

            // Compose the channelized note-on status, then send note and velocity arguments.
__midi_note_on

Send note-on as three bytes: 0x90 | (ch & 0x0F), note, velocity.

Syntax

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

Parameters

ch

MIDI channel 0..15; the low nibble enters the status byte. Zero corresponds to displayed MIDI channel 1.

note

MIDI note 0..127; 69 is A4. The helper does not mask to seven bits, so the caller enforces the range.

velocity

Velocity 0..127; not automatically masked to seven bits.

Return value

No return value. Changes sound registers or driver state.

Conditions and limits

Requires an external circuit connecting $4016 D0 to TX and $4017 D4 to RX. A standard controller does not provide MIDI and these calls do not play the internal APU. Stop DMC playback/DMA and mask NMI and IRQ during transfers; avoid concurrent port/shared-scratch access. NTSC’s 57 cycles/bit give about 31,400 bps, roughly +0.5% from 31,250. PAL timing and round-trip communication with a physical adapter are unverified.

Usage example

c
// Controller-port byte encoding. This does not synthesize sound inside the FC.
#define SOUND_PORT_ONLY
#include "fc_sound_example.h"
void main(void) {
    sound_begin("MIDI PORT BYTES");
    // sound_begin has disabled DMC DMA and all internal APU voices. A compatible external circuit is required.
    // Stop NMI and mask IRQ so neither can split a software-timed serial frame.
    __ppu_ctrl_set(0); __irq_disable();
    __midi_out_byte(0x7E);
    __midi_program_change(2,5);
    __midi_control_change(2,7,100);
    __midi_note_on(2,69,96);
    __midi_note_off(2,69,0);
    __midi_clock(); __midi_start(); __midi_continue(); __midi_stop();
    // The emulator's unconnected input bit is low. Reading returns zero here.
    // A real input held high would block waiting for its start bit, without timeout.
    sound_result[0]=__midi_in_byte();
    sound_result[1]=1;
    __ppu_ctrl_set(0x80); __irq_enable();
    sound_end();
}

Expected result

The screen says EXTERNAL PORT DATA and no internal voice sounds. Transmitted bytes in hexadecimal are 7E; C2 05; B2 07 64; 92 45 60; 82 45 00; F8 FA FB FC. The check requires 57-cycle NTSC transmit cells and 57-cycle data-read spacing. Received 00 comes from the emulator’s unconnected input and does not prove communication with an external device.

__midi_note_on: NTSC
NTSC

Complete example program

Build command

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\sound_midi.c -I .\kitaqfc\lib -I .\kitaq-docs\samples -o .\out\fc-midi.nes --mapper=nrom --nes-chr=.\kitaq-docs\samples\font.chr --no-cache --no-disasm
Implementation and source

kitaqfc/lib/intrinsics.h:296

Declaration notes (original source)
Send 90|(ch&0F), note and velocity. Supply 7-bit data bytes; they are not masked by this helper.

kitaqfc/kitaqfc/CodeGenerator.cs:4798

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

EmitHelperStart("__midi_note_on");
            EmitAsm("LDA", Mem(CallArgBase));
            EmitAsm("AND", Imm(0x0F));
            EmitAsm("ORA", Imm(0x90));
            EmitAsm("JSR", Abs("__kq_midi_out_a"));
            EmitAsm("LDA", Mem(CallArgBase + 1));
            EmitAsm("JSR", Abs("__kq_midi_out_a"));
            EmitAsm("LDA", Mem(CallArgBase + 2));
            EmitAsm("JSR", Abs("__kq_midi_out_a"));
            EmitAsm("RTS");

EmitHelperStart("__kq_midi_out_a");
            bool midiScratchInZeroPage = _runtimeIntrinsicTmp0Address <= 0xFF;
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("LDA", Imm(0));
            EmitAsm("STA", Mem(0x4016));
            // Unroll so neither branch cost nor payload bits change the baud.
            // Each data edge: 44 NOP cycles + LSR zp (5) + LDA/ADC (4) + STA (4).
            for (int bit = 0; bit < 8; bit++)
            {
                for (int delay = 0; delay < (midiScratchInZeroPage ? 22 : 20); delay++) EmitAsm("NOP");
                if (!midiScratchInZeroPage) EmitAsm("BIT", Mem(CallArgBase));
                EmitAsm("LSR", Mem(_runtimeIntrinsicTmp0Address));
                EmitAsm("LDA", Imm(0));
                EmitAsm("ADC", Imm(0));
                EmitAsm("STA", Mem(0x4016));
            }
            // 51 delay cycles plus LDA/STA (6) place the stop bit 57 cycles later.
            for (int delay = 0; delay < 24; delay++) EmitAsm("NOP");
            EmitAsm("BIT", Mem(CallArgBase));
            EmitAsm("LDA", Imm(1));
            EmitAsm("STA", Mem(0x4016));
            EmitMidiBitDelay(); // Hold stop high for at least one full bit cell.
            EmitAsm("RTS");

            EmitHelperStart("__midi_out_byte");
            EmitAsm("LDA", Mem(CallArgBase));
            EmitAsm("JSR", Abs("__kq_midi_out_a"));
            EmitAsm("RTS");

            // Poll in nine-cycle steps, then sample near data-bit centers.
            // IRQ/NMI masking belongs to the caller; no timeout/framing status is returned.
            EmitHelperStart("__midi_in_byte");
            EmitAsm("LDA", Imm(0));
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp0Address));
            string waitStart = NewGeneratedLabel("midi_wait_start");
            _assembly.Add(Expr.Make(Tag.Label, waitStart));
            EmitAsm("LDA", Mem(0x4017));
            EmitAsm("AND", Imm(0x10));
            EmitAsm("BNE", Rel(waitStart));
            // 81 cycles from the start-detect read to the first data read.
            // Polling uncertainty is at most nine cycles within a 57-cycle cell.
            for (int delay = 0; delay < 35; delay++) EmitAsm("NOP");
            EmitAsm("BIT", Mem(CallArgBase));
            for (int bit = 0; bit < 8; bit++)
            {
                EmitAsm("LDA", Mem(0x4017));
                EmitAsm("AND", Imm(0x10));
                EmitAsm("CMP", Imm(0x10));
                EmitAsm("ROR", Mem(_runtimeIntrinsicTmp0Address));
                for (int delay = 0; delay < (midiScratchInZeroPage ? 22 : 20); delay++) EmitAsm("NOP");
                if (!midiScratchInZeroPage) EmitAsm("BIT", Mem(CallArgBase));
            }
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("RTS");

            // Compose the channelized note-on status, then send note and velocity arguments.
__midi_out_byte

Synchronously transmit one 8N1 byte: low start, eight data bits least-significant first, high stop.

Syntax

c
void __midi_out_byte(u8 byte);

Parameters

byte

Raw byte 0..255, including status or data bytes.

Return value

No return value. Changes sound registers or driver state.

Conditions and limits

Requires an external circuit connecting $4016 D0 to TX and $4017 D4 to RX. A standard controller does not provide MIDI and these calls do not play the internal APU. Stop DMC playback/DMA and mask NMI and IRQ during transfers; avoid concurrent port/shared-scratch access. NTSC’s 57 cycles/bit give about 31,400 bps, roughly +0.5% from 31,250. PAL timing and round-trip communication with a physical adapter are unverified.

Usage example

c
// Controller-port byte encoding. This does not synthesize sound inside the FC.
#define SOUND_PORT_ONLY
#include "fc_sound_example.h"
void main(void) {
    sound_begin("MIDI PORT BYTES");
    // sound_begin has disabled DMC DMA and all internal APU voices. A compatible external circuit is required.
    // Stop NMI and mask IRQ so neither can split a software-timed serial frame.
    __ppu_ctrl_set(0); __irq_disable();
    __midi_out_byte(0x7E);
    __midi_program_change(2,5);
    __midi_control_change(2,7,100);
    __midi_note_on(2,69,96);
    __midi_note_off(2,69,0);
    __midi_clock(); __midi_start(); __midi_continue(); __midi_stop();
    // The emulator's unconnected input bit is low. Reading returns zero here.
    // A real input held high would block waiting for its start bit, without timeout.
    sound_result[0]=__midi_in_byte();
    sound_result[1]=1;
    __ppu_ctrl_set(0x80); __irq_enable();
    sound_end();
}

Expected result

The screen says EXTERNAL PORT DATA and no internal voice sounds. Transmitted bytes in hexadecimal are 7E; C2 05; B2 07 64; 92 45 60; 82 45 00; F8 FA FB FC. The check requires 57-cycle NTSC transmit cells and 57-cycle data-read spacing. Received 00 comes from the emulator’s unconnected input and does not prove communication with an external device.

__midi_out_byte: NTSC
NTSC

Complete example program

Build command

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\sound_midi.c -I .\kitaqfc\lib -I .\kitaq-docs\samples -o .\out\fc-midi.nes --mapper=nrom --nes-chr=.\kitaq-docs\samples\font.chr --no-cache --no-disasm
Implementation and source

kitaqfc/lib/intrinsics.h:292

Declaration notes (original source)
Send start-low, eight least-significant-first bits and stop-high in 57-cycle
NTSC bit cells (about 31400 bps). Stop DMC DMA and mask IRQ/NMI during transfers;
PAL timing and electrical adapter compatibility are not established here.

kitaqfc/kitaqfc/CodeGenerator.cs:4795

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

EmitHelperStart("__midi_out_byte");
            EmitAsm("LDA", Mem(CallArgBase));
            EmitAsm("JSR", Abs("__kq_midi_out_a"));
            EmitAsm("RTS");

EmitHelperStart("__kq_midi_out_a");
            bool midiScratchInZeroPage = _runtimeIntrinsicTmp0Address <= 0xFF;
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("LDA", Imm(0));
            EmitAsm("STA", Mem(0x4016));
            // Unroll so neither branch cost nor payload bits change the baud.
            // Each data edge: 44 NOP cycles + LSR zp (5) + LDA/ADC (4) + STA (4).
            for (int bit = 0; bit < 8; bit++)
            {
                for (int delay = 0; delay < (midiScratchInZeroPage ? 22 : 20); delay++) EmitAsm("NOP");
                if (!midiScratchInZeroPage) EmitAsm("BIT", Mem(CallArgBase));
                EmitAsm("LSR", Mem(_runtimeIntrinsicTmp0Address));
                EmitAsm("LDA", Imm(0));
                EmitAsm("ADC", Imm(0));
                EmitAsm("STA", Mem(0x4016));
            }
            // 51 delay cycles plus LDA/STA (6) place the stop bit 57 cycles later.
            for (int delay = 0; delay < 24; delay++) EmitAsm("NOP");
            EmitAsm("BIT", Mem(CallArgBase));
            EmitAsm("LDA", Imm(1));
            EmitAsm("STA", Mem(0x4016));
            EmitMidiBitDelay(); // Hold stop high for at least one full bit cell.
            EmitAsm("RTS");

            EmitHelperStart("__midi_out_byte");
            EmitAsm("LDA", Mem(CallArgBase));
            EmitAsm("JSR", Abs("__kq_midi_out_a"));
            EmitAsm("RTS");

            // Poll in nine-cycle steps, then sample near data-bit centers.
            // IRQ/NMI masking belongs to the caller; no timeout/framing status is returned.
            EmitHelperStart("__midi_in_byte");
            EmitAsm("LDA", Imm(0));
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp0Address));
            string waitStart = NewGeneratedLabel("midi_wait_start");
            _assembly.Add(Expr.Make(Tag.Label, waitStart));
            EmitAsm("LDA", Mem(0x4017));
            EmitAsm("AND", Imm(0x10));
            EmitAsm("BNE", Rel(waitStart));
            // 81 cycles from the start-detect read to the first data read.
            // Polling uncertainty is at most nine cycles within a 57-cycle cell.
            for (int delay = 0; delay < 35; delay++) EmitAsm("NOP");
            EmitAsm("BIT", Mem(CallArgBase));
            for (int bit = 0; bit < 8; bit++)
            {
                EmitAsm("LDA", Mem(0x4017));
                EmitAsm("AND", Imm(0x10));
                EmitAsm("CMP", Imm(0x10));
                EmitAsm("ROR", Mem(_runtimeIntrinsicTmp0Address));
                for (int delay = 0; delay < (midiScratchInZeroPage ? 22 : 20); delay++) EmitAsm("NOP");
                if (!midiScratchInZeroPage) EmitAsm("BIT", Mem(CallArgBase));
            }
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("RTS");

            // Compose the channelized note-on status, then send note and velocity arguments.
__midi_program_change

Send program change as 0xC0 | (ch & 0x0F) followed by the program number.

Syntax

c
void __midi_program_change(u8 ch, u8 program);

Parameters

ch

MIDI channel 0..15; the low nibble enters the status byte. Zero corresponds to displayed MIDI channel 1.

program

Program 0..127; instrument mapping depends on the receiver.

Return value

No return value. Changes sound registers or driver state.

Conditions and limits

Requires an external circuit connecting $4016 D0 to TX and $4017 D4 to RX. A standard controller does not provide MIDI and these calls do not play the internal APU. Stop DMC playback/DMA and mask NMI and IRQ during transfers; avoid concurrent port/shared-scratch access. NTSC’s 57 cycles/bit give about 31,400 bps, roughly +0.5% from 31,250. PAL timing and round-trip communication with a physical adapter are unverified.

Usage example

c
// Controller-port byte encoding. This does not synthesize sound inside the FC.
#define SOUND_PORT_ONLY
#include "fc_sound_example.h"
void main(void) {
    sound_begin("MIDI PORT BYTES");
    // sound_begin has disabled DMC DMA and all internal APU voices. A compatible external circuit is required.
    // Stop NMI and mask IRQ so neither can split a software-timed serial frame.
    __ppu_ctrl_set(0); __irq_disable();
    __midi_out_byte(0x7E);
    __midi_program_change(2,5);
    __midi_control_change(2,7,100);
    __midi_note_on(2,69,96);
    __midi_note_off(2,69,0);
    __midi_clock(); __midi_start(); __midi_continue(); __midi_stop();
    // The emulator's unconnected input bit is low. Reading returns zero here.
    // A real input held high would block waiting for its start bit, without timeout.
    sound_result[0]=__midi_in_byte();
    sound_result[1]=1;
    __ppu_ctrl_set(0x80); __irq_enable();
    sound_end();
}

Expected result

The screen says EXTERNAL PORT DATA and no internal voice sounds. Transmitted bytes in hexadecimal are 7E; C2 05; B2 07 64; 92 45 60; 82 45 00; F8 FA FB FC. The check requires 57-cycle NTSC transmit cells and 57-cycle data-read spacing. Received 00 comes from the emulator’s unconnected input and does not prove communication with an external device.

__midi_program_change: NTSC
NTSC

Complete example program

Build command

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\sound_midi.c -I .\kitaqfc\lib -I .\kitaq-docs\samples -o .\out\fc-midi.nes --mapper=nrom --nes-chr=.\kitaq-docs\samples\font.chr --no-cache --no-disasm
Implementation and source

kitaqfc/lib/intrinsics.h:302

Declaration notes (original source)
Send C0|(ch&0F) followed by the caller-supplied 7-bit program value.

kitaqfc/kitaqfc/CodeGenerator.cs:4801

csharp
case "__midi_program_change": return EmitKitaqfcHelperCall(funcName, args, ctx, new[] {1,1}, funcName);
                // Real-time MIDI commands send a single status byte through the common output routine.

EmitHelperStart("__midi_program_change");
            EmitAsm("LDA", Mem(CallArgBase));
            EmitAsm("AND", Imm(0x0F));
            EmitAsm("ORA", Imm(0xC0));
            EmitAsm("JSR", Abs("__kq_midi_out_a"));
            EmitAsm("LDA", Mem(CallArgBase + 1));
            EmitAsm("JSR", Abs("__kq_midi_out_a"));
            EmitAsm("RTS");

EmitHelperStart("__kq_midi_out_a");
            bool midiScratchInZeroPage = _runtimeIntrinsicTmp0Address <= 0xFF;
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("LDA", Imm(0));
            EmitAsm("STA", Mem(0x4016));
            // Unroll so neither branch cost nor payload bits change the baud.
            // Each data edge: 44 NOP cycles + LSR zp (5) + LDA/ADC (4) + STA (4).
            for (int bit = 0; bit < 8; bit++)
            {
                for (int delay = 0; delay < (midiScratchInZeroPage ? 22 : 20); delay++) EmitAsm("NOP");
                if (!midiScratchInZeroPage) EmitAsm("BIT", Mem(CallArgBase));
                EmitAsm("LSR", Mem(_runtimeIntrinsicTmp0Address));
                EmitAsm("LDA", Imm(0));
                EmitAsm("ADC", Imm(0));
                EmitAsm("STA", Mem(0x4016));
            }
            // 51 delay cycles plus LDA/STA (6) place the stop bit 57 cycles later.
            for (int delay = 0; delay < 24; delay++) EmitAsm("NOP");
            EmitAsm("BIT", Mem(CallArgBase));
            EmitAsm("LDA", Imm(1));
            EmitAsm("STA", Mem(0x4016));
            EmitMidiBitDelay(); // Hold stop high for at least one full bit cell.
            EmitAsm("RTS");

            EmitHelperStart("__midi_out_byte");
            EmitAsm("LDA", Mem(CallArgBase));
            EmitAsm("JSR", Abs("__kq_midi_out_a"));
            EmitAsm("RTS");

            // Poll in nine-cycle steps, then sample near data-bit centers.
            // IRQ/NMI masking belongs to the caller; no timeout/framing status is returned.
            EmitHelperStart("__midi_in_byte");
            EmitAsm("LDA", Imm(0));
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp0Address));
            string waitStart = NewGeneratedLabel("midi_wait_start");
            _assembly.Add(Expr.Make(Tag.Label, waitStart));
            EmitAsm("LDA", Mem(0x4017));
            EmitAsm("AND", Imm(0x10));
            EmitAsm("BNE", Rel(waitStart));
            // 81 cycles from the start-detect read to the first data read.
            // Polling uncertainty is at most nine cycles within a 57-cycle cell.
            for (int delay = 0; delay < 35; delay++) EmitAsm("NOP");
            EmitAsm("BIT", Mem(CallArgBase));
            for (int bit = 0; bit < 8; bit++)
            {
                EmitAsm("LDA", Mem(0x4017));
                EmitAsm("AND", Imm(0x10));
                EmitAsm("CMP", Imm(0x10));
                EmitAsm("ROR", Mem(_runtimeIntrinsicTmp0Address));
                for (int delay = 0; delay < (midiScratchInZeroPage ? 22 : 20); delay++) EmitAsm("NOP");
                if (!midiScratchInZeroPage) EmitAsm("BIT", Mem(CallArgBase));
            }
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("RTS");

            // Compose the channelized note-on status, then send note and velocity arguments.
__midi_start

Send one realtime Start byte FA without changing the game’s song pointers.

Syntax

c
void __midi_start(void);

Return value

No return value. Changes sound registers or driver state.

Conditions and limits

Requires an external circuit connecting $4016 D0 to TX and $4017 D4 to RX. A standard controller does not provide MIDI and these calls do not play the internal APU. Stop DMC playback/DMA and mask NMI and IRQ during transfers; avoid concurrent port/shared-scratch access. NTSC’s 57 cycles/bit give about 31,400 bps, roughly +0.5% from 31,250. PAL timing and round-trip communication with a physical adapter are unverified.

Usage example

c
// Controller-port byte encoding. This does not synthesize sound inside the FC.
#define SOUND_PORT_ONLY
#include "fc_sound_example.h"
void main(void) {
    sound_begin("MIDI PORT BYTES");
    // sound_begin has disabled DMC DMA and all internal APU voices. A compatible external circuit is required.
    // Stop NMI and mask IRQ so neither can split a software-timed serial frame.
    __ppu_ctrl_set(0); __irq_disable();
    __midi_out_byte(0x7E);
    __midi_program_change(2,5);
    __midi_control_change(2,7,100);
    __midi_note_on(2,69,96);
    __midi_note_off(2,69,0);
    __midi_clock(); __midi_start(); __midi_continue(); __midi_stop();
    // The emulator's unconnected input bit is low. Reading returns zero here.
    // A real input held high would block waiting for its start bit, without timeout.
    sound_result[0]=__midi_in_byte();
    sound_result[1]=1;
    __ppu_ctrl_set(0x80); __irq_enable();
    sound_end();
}

Expected result

The screen says EXTERNAL PORT DATA and no internal voice sounds. Transmitted bytes in hexadecimal are 7E; C2 05; B2 07 64; 92 45 60; 82 45 00; F8 FA FB FC. The check requires 57-cycle NTSC transmit cells and 57-cycle data-read spacing. Received 00 comes from the emulator’s unconnected input and does not prove communication with an external device.

__midi_start: NTSC
NTSC

Complete example program

Build command

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\sound_midi.c -I .\kitaqfc\lib -I .\kitaq-docs\samples -o .\out\fc-midi.nes --mapper=nrom --nes-chr=.\kitaq-docs\samples\font.chr --no-cache --no-disasm
Implementation and source

kitaqfc/lib/intrinsics.h:306

Declaration notes (original source)
Transmit realtime Start (FA) through the serial adapter path.

kitaqfc/kitaqfc/CodeGenerator.cs:4804

csharp
case "__midi_start": if (!Need(0)) return true; EmitAsm("LDA", Imm(0xFA)); EmitAsm("JSR", Abs("__kq_midi_out_a")); return true;

EmitHelperStart("__kq_midi_out_a");
            bool midiScratchInZeroPage = _runtimeIntrinsicTmp0Address <= 0xFF;
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("LDA", Imm(0));
            EmitAsm("STA", Mem(0x4016));
            // Unroll so neither branch cost nor payload bits change the baud.
            // Each data edge: 44 NOP cycles + LSR zp (5) + LDA/ADC (4) + STA (4).
            for (int bit = 0; bit < 8; bit++)
            {
                for (int delay = 0; delay < (midiScratchInZeroPage ? 22 : 20); delay++) EmitAsm("NOP");
                if (!midiScratchInZeroPage) EmitAsm("BIT", Mem(CallArgBase));
                EmitAsm("LSR", Mem(_runtimeIntrinsicTmp0Address));
                EmitAsm("LDA", Imm(0));
                EmitAsm("ADC", Imm(0));
                EmitAsm("STA", Mem(0x4016));
            }
            // 51 delay cycles plus LDA/STA (6) place the stop bit 57 cycles later.
            for (int delay = 0; delay < 24; delay++) EmitAsm("NOP");
            EmitAsm("BIT", Mem(CallArgBase));
            EmitAsm("LDA", Imm(1));
            EmitAsm("STA", Mem(0x4016));
            EmitMidiBitDelay(); // Hold stop high for at least one full bit cell.
            EmitAsm("RTS");

            EmitHelperStart("__midi_out_byte");
            EmitAsm("LDA", Mem(CallArgBase));
            EmitAsm("JSR", Abs("__kq_midi_out_a"));
            EmitAsm("RTS");

            // Poll in nine-cycle steps, then sample near data-bit centers.
            // IRQ/NMI masking belongs to the caller; no timeout/framing status is returned.
            EmitHelperStart("__midi_in_byte");
            EmitAsm("LDA", Imm(0));
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp0Address));
            string waitStart = NewGeneratedLabel("midi_wait_start");
            _assembly.Add(Expr.Make(Tag.Label, waitStart));
            EmitAsm("LDA", Mem(0x4017));
            EmitAsm("AND", Imm(0x10));
            EmitAsm("BNE", Rel(waitStart));
            // 81 cycles from the start-detect read to the first data read.
            // Polling uncertainty is at most nine cycles within a 57-cycle cell.
            for (int delay = 0; delay < 35; delay++) EmitAsm("NOP");
            EmitAsm("BIT", Mem(CallArgBase));
            for (int bit = 0; bit < 8; bit++)
            {
                EmitAsm("LDA", Mem(0x4017));
                EmitAsm("AND", Imm(0x10));
                EmitAsm("CMP", Imm(0x10));
                EmitAsm("ROR", Mem(_runtimeIntrinsicTmp0Address));
                for (int delay = 0; delay < (midiScratchInZeroPage ? 22 : 20); delay++) EmitAsm("NOP");
                if (!midiScratchInZeroPage) EmitAsm("BIT", Mem(CallArgBase));
            }
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("RTS");

            // Compose the channelized note-on status, then send note and velocity arguments.
__midi_stop

Send one realtime Stop byte FC. This does not directly silence the APU or previously sent notes.

Syntax

c
void __midi_stop(void);

Return value

No return value. Changes sound registers or driver state.

Conditions and limits

Requires an external circuit connecting $4016 D0 to TX and $4017 D4 to RX. A standard controller does not provide MIDI and these calls do not play the internal APU. Stop DMC playback/DMA and mask NMI and IRQ during transfers; avoid concurrent port/shared-scratch access. NTSC’s 57 cycles/bit give about 31,400 bps, roughly +0.5% from 31,250. PAL timing and round-trip communication with a physical adapter are unverified.

Usage example

c
// Controller-port byte encoding. This does not synthesize sound inside the FC.
#define SOUND_PORT_ONLY
#include "fc_sound_example.h"
void main(void) {
    sound_begin("MIDI PORT BYTES");
    // sound_begin has disabled DMC DMA and all internal APU voices. A compatible external circuit is required.
    // Stop NMI and mask IRQ so neither can split a software-timed serial frame.
    __ppu_ctrl_set(0); __irq_disable();
    __midi_out_byte(0x7E);
    __midi_program_change(2,5);
    __midi_control_change(2,7,100);
    __midi_note_on(2,69,96);
    __midi_note_off(2,69,0);
    __midi_clock(); __midi_start(); __midi_continue(); __midi_stop();
    // The emulator's unconnected input bit is low. Reading returns zero here.
    // A real input held high would block waiting for its start bit, without timeout.
    sound_result[0]=__midi_in_byte();
    sound_result[1]=1;
    __ppu_ctrl_set(0x80); __irq_enable();
    sound_end();
}

Expected result

The screen says EXTERNAL PORT DATA and no internal voice sounds. Transmitted bytes in hexadecimal are 7E; C2 05; B2 07 64; 92 45 60; 82 45 00; F8 FA FB FC. The check requires 57-cycle NTSC transmit cells and 57-cycle data-read spacing. Received 00 comes from the emulator’s unconnected input and does not prove communication with an external device.

__midi_stop: NTSC
NTSC

Complete example program

Build command

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\sound_midi.c -I .\kitaqfc\lib -I .\kitaq-docs\samples -o .\out\fc-midi.nes --mapper=nrom --nes-chr=.\kitaq-docs\samples\font.chr --no-cache --no-disasm
Implementation and source

kitaqfc/lib/intrinsics.h:310

Declaration notes (original source)
Transmit realtime Stop (FC) through the serial adapter path.

kitaqfc/kitaqfc/CodeGenerator.cs:4806

csharp
case "__midi_stop": if (!Need(0)) return true; EmitAsm("LDA", Imm(0xFC)); EmitAsm("JSR", Abs("__kq_midi_out_a")); return true;
                // Sprite helpers modify the runtime OAM representation; DMA remains a separate operation.

EmitHelperStart("__kq_midi_out_a");
            bool midiScratchInZeroPage = _runtimeIntrinsicTmp0Address <= 0xFF;
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("LDA", Imm(0));
            EmitAsm("STA", Mem(0x4016));
            // Unroll so neither branch cost nor payload bits change the baud.
            // Each data edge: 44 NOP cycles + LSR zp (5) + LDA/ADC (4) + STA (4).
            for (int bit = 0; bit < 8; bit++)
            {
                for (int delay = 0; delay < (midiScratchInZeroPage ? 22 : 20); delay++) EmitAsm("NOP");
                if (!midiScratchInZeroPage) EmitAsm("BIT", Mem(CallArgBase));
                EmitAsm("LSR", Mem(_runtimeIntrinsicTmp0Address));
                EmitAsm("LDA", Imm(0));
                EmitAsm("ADC", Imm(0));
                EmitAsm("STA", Mem(0x4016));
            }
            // 51 delay cycles plus LDA/STA (6) place the stop bit 57 cycles later.
            for (int delay = 0; delay < 24; delay++) EmitAsm("NOP");
            EmitAsm("BIT", Mem(CallArgBase));
            EmitAsm("LDA", Imm(1));
            EmitAsm("STA", Mem(0x4016));
            EmitMidiBitDelay(); // Hold stop high for at least one full bit cell.
            EmitAsm("RTS");

            EmitHelperStart("__midi_out_byte");
            EmitAsm("LDA", Mem(CallArgBase));
            EmitAsm("JSR", Abs("__kq_midi_out_a"));
            EmitAsm("RTS");

            // Poll in nine-cycle steps, then sample near data-bit centers.
            // IRQ/NMI masking belongs to the caller; no timeout/framing status is returned.
            EmitHelperStart("__midi_in_byte");
            EmitAsm("LDA", Imm(0));
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp0Address));
            string waitStart = NewGeneratedLabel("midi_wait_start");
            _assembly.Add(Expr.Make(Tag.Label, waitStart));
            EmitAsm("LDA", Mem(0x4017));
            EmitAsm("AND", Imm(0x10));
            EmitAsm("BNE", Rel(waitStart));
            // 81 cycles from the start-detect read to the first data read.
            // Polling uncertainty is at most nine cycles within a 57-cycle cell.
            for (int delay = 0; delay < 35; delay++) EmitAsm("NOP");
            EmitAsm("BIT", Mem(CallArgBase));
            for (int bit = 0; bit < 8; bit++)
            {
                EmitAsm("LDA", Mem(0x4017));
                EmitAsm("AND", Imm(0x10));
                EmitAsm("CMP", Imm(0x10));
                EmitAsm("ROR", Mem(_runtimeIntrinsicTmp0Address));
                for (int delay = 0; delay < (midiScratchInZeroPage ? 22 : 20); delay++) EmitAsm("NOP");
                if (!midiScratchInZeroPage) EmitAsm("BIT", Mem(CallArgBase));
            }
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("RTS");

            // Compose the channelized note-on status, then send note and velocity arguments.
__mirroring_set

Map the four logical nametable regions onto the console’s two pages of screen RAM, for scrolling layouts or single-screen page selection.

Syntax

c
void __mirroring_set(u8 mode);

Parameters

mode

Mode numbers vary by board. MMC1/SUROM: 0=lower single screen, 1=upper single screen, 2=vertical, 3=horizontal. MMC3: 0=vertical, 1=horizontal. MMC5: 0=horizontal, 1=vertical, 2=lower single screen, 3=upper single screen. AxROM: 0=lower single screen, 1=upper single screen. FME7: 0=vertical, 1=horizontal, 2=lower single screen, 3=upper single screen. MMC3/AxROM use the low bit; the others use the low two bits.

Return value

No return value.

Conditions and limits

Intrinsic declarations are in intrinsics.h. Include nes_game.h for the nes_mapper_irq_* macros. Select the board at build time with --mapper.

Vertical mirroring aliases the four regions as A B A B; horizontal mirroring gives A A B B. Lower single screen gives A A A A and upper single screen B B B B. This changes memory mapping; it neither flips nor copies background graphics. Unsupported boards, including fixed-mirroring NROM, are rejected at build time.

MMC1 writes control $0C|mode, selecting a fixed upper 16 KiB PRG window and 8 KiB CHR mode. SUROM also updates its control shadow. AxROM changes the screen bit while retaining the runtime PRG bank value. If you also write mapper registers directly, keep them consistent with the runtime bank state.

For MMC5 the API writes $50/$44/$00/$55 to $5105. These arrangements use only the console’s two CIRAM pages. ExRAM and fill mode are outside this API. KUROSAKI supports all sixteen CIRAM-only arrangements; MMC5 ExRAM and fill rendering remain unsupported.

Change the sample’s --mapper=mmc3 to mmc1, mmc5, axrom or fme7 to inspect each board. The sample initializes MMC5/FME7 font CHR banks through their board registers.

Usage example

c
k=1;
    for (i=0;i<4;i++) {
        __mirroring_set(i);
        for (j=0;j<4;j++) { mirror_result[k]=mirror_read(0x2000+(u16)j*1024); k++; }
    }

This is a calling fragment. Follow the stated initialization and buffer requirements before using it in a complete program.

Expected result

The lesson writes 17 to the lower physical RAM page and 34 to the upper page, then reads the four logical regions. A denotes the lower page and B the upper page. Regions with the same letter share RAM, so a write through one is visible through the others. Columns are $2000, $2400, $2800 and $2C00; rows are modes 0..3. Text is displayed after the readback.

__mirroring_set: MMC1-DEFAULT
MMC1-DEFAULT
__mirroring_set: MMC3-DEFAULT
MMC3-DEFAULT
__mirroring_set: MMC5-DEFAULT
MMC5-DEFAULT
__mirroring_set: AXROM-DEFAULT
AXROM-DEFAULT
__mirroring_set: FME7-DEFAULT
FME7-DEFAULT

Complete example program

Build command

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

kitaqfc/lib/intrinsics.h:322

Declaration notes (original source)
Emit the selected mapper mirroring operation; available modes and writable control depend on the board.
__mul16x8

Compute a unsigned multiplication.

Syntax

c
u16 __mul16x8(u16 a, u8 b);

Parameters

a

Unsigned word, 0..65535.

b

Unsigned coefficient, 0..255.

Return value

Returns the low 16 bits of a*b.

Conditions and limits

On FC, q8_8/q1_7 names still use raw integer products without a fixed-point downshift. The Q1.7 functions alias their corresponding signed operations. The caller handles units; signed results are returned as u16 bit patterns.

Usage example

c
// Compare literal coefficients with values read from RAM.
result[2]=__mul16x8(513,127);
v0=513;v1=127;
result[3]=__mul16x8(v0,v1);

This is a calling fragment. Follow the stated initialization and buffer requirements before using it in a complete program.

Expected result

Compare literal inputs with values stored in RAM. Representative results appear as unsigned 16-bit values; negatives use two’s-complement bit patterns. GB rescales fixed-point products, while FC returns integer products, so identical inputs need not give identical results.

The excerpt stores these 16-bit values: result[2]=65151, result[3]=65151. Each literal-input calculation matches its RAM-input counterpart. The screen shows representative values; the result array holds every result.

__mul16x8: NROM
NROM

Complete example program

Build command

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\batch3_math.c -I .\kitaqfc\lib -I .\kitaq-docs\samples -o .\out\batch3_math.nes --no-cache --no-disasm --mapper=nrom --nes-chr=.\kitaq-docs\samples\font.chr
Implementation and source

kitaqfc/lib/intrinsics.h:51

Declaration notes (original source)
Return the low word of the unsigned product; overflow wraps without saturation.

kitaqfc/kitaqfc/CodeGenerator.cs:4647

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

void EmitHelperMath()
        {
            // Consume eight multiplier bits and accumulate the low product word in shared temporaries, returning A/X.
            EmitHelperStart("__mul16x8");
            EmitAsm("LDA", Imm(0));
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp1Address));
            EmitAsm("LDX", Imm(8));
            string loop = NewGeneratedLabel("mul16x8_loop");
            string skip = NewGeneratedLabel("mul16x8_skip_add");
            _assembly.Add(Expr.Make(Tag.Label, loop));
            EmitAsm("LSR", Mem(CallArgBase + 2));
            EmitAsm("BCC", Rel(skip));
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("CLC");
            EmitAsm("ADC", Mem(CallArgBase));
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp1Address));
            EmitAsm("ADC", Mem(CallArgBase + 1));
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp1Address));
            _assembly.Add(Expr.Make(Tag.Label, skip));
            EmitAsm("ASL", Mem(CallArgBase));
            EmitAsm("ROL", Mem(CallArgBase + 1));
            EmitAsm("DEX");
            EmitAsm("BNE", Rel(loop));
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("LDX", Mem(_runtimeIntrinsicTmp1Address));
            EmitAsm("RTS");

            // Zero-extend the byte multiplicand, reuse word multiplication and return its high byte.
            EmitHelperStart("__mul8x8_hi");
            EmitAsm("LDA", Mem(CallArgBase + 1));
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp2Address));
            EmitAsm("LDA", Imm(0));
            EmitAsm("STA", Mem(CallArgBase + 1));
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp2Address));
            EmitAsm("STA", Mem(CallArgBase + 2));
            EmitAsm("JSR", Abs("__mul16x8"));
            EmitAsm("TXA");
            EmitAsm("RTS");

            // Convert signed operands to magnitudes, multiply, then restore the product sign modulo 65536.
            EmitHelperStart("__smul16x8");
            EmitAsm("LDA", Imm(0));
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp2Address));
            string aPos = NewGeneratedLabel("smul_a_positive");
            EmitAsm("LDA", Mem(CallArgBase + 1));
            EmitAsm("AND", Imm(0x80));
            EmitAsm("BEQ", Rel(aPos));
            EmitAsm("LDA", Mem(CallArgBase)); EmitAsm("EOR", Imm(0xFF)); EmitAsm("CLC"); EmitAsm("ADC", Imm(1)); EmitAsm("STA", Mem(CallArgBase));
            EmitAsm("LDA", Mem(CallArgBase + 1)); EmitAsm("EOR", Imm(0xFF)); EmitAsm("ADC", Imm(0)); EmitAsm("STA", Mem(CallArgBase + 1));
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp2Address)); EmitAsm("EOR", Imm(1)); EmitAsm("STA", Mem(_runtimeIntrinsicTmp2Address));
            _assembly.Add(Expr.Make(Tag.Label, aPos));
            string bPos = NewGeneratedLabel("smul_b_positive");
            EmitAsm("LDA", Mem(CallArgBase + 2));
            EmitAsm("AND", Imm(0x80));
            EmitAsm("BEQ", Rel(bPos));
            EmitAsm("LDA", Mem(CallArgBase + 2)); EmitAsm("EOR", Imm(0xFF)); EmitAsm("CLC"); EmitAsm("ADC", Imm(1)); EmitAsm("STA", Mem(CallArgBase + 2));
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp2Address)); EmitAsm("EOR", Imm(1)); EmitAsm("STA", Mem(_runtimeIntrinsicTmp2Address));
            _assembly.Add(Expr.Make(Tag.Label, bPos));
            EmitAsm("JSR", Abs("__mul16x8"));
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("STX", Mem(_runtimeIntrinsicTmp1Address));
            string sdone = NewGeneratedLabel("smul_done");
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp2Address));
            EmitAsm("BEQ", Rel(sdone));
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp0Address)); EmitAsm("EOR", Imm(0xFF)); EmitAsm("CLC"); EmitAsm("ADC", Imm(1)); EmitAsm("STA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp1Address)); EmitAsm("EOR", Imm(0xFF)); EmitAsm("ADC", Imm(0)); EmitAsm("STA", Mem(_runtimeIntrinsicTmp1Address));
            _assembly.Add(Expr.Make(Tag.Label, sdone));
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("LDX", Mem(_runtimeIntrinsicTmp1Address));
            EmitAsm("RTS");

            EmitMacHelper("__mac16", "__mul16x8");
            EmitMacHelper("__smac16", "__smul16x8");
            EmitDot2Helper("__dot2_q8_8", "__mul16x8");
            EmitDot3Helper("__dot3_q8_8", "__mul16x8");
            EmitDot2Helper("__sdot2_q8_8", "__smul16x8");
            EmitDot3Helper("__sdot3_q8_8", "__smul16x8");
        }
__mul8x8_hi

Multiply two unsigned bytes and return the high byte.

Syntax

c
u8 __mul8x8_hi(u8 a, u8 b);

Parameters

a

Unsigned byte, 0..255.

b

Unsigned byte, 0..255.

Return value

floor(a*b/256), in 0..254.

Usage example

c
// Compare literal coefficients with values read from RAM.
result[0]=__mul8x8_hi(255,255);
v0=255;v1=255;
result[1]=__mul8x8_hi(v0,v1);

This is a calling fragment. Follow the stated initialization and buffer requirements before using it in a complete program.

Expected result

Compare literal inputs with values stored in RAM. Representative results appear as unsigned 16-bit values; negatives use two’s-complement bit patterns. GB rescales fixed-point products, while FC returns integer products, so identical inputs need not give identical results.

The excerpt stores these 16-bit values: result[0]=254, result[1]=254. Each literal-input calculation matches its RAM-input counterpart. The screen shows representative values; the result array holds every result.

__mul8x8_hi: NROM
NROM

Complete example program

Build command

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\batch3_math.c -I .\kitaqfc\lib -I .\kitaq-docs\samples -o .\out\batch3_math.nes --no-cache --no-disasm --mapper=nrom --nes-chr=.\kitaq-docs\samples\font.chr
Implementation and source

kitaqfc/lib/intrinsics.h:49

Declaration notes (original source)
Return the high byte of the unsigned 8-by-8 product, equivalent to floor(a*b/256).

kitaqfc/kitaqfc/CodeGenerator.cs:4649

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

void EmitHelperMath()
        {
            // Consume eight multiplier bits and accumulate the low product word in shared temporaries, returning A/X.
            EmitHelperStart("__mul16x8");
            EmitAsm("LDA", Imm(0));
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp1Address));
            EmitAsm("LDX", Imm(8));
            string loop = NewGeneratedLabel("mul16x8_loop");
            string skip = NewGeneratedLabel("mul16x8_skip_add");
            _assembly.Add(Expr.Make(Tag.Label, loop));
            EmitAsm("LSR", Mem(CallArgBase + 2));
            EmitAsm("BCC", Rel(skip));
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("CLC");
            EmitAsm("ADC", Mem(CallArgBase));
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp1Address));
            EmitAsm("ADC", Mem(CallArgBase + 1));
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp1Address));
            _assembly.Add(Expr.Make(Tag.Label, skip));
            EmitAsm("ASL", Mem(CallArgBase));
            EmitAsm("ROL", Mem(CallArgBase + 1));
            EmitAsm("DEX");
            EmitAsm("BNE", Rel(loop));
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("LDX", Mem(_runtimeIntrinsicTmp1Address));
            EmitAsm("RTS");

            // Zero-extend the byte multiplicand, reuse word multiplication and return its high byte.
            EmitHelperStart("__mul8x8_hi");
            EmitAsm("LDA", Mem(CallArgBase + 1));
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp2Address));
            EmitAsm("LDA", Imm(0));
            EmitAsm("STA", Mem(CallArgBase + 1));
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp2Address));
            EmitAsm("STA", Mem(CallArgBase + 2));
            EmitAsm("JSR", Abs("__mul16x8"));
            EmitAsm("TXA");
            EmitAsm("RTS");

            // Convert signed operands to magnitudes, multiply, then restore the product sign modulo 65536.
            EmitHelperStart("__smul16x8");
            EmitAsm("LDA", Imm(0));
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp2Address));
            string aPos = NewGeneratedLabel("smul_a_positive");
            EmitAsm("LDA", Mem(CallArgBase + 1));
            EmitAsm("AND", Imm(0x80));
            EmitAsm("BEQ", Rel(aPos));
            EmitAsm("LDA", Mem(CallArgBase)); EmitAsm("EOR", Imm(0xFF)); EmitAsm("CLC"); EmitAsm("ADC", Imm(1)); EmitAsm("STA", Mem(CallArgBase));
            EmitAsm("LDA", Mem(CallArgBase + 1)); EmitAsm("EOR", Imm(0xFF)); EmitAsm("ADC", Imm(0)); EmitAsm("STA", Mem(CallArgBase + 1));
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp2Address)); EmitAsm("EOR", Imm(1)); EmitAsm("STA", Mem(_runtimeIntrinsicTmp2Address));
            _assembly.Add(Expr.Make(Tag.Label, aPos));
            string bPos = NewGeneratedLabel("smul_b_positive");
            EmitAsm("LDA", Mem(CallArgBase + 2));
            EmitAsm("AND", Imm(0x80));
            EmitAsm("BEQ", Rel(bPos));
            EmitAsm("LDA", Mem(CallArgBase + 2)); EmitAsm("EOR", Imm(0xFF)); EmitAsm("CLC"); EmitAsm("ADC", Imm(1)); EmitAsm("STA", Mem(CallArgBase + 2));
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp2Address)); EmitAsm("EOR", Imm(1)); EmitAsm("STA", Mem(_runtimeIntrinsicTmp2Address));
            _assembly.Add(Expr.Make(Tag.Label, bPos));
            EmitAsm("JSR", Abs("__mul16x8"));
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("STX", Mem(_runtimeIntrinsicTmp1Address));
            string sdone = NewGeneratedLabel("smul_done");
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp2Address));
            EmitAsm("BEQ", Rel(sdone));
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp0Address)); EmitAsm("EOR", Imm(0xFF)); EmitAsm("CLC"); EmitAsm("ADC", Imm(1)); EmitAsm("STA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp1Address)); EmitAsm("EOR", Imm(0xFF)); EmitAsm("ADC", Imm(0)); EmitAsm("STA", Mem(_runtimeIntrinsicTmp1Address));
            _assembly.Add(Expr.Make(Tag.Label, sdone));
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("LDX", Mem(_runtimeIntrinsicTmp1Address));
            EmitAsm("RTS");

            EmitMacHelper("__mac16", "__mul16x8");
            EmitMacHelper("__smac16", "__smul16x8");
            EmitDot2Helper("__dot2_q8_8", "__mul16x8");
            EmitDot3Helper("__dot3_q8_8", "__mul16x8");
            EmitDot2Helper("__sdot2_q8_8", "__smul16x8");
            EmitDot3Helper("__sdot3_q8_8", "__smul16x8");
        }
__nametable_put

Write one tile byte directly to nametable 0 at PPU address 0x2000 + y*32 + x. The helper resets the latch before setting the address. It does not write tile graphics or palette attributes and does not clip coordinates.

Syntax

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

Parameters

x / y

Tile coordinates, not pixels. Keep x in 0..31 and y in 0..29 for the visible 32×30 tile map. Each tile covers 8×8 pixels. The byte arguments are not clamped; invalid coordinates can write outside the intended tile area.

tile

Tile number 0..255 in the background pattern table selected by PPUCTRL. It selects existing CHR graphics; it is not a pixel color or a source-data pointer.

Return value

None (void).

Conditions and limits

Direct PPU access requires rendering to be off or the entire operation to fit a safe VBlank interval. These functions do not wait, queue, mask NMI or restore scroll. Prevent an interrupt from changing the shared latch/address during the call. PPUCTRL bit 2 selects a destination step of 1 or 32 per data byte. The PPU handles mirroring and address wrapping; CHR-ROM cannot be written. Set the intended scroll state after access and before rendering.

NESdev: PPU registers

NESdev: PPU scrolling

Usage example

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

This is a calling fragment. Follow the stated initialization and buffer requirements before using it in a complete program.

Expected result

This program teaches register setup, direct tile/attribute writes, scrolling and separate background/sprite palette loads. The blue band at (0,16) measures 256×16 pixels: its first row repeats a solid 8×8 tile, a left-half 4×8 bar and an eight-row triangle; its second row contains only left-half bars. A solid green 64×32 rectangle starts at (16,48). A red 64×32 region at (144,48) contains an 8-by-4 grid of triangle tiles.

At y=96, blue markers at x=16, 24 and 32 are respectively a square, left-half bar and triangle; a red left-half bar is at x=80 and a green triangle at x=144. These are background tiles. At y=144, the separately colored sprites are a red 8×8 square at x=16, a blue left-half 4×8 bar at x=80 and a green eight-row triangle at x=144. Final scrolling is (0,0).

The image checks 16,384 pixels for the expected shapes and colors, including sprite colors. Separate state tests cover 31 cases: register replacement, status/latch effects, saved scroll coordinates, zero/255-byte transfers, increments 1/32, map edges, zero rectangle dimensions, table selectors, whole-byte attributes and both palette targets. Values of Y=255 are checked as register writes, not as correct scrolling artwork. Evidence is from KUROSAKI with NROM; real hardware is not tested.

__nametable_put: NROM
NROM

Complete example program

Build command

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\ppu_intrinsics.c -I .\kitaqfc\lib -I .\kitaq-docs\samples --nes-chr=.\kitaq-docs\samples\api-examples\fc\vram_shapes.chr -o .\out\ppu_intrinsics.nes --no-cache --no-disasm
Implementation and source

kitaqfc/lib/intrinsics.h:124

Declaration notes (original source)
Write one tile directly at 2000+y*32+x; use x<32, y<30 for visible nametable cells.

kitaqfc/kitaqfc/CodeGenerator.cs:4820

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

void EmitHelperNametablePut()
        {
            EmitHelperStart("__nametable_put");
            // args: x, y, tile. PPU address = $2000 + y * 32 + x.
            EmitAsm("LDA", Mem(CallArgBase + 1));
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("LDA", Imm(0));
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp1Address));
            for (int i = 0; i < 5; i++) { EmitAsm("ASL", Mem(_runtimeIntrinsicTmp0Address)); EmitAsm("ROL", Mem(_runtimeIntrinsicTmp1Address)); }
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("CLC");
            EmitAsm("ADC", Mem(CallArgBase));
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp1Address));
            EmitAsm("ADC", Imm(0x20));
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp1Address));
            EmitAsm("LDA", Mem(0x2002));
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp1Address));
            EmitAsm("STA", Mem(0x2006));
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("STA", Mem(0x2006));
            EmitAsm("LDA", Mem(CallArgBase + 2));
            EmitAsm("STA", Mem(0x2007));
            EmitAsm("RTS");
        }
__nametable_put_nt

Select logical nametable nt & 3 and write one tile at 0x2000 + (nt & 3)*0x400 + y*32 + x. The latch is reset before the address pair. Cartridge mirroring can make different logical table numbers address the same physical memory.

Syntax

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

Parameters

nt

Logical nametable selector. Only the lowest two bits are used (nt & 3); for example, 4 selects table 0 and 7 selects table 3. This does not allocate independent storage or change mapper mirroring.

x / y

Tile coordinates, not pixels. Keep x in 0..31 and y in 0..29 for the visible 32×30 tile map. Each tile covers 8×8 pixels. The byte arguments are not clamped; invalid coordinates can write outside the intended tile area.

tile

Tile number 0..255 in the background pattern table selected by PPUCTRL. It selects existing CHR graphics; it is not a pixel color or a source-data pointer.

Return value

None (void).

Conditions and limits

Direct PPU access requires rendering to be off or the entire operation to fit a safe VBlank interval. These functions do not wait, queue, mask NMI or restore scroll. Prevent an interrupt from changing the shared latch/address during the call. PPUCTRL bit 2 selects a destination step of 1 or 32 per data byte. The PPU handles mirroring and address wrapping; CHR-ROM cannot be written. Set the intended scroll state after access and before rendering.

NESdev: PPU registers

NESdev: PPU scrolling

Usage example

c
__nametable_put_nt(4,10,12,2); // Red left-half marker at (80,96), table 0.

This is a calling fragment. Follow the stated initialization and buffer requirements before using it in a complete program.

Expected result

This program teaches register setup, direct tile/attribute writes, scrolling and separate background/sprite palette loads. The blue band at (0,16) measures 256×16 pixels: its first row repeats a solid 8×8 tile, a left-half 4×8 bar and an eight-row triangle; its second row contains only left-half bars. A solid green 64×32 rectangle starts at (16,48). A red 64×32 region at (144,48) contains an 8-by-4 grid of triangle tiles.

At y=96, blue markers at x=16, 24 and 32 are respectively a square, left-half bar and triangle; a red left-half bar is at x=80 and a green triangle at x=144. These are background tiles. At y=144, the separately colored sprites are a red 8×8 square at x=16, a blue left-half 4×8 bar at x=80 and a green eight-row triangle at x=144. Final scrolling is (0,0).

The image checks 16,384 pixels for the expected shapes and colors, including sprite colors. Separate state tests cover 31 cases: register replacement, status/latch effects, saved scroll coordinates, zero/255-byte transfers, increments 1/32, map edges, zero rectangle dimensions, table selectors, whole-byte attributes and both palette targets. Values of Y=255 are checked as register writes, not as correct scrolling artwork. Evidence is from KUROSAKI with NROM; real hardware is not tested.

__nametable_put_nt: NROM
NROM

Complete example program

Build command

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\ppu_intrinsics.c -I .\kitaqfc\lib -I .\kitaq-docs\samples --nes-chr=.\kitaq-docs\samples\api-examples\fc\vram_shapes.chr -o .\out\ppu_intrinsics.nes --no-cache --no-disasm
Implementation and source

kitaqfc/lib/intrinsics.h:126

Declaration notes (original source)
Write one tile in nametable nt&3. Mirroring can alias logical nametables; coordinates are unchecked.

kitaqfc/kitaqfc/CodeGenerator.cs:4821

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

void EmitHelperNametablePutNt()
        {
            EmitHelperStart("__nametable_put_nt");
            // args: nt,x,y,tile. PPU address = $2000 + nt*$400 + y*32 + x.
            EmitAsm("LDA", Mem(CallArgBase));
            EmitAsm("AND", Imm(3));
            EmitAsm("ASL");
            EmitAsm("ASL");
            EmitAsm("CLC");
            EmitAsm("ADC", Imm(0x20));
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp2Address));
            EmitAsm("LDA", Mem(CallArgBase + 2));
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("LDA", Imm(0));
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp1Address));
            for (int i = 0; i < 5; i++) { EmitAsm("ASL", Mem(_runtimeIntrinsicTmp0Address)); EmitAsm("ROL", Mem(_runtimeIntrinsicTmp1Address)); }
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("CLC");
            EmitAsm("ADC", Mem(CallArgBase + 1));
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp1Address));
            EmitAsm("ADC", Mem(_runtimeIntrinsicTmp2Address));
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp1Address));
            EmitAsm("LDA", Mem(0x2002));
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp1Address));
            EmitAsm("STA", Mem(0x2006));
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("STA", Mem(0x2006));
            EmitAsm("LDA", Mem(CallArgBase + 3));
            EmitAsm("STA", Mem(0x2007));
            EmitAsm("RTS");
        }
__nametable_rect

Fill a w-by-h rectangle of tile bytes in nametable 0, starting at (x,y). Reset the latch and address for each row, write w copies of tile, then advance the next row start by 32 bytes. Palette attributes are not changed.

Syntax

c
void __nametable_rect(u8 x, u8 y, u8 w, u8 h, u8 tile);

Parameters

x / y

Tile coordinates, not pixels. Keep x in 0..31 and y in 0..29 for the visible 32×30 tile map. Each tile covers 8×8 pixels. The byte arguments are not clamped; invalid coordinates can write outside the intended tile area.

w / h

Width and height in tiles, each an unsigned byte. Keep x+w<=32 and y+h<=30. Height zero performs no PPU accesses. Width zero writes no tile bytes, but still resets the latch and seeks each of the h row addresses. Use PPU increment 1 for horizontal rows.

tile

Tile number 0..255 in the background pattern table selected by PPUCTRL. It selects existing CHR graphics; it is not a pixel color or a source-data pointer.

Return value

None (void).

Conditions and limits

Direct PPU access requires rendering to be off or the entire operation to fit a safe VBlank interval. These functions do not wait, queue, mask NMI or restore scroll. Prevent an interrupt from changing the shared latch/address during the call. PPUCTRL bit 2 selects a destination step of 1 or 32 per data byte. The PPU handles mirroring and address wrapping; CHR-ROM cannot be written. Set the intended scroll state after access and before rendering.

NESdev: PPU registers

NESdev: PPU scrolling

Usage example

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

This is a calling fragment. Follow the stated initialization and buffer requirements before using it in a complete program.

Expected result

This program teaches register setup, direct tile/attribute writes, scrolling and separate background/sprite palette loads. The blue band at (0,16) measures 256×16 pixels: its first row repeats a solid 8×8 tile, a left-half 4×8 bar and an eight-row triangle; its second row contains only left-half bars. A solid green 64×32 rectangle starts at (16,48). A red 64×32 region at (144,48) contains an 8-by-4 grid of triangle tiles.

At y=96, blue markers at x=16, 24 and 32 are respectively a square, left-half bar and triangle; a red left-half bar is at x=80 and a green triangle at x=144. These are background tiles. At y=144, the separately colored sprites are a red 8×8 square at x=16, a blue left-half 4×8 bar at x=80 and a green eight-row triangle at x=144. Final scrolling is (0,0).

The image checks 16,384 pixels for the expected shapes and colors, including sprite colors. Separate state tests cover 31 cases: register replacement, status/latch effects, saved scroll coordinates, zero/255-byte transfers, increments 1/32, map edges, zero rectangle dimensions, table selectors, whole-byte attributes and both palette targets. Values of Y=255 are checked as register writes, not as correct scrolling artwork. Evidence is from KUROSAKI with NROM; real hardware is not tested.

__nametable_rect: NROM
NROM

Complete example program

Build command

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\ppu_intrinsics.c -I .\kitaqfc\lib -I .\kitaq-docs\samples --nes-chr=.\kitaq-docs\samples\api-examples\fc\vram_shapes.chr -o .\out\ppu_intrinsics.nes --no-cache --no-disasm
Implementation and source

kitaqfc/lib/intrinsics.h:129

Declaration notes (original source)
Fill rows of nametable zero directly, advancing each row start by 32 bytes.
Keep the rectangle in bounds and use PPU increment one; this does not enqueue or clip.

kitaqfc/kitaqfc/CodeGenerator.cs:4822

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

void EmitHelperNametableRect()
        {
            EmitHelperStart("__nametable_rect");
            // args: x,y,w,h,tile; writes tile-filled rectangle to nametable 0.
            EmitAsm("LDA", Mem(CallArgBase + 1));
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("LDA", Imm(0));
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp1Address));
            for (int i = 0; i < 5; i++) { EmitAsm("ASL", Mem(_runtimeIntrinsicTmp0Address)); EmitAsm("ROL", Mem(_runtimeIntrinsicTmp1Address)); }
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp0Address)); EmitAsm("CLC"); EmitAsm("ADC", Mem(CallArgBase)); EmitAsm("STA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp1Address)); EmitAsm("ADC", Imm(0x20)); EmitAsm("STA", Mem(_runtimeIntrinsicTmp1Address));
            EmitAsm("LDA", Mem(CallArgBase + 3)); EmitAsm("STA", Mem(_runtimeIntrinsicTmp2Address));
            string rowLoop = NewGeneratedLabel("nt_rect_row");
            string done = NewGeneratedLabel("nt_rect_done");
            _assembly.Add(Expr.Make(Tag.Label, rowLoop));
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp2Address)); EmitAsm("BEQ", Rel(done));
            EmitAsm("LDA", Mem(0x2002)); EmitAsm("LDA", Mem(_runtimeIntrinsicTmp1Address)); EmitAsm("STA", Mem(0x2006)); EmitAsm("LDA", Mem(_runtimeIntrinsicTmp0Address)); EmitAsm("STA", Mem(0x2006));
            EmitAsm("LDY", Imm(0));
            string colLoop = NewGeneratedLabel("nt_rect_col");
            string rowDone = NewGeneratedLabel("nt_rect_row_done");
            _assembly.Add(Expr.Make(Tag.Label, colLoop));
            EmitAsm("CPY", Mem(CallArgBase + 2)); EmitAsm("BEQ", Rel(rowDone));
            EmitAsm("LDA", Mem(CallArgBase + 4)); EmitAsm("STA", Mem(0x2007)); EmitAsm("INY"); EmitAsm("BNE", Rel(colLoop));
            _assembly.Add(Expr.Make(Tag.Label, rowDone));
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp0Address)); EmitAsm("CLC"); EmitAsm("ADC", Imm(32)); EmitAsm("STA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("BCC", Rel(rowLoop + "_nohi"));
            EmitAsm("INC", Mem(_runtimeIntrinsicTmp1Address));
            _assembly.Add(Expr.Make(Tag.Label, rowLoop + "_nohi"));
            EmitAsm("DEC", Mem(_runtimeIntrinsicTmp2Address));
            EmitAsm("JMP", Abs(rowLoop));
            _assembly.Add(Expr.Make(Tag.Label, done));
            EmitAsm("RTS");
        }
__nametable_rect_nt

Fill w tiles per row for h rows in logical nametable nt & 3. The first address is 0x2000 + (nt & 3)*0x400 + y*32 + x; each row start advances by 32. This writes tile bytes only, with no attribute update, clipping or mirroring configuration.

Syntax

c
void __nametable_rect_nt(u8 nt, u8 x, u8 y, u8 w, u8 h, u8 tile);

Parameters

nt

Logical nametable selector. Only the lowest two bits are used (nt & 3); for example, 4 selects table 0 and 7 selects table 3. This does not allocate independent storage or change mapper mirroring.

x / y

Tile coordinates, not pixels. Keep x in 0..31 and y in 0..29 for the visible 32×30 tile map. Each tile covers 8×8 pixels. The byte arguments are not clamped; invalid coordinates can write outside the intended tile area.

w / h

Width and height in tiles, each an unsigned byte. Keep x+w<=32 and y+h<=30. Height zero performs no PPU accesses. Width zero writes no tile bytes, but still resets the latch and seeks each of the h row addresses. Use PPU increment 1 for horizontal rows.

tile

Tile number 0..255 in the background pattern table selected by PPUCTRL. It selects existing CHR graphics; it is not a pixel color or a source-data pointer.

Return value

None (void).

Conditions and limits

Direct PPU access requires rendering to be off or the entire operation to fit a safe VBlank interval. These functions do not wait, queue, mask NMI or restore scroll. Prevent an interrupt from changing the shared latch/address during the call. PPUCTRL bit 2 selects a destination step of 1 or 32 per data byte. The PPU handles mirroring and address wrapping; CHR-ROM cannot be written. Set the intended scroll state after access and before rendering.

NESdev: PPU registers

NESdev: PPU scrolling

Usage example

c
__nametable_rect_nt(4,18,6,8,4,3); // nt&3=0; red triangle tiles at (144,48).

This is a calling fragment. Follow the stated initialization and buffer requirements before using it in a complete program.

Expected result

This program teaches register setup, direct tile/attribute writes, scrolling and separate background/sprite palette loads. The blue band at (0,16) measures 256×16 pixels: its first row repeats a solid 8×8 tile, a left-half 4×8 bar and an eight-row triangle; its second row contains only left-half bars. A solid green 64×32 rectangle starts at (16,48). A red 64×32 region at (144,48) contains an 8-by-4 grid of triangle tiles.

At y=96, blue markers at x=16, 24 and 32 are respectively a square, left-half bar and triangle; a red left-half bar is at x=80 and a green triangle at x=144. These are background tiles. At y=144, the separately colored sprites are a red 8×8 square at x=16, a blue left-half 4×8 bar at x=80 and a green eight-row triangle at x=144. Final scrolling is (0,0).

The image checks 16,384 pixels for the expected shapes and colors, including sprite colors. Separate state tests cover 31 cases: register replacement, status/latch effects, saved scroll coordinates, zero/255-byte transfers, increments 1/32, map edges, zero rectangle dimensions, table selectors, whole-byte attributes and both palette targets. Values of Y=255 are checked as register writes, not as correct scrolling artwork. Evidence is from KUROSAKI with NROM; real hardware is not tested.

__nametable_rect_nt: NROM
NROM

Complete example program

Build command

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\ppu_intrinsics.c -I .\kitaqfc\lib -I .\kitaq-docs\samples --nes-chr=.\kitaq-docs\samples\api-examples\fc\vram_shapes.chr -o .\out\ppu_intrinsics.nes --no-cache --no-disasm
Implementation and source

kitaqfc/lib/intrinsics.h:131

Declaration notes (original source)
Fill a rectangle in nametable nt&3 with the same unchecked, direct-write row loop.

kitaqfc/kitaqfc/CodeGenerator.cs:4823

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

void EmitHelperNametableRectNt()
        {
            EmitHelperStart("__nametable_rect_nt");
            // args: nt,x,y,w,h,tile.
            EmitAsm("LDA", Mem(CallArgBase));
            EmitAsm("AND", Imm(3));
            EmitAsm("ASL");
            EmitAsm("ASL");
            EmitAsm("CLC");
            EmitAsm("ADC", Imm(0x20));
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp2Address));
            EmitAsm("LDA", Mem(CallArgBase + 2));
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("LDA", Imm(0));
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp1Address));
            for (int i = 0; i < 5; i++) { EmitAsm("ASL", Mem(_runtimeIntrinsicTmp0Address)); EmitAsm("ROL", Mem(_runtimeIntrinsicTmp1Address)); }
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp0Address)); EmitAsm("CLC"); EmitAsm("ADC", Mem(CallArgBase + 1)); EmitAsm("STA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp1Address)); EmitAsm("ADC", Mem(_runtimeIntrinsicTmp2Address)); EmitAsm("STA", Mem(_runtimeIntrinsicTmp1Address));
            EmitAsm("LDA", Mem(CallArgBase + 4)); EmitAsm("STA", Mem(_runtimeIntrinsicTmp2Address));
            string rowLoop = NewGeneratedLabel("nt_rect_nt_row");
            string done = NewGeneratedLabel("nt_rect_nt_done");
            _assembly.Add(Expr.Make(Tag.Label, rowLoop));
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp2Address)); EmitAsm("BEQ", Rel(done));
            EmitAsm("LDA", Mem(0x2002)); EmitAsm("LDA", Mem(_runtimeIntrinsicTmp1Address)); EmitAsm("STA", Mem(0x2006)); EmitAsm("LDA", Mem(_runtimeIntrinsicTmp0Address)); EmitAsm("STA", Mem(0x2006));
            EmitAsm("LDY", Imm(0));
            string colLoop = NewGeneratedLabel("nt_rect_nt_col");
            string rowDone = NewGeneratedLabel("nt_rect_nt_row_done");
            _assembly.Add(Expr.Make(Tag.Label, colLoop));
            EmitAsm("CPY", Mem(CallArgBase + 3)); EmitAsm("BEQ", Rel(rowDone));
            EmitAsm("LDA", Mem(CallArgBase + 5)); EmitAsm("STA", Mem(0x2007)); EmitAsm("INY"); EmitAsm("BNE", Rel(colLoop));
            _assembly.Add(Expr.Make(Tag.Label, rowDone));
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp0Address)); EmitAsm("CLC"); EmitAsm("ADC", Imm(32)); EmitAsm("STA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("BCC", Rel(rowLoop + "_nohi"));
            EmitAsm("INC", Mem(_runtimeIntrinsicTmp1Address));
            _assembly.Add(Expr.Make(Tag.Label, rowLoop + "_nohi"));
            EmitAsm("DEC", Mem(_runtimeIntrinsicTmp2Address));
            EmitAsm("JMP", Abs(rowLoop));
            _assembly.Add(Expr.Make(Tag.Label, done));
            EmitAsm("RTS");
        }
__nmi_disable

Disable NMI generation at the PPU by clearing bit 7 of the compiler's PPUCTRL shadow and writing it to $2000. Preserve its other bits. This neither changes the CPU IRQ mask nor turns off rendering.

Syntax

c
void __nmi_disable(void);

Return value

None (void).

Conditions and limits

These setters use the PPUCTRL shadow, not hardware readback; direct writes to $2000 do not update that shadow. Enabling NMI while the VBlank flag is set can trigger it immediately. Disabling PPU generation cannot cancel a request already latched by the CPU.

NESdev: NMI

NESdev: CPU interrupts

Usage example

c
__nmi_disable();
before_count=__nmi_ready();
for(i=0;i<2000;i++)frozen=__nmi_ready();
frozen=(u8)(frozen-before_count); // No default-handler advances while off.

This is a calling fragment. Follow the stated initialization and buffer requirements before using it in a complete program.

Expected result

The program shows how the CPU IRQ mask differs from PPU NMI control. On the black screen, blue text must show IRQ DISABLED I = 001, IRQ ENABLED I = 000, SAVED SECTION I = 001 and RESTORED I = 000. Each value is the extracted I bit, not the whole status byte. FAILED CHECKS must be 000.

Run for at least 360 emulator frames so the 256-wait demonstration can finish. NMI DELTA 3 WAITS must be 003 even with IRQ masked; NMI DELTA 256 WAITS must be 000 because the byte wraps. NMI OFF DELTA must be 000. This program uses the default handler without runtime.c. Separate state checks cover 21 cases, including status-bit restoration and bounded waits with NMI disabled or an empty custom handler. KUROSAKI/NROM is tested; physical hardware is not.

__nmi_disable: NROM
NROM

Complete example program

Build command

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\interrupt_intrinsics.c -I .\kitaqfc\lib -I .\kitaq-docs\samples --nes-chr=.\kitaq-docs\samples\font.chr -o .\out\interrupt_intrinsics.nes --no-cache --no-disasm
Implementation and source

kitaqfc/lib/intrinsics.h:174

Declaration notes (original source)
Clear bit 7 in the PPUCTRL shadow and write it to hardware; CPU IRQ masking is separate.

kitaqfc/kitaqfc/CodeGenerator.cs:4730

csharp
case "__nmi_disable":
                    if (!Need(0)) return true;
                    EmitAsm("LDA", Mem(_runtimePpuCtrlShadowAddress)); EmitAsm("AND", Imm(0x7F)); EmitAsm("STA", Mem(_runtimePpuCtrlShadowAddress)); EmitAsm("STA", Mem(0x2000)); return true;
__nmi_enable

Enable PPU-generated NMI by setting bit 7 of the compiler's PPUCTRL shadow, then writing that byte to $2000. The other shadow bits and the CPU IRQ mask are preserved. This does not enable background or sprite rendering.

Syntax

c
void __nmi_enable(void);

Return value

None (void).

Conditions and limits

These setters use the PPUCTRL shadow, not hardware readback; direct writes to $2000 do not update that shadow. Enabling NMI while the VBlank flag is set can trigger it immediately. Disabling PPU generation cannot cancel a request already latched by the CPU.

NESdev: NMI

NESdev: CPU interrupts

Usage example

c
__nmi_enable(); // Preserve the PPUCTRL shadow's other bits; CPU I stays set.
__nmi_wait(); // The default NMI handler still runs with IRQ masked.

This is a calling fragment. Follow the stated initialization and buffer requirements before using it in a complete program.

Expected result

The program shows how the CPU IRQ mask differs from PPU NMI control. On the black screen, blue text must show IRQ DISABLED I = 001, IRQ ENABLED I = 000, SAVED SECTION I = 001 and RESTORED I = 000. Each value is the extracted I bit, not the whole status byte. FAILED CHECKS must be 000.

Run for at least 360 emulator frames so the 256-wait demonstration can finish. NMI DELTA 3 WAITS must be 003 even with IRQ masked; NMI DELTA 256 WAITS must be 000 because the byte wraps. NMI OFF DELTA must be 000. This program uses the default handler without runtime.c. Separate state checks cover 21 cases, including status-bit restoration and bounded waits with NMI disabled or an empty custom handler. KUROSAKI/NROM is tested; physical hardware is not.

__nmi_enable: NROM
NROM

Complete example program

Build command

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\interrupt_intrinsics.c -I .\kitaqfc\lib -I .\kitaq-docs\samples --nes-chr=.\kitaq-docs\samples\font.chr -o .\out\interrupt_intrinsics.nes --no-cache --no-disasm
Implementation and source

kitaqfc/lib/intrinsics.h:172

Declaration notes (original source)
Set bit 7 in the PPUCTRL shadow and write it to hardware, preserving the other shadow bits.

kitaqfc/kitaqfc/CodeGenerator.cs:4727

csharp
case "__nmi_enable":
                    if (!Need(0)) return true;
                    EmitAsm("LDA", Mem(_runtimePpuCtrlShadowAddress)); EmitAsm("ORA", Imm(0x80)); EmitAsm("STA", Mem(_runtimePpuCtrlShadowAddress)); EmitAsm("STA", Mem(0x2000)); return true;
__nmi_ready

Read the compiler's current NMI counter without waiting or clearing it. Despite the name, the result is a counter byte rather than a Boolean ready flag. Compare it with a saved value to detect progress by the default NMI handler.

Syntax

c
u8 __nmi_ready(void);

Return value

An unsigned byte, 0–255, incremented modulo 256 by the default NMI handler. Zero is valid. After 256 advances the saved and current values match again; infrequent polling cannot count arbitrarily long intervals.

Conditions and limits

This counter belongs to the compiler's default NMI handler. Defining __nes_nmi replaces that handler. runtime.c uses its own nes_nmi_counter and nes_wait_nmi. An empty custom handler leaves the compiler counter unchanged, so __nmi_wait never returns. Enable NMI and call the wait outside the NMI handler.

NESdev: NMI

NESdev: CPU interrupts

Usage example

c
before_count=__nmi_ready(); // A wrapping byte, not a Boolean ready flag.

This is a calling fragment. Follow the stated initialization and buffer requirements before using it in a complete program.

Expected result

The program shows how the CPU IRQ mask differs from PPU NMI control. On the black screen, blue text must show IRQ DISABLED I = 001, IRQ ENABLED I = 000, SAVED SECTION I = 001 and RESTORED I = 000. Each value is the extracted I bit, not the whole status byte. FAILED CHECKS must be 000.

Run for at least 360 emulator frames so the 256-wait demonstration can finish. NMI DELTA 3 WAITS must be 003 even with IRQ masked; NMI DELTA 256 WAITS must be 000 because the byte wraps. NMI OFF DELTA must be 000. This program uses the default handler without runtime.c. Separate state checks cover 21 cases, including status-bit restoration and bounded waits with NMI disabled or an empty custom handler. KUROSAKI/NROM is tested; physical hardware is not.

__nmi_ready: NROM
NROM

Complete example program

Build command

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\interrupt_intrinsics.c -I .\kitaqfc\lib -I .\kitaq-docs\samples --nes-chr=.\kitaq-docs\samples\font.chr -o .\out\interrupt_intrinsics.nes --no-cache --no-disasm
Implementation and source

kitaqfc/lib/intrinsics.h:170

Declaration notes (original source)
Return the current wrapping NMI counter byte, not a Boolean ready flag.

kitaqfc/kitaqfc/CodeGenerator.cs:4740

csharp
case "__nmi_ready": if (!Need(0)) return true; EmitAsm("LDA", Mem(_runtimeNmiCounterAddress)); return true;
                // Reset OAMADDR and transfer page 02; the page-selecting variant evaluates its own byte argument.

// The default NMI executes the queued VRAM work and increments the runtime frame counter before RTI.
            if (!_functions.ContainsKey("__nes_nmi"))
            {
                EmitPlacement(0, true);
                _assembly.Add(Expr.Make(Tag.Function, "__kq_nmi_default"));
                // NMI may interrupt argument evaluation, a pointer dispatch or an ordinary expression.
                // Preserve registers and every shared byte touched by the queue executor; RTI restores P.
                EmitAsm("PHA"); EmitAsm("TXA"); EmitAsm("PHA"); EmitAsm("TYA"); EmitAsm("PHA");
                int[] queueScratch = { CallArgBase, CallArgBase + 1, CallArgBase + 2,
                    _runtimeIntrinsicTmp0Address, _runtimeIntrinsicTmp1Address, _runtimeIntrinsicTmp2Address };
                foreach (int address in queueScratch) { EmitAsm("LDA", Mem(address)); EmitAsm("PHA"); }
                // The optional library hook preserves its own registers and uses no
                // compiler scratch. It consumes only RAM records and fixed ROM tables.
                if (_functions.ContainsKey("__nes_audio_vblank_tick"))
                    EmitAsm("JSR", Abs("__nes_audio_vblank_tick"));
                EmitAsm("JSR", Abs("__vramq_exec"));
                EmitAsm("INC", Mem(_runtimeNmiCounterAddress));
                foreach (int address in queueScratch.Reverse()) { EmitAsm("PLA"); EmitAsm("STA", Mem(address)); }
                EmitAsm("PLA"); EmitAsm("TAY"); EmitAsm("PLA"); EmitAsm("TAX"); EmitAsm("PLA");
                EmitAsm("RTI");
            }
__nmi_wait

Read the compiler's NMI counter on entry and busy-wait until its value changes. With the default NMI handler, this waits for a subsequent handler completion, including execution of committed VRAM-queue work. The call does not enable NMI, commit queued commands or impose a timeout.

Syntax

c
void __nmi_wait(void);

Return value

None (void).

Conditions and limits

This counter belongs to the compiler's default NMI handler. Defining __nes_nmi replaces that handler. runtime.c uses its own nes_nmi_counter and nes_wait_nmi. An empty custom handler leaves the compiler counter unchanged, so __nmi_wait never returns. Enable NMI and call the wait outside the NMI handler.

NESdev: NMI

NESdev: CPU interrupts

Usage example

c
__nmi_wait();__nmi_wait();__nmi_wait();
delta=(u8)(__nmi_ready()-before_count); // Three completed default NMIs.

This is a calling fragment. Follow the stated initialization and buffer requirements before using it in a complete program.

Expected result

The program shows how the CPU IRQ mask differs from PPU NMI control. On the black screen, blue text must show IRQ DISABLED I = 001, IRQ ENABLED I = 000, SAVED SECTION I = 001 and RESTORED I = 000. Each value is the extracted I bit, not the whole status byte. FAILED CHECKS must be 000.

Run for at least 360 emulator frames so the 256-wait demonstration can finish. NMI DELTA 3 WAITS must be 003 even with IRQ masked; NMI DELTA 256 WAITS must be 000 because the byte wraps. NMI OFF DELTA must be 000. This program uses the default handler without runtime.c. Separate state checks cover 21 cases, including status-bit restoration and bounded waits with NMI disabled or an empty custom handler. KUROSAKI/NROM is tested; physical hardware is not.

__nmi_wait: NROM
NROM

Complete example program

Build command

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\interrupt_intrinsics.c -I .\kitaqfc\lib -I .\kitaq-docs\samples --nes-chr=.\kitaq-docs\samples\font.chr -o .\out\interrupt_intrinsics.nes --no-cache --no-disasm
Implementation and source

kitaqfc/lib/intrinsics.h:168

Declaration notes (original source)
NMI / IRQ
Spin until the runtime NMI counter changes; the configured handler must advance it. No timeout is emitted.

kitaqfc/kitaqfc/CodeGenerator.cs:4738

csharp
case "__nmi_wait": if (!Need(0)) return true; EmitAsm("JSR", Abs("__nmi_wait")); return true;
                // Return the current NMI counter byte; callers decide how to compare it with their previous value.

void EmitHelperNmiWait()
        {
            EmitHelperStart("__nmi_wait");
            EmitAsm("LDA", Mem(_runtimeNmiCounterAddress));
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp0Address));
            string loop = NewGeneratedLabel("nmi_wait_loop");
            _assembly.Add(Expr.Make(Tag.Label, loop));
            EmitAsm("LDA", Mem(_runtimeNmiCounterAddress));
            EmitAsm("CMP", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("BEQ", Rel(loop));
            EmitAsm("RTS");
        }

// The default NMI executes the queued VRAM work and increments the runtime frame counter before RTI.
            if (!_functions.ContainsKey("__nes_nmi"))
            {
                EmitPlacement(0, true);
                _assembly.Add(Expr.Make(Tag.Function, "__kq_nmi_default"));
                // NMI may interrupt argument evaluation, a pointer dispatch or an ordinary expression.
                // Preserve registers and every shared byte touched by the queue executor; RTI restores P.
                EmitAsm("PHA"); EmitAsm("TXA"); EmitAsm("PHA"); EmitAsm("TYA"); EmitAsm("PHA");
                int[] queueScratch = { CallArgBase, CallArgBase + 1, CallArgBase + 2,
                    _runtimeIntrinsicTmp0Address, _runtimeIntrinsicTmp1Address, _runtimeIntrinsicTmp2Address };
                foreach (int address in queueScratch) { EmitAsm("LDA", Mem(address)); EmitAsm("PHA"); }
                // The optional library hook preserves its own registers and uses no
                // compiler scratch. It consumes only RAM records and fixed ROM tables.
                if (_functions.ContainsKey("__nes_audio_vblank_tick"))
                    EmitAsm("JSR", Abs("__nes_audio_vblank_tick"));
                EmitAsm("JSR", Abs("__vramq_exec"));
                EmitAsm("INC", Mem(_runtimeNmiCounterAddress));
                foreach (int address in queueScratch.Reverse()) { EmitAsm("PLA"); EmitAsm("STA", Mem(address)); }
                EmitAsm("PLA"); EmitAsm("TAY"); EmitAsm("PLA"); EmitAsm("TAX"); EmitAsm("PLA");
                EmitAsm("RTI");
            }
__oam_clear

Hide all 64 sprites in the page-02 shadow buffer by setting each Y byte to 0xF0. Tile numbers, attributes and X coordinates remain unchanged.

Syntax

c
void __oam_clear(void);

Return value

None (void).

Conditions and limits

These operations edit CPU RAM at 0x0200–0x02FF; they do not immediately change hardware OAM. Transfer the completed page with __oam_dma. They do not maintain the sprite library’s allocation flags, coordinate arrays or active count. Keep interrupt handlers from editing or transferring a partially prepared page.

Usage example

c
__sprite_set(63,144,111,128,0); // Seed the CLEAR row with a visible entry.
__oam_clear(); // Set every Y byte to 240; tile, attributes and X are retained.

This is a calling fragment. Follow the stated initialization and buffer requirements before using it in a complete program.

Expected result

The example separates each operation into a labeled row. SET and MOVE show 8×8 right arrows at (144,16) and (144,32); TILE shows a green square at (144,48); ATTR shows the arrow flipped both ways at (144,64). HIDE at (144,80) and CLEAR at (144,112) must stay empty. META shows a pair at (136,96) and (144,96), with the second arrow mirrored horizontally. NEXT 07 is the next write position after writing slots 5 and 6. Arrows have red edges, green shafts and blue tip details. Verification compares 8,704 pixels per image and checks the transferred OAM bytes; physical hardware is untested.

TRANSFER shows an arrow at (144,128). This program sends page 02 directly with __oam_dma, including every field prepared by the individual sprite operations.

__oam_clear: PAGE02
PAGE02

Complete example program

Build command

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\oam_intrinsics.c -I .\kitaqfc\lib -I .\kitaq-docs\samples --nes-chr=.\kitaq-docs\samples\sprite_example.chr -o .\out\oam_intrinsics.nes --no-cache --no-disasm
Implementation and source

kitaqfc/lib/intrinsics.h:190

Declaration notes (original source)
Hide all 64 page-02 shadow sprites by writing Y=F0; hardware OAM changes only after DMA.
csharp
EmitHelperStart("__oam_clear");
            EmitAsm("LDX", Imm(0));
            EmitAsm("LDA", Imm(0xF0));
            string loop = NewGeneratedLabel("oam_clear_loop");
            _assembly.Add(Expr.Make(Tag.Label, loop));
            EmitAsm("STA", new AsmOperand(0x0200, AddressMode.AbsoluteX));
            EmitAsm("INX"); EmitAsm("INX"); EmitAsm("INX"); EmitAsm("INX");
            EmitAsm("BNE", Rel(loop));
            EmitAsm("RTS");
__oam_dma

Transfer the complete 256-byte shadow page at 0x0200–0x02FF to hardware OAM, starting at OAM entry 0. The CPU pauses during the transfer and resumes after it finishes.

Syntax

c
void __oam_dma(void);

Return value

None (void).

Conditions and limits

Call during VBlank or with rendering disabled; neither DMA intrinsic waits for VBlank. Both set OAMADDR to zero before transfer. Hardware DMA normally stalls the CPU for 513 or 514 cycles; DMC activity can extend it. KUROSAKI approximates this as 513 cycles, so its image verification does not establish exact hardware timing. KQ2421 can still be emitted after an explicit VBlank wait because the compiler classifies the caller by name.

Usage example

c
m_wait();__oam_dma(); // Transfer all 256 bytes from page 02 at VBlank.

This is a calling fragment. Follow the stated initialization and buffer requirements before using it in a complete program.

Expected result

The example separates each operation into a labeled row. SET and MOVE show 8×8 right arrows at (144,16) and (144,32); TILE shows a green square at (144,48); ATTR shows the arrow flipped both ways at (144,64). HIDE at (144,80) and CLEAR at (144,112) must stay empty. META shows a pair at (136,96) and (144,96), with the second arrow mirrored horizontally. NEXT 07 is the next write position after writing slots 5 and 6. Arrows have red edges, green shafts and blue tip details. Verification compares 8,704 pixels per image and checks the transferred OAM bytes; physical hardware is untested.

TRANSFER shows an arrow at (144,128). This program sends page 02 directly with __oam_dma, including every field prepared by the individual sprite operations.

__oam_dma: PAGE02
PAGE02

Complete example program

Build command

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\oam_intrinsics.c -I .\kitaqfc\lib -I .\kitaq-docs\samples --nes-chr=.\kitaq-docs\samples\sprite_example.chr -o .\out\oam_intrinsics.nes --no-cache --no-disasm
Implementation and source

kitaqfc/lib/intrinsics.h:186

Declaration notes (original source)
OAM / input
Set OAMADDR to zero and start DMA from CPU page 02; arrange safe PPU timing in the caller.
csharp
case "__oam_dma": if (!Need(0)) return true; EmitAsm("LDA", Imm(0)); EmitAsm("STA", Mem(0x2003)); EmitAsm("LDA", Imm(0x02)); EmitAsm("STA", Mem(0x4014)); return true;
__oam_dma_page

Transfer 256 bytes from the selected CPU memory page to hardware OAM, starting at entry 0. This permits a separate prepared OAM page without copying it into page 02 first. The selected source is not modified.

Syntax

c
void __oam_dma_page(u8 page);

Parameters

page

High byte of the source CPU address: pass 4 for 0x0400–0x04FF, not 0x0400. Supply a complete readable 256-byte page and keep any required bank mapped. Avoid pages containing I/O registers, whose reads can have side effects. No source validation is performed.

Return value

None (void).

Conditions and limits

Call during VBlank or with rendering disabled; neither DMA intrinsic waits for VBlank. Both set OAMADDR to zero before transfer. Hardware DMA normally stalls the CPU for 513 or 514 cycles; DMC activity can extend it. KUROSAKI approximates this as 513 cycles, so its image verification does not establish exact hardware timing. KQ2421 can still be emitted after an explicit VBlank wait because the compiler classifies the caller by name.

Usage example

c
m_wait();__oam_dma_page(4); // The argument is a page number, not 0x0400.

This is a calling fragment. Follow the stated initialization and buffer requirements before using it in a complete program.

Expected result

The example separates each operation into a labeled row. SET and MOVE show 8×8 right arrows at (144,16) and (144,32); TILE shows a green square at (144,48); ATTR shows the arrow flipped both ways at (144,64). HIDE at (144,80) and CLEAR at (144,112) must stay empty. META shows a pair at (136,96) and (144,96), with the second arrow mirrored horizontally. NEXT 07 is the next write position after writing slots 5 and 6. Arrows have red edges, green shafts and blue tip details. Verification compares 8,704 pixels per image and checks the transferred OAM bytes; physical hardware is untested.

This variant copies the prepared page 02 into a separate array at 0x0400, then changes only entry 7’s tile to 129. __oam_dma_page(4) must show a green square at (144,128) beside TRANSFER, while page 02 still contains the arrow’s tile 128. All other rows match the default-page example. The check compares all 256 hardware OAM bytes with page 04 and confirms that page 02 was not changed.

__oam_dma_page: PAGE04
PAGE04

Complete example program

Build command

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\oam_page.c -I .\kitaqfc\lib -I .\kitaq-docs\samples --nes-chr=.\kitaq-docs\samples\sprite_example.chr -o .\out\oam_page.nes --no-cache --no-disasm
Implementation and source

kitaqfc/lib/intrinsics.h:188

Declaration notes (original source)
Set OAMADDR to zero and DMA 256 bytes from page<<8. The source page must remain readable.
csharp
case "__oam_dma_page": if (!Need(1)) return true; EmitAsm("LDA", Imm(0)); EmitAsm("STA", Mem(0x2003)); Load1(args[0]); EmitAsm("STA", Mem(0x4014)); return true;
__pad_buttons

Keep bits 0–3 of the supplied NES pad snapshot (pad & 0x0F): A, B, Select and Start. This only filters a value already read; it does not access controller hardware.

Syntax

c
u8 __pad_buttons(u8 pad);

Parameters

pad

An already sampled 8-bit NES pad value, such as the result of __pad_read1_safe(). Supply the value itself, not a pointer, a port number or an input_* logical mask.

Return value

A mask from 0 to 15 in the bit order described above. More than one bit can be set; test a button with result & bit, rather than comparing the whole result to 1.

Conditions and limits

NES raw layout: A=0x01, B=0x02, SELECT=0x04, START=0x08, UP=0x10, DOWN=0x20, LEFT=0x40, RIGHT=0x80. A set bit means pressed. Use these values with nes_pad_* and raw __pad_* results, not the shared BTN_* masks from input.h.

Usage example

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

This is a calling fragment. Follow the stated initialization and buffer requirements before using it in a complete program.

Expected result

This program compares both controller ports, ordinary and repeated reads, mask filters and the shorthand macros. Hold A + Right on controller 1 and B + Left on controller 2. RAW P1, SAFE P1 and ALIAS P1 are 129; the three P2 values are 66. BUTTONS P1 is 1 and DIRS P1 is 128: filtering directions preserves their high-bit positions. FAILED CHECKS must be 0. The captured values remain on screen.

__pad_buttons: NROM
NROM

Complete example program

Build command

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\input_raw.c -I .\kitaqfc\lib -I .\kitaq-docs\samples -o .\out\input_raw.nes --mapper=nrom --nes-chr=.\kitaq-docs\samples\font.chr --no-disasm --no-cache
Implementation and source

kitaqfc/lib/intrinsics.h:213

Declaration notes (original source)
Return pad & 0F for A/B/Select/Start; this masks a supplied snapshot and performs no polling.
__pad_dirs

Keep bits 4–7 of the supplied NES pad snapshot (pad & 0xF0): Up, Down, Left and Right. The bits stay in their original positions; Right alone returns 128, not 8. No controller read is performed.

Syntax

c
u8 __pad_dirs(u8 pad);

Parameters

pad

An already sampled 8-bit NES pad value, such as the result of __pad_read1_safe(). Supply the value itself, not a pointer, a port number or an input_* logical mask.

Return value

The supplied value AND 0xF0: 0, 16, 32, …, 240. Direction bits remain in bits 4–7.

Conditions and limits

NES raw layout: A=0x01, B=0x02, SELECT=0x04, START=0x08, UP=0x10, DOWN=0x20, LEFT=0x40, RIGHT=0x80. A set bit means pressed. Use these values with nes_pad_* and raw __pad_* results, not the shared BTN_* masks from input.h.

Usage example

c
directions=__pad_dirs(raw1); // High nibble retained in place: 128, not 8.

This is a calling fragment. Follow the stated initialization and buffer requirements before using it in a complete program.

Expected result

This program compares both controller ports, ordinary and repeated reads, mask filters and the shorthand macros. Hold A + Right on controller 1 and B + Left on controller 2. RAW P1, SAFE P1 and ALIAS P1 are 129; the three P2 values are 66. BUTTONS P1 is 1 and DIRS P1 is 128: filtering directions preserves their high-bit positions. FAILED CHECKS must be 0. The captured values remain on screen.

__pad_dirs: NROM
NROM

Complete example program

Build command

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\input_raw.c -I .\kitaqfc\lib -I .\kitaq-docs\samples -o .\out\input_raw.nes --mapper=nrom --nes-chr=.\kitaq-docs\samples\font.chr --no-disasm --no-cache
Implementation and source

kitaqfc/lib/intrinsics.h:215

Declaration notes (original source)
Return pad & F0 in the original bit positions; this does not normalize directions to bits 0..3.
__pad_read1

Latch both controllers with a 1-to-0 write to $4016, then read the eight D0 serial bits from controller 1 at $4016. Return one NES-format held-button snapshot without retrying or calculating edges.

Syntax

c
u8 __pad_read1(void);

Return value

An 8-bit held-button mask in NES raw order. Each set bit identifies a pressed button; 0 means none. It is not a normalized Boolean.

Conditions and limits

NES raw layout: A=0x01, B=0x02, SELECT=0x04, START=0x08, UP=0x10, DOWN=0x20, LEFT=0x40, RIGHT=0x80. A set bit means pressed. Use these values with nes_pad_* and raw __pad_* results, not the shared BTN_* masks from input.h.

This call reads a fresh hardware snapshot; it does not update input_* or nes_pad1_* cached state and does not generate pressed/released edges or repeats. Read once, save the result, and derive edges against your saved previous mask when needed.

Usage example

c
raw1=__pad_read1(); // Controller 1: A (1) | Right (128) = 129.

This is a calling fragment. Follow the stated initialization and buffer requirements before using it in a complete program.

Expected result

This program compares both controller ports, ordinary and repeated reads, mask filters and the shorthand macros. Hold A + Right on controller 1 and B + Left on controller 2. RAW P1, SAFE P1 and ALIAS P1 are 129; the three P2 values are 66. BUTTONS P1 is 1 and DIRS P1 is 128: filtering directions preserves their high-bit positions. FAILED CHECKS must be 0. The captured values remain on screen.

__pad_read1: NROM
NROM

Complete example program

Build command

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\input_raw.c -I .\kitaqfc\lib -I .\kitaq-docs\samples -o .\out\input_raw.nes --mapper=nrom --nes-chr=.\kitaq-docs\samples\font.chr --no-disasm --no-cache
Implementation and source

kitaqfc/lib/intrinsics.h:205

Declaration notes (original source)
Latch and sample eight D0 bits from port 1; bit order is A,B,Select,Start,Up,Down,Left,Right.
__pad_read1_d1

Read eight serial bits from expansion input D1 at $4016. Write 1 then 0 to $4016 to latch the controller streams, sample bit 1 eight times, and place successive samples into result bits 0–7. This is one read of the D1 channel for port 1; it does not read the ordinary D0 controller channel or retry for matching samples.

Syntax

c
u8 __pad_read1_d1(void);

Return value

An 8-bit held-button mask in NES raw order. Each set bit identifies a pressed button; 0 means none. It is not a normalized Boolean.

Conditions and limits

NES raw layout: A=0x01, B=0x02, SELECT=0x04, START=0x08, UP=0x10, DOWN=0x20, LEFT=0x40, RIGHT=0x80. A set bit means pressed. Use these values with nes_pad_* and raw __pad_* results, not the shared BTN_* masks from input.h.

Use a peripheral that supplies a compatible eight-bit serial pad stream on the selected D1 line. The function does not detect the attached device or implement every expansion-device protocol. Each call strobes both controller streams; finish a complete read before starting another. Results are fresh snapshots and do not update the input_* or nes_pad1_* caches.

The published KUROSAKI controller model returns only D0 plus fixed bit 6. D1 and the microphone bit are therefore always 0 in that emulator, even while ordinary controller buttons are held. The supplied run verifies compilation, ordinary-button input and this zero-signal observation. Nonzero peripheral signals, electrical timing and real-device compatibility remain unverified; a zero here is not proof that an attached device works or that these functions cannot read it on hardware.

Usage example

c
d1_first=__pad_read1_d1();

This is a calling fragment. Follow the stated initialization and buffer requirements before using it in a complete program.

Expected result

This program contrasts ordinary D0 input with expansion D1 and the microphone's digital level. Hold A on controller 1 and B on controller 2 until the snapshot is captured. NORMAL P1 and NORMAL P2 display 001 and 002. In the supplied KUROSAKI run, D1 PORT 1, D1 PORT 2, EXP ALIAS 1, EXP ALIAS 2, MIC LEVEL and MIC DIRECT all display 000 because those signal lines are not emulated. The displayed values then stay fixed. With suitable peripherals and an implementation of their signal lines, the expansion and microphone values depend on the actual inputs; consult the bit layouts above rather than expecting these zeros.

The published KUROSAKI controller model returns only D0 plus fixed bit 6. D1 and the microphone bit are therefore always 0 in that emulator, even while ordinary controller buttons are held. The supplied run verifies compilation, ordinary-button input and this zero-signal observation. Nonzero peripheral signals, electrical timing and real-device compatibility remain unverified; a zero here is not proof that an attached device works or that these functions cannot read it on hardware.

__pad_read1_d1: NROM
NROM

Complete example program

Build command

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\expansion_input.c -I .\kitaq-docs\samples -I .\kitaqfc\lib -o .\out\expansion_input.nes --mapper=nrom --nes-chr=.\kitaq-docs\samples\font.chr --no-cache --no-disasm
Implementation and source

kitaqfc/lib/intrinsics.h:223

Declaration notes (original source)
Famicom/NES expansion input devices.
D1 readers are for expansion-port controllers/joysticks that expose a
standard 8-bit joypad stream on data line D1 instead of D0.
__joypad2p_voice() / __mic_read2p() return the Famicom 2P microphone bit.

Latch and read an eight-button stream from port-1 data line D1 rather than D0.
__pad_read1_safe

Sample controller 1 using repeated pairs of complete __pad_read1() calls. Return the NES-format mask when both reads in a pair agree. This returns a held snapshot, not new-press events.

Syntax

c
u8 __pad_read1_safe(void);

Return value

An 8-bit held-button mask in NES raw order. Each set bit identifies a pressed button; 0 means none. It is not a normalized Boolean.

Conditions and limits

NES raw layout: A=0x01, B=0x02, SELECT=0x04, START=0x08, UP=0x10, DOWN=0x20, LEFT=0x40, RIGHT=0x80. A set bit means pressed. Use these values with nes_pad_* and raw __pad_* results, not the shared BTN_* masks from input.h.

This call reads a fresh hardware snapshot; it does not update input_* or nes_pad1_* cached state and does not generate pressed/released edges or repeats. Read once, save the result, and derive edges against your saved previous mask when needed.

The safe reader performs pairs of complete reads until the two masks match. Each read strobes $4016, restarting both controllers' serial streams. There is no retry limit. This consistency check does not establish compatibility with every DMC/peripheral combination.

Usage example

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

This is a calling fragment. Follow the stated initialization and buffer requirements before using it in a complete program.

Expected result

This program compares both controller ports, ordinary and repeated reads, mask filters and the shorthand macros. Hold A + Right on controller 1 and B + Left on controller 2. RAW P1, SAFE P1 and ALIAS P1 are 129; the three P2 values are 66. BUTTONS P1 is 1 and DIRS P1 is 128: filtering directions preserves their high-bit positions. FAILED CHECKS must be 0. The captured values remain on screen.

__pad_read1_safe: NROM
NROM

Complete example program

Build command

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\input_raw.c -I .\kitaqfc\lib -I .\kitaq-docs\samples -o .\out\input_raw.nes --mapper=nrom --nes-chr=.\kitaq-docs\samples\font.chr --no-disasm --no-cache
Implementation and source

kitaqfc/lib/intrinsics.h:209

Declaration notes (original source)
Repeat pairs of port-1 polls until both bytes match; the retry loop has no fixed limit.
__pad_read2

Latch both controllers with a 1-to-0 write to $4016, then read the eight D0 serial bits from controller 2 at $4017. Return one NES-format held-button snapshot without retrying or calculating edges.

Syntax

c
u8 __pad_read2(void);

Return value

An 8-bit held-button mask in NES raw order. Each set bit identifies a pressed button; 0 means none. It is not a normalized Boolean.

Conditions and limits

NES raw layout: A=0x01, B=0x02, SELECT=0x04, START=0x08, UP=0x10, DOWN=0x20, LEFT=0x40, RIGHT=0x80. A set bit means pressed. Use these values with nes_pad_* and raw __pad_* results, not the shared BTN_* masks from input.h.

This call reads a fresh hardware snapshot; it does not update input_* or nes_pad1_* cached state and does not generate pressed/released edges or repeats. Read once, save the result, and derive edges against your saved previous mask when needed.

Usage example

c
raw2=__pad_read2(); // Controller 2: B (2) | Left (64) = 66.

This is a calling fragment. Follow the stated initialization and buffer requirements before using it in a complete program.

Expected result

This program compares both controller ports, ordinary and repeated reads, mask filters and the shorthand macros. Hold A + Right on controller 1 and B + Left on controller 2. RAW P1, SAFE P1 and ALIAS P1 are 129; the three P2 values are 66. BUTTONS P1 is 1 and DIRS P1 is 128: filtering directions preserves their high-bit positions. FAILED CHECKS must be 0. The captured values remain on screen.

__pad_read2: NROM
NROM

Complete example program

Build command

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\input_raw.c -I .\kitaqfc\lib -I .\kitaq-docs\samples -o .\out\input_raw.nes --mapper=nrom --nes-chr=.\kitaq-docs\samples\font.chr --no-disasm --no-cache
Implementation and source

kitaqfc/lib/intrinsics.h:207

Declaration notes (original source)
Latch and sample eight D0 bits from port 2 in the same NES button order.
__pad_read2_d1

Read eight serial bits from expansion input D1 at $4017. Write 1 then 0 to $4016 to latch the controller streams, sample bit 1 eight times, and place successive samples into result bits 0–7. This is one read of the D1 channel for port 2; it does not read the ordinary D0 controller channel or retry for matching samples.

Syntax

c
u8 __pad_read2_d1(void);

Return value

An 8-bit held-button mask in NES raw order. Each set bit identifies a pressed button; 0 means none. It is not a normalized Boolean.

Conditions and limits

NES raw layout: A=0x01, B=0x02, SELECT=0x04, START=0x08, UP=0x10, DOWN=0x20, LEFT=0x40, RIGHT=0x80. A set bit means pressed. Use these values with nes_pad_* and raw __pad_* results, not the shared BTN_* masks from input.h.

Use a peripheral that supplies a compatible eight-bit serial pad stream on the selected D1 line. The function does not detect the attached device or implement every expansion-device protocol. Each call strobes both controller streams; finish a complete read before starting another. Results are fresh snapshots and do not update the input_* or nes_pad1_* caches.

The published KUROSAKI controller model returns only D0 plus fixed bit 6. D1 and the microphone bit are therefore always 0 in that emulator, even while ordinary controller buttons are held. The supplied run verifies compilation, ordinary-button input and this zero-signal observation. Nonzero peripheral signals, electrical timing and real-device compatibility remain unverified; a zero here is not proof that an attached device works or that these functions cannot read it on hardware.

Usage example

c
d1_second=__pad_read2_d1();

This is a calling fragment. Follow the stated initialization and buffer requirements before using it in a complete program.

Expected result

This program contrasts ordinary D0 input with expansion D1 and the microphone's digital level. Hold A on controller 1 and B on controller 2 until the snapshot is captured. NORMAL P1 and NORMAL P2 display 001 and 002. In the supplied KUROSAKI run, D1 PORT 1, D1 PORT 2, EXP ALIAS 1, EXP ALIAS 2, MIC LEVEL and MIC DIRECT all display 000 because those signal lines are not emulated. The displayed values then stay fixed. With suitable peripherals and an implementation of their signal lines, the expansion and microphone values depend on the actual inputs; consult the bit layouts above rather than expecting these zeros.

The published KUROSAKI controller model returns only D0 plus fixed bit 6. D1 and the microphone bit are therefore always 0 in that emulator, even while ordinary controller buttons are held. The supplied run verifies compilation, ordinary-button input and this zero-signal observation. Nonzero peripheral signals, electrical timing and real-device compatibility remain unverified; a zero here is not proof that an attached device works or that these functions cannot read it on hardware.

__pad_read2_d1: NROM
NROM

Complete example program

Build command

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\expansion_input.c -I .\kitaq-docs\samples -I .\kitaqfc\lib -o .\out\expansion_input.nes --mapper=nrom --nes-chr=.\kitaq-docs\samples\font.chr --no-cache --no-disasm
Implementation and source

kitaqfc/lib/intrinsics.h:225

Declaration notes (original source)
Latch and read an eight-button stream from port-2 data line D1 rather than D0.
__pad_read2_safe

Sample controller 2 using repeated pairs of complete __pad_read2() calls. Return the NES-format mask when both reads in a pair agree. This returns a held snapshot, not new-press events.

Syntax

c
u8 __pad_read2_safe(void);

Return value

An 8-bit held-button mask in NES raw order. Each set bit identifies a pressed button; 0 means none. It is not a normalized Boolean.

Conditions and limits

NES raw layout: A=0x01, B=0x02, SELECT=0x04, START=0x08, UP=0x10, DOWN=0x20, LEFT=0x40, RIGHT=0x80. A set bit means pressed. Use these values with nes_pad_* and raw __pad_* results, not the shared BTN_* masks from input.h.

This call reads a fresh hardware snapshot; it does not update input_* or nes_pad1_* cached state and does not generate pressed/released edges or repeats. Read once, save the result, and derive edges against your saved previous mask when needed.

The safe reader performs pairs of complete reads until the two masks match. Each read strobes $4016, restarting both controllers' serial streams. There is no retry limit. This consistency check does not establish compatibility with every DMC/peripheral combination.

Usage example

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

This is a calling fragment. Follow the stated initialization and buffer requirements before using it in a complete program.

Expected result

This program compares both controller ports, ordinary and repeated reads, mask filters and the shorthand macros. Hold A + Right on controller 1 and B + Left on controller 2. RAW P1, SAFE P1 and ALIAS P1 are 129; the three P2 values are 66. BUTTONS P1 is 1 and DIRS P1 is 128: filtering directions preserves their high-bit positions. FAILED CHECKS must be 0. The captured values remain on screen.

__pad_read2_safe: NROM
NROM

Complete example program

Build command

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\input_raw.c -I .\kitaqfc\lib -I .\kitaq-docs\samples -o .\out\input_raw.nes --mapper=nrom --nes-chr=.\kitaq-docs\samples\font.chr --no-disasm --no-cache
Implementation and source

kitaqfc/lib/intrinsics.h:211

Declaration notes (original source)
Repeat pairs of port-2 polls until both bytes match; this does not populate pad.c globals.
__palette_bg_load

Reset the PPU latch, set address 0x3F00 and write exactly 16 source bytes through PPUDATA for the four background palettes. This is an immediate transfer; it does not change nametable attribute selections or load tile graphics.

Syntax

c
void __palette_bg_load(const u8* pal16);

Parameters

pal16

CPU pointer to 16 readable NES palette-code bytes. These are palette codes, not RGB triplets. Keep the source bank mapped until the synchronous call returns; no bank switching or length check is performed. Use PPU increment 1 to reach the intended 16 palette addresses.

Return value

None (void).

Conditions and limits

PPU addresses 0x3F10/14/18/1C alias 0x3F00/04/08/0C. Loading the sprite palette therefore also writes those background entries. The compiler sends the supplied bytes unchanged; it does not validate color choices, merge palettes or preserve the aliased entries.

Direct PPU access requires rendering to be off or the entire operation to fit a safe VBlank interval. These functions do not wait, queue, mask NMI or restore scroll. Prevent an interrupt from changing the shared latch/address during the call. PPUCTRL bit 2 selects a destination step of 1 or 32 per data byte. The PPU handles mirroring and address wrapping; CHR-ROM cannot be written. Set the intended scroll state after access and before rendering.

NESdev: PPU registers

NESdev: PPU scrolling

Usage example

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

This is a calling fragment. Follow the stated initialization and buffer requirements before using it in a complete program.

Expected result

This program teaches register setup, direct tile/attribute writes, scrolling and separate background/sprite palette loads. The blue band at (0,16) measures 256×16 pixels: its first row repeats a solid 8×8 tile, a left-half 4×8 bar and an eight-row triangle; its second row contains only left-half bars. A solid green 64×32 rectangle starts at (16,48). A red 64×32 region at (144,48) contains an 8-by-4 grid of triangle tiles.

At y=96, blue markers at x=16, 24 and 32 are respectively a square, left-half bar and triangle; a red left-half bar is at x=80 and a green triangle at x=144. These are background tiles. At y=144, the separately colored sprites are a red 8×8 square at x=16, a blue left-half 4×8 bar at x=80 and a green eight-row triangle at x=144. Final scrolling is (0,0).

The image checks 16,384 pixels for the expected shapes and colors, including sprite colors. Separate state tests cover 31 cases: register replacement, status/latch effects, saved scroll coordinates, zero/255-byte transfers, increments 1/32, map edges, zero rectangle dimensions, table selectors, whole-byte attributes and both palette targets. Values of Y=255 are checked as register writes, not as correct scrolling artwork. Evidence is from KUROSAKI with NROM; real hardware is not tested.

__palette_bg_load: NROM
NROM

Complete example program

Build command

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\ppu_intrinsics.c -I .\kitaqfc\lib -I .\kitaq-docs\samples --nes-chr=.\kitaq-docs\samples\api-examples\fc\vram_shapes.chr -o .\out\ppu_intrinsics.nes --no-cache --no-disasm
Implementation and source

kitaqfc/lib/intrinsics.h:138

Declaration notes (original source)
Stream exactly 16 source bytes to 3F00. Arrange PPU access timing and increment one.

kitaqfc/kitaqfc/CodeGenerator.cs:4827

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

void EmitHelperPalette(string name, int ppuAddress)
        {
            EmitHelperStart(name);
            EmitAsm("LDA", Mem(0x2002));
            EmitAsm("LDA", Imm((ppuAddress >> 8) & 0xFF));
            EmitAsm("STA", Mem(0x2006));
            EmitAsm("LDA", Imm(ppuAddress & 0xFF));
            EmitAsm("STA", Mem(0x2006));
            EmitAsm("LDY", Imm(0));
            string loop = NewGeneratedLabel("pal_loop");
            _assembly.Add(Expr.Make(Tag.Label, loop));
            EmitAsm("LDA", IndY(CallArgBase));
            EmitAsm("STA", Mem(0x2007));
            EmitAsm("INY");
            EmitAsm("CPY", Imm(16));
            EmitAsm("BNE", Rel(loop));
            EmitAsm("RTS");
        }

EmitHelperPalette("__palette_bg_load", 0x3F00);
__palette_sp_load

Reset the PPU latch, set address 0x3F10 and write exactly 16 source bytes through PPUDATA for the four sprite palettes. Sprite attribute bits choose which palette is displayed. The call neither modifies OAM nor transfers sprite tile graphics.

Syntax

c
void __palette_sp_load(const u8* pal16);

Parameters

pal16

CPU pointer to 16 readable NES palette-code bytes. These are palette codes, not RGB triplets. Keep the source bank mapped until the synchronous call returns; no bank switching or length check is performed. Use PPU increment 1 to reach the intended 16 palette addresses.

Return value

None (void).

Conditions and limits

PPU addresses 0x3F10/14/18/1C alias 0x3F00/04/08/0C. Loading the sprite palette therefore also writes those background entries. The compiler sends the supplied bytes unchanged; it does not validate color choices, merge palettes or preserve the aliased entries.

Direct PPU access requires rendering to be off or the entire operation to fit a safe VBlank interval. These functions do not wait, queue, mask NMI or restore scroll. Prevent an interrupt from changing the shared latch/address during the call. PPUCTRL bit 2 selects a destination step of 1 or 32 per data byte. The PPU handles mirroring and address wrapping; CHR-ROM cannot be written. Set the intended scroll state after access and before rendering.

NESdev: PPU registers

NESdev: PPU scrolling

Usage example

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

This is a calling fragment. Follow the stated initialization and buffer requirements before using it in a complete program.

Expected result

This program teaches register setup, direct tile/attribute writes, scrolling and separate background/sprite palette loads. The blue band at (0,16) measures 256×16 pixels: its first row repeats a solid 8×8 tile, a left-half 4×8 bar and an eight-row triangle; its second row contains only left-half bars. A solid green 64×32 rectangle starts at (16,48). A red 64×32 region at (144,48) contains an 8-by-4 grid of triangle tiles.

At y=96, blue markers at x=16, 24 and 32 are respectively a square, left-half bar and triangle; a red left-half bar is at x=80 and a green triangle at x=144. These are background tiles. At y=144, the separately colored sprites are a red 8×8 square at x=16, a blue left-half 4×8 bar at x=80 and a green eight-row triangle at x=144. Final scrolling is (0,0).

The image checks 16,384 pixels for the expected shapes and colors, including sprite colors. Separate state tests cover 31 cases: register replacement, status/latch effects, saved scroll coordinates, zero/255-byte transfers, increments 1/32, map edges, zero rectangle dimensions, table selectors, whole-byte attributes and both palette targets. Values of Y=255 are checked as register writes, not as correct scrolling artwork. Evidence is from KUROSAKI with NROM; real hardware is not tested.

__palette_sp_load: NROM
NROM

Complete example program

Build command

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\ppu_intrinsics.c -I .\kitaqfc\lib -I .\kitaq-docs\samples --nes-chr=.\kitaq-docs\samples\api-examples\fc\vram_shapes.chr -o .\out\ppu_intrinsics.nes --no-cache --no-disasm
Implementation and source

kitaqfc/lib/intrinsics.h:140

Declaration notes (original source)
Stream exactly 16 source bytes to 3F10; hardware palette aliases also affect background entries.

kitaqfc/kitaqfc/CodeGenerator.cs:4828

csharp
case "__palette_sp_load": return EmitKitaqfcHelperCall(funcName, args, ctx, new[] {2}, funcName);
                // Clear queued length and readiness; the overflow indicator has a separate reset intrinsic.

void EmitHelperPalette(string name, int ppuAddress)
        {
            EmitHelperStart(name);
            EmitAsm("LDA", Mem(0x2002));
            EmitAsm("LDA", Imm((ppuAddress >> 8) & 0xFF));
            EmitAsm("STA", Mem(0x2006));
            EmitAsm("LDA", Imm(ppuAddress & 0xFF));
            EmitAsm("STA", Mem(0x2006));
            EmitAsm("LDY", Imm(0));
            string loop = NewGeneratedLabel("pal_loop");
            _assembly.Add(Expr.Make(Tag.Label, loop));
            EmitAsm("LDA", IndY(CallArgBase));
            EmitAsm("STA", Mem(0x2007));
            EmitAsm("INY");
            EmitAsm("CPY", Imm(16));
            EmitAsm("BNE", Rel(loop));
            EmitAsm("RTS");
        }

EmitHelperPalette("__palette_sp_load", 0x3F10);
__ppu_addr

Evaluate ppu_addr once, read PPUSTATUS to reset the shared write latch, then write the high and low address bytes to PPUADDR. This sets the destination for later PPUDATA access without writing any data itself. The PPU applies its address mask and mirroring.

Syntax

c
void __ppu_addr(u16 ppu_addr);

Parameters

ppu_addr

PPU byte address (0x0000–0x3FFF), not a CPU pointer. The target must be writable: pattern changes need CHR RAM, not CHR ROM. PPU mapping and mirroring apply. Select increment 1 in PPUCTRL for contiguous copies and fills. Execute during safe PPU timing, then restore the desired scroll state; the executor writes PPUADDR and PPUDATA without restoring scroll settings.

Return value

None (void).

Conditions and limits

Direct PPU access requires rendering to be off or the entire operation to fit a safe VBlank interval. These functions do not wait, queue, mask NMI or restore scroll. Prevent an interrupt from changing the shared latch/address during the call. PPUCTRL bit 2 selects a destination step of 1 or 32 per data byte. The PPU handles mirroring and address wrapping; CHR-ROM cannot be written. Set the intended scroll state after access and before rendering.

NESdev: PPU registers

NESdev: PPU scrolling

Usage example

c
address_port=0x21;
__ppu_addr(0x2192); // Reset latch and set the cell at tile (18,12).

This is a calling fragment. Follow the stated initialization and buffer requirements before using it in a complete program.

Expected result

This program teaches register setup, direct tile/attribute writes, scrolling and separate background/sprite palette loads. The blue band at (0,16) measures 256×16 pixels: its first row repeats a solid 8×8 tile, a left-half 4×8 bar and an eight-row triangle; its second row contains only left-half bars. A solid green 64×32 rectangle starts at (16,48). A red 64×32 region at (144,48) contains an 8-by-4 grid of triangle tiles.

At y=96, blue markers at x=16, 24 and 32 are respectively a square, left-half bar and triangle; a red left-half bar is at x=80 and a green triangle at x=144. These are background tiles. At y=144, the separately colored sprites are a red 8×8 square at x=16, a blue left-half 4×8 bar at x=80 and a green eight-row triangle at x=144. Final scrolling is (0,0).

The image checks 16,384 pixels for the expected shapes and colors, including sprite colors. Separate state tests cover 31 cases: register replacement, status/latch effects, saved scroll coordinates, zero/255-byte transfers, increments 1/32, map edges, zero rectangle dimensions, table selectors, whole-byte attributes and both palette targets. Values of Y=255 are checked as register writes, not as correct scrolling artwork. Evidence is from KUROSAKI with NROM; real hardware is not tested.

__ppu_addr: NROM
NROM

Complete example program

Build command

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\ppu_intrinsics.c -I .\kitaqfc\lib -I .\kitaq-docs\samples --nes-chr=.\kitaq-docs\samples\api-examples\fc\vram_shapes.chr -o .\out\ppu_intrinsics.nes --no-cache --no-disasm
Implementation and source

kitaqfc/lib/intrinsics.h:106

Declaration notes (original source)
Set the PPU address through the compiler helper; address writes affect the shared scroll/address latch.

kitaqfc/kitaqfc/CodeGenerator.cs:4698

csharp
case "__ppu_addr":
                    if (!Need(1)) return true;
                    PpuAddr(args[0]); return true;

void PpuAddr(Expr e)
            {
                int[] t = AcquireTemps(2);
                try
                {
                    EmitLoadValue(e, ctx, 2);
                    EmitAsm("STA", Mem(t[0]));
                    EmitAsm("STX", Mem(t[1]));
                    EmitAsm("LDA", Mem(0x2002));
                    EmitAsm("LDA", Mem(t[1]));
                    EmitAsm("STA", Mem(0x2006));
                    EmitAsm("LDA", Mem(t[0]));
                    EmitAsm("STA", Mem(0x2006));
                }
                finally { ReleaseTemps(t); }
            }
__ppu_ctrl_get

Return the software shadow of PPUCTRL. Use it to save NMI enable, pattern-table selection and address increment before a temporary change.

Syntax

c
u8 __ppu_ctrl_get(void);

Return value

Return the last control value maintained by the library or intrinsic setters as u8. This does not read PPUSTATUS.

Conditions and limits

PPUCTRL and PPUMASK are write-only. This getter does not read hardware. Direct writes to $2000/$2001 or custom assembly bypass the shadows. Use the corresponding __ppu_*_set operations consistently. Reading the shadow does not change the latch or interrupt state.

Usage example

c
__ppu_ctrl_set(4);
    if(__ppu_ctrl_get()!=4)failures=1; // Inspect the software shadow, not PPUSTATUS.
    __ppu_ctrl_set(0);

This is a calling fragment. Follow the stated initialization and buffer requirements before using it in a complete program.

Expected result

The four library calls turn rendering off, load both palettes, clear a full nametable and turn rendering on. The sample reads the palette and cleared table back before drawing. Expect blue copied squares, bars and triangles at y=16, green filled tiles at y=96, and red square/bar/triangle markers at y=208. Each large block uses 255 tiles, leaving the last cell black. FAILED CHECKS is 0.

__ppu_ctrl_get: ALTERNATIVES
ALTERNATIVES

Complete example program

Build command

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\ppu_implemented.c -I .\kitaqfc\lib -I .\kitaq-docs\samples -o .\out\ppu_implemented.nes --mapper=nrom --nes-chr=.\kitaq-docs\samples\api-examples\fc\vram_shapes.chr --no-cache --no-disasm
Implementation and source

kitaqfc/lib/intrinsics.h:103

Declaration notes (original source)
Read the software PPUCTRL/PPUMASK shadows; use the setters for coherent state.
Raw writes to $2000/$2001 bypass these shadows. No PPU hardware read occurs.

kitaqfc/kitaqfc/CodeGenerator.cs:4691

csharp
case "__ppu_ctrl_get":
                    if (!Need(0)) return true;
                    EmitAsm("LDA", Mem(_runtimePpuCtrlShadowAddress)); return true;
__ppu_ctrl_set

Replace PPUCTRL and its software shadow with the supplied byte. This controls NMI enable, PPU address increment, pattern-table choices, sprite size and base nametable selection. It neither changes PPUMASK nor preserves unmentioned bits.

Syntax

c
void __ppu_ctrl_set(u8 value);

Parameters

value

Unsigned byte 0..255, written as supplied. This is a complete register/data byte, not a Boolean or a bit number.

Return value

None (void).

NESdev: PPU registers

NESdev: PPU scrolling

Usage example

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

This is a calling fragment. Follow the stated initialization and buffer requirements before using it in a complete program.

Expected result

This program teaches register setup, direct tile/attribute writes, scrolling and separate background/sprite palette loads. The blue band at (0,16) measures 256×16 pixels: its first row repeats a solid 8×8 tile, a left-half 4×8 bar and an eight-row triangle; its second row contains only left-half bars. A solid green 64×32 rectangle starts at (16,48). A red 64×32 region at (144,48) contains an 8-by-4 grid of triangle tiles.

At y=96, blue markers at x=16, 24 and 32 are respectively a square, left-half bar and triangle; a red left-half bar is at x=80 and a green triangle at x=144. These are background tiles. At y=144, the separately colored sprites are a red 8×8 square at x=16, a blue left-half 4×8 bar at x=80 and a green eight-row triangle at x=144. Final scrolling is (0,0).

The image checks 16,384 pixels for the expected shapes and colors, including sprite colors. Separate state tests cover 31 cases: register replacement, status/latch effects, saved scroll coordinates, zero/255-byte transfers, increments 1/32, map edges, zero rectangle dimensions, table selectors, whole-byte attributes and both palette targets. Values of Y=255 are checked as register writes, not as correct scrolling artwork. Evidence is from KUROSAKI with NROM; real hardware is not tested.

__ppu_ctrl_set: NROM
NROM

Complete example program

Build command

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\ppu_intrinsics.c -I .\kitaqfc\lib -I .\kitaq-docs\samples --nes-chr=.\kitaq-docs\samples\api-examples\fc\vram_shapes.chr -o .\out\ppu_intrinsics.nes --no-cache --no-disasm
Implementation and source

kitaqfc/lib/intrinsics.h:100

Declaration notes (original source)
Replace PPUCTRL, including NMI enable, pattern-table choices and address increment mode.

kitaqfc/kitaqfc/CodeGenerator.cs:4687

csharp
case "__ppu_ctrl_set":
                    if (!Need(1)) return true;
                    Store1(args[0], _runtimePpuCtrlShadowAddress); EmitAsm("STA", Mem(0x2000)); return true;
                // Read software state: the corresponding hardware registers are write-only.
__ppu_data

Write value once to PPUDATA at the current PPU address. The PPU then advances that address by 1 or 32 according to PPUCTRL. This call does not set an address, reset a latch or enqueue a transfer.

Syntax

c
void __ppu_data(u8 value);

Parameters

value

Unsigned byte 0..255, written as supplied. This is a complete register/data byte, not a Boolean or a bit number.

Return value

None (void).

Conditions and limits

Direct PPU access requires rendering to be off or the entire operation to fit a safe VBlank interval. These functions do not wait, queue, mask NMI or restore scroll. Prevent an interrupt from changing the shared latch/address during the call. PPUCTRL bit 2 selects a destination step of 1 or 32 per data byte. The PPU handles mirroring and address wrapping; CHR-ROM cannot be written. Set the intended scroll state after access and before rendering.

NESdev: PPU registers

NESdev: PPU scrolling

Usage example

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

This is a calling fragment. Follow the stated initialization and buffer requirements before using it in a complete program.

Expected result

This program teaches register setup, direct tile/attribute writes, scrolling and separate background/sprite palette loads. The blue band at (0,16) measures 256×16 pixels: its first row repeats a solid 8×8 tile, a left-half 4×8 bar and an eight-row triangle; its second row contains only left-half bars. A solid green 64×32 rectangle starts at (16,48). A red 64×32 region at (144,48) contains an 8-by-4 grid of triangle tiles.

At y=96, blue markers at x=16, 24 and 32 are respectively a square, left-half bar and triangle; a red left-half bar is at x=80 and a green triangle at x=144. These are background tiles. At y=144, the separately colored sprites are a red 8×8 square at x=16, a blue left-half 4×8 bar at x=80 and a green eight-row triangle at x=144. Final scrolling is (0,0).

The image checks 16,384 pixels for the expected shapes and colors, including sprite colors. Separate state tests cover 31 cases: register replacement, status/latch effects, saved scroll coordinates, zero/255-byte transfers, increments 1/32, map edges, zero rectangle dimensions, table selectors, whole-byte attributes and both palette targets. Values of Y=255 are checked as register writes, not as correct scrolling artwork. Evidence is from KUROSAKI with NROM; real hardware is not tested.

__ppu_data: NROM
NROM

Complete example program

Build command

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\ppu_intrinsics.c -I .\kitaqfc\lib -I .\kitaq-docs\samples --nes-chr=.\kitaq-docs\samples\api-examples\fc\vram_shapes.chr -o .\out\ppu_intrinsics.nes --no-cache --no-disasm
Implementation and source

kitaqfc/lib/intrinsics.h:108

Declaration notes (original source)
Write one PPUDATA byte using the current PPU address and increment mode.

kitaqfc/kitaqfc/CodeGenerator.cs:4701

csharp
case "__ppu_data":
                    if (!Need(1)) return true;
                    Load1(args[0]); EmitAsm("STA", Mem(0x2007)); return true;
                // Reading PPUSTATUS also has hardware side effects; this is not a side-effect-free status cache.
__ppu_mask_get

Return the software shadow of PPUMASK. Use it to preserve rendering, left-edge visibility, grayscale and color-emphasis settings.

Syntax

c
u8 __ppu_mask_get(void);

Return value

Return the last control value maintained by the library or intrinsic setters as u8. This does not read PPUSTATUS.

Conditions and limits

PPUCTRL and PPUMASK are write-only. This getter does not read hardware. Direct writes to $2000/$2001 or custom assembly bypass the shadows. Use the corresponding __ppu_*_set operations consistently. Reading the shadow does not change the latch or interrupt state.

Usage example

c
__ppu_mask_set(0xE7);
    if(__ppu_mask_get()!=0xE7)failures=1;
    __ppu_mask_set(0);

This is a calling fragment. Follow the stated initialization and buffer requirements before using it in a complete program.

Expected result

The four library calls turn rendering off, load both palettes, clear a full nametable and turn rendering on. The sample reads the palette and cleared table back before drawing. Expect blue copied squares, bars and triangles at y=16, green filled tiles at y=96, and red square/bar/triangle markers at y=208. Each large block uses 255 tiles, leaving the last cell black. FAILED CHECKS is 0.

__ppu_mask_get: ALTERNATIVES
ALTERNATIVES

Complete example program

Build command

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\ppu_implemented.c -I .\kitaqfc\lib -I .\kitaq-docs\samples -o .\out\ppu_implemented.nes --mapper=nrom --nes-chr=.\kitaq-docs\samples\api-examples\fc\vram_shapes.chr --no-cache --no-disasm
Implementation and source

kitaqfc/lib/intrinsics.h:104

kitaqfc/kitaqfc/CodeGenerator.cs:4694

csharp
case "__ppu_mask_get":
                    if (!Need(0)) return true;
                    EmitAsm("LDA", Mem(_runtimePpuMaskShadowAddress)); return true;
                // Direct PPU address/data access does not wait for a safe rendering interval here.
__ppu_mask_set

Replace the entire PPUMASK byte and its software shadow with value. Select rendering, left-edge visibility, greyscale and emphasis explicitly; the function does not merge bits with the previous mask or change PPUCTRL.

Syntax

c
void __ppu_mask_set(u8 value);

Parameters

value

Unsigned byte 0..255, written as supplied. This is a complete register/data byte, not a Boolean or a bit number.

Return value

None (void).

NESdev: PPU registers

NESdev: PPU scrolling

Usage example

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

This is a calling fragment. Follow the stated initialization and buffer requirements before using it in a complete program.

Expected result

This program teaches register setup, direct tile/attribute writes, scrolling and separate background/sprite palette loads. The blue band at (0,16) measures 256×16 pixels: its first row repeats a solid 8×8 tile, a left-half 4×8 bar and an eight-row triangle; its second row contains only left-half bars. A solid green 64×32 rectangle starts at (16,48). A red 64×32 region at (144,48) contains an 8-by-4 grid of triangle tiles.

At y=96, blue markers at x=16, 24 and 32 are respectively a square, left-half bar and triangle; a red left-half bar is at x=80 and a green triangle at x=144. These are background tiles. At y=144, the separately colored sprites are a red 8×8 square at x=16, a blue left-half 4×8 bar at x=80 and a green eight-row triangle at x=144. Final scrolling is (0,0).

The image checks 16,384 pixels for the expected shapes and colors, including sprite colors. Separate state tests cover 31 cases: register replacement, status/latch effects, saved scroll coordinates, zero/255-byte transfers, increments 1/32, map edges, zero rectangle dimensions, table selectors, whole-byte attributes and both palette targets. Values of Y=255 are checked as register writes, not as correct scrolling artwork. Evidence is from KUROSAKI with NROM; real hardware is not tested.

__ppu_mask_set: NROM
NROM

Complete example program

Build command

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\ppu_intrinsics.c -I .\kitaqfc\lib -I .\kitaq-docs\samples --nes-chr=.\kitaq-docs\samples\api-examples\fc\vram_shapes.chr -o .\out\ppu_intrinsics.nes --no-cache --no-disasm
Implementation and source

kitaqfc/lib/intrinsics.h:98

Declaration notes (original source)
Replace PPUMASK with the supplied byte; the caller chooses rendering, clipping and emphasis bits.

kitaqfc/kitaqfc/CodeGenerator.cs:4683

csharp
case "__ppu_mask_set":
                    if (!Need(1)) return true;
                    Store1(args[0], _runtimePpuMaskShadowAddress); EmitAsm("STA", Mem(0x2001)); return true;
                // Update the PPUCTRL shadow before writing the hardware register.
__ppu_off

Write zero to PPUMASK and its software shadow, disabling background and sprite rendering. All mask bits are cleared, including greyscale and emphasis. PPUCTRL and NMI enable remain unchanged; use separate NMI control when preparing shared transfer state.

Syntax

c
void __ppu_off(void);

Return value

None (void).

NESdev: PPU registers

NESdev: PPU scrolling

Usage example

c
__ppu_off(); // PPUMASK and its software shadow become zero; NMI is unchanged.
__ppu_ctrl_set(0); // Disable NMI separately while preparing the whole scene.

This is a calling fragment. Follow the stated initialization and buffer requirements before using it in a complete program.

Expected result

This program teaches register setup, direct tile/attribute writes, scrolling and separate background/sprite palette loads. The blue band at (0,16) measures 256×16 pixels: its first row repeats a solid 8×8 tile, a left-half 4×8 bar and an eight-row triangle; its second row contains only left-half bars. A solid green 64×32 rectangle starts at (16,48). A red 64×32 region at (144,48) contains an 8-by-4 grid of triangle tiles.

At y=96, blue markers at x=16, 24 and 32 are respectively a square, left-half bar and triangle; a red left-half bar is at x=80 and a green triangle at x=144. These are background tiles. At y=144, the separately colored sprites are a red 8×8 square at x=16, a blue left-half 4×8 bar at x=80 and a green eight-row triangle at x=144. Final scrolling is (0,0).

The image checks 16,384 pixels for the expected shapes and colors, including sprite colors. Separate state tests cover 31 cases: register replacement, status/latch effects, saved scroll coordinates, zero/255-byte transfers, increments 1/32, map edges, zero rectangle dimensions, table selectors, whole-byte attributes and both palette targets. Values of Y=255 are checked as register writes, not as correct scrolling artwork. Evidence is from KUROSAKI with NROM; real hardware is not tested.

__ppu_off: NROM
NROM

Complete example program

Build command

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\ppu_intrinsics.c -I .\kitaqfc\lib -I .\kitaq-docs\samples --nes-chr=.\kitaq-docs\samples\api-examples\fc\vram_shapes.chr -o .\out\ppu_intrinsics.nes --no-cache --no-disasm
Implementation and source

kitaqfc/lib/intrinsics.h:96

Declaration notes (original source)
Invoke the compiler rendering-disable helper before bulk direct PPU updates.

kitaqfc/kitaqfc/CodeGenerator.cs:4679

csharp
case "__ppu_off":
                    if (!Need(0)) return true;
                    EmitAsm("LDA", Imm(0)); EmitAsm("STA", Mem(_runtimePpuMaskShadowAddress)); EmitAsm("STA", Mem(0x2001)); return true;
                // Keep the supplied mask available for later runtime code that restores PPU state.
__ppu_on

Replace PPUMASK and its software shadow with 0x1E, enabling background and sprite rendering, including the leftmost eight pixels. This overwrites greyscale/emphasis choices; it does not change PPUCTRL or enable NMI.

Syntax

c
void __ppu_on(void);

Return value

None (void).

NESdev: PPU registers

NESdev: PPU scrolling

Usage example

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

This is a calling fragment. Follow the stated initialization and buffer requirements before using it in a complete program.

Expected result

This program teaches register setup, direct tile/attribute writes, scrolling and separate background/sprite palette loads. The blue band at (0,16) measures 256×16 pixels: its first row repeats a solid 8×8 tile, a left-half 4×8 bar and an eight-row triangle; its second row contains only left-half bars. A solid green 64×32 rectangle starts at (16,48). A red 64×32 region at (144,48) contains an 8-by-4 grid of triangle tiles.

At y=96, blue markers at x=16, 24 and 32 are respectively a square, left-half bar and triangle; a red left-half bar is at x=80 and a green triangle at x=144. These are background tiles. At y=144, the separately colored sprites are a red 8×8 square at x=16, a blue left-half 4×8 bar at x=80 and a green eight-row triangle at x=144. Final scrolling is (0,0).

The image checks 16,384 pixels for the expected shapes and colors, including sprite colors. Separate state tests cover 31 cases: register replacement, status/latch effects, saved scroll coordinates, zero/255-byte transfers, increments 1/32, map edges, zero rectangle dimensions, table selectors, whole-byte attributes and both palette targets. Values of Y=255 are checked as register writes, not as correct scrolling artwork. Evidence is from KUROSAKI with NROM; real hardware is not tested.

__ppu_on: NROM
NROM

Complete example program

Build command

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\ppu_intrinsics.c -I .\kitaqfc\lib -I .\kitaq-docs\samples --nes-chr=.\kitaq-docs\samples\api-examples\fc\vram_shapes.chr -o .\out\ppu_intrinsics.nes --no-cache --no-disasm
Implementation and source

kitaqfc/lib/intrinsics.h:94

Declaration notes (original source)
PRG bank
Resolve a symbol or function placement to its byte bank number at compilation time.
u8 __bankof(u16 symbol_or_function);
Select the mapper-specific PRG bank; the common bank and legal bank range depend on the target board.
void __bankswitch(u8 bank);
Use the same mapper PRG-switch helper as __bankswitch; retain executable code in a valid mapping.
void __prg_bank_set(u8 bank);
Call a declared zero-argument function; placement selects its bank and its declared type determines the result.
u8 __farcall(u8 bank, u16 func);
Switch to the source bank, copy len bytes forward and restore the previous bank.
Keep destination RAM and helper code accessible throughout; overlapping copies are not memmove.
void __far_memcpy(u8* dst, u8 bank, u16 src, u16 len);
Temporarily select bank, read one byte at addr and restore the prior bank mapping.
u8 __farpeek8(u8 bank, u16 addr);
Temporarily select bank and read a little-endian word, then restore the previous mapping.
u16 __farpeek16(u8 bank, u16 addr);

 PPU / VRAM
Invoke the compiler rendering-enable helper; this header contains declarations, not C fallback implementations.

kitaqfc/kitaqfc/CodeGenerator.cs:4676

csharp
case "__ppu_on":
                    if (!Need(0)) return true;
                    EmitAsm("LDA", Imm(0x1E)); EmitAsm("STA", Mem(_runtimePpuMaskShadowAddress)); EmitAsm("STA", Mem(0x2001)); return true;
__ppu_read_status

Read PPUSTATUS once and return the byte observed before the read clears its VBlank flag. The read also resets the shared scroll/address write latch. This is a hardware status read with side effects, not a cached frame counter or an NMI-completion test.

Syntax

c
u8 __ppu_read_status(void);

Return value

Raw status byte. Bit 7 is VBlank, bit 6 sprite-zero hit and bit 5 sprite overflow. Bits 0..4 retain the low bits of the PPU I/O latch, so they need not be zero. Test VBlank with (status & 0x80) != 0; a nonzero whole byte does not mean VBlank. These low bits are not application status or a frame counter.

Conditions and limits

A PPUSTATUS read can affect NMI timing and the next writes to both PPUSCROLL and PPUADDR. Do not insert it into another routine's half-written register pair. These calls do not wait for VBlank or complete pending transfer queues.

NESdev: PPU registers

NESdev: PPU scrolling

Usage example

c
address_port=0x21;
status_snapshot=__ppu_read_status(); // Also resets the shared write latch.
address_port=0x21;address_port=0x84;data_port=3; // Blue triangle at (32,96).

This is a calling fragment. Follow the stated initialization and buffer requirements before using it in a complete program.

Expected result

This program teaches register setup, direct tile/attribute writes, scrolling and separate background/sprite palette loads. The blue band at (0,16) measures 256×16 pixels: its first row repeats a solid 8×8 tile, a left-half 4×8 bar and an eight-row triangle; its second row contains only left-half bars. A solid green 64×32 rectangle starts at (16,48). A red 64×32 region at (144,48) contains an 8-by-4 grid of triangle tiles.

At y=96, blue markers at x=16, 24 and 32 are respectively a square, left-half bar and triangle; a red left-half bar is at x=80 and a green triangle at x=144. These are background tiles. At y=144, the separately colored sprites are a red 8×8 square at x=16, a blue left-half 4×8 bar at x=80 and a green eight-row triangle at x=144. Final scrolling is (0,0).

The image checks 16,384 pixels for the expected shapes and colors, including sprite colors. Separate state tests cover 31 cases: register replacement, status/latch effects, saved scroll coordinates, zero/255-byte transfers, increments 1/32, map edges, zero rectangle dimensions, table selectors, whole-byte attributes and both palette targets. Values of Y=255 are checked as register writes, not as correct scrolling artwork. Evidence is from KUROSAKI with NROM; real hardware is not tested.

__ppu_read_status: NROM
NROM

Complete example program

Build command

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\ppu_intrinsics.c -I .\kitaqfc\lib -I .\kitaq-docs\samples --nes-chr=.\kitaq-docs\samples\api-examples\fc\vram_shapes.chr -o .\out\ppu_intrinsics.nes --no-cache --no-disasm
Implementation and source

kitaqfc/lib/intrinsics.h:110

Declaration notes (original source)
Read PPUSTATUS; the hardware clears VBlank status and resets the shared write latch.

kitaqfc/kitaqfc/CodeGenerator.cs:4705

csharp
case "__ppu_read_status":
                    if (!Need(0)) return true;
                    EmitAsm("LDA", Mem(0x2002)); return true;
__prg_bank_set

Use the same mapper-aware PRG-bank selection as __bankswitch. This alias has the same bank validation and does not restore the previous mapping automatically.

Syntax

c
void __prg_bank_set(u8 bank);

Parameters

bank

An unsigned 8-bit ROM-bank number. For a call, it must agree with the function's placement; for a read, it selects the source mapping. Use the target cartridge's valid range.

Return value

None (void).

Conditions and limits

A permanent bank change must not unmap the executing code or its return address. Put this call, bank_switch itself when used, and the code that continues afterwards in common/fixed bank 0, as the sample does.

The library record starts at zero and does not discover the cartridge's startup mapping. Initialize it with bank_switch. Direct intrinsics and raw mapper writes do not update this separate record; bank_get_current therefore cannot verify the actual hardware mapping.

FC switch-bank numbers are logical compiler numbers. Zero denotes common ROM, not a switch target. The helper normalizes zero to bank 1. On SUROM512, only 1–30 are valid: an out-of-range constant passed directly to the switching intrinsic is a build error; an invalid runtime value falls back to 1 and sets the mapper-fault state. Read common ROM directly instead of requesting a bank-zero switch.

Usage example

c
__prg_bank_set(1);bank_expect(home1[0]==0xA1,21);
__prg_bank_set(2); // Same mapper helper as __bankswitch.

This is a calling fragment. Follow the stated initialization and buffer requirements before using it in a complete program.

Expected result

The initial bank_switch(2) records and selects bank 2. Later, direct switching selects bank 1 and reads A1 from home1 while bank_get_current still reports 2; switching back makes home2 read B2 again.

The complete program teaches how a bank number and near address identify data, how callbacks cross banks, and how to distinguish the library's record from the actual mapping. All displayed numbers are hexadecimal. BANK IN and BANK OUT must show 02, BYTE HEX D3, WORD HEX 5C7A, and both FAILED CHECKS and FIRST FAILURE 00. CGB and FC display blue text; DMG displays black text.

CALL COUNT is 02: the far_call macro and __farcall each invoke the callback once.

__prg_bank_set: MMC3
MMC3

Complete example program

Build command

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

kitaqfc/lib/intrinsics.h:81

Declaration notes (original source)
Use the same mapper PRG-switch helper as __bankswitch; retain executable code in a valid mapping.

kitaqfc/kitaqfc/CodeGenerator.cs:4746

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

Advance the shared 16-bit FC Galois LFSR once and return the XOR of the updated low and high bytes. An LFSR is a shift register whose feedback is calculated with XOR; a fixed seed therefore produces a reproducible byte sequence.

Syntax

c
u8 __rng8(void);

Return value

One u8 result (0–255), after advancing the shared generator once. This describes the value's type and bounds, not equal likelihood for every byte.

Conditions and limits

The FC step shifts the 16-bit state right by one. If the original least significant bit was 1, it XORs the shifted state with 0xB400. The returned byte is low_byte XOR high_byte, not the raw low byte.

If both state bytes are zero, the next draw substitutes 0xA55A before shifting. Thus __rng_seed(0) followed by __rng8() returns 255. The seed call stores zero; the substitution happens in the draw call.

These calls share persistent state. Reproduction requires the same target, seed and sequence of calls, including calls made by other helpers. Keep seed and draw operations in one execution context; these checks do not establish safe concurrent use from an interrupt handler.

GB and FC use different algorithms and do not promise the same sequence for the same seed. These are deterministic gameplay generators, not a source of cryptographic randomness; the tests check implementation behavior, not statistical quality or period.

Usage example

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

This is a calling fragment. Follow the stated initialization and buffer requirements before using it in a complete program.

Expected result

This sample reads three consecutive FC bytes. Seed 0x1234 produces FIRST 00019, SECOND 00137 and THIRD 00240. Those values show the call order; they are not a frequency or randomness score.

The complete NROM program prints blue decimal results on black. FAILED must read 00000. Text is uploaded with rendering disabled before the result screen is enabled.

Verification compared 32 outputs and eight subsequent byte draws per case with an independent host model: 93 GB cases and 18 FC cases, including constant/runtime seeds and GB range/chance/weight boundaries. The teaching screens were checked after 240 emulator frames. Physical hardware and statistical quality were not tested.

__rng8: NROM
NROM

Complete example program

Build command

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\rng_core.c -I .\kitaqfc\lib -I .\kitaq-docs\samples -o .\out\rng_core.nes --mapper=nrom --nes-chr=.\kitaq-docs\samples\font.chr --no-cache --no-disasm
Implementation and source

kitaqfc/lib/intrinsics.h:39

Declaration notes (original source)
Advance the shared 16-bit LFSR and return the XOR of its two bytes; zero state is reseeded to A55A.

kitaqfc/kitaqfc/CodeGenerator.cs:4663

csharp
case "__rng8":
                    if (!Need(0)) return true;
                    EmitAsm("JSR", Abs("__rng8"));
                    return true;
                // Far-memory helpers pack the address, bank and length arguments according to their explicit byte/word signatures.

EmitHelperStart("__rng8");
            // 16-bit Galois LFSR: if low bit set, shift right then xor $B400.
            EmitAsm("LDA", Mem(_runtimeRngLoAddress));
            EmitAsm("ORA", Mem(_runtimeRngHiAddress));
            // Replace the all-zero LFSR state with a deterministic nonzero seed before advancing.
            string seeded = NewGeneratedLabel("rng_seeded");
            EmitAsm("BNE", Rel(seeded));
            EmitAsm("LDA", Imm(0x5A));
            EmitAsm("STA", Mem(_runtimeRngLoAddress));
            EmitAsm("LDA", Imm(0xA5));
            EmitAsm("STA", Mem(_runtimeRngHiAddress));
            _assembly.Add(Expr.Make(Tag.Label, seeded));
            EmitAsm("LDA", Mem(_runtimeRngLoAddress));
            EmitAsm("AND", Imm(1));
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("LSR", Mem(_runtimeRngHiAddress));
            EmitAsm("ROR", Mem(_runtimeRngLoAddress));
            string rngNoXor = NewGeneratedLabel("rng_no_xor");
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("BEQ", Rel(rngNoXor));
            EmitAsm("LDA", Mem(_runtimeRngHiAddress));
            EmitAsm("EOR", Imm(0xB4));
            EmitAsm("STA", Mem(_runtimeRngHiAddress));
            _assembly.Add(Expr.Make(Tag.Label, rngNoXor));
            EmitAsm("LDA", Mem(_runtimeRngLoAddress));
            EmitAsm("EOR", Mem(_runtimeRngHiAddress));
            EmitAsm("RTS");
__rng_seed

Replace both state bytes of the FC generator. Reusing a seed lets a test or replay start from the same point in its pseudorandom sequence; the call does not advance the generator.

Syntax

c
void __rng_seed(u16 seed);

Parameters

seed

Unsigned 16-bit seed, 0–65535. Both bytes are stored as supplied. Choose and record the seed before the sequence of gameplay draws you want to reproduce.

Return value

None (void).

Conditions and limits

If both state bytes are zero, the next draw substitutes 0xA55A before shifting. Thus __rng_seed(0) followed by __rng8() returns 255. The seed call stores zero; the substitution happens in the draw call.

These calls share persistent state. Reproduction requires the same target, seed and sequence of calls, including calls made by other helpers. Keep seed and draw operations in one execution context; these checks do not establish safe concurrent use from an interrupt handler.

GB and FC use different algorithms and do not promise the same sequence for the same seed. These are deterministic gameplay generators, not a source of cryptographic randomness; the tests check implementation behavior, not statistical quality or period.

Usage example

c
__rng_seed(0x1234);
repeated=__rng8(); // The first byte is 19 again.
__rng_seed(0);
zero=__rng8(); // The next draw uses the FC fallback state and returns 255.

This is a calling fragment. Follow the stated initialization and buffer requirements before using it in a complete program.

Expected result

This sample shows repeatable FC startup and the zero-seed rule. FIRST and RESEEDED both display 00019 for seed 0x1234. ZERO SEED displays 00255 after seeding with zero.

The complete NROM program prints blue decimal results on black. FAILED must read 00000. Text is uploaded with rendering disabled before the result screen is enabled.

Verification compared 32 outputs and eight subsequent byte draws per case with an independent host model: 93 GB cases and 18 FC cases, including constant/runtime seeds and GB range/chance/weight boundaries. The teaching screens were checked after 240 emulator frames. Physical hardware and statistical quality were not tested.

__rng_seed: NROM
NROM

Complete example program

Build command

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\rng_core.c -I .\kitaqfc\lib -I .\kitaq-docs\samples -o .\out\rng_core.nes --mapper=nrom --nes-chr=.\kitaq-docs\samples\font.chr --no-cache --no-disasm
Implementation and source

kitaqfc/lib/intrinsics.h:37

Declaration notes (original source)
Replace both persistent state bytes. Reusing a seed reproduces the sequence; this is not cryptographic randomness.

kitaqfc/kitaqfc/CodeGenerator.cs:4661

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

EmitHelperStart("__rng_seed");
            EmitAsm("LDA", Mem(CallArgBase));
            EmitAsm("STA", Mem(_runtimeRngLoAddress));
            EmitAsm("LDA", Mem(CallArgBase + 1));
            EmitAsm("STA", Mem(_runtimeRngHiAddress));
            EmitAsm("RTS");
__rob_flash

Select the on or off rendering mask and wait for one NMI-counter change. The selected mask remains in effect after return.

Syntax

c
void __rob_flash(u8 bright);

Parameters

bright

Zero selects off; any nonzero byte selects on, including 255. This is not a brightness level.

Return value

No return value. Return to the caller after the requested waits complete.

Conditions and limits

Include intrinsics.h. The default NMI handler must be running and advancing its counter. Disabled NMI or a custom handler that does not maintain that counter leaves the wait blocked. There is no timeout.

The on phase sets PPUMASK and its software shadow to 0x1E; the off phase sets both to zero. The previous mask is not restored. This changes background, sprite and left-edge rendering together and also replaces grayscale and color-emphasis settings.

Enabling rendering does not create a white screen by itself. Supply bright tiles and palette entries plus a dark backdrop for the off phase. The sample uses solid white tiles and a black backdrop, flashing the whole screen during the operation.

Wait counts measure NMI-counter changes. The interval from entry to the first NMI may be shorter than one frame. Call from foreground code, not an interrupt. Another routine changing the rendering mask can disturb the pattern.

These are low-level display-mask and wait primitives. They do not generate a robot command preamble, device-specific command sequence or reception acknowledgement. Verification covers PPUMASK writes, NMI counts and pixels in KUROSAKI; physical robot reception has not been tested.

Usage example

c
start=__nmi_ready();
    __rob_flash(1); // Enable rendering and wait for one NMI-counter change.
    rob_result[0]=(u8)(__nmi_ready()-start);
    __rob_flash(0); // Disable rendering; the black backdrop remains visible.

This is a calling fragment. Follow the stated initialization and buffer requirements before using it in a complete program.

Expected result

After execution, the screen shows FLASH WAITS 1, PULSE 3 ON / 2 OFF = 5 and A5 BYTE WAITS 48. The diagram represents 0xA5: 1,0,1,0 on the upper row and 0,1,0,1 below. Each cell is one wait; white means rendering enabled and black means disabled. A one has four white cells followed by two black cells; a zero has two white followed by four black. FINAL MASK 0 and TIMING CHECKS OK indicate completion.

__rob_flash: NTSC — After execution, the screen shows FLASH WAITS 1, PULSE 3 ON / 2 OFF = 5 and A5 BYTE WAITS 48. The diagram represents 0xA5: 1,0,1,0 on the upper row and 0,1,0,1 below. Each cell is one wait; white means rendering enabled and black means disabled. A one has four white cells followed by two black cells; a zero has two white followed by four black. FINAL MASK 0 and TIMING CHECKS OK indicate completion.
NTSC — After execution, the screen shows FLASH WAITS 1, PULSE 3 ON / 2 OFF = 5 and A5 BYTE WAITS 48. The diagram represents 0xA5: 1,0,1,0 on the upper row and 0,1,0,1 below. Each cell is one wait; white means rendering enabled and black means disabled. A one has four white cells followed by two black cells; a zero has two white followed by four black. FINAL MASK 0 and TIMING CHECKS OK indicate completion.

Complete example program

Build command

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\rob_timing.c -I .\kitaqfc\lib -I .\kitaq-docs\samples --mapper=nrom --nes-chr=.\kitaq-docs\samples\api-examples\fc\vram_shapes.chr -o .\out\rob_timing.nes --no-cache --no-disasm
Implementation and source

kitaqfc/lib/intrinsics.h:272

Declaration notes (original source)
Set PPUMASK and its shadow to 1E or 00, then wait for an NMI-counter change.
The caller supplies image/palette brightness; rendering enable alone does not create a white screen.

kitaqfc/kitaqfc/CodeGenerator.cs:7867

csharp
EmitHelperStart("__rob_flash");
            EmitAsm("LDA", Mem(CallArgBase));
            string black = NewGeneratedLabel("rob_black");
            EmitAsm("BEQ", Rel(black));
            EmitAsm("LDA", Imm(0x1E));
            EmitAsm("STA", Mem(_runtimePpuMaskShadowAddress));
            EmitAsm("STA", Mem(0x2001));
            EmitAsm("JSR", Abs("__nmi_wait"));
            EmitAsm("RTS");
            _assembly.Add(Expr.Make(Tag.Label, black));
            EmitAsm("LDA", Imm(0x00));
            EmitAsm("STA", Mem(_runtimePpuMaskShadowAddress));
            EmitAsm("STA", Mem(0x2001));
            EmitAsm("JSR", Abs("__nmi_wait"));
            EmitAsm("RTS");

            // Emit the requested number of enabled frames followed by the requested disabled frames.
__rob_pulse

Perform the requested on waits followed by the requested off waits, producing one pulse.

Syntax

c
void __rob_pulse(u8 on_frames, u8 off_frames);

Parameters

on_frames

On-phase wait count, 0..255. Zero skips the on phase.

off_frames

Off-phase wait count, 0..255. Zero skips the off phase. The total can reach 510 waits.

Return value

No return value. Return to the caller after the requested waits complete.

Conditions and limits

With both counts zero, neither the mask nor timing changes. Any off waits leave mask zero; an on-only pulse leaves 0x1E.

Include intrinsics.h. The default NMI handler must be running and advancing its counter. Disabled NMI or a custom handler that does not maintain that counter leaves the wait blocked. There is no timeout.

The on phase sets PPUMASK and its software shadow to 0x1E; the off phase sets both to zero. The previous mask is not restored. This changes background, sprite and left-edge rendering together and also replaces grayscale and color-emphasis settings.

Enabling rendering does not create a white screen by itself. Supply bright tiles and palette entries plus a dark backdrop for the off phase. The sample uses solid white tiles and a black backdrop, flashing the whole screen during the operation.

Wait counts measure NMI-counter changes. The interval from entry to the first NMI may be shorter than one frame. Call from foreground code, not an interrupt. Another routine changing the rendering mask can disturb the pattern.

These are low-level display-mask and wait primitives. They do not generate a robot command preamble, device-specific command sequence or reception acknowledgement. Verification covers PPUMASK writes, NMI counts and pixels in KUROSAKI; physical robot reception has not been tested.

Usage example

c
start=__nmi_ready();
    __rob_pulse(3,2); // Three enabled waits, then two disabled waits.
    rob_result[1]=(u8)(__nmi_ready()-start);

This is a calling fragment. Follow the stated initialization and buffer requirements before using it in a complete program.

Expected result

After execution, the screen shows FLASH WAITS 1, PULSE 3 ON / 2 OFF = 5 and A5 BYTE WAITS 48. The diagram represents 0xA5: 1,0,1,0 on the upper row and 0,1,0,1 below. Each cell is one wait; white means rendering enabled and black means disabled. A one has four white cells followed by two black cells; a zero has two white followed by four black. FINAL MASK 0 and TIMING CHECKS OK indicate completion.

__rob_pulse: NTSC — After execution, the screen shows FLASH WAITS 1, PULSE 3 ON / 2 OFF = 5 and A5 BYTE WAITS 48. The diagram represents 0xA5: 1,0,1,0 on the upper row and 0,1,0,1 below. Each cell is one wait; white means rendering enabled and black means disabled. A one has four white cells followed by two black cells; a zero has two white followed by four black. FINAL MASK 0 and TIMING CHECKS OK indicate completion.
NTSC — After execution, the screen shows FLASH WAITS 1, PULSE 3 ON / 2 OFF = 5 and A5 BYTE WAITS 48. The diagram represents 0xA5: 1,0,1,0 on the upper row and 0,1,0,1 below. Each cell is one wait; white means rendering enabled and black means disabled. A one has four white cells followed by two black cells; a zero has two white followed by four black. FINAL MASK 0 and TIMING CHECKS OK indicate completion.

Complete example program

Build command

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\rob_timing.c -I .\kitaqfc\lib -I .\kitaq-docs\samples --mapper=nrom --nes-chr=.\kitaq-docs\samples\api-examples\fc\vram_shapes.chr -o .\out\rob_timing.nes --no-cache --no-disasm
Implementation and source

kitaqfc/lib/intrinsics.h:274

Declaration notes (original source)
Run on_frames enabled-mask waits followed by off_frames disabled-mask waits; NMI must advance.

kitaqfc/kitaqfc/CodeGenerator.cs:7884

csharp
EmitHelperStart("__rob_pulse");
            EmitAsm("LDX", Mem(CallArgBase));
            string onLoop = NewGeneratedLabel("rob_on_loop");
            string offStart = NewGeneratedLabel("rob_off_start");
            _assembly.Add(Expr.Make(Tag.Label, onLoop));
            EmitAsm("CPX", Imm(0));
            EmitAsm("BEQ", Rel(offStart));
            EmitAsm("LDA", Imm(1));
            EmitAsm("STA", Mem(CallArgBase));
            EmitAsm("JSR", Abs("__rob_flash"));
            EmitAsm("DEX");
            EmitAsm("BNE", Rel(onLoop));
            _assembly.Add(Expr.Make(Tag.Label, offStart));
            EmitAsm("LDX", Mem(CallArgBase + 1));
            string offLoop = NewGeneratedLabel("rob_off_loop");
            string pulseDone = NewGeneratedLabel("rob_pulse_done");
            _assembly.Add(Expr.Make(Tag.Label, offLoop));
            EmitAsm("CPX", Imm(0));
            EmitAsm("BEQ", Rel(pulseDone));
            EmitAsm("LDA", Imm(0));
            EmitAsm("STA", Mem(CallArgBase));
            EmitAsm("JSR", Abs("__rob_flash"));
            EmitAsm("DEX");
            EmitAsm("BNE", Rel(offLoop));
            _assembly.Add(Expr.Make(Tag.Label, pulseDone));
            EmitAsm("RTS");

            // Choose a four/two or two/four frame pulse pattern for each current high bit.
__rob_send_byte

Encode one byte as eight blink patterns, most-significant bit first. One uses four on waits and two off waits; zero uses two on waits and four off waits. The total is 48 NMI waits.

Syntax

c
void __rob_send_byte(u8 pattern);

Parameters

pattern

Eight-bit pattern, 0..255. For 0xA5 the order is 1,0,1,0,0,1,0,1. This is not a channel ID or a robot action number.

Return value

No return value. Return to the caller after the requested waits complete.

Conditions and limits

The last bit also ends with off waits, leaving PPUMASK and its shadow at zero. Send exactly eight bits; no start or end marker is added.

Include intrinsics.h. The default NMI handler must be running and advancing its counter. Disabled NMI or a custom handler that does not maintain that counter leaves the wait blocked. There is no timeout.

The on phase sets PPUMASK and its software shadow to 0x1E; the off phase sets both to zero. The previous mask is not restored. This changes background, sprite and left-edge rendering together and also replaces grayscale and color-emphasis settings.

Enabling rendering does not create a white screen by itself. Supply bright tiles and palette entries plus a dark backdrop for the off phase. The sample uses solid white tiles and a black backdrop, flashing the whole screen during the operation.

Wait counts measure NMI-counter changes. The interval from entry to the first NMI may be shorter than one frame. Call from foreground code, not an interrupt. Another routine changing the rendering mask can disturb the pattern.

These are low-level display-mask and wait primitives. They do not generate a robot command preamble, device-specific command sequence or reception acknowledgement. Verification covers PPUMASK writes, NMI counts and pixels in KUROSAKI; physical robot reception has not been tested.

Usage example

c
start=__nmi_ready();
    __rob_send_byte(0xA5); // 10100101, most-significant bit first; 48 waits.
    rob_result[2]=(u8)(__nmi_ready()-start);
    rob_result[3]=__ppu_mask_get(); // Zero: the sequence ends with rendering off.

This is a calling fragment. Follow the stated initialization and buffer requirements before using it in a complete program.

Expected result

After execution, the screen shows FLASH WAITS 1, PULSE 3 ON / 2 OFF = 5 and A5 BYTE WAITS 48. The diagram represents 0xA5: 1,0,1,0 on the upper row and 0,1,0,1 below. Each cell is one wait; white means rendering enabled and black means disabled. A one has four white cells followed by two black cells; a zero has two white followed by four black. FINAL MASK 0 and TIMING CHECKS OK indicate completion.

__rob_send_byte: NTSC — After execution, the screen shows FLASH WAITS 1, PULSE 3 ON / 2 OFF = 5 and A5 BYTE WAITS 48. The diagram represents 0xA5: 1,0,1,0 on the upper row and 0,1,0,1 below. Each cell is one wait; white means rendering enabled and black means disabled. A one has four white cells followed by two black cells; a zero has two white followed by four black. FINAL MASK 0 and TIMING CHECKS OK indicate completion.
NTSC — After execution, the screen shows FLASH WAITS 1, PULSE 3 ON / 2 OFF = 5 and A5 BYTE WAITS 48. The diagram represents 0xA5: 1,0,1,0 on the upper row and 0,1,0,1 below. Each cell is one wait; white means rendering enabled and black means disabled. A one has four white cells followed by two black cells; a zero has two white followed by four black. FINAL MASK 0 and TIMING CHECKS OK indicate completion.

Complete example program

Build command

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\rob_timing.c -I .\kitaqfc\lib -I .\kitaq-docs\samples --mapper=nrom --nes-chr=.\kitaq-docs\samples\api-examples\fc\vram_shapes.chr -o .\out\rob_timing.nes --no-cache --no-disasm
Implementation and source

kitaqfc/lib/intrinsics.h:278

Declaration notes (original source)
Send eight bits MSB first: one uses 4 on/2 off waits; zero uses 2 on/4 off.
Requires an advancing runtime NMI counter; 48 waits in total, final PPUMASK=0.
This raw pattern does not add a robot command preamble or validate reception.

kitaqfc/kitaqfc/CodeGenerator.cs:7912

csharp
EmitHelperStart("__rob_send_byte");
            EmitAsm("LDA", Mem(CallArgBase));
            EmitAsm("LDX", Imm(8));
            string bitLoop = NewGeneratedLabel("rob_bit_loop");
            string bitZero = NewGeneratedLabel("rob_bit_zero");
            string bitNext = NewGeneratedLabel("rob_bit_next");
            _assembly.Add(Expr.Make(Tag.Label, bitLoop));
            // Preserve the remaining payload and outer count on the CPU stack.
            // Pulse consumes X; its NMI wait also uses the shared scratch byte.
            // ASL selects the next MSB in carry, which TXA/PHA leave unchanged.
            EmitAsm("ASL");
            EmitAsm("PHA");
            EmitAsm("TXA");
            EmitAsm("PHA");
            EmitAsm("BCC", Rel(bitZero));
            EmitAsm("LDA", Imm(4));
            EmitAsm("STA", Mem(CallArgBase));
            EmitAsm("LDA", Imm(2));
            EmitAsm("STA", Mem(CallArgBase + 1));
            EmitAsm("JSR", Abs("__rob_pulse"));
            EmitAsm("JMP", Abs(bitNext));
            _assembly.Add(Expr.Make(Tag.Label, bitZero));
            EmitAsm("LDA", Imm(2));
            EmitAsm("STA", Mem(CallArgBase));
            EmitAsm("LDA", Imm(4));
            EmitAsm("STA", Mem(CallArgBase + 1));
            EmitAsm("JSR", Abs("__rob_pulse"));
            _assembly.Add(Expr.Make(Tag.Label, bitNext));
            EmitAsm("PLA");
            EmitAsm("TAX");
            EmitAsm("PLA");
            EmitAsm("DEX");
            EmitAsm("BNE", Rel(bitLoop));
            EmitAsm("RTS");
        }
__scroll_latch_reset

Read PPUSTATUS and discard the result to reset the shared PPU scroll/address write latch. It also clears the VBlank flag. No zero scroll coordinates are written, and saved X/Y values are not changed.

Syntax

c
void __scroll_latch_reset(void);

Return value

None (void).

Conditions and limits

A PPUSTATUS read can affect NMI timing and the next writes to both PPUSCROLL and PPUADDR. Do not insert it into another routine's half-written register pair. These calls do not wait for VBlank or complete pending transfer queues.

NESdev: PPU registers

NESdev: PPU scrolling

Usage example

c
address_port=0x21; // Leave the shared latch halfway through an address pair.
__scroll_latch_reset();
address_port=0x21;address_port=0x83;data_port=2; // Blue left-half marker.

This is a calling fragment. Follow the stated initialization and buffer requirements before using it in a complete program.

Expected result

This program teaches register setup, direct tile/attribute writes, scrolling and separate background/sprite palette loads. The blue band at (0,16) measures 256×16 pixels: its first row repeats a solid 8×8 tile, a left-half 4×8 bar and an eight-row triangle; its second row contains only left-half bars. A solid green 64×32 rectangle starts at (16,48). A red 64×32 region at (144,48) contains an 8-by-4 grid of triangle tiles.

At y=96, blue markers at x=16, 24 and 32 are respectively a square, left-half bar and triangle; a red left-half bar is at x=80 and a green triangle at x=144. These are background tiles. At y=144, the separately colored sprites are a red 8×8 square at x=16, a blue left-half 4×8 bar at x=80 and a green eight-row triangle at x=144. Final scrolling is (0,0).

The image checks 16,384 pixels for the expected shapes and colors, including sprite colors. Separate state tests cover 31 cases: register replacement, status/latch effects, saved scroll coordinates, zero/255-byte transfers, increments 1/32, map edges, zero rectangle dimensions, table selectors, whole-byte attributes and both palette targets. Values of Y=255 are checked as register writes, not as correct scrolling artwork. Evidence is from KUROSAKI with NROM; real hardware is not tested.

__scroll_latch_reset: NROM
NROM

Complete example program

Build command

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\ppu_intrinsics.c -I .\kitaqfc\lib -I .\kitaq-docs\samples --nes-chr=.\kitaq-docs\samples\api-examples\fc\vram_shapes.chr -o .\out\ppu_intrinsics.nes --no-cache --no-disasm
Implementation and source

kitaqfc/lib/intrinsics.h:112

Declaration notes (original source)
Read PPUSTATUS to reset the scroll/address write latch; the read also clears VBlank status.

kitaqfc/kitaqfc/CodeGenerator.cs:4708

csharp
case "__scroll_latch_reset":
                    if (!Need(0)) return true;
                    EmitAsm("LDA", Mem(0x2002)); return true;
                // Store both scroll components, reset the write latch, then emit the X/Y pair.
__scroll_set

Save both pixel offsets in the runtime X/Y shadows, read PPUSTATUS to reset the shared latch, then write X and Y to PPUSCROLL in that order. This updates scrolling immediately; it does not queue a camera move, wait for NMI or choose a base nametable.

Syntax

c
void __scroll_set(u8 x, u8 y);

Parameters

x / y

Offsets are unsigned pixel bytes. Use X 0..255 and normally Y 0..239; Y 240..255 is written unchanged but has special PPU row behavior, not ordinary visible-map scrolling. PPUCTRL supplies the base nametable bits. Restore intended scrolling after direct VRAM access and before rendering; the function does not schedule that timing for you.

Return value

None (void).

Conditions and limits

A PPUSTATUS read can affect NMI timing and the next writes to both PPUSCROLL and PPUADDR. Do not insert it into another routine's half-written register pair. These calls do not wait for VBlank or complete pending transfer queues.

NESdev: PPU registers

NESdev: PPU scrolling

Usage example

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

This is a calling fragment. Follow the stated initialization and buffer requirements before using it in a complete program.

Expected result

This program teaches register setup, direct tile/attribute writes, scrolling and separate background/sprite palette loads. The blue band at (0,16) measures 256×16 pixels: its first row repeats a solid 8×8 tile, a left-half 4×8 bar and an eight-row triangle; its second row contains only left-half bars. A solid green 64×32 rectangle starts at (16,48). A red 64×32 region at (144,48) contains an 8-by-4 grid of triangle tiles.

At y=96, blue markers at x=16, 24 and 32 are respectively a square, left-half bar and triangle; a red left-half bar is at x=80 and a green triangle at x=144. These are background tiles. At y=144, the separately colored sprites are a red 8×8 square at x=16, a blue left-half 4×8 bar at x=80 and a green eight-row triangle at x=144. Final scrolling is (0,0).

The image checks 16,384 pixels for the expected shapes and colors, including sprite colors. Separate state tests cover 31 cases: register replacement, status/latch effects, saved scroll coordinates, zero/255-byte transfers, increments 1/32, map edges, zero rectangle dimensions, table selectors, whole-byte attributes and both palette targets. Values of Y=255 are checked as register writes, not as correct scrolling artwork. Evidence is from KUROSAKI with NROM; real hardware is not tested.

__scroll_set: NROM
NROM

Complete example program

Build command

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\ppu_intrinsics.c -I .\kitaqfc\lib -I .\kitaq-docs\samples --nes-chr=.\kitaq-docs\samples\api-examples\fc\vram_shapes.chr -o .\out\ppu_intrinsics.nes --no-cache --no-disasm
Implementation and source

kitaqfc/lib/intrinsics.h:114

Declaration notes (original source)
Save X/Y in runtime shadows, reset the PPU latch and write both scroll bytes immediately.

kitaqfc/kitaqfc/CodeGenerator.cs:4712

csharp
case "__scroll_set":
                    if (!Need(2)) return true;
                    Store1(args[0], _runtimeScrollXAddress); Store1(args[1], _runtimeScrollYAddress);
                    EmitAsm("LDA", Mem(0x2002)); EmitAsm("LDA", Mem(_runtimeScrollXAddress)); EmitAsm("STA", Mem(0x2005)); EmitAsm("LDA", Mem(_runtimeScrollYAddress)); EmitAsm("STA", Mem(0x2005)); return true;
                // Changing X rewrites both latch bytes, using the retained Y component.
__scroll_x_set

Replace saved X, reset the shared latch with a status read, then write the new X and saved Y to PPUSCROLL. Y comes from the software shadow, not from the PPU; direct register writes do not update that shadow.

Syntax

c
void __scroll_x_set(u8 x);

Parameters

x

Offsets are unsigned pixel bytes. Use X 0..255 and normally Y 0..239; Y 240..255 is written unchanged but has special PPU row behavior, not ordinary visible-map scrolling. PPUCTRL supplies the base nametable bits. Restore intended scrolling after direct VRAM access and before rendering; the function does not schedule that timing for you.

Return value

None (void).

Conditions and limits

A PPUSTATUS read can affect NMI timing and the next writes to both PPUSCROLL and PPUADDR. Do not insert it into another routine's half-written register pair. These calls do not wait for VBlank or complete pending transfer queues.

NESdev: PPU registers

NESdev: PPU scrolling

Usage example

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

This is a calling fragment. Follow the stated initialization and buffer requirements before using it in a complete program.

Expected result

This program teaches register setup, direct tile/attribute writes, scrolling and separate background/sprite palette loads. The blue band at (0,16) measures 256×16 pixels: its first row repeats a solid 8×8 tile, a left-half 4×8 bar and an eight-row triangle; its second row contains only left-half bars. A solid green 64×32 rectangle starts at (16,48). A red 64×32 region at (144,48) contains an 8-by-4 grid of triangle tiles.

At y=96, blue markers at x=16, 24 and 32 are respectively a square, left-half bar and triangle; a red left-half bar is at x=80 and a green triangle at x=144. These are background tiles. At y=144, the separately colored sprites are a red 8×8 square at x=16, a blue left-half 4×8 bar at x=80 and a green eight-row triangle at x=144. Final scrolling is (0,0).

The image checks 16,384 pixels for the expected shapes and colors, including sprite colors. Separate state tests cover 31 cases: register replacement, status/latch effects, saved scroll coordinates, zero/255-byte transfers, increments 1/32, map edges, zero rectangle dimensions, table selectors, whole-byte attributes and both palette targets. Values of Y=255 are checked as register writes, not as correct scrolling artwork. Evidence is from KUROSAKI with NROM; real hardware is not tested.

__scroll_x_set: NROM
NROM

Complete example program

Build command

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\ppu_intrinsics.c -I .\kitaqfc\lib -I .\kitaq-docs\samples --nes-chr=.\kitaq-docs\samples\api-examples\fc\vram_shapes.chr -o .\out\ppu_intrinsics.nes --no-cache --no-disasm
Implementation and source

kitaqfc/lib/intrinsics.h:116

Declaration notes (original source)
Replace saved X and write an X/Y pair using the retained Y shadow.

kitaqfc/kitaqfc/CodeGenerator.cs:4717

csharp
case "__scroll_x_set":
                    if (!Need(1)) return true;
                    Store1(args[0], _runtimeScrollXAddress);
                    EmitAsm("LDA", Mem(0x2002)); EmitAsm("LDA", Mem(_runtimeScrollXAddress)); EmitAsm("STA", Mem(0x2005)); EmitAsm("LDA", Mem(_runtimeScrollYAddress)); EmitAsm("STA", Mem(0x2005)); return true;
                // Changing Y rewrites both latch bytes, using the retained X component.
__scroll_y_set

Replace saved Y, reset the shared latch with a status read, then write saved X and the new Y to PPUSCROLL. Both scroll bytes are written even though only Y changes. X is taken from the runtime shadow.

Syntax

c
void __scroll_y_set(u8 y);

Parameters

y

Offsets are unsigned pixel bytes. Use X 0..255 and normally Y 0..239; Y 240..255 is written unchanged but has special PPU row behavior, not ordinary visible-map scrolling. PPUCTRL supplies the base nametable bits. Restore intended scrolling after direct VRAM access and before rendering; the function does not schedule that timing for you.

Return value

None (void).

Conditions and limits

A PPUSTATUS read can affect NMI timing and the next writes to both PPUSCROLL and PPUADDR. Do not insert it into another routine's half-written register pair. These calls do not wait for VBlank or complete pending transfer queues.

NESdev: PPU registers

NESdev: PPU scrolling

Usage example

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

This is a calling fragment. Follow the stated initialization and buffer requirements before using it in a complete program.

Expected result

This program teaches register setup, direct tile/attribute writes, scrolling and separate background/sprite palette loads. The blue band at (0,16) measures 256×16 pixels: its first row repeats a solid 8×8 tile, a left-half 4×8 bar and an eight-row triangle; its second row contains only left-half bars. A solid green 64×32 rectangle starts at (16,48). A red 64×32 region at (144,48) contains an 8-by-4 grid of triangle tiles.

At y=96, blue markers at x=16, 24 and 32 are respectively a square, left-half bar and triangle; a red left-half bar is at x=80 and a green triangle at x=144. These are background tiles. At y=144, the separately colored sprites are a red 8×8 square at x=16, a blue left-half 4×8 bar at x=80 and a green eight-row triangle at x=144. Final scrolling is (0,0).

The image checks 16,384 pixels for the expected shapes and colors, including sprite colors. Separate state tests cover 31 cases: register replacement, status/latch effects, saved scroll coordinates, zero/255-byte transfers, increments 1/32, map edges, zero rectangle dimensions, table selectors, whole-byte attributes and both palette targets. Values of Y=255 are checked as register writes, not as correct scrolling artwork. Evidence is from KUROSAKI with NROM; real hardware is not tested.

__scroll_y_set: NROM
NROM

Complete example program

Build command

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\ppu_intrinsics.c -I .\kitaqfc\lib -I .\kitaq-docs\samples --nes-chr=.\kitaq-docs\samples\api-examples\fc\vram_shapes.chr -o .\out\ppu_intrinsics.nes --no-cache --no-disasm
Implementation and source

kitaqfc/lib/intrinsics.h:118

Declaration notes (original source)
Replace saved Y and write an X/Y pair using the retained X shadow.

kitaqfc/kitaqfc/CodeGenerator.cs:4722

csharp
case "__scroll_y_set":
                    if (!Need(1)) return true;
                    Store1(args[0], _runtimeScrollYAddress);
                    EmitAsm("LDA", Mem(0x2002)); EmitAsm("LDA", Mem(_runtimeScrollXAddress)); EmitAsm("STA", Mem(0x2005)); EmitAsm("LDA", Mem(_runtimeScrollYAddress)); EmitAsm("STA", Mem(0x2005)); return true;
                // Modify only the NMI-enable bit in the PPUCTRL shadow and publish the new register value.
__sdot2_q1_7

Compute a signed two-term dot product.

Syntax

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

Parameters

a0

Signed 16-bit bit pattern, representing −32768..32767.

a1

Signed 16-bit bit pattern, representing −32768..32767.

b0

Signed byte bit pattern, coefficient −128..127.

b1

Signed byte bit pattern, coefficient −128..127.

Return value

Returns the low 16 bits of a0*b0+a1*b1.

Conditions and limits

On FC, q8_8/q1_7 names still use raw integer products without a fixed-point downshift. The Q1.7 functions alias their corresponding signed operations. The caller handles units; signed results are returned as u16 bit patterns.

Usage example

c
// Compare literal coefficients with values read from RAM.
result[34]=__sdot2_q1_7(-513,769,-64,127);
v0=-513;v1=769;v2=-64;v3=127;
result[35]=__sdot2_q1_7(v0,v1,v2,v3);
// Reverse coefficient signs and verify negative products.
result[36]=__sdot2_q1_7(-513,769,64,-127);
v0=-513;v1=769;v2=64;v3=-127;
result[37]=__sdot2_q1_7(v0,v1,v2,v3);

This is a calling fragment. Follow the stated initialization and buffer requirements before using it in a complete program.

Expected result

Compare literal inputs with values stored in RAM. Representative results appear as unsigned 16-bit values; negatives use two’s-complement bit patterns. GB rescales fixed-point products, while FC returns integer products, so identical inputs need not give identical results.

The excerpt stores these 16-bit values: result[34]=64959, result[35]=64959, result[36]=577, result[37]=577. Each literal-input calculation matches its RAM-input counterpart. The screen shows representative values; the result array holds every result.

__sdot2_q1_7: NROM
NROM

Complete example program

Build command

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\batch3_math.c -I .\kitaqfc\lib -I .\kitaq-docs\samples -o .\out\batch3_math.nes --no-cache --no-disasm --mapper=nrom --nes-chr=.\kitaq-docs\samples\font.chr
Implementation and source

kitaqfc/lib/intrinsics.h:71

Declaration notes (original source)
FC alias of __sdot2_q8_8: two raw signed-pattern products, summed without rescaling.

kitaqfc/kitaqfc/CodeGenerator.cs:4658

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

void EmitHelperMath()
        {
            // Consume eight multiplier bits and accumulate the low product word in shared temporaries, returning A/X.
            EmitHelperStart("__mul16x8");
            EmitAsm("LDA", Imm(0));
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp1Address));
            EmitAsm("LDX", Imm(8));
            string loop = NewGeneratedLabel("mul16x8_loop");
            string skip = NewGeneratedLabel("mul16x8_skip_add");
            _assembly.Add(Expr.Make(Tag.Label, loop));
            EmitAsm("LSR", Mem(CallArgBase + 2));
            EmitAsm("BCC", Rel(skip));
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("CLC");
            EmitAsm("ADC", Mem(CallArgBase));
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp1Address));
            EmitAsm("ADC", Mem(CallArgBase + 1));
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp1Address));
            _assembly.Add(Expr.Make(Tag.Label, skip));
            EmitAsm("ASL", Mem(CallArgBase));
            EmitAsm("ROL", Mem(CallArgBase + 1));
            EmitAsm("DEX");
            EmitAsm("BNE", Rel(loop));
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("LDX", Mem(_runtimeIntrinsicTmp1Address));
            EmitAsm("RTS");

            // Zero-extend the byte multiplicand, reuse word multiplication and return its high byte.
            EmitHelperStart("__mul8x8_hi");
            EmitAsm("LDA", Mem(CallArgBase + 1));
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp2Address));
            EmitAsm("LDA", Imm(0));
            EmitAsm("STA", Mem(CallArgBase + 1));
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp2Address));
            EmitAsm("STA", Mem(CallArgBase + 2));
            EmitAsm("JSR", Abs("__mul16x8"));
            EmitAsm("TXA");
            EmitAsm("RTS");

            // Convert signed operands to magnitudes, multiply, then restore the product sign modulo 65536.
            EmitHelperStart("__smul16x8");
            EmitAsm("LDA", Imm(0));
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp2Address));
            string aPos = NewGeneratedLabel("smul_a_positive");
            EmitAsm("LDA", Mem(CallArgBase + 1));
            EmitAsm("AND", Imm(0x80));
            EmitAsm("BEQ", Rel(aPos));
            EmitAsm("LDA", Mem(CallArgBase)); EmitAsm("EOR", Imm(0xFF)); EmitAsm("CLC"); EmitAsm("ADC", Imm(1)); EmitAsm("STA", Mem(CallArgBase));
            EmitAsm("LDA", Mem(CallArgBase + 1)); EmitAsm("EOR", Imm(0xFF)); EmitAsm("ADC", Imm(0)); EmitAsm("STA", Mem(CallArgBase + 1));
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp2Address)); EmitAsm("EOR", Imm(1)); EmitAsm("STA", Mem(_runtimeIntrinsicTmp2Address));
            _assembly.Add(Expr.Make(Tag.Label, aPos));
            string bPos = NewGeneratedLabel("smul_b_positive");
            EmitAsm("LDA", Mem(CallArgBase + 2));
            EmitAsm("AND", Imm(0x80));
            EmitAsm("BEQ", Rel(bPos));
            EmitAsm("LDA", Mem(CallArgBase + 2)); EmitAsm("EOR", Imm(0xFF)); EmitAsm("CLC"); EmitAsm("ADC", Imm(1)); EmitAsm("STA", Mem(CallArgBase + 2));
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp2Address)); EmitAsm("EOR", Imm(1)); EmitAsm("STA", Mem(_runtimeIntrinsicTmp2Address));
            _assembly.Add(Expr.Make(Tag.Label, bPos));
            EmitAsm("JSR", Abs("__mul16x8"));
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("STX", Mem(_runtimeIntrinsicTmp1Address));
            string sdone = NewGeneratedLabel("smul_done");
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp2Address));
            EmitAsm("BEQ", Rel(sdone));
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp0Address)); EmitAsm("EOR", Imm(0xFF)); EmitAsm("CLC"); EmitAsm("ADC", Imm(1)); EmitAsm("STA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp1Address)); EmitAsm("EOR", Imm(0xFF)); EmitAsm("ADC", Imm(0)); EmitAsm("STA", Mem(_runtimeIntrinsicTmp1Address));
            _assembly.Add(Expr.Make(Tag.Label, sdone));
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("LDX", Mem(_runtimeIntrinsicTmp1Address));
            EmitAsm("RTS");

            EmitMacHelper("__mac16", "__mul16x8");
            EmitMacHelper("__smac16", "__smul16x8");
            EmitDot2Helper("__dot2_q8_8", "__mul16x8");
            EmitDot3Helper("__dot3_q8_8", "__mul16x8");
            EmitDot2Helper("__sdot2_q8_8", "__smul16x8");
            EmitDot3Helper("__sdot3_q8_8", "__smul16x8");
        }
__sdot2_q8_8

Compute a signed two-term dot product.

Syntax

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

Parameters

a0

Signed 16-bit bit pattern, representing −32768..32767.

a1

Signed 16-bit bit pattern, representing −32768..32767.

b0

Signed byte bit pattern, coefficient −128..127.

b1

Signed byte bit pattern, coefficient −128..127.

Return value

Returns the low 16 bits of a0*b0+a1*b1.

Conditions and limits

On FC, q8_8/q1_7 names still use raw integer products without a fixed-point downshift. The Q1.7 functions alias their corresponding signed operations. The caller handles units; signed results are returned as u16 bit patterns.

Usage example

c
// Compare literal coefficients with values read from RAM.
result[26]=__sdot2_q8_8(-513,769,-64,127);
v0=-513;v1=769;v2=-64;v3=127;
result[27]=__sdot2_q8_8(v0,v1,v2,v3);
// Reverse coefficient signs and verify negative products.
result[28]=__sdot2_q8_8(-513,769,64,-127);
v0=-513;v1=769;v2=64;v3=-127;
result[29]=__sdot2_q8_8(v0,v1,v2,v3);

This is a calling fragment. Follow the stated initialization and buffer requirements before using it in a complete program.

Expected result

Compare literal inputs with values stored in RAM. Representative results appear as unsigned 16-bit values; negatives use two’s-complement bit patterns. GB rescales fixed-point products, while FC returns integer products, so identical inputs need not give identical results.

The excerpt stores these 16-bit values: result[26]=64959, result[27]=64959, result[28]=577, result[29]=577. Each literal-input calculation matches its RAM-input counterpart. The screen shows representative values; the result array holds every result.

__sdot2_q8_8: NROM
NROM

Complete example program

Build command

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\batch3_math.c -I .\kitaqfc\lib -I .\kitaq-docs\samples -o .\out\batch3_math.nes --no-cache --no-disasm --mapper=nrom --nes-chr=.\kitaq-docs\samples\font.chr
Implementation and source

kitaqfc/lib/intrinsics.h:67

Declaration notes (original source)
Sum two signed-pattern products modulo 65536; no Q8.8 scaling shift is generated.

kitaqfc/kitaqfc/CodeGenerator.cs:4656

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

void EmitHelperMath()
        {
            // Consume eight multiplier bits and accumulate the low product word in shared temporaries, returning A/X.
            EmitHelperStart("__mul16x8");
            EmitAsm("LDA", Imm(0));
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp1Address));
            EmitAsm("LDX", Imm(8));
            string loop = NewGeneratedLabel("mul16x8_loop");
            string skip = NewGeneratedLabel("mul16x8_skip_add");
            _assembly.Add(Expr.Make(Tag.Label, loop));
            EmitAsm("LSR", Mem(CallArgBase + 2));
            EmitAsm("BCC", Rel(skip));
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("CLC");
            EmitAsm("ADC", Mem(CallArgBase));
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp1Address));
            EmitAsm("ADC", Mem(CallArgBase + 1));
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp1Address));
            _assembly.Add(Expr.Make(Tag.Label, skip));
            EmitAsm("ASL", Mem(CallArgBase));
            EmitAsm("ROL", Mem(CallArgBase + 1));
            EmitAsm("DEX");
            EmitAsm("BNE", Rel(loop));
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("LDX", Mem(_runtimeIntrinsicTmp1Address));
            EmitAsm("RTS");

            // Zero-extend the byte multiplicand, reuse word multiplication and return its high byte.
            EmitHelperStart("__mul8x8_hi");
            EmitAsm("LDA", Mem(CallArgBase + 1));
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp2Address));
            EmitAsm("LDA", Imm(0));
            EmitAsm("STA", Mem(CallArgBase + 1));
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp2Address));
            EmitAsm("STA", Mem(CallArgBase + 2));
            EmitAsm("JSR", Abs("__mul16x8"));
            EmitAsm("TXA");
            EmitAsm("RTS");

            // Convert signed operands to magnitudes, multiply, then restore the product sign modulo 65536.
            EmitHelperStart("__smul16x8");
            EmitAsm("LDA", Imm(0));
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp2Address));
            string aPos = NewGeneratedLabel("smul_a_positive");
            EmitAsm("LDA", Mem(CallArgBase + 1));
            EmitAsm("AND", Imm(0x80));
            EmitAsm("BEQ", Rel(aPos));
            EmitAsm("LDA", Mem(CallArgBase)); EmitAsm("EOR", Imm(0xFF)); EmitAsm("CLC"); EmitAsm("ADC", Imm(1)); EmitAsm("STA", Mem(CallArgBase));
            EmitAsm("LDA", Mem(CallArgBase + 1)); EmitAsm("EOR", Imm(0xFF)); EmitAsm("ADC", Imm(0)); EmitAsm("STA", Mem(CallArgBase + 1));
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp2Address)); EmitAsm("EOR", Imm(1)); EmitAsm("STA", Mem(_runtimeIntrinsicTmp2Address));
            _assembly.Add(Expr.Make(Tag.Label, aPos));
            string bPos = NewGeneratedLabel("smul_b_positive");
            EmitAsm("LDA", Mem(CallArgBase + 2));
            EmitAsm("AND", Imm(0x80));
            EmitAsm("BEQ", Rel(bPos));
            EmitAsm("LDA", Mem(CallArgBase + 2)); EmitAsm("EOR", Imm(0xFF)); EmitAsm("CLC"); EmitAsm("ADC", Imm(1)); EmitAsm("STA", Mem(CallArgBase + 2));
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp2Address)); EmitAsm("EOR", Imm(1)); EmitAsm("STA", Mem(_runtimeIntrinsicTmp2Address));
            _assembly.Add(Expr.Make(Tag.Label, bPos));
            EmitAsm("JSR", Abs("__mul16x8"));
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("STX", Mem(_runtimeIntrinsicTmp1Address));
            string sdone = NewGeneratedLabel("smul_done");
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp2Address));
            EmitAsm("BEQ", Rel(sdone));
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp0Address)); EmitAsm("EOR", Imm(0xFF)); EmitAsm("CLC"); EmitAsm("ADC", Imm(1)); EmitAsm("STA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp1Address)); EmitAsm("EOR", Imm(0xFF)); EmitAsm("ADC", Imm(0)); EmitAsm("STA", Mem(_runtimeIntrinsicTmp1Address));
            _assembly.Add(Expr.Make(Tag.Label, sdone));
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("LDX", Mem(_runtimeIntrinsicTmp1Address));
            EmitAsm("RTS");

            EmitMacHelper("__mac16", "__mul16x8");
            EmitMacHelper("__smac16", "__smul16x8");
            EmitDot2Helper("__dot2_q8_8", "__mul16x8");
            EmitDot3Helper("__dot3_q8_8", "__mul16x8");
            EmitDot2Helper("__sdot2_q8_8", "__smul16x8");
            EmitDot3Helper("__sdot3_q8_8", "__smul16x8");
        }
__sdot3_q1_7

Compute a signed three-term dot product.

Syntax

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

Parameters

a0

Signed 16-bit bit pattern, representing −32768..32767.

a1

Signed 16-bit bit pattern, representing −32768..32767.

a2

Signed 16-bit bit pattern, representing −32768..32767.

b0

Signed byte bit pattern, coefficient −128..127.

b1

Signed byte bit pattern, coefficient −128..127.

b2

Signed byte bit pattern, coefficient −128..127.

Return value

Returns the low 16 bits of a0*b0+a1*b1+a2*b2.

Conditions and limits

On FC, q8_8/q1_7 names still use raw integer products without a fixed-point downshift. The Q1.7 functions alias their corresponding signed operations. The caller handles units; signed results are returned as u16 bit patterns.

Usage example

c
// Compare literal coefficients with values read from RAM.
result[38]=__sdot3_q1_7(-513,769,-1025,-64,127,-3);
v0=-513;v1=769;v2=-1025;v3=-64;v4=127;v5=-3;
result[39]=__sdot3_q1_7(v0,v1,v2,v3,v4,v5);
// Reverse coefficient signs and verify negative products.
result[40]=__sdot3_q1_7(-513,769,-1025,64,-127,3);
v0=-513;v1=769;v2=-1025;v3=64;v4=-127;v5=3;
result[41]=__sdot3_q1_7(v0,v1,v2,v3,v4,v5);

This is a calling fragment. Follow the stated initialization and buffer requirements before using it in a complete program.

Expected result

Compare literal inputs with values stored in RAM. Representative results appear as unsigned 16-bit values; negatives use two’s-complement bit patterns. GB rescales fixed-point products, while FC returns integer products, so identical inputs need not give identical results.

The excerpt stores these 16-bit values: result[38]=2498, result[39]=2498, result[40]=63038, result[41]=63038. Each literal-input calculation matches its RAM-input counterpart. The screen shows representative values; the result array holds every result.

__sdot3_q1_7: NROM
NROM

Complete example program

Build command

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\batch3_math.c -I .\kitaqfc\lib -I .\kitaq-docs\samples -o .\out\batch3_math.nes --no-cache --no-disasm --mapper=nrom --nes-chr=.\kitaq-docs\samples\font.chr
Implementation and source

kitaqfc/lib/intrinsics.h:73

Declaration notes (original source)
FC alias of __sdot3_q8_8: three raw signed-pattern products, summed without rescaling.

kitaqfc/kitaqfc/CodeGenerator.cs:4659

csharp
case "__sdot3_q1_7": return EmitKitaqfcHelperCall(funcName, args, ctx, new[] {2,2,2,1,1,1}, funcName);
                // Pass a word seed to the RNG helper; the zero-argument generator is called directly below.

void EmitHelperMath()
        {
            // Consume eight multiplier bits and accumulate the low product word in shared temporaries, returning A/X.
            EmitHelperStart("__mul16x8");
            EmitAsm("LDA", Imm(0));
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp1Address));
            EmitAsm("LDX", Imm(8));
            string loop = NewGeneratedLabel("mul16x8_loop");
            string skip = NewGeneratedLabel("mul16x8_skip_add");
            _assembly.Add(Expr.Make(Tag.Label, loop));
            EmitAsm("LSR", Mem(CallArgBase + 2));
            EmitAsm("BCC", Rel(skip));
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("CLC");
            EmitAsm("ADC", Mem(CallArgBase));
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp1Address));
            EmitAsm("ADC", Mem(CallArgBase + 1));
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp1Address));
            _assembly.Add(Expr.Make(Tag.Label, skip));
            EmitAsm("ASL", Mem(CallArgBase));
            EmitAsm("ROL", Mem(CallArgBase + 1));
            EmitAsm("DEX");
            EmitAsm("BNE", Rel(loop));
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("LDX", Mem(_runtimeIntrinsicTmp1Address));
            EmitAsm("RTS");

            // Zero-extend the byte multiplicand, reuse word multiplication and return its high byte.
            EmitHelperStart("__mul8x8_hi");
            EmitAsm("LDA", Mem(CallArgBase + 1));
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp2Address));
            EmitAsm("LDA", Imm(0));
            EmitAsm("STA", Mem(CallArgBase + 1));
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp2Address));
            EmitAsm("STA", Mem(CallArgBase + 2));
            EmitAsm("JSR", Abs("__mul16x8"));
            EmitAsm("TXA");
            EmitAsm("RTS");

            // Convert signed operands to magnitudes, multiply, then restore the product sign modulo 65536.
            EmitHelperStart("__smul16x8");
            EmitAsm("LDA", Imm(0));
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp2Address));
            string aPos = NewGeneratedLabel("smul_a_positive");
            EmitAsm("LDA", Mem(CallArgBase + 1));
            EmitAsm("AND", Imm(0x80));
            EmitAsm("BEQ", Rel(aPos));
            EmitAsm("LDA", Mem(CallArgBase)); EmitAsm("EOR", Imm(0xFF)); EmitAsm("CLC"); EmitAsm("ADC", Imm(1)); EmitAsm("STA", Mem(CallArgBase));
            EmitAsm("LDA", Mem(CallArgBase + 1)); EmitAsm("EOR", Imm(0xFF)); EmitAsm("ADC", Imm(0)); EmitAsm("STA", Mem(CallArgBase + 1));
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp2Address)); EmitAsm("EOR", Imm(1)); EmitAsm("STA", Mem(_runtimeIntrinsicTmp2Address));
            _assembly.Add(Expr.Make(Tag.Label, aPos));
            string bPos = NewGeneratedLabel("smul_b_positive");
            EmitAsm("LDA", Mem(CallArgBase + 2));
            EmitAsm("AND", Imm(0x80));
            EmitAsm("BEQ", Rel(bPos));
            EmitAsm("LDA", Mem(CallArgBase + 2)); EmitAsm("EOR", Imm(0xFF)); EmitAsm("CLC"); EmitAsm("ADC", Imm(1)); EmitAsm("STA", Mem(CallArgBase + 2));
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp2Address)); EmitAsm("EOR", Imm(1)); EmitAsm("STA", Mem(_runtimeIntrinsicTmp2Address));
            _assembly.Add(Expr.Make(Tag.Label, bPos));
            EmitAsm("JSR", Abs("__mul16x8"));
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("STX", Mem(_runtimeIntrinsicTmp1Address));
            string sdone = NewGeneratedLabel("smul_done");
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp2Address));
            EmitAsm("BEQ", Rel(sdone));
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp0Address)); EmitAsm("EOR", Imm(0xFF)); EmitAsm("CLC"); EmitAsm("ADC", Imm(1)); EmitAsm("STA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp1Address)); EmitAsm("EOR", Imm(0xFF)); EmitAsm("ADC", Imm(0)); EmitAsm("STA", Mem(_runtimeIntrinsicTmp1Address));
            _assembly.Add(Expr.Make(Tag.Label, sdone));
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("LDX", Mem(_runtimeIntrinsicTmp1Address));
            EmitAsm("RTS");

            EmitMacHelper("__mac16", "__mul16x8");
            EmitMacHelper("__smac16", "__smul16x8");
            EmitDot2Helper("__dot2_q8_8", "__mul16x8");
            EmitDot3Helper("__dot3_q8_8", "__mul16x8");
            EmitDot2Helper("__sdot2_q8_8", "__smul16x8");
            EmitDot3Helper("__sdot3_q8_8", "__smul16x8");
        }
__sdot3_q8_8

Compute a signed three-term dot product.

Syntax

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

Parameters

a0

Signed 16-bit bit pattern, representing −32768..32767.

a1

Signed 16-bit bit pattern, representing −32768..32767.

a2

Signed 16-bit bit pattern, representing −32768..32767.

b0

Signed byte bit pattern, coefficient −128..127.

b1

Signed byte bit pattern, coefficient −128..127.

b2

Signed byte bit pattern, coefficient −128..127.

Return value

Returns the low 16 bits of a0*b0+a1*b1+a2*b2.

Conditions and limits

On FC, q8_8/q1_7 names still use raw integer products without a fixed-point downshift. The Q1.7 functions alias their corresponding signed operations. The caller handles units; signed results are returned as u16 bit patterns.

Usage example

c
// Compare literal coefficients with values read from RAM.
result[30]=__sdot3_q8_8(-513,769,-1025,-64,127,-3);
v0=-513;v1=769;v2=-1025;v3=-64;v4=127;v5=-3;
result[31]=__sdot3_q8_8(v0,v1,v2,v3,v4,v5);
// Reverse coefficient signs and verify negative products.
result[32]=__sdot3_q8_8(-513,769,-1025,64,-127,3);
v0=-513;v1=769;v2=-1025;v3=64;v4=-127;v5=3;
result[33]=__sdot3_q8_8(v0,v1,v2,v3,v4,v5);

This is a calling fragment. Follow the stated initialization and buffer requirements before using it in a complete program.

Expected result

Compare literal inputs with values stored in RAM. Representative results appear as unsigned 16-bit values; negatives use two’s-complement bit patterns. GB rescales fixed-point products, while FC returns integer products, so identical inputs need not give identical results.

The excerpt stores these 16-bit values: result[30]=2498, result[31]=2498, result[32]=63038, result[33]=63038. Each literal-input calculation matches its RAM-input counterpart. The screen shows representative values; the result array holds every result.

__sdot3_q8_8: NROM
NROM

Complete example program

Build command

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\batch3_math.c -I .\kitaqfc\lib -I .\kitaq-docs\samples -o .\out\batch3_math.nes --no-cache --no-disasm --mapper=nrom --nes-chr=.\kitaq-docs\samples\font.chr
Implementation and source

kitaqfc/lib/intrinsics.h:69

Declaration notes (original source)
Sum three signed-pattern products modulo 65536; choose operand units explicitly in the caller.

kitaqfc/kitaqfc/CodeGenerator.cs:4657

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

void EmitHelperMath()
        {
            // Consume eight multiplier bits and accumulate the low product word in shared temporaries, returning A/X.
            EmitHelperStart("__mul16x8");
            EmitAsm("LDA", Imm(0));
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp1Address));
            EmitAsm("LDX", Imm(8));
            string loop = NewGeneratedLabel("mul16x8_loop");
            string skip = NewGeneratedLabel("mul16x8_skip_add");
            _assembly.Add(Expr.Make(Tag.Label, loop));
            EmitAsm("LSR", Mem(CallArgBase + 2));
            EmitAsm("BCC", Rel(skip));
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("CLC");
            EmitAsm("ADC", Mem(CallArgBase));
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp1Address));
            EmitAsm("ADC", Mem(CallArgBase + 1));
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp1Address));
            _assembly.Add(Expr.Make(Tag.Label, skip));
            EmitAsm("ASL", Mem(CallArgBase));
            EmitAsm("ROL", Mem(CallArgBase + 1));
            EmitAsm("DEX");
            EmitAsm("BNE", Rel(loop));
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("LDX", Mem(_runtimeIntrinsicTmp1Address));
            EmitAsm("RTS");

            // Zero-extend the byte multiplicand, reuse word multiplication and return its high byte.
            EmitHelperStart("__mul8x8_hi");
            EmitAsm("LDA", Mem(CallArgBase + 1));
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp2Address));
            EmitAsm("LDA", Imm(0));
            EmitAsm("STA", Mem(CallArgBase + 1));
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp2Address));
            EmitAsm("STA", Mem(CallArgBase + 2));
            EmitAsm("JSR", Abs("__mul16x8"));
            EmitAsm("TXA");
            EmitAsm("RTS");

            // Convert signed operands to magnitudes, multiply, then restore the product sign modulo 65536.
            EmitHelperStart("__smul16x8");
            EmitAsm("LDA", Imm(0));
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp2Address));
            string aPos = NewGeneratedLabel("smul_a_positive");
            EmitAsm("LDA", Mem(CallArgBase + 1));
            EmitAsm("AND", Imm(0x80));
            EmitAsm("BEQ", Rel(aPos));
            EmitAsm("LDA", Mem(CallArgBase)); EmitAsm("EOR", Imm(0xFF)); EmitAsm("CLC"); EmitAsm("ADC", Imm(1)); EmitAsm("STA", Mem(CallArgBase));
            EmitAsm("LDA", Mem(CallArgBase + 1)); EmitAsm("EOR", Imm(0xFF)); EmitAsm("ADC", Imm(0)); EmitAsm("STA", Mem(CallArgBase + 1));
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp2Address)); EmitAsm("EOR", Imm(1)); EmitAsm("STA", Mem(_runtimeIntrinsicTmp2Address));
            _assembly.Add(Expr.Make(Tag.Label, aPos));
            string bPos = NewGeneratedLabel("smul_b_positive");
            EmitAsm("LDA", Mem(CallArgBase + 2));
            EmitAsm("AND", Imm(0x80));
            EmitAsm("BEQ", Rel(bPos));
            EmitAsm("LDA", Mem(CallArgBase + 2)); EmitAsm("EOR", Imm(0xFF)); EmitAsm("CLC"); EmitAsm("ADC", Imm(1)); EmitAsm("STA", Mem(CallArgBase + 2));
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp2Address)); EmitAsm("EOR", Imm(1)); EmitAsm("STA", Mem(_runtimeIntrinsicTmp2Address));
            _assembly.Add(Expr.Make(Tag.Label, bPos));
            EmitAsm("JSR", Abs("__mul16x8"));
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("STX", Mem(_runtimeIntrinsicTmp1Address));
            string sdone = NewGeneratedLabel("smul_done");
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp2Address));
            EmitAsm("BEQ", Rel(sdone));
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp0Address)); EmitAsm("EOR", Imm(0xFF)); EmitAsm("CLC"); EmitAsm("ADC", Imm(1)); EmitAsm("STA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp1Address)); EmitAsm("EOR", Imm(0xFF)); EmitAsm("ADC", Imm(0)); EmitAsm("STA", Mem(_runtimeIntrinsicTmp1Address));
            _assembly.Add(Expr.Make(Tag.Label, sdone));
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("LDX", Mem(_runtimeIntrinsicTmp1Address));
            EmitAsm("RTS");

            EmitMacHelper("__mac16", "__mul16x8");
            EmitMacHelper("__smac16", "__smul16x8");
            EmitDot2Helper("__dot2_q8_8", "__mul16x8");
            EmitDot3Helper("__dot3_q8_8", "__mul16x8");
            EmitDot2Helper("__sdot2_q8_8", "__smul16x8");
            EmitDot3Helper("__sdot3_q8_8", "__smul16x8");
        }
__serial_rx_bit

Sample $4017 bit 4 once as the receive line supplied by an external adapter.

Syntax

c
u8 __serial_rx_bit(void);

Return value

1 when bit 4 is set, otherwise 0. This is not a data-ready or communication-error status.

Conditions and limits

These APIs access $4016/$4017 directly. Do not interleave interrupt-driven gamepad, keyboard or serial operations on the same ports. Controller shift positions can also be affected. The sample disables NMI while capturing input, then enables it when initializing the display.

These low-level primitives assume an external adapter mapping OUT0 to TX and $4017 D4 to RX. They add no bit interval, baud clock, start/stop bits, byte assembly or receive wait. Their role differs from byte-oriented helpers such as __midi_out().

The caller must sample at the appropriate time. This neither waits for data nor assembles multiple bits, and it does not change the $4016 output.

Bit conversion is also unit-tested in an owned test ROM by redirecting register reads to RAM fixture values. The tests cover 00/08/10/18 combinations with unrelated bits set. This unit test is separate from the unmodified sample run and from real peripheral behavior.

These images run the downloadable ROMs unchanged in KUROSAKI. Its current input bus models gamepads but not keyboard, light-gun or serial-adapter signals. The external input bits read as zero, so the captures do not verify those devices. Physical key input, optical sensing and communication remain untested.

Usage example

c
#include "fc_ports_scene.h"
__prg_rom u8 tx_values[6]={0,1,2,3,254,255};
void main(void){
    u8 i;
    __irq_disable();__ppu_ctrl_set(0);port_phase=1;
    for(i=0;i<6;i++)__serial_tx_bit(tx_values[i]);
    __serial_tx_bit(1); // Leave OUT0 high; no byte framing or baud clock is added.
    port_result[0]=__serial_rx_bit();port_phase=2;port_result[31]=165;
    m_init();m_text(2,2,"SERIAL BIT PRIMITIVES");m_wait();
    m_text(2,5,"VALUE       EXPECTED OUT0");m_wait();
    for(i=0;i<6;i++){port_number(3,7+i*2,tx_values[i]);m_put(23,7+i*2,'0'+(tx_values[i]&1));m_wait();}
    m_text(2,20,"RX SAMPLE");port_number(23,20,port_result[0]);m_wait();
    m_text(2,23,"NO BYTE FRAMING OR CLOCK");m_wait();
    while(1){m_wait();}
}

This is a calling fragment. Follow the stated initialization and buffer requirements before using it in a complete program.

Expected result

For VALUE 000/001/002/003/254/255, EXPECTED OUT0 is 0/1/0/1/0/1. This column explains the low-bit mapping; it is not electrical readback. CPU traces confirm those writes followed by a final one. RX SAMPLE is 000 because KUROSAKI does not model the receive line.

__serial_rx_bit: SERIAL-DEFAULT — For VALUE 000/001/002/003/254/255, EXPECTED OUT0 is 0/1/0/1/0/1. This column explains the low-bit mapping; it is not electrical readback. CPU traces confirm those writes followed by a final one. RX SAMPLE is 000 because KUROSAKI does not model the receive line.
SERIAL-DEFAULT — For VALUE 000/001/002/003/254/255, EXPECTED OUT0 is 0/1/0/1/0/1. This column explains the low-bit mapping; it is not electrical readback. CPU traces confirm those writes followed by a final one. RX SAMPLE is 000 because KUROSAKI does not model the receive line.

Complete example program

Build command

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\peripheral_serial.c -I .\kitaqfc\lib -I .\kitaq-docs\samples -o .\out\peripheral_serial.nes --mapper=nrom --nes-chr=.\kitaq-docs\samples\font.chr --no-cache --no-disasm
.\kurosaki\kurosaki.exe run .\out\peripheral_serial.nes --frames 120 --png .\out\peripheral_serial.png
Implementation and source

kitaqfc/lib/intrinsics.h:288

Declaration notes (original source)
Read 4017 bit 4 and normalize it to 0 or 1; an external serial adapter supplies the signal.

kitaqfc/kitaqfc/CodeGenerator.cs:7965

csharp
EmitHelperStart("__serial_rx_bit");
            EmitAsm("LDA", Mem(0x4017));
            EmitAsm("AND", Imm(0x10));
            string rx0 = NewGeneratedLabel("serial_rx0");
            string rxd = NewGeneratedLabel("serial_rxdone");
            EmitAsm("BEQ", Rel(rx0));
            EmitAsm("LDA", Imm(1));
            EmitAsm("JMP", Abs(rxd));
            _assembly.Add(Expr.Make(Tag.Label, rx0));
            EmitAsm("LDA", Imm(0));
            _assembly.Add(Expr.Make(Tag.Label, rxd));
            EmitAsm("RTS");
__serial_tx_bit

Write the argument’s low bit to OUT0 through $4016, providing a one-bit output operation for an external serial adapter.

Syntax

c
void __serial_tx_bit(u8 bit);

Parameters

bit

Value to output. Only bit & 1 is used: 2 outputs zero, while 255 outputs one.

Return value

No return value.

Conditions and limits

These APIs access $4016/$4017 directly. Do not interleave interrupt-driven gamepad, keyboard or serial operations on the same ports. Controller shift positions can also be affected. The sample disables NMI while capturing input, then enables it when initializing the display.

These low-level primitives assume an external adapter mapping OUT0 to TX and $4017 D4 to RX. They add no bit interval, baud clock, start/stop bits, byte assembly or receive wait. Their role differs from byte-oriented helpers such as __midi_out().

Writes the entire $4016 value as zero or one, clearing OUT1/OUT2. No hold delay is inserted. OUT0 controls the normal gamepad latch, so this operation changes gamepad reading state too.

These images run the downloadable ROMs unchanged in KUROSAKI. Its current input bus models gamepads but not keyboard, light-gun or serial-adapter signals. The external input bits read as zero, so the captures do not verify those devices. Physical key input, optical sensing and communication remain untested.

Usage example

c
#include "fc_ports_scene.h"
__prg_rom u8 tx_values[6]={0,1,2,3,254,255};
void main(void){
    u8 i;
    __irq_disable();__ppu_ctrl_set(0);port_phase=1;
    for(i=0;i<6;i++)__serial_tx_bit(tx_values[i]);
    __serial_tx_bit(1); // Leave OUT0 high; no byte framing or baud clock is added.
    port_result[0]=__serial_rx_bit();port_phase=2;port_result[31]=165;
    m_init();m_text(2,2,"SERIAL BIT PRIMITIVES");m_wait();
    m_text(2,5,"VALUE       EXPECTED OUT0");m_wait();
    for(i=0;i<6;i++){port_number(3,7+i*2,tx_values[i]);m_put(23,7+i*2,'0'+(tx_values[i]&1));m_wait();}
    m_text(2,20,"RX SAMPLE");port_number(23,20,port_result[0]);m_wait();
    m_text(2,23,"NO BYTE FRAMING OR CLOCK");m_wait();
    while(1){m_wait();}
}

This is a calling fragment. Follow the stated initialization and buffer requirements before using it in a complete program.

Expected result

For VALUE 000/001/002/003/254/255, EXPECTED OUT0 is 0/1/0/1/0/1. This column explains the low-bit mapping; it is not electrical readback. CPU traces confirm those writes followed by a final one. RX SAMPLE is 000 because KUROSAKI does not model the receive line.

__serial_tx_bit: SERIAL-DEFAULT — For VALUE 000/001/002/003/254/255, EXPECTED OUT0 is 0/1/0/1/0/1. This column explains the low-bit mapping; it is not electrical readback. CPU traces confirm those writes followed by a final one. RX SAMPLE is 000 because KUROSAKI does not model the receive line.
SERIAL-DEFAULT — For VALUE 000/001/002/003/254/255, EXPECTED OUT0 is 0/1/0/1/0/1. This column explains the low-bit mapping; it is not electrical readback. CPU traces confirm those writes followed by a final one. RX SAMPLE is 000 because KUROSAKI does not model the receive line.

Complete example program

Build command

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\peripheral_serial.c -I .\kitaqfc\lib -I .\kitaq-docs\samples -o .\out\peripheral_serial.nes --mapper=nrom --nes-chr=.\kitaq-docs\samples\font.chr --no-cache --no-disasm
.\kurosaki\kurosaki.exe run .\out\peripheral_serial.nes --frames 120 --png .\out\peripheral_serial.png
Implementation and source

kitaqfc/lib/intrinsics.h:286

Declaration notes (original source)
Expansion-port TTL serial / MIDI adapter support.
The low-level serial layer assumes an external adapter that maps OUT0 to TX
and D4 on $4017 to RX. MIDI helpers bit-bang 8N1 bytes at an approximate
31250 bps timing suitable for adapter bring-up and simple music sync.

Write bit & 1 to 4016, replacing the port output value; coordinate with other port users.

kitaqfc/kitaqfc/CodeGenerator.cs:7958

csharp
EmitHelperStart("__serial_tx_bit");
            EmitAsm("LDA", Mem(CallArgBase));
            EmitAsm("AND", Imm(1));
            EmitAsm("STA", Mem(0x4016));
            EmitAsm("RTS");
__smac16

Compute a signed multiply-accumulate.

Syntax

c
u16 __smac16(u16 acc, u16 a, u8 b);

Parameters

acc

16-bit accumulator to add.

a

Signed 16-bit bit pattern, representing −32768..32767.

b

Signed byte bit pattern, coefficient −128..127.

Return value

Returns the low 16 bits of acc+a*b.

Conditions and limits

On FC, q8_8/q1_7 names still use raw integer products without a fixed-point downshift. The Q1.7 functions alias their corresponding signed operations. The caller handles units; signed results are returned as u16 bit patterns.

Usage example

c
// Compare literal coefficients with values read from RAM.
result[10]=__smac16(65500,-513,-64);
v0=65500;v1=-513;v2=-64;
result[11]=__smac16(v0,v1,v2);
// Reverse coefficient signs and verify negative products.
result[12]=__smac16(65500,-513,64);
v0=65500;v1=-513;v2=64;
result[13]=__smac16(v0,v1,v2);

This is a calling fragment. Follow the stated initialization and buffer requirements before using it in a complete program.

Expected result

Compare literal inputs with values stored in RAM. Representative results appear as unsigned 16-bit values; negatives use two’s-complement bit patterns. GB rescales fixed-point products, while FC returns integer products, so identical inputs need not give identical results.

The excerpt stores these 16-bit values: result[10]=32796, result[11]=32796, result[12]=32668, result[13]=32668. Each literal-input calculation matches its RAM-input counterpart. The screen shows representative values; the result array holds every result.

__smac16: NROM
NROM

Complete example program

Build command

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\batch3_math.c -I .\kitaqfc\lib -I .\kitaq-docs\samples -o .\out\batch3_math.nes --no-cache --no-disasm --mapper=nrom --nes-chr=.\kitaq-docs\samples\font.chr
Implementation and source

kitaqfc/lib/intrinsics.h:57

Declaration notes (original source)
Add the signed-pattern product to acc modulo 65536; the API carries the result as u16 bits.

kitaqfc/kitaqfc/CodeGenerator.cs:4651

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

void EmitHelperMath()
        {
            // Consume eight multiplier bits and accumulate the low product word in shared temporaries, returning A/X.
            EmitHelperStart("__mul16x8");
            EmitAsm("LDA", Imm(0));
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp1Address));
            EmitAsm("LDX", Imm(8));
            string loop = NewGeneratedLabel("mul16x8_loop");
            string skip = NewGeneratedLabel("mul16x8_skip_add");
            _assembly.Add(Expr.Make(Tag.Label, loop));
            EmitAsm("LSR", Mem(CallArgBase + 2));
            EmitAsm("BCC", Rel(skip));
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("CLC");
            EmitAsm("ADC", Mem(CallArgBase));
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp1Address));
            EmitAsm("ADC", Mem(CallArgBase + 1));
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp1Address));
            _assembly.Add(Expr.Make(Tag.Label, skip));
            EmitAsm("ASL", Mem(CallArgBase));
            EmitAsm("ROL", Mem(CallArgBase + 1));
            EmitAsm("DEX");
            EmitAsm("BNE", Rel(loop));
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("LDX", Mem(_runtimeIntrinsicTmp1Address));
            EmitAsm("RTS");

            // Zero-extend the byte multiplicand, reuse word multiplication and return its high byte.
            EmitHelperStart("__mul8x8_hi");
            EmitAsm("LDA", Mem(CallArgBase + 1));
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp2Address));
            EmitAsm("LDA", Imm(0));
            EmitAsm("STA", Mem(CallArgBase + 1));
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp2Address));
            EmitAsm("STA", Mem(CallArgBase + 2));
            EmitAsm("JSR", Abs("__mul16x8"));
            EmitAsm("TXA");
            EmitAsm("RTS");

            // Convert signed operands to magnitudes, multiply, then restore the product sign modulo 65536.
            EmitHelperStart("__smul16x8");
            EmitAsm("LDA", Imm(0));
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp2Address));
            string aPos = NewGeneratedLabel("smul_a_positive");
            EmitAsm("LDA", Mem(CallArgBase + 1));
            EmitAsm("AND", Imm(0x80));
            EmitAsm("BEQ", Rel(aPos));
            EmitAsm("LDA", Mem(CallArgBase)); EmitAsm("EOR", Imm(0xFF)); EmitAsm("CLC"); EmitAsm("ADC", Imm(1)); EmitAsm("STA", Mem(CallArgBase));
            EmitAsm("LDA", Mem(CallArgBase + 1)); EmitAsm("EOR", Imm(0xFF)); EmitAsm("ADC", Imm(0)); EmitAsm("STA", Mem(CallArgBase + 1));
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp2Address)); EmitAsm("EOR", Imm(1)); EmitAsm("STA", Mem(_runtimeIntrinsicTmp2Address));
            _assembly.Add(Expr.Make(Tag.Label, aPos));
            string bPos = NewGeneratedLabel("smul_b_positive");
            EmitAsm("LDA", Mem(CallArgBase + 2));
            EmitAsm("AND", Imm(0x80));
            EmitAsm("BEQ", Rel(bPos));
            EmitAsm("LDA", Mem(CallArgBase + 2)); EmitAsm("EOR", Imm(0xFF)); EmitAsm("CLC"); EmitAsm("ADC", Imm(1)); EmitAsm("STA", Mem(CallArgBase + 2));
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp2Address)); EmitAsm("EOR", Imm(1)); EmitAsm("STA", Mem(_runtimeIntrinsicTmp2Address));
            _assembly.Add(Expr.Make(Tag.Label, bPos));
            EmitAsm("JSR", Abs("__mul16x8"));
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("STX", Mem(_runtimeIntrinsicTmp1Address));
            string sdone = NewGeneratedLabel("smul_done");
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp2Address));
            EmitAsm("BEQ", Rel(sdone));
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp0Address)); EmitAsm("EOR", Imm(0xFF)); EmitAsm("CLC"); EmitAsm("ADC", Imm(1)); EmitAsm("STA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp1Address)); EmitAsm("EOR", Imm(0xFF)); EmitAsm("ADC", Imm(0)); EmitAsm("STA", Mem(_runtimeIntrinsicTmp1Address));
            _assembly.Add(Expr.Make(Tag.Label, sdone));
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("LDX", Mem(_runtimeIntrinsicTmp1Address));
            EmitAsm("RTS");

            EmitMacHelper("__mac16", "__mul16x8");
            EmitMacHelper("__smac16", "__smul16x8");
            EmitDot2Helper("__dot2_q8_8", "__mul16x8");
            EmitDot3Helper("__dot3_q8_8", "__mul16x8");
            EmitDot2Helper("__sdot2_q8_8", "__smul16x8");
            EmitDot3Helper("__sdot3_q8_8", "__smul16x8");
        }
__smac16_q1_7

Compute a signed multiply-accumulate.

Syntax

c
u16 __smac16_q1_7(u16 acc, u16 a, u8 b);

Parameters

acc

16-bit accumulator to add.

a

Signed 16-bit bit pattern, representing −32768..32767.

b

Signed byte bit pattern, coefficient −128..127.

Return value

Returns the low 16 bits of acc+a*b.

Conditions and limits

On FC, q8_8/q1_7 names still use raw integer products without a fixed-point downshift. The Q1.7 functions alias their corresponding signed operations. The caller handles units; signed results are returned as u16 bit patterns.

Usage example

c
// Compare literal coefficients with values read from RAM.
result[18]=__smac16_q1_7(65500,-513,-64);
v0=65500;v1=-513;v2=-64;
result[19]=__smac16_q1_7(v0,v1,v2);
// Reverse coefficient signs and verify negative products.
result[20]=__smac16_q1_7(65500,-513,64);
v0=65500;v1=-513;v2=64;
result[21]=__smac16_q1_7(v0,v1,v2);

This is a calling fragment. Follow the stated initialization and buffer requirements before using it in a complete program.

Expected result

Compare literal inputs with values stored in RAM. Representative results appear as unsigned 16-bit values; negatives use two’s-complement bit patterns. GB rescales fixed-point products, while FC returns integer products, so identical inputs need not give identical results.

The excerpt stores these 16-bit values: result[18]=32796, result[19]=32796, result[20]=32668, result[21]=32668. Each literal-input calculation matches its RAM-input counterpart. The screen shows representative values; the result array holds every result.

__smac16_q1_7: NROM
NROM

Complete example program

Build command

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\batch3_math.c -I .\kitaqfc\lib -I .\kitaq-docs\samples -o .\out\batch3_math.nes --no-cache --no-disasm --mapper=nrom --nes-chr=.\kitaq-docs\samples\font.chr
Implementation and source

kitaqfc/lib/intrinsics.h:61

Declaration notes (original source)
FC alias of __smac16: accumulate a raw signed-pattern product modulo 65536.

kitaqfc/kitaqfc/CodeGenerator.cs:4653

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

void EmitHelperMath()
        {
            // Consume eight multiplier bits and accumulate the low product word in shared temporaries, returning A/X.
            EmitHelperStart("__mul16x8");
            EmitAsm("LDA", Imm(0));
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp1Address));
            EmitAsm("LDX", Imm(8));
            string loop = NewGeneratedLabel("mul16x8_loop");
            string skip = NewGeneratedLabel("mul16x8_skip_add");
            _assembly.Add(Expr.Make(Tag.Label, loop));
            EmitAsm("LSR", Mem(CallArgBase + 2));
            EmitAsm("BCC", Rel(skip));
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("CLC");
            EmitAsm("ADC", Mem(CallArgBase));
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp1Address));
            EmitAsm("ADC", Mem(CallArgBase + 1));
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp1Address));
            _assembly.Add(Expr.Make(Tag.Label, skip));
            EmitAsm("ASL", Mem(CallArgBase));
            EmitAsm("ROL", Mem(CallArgBase + 1));
            EmitAsm("DEX");
            EmitAsm("BNE", Rel(loop));
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("LDX", Mem(_runtimeIntrinsicTmp1Address));
            EmitAsm("RTS");

            // Zero-extend the byte multiplicand, reuse word multiplication and return its high byte.
            EmitHelperStart("__mul8x8_hi");
            EmitAsm("LDA", Mem(CallArgBase + 1));
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp2Address));
            EmitAsm("LDA", Imm(0));
            EmitAsm("STA", Mem(CallArgBase + 1));
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp2Address));
            EmitAsm("STA", Mem(CallArgBase + 2));
            EmitAsm("JSR", Abs("__mul16x8"));
            EmitAsm("TXA");
            EmitAsm("RTS");

            // Convert signed operands to magnitudes, multiply, then restore the product sign modulo 65536.
            EmitHelperStart("__smul16x8");
            EmitAsm("LDA", Imm(0));
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp2Address));
            string aPos = NewGeneratedLabel("smul_a_positive");
            EmitAsm("LDA", Mem(CallArgBase + 1));
            EmitAsm("AND", Imm(0x80));
            EmitAsm("BEQ", Rel(aPos));
            EmitAsm("LDA", Mem(CallArgBase)); EmitAsm("EOR", Imm(0xFF)); EmitAsm("CLC"); EmitAsm("ADC", Imm(1)); EmitAsm("STA", Mem(CallArgBase));
            EmitAsm("LDA", Mem(CallArgBase + 1)); EmitAsm("EOR", Imm(0xFF)); EmitAsm("ADC", Imm(0)); EmitAsm("STA", Mem(CallArgBase + 1));
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp2Address)); EmitAsm("EOR", Imm(1)); EmitAsm("STA", Mem(_runtimeIntrinsicTmp2Address));
            _assembly.Add(Expr.Make(Tag.Label, aPos));
            string bPos = NewGeneratedLabel("smul_b_positive");
            EmitAsm("LDA", Mem(CallArgBase + 2));
            EmitAsm("AND", Imm(0x80));
            EmitAsm("BEQ", Rel(bPos));
            EmitAsm("LDA", Mem(CallArgBase + 2)); EmitAsm("EOR", Imm(0xFF)); EmitAsm("CLC"); EmitAsm("ADC", Imm(1)); EmitAsm("STA", Mem(CallArgBase + 2));
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp2Address)); EmitAsm("EOR", Imm(1)); EmitAsm("STA", Mem(_runtimeIntrinsicTmp2Address));
            _assembly.Add(Expr.Make(Tag.Label, bPos));
            EmitAsm("JSR", Abs("__mul16x8"));
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("STX", Mem(_runtimeIntrinsicTmp1Address));
            string sdone = NewGeneratedLabel("smul_done");
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp2Address));
            EmitAsm("BEQ", Rel(sdone));
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp0Address)); EmitAsm("EOR", Imm(0xFF)); EmitAsm("CLC"); EmitAsm("ADC", Imm(1)); EmitAsm("STA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp1Address)); EmitAsm("EOR", Imm(0xFF)); EmitAsm("ADC", Imm(0)); EmitAsm("STA", Mem(_runtimeIntrinsicTmp1Address));
            _assembly.Add(Expr.Make(Tag.Label, sdone));
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("LDX", Mem(_runtimeIntrinsicTmp1Address));
            EmitAsm("RTS");

            EmitMacHelper("__mac16", "__mul16x8");
            EmitMacHelper("__smac16", "__smul16x8");
            EmitDot2Helper("__dot2_q8_8", "__mul16x8");
            EmitDot3Helper("__dot3_q8_8", "__mul16x8");
            EmitDot2Helper("__sdot2_q8_8", "__smul16x8");
            EmitDot3Helper("__sdot3_q8_8", "__smul16x8");
        }
__smul16x8

Compute a signed multiplication.

Syntax

c
u16 __smul16x8(u16 a, u8 b);

Parameters

a

Signed 16-bit bit pattern, representing −32768..32767.

b

Signed byte bit pattern, coefficient −128..127.

Return value

Returns the low 16 bits of a*b.

Conditions and limits

On FC, q8_8/q1_7 names still use raw integer products without a fixed-point downshift. The Q1.7 functions alias their corresponding signed operations. The caller handles units; signed results are returned as u16 bit patterns.

Usage example

c
// Compare literal coefficients with values read from RAM.
result[4]=__smul16x8(-513,-64);
v0=-513;v1=-64;
result[5]=__smul16x8(v0,v1);
// Reverse coefficient signs and verify negative products.
result[6]=__smul16x8(-513,64);
v0=-513;v1=64;
result[7]=__smul16x8(v0,v1);

This is a calling fragment. Follow the stated initialization and buffer requirements before using it in a complete program.

Expected result

Compare literal inputs with values stored in RAM. Representative results appear as unsigned 16-bit values; negatives use two’s-complement bit patterns. GB rescales fixed-point products, while FC returns integer products, so identical inputs need not give identical results.

The excerpt stores these 16-bit values: result[4]=32832, result[5]=32832, result[6]=32704, result[7]=32704. Each literal-input calculation matches its RAM-input counterpart. The screen shows representative values; the result array holds every result.

__smul16x8: NROM
NROM

Complete example program

Build command

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\batch3_math.c -I .\kitaqfc\lib -I .\kitaq-docs\samples -o .\out\batch3_math.nes --no-cache --no-disasm --mapper=nrom --nes-chr=.\kitaq-docs\samples\font.chr
Implementation and source

kitaqfc/lib/intrinsics.h:53

Declaration notes (original source)
Interpret a and b as signed two's-complement bit patterns and return the low word of their product.

kitaqfc/kitaqfc/CodeGenerator.cs:4648

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

void EmitHelperMath()
        {
            // Consume eight multiplier bits and accumulate the low product word in shared temporaries, returning A/X.
            EmitHelperStart("__mul16x8");
            EmitAsm("LDA", Imm(0));
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp1Address));
            EmitAsm("LDX", Imm(8));
            string loop = NewGeneratedLabel("mul16x8_loop");
            string skip = NewGeneratedLabel("mul16x8_skip_add");
            _assembly.Add(Expr.Make(Tag.Label, loop));
            EmitAsm("LSR", Mem(CallArgBase + 2));
            EmitAsm("BCC", Rel(skip));
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("CLC");
            EmitAsm("ADC", Mem(CallArgBase));
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp1Address));
            EmitAsm("ADC", Mem(CallArgBase + 1));
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp1Address));
            _assembly.Add(Expr.Make(Tag.Label, skip));
            EmitAsm("ASL", Mem(CallArgBase));
            EmitAsm("ROL", Mem(CallArgBase + 1));
            EmitAsm("DEX");
            EmitAsm("BNE", Rel(loop));
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("LDX", Mem(_runtimeIntrinsicTmp1Address));
            EmitAsm("RTS");

            // Zero-extend the byte multiplicand, reuse word multiplication and return its high byte.
            EmitHelperStart("__mul8x8_hi");
            EmitAsm("LDA", Mem(CallArgBase + 1));
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp2Address));
            EmitAsm("LDA", Imm(0));
            EmitAsm("STA", Mem(CallArgBase + 1));
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp2Address));
            EmitAsm("STA", Mem(CallArgBase + 2));
            EmitAsm("JSR", Abs("__mul16x8"));
            EmitAsm("TXA");
            EmitAsm("RTS");

            // Convert signed operands to magnitudes, multiply, then restore the product sign modulo 65536.
            EmitHelperStart("__smul16x8");
            EmitAsm("LDA", Imm(0));
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp2Address));
            string aPos = NewGeneratedLabel("smul_a_positive");
            EmitAsm("LDA", Mem(CallArgBase + 1));
            EmitAsm("AND", Imm(0x80));
            EmitAsm("BEQ", Rel(aPos));
            EmitAsm("LDA", Mem(CallArgBase)); EmitAsm("EOR", Imm(0xFF)); EmitAsm("CLC"); EmitAsm("ADC", Imm(1)); EmitAsm("STA", Mem(CallArgBase));
            EmitAsm("LDA", Mem(CallArgBase + 1)); EmitAsm("EOR", Imm(0xFF)); EmitAsm("ADC", Imm(0)); EmitAsm("STA", Mem(CallArgBase + 1));
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp2Address)); EmitAsm("EOR", Imm(1)); EmitAsm("STA", Mem(_runtimeIntrinsicTmp2Address));
            _assembly.Add(Expr.Make(Tag.Label, aPos));
            string bPos = NewGeneratedLabel("smul_b_positive");
            EmitAsm("LDA", Mem(CallArgBase + 2));
            EmitAsm("AND", Imm(0x80));
            EmitAsm("BEQ", Rel(bPos));
            EmitAsm("LDA", Mem(CallArgBase + 2)); EmitAsm("EOR", Imm(0xFF)); EmitAsm("CLC"); EmitAsm("ADC", Imm(1)); EmitAsm("STA", Mem(CallArgBase + 2));
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp2Address)); EmitAsm("EOR", Imm(1)); EmitAsm("STA", Mem(_runtimeIntrinsicTmp2Address));
            _assembly.Add(Expr.Make(Tag.Label, bPos));
            EmitAsm("JSR", Abs("__mul16x8"));
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("STX", Mem(_runtimeIntrinsicTmp1Address));
            string sdone = NewGeneratedLabel("smul_done");
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp2Address));
            EmitAsm("BEQ", Rel(sdone));
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp0Address)); EmitAsm("EOR", Imm(0xFF)); EmitAsm("CLC"); EmitAsm("ADC", Imm(1)); EmitAsm("STA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp1Address)); EmitAsm("EOR", Imm(0xFF)); EmitAsm("ADC", Imm(0)); EmitAsm("STA", Mem(_runtimeIntrinsicTmp1Address));
            _assembly.Add(Expr.Make(Tag.Label, sdone));
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("LDX", Mem(_runtimeIntrinsicTmp1Address));
            EmitAsm("RTS");

            EmitMacHelper("__mac16", "__mul16x8");
            EmitMacHelper("__smac16", "__smul16x8");
            EmitDot2Helper("__dot2_q8_8", "__mul16x8");
            EmitDot3Helper("__dot3_q8_8", "__mul16x8");
            EmitDot2Helper("__sdot2_q8_8", "__smul16x8");
            EmitDot3Helper("__sdot3_q8_8", "__smul16x8");
        }
__smul16x8_q1_7

Compute a signed multiplication.

Syntax

c
u16 __smul16x8_q1_7(u16 a, u8 b);

Parameters

a

Signed 16-bit bit pattern, representing −32768..32767.

b

Signed byte bit pattern, coefficient −128..127.

Return value

Returns the low 16 bits of a*b.

Conditions and limits

On FC, q8_8/q1_7 names still use raw integer products without a fixed-point downshift. The Q1.7 functions alias their corresponding signed operations. The caller handles units; signed results are returned as u16 bit patterns.

Usage example

c
// Compare literal coefficients with values read from RAM.
result[14]=__smul16x8_q1_7(-513,-64);
v0=-513;v1=-64;
result[15]=__smul16x8_q1_7(v0,v1);
// Reverse coefficient signs and verify negative products.
result[16]=__smul16x8_q1_7(-513,64);
v0=-513;v1=64;
result[17]=__smul16x8_q1_7(v0,v1);

This is a calling fragment. Follow the stated initialization and buffer requirements before using it in a complete program.

Expected result

Compare literal inputs with values stored in RAM. Representative results appear as unsigned 16-bit values; negatives use two’s-complement bit patterns. GB rescales fixed-point products, while FC returns integer products, so identical inputs need not give identical results.

The excerpt stores these 16-bit values: result[14]=32832, result[15]=32832, result[16]=32704, result[17]=32704. Each literal-input calculation matches its RAM-input counterpart. The screen shows representative values; the result array holds every result.

__smul16x8_q1_7: NROM
NROM

Complete example program

Build command

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\batch3_math.c -I .\kitaqfc\lib -I .\kitaq-docs\samples -o .\out\batch3_math.nes --no-cache --no-disasm --mapper=nrom --nes-chr=.\kitaq-docs\samples\font.chr
Implementation and source

kitaqfc/lib/intrinsics.h:59

Declaration notes (original source)
FC alias of __smul16x8: signed-pattern raw product, with no Q1.7 scaling shift.

kitaqfc/kitaqfc/CodeGenerator.cs:4652

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

void EmitHelperMath()
        {
            // Consume eight multiplier bits and accumulate the low product word in shared temporaries, returning A/X.
            EmitHelperStart("__mul16x8");
            EmitAsm("LDA", Imm(0));
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp1Address));
            EmitAsm("LDX", Imm(8));
            string loop = NewGeneratedLabel("mul16x8_loop");
            string skip = NewGeneratedLabel("mul16x8_skip_add");
            _assembly.Add(Expr.Make(Tag.Label, loop));
            EmitAsm("LSR", Mem(CallArgBase + 2));
            EmitAsm("BCC", Rel(skip));
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("CLC");
            EmitAsm("ADC", Mem(CallArgBase));
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp1Address));
            EmitAsm("ADC", Mem(CallArgBase + 1));
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp1Address));
            _assembly.Add(Expr.Make(Tag.Label, skip));
            EmitAsm("ASL", Mem(CallArgBase));
            EmitAsm("ROL", Mem(CallArgBase + 1));
            EmitAsm("DEX");
            EmitAsm("BNE", Rel(loop));
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("LDX", Mem(_runtimeIntrinsicTmp1Address));
            EmitAsm("RTS");

            // Zero-extend the byte multiplicand, reuse word multiplication and return its high byte.
            EmitHelperStart("__mul8x8_hi");
            EmitAsm("LDA", Mem(CallArgBase + 1));
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp2Address));
            EmitAsm("LDA", Imm(0));
            EmitAsm("STA", Mem(CallArgBase + 1));
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp2Address));
            EmitAsm("STA", Mem(CallArgBase + 2));
            EmitAsm("JSR", Abs("__mul16x8"));
            EmitAsm("TXA");
            EmitAsm("RTS");

            // Convert signed operands to magnitudes, multiply, then restore the product sign modulo 65536.
            EmitHelperStart("__smul16x8");
            EmitAsm("LDA", Imm(0));
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp2Address));
            string aPos = NewGeneratedLabel("smul_a_positive");
            EmitAsm("LDA", Mem(CallArgBase + 1));
            EmitAsm("AND", Imm(0x80));
            EmitAsm("BEQ", Rel(aPos));
            EmitAsm("LDA", Mem(CallArgBase)); EmitAsm("EOR", Imm(0xFF)); EmitAsm("CLC"); EmitAsm("ADC", Imm(1)); EmitAsm("STA", Mem(CallArgBase));
            EmitAsm("LDA", Mem(CallArgBase + 1)); EmitAsm("EOR", Imm(0xFF)); EmitAsm("ADC", Imm(0)); EmitAsm("STA", Mem(CallArgBase + 1));
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp2Address)); EmitAsm("EOR", Imm(1)); EmitAsm("STA", Mem(_runtimeIntrinsicTmp2Address));
            _assembly.Add(Expr.Make(Tag.Label, aPos));
            string bPos = NewGeneratedLabel("smul_b_positive");
            EmitAsm("LDA", Mem(CallArgBase + 2));
            EmitAsm("AND", Imm(0x80));
            EmitAsm("BEQ", Rel(bPos));
            EmitAsm("LDA", Mem(CallArgBase + 2)); EmitAsm("EOR", Imm(0xFF)); EmitAsm("CLC"); EmitAsm("ADC", Imm(1)); EmitAsm("STA", Mem(CallArgBase + 2));
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp2Address)); EmitAsm("EOR", Imm(1)); EmitAsm("STA", Mem(_runtimeIntrinsicTmp2Address));
            _assembly.Add(Expr.Make(Tag.Label, bPos));
            EmitAsm("JSR", Abs("__mul16x8"));
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("STX", Mem(_runtimeIntrinsicTmp1Address));
            string sdone = NewGeneratedLabel("smul_done");
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp2Address));
            EmitAsm("BEQ", Rel(sdone));
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp0Address)); EmitAsm("EOR", Imm(0xFF)); EmitAsm("CLC"); EmitAsm("ADC", Imm(1)); EmitAsm("STA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp1Address)); EmitAsm("EOR", Imm(0xFF)); EmitAsm("ADC", Imm(0)); EmitAsm("STA", Mem(_runtimeIntrinsicTmp1Address));
            _assembly.Add(Expr.Make(Tag.Label, sdone));
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("LDX", Mem(_runtimeIntrinsicTmp1Address));
            EmitAsm("RTS");

            EmitMacHelper("__mac16", "__mul16x8");
            EmitMacHelper("__smac16", "__smul16x8");
            EmitDot2Helper("__dot2_q8_8", "__mul16x8");
            EmitDot3Helper("__dot3_q8_8", "__mul16x8");
            EmitDot2Helper("__sdot2_q8_8", "__smul16x8");
            EmitDot3Helper("__sdot3_q8_8", "__smul16x8");
        }
__split_scroll_sprite0

Wait for a fresh sprite-zero hit and write scroll X/Y for a horizontal split, keeping the top aligned while shifting the lower background.

Syntax

c
void __split_scroll_sprite0(u8 x, u8 y);

Parameters

x

Horizontal scroll value, 0..255, not an object’s screen X coordinate. The sample uses 32 to display the lower part of the same background shifted left by 32 pixels.

y

Temporary vertical scroll setting, 0..255; the usual visible range is 0..239. This helper writes $2005, so it does not immediately change the live vertical position during rendering. The horizontal-split sample passes zero.

Return value

No return value.

Conditions and limits

Overlap an opaque pixel of OAM sprite 0 with an opaque background pixel, with both layers enabled. The behind-background priority flag still permits a hit. The leftmost eight pixels require the corresponding layer enables; overlap at X=255 cannot trigger a hit.

Poll PPUSTATUS ($2002) bit 6 until it is clear, then until it is set. If a hit has already occurred when called, wait for a fresh hit in the next frame. Reading the flag does not acknowledge it; the PPU clears it during pre-render.

There is no timeout or failure return. A hidden sprite 0, transparent background, disabled rendering or another setup without overlap leaves the call blocked. Guarantee a hit before calling. CPU IRQ enable is not required, and this function does not enable NMI. The sample enables NMI for its separate frame wait.

Reads of $2002 also affect the shared PPU address/scroll write latch and the VBlank flag. Do not split a two-write $2005/$2006 operation around this call. Use it from the foreground, not from an NMI/IRQ handler.

After the hit, read $2002 and write X then Y to $2005. Fine X changes immediately; coarse X takes effect at the next horizontal reload. Y normally takes effect at the next pre-render reload, unless another scroll write replaces it first. This is not an immediate arbitrary-X/Y raster repositioning operation.

Polling and instruction execution delay the writes. KUROSAKI renders by scanline, so these captures do not establish a dot-exact hardware boundary. The sample places the change inside the white band at Y=96..103, keeping boundary uncertainty away from the blue and red bars.

The shared header fc_sprite0_scene.h prepares original blue/red bars and a white horizontal band. Sprite 0 sits at screen (16,96), hidden behind the background; its raw OAM Y is 95 because display Y is OAM Y+1. Each frame restores the top scroll to zero during VBlank before waiting for the hit.

NESdev: PPU scrolling

NESdev: PPU programmer reference

Usage example

c
void main(void) {
    s0_prepare();
    while (1) {
        s0_frame();
        __split_scroll_sprite0(32,0);
        s0_completed++;
    }
}

This is a calling fragment. Follow the stated initialization and buffer requirements before using it in a complete program.

Expected result

Above the white band, blue is at X=64..95 and red at X=160..191. Below it, from Y=104 onward, blue moves to X=32..63 and red to X=128..159: only the lower area shifts left by 32 pixels. The horizontal band is at Y=96..103 and the bars end at Y=207. Verification checks one completion per frame and every image pixel’s position and color.

__split_scroll_sprite0: SPLIT-DEFAULT — Above the white band, blue is at X=64..95 and red at X=160..191. Below it, from Y=104 onward, blue moves to X=32..63 and red to X=128..159: only the lower area shifts left by 32 pixels. The horizontal band is at Y=96..103 and the bars end at Y=207. Verification checks one completion per frame and every image pixel’s position and color.
SPLIT-DEFAULT — Above the white band, blue is at X=64..95 and red at X=160..191. Below it, from Y=104 onward, blue moves to X=32..63 and red to X=128..159: only the lower area shifts left by 32 pixels. The horizontal band is at Y=96..103 and the bars end at Y=207. Verification checks one completion per frame and every image pixel’s position and color.

Complete example program

Build command

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\sprite0_split.c -I .\kitaqfc\lib -I .\kitaq-docs\samples -o .\out\sprite0_split.nes --mapper=nrom --nes-chr=.\kitaq-docs\samples\sprite0_scene.chr --no-cache --no-disasm
Implementation and source

kitaqfc/lib/intrinsics.h:336

Declaration notes (original source)
Wait for a new sprite-zero hit, reset the PPU latch and write X/Y scroll; requires a visible hit setup.
__sprite0_wait_hit

Wait for a fresh sprite-zero/background overlap, synchronizing foreground work to a known region of the screen without changing scroll values.

Syntax

c
void __sprite0_wait_hit(void);

Return value

No return value.

Conditions and limits

Overlap an opaque pixel of OAM sprite 0 with an opaque background pixel, with both layers enabled. The behind-background priority flag still permits a hit. The leftmost eight pixels require the corresponding layer enables; overlap at X=255 cannot trigger a hit.

Poll PPUSTATUS ($2002) bit 6 until it is clear, then until it is set. If a hit has already occurred when called, wait for a fresh hit in the next frame. Reading the flag does not acknowledge it; the PPU clears it during pre-render.

There is no timeout or failure return. A hidden sprite 0, transparent background, disabled rendering or another setup without overlap leaves the call blocked. Guarantee a hit before calling. CPU IRQ enable is not required, and this function does not enable NMI. The sample enables NMI for its separate frame wait.

Reads of $2002 also affect the shared PPU address/scroll write latch and the VBlank flag. Do not split a two-write $2005/$2006 operation around this call. Use it from the foreground, not from an NMI/IRQ handler.

Polling and instruction execution delay the writes. KUROSAKI renders by scanline, so these captures do not establish a dot-exact hardware boundary. The sample places the change inside the white band at Y=96..103, keeping boundary uncertainty away from the blue and red bars.

The shared header fc_sprite0_scene.h prepares original blue/red bars and a white horizontal band. Sprite 0 sits at screen (16,96), hidden behind the background; its raw OAM Y is 95 because display Y is OAM Y+1. Each frame restores the top scroll to zero during VBlank before waiting for the hit.

NESdev: PPU scrolling

NESdev: PPU programmer reference

Usage example

c
void main(void) {
    s0_prepare();
    while (1) {
        s0_frame();
        __sprite0_wait_hit();
        s0_completed++;
    }
}

This is a calling fragment. Follow the stated initialization and buffer requirements before using it in a complete program.

Expected result

The blue bar occupies X=64..95 and the red bar X=160..191, aligned above and below the white band. Waiting alone does not change scrolling. The executed lesson completes 116 waits after 120 frames and 117 after 121 frames, verifying that it keeps advancing.

__sprite0_wait_hit: WAIT-DEFAULT — The blue bar occupies X=64..95 and the red bar X=160..191, aligned above and below the white band. Waiting alone does not change scrolling. The executed lesson completes 116 waits after 120 frames and 117 after 121 frames, verifying that it keeps advancing.
WAIT-DEFAULT — The blue bar occupies X=64..95 and the red bar X=160..191, aligned above and below the white band. Waiting alone does not change scrolling. The executed lesson completes 116 waits after 120 frames and 117 after 121 frames, verifying that it keeps advancing.

Complete example program

Build command

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\sprite0_wait.c -I .\kitaqfc\lib -I .\kitaq-docs\samples -o .\out\sprite0_wait.nes --mapper=nrom --nes-chr=.\kitaq-docs\samples\sprite0_scene.chr --no-cache --no-disasm
Implementation and source

kitaqfc/lib/intrinsics.h:334

Declaration notes (original source)
Wait for the current hit flag to clear, then for a new hit. No timeout is provided.
__sprite_attr

Replace the complete attribute byte of one shadow sprite. Use it to choose the palette, set background priority, or flip the pattern horizontally or vertically without changing its tile or position.

Syntax

c
void __sprite_attr(u8 index, u8 attr);

Parameters

index

Sprite entry number, 0–63, not a byte offset. The implementation multiplies it by four using an 8-bit cursor; out-of-range values wrap modulo 64 and can overwrite another entry. No allocation check is performed.

attr

Complete attribute byte: bits 0–1 select sprite palette 0–3; bit 5 places opaque sprite pixels behind opaque background pixels; bits 6 and 7 flip horizontally and vertically. Keep bits 2–4 zero. Combine flags with bitwise OR; the supplied byte replaces all previous attributes.

Return value

None (void).

Conditions and limits

These operations edit CPU RAM at 0x0200–0x02FF; they do not immediately change hardware OAM. Transfer the completed page with __oam_dma. They do not maintain the sprite library’s allocation flags, coordinate arrays or active count. Keep interrupt handlers from editing or transferring a partially prepared page.

Usage example

c
__sprite_attr(3,0xC0); // Flip the arrow horizontally and vertically.

This is a calling fragment. Follow the stated initialization and buffer requirements before using it in a complete program.

Expected result

The example separates each operation into a labeled row. SET and MOVE show 8×8 right arrows at (144,16) and (144,32); TILE shows a green square at (144,48); ATTR shows the arrow flipped both ways at (144,64). HIDE at (144,80) and CLEAR at (144,112) must stay empty. META shows a pair at (136,96) and (144,96), with the second arrow mirrored horizontally. NEXT 07 is the next write position after writing slots 5 and 6. Arrows have red edges, green shafts and blue tip details. Verification compares 8,704 pixels per image and checks the transferred OAM bytes; physical hardware is untested.

TRANSFER shows an arrow at (144,128). This program sends page 02 directly with __oam_dma, including every field prepared by the individual sprite operations.

__sprite_attr: PAGE02
PAGE02

Complete example program

Build command

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\oam_intrinsics.c -I .\kitaqfc\lib -I .\kitaq-docs\samples --nes-chr=.\kitaq-docs\samples\sprite_example.chr -o .\out\oam_intrinsics.nes --no-cache --no-disasm
Implementation and source

kitaqfc/lib/intrinsics.h:198

Declaration notes (original source)
Replace the selected shadow attribute byte, including palette, priority and flip bits.
csharp
EmitHelperStart("__sprite_attr");
            EmitSpriteIndexToX();
            EmitAsm("LDA", Mem(CallArgBase + 1)); EmitAsm("STA", new AsmOperand(OamRamBase + 2, AddressMode.AbsoluteX));
            EmitAsm("RTS");
__sprite_hide

Hide one shadow sprite by setting its Y byte to 0xF0. Its X, tile and attributes remain available for reuse. The function does not free an allocation or change the sprite library’s active count.

Syntax

c
void __sprite_hide(u8 index);

Parameters

index

Sprite entry number, 0–63, not a byte offset. The implementation multiplies it by four using an 8-bit cursor; out-of-range values wrap modulo 64 and can overwrite another entry. No allocation check is performed.

Return value

None (void).

Conditions and limits

These operations edit CPU RAM at 0x0200–0x02FF; they do not immediately change hardware OAM. Transfer the completed page with __oam_dma. They do not maintain the sprite library’s allocation flags, coordinate arrays or active count. Keep interrupt handlers from editing or transferring a partially prepared page.

Usage example

c
__sprite_hide(4); // The HIDE row remains empty after DMA.

This is a calling fragment. Follow the stated initialization and buffer requirements before using it in a complete program.

Expected result

The example separates each operation into a labeled row. SET and MOVE show 8×8 right arrows at (144,16) and (144,32); TILE shows a green square at (144,48); ATTR shows the arrow flipped both ways at (144,64). HIDE at (144,80) and CLEAR at (144,112) must stay empty. META shows a pair at (136,96) and (144,96), with the second arrow mirrored horizontally. NEXT 07 is the next write position after writing slots 5 and 6. Arrows have red edges, green shafts and blue tip details. Verification compares 8,704 pixels per image and checks the transferred OAM bytes; physical hardware is untested.

TRANSFER shows an arrow at (144,128). This program sends page 02 directly with __oam_dma, including every field prepared by the individual sprite operations.

__sprite_hide: PAGE02
PAGE02

Complete example program

Build command

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\oam_intrinsics.c -I .\kitaqfc\lib -I .\kitaq-docs\samples --nes-chr=.\kitaq-docs\samples\sprite_example.chr -o .\out\oam_intrinsics.nes --no-cache --no-disasm
Implementation and source

kitaqfc/lib/intrinsics.h:200

Declaration notes (original source)
Write F0 to the selected shadow Y byte; issue DMA to make the change visible.
csharp
EmitHelperStart("__sprite_hide");
            EmitSpriteIndexToX();
            EmitAsm("LDA", Imm(0xF0)); EmitAsm("STA", new AsmOperand(OamRamBase + 0, AddressMode.AbsoluteX));
            EmitAsm("RTS");
__sprite_move

Change only the X and Y bytes of one shadow sprite. Its tile number and attributes stay intact, so the same graphic moves without changing its palette or flips.

Syntax

c
void __sprite_move(u8 index, u8 x, u8 y);

Parameters

index

Sprite entry number, 0–63, not a byte offset. The implementation multiplies it by four using an 8-bit cursor; out-of-range values wrap modulo 64 and can overwrite another entry. No allocation check is performed.

x

Horizontal position in pixels, stored as an unsigned byte. This is a pixel coordinate, not a tile coordinate.

y

Raw OAM Y byte. Use visible_top-1 for a visible top below scanline zero; values 239..255 are outside the visible 240-line area.

Return value

None (void).

Conditions and limits

These operations edit CPU RAM at 0x0200–0x02FF; they do not immediately change hardware OAM. Transfer the completed page with __oam_dma. They do not maintain the sprite library’s allocation flags, coordinate arrays or active count. Keep interrupt handlers from editing or transferring a partially prepared page.

Usage example

c
__sprite_move(1,144,31); // Move to (144,32), retaining tile 128 and attributes 0.

This is a calling fragment. Follow the stated initialization and buffer requirements before using it in a complete program.

Expected result

The example separates each operation into a labeled row. SET and MOVE show 8×8 right arrows at (144,16) and (144,32); TILE shows a green square at (144,48); ATTR shows the arrow flipped both ways at (144,64). HIDE at (144,80) and CLEAR at (144,112) must stay empty. META shows a pair at (136,96) and (144,96), with the second arrow mirrored horizontally. NEXT 07 is the next write position after writing slots 5 and 6. Arrows have red edges, green shafts and blue tip details. Verification compares 8,704 pixels per image and checks the transferred OAM bytes; physical hardware is untested.

TRANSFER shows an arrow at (144,128). This program sends page 02 directly with __oam_dma, including every field prepared by the individual sprite operations.

__sprite_move: PAGE02
PAGE02

Complete example program

Build command

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\oam_intrinsics.c -I .\kitaqfc\lib -I .\kitaq-docs\samples --nes-chr=.\kitaq-docs\samples\sprite_example.chr -o .\out\oam_intrinsics.nes --no-cache --no-disasm
Implementation and source

kitaqfc/lib/intrinsics.h:194

Declaration notes (original source)
Replace only shadow X/Y for index 0..63; tile and attribute bytes remain unchanged.
csharp
EmitHelperStart("__sprite_move");
            EmitSpriteIndexToX();
            EmitAsm("LDA", Mem(CallArgBase + 2)); EmitAsm("STA", new AsmOperand(OamRamBase + 0, AddressMode.AbsoluteX));
            EmitAsm("LDA", Mem(CallArgBase + 1)); EmitAsm("STA", new AsmOperand(OamRamBase + 3, AddressMode.AbsoluteX));
            EmitAsm("RTS");
__sprite_set

Write all four fields of one sprite into the page-02 shadow buffer. Arguments are index, X, Y, tile and attributes; the stored byte order is Y, tile, attributes, X.

Syntax

c
void __sprite_set(u8 index, u8 x, u8 y, u8 tile, u8 attr);

Parameters

index

Sprite entry number, 0–63, not a byte offset. The implementation multiplies it by four using an 8-bit cursor; out-of-range values wrap modulo 64 and can overwrite another entry. No allocation check is performed.

x

Horizontal position in pixels, stored as an unsigned byte. This is a pixel coordinate, not a tile coordinate.

y

Raw OAM Y byte. Use visible_top-1 for a visible top below scanline zero; values 239..255 are outside the visible 240-line area.

tile

Eight-bit tile number. Load its graphics first. Hardware rules for 8×16 sprites still apply; this function does not adjust the number for paired tiles.

attr

Complete attribute byte: bits 0–1 select sprite palette 0–3; bit 5 places opaque sprite pixels behind opaque background pixels; bits 6 and 7 flip horizontally and vertically. Keep bits 2–4 zero. Combine flags with bitwise OR; the supplied byte replaces all previous attributes.

Return value

None (void).

Conditions and limits

These operations edit CPU RAM at 0x0200–0x02FF; they do not immediately change hardware OAM. Transfer the completed page with __oam_dma. They do not maintain the sprite library’s allocation flags, coordinate arrays or active count. Keep interrupt handlers from editing or transferring a partially prepared page.

Usage example

c
__sprite_set(0,144,15,128,0); // An 8x8 arrow at visible position (144,16).

This is a calling fragment. Follow the stated initialization and buffer requirements before using it in a complete program.

Expected result

The example separates each operation into a labeled row. SET and MOVE show 8×8 right arrows at (144,16) and (144,32); TILE shows a green square at (144,48); ATTR shows the arrow flipped both ways at (144,64). HIDE at (144,80) and CLEAR at (144,112) must stay empty. META shows a pair at (136,96) and (144,96), with the second arrow mirrored horizontally. NEXT 07 is the next write position after writing slots 5 and 6. Arrows have red edges, green shafts and blue tip details. Verification compares 8,704 pixels per image and checks the transferred OAM bytes; physical hardware is untested.

TRANSFER shows an arrow at (144,128). This program sends page 02 directly with __oam_dma, including every field prepared by the individual sprite operations.

__sprite_set: PAGE02
PAGE02

Complete example program

Build command

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\oam_intrinsics.c -I .\kitaqfc\lib -I .\kitaq-docs\samples --nes-chr=.\kitaq-docs\samples\sprite_example.chr -o .\out\oam_intrinsics.nes --no-cache --no-disasm
Implementation and source

kitaqfc/lib/intrinsics.h:192

Declaration notes (original source)
Write Y, tile, attributes and X to one shadow entry. Use indices 0..63; coordinates are raw OAM bytes.
csharp
EmitHelperStart("__sprite_set");
            EmitSpriteIndexToX();
            EmitAsm("LDA", Mem(CallArgBase + 2)); EmitAsm("STA", new AsmOperand(OamRamBase + 0, AddressMode.AbsoluteX));
            EmitAsm("LDA", Mem(CallArgBase + 3)); EmitAsm("STA", new AsmOperand(OamRamBase + 1, AddressMode.AbsoluteX));
            EmitAsm("LDA", Mem(CallArgBase + 4)); EmitAsm("STA", new AsmOperand(OamRamBase + 2, AddressMode.AbsoluteX));
            EmitAsm("LDA", Mem(CallArgBase + 1)); EmitAsm("STA", new AsmOperand(OamRamBase + 3, AddressMode.AbsoluteX));
            EmitAsm("RTS");
__sprite_tile

Replace only the tile-number byte of one shadow sprite. Position and attributes are preserved. This selects an existing pattern; it does not upload graphics to CHR memory.

Syntax

c
void __sprite_tile(u8 index, u8 tile);

Parameters

index

Sprite entry number, 0–63, not a byte offset. The implementation multiplies it by four using an 8-bit cursor; out-of-range values wrap modulo 64 and can overwrite another entry. No allocation check is performed.

tile

Eight-bit tile number. Load its graphics first. Hardware rules for 8×16 sprites still apply; this function does not adjust the number for paired tiles.

Return value

None (void).

Conditions and limits

These operations edit CPU RAM at 0x0200–0x02FF; they do not immediately change hardware OAM. Transfer the completed page with __oam_dma. They do not maintain the sprite library’s allocation flags, coordinate arrays or active count. Keep interrupt handlers from editing or transferring a partially prepared page.

Usage example

c
__sprite_tile(2,129); // Select the green square at (144,48).

This is a calling fragment. Follow the stated initialization and buffer requirements before using it in a complete program.

Expected result

The example separates each operation into a labeled row. SET and MOVE show 8×8 right arrows at (144,16) and (144,32); TILE shows a green square at (144,48); ATTR shows the arrow flipped both ways at (144,64). HIDE at (144,80) and CLEAR at (144,112) must stay empty. META shows a pair at (136,96) and (144,96), with the second arrow mirrored horizontally. NEXT 07 is the next write position after writing slots 5 and 6. Arrows have red edges, green shafts and blue tip details. Verification compares 8,704 pixels per image and checks the transferred OAM bytes; physical hardware is untested.

TRANSFER shows an arrow at (144,128). This program sends page 02 directly with __oam_dma, including every field prepared by the individual sprite operations.

__sprite_tile: PAGE02
PAGE02

Complete example program

Build command

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\oam_intrinsics.c -I .\kitaqfc\lib -I .\kitaq-docs\samples --nes-chr=.\kitaq-docs\samples\sprite_example.chr -o .\out\oam_intrinsics.nes --no-cache --no-disasm
Implementation and source

kitaqfc/lib/intrinsics.h:196

Declaration notes (original source)
Replace the selected shadow tile byte; this does not upload tile patterns.
csharp
EmitHelperStart("__sprite_tile");
            EmitSpriteIndexToX();
            EmitAsm("LDA", Mem(CallArgBase + 1)); EmitAsm("STA", new AsmOperand(OamRamBase + 1, AddressMode.AbsoluteX));
            EmitAsm("RTS");
__vram_fill

Set ppu_addr, then write the same byte value to PPUDATA exactly len times. This is a direct fill, completed before return. It does not interpret tile dimensions, attributes or rectangular boundaries.

Syntax

c
void __vram_fill(u16 ppu_addr, u8 value, u8 len);

Parameters

ppu_addr

PPU byte address (0x0000–0x3FFF), not a CPU pointer. The target must be writable: pattern changes need CHR RAM, not CHR ROM. PPU mapping and mirroring apply. Select increment 1 in PPUCTRL for contiguous copies and fills. Execute during safe PPU timing, then restore the desired scroll state; the executor writes PPUADDR and PPUDATA without restoring scroll settings.

value

One byte, 0–255, written repeatedly. Its visible meaning depends on whether the destination contains tile indices, pixel planes or another byte-based structure.

len

Number of bytes, 0..255. Zero still sets the PPU address and resets the latch, but writes no data. There is no destination-capacity check, clipping, bank switch or deferred transfer.

Return value

None (void).

Conditions and limits

Direct PPU access requires rendering to be off or the entire operation to fit a safe VBlank interval. These functions do not wait, queue, mask NMI or restore scroll. Prevent an interrupt from changing the shared latch/address during the call. PPUCTRL bit 2 selects a destination step of 1 or 32 per data byte. The PPU handles mirroring and address wrapping; CHR-ROM cannot be written. Set the intended scroll state after access and before rendering.

NESdev: PPU registers

NESdev: PPU scrolling

Usage example

c
__vram_fill(0x2060,2,32); // A second blue row of 4-pixel-wide left halves.

This is a calling fragment. Follow the stated initialization and buffer requirements before using it in a complete program.

Expected result

This program teaches register setup, direct tile/attribute writes, scrolling and separate background/sprite palette loads. The blue band at (0,16) measures 256×16 pixels: its first row repeats a solid 8×8 tile, a left-half 4×8 bar and an eight-row triangle; its second row contains only left-half bars. A solid green 64×32 rectangle starts at (16,48). A red 64×32 region at (144,48) contains an 8-by-4 grid of triangle tiles.

At y=96, blue markers at x=16, 24 and 32 are respectively a square, left-half bar and triangle; a red left-half bar is at x=80 and a green triangle at x=144. These are background tiles. At y=144, the separately colored sprites are a red 8×8 square at x=16, a blue left-half 4×8 bar at x=80 and a green eight-row triangle at x=144. Final scrolling is (0,0).

The image checks 16,384 pixels for the expected shapes and colors, including sprite colors. Separate state tests cover 31 cases: register replacement, status/latch effects, saved scroll coordinates, zero/255-byte transfers, increments 1/32, map edges, zero rectangle dimensions, table selectors, whole-byte attributes and both palette targets. Values of Y=255 are checked as register writes, not as correct scrolling artwork. Evidence is from KUROSAKI with NROM; real hardware is not tested.

__vram_fill: NROM
NROM

Complete example program

Build command

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\ppu_intrinsics.c -I .\kitaqfc\lib -I .\kitaq-docs\samples --nes-chr=.\kitaq-docs\samples\api-examples\fc\vram_shapes.chr -o .\out\ppu_intrinsics.nes --no-cache --no-disasm
Implementation and source

kitaqfc/lib/intrinsics.h:122

Declaration notes (original source)
Write repeated bytes directly; this is not an enqueue operation.

kitaqfc/kitaqfc/CodeGenerator.cs:4818

csharp
case "__vram_fill": return EmitKitaqfcHelperCall(funcName, args, ctx, new[] {2,1,1}, funcName);
                // Nametable and attribute helpers receive byte coordinates, values and optional table selectors.

void EmitHelperVramFill()
        {
            EmitHelperStart("__vram_fill");
            EmitPpuAddrFromCallArg();
            EmitAsm("LDX", Mem(CallArgBase + 3));
            string loop = NewGeneratedLabel("vram_fill_loop");
            string done = NewGeneratedLabel("vram_fill_done");
            EmitAsm("BEQ", Rel(done));
            _assembly.Add(Expr.Make(Tag.Label, loop));
            EmitAsm("LDA", Mem(CallArgBase + 2));
            EmitAsm("STA", Mem(0x2007));
            EmitAsm("DEX");
            EmitAsm("BNE", Rel(loop));
            _assembly.Add(Expr.Make(Tag.Label, done));
            EmitAsm("RTS");
        }
__vram_write

Reset the PPU latch, set ppu_addr and synchronously copy len consecutive source bytes to PPUDATA. The count is eight-bit; the compiler uses indexed source reads. No pointer is retained for later execution, and changing the source after return does not change VRAM.

Syntax

c
void __vram_write(u16 ppu_addr, const u8* src, u8 len);

Parameters

ppu_addr

PPU byte address (0x0000–0x3FFF), not a CPU pointer. The target must be writable: pattern changes need CHR RAM, not CHR ROM. PPU mapping and mirroring apply. Select increment 1 in PPUCTRL for contiguous copies and fills. Execute during safe PPU timing, then restore the desired scroll state; the executor writes PPUADDR and PPUDATA without restoring scroll settings.

src

CPU pointer to at least the requested number of readable bytes. Keep the source mapped and stable until this synchronous transfer returns. There is no bank switching, retained queue pointer or source-length check; a zero count does not dereference the pointer.

len

Number of bytes, 0..255. Zero still sets the PPU address and resets the latch, but writes no data. There is no destination-capacity check, clipping, bank switch or deferred transfer.

Return value

None (void).

Conditions and limits

Direct PPU access requires rendering to be off or the entire operation to fit a safe VBlank interval. These functions do not wait, queue, mask NMI or restore scroll. Prevent an interrupt from changing the shared latch/address during the call. PPUCTRL bit 2 selects a destination step of 1 or 32 per data byte. The PPU handles mirroring and address wrapping; CHR-ROM cannot be written. Set the intended scroll state after access and before rendering.

NESdev: PPU registers

NESdev: PPU scrolling

Usage example

c
__vram_write(0x2040,pattern,32); // Blue row: solid tile, left half, triangle.
pattern[0]=3; // The completed transfer keeps its first solid tile.

This is a calling fragment. Follow the stated initialization and buffer requirements before using it in a complete program.

Expected result

This program teaches register setup, direct tile/attribute writes, scrolling and separate background/sprite palette loads. The blue band at (0,16) measures 256×16 pixels: its first row repeats a solid 8×8 tile, a left-half 4×8 bar and an eight-row triangle; its second row contains only left-half bars. A solid green 64×32 rectangle starts at (16,48). A red 64×32 region at (144,48) contains an 8-by-4 grid of triangle tiles.

At y=96, blue markers at x=16, 24 and 32 are respectively a square, left-half bar and triangle; a red left-half bar is at x=80 and a green triangle at x=144. These are background tiles. At y=144, the separately colored sprites are a red 8×8 square at x=16, a blue left-half 4×8 bar at x=80 and a green eight-row triangle at x=144. Final scrolling is (0,0).

The image checks 16,384 pixels for the expected shapes and colors, including sprite colors. Separate state tests cover 31 cases: register replacement, status/latch effects, saved scroll coordinates, zero/255-byte transfers, increments 1/32, map edges, zero rectangle dimensions, table selectors, whole-byte attributes and both palette targets. Values of Y=255 are checked as register writes, not as correct scrolling artwork. Evidence is from KUROSAKI with NROM; real hardware is not tested.

__vram_write: NROM
NROM

Complete example program

Build command

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\ppu_intrinsics.c -I .\kitaqfc\lib -I .\kitaq-docs\samples --nes-chr=.\kitaq-docs\samples\api-examples\fc\vram_shapes.chr -o .\out\ppu_intrinsics.nes --no-cache --no-disasm
Implementation and source

kitaqfc/lib/intrinsics.h:120

Declaration notes (original source)
Write a byte-counted source range directly; arrange PPU access timing and source-bank visibility.

kitaqfc/kitaqfc/CodeGenerator.cs:4817

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

void EmitHelperVramWrite()
        {
            EmitHelperStart("__vram_write");
            EmitPpuAddrFromCallArg();
            EmitAsm("LDY", Imm(0));
            string loop = NewGeneratedLabel("vram_write_loop");
            string done = NewGeneratedLabel("vram_write_done");
            _assembly.Add(Expr.Make(Tag.Label, loop));
            EmitAsm("CPY", Mem(CallArgBase + 4));
            EmitAsm("BEQ", Rel(done));
            EmitAsm("LDA", IndY(CallArgBase + 2));
            EmitAsm("STA", Mem(0x2007));
            EmitAsm("INY");
            EmitAsm("BNE", Rel(loop));
            _assembly.Add(Expr.Make(Tag.Label, done));
            EmitAsm("RTS");
        }
__vramq_capacity

Return the total capacity of the command buffer, regardless of how much is currently used.

Syntax

c
u8 __vramq_capacity(void);

Return value

An unsigned byte in the platform-specific units described here. Reading it does not modify or execute the queue.

Conditions and limits

FC counts bytes of encoded commands in a fixed 128-byte intrinsic buffer. A tile write costs 4 bytes, a fill record 5 and a pointer-copy record 6, including metadata. A ready queue can be consumed by NMI; inspect or build it before committing, and coordinate the producer with the consumer.

These intrinsics operate on the same command buffer used by lib/vram.c. The nes_vram_queue_* functions in runtime.c manage a separate queue with a different record format. Clearing or querying one does not clear or measure the other.

Build and consume the queue in coordinated phases. Do not append while an interrupt executes it, change its bookkeeping directly, or overwrite retained source data unintentionally. These APIs provide neither locking nor double buffering. Capacity describes the command buffer, not free bytes in hardware VRAM.

Usage example

c
capacity=__vramq_capacity();
if (capacity!=128) failures++;

This is a calling fragment. Follow the stated initialization and buffer requirements before using it in a complete program.

Expected result

The complete program teaches queue capacity, retained pointers, independent reset flags and immediate versus NMI execution. Expect CAPACITY 128, ENCODED BYTES 030, AFTER EXEC 000, OVERFLOW SEEN 001 and FAILED CHECKS 000. Six records use 30 buffer bytes but perform 512 byte writes. OVERFLOW SEEN preserves the earlier intentional rejection; it is not a claim that the final latch remains set.

The blue copy starts at pixel (0,16), and the green fill at (0,88). Each covers seven full 256-pixel rows of tiles, then 248 pixels of the eighth tile row: 255 tiles total. The final 8 × 8 cell stays black. The copy repeats solid, left-half and triangular 8 × 8 patterns; its first tile is a triangle because the source changed after enqueueing. The fill stays solid even after its caller variable changes. Both shape and color are checked.

At y=160, expect a red solid square at x=16, a red left-half tile at x=224 and a red solid square at x=240. The first survives zero-length copy/fill records; the middle proves default-NMI consumption; the last is a single put. The cancelled cell at (248,160) must remain black. The long transfers run with rendering and NMI off; only the final small write uses NMI. This is NROM emulator evidence, not a real-hardware test.

__vramq_capacity: NROM
NROM

Complete example program

Build command

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\vramq_intrinsics.c -I .\kitaqfc\lib -I .\kitaq-docs\samples -o .\out\vramq_intrinsics.nes --mapper=nrom --nes-chr=.\kitaq-docs\samples\api-examples\fc\vram_shapes.chr --no-cache --no-disasm
Implementation and source

kitaqfc/lib/intrinsics.h:164

Declaration notes (original source)
Return total encoded command-buffer capacity; this is independent of PPU VRAM capacity.
__vramq_clear

Discard all pending intrinsic-queue records and cancel readiness by setting both length and ready to 0. It does not write VRAM or clear the overflow latch. To reset that latch, call __vramq_clear_overflow separately.

Syntax

c
void __vramq_clear(void);

Return value

None (void).

Conditions and limits

FC counts bytes of encoded commands in a fixed 128-byte intrinsic buffer. A tile write costs 4 bytes, a fill record 5 and a pointer-copy record 6, including metadata. A ready queue can be consumed by NMI; inspect or build it before committing, and coordinate the producer with the consumer.

These intrinsics operate on the same command buffer used by lib/vram.c. The nes_vram_queue_* functions in runtime.c manage a separate queue with a different record format. Clearing or querying one does not clear or measure the other.

Build and consume the queue in coordinated phases. Do not append while an interrupt executes it, change its bookkeeping directly, or overwrite retained source data unintentionally. These APIs provide neither locking nor double buffering. Capacity describes the command buffer, not free bytes in hardware VRAM.

Usage example

c
__vramq_clear();
if (__vramq_len()!=0 || __vramq_overflow()!=1) failures++;
// Clearing queued work deliberately leaves the overflow latch set.

This is a calling fragment. Follow the stated initialization and buffer requirements before using it in a complete program.

Expected result

The complete program teaches queue capacity, retained pointers, independent reset flags and immediate versus NMI execution. Expect CAPACITY 128, ENCODED BYTES 030, AFTER EXEC 000, OVERFLOW SEEN 001 and FAILED CHECKS 000. Six records use 30 buffer bytes but perform 512 byte writes. OVERFLOW SEEN preserves the earlier intentional rejection; it is not a claim that the final latch remains set.

The blue copy starts at pixel (0,16), and the green fill at (0,88). Each covers seven full 256-pixel rows of tiles, then 248 pixels of the eighth tile row: 255 tiles total. The final 8 × 8 cell stays black. The copy repeats solid, left-half and triangular 8 × 8 patterns; its first tile is a triangle because the source changed after enqueueing. The fill stays solid even after its caller variable changes. Both shape and color are checked.

At y=160, expect a red solid square at x=16, a red left-half tile at x=224 and a red solid square at x=240. The first survives zero-length copy/fill records; the middle proves default-NMI consumption; the last is a single put. The cancelled cell at (248,160) must remain black. The long transfers run with rendering and NMI off; only the final small write uses NMI. This is NROM emulator evidence, not a real-hardware test.

__vramq_clear: NROM
NROM

Complete example program

Build command

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\vramq_intrinsics.c -I .\kitaqfc\lib -I .\kitaq-docs\samples -o .\out\vramq_intrinsics.nes --mapper=nrom --nes-chr=.\kitaq-docs\samples\api-examples\fc\vram_shapes.chr --no-cache --no-disasm
Implementation and source

kitaqfc/lib/intrinsics.h:146

Declaration notes (original source)
NMI VRAM transfer queue. Queue entries are consumed by __vramq_exec(),
and the default NMI handler calls __vramq_exec() automatically.

Reset pending intrinsic-queue records; this is separate from runtime.c nes_vram_queue_clear.
__vramq_clear_overflow

Reset only the overflow latch to 0. Pending records, their length and readiness remain unchanged. This acknowledges a capacity failure; it neither retries the rejected record nor rolls back successful earlier appends.

Syntax

c
void __vramq_clear_overflow(void);

Return value

None (void).

Conditions and limits

FC counts bytes of encoded commands in a fixed 128-byte intrinsic buffer. A tile write costs 4 bytes, a fill record 5 and a pointer-copy record 6, including metadata. A ready queue can be consumed by NMI; inspect or build it before committing, and coordinate the producer with the consumer.

These intrinsics operate on the same command buffer used by lib/vram.c. The nes_vram_queue_* functions in runtime.c manage a separate queue with a different record format. Clearing or querying one does not clear or measure the other.

Build and consume the queue in coordinated phases. Do not append while an interrupt executes it, change its bookkeeping directly, or overwrite retained source data unintentionally. These APIs provide neither locking nor double buffering. Capacity describes the command buffer, not free bytes in hardware VRAM.

Usage example

c
__vramq_clear_overflow();
if (__vramq_overflow()!=0 || __vramq_len()!=4) failures++;
// Resetting the latch deliberately keeps this pending put record.

This is a calling fragment. Follow the stated initialization and buffer requirements before using it in a complete program.

Expected result

The complete program teaches queue capacity, retained pointers, independent reset flags and immediate versus NMI execution. Expect CAPACITY 128, ENCODED BYTES 030, AFTER EXEC 000, OVERFLOW SEEN 001 and FAILED CHECKS 000. Six records use 30 buffer bytes but perform 512 byte writes. OVERFLOW SEEN preserves the earlier intentional rejection; it is not a claim that the final latch remains set.

The blue copy starts at pixel (0,16), and the green fill at (0,88). Each covers seven full 256-pixel rows of tiles, then 248 pixels of the eighth tile row: 255 tiles total. The final 8 × 8 cell stays black. The copy repeats solid, left-half and triangular 8 × 8 patterns; its first tile is a triangle because the source changed after enqueueing. The fill stays solid even after its caller variable changes. Both shape and color are checked.

At y=160, expect a red solid square at x=16, a red left-half tile at x=224 and a red solid square at x=240. The first survives zero-length copy/fill records; the middle proves default-NMI consumption; the last is a single put. The cancelled cell at (248,160) must remain black. The long transfers run with rendering and NMI off; only the final small write uses NMI. This is NROM emulator evidence, not a real-hardware test.

__vramq_clear_overflow: NROM
NROM

Complete example program

Build command

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\vramq_intrinsics.c -I .\kitaqfc\lib -I .\kitaq-docs\samples -o .\out\vramq_intrinsics.nes --mapper=nrom --nes-chr=.\kitaq-docs\samples\api-examples\fc\vram_shapes.chr --no-cache --no-disasm
Implementation and source

kitaqfc/lib/intrinsics.h:162

Declaration notes (original source)
Clear the overflow latch explicitly after handling the failed submission.
__vramq_commit

Set the queue's ready flag to 1 so the executor may consume it. This does not copy payloads, execute PPU writes or wait for NMI. Once committed, keep the queue and retained sources stable until execution finishes. With NMI enabled, the default handler can consume it asynchronously.

Syntax

c
void __vramq_commit(void);

Return value

None (void).

Conditions and limits

FC counts bytes of encoded commands in a fixed 128-byte intrinsic buffer. A tile write costs 4 bytes, a fill record 5 and a pointer-copy record 6, including metadata. A ready queue can be consumed by NMI; inspect or build it before committing, and coordinate the producer with the consumer.

These intrinsics operate on the same command buffer used by lib/vram.c. The nes_vram_queue_* functions in runtime.c manage a separate queue with a different record format. Clearing or querying one does not clear or measure the other.

Build and consume the queue in coordinated phases. Do not append while an interrupt executes it, change its bookkeeping directly, or overwrite retained source data unintentionally. These APIs provide neither locking nor double buffering. Capacity describes the command buffer, not free bytes in hardware VRAM.

Usage example

c
__vramq_commit(); // NMI is disabled here: this only marks the queue ready.
if (__vramq_len()!=30) failures++;

This is a calling fragment. Follow the stated initialization and buffer requirements before using it in a complete program.

Expected result

The complete program teaches queue capacity, retained pointers, independent reset flags and immediate versus NMI execution. Expect CAPACITY 128, ENCODED BYTES 030, AFTER EXEC 000, OVERFLOW SEEN 001 and FAILED CHECKS 000. Six records use 30 buffer bytes but perform 512 byte writes. OVERFLOW SEEN preserves the earlier intentional rejection; it is not a claim that the final latch remains set.

The blue copy starts at pixel (0,16), and the green fill at (0,88). Each covers seven full 256-pixel rows of tiles, then 248 pixels of the eighth tile row: 255 tiles total. The final 8 × 8 cell stays black. The copy repeats solid, left-half and triangular 8 × 8 patterns; its first tile is a triangle because the source changed after enqueueing. The fill stays solid even after its caller variable changes. Both shape and color are checked.

At y=160, expect a red solid square at x=16, a red left-half tile at x=224 and a red solid square at x=240. The first survives zero-length copy/fill records; the middle proves default-NMI consumption; the last is a single put. The cancelled cell at (248,160) must remain black. The long transfers run with rendering and NMI off; only the final small write uses NMI. This is NROM emulator evidence, not a real-hardware test.

__vramq_commit: NROM
NROM

Complete example program

Build command

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\vramq_intrinsics.c -I .\kitaqfc\lib -I .\kitaq-docs\samples -o .\out\vramq_intrinsics.nes --mapper=nrom --nes-chr=.\kitaq-docs\samples\api-examples\fc\vram_shapes.chr --no-cache --no-disasm
Implementation and source

kitaqfc/lib/intrinsics.h:154

Declaration notes (original source)
Publish the pending records to the configured consumer. Do not mutate them while consumption can run.
__vramq_copy

Append one six-byte record that copies len bytes from src to ppu_addr at execution time. The queue retains the source pointer, not the payload. A 255-byte copy still occupies only six command-buffer bytes. Bytes are streamed in increasing source order; select PPU increment 1 for a contiguous destination.

Syntax

c
void __vramq_copy(u16 ppu_addr, const u8* src, u8 len);

Parameters

ppu_addr

PPU byte address (0x0000–0x3FFF), not a CPU pointer. The target must be writable: pattern changes need CHR RAM, not CHR ROM. PPU mapping and mirroring apply. Select increment 1 in PPUCTRL for contiguous copies and fills. Execute during safe PPU timing, then restore the desired scroll state; the executor writes PPUADDR and PPUDATA without restoring scroll settings.

src

Near pointer to readable bytes, retained until execution. Keep the required bytes alive and their memory/ROM bank mapped for the entire flush; no bank number or payload copy is stored. A nonempty transfer requires a valid source. Do not use an expired local array.

len

Number of bytes, from 0 to 255. One record handles the full count; it is not split into 127-byte chunks. Zero still occupies a copy/fill record. On execution, a zero count sets the PPU address but writes no PPUDATA bytes. A value of 256 is outside this argument's range.

Return value

None (void).

Conditions and limits

The append returns no success value. If the whole record does not fit, it sets __vramq_overflow() to 1 and leaves existing records and length intact. Exact fits are accepted. An old overflow flag does not block an append that fits, so handle or reset the latch before using it to judge a new batch. A sequence of appends is not transactional.

FC counts bytes of encoded commands in a fixed 128-byte intrinsic buffer. A tile write costs 4 bytes, a fill record 5 and a pointer-copy record 6, including metadata. A ready queue can be consumed by NMI; inspect or build it before committing, and coordinate the producer with the consumer.

These intrinsics operate on the same command buffer used by lib/vram.c. The nes_vram_queue_* functions in runtime.c manage a separate queue with a different record format. Clearing or querying one does not clear or measure the other.

Build and consume the queue in coordinated phases. Do not append while an interrupt executes it, change its bookkeeping directly, or overwrite retained source data unintentionally. These APIs provide neither locking nor double buffering. Capacity describes the command buffer, not free bytes in hardware VRAM.

Usage example

c
__vramq_copy(0x2040,source_bytes,255);
source_bytes[0]=3; // The executor must see this change through its retained pointer.

This is a calling fragment. Follow the stated initialization and buffer requirements before using it in a complete program.

Expected result

The complete program teaches queue capacity, retained pointers, independent reset flags and immediate versus NMI execution. Expect CAPACITY 128, ENCODED BYTES 030, AFTER EXEC 000, OVERFLOW SEEN 001 and FAILED CHECKS 000. Six records use 30 buffer bytes but perform 512 byte writes. OVERFLOW SEEN preserves the earlier intentional rejection; it is not a claim that the final latch remains set.

The blue copy starts at pixel (0,16), and the green fill at (0,88). Each covers seven full 256-pixel rows of tiles, then 248 pixels of the eighth tile row: 255 tiles total. The final 8 × 8 cell stays black. The copy repeats solid, left-half and triangular 8 × 8 patterns; its first tile is a triangle because the source changed after enqueueing. The fill stays solid even after its caller variable changes. Both shape and color are checked.

At y=160, expect a red solid square at x=16, a red left-half tile at x=224 and a red solid square at x=240. The first survives zero-length copy/fill records; the middle proves default-NMI consumption; the last is a single put. The cancelled cell at (248,160) must remain black. The long transfers run with rendering and NMI off; only the final small write uses NMI. This is NROM emulator evidence, not a real-hardware test.

__vramq_copy: NROM
NROM

Complete example program

Build command

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\vramq_intrinsics.c -I .\kitaqfc\lib -I .\kitaq-docs\samples -o .\out\vramq_intrinsics.nes --mapper=nrom --nes-chr=.\kitaq-docs\samples\api-examples\fc\vram_shapes.chr --no-cache --no-disasm
Implementation and source

kitaqfc/lib/intrinsics.h:150

Declaration notes (original source)
Retain the source address for later consumption; keep its storage and bank mapping valid.
__vramq_exec

Execute a committed queue immediately, in record order. Without readiness, return without changing the queue. After execution, clear length and readiness but preserve overflow. This routine does not wait for safe PPU timing, impose a transfer budget or restore scroll; arrange these conditions at the call site.

Syntax

c
void __vramq_exec(void);

Return value

None (void).

Conditions and limits

PPU byte address (0x0000–0x3FFF), not a CPU pointer. The target must be writable: pattern changes need CHR RAM, not CHR ROM. PPU mapping and mirroring apply. Select increment 1 in PPUCTRL for contiguous copies and fills. Execute during safe PPU timing, then restore the desired scroll state; the executor writes PPUADDR and PPUDATA without restoring scroll settings.

FC counts bytes of encoded commands in a fixed 128-byte intrinsic buffer. A tile write costs 4 bytes, a fill record 5 and a pointer-copy record 6, including metadata. A ready queue can be consumed by NMI; inspect or build it before committing, and coordinate the producer with the consumer.

These intrinsics operate on the same command buffer used by lib/vram.c. The nes_vram_queue_* functions in runtime.c manage a separate queue with a different record format. Clearing or querying one does not clear or measure the other.

Build and consume the queue in coordinated phases. Do not append while an interrupt executes it, change its bookkeeping directly, or overwrite retained source data unintentionally. These APIs provide neither locking nor double buffering. Capacity describes the command buffer, not free bytes in hardware VRAM.

Usage example

c
// Rendering is stopped and the PPU increment is 1.
__vramq_exec();
after_exec=__vramq_len();
if (after_exec!=0 || __vramq_overflow()!=0) failures++;

This is a calling fragment. Follow the stated initialization and buffer requirements before using it in a complete program.

Expected result

The complete program teaches queue capacity, retained pointers, independent reset flags and immediate versus NMI execution. Expect CAPACITY 128, ENCODED BYTES 030, AFTER EXEC 000, OVERFLOW SEEN 001 and FAILED CHECKS 000. Six records use 30 buffer bytes but perform 512 byte writes. OVERFLOW SEEN preserves the earlier intentional rejection; it is not a claim that the final latch remains set.

The blue copy starts at pixel (0,16), and the green fill at (0,88). Each covers seven full 256-pixel rows of tiles, then 248 pixels of the eighth tile row: 255 tiles total. The final 8 × 8 cell stays black. The copy repeats solid, left-half and triangular 8 × 8 patterns; its first tile is a triangle because the source changed after enqueueing. The fill stays solid even after its caller variable changes. Both shape and color are checked.

At y=160, expect a red solid square at x=16, a red left-half tile at x=224 and a red solid square at x=240. The first survives zero-length copy/fill records; the middle proves default-NMI consumption; the last is a single put. The cancelled cell at (248,160) must remain black. The long transfers run with rendering and NMI off; only the final small write uses NMI. This is NROM emulator evidence, not a real-hardware test.

__vramq_exec: NROM
NROM

Complete example program

Build command

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\vramq_intrinsics.c -I .\kitaqfc\lib -I .\kitaq-docs\samples -o .\out\vramq_intrinsics.nes --mapper=nrom --nes-chr=.\kitaq-docs\samples\api-examples\fc\vram_shapes.chr --no-cache --no-disasm
Implementation and source

kitaqfc/lib/intrinsics.h:156

Declaration notes (original source)
Execute a ready queue directly; committing and safe PPU timing are caller responsibilities.
__vramq_fill

Append one five-byte record that repeats the byte value for len PPU writes. The value is stored in the record, so changing a caller variable later does not change the queued fill. This repeats a byte, not a multi-byte tile pattern.

Syntax

c
void __vramq_fill(u16 ppu_addr, u8 value, u8 len);

Parameters

ppu_addr

PPU byte address (0x0000–0x3FFF), not a CPU pointer. The target must be writable: pattern changes need CHR RAM, not CHR ROM. PPU mapping and mirroring apply. Select increment 1 in PPUCTRL for contiguous copies and fills. Execute during safe PPU timing, then restore the desired scroll state; the executor writes PPUADDR and PPUDATA without restoring scroll settings.

value

One byte, 0–255, written repeatedly. Its visible meaning depends on whether the destination contains tile indices, pixel planes or another byte-based structure.

len

Number of bytes, from 0 to 255. One record handles the full count; it is not split into 127-byte chunks. Zero still occupies a copy/fill record. On execution, a zero count sets the PPU address but writes no PPUDATA bytes. A value of 256 is outside this argument's range.

Return value

None (void).

Conditions and limits

The append returns no success value. If the whole record does not fit, it sets __vramq_overflow() to 1 and leaves existing records and length intact. Exact fits are accepted. An old overflow flag does not block an append that fits, so handle or reset the latch before using it to judge a new batch. A sequence of appends is not transactional.

FC counts bytes of encoded commands in a fixed 128-byte intrinsic buffer. A tile write costs 4 bytes, a fill record 5 and a pointer-copy record 6, including metadata. A ready queue can be consumed by NMI; inspect or build it before committing, and coordinate the producer with the consumer.

These intrinsics operate on the same command buffer used by lib/vram.c. The nes_vram_queue_* functions in runtime.c manage a separate queue with a different record format. Clearing or querying one does not clear or measure the other.

Build and consume the queue in coordinated phases. Do not append while an interrupt executes it, change its bookkeeping directly, or overwrite retained source data unintentionally. These APIs provide neither locking nor double buffering. Capacity describes the command buffer, not free bytes in hardware VRAM.

Usage example

c
__vramq_fill(0x2160,fill_value,255); // Seven complete rows and 31 tiles in the eighth.
fill_value=3; // The queued value stays 1, so the green block remains solid.

This is a calling fragment. Follow the stated initialization and buffer requirements before using it in a complete program.

Expected result

The complete program teaches queue capacity, retained pointers, independent reset flags and immediate versus NMI execution. Expect CAPACITY 128, ENCODED BYTES 030, AFTER EXEC 000, OVERFLOW SEEN 001 and FAILED CHECKS 000. Six records use 30 buffer bytes but perform 512 byte writes. OVERFLOW SEEN preserves the earlier intentional rejection; it is not a claim that the final latch remains set.

The blue copy starts at pixel (0,16), and the green fill at (0,88). Each covers seven full 256-pixel rows of tiles, then 248 pixels of the eighth tile row: 255 tiles total. The final 8 × 8 cell stays black. The copy repeats solid, left-half and triangular 8 × 8 patterns; its first tile is a triangle because the source changed after enqueueing. The fill stays solid even after its caller variable changes. Both shape and color are checked.

At y=160, expect a red solid square at x=16, a red left-half tile at x=224 and a red solid square at x=240. The first survives zero-length copy/fill records; the middle proves default-NMI consumption; the last is a single put. The cancelled cell at (248,160) must remain black. The long transfers run with rendering and NMI off; only the final small write uses NMI. This is NROM emulator evidence, not a real-hardware test.

__vramq_fill: NROM
NROM

Complete example program

Build command

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\vramq_intrinsics.c -I .\kitaqfc\lib -I .\kitaq-docs\samples -o .\out\vramq_intrinsics.nes --mapper=nrom --nes-chr=.\kitaq-docs\samples\api-examples\fc\vram_shapes.chr --no-cache --no-disasm
Implementation and source

kitaqfc/lib/intrinsics.h:152

Declaration notes (original source)
Append a repeated-byte update; the source value is stored in the record.
__vramq_len

Return the amount of command-buffer storage currently occupied by pending commands. This is not the number of bytes that will be written to video memory.

Syntax

c
u8 __vramq_len(void);

Return value

An unsigned byte in the platform-specific units described here. Reading it does not modify or execute the queue.

Conditions and limits

FC counts bytes of encoded commands in a fixed 128-byte intrinsic buffer. A tile write costs 4 bytes, a fill record 5 and a pointer-copy record 6, including metadata. A ready queue can be consumed by NMI; inspect or build it before committing, and coordinate the producer with the consumer.

These intrinsics operate on the same command buffer used by lib/vram.c. The nes_vram_queue_* functions in runtime.c manage a separate queue with a different record format. Clearing or querying one does not clear or measure the other.

Build and consume the queue in coordinated phases. Do not append while an interrupt executes it, change its bookkeeping directly, or overwrite retained source data unintentionally. These APIs provide neither locking nor double buffering. Capacity describes the command buffer, not free bytes in hardware VRAM.

Usage example

c
encoded=__vramq_len();
if (encoded!=30 || capacity-encoded!=98) failures++;
// 4+6+5+6+5+4 encoded bytes describe 512 actual byte writes.

This is a calling fragment. Follow the stated initialization and buffer requirements before using it in a complete program.

Expected result

The complete program teaches queue capacity, retained pointers, independent reset flags and immediate versus NMI execution. Expect CAPACITY 128, ENCODED BYTES 030, AFTER EXEC 000, OVERFLOW SEEN 001 and FAILED CHECKS 000. Six records use 30 buffer bytes but perform 512 byte writes. OVERFLOW SEEN preserves the earlier intentional rejection; it is not a claim that the final latch remains set.

The blue copy starts at pixel (0,16), and the green fill at (0,88). Each covers seven full 256-pixel rows of tiles, then 248 pixels of the eighth tile row: 255 tiles total. The final 8 × 8 cell stays black. The copy repeats solid, left-half and triangular 8 × 8 patterns; its first tile is a triangle because the source changed after enqueueing. The fill stays solid even after its caller variable changes. Both shape and color are checked.

At y=160, expect a red solid square at x=16, a red left-half tile at x=224 and a red solid square at x=240. The first survives zero-length copy/fill records; the middle proves default-NMI consumption; the last is a single put. The cancelled cell at (248,160) must remain black. The long transfers run with rendering and NMI off; only the final small write uses NMI. This is NROM emulator evidence, not a real-hardware test.

__vramq_len: NROM
NROM

Complete example program

Build command

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\vramq_intrinsics.c -I .\kitaqfc\lib -I .\kitaq-docs\samples -o .\out\vramq_intrinsics.nes --mapper=nrom --nes-chr=.\kitaq-docs\samples\api-examples\fc\vram_shapes.chr --no-cache --no-disasm
Implementation and source

kitaqfc/lib/intrinsics.h:158

Declaration notes (original source)
Return encoded bytes currently occupied, not transfer payload bytes or command count.
__vramq_overflow

Read the queue-overflow flag without clearing it; it can retain an earlier failure.

Syntax

c
u8 __vramq_overflow(void);

Return value

0 if no overflow is currently latched; 1 after a rejected append until explicit reset. Flushing does not clear this latch.

Conditions and limits

FC counts bytes of encoded commands in a fixed 128-byte intrinsic buffer. A tile write costs 4 bytes, a fill record 5 and a pointer-copy record 6, including metadata. A ready queue can be consumed by NMI; inspect or build it before committing, and coordinate the producer with the consumer.

These intrinsics operate on the same command buffer used by lib/vram.c. The nes_vram_queue_* functions in runtime.c manage a separate queue with a different record format. Clearing or querying one does not clear or measure the other.

Build and consume the queue in coordinated phases. Do not append while an interrupt executes it, change its bookkeeping directly, or overwrite retained source data unintentionally. These APIs provide neither locking nor double buffering. Capacity describes the command buffer, not free bytes in hardware VRAM.

Usage example

c
overflow_seen=__vramq_overflow();
if (overflow_seen!=1 || __vramq_overflow()!=1) failures++;

This is a calling fragment. Follow the stated initialization and buffer requirements before using it in a complete program.

Expected result

The complete program teaches queue capacity, retained pointers, independent reset flags and immediate versus NMI execution. Expect CAPACITY 128, ENCODED BYTES 030, AFTER EXEC 000, OVERFLOW SEEN 001 and FAILED CHECKS 000. Six records use 30 buffer bytes but perform 512 byte writes. OVERFLOW SEEN preserves the earlier intentional rejection; it is not a claim that the final latch remains set.

The blue copy starts at pixel (0,16), and the green fill at (0,88). Each covers seven full 256-pixel rows of tiles, then 248 pixels of the eighth tile row: 255 tiles total. The final 8 × 8 cell stays black. The copy repeats solid, left-half and triangular 8 × 8 patterns; its first tile is a triangle because the source changed after enqueueing. The fill stays solid even after its caller variable changes. Both shape and color are checked.

At y=160, expect a red solid square at x=16, a red left-half tile at x=224 and a red solid square at x=240. The first survives zero-length copy/fill records; the middle proves default-NMI consumption; the last is a single put. The cancelled cell at (248,160) must remain black. The long transfers run with rendering and NMI off; only the final small write uses NMI. This is NROM emulator evidence, not a real-hardware test.

__vramq_overflow: NROM
NROM

Complete example program

Build command

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\vramq_intrinsics.c -I .\kitaqfc\lib -I .\kitaq-docs\samples -o .\out\vramq_intrinsics.nes --mapper=nrom --nes-chr=.\kitaq-docs\samples\api-examples\fc\vram_shapes.chr --no-cache --no-disasm
Implementation and source

kitaqfc/lib/intrinsics.h:160

Declaration notes (original source)
Read the latched capacity failure; reading does not reset it.
__vramq_put

Append one PPU address and byte value as a four-byte record. The value is copied into the queue now, and written when the queue executes. A single nametable tile update is one use; the address may instead name another writable PPU byte.

Syntax

c
void __vramq_put(u16 ppu_addr, u8 value);

Parameters

ppu_addr

PPU byte address (0x0000–0x3FFF), not a CPU pointer. The target must be writable: pattern changes need CHR RAM, not CHR ROM. PPU mapping and mirroring apply. Select increment 1 in PPUCTRL for contiguous copies and fills. Execute during safe PPU timing, then restore the desired scroll state; the executor writes PPUADDR and PPUDATA without restoring scroll settings.

value

One byte, 0–255, written repeatedly. Its visible meaning depends on whether the destination contains tile indices, pixel planes or another byte-based structure.

Return value

None (void).

Conditions and limits

The append returns no success value. If the whole record does not fit, it sets __vramq_overflow() to 1 and leaves existing records and length intact. Exact fits are accepted. An old overflow flag does not block an append that fits, so handle or reset the latch before using it to judge a new batch. A sequence of appends is not transactional.

FC counts bytes of encoded commands in a fixed 128-byte intrinsic buffer. A tile write costs 4 bytes, a fill record 5 and a pointer-copy record 6, including metadata. A ready queue can be consumed by NMI; inspect or build it before committing, and coordinate the producer with the consumer.

These intrinsics operate on the same command buffer used by lib/vram.c. The nes_vram_queue_* functions in runtime.c manage a separate queue with a different record format. Clearing or querying one does not clear or measure the other.

Build and consume the queue in coordinated phases. Do not append while an interrupt executes it, change its bookkeeping directly, or overwrite retained source data unintentionally. These APIs provide neither locking nor double buffering. Capacity describes the command buffer, not free bytes in hardware VRAM.

Usage example

c
__vramq_put(0x2282,1); // Red 8x8 sentinel at pixel (16,160).

This is a calling fragment. Follow the stated initialization and buffer requirements before using it in a complete program.

Expected result

The complete program teaches queue capacity, retained pointers, independent reset flags and immediate versus NMI execution. Expect CAPACITY 128, ENCODED BYTES 030, AFTER EXEC 000, OVERFLOW SEEN 001 and FAILED CHECKS 000. Six records use 30 buffer bytes but perform 512 byte writes. OVERFLOW SEEN preserves the earlier intentional rejection; it is not a claim that the final latch remains set.

The blue copy starts at pixel (0,16), and the green fill at (0,88). Each covers seven full 256-pixel rows of tiles, then 248 pixels of the eighth tile row: 255 tiles total. The final 8 × 8 cell stays black. The copy repeats solid, left-half and triangular 8 × 8 patterns; its first tile is a triangle because the source changed after enqueueing. The fill stays solid even after its caller variable changes. Both shape and color are checked.

At y=160, expect a red solid square at x=16, a red left-half tile at x=224 and a red solid square at x=240. The first survives zero-length copy/fill records; the middle proves default-NMI consumption; the last is a single put. The cancelled cell at (248,160) must remain black. The long transfers run with rendering and NMI off; only the final small write uses NMI. This is NROM emulator evidence, not a real-hardware test.

__vramq_put: NROM
NROM

Complete example program

Build command

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\vramq_intrinsics.c -I .\kitaqfc\lib -I .\kitaq-docs\samples -o .\out\vramq_intrinsics.nes --mapper=nrom --nes-chr=.\kitaq-docs\samples\api-examples\fc\vram_shapes.chr --no-cache --no-disasm
Implementation and source

kitaqfc/lib/intrinsics.h:148

Declaration notes (original source)
Append one address/value update; use the overflow latch to detect capacity failure.
__xy_in_rect

Test membership in a rectangle including its top/left and excluding its bottom/right.

Syntax

c
u8 __xy_in_rect(u8 x, u8 y, u8 rx, u8 ry, u8 rw, u8 rh);

Parameters

x

Point X, 0..255.

y

Point Y, 0..255.

rx

Rectangle left, 0..255.

ry

Rectangle top, 0..255.

rw

Width, 0..255.

rh

Height, 0..255.

Return value

Returns 1 inside, otherwise 0. Zero width or height is empty.

Conditions and limits

Uses coordinate differences: a right bound above 255 does not wrap to the left.

Usage example

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);

This is a calling fragment. Follow the stated initialization and buffer requirements before using it in a complete program.

Expected result

Exercise the interior, boundaries, outside point and zero width: INSIDE=1, RIGHT EDGE=0, LEFT EDGE=0, HIGH X=1. Verify distance 15, wrapped distance 254 and array indices 64/65280.

__xy_in_rect: NROM
NROM

Complete example program

Build command

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\batch3_coordinates.c -I .\kitaqfc\lib -I .\kitaq-docs\samples -o .\out\batch3_coordinates.nes --no-cache --no-disasm --mapper=nrom --nes-chr=.\kitaq-docs\samples\font.chr
Implementation and source

kitaqfc/lib/intrinsics.h:31

Declaration notes (original source)
Return 0 or 1 for half-open bounds. Zero extents are empty; coordinates do not wrap across 255.

kitaqfc/kitaqfc/CodeGenerator.cs:4627

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

void EmitHelperGeometryAndRng()
        {
            EmitHelperStart("__xy_in_rect");
            string xyFalse = NewGeneratedLabel("xy_in_rect_false");
            string xyDone = NewGeneratedLabel("xy_in_rect_done");
            // return (x-rx < rw) && (y-ry < rh), using unsigned subtract to avoid rx+rw overflow.
            EmitAsm("LDA", Mem(CallArgBase));
            EmitAsm("SEC");
            EmitAsm("SBC", Mem(CallArgBase + 2));
            EmitAsm("BCC", Rel(xyFalse));
            EmitAsm("CMP", Mem(CallArgBase + 4));
            EmitAsm("BCS", Rel(xyFalse));
            EmitAsm("LDA", Mem(CallArgBase + 1));
            EmitAsm("SEC");
            EmitAsm("SBC", Mem(CallArgBase + 3));
            EmitAsm("BCC", Rel(xyFalse));
            EmitAsm("CMP", Mem(CallArgBase + 5));
            EmitAsm("BCS", Rel(xyFalse));
            EmitAsm("LDA", Imm(1));
            EmitAsm("JMP", Abs(xyDone));
            _assembly.Add(Expr.Make(Tag.Label, xyFalse));
            EmitAsm("LDA", Imm(0));
            _assembly.Add(Expr.Make(Tag.Label, xyDone));
            EmitAsm("RTS");

            EmitHelperStart("__manhattan");
            // dx = abs(x1-x2)
            EmitAsm("LDA", Mem(CallArgBase));
            EmitAsm("SEC");
            EmitAsm("SBC", Mem(CallArgBase + 2));
            string mdxOk = NewGeneratedLabel("manhattan_dx_ok");
            EmitAsm("BCS", Rel(mdxOk));
            EmitAsm("EOR", Imm(0xFF));
            EmitAsm("CLC");
            EmitAsm("ADC", Imm(1));
            _assembly.Add(Expr.Make(Tag.Label, mdxOk));
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp0Address));
            // dy = abs(y1-y2), then add dx.
            EmitAsm("LDA", Mem(CallArgBase + 1));
            EmitAsm("SEC");
            EmitAsm("SBC", Mem(CallArgBase + 3));
            string mdyOk = NewGeneratedLabel("manhattan_dy_ok");
            EmitAsm("BCS", Rel(mdyOk));
            EmitAsm("EOR", Imm(0xFF));
            EmitAsm("CLC");
            EmitAsm("ADC", Imm(1));
            _assembly.Add(Expr.Make(Tag.Label, mdyOk));
            EmitAsm("CLC");
            EmitAsm("ADC", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("RTS");

            // Save X-coordinate separately while multiplying width by Y, then add X with carry into the high byte.
            EmitHelperStart("__map_index");
            // args: x, y, width. Return y * width + x as u16.
            EmitAsm("LDA", Mem(CallArgBase));      // x
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp2Address));
            EmitAsm("LDA", Mem(CallArgBase + 1));  // y
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp1Address));
            EmitAsm("LDA", Mem(CallArgBase + 2));  // width as u16 multiplicand
            EmitAsm("STA", Mem(CallArgBase));
            EmitAsm("LDA", Imm(0));
            EmitAsm("STA", Mem(CallArgBase + 1));
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp1Address));
            EmitAsm("STA", Mem(CallArgBase + 2));
            EmitAsm("JSR", Abs("__mul16x8"));
            EmitAsm("CLC");
            EmitAsm("ADC", Mem(_runtimeIntrinsicTmp2Address));
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("TXA");
            EmitAsm("ADC", Imm(0));
            EmitAsm("TAX");
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("RTS");

            // Replace both bytes of the persistent generator state with the supplied seed.
            EmitHelperStart("__rng_seed");
            EmitAsm("LDA", Mem(CallArgBase));
            EmitAsm("STA", Mem(_runtimeRngLoAddress));
            EmitAsm("LDA", Mem(CallArgBase + 1));
            EmitAsm("STA", Mem(_runtimeRngHiAddress));
            EmitAsm("RTS");

            EmitHelperStart("__rng8");
            // 16-bit Galois LFSR: if low bit set, shift right then xor $B400.
            EmitAsm("LDA", Mem(_runtimeRngLoAddress));
            EmitAsm("ORA", Mem(_runtimeRngHiAddress));
            // Replace the all-zero LFSR state with a deterministic nonzero seed before advancing.
            string seeded = NewGeneratedLabel("rng_seeded");
            EmitAsm("BNE", Rel(seeded));
            EmitAsm("LDA", Imm(0x5A));
            EmitAsm("STA", Mem(_runtimeRngLoAddress));
            EmitAsm("LDA", Imm(0xA5));
            EmitAsm("STA", Mem(_runtimeRngHiAddress));
            _assembly.Add(Expr.Make(Tag.Label, seeded));
            EmitAsm("LDA", Mem(_runtimeRngLoAddress));
            EmitAsm("AND", Imm(1));
            EmitAsm("STA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("LSR", Mem(_runtimeRngHiAddress));
            EmitAsm("ROR", Mem(_runtimeRngLoAddress));
            string rngNoXor = NewGeneratedLabel("rng_no_xor");
            EmitAsm("LDA", Mem(_runtimeIntrinsicTmp0Address));
            EmitAsm("BEQ", Rel(rngNoXor));
            EmitAsm("LDA", Mem(_runtimeRngHiAddress));
            EmitAsm("EOR", Imm(0xB4));
            EmitAsm("STA", Mem(_runtimeRngHiAddress));
            _assembly.Add(Expr.Make(Tag.Label, rngNoXor));
            EmitAsm("LDA", Mem(_runtimeRngLoAddress));
            EmitAsm("EOR", Mem(_runtimeRngHiAddress));
            EmitAsm("RTS");
        }
__zapper_light

The default port-2 light reader. Calls the same helper as __zapper_light2().

Syntax

c
u8 __zapper_light(void);

Return value

1 when $4017 bit 3 is clear, otherwise 0. Port 1 is not read.

Conditions and limits

These APIs access $4016/$4017 directly. Do not interleave interrupt-driven gamepad, keyboard or serial operations on the same ports. Controller shift positions can also be affected. The sample disables NMI while capturing input, then enables it when initializing the display.

Reads the instantaneous signal. It does not strobe, wait for a frame, detect an edge, locate an aim point or decide a hit. Separate raw and trigger/light calls each read the register at their own time, so their values can differ while the signal changes.

The light input is active-low: a clear bit 3 produces a return value of one. That conversion does not detect whether a gun is connected; a gamepad or unconnected input may also yield one. Drawing a target on screen does not turn this into a hit-testing API.

Bit conversion is also unit-tested in an owned test ROM by redirecting register reads to RAM fixture values. The tests cover 00/08/10/18 combinations with unrelated bits set. This unit test is separate from the unmodified sample run and from real peripheral behavior.

These images run the downloadable ROMs unchanged in KUROSAKI. Its current input bus models gamepads but not keyboard, light-gun or serial-adapter signals. The external input bits read as zero, so the captures do not verify those devices. Physical key input, optical sensing and communication remain untested.

NESdev: Zapper

Usage example

c
#include "fc_ports_scene.h"
void main(void){
    __irq_disable();__ppu_ctrl_set(0);port_phase=1;
    port_result[0]=__zapper_raw1();port_result[1]=__zapper_trigger1();port_result[2]=__zapper_light1();
    port_result[3]=__zapper_raw2();port_result[4]=__zapper_trigger2();port_result[5]=__zapper_light2();
    port_result[6]=__zapper_trigger();port_result[7]=__zapper_light();
    port_phase=2;port_result[31]=165;
    m_init();m_text(2,2,"LIGHT-GUN INPUT SNAPSHOT");m_wait();
    m_text(2,5,"PORT   RAW  TRIGGER  LIGHT");m_wait();
    m_put(3,8,'1');port_hex(9,8,port_result[0]);port_number(15,8,port_result[1]);port_number(24,8,port_result[2]);m_wait();
    m_put(3,10,'2');port_hex(9,10,port_result[3]);port_number(15,10,port_result[4]);port_number(24,10,port_result[5]);m_wait();
    m_text(2,13,"DEFAULT = PORT 2");m_wait();
    m_text(2,15,"TRIGGER");port_number(15,15,port_result[6]);m_wait();
    m_text(2,17,"LIGHT");port_number(15,17,port_result[7]);m_wait();
    m_text(2,21,"LIGHT 1 IS NOT A HIT TEST");m_wait();
    while(1){m_wait();}
}

This is a calling fragment. Follow the stated initialization and buffer requirements before using it in a complete program.

Expected result

In KUROSAKI, both ports show RAW=00, TRIGGER=000 and LIGHT=001. The unsuffixed calls show the same 000/001 as port 2. LIGHT=001 comes from unmodeled input bit 3 reading zero, not from detecting screen brightness. The screen also states LIGHT 1 IS NOT A HIT TEST.

__zapper_light: ZAPPER-DEFAULT — In KUROSAKI, both ports show RAW=00, TRIGGER=000 and LIGHT=001. The unsuffixed calls show the same 000/001 as port 2. LIGHT=001 comes from unmodeled input bit 3 reading zero, not from detecting screen brightness. The screen also states LIGHT 1 IS NOT A HIT TEST.
ZAPPER-DEFAULT — In KUROSAKI, both ports show RAW=00, TRIGGER=000 and LIGHT=001. The unsuffixed calls show the same 000/001 as port 2. LIGHT=001 comes from unmodeled input bit 3 reading zero, not from detecting screen brightness. The screen also states LIGHT 1 IS NOT A HIT TEST.

Complete example program

Build command

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\peripheral_zapper.c -I .\kitaqfc\lib -I .\kitaq-docs\samples -o .\out\peripheral_zapper.nes --mapper=nrom --nes-chr=.\kitaq-docs\samples\font.chr --no-cache --no-disasm
.\kurosaki\kurosaki.exe run .\out\peripheral_zapper.nes --frames 120 --png .\out\peripheral_zapper.png
Implementation and source

kitaqfc/lib/intrinsics.h:253

Declaration notes (original source)
Default to the port-2 normalized light reader.

kitaqfc/kitaqfc/CodeGenerator.cs:7753

csharp
EmitHelperStart("__zapper_light2");
            EmitNormalizeBoolFromMask(0x4017, 0x08, true);
        void EmitNormalizeBoolFromMask(int port, int mask, bool invert)
        {
            string zero = NewGeneratedLabel("bool_zero");
            string done = NewGeneratedLabel("bool_done");
            EmitAsm("LDA", Mem(port));
            EmitAsm("AND", Imm(mask));
            if (invert) EmitAsm("BNE", Rel(zero)); else EmitAsm("BEQ", Rel(zero));
            EmitAsm("LDA", Imm(1));
            EmitAsm("JMP", Abs(done));
            _assembly.Add(Expr.Make(Tag.Label, zero));
            EmitAsm("LDA", Imm(0));
            _assembly.Add(Expr.Make(Tag.Label, done));
            EmitAsm("RTS");
        }
__zapper_light1

Read the light-sense line on port 1 ($4016), converting the active-low input to one for sensed light.

Syntax

c
u8 __zapper_light1(void);

Return value

1 when $4016 bit 3 is clear; 0 when it is set.

Conditions and limits

These APIs access $4016/$4017 directly. Do not interleave interrupt-driven gamepad, keyboard or serial operations on the same ports. Controller shift positions can also be affected. The sample disables NMI while capturing input, then enables it when initializing the display.

Reads the instantaneous signal. It does not strobe, wait for a frame, detect an edge, locate an aim point or decide a hit. Separate raw and trigger/light calls each read the register at their own time, so their values can differ while the signal changes.

The light input is active-low: a clear bit 3 produces a return value of one. That conversion does not detect whether a gun is connected; a gamepad or unconnected input may also yield one. Drawing a target on screen does not turn this into a hit-testing API.

Bit conversion is also unit-tested in an owned test ROM by redirecting register reads to RAM fixture values. The tests cover 00/08/10/18 combinations with unrelated bits set. This unit test is separate from the unmodified sample run and from real peripheral behavior.

These images run the downloadable ROMs unchanged in KUROSAKI. Its current input bus models gamepads but not keyboard, light-gun or serial-adapter signals. The external input bits read as zero, so the captures do not verify those devices. Physical key input, optical sensing and communication remain untested.

NESdev: Zapper

Usage example

c
#include "fc_ports_scene.h"
void main(void){
    __irq_disable();__ppu_ctrl_set(0);port_phase=1;
    port_result[0]=__zapper_raw1();port_result[1]=__zapper_trigger1();port_result[2]=__zapper_light1();
    port_result[3]=__zapper_raw2();port_result[4]=__zapper_trigger2();port_result[5]=__zapper_light2();
    port_result[6]=__zapper_trigger();port_result[7]=__zapper_light();
    port_phase=2;port_result[31]=165;
    m_init();m_text(2,2,"LIGHT-GUN INPUT SNAPSHOT");m_wait();
    m_text(2,5,"PORT   RAW  TRIGGER  LIGHT");m_wait();
    m_put(3,8,'1');port_hex(9,8,port_result[0]);port_number(15,8,port_result[1]);port_number(24,8,port_result[2]);m_wait();
    m_put(3,10,'2');port_hex(9,10,port_result[3]);port_number(15,10,port_result[4]);port_number(24,10,port_result[5]);m_wait();
    m_text(2,13,"DEFAULT = PORT 2");m_wait();
    m_text(2,15,"TRIGGER");port_number(15,15,port_result[6]);m_wait();
    m_text(2,17,"LIGHT");port_number(15,17,port_result[7]);m_wait();
    m_text(2,21,"LIGHT 1 IS NOT A HIT TEST");m_wait();
    while(1){m_wait();}
}

This is a calling fragment. Follow the stated initialization and buffer requirements before using it in a complete program.

Expected result

In KUROSAKI, both ports show RAW=00, TRIGGER=000 and LIGHT=001. The unsuffixed calls show the same 000/001 as port 2. LIGHT=001 comes from unmodeled input bit 3 reading zero, not from detecting screen brightness. The screen also states LIGHT 1 IS NOT A HIT TEST.

__zapper_light1: ZAPPER-DEFAULT — In KUROSAKI, both ports show RAW=00, TRIGGER=000 and LIGHT=001. The unsuffixed calls show the same 000/001 as port 2. LIGHT=001 comes from unmodeled input bit 3 reading zero, not from detecting screen brightness. The screen also states LIGHT 1 IS NOT A HIT TEST.
ZAPPER-DEFAULT — In KUROSAKI, both ports show RAW=00, TRIGGER=000 and LIGHT=001. The unsuffixed calls show the same 000/001 as port 2. LIGHT=001 comes from unmodeled input bit 3 reading zero, not from detecting screen brightness. The screen also states LIGHT 1 IS NOT A HIT TEST.

Complete example program

Build command

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\peripheral_zapper.c -I .\kitaqfc\lib -I .\kitaq-docs\samples -o .\out\peripheral_zapper.nes --mapper=nrom --nes-chr=.\kitaq-docs\samples\font.chr --no-cache --no-disasm
.\kurosaki\kurosaki.exe run .\out\peripheral_zapper.nes --frames 120 --png .\out\peripheral_zapper.png
Implementation and source

kitaqfc/lib/intrinsics.h:247

Declaration notes (original source)
Invert port-1 bit 3 and normalize: one means light detected.

kitaqfc/kitaqfc/CodeGenerator.cs:7751

csharp
EmitHelperStart("__zapper_light1");
            EmitNormalizeBoolFromMask(0x4016, 0x08, true);
        void EmitNormalizeBoolFromMask(int port, int mask, bool invert)
        {
            string zero = NewGeneratedLabel("bool_zero");
            string done = NewGeneratedLabel("bool_done");
            EmitAsm("LDA", Mem(port));
            EmitAsm("AND", Imm(mask));
            if (invert) EmitAsm("BNE", Rel(zero)); else EmitAsm("BEQ", Rel(zero));
            EmitAsm("LDA", Imm(1));
            EmitAsm("JMP", Abs(done));
            _assembly.Add(Expr.Make(Tag.Label, zero));
            EmitAsm("LDA", Imm(0));
            _assembly.Add(Expr.Make(Tag.Label, done));
            EmitAsm("RTS");
        }
__zapper_light2

Read the light-sense line on port 2 ($4017), converting the active-low input to one for sensed light.

Syntax

c
u8 __zapper_light2(void);

Return value

1 when $4017 bit 3 is clear; 0 when it is set.

Conditions and limits

These APIs access $4016/$4017 directly. Do not interleave interrupt-driven gamepad, keyboard or serial operations on the same ports. Controller shift positions can also be affected. The sample disables NMI while capturing input, then enables it when initializing the display.

Reads the instantaneous signal. It does not strobe, wait for a frame, detect an edge, locate an aim point or decide a hit. Separate raw and trigger/light calls each read the register at their own time, so their values can differ while the signal changes.

The light input is active-low: a clear bit 3 produces a return value of one. That conversion does not detect whether a gun is connected; a gamepad or unconnected input may also yield one. Drawing a target on screen does not turn this into a hit-testing API.

Bit conversion is also unit-tested in an owned test ROM by redirecting register reads to RAM fixture values. The tests cover 00/08/10/18 combinations with unrelated bits set. This unit test is separate from the unmodified sample run and from real peripheral behavior.

These images run the downloadable ROMs unchanged in KUROSAKI. Its current input bus models gamepads but not keyboard, light-gun or serial-adapter signals. The external input bits read as zero, so the captures do not verify those devices. Physical key input, optical sensing and communication remain untested.

NESdev: Zapper

Usage example

c
#include "fc_ports_scene.h"
void main(void){
    __irq_disable();__ppu_ctrl_set(0);port_phase=1;
    port_result[0]=__zapper_raw1();port_result[1]=__zapper_trigger1();port_result[2]=__zapper_light1();
    port_result[3]=__zapper_raw2();port_result[4]=__zapper_trigger2();port_result[5]=__zapper_light2();
    port_result[6]=__zapper_trigger();port_result[7]=__zapper_light();
    port_phase=2;port_result[31]=165;
    m_init();m_text(2,2,"LIGHT-GUN INPUT SNAPSHOT");m_wait();
    m_text(2,5,"PORT   RAW  TRIGGER  LIGHT");m_wait();
    m_put(3,8,'1');port_hex(9,8,port_result[0]);port_number(15,8,port_result[1]);port_number(24,8,port_result[2]);m_wait();
    m_put(3,10,'2');port_hex(9,10,port_result[3]);port_number(15,10,port_result[4]);port_number(24,10,port_result[5]);m_wait();
    m_text(2,13,"DEFAULT = PORT 2");m_wait();
    m_text(2,15,"TRIGGER");port_number(15,15,port_result[6]);m_wait();
    m_text(2,17,"LIGHT");port_number(15,17,port_result[7]);m_wait();
    m_text(2,21,"LIGHT 1 IS NOT A HIT TEST");m_wait();
    while(1){m_wait();}
}

This is a calling fragment. Follow the stated initialization and buffer requirements before using it in a complete program.

Expected result

In KUROSAKI, both ports show RAW=00, TRIGGER=000 and LIGHT=001. The unsuffixed calls show the same 000/001 as port 2. LIGHT=001 comes from unmodeled input bit 3 reading zero, not from detecting screen brightness. The screen also states LIGHT 1 IS NOT A HIT TEST.

__zapper_light2: ZAPPER-DEFAULT — In KUROSAKI, both ports show RAW=00, TRIGGER=000 and LIGHT=001. The unsuffixed calls show the same 000/001 as port 2. LIGHT=001 comes from unmodeled input bit 3 reading zero, not from detecting screen brightness. The screen also states LIGHT 1 IS NOT A HIT TEST.
ZAPPER-DEFAULT — In KUROSAKI, both ports show RAW=00, TRIGGER=000 and LIGHT=001. The unsuffixed calls show the same 000/001 as port 2. LIGHT=001 comes from unmodeled input bit 3 reading zero, not from detecting screen brightness. The screen also states LIGHT 1 IS NOT A HIT TEST.

Complete example program

Build command

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\peripheral_zapper.c -I .\kitaqfc\lib -I .\kitaq-docs\samples -o .\out\peripheral_zapper.nes --mapper=nrom --nes-chr=.\kitaq-docs\samples\font.chr --no-cache --no-disasm
.\kurosaki\kurosaki.exe run .\out\peripheral_zapper.nes --frames 120 --png .\out\peripheral_zapper.png
Implementation and source

kitaqfc/lib/intrinsics.h:249

Declaration notes (original source)
Invert port-2 bit 3 and normalize: one means light detected.

kitaqfc/kitaqfc/CodeGenerator.cs:7753

csharp
EmitHelperStart("__zapper_light2");
            EmitNormalizeBoolFromMask(0x4017, 0x08, true);
        void EmitNormalizeBoolFromMask(int port, int mask, bool invert)
        {
            string zero = NewGeneratedLabel("bool_zero");
            string done = NewGeneratedLabel("bool_done");
            EmitAsm("LDA", Mem(port));
            EmitAsm("AND", Imm(mask));
            if (invert) EmitAsm("BNE", Rel(zero)); else EmitAsm("BEQ", Rel(zero));
            EmitAsm("LDA", Imm(1));
            EmitAsm("JMP", Abs(done));
            _assembly.Add(Expr.Make(Tag.Label, zero));
            EmitAsm("LDA", Imm(0));
            _assembly.Add(Expr.Make(Tag.Label, done));
            EmitAsm("RTS");
        }
__zapper_raw1

Read both light-gun bits from port 1 ($4016) together when trigger and light must be decoded from the same register read.

Syntax

c
u8 __zapper_raw1(void);

Return value

Returns $4016 & 0x18: 0x00, 0x08, 0x10 or 0x18. Bit 4 is the trigger line; bit 3 is the raw no-light signal.

Conditions and limits

These APIs access $4016/$4017 directly. Do not interleave interrupt-driven gamepad, keyboard or serial operations on the same ports. Controller shift positions can also be affected. The sample disables NMI while capturing input, then enables it when initializing the display.

Reads the instantaneous signal. It does not strobe, wait for a frame, detect an edge, locate an aim point or decide a hit. Separate raw and trigger/light calls each read the register at their own time, so their values can differ while the signal changes.

The light input is active-low: a clear bit 3 produces a return value of one. That conversion does not detect whether a gun is connected; a gamepad or unconnected input may also yield one. Drawing a target on screen does not turn this into a hit-testing API.

Bit conversion is also unit-tested in an owned test ROM by redirecting register reads to RAM fixture values. The tests cover 00/08/10/18 combinations with unrelated bits set. This unit test is separate from the unmodified sample run and from real peripheral behavior.

These images run the downloadable ROMs unchanged in KUROSAKI. Its current input bus models gamepads but not keyboard, light-gun or serial-adapter signals. The external input bits read as zero, so the captures do not verify those devices. Physical key input, optical sensing and communication remain untested.

NESdev: Zapper

Usage example

c
#include "fc_ports_scene.h"
void main(void){
    __irq_disable();__ppu_ctrl_set(0);port_phase=1;
    port_result[0]=__zapper_raw1();port_result[1]=__zapper_trigger1();port_result[2]=__zapper_light1();
    port_result[3]=__zapper_raw2();port_result[4]=__zapper_trigger2();port_result[5]=__zapper_light2();
    port_result[6]=__zapper_trigger();port_result[7]=__zapper_light();
    port_phase=2;port_result[31]=165;
    m_init();m_text(2,2,"LIGHT-GUN INPUT SNAPSHOT");m_wait();
    m_text(2,5,"PORT   RAW  TRIGGER  LIGHT");m_wait();
    m_put(3,8,'1');port_hex(9,8,port_result[0]);port_number(15,8,port_result[1]);port_number(24,8,port_result[2]);m_wait();
    m_put(3,10,'2');port_hex(9,10,port_result[3]);port_number(15,10,port_result[4]);port_number(24,10,port_result[5]);m_wait();
    m_text(2,13,"DEFAULT = PORT 2");m_wait();
    m_text(2,15,"TRIGGER");port_number(15,15,port_result[6]);m_wait();
    m_text(2,17,"LIGHT");port_number(15,17,port_result[7]);m_wait();
    m_text(2,21,"LIGHT 1 IS NOT A HIT TEST");m_wait();
    while(1){m_wait();}
}

This is a calling fragment. Follow the stated initialization and buffer requirements before using it in a complete program.

Expected result

In KUROSAKI, both ports show RAW=00, TRIGGER=000 and LIGHT=001. The unsuffixed calls show the same 000/001 as port 2. LIGHT=001 comes from unmodeled input bit 3 reading zero, not from detecting screen brightness. The screen also states LIGHT 1 IS NOT A HIT TEST.

__zapper_raw1: ZAPPER-DEFAULT — In KUROSAKI, both ports show RAW=00, TRIGGER=000 and LIGHT=001. The unsuffixed calls show the same 000/001 as port 2. LIGHT=001 comes from unmodeled input bit 3 reading zero, not from detecting screen brightness. The screen also states LIGHT 1 IS NOT A HIT TEST.
ZAPPER-DEFAULT — In KUROSAKI, both ports show RAW=00, TRIGGER=000 and LIGHT=001. The unsuffixed calls show the same 000/001 as port 2. LIGHT=001 comes from unmodeled input bit 3 reading zero, not from detecting screen brightness. The screen also states LIGHT 1 IS NOT A HIT TEST.

Complete example program

Build command

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\peripheral_zapper.c -I .\kitaqfc\lib -I .\kitaq-docs\samples -o .\out\peripheral_zapper.nes --mapper=nrom --nes-chr=.\kitaq-docs\samples\font.chr --no-cache --no-disasm
.\kurosaki\kurosaki.exe run .\out\peripheral_zapper.nes --frames 120 --png .\out\peripheral_zapper.png
Implementation and source

kitaqfc/lib/intrinsics.h:239

Declaration notes (original source)
Zapper / light gun. The no-suffix names default to the usual port-2/Famicom
expansion-port mapping. __zapper_light*() returns 1 when light is detected.

Return raw 4016 & 18, retaining trigger and active-low light bits.

kitaqfc/kitaqfc/CodeGenerator.cs:4775

csharp
case "__zapper_raw1": if (!Need(0)) return true; EmitAsm("LDA", Mem(0x4016)); EmitAsm("AND", Imm(0x18)); return true;
__zapper_raw2

Read both light-gun bits from port 2 ($4017) together when trigger and light must be decoded from the same register read.

Syntax

c
u8 __zapper_raw2(void);

Return value

Returns $4017 & 0x18: 0x00, 0x08, 0x10 or 0x18. Bit 4 is the trigger line; bit 3 is the raw no-light signal.

Conditions and limits

These APIs access $4016/$4017 directly. Do not interleave interrupt-driven gamepad, keyboard or serial operations on the same ports. Controller shift positions can also be affected. The sample disables NMI while capturing input, then enables it when initializing the display.

Reads the instantaneous signal. It does not strobe, wait for a frame, detect an edge, locate an aim point or decide a hit. Separate raw and trigger/light calls each read the register at their own time, so their values can differ while the signal changes.

The light input is active-low: a clear bit 3 produces a return value of one. That conversion does not detect whether a gun is connected; a gamepad or unconnected input may also yield one. Drawing a target on screen does not turn this into a hit-testing API.

Bit conversion is also unit-tested in an owned test ROM by redirecting register reads to RAM fixture values. The tests cover 00/08/10/18 combinations with unrelated bits set. This unit test is separate from the unmodified sample run and from real peripheral behavior.

These images run the downloadable ROMs unchanged in KUROSAKI. Its current input bus models gamepads but not keyboard, light-gun or serial-adapter signals. The external input bits read as zero, so the captures do not verify those devices. Physical key input, optical sensing and communication remain untested.

NESdev: Zapper

Usage example

c
#include "fc_ports_scene.h"
void main(void){
    __irq_disable();__ppu_ctrl_set(0);port_phase=1;
    port_result[0]=__zapper_raw1();port_result[1]=__zapper_trigger1();port_result[2]=__zapper_light1();
    port_result[3]=__zapper_raw2();port_result[4]=__zapper_trigger2();port_result[5]=__zapper_light2();
    port_result[6]=__zapper_trigger();port_result[7]=__zapper_light();
    port_phase=2;port_result[31]=165;
    m_init();m_text(2,2,"LIGHT-GUN INPUT SNAPSHOT");m_wait();
    m_text(2,5,"PORT   RAW  TRIGGER  LIGHT");m_wait();
    m_put(3,8,'1');port_hex(9,8,port_result[0]);port_number(15,8,port_result[1]);port_number(24,8,port_result[2]);m_wait();
    m_put(3,10,'2');port_hex(9,10,port_result[3]);port_number(15,10,port_result[4]);port_number(24,10,port_result[5]);m_wait();
    m_text(2,13,"DEFAULT = PORT 2");m_wait();
    m_text(2,15,"TRIGGER");port_number(15,15,port_result[6]);m_wait();
    m_text(2,17,"LIGHT");port_number(15,17,port_result[7]);m_wait();
    m_text(2,21,"LIGHT 1 IS NOT A HIT TEST");m_wait();
    while(1){m_wait();}
}

This is a calling fragment. Follow the stated initialization and buffer requirements before using it in a complete program.

Expected result

In KUROSAKI, both ports show RAW=00, TRIGGER=000 and LIGHT=001. The unsuffixed calls show the same 000/001 as port 2. LIGHT=001 comes from unmodeled input bit 3 reading zero, not from detecting screen brightness. The screen also states LIGHT 1 IS NOT A HIT TEST.

__zapper_raw2: ZAPPER-DEFAULT — In KUROSAKI, both ports show RAW=00, TRIGGER=000 and LIGHT=001. The unsuffixed calls show the same 000/001 as port 2. LIGHT=001 comes from unmodeled input bit 3 reading zero, not from detecting screen brightness. The screen also states LIGHT 1 IS NOT A HIT TEST.
ZAPPER-DEFAULT — In KUROSAKI, both ports show RAW=00, TRIGGER=000 and LIGHT=001. The unsuffixed calls show the same 000/001 as port 2. LIGHT=001 comes from unmodeled input bit 3 reading zero, not from detecting screen brightness. The screen also states LIGHT 1 IS NOT A HIT TEST.

Complete example program

Build command

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\peripheral_zapper.c -I .\kitaqfc\lib -I .\kitaq-docs\samples -o .\out\peripheral_zapper.nes --mapper=nrom --nes-chr=.\kitaq-docs\samples\font.chr --no-cache --no-disasm
.\kurosaki\kurosaki.exe run .\out\peripheral_zapper.nes --frames 120 --png .\out\peripheral_zapper.png
Implementation and source

kitaqfc/lib/intrinsics.h:241

Declaration notes (original source)
Return raw 4017 & 18 for the second port.

kitaqfc/kitaqfc/CodeGenerator.cs:4776

csharp
case "__zapper_raw2": if (!Need(0)) return true; EmitAsm("LDA", Mem(0x4017)); EmitAsm("AND", Imm(0x18)); return true;
__zapper_trigger

The default port-2 trigger reader. Calls the same helper as __zapper_trigger2().

Syntax

c
u8 __zapper_trigger(void);

Return value

The normalized $4017 bit 4. It never automatically switches to port 1.

Conditions and limits

These APIs access $4016/$4017 directly. Do not interleave interrupt-driven gamepad, keyboard or serial operations on the same ports. Controller shift positions can also be affected. The sample disables NMI while capturing input, then enables it when initializing the display.

Reads the instantaneous signal. It does not strobe, wait for a frame, detect an edge, locate an aim point or decide a hit. Separate raw and trigger/light calls each read the register at their own time, so their values can differ while the signal changes.

The result is the trigger line’s logical state. It does not guarantee that the trigger remains fully pulled, and switch behavior depends on the device. For a game firing event, compare with the previous reading to detect the desired transition.

Bit conversion is also unit-tested in an owned test ROM by redirecting register reads to RAM fixture values. The tests cover 00/08/10/18 combinations with unrelated bits set. This unit test is separate from the unmodified sample run and from real peripheral behavior.

These images run the downloadable ROMs unchanged in KUROSAKI. Its current input bus models gamepads but not keyboard, light-gun or serial-adapter signals. The external input bits read as zero, so the captures do not verify those devices. Physical key input, optical sensing and communication remain untested.

NESdev: Zapper

Usage example

c
#include "fc_ports_scene.h"
void main(void){
    __irq_disable();__ppu_ctrl_set(0);port_phase=1;
    port_result[0]=__zapper_raw1();port_result[1]=__zapper_trigger1();port_result[2]=__zapper_light1();
    port_result[3]=__zapper_raw2();port_result[4]=__zapper_trigger2();port_result[5]=__zapper_light2();
    port_result[6]=__zapper_trigger();port_result[7]=__zapper_light();
    port_phase=2;port_result[31]=165;
    m_init();m_text(2,2,"LIGHT-GUN INPUT SNAPSHOT");m_wait();
    m_text(2,5,"PORT   RAW  TRIGGER  LIGHT");m_wait();
    m_put(3,8,'1');port_hex(9,8,port_result[0]);port_number(15,8,port_result[1]);port_number(24,8,port_result[2]);m_wait();
    m_put(3,10,'2');port_hex(9,10,port_result[3]);port_number(15,10,port_result[4]);port_number(24,10,port_result[5]);m_wait();
    m_text(2,13,"DEFAULT = PORT 2");m_wait();
    m_text(2,15,"TRIGGER");port_number(15,15,port_result[6]);m_wait();
    m_text(2,17,"LIGHT");port_number(15,17,port_result[7]);m_wait();
    m_text(2,21,"LIGHT 1 IS NOT A HIT TEST");m_wait();
    while(1){m_wait();}
}

This is a calling fragment. Follow the stated initialization and buffer requirements before using it in a complete program.

Expected result

In KUROSAKI, both ports show RAW=00, TRIGGER=000 and LIGHT=001. The unsuffixed calls show the same 000/001 as port 2. LIGHT=001 comes from unmodeled input bit 3 reading zero, not from detecting screen brightness. The screen also states LIGHT 1 IS NOT A HIT TEST.

__zapper_trigger: ZAPPER-DEFAULT — In KUROSAKI, both ports show RAW=00, TRIGGER=000 and LIGHT=001. The unsuffixed calls show the same 000/001 as port 2. LIGHT=001 comes from unmodeled input bit 3 reading zero, not from detecting screen brightness. The screen also states LIGHT 1 IS NOT A HIT TEST.
ZAPPER-DEFAULT — In KUROSAKI, both ports show RAW=00, TRIGGER=000 and LIGHT=001. The unsuffixed calls show the same 000/001 as port 2. LIGHT=001 comes from unmodeled input bit 3 reading zero, not from detecting screen brightness. The screen also states LIGHT 1 IS NOT A HIT TEST.

Complete example program

Build command

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\peripheral_zapper.c -I .\kitaqfc\lib -I .\kitaq-docs\samples -o .\out\peripheral_zapper.nes --mapper=nrom --nes-chr=.\kitaq-docs\samples\font.chr --no-cache --no-disasm
.\kurosaki\kurosaki.exe run .\out\peripheral_zapper.nes --frames 120 --png .\out\peripheral_zapper.png
Implementation and source

kitaqfc/lib/intrinsics.h:251

Declaration notes (original source)
Default to the port-2 normalized trigger reader.

kitaqfc/kitaqfc/CodeGenerator.cs:7749

csharp
EmitHelperStart("__zapper_trigger2");
            EmitNormalizeBoolFromMask(0x4017, 0x10, false);
        void EmitNormalizeBoolFromMask(int port, int mask, bool invert)
        {
            string zero = NewGeneratedLabel("bool_zero");
            string done = NewGeneratedLabel("bool_done");
            EmitAsm("LDA", Mem(port));
            EmitAsm("AND", Imm(mask));
            if (invert) EmitAsm("BNE", Rel(zero)); else EmitAsm("BEQ", Rel(zero));
            EmitAsm("LDA", Imm(1));
            EmitAsm("JMP", Abs(done));
            _assembly.Add(Expr.Make(Tag.Label, zero));
            EmitAsm("LDA", Imm(0));
            _assembly.Add(Expr.Make(Tag.Label, done));
            EmitAsm("RTS");
        }
__zapper_trigger1

Read and normalize the trigger line on port 1 ($4016).

Syntax

c
u8 __zapper_trigger1(void);

Return value

1 when $4016 bit 4 is set, otherwise 0.

Conditions and limits

These APIs access $4016/$4017 directly. Do not interleave interrupt-driven gamepad, keyboard or serial operations on the same ports. Controller shift positions can also be affected. The sample disables NMI while capturing input, then enables it when initializing the display.

Reads the instantaneous signal. It does not strobe, wait for a frame, detect an edge, locate an aim point or decide a hit. Separate raw and trigger/light calls each read the register at their own time, so their values can differ while the signal changes.

The result is the trigger line’s logical state. It does not guarantee that the trigger remains fully pulled, and switch behavior depends on the device. For a game firing event, compare with the previous reading to detect the desired transition.

Bit conversion is also unit-tested in an owned test ROM by redirecting register reads to RAM fixture values. The tests cover 00/08/10/18 combinations with unrelated bits set. This unit test is separate from the unmodified sample run and from real peripheral behavior.

These images run the downloadable ROMs unchanged in KUROSAKI. Its current input bus models gamepads but not keyboard, light-gun or serial-adapter signals. The external input bits read as zero, so the captures do not verify those devices. Physical key input, optical sensing and communication remain untested.

NESdev: Zapper

Usage example

c
#include "fc_ports_scene.h"
void main(void){
    __irq_disable();__ppu_ctrl_set(0);port_phase=1;
    port_result[0]=__zapper_raw1();port_result[1]=__zapper_trigger1();port_result[2]=__zapper_light1();
    port_result[3]=__zapper_raw2();port_result[4]=__zapper_trigger2();port_result[5]=__zapper_light2();
    port_result[6]=__zapper_trigger();port_result[7]=__zapper_light();
    port_phase=2;port_result[31]=165;
    m_init();m_text(2,2,"LIGHT-GUN INPUT SNAPSHOT");m_wait();
    m_text(2,5,"PORT   RAW  TRIGGER  LIGHT");m_wait();
    m_put(3,8,'1');port_hex(9,8,port_result[0]);port_number(15,8,port_result[1]);port_number(24,8,port_result[2]);m_wait();
    m_put(3,10,'2');port_hex(9,10,port_result[3]);port_number(15,10,port_result[4]);port_number(24,10,port_result[5]);m_wait();
    m_text(2,13,"DEFAULT = PORT 2");m_wait();
    m_text(2,15,"TRIGGER");port_number(15,15,port_result[6]);m_wait();
    m_text(2,17,"LIGHT");port_number(15,17,port_result[7]);m_wait();
    m_text(2,21,"LIGHT 1 IS NOT A HIT TEST");m_wait();
    while(1){m_wait();}
}

This is a calling fragment. Follow the stated initialization and buffer requirements before using it in a complete program.

Expected result

In KUROSAKI, both ports show RAW=00, TRIGGER=000 and LIGHT=001. The unsuffixed calls show the same 000/001 as port 2. LIGHT=001 comes from unmodeled input bit 3 reading zero, not from detecting screen brightness. The screen also states LIGHT 1 IS NOT A HIT TEST.

__zapper_trigger1: ZAPPER-DEFAULT — In KUROSAKI, both ports show RAW=00, TRIGGER=000 and LIGHT=001. The unsuffixed calls show the same 000/001 as port 2. LIGHT=001 comes from unmodeled input bit 3 reading zero, not from detecting screen brightness. The screen also states LIGHT 1 IS NOT A HIT TEST.
ZAPPER-DEFAULT — In KUROSAKI, both ports show RAW=00, TRIGGER=000 and LIGHT=001. The unsuffixed calls show the same 000/001 as port 2. LIGHT=001 comes from unmodeled input bit 3 reading zero, not from detecting screen brightness. The screen also states LIGHT 1 IS NOT A HIT TEST.

Complete example program

Build command

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\peripheral_zapper.c -I .\kitaqfc\lib -I .\kitaq-docs\samples -o .\out\peripheral_zapper.nes --mapper=nrom --nes-chr=.\kitaq-docs\samples\font.chr --no-cache --no-disasm
.\kurosaki\kurosaki.exe run .\out\peripheral_zapper.nes --frames 120 --png .\out\peripheral_zapper.png
Implementation and source

kitaqfc/lib/intrinsics.h:243

Declaration notes (original source)
Return port-1 trigger bit 4 normalized to 0 or 1.

kitaqfc/kitaqfc/CodeGenerator.cs:7747

csharp
EmitHelperStart("__zapper_trigger1");
            EmitNormalizeBoolFromMask(0x4016, 0x10, false);
        void EmitNormalizeBoolFromMask(int port, int mask, bool invert)
        {
            string zero = NewGeneratedLabel("bool_zero");
            string done = NewGeneratedLabel("bool_done");
            EmitAsm("LDA", Mem(port));
            EmitAsm("AND", Imm(mask));
            if (invert) EmitAsm("BNE", Rel(zero)); else EmitAsm("BEQ", Rel(zero));
            EmitAsm("LDA", Imm(1));
            EmitAsm("JMP", Abs(done));
            _assembly.Add(Expr.Make(Tag.Label, zero));
            EmitAsm("LDA", Imm(0));
            _assembly.Add(Expr.Make(Tag.Label, done));
            EmitAsm("RTS");
        }
__zapper_trigger2

Read and normalize the trigger line on port 2 ($4017).

Syntax

c
u8 __zapper_trigger2(void);

Return value

1 when $4017 bit 4 is set, otherwise 0.

Conditions and limits

These APIs access $4016/$4017 directly. Do not interleave interrupt-driven gamepad, keyboard or serial operations on the same ports. Controller shift positions can also be affected. The sample disables NMI while capturing input, then enables it when initializing the display.

Reads the instantaneous signal. It does not strobe, wait for a frame, detect an edge, locate an aim point or decide a hit. Separate raw and trigger/light calls each read the register at their own time, so their values can differ while the signal changes.

The result is the trigger line’s logical state. It does not guarantee that the trigger remains fully pulled, and switch behavior depends on the device. For a game firing event, compare with the previous reading to detect the desired transition.

Bit conversion is also unit-tested in an owned test ROM by redirecting register reads to RAM fixture values. The tests cover 00/08/10/18 combinations with unrelated bits set. This unit test is separate from the unmodified sample run and from real peripheral behavior.

These images run the downloadable ROMs unchanged in KUROSAKI. Its current input bus models gamepads but not keyboard, light-gun or serial-adapter signals. The external input bits read as zero, so the captures do not verify those devices. Physical key input, optical sensing and communication remain untested.

NESdev: Zapper

Usage example

c
#include "fc_ports_scene.h"
void main(void){
    __irq_disable();__ppu_ctrl_set(0);port_phase=1;
    port_result[0]=__zapper_raw1();port_result[1]=__zapper_trigger1();port_result[2]=__zapper_light1();
    port_result[3]=__zapper_raw2();port_result[4]=__zapper_trigger2();port_result[5]=__zapper_light2();
    port_result[6]=__zapper_trigger();port_result[7]=__zapper_light();
    port_phase=2;port_result[31]=165;
    m_init();m_text(2,2,"LIGHT-GUN INPUT SNAPSHOT");m_wait();
    m_text(2,5,"PORT   RAW  TRIGGER  LIGHT");m_wait();
    m_put(3,8,'1');port_hex(9,8,port_result[0]);port_number(15,8,port_result[1]);port_number(24,8,port_result[2]);m_wait();
    m_put(3,10,'2');port_hex(9,10,port_result[3]);port_number(15,10,port_result[4]);port_number(24,10,port_result[5]);m_wait();
    m_text(2,13,"DEFAULT = PORT 2");m_wait();
    m_text(2,15,"TRIGGER");port_number(15,15,port_result[6]);m_wait();
    m_text(2,17,"LIGHT");port_number(15,17,port_result[7]);m_wait();
    m_text(2,21,"LIGHT 1 IS NOT A HIT TEST");m_wait();
    while(1){m_wait();}
}

This is a calling fragment. Follow the stated initialization and buffer requirements before using it in a complete program.

Expected result

In KUROSAKI, both ports show RAW=00, TRIGGER=000 and LIGHT=001. The unsuffixed calls show the same 000/001 as port 2. LIGHT=001 comes from unmodeled input bit 3 reading zero, not from detecting screen brightness. The screen also states LIGHT 1 IS NOT A HIT TEST.

__zapper_trigger2: ZAPPER-DEFAULT — In KUROSAKI, both ports show RAW=00, TRIGGER=000 and LIGHT=001. The unsuffixed calls show the same 000/001 as port 2. LIGHT=001 comes from unmodeled input bit 3 reading zero, not from detecting screen brightness. The screen also states LIGHT 1 IS NOT A HIT TEST.
ZAPPER-DEFAULT — In KUROSAKI, both ports show RAW=00, TRIGGER=000 and LIGHT=001. The unsuffixed calls show the same 000/001 as port 2. LIGHT=001 comes from unmodeled input bit 3 reading zero, not from detecting screen brightness. The screen also states LIGHT 1 IS NOT A HIT TEST.

Complete example program

Build command

powershell
New-Item -ItemType Directory -Force .\out | Out-Null
.\kitaqfc\kitaqfc.exe .\kitaq-docs\samples\api-examples\fc\peripheral_zapper.c -I .\kitaqfc\lib -I .\kitaq-docs\samples -o .\out\peripheral_zapper.nes --mapper=nrom --nes-chr=.\kitaq-docs\samples\font.chr --no-cache --no-disasm
.\kurosaki\kurosaki.exe run .\out\peripheral_zapper.nes --frames 120 --png .\out\peripheral_zapper.png
Implementation and source

kitaqfc/lib/intrinsics.h:245

Declaration notes (original source)
Return port-2 trigger bit 4 normalized to 0 or 1.

kitaqfc/kitaqfc/CodeGenerator.cs:7749

csharp
EmitHelperStart("__zapper_trigger2");
            EmitNormalizeBoolFromMask(0x4017, 0x10, false);
        void EmitNormalizeBoolFromMask(int port, int mask, bool invert)
        {
            string zero = NewGeneratedLabel("bool_zero");
            string done = NewGeneratedLabel("bool_done");
            EmitAsm("LDA", Mem(port));
            EmitAsm("AND", Imm(mask));
            if (invert) EmitAsm("BNE", Rel(zero)); else EmitAsm("BEQ", Rel(zero));
            EmitAsm("LDA", Imm(1));
            EmitAsm("JMP", Abs(done));
            _assembly.Add(Expr.Make(Tag.Label, zero));
            EmitAsm("LDA", Imm(0));
            _assembly.Add(Expr.Make(Tag.Label, done));
            EmitAsm("RTS");
        }

Command syntax reference

The following help was captured for the source edition. Consult it for optional arguments, defaults and accepted values. Replace the illustrative input filenames with your own outputs. Captured tool responses retain their original wording.

kitaqfc
help
KITAQFC native Rust compiler

Build a NES ROM:
  kitaqfc SOURCE.c ... -I INCLUDE --mapper=nrom -o GAME.nes
  kitaqfc SOURCE.c ... -I lib --mapper=mmc3 --nes-chr=tiles.chr -O2 -o GAME.nes

Build a Famicom Disk System image:
  kitaqfc SOURCE.c ... --mapper=fds --fds-layout=fds32 -o GAME.fds
  --fds-meta=FILE --fds-meta-out[=FILE] --fds-out[=FILE]
  --fds-auto-overlay / --fds-no-auto-overlay
  --fds-overlay-farcall / --fds-no-overlay-farcall
  --fds-overlay-guard / --fds-no-overlay-guard
  --fds-overlay-trim --fds-overlay-prefix=NAME --fds-overlay-start-id=N
  --fds-overlay-table / --fds-no-overlay-table
  --fds-header / --fds-raw --fds-game-code=CODE
  --fds-license-bypass / --fds-no-license-bypass

Cartridge and optimization:
  --target=nes|fc|famicom|6502|2a03
  --mapper=nrom|uxrom|cnrom|axrom|mmc1|mmc3|mmc5|vrc6|vrc7|fme7|fds|surom512
  --board=auto|generic|surom512 --mirroring=horizontal|vertical|four_screen
  --battery / --no-battery --nes-chr-ram --nes-chr=FILE
  -O0 / -O1 / -O2 --nes-opt=all|off
  --abi=legacy|stack|fastcall --reg-cc-v2 / --no-reg-cc-v2
  --zp-alloc --static-frame --small-inline --loop-lowering
  --library-lto-lite --mapper-aware-placement --kurosaki-profile=FILE
  --nes-local-ram=START:LENGTH --nes-temp-ram=START:LENGTH
  -Zcheck -Zcheck-bounds -Zconst-scalar-in-rom

Diagnostics and reports:
  --strict / --permissive --max-errors=N --diag-json[=FILE|-]
  --profile=dev|release|test --cache / --no-cache --deps-out[=FILE]
  --debug-output[=DIR] --trace[=tokens,ast,ir,asm] --trace-out=DIR
  --no-debug-output --no-trace --fast-build --emit-path-manifest=FILE
  --kurosaki-metadata[=FILE] --nes-opt-report[=FILE]
  --func-size-report --hotspot-report --cross-bank-report --farcall-suggest
  --abi-verify --abi-diff-report --bank-sim --opt-diff --rst-report --repro-check
  Report switches accept =FILE; otherwise reports are placed beside the ROM.

Frontend and typed assembly:
  tokenize SOURCE.c ... -I INCLUDE -o tokens.json
  parse SOURCE.c ... -I INCLUDE -o ast.json
  lower SOURCE.c ... -I INCLUDE -o lowered.json
  emit-ir SOURCE.c ... -I INCLUDE -o assembly.json
  assemble-ir ASSEMBLY.json --mapper=nrom -o ROM.nes

Utilities and development:
  kqhelp, symfind, src2asm, romdiff, template, snippet,
  conventions, fixhint, irsum, attrviz, recipe, test
  SOURCE.c ... --watch [--watch-max-builds=N]
  devserver SOURCE.c ... [--once --poll-ms=N --debounce-ms=N --max-builds=N]
  SOURCE.c ... --repro-pack[=DIR] --auto-minimize
  SOURCE.c ... --minimize[=FILE] --minimize-work=DIR --minimize-out=FILE

Legacy ROM-header and RST flags retain the C# NES backend's behavior.
NES disassembly and managed debugger attachment do not run in that backend.

Developer command syntax

These usages come from Program.DebugTools.cs and Program.VibeTools.cs. ROM comparison, symbol search and templates are subcommands separate from ordinary C compilation.

usage
usage: kitaqfc kqhelp <KQxxxx>
usage
usage: kitaqfc symfind <pattern> [--map=<.map>] [--dbg=<.dbg>] [--regex] [--ignore-case] [--out=<file>|-]
usage
usage: kitaqfc src2asm <source.c:line> [--disasm=<dis.s>] [--funcsizes=<.funcsizes.txt>] [--context=N] [--out=<file>|-]
usage
usage: kitaqfc romdiff <old.nes> <new.nes> [--old-func=<.funcsizes.txt>] [--new-func=<.funcsizes.txt>] [--old-map=<.map>] [--new-map=<.map>] [--out=<file>|-]
usage
usage: kitaqfc template [out.c] [--out=<file.c>] [--overwrite] [--no-build-script] [--no-readme]
usage
usage: kitaqfc conventions [--out=<file>|--stdout]
usage
usage: kitaqfc snippet list | get <id> [--out=<file>|-] | emit <category|all> [--out=<file>|-]
usage
usage: kitaqfc fixhint [diag.json|stderr.log] [--in=<file>] [--out=<file>|-] [--max=N]
usage
usage: kitaqfc irsum [--ir=<syntax_tree_lowered.txt>] [--asm=<assembly_code.txt>] [--top=N] [--out=<file>|-]
usage
usage: kitaqfc devserver [--poll-ms=N] [--debounce-ms=N] [--once] [--max-builds=N] [--deps=<file>] <compile args...>
usage
usage: kitaqfc recipe [--history=<file>] [--last=N] [--out=<file>] [--script=<file>] [--stdout]

Long options recognized by the parser

Spellings were extracted from src/cli.rs, including compatibility and investigative options. Consult help and the corresponding handler to determine whether each option takes a value.

--abi--abi-diff-report--abi-verify--attach--auto-inline-small--auto-minimize--bank-sim--battery--board--cache--cart--cgb--cgb-consistency--chr-rom--container--cross-bank-report--debug-out--debug-output--deps-out--dest--diag-json--disasm--disasm-changed--disasm-changed-out--emit-ai-metadata--farcall-suggest--fast--fast-build--fastcall-v2--fds-approval-bypass--fds-auto-overlay--fds-auto-overlays--fds-builder-manifest--fds-bypass-license--fds-game-code--fds-header--fds-image--fds-layout--fds-license-bypass--fds-manifest--fds-meta--fds-meta-out--fds-metadata--fds-metadata-out--fds-no-approval-bypass--fds-no-auto-overlay--fds-no-auto-overlays--fds-no-header--fds-no-license-bypass--fds-no-overlay-farcall--fds-no-overlay-guard--fds-no-overlay-table--fds-no-residency-guard--fds-out--fds-overlay-farcall--fds-overlay-guard--fds-overlay-id-base--fds-overlay-no-trim--fds-overlay-prefix--fds-overlay-start-id--fds-overlay-table--fds-overlay-trim--fds-raw--fds-residency-guard--func-size-report--help--hotspot-report--include-dir--kurosaki-meta--kurosaki-metadata--kurosaki-out--kurosaki-profile--library-lto-lite--loop-lowering--lto-lite--mapper--mapper-aware-placement--max-errors--minimize-on-fail--mirroring--nes-chr--nes-chr-ram--nes-local-ram--nes-opt--nes-opt-report--nes-temp-ram--no-auto-inline-small--no-battery--no-cache--no-disasm--no-fastcall-v2--no-library-lto-lite--no-loop-lowering--no-lto-lite--no-mapper-aware-placement--no-reg-cc-v2--no-rst--no-small-inline--no-static-frame--no-whole-program-zp--no-zp-alloc--opt-diff--output-format--permissive--profile--ramsize--reg-cc-v2--repro-check--repro-pack--rom-header--rom-title--romsize--rst--rst-disable--rst-enable--rst-exclude--rst-max-calls--rst-max-vectors--rst-off--rst-on--rst-report--rst-safe--rst-speed-safe--rst-unsafe--rst-use-38--sgb--small-inline--static-frame--strict--target--trace--trace-out--verify-cgb-symbols--version--vlist--vlist-out--whole-program-zp--zp-alloc

Appendix: assembly instruction index

This table is extracted from src/machine/opcodes.rs. It describes the compiler internal spellings and operand formats. Entries with branch destinations or memory addresses are syntax examples, not standalone programs. Consult calling conventions and code generation for preserved registers and flag changes.

opcodeInstructionFormatSyntax example
00NOPIMPNOP
F3DIIMPDI
FBEIIMPEI
76HALTIMPHALT
10STOPIMM8STOP #1
C9RETIMPRET
D9RETIIMPRETI
C3JPABSJP 0xC000
E9JP_HLIMPJP_HL
18JRRELJR +target
20JR_NZRELJR_NZ +target
28JR_ZRELJR_Z +target
30JR_NCRELJR_NC +target
38JR_CRELJR_C +target
CDCALLABSCALL 0xC000
C4CALL_NZABSCALL_NZ 0xC000
CCCALL_ZABSCALL_Z 0xC000
D4CALL_NCABSCALL_NC 0xC000
DCCALL_CABSCALL_C 0xC000
C2JP_NZABSJP_NZ 0xC000
CAJP_ZABSJP_Z 0xC000
D2JP_NCABSJP_NC 0xC000
DAJP_CABSJP_C 0xC000
C7RST_00IMPRST_00
CFRST_08IMPRST_08
D7RST_10IMPRST_10
DFRST_18IMPRST_18
E7RST_20IMPRST_20
EFRST_28IMPRST_28
F7RST_30IMPRST_30
FFRST_38IMPRST_38
3ELD_A_IMMIMM8LD_A_IMM #1
06LD_B_IMMIMM8LD_B_IMM #1
0ELD_C_IMMIMM8LD_C_IMM #1
16LD_D_IMMIMM8LD_D_IMM #1
1ELD_E_IMMIMM8LD_E_IMM #1
26LD_H_IMMIMM8LD_H_IMM #1
2ELD_L_IMMIMM8LD_L_IMM #1
7FLD_A_AIMPLD_A_A
78LD_A_BIMPLD_A_B
79LD_A_CIMPLD_A_C
7ALD_A_DIMPLD_A_D
7BLD_A_EIMPLD_A_E
7CLD_A_HIMPLD_A_H
7DLD_A_LIMPLD_A_L
47LD_B_AIMPLD_B_A
40LD_B_BIMPLD_B_B
41LD_B_CIMPLD_B_C
42LD_B_DIMPLD_B_D
43LD_B_EIMPLD_B_E
44LD_B_HIMPLD_B_H
45LD_B_LIMPLD_B_L
4FLD_C_AIMPLD_C_A
48LD_C_BIMPLD_C_B
49LD_C_CIMPLD_C_C
4ALD_C_DIMPLD_C_D
4BLD_C_EIMPLD_C_E
4CLD_C_HIMPLD_C_H
4DLD_C_LIMPLD_C_L
57LD_D_AIMPLD_D_A
50LD_D_BIMPLD_D_B
51LD_D_CIMPLD_D_C
52LD_D_DIMPLD_D_D
53LD_D_EIMPLD_D_E
54LD_D_HIMPLD_D_H
55LD_D_LIMPLD_D_L
5FLD_E_AIMPLD_E_A
58LD_E_BIMPLD_E_B
59LD_E_CIMPLD_E_C
5ALD_E_DIMPLD_E_D
5BLD_E_EIMPLD_E_E
5CLD_E_HIMPLD_E_H
5DLD_E_LIMPLD_E_L
67LD_H_AIMPLD_H_A
60LD_H_BIMPLD_H_B
61LD_H_CIMPLD_H_C
62LD_H_DIMPLD_H_D
63LD_H_EIMPLD_H_E
64LD_H_HIMPLD_H_H
65LD_H_LIMPLD_H_L
6FLD_L_AIMPLD_L_A
68LD_L_BIMPLD_L_B
69LD_L_CIMPLD_L_C
6ALD_L_DIMPLD_L_D
6BLD_L_EIMPLD_L_E
6CLD_L_HIMPLD_L_H
6DLD_L_LIMPLD_L_L
EALD_MEM_AABSLD_MEM_A 0xC000
FALD_A_MEMABSLD_A_MEM 0xC000
E0LDH_MEM_ALDHLDH_MEM_A 0x40
F0LDH_A_MEMLDHLDH_A_MEM 0x40
E2LD_C_MEM_AIMPLD_C_MEM_A
F2LD_A_MEM_CIMPLD_A_MEM_C
77LD_HL_AIMPLD_HL_A
7ELD_A_HLIMPLD_A_HL
70LD_HL_BIMPLD_HL_B
71LD_HL_CIMPLD_HL_C
72LD_HL_DIMPLD_HL_D
73LD_HL_EIMPLD_HL_E
74LD_HL_HIMPLD_HL_H
75LD_HL_LIMPLD_HL_L
36LD_HL_REF_IMMIMM8LD_HL_REF_IMM #1
12LD_DE_AIMPLD_DE_A
1ALD_A_DEIMPLD_A_DE
02LD_BC_AIMPLD_BC_A
0ALD_A_BCIMPLD_A_BC
22LDI_HL_AIMPLDI_HL_A
2ALDI_A_HLIMPLDI_A_HL
32LDD_HL_AIMPLDD_HL_A
3ALDD_A_HLIMPLDD_A_HL
01LD_BC_IMMIMM16LD_BC_IMM #0xC000
11LD_DE_IMMIMM16LD_DE_IMM #0xC000
21LD_HL_IMMIMM16LD_HL_IMM #0xC000
31LD_SP_IMMIMM16LD_SP_IMM #0xC000
F9LD_SP_HLIMPLD_SP_HL
F8LD_HL_SP_IMMRELLD_HL_SP_IMM +target
E8ADD_SP_IMMRELADD_SP_IMM +target
08LD_MEM_SPIMM16LD_MEM_SP #0xC000
C5PUSH_BCIMPPUSH_BC
C1POP_BCIMPPOP_BC
D5PUSH_DEIMPPUSH_DE
D1POP_DEIMPPOP_DE
E5PUSH_HLIMPPUSH_HL
E1POP_HLIMPPOP_HL
F5PUSH_AFIMPPUSH_AF
F1POP_AFIMPPOP_AF
3CINC_AIMPINC_A
04INC_BIMPINC_B
0CINC_CIMPINC_C
14INC_DIMPINC_D
1CINC_EIMPINC_E
24INC_HIMPINC_H
2CINC_LIMPINC_L
34INC_HL_REFIMPINC_HL_REF
3DDEC_AIMPDEC_A
05DEC_BIMPDEC_B
0DDEC_CIMPDEC_C
15DEC_DIMPDEC_D
1DDEC_EIMPDEC_E
25DEC_HIMPDEC_H
2DDEC_LIMPDEC_L
35DEC_HL_REFIMPDEC_HL_REF
87ADD_AIMPADD_A
80ADD_BIMPADD_B
81ADD_CIMPADD_C
82ADD_DIMPADD_D
83ADD_EIMPADD_E
84ADD_HIMPADD_H
85ADD_LIMPADD_L
86ADD_HL_REFIMPADD_HL_REF
C6ADD_A_IMMIMM8ADD_A_IMM #1
8FADC_AIMPADC_A
88ADC_BIMPADC_B
89ADC_CIMPADC_C
8AADC_DIMPADC_D
8BADC_EIMPADC_E
8CADC_HIMPADC_H
8DADC_LIMPADC_L
8EADC_HL_REFIMPADC_HL_REF
CEADC_IMMIMM8ADC_IMM #1
97SUB_AIMPSUB_A
90SUB_BIMPSUB_B
91SUB_CIMPSUB_C
92SUB_DIMPSUB_D
93SUB_EIMPSUB_E
94SUB_HIMPSUB_H
95SUB_LIMPSUB_L
96SUB_HL_REFIMPSUB_HL_REF
D6SUB_IMMIMM8SUB_IMM #1
9FSBC_AIMPSBC_A
98SBC_BIMPSBC_B
99SBC_CIMPSBC_C
9ASBC_DIMPSBC_D
9BSBC_EIMPSBC_E
9CSBC_HIMPSBC_H
9DSBC_LIMPSBC_L
9ESBC_HL_REFIMPSBC_HL_REF
DESBC_IMMIMM8SBC_IMM #1
A7AND_AIMPAND_A
A0AND_BIMPAND_B
A1AND_CIMPAND_C
A2AND_DIMPAND_D
A3AND_EIMPAND_E
A4AND_HIMPAND_H
A5AND_LIMPAND_L
A6AND_HL_REFIMPAND_HL_REF
E6AND_IMMIMM8AND_IMM #1
B7OR_AIMPOR_A
B0OR_BIMPOR_B
B1OR_CIMPOR_C
B2OR_DIMPOR_D
B3OR_EIMPOR_E
B4OR_HIMPOR_H
B5OR_LIMPOR_L
B6OR_HL_REFIMPOR_HL_REF
F6OR_IMMIMM8OR_IMM #1
AFXOR_AIMPXOR_A
A8XOR_BIMPXOR_B
A9XOR_CIMPXOR_C
AAXOR_DIMPXOR_D
ABXOR_EIMPXOR_E
ACXOR_HIMPXOR_H
ADXOR_LIMPXOR_L
AEXOR_HL_REFIMPXOR_HL_REF
EEXOR_IMMIMM8XOR_IMM #1
BFCP_AIMPCP_A
B8CP_BIMPCP_B
B9CP_CIMPCP_C
BACP_DIMPCP_D
BBCP_EIMPCP_E
BCCP_HIMPCP_H
BDCP_LIMPCP_L
BECP_HL_REFIMPCP_HL_REF
FECP_IMMIMM8CP_IMM #1
03INC_BCIMPINC_BC
0BDEC_BCIMPDEC_BC
13INC_DEIMPINC_DE
1BDEC_DEIMPDEC_DE
23INC_HLIMPINC_HL
2BDEC_HLIMPDEC_HL
33INC_SPIMPINC_SP
3BDEC_SPIMPDEC_SP
09ADD_HL_BCIMPADD_HL_BC
19ADD_HL_DEIMPADD_HL_DE
29ADD_HL_HLIMPADD_HL_HL
39ADD_HL_SPIMPADD_HL_SP
07RLCAIMPRLCA
17RLAIMPRLA
0FRRCAIMPRRCA
1FRRAIMPRRA
CBPREFIX_CBIMPPREFIX_CB
2FCPLIMPCPL
3FCCFIMPCCF
37SCFIMPSCF
27DAAIMPDAA