A program that wants to make a sound opens /dev/dsp, writes PCM, and closes
it. That is what UNIX audio has always been and it now exists here:
tone 440 2
Three pieces, in the order a sample passes through them. **/dev/dsp** is a
character device where write() is audio and a handful of OSS ioctls say what
the samples are. **AudioEngine** is everything above a driver that is not
specific to a chip -- the format, whether there is room, and what a write does
when there is not. **A driver** is bound by class, exactly as a display driver
is: device_find_class(DEVCLASS_AUDIO).
OSS numbering rather than something new, because every UNIX audio program ever
written already knows it and the only thing a different set would achieve is
making ports harder. They are read/write ioctls, which is the part that
matters: a program asks for 44100, the device sets 48000 and writes 48000
back, the program resamples. One that ignores the answer plays 9% fast, and
that is its bug rather than the driver's.
**The engine is deliberately one stream.** Mixing several programs is a real
feature with a real design behind it, and building it before anything had
played a single sound would be exactly what docs/plan/1.0.md keeps a register
for: work that exists and is connected to nothing. The second program that
wants to play at the same time is what should cause it to be written.
**audio_wait() sleeps on the timer tick, not on the device.** That looks like
the lazy choice and is not: a ring that drains is a ring with room, so asking
again in ten milliseconds always makes progress, whereas sleeping on the card
makes audio depend on an interrupt arriving. On a machine whose line is
misrouted that is not a stutter, it is a writer that never wakes. The handler
still accounts for finished buffers -- deleting it would cost efficiency, not
correctness -- and it retires descriptors through the same `tail` pointer
queue() uses, so the two cannot disagree about what is in flight.
**AC'97 first because HDA is two unproven things at once.** It is the simplest
audio device that exists on real machines and in QEMU both. Intel HDA is what
the laptop actually has and is a much larger driver; writing it against an
interface nothing had ever played through would have meant debugging the
driver and the interface simultaneously. HDA arrives as a second
struct audio_ops and nothing above it changes -- which is the entire point of
the class.
Two things the wiring needed: PCI class 0x04 now maps to DEVCLASS_AUDIO (the
driver bound and /proc/devices showed it, but audio_get() found nothing,
because the class byte had never been translated), and include/x86.h grew
inw() -- AC'97's mixer is all 16-bit registers and reading one a byte at a
time returns the low half twice on some codecs.
**Tested by measuring what came out, not by not crashing.** QEMU's wav backend
writes the guest's audio to a file; `tone 440 2` produced 1.97 seconds of
44.1 kHz stereo with peak amplitude 14177 against the generator's table
maximum of 14178, and 880 zero crossings per second -- 440 Hz exactly. wav is
on every QEMU target rather than a host backend, because a build machine may
have no sound card and it is the only backend whose output can be checked.
Both supported configurations: one boot each, no traps, no panics, socktest
and memfdtest green.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
242 lines
5.4 KiB
C
242 lines
5.4 KiB
C
#pragma once
|
|
|
|
struct dtr {
|
|
ushort limit;
|
|
uint64 base;
|
|
} __attribute__((packed));
|
|
|
|
static inline uchar
|
|
inb(ushort port)
|
|
{
|
|
uchar data;
|
|
asm volatile("in %1,%0" : "=a" (data) : "d" (port));
|
|
return data;
|
|
}
|
|
|
|
// Cycle counter. Not a clock -- it does not convert to time without knowing the
|
|
// frequency, and it is not synchronised across cores -- but for "which of these
|
|
// two paths costs more" it is exactly the right instrument, and the only one
|
|
// with fine enough resolution to see inside a single syscall.
|
|
static inline uint64
|
|
rdtsc(void)
|
|
{
|
|
uint lo, hi;
|
|
asm volatile("rdtsc" : "=a" (lo), "=d" (hi));
|
|
return ((uint64)hi << 32) | lo;
|
|
}
|
|
|
|
static inline void
|
|
insl(int port, void *addr, int cnt)
|
|
{
|
|
asm volatile("cld; rep insl" :
|
|
"=D" (addr), "=c" (cnt) :
|
|
"d" (port), "0" (addr), "1" (cnt) :
|
|
"memory", "cc");
|
|
}
|
|
|
|
static inline void
|
|
outb(ushort port, uchar data)
|
|
{
|
|
asm volatile("out %0,%1" : : "a" (data), "d" (port));
|
|
}
|
|
|
|
// 16 bits. Added for AC'97, whose mixer is entirely 16-bit registers: reading
|
|
// one of them a byte at a time returns the low half twice on some codecs,
|
|
// because the read of the low byte is what latches the pair.
|
|
static inline ushort
|
|
inw(ushort port)
|
|
{
|
|
ushort data;
|
|
asm volatile("inw %1,%0" : "=a" (data) : "d" (port));
|
|
return data;
|
|
}
|
|
|
|
static inline uint
|
|
inl(ushort port)
|
|
{
|
|
uint data;
|
|
asm volatile("inl %1,%0" : "=a" (data) : "d" (port));
|
|
return data;
|
|
}
|
|
|
|
static inline void
|
|
outw(ushort port, ushort data)
|
|
{
|
|
asm volatile("out %0,%1" : : "a" (data), "d" (port));
|
|
}
|
|
|
|
// PCI configuration space is reached through a pair of 32-bit ports, so this
|
|
// is not optional decoration -- see kernel/dev/pci.c.
|
|
static inline void
|
|
outl(ushort port, uint data)
|
|
{
|
|
asm volatile("outl %0,%1" : : "a" (data), "d" (port));
|
|
}
|
|
|
|
static inline void
|
|
outsl(int port, const void *addr, int cnt)
|
|
{
|
|
asm volatile("cld; rep outsl" :
|
|
"=S" (addr), "=c" (cnt) :
|
|
"d" (port), "0" (addr), "1" (cnt) :
|
|
"cc");
|
|
}
|
|
|
|
static inline void
|
|
stosb(void *addr, int data, int cnt)
|
|
{
|
|
asm volatile("cld; rep stosb" :
|
|
"=D" (addr), "=c" (cnt) :
|
|
"0" (addr), "1" (cnt), "a" (data) :
|
|
"memory", "cc");
|
|
}
|
|
|
|
// 64-bit-only helpers. `rep stosq` and `xchgq` do not exist in 32-bit mode, so
|
|
// these must not even be parsed by the -m32 bootloader build -- they compiled
|
|
// there before only because uint64 was accidentally 32 bits wide, which is the
|
|
// bug include/types.h now asserts against.
|
|
#ifdef __x86_64__
|
|
static inline void
|
|
stosq(void *addr, uint64 data, int cnt)
|
|
{
|
|
asm volatile("cld; rep stosq" :
|
|
"=D" (addr), "=c" (cnt) :
|
|
"0" (addr), "1" (cnt), "a" (data) :
|
|
"memory", "cc");
|
|
}
|
|
|
|
struct segdesc;
|
|
|
|
static inline void
|
|
lgdt(struct segdesc *p, int size)
|
|
{
|
|
struct dtr d;
|
|
d.limit = size - 1;
|
|
d.base = (uint64)p;
|
|
asm volatile("lgdt (%0)" : : "r" (&d));
|
|
}
|
|
|
|
struct gatedesc;
|
|
|
|
static inline void
|
|
lidt(struct gatedesc *p, int size)
|
|
{
|
|
struct dtr d;
|
|
d.limit = size - 1;
|
|
d.base = (uint64)p;
|
|
asm volatile("lidt (%0)" : : "r" (&d));
|
|
}
|
|
|
|
static inline void
|
|
ltr(ushort sel)
|
|
{
|
|
asm volatile("ltr %0" : : "r" (sel));
|
|
}
|
|
|
|
static inline uint64
|
|
readeflags(void)
|
|
{
|
|
uint64 flags;
|
|
asm volatile("pushfq; pop %0" : "=r" (flags));
|
|
return flags;
|
|
}
|
|
|
|
static inline void
|
|
cli(void)
|
|
{
|
|
asm volatile("cli");
|
|
}
|
|
|
|
static inline void
|
|
sti(void)
|
|
{
|
|
asm volatile("sti");
|
|
}
|
|
|
|
static inline void
|
|
hlt(void)
|
|
{
|
|
asm volatile("hlt");
|
|
}
|
|
|
|
static inline uint64
|
|
xchg(volatile uint64 *addr, uint64 newval)
|
|
{
|
|
uint64 result;
|
|
asm volatile("lock; xchgq %0, %1" :
|
|
"+m" (*addr), "=a" (result) :
|
|
"1" (newval) :
|
|
"cc");
|
|
return result;
|
|
}
|
|
#endif /* __x86_64__ */
|
|
|
|
static inline uint64
|
|
rcr2(void)
|
|
{
|
|
uint64 val;
|
|
asm volatile("mov %%cr2,%0" : "=r" (val));
|
|
return val;
|
|
}
|
|
|
|
static inline void
|
|
lcr3(uint64 val)
|
|
{
|
|
asm volatile("mov %0,%%cr3" : : "r" (val));
|
|
}
|
|
|
|
// Invalidate the TLB entry for one page. Needed any time a PTE is changed
|
|
// or cleared for an address space that may still be the active CR3 —
|
|
// otherwise the CPU can keep translating through the stale entry until the
|
|
// next full CR3 reload (context switch), silently reading/writing the
|
|
// wrong physical page.
|
|
static inline void
|
|
invlpg(void *va)
|
|
{
|
|
asm volatile("invlpg (%0)" : : "r" (va) : "memory");
|
|
}
|
|
|
|
static inline void
|
|
wrmsr(uint32 msr, uint64 val)
|
|
{
|
|
uint32 low = (uint32)val;
|
|
uint32 high = (uint32)(val >> 32);
|
|
asm volatile("wrmsr" : : "c" (msr), "a" (low), "d" (high));
|
|
}
|
|
|
|
static inline uint64
|
|
rdmsr(uint32 msr)
|
|
{
|
|
uint32 low, high;
|
|
asm volatile("rdmsr" : "=a" (low), "=d" (high) : "c" (msr));
|
|
return ((uint64)high << 32) | low;
|
|
}
|
|
|
|
static inline uint64
|
|
readcs(void)
|
|
{
|
|
uint64 val;
|
|
asm volatile("mov %%cs, %0" : "=r" (val));
|
|
return val;
|
|
}
|
|
|
|
// CPUID. `leaf` in EAX, results in EAX/EBX/ECX/EDX.
|
|
//
|
|
// %rbx is callee-saved *and* the PIC base register on some builds, so it is
|
|
// exchanged out rather than named as an operand -- naming it directly is the
|
|
// classic way to get "inline asm clobbers reserved register" or, worse, silent
|
|
// corruption in the caller.
|
|
static inline void
|
|
cpuid_leaf(uint32 leaf, uint32 *a, uint32 *b, uint32 *c, uint32 *d)
|
|
{
|
|
uint32 ra, rb, rc, rd;
|
|
asm volatile("xchgq %%rbx, %q1\n\t"
|
|
"cpuid\n\t"
|
|
"xchgq %%rbx, %q1"
|
|
: "=a"(ra), "=&r"(rb), "=c"(rc), "=d"(rd)
|
|
: "0"(leaf), "2"(0));
|
|
if (a) *a = ra;
|
|
if (b) *b = rb;
|
|
if (c) *c = rc;
|
|
if (d) *d = rd;
|
|
}
|