SchOS/kernel/dev/cdev.c
Nexoniarz 9fea18b18b audio: /dev/dsp, AudioEngine, and an AC'97 under it
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>
2026-09-24 14:23:13 +02:00

218 lines
7.2 KiB
C

#include "types.h"
#include "defs.h"
#include "param.h"
#include "spinlock.h"
#include "sleeplock.h"
#include "fs.h"
#include "file.h"
#include "x86.h"
#include "errno.h"
extern int consoleread(struct inode*, uint64, char*, int);
extern int consolewrite(struct inode*, uint64, char*, int);
extern int mouseread(struct inode*, uint64, char*, int);
// --- /dev/random and /dev/urandom -------------------------------------------
//
// This was a linear congruential generator seeded with a constant, which means
// **every boot produced the same bytes in the same order**. Not weak: the same.
// A program asking for a key, a nonce, a temporary filename or a hash seed got
// one it shared with every other SchOS machine that had ever run.
//
// What is here now is still not a CSPRNG and does not pretend to be. It is an
// LCG that is *seeded* from something real and re-stirred from the cycle
// counter on every read, which fixes the one property that made the old one
// dangerous rather than merely poor. See "What it does not do" in
// docs/architecture/devices.md.
static uint64 rseed;
static int rseed_ready;
static void rseed_init(void);
// RDRAND, if this CPU has it. CPUID leaf 1, ECX bit 30.
//
// Checked rather than assumed: the instruction faults with #UD on a CPU that
// does not implement it, and "the random device is a kernel panic" is a worse
// failure than a weak seed.
static int
rdrand64(uint64 *out)
{
uint32 a, b, c, d;
unsigned char ok = 0;
uint64 v = 0;
cpuid_leaf(1, &a, &b, &c, &d);
if (!(c & (1u << 30)))
return 0;
// Ten tries, per Intel's own guidance: the instruction is allowed to fail
// when its entropy pool is momentarily empty, and it says so in CF.
for (int i = 0; i < 10; i++) {
asm volatile("rdrand %0; setc %1" : "=r"(v), "=qm"(ok) :: "cc");
if (ok) { *out = v; return 1; }
}
return 0;
}
// Seeded at boot rather than on the first read.
//
// Lazy would work, but it puts the one line that says where the entropy came
// from in the middle of whatever the machine was doing hours later, and it
// makes the first reader pay for the CPUID. Called from main(); the lazy path
// stays as a belt-and-braces for anything that reads before init runs.
void
random_init(void)
{
if (!rseed_ready)
rseed_init();
}
static void
rseed_init(void)
{
uint64 v;
rseed_ready = 1;
if (rdrand64(&v)) {
rseed = v;
kinfo("random: seeded from RDRAND\n");
return;
}
// No RDRAND. The cycle counter is the only thing here that is different on
// every boot and not written down anywhere -- it is not entropy in any
// serious sense, but it is not a constant either, which was the whole
// problem. Mixed with the wall clock so that two machines starting at the
// same instant do not agree.
rseed = rdtsc();
rseed ^= boot_epoch * 0x9E3779B97F4A7C15ULL;
rseed ^= (uint64)(uintptr_t)&rseed;
if (rseed == 0)
rseed = 0x9E3779B97F4A7C15ULL;
kinfo("random: no RDRAND; seeded from the cycle counter and the clock\n");
}
int
nullwrite(struct inode *ip, uint64 off, char *dst, int n)
{
return n;
}
int
nullread(struct inode *ip, uint64 off, char *dst, int n)
{
return 0;
}
int
zeroread(struct inode *ip, uint64 off, char *dst, int n)
{
memset(dst, 0, n);
return n;
}
int
randomread(struct inode *ip, uint64 off, char *dst, int n)
{
if(!rseed_ready)
rseed_init();
for(int i = 0; i < n; i++){
// Re-stirred from the cycle counter every byte, so two reads a moment
// apart do not continue one sequence -- and so that a process cannot
// predict the next caller's bytes from its own.
rseed ^= rdtsc();
rseed = rseed * 6364136223846793005ULL + 1442695040888963407ULL;
dst[i] = (rseed >> 33) & 0xFF;
}
return n;
}
// /dev/full — reads as an endless run of zero bytes, every write fails with
// ENOSPC. Programs use it to exercise their out-of-disk-space paths.
int
fullwrite(struct inode *ip, uint64 off, char *dst, int n)
{
return -ENOSPC;
}
int
mousewrite(struct inode *ip, uint64 off, char *dst, int n)
{
return n;
}
int
memread(struct inode *ip, uint64 off, char *dst, int n)
{
switch(ip->minor) {
case 1: return consoleread(ip, off, dst, n);
case 2: return nullread(ip, off, dst, n);
case 3: return consoleread(ip, off, dst, n);
/* 10 = /dev/tty, 11..16 = /dev/tty1..tty6. consoleread resolves the
minor to a terminal; see vt_for_minor(). */
case 10: case 11: case 12: case 13: case 14: case 15: case 16:
return consoleread(ip, off, dst, n);
case 5: return zeroread(ip, off, dst, n);
case 7: return zeroread(ip, off, dst, n); // /dev/full reads as zeros
case 8:
case 9: return randomread(ip, off, dst, n);
// Not "operation not permitted", which is what -1 means once it reaches
// userspace. There is no device on this minor.
default: return -ENODEV;
}
}
int
memwrite(struct inode *ip, uint64 off, char *dst, int n)
{
switch(ip->minor) {
case 1: return consolewrite(ip, off, dst, n);
case 10: case 11: case 12: case 13: case 14: case 15: case 16:
return consolewrite(ip, off, dst, n);
case 7: return fullwrite(ip, off, dst, n); // /dev/full: always ENOSPC
case 2:
case 5:
case 8:
case 9: return nullwrite(ip, off, dst, n);
default: return -ENODEV;
}
}
struct devsw devsw[] = {
[0] = { 0, 0 },
[1] = { memread, memwrite },
// /dev/fb0 (major 2) — the framebuffer, kernel/dev/fb.c. This used to be
// { nullread, nullwrite }, so writing pixels to it succeeded and did
// nothing.
// The close hook gives the text console its screen back when whatever was
// drawing on the framebuffer exits — see fbclose().
[2] = { fbread, fbwrite, 0, fbclose },
[3] = { mouseread, mousewrite },
[4] = { 0, 0 },
// /dev/tty (major 5) — alias for the console
[5] = { memread, memwrite },
// /proc status files (major 6) — see kernel/dev/procfs.c
[6] = { procread, procwrite },
// /dev/input/eventN (major 7) — raw evdev events, kernel/dev/input.c. The
// minor is the device index, so event0 is the keyboard and event1 the mouse.
[7] = { inputread, inputwrite, inputready, inputclose, inputopen },
// /dev/hdN (major 8) — raw disks, kernel/dev/disk.c. The minor is the IDE
// device number. No `ready` hook: a disk is always ready, which is what
// fileread assumes when one is absent.
[8] = { diskread, diskwrite },
// /dev/pci (major 9) -- PCI devices reachable from userspace, for drivers
// that do not live in the kernel. Registers arrive through mmap and
// everything else through ioctl; see include/pciuser.h.
[9] = { pciuser_read, pciuser_write },
// /dev/dbg (major 10) -- ask the kernel a question. Write a line, read the
// answer; see include/dbg.h and docs/architecture/debugging.md.
[10] = { dbg_read, dbg_write },
// /dev/netdev (major 11) -- a process, being a network interface. The
// reverse of major 9: that hands a device to a process, this lets a process
// be one. See include/netuser.h.
[11] = { netuser_read, netuser_write, netuser_ready },
// /dev/dsp (major 12) -- UNIX audio, kernel/dev/audio.c. Write PCM to it.
// The close hook plays out what is still queued: a program that writes a
// sound and exits expects to hear the end of it.
[12] = { dspread, dspwrite, 0, dspclose },
};