Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 41 additions & 14 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -305,9 +305,8 @@ several if the net is reached more than once) -- *not* every reader on the net,
unlike `equipotential_response`. Nets are returned breadth-first from the
requested one and de-duplicated, capped at `kMaxTraceNets` (`MAX_TRACE_NETS` in
`protocol.py`, 500) with `truncated: true` when hit. `AppLogic.cpp` adds
each to `GUIData` in order; the incremental layout in `EquipotentialView.cpp`
relies on that ordering (each net shares an already-placed instance with an
earlier one) to chain the cone right-to-left. Reachable from the tree
each to `GUIData` in order; the layered placement breaks ordering ties by
first appearance, so the cone keeps a stable shape as it grows. Reachable from the tree
(right-click a term/bus-bit row -> "Trace to Driver", bus row -> "Trace Bus
to Driver"; both clear the view first, like "Show Equipotential") and from the
schematic (right-click a pin -> "Trace to Driver"; this one *adds* to the view
Expand All @@ -321,8 +320,8 @@ the view yet are "open" (`Port::open`, drawn with a hollow-circle stub), and a
while it loads; `SchematicInteraction::PendingRequests` stops repeat sends).
A single click on a merged bus pin shows its bits instead. Hovering a pin
highlights it and shows a tooltip naming its click action. When the requested
net arrives the view pans so the clicked box stays put on screen, even if the
hierarchy frames re-lay everything out. Pins are hit-tested nearest-first by
net arrives the view pans so the clicked box stays put on screen, even though
the whole schematic is laid out again. Pins are hit-tested nearest-first by
`SchematicInteraction::pickPin`. `GUIData::addEquipotential()` rejects a net
whose endpoints are all already shown (`equipotentialCovers()`), so neither a
re-click nor a trace re-listing a shown net draws its wires twice.
Expand All @@ -335,8 +334,9 @@ shows (**View > Show Hierarchy**, on by default, also in the canvas context
menu): `layoutHierarchyGroups()` in `EquipotentialView.cpp` draws every
module enclosing a displayed leaf as a nested translucent frame labelled
`instance (Model)`, re-laying the leaves out inside it — each leaf keeps the
logic column the incremental layout gave it, and inside a frame its leaves
and sub-frames are bucketed into columns by that column. Frames are
logic level the layered placement gave it (inside a frame its leaves and
sub-frames are bucketed into columns by level) and, where the column allows,
the height that aligned its pins. Frames are
`InstanceShape`s with `isHierGroup` set, inserted at the front of
`SchematicView::instances` and drawn before the nets
(`SchematicView::render()`); the leaves stay top-level shapes, so wiring and
Expand All @@ -345,6 +345,17 @@ hit-testing are unchanged. Right-clicking a frame offers Show Properties and
last trace (not just the last net) with a "Hierarchy" column giving each
occurrence's enclosing modules.

Instance entries (`instances_response`/`primitives_response` children,
equipotential occurrences, `instance_resolved`'s `instance`) carry a
`primitive_type` (`"and"`, `"nand"`, `"or"`, `"nor"`, `"xor"`, `"xnor"`,
`"inv"`, `"buf"`, `"dff"`, `"assign"`, `"unknown"`) that picks the gate
symbol `SchematicView::drawInstance()` draws, and term entries carry
`"clock": true` on a sequential cell's clock pin (the DFF clock notch).
Both come from naja's modeling of the cell -- `SNLDesignModeling`'s
truth-table checks (`isAnd`, ...), `isSequential` and `isClock` -- and
**never from a cell or pin name**: a cell naja has no model for (e.g.
gate-level Verilog without Liberty) is `"unknown"` and draws as a box.

`diagnosis_response` is different: it's a **server push**, not a reply to a
request (a diagnosis run finishes on its own schedule), and it *annotates*
the already-loaded netlist rather than loading anything:
Expand Down Expand Up @@ -388,7 +399,7 @@ the same instance-name path as diagnosis items:
{ "response": "instance_resolved", "path": ["u1","u2"], "found": true,
"instance": { // absent for the top design ([])
"path": [["u1", 3, "Mod"], ["u2", 7, "AND2"]], // [name, child_id, model] per level
"design_ref": {...}, "has_instances": false, "source_loc": null,
"design_ref": {...}, "primitive_type": "and", "has_instances": false, "source_loc": null,
"terms": [ {"name": "A", "child_id": 0, "direction": 0}, ... ] } } // expanded_instance_terms shape
```
The viewer then:
Expand Down Expand Up @@ -481,12 +492,28 @@ appear as boxes/pins there).
views. Its listener (installed in `AppLogic.cpp`) reports each change to
the host. `NetlistTree` reveals selections made outside it by comparing
`revision()`. Cleared on every fresh `root_response`/`root_loaded`.
- **`SchematicLayout`** — the schematic's pure placement geometry, split out
of `EquipotentialView` so it's unit-testable without ImGui frames or a
provider (`tests/SchematicLayoutTest.cpp`): `IncrementalLayout` (per-net
instance placement and column de-overlap) and `layoutHierarchyGroups()`
(module frames). Changes to how the schematic is laid out belong here,
with a test; `EquipotentialView` only turns the result into drawn shapes.
- **`SchematicLayout`** — the schematic's pure geometry, split out of
`EquipotentialView` so it's unit-testable without ImGui frames or a
provider (`tests/SchematicLayoutTest.cpp`). It follows the classic
schematic-generation pipeline and is recomputed from scratch every frame (deterministic, so
nothing moves unless the inputs change):
- `symbolGeometry()`: symbol sizes, and pins on a `kPinPitch` grid (gate
inputs centered on the output, box pins down from the top).
- `layeredPlacement()`: Sugiyama-style placement. It breaks cycles, assigns
longest-path levels, orders each column by barycenter, then places heights
by isotonic regression so connected pins line up (straight wires).
- `stackPorts()`: top-level ports on the sheet's left/right edges.
- `routeNets()`: one orthogonal tree per driving pin. Trunks go in the
box-free channels between columns, on distinct tracks that respect
channel routing's vertical constraints, plus a spine that detours around
boxes, labels and other spines. Junction dots mark real branch points.
- `layoutHierarchyGroups()`: module frames.

Changes to how the schematic is laid out belong here, with a test;
`EquipotentialView` only turns the result into drawn shapes, and
`SchematicView` draws them. A gate symbol's body fills its box exactly, so
pin ticks always touch it. Pin names go inside boxes, none on gates, and
instance names above.
- **`SchematicInteraction`** — the pure side of the schematic's pin
interactions: `pickPin()` (nearest-pin hit test, radius capped in world
units so a pin doesn't swallow box-body clicks at low zoom) and
Expand Down
1 change: 1 addition & 0 deletions REUSE.toml
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ path = [
"python/tests/test_transports.py",
"python/tests/test_widget.py",
"scripts/fakeram45_64x32.lib",
"scripts/gate_shapes_example.v",
"scripts/najaeda_server.py",
"scripts/test_server.py",
"scripts/tinyrocket.v",
Expand Down
48 changes: 42 additions & 6 deletions python/naja_schematic/protocol.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,39 @@ def has_visible_primitive_instances(design):
return any(not instance.getModel().isAssign()
for instance in design.getPrimitiveInstances())

# Gate/cell function classification, from naja's own modeling of the cell
# (the truth table a Liberty `function` or an NLDB0 gate defines, and its
# sequential model) -- never from its name. A cell naja has no model for is
# "unknown" and draws as the generic box. LocalSNLProvider.cpp's
# getPrimitiveType() mirrors this, same as the rest of the wire protocol.
_PRIMITIVE_TYPE_CHECKS = (
("isSequential", "dff"),
("isInv", "inv"),
("isBuf", "buf"),
("isAnd", "and"),
("isNand", "nand"),
("isOr", "or"),
("isNor", "nor"),
("isXor", "xor"),
("isXnor", "xnor"),
)

def get_primitive_type(model):
if model is None:
return "unknown"
if model.isAssign():
return "assign"
for check, ptype in _PRIMITIVE_TYPE_CHECKS:
if getattr(model, check)():
return ptype
return "unknown"

def with_clock(entry, term):
# Only sent when set: marks a flip-flop's clock pin (see drawDffInstance).
if term.is_clock():
entry["clock"] = True
return entry

def is_anonymous_constant_net(net):
# Anonymous scalar constant nets (1'b0/1'b1 tie-offs, e.g. an unconnected
# input najaeda ties off implicitly) are structural noise, not
Expand Down Expand Up @@ -92,6 +125,7 @@ def serialize_model(model, child_id, name, source_loc=None):
"name": name,
"child_id": child_id,
"model_name": model.getName(),
"primitive_type": get_primitive_type(model),
"design_ref": {
"db_id": model.getDB().getID(),
"library_id": model.getLibrary().getID(),
Expand Down Expand Up @@ -223,7 +257,7 @@ def equipotential_to_json(equipotential, sinks=None):
inst_model = instTerm.getInstance().getModel()
has_instances = (inst_model.hasNonPrimitiveInstances() or
has_visible_primitive_instances(inst_model))
occurrences.append({
occurrences.append(with_clock({
"path": path,
"term_id": term.getID(),
"name": term.getName(),
Expand All @@ -234,6 +268,7 @@ def equipotential_to_json(equipotential, sinks=None):
"library_id": inst_model.getLibrary().getID(),
"design_id": inst_model.getID(),
},
"primitive_type": get_primitive_type(inst_model),
"has_instances": has_instances,
# Lets the view tell whether every pin of this instance is already
# on screen (solid box) or only a subset (dashed, expandable).
Expand All @@ -242,7 +277,7 @@ def equipotential_to_json(equipotential, sinks=None):
# Net on this inst term inside the instance's parent design --
# shown in the schematic's pin hover tooltip.
"net": net_display_name(instTerm.getNet())
})
}, term))
for term in equipotential.getTerms():
# A top-level output is a receiver of the net; an input/inout drives it.
if (sinks is not None
Expand Down Expand Up @@ -464,18 +499,18 @@ def bit_terms_json(design):
for b in range(lo, hi + 1):
bit_term = term.getBusTermBit(b)
if bit_term:
terms.append({
terms.append(with_clock({
"name": f"{term.getName()}[{b}]",
"child_id": bit_term.getID(),
"direction": direction_to_int(bit_term.getDirection()),
"bit": b,
})
}, bit_term))
else:
terms.append({
terms.append(with_clock({
"name": term.getName(),
"child_id": term.getID(),
"direction": direction_to_int(term.getDirection()),
})
}, term))
return terms


Expand Down Expand Up @@ -516,6 +551,7 @@ def _handle_resolve_instance(u, request):
current = inst.getModel()
reply["instance"] = {
"path": levels,
"primitive_type": get_primitive_type(design),
"design_ref": {
"db_id": design.getDB().getID(),
"library_id": design.getLibrary().getID(),
Expand Down
17 changes: 17 additions & 0 deletions python/tests/test_protocol.py
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,7 @@ def test_resolve_instance(top):
assert [level[0] for level in inst["path"]] == ["u_sub", "u_and"]
assert [level[2] for level in inst["path"]] == ["sub", "LUT2"]
assert not inst["has_instances"]
assert inst["primitive_type"] == "unknown" # LUT2 isn't a named gate
assert {t["name"] for t in inst["terms"]} == {"I0", "I1", "O"}

(reply,) = handle_request({"request": "resolve_instance", "path": ["u_sub"]})
Expand All @@ -150,3 +151,19 @@ def test_instance_selected_is_a_notification(top):
def test_focus_instance_message():
assert protocol.focus_instance(["u_sub", "u_and"]) == {
"response": "focus_instance", "path": ["u_sub", "u_and"]}


def test_get_primitive_type_uses_naja_modeling_not_names():
checks = ("isAssign", "isSequential", "isInv", "isBuf", "isAnd", "isNand",
"isOr", "isNor", "isXor", "isXnor")

def model(name, *true_checks):
return SimpleNamespace(getName=lambda: name,
**{c: (lambda c=c: c in true_checks) for c in checks})
assert protocol.get_primitive_type(None) == "unknown"
assert protocol.get_primitive_type(model("x", "isAssign")) == "assign"
assert protocol.get_primitive_type(model("x", "isSequential")) == "dff"
assert protocol.get_primitive_type(model("ND2", "isNand")) == "nand"
assert protocol.get_primitive_type(model("OAI", "isXnor")) == "xnor"
# The name never matters: an unmodelled "AND2" is just a box.
assert protocol.get_primitive_type(model("AND2_X1")) == "unknown"
38 changes: 38 additions & 0 deletions scripts/gate_shapes_example.v
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
// Small gate-level example exercising one instance of every PrimitiveType
// the standard shapes library (SchematicView::drawInstance()) recognizes --
// AND/NAND/OR/NOR/XOR/XNOR at a few input arities, INV/BUF, and a DFF --
// using cell names from the Nangate library (NangateOpenCellLibrary_typical.lib)
// in this directory. Load with:
// naja-schematic-standalone scripts/gate_shapes_example.v \
// --liberty scripts/NangateOpenCellLibrary_typical.lib

module top(
input a, b, c, d,
input clk, rst_n,
output y_and2, y_and3, y_and4,
output y_nand2, y_nand3,
output y_or2, y_nor2,
output y_xor2, y_xnor2,
output y_inv, y_buf,
output y_q, y_qn
);

AND2_X1 u_and2 (.A1(a), .A2(b), .ZN(y_and2));
AND3_X1 u_and3 (.A1(a), .A2(b), .A3(c), .ZN(y_and3));
AND4_X1 u_and4 (.A1(a), .A2(b), .A3(c), .A4(d), .ZN(y_and4));

NAND2_X1 u_nand2 (.A1(a), .A2(b), .ZN(y_nand2));
NAND3_X1 u_nand3 (.A1(a), .A2(b), .A3(c), .ZN(y_nand3));

OR2_X1 u_or2 (.A1(a), .A2(b), .ZN(y_or2));
NOR2_X1 u_nor2 (.A1(a), .A2(b), .ZN(y_nor2));

XOR2_X1 u_xor2 (.A(a), .B(b), .Z(y_xor2));
XNOR2_X1 u_xnor2 (.A(a), .B(b), .ZN(y_xnor2));

INV_X1 u_inv (.A(a), .ZN(y_inv));
BUF_X1 u_buf (.A(a), .Z(y_buf));

DFFR_X1 u_dff (.D(a), .CK(clk), .RN(rst_n), .Q(y_q), .QN(y_qn));

endmodule
2 changes: 2 additions & 0 deletions src/AppLogic.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,7 @@ void setupProvider(AppState& state) {
ep.childId = t.value("child_id", 0u);
if (t.contains("bit") && !t["bit"].is_null())
ep.bit = t["bit"].get<int>();
ep.clock = t.value("clock", false);
int dirInt = t.value("direction", 0);
ep.direction = dirInt == 1 ? Direction::Output
: dirInt == 2 ? Direction::Inout
Expand All @@ -256,6 +257,7 @@ void setupProvider(AppState& state) {
ic.childId = c.value("child_id", 0u);
if (c.contains("design_ref"))
ic.designRef = c["design_ref"].get<DesignRef>();
ic.primitiveType = primitiveTypeFromString(c.value("primitive_type", std::string("unknown")));
ic.hasInstances = c.value("has_instances", false) || c.value("has_primitives", false);
data.children.push_back(std::move(ic));
}
Expand Down
Loading
Loading