Skip to content

[multicast] Bitmap-based replication and bit-slice assignment for softnpu/a4x2 - #240

Merged
zeeshanlakhani merged 7 commits into
mainfrom
zl/multicast
Oct 19, 2026
Merged

zeeshanlakhani merged 7 commits into
mainfrom
zl/multicast

Conversation

@zeeshanlakhani

@zeeshanlakhani zeeshanlakhani commented Mar 17, 2026 •

Copy link
Copy Markdown
Contributor

This work includes and/or incorporates:

  • Bitmap-based packet replication for softnpu: a P4 program can make use of a port_bitmap field in its egress metadata and set the field during ingress (via table action, metadata assignment, etc.):

    struct egress_metadata_t {
        ...
        bit<128> port_bitmap;
    }
    
    action set_port_bitmap(bit<128> bitmap) {
        egress.port_bitmap = bitmap;
    }
    • This bitmap is read after ingress, ahead of broadcast/unicast (drop > non-zero bitmap > broadcast > unicast), and it'll show up in dtrace metadata dumps for debugging, just like the other fields.
    • Checks have been added as well, including a check_port_bitmap requiring a bit<N>.
    • The p4rs::bitmap_to_port_list() function skips the ingress port and bits at or above the given radix. If a non-zero bitmap filters down to no ports, the packet gets dropped. A zeroed bitmap falls back to broadcast/unicast handling. Bit N (value 2^N) is port N, which is the same encoding 128w1 << port produces via shl_le.
    • Each selected port runs egress with its own copy of headers and metadata.
    • drop is now checked before any port selection takes place, enforcing that the egress pipeline never runs on a dropped packet.
  • The pipeline codegen no longer hardcodes the ingress_metadata_t/egress_metadata_t types, which are now derived from the control parameters. The generated locals use fixed names (i.e., __ingress_metadata, __egress_metadata) in order to prevent P4 parameter names from shadowing pipeline locals.

  • Fixtures have been updated, including the in-repo sidecar-lite example, which now makes use of a mcast_replication_v6 table whose set_port_bitmap action drives the port_bitmap field.

    • The associated mcast test fixture demonstrates dendrite-style MAC splitting, i.e. unicast in the ingress pipeline and replicas in the egress pipeline.
  • The addition of shift operators (<<, >>). Binary expressions follow operator precedence, associating operators at equal precedence from left to right.

  • The addition of bit-slice assignment (lval[hi:lo] = expr, P4-16 8.6), and slice reads are fixed now too.

    • Note: only bit<W> slices are lowered, whereas varbit slicing results in a language error (8.10), and int<W>/compile-time int slicing are reported as SoftNPU/x4c restrictions.
  • Fixes and tightening around unsigned comparisons and diagnostic carets, which are now pulled from the token spans themselves, so that operators and full dotted names get marked properly.

  • bit<W> -> bit<V> widening keeps the existing value (for sub-byte widths too).

    • Implicit bit widening is an x4c extension. P4-16 8.11 requires explicit casts between different bit widths, but x4c does not parse explicit casts as of yet.
  • New HLIR checks on initializers, assignments, and slice bounds: runtime (unsized) int is rejected, and x4c/SoftNPU limitations get bubbled up instead of looking just like type errors.

    • The use_p4! macro now parses every file before AST checking, returning semantic errors as a syn::Error.
  • Unaligned bit<N> action parameters have been fixed. For example, VLAN 300 returned as 44 (and 256 as 0!).

  • New check: a table's default_action must appear in its actions list (P4-16 14.2.1.3).

  • Bug fix: <= never lexed (s/">="/"<="/ for LessThanEquals).

@zeeshanlakhani
zeeshanlakhani marked this pull request as ready for review March 18, 2026 05:32
zeeshanlakhani added a commit to oxidecomputer/softnpu that referenced this pull request Mar 18, 2026
…protocol

