diff --git a/Makefile b/Makefile index a9e0f04b..6ba7ce3a 100644 --- a/Makefile +++ b/Makefile @@ -90,6 +90,7 @@ TINY_P8_ACCEPTANCE ?= NO TINY_P9_ACCEPTANCE ?= NO TINY_P9_QUICK ?= NO TINY_P9_WORKLOAD ?= QVGA_RGB565 +TINY_P10_ACCEPTANCE ?= NO COREMARK_MODE ?= quick NPU_P5_ACCEPTANCE ?= NO NPU_P6_ACCEPTANCE ?= NO @@ -147,6 +148,9 @@ endif ifeq ($(TINY_P9_QUICK),YES) CONFIG_KEY_VARS += TINY_P9_QUICK endif +ifeq ($(TINY_P10_ACCEPTANCE),YES) +CONFIG_KEY_VARS += TINY_P10_ACCEPTANCE +endif VARIANT_ID := $(strip $(shell $(VCS_SHELL_PYTHON) $(ROOT_PATH)/scripts/config_key.py \ --lock $(LOCK_FILE) --profile $(PROFILE_NAME) --timestamp $(BUILD_TIMESTAMP) \ $(foreach var,$(CONFIG_KEY_VARS),--value $(var)=$($(var))) | tail -n 1)) @@ -311,6 +315,12 @@ $(call validate_value,TINY_P7_ACCEPTANCE,$(VALID_BOOL)) $(call validate_value,TINY_P8_ACCEPTANCE,$(VALID_BOOL)) $(call validate_value,TINY_P9_ACCEPTANCE,$(VALID_BOOL)) $(call validate_value,TINY_P9_QUICK,$(VALID_BOOL)) +$(call validate_value,TINY_P10_ACCEPTANCE,$(VALID_BOOL)) +ifeq ($(TINY_P10_ACCEPTANCE),YES) +ifneq ($(SOC)/$(APP)/$(HAVE_CSR)/$(TINY_P7_ACCEPTANCE)/$(TINY_P8_ACCEPTANCE)/$(TINY_P9_ACCEPTANCE)/$(TINY_P9_QUICK),TINY/ci_smoke/YES/NO/NO/NO/NO) +$(error TINY_P10_ACCEPTANCE requires Tiny ci_smoke with CSR and no P7/P8/P9 acceptance image) +endif +endif $(call validate_value,TINY_P9_WORKLOAD,QVGA_RGB565 VGA_YUV422 RECOVERY) ifneq ($(TINY_P9_WORKLOAD),QVGA_RGB565) ifneq ($(TINY_P9_ACCEPTANCE)/$(TINY_P9_QUICK),YES/NO) diff --git a/app/apps/ci_smoke/p10_frame.h b/app/apps/ci_smoke/p10_frame.h new file mode 100644 index 00000000..0c729c82 --- /dev/null +++ b/app/apps/ci_smoke/p10_frame.h @@ -0,0 +1,45 @@ +#ifndef RETROSOC_P10_FRAME_H +#define RETROSOC_P10_FRAME_H + +#include +#include +#include + +#include + +/* Application statistics only; these are not DVP or hardware counters. */ +typedef struct { + uint32_t red[32]; + uint32_t pixels; + uint32_t checksum; +} rs_p10_statistics_t; + +static inline rs_status_t rs_p10_accumulate(rs_p10_statistics_t *statistics, uint32_t word) { + if (statistics == NULL || statistics->pixels > UINT32_MAX - 2U) { + return RS_EINVAL; + } + const uint32_t first = (word >> 11U) & UINT32_C(31); + const uint32_t second = word >> 27U; + if (statistics->red[first] == UINT32_MAX || statistics->red[second] == UINT32_MAX || + (first == second && statistics->red[first] == UINT32_MAX - 1U)) { + return RS_EINVAL; + } + statistics->red[first]++; + statistics->red[second]++; + statistics->pixels += 2U; + statistics->checksum = (statistics->checksum ^ word) * UINT32_C(16777619); + return RS_OK; +} + +static inline rs_status_t rs_p10_chunk(uint32_t bytes, uint32_t index, uint32_t capacity_blocks, + uint32_t *word_offset, uint32_t *block) { + if (word_offset == NULL || block == NULL || bytes == 0U || bytes % 1024U != 0U || + index >= bytes / 1024U || capacity_blocks < 16U || bytes / 512U > capacity_blocks - 16U) { + return RS_EINVAL; + } + *word_offset = index * 256U; + *block = 16U + index * 2U; + return RS_OK; +} + +#endif diff --git a/app/apps/ci_smoke/tiny.mk b/app/apps/ci_smoke/tiny.mk index efd7445e..0e8403ec 100644 --- a/app/apps/ci_smoke/tiny.mk +++ b/app/apps/ci_smoke/tiny.mk @@ -19,4 +19,7 @@ endif ifeq ($(TINY_P9_QUICK),YES) APP_CFLAGS += -DRS_TINY_P9_QUICK endif +endif +ifeq ($(TINY_P10_ACCEPTANCE),YES) +APP_SRCS := $(ROOT_PATH)/app/apps/ci_smoke/tiny_r2_p10.c endif \ No newline at end of file diff --git a/app/apps/ci_smoke/tiny_r2_p10.c b/app/apps/ci_smoke/tiny_r2_p10.c new file mode 100644 index 00000000..13ad6f2f --- /dev/null +++ b/app/apps/ci_smoke/tiny_r2_p10.c @@ -0,0 +1,263 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "p9_frame.h" +#include "p9_startup.h" +#include "p10_frame.h" + +#define RS_P10_FRAME_ADDRESS UINT32_C(0x54010000) +#define RS_P10_BYTES UINT32_C(153600) +#define RS_P10_WORDS (RS_P10_BYTES / 4U) +#define RS_P10_GUARD UINT32_C(0x5A36C9E7) + +typedef struct { + uint32_t before[16]; + uint32_t words[256]; + uint32_t after[16]; +} rs_p10_buffer_t; + +typedef struct { + uint64_t cycles; + uint64_t instructions; + uint32_t bytes; + uint32_t calls; +} rs_p10_window_t; + +static _Alignas(64) rs_p10_buffer_t rs_p10_transmit; +static _Alignas(64) rs_p10_buffer_t rs_p10_receive; +static rs_p10_statistics_t rs_p10_statistics = {.checksum = UINT32_C(2166136261)}; +static rs_p10_window_t rs_p10_windows[5]; +static uint64_t rs_p10_cycle_start; +static uint64_t rs_p10_instruction_start; + +static uint64_t rs_p10_cycles(void) { + uint32_t high, low, again; + do { + high = __RV_CSR_READ(CSR_CYCLEH); + low = __RV_CSR_READ(CSR_CYCLE); + again = __RV_CSR_READ(CSR_CYCLEH); + } while (high != again); + return ((uint64_t)high << 32U) | low; +} + +static uint64_t rs_p10_instructions(void) { + uint32_t high, low, again; + do { + high = __RV_CSR_READ(CSR_INSTRETH); + low = __RV_CSR_READ(CSR_INSTRET); + again = __RV_CSR_READ(CSR_INSTRETH); + } while (high != again); + return ((uint64_t)high << 32U) | low; +} + +static bool rs_p10_begin(void) { + if (rs_sysctrl_set_perf_control(true, true, false) != RS_OK) { + return false; + } + rs_p10_cycle_start = rs_p10_cycles(); + rs_p10_instruction_start = rs_p10_instructions(); + return true; +} + +static bool rs_p10_end(uint32_t phase, uint32_t bytes) { + const uint64_t instructions = rs_p10_instructions() - rs_p10_instruction_start; + const uint64_t cycles = rs_p10_cycles() - rs_p10_cycle_start; + if (phase >= 5U || rs_sysctrl_set_perf_control(false, false, true) != RS_OK) { + return false; + } + rs_p10_windows[phase].cycles += cycles; + rs_p10_windows[phase].instructions += instructions; + rs_p10_windows[phase].bytes += bytes; + rs_p10_windows[phase].calls++; + return true; +} + +static bool rs_p10_guards(void) { + const volatile uint32_t *const frame = (const volatile uint32_t *)RS_P10_FRAME_ADDRESS; + if (*(const volatile uint32_t *)(RS_P10_FRAME_ADDRESS - 4U) != RS_P10_GUARD || + frame[RS_P10_WORDS] != RS_P10_GUARD) { + return false; + } + for (uint32_t index = 0U; index < 16U; ++index) { + if (rs_p10_transmit.before[index] != RS_P10_GUARD || + rs_p10_transmit.after[index] != RS_P10_GUARD || + rs_p10_receive.before[index] != RS_P10_GUARD || + rs_p10_receive.after[index] != RS_P10_GUARD) { + return false; + } + } + return true; +} + +static bool rs_p10_pins(uint32_t first, uint32_t last, rs_gpio_mode_t mode) { + const rs_gpio_config_t config = { + .mode = mode, + .pull = RS_GPIO_PULL_NONE, + .trigger = RS_GPIO_TRIGGER_NONE, + }; + for (uint32_t pin = first; pin <= last; ++pin) { + if (rs_gpio_configure(pin, &config) != RS_OK) { + return false; + } + } + return true; +} + +static uint8_t rs_p10_capture(void) { + const rs_dvp_config_t config = { + .format = RS_DVP_FORMAT_RGB565, + .frame_width = 320U, + .frame_height = 240U, + .crop_width = 320U, + .crop_height = 240U, + .snapshot = true, + .stream_enable = true, + }; + rs_dvp_status_t dvp; + rs_dma_status_t dma; + rs_xpi_error_t xpi; + if (rs_dvp_configure(&config) != RS_OK || !rs_p10_begin()) { + return 3U; + } + const rs_status_t result = + rs_dvp_capture_dma(RS_P10_FRAME_ADDRESS, RS_P10_WORDS, RS_TIMEOUT_DEFAULT); + if (!rs_p10_end(0U, RS_P10_BYTES) || result != RS_OK || rs_dvp_get_status(&dvp) != RS_OK || + rs_dma_get_status(RS_DMA_CHANNEL_DVP, &dma) != RS_OK || rs_xpi_error_get(&xpi) != RS_OK || + xpi.valid || dvp.error_status != 0U || dvp.active || dvp.pixel_count != 76800U || + dvp.word_count != RS_P10_WORDS || !dma.done || dma.busy || dma.error || + dma.bytes_done != RS_P10_BYTES || !rs_p10_guards()) { + return 3U; + } + /* No further camera frame or DMA work is admitted during processing/save. */ + return rs_dvp_abort() == RS_OK ? 0U : 3U; +} + +static uint8_t rs_p10_process(void) { + const volatile uint32_t *const frame = (const volatile uint32_t *)RS_P10_FRAME_ADDRESS; + if (!rs_p10_begin()) { + return 4U; + } + for (uint32_t index = 0U; index < RS_P10_WORDS; ++index) { + const uint32_t word = frame[index]; + if (word != rs_p9_frame_word(index / 160U, index % 160U, false) || + rs_p10_accumulate(&rs_p10_statistics, word) != RS_OK) { + (void)rs_p10_end(1U, index * 4U); + return 4U; + } + } + return rs_p10_end(1U, RS_P10_BYTES) && rs_p10_guards() ? 0U : 4U; +} + +static uint8_t rs_p10_save(const rs_sd_memory_info_t *card) { + const volatile uint32_t *const frame = (const volatile uint32_t *)RS_P10_FRAME_ADDRESS; + for (uint32_t chunk = 0U; chunk < 150U; ++chunk) { + uint32_t offset, block; + if (rs_p10_chunk(RS_P10_BYTES, chunk, card->capacity_blocks, &offset, &block) != RS_OK || + !rs_p10_begin()) { + return 5U; + } + for (uint32_t word = 0U; word < 256U; ++word) { + rs_p10_transmit.words[word] = frame[offset + word]; + rs_p10_receive.words[word] = UINT32_C(0xDEADBEEF); + } + if (!rs_p10_end(2U, 1024U) || !rs_p10_begin()) { + return 5U; + } + rs_status_t result = rs_sd_memory_write_blocks(RS_SDIO_INSTANCE_0, card, block, 2U, + rs_p10_transmit.words, RS_TIMEOUT_DEFAULT); + if (!rs_p10_end(3U, 1024U) || result != RS_OK || !rs_p10_begin()) { + return 5U; + } + result = rs_sd_memory_read_blocks(RS_SDIO_INSTANCE_0, card, block, 2U, rs_p10_receive.words, + RS_TIMEOUT_DEFAULT); + bool matches = result == RS_OK; + for (uint32_t word = 0U; word < 256U; ++word) { + const uint32_t index = offset + word; + if (rs_p10_receive.words[word] != rs_p9_frame_word(index / 160U, index % 160U, false)) { + matches = false; + } + } + if (!rs_p10_end(4U, 1024U) || !matches || !rs_p10_guards()) { + return 6U; + } + if (chunk % 32U == 0U) { + printf("P10_PROGRESS chunks=%u\n", chunk + 1U); + } + } + return 0U; +} + +int main(void) { + rs_tiny_rcu_snapshot_t clock; + rs_xpi_psram_t psram; + rs_sd_memory_info_t card; + uint8_t code = 0U; + __disable_irq(); + if (rs_tiny_rcu_get_snapshot(&clock) != RS_OK || !rs_p9_console_ready(&clock, rs_uart_init)) { + rs_sysctrl_write_test_status(false, 1U); + for (;;) { + } + } + if (clock.sys_hz != 24000000U || clock.mem_hz != 24000000U || clock.pclk_hz != 24000000U || + !rs_p9_psram_pin_ready(rs_gpio_configure) || !rs_p10_pins(12U, 23U, RS_GPIO_MODE_ALT1) || + !rs_p10_pins(0U, 5U, RS_GPIO_MODE_ALT0) || rs_tiny_rcu_set_clkout(1U, 1U) != RS_OK || + rs_xpi_psram_initialize(clock.mem_hz, RS_TIMEOUT_DEFAULT, &psram) != RS_OK || + rs_xpi_psram_validate_dma(&psram, RS_P10_FRAME_ADDRESS, RS_P10_BYTES, 4U) != RS_OK || + rs_sd_memory_initialize(RS_SDIO_INSTANCE_0, clock.pclk_hz, &card, RS_TIMEOUT_DEFAULT) != + RS_OK || + card.capacity_blocks < 316U || card.actual_clock_hz != 12000000U) { + code = 2U; + } else { + *(volatile uint32_t *)(RS_P10_FRAME_ADDRESS - 4U) = RS_P10_GUARD; + ((volatile uint32_t *)RS_P10_FRAME_ADDRESS)[RS_P10_WORDS] = RS_P10_GUARD; + for (uint32_t index = 0U; index < 16U; ++index) { + rs_p10_transmit.before[index] = RS_P10_GUARD; + rs_p10_transmit.after[index] = RS_P10_GUARD; + rs_p10_receive.before[index] = RS_P10_GUARD; + rs_p10_receive.after[index] = RS_P10_GUARD; + } + printf("P10_STAGE capture\n"); + code = rs_p10_capture(); + if (code == 0U) { + printf("P10_STAGE process\n"); + code = rs_p10_process(); + } + if (code == 0U) { + printf("P10_STAGE save\n"); + code = rs_p10_save(&card); + } + if (code == 0U) { + rs_xpi_error_t error; + rs_sdio_status_t storage; + if (rs_xpi_error_get(&error) != RS_OK || error.valid || + rs_sdio_status_get(RS_SDIO_INSTANCE_0, &storage) != RS_OK || + storage.error_status != 0U || storage.busy || !rs_p10_guards()) { + code = 7U; + } + } + } + (void)rs_sysctrl_set_perf_control(false, false, true); + for (uint32_t phase = 0U; phase < 5U; ++phase) { + printf("P10_PHASE id=%u calls=%u bytes=%u cycles=%llu instructions=%llu\n", phase, + rs_p10_windows[phase].calls, rs_p10_windows[phase].bytes, + (unsigned long long)rs_p10_windows[phase].cycles, + (unsigned long long)rs_p10_windows[phase].instructions); + } + for (uint32_t bin = 0U; bin < 32U; ++bin) { + printf("P10_HIST bin=%u pixels=%u\n", bin, rs_p10_statistics.red[bin]); + } + printf("P10_RESULT code=%u pixels=%u bytes=%u sectors=%u checksum=%u\n", code, + rs_p10_statistics.pixels, code == 0U ? RS_P10_BYTES : 0U, code == 0U ? 300U : 0U, + rs_p10_statistics.checksum); + (void)rs_tiny_rcu_set_clkout(0U, 0U); + rs_sysctrl_write_test_status(code == 0U, code); + for (;;) { + } +} diff --git a/rtl/tiny/dv/retrosoc_tiny_tb.sv b/rtl/tiny/dv/retrosoc_tiny_tb.sv index ad3e6bac..b3225e29 100644 --- a/rtl/tiny/dv/retrosoc_tiny_tb.sv +++ b/rtl/tiny/dv/retrosoc_tiny_tb.sv @@ -75,6 +75,7 @@ module retrosoc_tiny_tb; `include "tiny_p8_observers.svh" `include "tiny_p9_observers.svh" `include "tiny_p9_recovery.svh" + `include "tiny_p10_observers.svh" `endif integer s_max_cycles = 20000000; integer s_done_cycles = 0; diff --git a/rtl/tiny/dv/tiny_p10_observer.sv b/rtl/tiny/dv/tiny_p10_observer.sv new file mode 100644 index 00000000..6f770d93 --- /dev/null +++ b/rtl/tiny/dv/tiny_p10_observer.sv @@ -0,0 +1,134 @@ +// Verification-only transaction accounting. No DUT signal is driven. +module tiny_p10_observer #( + parameter integer Owners = 3 +) ( + input logic clk_i, + input logic rst_n_i, + input logic enable_i, + input logic [Owners-1:0] read_accept_i, + input logic [Owners-1:0] read_done_i, + input logic [Owners-1:0] write_accept_i, + input logic [Owners-1:0] write_done_i, + input logic [Owners-1:0] error_i, + input logic [Owners-1:0][7:0] read_bytes_i, + input logic [Owners-1:0][7:0] write_bytes_i, + input logic [Owners-1:0][4:0] stalled_i, + input logic [ 5:0] active_targets_i, + input logic [ 3:0] bank_issues_i, + input logic dma_beat_overlap_i +); + longint unsigned s_cycle = 0; + integer s_window = 0; + logic s_previous = 0; + logic [Owners-1:0] s_read_pending = 0, s_write_pending = 0; + longint unsigned s_read_start[Owners], s_write_start[Owners]; + longint unsigned s_reads[Owners], s_writes[Owners], s_read_done[Owners], s_write_done[Owners]; + longint unsigned s_read_bytes[Owners], s_write_bytes[Owners], s_errors[Owners]; + longint unsigned s_read_max[Owners], s_write_max[Owners]; + longint unsigned s_stall[Owners][5], s_stall_max[Owners][5]; + logic [Owners-1:0] s_read_initial = 0, s_write_initial = 0; + integer s_active_max = 0; + longint unsigned s_bank_overlap = 0, s_dma_overlap = 0; + integer s_bank_count; + + always @(posedge clk_i) begin + if (!rst_n_i) begin + s_previous = 0; + s_read_pending = 0; + s_write_pending = 0; + s_window = 0; + s_cycle = 0; + end else begin + s_cycle++; + if (enable_i && !s_previous) begin + s_window++; + s_read_initial = s_read_pending; + s_write_initial = s_write_pending; + s_active_max = 0; + s_bank_overlap = 0; + s_dma_overlap = 0; + for (integer owner = 0; owner < Owners; owner++) begin + s_reads[owner] = 0; + s_writes[owner] = 0; + s_read_done[owner] = 0; + s_write_done[owner] = 0; + s_read_bytes[owner] = 0; + s_write_bytes[owner] = 0; + s_errors[owner] = 0; + s_read_max[owner] = 0; + s_write_max[owner] = 0; + for (integer channel = 0; channel < 5; channel++) begin + s_stall[owner][channel] = 0; + s_stall_max[owner][channel] = 0; + end + end + end + if (!enable_i && s_previous) begin + for (integer owner = 0; owner < Owners; owner++) begin + $display( + "P10_BUS window=%0d owner=%0d reads=%0d writes=%0d read_done=%0d write_done=%0d read_initial=%0d write_initial=%0d read_pending=%0d write_pending=%0d read_bytes=%0d write_bytes=%0d errors=%0d read_latency_max=%0d write_latency_max=%0d ar_max=%0d r_max=%0d aw_max=%0d w_max=%0d b_max=%0d", + s_window, owner, s_reads[owner], s_writes[owner], s_read_done[owner], + s_write_done[owner], s_read_initial[owner], s_write_initial[owner], + s_read_pending[owner], s_write_pending[owner], s_read_bytes[owner], + s_write_bytes[owner], s_errors[owner], s_read_max[owner], s_write_max[owner], + s_stall_max[owner][0], s_stall_max[owner][1], s_stall_max[owner][2], + s_stall_max[owner][3], s_stall_max[owner][4]); + end + $display( + "P10_CONCURRENCY window=%0d active_targets_max=%0d bank_overlap=%0d dma_beat_overlap=%0d", + s_window, s_active_max, s_bank_overlap, s_dma_overlap); + end + for (integer owner = 0; owner < Owners; owner++) begin + if (read_accept_i[owner]) begin + if (s_read_pending[owner]) $fatal(1, "SIM_TEST_FAIL P10 duplicate AR"); + s_read_pending[owner] = 1; + s_read_start[owner] = s_cycle; + if (enable_i) s_reads[owner]++; + end + if (write_accept_i[owner]) begin + if (s_write_pending[owner]) $fatal(1, "SIM_TEST_FAIL P10 duplicate AW"); + s_write_pending[owner] = 1; + s_write_start[owner] = s_cycle; + if (enable_i) s_writes[owner]++; + end + if (read_done_i[owner]) begin + if (!s_read_pending[owner]) $fatal(1, "SIM_TEST_FAIL P10 unsolicited RLAST"); + s_read_pending[owner] = 0; + if (enable_i) begin + s_read_done[owner]++; + if (s_cycle - s_read_start[owner] > s_read_max[owner]) + s_read_max[owner] = s_cycle - s_read_start[owner]; + end + end + if (write_done_i[owner]) begin + if (!s_write_pending[owner]) $fatal(1, "SIM_TEST_FAIL P10 unsolicited B"); + s_write_pending[owner] = 0; + if (enable_i) begin + s_write_done[owner]++; + if (s_cycle - s_write_start[owner] > s_write_max[owner]) + s_write_max[owner] = s_cycle - s_write_start[owner]; + end + end + if (enable_i) begin + s_read_bytes[owner] += 64'(read_bytes_i[owner]); + s_write_bytes[owner] += 64'(write_bytes_i[owner]); + if (error_i[owner]) s_errors[owner]++; + for (integer channel = 0; channel < 5; channel++) begin + s_stall[owner][channel] = ((stalled_i[owner] >> channel) & 5'b1) != 0 ? + s_stall[owner][channel] + 1 : 0; + if (s_stall[owner][channel] > s_stall_max[owner][channel]) + s_stall_max[owner][channel] = s_stall[owner][channel]; + end + end + end + if (enable_i) begin + if (integer'(active_targets_i) > s_active_max) s_active_max = integer'(active_targets_i); + s_bank_count = 0; + for (integer bank = 0; bank < 4; bank++) if (bank_issues_i[bank]) s_bank_count++; + if (s_bank_count > 1) s_bank_overlap++; + if (dma_beat_overlap_i) s_dma_overlap++; + end + s_previous = enable_i; + end + end +endmodule diff --git a/rtl/tiny/dv/tiny_p10_observers.svh b/rtl/tiny/dv/tiny_p10_observers.svh new file mode 100644 index 00000000..631d9fe7 --- /dev/null +++ b/rtl/tiny/dv/tiny_p10_observers.svh @@ -0,0 +1,59 @@ +// Opt-in P10 observations, excluded from mapped-netlist elaboration. +wire s_p10_camera = $test$plusargs("tiny_p10_camera"); +wire s_p10_audio = $test$plusargs("tiny_p10_audio"); +wire s_p10_enabled = s_p10_camera ? u_dut.u_soc.s_perf_enable : (s_p10_audio && s_p6_audio_enabled); +wire [2:0] s_p10_ar, s_p10_r, s_p10_aw, s_p10_b, s_p10_error; +wire [2:0][7:0] s_p10_read_bytes, s_p10_write_bytes; +wire [2:0][4:0] s_p10_stalled; +wire [3:0] s_p10_bank_issues; +logic [5:0] s_p10_targets; +always_comb begin + s_p10_targets = 0; + for (integer target_index = 0; target_index < `RETROSOC_TINY__TARGET_COUNT; target_index++) + if (!u_dut.u_soc.u_fabric.target_idle_o[target_index]) s_p10_targets++; +end +for (genvar owner = 0; owner < 3; owner++) begin : gen_p10_bus + `define _P10_AXI u_dut.u_soc.u_masters_axi4_if[owner] + logic [2:0] s_read_size = 0; + always @(posedge u_dut.u_soc.s_exec_clk) + if (`_P10_AXI.arvalid && `_P10_AXI.arready) + s_read_size <= `_P10_AXI.arsize; + assign s_p10_ar[owner] = `_P10_AXI.arvalid && `_P10_AXI.arready; + assign s_p10_r[owner] = `_P10_AXI.rvalid && `_P10_AXI.rready && `_P10_AXI.rlast; + assign s_p10_aw[owner] = `_P10_AXI.awvalid && `_P10_AXI.awready; + assign s_p10_b[owner] = `_P10_AXI.bvalid && `_P10_AXI.bready; + assign s_p10_error[owner] = (`_P10_AXI.rvalid && `_P10_AXI.rready && `_P10_AXI.rresp != 0) || + (s_p10_b[owner] && `_P10_AXI.bresp != 0); + assign s_p10_read_bytes[owner] = (`_P10_AXI.rvalid && `_P10_AXI.rready && `_P10_AXI.rresp == 0) + ? (8'd1 << s_read_size) : 8'd0; + assign s_p10_write_bytes[owner] = (`_P10_AXI.wvalid && `_P10_AXI.wready) ? + 8'(`_P10_AXI.wstrb[0]) + 8'(`_P10_AXI.wstrb[1]) + + 8'(`_P10_AXI.wstrb[2]) + 8'(`_P10_AXI.wstrb[3]) : 8'd0; + assign s_p10_stalled[owner] = { + `_P10_AXI.bvalid && !`_P10_AXI.bready, + `_P10_AXI.wvalid && !`_P10_AXI.wready, + `_P10_AXI.awvalid && !`_P10_AXI.awready, + `_P10_AXI.rvalid && !`_P10_AXI.rready, + `_P10_AXI.arvalid && !`_P10_AXI.arready + }; + `undef _P10_AXI +end +for (genvar bank = 0; bank < 4; bank++) begin : gen_p10_bank + assign s_p10_bank_issues[bank] = u_dut.u_soc.u_sram.gen_group[bank].u_group.s_issue; +end +tiny_p10_observer u_p10_observer ( + .clk_i (u_dut.u_soc.s_exec_clk), + .rst_n_i (u_dut.u_soc.s_fabric_rst_n && (s_p10_camera || s_p10_audio)), + .enable_i (s_p10_enabled), + .read_accept_i (s_p10_ar), + .read_done_i (s_p10_r), + .write_accept_i (s_p10_aw), + .write_done_i (s_p10_b), + .error_i (s_p10_error), + .read_bytes_i (s_p10_read_bytes), + .write_bytes_i (s_p10_write_bytes), + .stalled_i (s_p10_stalled), + .active_targets_i (s_p10_targets), + .bank_issues_i (s_p10_bank_issues), + .dma_beat_overlap_i((s_p10_read_bytes[1] != 0) && (s_p10_write_bytes[1] != 0)) +); diff --git a/rtl/tiny/filelist/tb.fl b/rtl/tiny/filelist/tb.fl index 65c3e02b..35466e3e 100644 --- a/rtl/tiny/filelist/tb.fl +++ b/rtl/tiny/filelist/tb.fl @@ -3,3 +3,4 @@ /dv/tiny_dvp_camera.sv /tests/rtl/sdio_native_model.sv /dv/retrosoc_tiny_tb.sv +/dv/tiny_p10_observer.sv diff --git a/scripts/tiny_r2_p10.py b/scripts/tiny_r2_p10.py new file mode 100644 index 00000000..cfd98fdb --- /dev/null +++ b/scripts/tiny_r2_p10.py @@ -0,0 +1,637 @@ +"""Single-execution, source-bound Tiny P10 measurements; never phase signoff.""" +from __future__ import annotations + +import argparse +from datetime import datetime, timezone +import json +import os +from pathlib import Path +import re +import shutil +import subprocess +import sys + +ROOT = Path(__file__).resolve().parents[1] +sys.path.insert(0, str(ROOT)) +from scripts import tiny_r2_baseline as b # noqa: E402 +from scripts import tiny_r2_p3 as p3 # noqa: E402 +from scripts import tiny_r2_p4 as p4 # noqa: E402 +from scripts import tiny_r2_p5 as p5 # noqa: E402 +from scripts import tiny_r2_p6 as p6 # noqa: E402 +from scripts.check_simulation import DEFAULT_FAILURE # noqa: E402 +from scripts.check_c_warnings import self_owned_warnings # noqa: E402 + +PHASE = "TINY-R2-P10" +COLLECTOR_UPDATE_PATHS = {"scripts/tiny_r2_p10.py", "tests/test_tiny_r2_p10.py"} +STAGES = ("matched", "software", "audio", "camera", "backend", "report") +P1 = "build/ihp130-tiny-2026-10-05-20-02-fa1c4ce1e303/meta/tiny-r2-p1/baseline-report.json" +P3 = "build/ics55-tiny-performance-2026-10-09-17-10-53cc6674d532/meta/tiny-r2-p3/report.json" +P5 = "build/ics55-tiny-2026-10-10-18-58-b4aa59f3e5d9/meta/tiny-r2-p5/report.json" +MATCHED = ("perf-o2-plain-flat", "perf-o2-plain-banked") +BUS_FIELDS = set("window owner reads writes read_done write_done read_initial write_initial " + "read_pending write_pending read_bytes write_bytes errors read_latency_max " + "write_latency_max ar_max r_max aw_max w_max b_max".split()) +CONCURRENCY_FIELDS = set("window active_targets_max bank_overlap dma_beat_overlap".split()) +DEFERRED = ["full long Icarus firmware: maintainer deferred to final Tiny qualification", + "complete PR/nightly: retained matrix includes deferred Icarus and P7 clock transitions", + "fresh full-system VGA: historical evidence retained", + "physical/PVT/board/sensor and higher-frequency qualification"] + + +def rows(content: str, prefix: str, fields: set[str]) -> list[dict]: + return [b.parse_fields(line, fields) for line in content.splitlines() + if line.startswith(prefix + " ")] + + +def require_terminal(content: str, flow: dict) -> None: + if (flow.get("exit_code") != 0 or flow.get("status") != "passed" or + DEFAULT_FAILURE.search(content) or len(re.findall( + r"^SIM_TEST_PASS Tiny cycles=[1-9][0-9]*$", content, re.M)) != 1): + raise ValueError("Tiny command/terminal/forbidden-marker verdict failed") + + +def golden() -> tuple[list[int], int]: + """Independent byte/pixel oracle, not firmware's packed-word implementation.""" + histogram = [0] * 32 + checksum = 2166136261 + for row in range(240): + pixels = [(((2 * x + 3 * row) & 255) << 8) | ((2 * x + 3 * row + 1) & 255) + for x in range(320)] + for pixel in pixels: + histogram[pixel >> 11] += 1 + for x in range(0, 320, 2): + checksum = ((checksum ^ (pixels[x] | (pixels[x + 1] << 16))) * 16777619) & 0xffffffff + return histogram, checksum + + +def check_observations(content: str, windows: int, targets: int) -> dict: + bus = rows(content, "P10_BUS", BUS_FIELDS) + concurrency = rows(content, "P10_CONCURRENCY", CONCURRENCY_FIELDS) + if [(r["window"], r["owner"]) for r in bus] != [ + (window, owner) for window in range(1, windows + 1) for owner in range(3)]: + raise ValueError("missing, reordered or duplicate bus observation") + if [r["window"] for r in concurrency] != list(range(1, windows + 1)): + raise ValueError("incomplete concurrency windows") + for row in bus: + for direction in ("read", "write"): + initial, pending = row[direction + "_initial"], row[direction + "_pending"] + if (initial > 1 or pending > 1 or row[direction + "s"] + initial != + row[direction + "_done"] + pending): + raise ValueError("transaction conservation mismatch") + if row["errors"]: + raise ValueError("bus error inside successful workload window") + if any(r["active_targets_max"] > targets for r in concurrency): + raise ValueError("concurrency exceeds canonical topology") + return {"bus": bus, "concurrency": concurrency, + "units": "SYS cycles; bytes from actual handshakes; censored CPU transactions retained"} + + +def camera_measurements(content: str, flow: dict, targets: int) -> dict: + require_terminal(content, flow) + histogram, checksum = golden() + result = rows(content, "P10_RESULT", {"code", "pixels", "bytes", "sectors", "checksum"}) + if result != [dict(code=0, pixels=76800, bytes=153600, sectors=300, checksum=checksum)]: + raise ValueError("camera pipeline did not complete exact work") + if rows(content, "P10_HIST", {"bin", "pixels"}) != [ + {"bin": index, "pixels": value} for index, value in enumerate(histogram)]: + raise ValueError("CPU histogram differs from independent oracle") + phases = rows(content, "P10_PHASE", {"id", "calls", "bytes", "cycles", "instructions"}) + if len(phases) != 5: + raise ValueError("missing pipeline phase measurements") + for index, row in enumerate(phases): + if (row["id"] != index or row["calls"] != (1 if index < 2 else 150) or + row["bytes"] != 153600 or not 0 < row["instructions"] <= row["cycles"]): + raise ValueError("invalid pipeline phase accounting") + observed = check_observations(content, 452, targets) + for row in observed["bus"]: + if row["owner"] != 0 and (row["read_pending"] or row["write_pending"]): + raise ValueError("DMA work not drained at pipeline boundary") + # Capture uses DMA2, saving uses the independent SDIO master and SRAM bounce buffers. + if observed["bus"][1]["write_bytes"] != 153600: + raise ValueError("camera DMA byte count mismatch") + sdio = [row for row in observed["bus"] if row["owner"] == 2] + for sample in sdio: + # Each HAL call fetches one 16-byte descriptor and writes back its + # four-byte status. Preserve those bytes in the raw bus observations. + expected = (0, 0) + if sample["window"] >= 4 and (sample["window"] - 4) % 3 == 0: + expected = (1040, 4) + elif sample["window"] >= 5 and (sample["window"] - 5) % 3 == 0: + expected = (16, 1028) + if (sample["read_bytes"], sample["write_bytes"]) != expected: + raise ValueError("SDIO payload/descriptor/writeback accounting mismatch") + fifo = rows(content, "P9_OBSERVATION", {"version", "accepted_words", "delivered_words", + "fifo_high_water", "backpressure", "pixel_half_ns"}) + if len(fifo) != 1: + raise ValueError("missing pixel FIFO observations") + row = fifo[0] + available = 512 - 4 * row["fifo_high_water"] - 16 + if (row["version"] != 1 or row["accepted_words"] != 38400 or row["delivered_words"] != 38400 or + row["fifo_high_water"] == 0 or available <= 0 or row["pixel_half_ns"] != 500 or + row["backpressure"] >= available * 24): + raise ValueError("capture FIFO/rate budget invalid") + return {"phases": phases, "histogram": histogram, "checksum": checksum, + "observations": observed, "fifo": row, + "sdio_bytes": {"payload_read": 153600, "payload_write": 153600, + "descriptor_read": 4800, "descriptor_writeback": 1200, + "observed_read": sum(r["read_bytes"] for r in sdio), + "observed_write": sum(r["write_bytes"] for r in sdio)}, + "service_budget": {"payload_bytes": 512, "margin_bytes": 16, + "empty_no_service_us": 496, "pixel_hz": 1000000, + "high_water_no_service_us": available, + "observed_backpressure_us": row["backpressure"] / 24}} + + +def audio_measurements(content: str, flow: dict, targets: int) -> dict: + require_terminal(content, flow) + result = p6.parse_log(content, flow) + result["window_observations"] = check_observations(content, 2, targets) + if not any(r["bank_overlap"] for r in result["window_observations"]["concurrency"]): + raise ValueError("no useful independent-bank overlap observed") + return result + + +def sources(root: Path, pdk: str) -> dict: + result = b.source_inputs(root, pdk) + names = subprocess.check_output(["git", "ls-files", "--cached", "--others", "--exclude-standard", + "-z", "--", "tests", "configs"], cwd=root, text=True).split("\0") + for name in [*names, "docs/ip/tiny-soc.md"]: + path = root / name + if name and path.is_file(): + result[name] = b.sha256(path) + return result + + +def make_command(args, stage: str) -> list[str]: + command = ["make", "--no-print-directory", f"CONFIG=configs/ci/{args.pdk.lower()}-tiny.mk", + "SOC=TINY", f"PDK={args.pdk}", "APP=ci_smoke", "SYNTH=NONE", "STA=NONE", + f"BUILD_TIMESTAMP={args.timestamp}", f"JOBS={args.jobs}"] + if stage in ("matched", "software"): + command += ["SIMU=VERILATOR", "FIRMWARE_NAME=retrosoc_p10_replay"] + elif stage in ("audio", "camera"): + command += ["SIMU=VERILATOR", "HAVE_SVA=YES"] + if stage == "camera": + command.append("TINY_P10_ACCEPTANCE=YES") + else: + command += ["SIMU=IVERILOG"] + return command + + +def variant_for(root: Path, command: list[str]) -> Path: + output = subprocess.check_output([*command, "config"], cwd=root, text=True) + values = dict(line.split(None, 1) for line in output.splitlines() if line.strip()) + variant = Path(values["VARIANT_ROOT"]).resolve() + if not variant.is_relative_to(root / "build"): + raise ValueError("P10 evidence must be below the repository build root") + return variant + + +def location(args, stage: str) -> tuple[Path, Path]: + variant = variant_for(args.root, make_command(args, stage)) + return variant, variant / "meta/tiny-r2-p10" / stage + + +def capture_artifacts(paths: list[Path]) -> dict: + return {str(p): b.artifact(p) for p in paths} + + +def source_delta(record: dict, source: dict, allow_collector_update: bool = False) -> dict: + original = record.get("source_inputs") + if not original or not source or record.get("source_after") != original: + raise ValueError("missing source identity or inputs changed during execution") + changed = {name: {"before": original.get(name), "after": source.get(name)} + for name in set(original) | set(source) if original.get(name) != source.get(name)} + if changed and (not allow_collector_update or not set(changed) <= COLLECTOR_UPDATE_PATHS): + raise ValueError("execution inputs changed beyond the explicit collector-only update") + return changed + + +def checked_record(path: Path, source: dict, *, allow_collector_update: bool = False) -> dict: + record = b.read_json(path) + source_delta(record, source, allow_collector_update) + if (record.get("phase") != PHASE or record.get("status") != "passed" or + record.get("stage") not in STAGES[:-1] or record.get("stage") != path.parent.name or + record.get("pdk") not in ("ICS55", "IHP130") or record.get("maximum_attempts") != 1 or + not record.get("artifacts") or not record.get("tools") or + not record.get("started_at") or not record.get("finished_at")): + raise ValueError(f"cannot resume incomplete or changed P10 record: {path}") + captured = record["source"] + if (b.read_json(b.check_artifact(captured["inputs"])) != record["source_inputs"] or + captured["input_digest"] != b.digest_map(record["source_inputs"])): + raise ValueError("source snapshot differs from stage identity") + configuration = record["configuration"]["configuration"] + if (configuration["SOC"] != "TINY" or configuration["PDK"] != record["pdk"] or + configuration["EXT_CLK_HZ"] != "24000000" or configuration["SRAM_SIZE_KIB"] != "128"): + raise ValueError("stage is not the selected Tiny SAFE24 profile") + for artifact in record["artifacts"].values(): + b.check_artifact(artifact) + for artifact in record["source"].values(): + if isinstance(artifact, dict) and "sha256" in artifact: + b.check_artifact(artifact) + for tool in record.get("tools", {}).values(): + b.check_artifact(tool["executable"]) + b.check_artifact(tool["archive"]) + for tool in record.get("host_tools", {}).values(): + b.check_artifact(tool) + if record.get("stage") in ("audio", "camera"): + stage = record["stage"] + flow = b.read_json(path.parent / f"{stage}.json") + content = (path.parent / f"{stage}.log").read_text() + parser = camera_measurements if stage == "camera" else audio_measurements + if parser(content, flow, record["target_count"]) != record["measurements"]: + raise ValueError("stored measurements differ from retained execution") + for case in record.get("cases", []): + label = case["name"] + flow = b.read_json(path.parent / f"{label}.json") + content = (path.parent / f"{label}.log").read_text() + if label == "compat-p1": + measured = b.parse_log(content, flow) + else: + measured = p4.parse_log(content, flow) + measured["fabric"] = rows(content, "R2_P5_FABRIC", p5.FABRIC_FIELDS) + if measured != case["samples"]: + raise ValueError("stored replay measurements differ from execution") + b.check_artifact(case["image"]) + if record["stage"] in ("matched", "software"): + expected = set(MATCHED) if record["pdk"] == "ICS55" else {"compat-p1"} + if record["stage"] == "software": + expected = {case[0] for case in p3.MATRIX} - set(MATCHED) + names = [case["name"] for case in record["cases"]] + if set(names) != expected or len(names) != len(expected): + raise ValueError("missing or duplicate replay images") + if record["stage"] == "backend": + for label in ("firmware-lint", "synthesis", "netlist-boot", "netlist-dvp", "sta"): + flow = b.read_json(path.parent / f"{label}.json") + if flow.get("exit_code") != 0 or flow.get("status") != "passed": + raise ValueError("backend gate failed") + variant = path.parents[3] + runtime = b.read_json(path.parent / "vvp-pre-simulation.json") + for key in ("executable", "archive"): + b.check_artifact(runtime["runtime"][key]) + for sub in ("sim/iverilog/netl", "sim/iverilog/netl/dvp"): + directory = variant / sub + flow = b.read_json(directory / "result-sim.json") + require_terminal((directory / "sim.log").read_text(), flow) + if (runtime["runtime"]["executable"]["path"] not in flow["command"] or + runtime["captured_at"] > flow["started_at"]): + raise ValueError("unbound or late-captured Icarus runtime") + audit = b.read_json(variant / "syn/yosys/tiny-r2-p7-netlist.json") + for artifact in audit["inputs"]: + b.check_artifact(artifact) + if (audit["status"] != "passed" or audit["pdk"] != record["pdk"] or + audit["binding"]["main_macros"] != 32 or audit["binding"]["private_crypto_macros"] != 6 or + audit["binding"]["pll_macros"] != int(record["pdk"] == "ICS55")): + raise ValueError("macro binding audit failed") + return record + + +def run_make(args, directory: Path, label: str, command: list[str], *, observation=False) -> dict: + if (directory / f"{label}.json").exists(): + raise ValueError(f"attempt already exists: {label}") + print(f"{args.pdk}/{args.stage}: {label}", flush=True) + record = {"command": command, "status": "running", + "started_at": datetime.now(timezone.utc).isoformat()} + with (directory / f"{label}.log").open("w") as log: + process = subprocess.Popen(command, cwd=args.root, stdout=log, stderr=subprocess.STDOUT) + record["pid"] = process.pid + b.dump(directory / f"{label}.json", record) + code = process.wait() + warnings = self_owned_warnings(args.root, (directory / f"{label}.log").read_text()) + record.update(exit_code=code, self_owned_warnings=warnings, + status="passed" if code == 0 and not warnings else "failed", + finished_at=datetime.now(timezone.utc).isoformat()) + b.dump(directory / f"{label}.json", record) + if not observation and record["status"] != "passed": + raise ValueError(f"Make gate failed: {directory / (label + '.json')}") + return record + + +def simulate(args, directory: Path, label: str, model: Path, image: Path, flags: list[str], + timeout: int, parse) -> tuple[dict, list[Path]]: + paths = [model, image] + identities = capture_artifacts(paths) + command = ["timeout", "--foreground", "--kill-after=30s", f"{timeout}s", str(model), + f"+firmware={image}", "+max_cycles=100000000", *flags] + b.dump(directory / f"{label}-launch.json", {"command": command, "artifacts": identities, + "maximum_attempts": 1}) + flow = b.execute(args.root, directory, label, command, cwd=args.root, quiet=True) + content = (directory / f"{label}.log").read_text() + require_terminal(content, flow) + measurements = parse(content, flow) + for artifact in identities.values(): + b.check_artifact(artifact) + return measurements, [*paths, directory / f"{label}.log", directory / f"{label}.json", + directory / f"{label}-launch.json"] + + +def replay(args, variant: Path, directory: Path, record: dict) -> list[Path]: + command = make_command(args, args.stage) + model = variant / "sim/verilator/emu" + artifacts = [] + if args.stage == "matched": + if model.exists(): + raise ValueError("fresh replay model required") + run_make(args, directory, "compile-model", [*command, "comp", "manifest"]) + else: + _, previous = location(args, "matched") + matched = checked_record(previous / "record.json", record["source_inputs"]) + artifacts += [previous / "record.json"] + record["matched_stage"] = b.artifact(previous / "record.json") + if matched["pdk"] != "ICS55": + raise ValueError("compiler/placement comparison uses ICS55 only") + configuration = b.read_json(variant / "meta/manifest.json") + record["configuration"] = configuration + runtime_before = b.runtime_inputs(args.root, variant, "verilator") + record["runtime_inputs"] = runtime_before + record["cases"] = [] + if args.pdk == "IHP130": + reference = args.root / P1 + original = b.read_json(reference)["simulators"]["verilator"] + b.validate_runs(original["runs"]) + image_path = b.check_artifact(original["image_manifest"]) + image = b.read_json(image_path) + firmware = b.check_artifact(image["artifacts"]["tiny_baseline.hex"]) + b.retained_image(image_path.parent.parent.parent, firmware) + first, end = image["descriptor_range"] + flags = ["+tiny_r2_baseline", f"+baseline_tcd_base={first:x}", f"+baseline_tcd_end={end:x}"] + samples, paths = simulate(args, directory, "compat-p1", model, firmware, flags, 10800, b.parse_log) + record["cases"].append({"name": "compat-p1", "image": b.artifact(image_path), + "samples": samples, "baseline_samples": original["runs"][0]["samples"]}) + artifacts += [reference, image_path, *paths] + else: + reference = args.root / (P5 if args.stage == "matched" else P3) + report = b.read_json(reference) + if args.stage == "matched": + old = p5.validate_replay(b.check_artifact(report["executions"]["verilator"]), "verilator") + cases = old["cases"] + else: + cases = p4.reference_cases(reference, [c["name"] for c in report["cases"] + if c["name"] not in MATCHED]) + for case in cases: + image_path = b.check_artifact(case["image"]) + image = p3.validate_image(image_path) + p5.compatible_hardware(image["configuration"], configuration) + first, end = image["layout"]["descriptor_range"] + flags = ["+tiny_r2_p3", "+tiny_r2_p5", f"+baseline_tcd_base={first:x}", + f"+baseline_tcd_end={end:x}"] + samples, paths = simulate(args, directory, case["name"], model, + b.check_artifact(image["artifacts"]["tiny_p3.hex"]), flags, 10800, p4.parse_log) + fabric = rows((directory / f"{case['name']}.log").read_text(), "R2_P5_FABRIC", p5.FABRIC_FIELDS) + if [r["window"] for r in fabric] != list(range(1, 9)): + raise ValueError("missing replay fabric measurements") + samples["fabric"] = fabric + record["cases"].append({"name": case["name"], "image": b.artifact(image_path), + "samples": samples, "baseline_samples": case["samples"]}) + artifacts += [image_path, *paths] + b.dump(directory / "record.json", record) + artifacts.append(reference) + if runtime_before != b.runtime_inputs(args.root, variant, "verilator"): + raise ValueError("replay model inputs changed") + artifacts += [args.root / name for name in runtime_before] + return artifacts + + +def application(args, variant: Path, directory: Path, record: dict) -> list[Path]: + command = make_command(args, args.stage) + outputs = [variant / "sw/firmware", variant / "sw/retrosoc_fw.hex", variant / "sim/verilator/emu"] + if any(p.exists() for p in outputs): + raise ValueError("fresh application outputs required") + run_make(args, directory, "build", [*command, "firmware", "comp", "manifest"]) + run_make(args, directory, "size", ["riscv32-unknown-elf-size", str(outputs[0])]) + run_make(args, directory, "symbols", ["riscv32-unknown-elf-nm", "-n", str(outputs[0])]) + size = (directory / "size.log").read_text().splitlines()[-1].split() + symbols = (directory / "symbols.log").read_text() + ebss = re.search(r"^([0-9a-f]+) [A-Za-z] _ebss$", symbols, re.M) + if ebss is None or not 0x30000000 < int(ebss[1], 16) <= 0x30020000 - 4096: + raise ValueError("application lacks 4 KiB flat-SRAM stack headroom") + record["firmware_size"] = dict(zip(("text", "data", "bss"), map(int, size[:3]), strict=True)) + record["stack_headroom"] = 0x30020000 - int(ebss[1], 16) + record["configuration"] = b.read_json(variant / "meta/manifest.json") + runtime = b.runtime_inputs(args.root, variant, "verilator") + targets = b.read_json(args.root / "rtl/tiny/integration/soc_topology.json")["fabric"]["target_count"] + record["target_count"] = targets + flags = ["+tiny_p10_audio"] + parse = audio_measurements + if args.stage == "camera": + flags = ["+tiny_psram", "+tiny_p9_camera", "+tiny_p9_half_ns=500", "+tiny_p10_camera"] + parse = camera_measurements + samples, paths = simulate(args, directory, args.stage, outputs[2], outputs[1], flags, + 14400 if args.stage == "camera" else 10800, + lambda log, flow: parse(log, flow, targets)) + record["measurements"] = samples + record["runtime_inputs"] = runtime + if runtime != b.runtime_inputs(args.root, variant, "verilator"): + raise ValueError("application runtime inputs changed") + return [*outputs, *paths, variant / "sw/retrosoc_fw.map", variant / "sw/retrosoc_fw.bin", + variant / "sw/retrosoc_fw.txt", *[args.root / name for name in runtime]] + + +def backend(args, variant: Path, directory: Path, record: dict) -> list[Path]: + command = make_command(args, "backend") + netlist = variant / "syn/yosys/out/retrosoc_tiny_asic_yosys.v" + if netlist.exists(): + raise ValueError("fresh backend variant required") + run_make(args, directory, "firmware-lint", [*command, "SIMU=VERILATOR", "firmware", "rtl-lint"]) + run_make(args, directory, "synthesis", [*command, "SYNTH=YOSYS", "YOSYS_TIMEOUT=10800", "synth"]) + runtime_identity = b.vvp_identity("vvp", record["tools"]["iverilog"]) + b.dump(directory / "vvp-pre-simulation.json", { + "captured_at": datetime.now(timezone.utc).isoformat(), "runtime": runtime_identity}) + command += ["VVP=" + runtime_identity["executable"]["path"]] + run_make(args, directory, "netlist-boot", [*command, "netsim-boot"]) + model = b.artifact(variant / "sim/iverilog/netl/simv") + runtime = b.runtime_inputs(args.root, variant, "iverilog") + run_make(args, directory, "netlist-dvp", [*command, "-o", "netcomp", "netsim-dvp"]) + b.check_artifact(model) + if runtime != b.runtime_inputs(args.root, variant, "iverilog"): + raise ValueError("netlist simulator inputs changed between workloads") + record["runtime_inputs"] = runtime + run_make(args, directory, "sta", [*command, "STA=OPENSTA", "sta"]) + record["observations"] = {} + for name in ("check-warnings", "check-metrics"): + record["observations"][name] = run_make(args, directory, name, [*command, name], observation=True) + paths = [netlist, netlist.with_suffix(".config"), Path(model["path"])] + for sub in ("sim/iverilog/netl", "sim/iverilog/netl/dvp"): + base = variant / sub + flow = b.read_json(base / "result-sim.json") + require_terminal((base / "sim.log").read_text(), flow) + paths += [base / name for name in ("sim.log", "result-sim.json", "result-sim-check.json")] + paths += [variant / "syn/yosys/tiny-r2-p7-netlist.json", variant / "meta/metrics.json", + variant / "meta/warnings.json", variant / "sta/opensta/retrosoc_core.sdc"] + paths += list((variant / "sta/opensta").glob("*.rpt")) + paths += list((variant / "syn/yosys").glob("*.rpt")) + paths += [variant / "sw" / name for name in ("tiny_boot.elf", "tiny_boot.hex", + "tiny_p9_netlist.elf", "tiny_p9_netlist.hex")] + paths += [args.root / name for name in runtime] + record["configuration"] = b.read_json(variant / "meta/manifest.json") + record["metrics"] = b.read_json(variant / "meta/metrics.json") + return paths + + +def report_stage(args, directory: Path, source: dict) -> int: + stages = ("matched", "software", "audio", "camera", "backend") if args.pdk == "ICS55" else ( + "matched", "audio", "camera", "backend") + records, gaps = {}, [] + for stage in stages: + selected = argparse.Namespace(**vars(args)) + if args.retained_timestamp and stage != "camera": + selected.timestamp = args.retained_timestamp + _, parent = location(selected, stage) + try: + record = checked_record(parent / "record.json", source, + allow_collector_update=args.allow_collector_update) + records[stage] = {"record": b.artifact(parent / "record.json"), + "execution_source_digest": record["source"]["input_digest"], + "validation_input_delta": source_delta(record, source, args.allow_collector_update), + "cases": record.get("cases"), "measurements": record.get("measurements"), + "metrics": record.get("metrics"), "observations": record.get("observations"), + "firmware_size": record.get("firmware_size"), + "stack_headroom": record.get("stack_headroom")} + except (OSError, ValueError) as error: + gaps.append({"stage": stage, "reason": str(error)}) + directory.mkdir(parents=True, exist_ok=True) + comparisons = [] + if "matched" in records: + for case in records["matched"]["cases"]: + comparisons.append({"name": case["name"], "image": case["image"], + "firmware": retained_footprint(case["image"]), + "workloads": compare_samples(case["baseline_samples"], case["samples"]), + "attribution": "whole platform revision; not an isolated CPU/fabric change"}) + software = [] + if "matched" in records and "software" in records: + cases = {case["name"]: case for stage in ("matched", "software") for case in records[stage]["cases"]} + pairs = [("isa", "compat-os-flat", "perf-os-plain-flat"), + ("optimizer", "perf-o2-plain-flat", "perf-o3-plain-flat"), + ("optimizer", "perf-o2-plain-flat", "perf-os-plain-flat"), + ("placement", "perf-o2-plain-flat", "perf-o2-plain-banked")] + pairs += [("lto", f"perf-{opt}-plain-flat", f"perf-{opt}-lto-flat") for opt in ("o2", "o3", "os")] + for kind, baseline, candidate in pairs: + software.append({"kind": kind, "baseline": baseline, "candidate": candidate, + "baseline_firmware": retained_footprint(cases[baseline]["image"]), + "candidate_firmware": retained_footprint(cases[candidate]["image"]), + "workloads": compare_samples(cases[baseline]["samples"], cases[candidate]["samples"])}) + quality = {} + for timestamp in dict.fromkeys((args.retained_timestamp or args.timestamp, args.timestamp)): + selected = argparse.Namespace(**vars(args)) + selected.timestamp = timestamp + _, backend_directory = location(selected, "backend") + for path in (backend_directory.parent / "validation").glob("*.json"): + value = b.read_json(path) + quality[timestamp + "/" + path.stem] = { + "record": b.artifact(path), "status": value.get("status"), "exit_code": value.get("exit_code")} + b.dump(directory / "report.json", {"phase": PHASE, "phase_accepted": False, "pdk": args.pdk, + "status": "selected_gates_complete" if not gaps else "incomplete", "stages": records, + "gaps": gaps, "deferred": DEFERRED, "matched_comparisons": comparisons, + "software_comparisons": software, "quality_attempts": quality, + "collector": b.artifact(Path(__file__)), "validation_source_digest": b.digest_map(source), + "retained_timestamp": args.retained_timestamp, + "qualification": "SAFE24 observations only; full regression and human acceptance remain open"}) + return int(bool(gaps)) + + +def retained_footprint(identity: dict) -> dict: + image = b.read_json(b.check_artifact(identity)) + log = b.check_artifact(image["artifacts"]["size.log"]).read_text() + sections = {name: int(size) for name, size in re.findall(r"^(\.[\w.]+)\s+(\d+)\s+\d+\s*$", log, re.M)} + if ".text" not in sections or ".bss" not in sections: + raise ValueError("retained image lacks section-size evidence") + return {"sections": sections, "layout": image.get("layout"), + "stack_headroom": image.get("stack_headroom", image.get("layout", {}).get("stack_headroom")), + "size_log": image["artifacts"]["size.log"]} + + +def compare_samples(baseline: dict, candidate: dict) -> list[dict]: + old = baseline.get("cases", baseline.get("case", [])) + new = candidate.get("cases", candidate.get("case", [])) + if not old or len(old) != len(new): + raise ValueError("comparison workload mismatch") + result = [] + for a, z in zip(old, new, strict=True): + if any(a[key] != z[key] for key in ("name", "payload_bytes", "checksum", "kernel_calls")): + raise ValueError("comparison is not identical work") + result.append({"name": a["name"], "payload_bytes": a["payload_bytes"], + "baseline_cycles": a["cycles"], "candidate_cycles": z["cycles"], + "baseline_instructions": a["instructions"], "candidate_instructions": z["instructions"], + "baseline_cpi": a["cycles"] / a["instructions"], + "candidate_cpi": z["cycles"] / z["instructions"], + "cycle_ratio": a["cycles"] / z["cycles"]}) + return result + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--root", type=Path, default=ROOT) + parser.add_argument("--pdk", choices=("ICS55", "IHP130"), required=True) + parser.add_argument("--timestamp", required=True) + parser.add_argument("--jobs", type=int, default=4) + parser.add_argument("--stage", choices=STAGES, required=True) + parser.add_argument("--dry-run", action="store_true") + parser.add_argument("--resume", action="store_true") + parser.add_argument("--retained-timestamp", help="report-only: preserve completed stages from this campaign") + parser.add_argument("--allow-collector-update", action="store_true", + help="report-only: admit explicitly listed collector/test changes, never DUT changes") + args = parser.parse_args() + args.root = args.root.resolve() + if (not re.fullmatch(r"\d{4}-\d{2}-\d{2}-\d{2}-\d{2}", args.timestamp) or args.jobs < 1 or + (args.pdk == "IHP130" and args.stage == "software")): + parser.error("invalid timestamp/jobs or unsupported IHP130 software matrix") + if (args.retained_timestamp or args.allow_collector_update) and args.stage != "report": + parser.error("retained campaign and collector update are report-only") + if args.retained_timestamp and not re.fullmatch(r"\d{4}-\d{2}-\d{2}-\d{2}-\d{2}", args.retained_timestamp): + parser.error("invalid retained timestamp") + variant, directory = location(args, args.stage) + if args.dry_run: + selections = list(MATCHED) if args.stage == "matched" and args.pdk == "ICS55" else ( + [case[0] for case in p3.MATRIX if case[0] not in MATCHED] if args.stage == "software" else + ["compat-p1"] if args.stage == "matched" else [args.stage]) + print(json.dumps({"stage": args.stage, "pdk": args.pdk, "variant": str(variant), + "make": make_command(args, args.stage), "evidence": str(directory), + "case_names": selections, "maximum_attempts": 1, + "max_cycles": 100000000, + "wall_seconds": 14400 if args.stage == "camera" else 10800, + "references": {"IHP130": P1, "ICS55": P5, "software": P3}, + "deferred": DEFERRED}, indent=2)) + return 0 + source = sources(args.root, args.pdk) + if args.stage == "report": + return report_stage(args, directory, source) + if directory.exists(): + if args.resume: + checked_record(directory / "record.json", source) + print(f"Reused verified completed stage: {directory}") + return 0 + raise ValueError("evidence exists; use --resume for an unchanged successful stage or a fresh timestamp") + directory.mkdir(parents=True, exist_ok=False) + record = {"phase": PHASE, "phase_accepted": False, "stage": args.stage, "pdk": args.pdk, + "source_inputs": source, "status": "running", "pid": os.getpid(), + "started_at": datetime.now(timezone.utc).isoformat(), "maximum_attempts": 1} + b.dump(directory / "record.json", record) + print(f"P10 {args.pdk}/{args.stage}: {directory}", flush=True) + try: + record["tools"] = b.tools_identity(args.root) + record["host_tools"] = {name: b.artifact(Path(shutil.which(name)).resolve()) for name in + ("make", "cc", "c++", "timeout")} + record["source"] = b.snapshot(args.root, directory / "source", source) + b.dump(directory / "record.json", record) + if args.stage in ("matched", "software"): + paths = replay(args, variant, directory, record) + elif args.stage in ("audio", "camera"): + paths = application(args, variant, directory, record) + else: + paths = backend(args, variant, directory, record) + after = sources(args.root, args.pdk) + if source != after: + raise ValueError("source inputs changed during P10 execution") + record["source_after"] = after + paths += [p for p in directory.iterdir() if p.is_file() and p.name != "record.json"] + record["artifacts"] = capture_artifacts(paths) + record["status"] = "passed" + except (OSError, ValueError, subprocess.SubprocessError) as error: + record.update(status="failed_or_incomplete", reason=str(error)) + record["finished_at"] = datetime.now(timezone.utc).isoformat() + b.dump(directory / "record.json", record) + print(f"P10 {args.pdk}/{args.stage}: {record['status']}", flush=True) + return 0 if record["status"] == "passed" else 1 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tests/c/test_runtime.c b/tests/c/test_runtime.c index e0d0de77..4052a18c 100644 --- a/tests/c/test_runtime.c +++ b/tests/c/test_runtime.c @@ -44,6 +44,7 @@ #include "../../crt/src/hal/ga2d_math.h" #include "../../app/apps/ci_smoke/p9_frame.h" +#include "../../app/apps/ci_smoke/p10_frame.h" #include "../../crt/src/hal/opipsram_math.c" #define TEST_STORAGE_ADDRESS ((uintptr_t)0x1000U) @@ -3472,6 +3473,31 @@ static int test_p9_frame_layout(void) { return 0; } +static int test_p10_frame_statistics(void) { + rs_p10_statistics_t statistics = {.checksum = UINT32_C(2166136261)}; + uint32_t offset = UINT32_MAX; + uint32_t block = UINT32_MAX; + if (rs_p10_accumulate(&statistics, UINT32_C(0xF8000000)) != RS_OK || statistics.pixels != 2U || + statistics.red[0] != 1U || statistics.red[31] != 1U || + rs_p10_accumulate(&statistics, 0U) != RS_OK || statistics.red[0] != 3U) { + return 1; + } + statistics.red[0] = UINT32_MAX - 1U; + if (rs_p10_accumulate(&statistics, 0U) != RS_EINVAL || statistics.red[0] != UINT32_MAX - 1U || + rs_p10_accumulate(NULL, 0U) != RS_EINVAL) { + return 2; + } + if (rs_p10_chunk(153600U, 149U, 316U, &offset, &block) != RS_OK || offset != 38144U || + block != 314U || rs_p10_chunk(153600U, 150U, 1024U, &offset, &block) != RS_EINVAL || + rs_p10_chunk(153600U, 0U, 315U, &offset, &block) != RS_EINVAL || + rs_p10_chunk(UINT32_MAX, 0U, UINT32_MAX, &offset, &block) != RS_EINVAL || + rs_p10_chunk(512U, 0U, 1024U, &offset, &block) != RS_EINVAL || + rs_p10_chunk(0U, 0U, 1024U, &offset, &block) != RS_EINVAL) { + return 3; + } + return 0; +} + int main(void) { const int results[] = { test_string_helpers(), @@ -3501,6 +3527,7 @@ int main(void) { test_tiny_dma_config_validation(), test_dvp_hal(), test_p9_frame_layout(), + test_p10_frame_statistics(), test_opipsram_helpers(), test_user_ip_validation(), test_extension_validation(), diff --git a/tests/rtl/tiny_p10_observer_tb.sv b/tests/rtl/tiny_p10_observer_tb.sv new file mode 100644 index 00000000..ba1f12e9 --- /dev/null +++ b/tests/rtl/tiny_p10_observer_tb.sv @@ -0,0 +1,63 @@ +module tiny_p10_observer_tb; + logic clk = 0; + always #5 clk = !clk; + logic reset_n = 0, enable = 0; + logic [2:0] ar = 0, r = 0, aw = 0, b = 0, errors = 0; + logic [2:0][7:0] read_bytes = 0, write_bytes = 0; + logic [2:0][4:0] stalled = 0; + tiny_p10_observer u_observer ( + .clk_i (clk), + .rst_n_i (reset_n), + .enable_i (enable), + .read_accept_i (ar), + .read_done_i (r), + .write_accept_i (aw), + .write_done_i (b), + .error_i (errors), + .read_bytes_i (read_bytes), + .write_bytes_i (write_bytes), + .stalled_i (stalled), + .active_targets_i (6'd3), + .bank_issues_i (4'b0101), + .dma_beat_overlap_i(read_bytes[1] != 0 && write_bytes[1] != 0) + ); + initial begin + repeat (2) @(negedge clk); + reset_n = 1; + ar[0] = 1; + @(negedge clk); + ar = 0; + enable = 1; + aw[1] = 1; + write_bytes[1] = 4; + stalled[0][0] = 1; + @(negedge clk); + aw = 0; + write_bytes = 0; + r[0] = 1; + read_bytes[0] = 4; + @(negedge clk); + r = 0; + read_bytes = 0; + stalled = 0; + b[1] = 1; + @(negedge clk); + b = 0; + enable = 0; + @(negedge clk); + if ($test$plusargs("orphan")) begin + r[2] = 1; + @(negedge clk); + end + enable = 1; + ar[2] = 1; + @(negedge clk); + ar = 0; + enable = 0; + @(negedge clk); + r[2] = 1; + @(negedge clk); + $display("SIM_TEST_PASS Tiny cycles=9"); + $finish; + end +endmodule diff --git a/tests/test_tiny_r2_p10.py b/tests/test_tiny_r2_p10.py new file mode 100644 index 00000000..10c1bd72 --- /dev/null +++ b/tests/test_tiny_r2_p10.py @@ -0,0 +1,179 @@ +"""P10 single-run acceptance, observer conservation and configuration isolation.""" +from __future__ import annotations + +from copy import deepcopy +from pathlib import Path +import os +import subprocess + +import pytest + +from scripts import tiny_r2_p10 as p10 +from scripts.check_simulation import DEFAULT_FAILURE + +ROOT = Path(__file__).resolve().parents[1] + + +def configuration(*extra): + return subprocess.run(["make", "--no-print-directory", "CONFIG=configs/ci/ics55-tiny.mk", + "APP=ci_smoke", "BUILD_TIMESTAMP=2026-10-17-00-00", *extra, "config"], + cwd=ROOT, text=True, capture_output=True, check=False) + + +def test_acceptance_configuration_is_separate(): + baseline = configuration() + selected = configuration("TINY_P10_ACCEPTANCE=YES") + assert baseline.returncode == selected.returncode == 0 + assert baseline.stdout != selected.stdout + base_values = dict(line.split(None, 1) for line in baseline.stdout.splitlines() if line.strip()) + new_values = dict(line.split(None, 1) for line in selected.stdout.splitlines() if line.strip()) + assert base_values["VARIANT_ROOT"] != new_values["VARIANT_ROOT"] + for extra in ("TINY_P7_ACCEPTANCE=YES", "TINY_P8_ACCEPTANCE=YES", "TINY_P9_ACCEPTANCE=YES", + "APP=bringup", "SOC=MINI", "TINY_P9_QUICK=YES", "TINY_P10_ACCEPTANCE=maybe"): + assert configuration("TINY_P10_ACCEPTANCE=YES", extra).returncode != 0 + + +@pytest.mark.parametrize("simulator", ["iverilog", "verilator"]) +def test_observer_preserves_boundary_transactions(tmp_path, simulator): + sources = [ROOT / "rtl/tiny/dv/tiny_p10_observer.sv", ROOT / "tests/rtl/tiny_p10_observer_tb.sv"] + model = tmp_path / "sim" + if simulator == "iverilog": + compile_command = ["iverilog", "-g2012", "-s", "tiny_p10_observer_tb", "-o", str(model)] + launch = ["vvp", str(model)] + else: + compile_command = ["verilator", "--binary", "--timing", "--assert", "-Wno-fatal", "-j", "4", + "--top-module", "tiny_p10_observer_tb", "--Mdir", str(tmp_path / "obj"), + "-o", str(model)] + launch = [str(model)] + compiled = subprocess.run([*compile_command, *map(str, sources)], capture_output=True, text=True, + timeout=120, check=False, env={**os.environ, "CCACHE_DISABLE": "1"}) + assert compiled.returncode == 0, compiled.stdout + compiled.stderr + good = subprocess.run(launch, capture_output=True, text=True, timeout=30, check=False) + content = good.stdout + good.stderr + p10.require_terminal(content, {"exit_code": good.returncode, "status": "passed"}) + result = p10.check_observations(content, 2, 28) + first, writer, _, _, _, last = result["bus"] + assert (first["reads"], first["read_initial"], first["read_done"], first["read_pending"]) == (0, 1, 1, 0) + assert first["read_bytes"] == 4 and first["ar_max"] == 2 and first["read_latency_max"] == 2 + assert writer["writes"] == writer["write_done"] == 1 and writer["write_bytes"] == 4 + assert last["reads"] == last["read_pending"] == 1 and last["read_done"] == 0 + assert result["concurrency"][0]["bank_overlap"] == 3 + bad = subprocess.run([*launch, "+orphan"], capture_output=True, text=True, timeout=30, check=False) + assert DEFAULT_FAILURE.search(bad.stdout + bad.stderr) + + +def camera_log(): + histogram, checksum = p10.golden() + content = [f"P10_RESULT code=0 pixels=76800 bytes=153600 sectors=300 checksum={checksum}"] + content += [f"P10_HIST bin={index} pixels={value}" for index, value in enumerate(histogram)] + content += [f"P10_PHASE id={index} calls={1 if index < 2 else 150} bytes=153600 cycles=100 instructions=50" + for index in range(5)] + for window in range(1, 453): + for owner in range(3): + row = dict.fromkeys(p10.BUS_FIELDS, 0) + row.update(window=window, owner=owner) + if window == 1 and owner == 1: + row["write_bytes"] = 153600 + row["writes"] = row["write_done"] = 9600 + if window >= 4 and (window - 4) % 3 == 0 and owner == 2: + row["read_bytes"], row["write_bytes"] = 1040, 4 + row["reads"] = row["read_done"] = 17 + row["writes"] = row["write_done"] = 1 + if window > 2 and (window - 2) % 3 == 0 and owner == 2: + row["read_bytes"], row["write_bytes"] = 16, 1028 + row["reads"] = row["read_done"] = 1 + row["writes"] = row["write_done"] = 17 + content.append("P10_BUS " + " ".join(f"{k}={v}" for k, v in sorted(row.items()))) + content.append(f"P10_CONCURRENCY window={window} active_targets_max=2 bank_overlap=1 dma_beat_overlap=0") + content += ["P9_OBSERVATION version=1 accepted_words=38400 delivered_words=38400 " + "fifo_high_water=4 backpressure=308 pixel_half_ns=500", "SIM_TEST_PASS Tiny cycles=100"] + return "\n".join(content) + + +def test_camera_oracle_and_complete_pipeline(): + histogram, checksum = p10.golden() + assert sum(histogram) == 76800 and checksum == 2323003269 + assert histogram[:10] == [2368] * 10 and histogram[-1] == 2371 + result = p10.camera_measurements(camera_log(), {"exit_code": 0, "status": "passed"}, 28) + assert result["phases"][4]["calls"] == 150 + assert result["sdio_bytes"]["observed_write"] == 154800 + assert result["sdio_bytes"]["observed_read"] == 158400 + + +@pytest.mark.parametrize("old,new", [ + ("code=0", "code=1"), ("sectors=300", "sectors=299"), + ("bin=31 pixels=2371", "bin=31 pixels=2370"), ("calls=150", "calls=149"), + ("read_pending=0", "read_pending=1"), ("errors=0", "errors=1"), + ("active_targets_max=2", "active_targets_max=29"), + ("fifo_high_water=4", "fifo_high_water=128"), ("backpressure=308", "backpressure=11904"), + ("pixel_half_ns=500", "pixel_half_ns=250"), ("SIM_TEST_PASS", "SIM_TEST_FAIL"), + ("write_bytes=1028", "write_bytes=1024"), ("read_bytes=1040", "read_bytes=1024"), + ("write_bytes=1028", "write_bytes=1027"), ("read_bytes=1040", "read_bytes=1044"), +]) +def test_pipeline_rejects_incomplete_or_corrupt_evidence(old, new): + with pytest.raises(ValueError): + p10.camera_measurements(camera_log().replace(old, new, 1), {"exit_code": 0, "status": "passed"}, 28) + + +@pytest.mark.parametrize("flow", [{"exit_code": 1, "status": "passed"}, + {"exit_code": 0, "status": "failed"}]) +def test_terminal_text_never_overrides_command_failure(flow): + with pytest.raises(ValueError): + p10.require_terminal("SIM_TEST_PASS Tiny cycles=100\n", flow) + + +def test_resume_rejects_missing_or_changed_source(tmp_path): + record = {"phase": p10.PHASE, "status": "passed", "source_inputs": {"a": "one"}, + "source_after": {"a": "one"}, "artifacts": {}, "source": {}} + path = tmp_path / "record.json" + p10.b.dump(path, record) + with pytest.raises(ValueError): + p10.checked_record(path, {"a": "two"}) + failed = deepcopy(record) + failed["status"] = "running" + p10.b.dump(path, failed) + with pytest.raises(ValueError): + p10.checked_record(path, {"a": "one"}) + + +def test_matched_comparison_rejects_different_work(): + row = dict(name="memory", payload_bytes=16384, checksum=7, kernel_calls=0, + cycles=200, instructions=100) + other = {**row, "cycles": 150} + compared = p10.compare_samples({"cases": [row]}, {"cases": [other]}) + assert compared[0]["cycle_ratio"] == 200 / 150 + other["payload_bytes"] = 65536 + with pytest.raises(ValueError): + p10.compare_samples({"cases": [row]}, {"cases": [other]}) + + +def test_collector_update_preserves_original_source_identity(): + source = {"scripts/tiny_r2_p10.py": "old", "rtl/tiny/top/retrosoc_tiny.sv": "rtl"} + record = {"source_inputs": deepcopy(source), "source_after": deepcopy(source)} + current = {**source, "scripts/tiny_r2_p10.py": "new"} + with pytest.raises(ValueError): + p10.source_delta(record, current) + assert p10.source_delta(record, current, True) == { + "scripts/tiny_r2_p10.py": {"before": "old", "after": "new"}} + assert record["source_inputs"] == record["source_after"] == source + for path in ("rtl/tiny/top/retrosoc_tiny.sv", "app/apps/ci_smoke/tiny_r2_p10.c", + "Makefile", "dependencies/dependencies.lock.json", "tests/unreviewed.py"): + with pytest.raises(ValueError): + p10.source_delta(record, {**current, path: "changed"}, True) + record["source_after"] = current + with pytest.raises(ValueError): + p10.source_delta(record, current, True) + + +def test_retained_section_sizes_are_not_elf_file_size(tmp_path): + log = tmp_path / "size.log" + log.write_text("firmware:\nsection size addr\n.init 204 0\n.text 9936 805306368\n" + ".bss 23116 805316352\n.comment 15 0\nTotal 33271\n") + image = tmp_path / "image.json" + p10.b.dump(image, {"artifacts": {"size.log": p10.b.artifact(log)}, + "layout": {"stack_headroom": 4096}}) + result = p10.retained_footprint(p10.b.artifact(image)) + assert result["sections"][".text"] == 9936 and result["stack_headroom"] == 4096 + log.write_text("modified") + with pytest.raises(ValueError): + p10.retained_footprint(p10.b.artifact(image))