From 4816bb5803ee884934a32bec8e2802f0bbb31790 Mon Sep 17 00:00:00 2001 From: Dylan Neve Date: Tue, 27 Oct 2026 17:29:52 +0000 Subject: [PATCH] SCSND: iOS's sound played by the Scion app AVF's own sound device (crosvm's vhost-user virtio-snd, audio=1) failed to start two times in three on the Pixel 10 and took the VM with it, so iOS had no sound under AVF. Now the app lends one more region of RAM (scsnd.addr= scsnd.size=), as it lends the framebuffer's, the camera's and the GPU's: a header and a ring of samples in /dev/hakios-audio's format (sys/hakios.scsnd.h). The new sink (bsd/dev/hakios.scsnd.c, registered from hakios_audio_init by patch 1401) copies what the audio server writes into the ring and never waits; what does not fit is counted and lost. iOS's speaker volume is passed on in the header for the app to apply. avfboot keeps the region out of the kernel's memory. The app side: Scion-Project/scion scsnd/app (scsnd.c plays the ring with AAudio). Co-Authored-By: Claude Opus 5.5 (cherry picked from commit 93884ed9886e187aa409914a6045bc7f3949f749) --- avf/shim/avfboot.c | 11 +++ xnu/files/bsd/dev/hakios.scsnd.c | 92 ++++++++++++++++++++++++++ xnu/files/bsd/sys/hakios.scsnd.h | 44 ++++++++++++ xnu/patches/1401-avf-scion-sound.patch | 27 ++++++++ 4 files changed, 174 insertions(+) create mode 100644 xnu/files/bsd/dev/hakios.scsnd.c create mode 100644 xnu/files/bsd/sys/hakios.scsnd.h create mode 100644 xnu/patches/1401-avf-scion-sound.patch diff --git a/avf/shim/avfboot.c b/avf/shim/avfboot.c index 87b3341ad..8e76d7e23 100644 --- a/avf/shim/avfboot.c +++ b/avf/shim/avfboot.c @@ -718,6 +718,17 @@ void avfboot_main(uint64_t fdt_pa) uart_kv("+", scgpu_size); uart_putc('\n'); } + /* Scion's speaker (SCSND, xnu/files/bsd/sys/hakios.scsnd.h): a ring of samples, likewise */ + uint64_t scsnd_pa = arg_num(m.bootargs, "scsnd.addr="), scsnd_size = arg_num(m.bootargs, "scsnd.size="); + if (scsnd_pa || scsnd_size) { + if (scsnd_pa < m.ram_lo || scsnd_pa + scsnd_size > m.ram_hi || (scsnd_pa & 0xfff) || scsnd_size < 0x10000) + die("scsnd.addr/size do not describe a region in RAM"); + if (scsnd_pa < win_end) + win_end = scsnd_pa & ~(BASE_ALIGN - 1); + uart_kv("avfboot: scsnd ", scsnd_pa); + uart_kv("+", scsnd_size); + uart_putc('\n'); + } if (win_end <= phys_base) die("not enough RAM"); uint64_t win_size = win_end - phys_base; diff --git a/xnu/files/bsd/dev/hakios.scsnd.c b/xnu/files/bsd/dev/hakios.scsnd.c new file mode 100644 index 000000000..52cb94061 --- /dev/null +++ b/xnu/files/bsd/dev/hakios.scsnd.c @@ -0,0 +1,92 @@ +/* + * SCSND: the sink of /dev/hakios-audio under AVF, the Scion app (sys/hakios.scsnd.h, patch 1401). + * + * With scsnd.addr= and scsnd.size= the app has lent a region with a header and a ring of samples. + * What the audio server writes goes into the ring as it comes, and the app plays it through Android's + * audio. Never waits (the writer is the audio server's real-time thread): what does not fit is lost, + * which only happens when the app has stopped taking. iOS's volume is passed on in the header. + */ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +extern vm_offset_t ml_io_map_unmappable(vm_offset_t phys_addr, vm_size_t size, unsigned int flags); + +static volatile struct scsnd_header *scsnd; +static uint8_t *scsnd_ring; +static uint32_t scsnd_bytes; +static uint32_t volume_seen = ~0u; + +/* Called with /dev/hakios-audio's lock held: one writer at a time. */ +static uint32_t +scsnd_play(const int16_t *frames, uint32_t count) +{ + volatile struct scsnd_header *h = scsnd; + uint32_t below, generation; + bool muted; + + if (hakios_audio_volume(&below, &muted, &generation) && generation != volume_seen) { + h->below = below; + h->muted = muted ? 1 : 0; + volume_seen = generation; + } + uint32_t head = h->head; /* (ours alone) */ + uint32_t tail = __atomic_load_n((uint32_t *)(uintptr_t)&h->tail, __ATOMIC_ACQUIRE); + uint32_t used = head - tail, room = used <= scsnd_bytes ? scsnd_bytes - used : 0; + uint32_t bytes = count * HAKIOS_AUDIO_FRAME; + + if (bytes > room) { + bytes = room - room % HAKIOS_AUDIO_FRAME; + } + uint32_t at = head & (scsnd_bytes - 1); + uint32_t first = bytes < scsnd_bytes - at ? bytes : scsnd_bytes - at; + memcpy(scsnd_ring + at, frames, first); + memcpy(scsnd_ring, (const uint8_t *)frames + first, bytes - first); + __atomic_store_n((uint32_t *)(uintptr_t)&h->head, head + bytes, __ATOMIC_RELEASE); + if (bytes < count * HAKIOS_AUDIO_FRAME) { + h->dropped += count - bytes / HAKIOS_AUDIO_FRAME; + } + return bytes / HAKIOS_AUDIO_FRAME; +} + +void +hakios_scsnd_init(void) +{ + uint64_t pa = 0, size = 0; + volatile struct scsnd_header *h; + + if (!PE_parse_boot_argn("scsnd.addr", &pa, sizeof(pa)) || pa == 0) { + return; + } + (void)PE_parse_boot_argn("scsnd.size", &size, sizeof(size)); + if ((pa & PAGE_MASK) || size < 0x10000 || size > (64ull << 20)) { + printf("scsnd: scsnd.addr=%#llx scsnd.size=%#llx: not a region\n", pa, size); + return; + } + h = (volatile struct scsnd_header *)ml_io_map_unmappable((vm_offset_t)pa, (vm_size_t)round_page(size), VM_WIMG_DEFAULT); + if (h == NULL || h->magic != SCSND_MAGIC || h->version != SCSND_VERSION) { + printf("scsnd: no header from the app at %#llx (magic %#x): no sound\n", pa, h != NULL ? h->magic : 0); + return; + } + uint32_t off = h->ring_off, bytes = h->ring_bytes; + if (off < sizeof(struct scsnd_header) || bytes < 4096 || (bytes & (bytes - 1)) || (uint64_t)off + bytes > size || + h->rate != HAKIOS_AUDIO_RATE || h->channels != HAKIOS_AUDIO_CHANNELS) { + printf("scsnd: the app's ring (%u bytes at %u, %u Hz, %u channels) is not one to write into\n", bytes, off, + h->rate, h->channels); + return; + } + scsnd = h; + scsnd_ring = (uint8_t *)(uintptr_t)h + off; + scsnd_bytes = bytes; + __atomic_store_n((uint32_t *)(uintptr_t)&h->guest_ready, 1, __ATOMIC_RELEASE); + printf("scsnd: the app's speaker at %#llx: a ring of %u ms\n", pa, + bytes / HAKIOS_AUDIO_FRAME * 1000 / HAKIOS_AUDIO_RATE); + hakios_audio_set_sink("the Scion app (SCSND)", scsnd_play); +} diff --git a/xnu/files/bsd/sys/hakios.scsnd.h b/xnu/files/bsd/sys/hakios.scsnd.h new file mode 100644 index 000000000..e9bc383f6 --- /dev/null +++ b/xnu/files/bsd/sys/hakios.scsnd.h @@ -0,0 +1,44 @@ +/* + * SCSND: iOS's sound played by the Scion app (patch 1401, for the Android Virtualization Framework). + * + * AVF's own sound device (crosvm's vhost-user virtio-snd) failed to start two times in three on the + * Pixel 10, and the VM went with it. Instead the app lends the guest a region of RAM, as it lends + * the framebuffer's (SCFB), the camera's (SCCAM) and the GPU's (SCGPU), and says where with + * scsnd.addr= scsnd.size= in the kernel parameters; avfboot keeps it out of the kernel's memory. + * The region begins with the header below, which the app writes before the VM starts, and holds a + * ring of samples in /dev/hakios-audio's one format (sys/hakios.audio.h: 48000 frames a second, + * 16-bit, left then right). The kernel's sink (bsd/dev/hakios.scsnd.c) puts in what the audio server + * writes; the app takes it out and plays it through Android's audio (AAudio). + * + * The ring: ring_bytes (a power of two, whole frames) at ring_off from the region's start. head and + * tail count bytes, free-running, modulo 2^32: the guest alone moves head (after the samples are in, + * with a barrier), the app alone moves tail (after it has taken them). What is in: head - tail. The + * guest never waits: what does not fit is lost (counted in dropped). The app never lets more than a + * little build up: if it finds too much, it skips ahead (counted in skipped). + * + * The volume iOS sets on its speaker (HAKIOS_AUDIO_SET_VOLUME) is passed on in below/muted, and the + * app scales by it: iOS's volume is then a part of Android's media volume. + */ +#ifndef _SYS_HAKIOS_SCSND_H_ +#define _SYS_HAKIOS_SCSND_H_ + +#include + +#define SCSND_MAGIC 0x4e534353u /* "SCSN" */ +#define SCSND_VERSION 1u + +struct scsnd_header { + uint32_t magic, version; /* the app's */ + uint32_t ring_off, ring_bytes; /* the app's: where the ring is, and how big */ + uint32_t rate, channels; /* the app's: 48000, 2 (what it plays) */ + uint32_t head; /* the guest's: bytes put in */ + uint32_t tail; /* the app's: bytes taken out */ + uint32_t below, muted; /* the guest's: iOS's volume, eighths of a dB under the loudest; muted */ + uint32_t guest_ready; /* the guest's: 1 once its sink is in place */ + uint32_t dropped; /* the guest's: frames that did not fit */ + uint32_t skipped; /* the app's: frames it skipped to catch up */ + uint32_t underruns; /* the app's: times it had less than it was asked for */ + uint32_t reserved[18]; /* (128 bytes in all) */ +}; + +#endif diff --git a/xnu/patches/1401-avf-scion-sound.patch b/xnu/patches/1401-avf-scion-sound.patch new file mode 100644 index 000000000..3aab6790e --- /dev/null +++ b/xnu/patches/1401-avf-scion-sound.patch @@ -0,0 +1,27 @@ +diff --git a/bsd/conf/files b/bsd/conf/files +index dcc51a5..48d9d32 100644 +--- a/bsd/conf/files ++++ b/bsd/conf/files +@@ -145,6 +145,7 @@ bsd/dev/hakios.apfs_check.c optional apfs + bsd/dev/hakios.sound.c standard + bsd/dev/hakios.vgpu3d.c standard + bsd/dev/hakios.vgpu3d_xnu.c standard ++bsd/dev/hakios.scsnd.c standard + bsd/dev/hakios.qmi.c standard + bsd/dev/hakios.nas.c standard + bsd/dev/hakios.wlfw.c standard +diff --git a/bsd/dev/hakios.audio.c b/bsd/dev/hakios.audio.c +index de97d92..77b49c4 100644 +--- a/bsd/dev/hakios.audio.c ++++ b/bsd/dev/hakios.audio.c +@@ -347,6 +347,10 @@ hakios_audio_init(void) + report_call = thread_call_allocate(audio_report, NULL); + audio_recording_init(); + hakios_virtio_sound_init(); ++ { ++ extern void hakios_scsnd_init(void); ++ hakios_scsnd_init(); /* scsnd.addr=: the Scion app plays it (bsd/dev/hakios.scsnd.c) */ ++ } + hakios_speaker_init(); + hakios_haptic_init(); + hakios_torch_init();