This extends the management message protocol with operations for multicast
group lifecycle: create, destroy, add port, remove port, and list. These dispatch
to the corresponding [p4rs](https://github.com/oxidecomputer/p4) Pipeline trait
methods (add_mcast_group, remove_mcast_group, add_mcast_port, etc.) that were added
to support bifurcated multicast replication in the p4rs codegen.

Mutating operations (create, destroy, add, remove) are fire-and-forget, matching the
existing TableAdd/TableRemove pattern. `MulticastGroupList` is the only round-trip
operation, returning a sorted group-to-ports mapping for use by dendrite's
`AsicMulticastOps` queries.

The `MulticastPortAdd` struct carries rid and level1_excl_id for API
parity with dendrite's AsicMulticastOps trait, though softnpu does not
use them. Tofino's PRE handles per-replica identification and
exclusion via these fields. softnpu handles the equivalent via
`McastReplicationTag` in the codegenning. 

This branch depends on the related oxidecomputer/p4#240.
zeeshanlakhani added a commit to oxidecomputer/softnpu that referenced this pull request Mar 19, 2026
…protocol

This extends the management message protocol with operations for multicast
group lifecycle: create, destroy, add port, remove port, and list. These dispatch
to the corresponding [p4rs](https://github.com/oxidecomputer/p4) Pipeline trait
methods (add_mcast_group, remove_mcast_group, add_mcast_port, etc.) that were added
to support bifurcated multicast replication in the p4rs codegen.

Mutating operations (create, destroy, add, remove) are fire-and-forget, matching the
existing TableAdd/TableRemove pattern. `MulticastGroupList` is the only round-trip
operation, returning a sorted group-to-ports mapping for use by dendrite's
`AsicMulticastOps` queries.

The `MulticastPortAdd` struct carries rid and level1_excl_id for API
parity with dendrite's AsicMulticastOps trait, though softnpu does not
use them. Tofino's PRE handles per-replica identification and
exclusion via these fields. softnpu handles the equivalent via
`McastReplicationTag` in the codegenning. 

This branch depends on the related oxidecomputer/p4#240.
@zeeshanlakhani
zeeshanlakhani force-pushed the zl/multicast branch 2 times, most recently from a0ee3d2 to 0e20707 Compare March 20, 2026 16:55
@zeeshanlakhani zeeshanlakhani changed the title [multicast] Add multicast replication support for (upcoming) softnpu/a4x2 [multicast] Add multicast replication and bit-slice assignment for softnpu/a4x2 Mar 20, 2026
zeeshanlakhani added a commit to oxidecomputer/softnpu that referenced this pull request Mar 20, 2026
…protocol

This extends the management message protocol with operations for multicast
group lifecycle: create, destroy, add port, remove port, and list. These dispatch
to the corresponding [p4rs](https://github.com/oxidecomputer/p4) Pipeline trait
methods (add_mcast_group, remove_mcast_group, add_mcast_port, etc.) that were added
to support bifurcated multicast replication in the p4rs codegen.

Mutating operations (create, destroy, add, remove) are fire-and-forget, matching the
existing TableAdd/TableRemove pattern. `MulticastGroupList` is the only round-trip
operation, returning a sorted group-to-ports mapping for use by dendrite's
`AsicMulticastOps` queries.

The `MulticastPortAdd` struct carries rid and level1_excl_id for API
parity with dendrite's AsicMulticastOps trait, though softnpu does not
use them. Tofino's PRE handles per-replica identification and
exclusion via these fields. softnpu handles the equivalent via
`McastReplicationTag` in the codegenning. 

This branch depends on the related oxidecomputer/p4#240.
@zeeshanlakhani
zeeshanlakhani force-pushed the zl/multicast branch 2 times, most recently from c6c1858 to 3df53f6 Compare March 25, 2026 03:38
@zeeshanlakhani zeeshanlakhani changed the title [multicast] Add multicast replication and bit-slice assignment for softnpu/a4x2 [multicast] Bitmap-based replication and bit-slice assignment for softnpu/a4x2 Mar 25, 2026
zeeshanlakhani added a commit to oxidecomputer/softnpu that referenced this pull request Mar 25, 2026
…protocol

This extends the management message protocol with operations for multicast
group lifecycle: create, destroy, add port, remove port, and list. These dispatch
to the corresponding [p4rs](https://github.com/oxidecomputer/p4) Pipeline trait
methods (add_mcast_group, remove_mcast_group, add_mcast_port, etc.) that were added
to support bifurcated multicast replication in the p4rs codegen.

Mutating operations (create, destroy, add, remove) are fire-and-forget, matching the
existing TableAdd/TableRemove pattern. `MulticastGroupList` is the only round-trip
operation, returning a sorted group-to-ports mapping for use by dendrite's
`AsicMulticastOps` queries.

The `MulticastPortAdd` struct carries rid and level1_excl_id for API
parity with dendrite's AsicMulticastOps trait, though softnpu does not
use them. Tofino's PRE handles per-replica identification and
exclusion via these fields. softnpu handles the equivalent via
`McastReplicationTag` in the codegenning. 

This branch depends on the related oxidecomputer/p4#240.
@zeeshanlakhani
zeeshanlakhani force-pushed the zl/multicast branch 2 times, most recently from 3df53f6 to 3e15edd Compare March 25, 2026 15:06
zeeshanlakhani added a commit to oxidecomputer/sidecar-lite that referenced this pull request Mar 25, 2026
This adds multicast replication to sidecar-lite, emulating DPD's
sidecar.p4

We manage 2 bitmaps per multicast destination (external, underlay),
in which replication is driven via the `Replicate` extern (from p4rs #240).
The ingress control block suppresses (or doesn't) these bitmaps based
on the geneve multicast option tag, and then merges both into a single
replication bitmap. This tag is read-only, matching DPD's sidecar.p4.
OPTE (would) stamp it before the packet reaches the switch.

Egress decap is gated on the reserved underlay subnet (ff04::/64, per
RFC 7346 admin-local scope + Omicron) and tag=2 (Both), matching DPD's
egress mcast_tag_check. Ports in the decap table get geneve stripped,
others keep encapsulation for OPTE to handle on the receiving sled.

Multicast dst MAC derivation (RFC 1112 for IPv4, RFC 2464 for IPv6)
and per-port source MAC rewrite run on every replicated copy. Egress
validates the outer IP multicast range before any mcast processing.

### References

This work depends on the P4(rs) work in
[#240](oxidecomputer/p4#240).
…tnpu/a4x2

## Replication

This introduces bitmap-based packet replication for softnpu. Replication is
opt-in via a Replicate extern that P4 programs declare and call explicitly:

```p4
extern Replicate {
    void replicate(in bit<128> bitmap);
}

Replicate() rep;
rep.replicate(egress.bitmap_a | egress.bitmap_b);
```

The codegen scans the AST for the extern call, extracts the bitmap expression,
and generates the replication loop at the pipeline level between ingress and
egress. The call is elided from generated code after compile-time validation
of the argument.

`p4rs::replicate()` collects set bits from the bitmap expression,
filtering out the ingress port to prevent self-replication. It interprets
bitmaps in little-endian integer order: bit N (value 2^N) corresponds to port N.
This matches the encoding produced by P4 shifting (`128w1 << port`) via `shl_le`,
so replication bitmaps and shift-based bitmap checks use the same convention.

The pipeline codegen no longer hardcodes metadata struct names
(`ingress_metadata_t`, `egress_metadata_t`), deriving variable names and
types from P4 control parameter declarations.

Generated P4 structs now initialize `bit<N>` fields to properly-sized
zero bitvecs (`bitvec![u8, Msb0; 0; N]`) instead of empty `BitVec`
(length 0). This fixes bitmap comparisons and arithmetic on
uninitialized metadata fields.

## Shift operators

Adds left shift ("<<") and right shift (">>") support across the full
compiler pipeline: lexer, parser, AST, HLIR, type checker, and codegen.
The lexer previously tokenized "<<" but was not wired through the parser
or AST (yet).

## Bit-slice assignment

Adds `Statement::SliceAssignment` for P4-16 spec 8.6 `lval[hi:lo] = expr`
syntax, including parser updates, HLIR bounds validation, type checking
(RHS width must equal hi - lo + 1), and codegen with byte-reversal-aware bitvec
range mapping. Non-contiguous slices after byte reversal fall back to
arithmetic extraction.

Slice reads assigned to local variables (e.g., `bit<16> lo = x[15:0]`)
produce owned BitVec values via `.to_bitvec()`.

## Slice codegen handling

- Fixes a latent issue where the slice-to-bitvec mapping ignored header byte
  reversal, producing incorrect ranges for sub-byte slices on multi-byte fields
  (e.g., field[31:28] on bit<32>). Fixes Varbit/Int slice reads, which were
  rejected due to swapped destructure naming.
- Fixes single-bit slices (x[n:n]) rejected in the read context.

## Bitwise operators

Clones operands for BitOr, BitAnd, Xor, and Mask in the Expression
codegen. The previous generated code moved out of mutable references for
BitVec operands, which does not implement Copy.

## Tests

- Bitmap replication (port selection, self-replication filtering, empty
  bitmap, broadcast precedence), emulating multicast concepts.
- Slice assignment with RFC 1112 MAC derivation and same-field aliasing.
- Shift operators and width resizing.
- Sub-byte slice reads verify byte-reversal correctness.
zeeshanlakhani added a commit to oxidecomputer/dendrite that referenced this pull request Mar 30, 2026
This wires up the softnpu ASIC backend to support multicast end-to-end by
translating DPD's sidecar.p4 table operations into sidecar-lite.p4's
simplified P4 pipeline.

## AsicMulticastOps

We replace the stubbed AsicMulticastOps implementation (which returned
"OperationUnsupported" for group creation and port addition with in-memory group
tracking via McGroupData, following the tofino_stub pattern.

Group membership is used by the table translation layer to build port
bitmaps for sidecar-lite's Replicate extern. Ports >= 128 are
rejected at add time to match sidecar-lite's 128-bit bitmap width.

## Table translation (asic/src/softnpu/table.rs)

We map sidecar.p4 table names to sidecar-lite equivalents and translate
action parameters where the designs differ for emulation.

All multicast action arms are gated with #[cfg(feature = "multicast")].

## References

- [softnpu #183](oxidecomputer/softnpu#183)
- [propolis #1093](oxidecomputer/propolis#1093)
- [p4rs #240](oxidecomputer/p4#240)
- [sidecar-lite #152](oxidecomputer/sidecar-lite#152)
- tokio: 1.50 (due to softnpu)
- oxide-tokio-rt: 0.1.3 (following-up from tokio's move to 1.50)
zeeshanlakhani added a commit to oxidecomputer/dendrite that referenced this pull request Apr 1, 2026
This wires up the softnpu ASIC backend to support multicast end-to-end by
translating DPD's sidecar.p4 table operations into sidecar-lite.p4's
simplified P4 pipeline.

## AsicMulticastOps

We replace the stubbed AsicMulticastOps implementation (which returned
"OperationUnsupported" for group creation and port addition with in-memory group
tracking via McGroupData, following the tofino_stub pattern.

Group membership is used by the table translation layer to build port
bitmaps for sidecar-lite's Replicate extern. Ports >= 128 are
rejected at add time to match sidecar-lite's 128-bit bitmap width.

## Table translation (asic/src/softnpu/table.rs)

We map sidecar.p4 table names to sidecar-lite equivalents and translate
action parameters where the designs differ for emulation.

All multicast action arms are gated with #[cfg(feature = "multicast")].

## References

- [softnpu #183](oxidecomputer/softnpu#183)
- [propolis #1093](oxidecomputer/propolis#1093)
- [p4rs #240](oxidecomputer/p4#240)
- [sidecar-lite #152](oxidecomputer/sidecar-lite#152)
- tokio: 1.50 (due to softnpu)
- oxide-tokio-rt: 0.1.3 (following-up from tokio's move to 1.50)
zeeshanlakhani added a commit to oxidecomputer/dendrite that referenced this pull request Apr 28, 2026
This wires up the softnpu ASIC backend to support multicast end-to-end by translating DPD's sidecar.p4 table operations into sidecar-lite.p4's simplified P4 pipeline.

## AsicMulticastOps

We replace the stubbed AsicMulticastOps implementation (which returned "OperationUnsupported" for group creation and port addition with in-memory group tracking via McGroupData, following the tofino_stub pattern.

Group membership is used by the table translation layer to build port bitmaps for sidecar-lite's Replicate extern. Ports >= 128 are rejected at add time to match sidecar-lite's 128-bit bitmap width.

## Table translation (asic/src/softnpu/table.rs)

We map sidecar.p4 table names to sidecar-lite equivalents and translate action parameters where the designs differ for emulation.

## Fix update semantics

We fix an issue with stale in-memory sources after update with absent source list(s). `modify_group_external` wrote back `new.sources.or(existing)` to the in-memory record while `update_external_tables` in the same call canonicalized `new.sources` directly to rewrite the P4 source-filter entries. A `None` update therefore cleared the P4 table but left a stale `Some([...])` in memory.

All multicast action arms are gated with #[cfg(feature = "multicast")].



## References and Deps

- [softnpu #183](oxidecomputer/softnpu#183)
- [propolis #1093](oxidecomputer/propolis#1093)
- [p4rs #240](oxidecomputer/p4#240)
- [sidecar-lite #152](oxidecomputer/sidecar-lite#152)
- tokio: 1.50 (due to softnpu)
- oxide-tokio-rt: 0.1.3 (following-up from tokio's move to 1.50)
- bump oxide-tokio-rt to 0.1.4, tokio to 1.52.1 (matching Omicron)
zeeshanlakhani added a commit to oxidecomputer/sidecar-lite that referenced this pull request Jun 2, 2026
This adds multicast replication to sidecar-lite, emulating DPD's
sidecar.p4

We manage 2 bitmaps per multicast destination (external, underlay),
in which replication is driven via the `Replicate` extern (from p4rs #240).
The ingress control block suppresses (or doesn't) these bitmaps based
on the geneve multicast option tag, and then merges both into a single
replication bitmap. This tag is read-only, matching DPD's sidecar.p4.
OPTE (would) stamp it before the packet reaches the switch.

Egress decap is gated on the reserved underlay subnet (ff04::/64, per
RFC 7346 admin-local scope + Omicron) and tag=2 (Both), matching DPD's
egress mcast_tag_check. Ports in the decap table get geneve stripped,
others keep encapsulation for OPTE to handle on the receiving sled.

Multicast dst MAC derivation (RFC 1112 for IPv4, RFC 2464 for IPv6)
and per-port source MAC rewrite run on every replicated copy. Egress
validates the outer IP multicast range before any mcast processing.

Decap port selection uses a RID-keyed bitmap table (tbl_decap_ports),
matching Dendrite's tbl_decap_ports + port_bitmap_check. The egress
shifts 1 << egress.port and ANDs against the bitmap to determine
per-port decap, with optional VLAN insertion (following the current
sidecar in DPD).

We augmented the IPv4 header checksum after TTL decrement to use
ones-complement addition with carry fold via 16-to-32-bit widening.

### References

This work depends on the P4(rs) work in
[#240](oxidecomputer/p4#240).
zeeshanlakhani added a commit to oxidecomputer/propolis that referenced this pull request Sep 2, 2026
The mainline PR points softnpu at main to merge in pieces. a4x2 needs
bitmap-based replication from softnpu zl/multicast, so this branch
carries the pin until oxidecomputer/p4#240 and oxidecomputer/softnpu#183
land.
@zeeshanlakhani
zeeshanlakhani marked this pull request as draft September 5, 2026 03:44
@rcgoodfellow

rcgoodfellow commented Sep 29, 2026 •

Copy link
Copy Markdown
Collaborator

What does the Replicate extern get us that a simple bitmap in egress metadata does not? I think this could be implemented in a simpler way by just using the port_bitmap metadata field introduced in this PR and then implementing multicast replication similar to how broadcast replication is already done.

Having replication state in metadata as a field also makes things a bit more debuggable, as then it's a concrete value in the state of the pipeline that things like dtrace scripts can observe.

@zeeshanlakhani

Copy link
Copy Markdown
Contributor Author

What does the Replicate extern get us that a simple bitmap in egress metadata does not? I think this could be implemented in a simpler way by just using the port_bitmap metadata field introduced in this PR and then implementing multicast replication similar to how broadcast replication is already done.

Having replication state in metadata as a field also makes things a bit more debuggable, as then it's a concrete value in the state of the pipeline that things like dtrace scripts can observe.

Yep. I'm actually taking this approach in my cleanup work that will go in soon. The original thinking was to match and model the PRE being a fixed-fucntion block on hardware with an extern, while also avoiding some extra magic field naming.

The other thing pushing me in this direction is that sidecar.p4 already does it this way. The mcast_grp_a/mcast_grp_b fields are metadata and get matched on downstream in mcast_tag_check, and the egress keys-on tbl_decap_ports for eg_intr_md.egress_rid, allowing later stages to read the state back and invalidate a group. The extern doesn't give us this ability.

I'll continue to move to the simpler mechanism.

@zeeshanlakhani zeeshanlakhani self-assigned this Oct 12, 2026
Includes:

- Per Ry's review, this drops the `Replicate` extern and the AST scanning that
  went with it for a `port_bitmap` field in egress metadata, read after
  ingress, ahead of broadcast/unicast. It'll now show up in dtrace
  metadata dumps for debugging, like other fields. Each selected port
  runs egress with its own copy of headers and metadata.
- The example/test sidecar-lite is modified to remove its `Replicate`
  extern.
  - The mcast fixture now carries dendrite-style MAC splitting, i.e.
    unicast in the ingress pipeline and replicas in the egress pipeline,
    while the shift, slice, and metadata-names tests check and assert on
    packet bytes and `process_packet_headers` together.
- Fixes and tightening around expression precedence, shift typing (>=
  width returns zero), unsigned comparisons, slice reads/writes, and
  operator carets in diagnostics.
- bit<W> -> bit<V> widening keeps the existing value (for sub-byte
  widths too).
  - This is an x4c extension at the moment since there's no casting yet,
    and narrowing is now rejected.
- Added HLIR checks on initializers, assignments, and slice bounds: runtime
  (unsized) int is rejected, and x4c/SoftNPU limitations get bubbled up
  instead of looking just like type errors.
@zeeshanlakhani
zeeshanlakhani marked this pull request as ready for review October 13, 2026 01:08
@zeeshanlakhani

Copy link
Copy Markdown
Contributor Author

@rcgoodfellow back up for review.

This cleanup does two things:

- The checker now errors appropriately when a table's default action
  isn't listed in its actions list (P4-16 14.2.1.3). I've updated
  the fixtures/tests.

- `dynamic_router_noaddr.p4` failed to compile (on `main` even) because
  its `local` action shadowed the `local` table, but nothing built it,
  and it went unnoticed. This is now fixed.
When adding new tests to sidecar-lite (based on this dep), I started
seeing things like VLAN 300 coming out as 44 (and 256 as 0) on a decap
action. VLAN 100 in the tests hid this issue because it fit into 1 byte.

This work fixes the issue in `extract_bit_action_parameter`, which used
`resize(n)` and dropped the low nibble of the high byte. We now shift the
partial last byte into the high bits, the same layout as `store_le` and
the header fields, which works for any width.

Plus, we also include the latent codegen offset (`n >> 3` ->
`div_ceil(8)`), short `parameter_data` now being ignored vs panicking,
and updated harness checks (vid, timeout, extra frames).
zeeshanlakhani added a commit to oxidecomputer/sidecar-lite that referenced this pull request Oct 15, 2026
This adds multicast replication to sidecar-lite, emulating DPD's
sidecar.p4 on dendrite @ zl/mcast-swadm, following RFD 488.

We manage 2 bitmaps per underlay group (external and underlay), for
which replication is driven by p4rs `replicate` (dep @ p4#240). Ingress
zeroes out at most one of these bitmaps determined by the geneve
multicast option tag (tag 0 zeroes out underlay; tag 1 zeroes out
external), and merges both.

Tag 0 (External) also decaps upon ingress. With no tag given, as on
external NAT ingress packets, both bitmaps remain in place.

Only geneve-encapsulated multicast commits to replication. Multicast
packets that miss ingress NAT drop on the empty bitmap, matching DPD's
`drop_mcastv{4,6}_no_group` semantics. Packet copies that
remain encapsulated lose a hop on the outer IPv6 header, as modelled in
`MulticastRouter6`.

External multicast is NAT-encapsulated into the underlay via
`nat_v{4,6}_mcast(_untagged)`, reusing `forward_to_sled`. Tagged and
untagged variants exist as separate tables since x4c cannot key
directly on header validity. The encap piece now zeroes out the outer
src, traffic class, flow label, and inner eth src, like DPD's
`add_encap_headers`.

Egress decap checks `ff04::/64` and `tag=2`, matching DPD's egress
`mcast_tag_check`, with per-port decap from a RID-keyed bitmap
(`tbl_decap_ports`) and optional VLAN. `tag=2` decapsulation also
rejects inner unicast. `tag=0` copies (already decapped in ingress)
use the table for the VLAN. Dst MAC derivation (RFC 1112, RFC 2464)
and per-port src MAC rewriting run on every packet copy.

Includes:

- The parser rejects `ff01` addresses outright, as well as `ff02` destined
  packets with a hop limit of 0 (`ff02` *does* still go to the scrimlet).
- Multicast packets with a TTL/hop_limit <= 1 get dropped in ingress
  after the `fwd_from_scrimlet` return, letting scrimlet transmissions
  pass through.
- Both decapsulation paths drop inner TTL <= 1. IPv4 checksums use
  RFC 1624 (eqn. 3): a checksum of 0xfeff turns into 0x0000 after
  decrementing the TTL (XOR used here due to `~` not being parsable by x4c).
- scadm gets add/remove/dump for replication (v6), source filters, mcast
  NAT, decap ports, src MAC, and asic-id-to-port (for logical mapping).
- Unicast modifications:
  - The router drops TTL/hop_limit <= 1 and decrements remaining packets
    on routes to nonzero ports, updating the IPv4 checksum.
    - Port 0 (the scrimlet) skips both the TTL check and decrement.
    - DPD's Router4/6 still decrement service-bound packets.
  - The NAT encap zeroing described above is shared with unicast NAT.
  - Packets with a parsed Geneve header skip ingress NAT, including
    unicast.

### References

This work depends on the P4(rs) work in
[#240](oxidecomputer/p4#240) and softnpu
zl/multicast.

@rcgoodfellow rcgoodfellow left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @zeeshanlakhani. Things are looking much better after moving from an extern to metadata for multicast egress replication. Also some great compiler additions here! Comments follow.

Comment thread codegen/rust/src/expression.rs Outdated
Comment thread codegen/rust/src/expression.rs Outdated
};

let (hi, lo) = Self::slice_bounds(begin, end);
if Self::slice_is_contiguous(hi, lo, field_width) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

slice_is_contiguous calls reversed_slice_range and returns is_some on the result (effectively throwing away the calculated indices) and then generate_slice calls reversed_slice_range again with an unwrap to get the actual start and end indices. It's seems like the flow could be improved here to only call reversed_slice_range once and then match on the resulting Option for generate_slice obviating the need for the extra calls and the unwrap.

But a higher-level question for the entire implementation: is generate_slice_read_arith sufficiently general for both cases? It's a LOT simpler than going through all the index calculations and it may actually be faster code at the end of the day to perform the slice operation through a simple shift on an integer rather than futzing with index extraction over the bitvec.

@zeeshanlakhani zeeshanlakhani Oct 18, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, you're right. It should be general enough already and had handled every non-contiguous slice plus all the non-byte-multiple widths > 8.

The next push will use arithmetic for all slice reads and writes. I've dropped reversed_slice_range, slice_is_contiguous, and generate_slice methods.

Note: one thing did come up during this edit: the mask was (1u128 << w) - 1, which overflows for a full [127:0]. It never hit previously because only the range path took that case; it's u128::MAX >> (128 - w) now.

#(#members),*
}

impl Default for #name {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What required us to move away from a derived default impl?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I needed the bitvecs to maintain their declared widths, but the derived Default implementation left them empty, and slice reads panic in load_le on empty bitvecs.

We've been getting away with this through is_empty() guards in p4rs and with resize in slice assignment.

Comment thread lang/p4rs/src/lib.rs Outdated
Comment thread codegen/rust/src/statement.rs Outdated
Comment thread p4/src/check.rs
});
return;
}
let Some(name_info) = names.get(name) else {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are we dropping the error diagnostic for undefined call names?

@zeeshanlakhani zeeshanlakhani Oct 18, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, I'll restore it, similar reasoning initially to the one above.

Comment thread p4/src/error.rs Outdated
Comment thread p4/src/parser.rs Outdated
self.parse_binary(0)
}

fn parse_binary(&mut self, min_prec: u8) -> Result<Box<Expression>, Error> {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's a bit odd that a function called parse_binary can return expressions that are not binary expressions.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point. It's precedence related really, so I've renamed it to parse_with_precedence.

Comment thread test/src/p4/hub.p4
egress.broadcast = true;
}

action broadcast_drop() {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is a broadcast drop somehow different than a unicast drop? Seems like it should be the case that if egress.drop is set, it should not really matter what egress.broadcast or egress.port are.

@zeeshanlakhani zeeshanlakhani Oct 18, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nope, it's the same drop semantics. One action covers drop with broadcast set and the other with the unicast port set (drop_precedes_broadcast drop_precedes_unicast in hub.rs).

On main, broadcast packet copies ran through egress before checking drop (note: egress can clear it). Now, drop is checked before selecting any ports. Packets with drop set after ingress never actually reach egress.

Comment thread x4c/src/lib.rs Outdated

@rcgoodfellow rcgoodfellow left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @zeeshanlakhani. LGTM

@zeeshanlakhani
zeeshanlakhani merged commit c8b190f into main Oct 19, 2026
4 checks passed
@zeeshanlakhani
zeeshanlakhani deleted the zl/multicast branch October 19, 2026 04:59
taspelund added a commit to oxidecomputer/sidecar-lite that referenced this pull request Oct 22, 2026
oxidecomputer/p4#240 (c8b190f) makes x4c reject a table whose
default_action is not in its actions list (P4-16 14.2.1.3). Add NoAction
to the six tables that default to it, and drop to both rtr tables, and
move the p4 lock to c8b190f so the build runs the check.

Stop at c8b190f rather than p4 main: #278 (aa0d3c8) inserts
Pipeline::clear_table_entries mid-trait, so a libsidecar_lite.so built
against it would not match the vtable of softnpu hosts built against
earlier p4rs.

Signed-off-by: Trey Aspelund <trey@oxidecomputer.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants