Foundry/tt/tests/test_launch.c

1685 lines
79 KiB
C

/* SPDX-License-Identifier: AGPL-3.0-or-later */
/* Copyright (C) 2026 David (Corey) Herrera */
/**
* @file test_launch.c
* @brief Phase 2 off-device tests: ELF parsing and segment placement, the embedded
* blob format, launch-message and CB encoding, runtime-arg planting, reset
* read-modify-write, and the mailbox completion poll including its timeout.
*
* NO DEVICE IS TOUCHED. Every case runs against a simulated Tensix — 1.5 MiB of host
* memory standing in for L1 plus one word standing in for the soft-reset register —
* behind the same ft_tt_noc_access vtable the real TLB path implements. That is the
* whole point of that indirection: this file exercises the exact bytes the card would
* have received, on a machine where a mistake costs a failed assert instead of a
* power cycle.
*
* What this CANNOT tell you: whether the transliterated mailbox layout matches the
* firmware binary, or whether a real SFPI-built ELF parses. Both need the card and
* its host. docs/tt_launch_notes.md §6 lists them in the order to check them.
*
* @status Green on macOS/arm64 under
* `cc -std=c99 -Wall -Wextra -Werror -pedantic -Wshadow -Wconversion`.
* Every case has been observed RED — the mutation table is docs/tt_launch_notes.md §8.
* @issues None
* @todo None
*/
#include <inttypes.h>
#include <stdint.h>
#include <stdio.h>
#include <string.h>
#include "ft_tt_arc_telemetry.h"
#include "ft_tt_arch.h"
#include "ft_tt_buffer.h"
#include "ft_tt_elf.h"
#include "ft_tt_kernel_blob.h"
#include "ft_tt_launch.h"
#include "ft_tt_mailbox.h"
#include "ft_tt_op_l1_layout.h"
#include "ft_tt_phase2_l1_layout.h"
/* ------------------------------------------------------------------------- */
/* Tiny harness */
/* ------------------------------------------------------------------------- */
static int checks_run;
static int checks_failed;
static void check(int condition, const char *what)
{
checks_run++;
if (condition) {
printf(" ok %s\n", what);
} else {
checks_failed++;
printf(" FAIL %s\n", what);
}
}
static void check_u64(uint64_t got, uint64_t want, const char *what)
{
checks_run++;
if (got == want) {
printf(" ok %s = 0x%" PRIx64 "\n", what, got);
} else {
checks_failed++;
printf(" FAIL %s = 0x%" PRIx64 ", expected 0x%" PRIx64 "\n", what, got, want);
}
}
/* ------------------------------------------------------------------------- */
/* Simulated Tensix */
/* ------------------------------------------------------------------------- */
/**
* Stands in for one core. `go_reads_before_done` scripts the completion handshake:
* after that many reads of the go message, the simulated firmware writes DONE,
* exactly as the BRISC firmware does. UINT32_MAX means "never finishes", which is
* how the timeout path is exercised.
*/
typedef struct simulated_tensix {
uint8_t l1[FT_TT_MEM_L1_SIZE];
uint32_t soft_reset;
/* The two Tensix debug registers a snapshot reads alongside the soft reset. Modelled
* here because a fake that answered FT_TT_EINVAL for them would let a snapshot which
* SILENTLY DROPPED them look identical to one that reads them. */
uint32_t instrn_buf_status;
uint32_t invalid_instrn;
int refuse_debug_registers; /* model a device that will not answer them */
uint32_t go_reads;
uint32_t go_reads_before_done;
uint32_t write_calls;
int fail_after_writes; /* -1 = never fail */
} simulated_tensix;
static simulated_tensix simulated_core;
static int simulated_write(void *user, uint16_t x, uint16_t y, uint64_t addr, const void *src,
size_t bytes)
{
simulated_tensix *core = (simulated_tensix *)user;
(void)x;
(void)y;
core->write_calls++;
if (core->fail_after_writes >= 0 && (int)core->write_calls > core->fail_after_writes)
return FT_TT_EIO;
if (addr == FT_TT_TENSIX_SOFT_RESET_ADDR && bytes == 4u) {
memcpy(&core->soft_reset, src, 4u);
return FT_TT_OK;
}
if (addr + bytes > FT_TT_MEM_L1_SIZE)
return FT_TT_EINVAL;
memcpy(core->l1 + addr, src, bytes);
return FT_TT_OK;
}
static int simulated_read(void *user, uint16_t x, uint16_t y, uint64_t addr, void *dst, size_t bytes)
{
simulated_tensix *core = (simulated_tensix *)user;
(void)x;
(void)y;
if (addr == FT_TT_TENSIX_SOFT_RESET_ADDR && bytes == 4u) {
memcpy(dst, &core->soft_reset, 4u);
return FT_TT_OK;
}
if (addr == FT_TT_TENSIX_INSTRN_BUF_STATUS_ADDR && bytes == 4u) {
if (core->refuse_debug_registers)
return FT_TT_EIO;
memcpy(dst, &core->instrn_buf_status, 4u);
return FT_TT_OK;
}
if (addr == FT_TT_TENSIX_DBG_INVALID_INSTRN_ADDR && bytes == 4u) {
if (core->refuse_debug_registers)
return FT_TT_EIO;
memcpy(dst, &core->invalid_instrn, 4u);
return FT_TT_OK;
}
if (addr + bytes > FT_TT_MEM_L1_SIZE)
return FT_TT_EINVAL;
/* The scripted half of the fake firmware: reading the go message eventually
* returns DONE. */
if (addr == FT_TT_MB_GO_SLOT_ADDR(0) && bytes == sizeof(ft_tt_go_msg)) {
core->go_reads++;
if (core->go_reads >= core->go_reads_before_done)
core->l1[FT_TT_MB_GO_SLOT_ADDR(0) + offsetof(ft_tt_go_msg, signal)] =
(uint8_t)FT_TT_RUN_MSG_DONE;
}
memcpy(dst, core->l1 + addr, bytes);
return FT_TT_OK;
}
static ft_tt_noc_access simulated_access(void)
{
ft_tt_noc_access access;
access.write = simulated_write;
access.read = simulated_read;
access.user = &simulated_core;
return access;
}
static void reset_simulated_core(void)
{
memset(&simulated_core, 0, sizeof(simulated_core));
simulated_core.go_reads_before_done = UINT32_MAX;
simulated_core.fail_after_writes = -1;
/* A healthy core: 140 of 140 read this on silicon, in every state observed. */
simulated_core.instrn_buf_status = FT_TT_TENSIX_INSTRN_BUF_READY;
simulated_core.invalid_instrn = 0u;
}
static uint32_t simulated_l1_u32(uint32_t address)
{
uint32_t value;
memcpy(&value, simulated_core.l1 + address, sizeof(value));
return value;
}
/* ------------------------------------------------------------------------- */
/* Synthetic ELF builder */
/* ------------------------------------------------------------------------- */
/*
* Lays out an ELF32 little-endian RISC-V executable shaped like the ones
* build_tensix_kernel.sh produces: a text PT_LOAD whose VMA equals its LMA, an
* optional data PT_LOAD whose VMA is in local memory and whose LMA is in L1, and a
* non-allocatable `.segments` note carrying the per-region size limits.
*/
#define SYNTHETIC_ELF_CAPACITY 4096u
typedef struct synthetic_elf_spec {
uint32_t entry;
uint32_t text_vaddr;
uint32_t text_bytes;
uint32_t text_limit;
int with_data;
uint32_t data_vaddr;
uint32_t data_lma;
uint32_t data_bytes;
uint32_t data_limit;
int with_segment_note;
uint16_t elf_type;
uint16_t machine;
uint32_t text_filesz_override; /* 0 = use text_bytes */
} synthetic_elf_spec;
typedef struct synthetic_elf {
uint8_t bytes[SYNTHETIC_ELF_CAPACITY];
size_t size;
} synthetic_elf;
static void put_u16(uint8_t *base, size_t offset, uint16_t value)
{
base[offset] = (uint8_t)(value & 0xFFu);
base[offset + 1u] = (uint8_t)(value >> 8);
}
static void put_u32(uint8_t *base, size_t offset, uint32_t value)
{
base[offset] = (uint8_t)(value & 0xFFu);
base[offset + 1u] = (uint8_t)((value >> 8) & 0xFFu);
base[offset + 2u] = (uint8_t)((value >> 16) & 0xFFu);
base[offset + 3u] = (uint8_t)((value >> 24) & 0xFFu);
}
static void write_program_header(uint8_t *base, size_t at, uint32_t offset, uint32_t vaddr,
uint32_t paddr, uint32_t filesz, uint32_t memsz)
{
put_u32(base, at + 0u, 1u); /* PT_LOAD */
put_u32(base, at + 4u, offset);
put_u32(base, at + 8u, vaddr);
put_u32(base, at + 12u, paddr);
put_u32(base, at + 16u, filesz);
put_u32(base, at + 20u, memsz);
put_u32(base, at + 24u, 5u); /* PF_R | PF_X */
put_u32(base, at + 28u, 4u);
}
static void write_section_header(uint8_t *base, size_t at, uint32_t name, uint32_t type,
uint32_t offset, uint32_t size)
{
memset(base + at, 0, 40u);
put_u32(base, at + 0u, name);
put_u32(base, at + 4u, type);
put_u32(base, at + 16u, offset);
put_u32(base, at + 20u, size);
}
/** Fixed layout so every offset below is a constant, not a running total. */
#define ELF_PHDR_OFFSET 64u
#define ELF_TEXT_OFFSET 256u
#define ELF_DATA_OFFSET 1024u
#define ELF_NOTE_OFFSET 1536u
#define ELF_STRTAB_OFFSET 1664u
#define ELF_SHDR_OFFSET 1792u
static void build_synthetic_elf(const synthetic_elf_spec *spec, synthetic_elf *out)
{
static const char string_table[] = "\0.segments\0.shstrtab";
uint32_t note_words[6];
uint32_t note_bytes = 0;
uint32_t text_filesz = spec->text_filesz_override ? spec->text_filesz_override : spec->text_bytes;
uint16_t phnum = spec->with_data ? 2u : 1u;
uint32_t index;
memset(out, 0, sizeof(*out));
memcpy(out->bytes, "\x7f" "ELF", 4u);
out->bytes[4] = 1u; /* ELFCLASS32 */
out->bytes[5] = 1u; /* ELFDATA2LSB */
out->bytes[6] = 1u; /* EV_CURRENT */
put_u16(out->bytes, 16u, spec->elf_type);
put_u16(out->bytes, 18u, spec->machine);
put_u32(out->bytes, 20u, 1u);
put_u32(out->bytes, 24u, spec->entry);
put_u32(out->bytes, 28u, ELF_PHDR_OFFSET);
put_u32(out->bytes, 32u, ELF_SHDR_OFFSET);
put_u16(out->bytes, 40u, 52u); /* e_ehsize */
put_u16(out->bytes, 42u, 32u); /* e_phentsize */
put_u16(out->bytes, 44u, phnum);
put_u16(out->bytes, 46u, 40u); /* e_shentsize */
put_u16(out->bytes, 48u, 3u); /* null, .segments, .shstrtab */
put_u16(out->bytes, 50u, 2u); /* e_shstrndx */
write_program_header(out->bytes, ELF_PHDR_OFFSET, ELF_TEXT_OFFSET, spec->text_vaddr,
spec->text_vaddr, text_filesz, spec->text_bytes);
if (spec->with_data)
write_program_header(out->bytes, ELF_PHDR_OFFSET + 32u, ELF_DATA_OFFSET, spec->data_vaddr,
spec->data_lma, spec->data_bytes, spec->data_bytes);
/* Recognisable contents so a misplaced write is obvious in the dump. */
for (index = 0; index < spec->text_bytes && index < 256u; index++)
out->bytes[ELF_TEXT_OFFSET + index] = (uint8_t)(0xA0u + (index & 0x0Fu));
if (spec->with_data)
for (index = 0; index < spec->data_bytes && index < 256u; index++)
out->bytes[ELF_DATA_OFFSET + index] = (uint8_t)(0xD0u + (index & 0x0Fu));
if (spec->with_segment_note) {
note_words[0] = spec->text_vaddr;
note_words[1] = spec->text_vaddr;
note_words[2] = spec->text_limit;
note_bytes = 12u;
if (spec->with_data) {
note_words[3] = spec->data_vaddr;
note_words[4] = spec->data_vaddr;
note_words[5] = spec->data_limit;
note_bytes = 24u;
}
for (index = 0; index < note_bytes / 4u; index++)
put_u32(out->bytes, ELF_NOTE_OFFSET + index * 4u, note_words[index]);
}
memcpy(out->bytes + ELF_STRTAB_OFFSET, string_table, sizeof(string_table));
write_section_header(out->bytes, ELF_SHDR_OFFSET, 0u, 0u, 0u, 0u);
write_section_header(out->bytes, ELF_SHDR_OFFSET + 40u, 1u /* ".segments" */, 1u /* PROGBITS */,
ELF_NOTE_OFFSET, note_bytes);
write_section_header(out->bytes, ELF_SHDR_OFFSET + 80u, 11u /* ".shstrtab" */, 3u /* STRTAB */,
ELF_STRTAB_OFFSET, (uint32_t)sizeof(string_table));
out->size = ELF_SHDR_OFFSET + 120u;
}
/** A plausible TRISC0 kernel: text just above the firmware hole, data staged in L1. */
static void build_kernel_elf(synthetic_elf *out, uint32_t text_vaddr, uint32_t text_bytes)
{
synthetic_elf_spec spec;
memset(&spec, 0, sizeof(spec));
spec.entry = text_vaddr;
spec.text_vaddr = text_vaddr;
spec.text_bytes = text_bytes;
spec.text_limit = FT_TT_MEM_MAX_KERNEL_SIZE;
spec.with_data = 1;
spec.data_vaddr = FT_TT_MEM_LOCAL_BASE + 0x400u;
spec.data_lma = text_vaddr + text_bytes;
spec.data_bytes = 64u;
spec.data_limit = FT_TT_MEM_TRISC_LOCAL_SIZE;
spec.with_segment_note = 1;
spec.elf_type = 2u;
spec.machine = 243u;
build_synthetic_elf(&spec, out);
}
/** A plausible TRISC0 firmware: text AT the firmware base, data image relocated. */
static void build_firmware_elf(synthetic_elf *out, unsigned processor, uint32_t text_bytes)
{
synthetic_elf_spec spec;
memset(&spec, 0, sizeof(spec));
spec.entry = ft_tt_processor_firmware_base(processor);
spec.text_vaddr = spec.entry;
spec.text_bytes = text_bytes;
spec.text_limit = ft_tt_processor_firmware_size(processor);
spec.with_data = 1;
spec.data_vaddr = FT_TT_MEM_LOCAL_BASE + 0x100u;
spec.data_lma = spec.entry + text_bytes; /* deliberately NOT where it belongs */
spec.data_bytes = 32u;
spec.data_limit = FT_TT_MEM_TRISC_LOCAL_SIZE;
spec.with_segment_note = 1;
spec.elf_type = 2u;
spec.machine = 243u;
build_synthetic_elf(&spec, out);
}
/* ------------------------------------------------------------------------- */
/* 1. Mailbox layout */
/* ------------------------------------------------------------------------- */
/*
* These are a regression lock on ft_tt_mailbox.h, not an independent derivation: they
* pin the offsets that were computed from upstream's field order so a later edit to
* the struct cannot move them silently. The values themselves are only proven correct
* by the read-back in docs/tt_launch_notes.md §6 step 4.
*/
/** The p150a as ARC telemetry reports it: 14 Tensix columns, 8 DRAM banks, nothing
* harvested. */
static ft_tt_grid_info p150a_grid(void)
{
ft_tt_grid_info grid;
memset(&grid, 0, sizeof(grid));
grid.valid = true;
/* Off-hardware nothing selects an architecture, so the fixture names the one it
* is a fixture for. Every geometry constant ft_tt_coord uses arrives this way. */
grid.arch = ft_tt_arch_blackhole();
/* The card reports translation on, and DRAM's translated column pair only exists
* when it is: without this the coordinate module correctly answers in NoC0. */
grid.noc_translation_enabled = 1u;
grid.tensix_enabled_col = 16383u;
grid.tensix_column_count = 14u;
grid.tensix_core_count = 140u;
grid.enabled_gddr = 255u;
grid.dram_bank_count = 8u;
return grid;
}
static void test_mailbox_layout(void)
{
printf("\nmailbox layout (locked against ft_tt_mailbox.h)\n");
check_u64(sizeof(ft_tt_kernel_config_msg), 144u, "sizeof(kernel_config_msg)");
check_u64(sizeof(ft_tt_launch_msg), 144u, "sizeof(launch_msg)");
check_u64(sizeof(ft_tt_go_msg), 4u, "sizeof(go_msg)");
check_u64(FT_TT_MB_SUBORDINATE_SYNC_ADDR, 0x68u, "subordinate_sync L1 address");
check_u64(FT_TT_MB_LAUNCH_RD_PTR_ADDR, 0x6Cu, "launch_msg_rd_ptr L1 address");
check_u64(FT_TT_MB_LAUNCH_ADDR, 0x70u, "launch[0] L1 address");
check_u64(FT_TT_MB_GO_MESSAGES_ADDR, 0x4F0u, "go_messages[0] L1 address");
check_u64(FT_TT_MB_GO_MESSAGE_INDEX_ADDR, 0x520u, "go_message_index L1 address");
check_u64(offsetof(ft_tt_kernel_config_msg, enables), 120u, "kernel_config.enables offset");
check_u64(offsetof(ft_tt_kernel_config_msg, kernel_text_offset), 52u,
"kernel_config.kernel_text_offset offset");
check_u64(offsetof(ft_tt_kernel_config_msg, preload), 143u,
"kernel_config.preload offset (must be last)");
check(FT_TT_RUN_MSG_DONE == 0u, "RUN_MSG_DONE is zero, which is why the handshake needs priming");
check(FT_TT_SANITIZE_OK != 0u,
"DebugSanitizeOK is NOT zero, which is why the watcher slots need priming too");
/* core_info sits BEHIND watcher and dprint_buf, so these three numbers are one
* chain: a wrong watcher size moves core_info and the firmware then resolves NoC
* endpoints out of someone else's bytes. */
check_u64(sizeof(ft_tt_watcher_msg), 288u, "sizeof(watcher_msg)");
check_u64(sizeof(ft_tt_device_print_memory_layout), 1020u, "sizeof(dprint_buf)");
check_u64(sizeof(ft_tt_core_info_msg), 248u, "sizeof(core_info_msg)");
check_u64(FT_TT_MB_WATCHER_ADDR, 0x530u, "watcher L1 address");
check_u64(FT_TT_MB_CORE_INFO_ADDR, 0xA50u, "core_info L1 address");
check_u64(FT_TT_MB_WATCHER_SANITIZE_ADDR(0), 0x548u, "watcher.sanitize[0] L1 address");
check_u64(FT_TT_MB_WATCHER_SANITIZE_ADDR(1), 0x560u, "watcher.sanitize[1] L1 address");
check_u64(offsetof(ft_tt_core_info_msg, virtual_non_worker_cores), 137u,
"core_info.virtual_non_worker_cores offset");
check_u64(offsetof(ft_tt_core_info_msg, noc_size_x), 228u, "core_info.noc_size_x offset");
check_u64(offsetof(ft_tt_core_info_msg, core_magic_number), 240u,
"core_info.core_magic_number offset");
}
/* ------------------------------------------------------------------------- */
/* 1b. core_info encoding */
/* ------------------------------------------------------------------------- */
/**
* The sentinel is the whole test. Zero-filled tables are not "empty" to the firmware:
* an all-zero addressable_core reads as (0, 0) of type TENSIX, which is a real
* coordinate with a real type, and get_core_type() matches it.
*/
static void test_core_info_encoding(void)
{
const ft_tt_grid_info grid = p150a_grid();
ft_tt_launch_non_worker_core storage[FT_TT_LAUNCH_MAX_NON_WORKER_CORES];
ft_tt_launch_non_worker_core too_many[FT_TT_LAUNCH_MAX_NON_WORKER_CORES];
ft_tt_launch_core_info_config config;
ft_tt_core_info_msg message;
unsigned index;
unsigned dram_entries = 0;
bool every_unused_slot_sentinel = true;
printf("\ncore_info: the endpoint tables NoC sanitize resolves against\n");
check(ft_tt_launch_core_info_from_grid(&grid, 0u, 0u, 0x14000u, storage,
FT_TT_LAUNCH_MAX_NON_WORKER_CORES,
&config) == FT_TT_OK,
"a config builds from the p150a grid");
check_u64(config.virtual_non_worker_count, 24u,
"8 enabled DRAM banks x 3 NoC ports are registered");
check_u64(config.noc_size_x, 17u, "noc_size_x");
check_u64(config.noc_size_y, 12u, "noc_size_y");
check_u64(config.worker_grid_size_x, 14u, "worker_grid_size_x");
check_u64(config.noc_dram_addr_base, 0u, "noc_dram_addr_base");
check_u64(config.noc_dram_addr_end, 8uLL * 0xff000000uLL, "noc_dram_addr_end (8 banks)");
check(storage[0].x == 17u && storage[0].y == 12u,
"the first entry is DRAM bank 0 port 0 at TRANSLATED (17, 12)");
check(storage[0].type == FT_TT_ADDRESSABLE_CORE_DRAM, "typed DRAM, not TENSIX");
check(ft_tt_launch_encode_core_info(&config, &message, sizeof(message)) == FT_TT_OK,
"it encodes");
check_u64(message.core_magic_number, FT_TT_CORE_MAGIC_WORKER, "core_magic_number is WORKER");
for (index = 0; index < FT_TT_MAX_PHYSICAL_NON_WORKER_CORES; index++) {
if (message.non_worker_cores[index].x != FT_TT_CORE_COORD_INVALID ||
message.non_worker_cores[index].y != FT_TT_CORE_COORD_INVALID ||
message.non_worker_cores[index].type != FT_TT_ADDRESSABLE_CORE_UNKNOWN)
every_unused_slot_sentinel = false;
}
check(every_unused_slot_sentinel,
"every PHYSICAL slot is the 0xFF/UNKNOWN sentinel — Blackhole skips that table");
for (index = 0; index < FT_TT_MAX_VIRTUAL_NON_WORKER_CORES; index++) {
if (message.virtual_non_worker_cores[index].type == FT_TT_ADDRESSABLE_CORE_DRAM) {
dram_entries++;
} else if (message.virtual_non_worker_cores[index].x != FT_TT_CORE_COORD_INVALID ||
message.virtual_non_worker_cores[index].y != FT_TT_CORE_COORD_INVALID ||
message.virtual_non_worker_cores[index].type !=
FT_TT_ADDRESSABLE_CORE_UNKNOWN) {
every_unused_slot_sentinel = false;
}
}
check_u64(dram_entries, 24u, "24 DRAM ports in the virtual table");
check(every_unused_slot_sentinel,
"and the 5 unused virtual slots are the sentinel, NOT zero (which would read as "
"Tensix (0, 0))");
check(message.harvested_coords[0] == FT_TT_CORE_COORD_INVALID &&
message.virtual_harvested_coords[1] == FT_TT_CORE_COORD_INVALID,
"an unharvested part gets the sentinel in both harvested arrays");
/* Refusals. */
check(ft_tt_launch_encode_core_info(&config, &message, sizeof(message) - 1u) == FT_TT_EINVAL,
"a short buffer is refused");
check(ft_tt_launch_encode_core_info(NULL, &message, sizeof(message)) == FT_TT_EINVAL,
"a NULL config is refused");
for (index = 0; index < FT_TT_LAUNCH_MAX_NON_WORKER_CORES; index++) {
too_many[index].x = 1u;
too_many[index].y = 2u;
too_many[index].type = FT_TT_ADDRESSABLE_CORE_DRAM;
}
config.virtual_non_worker_cores = too_many;
config.virtual_non_worker_count = FT_TT_MAX_VIRTUAL_NON_WORKER_CORES + 1u;
check(ft_tt_launch_encode_core_info(&config, &message, sizeof(message)) == FT_TT_EINVAL,
"more entries than the ABI's 29 virtual slots is refused");
config.virtual_non_worker_count = 1u;
too_many[0].x = FT_TT_CORE_COORD_INVALID;
check(ft_tt_launch_encode_core_info(&config, &message, sizeof(message)) == FT_TT_EINVAL,
"a coordinate that collides with the 0xFF sentinel is refused");
check(ft_tt_launch_core_info_from_grid(&grid, 0u, 0u, 0x14000u, storage, 4u, &config) ==
FT_TT_ENOMEM,
"too little storage for the DRAM ports is refused, not truncated");
}
/* ------------------------------------------------------------------------- */
/* 1b2. Watcher priming */
/* ------------------------------------------------------------------------- */
/**
* The firmware writes a sanitize report only into a slot that already reads
* DebugSanitizeOK. A cleared mailbox reads 0, so an unprimed core stops on a violation
* and records nothing — which is exactly what happened on the card before this existed.
*/
static void test_watcher_priming(void)
{
ft_tt_noc_access noc = simulated_access();
ft_tt_debug_sanitize_addr_msg report;
ft_tt_watcher_msg watcher;
uint32_t enable = 0;
printf("\nwatcher priming: DebugSanitizeOK is 2, and a cleared slot reads 0\n");
reset_simulated_core();
check(ft_tt_launch_clear_mailbox(&noc, 1u, 2u) == FT_TT_OK, "the mailbox is cleared");
check(noc.read(noc.user, 1u, 2u, FT_TT_MB_WATCHER_SANITIZE_ADDR(0), &report,
sizeof(report)) == FT_TT_OK,
"slot 0 is readable");
check_u64(report.return_code, 0u, "and a CLEARED slot reads 0, not DebugSanitizeOK");
check(ft_tt_launch_prime_watcher(&noc, 1u, 2u) == FT_TT_OK, "priming succeeds");
check(noc.read(noc.user, 1u, 2u, FT_TT_MB_ADDR(watcher), &watcher, sizeof(watcher)) ==
FT_TT_OK,
"the whole watcher block is readable");
check_u64(watcher.sanitize[0].return_code, FT_TT_SANITIZE_OK, "slot 0 now reads OK");
check_u64(watcher.sanitize[1].return_code, FT_TT_SANITIZE_OK, "slot 1 now reads OK");
check(noc.read(noc.user, 1u, 2u, FT_TT_MB_ADDR(watcher.enable), &enable, sizeof(enable)) ==
FT_TT_OK,
"enable is readable");
check_u64(enable, FT_TT_WATCHER_ENABLED, "and watcher.enable says enabled");
/* return_code shares its 32-bit word with which_risc; priming must not take it. */
check_u64(watcher.sanitize[0].which_risc, 0u, "which_risc, its word neighbour, is untouched");
check_u64(watcher.sanitize[0].noc_addr, 0u, "and so is the rest of the slot");
check(ft_tt_launch_prime_watcher(NULL, 1u, 2u) == FT_TT_EINVAL, "a NULL accessor is refused");
}
/* ------------------------------------------------------------------------- */
/* 1c. The Phase 2 test L1 map, derived rather than chosen */
/* ------------------------------------------------------------------------- */
static void test_phase2_layout_derivation(void)
{
ft_tt_image_placement placements[FT_TT_PROC_COUNT];
ft_tt_phase2_l1_layout layout;
unsigned processor;
printf("\nPhase 2 L1 map: derived from the firmware placements\n");
memset(placements, 0, sizeof(placements));
check(ft_tt_phase2_l1_layout_derive(NULL, 0u, &layout) == FT_TT_OK,
"with no placements at all it still derives");
check_u64(layout.kernel_config_base,
(FT_TT_PHASE2_RESERVED_L1_END + FT_TT_PHASE2_LAYOUT_ALIGNMENT - 1u) &
~(uint32_t)(FT_TT_PHASE2_LAYOUT_ALIGNMENT - 1u),
"and the base is the end of the reserved regions, not FT_TT_L1_USABLE_BASE");
check(layout.kernel_config_base > FT_TT_L1_USABLE_BASE,
"which is ABOVE the allocator floor — the .data staging and bank tables live "
"between them");
/* A firmware image that stops below the reserved end must not lower the floor. */
placements[FT_TT_PROC_DM0].bytes_written = 256u;
placements[FT_TT_PROC_DM0].lowest_l1_address = FT_TT_L1_USABLE_BASE;
placements[FT_TT_PROC_DM0].highest_l1_address = FT_TT_L1_USABLE_BASE + 256u;
check(ft_tt_phase2_l1_layout_derive(placements, FT_TT_PROC_COUNT, &layout) == FT_TT_OK,
"a small firmware image derives");
check(layout.kernel_config_base >= FT_TT_PHASE2_RESERVED_L1_END,
"and does NOT pull the base below the reserved end");
/* One that reaches past it must raise the floor above itself. */
placements[FT_TT_PROC_DM0].highest_l1_address = FT_TT_PHASE2_RESERVED_L1_END + 0x1000u + 4u;
check(ft_tt_phase2_l1_layout_derive(placements, FT_TT_PROC_COUNT, &layout) == FT_TT_OK,
"a firmware image reaching past the reserved end derives");
check(layout.kernel_config_base >= placements[FT_TT_PROC_DM0].highest_l1_address,
"and the base moves above it");
check((layout.kernel_config_base % FT_TT_PHASE2_LAYOUT_ALIGNMENT) == 0u,
"the base is 64-byte aligned — the DRAM alignment, not L1's 16, because a CB is "
"the local end of a DRAM read");
check(FT_TT_PHASE2_LAYOUT_ALIGNMENT == 64u,
"and 64 is what that alignment is on this part");
check((FT_TT_PHASE2_CB_IN0_OFFSET % FT_TT_PHASE2_LAYOUT_ALIGNMENT) == 0u &&
(FT_TT_PHASE2_RTA_OFFSET % FT_TT_PHASE2_LAYOUT_ALIGNMENT) == 0u &&
(FT_TT_PHASE2_DESTINATION_OFFSET % FT_TT_PHASE2_LAYOUT_ALIGNMENT) == 0u,
"every offset is a multiple of it, so aligning the base aligns the whole map");
check_u64(layout.cb_config_address, layout.kernel_config_base, "cb_config is offset 0");
check(layout.destination_address - layout.kernel_config_base ==
FT_TT_PHASE2_DESTINATION_OFFSET,
"and every other field is a fixed offset from the base");
check(layout.cb_config_address - layout.kernel_config_base <= 0xFFFFu &&
ft_tt_phase2_runtime_args_address(&layout, FT_TT_PROC_COUNT - 1u) -
layout.kernel_config_base <= 0xFFFFu,
"the CB and runtime-arg offsets still fit their 16-bit wire fields");
for (processor = 0; processor < FT_TT_PROC_COUNT; processor++) {
check(ft_tt_phase2_runtime_args_address(&layout, processor) ==
layout.runtime_args_base + processor * FT_TT_PHASE2_RTA_STRIDE,
"runtime-arg block address is base + processor * stride");
}
check(ft_tt_phase2_runtime_args_address(&layout, FT_TT_PROC_COUNT) == 0u,
"and an out-of-range processor gets 0, the encoder's no-args value");
/* A base that cannot hold the map is refused rather than wrapping. */
placements[FT_TT_PROC_DM0].highest_l1_address = FT_TT_MEM_L1_SIZE - 16u;
check(ft_tt_phase2_l1_layout_derive(placements, FT_TT_PROC_COUNT, &layout) == FT_TT_EINVAL,
"a base with no room left for the map is refused");
}
/* ------------------------------------------------------------------------- */
/* 2. ELF parsing */
/* ------------------------------------------------------------------------- */
static void test_elf_parses_a_kernel(void)
{
synthetic_elf elf;
ft_tt_elf_image image;
uint32_t low = 0, high = 0;
printf("\nELF parsing: a well-formed kernel image\n");
build_kernel_elf(&elf, 0x7000u, 512u);
check(ft_tt_elf_parse(elf.bytes, elf.size, &image) == FT_TT_OK, "parses");
check_u64(image.entry_address, 0x7000u, "entry address");
check_u64(image.segment_count, 2u, "loadable segment count");
check_u64(image.segments[0].exec_address, 0x7000u, "text execution address");
check_u64(image.segments[0].load_address, 0x7000u, "text load address (VMA == LMA)");
check_u64(image.segments[0].file_bytes, 512u, "text file bytes");
check_u64(image.segments[1].exec_address, FT_TT_MEM_LOCAL_BASE + 0x400u,
"data executes from local memory");
check_u64(image.segments[1].load_address, 0x7000u + 512u, "data is staged in L1 after text");
check(ft_tt_elf_text_segment(&image) == &image.segments[0], "text segment accessor");
check(ft_tt_elf_load_extent(&image, &low, &high) == FT_TT_OK, "load extent computes");
check_u64(low, 0x7000u, "load extent low");
check_u64(high, 0x7000u + 512u + 64u, "load extent high");
check_u64(image.loaded_bytes, 512u + 64u, "bytes a load would transfer");
}
static void test_elf_rejections(void)
{
synthetic_elf elf;
ft_tt_elf_image image;
synthetic_elf_spec spec;
printf("\nELF parsing: malformed images are refused\n");
build_kernel_elf(&elf, 0x7000u, 512u);
elf.bytes[1] = 'X';
check(ft_tt_elf_parse(elf.bytes, elf.size, &image) == FT_TT_EINVAL, "bad magic");
build_kernel_elf(&elf, 0x7000u, 512u);
elf.bytes[4] = 2u;
check(ft_tt_elf_parse(elf.bytes, elf.size, &image) == FT_TT_EINVAL, "ELF64");
build_kernel_elf(&elf, 0x7000u, 512u);
elf.bytes[5] = 2u;
check(ft_tt_elf_parse(elf.bytes, elf.size, &image) == FT_TT_EINVAL, "big-endian");
memset(&spec, 0, sizeof(spec));
build_kernel_elf(&elf, 0x7000u, 512u);
put_u16(elf.bytes, 16u, 1u); /* ET_REL */
check(ft_tt_elf_parse(elf.bytes, elf.size, &image) == FT_TT_EINVAL, "not ET_EXEC");
build_kernel_elf(&elf, 0x7000u, 512u);
put_u16(elf.bytes, 18u, 62u); /* EM_X86_64 */
check(ft_tt_elf_parse(elf.bytes, elf.size, &image) == FT_TT_EINVAL, "not EM_RISCV");
build_kernel_elf(&elf, 0x7000u, 512u);
put_u32(elf.bytes, 24u, 0x7004u);
check(ft_tt_elf_parse(elf.bytes, elf.size, &image) == FT_TT_EINVAL,
"entry is not the start of the first segment");
build_kernel_elf(&elf, 0x7002u, 512u); /* misaligned vaddr */
check(ft_tt_elf_parse(elf.bytes, elf.size, &image) == FT_TT_EINVAL, "misaligned segment");
build_kernel_elf(&elf, 0x7000u, 512u);
put_u32(elf.bytes, ELF_PHDR_OFFSET + 16u, 510u); /* p_filesz not a word multiple */
check(ft_tt_elf_parse(elf.bytes, elf.size, &image) == FT_TT_EINVAL,
"file size is not a multiple of 4");
build_kernel_elf(&elf, 0x7000u, 512u);
put_u32(elf.bytes, ELF_PHDR_OFFSET + 16u, 0x10000u); /* p_filesz past the blob */
check(ft_tt_elf_parse(elf.bytes, elf.size, &image) == FT_TT_EINVAL, "contents past end of blob");
build_kernel_elf(&elf, 0x7000u, 512u);
put_u32(elf.bytes, 28u, 0u); /* no program headers */
check(ft_tt_elf_parse(elf.bytes, elf.size, &image) == FT_TT_EINVAL, "no program headers");
check(ft_tt_elf_parse(NULL, 0u, &image) == FT_TT_EINVAL, "NULL blob");
check(ft_tt_elf_parse(elf.bytes, 8u, &image) == FT_TT_EINVAL, "blob shorter than a header");
}
static void test_elf_segment_limits(void)
{
synthetic_elf elf;
ft_tt_elf_image image;
synthetic_elf_spec spec;
printf("\nELF parsing: the linker's region limits are enforced\n");
memset(&spec, 0, sizeof(spec));
spec.entry = 0x7000u;
spec.text_vaddr = 0x7000u;
spec.text_bytes = 512u;
spec.text_limit = 256u; /* the kernel is twice its region */
spec.with_segment_note = 1;
spec.elf_type = 2u;
spec.machine = 243u;
build_synthetic_elf(&spec, &elf);
check(ft_tt_elf_parse(elf.bytes, elf.size, &image) == FT_TT_EINVAL,
"text over its region limit is refused");
spec.text_limit = 512u; /* exactly its region */
build_synthetic_elf(&spec, &elf);
check(ft_tt_elf_parse(elf.bytes, elf.size, &image) == FT_TT_OK,
"text exactly at its region limit is accepted");
spec.with_segment_note = 0; /* no note: nothing to check against */
build_synthetic_elf(&spec, &elf);
check(ft_tt_elf_parse(elf.bytes, elf.size, &image) == FT_TT_OK,
"an image with no .segments note still parses");
}
/* ------------------------------------------------------------------------- */
/* 3. Embedded blob format */
/* ------------------------------------------------------------------------- */
static void test_kernel_blob_format(void)
{
static const unsigned char fake_trisc0[] = {1, 2, 3, 4};
static const unsigned char fake_trisc1[] = {5, 6, 7, 8, 9};
static const ft_tt_kernel_image images[] = {
{"vector_add_compute.c", FT_TT_PROC_MATH0, fake_trisc0, sizeof(fake_trisc0)},
{"vector_add_compute.c", FT_TT_PROC_MATH1, fake_trisc1, sizeof(fake_trisc1)},
};
static const ft_tt_kernel_program program = {"vector_add", "7.68.0", "0,1,16", images, 2u};
printf("\nembedded blob format\n");
check(ft_tt_kernel_image_for_processor(&program, FT_TT_PROC_MATH0) == &images[0],
"lookup finds MATH0");
check(ft_tt_kernel_image_for_processor(&program, FT_TT_PROC_MATH1) == &images[1],
"lookup finds MATH1");
check(ft_tt_kernel_image_for_processor(&program, FT_TT_PROC_MATH2) == NULL,
"lookup returns NULL for an absent processor");
check(ft_tt_kernel_image_for_processor(NULL, FT_TT_PROC_MATH0) == NULL, "lookup tolerates NULL");
check_u64(ft_tt_kernel_program_enables(&program),
(1u << FT_TT_PROC_MATH0) | (1u << FT_TT_PROC_MATH1), "derived enables mask");
check_u64(ft_tt_kernel_program_enables(NULL), 0u, "enables of NULL is 0");
check_u64(images[1].elf_size, 5u, "blob size travels with the blob");
}
/* ------------------------------------------------------------------------- */
/* 4. Segment placement into L1 */
/* ------------------------------------------------------------------------- */
static void test_kernel_image_placement(void)
{
ft_tt_noc_access noc = simulated_access();
ft_tt_image_placement placement;
synthetic_elf elf;
const uint32_t text_address = FT_TT_MEM_TRISC0_FIRMWARE_BASE + 0x400u;
printf("\nplacement: a kernel image lands at its ELF addresses\n");
reset_simulated_core();
build_kernel_elf(&elf, text_address, 512u);
check(ft_tt_launch_load_image(&noc, 1, 2, FT_TT_PROC_MATH0, FT_TT_IMAGE_KERNEL, elf.bytes,
elf.size, &placement) == FT_TT_OK,
"kernel image loads");
check_u64(placement.entry_address, text_address, "placement reports the entry point");
check_u64(placement.bytes_written, 512u + 64u, "text and data were both written");
check_u64(simulated_core.l1[text_address], 0xA0u, "text landed at its execution address");
check_u64(simulated_core.l1[text_address + 512u], 0xD0u,
"kernel data landed at its ELF load address");
check(simulated_core.l1[ft_tt_processor_local_init_scratch(FT_TT_PROC_MATH0)] == 0u,
"kernel data did NOT go to the firmware init scratch");
}
static void test_firmware_image_placement(void)
{
ft_tt_noc_access noc = simulated_access();
ft_tt_image_placement placement;
synthetic_elf elf;
const uint32_t firmware_base = ft_tt_processor_firmware_base(FT_TT_PROC_MATH0);
const uint32_t scratch = ft_tt_processor_local_init_scratch(FT_TT_PROC_MATH0);
printf("\nplacement: firmware data is relocated to the init scratch\n");
reset_simulated_core();
build_firmware_elf(&elf, FT_TT_PROC_MATH0, 256u);
check(ft_tt_launch_load_image(&noc, 1, 2, FT_TT_PROC_MATH0, FT_TT_IMAGE_FIRMWARE, elf.bytes,
elf.size, &placement) == FT_TT_OK,
"firmware image loads");
check_u64(simulated_core.l1[firmware_base], 0xA0u, "text landed at the firmware base");
/* data_vaddr was MEM_LOCAL_BASE + 0x100, so it must land at scratch + 0x100 —
* NOT at the ELF's own load address, which the firmware never reads. */
check_u64(simulated_core.l1[scratch + 0x100u], 0xD0u,
"firmware data landed at init scratch + its local-memory offset");
check(simulated_core.l1[firmware_base + 256u] == 0u,
"firmware data did NOT go to its ELF load address");
}
static void test_placement_refusals(void)
{
ft_tt_noc_access noc = simulated_access();
synthetic_elf elf;
printf("\nplacement: bad placements are refused before any byte is written\n");
reset_simulated_core();
build_firmware_elf(&elf, FT_TT_PROC_MATH1, 256u);
check(ft_tt_launch_load_image(&noc, 1, 2, FT_TT_PROC_MATH0, FT_TT_IMAGE_FIRMWARE, elf.bytes,
elf.size, NULL) == FT_TT_EINVAL,
"firmware for the wrong processor is refused");
build_firmware_elf(&elf, FT_TT_PROC_MATH0, 4096u); /* hole is 2560 bytes */
check(ft_tt_launch_load_image(&noc, 1, 2, FT_TT_PROC_MATH0, FT_TT_IMAGE_FIRMWARE, elf.bytes,
elf.size, NULL) == FT_TT_EINVAL,
"firmware larger than its hole is refused");
build_kernel_elf(&elf, FT_TT_MEM_TRISC0_FIRMWARE_BASE - 0x100u, 256u);
check(ft_tt_launch_load_image(&noc, 1, 2, FT_TT_PROC_MATH0, FT_TT_IMAGE_KERNEL, elf.bytes,
elf.size, NULL) == FT_TT_EINVAL,
"kernel text below its firmware is refused");
build_kernel_elf(&elf, FT_TT_MEM_L1_SIZE - 128u, 256u);
check(ft_tt_launch_load_image(&noc, 1, 2, FT_TT_PROC_MATH0, FT_TT_IMAGE_KERNEL, elf.bytes,
elf.size, NULL) == FT_TT_EINVAL,
"a segment running off the end of L1 is refused");
/* Our kernels are not execute-in-place, so their text sits in the TAIL of their own
* processor's firmware hole and the linker's only bound is MEM_*_KERNEL_SIZE — all
* of L1. One byte past the hole is TRISC1's firmware. The measured margin on the
* real vector_add build is 68 bytes (docs/tt_bringup_log.md, step 6). */
build_kernel_elf(&elf, FT_TT_MEM_TRISC0_FIRMWARE_BASE + 0x900u, 512u);
check(ft_tt_launch_load_image(&noc, 1, 2, FT_TT_PROC_MATH0, FT_TT_IMAGE_KERNEL, elf.bytes,
elf.size, NULL) == FT_TT_EINVAL,
"a kernel that overruns its firmware hole into the next processor is refused");
check(simulated_core.write_calls == 0u, "no write reached the device on any refusal");
check(ft_tt_launch_load_image(&noc, 1, 2, 99u, FT_TT_IMAGE_KERNEL, elf.bytes, elf.size, NULL) ==
FT_TT_EINVAL,
"an out-of-range processor index is refused");
}
/**
* The OTHER half of the merged placement rule: a RELOCATE_KERNEL_TEXT=1 image, linked
* into this processor's slab of the KERNEL_CONFIG region rather than the tail of its
* firmware hole. Same entry point, same kind, no flag — the ELF's text address is what
* selects the region.
*/
static void test_relocated_kernel_placement(void)
{
ft_tt_noc_access noc = simulated_access();
ft_tt_image_placement placement;
synthetic_elf elf;
const uint32_t slab = ft_tt_op_text_base(FT_TT_PROC_MATH0);
printf("\nplacement: a relocated kernel lands in its kernel-config slab\n");
reset_simulated_core();
check(slab >= FT_TT_OP_KERNEL_CONFIG_BASE, "MATH0's slab is inside the kernel-config region");
build_kernel_elf(&elf, slab, 512u);
check(ft_tt_launch_load_image(&noc, 1, 2, FT_TT_PROC_MATH0, FT_TT_IMAGE_KERNEL, elf.bytes,
elf.size, &placement) == FT_TT_OK,
"a relocated kernel loads through the SAME entry point as an in-hole one");
check_u64(placement.entry_address, slab, "placement reports the slab base as the entry");
check_u64(simulated_core.l1[slab], 0xA0u, "text landed at the slab base");
check_u64(simulated_core.l1[slab + 512u], 0xD0u, "its data image followed the text");
}
/**
* "Never straddling", proven from both sides of both regions, plus the neighbour's slab.
*
* These are the cases the merged rule exists for. Each one is a placement that ONE of the
* two old rules would have let through — an image in the slab was refused outright by
* ft_tt_launch.c and accepted blind by ft_tt_op_dispatch.c, and vice versa — so a check
* that only asserts "a good image loads" cannot see the merge at all.
*/
static void test_merged_region_refusals(void)
{
ft_tt_noc_access noc = simulated_access();
const uint32_t slab = ft_tt_op_text_base(FT_TT_PROC_MATH0);
const uint32_t slab_end = slab + FT_TT_OP_TEXT_SLAB_BYTES;
const uint32_t hole = FT_TT_MEM_TRISC0_FIRMWARE_BASE;
synthetic_elf_spec spec;
synthetic_elf elf;
printf("\nplacement: a kernel is wholly in ONE region or it is refused\n");
reset_simulated_core();
/* Between the two regions: above the firmware hole, below the slab. This is the
* address a default-linked kernel reaches by being too big, and it is where the next
* processor's firmware lives. */
build_kernel_elf(&elf, FT_TT_MEM_MAP_END, 256u);
check(ft_tt_launch_load_image(&noc, 1, 2, FT_TT_PROC_MATH0, FT_TT_IMAGE_KERNEL, elf.bytes,
elf.size, NULL) == FT_TT_EINVAL,
"text between the two regions is refused");
/* A neighbour's slab is not this processor's. Loading MATH0's image as MATH1 is the
* cheapest way to make that mistake and it must not load. */
build_kernel_elf(&elf, slab, 256u);
check(ft_tt_launch_load_image(&noc, 1, 2, FT_TT_PROC_MATH1, FT_TT_IMAGE_KERNEL, elf.bytes,
elf.size, NULL) == FT_TT_EINVAL,
"an image in ANOTHER processor's slab is refused");
/* Inside its own slab but not AT the base. Either the ELF was not built with
* RELOCATE_KERNEL_TEXT, or this map and build_tensix_program.sh's have drifted apart:
* it would load fine and leave the launch message pointing at empty L1. */
build_kernel_elf(&elf, slab + 0x400u, 256u);
check(ft_tt_launch_load_image(&noc, 1, 2, FT_TT_PROC_MATH0, FT_TT_IMAGE_KERNEL, elf.bytes,
elf.size, NULL) == FT_TT_EINVAL,
"an image inside its slab but not at the slab BASE is refused");
/* Straddling the top of the slab: text at the slab base, .data image running off the
* end into the next slab. Refusing this is what stops one op overwriting another. */
memset(&spec, 0, sizeof(spec));
spec.entry = slab;
spec.text_vaddr = slab;
spec.text_bytes = 256u;
spec.text_limit = FT_TT_MEM_MAX_KERNEL_SIZE;
spec.with_data = 1;
spec.data_vaddr = FT_TT_MEM_LOCAL_BASE + 0x400u;
spec.data_lma = slab_end - 32u;
spec.data_bytes = 64u;
spec.data_limit = FT_TT_MEM_TRISC_LOCAL_SIZE;
spec.with_segment_note = 1;
spec.elf_type = 2u;
spec.machine = 243u;
build_synthetic_elf(&spec, &elf);
check(ft_tt_launch_load_image(&noc, 1, 2, FT_TT_PROC_MATH0, FT_TT_IMAGE_KERNEL, elf.bytes,
elf.size, NULL) == FT_TT_EINVAL,
"a segment straddling the TOP of the slab is refused");
/* ...and the same image ending exactly AT the boundary is legal, so the refusal above
* is a bound and not an off-by-one that refuses everything near the edge. */
spec.data_lma = slab_end - 64u;
build_synthetic_elf(&spec, &elf);
check(ft_tt_launch_load_image(&noc, 1, 2, FT_TT_PROC_MATH0, FT_TT_IMAGE_KERNEL, elf.bytes,
elf.size, NULL) == FT_TT_OK,
"a segment ending exactly at the slab end is accepted");
/* Straddling the BOTTOM of the firmware hole. This bound is new: the old in-hole rule
* checked only that a segment ended before the hole did, so a .data image staged
* BELOW the hole — on this processor's own firmware, or on the mailbox under it —
* was accepted. */
reset_simulated_core();
spec.entry = hole + 0x400u;
spec.text_vaddr = hole + 0x400u;
spec.data_lma = hole - 64u;
build_synthetic_elf(&spec, &elf);
check(ft_tt_launch_load_image(&noc, 1, 2, FT_TT_PROC_MATH0, FT_TT_IMAGE_KERNEL, elf.bytes,
elf.size, NULL) == FT_TT_EINVAL,
"a segment straddling the BOTTOM of the firmware hole is refused");
/* Text exactly at the firmware base IS the firmware, not a kernel. */
build_kernel_elf(&elf, hole, 256u);
check(ft_tt_launch_load_image(&noc, 1, 2, FT_TT_PROC_MATH0, FT_TT_IMAGE_KERNEL, elf.bytes,
elf.size, NULL) == FT_TT_EINVAL,
"kernel text exactly at the firmware base is refused");
check(simulated_core.write_calls == 0u,
"no write reached the device on any of these refusals");
}
/* ------------------------------------------------------------------------- */
/* 5. Launch-message encoding */
/* ------------------------------------------------------------------------- */
static ft_tt_launch_config baseline_config(void)
{
ft_tt_launch_config config;
memset(&config, 0, sizeof(config));
config.kernel_config_base = 0x10000u;
config.enables = (1u << FT_TT_PROC_MATH0) | (1u << FT_TT_PROC_MATH1) | (1u << FT_TT_PROC_MATH2);
config.kernel_text_address[FT_TT_PROC_MATH0] = 0x6A00u;
config.kernel_text_address[FT_TT_PROC_MATH1] = 0x7400u;
config.kernel_text_address[FT_TT_PROC_MATH2] = 0x7E00u;
config.kernel_text_bytes[FT_TT_PROC_MATH0] = 1228u;
config.local_cb_config_address = 0x10100u;
config.local_cb_mask = 0x10003u;
config.runtime_args_address[FT_TT_PROC_MATH0] = 0x10800u;
config.brisc_noc_id = 0u;
return config;
}
static void test_launch_message_encoding(void)
{
ft_tt_launch_config config = baseline_config();
ft_tt_kernel_config_msg message;
printf("\nlaunch message: encoding\n");
check(ft_tt_launch_encode_launch_message(&config, &message, sizeof(message)) == FT_TT_OK,
"encodes");
check_u64(message.kernel_config_base[FT_TT_PROGRAMMABLE_CORE_TYPE_TENSIX], 0x10000u,
"kernel_config_base[TENSIX]");
check_u64(message.mode, FT_TT_DISPATCH_MODE_HOST, "dispatch mode is HOST");
check_u64(message.enables, config.enables, "enables passes through");
check_u64(message.local_cb_offset, 0x100u, "local CB offset is base-relative");
check_u64(message.local_cb_mask, 0x10003u, "local CB mask passes through");
check_u64(message.min_remote_cb_start_index, FT_TT_NUM_CIRCULAR_BUFFERS,
"remote CBs are switched off");
check_u64(message.rta_offset[FT_TT_PROC_MATH0].rta_offset, 0x800u,
"runtime-arg offset is base-relative");
check_u64(message.rta_offset[FT_TT_PROC_MATH1].rta_offset, FT_TT_RTA_NO_ARGS_SENTINEL,
"a processor with no args gets the sentinel, not 0");
check_u64(message.rta_offset[FT_TT_PROC_MATH0].crta_offset, FT_TT_RTA_NO_ARGS_SENTINEL,
"common runtime args are always the sentinel");
check_u64(message.kernel_text_size[FT_TT_PROC_MATH0], 1228u, "kernel text size");
check_u64(message.preload, 0u, "preload stays 0 for host dispatch");
/* The whole point of the wraparound encoding: base + offset must reconstruct the
* text address in uint32 arithmetic, exactly as the TRISC firmware computes it. */
printf("\nlaunch message: the non-XIP text-offset encoding round-trips\n");
check_u64((uint32_t)(message.kernel_config_base[FT_TT_PROGRAMMABLE_CORE_TYPE_TENSIX] +
message.kernel_text_offset[FT_TT_PROC_MATH0]),
0x6A00u, "MATH0: base + offset reconstructs the entry point");
check_u64((uint32_t)(message.kernel_config_base[FT_TT_PROGRAMMABLE_CORE_TYPE_TENSIX] +
message.kernel_text_offset[FT_TT_PROC_MATH2]),
0x7E00u, "MATH2: base + offset reconstructs the entry point");
check(message.kernel_text_offset[FT_TT_PROC_MATH0] > 0x80000000u,
"the offset really is a wrapped negative, not a coincidence");
}
static void test_launch_message_refusals(void)
{
ft_tt_launch_config config;
ft_tt_kernel_config_msg message;
printf("\nlaunch message: bad configurations are refused\n");
config = baseline_config();
config.kernel_config_base = 0x10008u;
check(ft_tt_launch_encode_launch_message(&config, &message, sizeof(message)) == FT_TT_EINVAL,
"a base that is not 16-byte aligned");
config = baseline_config();
config.enables = 0u;
check(ft_tt_launch_encode_launch_message(&config, &message, sizeof(message)) == FT_TT_EINVAL,
"enables == 0, which would run nothing");
config = baseline_config();
config.kernel_text_address[FT_TT_PROC_MATH1] = 0u;
check(ft_tt_launch_encode_launch_message(&config, &message, sizeof(message)) == FT_TT_EINVAL,
"a processor enabled with no text address");
config = baseline_config();
config.runtime_args_address[FT_TT_PROC_MATH0] = config.kernel_config_base + 0xFFFFu;
check(ft_tt_launch_encode_launch_message(&config, &message, sizeof(message)) == FT_TT_EINVAL,
"runtime args beyond the 16-bit offset field");
config = baseline_config();
config.runtime_args_address[FT_TT_PROC_MATH0] = config.kernel_config_base - 16u;
check(ft_tt_launch_encode_launch_message(&config, &message, sizeof(message)) == FT_TT_EINVAL,
"runtime args below the config base");
config = baseline_config();
check(ft_tt_launch_encode_launch_message(&config, &message, sizeof(message) - 1u) ==
FT_TT_EINVAL,
"a buffer too small for the message");
check(ft_tt_launch_encode_launch_message(NULL, &message, sizeof(message)) == FT_TT_EINVAL,
"a NULL config");
}
/* ------------------------------------------------------------------------- */
/* 6. CB config and runtime args */
/* ------------------------------------------------------------------------- */
static void test_cb_config_encoding(void)
{
const ft_tt_launch_cb_entry entries[] = {
{0u, 0x20000u, 4096u, 2048u, 2u},
{1u, 0x21000u, 4096u, 2048u, 2u},
{16u, 0x22000u, 4096u, 2048u, 2u},
};
uint32_t buffer[FT_TT_NUM_CIRCULAR_BUFFERS * FT_TT_WORDS_PER_LOCAL_CB_CONFIG];
ft_tt_launch_cb_entry bad;
uint64_t mask = 0;
printf("\ncircular-buffer config encoding\n");
memset(buffer, 0xEE, sizeof(buffer));
check(ft_tt_launch_encode_cb_config(entries, 3u, buffer, sizeof(buffer), &mask) == FT_TT_OK,
"encodes");
check_u64(mask, (1u << 0) | (1u << 1) | (1u << 16), "mask has a bit per configured CB");
check_u64(buffer[0 * 4 + 0], 0x20000u, "CB0 word 0 is the address");
check_u64(buffer[0 * 4 + 1], 4096u, "CB0 word 1 is the size in bytes");
check_u64(buffer[0 * 4 + 2], 2u, "CB0 word 2 is the page count");
check_u64(buffer[0 * 4 + 3], 2048u, "CB0 word 3 is the page size in bytes");
check_u64(buffer[16 * 4 + 0], 0x22000u, "CB16 is written at its own stride");
check_u64(buffer[2 * 4 + 0], 0u, "an unconfigured CB is zeroed, not left as garbage");
bad = entries[0];
bad.index = FT_TT_NUM_CIRCULAR_BUFFERS;
check(ft_tt_launch_encode_cb_config(&bad, 1u, buffer, sizeof(buffer), &mask) == FT_TT_EINVAL,
"a CB index past the end is refused");
check(ft_tt_launch_encode_cb_config(entries, 3u, buffer, 16u, &mask) == FT_TT_EINVAL,
"a short buffer is refused");
}
static void test_runtime_argument_planting(void)
{
ft_tt_noc_access noc = simulated_access();
const uint32_t args[] = {7u, 0xDEADBEEFu, 3u};
const uint32_t address = 0x10800u;
printf("\nruntime arguments\n");
reset_simulated_core();
check(ft_tt_launch_plant_runtime_args(&noc, 1, 2, address, args, 3u) == FT_TT_OK, "plants");
check_u64(simulated_l1_u32(address), 7u, "arg 0");
check_u64(simulated_l1_u32(address + 4u), 0xDEADBEEFu, "arg 1");
check_u64(simulated_l1_u32(address + 8u), 3u, "arg 2");
check_u64(simulated_l1_u32(address + 12u), 0u, "nothing was written past the last arg");
check(ft_tt_launch_plant_runtime_args(&noc, 1, 2, address + 2u, args, 3u) == FT_TT_EINVAL,
"a misaligned address is refused");
check(ft_tt_launch_plant_runtime_args(&noc, 1, 2, address, args, 0u) == FT_TT_EINVAL,
"an empty arg list is refused");
check(ft_tt_launch_plant_runtime_args(&noc, 1, 2, address, NULL, 3u) == FT_TT_EINVAL,
"a NULL arg list is refused");
}
/* ------------------------------------------------------------------------- */
/* 7. Reset control */
/* ------------------------------------------------------------------------- */
static void test_reset_control(void)
{
ft_tt_noc_access noc = simulated_access();
uint32_t value = 0;
printf("\nreset control (read-modify-write at 0xFFB121B0)\n");
reset_simulated_core();
simulated_core.soft_reset = 0u;
check(ft_tt_launch_assert_reset(&noc, 1, 2, FT_TT_SOFT_RESET_ALL_RISCS) == FT_TT_OK,
"assert all");
check_u64(simulated_core.soft_reset, FT_TT_SOFT_RESET_ALL_RISCS, "all five bits are set");
check(ft_tt_launch_deassert_reset(&noc, 1, 2, FT_TT_SOFT_RESET_BRISC, false) == FT_TT_OK,
"deassert BRISC only");
check_u64(simulated_core.soft_reset,
FT_TT_SOFT_RESET_ALL_RISCS & ~(uint32_t)FT_TT_SOFT_RESET_BRISC,
"BRISC released, the other four still held");
check(ft_tt_launch_read_reset(&noc, 1, 2, &value) == FT_TT_OK, "read back");
check_u64(value, simulated_core.soft_reset, "read matches");
/* Unrelated bits in the register must survive a read-modify-write. */
simulated_core.soft_reset = 0x00000055u;
check(ft_tt_launch_assert_reset(&noc, 1, 2, FT_TT_SOFT_RESET_TRISC1) == FT_TT_OK,
"assert one bit with foreign bits set");
check_u64(simulated_core.soft_reset, 0x00000055u | FT_TT_SOFT_RESET_TRISC1,
"foreign bits survived");
simulated_core.soft_reset = FT_TT_SOFT_RESET_ALL_RISCS;
check(ft_tt_launch_deassert_reset(&noc, 1, 2, FT_TT_SOFT_RESET_BRISC, true) == FT_TT_OK,
"deassert with staggered start");
check((simulated_core.soft_reset & FT_TT_SOFT_RESET_STAGGERED_START) != 0u,
"staggered-start bit 31 was set");
}
/* ------------------------------------------------------------------------- */
/* 8. Mailbox protocol */
/* ------------------------------------------------------------------------- */
static void test_mailbox_clear(void)
{
ft_tt_noc_access noc = simulated_access();
printf("\nmailbox: clearing\n");
reset_simulated_core();
memset(simulated_core.l1, 0xAB, sizeof(simulated_core.l1));
check(ft_tt_launch_clear_mailbox(&noc, 1, 2) == FT_TT_OK, "clears");
check_u64(simulated_core.l1[FT_TT_MEM_MAILBOX_BASE], 0u, "first byte of the region");
check_u64(simulated_core.l1[FT_TT_MEM_MAILBOX_END - 1u], 0u, "last byte of the region");
check_u64(simulated_core.l1[FT_TT_MEM_MAILBOX_BASE - 1u], 0xABu,
"the byte below the region was not touched");
check_u64(simulated_core.l1[FT_TT_MEM_MAILBOX_END], 0xABu,
"the byte above the region was not touched");
}
static void test_firmware_handshake_is_not_vacuous(void)
{
ft_tt_noc_access noc = simulated_access();
const uint32_t signal_address = FT_TT_MB_GO_SLOT_ADDR(0) + (uint32_t)offsetof(ft_tt_go_msg, signal);
printf("\nmailbox: the firmware handshake has to be primed\n");
reset_simulated_core();
check(ft_tt_launch_clear_mailbox(&noc, 1, 2) == FT_TT_OK, "mailbox cleared");
check_u64(simulated_core.l1[signal_address], FT_TT_RUN_MSG_DONE,
"a cleared mailbox already reads DONE");
check(ft_tt_launch_prime_firmware_handshake(&noc, 1, 2) == FT_TT_OK, "prime writes INIT");
check_u64(simulated_core.l1[signal_address], FT_TT_RUN_MSG_INIT, "go signal is now INIT");
check_u64(simulated_core.l1[FT_TT_MB_GO_SLOT_ADDR(0)], 0u,
"priming touched only the signal byte");
/* With the handshake primed, a firmware that never boots must time out rather
* than report success. */
simulated_core.go_reads_before_done = UINT32_MAX;
check(ft_tt_launch_wait_firmware_ready(&noc, 1, 2, 20u) == FT_TT_EIO,
"an unprimed-looking core times out instead of passing");
simulated_core.go_reads = 0u;
simulated_core.go_reads_before_done = 3u;
simulated_core.l1[signal_address] = (uint8_t)FT_TT_RUN_MSG_INIT;
check(ft_tt_launch_wait_firmware_ready(&noc, 1, 2, 2000u) == FT_TT_OK,
"a booting firmware is detected");
}
static void test_launch_message_write_and_go(void)
{
ft_tt_noc_access noc = simulated_access();
ft_tt_launch_config config = baseline_config();
ft_tt_kernel_config_msg from_l1;
const uint32_t signal_address = FT_TT_MB_GO_SLOT_ADDR(0) + (uint32_t)offsetof(ft_tt_go_msg, signal);
printf("\nmailbox: launch message and go\n");
reset_simulated_core();
check(ft_tt_launch_write_launch_message(&noc, 1, 2, &config) == FT_TT_OK, "writes slot 0");
memcpy(&from_l1, simulated_core.l1 + FT_TT_MB_LAUNCH_SLOT_ADDR(0), sizeof(from_l1));
check_u64(from_l1.enables, config.enables, "enables round-tripped through L1");
check_u64(from_l1.kernel_config_base[FT_TT_PROGRAMMABLE_CORE_TYPE_TENSIX], 0x10000u,
"config base round-tripped through L1");
check_u64(simulated_core.l1[FT_TT_MB_LAUNCH_SLOT_ADDR(1)], 0u, "slot 1 was left alone");
check(ft_tt_launch_send_go(&noc, 1, 2) == FT_TT_OK, "go is sent");
check_u64(simulated_core.l1[signal_address], FT_TT_RUN_MSG_GO, "go signal is GO");
config.kernel_config_base = 0x10008u;
check(ft_tt_launch_write_launch_message(&noc, 1, 2, &config) == FT_TT_EINVAL,
"an invalid config is refused before it reaches L1");
}
static void test_completion_poll(void)
{
ft_tt_noc_access noc = simulated_access();
ft_tt_launch_state state;
const uint32_t signal_address = FT_TT_MB_GO_SLOT_ADDR(0) + (uint32_t)offsetof(ft_tt_go_msg, signal);
printf("\nmailbox: completion poll\n");
reset_simulated_core();
simulated_core.l1[signal_address] = (uint8_t)FT_TT_RUN_MSG_GO;
simulated_core.go_reads_before_done = 5u;
check(ft_tt_launch_wait_done(&noc, 1, 2, 2000u, NULL) == FT_TT_OK,
"returns OK when the firmware writes DONE");
check(simulated_core.go_reads >= 5u, "it really polled rather than returning at once");
printf("\nmailbox: completion poll TIMES OUT rather than spinning forever\n");
reset_simulated_core();
simulated_core.l1[signal_address] = (uint8_t)FT_TT_RUN_MSG_GO;
simulated_core.go_reads_before_done = UINT32_MAX;
memset(&state, 0xCD, sizeof(state));
check(ft_tt_launch_wait_done(&noc, 1, 2, 30u, &state) == FT_TT_EIO,
"returns FT_TT_EIO on timeout");
check_u64(state.go_messages[0].signal, FT_TT_RUN_MSG_GO,
"the timeout snapshot captured the stuck go signal");
check(state.reset_register == simulated_core.soft_reset,
"the timeout snapshot captured the reset register");
printf("\nmailbox: a corrupted go signal is reported, not waited on\n");
reset_simulated_core();
simulated_core.l1[signal_address] = 0x33u;
simulated_core.go_reads_before_done = UINT32_MAX;
check(ft_tt_launch_wait_done(&noc, 1, 2, 2000u, NULL) == FT_TT_ERROR,
"an unexpected signal value returns FT_TT_ERROR immediately");
printf("\nmailbox: an unbounded wait is refused\n");
reset_simulated_core();
simulated_core.l1[signal_address] = (uint8_t)FT_TT_RUN_MSG_GO;
check(ft_tt_launch_wait_done(&noc, 1, 2, 0u, NULL) == FT_TT_EINVAL,
"timeout_ms == 0 is refused (a hang here costs a power cycle)");
}
static void test_state_snapshot_and_dump(void)
{
ft_tt_noc_access noc = simulated_access();
ft_tt_launch_config config = baseline_config();
ft_tt_launch_state state;
printf("\nmailbox: state snapshot\n");
reset_simulated_core();
simulated_core.soft_reset = FT_TT_SOFT_RESET_ALL_RISCS & ~(uint32_t)FT_TT_SOFT_RESET_BRISC;
check(ft_tt_launch_write_launch_message(&noc, 1, 2, &config) == FT_TT_OK, "launch message set");
simulated_core.l1[FT_TT_MB_SUBORDINATE_SYNC_ADDR + 1u] = (uint8_t)FT_TT_RUN_SYNC_MSG_GO;
memcpy(simulated_core.l1 + FT_TT_MB_WATCHER_ADDR +
offsetof(ft_tt_watcher_msg, debug_waypoint[FT_TT_PROC_MATH0]),
"GW ", 4u);
/* The wedge shape this snapshot exists to describe: BRISC out of reset with the four
* subordinates held, and the Tensix instruction FIFO not accepting a write. MEASURED on
* silicon 2026-08-17 — 140 healthy cores read 0x77 with invalid_instrn 0, the core the
* 1x5 multicast relaunch arm wedged read exactly these two values. */
simulated_core.instrn_buf_status = 0x66u;
simulated_core.invalid_instrn = 1u;
check(ft_tt_launch_read_state(&noc, 1, 2, &state) == FT_TT_OK, "reads");
check_u64(state.reset_register, simulated_core.soft_reset, "reset register");
check_u64(state.instrn_buf_status, 0x66u, "the Tensix instruction FIFO status is carried");
check(state.instrn_buf_status != FT_TT_TENSIX_INSTRN_BUF_READY,
"and 0x66 is NOT the ready value, so the wedge shape is distinguishable");
check_u64(state.invalid_instrn, 1u, "and so is the invalid-instruction flag");
check_u64(state.subordinate_sync[1], FT_TT_RUN_SYNC_MSG_GO, "TRISC0 sync byte");
check_u64(state.kernel_config.enables, config.enables, "launch slot the read pointer selects");
check(memcmp(state.watcher.debug_waypoint[FT_TT_PROC_MATH0].waypoint, "GW ", 4u) == 0,
"waypoint bytes");
/* A garbage read pointer must not make the snapshot read a wild address. */
simulated_core.l1[FT_TT_MB_LAUNCH_RD_PTR_ADDR] = 0xFFu;
simulated_core.l1[FT_TT_MB_LAUNCH_RD_PTR_ADDR + 1u] = 0xFFu;
simulated_core.l1[FT_TT_MB_LAUNCH_RD_PTR_ADDR + 2u] = 0xFFu;
simulated_core.l1[FT_TT_MB_LAUNCH_RD_PTR_ADDR + 3u] = 0xFFu;
check(ft_tt_launch_read_state(&noc, 1, 2, &state) == FT_TT_OK,
"a garbage read pointer is clamped, not followed");
/* A debug register that does not answer must not cost the MAILBOX snapshot, which is
* the half a wedged core is being read for. It reports UNREAD instead. */
simulated_core.refuse_debug_registers = 1;
check(ft_tt_launch_read_state(&noc, 1, 2, &state) == FT_TT_OK,
"a debug register that refuses does not abort the snapshot");
check_u64(state.instrn_buf_status, FT_TT_TENSIX_INSTRN_BUF_UNREAD,
"and it reports UNREAD rather than a value it did not read");
check_u64(state.subordinate_sync[1], FT_TT_RUN_SYNC_MSG_GO,
"while the mailbox half is still there");
simulated_core.refuse_debug_registers = 0;
printf(" (dump follows on stderr; it is inspected by eye, not asserted)\n");
ft_tt_launch_dump_state(&state, 1, 2);
}
static void test_access_errors_propagate(void)
{
ft_tt_noc_access noc = simulated_access();
ft_tt_launch_config config = baseline_config();
ft_tt_noc_access broken;
printf("\nerror transparency: device failures are not swallowed\n");
reset_simulated_core();
simulated_core.fail_after_writes = 0;
check(ft_tt_launch_write_launch_message(&noc, 1, 2, &config) == FT_TT_EIO,
"a failing write surfaces as FT_TT_EIO");
broken.write = NULL;
broken.read = NULL;
broken.user = NULL;
check(ft_tt_launch_clear_mailbox(&broken, 1, 2) == FT_TT_EINVAL,
"an accessor with no callbacks is refused");
check(ft_tt_launch_read_state(&broken, 1, 2, NULL) == FT_TT_EINVAL, "a NULL out-state");
}
/* ------------------------------------------------------------------------- */
/* 9. Multi-core placement */
/* ------------------------------------------------------------------------- */
/** The topology this card reports, built by hand so coordinate derivation can be
* exercised with no device present. Same values as tests/test_noc.c. */
/** This test's own buffer, not a property of any part. The backend no longer carries a
* core count in a header (Phase 5): a set is bound to CALLER storage whose capacity comes
* from the live grid, so the number below only has to be at least as large as the
* synthetic grids these cases build. */
#define TEST_CORE_STORAGE_CAPACITY 256u
static ft_tt_launch_core_placement test_core_storage[TEST_CORE_STORAGE_CAPACITY];
static void test_core_set_construction(void)
{
static ft_tt_launch_core_set set;
ft_tt_launch_config config = baseline_config();
ft_tt_launch_config broken = baseline_config();
const ft_tt_grid_info grid = p150a_grid();
const uint32_t live_cores = ft_tt_coord_tensix_core_count(&grid);
unsigned index;
printf("\nplacement group: construction\n");
check(ft_tt_launch_core_set_add(&set, 1u, 2u, &config) == FT_TT_EINVAL,
"a set that was never given storage refuses an add instead of writing through NULL");
check_u64(ft_tt_launch_core_set_capacity(&set), 0u, "and reports no capacity");
check(ft_tt_launch_core_set_init(&set, NULL, 4u) == FT_TT_EINVAL, "NULL storage is refused");
check(ft_tt_launch_core_set_init(&set, test_core_storage, 0u) == FT_TT_EINVAL,
"and so is capacity 0");
check(ft_tt_launch_core_set_init(&set, test_core_storage, TEST_CORE_STORAGE_CAPACITY) ==
FT_TT_OK,
"storage binds");
check_u64(ft_tt_launch_core_set_count(&set), 0u, "a fresh set is empty");
check(ft_tt_launch_core_set_at(&set, 0u) == NULL, "and has no entry 0");
check(ft_tt_launch_core_set_add(&set, 1u, 2u, &config) == FT_TT_OK, "first core added");
check(ft_tt_launch_core_set_add(&set, 5u, 9u, &config) == FT_TT_OK, "second core added");
check_u64(ft_tt_launch_core_set_count(&set), 2u, "count");
check(ft_tt_launch_core_set_at(&set, 1u)->x == 5u && ft_tt_launch_core_set_at(&set, 1u)->y == 9u,
"entry 1 is the second core's coordinate");
check(ft_tt_launch_core_set_at(&set, 2u) == NULL, "one past the end is NULL");
/* Two entries for one Tensix means the second launch message silently
* overwrites the first and only one program ever runs. */
check(ft_tt_launch_core_set_add(&set, 1u, 2u, &config) == FT_TT_EINVAL,
"a duplicate coordinate is refused");
check_u64(ft_tt_launch_core_set_count(&set), 2u, "and the refusal did not grow the set");
broken.enables = 0u;
check(ft_tt_launch_core_set_add(&set, 7u, 7u, &broken) == FT_TT_EINVAL,
"a config the encoder rejects is refused at ADD time, not mid-stage");
check(ft_tt_launch_core_set_add(NULL, 1u, 1u, &config) == FT_TT_EINVAL, "a NULL set");
/* Coordinate first, config second: a program's text addresses are not known
* until its images have been loaded, and loading needs the coordinate. */
check(ft_tt_launch_core_set_add(&set, 9u, 9u, NULL) == FT_TT_OK,
"a coordinate can be added with no config yet");
check_u64(ft_tt_launch_core_set_at(&set, 2u)->config.enables, 0u,
"and it comes back with nothing enabled");
check(ft_tt_launch_core_set_configure(&set, 2u, &config) == FT_TT_OK,
"configuring it once the images are placed works");
check_u64(ft_tt_launch_core_set_at(&set, 2u)->config.enables, config.enables,
"and the config took");
check(ft_tt_launch_core_set_configure(&set, 3u, &config) == FT_TT_EINVAL,
"configuring an entry past the end is refused");
check(ft_tt_launch_core_set_configure(&set, 0u, &broken) == FT_TT_EINVAL,
"and so is a config the encoder rejects");
/* The capacity is whatever the CALLER sized it from, and here that is the live core
* count of the synthetic grid — so a constant creeping back into ft_tt_launch.h goes
* red here rather than passing on a part that happens to have 140 cores. */
check(ft_tt_launch_core_set_init(&set, test_core_storage, live_cores) == FT_TT_OK,
"a set sized from the LIVE core count");
check_u64(ft_tt_launch_core_set_capacity(&set), live_cores, "reports that capacity");
for (index = 0; index < TEST_CORE_STORAGE_CAPACITY; index++) {
if (ft_tt_launch_core_set_add(&set, (uint16_t)(index % 100u), (uint16_t)(index / 100u),
&config) != FT_TT_OK)
break;
}
check_u64(index, live_cores, "exactly the live core count fits, no more and no fewer");
check(ft_tt_launch_core_set_add(&set, 200u, 200u, &config) == FT_TT_ENOMEM,
"one more than the caller sized for is refused");
}
static void test_core_set_derives_and_classifies(void)
{
static ft_tt_launch_core_set set;
const ft_tt_grid_info grid = p150a_grid();
ft_tt_launch_config config = baseline_config();
const ft_tt_launch_core_placement *first;
printf("\nplacement group: coordinates come from the grid and are classified\n");
(void)ft_tt_launch_core_set_init(&set, test_core_storage, TEST_CORE_STORAGE_CAPACITY);
check(ft_tt_launch_core_set_add_tensix(&set, &grid, 0u, &config) == FT_TT_OK,
"Tensix 0 derives and classifies");
first = ft_tt_launch_core_set_at(&set, 0u);
check(first != NULL && first->x == 1u && first->y == 2u,
"and it is translated (1, 2), the first Tensix on this part");
check(ft_tt_launch_core_set_add_tensix(&set, &grid, 139u, &config) == FT_TT_OK,
"so does the last one, index 139");
check(ft_tt_launch_core_set_add_tensix(&set, &grid, 140u, &config) != FT_TT_OK,
"an index past the enabled count is refused, not clamped");
check(ft_tt_launch_core_set_add_tensix(&set, NULL, 3u, &config) == FT_TT_EINVAL, "a NULL grid");
check_u64(ft_tt_launch_core_set_count(&set), 2u, "only the two good ones are in the set");
/* An unconfigured entry must not reach the device as a launch of nothing. */
check(ft_tt_launch_core_set_add_tensix(&set, &grid, 5u, NULL) == FT_TT_OK,
"a coordinate-only entry is allowed");
{
ft_tt_noc_access noc = simulated_access();
unsigned failed = UINT32_MAX;
reset_simulated_core();
check(ft_tt_launch_write_launch_messages(&noc, &set, &failed) == FT_TT_EINVAL,
"but staging the group refuses it rather than launching nothing");
check_u64(failed, 2u, "naming the entry that was never configured");
}
}
static void test_core_set_stage_kick_wait_park(void)
{
static ft_tt_launch_core_set set;
ft_tt_noc_access noc = simulated_access();
ft_tt_launch_config config = baseline_config();
const uint32_t signal_address =
FT_TT_MB_GO_SLOT_ADDR(0) + (uint32_t)offsetof(ft_tt_go_msg, signal);
unsigned failed = UINT32_MAX;
printf("\nplacement group: stage every core, then kick, then wait\n");
(void)ft_tt_launch_core_set_init(&set, test_core_storage, TEST_CORE_STORAGE_CAPACITY);
(void)ft_tt_launch_core_set_add(&set, 1u, 2u, &config);
(void)ft_tt_launch_core_set_add(&set, 3u, 2u, &config);
(void)ft_tt_launch_core_set_add(&set, 5u, 2u, &config);
reset_simulated_core();
check(ft_tt_launch_write_launch_messages(&noc, &set, &failed) == FT_TT_OK,
"all three launch messages written");
check_u64(simulated_core.write_calls, 3u, "one write per core, and no more");
check(ft_tt_launch_send_go_all(&noc, &set, &failed) == FT_TT_OK, "all three kicked");
check_u64(simulated_core.l1[signal_address], FT_TT_RUN_MSG_GO, "the go signal is GO");
simulated_core.go_reads = 0u;
simulated_core.go_reads_before_done = 2u;
check(ft_tt_launch_wait_done_all(&noc, &set, 2000u, &failed) == FT_TT_OK,
"all three completed");
check(ft_tt_launch_park_all(&noc, &set) == FT_TT_OK, "all three parked");
check_u64(simulated_core.soft_reset, FT_TT_SOFT_RESET_ALL_RISCS,
"and every RISC-V is back in reset");
printf("\nplacement group: failures name the core they happened on\n");
reset_simulated_core();
simulated_core.fail_after_writes = 1; /* core 0's message lands, core 1's does not */
failed = UINT32_MAX;
check(ft_tt_launch_write_launch_messages(&noc, &set, &failed) == FT_TT_EIO,
"a failing write stops the group");
check_u64(failed, 1u, "and reports WHICH core failed");
reset_simulated_core();
simulated_core.l1[signal_address] = (uint8_t)FT_TT_RUN_MSG_GO;
simulated_core.go_reads_before_done = UINT32_MAX;
failed = UINT32_MAX;
check(ft_tt_launch_wait_done_all(&noc, &set, 30u, &failed) == FT_TT_EIO,
"a core that never finishes times out");
check_u64(failed, 0u, "on the first core");
check(ft_tt_launch_wait_done_all(&noc, &set, 0u, NULL) == FT_TT_EINVAL,
"an unbounded group wait is refused, exactly like the single-core one");
/* Cleanup must not stop at the first unreachable core, or one bad core leaves
* the rest of the group running and still able to issue NoC transactions. */
printf("\nplacement group: park_all tries every core even when one fails\n");
reset_simulated_core();
simulated_core.fail_after_writes = 0;
check(ft_tt_launch_park_all(&noc, &set) == FT_TT_EIO, "the failure is reported");
check_u64(simulated_core.write_calls, 3u, "but all three cores were tried");
check(ft_tt_launch_park_all(&noc, NULL) == FT_TT_EINVAL, "a NULL set is refused");
check(ft_tt_launch_send_go_all(NULL, &set, NULL) == FT_TT_EINVAL, "a NULL accessor is refused");
}
/* ------------------------------------------------------------------------- */
/* 10. The allocator floor above the loaded images */
/* ------------------------------------------------------------------------- */
static void test_l1_floor_above_loaded_images(void)
{
ft_tt_noc_access noc = simulated_access();
const ft_tt_grid_info grid = p150a_grid();
ft_tt_image_placement placements[FT_TT_PROC_COUNT];
ft_tt_allocator allocator;
ft_tt_buffer buffer;
synthetic_elf elf;
uint32_t floor;
printf("\nallocator floor: it must clear the images that are loaded\n");
memset(placements, 0, sizeof(placements));
check_u64(ft_tt_image_placement_l1_floor(NULL, 0u), FT_TT_L1_USABLE_BASE,
"with nothing loaded the floor is the usable base");
check_u64(ft_tt_image_placement_l1_floor(placements, FT_TT_PROC_COUNT), FT_TT_L1_USABLE_BASE,
"and placements nothing was written into are ignored, not read as address 0");
check_u64(ft_tt_image_placement_l1_floor(NULL, 3u), 0u, "a NULL array with a count is refused");
/* A kernel image is the no-op case: its text lives in the tail of its own
* firmware hole, below the usable base, so it cannot reach the arena. */
reset_simulated_core();
build_kernel_elf(&elf, FT_TT_MEM_TRISC0_FIRMWARE_BASE + 0x400u, 512u);
check(ft_tt_launch_load_image(&noc, 1, 2, FT_TT_PROC_MATH0, FT_TT_IMAGE_KERNEL, elf.bytes,
elf.size, &placements[FT_TT_PROC_MATH0]) == FT_TT_OK,
"a kernel image loads");
check_u64(ft_tt_image_placement_l1_floor(placements, FT_TT_PROC_COUNT), FT_TT_L1_USABLE_BASE,
"a kernel below the usable base does not raise the floor");
/* Firmware is the case that bites: BRISC's .data is staged AT 0x9F80, which is
* exactly where a default allocator starts handing out addresses. */
memset(placements, 0, sizeof(placements));
reset_simulated_core();
build_firmware_elf(&elf, FT_TT_PROC_DM0, 256u);
check(ft_tt_launch_load_image(&noc, 1, 2, FT_TT_PROC_DM0, FT_TT_IMAGE_FIRMWARE, elf.bytes,
elf.size, &placements[FT_TT_PROC_DM0]) == FT_TT_OK,
"BRISC firmware loads");
check(placements[FT_TT_PROC_DM0].highest_l1_address > FT_TT_L1_USABLE_BASE,
"its .data image reaches ABOVE the allocator's own floor");
floor = ft_tt_image_placement_l1_floor(placements, FT_TT_PROC_COUNT);
check_u64(floor, placements[FT_TT_PROC_DM0].highest_l1_address,
"so the derived floor is the top of that image");
check((floor % FT_TT_L1_ALIGNMENT) == 0u, "and it is 16-byte aligned");
/* The collision, shown rather than asserted: a DEFAULT allocator hands out an
* address inside the firmware image that is about to run. */
check(ft_tt_allocator_init(&allocator, NULL, &grid, 0u) == FT_TT_OK, "default allocator");
check(ft_tt_buffer_alloc(&allocator, FT_TT_MEM_SPACE_L1, 0u, 64u, &buffer) == FT_TT_OK,
"it allocates");
check(buffer.address < (uint64_t)placements[FT_TT_PROC_DM0].highest_l1_address,
"and the address it hands out lands INSIDE the loaded firmware image");
check(ft_tt_allocator_init(&allocator, NULL, &grid, floor) == FT_TT_OK,
"an allocator built with the derived floor");
check(ft_tt_buffer_alloc(&allocator, FT_TT_MEM_SPACE_L1, 0u, 64u, &buffer) == FT_TT_OK,
"still allocates");
check(buffer.address >= (uint64_t)placements[FT_TT_PROC_DM0].highest_l1_address,
"but now the first address is above every loaded image");
(void)ft_tt_allocator_reset(&allocator);
check(ft_tt_buffer_alloc(&allocator, FT_TT_MEM_SPACE_L1, 0u, FT_TT_L1_USABLE_SIZE, &buffer) ==
FT_TT_ENOMEM,
"and the span that would have covered the image is now REFUSED");
check(ft_tt_buffer_alloc(&allocator, FT_TT_MEM_SPACE_L1, 0u,
FT_TT_L1_USABLE_SIZE - (size_t)(floor - FT_TT_L1_USABLE_BASE),
&buffer) == FT_TT_OK,
"while everything from just above the images to the top of L1 still fits");
}
/* ------------------------------------------------------------------------- */
int main(void)
{
printf("Foundry Blackhole backend — Phase 2 launch tests (no device)\n");
test_mailbox_layout();
test_core_info_encoding();
test_watcher_priming();
test_phase2_layout_derivation();
test_elf_parses_a_kernel();
test_elf_rejections();
test_elf_segment_limits();
test_kernel_blob_format();
test_kernel_image_placement();
test_firmware_image_placement();
test_placement_refusals();
test_relocated_kernel_placement();
test_merged_region_refusals();
test_launch_message_encoding();
test_launch_message_refusals();
test_cb_config_encoding();
test_runtime_argument_planting();
test_reset_control();
test_mailbox_clear();
test_firmware_handshake_is_not_vacuous();
test_launch_message_write_and_go();
test_completion_poll();
test_state_snapshot_and_dump();
test_access_errors_propagate();
test_core_set_construction();
test_core_set_derives_and_classifies();
test_core_set_stage_kick_wait_park();
test_l1_floor_above_loaded_images();
printf("\n%d/%d checks failed\n", checks_failed, checks_run);
return checks_failed == 0 ? 0 : 1;
}