diff --git a/hw/arm/boot.c b/hw/arm/boot.c index 8bbd119cc5..c011fe508d 100644 --- a/hw/arm/boot.c +++ b/hw/arm/boot.c @@ -42,35 +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,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 +82,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 +145,64 @@ static const char* KEEP_COMP[] = { "i2c,t8101\0i2c,s5l8940x\0iic,soft\0$", }; +// 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$", + "audio-control,cs35l27\0$", + "audio-data,cs35l27\0$", + "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[] = { "accel\0$", "gyro\0$", @@ -292,7 +321,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 +331,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; @@ -322,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); @@ -359,7 +391,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 +649,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 d74bf7ce42..c060ce6a6d 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) { @@ -2973,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); } @@ -3045,10 +3050,108 @@ 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); } +/* + * 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; @@ -3343,9 +3446,16 @@ 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); + t8030_create_audio_drain(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 +3531,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 +3844,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 +3927,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/aop-audio.c b/hw/audio/aop-audio.c index 3773b98412..2966db16f8 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/cs35l27.c b/hw/audio/cs35l27.c index 6b79b886e6..3d50477052 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 ca002bbfad..c7fa36c689 100644 --- a/hw/audio/cs42l77.c +++ b/hw/audio/cs42l77.c @@ -22,7 +22,9 @@ #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" #if 0 #define DPRINTF(v, ...) fprintf(stderr, v, ##__VA_ARGS__) @@ -59,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; @@ -124,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; } @@ -147,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; } @@ -163,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; @@ -179,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; @@ -227,6 +245,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 0c200f75ef..38724022b8 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) @@ -171,12 +167,19 @@ 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; 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 +188,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 +198,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 +218,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; } @@ -231,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; @@ -244,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; } @@ -260,17 +293,20 @@ 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); - 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) @@ -282,14 +318,33 @@ 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; 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; } } @@ -311,7 +366,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 +390,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 +424,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 +447,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 +469,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 +542,30 @@ 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); + return; } - return sbd; + // 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); } diff --git a/hw/audio/trace-events b/hw/audio/trace-events index e69de29bb2..3a2af1c178 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 1e422b2bea..43c55db56e 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 4aeb4c7ed9..0b57a2915e 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;