From 3e358d9bfa9289fefbc68e83ab40c80f4b123641 Mon Sep 17 00:00:00 2001 From: Catherine Date: Wed, 25 Oct 2023 10:51:39 +0000 Subject: [PATCH] cxxrtl: add a way to observe state changes during the commit step. The commit observer is a structure containing a callback that is invoked whenever the `commit()` method changes a wire or a memory. This allows code external to the compiled netlist to react to changes in the design state in a very efficient way. One example of how this feature can be used is an efficient implementation of record/replay. Note that the VCD writer does not benefit from this feature because it must be able to react to changes in any debug items and not just those that contain design state. --- backends/cxxrtl/cxxrtl_backend.cc | 42 ++++++++++++++++--------- backends/cxxrtl/runtime/cxxrtl/cxxrtl.h | 40 +++++++++++++++++++++-- 2 files changed, 65 insertions(+), 17 deletions(-) diff --git a/backends/cxxrtl/cxxrtl_backend.cc b/backends/cxxrtl/cxxrtl_backend.cc index 1ab865a27..2c35a1943 100644 --- a/backends/cxxrtl/cxxrtl_backend.cc +++ b/backends/cxxrtl/cxxrtl_backend.cc @@ -2115,19 +2115,19 @@ struct CxxrtlWorker { if (wire_type.type == WireType::MEMBER && edge_wires[wire]) f << indent << "prev_" << mangle(wire) << " = " << mangle(wire) << ";\n"; if (wire_type.is_buffered()) - f << indent << "if (" << mangle(wire) << ".commit()) changed = true;\n"; + f << indent << "if (" << mangle(wire) << ".commit(observer)) changed = true;\n"; } if (!module->get_bool_attribute(ID(cxxrtl_blackbox))) { for (auto &mem : mod_memories[module]) { if (!writable_memories.count({module, mem.memid})) continue; - f << indent << "if (" << mangle(&mem) << ".commit()) changed = true;\n"; + f << indent << "if (" << mangle(&mem) << ".commit(observer)) changed = true;\n"; } for (auto cell : module->cells()) { if (is_internal_cell(cell->type)) continue; const char *access = is_cxxrtl_blackbox_cell(cell) ? "->" : "."; - f << indent << "if (" << mangle(cell) << access << "commit()) changed = true;\n"; + f << indent << "if (" << mangle(cell) << access << "commit(observer)) changed = true;\n"; } } f << indent << "return changed;\n"; @@ -2146,7 +2146,7 @@ struct CxxrtlWorker { if (!metadata_item.first.isPublic()) continue; if (metadata_item.second.size() > 64 && (metadata_item.second.flags & RTLIL::CONST_FLAG_STRING) == 0) { - f << indent << "/* attribute " << metadata_item.first.str().substr(1) << " is over 64 bits wide */"; + f << indent << "/* attribute " << metadata_item.first.str().substr(1) << " is over 64 bits wide */\n"; continue; } f << indent << "{ " << escape_cxx_string(metadata_item.first.str().substr(1)) << ", "; @@ -2371,16 +2371,22 @@ struct CxxrtlWorker { dump_eval_method(module); f << indent << "}\n"; f << "\n"; - f << indent << "bool commit() override {\n"; + f << indent << "template\n"; + f << indent << "bool commit(ObserverT &observer) {\n"; dump_commit_method(module); f << indent << "}\n"; f << "\n"; + f << indent << "bool commit() override {\n"; + f << indent << indent << "null_observer observer;\n"; + f << indent << indent << "return commit<>(observer);\n"; + f << indent << "}\n"; if (debug_info) { + f << "\n"; f << indent << "void debug_info(debug_items &items, std::string path = \"\") override {\n"; dump_debug_info_method(module); f << indent << "}\n"; - f << "\n"; } + f << "\n"; f << indent << "static std::unique_ptr<" << mangle(module); f << template_params(module, /*is_decl=*/false) << "> "; f << "create(std::string name, metadata_map parameters, metadata_map attributes);\n"; @@ -2457,8 +2463,18 @@ struct CxxrtlWorker { f << indent << "};\n"; f << "\n"; f << indent << "void reset() override;\n"; + f << "\n"; f << indent << "bool eval() override;\n"; - f << indent << "bool commit() override;\n"; + f << "\n"; + f << indent << "template\n"; + f << indent << "bool commit(ObserverT &observer) {\n"; + dump_commit_method(module); + f << indent << "}\n"; + f << "\n"; + f << indent << "bool commit() override {\n"; + f << indent << indent << "null_observer observer;\n"; + f << indent << indent << "return commit<>(observer);\n"; + f << indent << "}\n"; if (debug_info) { if (debug_eval) { f << "\n"; @@ -2490,24 +2506,20 @@ struct CxxrtlWorker { f << indent << "bool " << mangle(module) << "::eval() {\n"; dump_eval_method(module); f << indent << "}\n"; - f << "\n"; - f << indent << "bool " << mangle(module) << "::commit() {\n"; - dump_commit_method(module); - f << indent << "}\n"; - f << "\n"; if (debug_info) { if (debug_eval) { + f << "\n"; f << indent << "void " << mangle(module) << "::debug_eval() {\n"; dump_debug_eval_method(module); f << indent << "}\n"; - f << "\n"; } + f << "\n"; f << indent << "CXXRTL_EXTREMELY_COLD\n"; f << indent << "void " << mangle(module) << "::debug_info(debug_items &items, std::string path) {\n"; dump_debug_info_method(module); f << indent << "}\n"; - f << "\n"; } + f << "\n"; } void dump_design(RTLIL::Design *design) @@ -3267,6 +3279,8 @@ struct CxxrtlBackend : public Backend { log(" wire<8> p_o_data;\n"); log("\n"); log(" bool eval() override;\n"); + log(" template\n"); + log(" bool commit(ObserverT &observer);\n"); log(" bool commit() override;\n"); log("\n"); log(" static std::unique_ptr\n"); diff --git a/backends/cxxrtl/runtime/cxxrtl/cxxrtl.h b/backends/cxxrtl/runtime/cxxrtl/cxxrtl.h index 78dbf3707..53fb3dbc0 100644 --- a/backends/cxxrtl/runtime/cxxrtl/cxxrtl.h +++ b/backends/cxxrtl/runtime/cxxrtl/cxxrtl.h @@ -841,6 +841,29 @@ std::ostream &operator<<(std::ostream &os, const value_formatted &vf) return os; } +// An object that can be passed to a `commit()` method in order to produce a replay log of every +// state change in the simulation. +struct observer { + // Called when a `commit()` method for a wire is about to update the `chunks` chunks at `base` + // with `chunks` chunks at `value` that have a different bit pattern. It is guaranteed that + // `chunks` is equal to the wire chunk count and `base` points to the first chunk. + virtual void on_commit(size_t chunks, const chunk_t *base, const chunk_t *value) = 0; + + // Called when a `commit()` method for a memory is about to update the `chunks` chunks at + // `&base[chunks * index]` with `chunks` chunks at `value` that have a different bit pattern. + // It is guaranteed that `chunks` covers is equal to the memory element chunk count and `base` + // points to the first chunk of the first element of the memory. + virtual void on_commit(size_t chunks, const chunk_t *base, const chunk_t *value, size_t index) = 0; +}; + +// The `null_observer` class has the same interface as `observer`, but has no invocation overhead, +// since its methods are final and have no implementation. This allows the observer feature to be +// zero-cost when not in use. +struct null_observer final: observer { + void on_commit(size_t chunks, const chunk_t *base, const chunk_t *value) override {} + void on_commit(size_t chunks, const chunk_t *base, const chunk_t *value, size_t index) override {} +}; + template struct wire { static constexpr size_t bits = Bits; @@ -875,8 +898,14 @@ struct wire { next.template set(other); } - bool commit() { + // This method intentionally takes a mandatory argument (to make it more difficult to misuse in + // black box implementations, leading to missed observer events). It is generic over its argument + // to make sure the `on_commit` call is devirtualized. This is somewhat awkward but lets us keep + // a single implementation for both this method and the one in `memory`. + template + bool commit(ObserverT &observer) { if (curr != next) { + observer.on_commit(curr.chunks, curr.data, next.data); curr = next; return true; } @@ -950,12 +979,17 @@ struct memory { write { index, val, mask, priority }); } - bool commit() { + // See the note for `wire::commit()`. + template + bool commit(ObserverT &observer) { bool changed = false; for (const write &entry : write_queue) { value elem = data[entry.index]; elem = elem.update(entry.val, entry.mask); - changed |= (data[entry.index] != elem); + if (data[entry.index] != elem) { + observer.on_commit(value::chunks, data[0].data, elem.data, entry.index); + changed |= true; + } data[entry.index] = elem; } write_queue.clear();