# dmakit

A Linux-faithful model of streaming DMA, plus a software-emulated AMD IOMMU that every device access goes through. You can map memory, send DMAs and raise MSIs. When a device touches something it shouldn't, the emulator intercepts it the way hardware does: it writes the fault into the event-log ring and, if enabled, raises the IOMMU interrupt.

It runs two ways:

- **With a Linux-style driver** that programs the IOMMU and drains the ring, as a kernel with the IOMMU on does.
- **Bare-metal, with no OS driver** (as on a machine booted with `amd_iommu=off`). Your code programs the registers, device table and page tables. Nothing drains a log or moves a pointer unless your code does. DMA writes are posted and silent, so you find failures by comparing data or reading the event log. PCIe PRI page requests fill the PPR log, which you can deliberately overflow. Devices can use ATS, with their own ATC, so the whole mockPageOut → PRI → mockPageIn → retry cycle can be run step by step, with or without a PASID. dmakit 1.0 covers the rest of the IOMMU a bare-metal test program drives:
  - guest (v2) translation through GCR3 tables
  - large and NextLevel=7 pages, skipped levels and the exclusion range
  - dual event/PPR logs with auto-swap, PPR auto response and early warning
  - 128-bit IRTEs, x2APIC, guest virtual APIC and the GA log
  - performance counters
  - dmakit 2.0: host Accessed/Dirty bits (dirty tracking), the interrupt message types (NMI, INIT, ExtInt, SMI) with their DTE pass bits, v2 page tables for the DMA API (`amd_iommu=pgtbl_v2`), and several IOMMUs
  - dmakit 2.1: a VM with a virtual AMD IOMMU, run the way a VMM and Linux iommufd run one (nested translation with the GCR3 root as a GPA)
  - dmakit 2.2: SEV-SNP (the RMP check of every device access, RMP_PAGE_FAULT, the register locks, the completion store range and the DTE rules) and Secure ATS

The library comes in two implementations with the same API:

- JavaScript ES modules (`js/`; each architecture's files in `js/amd/`, `js/arm/` and `js/riscv/`)
- header-only C++20 (`cpp/include/dmakit/`, with the same `amd/`, `arm/` and `riscv/` folders)

Both print identical results. This is enforced by fixed scenarios, 1,580 seeded random scenarios and sanitizer runs (GCC and clang ASan+UBSan; `tests/test-replaykit.sh` builds replaykit with them too).

- **Kernel reference:** Linux 7.3-rc2, commit `80a4e3ad8daba66915a9bdf0fcae5831cc8dbd5f`. This covers `kernel/dma/swiotlb.c`, `kernel/dma/direct.{c,h}`, `drivers/iommu/dma-iommu.c` and the headers they use. Each function names the kernel function it follows.
- **IOMMU reference:** AMD I/O Virtualization Technology (IOMMU) Specification 48882, rev 3.11; Arm SMMUv3 (Arm IHI 0070 H.a: stage 1, ATS and PRI); RISC-V IOMMU Architecture Specification 1.0 (first stage, ATS and PRI). The Arm and RISC-V translation was also checked against QEMU master 9ded45a6 (in the portable-media-stack repository).
- **Version:** 4.3.0 (C++20) · **Licence:** MIT, except the Linux-derived files (GPL-2.0-only; see Licensing)

## Licensing

dmakit is MIT-licensed (`LICENSE`) except `js/swiotlb.mjs`, `js/swiotlb-layout.mjs`, `js/dma-direct.mjs`, `js/iommu-dma.mjs`, `js/iommu-dma-geometry.mjs` and their C++ twins `cpp/include/dmakit/swiotlb.hpp`, `dma_direct.hpp`, `iommu_dma.hpp` and `iommu_dma_geometry.hpp`. Those follow `kernel/dma/swiotlb.c`, `kernel/dma/direct.{c,h}` and `drivers/iommu/dma-iommu.c` closely, so they are derived from the Linux kernel and are under the GNU GPL version 2 only, copyright the kernel authors; their SPDX headers say so, and the full text is in `LICENSES/GPL-2.0`. The single-file builds on the site (`notes/lib/dmakit/single/`) inline those headers and are GPL-2.0-only as a whole.

## Quick start

JavaScript (Node 18.17+ or 20.1+, or a browser; no dependencies):

```js
import { Platform } from "./dmakit/js/index.mjs";

const p = new Platform({ strict: false });           // emulated AMD IOMMU, lazy invalidation
const nic = p.addDevice({ bdf: "03:00.0" });
const buf = p.alloc(4096, { fill: 0x41 });
const dma = p.dma.map(nic, buf, 4096, "to");          // dma_map_single(): page tables + INVALIDATE + COMPLETION_WAIT

nic.dmaRead(dma, 8);                                  // { ok: true, bytes }
nic.dmaWrite(0xdead0000n, [1, 2]);                    // { ok: false, fault } → IO_PAGE_FAULT in the event log
p.driver.log.at(-1);                                  // 'AMD-Vi: Event logged [IO_PAGE_FAULT device=0000:03:00.0 domain=0x0001 address=0xdead0000 flags=0x0000]'

p.onInterrupt(0x41, (irq) => console.log(irq));
nic.msi.configure({ vectors: [0x41], dest: 2 });      // programs an IRTE and invalidates the interrupt table
nic.raiseMsi(0);                                      // remapped through the IRTE → handler runs
```

C++20 (header-only; `-I dmakit/cpp/include`):

```cpp
#include "dmakit/platform.hpp"
using namespace dmakit;

PlatformBase<AmdArch> p;                                // IOMMU on, strict
auto& nic = p.add_device({"03:00.0"});
std::uint64_t buf = p.alloc(4096, 0x41);
std::uint64_t dma = p.map(nic, buf, 4096, DMA_TO_DEVICE);
DmaResult r = nic.dma_write(0xdead0000, {1, 2});        // r.ok == false; p.driver->events.back().name() == "IO_PAGE_FAULT"
```

For new C++ code, prefer `PlatformBase<AmdArch>` and `PlatformDeviceBase<AmdArch>` for the AMD
platform and device, `std::uint64_t` for fixed-width values, and `std::optional<unsigned>` for
optional IDs. The existing `Platform`, `PlatformDevice`, `DeviceOptions`, `u64` and `OptId`
aliases remain available until a separately reviewed source-level migration removes them.

`dmakit.hpp` and `platform.hpp` cover AMD. For Arm SMMUv3 include `dmakit/platform_base.hpp` with `dmakit/arm/arm_arch.hpp` (or `arm_smmu_bare.hpp` for the bare-metal framework); for RISC-V, `dmakit/riscv/riscv_arch.hpp` (or `riscv_iommu_bare.hpp`).

Bare platform, driving the IOMMU yourself (JavaScript; the C++ API is the same in `dmakit/bare.hpp`):

```js
import { Platform, IommuFramework, describeRecord } from "./dmakit/js/index.mjs";

const p = new Platform({ driver: false });            // amd_iommu=off: nothing programs the IOMMU
const fw = new IommuFramework(p);
fw.setup({ evtLen: 8, pprLen: 8 });                   // 4 KiB rings; base writes clear head/tail
fw.control({ iommu: 1, eventLog: 1, cmdBuf: 1, ppr: 1, pprLog: 1 });   // no interrupts
const dev = p.addDevice({ bdf: "03:00.0" });
fw.writeDte(dev, { mode: 4, domain: 1, ppr: true });
fw.map(dev, 0x1000n, p.alloc(4096), 4096, "r");

dev.dmaWrite(0x1000n, [1]);                           // { posted: true }: the device is told nothing
fw.event.pending().map(describeRecord);               // ['IO_PAGE_FAULT device=0000:03:00.0 domain=0x0001 address=0x1000 flags=0x0070']

while (!fw.status().pprOverflow) dev.pri.request({ address: 0x100000n, prg: 1 });
fw.ppr.count;                                         // 255: full is all slots but one
fw.ppr.consume(16); fw.restart(fw.ppr);               // §2.6.2
fw.completePpr(dev, { prg: 1 });                      // dev.pri.responses gets the PRG response
```

ATS and the page-in cycle (the DTE needs `i: true`); `examples/pasid_pagein.mjs` runs it with a PASID:

```js
fw.writeDte(dev, { mode: 4, domain: 1, ppr: true, i: true });
fw.mockPageIn(dev, 0x4000n, frameA);
dev.ats.translate(0x4000n);                           // { ok: true, r: true, w: true, phys: frameA }; cached in the ATC
fw.mockPageOut(dev, 0x4000n);                         // PTE cleared, INVALIDATE_IOMMU_PAGES, INVALIDATE_IOTLB_PAGES, COMPLETION_WAIT
dev.ats.dmaWrite(0x4000n, [1]);                       // { ok: false, atcMiss: true }: nothing to send
dev.ats.translate(0x4000n);                           // R=0 W=0, and no event is logged (§2.5.3)
dev.pri.request({ address: 0x4000n, write: true, prg: 5 });
const [req] = fw.ppr.consume();
fw.mockPageIn(dev, req.address, frameB);
fw.completePpr(dev, { prg: req.prg });
dev.ats.translate(0x4000n);                           // R=1 W=1 phys frameB
dev.ats.dmaWrite(0x4000n, [2]);                       // pretranslated; lands in frameB
```

Sixteen C++ examples ship with the library:

- `cpp/examples/iommu_fault.cpp`: mapped DMA, a permission fault, an unmapped DMA, MSI remapping, and use-after-unmap under lazy invalidation.
- `cpp/examples/swiotlb_bounce.cpp`: SWIOTLB slot by slot.
- `cpp/examples/ats_pri_pagein.cpp`: the mockPageOut / mockPageIn cycle. It runs page out, ATS, PRI, page in, COMPLETE_PPR_REQUEST and the retry. It then shows two mistakes: a page out without INVALIDATE_IOTLB_PAGES, and ATS with DTE[I]=0.
- `cpp/examples/ppr_overflow.cpp`: a test program on a bare platform. A DMA write the page tables forbid is found by data compare and the event log. Then PRI page requests fill the PPR log until PprOverflow, the test checks the buffer, and it recovers.
- `cpp/examples/pasid_pagein.cpp`: the same page-in cycle with a PASID. The GVA goes through the GCR3 table and guest tables, nested through the host tables, and the example shows the A and D bits. It uses mockGuestPageOut with GN=1 invalidations, PRI with a PASID and COMPLETE_PPR_REQUEST with GN=1. Then two faults: a PASID with no GCR3 entry, and a nested walk that fails.
- `cpp/examples/ppr_autoswap.cpp`: PPR overflow protection. Dual PPR logs auto-swap, with the early warning first, until both are full. Then PPR auto response (L=1 answered, L=0 dropped) and always-on auto response.
- `cpp/examples/vapic_counters.cpp`: a 128-bit IRTE to an x2APIC destination, and a guest virtual APIC. A running vCPU gets a doorbell; a stopped one gets a GA log entry and interrupt, then the hypervisor reschedules it. Two performance counters, one of which wraps and logs EVENT_COUNTER_ZERO.
- `cpp/examples/dirty_tracking.cpp`: host dirty tracking as Linux iommufd drives it for live migration. DTE[HAD]=11b, the A and D bits the device's reads and writes set, then reading and clearing D.
- `cpp/examples/viommu_hw.cpp`: AMD's hardware-assisted vIOMMU. The IOMMU runs each guest's own command buffer and event log from a register window of its own; an unmapped ID halts one guest's buffer, and the host injects an event with INSERT_GUEST_EVENT.
- `cpp/examples/viommu_hw_edges.cpp`: the same machine's edges. The vendor capability places and locks the windows, a guest's logs overflow and restart, guest interrupts go through the IOMMU's own DTE and two-level table (and what each mistake in them does), memory stops answering, and the host resets a guest.
- `cpp/examples/viommu_hw_callbacks.cpp`: a callback that resets a guest while the IOMMU runs its command: the host's interrupt handler queues RESET_VMMIO from the guest's completion interrupt (and sets the guest up again under the same GuestID) or from the host event of a fault on its buffer, and the IOMMU stops cleanly.
- `cpp/examples/callbacks_reentry.cpp`: callbacks that change the world while the IOMMU is using it. An interrupt handler flushes the IOTLB entry being translated, a fault handler adds a handler and drains the log again, and a second device with the same BDF is refused. It is the regression example for the "pass the key, not the reference" rule (see Code style).
- `cpp/examples/table_errors.cpp`: memory that does not answer the IOMMU's own reads. A device table entry or a page table entry that is not there aborts the request and logs DEV_TAB_HARDWARE_ERROR or PAGE_TAB_HARDWARE_ERROR (with the request's DomainID, RW and TR), a DeviceID beyond the device table (with the attempted address and the PASID) and a guest walk that meets such a table are logged, a reserved sATS bit says RZ, and the DTE's guest fields and two event records are written and read back.
- `cpp/examples/vmm/viommu_nested.cpp`: a VM with a virtual AMD IOMMU. The guest programs its own IOMMU, the VMM turns its entries into nested host entries, and an assigned NIC's DMA goes through both stages.
- `cpp/examples/deferred_commands.cpp`: deferred command execution. An invalidation and a COMPLETION_WAIT are queued while a stale IOTLB entry still takes a DMA, then stepped one at a time: queued, applied, observed.
- `cpp/examples/snp_secure_ats.cpp`: SEV-SNP and Secure ATS. The RMP check before and after SNP_INIT, sATS returning GPAs, and a completion store outside its range.

