diff --git a/Documentation/zigux/phase10-virtio-net-slice.md b/Documentation/zigux/phase10-virtio-net-slice.md index 89652c7e1f2bc1..0646cb9f3fd161 100644 --- a/Documentation/zigux/phase10-virtio-net-slice.md +++ b/Documentation/zigux/phase10-virtio-net-slice.md @@ -11,9 +11,10 @@ Current starter: - `VirtioNetProbeLab.summarizeReceiveQueueRefill()` now turns the last bounded mergeable-buffer plan into an explicit refill-path summary so the lab slice can distinguish fresh mergeable allocation from recycled-room reuse without widening into page ownership or receive completion - `VirtioNetProbeLab.planReceiveQueueRefillBatch()` now turns that same bounded refill summary into queue-slot and byte counts for one refill pass, including a clampable batch limit and a fail-closed overfill guard, without widening into DMA submission, kicks, or receive completion - `VirtioNetProbeLab.reserveReceiveQueueRefillDescriptors()` now clamps one refill pass against the descriptors currently available on the receive queue, yielding one bounded reservation plan with pending-buffer carryover while still stopping short of live descriptor writes, DMA submission, or queue kicks +- `VirtioNetProbeLab.decideReceiveQueueRefillNotify()` now turns that bounded reservation plan into a queue-local notify decision that can trigger on empty-queue transitions or descriptor thresholds while still stopping short of live descriptor writes, DMA submission, or queue kicks Validation: -- `zig build test --build-file zigux/tests/phase10_build.zig --summary all` +- `zig build phase10-virtio-net-tests --build-file zigux/tests/phase10_build.zig --summary all` - the shared Phase 10 gate now includes `phase10-virtio-net-tests` beside the existing virtio core, ring, input, and survey checks Non-goals: diff --git a/drivers/net/virtio_net.zig b/drivers/net/virtio_net.zig index 03892559cbdad9..5630b35b88d45d 100644 --- a/drivers/net/virtio_net.zig +++ b/drivers/net/virtio_net.zig @@ -149,6 +149,31 @@ pub const ReceiveQueueRefillReservationPlan = struct { total_allocation_bytes: u32, }; +pub const ReceiveQueueRefillNotifyRequest = struct { + queue_was_empty: bool, + notifications_enabled: bool = true, + notify_after_descriptors: u16 = 0, +}; + +pub const ReceiveQueueRefillNotifyDecision = struct { + anchor: []const u8, + planned_queue_pairs: u16, + rx_queue_count: u16, + queue_was_empty: bool, + queue_became_non_empty: bool, + notifications_enabled: bool, + notify_after_descriptors: u16, + refill_count: u16, + descriptors_reserved: u16, + buffers_after_reservation: u16, + buffers_left_pending: u16, + descriptor_budget_exhausted: bool, + queue_will_be_full: bool, + refill_path: ReceiveQueueRefillPath, + reached_notify_threshold: bool, + should_notify: bool, +}; + pub const VirtioNetProbeLab = struct { const Self = @This(); @@ -381,6 +406,42 @@ pub const VirtioNetProbeLab = struct { }; } + pub fn decideReceiveQueueRefillNotify( + self: *Self, + reservation: ReceiveQueueRefillReservationPlan, + request: ReceiveQueueRefillNotifyRequest, + ) ReceiveQueueRefillNotifyDecision { + _ = self; + const notify_after_descriptors = if (request.notify_after_descriptors == 0) + reservation.descriptors_per_buffer + else + request.notify_after_descriptors; + const queue_became_non_empty = request.queue_was_empty and reservation.refill_count != 0; + const reached_notify_threshold = reservation.descriptors_reserved != 0 and + reservation.descriptors_reserved >= notify_after_descriptors; + + return .{ + .anchor = reservation.anchor, + .planned_queue_pairs = reservation.planned_queue_pairs, + .rx_queue_count = reservation.rx_queue_count, + .queue_was_empty = request.queue_was_empty, + .queue_became_non_empty = queue_became_non_empty, + .notifications_enabled = request.notifications_enabled, + .notify_after_descriptors = notify_after_descriptors, + .refill_count = reservation.refill_count, + .descriptors_reserved = reservation.descriptors_reserved, + .buffers_after_reservation = reservation.buffers_after_reservation, + .buffers_left_pending = reservation.buffers_left_pending, + .descriptor_budget_exhausted = reservation.descriptor_budget_exhausted, + .queue_will_be_full = reservation.queue_will_be_full, + .refill_path = reservation.refill_path, + .reached_notify_threshold = reached_notify_threshold, + .should_notify = request.notifications_enabled and + reservation.descriptors_reserved != 0 and + (queue_became_non_empty or reached_notify_threshold), + }; + } + fn checkedMulU16(lhs: u16, rhs: u16) !u16 { const value = @as(u32, lhs) * rhs; return std.math.cast(u16, value) orelse error.QueueCountOverflow; diff --git a/zigux/tests/phase10_virtio_net.zig b/zigux/tests/phase10_virtio_net.zig index 7f441b2396e1c4..323b083b2ee4ed 100644 --- a/zigux/tests/phase10_virtio_net.zig +++ b/zigux/tests/phase10_virtio_net.zig @@ -326,6 +326,89 @@ test "phase10 virtio net reserves refill descriptors without widening into live try std.testing.expectEqual(@as(u32, 25600), recycled_reservation.total_allocation_bytes); } +test "phase10 virtio net makes a bounded notify decision from the queued refill reservation" { + var device = try virtio_net.VirtioNetProbeLab.init(&.{ + virtio_net.feature_mergeable_rx_buffers, + virtio_net.feature_control_vq, + virtio_net.feature_multiqueue, + }); + const snapshot = try device.captureProbeSnapshot(.{ + .driver_feature_bits = &.{ + virtio_net.feature_mergeable_rx_buffers, + virtio_net.feature_control_vq, + virtio_net.feature_multiqueue, + }, + .requested_queue_pairs = 2, + .max_queue_pairs = 2, + }); + _ = try device.planMergeableReceiveBuffer(snapshot, .{ + .header_len = 12, + .average_packet_len = 1500, + .min_buf_len = 512, + .headroom = 256, + .cache_line_size = 64, + .skb_shared_info_size = 320, + }); + + const reservation = try device.reserveReceiveQueueRefillDescriptors(.{ + .queue_capacity = 256, + .buffers_posted = 192, + .batch_limit = 32, + .descriptors_available = 48, + .descriptors_per_buffer = 2, + }); + + const empty_transition = device.decideReceiveQueueRefillNotify(reservation, .{ + .queue_was_empty = true, + .notify_after_descriptors = 64, + }); + try std.testing.expectEqualStrings("drivers/net/virtio_net.c", empty_transition.anchor); + try std.testing.expect(empty_transition.queue_became_non_empty); + try std.testing.expectEqual(@as(u16, 64), empty_transition.notify_after_descriptors); + try std.testing.expect(!empty_transition.reached_notify_threshold); + try std.testing.expect(empty_transition.should_notify); + + const threshold_only = device.decideReceiveQueueRefillNotify(reservation, .{ + .queue_was_empty = false, + .notify_after_descriptors = 48, + }); + try std.testing.expect(!threshold_only.queue_became_non_empty); + try std.testing.expect(threshold_only.reached_notify_threshold); + try std.testing.expect(threshold_only.should_notify); + + const suppressed = device.decideReceiveQueueRefillNotify(reservation, .{ + .queue_was_empty = true, + .notifications_enabled = false, + .notify_after_descriptors = 16, + }); + try std.testing.expect(suppressed.queue_became_non_empty); + try std.testing.expect(suppressed.reached_notify_threshold); + try std.testing.expect(!suppressed.should_notify); + + const no_threshold = device.decideReceiveQueueRefillNotify(reservation, .{ + .queue_was_empty = false, + .notify_after_descriptors = 64, + }); + try std.testing.expect(!no_threshold.queue_became_non_empty); + try std.testing.expect(!no_threshold.reached_notify_threshold); + try std.testing.expect(!no_threshold.should_notify); + + const exhausted_reservation = try device.reserveReceiveQueueRefillDescriptors(.{ + .queue_capacity = 256, + .buffers_posted = 192, + .batch_limit = 32, + .descriptors_available = 0, + .descriptors_per_buffer = 2, + }); + const no_descriptors = device.decideReceiveQueueRefillNotify(exhausted_reservation, .{ + .queue_was_empty = true, + }); + try std.testing.expect(!no_descriptors.queue_became_non_empty); + try std.testing.expect(!no_descriptors.reached_notify_threshold); + try std.testing.expect(!no_descriptors.should_notify); + try std.testing.expect(no_descriptors.descriptor_budget_exhausted); +} + test "phase10 virtio net rejects mergeable buffer plans that widen beyond the negotiated safe path" { var untouched_device = try virtio_net.VirtioNetProbeLab.init(&.{virtio_net.feature_mergeable_rx_buffers}); try std.testing.expectError(error.MergeableBufferPlanUnavailable, untouched_device.summarizeReceiveQueueRefill());