diff --git a/Cargo.lock b/Cargo.lock index a66297b3989..304e508f9d5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -652,7 +652,7 @@ dependencies = [ [[package]] name = "cpal" version = "0.18.0" -source = "git+https://github.com/ToyOSOrg/cpal?branch=toyos-0.18.0#f46381d27cad65aa5ef5ece5cec1227aa31ef269" +source = "git+https://github.com/ToyOSOrg/cpal?branch=toyos-0.18.0#60031a060b34460e79854ba8d441a0185b50c26a" dependencies = [ "alsa", "block2 0.6.2", @@ -3690,6 +3690,7 @@ dependencies = [ "toyos-inspect", "toyos-mixer", "toyos-pci-claim", + "toyos-resample", "toyos-virtio", "toyos-virtio-sound", ] @@ -4222,6 +4223,9 @@ dependencies = [ [[package]] name = "toyos-hda" version = "0.1.0" +dependencies = [ + "toyos-mixer", +] [[package]] name = "toyos-i219" @@ -4411,6 +4415,14 @@ version = "0.1.0" name = "toyos-random" version = "0.1.0" +[[package]] +name = "toyos-resample" +version = "0.1.0" +dependencies = [ + "rubato", + "toyos-mixer", +] + [[package]] name = "toyos-root" version = "0.1.0" @@ -4561,6 +4573,7 @@ name = "toyos-virtio-sound" version = "0.1.0" dependencies = [ "toyos-device-memory", + "toyos-mixer", "toyos-virtio", ] diff --git a/Cargo.toml b/Cargo.toml index d1205df4082..c7de5b2fd02 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -118,6 +118,7 @@ members = [ "system/shell", "system/soundserver", "system/soundserver/mixer", + "system/soundserver/resample", "system/soundserver/virtio-sound", "system/sshserver", "system/supervisor", diff --git a/images/system.toml b/images/system.toml index a4d35efc5aa..d94bb85137a 100644 --- a/images/system.toml +++ b/images/system.toml @@ -71,11 +71,13 @@ receives = ["power"] # 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. `input` is where a source -# of keys and pointer reports reaches the owner of input, which it is. +# of keys and pointer reports reaches the owner of input, which it is. `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", "input"] -receives = ["soundserver", "filepicker"] +receives = ["soundserver", "filepicker", "volume"] devices = ["framebuffer", "keyboard", "mouse"] starts = ["terminal", "files", "/apps"] login = true @@ -92,7 +94,7 @@ devices = ["isa:0060,0064:1,12"] [programs.soundserver] service = true -serves = ["soundserver"] +serves = ["soundserver", "volume"] devices = ["hda-audio", "pci:1af4:1059"] syscap = ["rt"] diff --git a/issues/a-metal-row-names-no-hardware-and-a-bench-without-it-reds.md b/issues/a-metal-row-names-no-hardware-and-a-bench-without-it-reds.md index e96841b3cdf..392be5c1a0f 100644 --- a/issues/a-metal-row-names-no-hardware-and-a-bench-without-it-reds.md +++ b/issues/a-metal-row-names-no-hardware-and-a-bench-without-it-reds.md @@ -18,9 +18,6 @@ that most rows ride: NotFound`: the job claims the T14's I219, which that bench has not. The `claim_reuses_its_remapping_entry` and `claim_refused_without_remapping` rows read that job. -- `test_rs_hda_client_stall` exited 101 on the null sink, which the - `hda_client_stall` row reads: - `issues/the-amd-bench-answers-on-two-hda-controllers-and-plays-on-the-null-sink.md`. That run asked for `blackbox_unclaimed_page` alone, which passed. An unfiltered `cargo test --test toyos-build -- --metal --metal-host diff --git a/issues/cpal-backend-hardcodes-the-format.md b/issues/cpal-backend-hardcodes-the-format.md index 4bd1ae0e42e..f386d6a259d 100644 --- a/issues/cpal-backend-hardcodes-the-format.md +++ b/issues/cpal-backend-hardcodes-the-format.md @@ -4,25 +4,14 @@ kind: defect opened: 2026-08-01 --- -# The cpal ToyOS backend hardcodes 44100/2ch/i16 and rejects everything else +# The cpal ToyOS backend offers I16 alone -soundd's resampler and channel-conversion paths are unreachable from any real -client and therefore effectively untested. The backend also `assert_eq!`s the -device rate against a compile-time constant, so changing the driver's rate -aborts every cpal app. - -**`Drop`'s bound on soundd is built from copies the same way.** A dropped -`Stream` waits for soundd to close its signal pipe for at most soundd's 5 ms -fade, in whole periods, plus the 8 periods of the stream's ring: 1280 frames at -44100 Hz (`RELEASE_WITHIN`). Past it, `Drop` refuses by name and returns. The -fade (`toyos_mixer::ramp_frames`) and the ring depth (soundd's `slot_count`, see -`issues/client-ring-depth-is-the-devices-pipeline-depth.md`) are copies: -`StreamOpenResponse.slot_count` reaches `AudioStream::open` and stays private, -and no message carries the fade. If soundd's fade grows or a device's pipeline -deepens, the bound shrinks under what soundd needs, and a healthy soundd is -refused. That part goes when `AudioStream` says the ring depth and the fade and -the host derives `RELEASE_WITHIN` from them, which changes `sdk/toyos/src` and the -audio protocol. +cpal's ToyOS backend (`toyos-0.18.0`) takes its rate and its release bound from +soundserver: `default_output_config` is the described device's, +`supported_output_configs` is mono and stereo over the client range soundserver +accepts, and `RELEASE_WITHIN` is derived from the fade, the ring and the device +period (#877). Its sample format is still I16 alone: a client asking for any +other format is refused. Deferred to the quiet-tree window, not neglected: editing that fork needs `.cargo/config.toml` path overrides, which redirect cpal for **every** agent in diff --git a/issues/hda-has-no-jack-detection-volume-or-keys.md b/issues/hda-has-no-jack-detection-and-nothing-persists.md similarity index 54% rename from issues/hda-has-no-jack-detection-volume-or-keys.md rename to issues/hda-has-no-jack-detection-and-nothing-persists.md index be6189dd347..86f714474e1 100644 --- a/issues/hda-has-no-jack-detection-volume-or-keys.md +++ b/issues/hda-has-no-jack-detection-and-nothing-persists.md @@ -4,28 +4,20 @@ kind: track opened: 2026-08-03 --- -# HDA plays, but there is no jack detection, no volume control and no volume keys +# HDA plays, but there is no jack detection and nothing persists -The driver is built and audible on both QEMU and the T14: codec decoding and -output-path selection are a pure crate, the register allowlist is in the kernel -with soundd holding no physical address, and three guest gates stand on it. What -was staged after that never started, and it is about 850 lines of greenfield -blocked on nothing: +The driver is soundserver's: codec decoding, output-path selection, the +controller choice and the master are a pure crate, and the kernel lists and +binds controllers. The master volume and mute are the codec's, served on +soundserver's `volume` port, and the volume keys set them from the compositor +(#877). What remains: - **Jack detection and routing.** `GET_PIN_SENSE` is spelled - (`lib/hda/src/verb.rs:107`) and nothing issues it — that constant is its - only occurrence in the tree. Still wanted: a poll on the presence bit, a - switch between the fixed speaker and the headphone pin, and a ramp across the - switch so it is not a click. -- **Master volume and mute**, at the codec rather than in the mixer, and the - message to set it. Path setup already emits `SET_AMP_GAIN_MUTE` once per pin - and once per converter (`lib/hda/src/config.rs:89`, `:94`, `:109-117`), at - the amp's own `zero_db`; what is missing is a level anyone can change after - that, not the verb. -- **Volume keys.** The premise was never established either — the diagnostic - that would say what the T14's keyboard sends for them was never committed. -- **Persistence** of the chosen output and level, blocked on the kernel keeping - anything it enumerates (`issues/the-kernel-keeps-nothing-it-enumerates.md`). + (`lib/hda/src/verb.rs`) and nothing issues it. Still wanted: a poll on the + presence bit, a switch between the fixed speaker and the headphone pin, and a + ramp across the switch so it is not a click. +- **Persistence** of the chosen output and the master level, blocked on the + kernel keeping anything it enumerates (`issues/the-kernel-keeps-nothing-it-enumerates.md`). **Two pieces of the shipped work are still owed a verdict.** The four HDA sections of the audio baseline were never recorded, so the HDA arm asserts harm diff --git a/issues/hda-stream-format-test-takes-its-oracle-from-another-implementations-source.md b/issues/hda-stream-format-test-takes-its-oracle-from-another-implementations-source.md new file mode 100644 index 00000000000..500363c57bb --- /dev/null +++ b/issues/hda-stream-format-test-takes-its-oracle-from-another-implementations-source.md @@ -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. diff --git a/issues/the-amd-bench-answers-on-two-hda-controllers-and-plays-on-the-null-sink.md b/issues/the-amd-bench-answers-on-two-hda-controllers-and-plays-on-the-null-sink.md deleted file mode 100644 index 4bdb1802f13..00000000000 --- a/issues/the-amd-bench-answers-on-two-hda-controllers-and-plays-on-the-null-sink.md +++ /dev/null @@ -1,37 +0,0 @@ ---- -status: open -kind: defect -opened: 2026-10-11 ---- - -# The AMD bench answers on two HDA controllers, so the kernel refuses both and soundserver plays on the null sink - -On the AMD bench's boot at `fb03703a1` (PR #852, the log in -issuecomment-6107031099 and issuecomment-6107031259) two HDA controllers have -a live link, `03:00.1` (`1002:1637`) and `03:00.6` (`1022:15e3`), each -`statests=0x0001`. `kernel/src/drivers/hda.rs` refuses a machine with more -than one by name: `hda: 2 controllers answer on this machine and choosing -between them means walking their codec graphs, which is the driver's — refused -by name, no HDA audio`. The supervisor then finds no `hda-audio` for -soundserver, which presents the null sink. - -So the bench has no audio, and `test_rs_hda_client_stall`, which the -`hda_client_stall` row judges, exits 101 there: its `await_suspended` reads -the stream's state as `idle` for 5 s after its only client closed, and the -job's premise is a device stream that suspends. `test_rs_audio_tone`, -`test_rs_audio_idle_suspend` and `test_rs_null_sink_client_exits` exited 0 on -the same boot, on the null sink. - -Which of the two carries the laptop's speakers is unread; by its vendor -`1002:1637` is the GPU's display audio. - -Owner: the HDA stage of `issues/every-driver-is-still-in-the-kernel.md`, since -the refusal names choosing as the driver's work and that stage moves the -driver to soundserver. That a row needing an HDA controller runs on a bench -without one usable is `issues/a-metal-row-names-no-hardware-and-a-bench-without-it-reds.md`'s. - -## Exit condition - -The driver walks both controllers' codec graphs and plays on the one whose -graph reaches an output pin, and an AMD bench boot reads soundserver on HDA -and `test_rs_hda_client_stall` exiting 0. diff --git a/issues/the-kernel-keeps-nothing-it-enumerates.md b/issues/the-kernel-keeps-nothing-it-enumerates.md index fb3b496e90f..30105d2768d 100644 --- a/issues/the-kernel-keeps-nothing-it-enumerates.md +++ b/issues/the-kernel-keeps-nothing-it-enumerates.md @@ -37,7 +37,7 @@ On top of retention, in dependency order: is taken by an HTTP GET demo that should be renamed `http` first, which is the owner's call. 6. **Master gain, and `audio volume`** — see - `issues/hda-has-no-jack-detection-volume-or-keys.md`, which needs the + `issues/hda-has-no-jack-detection-and-nothing-persists.md`, which needs the same thing. 7. **Adopting a disk from inside the guest**, so designation stops being a host-side whole-device stamp minted by the build system. Blocked on the diff --git a/kernel/src/device.rs b/kernel/src/device.rs index 0000fe6a09b..e3fbd00a38b 100644 --- a/kernel/src/device.rs +++ b/kernel/src/device.rs @@ -228,7 +228,7 @@ pub fn try_claim(class: DeviceType, selector: [u64; 2]) -> Result { - 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)) } diff --git a/kernel/src/drivers/hda.rs b/kernel/src/drivers/hda.rs index a7977f22067..4b97e8c9569 100644 --- a/kernel/src/drivers/hda.rs +++ b/kernel/src/drivers/hda.rs @@ -3,18 +3,22 @@ //! The kernel touches only registers whose value is an address, indexes a //! kernel structure, or acknowledges the interrupt; everything else a driver //! reaches goes through [`reg_write`] and [`reg_read`], gated by a positive -//! list and refused by name. Codec and pin decisions belong to soundserver, never -//! to this file. +//! list and refused by name. Which controller carries the stream, and every +//! codec and pin decision, belong to soundserver, never to this file: [`init`] +//! lists every controller it can bind and [`bind`] binds the one it is told. use core::cell::UnsafeCell; use core::sync::atomic::{AtomicBool, AtomicU32, AtomicU64, AtomicUsize, Ordering}; use toyos_abi::audio::AudioCompletionRecord; -use toyos_abi::hda::HdaInfo; +use alloc::vec::Vec; + +use toyos_abi::hda::{HdaController, HdaInfo, Reg, MAX_CONTROLLERS}; use toyos_abi::syscall::{RegWidth, SyscallError}; use toyos_hda::stream; use super::pci::PciDevice; +use crate::device::ClaimError; use crate::log; use crate::mm::policy::{CachePolicy, MmioPolicy}; use crate::mm::Mmio; @@ -117,8 +121,8 @@ struct StreamIsr { named_error: AtomicBool, } -// SAFETY: `stream` is written once at init before the vector is armed and is -// read-only afterwards; every other field is atomic. +// SAFETY: `stream` is written once, by the bind that left `Listed`, before its controller can +// raise the vector, and is read-only afterwards; every other field is atomic. unsafe impl Sync for StreamIsr {} static ISR: StreamIsr = StreamIsr { @@ -131,7 +135,7 @@ static ISR: StreamIsr = StreamIsr { }; fn isr_stream() -> Option { - // SAFETY: sole writer is `init`, before the vector is armed. + // SAFETY: sole writer is `bind_listed`, before any controller can raise the vector. unsafe { *ISR.stream.get() } } @@ -205,34 +209,115 @@ pub fn drain_completed(buf: &mut crate::user_ptr::UserBytesMut) -> usize { } -struct HdaController { +/// One controller [`init`] can bind: out of reset with its link up, its message armed at the HDA +/// vector, and bus mastering off and `INTCTL` clear, so it raises nothing. +struct Listed { + pci: PciDevice, regs: Mmio, + /// Its output stream descriptor's own window. stream: Mmio, - /// Kept so the pages outlive the mappings that name them, and so a refused controller gives - /// them back. - _bdl: super::DmaPool, - _pcm: super::DmaPool, + stream_index: u8, + stream_offset: u32, + statests: u16, + message: Message, } -static CONTROLLER: Lock> = Lock::new(None); -static INFO: Lock> = Lock::new(None); -static REFUSALS: AtomicUsize = AtomicUsize::new(0); +/// Which of the function's two message capabilities [`arm_interrupt`] armed, so the one that is +/// turned off is the one that is on. +#[derive(Clone, Copy)] +enum Message { + MsiX, + Msi, +} + +/// The stream's memory, taken once at boot and kept for it: its shape is the driver's constants, +/// the same for every controller, and every claim's PCM object maps the PCM pool. +#[derive(Clone)] +struct StreamMemory { + space: crate::iommu::DeviceSpace, + /// The descriptor list's device address, already written. + bdl: u64, + pcm: Region, +} -pub fn info() -> Option<(HdaInfo, Region)> { - INFO.lock().clone() +/// What every call on the HDA claim is decided by. One-shot: `Listed` → `Binding` → `Bound` or +/// `Refused`, and nothing returns to `Listed`. +enum State { + /// Every controller the kernel can bind, in PCI order. The register calls reach `ICS`, `ICW` + /// and `IRR` on any of them. + Listed(Vec), + /// One bind owns every listed controller; every other call is refused, and nothing waits. + Binding, + /// The stream runs on this controller, which the claim lists alone, at index 0. + Bound(Listed), + /// The bind failed on the device; the log line names how. + Refused, } +struct Hda { + state: State, + memory: StreamMemory, +} + +/// No device wait is made under this lock: [`bind`] moves the list out of it and runs its waits +/// with nothing held. +static CONTROLLER: Lock> = Lock::new(None); +static REFUSALS: AtomicUsize = AtomicUsize::new(0); + +/// What an `hda-audio` claim describes: the stream's shape and the controllers it lists, read +/// from the one state every call is decided by. +pub fn describe() -> Result<(HdaInfo, Region), ClaimError> { + let guard = CONTROLLER.lock(); + let hda = guard.as_ref().ok_or(ClaimError::Absent)?; + let listed = match &hda.state { + State::Listed(list) => list.as_slice(), + State::Bound(controller) => core::slice::from_ref(controller), + // The binder's frame holds the claim until the bind returns. + State::Binding => return Err(ClaimError::Owned), + State::Refused => return Err(ClaimError::Unusable), + }; + let mut controllers = [HdaController { + stream_offset: 0, + statests: 0, + bus: 0, + dev: 0, + func: 0, + _pad: [0; 3], + }; MAX_CONTROLLERS]; + for (slot, controller) in controllers.iter_mut().zip(listed) { + *slot = HdaController { + stream_offset: controller.stream_offset, + statests: controller.statests, + bus: controller.pci.bus, + dev: controller.pci.dev, + func: controller.pci.func, + _pad: [0; 3], + }; + } + let info = HdaInfo { + pcm: toyos_abi::HANDLE_INVALID, + period_bytes: PERIOD_BYTES as u32, + stream_tag: STREAM_TAG, + periods: PERIODS as u8, + count: listed.len() as u8, + _pad: 0, + controllers, + }; + Ok((info, hda.memory.pcm.clone())) +} /// A register is on the list only if its value is not an address and indexes nothing the kernel -/// allocated; `SDnCBL` and `SDnLVI` are absent because the kernel writes them itself. -fn write_permit(stream_offset: u64, offset: u64, width: RegWidth) -> Result<&'static str, ()> { - let sd = |field: u64| stream_offset + field; +/// allocated; `SDnCBL` and `SDnLVI` are absent because the kernel writes them itself. `stream` is +/// the bound controller's descriptor offset, and `None` before a bind: an unbound controller has +/// no stream for a driver to touch. +fn write_permit(stream: Option, offset: u64, width: RegWidth) -> Result<&'static str, ()> { + let sd = |field: u64| stream.map(|at| at + field); match (offset, width) { (IMMEDIATE_COMMAND, RegWidth::U32) => Ok("ICW: a codec verb, which names no memory"), (IMMEDIATE_STATUS, RegWidth::U16) => Ok("ICS: the immediate-command busy and valid bits"), - (o, RegWidth::U8) if o == sd(SD_CTL) => Ok("SDnCTL: run and interrupt enables"), - (o, RegWidth::U8) if o == sd(SD_CTL_TAG) => Ok("SDnCTL: the stream tag"), - (o, RegWidth::U16) if o == sd(SD_FMT) => Ok("SDnFMT: the format word"), + (o, RegWidth::U8) if Some(o) == sd(SD_CTL) => Ok("SDnCTL: run and interrupt enables"), + (o, RegWidth::U8) if Some(o) == sd(SD_CTL_TAG) => Ok("SDnCTL: the stream tag"), + (o, RegWidth::U16) if Some(o) == sd(SD_FMT) => Ok("SDnFMT: the format word"), _ => Err(()), } } @@ -247,51 +332,72 @@ fn read_permit(offset: u64, width: RegWidth) -> Result<&'static str, ()> { } } -fn refuse(what: &str, offset: u64, width: RegWidth) -> SyscallError { +fn refuse(what: &str, reg: Reg, width: RegWidth) -> SyscallError { if REFUSALS.fetch_add(1, Ordering::Relaxed) < MAX_NAMED_REFUSALS { - log!("hda: refused a {width:?} {what} of {offset:#x} — not on the allow-list"); + log!( + "hda: refused a {width:?} {what} of {:#x} on controller {} — not on the allow-list", + reg.offset, + reg.controller + ); } SyscallError::PermissionDenied } -pub fn reg_read(offset: u64, width: RegWidth) -> Result { - if read_permit(offset, width).is_err() { - return Err(refuse("read", offset, width)); - } +/// `f` over the controller `reg` names in the claim's current list, and whether the stream is +/// bound to it, for one register access under [`CONTROLLER`]. +fn on_controller( + reg: Reg, + f: impl FnOnce(&Listed, bool) -> Result, +) -> Result { let guard = CONTROLLER.lock(); - let controller = guard.as_ref().ok_or(SyscallError::NotFound)?; - Ok(match width { - RegWidth::U8 => controller.regs.read_u8(offset) as u32, - RegWidth::U16 => controller.regs.read_u16(offset) as u32, - RegWidth::U32 => controller.regs.read_u32(offset), + let hda = guard.as_ref().ok_or(SyscallError::NotFound)?; + match &hda.state { + State::Listed(list) => f(list.get(reg.controller as usize).ok_or(SyscallError::InvalidArgument)?, false), + State::Bound(controller) if reg.controller == 0 => f(controller, true), + State::Bound(_) => Err(SyscallError::InvalidArgument), + State::Binding => Err(SyscallError::WouldBlock), + State::Refused => Err(SyscallError::Io), + } +} + +pub fn reg_read(reg: Reg, width: RegWidth) -> Result { + if read_permit(reg.offset as u64, width).is_err() { + return Err(refuse("read", reg, width)); + } + on_controller(reg, |controller, _| { + let offset = reg.offset as u64; + Ok(match width { + RegWidth::U8 => controller.regs.read_u8(offset) as u32, + RegWidth::U16 => controller.regs.read_u16(offset) as u32, + RegWidth::U32 => controller.regs.read_u32(offset), + }) }) } -pub fn reg_write(offset: u64, width: RegWidth, value: u32) -> Result<(), SyscallError> { - let stream_offset = info().ok_or(SyscallError::NotFound)?.0.stream_offset as u64; - if write_permit(stream_offset, offset, width).is_err() { - return Err(refuse("write", offset, width)); - } +pub fn reg_write(reg: Reg, width: RegWidth, value: u32) -> Result<(), SyscallError> { if value > width.max_value() { return Err(SyscallError::InvalidArgument); } - - let guard = CONTROLLER.lock(); - let controller = guard.as_ref().ok_or(SyscallError::NotFound)?; - - if offset == stream_offset + SD_CTL { - return start_stop(controller, value as u8); - } - match width { - RegWidth::U8 => controller.regs.write_u8(offset, value as u8), - RegWidth::U16 => controller.regs.write_u16(offset, value as u16), - RegWidth::U32 => controller.regs.write_u32(offset, value), - } - Ok(()) + on_controller(reg, |controller, bound| { + let offset = reg.offset as u64; + let stream = bound.then_some(controller.stream_offset as u64); + if write_permit(stream, offset, width).is_err() { + return Err(refuse("write", reg, width)); + } + if Some(offset) == stream.map(|at| at + SD_CTL) { + return start_stop(controller, value as u8); + } + match width { + RegWidth::U8 => controller.regs.write_u8(offset, value as u8), + RegWidth::U16 => controller.regs.write_u16(offset, value as u16), + RegWidth::U32 => controller.regs.write_u32(offset, value), + } + Ok(()) + }) } /// `SDnCTL`'s first byte, where the driver starts and stops the engine. -fn start_stop(controller: &HdaController, value: u8) -> Result<(), SyscallError> { +fn start_stop(controller: &Listed, value: u8) -> Result<(), SyscallError> { // Refused: stream reset clears the descriptor's address and length registers, which the // driver cannot write back, so `RUN` would point the DMA engine at physical address zero. if value & SD_CTL_SRST != 0 { @@ -314,11 +420,119 @@ fn start_stop(controller: &HdaController, value: u8) -> Result<(), SyscallError> Ok(()) } +/// Bind the stream to the listed controller at `index`, and put every other one back into reset. +/// +/// Takes no memory: the domain, the pools and the descriptor list are [`init`]'s, so nothing here +/// can be refused for want of it. The waits run with no lock held, on the caller's thread. +pub fn bind(index: u8) -> Result<(), SyscallError> { + let (list, memory) = { + let mut guard = CONTROLLER.lock(); + let hda = guard.as_mut().ok_or(SyscallError::NotFound)?; + match &hda.state { + State::Listed(list) if (index as usize) < list.len() => {} + State::Listed(_) => return Err(SyscallError::InvalidArgument), + State::Binding => return Err(SyscallError::WouldBlock), + State::Bound(_) if index == 0 => return Ok(()), + State::Bound(_) => return Err(SyscallError::InvalidArgument), + State::Refused => return Err(SyscallError::Io), + } + let State::Listed(list) = core::mem::replace(&mut hda.state, State::Binding) else { + unreachable!("matched as listed under the same guard") + }; + (list, hda.memory.clone()) + }; + let outcome = bind_listed(list, index as usize, &memory); + let bound = matches!(outcome, State::Bound(_)); + CONTROLLER.lock().as_mut().expect("a bind leaves the state it took").state = outcome; + if bound { Ok(()) } else { Err(SyscallError::Io) } +} + +/// The bind's body, over the list it took out of [`CONTROLLER`]; answers the state it leaves. +fn bind_listed(mut list: Vec, index: usize, memory: &StreamMemory) -> State { + let chosen = list.remove(index); + let pci = chosen.pci; + // Before `SD_BDPL` tells the device an address, and after both pools' mappings. + memory.space.attach(pci.bus, pci.dev, pci.func); + + if !reset_stream(chosen.stream) { + log!( + "hda: {:02x}:{:02x}.{} stream {} never left reset — refused, and every listed \ + controller goes back into reset", + pci.bus, + pci.dev, + pci.func, + chosen.stream_index + ); + silence(&chosen); + for other in &list { + silence(other); + } + return State::Refused; + } + + let stream = chosen.stream; + stream.write_u32(SD_BDPL, memory.bdl as u32); + stream.write_u32(SD_BDPU, (memory.bdl >> 32) as u32); + stream.write_u32( + SD_CBL, + stream::cyclic_length(PERIOD_BYTES as u32, PERIODS).expect("fits a u32"), + ); + stream.write_u16(SD_LVI, stream::last_valid_index(PERIODS).expect("a ring of eight") as u16); + stream.write_u8(SD_CTL_TAG, STREAM_TAG << 4); + stream.write_u8(SD_CTL, SD_CTL_IOCE | SD_CTL_FEIE | SD_CTL_DEIE); + stream.write_u8(SD_STS, SD_STS_WRITE_CLEAR); + + // SAFETY: the bind that left `Listed` is this field's one writer, and no state returns there; + // no listed controller raises the vector before the bus mastering and `INTCTL` below. Bus + // mastering is armed here, after the attach, and never at boot. + unsafe { + *ISR.stream.get() = Some(stream); + } + pci.enable_bus_master(); + chosen.regs.write_u32(INTCTL, INTCTL_GIE | (1 << chosen.stream_index)); -/// Bring up the one HDA controller this machine has a codec behind; more than one live link is a -/// refusal naming every controller found, never a first match. + for other in &list { + silence(other); + } + log!( + "hda: {:02x}:{:02x}.{} bound, statests={:#06x}, output stream {} at {:#x}, tag {STREAM_TAG}, \ + {PERIODS} periods of {PERIOD_BYTES} bytes; {} other listed controller(s) back into reset", + pci.bus, + pci.dev, + pci.func, + chosen.statests, + chosen.stream_index, + chosen.stream_offset, + list.len() + ); + State::Bound(chosen) +} + +/// Turn a listed controller's message off and put it back into reset. Nothing reaches it again +/// either way, so one that never reads back is named and left. +fn silence(controller: &Listed) { + let pci = controller.pci; + match controller.message { + Message::MsiX => pci.disable_msix(), + Message::Msi => pci.disable_msi(), + } + let regs = controller.regs; + regs.write_u32(GCTL, 0); + if !crate::clock::settles(SETTLE_NS, || regs.read_u32(GCTL) & GCTL_CRST == 0) { + log!( + "hda: {:02x}:{:02x}.{} never went back into reset (gctl={:#010x})", + pci.bus, + pci.dev, + pci.func, + regs.read_u32(GCTL) + ); + } +} + +/// List every controller this machine has a codec behind and take the stream's memory once; +/// bind none. Which one carries the audio is the driver's to choose, by walking their codecs. pub fn init(devices: &[PciDevice]) { - let controllers: alloc::vec::Vec<&PciDevice> = devices + let controllers: Vec<&PciDevice> = devices .iter() .filter(|d| d.matches_class(CLASS_MULTIMEDIA, SUBCLASS_HDA, None)) .collect(); @@ -326,70 +540,87 @@ pub fn init(devices: &[PciDevice]) { return; } - let mut live: alloc::vec::Vec<(&PciDevice, Mmio, u16, u16)> = alloc::vec::Vec::new(); + let mut live: Vec<(&PciDevice, Mmio, u16, u16)> = Vec::new(); for pci in &controllers { if let Some((regs, gcap, statests)) = probe(pci) { live.push((pci, regs, gcap, statests)); } } - - let (pci, regs, gcap, statests) = match live.len() { - 0 => { + if live.is_empty() { + log!( + "hda: {} class 0403 function(s) and no codec on any link — no HDA audio on this \ + machine", + controllers.len() + ); + return; + } + if live.len() > MAX_CONTROLLERS { + for (pci, _, _, statests) in &live { log!( - "hda: {} class 0403 function(s) and no codec on any link — no HDA audio on this \ - machine", - controllers.len() + "hda: {:02x}:{:02x}.{} has a live link (statests={statests:#06x})", + pci.bus, + pci.dev, + pci.func ); - return; } - 1 => live.remove(0), - n => { - for (pci, _, _, statests) in &live { - log!( - "hda: {:02x}:{:02x}.{} has a live link (statests={statests:#06x})", - pci.bus, - pci.dev, - pci.func - ); - } + log!( + "hda: {} controllers answer on this machine and a claim lists at most {MAX_CONTROLLERS} \ + — refused by name, no HDA audio", + live.len() + ); + return; + } + + let mut list: Vec = Vec::new(); + for (pci, regs, gcap, statests) in live { + let output_streams = (gcap >> 12) & 0xF; + let input_streams = (gcap >> 8) & 0xF; + if output_streams == 0 { log!( - "hda: {n} controllers answer on this machine and choosing between them means \ - walking their codec graphs, which is the driver's — refused by name, no HDA audio" + "hda: {:02x}:{:02x}.{} reports no output stream descriptor (gcap={gcap:#06x}) — \ + refused", + pci.bus, + pci.dev, + pci.func ); - return; + continue; } - }; - - let output_streams = (gcap >> 12) & 0xF; - let input_streams = (gcap >> 8) & 0xF; - if output_streams == 0 { + let Some(message) = arm_interrupt(pci) else { continue }; + // Its registers are the kernel's to serve from boot, bound or not. + crate::pcidev::note_kernel_driver(pci); + let stream_index = input_streams as u8; + let stream_offset = STREAM_BASE + stream_index as u64 * STREAM_STRIDE; log!( - "hda: {:02x}:{:02x}.{} reports no output stream descriptor (gcap={gcap:#06x}) — \ - refused", + "hda: {:02x}:{:02x}.{} listed as controller {}, statests={statests:#06x}, output \ + stream {stream_index} at {stream_offset:#x}", pci.bus, pci.dev, - pci.func + pci.func, + list.len() ); + list.push(Listed { + pci: *pci, + regs, + stream: regs.subregion(stream_offset, STREAM_STRIDE), + stream_index, + stream_offset: stream_offset as u32, + statests, + message, + }); + } + if list.is_empty() { return; } - let stream_index = input_streams as u8; - let stream_offset = STREAM_BASE + stream_index as u64 * STREAM_STRIDE; - let stream = regs.subregion(stream_offset, STREAM_STRIDE); - let space = crate::iommu::DeviceSpace::create(); - let bdl = super::DmaPool::alloc_in(PERIODS * 16, space); - let pcm = super::DmaPool::alloc_in(PERIODS * PERIOD_BYTES, space); - // Before `SD_BDPL` tells the device an address, and after both pools' mappings. - space.attach(pci.bus, pci.dev, pci.func); + let bdl = super::DmaPool::alloc_in(PERIODS * 16, space).leak(); + let pcm = super::DmaPool::alloc_in(PERIODS * PERIOD_BYTES, space).leak(); // Unaligned: the descriptor layout is the HDA specification's (§3.6.2), not a Rust one. - let bdl_view = bdl.view().unaligned(); - let pcm_view = pcm.view(); - // Nothing races these writes: the controller is not told this address until `SD_BDPL`, below. + let bdl_view = bdl.unaligned(); + // Nothing races these writes: no controller is told this address until a bind's `SD_BDPL`. bdl_view.zero(); - pcm_view.zero(); - - let entries = stream::build_bdl(pcm_view.device_addr(), PERIOD_BYTES as u32, PERIODS) + pcm.zero(); + let entries = stream::build_bdl(pcm.device_addr(), PERIOD_BYTES as u32, PERIODS) .expect("hda: the pipeline's own shape builds a descriptor list"); for (i, entry) in entries.iter().enumerate() { // `build_bdl` returns exactly `PERIODS` entries, matching the pool's `PERIODS * 16` bytes. @@ -397,65 +628,22 @@ pub fn init(devices: &[PciDevice]) { bdl_view.write::(i * 16 + 8, entry.length); bdl_view.write::(i * 16 + 12, u32::from(entry.interrupt_on_completion)); } - - if !reset_stream(stream) { - log!( - "hda: {:02x}:{:02x}.{} stream {stream_index} never left reset — refused", - pci.bus, - pci.dev, - pci.func - ); - return; - } - - stream.write_u32(SD_BDPL, bdl_view.device_addr() as u32); - stream.write_u32(SD_BDPU, (bdl_view.device_addr() >> 32) as u32); - stream.write_u32( - SD_CBL, - stream::cyclic_length(PERIOD_BYTES as u32, PERIODS).expect("fits a u32"), - ); - stream.write_u16(SD_LVI, stream::last_valid_index(PERIODS).expect("a ring of eight") as u16); - stream.write_u8(SD_CTL_TAG, STREAM_TAG << 4); - stream.write_u8(SD_CTL, SD_CTL_IOCE | SD_CTL_FEIE | SD_CTL_DEIE); - stream.write_u8(SD_STS, SD_STS_WRITE_CLEAR); - - // SAFETY: no vector is armed yet, so nothing can observe a half-written - // Option. - unsafe { - *ISR.stream.get() = Some(stream); - } - - pci.enable_bus_master(); - if !arm_interrupt(pci) { - return; - } - regs.write_u32(INTCTL, INTCTL_GIE | (1 << stream_index)); - let pcm_region = Region { - phys: crate::DirectMap::from_phys(pcm_view.host_phys()), + phys: crate::DirectMap::from_phys(pcm.host_phys()), size: crate::mm::PAGE_2M, cache: CachePolicy::Normal, pages: None, }; - *CONTROLLER.lock() = Some(HdaController { regs, stream, _bdl: bdl, _pcm: pcm }); - *INFO.lock() = Some((HdaInfo { - pcm: toyos_abi::HANDLE_INVALID, - period_bytes: PERIOD_BYTES as u32, - stream_offset: stream_offset as u32, - statests, - stream_tag: STREAM_TAG, - periods: PERIODS as u8, - }, pcm_region)); - log!( - "hda: {:02x}:{:02x}.{} bound, statests={statests:#06x}, output stream {stream_index} at \ - {stream_offset:#x}, tag {STREAM_TAG}, {PERIODS} periods of {PERIOD_BYTES} bytes", - pci.bus, - pci.dev, - pci.func + "hda: {} controller(s) listed, none bound; the stream's descriptor list and {PERIODS} \ + periods of {PERIOD_BYTES} bytes taken once", + list.len() ); - + *CONTROLLER.lock() = Some(Hda { + state: State::Listed(list), + memory: StreamMemory { space, bdl: bdl.device_addr(), pcm: pcm_region }, + }); } /// Take one controller out of reset and ask whether anything is on its link; `None` for every way @@ -574,21 +762,24 @@ fn reset_stream(stream: Mmio) -> bool { crate::clock::settles(SETTLE_NS, || stream.read_u8(SD_CTL) & SD_CTL_SRST == 0) } -/// Arm the completion interrupt, or say why this machine has no HDA audio; a refusal, never a +/// Arm the completion interrupt, or say why this controller cannot be listed; a refusal, never a /// panic, over a peripheral. -fn arm_interrupt(pci: &PciDevice) -> bool { +fn arm_interrupt(pci: &PciDevice) -> Option { let vector = crate::arch::trap::HDA_VECTOR; - if pci.enable_msix(vector).is_some() || pci.enable_msi(vector) { - return true; + if pci.enable_msix(vector).is_some() { + return Some(Message::MsiX); + } + if pci.enable_msi(vector) { + return Some(Message::Msi); } log!( "hda: {:02x}:{:02x}.{} has no interrupt this driver can arm — the line above says whether \ - a capability was missing or a message was refused — NOT INITIALISED", + a capability was missing or a message was refused — not listed", pci.bus, pci.dev, pci.func ); - false + None } fn spin_ns(duration: u64) { diff --git a/kernel/src/syscall/device.rs b/kernel/src/syscall/device.rs index 64aebc035d2..6a6e4b76836 100644 --- a/kernel/src/syscall/device.rs +++ b/kernel/src/syscall/device.rs @@ -78,7 +78,10 @@ pub(super) fn sys_device_reg(handle: RawHandle, offset: u64, width: u64, value: match value { None => { let read = match target { - RegTarget::Hda => crate::drivers::hda::reg_read(offset, width), + RegTarget::Hda => match toyos_abi::hda::Reg::from_wire(offset) { + Some(reg) => crate::drivers::hda::reg_read(reg, width), + None => Err(SyscallError::InvalidArgument), + }, // The one place a caller's offset becomes an access: the // witness `pcidev::config_window` answers is the only thing // the register read takes, so an unchecked number cannot @@ -98,7 +101,10 @@ pub(super) fn sys_device_reg(handle: RawHandle, offset: u64, width: u64, value: Some(value) => match u32::try_from(value) { Ok(value) => { let written = match target { - RegTarget::Hda => crate::drivers::hda::reg_write(offset, width, value), + RegTarget::Hda => match toyos_abi::hda::Reg::from_wire(offset) { + Some(reg) => crate::drivers::hda::reg_write(reg, width, value), + None => Err(SyscallError::InvalidArgument), + }, // Config space has no write path from userland at all. RegTarget::PciConfig(_) => Err(SyscallError::NotSupported), }; @@ -112,6 +118,22 @@ pub(super) fn sys_device_reg(handle: RawHandle, offset: u64, width: u64, value: } } +/// Bind the `hda-audio` claim's stream to the listed controller `controller` +/// names (`toyos_abi::syscall::hda_bind`). Its waits run on the caller's +/// thread with no lock held, and nothing is copied to the caller. +pub(super) fn sys_hda_bind(handle: RawHandle, controller: u64) -> u64 { + if let Err(e) = holds_claim(handle, device::DeviceType::HdaAudio) { + return e.refuse(); + } + let Ok(controller) = u8::try_from(controller) else { + return SyscallError::InvalidArgument.to_u64(); + }; + match crate::drivers::hda::bind(controller) { + Ok(()) => 0, + Err(e) => e.to_u64(), + } +} + /// One operation on the `acpi` claim (`toyos_abi::acpi::op`), for the process /// the claim's first read bound: a handle moved on to another answers /// `PermissionDenied`, as its ports answer nothing there. diff --git a/kernel/src/syscall/dispatch.rs b/kernel/src/syscall/dispatch.rs index d5286ccfe98..0a5908ff84e 100644 --- a/kernel/src/syscall/dispatch.rs +++ b/kernel/src/syscall/dispatch.rs @@ -23,7 +23,7 @@ use super::HANDLE_LEN; #[cfg(feature = "test-actuators")] use super::debug::{canary, debug_heap_alloc, ring0_timer_in_syscall, FATAL_HALT_NONCE, LOCK_ACROSS_SWITCH}; use super::device::{ - holds_claim, sys_acpi, sys_device_bar_map, sys_device_claim, sys_device_dma_alloc, sys_device_dma_map, + holds_claim, sys_acpi, sys_hda_bind, sys_device_bar_map, sys_device_claim, sys_device_dma_alloc, sys_device_dma_map, sys_device_dma_unmap, sys_device_reg, sys_gpu_reset_scanout, sys_partition_transfer, Transfer, }; @@ -687,6 +687,7 @@ pub(crate) fn syscall_dispatch(num: u64, a1: u64, a2: u64, a3: u64, a4: u64) -> SYS_ACPI => sys_acpi(&ctx, RawHandle(a1 as u32), a2, a3), SYS_DEVICE_REG_READ => sys_device_reg(RawHandle(a1 as u32), a2, a3, None), SYS_DEVICE_REG_WRITE => sys_device_reg(RawHandle(a1 as u32), a2, a3, Some(a4)), + SYS_HDA_BIND => sys_hda_bind(RawHandle(a1 as u32), a2), _ => SyscallError::InvalidArgument.to_u64(), } })(); diff --git a/lib/hda/Cargo.toml b/lib/hda/Cargo.toml index 945ca2b8774..6abc41070b8 100644 --- a/lib/hda/Cargo.toml +++ b/lib/hda/Cargo.toml @@ -11,3 +11,8 @@ description = "HDA codec decoding and output-path selection, pure." version = "0.1.0" edition = "2021" license = "MIT OR Apache-2.0" + +[dev-dependencies] +# The device rates soundserver declares, best first: the rate choice is tested +# against the declaration it runs on. +toyos-mixer = { path = "../../system/soundserver/mixer" } diff --git a/lib/hda/fixtures/amd-bench.txt b/lib/hda/fixtures/amd-bench.txt new file mode 100644 index 00000000000..372ad3a1e1d --- /dev/null +++ b/lib/hda/fixtures/amd-bench.txt @@ -0,0 +1,163 @@ +hda: 03:00.1 listed as controller 0, statests=0x0001, output stream 0 at 0x80 +hda: 03:00.6 listed as controller 1, statests=0x0001, output stream 4 at 0x100 +m1: hda controller 0 codec0 node=0x00 verb=0xf00 payload=0x00 answer=0x1002aa01 +m1: hda controller 0 codec0 node=0x00 verb=0xf00 payload=0x04 answer=0x00010001 +m1: hda controller 0 codec0 node=0x01 verb=0xf00 payload=0x05 answer=0x00000001 +m1: hda controller 0 codec0 node=0x01 verb=0xf00 payload=0x04 answer=0x0002000c +m1: hda controller 0 codec0 node=0x02 verb=0xf00 payload=0x09 answer=0x00000221 +m1: hda controller 0 codec0 node=0x02 verb=0xf00 payload=0x0a answer=0x00020070 +m1: hda controller 0 codec0 node=0x03 verb=0xf00 payload=0x09 answer=0x00400381 +m1: hda controller 0 codec0 node=0x03 verb=0xf00 payload=0x0e answer=0x00000001 +m1: hda controller 0 codec0 node=0x03 verb=0xf02 payload=0x00 answer=0x00000002 +m1: hda controller 0 codec0 node=0x03 verb=0xf00 payload=0x0c answer=0x00000094 +m1: hda controller 0 codec0 node=0x03 verb=0xf1c payload=0x00 answer=0x185600f0 +m1: hda controller 0 codec0 node=0x04 verb=0xf00 payload=0x09 answer=0x00000221 +m1: hda controller 0 codec0 node=0x04 verb=0xf00 payload=0x0a answer=0x00020070 +m1: hda controller 0 codec0 node=0x05 verb=0xf00 payload=0x09 answer=0x00400381 +m1: hda controller 0 codec0 node=0x05 verb=0xf00 payload=0x0e answer=0x00000001 +m1: hda controller 0 codec0 node=0x05 verb=0xf02 payload=0x00 answer=0x00000004 +m1: hda controller 0 codec0 node=0x05 verb=0xf00 payload=0x0c answer=0x00000094 +m1: hda controller 0 codec0 node=0x05 verb=0xf1c payload=0x00 answer=0x185600f0 +m1: hda controller 0 codec0 node=0x06 verb=0xf00 payload=0x09 answer=0x00000221 +m1: hda controller 0 codec0 node=0x06 verb=0xf00 payload=0x0a answer=0x00020070 +m1: hda controller 0 codec0 node=0x07 verb=0xf00 payload=0x09 answer=0x00400381 +m1: hda controller 0 codec0 node=0x07 verb=0xf00 payload=0x0e answer=0x00000001 +m1: hda controller 0 codec0 node=0x07 verb=0xf02 payload=0x00 answer=0x00000006 +m1: hda controller 0 codec0 node=0x07 verb=0xf00 payload=0x0c answer=0x00000094 +m1: hda controller 0 codec0 node=0x07 verb=0xf1c payload=0x00 answer=0x585600f0 +m1: hda controller 0 codec0 node=0x08 verb=0xf00 payload=0x09 answer=0x00000221 +m1: hda controller 0 codec0 node=0x08 verb=0xf00 payload=0x0a answer=0x00020070 +m1: hda controller 0 codec0 node=0x09 verb=0xf00 payload=0x09 answer=0x00400381 +m1: hda controller 0 codec0 node=0x09 verb=0xf00 payload=0x0e answer=0x00000001 +m1: hda controller 0 codec0 node=0x09 verb=0xf02 payload=0x00 answer=0x00000008 +m1: hda controller 0 codec0 node=0x09 verb=0xf00 payload=0x0c answer=0x00000094 +m1: hda controller 0 codec0 node=0x09 verb=0xf1c payload=0x00 answer=0x585600f0 +m1: hda controller 0 codec0 node=0x0a verb=0xf00 payload=0x09 answer=0x00000221 +m1: hda controller 0 codec0 node=0x0a verb=0xf00 payload=0x0a answer=0x00020070 +m1: hda controller 0 codec0 node=0x0b verb=0xf00 payload=0x09 answer=0x00400381 +m1: hda controller 0 codec0 node=0x0b verb=0xf00 payload=0x0e answer=0x00000001 +m1: hda controller 0 codec0 node=0x0b verb=0xf02 payload=0x00 answer=0x0000000a +m1: hda controller 0 codec0 node=0x0b verb=0xf00 payload=0x0c answer=0x00000094 +m1: hda controller 0 codec0 node=0x0b verb=0xf1c payload=0x00 answer=0x585600f0 +m1: hda controller 0 codec0 node=0x0c verb=0xf00 payload=0x09 answer=0x00000221 +m1: hda controller 0 codec0 node=0x0c verb=0xf00 payload=0x0a answer=0x00020070 +m1: hda controller 0 codec0 node=0x0d verb=0xf00 payload=0x09 answer=0x00400381 +m1: hda controller 0 codec0 node=0x0d verb=0xf00 payload=0x0e answer=0x00000001 +m1: hda controller 0 codec0 node=0x0d verb=0xf02 payload=0x00 answer=0x0000000c +m1: hda controller 0 codec0 node=0x0d verb=0xf00 payload=0x0c answer=0x00000094 +m1: hda controller 0 codec0 node=0x0d verb=0xf1c payload=0x00 answer=0x585600f0 +m1: hda controller 1 codec0 node=0x00 verb=0xf00 payload=0x00 answer=0x10ec0287 +m1: hda controller 1 codec0 node=0x00 verb=0xf00 payload=0x04 answer=0x00010001 +m1: hda controller 1 codec0 node=0x01 verb=0xf00 payload=0x05 answer=0x00000101 +m1: hda controller 1 codec0 node=0x01 verb=0xf00 payload=0x04 answer=0x00020024 +m1: hda controller 1 codec0 node=0x02 verb=0xf00 payload=0x09 answer=0x0000041d +m1: hda controller 1 codec0 node=0x02 verb=0xf00 payload=0x12 answer=0x00025757 +m1: hda controller 1 codec0 node=0x02 verb=0xf00 payload=0x0a answer=0x000e0040 +m1: hda controller 1 codec0 node=0x03 verb=0xf00 payload=0x09 answer=0x0000041d +m1: hda controller 1 codec0 node=0x03 verb=0xf00 payload=0x12 answer=0x00025757 +m1: hda controller 1 codec0 node=0x03 verb=0xf00 payload=0x0a answer=0x000e0040 +m1: hda controller 1 codec0 node=0x04 verb=0xf00 payload=0x09 answer=0x00f00000 +m1: hda controller 1 codec0 node=0x05 verb=0xf00 payload=0x09 answer=0x00f00000 +m1: hda controller 1 codec0 node=0x06 verb=0xf00 payload=0x09 answer=0x00000411 +m1: hda controller 1 codec0 node=0x06 verb=0xf00 payload=0x0a answer=0x000e0040 +m1: hda controller 1 codec0 node=0x07 verb=0xf00 payload=0x09 answer=0x0010051b +m1: hda controller 1 codec0 node=0x07 verb=0xf00 payload=0x0e answer=0x00000001 +m1: hda controller 1 codec0 node=0x07 verb=0xf02 payload=0x00 answer=0x00000024 +m1: hda controller 1 codec0 node=0x07 verb=0xf00 payload=0x0d answer=0x80023f17 +m1: hda controller 1 codec0 node=0x07 verb=0xf00 payload=0x0a answer=0x000e0040 +m1: hda controller 1 codec0 node=0x08 verb=0xf00 payload=0x09 answer=0x0010051b +m1: hda controller 1 codec0 node=0x08 verb=0xf00 payload=0x0e answer=0x00000001 +m1: hda controller 1 codec0 node=0x08 verb=0xf02 payload=0x00 answer=0x00000023 +m1: hda controller 1 codec0 node=0x08 verb=0xf00 payload=0x0d answer=0x80023f17 +m1: hda controller 1 codec0 node=0x08 verb=0xf00 payload=0x0a answer=0x000e0560 +m1: hda controller 1 codec0 node=0x09 verb=0xf00 payload=0x09 answer=0x0010051b +m1: hda controller 1 codec0 node=0x09 verb=0xf00 payload=0x0e answer=0x00000001 +m1: hda controller 1 codec0 node=0x09 verb=0xf02 payload=0x00 answer=0x00000022 +m1: hda controller 1 codec0 node=0x09 verb=0xf00 payload=0x0d answer=0x80023f17 +m1: hda controller 1 codec0 node=0x09 verb=0xf00 payload=0x0a answer=0x000e0560 +m1: hda controller 1 codec0 node=0x0a verb=0xf00 payload=0x09 answer=0x0010051b +m1: hda controller 1 codec0 node=0x0a verb=0xf00 payload=0x0e answer=0x00000001 +m1: hda controller 1 codec0 node=0x0a verb=0xf02 payload=0x00 answer=0x00000025 +m1: hda controller 1 codec0 node=0x0a verb=0xf00 payload=0x0d answer=0x80023f17 +m1: hda controller 1 codec0 node=0x0a verb=0xf00 payload=0x0a answer=0x000e0040 +m1: hda controller 1 codec0 node=0x0b verb=0xf00 payload=0x09 answer=0x00f00000 +m1: hda controller 1 codec0 node=0x0c verb=0xf00 payload=0x09 answer=0x00f00000 +m1: hda controller 1 codec0 node=0x0d verb=0xf00 payload=0x09 answer=0x00f00000 +m1: hda controller 1 codec0 node=0x0e verb=0xf00 payload=0x09 answer=0x00f00000 +m1: hda controller 1 codec0 node=0x0f verb=0xf00 payload=0x09 answer=0x00f00000 +m1: hda controller 1 codec0 node=0x10 verb=0xf00 payload=0x09 answer=0x00f00000 +m1: hda controller 1 codec0 node=0x11 verb=0xf00 payload=0x09 answer=0x00f00000 +m1: hda controller 1 codec0 node=0x12 verb=0xf00 payload=0x09 answer=0x0040040b +m1: hda controller 1 codec0 node=0x12 verb=0xf00 payload=0x0d answer=0x00270300 +m1: hda controller 1 codec0 node=0x12 verb=0xf00 payload=0x0c answer=0x00000020 +m1: hda controller 1 codec0 node=0x12 verb=0xf1c payload=0x00 answer=0x411111f0 +m1: hda controller 1 codec0 node=0x13 verb=0xf00 payload=0x09 answer=0x0040040b +m1: hda controller 1 codec0 node=0x13 verb=0xf00 payload=0x0d answer=0x00270300 +m1: hda controller 1 codec0 node=0x13 verb=0xf00 payload=0x0c answer=0x00000020 +m1: hda controller 1 codec0 node=0x13 verb=0xf1c payload=0x00 answer=0x411111f0 +m1: hda controller 1 codec0 node=0x14 verb=0xf00 payload=0x09 answer=0x0040058d +m1: hda controller 1 codec0 node=0x14 verb=0xf00 payload=0x0e answer=0x00000001 +m1: hda controller 1 codec0 node=0x14 verb=0xf02 payload=0x00 answer=0x00000002 +m1: hda controller 1 codec0 node=0x14 verb=0xf00 payload=0x12 answer=0x80000000 +m1: hda controller 1 codec0 node=0x14 verb=0xf00 payload=0x0c answer=0x00010014 +m1: hda controller 1 codec0 node=0x14 verb=0xf1c payload=0x00 answer=0x90170110 +m1: hda controller 1 codec0 node=0x15 verb=0xf00 payload=0x09 answer=0x00f00000 +m1: hda controller 1 codec0 node=0x16 verb=0xf00 payload=0x09 answer=0x00f00000 +m1: hda controller 1 codec0 node=0x17 verb=0xf00 payload=0x09 answer=0x0040058d +m1: hda controller 1 codec0 node=0x17 verb=0xf00 payload=0x0e answer=0x00000004 +m1: hda controller 1 codec0 node=0x17 verb=0xf02 payload=0x00 answer=0x08060302 +m1: hda controller 1 codec0 node=0x17 verb=0xf00 payload=0x12 answer=0x80000000 +m1: hda controller 1 codec0 node=0x17 verb=0xf00 payload=0x0c answer=0x0000001c +m1: hda controller 1 codec0 node=0x17 verb=0xf1c payload=0x00 answer=0x411111f0 +m1: hda controller 1 codec0 node=0x18 verb=0xf00 payload=0x09 answer=0x0040048b +m1: hda controller 1 codec0 node=0x18 verb=0xf00 payload=0x0d answer=0x00270300 +m1: hda controller 1 codec0 node=0x18 verb=0xf00 payload=0x0c answer=0x00000024 +m1: hda controller 1 codec0 node=0x18 verb=0xf1c payload=0x00 answer=0x411111f0 +m1: hda controller 1 codec0 node=0x19 verb=0xf00 payload=0x09 answer=0x0040048b +m1: hda controller 1 codec0 node=0x19 verb=0xf00 payload=0x0d answer=0x00270300 +m1: hda controller 1 codec0 node=0x19 verb=0xf00 payload=0x0c answer=0x00003724 +m1: hda controller 1 codec0 node=0x19 verb=0xf1c payload=0x00 answer=0x03a11030 +m1: hda controller 1 codec0 node=0x1a verb=0xf00 payload=0x09 answer=0x0040048b +m1: hda controller 1 codec0 node=0x1a verb=0xf00 payload=0x0d answer=0x00270300 +m1: hda controller 1 codec0 node=0x1a verb=0xf00 payload=0x0c answer=0x00003724 +m1: hda controller 1 codec0 node=0x1a verb=0xf1c payload=0x00 answer=0x411111f0 +m1: hda controller 1 codec0 node=0x1b verb=0xf00 payload=0x09 answer=0x0040058f +m1: hda controller 1 codec0 node=0x1b verb=0xf00 payload=0x0e answer=0x00000002 +m1: hda controller 1 codec0 node=0x1b verb=0xf02 payload=0x00 answer=0x00000302 +m1: hda controller 1 codec0 node=0x1b verb=0xf00 payload=0x12 answer=0x80000000 +m1: hda controller 1 codec0 node=0x1b verb=0xf00 payload=0x0d answer=0x00270300 +m1: hda controller 1 codec0 node=0x1b verb=0xf00 payload=0x0c answer=0x00013734 +m1: hda controller 1 codec0 node=0x1b verb=0xf1c payload=0x00 answer=0x411111f0 +m1: hda controller 1 codec0 node=0x1c verb=0xf00 payload=0x09 answer=0x00f00000 +m1: hda controller 1 codec0 node=0x1d verb=0xf00 payload=0x09 answer=0x00400400 +m1: hda controller 1 codec0 node=0x1d verb=0xf00 payload=0x0c answer=0x00000020 +m1: hda controller 1 codec0 node=0x1d verb=0xf1c payload=0x00 answer=0x40471a6d +m1: hda controller 1 codec0 node=0x1e verb=0xf00 payload=0x09 answer=0x00400501 +m1: hda controller 1 codec0 node=0x1e verb=0xf00 payload=0x0e answer=0x00000001 +m1: hda controller 1 codec0 node=0x1e verb=0xf02 payload=0x00 answer=0x00000006 +m1: hda controller 1 codec0 node=0x1e verb=0xf00 payload=0x0c answer=0x00000010 +m1: hda controller 1 codec0 node=0x1e verb=0xf1c payload=0x00 answer=0x411111f0 +m1: hda controller 1 codec0 node=0x1f verb=0xf00 payload=0x09 answer=0x00f00000 +m1: hda controller 1 codec0 node=0x20 verb=0xf00 payload=0x09 answer=0x00f00040 +m1: hda controller 1 codec0 node=0x21 verb=0xf00 payload=0x09 answer=0x0040058d +m1: hda controller 1 codec0 node=0x21 verb=0xf00 payload=0x0e answer=0x00000002 +m1: hda controller 1 codec0 node=0x21 verb=0xf02 payload=0x00 answer=0x00000302 +m1: hda controller 1 codec0 node=0x21 verb=0xf00 payload=0x12 answer=0x80000000 +m1: hda controller 1 codec0 node=0x21 verb=0xf00 payload=0x0c answer=0x0001001c +m1: hda controller 1 codec0 node=0x21 verb=0xf1c payload=0x00 answer=0x03211020 +m1: hda controller 1 codec0 node=0x22 verb=0xf00 payload=0x09 answer=0x0020010b +m1: hda controller 1 codec0 node=0x22 verb=0xf00 payload=0x0e answer=0x00000005 +m1: hda controller 1 codec0 node=0x22 verb=0xf02 payload=0x00 answer=0x1d1b1a19 +m1: hda controller 1 codec0 node=0x22 verb=0xf02 payload=0x04 answer=0x00000013 +m1: hda controller 1 codec0 node=0x22 verb=0xf00 payload=0x0d answer=0x80000000 +m1: hda controller 1 codec0 node=0x23 verb=0xf00 payload=0x09 answer=0x0020010b +m1: hda controller 1 codec0 node=0x23 verb=0xf00 payload=0x0e answer=0x00000005 +m1: hda controller 1 codec0 node=0x23 verb=0xf02 payload=0x00 answer=0x1d1b1a19 +m1: hda controller 1 codec0 node=0x23 verb=0xf02 payload=0x04 answer=0x00000012 +m1: hda controller 1 codec0 node=0x23 verb=0xf00 payload=0x0d answer=0x80000000 +m1: hda controller 1 codec0 node=0x24 verb=0xf00 payload=0x09 answer=0x00300101 +m1: hda controller 1 codec0 node=0x24 verb=0xf00 payload=0x0e answer=0x00000003 +m1: hda controller 1 codec0 node=0x24 verb=0xf02 payload=0x00 answer=0x00181312 +m1: hda controller 1 codec0 node=0x25 verb=0xf00 payload=0x09 answer=0x00300101 +m1: hda controller 1 codec0 node=0x25 verb=0xf00 payload=0x0e answer=0x00000002 +m1: hda controller 1 codec0 node=0x25 verb=0xf02 payload=0x00 answer=0x00001312 diff --git a/lib/hda/src/config.rs b/lib/hda/src/config.rs index 06e1fd441b1..8983b8daa21 100644 --- a/lib/hda/src/config.rs +++ b/lib/hda/src/config.rs @@ -12,10 +12,10 @@ use alloc::vec::Vec; -use crate::caps::AmpCaps; +use crate::caps::{AmpCaps, GainRange, PcmCaps}; use crate::graph::{Codec, FunctionGroup}; use crate::path::{OutputPath, PinSetup}; -use crate::verb::{self, Address, Node, Verb}; +use crate::verb::{self, Address, Node, Response, Verb}; /// `Set Power State`'s fully-on value. const POWER_D0: u8 = 0x00; @@ -36,6 +36,7 @@ const EAPD_ENABLE: u8 = 1 << 1; const AMP_SET_OUTPUT: u16 = 1 << 15; const AMP_SET_LEFT: u16 = 1 << 13; const AMP_SET_RIGHT: u16 = 1 << 12; +const AMP_SET_MUTE: u16 = 1 << 7; /// The whole configuration sequence for one output path, in the order it has to /// be sent. @@ -141,22 +142,178 @@ fn nodes(path: &OutputPath) -> Vec { out } -/// The rate and width this driver asks a converter for, and whether the -/// converter offers them. -pub const RATE: u32 = 44_100; +/// The width this driver asks a converter for. pub const WIDTH: u8 = 16; -/// `SDnFMT` and the codec's `Set Converter Format` payload for this path, or -/// `None` where the converter does not offer [`RATE`] at [`WIDTH`]. -pub fn format(codecs: &[Codec], path: &OutputPath) -> Option<(u16, u8)> { +/// The first of `rates`, best first, that the path's converter offers at +/// [`WIDTH`]; or, where it offers none of them, the converter's own PCM word +/// (`None` where it has none), so a refusal can name what it does offer. +/// +/// The converter's own word, never its function group's: every converter in +/// reach sets Format Override, and one that did not would take the group's. +pub fn rate(codecs: &[Codec], path: &OutputPath, rates: &[u32]) -> Result> { + let pcm = group_of(codecs, path) + .and_then(|group| group.widget(path.converter)) + .and_then(|converter| converter.pcm) + .ok_or(None)?; + rates.iter().copied().find(|&rate| pcm.supports(rate, WIDTH)).ok_or(Some(pcm)) +} + +/// `SDnFMT` and the codec's `Set Converter Format` payload for this path at +/// `rate`, or `None` where the converter does not offer `rate` at [`WIDTH`]. +pub fn format(codecs: &[Codec], path: &OutputPath, rate: u32) -> Option<(u16, u8)> { let group = group_of(codecs, path)?; let converter = group.widget(path.converter)?; let pcm = converter.pcm?; - if !pcm.supports(RATE, WIDTH) { + if !pcm.supports(rate, WIDTH) { return None; } let channels = converter.caps.channels.min(2); - Some((crate::stream::stream_format(RATE, WIDTH, channels)?, channels)) + Some((crate::stream::stream_format(rate, WIDTH, channels)?, channels)) +} + +/// One position of the master level, in quarter-decibels: 3 dB, quantised up +/// to whole steps of the amplifier that carries it. +const POSITION_QUARTER_DB: u8 = 12; + +/// `Get Amplifier Gain/Mute`'s payload: the output amplifier, left channel. +const AMP_GET_OUTPUT_LEFT: u16 = (1 << 15) | (1 << 13); + +/// A `Get Amplifier Gain/Mute` answer's mute bit, and its gain index. +const AMP_ANSWER_MUTE: u32 = 1 << 7; +const AMP_ANSWER_GAIN: u32 = 0x7F; + +/// The path's master level and mute, on the codec. +/// +/// The level is on the path's amplifier that has a gain range, and moves in +/// positions of 3 dB from its 0 dB, position 0, down to its minimum. Mute is +/// on every output pin amplifier with a mute bit, or else on the converter's. +#[derive(Clone, PartialEq, Eq, Debug)] +pub struct Master { + codec: Address, + gain: Node, + range: GainRange, + mute: Vec, +} + +/// Where the master stands: a position below the top, and whether it is muted. +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub struct Setting { + pub position: u8, + pub muted: bool, +} + +impl Setting { + /// 0 dB and unmuted: what [`verbs`] leaves a path at. + pub const TOP: Self = Self { position: 0, muted: false }; +} + +/// Why a path has no master. +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub enum NoMaster { + /// No amplifier on the path has a gain range. + NoGain, + /// Neither an output pin nor the converter has a mute bit. + NoMute, +} + +/// The master `path` carries, or which half it lacks. +pub fn master(path: &OutputPath) -> Result { + let pin_amps = || pins(path).filter_map(|pin| pin.amp.map(|amp| (pin.node, amp))); + let amps = path.converter_amp.map(|amp| (path.converter, amp)).into_iter().chain(pin_amps()); + let (gain, range) = amps + .filter_map(|(node, amp)| amp.gain.map(|range| (node, range))) + .next() + .ok_or(NoMaster::NoGain)?; + let mut mute: Vec = pin_amps().filter(|(_, amp)| amp.mute).map(|(node, _)| node).collect(); + if mute.is_empty() && path.converter_amp.is_some_and(|amp| amp.mute) { + mute.push(path.converter); + } + if mute.is_empty() { + return Err(NoMaster::NoMute); + } + Ok(Master { codec: path.codec, gain, range, mute }) +} + +impl Master { + /// Amplifier steps per position: 3 dB rounded up to whole steps. + fn per_position(&self) -> u8 { + POSITION_QUARTER_DB.div_ceil(self.range.step_size_quarter_db) + } + + /// The lowest position, the amplifier's minimum. + pub fn bottom(&self) -> u8 { + self.range.zero_db.div_ceil(self.per_position()) + } + + /// `setting` moved by `delta` positions, clamped to the range, and + /// unmuted: a step while muted unmutes. + pub fn step(&self, setting: Setting, delta: i32) -> Setting { + let position = (setting.position as i32).saturating_add(delta).clamp(0, self.bottom() as i32) as u8; + Setting { position, muted: false } + } + + fn index(&self, position: u8) -> u8 { + self.range.zero_db.saturating_sub(position.min(self.bottom()) * self.per_position()) + } + + /// The verbs that set `setting`, gain and mute both, on every amplifier + /// the master names. + pub fn set(&self, setting: Setting) -> Vec { + let amp = |node: Node, gain: u8, muted: bool| { + let mute = if muted { AMP_SET_MUTE } else { 0 }; + Verb::long( + self.codec, + node, + verb::SET_AMP_GAIN_MUTE as u8, + AMP_SET_OUTPUT | AMP_SET_LEFT | AMP_SET_RIGHT | mute | gain as u16, + ) + }; + let gain = self.index(setting.position); + let mut out = alloc::vec![amp(self.gain, gain, setting.muted && self.mute.contains(&self.gain))]; + for &node in self.mute.iter().filter(|&&node| node != self.gain) { + out.push(amp(node, 0, setting.muted)); + } + out + } + + /// The verbs that read the master back: the gain amplifier, then every + /// mute amplifier that is not it. + pub fn read(&self) -> Vec { + let get = |node: Node| { + Verb::long(self.codec, node, verb::GET_AMP_GAIN_MUTE as u8, AMP_GET_OUTPUT_LEFT) + }; + core::iter::once(self.gain) + .chain(self.mute.iter().copied().filter(|&node| node != self.gain)) + .map(get) + .collect() + } + + /// The setting the answers to [`Self::read`] say, or `None` where they + /// name no setting: a count that is not the read's, a gain above 0 dB or + /// between two positions, or mute amplifiers that disagree. + pub fn decode(&self, answers: &[Response]) -> Option { + let [gain, rest @ ..] = answers else { return None }; + if answers.len() != self.read().len() { + return None; + } + let index = (gain.raw() & AMP_ANSWER_GAIN) as u8; + let below = self.range.zero_db.checked_sub(index)?; + let position = below.div_ceil(self.per_position()); + if self.index(position) != index { + return None; + } + let mute_answers: Vec = if self.mute.contains(&self.gain) { + core::iter::once(gain).chain(rest).map(|a| a.raw() & AMP_ANSWER_MUTE != 0).collect() + } else { + rest.iter().map(|a| a.raw() & AMP_ANSWER_MUTE != 0).collect() + }; + let muted = mute_answers[0]; + if mute_answers.iter().any(|&m| m != muted) { + return None; + } + Some(Setting { position, muted }) + } } fn group_of<'a>(codecs: &'a [Codec], path: &OutputPath) -> Option<&'a FunctionGroup> { @@ -173,9 +330,10 @@ mod tests { use super::*; use crate::fixture; use crate::path::find_output_path; + use toyos_mixer::DEVICE_RATES; - /// The bit this sequence never sets, which is why it lives only here. - const AMP_MUTE: u16 = 1 << 7; + /// The bit this sequence never sets. + const AMP_MUTE: u16 = AMP_SET_MUTE; fn laptop() -> (Vec, OutputPath) { let codecs = fixture::laptop(); @@ -282,28 +440,167 @@ mod tests { assert_eq!(selects, [0x0207_0100, 0x0307_0101]); } + /// The laptop's path with its converter's PCM word replaced by `word`. + fn converter_offering(word: u32) -> (Vec, OutputPath) { + let mut codecs = fixture::laptop(); + let group = &mut codecs[0].groups[0]; + let dac = group.widgets.iter_mut().find(|w| w.node == Node(0x02)).unwrap(); + dac.pcm = Some(PcmCaps::decode(crate::verb::Response::new(word).unwrap())); + let path = find_output_path(&codecs).unwrap(); + (codecs, path) + } + #[test] - fn both_machines_offer_the_one_rate_this_driver_asks_for() { + fn a_converter_offering_44100_runs_at_44100_as_both_machines_do() { let (codecs, path) = laptop(); - assert_eq!(format(&codecs, &path), Some((0x4011, 2))); + assert_eq!(rate(&codecs, &path, &DEVICE_RATES), Ok(44_100)); + assert_eq!(format(&codecs, &path, 44_100), Some((0x4011, 2))); let codecs = fixture::qemu(); let path = find_output_path(&codecs).unwrap(); - assert_eq!(format(&codecs, &path), Some((0x4011, 2))); + assert_eq!(rate(&codecs, &path, &DEVICE_RATES), Ok(44_100)); + assert_eq!(format(&codecs, &path, 44_100), Some((0x4011, 2))); + } + + /// The AMD bench's analog controller's codecs and the path chosen on them. + fn amd_bench() -> (Vec, OutputPath) { + let mut bench = fixture::amd_bench(); + let (index, path) = crate::path::choose(&bench).unwrap(); + (bench.swap_remove(index), path) } #[test] - fn a_converter_that_does_not_offer_the_rate_is_a_refusal_and_not_a_substitution() { - let mut codecs = fixture::laptop(); - // 48 kHz only, at 16 bits: a converter that exists and cannot play the - // one rate this pipeline runs at. - let group = &mut codecs[0].groups[0]; - let dac = group.widgets.iter_mut().find(|w| w.node == Node(0x02)).unwrap(); - dac.pcm = Some(crate::caps::PcmCaps::decode( - crate::verb::Response::new(0x0002_0040).unwrap(), - )); + fn the_amd_bench_s_converter_offering_48000_alone_runs_at_48000() { + // Its own word is `0x000e0040`: 48 kHz at 16, 20 and 24 bits. The + // recorded boot read `format 0x0011 (48000 Hz 2 ch 16-bit)` and `path + // configured in 14 verbs, stream tag 1`. + let (codecs, path) = amd_bench(); + assert_eq!(rate(&codecs, &path, &DEVICE_RATES), Ok(48_000)); + assert_eq!(format(&codecs, &path, 48_000), Some((0x0011, 2))); + assert_eq!(verbs(&codecs, &path, 0x0011, 1).unwrap().len(), 14); + } + + #[test] + fn the_amd_bench_s_master_gains_on_the_converter_and_mutes_on_both_pins() { + let (_, path) = amd_bench(); + let master = master(&path).unwrap(); + // The converter's amplifier is 88 steps of 0.75 dB with 0 dB at 87, + // and both pins' are mute-only. + assert_eq!(master.bottom(), 22); + let raw: Vec = master.set(Setting { position: 1, muted: true }).iter().map(|v| v.raw()).collect(); + assert_eq!(raw, [0x0023_B053, 0x0143_B080, 0x0213_B080]); + } + + #[test] + fn a_converter_offering_48000_beside_others_but_not_44100_runs_at_48000() { + // 32, 48 and 96 kHz at 16 bits. + let (codecs, path) = converter_offering(0x0002_0150); + assert_eq!(rate(&codecs, &path, &DEVICE_RATES), Ok(48_000)); + } + + #[test] + fn a_converter_offering_neither_rate_is_refused_naming_what_it_offers() { + // 32 and 96 kHz at 16 bits. + let (codecs, path) = converter_offering(0x0002_0110); + let Err(Some(pcm)) = rate(&codecs, &path, &DEVICE_RATES) else { + panic!("a converter offering neither rate was given one"); + }; + assert_eq!(pcm.rates().collect::>(), [32_000, 96_000]); + assert_eq!(format(&codecs, &path, 44_100), None); + assert_eq!(format(&codecs, &path, 48_000), None); + } + + /// What a codec answers a `Get Amplifier Gain/Mute` with after `verbs`: + /// the gain and mute each amplifier was last set to, left channel. + fn answers(master: &Master, sent: &[Verb]) -> Vec { + master + .read() + .iter() + .map(|get| { + let node = (get.raw() >> 20) & 0xFF; + let set = sent.iter().rev().find(|v| (v.raw() >> 20) & 0xFF == node).unwrap(); + Response::new(set.raw() & 0xFF).unwrap() + }) + .collect() + } + + #[test] + fn the_laptop_s_master_gains_on_the_converter_and_mutes_on_both_pins() { + let (_, path) = laptop(); + let master = master(&path).unwrap(); + // 88 steps of 0.75 dB, 0 dB at 87: four steps a position, 22 positions down. + assert_eq!(master.bottom(), 22); + let raw: Vec = master.set(Setting { position: 1, muted: true }).iter().map(|v| v.raw()).collect(); + assert_eq!(raw, [0x0023_B053, 0x0143_B080, 0x0213_B080]); + let read: Vec = master.read().iter().map(|v| v.raw()).collect(); + assert_eq!(read, [0x002B_A000, 0x014B_A000, 0x021B_A000]); + } + + #[test] + fn qemus_master_gains_and_mutes_on_its_converter() { + let codecs = fixture::qemu(); let path = find_output_path(&codecs).unwrap(); - assert_eq!(format(&codecs, &path), None); + let master = master(&path).unwrap(); + let raw: Vec = master.set(Setting { position: 2, muted: true }).iter().map(|v| v.raw()).collect(); + // 0 dB at 74, 1 dB steps: three steps a position. + assert_eq!(raw, [0x0023_B0C4]); + assert_eq!(master.read().len(), 1); + } + + #[test] + fn every_setting_reads_back_as_itself() { + for (_, path) in [laptop(), { + let codecs = fixture::qemu(); + let path = find_output_path(&codecs).unwrap(); + (codecs, path) + }] { + let master = master(&path).unwrap(); + for position in 0..=master.bottom() { + for muted in [false, true] { + let setting = Setting { position, muted }; + assert_eq!(master.decode(&answers(&master, &master.set(setting))), Some(setting)); + } + } + } + } + + #[test] + fn a_step_is_clamped_to_the_range_and_unmutes() { + let (_, path) = laptop(); + let master = master(&path).unwrap(); + let muted_top = Setting { position: 0, muted: true }; + assert_eq!(master.step(muted_top, -1), Setting::TOP); + assert_eq!(master.step(Setting::TOP, 1), Setting { position: 1, muted: false }); + let bottom = Setting { position: master.bottom(), muted: false }; + assert_eq!(master.step(bottom, 1), bottom); + assert_eq!(master.step(Setting::TOP, i32::MAX), bottom); + assert_eq!(master.step(bottom, i32::MIN), Setting::TOP); + // The bottom is the amplifier's minimum, index 0, not past it. + assert_eq!(master.set(bottom)[0].raw() & 0x7F, 0); + } + + #[test] + fn answers_that_name_no_setting_are_refused() { + let (_, path) = laptop(); + let master = master(&path).unwrap(); + let r = |raw| Response::new(raw).unwrap(); + // Above 0 dB, between two positions, the pins disagreeing, and one answer short. + assert_eq!(master.decode(&[r(0x58), r(0), r(0)]), None); + assert_eq!(master.decode(&[r(0x56), r(0), r(0)]), None); + assert_eq!(master.decode(&[r(0x57), r(0x80), r(0)]), None); + assert_eq!(master.decode(&[r(0x57), r(0)]), None); + assert_eq!(master.decode(&[r(0x57), r(0x80), r(0x80)]), Some(Setting { position: 0, muted: true })); + } + + #[test] + fn a_path_with_no_gain_range_or_no_mute_bit_has_no_master() { + let (_, mut path) = laptop(); + let converter = path.converter_amp.take(); + assert_eq!(master(&path), Err(NoMaster::NoGain)); + path.converter_amp = converter; + path.output.amp = None; + path.headphone.as_mut().unwrap().amp = None; + assert_eq!(master(&path), Err(NoMaster::NoMute)); } #[test] diff --git a/lib/hda/src/fixture.rs b/lib/hda/src/fixture.rs index ac8f1d368f0..c224b0a0f05 100644 --- a/lib/hda/src/fixture.rs +++ b/lib/hda/src/fixture.rs @@ -1,16 +1,16 @@ -//! Graphs to test against: one real machine's, and the states no real machine -//! in reach constructs. +//! Graphs to test against: real machines', and the states no real machine in +//! reach constructs. //! -//! The real ones are the boot-time codec probe's own `hda:` lines, committed -//! verbatim — the second life that log format was designed for, and it outlives -//! the probe: the diagnostic has since been deleted and the fixtures stay as -//! artifacts. +//! The real ones are committed verbatim from a boot: the codec probe's `hda:` +//! lines (the laptop, QEMU), or soundserver's walk recorded verb by verb (the +//! AMD bench). use alloc::vec; use alloc::vec::Vec; use crate::caps::{AmpCaps, ConfigDefault, PcmCaps, PinCaps, WidgetCaps}; use crate::graph::{Codec, FunctionGroup, FunctionKind, Pin, Widget}; +use crate::probe; use crate::verb::{Address, Node, Response, Subordinates}; /// The laptop's ALC257 and its display audio, as the probe read @@ -26,6 +26,62 @@ pub fn qemu() -> Vec { parse(include_str!("../fixtures/qemu-intel-hda.txt")) } +/// The AMD bench's two controllers, as soundserver's walk read them: +/// `03:00.1`, the GPU's display audio, and `03:00.6`, the analog codec. +/// +/// The file is the kernel's `listed as controller` line for each controller's +/// `STATESTS` and one `m1:` line per verb the walk sent, with the codec's raw +/// answer. Replaying them through [`probe::enumerate`] builds the graphs +/// soundserver built on that boot. +pub fn amd_bench() -> Vec> { + recorded(include_str!("../fixtures/amd-bench.txt")) +} + +/// Every controller a recorded walk lists, walked again against its answers, +/// in list order. +fn recorded(text: &'static str) -> Vec> { + text.lines() + .filter(|l| l.contains(" listed as controller ")) + .map(|l| { + let statests = l.split_once("statests=").and_then(|(_, w)| hex(w.split(',').next()?)); + statests.expect("a listed controller names its STATESTS") as u16 + }) + .enumerate() + .map(|(controller, statests)| { + probe::enumerate(&mut Recorded { text, controller }, statests) + .into_iter() + .map(|codec| codec.expect("the recorded codec answered")) + .collect() + }) + .collect() +} + +/// A codec that answers what the recorded one answered, and refuses to make up +/// an answer to a verb the recorded walk never sent. +struct Recorded { + text: &'static str, + controller: usize, +} + +impl probe::Verbs for Recorded { + fn get(&mut self, codec: Address, node: Node, verb: u16, payload: u8) -> Option { + let asked = alloc::format!( + "m1: hda controller {} codec{codec} node={:#04x} verb={verb:#05x} payload={payload:#04x} answer=", + self.controller, + node.0 + ); + let answer = self + .text + .lines() + .find_map(|l| l.trim().strip_prefix(asked.as_str())) + .unwrap_or_else(|| panic!("the recorded walk never sent `{asked}`")); + match answer { + "none" => None, + word => Some(Response::new(hex(word).expect("a recorded answer is a word")).expect("not all ones")), + } + } +} + fn hex(text: &str) -> Option { u32::from_str_radix(text.strip_prefix("0x").unwrap_or(text), 16).ok() } @@ -263,6 +319,19 @@ mod tests { assert_eq!(display.device, 0x2812); } + #[test] + fn the_amd_bench_s_recorded_walk_replays_to_both_controllers_codecs() { + // As soundserver named them on that boot: `03:00.1 codec0 + // vendor=1002 device=aa01` and `03:00.6 codec0 vendor=10ec device=0287`, + // each with one function group. + let bench = amd_bench(); + let ids: Vec<(u16, u16, usize)> = + bench.iter().flatten().map(|c| (c.vendor, c.device, c.groups.len())).collect(); + assert_eq!(ids, [(0x1002, 0xaa01, 1), (0x10ec, 0x0287, 1)]); + let analog = &bench[1][0].groups[0]; + assert_eq!(analog.range.count as usize, analog.widgets.len()); + } + #[test] fn the_speaker_pin_round_trips_through_the_log_format() { let codecs = laptop(); diff --git a/lib/hda/src/lib.rs b/lib/hda/src/lib.rs index 43c10ef0d1b..146c36827b1 100644 --- a/lib/hda/src/lib.rs +++ b/lib/hda/src/lib.rs @@ -30,6 +30,6 @@ mod fixture; pub use caps::{AmpCaps, ConfigDefault, GainRange, PinCaps, WidgetCaps, WidgetKind}; pub use graph::{Codec, FunctionGroup, Widget}; -pub use path::{find_output_path, Hop, OutputPath, PathError, PinSetup}; +pub use path::{choose, find_output_path, Hop, OutputPath, PathError, PinSetup, Unchosen}; pub use probe::{enumerate, CodecFault, Verbs}; pub use verb::{Address, Node, Response, Verb}; diff --git a/lib/hda/src/path.rs b/lib/hda/src/path.rs index 906a16654a1..2f3a87ea091 100644 --- a/lib/hda/src/path.rs +++ b/lib/hda/src/path.rs @@ -125,6 +125,31 @@ pub fn find_output_path(codecs: &[Codec]) -> Result { })) } +/// Which of a machine's controllers carries its audio: the one whose codecs +/// reach an output ([`find_output_path`]), with that output. +/// +/// None, or several, is [`Unchosen`], carrying what each controller's walk +/// found: choosing between two audible outputs is a default-device policy, and +/// nothing here keeps one. +pub fn choose(controllers: &[Vec]) -> Result<(usize, OutputPath), Unchosen> { + let found: Vec> = + controllers.iter().map(|codecs| find_output_path(codecs)).collect(); + if found.iter().filter(|walk| walk.is_ok()).count() != 1 { + return Err(Unchosen(found)); + } + let (index, path) = found + .into_iter() + .enumerate() + .find_map(|(i, walk)| walk.ok().map(|path| (i, path))) + .expect("counted one"); + Ok((index, path)) +} + +/// Every controller's walk, in the order [`choose`] was given them, where none +/// or several reach an output. +#[derive(Clone, PartialEq, Eq, Debug)] +pub struct Unchosen(pub Vec>); + /// `Err(None)` is a group with no wired output pin to try, which is not a /// fault — display audio is exactly that, and it is the ordinary case on a /// machine with two codecs. @@ -263,6 +288,62 @@ mod tests { use super::*; use crate::fixture; + /// The laptop's two codecs, each alone on a controller of its own: the + /// analog one and its display audio, as real words a machine sent. + fn analog_and_display() -> (Vec, Vec) { + let mut codecs = fixture::laptop(); + let display = codecs.split_off(1); + (codecs, display) + } + + #[test] + fn the_one_controller_of_the_laptop_is_chosen() { + let (index, path) = choose(&[fixture::laptop()]).unwrap(); + assert_eq!(index, 0); + assert_eq!(path, find_output_path(&fixture::laptop()).unwrap()); + } + + #[test] + fn the_amd_bench_binds_its_analog_controller_over_the_gpu_s_display_audio() { + // As soundserver read it on the recorded boot: `hda bound 03:00.6, not + // 03:00.1`, `converter 0x02 -> pin 0x14 (speaker), headphone 0x21`. + let mut bench = fixture::amd_bench(); + let (index, path) = choose(&bench).unwrap(); + assert_eq!(index, 1); + assert_eq!(path.converter, Node(0x02)); + assert_eq!(path.output.node, Node(0x14)); + assert_eq!(path.device, DefaultDevice::Speaker); + assert_eq!(path.headphone.expect("the jack").node, Node(0x21)); + assert!(matches!(find_output_path(&bench[0]), Err(PathError::NoOutputPin { .. }))); + + bench.reverse(); + assert_eq!(choose(&bench).unwrap().0, 0); + } + + #[test] + fn two_controllers_that_each_reach_an_output_are_refused_naming_both() { + let Err(Unchosen(found)) = choose(&[fixture::laptop(), fixture::qemu()]) else { + panic!("two audible outputs were chosen between"); + }; + assert_eq!(found.len(), 2); + assert!(found.iter().all(Result::is_ok)); + } + + #[test] + fn controllers_none_of_which_reaches_an_output_are_refused_naming_both() { + let (_, display) = analog_and_display(); + let Err(Unchosen(found)) = choose(&[display, fixture::synthetic_dead_end()]) else { + panic!("a controller with no output was chosen"); + }; + assert_eq!( + found, + [ + Err(PathError::NoOutputPin { codecs: alloc::vec![Address::new(2).unwrap()] }), + Err(PathError::NoConverter { pin: Node(0x20) }), + ] + ); + } + #[test] fn the_laptop_picks_its_internal_speaker_and_the_jack_that_shares_a_converter() { let codecs = fixture::laptop(); diff --git a/lib/hda/src/verb.rs b/lib/hda/src/verb.rs index e067a575099..7fce84c454b 100644 --- a/lib/hda/src/verb.rs +++ b/lib/hda/src/verb.rs @@ -106,6 +106,7 @@ pub const SET_CONNECTION_SELECT: u16 = 0x701; pub const GET_CONFIG_DEFAULT: u16 = 0xF1C; pub const GET_PIN_SENSE: u16 = 0xF09; pub const SET_AMP_GAIN_MUTE: u16 = 0x3; +pub const GET_AMP_GAIN_MUTE: u16 = 0xB; pub const SET_CONVERTER_FORMAT: u16 = 0x2; pub const PARAM_VENDOR_ID: u8 = 0x00; diff --git a/sdk/abi/src/hda.rs b/sdk/abi/src/hda.rs index bfc516ea9dd..265bb8a208e 100644 --- a/sdk/abi/src/hda.rs +++ b/sdk/abi/src/hda.rs @@ -7,15 +7,47 @@ //! against an allow-list and refused by name. Nothing here names a physical //! address. //! +//! **The kernel binds no controller by itself.** It lists every controller it +//! can bind and takes the stream's memory once; the driver walks each listed +//! controller's codecs over the register calls, chooses one, and binds it with +//! [`syscall::hda_bind`]. From then on the claim's list is the bound +//! controller alone, at index 0. +//! //! Completions come back as [`AudioCompletionRecord`](crate::audio::AudioCompletionRecord), //! whose mask is derived from a position read in the interrupt handler. //! //! [`syscall::device_reg_read`]: crate::syscall::device_reg_read //! [`syscall::device_reg_write`]: crate::syscall::device_reg_write +//! [`syscall::hda_bind`]: crate::syscall::hda_bind + +/// The most controllers a claim lists. A machine with more live ones is +/// refused by name, naming each. +pub const MAX_CONTROLLERS: usize = 4; crate::user_safe! { - /// The controller and stream the kernel brought up, as the driver needs to see - /// them. + /// One controller the claim lists. + #[derive(Clone, Copy)] + pub struct HdaController { + /// Byte offset of the output stream descriptor inside this controller's + /// register window, so the driver names `SDnCTL` and `SDnFMT` by the + /// same arithmetic the allow-list does. + pub stream_offset: u32, + /// Every codec address `STATESTS` reported, one bit per link address. + /// The driver enumerates all of them and chooses by capability; the + /// kernel read this register to know the link is alive and decided + /// nothing with it. + pub statests: u16, + /// The PCI bus, device and function, for the lines that name it. + pub bus: u8, + pub dev: u8, + pub func: u8, + pub _pad: [u8; 3], + } +} + +crate::user_safe! { + /// The stream the kernel took the memory of, and the controllers it can be + /// bound to, as the driver needs to see them. /// /// No register window and no physical address: everything here is a shared /// memory token, a shape the driver has to know to fill the ring, or a number @@ -27,19 +59,70 @@ crate::user_safe! { /// list already pointing at them. pub pcm: crate::RawHandle, pub period_bytes: u32, - /// Byte offset of the output stream descriptor inside the register window, - /// so the driver names `SDnCTL` and `SDnFMT` by the same arithmetic the - /// allow-list does. - pub stream_offset: u32, - /// Every codec address `STATESTS` reported, one bit per link address. The - /// driver enumerates all of them and chooses by capability; the kernel - /// read this register to know the link is alive and decided nothing with - /// it. - pub statests: u16, - /// The stream tag the kernel put in the descriptor. It has to reach the + /// The stream tag the kernel puts in the descriptor. It has to reach the /// codec's converter, and sending that verb is the driver's. pub stream_tag: u8, pub periods: u8, + /// How many of [`controllers`](Self::controllers) are listed: every + /// controller the kernel can bind, in PCI order, or the bound one + /// alone once a bind has run. + pub count: u8, + pub _pad: u8, + pub controllers: [HdaController; MAX_CONTROLLERS], } } +impl HdaInfo { + /// The listed controllers. + /// + /// A `count` past [`MAX_CONTROLLERS`] is the kernel's bug, and it panics + /// here rather than reading past the list. + pub fn listed(&self) -> &[HdaController] { + &self.controllers[..self.count as usize] + } +} + +/// One register of one listed controller: its index in the claim's current +/// list, and a byte offset inside that controller's register window. +/// +/// The register calls carry it as their offset word, decoded once at the +/// boundary by [`Reg::from_wire`]. +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub struct Reg { + pub controller: u8, + pub offset: u32, +} + +impl Reg { + /// The offset word the register calls carry: the offset in the low 32 + /// bits, the controller's index above them. + pub const fn wire(self) -> u64 { + (self.controller as u64) << 32 | self.offset as u64 + } + + /// The register a word names, or `None` for a word whose controller index + /// no list can hold. + pub const fn from_wire(word: u64) -> Option { + let controller = word >> 32; + if controller >= MAX_CONTROLLERS as u64 { + return None; + } + Some(Self { controller: controller as u8, offset: word as u32 }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn a_register_word_is_refused_unless_its_controller_index_fits_a_list() { + for controller in 0..MAX_CONTROLLERS as u8 { + let reg = Reg { controller, offset: 0x68 }; + assert_eq!(Reg::from_wire(reg.wire()), Some(reg)); + } + assert_eq!(Reg::from_wire((MAX_CONTROLLERS as u64) << 32 | 0x68), None); + assert_eq!(Reg::from_wire(1 << 40), None); + assert_eq!(Reg::from_wire(u64::MAX), None); + } +} diff --git a/sdk/abi/src/syscall.rs b/sdk/abi/src/syscall.rs index d2e9c5a3512..0514d57bd49 100644 --- a/sdk/abi/src/syscall.rs +++ b/sdk/abi/src/syscall.rs @@ -292,19 +292,23 @@ pub const SYS_TRACE_READ: u64 = 127; /// See [`acpi_access`], [`acpi_lock_take`] and [`acpi_s5`]. pub const SYS_ACPI: u64 = 128; +/// Bind the `hda-audio` claim's stream to the listed controller it names. See +/// [`hda_bind`]. +pub const SYS_HDA_BIND: u64 = 129; + /// Bins in the per-process syscall profile — one for every number this ABI /// issues, and one at the end for every number it does not. /// /// **The profile's parts sum to its total, and that is the whole requirement.** /// A bin array narrower than the ABI reaches drops calls out of the line while /// the total goes on counting them. -pub const SYSCALL_PROFILE_BINS: usize = 130; +pub const SYSCALL_PROFILE_BINS: usize = 131; /// Where a number this ABI does not issue is counted. Merging is a degradation /// a reader can see in the line; dropping is one nobody can. pub const SYSCALL_PROFILE_OTHER: usize = SYSCALL_PROFILE_BINS - 1; -const _: () = assert!(SYS_ACPI < SYSCALL_PROFILE_OTHER as u64); +const _: () = assert!(SYS_HDA_BIND < SYSCALL_PROFILE_OTHER as u64); pub const WNOHANG: u64 = 1; @@ -1778,16 +1782,17 @@ impl RegWidth { /// Read one register of the device `handle` claims. /// -/// `offset` is a byte offset inside that device's register window. The kernel -/// checks it against the device's read allow-list and refuses anything else by -/// name; there is no way to name an address here and no way to reach a -/// register the list does not carry. +/// `offset` names a register inside that device's register window, as the +/// claim's class spells it: a byte offset, or for an HDA claim a +/// [`crate::hda::Reg`]'s word. The kernel checks it against the device's read +/// allow-list and refuses anything else by name; there is no way to name an +/// address here and no way to reach a register the list does not carry. pub fn device_reg_read( handle: RawHandle, - offset: u32, + offset: u64, width: RegWidth, ) -> Result { - check(syscall(SYS_DEVICE_REG_READ, handle.0 as u64, offset as u64, width.bytes(), 0)) + check(syscall(SYS_DEVICE_REG_READ, handle.0 as u64, offset, width.bytes(), 0)) .map(|v| v as u32) } @@ -1799,20 +1804,32 @@ pub fn device_reg_read( /// the failure mode a refusal list does not have. pub fn device_reg_write( handle: RawHandle, - offset: u32, + offset: u64, width: RegWidth, value: u32, ) -> Result<(), SyscallError> { check(syscall( SYS_DEVICE_REG_WRITE, handle.0 as u64, - offset as u64, + offset, width.bytes(), value as u64, )) .map(|_| ()) } +/// Bind the `hda-audio` claim's stream to the listed controller at +/// `controller`, and put every other listed controller back into reset. +/// +/// One-shot for the boot: once a bind has run, the claim's list is the bound +/// controller alone, at index 0, and binding index 0 again answers success. +/// [`SyscallError::WouldBlock`] while another bind runs, and +/// [`SyscallError::Io`] for every call once a bind failed on the device; the +/// kernel's `hda:` line says why. +pub fn hda_bind(claim: RawHandle, controller: u8) -> Result<(), SyscallError> { + check_unit(syscall(SYS_HDA_BIND, claim.0 as u64, controller as u64, 0, 0)) +} + // Ports and namespaces /// Both ends of a fresh port. diff --git a/sdk/toyos/src/audio.rs b/sdk/toyos/src/audio.rs index 614652b0283..cb2cd8d61ac 100644 --- a/sdk/toyos/src/audio.rs +++ b/sdk/toyos/src/audio.rs @@ -11,6 +11,23 @@ pub const MSG_STREAM_SET_VOLUME: u32 = 3; pub const MSG_STREAM_CLOSE: u32 = 4; /// soundserver rejected `MSG_STREAM_OPEN` (unsupported format/channels/rate). pub const MSG_STREAM_ERROR: u32 = 5; +/// On a connection that carries no stream: ask what device soundserver runs. +/// Answered once with [`MSG_DEVICE_DESCRIBED`] by a write that does not wait, +/// and the connection is closed. +pub const MSG_DEVICE_DESCRIBE: u32 = 6; +/// [`DeviceDescription`]. +pub const MSG_DEVICE_DESCRIBED: u32 = 7; + +/// On soundserver's `volume` port, which the compositor alone holds: move the +/// master level by [`VolumeStep::delta`] positions. A step while muted unmutes. +pub const MSG_VOLUME_STEP: u32 = 1; +/// On the `volume` port: mute, or unmute at the level it had. A bare header. +pub const MSG_VOLUME_MUTE: u32 = 2; +/// The answer to either: [`Volume`], read back from the device after the set. +pub const MSG_VOLUME: u32 = 3; +/// The answer to either where the device has no master, or a set did not +/// read back: a bare header. +pub const MSG_VOLUME_UNAVAILABLE: u32 = 4; /// The only sample format currently implemented end-to-end. pub const FORMAT_S16LE: u16 = 0; @@ -31,6 +48,33 @@ crate::ipc_payload! { pub device_sample_rate: u32, pub device_channels: u16, pub slot_count: u16, + /// The device frames soundserver fades a closed stream out over, so a + /// client can derive how long it may take to let go of one. + pub fade_frames: u32, + } + + /// The device soundserver runs, and the client rates it takes: a client at + /// any of them is resampled to the device's rate. + pub struct DeviceDescription { + pub sample_rate: u32, + pub period_frames: u32, + pub min_client_rate: u32, + pub max_client_rate: u32, + pub channels: u16, + pub _pad: u16, + } + + pub struct VolumeStep { + pub delta: i32, + } + + /// The master as the device reads back: `position` positions below the + /// top, of `bottom`, and whether it is muted (`1`) or not (`0`). + pub struct Volume { + pub position: u8, + pub bottom: u8, + pub muted: u8, + pub _pad: u8, } pub struct StreamSetVolume { @@ -195,6 +239,20 @@ pub struct AudioStream { period_frames: u32, device_sample_rate: u32, device_channels: u16, + slot_count: u32, + fade_frames: u32, +} + +/// What device soundserver runs, asked on a connection of its own before any +/// stream exists. +pub fn describe_device() -> Result { + let control = AudioStream::connect_soundserver()?; + control.signal(MSG_DEVICE_DESCRIBE).map_err(AudioError::Ipc)?; + let header = control.recv_header().map_err(AudioError::Ipc)?; + match header.msg_type { + MSG_DEVICE_DESCRIBED => control.recv_payload(&header).map_err(AudioError::Ipc), + other => Err(AudioError::Protocol(other)), + } } impl AudioStream { @@ -227,6 +285,8 @@ impl AudioStream { period_frames: resp.client_period_frames, device_sample_rate: resp.device_sample_rate, device_channels: resp.device_channels, + slot_count, + fade_frames: resp.fade_frames, }) } @@ -261,6 +321,16 @@ impl AudioStream { self.device_channels } + /// The periods of the stream's ring. + pub fn slot_count(&self) -> u32 { + self.slot_count + } + + /// The device frames soundserver fades this stream out over once it closes. + pub fn fade_frames(&self) -> u32 { + self.fade_frames + } + pub fn set_volume(&self, gain: f32) -> Result<(), AudioError> { self.control.send(MSG_STREAM_SET_VOLUME, &StreamSetVolume { gain }) .map_err(AudioError::Ipc) diff --git a/sdk/toyos/src/device.rs b/sdk/toyos/src/device.rs index d5e1900e74a..2f5fad9e98e 100644 --- a/sdk/toyos/src/device.rs +++ b/sdk/toyos/src/device.rs @@ -136,7 +136,7 @@ impl PciDev { offset: u32, width: toyos_abi::syscall::RegWidth, ) -> Result { - syscall::device_reg_read(self.0.as_handle(), offset, width) + syscall::device_reg_read(self.0.as_handle(), u64::from(offset), width) } /// Map memory BAR `bar`, bounded to the `bar_bytes` the description gave. @@ -276,12 +276,13 @@ impl AsHandle for PartitionDev { fn as_handle(&self) -> RawHandle { self.0.as_handle() } } -/// An Intel HDA controller the kernel brought up and drives no policy on. +/// The Intel HDA controllers the kernel brought up and drives no policy on. /// /// What the claimant gets is a PCM ring it may write, an interrupt it may wait -/// on, and two register calls checked against the kernel's allow-list. It gets -/// no register window and no physical address, so there is nothing here that -/// can point the device at memory. +/// on, two register calls checked against the kernel's allow-list, and the +/// choice of which listed controller the stream is bound to. It gets no +/// register window and no physical address, so there is nothing here that can +/// point a device at memory. pub struct HdaDev(pub(crate) Device); impl HdaDev { @@ -317,19 +318,25 @@ impl HdaDev { pub fn reg_read( &self, - offset: u32, + reg: toyos_abi::hda::Reg, width: toyos_abi::syscall::RegWidth, ) -> Result { - syscall::device_reg_read(self.0.as_handle(), offset, width) + syscall::device_reg_read(self.0.as_handle(), reg.wire(), width) } pub fn reg_write( &self, - offset: u32, + reg: toyos_abi::hda::Reg, width: toyos_abi::syscall::RegWidth, value: u32, ) -> Result<(), SyscallError> { - syscall::device_reg_write(self.0.as_handle(), offset, width, value) + syscall::device_reg_write(self.0.as_handle(), reg.wire(), width, value) + } + + /// Bind the stream to the listed controller at `controller`; see + /// [`syscall::hda_bind`]. From then on that controller is index 0. + pub fn bind(&self, controller: u8) -> Result<(), SyscallError> { + syscall::hda_bind(self.0.as_handle(), controller) } } diff --git a/system/compositor/desktop/src/input.rs b/system/compositor/desktop/src/input.rs index b95ea265743..b150bd98e05 100644 --- a/system/compositor/desktop/src/input.rs +++ b/system/compositor/desktop/src/input.rs @@ -5,6 +5,7 @@ use crate::hit::{hit_test, Hit}; use crate::layout::{set_mode, Desk}; use crate::rect::{Point, Rect}; use crate::stack::Stack; +use crate::volume::VolumeKey; use crate::window::{WindowId, WindowMode}; /// HID usage codes for the keys the desktop keeps for itself. @@ -22,6 +23,9 @@ mod usage { pub const LEFT: u8 = 0x50; pub const DOWN: u8 = 0x51; pub const UP: u8 = 0x52; + pub const MUTE: u8 = 0x7F; + pub const VOLUME_UP: u8 = 0x80; + pub const VOLUME_DOWN: u8 = 0x81; } /// What a key transition means to the desktop. @@ -44,6 +48,8 @@ pub enum KeyAction { CloseFocused, Paste, SpawnTerminal, + /// A volume key's press: the desktop's, whatever has focus. + Volume(VolumeKey), } /// What `ev` does, given the focused window's mode and whether the launcher is @@ -57,13 +63,28 @@ pub enum KeyAction { /// saw the press of a key it is holding needs the release of it too, and /// swallowing that leaves the window believing the key is still down. /// +/// **The volume keys are the desktop's**: a press is [`KeyAction::Volume`], +/// and its release is swallowed, since no window saw the press. +/// /// **While a prompt is up every transition is the prompt's**, the focused /// window's, and no chord is the desktop's: nothing closes, cycles, /// minimizes or opens a window under it, and nothing is pasted into it, the /// clipboard being any client's to write. [`PromptGate`] says which of them /// reach it. pub fn key_action(ev: RawKeyEvent, focused: Option, launcher_open: bool, prompt: bool) -> KeyAction { - if prompt || !ev.pressed() { + if prompt { + return KeyAction::Forward; + } + let volume = match ev.keycode { + usage::MUTE => Some(VolumeKey::Mute), + usage::VOLUME_UP => Some(VolumeKey::Up), + usage::VOLUME_DOWN => Some(VolumeKey::Down), + _ => None, + }; + if let Some(key) = volume { + return if ev.pressed() { KeyAction::Volume(key) } else { KeyAction::Ignore }; + } + if !ev.pressed() { return KeyAction::Forward; } if launcher_open && ev.keycode == usage::ESCAPE { @@ -478,6 +499,23 @@ mod tests { } } + #[test] + fn a_volume_key_s_press_is_the_desktop_s_and_its_release_is_swallowed() { + for (code, want) in [ + (usage::MUTE, VolumeKey::Mute), + (usage::VOLUME_UP, VolumeKey::Up), + (usage::VOLUME_DOWN, VolumeKey::Down), + ] { + for focused in [None, Some(WindowMode::Normal)] { + assert_eq!(key_action(key(code, 0), focused, false, false), KeyAction::Volume(want)); + assert_eq!(key_action(key(code, MOD_RELEASED), focused, false, false), KeyAction::Ignore); + } + // Under a prompt every transition is the prompt's. + assert_eq!(key_action(key(code, 0), None, false, true), KeyAction::Forward); + assert_eq!(key_action(key(code, MOD_RELEASED), None, false, true), KeyAction::Forward); + } + } + #[test] fn escape_closes_the_launcher_only_while_it_is_open() { let esc = key(usage::ESCAPE, 0); diff --git a/system/compositor/desktop/src/lib.rs b/system/compositor/desktop/src/lib.rs index db548978ea2..92c941e043c 100644 --- a/system/compositor/desktop/src/lib.rs +++ b/system/compositor/desktop/src/lib.rs @@ -32,6 +32,7 @@ pub mod plan; pub mod rect; pub mod stack; pub mod taskbar; +pub mod volume; pub mod window; pub use budget::{create_verdict, max_windows, pending_admits, window_bytes, Port, Verdict}; diff --git a/system/compositor/desktop/src/volume.rs b/system/compositor/desktop/src/volume.rs new file mode 100644 index 00000000000..76fd10c376a --- /dev/null +++ b/system/compositor/desktop/src/volume.rs @@ -0,0 +1,233 @@ +//! The volume keys' half of the desktop: the requests the compositor keeps in +//! flight to soundserver's `volume` port, and the overlay that shows each +//! answer. +//! +//! **The compositor never waits on soundserver.** One request is in flight at +//! a time; presses that arrive meanwhile are added up here and sent as one when +//! the answer comes. + +use alloc::format; +use alloc::string::String; + +use crate::layout::Desk; +use crate::rect::Rect; + +/// Which volume key was pressed. +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub enum VolumeKey { + Mute, + Up, + Down, +} + +/// One request on the `volume` port. +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub enum Request { + /// Move the level by this many positions down; a step unmutes. + Step(i32), + /// Mute, or unmute at the level it had. + Mute, +} + +/// The presses not yet sent, in the one order that keeps their meaning: a +/// step unmutes, so only the mutes pressed after the last step decide what +/// follows it. +#[derive(Default)] +pub struct Requests { + in_flight: bool, + /// Positions down, summed over every step pressed since the last send. + steps: i32, + stepped: bool, + /// Mute presses since the last step, whose parity is all that counts. + mutes: u32, +} + +impl Requests { + /// A press; answers the request to send now, if none is in flight. + pub fn press(&mut self, key: VolumeKey) -> Option { + match key { + VolumeKey::Mute => self.mutes += 1, + // Up raises the level, which is a position nearer the top. + VolumeKey::Up | VolumeKey::Down => { + self.steps = self.steps.saturating_add(if key == VolumeKey::Up { -1 } else { 1 }); + self.stepped = true; + self.mutes = 0; + } + } + if self.in_flight { None } else { self.take() } + } + + /// The request in flight was answered; answers the next to send. + pub fn answered(&mut self) -> Option { + self.in_flight = false; + self.take() + } + + /// The connection ended: nothing is in flight on it, and what was pressed + /// and not sent goes on the next one. + pub fn disconnected(&mut self) { + self.in_flight = false; + } + + fn take(&mut self) -> Option { + let request = if self.stepped { + self.stepped = false; + Some(Request::Step(core::mem::take(&mut self.steps))) + } else if core::mem::take(&mut self.mutes) % 2 == 1 { + Some(Request::Mute) + } else { + None + }; + self.in_flight = request.is_some(); + request + } +} + +/// What an answer from soundserver says. +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub enum Shown { + /// `position` positions below the top, of `bottom`. + Level { position: u8, bottom: u8 }, + Muted, + Unavailable, +} + +impl Shown { + /// The overlay's text: the level counted up from the bottom. + pub fn text(self) -> String { + match self { + Self::Level { position, bottom } => { + format!("Volume {}/{bottom}", bottom.saturating_sub(position)) + } + Self::Muted => String::from("Volume muted"), + Self::Unavailable => String::from("Volume unavailable"), + } + } +} + +/// How long the overlay stays after the last answer, in nanoseconds. +pub const OVERLAY_HOLD_NANOS: u64 = 1_500_000_000; + +/// The overlay: shown by an answer, hidden [`OVERLAY_HOLD_NANOS`] after the last. +#[derive(Default)] +pub struct Overlay { + shown: Option<(Shown, u64)>, +} + +impl Overlay { + /// An answer arrived at `now`. + pub fn answered(&mut self, shown: Shown, now: u64) { + self.shown = Some((shown, now.saturating_add(OVERLAY_HOLD_NANOS))); + } + + /// What it shows at `now`, if it is up. + pub fn visible(&self, now: u64) -> Option { + self.shown.filter(|&(_, until)| now < until).map(|(shown, _)| shown) + } + + /// Forget an overlay that has hidden at `now`; answers whether it did, so + /// the compositor damages where it was. + pub fn expire(&mut self, now: u64) -> bool { + if self.shown.is_some_and(|(_, until)| now >= until) { + self.shown = None; + return true; + } + false + } +} + +/// The widest overlay text, in characters. +const OVERLAY_CHARS: i32 = 18; + +/// Where the overlay is drawn: above the taskbar, centred, one taskbar high. +pub fn overlay_rect(desk: &Desk) -> Rect { + let strip = desk.taskbar(0).strip(); + let w = OVERLAY_CHARS * desk.font_w + desk.chrome.taskbar; + let h = desk.chrome.taskbar; + let x0 = strip.x0 + (strip.w() - w) / 2; + Rect::corners(x0, strip.y0 - h - desk.chrome.taskbar_padding * 2, x0 + w, strip.y0 - desk.chrome.taskbar_padding * 2) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn one_request_is_in_flight_and_the_presses_meanwhile_go_as_one() { + let mut requests = Requests::default(); + assert_eq!(requests.press(VolumeKey::Up), Some(Request::Step(-1))); + assert_eq!(requests.press(VolumeKey::Down), None); + assert_eq!(requests.press(VolumeKey::Down), None); + assert_eq!(requests.press(VolumeKey::Down), None); + assert_eq!(requests.answered(), Some(Request::Step(3))); + assert_eq!(requests.answered(), None); + // Nothing in flight, so the next press goes at once. + assert_eq!(requests.press(VolumeKey::Mute), Some(Request::Mute)); + } + + #[test] + fn only_the_mutes_after_the_last_step_follow_it() { + let mut requests = Requests::default(); + assert_eq!(requests.press(VolumeKey::Mute), Some(Request::Mute)); + // A mute, a step that unmutes, then a mute: the step, then the mute. + requests.press(VolumeKey::Mute); + requests.press(VolumeKey::Up); + requests.press(VolumeKey::Mute); + assert_eq!(requests.answered(), Some(Request::Step(-1))); + assert_eq!(requests.answered(), Some(Request::Mute)); + assert_eq!(requests.answered(), None); + // Two mutes cancel. + assert_eq!(requests.press(VolumeKey::Mute), Some(Request::Mute)); + requests.press(VolumeKey::Mute); + requests.press(VolumeKey::Mute); + assert_eq!(requests.answered(), None); + } + + #[test] + fn a_connection_that_ends_sends_what_was_pressed_on_the_next() { + let mut requests = Requests::default(); + assert_eq!(requests.press(VolumeKey::Up), Some(Request::Step(-1))); + requests.disconnected(); + assert_eq!(requests.press(VolumeKey::Up), Some(Request::Step(-1))); + } + + #[test] + fn an_answer_shows_the_overlay_and_it_hides_a_hold_after_the_last() { + let mut overlay = Overlay::default(); + assert_eq!(overlay.visible(0), None); + let level = Shown::Level { position: 2, bottom: 22 }; + overlay.answered(level, 100); + assert_eq!(overlay.visible(100), Some(level)); + // A second answer before the first's hold restarts it. + overlay.answered(Shown::Muted, 1_000_000_000); + assert_eq!(overlay.visible(100 + OVERLAY_HOLD_NANOS), Some(Shown::Muted)); + assert!(!overlay.expire(1_000_000_000 + OVERLAY_HOLD_NANOS - 1)); + assert_eq!(overlay.visible(1_000_000_000 + OVERLAY_HOLD_NANOS), None); + assert!(overlay.expire(1_000_000_000 + OVERLAY_HOLD_NANOS)); + assert!(!overlay.expire(u64::MAX)); + } + + #[test] + fn the_overlay_says_the_level_from_the_bottom_and_fits_its_box() { + assert_eq!(Shown::Level { position: 0, bottom: 22 }.text(), "Volume 22/22"); + assert_eq!(Shown::Level { position: 22, bottom: 22 }.text(), "Volume 0/22"); + for shown in [Shown::Level { position: 255, bottom: 255 }, Shown::Muted, Shown::Unavailable] { + assert!(shown.text().chars().count() <= OVERLAY_CHARS as usize); + } + } + + #[test] + fn the_overlay_sits_above_the_taskbar_centred() { + let desk = crate::layout::Desk { + screen: Rect::new(0, 0, 1920, 1080), + chrome: crate::layout::Chrome::DEFAULT, + font_w: 8, + apps: 0, + }; + let r = overlay_rect(&desk); + let strip = desk.taskbar(0).strip(); + assert!(r.y1 <= strip.y0); + assert_eq!(r.x0 - strip.x0, strip.x1 - r.x1); + assert!(r.w() as usize >= OVERLAY_CHARS as usize * 8); + } +} diff --git a/system/compositor/src/main.rs b/system/compositor/src/main.rs index b56e1873fd7..9d386ead76b 100644 --- a/system/compositor/src/main.rs +++ b/system/compositor/src/main.rs @@ -49,22 +49,21 @@ pub const INPUT_POLL_HANDLES: u32 = 1 + toyos_input::wire::CONNECTIONS as u32; /// Hard ceiling on live client windows, from the poller rather than from memory. /// -/// Every window's handle is registered in the same batch as the four fixed -/// ones, the pending connections, the prompt's two, the clipboard's two and -/// the input port's, and [`Poller::MAX_HANDLES`] is the widest set one poller -/// can carry. Unlike the memory budget this does not move when the +/// Every window's handle is registered in the same batch as every other +/// handle the compositor watches, counted below, and [`Poller::MAX_HANDLES`] +/// is the widest set one poller can carry. Unlike the memory budget this does not move when the /// resolution does, which is why the poller is sized from it: /// `MSG_SET_RESOLUTION` can make windows cheaper mid-run, and a poller sized /// for the old screen would then be too small. At any resolution this machine /// can actually scan out, the memory budget is far below this and is what /// binds. -pub const MAX_WINDOW_SLOTS: u32 = - Poller::MAX_HANDLES - - FIXED_POLL_HANDLES - - MAX_PENDING_CONNS - - PROMPT_POLL_HANDLES - - CLIPBOARD_POLL_HANDLES - - INPUT_POLL_HANDLES; +pub const MAX_WINDOW_SLOTS: u32 = Poller::MAX_HANDLES + - FIXED_POLL_HANDLES + - MAX_PENDING_CONNS + - PROMPT_POLL_HANDLES + - CLIPBOARD_POLL_HANDLES + - INPUT_POLL_HANDLES + - VOLUME_POLL_HANDLES; /// The prompt's own: its connection waiting for its first frame, and its /// window, which no client's ceiling counts (`toyos_desktop::pending_admits`, @@ -75,6 +74,9 @@ pub const PROMPT_POLL_HANDLES: u32 = 2; /// for its first frame or watching (`toyos_desktop::clipboard`). pub const CLIPBOARD_POLL_HANDLES: u32 = 2; +/// soundserver's `volume` port: the one connection the volume keys' requests go on. +pub const VOLUME_POLL_HANDLES: u32 = 1; + /// What the launcher offers before `/apps` is read: the label it shows and the /// program it starts. pub const BUILT_IN_APPS: &[(&str, &str)] = diff --git a/system/compositor/src/render.rs b/system/compositor/src/render.rs index 194d369847a..084daaacc1b 100644 --- a/system/compositor/src/render.rs +++ b/system/compositor/src/render.rs @@ -313,6 +313,16 @@ fn draw_launcher( } } +/// The volume overlay: `text` centred in `rect`. Drawn whole whenever a +/// region touches it, so a region that cuts it is repainted whole. +pub fn draw_overlay(surface: &Framebuffer, font: &font::Font, rect: Rect, text: &str) { + fill(surface, rect, LAUNCHER_BG); + let text_w = (text.chars().count() * font.width()) as i32; + let x = rect.x0 + (rect.w() - text_w).max(0) / 2; + let y = rect.y0 + (rect.h() - GLYPH_H) / 2; + font.draw_string(surface, x as usize, y as usize, text, LAUNCHER_TEXT, LAUNCHER_BG); +} + /// Scale an RGB image to the screen and convert to its pixel format. pub fn scale_wallpaper( src: &[u8], diff --git a/system/compositor/src/session.rs b/system/compositor/src/session.rs index e0e2c6657c5..49af3da6f2d 100644 --- a/system/compositor/src/session.rs +++ b/system/compositor/src/session.rs @@ -26,6 +26,8 @@ use toyos_abi::RawHandle; use toyos_input::wire::{self, Record, CONNECTIONS, KEEP, KEY_LEN, POINTER_LEN}; use toyos_input::{Merge, Pointer, Slot, State}; use toyos_desktop::clipboard::{Landing, Watch}; +use toyos_desktop::volume::{overlay_rect, Overlay, Request, Requests, Shown}; +use toyos::audio::{Volume, VolumeStep, MSG_VOLUME, MSG_VOLUME_MUTE, MSG_VOLUME_STEP, MSG_VOLUME_UNAVAILABLE}; use toyos_desktop::{ cursor_from_abs, cursor_style, fold_mouse, hit_test, key_action, set_mode, tab_action, Chrome, CursorStyle, Damage, Desk, Grab, Held, Hit, KeyAction, Level, Point, Port, PromptGate, Rect, @@ -43,8 +45,13 @@ use crate::stats::FrameTotals; use crate::{ launcher_apps, CLIPBOARD_POLL_HANDLES, DOUBLE_CLICK_TIME, DRAIN_BUDGET, FIXED_POLL_HANDLES, FLAG_HARDWARE_CURSOR, FRAME_INTERVAL, INPUT_POLL_HANDLES, MAX_WINDOW_SLOTS, PROMPT_POLL_HANDLES, + VOLUME_POLL_HANDLES, }; +/// The widest answer on the `volume` port, plus one, so an over-long frame is +/// refused rather than truncated into an answer. +const VOLUME_KEPT_PAYLOAD: usize = std::mem::size_of::() + 1; + /// The most frames one source's turn takes before the others are served. const SOURCE_FRAMES: usize = 64; @@ -176,6 +183,14 @@ pub struct Session { /// Tokens the last `wait` reported ready. ready: Vec, + /// soundserver's `volume` port, connected on the first volume key and + /// again after it ends; the requests on it; and the overlay its answers show. + volume: Option<(toyos::Connection, ipc::FrameRx)>, + volume_requests: Requests, + overlay: Overlay, + /// What the overlay's clock counts from. + epoch: Instant, + totals: FrameTotals, cached_stats: SystemStats, prev_busy_ticks: u64, @@ -268,10 +283,7 @@ impl Session { total_mem / (1024 * 1024), ); - // Sized for the slot ceiling rather than for `max_windows`: the batch - // between two `wait` calls is the four fixed registrations, one per - // live window and one per pending connection, the prompt's two, the - // clipboard's two and the input port's beside them, and + // Sized for the slot ceiling rather than for `max_windows`: // `MSG_SET_RESOLUTION` can raise `max_windows` mid-run. let poller = Poller::new( FIXED_POLL_HANDLES @@ -279,7 +291,8 @@ impl Session { + MAX_PENDING_CONNS + PROMPT_POLL_HANDLES + CLIPBOARD_POLL_HANDLES - + INPUT_POLL_HANDLES, + + INPUT_POLL_HANDLES + + VOLUME_POLL_HANDLES, ); poller.watch(&inputs, READABLE, inputs.as_handle().0 as u64); poller.watch(&kb, READABLE, kb.as_handle().0 as u64); @@ -349,6 +362,10 @@ impl Session { prev_busy_ticks: 0, prev_total_ticks: 0, last_taskbar_update: now, + volume: None, + volume_requests: Requests::default(), + overlay: Overlay::default(), + epoch: now, } } @@ -375,6 +392,9 @@ impl Session { } } self.tick_taskbar(); + if self.overlay.expire(self.overlay_now()) { + self.damage.add(overlay_rect(&self.desk)); + } self.present(); self.prompt_gone(); } @@ -395,6 +415,7 @@ impl Session { let accept_ready = self.is_ready(self.acceptor.as_handle()); let prompt_ready = self.is_ready(self.prompts.as_handle()); let clipboard_ready = self.is_ready(self.clipboards.as_handle()); + let volume_ready = self.volume.as_ref().is_some_and(|(conn, _)| self.is_ready(conn.as_handle())); let client_ready = self.stack.iter().any(|w| self.is_ready(w.client.conn.as_handle())) || self.pending.iter().any(|p| self.is_ready(p.conn.as_handle())) || self.watcher.as_ref().is_some_and(|w| self.is_ready(w.conn.as_handle())); @@ -421,6 +442,7 @@ impl Session { && !prompt_ready && !clipboard_ready && !client_ready + && !volume_ready { return false; } @@ -447,6 +469,9 @@ impl Session { if clipboard_ready { self.accept(Port::Clipboard); } + if volume_ready { + self.volume_answers(); + } let frames = self.take_frames(); self.dispatch(frames); self.watched(); @@ -456,6 +481,81 @@ impl Session { true } + /// Nanoseconds since the session's epoch, the clock the overlay is timed by. + fn overlay_now(&self) -> u64 { + self.epoch.elapsed().as_nanos() as u64 + } + + /// One request to soundserver's `volume` port, by a write that does not + /// wait; connected first if it is not. A refusal ends the connection, by + /// name, and what was pressed goes on the next one. + fn send_volume(&mut self, request: Request) { + if self.volume.is_none() { + match endow::service("volume") { + Ok(conn) => { + self.poller.watch(&conn, READABLE, conn.as_handle().0 as u64); + self.volume = Some((conn, ipc::FrameRx::new())); + } + Err(e) => { + eprintln!("compositor: soundserver's volume port did not connect ({e:?})"); + self.volume_requests.disconnected(); + return; + } + } + } + let (conn, _) = self.volume.as_ref().expect("connected above"); + let sent = match request { + Request::Step(delta) => conn.try_send(MSG_VOLUME_STEP, &VolumeStep { delta }), + Request::Mute => conn.try_send_bytes(MSG_VOLUME_MUTE, &[]), + }; + if let Err(e) = sent { + eprintln!("compositor: a volume request was not taken ({e:?}); reconnecting on the next key"); + self.volume = None; + self.volume_requests.disconnected(); + } + } + + /// soundserver's answers, each shown on the overlay, and the next request + /// sent behind each. + fn volume_answers(&mut self) { + loop { + let Some((conn, rx)) = self.volume.as_mut() else { return }; + let shown = match rx.pump(conn) { + RxStep::Idle => return, + RxStep::Frame { msg_type: MSG_VOLUME, payload_len } => { + match ipc::decode_payload::(rx.payload(payload_len)) { + Ok(v) if v.muted != 0 => Shown::Muted, + Ok(v) => Shown::Level { position: v.position, bottom: v.bottom }, + Err(e) => { + eprintln!("compositor: a volume answer did not decode ({e:?}); disconnecting"); + self.volume = None; + self.volume_requests.disconnected(); + return; + } + } + } + RxStep::Frame { msg_type: MSG_VOLUME_UNAVAILABLE, .. } => Shown::Unavailable, + RxStep::Frame { msg_type, .. } => { + eprintln!("compositor: soundserver's volume port sent message {msg_type}; disconnecting"); + self.volume = None; + self.volume_requests.disconnected(); + return; + } + RxStep::Eof | RxStep::Malformed => { + eprintln!("compositor: soundserver's volume port ended; reconnecting on the next key"); + self.volume = None; + self.volume_requests.disconnected(); + return; + } + }; + self.overlay.answered(shown, self.overlay_now()); + self.damage.add(overlay_rect(&self.desk)); + if let Some(next) = self.volume_requests.answered() { + self.send_volume(next); + } + } + } + fn is_ready(&self, handle: RawHandle) -> bool { self.ready.contains(&(handle.0 as u64)) } @@ -526,6 +626,11 @@ impl Session { } } KeyAction::SpawnTerminal => self.launch("/system/bin/terminal"), + KeyAction::Volume(key) => { + if let Some(request) = self.volume_requests.press(key) { + self.send_volume(request); + } + } KeyAction::Paste => { if let Some(i) = focused { self.paste(i); @@ -1526,6 +1631,12 @@ impl Session { /// Re-arm the one-shot poll registrations for every handle that fired. fn rearm(&mut self, kb: bool, mouse: bool, inputs: bool, acceptor: bool, prompts: bool, clipboards: bool) { + if let Some((conn, _)) = &self.volume { + let handle = conn.as_handle(); + if self.is_ready(handle) { + self.poller.watch(conn, READABLE, handle.0 as u64); + } + } if inputs { self.poller.watch(&self.inputs, READABLE, self.inputs.as_handle().0 as u64); } @@ -1624,6 +1735,14 @@ impl Session { ); } + // Above every window and the taskbar, under the cursor. + if let Some(shown) = self.overlay.visible(self.overlay_now()) { + let rect = overlay_rect(&self.desk); + if regions.iter().any(|r| r.overlaps(rect)) { + render::draw_overlay(&self.back.surface, &self.font, rect, &shown.text()); + } + } + // Into the back buffer, so a region containing the cursor carries it // over with everything else rather than the panel being touched twice. if !self.hw_cursor { diff --git a/system/ps2server/ps2/src/key.rs b/system/ps2server/ps2/src/key.rs index 95694bb69c4..19b07a537d6 100644 --- a/system/ps2server/ps2/src/key.rs +++ b/system/ps2server/ps2/src/key.rs @@ -44,6 +44,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. @@ -54,11 +58,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 diff --git a/system/ps2server/ps2/tests/decode.rs b/system/ps2server/ps2/tests/decode.rs index aef9b850401..3d73266b456 100644 --- a/system/ps2server/ps2/tests/decode.rs +++ b/system/ps2server/ps2/tests/decode.rs @@ -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)> { @@ -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 = (0x04u8..=0x38) .chain([0x64]) .chain(0x4Au8..=0x52) + .chain(0x7Fu8..=0x81) .chain(0xE0u8..=0xE7) .collect(); for usage in required { diff --git a/system/soundserver/Cargo.toml b/system/soundserver/Cargo.toml index c43a495803e..bf3aa9887f2 100644 --- a/system/soundserver/Cargo.toml +++ b/system/soundserver/Cargo.toml @@ -13,6 +13,7 @@ toyos-virtio-sound = { path = "virtio-sound" } toyos-virtio = { path = "../../lib/virtio" } toyos-pci-claim = { path = "../../lib/pci-claim" } toyos-inspect = { path = "../../lib/inspect" } +toyos-resample = { path = "resample" } rubato = { version = "0.16", default-features = false } [package.metadata.toyos.host] diff --git a/system/soundserver/mixer/src/lib.rs b/system/soundserver/mixer/src/lib.rs index 28c32d21d8f..592300bb455 100644 --- a/system/soundserver/mixer/src/lib.rs +++ b/system/soundserver/mixer/src/lib.rs @@ -66,6 +66,6 @@ pub use gain::{Gain, GainRamp}; pub use mix::{accumulate, mix_interleaved}; pub use shape::{ client_period_frames, deferral_floor_nanos, period_frames, period_nanos, ramp_frames, - scratch_frames, Shape, DEFERRAL_RESERVE, MAX_CLIENT_RATE, MAX_PIPELINE, MIN_CLIENT_RATE, + scratch_frames, Shape, DEFERRAL_RESERVE, DEVICE_RATES, MAX_CLIENT_RATE, MAX_PIPELINE, MIN_CLIENT_RATE, }; pub use stats::{wake_left_idle, MixStats}; diff --git a/system/soundserver/mixer/src/shape.rs b/system/soundserver/mixer/src/shape.rs index e3eef152c35..85edef46acd 100644 --- a/system/soundserver/mixer/src/shape.rs +++ b/system/soundserver/mixer/src/shape.rs @@ -23,6 +23,11 @@ pub const DEFERRAL_RESERVE: usize = 5; /// the power-of-two rule below, 16 is the deepest that does. pub const MAX_PIPELINE: usize = 16; +/// The rates soundserver runs a device at, best first: a device runs at the +/// first one it offers. 44.1 kHz leads, so a converter that offers it is +/// unchanged; 48 kHz is the one every other converter offers. +pub const DEVICE_RATES: [u32; 2] = [44_100, 48_000]; + /// The rates a client may ask for. Outside them the open is refused, which is /// what keeps [`client_period_frames`] inside [`scratch_frames`]. pub const MIN_CLIENT_RATE: u32 = 8_000; diff --git a/system/soundserver/resample/Cargo.toml b/system/soundserver/resample/Cargo.toml new file mode 100644 index 00000000000..bb3cc4dd982 --- /dev/null +++ b/system/soundserver/resample/Cargo.toml @@ -0,0 +1,23 @@ +# A member of the host workspace (root `Cargo.toml`), like `toyos-mixer` beside +# it: its tests run on the host, on every interpolator rubato can take on the +# host's architecture, and soundserver depends on it by path. + +[package] +name = "toyos-resample" +description = "soundserver's resampler: rubato's windowed sinc with the parameters a client rate and a device rate take, held to the dithered 16-bit wire." +version = "0.1.0" +edition = "2021" +license = "MIT OR Apache-2.0" + +[lib] +doctest = false + +[dependencies] +rubato = { version = "0.16", default-features = false } + +[dev-dependencies] +# The device rates the tests hold the resampler to. +toyos-mixer = { path = "../mixer" } + +[lints.rust] +warnings = "deny" diff --git a/system/soundserver/resample/src/aarch64.rs b/system/soundserver/resample/src/aarch64.rs new file mode 100644 index 00000000000..1255d167986 --- /dev/null +++ b/system/soundserver/resample/src/aarch64.rs @@ -0,0 +1,22 @@ +//! The interpolator rubato can take on an AArch64 CPU ToyOS runs on: the +//! target compiles NEON in. + +use rubato::sinc_interpolator::sinc_interpolator_neon::NeonInterpolator; +use rubato::sinc_interpolator::SincInterpolator; +use rubato::WindowFunction; + +/// Built with the sinc length and cutoff rubato's own dispatch derives. +pub(crate) fn interpolators( + sinc_len: usize, + oversampling: usize, + cutoff: f32, + window: WindowFunction, +) -> Vec<(&'static str, Box>)> { + vec![( + "NeonInterpolator", + Box::new( + NeonInterpolator::::new(sinc_len, oversampling, cutoff, window) + .unwrap_or_else(|e| panic!("NeonInterpolator: {e:?}")), + ), + )] +} diff --git a/system/soundserver/resample/src/interpolators.rs b/system/soundserver/resample/src/interpolators.rs new file mode 100644 index 00000000000..3e9106c5d73 --- /dev/null +++ b/system/soundserver/resample/src/interpolators.rs @@ -0,0 +1,11 @@ +//! Every interpolator rubato can take on a CPU ToyOS runs on, by +//! architecture: the selector the resampler's tests build each one through. + +#[cfg(target_arch = "x86_64")] +#[path = "x86_64.rs"] +mod arch; +#[cfg(target_arch = "aarch64")] +#[path = "aarch64.rs"] +mod arch; + +pub(crate) use arch::interpolators; diff --git a/system/soundserver/resample/src/lib.rs b/system/soundserver/resample/src/lib.rs new file mode 100644 index 00000000000..a94c751158d --- /dev/null +++ b/system/soundserver/resample/src/lib.rs @@ -0,0 +1,71 @@ +//! The resampler a client at another rate than the device's is carried by: +//! `rubato`'s windowed sinc, with the parameters its two rates take. +//! +//! Held to the dithered 16-bit wire by published practice, on every +//! interpolator rubato can take on a CPU ToyOS runs on (`tests.rs`): +//! - the passband, 20 Hz to 0.4535 of the lower rate and at most 20 kHz, +//! within ±0.007 dB: the TI SRC4192/SRC4193 datasheet's (SBFS022C) +//! interpolation filter; +//! - the stop band, from 0.5465 of the device rate when a client is faster, +//! and in-band THD+N, each 125 dB below the tone: SoX's `rate` at its +//! "high" quality, its figure for 16-bit mastering with dither. +//! +//! **The parameters.** A 512-tap Blackman-Harris² sinc, oversampled 256 +//! times and interpolated cubically, cut off where rubato's +//! `calculate_cutoff` puts a 512-tap filter. Below a ratio of 1 rubato scales +//! the cutoff by the ratio and keeps the sinc's length in input frames, which +//! widens the transition against the output's Nyquist by the ratio; so a +//! client faster than the device lengthens the sinc by the ratio, rounded up +//! to a multiple of 8, which gives the 512-tap filter's response against the +//! lower rate, and divides the oversampling by it, rounded up, which keeps the +//! table at 512 KiB. + +use rubato::{ + calculate_cutoff, SincFixedOut, SincInterpolationParameters, SincInterpolationType, + WindowFunction, +}; + +const SINC_LEN: usize = 512; +const OVERSAMPLING: usize = 256; +const WINDOW: WindowFunction = WindowFunction::BlackmanHarris2; +const INTERPOLATION: SincInterpolationType = SincInterpolationType::Cubic; + +/// How far the ratio may move from its nominal value; soundserver never moves +/// it, and rubato sizes its buffers by it. +const MAX_RATIO_RELATIVE: f64 = 2.0; + +/// The parameters a client at `client_rate` on a device at `device_rate` takes. +fn parameters(client_rate: u32, device_rate: u32) -> SincInterpolationParameters { + let faster = (client_rate as f64 / device_rate as f64).max(1.0); + let sinc_len = 8 * ((SINC_LEN as f64 * faster / 8.0).ceil() as usize); + SincInterpolationParameters { + sinc_len, + f_cutoff: calculate_cutoff::(SINC_LEN, WINDOW), + interpolation: INTERPOLATION, + oversampling_factor: (OVERSAMPLING as f64 / faster).ceil() as usize, + window: WINDOW, + } +} + +/// The resampler for one stream: `client_rate` in, `device_rate` out in +/// periods of `period_frames`, over `channels`. +pub fn build( + client_rate: u32, + device_rate: u32, + period_frames: usize, + channels: usize, +) -> SincFixedOut { + SincFixedOut::::new( + device_rate as f64 / client_rate as f64, + MAX_RATIO_RELATIVE, + parameters(client_rate, device_rate), + period_frames, + channels, + ) + .expect("the parameters are this module's and every ratio between accepted rates is positive") +} + +#[cfg(test)] +mod interpolators; +#[cfg(test)] +mod tests; diff --git a/system/soundserver/resample/src/tests.rs b/system/soundserver/resample/src/tests.rs new file mode 100644 index 00000000000..8731259c5a6 --- /dev/null +++ b/system/soundserver/resample/src/tests.rs @@ -0,0 +1,226 @@ +//! The resampler against the analytic sine, from every client rate in common +//! use to both device rates, on every interpolator rubato can take on a CPU +//! ToyOS runs on, each built explicitly; and what [`build`] makes, which is +//! what a stream runs, is one of them. +//! +//! Each tone's output is fitted by IEEE 1057's three-parameter fit at its +//! known frequency: the fitted amplitude is the gain, and what the fit leaves +//! is THD+N, unweighted over the whole output band. + +use std::f64::consts::PI; + +use rubato::{Resampler, SincFixedOut}; + +use super::interpolators::interpolators; +use super::{build, parameters, INTERPOLATION, MAX_RATIO_RELATIVE}; + +/// The device period, which is the resampler's fixed output chunk. +const PERIOD: usize = 128; +const CHANNELS: usize = 2; +/// The test tone, as AES17 measures THD+N. +const TONE_DBFS: f64 = -1.0; +/// Every client rate in common use, from the lowest soundserver accepts to the highest. +const CLIENTS: [u32; 12] = + [8000, 11025, 16000, 22050, 24000, 32000, 44100, 48000, 88200, 96000, 176400, 192000]; +/// The TI SRC4192/SRC4193's band edges, as fractions of the lower of the two rates. +const PASS_EDGE: f64 = 0.4535; +const STOP_EDGE: f64 = 0.5465; +/// The SRC4192's interpolation-filter ripple. +const RIPPLE_DB: f64 = 0.007; +/// SoX's `rate` at "high" quality: 16-bit mastering with dither. +const REJECT_DB: f64 = 125.0; +/// Fitted output per tone. +const SECONDS: f64 = 0.5; + +fn pairs() -> Vec<(u32, u32)> { + let mut v = Vec::new(); + for device in toyos_mixer::DEVICE_RATES { + for client in CLIENTS { + if client != device { + v.push((client, device)); + } + } + } + v +} + +fn amplitude() -> f64 { + 10f64.powf(TONE_DBFS / 20.0) +} + +/// Every interpolator this CPU's architecture can take, each in a resampler +/// built as [`build`] builds it, with the sinc length and cutoff rubato's own +/// dispatch derives: the length rounded up to a multiple of 8, and the cutoff +/// scaled by the ratio below 1. +fn explicit(client: u32, device: u32) -> Vec<(&'static str, SincFixedOut)> { + let p = parameters(client, device); + let ratio = device as f64 / client as f64; + let sinc_len = 8 * (p.sinc_len as f32 / 8.0).ceil() as usize; + let cutoff = if ratio >= 1.0 { p.f_cutoff } else { p.f_cutoff * ratio as f32 }; + interpolators(sinc_len, p.oversampling_factor, cutoff, p.window) + .into_iter() + .map(|(name, interpolator)| { + let resampler = SincFixedOut::::new_with_interpolator( + ratio, + MAX_RATIO_RELATIVE, + INTERPOLATION, + interpolator, + PERIOD, + CHANNELS, + ) + .unwrap(); + (name, resampler) + }) + .collect() +} + +/// A `freq` sine at `client` through `resampler`, channel 0, from the first +/// frame past the start-up transient; and that frame's index. +fn run(mut resampler: SincFixedOut, client: u32, device: u32, freq: f64, seconds: f64) -> (Vec, usize) { + let skip = resampler.output_delay() + 4096; + let total = skip + (device as f64 * seconds) as usize; + let mut out: Vec = Vec::with_capacity(total + PERIOD); + let mut outbuf = resampler.output_buffer_allocate(true); + let mut chunk = vec![vec![0f32; resampler.input_frames_max()]; CHANNELS]; + let mut n_in: u64 = 0; + while out.len() < total { + let need = resampler.input_frames_next(); + for i in 0..need { + let t = (n_in + i as u64) as f64 / client as f64; + let v = (amplitude() * (2.0 * PI * freq * t).sin()) as f32; + for channel in chunk.iter_mut() { + channel[i] = v; + } + } + n_in += need as u64; + let slices: Vec<&[f32]> = chunk.iter().map(|c| &c[..need]).collect(); + let (_, produced) = resampler.process_into_buffer(&slices, &mut outbuf, None).unwrap(); + out.extend_from_slice(&outbuf[0][..produced]); + } + out.truncate(total); + (out.split_off(skip), skip) +} + +/// IEEE 1057's three-parameter fit at a known frequency, `y ≈ a cos + b sin + c`: +/// the gain in dB against the tone, and the residual's level below the fitted +/// tone in dB. +fn fit(ys: &[f32], n0: usize, freq: f64, device: u32) -> (f64, f64) { + let w = 2.0 * PI * freq / device as f64; + let mut m = [[0f64; 3]; 3]; + let mut r = [0f64; 3]; + for (k, &y) in ys.iter().enumerate() { + let n = (k + n0) as f64; + let v = [(w * n).cos(), (w * n).sin(), 1.0]; + for i in 0..3 { + for j in 0..3 { + m[i][j] += v[i] * v[j]; + } + r[i] += y as f64 * v[i]; + } + } + let det = |m: [[f64; 3]; 3]| { + m[0][0] * (m[1][1] * m[2][2] - m[1][2] * m[2][1]) - m[0][1] * (m[1][0] * m[2][2] - m[1][2] * m[2][0]) + + m[0][2] * (m[1][0] * m[2][1] - m[1][1] * m[2][0]) + }; + let d = det(m); + let mut s = [0f64; 3]; + for i in 0..3 { + let mut mi = m; + for j in 0..3 { + mi[j][i] = r[j]; + } + s[i] = det(mi) / d; + } + let (mut err, mut sig) = (0f64, 0f64); + for (k, &y) in ys.iter().enumerate() { + let n = (k + n0) as f64; + let f = s[0] * (w * n).cos() + s[1] * (w * n).sin() + s[2]; + err += (y as f64 - f).powi(2); + sig += (f - s[2]).powi(2); + } + (20.0 * ((s[0] * s[0] + s[1] * s[1]).sqrt() / amplitude()).log10(), 10.0 * (sig / err).log10()) +} + +/// 20 Hz, AES17's 997 Hz, and the passband's edge. +fn in_band(client: u32, device: u32) -> [f64; 3] { + [20.0, 997.0, (PASS_EDGE * client.min(device) as f64).min(20_000.0)] +} + +/// From the stop edge to 0.499 of the client's rate, a sixth of an octave +/// apart, where a client faster than the device has a stop band below its own +/// Nyquist; empty otherwise. +fn stop_band(client: u32, device: u32) -> Vec { + let (lo, hi) = (STOP_EDGE * device as f64, 0.499 * client as f64); + if client <= device || lo >= hi { + return Vec::new(); + } + let mut v = Vec::new(); + let mut f = lo; + while f < hi { + v.push(f); + f *= 2f64.powf(1.0 / 6.0); + } + v.push(hi); + v +} + +#[test] +fn the_passband_is_flat_within_the_src4192_s_ripple() { + for (client, device) in pairs() { + for freq in in_band(client, device) { + for (name, resampler) in explicit(client, device) { + let (ys, n0) = run(resampler, client, device, freq, SECONDS); + let (gain, _) = fit(&ys, n0, freq, device); + assert!( + gain.abs() <= RIPPLE_DB, + "{name}: {client} -> {device} Hz at {freq:.1} Hz has a gain of {gain:+.5} dB" + ); + } + } + } +} + +#[test] +fn in_band_thd_n_is_125_db_below_the_tone() { + for (client, device) in pairs() { + for freq in in_band(client, device) { + for (name, resampler) in explicit(client, device) { + let (ys, n0) = run(resampler, client, device, freq, SECONDS); + let (_, thd_n) = fit(&ys, n0, freq, device); + assert!( + thd_n >= REJECT_DB, + "{name}: {client} -> {device} Hz at {freq:.1} Hz leaves THD+N {thd_n:.1} dB below the tone" + ); + } + } + } +} + +#[test] +fn the_stop_band_is_rejected_by_125_db() { + for (client, device) in pairs() { + for freq in stop_band(client, device) { + for (name, resampler) in explicit(client, device) { + let (ys, _) = run(resampler, client, device, freq, SECONDS); + let rms = (ys.iter().map(|&y| (y as f64).powi(2)).sum::() / ys.len() as f64).sqrt(); + let rejected = -20.0 * (rms / (amplitude() / 2f64.sqrt())).log10(); + assert!( + rejected >= REJECT_DB, + "{name}: {client} -> {device} Hz rejects {freq:.1} Hz by {rejected:.1} dB" + ); + } + } + } +} + +#[test] +fn what_a_stream_runs_is_one_of_the_explicit_builds() { + for (client, device) in pairs() { + let (built, _) = run(build(client, device, PERIOD, CHANNELS), client, device, 997.0, 0.25); + let same = explicit(client, device).into_iter().any(|(_, resampler)| { + let (theirs, _) = run(resampler, client, device, 997.0, 0.25); + theirs.len() == built.len() && theirs.iter().zip(&built).all(|(a, b)| a.to_bits() == b.to_bits()) + }); + assert!(same, "{client} -> {device} Hz: what a stream runs is none of the explicit builds"); + } +} diff --git a/system/soundserver/resample/src/x86_64.rs b/system/soundserver/resample/src/x86_64.rs new file mode 100644 index 00000000000..bce8002b486 --- /dev/null +++ b/system/soundserver/resample/src/x86_64.rs @@ -0,0 +1,33 @@ +//! The interpolators rubato can take on an x86-64 CPU ToyOS runs on: ToyOS +//! requires AVX, and every CPU with it has SSE3. + +use rubato::sinc_interpolator::sinc_interpolator_avx::AvxInterpolator; +use rubato::sinc_interpolator::sinc_interpolator_sse::SseInterpolator; +use rubato::sinc_interpolator::SincInterpolator; +use rubato::WindowFunction; + +/// Each by name, built with the sinc length and cutoff rubato's own dispatch +/// derives. A CPU that cannot run one panics, naming the feature it lacks. +pub(crate) fn interpolators( + sinc_len: usize, + oversampling: usize, + cutoff: f32, + window: WindowFunction, +) -> Vec<(&'static str, Box>)> { + vec![ + ( + "SseInterpolator", + Box::new( + SseInterpolator::::new(sinc_len, oversampling, cutoff, window) + .unwrap_or_else(|e| panic!("SseInterpolator: {e:?}")), + ), + ), + ( + "AvxInterpolator", + Box::new( + AvxInterpolator::::new(sinc_len, oversampling, cutoff, window) + .unwrap_or_else(|e| panic!("AvxInterpolator: {e:?}")), + ), + ), + ] +} diff --git a/system/soundserver/src/client.rs b/system/soundserver/src/client.rs index dc40bfa1a45..5529aa8f728 100644 --- a/system/soundserver/src/client.rs +++ b/system/soundserver/src/client.rs @@ -10,12 +10,9 @@ //! channel conversion, the interleave and the sum are calls into the crate, //! whose corpus holds the answer each of them used to give inline. //! -//! The resampler is `rubato`'s, and it is the one thing on this path that -//! nothing here certifies — what is certified is everything on both sides of it. +//! The resampler is `rubato`'s, built and held to its bounds in `toyos-resample`. -use rubato::{ - Resampler, SincFixedOut, SincInterpolationParameters, SincInterpolationType, WindowFunction, -}; +use rubato::{Resampler, SincFixedOut}; use toyos::audio::{ AudioSlotReader, StreamOpenRequest, StreamOpenResponse, MSG_STREAM_OPENED, @@ -216,6 +213,7 @@ pub(crate) fn open_stream( device_sample_rate, device_channels, slot_count: slot_count as u16, + fade_frames: ramp_frames, }; let sent = control.send_handles([client_shm, signal_read.into()]); if sent.is_err() || control.send(MSG_STREAM_OPENED, &opened).is_err() { @@ -224,21 +222,12 @@ pub(crate) fn open_stream( } let resampler = if req.sample_rate != device_sample_rate { - let params = SincInterpolationParameters { - sinc_len: 128, - f_cutoff: 0.95, - interpolation: SincInterpolationType::Cubic, - oversampling_factor: 128, - window: WindowFunction::BlackmanHarris2, - }; - let resample_ratio = device_sample_rate as f64 / req.sample_rate as f64; - let resampler = SincFixedOut::::new( - resample_ratio, - 2.0, - params, + let resampler = toyos_resample::build( + req.sample_rate, + device_sample_rate, device_period_frames as usize, device_channels as usize, - ).expect("failed to create resampler"); + ); // The pull loop tops accum up to input_frames_next() one slot at a // time, so it peaks below input_frames_max + one client period. let accum_capacity = resampler.input_frames_max() + client_period_frames as usize; diff --git a/system/soundserver/src/control.rs b/system/soundserver/src/control.rs index 3e3ef91e58b..89340b06eac 100644 --- a/system/soundserver/src/control.rs +++ b/system/soundserver/src/control.rs @@ -11,8 +11,9 @@ //! handling for every other client. use toyos::audio::{ - StreamOpenRequest, StreamSetVolume, FORMAT_S16LE, MSG_STREAM_CLOSE, MSG_STREAM_ERROR, - MSG_STREAM_OPEN, MSG_STREAM_SET_VOLUME, + DeviceDescription, StreamOpenRequest, StreamSetVolume, FORMAT_S16LE, MSG_DEVICE_DESCRIBE, + MSG_DEVICE_DESCRIBED, MSG_STREAM_CLOSE, MSG_STREAM_ERROR, MSG_STREAM_OPEN, + MSG_STREAM_SET_VOLUME, }; use toyos::ipc::{self, RxStep}; use toyos::poller::{Poller, READABLE}; @@ -24,6 +25,7 @@ use toyos_mixer::{period_nanos, Gain, MAX_CLIENT_RATE, MIN_CLIENT_RATE}; use crate::client::{open_stream, Departure}; use crate::command::{remove, submit, CommandRing, MixCommand}; use crate::inspect::{self, Device, Published}; +use crate::volume::{self, VolumeClient}; /// Control connections soundserver will hold at once. /// @@ -86,6 +88,9 @@ enum Control { Close { idx: usize }, /// `inspect`, on a connection that carries no stream: answered and closed. Inspect, + /// What device soundserver runs, on a connection that carries no stream: + /// answered and closed. + Describe, /// An unknown message type, a payload the wrong width for its type, or a /// message this connection is not in the state to send. All three are the /// client getting it wrong, and all three end the connection. @@ -112,6 +117,7 @@ fn classify(msg_type: u32, payload_len: usize, stream_idx: Option) -> Con } (MSG_STREAM_CLOSE, Some(idx)) => Control::Close { idx }, (toyos_inspect::MSG_INSPECT, None) if payload_len == 0 => Control::Inspect, + (MSG_DEVICE_DESCRIBE, None) if payload_len == 0 => Control::Describe, _ => Control::Violation, } } @@ -129,8 +135,22 @@ fn reject_open(req: &StreamOpenRequest) -> Option<&'static str> { None } +/// What a client asking [`MSG_DEVICE_DESCRIBE`] is told. +fn description(device: &Device) -> DeviceDescription { + DeviceDescription { + sample_rate: device.rate, + period_frames: device.period_frames, + min_client_rate: MIN_CLIENT_RATE, + max_client_rate: MAX_CLIENT_RATE, + channels: device.channels, + _pad: 0, + } +} + pub(crate) fn control_thread( acceptor: Acceptor, + volume_acceptor: Acceptor, + mut master: Option, cmd_ring: &CommandRing, cmd_pipe_write: RawHandle, device_sample_rate: u32, @@ -163,15 +183,23 @@ pub(crate) fn control_thread( } let mut clients: Vec = Vec::new(); + let mut volume_clients: Vec = Vec::new(); let mut next_idx: usize = 0; const TOKEN_ACCEPT: u64 = u64::MAX; + const TOKEN_VOLUME_ACCEPT: u64 = u64::MAX - 1; + /// A `volume` connection's token: its index above every control client's. + const TOKEN_VOLUME: u64 = 1 << 32; loop { poller.watch(&acceptor, READABLE, TOKEN_ACCEPT); + poller.watch(&volume_acceptor, READABLE, TOKEN_VOLUME_ACCEPT); for (i, client) in clients.iter().enumerate() { poller.watch(&client.conn, READABLE, i as u64); } + for (i, client) in volume_clients.iter().enumerate() { + poller.watch(&client.conn, READABLE, TOKEN_VOLUME + i as u64); + } let mut ready: Vec = Vec::new(); poller.wait(1, u64::MAX, |t| ready.push(t)); @@ -198,6 +226,21 @@ pub(crate) fn control_thread( } } + if ready.contains(&TOKEN_VOLUME_ACCEPT) { + match volume_acceptor.accept() { + // The compositor holds the one connector, so a second + // connection is a reconnect and the first is dropped. + Ok(conn) => volume::accepted(&mut volume_clients, conn), + Err(e) => say!("soundserver: volume accept failed: {e:?}"), + } + } + let mut at = TOKEN_VOLUME; + volume_clients.retain_mut(|client| { + let token = at; + at += 1; + !ready.contains(&token) || volume::serve(client, master.as_mut()) + }); + let mut dead: Vec = Vec::new(); for i in 0..clients.len() { if !ready.contains(&(i as u64)) { @@ -305,6 +348,16 @@ pub(crate) fn control_thread( disconnected = true; break 'msgs; } + Control::Describe => { + if let Err(e) = + clients[i].conn.try_send(MSG_DEVICE_DESCRIBED, &description(device)) + { + say!("soundserver: a device description was not answered ({e:?})"); + } + dead.push(i); + disconnected = true; + break 'msgs; + } Control::Violation => { say!("soundserver: protocol violation (msg {msg_type}), disconnecting client"); if let Some(idx) = clients[i].stream_idx { @@ -408,8 +461,13 @@ mod tests { assert_eq!(classify(toyos_inspect::MSG_INSPECT, 0, Some(3)), Control::Violation); assert_eq!(classify(toyos_inspect::MSG_INSPECT, 1, None), Control::Violation); + // The device's description likewise. + assert_eq!(classify(MSG_DEVICE_DESCRIBE, 0, None), Control::Describe); + assert_eq!(classify(MSG_DEVICE_DESCRIBE, 0, Some(3)), Control::Violation); + assert_eq!(classify(MSG_DEVICE_DESCRIBE, 1, None), Control::Violation); + // Nothing else is a message — including the two soundserver only ever sends. - for other in [0, MSG_STREAM_OPENED, MSG_STREAM_ERROR, u32::MAX] { + for other in [0, MSG_STREAM_OPENED, MSG_STREAM_ERROR, MSG_DEVICE_DESCRIBED, u32::MAX] { for stream_idx in [None, Some(3)] { for len in [0, VOL, OPEN] { assert_eq!(classify(other, len, stream_idx), Control::Violation); diff --git a/system/soundserver/src/hda.rs b/system/soundserver/src/hda.rs index d7322e320f5..2a8d244e4b4 100644 --- a/system/soundserver/src/hda.rs +++ b/system/soundserver/src/hda.rs @@ -1,21 +1,28 @@ //! soundserver as the driver of an Intel HDA controller. //! -//! The kernel brought the controller up, owns the buffer descriptor list and -//! the interrupt, and answers five register writes and two reads. Everything -//! that is a *decision* is here and in `toyos-hda` — which codecs answered, -//! which pin, which converter, the amplifiers, EAPD, the format — and every one -//! of those decisions is a pure function this file calls. +//! The kernel lists every controller it can bind, owns the buffer descriptor +//! list and the interrupt, and answers five register writes, two reads and a +//! bind. Everything that is a *decision* is here and in `toyos-hda` — which +//! controller, which codecs answered, which pin, which converter, the rate, the +//! amplifiers, EAPD, the format and the master — and every one of those +//! decisions is a pure function this file calls. //! //! What this file itself owns is the I/O: one verb over the immediate-command -//! registers, and the stream's run bit. +//! registers, the bind, and the stream's run bit. After the claim, the control +//! thread owns the codec's verbs ([`MasterControl`]) and the mix thread the run bit +//! ([`Hda`]). + +use std::sync::Arc; use toyos::HdaDev; -use toyos_abi::hda::HdaInfo; +use toyos_abi::hda::{HdaController, HdaInfo, Reg}; use toyos_abi::syscall::{RegWidth, SyscallError}; +use toyos_hda::caps::PcmCaps; +use toyos_hda::config::{self, Master, NoMaster, Setting}; use toyos_hda::graph::Codec; -use toyos_hda::path::{OutputPath, PathError}; +use toyos_hda::path::{PathError, Unchosen}; use toyos_hda::verb::{Address, Node, Response, Verb}; -use toyos_hda::{config, probe}; +use toyos_hda::probe; /// The controller's immediate-command registers, as byte offsets into the /// register window. The kernel's allow-list carries the same three numbers and @@ -46,80 +53,198 @@ const SD_CTL_DEIE: u32 = 1 << 4; /// audio down with a hang instead of a refusal. const VERB_POLLS: u32 = 4096; +/// The bound controller's index once the bind has run: the claim lists it alone. +const BOUND: u8 = 0; + +/// One controller's verb interface: the claim, and the controller's index in +/// the claim's current list. +struct Link { + dev: Arc, + controller: u8, +} + +impl Link { + fn reg(&self, offset: u32) -> Reg { + Reg { controller: self.controller, offset } + } + + /// One verb over the immediate-command registers: two writes, a bounded + /// poll, and a read. + /// + /// CORB/RIRB would batch several verbs behind one ring-pointer write, and + /// there is nothing here to batch for: verbs are sent at claim time and on + /// a press of a volume key, never in the audio path. + fn send(&self, verb: Verb) -> Option { + let dev = &self.dev; + if !self.settles(|status| status & IMMEDIATE_BUSY == 0) { + return None; + } + dev.reg_write(self.reg(IMMEDIATE_STATUS), RegWidth::U16, IMMEDIATE_RESULT_VALID).ok()?; + dev.reg_write(self.reg(IMMEDIATE_COMMAND), RegWidth::U32, verb.raw()).ok()?; + dev.reg_write(self.reg(IMMEDIATE_STATUS), RegWidth::U16, IMMEDIATE_BUSY).ok()?; + if !self.settles(|status| { + status & IMMEDIATE_BUSY == 0 && status & IMMEDIATE_RESULT_VALID != 0 + }) { + return None; + } + let response = dev.reg_read(self.reg(IMMEDIATE_RESPONSE), RegWidth::U32).ok()?; + dev.reg_write(self.reg(IMMEDIATE_STATUS), RegWidth::U16, IMMEDIATE_RESULT_VALID).ok()?; + Response::new(response) + } + + fn settles(&self, ready: impl Fn(u32) -> bool) -> bool { + for _ in 0..VERB_POLLS { + match self.dev.reg_read(self.reg(IMMEDIATE_STATUS), RegWidth::U16) { + Ok(status) if ready(status) => return true, + Ok(_) => {} + Err(_) => return false, + } + } + false + } +} + +impl probe::Verbs for Link { + fn get(&mut self, codec: Address, node: Node, verb: u16, payload: u8) -> Option { + self.send(Verb::short(codec, node, verb, payload)) + } +} + +/// The stream: the mix thread's, which starts and stops it. pub struct Hda { - dev: HdaDev, + dev: Arc, info: HdaInfo, + stream_offset: u32, /// Set once the engine has been told to run, so a stop and a resume are one /// register write each and not one per period. running: bool, } -/// Why this machine's HDA controller cannot carry audio. Each is a line soundserver -/// prints before falling back to the null sink — "no sound" without which of -/// these it was is a report nobody can act on. +/// The codec's master: the control thread's, which sets and reads it back. +pub struct MasterControl { + link: Link, + master: Master, + setting: Setting, +} + +/// What a claim of the HDA controllers came to. +pub struct Claimed { + pub hda: Hda, + pub rate: u32, + pub channels: u8, + /// `None` where the path has no master, which soundserver names and serves + /// as unavailable. + pub master: Option, +} + +/// Why this machine's HDA controllers cannot carry audio. Each is a line +/// soundserver prints before falling back to the null sink — "no sound" without +/// which of these it was is a report nobody can act on. pub enum Refusal { /// The kernel's answers stopped making sense, which is a bug here or there /// and never a property of the machine. Kernel(SyscallError), - /// No codec `STATESTS` named answered a verb. - NoCodec, - /// Every codec answered and none offers an output a human can hear. - NoOutput(toyos_hda::PathError), - /// The converter behind the chosen pin does not offer the one rate this - /// pipeline runs at. - Rate, + /// None of the listed controllers reaches an output, or several do. + Choice(Vec, Unchosen), + /// The kernel refused the bind; its `hda:` line says why. + Bind(HdaController, SyscallError), + /// The converter behind the chosen pin offers none of the device rates, at + /// 16 bits; what it does offer, where it has a PCM word at all. + Rate(Option), +} + +/// `bb:dd.f`, as the kernel's `hda:` lines name a controller. +struct Pci<'a>(&'a HdaController); + +impl core::fmt::Display for Pci<'_> { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + write!(f, "{:02x}:{:02x}.{}", self.0.bus, self.0.dev, self.0.func) + } +} + +fn path_error(f: &mut core::fmt::Formatter<'_>, why: &PathError) -> core::fmt::Result { + match why { + PathError::NoOutputPin { codecs } if codecs.is_empty() => { + write!(f, "no codec answered") + } + PathError::NoOutputPin { codecs } => { + write!(f, "no output a human can hear, on codec")?; + for (i, address) in codecs.iter().enumerate() { + write!(f, "{}{address}", if i == 0 { " " } else { ", " })?; + } + Ok(()) + } + PathError::Cycle { pin, at } => { + write!(f, "pin {:#04x}'s connection list leads back to {:#04x}", pin.0, at.0) + } + PathError::OutsideGroup { pin, named } => write!( + f, + "pin {:#04x} names node {:#04x}, which its function group never declared", + pin.0, named.0 + ), + PathError::NoConverter { pin } => write!(f, "no converter behind pin {:#04x}", pin.0), + } } impl core::fmt::Display for Refusal { fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { match self { Self::Kernel(e) => write!(f, "the kernel refused a call this driver has to make ({e})"), - Self::NoCodec => write!(f, "no codec STATESTS named answered a verb"), - Self::NoOutput(PathError::NoOutputPin { codecs }) => { - write!(f, "no output a human can hear, on codec")?; - for (i, address) in codecs.iter().enumerate() { - write!(f, "{}{address}", if i == 0 { " " } else { ", " })?; + Self::Choice(controllers, Unchosen(found)) => { + let reaching = found.iter().filter(|walk| walk.is_ok()).count(); + if reaching == 0 { + write!(f, "no controller reaches an output")?; + } else { + write!(f, "{reaching} controllers reach an output, and choosing between them is a policy nothing keeps")?; + } + for (controller, walk) in controllers.iter().zip(found) { + write!(f, "; {}: ", Pci(controller))?; + match walk { + Ok(path) => write!(f, "pin {:#04x} ({})", path.output.node.0, path.device.name())?, + Err(why) => path_error(f, why)?, + } } Ok(()) } - Self::NoOutput(PathError::Cycle { pin, at }) => { - write!(f, "pin {:#04x}'s connection list leads back to {:#04x}", pin.0, at.0) + Self::Bind(controller, e) => { + write!(f, "the kernel refused to bind {} ({e}); its `hda:` line says why", Pci(controller)) } - Self::NoOutput(PathError::OutsideGroup { pin, named }) => write!( - f, - "pin {:#04x} names node {:#04x}, which its function group never declared", - pin.0, named.0 - ), - Self::NoOutput(PathError::NoConverter { pin }) => { - write!(f, "no converter behind pin {:#04x}", pin.0) + Self::Rate(None) => write!(f, "the converter has no PCM word"), + Self::Rate(Some(pcm)) => { + write!(f, "the converter offers none of {:?} Hz at {} bits; it offers", toyos_mixer::DEVICE_RATES, config::WIDTH)?; + for rate in pcm.rates() { + write!(f, " {rate}")?; + } + write!(f, " Hz at {:?} bits", pcm.widths().collect::>()) } - Self::Rate => write!( - f, - "the converter does not offer {} Hz at {} bits", - config::RATE, - config::WIDTH - ), } } } -impl Hda { - /// Walk the controller's codecs, choose an output and configure it. - /// - /// The claim is the argument: `/system/bin/supervisor` minted it and endowed it, so - /// "does this machine have an HDA?" was already answered before soundserver's - /// first instruction. - pub fn claim(dev: HdaDev) -> Result<(Self, OutputPath, u8), Refusal> { - let info = dev.info().map_err(Refusal::Kernel)?; - let mut hda = Hda { dev, info, running: false }; +impl core::fmt::Display for NoMasterLine { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + match self.0 { + NoMaster::NoGain => write!(f, "no amplifier on the path has a gain range"), + NoMaster::NoMute => write!(f, "neither an output pin nor the converter has a mute bit"), + } + } +} - let found = probe::enumerate(&mut hda, info.statests); - let mut codecs: Vec = Vec::new(); - for entry in found { +struct NoMasterLine(NoMaster); + +/// Every listed controller's codecs, walked over its own verb interface, in +/// list order. +fn walk(dev: &Arc, listed: &[HdaController]) -> Vec> { + let mut walks = Vec::new(); + for (index, controller) in listed.iter().enumerate() { + let mut link = Link { dev: dev.clone(), controller: index as u8 }; + let mut codecs = Vec::new(); + for entry in probe::enumerate(&mut link, controller.statests) { match entry { Ok(codec) => { say!( - "soundserver: hda codec{} vendor={:04x} device={:04x}, {} function group(s)", + "soundserver: hda {} codec{} vendor={:04x} device={:04x}, {} function group(s)", + Pci(controller), codec.address, codec.vendor, codec.device, @@ -127,50 +252,85 @@ impl Hda { ); codecs.push(codec); } - Err((address, fault)) => { - say!("soundserver: hda codec{address} answered nothing usable ({fault:?})") - } + Err((address, fault)) => say!( + "soundserver: hda {} codec{address} answered nothing usable ({fault:?})", + Pci(controller) + ), } } - if codecs.is_empty() { - return Err(Refusal::NoCodec); + walks.push(codecs); + } + walks +} + +impl Hda { + /// Walk every listed controller's codecs, choose the one controller whose + /// graph reaches an output, bind it, and configure its path at the first + /// device rate its converter offers. + /// + /// The claim is the argument: `/system/bin/supervisor` minted it and endowed it, so + /// "does this machine have an HDA?" was already answered before soundserver's + /// first instruction. Everything here runs before soundserver starts a thread. + pub fn claim(dev: HdaDev) -> Result { + let info = dev.info().map_err(Refusal::Kernel)?; + let listed = info.listed().to_vec(); + let dev = Arc::new(dev); + + let mut walks = walk(&dev, &listed); + let (chosen, path) = toyos_hda::choose(&walks).map_err(|u| Refusal::Choice(listed.clone(), u))?; + let controller = listed[chosen]; + dev.bind(chosen as u8).map_err(|e| Refusal::Bind(controller, e))?; + for (index, other) in listed.iter().enumerate().filter(|&(i, _)| i != chosen) { + say!("soundserver: hda bound {}, not {} (controller {index})", Pci(&controller), Pci(other)); } + let codecs = walks.swap_remove(chosen); - let path = toyos_hda::find_output_path(&codecs).map_err(Refusal::NoOutput)?; - let (format, channels) = config::format(&codecs, &path).ok_or(Refusal::Rate)?; + let rate = config::rate(&codecs, &path, &toyos_mixer::DEVICE_RATES).map_err(Refusal::Rate)?; + let (format, channels) = + config::format(&codecs, &path, rate).expect("a rate the converter offers has a format"); say!( - "soundserver: hda codec{} group {:#04x} converter {:#04x} -> pin {:#04x} ({}), \ + "soundserver: hda {} codec{} group {:#04x} converter {:#04x} -> pin {:#04x} ({}), \ headphone {}, format {:#06x} ({} Hz {} ch {}-bit)", + Pci(&controller), path.codec, path.group.0, path.converter.0, path.output.node.0, path.device.name(), match &path.headphone { - Some(hp) => alloc_node(hp.node), + Some(hp) => format!("{:#04x}", hp.node.0), None => String::from("none"), }, format, - config::RATE, + rate, channels, config::WIDTH, ); + let link = Link { dev: dev.clone(), controller: BOUND }; let verbs = config::verbs(&codecs, &path, format, info.stream_tag) .expect("the path names a codec this walk produced"); let sent = verbs.len(); for verb in verbs { - hda.send(verb); + link.send(verb); } + let hda = Hda { dev: dev.clone(), info, stream_offset: controller.stream_offset, running: false }; // The tag before the format, and both before the engine is ever told to // run: a descriptor that starts with neither plays whatever the last // owner of the stream left in it. - hda.write(SD_CTL_TAG, RegWidth::U8, (info.stream_tag as u32) << 4) - .map_err(Refusal::Kernel)?; + hda.write(SD_CTL_TAG, RegWidth::U8, (info.stream_tag as u32) << 4).map_err(Refusal::Kernel)?; hda.write(SD_FMT, RegWidth::U16, format as u32).map_err(Refusal::Kernel)?; say!("soundserver: hda path configured in {sent} verbs, stream tag {}", info.stream_tag); - Ok((hda, path, channels)) + + let master = match config::master(&path) { + Ok(master) => Some(MasterControl { link, master, setting: Setting::TOP }), + Err(why) => { + say!("soundserver: hda path has no master ({}); volume answers unavailable", NoMasterLine(why)); + None + } + }; + Ok(Claimed { hda, rate, channels, master }) } pub fn info(&self) -> HdaInfo { @@ -207,52 +367,52 @@ impl Hda { } fn write(&self, field: u32, width: RegWidth, value: u32) -> Result<(), SyscallError> { - self.dev.reg_write(self.info.stream_offset + field, width, value) + let reg = Reg { controller: BOUND, offset: self.stream_offset + field }; + self.dev.reg_write(reg, width, value) } +} - /// One verb over the immediate-command registers: two writes, a bounded - /// poll, and a read. - /// - /// CORB/RIRB would batch several verbs behind one ring-pointer write, and - /// there is nothing here to batch for: verbs are sent at claim time and on a - /// jack poll, never in the audio path. What the ring would buy is a - /// syscall this driver spends about a hundred times per boot. - fn send(&mut self, verb: Verb) -> Option { - let dev = &self.dev; - if !self.settles(|status| status & IMMEDIATE_BUSY == 0) { - return None; - } - dev.reg_write(IMMEDIATE_STATUS, RegWidth::U16, IMMEDIATE_RESULT_VALID).ok()?; - dev.reg_write(IMMEDIATE_COMMAND, RegWidth::U32, verb.raw()).ok()?; - dev.reg_write(IMMEDIATE_STATUS, RegWidth::U16, IMMEDIATE_BUSY).ok()?; - if !self.settles(|status| { - status & IMMEDIATE_BUSY == 0 && status & IMMEDIATE_RESULT_VALID != 0 - }) { - return None; - } - let response = dev.reg_read(IMMEDIATE_RESPONSE, RegWidth::U32).ok()?; - dev.reg_write(IMMEDIATE_STATUS, RegWidth::U16, IMMEDIATE_RESULT_VALID).ok()?; - Response::new(response) - } +/// What the master answers a request with: the setting read back from the +/// codec after it was set, never the intent. +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub struct Answer { + pub setting: Setting, + pub bottom: u8, +} - fn settles(&self, ready: impl Fn(u32) -> bool) -> bool { - for _ in 0..VERB_POLLS { - match self.dev.reg_read(IMMEDIATE_STATUS, RegWidth::U16) { - Ok(status) if ready(status) => return true, - Ok(_) => {} - Err(_) => return false, - } - } - false +impl MasterControl { + /// Move the level by `delta` positions; a step while muted unmutes. + pub fn step(&mut self, delta: i32) -> Option { + self.apply(self.master.step(self.setting, delta)) } -} -impl probe::Verbs for Hda { - fn get(&mut self, codec: Address, node: Node, verb: u16, payload: u8) -> Option { - self.send(Verb::short(codec, node, verb, payload)) + /// Mute, or unmute at the level it had. + pub fn toggle_mute(&mut self) -> Option { + self.apply(Setting { muted: !self.setting.muted, ..self.setting }) } -} -fn alloc_node(node: Node) -> String { - format!("{:#04x}", node.0) + /// Set `setting` and read it back. `None`, named, where a verb went + /// unanswered or the answers name no setting. + fn apply(&mut self, setting: Setting) -> Option { + for verb in self.master.set(setting) { + if self.link.send(verb).is_none() { + say!("soundserver: hda master set went unanswered ({:#010x})", verb.raw()); + return None; + } + } + let mut answers = Vec::new(); + for verb in self.master.read() { + let Some(answer) = self.link.send(verb) else { + say!("soundserver: hda master readback went unanswered"); + return None; + }; + answers.push(answer); + } + let Some(read) = self.master.decode(&answers) else { + say!("soundserver: hda master read back as no setting ({answers:?})"); + return None; + }; + self.setting = read; + Some(Answer { setting: read, bottom: self.master.bottom() }) + } } diff --git a/system/soundserver/src/main.rs b/system/soundserver/src/main.rs index e47bca86fb6..e098079e776 100644 --- a/system/soundserver/src/main.rs +++ b/system/soundserver/src/main.rs @@ -13,6 +13,7 @@ //! | [`ring`] | the lock-free ring every hand-off to or from the mix thread is | //! | [`log`] | soundserver's lines, and why the mix thread's never wait | //! | [`hda`], [`virtio`] | the two drivers | +//! | [`volume`] | the `volume` port: the master level and mute, on the codec | //! //! This file is the fourth thing: which of them the machine gets. A sound card //! this process was endowed with runs the device loop; anything else — no card, @@ -24,7 +25,7 @@ use toyos::port::Acceptor; use toyos::shm::SharedMemory; use toyos::{HdaDev, PciDev}; use toyos_abi::syscall::{self, DeviceType}; -use toyos_mixer::{period_frames, ramp_frames}; +use toyos_mixer::{period_frames, ramp_frames, DEVICE_RATES}; use std::sync::Arc; @@ -45,6 +46,7 @@ mod mix; mod log; mod ring; mod virtio; +mod volume; use backend::{Backend, HdaBackend, VirtioBackend}; use command::CommandRing; @@ -52,12 +54,11 @@ use inspect::{Device, Published, State}; use control::control_thread; use mix::{mix_thread, null_sink_thread}; -/// The virtual output soundserver presents when the machine has no audio hardware. -/// These match the one configuration cpal's ToyOS backend advertises -/// (`src/host/toyos/mod.rs`): 44100 Hz stereo i16, 128 frames per period. A -/// stream negotiates against them exactly as it would a real device, so a -/// no-hardware machine is invisible to the client. -const NULL_SINK_RATE: u32 = 44_100; +/// The virtual output soundserver presents when the machine has no audio hardware: +/// the first device rate, stereo, 128 frames per period. A stream negotiates +/// against them exactly as it would a real device, so a no-hardware machine is +/// invisible to the client. +const NULL_SINK_RATE: u32 = DEVICE_RATES[0]; const NULL_SINK_CHANNELS: u16 = 2; const NULL_SINK_PERIOD_FRAMES: usize = 128; /// Same DMA-pipeline depth as both hardware backends: the client ring @@ -70,6 +71,9 @@ fn main() { log::claim_lane(); let acceptor = endow::acceptor("soundserver") .expect("the manifest declares this program serves `soundserver`"); + let volume = endow::acceptor("volume") + .expect("the manifest declares this program serves `volume`"); + let ports = Ports { acceptor, volume }; // **"Which sound card does this machine have?" is already answered.** The supervisor // mints a claim per class the manifest names and endows what the machine @@ -85,28 +89,35 @@ fn main() { // no machine in this project has both. The T14 has only the second. if let Some(dev) = endow::pci_function::(virtio::PCI_ID) { match virtio::Virtio::claim(dev) { - Ok((virtio, rate, channels)) => return run_virtio(acceptor, virtio, rate, channels), + Ok((virtio, rate, channels)) => return run_virtio(ports, virtio, rate, channels), Err(why) => { say!("soundserver: the virtio-sound device cannot carry audio: {why}"); - return run_null_sink(acceptor); + return run_null_sink(ports); } } } let Some(dev) = endow::device::(DeviceType::HdaAudio) else { - return run_null_sink(acceptor); + return run_null_sink(ports); }; match hda::Hda::claim(dev) { - Ok((hda, _path, channels)) => run_hda(acceptor, hda, channels), + Ok(claimed) => run_hda(ports, claimed), Err(why) => { say!("soundserver: the HDA controller cannot carry audio: {why}"); - run_null_sink(acceptor) + run_null_sink(ports) } } } -fn run_virtio(acceptor: Acceptor, virtio: virtio::Virtio, rate: u32, channels: u8) { +/// The two ports soundserver serves: streams, and the master. +struct Ports { + acceptor: Acceptor, + volume: Acceptor, +} + +fn run_virtio(ports: Ports, virtio: virtio::Virtio, rate: u32, channels: u8) { run_with_device( - acceptor, + ports, + None, "virtio-sound", &mut VirtioBackend { virtio }, toyos_virtio_sound::PERIODS, @@ -116,7 +127,8 @@ fn run_virtio(acceptor: Acceptor, virtio: virtio::Virtio, rate: u32, channels: u ); } -fn run_hda(acceptor: Acceptor, hda: hda::Hda, channels: u8) { +fn run_hda(ports: Ports, claimed: hda::Claimed) { + let hda::Claimed { hda, rate, channels, master } = claimed; let info = hda.info(); let num_buffers = info.periods as usize; let period_bytes = info.period_bytes as usize; @@ -128,18 +140,20 @@ fn run_hda(acceptor: Acceptor, hda: hda::Hda, channels: u8) { let buffers = (0..num_buffers).map(|i| unsafe { base.add(i * period_bytes) }).collect(); run_with_device( - acceptor, + ports, + master, "hda", &mut HdaBackend { hda, buffers, period_bytes }, num_buffers, - toyos_hda::config::RATE, + rate, channels as u16, period_bytes, ); } fn run_with_device( - acceptor: Acceptor, + ports: Ports, + master: Option, kind: &'static str, backend: &mut dyn Backend, num_buffers: usize, @@ -156,7 +170,7 @@ fn run_with_device( Ok(frames) => frames, Err(why) => { say!("soundserver: this audio device's shape cannot carry audio: {why}"); - return run_null_sink(acceptor); + return run_null_sink(ports); } }; @@ -186,7 +200,9 @@ fn run_with_device( .name("soundserver-ctrl".into()) .spawn(move || { control_thread( - acceptor, + ports.acceptor, + ports.volume, + master, &cmd_ring2, cmd_pipe.write, device_sample_rate, @@ -214,7 +230,7 @@ fn run_with_device( ); } -fn run_null_sink(acceptor: Acceptor) { +fn run_null_sink(ports: Ports) { let device_sample_rate = NULL_SINK_RATE; let device_channels = NULL_SINK_CHANNELS; let device_period_frames = NULL_SINK_PERIOD_FRAMES; @@ -244,7 +260,9 @@ fn run_null_sink(acceptor: Acceptor) { .name("soundserver-ctrl".into()) .spawn(move || { control_thread( - acceptor, + ports.acceptor, + ports.volume, + None, &cmd_ring2, cmd_pipe.write, device_sample_rate, diff --git a/system/soundserver/src/volume.rs b/system/soundserver/src/volume.rs new file mode 100644 index 00000000000..5dabb091b40 --- /dev/null +++ b/system/soundserver/src/volume.rs @@ -0,0 +1,109 @@ +//! The `volume` port: the master level and mute, on the codec. +//! +//! The compositor holds the one connector. Each request is set on the device +//! and read back, and the answer is what the device read back, never the +//! intent, sent by one write that does not wait: a peer that will not take it +//! is dropped by name. A machine whose device has no master — virtio-sound, +//! the null sink, an HDA path with no amplifier to set — answers every +//! request unavailable, by name, and no mixer gain stands in for a master. + +use toyos::audio::{Volume, VolumeStep, MSG_VOLUME, MSG_VOLUME_MUTE, MSG_VOLUME_STEP, MSG_VOLUME_UNAVAILABLE}; +use toyos::ipc::{self, RxStep}; +use toyos::Connection; + +use crate::hda::{Answer, MasterControl}; + +/// The widest request decoded, plus one, so an over-long frame is refused +/// rather than truncated into a request. +const MAX_KEPT_PAYLOAD: usize = core::mem::size_of::() + 1; + +pub(crate) struct VolumeClient { + pub(crate) conn: Connection, + rx: ipc::FrameRx, +} + +/// What one framed request asks for, decided before anything is done. +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +enum Request { + Step, + Mute, + Violation, +} + +fn classify(msg_type: u32, payload_len: usize) -> Request { + match (msg_type, payload_len) { + (MSG_VOLUME_STEP, len) if len == core::mem::size_of::() => Request::Step, + (MSG_VOLUME_MUTE, 0) => Request::Mute, + _ => Request::Violation, + } +} + +/// A new connection: one holder at a time, so one before it is a holder that +/// reconnected, and it goes. +pub(crate) fn accepted(clients: &mut Vec, conn: Connection) { + clients.clear(); + clients.push(VolumeClient { conn, rx: ipc::FrameRx::new() }); +} + +/// Serve every request the client has framed; `false` once it is to go. +pub(crate) fn serve(client: &mut VolumeClient, mut master: Option<&mut MasterControl>) -> bool { + loop { + let (msg_type, payload_len) = match client.rx.pump(&client.conn) { + RxStep::Idle => return true, + RxStep::Eof => return false, + RxStep::Malformed => { + say!("soundserver: a volume frame this protocol cannot describe, disconnecting"); + return false; + } + RxStep::Frame { msg_type, payload_len } => (msg_type, payload_len), + }; + let answer = match classify(msg_type, payload_len) { + Request::Step => { + let step: VolumeStep = ipc::decode_payload(client.rx.payload(payload_len)) + .expect("a payload as long as the struct decodes"); + master.as_deref_mut().and_then(|master| master.step(step.delta)) + } + Request::Mute => master.as_deref_mut().and_then(MasterControl::toggle_mute), + Request::Violation => { + say!("soundserver: volume protocol violation (msg {msg_type}), disconnecting"); + return false; + } + }; + if let Err(e) = send(&client.conn, answer) { + say!("soundserver: a volume answer was not taken ({e:?}), disconnecting"); + return false; + } + } +} + +fn send(conn: &Connection, answer: Option) -> Result<(), ipc::TrySendError> { + match answer { + Some(Answer { setting, bottom }) => conn.try_send( + MSG_VOLUME, + &Volume { position: setting.position, bottom, muted: setting.muted as u8, _pad: 0 }, + ), + None => conn.try_send_bytes(MSG_VOLUME_UNAVAILABLE, &[]), + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn a_volume_request_is_refused_unless_its_type_and_width_fit() { + const STEP: usize = core::mem::size_of::(); + assert_eq!(classify(MSG_VOLUME_STEP, STEP), Request::Step); + assert_eq!(classify(MSG_VOLUME_MUTE, 0), Request::Mute); + for wrong in [0, STEP - 1, STEP + 1] { + assert_eq!(classify(MSG_VOLUME_STEP, wrong), Request::Violation); + } + assert_eq!(classify(MSG_VOLUME_MUTE, 1), Request::Violation); + // The two soundserver only ever sends, and anything else. + for other in [0, MSG_VOLUME, MSG_VOLUME_UNAVAILABLE, u32::MAX] { + for len in [0, STEP] { + assert_eq!(classify(other, len), Request::Violation); + } + } + } +} diff --git a/system/soundserver/virtio-sound/Cargo.toml b/system/soundserver/virtio-sound/Cargo.toml index 30ac314a1f7..3ca48c9ddaa 100644 --- a/system/soundserver/virtio-sound/Cargo.toml +++ b/system/soundserver/virtio-sound/Cargo.toml @@ -19,6 +19,8 @@ doctest = false toyos-virtio = { path = "../../../lib/virtio" } # `DmaBuffers`, the grant the three queues and every buffer are laid out in. toyos-device-memory = { path = "../../../lib/device-memory" } +# The device rates soundserver runs at, best first. +toyos-mixer = { path = "../mixer" } [lints.rust] warnings = "deny" diff --git a/system/soundserver/virtio-sound/src/lib.rs b/system/soundserver/virtio-sound/src/lib.rs index e4a5dec9a6e..abccf35fc01 100644 --- a/system/soundserver/virtio-sound/src/lib.rs +++ b/system/soundserver/virtio-sound/src/lib.rs @@ -100,10 +100,15 @@ const _: () = { assert!(PERIOD_BYTES.is_multiple_of(4)); }; -/// The rates this driver can encode, best first. 44100 leads because it is -/// what the mixer, the resampler and the recorded counters are sized against; -/// 48000 is the one every other device offers. -const RATES: [(u32, u8); 2] = [(44100, wire::RATE_44100), (48000, wire::RATE_48000)]; +/// The specification's code for one of soundserver's device rates +/// ([`toyos_mixer::DEVICE_RATES`]), whose order this driver takes as its own. +fn rate_code(rate: u32) -> Option { + match rate { + 44_100 => Some(wire::RATE_44100), + 48_000 => Some(wire::RATE_48000), + _ => None, + } +} /// How many times a control command's completion is polled before the device /// is called silent, and the spins between two looks. @@ -436,8 +441,9 @@ fn choose(info: &PcmInfo) -> Result<(u32, u8, u8), Refusal> { if info.formats & (1 << wire::FMT_S16) == 0 { return Err(Refusal::NoFormat { formats: info.formats }); } - let (rate, code) = *RATES + let (rate, code) = toyos_mixer::DEVICE_RATES .iter() + .map(|&rate| (rate, rate_code(rate).expect("every device rate has a code"))) .find(|(_, code)| info.rates & (1 << code) != 0) .ok_or(Refusal::NoRate { rates: info.rates })?; // Stereo where the device takes it; the mixer converts either way. diff --git a/system/supervisor/src/main.rs b/system/supervisor/src/main.rs index 408390b4947..e936cc3c9fe 100644 --- a/system/supervisor/src/main.rs +++ b/system/supervisor/src/main.rs @@ -2399,7 +2399,7 @@ fn refused(name: &str, why: SyscallError) -> String { SyscallError::ResourceExhausted => format!("no claim slot is free for {name}"), SyscallError::NotSupported => { format!("{name} is on this machine and could not be handed over; the kernel's \ - `pcidev:`, `partclaim:`, `isa:` or `acpi:` line says why") + `pcidev:`, `partclaim:`, `isa:`, `acpi:` or `hda:` line says why") } other => format!("{name} was refused with {other:?}"), } diff --git a/tests/common/audio.rs b/tests/common/audio.rs index 912ba282bf2..017edf804f1 100644 --- a/tests/common/audio.rs +++ b/tests/common/audio.rs @@ -71,6 +71,40 @@ pub fn tone_on_metal(log: &Serial) -> Result<(), String> { Ok(()) } +/// The resampled tone client played to its end through the HDA controller, at +/// a rate the device does not run, and no period of it went unfilled. +pub fn resampled_tone_on_metal(log: &Serial) -> Result<(), String> { + log.must_say("soundserver: hda path configured in")?; + log.must_not_say(NULL_SINK)?; + let device = rate_after(log.text(), "soundserver: ready, ") + .ok_or("soundserver never said the rate it runs the device at")?; + let window = job_window(log.text(), "test_rs_audio_tone_resampled")?; + let client = rate_after(window, "soundserver: opening stream: ") + .ok_or_else(|| format!("soundserver opened no stream in the job's window:\n{window}"))?; + if client == device { + return Err(format!( + "the stream opened at the device's own rate, {device} Hz, so nothing was resampled:\n{window}" + )); + } + let underruns = sum_field(window, "underruns"); + if underruns != 0 { + return Err(format!( + "soundserver filled {underruns} period(s) the {client} Hz tone had not covered on a \ + {device} Hz device:\n{window}" + )); + } + Ok(()) +} + +/// The first `Hz` after `marker` in `text`. +fn rate_after(text: &str, marker: &str) -> Option { + let at = text.find(marker)? + marker.len(); + let rest = &text[at..]; + let hz = rest.find("Hz")?; + let digits = rest[..hz].rsplit(|c: char| !c.is_ascii_digit()).next()?; + digits.parse().ok() +} + /// soundserver's own word for the sink it took when the machine has none. pub const NULL_SINK: &str = "soundserver: no audio device, presenting a null sink"; diff --git a/tests/guest/rust-tests/Cargo.lock b/tests/guest/rust-tests/Cargo.lock index bd36d4c8f7a..d7a46d7a57b 100644 --- a/tests/guest/rust-tests/Cargo.lock +++ b/tests/guest/rust-tests/Cargo.lock @@ -104,7 +104,7 @@ dependencies = [ [[package]] name = "cpal" version = "0.18.0" -source = "git+https://github.com/ToyOSOrg/cpal?branch=toyos-0.18.0#f46381d27cad65aa5ef5ece5cec1227aa31ef269" +source = "git+https://github.com/ToyOSOrg/cpal?branch=toyos-0.18.0#60031a060b34460e79854ba8d441a0185b50c26a" dependencies = [ "alsa", "block2", diff --git a/tests/guest/rust-tests/src/bin/audio_tone.rs b/tests/guest/rust-tests/src/bin/audio_tone.rs index f28fe525b2d..0a4fa567b37 100644 --- a/tests/guest/rust-tests/src/bin/audio_tone.rs +++ b/tests/guest/rust-tests/src/bin/audio_tone.rs @@ -1,9 +1,13 @@ -//! Play a deterministic 440Hz sine to the end. +//! Play a deterministic 440Hz sine to the end, at the device's own rate. + +use cpal::traits::{DeviceTrait, HostTrait}; #[path = "../tone.rs"] mod tone; fn main() { - tone::play_tone(); + let device = cpal::default_host().default_output_device().expect("no audio output device"); + let config = device.default_output_config().expect("no audio config"); + tone::play(&device, config.into()); println!("tone done"); } diff --git a/tests/guest/rust-tests/src/bin/audio_tone_resampled.rs b/tests/guest/rust-tests/src/bin/audio_tone_resampled.rs new file mode 100644 index 00000000000..e499ceb466b --- /dev/null +++ b/tests/guest/rust-tests/src/bin/audio_tone_resampled.rs @@ -0,0 +1,22 @@ +//! Play the same 440Hz sine to the end at a rate the device does not run: +//! the other of soundserver's two device rates, 44.1 kHz and 48 kHz, so +//! soundserver resamples it. + +use cpal::traits::{DeviceTrait, HostTrait}; + +#[path = "../tone.rs"] +mod tone; + +fn main() { + let device = cpal::default_host().default_output_device().expect("no audio output device"); + let mut config: cpal::StreamConfig = device.default_output_config().expect("no audio config").into(); + config.sample_rate = match config.sample_rate { + 44_100 => 48_000, + 48_000 => 44_100, + other => panic!("the device runs at {other} Hz, which is neither of soundserver's device rates"), + }; + config.buffer_size = cpal::BufferSize::Default; + println!("tone at {} Hz", config.sample_rate); + tone::play(&device, config); + println!("tone done"); +} diff --git a/tests/guest/rust-tests/src/bin/cpal_drop_unreleased.rs b/tests/guest/rust-tests/src/bin/cpal_drop_unreleased.rs index f33e6813a32..5f1cc6e5d0e 100644 --- a/tests/guest/rust-tests/src/bin/cpal_drop_unreleased.rs +++ b/tests/guest/rust-tests/src/bin/cpal_drop_unreleased.rs @@ -1,6 +1,7 @@ //! cpal's ToyOS host, dropped while its stream's server never lets go of it. //! -//! This binary is the stream's server: it answers the open as soundserver does, and +//! This binary is the stream's server: it describes its device and answers the +//! open as soundserver does, and //! then holds the signal pipe open without writing it until the client's //! `Drop` has come back — a soundserver whose mix loop has stopped. Its child, the //! same binary with `client`, reaches it as `soundserver`, builds a stream through @@ -19,7 +20,8 @@ use std::sync::mpsc; use cpal::traits::{DeviceTrait, HostTrait}; use toyos::audio::{ - StreamOpenRequest, StreamOpenResponse, MSG_STREAM_CLOSE, MSG_STREAM_OPEN, MSG_STREAM_OPENED, + DeviceDescription, StreamOpenRequest, StreamOpenResponse, MSG_DEVICE_DESCRIBE, + MSG_DEVICE_DESCRIBED, MSG_STREAM_CLOSE, MSG_STREAM_OPEN, MSG_STREAM_OPENED, }; use toyos::ipc::IpcError; use toyos::shm::SharedMemory; @@ -32,14 +34,25 @@ const SELF: &str = "/system/bin/test_rs_cpal_drop_unreleased"; /// cpal's ToyOS host's words for a soundserver that did not let go. const REFUSAL: &str = "soundserver did not let go of the closed stream within "; -/// What soundserver answers a 44100 Hz stereo open with, which is the one stream -/// cpal's ToyOS host opens. +/// The device this server describes: soundserver's on the T14. +const DEVICE: DeviceDescription = DeviceDescription { + sample_rate: 44_100, + period_frames: 128, + min_client_rate: 8_000, + max_client_rate: 192_000, + channels: 2, + _pad: 0, +}; + +/// What soundserver answers an open at the device's own config with, which is +/// the one stream the client opens. const OPENED: StreamOpenResponse = StreamOpenResponse { client_period_frames: 128, client_period_bytes: 512, device_sample_rate: 44_100, device_channels: 2, slot_count: 8, + fade_frames: 220, }; fn main() { @@ -64,9 +77,20 @@ fn serve() { .spawn() .expect("spawn the client"); - let conn = acceptor.accept().expect("the client's connection"); - let (kind, _): (u32, StreamOpenRequest) = conn.recv().expect("the client's open"); - assert_eq!(kind, MSG_STREAM_OPEN, "the client's first frame is not an open"); + // cpal asks for the device on a connection of its own each time it needs + // it, and the stream is the connection whose first frame is an open. + let conn = loop { + let conn = acceptor.accept().expect("the client's connection"); + let header = conn.recv_header().expect("the client's first frame"); + match header.msg_type { + MSG_DEVICE_DESCRIBE => conn.send(MSG_DEVICE_DESCRIBED, &DEVICE).expect("the description"), + MSG_STREAM_OPEN => { + let _: StreamOpenRequest = conn.recv_payload(&header).expect("the client's open"); + break conn; + } + other => panic!("the client's first frame is message {other}, neither a describe nor an open"), + } + }; let ring = SharedMemory::create( AudioSlotHeader::SIZE + OPENED.slot_count as usize * OPENED.client_period_bytes as usize, ) diff --git a/tests/guest/rust-tests/src/bin/volume_step.rs b/tests/guest/rust-tests/src/bin/volume_step.rs new file mode 100644 index 00000000000..d6d50c131bb --- /dev/null +++ b/tests/guest/rust-tests/src/bin/volume_step.rs @@ -0,0 +1,22 @@ +//! One step of the master level on soundserver's `volume` port and back, +//! each answer the codec's own readback: the step reads back one position +//! down and unmuted, and the step back leaves the level at the top. + +use toyos::audio::{Volume, VolumeStep, MSG_VOLUME, MSG_VOLUME_STEP}; + +fn step(volume: &toyos::Connection, delta: i32) -> Volume { + volume.send(MSG_VOLUME_STEP, &VolumeStep { delta }).expect("the step is sent"); + let header = volume.recv_header().expect("soundserver answers the step"); + assert_eq!(header.msg_type, MSG_VOLUME, "soundserver answered the step with message {}", header.msg_type); + volume.recv_payload(&header).expect("the answer is a volume") +} + +fn main() { + let volume = toyos::endow::service("volume").expect("this job holds soundserver's volume port"); + let down = step(&volume, 1); + println!("step down: position {} of {}, muted {}", down.position, down.bottom, down.muted); + assert_eq!((down.position, down.muted), (1, 0), "one step down reads back one position below the top"); + let top = step(&volume, -1); + println!("step up: position {} of {}, muted {}", top.position, top.bottom, top.muted); + assert_eq!((top.position, top.muted), (0, 0), "the step back reads back the top"); +} diff --git a/tests/guest/rust-tests/src/tone.rs b/tests/guest/rust-tests/src/tone.rs index 87291988455..507f877d997 100644 --- a/tests/guest/rust-tests/src/tone.rs +++ b/tests/guest/rust-tests/src/tone.rs @@ -3,7 +3,7 @@ use std::sync::atomic::{AtomicBool, AtomicU64, Ordering}; use std::sync::Arc; -use cpal::traits::{DeviceTrait, HostTrait, StreamTrait}; +use cpal::traits::{DeviceTrait, StreamTrait}; const FREQ_HZ: f64 = 440.0; const DURATION_SECS: f64 = 3.0; @@ -11,17 +11,15 @@ const DURATION_SECS: f64 = 3.0; /// so mixing can never clip. const AMPLITUDE: f64 = 16000.0; -/// Play a deterministic 440Hz sine and block until it has drained. +/// Play a deterministic 440Hz sine on `device` at `config` and block until it +/// has drained. /// /// The sample value is derived from the absolute sample index, not from /// per-callback state, so the generated signal is identical regardless of /// how the callback invocations are sized or timed. -pub fn play_tone() { - let host = cpal::default_host(); - let device = host.default_output_device().expect("no audio output device"); - let config = device.default_output_config().expect("no audio config"); - let sample_rate = config.sample_rate() as f64; - let channels = config.channels() as usize; +pub fn play(device: &cpal::Device, config: cpal::StreamConfig) { + let sample_rate = config.sample_rate as f64; + let channels = config.channels as usize; let total_samples = (sample_rate * DURATION_SECS) as u64; let done = Arc::new(AtomicBool::new(false)); @@ -32,7 +30,7 @@ pub fn play_tone() { let stream = device .build_output_stream( - config.into(), + config, move |data: &mut [i16], _: &cpal::OutputCallbackInfo| { let mut n = position.load(Ordering::Relaxed); for frame in data.chunks_exact_mut(channels) { diff --git a/tests/images/metalcase.toml b/tests/images/metalcase.toml index cddef6dfb16..68dd34b70f7 100644 --- a/tests/images/metalcase.toml +++ b/tests/images/metalcase.toml @@ -32,7 +32,7 @@ devices = ["framebuffer", "keyboard", "mouse"] [programs.soundserver] service = true -serves = ["soundserver"] +serves = ["soundserver", "volume"] devices = ["hda-audio", "pci:1af4:1059"] syscap = ["rt"] diff --git a/tests/images/metaldevicecase.toml b/tests/images/metaldevicecase.toml index a7c8e57ca37..a31413e96f5 100644 --- a/tests/images/metaldevicecase.toml +++ b/tests/images/metaldevicecase.toml @@ -20,7 +20,7 @@ syscap = ["logread"] [programs.soundserver] service = true -serves = ["soundserver"] +serves = ["soundserver", "volume"] devices = ["hda-audio"] syscap = ["rt"] diff --git a/tests/images/netcase.toml b/tests/images/netcase.toml index 6fc679f695d..a369635eb0b 100644 --- a/tests/images/netcase.toml +++ b/tests/images/netcase.toml @@ -29,7 +29,7 @@ devices = ["pci:1af4:1041"] [programs.soundserver] service = true -serves = ["soundserver"] +serves = ["soundserver", "volume"] devices = ["pci:1af4:1059"] syscap = ["rt"] diff --git a/tests/images/testcases.toml b/tests/images/testcases.toml index 3cb3a82b1b7..2b01571eb7c 100644 --- a/tests/images/testcases.toml +++ b/tests/images/testcases.toml @@ -25,7 +25,7 @@ receives = ["power"] [programs.soundserver] service = true -serves = ["soundserver"] +serves = ["soundserver", "volume"] devices = ["hda-audio", "pci:1af4:1059"] syscap = ["rt"] @@ -48,10 +48,11 @@ syscap = ["rt"] # without it would make that arm vacuous rather than red. # `counters` and `trace` because `counters_read` reads every counter there is, # and narrows each away to prove its refusal. +# `volume` because `volume_step` steps soundserver's master and back. # `log` because `counters_metal` measures its idle second only once the log # holds its own line, read back off logkeeper. [programs.test-runner] -receives = ["soundserver", "power", "log"] +receives = ["soundserver", "volume", "power", "log"] syscap = ["device", "dup", "logread", "power", "roster", "counters", "trace"] [programs.toybox] diff --git a/tests/toyos.rs b/tests/toyos.rs index bc9287762d7..88d2c0bad10 100644 --- a/tests/toyos.rs +++ b/tests/toyos.rs @@ -149,9 +149,14 @@ const RUST_SKIP: &[&str] = &[ "readdir_bound", // Fills the VFS `created_dirs` cap and leaves it there. `mkdir_cap` runs it. "mkdir_cap", - // Audio is judged on the T14 and nowhere else: the `hda_tone` metal row - // runs it. + // No guest's sound device plays audio: the `hda_tone` and + // `hda_tone_resampled` metal rows run these on the T14 and the AMD bench. "audio_tone", + "audio_tone_resampled", + // It steps the codec's master and reads it back, which no guest's sound + // device has: the `hda_volume_step` metal row runs it on the T14 and the + // AMD bench. + "volume_step", // It hands the ACPI claim to a server of its own and kills it, which needs // a boot that starts none: the `acpi_server_death` metal row runs it on // tests/images/acpicase.toml. @@ -466,6 +471,7 @@ const METAL: &[(&str, metal::Metal)] = &[ }, ), ( + // A tone through soundserver's HDA path. The T14 and the AMD bench. "hda_tone", metal::Metal { arms: TESTCASES, @@ -475,6 +481,32 @@ const METAL: &[(&str, metal::Metal)] = &[ }, }, ), + ( + // A tone at the device rate the device does not run, through cpal, + // played to its end with every period covered: soundserver resamples it. + // The T14 (44.1 kHz) and the AMD bench (48 kHz). + "hda_tone_resampled", + metal::Metal { + arms: TESTCASES, + judge: |b| { + b[0].job_passed("test_rs_audio_tone_resampled")?; + audio::resampled_tone_on_metal(&b[0].log()) + }, + }, + ), + ( + // One step of the codec's master and back, each read back from the + // codec, on the HDA path soundserver configured. The T14 and the AMD + // bench. + "hda_volume_step", + metal::Metal { + arms: TESTCASES, + judge: |b| { + b[0].job_passed("test_rs_volume_step")?; + b[0].log().must_say("soundserver: hda path configured in").map(|_| ()) + }, + }, + ), // ---- one image: tests/images/testcases.toml armed with the chipset watchdog ---- ( // Two boots, and they cannot merge: the control is the same image @@ -1044,6 +1076,8 @@ const TESTCASES: &[metal::Arm] = &[metal::once( "test_rs_wake_storm_cost", "test_rs_wake_onto_busy", "test_rs_audio_tone", + "test_rs_audio_tone_resampled", + "test_rs_volume_step", "test_rs_syscall_cost", "test_rs_spawn_cost", claims::RECLAIM,