/* SPDX-License-Identifier: AGPL-3.0-or-later */ /* Copyright (C) 2026 David (Corey) Herrera */ /** * @file ft_tt_runtime.c * @brief Implementation of the serving-process host runtime (see ft_tt_runtime.h). * * @status Written for Phase 7. Carries the TRANSPORT SEAM since 2026-08-17: * `ft_tt_runtime_open()` selects tt-kmd or foundry's own userspace driver from * `FT_TT_DRIVER`, defaulting to `kmd`, and a serve has run on the card over * both. Nothing above this file changed. * @issues None * @todo None */ #ifndef _POSIX_C_SOURCE #define _POSIX_C_SOURCE 200809L #endif #include "ft_tt_runtime.h" #include #include #include #if defined(__SSE2__) #include #endif /** Firmware ELF names build_tensix_program.sh writes, indexed by FT_TT_PROC_*. */ static const char *const k_firmware_file_names[FT_TT_PROC_COUNT] = { "fw_brisc.elf", "fw_ncrisc.elf", "fw_trisc0.elf", "fw_trisc1.elf", "fw_trisc2.elf" }; /** Kernel ELF names from the same build, indexed the same way. */ static const char *const k_kernel_file_names[FT_TT_PROC_COUNT] = { "brisc.elf", "ncrisc.elf", "trisc0.elf", "trisc1.elf", "trisc2.elf" }; const char *ft_tt_runtime_kernel_image_name(unsigned processor) { if (processor >= FT_TT_PROC_COUNT) return NULL; return k_kernel_file_names[processor]; } /* ================================================================ * Files * ================================================================ */ /** Read a whole file into a fresh buffer. Caller frees. */ static int read_whole_file(const char *path, unsigned char **out_data, size_t *out_size) { FILE *file = fopen(path, "rb"); unsigned char *buffer = NULL; long size; if (!file) { ft_tt_log_error("tt runtime: cannot open %s", path); return FT_TT_EIO; } if (fseek(file, 0, SEEK_END) != 0 || (size = ftell(file)) < 0 || fseek(file, 0, SEEK_SET) != 0) { fclose(file); return FT_TT_EIO; } buffer = malloc((size_t)size); if (!buffer) { fclose(file); return FT_TT_ENOMEM; } if (fread(buffer, 1, (size_t)size, file) != (size_t)size) { ft_tt_log_error("tt runtime: short read on %s", path); free(buffer); fclose(file); return FT_TT_EIO; } fclose(file); *out_data = buffer; *out_size = (size_t)size; return FT_TT_OK; } /** Copy the `.src` sidecar's first line into `out`, or fall back to the ELF name. A * missing sidecar is not an error: it only costs a log line its provenance. */ static void read_source_name(const char *directory, const char *elf_name, char *out, size_t out_bytes) { char path[512]; FILE *file = NULL; size_t length; (void)snprintf(out, out_bytes, "%s", elf_name); if (snprintf(path, sizeof(path), "%s/%s.src", directory, elf_name) < 0) return; file = fopen(path, "r"); if (!file) return; if (fgets(out, (int)out_bytes, file)) { length = strlen(out); while (length > 0 && (out[length - 1] == '\n' || out[length - 1] == '\r')) out[--length] = '\0'; if (length == 0) (void)snprintf(out, out_bytes, "%s", elf_name); } fclose(file); } /** The shared body of both loaders: read `names[p]` for every p in `mask`. */ static int load_named_images(const char *directory, uint32_t processor_mask, const char *const *names, ft_tt_runtime_images *out_images) { unsigned processor; if (!directory || !out_images || processor_mask == 0u) return FT_TT_EINVAL; memset(out_images, 0, sizeof(*out_images)); (void)snprintf(out_images->name, sizeof(out_images->name), "%s", directory); for (processor = 0; processor < FT_TT_PROC_COUNT; processor++) { char path[512]; int rc; if ((processor_mask & (1u << processor)) == 0u) continue; if (snprintf(path, sizeof(path), "%s/%s", directory, names[processor]) < 0) { ft_tt_runtime_images_release(out_images); return FT_TT_EINVAL; } rc = read_whole_file(path, &out_images->owned_data[processor], &out_images->elf_size[processor]); if (rc != FT_TT_OK) { ft_tt_runtime_images_release(out_images); return rc; } out_images->elf_data[processor] = out_images->owned_data[processor]; read_source_name(directory, names[processor], out_images->source_names[processor], sizeof(out_images->source_names[processor])); } out_images->source = FT_TT_PROGRAM_SOURCE_DIRECTORY; return FT_TT_OK; } int ft_tt_runtime_firmware_load(const char *directory, ft_tt_runtime_images *out_images) { uint32_t all = 0u; unsigned processor; for (processor = 0; processor < FT_TT_PROC_COUNT; processor++) all |= 1u << processor; return load_named_images(directory, all, k_firmware_file_names, out_images); } int ft_tt_runtime_program_load(const char *directory, uint32_t processor_mask, ft_tt_runtime_images *out_images) { unsigned processor; unsigned count = 0; int rc = load_named_images(directory, processor_mask, k_kernel_file_names, out_images); if (rc != FT_TT_OK) return rc; for (processor = 0; processor < FT_TT_PROC_COUNT; processor++) { if ((processor_mask & (1u << processor)) == 0u) continue; out_images->images[count].source_name = out_images->source_names[processor]; out_images->images[count].processor = processor; out_images->images[count].elf_data = out_images->elf_data[processor]; out_images->images[count].elf_size = out_images->elf_size[processor]; count++; } out_images->program.name = out_images->name; out_images->program.toolchain = "on-disk"; out_images->program.compile_time_args = "on-disk"; out_images->program.images = out_images->images; out_images->program.image_count = count; return FT_TT_OK; } void ft_tt_runtime_images_release(ft_tt_runtime_images *images) { unsigned processor; if (!images) return; /* `owned_data`, NOT `elf_data`: an embedded image points into a `static const` array, * and free()ing that is the one way this struct could turn the portability win into a * crash. Two arrays make it unrepresentable rather than merely avoided. */ for (processor = 0; processor < FT_TT_PROC_COUNT; processor++) { free(images->owned_data[processor]); images->owned_data[processor] = NULL; images->elf_data[processor] = NULL; images->elf_size[processor] = 0; } images->program.images = NULL; images->program.image_count = 0; images->source = FT_TT_PROGRAM_SOURCE_NONE; } /* ================================================================ * The bulk DRAM accessor * ================================================================ */ /** * The ft_tt_noc_access vtable over ft_tt_{read,write}_dram(), and nothing else. * * Every byte moves at FT_TT_NOC_WIDTH_BLOCK16 through a WC window, with the DRAM * space bounds and the 64 B read / 16 B write alignment those helpers enforce. A * register address does not fit the space and a 4-byte length does not fit the unit, * so this accessor CANNOT carry run control even by mistake — which is the point of * having two rather than one wider one. */ static int bulk_dram_write(void *user, uint16_t x, uint16_t y, uint64_t addr, const void *src, size_t bytes) { ft_tt_noc_path *path = (ft_tt_noc_path *)user; ft_tt_noc_coord coord; int rc; if (!path || !src) return FT_TT_EINVAL; coord.x = x; coord.y = y; rc = ft_tt_write_dram(path, coord, addr, src, bytes); /* WC stores may sit in the write-combining buffer indefinitely. The UC run-control * accesses that follow (launch args, go message) would drain it as a side effect of * x86's ordering rules, but relying on that makes correctness depend on the ORDER of * two unrelated modules' calls. One store fence per transfer costs nothing measurable * against a PCIe round trip and removes the question. */ #if defined(__SSE2__) _mm_sfence(); #endif return rc; } static int bulk_dram_read(void *user, uint16_t x, uint16_t y, uint64_t addr, void *dst, size_t bytes) { ft_tt_noc_path *path = (ft_tt_noc_path *)user; ft_tt_noc_coord coord; if (!path || !dst) return FT_TT_EINVAL; coord.x = x; coord.y = y; return ft_tt_read_dram(path, coord, addr, dst, bytes); } /** * The same vtable over ft_tt_{read,write}_l1(), so a caller that wants more than a word out * of a core's L1 does not pay the register accessor's 4 bytes per PCIe transaction. * * The L1 space check keeps it honest in both directions: 16-byte alignment in address AND * length means a 4-byte control word, a go message or a soft-reset register cannot come * through here, and an address outside L1 is a refusal rather than a NoC transaction. */ static int bulk_l1_write(void *user, uint16_t x, uint16_t y, uint64_t addr, const void *src, size_t bytes) { ft_tt_noc_path *path = (ft_tt_noc_path *)user; ft_tt_noc_coord coord; int rc; if (!path || !src) return FT_TT_EINVAL; coord.x = x; coord.y = y; rc = ft_tt_write_l1(path, coord, addr, src, bytes); /* Same store fence as the DRAM arm, for the same reason: a WC store may sit in the * write-combining buffer, and correctness must not depend on an unrelated module's * later UC access draining it. */ #if defined(__SSE2__) _mm_sfence(); #endif return rc; } static int bulk_l1_read(void *user, uint16_t x, uint16_t y, uint64_t addr, void *dst, size_t bytes) { ft_tt_noc_path *path = (ft_tt_noc_path *)user; ft_tt_noc_coord coord; if (!path || !dst) return FT_TT_EINVAL; coord.x = x; coord.y = y; return ft_tt_read_l1(path, coord, addr, dst, bytes); } /* ================================================================ * The card * ================================================================ */ int ft_tt_runtime_driver_select(ft_tt_runtime_driver *out_driver, bool *out_explicit) { const char *choice = getenv(FT_TT_RUNTIME_DRIVER_ENVIRONMENT); if (!out_driver) return FT_TT_EINVAL; if (out_explicit) *out_explicit = false; if (!choice || choice[0] == '\0') { *out_driver = FT_TT_RUNTIME_DRIVER_KMD; return FT_TT_OK; } if (strcmp(choice, "kmd") == 0) { *out_driver = FT_TT_RUNTIME_DRIVER_KMD; } else if (strcmp(choice, "udrv") == 0) { *out_driver = FT_TT_RUNTIME_DRIVER_UDRV; } else { /* Not defaulted. A typo here would run a whole round on the transport * the operator was trying to leave, and every number in it would be * attributed to the other one. */ ft_tt_log_error("%s='%s' names no transport — it must be kmd or udrv. REFUSING", FT_TT_RUNTIME_DRIVER_ENVIRONMENT, choice); return FT_TT_EINVAL; } if (out_explicit) *out_explicit = true; return FT_TT_OK; } const char *ft_tt_runtime_driver_string(ft_tt_runtime_driver driver) { switch (driver) { case FT_TT_RUNTIME_DRIVER_KMD: return "kmd"; case FT_TT_RUNTIME_DRIVER_UDRV: return "udrv"; default: return "unknown"; } } int ft_tt_runtime_pci_address(const ft_tt_runtime *runtime, char *out, size_t out_bytes) { ft_tt_device_info info; if (!runtime || !out || out_bytes == 0) return FT_TT_EINVAL; out[0] = '\0'; if (runtime->driver == FT_TT_RUNTIME_DRIVER_UDRV) { const char *address = ft_tt_runtime_udrv_pci_address(runtime->udrv); if (!address || address[0] == '\0') return FT_TT_EUNAVAIL; (void)snprintf(out, out_bytes, "%s", address); return FT_TT_OK; } if (!runtime->ctx || ft_tt_device_get_info(runtime->ctx, &info) != FT_TT_OK) return FT_TT_EUNAVAIL; (void)snprintf(out, out_bytes, "%04x:%02x:%02x.%x", info.pci_domain, info.pci_bus, info.pci_device, info.pci_function); return FT_TT_OK; } int ft_tt_runtime_window_stats_read(const ft_tt_runtime *runtime, ft_tt_runtime_window_stats *out) { if (!out) return FT_TT_EINVAL; memset(out, 0, sizeof(*out)); if (!runtime) return FT_TT_EINVAL; if (runtime->driver == FT_TT_RUNTIME_DRIVER_UDRV) return ft_tt_runtime_udrv_window_stats_read(runtime->udrv, out); out->register_read = runtime->binding.path.bytes_read; out->register_written = runtime->binding.path.bytes_written; out->register_aims = ft_tt_noc_path_aim_count(&runtime->binding.path); out->bulk_read = runtime->bulk_path.bytes_read; out->bulk_written = runtime->bulk_path.bytes_written; out->bulk_aims = ft_tt_noc_path_aim_count(&runtime->bulk_path); /* tt-kmd's CONFIGURE_TLB is a syscall that reports its own failures, so * there is no readback to disagree and no counter for one. Zero here means * "not applicable", not "none happened" — the udrv arm is where it counts. */ out->aim_failures = 0u; return FT_TT_OK; } uint64_t ft_tt_runtime_dram_bytes(const ft_tt_runtime *runtime) { if (!runtime || !runtime->grid.valid || !runtime->grid.arch) return 0u; /* Both factors are live: the bank count from telemetry, the bank size from * the selected architecture. This is ft_tt_mem_info()'s own arithmetic with * the context — which one of the two transports does not have — removed. */ return (uint64_t)runtime->grid.dram_bank_count * (uint64_t)runtime->grid.arch->core.dram_bytes_per_bank; } /** The tt-kmd half of the open: chardev, telemetry, two ioctl-allocated windows. */ static int runtime_open_kmd(int device_index, ft_tt_runtime *runtime) { int rc; runtime->ctx = ft_tt_init_device(device_index); if (!runtime->ctx) { ft_tt_log_error("tt runtime: could not open device %d", device_index); return FT_TT_ENODEV; } rc = ft_tt_arc_telemetry_read_grid(runtime->ctx, &runtime->grid); if (rc != FT_TT_OK) { ft_tt_log_error("tt runtime: telemetry read failed: %s", ft_tt_error_string(rc)); return rc; } /* A topology we cannot map is a topology we must not probe: a wrong NoC endpoint hangs * the chip and costs a power cycle (spec §7.3). Refuse before claiming a window. */ rc = ft_tt_coord_check_grid(&runtime->grid); if (rc != FT_TT_OK) { ft_tt_log_error("tt runtime: grid is not a topology we can map; refusing the card"); return rc; } rc = ft_tt_tlb_window_alloc(runtime->ctx, FT_TT_RUNTIME_WINDOW_BYTES, FT_TT_KMD_CACHE_UC, &runtime->window); if (rc != FT_TT_OK) return rc; rc = ft_tt_launch_noc_over_window(runtime->ctx, runtime->window, &runtime->binding, &runtime->noc); if (rc != FT_TT_OK) return rc; /* A SECOND window, write-combined, for bulk DRAM. There are 201 and the backend uses * two, so this is not a scarce resource; what it buys is that data stops inheriting the * register accessor's 4-byte width. */ rc = ft_tt_noc_path_open(runtime->ctx, FT_TT_KMD_CACHE_WC, &runtime->bulk_path); if (rc != FT_TT_OK) return rc; runtime->bulk_dram.write = bulk_dram_write; runtime->bulk_dram.read = bulk_dram_read; runtime->bulk_l1.write = bulk_l1_write; runtime->bulk_l1.read = bulk_l1_read; return FT_TT_OK; } int ft_tt_runtime_open(int device_index, ft_tt_runtime *out_runtime) { ft_tt_runtime_driver driver = FT_TT_RUNTIME_DRIVER_KMD; int rc = ft_tt_runtime_driver_select(&driver, NULL); if (rc != FT_TT_OK) return rc; return ft_tt_runtime_open_with_driver(device_index, driver, out_runtime); } int ft_tt_runtime_open_with_driver(int device_index, ft_tt_runtime_driver driver, ft_tt_runtime *out_runtime) { int rc; if (!out_runtime) return FT_TT_EINVAL; if (driver != FT_TT_RUNTIME_DRIVER_KMD && driver != FT_TT_RUNTIME_DRIVER_UDRV) return FT_TT_EINVAL; /* Filled IN PLACE rather than in a stack copy: on the tt-kmd path the * accessors' `user` pointers are into the runtime itself, and a copy after * the fact is what makes them need re-binding. Building here removes that * whole class of mistake, and the udrv path's pointers are into its own * heap handle and never needed it. */ memset(out_runtime, 0, sizeof(*out_runtime)); out_runtime->driver = driver; if (driver == FT_TT_RUNTIME_DRIVER_UDRV) rc = ft_tt_runtime_udrv_open(device_index, &out_runtime->udrv, &out_runtime->grid, &out_runtime->noc, &out_runtime->bulk_dram, &out_runtime->bulk_l1); else rc = runtime_open_kmd(device_index, out_runtime); if (rc != FT_TT_OK) { /* NO FALLBACK. An explicit transport that cannot be honoured is a * refusal; the caller decides whether that ends the process, and * ft_tt_runtime_driver_select()'s `explicit` flag is how it tells an * absent card from a broken instruction. */ ft_tt_runtime_close(out_runtime); return rc; } if (driver == FT_TT_RUNTIME_DRIVER_KMD) { out_runtime->noc.user = &out_runtime->binding; out_runtime->bulk_dram.user = &out_runtime->bulk_path; out_runtime->bulk_l1.user = &out_runtime->bulk_path; } return FT_TT_OK; } void ft_tt_runtime_close(ft_tt_runtime *runtime) { if (!runtime) return; if (runtime->driver == FT_TT_RUNTIME_DRIVER_UDRV) { ft_tt_runtime_udrv_close(runtime->udrv); } else { (void)ft_tt_noc_path_close(&runtime->bulk_path); if (runtime->window && runtime->ctx) (void)ft_tt_tlb_window_free(runtime->ctx, runtime->window); ft_tt_ctx_free(runtime->ctx); } memset(runtime, 0, sizeof(*runtime)); } int ft_tt_runtime_boot_core(const ft_tt_noc_access *noc, uint16_t x, uint16_t y, const ft_tt_runtime_images *firmware) { unsigned processor; int rc; if (!noc || !firmware) return FT_TT_EINVAL; /* The five RISC-Vs and NOTHING ELSE. The same register has bits for the matrix backend, * and asserting them here corrupts that core's compute permanently — see the warning * block in ft_tt_launch.h. The firmware brings the backend up itself. */ rc = ft_tt_launch_assert_reset(noc, x, y, FT_TT_SOFT_RESET_ALL_RISCS); if (rc != FT_TT_OK) return rc; rc = ft_tt_launch_clear_mailbox(noc, x, y); if (rc != FT_TT_OK) return rc; for (processor = 0; processor < FT_TT_PROC_COUNT; processor++) { rc = ft_tt_launch_load_image(noc, x, y, processor, FT_TT_IMAGE_FIRMWARE, firmware->elf_data[processor], firmware->elf_size[processor], NULL); if (rc != FT_TT_OK) return rc; } /* RUN_MSG_DONE is 0, so a cleared mailbox already reads DONE. Without this the boot * wait below is vacuous — it would pass on a dead core. */ rc = ft_tt_launch_prime_firmware_handshake(noc, x, y); if (rc != FT_TT_OK) return rc; /* BRISC only: its firmware releases the other four itself, and releasing them from the * host races it. */ rc = ft_tt_launch_deassert_reset(noc, x, y, FT_TT_SOFT_RESET_BRISC, false); if (rc != FT_TT_OK) return rc; return ft_tt_launch_wait_firmware_ready(noc, x, y, FT_TT_RUNTIME_BOOT_TIMEOUT_MS); }