Skip to content

Audio leaves soundserver for an hda driver per controller; the matcher takes any prog-if - #953

Merged
Japabu merged 3 commits into
mainfrom
wt/toyos-hda-2
Oct 20, 2026
Merged

Japabu merged 3 commits into
mainfrom
wt/toyos-hda-2

Conversation

@Japabu

@Japabu Japabu commented Oct 20, 2026 •

Copy link
Copy Markdown
Collaborator

Draft. Finishes the audio migration that #950 started; #950 was merged early (a15ac4d). Audio is broken on both benches on main until this lands: on the T14 soundserver falls back to the null sink, and on the AMD bench soundserver does not start. main's guest job is also red on #950's QEMU row.

Bench at aa7767b: T14 3/3 PASS. AMD 0/3, because both HDA controllers sit behind the full bus-3 bridge, which P1 (#947) fixes. Review round 1 at 66776de is below.

Problem (measured)

  • GitHub guest job red (run 38727776749). hda_drives_its_codec_through_its_claim never saw soundserver: hda path configured, yet volume_step exited 0 with the codec's readbacks. So the path was configured; soundserver just printed the line between the ready marker and the job's window, and the test captures neither of those spans. This was a gap in what the harness captures, not a TCG speed problem or a fixed timeout.
  • The T14 matched nothing. Its HDA function is 00:1f.3 [0403] 8086:a0c8 prog_if=80, read from the readback's enumeration line. pci:04:03:00 names programming interface 00 only.
  • The AMD bench has two HDA functions (03:00.1 GPU display audio, 03:00.6 analog). A device function is claimed by its arrival, and a drives row has one process per function (lifecycle stage 1.2) #926 refuses to start a single-process row whose matcher names two functions.
  • The tone jobs panicked on a null sink at master.rs:12 ("message 4").

Change

  • lib/manifest: pci:CC:SS:* matches every programming interface of a subclass (PciMatch::prog_if: Option<u8>). An overlap is still decided from the text alone. HD Audio's 00 and 80 are the same register model.
  • hda (system/hda): a drives row on pci:04:03:*, one process per controller, as in stage 3 of issues/a-device-function-is-driven-from-its-arrival-to-its-end.md. Each process resets its own controller, walks its codecs and finds its output; toyos-hda moved to system/hda/driver. The multi-controller choose is gone, because each driver decides for its own controller. A driver that reaches no output ends by name.
  • toyos-sound-output (system/soundserver/output): the protocol between soundserver and its output drivers, on soundserver's new sound-output port. It is pure, and every frame is decoded and refused there. A driver offers its rate, channels, ring shape and master. soundserver lends it the ring of periods, and the driver maps that ring through its claim and points its descriptor list at it. A second connection carries start, stop and each period played, stamped with the claim's interrupt times. Master requests go to the driver, and its codec readback comes back to the volume port. The driver is a client of soundserver's port and serves no name, per the track's ruling.
  • soundserver is the null sink until an output is offered, and switches to that output only once no stream is open, so a stream never changes device. Later offers are refused. Nothing a driver sends ends soundserver. A hang-up, a frame the protocol does not have, or a report that is no run of the ring drops the driver by name: the streams on it end and soundserver is the null sink again. Overlapping reports from a reader a ring behind are counted as a lap (a drain). Offers are bounded to 16 periods of at most 16 KiB. Master requests are forwarded one at a time, and each answer goes only to the connection that asked. virtio-sound stays in-process and is recorded as stage 3's next item, because moving it is not cheap here.
  • The master jobs fail by name when soundserver has no master, instead of on a message number.

Tests and evidence

Evidence: https://github.com/ToyOSOrg/evidence/tree/f6b1654005d426e21b33932063e231d97fa0ecea/hda-drivers

check revision result
cargo run -- --ci host aa7767b's tree, before two edits (a module-header comment and a Cargo description) 85 of 86 green. The red step is llvm::tests::the_caller_s_environment_reaches_neither_the_build_nor_the_key, which is also red on origin/main 0e6716f (exit 101); recorded by #952's issues/the-llvm-environment-test-breaks-git-on-macos.md
cargo run -- --build-only, --arch aarch64 aa7767b 0, 0
QEMU q35 + VT-d hda_plays_to_the_controller_that_reaches_an_output aa7767b's tree 0
control: RINTCTL masked, same row + patch red: no driver offers, the row stalls
control: the offer's rate check removed + patch 101
metal: testcases staged for hda_tone, hda_tone_resampled, hda_volume_step aa7767b staged, not run
  • Host tests:
    • matcher wildcard and overlap;
    • every protocol frame round-trips, and each malformed offer, master or report is refused;
    • ring-mask runs;
    • report combining (clocking across reports, a period reported twice);
    • the controller model with a lent ring (unaligned, or past 32 bits on a narrow controller);
    • each of the AMD bench's recorded controllers walked alone (display audio reaches nothing, analog reaches its speaker).
  • QEMU row (Profile::MetalTwoHda: two ich9-intel-hda, the first with no codec). This needs a guest because it involves three processes and two claims behind a remapping unit. Both drivers start: the first ends with plays nothing: … no codec, the second offers, and soundserver plays to it. volume_step then steps that codec. The row waits for the console line rather than reading the boot log. It replaces soundserver drives the HDA function whole through a pci claim; the kernel drives no audio #950's one-controller row, which this boot covers. It runs on q35 only, because virt has no unit to put the functions behind.
  • Exploratory, not a row: the same boot running audio_tone passed through the driver process, with 699 completions in a 2 s window and underruns=0. The tone rows stay on the benches because of cpal's close bound (issues/cpals-close-bound-is-one-ring-of-device-time.md, owner: the cpal fork).

Review round 1 (66776de)

Evidence: https://github.com/ToyOSOrg/evidence/tree/74a5501d83ca5430ece25e9a0412195554c79166/hda-drivers/review-1

check result
host tests: lagging and overlapping reports are a lap; an oversize offer (period, ring) is refused; a master flood keeps one request in flight; an answer after a reconnect goes only to the asker; a driver whose connection takes no request is a named refusal pass
cargo run -- --ci host 85/86; the red is the same llvm environment test
both images 0 / 0
QEMU two-controller row 0; exploratory audio_tone 0, underruns=0
testcases staged, T14 only (the tone jobs' soundserver path changed); AMD waits for P1 staged

Review round 2 (16bb9e7)

  • The driver waits for the offer's answer through its poller, with a 30 s hang ceiling. When it expires the driver ends by name and the controller is released.
  • MSG_START carries a run number that each Played report echoes, and soundserver drops reports of any other run.
  • The relay tests that could not fail are deleted.
  • Checks: --ci host 86/86 green; QEMU two-controller row passes; exploratory tone exit 0 with underruns=0; T14 restaged. Bench at 66776de: T14 3/3 PASS.

Evidence: https://github.com/ToyOSOrg/evidence/tree/ec8de94003235cd61f798ad35fac05157a1ef244/hda-drivers/review-2

Decisions to check

  • soundserver plays to the first offer it can mix into. The analog driver is the only one that offers on the AMD bench. Choosing among two audible outputs is a default-device policy, and nothing keeps one; a second offer is refused by name.
  • The output starts only when no stream is open. Moving a stream to another device, a driver's gone, and hot-plug are stage 3's remaining items and stage 2's, recorded on the track.
  • The driver takes the RT band (syscap = ["rt"], the existing capability), because its interrupt is a period's end. This uses an existing mechanism and adds none.

Risks

  • Neither bench has run this. On a bench, a tone job that started before the driver's offer was played to would play to the null sink and fail by name. On the testcases boot the audio jobs run after the wake jobs, seconds after boot. The judges require the playing to line to come before the job's spawn.
  • One extra process hop per period (interrupt → driver → soundserver). The DLL is clocked by the claim's interrupt times, not by the hop.

🤖 Generated with Claude Code

…r takes any prog-if

#950 left three defects on main, each measured:

- GitHub's guest job red `hda_drives_its_codec_through_its_claim`:
  soundserver configured the path (`volume_step` exited 0 with the
  codec's readbacks) but said so between the ready marker and the job's
  window, which neither capture piece holds. The row now awaits the line
  on the console instead of reading the boot log.
- The T14 matched nothing: its HDA function is `00:1f.3 [0403] 8086:a0c8
  prog_if=80` (the readback's enumeration line), and `pci:04:03:00`
  named prog-if 00 only. `lib/manifest`'s grammar gains `pci:CC:SS:*`,
  every programming interface of a subclass; HD Audio's 00 and 80 are one
  register model.
- The AMD bench's two HDA functions made the single-process row's start
  refused. By the lifecycle ruling (stage 3 of
  issues/a-device-function-is-driven-from-its-arrival-to-its-end.md),
  audio's driver leaves the shared service:

`hda` (system/hda) is a `drives` row on `pci:04:03:*`, one process per
controller. Each resets its controller, walks its own codecs and finds
its output (`toyos-hda`, moved to system/hda/driver; `choose` over
several controllers is gone, each driver decides for its own), and a
driver that reaches none ends by name. One that does offers it on
soundserver's new `sound-output` port (`toyos-sound-output`, every frame
decoded and refused there): soundserver lends it the ring of periods,
which the driver maps through its claim and points its descriptor list
at, and the stream connection carries start, stop and the periods played
with the claim's interrupt times. The master's requests go to the driver
and its codec readback comes back to the volume port.

soundserver is the null sink until an output is offered and plays to it
once no stream is open, so a stream never changes device under it;
virtio-sound stays driven inside soundserver (stage 3's next item).

The tone jobs' master now fails by name on a soundserver with no master
rather than on a message number.

QEMU: `hda_plays_to_the_controller_that_reaches_an_output` boots two
ich9-intel-hda, the first with no codec: its driver plays nothing,
soundserver plays to the second's, and `volume_step` steps that codec.

Records: the lifecycle track's stage 3, the drivers track's HDA item, the
jack-detection track's paths; filed cpal's close bound
(issues/cpals-close-bound-is-one-ring-of-device-time.md) and the host-CI
red that is main's too
(issues/the-llvm-environment-test-dies-where-git-is-the-xcode-shim.md).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Japabu

Japabu commented Oct 20, 2026

Copy link
Copy Markdown
Collaborator Author

Bench at aa7767b (same image on both, sha in /Users/jan/.claude/jobs/2280e09e/tmp/scratchpad/agents/hda/r2/metal): T14 3/3 PASS (hda_tone, hda_tone_resampled, hda_volume_step). AMD bench 0/3: both HDA controllers (03:00.1, 03:00.6) are behind the bus-3 bridge whose window is full, so pcidev keeps BAR 0 where firmware put it and refuses the claim (no 2 MiB the bridges above it forward is free → NOT HANDED OVER); the hda drivers never start and the tone jobs fail by name on the null sink. That is the placement defect P1 (#947) removes by mapping BARs in place in 4 KiB pages; AMD audio returns when #947 lands. Temporary gap accepted under 'Migrate directly' (main's AMD audio is already broken since #950).

@Japabu

Japabu commented Oct 20, 2026

Copy link
Copy Markdown
Collaborator Author

Review of #953 at aa7767b5f

Architecture. The split follows the owner's stage-3 ruling. hda is a drives row, one process per function (images/system.toml:111-114), and it reaches soundserver only through receives = ["sound-output"]. soundserver stays the single client-facing service. toyos-sound-output is a real two-party boundary that owns its wire types. Neither side uses kernel ABI errors as the protocol: SyscallError appears only on the driver's own claim (system/hda/src/main.rs:334-337). No kernel file changes. The driver enters the RT band through the existing rt syscap (main.rs:447-451, syscap = ["rt"]), so no scheduling mechanism is added. The deleted multi-controller choose is the right deletion: each driver decides for its own controller. #926's single-instance refusal is untouched, because hda is a drives row and claims_apart (lib/manifest/src/lib.rs:495-527) still refuses any two entries whose text can name one function. With pci:04:03:*, overlaps now treats prog_if like the card (device.rs:34-48). The only other 04:xx matcher is virtio-sound's 04:01:00, and the two stay apart (tested at device.rs:241). No other row changes meaning. Diff: +1863/−906. The growth is the new process (452), the protocol crate (364, about half tests), soundserver's output.rs (394), and the hda crate move.

Verified.

  • DLL clock. The DLL is still clocked by the claim's interrupt times. The driver forwards record.first_nanos/last_nanos unchanged (main.rs:349). combine keeps the newest last_nanos and counts the earlier report's periods as clocked by it, which is correct for abutting runs (output.rs:278-294). The hop adds only latency.
  • Null-sink race. The bench judges cannot pass silently. on_an_output (tests/common/audio.rs:57-69) requires playing to the output offered before the job's spawn record. That line is said only after the mix thread received Attach. The control thread submits Attach before it serves any later stream-open (control.rs:171-178), so a job spawned after the line streams to the output. A late log line can only produce a false red. volume_step fails by name on MSG_VOLUME_UNAVAILABLE (master.rs:12-15). judges_verdict has the reversed-order negative.
  • QEMU row. hda_plays_to_the_controller_that_reaches_an_output checks exactly one driver silent with "no codec is on its link", exactly one offer, the silent one at the lower BDF, one playing to line, no DMA fault, and volume_step green. In the evidence log, 00:04.0 exits 0 and 00:05.0 offers, is accepted and is played to. The RINTCTL control turns the row red (both drivers play nothing), so the await is not vacuous. The rate control is a host-unit control of the decoder, not of the row; that is acceptable. The deleted one-controller row's coverage (claim, reset, CORB/RIRB, walk, master readback behind VT-d) is absorbed by this boot.
  • Records. Stage 3, every-driver-is-still-in-the-kernel.md, jack-detection and the cpal close-bound issue are accurate about what is done and what is next. No stale soundserver/hda or pci:04:03:00 references remain outside the matcher tests.
  • llvm red. It is independent of this PR: host/main-llvm-environment-test.log shows the same failure on 0e6716f (exit 101), from the harness's own git under the poisoned SDKROOT.

BLOCKER

  1. system/soundserver/src/output.rs:287 — a late soundserver now panics on two reports that legitimately overlap.

    • The driver reports each interrupt's position delta (main.rs:339-351). soundserver ORs every pending report at one wake (output.rs:333-347) and asserts the masks are disjoint.
    • Once the mix thread falls a ring (8 × 128 frames ≈ 23 ms) behind the driver, two pending reports cover the same period. One example is {0..6} then {7,0..5}; another is a 0xFF lap followed by anything. The assert then kills soundserver and all audio.
    • Before this PR, the same lateness produced one position-derived mask, at most Run::Lapped, which mix.rs:378 deliberately "counts as the drain it is".
    • The PR's own cpal issue records QEMU/TCG completing "bursts of up to a ring" (max_batch=7), so this is reachable on QEMU under load and on a stalled metal machine.
    • a_period_reported_twice_ends_soundserver_by_name enshrines the regression as intended behaviour.
    • Fix: a union that covers or overlaps the ring is Lapped (all bits, clocked by the newest report), not a protocol violation. Add a host test of two overlapping reports.
  2. system/soundserver/output/src/lib.rs:78-91 — an offer's period_bytes has no upper bound.

    • The decoder checks only that it is non-zero and a whole number of frames. offered then allocates periods × period_bytes of shared memory chosen by the driver (output.rs:163, up to 32 × 4 GiB).
    • The mixer then sizes its period, scratch and every client slot from it, and toyos_mixer::period_frames bounds nothing either.
    • The brief asks for lent-ring bounds at both ends. The driver side checks the lent size and alignment (main.rs:252, controller.rs RingUnaligned/Beyond32Bits), but soundserver's side takes an untrusted length into an allocation.
    • Fix: bound period_bytes in Offer::decode (to the largest period the mixer is built for) and add a refusal case to an_offer_outside_what_soundserver_mixes_is_refused.
  3. system/soundserver/src/volume.rs:49-97 with output.rs:251-261 — forwarded master requests have no in-flight bound and no owner.

    • serve forwards every framed request at once with try_send, and ask panics if the driver's inbound buffer is full. A volume client that writes requests faster than the driver performs codec round-trips therefore crashes soundserver. The volume protocol does not forbid this; the current compositor happens to keep one request outstanding.
    • Answers go to clients.first() (volume.rs:85-92). After a reconnect, which accepted handles by replacing the holder, the new connection gets the previous holder's answer as well as its own.
    • Before this PR the master was synchronous, so neither could happen.
    • Fix: keep one forwarded request in flight, read the client's next frame only after the answer, and drop an answer whose client has gone. This removes both the panic and the misrouting.
  4. issues/the-llvm-environment-test-dies-where-git-is-the-xcode-shim.md duplicates kernel-loom: each model's vacuity witness is its own test's #952's issues/the-llvm-environment-test-breaks-git-on-macos.md. Both record the same defect: the rerun's git resolving through the poisoned SDKROOT. Whichever PR lands second must drop its copy, or point to the other's, so the defect has one record. The orchestrator should pick which one.

SUGGESTION

  • system/hda/src/main.rs:408 — the offer's answer is a blocking recv_header with no failure timeout. soundserver answers immediately by design, so a bound sized as a hang ceiling would turn a stuck soundserver into a named driver exit instead of a held controller.
  • system/hda/src/main.rs:156 and the _ring field — dma_map already keeps the region alive while it is mapped (sdk/toyos/src/device.rs:156-159). Holding the handle is harmless, but the field comment ("the device reaches the third while it is mapped") suggests it is required.
  • issues/hda-has-no-jack-detection-and-nothing-persists.md:24-26 — "the refusal-to-null-sink path is asserted only on a machine that has no controller at all" is now partly stale. The QEMU row asserts a codec-less controller's refusal with soundserver staying the null sink until an offer.
  • output.rs:66 — Phase::Playing's Option<u8> is only ever tested for Some. A bool (has a master) says what it is used for.

Evidence. The commands, revisions and exits in hda-drivers/ match the PR table. The metal staging command named hda_tone hda_volume_step; the substring filter staged three registrations, which matches the claim that hda_tone_resampled was staged too. Nothing on metal has been measured. Blocker 1 needs a host test, not a guest run. Blockers 2 and 3 are host-testable in toyos-sound-output and in soundserver's volume/output modules.

SEND BACK

…aster requests bounded

- A reader a ring behind holds reports that overlap ({0..6} then
  {7, 0..5}); `combine` makes any overlapping, or non-run, union a whole
  lap, which the mix loop counts as the drain it is. A report that is no
  run of the ring, a frame the stream protocol has not, or a hang-up now
  ends the device loop (`Backend::ended`) instead of soundserver: the mix
  thread ends the streams it held, tells the control thread on a pipe of
  its own, and soundserver is the null sink again. A control-connection
  violation drops the driver the same way and sends `MixCommand::Detach`.
  A stream opened against an output since gone carries that output's
  generation and is ended where it reaches another's loop. A report read
  while the engine is stopped is dropped.
- `Offer::decode` bounds the ring to the mixer's deepest pipeline (16) and
  a period to `MAX_PERIOD_BYTES` (16 KiB), before soundserver allocates
  the lent ring from them.
- The volume port forwards one master request at a time (`volume::Relay`):
  the holder's next is not read, nor its connection watched, until the
  answer, which goes only to the connection that asked. A driver whose
  connection takes no request is answered unavailable by name.
- Dropped the llvm issue; #952's
  `issues/the-llvm-environment-test-breaks-git-on-macos.md` is the record.
- The `hda` driver's ring comment, `Phase::Playing`'s master as a bool, and
  the jack-detection track's stale line.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Japabu

Japabu commented Oct 20, 2026

Copy link
Copy Markdown
Collaborator Author

Round 1 at 66776de2f. Evidence: https://github.com/ToyOSOrg/evidence/tree/74a5501d83ca5430ece25e9a0412195554c79166/hda-drivers/review-1

  1. Overlapping reports. combine turns an overlapping or non-run union into a whole lap, clocked by the newest report; the mix loop counts it as a drain. No input from the driver ends soundserver:
    • A frame the stream protocol does not have, a report that is no run of the ring, or a hang-up ends the device loop (Backend::ended).
    • The mix thread then ends its streams, tells the control thread on a pipe, and soundserver is the null sink again.
    • A control-connection violation drops the driver the same way and sends MixCommand::Detach.
    • A stream opened against an output that is gone carries its generation and is ended rather than mixed.
    • Reports read while the engine is stopped are dropped.
    • Tested by reports_a_lagging_reader_holds_are_a_lap. The test that asserted a panic is gone.
  2. Offer bounds. Offer::decode bounds periods to the mixer's MAX_PIPELINE (16) and period_bytes to MAX_PERIOD_BYTES (16 KiB). Refusals are added for an oversize period and for 32 periods; the widest accepted offer is also tested.
  3. Master requests. volume::Relay keeps one request in flight. The holder's next request is not read, and its connection is not watched, until the answer comes. The answer goes only to the connection that asked. A driver whose connection is full gets Asked::Refused, which is answered unavailable by name. Tested by one_master_request_is_in_flight_at_a_time, an_answer_goes_only_to_the_connection_that_asked and a_driver_that_takes_no_request_is_a_named_refusal.
  4. llvm issue. My file is dropped; kernel-loom: each model's vacuity witness is its own test's #952's is the record.

Suggestions:

  • Accepted: the ring comment, Phase::Playing as a bool, and the stale jack-detection line.
  • Declined: a timeout on the offer's blocking recv. The SDK has no bounded recv, and soundserver answers within the pass that reads the offer.

Checks:

  • --ci host: 85/86, the red being the llvm test that is red on main too.
  • Both images build.
  • The QEMU two-controller row passes, and an exploratory audio_tone run on it passes with underruns=0.
  • testcases is restaged for the T14 only.

@Japabu

Japabu commented Oct 20, 2026

Copy link
Copy Markdown
Collaborator Author

Re-review of #953 at 66776de2f (round 1's fixes only)

Previous blockers

  1. Overlapping reports. Resolved.
    • combine (system/soundserver/src/output.rs:316-341) turns an overlapping or non-run union into a full lap clocked by the newest report. mix.rs:378 counts that lap as a drain, as it counted a position-derived lap before this PR.
    • Every report is also checked to be a run of the ring before it is combined (output.rs:411-416).
    • Dropping reports read while the engine is stopped is right: after a stop, fillable frees the whole ring (mix.rs:205), so a stale mask would otherwise hit the repeated completion assert.
    • Driver input no longer panics soundserver. I traced the drop paths:
      • A fault on the control connection drops the driver, raises Event::Gone, then sends Detach. The mix thread ends its streams and returns to the null sink.
      • A fault on the stream connection sets Backend::ended. The mix thread ends its streams, writes the ended pipe, and the control thread drops the driver. The Detach that follows reaches the null sink, which ignores it.
      • Generations keep a stream opened on a departed output from being mixed. Attach can only follow Detach in the FIFO, so the remaining panic! at mix.rs:380 is a true invariant.
      • A request in flight when the driver goes is answered unavailable (control.rs:252-254).
  2. Offer bound. Resolved. Offer::decode limits a ring to 16 periods of at most 16 KiB before anything is allocated, so the largest ring is 256 KiB. MAX_PERIODS now comes from the mixer's MAX_PIPELINE, and run's shift guard is correct for a ring of up to 16.
  3. Master requests. Resolved. At most one request is in flight: the holder is not watched or read while one is (control.rs:198-200, volume.rs:84). An answer goes only to the connection id that asked, and a full driver buffer is answered unavailable by name.
  4. llvm duplicate. Resolved. This PR's issue file is deleted, and kernel-loom: each model's vacuity witness is its own test's #952's is the record.

On the declined timeout. The offer's recv_header (system/hda/src/main.rs:409) is a wait for another process's answer, so root CLAUDE.md's "A wait has a failure timeout" applies. The reason given for declining is factually wrong. The SDK has no bounded recv, but the driver already holds a Poller, and Poller::wait(1, timeout_nanos, …) bounds a wait for control to become readable. After that, recv_header cannot block on a frame soundserver has started writing. The fix is a few lines and replaces a silent hang, with the controller held, by a named plays nothing: soundserver did not answer the offer. A cheap fix that the rule names directly should not become a recorded gap.

BLOCKER

  • system/hda/src/main.rs:405-412: bound the wait for the offer's answer through the poller, with a timeout sized as a hang ceiling, and end by name when it expires.

SUGGESTION

  • Relay tests. system/soundserver/src/volume.rs one_master_request_is_in_flight_at_a_time (asserting !idle() 1000 times) and a_driver_that_takes_no_request_is_a_named_refusal only exercise Option::replace/take and a four-line match; they cannot fail for the defects they are named after. The one-in-flight and asker filters live in serve/answer and the control loop's watch, and inspection establishes them. Delete these two tests, or test serve/answer themselves. an_answer_goes_only_to_the_connection_that_asked checks only that mint returns distinct ids, not the filter in answer.
  • Stale report after a restart. The stream connection carries no run epoch. A Played report the driver sends after soundserver wrote Stop, but before the driver read it, is dropped only if it lands before the restart wake's completion(). One that lands after Start is taken as part of the new run. This window is microseconds against a client connect of milliseconds, so it is unlikely, but a run counter on Start that Played echoes would close it.

Coverage. Host-only coverage is right for the lap rule and the offer bound: both are decoding of a peer's input, which the policy assigns to unit tests, and the tests would fail for the defects they name. Driver-drop recovery spans two threads and pipes. Inspection establishes it, as traced above, and a guest test needs no driver fault-injection path, which production does not need. The tautological tests above are the gap in the master-request evidence; a guest run is not. I did not reinspect the QEMU row or the T14 restaging; the orchestrator judges the bench.

SEND BACK

… reports carry their run

- The `hda` driver waits for soundserver's answer to its offer through
  its poller, for at most `OFFER_ANSWER` (30 s, a hang ceiling:
  soundserver answers in the pass that reads the offer). Past it the
  driver ends by name and gives the controller back.
- `MSG_START` names a run and every `Played` echoes it; soundserver drops
  a report of another run than its engine's now, so one the driver sent
  before reading a stop cannot be taken as the next run's.
- Deleted the relay tests that could not fail for the defects they were
  named after; the one-in-flight rule and the asker filter are `serve`,
  `answer` and the control loop's watch.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Japabu

Japabu commented Oct 20, 2026

Copy link
Copy Markdown
Collaborator Author

Round 2 at 16bb9e74b. Evidence: https://github.com/ToyOSOrg/evidence/tree/ec8de94003235cd61f798ad35fac05157a1ef244/hda-drivers/review-2

  • Blocker (offer wait). The driver now waits for soundserver's answer to its offer through its poller, for at most OFFER_ANSWER (30 s). That is a hang ceiling, since soundserver answers in the same pass that reads the offer. When it expires the driver ends with plays nothing: soundserver: it did not answer the offer within 30s, which gives the controller back.
  • Run counter (suggestion, taken). MSG_START now carries a run number, and every Played report echoes it. soundserver drops any report whose run is not its engine's current one, so a report sent before the driver read a stop cannot be counted into the next run. The protocol tests cover the run round-trip and refuse a start that carries no run.
  • Relay tests (suggestion, taken). The three tests that could not fail for the defects they were named after are deleted.

Checks:

  • --ci host: 86/86 green.
  • QEMU two-controller row: passes.
  • Exploratory audio_tone on that boot: exit 0, 699 completions per 2 s, underruns=0.

The run counter changes the start/report protocol that the bench rows exercise, so I staged testcases for the T14 at this head. The AMD bench still waits for P1.

@Japabu

Japabu commented Oct 20, 2026

Copy link
Copy Markdown
Collaborator Author

Follow-up on #953 at 16bb9e74b (round 2's findings only, 66776de2f..16bb9e74b)

  • Blocker: the offer answer's wait. Resolved. offered (system/hda/src/main.rs:420-433) now watches control on its own poller and waits at most OFFER_ANSWER (30 s, a hang ceiling). On expiry the driver ends by name ("it did not answer the offer within 30s") and gives the controller back. recv_header runs only once the connection is readable. The constant's comment states the ceiling and why it exists, and nothing more.
  • Suggestion: stale report after a restart. Taken and correct.
    • submit bumps a wrapping run number and sends it with MSG_START (output.rs:433-438).
    • The driver stores it before stream.start() and stamps every Played with it (main.rs:308-311, 356-362). Its loop calls interrupts() before reading stream frames, so a report built before a Stop or Start is read carries the old run.
    • completion combines only reports of the current run while running (output.rs:416-418), so an old run's report landing after the next Start is dropped.
    • Engine::decode refuses a payload-less MSG_START, and the round-trip test covers the new field.
  • Suggestion: tautological relay tests. All three are deleted. The one-in-flight and asker rules rest on inspection of serve/answer and the control loop's watch, as established in round 2.

Evidence ec8de94 hda-drivers/review-2 at this head: --ci host 0 (86 green), the QEMU two-controller row 0, and the exploratory tone 0 with underruns=0. The T14 rows are the orchestrator's to judge.

No required findings remain, and I have nothing new to add.

LAND

@Japabu

Japabu commented Oct 20, 2026

Copy link
Copy Markdown
Collaborator Author

Bench at 16bb9e7 (round 2), one T14 boot, image sha256 2859bfd2a23c…f788: hda_tone, hda_tone_resampled, hda_volume_step PASS (3/3). Follow-up review: LAND. AMD audio returns with P1 (#947), as recorded. Landing.

@Japabu
Japabu marked this pull request as ready for review October 20, 2026 18:38
@Japabu
Japabu added this pull request to the merge queue Oct 20, 2026
Merged via the queue into main with commit ffd8bb9 Oct 20, 2026
6 checks passed
@Japabu
Japabu deleted the wt/toyos-hda-2 branch October 20, 2026 19:01
Japabu added a commit that referenced this pull request Oct 20, 2026
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Japabu added a commit that referenced this pull request Oct 20, 2026
#953 moved the HDA driver out of soundserver into system/hda. The
modify/delete conflict on system/soundserver/src/hda.rs resolves to the
deletion: this branch's one hunk there (the description read through
toyos_pci_claim::describe) is carried to the new driver.

The new driver is a pci-claim caller main added, so it takes this
branch's rule rather than a second path: its description is
toyos_pci_claim::describe, ended by name instead of an expect, and the
ring soundserver lends is mapped through toyos_pci_claim::lend, since
KernelRefused::on is private here. lend names the region's own refusals
Why::Unlendable (not ordinary memory, already lent, or sent without
MAP): those are the peer's words about its region, not the claim's, so
Why::of's panic on an unexpected word would misreport them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.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.

1 participant