diff --git a/.github/cases/blastoise/default.json b/.github/cases/blastoise/default.json index ed2b55d89..7d47a1ca6 100644 --- a/.github/cases/blastoise/default.json +++ b/.github/cases/blastoise/default.json @@ -1,7 +1,7 @@ { "mlir.rvv_vp_intrinsic_add": 386, - "mlir.rvv_vp_intrinsic_add_scalable": 581, - "mlir.hello": 94, + "mlir.rvv_vp_intrinsic_add_scalable": 567, + "mlir.hello": 91, "mlir.stripmining": 26732, "asm.mmm": 92609, "asm.smoke": 5017, @@ -499,15 +499,15 @@ "codegen.vfredusum_vs": 100812, "codegen.vfredmax_vs": 100812, "codegen.vfredmin_vs": 100812, - "rvv_bench.ascii_to_utf16": 1544388, - "rvv_bench.ascii_to_utf32": 674573, + "rvv_bench.ascii_to_utf16": 1544483, + "rvv_bench.ascii_to_utf32": 674689, "rvv_bench.byteswap": 3364202, "rvv_bench.chacha20": 0, "rvv_bench.mandelbrot": 4055251, - "rvv_bench.memcpy": 1485024, - "rvv_bench.memset": 373911, - "rvv_bench.mergelines": 3275863, + "rvv_bench.memcpy": 1717636, + "rvv_bench.memset": 373712, + "rvv_bench.mergelines": 3310606, "rvv_bench.poly1305": 0, - "rvv_bench.strlen": 876331, - "rvv_bench.utf8_count": 6320514 + "rvv_bench.strlen": 876017, + "rvv_bench.utf8_count": 6320478 } \ No newline at end of file diff --git a/.github/cases/machamp/default.json b/.github/cases/machamp/default.json index 8d2a86f62..7c24e91a6 100644 --- a/.github/cases/machamp/default.json +++ b/.github/cases/machamp/default.json @@ -1,6 +1,6 @@ { "mlir.rvv_vp_intrinsic_add": 410, - "mlir.rvv_vp_intrinsic_add_scalable": 626, + "mlir.rvv_vp_intrinsic_add_scalable": 596, "mlir.hello": 95, "mlir.stripmining": 14447, "asm.mmm": 91473, @@ -435,14 +435,14 @@ "codegen.vxor_vx": 74636, "codegen.vzext_vf2": 39984, "codegen.vzext_vf4": 6585, - "rvv_bench.ascii_to_utf16": 1464354, - "rvv_bench.ascii_to_utf32": 629517, + "rvv_bench.ascii_to_utf16": 1464627, + "rvv_bench.ascii_to_utf32": 629781, "rvv_bench.byteswap": 3294942, "rvv_bench.chacha20": 0, - "rvv_bench.memcpy": 1576831, - "rvv_bench.memset": 287462, - "rvv_bench.mergelines": 3092453, + "rvv_bench.memcpy": 1822850, + "rvv_bench.memset": 287111, + "rvv_bench.mergelines": 3128950, "rvv_bench.poly1305": 0, - "rvv_bench.strlen": 711050, - "rvv_bench.utf8_count": 5726377 + "rvv_bench.strlen": 711138, + "rvv_bench.utf8_count": 5726147 } \ No newline at end of file diff --git a/.github/cases/sandslash/default.json b/.github/cases/sandslash/default.json index 0f3249900..e6e1ddf54 100644 --- a/.github/cases/sandslash/default.json +++ b/.github/cases/sandslash/default.json @@ -1,6 +1,6 @@ { "mlir.rvv_vp_intrinsic_add": 432, - "mlir.rvv_vp_intrinsic_add_scalable": 734, + "mlir.rvv_vp_intrinsic_add_scalable": 702, "mlir.hello": 105, "mlir.stripmining": 15253, "asm.mmm": 91490, @@ -435,14 +435,14 @@ "codegen.vxor_vx": 523658, "codegen.vzext_vf2": 167305, "codegen.vzext_vf4": 26795, - "rvv_bench.ascii_to_utf16": 1392509, - "rvv_bench.ascii_to_utf32": 594028, + "rvv_bench.ascii_to_utf16": 1392595, + "rvv_bench.ascii_to_utf32": 594118, "rvv_bench.byteswap": 3602804, "rvv_bench.chacha20": 0, - "rvv_bench.memcpy": 1840029, - "rvv_bench.memset": 202628, - "rvv_bench.mergelines": 3052758, + "rvv_bench.memcpy": 2113707, + "rvv_bench.memset": 202519, + "rvv_bench.mergelines": 3042492, "rvv_bench.poly1305": 0, - "rvv_bench.strlen": 720670, - "rvv_bench.utf8_count": 4814077 + "rvv_bench.strlen": 720886, + "rvv_bench.utf8_count": 4814154 } \ No newline at end of file diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index c51e3044f..e4a4f9a32 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -157,26 +157,13 @@ jobs: fail-fast: false matrix: ${{ fromJSON(needs.gen-matrix.outputs.ci-tests) }} runs-on: [self-hosted, linux, nixos] - outputs: - result: ${{ steps.ci-run.outputs.result }} steps: - uses: actions/checkout@v4 with: ref: ${{ github.event.pull_request.head.sha }} - name: "Run testcases" - id: ci-run run: | - nix develop -c t1-helper runTests --jobs "${{ matrix.jobs }}" \ - --resultDir test-results-$(head -c 10 /dev/urandom | base32) - - - uses: actions/upload-artifact@v4 - if: ${{ !cancelled() }} - with: - name: test-reports-${{ matrix.id }} - path: | - test-results-*/failed-tests.md - test-results-*/cycle-updates.md - test-results-*/*_cycle.json + nix develop -c t1-helper runTests --jobs "${{ matrix.jobs }}" report: name: "Report CI result" @@ -191,21 +178,14 @@ jobs: with: fetch-depth: 0 ref: ${{ github.head_ref }} - - uses: actions/download-artifact@v4 - with: - pattern: test-reports-* - merge-multiple: true - name: "Print step summary" run: | - echo -e "\n## Failed tests\n" >> $GITHUB_STEP_SUMMARY - shopt -s nullglob - cat test-results-*/failed-tests.md >> $GITHUB_STEP_SUMMARY - echo -e "\n## Cycle updates\n" >> $GITHUB_STEP_SUMMARY - shopt -s nullglob - cat test-results-*/cycle-updates.md >> $GITHUB_STEP_SUMMARY + nix develop -c t1-helper postCI --failed-test-file-path ./failed-test.md --cycle-update-file-path ./cycle-update.md + cat ./failed-test.md >> $GITHUB_STEP_SUMMARY + echo >> $GITHUB_STEP_SUMMARY + cat ./cycle-update.md >> $GITHUB_STEP_SUMMARY - name: "Commit cycle updates" run: | - nix develop -c t1-helper mergeCycleData git config user.name github-actions git config user.email github-actions@github.com changed_cases=$(git diff --name-only '.github/cases/**/default.json') @@ -218,17 +198,3 @@ jobs: else echo "No cycle change detect" fi - - uses: geekyeggo/delete-artifact@v5 - with: - # test-reports has been used, it can be deleted - name: test-reports-* - - clean-after-cancelled: - name: "Clean test reports [ON CANCELLED]" - if: ${{ cancelled() }} - runs-on: [self-hosted, linux, nixos] - needs: [run-testcases] - steps: - - uses: geekyeggo/delete-artifact@v5 - with: - name: test-reports-* diff --git a/.gitignore b/.gitignore index c4d6197ba..502543d39 100644 --- a/.gitignore +++ b/.gitignore @@ -21,3 +21,4 @@ __pycache__ .envrc test-results target +.ccls-cache diff --git a/difftest/libspike_interfaces/spike_interfaces.cc b/difftest/libspike_interfaces/spike_interfaces.cc index 514877bab..e6b99e3b5 100644 --- a/difftest/libspike_interfaces/spike_interfaces.cc +++ b/difftest/libspike_interfaces/spike_interfaces.cc @@ -21,7 +21,7 @@ cfg_t make_spike_cfg(const std::string& varch) { return cfg; } -Spike::Spike(const char* arch, const char* set, const char* lvl) +Spike::Spike(const char* arch, const char* set, const char* lvl, size_t lane_number) : sim(), varch(arch), isa(set, lvl), @@ -34,13 +34,16 @@ Spike::Spike(const char* arch, const char* set, const char* lvl) /*halt on reset*/ true, /*log_file_t*/ nullptr, /*sout*/ std::cerr) { + proc.VU.lane_num = lane_number; + proc.VU.lane_granularity = 32; + auto& csrmap = proc.get_state()->csrmap; csrmap[CSR_MSIMEND] = std::make_shared(&proc, CSR_MSIMEND, 1); proc.enable_log_commits(); } -spike_t* spike_new(const char* arch, const char* set, const char* lvl) { - return new spike_t{new Spike(arch, set, lvl)}; +spike_t* spike_new(const char* arch, const char* set, const char* lvl, size_t lane_number) { + return new spike_t{new Spike(arch, set, lvl, lane_number)}; } const char* proc_disassemble(spike_processor_t* proc) { @@ -77,8 +80,8 @@ reg_t proc_get_insn(spike_processor_t* proc) { return fetch.insn.bits(); } -uint8_t* proc_get_vreg_addr(spike_processor_t* proc) { - return &proc->p->VU.elt(0, 0); +uint8_t proc_get_vreg_data(spike_processor_t* proc, uint32_t vreg_idx, uint32_t vreg_offset) { + return proc->p->VU.elt(vreg_idx, vreg_offset); } uint32_t extract_f32(freg_t f) { return (uint32_t)f.v[0]; } @@ -89,39 +92,22 @@ inline uint32_t clip(uint32_t binary, int a, int b) { return (binary >> a) & mask; } -uint64_t proc_get_rs(spike_processor_t* proc) { +uint32_t proc_get_rs1(spike_processor_t* proc) { auto pc = proc->p->get_state()->pc; auto fetch = proc->p->get_mmu()->load_insn(pc); - return (uint64_t)fetch.insn.rs1() << 32 | (uint64_t)fetch.insn.rs2(); + return (uint32_t)fetch.insn.rs1(); } -uint32_t proc_get_rd(spike_processor_t* proc) { +uint32_t proc_get_rs2(spike_processor_t* proc) { auto pc = proc->p->get_state()->pc; auto fetch = proc->p->get_mmu()->load_insn(pc); - return fetch.insn.rd(); + return (uint32_t)fetch.insn.rs2(); } -uint64_t proc_get_rs_bits(spike_processor_t* proc) { - auto state = proc->p->get_state(); - auto &xr = state->XPR; - auto &fr = state->FPR; - auto pc = state->pc; - auto inst_bits = proc_get_insn(proc); - - uint32_t opcode = clip(inst_bits, 0, 6); - uint32_t width = clip(inst_bits, 12, 14); // also funct3 +uint32_t proc_get_rd(spike_processor_t* proc) { + auto pc = proc->p->get_state()->pc; auto fetch = proc->p->get_mmu()->load_insn(pc); - uint32_t rs1_bits, rs2_bits; - bool is_fp_operands = opcode == 0b1010111 && (width == 0b101 /* OPFVF */); - if (is_fp_operands) { - rs1_bits = extract_f32(fr[fetch.insn.rs1()]); - rs2_bits = extract_f32(fr[fetch.insn.rs2()]); - } else { - rs1_bits = xr[fetch.insn.rs1()]; - rs2_bits = xr[fetch.insn.rs2()]; - } - - return (uint64_t)rs1_bits << 32 | (uint64_t)rs2_bits; + return fetch.insn.rd(); } uint64_t proc_vu_get_vtype(spike_processor_t* proc) { @@ -158,6 +144,10 @@ reg_t state_get_pc(spike_state_t* state) { return state->s->pc; } +void state_set_mcycle(spike_state_t* state, size_t mcycle) { + state->s->mcycle->write((int64_t)mcycle); +} + void state_clear(spike_state_t* state) { state->s->log_reg_write.clear(); state->s->log_mem_read.clear(); @@ -189,6 +179,29 @@ void state_set_pc(spike_state_t* state, uint64_t pc) { state->s->pc = pc; } +uint32_t state_get_reg(spike_state_t* state, uint32_t index, bool is_fp) { + if (is_fp) { + auto &fr = state->s->FPR; + return extract_f32(fr[index]); + } + auto &xr = state->s->XPR; + return (uint32_t)xr[index]; +} + +uint32_t state_get_reg_write_size(spike_state_t* state) { + return state->s->log_reg_write.size(); +} + +uint32_t state_get_reg_write_index(spike_state_t* state) { + int vec_idx = 0; + int i = 0; + for (auto [idx, data] : state->s->log_reg_write) { + vec_idx |= (idx & 0xf) << (i * 4); + i++; + } + return vec_idx; +} + uint32_t state_get_mem_write_size(spike_state_t* state) { return state->s->log_mem_write.size(); } diff --git a/difftest/libspike_interfaces/spike_interfaces.h b/difftest/libspike_interfaces/spike_interfaces.h index fcaf46bc7..df355abe4 100644 --- a/difftest/libspike_interfaces/spike_interfaces.h +++ b/difftest/libspike_interfaces/spike_interfaces.h @@ -31,7 +31,7 @@ class sim_t : public simif_t { class Spike { public: - Spike(const char* arch, const char* set, const char* lvl); + Spike(const char* arch, const char* set, const char* lvl, size_t lane_number); processor_t* get_proc() { return &proc; } private: diff --git a/difftest/libspike_interfaces/spike_interfaces_c.h b/difftest/libspike_interfaces/spike_interfaces_c.h index 2c2a73049..1bcb65f50 100644 --- a/difftest/libspike_interfaces/spike_interfaces_c.h +++ b/difftest/libspike_interfaces/spike_interfaces_c.h @@ -14,7 +14,7 @@ typedef struct spike_processor_t spike_processor_t; typedef struct spike_state_t spike_state_t; void spike_register_callback(ffi_callback callback); -spike_t* spike_new(const char* arch, const char* set, const char* lvl); +spike_t* spike_new(const char* arch, const char* set, const char* lvl, size_t lane_number); const char* proc_disassemble(spike_processor_t* proc); void proc_reset(spike_processor_t* proc); spike_processor_t* spike_get_proc(spike_t* spike); @@ -22,10 +22,10 @@ spike_state_t* proc_get_state(spike_processor_t* proc); uint64_t proc_func(spike_processor_t* proc); uint64_t proc_get_insn(spike_processor_t* proc); -uint8_t* proc_get_vreg_addr(spike_processor_t* proc); -uint64_t proc_get_rs(spike_processor_t* proc); +uint8_t proc_get_vreg_data(spike_processor_t* proc, uint32_t vreg_idx, uint32_t vreg_offset); +uint32_t proc_get_rs1(spike_processor_t* proc); +uint32_t proc_get_rs2(spike_processor_t* proc); uint32_t proc_get_rd(spike_processor_t* proc); -uint64_t proc_get_rs_bits(spike_processor_t* proc); uint64_t proc_vu_get_vtype(spike_processor_t* proc); uint32_t proc_vu_get_vxrm(spike_processor_t* proc); @@ -38,6 +38,9 @@ uint16_t proc_vu_get_vstart(spike_processor_t* proc); uint64_t state_get_pc(spike_state_t* state); uint64_t state_handle_pc(spike_state_t* state, uint64_t new_pc); void state_set_pc(spike_state_t* state, uint64_t pc); +uint32_t state_get_reg(spike_state_t* state, uint32_t index, bool is_fp); +uint32_t state_get_reg_write_size(spike_state_t* state); +uint32_t state_get_reg_write_index(spike_state_t* state); uint32_t state_get_mem_write_size(spike_state_t* state); uint32_t state_get_mem_write_addr(spike_state_t* state, uint32_t index); uint64_t state_get_mem_write_value(spike_state_t* state, uint32_t index); @@ -45,6 +48,7 @@ uint8_t state_get_mem_write_size_by_byte(spike_state_t* state, uint32_t index); uint32_t state_get_mem_read_size(spike_state_t* state); uint32_t state_get_mem_read_addr(spike_state_t* state, uint32_t index); uint8_t state_get_mem_read_size_by_byte(spike_state_t* state, uint32_t index); +void state_set_mcycle(spike_state_t* state, size_t mcycle); void state_clear(spike_state_t* state); void spike_destruct(spike_t* spike); diff --git a/difftest/t1-simulator/default.nix b/difftest/t1-simulator/default.nix index 643c2e51e..9138d3118 100644 --- a/difftest/t1-simulator/default.nix +++ b/difftest/t1-simulator/default.nix @@ -1,18 +1,26 @@ { lib , libspike , rustPlatform +, rust-analyzer , libspike_interfaces -, rtl }: -rustPlatform.buildRustPackage { - name = "t1-simulator"; - src = with lib.fileset; toSource { - root = ./.; - fileset = fileFilter (file: file.name != "default.nix") ./.; +let + self = rustPlatform.buildRustPackage { + name = "t1-simulator"; + src = with lib.fileset; toSource { + root = ./.; + fileset = fileFilter (file: file.name != "default.nix") ./.; + }; + passthru.devShell = self.overrideAttrs (old: { + nativeBuildInputs = old.nativeBuildInputs ++ [ + rust-analyzer + ]; + }); + buildInputs = [ libspike libspike_interfaces ]; + cargoLock = { + lockFile = ./Cargo.lock; + }; }; - buildInputs = [ libspike libspike_interfaces ]; - cargoLock = { - lockFile = ./Cargo.lock; - }; -} +in +self diff --git a/difftest/t1-simulator/readme.md b/difftest/t1-simulator/readme.md new file mode 100644 index 000000000..f82e152a0 --- /dev/null +++ b/difftest/t1-simulator/readme.md @@ -0,0 +1,11 @@ +## Build + +```bash +nix build ".#t1-simulator" +``` + +## Develop + +```bash +nix develop '.#t1-simulator.devShell' +``` diff --git a/difftest/t1-simulator/src/difftest.rs b/difftest/t1-simulator/src/difftest.rs new file mode 100644 index 000000000..6c19be573 --- /dev/null +++ b/difftest/t1-simulator/src/difftest.rs @@ -0,0 +1,169 @@ +mod dut; +mod spike; + +use dut::*; +pub use spike::SpikeHandle; +use std::path::Path; +use tracing::trace; + +pub struct Difftest { + spike: SpikeHandle, + dut: Dut, +} + +impl Difftest { + pub fn new( + size: usize, + elf_file: String, + log_file: String, + vlen: u32, + dlen: u32, + set: String, + ) -> Self { + Self { + spike: SpikeHandle::new(size, Path::new(&elf_file), vlen, dlen, set), + dut: Dut::new(Path::new(&log_file)), + } + } + + fn peek_issue(&mut self, issue: IssueEvent) -> anyhow::Result<()> { + self.spike.peek_issue(issue).unwrap(); + + Ok(()) + } + + fn update_lsu_idx(&mut self, lsu_enq: LsuEnqEvent) -> anyhow::Result<()> { + self.spike.update_lsu_idx(lsu_enq).unwrap(); + + Ok(()) + } + + fn poke_inst(&mut self) -> anyhow::Result<()> { + loop { + let se = self.spike.find_se_to_issue(); + if (se.is_vfence_insn || se.is_exit_insn) && self.spike.to_rtl_queue.len() == 1 { + if se.is_exit_insn { + return Ok(()); + } + + self.spike.to_rtl_queue.pop_back(); + } else { + break; + } + } + + // TODO: remove these, now just for aligning online difftest + if let Some(se) = self.spike.to_rtl_queue.front() { + // it is ensured there are some other instruction not committed, thus + // se_to_issue should not be issued + if se.is_vfence_insn || se.is_exit_insn { + assert!( + self.spike.to_rtl_queue.len() > 1, + "to_rtl_queue are smaller than expected" + ); + if se.is_exit_insn { + trace!("DPIPokeInst: exit waiting for fence"); + } else { + trace!("DPIPokeInst: waiting for fence, no issuing new instruction"); + } + } else { + trace!( + "DPIPokeInst: poke instruction: pc={:#x}, inst={}", + se.pc, + se.disasm + ); + } + } + Ok(()) + } + + pub fn diff(&mut self) -> anyhow::Result<()> { + self.poke_inst().unwrap(); + + let event = self.dut.step()?; + + match &*event.event { + "peekTL" => {} + "issue" => { + let idx = event.parameter.idx.unwrap(); + let cycle = event.parameter.cycle.unwrap(); + self.spike.cycle = cycle; + self.peek_issue(IssueEvent { idx, cycle }).unwrap(); + } + "lsuEnq" => { + let enq = event.parameter.enq.unwrap(); + let cycle = event.parameter.cycle.unwrap(); + self.spike.cycle = cycle; + self.update_lsu_idx(LsuEnqEvent { enq, cycle }).unwrap(); + } + "vrfWriteFromLsu" => { + let idx = event.parameter.idx.unwrap(); + let vd = event.parameter.vd.unwrap(); + let offset = event.parameter.offset.unwrap(); + let mask = event.parameter.mask.unwrap(); + let data = event.parameter.data.unwrap(); + let instruction = event.parameter.instruction.unwrap(); + let lane = event.parameter.lane.unwrap(); + let cycle = event.parameter.cycle.unwrap(); + self.spike.cycle = cycle; + assert!(idx < self.spike.config.dlen / 32); + + self + .spike + .peek_vrf_write_from_lsu(VrfWriteEvent { + idx: lane.trailing_zeros(), + vd, + offset, + mask, + data, + instruction, + cycle, + }) + .unwrap(); + } + "vrfWriteFromLane" => { + let idx = event.parameter.idx.unwrap(); + let vd = event.parameter.vd.unwrap(); + let offset = event.parameter.offset.unwrap(); + let mask = event.parameter.mask.unwrap(); + let data = event.parameter.data.unwrap(); + let instruction = event.parameter.instruction.unwrap(); + let cycle = event.parameter.cycle.unwrap(); + self.spike.cycle = cycle; + assert!(idx < self.spike.config.dlen / 32); + self + .spike + .peek_vrf_write_from_lane(VrfWriteEvent { + idx, + vd, + offset, + mask, + data, + instruction, + cycle, + }) + .unwrap(); + } + "inst" => { + let data = event.parameter.data.unwrap() as u32; + let cycle = event.parameter.cycle.unwrap(); + self.spike.cycle = cycle; + // let vxsat = event.parameter.vxsat.unwrap(); + // let rd_valid = event.parameter.rd_valid.unwrap(); + // let rd = event.parameter.rd.unwrap(); + // let mem = event.parameter.mem.unwrap(); + + let se = self.spike.to_rtl_queue.back().unwrap(); + se.record_rd_write(data).unwrap(); + se.check_is_ready_for_commit(cycle).unwrap(); + + self.spike.to_rtl_queue.pop_back(); + } + _ => { + panic!("unknown event: {}", event.event) + } + } + + Ok(()) + } +} diff --git a/difftest/t1-simulator/src/difftest/dut.rs b/difftest/t1-simulator/src/difftest/dut.rs new file mode 100644 index 000000000..51cb86fa1 --- /dev/null +++ b/difftest/t1-simulator/src/difftest/dut.rs @@ -0,0 +1,101 @@ +use serde::Deserialize; +use std::io::BufRead; +use std::path::Path; + +#[derive(Deserialize, Debug, PartialEq, Clone)] +pub enum Opcode { + PutFullData = 0, + PutPartialData = 1, + Get = 4, + // AccessAckData = 0, + // AccessAck = 0, +} + +#[derive(Deserialize, Debug)] +pub struct Parameter { + pub idx: Option, + pub enq: Option, + pub opcode: Option, + pub param: Option, + pub size: Option, + pub source: Option, + pub address: Option, + pub mask: Option, + pub data: Option, + pub corrupt: Option, + pub dready: Option, + pub vd: Option, + pub offset: Option, + pub instruction: Option, + pub lane: Option, + pub vxsat: Option, + pub rd_valid: Option, + pub rd: Option, + pub mem: Option, + pub cycle: Option, +} + +#[derive(Deserialize, Debug)] +pub struct JsonEvents { + pub event: String, + pub parameter: Parameter, +} + +pub struct IssueEvent { + pub idx: u32, + pub cycle: usize, +} + +pub struct LsuEnqEvent { + pub enq: u32, + pub cycle: usize, +} + +pub struct VrfWriteEvent { + pub idx: u32, + pub vd: u32, + pub offset: u32, + pub mask: u32, + pub data: u64, + pub instruction: u32, + pub cycle: usize, +} + +#[derive(Debug)] +pub struct Dut { + events: Vec, + idx: u32, +} + +impl Dut { + fn read_json(path: &Path) -> anyhow::Result> { + let file = std::fs::File::open(path).unwrap(); + let reader = std::io::BufReader::new(file); + + let mut events = Vec::new(); + + for line in reader.lines() { + let line = line.expect("line read error"); + let event: JsonEvents = serde_json::from_str(&line)?; + events.push(event); + } + + Ok(events) + } + + pub fn new(path: &Path) -> Self { + let events = Self::read_json(path).unwrap(); + let idx = 0; + Self { events, idx } + } + + pub fn step(&mut self) -> anyhow::Result<&JsonEvents> { + let event = match self.events.get(self.idx as usize) { + Some(event) => event, + None => return Err(anyhow::anyhow!("no more events")), + }; + self.idx += 1; + + Ok(event) + } +} diff --git a/difftest/t1-simulator/src/difftest/spike.rs b/difftest/t1-simulator/src/difftest/spike.rs new file mode 100644 index 000000000..9ddacaacb --- /dev/null +++ b/difftest/t1-simulator/src/difftest/spike.rs @@ -0,0 +1,404 @@ +use lazy_static::lazy_static; +use std::collections::VecDeque; +use std::fs::File; +use std::io::Read; +use std::path::Path; +use std::sync::Mutex; +use tracing::{info, trace}; +use xmas_elf::{ + header, + program::{ProgramHeader, Type}, + ElfFile, +}; + +mod libspike_interfaces; +use libspike_interfaces::*; + +mod spike_event; +use spike_event::*; + +use super::dut::*; + +const LSU_IDX_DEFAULT: u8 = 0xff; + +// read the addr from spike memory +// caller should make sure the address is valid +#[no_mangle] +pub extern "C" fn rs_addr_to_mem(addr: u64) -> *mut u8 { + let addr = addr as usize; + let mut spike_mem = SPIKE_MEM.lock().unwrap(); + let spike_mut = spike_mem.as_mut().unwrap(); + &mut spike_mut.mem[addr] as *mut u8 +} + +pub struct SpikeMem { + pub mem: Vec, + pub size: usize, +} + +lazy_static! { + static ref SPIKE_MEM: Mutex>> = Mutex::new(None); +} + +fn init_memory(size: usize) { + let mut spike_mem = SPIKE_MEM.lock().unwrap(); + if spike_mem.is_none() { + info!("Creating SpikeMem with size: 0x{:x}", size); + *spike_mem = Some(Box::new(SpikeMem { + mem: vec![0; size], + size, + })); + } +} + +fn ld(addr: usize, len: usize, bytes: Vec) -> anyhow::Result<()> { + trace!("ld: addr: 0x{:x}, len: 0x{:x}", addr, len); + let mut spike_mem = SPIKE_MEM.lock().unwrap(); + let spike_ref = spike_mem.as_mut().unwrap(); + + assert!(addr + len <= spike_ref.size); + + let dst = &mut spike_ref.mem[addr..addr + len]; + for (i, byte) in bytes.iter().enumerate() { + dst[i] = *byte; + } + + Ok(()) +} + +fn read_mem(addr: usize) -> anyhow::Result { + let mut spike_mem = SPIKE_MEM.lock().unwrap(); + let spike_ref = spike_mem.as_mut().unwrap(); + + let dst = &mut spike_ref.mem[addr]; + + Ok(*dst) +} + +fn load_elf(fname: &Path) -> anyhow::Result { + let mut file = File::open(fname).unwrap(); + let mut buffer = Vec::new(); + file.read_to_end(&mut buffer).unwrap(); + + let elf_file = ElfFile::new(&buffer).unwrap(); + + let header = elf_file.header; + assert_eq!(header.pt2.machine().as_machine(), header::Machine::RISC_V); + assert_eq!(header.pt1.class(), header::Class::ThirtyTwo); + + for ph in elf_file.program_iter() { + if let ProgramHeader::Ph32(ph) = ph { + if ph.get_type() == Ok(Type::Load) { + let offset = ph.offset as usize; + let size = ph.file_size as usize; + let addr = ph.virtual_addr as usize; + + let slice = &buffer[offset..offset + size]; + ld(addr, size, slice.to_vec()).unwrap(); + } + } + } + + Ok(header.pt2.entry_point()) +} + +pub fn clip(binary: u64, a: i32, b: i32) -> u32 { + assert!(a <= b, "a should be less than or equal to b"); + let nbits = b - a + 1; + let mask = if nbits >= 32 { + u32::MAX + } else { + (1 << nbits) - 1 + }; + (binary as u32 >> a) & mask +} + +pub struct Config { + pub vlen: u32, + pub dlen: u32, +} + +pub fn add_rtl_write(se: &mut SpikeEvent, vrf_write: VrfWriteEvent, record_idx_base: usize) { + (0..4).for_each(|j| { + if ((vrf_write.mask >> j) & 1) != 0 { + let written_byte = ((vrf_write.data >> (8 * j)) & 0xff) as u8; + let record_iter = se + .vrf_access_record + .all_writes + .get_mut(&(record_idx_base + j)); + + if let Some(record) = record_iter { + assert_eq!( + record.byte, + written_byte, + "{j}th byte incorrect ({:02X} != {written_byte:02X}) for vrf \ + write (lane={}, vd={}, offset={}, mask={:04b}) \ + [vrf_idx={}] (lsu_idx={}, disasm: {}, pc: {:#x}, bits: {:#x})", + record.byte, + vrf_write.idx, + vrf_write.vd, + vrf_write.offset, + vrf_write.mask, + record_idx_base + j, + se.lsu_idx, + se.disasm, + se.pc, + se.inst_bits + ); + record.executed = true; + } + } // end if mask + }) // end for j +} + +pub struct SpikeHandle { + spike: Spike, + + /// to rtl stack + /// in the spike thread, spike should detech if this queue is full, if not + /// full, execute until a vector instruction, record the behavior of this + /// instruction, and send to str_stack. in the RTL thread, the RTL driver will + /// consume from this queue, drive signal based on the queue. size of this + /// queue should be as big as enough to make rtl free to run, reducing the + /// context switch overhead. + pub to_rtl_queue: VecDeque, + + /// config for v extension + pub config: Config, + + /// implement the get_t() for mcycle csr update + pub cycle: usize, + + /// for mcycle csr update + pub spike_cycle: usize, +} + +impl SpikeHandle { + pub fn new(size: usize, fname: &Path, vlen: u32, dlen: u32, set: String) -> Self { + // register the addr_to_mem callback + unsafe { spike_register_callback(rs_addr_to_mem) } + + // create a new spike memory instance + init_memory(size); + + // load the elf file + let entry_addr = load_elf(fname).unwrap(); + + // initialize spike + let arch = &format!("vlen:{vlen},elen:32"); + let lvl = "M"; + + let spike = Spike::new(arch, &set, lvl, (dlen / 32) as usize); + + // initialize processor + let proc = spike.get_proc(); + let state = proc.get_state(); + proc.reset(); + state.set_pc(entry_addr); + + SpikeHandle { + spike, + to_rtl_queue: VecDeque::new(), + config: Config { vlen, dlen }, + cycle: 0, + spike_cycle: 0, + } + } + + // just execute one instruction for no-difftest + pub fn exec(&self) -> anyhow::Result<()> { + let spike = &self.spike; + let proc = spike.get_proc(); + let state = proc.get_state(); + + let new_pc = proc.func(); + + state.handle_pc(new_pc).unwrap(); + + let ret = state.exit(); + + if ret == 0 { + return Err(anyhow::anyhow!("simulation finished!")); + } + + Ok(()) + } + + // execute the spike processor for one instruction and record + // the spike event for difftest + pub fn spike_step(&mut self) -> Option { + let proc = self.spike.get_proc(); + let state = proc.get_state(); + + state.set_mcycle(self.cycle + self.spike_cycle); + + let pc = state.get_pc(); + let disasm = proc.disassemble(); + + let mut event = self.create_spike_event(); + state.clear(); + + let new_pc; + match event { + // inst is load / store / v / quit + Some(ref mut se) => { + info!( + "[{}] SpikeStep: spike run vector insn, pc={:#x}, disasm={:?}, spike_cycle={:?}", + self.cycle, pc, disasm, self.spike_cycle + ); + se.pre_log_arch_changes(&self.spike, self.config.vlen) + .unwrap(); + new_pc = proc.func(); + se.log_arch_changes(&self.spike, self.config.vlen).unwrap(); + } + None => { + info!( + "[{}] SpikeStep: spike run scalar insn, pc={:#x}, disasm={:?}, spike_cycle={:?}", + self.cycle, pc, disasm, self.spike_cycle + ); + new_pc = proc.func(); + } + } + + state.handle_pc(new_pc).unwrap(); + + self.spike_cycle += 1; + + event + } + + // step the spike processor until the instruction is load/store/v/quit + // if the instruction is load/store/v/quit, execute it and return + fn create_spike_event(&mut self) -> Option { + let spike = &self.spike; + let proc = spike.get_proc(); + + let insn = proc.get_insn(); + + let opcode = clip(insn, 0, 6); + let width = clip(insn, 12, 14); + let rs1 = clip(insn, 15, 19); + let csr = clip(insn, 20, 31); + + // early return vsetvl scalar instruction + let is_vsetvl = opcode == 0b1010111 && width == 0b111; + if is_vsetvl { + return None; + } + + let is_load_type = opcode == 0b0000111 && (((width - 1) & 0b100) != 0); + let is_store_type = opcode == 0b0100111 && (((width - 1) & 0b100) != 0); + let is_v_type = opcode == 0b1010111; + + let is_csr_type = opcode == 0b1110011 && ((width & 0b011) != 0); + let is_csr_write = is_csr_type && (((width & 0b100) | rs1) != 0); + + let is_quit = is_csr_write && csr == 0x7cc; + + if is_load_type || is_store_type || is_v_type || is_quit { + return SpikeEvent::new(spike); + } + None + } + + pub fn find_se_to_issue(&mut self) -> SpikeEvent { + // find the first instruction that is not issued from the back + for se in self.to_rtl_queue.iter().rev() { + if !se.is_issued { + return se.clone(); + } + } + + loop { + if let Some(se) = self.spike_step() { + self.to_rtl_queue.push_front(se.clone()); + return se; + } + } + } + + pub fn peek_issue(&mut self, issue: IssueEvent) -> anyhow::Result<()> { + let se = self.to_rtl_queue.front_mut().unwrap(); + if se.is_vfence_insn || se.is_exit_insn { + return Ok(()); + } + + se.is_issued = true; + se.issue_idx = issue.idx as u8; + + info!( + "[{}] SpikePeekIssue: idx={}, pc={:#x}, inst={}", + issue.cycle, issue.idx, se.pc, se.disasm + ); + + Ok(()) + } + + pub fn update_lsu_idx(&mut self, lsu_enq: LsuEnqEvent) -> anyhow::Result<()> { + let enq = lsu_enq.enq; + assert!(enq > 0, "enq should be greater than 0"); + let cycle = lsu_enq.cycle; + + if let Some(se) = self + .to_rtl_queue + .iter_mut() + .rev() + .find(|se| se.is_issued && (se.is_load || se.is_store) && se.lsu_idx == LSU_IDX_DEFAULT) + { + let index = enq.trailing_zeros() as u8; + se.lsu_idx = index; + info!("[{cycle}] UpdateLSUIdx: Instruction is allocated with pc: {:#x}, inst: {} and lsu_idx: {index}", se.pc, se.disasm); + } + Ok(()) + } + + pub fn peek_vrf_write_from_lsu(&mut self, vrf_write: VrfWriteEvent) -> anyhow::Result<()> { + let cycle = vrf_write.cycle; + let vlen_in_bytes = self.config.vlen / 8; + let lane_number = self.config.dlen / 32; + let record_idx_base = (vrf_write.vd * vlen_in_bytes + + (vrf_write.idx + lane_number * vrf_write.offset) * 4) as usize; + + if let Some(se) = self + .to_rtl_queue + .iter_mut() + .rev() + .find(|se| se.issue_idx == vrf_write.instruction as u8) + { + info!("[{cycle}] RecordRFAccesses: lane={}, vd={}, offset={}, mask={:04b}, data={:08x}, instruction={}, rtl detect vrf queue write" , vrf_write.idx, vrf_write.vd, vrf_write.offset, vrf_write.mask, vrf_write.data, vrf_write.instruction); + + add_rtl_write(se, vrf_write, record_idx_base); + return Ok(()); + } + + panic!( + "[{cycle}] cannot find se with issue_idx={}", + vrf_write.instruction + ) + } + + pub fn peek_vrf_write_from_lane(&mut self, vrf_write: VrfWriteEvent) -> anyhow::Result<()> { + let cycle = vrf_write.cycle; + let vlen_in_bytes = self.config.vlen / 8; + let lane_number = self.config.dlen / 32; + let record_idx_base = (vrf_write.vd * vlen_in_bytes + + (vrf_write.idx + lane_number * vrf_write.offset) * 4) as usize; + + if let Some(se) = self + .to_rtl_queue + .iter_mut() + .rev() + .find(|se| se.issue_idx == vrf_write.instruction as u8) + { + if !se.is_load { + info!("[{cycle}] RecordRFAccesses: lane={}, vd={}, offset={}, mask={:04b}, data={:08x}, instruction={}, rtl detect vrf write", vrf_write.idx, vrf_write.vd, vrf_write.offset, vrf_write.mask, vrf_write.data, vrf_write.instruction); + + add_rtl_write(se, vrf_write, record_idx_base); + } + return Ok(()); + } + + info!("[{cycle}] RecordRFAccess: index={} rtl detect vrf write which cannot find se, maybe from committed load insn", vrf_write.idx); + Ok(()) + } +} diff --git a/difftest/t1-simulator/src/spike/libspike_interfaces.rs b/difftest/t1-simulator/src/difftest/spike/libspike_interfaces.rs similarity index 77% rename from difftest/t1-simulator/src/spike/libspike_interfaces.rs rename to difftest/t1-simulator/src/difftest/spike/libspike_interfaces.rs index 84d0d225c..a3535fa63 100644 --- a/difftest/t1-simulator/src/spike/libspike_interfaces.rs +++ b/difftest/t1-simulator/src/difftest/spike/libspike_interfaces.rs @@ -6,16 +6,16 @@ pub struct Spike { } impl Spike { - pub fn new(arch: &str, set: &str, lvl: &str) -> Self { + pub fn new(arch: &str, set: &str, lvl: &str, lane_number: usize) -> Self { let arch = CString::new(arch).unwrap(); let set = CString::new(set).unwrap(); let lvl = CString::new(lvl).unwrap(); - let spike = unsafe { spike_new(arch.as_ptr(), set.as_ptr(), lvl.as_ptr()) }; + let spike = unsafe { spike_new(arch.as_ptr(), set.as_ptr(), lvl.as_ptr(), lane_number) }; Spike { spike } } pub fn get_proc(&self) -> Processor { - let processor = unsafe { spike_get_proc(self.spike as *mut ()) }; + let processor = unsafe { spike_get_proc(self.spike) }; Processor { processor } } } @@ -31,10 +31,10 @@ pub struct Processor { } impl Processor { - pub fn disassemble(&self) -> std::borrow::Cow { + pub fn disassemble(&self) -> String { let bytes = unsafe { proc_disassemble(self.processor) }; let c_str = unsafe { CStr::from_ptr(bytes as *mut c_char) }; - c_str.to_string_lossy() + format!("{}", c_str.to_string_lossy()) } pub fn reset(&self) { @@ -54,22 +54,20 @@ impl Processor { unsafe { proc_get_insn(self.processor) } } - pub fn get_vreg_addr(&self) -> *mut u8 { - unsafe { proc_get_vreg_addr(self.processor) } + pub fn get_vreg_data(&self, idx: u32, offset: u32) -> u8 { + unsafe { proc_get_vreg_data(self.processor, idx, offset) } } - pub fn get_rs(&self) -> (u32, u32) { - let rs: u64 = unsafe { proc_get_rs(self.processor) }; - ((rs >> 32) as u32, rs as u32) + pub fn get_rs1(&self) -> u32 { + unsafe { proc_get_rs1(self.processor) } } - pub fn get_rd(&self) -> u32 { - unsafe { proc_get_rd(self.processor) } + pub fn get_rs2(&self) -> u32 { + unsafe { proc_get_rs2(self.processor) } } - pub fn get_rs_bits(&self) -> (u32, u32) { - let rs_bits: u64 = unsafe { proc_get_rs_bits(self.processor) }; - ((rs_bits >> 32) as u32, rs_bits as u32) + pub fn get_rd(&self) -> u32 { + unsafe { proc_get_rd(self.processor) } } // vu @@ -128,6 +126,18 @@ impl State { } } + pub fn get_reg(&self, idx: u32, is_fp: bool) -> u32 { + unsafe { state_get_reg(self.state, idx, is_fp) } + } + + pub fn get_reg_write_size(&self) -> u32 { + unsafe { state_get_reg_write_size(self.state) } + } + + pub fn get_reg_write_index(&self, index: u32) -> u32 { + unsafe { state_get_reg_write_index(self.state) >> (index * 4) } + } + pub fn get_mem_write_size(&self) -> u32 { unsafe { state_get_mem_write_size(self.state) } } @@ -149,6 +159,10 @@ impl State { (addr, size_by_byte) } + pub fn set_mcycle(&self, mcycle: usize) { + unsafe { state_set_mcycle(self.state, mcycle) } + } + pub fn clear(&self) { unsafe { state_clear(self.state) } } @@ -169,7 +183,7 @@ type FfiCallback = extern "C" fn(u64) -> *mut u8; #[link(name = "spike_interfaces")] extern "C" { pub fn spike_register_callback(callback: FfiCallback); - fn spike_new(arch: *const c_char, set: *const c_char, lvl: *const c_char) -> *mut (); + fn spike_new(arch: *const c_char, set: *const c_char, lvl: *const c_char, lane_number: usize) -> *mut (); fn spike_get_proc(spike: *mut ()) -> *mut (); fn spike_destruct(spike: *mut ()); fn proc_disassemble(proc: *mut ()) -> *mut c_char; @@ -177,10 +191,10 @@ extern "C" { fn proc_get_state(proc: *mut ()) -> *mut (); fn proc_func(proc: *mut ()) -> u64; fn proc_get_insn(proc: *mut ()) -> u64; - fn proc_get_vreg_addr(proc: *mut ()) -> *mut u8; - fn proc_get_rs(proc: *mut ()) -> u64; + fn proc_get_vreg_data(proc: *mut (), vreg_idx: u32, vreg_offset: u32) -> u8; + fn proc_get_rs1(proc: *mut ()) -> u32; + fn proc_get_rs2(proc: *mut ()) -> u32; fn proc_get_rd(proc: *mut ()) -> u32; - fn proc_get_rs_bits(proc: *mut ()) -> u64; fn proc_vu_get_vtype(proc: *mut ()) -> u64; fn proc_vu_get_vxrm(proc: *mut ()) -> u32; @@ -193,6 +207,9 @@ extern "C" { fn proc_destruct(proc: *mut ()); fn state_set_pc(state: *mut (), pc: u64); fn state_get_pc(state: *mut ()) -> u64; + fn state_get_reg(state: *mut (), index: u32, is_fp: bool) -> u32; + fn state_get_reg_write_size(state: *mut ()) -> u32; + fn state_get_reg_write_index(state: *mut ()) -> u32; fn state_get_mem_write_size(state: *mut ()) -> u32; fn state_get_mem_write_addr(state: *mut (), index: u32) -> u32; fn state_get_mem_write_value(state: *mut (), index: u32) -> u64; @@ -201,6 +218,7 @@ extern "C" { fn state_get_mem_read_addr(state: *mut (), index: u32) -> u32; fn state_get_mem_read_size_by_byte(state: *mut (), index: u32) -> u8; fn state_handle_pc(state: *mut (), pc: u64) -> u64; + fn state_set_mcycle(state: *mut (), mcycle: usize); fn state_clear(state: *mut ()); fn state_destruct(state: *mut ()); fn state_exit(state: *mut ()) -> u64; diff --git a/difftest/t1-simulator/src/difftest/spike/spike_event.rs b/difftest/t1-simulator/src/difftest/spike/spike_event.rs new file mode 100644 index 000000000..e027f9607 --- /dev/null +++ b/difftest/t1-simulator/src/difftest/spike/spike_event.rs @@ -0,0 +1,402 @@ +use super::Spike; +use super::{clip, read_mem}; +use std::collections::HashMap; +use tracing::{info, trace}; + +#[derive(Debug, Clone)] +pub struct SingleMemWrite { + pub val: u8, + pub executed: bool, // set to true when rtl execute this mem access +} + +#[derive(Debug, Clone)] +pub struct SingleMemRead { + pub val: u8, + pub executed: bool, // set to true when rtl execute this mem access +} + +#[derive(Debug, Clone)] +pub struct MemWriteRecord { + pub writes: Vec, + pub num_completed_writes: usize, +} + +#[derive(Debug, Clone)] +pub struct MemReadRecord { + pub reads: Vec, + pub num_completed_reads: usize, +} + +#[derive(Debug, Clone)] +pub struct SingleVrfWrite { + pub byte: u8, + pub executed: bool, // set to true when rtl execute this mem access +} + +#[derive(Default, Debug, Clone)] +pub struct VdWriteRecord { + vd_bytes: Vec, +} + +#[derive(Default, Debug, Clone)] +pub struct MemAccessRecord { + pub all_writes: HashMap, + pub all_reads: HashMap, +} + +#[derive(Default, Debug, Clone)] +pub struct VrfAccessRecord { + pub all_writes: HashMap, +} + +#[derive(Default, Debug, Clone)] +pub struct SpikeEvent { + pub lsu_idx: u8, + pub issue_idx: u8, + + pub is_issued: bool, + + pub is_load: bool, + pub is_store: bool, + pub is_whole: bool, + pub is_widening: bool, + pub is_mask_vd: bool, + pub is_exit_insn: bool, + pub is_vfence_insn: bool, + + pub pc: u64, + pub inst_bits: u64, + + // scalar to vector interface(used for driver) + pub rs1_bits: u32, + pub rs2_bits: u32, + pub rd_idx: u32, + + // vtype + pub vsew: u32, + pub vlmul: u32, + pub vma: bool, + pub vta: bool, + pub vxrm: u32, + pub vnf: u32, + + // other CSR + pub vill: bool, + pub vxsat: bool, + + pub vl: u32, + pub vstart: u16, + pub disasm: String, + + pub vd_write_record: VdWriteRecord, + + pub is_rd_written: bool, + pub rd_bits: u32, + pub is_rd_fp: bool, // whether rd is a fp register + + pub mem_access_record: MemAccessRecord, + pub vrf_access_record: VrfAccessRecord, +} + +impl SpikeEvent { + pub fn new(spike: &Spike) -> Option { + let inst_bits = spike.get_proc().get_insn(); + // inst info + let opcode = clip(inst_bits, 0, 6); + let width = clip(inst_bits, 12, 14); // also funct3 + let funct6 = clip(inst_bits, 26, 31); + let mop = clip(inst_bits, 26, 27); + let lumop = clip(inst_bits, 20, 24); + let vm = clip(inst_bits, 25, 25); + + // rs1, rs2 + let is_rs_fp = opcode == 0b1010111 && width == 0b101/* OPFVF */; + let proc = spike.get_proc(); + let state = proc.get_state(); + let (rs1, rs2) = (proc.get_rs1(), proc.get_rs2()); + + // vtype + let vtype = proc.vu_get_vtype(); + + Some(SpikeEvent { + lsu_idx: 255, + issue_idx: 255, + inst_bits, + rs1_bits: state.get_reg(rs1, is_rs_fp), + rs2_bits: state.get_reg(rs2, is_rs_fp), + // rd + is_rd_fp: (opcode == 0b1010111) + && (rs1 == 0) + && (funct6 == 0b010000) + && (vm == 1) + && (width == 0b001), + rd_idx: proc.get_rd(), + is_rd_written: false, + + // vtype + vlmul: clip(vtype, 0, 2), + vma: clip(vtype, 7, 7) != 0, + vta: clip(vtype, 6, 6) != 0, + vsew: clip(vtype, 3, 5), + vxrm: proc.vu_get_vxrm(), + vnf: proc.vu_get_vnf(), + + vill: proc.vu_get_vill(), + vxsat: proc.vu_get_vxsat(), + vl: proc.vu_get_vl(), + vstart: proc.vu_get_vstart(), + + // se info + disasm: spike.get_proc().disassemble(), + pc: proc.get_state().get_pc(), + is_load: opcode == 0b0000111, + is_store: opcode == 0b0100111, + is_whole: mop == 0 && lumop == 8, + is_widening: opcode == 0b1010111 && (funct6 >> 4) == 0b11, + is_mask_vd: opcode == 0b1010111 && (funct6 >> 3 == 0b011 || funct6 == 0b010001), + is_exit_insn: opcode == 0b1110011, + is_vfence_insn: false, + + is_issued: false, + ..Default::default() + }) + } + + pub fn get_vrf_write_range(&self, vlen_in_bytes: u32) -> anyhow::Result<(u32, u32)> { + if self.is_store { + return Ok((0, 0)); + } + + if self.is_load { + let vd_bytes_start = self.rd_idx * vlen_in_bytes; + if self.is_whole { + return Ok((vd_bytes_start, vlen_in_bytes * (1 + self.vnf))); + } + let len = if self.vlmul & 0b100 != 0 { + vlen_in_bytes * (1 + self.vnf) + } else { + (vlen_in_bytes * (1 + self.vnf)) << self.vlmul + }; + return Ok((vd_bytes_start, len)); + } + + let vd_bytes_start = self.rd_idx * vlen_in_bytes; + + if self.is_mask_vd { + return Ok((vd_bytes_start, vlen_in_bytes)); + } + + let len = if self.vlmul & 0b100 != 0 { + vlen_in_bytes >> (8 - self.vlmul) + } else { + vlen_in_bytes << self.vlmul + }; + + Ok((vd_bytes_start, if self.is_widening { len * 2 } else { len })) + } + + pub fn pre_log_arch_changes(&mut self, spike: &Spike, vlen: u32) -> anyhow::Result<()> { + self.rd_bits = spike.get_proc().get_rd(); + + // record the vrf writes before executing the insn + let vlen_in_bytes = vlen; + + let proc = spike.get_proc(); + let (start, len) = self.get_vrf_write_range(vlen_in_bytes).unwrap(); + self.vd_write_record.vd_bytes.resize(len as usize, 0u8); + for i in 0..len { + let offset = start + i; + let vreg_index = offset / vlen_in_bytes; + let vreg_offset = offset % vlen_in_bytes; + let cur_byte = proc.get_vreg_data(vreg_index, vreg_offset); + self.vd_write_record.vd_bytes[i as usize] = cur_byte; + } + + Ok(()) + } + + pub fn log_arch_changes(&mut self, spike: &Spike, vlen: u32) -> anyhow::Result<()> { + self.log_vrf_write(spike, vlen).unwrap(); + self.log_reg_write(spike).unwrap(); + self.log_mem_write(spike).unwrap(); + self.log_mem_read(spike).unwrap(); + + Ok(()) + } + + fn log_vrf_write(&mut self, spike: &Spike, vlen: u32) -> anyhow::Result<()> { + let proc = spike.get_proc(); + // record vrf writes + // note that we do not need log_reg_write to find records, we just decode the + // insn and compare bytes + let vlen_in_bytes = vlen / 8; + let (start, len) = self.get_vrf_write_range(vlen_in_bytes).unwrap(); + trace!("start: {start}, len: {len}"); + for i in 0..len { + let offset = start + i; + let origin_byte = self.vd_write_record.vd_bytes[i as usize]; + let vreg_index = offset / vlen_in_bytes; + let vreg_offset = offset % vlen_in_bytes; + let cur_byte = proc.get_vreg_data(vreg_index, vreg_offset); + if origin_byte != cur_byte { + self + .vrf_access_record + .all_writes + .entry(offset as usize) + .or_insert(SingleVrfWrite { + byte: cur_byte, + executed: false, + }); + trace!( + "SpikeVRFChange: vrf={:?}, change_from={origin_byte}, change_to={cur_byte}, vrf_idx={offset}", + vec![offset / vlen_in_bytes, offset % vlen_in_bytes], + ); + } + } + Ok(()) + } + + fn log_reg_write(&mut self, spike: &Spike) -> anyhow::Result<()> { + let proc = spike.get_proc(); + let state = proc.get_state(); + // in spike, log_reg_write is arrange: + // xx0000 <- x + // xx0001 <- f + // xx0010 <- vreg + // xx0011 <- vec + // xx0100 <- csr + let reg_write_size = state.get_reg_write_size(); + // TODO: refactor it. + (0..reg_write_size).for_each(|idx| match state.get_reg_write_index(idx) & 0xf { + 0b0000 => { + // scalar rf + let data = state.get_reg(self.rd_idx, false); + if data != self.rd_bits { + trace!( + "ScalarRFChange: idx={}, change_from={}, change_to={data}", + self.rd_idx, self.rd_bits + ); + self.rd_bits = data; + self.is_rd_written = true; + } + } + 0b0001 => { + let data = state.get_reg(self.rd_idx, true); + if data != self.rd_bits { + trace!( + "FloatRFChange: idx={}, change_from={}, change_to={data}", + self.rd_idx, self.rd_bits + ); + self.rd_bits = data; + self.is_rd_written = true; + } + } + _ => trace!("UnknownRegChange, idx={:08x}, spike detect unknown reg change", state.get_reg_write_index(idx)), + }); + + Ok(()) + } + + fn log_mem_write(&mut self, spike: &Spike) -> anyhow::Result<()> { + let proc = spike.get_proc(); + let state = proc.get_state(); + + let mem_write_size = state.get_mem_write_size(); + (0..mem_write_size).for_each(|i| { + let (addr, value, size) = state.get_mem_write(i); + (0..size).for_each(|offset| { + self + .mem_access_record + .all_writes + .entry(addr + offset as u32) + .or_insert(MemWriteRecord { + writes: vec![], + num_completed_writes: 0, + }) + .writes + .push(SingleMemWrite { + val: (value >> (offset * 8)) as u8, + executed: false, + }); + }); + info!("SpikeMemWrite: addr={addr:x}, value={value:x}, size={size}"); + }); + + Ok(()) + } + + fn log_mem_read(&mut self, spike: &Spike) -> anyhow::Result<()> { + let proc = spike.get_proc(); + let state = proc.get_state(); + + let mem_read_size = state.get_mem_read_size(); + (0..mem_read_size).for_each(|i| { + let (addr, size) = state.get_mem_read(i); + let mut value = 0; + (0..size).for_each(|offset| { + let byte = read_mem(addr as usize + offset as usize).unwrap(); + value |= (byte as u64) << (offset * 8); + // record the read + self + .mem_access_record + .all_reads + .entry(addr + offset as u32) + .or_insert(MemReadRecord { + reads: vec![], + num_completed_reads: 0, + }) + .reads + .push(SingleMemRead { + val: byte, + executed: false, + }); + }); + info!("SpikeMemRead: addr={addr:x}, value={value:x}, size={size}"); + }); + + Ok(()) + } + + pub fn record_rd_write(&self, data: u32) -> anyhow::Result<()> { + // TODO: rtl should indicate whether resp_bits_data is valid + if self.is_rd_written { + assert_eq!( + data, self.rd_bits, + "expect to write rd[{}] = {}, actual {}", + self.rd_idx, self.rd_bits, data + ); + } + + Ok(()) + } + + pub fn check_is_ready_for_commit(&self, cycle: usize) -> anyhow::Result<()> { + // for (addr, record) in &self.mem_access_record.all_writes { + // assert_eq!( + // record.num_completed_writes, + // record.writes.len(), + // "[{cycle}] expect to write mem {addr:#x}, not executed when commit (pc={:#x}, inst={})", + // self.pc, + // self.disasm + // ); + // } + // for (addr, record) in &self.mem_access_record.all_reads { + // assert_eq!( + // record.num_completed_reads, + // record.reads.len(), + // "[{cycle}] expect to read mem {addr:#x}, not executed when commit (pc={:#x}, inst={})", + // self.pc, + // self.disasm + // ); + // } + for (idx, record) in &self.vrf_access_record.all_writes { + assert!( + record.executed, + "[{cycle}] expect to write vrf {idx}, not executed when commit (pc={:#x}, inst={})", + self.pc, self.disasm + ); + } + + Ok(()) + } +} diff --git a/difftest/t1-simulator/src/main.rs b/difftest/t1-simulator/src/main.rs index 3f75f10ca..906018f88 100644 --- a/difftest/t1-simulator/src/main.rs +++ b/difftest/t1-simulator/src/main.rs @@ -1,7 +1,8 @@ -mod spike; +mod difftest; use clap::Parser; -use spike::SpikeHandle; +use difftest::Difftest; +use difftest::SpikeHandle; use std::path::Path; use tracing::{info, Level}; use tracing_subscriber::{EnvFilter, FmtSubscriber}; @@ -14,19 +15,62 @@ struct Args { #[arg(short, long)] elf_file: String, - /// count step of instruction trace - #[arg(long, default_value = "100000")] - step: u64, + /// Path to the log file + #[arg(short, long)] + log_file: Option, + + /// Log level: trace, debug, info, warn, error + #[arg(short, long, default_value = "info")] + log_level: String, - /// vlen of the vector extension - #[arg(long, default_value = "1024")] + /// vlen config (default blastoise 512) + #[arg(short, long)] vlen: u32, + + /// dlen config (default blastoise 256) + #[arg(short, long)] + dlen: u32, + + /// ISA config + #[arg(short, long, default_value = "rv32gcv")] + set: String, +} + +fn run_spike(args: Args) -> anyhow::Result<()> { + let mut count: u64 = 0; + + let spike = SpikeHandle::new( + 1usize << 32, + Path::new(&args.elf_file), + args.vlen, + args.dlen, + args.set, + ); + loop { + count += 1; + if count % 1000000 == 0 { + info!("count = {}", count); + } + match spike.exec() { + Ok(_) => {} + Err(_) => { + info!("total v instrucions count = {}", count); + info!("Simulation quit graceful"); + return Ok(()); + } + }; + } } fn main() -> anyhow::Result<()> { + // parse args + let args = Args::parse(); + + // setup log + let log_level: Level = args.log_level.parse()?; let global_logger = FmtSubscriber::builder() .with_env_filter(EnvFilter::from_default_env()) - .with_max_level(Level::TRACE) + .with_max_level(log_level) .without_time() .with_target(false) .compact() @@ -34,31 +78,27 @@ fn main() -> anyhow::Result<()> { tracing::subscriber::set_global_default(global_logger) .expect("internal error: fail to setup log subscriber"); - let args = Args::parse(); + // if there is no log file, just run spike and quit + if args.log_file.is_none() { + run_spike(args)?; + return Ok(()); + } - // count the instruction - let mut count: u64 = 0; + // if there is a log file, run difftest + let mut diff = Difftest::new( + 1usize << 32, + args.elf_file, + args.log_file.unwrap(), + args.vlen, + args.dlen, + args.set, + ); - // if there is no log file, just run spike and quit - let spike = SpikeHandle::new(1usize << 32, Path::new(&args.elf_file), args.vlen); loop { - count += 1; - if count % args.step == 0 { - info!( - "count = {}, pc = {:#x}, inst = {}", - count, - spike.get_pc(), - spike.get_disasm() - ); - } - - // TODO:Thinking about add features to capture exceptions like Illegal Instruction. - // And T1 will add more SoC-level checker, e.g. memory boundary checker. - match spike.exec() { + match diff.diff() { Ok(_) => {} Err(e) => { - info!("total instrucions count = {}", count); - info!("Simulation quit with error/quit: {:?}", e); + info!("Simulation quit/error with {}", e); return Ok(()); } } diff --git a/difftest/t1-simulator/src/spike.rs b/difftest/t1-simulator/src/spike.rs deleted file mode 100644 index e9279185d..000000000 --- a/difftest/t1-simulator/src/spike.rs +++ /dev/null @@ -1,148 +0,0 @@ -use lazy_static::lazy_static; -use std::fs::File; -use std::io::Read; -use std::path::Path; -use std::sync::Mutex; -use tracing::{info, trace}; -use xmas_elf::{ - header, - program::{ProgramHeader, Type}, - ElfFile, -}; - -mod libspike_interfaces; -use libspike_interfaces::*; - -// read the addr from spike memory -// caller should make sure the address is valid -#[no_mangle] -pub extern "C" fn rs_addr_to_mem(addr: u64) -> *mut u8 { - let addr = addr as usize; - let mut spike_mem = SPIKE_MEM.lock().unwrap(); - let spike_mut = spike_mem.as_mut().unwrap(); - &mut spike_mut.mem[addr] as *mut u8 -} - -pub struct SpikeMem { - pub mem: Vec, - pub size: usize, -} - -lazy_static! { - static ref SPIKE_MEM: Mutex>> = Mutex::new(None); -} - -fn init_memory(size: usize) { - let mut spike_mem = SPIKE_MEM.lock().unwrap(); - if spike_mem.is_none() { - info!("Creating SpikeMem with size: 0x{:x}", size); - *spike_mem = Some(Box::new(SpikeMem { - mem: vec![0; size], - size, - })); - } -} - -fn ld(addr: usize, len: usize, bytes: Vec) -> anyhow::Result<()> { - trace!("ld: addr: 0x{:x}, len: 0x{:x}", addr, len); - let mut spike_mem = SPIKE_MEM.lock().unwrap(); - let spike_ref = spike_mem.as_mut().unwrap(); - - assert!(addr + len <= spike_ref.size); - - let dst = &mut spike_ref.mem[addr..addr + len]; - for (i, byte) in bytes.iter().enumerate() { - dst[i] = *byte; - } - - Ok(()) -} - -fn load_elf(fname: &Path) -> anyhow::Result { - let mut file = File::open(fname).unwrap(); - let mut buffer = Vec::new(); - file.read_to_end(&mut buffer).unwrap(); - - let elf_file = ElfFile::new(&buffer).unwrap(); - - let header = elf_file.header; - assert_eq!(header.pt2.machine().as_machine(), header::Machine::RISC_V); - assert_eq!(header.pt1.class(), header::Class::ThirtyTwo); - - for ph in elf_file.program_iter() { - match ph { - ProgramHeader::Ph32(ph) => { - if ph.get_type() == Ok(Type::Load) { - let offset = ph.offset as usize; - let size = ph.file_size as usize; - let addr = ph.virtual_addr as usize; - - let slice = &buffer[offset..offset + size]; - ld(addr, size, slice.to_vec()).unwrap(); - } - } - _ => (), - } - } - - Ok(header.pt2.entry_point()) -} - -pub struct SpikeHandle { - spike: Spike, -} - -impl SpikeHandle { - pub fn new(size: usize, fname: &Path, vlen: u32) -> Self { - // register the addr_to_mem callback - unsafe { spike_register_callback(rs_addr_to_mem) } - - // create a new spike memory instance - init_memory(size); - - // load the elf file - let entry_addr = load_elf(fname).unwrap(); - - // initialize spike - let arch = &format!("vlen:{},elen:32", vlen); - let set = "rv32imacv"; - let lvl = "M"; - - let spike = Spike::new(arch, set, lvl); - - // initialize processor - let proc = spike.get_proc(); - let state = proc.get_state(); - proc.reset(); - state.set_pc(entry_addr); - - SpikeHandle { spike } - } - - // just execute one instruction for no-difftest - pub fn exec(&self) -> anyhow::Result<()> { - let spike = &self.spike; - let proc = spike.get_proc(); - let state = proc.get_state(); - - let new_pc = proc.func(); - - state.handle_pc(new_pc).unwrap(); - - let ret = state.exit(); - - if ret == 0 { - return Err(anyhow::anyhow!("simulation finished!")); - } - - Ok(()) - } - - pub fn get_pc(&self) -> u64 { - self.spike.get_proc().get_state().get_pc() - } - - pub fn get_disasm(&self) -> String { - format!("{:?}", self.spike.get_proc().disassemble()) - } -} diff --git a/flake.nix b/flake.nix index 73c28df8a..089dbbb23 100644 --- a/flake.nix +++ b/flake.nix @@ -22,9 +22,10 @@ # TODO: The dev shell will only depends on the T1 script package, let it manage different dev/ci/release flows. default = pkgs.mkShell { buildInputs = with pkgs; [ - # To develop T1-script, run nix develop .#t1-script.dev ammonite + # To develop T1-script, run nix develop .#t1-script.withLsp t1-script + zstd ]; }; }; diff --git a/ipemu/csrc/dpi.cc b/ipemu/csrc/dpi.cc index 3ecc9c7b5..4dd44668e 100644 --- a/ipemu/csrc/dpi.cc +++ b/ipemu/csrc/dpi.cc @@ -15,6 +15,7 @@ static bool terminated = false; void sigint_handler(int s) { + ProgramOutputStoreFile.close(); terminated = true; dpi_finish(); } @@ -26,12 +27,14 @@ void sigint_handler(int s) { } \ } catch (ReturnException & e) { \ terminated = true; \ + ProgramOutputStoreFile.close(); \ Log("SimulationExit") \ .info("detect returning instruction, gracefully quit simulation"); \ - vbridge_impl_instance.on_exit(); \ + vbridge_impl_instance.on_exit(); \ dpi_finish(); \ } catch (std::runtime_error & e) { \ terminated = true; \ + ProgramOutputStoreFile.close(); \ svSetScope( \ svGetScopeFromName("TOP.TestBench.dpiError")); \ dpi_error(fmt::format("runtime_error occurs: {}", e.what()).c_str()); \ diff --git a/ipemu/csrc/simple_sim.h b/ipemu/csrc/simple_sim.h index 175350bae..0ec0826f4 100644 --- a/ipemu/csrc/simple_sim.h +++ b/ipemu/csrc/simple_sim.h @@ -1,6 +1,7 @@ #pragma once #include +#include #include @@ -8,6 +9,12 @@ #include "spdlog_ext.h" #include "uartlite.h" +// File that contains program stdout/stderr from MMIO +// Initialize in vbridge_impl.cc, closed in dpi.cc +// +// Require C++ 17 here to have inline keyword feature, so that the compiler can collapse all symbol into one declaration and bypass mold error. +inline std::ofstream ProgramOutputStoreFile; + class simple_sim : public simif_t { private: char *mem; @@ -56,8 +63,8 @@ class simple_sim : public simif_t { if (uart_addr <= addr && addr < uart_addr + sizeof(uartlite_regs)) { bool res = uart.do_write(addr - uart_addr, len, bytes); while (uart.exist_tx()) { - std::cerr << uart.getc(); - std::cerr.flush(); + ProgramOutputStoreFile << uart.getc(); + ProgramOutputStoreFile.flush(); } return res; } diff --git a/ipemu/csrc/vbridge_impl.cc b/ipemu/csrc/vbridge_impl.cc index f7062a125..72f5c37df 100644 --- a/ipemu/csrc/vbridge_impl.cc +++ b/ipemu/csrc/vbridge_impl.cc @@ -218,6 +218,8 @@ static VBridgeImpl vbridgeImplFromArgs() { args::Flag no_console_logging(parser, "no_console_logging", "Disable console logging utilities.", { "no-console-logging" }); args::ValueFlag log_path(parser, "log path", "Path to store logging file", {"log-path"}); + args::ValueFlag program_output_path(parser, "program output path", "Path to store stdout/stderr produce by program", {"program-output-path"}); + args::ValueFlag vlen(parser, "vlen", "match from RTL config, tobe removed", {"vlen"}, args::Options::Required); args::ValueFlag dlen(parser, "dlen", "match from RTL config, tobe removed", {"dlen"}, args::Options::Required); args::ValueFlag tl_bank_number(parser, "tl_bank_number", "match from RTL config, tobe removed", {"tl_bank_number"}, args::Options::Required); @@ -247,6 +249,8 @@ static VBridgeImpl vbridgeImplFromArgs() { Log = JsonLogger(no_logging.Get(), no_file_logging.Get(), no_console_logging.Get(), log_path.Get()); + ProgramOutputStoreFile.open(program_output_path.Get()); + Config cosim_config { .bin_path = bin_path.Get(), .wave_path = wave_path.Get(), @@ -362,7 +366,7 @@ void VBridgeImpl::getCoverage() { return ctx->coveragep()->write(); } std::optional VBridgeImpl::spike_step() { auto state = proc.get_state(); - state->mcycle->write((int64_t) get_t() + spike_cycles); + state->mcycle->write((int64_t) get_t() / 10 + spike_cycles); auto fetch = proc.get_mmu()->load_insn(state->pc); auto event = create_spike_event(fetch); diff --git a/ipemu/src/TestBench.scala b/ipemu/src/TestBench.scala index 1570aadd1..07649af22 100644 --- a/ipemu/src/TestBench.scala +++ b/ipemu/src/TestBench.scala @@ -6,7 +6,6 @@ package org.chipsalliance.t1.ipemu import chisel3._ import chisel3.experimental.SerializableModuleGenerator import chisel3.probe._ -import chisel3.util.experimental.BoringUtils.bore import org.chipsalliance.t1.ipemu.dpi._ import org.chipsalliance.t1.rtl.{T1, T1Parameter} @@ -34,33 +33,39 @@ class TestBench(generator: SerializableModuleGenerator[T1, T1Parameter]) extends val dut: T1 = withClockAndReset(clock, reset)(Module(generator.module())) dut.storeBufferClear := true.B - val lsuProbe = probe.read(dut.lsuProbe).suggestName("lsuProbe") - val laneProbes = dut.laneProbes.zipWithIndex.map{case (p, idx) => val wire = Wire(p.cloneType).suggestName(s"lane${idx}Probe") wire := probe.read(p) - wire } - val laneVrfProbes = dut.laneVrfProbes.zipWithIndex.map{case (p, idx) => + val lsuProbe = probe.read(dut.lsuProbe).suggestName("lsuProbe") + + val laneVrfProbes = dut.laneVrfProbes.zipWithIndex.map{ case (p, idx) => val wire = Wire(p.cloneType).suggestName(s"lane${idx}VrfProbe") wire := probe.read(p) wire } - val t1Probe = probe.read(dut.t1Probe).suggestName("instructionCountProbe") - - // Monitor - withClockAndReset(clock, reset)(Module(new Module { - // h/t: GrandCentral - override def desiredName: String = "XiZhiMen" - val lsuProbeMonitor = bore(lsuProbe) - dontTouch(lsuProbeMonitor) - val laneProbesMonitor = laneProbes.map(bore(_)) - laneProbesMonitor.foreach(dontTouch(_)) - val laneVrfProbesMonitor = laneVrfProbes.map(bore(_)) - laneVrfProbesMonitor.foreach(dontTouch(_)) - })) + val t1Probe = probe.read(dut.t1Probe) + + withClockAndReset(clock, reset) { + // count cycle for peek tl + val cycleCounter = RegInit(0.U(64.W)) + cycleCounter := cycleCounter + 1.U + + // memory write + lsuProbe.slots.zipWithIndex.foreach { case (mshr, i) => when(mshr.writeValid)(printf(cf"""{"event":"vrfWriteFromLsu","parameter":{"idx":$i,"vd":${mshr.dataVd},"offset":${mshr.dataOffset},"mask":${mshr.dataMask},"data":${mshr.dataData},"instruction":${mshr.dataInstruction},"lane":${mshr.targetLane},"cycle": ${cycleCounter}}}\n""")) } + // vrf write + laneVrfProbes.zipWithIndex.foreach { case (lane, i) => when(lane.valid)(printf(cf"""{"event":"vrfWriteFromLane","parameter":{"idx":$i,"vd":${lane.requestVd},"offset":${lane.requestOffset},"mask":${lane.requestMask},"data":${lane.requestData},"instruction":${lane.requestInstruction},"cycle": ${cycleCounter}}}\n""")) } + // issue + when(dut.request.fire)(printf(cf"""{"event":"issue","parameter":{"idx":${t1Probe.instructionCounter},"cycle": ${cycleCounter}}}\n""")) + // inst + when(dut.response.valid)(printf(cf"""{"event":"inst","parameter":{"data":${dut.response.bits.data},"vxsat":${dut.response.bits.vxsat},"rd_valid":${dut.response.bits.rd.valid},"rd":${dut.response.bits.rd.bits},"mem":${dut.response.bits.mem},"cycle": ${cycleCounter}}}\n""")) + // peekTL + dut.memoryPorts.zipWithIndex.foreach { case (bundle, i) => when(bundle.a.valid)(printf(cf"""{"event":"peekTL","parameter":{"idx":$i,"opcode":${bundle.a.bits.opcode},"param":${bundle.a.bits.param},"size":${bundle.a.bits.size},"source":${bundle.a.bits.source},"address":${bundle.a.bits.address},"mask":${bundle.a.bits.mask},"data":${bundle.a.bits.data},"corrupt":${bundle.a.bits.corrupt},"dready":${bundle.d.ready},"cycle": ${cycleCounter}}}\n""")) } + // lsu enq + when(lsuProbe.reqEnq.orR)(printf(cf"""{"event":"lsuEnq","parameter":{"enq":${lsuProbe.reqEnq},"cycle": ${cycleCounter}}}\n""")) + } // Monitors // TODO: These monitors should be purged out after offline difftest is landed diff --git a/nix/overlay.nix b/nix/overlay.nix index 5db1f1a3a..17dac14b3 100644 --- a/nix/overlay.nix +++ b/nix/overlay.nix @@ -23,7 +23,9 @@ in fetchMillDeps = final.callPackage ./pkgs/mill-builder.nix { }; circt-full = final.callPackage ./pkgs/circt-full.nix { }; rvv-codegen = final.callPackage ./pkgs/rvv-codegen.nix { }; - add-determinism = final.callPackage ./pkgs/add-determinism { }; # faster strip-undetereminism + add-determinism = final.callPackage ./pkgs/add-determinism { }; # faster strip-undetereminism + # difftest simulator + t1-simulator = final.callPackage ../difftest/t1-simulator { }; mill = let jre = final.jdk21; in (prev.mill.override { inherit jre; }).overrideAttrs (_: { diff --git a/nix/t1/default.nix b/nix/t1/default.nix index cefaf79ed..dfeea63ac 100644 --- a/nix/t1/default.nix +++ b/nix/t1/default.nix @@ -51,7 +51,7 @@ lib.makeScope newScope elaborateConfigJson = configPath; elaborateConfig = builtins.fromJSON (lib.readFile configPath); - cases = innerSelf.callPackage ../../tests { }; + cases = innerSelf.callPackage ../../tests { ip-emu = ip.emu; }; # for the convenience to use x86 cases on non-x86 machines, avoiding the extra build time cases-x86 = diff --git a/script/default.nix b/script/default.nix index 389d968f4..b83ee6eb1 100644 --- a/script/default.nix +++ b/script/default.nix @@ -31,7 +31,7 @@ let millDepsHash = "sha256-J8bBgM/F+8x8EQ1DR6Va/ZY2hnsjkkzk4a+ctDMKK3k="; }; - passthru.dev = self.overrideAttrs (old: { + passthru.withLsp = self.overrideAttrs (old: { nativeBuildInputs = old.nativeBuildInputs ++ [ metals # Metals require java to work correctly diff --git a/script/src/Main.scala b/script/src/Main.scala index 234f553b4..8cf2b5894 100644 --- a/script/src/Main.scala +++ b/script/src/Main.scala @@ -10,17 +10,21 @@ object Logger { val level = sys.env.getOrElse("LOG_LEVEL", "INFO") match case "TRACE" | "trace" => 0 case "ERROR" | "error" => 1 - case "WARN" | "warn" => 2 - case _ => 3 + case "INFO" | "info" => 2 + case _ => 4 + + def info(message: String) = + if level <= 2 then println(s"${BOLD}${GREEN}[INFO]${RESET} ${message}") - def info(message: String) = println( - s"${BOLD}${GREEN}[INFO]${RESET} ${message}" - ) def trace(message: String) = if level <= 0 then println(s"${BOLD}${GREEN}[TRACE]${RESET} ${message}") - def error(message: String) = println( - s"${BOLD}${RED}[ERROR]${RESET} ${message}" - ) + + def error(message: String) = + if level <= 2 then println(s"${BOLD}${RED}[ERROR]${RESET} ${message}") + + def fatal(message: String) = + println(s"${BOLD}${RED}[FATAL]${RESET} ${message}") + sys.exit(1) } object Main: @@ -84,10 +88,8 @@ object Main: def resolveElaborateConfig( configName: String ): os.Path = - if os.exists(os.Path(configName, os.pwd)) then - os.Path(configName) - else - os.pwd / "configgen" / "generated" / s"$configName.json" + if os.exists(os.Path(configName, os.pwd)) then os.Path(configName) + else os.pwd / "configgen" / "generated" / s"$configName.json" end resolveElaborateConfig def prepareOutputDir( @@ -98,7 +100,10 @@ object Main: caseName: String ): os.Path = val pathTail = - if os.exists(os.Path(caseName, os.pwd)) || os.exists(os.Path(config, os.pwd)) then + if os.exists(os.Path(caseName, os.pwd)) || os.exists( + os.Path(config, os.pwd) + ) + then // It is hard to canoncalize user specify path, so here we use date time instead val now = java.time.LocalDateTime .now() @@ -159,9 +164,11 @@ object Main: doc = "prevent emulator produce log (both console and file)" ) noLog: Flag = Flag(false), @arg( - name = "no-file-logging", - doc = "prevent emulator print log to file" - ) noFileLog: Flag = Flag(true), + name = "with-file-logging", + doc = """enable file logging, default is false. + |WARN: the emulator will write all the information in each cycle, which will produce a huge file log, use with care. + |""".stripMargin + ) withFileLog: Flag = Flag(false), @arg( name = "no-console-logging", short = 'q', @@ -175,6 +182,14 @@ object Main: name = "emulator-log-file-path", doc = "Set the logging output path" ) emulatorLogFilePath: Option[os.Path] = None, + @arg( + name = "event-log-path", + doc = "Set the event log path" + ) eventLogFilePath: Option[os.Path] = None, + @arg( + name = "program-output-path", + doc = "Path to store the ELF stdout/stderr" + ) programOutputFilePath: Option[os.Path] = None, @arg( name = "out-dir", doc = "path to save wave file and perf result file" @@ -223,6 +238,13 @@ object Main: val emulatorLogPath = if emulatorLogFilePath.isDefined then emulatorLogFilePath.get else outputPath / "emulator.log" + val eventLogPath = + if eventLogFilePath.isDefined then eventLogFilePath.get + else outputPath / "rtl-event.log" + val programOutputPath = + if programOutputFilePath.isDefined then programOutputFilePath.get + else outputPath / "mmio-store.txt" + if os.exists(programOutputPath) then os.remove(programOutputPath) def dumpCycleAsFloat() = val ratio = dumpCycle.toFloat @@ -231,8 +253,7 @@ object Main: s"Can't use $dumpCycle as ratio, use 0 as waveform dump start point" ) 0 - else if ratio == 0.0 then - 0 + else if ratio == 0.0 then 0 else val cycleRecordFilePath = os.pwd / ".github" / "cases" / config / "default.json" @@ -254,8 +275,7 @@ object Main: scala.math.floor(cycle * 10 * ratio).toInt val dumpStartPoint: Int = - try - dumpCycle.toInt + try dumpCycle.toInt catch case _ => try dumpCycleAsFloat() @@ -296,9 +316,11 @@ object Main: .arr(0) .obj("beatbyte") .toString(), - s"--log-path=${emulatorLogPath}" + s"--log-path=${emulatorLogPath}", + "--program-output-path", + programOutputPath.toString ) ++ optionals(noLog.value, Seq("--no-logging")) - ++ optionals(noFileLog.value, Seq("--no-file-logging")) + ++ optionals((!withFileLog.value), Seq("--no-file-logging")) ++ optionals(noConsoleLog.value, Seq("--no-console-logging")) ++ optionals( dramsim3Config.isDefined, @@ -317,15 +339,18 @@ object Main: Logger.info(s"Starting IP emulator: `${processArgs.mkString(" ")}`") if dryRun.value then return + if os.exists(eventLogPath) then os.remove(eventLogPath) os.proc(processArgs) - .call(env = - Map( + .call( + env = Map( "EMULATOR_FILE_LOG_LEVEL" -> emulatorLogLevel, "EMULATOR_CONSOLE_LOG_LEVEL" -> emulatorLogLevel - ) + ), + stderr = eventLogPath ) + Logger.info(s"RTL event log saved to ${eventLogPath}") - if (!noFileLog.value) then + if (!withFileLog.value) then Logger.info(s"Emulator log save to ${emulatorLogPath}") if (trace.value) then @@ -353,18 +378,21 @@ object Main: @arg( name = "out-link", short = 'o', - doc = "Path to be a symlink to the RTL build output, default using $config_subsystem_rtl" - ) outLink: Option[String] = None, + doc = + "Path to be a symlink to the RTL build output, default using $config_subsystem_rtl" + ) outLink: Option[String] = None ): Unit = val finalOutLink = outLink.getOrElse(s"${config}_subsystem_rtl") - os.proc(Seq( - "nix", - "build", - "--print-build-logs", - s".#t1.${config}.subsystem.rtl", - "--out-link", - finalOutLink - )).call(stdout = os.Inherit, stderr = os.Inherit, stdin = os.Inherit) + os.proc( + Seq( + "nix", + "build", + "--print-build-logs", + s".#t1.${config}.subsystem.rtl", + "--out-link", + finalOutLink + ) + ).call(stdout = os.Inherit, stderr = os.Inherit, stdin = os.Inherit) Logger.info(s"RTLs store in $finalOutLink") // @@ -418,7 +446,8 @@ object Main: case (_, cycle) => cycle <= 0 // Initialize a list of buckets - val cargoInit = (0 until math.min(bucketSize, allCycleData.length)).map(_ => Bucket()) + val cargoInit = + (0 until math.min(bucketSize, allCycleData.length)).map(_ => Bucket()) // Group tests that have cycle data into subset by their cycle size val cargoStaged = normalData .sortBy(_._2)(Ordering[Int].reverse) @@ -439,8 +468,7 @@ object Main: cargo.updated(idx, newBucket) cargoFinal.map(_.buffer.mkString(";")).toSeq - else - cargoStaged.map(_.buffer.mkString(";")).toSeq + else cargoStaged.map(_.buffer.mkString(";")).toSeq end scheduleTasks // Turn Seq( "A;B", "C;D" ) to GitHub Action matrix style json: { "include": [ { "jobs": "A;B", id: 1 }, { "jobs": "C;D", id: 2 } ] } @@ -466,56 +494,6 @@ object Main: println(toMatrixJson(scheduleTasks(testPlans, runnersAmount))) } - def writeCycleUpdates( - testName: String, - testRunDir: os.Path, - resultDir: os.Path - ): Unit = - val isEmulatorTask = raw"([^,]+),([^,]+)".r - testName match - case isEmulatorTask(e, t) => - val passedFile = os.pwd / os.RelPath(s".github/cases/$e/default.json") - val original = ujson.read(os.read(passedFile)) - - val perfCycleRegex = raw"total_cycles:\s(\d+)".r - val newCycleCount = os.read - .lines(testRunDir / os.RelPath(s"$e/$t/perf.txt")) - .apply(0) match - case perfCycleRegex(cycle) => cycle.toInt - case _ => - throw new Exception("perf.txt file is not format as expected") - - val oldCycleCount = original.obj.get(t).map(_.num.toInt).getOrElse(-1) - val cycleUpdateFile = resultDir / "cycle-updates.md" - Logger.info(f"job '$testName' cycle $oldCycleCount -> $newCycleCount") - oldCycleCount match - case -1 => - os.write.append( - cycleUpdateFile, - s"* 🆕 $testName: NaN -> $newCycleCount\n" - ) - case _ => - if oldCycleCount > newCycleCount then - os.write.append( - cycleUpdateFile, - s"* 🚀 $testName: $oldCycleCount -> $newCycleCount\n" - ) - else if oldCycleCount < newCycleCount then - os.write.append( - cycleUpdateFile, - s"* 🐢 $testName: $oldCycleCount -> $newCycleCount\n" - ) - - val newCycleFile = resultDir / s"${e}_cycle.json" - val newCycleRecord = - if os.exists(newCycleFile) then ujson.read(os.read(newCycleFile)) - else ujson.Obj() - - newCycleRecord(t) = newCycleCount - os.write.over(newCycleFile, ujson.write(newCycleRecord, indent = 2)) - case _ => throw new Exception(f"unknown job format '$testName'") - end writeCycleUpdates - // Run jobs and give a brief result report // - Log of tailed tests will be tailed and copied into $resultDir/failed-logs/$testName.log // - List of failed tests will be written into $resultDir/failed-tests.md @@ -528,106 +506,166 @@ object Main: @main def runTests( jobs: String, - resultDir: Option[os.Path], dontBail: Flag = Flag(false) ): Unit = if jobs == "" then Logger.info("No test found, exiting") return - var actualResultDir = resultDir.getOrElse(os.pwd / "test-results") - val testRunDir = os.pwd / "testrun" - os.makeDir.all(actualResultDir / "failed-logs") - val allJobs = jobs.split(";") - val failed = allJobs.zipWithIndex.foldLeft(Seq[String]()): + def findFailedTests() = allJobs.zipWithIndex.foldLeft(Seq[String]()): (allFailedTest, currentTest) => val (testName, index) = currentTest val Array(config, caseName) = testName.split(",") - println() + println("\n") Logger.info( s"${BOLD}[${index + 1}/${allJobs.length}]${RESET} Running test case $caseName with config $config" ) - try - ipemu( - testCase = caseName, - config = config, - noLog = Flag(false), - noConsoleLog = Flag(true), - noFileLog = Flag(false), - emulatorLogLevel = "FATAL", - emulatorLogFilePath = Some( - actualResultDir / "failed-logs" / s"${testName.replaceAll(",", "-")}.txt" - ), - baseOutDir = Some(testRunDir.toString()) + + val testResultPath = + os.Path(nixResolvePath(s".#t1.$config.cases.$caseName.emu-result")) + val testSuccess = + os.read(testResultPath / "emu-success").trim().toInt == 1 + + if !testSuccess then + Logger.error(s"Test case $testName failed") + val err = os.read(testResultPath / "emu.log") + Logger.error(s"Detail error: $err") + + Logger.info("Running difftest") + val diffTestSuccess = + try + difftest( + config = config, + caseAttr = caseName, + logLevel = "ERROR" + ) + true + catch + err => + Logger.error(s"difftest run failed: $err") + false + + if diffTestSuccess != testSuccess then + Logger.fatal( + "Got different online and offline difftest result, please check this test manually. CI aborted." ) - writeCycleUpdates(testName, testRunDir, actualResultDir) - allFailedTest - catch - err => - val outDir = testRunDir / config / caseName - Logger.error(s"Test case $testName failed") - allFailedTest :+ testName - - os.write.over( - actualResultDir / "failed-tests.md", - "" - ) // touch file, to avoid upload-artifacts warning - - if failed.length > 0 then + + if !testSuccess then allFailedTest :+ s"t1.$config.cases.$caseName" + else allFailedTest + end findFailedTests + + val failedTests = findFailedTests() + if failedTests.isEmpty then Logger.info(s"All tests passed") + else val listOfFailJobs = - failed.map(job => s"* $job").appended("").mkString("\n") - os.write.over(actualResultDir / "failed-tests.md", listOfFailJobs) - val failedJobsWithError = failed + failedTests.map(job => s"* $job").appended("").mkString("\n") + val failedJobsWithError = failedTests .map(testName => - s"* $testName\n >>> ERROR SUMMARY <<<\n${os - .read(actualResultDir / "failed-logs" / s"${testName.replaceAll(",", "-")}.txt")}" + val testResult = os.Path(nixResolvePath(s".#$testName.emu-result")) + val emuLog = os.read(testResult / "emu.log") + if emuLog.nonEmpty then + s"* $testName\n >>> ERROR SUMMARY <<<\n${emuLog}" + else + s"* $testName\n >>> OTHER ERROR <<<\n${os.read(testResult / "emu.journal")}" ) .appended("") .mkString("\n") - Logger.error( - s"\n\n${BOLD}${failed.length} tests failed${RESET}:\n${failedJobsWithError}" - ) - - if !dontBail.value then - Logger.error("Tests failed") - System.exit(1) - else Logger.info(s"All tests passed") + if dontBail.value then + Logger.error( + s"${BOLD}${failedTests.length} tests failed${RESET}:\n${failedJobsWithError}" + ) + else + Logger.fatal( + s"${BOLD}${failedTests.length} tests failed${RESET}:\n${failedJobsWithError}" + ) end runTests + // PostCI do the below four things: + // * read default.json at .github/cases/$config/default.json + // * generate case information for each entry in default.json (cycle, run success) + // * collect and report failed tests + // * collect and report cycle update @main - def mergeCycleData(filePat: String = "default.json") = - Logger.info("Updating cycle data") - val original = os - .walk(os.pwd / ".github" / "cases") - .filter(_.last == filePat) - .map: path => - val config = path.segments.toSeq.reverse(1) - (config, ujson.read(os.read(path))) - .toMap - os.walk(os.pwd) - .filter(_.last.endsWith("_cycle.json")) - .map: path => - val config = path.last.split("_")(0) - Logger.trace(s"Reading new cycle data from $path") - (config, ujson.read(os.read(path))) - .foreach: - case (name, latest) => - val old = original.apply(name) - latest.obj.foreach: - case (k, v) => old.update(k, v) - - original.foreach: - case (name, data) => - val config = name.split(",")(0) - os.write.over( - os.pwd / ".github" / "cases" / config / filePat, - ujson.write(data, indent = 2) - ) + def postCI( + @arg( + name = "failed-test-file-path", + doc = "specify the failed test markdown file output path" + ) failedTestsFilePath: String, + @arg( + name = "cycle-update-file-path", + doc = "specify the cycle update markdown file output path" + ) cycleUpdateFilePath: String + ) = + case class CaseStatus( + caseName: String, + isFailed: Boolean, + oldCycle: Int, + newCycle: Int + ) + + def collectCaseStatus( + config: String, + caseName: String, + cycle: Int + ): CaseStatus = + val emuResultPath = os.Path(nixResolvePath(s".#t1.$config.cases.$caseName.emu-result")) + val testFail = os.read(emuResultPath / "emu-success") == "0" + + val perfCycleRegex = raw"total_cycles:\s(\d+)".r + val newCycle = os.read + .lines(emuResultPath / "perf.txt") + .apply(0) match + case perfCycleRegex(cycle) => cycle.toInt + case _ => + throw new Exception("perf.txt file is not format as expected") + CaseStatus( + caseName = caseName, + isFailed = testFail, + oldCycle = cycle, + newCycle = newCycle + ) + end collectCaseStatus + + val allCycleRecords = + os.walk(os.pwd / ".github" / "cases").filter(_.last == "default.json") + allCycleRecords.foreach: file => + val config = file.segments.toSeq.reverse.apply(1) + var cycleRecord = ujson.read(os.read(file)) - Logger.info("Cycle data updated") - end mergeCycleData + nixResolvePath(s".#t1.$config.cases._allEmuResult") + + val allCaseStatus = cycleRecord.obj.map(rec => + rec match { + case (caseName, cycle) => + collectCaseStatus(config, caseName, cycle.num.toInt) + } + ) + + val failedCases = allCaseStatus + .filter(c => c.isFailed) + .map(c => s"* `.#t1.${config}.cases.${c.caseName}`") + val failedTestsRecordFile = os.Path(failedTestsFilePath, os.pwd) + os.write.over(failedTestsRecordFile, "## Failed tests\n") + os.write.append(failedTestsRecordFile, failedCases) + + val cycleUpdateRecordFile = os.Path(cycleUpdateFilePath, os.pwd) + os.write.over(cycleUpdateRecordFile, "## Cycle Update\n") + val allCycleUpdates = allCaseStatus + .filter(c => c.oldCycle != c.newCycle) + .map: caseStatus => + caseStatus match + case CaseStatus(caseName, _, oldCycle, newCycle) => + cycleRecord(caseName) = newCycle + if oldCycle == -1 then s"* 🆕 ${caseName}: NaN -> ${newCycle}" + else if oldCycle > newCycle then + s"* 🚀 $caseName: $oldCycle -> $newCycle" + else s"* 🐢 $caseName: $oldCycle -> $newCycle" + os.write.append(cycleUpdateRecordFile, allCycleUpdates.mkString("\n")) + + os.write.over(file, ujson.write(cycleRecord, indent = 2)) + end postCI @main def generateTestPlan() = @@ -639,6 +677,18 @@ object Main: println(ujson.write(Map("config" -> testPlans))) end generateTestPlan + def nixResolvePath(attr: String): String = + os.proc( + "nix", + "build", + "--no-link", + "--no-warn-dirty", + "--print-out-paths", + attr + ).call() + .out + .trim() + @main def generateRegressionTestPlan(runnersAmount: Int): Unit = // Find emulator configs @@ -651,21 +701,9 @@ object Main: // but all we need is the name. path.segments.toSeq.reverse.drop(1).head - def nixBuild(attr: String): String = - os.proc( - "nix", - "build", - "--no-link", - "--no-warn-dirty", - "--print-out-paths", - attr - ).call() - .out - .trim() - import scala.util.chaining._ val testPlans: Seq[String] = emulatorConfigs.flatMap: configName => - val allCasesPath = nixBuild(s".#t1.$configName.cases.all") + val allCasesPath = nixResolvePath(s".#t1.$configName.cases.all") os.walk(os.Path(allCasesPath) / "configs") .filter: path => path.ext == "json" @@ -698,11 +736,73 @@ object Main: .toSeq .map(_.mkString(";")) - val finalTestPlan = (testPlans.toSet -- currentTestPlan.toSet -- perfCases.toSet).toSeq + val finalTestPlan = + (testPlans.toSet -- currentTestPlan.toSet -- perfCases.toSet).toSeq buckets(finalTestPlan, runnersAmount) .pipe(toMatrixJson) .pipe(println) end generateRegressionTestPlan + @main + def difftest( + @arg( + name = "config", + short = 'c', + doc = "specify the elaborate config for running test case" + ) config: String, + @arg( + name = "case-attr", + short = 'C', + doc = "Specify test case attribute to run diff test" + ) caseAttr: String, + @arg( + name = "log-level", + short = 'L', + doc = "Specify log level to run diff test" + ) logLevel: String = "ERROR" + ): Unit = + val difftest = nixResolvePath(".#t1-simulator") + + val fullCaseAttr = s".#t1.${config}.cases.${caseAttr}" + val caseElf = nixResolvePath(fullCaseAttr) + + import scala.util.chaining._ + val configJson = nixResolvePath(s".#t1.${config}.elaborateConfigJson") + .pipe(p => os.Path(p)) + .pipe(p => os.read(p)) + .pipe(text => ujson.read(text)) + val dLen = configJson.obj("parameter").obj("dLen").num.toInt + val vLen = configJson.obj("parameter").obj("vLen").num.toInt + + Logger.trace(s"Running emulator to get event log") + val eventLog = nixResolvePath(s"${fullCaseAttr}.emu-result") + + Logger.trace("Running zstd to get event log") + os.proc(Seq( + "zstd", + "--decompress", + "-f", + s"${eventLog}/rtl-event.log.zstd", + "-o", + s"${config}-${caseAttr}.event.log" + )).call(stdout = os.Inherit, stderr = os.Inherit) + Logger.info(s"Starting t1-simulator with DLEN ${dLen}, VLEN ${vLen} for ${fullCaseAttr}") + os.proc( + Seq( + s"${difftest}/bin/t1-simulator", + "--vlen", + vLen.toString(), + "--dlen", + dLen.toString(), + "--elf-file", + s"${caseElf}/bin/${caseAttr}.elf", + "--log-file", + s"${config}-${caseAttr}.event.log", + "--log-level", + s"${logLevel}" + ) + ).call(stdout = os.Inherit, stderr = os.Inherit) + end difftest + def main(args: Array[String]): Unit = ParserForMethods(this).runOrExit(args) end Main diff --git a/tests/builder.nix b/tests/builder.nix index 69832ac21..7016042aa 100644 --- a/tests/builder.nix +++ b/tests/builder.nix @@ -5,6 +5,8 @@ , elaborateConfig , isFp , vLen + +, makeEmuResult }: # args from makeBuilder @@ -18,47 +20,51 @@ let # avoid adding jq to buildInputs, since it will make overriding buildInputs more error prone jqBin = "${jq}/bin/jq"; -in -stdenv.mkDerivation (self: rec { - # don't set name directory, since it will be suffixed with target triple - pname = "${casePrefix}.${caseName}"; - name = pname; + caseDrv = stdenv.mkDerivation (self: rec { + # don't set name directory, since it will be suffixed with target triple + pname = "${casePrefix}.${caseName}"; + name = pname; - CC = "${stdenv.targetPlatform.config}-cc"; + CC = "${stdenv.targetPlatform.config}-cc"; - NIX_CFLAGS_COMPILE = - let - march = (if isFp then "rv32gc_zve32f" else "rv32gc_zve32x") + NIX_CFLAGS_COMPILE = + let + march = (if isFp then "rv32gc_zve32f" else "rv32gc_zve32x") + "_zvl${toString (lib.min 1024 vLen)}b"; - in - [ - "-mabi=ilp32f" - "-march=${march}" - "-mno-relax" - "-static" - "-mcmodel=medany" - "-fvisibility=hidden" - "-fno-PIC" - "-g" - "-O3" - ]; + in + [ + "-mabi=ilp32f" + "-march=${march}" + "-mno-relax" + "-static" + "-mcmodel=medany" + "-fvisibility=hidden" + "-fno-PIC" + "-g" + "-O3" + ]; + + installPhase = '' + runHook preInstall - installPhase = '' - runHook preInstall + mkdir -p $out/bin + cp ${pname}.elf $out/bin - mkdir -p $out/bin - cp ${pname}.elf $out/bin + ${jqBin} --null-input \ + --arg name ${pname} \ + --arg type ${casePrefix} \ + --arg elfPath "$out/bin/${pname}.elf" \ + '{ "name": $name, "elf": { "path": $elfPath } }' \ + > $out/${pname}.json - ${jqBin} --null-input \ - --arg name ${pname} \ - --arg type ${casePrefix} \ - --arg elfPath "$out/bin/${pname}.elf" \ - '{ "name": $name, "elf": { "path": $elfPath } }' \ - > $out/${pname}.json + runHook postInstall + ''; - runHook postInstall - ''; + dontFixup = true; - dontFixup = true; -} // overrides) + passthru.emu-result = makeEmuResult caseDrv; + + } // overrides); +in +caseDrv diff --git a/tests/default.nix b/tests/default.nix index 9ab1cef9a..19154e5b6 100644 --- a/tests/default.nix +++ b/tests/default.nix @@ -1,8 +1,10 @@ { lib +, configName , elaborateConfig , newScope , rv32-stdenv , runCommand +, ip-emu }: let @@ -17,6 +19,10 @@ let scope = lib.recurseIntoAttrs (lib.makeScope newScope (casesSelf: { recurseForDerivations = true; + inherit ip-emu; + + makeEmuResult = casesSelf.callPackage ./make-emu-result.nix { }; + makeBuilder = casesSelf.callPackage ./builder.nix { }; findAndBuild = dir: build: @@ -56,6 +62,27 @@ let inherit (scope) mlir intrinsic asm perf codegen rvv_bench; }; + # This derivation is for internal use only. + # We have a large test suite used in CI, but resolving each test individually is too slow for production. + # This "fake" derivation serves as a workaround, making all tests dependencies of this single derivation. + # This allows Nix to resolve the path only once, while still pulling all tests into the local Nix store. + _allEmuResult = + let + testPlan = builtins.fromJSON (lib.readFile ../.github/cases/${configName}/default.json); + # flattern the attr set to a list of test case derivations + # AttrSet (AttrSet Derivation) -> List Derivation + allCases = lib.filter (val: lib.isDerivation val && lib.hasAttr val.pname testPlan) + (lib.concatLists (map lib.attrValues (lib.attrValues scopeStripped))); + script = '' + echo "fake-derivation" > $out + '' + (lib.concatMapStringsSep "\n" + (caseDrv: '' + echo ${caseDrv.emu-result} > /dev/null + '') + allCases); + in + runCommand "catch-all-emu-result" { } script; + all = let allCases = lib.filter lib.isDerivation @@ -72,4 +99,4 @@ let in runCommand "build-all-testcases" { } script; in -lib.recurseIntoAttrs (scopeStripped // { inherit all; }) +lib.recurseIntoAttrs (scopeStripped // { inherit all _allEmuResult; }) diff --git a/tests/make-emu-result.nix b/tests/make-emu-result.nix new file mode 100644 index 000000000..5aa2070a3 --- /dev/null +++ b/tests/make-emu-result.nix @@ -0,0 +1,37 @@ +# CallPackage args +{ runCommand +, zstd +, t1-script +, ip-emu +, elaborateConfigJson +}: + +# makeEmuResult arg +testCase: + +runCommand "get-emu-result" { nativeBuildInputs = [ zstd ]; } '' + echo "[NIX] Running test case ${testCase.pname}" + + mkdir -p "$out" + + set +e + ${t1-script}/bin/t1-helper \ + "ipemu" \ + --emulator-path ${ip-emu}/bin/emulator \ + --config ${elaborateConfigJson} \ + --case ${testCase}/bin/${testCase.pname}.elf \ + --no-console-logging \ + --with-file-logging \ + --emulator-log-level "FATAL" \ + --emulator-log-file-path "$out/emu.log" \ + --out-dir $out &> $out/emu-wrapper.journal + + if (( $? )); then + printf "0" > $out/emu-success + else + printf "1" > $out/emu-success + fi + + zstd $out/rtl-event.log -o $out/rtl-event.log.zstd + rm $out/rtl-event.log +'' diff --git a/tests/rvv_bench/default.nix b/tests/rvv_bench/default.nix index e5376b838..3d4b86775 100644 --- a/tests/rvv_bench/default.nix +++ b/tests/rvv_bench/default.nix @@ -3,30 +3,37 @@ , makeBuilder , findAndBuild , t1main +, makeEmuResult }: let include = ./_include; builder = makeBuilder { casePrefix = "rvv_bench"; }; build = { caseName, sourcePath }: - builder { - inherit caseName; + let + drv = builder + { + inherit caseName; - src = sourcePath; + src = sourcePath; - isFp = lib.pathExists (lib.path.append sourcePath "isFp"); + isFp = lib.pathExists (lib.path.append sourcePath "isFp"); - buildPhase = '' - runHook preBuild + buildPhase = '' + runHook preBuild - $CC -E -DINC=$PWD/${caseName}.S -E ${include}/template.S -o functions.S - $CC -I${include} ${caseName}.c -T${linkerScript} ${t1main} functions.S -o $pname.elf + $CC -E -DINC=$PWD/${caseName}.S -E ${include}/template.S -o functions.S + $CC -I${include} ${caseName}.c -T${linkerScript} ${t1main} functions.S -o $pname.elf - runHook postBuild - ''; + runHook postBuild + ''; - meta.description = "test case '${caseName}', written in C intrinsic"; - }; + meta.description = "test case '${caseName}', written in C intrinsic"; + + passthru.emu-result = makeEmuResult drv; + }; + in + drv; in - findAndBuild ./. build +findAndBuild ./. build diff --git a/tests/t1_main.S b/tests/t1_main.S index 70f676b45..85ed6ac32 100644 --- a/tests/t1_main.S +++ b/tests/t1_main.S @@ -10,9 +10,6 @@ _start: call test // exit - li a0, 0x10000000 - li a1, -1 - sw a1, 4(a0) csrwi 0x7cc, 0 .p2align 2