/* 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 #include #include #include #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; }