Foundry/tt/tests/ft_tt_hardware_session.h

287 lines
14 KiB
C

/* SPDX-License-Identifier: AGPL-3.0-or-later */
/* Copyright (C) 2026 David (Corey) Herrera */
/**
* @file ft_tt_hardware_session.h
* @brief The setup every on-card Phase 2 test repeats: open the device, read the grid
* from ARC telemetry, derive a real Tensix coordinate, claim a TLB window, and build
* an ft_tt_noc_access over it. Plus the two whole-core operations that follow —
* booting Tenstorrent's firmware on the five RISC-Vs, and parking them in reset.
* Which core and which firmware build come from the environment
* (FT_TT_TENSIX_INDEX, FT_TT_FIRMWARE_DIR) so two workers can share the card.
*
* This is TEST scaffolding, not backend API. It lives here because the checklist in
* docs/tt_launch_notes.md §6 is nine separate programs against one core, and repeating
* the opening 60 lines nine times is how one of them ends up hardcoding a coordinate.
* Nothing here guesses: the coordinate comes from ft_tt_coord against live telemetry
* every time (spec §7.3 — a wrong NoC coordinate hangs the chip).
*
* ---------------------------------------------------------------------------
* IT IS THE PRODUCT'S TRANSPORT SEAM, NOT A SECOND ONE
* ---------------------------------------------------------------------------
* `ft_tt_hardware_session_open()` opens through `ft_tt_runtime_open_with_driver()`,
* so `FT_TT_DRIVER=kmd|udrv` selects the transport for every on-card suite exactly
* as it does for a serve. Until 2026-08-18 this file called `ft_tt_init_device()`
* directly — i.e. every one of the ~30 on-card suites was hard-wired to tt-kmd, and
* the card could have moved to a board with no vendor module where NOT ONE OF THEM
* would run. The engine had a seam; the instruments did not.
*
* The default stays `kmd` and an unrecognised value is a REFUSAL, not a fallback —
* `ft_tt_runtime_driver_select()` owns both rules and this file does not restate
* them.
*
* Two fields the tt-kmd path has and the userspace one does not — `runtime.ctx`
* and `runtime.binding` — are reachable through `session->runtime`, deliberately
* NOT mirrored here. A harness that needs them is a harness that needs tt-kmd, and
* it should say so at the point it reaches for one (`session->runtime.ctx == NULL`
* is that test) rather than dereference a session field that quietly became NULL.
*
* @status Runs on the p150a test card. Every function is used by the on-card
* test_phase2_*.c programs. Transport selection added 2026-08-18 and exercised
* over `udrv` with `tenstorrent` unloaded.
* @issues
* - The firmware ELFs are read from a program directory on disk rather than embedded:
* the tests take whatever build_tensix_program.sh produced. The shipping backend
* embeds them (ft_tt_programs.h).
* @todo None
*/
#ifndef FT_TT_HARDWARE_SESSION_H
#define FT_TT_HARDWARE_SESSION_H
#include <stddef.h>
#include <stdint.h>
#include "ft_tt_arc_telemetry.h"
#include "ft_tt_coord.h"
#include "ft_tt_device.h"
#include "ft_tt_kernel_blob.h"
#include "ft_tt_launch.h"
#include "ft_tt_runtime.h"
#include "ft_tt_tensix_claim.h"
#include "ft_tt_tlb.h"
/** Environment variable naming the directory where build_tensix_program.sh left
* fw_{brisc,ncrisc,trisc0,trisc1,trisc2}.elf (a program directory such as
* tt/programs/<op>). REQUIRED: there is no default, because a kernel is linked at its own
* firmware's `__fw_export_text_end`, so mixing a kernel from one build with firmware from
* another is not a configuration, it is a wedged core. */
#define FT_TT_FIRMWARE_DIRECTORY_ENVIRONMENT "FT_TT_FIRMWARE_DIR"
/** How long to give the five RISC-Vs to boot. Measured at well under 100 ms. */
#define FT_TT_FIRMWARE_BOOT_TIMEOUT_MS 2000u
/** Environment variable naming which enabled Tensix a single-core test drives. Unset
* means index 0, which is what every Phase 2 test used before this existed. */
#define FT_TT_TENSIX_INDEX_ENVIRONMENT "FT_TT_TENSIX_INDEX"
/** How many separate core claims one session may hold. More than one because a
* harness can learn about a second core only after resolving the first — the
* cross-core test reads its peer index after the compute index — and forcing
* those into one call would mean restructuring the resolution order of every
* such test for no safety gained. Each call is its own all-or-nothing set. */
#define FT_TT_HARDWARE_SESSION_MAX_CLAIMS 4u
/** An open card on whichever transport `FT_TT_DRIVER` names, its accessors, and
* the CORES this process owns for the duration.
*
* `noc` and `grid` are copies of the runtime's, and copying them is safe for the
* reason that matters: an accessor vtable and a grid description are both written
* once at open and never again, and `noc.user` points into `runtime`, which lives
* inside this struct. Anything that MOVES — the byte counters, the aim counts —
* is deliberately not copied; read it from `runtime` so a measurement can never
* be taken from a frozen snapshot.
*
* `cores` is the per-core half of the coordination split (ft_tt_tensix_claim.h):
* the whole-card lock in tools/ft_tt_card_lock.h stays for operations that reset
* the card, and everything a single-core or small-group harness does is claimed
* core by core instead, so two suites on disjoint cores run at the same time.
* Entries are NULL until something claims; all are released by
* ft_tt_hardware_session_close(). A claim is a flock under /tmp and needs NO
* device handle on either transport. */
typedef struct ft_tt_hardware_session {
ft_tt_runtime runtime;
ft_tt_noc_access noc;
ft_tt_grid_info grid;
ft_tt_tensix_claim_set *cores[FT_TT_HARDWARE_SESSION_MAX_CLAIMS];
} ft_tt_hardware_session;
/**
* The tt-kmd context behind this session, or NULL on the userspace driver.
*
* For the two harnesses that legitimately want the vendor handle — allocating
* DRAM through `ft_tt_allocator_init()`, opening a second `ft_tt_noc_path` — so
* that "this arm needs tt-kmd" is a readable test with a message, rather than a
* NULL dereference on a field that used to always be set.
*/
ft_tt_ctx *ft_tt_hardware_session_kmd_context(const ft_tt_hardware_session *session);
/** The five firmware images, read from disk once and reused across cores. */
typedef struct ft_tt_firmware_set {
unsigned char *elf_data[FT_TT_PROC_COUNT];
size_t elf_size[FT_TT_PROC_COUNT];
} ft_tt_firmware_set;
/**
* Open device 0 on the transport `FT_TT_DRIVER` selects, validate its grid, claim
* the windows and wire up the accessors.
*
* @param out_session zeroed and filled on success. The accessors point into it, so
* it must not be copied by value after this returns
* @return FT_TT_OK, or a negative FT_TT_* code with the reason logged.
* FT_TT_EINVAL when `FT_TT_DRIVER` names no transport — a typo
* is refused rather than defaulted, because a whole suite
* attributed to the wrong transport is worse than no suite
*
* Thread-safe: no Reentrant: no Signal-safe: no
*/
int ft_tt_hardware_session_open(ft_tt_hardware_session *out_session);
/** Free the window and close the device. Safe on a NULL or half-opened session. */
void ft_tt_hardware_session_close(ft_tt_hardware_session *session);
/**
* NoC coordinate of the `index`-th enabled Tensix, in the TRANSLATED system that a TLB
* window must be aimed with.
*
* @return FT_TT_OK, or FT_TT_EINVAL / FT_TT_EUNAVAIL from the coordinate module
*/
int ft_tt_hardware_session_tensix(const ft_tt_hardware_session *session, uint32_t index,
uint16_t *out_x, uint16_t *out_y);
/**
* Which enabled Tensix this process was told to drive, from
* FT_TT_TENSIX_INDEX_ENVIRONMENT. Unset is index 0.
*
* This exists so two people — or two agents — can run the single-core suite at once
* without editing sources. It resolves an INDEX, not a coordinate: the coordinate still
* comes from ft_tt_coord against live telemetry, so a bad value here is a refusal, never
* a NoC transaction at a guessed endpoint (spec §7.3).
*
* @param out_index written only on success
* @return FT_TT_OK; FT_TT_EINVAL for a NULL out, a non-numeric value or
* trailing junk. The RANGE is checked against the live core count when
* the index is resolved, not here — a constant bound would be wrong on
* a 120-core part
*
* Thread-safe: no (getenv) Reentrant: no Signal-safe: no
*/
int ft_tt_tensix_index_from_environment(uint32_t *out_index);
/**
* CLAIM the cores this process is about to drive, exclusively against every other
* process using ft_tt_tensix_claim.h.
*
* Refuses rather than blocking, and prints WHO holds a core it cannot get — a pid
* and the reason that process registered. A timeout would tell the next person
* nothing; a name tells them everything.
*
* All or nothing: a refusal releases anything already taken, so a harness never
* runs on half its cores.
*
* @param retire RECOVERY-ON-ACQUIRE, run once per core after the whole set is
* held. A released claim is NOT a retired residency: the kernel
* gives the lock back when a holder dies and cannot give the CORE
* back — a SIGKILLed process was measured leaving a core with all
* five processors running for over 40 s. Pass a function that
* retires (a queue's `ft_tt_queue_request_stop()` + re-init) or
* that INSPECTS and refuses a core that is not idle. Passing NULL
* means "this harness accepts whatever state it finds", which is
* only defensible for a program that boots firmware from scratch
* anyway. **Never retire by parking**: asserting reset on a
* resident triad breaks the core's NEXT boot (DEV_NOTES §1).
* @return FT_TT_OK; FT_TT_EPERM when a core is held, reserved or unretirable
* (already explained on stderr); FT_TT_EINVAL; FT_TT_EIO
*
* Thread-safe: no Reentrant: no Signal-safe: no
*/
int ft_tt_hardware_session_claim_cores(ft_tt_hardware_session *session, const uint32_t *indices,
size_t count, const char *reason,
ft_tt_tensix_claim_retire_fn retire,
void *retire_context);
/**
* ft_tt_tensix_index_from_environment() plus ft_tt_hardware_session_tensix(), printing
* the resolved index and TRANSLATED coordinate — which every one of these tests wants in
* its log, because "which core did this run on" is the first question a failure raises.
*
* It also CLAIMS that core, because a harness that names its core should own it.
* The session is no longer `const` for exactly that reason. A harness that drives
* MORE cores than the one this resolves must claim the rest itself with
* ft_tt_hardware_session_claim_cores() — this call claims one core, and one only.
*
* @param out_index may be NULL
* @return FT_TT_OK, or the first failing step's code (logged); FT_TT_EPERM
* when the core is held by another process
*/
int ft_tt_hardware_session_tensix_from_environment(ft_tt_hardware_session *session,
uint16_t *out_x, uint16_t *out_y,
uint32_t *out_index);
/** The firmware/program directory this process should read, from
* FT_TT_FIRMWARE_DIRECTORY_ENVIRONMENT, else $HOME/ttprog. NULL (with the reason logged)
* only when both are unset; ft_tt_firmware_set_load() refuses a NULL directory with
* FT_TT_EINVAL. */
const char *ft_tt_firmware_directory(void);
/**
* Read the five firmware ELFs out of `directory` (pass ft_tt_firmware_directory()).
*
* @return FT_TT_OK, FT_TT_EIO if a file is missing or short, FT_TT_ENOMEM.
* Ownership passes to the caller: release with ft_tt_firmware_set_release().
*/
int ft_tt_firmware_set_load(const char *directory, ft_tt_firmware_set *out_set);
/** Free every image. Safe on NULL and on a partially loaded set. */
void ft_tt_firmware_set_release(ft_tt_firmware_set *set);
/**
* Bring one Tensix from unknown to "firmware running, waiting for a launch message":
* assert reset on the five RISC-Vs, clear the mailbox, load the five firmware images,
* prime the boot handshake with RUN_MSG_INIT, deassert BRISC only, and wait for BRISC to
* write RUN_MSG_DONE.
*
* This is docs/tt_launch_notes.md §6 steps 3 and 4 as one call. Every ordering
* constraint in it is load-bearing and documented there; do not reorder.
*
* @param placements indexed by FT_TT_PROC_*, receives where each firmware image landed;
* may be NULL. This is what ft_tt_image_placement_l1_floor() consumes,
* and it is the FIRMWARE placements that matter — firmware `.data`
* staging starts exactly at FT_TT_L1_USABLE_BASE, while kernel images
* sit below it and cannot reach the arena at all (ft_tt_launch.h)
* @return FT_TT_OK, or the first failing step's code (logged)
*/
int ft_tt_core_boot_firmware(const ft_tt_noc_access *noc, uint16_t x, uint16_t y,
const ft_tt_firmware_set *firmware,
ft_tt_image_placement placements[FT_TT_PROC_COUNT]);
/** Write every image of `program` into `(x, y)`'s L1, reporting where each landed.
* `placements` is indexed by FT_TT_PROC_* and zeroed for processors the program
* leaves idle; it may be NULL. */
int ft_tt_core_load_program(const ft_tt_noc_access *noc, uint16_t x, uint16_t y,
const ft_tt_kernel_program *program,
ft_tt_image_placement placements[FT_TT_PROC_COUNT]);
/**
* Put every RISC-V of one Tensix back in reset. Call it on EVERY exit path, including
* failures — a core left running is a core that can still issue NoC transactions.
*
* The five RISC-Vs only. The matrix backend's reset bits are in the same register and
* must NOT be touched (ft_tt_launch.h).
*/
int ft_tt_core_park(const ft_tt_noc_access *noc, uint16_t x, uint16_t y);
/**
* Wait until TRISC0 reports RUN_SYNC_MSG_DONE.
*
* BRISC asks TRISC0 to re-zero all 64 circular-buffer counter registers after every run
* (BRISC's post-run firmware step), and it does that AFTER writing RUN_MSG_DONE is
* scheduled — so a host that stages CB counters between runs must wait for this or its
* writes get zeroed underneath it.
*
* @return FT_TT_OK, FT_TT_EIO on timeout
*/
int ft_tt_core_wait_sync_registers_ready(const ft_tt_noc_access *noc, uint16_t x, uint16_t y,
uint32_t timeout_ms);
#endif /* FT_TT_HARDWARE_SESSION_H */