Files
2026-10-08 13:25:54 +00:00

1069 lines
45 KiB
C++

// SpeedBreaker runtime. GPL-3.0-or-later (see COPYING).
//
// XMA decoder contexts. The context layout and API semantics follow Xenia
// (xenia-canary: apu/xma_context_new.cc, kernel/xboxkrnl/xboxkrnl_audio_xma.cc).
//
// Decoding is frame by frame, like the hardware: from the bit offset in
// input_buffer_read_offset, each XMA frame (15-bit length prefix, possibly
// split across packets of its stream, which follow the packet skip counts
// and can continue into the other input buffer) is copied into a synthetic
// single-frame XMA packet for FFmpeg's XMA1 decoder, which keeps its MDCT
// overlap across frames. Each frame yields 512 samples (4 output blocks per
// channel) of big-endian s16 in the output ring. So:
// - hardware loops jump at the exact frame (loop_start/loop_end bit
// offsets) and subframe (loop_subframe_end/skip), with no decoder reset:
// the overlap carries across the loop point as on the console;
// - a read offset set in the middle of a packet (a seek) starts exactly there;
// - the frame FFmpeg drops after a fresh start (its warm-up) is output as
// silence in its own place, at the start of the stream.
// The previous packet-at-a-time decoder looped at packet granularity with a
// decoder flush, and put the missing first frame's silence in the middle of
// every sound: gameplay audio (SFX restarted all the time) was full of holes.
//
// A context decodes one pass per XMAEnableContext kick (Xenia's Work()): it
// fills the output ring as far as it has room, then waits for the next kick.
// Ring semantics: write == read with output_buffer_valid set means empty; a
// pass that fills the ring clears output_buffer_valid (full). A pass that
// starts with no input moves write_offset to read_offset and clears valid
// only if unread output remains: NFSMW uses write == read to detect a
// stalled voice, and reads write == read with valid clear as a FULL ring
// (clearing valid on an empty ring left its stream player waiting forever:
// the mid-race audio cut-out). Unlike Xenia, that fake full ring is undone
// when input arrives before the game has read past the real output: EA's
// player queues speech and music as sub-streams, and one queued after the
// last had drained would start with a ring of stale audio and lose its tail.
// Where more data can still come (the other input buffer isn't valid yet),
// a frame split across it waits for it, its length field included (Xenia
// drops such a frame); a frame that runs past data that can never come is
// skipped.
// NFSMW_XMA_SILENT=1 outputs zero PCM at the same rate.
#include <stdafx.h>
#include <kernel/function.h>
#include <kernel/vmem.h>
#include <cpu/guest_thread.h>
extern "C"
{
#include <libavcodec/avcodec.h>
#include <libavutil/channel_layout.h>
}
uint32_t MmGetPhysicalAddress(uint32_t address); // imports_memory.cpp
namespace
{
// Xenia's XMA_CONTEXT_DATA: 16 big-endian dwords, bitfields LSB first.
struct XmaContextData
{
// DWORD 0
uint32_t input_buffer_0_packet_count : 12;
uint32_t loop_count : 8;
uint32_t input_buffer_0_valid : 1;
uint32_t input_buffer_1_valid : 1;
uint32_t output_buffer_block_count : 5;
uint32_t output_buffer_write_offset : 5;
// DWORD 1
uint32_t input_buffer_1_packet_count : 12;
uint32_t loop_subframe_start : 2;
uint32_t loop_subframe_end : 3;
uint32_t loop_subframe_skip : 3;
uint32_t subframe_decode_count : 4;
uint32_t subframe_skip_count : 3;
uint32_t sample_rate : 2;
uint32_t is_stereo : 1;
uint32_t unk_dword_1_c : 1;
uint32_t output_buffer_valid : 1;
// DWORD 2
uint32_t input_buffer_read_offset : 26;
uint32_t unk_dword_2 : 6;
// DWORD 3
uint32_t loop_start : 26;
uint32_t unk_dword_3 : 6;
// DWORD 4
uint32_t loop_end : 26;
uint32_t packet_metadata : 5;
uint32_t current_buffer : 1;
// DWORD 5-8
uint32_t input_buffer_0_ptr;
uint32_t input_buffer_1_ptr;
uint32_t output_buffer_ptr;
uint32_t work_buffer_ptr;
// DWORD 9
uint32_t output_buffer_read_offset : 5;
uint32_t : 25;
uint32_t stop_when_done : 1;
uint32_t interrupt_when_done : 1;
// DWORD 10-15
uint32_t unk_dwords_10_15[6];
static XmaContextData Load(const void* guest)
{
XmaContextData d;
auto* src = static_cast<const uint32_t*>(guest);
auto* dst = reinterpret_cast<uint32_t*>(&d);
for (size_t i = 0; i < sizeof(d) / 4; i++)
dst[i] = ByteSwap(src[i]);
return d;
}
void Store(void* guest) const
{
auto* src = reinterpret_cast<const uint32_t*>(this);
auto* dst = static_cast<uint32_t*>(guest);
for (size_t i = 0; i < sizeof(*this) / 4; i++)
dst[i] = ByteSwap(src[i]);
}
};
static_assert(sizeof(XmaContextData) == 64);
struct XmaLoopData
{
be<uint32_t> loopStart;
be<uint32_t> loopEnd;
uint8_t loopCount;
uint8_t loopSubframeEnd;
uint8_t loopSubframeSkip;
};
struct XmaContextInit
{
be<uint32_t> inputBuffer0Ptr;
be<uint32_t> inputBuffer0PacketCount;
be<uint32_t> inputBuffer1Ptr;
be<uint32_t> inputBuffer1PacketCount;
be<uint32_t> inputBufferReadOffset;
be<uint32_t> outputBufferPtr;
be<uint32_t> outputBufferBlockCount;
be<uint32_t> workBuffer;
be<uint32_t> subframeDecodeCount;
be<uint32_t> channelCount;
be<uint32_t> sampleRate;
XmaLoopData loopData;
};
static_assert(sizeof(XmaContextInit) == 56);
constexpr uint32_t CONTEXT_COUNT = 320; // hardware contexts
constexpr uint32_t OUTPUT_BLOCK_SIZE = 256; // bytes per output block (a subframe of one channel)
constexpr uint32_t PACKET_SIZE = 2048;
constexpr uint32_t BITS_PER_PACKET = PACKET_SIZE * 8;
constexpr uint32_t HEADER_BITS = 32;
constexpr uint32_t PAYLOAD_BITS = BITS_PER_PACKET - HEADER_BITS;
constexpr uint32_t LENGTH_BITS = 15; // frame length prefix
constexpr uint32_t SAMPLES_PER_FRAME = 512;
constexpr uint32_t BLOCKS_PER_FRAME_CHANNEL = SAMPLES_PER_FRAME * 2 / OUTPUT_BLOCK_SIZE; // 4
constexpr auto TICK = std::chrono::milliseconds(5);
struct HostState
{
bool allocated = false;
AVCodecContext* decoder = nullptr;
int decoderRate = 0, decoderChannels = 0;
// The current frame's PCM (BE s16, interleaved) and the blocks of it
// not yet in the output ring (Xenia's current_frame_remaining_subframes_).
std::vector<uint8_t> frame;
uint32_t frameBlocks = 0, frameBlocksLeft = 0;
uint32_t frameLimitBlocks = 0; // loop end: blocks of this frame to output (0 = all)
bool loopStartSkipPending = false;
// A starved pass faked a full ring: the real write offset, and the
// read offset it moved the write offset to.
bool starvedFake = false;
uint32_t starvedRealWrite = 0, starvedRead = 0;
uint64_t frames = 0;
};
// Xenia's XMA_CONTEXT_DATA / packet helpers.
uint32_t FrameOffsetField(const uint8_t* packet) // bits of the previous frame at the payload start
{
return (uint32_t(packet[0] & 3) << 13) | (uint32_t(packet[1]) << 5) | (packet[2] >> 3);
}
uint32_t SkipCount(const uint8_t* packet) { return packet[3]; }
uint32_t Bit(const uint8_t* data, uint32_t pos) { return (data[pos >> 3] >> (7 - (pos & 7))) & 1u; }
void PutBit(uint8_t* data, uint32_t pos, uint32_t v)
{
uint8_t mask = uint8_t(0x80 >> (pos & 7));
data[pos >> 3] = uint8_t(v ? data[pos >> 3] | mask : data[pos >> 3] & ~mask);
}
const AVCodec* s_codec = nullptr;
AVPacket* s_packet = nullptr;
AVFrame* s_frame = nullptr;
const bool s_silent = [] { const char* v = std::getenv("NFSMW_XMA_SILENT"); return v && v[0] == '1'; }();
std::mutex s_mutex; // serialises the API and the decoder, like register access
// NFSMW_XMA_STATS=1: every 2 s, how the decode went (under s_mutex).
struct XmaStats
{
uint64_t frames = 0, silentFrames = 0, loops = 0, ringFull = 0, starved = 0, unstarved = 0, stalls = 0, skipped = 0;
// API calls in the window
uint32_t creates = 0, releases = 0, inits = 0, enables = 0, disables = 0, inputValid = 0, outputValid = 0,
blockWhileInUse = 0, seeks = 0, loopData = 0;
};
XmaStats s_stats;
uint32_t s_contextArray = 0; // guest address of the 320-context array
HostState s_state[CONTEXT_COUNT];
std::atomic<bool> s_logStarted{ false };
// Host pointer of a physical address (0..512 MB).
uint8_t* Physical(uint32_t physicalAddress)
{
return static_cast<uint8_t*>(g_memory.Translate(vmem::Physical().Base() | (physicalAddress & 0x1FFFFFFF)));
}
// A guest buffer pointer as the context holds it (physical, as Xenia's
// GetPhysicalAddress: the 0xE0000000 window is offset by 4 KB).
uint32_t ToPhysical(uint32_t guest)
{
return guest ? MmGetPhysicalAddress(guest) : 0;
}
int ContextIndex(const void* guestContext)
{
uint32_t addr = g_memory.MapVirtual(guestContext);
if (s_contextArray == 0 || addr < s_contextArray || (addr - s_contextArray) % 64 != 0)
return -1;
uint32_t index = (addr - s_contextArray) / 64;
return index < CONTEXT_COUNT ? int(index) : -1;
}
int SampleRate(uint32_t id)
{
static const int rates[4] = { 24000, 32000, 44100, 48000 };
return rates[id & 3];
}
void ResetFrame(HostState& st)
{
st.frameBlocks = st.frameBlocksLeft = st.frameLimitBlocks = 0;
st.loopStartSkipPending = false;
st.starvedFake = false;
}
void FreeDecoder(HostState& st)
{
if (st.decoder)
avcodec_free_context(&st.decoder);
st.decoderRate = st.decoderChannels = 0;
ResetFrame(st);
}
// A decoder for one XMA stream of `channels` (1 or 2) at `rate`. A new
// (or flushed) decoder drops its first frame: that frame comes out as silence.
bool EnsureDecoder(HostState& st, int rate, int channels)
{
if (st.decoder && st.decoderRate == rate && st.decoderChannels == channels)
return true;
if (st.decoder)
avcodec_free_context(&st.decoder);
st.decoderRate = st.decoderChannels = 0;
if (!s_codec)
{
// XMA1 (packet header: 4-bit sequence number) or XMA2 (6-bit
// frame count); the frame offset and skip fields are in the same
// place, and the synthetic packets below work for both. NFSMW
// (2005) uses XMA1; NFSMW_XMA=2 switches.
const char* v = std::getenv("NFSMW_XMA");
s_codec = avcodec_find_decoder(v && v[0] == '2' ? AV_CODEC_ID_XMA2 : AV_CODEC_ID_XMA1);
s_packet = av_packet_alloc();
s_frame = av_frame_alloc();
if (!s_codec)
{
fprintf(stderr, "[xma] FFmpeg has no XMA decoder\n");
return false;
}
}
st.decoder = avcodec_alloc_context3(s_codec);
st.decoder->sample_rate = rate;
av_channel_layout_default(&st.decoder->ch_layout, channels);
st.decoder->block_align = PACKET_SIZE;
// Output every decoded sample, as the hardware does.
st.decoder->flags2 |= AV_CODEC_FLAG2_SKIP_MANUAL;
if (s_codec->id == AV_CODEC_ID_XMA2)
{
// XMA2WAVEFORMATEX extension: NumStreams = 1, ChannelMask 0.
st.decoder->extradata = static_cast<uint8_t*>(av_mallocz(34 + AV_INPUT_BUFFER_PADDING_SIZE));
st.decoder->extradata_size = 34;
st.decoder->extradata[0] = 1;
}
else
{
// XMAWAVEFORMAT: 8-byte header (NumStreams at 4) + one 20-byte
// stream entry (Channels at 17).
st.decoder->extradata = static_cast<uint8_t*>(av_mallocz(28 + AV_INPUT_BUFFER_PADDING_SIZE));
st.decoder->extradata_size = 28;
st.decoder->extradata[4] = 1;
st.decoder->extradata[8 + 17] = uint8_t(channels);
}
if (avcodec_open2(st.decoder, s_codec, nullptr) < 0)
{
fprintf(stderr, "[xma] avcodec_open2 failed (%d Hz, %d ch)\n", rate, channels);
avcodec_free_context(&st.decoder);
return false;
}
st.decoderRate = rate;
st.decoderChannels = channels;
return true;
}
int16_t ToS16(float v)
{
v = std::clamp(v, -1.0f, 1.0f);
return int16_t(std::lrintf(v * 32767.0f));
}
// --- Input buffers -------------------------------------------------------
bool BufferValid(const XmaContextData& c, uint32_t buffer)
{
return buffer ? c.input_buffer_1_valid : c.input_buffer_0_valid;
}
uint32_t BufferPtr(const XmaContextData& c, uint32_t buffer)
{
return buffer ? c.input_buffer_1_ptr : c.input_buffer_0_ptr;
}
uint32_t BufferPackets(const XmaContextData& c, uint32_t buffer)
{
return buffer ? c.input_buffer_1_packet_count : c.input_buffer_0_packet_count;
}
// Xenia's SwapInputBuffer: the current buffer is done.
void SwapInputBuffer(XmaContextData& c)
{
if (c.current_buffer)
c.input_buffer_1_valid = 0;
else
c.input_buffer_0_valid = 0;
c.current_buffer ^= 1;
c.input_buffer_read_offset = HEADER_BITS;
}
// Packet `index` counted from the current buffer's start, continuing into
// the other buffer when past its end. nullptr if it isn't there; then
// `pending` says whether it can still come (the other buffer isn't valid
// yet) or never will (past the end of both buffers).
const uint8_t* StreamPacket(const XmaContextData& c, uint32_t index, bool& pending)
{
uint32_t buffer = c.current_buffer;
uint32_t count = BufferPackets(c, buffer);
pending = false;
if (index >= count)
{
index -= count;
buffer ^= 1;
if (!BufferValid(c, buffer))
{
pending = true;
return nullptr;
}
if (index >= BufferPackets(c, buffer))
return nullptr;
}
uint32_t ptr = BufferPtr(c, buffer);
return ptr ? Physical(ptr + index * PACKET_SIZE) : nullptr;
}
// Move to the first frame starting at or after packet `index` of the
// stream (following the skip counts of packets that only continue a
// frame), swapping to the other buffer when this one ends.
void GoToNextPacket(XmaContextData& c, uint32_t index)
{
uint32_t count = BufferPackets(c, c.current_buffer);
while (index < count)
{
const uint8_t* packet = Physical(BufferPtr(c, c.current_buffer) + index * PACKET_SIZE);
uint32_t field = FrameOffsetField(packet);
if (field < PAYLOAD_BITS)
{
c.input_buffer_read_offset = index * BITS_PER_PACKET + HEADER_BITS + field;
return;
}
uint32_t skip = SkipCount(packet);
if (skip == 0xFF)
skip = 0;
index += skip + 1;
}
// Past this buffer: continue in the other one at the same stream
// position (the skip count carries across).
uint32_t carry = index - count;
SwapInputBuffer(c);
c.input_buffer_read_offset = carry * BITS_PER_PACKET + HEADER_BITS; // snapped to its first frame when decoded
}
// Copies `bits` of the stream from bit `offset` of packet `index` into
// `out` (payloads only, following skip counts). Returns the bits copied:
// fewer when a packet isn't there (`pending` as StreamPacket's).
uint32_t GatherBits(const XmaContextData& c, uint32_t index, uint32_t offset, uint32_t bits, uint8_t* out, bool& pending)
{
uint32_t copied = 0;
pending = false;
while (copied < bits)
{
const uint8_t* packet = StreamPacket(c, index, pending);
if (!packet)
break;
for (; offset < BITS_PER_PACKET && copied < bits; offset++, copied++)
PutBit(out, copied, Bit(packet, offset));
uint32_t skip = SkipCount(packet);
index += (skip == 0xFF ? 0 : skip) + 1;
offset = HEADER_BITS;
}
return copied;
}
// --- Frames --------------------------------------------------------------
// Decode the frame whose bits are in `frameBits` (length `length`) into
// st.frame. Frames FFmpeg doesn't output (the first after a fresh start,
// or a corrupt one) are silence.
void DecodeFrameBits(HostState& st, const uint8_t* frameBits, uint32_t length, int channels)
{
st.frame.assign(size_t(SAMPLES_PER_FRAME) * channels * 2, 0);
st.frames++;
s_stats.frames++;
bool got = false;
if (!s_silent && st.decoder)
{
// One XMA packet holding only this frame: first frame at bit 0 of
// the payload, its "another frame follows" bit cleared. A frame
// longer than a payload continues in a second packet, whose
// header gives the bits that belong to it. (Beyond two payloads,
// 32704 bits, FFmpeg keeps only what fits: the frame loses its
// last bits. NFSMW's longest frame is 6155 bits.)
static uint8_t packets[2][PACKET_SIZE + AV_INPUT_BUFFER_PADDING_SIZE];
uint32_t first = std::min(length, PAYLOAD_BITS);
memset(packets[0], 0, PACKET_SIZE);
for (uint32_t i = 0; i < first; i++)
PutBit(packets[0], HEADER_BITS + i, Bit(frameBits, i));
int count = 1;
if (length > PAYLOAD_BITS)
{
uint32_t rest = length - PAYLOAD_BITS;
memset(packets[1], 0, PACKET_SIZE);
packets[1][0] = uint8_t(rest >> 13);
packets[1][1] = uint8_t(rest >> 5);
packets[1][2] = uint8_t(rest << 3);
for (uint32_t i = 0; i < rest; i++)
PutBit(packets[1], HEADER_BITS + i, Bit(frameBits, PAYLOAD_BITS + i));
count = 2;
}
uint32_t trailer = length - 1; // the frame's last bit
PutBit(trailer < PAYLOAD_BITS ? packets[0] : packets[1], HEADER_BITS + (trailer < PAYLOAD_BITS ? trailer : trailer - PAYLOAD_BITS), 0);
for (int p = 0; p < count; p++)
{
av_packet_unref(s_packet);
s_packet->data = packets[p];
s_packet->size = PACKET_SIZE;
if (avcodec_send_packet(st.decoder, s_packet) != 0)
continue;
while (avcodec_receive_frame(st.decoder, s_frame) == 0)
{
int n = std::min<int>(s_frame->nb_samples, SAMPLES_PER_FRAME);
int ch = std::min(channels, s_frame->ch_layout.nb_channels);
for (int i = 0; i < n; i++)
for (int c = 0; c < channels; c++)
{
const float* plane = reinterpret_cast<const float*>(s_frame->extended_data[std::min(c, ch - 1)]);
int16_t v = ToS16(plane[i]);
size_t at = (size_t(i) * channels + c) * 2;
st.frame[at] = uint8_t(uint16_t(v) >> 8);
st.frame[at + 1] = uint8_t(v);
}
got = n > 0;
av_frame_unref(s_frame);
}
}
}
if (!got)
s_stats.silentFrames++;
st.frameBlocks = st.frameBlocksLeft = BLOCKS_PER_FRAME_CHANNEL * channels;
}
// Xenia's Decode(): the next frame of the stream into st.frame, advancing
// the read offset. False if nothing could be decoded (no input, or the
// rest of a split frame hasn't arrived).
bool DecodeFrame(HostState& st, XmaContextData& c, int channels)
{
bool loopEndFrame = false;
for (int guard = 0; guard < 64; guard++)
{
if (!c.input_buffer_0_valid && !c.input_buffer_1_valid)
return false;
if (!BufferValid(c, c.current_buffer))
{
SwapInputBuffer(c);
if (!BufferValid(c, c.current_buffer))
return false;
}
uint32_t count = BufferPackets(c, c.current_buffer);
if (count == 0 || BufferPtr(c, c.current_buffer) == 0)
{
SwapInputBuffer(c);
continue;
}
if (c.input_buffer_read_offset < HEADER_BITS)
c.input_buffer_read_offset = HEADER_BITS;
uint32_t index = c.input_buffer_read_offset / BITS_PER_PACKET;
if (index >= count)
{
GoToNextPacket(c, index);
continue;
}
const uint8_t* packet = Physical(BufferPtr(c, c.current_buffer) + index * PACKET_SIZE);
uint32_t skip = SkipCount(packet);
uint32_t nextIndex = index + (skip == 0xFF ? 0 : skip) + 1;
uint32_t field = FrameOffsetField(packet);
if (field >= PAYLOAD_BITS)
{
GoToNextPacket(c, nextIndex); // no frame starts in this packet
continue;
}
uint32_t offset = c.input_buffer_read_offset % BITS_PER_PACKET;
if (offset < HEADER_BITS + field)
{
// In the tail of a frame from the previous packet: go to the
// first frame that starts here.
offset = HEADER_BITS + field;
c.input_buffer_read_offset = index * BITS_PER_PACKET + offset;
}
// Hardware loop (Xenia's UpdateLoopStatus): at loop_end, back to
// loop_start; that frame's output is limited to subframes
// 0..loop_subframe_end, and loop_subframe_skip leading subframes
// of it are dropped. (Checked at a frame start, as Xenia's read
// offsets always are.)
if (c.loop_count > 0 && !loopEndFrame &&
c.input_buffer_read_offset == std::max(HEADER_BITS, uint32_t(c.loop_end)))
{
loopEndFrame = true;
c.input_buffer_read_offset = std::max(HEADER_BITS, uint32_t(c.loop_start));
st.loopStartSkipPending = true;
if (c.loop_count != 255)
c.loop_count--;
s_stats.loops++;
continue;
}
uint8_t lengthBits[4] = {};
bool pending = false;
if (GatherBits(c, index, offset, LENGTH_BITS, lengthBits, pending) < LENGTH_BITS)
{
if (pending)
{
// The length runs into the other buffer, not valid yet:
// wait for it, as for a split frame body.
s_stats.stalls++;
return false;
}
SwapInputBuffer(c); // nothing more can follow: this buffer is done
continue;
}
uint32_t length = (uint32_t(lengthBits[0]) << 7) | (lengthBits[1] >> 1);
if (length <= LENGTH_BITS || length == 0x7FFF)
{
GoToNextPacket(c, nextIndex); // end of the frames in this packet
continue;
}
static uint8_t frameBits[(0x7FFF + 7) / 8 + 8];
if (GatherBits(c, index, offset, length, frameBits, pending) < length)
{
if (pending)
{
s_stats.stalls++;
return false; // the rest of the frame comes with the other buffer
}
// It runs past data that can never come (corrupt, or with
// 1-packet buffers a third packet): skip it.
s_stats.skipped++;
GoToNextPacket(c, nextIndex);
continue;
}
uint32_t trailer = Bit(frameBits, length - 1);
if (!s_silent)
EnsureDecoder(st, SampleRate(c.sample_rate), channels);
DecodeFrameBits(st, frameBits, length, channels);
if (loopEndFrame)
st.frameLimitBlocks = (uint32_t(c.loop_subframe_end) + 1) * channels;
else
st.frameLimitBlocks = 0;
if (st.loopStartSkipPending)
{
uint32_t skipBlocks = uint32_t(c.loop_subframe_skip) * channels;
if (skipBlocks < st.frameBlocksLeft)
st.frameBlocksLeft -= skipBlocks;
st.loopStartSkipPending = false;
}
// Next frame: in this packet if the frame ends here and says
// another follows, else the next packet of the stream.
if (offset + length < BITS_PER_PACKET && trailer)
c.input_buffer_read_offset = index * BITS_PER_PACKET + offset + length;
else
GoToNextPacket(c, nextIndex);
return true;
}
return false;
}
// Xenia's StoreContextMerged: write back only what the decoder owns, so a
// field the game wrote to the context meanwhile isn't lost.
void StoreMerged(void* guest, const XmaContextData& data, const XmaContextData& initial)
{
XmaContextData fresh = XmaContextData::Load(guest);
fresh.loop_count = data.loop_count;
fresh.output_buffer_write_offset = data.output_buffer_write_offset;
if (initial.input_buffer_0_valid && !data.input_buffer_0_valid)
fresh.input_buffer_0_valid = 0;
if (initial.input_buffer_1_valid && !data.input_buffer_1_valid)
fresh.input_buffer_1_valid = 0;
if (initial.output_buffer_valid && !data.output_buffer_valid)
fresh.output_buffer_valid = 0;
fresh.input_buffer_read_offset = data.input_buffer_read_offset;
fresh.current_buffer = data.current_buffer;
fresh.Store(guest);
}
// Xenia's Work(): the pass for an XMAEnableContext kick.
void Service(uint32_t index)
{
HostState& st = s_state[index];
void* guest = g_memory.Translate(s_contextArray + index * 64);
XmaContextData c = XmaContextData::Load(guest);
if (st.starvedFake && (c.input_buffer_0_valid || c.input_buffer_1_valid))
{
// New input after a starved pass faked a full ring. If the game
// has read no further than the real output (it didn't use the
// fake data), undo the fake: continue at the real write offset.
const uint32_t n = c.output_buffer_block_count ? c.output_buffer_block_count : 32;
uint32_t readSoFar = (c.output_buffer_read_offset + n - st.starvedRead) % n;
uint32_t realSpan = (st.starvedRealWrite + n - st.starvedRead) % n;
if (readSoFar <= realSpan)
{
c.output_buffer_write_offset = st.starvedRealWrite;
c.output_buffer_valid = 1;
c.Store(guest);
s_stats.unstarved++;
}
st.starvedFake = false;
}
const XmaContextData initial = c;
if (!c.output_buffer_valid || c.output_buffer_ptr == 0)
return;
const bool inputAtStart = c.input_buffer_0_valid || c.input_buffer_1_valid;
const int channels = c.is_stereo ? 2 : 1;
const uint32_t count = c.output_buffer_block_count ? c.output_buffer_block_count : 32;
uint8_t* out = Physical(c.output_buffer_ptr);
uint32_t w = c.output_buffer_write_offset % count, r = c.output_buffer_read_offset % count;
uint32_t freeBlocks = w == r ? count : (r + count - w) % count;
bool wrote = false;
// Each frame yields at least one block unless a degenerate hardware
// loop drops them all: bound the frames per pass.
for (uint32_t frames = 0; frames < count + 8; frames++)
{
// 1. The current frame into the ring, block by block.
while (st.frameBlocksLeft > 0 && freeBlocks > 0)
{
uint32_t done = st.frameBlocks - st.frameBlocksLeft;
if (st.frameLimitBlocks && done >= st.frameLimitBlocks)
{
st.frameBlocksLeft = 0; // loop end: the rest of the frame isn't played
break;
}
memcpy(out + w * OUTPUT_BLOCK_SIZE, st.frame.data() + done * OUTPUT_BLOCK_SIZE, OUTPUT_BLOCK_SIZE);
w = (w + 1) % count;
freeBlocks--;
st.frameBlocksLeft--;
wrote = true;
}
if (st.frameBlocksLeft > 0 || freeBlocks == 0)
break; // ring full
// 2. The next frame.
if (!DecodeFrame(st, c, channels))
break;
}
if (inputAtStart || wrote)
c.output_buffer_write_offset = w;
else if (c.output_buffer_write_offset != c.output_buffer_read_offset)
{
// Starved: NFSMW detects a stalled voice by write == read.
if (!st.starvedFake)
{
st.starvedFake = true;
st.starvedRealWrite = c.output_buffer_write_offset;
st.starvedRead = c.output_buffer_read_offset;
}
c.output_buffer_write_offset = c.output_buffer_read_offset;
c.output_buffer_valid = 0;
s_stats.starved++;
}
if (wrote && freeBlocks == 0)
{
c.output_buffer_valid = 0; // full: the game reads, then sets it valid again
s_stats.ringFull++;
}
StoreMerged(guest, c, initial);
}
// NFSMW_XMA_LOG=1: every 2 s, the state of each allocated context.
void LogContexts()
{
static const bool enabled = std::getenv("NFSMW_XMA_LOG") != nullptr;
static auto next = std::chrono::steady_clock::now();
if (!enabled || std::chrono::steady_clock::now() < next)
return;
next += std::chrono::seconds(2);
for (uint32_t i = 0; i < CONTEXT_COUNT; i++)
{
const HostState& st = s_state[i];
if (!st.allocated)
continue;
XmaContextData c = XmaContextData::Load(g_memory.Translate(s_contextArray + i * 64));
if (!c.input_buffer_0_valid && !c.input_buffer_1_valid && !c.output_buffer_valid)
continue;
fprintf(stderr, "[xma] ctx %3u in0 %u(%u pk) in1 %u(%u pk) cur %u rd %u | out %s w %u r %u /%u | %s %dHz loop %u [%u,%u) | frames %llu\n",
i, uint32_t(c.input_buffer_0_valid), uint32_t(c.input_buffer_0_packet_count),
uint32_t(c.input_buffer_1_valid), uint32_t(c.input_buffer_1_packet_count), uint32_t(c.current_buffer),
uint32_t(c.input_buffer_read_offset), c.output_buffer_valid ? "ok" : "--", uint32_t(c.output_buffer_write_offset),
uint32_t(c.output_buffer_read_offset), uint32_t(c.output_buffer_block_count), c.is_stereo ? "st" : "mo",
SampleRate(c.sample_rate), uint32_t(c.loop_count), uint32_t(c.loop_start), uint32_t(c.loop_end),
(unsigned long long)st.frames);
}
}
void LogStats()
{
static const bool enabled = std::getenv("NFSMW_XMA_STATS") != nullptr;
static auto next = std::chrono::steady_clock::now() + std::chrono::seconds(2);
if (!enabled || std::chrono::steady_clock::now() < next)
return;
next += std::chrono::seconds(2);
uint32_t allocated = 0, withInput = 0, outValid = 0, outPending = 0;
for (uint32_t i = 0; i < CONTEXT_COUNT; i++)
{
if (!s_state[i].allocated)
continue;
allocated++;
XmaContextData c = XmaContextData::Load(g_memory.Translate(s_contextArray + i * 64));
withInput += c.input_buffer_0_valid || c.input_buffer_1_valid;
outValid += c.output_buffer_valid;
outPending += c.output_buffer_write_offset != c.output_buffer_read_offset;
}
fprintf(stderr, "[xma] stats: %llu frames (%llu silent, %llu skipped), %llu loops, %llu ring-full, %llu starved (%llu undone), %llu split-frame waits | "
"contexts: %u allocated, %u with input, %u output valid, %u unread | calls: %u create %u release %u init %u enable %u disable "
"%u input-valid %u output-valid %u block %u seek %u loop\n",
(unsigned long long)s_stats.frames, (unsigned long long)s_stats.silentFrames, (unsigned long long)s_stats.skipped,
(unsigned long long)s_stats.loops, (unsigned long long)s_stats.ringFull, (unsigned long long)s_stats.starved,
(unsigned long long)s_stats.unstarved, (unsigned long long)s_stats.stalls,
allocated, withInput, outValid, outPending, s_stats.creates, s_stats.releases, s_stats.inits, s_stats.enables,
s_stats.disables, s_stats.inputValid, s_stats.outputValid, s_stats.blockWhileInUse, s_stats.seeks, s_stats.loopData);
s_stats = {};
}
// NFSMW_XMA_LOG / NFSMW_XMA_STATS: the periodic logs. (Decoding needs no
// thread: each kick is serviced in XMAEnableContext.)
void LogThread()
{
while (true)
{
{
std::lock_guard lock(s_mutex);
LogContexts();
LogStats();
}
std::this_thread::sleep_for(TICK);
}
}
// NFSMW_XMA_LOG=1: API calls on contexts 0-3 (NFSMW_XMA_LOG_CTX=<n>: context n, "st": stereo ones).
void LogCall(const char* name, void* context, uint32_t a = 0, uint32_t b = 0)
{
static const bool enabled = std::getenv("NFSMW_XMA_LOG") != nullptr;
if (!enabled)
return;
int i = ContextIndex(context);
static const char* filter = std::getenv("NFSMW_XMA_LOG_CTX");
if (i < 0)
return;
XmaContextData c = XmaContextData::Load(context);
if (filter && filter[0] == 's' ? !c.is_stereo : filter ? i != std::atoi(filter) : i > 3)
return;
fprintf(stderr, "[xma] t%u call %s ctx %d (%08X, %08X) | in0 %u in1 %u cur %u rd %u out %u w %u r %u\n",
GuestThread::GetCurrentThreadId(), name, i, a, b,
uint32_t(c.input_buffer_0_valid), uint32_t(c.input_buffer_1_valid), uint32_t(c.current_buffer),
uint32_t(c.input_buffer_read_offset), uint32_t(c.output_buffer_valid), uint32_t(c.output_buffer_write_offset),
uint32_t(c.output_buffer_read_offset));
}
// NFSMW_XMA_STATS=1: the first loops set up, to check how the game uses
// loop_end (whether it is the offset of a frame of the stream).
void LogLoop(const char* how, const XmaContextData& c)
{
static const bool enabled = std::getenv("NFSMW_XMA_STATS") != nullptr;
static int logged = 0;
if (!enabled || c.loop_count == 0 || logged >= 32)
return;
logged++;
fprintf(stderr, "[xma] loop via %s: count %u start %u end %u subframe end %u skip %u | in0 %u pk, read %u\n", how,
uint32_t(c.loop_count), uint32_t(c.loop_start), uint32_t(c.loop_end), uint32_t(c.loop_subframe_end),
uint32_t(c.loop_subframe_skip), uint32_t(c.input_buffer_0_packet_count), uint32_t(c.input_buffer_read_offset));
}
template<typename F>
uint32_t WithContext(void* context, F&& f)
{
std::lock_guard lock(s_mutex);
XmaContextData c = XmaContextData::Load(context);
uint32_t result = f(c);
c.Store(context);
return result;
}
}
uint32_t XMACreateContext(be<uint32_t>* contextOut)
{
std::lock_guard lock(s_mutex);
s_stats.creates++;
if (s_contextArray == 0)
{
s_contextArray = vmem::Physical().Alloc(CONTEXT_COUNT * 64, 0x1000,
vmem::X_MEM_RESERVE | vmem::X_MEM_COMMIT, 0x04, true);
if (s_contextArray == 0)
return STATUS_NO_MEMORY;
}
for (uint32_t i = 0; i < CONTEXT_COUNT; i++)
{
if (!s_state[i].allocated)
{
FreeDecoder(s_state[i]);
s_state[i].allocated = true;
memset(g_memory.Translate(s_contextArray + i * 64), 0, 64);
*contextOut = s_contextArray + i * 64;
if ((std::getenv("NFSMW_XMA_LOG") || std::getenv("NFSMW_XMA_STATS")) && !s_logStarted.exchange(true))
std::thread(LogThread).detach();
return STATUS_SUCCESS;
}
}
*contextOut = 0;
return STATUS_NO_MEMORY;
}
uint32_t XMAReleaseContext(void* context)
{
std::lock_guard lock(s_mutex);
s_stats.releases++;
int i = ContextIndex(context);
if (i >= 0)
{
FreeDecoder(s_state[i]);
s_state[i].allocated = false;
memset(context, 0, 64); // as Xenia's Release
}
return 0;
}
uint32_t XMAInitializeContext(void* context, XmaContextInit* init)
{
std::lock_guard lock(s_mutex);
s_stats.inits++;
LogCall("XMAInitializeContext", context, uint32_t(init->inputBuffer0PacketCount), uint32_t(init->outputBufferBlockCount));
XmaContextData c{};
c.input_buffer_0_ptr = ToPhysical(init->inputBuffer0Ptr);
c.input_buffer_0_packet_count = init->inputBuffer0PacketCount;
c.input_buffer_1_ptr = ToPhysical(init->inputBuffer1Ptr);
c.input_buffer_1_packet_count = init->inputBuffer1PacketCount;
c.input_buffer_read_offset = init->inputBufferReadOffset;
c.output_buffer_ptr = ToPhysical(init->outputBufferPtr);
c.output_buffer_block_count = init->outputBufferBlockCount;
c.subframe_decode_count = init->subframeDecodeCount;
c.is_stereo = init->channelCount >= 1; // Xenia: channel_count is 0 for mono
c.sample_rate = init->sampleRate;
c.loop_start = init->loopData.loopStart;
c.loop_end = init->loopData.loopEnd;
c.loop_count = init->loopData.loopCount;
c.loop_subframe_end = init->loopData.loopSubframeEnd;
c.loop_subframe_skip = init->loopData.loopSubframeSkip;
c.Store(context);
LogLoop("init", c);
if (int i = ContextIndex(context); i >= 0)
{
// A new stream: no overlap from the last one, no undelivered frame.
HostState& st = s_state[i];
ResetFrame(st);
if (st.decoder)
avcodec_flush_buffers(st.decoder);
}
return 0;
}
uint32_t XMASetLoopData(void* context, XmaLoopData* loop)
{
return WithContext(context, [&](XmaContextData& c) {
s_stats.loopData++;
c.loop_start = loop->loopStart;
c.loop_end = loop->loopEnd;
c.loop_count = loop->loopCount;
c.loop_subframe_end = loop->loopSubframeEnd;
c.loop_subframe_skip = loop->loopSubframeSkip;
LogLoop("set", c);
return 0u;
});
}
uint32_t XMASetInputBufferReadOffset(void* context, uint32_t value)
{
{ std::lock_guard lock(s_mutex); LogCall("XMASetInputBufferReadOffset", context, value); }
return WithContext(context, [&](XmaContextData& c) { s_stats.seeks++; c.input_buffer_read_offset = value; return 0u; });
}
uint32_t XMASetInputBuffer0(void* context, uint32_t buffer, uint32_t packetCount)
{
{ std::lock_guard lock(s_mutex); LogCall("XMASetInputBuffer0", context, buffer, packetCount); }
return WithContext(context, [&](XmaContextData& c) {
c.input_buffer_0_ptr = ToPhysical(buffer);
c.input_buffer_0_packet_count = packetCount;
return 0u;
});
}
uint32_t XMASetInputBuffer1(void* context, uint32_t buffer, uint32_t packetCount)
{
{ std::lock_guard lock(s_mutex); LogCall("XMASetInputBuffer1", context, buffer, packetCount); }
return WithContext(context, [&](XmaContextData& c) {
c.input_buffer_1_ptr = ToPhysical(buffer);
c.input_buffer_1_packet_count = packetCount;
return 0u;
});
}
uint32_t XMAIsInputBuffer0Valid(void* context)
{
return WithContext(context, [](XmaContextData& c) { return uint32_t(c.input_buffer_0_valid); });
}
uint32_t XMAIsInputBuffer1Valid(void* context)
{
return WithContext(context, [](XmaContextData& c) { return uint32_t(c.input_buffer_1_valid); });
}
uint32_t XMASetInputBuffer0Valid(void* context)
{
{ std::lock_guard lock(s_mutex); LogCall("XMASetInputBuffer0Valid", context); }
return WithContext(context, [](XmaContextData& c) { s_stats.inputValid++; c.input_buffer_0_valid = 1; return 0u; });
}
uint32_t XMASetInputBuffer1Valid(void* context)
{
{ std::lock_guard lock(s_mutex); LogCall("XMASetInputBuffer1Valid", context); }
return WithContext(context, [](XmaContextData& c) { s_stats.inputValid++; c.input_buffer_1_valid = 1; return 0u; });
}
uint32_t XMAIsOutputBufferValid(void* context)
{
return WithContext(context, [](XmaContextData& c) { return uint32_t(c.output_buffer_valid); });
}
uint32_t XMASetOutputBufferValid(void* context)
{
{ std::lock_guard lock(s_mutex); LogCall("XMASetOutputBufferValid", context); }
return WithContext(context, [](XmaContextData& c) { s_stats.outputValid++; c.output_buffer_valid = 1; return 0u; });
}
uint32_t XMAGetOutputBufferReadOffset(void* context)
{
return WithContext(context, [](XmaContextData& c) { return uint32_t(c.output_buffer_read_offset); });
}
uint32_t XMASetOutputBufferReadOffset(void* context, uint32_t value)
{
{ std::lock_guard lock(s_mutex); LogCall("XMASetOutputBufferReadOffset", context, value); }
return WithContext(context, [&](XmaContextData& c) { c.output_buffer_read_offset = value; return 0u; });
}
uint32_t XMAGetOutputBufferWriteOffset(void* context)
{
// NFSMW_XMA_LOG: report contexts being polled hard (a player spinning).
static const bool logPolls = std::getenv("NFSMW_XMA_LOG") != nullptr;
if (logPolls)
{
static uint32_t polls[CONTEXT_COUNT];
static auto windowStart = std::chrono::steady_clock::now();
int i = ContextIndex(context);
if (i >= 0)
polls[i]++;
auto now = std::chrono::steady_clock::now();
if (now - windowStart > std::chrono::seconds(2))
{
for (uint32_t k = 0; k < CONTEXT_COUNT; k++)
if (polls[k] > 20000)
fprintf(stderr, "[xma] hot poll: ctx %u polled %u times in 2 s\n", k, polls[k]);
memset(polls, 0, sizeof(polls));
windowStart = now;
}
}
return WithContext(context, [](XmaContextData& c) { return uint32_t(c.output_buffer_write_offset); });
}
uint32_t XMAEnableContext(void* context)
{
std::lock_guard lock(s_mutex);
LogCall("XMAEnableContext", context);
s_stats.enables++;
// The hardware starts on the kick; the game's players poll for the result
// right away, so the pass runs now.
if (int i = ContextIndex(context); i >= 0)
Service(uint32_t(i));
return 0;
}
// Passes run inside XMAEnableContext, so there is nothing to stop; holding
// the decoder lock waits out a pass in progress, which is what `wait` asks.
uint32_t XMADisableContext(void* context, uint32_t wait)
{
std::lock_guard lock(s_mutex);
LogCall("XMADisableContext", context, wait);
s_stats.disables++;
return 0;
}
// "In use" means the decoder is working on the context. Our passes run under
// s_mutex, so acquiring it is enough (Xenia's version returns at once:
// work_buffer_ptr is never set). Waiting for the input buffers to drain, as
// before, hung forever: a disabled context is never decoded (XAudio disables,
// then blocks, when it requeues or seeks a playing voice).
uint32_t XMABlockWhileInUse(void* context)
{
std::lock_guard lock(s_mutex);
s_stats.blockWhileInUse++;
LogCall("XMABlockWhileInUse", context);
return 0;
}
GUEST_FUNCTION_HOOK(__imp__XMACreateContext, XMACreateContext);
GUEST_FUNCTION_HOOK(__imp__XMAReleaseContext, XMAReleaseContext);
GUEST_FUNCTION_HOOK(__imp__XMAInitializeContext, XMAInitializeContext);
GUEST_FUNCTION_HOOK(__imp__XMASetLoopData, XMASetLoopData);
GUEST_FUNCTION_HOOK(__imp__XMASetInputBufferReadOffset, XMASetInputBufferReadOffset);
GUEST_FUNCTION_HOOK(__imp__XMASetInputBuffer0, XMASetInputBuffer0);
GUEST_FUNCTION_HOOK(__imp__XMASetInputBuffer1, XMASetInputBuffer1);
GUEST_FUNCTION_HOOK(__imp__XMAIsInputBuffer0Valid, XMAIsInputBuffer0Valid);
GUEST_FUNCTION_HOOK(__imp__XMAIsInputBuffer1Valid, XMAIsInputBuffer1Valid);
GUEST_FUNCTION_HOOK(__imp__XMASetInputBuffer0Valid, XMASetInputBuffer0Valid);
GUEST_FUNCTION_HOOK(__imp__XMASetInputBuffer1Valid, XMASetInputBuffer1Valid);
GUEST_FUNCTION_HOOK(__imp__XMAIsOutputBufferValid, XMAIsOutputBufferValid);
GUEST_FUNCTION_HOOK(__imp__XMASetOutputBufferValid, XMASetOutputBufferValid);
GUEST_FUNCTION_HOOK(__imp__XMAGetOutputBufferReadOffset, XMAGetOutputBufferReadOffset);
GUEST_FUNCTION_HOOK(__imp__XMASetOutputBufferReadOffset, XMASetOutputBufferReadOffset);
GUEST_FUNCTION_HOOK(__imp__XMAGetOutputBufferWriteOffset, XMAGetOutputBufferWriteOffset);
GUEST_FUNCTION_HOOK(__imp__XMAEnableContext, XMAEnableContext);
GUEST_FUNCTION_HOOK(__imp__XMADisableContext, XMADisableContext);
GUEST_FUNCTION_HOOK(__imp__XMABlockWhileInUse, XMABlockWhileInUse);