/* SPDX-License-Identifier: AGPL-3.0-or-later */ /* Copyright (C) 2026 David (Corey) Herrera */ /** * @file test_noc_copy.c * @brief Off-hardware gate for `ft_tt_noc_copy.h` — the PIO copy shape and the * window-walk arithmetic that BOTH transports now share. * * This is where the shared code earns the right to be shared. The tt-kmd * transport and the userspace-driver transport differ only in how a window is * aimed; everything that moves a byte is this header, and this file proves it * over host memory with no device anywhere in the source list. * * THE FIXTURE PATTERN IS DELIBERATELY NON-PERIODIC, and the first thing this * file checks is that it is. A 256-periodic fixture made every page identical * in an earlier round here and turned a whole battery of byte comparisons * vacuous — a copy that dropped a block, doubled one, or was off by 256 bytes * would still have compared equal. So the generator is an LCG over the * absolute offset, and its periodicity is a CHECK, not a comment. * * WHAT THIS CANNOT TEST, stated so nobody assumes otherwise: that the store * shape reaching a device is 16 B units in 256 B blocks (that is an * instruction-selection property, checked in the disassembly on the card host), * and that `ft_tt_noc_copy_store_fence()` retires a write-combining buffer — * there are no WC buffers over ordinary host memory. Both are on-card * questions and `test_udrv_card_noc.c` answers the second. * * @status Runs anywhere. No device, no root, no card. * @issues None * @todo None */ #include #include #include #include #include #include "ft_tt_noc_copy.h" #include "ft_tt_test_report.h" /** A 2 MiB aperture, the only window size this board has. */ #define WINDOW_BYTES ((size_t)2u * 1024u * 1024u) /** Bytes of guard either side of every destination buffer. A copy that writes * one byte too many is a silent corruption of a neighbour, which is exactly * the failure the window walk can produce, so every buffer is fenced. */ #define GUARD_BYTES 256u /** * Guard filler, and THE TWO MUST DIFFER. * * This was a vacuous gate for one build of this file and the mutation battery * is what found it: with both buffers guarded by the same byte, a copy that * overran its destination by one unit read that unit out of the SOURCE's guard * and wrote the identical value into the DESTINATION's, so an over-run of * exactly the shape the walk can produce left every guard intact and every * check green. Giving the source a different filler makes the over-run write a * byte the destination guard cannot already hold. */ #define GUARD_BYTE 0xC7u #define SOURCE_GUARD_BYTE 0x3Eu /** * The fixture pattern: an LCG folded over the ABSOLUTE offset, so no two * 16-byte units, 256-byte blocks or 4 KiB pages are alike. */ static unsigned char pattern_byte(size_t offset, uint32_t salt) { uint32_t state = (uint32_t)(offset + 1u) * 2654435761u + salt; state ^= state >> 15; state *= 2246822519u; state ^= state >> 13; return (unsigned char)(state & 0xFFu); } static void fill_pattern(unsigned char *buffer, size_t base_offset, size_t bytes, uint32_t salt) { size_t index; for (index = 0u; index < bytes; index++) buffer[index] = pattern_byte(base_offset + index, salt); } /** Whether `bytes` of the pattern repeat with period `period`. */ static bool pattern_repeats_at(size_t bytes, size_t period, uint32_t salt) { size_t index; if (period == 0u || bytes <= period) return false; for (index = 0u; index + period < bytes; index++) { if (pattern_byte(index, salt) != pattern_byte(index + period, salt)) return false; } return true; } /* ------------------------------------------------------------------------ * * The fixture is not vacuous * ------------------------------------------------------------------------ */ static void stage_the_fixture_discriminates(void) { const size_t span = 64u * 1024u; size_t distinct_units = 0u; size_t index; size_t probe; ft_tt_test_step("The fixture pattern — a comparison against a periodic pattern proves nothing"); ft_tt_test_check("not 16-periodic (the copy UNIT)", !pattern_repeats_at(span, 16u, 0u), ""); ft_tt_test_check("not 256-periodic (the copy BLOCK — the exact shape that made a battery " "vacuous here once)", !pattern_repeats_at(span, 256u, 0u), ""); ft_tt_test_check("not 4096-periodic (a host page)", !pattern_repeats_at(span, 4096u, 0u), ""); /* And that two different 16 B units really do differ, sampled across the * whole span rather than at one lucky offset. */ for (probe = 0u; probe + 32u <= span; probe += 4096u) { bool differs = false; for (index = 0u; index < 16u; index++) { if (pattern_byte(probe + index, 0u) != pattern_byte(probe + 16u + index, 0u)) differs = true; } if (differs) distinct_units++; } ft_tt_test_check_fmt("every sampled pair of adjacent 16 B units differs", distinct_units == span / 4096u, "%zu of %zu", distinct_units, span / 4096u); ft_tt_test_check("two salts give different bytes — a second write is distinguishable from " "the first", pattern_byte(0u, 0u) != pattern_byte(0u, 1u) || pattern_byte(1u, 0u) != pattern_byte(1u, 1u), ""); } /* ------------------------------------------------------------------------ * * Width * ------------------------------------------------------------------------ */ static void stage_widths(void) { ft_tt_test_step("Transfer widths"); ft_tt_test_check_fmt("BLOCK16 is 16 bytes", ft_tt_noc_transfer_width_bytes(FT_TT_NOC_WIDTH_BLOCK16) == 16u, "%u", ft_tt_noc_transfer_width_bytes(FT_TT_NOC_WIDTH_BLOCK16)); ft_tt_test_check_fmt("WORD32 is 4 bytes", ft_tt_noc_transfer_width_bytes(FT_TT_NOC_WIDTH_WORD32) == 4u, "%u", ft_tt_noc_transfer_width_bytes(FT_TT_NOC_WIDTH_WORD32)); ft_tt_test_check_fmt("an unknown width is 0, not a plausible default", ft_tt_noc_transfer_width_bytes((ft_tt_noc_transfer_width)7) == 0u, "%u", ft_tt_noc_transfer_width_bytes((ft_tt_noc_transfer_width)7)); ft_tt_test_check_fmt("the block is 16 units of the unit size", FT_TT_NOC_COPY_BLOCK_BYTES == 16u * FT_TT_NOC_COPY_UNIT_BYTES, "%u / %u", FT_TT_NOC_COPY_BLOCK_BYTES, FT_TT_NOC_COPY_UNIT_BYTES); } /* ------------------------------------------------------------------------ * * The window walk * ------------------------------------------------------------------------ */ static void stage_window_slice(void) { ft_tt_noc_window_slice slice; ft_tt_test_step("The window walk — pure arithmetic, and the half that corrupts a neighbour " "rather than failing"); memset(&slice, 0, sizeof(slice)); ft_tt_test_check("a NULL out is refused", !ft_tt_noc_window_slice_compute(0u, 16u, WINDOW_BYTES, NULL), ""); ft_tt_test_check("a window size that is not a power of two is refused", !ft_tt_noc_window_slice_compute(0u, 16u, 3u, &slice), ""); ft_tt_test_check("a zero window size is refused", !ft_tt_noc_window_slice_compute(0u, 16u, 0u, &slice), ""); (void)ft_tt_noc_window_slice_compute(0u, 4096u, WINDOW_BYTES, &slice); ft_tt_test_check_fmt("a transfer inside one window takes one slice at offset 0", slice.window_base == 0u && slice.offset_in_window == 0u && slice.bytes == 4096u, "base 0x%llx off %zu bytes %zu", (unsigned long long)slice.window_base, slice.offset_in_window, slice.bytes); (void)ft_tt_noc_window_slice_compute(WINDOW_BYTES + 4096u, 256u, WINDOW_BYTES, &slice); ft_tt_test_check_fmt("an address in the second window bases there, not at zero", slice.window_base == WINDOW_BYTES && slice.offset_in_window == 4096u && slice.bytes == 256u, "base 0x%llx off %zu", (unsigned long long)slice.window_base, slice.offset_in_window); (void)ft_tt_noc_window_slice_compute(WINDOW_BYTES - 16u, 64u, WINDOW_BYTES, &slice); ft_tt_test_check_fmt("a transfer straddling a boundary is CUT at the boundary", slice.window_base == 0u && slice.offset_in_window == WINDOW_BYTES - 16u && slice.bytes == 16u, "%zu bytes of 64 in this slice", slice.bytes); (void)ft_tt_noc_window_slice_compute(WINDOW_BYTES, 16u, WINDOW_BYTES, &slice); ft_tt_test_check_fmt("an address exactly ON a boundary starts the next window at offset 0", slice.window_base == WINDOW_BYTES && slice.offset_in_window == 0u, "base 0x%llx off %zu", (unsigned long long)slice.window_base, slice.offset_in_window); /* Walk a 5 MiB transfer the way a transport does and require the slices to * TILE the span: contiguous, no gap, no overlap, no zero-length step. */ { const size_t total = 5u * 1024u * 1024u; uint64_t address = WINDOW_BYTES - 4096u; uint64_t expect = address; size_t remaining = total; size_t steps = 0u; size_t covered = 0u; bool contiguous = true; while (remaining > 0u && steps < 64u) { if (!ft_tt_noc_window_slice_compute(address, remaining, WINDOW_BYTES, &slice)) break; if (slice.bytes == 0u) break; if (slice.window_base + slice.offset_in_window != expect) contiguous = false; expect += slice.bytes; address += slice.bytes; covered += slice.bytes; remaining -= slice.bytes; steps++; } ft_tt_test_check_fmt("a 5 MiB transfer is covered exactly", covered == total, "%zu of %zu", covered, total); ft_tt_test_check("...by contiguous slices with no gap and no overlap", contiguous, ""); ft_tt_test_check_fmt("...and it really did move the window, more than once", steps == 4u, "%zu slices", steps); } } /* ------------------------------------------------------------------------ * * The copy * ------------------------------------------------------------------------ */ /** Allocate a guarded buffer: GUARD_BYTES of `filler`, `bytes` of payload, GUARD_BYTES. */ static unsigned char *guarded_alloc(size_t bytes, unsigned char filler) { unsigned char *block = malloc(bytes + 2u * GUARD_BYTES); if (!block) return NULL; memset(block, filler, bytes + 2u * GUARD_BYTES); return block; } static bool guards_intact(const unsigned char *block, size_t bytes) { size_t index; for (index = 0u; index < GUARD_BYTES; index++) { if (block[index] != GUARD_BYTE) return false; if (block[GUARD_BYTES + bytes + index] != GUARD_BYTE) return false; } return true; } /** * One copy in one direction at one length, byte-compared, with the guards * either side of the destination checked for over-run. */ static void check_one_copy(size_t bytes, bool is_read, ft_tt_noc_transfer_width width, const char *label) { unsigned char *source_block = guarded_alloc(bytes, SOURCE_GUARD_BYTE); unsigned char *destination_block = guarded_alloc(bytes, GUARD_BYTE); unsigned char *source; unsigned char *destination; bool equal; if (!source_block || !destination_block) { ft_tt_test_check_fmt("allocation", false, "%s", label); free(source_block); free(destination_block); return; } source = source_block + GUARD_BYTES; destination = destination_block + GUARD_BYTES; fill_pattern(source, 0u, bytes, 0u); /* `aperture` is host memory here. Which side of the copy it is decides * nothing about the shape — the same units and blocks are issued either * way — so both directions are exercised through the same entry point. */ if (is_read) ft_tt_noc_copy_slice((volatile unsigned char *)source, destination, bytes, true, width); else ft_tt_noc_copy_slice((volatile unsigned char *)destination, source, bytes, false, width); equal = memcmp(source, destination, bytes) == 0; ft_tt_test_check_fmt(label, equal, "%zu bytes, %s", bytes, equal ? "byte-exact" : "MISMATCH"); ft_tt_test_check_fmt("...and nothing outside the destination was written", guards_intact(destination_block, bytes), "%s, %u B guard either side", label, GUARD_BYTES); free(source_block); free(destination_block); } static void stage_the_copy(void) { ft_tt_test_step("The PIO copy — every length class, both directions, guarded"); /* Lengths chosen against the shape: under one unit's block, exactly one * block, a block plus a tail, several blocks, and a page. */ check_one_copy(16u, false, FT_TT_NOC_WIDTH_BLOCK16, "block16 write, one unit"); check_one_copy(240u, false, FT_TT_NOC_WIDTH_BLOCK16, "block16 write, one unit short of a block"); check_one_copy(256u, false, FT_TT_NOC_WIDTH_BLOCK16, "block16 write, exactly one block"); check_one_copy(272u, false, FT_TT_NOC_WIDTH_BLOCK16, "block16 write, a block and a tail"); check_one_copy(4096u, false, FT_TT_NOC_WIDTH_BLOCK16, "block16 write, a page"); check_one_copy(65536u, false, FT_TT_NOC_WIDTH_BLOCK16, "block16 write, 64 KiB"); check_one_copy(4096u, true, FT_TT_NOC_WIDTH_BLOCK16, "block16 read, a page"); check_one_copy(272u, true, FT_TT_NOC_WIDTH_BLOCK16, "block16 read, a block and a tail"); check_one_copy(4u, false, FT_TT_NOC_WIDTH_WORD32, "word32 write, one word"); check_one_copy(64u, false, FT_TT_NOC_WIDTH_WORD32, "word32 write, sixteen words"); check_one_copy(4u, true, FT_TT_NOC_WIDTH_WORD32, "word32 read, one word"); check_one_copy(4096u, true, FT_TT_NOC_WIDTH_WORD32, "word32 read, a page"); /* Zero is a legal no-op and must not touch the destination at all. */ { unsigned char destination[64]; unsigned char source[64]; memset(destination, GUARD_BYTE, sizeof(destination)); memset(source, 0x11u, sizeof(source)); ft_tt_noc_copy_slice((volatile unsigned char *)destination, source, 0u, false, FT_TT_NOC_WIDTH_BLOCK16); ft_tt_test_check("a zero-byte copy writes nothing", destination[0] == GUARD_BYTE, ""); } /* And the comparison can say no. Without this every equality above could be * a statement about two buffers that were already identical. */ { unsigned char source[512]; unsigned char destination[512]; fill_pattern(source, 0u, sizeof(source), 0u); fill_pattern(destination, 0u, sizeof(destination), 1u); ft_tt_test_check("two DIFFERENT patterns compare unequal — the byte comparison " "discriminates", memcmp(source, destination, sizeof(source)) != 0, ""); ft_tt_noc_copy_slice((volatile unsigned char *)destination, source, sizeof(source), false, FT_TT_NOC_WIDTH_BLOCK16); ft_tt_test_check("...and equal once the copy has run", memcmp(source, destination, sizeof(source)) == 0, ""); } } /* ------------------------------------------------------------------------ */ int main(void) { stage_the_fixture_discriminates(); stage_widths(); stage_window_slice(); stage_the_copy(); /* Callable everywhere; there are no write-combining buffers over host * memory, so this is a compile-and-link check, not a behavioural one. Said * out loud rather than dressed up as a gate. */ ft_tt_noc_copy_store_fence(); ft_tt_test_step("(the store fence is exercised on the card, not here — host memory has no " "write-combining buffers to retire)"); return ft_tt_test_finish(); }