Skip to content
Draft
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
15 changes: 14 additions & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@ members = [
"system/shell",
"system/soundserver",
"system/soundserver/mixer",
"system/soundserver/resample",
"system/soundserver/virtio-sound",
"system/sshserver",
"system/supervisor",
Expand Down
8 changes: 5 additions & 3 deletions images/system.toml
Original file line number Diff line number Diff line change
Expand Up @@ -70,18 +70,20 @@ receives = ["power"]
# shell. `prompt` is the layer above every window, which `filepicker` alone
# reaches: serving it makes the sessions its launches open the screen's, whose
# packages may ask the person at it for a folder. `clipboard` is sent every copy
# made in the session, and `vdagent` alone receives it.
# made in the session, and `vdagent` alone receives it. `volume` is
# soundserver's master level and mute, which the volume keys set and no other
# row receives.
[programs.compositor]
service = true
serves = ["compositor", "prompt", "clipboard"]
receives = ["soundserver", "filepicker"]
receives = ["soundserver", "filepicker", "volume"]
devices = ["framebuffer", "keyboard", "mouse"]
starts = ["terminal", "files", "/apps"]
login = true

[programs.soundserver]
service = true
serves = ["soundserver"]
serves = ["soundserver", "volume"]
devices = ["hda-audio", "pci:1af4:1059"]
syscap = ["rt"]

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
---
status: open
kind: defect
opened: 2026-10-12
---

# HDA plays only at 44100 Hz, and the AMD bench's speaker converter offers 48000 Hz alone

`lib/hda/src/config.rs` asks every converter for one rate, `RATE = 44_100`. soundserver refuses a converter that does not offer it (`Refusal::Rate` in `system/soundserver/src/hda.rs`) and presents the null sink.

**The AMD bench's speaker path** is pin 0x14 straight to converter 0x02:

- the converter's Format Override is set, so its own PCM word is the one that holds: `0x000e0040`, 48 kHz alone, at 16, 20 and 24 bits;
- its function group's word, `0x000e0560`, offers 44.1 kHz, but that word does not apply to a converter that overrides the format;
- the T14's converter reads `0x000e0060`: 44.1 and 48 kHz.

That was read off a boot of the bench's demo build. On `main` the bench never reaches its converter, because `issues/the-amd-bench-answers-on-two-hda-controllers-and-plays-on-the-null-sink.md` refuses both controllers first.

**The owner's ruling (2026-10-12):** native rate per chip. 44.1 kHz wherever the converter offers it, so the T14 is unchanged; a converter offering only 48 kHz runs at 48 kHz with high-quality resampling.

**Two more things stand on 44.1 kHz:**

- **cpal.** cpal's ToyOS backend advertises 44.1 kHz alone and asserts that soundserver's device rate equals it (`issues/cpal-backend-hardcodes-the-format.md`). A device at 48 kHz therefore aborts every cpal client. virtio-sound already runs at 48 kHz where a device offers no 44.1 kHz.
- **The resampler.** soundserver's resampler carries a client whose rate is not the device's, and no test holds its quality. Measured on the host against the analytic sine, with soundserver's parameters, from every client rate in common use to both device rates (#877; [a5-sweep-avx.log](https://github.com/ToyOSOrg/evidence/blob/0e375868c74ce04882bed06e2b188f3271b6e9dd/a5-sweep-avx.log)):
- in-band THD+N is at least 133.6 dB below a −1 dBFS tone;
- the gain at the passband's edge is −0.31 dB wherever the client's rate is below the device's, and down to −3.63 dB where it is above (192 to 44.1 kHz);
- a tone just above 0.5465 of 44.1 kHz, resampled down from 192 kHz, is rejected by only 21.4 dB.

Owner: soundserver's HDA driver (`system/soundserver/src/hda.rs`) and `lib/hda/src/config.rs`. The design is #877's A5.

## Exit condition

All of:

- an AMD bench boot reads soundserver on HDA at 48000 Hz, with its audio rows green;
- a T14 boot reads 44100 Hz, with its audio rows green;
- a cpal client takes the rate soundserver answers;
- a host test holds soundserver's resampler, on every interpolator rubato can take on a CPU ToyOS runs on and from every client rate in common use to both device rates, to a passband, a stop-band and an in-band THD+N bound, each cited from published practice, under the margin below the dithered 16-bit wire's own noise that the owner signed (#877's A5).
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
---
status: open
kind: defect
opened: 2026-10-13
---

# The HDA stream-format test takes its oracle from another implementation's source

`lib/hda/src/stream.rs`'s `every_rate_linux_tabulates_encodes_to_the_number_linux_tabulates`
holds the rate half of `SDnFMT` to a table copied from Linux's
`sound/hda/hdac_device.c`, and its doc names the file. Root `CLAUDE.md` (#902)
reads another implementation's source only clean-room, and `reviewer.md`'s
**Fit** refuses a test whose oracle is another implementation's source.

Found while #877 changed `lib/hda/src/config.rs` beside it; #877 does not touch
`stream.rs`.

Owner: `lib/hda`.

## Exit condition

The test's expected words are derived from the Intel High Definition Audio
specification's stream-format table (base, multiplier and divisor bits), cited
at the site, and no source, test or comment under `lib/hda` names Linux.
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
---
status: open
kind: defect
opened: 2026-10-12
---

# The AMD bench's keyboard acks the scancode-set query and never answers it, so the kernel disables it

The AMD bench's i8042 probe reaches the keyboard and stops at the set query. A boot of the bench's demo build logged the step that went silent:

- firmware's config byte reads `0x77`, translate on;
- `0xF5` is acked;
- `0xF0` is acked, then `0x00` is acked;
- no set byte arrives in the 750 ms left of the keyboard stage, and no late byte waits in the output buffer after it.

`main` reads that as `SetQuery::Silent` and disables the keyboard: `i8042: kbd DISABLED - the 0xF0 0x00 set query did not complete` (`kernel/src/arch/x86_64/i8042/mod.rs`). The bench has no other keyboard.

What the same boots measured, which a fix has to keep:

- **`0xF4` shares the stage the wait spends.** Read from the code, its ack is then read against the spent deadline, and it fails. With a deadline of its own, the demo build's `0xF4` was acked and the keyboard resumed scanning.
- **The arming write was not starved.** It ran about 1.35 s into the probe's 2.1 s budget.
- **The wire is set 1.** Under the demo build's assumption, set 2 behind the controller's translation, the bench's volume keys arrived as `E0 30` and `E0 2E`. That assumption is the one the `Refused` arm makes when translate is on, and those bytes are set 1's codes for Volume Up and Down.
- **The wait for the set byte costs 750 ms of every boot.**

Owner: the i8042 driver, `kernel/src/arch/x86_64/i8042/`. The design is #877's A2.

## Exit condition

All of:

- an AMD bench boot reads `i8042: kbd set2+xlat (assumed, …) scanning on, GSI 1 … on`;
- a T14 boot reads its `Refused` arm as `main` does, and QEMU reads `Told(0x41)` as `main` does;
- the set byte's wait is bounded by a device response time cited at its site from a hardware document, or, where no document states one, the 750 ms stays recorded in an open issue.
22 changes: 13 additions & 9 deletions kernel/ps2/src/key.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
//! Scancode set 1 → HID usage.
//!
//! Set 1 is what arrives when the keyboard is in set 2 and the controller's
//! translation bit is on — Linux's default configuration, and therefore the
//! best-trodden path on an EC we cannot debug. The translation table lives in
//! translation bit is on, as the i8042 driver sets it. The translation table lives in
//! the controller, so it costs this crate nothing; what is left is three
//! states (base, `0xE0`, `0xE1`) and two 128-byte tables.

Expand Down Expand Up @@ -44,6 +43,10 @@ pub const SET1: [u8; 128] = [

/// `scancode & 0x7F` → HID usage, for the byte after an `0xE0` prefix.
///
/// Mute, Volume Down and Volume Up are Microsoft's Keyboard Scan Code
/// Specification's set-1 codes, given the Keyboard/Keypad page's usages for
/// the same keys, since a key transition carries that page alone.
///
/// `0x2A` and `0x36` are deliberately unmapped: under translation PrtScn is
/// `E0 2A E0 37` make / `E0 B7 E0 AA` break, and mapping them would emit a
/// phantom Shift press and release around every PrtScn.
Expand All @@ -54,11 +57,12 @@ pub const SET1_E0: [u8; 128] = [
X, X, X, X, X, X, X, X,
// 0x18 0x19 0x1A 0x1B KPEnt RCtrl 0x1E 0x1F
X, X, X, X, 0x58, 0xE4, X, X,
X, X, X, X, X, X, X, X,
// 0x28 0x29 fake 0x2B 0x2C 0x2D 0x2E 0x2F
X, X, X, X, X, X, X, X,
// 0x30 0x31 0x32 0x33 0x34 KP/ fake PrtSc
X, X, X, X, X, 0x54, X, 0x46,
// Mute 0x21 0x22 0x23 0x24 0x25 0x26 0x27
0x7F, X, X, X, X, X, X, X,
// 0x28 0x29 fake 0x2B 0x2C 0x2D VolDn 0x2F
X, X, X, X, X, X, 0x81, X,
// VolUp 0x31 0x32 0x33 0x34 KP/ fake PrtSc
0x80, X, X, X, X, 0x54, X, 0x46,
// RAlt 0x39 0x3A 0x3B 0x3C 0x3D 0x3E 0x3F
0xE6, X, X, X, X, X, X, X,
// 0x40 0x41 0x42 0x43 0x44 0x45 0x46 Home
Expand Down Expand Up @@ -169,8 +173,8 @@ impl KeyDecoder {
// **So a keyboard that resets itself is undetectable on this
// wire, and that is a property of the mode rather than of this
// decoder.** The driver runs the keyboard in set 2 with the
// controller translating to set 1 — Linux's default and the
// best-trodden EC path — and `0xAA`, the BAT-complete byte a
// controller translating to set 1,
// and `0xAA`, the BAT-complete byte a
// self-reset sends, is bit-identical to `0x2A | 0x80`. The
// T14's EC does reset the keyboard after suspend and after a
// lid event, so this is reached on real hardware rather than in
Expand Down
11 changes: 7 additions & 4 deletions kernel/ps2/tests/decode.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,11 @@
use toyos_ps2::key::{SET1, SET1_E0};
use toyos_ps2::{KeyDecoder, KeyOutcome, MouseDecoder, MouseOutcome};

/// Every HID keyboard usage the layouts and the compositor can name. Outside
/// these two ranges a usage is not a keyboard usage at all.
/// Every HID keyboard usage the layouts and the compositor can name: the
/// keys, Mute, Volume Up and Volume Down, and the modifiers. Outside these
/// ranges a usage is not one this decoder may emit.
fn is_hid_keyboard_usage(usage: u8) -> bool {
(0x04..=0x65).contains(&usage) || (0xE0..=0xE7).contains(&usage)
(0x04..=0x65).contains(&usage) || (0x7F..=0x81).contains(&usage) || (0xE0..=0xE7).contains(&usage)
}

fn press(decoder: &mut KeyDecoder, bytes: &[u8]) -> Vec<(u8, bool)> {
Expand Down Expand Up @@ -52,10 +53,12 @@ fn every_usage_the_rest_of_the_tree_names_is_reachable() {
reachable[usage as usize] = true;
}
// Letters, digits and punctuation (layout_lookup's 0x04..=0x38), the ISO
// key, the escape-sequence keys, and every modifier.
// key, the escape-sequence keys, Mute and the two volume keys the
// compositor keeps, and every modifier.
let required: Vec<u8> = (0x04u8..=0x38)
.chain([0x64])
.chain(0x4Au8..=0x52)
.chain(0x7Fu8..=0x81)
.chain(0xE0u8..=0xE7)
.collect();
for usage in required {
Expand Down
42 changes: 34 additions & 8 deletions kernel/src/arch/x86_64/i8042/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ use crate::arch::idt::I8042_VECTOR;
use crate::irq_ring::IrqSource;
use crate::log;
use crate::sync::Lock;
use crate::time::{Budget, Cadence, Duration};
use crate::time::{Bound, Budget, Cadence, Duration};
use super::ioapic::{self, Gsi};

mod tally;
Expand Down Expand Up @@ -746,6 +746,15 @@ const KEYBOARD: Budget = Budget::of(
Duration::from_millis(750),
"the keyboard stage is named as the one that ran out",
);
/// The set byte's wait, from the kernel's read of the second ack: taken out of
/// [`KEYBOARD`], so `0xF4` keeps the stage's remainder.
const SET_BYTE: Bound = Bound::from_spec(
Duration::from_micros(21_125),
"IBM PS/2 Hardware Interface Technical Reference (October 1990): the keyboard responds \
within 20 ms (Keyboards (101- and 102-Key), pp. 21 and 39), and an 11-bit frame (p. 38) \
takes at most 1.125 ms, its first clock within 25 us and each bit within 100 us \
(Keyboard/Auxiliary Device Controller, p. 14, Figure 6, T1, T3 and T4)",
);
/// The aux port's `0xFF` is a device reset, answered with a real self-test:
/// this stage must not be shortened to fix an arithmetic error elsewhere.
const AUX_RESET: Budget = Budget::of(
Expand Down Expand Up @@ -885,14 +894,16 @@ enum SetQuery {
Told(u8),
/// A non-ack byte came back; the device does not implement the exchange.
Refused(u8),
/// Nothing came back, or the controller never took the write.
/// Both bytes were acked and no set byte came within [`SET_BYTE`]: the
/// device answered on this wire and does not implement the read-back.
Unanswered,
/// The exchange stopped before both acks: the controller never took a
/// write, or an ack never came.
Silent,
}

/// Ask which scancode set the keyboard is in. Read, never write: nothing
/// else in the machine's life sends the matching `0xF0 0x02` either (Linux's
/// `atkbd_select_set`, EDK2's `Ps2KeyboardDxe`), and a write cannot improve
/// on a read that already answers.
/// Ask which scancode set the keyboard is in. Read, never write: a write
/// cannot improve on a read that already answers.
fn query_scancode_set(deadline: u64) -> SetQuery {
if !write_data(0xF0, deadline) {
return SetQuery::Silent;
Expand All @@ -911,9 +922,9 @@ fn query_scancode_set(deadline: u64) -> SetQuery {
Some(other) => return SetQuery::Refused(other),
None => return SetQuery::Silent,
}
match read_data(deadline) {
match read_data((crate::clock::nanos_since_boot() + SET_BYTE.nanos()).min(deadline)) {
Some(set) => SetQuery::Told(set),
None => SetQuery::Silent,
None => SetQuery::Unanswered,
}
}

Expand Down Expand Up @@ -1114,6 +1125,21 @@ pub fn init(rsdp_addr: u64) {
);
return;
}
SetQuery::Unanswered if before & CFG_TRANSLATE != 0 => {
log!(
"i8042: kbd acked both bytes of the set query and sent no set byte within {}us; firmware's own cfg {:#04x} has translate on, so the wire is set 1",
SET_BYTE.nanos() / 1_000,
before
);
("set2+xlat", "assumed, the set query went unanswered")
}
SetQuery::Unanswered => {
log!(
"i8042: kbd DISABLED - the set query went unanswered and firmware's cfg {:#04x} has translate off, so nothing says what the wire carries",
before
);
return;
}
SetQuery::Silent => {
log!("i8042: kbd DISABLED - the 0xF0 0x00 set query did not complete");
return;
Expand Down
2 changes: 1 addition & 1 deletion kernel/src/device.rs
Original file line number Diff line number Diff line change
Expand Up @@ -238,7 +238,7 @@ pub fn try_claim(class: DeviceType, selector: [u64; 2]) -> Result<Arc<DeviceClai
Ok(DeviceClaim::new(class, DeviceInfo::Acpi(info), claim))
}
DeviceType::HdaAudio => {
let (info, pcm) = crate::drivers::hda::info().ok_or(ClaimError::Absent)?;
let (info, pcm) = crate::drivers::hda::describe()?;
let claim = Claim::acquire(class)?;
Ok(DeviceClaim::new(class, DeviceInfo::Hda(info, shm(pcm)), claim))
}
Expand Down
Loading