From 7ff66b4ce01a7891fce15eb5de8f0a15febc6679 Mon Sep 17 00:00:00 2001 From: Dylan Neve Date: Wed, 21 Oct 2026 20:54:21 +0100 Subject: [PATCH 1/2] t8030: opt-in audio hardware (audio=on, audiodev=) Upstream already models the MCA audio controller, the CS42L77 codec and the CS35L27 amp, but t8030 deletes every audio node from the device tree, so iOS never loads its audio drivers, and the MCA's sound card can't be pointed at a backend (with any -audiodev on the command line it failed with "Failed to create QEMU sound card"). - New machine options: audio=on keeps the MCA switch/cluster, CS42L77, CS35L27 and audio resource manager nodes and creates the models; audiodev= picks the -audiodev for the speaker output. Default off: without audio=on the guest sees the same device tree as before and the (unused) audio models are no longer created. - MCA: a realized qdev with an "audiodev" property; the voice is opened in realize and errors are reported instead of printed. - Trace events for the MCA, CS35L27 and CS42L77 instead of #if 0 printfs. Co-Authored-By: Claude Opus 5.5 --- hw/arm/boot.c | 30 +++++++++++------- hw/arm/t8030.c | 32 ++++++++++++++++--- hw/audio/cs35l27.c | 5 +-- hw/audio/cs42l77.c | 2 ++ hw/audio/mca.c | 71 +++++++++++++++++++++++++----------------- hw/audio/trace-events | 16 ++++++++++ include/hw/arm/boot.h | 2 ++ include/hw/arm/t8030.h | 2 ++ 8 files changed, 112 insertions(+), 48 deletions(-) diff --git a/hw/arm/boot.c b/hw/arm/boot.c index 8bbd119cc56..509a737af90 100644 --- a/hw/arm/boot.c +++ b/hw/arm/boot.c @@ -65,12 +65,7 @@ static const char* KEEP_COMP[] = { // "aop-audio-hpdbg\0$", // "audio-data,aop-audio-hpdbg\0$", // "haptics-support,leap\0$", - // "audio,embedded-resource-manager\0$", - // "audio-control,cs35l27\0$", // "audio-data,audio-loopback\0$", - // "audio-data,cs35l27\0$", - // "audio-data,cs42l77\0$", - // "audio-control,cs42l77\0$", "aes,s8000\0$", "aic,1\0$", "apcie-bridge\0$", @@ -108,9 +103,6 @@ static const char* KEEP_COMP[] = { "iop,s8000\0$", "iop-nub,sep\0$", "lcd,pinot\0$", - // "mca-switch,t8030\0$", - // "mcaCluster,t8030\0$", - // "mca,t8030\0$", "mipi-dsim-1,synopsys\0$", "N104AP\0iPhone12,1\0AppleARM\0$", "N104DEV\0iPhone12,1\0AppleARM\0$", @@ -174,6 +166,19 @@ static const char* KEEP_COMP[] = { "i2c,t8101\0i2c,s5l8940x\0iic,soft\0$", }; +// Kept only with audio=on: the speaker path through the MCA (I2S) clusters, +// the CS42L77 codec on SPI3 and the CS35L27 amp on I2C2. +static const char* AUDIO_COMP[] = { + "mca-switch,t8030\0$", + "mcaCluster,t8030\0$", + "mca,t8030\0$", + "audio-control,cs35l27\0$", + "audio-data,cs35l27\0$", + "audio-control,cs42l77\0$", + "audio-data,cs42l77\0$", + "audio,embedded-resource-manager\0$", +}; + static const char* REM_NAMES[] = { "accel\0$", "gyro\0$", @@ -292,7 +297,7 @@ static bool apple_boot_dt_match(const AppleDTProp* prop, const char** list, size return false; } -static void apple_boot_process_dt_node(AppleDTNode* node, AppleDTNode* parent, bool keep_gpu) +static void apple_boot_process_dt_node(AppleDTNode* node, AppleDTNode* parent, bool keep_gpu, bool audio) { GList* iter = NULL; AppleDTNode* child = NULL; @@ -302,7 +307,8 @@ static void apple_boot_process_dt_node(AppleDTNode* node, AppleDTNode* parent, b if ((prop = apple_dt_get_prop(node, "compatible")) != NULL) { assert_nonnull(prop->data); - found = keep_gpu && apple_boot_dt_match(prop, GPU_KEEP_COMP, ARRAY_SIZE(GPU_KEEP_COMP)); + found = (keep_gpu && apple_boot_dt_match(prop, GPU_KEEP_COMP, ARRAY_SIZE(GPU_KEEP_COMP))) + || (audio && apple_boot_dt_match(prop, AUDIO_COMP, ARRAY_SIZE(AUDIO_COMP))); for (i = 0; !found && i < ARRAY_SIZE(KEEP_COMP); i++) { if (memcmp(prop->data, KEEP_COMP[i], MIN(prop->len, sstrlen(KEEP_COMP[i]))) == 0) { found = true; @@ -359,7 +365,7 @@ static void apple_boot_process_dt_node(AppleDTNode* node, AppleDTNode* parent, b // iter might get invalidated iter = iter->next; - apple_boot_process_dt_node(child, node, keep_gpu); + apple_boot_process_dt_node(child, node, keep_gpu, audio); } } @@ -617,7 +623,7 @@ void apple_boot_populate_dt(AppleDTNode* root, AppleBootInfo* info, bool auto_bo apple_boot_init_mem_ranges(root); - apple_boot_process_dt_node(root, NULL, info->keep_gpu); + apple_boot_process_dt_node(root, NULL, info->keep_gpu, info->audio); // Prevent further additions. info->device_tree_size = ROUND_UP_16K(apple_dt_finalise(root)); diff --git a/hw/arm/t8030.c b/hw/arm/t8030.c index d74bf7ce42b..8c5ec5a5c30 100644 --- a/hw/arm/t8030.c +++ b/hw/arm/t8030.c @@ -1339,6 +1339,7 @@ static void t8030_memory_setup(AppleT8030MachineState* t8030) if (t8030->sptm != NULL) { apple_sptm_allocate_memory_map(memory_map, &t8030->sptm_layout); } info->keep_gpu = t8030->gpu; + info->audio = t8030->audio; apple_boot_populate_dt(t8030->device_tree, info, auto_boot); switch (hdr->file_type) { @@ -3045,6 +3046,8 @@ static void t8030_create_mca(AppleT8030MachineState* t8030) sysbus_mmio_map(sbd, APPLE_MCA_MMIO_DMA, t8030->armio_base + ldq_le_p(prop->data + sizeof(uint64_t) * 2)); sysbus_mmio_map(sbd, APPLE_MCA_MMIO_MCLK_CFG, t8030->armio_base + ldq_le_p(prop->data + sizeof(uint64_t) * 4)); + if (t8030->audiodev != NULL) { qdev_prop_set_string(DEVICE(sbd), "audiodev", t8030->audiodev); } + object_property_add_child(OBJECT(t8030), "mca", OBJECT(sbd)); sysbus_realize_and_unref(sbd, &error_fatal); } @@ -3343,9 +3346,15 @@ static void t8030_create_devices(AppleT8030MachineState* t8030) t8030_create_mt_spi(t8030); t8030_create_aop(t8030); if (t8030->gpu) { t8030_create_gpu(t8030); } - t8030_create_mca(t8030); - t8030_create_speaker_top(t8030); - t8030_create_speaker_bottom(t8030); + if (t8030->audio) { + t8030_create_mca(t8030); + t8030_create_speaker_top(t8030); + t8030_create_speaker_bottom(t8030); + } + else if (t8030->audiodev != NULL) { + error_setg(&error_fatal, "audiodev= needs audio=on"); + return; + } t8030_create_buttons(t8030); t8030_create_mipi_dsim(t8030); t8030_create_scaler(t8030); @@ -3421,8 +3430,14 @@ static void t8101_create_devices(AppleT8030MachineState* t8030) t8030_create_roswell(t8030); t8030_create_display(t8030); t8030_create_aop(t8030); - t8030_create_mca(t8030); - t8030_create_speaker_top(t8030); + if (t8030->audio) { + t8030_create_mca(t8030); + t8030_create_speaker_top(t8030); + } + else if (t8030->audiodev != NULL) { + error_setg(&error_fatal, "audiodev= needs audio=on"); + return; + } t8030_create_buttons(t8030); t8030_create_scaler(t8030); t8030_create_accbuck(t8030); @@ -3728,6 +3743,8 @@ PROP_VISIT_GETTER_SETTER(uint32, disp_height); PROP_GETTER_SETTER(bool, enable_pac); PROP_GETTER_SETTER(bool, patch_check); PROP_GETTER_SETTER(bool, gpu); +PROP_GETTER_SETTER(bool, audio); +PROP_STR_GETTER_SETTER(audiodev); static void apple_soc_machine_class_init(ObjectClass* klass, const AppleSoCConfig* soc) { @@ -3809,6 +3826,11 @@ static void apple_soc_machine_class_init(ObjectClass* klass, const AppleSoCConfi object_class_property_set_description(klass, "gpu", "Expose the AGX GPU and emulate its coprocessor's firmware " "(experimental, docs/GPU-EMULATION-SCOPE.md). Default off."); + object_class_property_add_bool(klass, "audio", t8030_get_audio, t8030_set_audio); + object_class_property_set_description(klass, "audio", + "Expose the audio hardware (MCA, CS42L77 codec, CS35L27 amp) to iOS"); + object_class_property_add_str(klass, "audiodev", t8030_get_audiodev, t8030_set_audiodev); + object_class_property_set_description(klass, "audiodev", "ID of the -audiodev that plays the speaker output"); } static void t8030_class_init(ObjectClass* klass, const void* data) { apple_soc_machine_class_init(klass, &t8030_soc); } diff --git a/hw/audio/cs35l27.c b/hw/audio/cs35l27.c index 6b79b886e6f..3d504770522 100644 --- a/hw/audio/cs35l27.c +++ b/hw/audio/cs35l27.c @@ -20,6 +20,7 @@ #include "qemu/osdep.h" #include "hw/audio/cs35l27.h" #include "hw/i2c/i2c.h" +#include "trace.h" #if 0 #define DPRINTF(v, ...) fprintf(stderr, v, ##__VA_ARGS__) @@ -86,7 +87,7 @@ static uint8_t apple_cs35l27_rx(I2CSlave* i2c) break; } - DPRINTF("%s: addr=0x%X, ret=0x%X\n", __func__, s->addr, ret); + trace_apple_cs35l27_read(s->addr, ret); s->addr += 1; if (s->addr == 4) { @@ -114,7 +115,7 @@ static int apple_cs35l27_tx(I2CSlave* i2c, uint8_t data) s->tx_bytes += 1; } else if (s->tx_bytes < 0x40) { - DPRINTF("%s: addr=0x%X,data=0x%02X\n", __func__, s->addr, data); + trace_apple_cs35l27_write(s->addr, data); if (s->addr < 0x1000000) { s->regs[s->addr] = data; } s->addr += 1; s->tx_bytes += 1; diff --git a/hw/audio/cs42l77.c b/hw/audio/cs42l77.c index ca002bbfad7..3bad76848da 100644 --- a/hw/audio/cs42l77.c +++ b/hw/audio/cs42l77.c @@ -23,6 +23,7 @@ #include "hw/ssi/ssi.h" #include "qemu/bswap.h" #include "qemu/lockable.h" +#include "trace.h" #if 0 #define DPRINTF(v, ...) fprintf(stderr, v, ##__VA_ARGS__) @@ -227,6 +228,7 @@ static uint32_t apple_cs42l77_transfer(SSIPeripheral* dev, uint32_t val) } if (s->pos == s->end_pos) { + trace_apple_cs42l77_cmd(s->cur_cmd, s->start_addr, s->length); DPRINTF("%s: cmd 0x%X end at 0x%X, start_addr=0x%llX, address=0x%llX, " "len=0x%X\n", __func__, s->cur_cmd, s->pos, s->start_addr, s->address, s->length); diff --git a/hw/audio/mca.c b/hw/audio/mca.c index 0c200f75ef4..67e431833db 100644 --- a/hw/audio/mca.c +++ b/hw/audio/mca.c @@ -19,18 +19,14 @@ #include "qemu/osdep.h" #include "audio/audio.h" +#include "hw/qdev-properties.h" +#include "hw/qdev-properties-system.h" #include "hw/audio/mca.h" +#include "qapi/error.h" #include "qemu/error-report.h" #include "qemu/fifo32.h" #include "qemu/timer.h" - -#if 0 - #define MCA_DPRINTF(fmt, ...) fprintf(stderr, fmt, ##__VA_ARGS__) -#else - #define MCA_DPRINTF(fmt, ...) \ - do { } \ - while (0) -#endif +#include "trace.h" // MMIO Index 0: SmartIO MCA #define MCA_SIO_REG_STRIDE (0x4000) @@ -177,6 +173,12 @@ struct AppleMCAState uint32_t ring_used; }; +static void apple_mca_realize(DeviceState* dev, Error** errp); + +static const Property apple_mca_properties[] = { + DEFINE_AUDIO_PROPERTIES(AppleMCAState, card), +}; + static void apple_mca_class_init(ObjectClass* klass, const void* data) { DeviceClass* dc; @@ -185,7 +187,8 @@ static void apple_mca_class_init(ObjectClass* klass, const void* data) dc->desc = "Apple Multi-Channel Audio"; dc->user_creatable = false; - // dc->realize = apple_mca_realize; + dc->realize = apple_mca_realize; + device_class_set_props(dc, apple_mca_properties); set_bit(DEVICE_CATEGORY_SOUND, dc->categories); } @@ -194,6 +197,7 @@ OBJECT_DEFINE_SIMPLE_TYPE_CLASS_INIT(AppleMCAState, apple_mca, APPLE_MCA, SYS_BU static uint32_t apple_mca_dma_into_ring(AppleMCAState* s, uint32_t want) { uint32_t done = 0; + uint32_t real = 0; if (want > (MCA_RING_SIZE - s->ring_used)) { uint32_t drop = want - (MCA_RING_SIZE - s->ring_used); @@ -213,11 +217,13 @@ static uint32_t apple_mca_dma_into_ring(AppleMCAState* s, uint32_t want) got = apple_sio_dma_read(s->tx_ep, s->ring + tail, run); if (got < run) { memset(s->ring + tail + got, 0, run - got); } + real += got; s->ring_used += run; done += run; } + trace_apple_mca_pull(want, real); return done; } @@ -260,6 +266,7 @@ static void apple_mca_set_running(AppleMCAState* s, bool running) if (running == s->running) { return; } s->running = running; + trace_apple_mca_running(running); AUD_set_active_out(s->voice, running); AUD_set_volume_out(s->voice, !running, 255, 255); @@ -282,7 +289,7 @@ static void apple_mca_sio_reg_write(void* opaque, hwaddr addr, uint64_t data, un index = addr / MCA_SIO_REG_STRIDE; off = addr % MCA_SIO_REG_STRIDE; - MCA_DPRINTF("sio_cluster_%lld: 0x%llX <- 0x%llX\n", index, off, data); + trace_apple_mca_sio_write(index, off, data); switch (off) { case RXB_REG_BASE + REG_RX_CFG: s->sio_clusters[index].rxb_config = data; break; @@ -311,7 +318,7 @@ static uint64_t apple_mca_sio_reg_read(void* const opaque, const hwaddr addr, co case TXB_REG_BASE + REG_SIO_UNIT_CTL: ret = s->sio_clusters[index].txb_control; break; } - MCA_DPRINTF("sio_cluster_%lld: 0x%llX -> 0x%llX\n", index, off, ret); + trace_apple_mca_sio_read(index, off, ret); return ret; } @@ -335,10 +342,12 @@ static void apple_mca_dma_reg_write(void* opaque, hwaddr addr, uint64_t data, un index = addr / MCA_DMA_REG_STRIDE; off = addr % MCA_DMA_REG_STRIDE; - MCA_DPRINTF("dma_cluster_%lld: 0x%llX <- 0x%llX\n", index, off, data); switch (off) { - case REG_MCA_DMA_CFG : s->dma_clusters[index].config = data; break; + case REG_MCA_DMA_CFG: + trace_apple_mca_dma_cfg(index, data); + s->dma_clusters[index].config = data; + break; case REG_MCA_DMA_FIFO_INOUT: fifo32_push(&s->dma_clusters[index].fifo, data); break; default : break; } @@ -367,7 +376,6 @@ static uint64_t apple_mca_dma_reg_read(void* const opaque, const hwaddr addr, co break; default: ret = 0; break; } - MCA_DPRINTF("dma_cluster_%lld: 0x%llX -> 0x%llX\n", index, off, ret); return ret; } @@ -391,7 +399,7 @@ static void apple_mca_mclk_reg_write(void* opaque, hwaddr addr, uint64_t data, u index = addr / MCLK_CFG_REG_STRIDE; off = addr % MCLK_CFG_REG_STRIDE; - MCA_DPRINTF("mclk_cluster_%lld: 0x%llX <- 0x%llX\n", index, off, data); + trace_apple_mca_mclk_write(index, data); switch (off) { case REG_MCLK_CFG: s->mclk_clusters[index] = (uint32_t)data; break; @@ -413,7 +421,6 @@ static uint64_t apple_mca_mclk_reg_read(void* const opaque, const hwaddr addr, c case REG_MCLK_CFG: ret = s->mclk_clusters[index]; break; default : ret = 0; break; } - MCA_DPRINTF("mclk_cluster_%lld: 0x%llX -> 0x%llX\n", index, off, ret); return ret; } @@ -487,18 +494,24 @@ SysBusDevice* apple_mca_create(AppleDTNode* node, AppleSIODMAEndpoint* tx_ep, Ap s->timer = timer_new_ns(QEMU_CLOCK_VIRTUAL, apple_mca_serialise, s); - if (AUD_register_card("mca", &s->card, NULL)) { - audsettings settings = {0}; - settings.fmt = AUDIO_FORMAT_S16; - settings.freq = 48000; - settings.nchannels = 2; - settings.endianness = 0; // LE - s->voice = AUD_open_out(&s->card, s->voice, "mca.out", s, apple_mca_out_callback, &settings); - if (s->voice == NULL) { error_report("Failed to create voice for Multi-Channel Audio"); } - } - else { - error_report("Failed to create QEMU sound card for Multi-Channel Audio"); - } - return sbd; } + +static void apple_mca_realize(DeviceState* dev, Error** errp) +{ + AppleMCAState* s = APPLE_MCA(dev); + audsettings settings = {0}; + + // Without audiodev= this uses the default backend (none if QEMU has no driver for the host). + if (!AUD_register_card("mca", &s->card, errp)) { return; } + + settings.fmt = AUDIO_FORMAT_S16; + settings.freq = MCA_FRAME_RATE; + settings.nchannels = MCA_CHANNELS; + settings.endianness = 0; // LE + s->voice = AUD_open_out(&s->card, s->voice, "mca.out", s, apple_mca_out_callback, &settings); + if (s->voice == NULL) { + error_setg(errp, "Failed to open the Multi-Channel Audio output voice"); + AUD_remove_card(&s->card); + } +} diff --git a/hw/audio/trace-events b/hw/audio/trace-events index e69de29bb2d..3a2af1c178e 100644 --- a/hw/audio/trace-events +++ b/hw/audio/trace-events @@ -0,0 +1,16 @@ +# See docs/devel/tracing.rst for the syntax documentation. + +# mca.c +apple_mca_sio_write(uint64_t cluster, uint64_t off, uint64_t val) "cluster %" PRIu64 " 0x%03" PRIx64 " <- 0x%" PRIx64 +apple_mca_sio_read(uint64_t cluster, uint64_t off, uint64_t val) "cluster %" PRIu64 " 0x%03" PRIx64 " -> 0x%" PRIx64 +apple_mca_dma_cfg(uint64_t index, uint64_t val) "dma %" PRIu64 " cfg <- 0x%" PRIx64 +apple_mca_mclk_write(uint64_t index, uint64_t val) "mclk %" PRIu64 " <- 0x%" PRIx64 +apple_mca_running(int running) "speaker TX %d" +apple_mca_pull(uint32_t want, uint32_t got) "want %u got %u bytes from DMA" + +# cs35l27.c +apple_cs35l27_write(uint32_t addr, uint8_t val) "0x%06x <- 0x%02x" +apple_cs35l27_read(uint32_t addr, uint8_t val) "0x%06x -> 0x%02x" + +# cs42l77.c +apple_cs42l77_cmd(uint8_t cmd, uint64_t addr, uint16_t len) "cmd 0x%02x addr 0x%" PRIx64 " len %u" diff --git a/include/hw/arm/boot.h b/include/hw/arm/boot.h index 1e422b2bea7..43c55db56ef 100644 --- a/include/hw/arm/boot.h +++ b/include/hw/arm/boot.h @@ -376,6 +376,8 @@ typedef struct bool non_cold_boot; bool had_autoboot; bool keep_gpu; // keep the GPU's device-tree nodes (t8030 gpu=on) + // Keep the audio nodes (MCA, codec, amps) in the device tree. + bool audio; AppleBootMode boot_mode; } AppleBootInfo; diff --git a/include/hw/arm/t8030.h b/include/hw/arm/t8030.h index 4aeb4c7ed94..0b57a2915e2 100644 --- a/include/hw/arm/t8030.h +++ b/include/hw/arm/t8030.h @@ -162,4 +162,6 @@ typedef struct bool enable_pac; bool patch_check; bool gpu; + bool audio; + char* audiodev; } AppleT8030MachineState; From d3187aa2043ca8eafe8855375df326dfb73bc9c1 Mon Sep 17 00:00:00 2001 From: Makr Date: Thu, 22 Oct 2026 20:08:36 +0100 Subject: [PATCH 2/2] audio: AOP audio, a full audio device tree and an I2S drain for audio=on (port from MakrSas/Inferno) Folds MakrSas/Inferno's audio work (branch ios) into Pyre's opt-in audio=on. MakrSas reports playback and recording working end to end from an iOS app to the emulator's audio backend with it. Everything here applies only with -M t8030,audio=on; without it the guest sees the same device tree as before and none of these models are created. - The AOP audio service (hw/audio/aop-audio.c) is created with audio=on. MakrSas gated it on the INFERNO_AUDIO environment variable; here it is the machine property. The service now: * reports one IO handler and a version, and answers attach/detach and the buffer-size question, so AppleAOPAudioController no longer fails _getBuffer and re-handshakes forever; * records the power state the guest requests (DEV_PROP_REQUESTED_STATE) and reports it back, so the codec's MCLK manager stops re-requesting; * answers RegisterAccess with zeros; * builds replies in a local buffer and copies out only as much as the guest's buffer holds, instead of writing past a smaller one; * bounds-checks GetDeviceID's index (new here). The debug printfs stay off (#if 0). - boot.c: AUDIO_COMP grows to the whole tree MakrSas found the audio server needs: the AOP audio nodes, audio-aop-mca2, the low-power mic, the CS42L77's halogen/hawking/dsp-debug1/mikeybus/loopback data nodes, the alc links and the haptics chain (without the actuator the built-in route is empty, so AVAudioSession throws 'nort'). The aop-mca name is exempt from REM_NAMES under audio=on. MakrSas kept audio-aop-mca2 in KEEP_COMP for every boot; here it is in AUDIO_COMP. - t8030: a 5 ms timer drains every I2S SIO DMA channel except the speaker's, so transfers queued on ports that nothing models complete and the audio server is not killed by its watchdog. - MCA: the capture side runs on its own clock; the timer keeps running while the unit is stopped and throws samples away, so queued transfers still complete; it is armed at realize. A TX unit reset reads back as done ("reset SAC fail" panic), and the DMA FIFO is emptied on reset and on the enabling edge (fifoCheckEmpty). - CS42L77: transfers past the end of the register map read as zero and writes are dropped, instead of overrunning regs[]. Ported from MakrSas/Inferno commits 102ac5d7f6, a06b785e2d, 6fbf9c0c65, 83985661f8, 58f0040d9d, 683bc40050 and c01889c490 by Makr , under the same licences as the files they touch. The SIO stop fix (2a79c1923b) and the AOP reply-length fix (181c83d1d5), which MakrSas found through audio but which affect every machine, are in a separate PR. MakrSas's INFERNO_AUDIO_DMA_TRACE debug dump is not ported. Co-Authored-By: Claude Opus 5.5 --- hw/arm/boot.c | 74 ++++++++++------ hw/arm/t8030.c | 109 +++++++++++++++++++++++- hw/audio/aop-audio.c | 195 +++++++++++++++++++++++++++++++++++-------- hw/audio/cs42l77.c | 25 +++++- hw/audio/mca.c | 74 +++++++++++++--- 5 files changed, 401 insertions(+), 76 deletions(-) diff --git a/hw/arm/boot.c b/hw/arm/boot.c index 509a737af90..c011fe508d4 100644 --- a/hw/arm/boot.c +++ b/hw/arm/boot.c @@ -42,30 +42,9 @@ static const char* KEEP_COMP[] = { "adbe0,s8000\0$", - // "aop-audio\0$", - // "aop-audio-control\0$", - // "aop-audio-speaker\0$", - // "audio-aop-lp-mic-in\0$", - // "audio-aop-mca2\0$", - // "audio-aop-pcmaudiomgr\0$", - // "alc,t8030\0$", - // "audio-data,aop-audio-haptic\0$", - // "audio-data,halogen\0$", - // "audio-data,hawking\0$", // "audio-data,baseband-voice\0$", - // "audio-data,dsp-debug1\0$", - // "audio-data,mikeybus-secondary\0$", // "audio-data,aop-audio-hpdb\0$", - // "halle-sensor,aop-ad5860-config\0$", - // "audio-aop-hall\0$", // "audio-aop-hpdbg\0$", - // "audio-aop-haptic\0$", - // "audio-aop-haptic-leap\0$", - // "audio-aop-hapticmgr\0$", - // "aop-audio-hpdbg\0$", - // "audio-data,aop-audio-hpdbg\0$", - // "haptics-support,leap\0$", - // "audio-data,audio-loopback\0$", "aes,s8000\0$", "aic,1\0$", "apcie-bridge\0$", @@ -166,9 +145,14 @@ static const char* KEEP_COMP[] = { "i2c,t8101\0i2c,s5l8940x\0iic,soft\0$", }; -// Kept only with audio=on: the speaker path through the MCA (I2S) clusters, -// the CS42L77 codec on SPI3 and the CS35L27 amp on I2C2. +// Kept only with audio=on (AppleBootInfo.audio). Each of these nodes pulls in a +// driver that waits on its neighbours, so the tree is only useful complete; without +// audio=on none of it reaches the guest and none of those drivers load. +// The audio part of this list follows MakrSas/Inferno (branch ios), which got +// playback and recording working end to end with it. static const char* AUDIO_COMP[] = { + // The speaker path: the MCA (I2S) switch and clusters, the CS42L77 codec on + // SPI3, the CS35L27 amp on I2C2 and the audio resource manager. "mca-switch,t8030\0$", "mcaCluster,t8030\0$", "mca,t8030\0$", @@ -177,6 +161,46 @@ static const char* AUDIO_COMP[] = { "audio-control,cs42l77\0$", "audio-data,cs42l77\0$", "audio,embedded-resource-manager\0$", + // The AOP audio service (hw/audio/aop-audio.c) and its devices. The amp waits + // for aop-audio-speaker as its power provider, and the resource manager needs + // the PCM asset manager before it publishes anything to userspace. + "aop-audio\0$", + "aop-audio-speaker\0$", + "aop-audio-control\0$", + "audio-aop-pcmaudiomgr\0$", + // The AOP's own MCA data devices: the model advertises 'mca0' and 'mca1'. + "audio-aop-mca2\0$", + // The always-listening microphone ('lpai'). The CS42L77's function-codec_lpmic + // names it; without it the AOP audio controller cannot hand out buffers. + "audio-aop-lp-mic-in\0$", + // The CS42L77's function-halogen_master, -hawk_master, -dsp-debug1_master and + // -msp_master name these data nodes; without them AppleCS42L77Audio::start + // waits for providers that never appear. + "audio-data,halogen\0$", + "audio-data,hawking\0$", + "audio-data,dsp-debug1\0$", + "audio-data,mikeybus-secondary\0$", + "audio-data,audio-loopback\0$", + // alc0..3: the I2S links the AOP drives itself ('aph ', 'aphd'), with their + // headphone-debug children. + "alc,t8030\0$", + "aop-audio-hpdbg\0$", + "audio-data,aop-audio-hpdbg\0$", + // The haptics chain. The audio server counts the actuator as a transducer; + // without it the built-in route comes up empty (AVAudioSession 'nort'). + "aop-audio-haptic\0$", + "audio-aop-hapticmgr\0$", + "audio-data,aop-audio-haptic\0$", + "audio-aop-haptic-leap\0$", + "haptics-support,leap\0$", + "audio-aop-hall\0$", + "halle-sensor,aop-ad5860-config\0$", +}; + +// Names in REM_NAMES that audio=on keeps: `aop-mca` is the AOP's own MCA, which +// the AOP audio devices describe their links through. +static const char* AUDIO_KEEP_NAMES[] = { + "aop-mca\0$", }; static const char* REM_NAMES[] = { @@ -328,7 +352,9 @@ static void apple_boot_process_dt_node(AppleDTNode* node, AppleDTNode* parent, b && !(keep_gpu && apple_boot_dt_match(prop, GPU_KEEP_NAMES, ARRAY_SIZE(GPU_KEEP_NAMES)))) { assert_nonnull(prop->data); - for (i = 0; i < ARRAY_SIZE(REM_NAMES); i++) { + bool keep_name = audio && apple_boot_dt_match(prop, AUDIO_KEEP_NAMES, ARRAY_SIZE(AUDIO_KEEP_NAMES)); + + for (i = 0; !keep_name && i < ARRAY_SIZE(REM_NAMES); i++) { uint64_t size = MIN(prop->len, sstrlen(REM_NAMES[i])); if (memcmp(prop->data, REM_NAMES[i], size) == 0) { assert_nonnull(parent); diff --git a/hw/arm/t8030.c b/hw/arm/t8030.c index 8c5ec5a5c30..c060ce6a6dd 100644 --- a/hw/arm/t8030.c +++ b/hw/arm/t8030.c @@ -2974,10 +2974,14 @@ static void t8030_create_aop(AppleT8030MachineState* t8030) assert_nonnull(dma_mr); object_property_add_const_link(OBJECT(aop), "dma-mr", OBJECT(dma_mr)); - // sbd = apple_aop_audio_create(APPLE_AOP(aop)); - // assert_nonnull(sbd); - // object_property_add_child(OBJECT(aop), "aop-audio", OBJECT(sbd)); - // sysbus_realize_and_unref(sbd, &error_fatal); + // The AOP audio service goes with the audio device-tree nodes (audio=on): + // a machine without sound gets neither, and the drivers behind them never load. + if (t8030->audio) { + sbd = apple_aop_audio_create(APPLE_AOP(aop)); + assert_nonnull(sbd); + object_property_add_child(OBJECT(aop), "aop-audio", OBJECT(sbd)); + sysbus_realize_and_unref(sbd, &error_fatal); + } sysbus_realize_and_unref(aop, &error_fatal); } @@ -3052,6 +3056,102 @@ static void t8030_create_mca(AppleT8030MachineState* t8030) sysbus_realize_and_unref(sbd, &error_fatal); } +/* + * audio=on: every I2S port has a SIO DMA channel, but only the speaker's + * (0x7A/0x7B, read by the MCA model) has anything consuming it. The audio + * server starts every member of an aggregate at once (codec, hawking, + * loopback, the actuator), and a transfer queued on a channel nobody reads + * never completes: its IO thread sleeps uninterruptibly in the kernel, the + * server misses its RPCs and the watchdog kills it, taking every sound in the + * system with it. So the other ports are drained here at about the rate a + * real one consumes: playback is taken and dropped, capture gets silence. + * From MakrSas/Inferno (branch ios, "audio: give the guest an output route"). + */ +#define AUDIO_DRAIN_INTERVAL_NS (5 * SCALE_MS) +#define AUDIO_DRAIN_BYTES (48000 * 2 * 2 / 200) +#define AUDIO_DRAIN_MAX_PORTS (16) +#define AUDIO_DRAIN_SKIP_TX (0x7A) +#define AUDIO_DRAIN_SKIP_RX (0x7B) + +static const char* AUDIO_I2S_NODES[] = { + "arm-io/mca0/mca0a", "arm-io/mca0/mca0b", "arm-io/mca1/mca1a", "arm-io/mca1/mca1b", + "arm-io/mca2/mca2a", "arm-io/mca2/mca2b", "arm-io/mca3/mca3a", "arm-io/mca3/mca3b", + "arm-io/mca4/mca4a", "arm-io/mca4/mca4b", "arm-io/mca5/mca5a", "arm-io/mca5/mca5b", + "arm-io/alc0", "arm-io/alc1", "arm-io/alc2", "arm-io/alc3", +}; + +typedef struct +{ + AppleSIODMAEndpoint* tx[AUDIO_DRAIN_MAX_PORTS]; + AppleSIODMAEndpoint* rx[AUDIO_DRAIN_MAX_PORTS]; + size_t count; + QEMUTimer* timer; +} T8030AudioDrain; + +static void t8030_audio_drain(void* opaque) +{ + T8030AudioDrain* drain = opaque; + uint8_t buffer[AUDIO_DRAIN_BYTES]; + size_t i; + + for (i = 0; i < drain->count; i++) { + if (drain->tx[i] != NULL) { apple_sio_dma_read(drain->tx[i], buffer, sizeof(buffer)); } + if (drain->rx[i] != NULL) { apple_sio_dma_blit(drain->rx[i], 0, sizeof(buffer)); } + } + + timer_mod_ns(drain->timer, qemu_clock_get_ns(QEMU_CLOCK_VIRTUAL) + AUDIO_DRAIN_INTERVAL_NS); +} + +static void t8030_create_audio_drain(AppleT8030MachineState* t8030) +{ + AppleSIO* sio; + T8030AudioDrain* drain; + AppleSIODMAEndpoint* skip_tx; + AppleSIODMAEndpoint* skip_rx; + size_t i; + + sio = APPLE_SIO(object_property_get_link(OBJECT(t8030), "sio", &error_fatal)); + skip_tx = apple_sio_get_endpoint(sio, AUDIO_DRAIN_SKIP_TX); + skip_rx = apple_sio_get_endpoint(sio, AUDIO_DRAIN_SKIP_RX); + drain = g_new0(T8030AudioDrain, 1); + + for (i = 0; i < ARRAY_SIZE(AUDIO_I2S_NODES) && drain->count < AUDIO_DRAIN_MAX_PORTS; i++) { + AppleDTNode* node; + AppleDTProp* prop; + AppleSIODMAEndpoint* tx; + AppleSIODMAEndpoint* rx; + uint8_t channel; + + node = apple_dt_get_node(t8030->device_tree, AUDIO_I2S_NODES[i]); + if (node == NULL) { continue; } + + // The channel the node names, and the next one for the other direction, + // as t8030_create_mca() does for the speaker. + prop = apple_dt_get_prop(node, "dma-channels"); + if (prop == NULL || prop->len == 0) { continue; } + + channel = ((const uint8_t*)prop->data)[0]; + tx = apple_sio_get_endpoint(sio, channel); + rx = apple_sio_get_endpoint(sio, channel + 1); + + if (tx == skip_tx || tx == skip_rx) { tx = NULL; } + if (rx == skip_tx || rx == skip_rx) { rx = NULL; } + if (tx == NULL && rx == NULL) { continue; } + + drain->tx[drain->count] = tx; + drain->rx[drain->count] = rx; + drain->count++; + } + + if (drain->count == 0) { + g_free(drain); + return; + } + + drain->timer = timer_new_ns(QEMU_CLOCK_VIRTUAL, t8030_audio_drain, drain); + timer_mod_ns(drain->timer, qemu_clock_get_ns(QEMU_CLOCK_VIRTUAL) + AUDIO_DRAIN_INTERVAL_NS); +} + static void t8030_create_speaker_top(AppleT8030MachineState* t8030) { AppleDTNode* child; @@ -3350,6 +3450,7 @@ static void t8030_create_devices(AppleT8030MachineState* t8030) t8030_create_mca(t8030); t8030_create_speaker_top(t8030); t8030_create_speaker_bottom(t8030); + t8030_create_audio_drain(t8030); } else if (t8030->audiodev != NULL) { error_setg(&error_fatal, "audiodev= needs audio=on"); diff --git a/hw/audio/aop-audio.c b/hw/audio/aop-audio.c index 3773b98412c..2966db16f88 100644 --- a/hw/audio/aop-audio.c +++ b/hw/audio/aop-audio.c @@ -69,6 +69,8 @@ #define DEV_PROP_CHANNEL_CTRL_LEN (0x10) #define DEV_PROP_STREAM_FORMAT (0x12E) #define DEV_PROP_STREAM_FORMAT_LEN (0x10) +// AppleAOPAudioDeviceProvider::setDevicePowerState writes the state it wants here, then reads DEV_PROP_STATE. +#define DEV_PROP_REQUESTED_STATE (0xCA) struct AppleAOPAudioState { @@ -79,8 +81,39 @@ struct AppleAOPAudioState uint32_t enabled_chans; uint32_t voice_trigger_chans; uint32_t history_chans; + // Power states the guest has asked devices to enter; see apple_aop_audio_set_state(). + struct + { + uint32_t device; + uint32_t state; + } states[16]; }; +/* + * The driver asks for a power state through DEV_PROP_REQUESTED_STATE and then reads DEV_PROP_STATE until the + * device reports being there. Answered with a fixed state, it never was: the codec's MCLK manager ('acmm') + * repeated its request about once a minute for as long as the guest ran. The real AOP moves the device; here + * the device simply arrives. A state of zero means none was ever requested. + */ +static uint32_t apple_aop_audio_get_state(AppleAOPAudioState* s, uint32_t device) +{ + for (size_t i = 0; i < ARRAY_SIZE(s->states); i++) { + if (s->states[i].device == device) { return s->states[i].state; } + } + return 0; +} + +static void apple_aop_audio_set_state(AppleAOPAudioState* s, uint32_t device, uint32_t state) +{ + for (size_t i = 0; i < ARRAY_SIZE(s->states); i++) { + if (s->states[i].device == device || s->states[i].device == 0) { + s->states[i].device = device; + s->states[i].state = state; + return; + } + } +} + static void apple_aop_audio_class_init(ObjectClass* klass, const void* data) { DeviceClass* dc; @@ -108,6 +141,9 @@ OBJECT_DEFINE_SIMPLE_TYPE_CLASS_INIT(AppleAOPAudioState, apple_aop_audio, APPLE_ // - mcaN | Multi-Channel Audio (cluster N) // - apac | AOP Audio Controller? static const uint32_t apple_aop_devices[] = { + // 'lpai' goes with its device tree node, and that node with the CS42L77 its function-codec_lpmic + // names. Without the microphone at all the controller has neither an input handler nor a mic + // device, cannot hand out buffers, and restarts for as long as the guest runs. 'edtC', 'acmm', 'aphc', 'lpfw', 'leap', 'aphd', 'aph ', 'ahdc', 'pcmM', 'lpai', 'mca0', 'mca1', 'apac', }; @@ -120,8 +156,11 @@ static AppleAOPResult apple_aop_audio_get_prop(void* opaque, uint32_t prop, void stl_le_p(out, 'aop '); break; case PROPERTY_DEVICE_COUNT : stl_le_p(out, ARRAY_SIZE(apple_aop_devices)); break; - case PROPERTY_IO_HANDLER_COUNT: stl_le_p(out, 0); break; - default : break; + case PROPERTY_IO_HANDLER_COUNT: stl_le_p(out, 1); break; + case PROPERTY_VERSION : stl_le_p(out, 1); break; + default: + AOP_DPRINTF("AOPAudio GetProperty 0x%X — UNHANDLED, answering with nothing", prop); + break; } return AOP_RESULT_OK; @@ -131,6 +170,12 @@ static AppleAOPResult apple_aop_audio_handle_command(void* opaque, uint16_t seq, void* payload_out, uint32_t out_len) { AppleAOPAudioState* s = opaque; + /* + * Answers are built here and copied out only as far as the guest's buffer goes. Its size is the + * guest's to choose, and a property written in full into a smaller one would run past the end of + * the heap buffer the endpoint allocated for it. + */ + uint8_t out[64] = { 0 }; if (payload == NULL || len < COMMAND_HDR_LEN || ldl_le_p(payload) != 0xFFFFFFFF) { return AOP_RESULT_ERROR; } @@ -138,58 +183,61 @@ static AppleAOPResult apple_aop_audio_handle_command(void* opaque, uint16_t seq, case COMMAND_GET_DEVICE_ID: AOP_DPRINTF("AOPAudio GetDeviceID %d", ldl_le_p(payload + COMMAND_HDR_LEN)); - stl_le_p(payload_out, apple_aop_devices[ldl_le_p(payload + COMMAND_HDR_LEN)]); + // The index is the guest's: one past the list reads as no device. + if (len >= COMMAND_HDR_LEN + 4 && ldl_le_p(payload + COMMAND_HDR_LEN) < ARRAY_SIZE(apple_aop_devices)) { + stl_le_p(out, apple_aop_devices[ldl_le_p(payload + COMMAND_HDR_LEN)]); + } break; case COMMAND_GET_DEVICE_PROP: - AOP_DPRINTF("AOPAudio GetDeviceProperty %X 0x%X", ldl_le_p(payload + COMMAND_HDR_LEN), + AOP_DPRINTF("AOPAudio GetDeviceProperty '%.4s' 0x%X", (const char*)payload + COMMAND_HDR_LEN, ldl_le_p(payload + COMMAND_HDR_LEN + 4)); switch (ldl_le_p(payload + COMMAND_HDR_LEN)) { case 'lpai': switch (ldl_le_p(payload + COMMAND_HDR_LEN + 4)) { case DEV_PROP_STATE: - stl_le_p(payload_out, DEV_PROP_STATE_LEN); - stl_le_p(payload_out + 4, 'idle'); + stl_le_p(out, DEV_PROP_STATE_LEN); + stl_le_p(out + 4, 'idle'); break; case DEV_PROP_CHANNEL_CTRL: - stl_le_p(payload_out, DEV_PROP_CHANNEL_CTRL_LEN); - stl_le_p(payload_out + 4, s->supported_chans); - stl_le_p(payload_out + 8, s->enabled_chans); - stl_le_p(payload_out + 12, s->voice_trigger_chans); - stl_le_p(payload_out + 16, s->history_chans); + stl_le_p(out, DEV_PROP_CHANNEL_CTRL_LEN); + stl_le_p(out + 4, s->supported_chans); + stl_le_p(out + 8, s->enabled_chans); + stl_le_p(out + 12, s->voice_trigger_chans); + stl_le_p(out + 16, s->history_chans); break; case DEV_PROP_STREAM_FORMAT: - stl_le_p(payload_out, DEV_PROP_STREAM_FORMAT_LEN); - stl_le_p(payload_out + 4, 'pcm '); - stl_le_p(payload_out + 8, 48000); - stl_le_p(payload_out + 12, 2); - stl_le_p(payload_out + 16, 2); + stl_le_p(out, DEV_PROP_STREAM_FORMAT_LEN); + stl_le_p(out + 4, 'pcm '); + stl_le_p(out + 8, 48000); + stl_le_p(out + 12, 2); + stl_le_p(out + 16, 2); break; case DEV_PROP_SUPPORTS_HISTORICAL_DATA: - stl_le_p(payload_out, DEV_PROP_SUPPORTS_HISTORICAL_DATA_LEN); - stl_le_p(payload_out + 4, 0); + stl_le_p(out, DEV_PROP_SUPPORTS_HISTORICAL_DATA_LEN); + stl_le_p(out + 4, 0); break; } break; case 'lai ': switch (ldl_le_p(payload + COMMAND_HDR_LEN + 4)) { case DEV_PROP_STATE: - stl_le_p(payload_out, DEV_PROP_STATE_LEN); - stl_le_p(payload_out + 4, 'idle'); + stl_le_p(out, DEV_PROP_STATE_LEN); + stl_le_p(out + 4, 'idle'); break; case DEV_PROP_SUPPORTS_HISTORICAL_DATA: - stl_le_p(payload_out, DEV_PROP_SUPPORTS_HISTORICAL_DATA_LEN); - stl_le_p(payload_out + 4, 0); + stl_le_p(out, DEV_PROP_SUPPORTS_HISTORICAL_DATA_LEN); + stl_le_p(out + 4, 0); break; } break; case 'mca0': case 'mca1': switch (ldl_le_p(payload + COMMAND_HDR_LEN + 4)) { - case DEV_PROP_MCA_RX_STATUS: stl_le_p(payload_out, DEV_PROP_MCA_RX_STATUS_LEN); break; - case DEV_PROP_MCA_TX_STATUS: stl_le_p(payload_out, DEV_PROP_MCA_TX_STATUS_LEN); break; + case DEV_PROP_MCA_RX_STATUS: stl_le_p(out, DEV_PROP_MCA_RX_STATUS_LEN); break; + case DEV_PROP_MCA_TX_STATUS: stl_le_p(out, DEV_PROP_MCA_TX_STATUS_LEN); break; case DEV_PROP_MCA_RX0_SHIM_OVERRUN: - stl_le_p(payload_out, DEV_PROP_MCA_RX0_SHIM_OVERRUN_LEN); + stl_le_p(out, DEV_PROP_MCA_RX0_SHIM_OVERRUN_LEN); break; } break; @@ -197,8 +245,8 @@ static AppleAOPResult apple_aop_audio_handle_command(void* opaque, uint16_t seq, case 'apac': switch (ldl_le_p(payload + COMMAND_HDR_LEN + 4)) { case DEV_PROP_STATE: - stl_le_p(payload_out, DEV_PROP_STATE_LEN); - stl_le_p(payload_out + 4, 'pwrd'); + stl_le_p(out, DEV_PROP_STATE_LEN); + stl_le_p(out + 4, 'pwrd'); break; } break; @@ -206,29 +254,65 @@ static AppleAOPResult apple_aop_audio_handle_command(void* opaque, uint16_t seq, case 'aph ': switch (ldl_le_p(payload + COMMAND_HDR_LEN + 4)) { case DEV_PROP_STATE: - stl_le_p(payload_out, DEV_PROP_STATE_LEN); - stl_le_p(payload_out + 4, 'pw1 '); + stl_le_p(out, DEV_PROP_STATE_LEN); + stl_le_p(out + 4, 'pw1 '); break; } break; case DEV_PCM_MGR: switch (ldl_le_p(payload + COMMAND_HDR_LEN + 4)) { case DEV_PROP_PCM_NUM_SUPPORTED_ASSETS: - stl_le_p(payload_out, DEV_PROP_PCM_NUM_SUPPORTED_ASSETS_LEN); - stl_le_p(payload_out + 4, 2); + stl_le_p(out, DEV_PROP_PCM_NUM_SUPPORTED_ASSETS_LEN); + stl_le_p(out + 4, 2); break; } break; case DEV_LEAP_FW: switch (ldl_le_p(payload + COMMAND_HDR_LEN + 4)) { case DEV_PROP_LEAP_FW_BUFFER_BYTES_MAX: - stl_le_p(payload_out, DEV_PROP_LEAP_FW_BUFFER_BYTES_MAX_LEN); - stq_le_p(payload_out + 4, 128 * KiB); + stl_le_p(out, DEV_PROP_LEAP_FW_BUFFER_BYTES_MAX_LEN); + stq_le_p(out + 4, 128 * KiB); + break; + } + break; + default: + // The IO handlers are asked the same question, and an + // answer of nothing reads as a buffer of no bytes — which + // is how the controller ends up with no resources at all. + switch (ldl_le_p(payload + COMMAND_HDR_LEN + 4)) { + case DEV_PROP_LEAP_FW_BUFFER_BYTES_MAX: + stl_le_p(out, DEV_PROP_LEAP_FW_BUFFER_BYTES_MAX_LEN); + stq_le_p(out + 4, 64 * KiB); break; } break; } break; + case COMMAND_ATTACH_DEVICE: + case COMMAND_DETACH_DEVICE: + // The reply is four bytes wide; zero reads as success everywhere + // else in this protocol. + AOP_DPRINTF("AOPAudio %s '%.4s'", ldl_le_p(payload + sizeof(uint32_t)) == COMMAND_ATTACH_DEVICE + ? "Attach" : "Detach", (const char*)payload + COMMAND_HDR_LEN); + stl_le_p(out, 0); + break; + case COMMAND_REGISTER_ACCESS: { + /* + * A register read or write on a device behind the AOP: after the header come the target's + * id, four bytes of which one says write, then the address and the width, both big-endian, + * and on a write the value as the last byte of the packet. + * + * The answer stays all zeros on purpose. Handing back what the guest had written lets its + * driver walk further into a setup this model cannot finish, and the audio server then + * hangs with no devices at all; zeros leave it with a working route. + */ + const uint8_t* body = (const uint8_t*)payload + COMMAND_HDR_LEN; + + AOP_DPRINTF("AOPAudio RegisterAccess '%.4s' %s 0x%X (%u bytes)%s", (const char*)body, + body[6] ? "write" : "read", ldl_be_p(body + 8), ldl_be_p(body + 12), + body[6] ? "" : ", answering zeros"); + break; + } case COMMAND_SET_DEVICE_PROP: AOP_DPRINTF("AOPAudio SetDeviceProperty %X 0x%X", ldl_le_p(payload + COMMAND_HDR_LEN), ldl_le_p(payload + COMMAND_HDR_LEN + 4)); @@ -246,9 +330,52 @@ static AppleAOPResult apple_aop_audio_handle_command(void* opaque, uint16_t seq, break; } break; - default: break; + default: { + g_autoptr(GString) hex = g_string_new(NULL); + uint32_t i; + + for (i = 0; i < len; i++) { g_string_append_printf(hex, "%02x", ((const uint8_t*)payload)[i]); } + AOP_DPRINTF("AOPAudio command 0x%X — UNHANDLED, len %u out %u, payload %s", + ldl_le_p(payload + sizeof(uint32_t)), len, out_len, hex->str); + break; + } + } + + // Power states, whatever the device: see apple_aop_audio_set_state(). + if (len >= COMMAND_HDR_LEN + 8) { + uint32_t command = ldl_le_p(payload + sizeof(uint32_t)); + uint32_t device = ldl_le_p(payload + COMMAND_HDR_LEN); + uint32_t property = ldl_le_p(payload + COMMAND_HDR_LEN + 4); + uint32_t state; + + if (command == COMMAND_SET_DEVICE_PROP && property == DEV_PROP_REQUESTED_STATE) { + g_autoptr(GString) hex = g_string_new(NULL); + const uint8_t* candidate = (const uint8_t*)payload + COMMAND_HDR_LEN + 32; + bool fourcc = len >= COMMAND_HDR_LEN + 36; + + // After the value's length comes a 48-byte request: the device's id, a request number, a + // timestamp, two zero words, and then the state wanted ('idle', for 'acmm'). All of it goes to the + // log; the state is taken only when it reads as a four-character code. + for (uint32_t i = COMMAND_HDR_LEN + 8; i < len; i++) { + g_string_append_printf(hex, "%02x", ((const uint8_t*)payload)[i]); + } + AOP_DPRINTF("AOPAudio '%.4s' requests a state: %s", (const char*)payload + COMMAND_HDR_LEN, hex->str); + + for (int i = 0; fourcc && i < 4; i++) { fourcc = candidate[i] >= 0x20 && candidate[i] < 0x7F; } + if (fourcc) { apple_aop_audio_set_state(s, device, ldl_le_p(candidate)); } + } + else if (command == COMMAND_GET_DEVICE_PROP && property == DEV_PROP_STATE) { + AOP_DPRINTF("AOPAudio '%.4s' state asked for, %u bytes of room", (const char*)payload + COMMAND_HDR_LEN, + out_len); + if ((state = apple_aop_audio_get_state(s, device)) != 0) { + memset(out, 0, sizeof(out)); + stl_le_p(out, DEV_PROP_STATE_LEN); + stl_le_p(out + 4, state); + } + } } + if (payload_out != NULL) { memcpy(payload_out, out, MIN(out_len, sizeof(out))); } return AOP_RESULT_OK; } diff --git a/hw/audio/cs42l77.c b/hw/audio/cs42l77.c index 3bad76848da..c7fa36c689f 100644 --- a/hw/audio/cs42l77.c +++ b/hw/audio/cs42l77.c @@ -22,6 +22,7 @@ #include "hw/qdev-core.h" #include "hw/ssi/ssi.h" #include "qemu/bswap.h" +#include "qemu/error-report.h" #include "qemu/lockable.h" #include "trace.h" @@ -60,11 +61,27 @@ struct AppleCS42L77State uint16_t length; uint8_t cur_cmd; bool data_ready; + bool warned_range; uint8_t regs[CS42L77_REG_SIZE]; }; static void apple_cs42l77_realize(SSIPeripheral* dev, Error** errp) { qemu_mutex_init(&APPLE_CS42L77(dev)->mutex); } +/* + * The guest sets a 24-bit start address and reads or writes on from there, so a transfer that starts + * near the top of the map runs past the end of the register array. Past the end reads as zero and + * writes are dropped; the first such access is reported, with where it happened. + */ +static bool apple_cs42l77_in_range(AppleCS42L77State* s) +{ + if (s->address < CS42L77_REG_SIZE) { return true; } + if (!s->warned_range) { + s->warned_range = true; + warn_report("CS42L77: register access past the end of the map, at 0x%" PRIx64, s->address); + } + return false; +} + static uint32_t apple_cs42l77_transfer(SSIPeripheral* dev, uint32_t val) { AppleCS42L77State* s; @@ -125,7 +142,7 @@ static uint32_t apple_cs42l77_transfer(SSIPeripheral* dev, uint32_t val) DPRINTF("%s: val=0x%X -> 0x%X, address=0x%llX\n", __func__, val, ret, s->address); } else { - ret = s->regs[s->address]; + ret = apple_cs42l77_in_range(s) ? s->regs[s->address] : 0; DPRINTF("%s: address=0x%llX -> 0x%X\n", __func__, s->address, ret); s->address += 1; } @@ -148,7 +165,7 @@ static uint32_t apple_cs42l77_transfer(SSIPeripheral* dev, uint32_t val) DPRINTF("%s: val=0x%X -> 0x%X, address=0x%llX\n", __func__, val, ret, s->address); } else { - ret = s->regs[s->address]; + ret = apple_cs42l77_in_range(s) ? s->regs[s->address] : 0; DPRINTF("%s: address=0x%llX -> 0x%X\n", __func__, s->address, ret); s->address += 1; } @@ -164,7 +181,7 @@ static uint32_t apple_cs42l77_transfer(SSIPeripheral* dev, uint32_t val) } else { DPRINTF("%s: address=0x%llX <- 0x%X\n", __func__, s->address, val); - s->regs[s->address] = (uint8_t)val; + if (apple_cs42l77_in_range(s)) { s->regs[s->address] = (uint8_t)val; } s->address += 1; } ret = STS_OK; @@ -180,7 +197,7 @@ static uint32_t apple_cs42l77_transfer(SSIPeripheral* dev, uint32_t val) } else { DPRINTF("%s: address=0x%llX <- 0x%X\n", __func__, s->address, val); - s->regs[s->address] = (uint8_t)val; + if (apple_cs42l77_in_range(s)) { s->regs[s->address] = (uint8_t)val; } s->address += 1; } ret = STS_OK; diff --git a/hw/audio/mca.c b/hw/audio/mca.c index 67e431833db..38724022b87 100644 --- a/hw/audio/mca.c +++ b/hw/audio/mca.c @@ -167,6 +167,7 @@ struct AppleMCAState SWVoiceOut* voice; QEMUTimer* timer; int64_t last_ns; + int64_t rx_last_ns; bool running; uint8_t ring[MCA_RING_SIZE]; uint32_t ring_head; @@ -237,6 +238,27 @@ static void apple_mca_serialise(void* opaque) now = qemu_clock_get_ns(QEMU_CLOCK_VIRTUAL); + /* + * The capture side runs on the port's own clock rather than on how much playback the guest had + * queued. Tying it to the playback side means a stream that only records — the speaker's current + * and voltage feedback while a ringtone plays, a recording app — is never fed at all, and the + * thread waiting on that transfer never comes back. + */ + { + uint64_t rx_frames; + + rx_frames = muldiv64(now - s->rx_last_ns, MCA_FRAME_RATE, NANOSECONDS_PER_SECOND); + rx_frames = MIN(rx_frames, (uint64_t)MCA_QUANTUM_BYTES * MCA_MAX_CATCHUP / MCA_FRAME_BYTES); + + if (rx_frames != 0) { + // TX 2ch, RX 4ch. FIXME + apple_sio_dma_blit(s->rx_ep, 0, rx_frames * (MCA_FRAME_BYTES * 2)); + s->rx_last_ns += muldiv64(rx_frames, NANOSECONDS_PER_SECOND, MCA_FRAME_RATE); + } + + if (now - s->rx_last_ns > MCA_MAX_DEBT_NS) { s->rx_last_ns = now; } + } + if (now <= s->last_ns) { timer_mod_ns(s->timer, now + MCA_QUANTUM_NS); return; @@ -250,8 +272,13 @@ static void apple_mca_serialise(void* opaque) pulled = apple_mca_dma_into_ring(s, due); frames = pulled / MCA_FRAME_BYTES; - // TX 2ch, RX 4ch. FIXME - apple_sio_dma_blit(s->rx_ep, 0, frames * (MCA_FRAME_BYTES * 2)); + // Stopped, the samples go nowhere — the voice is inactive — but they still have to be taken, + // and what was taken is thrown away rather than played on the next start. See + // apple_mca_set_running(). + if (!s->running) { + s->ring_head = 0; + s->ring_used = 0; + } if (now - s->last_ns > MCA_MAX_DEBT_NS) { s->last_ns = now; } @@ -271,13 +298,15 @@ static void apple_mca_set_running(AppleMCAState* s, bool running) AUD_set_active_out(s->voice, running); AUD_set_volume_out(s->voice, !running, 255, 255); - if (running) { - s->last_ns = qemu_clock_get_ns(QEMU_CLOCK_VIRTUAL); - timer_mod_ns(s->timer, s->last_ns + MCA_QUANTUM_NS); - } - else { - timer_del(s->timer); - } + /* + * The timer is armed either way. A transfer the guest queued still has to be consumed once it has + * stopped the unit, or the thread that queued it waits for a completion that never comes — an + * uninterruptible wait, which on a pause takes the audio server down with it and every sound in + * the system until the machine is rebooted. + */ + s->last_ns = qemu_clock_get_ns(QEMU_CLOCK_VIRTUAL); + s->rx_last_ns = s->last_ns; + timer_mod_ns(s->timer, s->last_ns + MCA_QUANTUM_NS); } static void apple_mca_sio_reg_write(void* opaque, hwaddr addr, uint64_t data, unsigned size) @@ -296,7 +325,26 @@ static void apple_mca_sio_reg_write(void* opaque, hwaddr addr, uint64_t data, un case TXB_REG_BASE + REG_TX_CFG: s->sio_clusters[index].txb_config = data; break; case TXB_REG_BASE + REG_SIO_UNIT_CTL: if (index == 5) { apple_mca_set_running(s, (data & SIO_UNIT_CTL_ENABLE) != 0); } - s->sio_clusters[index].txb_control = data; + /* + * Before a start the driver fills the unit's DMA FIFO with zeros until it reads full, and it + * checks the FIFO is empty before a start and after a stop. On the hardware the serialiser + * takes the zeros as soon as the unit runs, and a reset flushes whatever is left. Nothing here + * ever took them: the FIFO stayed full after the first stream, fifoCheckEmpty failed, and every + * start after that — the speaker and the actuator's aggregate with it — was refused. + * + * A cluster has two DMA blocks, A then B, so the B side of cluster N is DMA block 2N + 1: the + * speaker, mca5b, fills block 11 (its node's reg says so), not block 5. + */ + if (index * 2 + 1 < s->dma_cluster_count + && ((data & SIO_UNIT_CTL_RESET) + || ((data & SIO_UNIT_CTL_ENABLE) && !(s->sio_clusters[index].txb_control & SIO_UNIT_CTL_ENABLE)))) + { + fifo32_reset(&s->dma_clusters[index * 2 + 1].fifo); + } + // A reset leaves the unit's control at zero once it is done, and the driver polls for exactly + // that, a millisecond at a time: kept as written, it reads as a reset that never finished, and + // AppleMCA2Switch_TxCtl::reset panics with "reset SAC fail". + s->sio_clusters[index].txb_control = (data & SIO_UNIT_CTL_RESET) ? 0 : data; break; } } @@ -513,5 +561,11 @@ static void apple_mca_realize(DeviceState* dev, Error** errp) if (s->voice == NULL) { error_setg(errp, "Failed to open the Multi-Channel Audio output voice"); AUD_remove_card(&s->card); + return; } + + // Armed from the start: the guest may queue a transfer before it ever enables the unit. + s->last_ns = qemu_clock_get_ns(QEMU_CLOCK_VIRTUAL); + s->rx_last_ns = s->last_ns; + timer_mod_ns(s->timer, s->last_ns + MCA_QUANTUM_NS); }