From 26c84988e7f3edf29708828374390ed0bfde79a0 Mon Sep 17 00:00:00 2001 From: salh Date: Mon, 20 Apr 2026 06:43:35 +0300 Subject: [PATCH] Add setup_and_build.bat script --- setup_and_build.bat | 130 ++ .../ac6_backend_capture_bridge.cpp | 108 ++ .../ac6_backend_capture_bridge.h | 13 + src/ac6_backend_fixes/ac6_backend_hooks.cpp | 8 +- src/ac6_backend_fixes/ac6_backend_hooks.h | 1 + .../ac6_backend_pass_classifier.cpp | 5 + src/ac6_native_graphics_overlay.cpp | 27 +- .../include/native/audio/audio_client.h | 1 + .../include/native/audio/audio_runtime.h | 1 + .../include/native/audio/conversion.h | 209 ++- .../native/audio/render_driver_frame_layout.h | 18 + thirdparty/rexglue-sdk/patch1.patch | 376 +++++ thirdparty/rexglue-sdk/patch2.patch | 1449 +++++++++++++++++ thirdparty/rexglue-sdk/patch3.patch | 156 ++ .../src/kernel/xboxkrnl/xboxkrnl_audio.cpp | 5 +- .../src/native/audio/CMakeLists.txt | 4 +- .../src/native/audio/audio_runtime.cpp | 39 +- .../audio/render_driver_frame_layout.cpp | 138 ++ .../src/native/audio/sdl/sdl_audio_driver.cpp | 8 +- .../audio/wasapi/wasapi_audio_driver.cpp | 6 +- .../src/native/audio/xma/context.cpp | 30 + 21 files changed, 2692 insertions(+), 40 deletions(-) create mode 100644 setup_and_build.bat create mode 100644 thirdparty/rexglue-sdk/include/native/audio/render_driver_frame_layout.h create mode 100644 thirdparty/rexglue-sdk/patch1.patch create mode 100644 thirdparty/rexglue-sdk/patch2.patch create mode 100644 thirdparty/rexglue-sdk/patch3.patch create mode 100644 thirdparty/rexglue-sdk/src/native/audio/render_driver_frame_layout.cpp diff --git a/setup_and_build.bat b/setup_and_build.bat new file mode 100644 index 00000000..7df7f8ae --- /dev/null +++ b/setup_and_build.bat @@ -0,0 +1,130 @@ +@echo off +setlocal enabledelayedexpansion + +echo ========================================= +echo Prerequisites Check +echo ========================================= + +:: Check CMake +cmake --version >nul 2>&1 +if %errorlevel% neq 0 ( + echo [ERROR] CMake not found on PATH. + echo Please download and install CMake 3.25+ from https://cmake.org/download/ + echo Make sure to add CMake to the system PATH during installation. + pause + exit /b 1 +) + +:: Check Ninja +ninja --version >nul 2>&1 +if %errorlevel% neq 0 ( + echo [ERROR] Ninja not found on PATH. + echo Please download Ninja from https://github.com/ninja-build/ninja/releases and add it to your PATH. + pause + exit /b 1 +) + +:: Check Clang +clang --version >nul 2>&1 +if %errorlevel% neq 0 ( + echo [ERROR] Clang not found on PATH. + echo Please download LLVM/Clang from https://github.com/llvm/llvm-project/releases or via Visual Studio Installer (C++ Clang tools) and add to PATH. + pause + exit /b 1 +) +clang++ --version >nul 2>&1 +if %errorlevel% neq 0 ( + echo [ERROR] Clang++ not found on PATH. + echo Please ensure clang++ is available. + pause + exit /b 1 +) + +:: Check Windows SDK +set sdk_found=0 +reg query "HKLM\SOFTWARE\WOW6432Node\Microsoft\Microsoft SDKs\Windows\v10.0" /v InstallationFolder >nul 2>&1 +if !errorlevel! equ 0 set sdk_found=1 +reg query "HKLM\SOFTWARE\Microsoft\Microsoft SDKs\Windows\v10.0" /v InstallationFolder >nul 2>&1 +if !errorlevel! equ 0 set sdk_found=1 + +if !sdk_found! equ 0 ( + echo [ERROR] Windows SDK 10.0.19041+ not found in registry. + echo Please install it via the Visual Studio Installer by selecting the "Windows 10 SDK (10.0.19041.0)" or newer under the "Desktop development with C++" workload. + pause + exit /b 1 +) +echo [OK] All prerequisites found! +echo. + +echo ========================================= +echo Git Branch Check +echo ========================================= +for /f "delims=" %%i in ('git rev-parse --abbrev-ref HEAD') do set CURRENT_BRANCH=%%i +echo Current branch: !CURRENT_BRANCH! +if /I "!CURRENT_BRANCH!"=="main" ( + echo Switching from main to dev-test branch... + git checkout dev-test + if !errorlevel! neq 0 ( + echo [ERROR] Failed to checkout dev-test branch. + pause + exit /b 1 + ) +) else ( + echo Already on !CURRENT_BRANCH! branch or not on main. +) +echo. + +echo ========================================= +echo ISO Detection and Extraction +echo ========================================= +set ISO_FILE= +for %%f in (*.iso) do ( + set ISO_FILE=%%f + goto :found_iso +) +:found_iso +if "!ISO_FILE!"=="" ( + echo [ERROR] No .iso file found in the current directory. + echo Please place the Ace Combat 6 ISO in this folder. + pause + exit /b 1 +) +echo Found ISO: !ISO_FILE! + +set EXTRACT_XISO_EXE=extract-xiso.exe +if not exist "!EXTRACT_XISO_EXE!" ( + echo extract-xiso not found. Downloading... + powershell -Command "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; Invoke-WebRequest -Uri 'https://github.com/XboxDev/extract-xiso/releases/download/build-202310260309/extract-xiso-win32-x64.zip' -OutFile 'extract-xiso.zip'; Expand-Archive -Path 'extract-xiso.zip' -DestinationPath 'extract-xiso-temp' -Force; Move-Item -Path 'extract-xiso-temp\extract-xiso.exe' -Destination '.' -Force; Remove-Item 'extract-xiso.zip'; Remove-Item 'extract-xiso-temp' -Recurse -Force" + if not exist "!EXTRACT_XISO_EXE!" ( + echo [ERROR] Failed to download or extract extract-xiso. + echo Please download it manually from https://github.com/XboxDev/extract-xiso/releases and place extract-xiso.exe in this folder. + pause + exit /b 1 + ) +) + +set EXTRACT_DIR=assets +echo Extracting '!ISO_FILE!' to '!EXTRACT_DIR!' directory... +if not exist "!EXTRACT_DIR!" mkdir "!EXTRACT_DIR!" +!EXTRACT_XISO_EXE! -d "!EXTRACT_DIR!" "!ISO_FILE!" +if !errorlevel! neq 0 ( + echo [ERROR] Failed to extract ISO. + pause + exit /b 1 +) +echo [OK] Extraction complete! +echo. + +echo ========================================= +echo Building the Game +echo ========================================= +cmake --build --preset win-amd64-relwithdebinfo +if !errorlevel! neq 0 ( + echo [ERROR] Build failed. + pause + exit /b 1 +) + +echo [SUCCESS] Setup and Build completed successfully! +pause +exit /b 0 diff --git a/src/ac6_backend_fixes/ac6_backend_capture_bridge.cpp b/src/ac6_backend_fixes/ac6_backend_capture_bridge.cpp index 7e88cf41..c02164ee 100644 --- a/src/ac6_backend_fixes/ac6_backend_capture_bridge.cpp +++ b/src/ac6_backend_fixes/ac6_backend_capture_bridge.cpp @@ -40,6 +40,57 @@ uint32_t CountBoundSamplers( return count; } +bool IsSamplerBound(const ac6::d3d::SamplerBinding& sampler) { + return sampler.mag_filter || sampler.min_filter || sampler.mip_filter || + sampler.mip_level || sampler.border_color; +} + +struct SamplerFilterSummary { + uint32_t point_min_count = 0; + uint32_t linear_min_count = 0; + uint32_t point_mip_count = 0; + uint32_t linear_mip_count = 0; + uint32_t anisotropic_count = 0; + uint32_t mip_clamp_count = 0; + uint32_t max_mip_level = 0; +}; + +SamplerFilterSummary SummarizeSamplerFiltering( + const std::array& samplers) { + constexpr uint32_t kTexfPoint = 1; + constexpr uint32_t kTexfLinear = 2; + constexpr uint32_t kTexfAnisotropic = 3; + + SamplerFilterSummary summary; + for (const auto& sampler : samplers) { + if (!IsSamplerBound(sampler)) { + continue; + } + + if (sampler.min_filter == kTexfPoint) { + ++summary.point_min_count; + } else if (sampler.min_filter == kTexfLinear) { + ++summary.linear_min_count; + } + + if (sampler.mip_filter == kTexfPoint) { + ++summary.point_mip_count; + } else if (sampler.mip_filter == kTexfLinear) { + ++summary.linear_mip_count; + } + + if (sampler.mag_filter == kTexfAnisotropic || sampler.min_filter == kTexfAnisotropic) { + ++summary.anisotropic_count; + } + + if (sampler.mip_level != 0) { + ++summary.mip_clamp_count; + summary.max_mip_level = std::max(summary.max_mip_level, sampler.mip_level); + } + } + return summary; +} + void HashU32(uint64_t& hash, uint32_t value) { constexpr uint64_t kFnvPrime = 1099511628211ull; hash ^= value; @@ -65,6 +116,20 @@ bool IsHalfResLike(const ac6::d3d::ShadowState& shadow_state, shadow_state.viewport.height * 4 <= swap_height * 3; } +bool IsQuarterResLike(const ac6::d3d::ShadowState& shadow_state, + const rex::system::GraphicsSwapSubmission* swap_submission) { + if (!swap_submission || !swap_submission->frontbuffer_width || + !swap_submission->frontbuffer_height || !shadow_state.viewport.width || + !shadow_state.viewport.height) { + return false; + } + + const uint32_t swap_width = swap_submission->frontbuffer_width; + const uint32_t swap_height = swap_submission->frontbuffer_height; + return shadow_state.viewport.width * 2 <= swap_width && + shadow_state.viewport.height * 2 <= swap_height; +} + bool IsLikelyUiPass(const ac6::d3d::FrameCaptureSummary& capture_summary, const ac6::d3d::ShadowState& shadow_state, const rex::system::GraphicsSwapSubmission* swap_submission) { @@ -94,6 +159,19 @@ bool IsLikelyParticlePass(const ac6::d3d::FrameCaptureSummary& capture_summary, shadow_state.depth_stencil == 0); } +bool IsLikelyPointSpritePass(const ac6::d3d::FrameCaptureSummary& capture_summary, + const ac6::d3d::ShadowState& shadow_state) { + if (capture_summary.topology_pointlist == 0 || capture_summary.primitive_draw_count == 0) { + return false; + } + + const bool mostly_point_sprites = + capture_summary.topology_pointlist * 2 >= capture_summary.primitive_draw_count; + const bool compact_stream_layout = CountBoundStreams(shadow_state.streams) <= 2; + const bool textured = CountNonZero(shadow_state.textures) != 0; + return mostly_point_sprites && compact_stream_layout && textured; +} + bool IsLikelyAdditive(const ac6::d3d::FrameCaptureSummary& capture_summary, const ac6::d3d::ShadowState& shadow_state) { if (shadow_state.depth_stencil != 0) { @@ -138,8 +216,17 @@ RenderEventSignature BuildRenderEventSignature( signature.resolve_count = capture_summary.resolve_count; signature.indexed_draw_count = capture_summary.indexed_draw_count; signature.primitive_draw_count = capture_summary.primitive_draw_count; + signature.topology_pointlist_count = capture_summary.topology_pointlist; signature.texture_count = CountNonZero(shadow_state.textures); signature.sampler_count = CountBoundSamplers(shadow_state.samplers); + const SamplerFilterSummary sampler_filters = SummarizeSamplerFiltering(shadow_state.samplers); + signature.point_min_sampler_count = sampler_filters.point_min_count; + signature.linear_min_sampler_count = sampler_filters.linear_min_count; + signature.point_mip_sampler_count = sampler_filters.point_mip_count; + signature.linear_mip_sampler_count = sampler_filters.linear_mip_count; + signature.anisotropic_sampler_count = sampler_filters.anisotropic_count; + signature.mip_clamp_sampler_count = sampler_filters.mip_clamp_count; + signature.max_sampler_mip_level = sampler_filters.max_mip_level; signature.stream_count = CountBoundStreams(shadow_state.streams); signature.fetch_constant_count = CountNonZero(shadow_state.texture_fetch_ptrs); signature.shader_gpr_alloc = shadow_state.shader_gpr_alloc; @@ -148,12 +235,18 @@ RenderEventSignature BuildRenderEventSignature( signature.has_depth_stencil = shadow_state.depth_stencil != 0; signature.has_resolve = capture_summary.resolve_count != 0; signature.half_res_like = IsHalfResLike(shadow_state, swap_submission); + signature.quarter_res_like = IsQuarterResLike(shadow_state, swap_submission); signature.post_process_like = signature.has_resolve && !signature.has_depth_stencil; signature.ui_like = IsLikelyUiPass(capture_summary, shadow_state, swap_submission); signature.particle_like = IsLikelyParticlePass(capture_summary, shadow_state); + signature.point_sprite_like = + IsLikelyPointSpritePass(capture_summary, shadow_state); + signature.point_filtered_like = + sampler_filters.point_min_count != 0 || sampler_filters.point_mip_count != 0; + signature.mip_clamped_like = sampler_filters.mip_clamp_count != 0; signature.additive_like = IsLikelyAdditive(capture_summary, shadow_state); @@ -197,6 +290,9 @@ std::string BuildSignatureTags(const RenderEventSignature& signature) { if (signature.half_res_like) { append("half_res"); } + if (signature.quarter_res_like) { + append("quarter_res"); + } if (signature.post_process_like) { append("post"); } @@ -206,6 +302,18 @@ std::string BuildSignatureTags(const RenderEventSignature& signature) { if (signature.particle_like) { append("particles"); } + if (signature.point_sprite_like) { + append("point_sprites"); + } + if (signature.point_filtered_like) { + append("point_filter"); + } + if (signature.mip_clamped_like) { + append("mip_clamp"); + } + if (signature.anisotropic_sampler_count != 0) { + append("aniso"); + } if (signature.additive_like) { append("additive"); } diff --git a/src/ac6_backend_fixes/ac6_backend_capture_bridge.h b/src/ac6_backend_fixes/ac6_backend_capture_bridge.h index 386f6352..ea17b519 100644 --- a/src/ac6_backend_fixes/ac6_backend_capture_bridge.h +++ b/src/ac6_backend_fixes/ac6_backend_capture_bridge.h @@ -15,6 +15,7 @@ enum class SignatureClass : uint8_t { kPostProcess, kUiComposite, kParticles, + kPointSpriteEffects, kClouds, kSmoke, kExplosions, @@ -34,8 +35,16 @@ struct RenderEventSignature { uint32_t resolve_count = 0; uint32_t indexed_draw_count = 0; uint32_t primitive_draw_count = 0; + uint32_t topology_pointlist_count = 0; uint32_t texture_count = 0; uint32_t sampler_count = 0; + uint32_t point_min_sampler_count = 0; + uint32_t linear_min_sampler_count = 0; + uint32_t point_mip_sampler_count = 0; + uint32_t linear_mip_sampler_count = 0; + uint32_t anisotropic_sampler_count = 0; + uint32_t mip_clamp_sampler_count = 0; + uint32_t max_sampler_mip_level = 0; uint32_t stream_count = 0; uint32_t fetch_constant_count = 0; uint32_t shader_gpr_alloc = 0; @@ -44,9 +53,13 @@ struct RenderEventSignature { bool has_depth_stencil = false; bool has_resolve = false; bool half_res_like = false; + bool quarter_res_like = false; bool post_process_like = false; bool ui_like = false; bool particle_like = false; + bool point_sprite_like = false; + bool point_filtered_like = false; + bool mip_clamped_like = false; bool additive_like = false; SignatureClass classification = SignatureClass::kUnknown; }; diff --git a/src/ac6_backend_fixes/ac6_backend_hooks.cpp b/src/ac6_backend_fixes/ac6_backend_hooks.cpp index 34b0db8a..3a278693 100644 --- a/src/ac6_backend_fixes/ac6_backend_hooks.cpp +++ b/src/ac6_backend_fixes/ac6_backend_hooks.cpp @@ -96,6 +96,7 @@ void AnalyzeFrameBoundary( if (audio_timing) { g_snapshot.audio_timing_valid = true; g_snapshot.audio_consumed_frames = audio_timing->consumed_frames; + g_snapshot.audio_queued_played_frames = audio_timing->queued_played_frames; g_snapshot.audio_submitted_tic = audio_timing->submitted_tic; g_snapshot.audio_host_elapsed_tic = audio_timing->host_elapsed_tic; g_snapshot.audio_startup_inflight_frames = audio_timing->startup_inflight_frames; @@ -104,6 +105,7 @@ void AnalyzeFrameBoundary( } else { g_snapshot.audio_timing_valid = false; g_snapshot.audio_consumed_frames = 0; + g_snapshot.audio_queued_played_frames = 0; g_snapshot.audio_submitted_tic = 0; g_snapshot.audio_host_elapsed_tic = 0; g_snapshot.audio_startup_inflight_frames = 0; @@ -122,11 +124,13 @@ void AnalyzeFrameBoundary( if (ShouldLogSignature(g_snapshot)) { REXLOG_INFO( - "AC6 backend signature frame={} class={} id={:016X} hits={} tags={} draws={} resolves={}", + "AC6 backend signature frame={} class={} id={:016X} hits={} tags={} draws={} resolves={} viewport={}x{} pointlist={}", g_snapshot.frame_index, ToString(g_snapshot.latest_signature.classification), g_snapshot.latest_signature.stable_id, g_snapshot.repeated_signature_count, g_snapshot.latest_signature_tags, g_snapshot.capture_draw_count, - g_snapshot.capture_resolve_count); + g_snapshot.capture_resolve_count, g_snapshot.latest_signature.viewport_width, + g_snapshot.latest_signature.viewport_height, + g_snapshot.latest_signature.topology_pointlist_count); } } diff --git a/src/ac6_backend_fixes/ac6_backend_hooks.h b/src/ac6_backend_fixes/ac6_backend_hooks.h index 7d088e57..fa56a18f 100644 --- a/src/ac6_backend_fixes/ac6_backend_hooks.h +++ b/src/ac6_backend_fixes/ac6_backend_hooks.h @@ -56,6 +56,7 @@ struct BackendDiagnosticsSnapshot { uint64_t guest_vblank_interval_ticks = 0; uint64_t last_guest_vblank_tick = 0; uint64_t audio_consumed_frames = 0; + uint64_t audio_queued_played_frames = 0; uint64_t audio_submitted_tic = 0; uint64_t audio_host_elapsed_tic = 0; double host_frame_time_ms = 0.0; diff --git a/src/ac6_backend_fixes/ac6_backend_pass_classifier.cpp b/src/ac6_backend_fixes/ac6_backend_pass_classifier.cpp index 831a64e6..51de719a 100644 --- a/src/ac6_backend_fixes/ac6_backend_pass_classifier.cpp +++ b/src/ac6_backend_fixes/ac6_backend_pass_classifier.cpp @@ -11,6 +11,9 @@ SignatureClass ClassifySignature(const RenderEventSignature& signature) { signature.viewport_width >= signature.viewport_height * 2) { return SignatureClass::kMissileTrails; } + if (signature.point_sprite_like) { + return SignatureClass::kPointSpriteEffects; + } if (signature.half_res_like && signature.post_process_like && signature.sampler_count >= 4 && signature.fetch_constant_count >= 2) { return SignatureClass::kClouds; @@ -43,6 +46,8 @@ const char* ToString(const SignatureClass signature_class) { return "ui_composite"; case SignatureClass::kParticles: return "particles"; + case SignatureClass::kPointSpriteEffects: + return "point_sprite_effects"; case SignatureClass::kClouds: return "clouds"; case SignatureClass::kSmoke: diff --git a/src/ac6_native_graphics_overlay.cpp b/src/ac6_native_graphics_overlay.cpp index af9a6cdd..0ddd8377 100644 --- a/src/ac6_native_graphics_overlay.cpp +++ b/src/ac6_native_graphics_overlay.cpp @@ -69,6 +69,27 @@ void NativeGraphicsStatusDialog::OnDraw(ImGuiIO& io) { ImGui::Text("signature: %016llX hits=%u", static_cast(diagnostics.latest_signature.stable_id), diagnostics.repeated_signature_count); + const uint32_t signature_viewport_width = diagnostics.latest_signature.viewport_width; + const uint32_t signature_viewport_height = diagnostics.latest_signature.viewport_height; + const uint32_t viewport_scale_x = diagnostics.frontbuffer_width + ? (signature_viewport_width * 100) / + diagnostics.frontbuffer_width + : 0; + const uint32_t viewport_scale_y = diagnostics.frontbuffer_height + ? (signature_viewport_height * 100) / + diagnostics.frontbuffer_height + : 0; + ImGui::Text("signature viewport: %ux%u (%u%% x %u%% of frontbuffer)", + signature_viewport_width, signature_viewport_height, + viewport_scale_x, viewport_scale_y); + ImGui::Text("signature point-list / primitive draws: %u / %u", + diagnostics.latest_signature.topology_pointlist_count, + diagnostics.latest_signature.primitive_draw_count); + ImGui::Text("effect hints: half_res=%s quarter_res=%s point_sprites=%s additive=%s", + diagnostics.latest_signature.half_res_like ? "yes" : "no", + diagnostics.latest_signature.quarter_res_like ? "yes" : "no", + diagnostics.latest_signature.point_sprite_like ? "yes" : "no", + diagnostics.latest_signature.additive_like ? "yes" : "no"); ImGui::TextWrapped("signature tags: %s", diagnostics.latest_signature_tags.empty() ? "none" @@ -93,9 +114,11 @@ void NativeGraphicsStatusDialog::OnDraw(ImGuiIO& io) { ImGui::Text("audio underruns / dropped / silence inject: %u / %u / %u", diagnostics.audio_underruns, diagnostics.audio_dropped_frames, diagnostics.audio_silence_injections); - ImGui::Text("audio consumed frames / submitted tic / host tic: %llu / %llu / %llu", + ImGui::Text("audio consumed / queued-played / submitted tic: %llu / %llu / %llu", static_cast(diagnostics.audio_consumed_frames), - static_cast(diagnostics.audio_submitted_tic), + static_cast(diagnostics.audio_queued_played_frames), + static_cast(diagnostics.audio_submitted_tic)); + ImGui::Text("audio host tic: %llu", static_cast(diagnostics.audio_host_elapsed_tic)); ImGui::Text("audio startup inflight / callback dispatch / throttle: %u / %u / %u", diagnostics.audio_startup_inflight_frames, diff --git a/thirdparty/rexglue-sdk/include/native/audio/audio_client.h b/thirdparty/rexglue-sdk/include/native/audio/audio_client.h index f59631ca..21aea5f2 100644 --- a/thirdparty/rexglue-sdk/include/native/audio/audio_client.h +++ b/thirdparty/rexglue-sdk/include/native/audio/audio_client.h @@ -62,6 +62,7 @@ struct AudioClientState { uint32_t callback_arg{0}; uint32_t wrapped_callback_arg{0}; uint64_t next_sequence_number{1}; + uint64_t queued_played_frames{0}; AudioDriverTelemetry telemetry{}; AudioClock clock{}; AudioDriver* driver{nullptr}; diff --git a/thirdparty/rexglue-sdk/include/native/audio/audio_runtime.h b/thirdparty/rexglue-sdk/include/native/audio/audio_runtime.h index 4bf44f31..6584c4a5 100644 --- a/thirdparty/rexglue-sdk/include/native/audio/audio_runtime.h +++ b/thirdparty/rexglue-sdk/include/native/audio/audio_runtime.h @@ -51,6 +51,7 @@ struct AudioTelemetrySnapshot { struct AudioClientTimingSnapshot { uint64_t consumed_samples{0}; uint64_t consumed_frames{0}; + uint64_t queued_played_frames{0}; uint64_t submitted_tic{0}; uint64_t startup_cap_tic{0}; uint64_t synthetic_startup_tic{0}; diff --git a/thirdparty/rexglue-sdk/include/native/audio/conversion.h b/thirdparty/rexglue-sdk/include/native/audio/conversion.h index 1ae517a7..46c828cc 100644 --- a/thirdparty/rexglue-sdk/include/native/audio/conversion.h +++ b/thirdparty/rexglue-sdk/include/native/audio/conversion.h @@ -3,13 +3,45 @@ #pragma once +#include #include +#include #include #include namespace rex::audio::conversion { +inline constexpr float kStereoDownmixCenterGain = 0.70710678f; +inline constexpr float kStereoDownmixSurroundGain = 0.5f; +inline constexpr float kStereoDownmixLfeGain = 0.0f; +inline constexpr float kStereoDownmixPeakHeadroom = 0.92f; +inline constexpr float kStereoDownmixNormalize = + 1.0f / (1.0f + kStereoDownmixCenterGain + kStereoDownmixSurroundGain + + kStereoDownmixLfeGain); + +inline float SanitizeGuestAudioSample(float sample) { + if (!std::isfinite(sample)) { + return 0.0f; + } + if (sample > 1.0f) { + return 1.0f; + } + if (sample < -1.0f) { + return -1.0f; + } + return sample; +} +#if REX_ARCH_AMD64 +inline __m128 SanitizeGuestAudioSamples(__m128 samples) { + const __m128 ordered_mask = _mm_cmpord_ps(samples, samples); + const __m128 min_sample = _mm_set1_ps(-1.0f); + const __m128 max_sample = _mm_set1_ps(1.0f); + samples = _mm_and_ps(samples, ordered_mask); + return _mm_min_ps(max_sample, _mm_max_ps(min_sample, samples)); +} +#endif + #if REX_ARCH_AMD64 inline void sequential_6_BE_to_interleaved_6_LE(float* output, const float* input, size_t ch_sample_count) { @@ -38,33 +70,108 @@ inline void sequential_6_BE_to_interleaved_2_LE(float* output, const float* inpu const __m128i byte_swap_shuffle = _mm_set_epi8(12, 13, 14, 15, 8, 9, 10, 11, 4, 5, 6, 7, 0, 1, 2, 3); - const __m128 half = _mm_set1_ps(0.5f); - const __m128 two_fifths = _mm_set1_ps(1.0f / 2.5f); + const __m128 center_gain = _mm_set1_ps(kStereoDownmixCenterGain); + const __m128 surround_gain = _mm_set1_ps(kStereoDownmixSurroundGain); + const __m128 lfe_gain = _mm_set1_ps(kStereoDownmixLfeGain); + const __m128 normalize = _mm_set1_ps(kStereoDownmixNormalize); + const __m128 peak_headroom = _mm_set1_ps(kStereoDownmixPeakHeadroom); + const __m128 sign_mask = _mm_set1_ps(-0.0f); - // put center on left and right, discard low frequency + // Use a dialogue-forward stereo fold-down. The old mapping mixed rears too + // heavily for cutscenes and could sound smeared on stereo playback. for (size_t sample = 0; sample < ch_sample_count; sample += 4) { - // load 4 samples from 6 channels each __m128 fl = _mm_loadu_ps(&input[0 * ch_sample_count + sample]); __m128 fr = _mm_loadu_ps(&input[1 * ch_sample_count + sample]); __m128 fc = _mm_loadu_ps(&input[2 * ch_sample_count + sample]); + __m128 lf = _mm_loadu_ps(&input[3 * ch_sample_count + sample]); __m128 bl = _mm_loadu_ps(&input[4 * ch_sample_count + sample]); __m128 br = _mm_loadu_ps(&input[5 * ch_sample_count + sample]); - // byte swap fl = _mm_castsi128_ps(_mm_shuffle_epi8(_mm_castps_si128(fl), byte_swap_shuffle)); fr = _mm_castsi128_ps(_mm_shuffle_epi8(_mm_castps_si128(fr), byte_swap_shuffle)); fc = _mm_castsi128_ps(_mm_shuffle_epi8(_mm_castps_si128(fc), byte_swap_shuffle)); + lf = _mm_castsi128_ps(_mm_shuffle_epi8(_mm_castps_si128(lf), byte_swap_shuffle)); bl = _mm_castsi128_ps(_mm_shuffle_epi8(_mm_castps_si128(bl), byte_swap_shuffle)); br = _mm_castsi128_ps(_mm_shuffle_epi8(_mm_castps_si128(br), byte_swap_shuffle)); + fl = SanitizeGuestAudioSamples(fl); + fr = SanitizeGuestAudioSamples(fr); + fc = SanitizeGuestAudioSamples(fc); + lf = SanitizeGuestAudioSamples(lf); + bl = SanitizeGuestAudioSamples(bl); + br = SanitizeGuestAudioSamples(br); + + __m128 left = _mm_add_ps( + _mm_add_ps(fl, _mm_mul_ps(fc, center_gain)), + _mm_add_ps(_mm_mul_ps(bl, surround_gain), _mm_mul_ps(lf, lfe_gain))); + __m128 right = _mm_add_ps( + _mm_add_ps(fr, _mm_mul_ps(fc, center_gain)), + _mm_add_ps(_mm_mul_ps(br, surround_gain), _mm_mul_ps(lf, lfe_gain))); + left = _mm_mul_ps(left, normalize); + right = _mm_mul_ps(right, normalize); + + // Apply a lightweight linked limiter instead of hard clipping. Mission + // mixes can stack enough combat layers to hit repeated peaks, which sounds + // like constant crackling when clipped. + const __m128 left_abs = _mm_andnot_ps(sign_mask, left); + const __m128 right_abs = _mm_andnot_ps(sign_mask, right); + const __m128 max_abs = _mm_max_ps(left_abs, right_abs); + const __m128 limiter_denominator = _mm_max_ps(max_abs, peak_headroom); + const __m128 limiter_scale = _mm_div_ps(peak_headroom, limiter_denominator); + left = _mm_mul_ps(left, limiter_scale); + right = _mm_mul_ps(right, limiter_scale); - __m128 center_halved = _mm_mul_ps(fc, half); - __m128 left = _mm_add_ps(_mm_add_ps(fl, bl), center_halved); - __m128 right = _mm_add_ps(_mm_add_ps(fr, br), center_halved); - left = _mm_mul_ps(left, two_fifths); - right = _mm_mul_ps(right, two_fifths); _mm_storeu_ps(&output[sample * 2], _mm_unpacklo_ps(left, right)); _mm_storeu_ps(&output[(sample + 2) * 2], _mm_unpackhi_ps(left, right)); } } + +inline void interleaved_6_BE_to_interleaved_2_LE(float* output, const float* input, + size_t ch_sample_count) { + for (size_t sample = 0; sample < ch_sample_count; ++sample) { + float fl = rex::byte_swap(input[sample * 6 + 0]); + float fr = rex::byte_swap(input[sample * 6 + 1]); + float fc = rex::byte_swap(input[sample * 6 + 2]); + float lf = rex::byte_swap(input[sample * 6 + 3]); + float bl = rex::byte_swap(input[sample * 6 + 4]); + float br = rex::byte_swap(input[sample * 6 + 5]); + fl = SanitizeGuestAudioSample(fl); + fr = SanitizeGuestAudioSample(fr); + fc = SanitizeGuestAudioSample(fc); + lf = SanitizeGuestAudioSample(lf); + bl = SanitizeGuestAudioSample(bl); + br = SanitizeGuestAudioSample(br); + float left = (fl + (fc * kStereoDownmixCenterGain) + (bl * kStereoDownmixSurroundGain) + + (lf * kStereoDownmixLfeGain)) * + kStereoDownmixNormalize; + float right = (fr + (fc * kStereoDownmixCenterGain) + (br * kStereoDownmixSurroundGain) + + (lf * kStereoDownmixLfeGain)) * + kStereoDownmixNormalize; + float max_abs = left >= 0.0f ? left : -left; + float right_abs = right >= 0.0f ? right : -right; + if (right_abs > max_abs) { + max_abs = right_abs; + } + if (max_abs > kStereoDownmixPeakHeadroom) { + const float limiter_scale = kStereoDownmixPeakHeadroom / max_abs; + left *= limiter_scale; + right *= limiter_scale; + } + output[sample * 2] = left; + output[sample * 2 + 1] = right; + } +} + +inline void render_driver_6_BE_to_interleaved_2_LE(float* output, const float* input, + size_t ch_sample_count) { + switch (ResolveRenderDriverFrameLayout(input, ch_sample_count)) { + case RenderDriverFrameLayout::kInterleaved: + interleaved_6_BE_to_interleaved_2_LE(output, input, ch_sample_count); + return; + case RenderDriverFrameLayout::kPlanar: + default: + sequential_6_BE_to_interleaved_2_LE(output, input, ch_sample_count); + return; + } +} #else inline void sequential_6_BE_to_interleaved_6_LE(float* output, const float* input, size_t ch_sample_count) { @@ -79,15 +186,85 @@ inline void sequential_6_BE_to_interleaved_2_LE(float* output, const float* inpu // Default 5.1 channel mapping is fl, fr, fc, lf, bl, br // https://docs.microsoft.com/en-us/windows/win32/xaudio2/xaudio2-default-channel-mapping for (size_t sample = 0; sample < ch_sample_count; sample++) { - // put center on left and right, discard low frequency float fl = rex::byte_swap(input[0 * ch_sample_count + sample]); float fr = rex::byte_swap(input[1 * ch_sample_count + sample]); float fc = rex::byte_swap(input[2 * ch_sample_count + sample]); - float br = rex::byte_swap(input[4 * ch_sample_count + sample]); - float bl = rex::byte_swap(input[5 * ch_sample_count + sample]); - float center_halved = fc * 0.5f; - output[sample * 2] = (fl + bl + center_halved) * (1.0f / 2.5f); - output[sample * 2 + 1] = (fr + br + center_halved) * (1.0f / 2.5f); + float lf = rex::byte_swap(input[3 * ch_sample_count + sample]); + float bl = rex::byte_swap(input[4 * ch_sample_count + sample]); + float br = rex::byte_swap(input[5 * ch_sample_count + sample]); + fl = SanitizeGuestAudioSample(fl); + fr = SanitizeGuestAudioSample(fr); + fc = SanitizeGuestAudioSample(fc); + lf = SanitizeGuestAudioSample(lf); + bl = SanitizeGuestAudioSample(bl); + br = SanitizeGuestAudioSample(br); + float left = (fl + (fc * kStereoDownmixCenterGain) + (bl * kStereoDownmixSurroundGain) + + (lf * kStereoDownmixLfeGain)) * + kStereoDownmixNormalize; + float right = (fr + (fc * kStereoDownmixCenterGain) + (br * kStereoDownmixSurroundGain) + + (lf * kStereoDownmixLfeGain)) * + kStereoDownmixNormalize; + float max_abs = left >= 0.0f ? left : -left; + float right_abs = right >= 0.0f ? right : -right; + if (right_abs > max_abs) { + max_abs = right_abs; + } + if (max_abs > kStereoDownmixPeakHeadroom) { + const float limiter_scale = kStereoDownmixPeakHeadroom / max_abs; + left *= limiter_scale; + right *= limiter_scale; + } + output[sample * 2] = left; + output[sample * 2 + 1] = right; + } +} + +inline void interleaved_6_BE_to_interleaved_2_LE(float* output, const float* input, + size_t ch_sample_count) { + for (size_t sample = 0; sample < ch_sample_count; sample++) { + float fl = rex::byte_swap(input[sample * 6 + 0]); + float fr = rex::byte_swap(input[sample * 6 + 1]); + float fc = rex::byte_swap(input[sample * 6 + 2]); + float lf = rex::byte_swap(input[sample * 6 + 3]); + float bl = rex::byte_swap(input[sample * 6 + 4]); + float br = rex::byte_swap(input[sample * 6 + 5]); + fl = SanitizeGuestAudioSample(fl); + fr = SanitizeGuestAudioSample(fr); + fc = SanitizeGuestAudioSample(fc); + lf = SanitizeGuestAudioSample(lf); + bl = SanitizeGuestAudioSample(bl); + br = SanitizeGuestAudioSample(br); + float left = (fl + (fc * kStereoDownmixCenterGain) + (bl * kStereoDownmixSurroundGain) + + (lf * kStereoDownmixLfeGain)) * + kStereoDownmixNormalize; + float right = (fr + (fc * kStereoDownmixCenterGain) + (br * kStereoDownmixSurroundGain) + + (lf * kStereoDownmixLfeGain)) * + kStereoDownmixNormalize; + float max_abs = left >= 0.0f ? left : -left; + float right_abs = right >= 0.0f ? right : -right; + if (right_abs > max_abs) { + max_abs = right_abs; + } + if (max_abs > kStereoDownmixPeakHeadroom) { + const float limiter_scale = kStereoDownmixPeakHeadroom / max_abs; + left *= limiter_scale; + right *= limiter_scale; + } + output[sample * 2] = left; + output[sample * 2 + 1] = right; + } +} + +inline void render_driver_6_BE_to_interleaved_2_LE(float* output, const float* input, + size_t ch_sample_count) { + switch (ResolveRenderDriverFrameLayout(input, ch_sample_count)) { + case RenderDriverFrameLayout::kInterleaved: + interleaved_6_BE_to_interleaved_2_LE(output, input, ch_sample_count); + return; + case RenderDriverFrameLayout::kPlanar: + default: + sequential_6_BE_to_interleaved_2_LE(output, input, ch_sample_count); + return; } } #endif diff --git a/thirdparty/rexglue-sdk/include/native/audio/render_driver_frame_layout.h b/thirdparty/rexglue-sdk/include/native/audio/render_driver_frame_layout.h new file mode 100644 index 00000000..178d9b37 --- /dev/null +++ b/thirdparty/rexglue-sdk/include/native/audio/render_driver_frame_layout.h @@ -0,0 +1,18 @@ +// Native audio runtime +// Part of the AC6 Recompilation native foundation + +#pragma once + +#include + +namespace rex::audio::conversion { + +enum class RenderDriverFrameLayout : unsigned char { + kPlanar, + kInterleaved, +}; + +RenderDriverFrameLayout ResolveRenderDriverFrameLayout(const float* input, size_t ch_sample_count); +const char* ToString(RenderDriverFrameLayout layout); + +} // namespace rex::audio::conversion diff --git a/thirdparty/rexglue-sdk/patch1.patch b/thirdparty/rexglue-sdk/patch1.patch new file mode 100644 index 00000000..23fc2486 --- /dev/null +++ b/thirdparty/rexglue-sdk/patch1.patch @@ -0,0 +1,376 @@ +From 807931c9384a822ad0a54b3fbdec5f7af7a83bc1 Mon Sep 17 00:00:00 2001 +From: Tom <1568512+tomcl7@users.noreply.github.com> +Date: Wed, 11 Mar 2026 17:43:12 -0400 +Subject: [PATCH] fix(audio): frame queuing, XMA decoder sync completion, and + diagnostics + +- Clamp queued_frames to 4-64 range, null validation in RegisterRenderDriverClient +- XMA work completion event, WriteRegister blocks until contexts finish +- XMABlockWhileInUse checks work_buffer_ptr to prevent hangs +--- + include/rex/audio/audio_system.h | 5 +-- + include/rex/audio/xma/context.h | 13 +++++++ + src/audio/audio_system.cpp | 44 +++++++++++++++++++--- + src/audio/sdl/sdl_audio_driver.cpp | 12 ++++++ + src/audio/xma_context.cpp | 3 +- + src/audio/xma_decoder.cpp | 35 +++++++++-------- + src/kernel/xboxkrnl/xboxkrnl_audio.cpp | 17 +++++++++ + src/kernel/xboxkrnl/xboxkrnl_audio_xma.cpp | 4 ++ + 8 files changed, 106 insertions(+), 27 deletions(-) + +diff --git a/include/native/audio/audio_system.h b/include/native/audio/audio_system.h +index cf67c344..6c6a72fb 100644 +--- a/include/native/audio/audio_system.h ++++ b/include/native/audio/audio_system.h +@@ -66,13 +66,12 @@ class AudioSystem : public system::IAudioSystem { + AudioDriver** out_driver) = 0; + virtual void DestroyDriver(AudioDriver* driver) = 0; + +- // TODO(gibbed): respect XAUDIO2_MAX_QUEUED_BUFFERS somehow (ie min(64, +- // XAUDIO2_MAX_QUEUED_BUFFERS)) +- // static const size_t kMaximumQueuedFrames = 64; ++ static constexpr size_t kMaximumQueuedFrames = 64; + + memory::Memory* memory_ = nullptr; + runtime::Processor* processor_ = nullptr; + std::unique_ptr xma_decoder_; ++ uint32_t queued_frames_; + + std::atomic worker_running_ = {false}; + system::object_ref worker_thread_; +diff --git a/include/native/audio/xma/context.h b/include/native/audio/xma/context.h +index 75581831..47db366e 100644 +--- a/include/native/audio/xma/context.h ++++ b/include/native/audio/xma/context.h +@@ -19,6 +19,7 @@ + + #include + #include ++#include + + // XMA audio format: + // From research, XMA appears to be based on WMA Pro with +@@ -165,6 +166,17 @@ class XmaContext { + void set_is_allocated(bool is_allocated) { is_allocated_ = is_allocated; } + void set_is_enabled(bool is_enabled) { is_enabled_ = is_enabled; } + ++ void SignalWorkDone() { ++ if (work_completion_event_) { ++ work_completion_event_->Set(); ++ } ++ } ++ void WaitForWorkDone() { ++ if (work_completion_event_) { ++ rex::thread::Wait(work_completion_event_.get(), false); ++ } ++ } ++ + private: + static void SwapInputBuffer(XMA_CONTEXT_DATA* data); + static bool TrySetupNextLoop(XMA_CONTEXT_DATA* data, bool ignore_input_buffer_offset); +@@ -188,6 +200,7 @@ class XmaContext { + int PrepareDecoder(uint8_t* packet, int sample_rate, bool is_two_channel); + + memory::Memory* memory_ = nullptr; ++ std::unique_ptr work_completion_event_; + + uint32_t id_ = 0; + uint32_t guest_ptr_ = 0; +diff --git a/src/native/audio/audio_system.cpp b/src/native/audio/audio_system.cpp +index 7a5042a6..6b9767c9 100644 +--- a/src/native/audio/audio_system.cpp ++++ b/src/native/audio/audio_system.cpp +@@ -24,7 +24,9 @@ + #include + #include + +-REXCVAR_DEFINE_INT32(audio_maxqframes, 64, "Audio", "Adjust audio maximum queued frames"); ++REXCVAR_DEFINE_INT32( ++ audio_maxqframes, 8, "Audio", ++ "Max buffered audio frames (range 4-64). Lower reduces latency but may cause stuttering."); + + // As with normal Microsoft, there are like twelve different ways to access + // the audio APIs. Early games use XMA*() methods almost exclusively to touch +@@ -44,8 +46,12 @@ AudioSystem::AudioSystem(runtime::Processor* processor) + : memory_(processor->memory()), processor_(processor), worker_running_(false) { + std::memset(clients_, 0, sizeof(clients_)); + ++ queued_frames_ = std::min( ++ static_cast(kMaximumQueuedFrames), ++ std::max(static_cast(REXCVAR_GET(audio_maxqframes)), static_cast(4))); ++ + for (size_t i = 0; i < kMaximumClientCount; ++i) { +- client_semaphores_[i] = rex::thread::Semaphore::Create(0, REXCVAR_GET(audio_maxqframes)); ++ client_semaphores_[i] = rex::thread::Semaphore::Create(0, queued_frames_); + assert_not_null(client_semaphores_[i]); + wait_handles_[i] = client_semaphores_[i].get(); + } +@@ -89,6 +95,7 @@ void AudioSystem::WorkerThreadMain() { + Initialize(); + + // Main run loop. ++ uint32_t diag_pump_count = 0; + while (worker_running_) { + // These handles signify the number of submitted samples. Once we reach + // 64 samples, we wait until our audio backend releases a semaphore +@@ -96,10 +103,16 @@ void AudioSystem::WorkerThreadMain() { + auto result = rex::thread::WaitAny(wait_handles_, rex::countof(wait_handles_), true, + std::chrono::milliseconds(500)); + if (result.first == rex::thread::WaitResult::kFailed) { +- // TODO: Assert? ++ REXAPU_WARN("AudioWorker: WaitAny failed"); + continue; + } + ++ if (result.first == rex::thread::WaitResult::kTimeout) { ++ if (diag_pump_count < 5) { ++ REXAPU_DEBUG("AudioWorker: WaitAny timed out (no semaphore signals)"); ++ } ++ } ++ + if (result.first == thread::WaitResult::kSuccess && result.second == kMaximumClientCount) { + // Shutdown event signaled. + if (paused_) { +@@ -121,10 +134,20 @@ void AudioSystem::WorkerThreadMain() { + global_lock.unlock(); + + if (client_callback) { ++ if (diag_pump_count < 10) { ++ REXAPU_DEBUG("AudioWorker: dispatching callback {:08X} with arg {:08X} for client {}", ++ client_callback, client_callback_arg, index); ++ } + SCOPE_profile_cpu_i("apu", "rex::audio::AudioSystem->client_callback"); + uint64_t args[] = {client_callback_arg}; + processor_->Execute(worker_thread_->thread_state(), client_callback, args, + rex::countof(args)); ++ if (diag_pump_count < 10) { ++ REXAPU_DEBUG("AudioWorker: callback returned for client {}", index); ++ } ++ diag_pump_count++; ++ } else { ++ REXAPU_DEBUG("AudioWorker: semaphore signaled for client {} but callback is 0", index); + } + + pumped = true; +@@ -188,13 +211,17 @@ void AudioSystem::Shutdown() { + } + + X_STATUS AudioSystem::RegisterClient(uint32_t callback, uint32_t callback_arg, size_t* out_index) { ++ REXAPU_DEBUG("AudioSystem::RegisterClient: callback={:08X} callback_arg={:08X}", callback, ++ callback_arg); + auto global_lock = global_critical_region_.Acquire(); + + auto index = FindFreeClient(); + assert_true(index >= 0); ++ REXAPU_DEBUG("AudioSystem::RegisterClient: using client index={} queued_frames={}", index, ++ queued_frames_); + + auto client_semaphore = client_semaphores_[index].get(); +- auto ret = client_semaphore->Release(REXCVAR_GET(audio_maxqframes), nullptr); ++ auto ret = client_semaphore->Release(queued_frames_, nullptr); + assert_true(ret); + + AudioDriver* driver; +@@ -219,6 +246,13 @@ X_STATUS AudioSystem::RegisterClient(uint32_t callback, uint32_t callback_arg, s + void AudioSystem::SubmitFrame(size_t index, uint32_t samples_ptr) { + SCOPE_profile_cpu_f("apu"); + ++ static uint32_t submit_count = 0; ++ if (submit_count < 10) { ++ REXAPU_DEBUG("AudioSystem::SubmitFrame called: index={} samples_ptr={:08X}", index, ++ samples_ptr); ++ submit_count++; ++ } ++ + auto global_lock = global_critical_region_.Acquire(); + assert_true(index < kMaximumClientCount); + assert_true(clients_[index].driver != NULL); +@@ -296,7 +330,7 @@ bool AudioSystem::Restore(stream::ByteStream* stream) { + client.in_use = true; + + auto client_semaphore = client_semaphores_[id].get(); +- auto ret = client_semaphore->Release(REXCVAR_GET(audio_maxqframes), nullptr); ++ auto ret = client_semaphore->Release(queued_frames_, nullptr); + assert_true(ret); + + AudioDriver* driver = nullptr; +diff --git a/src/native/audio/sdl/sdl_audio_driver.cpp b/src/native/audio/sdl/sdl_audio_driver.cpp +index 554c03bc..bd56f36c 100644 +--- a/src/native/audio/sdl/sdl_audio_driver.cpp ++++ b/src/native/audio/sdl/sdl_audio_driver.cpp +@@ -110,6 +110,13 @@ void SDLAudioDriver::SubmitFrame(uint32_t frame_ptr) { + + std::memcpy(output_frame, input_frame, frame_samples_ * sizeof(float)); + ++ static uint32_t sdl_submit_count = 0; ++ if (sdl_submit_count < 10) { ++ REXAPU_DEBUG("SDLAudioDriver::SubmitFrame: frame_ptr={:08X} queued_count={}", frame_ptr, ++ frames_queued_.size() + 1); ++ sdl_submit_count++; ++ } ++ + { + std::unique_lock guard(frames_mutex_); + frames_queued_.push(output_frame); +@@ -146,8 +153,13 @@ void SDLAudioDriver::SDLCallback(void* userdata, Uint8* stream, int len) { + assert_true(len == + static_cast(sizeof(float) * channel_samples_ * driver->sdl_device_channels_)); + ++ static uint32_t sdl_callback_count = 0; + std::unique_lock guard(driver->frames_mutex_); + if (driver->frames_queued_.empty()) { ++ if (sdl_callback_count < 10) { ++ REXAPU_DEBUG("SDLCallback: no frames queued (silence)"); ++ sdl_callback_count++; ++ } + std::memset(stream, 0, len); + } else { + auto buffer = driver->frames_queued_.front(); +diff --git a/src/native/audio/xma/context.cpp b/src/native/audio/xma/context.cpp +index 76e589e9..1b96d707 100644 +--- a/src/native/audio/xma/context.cpp ++++ b/src/native/audio/xma/context.cpp +@@ -39,7 +39,8 @@ namespace rex::audio { + + using stream::BitStream; + +-XmaContext::XmaContext() = default; ++XmaContext::XmaContext() ++ : work_completion_event_(rex::thread::Event::CreateAutoResetEvent(false)) {} + + XmaContext::~XmaContext() { + if (av_context_) { +diff --git a/src/native/audio/xma/decoder.cpp b/src/native/audio/xma/decoder.cpp +index f228692f..7780bac0 100644 +--- a/src/native/audio/xma/decoder.cpp ++++ b/src/native/audio/xma/decoder.cpp +@@ -137,18 +137,16 @@ X_STATUS XmaDecoder::Setup(system::KernelState* kernel_state) { + } + + void XmaDecoder::WorkerThreadMain() { +- uint32_t idle_loop_count = 0; + while (worker_running_) { + // Okay, let's loop through XMA contexts to find ones we need to decode! + bool did_work = false; + for (uint32_t n = 0; n < kContextCount && worker_running_; n++) { + XmaContext& context = contexts_[n]; +- did_work = context.Work() || did_work; +- +- // TODO: Need thread safety to do this. +- // Probably not too important though. +- // registers_.current_context = n; +- // registers_.next_context = (n + 1) % kContextCount; ++ bool worked = context.Work(); ++ if (worked) { ++ context.SignalWorkDone(); ++ } ++ did_work = did_work || worked; + } + + if (paused_) { +@@ -156,18 +154,11 @@ void XmaDecoder::WorkerThreadMain() { + resume_fence_.Wait(); + } + +- if (!did_work) { +- idle_loop_count++; +- } else { +- idle_loop_count = 0; +- } +- +- if (idle_loop_count > 500) { +- // Idle for an extended period. Introduce a 20ms wait. +- rex::thread::Wait(work_event_.get(), false, std::chrono::milliseconds(20)); ++ if (did_work) { ++ continue; + } +- +- rex::thread::MaybeYield(); ++ // No work done this iteration, block until signaled. ++ rex::thread::Wait(work_event_.get(), false); + } + } + +@@ -292,6 +283,7 @@ void XmaDecoder::WriteRegister(uint32_t addr, uint32_t value) { + + // The context ID is a bit in the range of the entire context array. + uint32_t base_context_id = (r - XmaRegister::Context0Kick) * 32; ++ uint32_t kicked_value = value; + for (int i = 0; value && i < 32; ++i, value >>= 1) { + if (value & 1) { + uint32_t context_id = base_context_id + i; +@@ -301,6 +293,13 @@ void XmaDecoder::WriteRegister(uint32_t addr, uint32_t value) { + } + // Signal the decoder thread to start processing. + work_event_->Set(); ++ // Block until the worker finishes, so the game sees updated context data. ++ for (int i = 0; kicked_value && i < 32; ++i, kicked_value >>= 1) { ++ if (kicked_value & 1) { ++ uint32_t context_id = base_context_id + i; ++ contexts_[context_id].WaitForWorkDone(); ++ } ++ } + } else if (r >= XmaRegister::Context0Lock && r <= XmaRegister::Context9Lock) { + // Context lock command. + // This requests a lock by flagging the context. +diff --git a/src/kernel/xboxkrnl/xboxkrnl_audio.cpp b/src/kernel/xboxkrnl/xboxkrnl_audio.cpp +index 31407f26..dfd9db81 100644 +--- a/src/kernel/xboxkrnl/xboxkrnl_audio.cpp ++++ b/src/kernel/xboxkrnl/xboxkrnl_audio.cpp +@@ -54,7 +54,17 @@ ppc_u32_result_t XAudioEnableDucker_entry(ppc_u32_t unk) { + + ppc_u32_result_t XAudioRegisterRenderDriverClient_entry(ppc_pu32_t callback_ptr, + ppc_pu32_t driver_ptr) { ++ REXKRNL_DEBUG("XAudioRegisterRenderDriverClient called! callback_ptr={:08X} driver_ptr={:08X}", ++ callback_ptr.guest_address(), driver_ptr.guest_address()); ++ if (!callback_ptr) { ++ return X_E_INVALIDARG; ++ } ++ + uint32_t callback = callback_ptr[0]; ++ ++ if (!callback) { ++ return X_E_INVALIDARG; ++ } + uint32_t callback_arg = callback_ptr[1]; + + auto* audio_system = static_cast(kernel_state()->emulator()->audio_system()); +@@ -82,6 +92,13 @@ ppc_u32_result_t XAudioSubmitRenderDriverFrame_entry(ppc_pvoid_t driver_ptr, + ppc_pvoid_t samples_ptr) { + assert_true((driver_ptr.guest_address() & 0xFFFF0000) == 0x41550000); + ++ static uint32_t submit_krnl_count = 0; ++ if (submit_krnl_count < 10) { ++ REXKRNL_DEBUG("XAudioSubmitRenderDriverFrame: driver={:08X} samples={:08X}", ++ driver_ptr.guest_address(), samples_ptr.guest_address()); ++ submit_krnl_count++; ++ } ++ + auto* audio_system = static_cast(kernel_state()->emulator()->audio_system()); + audio_system->SubmitFrame(driver_ptr.guest_address() & 0x0000FFFF, samples_ptr.guest_address()); + +diff --git a/src/kernel/xboxkrnl/xboxkrnl_audio_xma.cpp b/src/kernel/xboxkrnl/xboxkrnl_audio_xma.cpp +index 17a4b4e1..3102f4fc 100644 +--- a/src/kernel/xboxkrnl/xboxkrnl_audio_xma.cpp ++++ b/src/kernel/xboxkrnl/xboxkrnl_audio_xma.cpp +@@ -64,6 +64,7 @@ using rex::audio::XMA_CONTEXT_DATA; + // https://msdn.microsoft.com/en-us/library/windows/desktop/microsoft.directx_sdk.xaudio2.xaudio2_buffer(v=vs.85).aspx + + ppc_u32_result_t XMACreateContext_entry(ppc_pu32_t context_out_ptr) { ++ REXKRNL_DEBUG("XMACreateContext called!"); + auto xma_decoder = + static_cast(kernel_state()->emulator()->audio_system())->xma_decoder(); + uint32_t context_ptr = xma_decoder->AllocateContext(); +@@ -341,6 +342,9 @@ ppc_u32_result_t XMABlockWhileInUse_entry(ppc_pvoid_t context_ptr) { + if (!context.input_buffer_0_valid && !context.input_buffer_1_valid) { + break; + } ++ if (!context.work_buffer_ptr) { ++ break; ++ } + rex::thread::Sleep(std::chrono::milliseconds(1)); + } while (true); + return 0; +-- +2.52.0.windows.1 + diff --git a/thirdparty/rexglue-sdk/patch2.patch b/thirdparty/rexglue-sdk/patch2.patch new file mode 100644 index 00000000..0980fe1e --- /dev/null +++ b/thirdparty/rexglue-sdk/patch2.patch @@ -0,0 +1,1449 @@ +From a0271eca2604f6fc655eaa0b572e9a4fc3bd97f2 Mon Sep 17 00:00:00 2001 +From: Tom <1568512+tomcl7@users.noreply.github.com> +Date: Fri, 20 Mar 2026 08:18:02 -0400 +Subject: [PATCH] feat(audio): rewrite XMA decoder with loop fixes from Xenia + Canary + +Ports xma_context_new from Xenia Canary, fixing audio looping and improving decoder correctness: +- Exact-match loop detection (== not >=) with subframe precision +- Decode/Consume split architecture for subframe-level output control +- StoreContextMerged to prevent race conditions with game context writes +- SwapInputBuffer sets read offset past packet header +- Output space uses subframe_decode_count instead of full frame +- Consume-only context support, packet skip 0xFF, split frame headers +- XMA_CONTEXT_DATA: identified error_status, output_buffer_padding fields +- XMA helpers: const correctness, IsPacketXma2Type +--- + include/rex/audio/xma/context.h | 136 +++-- + include/rex/audio/xma/helpers.h | 30 +- + src/audio/xma_context.cpp | 994 ++++++++++++++------------------ + 3 files changed, 530 insertions(+), 630 deletions(-) + +diff --git a/include/native/audio/xma/context.h b/include/native/audio/xma/context.h +index 8dfd17c2..5298d1eb 100644 +--- a/include/native/audio/xma/context.h ++++ b/include/native/audio/xma/context.h +@@ -14,8 +14,6 @@ + #include + #include + #include +-#include +-// #include + + #include + #include +@@ -69,7 +67,8 @@ struct XMA_CONTEXT_DATA { + uint32_t loop_subframe_skip : 3; // +17bit, XMASetLoopData might be + // subframe_decode_count + uint32_t subframe_decode_count : 4; // +20bit +- uint32_t subframe_skip_count : 3; // +24bit ++ uint32_t output_buffer_padding : 3; // +24bit, extra output buffer blocks ++ // reserved per decoded frame + uint32_t sample_rate : 2; // +27bit enum of sample rates + uint32_t is_stereo : 1; // +29bit + uint32_t unk_dword_1_c : 1; // +30bit +@@ -77,12 +76,14 @@ struct XMA_CONTEXT_DATA { + + // DWORD 2 + uint32_t input_buffer_read_offset : 26; // XMAGetInputBufferReadOffset +- uint32_t unk_dword_2 : 6; // ErrorStatus/ErrorSet (?) ++ uint32_t error_status : 5; // ErrorStatus ++ uint32_t error_set : 1; // ErrorSet + + // DWORD 3 +- uint32_t loop_start : 26; // XMASetLoopData LoopStartOffset +- // frame offset in bits +- uint32_t unk_dword_3 : 6; // ? ParserErrorStatus/ParserErrorSet(?) ++ uint32_t loop_start : 26; // XMASetLoopData LoopStartOffset ++ // frame offset in bits ++ uint32_t parser_error_status : 5; // ParserErrorStatus ++ uint32_t parser_error_set : 1; // ParserErrorSet + + // DWORD 4 + uint32_t loop_end : 26; // XMASetLoopData LoopEndOffset +@@ -118,6 +119,32 @@ struct XMA_CONTEXT_DATA { + memory::copy_and_swap(reinterpret_cast(ptr), reinterpret_cast(this), + sizeof(XMA_CONTEXT_DATA) / 4); + } ++ ++ bool IsInputBufferValid(uint8_t buffer_index) const { ++ return buffer_index == 0 ? input_buffer_0_valid : input_buffer_1_valid; ++ } ++ ++ bool IsCurrentInputBufferValid() const { return IsInputBufferValid(current_buffer); } ++ ++ bool IsAnyInputBufferValid() const { return input_buffer_0_valid || input_buffer_1_valid; } ++ ++ uint32_t GetInputBufferAddress(uint8_t buffer_index) const { ++ return buffer_index == 0 ? input_buffer_0_ptr : input_buffer_1_ptr; ++ } ++ ++ uint32_t GetCurrentInputBufferAddress() const { return GetInputBufferAddress(current_buffer); } ++ ++ uint32_t GetInputBufferPacketCount(uint8_t buffer_index) const { ++ return buffer_index == 0 ? input_buffer_0_packet_count : input_buffer_1_packet_count; ++ } ++ ++ uint32_t GetCurrentInputBufferPacketCount() const { ++ return GetInputBufferPacketCount(current_buffer); ++ } ++ ++ bool IsConsumeOnlyContext() const { ++ return (input_buffer_0_packet_count | input_buffer_1_packet_count) == 0; ++ } + }; + static_assert_size(XMA_CONTEXT_DATA, 64); + +@@ -129,11 +156,26 @@ struct Xma2ExtraData { + static_assert_size(Xma2ExtraData, 34); + #pragma pack(pop) + ++struct kPacketInfo { ++ uint8_t frame_count_ = 0; ++ uint8_t current_frame_ = 0; ++ uint32_t current_frame_size_ = 0; ++ ++ bool isLastFrameInPacket() const { ++ return frame_count_ == 0 || current_frame_ == frame_count_ - 1; ++ } ++}; ++ ++static constexpr int kIdToSampleRate[4] = {24000, 32000, 44100, 48000}; ++ + class XmaContext { + public: + static const uint32_t kBytesPerPacket = 2048; + static const uint32_t kBitsPerPacket = kBytesPerPacket * 8; +- static const uint32_t kBitsPerHeader = 33; ++ static const uint32_t kBitsPerPacketHeader = 32; ++ static const uint32_t kBitsPerFrameHeader = 15; ++ static const uint32_t kBytesPerPacketHeader = 4; ++ static const uint32_t kBytesPerPacketData = kBytesPerPacket - kBytesPerPacketHeader; + + static const uint32_t kBytesPerSample = 2; + static const uint32_t kSamplesPerFrame = 512; +@@ -141,8 +183,9 @@ class XmaContext { + static const uint32_t kBytesPerFrameChannel = kSamplesPerFrame * kBytesPerSample; + static const uint32_t kBytesPerSubframeChannel = kSamplesPerSubframe * kBytesPerSample; + +- // static const uint32_t kOutputBytesPerBlock = 256; +- // static const uint32_t kOutputMaxSizeBytes = 31 * kOutputBytesPerBlock; ++ static const uint32_t kOutputBytesPerBlock = 256; ++ static const uint32_t kOutputMaxSizeBytes = 31 * kOutputBytesPerBlock; ++ static const uint32_t kMaxFrameSizeinBits = 0x4000 - kBitsPerPacketHeader; + + explicit XmaContext(); + ~XmaContext(); +@@ -181,25 +224,32 @@ class XmaContext { + + private: + static void SwapInputBuffer(XMA_CONTEXT_DATA* data); +- static bool TrySetupNextLoop(XMA_CONTEXT_DATA* data, bool ignore_input_buffer_offset); +- static void NextPacket(XMA_CONTEXT_DATA* data); + static int GetSampleRate(int id); +- // Get the offset of the next frame. Does not traverse packets. +- static size_t GetNextFrame(uint8_t* block, size_t size, size_t bit_offset); +- // Get the containing packet number of the frame pointed to by the offset. +- static int GetFramePacketNumber(uint8_t* block, size_t size, size_t bit_offset); +- // Get the packet number and the index of the frame inside that packet +- static std::tuple GetFrameNumber(uint8_t* block, size_t size, size_t bit_offset); +- // Get the number of frames contained in the packet (including truncated) and +- // if the last frame is split. +- static std::tuple GetPacketFrameCount(uint8_t* packet); +- +- // Convert sample format and swap bytes +- static void ConvertFrame(const uint8_t** samples, bool is_two_channel, uint8_t* output_buffer); ++ static int16_t GetPacketNumber(size_t size, size_t bit_offset); ++ static uint32_t GetCurrentInputBufferSize(XMA_CONTEXT_DATA* data); ++ ++ kPacketInfo GetPacketInfo(uint8_t* packet, uint32_t frame_offset); ++ uint32_t GetAmountOfBitsToRead(uint32_t remaining_stream_bits, uint32_t frame_size); ++ const uint8_t* GetNextPacket(XMA_CONTEXT_DATA* data, uint32_t next_packet_index, ++ uint32_t current_input_packet_count); ++ uint32_t GetNextPacketReadOffset(uint8_t* buffer, uint32_t next_packet_index, ++ uint32_t current_input_packet_count); ++ uint8_t* GetCurrentInputBuffer(XMA_CONTEXT_DATA* data); + +- bool ValidFrameOffset(uint8_t* block, size_t size_bytes, size_t frame_offset_bits); + void Decode(XMA_CONTEXT_DATA* data); +- int PrepareDecoder(uint8_t* packet, int sample_rate, bool is_two_channel); ++ void Consume(memory::RingBuffer* output_rb, const XMA_CONTEXT_DATA* data); ++ void UpdateLoopStatus(XMA_CONTEXT_DATA* data); ++ void ClearLocked(XMA_CONTEXT_DATA* data); ++ ++ memory::RingBuffer PrepareOutputRingBuffer(XMA_CONTEXT_DATA* data); ++ int PrepareDecoder(int sample_rate, bool is_two_channel); ++ void PreparePacket(uint32_t frame_size, uint32_t frame_padding); ++ bool DecodePacket(AVCodecContext* av_context, const AVPacket* av_packet, AVFrame* av_frame); ++ ++ void StoreContextMerged(const XMA_CONTEXT_DATA& data, const XMA_CONTEXT_DATA& initial_data, ++ uint8_t* context_ptr); ++ ++ static void ConvertFrame(const uint8_t** samples, bool is_two_channel, uint8_t* output_buffer); + + memory::Memory* memory_ = nullptr; + std::unique_ptr work_completion_event_; +@@ -209,35 +259,27 @@ class XmaContext { + std::mutex lock_; + std::atomic is_allocated_ = false; + std::atomic is_enabled_ = false; +- // bool is_dirty_ = true; + + // ffmpeg structures + AVPacket* av_packet_ = nullptr; + AVCodec* av_codec_ = nullptr; + AVCodecContext* av_context_ = nullptr; + AVFrame* av_frame_ = nullptr; +- // uint32_t decoded_consumed_samples_ = 0; // TODO do this dynamically +- // int decoded_idx_ = -1; +- +- // bool partial_frame_saved_ = false; +- // bool partial_frame_size_known_ = false; +- // size_t partial_frame_total_size_bits_ = 0; +- // size_t partial_frame_start_offset_bits_ = 0; +- // size_t partial_frame_offset_bits_ = 0; // blah internal don't use this +- // std::vector partial_frame_buffer_; +- uint32_t packets_skip_ = 0; +- +- // bool split_frame_pending_ = false; +- uint32_t split_frame_len_ = 0; +- uint32_t split_frame_len_partial_ = 0; +- uint8_t split_frame_padding_start_ = 0; +- // first byte contains bit offset information +- std::array xma_frame_; + +- // uint8_t* current_frame_ = nullptr; +- // conversion buffer for 2 channel frame ++ // Packet data buffer (two packets worth for split frame handling) ++ std::array input_buffer_; ++ // First byte contains bit offset information ++ std::array xma_frame_; ++ // Conversion buffer for up to 2-channel frame + std::array raw_frame_; +- // std::vector current_frame_ = std::vector(0); ++ ++ // Output buffer tracking ++ int32_t remaining_subframe_blocks_in_output_buffer_ = 0; ++ uint8_t current_frame_remaining_subframes_ = 0; ++ ++ // Loop subframe precision state ++ uint8_t loop_frame_output_limit_ = 0; ++ bool loop_start_skip_pending_ = false; + }; + + } // namespace rex::audio +diff --git a/include/native/audio/xma/helpers.h b/include/native/audio/xma/helpers.h +index b2e0e10e..638e406a 100644 +--- a/include/native/audio/xma/helpers.h ++++ b/include/native/audio/xma/helpers.h +@@ -17,30 +17,30 @@ + + namespace rex::audio::xma { + +-static const uint32_t kMaxFrameLength = 0x7FFF; ++static constexpr uint32_t kMaxFrameLength = 0x7FFF; + +-// Get number of frames that /begin/ in this packet. +-inline uint32_t GetPacketFrameCount(uint8_t* packet) { +- return (uint8_t)(packet[0] >> 2); ++// Get number of frames that /begin/ in this packet. Valid only for XMA2 packets. ++inline uint8_t GetPacketFrameCount(const uint8_t* packet) { ++ return packet[0] >> 2; + } + + // Get the first frame offset in bits +-inline uint32_t GetPacketFrameOffset(uint8_t* packet) { +- uint32_t val = (uint16_t)(((packet[0] & 0x3) << 13) | (packet[1] << 5) | (packet[2] >> 3)); +- // if (val > kBitsPerPacket - kBitsPerHeader) { +- // // There is no data in this packet +- // return -1; +- // } else { ++inline uint32_t GetPacketFrameOffset(const uint8_t* packet) { ++ uint32_t val = ++ static_cast(((packet[0] & 0x3) << 13) | (packet[1] << 5) | (packet[2] >> 3)); + return val + 32; +- // } + } + +-inline uint32_t GetPacketMetadata(uint8_t* packet) { +- return (uint8_t)(packet[2] & 0x7); ++inline uint8_t GetPacketMetadata(const uint8_t* packet) { ++ return packet[2] & 0x7; + } + +-inline uint32_t GetPacketSkipCount(uint8_t* packet) { +- return (uint8_t)(packet[3]); ++inline bool IsPacketXma2Type(const uint8_t* packet) { ++ return GetPacketMetadata(packet) == 1; ++} ++ ++inline uint8_t GetPacketSkipCount(const uint8_t* packet) { ++ return packet[3]; + } + + } // namespace rex::audio::xma +diff --git a/src/native/audio/xma/context.cpp b/src/native/audio/xma/context.cpp +index 49aae593..a6e79733 100644 +--- a/src/native/audio/xma/context.cpp ++++ b/src/native/audio/xma/context.cpp +@@ -52,9 +52,6 @@ XmaContext::~XmaContext() { + if (av_frame_) { + av_frame_free(&av_frame_); + } +- // if (current_frame_) { +- // delete[] current_frame_; +- // } + } + + int XmaContext::Setup(uint32_t id, memory::Memory* memory, uint32_t guest_ptr) { +@@ -94,35 +91,66 @@ int XmaContext::Setup(uint32_t id, memory::Memory* memory, uint32_t guest_ptr) { + } + + bool XmaContext::Work() { +- std::lock_guard lock(lock_); + if (!is_allocated() || !is_enabled()) { + return false; + } + ++ std::lock_guard lock(lock_); + set_is_enabled(false); + + auto context_ptr = memory()->TranslateVirtual(guest_ptr()); + XMA_CONTEXT_DATA data(context_ptr); +- Decode(&data); +- data.Store(context_ptr); +- return true; +-} ++ const XMA_CONTEXT_DATA initial_data = data; + +-void XmaContext::Enable() { +- std::lock_guard lock(lock_); ++ if (!data.output_buffer_valid) { ++ return true; ++ } + +- auto context_ptr = memory()->TranslateVirtual(guest_ptr()); +- XMA_CONTEXT_DATA data(context_ptr); ++ memory::RingBuffer output_rb = PrepareOutputRingBuffer(&data); + +- REXAPU_TRACE("XmaContext: kicking context {} (buffer {} {}/{} bits)", id(), +- static_cast(data.current_buffer), +- static_cast(data.input_buffer_read_offset), +- (data.current_buffer == 0 ? data.input_buffer_0_packet_count +- : data.input_buffer_1_packet_count) * +- kBitsPerPacket); ++ // Consume-only context: no input, just drain remaining subframes. ++ if (data.IsConsumeOnlyContext()) { ++ if (current_frame_remaining_subframes_ == 0) { ++ return true; ++ } ++ Consume(&output_rb, &data); ++ data.output_buffer_write_offset = output_rb.write_offset() / kOutputBytesPerBlock; ++ StoreContextMerged(data, initial_data, context_ptr); ++ return true; ++ } + +- data.Store(context_ptr); ++ // Minimum free blocks needed before attempting a decode. ++ // Use subframe_decode_count (clamped to 1) instead of full frame size. ++ const uint32_t effective_sdc = std::max(static_cast(1), data.subframe_decode_count); ++ const int32_t minimum_subframe_decode_count = ++ static_cast(effective_sdc) + data.output_buffer_padding; ++ ++ if (minimum_subframe_decode_count > remaining_subframe_blocks_in_output_buffer_) { ++ StoreContextMerged(data, initial_data, context_ptr); ++ return true; ++ } ++ ++ while (remaining_subframe_blocks_in_output_buffer_ >= minimum_subframe_decode_count) { ++ Decode(&data); ++ Consume(&output_rb, &data); ++ ++ if (!data.IsAnyInputBufferValid() || data.error_status == 4) { ++ break; ++ } ++ } + ++ data.output_buffer_write_offset = output_rb.write_offset() / kOutputBytesPerBlock; ++ ++ if (output_rb.empty()) { ++ data.output_buffer_valid = 0; ++ } ++ ++ StoreContextMerged(data, initial_data, context_ptr); ++ return true; ++} ++ ++void XmaContext::Enable() { ++ std::lock_guard lock(lock_); + set_is_enabled(true); + } + +@@ -143,685 +171,515 @@ void XmaContext::Clear() { + + auto context_ptr = memory()->TranslateVirtual(guest_ptr()); + XMA_CONTEXT_DATA data(context_ptr); ++ ClearLocked(&data); ++ data.Store(context_ptr); ++} + +- data.input_buffer_0_valid = 0; +- data.input_buffer_1_valid = 0; +- data.output_buffer_valid = 0; ++void XmaContext::ClearLocked(XMA_CONTEXT_DATA* data) { ++ data->input_buffer_0_valid = 0; ++ data->input_buffer_1_valid = 0; ++ data->output_buffer_valid = 0; + +- data.output_buffer_read_offset = 0; +- data.output_buffer_write_offset = 0; ++ data->input_buffer_read_offset = kBitsPerPacketHeader; ++ data->output_buffer_read_offset = 0; ++ data->output_buffer_write_offset = 0; + +- data.Store(context_ptr); ++ current_frame_remaining_subframes_ = 0; ++ loop_frame_output_limit_ = 0; ++ loop_start_skip_pending_ = false; + } + + void XmaContext::Disable() { + std::lock_guard lock(lock_); +- REXAPU_TRACE("XmaContext: disabling context {}", id()); + set_is_enabled(false); + } + + void XmaContext::Release() { +- // Lock it in case the decoder thread is working on it now. + std::lock_guard lock(lock_); + assert_true(is_allocated()); + + set_is_allocated(false); + auto context_ptr = memory()->TranslateVirtual(guest_ptr()); +- std::memset(context_ptr, 0, sizeof(XMA_CONTEXT_DATA)); // Zero it. ++ std::memset(context_ptr, 0, sizeof(XMA_CONTEXT_DATA)); + } + + void XmaContext::SwapInputBuffer(XMA_CONTEXT_DATA* data) { +- // No more frames. + if (data->current_buffer == 0) { + data->input_buffer_0_valid = 0; + } else { + data->input_buffer_1_valid = 0; + } + data->current_buffer ^= 1; +- data->input_buffer_read_offset = 0; ++ data->input_buffer_read_offset = kBitsPerPacketHeader; + } + +-bool XmaContext::TrySetupNextLoop(XMA_CONTEXT_DATA* data, bool ignore_input_buffer_offset) { +- // Setup the input buffer offset if next loop exists. +- // TODO(Pseudo-Kernel): Need to handle loop in the following cases. +- // 1. loop_start == loop_end == 0 +- // 2. loop_start > loop_end && loop_count > 0 +- if (data->loop_count > 0 && data->loop_start < data->loop_end && +- (ignore_input_buffer_offset || data->input_buffer_read_offset >= data->loop_end)) { +- // Loop back to the beginning. +- data->input_buffer_read_offset = data->loop_start; +- if (data->loop_count < 255) { +- data->loop_count--; +- } +- return true; ++void XmaContext::UpdateLoopStatus(XMA_CONTEXT_DATA* data) { ++ if (data->loop_count == 0) { ++ return; + } +- return false; +-} + +-/* +-void XmaContext::NextPacket( +- uint8_t* input_buffer, +- uint32_t input_size, +- uint32_t input_buffer_read_offset) { +-*/ +-void XmaContext::NextPacket(XMA_CONTEXT_DATA* data) { +- // auto packet_idx = GetFramePacketNumber(input_buffer, input_size, +- // input_buffer_read_offset); ++ const uint32_t loop_start = std::max(kBitsPerPacketHeader, data->loop_start); ++ const uint32_t loop_end = std::max(kBitsPerPacketHeader, data->loop_end); ++ ++ if (data->input_buffer_read_offset != loop_end) { ++ return; ++ } + +- // packet_idx++; +- // if (packet_idx++ >= input_size) ++ data->input_buffer_read_offset = loop_start; ++ loop_start_skip_pending_ = true; ++ ++ if (data->loop_count < 255) { ++ data->loop_count--; ++ } + } + + int XmaContext::GetSampleRate(int id) { +- switch (id) { +- case 0: +- return 24000; +- case 1: +- return 32000; +- case 2: +- return 44100; +- case 3: +- return 48000; +- } +- assert_always(); +- return 0; ++ return kIdToSampleRate[std::min(id, 3)]; + } + +-bool XmaContext::ValidFrameOffset(uint8_t* block, size_t size_bytes, size_t frame_offset_bits) { +- uint32_t packet_num = GetFramePacketNumber(block, size_bytes, frame_offset_bits); +- if (packet_num == -1) { +- // Invalid packet number +- return false; ++int16_t XmaContext::GetPacketNumber(size_t size, size_t bit_offset) { ++ if (bit_offset < kBitsPerPacketHeader) { ++ assert_always(); ++ return -1; ++ } ++ if (bit_offset >= (size << 3)) { ++ assert_always(); ++ return -1; + } ++ size_t byte_offset = bit_offset >> 3; ++ size_t packet_number = byte_offset / kBytesPerPacket; ++ return static_cast(packet_number); ++} + +- uint8_t* packet = block + (packet_num * kBytesPerPacket); +- size_t relative_offset_bits = frame_offset_bits % kBitsPerPacket; ++uint32_t XmaContext::GetCurrentInputBufferSize(XMA_CONTEXT_DATA* data) { ++ return data->GetCurrentInputBufferPacketCount() * kBytesPerPacket; ++} + +- uint32_t first_frame_offset = xma::GetPacketFrameOffset(packet); +- if (first_frame_offset == -1 || first_frame_offset > kBitsPerPacket) { +- // Packet only contains a partial frame, so no frames can start here. +- return false; ++uint8_t* XmaContext::GetCurrentInputBuffer(XMA_CONTEXT_DATA* data) { ++ return memory()->TranslatePhysical(data->GetCurrentInputBufferAddress()); ++} ++ ++uint32_t XmaContext::GetAmountOfBitsToRead(uint32_t remaining_stream_bits, uint32_t frame_size) { ++ return std::min(remaining_stream_bits, frame_size); ++} ++ ++const uint8_t* XmaContext::GetNextPacket(XMA_CONTEXT_DATA* data, uint32_t next_packet_index, ++ uint32_t current_input_packet_count) { ++ if (next_packet_index < current_input_packet_count) { ++ return memory()->TranslatePhysical(data->GetCurrentInputBufferAddress()) + ++ next_packet_index * kBytesPerPacket; ++ } ++ ++ const uint8_t next_buffer_index = data->current_buffer ^ 1; ++ if (!data->IsInputBufferValid(next_buffer_index)) { ++ return nullptr; ++ } ++ ++ const uint32_t next_buffer_address = data->GetInputBufferAddress(next_buffer_index); ++ if (!next_buffer_address) { ++ REXAPU_ERROR("XmaContext {}: Buffer marked valid but has null pointer!", id()); ++ return nullptr; + } + ++ return memory()->TranslatePhysical(next_buffer_address); ++} ++ ++uint32_t XmaContext::GetNextPacketReadOffset(uint8_t* buffer, uint32_t next_packet_index, ++ uint32_t current_input_packet_count) { ++ while (next_packet_index < current_input_packet_count) { ++ uint8_t* next_packet = buffer + (next_packet_index * kBytesPerPacket); ++ const uint32_t packet_frame_offset = xma::GetPacketFrameOffset(next_packet); ++ ++ if (packet_frame_offset <= kMaxFrameSizeinBits) { ++ return (next_packet_index * kBitsPerPacket) + packet_frame_offset; ++ } ++ next_packet_index++; ++ } ++ ++ return kBitsPerPacketHeader; ++} ++ ++memory::RingBuffer XmaContext::PrepareOutputRingBuffer(XMA_CONTEXT_DATA* data) { ++ const uint32_t output_capacity = data->output_buffer_block_count * kOutputBytesPerBlock; ++ const uint32_t output_read_offset = data->output_buffer_read_offset * kOutputBytesPerBlock; ++ const uint32_t output_write_offset = data->output_buffer_write_offset * kOutputBytesPerBlock; ++ ++ if (output_capacity > kOutputMaxSizeBytes) { ++ REXAPU_WARN( ++ "XmaContext {}: Output buffer exceeds expected size! " ++ "(Actual: {} Max: {})", ++ id(), output_capacity, kOutputMaxSizeBytes); ++ } ++ ++ uint8_t* output_buffer = memory()->TranslatePhysical(data->output_buffer_ptr); ++ ++ memory::RingBuffer output_rb(output_buffer, output_capacity); ++ output_rb.set_read_offset(output_read_offset); ++ output_rb.set_write_offset(output_write_offset); ++ remaining_subframe_blocks_in_output_buffer_ = ++ static_cast(output_rb.write_count()) / kOutputBytesPerBlock; ++ ++ return output_rb; ++} ++ ++kPacketInfo XmaContext::GetPacketInfo(uint8_t* packet, uint32_t frame_offset) { ++ kPacketInfo packet_info = {}; ++ ++ const uint32_t first_frame_offset = xma::GetPacketFrameOffset(packet); + BitStream stream(packet, kBitsPerPacket); + stream.SetOffset(first_frame_offset); ++ ++ if (frame_offset < first_frame_offset) { ++ packet_info.current_frame_ = 0; ++ packet_info.current_frame_size_ = first_frame_offset - frame_offset; ++ } ++ + while (true) { +- if (stream.offset_bits() == relative_offset_bits) { +- return true; ++ if (stream.BitsRemaining() < kBitsPerFrameHeader) { ++ break; + } + +- if (stream.BitsRemaining() < 15) { +- // Not enough room for another frame header. +- return false; ++ const uint64_t frame_size = stream.Peek(kBitsPerFrameHeader); ++ if (frame_size == 0 || frame_size == xma::kMaxFrameLength) { ++ break; + } + +- uint64_t size = stream.Read(15); +- if ((size - 15) > stream.BitsRemaining()) { +- // Last frame. +- return false; +- } else if (size == 0x7FFF) { +- // Invalid frame (and last of this packet) +- return false; ++ if (stream.offset_bits() == frame_offset) { ++ packet_info.current_frame_ = packet_info.frame_count_; ++ packet_info.current_frame_size_ = static_cast(frame_size); + } + +- stream.Advance(size - 16); ++ packet_info.frame_count_++; + +- // Read the trailing bit to see if frames follow +- if (stream.Read(1) == 0) { ++ if (frame_size > stream.BitsRemaining()) { + break; + } +- } + +- return false; +-} ++ stream.Advance(frame_size - 1); + +-static void dump_raw(AVFrame* frame, int id) { +- FILE* outfile = fopen(fmt::format("out{}.raw", id).c_str(), "ab"); +- if (!outfile) { +- return; ++ if (stream.Read(1) == 0) { ++ break; ++ } + } +- size_t data_size = sizeof(float); +- for (int i = 0; i < frame->nb_samples; i++) { +- for (int ch = 0; ch < frame->channels; ch++) { +- fwrite(frame->data[ch] + data_size * i, 1, data_size, outfile); ++ ++ if (xma::IsPacketXma2Type(packet)) { ++ const uint8_t xma2_frame_count = xma::GetPacketFrameCount(packet); ++ if (xma2_frame_count > packet_info.frame_count_) { ++ if (packet_info.current_frame_size_ == 0) { ++ packet_info.current_frame_ = packet_info.frame_count_; ++ } ++ packet_info.frame_count_ = xma2_frame_count; + } + } +- fclose(outfile); ++ return packet_info; + } + +-void XmaContext::Decode(XMA_CONTEXT_DATA* data) { +- SCOPE_profile_cpu_f("apu"); +- +- // What I see: +- // XMA outputs 2 bytes per sample +- // 512 samples per frame (128 per subframe) +- // Max output size is data.output_buffer_block_count * 256 ++void XmaContext::StoreContextMerged(const XMA_CONTEXT_DATA& data, ++ const XMA_CONTEXT_DATA& initial_data, uint8_t* context_ptr) { ++ XMA_CONTEXT_DATA fresh(context_ptr); + +- // This decoder is fed packets (max 4095 per buffer) +- // Packets contain "some" frames +- // 32bit header (big endian) ++ fresh.loop_count = data.loop_count; ++ fresh.output_buffer_write_offset = data.output_buffer_write_offset; ++ if (initial_data.input_buffer_0_valid && !data.input_buffer_0_valid) { ++ fresh.input_buffer_0_valid = 0; ++ } ++ if (initial_data.input_buffer_1_valid && !data.input_buffer_1_valid) { ++ fresh.input_buffer_1_valid = 0; ++ } + +- // Frames are the smallest thing the SPUs can decode. +- // They can and usually will span packets. ++ if (initial_data.output_buffer_valid && !data.output_buffer_valid) { ++ fresh.output_buffer_valid = 0; ++ } + +- // Sample rates (data.sample_rate): +- // 0 - 24 kHz +- // 1 - 32 kHz +- // 2 - 44.1 kHz +- // 3 - 48 kHz ++ fresh.input_buffer_read_offset = data.input_buffer_read_offset; ++ fresh.error_status = data.error_status; ++ fresh.current_buffer = data.current_buffer; ++ fresh.output_buffer_read_offset = data.output_buffer_read_offset; + +- // SPUs also support stereo decoding. (data.is_stereo) ++ fresh.Store(context_ptr); ++} + +- // Check the output buffer - we cannot decode anything else if it's +- // unavailable. +- if (!data->output_buffer_valid) { ++void XmaContext::Consume(memory::RingBuffer* output_rb, const XMA_CONTEXT_DATA* data) { ++ if (!current_frame_remaining_subframes_) { + return; + } + +- // No available data. +- if (!data->input_buffer_0_valid && !data->input_buffer_1_valid) { +- data->output_buffer_valid = 0; +- return; ++ if (loop_frame_output_limit_ > 0) { ++ const uint8_t total_subframes = (kBytesPerFrameChannel / kOutputBytesPerBlock) ++ << data->is_stereo; ++ const uint8_t consumed = total_subframes - current_frame_remaining_subframes_; ++ if (consumed >= loop_frame_output_limit_) { ++ remaining_subframe_blocks_in_output_buffer_ -= data->output_buffer_padding; ++ current_frame_remaining_subframes_ = 0; ++ loop_frame_output_limit_ = 0; ++ return; ++ } + } + +- // XAudio Loops +- // loop_count: +- // - XAUDIO2_MAX_LOOP_COUNT = 254 +- // - XAUDIO2_LOOP_INFINITE = 255 +- // loop_start/loop_end are bit offsets to a specific frame +- +- // Translate pointers for future use. +- // Sometimes the game will use rolling input buffers. If they do, we cannot +- // assume they form a complete block! In addition, the buffers DO NOT have +- // to be contiguous! +- uint8_t* in0 = +- data->input_buffer_0_valid ? memory()->TranslatePhysical(data->input_buffer_0_ptr) : nullptr; +- uint8_t* in1 = +- data->input_buffer_1_valid ? memory()->TranslatePhysical(data->input_buffer_1_ptr) : nullptr; +- uint8_t* current_input_buffer = data->current_buffer ? in1 : in0; +- +- REXAPU_TRACE("Processing context {} (offset {}, buffer {}, ptr {:p})", id(), +- static_cast(data->input_buffer_read_offset), +- static_cast(data->current_buffer), +- static_cast(current_input_buffer)); +- +- size_t input_buffer_0_size = data->input_buffer_0_packet_count * kBytesPerPacket; +- size_t input_buffer_1_size = data->input_buffer_1_packet_count * kBytesPerPacket; +- size_t input_total_size = input_buffer_0_size + input_buffer_1_size; +- +- size_t current_input_size = data->current_buffer ? input_buffer_1_size : input_buffer_0_size; +- size_t current_input_packet_count = current_input_size / kBytesPerPacket; +- +- // Output buffers are in raw PCM samples, 256 bytes per block. +- // Output buffer is a ring buffer. We need to write from the write offset +- // to the read offset. +- uint8_t* output_buffer = memory()->TranslatePhysical(data->output_buffer_ptr); +- uint32_t output_capacity = data->output_buffer_block_count * kBytesPerSubframeChannel; +- uint32_t output_read_offset = data->output_buffer_read_offset * kBytesPerSubframeChannel; +- uint32_t output_write_offset = data->output_buffer_write_offset * kBytesPerSubframeChannel; +- +- memory::RingBuffer output_rb(output_buffer, output_capacity); +- output_rb.set_read_offset(output_read_offset); +- output_rb.set_write_offset(output_write_offset); ++ const uint8_t effective_sdc = std::max(static_cast(1), data->subframe_decode_count); ++ int8_t subframes_to_write = std::min(static_cast(current_frame_remaining_subframes_), ++ static_cast(effective_sdc)); + +- // We can only decode an entire frame and write it out at a time, so +- // don't save any samples. +- // TODO(JoelLinn): subframes when looping +- size_t output_remaining_bytes = output_rb.write_count(); +- output_remaining_bytes -= output_remaining_bytes % (kBytesPerFrameChannel << data->is_stereo); +- +- // is_dirty_ = true; // TODO +- // is_dirty_ = false; // TODO +- assert_false(data->stop_when_done); +- assert_false(data->interrupt_when_done); +- static int total_samples = 0; +- bool reuse_input_buffer = false; +- // Decode until we can't write any more data. +- while (output_remaining_bytes > 0) { +- if (!data->input_buffer_0_valid && !data->input_buffer_1_valid) { +- // Out of data. +- break; ++ if (loop_frame_output_limit_ > 0) { ++ const uint8_t total_subframes = (kBytesPerFrameChannel / kOutputBytesPerBlock) ++ << data->is_stereo; ++ const uint8_t consumed = total_subframes - current_frame_remaining_subframes_; ++ const int8_t remaining_until_limit = static_cast(loop_frame_output_limit_ - consumed); ++ if (subframes_to_write > remaining_until_limit) { ++ subframes_to_write = remaining_until_limit; + } ++ } + +- // Setup the input buffer if we are at loop_end. +- // The input buffer must not be swapped out until all loops are processed. +- reuse_input_buffer = TrySetupNextLoop(data, false); +- +- // assert_true(packets_skip_ == 0); +- // assert_true(split_frame_len_ == 0); +- // assert_true(split_frame_len_partial_ == 0); +- +- // Where are we in the buffer (in XMA jargon) +- int packet_idx, frame_idx, frame_count; +- uint8_t* packet; +- bool frame_last_split; +- +- BitStream stream(current_input_buffer, current_input_size * 8); +- stream.SetOffset(data->input_buffer_read_offset); +- +- // if we had a buffer swap try to skip packets first +- if (packets_skip_ > 0) { +- packet_idx = GetFramePacketNumber(current_input_buffer, current_input_size, +- data->input_buffer_read_offset); +- while (packets_skip_ > 0) { +- packets_skip_--; +- packet_idx++; +- if (packet_idx >= current_input_packet_count) { +- if (!reuse_input_buffer) { +- // Last packet. Try setup once more. +- reuse_input_buffer = TrySetupNextLoop(data, true); +- } +- if (!reuse_input_buffer) { +- SwapInputBuffer(data); +- } +- return; +- } +- } +- // invalid frame pointer but needed for us +- data->input_buffer_read_offset = packet_idx * kBitsPerPacket; +- // continue; +- } ++ const int8_t raw_frame_read_offset = ++ ((kBytesPerFrameChannel / kOutputBytesPerBlock) << data->is_stereo) - ++ current_frame_remaining_subframes_; + +- if (split_frame_len_) { +- // handle a frame that was split over two packages +- packet_idx = GetFramePacketNumber(current_input_buffer, current_input_size, +- data->input_buffer_read_offset); +- packet = current_input_buffer + packet_idx * kBytesPerPacket; +- std::tie(frame_count, frame_last_split) = GetPacketFrameCount(packet); +- frame_idx = -1; +- +- stream = BitStream(current_input_buffer, (packet_idx + 1) * kBitsPerPacket); +- stream.SetOffset(packet_idx * kBitsPerPacket + 32); +- +- if (split_frame_len_ > xma::kMaxFrameLength) { +- // TODO write CopyPeekMethod +- auto offset = stream.offset_bits(); +- stream.Copy( +- xma_frame_.data() + 1 + ((split_frame_len_partial_ + split_frame_padding_start_) / 8), +- 15 - split_frame_len_partial_); +- stream.SetOffset(offset); +- BitStream slen(xma_frame_.data() + 1, 15 + split_frame_padding_start_); +- slen.Advance(split_frame_padding_start_); +- split_frame_len_ = static_cast(slen.Read(15)); +- } ++ output_rb->Write(raw_frame_.data() + (kOutputBytesPerBlock * raw_frame_read_offset), ++ subframes_to_write * kOutputBytesPerBlock); + +- if (frame_count > 0) { +- assert_true(xma::GetPacketFrameOffset(packet) - 32 == +- split_frame_len_ - split_frame_len_partial_); +- } +- +- auto offset = stream.Copy( +- xma_frame_.data() + 1 + ((split_frame_len_partial_ + split_frame_padding_start_) / 8), +- split_frame_len_ - split_frame_len_partial_); +- assert_true(offset == (split_frame_padding_start_ + split_frame_len_partial_) % 8); +- } else { +- if (data->input_buffer_read_offset % kBitsPerPacket == 0) { +- // Invalid offset. Go ahead and set it. +- int packet_number = GetFramePacketNumber(current_input_buffer, current_input_size, +- data->input_buffer_read_offset); +- +- if (packet_number == -1) { +- return; +- } +- +- auto offset = +- xma::GetPacketFrameOffset(current_input_buffer + kBytesPerPacket * packet_number) + +- data->input_buffer_read_offset; +- if (offset == -1) { +- // No more frames. +- SwapInputBuffer(data); +- // TODO partial frames? end? +- REXAPU_ERROR("XmaContext {}: TODO partial frames? end?", id()); +- assert_always("TODO"); +- return; +- } else { +- data->input_buffer_read_offset = offset; +- } +- } ++ const int8_t headroom = (current_frame_remaining_subframes_ - subframes_to_write == 0) ++ ? data->output_buffer_padding ++ : 0; + +- if (!ValidFrameOffset(current_input_buffer, current_input_size, +- data->input_buffer_read_offset)) { +- REXAPU_DEBUG("XmaContext {}: Invalid read offset {}!", id(), +- static_cast(data->input_buffer_read_offset)); +- SwapInputBuffer(data); +- return; +- } ++ remaining_subframe_blocks_in_output_buffer_ -= subframes_to_write + headroom; ++ current_frame_remaining_subframes_ -= subframes_to_write; ++} + +- // Where are we in the buffer (in XMA jargon) +- std::tie(packet_idx, frame_idx) = +- GetFrameNumber(current_input_buffer, current_input_size, data->input_buffer_read_offset); +- // TODO handle +- assert_true(packet_idx >= 0); +- assert_true(frame_idx >= 0); +- packet = current_input_buffer + packet_idx * kBytesPerPacket; +- // frames that belong to this packet +- std::tie(frame_count, frame_last_split) = GetPacketFrameCount(packet); +- assert_true(frame_count >= 0); // TODO end +- +- PrepareDecoder(packet, data->sample_rate, bool(data->is_stereo)); +- +- // Current frame is split to next packet: +- bool frame_is_split = frame_last_split && (frame_idx >= frame_count - 1); +- +- stream = BitStream(current_input_buffer, (packet_idx + 1) * kBitsPerPacket); +- stream.SetOffset(data->input_buffer_read_offset); +- // int frame_len; +- // int frame_len_partial +- split_frame_len_partial_ = static_cast(stream.BitsRemaining()); +- if (split_frame_len_partial_ >= 15) { +- split_frame_len_ = static_cast(stream.Peek(15)); +- } else { +- // assert_always(); +- split_frame_len_ = xma::kMaxFrameLength + 1; +- } +- assert_true(frame_is_split == (split_frame_len_ > split_frame_len_partial_)); ++int XmaContext::PrepareDecoder(int sample_rate, bool is_two_channel) { ++ sample_rate = GetSampleRate(sample_rate); + +- // TODO fix bitstream copy +- std::memset(xma_frame_.data(), 0, xma_frame_.size()); ++ uint32_t channels = is_two_channel ? 2 : 1; ++ if (av_context_->sample_rate != sample_rate || ++ av_context_->channels != static_cast(channels)) { ++ avcodec_close(av_context_); ++ av_free(av_context_); ++ av_context_ = avcodec_alloc_context3(av_codec_); + +- { +- auto offset = stream.Copy(xma_frame_.data() + 1, +- std::min(split_frame_len_, split_frame_len_partial_)); +- assert_true(offset < 8); +- split_frame_padding_start_ = static_cast(offset); +- } ++ av_context_->sample_rate = sample_rate; ++ av_context_->channels = channels; + +- if (frame_is_split) { +- // go to next xma packet of this stream +- packets_skip_ = xma::GetPacketSkipCount(packet) + 1; +- while (packets_skip_ > 0) { +- packets_skip_--; +- packet += kBytesPerPacket; +- packet_idx++; +- if (packet_idx >= current_input_packet_count) { +- if (!reuse_input_buffer) { +- // Last packet. Try setup once more. +- reuse_input_buffer = TrySetupNextLoop(data, true); +- } +- if (!reuse_input_buffer) { +- SwapInputBuffer(data); +- } +- return; +- } +- } +- // TODO guest might read this: +- data->input_buffer_read_offset = packet_idx * kBitsPerPacket; +- continue; +- } ++ if (avcodec_open2(av_context_, av_codec_, NULL) < 0) { ++ REXAPU_ERROR("XmaContext: Failed to reopen FFmpeg context"); ++ return -1; + } ++ return 1; ++ } ++ return 0; ++} + +- av_packet_->data = xma_frame_.data(); +- av_packet_->size = +- static_cast(1 + ((split_frame_padding_start_ + split_frame_len_) / 8) + +- (((split_frame_padding_start_ + split_frame_len_) % 8) ? 1 : 0)); +- +- auto padding_end = av_packet_->size * 8 - (8 + split_frame_padding_start_ + split_frame_len_); +- assert_true(padding_end < 8); +- xma_frame_[0] = ((split_frame_padding_start_ & 7) << 5) | ((padding_end & 7) << 2); ++void XmaContext::PreparePacket(uint32_t frame_size, uint32_t frame_padding) { ++ av_packet_->data = xma_frame_.data(); ++ av_packet_->size = static_cast(1 + ((frame_padding + frame_size) / 8) + ++ (((frame_padding + frame_size) % 8) ? 1 : 0)); + +- split_frame_len_ = 0; +- split_frame_len_partial_ = 0; +- split_frame_padding_start_ = 0; ++ auto padding_end = av_packet_->size * 8 - (8 + frame_padding + frame_size); ++ assert_true(padding_end < 8); ++ xma_frame_[0] = ((frame_padding & 7) << 5) | ((padding_end & 7) << 2); ++} + +- auto ret = avcodec_send_packet(av_context_, av_packet_); +- if (ret < 0) { +- REXAPU_ERROR("XmaContext {}: Error sending packet for decoding", id()); +- // TODO bail out +- assert_always(); +- } +- ret = avcodec_receive_frame(av_context_, av_frame_); +- /* +- if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) +- // TODO AVERROR_EOF??? +- break; +- else +- */ +- if (ret < 0) { +- REXAPU_ERROR("XmaContext {}: Error during decoding", id()); +- assert_always(); +- return; // TODO bail out +- } +- assert_true(ret == 0); +- +- { +- // copy over 1 frame +- // update input buffer read offset +- +- // assert(decoded_consumed_samples_ + kSamplesPerFrame <= +- // current_frame_.size()); +- assert_true(av_context_->sample_fmt == AV_SAMPLE_FMT_FLTP); +- // assert_true(frame_is_split == (frame_idx == -1)); +- +- // dump_raw(av_frame_, id()); +- ConvertFrame((const uint8_t**)av_frame_->data, bool(data->is_stereo), raw_frame_.data()); +- // decoded_consumed_samples_ += kSamplesPerFrame; +- +- auto byte_count = kBytesPerFrameChannel << data->is_stereo; +- assert_true(output_remaining_bytes >= byte_count); +- output_rb.Write(raw_frame_.data(), byte_count); +- output_remaining_bytes -= byte_count; +- data->output_buffer_write_offset = output_rb.write_offset() / 256; +- +- total_samples += id_ == 0 ? kSamplesPerFrame : 0; +- +- uint32_t offset = data->input_buffer_read_offset; +- // if (offset % (kBytesPerSample * 8) == 0) { +- // offset = xma::GetPacketFrameOffset(packet); +- //} +- offset = +- static_cast(GetNextFrame(current_input_buffer, current_input_size, offset)); +- // assert_true((offset == 0) == +- // (frame_is_split || (frame_idx + 1 >= frame_count))); +- if (frame_idx + 1 >= frame_count) { +- // Skip to next packet (no split frame) +- packets_skip_ = xma::GetPacketSkipCount(packet) + 1; +- while (packets_skip_ > 0) { +- packets_skip_--; +- packet_idx++; +- if (packet_idx >= current_input_packet_count) { +- if (!reuse_input_buffer) { +- // Last packet. Try setup once more. +- reuse_input_buffer = TrySetupNextLoop(data, true); +- } +- if (!reuse_input_buffer) { +- SwapInputBuffer(data); +- } +- return; +- } +- } +- packet = current_input_buffer + packet_idx * kBytesPerPacket; +- offset = xma::GetPacketFrameOffset(packet) + packet_idx * kBitsPerPacket; +- } +- if (offset == 0 || frame_idx == -1) { +- // Next packet but we already skipped to it +- if (packet_idx >= current_input_packet_count) { +- // Buffer is fully used +- if (!reuse_input_buffer) { +- // Last packet. Try setup once more. +- reuse_input_buffer = TrySetupNextLoop(data, true); +- } +- if (!reuse_input_buffer) { +- SwapInputBuffer(data); +- } +- break; +- } +- offset = xma::GetPacketFrameOffset(packet) + packet_idx * kBitsPerPacket; +- } +- // TODO buffer bounds check +- assert_true(data->input_buffer_read_offset < offset); +- data->input_buffer_read_offset = offset; +- } ++bool XmaContext::DecodePacket(AVCodecContext* av_context, const AVPacket* av_packet, ++ AVFrame* av_frame) { ++ auto ret = avcodec_send_packet(av_context, av_packet); ++ if (ret < 0) { ++ REXAPU_ERROR("XmaContext {}: Error sending packet for decoding ({})", id(), ret); ++ return false; + } ++ ret = avcodec_receive_frame(av_context, av_frame); + +- // assert_true((split_frame_len_ != 0) == (data->input_buffer_read_offset == +- // 0)); +- +- // The game will kick us again with a new output buffer later. +- // It's important that we only invalidate this if we actually wrote to it!! +- if (output_rb.write_offset() == output_rb.read_offset()) { +- data->output_buffer_valid = 0; ++ if (ret == AVERROR(EAGAIN)) { ++ return false; + } ++ if (ret < 0) { ++ REXAPU_ERROR("XmaContext {}: Error during decoding ({})", id(), ret); ++ return false; ++ } ++ return true; + } + +-size_t XmaContext::GetNextFrame(uint8_t* block, size_t size, size_t bit_offset) { +- // offset = xma::GetPacketFrameOffset(packet); +- // TODO meh +- // auto next_packet = bit_offset - bit_offset % kBitsPerPacket + +- // kBitsPerPacket; +- auto packet_idx = GetFramePacketNumber(block, size, bit_offset); +- +- BitStream stream(block, size * 8); +- stream.SetOffset(bit_offset); ++void XmaContext::Decode(XMA_CONTEXT_DATA* data) { ++ SCOPE_profile_cpu_f("apu"); + +- if (stream.BitsRemaining() < 15) { +- return 0; ++ if (!data->IsAnyInputBufferValid()) { ++ return; + } + +- uint64_t len = stream.Read(15); +- if ((len - 15) > stream.BitsRemaining()) { +- // assert_always("TODO"); +- // *bit_offset = next_packet; +- // return false; +- // return next_packet; +- return 0; +- } else if (len >= xma::kMaxFrameLength) { +- // assert_always("TODO"); +- // *bit_offset = next_packet; +- // return false; +- return 0; +- // return next_packet; ++ if (current_frame_remaining_subframes_ > 0) { ++ return; + } + +- stream.Advance(len - (15 + 1)); +- // Read the trailing bit to see if frames follow +- if (stream.Read(1) == 0) { +- return 0; ++ if (!data->IsCurrentInputBufferValid()) { ++ SwapInputBuffer(data); ++ if (!data->IsCurrentInputBufferValid()) { ++ return; ++ } + } + +- bit_offset += len; +- if (packet_idx < GetFramePacketNumber(block, size, bit_offset)) { +- return 0; ++ uint8_t* current_input_buffer = GetCurrentInputBuffer(data); ++ ++ input_buffer_.fill(0); ++ ++ // Detect loop end frame before UpdateLoopStatus resets the offset. ++ bool is_loop_end_frame = false; ++ if (data->loop_count > 0) { ++ const uint32_t loop_end = std::max(kBitsPerPacketHeader, data->loop_end); ++ is_loop_end_frame = (data->input_buffer_read_offset == loop_end); + } +- return bit_offset; +-} + +-int XmaContext::GetFramePacketNumber(uint8_t* block, size_t size, size_t bit_offset) { +- size *= 8; +- if (bit_offset >= size) { +- // Not good :( +- assert_always(); +- return -1; ++ UpdateLoopStatus(data); ++ ++ if (!data->output_buffer_block_count) { ++ REXAPU_ERROR("XmaContext {}: Error - Received 0 for output_buffer_block_count!", id()); ++ return; + } + +- size_t byte_offset = bit_offset >> 3; +- size_t packet_number = byte_offset / kBytesPerPacket; ++ if (data->input_buffer_read_offset < kBitsPerPacketHeader) { ++ data->input_buffer_read_offset = kBitsPerPacketHeader; ++ } + +- return (uint32_t)packet_number; +-} ++ const uint32_t current_input_size = GetCurrentInputBufferSize(data); ++ const uint32_t current_input_packet_count = current_input_size / kBytesPerPacket; + +-std::tuple XmaContext::GetFrameNumber(uint8_t* block, size_t size, size_t bit_offset) { +- auto packet_idx = GetFramePacketNumber(block, size, bit_offset); ++ const int16_t packet_index = GetPacketNumber(current_input_size, data->input_buffer_read_offset); + +- if (packet_idx < 0 || (packet_idx + 1) * kBytesPerPacket > size) { +- assert_always(); +- return {packet_idx, -2}; ++ if (packet_index == -1) { ++ REXAPU_ERROR("XmaContext {}: Invalid packet index. Input read offset: {}", id(), ++ static_cast(data->input_buffer_read_offset)); ++ return; + } + +- if (bit_offset == 0) { +- return {packet_idx, -1}; ++ uint8_t* packet = current_input_buffer + (packet_index * kBytesPerPacket); ++ const uint32_t packet_first_frame_offset = xma::GetPacketFrameOffset(packet); ++ uint32_t relative_offset = data->input_buffer_read_offset % kBitsPerPacket; ++ ++ if (relative_offset < packet_first_frame_offset) { ++ data->input_buffer_read_offset = (packet_index * kBitsPerPacket) + packet_first_frame_offset; ++ relative_offset = packet_first_frame_offset; + } + +- uint8_t* packet = block + (packet_idx * kBytesPerPacket); +- auto first_frame_offset = xma::GetPacketFrameOffset(packet); +- BitStream stream(block, size * 8); +- stream.SetOffset(packet_idx * kBitsPerPacket + first_frame_offset); ++ const uint8_t skip_count = xma::GetPacketSkipCount(packet); + +- int frame_idx = 0; +- while (true) { +- if (stream.BitsRemaining() < 15) { +- break; ++ // Full packet skip (0xFF) -- no new frames begin in this packet. ++ if (skip_count == 0xFF) { ++ uint32_t next_input_offset = ++ GetNextPacketReadOffset(current_input_buffer, packet_index + 1, current_input_packet_count); ++ if (next_input_offset == kBitsPerPacketHeader) { ++ SwapInputBuffer(data); + } ++ data->input_buffer_read_offset = next_input_offset; ++ return; ++ } + +- if (stream.offset_bits() == bit_offset) { +- break; +- } ++ kPacketInfo packet_info = GetPacketInfo(packet, relative_offset); ++ const uint32_t packet_to_skip = skip_count + 1; ++ const uint32_t next_packet_index = packet_index + packet_to_skip; + +- uint64_t size = stream.Read(15); +- if ((size - 15) > stream.BitsRemaining()) { +- // Last frame. +- break; +- } else if (size == 0x7FFF) { +- // Invalid frame (and last of this packet) +- break; ++ // Frame header split across packet boundary. ++ if (packet_info.current_frame_size_ == 0) { ++ const uint8_t* next_packet = GetNextPacket(data, next_packet_index, current_input_packet_count); ++ if (!next_packet) { ++ SwapInputBuffer(data); ++ return; + } ++ std::memcpy(input_buffer_.data(), packet + kBytesPerPacketHeader, kBytesPerPacketData); ++ std::memcpy(input_buffer_.data() + kBytesPerPacketData, next_packet + kBytesPerPacketHeader, ++ kBytesPerPacketData); + +- stream.Advance(size - (15 + 1)); ++ BitStream combined(input_buffer_.data(), (kBitsPerPacket - kBitsPerPacketHeader) * 2); ++ combined.SetOffset(relative_offset - kBitsPerPacketHeader); + +- // Read the trailing bit to see if frames follow +- if (stream.Read(1) == 0) { +- break; ++ uint64_t frame_size = combined.Peek(kBitsPerFrameHeader); ++ if (frame_size == xma::kMaxFrameLength) { ++ data->error_status = 4; ++ return; + } +- frame_idx++; ++ packet_info.current_frame_size_ = static_cast(frame_size); + } +- return {packet_idx, frame_idx}; +-} + +-std::tuple XmaContext::GetPacketFrameCount(uint8_t* packet) { +- auto first_frame_offset = xma::GetPacketFrameOffset(packet); +- if (first_frame_offset > kBitsPerPacket - kBitsPerHeader) { +- // frame offset is beyond packet end +- return {0, false}; +- } ++ BitStream stream(current_input_buffer, (packet_index + 1) * kBitsPerPacket); ++ stream.SetOffset(data->input_buffer_read_offset); + +- BitStream stream(packet, kBitsPerPacket); +- stream.SetOffset(first_frame_offset); +- int frame_count = 0; ++ const uint64_t bits_to_copy = GetAmountOfBitsToRead(static_cast(stream.BitsRemaining()), ++ packet_info.current_frame_size_); + +- while (true) { +- frame_count++; +- if (stream.BitsRemaining() < 15) { +- return {frame_count, true}; +- } ++ if (bits_to_copy == 0) { ++ REXAPU_ERROR("XmaContext {}: There are no bits to copy!", id()); ++ SwapInputBuffer(data); ++ return; ++ } + +- uint64_t size = stream.Read(15); +- if ((size - 15) > stream.BitsRemaining()) { +- return {frame_count, true}; +- } else if (size == 0x7FFF) { +- assert_always(); +- return {frame_count, true}; ++ if (packet_info.isLastFrameInPacket()) { ++ if (stream.BitsRemaining() < packet_info.current_frame_size_) { ++ const uint8_t* next_packet = ++ GetNextPacket(data, next_packet_index, current_input_packet_count); ++ if (!next_packet) { ++ data->error_status = 4; ++ return; ++ } ++ std::memcpy(input_buffer_.data() + kBytesPerPacketData, next_packet + kBytesPerPacketHeader, ++ kBytesPerPacketData); + } ++ } + +- stream.Advance(size - (15 + 1)); ++ std::memcpy(input_buffer_.data(), packet + kBytesPerPacketHeader, kBytesPerPacketData); + +- if (stream.Read(1) == 0) { +- return {frame_count, false}; ++ stream = BitStream(input_buffer_.data(), (kBitsPerPacket - kBitsPerPacketHeader) * 2); ++ stream.SetOffset(relative_offset - kBitsPerPacketHeader); ++ ++ xma_frame_.fill(0); ++ ++ const uint32_t padding_start = ++ static_cast(stream.Copy(xma_frame_.data() + 1, packet_info.current_frame_size_)); ++ ++ raw_frame_.fill(0); ++ ++ PrepareDecoder(data->sample_rate, bool(data->is_stereo)); ++ PreparePacket(packet_info.current_frame_size_, padding_start); ++ if (DecodePacket(av_context_, av_packet_, av_frame_)) { ++ ConvertFrame(reinterpret_cast(&av_frame_->data), bool(data->is_stereo), ++ raw_frame_.data()); ++ current_frame_remaining_subframes_ = 4 << data->is_stereo; ++ ++ // Loop end: limit output to subframes 0..loop_subframe_end. ++ if (is_loop_end_frame) { ++ loop_frame_output_limit_ = (data->loop_subframe_end + 1) << data->is_stereo; ++ } else { ++ loop_frame_output_limit_ = 0; + } +- } +-} + +-int XmaContext::PrepareDecoder(uint8_t* packet, int sample_rate, bool is_two_channel) { +- // Sanity check: Packet metadata is always 1 for XMA2/0 for XMA +- assert_true((packet[2] & 0x7) == 1 || (packet[2] & 0x7) == 0); ++ // Loop start: skip leading subframes per loop_subframe_skip. ++ if (loop_start_skip_pending_) { ++ const uint8_t skip = data->loop_subframe_skip << data->is_stereo; ++ if (skip < current_frame_remaining_subframes_) { ++ current_frame_remaining_subframes_ -= skip; ++ } ++ loop_start_skip_pending_ = false; ++ } ++ } + +- sample_rate = GetSampleRate(sample_rate); ++ // Compute where to go next. ++ if (!packet_info.isLastFrameInPacket()) { ++ const uint32_t next_frame_offset = ++ (data->input_buffer_read_offset + bits_to_copy) % kBitsPerPacket; ++ data->input_buffer_read_offset = (packet_index * kBitsPerPacket) + next_frame_offset; ++ return; ++ } + +- // Re-initialize the context with new sample rate and channels. +- uint32_t channels = is_two_channel ? 2 : 1; +- if (av_context_->sample_rate != sample_rate || av_context_->channels != channels) { +- // We have to reopen the codec so it'll realloc whatever data it needs. +- // TODO(DrChat): Find a better way. +- avcodec_close(av_context_); ++ uint32_t next_input_offset = ++ GetNextPacketReadOffset(current_input_buffer, next_packet_index, current_input_packet_count); + +- av_context_->sample_rate = sample_rate; +- av_context_->channels = channels; ++ if (next_input_offset == kBitsPerPacketHeader) { ++ SwapInputBuffer(data); ++ if (data->IsAnyInputBufferValid()) { ++ next_input_offset = xma::GetPacketFrameOffset( ++ memory()->TranslatePhysical(data->GetCurrentInputBufferAddress())); + +- if (avcodec_open2(av_context_, av_codec_, NULL) < 0) { +- REXAPU_ERROR("XmaContext: Failed to reopen FFmpeg context"); +- return -1; ++ if (next_input_offset > kMaxFrameSizeinBits) { ++ SwapInputBuffer(data); ++ return; ++ } + } +- return 1; + } +- return 0; ++ data->input_buffer_read_offset = next_input_offset; + } + + void XmaContext::ConvertFrame(const uint8_t** samples, bool is_two_channel, +-- +2.52.0.windows.1 + diff --git a/thirdparty/rexglue-sdk/patch3.patch b/thirdparty/rexglue-sdk/patch3.patch new file mode 100644 index 00000000..82bf06f3 --- /dev/null +++ b/thirdparty/rexglue-sdk/patch3.patch @@ -0,0 +1,156 @@ +From fba68dadef0d9026a795ba27f4d97841aa533009 Mon Sep 17 00:00:00 2001 +From: Ryan Fisher +Date: Sun, 22 Mar 2026 09:54:45 -0400 +Subject: [PATCH] fix(audio): SDL startup and export XMA constants + +- Export the XmaContext packet header and output size constants so Fable 2 can link against the XMA audio runtime correctly +- Harden SDL audio device startup by validating format detection, stereo fallback reopening, and device resume before playback begins +- Prevent SDL callback stalls when no audio frames are queued by feeding silence and checking stream writes instead of spinning indefinitely +--- + src/audio/sdl/sdl_audio_driver.cpp | 62 +++++++++++++++++++++++++----- + src/audio/xma_context.cpp | 3 ++ + 2 files changed, 56 insertions(+), 9 deletions(-) + +diff --git a/src/native/audio/sdl/sdl_audio_driver.cpp b/src/native/audio/sdl/sdl_audio_driver.cpp +index 932ceaaf..46719f3a 100644 +--- a/src/native/audio/sdl/sdl_audio_driver.cpp ++++ b/src/native/audio/sdl/sdl_audio_driver.cpp +@@ -9,6 +9,7 @@ + * @modified Tom Clay, 2026 - Adapted for ReXGlue runtime + */ + ++#include + #include + #include + +@@ -51,7 +52,7 @@ bool SDLAudioDriver::Initialize() { + sdl_initialized_ = true; + + SDL_AudioSpec desired_spec = {}; +- SDL_AudioSpec obtained_spec; ++ SDL_AudioSpec obtained_spec = {}; + desired_spec.freq = frame_frequency_; + desired_spec.format = SDL_AUDIO_F32LE; + desired_spec.channels = frame_channels_; +@@ -59,18 +60,43 @@ bool SDLAudioDriver::Initialize() { + sdl_stream_ = SDL_OpenAudioDeviceStream(SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK, &desired_spec, + SDLCallback, this); + if (!sdl_stream_) { +- REXAPU_ERROR("SDL_OpenAudioDevice() failed: {}", SDL_GetError()); ++ REXAPU_ERROR("SDL_OpenAudioDeviceStream() failed: {}", SDL_GetError()); + return false; + } +- SDL_GetAudioDeviceFormat(SDL_GetAudioStreamDevice(sdl_stream_), &obtained_spec, NULL); ++ ++ SDL_AudioDeviceID sdl_device = SDL_GetAudioStreamDevice(sdl_stream_); ++ if (!sdl_device) { ++ REXAPU_ERROR("SDL_GetAudioStreamDevice() failed: {}", SDL_GetError()); ++ return false; ++ } ++ ++ if (!SDL_GetAudioDeviceFormat(sdl_device, &obtained_spec, NULL)) { ++ REXAPU_WARN("SDL_GetAudioDeviceFormat() failed: {}", SDL_GetError()); ++ obtained_spec = desired_spec; ++ } ++ + if (obtained_spec.channels == 2) { + SDL_DestroyAudioStream(sdl_stream_); ++ sdl_stream_ = nullptr; + desired_spec.channels = 2; + sdl_device_channels_ = 2; + sdl_stream_ = SDL_OpenAudioDeviceStream(SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK, &desired_spec, + SDLCallback, this); ++ if (!sdl_stream_) { ++ REXAPU_ERROR("SDL_OpenAudioDeviceStream() stereo fallback failed: {}", SDL_GetError()); ++ return false; ++ } ++ sdl_device = SDL_GetAudioStreamDevice(sdl_stream_); ++ if (!sdl_device) { ++ REXAPU_ERROR("SDL_GetAudioStreamDevice() failed after stereo fallback: {}", SDL_GetError()); ++ return false; ++ } ++ } ++ ++ if (!SDL_ResumeAudioDevice(sdl_device)) { ++ REXAPU_ERROR("SDL_ResumeAudioDevice() failed: {}", SDL_GetError()); ++ return false; + } +- SDL_ResumeAudioDevice(SDL_GetAudioStreamDevice(sdl_stream_)); + + return true; + } +@@ -125,15 +151,21 @@ void SDLAudioDriver::Shutdown() { + } + + void SDLAudioDriver::SDLCallback(void* userdata, SDL_AudioStream* stream, int additional_amount, +- int total_amount) { ++ [[maybe_unused]] int total_amount) { + SCOPE_profile_cpu_f("apu"); + if (!userdata || !stream) { + REXAPU_ERROR("SDLAudioDriver::SDLCallback called with nullptr."); + return; + } + const auto driver = static_cast(userdata); +- const int len = static_cast(sizeof(float) * channel_samples_ * driver->sdl_device_channels_); +- float* data = SDL_stack_alloc(float, len); ++ const int sample_count = ++ static_cast(channel_samples_ * std::max(driver->sdl_device_channels_, 1)); ++ const int len = static_cast(sizeof(float) * sample_count); ++ float* data = SDL_stack_alloc(float, sample_count); ++ if (!data) { ++ REXAPU_ERROR("SDLAudioDriver::SDLCallback failed to allocate {} samples", sample_count); ++ return; ++ } + while (additional_amount > 0) { + static uint32_t sdl_callback_count = 0; + std::unique_lock guard(driver->frames_mutex_); +@@ -142,10 +174,18 @@ void SDLAudioDriver::SDLCallback(void* userdata, SDL_AudioStream* stream, int ad + REXAPU_DEBUG("SDLCallback: no frames queued (silence)"); + sdl_callback_count++; + } ++ std::memset(data, 0, len); ++ if (!SDL_PutAudioStreamData(stream, data, len)) { ++ REXAPU_ERROR("SDL_PutAudioStreamData() failed while filling silence: {}", SDL_GetError()); ++ break; ++ } ++ additional_amount -= len; + } else { + auto buffer = driver->frames_queued_.front(); + driver->frames_queued_.pop(); +- if (!REXCVAR_GET(audio_mute)) { ++ if (REXCVAR_GET(audio_mute)) { ++ std::memset(data, 0, len); ++ } else { + switch (driver->sdl_device_channels_) { + case 2: + conversion::sequential_6_BE_to_interleaved_2_LE(data, buffer, channel_samples_); +@@ -157,7 +197,11 @@ void SDLAudioDriver::SDLCallback(void* userdata, SDL_AudioStream* stream, int ad + assert_unhandled_case(driver->sdl_device_channels_); + break; + } +- SDL_PutAudioStreamData(stream, data, len); ++ } ++ if (!SDL_PutAudioStreamData(stream, data, len)) { ++ REXAPU_ERROR("SDL_PutAudioStreamData() failed: {}", SDL_GetError()); ++ driver->frames_unused_.push(buffer); ++ break; + } + driver->frames_unused_.push(buffer); + +diff --git a/src/native/audio/xma/context.cpp b/src/native/audio/xma/context.cpp +index 9b4fb07d..eafb4105 100644 +--- a/src/native/audio/xma/context.cpp ++++ b/src/native/audio/xma/context.cpp +@@ -40,6 +40,9 @@ namespace rex::audio { + + using stream::BitStream; + ++const uint32_t XmaContext::kBitsPerPacketHeader; ++const uint32_t XmaContext::kOutputMaxSizeBytes; ++ + XmaContext::XmaContext() + : work_completion_event_(rex::thread::Event::CreateAutoResetEvent(false)) {} + +-- +2.52.0.windows.1 + diff --git a/thirdparty/rexglue-sdk/src/kernel/xboxkrnl/xboxkrnl_audio.cpp b/thirdparty/rexglue-sdk/src/kernel/xboxkrnl/xboxkrnl_audio.cpp index ad730784..f20c8a19 100644 --- a/thirdparty/rexglue-sdk/src/kernel/xboxkrnl/xboxkrnl_audio.cpp +++ b/thirdparty/rexglue-sdk/src/kernel/xboxkrnl/xboxkrnl_audio.cpp @@ -185,13 +185,14 @@ ppc_u32_result_t XAudioGetRenderDriverTic_entry(ppc_pvoid_t driver_ptr) { REXKRNL_DEBUG( "XAudioGetRenderDriverTic driver={:08X} guest_tic=0 internal_tic={} " "submitted={} consumed={} queued_depth={} underruns={} callbacks={} " - "callback_throttles={} clock_samples={} clock_frames={} submitted_tic={} " + "callback_throttles={} clock_samples={} clock_frames={} queued_played={} submitted_tic={} " "synthetic_tic={} startup_cap_tic={} startup_inflight={} callback_floor_tic={} " "host_elapsed_tic={}", driver_handle, internal_tic, telemetry.submitted_frames, telemetry.consumed_frames, telemetry.queued_depth, telemetry.underrun_count, telemetry.callback_dispatch_count, telemetry.callback_throttle_count, - timing.consumed_samples, timing.consumed_frames, timing.submitted_tic, + timing.consumed_samples, timing.consumed_frames, timing.queued_played_frames, + timing.submitted_tic, timing.synthetic_startup_tic, timing.startup_cap_tic, timing.startup_inflight_frames, timing.callback_floor_tic, timing.host_elapsed_tic); diff --git a/thirdparty/rexglue-sdk/src/native/audio/CMakeLists.txt b/thirdparty/rexglue-sdk/src/native/audio/CMakeLists.txt index 3b0926de..ccd1a6cf 100644 --- a/thirdparty/rexglue-sdk/src/native/audio/CMakeLists.txt +++ b/thirdparty/rexglue-sdk/src/native/audio/CMakeLists.txt @@ -3,6 +3,7 @@ add_library(rexaudio STATIC audio_driver.cpp audio_clock.cpp + render_driver_frame_layout.cpp audio_runtime.cpp audio_system.cpp audio_trace.cpp @@ -16,6 +17,7 @@ add_library(rex::audio ALIAS rexaudio) if(WIN32) target_sources(rexaudio PRIVATE wasapi/wasapi_audio_driver.cpp + sdl/sdl_audio_driver.cpp ) else() target_sources(rexaudio PRIVATE @@ -33,7 +35,7 @@ target_link_libraries(rexaudio ) if(WIN32) - target_link_libraries(rexaudio PUBLIC ole32 avrt uuid) + target_link_libraries(rexaudio PUBLIC ole32 avrt uuid SDL3::SDL3) else() target_link_libraries(rexaudio PUBLIC SDL3::SDL3) endif() diff --git a/thirdparty/rexglue-sdk/src/native/audio/audio_runtime.cpp b/thirdparty/rexglue-sdk/src/native/audio/audio_runtime.cpp index ee210a46..74b3fd17 100644 --- a/thirdparty/rexglue-sdk/src/native/audio/audio_runtime.cpp +++ b/thirdparty/rexglue-sdk/src/native/audio/audio_runtime.cpp @@ -12,10 +12,9 @@ #include +#include #if defined(_WIN32) #include -#else -#include #endif #include #include @@ -24,7 +23,7 @@ #if defined(_WIN32) REXCVAR_DEFINE_STRING(audio_backend, "wasapi", "Audio", "Audio backend: wasapi") - .allowed({"wasapi"}) + .allowed({"wasapi", "sdl"}) #else REXCVAR_DEFINE_STRING(audio_backend, "sdl", "Audio", "Audio backend: sdl") .allowed({"sdl"}) @@ -51,6 +50,9 @@ namespace { std::unique_ptr CreateConfiguredDriver(memory::Memory* memory, AudioRuntime* runtime, const size_t client_index) { #if defined(_WIN32) + if (REXCVAR_GET(audio_backend) == "sdl") { + return std::make_unique(memory, runtime, client_index); + } return std::make_unique(memory, runtime, client_index); #else return std::make_unique(memory, runtime, client_index); @@ -136,8 +138,8 @@ AudioDriverTelemetry MergeDriverTelemetry(const AudioClientState& client) { return merged; } -bool StartupConsumptionObserved(const AudioClientState& client) { - return client.clock.consumed_samples() != 0; +bool QueuedPlaybackObserved(const AudioClientState& client) { + return client.queued_played_frames != 0; } uint32_t StartupInflightFrames(const AudioClientState& client) { @@ -179,7 +181,10 @@ bool ShouldThrottleStartupCallback(const AudioClientState& client, *out_spacing_remaining_ms = 0; } - if (StartupConsumptionObserved(client)) { + // Keep startup serialization active until the host has drained at least one + // queued render-driver frame. Host-injected underrun silence should advance + // tic timing, but it must not release callback pacing on its own. + if (QueuedPlaybackObserved(client)) { return false; } @@ -244,6 +249,7 @@ AudioClientTimingSnapshot BuildTimingSnapshot(const AudioClientState& client) { AudioClientTimingSnapshot snapshot; snapshot.consumed_samples = client.clock.consumed_samples(); snapshot.consumed_frames = client.clock.consumed_frames(); + snapshot.queued_played_frames = client.queued_played_frames; snapshot.submitted_tic = SubmittedTicSamples(client); snapshot.startup_cap_tic = StartupTicCapSamples(client); snapshot.synthetic_startup_tic = ComputeStartupSyntheticTic(client); @@ -562,6 +568,7 @@ bool AudioRuntime::ConsumeNextFrameForClient(const size_t index, AudioFrame* out AudioFrame frame = client.queued_frames.front(); client.queued_frames.pop_front(); + ++client.queued_played_frames; client.telemetry.queued_depth = static_cast(client.queued_frames.size()); trace_buffer_.Record(AudioTraceSubsystem::kCore, AudioTraceEventType::kFrameConsumed, static_cast(index), frame.guest_submit_ptr, @@ -765,6 +772,7 @@ size_t AudioRuntime::ConsumeQueuedFramesForClient(const size_t index, const size while (consumed < max_frames && !client.queued_frames.empty()) { const AudioFrame frame = client.queued_frames.front(); client.queued_frames.pop_front(); + ++client.queued_played_frames; client.telemetry.last_consume_ticks = static_cast(NextTickLocked()); client.telemetry.queued_depth = static_cast(client.queued_frames.size()); client.telemetry.consumed_frames = @@ -823,9 +831,10 @@ void AudioRuntime::WorkerThreadMain() { // Check each active client and dispatch callbacks to fill queue to target for (size_t i = 0; i < clients_.size(); ++i) { - // Before the first real playback consumption, keep callback lead tightly - // serialized so guest-side movie/cutscene workers cannot sprint ahead of - // the render driver on timeout wakes alone. + // Before the host drains the first queued render-driver frame, keep + // callback lead tightly serialized so guest-side movie/cutscene workers + // cannot sprint ahead on timeout wakes or underrun-silence progress + // alone. uint32_t dispatch = 0; while (true) { uint32_t client_callback = 0; @@ -846,7 +855,7 @@ void AudioRuntime::WorkerThreadMain() { break; } target = EffectiveCallbackTargetQueueDepth(clients_[i]); - startup_mode = clients_[i].telemetry.callback_dispatch_count == 0; + startup_mode = !QueuedPlaybackObserved(clients_[i]); callback_limit = startup_mode ? 1u : target; if (dispatch >= callback_limit) { break; @@ -981,13 +990,13 @@ void AudioRuntime::WorkerThreadMain() { const auto& c = clients_[i]; REXAPU_INFO( "AudioRuntime startup: iter={} client={} queued={} target={} low_water={} " - "submitted={} consumed={} underruns={} callbacks={} tic={} synthetic_tic={} " + "submitted={} consumed={} queued_played={} underruns={} callbacks={} tic={} synthetic_tic={} " "submitted_tic={} startup_cap_tic={} startup_inflight={} callback_throttles={} " "callback_empty={} last_callback_frames={} last_callback_us={} peak={} drift_ms={:.1f}", worker_iteration_count_, i, c.queued_frames.size(), EffectiveCallbackTargetQueueDepth(c), EffectiveCallbackLowWaterFrames(c), - c.telemetry.submitted_frames, c.telemetry.consumed_frames, c.telemetry.underrun_count, - c.telemetry.callback_dispatch_count, ComputeRenderDriverTic(c), + c.telemetry.submitted_frames, c.telemetry.consumed_frames, c.queued_played_frames, + c.telemetry.underrun_count, c.telemetry.callback_dispatch_count, ComputeRenderDriverTic(c), ComputeStartupSyntheticTic(c), SubmittedTicSamples(c), StartupTicCapSamples(c), StartupInflightFrames(c), c.telemetry.callback_throttle_count, c.telemetry.callback_empty_count, c.telemetry.last_callback_produced_frames, @@ -1005,12 +1014,12 @@ void AudioRuntime::WorkerThreadMain() { const auto& c = clients_[i]; REXAPU_DEBUG( "AudioRuntime periodic: client={} queued={} target={} low_water={} submitted={} " - "consumed={} underruns={} callbacks={} tic={} synthetic_tic={} submitted_tic={} " + "consumed={} queued_played={} underruns={} callbacks={} tic={} synthetic_tic={} submitted_tic={} " "startup_cap_tic={} startup_inflight={} callback_throttles={} callback_empty={} " "last_callback_frames={} last_callback_us={} peak={} drift_ms={:.1f}", i, c.queued_frames.size(), EffectiveCallbackTargetQueueDepth(c), EffectiveCallbackLowWaterFrames(c), c.telemetry.submitted_frames, - c.telemetry.consumed_frames, c.telemetry.underrun_count, + c.telemetry.consumed_frames, c.queued_played_frames, c.telemetry.underrun_count, c.telemetry.callback_dispatch_count, ComputeRenderDriverTic(c), ComputeStartupSyntheticTic(c), SubmittedTicSamples(c), StartupTicCapSamples(c), StartupInflightFrames(c), c.telemetry.callback_throttle_count, diff --git a/thirdparty/rexglue-sdk/src/native/audio/render_driver_frame_layout.cpp b/thirdparty/rexglue-sdk/src/native/audio/render_driver_frame_layout.cpp new file mode 100644 index 00000000..6c3fabe3 --- /dev/null +++ b/thirdparty/rexglue-sdk/src/native/audio/render_driver_frame_layout.cpp @@ -0,0 +1,138 @@ +// Native audio runtime +// Part of the AC6 Recompilation project + +#include + +#include +#include +#include + +#include +#include +#include +#include + +REXCVAR_DEFINE_STRING(audio_render_driver_layout, "auto", "Audio", + "Layout for XAudio render-driver frames: auto, planar, or interleaved") + .allowed({"auto", "planar", "interleaved"}) + .lifecycle(rex::cvar::Lifecycle::kRequiresRestart); + +namespace rex::audio::conversion { +namespace { + +std::atomic g_detected_layout{0}; + +float DecodeSanitizedSample(const float* input, const size_t ch_sample_count, const size_t sample, + const size_t channel, const RenderDriverFrameLayout layout) { + const size_t index = layout == RenderDriverFrameLayout::kInterleaved + ? (sample * 6) + channel + : (channel * ch_sample_count) + sample; + float value = rex::byte_swap(input[index]); + if (!std::isfinite(value)) { + return 0.0f; + } + return rex::clamp_float(value, -1.0f, 1.0f); +} + +struct LayoutScore { + double continuity = 0.0; + double energy = 0.0; +}; + +struct LayoutDetectionResult { + RenderDriverFrameLayout layout = RenderDriverFrameLayout::kPlanar; + bool cacheable = false; +}; + +LayoutScore ScoreLayout(const float* input, const size_t ch_sample_count, + const RenderDriverFrameLayout layout) { + const size_t inspect_samples = std::min(ch_sample_count, 64); + LayoutScore score{}; + if (!input || inspect_samples < 2) { + return score; + } + + for (size_t channel = 0; channel < 6; ++channel) { + float previous = DecodeSanitizedSample(input, ch_sample_count, 0, channel, layout); + score.energy += std::fabs(previous); + for (size_t sample = 1; sample < inspect_samples; ++sample) { + const float current = + DecodeSanitizedSample(input, ch_sample_count, sample, channel, layout); + score.continuity += std::fabs(current - previous); + score.energy += std::fabs(current); + previous = current; + } + } + return score; +} + +LayoutDetectionResult DetectFrameLayout(const float* input, const size_t ch_sample_count) { + const LayoutScore planar = ScoreLayout(input, ch_sample_count, RenderDriverFrameLayout::kPlanar); + const LayoutScore interleaved = + ScoreLayout(input, ch_sample_count, RenderDriverFrameLayout::kInterleaved); + + if (std::max(planar.energy, interleaved.energy) < 0.01) { + return {}; + } + + constexpr double kDecisionRatio = 0.75; + if (planar.continuity * kDecisionRatio < interleaved.continuity) { + return {RenderDriverFrameLayout::kPlanar, true}; + } + if (interleaved.continuity * kDecisionRatio < planar.continuity) { + return {RenderDriverFrameLayout::kInterleaved, true}; + } + + return {planar.continuity <= interleaved.continuity ? RenderDriverFrameLayout::kPlanar + : RenderDriverFrameLayout::kInterleaved, + false}; +} + +} // namespace + +RenderDriverFrameLayout ResolveRenderDriverFrameLayout(const float* input, + const size_t ch_sample_count) { + const auto& configured_layout = REXCVAR_GET(audio_render_driver_layout); + if (configured_layout == "planar") { + return RenderDriverFrameLayout::kPlanar; + } + if (configured_layout == "interleaved") { + return RenderDriverFrameLayout::kInterleaved; + } + + const int cached_layout = g_detected_layout.load(std::memory_order_acquire); + if (cached_layout == 1) { + return RenderDriverFrameLayout::kPlanar; + } + if (cached_layout == 2) { + return RenderDriverFrameLayout::kInterleaved; + } + + const LayoutDetectionResult detection = DetectFrameLayout(input, ch_sample_count); + if (!detection.cacheable) { + return detection.layout; + } + + const RenderDriverFrameLayout detected_layout = detection.layout; + const int detected_value = + detected_layout == RenderDriverFrameLayout::kInterleaved ? 2 : 1; + const int previous = + g_detected_layout.exchange(detected_value, std::memory_order_acq_rel); + if (previous == 0) { + REXAPU_INFO("Audio render-driver layout auto-detected: {}", ToString(detected_layout)); + } + return detected_layout; +} + +const char* ToString(const RenderDriverFrameLayout layout) { + switch (layout) { + case RenderDriverFrameLayout::kPlanar: + return "planar"; + case RenderDriverFrameLayout::kInterleaved: + return "interleaved"; + default: + return "unknown"; + } +} + +} // namespace rex::audio::conversion diff --git a/thirdparty/rexglue-sdk/src/native/audio/sdl/sdl_audio_driver.cpp b/thirdparty/rexglue-sdk/src/native/audio/sdl/sdl_audio_driver.cpp index d31bc0ef..ee8c4f2e 100644 --- a/thirdparty/rexglue-sdk/src/native/audio/sdl/sdl_audio_driver.cpp +++ b/thirdparty/rexglue-sdk/src/native/audio/sdl/sdl_audio_driver.cpp @@ -226,7 +226,7 @@ void SdlAudioDriver::FillStream(SDL_AudioStream* stream, int bytes_needed) { if (buffer) { if (!REXCVAR_GET(audio_mute)) { - conversion::sequential_6_BE_to_interleaved_2_LE( + conversion::render_driver_6_BE_to_interleaved_2_LE( pending_output_frame_.data(), buffer, kRenderDriverTicSamplesPerFrame); } else { std::memset(pending_output_frame_.data(), 0, sizeof(float) * pending_output_frame_.size()); @@ -247,6 +247,12 @@ void SdlAudioDriver::FillStream(SDL_AudioStream* stream, int bytes_needed) { ++underrun_count_; ++silence_injections_; bytes_needed -= std::min(bytes_needed, kOutputFrameBytes); + + consumed_frames_.fetch_add(1, std::memory_order_relaxed); + if (runtime_) { + runtime_->ReportSamplesConsumedForClient(client_index_, kRenderDriverTicSamplesPerFrame); + runtime_->WakeWorker(); + } continue; } diff --git a/thirdparty/rexglue-sdk/src/native/audio/wasapi/wasapi_audio_driver.cpp b/thirdparty/rexglue-sdk/src/native/audio/wasapi/wasapi_audio_driver.cpp index 162000f8..85ee46e9 100644 --- a/thirdparty/rexglue-sdk/src/native/audio/wasapi/wasapi_audio_driver.cpp +++ b/thirdparty/rexglue-sdk/src/native/audio/wasapi/wasapi_audio_driver.cpp @@ -436,7 +436,7 @@ void WasapiAudioDriver::RenderThreadMain() { if (buffer) { if (!REXCVAR_GET(audio_mute)) { - conversion::sequential_6_BE_to_interleaved_2_LE( + conversion::render_driver_6_BE_to_interleaved_2_LE( pending_output_frame_.data(), buffer, kRenderDriverTicSamplesPerFrame); } else { std::memset(pending_output_frame_.data(), 0, sizeof(float) * pending_output_frame_.size()); @@ -464,6 +464,10 @@ void WasapiAudioDriver::RenderThreadMain() { ++underrun_count_; ++silence_injections_; std::memset(target, 0, sizeof(float) * 2 * frames_to_write); + if (runtime_) { + runtime_->ReportSamplesConsumedForClient(client_index_, frames_to_write); + runtime_->WakeWorker(); + } } else { const size_t float_count = static_cast(frames_to_write) * 2; std::memcpy(target, pending_output_frame_.data() + pending_output_float_offset_, diff --git a/thirdparty/rexglue-sdk/src/native/audio/xma/context.cpp b/thirdparty/rexglue-sdk/src/native/audio/xma/context.cpp index 0dd29102..73994824 100644 --- a/thirdparty/rexglue-sdk/src/native/audio/xma/context.cpp +++ b/thirdparty/rexglue-sdk/src/native/audio/xma/context.cpp @@ -697,8 +697,29 @@ void XmaContext::Decode(XMA_CONTEXT_DATA* data) { return; } + auto skip_corrupt_packet = [&](const char* reason) { + data->error_status = 4; + last_error_status_ = static_cast(data->error_status); + + const uint32_t next_packet_index = packet_index + 1; + const bool next_packet_in_next_buffer = next_packet_index >= current_input_packet_count; + uint32_t next_input_offset = + GetNextPacketReadOffset(data, next_packet_index, current_input_packet_count); + if (next_packet_in_next_buffer || next_input_offset == kBitsPerPacketHeader) { + last_swapped_input_buffer_ = true; + SwapInputBuffer(data); + } + data->input_buffer_read_offset = next_input_offset; + last_input_read_offset_after_ = static_cast(data->input_buffer_read_offset); + log_decode_state(reason); + }; + uint8_t* packet = current_input_buffer + (packet_index * kBytesPerPacket); const uint32_t packet_first_frame_offset = xma::GetPacketFrameOffset(packet); + if (packet_first_frame_offset > kMaxFrameSizeinBits) { + skip_corrupt_packet("packet-frame-offset-invalid"); + return; + } uint32_t relative_offset = data->input_buffer_read_offset % kBitsPerPacket; if (relative_offset < packet_first_frame_offset) { @@ -761,6 +782,15 @@ void XmaContext::Decode(XMA_CONTEXT_DATA* data) { } last_frame_size_bits_ = packet_info.current_frame_size_; + const uint32_t combined_payload_bits = (kBitsPerPacket - kBitsPerPacketHeader) * 2; + const uint32_t combined_relative_offset = relative_offset - kBitsPerPacketHeader; + if (packet_info.current_frame_size_ == 0 || + combined_relative_offset > combined_payload_bits || + packet_info.current_frame_size_ > (combined_payload_bits - combined_relative_offset)) { + skip_corrupt_packet("frame-size-out-of-range"); + return; + } + BitStream stream(current_input_buffer, (packet_index + 1) * kBitsPerPacket); stream.SetOffset(data->input_buffer_read_offset);