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.
This commit is contained in:
Catherine 2023-10-25 10:51:39 +00:00
parent a94fafa8fe
commit 3e358d9bfa
2 changed files with 65 additions and 17 deletions

View File

@ -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<class ObserverT>\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<class ObserverT>\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<class ObserverT>\n");
log(" bool commit(ObserverT &observer);\n");
log(" bool commit() override;\n");
log("\n");
log(" static std::unique_ptr<bb_p_debug>\n");

View File

@ -841,6 +841,29 @@ std::ostream &operator<<(std::ostream &os, const value_formatted<Bits> &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<size_t Bits>
struct wire {
static constexpr size_t bits = Bits;
@ -875,8 +898,14 @@ struct wire {
next.template set<IntegerT>(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<class ObserverT>
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<class ObserverT>
bool commit(ObserverT &observer) {
bool changed = false;
for (const write &entry : write_queue) {
value<Width> 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<Width>::chunks, data[0].data, elem.data, entry.index);
changed |= true;
}
data[entry.index] = elem;
}
write_queue.clear();