From fff0df8d863cd8749e39f56c19eacea468da4824 Mon Sep 17 00:00:00 2001 From: Stefan Maier <145186748+Cesarus85@users.noreply.github.com> Date: Mon, 28 Sep 2026 20:43:16 +0200 Subject: [PATCH] Android: fix audio, which never played; VR: wider look-at tests - SDL 3.4 holds an audio stream's lock through its callback, and the game's data was put into the stream from the thread that runs the game's callback, which then waited for that lock for ever: the first callback never returned and nothing was heard. The data is now gathered and put in by SDL's own thread once the game is done. - debug.audio_log logs the level of the mixed sound. - In VR a player's unit sees what its head points at within 15 degrees, or what the weapon's laser points at, since the head only points roughly where the eyes look (the training's flashing lights ask for 5 degrees). - vr.log_performance also logs the unit's looking vector. Co-Authored-By: Claude Opus 5.5 --- port/android/host/host_sdl.c | 64 ++++++++++++++++++++++++++++++++++-- port/linux/include/halo_vr.h | 4 +++ port/linux/src/dsound_sdl.c | 32 ++++++++++++++++++ port/linux/src/port_config.c | 2 ++ port/linux/src/vr.c | 19 +++++++++++ source/game/players.c | 9 +++++ source/units/units.c | 20 +++++++++++ 7 files changed, 148 insertions(+), 2 deletions(-) diff --git a/port/android/host/host_sdl.c b/port/android/host/host_sdl.c index ea9ddbbc..a7a8654f 100644 --- a/port/android/host/host_sdl.c +++ b/port/android/host/host_sdl.c @@ -239,10 +239,20 @@ int host_sdl_rumble_gamepad(uint32_t gamepad, uint32_t low, uint32_t high, uint3 /* SDL calls audio_callback on its own audio thread, which cannot run guest code; it passes each request to the stream's thread (audio_thread), which -can, and waits for it to be done */ +can, and waits for it to be done. SDL holds the stream's lock through the +callback, so the data the game puts into the stream meanwhile, from the +other thread, is gathered here and put in by SDL's thread once the game is +done (SDL_PutAudioStreamData from audio_thread would wait for that lock +for ever, and the game would never be heard). */ struct audio_binding { uint32_t handle; + SDL_AudioStream *stream; + pthread_t thread; + int thread_known; + unsigned char *gathered; + size_t gathered_size; + size_t gathered_capacity; uint32_t callback; uint32_t userdata; pthread_mutex_t lock; @@ -258,6 +268,8 @@ static void *audio_thread(void *context) struct audio_binding *binding = context; pthread_mutex_lock(&binding->lock); + binding->thread = pthread_self(); + binding->thread_known = 1; for (;;) { int additional, total; @@ -287,9 +299,18 @@ static void SDLCALL audio_callback(void *userdata, SDL_AudioStream *stream, int pthread_cond_signal(&binding->requested); while (binding->pending) pthread_cond_wait(&binding->done, &binding->lock); + if (binding->gathered_size) + { + SDL_PutAudioStreamData(stream, binding->gathered, (int)binding->gathered_size); + binding->gathered_size = 0; + } pthread_mutex_unlock(&binding->lock); } +#define AUDIO_BINDINGS 4 + +static struct audio_binding *audio_bindings[AUDIO_BINDINGS]; + uint32_t host_sdl_open_audio_stream(uint32_t device, const void *spec, uint32_t callback, uint32_t userdata) { struct audio_binding *binding = SDL_calloc(1, sizeof(*binding)); @@ -309,16 +330,55 @@ uint32_t host_sdl_open_audio_stream(uint32_t device, const void *spec, uint32_t } /* the device starts paused, so no callback can run before this */ binding->handle = handle_new(_handle_audio, stream); + binding->stream = stream; if (callback && host_native_thread_create(audio_thread, binding, 256 * 1024) != 0) host_fatal("cannot start the audio thread"); + { + int index; + + for (index = 0; index < AUDIO_BINDINGS; index++) + { + if (!audio_bindings[index]) + { + audio_bindings[index] = binding; + break; + } + } + } return binding->handle; } int host_sdl_put_audio_stream_data(uint32_t stream, const void *data, int length) { SDL_AudioStream *object = handle_get(stream, _handle_audio); + int index; - return object ? SDL_PutAudioStreamData(object, data, length) : 0; + if (!object || length < 0) + return 0; + /* from inside the stream's callback: gathered (struct audio_binding) */ + for (index = 0; index < AUDIO_BINDINGS; index++) + { + struct audio_binding *binding = audio_bindings[index]; + + if (binding && binding->stream == object && binding->thread_known && + pthread_equal(binding->thread, pthread_self())) + { + if (binding->gathered_size + (size_t)length > binding->gathered_capacity) + { + size_t capacity = (binding->gathered_size + (size_t)length) * 2; + unsigned char *grown = SDL_realloc(binding->gathered, capacity); + + if (!grown) + return 0; + binding->gathered = grown; + binding->gathered_capacity = capacity; + } + memcpy(binding->gathered + binding->gathered_size, data, (size_t)length); + binding->gathered_size += (size_t)length; + return 1; + } + } + return SDL_PutAudioStreamData(object, data, length); } int host_sdl_resume_audio_stream_device(uint32_t stream) diff --git a/port/linux/include/halo_vr.h b/port/linux/include/halo_vr.h index 24762480..678d874c 100644 --- a/port/linux/include/halo_vr.h +++ b/port/linux/include/halo_vr.h @@ -62,6 +62,10 @@ int halo_vr_aim(short local_player_index, float current_yaw, float current_pitch /* where the head looks, in the game's axes; 0 when not in VR play */ int halo_vr_look_vector(short local_player_index, float vector[3]); +/* vr.log_performance: logs the unit's looking vector against the head's +every second (players.c) */ +void halo_vr_debug_look(short local_player_index, const float unit_looking[3], int in_vehicle); + /* how far the view has turned since this was last asked, once past a few degrees on an axis (the other axis 0); nonzero if it has. The game counts it as looking around (player_control.c's action tests). */ diff --git a/port/linux/src/dsound_sdl.c b/port/linux/src/dsound_sdl.c index 98dc1226..28c91607 100644 --- a/port/linux/src/dsound_sdl.c +++ b/port/linux/src/dsound_sdl.c @@ -429,6 +429,33 @@ static void mix(float *output, unsigned long frames) static SDL_AudioStream *audio_stream; static BOOL audio_started = FALSE; +static BOOL audio_log = FALSE; + +/* debug.audio_log: the level of what is mixed, every five seconds */ +static void audio_log_level(const float *samples, unsigned long count) +{ + static double sum; + static float peak; + static unsigned long total; + unsigned long index; + + for (index = 0; index < count; index++) + { + float value = samples[index] < 0.0f ? -samples[index] : samples[index]; + + sum += (double)value * value; + if (value > peak) + peak = value; + } + total += count; + if (total >= (unsigned long)OUTPUT_RATE * OUTPUT_CHANNELS * 5) + { + platform_log("audio: mixed peak %.4f, rms %.5f over %lu samples", peak, sqrt(sum / total), total); + sum = 0.0; + peak = 0.0f; + total = 0; + } +} static void SDLCALL audio_callback(void *userdata, SDL_AudioStream *stream, int additional_amount, int total_amount) { @@ -445,6 +472,8 @@ static void SDLCALL audio_callback(void *userdata, SDL_AudioStream *stream, int if (!frames) frames = 1; mix(buffer, frames); + if (audio_log) + audio_log_level(buffer, frames * OUTPUT_CHANNELS); SDL_PutAudioStreamData(stream, buffer, (int)(frames * OUTPUT_CHANNELS * sizeof(float))); additional_amount -= (int)(frames * OUTPUT_CHANNELS * sizeof(float)); } @@ -480,6 +509,7 @@ static void audio_start(void) return; audio_started = TRUE; master_volume = (float)config_real("audio.volume"); + audio_log = config_boolean("debug.audio_log"); if (config_boolean("audio.enabled") && platform_sdl_initialize()) { @@ -491,6 +521,8 @@ static void audio_start(void) if (audio_stream) { SDL_ResumeAudioStreamDevice(audio_stream); + platform_log("audio: device stream open, %d Hz, %d channels, volume %.2f", OUTPUT_RATE, OUTPUT_CHANNELS, + master_volume); return; } platform_log("cannot open an audio device (%s); sound is silent", SDL_GetError()); diff --git a/port/linux/src/port_config.c b/port/linux/src/port_config.c index 12df4d97..0196358d 100644 --- a/port/linux/src/port_config.c +++ b/port/linux/src/port_config.c @@ -220,6 +220,8 @@ static const struct config_setting config_settings[] = "Log texture uploads." }, { "debug.texture_no_cache", _config_boolean, "false", "HALO_TEXTURE_NO_CACHE", _environment_set_is_true, _platform_all, "Upload textures again every time they are used." }, + { "debug.audio_log", _config_boolean, "false", "HALO_AUDIO_LOG", _environment_set_is_true, _platform_all, + "Log the level of the mixed sound every five seconds." }, { "debug.sample_seconds", _config_real, "0.0", "HALO_SAMPLE", _environment_value, _platform_android, "Log where every game thread is this often, in seconds (read by the\n" "app, port/android/host/host_debug.c); 0 never." }, diff --git a/port/linux/src/vr.c b/port/linux/src/vr.c index 8c4b4c6d..9457f1e3 100644 --- a/port/linux/src/vr.c +++ b/port/linux/src/vr.c @@ -613,6 +613,21 @@ int halo_vr_look_vector(short local_player_index, float vector[3]) return 1; } +void halo_vr_debug_look(short local_player_index, const float unit_looking[3], int in_vehicle) +{ + static unsigned long long last_us; + float head[3] = { 0.0f, 0.0f, 0.0f }; + int valid; + unsigned long long now = now_us(); + + if (!vr.log_performance || local_player_index != 0 || now - last_us < 1000000) + return; + last_us = now; + valid = halo_vr_look_vector(local_player_index, head); + platform_log("VR look: head %s (%.2f %.2f %.2f), unit looking (%.2f %.2f %.2f)%s", valid ? "valid" : "INVALID", + head[0], head[1], head[2], unit_looking[0], unit_looking[1], unit_looking[2], in_vehicle ? ", in a vehicle" : ""); +} + int halo_vr_look_delta(float *yaw, float *pitch) { /* past the head's tremble */ @@ -683,6 +698,10 @@ int halo_vr_laser(short local_player_index, float start[3], float direction[3]) return 0; } int halo_vr_look_vector(short local_player_index, float vector[3]) { (void)local_player_index; (void)vector; return 0; } +void halo_vr_debug_look(short local_player_index, const float unit_looking[3], int in_vehicle) +{ + (void)local_player_index; (void)unit_looking; (void)in_vehicle; +} int halo_vr_look_delta(float *yaw, float *pitch) { *yaw = *pitch = 0.0f; return 0; } int halo_vr_hide_crosshair(void) { return 0; } diff --git a/source/game/players.c b/source/game/players.c index 469ecfb8..05e55526 100644 --- a/source/game/players.c +++ b/source/game/players.c @@ -3511,6 +3511,15 @@ void players_update_before_game( control_data.aiming_vector.k = vr_aim[2]; } } + { + real unit_look[3]; + + unit_look[0] = unit->unit.looking_vector.i; + unit_look[1] = unit->unit.looking_vector.j; + unit_look[2] = unit->unit.looking_vector.k; + halo_vr_debug_look(player->local_player_index, unit_look, + unit->object.parent_object_index != NONE); + } #endif control_data.throttle.i = action->throttle.i; control_data.throttle.j = action->throttle.j; diff --git a/source/units/units.c b/source/units/units.c index 060a21fe..92e17a60 100644 --- a/source/units/units.c +++ b/source/units/units.c @@ -688,6 +688,10 @@ symbols in this file: #include "sound/game_sound.h" #include "vehicles.h" +#ifdef HALO_ANDROID +#include "halo_vr.h" +#endif + /* ---------- constants */ enum @@ -2971,6 +2975,22 @@ boolean unit_can_see_point( { can_see_point = TRUE; } +#ifdef HALO_ANDROID + else if (unit->unit.player_index != NONE && halo_vr_active()) + { + /* VR: the head only points roughly where the eyes look, so it + sees within a wider angle; and the weapon's laser targets too + (the training's "target each of the flashing lights" asks for + 5 degrees) */ + real head_field_of_view = MAX(field_of_view, DEGREES_TO_RADIANS(15.f)); + + if (dot_product3d(&direction, &unit->unit.looking_vector)>cosine(head_field_of_view) || + dot_product3d(&direction, &unit->unit.aiming_vector)>cosine(field_of_view)) + { + can_see_point = TRUE; + } + } +#endif } return can_see_point;