Each has a JavaScript twin in `examples/` (`node examples/iommu_fault.mjs`; `viommu_nested` is in `examples/vmm/`) that prints the same lines. A twin exports `run(print)`, so a web page can run it and show the output. (`examples/viommu_hw_machine.mjs` is the one file there without a C++ twin: the helpers `viommu_hw`, `viommu_hw_edges` and `viommu_hw_callbacks` share, which each C++ example carries its own copy of.)

`node tools/flatten.mjs <example.cpp>` inlines the headers into one file you can paste into Compiler Explorer. The rules live in `js/flatten.mjs` (`flattenSource(entry, files)`), which has no Node dependencies, so a page can flatten files a reader has edited.

## Code style

The code is read by people new to the Linux DMA API and the AMD IOMMU as well as by experienced
engineers, so it aims for the simplest code both can follow. These rules apply to the C++ and the
JavaScript alike unless a line says otherwise:

The reference guides are [Folly's coding guidelines](https://github.com/facebook/folly/blob/main/folly/CODING_GUIDELINES.md),
[Chromium's C++ style guide](https://chromium.googlesource.com/chromium/src/+/main/styleguide/c++/c++.md),
[allowed C++ features](https://chromium.googlesource.com/chromium/src/+/main/styleguide/c++/c++-features.md),
its [C++ checks](https://chromium.googlesource.com/chromium/src/+/main/styleguide/c++/checks.md),
and the [documentation guidelines](https://chromium.googlesource.com/chromium/src/+/main/docs/documentation_guidelines.md).
Use them as review references, not as claims of wholesale compliance: Folly emphasizes small,
reusable components, tests and portability; Chromium follows Google's C++ rules and disallows
exceptions. Dmakit retains C++20, `snake_case`, 100 columns, and its established exception,
`noexcept` and Wasm boundaries. Folly's `throw_exception` convention is not a substitute for
dmakit's supported throw/catch behavior; do not claim exception semantics are preserved when
exceptions are disabled. Dmakit has no Folly dependency.

- **File size and types.** A file has at most 600 lines (`scripts/max-lines.mjs`); a bigger one is split along a seam into a family
  (`amd-iommu-translate-{host,guest,fault,ats,special}`, `platform-script-*`, ...) in both languages, with the old file re-exporting it. The exception: the C++ headers grew by their Doxygen comments, so a few are split further than their JavaScript twin (`riscv_iommu_{detail,format_tables,queues,bare_types}`, `swiotlb_slots`, `vmm/viommu_detail`, each `#include`d by the file it continues). The one exemption is `cpp/examples/viommu_hw_edges.cpp` (the code lab and Compiler Explorer show an example whole; `scripts/max-lines.mjs`).
  The JavaScript is checked by `tsc` strict (`scripts/lint/tsc-ratchet.mjs`) with JSDoc types and no baseline entry.
- **C++20**, following the [C++ Core Guidelines](https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines).
  It builds with the warnings in `compile_flags.txt` (`-Wall -Wextra -Wpedantic -Werror` and more) on GCC and Clang. CMake, the tests and clang-tidy all read that one file; clang-tidy adds Clang's `-Weverything` (see `.clang-tidy`).
- **Format gate in the build:** CMake runs clang-format first, so unformatted source does not build.
  Keep C++ and its JavaScript twin consistently formatted with the checked-in `.clang-format`;
  formatting enforces layout, not conceptual decomposition or readability.
- **Checks** (`.clang-tidy`): the Core Guidelines, bugprone, CERT, clang analyzer, concurrency,
  misc, modernize, performance, portability and readability families, every warning an error.
  Deliberately disabled checks each have their reason in the file. There is no `NOLINT`, `NOLINTBEGIN`
  or other inline suppression anywhere in the C++ (`git grep NOLINT -- '*.cpp' '*.hpp'` is empty): fix the code instead
  (name the magic number, restructure the return so it is eligible for NRVO, add `const`). Ask before
  disabling a check in `.clang-tidy`.
- **Toolchain (pinned):** clang-format and clang-tidy are LLVM 23 (`tests/lint.test.mjs` fails on
  any other major version; the CI replica was run with 23.1.3), and `doxygen` must pass with the
  `Doxyfile` (a documented function documents every parameter, and `@param`/`@throws` match the
  code). The Doxyfile does not demand a comment on every declaration.
- **Production-grade comments.** Treat the C++ as a production codebase: every class, function,
  constant group and non-obvious step has a Doxygen comment (what, why, parameters, exceptions),
  checked by the `Doxyfile` for the parts it can see (the rule that a declaration has a comment is enforced by review only). Reviewers check that the comment says
  something the code does not.
- **Migration to `std::uint64_t` and no aliases (in progress).** New and changed C++ spells
  `std::uint64_t` (and `std::uint8_t` ...), never `u64`/`u32`/`u8`, and adds no `typedef`; a `using`
  alias needs a written reason and the smallest scope. The existing `u64` aliases and `using`
  declarations are removed in a dedicated change, because they are mirrored across the whole
  library and the replay kit.
- **Function size and statements.** Functions may be long (the `readability-function-size` ceiling is
  125 lines in `.clang-tidy`, for the library, the examples and the tests alike;
  `readability-function-cognitive-complexity` stays at its default; a limit is lowered when the
  functions above it are split, never raised). What must stay simple is each statement: no chained
  operations, at most 3 lines per statement, variable initialisation or `return`, every intermediate
  result a named `const`, and a comment saying what each step does and why. Do not split a function
  merely to make it short unless the split reads better.
- **`consteval`, then `constexpr`, then `const`; no in-place mutation.** A function that must only run at
  compile time (a fixture builder, a validator used in `static_assert`) is `consteval`; a pure function
  that can also run at run time is `constexpr` with a `static_assert` known-answer check beside it
  (a named `constexpr` local per value, because the magic-number check applies there too); everything else
  is `const`. Compute a new named `const` instead of updating a local in place; keep in-place
  mutation for loop accumulators and containers being filled.
- **Layout** (`.clang-format`, both languages): 4-space indentation, never tabs, 100 columns,
  Stroustrup braces (NL.17), braces on every block and one statement per line. `npm run format`
  applies it. clang-format adds missing braces in C++ only, so in JavaScript they are a review
  rule.
- **Names**: descriptive whole words; nothing under three letters except the loop counters `i`,
  `j` and `k`. C++: `snake_case` for functions and variables, `CamelCase` for types (NL.10).
  JavaScript: `camelCase`, with classes in `CamelCase`. The two languages use the same name for
  the same thing (`dma_map_single` and `dmaMapSingle`). Names copied from the Linux kernel or an
  IOMMU specification (AMD, Arm SMMUv3, RISC-V: `PAGE_SIZE`, `DMA_TO_DEVICE`,
  `swiotlb_tbl_map_single`, `IO_PAGE_FAULT`, `CMD_ATC_INV`, `ATS.INVAL`) keep their spelling in
  both languages, so you can search for them there.
- **C++ aliases and types:** prefer a concrete, semantically meaningful type over an alias. Do not
  add aliases merely as shorthand or abbreviation; write standard types such as `std::uint64_t`,
  not `u64`. Never use `typedef`; use `using` only when required by an established interface or
  associated-type contract, or when spelling the full type would make a declaration impractical;
  explain that concrete need and keep the alias at the narrowest practical scope. Preserve
  architecture-associated type contracts; if a contract change is required, document the
  source-level migration in the changelog. The configured `readability-identifier-naming` check
  enforces selected case rules and broadly exempts uppercase global constants and enumerators.
  Reviewers must judge descriptive names, alias necessity and scope, and whether a retained name
  actually comes from a specification or kernel contract. `modernize-use-using` checks declaration
  syntax; it cannot determine whether an alias is needed.
- **Readable control flow:** keep one meaningful operation in each assignment or return. Give
  intermediate results names that explain their role, and extract a small focused helper when it
  makes a non-obvious step easier to follow. Preserve short-circuit ordering when a later
  expression relies on an earlier safety check. Avoid compressing multi-step logic into long
  boolean return chains.
- **Numbers**: no magic numbers. Each one is a named constant citing the specification section or
  kernel source it comes from (`amd::dte_field::mode`, `amd::command_field::opcode`). Reuse the
  constant that exists (`amd_iommu_format.hpp` and `amd-iommu-format.mjs`, with the files they
  include, hold the specification's fields) before adding one.
- **Compile-time and const values.** Use `consteval` only for operations that must be compile-time-only,
  such as builders, validators and fixtures. Use `constexpr` wherever possible for shared pure functions
  that remain callable at runtime, value-record operations that support compile-time evaluation, and
  immutable values that can be initialized at compile time; use `const` otherwise. A value is mutable
  only when it changes. Keep runtime I/O, parsing and interactive state-machine entry points callable at
  runtime; do not force runtime callers through `consteval`. Add `static_assert` known-answer, normalized round-trip and boundary checks for
  intended compile-time evaluation; these checks do not replace meaningful runtime validation and
  specific errors at checked API boundaries. The tracked [RISC-V limits test](tests/riscv_iommu_limits.cpp)
  shows compile-time `static_assert` checks; [numeric model tests](tests/numeric_model.cpp)
  cover accepted and rejected runtime granules. When exception handling conflicts with a
  supported
  `noexcept` or Wasm boundary, explain the limitation and use the established checked boundary; do not
  change compiler flags to make throws work. Preserve `noexcept`, Wasm flags, C++/JavaScript parity, raw
  masking and invalid teaching scenarios. Reuse existing codecs; do not duplicate algorithms or templates
  (PonyTail).
- **Comments**: a plain-English introduction at the top of each file. Use Doxygen comments where
  they explain a public API's purpose, each parameter, return value, failure behavior, preconditions,
  lifetime or ownership as applicable, and explain tricky internal logic where the reason is not
  clear from the code. Do not repeat a declaration's spelling or add a comment to every symbol. In
  JavaScript, non-trivial functions use JSDoc with `@param` meanings, `@returns` and
  `@throws`. Inside functions, explain why, not what. Explain an acronym the first time a file uses
  it.
- **Tests explain each result:** in compile-time fixtures for AMD, Arm and RISC-V, add separately
  diagnosed `static_assert`s for constexpr encoding, round-trip, legality and boundary results.
  Add runtime checks where behavior is runtime-only; keep them active under `NDEBUG` (`assert` alone
  is disabled there). Keep
  assertions focused so a failure names the result that broke, not a long boolean chain. For a
  little-endian oversize-read test, explain why `sizeof(std::uint64_t) + 1` is the smallest invalid
  size and why `std::numeric_limits<std::size_t>::max()` covers extreme arithmetic; check the exact
  exception type and message when they are part of the API contract. Check that access counters
  remain unchanged to prove rejection occurs before memory access. Test rejected reads and writes
  separately, including missing and malformed inputs where those are part of the API contract.
  Intentional catches state the
  expected exception type, message and state; do not use empty catches or turn swallowed exceptions
  into boolean success. Unexpected exceptions must remain visible.
  Doxygen coverage must reject undocumented public entities, missing parameter documentation and
  malformed tags. Start with meaningful public-header contracts and expand checked inputs as each
  header receives equivalent documentation. State the actual checked scope and prove that missing
  and malformed documentation fails; do not claim full exported-API coverage from a narrow check.
- **Pass the key, not the reference.** Never hold or pass a reference, pointer or iterator into a
  container across a call that can log an event, raise an interrupt or call a user hook: the
  callback may erase the element. Pass its key (a `GuestKey`: GuestID and setup serial) and look it
  up again at each use with `find_guest`; a helper that needs several fields copies them into
  locals first. `run_guest_commands` and the other `amd_viommu*.hpp` functions work this way (a reference is fine when nothing uses it after a call that can run a callback),
  because a handler can make the host reset a guest (`RESET_VMMIO All`); `viommu_hw_callbacks` runs
  that under the sanitizers the example test builds with. JavaScript objects cannot dangle, so the
  twin keeps the object and asks `isLive` before each use, which is the same serial check.
  The patterns that dangled, each now fixed and run by `callbacks_reentry` or
  `viommu_hw_callbacks`: an iterator or pointer into a map or vector (the IOTLB, the DTE cache, the
  MSI table, a handler list) kept across `count()`, which can log an event and deliver its
  interrupt; a handler run from inside the vector it lives in (`deliver`, `drain_events`); a
  `const&` to a table entry used after `raise_msi` or a hook; and a function that returns a
  reference to a temporary. Copy the value or snapshot the container first, and make loops that run
  user code non-re-entrant with a flag.
- **Standard library first**: `std::format`, `std::span` and `std::array`, and no `printf` or C
  arrays in C++; no npm dependencies in JavaScript.
- **Modern C++ (C++ Core Guidelines, as the Google, LLVM and Chromium codebases use them)**: `enum class`,
  never a plain `enum`; ownership in `std::unique_ptr` (`std::shared_ptr` only for real shared
  ownership), never a raw `new`/`delete` or `malloc`; `std::span`/`std::string_view` for borrowed
  ranges; no C casts or macros for constants.
- **Errors**: where a function cannot do its job, throw (Core Guidelines E.2); only a purpose-built type
  derived from `std::exception` (E.14), thrown by value and caught by reference (E.15), never a
  built-in or a string. `noexcept` where nothing can throw. JavaScript twins throw an `Error`
  subclass with a `name` (Google JS 5.8.2). Choices where the big guides disagree: enumerators are
  `snake_case` (Core Enum.5, not Google's `kName`); 100 columns (not 80); named exports only (Google
  JS 3.4.2.1, enforced by eslint); `null` for "absent" (what the code already does; `undefined` only where the language
  produces it), never both on purpose.
  The clang-tidy `cppcoreguidelines-*`/`modernize-*` checks and eslint (`no-var`, `prefer-const`,
  `eqeqeq`, `no-throw-literal`, `curly`) enforce what they can; the rest is for the reviewer.

`npm run lint` runs the clang-format and clang-tidy checks; missing clang tools are reported as
skipped, so install them for a complete lint run. `npm test` includes those lint tests with every
other test. The checks select `clang-format`, `clang-tidy` and `g++` from `PATH`, or the
`CLANG_FORMAT`, `CLANG_TIDY` and `CXX` overrides. Tooling updates must use the same verified LLVM
release for clang-format and clang-tidy across npm, CMake and environment-based checks; LLVM
23.1.3 is the verified current release for this update. Verify Doxygen's version independently
before installing it; 1.18.0 is the verified current release. The matching-version selection and
strict Doxygen gate are implementation work accompanying these guidelines; check their actual
validation results before treating them as enforced. An editor with clangd (VS Code, CLion,
Neovim, ...) reads `compile_flags.txt`, `.clang-tidy` and `.clang-format`, so it uses the same
configuration as the checks.

## Using it in another project

- **Copy the folder,** or add it as a git submodule. It has no dependencies.
- **JavaScript:** `import … from "<path>/dmakit/js/index.mjs"`, or add it as a local npm dependency (`"dmakit": "file:../dmakit"`).
- **CMake:**
  ```cmake
  add_subdirectory(dmakit)
  target_link_libraries(app PRIVATE dmakit::dmakit)
  ```
  Or `cmake --install` it and use `find_package`.
- **Plain compiler:** `g++ -std=c++20 -I dmakit/cpp/include app.cpp`

## The layers

| Layer | JavaScript | C++ | What it is |
|---|---|---|---|
| Memory | `SparseMemory` | `SparseMemory` | 4 KiB pages. Memory reads as zero until written, so a 64 MiB pool costs nothing until it's used. |
| Device | `createDevice()` | `Device` | The `struct device` fields the DMA API reads: DMA mask, bus limit, coherent, untrusted, `min_align_mask`, segment boundary. |
| SWIOTLB | `swiotlbTblMapSingle`, `…UnmapSingle`, `…SyncSingleForCpu/Device`, `swiotlbMap`, `swiotlbMaxMappingSize` | the same names in snake_case | The kernel's slot allocator and bounce copies (C++ continues in `swiotlb_slots.hpp`, which has no JavaScript file). |
| dma-direct | `dmaDirectMapPhys`, … | `dma_direct_map_phys`, … | No IOMMU: bounce if forced, not DMA-capable, or small kmalloc. |
| dma-iommu | `iommuDmaMapPhys`, …, `IovaDomain`, `IommuDomain` | the same | IOVA allocation, `dma_info_to_prot`, untrusted bounce with zeroed padding, strict or lazy flush queue. |
| DMA API | `dmaMapSingle`, `dmaUnmapSingle`, `dmaSyncSingleForCpu/Device` | `dma_map_single`, … | Dispatches to dma-direct or dma-iommu, like `kernel/dma/mapping.c`. |
| AMD IOMMU | `AmdIommu` | `amd::AmdIommu` | The emulated hardware: MMIO, device table, host and guest page tables, IRTEs, caches, command buffer, event/PPR/GA logs, counters. One file per concern, the same names in both languages: `amd-iommu` (state and the entry points of its parts: `-fields`, `-mmio`, `-ring-logs`, `-ppr`, `-device-table` and `-snp`), `-format` (register map and record codecs, split over `-registers`, `-codes`, `-tables` and `-logs`), `-commands`, `-translate` (DMA requests; its parts `-translate-host`, `-translate-guest`, `-translate-fault`, `-translate-ats` and `-translate-special` hold the page walks, IOTLB, aborts, ATS and Table 3's special address ranges), `-interrupts` and `-counters`. Bit and range helpers shared by every architecture are in `bits.mjs` / `bits.hpp`. |
| Arm SMMUv3, RISC-V IOMMU | `ArmSmmu`, `RiscvIommu` (built by `armArch`, `riscvArch`); bare-metal `SmmuFramework`, `RiscvFramework` | `arm::ArmSmmu`, `riscv::RiscvIommu`; `arm::SmmuFramework`, `riscv::RiscvFramework` | Slices: registers, tables, command and event or fault queues, stage 1 walks, and ATS and PRI (a PRI or page request queue, ATC invalidation, PRG responses). Files `arm-smmu-{format,walk,queue,ats,bare}`, `arm-smmu`, `riscv-iommu-{format,walk,ats,bare}`, `riscv-iommu` (`arm_smmu_*.hpp`, `riscv_iommu_*.hpp`). The C++ also has the continuation headers `riscv_iommu_{detail,format_tables,queues,bare_types}.hpp`, which have no JavaScript file (see "File size and types"). Used with `driver: false`. |
| Platform | `PlatformBase` (any architecture), `Platform` (AMD), `PlatformDevice`, `AmdIommuDriver`, `AmdDomain` | `PlatformBase<Arch>`, `PlatformBase<AmdArch>`, `PlatformDeviceBase<Arch>`, `PlatformDeviceBase<AmdArch>`, `AmdIommuDriver`, `AmdDomain` | A small machine: allocator, interrupt controller, PCI functions with MSI, one or more IOMMUs, and a Linux-style driver that programs them. `PlatformDevice` has its own JavaScript file, `platform-device.mjs`; its C++ template is in `platform_device.hpp`. In C++, `platform.hpp` includes its parts: `machine.hpp` (PCI addresses, allocator, interrupt controller), `platform_device.hpp` and `amd/amd_iommu_driver.hpp` (the driver and its domains). |

### SWIOTLB behaviour

- **Pool layout:** 2 KiB slots (`IO_TLB_SHIFT` 11) in 128-slot segments. A single bounce is at most 256 KiB, minus `min_align_mask` rounding.
- **Free list:** `list` records how many free slots run to the end of the segment. Search is next-fit from `area->index`, one area per CPU (`cpu & (nareas - 1)`), falling through to the other areas when one is full.
- **Alignment:**
  - `min_align_mask` keeps the buffer's offset.
  - `alloc_align_mask` is the IOVA granule for iommu-dma.
  - Streaming maps of at least `PAGE_SIZE` with no other constraint are page aligned.
- **Segment boundaries:** mappings respect the segment boundary (`dma_get_seg_boundary`).
- **Pad slots:** an offset wider than one slot is kept by skipping whole slots.
- **Copies:**
  - The bounce is always filled at map time, for every direction. This keeps partial DMA_FROM_DEVICE writes transparent.
  - Syncs copy only in the direction that needs it.
  - Unmap copies back unless `DMA_ATTR_SKIP_CPU_SYNC` is set.
  - An oversized sync is clamped, with the kernel's "Buffer overflow detected" warning.
- **Release:** freed slots merge with free neighbours, and `used` and `hiwater` are tracked.
- **Not modelled:** `CONFIG_SWIOTLB_DYNAMIC` transient pools, restricted-DMA pools, memory encryption and highmem.
- **Small pools:** dmakit accepts pools smaller than one 128-slot segment. In those, a slot past the end of the pool counts as allocated. The kernel never meets this case.

### iommu-dma behaviour

- **Direction and permission:** the DMA direction becomes the IOMMU permission, following `dma_info_to_prot()`.
- **Bouncing:** only untrusted devices, or `dma_kmalloc_needs_bounce`, bounce unaligned buffers. The bounce is aligned to the IOVA granule and its padding is zeroed.
- **IOVA allocation:**
  - IOVAs are tried below 4 GiB first (`pci_32bit_workaround`), then across the full mask.
  - The allocator is a top-down, size-aligned highest fit. The kernel's per-CPU rcaches and cached-node next-fit are not modelled.
- **Unmap modes:**
  - **Strict** invalidates before freeing the IOVA.
  - **Lazy** removes the PTEs but keeps IOTLB entries and the IOVA until the flush queue runs. This is the use-after-unmap window.
  - Linux gives untrusted devices strict domains, and you should too.

### The emulated AMD IOMMU

Everything below follows AMD 48882 rev 3.11. Section and table numbers refer to it.

- **Registers:**
  - Bases with Len: DevTabBase, CmdBufBase, the event log A/B and PPR log A/B bases, and the GA log base with its tail-shadow address.
  - Exclusion base/limit (0x20/0x28), Control and Status.
  - Head/Tail for every ring.
  - PPR early-warning registers (0x2088/0x2090), XT interrupt control (0x170/0x178/0x180) and the performance-counter block (0x4000, 0x40000+).
  - The Extended Feature Register is read-only and reports what the model implements (the default configuration; options such as `sats: false` remove bits): PreFSup, PPRSup, XTSup, NXSup, GTSup, IASup, GASup, PCSup, HATS=6 levels, GATS=5 levels, GLXSup=3 levels, GAMSup, dual logs with auto-swap, PASmax=20 bits, USSup, PprOvrflwEarlySup, PPRAutoRspSup, GIoSup, HASup, EPHSup, HDSup, SATSSup and SNPSup.
  - Extended Feature Register 2 (0x01A0) reports NumIntRemapSup=01b: 2048 interrupts per function in 2K mode, GCR3TRPModeSup, SnpPageMode0Sup when the platform is built with `snpMode0`, and HTRangeIgnore with `htRangeIgnore`.
  - Writing a base register clears that ring's head and tail.
  - A log starts on its enable 0→1 edge, which also clears its overflow bit.
- **Device table:** the full 256-bit DTE:
  - V, TV, HAD, Mode, root, IR, IW, PPR, GPRP, GIoV, GV, GLX, GCR3 root, DomainID, I, SE, SA, IoCtl, EX, SATS
  - IV, IntTabLen, IG, IntRoot, GuestPagingMode, InitPass, EIntPass, NMIPass, IntCtl, Lint0Pass and Lint1Pass.
  - V=0 passes DMA through untranslated (not once SNP-enabled, below). TV=0 is an IO_PAGE_FAULT. Mode 0 is identity.
  - IoCtl=11b, HAD=10b, IntCtl=11b and IntTabLen=11xxb log ILLEGAL_DEV_TABLE_ENTRY.
- **Host (v1) page tables (§2.2.3):** Modes 1 to 6, walked from emulated memory, with IR and IW ANDed across the DTE and every level.
  - Leaves at any level: NextLevel=0, or NextLevel=7 with the page size encoded in the address (Table 14).
  - Skipped levels: their index bits must be zero, and they count as 1s for permissions.
  - These are IO_PAGE_FAULTs with PR/RZ as Tables 44 and 57 give:
    - reserved bits (PTE 58:52, PDE 60:52)
    - NextLevel >= level
    - misaligned leaves
    - an address above the Mode's width
  - PTE[U] makes an ATS answer ask for untranslated requests. PTE[FC] is kept.
  - **Accessed and Dirty (§2.2.3.1-2):** with DTE[HAD]=01b the walk sets A in every entry it uses; with 11b it also sets D in the leaf for a write the tables allow. A cached translation without D that serves a write is walked again (as §2.2.7.4 does for guest tables), so a cleared D comes back after an invalidation. An ATS Translation Request sets D unless NW=1 (Table 32); PREFETCH_IOMMU_PAGES sets neither. Nested walks for guest translation set them too; the data page gets D only for a write the guest tables also allow.
- **Guest translation (§2.2.6, §2.2.7), with GTEn and DTE[GV]:**
  - A request with a PASID walks the GCR3 table, then the guest AMD64 tables, each read nested through the host tables.
    - The GCR3 table has 1 to 3 levels (GLX), indexed by PASID bits.
    - The guest tables have 4 or 5 levels, with 4 KiB, 2 MiB or 1 GiB pages.
  - The walk checks R/W, U/S (PrivAbrtEn) and NX. It sets Accessed on every entry used and Dirty on the leaf for an allowed write.
  - A cached guest translation that denies access is rewalked (§2.2.7.3). A demotion needs an invalidation, as the spec says.
  - GIoV sends requests without a PASID through PASID 0's tables.
  - A PASID request without guest translation is an INVALID_DEVICE_REQUEST (Type 100b).
  - Guest IO_PAGE_FAULT events carry GN=1, the PASID, US and NX (Figure 56); a table-read hardware error uses its own format (Tables 59 and 60), without US and NX.
- **Exclusion range:** DMA in [base, limit | 0xfff] skips translation for DTE[EX] devices, or for all devices with Allow. It does not apply to PASID requests. ATS inside it answers R=W=1 with the address itself.
- **Caches:**
  - DTE cache, stale until INVALIDATE_DEVTAB_ENTRY.
  - IOTLB: host entries keyed by DomainID and page, guest entries by DomainID, PASID and page. It is stale until INVALIDATE_IOMMU_PAGES:
    - S bit and range decoding
    - GN=1 removes one PASID's entries
    - GN=0 removes the domain's host entries and every guest entry of the domain
  - IRTE cache, stale until INVALIDATE_INTERRUPT_TABLE. IsRun, Destination, GALogIntr and GATag are re-read on every guest interrupt, as §2.2.5.2 requires.
  - INVALIDATE_IOMMU_ALL clears all of them.
- **Command buffer:**
  - COMPLETION_WAIT (store and interrupt) and all five invalidations.
  - PREFETCH_IOMMU_PAGES: advisory; PFCount, and Inval to replace entries. A page-table read memory does not answer is a PAGE_TAB_HARDWARE_ERROR and stops the buffer (§2.4.6.1).
  - COMPLETE_PPR_REQUEST, with GN and the PASID.
  - Any other opcode logs ILLEGAL_COMMAND_ERROR and halts the buffer with Head on the bad command.
- **Logs:** real 16-byte records. A ring is full when all slots but one are used.
  - **Event log:**
    - Records: IO_PAGE_FAULT (GN, NX, US, I, PR, RW, PE, RZ, TR), ILLEGAL_DEV_TABLE_ENTRY, ILLEGAL_COMMAND_ERROR, IOTLB_INV_TIMEOUT, INVALID_DEVICE_REQUEST, INVALID_PPR_REQUEST and EVENT_COUNTER_ZERO.
    - DTE[SA] suppresses memory faults.
    - DTE[SE] logs only a device's first IO_PAGE_FAULT until its DTE is invalidated.
    - DTE[IG] suppresses interrupt faults.
  - **Dual buffering (§2.5.17, §2.6.3):** event log A/B and PPR log A/B. DualXLogEn=00b uses A, 01b uses B, and 10b swaps automatically when the active log fills. It stops only when the other log is also full or overflowed.
  - **PPR log (§2.6):** a PCIe PRI page request becomes a PAGE_SERVICE_REQUEST (Figure 75), with GN, the PASID, US and NX when it has a PASID prefix.
    - Overflow sets PprOverflow (or PprOverflowB) and stops the log until it is restarted.
    - PRI with PPREn/PPRLogEn clear or DTE[PPR]=0 logs INVALID_PPR_REQUEST (Table 52).
  - **PPR overflow protection (§2.6.4):**
    - Auto response (not in auto-swap mode): when the log is full, or below the early threshold, the IOMMU answers L=1 requests itself with Success and drops L=0 requests. GPRP decides whether the response carries the PASID.
    - Always-on auto response answers every request.
    - The early warning sets PprOvrflwEarly (A or B) when fewer free slots than the threshold remain, with an interrupt when enabled.
  - **GA log (§2.7):** 8-byte GA_GUEST_NR entries with the GATag and DeviceID, GALogRun, GALOverflow (no interrupt) and GAInt.
- **ATS:**
  - DTE[I] lets a device use ATS.
  - A Translation Request (`dev.ats.translate`) reports R, W, and with a PASID also Exe and Priv. A large page comes back whole, with its size (the S-bit encoding), and the ATC caches it whole. A not-present page answers R=W=0 and is **not logged** (§2.5.3). I=0, V=0 or TV=0 is master aborted and logs INVALID_DEVICE_REQUEST (Type 000b, TR=1).
  - Pretranslated DMA from the ATC passes through when I=1 and is target aborted when I=0. An entry with U=1 sends untranslated requests instead.
  - INVALIDATE_IOTLB_PAGES reaches the device's ATC with the range and PASID. A device that does not answer (`dev.ats.respondsToInvalidation = false`) makes the IOMMU log IOTLB_INV_TIMEOUT with the command's address.
- **Secure ATS (§2.11):** with DTE[I]=1 and DTE[SATS]=1 a Translation Request is walked as usual but answered with the GPA the host tables walked from (for a guest translation, the guest's GPA), and a translated request's address is a GPA the host tables translate again, so a device that forges a translated address reaches only what the host tables map. SATS=1 with I=0 is a reserved bit (ILLEGAL_DEV_TABLE_ENTRY with RZ=1; an ATS or pretranslated request with I=0 is still an INVALID_DEVICE_REQUEST first). SATS=1 with Mode=0 (TV=1) is an ILLEGAL_DEV_TABLE_ENTRY (§2.12.2.4 Note).
- **SEV-SNP (§2.12):** `Platform.rmp` is the machine's Reverse Map Table, shared by every IOMMU: a 4 KiB page number maps to `{ asid, immutable }`, and a page with no entry is hypervisor-owned. `AmdIommu.snpInit()` stands for the ASP's successful SNP_INIT (it needs SNPSup and IommuEn); `IommuFramework.snpInit()` first points the completion store range at the semaphore page, as Linux's `iommu_set_cwwb_range` does. SNP_INIT makes that range and the event and PPR logs immutable RMP pages (Linux 7.3's `amd_iommu_snp_disable` hands back exactly these). Once SNP-enabled:
  - Every device access is RMP checked (§2.12.1): an untranslated or pretranslated request to a page that is not hypervisor-owned is target aborted and logs RMP_PAGE_FAULT (Figure 69) with Flags ASID_MISMATCH (a guest page) or PAGE_IMMUTABLE, AT, RW, SPA=1 and the SPA; a Translation Request to one answers R=W=0 and logs it with AT=01b (Table 54).
  - The registers of §2.12.2.1 are locked: writes are discarded, and Control keeps its IommuEn and DevTblSegEn.
  - There is no exclusion range; 0020h/0028h are the completion store base and limit (§2.12.2.3). A host COMPLETION_WAIT that stores outside [base, limit | 0xFFF] logs COMMAND_HARDWARE_ERROR with Type 00b (Table 58) and stops the command buffer (§2.5.7), without storing.
  - DTE[V]=0 aborts DMA (untranslated and pretranslated) and logs ILLEGAL_DEV_TABLE_ENTRY. Without SnpPageMode0Sup, a device with I=1 and SATS=0 does the same for every request (DTE bit 106).
- **Interrupt remapping:**
  - **Message types (Table 19, Table 10):** MSI data[10:8] is the type. With DM=0 (MSI address bit 2), fixed and arbitrated messages are remapped; NMI (and the reserved types 3 and 6), INIT and ExtInt pass through unmapped when NMIPass, InitPass or EIntPass is set and are target aborted otherwise; SMI passes through. With DM=1 anything but fixed and arbitrated is target aborted, unless NumIntRemapMode=01b: then all eight types are remapped with 2048 IRTEs. IV=0 passes every type through. The platform's interrupt controller records the type and runs vector handlers only for fixed and arbitrated messages.
  - 32-bit IRTEs, or 128-bit IRTEs with GAEn.
  - XTEn allows 32-bit x2APIC destinations.
  - IntCtl=01b passes interrupts through. IntCtl=00b target aborts them and logs INVALID_DEVICE_REQUEST (Type 101b) unless DTE[IG] is set.
  - RemapEn=0, an index past IntTabLen or a reserved encoding gives an IO_PAGE_FAULT with I=1, unless SupIOPF or DTE[IG] is set.
  - **Guest virtual APIC (GAMEn=001b):**
    - The vector's bit is set in the IRR of the vCPU's backing page.
    - If the IRTE says the vCPU is running, the IOMMU rings the doorbell of its core.
    - If not, and GALogIntr is set, it writes a GA log entry and raises the GA interrupt.
  - IntCapXTEn sends the IOMMU's own interrupts through the XT interrupt control registers.
- **Performance counters (§3.4.22):**
  - 2 banks of 4 counters, 48 bits each.
  - Event source (Table 84), PASID/DomainID/DeviceID match registers (mask 0 = any), and the lock registers.
  - A wrap to zero with CERE set logs EVENT_COUNTER_ZERO with the 52-bit note.
- **Driver:** `AmdIommuDriver` programs the IOMMU the way Linux does. Its interrupt handler drains the event log and prints Linux-style lines. Pass `autoDrain: false` to inspect the ring yourself. It restarts the log after an overflow.
  - **v2 page tables** (`pgtable: 'v2'`, as `amd_iommu=pgtbl_v2`): each domain is a 4-level x86-64 table with 47-bit IOVAs. A device's DTE turns host translation off (Mode 0) and GV and GIoV on, and its one-level GCR3 table points PASID 0 at the domain's table. Every device gets its own DomainID, as the spec asks for GIoV, and the driver invalidates with GN=1 and PASID 0. A v2 page has no write-only form, so a DMA_FROM_DEVICE mapping is readable.
  - **Several IOMMUs** (`iommus: n`): IOMMU i of n translates buses [256 i / n, 256 (i + 1) / n), as ACPI IVRS would list them. Each has its own registers, caches, command buffer, event log and interrupt vector (`iommuVector + i`); they share one device table, as Linux shares one per PCI segment. `addDevice({ shareDomain: other })` puts a device in another's DMA domain; unmapping then invalidates every IOMMU that has one of the domain's devices. `IommuFramework` drives one IOMMU, so a bare platform keeps `iommus: 1`.
- **Bare platform:** `new Platform({ driver: false })` (C++: `PlatformOptions::driver = false`) has the IOMMU and no driver.
  - Devices use dma-direct, so a DMA address is the physical address, as with `amd_iommu=off`.
  - DMA writes are posted: the result is `{ posted: true }` and says nothing about a fault. A failed read completes with Completer Abort (`status: "CA"`) and no reason.
  - `IommuFramework` (in `js/bare.mjs`, and in C++ `dmakit/bare.hpp`; it extends `IommuGuestTables`, `IommuTables` and `IommuRegisters` from `bare-guest.mjs`, `bare-tables.mjs` and `bare-registers.mjs`, in C++ `bare_guest.hpp`, `bare_tables.hpp` and `bare_registers.hpp`; `LogRing`, `Counter` and `describeRecord` are in `bare-log.mjs` / `bare_log.hpp`) gives your test code:
    - registers, `setup`, `control` (every field) and `status`
    - `writeDte`: fields merge with the ones written before
    - v1 `map`/`unmap` with any page size, `writeEntry` for raw entries, and `hostEntry` to read a leaf's A/D bits
    - `exclusion`, `pprEarly`, `xtInterrupt` and `counter(bank, n)`
    - guest tables: `enableGuest`, `guestMap`, `guestEntry`, `guestUnmap`, `guestRevoke`
    - interrupts: `intTable`, `writeIrte`, `setRunning`, `irr`
    - every command, with GN/PASID where the command has them
    - `mockPageOut` / `mockPageIn` / `mockGuestPageOut`
    - `restart` for every log
    - a `LogRing` per log (`event`, `eventB`, `ppr`, `pprB`, `ga`) with `head`, `tail`, `count`, `full`, `pending()` and `consume(n)`
    - `mapTable`/`unmapTable`/`tableEntry` for a v1 table no device owns, such as a VM's stage 2
- **A VM with a virtual IOMMU** (the software VMM, in `js/vmm/viommu.mjs` and `dmakit/vmm/viommu.hpp`, imported as `dmakit/vmm` in JavaScript; the guest's memory is `guest-memory.mjs` / `guest_memory.hpp` there; the core imports it only at the script runner's hooks), on a bare host. It follows Linux 7.3 `drivers/iommu/amd/nested.c` and `iommufd.c`:
  - `new Vm(hostFw, { ram, domain })`: `ram` is the VM's RAM layout (GPA to SPA). The host maps it into a 4-level stage-2 table, the nest parent, which only the host edits (`s2`, then `flushS2`, which invalidates every host DomainID of the VM).
  - The host, not the guest, enables ATS: `hostFw.writeDte(physicalDevice, { i: true })` keeps DTE[I] across the VMM's later writes.
  - The guest gets a whole virtual AMD IOMMU over its guest-physical memory (`vm.platform`, `vm.fw`): its own device table, GCR3 and guest page tables and command buffer, programmed with the same `IommuFramework` calls.
  - `vm.assign(physicalDevice, guestBdf)` gives the guest its view of a host device. DMA from that view is the VMM emulating the device through the virtual IOMMU in software; DMA from the physical device goes through the host IOMMU.
  - The VMM traps each guest command. `INVALIDATE_DEVTAB_ENTRY` makes it read and check the guest DTE (Mode 0; GV=1 needs a GCR3 root; no reserved GLX or GuestPagingMode) and write the host DTE: the nest parent, one host DomainID per guest DomainID (taken from the host, so two VMs never share one; `domain` sets the lowest), GV, GLX, GIoV, PPR and the GCR3 root as the guest wrote it, a GPA. `INVALIDATE_IOMMU_PAGES` goes on with the host DomainID and `INVALIDATE_IOMMU_ALL` as a full flush of each of the VM's host DomainIDs, `INVALIDATE_IOTLB_PAGES` and `COMPLETE_PPR_REQUEST` with the physical DeviceID (for a device it only emulates, nowhere), and `COMPLETION_WAIT` finishes on the host first. A PPR completion goes on only while the host's PPR is on, since the host would otherwise halt its command buffer for everyone. `INVALIDATE_INTERRUPT_TABLE` and `PREFETCH_IOMMU_PAGES` stay with the virtual IOMMU. Linux 7.3 has no uAPI for passing guest invalidations on; this follows the commands section 2.10.7 lets a guest send.
  - A guest DTE that blocks the device (IR/IW=0) is honoured, and one that does not translate (V, TV or GV=0) leaves stage 2 alone in charge. A guest DTE with a Host Page Table Root is refused, as `validate_gdte_nested` refuses it.
  - When the physical device does not answer an `INVALIDATE_IOTLB_PAGES`, the host logs `IOTLB_INV_TIMEOUT` with its own DeviceID and command SPA; the VMM logs one in the guest's event log with the guest's DeviceID and the GPA of the guest's command (section 2.10.3: the host makes the entry suitable for the guest).
  - `vm.unassign(guestDevice)` gives a device back as Linux's detach does: the host blocks its DTE (IR=IW=0, and I=0 so translated requests from its ATC are refused), flushes its domain and waits. Like Linux once ATS is off, it sends the device no ATC invalidation. It returns whether the wait finished; the VM's memory must not be reused before that. A device reset (not modelled) is still the host's job before another VM gets it.
  - `vm.log` records each VMM action. A DMA result's `phys` and `path` say where the first byte went and how it was translated.

#### Model choices

Where the model had to pick a behaviour, it does this:

- **Mode 7 in a DTE:** Table 44 treats a reserved paging mode as an IO_PAGE_FAULT (PR=1, RZ=0), not an ILLEGAL_DEV_TABLE_ENTRY. dmakit 0.3 logged the latter.
- **Special address ranges (Table 3, §2.1.2):** unless `htRangeIgnore: true`, an untranslated request without a PASID prefix to FD_0000_0000h to FF_FFFF_FFFFh follows the device's DTE: interrupt ranges abort (Types 0, 5, 6), `SysMgt` and `IoCtl` abort (0), forward (1) or translate (3 and 2, like memory) the system management and port I/O ranges, and `htAtsResv: true` reserves the address translation range. The page-table rows (legacy PIC IACK, configuration, the reserved ones) translate as memory. A 4-level table (`mode: 4`) reaches them. `DTE[IG]` suppresses these events (§2.5.9); the older 0xFEEx ATS and PASID-without-guest events ignore it. A device's DMA is a read or a posted write, so SysMgt=10b always aborts a write (it admits INTx messages only); a write Table 3 hands to interrupt remapping aborts without an event (`sendMsi` makes an interrupt); a pretranslated request passes on untranslated even to the interrupt rows (§2.1.4.2 says the attribute is ignored there).
- **Page-table faults:**
  - Nonzero index bits in a skipped level report PR=0.
  - A misaligned large leaf reports RZ=1.
- **Guest and nested faults:**
  - A fault in a nested (host) walk of a guest structure is reported with the GVA and GN=1.
  - The host tables must let the IOMMU read each guest structure (a guest table, or a GCR3 entry)
    and, to set its A or D bit, write it; otherwise the request faults with PR=1 and PE=1. DTE[IR]
    and DTE[IW] apply to the data page only. 48882 does not state this outright: it follows AMD64
    nested paging and §2.2.7.2 and §2.2.7.4.
  - A non-canonical GVA is a fault.
  - GIoV requests are treated as user (PMR=0).
- **IRTE faults:** RemapEn=0 reports PR=0. A reserved encoding reports PR=1, RZ=1.
- **PPR and PRI:**
  - PRI with PPREn=0 is INVALID_PPR_REQUEST (Table 52).
  - A V=0 DTE counts as PPR=0 (EPHSup=1).
  - PRI with a PASID but no guest translation is INVALID_DEVICE_REQUEST Type 100b.
  - Auto responses always use the Success code.
- **Auto-swap:** when the active log fills, the IOMMU swaps unless the other log's overflow bit is set. If the other log is also full, that log overflows and logging stops.
- **Host A/D bits:**
  - dmakit reports both HASup and HDSup.
  - HAD=10b is a reserved encoding, so it is an ILLEGAL_DEV_TABLE_ENTRY (Table 43) like IoCtl=11b.
  - D is set only for a write the tables allow, as Table 32 does for guest tables. A faulting write sets A only.
  - Nested walks set host A for guest-structure reads, and D only for the data page of a write. Writing guest A/D bits does not set host D.
- **Interrupt types:**
  - A message that a pass bit (or DM=1) target aborts logs no event.
  - LINT0 and LINT1 are HyperTransport message types (14 and 11). A PCIe MSI's three delivery-mode bits cannot encode them, so no device here sends one; the DTE bits are decoded and kept.
- **Nested fault reasons:** a failed host walk of a guest structure names the structure's GPA (`guest L3 entry at GPA 0x...: host L1 entry not present`), and a host table that denies what the guest table allows says so (`host table denies write`). The event record is the same either way.
- **The virtual IOMMU's EFR** is dmakit's own: a real VMM shows the guest a filtered view (a VM-specific IVRS).
- **v2 IOVAs:** Linux notes that hardware does not sign-extend PASID 0's addresses. dmakit checks that every GVA is canonical; the driver's 47-bit IOVAs satisfy both.
- **SEV-SNP and Secure ATS:**
  - The ILLEGAL_DEV_TABLE_ENTRY rules of §2.12.2.4 are checked before the Mode=0 RMP_PAGE_FAULT rule. With SnpPageMode0Sup=0 an ATS device with Mode=0 always meets one of them. Untranslated DMA from a Mode=0 device is identity then RMP checked.
  - The sATS=0 rule applies only to I=1 devices.
  - DMA to a guest's page reports ASID_MISMATCH. Reads are checked as well as writes, as §2.12.1 says "all device DMA".
  - A COMPLETION_WAIT store outside the completion store range logs COMMAND_HARDWARE_ERROR Type 00b (Table 58).
  - Secure ATS for a host-only table returns the address the host tables walked from (the IOVA, which is the GPA for a VM's device).
  - A translated request from an sATS=1 device whose DTE has TV=0 is an IO_PAGE_FAULT, as an untranslated one would be: with no host table to translate it again, its address must not be used as an SPA.
  - A pretranslated request that fails the RMP check is target-aborted like an untranslated one.
  - A Translation Request with V=0 stays an INVALID_DEVICE_REQUEST under SNP.
  - RMP checks are not cached: a page given to a guest after it was mapped is refused on the next access.

#### Hardware vIOMMU (§2.10; option `viommu`)

A bounded subset of AMD's hardware-assisted vIOMMU, mirroring the four QEMU patches `docs/evidence/iommu-qemu/qemu-master-amd-viommu-{1-windows,2-commands,3-logs,4-interrupts}.patch`: `docs/evidence/iommu-qemu/init-amd-viommu` (33 steps, V1 to V33, in the same four pieces) gives byte-identical output on dmakit and QEMU (`dmakit-run.mjs`). `new Platform({driver: false, viommu: {rid}})` (`rid`: the IOMMU's own DeviceID) turns it on; it is off by default, but 4.1.0 changed behaviour without it: a DTE with sATS=1 and I=0 is reserved, a DeviceID beyond DevTabBase.Size is a Table 44 fault whatever the options, and `lookupDte` is nullable (see the CHANGELOG, 4.1.0). Its tests are `viommu-hw` (the model's rules) and the examples `viommu_hw`, `viommu_hw_edges` and `viommu_hw_callbacks` (JavaScript against C++, byte for byte); the seeded parity suite does not drive it. Files (each with a C++ twin `cpp/include/dmakit/amd/amd_viommu*.hpp`): `js/amd/amd-viommu.mjs` (the vendor capability and the VFMMIO and VFCntl windows), `-guest` (guests, Run bits), `-run` (the guests' buffers) and `-interrupt` (guest interrupts).
- **Modelled:** the vendor-specific capability with RW1S Enable-locked base registers; a 4 KB VFMMIO window and a 64 B VFCntl window per 16-bit GuestID (`viommuRead`, `viommuWrite`, `windowAt` in `amd-viommu.mjs`; a platform maps the two bases to them); GstBufferTRPMode=1 (guest-physical buffers walked through the host page table of the Misc Control DeviceID); the Guest Device and Domain ID maps; a guest's command buffer (COMPLETION_WAIT, INVALIDATE_DEVTAB_ENTRY, INVALIDATE_IOTLB_PAGES, COMPLETE_PPR_REQUEST, INVALIDATE_IOMMU_PAGES), event log and PPR log, an illegal command halting only that guest's buffer; the host-only INSERT_GUEST_EVENT (with GUEST_EVENT_FAULT) and RESET_VMMIO; guest PRI through DTE vImuEn, GuestID and GDeviceID; guest interrupts through the IOMMU's own DTE and two-level interrupt table; EFR[vIommuSup], the vIOMMU Status register and the V bits that mark an event as caused by a guest buffer.
- **Options:** `sats: false` leaves out EFR[SATSSup] (the DTE's sATS and HPTMode bits become reserved); `memoryDecodes: (address) => boolean` makes a table, command or log read from an address that nothing answers a hardware error event (the ordinary interrupt remapping table's IRTE read is not checked; only the hardware vIOMMU's own two-level table reads are). `dmakit-run.mjs` sets all of them for `init-amd-viommu`.
- **Where QEMU's patch is not the spec, and dmakit follows QEMU** (marked `fragile:` in the code where it is a QEMU dependency, the rest listed in `docs/qemu-sats-viommu.md`): a host page-table read that nothing answers (a DMA or a guest buffer) logs PAGE_TAB_HARDWARE_ERROR and then the request's IO_PAGE_FAULT, where Table 60 has the hardware error alone (`amd-iommu-translate.mjs`; a GN=1 guest walk that meets such an entry logs the hardware error alone, with GN=1 and the PASID as D/P, and aborts the request); a GUEST_EVENT_FAULT pair needs two free host log slots, or neither is written (§2.5.12 only says they come in pairs); an INSERT_GUEST_EVENT whose entry is not queued yet waits (§2.4.9); a guest event or PPR log write that memory does not answer is dropped with no event (VIOMMU_HARDWARE_ERROR, §2.5.13); the vendor capability's offset is the capability list's, not the spec's 110h and 118h.
- **Not modelled:** GstBufferTRPMode=0 and the private address space (§2.10.1), VMVld, the B logs and PPR auto response of a guest, QueueID, VIOMMU_HARDWARE_ERROR, and anything beyond the subset above.

#### Not modelled

- The rest of SEV-SNP: RMP_HARDWARE_ERROR, the PAGE_LOCKED and PAGE_SIZE_MISMATCH flags, RMP checks of the IOMMU's own table reads, the RMP entry format and page-size rules of the AMD64 manual, and SNP guest virtual APIC (SNPAVICSup). SEV-TIO (§2.14), and the hardware vIOMMU of section 2.10 beyond the bounded subset described above. The virtual IOMMU (`Vm`) is the software path: the VMM traps every guest command.
- The SMI filter (SMIs always pass through), and LINT0/LINT1 messages (see above).
- More than one PCI segment, and MMIO beyond the registers above.
- The guest-side timing of ATS invalidation completions: a device answers at once, or not at all.

## Scenario language

`runScript` and `runPlatformScript` (JavaScript) and `run_script` and `run_platform_script` (C++) run small text scenarios and print one line per step. The parity tests diff their output. `cpp/tools/dmakit_script.cpp` runs a scenario from stdin; `tools/dmakit-script.mjs [--check] [file]` prints the same lines from JavaScript.

```
platform iommu=1 strict=0 nslabs=64
device bdf=03:00.0
alloc name=buf size=8192 fill=0x41
map id=a dev=03:00.0 phys=buf size=4096 dir=to
dma_read dev=03:00.0 addr=a+0x10 len=8
dma_write dev=03:00.0 addr=0xdead0000 len=4 value=0x11
msi dev=03:00.0 vectors=0x41 dest=2
raise dev=03:00.0 n=0
unmap id=a
flush dev=03:00.0
state
```

A bare platform scenario, where the script programs the IOMMU itself (every op is listed at the top of `js/platform-script.mjs`; the ops themselves live in `js/platform-script-*.mjs`):

```
platform iommu=1 driver=0
setup cmd=8 evt=8 ppr=4
control iommu=1 evt=1 cmd=1 ppr=1 pprlog=1
device bdf=03:00.0
dte dev=03:00.0 mode=4 domain=1 ppr=1
pri dev=03:00.0 addr=0x5000 prg=1
dump log=ppr
consume log=ppr n=1
complete_ppr dev=03:00.0 prg=1 code=0
restart log=ppr
state
```

SEV-SNP and Secure ATS on a bare platform (`rmp` gives a page to a guest; `snp_init` runs Linux's preparation and the ASP's SNP_INIT):

```
platform iommu=1 driver=0 snpmode0=0
setup
control iommu=1 evt=1 cmd=1
device bdf=03:00.0
alloc name=buf size=8192 fill=0x41
dte dev=03:00.0 mode=4 domain=1 i=1 sats=1
pt_map dev=03:00.0 iova=0x1000 phys=buf size=8192 perm=rw
rmp page=buf+0x1000 asid=5
snp_init
dma_write dev=03:00.0 addr=0x2000 len=4 value=0x11
ats_translate dev=03:00.0 addr=0x1000
cwait at=buf
dump log=evt
```

A VM with a virtual IOMMU: `vm <op>` runs any op in the guest, on its virtual IOMMU, and each VMM action is echoed as `vmm <line>`:

```
platform iommu=1 driver=0
setup
control iommu=1 evt=1 cmd=1 gt=1 gcr3trp=1
device bdf=03:00.0
viommu ram=0x81000000:0x140000000:0x100000,0x80001000:0x120001000:0x1000 domain=0x43
vassign dev=03:00.0 gbdf=01:00.0
vm setup devtab=2
vm control iommu=1 evt=1 cmd=1 gt=1
vm dte dev=01:00.0 mode=0 domain=1
vm guest dev=01:00.0 glx=1
vm gmap dev=01:00.0 pasid=513 gva=0x10001000 gpa=0x80001000 size=4096
vm inv_dte dev=01:00.0
dma_write dev=03:00.0 addr=0x10001230 len=16 value=0x56 pasid=513
s2 gpa=0x80001000 size=4096 perm=r
s2flush gpa=0x80001000 size=4096
state
```

The DMA-API language (`swiotlb`, `iommu`, `device`, `fill`, `map`, `dev_read`, `dev_write`, `sync_cpu`, `sync_dev`, `unmap`, `flush`, `state`) is documented in `js/script.mjs`.

## Tests

- `npm test` runs every `tests/*.test.mjs` and `tests/vmm/*.test.mjs` (the software VMM), one file per area:
  - kernel rules (`swiotlb`, `domain-map`)
  - the emulated AMD IOMMU against the spec: the platform (`platform`), the bare platform and PPR log (`bare`), ATS, the ATC and page-in (`ats`), features (`features-translation`, `features-guest`, `features-interrupts`), the special address ranges of Table 3 (`special-ranges`), the virtual IOMMU (`viommu`, `viommu-observer`), the hardware vIOMMU (`viommu-hw*`, `command-loop-reentry`), table reads memory does not answer (`table-errors`, `device-table-requests`), SEV-SNP and Secure ATS (`snp`)
  - the Arm SMMUv3 and RISC-V IOMMU against their specs (`arm-smmu`, `riscv-iommu` and their `-limits` files, each scenario also run by its C++ twin and compared line for line) and the architecture contract (`arch`)
  - the parity suite, which compiles the C++ runner with `g++` (set `CXX` to change it) and compares 2 fixed and 1,580 seeded random scenarios:
    - 200 DMA API, 120 platform, 150 bare platform and 150 ATS / PRI page-in
    - 120 each for translation, logs, guest translation, interrupts and counters, host A/D and message types, several IOMMUs and v2, the virtual IOMMU, and SEV-SNP with Secure ATS
  - the examples (`examples`): each C++ example against its JavaScript twin, its single-file build compiled with `-Werror` and run under ASan and UBSan, and the browser flattener
  - housekeeping: the exports (`exports`), clang-format and clang-tidy (`lint`) and the version in every file (`version`)
- `cmake -S . -B build && cmake --build build && ctest --test-dir build` builds and runs the C++ examples.
- For a sanitizer check, build `cpp/tools/dmakit_script.cpp` with `-fsanitize=address,undefined` and feed it the parity scenarios. The tests compile everything with the `compile_flags.txt` warnings and `-Werror`, and `tests/lint.test.mjs` runs clang-format and clang-tidy (see "Code style").
