mirror of
https://github.com/sal063/AC6_recomp
synced 2026-08-21 23:00:53 -04:00
Merge pull request #33 from Dipshet/fix-cutscene
Cutscene fixes: DoF at render scale, A/V resync after hitches, doubled dialogue
This commit is contained in:
@@ -40,6 +40,7 @@ set(AC6RECOMP_SOURCES
|
||||
src/ac6_backend_fixes/ac6_backend_capture_bridge.cpp
|
||||
src/ac6_backend_fixes/ac6_backend_hooks.cpp
|
||||
src/ac6_backend_fixes/ac6_backend_pass_classifier.cpp
|
||||
src/ac6_backend_fixes/ac6_cutscene_resync.cpp
|
||||
src/ac6_backend_fixes/ac6_fps_physics_fix.cpp
|
||||
src/ac6_backend_fixes/ac6_kbm_input.cpp
|
||||
src/ac6_backend_fixes/ac6_widescreen.cpp
|
||||
|
||||
@@ -0,0 +1,194 @@
|
||||
// Cutscene A/V resync (audio-master catch-up).
|
||||
//
|
||||
// The mechanism (confirmed in code and on real hardware): guest audio time =
|
||||
// XAudioGetRenderDriverTic = host samples consumed INCLUDING injected
|
||||
// underrun silence, so it tracks wall clock through any render hitch; the
|
||||
// audio worker is its own thread, so a render stall does not even pause real
|
||||
// audio. The in-engine cutscene sequencers (CAce6DemoManager::Exec "DD"
|
||||
// 0x82184460 / CX360DemoManagerEM::Exec "EM" 0x821856F8) tick their timeline
|
||||
// once per rendered frame. One long frame puts the video permanently behind
|
||||
// the audio; sustained sub-30fps render turns the whole cutscene into slow
|
||||
// motion against its soundtrack.
|
||||
//
|
||||
// This file wraps both Exec functions (weak symbols in the generated code,
|
||||
// same override pattern as ac6_fps_physics_fix.cpp). Audio is the master
|
||||
// clock and is NEVER touched - the cutscene catches up instead. Per rendered
|
||||
// frame, deficit = ticks the audio clock says should have run minus ticks
|
||||
// issued; while the deficit is >= 2 ticks, extra Exec calls run (capped per
|
||||
// frame by ac6_cutscene_resync_max_ticks) - a bounded timeline frame-skip,
|
||||
// the same audio-master pattern film players use. The >= 2 engage threshold
|
||||
// keeps normal 30fps playback provably untouched (steady-state phase jitter
|
||||
// is +/-1 tick and can never trigger it). The cap is also the slowest
|
||||
// sustained render rate that stays synced: N extras per frame holds sync
|
||||
// down to 30/(1+N) fps. 0 = uncapped (one hard jump-cut after a stall).
|
||||
//
|
||||
// Both Exec wrappers run on the guest game thread (the sole caller), so
|
||||
// plain statics are safe throughout.
|
||||
|
||||
#include <algorithm>
|
||||
#include <chrono>
|
||||
#include <cstdint>
|
||||
|
||||
#include <native/audio/audio_client.h>
|
||||
#include <native/audio/audio_system.h>
|
||||
#include <native/audio/conversion.h>
|
||||
#include <rex/cvar.h>
|
||||
#include <rex/logging.h>
|
||||
#include <rex/ppc.h>
|
||||
#include <rex/system/kernel_state.h>
|
||||
|
||||
REXCVAR_DEFINE_BOOL(ac6_cutscene_resync, true, "AC6",
|
||||
"Keep in-engine cutscene video locked to its audio. Audio "
|
||||
"is the master clock and is never altered; after a render "
|
||||
"hitch (or under sustained slow rendering) the cutscene "
|
||||
"timeline catches up by running extra sequencer ticks - a "
|
||||
"bounded frame-skip, like every film player's audio-master "
|
||||
"sync. No effect on normal full-speed playback (catch-up "
|
||||
"engages only past 2 ticks of drift; verified extras=0 in "
|
||||
"steady state, injected-hitch recovery in 3 frames, and "
|
||||
"sustained 14fps @ 5x draw scale staying locked).");
|
||||
REXCVAR_DEFINE_INT32(ac6_cutscene_resync_max_ticks, 3, "AC6",
|
||||
"Max extra cutscene sequencer ticks per rendered frame "
|
||||
"while catching up (ac6_cutscene_resync). Also sets the "
|
||||
"slowest sustained render rate that stays in sync: N "
|
||||
"extras holds sync down to 30/(1+N) fps (3 -> 7.5 fps). "
|
||||
"Low = gentle brief fast-forward after a stall; 0 = "
|
||||
"uncapped, one hard jump-cut.");
|
||||
|
||||
PPC_EXTERN_FUNC(__imp__rex_sub_82184460); // CAce6DemoManager::Exec ("DD")
|
||||
PPC_EXTERN_FUNC(__imp__rex_sub_821856F8); // CX360DemoManagerEM::Exec ("EM")
|
||||
|
||||
namespace {
|
||||
|
||||
using Clock = std::chrono::steady_clock;
|
||||
|
||||
// Demo sequencers advance one timeline frame per Exec at the game's native
|
||||
// 30fps cadence (cutscenes stay clamped to 30 under the FPS unlock).
|
||||
constexpr uint64_t kSamplesPerDemoTick = rex::audio::kAudioFrameSampleRate / 30; // 1600
|
||||
|
||||
// An Exec gap this long means the demo session ended (menus/gameplay between
|
||||
// cutscenes). Generous enough that a real mid-cutscene stall keeps its
|
||||
// session - that stall is exactly what the resync must recover from.
|
||||
constexpr int64_t kSessionResetMs = 1500;
|
||||
|
||||
int64_t NowMs() {
|
||||
return std::chrono::duration_cast<std::chrono::milliseconds>(
|
||||
Clock::now().time_since_epoch())
|
||||
.count();
|
||||
}
|
||||
|
||||
// Reads the single audio client's consumed-samples clock - the exact value
|
||||
// the guest sees through XAudioGetRenderDriverTic (48kHz sample units,
|
||||
// advancing in real time through hitches because underrun silence counts as
|
||||
// consumed). Returns false if the audio system/client is unavailable.
|
||||
bool ReadAudioClockSamples(PPCContext& ctx, uint64_t* out_samples) {
|
||||
if (!ctx.kernel_state) {
|
||||
return false;
|
||||
}
|
||||
auto* native_audio = ctx.kernel_state->native_audio_system();
|
||||
if (!native_audio) {
|
||||
return false;
|
||||
}
|
||||
const rex::audio::AudioClientTimingSnapshot timing =
|
||||
native_audio->GetClientTimingSnapshot(0);
|
||||
if (timing.consumed_samples == 0) {
|
||||
// No audio consumed yet (startup) - no usable master clock this frame.
|
||||
return false;
|
||||
}
|
||||
*out_samples = timing.consumed_samples;
|
||||
return true;
|
||||
}
|
||||
|
||||
struct DemoSession {
|
||||
const char* site = "";
|
||||
int64_t last_exec_ms = INT64_MIN;
|
||||
bool clock_valid = false;
|
||||
uint64_t baseline_samples = 0;
|
||||
uint64_t ticks_issued = 0;
|
||||
};
|
||||
|
||||
DemoSession g_session;
|
||||
|
||||
void ExecWithResync(PPCContext& ctx, uint8_t* base,
|
||||
void (*original)(PPCContext&, uint8_t*), const char* site) {
|
||||
const int64_t now_ms = NowMs();
|
||||
|
||||
const bool new_session = g_session.site != site ||
|
||||
g_session.last_exec_ms == INT64_MIN ||
|
||||
(now_ms - g_session.last_exec_ms) > kSessionResetMs;
|
||||
if (new_session) {
|
||||
g_session = DemoSession{};
|
||||
g_session.site = site;
|
||||
}
|
||||
g_session.last_exec_ms = now_ms;
|
||||
|
||||
// Stamp the cinematic-audio gate for the stereo fold-down (conversion.h):
|
||||
// the demo wrappers tick only while an in-engine cutscene plays, so their
|
||||
// freshness is the "cutscene audio active" signal. Unconditional - the
|
||||
// stamp is independent of whether the resync behavior itself is enabled.
|
||||
rex::audio::NotifyCinematicAudioTick(now_ms);
|
||||
|
||||
uint64_t audio_samples = 0;
|
||||
const bool clock_ok = ReadAudioClockSamples(ctx, &audio_samples);
|
||||
if (clock_ok && !g_session.clock_valid) {
|
||||
// First usable clock reading of this session: the tick issued this very
|
||||
// frame corresponds to "now" on the audio timeline.
|
||||
g_session.clock_valid = true;
|
||||
g_session.baseline_samples = audio_samples;
|
||||
g_session.ticks_issued = 0;
|
||||
}
|
||||
|
||||
const bool resync = REXCVAR_GET(ac6_cutscene_resync) &&
|
||||
g_session.clock_valid && clock_ok;
|
||||
// The original's input registers, for re-issuing the call. The extra ticks
|
||||
// must not see the first call's clobbered volatile registers.
|
||||
PPCContext saved_ctx;
|
||||
if (resync) {
|
||||
saved_ctx = ctx;
|
||||
}
|
||||
|
||||
// The frame's own tick.
|
||||
original(ctx, base);
|
||||
++g_session.ticks_issued;
|
||||
|
||||
int64_t deficit_ticks = 0;
|
||||
if (g_session.clock_valid && clock_ok) {
|
||||
const uint64_t expected =
|
||||
(audio_samples - g_session.baseline_samples) / kSamplesPerDemoTick;
|
||||
deficit_ticks = static_cast<int64_t>(expected) -
|
||||
static_cast<int64_t>(g_session.ticks_issued);
|
||||
}
|
||||
|
||||
// Catch-up: engage past 2 ticks of drift (steady-state jitter is +/-1 and
|
||||
// must never trigger), then tick the timeline down to zero deficit, capped
|
||||
// per frame. The deficit is recomputed from absolute clocks every frame,
|
||||
// so any remainder past the cap carries automatically.
|
||||
if (resync && deficit_ticks >= 2) {
|
||||
const int32_t cap = REXCVAR_GET(ac6_cutscene_resync_max_ticks);
|
||||
int64_t extras = deficit_ticks;
|
||||
if (cap > 0) {
|
||||
extras = std::min<int64_t>(extras, cap);
|
||||
}
|
||||
for (int64_t i = 0; i < extras; ++i) {
|
||||
ctx = saved_ctx;
|
||||
original(ctx, base);
|
||||
++g_session.ticks_issued;
|
||||
}
|
||||
REXLOG_DEBUG("[AC6-CUTSYNC] catch-up: site={} ran {} extra ticks (deficit was {}, cap {})",
|
||||
site, extras, deficit_ticks, cap);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
// CAce6DemoManager::Exec ("DD") - in-engine cutscene sequencer tick.
|
||||
PPC_FUNC_IMPL(rex_sub_82184460) {
|
||||
PPC_FUNC_PROLOGUE();
|
||||
ExecWithResync(ctx, base, __imp__rex_sub_82184460, "DD");
|
||||
}
|
||||
|
||||
// CX360DemoManagerEM::Exec ("EM") - in-engine cutscene sequencer tick.
|
||||
PPC_FUNC_IMPL(rex_sub_821856F8) {
|
||||
PPC_FUNC_PROLOGUE();
|
||||
ExecWithResync(ctx, base, __imp__rex_sub_821856F8, "EM");
|
||||
}
|
||||
@@ -19,6 +19,9 @@ REXCVAR_DECLARE(int32_t, draw_resolution_scale_y);
|
||||
REXCVAR_DECLARE(bool, param_gen_integer_guest_position);
|
||||
REXCVAR_DECLARE(bool, param_gen_host_subpixel_restore);
|
||||
REXCVAR_DECLARE(std::string, ac6_neutralize_deswizzle_hashes);
|
||||
REXCVAR_DECLARE(std::string, ac6_snap_guest_texel_hashes);
|
||||
REXCVAR_DECLARE(std::string, ac6_densify_x_fetch_hashes);
|
||||
REXCVAR_DECLARE(std::string, ac6_densify_y_fetch_hashes);
|
||||
REXCVAR_DECLARE(std::string, log_file);
|
||||
REXCVAR_DECLARE(std::string, log_level);
|
||||
REXCVAR_DECLARE(bool, ac6_d3d_trace);
|
||||
@@ -70,6 +73,13 @@ REXCVAR_DEFINE_BOOL(ac6_fix_deswizzle, true, "AC6",
|
||||
"sub-tile de-swizzle, which is always a wrong texel permutation once the "
|
||||
"emulator detiles to linear. On by default; effective at all draw scales "
|
||||
"(the swizzle is present at 1x too).");
|
||||
REXCVAR_DEFINE_BOOL(ac6_fix_dof, true, "AC6",
|
||||
"Fix the in-engine cutscene depth-of-field striping/ghosting at draw "
|
||||
"resolution scale > 1: the 6-pass DoF chain is authored for the 640x360 "
|
||||
"grid, so its guest-texel kernels alias against the extra detail of "
|
||||
"scaled sources (combed CoC weights, ghost copies at the sparse gather "
|
||||
"taps). Snaps the CoC pre-blurs to the guest grid and densifies the "
|
||||
"gather taps along each pass's axis. On by default; inert at 1x.");
|
||||
|
||||
#include "generated/ac6recomp_config.h"
|
||||
#include "generated/ac6recomp_init.h"
|
||||
@@ -187,6 +197,15 @@ void ApplyAc6FixDefaults() {
|
||||
AC6_SET_IF_UNSET(ac6_neutralize_deswizzle_hashes,
|
||||
"7d22894002d16018, 17e5e4ac3e713245:4");
|
||||
}
|
||||
// Cutscene DoF chain (CoC pre-blur pair snapped to the guest grid; the
|
||||
// two 13-tap gathers densified along their pass axes). The translator
|
||||
// only emits these at draw scale > 1, so no scale gate is needed here.
|
||||
if (REXCVAR_GET(ac6_fix_dof)) {
|
||||
AC6_SET_IF_UNSET(ac6_snap_guest_texel_hashes,
|
||||
"85d733927e3bba9c, d050dfa6f58567f6");
|
||||
AC6_SET_IF_UNSET(ac6_densify_x_fetch_hashes, "6328f9c40913c82c");
|
||||
AC6_SET_IF_UNSET(ac6_densify_y_fetch_hashes, "5bd20f9d0d911687");
|
||||
}
|
||||
}
|
||||
#undef AC6_SET_IF_UNSET
|
||||
|
||||
|
||||
+107
-4
@@ -3,13 +3,40 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <algorithm>
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
|
||||
#include <native/audio/render_driver_frame_layout.h>
|
||||
#include <rex/cvar.h>
|
||||
#include <rex/platform.h>
|
||||
#include <rex/types.h>
|
||||
|
||||
REXCVAR_DECLARE(bool, audio_cutscene_downmix);
|
||||
REXCVAR_DECLARE(double, audio_downmix_center_gain);
|
||||
REXCVAR_DECLARE(double, audio_downmix_surround_gain);
|
||||
REXCVAR_DECLARE(double, audio_downmix_lfe_gain);
|
||||
REXCVAR_DECLARE(double, audio_downmix_cutscene_center_gain);
|
||||
REXCVAR_DECLARE(double, audio_downmix_cutscene_surround_gain);
|
||||
REXCVAR_DECLARE(double, audio_downmix_cutscene_lfe_gain);
|
||||
REXCVAR_DECLARE(double, audio_downmix_cutscene_trim);
|
||||
REXCVAR_DECLARE(double, audio_downmix_cutscene_ramp_ms);
|
||||
|
||||
namespace rex::audio {
|
||||
|
||||
// Wall-clock ms of the last in-engine cutscene sequencer tick, stamped by the
|
||||
// demo-tick hook (ac6_cutscene_resync). Lets the stereo fold-down apply
|
||||
// cutscene-specific gains without reaching into game code. INT64_MIN = never.
|
||||
inline std::atomic<int64_t> g_last_cinematic_audio_tick_ms{INT64_MIN};
|
||||
|
||||
inline void NotifyCinematicAudioTick(int64_t now_ms) {
|
||||
g_last_cinematic_audio_tick_ms.store(now_ms, std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
} // namespace rex::audio
|
||||
|
||||
namespace rex::audio::conversion {
|
||||
|
||||
inline constexpr float kStereoDownmixCenterGain = 0.70710678f;
|
||||
@@ -20,6 +47,81 @@ inline constexpr float kStereoDownmixNormalize =
|
||||
1.0f / (1.0f + kStereoDownmixCenterGain + kStereoDownmixSurroundGain +
|
||||
kStereoDownmixLfeGain);
|
||||
|
||||
// Live fold-down gains for the path AC6 actually plays through (the AMD64
|
||||
// planar fold below; the other variants keep the compile-time constants).
|
||||
// The base gains default to those constants; the cutscene set applies while
|
||||
// the demo sequencer is ticking, because AC6's cutscene mixer submits its premix
|
||||
// spread across ALL six speaker slots as decorrelated near-copies of one mix
|
||||
// (measured: equal RMS on every channel, inter-channel correlation 0.69-0.94
|
||||
// at exactly lag 0, identical structure across scenes). Real speakers
|
||||
// separate the copies acoustically; an electrical 6-to-2 sum combs them -
|
||||
// heard as doubled dialogue. The fronts alone carry the complete mix, so the
|
||||
// cutscene defaults fold only the fronts. Gameplay audio (discrete channels)
|
||||
// is bit-identical to the old constants.
|
||||
struct StereoDownmixGains {
|
||||
float center;
|
||||
float surround;
|
||||
float lfe;
|
||||
float normalize;
|
||||
};
|
||||
|
||||
inline StereoDownmixGains GetStereoDownmixGains() {
|
||||
// The cutscene gains engage while demo-wrapper ticks are fresh; f slews
|
||||
// over ramp_ms so fold changes never step. Known cosmetic (accepted): the
|
||||
// wrapper keeps ticking through the gallery's menu->scene transitions, so
|
||||
// the gallery's front-weighted transition SFX plays through the
|
||||
// fronts-only fold hot; campaign flows are unaffected.
|
||||
const int64_t now_ms = std::chrono::duration_cast<std::chrono::milliseconds>(
|
||||
std::chrono::steady_clock::now().time_since_epoch())
|
||||
.count();
|
||||
const int64_t last_tick =
|
||||
g_last_cinematic_audio_tick_ms.load(std::memory_order_relaxed);
|
||||
const bool engaged = REXCVAR_GET(audio_cutscene_downmix) &&
|
||||
last_tick != INT64_MIN && (now_ms - last_tick) <= 250;
|
||||
static float f_state = 0.0f; // 0 = base fold, 1 = full cutscene gains
|
||||
static int64_t f_last_ms = INT64_MIN;
|
||||
const double ramp = std::max(1.0, REXCVAR_GET(audio_downmix_cutscene_ramp_ms));
|
||||
float step = 1.0f;
|
||||
if (f_last_ms != INT64_MIN && now_ms >= f_last_ms) {
|
||||
step = float(std::min(1.0, double(now_ms - f_last_ms) / ramp));
|
||||
}
|
||||
f_last_ms = now_ms;
|
||||
const float target = engaged ? 1.0f : 0.0f;
|
||||
if (target > f_state) {
|
||||
f_state = std::min(target, f_state + step);
|
||||
} else {
|
||||
f_state = std::max(target, f_state - step);
|
||||
}
|
||||
const float f = f_state;
|
||||
auto clamp_gain = [](double v) {
|
||||
return std::min(2.0f, std::max(0.0f, float(v)));
|
||||
};
|
||||
auto blend = [&](double cutscene_value, double base_value) {
|
||||
const float base = clamp_gain(base_value);
|
||||
const float cut =
|
||||
cutscene_value >= 0.0 ? clamp_gain(cutscene_value) : base;
|
||||
return base + (cut - base) * f;
|
||||
};
|
||||
StereoDownmixGains gains;
|
||||
gains.center = blend(REXCVAR_GET(audio_downmix_cutscene_center_gain),
|
||||
REXCVAR_GET(audio_downmix_center_gain));
|
||||
gains.surround = blend(REXCVAR_GET(audio_downmix_cutscene_surround_gain),
|
||||
REXCVAR_GET(audio_downmix_surround_gain));
|
||||
gains.lfe = blend(REXCVAR_GET(audio_downmix_cutscene_lfe_gain),
|
||||
REXCVAR_GET(audio_downmix_lfe_gain));
|
||||
// Normalization follows the live gains so loudness stays consistent at any
|
||||
// setting; at the stock base gains this equals the old fixed constant, so
|
||||
// gameplay output is bit-identical. The near-copy cutscene mixes are
|
||||
// self-correcting under it (fold of N unity-ish copies divided by the gain
|
||||
// sum lands at the same level whichever channels fold); the measured
|
||||
// residual vs the old fold is +0.6 dB, cancelled by the default trim.
|
||||
gains.normalize = 1.0f / (1.0f + gains.center + gains.surround + gains.lfe);
|
||||
const float trim = std::min(
|
||||
2.0f, std::max(0.0f, float(REXCVAR_GET(audio_downmix_cutscene_trim))));
|
||||
gains.normalize *= 1.0f + (trim - 1.0f) * f;
|
||||
return gains;
|
||||
}
|
||||
|
||||
inline float SanitizeGuestAudioSample(float sample) {
|
||||
if (!std::isfinite(sample)) {
|
||||
return 0.0f;
|
||||
@@ -70,10 +172,11 @@ 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 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 StereoDownmixGains live_gains = GetStereoDownmixGains();
|
||||
const __m128 center_gain = _mm_set1_ps(live_gains.center);
|
||||
const __m128 surround_gain = _mm_set1_ps(live_gains.surround);
|
||||
const __m128 lfe_gain = _mm_set1_ps(live_gains.lfe);
|
||||
const __m128 normalize = _mm_set1_ps(live_gains.normalize);
|
||||
const __m128 peak_headroom = _mm_set1_ps(kStereoDownmixPeakHeadroom);
|
||||
const __m128 sign_mask = _mm_set1_ps(-0.0f);
|
||||
|
||||
|
||||
@@ -32,6 +32,9 @@ REXCVAR_DECLARE(bool, draw_resolution_scaled_texture_offsets);
|
||||
REXCVAR_DECLARE(bool, param_gen_integer_guest_position);
|
||||
REXCVAR_DECLARE(bool, param_gen_host_subpixel_restore);
|
||||
REXCVAR_DECLARE(std::string, ac6_neutralize_deswizzle_hashes);
|
||||
REXCVAR_DECLARE(std::string, ac6_snap_guest_texel_hashes);
|
||||
REXCVAR_DECLARE(std::string, ac6_densify_x_fetch_hashes);
|
||||
REXCVAR_DECLARE(std::string, ac6_densify_y_fetch_hashes);
|
||||
REXCVAR_DECLARE(std::string, readback_resolve);
|
||||
REXCVAR_DECLARE(bool, readback_resolve_half_pixel_offset);
|
||||
REXCVAR_DECLARE(bool, readback_memexport);
|
||||
|
||||
+29
@@ -65,6 +65,35 @@ REXCVAR_DEFINE_STRING(ac6_neutralize_deswizzle_hashes, "", "GPU/Shader",
|
||||
"Xenos tfetch slot 4 (needed when only some fetches de-swizzle, e.g. "
|
||||
"the AC6 cloud compositor: scene fetch de-swizzles, mask/cloud "
|
||||
"fetches use plain UVs and must be left alone). Runtime, no rebuild.");
|
||||
REXCVAR_DEFINE_STRING(ac6_snap_guest_texel_hashes, "", "GPU/Shader",
|
||||
"AC6: same \"<hash>[:<slot>[+<slot>...]]\" token list as "
|
||||
"ac6_neutralize_deswizzle_hashes, naming guest pixel-shader ucode "
|
||||
"hashes whose texture taps encode fractional guest-texel bilinear "
|
||||
"positions (e.g. the cutscene depth-of-field gather pair). Sampling "
|
||||
"a resolution-scaled texture with such a kernel reinterprets every "
|
||||
"blend ratio against the finer host grid and alternates the kernel "
|
||||
"phase per output pixel, aliasing into striping at >1x. For matching "
|
||||
"fetches the normalized coordinate is snapped to the guest texel "
|
||||
"center (where host bilinear of a scaled texture returns exactly the "
|
||||
"guest texel's box average), restoring the guest sampling grid at any "
|
||||
"draw resolution scale. Applied only at >1x and only to "
|
||||
"resolution-scaled textures. Runtime, no rebuild.");
|
||||
REXCVAR_DEFINE_STRING(ac6_densify_x_fetch_hashes, "", "GPU/Shader",
|
||||
"AC6: \"<hash>[:<slot>[+<slot>...]]\" list for HORIZONTAL-axis "
|
||||
"separable filter passes (e.g. the cutscene DoF H gather "
|
||||
"6328f9c40913c82c). Each allowlisted fetch becomes the average of "
|
||||
"2*draw_resolution_scale_x samples spread along X across one "
|
||||
"guest-texel half-gap on each side of the original tap - the union "
|
||||
"of all taps' cells covers the kernel span continuously, so ghost "
|
||||
"copies of arbitrarily thin features merge at any resolution scale. "
|
||||
"Convolving the authored kernel with the 2-texel cell box widens the "
|
||||
"blur by ~2% (visually shape-faithful). Per-axis scale aware; no-op "
|
||||
"when draw_resolution_scale_x is 1. Read at shader translation.");
|
||||
REXCVAR_DEFINE_STRING(ac6_densify_y_fetch_hashes, "", "GPU/Shader",
|
||||
"AC6: as ac6_densify_x_fetch_hashes, for VERTICAL-axis separable "
|
||||
"filter passes (e.g. the cutscene DoF V gather 5bd20f9d0d911687): "
|
||||
"2*draw_resolution_scale_y samples along Y per fetch. No-op when "
|
||||
"draw_resolution_scale_y is 1.");
|
||||
REXCVAR_DEFINE_BOOL(ac6_flare_drop_quad2, true, "GPU/Shader",
|
||||
"AC6: cull the sun lens-flare's spurious second billboard "
|
||||
"(vertices 4-7) to remove the faint rectangle in the sky");
|
||||
|
||||
+166
-2
@@ -862,6 +862,63 @@ void DxbcShaderTranslator::ProcessTextureFetchInstruction(
|
||||
UcodeHashSlotInList(current_shader().ucode_data_hash(), tfetch_index,
|
||||
neutralize_hashes);
|
||||
}
|
||||
// Guest-texel snap for scale-broken filter kernels: some game passes (AC6's
|
||||
// cutscene depth-of-field gather pair) build their kernel out of fractional
|
||||
// guest-texel tap offsets whose bilinear blend ratios are tuned for the
|
||||
// guest-resolution texel grid. Against a resolution-scaled texture the same
|
||||
// UVs land on the finer host grid: every ratio is reinterpreted and the
|
||||
// kernel phase alternates per output pixel, aliasing into striping. For an
|
||||
// allowlisted shader (same syntax as the de-swizzle neutralize) snap the
|
||||
// final coordinate to the guest texel center, where host bilinear of a
|
||||
// scaled texture returns exactly the guest texel's box average - the guest
|
||||
// sampling grid at any scale. Unlike the passes above these shaders sample
|
||||
// via interpolators, so no param_gen requirement.
|
||||
bool apply_guest_texel_snap = false;
|
||||
if (instr.opcode == FetchOpcode::kTextureFetch &&
|
||||
!instr.attributes.unnormalized_coordinates &&
|
||||
instr.dimension == xenos::FetchOpDimension::k2D && is_pixel_shader() &&
|
||||
(draw_resolution_scale_x_ > 1 || draw_resolution_scale_y_ > 1)) {
|
||||
const std::string& snap_hashes = REXCVAR_GET(ac6_snap_guest_texel_hashes);
|
||||
apply_guest_texel_snap =
|
||||
!snap_hashes.empty() &&
|
||||
UcodeHashSlotInList(current_shader().ucode_data_hash(), tfetch_index,
|
||||
snap_hashes);
|
||||
}
|
||||
// Tap-footprint box filter for sparse-kernel shaders (AC6 DoF gathers):
|
||||
// taps several guest texels apart alias against the extra detail of a
|
||||
// resolution-scaled source. Each allowlisted fetch becomes the average of a
|
||||
// 4-sample cluster at (+/-0.25, +/-0.25) guest texels around the original
|
||||
// position - a guest-Nyquist low-pass at the exact tap location, preserving
|
||||
// the kernel's fractional placement (no output quantization, unlike the
|
||||
// texel-center snap above).
|
||||
// Axis densification for separable sparse-kernel passes (AC6 DoF gathers):
|
||||
// each allowlisted fetch becomes the average of 2*scale_axis samples spread
|
||||
// along the pass axis across one guest-texel half-gap on each side of the
|
||||
// tap. The union of adjacent taps' cells covers the kernel span
|
||||
// continuously, so ghost copies of features thinner than the tap spacing
|
||||
// merge at any resolution scale; the effective kernel is the authored one
|
||||
// convolved with a 2-guest-texel box (~2% wider - shape-faithful). Per-axis
|
||||
// resolution scale aware (Xenia allows asymmetric X/Y scales); no-op when
|
||||
// the relevant axis scale is 1.
|
||||
uint32_t densify_axis_samples[2] = {0, 0};
|
||||
if (instr.opcode == FetchOpcode::kTextureFetch &&
|
||||
!instr.attributes.unnormalized_coordinates &&
|
||||
instr.dimension == xenos::FetchOpDimension::k2D && is_pixel_shader()) {
|
||||
const std::string& densify_x_hashes = REXCVAR_GET(ac6_densify_x_fetch_hashes);
|
||||
if (draw_resolution_scale_x_ > 1 && !densify_x_hashes.empty() &&
|
||||
UcodeHashSlotInList(current_shader().ucode_data_hash(), tfetch_index,
|
||||
densify_x_hashes)) {
|
||||
densify_axis_samples[0] = 2 * uint32_t(draw_resolution_scale_x_);
|
||||
}
|
||||
const std::string& densify_y_hashes = REXCVAR_GET(ac6_densify_y_fetch_hashes);
|
||||
if (draw_resolution_scale_y_ > 1 && !densify_y_hashes.empty() &&
|
||||
UcodeHashSlotInList(current_shader().ucode_data_hash(), tfetch_index,
|
||||
densify_y_hashes)) {
|
||||
densify_axis_samples[1] = 2 * uint32_t(draw_resolution_scale_y_);
|
||||
}
|
||||
}
|
||||
const bool apply_cluster_filter =
|
||||
densify_axis_samples[0] != 0 || densify_axis_samples[1] != 0;
|
||||
uint32_t size_needed_components = 0b0000;
|
||||
if (instr.opcode == FetchOpcode::kGetTextureWeights) {
|
||||
// Size needed for denormalization for coordinate lerp factor.
|
||||
@@ -926,10 +983,12 @@ void DxbcShaderTranslator::ProcessTextureFetchInstruction(
|
||||
// the texture is 3D unconditionally.
|
||||
size_needed_components |= 0b1000;
|
||||
}
|
||||
if (apply_host_subpixel_correction || apply_deswizzle_identity) {
|
||||
if (apply_host_subpixel_correction || apply_deswizzle_identity ||
|
||||
apply_guest_texel_snap || apply_cluster_filter) {
|
||||
// Need the guest texture width/height (XY) to convert the host sub-pixel
|
||||
// offset (Tier B) or the SV_Position into normalized texture space
|
||||
// (de-swizzle identity).
|
||||
// (de-swizzle identity), or to locate the guest texel grid (texel snap /
|
||||
// cluster filters).
|
||||
size_needed_components |= 0b0011;
|
||||
}
|
||||
uint32_t size_and_is_3d_temp = size_needed_components ? PushSystemTemp() : UINT32_MAX;
|
||||
@@ -1303,6 +1362,32 @@ void DxbcShaderTranslator::ProcessTextureFetchInstruction(
|
||||
// Release deswizzle_temp.
|
||||
PopSystemTemp();
|
||||
}
|
||||
if (apply_guest_texel_snap) {
|
||||
// coord_and_sampler_temp.xy is the final normalized coordinate. Snap it
|
||||
// to the guest texel center so the allowlisted kernel samples the guest
|
||||
// grid regardless of the host storage resolution (see the gate above).
|
||||
uint32_t snap_temp = PushSystemTemp();
|
||||
// snap_temp.xy = (floor(uv * guest_size) + 0.5) / guest_size.
|
||||
a_.OpMul(dxbc::Dest::R(snap_temp, 0b0011),
|
||||
dxbc::Src::R(coord_and_sampler_temp), dxbc::Src::R(size_and_is_3d_temp));
|
||||
a_.OpRoundNI(dxbc::Dest::R(snap_temp, 0b0011), dxbc::Src::R(snap_temp));
|
||||
a_.OpAdd(dxbc::Dest::R(snap_temp, 0b0011), dxbc::Src::R(snap_temp),
|
||||
dxbc::Src::LF(0.5f, 0.5f, 0.0f, 0.0f));
|
||||
a_.OpDiv(dxbc::Dest::R(snap_temp, 0b0011), dxbc::Src::R(snap_temp),
|
||||
dxbc::Src::R(size_and_is_3d_temp));
|
||||
// Only textures stored at host resolution present a finer grid than the
|
||||
// kernel was tuned for; guest-resolution sources stay stock.
|
||||
a_.OpAnd(dxbc::Dest::R(snap_temp, 0b0100),
|
||||
LoadSystemConstant(SystemConstants::Index::kTexturesResolutionScaled,
|
||||
offsetof(SystemConstants, textures_resolution_scaled),
|
||||
dxbc::Src::kXXXX),
|
||||
dxbc::Src::LU(uint32_t(1) << tfetch_index));
|
||||
a_.OpIf(true, dxbc::Src::R(snap_temp, dxbc::Src::kZZZZ));
|
||||
a_.OpMov(dxbc::Dest::R(coord_and_sampler_temp, 0b0011), dxbc::Src::R(snap_temp));
|
||||
a_.OpEndIf();
|
||||
// Release snap_temp.
|
||||
PopSystemTemp();
|
||||
}
|
||||
switch (instr.dimension) {
|
||||
case xenos::FetchOpDimension::k1D:
|
||||
// Pad to 2D array coordinates.
|
||||
@@ -1957,6 +2042,65 @@ void DxbcShaderTranslator::ProcessTextureFetchInstruction(
|
||||
texture_binding_signed.bindful_srv_index,
|
||||
uint32_t(SRVMainRegister::kBindfulTexturesStart) + texture_binding_index_signed);
|
||||
}
|
||||
// Tap densification (see the gate above): re-emit the whole sample
|
||||
// block once per cluster offset around the original coordinate and
|
||||
// average the RAW results (before format decode - consistent with how
|
||||
// Xenos bilinear itself filters in storage space). Restricted to
|
||||
// plain 2D fetches. Cluster geometry: 2*scale_axis samples per listed
|
||||
// axis, spanning one guest texel each side of the tap (a grid if both
|
||||
// axes are listed).
|
||||
const bool cluster_this_srv = apply_cluster_filter &&
|
||||
srv_dimension == xenos::FetchOpDimension::k2D &&
|
||||
!layer_lerp_needed;
|
||||
float cluster_dx[64];
|
||||
float cluster_dy[64];
|
||||
uint32_t cluster_count = 1;
|
||||
cluster_dx[0] = 0.0f;
|
||||
cluster_dy[0] = 0.0f;
|
||||
if (cluster_this_srv) {
|
||||
float xs[8], ys[8];
|
||||
uint32_t nx = 1, ny = 1;
|
||||
xs[0] = 0.0f;
|
||||
ys[0] = 0.0f;
|
||||
if (densify_axis_samples[0]) {
|
||||
nx = std::min(densify_axis_samples[0], 8u);
|
||||
for (uint32_t j = 0; j < nx; ++j) {
|
||||
xs[j] = (float(j) + 0.5f) * 2.0f / float(nx) - 1.0f;
|
||||
}
|
||||
}
|
||||
if (densify_axis_samples[1]) {
|
||||
ny = std::min(densify_axis_samples[1], 8u);
|
||||
for (uint32_t j = 0; j < ny; ++j) {
|
||||
ys[j] = (float(j) + 0.5f) * 2.0f / float(ny) - 1.0f;
|
||||
}
|
||||
}
|
||||
cluster_count = 0;
|
||||
for (uint32_t jy = 0; jy < ny; ++jy) {
|
||||
for (uint32_t jx = 0; jx < nx; ++jx) {
|
||||
cluster_dx[cluster_count] = xs[jx];
|
||||
cluster_dy[cluster_count] = ys[jy];
|
||||
++cluster_count;
|
||||
}
|
||||
}
|
||||
}
|
||||
uint32_t cluster_accum_temp = UINT32_MAX;
|
||||
uint32_t cluster_coord_temp = UINT32_MAX;
|
||||
const uint32_t cluster_passes = cluster_this_srv ? cluster_count : 1;
|
||||
if (cluster_this_srv) {
|
||||
cluster_accum_temp = PushSystemTemp();
|
||||
cluster_coord_temp = PushSystemTemp();
|
||||
// The original tap coordinate (and the sampler index in W).
|
||||
a_.OpMov(dxbc::Dest::R(cluster_coord_temp), dxbc::Src::R(coord_and_sampler_temp));
|
||||
}
|
||||
for (uint32_t cluster_pass = 0; cluster_pass < cluster_passes; ++cluster_pass) {
|
||||
if (cluster_this_srv) {
|
||||
// coord.xy = original + cluster offset (guest texels) / guest_size.
|
||||
a_.OpDiv(dxbc::Dest::R(coord_and_sampler_temp, 0b0011),
|
||||
dxbc::Src::LF(cluster_dx[cluster_pass], cluster_dy[cluster_pass], 0.0f, 0.0f),
|
||||
dxbc::Src::R(size_and_is_3d_temp));
|
||||
a_.OpAdd(dxbc::Dest::R(coord_and_sampler_temp, 0b0011),
|
||||
dxbc::Src::R(coord_and_sampler_temp), dxbc::Src::R(cluster_coord_temp));
|
||||
}
|
||||
for (uint32_t layer = 0; layer < (layer_lerp_needed ? 2u : 1u); ++layer) {
|
||||
uint32_t layer_value_temp = system_temp_result_;
|
||||
if (layer) {
|
||||
@@ -2050,6 +2194,26 @@ void DxbcShaderTranslator::ProcessTextureFetchInstruction(
|
||||
PopSystemTemp();
|
||||
}
|
||||
}
|
||||
if (cluster_this_srv) {
|
||||
if (cluster_pass == 0) {
|
||||
a_.OpMov(dxbc::Dest::R(cluster_accum_temp, used_result_nonzero_components),
|
||||
dxbc::Src::R(system_temp_result_));
|
||||
} else {
|
||||
a_.OpAdd(dxbc::Dest::R(cluster_accum_temp, used_result_nonzero_components),
|
||||
dxbc::Src::R(cluster_accum_temp), dxbc::Src::R(system_temp_result_));
|
||||
}
|
||||
}
|
||||
} // cluster_pass
|
||||
if (cluster_this_srv) {
|
||||
// The tap's value = the average of the cluster samples.
|
||||
a_.OpMul(dxbc::Dest::R(system_temp_result_, used_result_nonzero_components),
|
||||
dxbc::Src::R(cluster_accum_temp),
|
||||
dxbc::Src::LF(1.0f / float(cluster_passes)));
|
||||
// Restore the unoffset coordinate for any downstream use.
|
||||
a_.OpMov(dxbc::Dest::R(coord_and_sampler_temp), dxbc::Src::R(cluster_coord_temp));
|
||||
// Release cluster_coord_temp and cluster_accum_temp.
|
||||
PopSystemTemp(2);
|
||||
}
|
||||
}
|
||||
if (instr.dimension == xenos::FetchOpDimension::k3DOrStacked) {
|
||||
// Close the stacked/3D check.
|
||||
|
||||
@@ -19,6 +19,67 @@ REXCVAR_DEFINE_BOOL(audio_trace_render_driver_verbose, false, "Audio",
|
||||
"Trace render-driver activity");
|
||||
REXCVAR_DEFINE_BOOL(audio_deep_trace, false, "Audio",
|
||||
"Enable verbose runtime audio tracing");
|
||||
REXCVAR_DEFINE_BOOL(audio_cutscene_downmix, true, "Audio",
|
||||
"Master switch for the cutscene-specific stereo fold-down "
|
||||
"(fronts-only fold while the demo sequencer is active - "
|
||||
"removes the doubled/combed dialogue caused by summing the "
|
||||
"cutscene mixer's six decorrelated near-copies to stereo). "
|
||||
"false = the original summing fold everywhere; the "
|
||||
"audio_downmix_cutscene_* cvars then have no effect. "
|
||||
"Gameplay audio is identical either way.");
|
||||
REXCVAR_DEFINE_DOUBLE(audio_downmix_center_gain, 0.70710678, "Audio",
|
||||
"Front-center gain in the 6ch-to-stereo fold-down (was a "
|
||||
"compile-time constant; default unchanged). Normalization "
|
||||
"follows the live gains. Read continuously.");
|
||||
REXCVAR_DEFINE_DOUBLE(audio_downmix_surround_gain, 0.5, "Audio",
|
||||
"Rear-channel gain in the 6ch-to-stereo fold-down. See "
|
||||
"audio_downmix_center_gain.");
|
||||
REXCVAR_DEFINE_DOUBLE(audio_downmix_lfe_gain, 0.0, "Audio",
|
||||
"LFE gain in the 6ch-to-stereo fold-down. See "
|
||||
"audio_downmix_center_gain.");
|
||||
REXCVAR_DEFINE_DOUBLE(audio_downmix_cutscene_center_gain, 0.0, "Audio",
|
||||
"Front-center fold-down gain used ONLY while an in-engine "
|
||||
"cutscene is playing (demo sequencer active); negative = "
|
||||
"inherit audio_downmix_center_gain. AC6's cutscene mixer "
|
||||
"spreads the premix across ALL six speaker slots as "
|
||||
"decorrelated near-copies of one mix; the fronts alone "
|
||||
"carry the complete mix, so the cutscene fold takes only "
|
||||
"them - center and surround default to 0 here.");
|
||||
REXCVAR_DEFINE_DOUBLE(audio_downmix_cutscene_surround_gain, 0.0, "Audio",
|
||||
"Rear-channel fold-down gain during in-engine cutscenes; "
|
||||
"negative = inherit. See audio_downmix_cutscene_center_gain.");
|
||||
REXCVAR_DEFINE_DOUBLE(audio_downmix_cutscene_lfe_gain, -1.0, "Audio",
|
||||
"LFE fold-down gain during in-engine cutscenes; negative = "
|
||||
"inherit. See audio_downmix_cutscene_center_gain.");
|
||||
REXCVAR_DEFINE_DOUBLE(audio_downmix_cutscene_trim, 0.933, "Audio",
|
||||
"Extra output gain on the cutscene fold (ramped in with "
|
||||
"the cutscene gains). Default 0.933 = the measured RMS "
|
||||
"ratio of the fronts-only fold vs the stock summing fold "
|
||||
"(-0.6 dB), so cutscene loudness matches the original "
|
||||
"baseline; 1.0 = no trim.");
|
||||
REXCVAR_DEFINE_DOUBLE(audio_downmix_cutscene_ramp_ms, 250.0, "Audio",
|
||||
"Slew time between the base fold and the cutscene gains "
|
||||
"when the demo-sequencer signal engages or releases.");
|
||||
REXCVAR_DEFINE_BOOL(audio_xma_loop_guard, true, "Audio",
|
||||
"Require a real loop window (loop_start < loop_end, "
|
||||
"read_offset >= loop_end) before engaging XMA loop "
|
||||
"handling, as in current Xenia master (PR #1808, debugged "
|
||||
"on Ace Combat 6). Off: legacy behavior, where streamed "
|
||||
"voices carrying degenerate loop metadata (loop_count="
|
||||
"0xff, loop_start == loop_end == 0) re-fire the loop "
|
||||
"machinery on every input buffer swap, truncating/"
|
||||
"skipping subframes of the stream (confirmed firing on "
|
||||
"AC6 cutscene streams via the deep-trace counter).");
|
||||
REXCVAR_DEFINE_BOOL(audio_xma_preserve_timeline, true, "Audio",
|
||||
"When the XMA decoder backend rejects a damaged frame, emit that "
|
||||
"frame's worth of silence instead of dropping it. The input read "
|
||||
"offset advances past the frame either way, so dropping the output "
|
||||
"silently shortens the stream's timeline by 512 samples per damaged "
|
||||
"frame - which would permanently desynchronize multi-stream sources "
|
||||
"(5.1 premixes carried as parallel stereo streams, e.g. AC6's "
|
||||
"cutscene mixes). Robustness fix - no in-game trigger is known in "
|
||||
"AC6 (instrumented runs decode every frame). On by default; off "
|
||||
"restores the legacy drop behavior.");
|
||||
|
||||
namespace rex::audio {
|
||||
|
||||
|
||||
+52
-5
@@ -1,4 +1,4 @@
|
||||
/**
|
||||
/**
|
||||
* ReXGlue native audio runtime
|
||||
* Part of the AC6 Recompilation project
|
||||
*/
|
||||
@@ -17,6 +17,8 @@
|
||||
#include <native/stream.h>
|
||||
|
||||
REXCVAR_DECLARE(bool, audio_deep_trace);
|
||||
REXCVAR_DECLARE(bool, audio_xma_loop_guard);
|
||||
REXCVAR_DECLARE(bool, audio_xma_preserve_timeline);
|
||||
|
||||
namespace rex::audio {
|
||||
|
||||
@@ -341,7 +343,31 @@ void XmaContext::UpdateLoopStatus(XMA_CONTEXT_DATA* data) {
|
||||
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) {
|
||||
if (REXCVAR_GET(audio_xma_loop_guard)) {
|
||||
// Xenia master's TrySetupNextLoop guard (PR #1808, debugged on Ace
|
||||
// Combat 6): streamed voices can carry degenerate loop metadata
|
||||
// (loop_count=0xff, loop_start == loop_end == 0). The clamps above turn
|
||||
// that into start == end == kBitsPerPacketHeader, and SwapInputBuffer
|
||||
// resets the read offset to exactly kBitsPerPacketHeader, so without a
|
||||
// real-window check every buffer swap re-fires the loop machinery -
|
||||
// the frame output limit truncates that frame and the loop-start skip
|
||||
// drops subframes. Require loop_start < loop_end (raw values) and use
|
||||
// master's read_offset >= loop_end trigger.
|
||||
if (data->loop_start >= data->loop_end ||
|
||||
data->input_buffer_read_offset < loop_end) {
|
||||
if (data->loop_start >= data->loop_end &&
|
||||
data->input_buffer_read_offset == loop_end && IsDeepTraceEnabled()) {
|
||||
REXAPU_DEBUG(
|
||||
"XmaContext {}: loop guard suppressed degenerate loop fire "
|
||||
"(loop_start={} loop_end={} loop_count={} read_offset={})",
|
||||
id(), static_cast<uint32_t>(data->loop_start),
|
||||
static_cast<uint32_t>(data->loop_end),
|
||||
static_cast<uint32_t>(data->loop_count),
|
||||
static_cast<uint32_t>(data->input_buffer_read_offset));
|
||||
}
|
||||
return;
|
||||
}
|
||||
} else if (data->input_buffer_read_offset != loop_end) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -853,11 +879,32 @@ void XmaContext::Decode(XMA_CONTEXT_DATA* data) {
|
||||
.sample_rate = static_cast<uint32_t>(GetSampleRate(data->sample_rate)),
|
||||
.is_two_channel = bool(data->is_stereo),
|
||||
};
|
||||
if (decoder_backend_ &&
|
||||
const bool frame_decoded =
|
||||
decoder_backend_ &&
|
||||
decoder_backend_->DecodePacket(
|
||||
decode_request, std::span<uint8_t>(raw_frame_.data(), raw_frame_.size()))) {
|
||||
decode_request, std::span<uint8_t>(raw_frame_.data(), raw_frame_.size()));
|
||||
if (!frame_decoded && REXCVAR_GET(audio_xma_preserve_timeline)) {
|
||||
// A frame the decoder rejects still occupies exactly kSamplesPerFrame
|
||||
// samples of the stream's timeline, and the read offset advances past it
|
||||
// below either way. Dropping the output entirely (the legacy behavior)
|
||||
// silently shortens the stream by one frame - a latent correctness bug
|
||||
// for multi-stream sources that must stay sample-locked, e.g. 5.1
|
||||
// premixes carried as parallel stereo streams (AC6's cutscene mixes).
|
||||
// Instrumented AC6 runs decode every cutscene frame successfully, so no
|
||||
// in-game trigger is known - this is robustness against decode errors,
|
||||
// not a fix for an audible symptom. Deliver the frame as silence
|
||||
// instead - raw_frame_ is zero-filled above, so falling through emits
|
||||
// one frame of silence in this stream (~10 ms, masked by the other
|
||||
// streams) and the timeline holds.
|
||||
REXAPU_DEBUG(
|
||||
"XmaContext {}: undecodable frame (packet={} offset={} frame_bits={}) - "
|
||||
"emitting silence to preserve the stream timeline",
|
||||
id(), last_packet_index_, last_input_read_offset_before_,
|
||||
packet_info.current_frame_size_);
|
||||
}
|
||||
if (frame_decoded || REXCVAR_GET(audio_xma_preserve_timeline)) {
|
||||
current_frame_remaining_subframes_ = 4 << data->is_stereo;
|
||||
last_decode_succeeded_ = true;
|
||||
last_decode_succeeded_ = frame_decoded;
|
||||
|
||||
if (is_loop_end_frame) {
|
||||
loop_frame_output_limit_ = (data->loop_subframe_end + 1) << data->is_stereo;
|
||||
|
||||
Reference in New Issue
Block a user