mirror of
https://github.com/sal063/AC6_recomp
synced 2026-08-16 13:26:43 -04:00
Add setup_and_build.bat script
This commit is contained in:
@@ -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
|
||||
@@ -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<ac6::d3d::SamplerBinding, ac6::d3d::kMaxSamplers>& 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");
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -69,6 +69,27 @@ void NativeGraphicsStatusDialog::OnDraw(ImGuiIO& io) {
|
||||
ImGui::Text("signature: %016llX hits=%u",
|
||||
static_cast<unsigned long long>(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<unsigned long long>(diagnostics.audio_consumed_frames),
|
||||
static_cast<unsigned long long>(diagnostics.audio_submitted_tic),
|
||||
static_cast<unsigned long long>(diagnostics.audio_queued_played_frames),
|
||||
static_cast<unsigned long long>(diagnostics.audio_submitted_tic));
|
||||
ImGui::Text("audio host tic: %llu",
|
||||
static_cast<unsigned long long>(diagnostics.audio_host_elapsed_tic));
|
||||
ImGui::Text("audio startup inflight / callback dispatch / throttle: %u / %u / %u",
|
||||
diagnostics.audio_startup_inflight_frames,
|
||||
|
||||
@@ -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};
|
||||
|
||||
@@ -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};
|
||||
|
||||
+193
-16
@@ -3,13 +3,45 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
|
||||
#include <native/audio/render_driver_frame_layout.h>
|
||||
#include <rex/platform.h>
|
||||
#include <rex/types.h>
|
||||
|
||||
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
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
// Native audio runtime
|
||||
// Part of the AC6 Recompilation native foundation
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <cstddef>
|
||||
|
||||
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
|
||||
Vendored
+376
@@ -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<XmaDecoder> xma_decoder_;
|
||||
+ uint32_t queued_frames_;
|
||||
|
||||
std::atomic<bool> worker_running_ = {false};
|
||||
system::object_ref<system::XHostThread> 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 <rex/kernel.h>
|
||||
#include <rex/memory.h>
|
||||
+#include <rex/thread.h>
|
||||
|
||||
// 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<rex::thread::Event> 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 <rex/thread.h>
|
||||
#include <rex/cvar.h>
|
||||
|
||||
-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<uint32_t>(kMaximumQueuedFrames),
|
||||
+ std::max(static_cast<uint32_t>(REXCVAR_GET(audio_maxqframes)), static_cast<uint32_t>(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<std::mutex> 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<int>(sizeof(float) * channel_samples_ * driver->sdl_device_channels_));
|
||||
|
||||
+ static uint32_t sdl_callback_count = 0;
|
||||
std::unique_lock<std::mutex> 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<audio::AudioSystem*>(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<audio::AudioSystem*>(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<audio::AudioSystem*>(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
|
||||
|
||||
Vendored
+1449
File diff suppressed because it is too large
Load Diff
Vendored
+156
@@ -0,0 +1,156 @@
|
||||
From fba68dadef0d9026a795ba27f4d97841aa533009 Mon Sep 17 00:00:00 2001
|
||||
From: Ryan Fisher <ryanfisher099@gmail.com>
|
||||
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 <algorithm>
|
||||
#include <array>
|
||||
#include <cstring>
|
||||
|
||||
@@ -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<SDLAudioDriver*>(userdata);
|
||||
- const int len = static_cast<int>(sizeof(float) * channel_samples_ * driver->sdl_device_channels_);
|
||||
- float* data = SDL_stack_alloc(float, len);
|
||||
+ const int sample_count =
|
||||
+ static_cast<int>(channel_samples_ * std::max<uint8_t>(driver->sdl_device_channels_, 1));
|
||||
+ const int len = static_cast<int>(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<std::mutex> 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
|
||||
|
||||
@@ -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);
|
||||
|
||||
+3
-1
@@ -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()
|
||||
|
||||
+24
-15
@@ -12,10 +12,9 @@
|
||||
|
||||
#include <native/audio/audio_runtime.h>
|
||||
|
||||
#include <native/audio/sdl/sdl_audio_driver.h>
|
||||
#if defined(_WIN32)
|
||||
#include <native/audio/wasapi/wasapi_audio_driver.h>
|
||||
#else
|
||||
#include <native/audio/sdl/sdl_audio_driver.h>
|
||||
#endif
|
||||
#include <rex/cvar.h>
|
||||
#include <rex/memory.h>
|
||||
@@ -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<AudioDriver> CreateConfiguredDriver(memory::Memory* memory, AudioRuntime* runtime,
|
||||
const size_t client_index) {
|
||||
#if defined(_WIN32)
|
||||
if (REXCVAR_GET(audio_backend) == "sdl") {
|
||||
return std::make_unique<sdl::SdlAudioDriver>(memory, runtime, client_index);
|
||||
}
|
||||
return std::make_unique<wasapi::WasapiAudioDriver>(memory, runtime, client_index);
|
||||
#else
|
||||
return std::make_unique<sdl::SdlAudioDriver>(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<uint32_t>(client.queued_frames.size());
|
||||
trace_buffer_.Record(AudioTraceSubsystem::kCore, AudioTraceEventType::kFrameConsumed,
|
||||
static_cast<uint32_t>(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<uint64_t>(NextTickLocked());
|
||||
client.telemetry.queued_depth = static_cast<uint32_t>(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,
|
||||
|
||||
@@ -0,0 +1,138 @@
|
||||
// Native audio runtime
|
||||
// Part of the AC6 Recompilation project
|
||||
|
||||
#include <native/audio/render_driver_frame_layout.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <atomic>
|
||||
#include <cmath>
|
||||
|
||||
#include <native/math.h>
|
||||
#include <rex/cvar.h>
|
||||
#include <rex/logging.h>
|
||||
#include <rex/types.h>
|
||||
|
||||
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<int> 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<size_t>(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
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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<size_t>(frames_to_write) * 2;
|
||||
std::memcpy(target, pending_output_frame_.data() + pending_output_float_offset_,
|
||||
|
||||
@@ -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<uint32_t>(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<uint32_t>(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);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user