chore: remove previously tracked scratch/backup files from index

Remove backup/, build logs, .bak files, and other workspace artifacts
that should have been gitignored from the start. Working tree is now
clean for rebase.
This commit is contained in:
salh
2026-04-17 20:39:32 +03:00
parent 5299511a5b
commit 2e099f544e
164 changed files with 2 additions and 340917 deletions
+2 -2
View File
@@ -1,4 +1,4 @@
[submodule "thirdparty/rexglue-sdk"]
[submodule "thirdparty/rexglue-sdk"]
path = thirdparty/rexglue-sdk
url = https://github.com/rapidsamphire/rexglue-sdk.git
branch = ac6recomp-fixes
branch = ac6recomp-fixes
-28
View File
@@ -1,28 +0,0 @@
# ac6recomp - ReXGlue Recompiled Project
#
# This file is yours to edit. 'rexglue migrate' will NOT overwrite it.
# SDK boilerplate lives in generated/rexglue.cmake.
cmake_minimum_required(VERSION 3.25)
project(ac6recomp LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 23)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
include(generated/rexglue.cmake)
# Sources
set(AC6RECOMP_SOURCES
src/main.cpp
src/render_hooks.cpp
src/d3d_hooks.cpp
)
if(WIN32)
add_executable(ac6recomp WIN32 ${AC6RECOMP_SOURCES})
else()
add_executable(ac6recomp ${AC6RECOMP_SOURCES})
endif()
rexglue_setup_target(ac6recomp)
-791
View File
@@ -1,791 +0,0 @@
# AC6 Graphics Native Master Plan
Date: 2026-04-14
Repo: `C:\AC6Recomp_ext`
## Objective
Replace the current emulator-era graphics stack with an AC6-specific native graphics stack so the host OS and host GPU own frame construction, resource lifetime, presentation, and asset loading like a native PC port.
This plan is not "make the placeholder renderer look better."
This is a migration from:
- guest D3D calls feeding Xenos-style command emulation
- PM4/ringbuffer/MMIO frame submission
- shared-memory resource interpretation on the hot path
- runtime shader translation on the hot path
- EDRAM/resolve ownership emulation
to:
- native frame graph execution
- native D3D12 resource ownership
- native asset loading and override layers
- native shader binaries and render pipelines
- native present path
The immediate bug targets remain:
- invisible missile trails
- pixelated clouds
- pixelated explosions
- other effect/composite mismatches caused by the emulated render path
The broader end-state target is:
- a host-native graphics subsystem that behaves like a native port
- assets visible to the host filesystem and host tooling
- mod-friendly model and texture replacement workflows
## Important Scope Boundary
The title can become native in graphics and asset ownership without instantly deleting every runtime shim in one step.
In practice:
- graphics, presentation, and asset/resource ownership can become native
- title services can be narrowed to thin AC6-specific ABI shims
- PPC/runtime/kernel glue can be reduced aggressively
- some ABI compatibility surface may still exist during transition
If the goal is "the OS owns the game like a native port," the key requirement is that graphics assets and rendering stop depending on guest GPU memory and Xenos command semantics on the hot path.
That is the priority.
## Current Live Renderer State
### 1. The current renderer is still emulator-authoritative
The live path is still:
`guest D3D -> AC6 hook capture/shadow -> original guest D3D execution -> VdSwap -> HandleVideoSwap -> PM4_XE_SWAP fallback -> command processor -> D3D12 IssueSwap -> Presenter`
Key code:
- `src/ac6recomp_app.h:30`
- `src/ac6_native_graphics.cpp:654`
- `src/ac6_native_graphics.cpp:733`
- `src/ac6_native_graphics.cpp:785`
- `thirdparty/rexglue-sdk/src/kernel/xboxkrnl/xboxkrnl_video.cpp:434`
- `thirdparty/rexglue-sdk/src/kernel/xboxkrnl/xboxkrnl_video.cpp:500`
- `thirdparty/rexglue-sdk/src/kernel/xboxkrnl/xboxkrnl_video.cpp:522`
- `thirdparty/rexglue-sdk/src/graphics/command_processor.cpp:1076`
- `thirdparty/rexglue-sdk/src/graphics/d3d12/command_processor.cpp:1923`
Meaning:
- the native backend can observe and optionally intercept swap
- but the real frame is still built by the Xenia-derived path unless takeover is explicitly enabled
- even then, current takeover is only a replay-preview scaffold, not true AC6 rendering
### 2. AC6-local graphics code is capture and heuristic scaffolding
Current AC6-local capture records:
- draw/clear/resolve events
- shadow state for RT/DS/textures/streams/samplers/fetch constants
- per-frame summaries and pass heuristics
Key code:
- `src/d3d_state.h:97`
- `src/d3d_state.h:152`
- `src/d3d_hooks.cpp:272`
- `src/d3d_hooks.cpp:783`
- `src/ac6_native_graphics.cpp:355`
- `src/ac6_native_graphics.cpp:592`
What is missing for real native replay:
- shader identity capture robust enough for direct native shader selection
- float/bool/loop constant capture
- blend/raster/depth state capture
- index/vertex layout reconstruction beyond basic pointers/strides
- actual resource contents and ownership
- stable material classification
- explicit effect-system semantics
### 3. Presentation is already partly native, but not frame construction
The presenter stack is already host-native enough to keep for now:
- `thirdparty/rexglue-sdk/src/native/ui/presenter.cpp`
- `thirdparty/rexglue-sdk/src/native/ui/d3d12/d3d12_presenter.cpp`
That code is not the main blocker.
The real blocker is that the source image being presented still comes from an emulated graphics core.
### 4. Remaining visible bugs still map to effect-path subsystems
Missile trails remain strongly consistent with memexport/resource coherency problems.
Relevant code:
- `thirdparty/rexglue-sdk/src/graphics/util/draw.cpp:722`
- `thirdparty/rexglue-sdk/src/graphics/d3d12/command_processor.cpp:2626`
- `thirdparty/rexglue-sdk/src/graphics/d3d12/command_processor.cpp:2643`
- `thirdparty/rexglue-sdk/src/graphics/d3d12/command_processor.cpp:2803`
- `thirdparty/rexglue-sdk/src/graphics/d3d12/command_processor.cpp:2813`
Cloud/explosion pixelation still maps primarily to texture format and filtering behavior:
- `thirdparty/rexglue-sdk/src/graphics/d3d12/texture_cache.cpp:96`
- `thirdparty/rexglue-sdk/src/graphics/d3d12/texture_cache.cpp:427`
Conclusion:
- presenter work alone is not enough
- frame ownership and effect-path resource ownership are the real migration target
## Native End-State Definition
The target state is not "less emulation."
The target state is:
1. AC6 rendering is described in native render passes, not Xenos packets.
2. AC6 resources are native D3D12 resources owned by host systems.
3. Textures/models/materials are loaded through native asset pipelines visible to the OS.
4. Guest GPU shared memory is no longer the primary rendering source.
5. Runtime shader translation is no longer required on the hot path for known AC6 workloads.
6. `VdSwap` is only a compatibility boundary, not the real render submission mechanism.
7. The presenter consumes a native AC6 backbuffer/final composite, not a swap texture extracted from emulated state.
8. Loose-file and packaged asset overrides are supported for texture/model swaps.
## "OS Owns The Game" Translation
The phrase "the OS owns the game like a native port" needs a concrete engineering meaning.
For this project, that means:
- the executable is a host-native process with host-native rendering
- host filesystems provide the authoritative asset source
- host tools can inspect, replace, and override assets without guest GPU-memory surgery
- the host graphics API owns texture/buffer residency and synchronization
- the host renderer owns shader/pipeline creation
- the host windowing/presentation layer owns swapchain behavior
It does not literally require deleting every compatibility shim on day one.
It does require making the following things host-authoritative:
- asset discovery
- asset loading
- resource decoding
- resource creation
- frame submission
- final composition
## Native Asset Ownership And Modding Target
This is a first-class requirement, not an optional post-launch feature.
### Required outcome
Users must be able to do:
- texture swaps
- model swaps
- material swaps
- effect texture replacements
- optional loose-file overrides during development
- packaged mod loading for distribution
without editing guest memory or relying on emulator-specific patching.
### Required architecture
The native renderer and asset pipeline must introduce:
1. Native asset registry
- canonical asset IDs for textures, models, materials, shaders, and effect resources
- mapping from legacy AC6 asset references to host-native asset IDs
2. Native filesystem-backed content roots
- base game extracted content root
- optional update/DLC root
- optional mod root stack
- deterministic precedence order
3. Override resolver
- loose files override packaged content
- mod packages override base extracted assets
- conflict resolution rules are deterministic and logged
4. Import/conversion pipeline
- texture decode to host-authoritative intermediate or final GPU-ready formats
- model conversion to native mesh/skin/material structures
- material/effect descriptor extraction
- shader/effect metadata extraction where possible
5. Stable resource manifests
- asset ID
- original source path/container reference
- converted output path
- hashes for invalidation
- dependency graph
6. Runtime resource cache
- native texture cache keyed by asset ID and variant
- native mesh cache keyed by asset ID and LOD/variant
- native material cache keyed by asset ID and permutation
### Modding workflow target
The intended modding workflow should become:
1. Extract or convert base AC6 assets into a host-visible content tree.
2. Build a manifest that maps game asset references to native asset IDs.
3. Allow `mods/<modname>/...` content roots with identical asset IDs or manifest aliases.
4. Resolve overrides before resource creation.
5. Rebuild only affected native caches when assets change.
For development, support should exist for:
- startup scan of mod roots
- verbose override logging
- optional hot-reload for textures/materials in debug builds
Hot-reload for skeletal models can come later if needed, but the file format and ownership model should support it from the start.
## Architecture Principles
### Principle 1: Keep the host-native UI/presenter stack where it already works
Do not rewrite:
- windowing
- surface ownership
- basic presenter shell
- immediate drawer / overlays
unless the native frame path proves they are structurally blocking.
### Principle 2: Remove graphics emulation from the hot path before chasing total runtime deletion
The first big win is:
- no PM4/ringbuffer/MMIO submission on the frame hot path
- no shared-memory texture interpretation on the frame hot path
- no shader translation on the frame hot path
### Principle 3: Make effects a first-wave native target
Opaque geometry is not enough.
The migration must explicitly prioritize:
- missile trails
- clouds
- explosions
- fullscreen composites
- post-processing
### Principle 4: Asset ownership and renderer ownership must migrate together
If rendering becomes native but assets still come from guest-style GPU memory interpretation, modding remains weak and effect bugs remain harder to isolate.
### Principle 5: D3D12 only first
No Vulkan parity work until:
- D3D12 native frame ownership is stable
- effects are correct
- asset override workflows exist
- performance is acceptable
## Major Systems To Retire
These are the emulator-era graphics systems that must eventually leave the default path:
1. PM4/ringbuffer submission path
- `thirdparty/rexglue-sdk/src/kernel/xboxkrnl/xboxkrnl_video.cpp`
- `thirdparty/rexglue-sdk/src/graphics/command_processor.cpp`
2. Xenos command processor execution
- `thirdparty/rexglue-sdk/src/graphics/command_processor.cpp`
- `thirdparty/rexglue-sdk/src/graphics/d3d12/command_processor.cpp`
3. Shared-memory guest resource interpretation on the render hot path
- `thirdparty/rexglue-sdk/include/rex/graphics/shared_memory.h`
- `thirdparty/rexglue-sdk/src/graphics/d3d12/shared_memory.*`
4. Texture decode/fetch behavior driven by guest texture state
- `thirdparty/rexglue-sdk/src/graphics/d3d12/texture_cache.cpp`
5. Shader microcode translation on the render hot path
- `thirdparty/rexglue-sdk/src/graphics/pipeline/shader/*`
6. EDRAM/render-target ownership transfer emulation
- `thirdparty/rexglue-sdk/src/graphics/d3d12/render_target_cache.cpp`
7. Swap-texture extraction as the final present source
- `thirdparty/rexglue-sdk/src/graphics/d3d12/command_processor.cpp:1923`
## Major Native Systems To Add
The native port path needs new systems, not only removals.
### 1. Native asset subsystem
Proposed tree:
- `src/native/assets/*`
- `src/native/assets/import/*`
- `src/native/assets/runtime/*`
- `src/native/assets/mods/*`
Responsibilities:
- asset registry
- manifests
- content roots
- override resolution
- conversion products
- host-visible metadata
### 2. Native graphics subsystem
Proposed tree:
- `src/native/graphics/*`
- `src/native/graphics/d3d12/*`
- `src/native/graphics/framegraph/*`
- `src/native/graphics/resources/*`
- `src/native/graphics/effects/*`
Responsibilities:
- renderer init/shutdown
- frame execution
- pass graph
- resource lifetime
- final composite
- integration with presenter
### 3. Native shader subsystem
Proposed tree:
- `src/native/graphics/shaders/*`
- `assets/native/shaders/*`
Responsibilities:
- shader fingerprint database
- native HLSL/DXIL compilation
- permutation management
- compatibility fallback during transition
### 4. Native effect subsystem
Proposed tree:
- `src/native/graphics/effects/trails/*`
- `src/native/graphics/effects/particles/*`
- `src/native/graphics/effects/clouds/*`
- `src/native/graphics/effects/explosions/*`
Responsibilities:
- native effect buffer formats
- native effect update and draw paths
- correct filtering/blending/composite behavior
## Phased Migration Plan
### Phase 0: Lock the migration target
Deliverables:
- declare D3D12 as the only first-wave native graphics backend
- declare asset ownership/modding as a core scope item
- freeze new feature work inside the emulator graphics path except bug containment and required diagnostics
Exit criteria:
- master plan accepted as the working architecture target
### Phase 1: Expand capture from heuristic to reconstruction-grade
Current capture is insufficient for native replay.
Add:
- vertex shader identity
- pixel shader identity
- float constant blocks
- bool/loop constant state
- blend/depth/raster state
- scissor state
- index format/base/size
- vertex declaration details
- texture descriptors, not just texture pointers
- resolve source/destination descriptors
- memexport stream descriptors
Potential touch points:
- `src/d3d_hooks.cpp`
- `src/d3d_state.h`
Exit criteria:
- a single captured frame can be serialized into a deterministic AC6 frame description with enough data to classify passes and drive partial native replay
### Phase 2: Build a native asset registry and conversion pipeline
This is the first real "OS owns the game" milestone.
Add:
- extracted content root conventions
- manifest format
- conversion database
- texture import path
- model import path
- material import path
- mod override root stack
Requirements:
- all textures used by swap/scene/effects can be resolved by host asset ID
- converted outputs are reproducible
- overrides are logged
Exit criteria:
- texture swaps work through loose files or mod roots before the full native renderer is complete
- at least one model class can be replaced by host asset override
### Phase 3: Break hard dependency on PM4 swap for frame ownership
Replace `VdSwap` behavior progressively:
- keep callback semantics
- keep guest frame-boundary expectations
- stop requiring synthetic `PM4_XE_SWAP` for normal presentation
Required code:
- `thirdparty/rexglue-sdk/src/kernel/xboxkrnl/xboxkrnl_video.cpp`
- `thirdparty/rexglue-sdk/include/rex/system/interfaces/graphics.h`
Exit criteria:
- normal frame present can complete without injecting swap packets into the command processor
### Phase 4: Stand up a real native renderer skeleton
Build a native renderer that owns:
- device-facing render resources
- command recording
- transient buffers
- transient render targets
- final backbuffer handoff to presenter
Do not aim for parity yet.
Initial goal:
- framegraph shell
- pass scheduling
- native swap/present ownership
- debug visualization of native passes
Exit criteria:
- a native frame can be submitted and presented without going through the emulated swap-texture path
### Phase 5: Native texture ownership and effect texture correctness
Make texture creation host-authoritative.
Tasks:
- move texture decode/import from emulated fetch-time interpretation to asset import/load time where possible
- establish native sampler/filter policy per asset/material/effect use
- preserve exact handling for AC6 effect-critical formats
- separate scene textures from effect/intermediate textures
Why this phase matters:
- clouds and explosions are directly tied to texture correctness and filtering
Exit criteria:
- cloud/explosion textures are loaded through native resource ownership
- the native renderer can sample them without relying on the D3D12 emulation texture cache
### Phase 6: Native memexport/effect-buffer replacement
This phase is the key to fixing missile trails.
Tasks:
- identify all AC6 passes using memexport-driven effect data
- replace shader-to-guest-memory export semantics with native structured buffers or typed effect buffers
- preserve any CPU-visible side effects only where the game actually consumes them
- create a compatibility bridge for transitional cases
Important rule:
- do not keep guest shared memory as the authoritative effect buffer source if the goal is native ownership
Exit criteria:
- missile trails are produced from native effect buffers
- the effect no longer depends on CPU readback from memexport ranges
### Phase 7: Native shader strategy
Move away from runtime microcode translation for known AC6 workloads.
Tasks:
- fingerprint hot shaders from capture
- cluster by real AC6 material/effect role
- hand-author or generate stable native HLSL/DXIL equivalents
- keep translator-backed fallback only for unknown signatures during transition
Priority order:
1. final composite / present-adjacent passes
2. clouds/explosions/trails
3. HUD/post
4. opaque scene passes
Exit criteria:
- effect and final composite passes no longer require runtime Xbox shader translation
### Phase 8: Replace EDRAM/resolve emulation with an explicit native framegraph
The native renderer must explicitly own:
- scene color/depth targets
- transparent/effect intermediate targets
- post targets
- resolves
- composites
- final output
Tasks:
- define transient render target pools
- define pass resource dependencies
- define barriers explicitly
- stop inheriting Xenos ownership-transfer behavior
Exit criteria:
- frame execution is graph-defined rather than inferred from guest EDRAM semantics
### Phase 9: Cut over default rendering
At this point:
- native renderer is default
- emulated graphics path remains behind a debug flag only
- presenter consumes native final output
Exit criteria:
- normal gameplay does not require the emulated graphics backend
### Phase 10: Retire emulator-era graphics code from default build
Remove default reliance on:
- PM4 swap synthesis
- command processor frame execution
- swap texture extraction
- shared-memory hot-path rendering
- shader translation hot path
- EDRAM render-target ownership logic
Keep only:
- minimal compatibility stubs required by the remaining runtime surface
Exit criteria:
- the graphics hot path is native-owned end to end
## Modding Plan In Detail
### Texture swaps
Minimum supported workflow:
- extract/convert base textures to a host-visible format tree
- map original asset IDs to converted file paths
- allow mod roots to override by asset ID
- create native GPU textures from overridden files at startup
Preferred formats:
- lossless intermediates for archival and conversion stability
- GPU-compressed deliverables where practical
### Model swaps
Minimum supported workflow:
- convert mesh containers into host-native mesh format
- preserve material slot mapping
- preserve skeleton/bone naming if animated
- preserve collision/attachment metadata if required by gameplay
A model override should be allowed only if:
- required material slots exist
- required bones/attachments exist for animated assets
- bounds and dependency validation pass
### Material/effect swaps
Minimum supported workflow:
- allow material descriptors to override texture bindings, blend modes, and shader variants within validated limits
### Manifest design
Each asset should have:
- stable asset ID
- source container/path
- asset class
- dependency list
- converted path
- mod override path if active
- hash/version metadata
### Safety and diagnostics
The runtime should log:
- which overrides were applied
- which overrides were rejected
- why they were rejected
This is necessary for actual mod usability.
## Validation Matrix
Validation must be scenario-driven, not just boot-driven.
### Core scenarios
- boot to menu
- hangar
- mission start
- heavy cloud map
- explosion-heavy combat
- repeated missile firing with trails in view
- cutscene with HUD transitions
- fullscreen and windowed
- alt-tab and resize
### Graphics gates
- clouds no longer pixelated
- explosions no longer pixelated
- missile trails visible and stable
- HUD correct
- post effects correct
- no black present frames
- no persistent flicker
### Native-ownership gates
- texture override works from host filesystem
- at least one model override works from host filesystem
- asset override precedence is deterministic
- no guest-memory patching required for those swaps
### Performance gates
- lower CPU overhead than the PM4 path in identical scenes
- fewer shader-compile hitches
- fewer readback-induced stalls
- stable frame pacing with FPS unlock on and off
## Risks
### Risk 1: shader replacement scope explodes
Mitigation:
- target stable hot passes first
- keep fallback path temporarily
- fingerprint and cluster before rewriting
### Risk 2: AC6 still depends on guest-visible side effects in obscure places
Mitigation:
- add compatibility bridges only where verified
- do not keep broad shared-memory semantics just because they are convenient
### Risk 3: model swap support is blocked by opaque asset formats
Mitigation:
- build extraction/conversion tooling in parallel with renderer work
- define intermediate host-native formats early
### Risk 4: a half-native state becomes permanent
Mitigation:
- define retirement criteria for each old subsystem
- do not accept "native preview" as done
## Recommended Immediate Work Order
This is the highest-value sequence from the current repo state.
1. Expand AC6 capture so it records reconstruction-grade draw/material/shader state.
2. Build the native asset registry and content-root override system.
3. Add texture override support first to prove host-native asset ownership.
4. Rework `VdSwap` so native frame boundaries can complete without PM4 swap synthesis.
5. Build the native framegraph shell and presenter handoff.
6. Replace memexport-driven trail buffers with native effect buffers.
7. Replace cloud/explosion texture handling with native-owned effect textures and material paths.
8. Introduce native shader replacements for effect/composite passes.
9. Move opaque scene rendering into the native path.
10. Cut over default rendering and retire emulator graphics code.
## Definition Of Success
The migration is successful when:
- the graphics hot path does not depend on Xenos packet execution
- the presenter is fed by a native AC6 final image
- clouds, explosions, and missile trails render correctly in the native path
- textures and models can be swapped through host-native asset overrides
- the game behaves like a native PC title from the perspective of rendering, assets, and presentation
At that point, the OS effectively "owns" the game in the way that matters:
- host filesystem owns content discovery
- host GPU API owns rendering
- host resource systems own assets
- mods operate through native content overrides instead of emulator-specific memory behavior
-6
View File
@@ -1,6 +0,0 @@
set CLAUDE_CODE_USE_OPENAI=1
set OPENAI_API_KEY=gg-gcli-t4ABavgSCLVd0yqCr-Kx0W2O8vd3xUpXO0Hyld9GIhg
set OPENAI_BASE_URL=https://gcli.ggchan.dev/v1
set OPENAI_MODEL=gemini-3.1-pro-preview
openclaude
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -1,369 +0,0 @@
#include "ac6_audio_policy.h"
#include <algorithm>
#include <chrono>
#include <mutex>
#include <vector>
#include <rex/cvar.h>
#include <rex/logging.h>
REXCVAR_DEFINE_BOOL(ac6_audio_deep_trace, false, "AC6",
"Enable high-volume AC6 audio diagnostics across AC6 hooks, kernel audio, "
"worker cadence, and host queue telemetry");
REXCVAR_DEFINE_BOOL(ac6_unlock_fps_video_safe, true, "AC6",
"Keep stock timing while AC6 movie-audio clients are active");
REXCVAR_DEFINE_BOOL(ac6_movie_audio_trace, false, "AC6",
"Trace AC6 movie-audio registration, submission, and timing transitions");
REXCVAR_DEFINE_BOOL(ac6_movie_audio_trace_verbose, false, "AC6",
"Trace AC6 movie-audio cadence, draw stats, and per-frame timing while "
"movie audio is active");
REXCVAR_DEFINE_BOOL(ac6_movie_audio_force_sync_dispatch, true, "AC6",
"Force AC6 movie-audio callbacks to run their update path synchronously "
"instead of handing work to cutscene worker threads");
REXCVAR_DECLARE(bool, ac6_timing_hooks_enabled);
using Clock = std::chrono::steady_clock;
namespace {
struct MovieAudioState {
uint32_t owner_ptr{0};
uint32_t callback_ptr{0};
uint32_t callback_arg{0};
uint32_t driver_ptr{0};
uint32_t last_samples_ptr{0};
uint64_t register_count{0};
uint64_t unregister_count{0};
uint64_t submit_count{0};
Clock::time_point last_register{};
Clock::time_point last_submit{};
};
std::mutex g_movie_audio_mutex;
std::vector<MovieAudioState> g_movie_audio_clients{};
bool g_movie_audio_last_reported_active{false};
uint64_t g_movie_audio_duplicate_events{0};
uint64_t g_movie_audio_active_frame_trace_count{0};
double MillisecondsBetween(const Clock::time_point newer,
const Clock::time_point older) {
if (older.time_since_epoch().count() == 0 ||
newer.time_since_epoch().count() == 0) {
return -1.0;
}
return std::chrono::duration<double, std::milli>(newer - older).count();
}
MovieAudioState* FindMovieAudioClientLocked(const uint32_t owner_ptr,
const uint32_t driver_ptr) {
if (driver_ptr != 0) {
for (auto& client : g_movie_audio_clients) {
if (client.driver_ptr == driver_ptr) {
return &client;
}
}
}
if (owner_ptr != 0) {
for (auto& client : g_movie_audio_clients) {
if (client.owner_ptr == owner_ptr) {
return &client;
}
}
}
return nullptr;
}
MovieAudioState& UpsertMovieAudioClientLocked(const uint32_t owner_ptr,
const uint32_t driver_ptr) {
if (auto* existing = FindMovieAudioClientLocked(owner_ptr, driver_ptr)) {
return *existing;
}
g_movie_audio_clients.emplace_back();
return g_movie_audio_clients.back();
}
const MovieAudioState* SelectPrimaryMovieAudioClientLocked() {
if (g_movie_audio_clients.empty()) {
return nullptr;
}
return &*std::max_element(
g_movie_audio_clients.begin(), g_movie_audio_clients.end(),
[](const MovieAudioState& lhs, const MovieAudioState& rhs) {
const auto lhs_activity =
lhs.last_submit.time_since_epoch().count() != 0 ? lhs.last_submit
: lhs.last_register;
const auto rhs_activity =
rhs.last_submit.time_since_epoch().count() != 0 ? rhs.last_submit
: rhs.last_register;
return lhs_activity < rhs_activity;
});
}
bool ComputeMovieAudioActiveLocked() {
return !g_movie_audio_clients.empty();
}
void ReportMovieAudioStateTransitionLocked() {
const bool active = ComputeMovieAudioActiveLocked();
if (active == g_movie_audio_last_reported_active) {
return;
}
g_movie_audio_last_reported_active = active;
if (!active) {
g_movie_audio_active_frame_trace_count = 0;
}
if (!(REXCVAR_GET(ac6_movie_audio_trace) ||
ac6::audio_policy::IsDeepTraceEnabled())) {
return;
}
const MovieAudioState* primary = SelectPrimaryMovieAudioClientLocked();
double since_submit_ms = -1.0;
double since_register_ms = -1.0;
if (primary) {
const auto now = Clock::now();
since_submit_ms = MillisecondsBetween(now, primary->last_submit);
since_register_ms = MillisecondsBetween(now, primary->last_register);
}
REXAPU_DEBUG(
"AC6 movie-audio timing {} active_clients={} primary_owner={:08X} primary_driver={:08X} "
"since_submit_ms={:.3f} since_register_ms={:.3f}",
active ? "enabled" : "restored",
g_movie_audio_clients.size(),
primary ? primary->owner_ptr : 0u,
primary ? primary->driver_ptr : 0u,
since_submit_ms,
since_register_ms);
}
void MaybeReportDuplicateMovieAudioClientsLocked(const char* reason) {
if (g_movie_audio_clients.size() <= 1 ||
!(REXCVAR_GET(ac6_movie_audio_trace) ||
ac6::audio_policy::IsDeepTraceEnabled())) {
return;
}
++g_movie_audio_duplicate_events;
const MovieAudioState* primary = SelectPrimaryMovieAudioClientLocked();
REXAPU_WARN(
"AC6 movie-audio duplicate clients after {}: active_clients={} duplicate_events={} "
"primary_owner={:08X} primary_driver={:08X}",
reason,
g_movie_audio_clients.size(),
g_movie_audio_duplicate_events,
primary ? primary->owner_ptr : 0u,
primary ? primary->driver_ptr : 0u);
}
} // namespace
namespace ac6::audio_policy {
bool IsDeepTraceEnabled() {
return REXCVAR_GET(ac6_audio_deep_trace);
}
bool IsMovieAudioActive() {
std::lock_guard<std::mutex> lock(g_movie_audio_mutex);
ReportMovieAudioStateTransitionLocked();
return ComputeMovieAudioActiveLocked();
}
bool ShouldKeepStockTimingForMovieAudio() {
return REXCVAR_GET(ac6_timing_hooks_enabled) &&
REXCVAR_GET(ac6_unlock_fps_video_safe) && IsMovieAudioActive();
}
MovieAudioSnapshot GetMovieAudioSnapshot() {
std::lock_guard<std::mutex> lock(g_movie_audio_mutex);
ReportMovieAudioStateTransitionLocked();
uint64_t register_count = 0;
uint64_t submit_count = 0;
for (const auto& client : g_movie_audio_clients) {
register_count += client.register_count;
submit_count += client.submit_count;
}
const MovieAudioState* primary = SelectPrimaryMovieAudioClientLocked();
return MovieAudioSnapshot{
ComputeMovieAudioActiveLocked(),
static_cast<uint32_t>(g_movie_audio_clients.size()),
register_count,
submit_count,
primary ? primary->owner_ptr : 0u,
primary ? primary->driver_ptr : 0u,
};
}
void OnMovieAudioClientRegistered(const uint32_t owner_ptr,
const uint32_t callback_ptr,
const uint32_t callback_arg,
const uint32_t driver_ptr) {
const auto now = Clock::now();
std::lock_guard<std::mutex> lock(g_movie_audio_mutex);
MovieAudioState& client = UpsertMovieAudioClientLocked(owner_ptr, driver_ptr);
const double since_prev_register_ms =
MillisecondsBetween(now, client.last_register);
const double since_prev_submit_ms =
MillisecondsBetween(now, client.last_submit);
client.owner_ptr = owner_ptr;
client.callback_ptr = callback_ptr;
client.callback_arg = callback_arg;
client.driver_ptr = driver_ptr;
client.register_count++;
client.last_register = now;
if (REXCVAR_GET(ac6_movie_audio_trace) || IsDeepTraceEnabled()) {
REXAPU_DEBUG(
"AC6 movie-audio register owner={:08X} callback={:08X} arg={:08X} driver={:08X} "
"registers={} submits={} active_clients={} since_prev_register_ms={:.3f} "
"since_prev_submit_ms={:.3f}",
owner_ptr,
callback_ptr,
callback_arg,
driver_ptr,
client.register_count,
client.submit_count,
g_movie_audio_clients.size(),
since_prev_register_ms,
since_prev_submit_ms);
}
MaybeReportDuplicateMovieAudioClientsLocked("register");
ReportMovieAudioStateTransitionLocked();
}
void OnMovieAudioClientUnregistered(const uint32_t owner_ptr,
const uint32_t driver_ptr) {
std::lock_guard<std::mutex> lock(g_movie_audio_mutex);
auto it = std::remove_if(
g_movie_audio_clients.begin(), g_movie_audio_clients.end(),
[owner_ptr, driver_ptr](const MovieAudioState& client) {
const bool matches_owner =
owner_ptr != 0 && owner_ptr == client.owner_ptr;
const bool matches_driver =
driver_ptr != 0 && driver_ptr == client.driver_ptr;
return matches_owner || matches_driver;
});
if (it == g_movie_audio_clients.end()) {
return;
}
if (REXCVAR_GET(ac6_movie_audio_trace) || IsDeepTraceEnabled()) {
for (auto iter = it; iter != g_movie_audio_clients.end(); ++iter) {
const double since_submit_ms =
MillisecondsBetween(Clock::now(), iter->last_submit);
REXAPU_DEBUG(
"AC6 movie-audio unregister owner={:08X} driver={:08X} submits={} registers={} "
"unregisters={} since_submit_ms={:.3f}",
iter->owner_ptr,
iter->driver_ptr,
iter->submit_count,
iter->register_count,
iter->unregister_count + 1,
since_submit_ms);
}
}
for (auto iter = it; iter != g_movie_audio_clients.end(); ++iter) {
iter->unregister_count++;
}
g_movie_audio_clients.erase(it, g_movie_audio_clients.end());
ReportMovieAudioStateTransitionLocked();
}
void OnMovieAudioFrameSubmitted(const uint32_t owner_ptr,
const uint32_t driver_ptr,
const uint32_t samples_ptr) {
const auto now = Clock::now();
std::lock_guard<std::mutex> lock(g_movie_audio_mutex);
MovieAudioState& client = UpsertMovieAudioClientLocked(owner_ptr, driver_ptr);
const double since_prev_submit_ms =
MillisecondsBetween(now, client.last_submit);
const double since_prev_register_ms =
MillisecondsBetween(now, client.last_register);
client.owner_ptr = owner_ptr;
client.driver_ptr = driver_ptr;
client.last_samples_ptr = samples_ptr;
client.submit_count++;
client.last_submit = now;
if ((REXCVAR_GET(ac6_movie_audio_trace) || IsDeepTraceEnabled()) &&
(client.submit_count <= 24 || (client.submit_count % 60) == 0 ||
since_prev_submit_ms >= 40.0)) {
REXAPU_DEBUG(
"AC6 movie-audio submit owner={:08X} driver={:08X} samples={:08X} submits={} "
"active_clients={} since_prev_submit_ms={:.3f} since_prev_register_ms={:.3f}",
owner_ptr,
driver_ptr,
samples_ptr,
client.submit_count,
g_movie_audio_clients.size(),
since_prev_submit_ms,
since_prev_register_ms);
}
MaybeReportDuplicateMovieAudioClientsLocked("submit");
ReportMovieAudioStateTransitionLocked();
}
void OnPresentFrame(const double frame_time_ms, const double fps,
const uint64_t frame_count,
const ac6::d3d::DrawStatsSnapshot& draw_stats) {
if (!(REXCVAR_GET(ac6_movie_audio_trace_verbose) || IsDeepTraceEnabled())) {
return;
}
const auto now = Clock::now();
uint32_t active_clients = 0;
uint32_t primary_owner = 0;
uint32_t primary_driver = 0;
uint64_t primary_submits = 0;
double since_primary_submit_ms = -1.0;
{
std::lock_guard<std::mutex> movie_lock(g_movie_audio_mutex);
if (!g_movie_audio_clients.empty()) {
++g_movie_audio_active_frame_trace_count;
active_clients = static_cast<uint32_t>(g_movie_audio_clients.size());
const MovieAudioState* primary = SelectPrimaryMovieAudioClientLocked();
if (primary) {
primary_owner = primary->owner_ptr;
primary_driver = primary->driver_ptr;
primary_submits = primary->submit_count;
since_primary_submit_ms =
MillisecondsBetween(now, primary->last_submit);
}
}
}
if (active_clients == 0) {
return;
}
if (g_movie_audio_active_frame_trace_count <= 180 ||
(g_movie_audio_active_frame_trace_count % 60) == 0 ||
frame_time_ms >= 40.0 || since_primary_submit_ms >= 40.0) {
REXAPU_DEBUG(
"AC6 movie-audio frame frame={} active_clients={} primary_owner={:08X} "
"primary_driver={:08X} primary_submits={} frame_ms={:.3f} fps={:.3f} "
"since_primary_submit_ms={:.3f} draws={} prim={} idx={} idx_shared={}",
frame_count,
active_clients,
primary_owner,
primary_driver,
primary_submits,
frame_time_ms,
fps,
since_primary_submit_ms,
draw_stats.draw_calls,
draw_stats.draw_calls_primitive,
draw_stats.draw_calls_indexed,
draw_stats.draw_calls_indexed_shared);
}
}
} // namespace ac6::audio_policy
@@ -1,41 +0,0 @@
#pragma once
#include <cstdint>
#include <rex/cvar.h>
#include "d3d_state.h"
REXCVAR_DECLARE(bool, ac6_audio_deep_trace);
REXCVAR_DECLARE(bool, ac6_unlock_fps_video_safe);
REXCVAR_DECLARE(bool, ac6_movie_audio_trace);
REXCVAR_DECLARE(bool, ac6_movie_audio_trace_verbose);
REXCVAR_DECLARE(bool, ac6_movie_audio_force_sync_dispatch);
namespace ac6::audio_policy {
struct MovieAudioSnapshot {
bool movie_audio_active{false};
uint32_t active_client_count{0};
uint64_t register_count{0};
uint64_t submit_count{0};
uint32_t primary_owner{0};
uint32_t primary_driver{0};
};
bool IsDeepTraceEnabled();
bool IsMovieAudioActive();
bool ShouldKeepStockTimingForMovieAudio();
MovieAudioSnapshot GetMovieAudioSnapshot();
void OnMovieAudioClientRegistered(uint32_t owner_ptr, uint32_t callback_ptr,
uint32_t callback_arg, uint32_t driver_ptr);
void OnMovieAudioClientUnregistered(uint32_t owner_ptr, uint32_t driver_ptr);
void OnMovieAudioFrameSubmitted(uint32_t owner_ptr, uint32_t driver_ptr,
uint32_t samples_ptr);
void OnPresentFrame(double frame_time_ms, double fps, uint64_t frame_count,
const ac6::d3d::DrawStatsSnapshot& draw_stats);
} // namespace ac6::audio_policy
File diff suppressed because it is too large Load Diff
@@ -1,28 +0,0 @@
# ac6recomp - ReXGlue Recompiled Project
#
# This file is yours to edit. 'rexglue migrate' will NOT overwrite it.
# SDK boilerplate lives in generated/rexglue.cmake.
cmake_minimum_required(VERSION 3.25)
project(ac6recomp LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 23)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
include(generated/rexglue.cmake)
# Sources
set(AC6RECOMP_SOURCES
src/main.cpp
src/ac6_audio_hooks.cpp
src/render_hooks.cpp
src/d3d_hooks.cpp
)
if(WIN32)
add_executable(ac6recomp WIN32 ${AC6RECOMP_SOURCES})
else()
add_executable(ac6recomp ${AC6RECOMP_SOURCES})
endif()
rexglue_setup_target(ac6recomp)
@@ -1,628 +0,0 @@
#include "generated/ac6recomp_config.h"
#include "render_hooks.h"
#include <algorithm>
#include <array>
#include <cmath>
#include <chrono>
#include <cstdint>
#include <cstring>
#include <limits>
#include <mutex>
#include <vector>
#include <rex/cvar.h>
#include <rex/logging.h>
#include <rex/ppc.h>
REXCVAR_DEFINE_BOOL(ac6_movie_audio_zero_frame_guard, true, "AC6",
"When AC6 movie audio submits a malformed frame, briefly reuse the last "
"good frame to mask cutouts.");
REXCVAR_DEFINE_INT32(ac6_movie_audio_zero_frame_guard_frames, 480, "AC6",
"Maximum consecutive malformed movie-audio frames to replace with the last "
"good frame.");
REXCVAR_DEFINE_DOUBLE(ac6_movie_audio_zero_frame_guard_rms_threshold, 1.0e-4, "AC6",
"Frames at or below this RMS are considered malformed for the movie-audio "
"guard.");
REXCVAR_DEFINE_DOUBLE(ac6_movie_audio_zero_frame_guard_zeroish_pct, 95.0, "AC6",
"Frames at or above this near-zero sample percentage are considered "
"malformed for the movie-audio guard.");
REXCVAR_DEFINE_DOUBLE(ac6_movie_audio_zero_frame_guard_cache_rms_threshold, 1.0e-3, "AC6",
"Frames below this RMS are not allowed to replace the cached movie-audio "
"guard frame.");
REXCVAR_DEFINE_BOOL(ac6_movie_audio_force_sync_dispatch, true, "AC6",
"Force AC6 movie-audio callbacks to run their update path synchronously "
"instead of handing work to the cutscene worker threads.");
REXCVAR_DECLARE(bool, ac6_movie_audio_trace);
REXCVAR_DECLARE(bool, ac6_movie_audio_trace_verbose);
REXCVAR_DECLARE(bool, ac6_audio_deep_trace);
PPC_EXTERN_IMPORT(__savegprlr_28);
PPC_EXTERN_IMPORT(__savegprlr_29);
PPC_EXTERN_IMPORT(__restgprlr_28);
PPC_EXTERN_IMPORT(__restgprlr_29);
PPC_EXTERN_IMPORT(__imp__XAudioRegisterRenderDriverClient);
PPC_EXTERN_IMPORT(__imp__XAudioSubmitRenderDriverFrame);
PPC_EXTERN_IMPORT(__imp__XAudioUnregisterRenderDriverClient);
PPC_EXTERN_FUNC(rex_sub_823A8648);
PPC_EXTERN_FUNC(__imp__rex_sub_823A2C30);
PPC_EXTERN_FUNC(__imp__rex_sub_823AD0D8);
PPC_EXTERN_FUNC(__imp__rex_sub_823AD1C0);
PPC_EXTERN_FUNC(__imp__rex_sub_823AD9C0);
PPC_EXTERN_FUNC(__imp__rex_sub_823B0DB8);
PPC_EXTERN_FUNC(__imp__rex_sub_823B0DF0);
PPC_EXTERN_IMPORT(__imp__KeSetEvent);
PPC_EXTERN_IMPORT(__imp__KeWaitForMultipleObjects);
PPC_EXTERN_IMPORT(__imp__RtlEnterCriticalSection);
PPC_EXTERN_IMPORT(__imp__RtlLeaveCriticalSection);
namespace {
constexpr size_t kMovieAudioFrameWordCount = 6 * 256;
struct MovieAudioFrameGuardState {
uint32_t owner_ptr{0};
uint32_t driver_ptr{0};
std::array<uint32_t, kMovieAudioFrameWordCount> last_good_frame_words{};
bool has_last_good_frame{false};
bool warned_limit_exhausted{false};
uint32_t consecutive_zero_frames{0};
uint64_t substituted_zero_frames{0};
};
std::mutex g_movie_audio_frame_guard_mutex;
std::vector<MovieAudioFrameGuardState> g_movie_audio_frame_guards;
struct MovieAudioFrameStats {
float min_sample{std::numeric_limits<float>::infinity()};
float max_sample{-std::numeric_limits<float>::infinity()};
double sum_squares{0.0};
uint32_t sample_count{0};
uint32_t zeroish_samples{0};
uint32_t nonzero_word_count{0};
bool has_nonfinite{false};
};
struct MovieAudioFrameDerivedStats {
double rms{0.0};
double zeroish_pct{0.0};
};
bool IsDeepTraceEnabled() {
return REXCVAR_GET(ac6_audio_deep_trace);
}
float ByteSwapFloatWord(const uint32_t value) {
const uint32_t swapped = __builtin_bswap32(value);
float result = 0.0f;
std::memcpy(&result, &swapped, sizeof(result));
return result;
}
MovieAudioFrameStats AnalyzeMovieAudioFrame(const uint32_t* frame_words) {
MovieAudioFrameStats stats;
for (size_t i = 0; i < kMovieAudioFrameWordCount; ++i) {
if (frame_words[i] != 0) {
++stats.nonzero_word_count;
}
const float sample = ByteSwapFloatWord(frame_words[i]);
if (!std::isfinite(sample)) {
stats.has_nonfinite = true;
continue;
}
stats.min_sample = std::min(stats.min_sample, sample);
stats.max_sample = std::max(stats.max_sample, sample);
stats.sum_squares += static_cast<double>(sample) * static_cast<double>(sample);
++stats.sample_count;
if (std::fabs(sample) <= 1.0e-6f) {
++stats.zeroish_samples;
}
}
if (stats.sample_count == 0) {
stats.min_sample = 0.0f;
stats.max_sample = 0.0f;
}
return stats;
}
MovieAudioFrameDerivedStats SummarizeMovieAudioFrame(const MovieAudioFrameStats& stats) {
MovieAudioFrameDerivedStats derived;
if (stats.sample_count != 0) {
derived.rms = std::sqrt(stats.sum_squares / static_cast<double>(stats.sample_count));
derived.zeroish_pct =
(static_cast<double>(stats.zeroish_samples) * 100.0) /
static_cast<double>(stats.sample_count);
}
return derived;
}
bool IsMalformedMovieAudioFrame(const MovieAudioFrameStats& stats,
const MovieAudioFrameDerivedStats& derived) {
if (stats.nonzero_word_count == 0 || stats.has_nonfinite) {
return true;
}
const double rms_threshold = REXCVAR_GET(ac6_movie_audio_zero_frame_guard_rms_threshold);
const double zeroish_pct_threshold =
REXCVAR_GET(ac6_movie_audio_zero_frame_guard_zeroish_pct);
return derived.rms <= rms_threshold || derived.zeroish_pct >= zeroish_pct_threshold;
}
MovieAudioFrameGuardState* FindMovieAudioFrameGuardLocked(const uint32_t owner_ptr,
const uint32_t driver_ptr) {
if (driver_ptr != 0) {
for (auto& state : g_movie_audio_frame_guards) {
if (state.driver_ptr == driver_ptr) {
return &state;
}
}
}
if (owner_ptr != 0) {
for (auto& state : g_movie_audio_frame_guards) {
if (state.owner_ptr == owner_ptr) {
return &state;
}
}
}
return nullptr;
}
MovieAudioFrameGuardState& UpsertMovieAudioFrameGuardLocked(const uint32_t owner_ptr,
const uint32_t driver_ptr) {
if (auto* existing = FindMovieAudioFrameGuardLocked(owner_ptr, driver_ptr)) {
if (owner_ptr != 0) {
existing->owner_ptr = owner_ptr;
}
if (driver_ptr != 0) {
existing->driver_ptr = driver_ptr;
}
return *existing;
}
g_movie_audio_frame_guards.emplace_back();
auto& state = g_movie_audio_frame_guards.back();
state.owner_ptr = owner_ptr;
state.driver_ptr = driver_ptr;
return state;
}
void RemoveMovieAudioFrameGuard(const uint32_t owner_ptr, const uint32_t driver_ptr) {
std::lock_guard<std::mutex> lock(g_movie_audio_frame_guard_mutex);
g_movie_audio_frame_guards.erase(
std::remove_if(g_movie_audio_frame_guards.begin(), g_movie_audio_frame_guards.end(),
[owner_ptr, driver_ptr](const MovieAudioFrameGuardState& state) {
const bool matches_owner =
owner_ptr != 0 && state.owner_ptr == owner_ptr;
const bool matches_driver =
driver_ptr != 0 && state.driver_ptr == driver_ptr;
return matches_owner || matches_driver;
}),
g_movie_audio_frame_guards.end());
}
bool ApplyMovieAudioZeroFrameGuard(const uint32_t owner_ptr,
const uint32_t driver_ptr,
const uint32_t samples_ptr,
uint8_t* base) {
if (!REXCVAR_GET(ac6_movie_audio_zero_frame_guard) || samples_ptr == 0) {
return false;
}
auto* frame_words = reinterpret_cast<uint32_t*>(PPC_RAW_ADDR(samples_ptr));
const auto frame_stats = AnalyzeMovieAudioFrame(frame_words);
const auto frame_derived = SummarizeMovieAudioFrame(frame_stats);
const bool malformed_frame = IsMalformedMovieAudioFrame(frame_stats, frame_derived);
const double cache_rms_threshold =
REXCVAR_GET(ac6_movie_audio_zero_frame_guard_cache_rms_threshold);
const int32_t configured_limit = REXCVAR_GET(ac6_movie_audio_zero_frame_guard_frames);
const uint32_t substitution_limit =
configured_limit <= 0 ? 0u : static_cast<uint32_t>(configured_limit);
std::lock_guard<std::mutex> lock(g_movie_audio_frame_guard_mutex);
auto& state = UpsertMovieAudioFrameGuardLocked(owner_ptr, driver_ptr);
if (!malformed_frame) {
if (frame_derived.rms >= cache_rms_threshold) {
std::memcpy(state.last_good_frame_words.data(), frame_words,
sizeof(uint32_t) * kMovieAudioFrameWordCount);
state.has_last_good_frame = true;
}
state.warned_limit_exhausted = false;
state.consecutive_zero_frames = 0;
return false;
}
++state.consecutive_zero_frames;
if (!state.has_last_good_frame || state.consecutive_zero_frames > substitution_limit) {
if (state.has_last_good_frame && substitution_limit != 0 &&
state.consecutive_zero_frames == substitution_limit + 1 &&
!state.warned_limit_exhausted &&
(REXCVAR_GET(ac6_movie_audio_trace) || IsDeepTraceEnabled())) {
REXAPU_WARN(
"AC6 malformed-frame guard hit substitution limit owner={:08X} driver={:08X} "
"samples={:08X} limit={} substitutions={} rms={:.6f} zeroish_pct={:.2f}",
owner_ptr, driver_ptr, samples_ptr, substitution_limit,
state.substituted_zero_frames, frame_derived.rms, frame_derived.zeroish_pct);
state.warned_limit_exhausted = true;
}
return false;
}
std::memcpy(frame_words, state.last_good_frame_words.data(),
sizeof(uint32_t) * kMovieAudioFrameWordCount);
++state.substituted_zero_frames;
if (REXCVAR_GET(ac6_movie_audio_trace) || IsDeepTraceEnabled()) {
REXAPU_WARN(
"AC6 malformed-frame guard substituted owner={:08X} driver={:08X} samples={:08X} "
"bad_run={} substitutions={} limit={} nonzero_words={} rms={:.6f} zeroish_pct={:.2f} "
"nonfinite={}",
owner_ptr, driver_ptr, samples_ptr, state.consecutive_zero_frames,
state.substituted_zero_frames, substitution_limit, frame_stats.nonzero_word_count,
frame_derived.rms, frame_derived.zeroish_pct, frame_stats.has_nonfinite);
}
return true;
}
} // namespace
PPC_FUNC_IMPL(rex_sub_823A6620) {
PPC_FUNC_PROLOGUE();
uint32_t ea{};
ctx.r12.u64 = ctx.lr;
__savegprlr_29(ctx, base);
ea = -128 + ctx.r1.u32;
PPC_STORE_U32(ea, ctx.r1.u32);
ctx.r1.u32 = ea;
ctx.r31.u64 = ctx.r3.u64;
ctx.r29.u64 = ctx.r4.u64;
ctx.r3.s64 = 0;
ctx.r30.s64 = ctx.r31.s64 + 24;
ctx.r11.u64 = PPC_LOAD_U32(ctx.r31.u32 + 24);
ctx.cr6.compare<uint32_t>(ctx.r11.u32, 0, ctx.xer);
if (ctx.cr6.eq) {
goto loc_823A6660;
}
const uint32_t old_driver_ptr = ctx.r11.u32;
if (REXCVAR_GET(ac6_movie_audio_trace_verbose) ||
REXCVAR_GET(ac6_audio_deep_trace)) {
REXAPU_DEBUG(
"AC6 hook rex_sub_823A6620 unregister-existing owner={:08X} old_driver={:08X}",
ctx.r31.u32,
old_driver_ptr);
}
ctx.r3.u64 = ctx.r11.u64;
ctx.lr = 0x823A6650;
__imp__XAudioUnregisterRenderDriverClient(ctx, base);
ctx.cr6.compare<int32_t>(ctx.r3.s32, 0, ctx.xer);
if (ctx.cr6.lt) {
goto loc_823A6680;
}
ac6::OnMovieAudioClientUnregistered(ctx.r31.u32, old_driver_ptr);
RemoveMovieAudioFrameGuard(ctx.r31.u32, old_driver_ptr);
ctx.r11.s64 = 0;
PPC_STORE_U32(ctx.r30.u32 + 0, ctx.r11.u32);
loc_823A6660:
ctx.cr6.compare<uint32_t>(ctx.r29.u32, 0, ctx.xer);
if (ctx.cr6.eq) {
goto loc_823A6680;
}
ctx.r11.u64 = PPC_LOAD_U32(ctx.r31.u32 + 12);
ctx.r4.u64 = ctx.r30.u64;
ctx.r3.s64 = ctx.r1.s64 + 80;
PPC_STORE_U32(ctx.r1.u32 + 80, ctx.r29.u32);
PPC_STORE_U32(ctx.r1.u32 + 84, ctx.r11.u32);
if (REXCVAR_GET(ac6_movie_audio_trace_verbose) ||
REXCVAR_GET(ac6_audio_deep_trace)) {
REXAPU_DEBUG(
"AC6 hook rex_sub_823A6620 register owner={:08X} callback={:08X} callback_arg={:08X}",
ctx.r31.u32,
ctx.r29.u32,
ctx.r11.u32);
}
ctx.lr = 0x823A6680;
__imp__XAudioRegisterRenderDriverClient(ctx, base);
if (ctx.r3.s32 >= 0) {
if (REXCVAR_GET(ac6_movie_audio_trace) ||
REXCVAR_GET(ac6_audio_deep_trace)) {
REXAPU_DEBUG(
"AC6 hook rex_sub_823A6620 register-result owner={:08X} callback={:08X} "
"driver={:08X} status={:08X}",
ctx.r31.u32,
ctx.r29.u32,
PPC_LOAD_U32(ctx.r30.u32 + 0),
static_cast<uint32_t>(ctx.r3.u32));
}
ac6::OnMovieAudioClientRegistered(ctx.r31.u32, ctx.r29.u32,
PPC_LOAD_U32(ctx.r31.u32 + 12),
PPC_LOAD_U32(ctx.r30.u32 + 0));
}
loc_823A6680:
ctx.r1.s64 = ctx.r1.s64 + 128;
__restgprlr_29(ctx, base);
return;
}
PPC_FUNC_IMPL(rex_sub_823A6778) {
PPC_FUNC_PROLOGUE();
uint32_t ea{};
ctx.r12.u64 = ctx.lr;
PPC_STORE_U32(ctx.r1.u32 + -8, ctx.r12.u32);
PPC_STORE_U64(ctx.r1.u32 + -24, ctx.r30.u64);
PPC_STORE_U64(ctx.r1.u32 + -16, ctx.r31.u64);
ea = -112 + ctx.r1.u32;
PPC_STORE_U32(ea, ctx.r1.u32);
ctx.r1.u32 = ea;
ctx.r11.s64 = -2113601536;
ctx.r10.s64 = -2113601536;
ctx.r31.u64 = ctx.r3.u64;
ctx.r11.s64 = ctx.r11.s64 + -13968;
ctx.r10.s64 = ctx.r10.s64 + -13996;
ctx.r9.s64 = -2106654720;
ctx.r30.s64 = 0;
ctx.r9.s64 = ctx.r9.s64 + -18624;
PPC_STORE_U32(ctx.r31.u32 + 0, ctx.r11.u32);
ctx.r11.s64 = 6144;
PPC_STORE_U32(ctx.r31.u32 + 4, ctx.r10.u32);
ctx.r10.s64 = -2103050240;
ctx.r6.u64 = ctx.r11.u64;
PPC_STORE_U32(ctx.r10.u32 + -3580, ctx.r11.u32);
loc_823A67C4:
std::atomic_thread_fence(std::memory_order_seq_cst);
ctx.r7.u64 = PPC_CHECK_GLOBAL_LOCK();
std::atomic_thread_fence(std::memory_order_seq_cst);
ctx.msr = (ctx.r13.u32 & 0x8020) | (ctx.msr & ~0x8020);
PPC_ENTER_GLOBAL_LOCK();
ea = ctx.r9.u32;
ctx.reserved.u32 = *(uint32_t*)PPC_RAW_ADDR(ea);
ctx.r8.u64 = __builtin_bswap32(ctx.reserved.u32);
ctx.cr6.compare<int32_t>(ctx.r8.s32, ctx.r30.s32, ctx.xer);
if (!ctx.cr6.eq) {
goto loc_823A67E8;
}
ea = ctx.r9.u32;
ctx.cr0.lt = 0;
ctx.cr0.gt = 0;
ctx.cr0.eq = __sync_bool_compare_and_swap(
reinterpret_cast<uint32_t*>(PPC_RAW_ADDR(ea)), ctx.reserved.s32,
__builtin_bswap32(ctx.r6.s32));
ctx.cr0.so = ctx.xer.so;
std::atomic_thread_fence(std::memory_order_seq_cst);
ctx.msr = (ctx.r7.u32 & 0x8020) | (ctx.msr & ~0x8020);
PPC_LEAVE_GLOBAL_LOCK();
if (!ctx.cr0.eq) {
goto loc_823A67C4;
}
goto loc_823A67F0;
loc_823A67E8:
ea = ctx.r9.u32;
ctx.cr0.lt = 0;
ctx.cr0.gt = 0;
ctx.cr0.eq = __sync_bool_compare_and_swap(
reinterpret_cast<uint32_t*>(PPC_RAW_ADDR(ea)), ctx.reserved.s32,
__builtin_bswap32(ctx.r8.s32));
ctx.cr0.so = ctx.xer.so;
std::atomic_thread_fence(std::memory_order_seq_cst);
ctx.msr = (ctx.r7.u32 & 0x8020) | (ctx.msr & ~0x8020);
PPC_LEAVE_GLOBAL_LOCK();
loc_823A67F0:
ctx.r3.u64 = PPC_LOAD_U32(ctx.r31.u32 + 24);
const uint32_t old_driver_ptr = ctx.r3.u32;
ctx.cr6.compare<uint32_t>(ctx.r3.u32, 0, ctx.xer);
if (ctx.cr6.eq) {
goto loc_823A680C;
}
ctx.lr = 0x823A6800;
__imp__XAudioUnregisterRenderDriverClient(ctx, base);
ctx.cr6.compare<int32_t>(ctx.r3.s32, 0, ctx.xer);
if (ctx.cr6.lt) {
goto loc_823A680C;
}
if (REXCVAR_GET(ac6_movie_audio_trace) ||
REXCVAR_GET(ac6_audio_deep_trace)) {
REXAPU_DEBUG(
"AC6 hook rex_sub_823A6778 destructor-unregister owner={:08X} old_driver={:08X} "
"status={:08X}",
ctx.r31.u32,
old_driver_ptr,
static_cast<uint32_t>(ctx.r3.u32));
}
ac6::OnMovieAudioClientUnregistered(ctx.r31.u32, old_driver_ptr);
RemoveMovieAudioFrameGuard(ctx.r31.u32, old_driver_ptr);
PPC_STORE_U32(ctx.r31.u32 + 24, ctx.r30.u32);
loc_823A680C:
ctx.r11.s64 = -2113601536;
ctx.r11.s64 = ctx.r11.s64 + -14044;
PPC_STORE_U32(ctx.r31.u32 + 4, ctx.r11.u32);
ctx.r1.s64 = ctx.r1.s64 + 112;
ctx.r12.u64 = PPC_LOAD_U32(ctx.r1.u32 + -8);
ctx.lr = ctx.r12.u64;
ctx.r30.u64 = PPC_LOAD_U64(ctx.r1.u32 + -24);
ctx.r31.u64 = PPC_LOAD_U64(ctx.r1.u32 + -16);
return;
}
PPC_FUNC_IMPL(rex_sub_823A6878) {
PPC_FUNC_PROLOGUE();
uint32_t ea{};
ctx.r12.u64 = ctx.lr;
PPC_STORE_U32(ctx.r1.u32 + -8, ctx.r12.u32);
PPC_STORE_U64(ctx.r1.u32 + -16, ctx.r31.u64);
ea = -112 + ctx.r1.u32;
PPC_STORE_U32(ea, ctx.r1.u32);
ctx.r1.u32 = ea;
ctx.r31.u64 = ctx.r3.u64;
const uint32_t packet_provider_ptr = ctx.r4.u32;
const uint32_t packet_provider_base_ptr =
packet_provider_ptr == 0 ? 0 : packet_provider_ptr - 8;
ctx.r3.u64 = ctx.r4.u64;
ctx.r4.s64 = ctx.r1.s64 + 80;
ctx.lr = 0x823A6898;
rex_sub_823A8648(ctx, base);
ctx.cr6.compare<int32_t>(ctx.r3.s32, 0, ctx.xer);
if (ctx.cr6.lt) {
goto loc_823A68C4;
}
const uint32_t pre_samples_ptr = PPC_LOAD_U32(ctx.r1.u32 + 88);
MovieAudioFrameStats pre_frame_stats{};
MovieAudioFrameDerivedStats pre_frame_derived{};
bool has_pre_frame_stats = false;
if (pre_samples_ptr != 0) {
const auto* pre_frame_words =
reinterpret_cast<const uint32_t*>(PPC_RAW_ADDR(pre_samples_ptr));
pre_frame_stats = AnalyzeMovieAudioFrame(pre_frame_words);
pre_frame_derived = SummarizeMovieAudioFrame(pre_frame_stats);
has_pre_frame_stats = true;
}
ctx.r11.u64 = PPC_LOAD_U32(ctx.r31.u32 + 0);
ctx.r4.s64 = ctx.r1.s64 + 80;
ctx.r3.u64 = ctx.r31.u64;
ctx.r11.u64 = PPC_LOAD_U32(ctx.r11.u32 + 32);
const uint32_t producer_callback_ptr = ctx.r11.u32;
ctx.ctr.u64 = ctx.r11.u64;
ctx.lr = 0x823A68B8;
PPC_CALL_INDIRECT_FUNC(ctx.ctr.u32);
ctx.r4.u64 = PPC_LOAD_U32(ctx.r1.u32 + 88);
ctx.r3.u64 = PPC_LOAD_U32(ctx.r31.u32 + 24);
const uint32_t driver_ptr = ctx.r3.u32;
const uint32_t samples_ptr = ctx.r4.u32;
const uint32_t packet_word_0 = PPC_LOAD_U32(ctx.r1.u32 + 80);
const uint32_t packet_word_1 = PPC_LOAD_U32(ctx.r1.u32 + 84);
const uint32_t packet_word_2 = PPC_LOAD_U32(ctx.r1.u32 + 88);
if (samples_ptr != 0) {
const auto* frame_words = reinterpret_cast<const uint32_t*>(PPC_RAW_ADDR(samples_ptr));
const auto frame_stats = AnalyzeMovieAudioFrame(frame_words);
const auto frame_derived = SummarizeMovieAudioFrame(frame_stats);
if (frame_stats.nonzero_word_count == 0 || frame_derived.rms <= 1.0e-4 ||
frame_stats.has_nonfinite) {
REXAPU_WARN(
"AC6 producer frame anomaly owner={:08X} driver={:08X} producer={:08X} "
"provider={:08X}/{:08X} packet=[{:08X} {:08X} {:08X}] pre_samples={:08X} "
"post_samples={:08X} pre_nonzero_words={} pre_rms={:.6f} pre_zeroish_pct={:.2f} "
"post_nonzero_words={} min={:.6f} max={:.6f} post_rms={:.6f} "
"post_zeroish_pct={:.2f} nonfinite={}",
ctx.r31.u32, driver_ptr, producer_callback_ptr, packet_provider_ptr,
packet_provider_base_ptr, packet_word_0, packet_word_1, packet_word_2,
pre_samples_ptr, samples_ptr,
has_pre_frame_stats ? pre_frame_stats.nonzero_word_count : 0u,
has_pre_frame_stats ? pre_frame_derived.rms : 0.0,
has_pre_frame_stats ? pre_frame_derived.zeroish_pct : 0.0,
frame_stats.nonzero_word_count, frame_stats.min_sample, frame_stats.max_sample,
frame_derived.rms, frame_derived.zeroish_pct, frame_stats.has_nonfinite);
}
} else {
REXAPU_WARN(
"AC6 producer returned null samples owner={:08X} driver={:08X} producer={:08X} "
"provider={:08X}/{:08X} packet=[{:08X} {:08X} {:08X}] pre_samples={:08X}",
ctx.r31.u32, driver_ptr, producer_callback_ptr, packet_provider_ptr,
packet_provider_base_ptr, packet_word_0, packet_word_1, packet_word_2,
pre_samples_ptr);
}
if (REXCVAR_GET(ac6_movie_audio_trace_verbose) ||
REXCVAR_GET(ac6_audio_deep_trace)) {
REXAPU_DEBUG(
"AC6 hook rex_sub_823A6878 submit owner={:08X} driver={:08X} samples={:08X}",
ctx.r31.u32,
driver_ptr,
samples_ptr);
}
ApplyMovieAudioZeroFrameGuard(ctx.r31.u32, driver_ptr, samples_ptr, base);
ctx.lr = 0x823A68C4;
__imp__XAudioSubmitRenderDriverFrame(ctx, base);
ac6::OnMovieAudioFrameSubmitted(ctx.r31.u32, driver_ptr, samples_ptr);
loc_823A68C4:
ctx.r1.s64 = ctx.r1.s64 + 112;
ctx.r12.u64 = PPC_LOAD_U32(ctx.r1.u32 + -8);
ctx.lr = ctx.r12.u64;
ctx.r31.u64 = PPC_LOAD_U64(ctx.r1.u32 + -16);
return;
}
PPC_FUNC_IMPL(rex_sub_823AD9C0) {
PPC_FUNC_PROLOGUE();
if (!REXCVAR_GET(ac6_movie_audio_force_sync_dispatch)) {
__imp__rex_sub_823AD9C0(ctx, base);
return;
}
uint32_t ea{};
ctx.r12.u64 = ctx.lr;
__savegprlr_28(ctx, base);
ea = -192 + ctx.r1.u32;
PPC_STORE_U32(ea, ctx.r1.u32);
ctx.r1.u32 = ea;
ctx.r30.s64 = 0;
ctx.r31.u64 = ctx.r3.u64;
ctx.r28.u64 = ctx.r30.u64;
ctx.lr = 0x823AD9DC;
__imp__rex_sub_823A2C30(ctx, base);
ctx.r11.s64 = -2107047936;
ctx.r29.s64 = ctx.r11.s64 + -6288;
ctx.r3.s64 = ctx.r29.s64 + 4;
ctx.lr = 0x823AD9EC;
__imp__RtlEnterCriticalSection(ctx, base);
const uint32_t worker_count = PPC_LOAD_U32(ctx.r31.u32 + 304);
ctx.r11.u64 = PPC_LOAD_U32(ctx.r13.u32 + 256);
PPC_STORE_U32(ctx.r31.u32 + 300, ctx.r11.u32);
if ((REXCVAR_GET(ac6_movie_audio_trace) || IsDeepTraceEnabled()) && worker_count != 0) {
REXAPU_DEBUG(
"AC6 cutscene audio forcing synchronous dispatch singleton={:08X} worker_count={} "
"current_thread={:08X}",
ctx.r31.u32, worker_count, PPC_LOAD_U32(ctx.r31.u32 + 300));
}
ctx.r3.u64 = ctx.r31.u64;
ctx.lr = 0x823ADA7C;
__imp__rex_sub_823AD0D8(ctx, base);
ctx.r4.s64 = 1;
ctx.r3.u64 = ctx.r31.u64;
ctx.lr = 0x823ADA88;
__imp__rex_sub_823AD1C0(ctx, base);
ctx.r11.u64 = ctx.r28.u32 & 0xFF;
ctx.cr6.compare<uint32_t>(ctx.r11.u32, 0, ctx.xer);
if (!ctx.cr6.eq) {
goto loc_823ADABC_sync;
}
ctx.r3.u64 = PPC_LOAD_U32(ctx.r31.u32 + 64);
ctx.r11.u64 = PPC_LOAD_U32(ctx.r3.u32 + 0);
ctx.r11.u64 = PPC_LOAD_U32(ctx.r11.u32 + 68);
ctx.ctr.u64 = ctx.r11.u64;
ctx.lr = 0x823ADAA8;
PPC_CALL_INDIRECT_FUNC(ctx.ctr.u32);
ctx.cr6.compare<int32_t>(ctx.r3.s32, 0, ctx.xer);
if (ctx.cr6.lt) {
goto loc_823ADABC_sync;
}
ctx.r4.s64 = 1;
ctx.r3.s64 = 0;
ctx.lr = 0x823ADABC;
__imp__rex_sub_823B0DB8(ctx, base);
loc_823ADABC_sync:
PPC_STORE_U32(ctx.r31.u32 + 300, ctx.r30.u32);
ctx.lr = 0x823ADAC4;
__imp__rex_sub_823B0DF0(ctx, base);
ctx.r3.s64 = ctx.r29.s64 + 4;
ctx.lr = 0x823ADACC;
__imp__RtlLeaveCriticalSection(ctx, base);
ctx.r3.s64 = 0;
ctx.r1.s64 = ctx.r1.s64 + 192;
__restgprlr_28(ctx, base);
return;
}
@@ -1,49 +0,0 @@
#pragma once
#include <rex/cvar.h>
#include <rex/graphics/flags.h>
#include <rex/logging/api.h>
#include <rex/rex_app.h>
#include <rex/ui/overlay/debug_overlay.h>
#include "render_hooks.h"
REXCVAR_DECLARE(bool, vfetch_index_rounding_bias);
REXCVAR_DECLARE(bool, guest_vblank_sync_to_refresh);
class Ac6recompApp : public rex::ReXApp {
public:
using rex::ReXApp::ReXApp;
static std::unique_ptr<rex::ui::WindowedApp> Create(
rex::ui::WindowedAppContext& ctx) {
return std::unique_ptr<Ac6recompApp>(new Ac6recompApp(ctx, "ac6recomp",
PPCImageConfig));
}
protected:
void OnPreSetup(rex::RuntimeConfig& config) override {
REXCVAR_SET(vfetch_index_rounding_bias, true);
// Preserve the previous project startup behavior unless the user overrides
// it explicitly in config.
REXCVAR_SET(ac6_unlock_fps, true);
REXCVAR_SET(vsync, false);
REXCVAR_SET(guest_vblank_sync_to_refresh, false);
if (REXCVAR_GET(ac6_audio_deep_trace)) {
if (REXCVAR_GET(log_level) == "info") {
REXCVAR_SET(log_level, "debug");
}
if (REXCVAR_GET(log_file).empty()) {
REXCVAR_SET(log_file, "ac6_audio_trace.log");
}
}
}
void OnCreateDialogs(rex::ui::ImGuiDrawer* drawer) override {
debug_overlay()->SetStatsProvider([] {
auto gs = ac6::GetFrameStats();
return rex::ui::FrameStats{gs.frame_time_ms, gs.fps, gs.frame_count};
});
}
};
@@ -1,423 +0,0 @@
#include "render_hooks.h"
#include "d3d_hooks.h"
#include <algorithm>
#include <chrono>
#include <mutex>
#include <vector>
#include <rex/cvar.h>
#include <rex/logging.h>
#include <rex/ppc/types.h>
REXCVAR_DEFINE_BOOL(ac6_unlock_fps, false, "AC6", "Unlock frame rate to 60fps");
REXCVAR_DEFINE_BOOL(ac6_audio_deep_trace, false, "AC6",
"Enable high-volume AC6 audio diagnostics across AC6 hooks, kernel audio, "
"worker cadence, and SDL queue telemetry");
REXCVAR_DEFINE_BOOL(ac6_timing_hooks_enabled, true, "AC6",
"Enable AC6 timing hooks that alter the game's presentation cadence");
REXCVAR_DEFINE_BOOL(ac6_unlock_fps_video_safe, true, "AC6",
"Keep stock timing while AC6 movie-audio clients are active");
REXCVAR_DEFINE_BOOL(ac6_movie_audio_trace, false, "AC6",
"Trace AC6 movie-audio registration, submission, and timing transitions");
REXCVAR_DEFINE_BOOL(ac6_movie_audio_trace_verbose, false, "AC6",
"Trace AC6 movie-audio cadence, draw stats, and per-frame timing while "
"movie audio is active");
using Clock = std::chrono::steady_clock;
namespace {
std::mutex g_frame_mutex;
double g_frame_time_ms{0.0};
double g_fps{0.0};
uint64_t g_frame_count{0};
Clock::time_point g_frame_start{};
struct MovieAudioState {
uint32_t owner_ptr{0};
uint32_t callback_ptr{0};
uint32_t callback_arg{0};
uint32_t driver_ptr{0};
uint32_t last_samples_ptr{0};
uint64_t register_count{0};
uint64_t unregister_count{0};
uint64_t submit_count{0};
Clock::time_point last_register{};
Clock::time_point last_submit{};
};
std::mutex g_movie_audio_mutex;
std::vector<MovieAudioState> g_movie_audio_clients{};
bool g_movie_audio_last_reported_active{false};
uint64_t g_movie_audio_duplicate_events{0};
uint64_t g_movie_audio_active_frame_trace_count{0};
double MillisecondsBetween(const Clock::time_point newer,
const Clock::time_point older) {
if (older.time_since_epoch().count() == 0 ||
newer.time_since_epoch().count() == 0) {
return -1.0;
}
return std::chrono::duration<double, std::milli>(newer - older).count();
}
MovieAudioState* FindMovieAudioClientLocked(const uint32_t owner_ptr,
const uint32_t driver_ptr) {
if (driver_ptr != 0) {
for (auto& client : g_movie_audio_clients) {
if (client.driver_ptr == driver_ptr) {
return &client;
}
}
}
if (owner_ptr != 0) {
for (auto& client : g_movie_audio_clients) {
if (client.owner_ptr == owner_ptr) {
return &client;
}
}
}
return nullptr;
}
MovieAudioState& UpsertMovieAudioClientLocked(const uint32_t owner_ptr,
const uint32_t driver_ptr) {
if (auto* existing = FindMovieAudioClientLocked(owner_ptr, driver_ptr)) {
return *existing;
}
g_movie_audio_clients.emplace_back();
return g_movie_audio_clients.back();
}
const MovieAudioState* SelectPrimaryMovieAudioClientLocked() {
if (g_movie_audio_clients.empty()) {
return nullptr;
}
return &*std::max_element(
g_movie_audio_clients.begin(), g_movie_audio_clients.end(),
[](const MovieAudioState& lhs, const MovieAudioState& rhs) {
const auto lhs_activity =
lhs.last_submit.time_since_epoch().count() != 0 ? lhs.last_submit
: lhs.last_register;
const auto rhs_activity =
rhs.last_submit.time_since_epoch().count() != 0 ? rhs.last_submit
: rhs.last_register;
return lhs_activity < rhs_activity;
});
}
bool ComputeMovieAudioActiveLocked() {
return !g_movie_audio_clients.empty();
}
bool IsAc6DeepTraceEnabled() {
return REXCVAR_GET(ac6_audio_deep_trace);
}
void ReportMovieAudioStateTransitionLocked() {
const bool active = ComputeMovieAudioActiveLocked();
if (active == g_movie_audio_last_reported_active) {
return;
}
g_movie_audio_last_reported_active = active;
if (!active) {
g_movie_audio_active_frame_trace_count = 0;
}
if (!(REXCVAR_GET(ac6_movie_audio_trace) || IsAc6DeepTraceEnabled())) {
return;
}
const MovieAudioState* primary = SelectPrimaryMovieAudioClientLocked();
double since_submit_ms = -1.0;
double since_register_ms = -1.0;
if (primary) {
const auto now = Clock::now();
since_submit_ms = MillisecondsBetween(now, primary->last_submit);
since_register_ms = MillisecondsBetween(now, primary->last_register);
}
REXAPU_DEBUG(
"AC6 movie-audio timing {} active_clients={} primary_owner={:08X} primary_driver={:08X} "
"since_submit_ms={:.3f} since_register_ms={:.3f}",
active ? "enabled" : "restored",
g_movie_audio_clients.size(),
primary ? primary->owner_ptr : 0u,
primary ? primary->driver_ptr : 0u,
since_submit_ms,
since_register_ms);
}
void MaybeReportDuplicateMovieAudioClientsLocked(const char* reason) {
if (g_movie_audio_clients.size() <= 1 ||
!(REXCVAR_GET(ac6_movie_audio_trace) || IsAc6DeepTraceEnabled())) {
return;
}
++g_movie_audio_duplicate_events;
const MovieAudioState* primary = SelectPrimaryMovieAudioClientLocked();
REXAPU_WARN(
"AC6 movie-audio duplicate clients after {}: active_clients={} duplicate_events={} "
"primary_owner={:08X} primary_driver={:08X}",
reason,
g_movie_audio_clients.size(),
g_movie_audio_duplicate_events,
primary ? primary->owner_ptr : 0u,
primary ? primary->driver_ptr : 0u);
}
bool IsMovieAudioActiveInternal() {
std::lock_guard<std::mutex> lock(g_movie_audio_mutex);
ReportMovieAudioStateTransitionLocked();
return ComputeMovieAudioActiveLocked();
}
bool ShouldKeepStockTimingForMovieAudio() {
return REXCVAR_GET(ac6_timing_hooks_enabled) &&
REXCVAR_GET(ac6_unlock_fps_video_safe) &&
IsMovieAudioActiveInternal();
}
} // namespace
bool ac6FlipIntervalHook() {
return REXCVAR_GET(ac6_timing_hooks_enabled) &&
REXCVAR_GET(ac6_unlock_fps) &&
!ShouldKeepStockTimingForMovieAudio();
}
bool ac6PresentIntervalHook(PPCRegister& r10) {
if (REXCVAR_GET(ac6_timing_hooks_enabled) &&
REXCVAR_GET(ac6_unlock_fps) &&
!ShouldKeepStockTimingForMovieAudio()) {
r10.u64 = 1;
return true;
}
return false;
}
void ac6DeltaDivisorHook(PPCRegister& r29) {
if (!REXCVAR_GET(ac6_timing_hooks_enabled) ||
!REXCVAR_GET(ac6_unlock_fps) ||
ShouldKeepStockTimingForMovieAudio()) {
return;
}
r29.u64 = 30;
}
void ac6PresentTimingHook(PPCRegister& /*r31*/) {
ac6::d3d::OnFrameBoundary();
auto now = Clock::now();
double ms = 0.0;
float fps_val = 0.0f;
uint64_t frame_count = 0;
{
std::lock_guard<std::mutex> lock(g_frame_mutex);
if (g_frame_start.time_since_epoch().count() != 0) {
ms =
std::chrono::duration<double, std::milli>(now - g_frame_start)
.count();
fps_val = ms > 0.0001 ? static_cast<float>(1000.0 / ms) : 0.0f;
g_frame_time_ms = ms;
g_fps = static_cast<double>(fps_val);
g_frame_count++;
}
g_frame_start = now;
frame_count = g_frame_count;
}
if (!(REXCVAR_GET(ac6_movie_audio_trace_verbose) || IsAc6DeepTraceEnabled())) {
return;
}
uint32_t active_clients = 0;
uint32_t primary_owner = 0;
uint32_t primary_driver = 0;
uint64_t primary_submits = 0;
double since_primary_submit_ms = -1.0;
{
std::lock_guard<std::mutex> movie_lock(g_movie_audio_mutex);
if (!g_movie_audio_clients.empty()) {
++g_movie_audio_active_frame_trace_count;
active_clients = static_cast<uint32_t>(g_movie_audio_clients.size());
const MovieAudioState* primary = SelectPrimaryMovieAudioClientLocked();
if (primary) {
primary_owner = primary->owner_ptr;
primary_driver = primary->driver_ptr;
primary_submits = primary->submit_count;
since_primary_submit_ms = MillisecondsBetween(now, primary->last_submit);
}
}
}
if (active_clients == 0) {
return;
}
const ac6::d3d::DrawStatsSnapshot draw_stats = ac6::d3d::GetDrawStats();
if (g_movie_audio_active_frame_trace_count <= 180 ||
(g_movie_audio_active_frame_trace_count % 60) == 0 ||
ms >= 40.0 || since_primary_submit_ms >= 40.0) {
REXAPU_DEBUG(
"AC6 movie-audio frame frame={} active_clients={} primary_owner={:08X} "
"primary_driver={:08X} primary_submits={} frame_ms={:.3f} fps={:.3f} "
"since_primary_submit_ms={:.3f} draws={} prim={} idx={} idx_shared={}",
frame_count,
active_clients,
primary_owner,
primary_driver,
primary_submits,
ms,
static_cast<double>(fps_val),
since_primary_submit_ms,
draw_stats.draw_calls,
draw_stats.draw_calls_primitive,
draw_stats.draw_calls_indexed,
draw_stats.draw_calls_indexed_shared);
}
}
namespace ac6 {
FrameStats GetFrameStats() {
std::lock_guard<std::mutex> frame_lock(g_frame_mutex);
std::lock_guard<std::mutex> movie_lock(g_movie_audio_mutex);
uint64_t register_count = 0;
uint64_t submit_count = 0;
for (const auto& client : g_movie_audio_clients) {
register_count += client.register_count;
submit_count += client.submit_count;
}
const MovieAudioState* primary = SelectPrimaryMovieAudioClientLocked();
return FrameStats{
g_frame_time_ms,
g_fps,
g_frame_count,
ComputeMovieAudioActiveLocked(),
static_cast<uint32_t>(g_movie_audio_clients.size()),
register_count,
submit_count,
primary ? primary->owner_ptr : 0u,
primary ? primary->driver_ptr : 0u,
};
}
bool IsMovieAudioActive() {
return IsMovieAudioActiveInternal();
}
void OnMovieAudioClientRegistered(const uint32_t owner_ptr,
const uint32_t callback_ptr,
const uint32_t callback_arg,
const uint32_t driver_ptr) {
const auto now = Clock::now();
std::lock_guard<std::mutex> lock(g_movie_audio_mutex);
MovieAudioState& client = UpsertMovieAudioClientLocked(owner_ptr, driver_ptr);
const double since_prev_register_ms =
MillisecondsBetween(now, client.last_register);
const double since_prev_submit_ms =
MillisecondsBetween(now, client.last_submit);
client.owner_ptr = owner_ptr;
client.callback_ptr = callback_ptr;
client.callback_arg = callback_arg;
client.driver_ptr = driver_ptr;
client.register_count++;
client.last_register = now;
if (REXCVAR_GET(ac6_movie_audio_trace) || IsAc6DeepTraceEnabled()) {
REXAPU_DEBUG(
"AC6 movie-audio register owner={:08X} callback={:08X} arg={:08X} driver={:08X} "
"registers={} submits={} active_clients={} since_prev_register_ms={:.3f} "
"since_prev_submit_ms={:.3f}",
owner_ptr,
callback_ptr,
callback_arg,
driver_ptr,
client.register_count,
client.submit_count,
g_movie_audio_clients.size(),
since_prev_register_ms,
since_prev_submit_ms);
}
MaybeReportDuplicateMovieAudioClientsLocked("register");
ReportMovieAudioStateTransitionLocked();
}
void OnMovieAudioClientUnregistered(const uint32_t owner_ptr,
const uint32_t driver_ptr) {
std::lock_guard<std::mutex> lock(g_movie_audio_mutex);
auto it = std::remove_if(
g_movie_audio_clients.begin(), g_movie_audio_clients.end(),
[owner_ptr, driver_ptr](const MovieAudioState& client) {
const bool matches_owner =
owner_ptr != 0 && owner_ptr == client.owner_ptr;
const bool matches_driver =
driver_ptr != 0 && driver_ptr == client.driver_ptr;
return matches_owner || matches_driver;
});
if (it == g_movie_audio_clients.end()) {
return;
}
if (REXCVAR_GET(ac6_movie_audio_trace) || IsAc6DeepTraceEnabled()) {
for (auto iter = it; iter != g_movie_audio_clients.end(); ++iter) {
const double since_submit_ms = MillisecondsBetween(Clock::now(), iter->last_submit);
REXAPU_DEBUG(
"AC6 movie-audio unregister owner={:08X} driver={:08X} submits={} registers={} "
"unregisters={} since_submit_ms={:.3f}",
iter->owner_ptr,
iter->driver_ptr,
iter->submit_count,
iter->register_count,
iter->unregister_count + 1,
since_submit_ms);
}
}
for (auto iter = it; iter != g_movie_audio_clients.end(); ++iter) {
iter->unregister_count++;
}
g_movie_audio_clients.erase(it, g_movie_audio_clients.end());
ReportMovieAudioStateTransitionLocked();
}
void OnMovieAudioFrameSubmitted(const uint32_t owner_ptr,
const uint32_t driver_ptr,
const uint32_t samples_ptr) {
const auto now = Clock::now();
std::lock_guard<std::mutex> lock(g_movie_audio_mutex);
MovieAudioState& client = UpsertMovieAudioClientLocked(owner_ptr, driver_ptr);
const double since_prev_submit_ms = MillisecondsBetween(now, client.last_submit);
const double since_prev_register_ms = MillisecondsBetween(now, client.last_register);
client.owner_ptr = owner_ptr;
client.driver_ptr = driver_ptr;
client.last_samples_ptr = samples_ptr;
client.submit_count++;
client.last_submit = now;
if ((REXCVAR_GET(ac6_movie_audio_trace) || IsAc6DeepTraceEnabled()) &&
(client.submit_count <= 24 ||
(client.submit_count % 60) == 0 ||
since_prev_submit_ms >= 40.0)) {
REXAPU_DEBUG(
"AC6 movie-audio submit owner={:08X} driver={:08X} samples={:08X} submits={} "
"active_clients={} since_prev_submit_ms={:.3f} since_prev_register_ms={:.3f}",
owner_ptr,
driver_ptr,
samples_ptr,
client.submit_count,
g_movie_audio_clients.size(),
since_prev_submit_ms,
since_prev_register_ms);
}
MaybeReportDuplicateMovieAudioClientsLocked("submit");
ReportMovieAudioStateTransitionLocked();
}
} // namespace ac6
@@ -1,32 +0,0 @@
#pragma once
#include <cstdint>
#include <rex/cvar.h>
REXCVAR_DECLARE(bool, ac6_unlock_fps);
REXCVAR_DECLARE(bool, ac6_audio_deep_trace);
namespace ac6 {
struct FrameStats {
double frame_time_ms;
double fps;
uint64_t frame_count;
bool movie_audio_active{false};
uint32_t movie_audio_client_count{0};
uint64_t movie_audio_register_count{0};
uint64_t movie_audio_submit_count{0};
uint32_t movie_audio_owner{0};
uint32_t movie_audio_driver{0};
};
FrameStats GetFrameStats();
bool IsMovieAudioActive();
void OnMovieAudioClientRegistered(uint32_t owner_ptr, uint32_t callback_ptr,
uint32_t callback_arg, uint32_t driver_ptr);
void OnMovieAudioClientUnregistered(uint32_t owner_ptr, uint32_t driver_ptr);
void OnMovieAudioFrameSubmitted(uint32_t owner_ptr, uint32_t driver_ptr,
uint32_t samples_ptr);
} // namespace ac6
@@ -1,46 +0,0 @@
/**
******************************************************************************
* Xenia : Xbox 360 Emulator Research Project *
******************************************************************************
* Copyright 2013 Ben Vanik. All rights reserved. *
* Released under the BSD license - see LICENSE in the root for more details. *
******************************************************************************
*
* @modified Tom Clay, 2026 - Adapted for ReXGlue runtime
*/
#pragma once
#include <cstdint>
#include <rex/kernel.h>
#include <rex/memory.h>
namespace rex::audio {
struct AudioDriverTelemetry {
uint32_t submitted_frames{0};
uint32_t consumed_frames{0};
uint32_t underrun_count{0};
uint32_t silence_injections{0};
uint32_t queued_depth{0};
uint32_t peak_queued_depth{0};
};
class AudioDriver {
public:
explicit AudioDriver(memory::Memory* memory);
virtual ~AudioDriver();
virtual void SubmitFrame(uint32_t samples_ptr) = 0;
virtual AudioDriverTelemetry GetTelemetry() const;
protected:
inline uint8_t* TranslatePhysical(uint32_t guest_address) const {
return memory_->TranslatePhysical(guest_address);
}
memory::Memory* memory_ = nullptr;
};
} // namespace rex::audio
@@ -1,105 +0,0 @@
/**
******************************************************************************
* Xenia : Xbox 360 Emulator Research Project *
******************************************************************************
* Copyright 2013 Ben Vanik. All rights reserved. *
* Released under the BSD license - see LICENSE in the root for more details. *
******************************************************************************
*
* @modified Tom Clay, 2026 - Adapted for ReXGlue runtime
*/
#pragma once
#include <atomic>
#include <queue>
#include <rex/audio/audio_driver.h>
#include <rex/kernel.h>
#include <rex/memory.h>
#include <rex/system/interfaces/audio.h>
#include <rex/system/function_dispatcher.h>
#include <rex/system/xthread.h>
#include <rex/thread.h>
#include <rex/thread/mutex.h>
namespace rex::stream {
class ByteStream;
} // namespace rex::stream
namespace rex::audio {
constexpr memory::fourcc_t kAudioSaveSignature = memory::make_fourcc("XAUD");
class AudioDriver;
class XmaDecoder;
class AudioSystem : public system::IAudioSystem {
public:
virtual ~AudioSystem();
memory::Memory* memory() const { return memory_; }
runtime::FunctionDispatcher* function_dispatcher() const { return function_dispatcher_; }
XmaDecoder* xma_decoder() const { return xma_decoder_.get(); }
virtual X_STATUS Setup(system::KernelState* kernel_state);
virtual void Shutdown();
X_STATUS RegisterClient(uint32_t callback, uint32_t callback_arg, size_t* out_index);
void UnregisterClient(size_t index);
void SubmitFrame(size_t index, uint32_t samples_ptr);
AudioDriverTelemetry GetClientTelemetry(size_t index);
uint32_t GetClientRenderDriverTic(size_t index);
bool Save(stream::ByteStream* stream);
bool Restore(stream::ByteStream* stream);
bool is_paused() const { return paused_; }
void Pause();
void Resume();
protected:
explicit AudioSystem(runtime::FunctionDispatcher* function_dispatcher);
virtual void Initialize();
void WorkerThreadMain();
virtual X_STATUS CreateDriver(size_t index, rex::thread::Semaphore* semaphore,
AudioDriver** out_driver) = 0;
virtual void DestroyDriver(AudioDriver* driver) = 0;
static constexpr size_t kMaximumQueuedFrames = 64;
static constexpr uint32_t kRenderDriverTicSamplesPerFrame = 256;
memory::Memory* memory_ = nullptr;
runtime::FunctionDispatcher* function_dispatcher_ = nullptr;
std::unique_ptr<XmaDecoder> xma_decoder_;
uint32_t queued_frames_;
std::atomic<bool> worker_running_ = {false};
system::object_ref<system::XHostThread> worker_thread_;
rex::thread::global_critical_region global_critical_region_;
static const size_t kMaximumClientCount = 8;
struct {
AudioDriver* driver;
uint32_t callback;
uint32_t callback_arg;
uint32_t wrapped_callback_arg;
bool in_use;
} clients_[kMaximumClientCount];
int FindFreeClient();
std::unique_ptr<rex::thread::Semaphore> client_semaphores_[kMaximumClientCount];
// Event is always there in case we have no clients.
std::unique_ptr<rex::thread::Event> shutdown_event_;
rex::thread::WaitHandle* wait_handles_[kMaximumClientCount + 1];
bool paused_ = false;
rex::thread::Fence pause_fence_;
std::unique_ptr<rex::thread::Event> resume_event_;
};
} // namespace rex::audio
@@ -1,103 +0,0 @@
/**
******************************************************************************
* Xenia : Xbox 360 Emulator Research Project *
******************************************************************************
* Copyright 2021 Ben Vanik. All rights reserved. *
* Released under the BSD license - see LICENSE in the root for more details. *
******************************************************************************
*
* @modified Tom Clay, 2026 - Adapted for ReXGlue runtime
*/
#pragma once
#include <cstdint>
#include <rex/platform.h>
#include <rex/types.h>
namespace rex::audio::conversion {
#if REX_ARCH_AMD64
inline void sequential_6_BE_to_interleaved_6_LE(float* output, const float* input,
size_t ch_sample_count) {
const uint32_t* in = reinterpret_cast<const uint32_t*>(input);
uint32_t* out = reinterpret_cast<uint32_t*>(output);
const __m128i byte_swap_shuffle =
_mm_set_epi8(12, 13, 14, 15, 8, 9, 10, 11, 4, 5, 6, 7, 0, 1, 2, 3);
for (size_t sample = 0; sample < ch_sample_count; sample++) {
__m128i sample0 =
_mm_set_epi32(in[3 * ch_sample_count + sample], in[2 * ch_sample_count + sample],
in[1 * ch_sample_count + sample], in[0 * ch_sample_count + sample]);
uint32_t sample1 = in[4 * ch_sample_count + sample];
uint32_t sample2 = in[5 * ch_sample_count + sample];
sample0 = _mm_shuffle_epi8(sample0, byte_swap_shuffle);
_mm_storeu_si128(reinterpret_cast<__m128i*>(&out[sample * 6]), sample0);
sample1 = rex::byte_swap(sample1);
out[sample * 6 + 4] = sample1;
sample2 = rex::byte_swap(sample2);
out[sample * 6 + 5] = sample2;
}
}
inline void sequential_6_BE_to_interleaved_2_LE(float* output, const float* input,
size_t ch_sample_count) {
assert_true(ch_sample_count % 4 == 0);
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);
// put center on left and right, discard low frequency
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 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));
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));
__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));
}
}
#else
inline void sequential_6_BE_to_interleaved_6_LE(float* output, const float* input,
size_t ch_sample_count) {
for (size_t sample = 0; sample < ch_sample_count; sample++) {
for (size_t channel = 0; channel < 6; channel++) {
output[sample * 6 + channel] = rex::byte_swap(input[channel * ch_sample_count + sample]);
}
}
}
inline void sequential_6_BE_to_interleaved_2_LE(float* output, const float* input,
size_t ch_sample_count) {
// 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);
}
}
#endif
} // namespace rex::audio::conversion
@@ -1,17 +0,0 @@
/**
******************************************************************************
* Xenia : Xbox 360 Emulator Research Project *
******************************************************************************
* Copyright 2013 Ben Vanik. All rights reserved. *
* Released under the BSD license - see LICENSE in the root for more details. *
******************************************************************************
*
* @modified Tom Clay, 2026 - Adapted for ReXGlue runtime
*/
#pragma once
#include <rex/cvar.h>
REXCVAR_DECLARE(bool, audio_mute);
REXCVAR_DECLARE(bool, ffmpeg_verbose);
@@ -1,32 +0,0 @@
/**
******************************************************************************
* Xenia : Xbox 360 Emulator Research Project *
******************************************************************************
* Copyright 2013 Ben Vanik. All rights reserved. *
* Released under the BSD license - see LICENSE in the root for more details. *
******************************************************************************
*
* @modified Tom Clay, 2026 - Adapted for ReXGlue runtime
*/
#pragma once
#include <rex/audio/audio_system.h>
namespace rex::audio::nop {
class NopAudioSystem : public AudioSystem {
public:
explicit NopAudioSystem(runtime::FunctionDispatcher* function_dispatcher);
~NopAudioSystem() override;
static bool IsAvailable() { return true; }
static std::unique_ptr<AudioSystem> Create(runtime::FunctionDispatcher* function_dispatcher);
X_STATUS CreateDriver(size_t index, rex::thread::Semaphore* semaphore,
AudioDriver** out_driver) override;
void DestroyDriver(AudioDriver* driver) override;
};
} // namespace rex::audio::nop
@@ -1,67 +0,0 @@
/**
******************************************************************************
* Xenia : Xbox 360 Emulator Research Project *
******************************************************************************
* Copyright 2020 Ben Vanik. All rights reserved. *
* Released under the BSD license - see LICENSE in the root for more details. *
******************************************************************************
*
* @modified Tom Clay, 2026 - Adapted for ReXGlue runtime
*/
#pragma once
#include <array>
#include <atomic>
#include <mutex>
#include <queue>
#include <stack>
#include <rex/audio/audio_driver.h>
#include <rex/thread.h>
#include <SDL3/SDL.h>
namespace rex::audio::sdl {
class SDLAudioDriver : public AudioDriver {
public:
SDLAudioDriver(memory::Memory* memory, rex::thread::Semaphore* semaphore);
~SDLAudioDriver() override;
bool Initialize();
void SubmitFrame(uint32_t frame_ptr) override;
AudioDriverTelemetry GetTelemetry() const override;
void Shutdown();
protected:
static void SDLCallback(void* userdata, SDL_AudioStream* stream, int additional_amount,
int total_amount);
rex::thread::Semaphore* semaphore_ = nullptr;
SDL_AudioStream* sdl_stream_ = nullptr;
bool sdl_initialized_ = false;
uint8_t sdl_device_channels_ = 0;
std::atomic<bool> shutting_down_ = false;
static const uint32_t frame_frequency_ = 48000;
static const uint32_t frame_channels_ = 6;
static const uint32_t channel_samples_ = 256;
static const uint32_t frame_samples_ = frame_channels_ * channel_samples_;
static const uint32_t frame_size_ = sizeof(float) * frame_samples_;
std::atomic<uint32_t> submitted_frames_ = 0;
std::atomic<uint32_t> consumed_frames_ = 0;
std::atomic<uint32_t> underrun_count_ = 0;
std::atomic<uint32_t> silence_injections_ = 0;
std::atomic<uint32_t> queued_depth_ = 0;
std::atomic<uint32_t> peak_queued_depth_ = 0;
std::queue<float*> frames_queued_ = {};
std::stack<float*> frames_unused_ = {};
std::mutex frames_mutex_ = {};
std::array<float, frame_samples_> pending_output_frame_ = {};
size_t pending_output_float_count_ = 0;
size_t pending_output_float_offset_ = 0;
};
} // namespace rex::audio::sdl
@@ -1,35 +0,0 @@
/**
******************************************************************************
* Xenia : Xbox 360 Emulator Research Project *
******************************************************************************
* Copyright 2020 Ben Vanik. All rights reserved. *
* Released under the BSD license - see LICENSE in the root for more details. *
******************************************************************************
*
* @modified Tom Clay, 2026 - Adapted for ReXGlue runtime
*/
#pragma once
#include <rex/audio/audio_system.h>
namespace rex::audio::sdl {
class SDLAudioSystem : public AudioSystem {
public:
explicit SDLAudioSystem(runtime::FunctionDispatcher* function_dispatcher);
~SDLAudioSystem() override;
static bool IsAvailable() { return true; }
static std::unique_ptr<AudioSystem> Create(runtime::FunctionDispatcher* function_dispatcher);
X_STATUS CreateDriver(size_t index, rex::thread::Semaphore* semaphore,
AudioDriver** out_driver) override;
void DestroyDriver(AudioDriver* driver) override;
protected:
void Initialize() override;
};
} // namespace rex::audio::sdl
@@ -1,285 +0,0 @@
/**
******************************************************************************
* Xenia : Xbox 360 Emulator Research Project *
******************************************************************************
* Copyright 2021 Ben Vanik. All rights reserved. *
* Released under the BSD license - see LICENSE in the root for more details. *
******************************************************************************
*
* @modified Tom Clay, 2026 - Adapted for ReXGlue runtime
*/
#pragma once
#include <array>
#include <atomic>
#include <mutex>
#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
// a few (very slight) modifications.
// XMA2 is fully backwards-compatible with XMA1.
// Helpful resources:
// https://github.com/koolkdev/libertyv/blob/master/libav_wrapper/xma2dec.c
// https://hcs64.com/mboard/forum.php?showthread=14818
// https://github.com/hrydgard/minidx9/blob/master/Include/xma2defs.h
// Forward declarations
struct AVCodec;
struct AVCodecParserContext;
struct AVCodecContext;
struct AVFrame;
struct AVPacket;
namespace rex::audio {
// This is stored in guest space in big-endian order.
// We load and swap the whole thing to splat here so that we can
// use bitfields.
// This could be important:
// https://www.fmod.org/questions/question/forum-15859
// Appears to be dumped in order (for the most part)
struct XMA_CONTEXT_DATA {
// DWORD 0
uint32_t input_buffer_0_packet_count : 12; // XMASetInputBuffer0, number of
// 2KB packets. Max 4095 packets.
// These packets form a block.
uint32_t loop_count : 8; // +12bit, XMASetLoopData NumLoops
uint32_t input_buffer_0_valid : 1; // +20bit, XMAIsInputBuffer0Valid
uint32_t input_buffer_1_valid : 1; // +21bit, XMAIsInputBuffer1Valid
uint32_t output_buffer_block_count : 5; // +22bit SizeWrite 256byte blocks
uint32_t output_buffer_write_offset : 5; // +27bit
// XMAGetOutputBufferWriteOffset
// AKA OffsetWrite
// DWORD 1
uint32_t input_buffer_1_packet_count : 12; // XMASetInputBuffer1, number of
// 2KB packets. Max 4095 packets.
// These packets form a block.
uint32_t loop_subframe_start : 2; // +12bit, XMASetLoopData
uint32_t loop_subframe_end : 3; // +14bit, XMASetLoopData
uint32_t loop_subframe_skip : 3; // +17bit, XMASetLoopData might be
// subframe_decode_count
uint32_t subframe_decode_count : 4; // +20bit
uint32_t output_buffer_padding : 3; // +24bit, extra output buffer blocks
// reserved per decoded frame
uint32_t sample_rate : 2; // +27bit enum of sample rates
uint32_t is_stereo : 1; // +29bit
uint32_t unk_dword_1_c : 1; // +30bit
uint32_t output_buffer_valid : 1; // +31bit, XMAIsOutputBufferValid
// DWORD 2
uint32_t input_buffer_read_offset : 26; // XMAGetInputBufferReadOffset
uint32_t error_status : 5; // ErrorStatus
uint32_t error_set : 1; // ErrorSet
// DWORD 3
uint32_t loop_start : 26; // XMASetLoopData LoopStartOffset
// frame offset in bits
uint32_t parser_error_status : 5; // ParserErrorStatus
uint32_t parser_error_set : 1; // ParserErrorSet
// DWORD 4
uint32_t loop_end : 26; // XMASetLoopData LoopEndOffset
// frame offset in bits
uint32_t packet_metadata : 5; // XMAGetPacketMetadata
uint32_t current_buffer : 1; // ?
// DWORD 5
uint32_t input_buffer_0_ptr; // physical address
// DWORD 6
uint32_t input_buffer_1_ptr; // physical address
// DWORD 7
uint32_t output_buffer_ptr; // physical address
// DWORD 8
uint32_t work_buffer_ptr; // PtrOverlapAdd(?)
// DWORD 9
// +0bit, XMAGetOutputBufferReadOffset AKA WriteBufferOffsetRead
uint32_t output_buffer_read_offset : 5;
uint32_t : 25;
uint32_t stop_when_done : 1; // +30bit
uint32_t interrupt_when_done : 1; // +31bit
// DWORD 10-15
uint32_t unk_dwords_10_15[6]; // reserved?
explicit XMA_CONTEXT_DATA(const void* ptr) {
memory::copy_and_swap(reinterpret_cast<uint32_t*>(this), reinterpret_cast<const uint32_t*>(ptr),
sizeof(XMA_CONTEXT_DATA) / 4);
}
void Store(void* ptr) {
memory::copy_and_swap(reinterpret_cast<uint32_t*>(ptr), reinterpret_cast<const uint32_t*>(this),
sizeof(XMA_CONTEXT_DATA) / 4);
}
bool IsInputBufferValid(uint8_t buffer_index) const {
return buffer_index == 0 ? input_buffer_0_valid : input_buffer_1_valid;
}
bool IsCurrentInputBufferValid() const { return IsInputBufferValid(current_buffer); }
bool IsAnyInputBufferValid() const { return input_buffer_0_valid || input_buffer_1_valid; }
uint32_t GetInputBufferAddress(uint8_t buffer_index) const {
return buffer_index == 0 ? input_buffer_0_ptr : input_buffer_1_ptr;
}
uint32_t GetCurrentInputBufferAddress() const { return GetInputBufferAddress(current_buffer); }
uint32_t GetInputBufferPacketCount(uint8_t buffer_index) const {
return buffer_index == 0 ? input_buffer_0_packet_count : input_buffer_1_packet_count;
}
uint32_t GetCurrentInputBufferPacketCount() const {
return GetInputBufferPacketCount(current_buffer);
}
bool IsConsumeOnlyContext() const {
return (input_buffer_0_packet_count | input_buffer_1_packet_count) == 0;
}
};
static_assert_size(XMA_CONTEXT_DATA, 64);
#pragma pack(push, 1)
// XMA2WAVEFORMATEX
struct Xma2ExtraData {
uint8_t raw[34];
};
static_assert_size(Xma2ExtraData, 34);
#pragma pack(pop)
struct kPacketInfo {
uint8_t frame_count_ = 0;
uint8_t current_frame_ = 0;
uint32_t current_frame_size_ = 0;
bool isLastFrameInPacket() const {
return frame_count_ == 0 || current_frame_ == frame_count_ - 1;
}
};
static constexpr int kIdToSampleRate[4] = {24000, 32000, 44100, 48000};
class XmaContext {
public:
static const uint32_t kBytesPerPacket = 2048;
static const uint32_t kBitsPerPacket = kBytesPerPacket * 8;
static const uint32_t kBitsPerPacketHeader = 32;
static const uint32_t kBitsPerFrameHeader = 15;
static const uint32_t kBytesPerPacketHeader = 4;
static const uint32_t kBytesPerPacketData = kBytesPerPacket - kBytesPerPacketHeader;
static const uint32_t kBytesPerSample = 2;
static const uint32_t kSamplesPerFrame = 512;
static const uint32_t kSamplesPerSubframe = 128;
static const uint32_t kBytesPerFrameChannel = kSamplesPerFrame * kBytesPerSample;
static const uint32_t kBytesPerSubframeChannel = kSamplesPerSubframe * kBytesPerSample;
static const uint32_t kOutputBytesPerBlock = 256;
static const uint32_t kOutputMaxSizeBytes = 31 * kOutputBytesPerBlock;
static const uint32_t kMaxFrameSizeinBits = 0x4000 - kBitsPerPacketHeader;
explicit XmaContext();
~XmaContext();
int Setup(uint32_t id, memory::Memory* memory, uint32_t guest_ptr);
bool Work();
void Enable();
bool Block(bool poll);
void Clear();
void Disable();
void Release();
memory::Memory* memory() const { return memory_; }
uint32_t id() { return id_; }
uint32_t guest_ptr() { return guest_ptr_; }
bool is_allocated() { return is_allocated_.load(std::memory_order_acquire); }
bool is_enabled() { return is_enabled_.load(std::memory_order_acquire); }
void set_is_allocated(bool is_allocated) {
is_allocated_.store(is_allocated, std::memory_order_release);
}
void set_is_enabled(bool is_enabled) { is_enabled_.store(is_enabled, std::memory_order_release); }
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 int GetSampleRate(int id);
static int16_t GetPacketNumber(size_t size, size_t bit_offset);
static uint32_t GetCurrentInputBufferSize(XMA_CONTEXT_DATA* data);
kPacketInfo GetPacketInfo(uint8_t* packet, uint32_t frame_offset);
uint32_t GetAmountOfBitsToRead(uint32_t remaining_stream_bits, uint32_t frame_size);
const uint8_t* GetNextPacket(XMA_CONTEXT_DATA* data, uint32_t next_packet_index,
uint32_t current_input_packet_count);
uint32_t GetNextPacketReadOffset(uint8_t* buffer, uint32_t next_packet_index,
uint32_t current_input_packet_count);
uint8_t* GetCurrentInputBuffer(XMA_CONTEXT_DATA* data);
void Decode(XMA_CONTEXT_DATA* data);
void Consume(memory::RingBuffer* output_rb, const XMA_CONTEXT_DATA* data);
void UpdateLoopStatus(XMA_CONTEXT_DATA* data);
void ClearLocked(XMA_CONTEXT_DATA* data);
memory::RingBuffer PrepareOutputRingBuffer(XMA_CONTEXT_DATA* data);
int PrepareDecoder(int sample_rate, bool is_two_channel);
void PreparePacket(uint32_t frame_size, uint32_t frame_padding);
bool DecodePacket(AVCodecContext* av_context, const AVPacket* av_packet, AVFrame* av_frame);
void StoreContextMerged(const XMA_CONTEXT_DATA& data, const XMA_CONTEXT_DATA& initial_data,
uint8_t* context_ptr);
static void ConvertFrame(const uint8_t** samples, bool is_two_channel, uint8_t* output_buffer);
memory::Memory* memory_ = nullptr;
std::unique_ptr<rex::thread::Event> work_completion_event_;
uint32_t id_ = 0;
uint32_t guest_ptr_ = 0;
std::mutex lock_;
std::atomic<bool> is_allocated_ = false;
std::atomic<bool> is_enabled_ = false;
// ffmpeg structures
AVPacket* av_packet_ = nullptr;
AVCodec* av_codec_ = nullptr;
AVCodecContext* av_context_ = nullptr;
AVFrame* av_frame_ = nullptr;
// Packet data buffer (two packets worth for split frame handling)
std::array<uint8_t, kBytesPerPacketData * 2> input_buffer_;
// First byte contains bit offset information
std::array<uint8_t, 1 + 4096> xma_frame_;
// Conversion buffer for up to 2-channel frame
std::array<uint8_t, kBytesPerFrameChannel * 2> raw_frame_;
// Output buffer tracking
int32_t remaining_subframe_blocks_in_output_buffer_ = 0;
uint8_t current_frame_remaining_subframes_ = 0;
// Loop subframe precision state
uint8_t loop_frame_output_limit_ = 0;
bool loop_start_skip_pending_ = false;
};
} // namespace rex::audio
@@ -1,92 +0,0 @@
/**
******************************************************************************
* Xenia : Xbox 360 Emulator Research Project *
******************************************************************************
* Copyright 2021 Ben Vanik. All rights reserved. *
* Released under the BSD license - see LICENSE in the root for more details. *
******************************************************************************
*
* @modified Tom Clay, 2026 - Adapted for ReXGlue runtime
*/
#pragma once
#include <atomic>
#include <mutex>
#include <queue>
#include <rex/audio/xma/context.h>
#include <rex/audio/xma/register_file.h>
#include <rex/bit.h>
#include <rex/kernel.h>
#include <rex/system/xthread.h>
namespace rex::runtime {
class FunctionDispatcher;
} // namespace rex::runtime
namespace rex::audio {
struct XMA_CONTEXT_DATA;
class XmaDecoder {
public:
explicit XmaDecoder(runtime::FunctionDispatcher* function_dispatcher);
~XmaDecoder();
memory::Memory* memory() const { return memory_; }
runtime::FunctionDispatcher* function_dispatcher() const { return function_dispatcher_; }
X_STATUS Setup(system::KernelState* kernel_state);
void Shutdown();
uint32_t context_array_ptr() const { return register_file_[XmaRegister::ContextArrayAddress]; }
uint32_t AllocateContext();
void ReleaseContext(uint32_t guest_ptr);
bool BlockOnContext(uint32_t guest_ptr, bool poll);
uint32_t ReadRegister(uint32_t addr);
void WriteRegister(uint32_t addr, uint32_t value);
bool is_paused() const { return paused_; }
void Pause();
void Resume();
protected:
int GetContextId(uint32_t guest_ptr);
private:
void WorkerThreadMain();
static uint32_t MMIOReadRegisterThunk(void* ppc_context, XmaDecoder* as, uint32_t addr) {
return as->ReadRegister(addr);
}
static void MMIOWriteRegisterThunk(void* ppc_context, XmaDecoder* as, uint32_t addr,
uint32_t value) {
as->WriteRegister(addr, value);
}
protected:
memory::Memory* memory_ = nullptr;
runtime::FunctionDispatcher* function_dispatcher_ = nullptr;
std::atomic<bool> worker_running_ = {false};
system::object_ref<system::XHostThread> worker_thread_;
std::unique_ptr<rex::thread::Event> work_event_ = nullptr;
std::atomic<bool> paused_ = false;
rex::thread::Fence pause_fence_; // Signaled when worker paused.
rex::thread::Fence resume_fence_; // Signaled when resume requested.
XmaRegisterFile register_file_;
static const uint32_t kContextCount = 320;
XmaContext contexts_[kContextCount];
bit::BitMap context_bitmap_;
uint32_t context_data_first_ptr_ = 0;
uint32_t context_data_last_ptr_ = 0;
};
} // namespace rex::audio
@@ -1,46 +0,0 @@
/**
******************************************************************************
* Xenia : Xbox 360 Emulator Research Project *
******************************************************************************
* Copyright 2021 Ben Vanik. All rights reserved. *
* Released under the BSD license - see LICENSE in the root for more details. *
******************************************************************************
*
* @modified Tom Clay, 2026 - Adapted for ReXGlue runtime
*/
// This file contains some functions used to help parse XMA data.
#pragma once
#include <stdint.h>
namespace rex::audio::xma {
static constexpr uint32_t kMaxFrameLength = 0x7FFF;
// Get number of frames that /begin/ in this packet. Valid only for XMA2 packets.
inline uint8_t GetPacketFrameCount(const uint8_t* packet) {
return packet[0] >> 2;
}
// Get the first frame offset in bits
inline uint32_t GetPacketFrameOffset(const uint8_t* packet) {
uint32_t val =
static_cast<uint16_t>(((packet[0] & 0x3) << 13) | (packet[1] << 5) | (packet[2] >> 3));
return val + 32;
}
inline uint8_t GetPacketMetadata(const uint8_t* packet) {
return packet[2] & 0x7;
}
inline bool IsPacketXma2Type(const uint8_t* packet) {
return GetPacketMetadata(packet) == 1;
}
inline uint8_t GetPacketSkipCount(const uint8_t* packet) {
return packet[3];
}
} // namespace rex::audio::xma
@@ -1,42 +0,0 @@
/**
******************************************************************************
* Xenia : Xbox 360 Emulator Research Project *
******************************************************************************
* Copyright 2021 Ben Vanik. All rights reserved. *
* Released under the BSD license - see LICENSE in the root for more details. *
******************************************************************************
*
* @modified Tom Clay, 2026 - Adapted for ReXGlue runtime
*/
#pragma once
#include <cstdint>
#include <cstdlib>
namespace rex::audio {
struct XmaRegister {
#define XE_XMA_REGISTER(index, name) static const uint32_t name = index;
#include <rex/audio/xma/register_table.inc>
#undef XE_XMA_REGISTER
};
struct XmaRegisterInfo {
const char* name;
};
class XmaRegisterFile {
public:
XmaRegisterFile();
static const XmaRegisterInfo* GetRegisterInfo(uint32_t index);
static const size_t kRegisterCount = (0xFFFF + 1) / 4;
uint32_t values[kRegisterCount];
uint32_t operator[](uint32_t reg) const { return values[reg]; }
uint32_t& operator[](uint32_t reg) { return values[reg]; }
};
} // namespace rex::audio
@@ -1,81 +0,0 @@
/**
******************************************************************************
* Xenia : Xbox 360 Emulator Research Project *
******************************************************************************
* Copyright 2021 Ben Vanik. All rights reserved. *
* Released under the BSD license - see LICENSE in the root for more details. *
******************************************************************************
*/
// This is a partial file designed to be included by other files when
// constructing various tables.
#ifndef XE_XMA_REGISTER
#define XE_XMA_REGISTER(index, name)
#define __XE_XMA_REGISTER_UNSET
#endif
#ifndef XE_XMA_REGISTER_CONTEXT_GROUP
#define XE_XMA_REGISTER_CONTEXT_GROUP(index, suffix) \
XE_XMA_REGISTER(index + 0, Context0##suffix) \
XE_XMA_REGISTER(index + 1, Context1##suffix) \
XE_XMA_REGISTER(index + 2, Context2##suffix) \
XE_XMA_REGISTER(index + 3, Context3##suffix) \
XE_XMA_REGISTER(index + 4, Context4##suffix) \
XE_XMA_REGISTER(index + 5, Context5##suffix) \
XE_XMA_REGISTER(index + 6, Context6##suffix) \
XE_XMA_REGISTER(index + 7, Context7##suffix) \
XE_XMA_REGISTER(index + 8, Context8##suffix) \
XE_XMA_REGISTER(index + 9, Context9##suffix)
#endif
// 0x0000..0x001F : ???
// 0x0020..0x03FF : all 0xFFs?
// 0x0400..0x043F : ???
// 0x0440..0x047F : all 0xFFs?
// 0x0480..0x048B : ???
// 0x048C..0x04C0 : all 0xFFs?
// 0x04C1..0x04CB : ???
// 0x04CC..0x04FF : all 0xFFs?
// 0x0500..0x051F : ???
// 0x0520..0x057F : all 0xFFs?
// 0x0580..0x058F : ???
// 0x0590..0x05FF : all 0xFFs?
// XMA stuff is probably only 0x0600..0x06FF
//---------------------------------------------------------------------------//
XE_XMA_REGISTER(0x0600, ContextArrayAddress)
// 0x0601..0x0605 : ???
XE_XMA_REGISTER(0x0606, CurrentContextIndex)
XE_XMA_REGISTER(0x0607, NextContextIndex)
// 0x0608 : ???
// 0x0609..0x060F : zero?
XE_XMA_REGISTER_CONTEXT_GROUP(0x0610, Unknown610)
// 0x061A..0x061F : zero?
XE_XMA_REGISTER_CONTEXT_GROUP(0x0620, Unknown620)
// 0x062A..0x0641 : zero?
// 0x0642..0x0644 : ???
// 0x0645..0x064F : zero?
XE_XMA_REGISTER_CONTEXT_GROUP(0x0650, Kick)
// 0x065A..0x065F : zero?
XE_XMA_REGISTER_CONTEXT_GROUP(0x0660, Unknown660)
// 0x066A..0x0681 : zero?
// 0x0682..0x0684 : ???
// 0x0685..0x068F : zero?
XE_XMA_REGISTER_CONTEXT_GROUP(0x0690, Lock)
// 0x069A..0x069F : zero?
XE_XMA_REGISTER_CONTEXT_GROUP(0x06A0, Clear)
//---------------------------------------------------------------------------//
// 0x0700..0x07FF : all 0xFFs?
// 0x0800..0x17FF : ???
// 0x1800..0x2FFF : all 0xFFs?
// 0x3000..0x30FF : ???
// 0x3100..0x3FFF : all 0xFFs?
#ifdef __XE_XMA_REGISTER_UNSET
#undef __XE_XMA_REGISTER_UNSET
#undef XE_XMA_REGISTER
#endif
@@ -1,27 +0,0 @@
# rexaudio - Audio subsystem library
# Audio processing and XMA decoding for Xbox 360 emulation
add_library(rexaudio STATIC
audio_driver.cpp
audio_system.cpp
xma_context.cpp
xma_decoder.cpp
xma_register_file.cpp
# NOP backend (always available)
nop/nop_audio_system.cpp
# SDL backend
sdl/sdl_audio_system.cpp
sdl/sdl_audio_driver.cpp
)
add_library(rex::audio ALIAS rexaudio)
target_include_directories(rexaudio PUBLIC
$<BUILD_INTERFACE:${PROJECT_SOURCE_DIR}/include>
$<INSTALL_INTERFACE:${CMAKE_INSTALL_INCLUDEDIR}>
)
target_link_libraries(rexaudio
PUBLIC rexcore SDL3::SDL3
PRIVATE libavcodec libavutil
)
@@ -1,24 +0,0 @@
/**
******************************************************************************
* Xenia : Xbox 360 Emulator Research Project *
******************************************************************************
* Copyright 2013 Ben Vanik. All rights reserved. *
* Released under the BSD license - see LICENSE in the root for more details. *
******************************************************************************
*
* @modified Tom Clay, 2026 - Adapted for ReXGlue runtime
*/
#include <rex/audio/audio_driver.h>
namespace rex::audio {
AudioDriver::AudioDriver(memory::Memory* memory) : memory_(memory) {}
AudioDriver::~AudioDriver() = default;
AudioDriverTelemetry AudioDriver::GetTelemetry() const {
return {};
}
} // namespace rex::audio
@@ -1,493 +0,0 @@
/**
******************************************************************************
* Xenia : Xbox 360 Emulator Research Project *
******************************************************************************
* Copyright 2022 Ben Vanik. All rights reserved. *
* Released under the BSD license - see LICENSE in the root for more details. *
******************************************************************************
*
* @modified Tom Clay, 2026 - Adapted for ReXGlue runtime
*/
#include <chrono>
#include <rex/assert.h>
#include <rex/audio/audio_driver.h>
#include <rex/audio/audio_system.h>
#include <rex/audio/flags.h>
#include <rex/audio/xma/decoder.h>
#include <rex/dbg.h>
#include <rex/logging.h>
#include <rex/math.h>
#include <rex/memory/ring_buffer.h>
#include <rex/stream.h>
#include <rex/string/buffer.h>
#include <rex/system/thread_state.h>
#include <rex/thread.h>
#include <rex/cvar.h>
REXCVAR_DECLARE(bool, audio_trace_telemetry);
REXCVAR_DECLARE(bool, ac6_audio_deep_trace);
REXCVAR_DEFINE_INT32(
audio_maxqframes, 8, "Audio",
"Max buffered audio frames (range 4-64). Lower reduces latency but may cause stuttering.");
REXCVAR_DEFINE_BOOL(audio_trace_worker_cadence, false, "Audio",
"Trace render-driver callback cadence, callback duration, and worker wakeups");
REXCVAR_DEFINE_BOOL(audio_trace_render_driver_verbose, false, "Audio",
"Trace render-driver client lifecycle and queue state on every important "
"transition");
// As with normal Microsoft, there are like twelve different ways to access
// the audio APIs. Early games use XMA*() methods almost exclusively to touch
// decoders. Later games use XAudio*() and direct memory writes to the XMA
// structures (as opposed to the XMA* calls), meaning that we have to support
// both.
//
// For ease of implementation, most audio related processing is handled in
// AudioSystem, and the functions here call off to it.
// The XMA*() functions just manipulate the audio system in the guest context
// and let the normal AudioSystem handling take it, to prevent duplicate
// implementations. They can be found in xboxkrnl_audio_xma.cc
namespace rex::audio {
using Clock = std::chrono::steady_clock;
namespace {
bool IsDeepTraceEnabled() {
return REXCVAR_GET(ac6_audio_deep_trace);
}
}
AudioSystem::AudioSystem(runtime::FunctionDispatcher* function_dispatcher)
: memory_(function_dispatcher->memory()),
function_dispatcher_(function_dispatcher),
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, queued_frames_);
assert_not_null(client_semaphores_[i]);
wait_handles_[i] = client_semaphores_[i].get();
}
shutdown_event_ = rex::thread::Event::CreateAutoResetEvent(false);
assert_not_null(shutdown_event_);
wait_handles_[kMaximumClientCount] = shutdown_event_.get();
xma_decoder_ = std::make_unique<rex::audio::XmaDecoder>(function_dispatcher_);
resume_event_ = rex::thread::Event::CreateAutoResetEvent(false);
assert_not_null(resume_event_);
}
AudioSystem::~AudioSystem() {
if (xma_decoder_) {
xma_decoder_->Shutdown();
}
}
X_STATUS AudioSystem::Setup(system::KernelState* kernel_state) {
X_STATUS result = xma_decoder_->Setup(kernel_state);
if (result) {
return result;
}
worker_running_ = true;
worker_thread_ = system::object_ref<system::XHostThread>(
new system::XHostThread(kernel_state, 128 * 1024, 0, [this]() {
WorkerThreadMain();
return 0;
}));
worker_thread_->set_name("Audio Worker");
worker_thread_->Create();
return X_STATUS_SUCCESS;
}
void AudioSystem::WorkerThreadMain() {
// Initialize driver and ringbuffer.
Initialize();
// Main run loop.
uint32_t diag_pump_count = 0;
std::array<Clock::time_point, kMaximumClientCount> last_callback_times{};
std::array<uint64_t, kMaximumClientCount> callback_counts{};
uint64_t timeout_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
// (signaling a sample has finished playing)
auto result = rex::thread::WaitAny(wait_handles_, rex::countof(wait_handles_), true,
std::chrono::milliseconds(500));
if (result.first == rex::thread::WaitResult::kFailed) {
REXAPU_WARN("AudioWorker: WaitAny failed");
continue;
}
if (result.first == rex::thread::WaitResult::kTimeout) {
++timeout_count;
if (diag_pump_count < 5) {
REXAPU_DEBUG("AudioWorker: WaitAny timed out (no semaphore signals)");
}
if (REXCVAR_GET(audio_trace_worker_cadence) || IsDeepTraceEnabled()) {
REXAPU_WARN("AudioWorker: WaitAny timeout count={}", timeout_count);
}
}
if (result.first == thread::WaitResult::kSuccess && result.second == kMaximumClientCount) {
// Shutdown event signaled.
if (paused_) {
pause_fence_.Signal();
thread::Wait(resume_event_.get(), false);
}
continue;
}
// Number of clients pumped
bool pumped = false;
if (result.first == rex::thread::WaitResult::kSuccess) {
auto index = result.second;
auto global_lock = global_critical_region_.Acquire();
uint32_t client_callback = clients_[index].callback;
uint32_t client_callback_arg = clients_[index].wrapped_callback_arg;
AudioDriver* driver = clients_[index].driver;
global_lock.unlock();
if (client_callback) {
const auto before_callback = Clock::now();
const double since_last_callback_ms =
last_callback_times[index].time_since_epoch().count() == 0
? -1.0
: std::chrono::duration<double, std::milli>(
before_callback - last_callback_times[index])
.count();
++callback_counts[index];
if (REXCVAR_GET(audio_trace_worker_cadence) || IsDeepTraceEnabled()) {
const auto telemetry = driver ? driver->GetTelemetry() : AudioDriverTelemetry{};
REXAPU_DEBUG(
"AudioWorker: callback-dispatch client={} callback={:08X} arg={:08X} count={} "
"since_last_ms={:.3f} submitted={} consumed={} queued_depth={} underruns={} "
"silence_injections={}",
index, client_callback, client_callback_arg, callback_counts[index],
since_last_callback_ms, telemetry.submitted_frames, telemetry.consumed_frames,
telemetry.queued_depth, telemetry.underrun_count, telemetry.silence_injections);
}
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};
function_dispatcher_->Execute(worker_thread_->thread_state(), client_callback, args,
rex::countof(args));
if (diag_pump_count < 10) {
REXAPU_DEBUG("AudioWorker: callback returned for client {}", index);
}
if (REXCVAR_GET(audio_trace_worker_cadence) || IsDeepTraceEnabled()) {
const auto after_callback = Clock::now();
const auto telemetry = driver ? driver->GetTelemetry() : AudioDriverTelemetry{};
REXAPU_DEBUG(
"AudioWorker: callback-return client={} duration_ms={:.3f} submitted={} "
"consumed={} queued_depth={} peak={} underruns={} silence_injections={}",
index,
std::chrono::duration<double, std::milli>(after_callback - before_callback).count(),
telemetry.submitted_frames, telemetry.consumed_frames, telemetry.queued_depth,
telemetry.peak_queued_depth, telemetry.underrun_count,
telemetry.silence_injections);
last_callback_times[index] = after_callback;
} else {
last_callback_times[index] = before_callback;
}
diag_pump_count++;
} else {
const auto telemetry = driver ? driver->GetTelemetry() : AudioDriverTelemetry{};
REXAPU_DEBUG(
"AudioWorker: semaphore signaled for client {} but callback is 0 "
"submitted={} consumed={} queued_depth={} underruns={}",
index, telemetry.submitted_frames, telemetry.consumed_frames, telemetry.queued_depth,
telemetry.underrun_count);
}
pumped = true;
}
if (!worker_running_) {
break;
}
if (!pumped) {
SCOPE_profile_cpu_i("apu", "Sleep");
rex::thread::Sleep(std::chrono::milliseconds(500));
}
}
worker_running_ = false;
// TODO(benvanik): call module API to kill?
}
int AudioSystem::FindFreeClient() {
for (size_t i = 0; i < kMaximumClientCount; i++) {
auto& client = clients_[i];
if (!client.in_use) {
return i;
}
}
return -1;
}
void AudioSystem::Initialize() {}
void AudioSystem::Shutdown() {
if (!worker_running_) {
return;
}
// Shut down XMA decoder first - its worker can stall in FFmpeg
if (xma_decoder_) {
xma_decoder_->Shutdown();
}
worker_running_ = false;
shutdown_event_->Set();
if (worker_thread_) {
// The worker may be stuck inside a guest callback that is itself blocked
// on guest objects (e.g. KeWaitForMultipleObjects).
// Terminate the thread to break the deadlock.
worker_thread_->Terminate(0);
worker_thread_.reset();
}
// Destroy all active client drivers (closes SDL audio devices, stopping
// callback threads) before the semaphores they reference are destroyed.
for (size_t i = 0; i < kMaximumClientCount; i++) {
if (clients_[i].in_use) {
DestroyDriver(clients_[i].driver);
if (clients_[i].wrapped_callback_arg) {
memory()->SystemHeapFree(clients_[i].wrapped_callback_arg);
}
clients_[i] = {nullptr, 0, 0, 0, false};
}
}
}
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(queued_frames_, nullptr);
assert_true(ret);
AudioDriver* driver;
auto result = CreateDriver(index, client_semaphore, &driver);
if (XFAILED(result)) {
return result;
}
assert_not_null(driver);
uint32_t ptr = memory()->SystemHeapAlloc(0x4);
memory::store_and_swap<uint32_t>(memory()->TranslateVirtual(ptr), callback_arg);
clients_[index] = {driver, callback, callback_arg, ptr, true};
if (REXCVAR_GET(audio_trace_render_driver_verbose) || IsDeepTraceEnabled()) {
const auto telemetry = driver->GetTelemetry();
REXAPU_DEBUG(
"AudioSystem::RegisterClient created client index={} callback={:08X} callback_arg={:08X} "
"wrapped_arg={:08X} queued_frames={} submitted={} consumed={} queued_depth={}",
index, callback, callback_arg, ptr, queued_frames_, telemetry.submitted_frames,
telemetry.consumed_frames, telemetry.queued_depth);
}
if (out_index) {
*out_index = index;
}
return X_STATUS_SUCCESS;
}
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);
(clients_[index].driver)->SubmitFrame(samples_ptr);
if (REXCVAR_GET(audio_trace_render_driver_verbose) || IsDeepTraceEnabled()) {
const auto telemetry = clients_[index].driver->GetTelemetry();
if (telemetry.submitted_frames <= 24 || (telemetry.submitted_frames % 60) == 0 ||
telemetry.queued_depth <= 1 || telemetry.underrun_count != 0) {
REXAPU_DEBUG(
"AudioSystem::SubmitFrame telemetry: index={} samples_ptr={:08X} submitted={} "
"consumed={} queued_depth={} peak={} underruns={} silence_injections={}",
index, samples_ptr, telemetry.submitted_frames, telemetry.consumed_frames,
telemetry.queued_depth, telemetry.peak_queued_depth, telemetry.underrun_count,
telemetry.silence_injections);
}
}
}
AudioDriverTelemetry AudioSystem::GetClientTelemetry(size_t index) {
auto global_lock = global_critical_region_.Acquire();
if (index >= kMaximumClientCount || !clients_[index].in_use || !clients_[index].driver) {
return {};
}
return clients_[index].driver->GetTelemetry();
}
uint32_t AudioSystem::GetClientRenderDriverTic(size_t index) {
const auto telemetry = GetClientTelemetry(index);
return telemetry.consumed_frames * kRenderDriverTicSamplesPerFrame;
}
void AudioSystem::UnregisterClient(size_t index) {
SCOPE_profile_cpu_f("apu");
auto global_lock = global_critical_region_.Acquire();
assert_true(index < kMaximumClientCount);
if ((REXCVAR_GET(audio_trace_render_driver_verbose) || IsDeepTraceEnabled()) &&
clients_[index].driver) {
const auto telemetry = clients_[index].driver->GetTelemetry();
REXAPU_DEBUG(
"AudioSystem::UnregisterClient before-destroy index={} callback={:08X} callback_arg={:08X} "
"wrapped_arg={:08X} submitted={} consumed={} queued_depth={} peak={} underruns={} "
"silence_injections={}",
index, clients_[index].callback, clients_[index].callback_arg,
clients_[index].wrapped_callback_arg, telemetry.submitted_frames,
telemetry.consumed_frames, telemetry.queued_depth, telemetry.peak_queued_depth,
telemetry.underrun_count, telemetry.silence_injections);
}
DestroyDriver(clients_[index].driver);
memory()->SystemHeapFree(clients_[index].wrapped_callback_arg);
clients_[index] = {nullptr, 0, 0, 0, false};
// Drain the semaphore of its count.
auto client_semaphore = client_semaphores_[index].get();
rex::thread::WaitResult wait_result;
do {
wait_result = rex::thread::Wait(client_semaphore, false, std::chrono::milliseconds(0));
} while (wait_result == rex::thread::WaitResult::kSuccess);
assert_true(wait_result == rex::thread::WaitResult::kTimeout);
}
bool AudioSystem::Save(stream::ByteStream* stream) {
stream->Write(kAudioSaveSignature);
// Count the number of used clients first.
// Any gaps should be handled gracefully.
uint32_t used_clients = 0;
for (size_t i = 0; i < kMaximumClientCount; i++) {
if (clients_[i].in_use) {
used_clients++;
}
}
stream->Write(used_clients);
for (uint32_t i = 0; i < kMaximumClientCount; i++) {
auto& client = clients_[i];
if (!client.in_use) {
continue;
}
stream->Write(i);
stream->Write(client.callback);
stream->Write(client.callback_arg);
stream->Write(client.wrapped_callback_arg);
}
return true;
}
bool AudioSystem::Restore(stream::ByteStream* stream) {
if (stream->Read<uint32_t>() != kAudioSaveSignature) {
REXAPU_ERROR("AudioSystem::Restore - Invalid magic value!");
return false;
}
uint32_t num_clients = stream->Read<uint32_t>();
for (uint32_t i = 0; i < num_clients; i++) {
auto id = stream->Read<uint32_t>();
assert_true(id < kMaximumClientCount);
auto& client = clients_[id];
// Reset the semaphore and recreate the driver ourselves.
if (client.driver) {
UnregisterClient(id);
}
client.callback = stream->Read<uint32_t>();
client.callback_arg = stream->Read<uint32_t>();
client.wrapped_callback_arg = stream->Read<uint32_t>();
client.in_use = true;
auto client_semaphore = client_semaphores_[id].get();
auto ret = client_semaphore->Release(queued_frames_, nullptr);
assert_true(ret);
AudioDriver* driver = nullptr;
auto status = CreateDriver(id, client_semaphore, &driver);
if (XFAILED(status)) {
REXAPU_ERROR(
"AudioSystem::Restore - Call to CreateDriver failed with status "
"{:08X}",
status);
return false;
}
assert_not_null(driver);
client.driver = driver;
}
return true;
}
void AudioSystem::Pause() {
if (paused_) {
return;
}
paused_ = true;
// Kind of a hack, but it works.
shutdown_event_->Set();
pause_fence_.Wait();
xma_decoder_->Pause();
}
void AudioSystem::Resume() {
if (!paused_) {
return;
}
paused_ = false;
resume_event_->Set();
xma_decoder_->Resume();
}
} // namespace rex::audio
@@ -1,37 +0,0 @@
/**
******************************************************************************
* Xenia : Xbox 360 Emulator Research Project *
******************************************************************************
* Copyright 2013 Ben Vanik. All rights reserved. *
* Released under the BSD license - see LICENSE in the root for more details. *
******************************************************************************
*
* @modified Tom Clay, 2026 - Adapted for ReXGlue runtime
*/
#include <rex/audio/flags.h>
#include <rex/audio/nop/nop_audio_system.h>
namespace rex::audio::nop {
std::unique_ptr<AudioSystem> NopAudioSystem::Create(
runtime::FunctionDispatcher* function_dispatcher) {
return std::make_unique<NopAudioSystem>(function_dispatcher);
}
NopAudioSystem::NopAudioSystem(runtime::FunctionDispatcher* function_dispatcher)
: AudioSystem(function_dispatcher) {}
NopAudioSystem::~NopAudioSystem() = default;
X_STATUS NopAudioSystem::CreateDriver(size_t index, rex::thread::Semaphore* semaphore,
AudioDriver** out_driver) {
return X_STATUS_NOT_IMPLEMENTED;
}
void NopAudioSystem::DestroyDriver(AudioDriver* driver) {
(void)driver;
assert_always();
}
} // namespace rex::audio::nop
@@ -1,516 +0,0 @@
/**
******************************************************************************
* Xenia : Xbox 360 Emulator Research Project *
******************************************************************************
* Copyright 2020 Ben Vanik. All rights reserved. *
* Released under the BSD license - see LICENSE in the root for more details. *
******************************************************************************
*
* @modified Tom Clay, 2026 - Adapted for ReXGlue runtime
*/
#include <algorithm>
#include <array>
#include <chrono>
#include <cmath>
#include <cstring>
#include <limits>
#include <rex/assert.h>
#include <rex/audio/conversion.h>
#include <rex/audio/flags.h>
#include <rex/audio/sdl/sdl_audio_driver.h>
#include <rex/cvar.h>
#include <rex/dbg.h>
#include <rex/logging.h>
#include <SDL3/SDL.h>
REXCVAR_DEFINE_BOOL(audio_mute, false, "Audio", "Mute audio output");
REXCVAR_DEFINE_BOOL(audio_trace_telemetry, false, "Audio",
"Trace SDL audio queue depth, underruns, and playback telemetry");
REXCVAR_DECLARE(bool, audio_trace_render_driver_verbose);
REXCVAR_DECLARE(bool, ac6_audio_deep_trace);
namespace rex::audio::sdl {
using Clock = std::chrono::steady_clock;
namespace {
bool IsDeepTraceEnabled() {
return REXCVAR_GET(ac6_audio_deep_trace);
}
struct OutputChunkStats {
float min_sample = std::numeric_limits<float>::infinity();
float max_sample = -std::numeric_limits<float>::infinity();
double sum_squares = 0.0;
uint32_t sample_count = 0;
uint32_t zeroish_samples = 0;
bool has_nonfinite = false;
};
float ByteSwapFloatWord(uint32_t value) {
value = rex::byte_swap(value);
float result = 0.0f;
std::memcpy(&result, &value, sizeof(result));
return result;
}
OutputChunkStats AnalyzeOutputChunk(const float* data, size_t sample_count) {
OutputChunkStats stats;
for (size_t i = 0; i < sample_count; ++i) {
const float sample = data[i];
if (!std::isfinite(sample)) {
stats.has_nonfinite = true;
continue;
}
stats.min_sample = std::min(stats.min_sample, sample);
stats.max_sample = std::max(stats.max_sample, sample);
stats.sum_squares += static_cast<double>(sample) * static_cast<double>(sample);
++stats.sample_count;
if (std::fabs(sample) <= 1.0e-6f) {
++stats.zeroish_samples;
}
}
if (stats.sample_count == 0) {
stats.min_sample = 0.0f;
stats.max_sample = 0.0f;
}
return stats;
}
OutputChunkStats AnalyzeGuestFrame(const float* data, size_t sample_count) {
OutputChunkStats stats;
const auto* words = reinterpret_cast<const uint32_t*>(data);
for (size_t i = 0; i < sample_count; ++i) {
const float sample = ByteSwapFloatWord(words[i]);
if (!std::isfinite(sample)) {
stats.has_nonfinite = true;
continue;
}
stats.min_sample = std::min(stats.min_sample, sample);
stats.max_sample = std::max(stats.max_sample, sample);
stats.sum_squares += static_cast<double>(sample) * static_cast<double>(sample);
++stats.sample_count;
if (std::fabs(sample) <= 1.0e-6f) {
++stats.zeroish_samples;
}
}
if (stats.sample_count == 0) {
stats.min_sample = 0.0f;
stats.max_sample = 0.0f;
}
return stats;
}
void MergeOutputChunkStats(OutputChunkStats& dst, const OutputChunkStats& src) {
dst.min_sample = std::min(dst.min_sample, src.min_sample);
dst.max_sample = std::max(dst.max_sample, src.max_sample);
dst.sum_squares += src.sum_squares;
dst.sample_count += src.sample_count;
dst.zeroish_samples += src.zeroish_samples;
dst.has_nonfinite = dst.has_nonfinite || src.has_nonfinite;
if (dst.sample_count == 0) {
dst.min_sample = 0.0f;
dst.max_sample = 0.0f;
}
}
}
SDLAudioDriver::SDLAudioDriver(memory::Memory* memory, rex::thread::Semaphore* semaphore)
: AudioDriver(memory), semaphore_(semaphore) {}
SDLAudioDriver::~SDLAudioDriver() {
assert_true(frames_queued_.empty());
assert_true(frames_unused_.empty());
assert_true(pending_output_float_count_ == 0);
assert_true(pending_output_float_offset_ == 0);
}
bool SDLAudioDriver::Initialize() {
// Prevent SDL from interfering with timer resolution (causes FPS drops)
SDL_SetHintWithPriority(SDL_HINT_TIMER_RESOLUTION, "0", SDL_HINT_OVERRIDE);
// Set audio category for proper OS audio handling
SDL_SetHint(SDL_HINT_AUDIO_CATEGORY, "playback");
// Set app name for audio device identification
SDL_SetAppMetadataProperty(SDL_PROP_APP_METADATA_NAME_STRING, "rexglue");
if (!SDL_InitSubSystem(SDL_INIT_AUDIO)) {
REXAPU_ERROR("SDL_InitSubSystem(SDL_INIT_AUDIO) failed: {}", SDL_GetError());
return false;
}
sdl_initialized_ = true;
SDL_AudioSpec desired_spec = {};
SDL_AudioSpec obtained_spec;
desired_spec.freq = frame_frequency_;
desired_spec.format = SDL_AUDIO_F32LE;
desired_spec.channels = frame_channels_;
sdl_device_channels_ = frame_channels_;
sdl_stream_ = SDL_OpenAudioDeviceStream(SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK, &desired_spec,
SDLCallback, this);
if (!sdl_stream_) {
REXAPU_ERROR("SDL_OpenAudioDevice() failed: {}", SDL_GetError());
return false;
}
SDL_GetAudioDeviceFormat(SDL_GetAudioStreamDevice(sdl_stream_), &obtained_spec, NULL);
if (obtained_spec.channels == 2) {
SDL_DestroyAudioStream(sdl_stream_);
desired_spec.channels = 2;
sdl_device_channels_ = 2;
sdl_stream_ = SDL_OpenAudioDeviceStream(SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK, &desired_spec,
SDLCallback, this);
}
SDL_ResumeAudioDevice(SDL_GetAudioStreamDevice(sdl_stream_));
if (REXCVAR_GET(audio_trace_render_driver_verbose) || IsDeepTraceEnabled()) {
REXAPU_DEBUG(
"SDLAudioDriver::Initialize stream={:p} requested_freq={} requested_channels={} "
"obtained_freq={} obtained_channels={} active_device_channels={}",
static_cast<void*>(sdl_stream_), frame_frequency_, frame_channels_, obtained_spec.freq,
obtained_spec.channels, sdl_device_channels_);
}
return true;
}
void SDLAudioDriver::SubmitFrame(uint32_t frame_ptr) {
if (shutting_down_.load(std::memory_order_acquire)) {
return;
}
const auto input_frame = memory_->TranslateVirtual<float*>(frame_ptr);
const auto guest_frame_stats = AnalyzeGuestFrame(input_frame, frame_samples_);
float* output_frame;
{
std::unique_lock<std::mutex> guard(frames_mutex_);
if (frames_unused_.empty()) {
output_frame = new float[frame_samples_];
} else {
output_frame = frames_unused_.top();
frames_unused_.pop();
}
}
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);
}
const uint32_t submitted = submitted_frames_.fetch_add(1, std::memory_order_relaxed) + 1;
const uint32_t queued_depth =
queued_depth_.fetch_add(1, std::memory_order_relaxed) + 1;
uint32_t previous_peak = peak_queued_depth_.load(std::memory_order_relaxed);
while (queued_depth > previous_peak &&
!peak_queued_depth_.compare_exchange_weak(previous_peak, queued_depth,
std::memory_order_relaxed)) {
}
if (REXCVAR_GET(audio_trace_telemetry) &&
(submitted <= 12 || (submitted % 120) == 0)) {
REXAPU_DEBUG(
"SDLAudioDriver::SubmitFrame: frame_ptr={:08X} submitted={} queued_depth={} peak={}",
frame_ptr, submitted, queued_depth, peak_queued_depth_.load(std::memory_order_relaxed));
}
if ((REXCVAR_GET(audio_trace_render_driver_verbose) || IsDeepTraceEnabled()) &&
(submitted <= 24 || (submitted % 60) == 0 || queued_depth <= 1)) {
const double guest_rms =
guest_frame_stats.sample_count == 0
? 0.0
: std::sqrt(guest_frame_stats.sum_squares /
static_cast<double>(guest_frame_stats.sample_count));
const double guest_zeroish_pct =
guest_frame_stats.sample_count == 0
? 0.0
: (static_cast<double>(guest_frame_stats.zeroish_samples) * 100.0) /
static_cast<double>(guest_frame_stats.sample_count);
REXAPU_DEBUG(
"SDLAudioDriver::SubmitFrame verbose: frame_ptr={:08X} submitted={} consumed={} "
"queued_depth={} peak={} underruns={} silence_injections={} guest_min={:.6f} "
"guest_max={:.6f} guest_rms={:.6f} guest_zeroish_pct={:.2f} guest_nonfinite={}",
frame_ptr, submitted, consumed_frames_.load(std::memory_order_relaxed), queued_depth,
peak_queued_depth_.load(std::memory_order_relaxed),
underrun_count_.load(std::memory_order_relaxed),
silence_injections_.load(std::memory_order_relaxed), guest_frame_stats.min_sample,
guest_frame_stats.max_sample, guest_rms, guest_zeroish_pct,
guest_frame_stats.has_nonfinite);
}
}
void SDLAudioDriver::Shutdown() {
if (sdl_stream_) {
if (REXCVAR_GET(audio_trace_render_driver_verbose) || IsDeepTraceEnabled()) {
const auto telemetry = GetTelemetry();
REXAPU_DEBUG(
"SDLAudioDriver::Shutdown stream={:p} submitted={} consumed={} underruns={} "
"silence_injections={} queued_depth={} peak={}",
static_cast<void*>(sdl_stream_), telemetry.submitted_frames, telemetry.consumed_frames,
telemetry.underrun_count, telemetry.silence_injections, telemetry.queued_depth,
telemetry.peak_queued_depth);
}
shutting_down_.store(true, std::memory_order_release);
// Disable device processing first, then unregister the callback. SDL
// blocks until any callback already in flight has returned.
if (!SDL_PauseAudioStreamDevice(sdl_stream_)) {
REXAPU_WARN("SDL_PauseAudioStreamDevice failed during shutdown: {}", SDL_GetError());
}
if (!SDL_SetAudioStreamGetCallback(sdl_stream_, nullptr, nullptr)) {
REXAPU_WARN("SDL_SetAudioStreamGetCallback(nullptr) failed during shutdown: {}",
SDL_GetError());
}
if (SDL_LockAudioStream(sdl_stream_)) {
if (!SDL_ClearAudioStream(sdl_stream_)) {
REXAPU_WARN("SDL_ClearAudioStream failed during shutdown: {}", SDL_GetError());
}
if (!SDL_UnlockAudioStream(sdl_stream_)) {
REXAPU_WARN("SDL_UnlockAudioStream failed during shutdown: {}", SDL_GetError());
}
} else {
REXAPU_WARN("SDL_LockAudioStream failed during shutdown: {}", SDL_GetError());
}
SDL_DestroyAudioStream(sdl_stream_);
sdl_stream_ = nullptr;
}
if (sdl_initialized_) {
SDL_QuitSubSystem(SDL_INIT_AUDIO);
sdl_initialized_ = false;
}
std::unique_lock<std::mutex> guard(frames_mutex_);
while (!frames_unused_.empty()) {
delete[] frames_unused_.top();
frames_unused_.pop();
}
while (!frames_queued_.empty()) {
delete[] frames_queued_.front();
frames_queued_.pop();
}
pending_output_float_count_ = 0;
pending_output_float_offset_ = 0;
}
AudioDriverTelemetry SDLAudioDriver::GetTelemetry() const {
return AudioDriverTelemetry{
submitted_frames_.load(std::memory_order_relaxed),
consumed_frames_.load(std::memory_order_relaxed),
underrun_count_.load(std::memory_order_relaxed),
silence_injections_.load(std::memory_order_relaxed),
queued_depth_.load(std::memory_order_relaxed),
peak_queued_depth_.load(std::memory_order_relaxed),
};
}
void SDLAudioDriver::SDLCallback(void* userdata, SDL_AudioStream* stream, int additional_amount,
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);
if (driver->shutting_down_.load(std::memory_order_acquire)) {
return;
}
const int len = static_cast<int>(sizeof(float) * channel_samples_ * driver->sdl_device_channels_);
static Clock::time_point last_callback_time{};
const auto callback_start = Clock::now();
const int requested_amount = additional_amount;
const int stream_queued_before = SDL_GetAudioStreamQueued(stream);
const double since_last_callback_ms =
last_callback_time.time_since_epoch().count() == 0
? -1.0
: std::chrono::duration<double, std::milli>(callback_start - last_callback_time).count();
if (REXCVAR_GET(audio_trace_render_driver_verbose) || IsDeepTraceEnabled()) {
REXAPU_DEBUG(
"SDLCallback begin stream={:p} additional_amount={} total_amount={} len={} "
"since_last_callback_ms={:.3f} queued_depth={} submitted={} consumed={} underruns={} "
"silence_injections={} stream_queued_bytes={}",
static_cast<void*>(stream), additional_amount, total_amount, len, since_last_callback_ms,
driver->queued_depth_.load(std::memory_order_relaxed),
driver->submitted_frames_.load(std::memory_order_relaxed),
driver->consumed_frames_.load(std::memory_order_relaxed),
driver->underrun_count_.load(std::memory_order_relaxed),
driver->silence_injections_.load(std::memory_order_relaxed), stream_queued_before);
}
float* data = SDL_stack_alloc(float, len / static_cast<int>(sizeof(float)));
const size_t output_frame_float_count =
channel_samples_ * static_cast<size_t>(driver->sdl_device_channels_);
int bytes_written = 0;
int silence_chunks = 0;
int audio_chunks = 0;
int stream_queued_peak = stream_queued_before;
OutputChunkStats aggregate_stats;
while (additional_amount > 0) {
static uint32_t sdl_callback_count = 0;
if (driver->pending_output_float_offset_ == driver->pending_output_float_count_) {
driver->pending_output_float_count_ = 0;
driver->pending_output_float_offset_ = 0;
float* buffer = nullptr;
{
std::unique_lock<std::mutex> guard(driver->frames_mutex_);
if (!driver->frames_queued_.empty()) {
buffer = driver->frames_queued_.front();
driver->frames_queued_.pop();
driver->queued_depth_.fetch_sub(1, std::memory_order_relaxed);
}
}
if (driver->shutting_down_.load(std::memory_order_acquire)) {
if (buffer) {
std::unique_lock<std::mutex> guard(driver->frames_mutex_);
driver->frames_unused_.push(buffer);
}
break;
}
if (buffer) {
if (!REXCVAR_GET(audio_mute)) {
switch (driver->sdl_device_channels_) {
case 2:
conversion::sequential_6_BE_to_interleaved_2_LE(
driver->pending_output_frame_.data(), buffer, channel_samples_);
break;
case 6:
conversion::sequential_6_BE_to_interleaved_6_LE(
driver->pending_output_frame_.data(), buffer, channel_samples_);
break;
default:
assert_unhandled_case(driver->sdl_device_channels_);
break;
}
} else {
std::memset(driver->pending_output_frame_.data(), 0,
output_frame_float_count * sizeof(float));
}
{
std::unique_lock<std::mutex> guard(driver->frames_mutex_);
driver->frames_unused_.push(buffer);
}
driver->pending_output_float_count_ = output_frame_float_count;
}
}
if (driver->pending_output_float_count_ == 0) {
const uint32_t underruns =
driver->underrun_count_.fetch_add(1, std::memory_order_relaxed) + 1;
const uint32_t silences =
driver->silence_injections_.fetch_add(1, std::memory_order_relaxed) + 1;
if (sdl_callback_count < 10) {
REXAPU_DEBUG("SDLCallback: no frames queued, emitting silence");
sdl_callback_count++;
}
if (REXCVAR_GET(audio_trace_telemetry) || IsDeepTraceEnabled()) {
REXAPU_WARN(
"SDLCallback underrun: count={} silence_injections={} queued_depth={} total_amount={} "
"additional_amount={}",
underruns, silences, driver->queued_depth_.load(std::memory_order_relaxed),
total_amount, additional_amount);
}
const int chunk_bytes = std::min(additional_amount, len);
std::memset(data, 0, chunk_bytes);
MergeOutputChunkStats(
aggregate_stats,
AnalyzeOutputChunk(data, chunk_bytes / static_cast<int>(sizeof(float))));
if (!SDL_PutAudioStreamData(stream, data, chunk_bytes)) {
REXAPU_ERROR("SDLCallback: SDL_PutAudioStreamData(silence) failed: {}", SDL_GetError());
break;
}
bytes_written += chunk_bytes;
++silence_chunks;
stream_queued_peak = std::max(stream_queued_peak, SDL_GetAudioStreamQueued(stream));
additional_amount -= chunk_bytes;
continue;
}
const size_t pending_float_count =
driver->pending_output_float_count_ - driver->pending_output_float_offset_;
const int chunk_bytes =
std::min(additional_amount,
static_cast<int>(pending_float_count * sizeof(float)));
const size_t chunk_float_count =
static_cast<size_t>(chunk_bytes / static_cast<int>(sizeof(float)));
const float* chunk_ptr =
driver->pending_output_frame_.data() + driver->pending_output_float_offset_;
const auto chunk_stats = AnalyzeOutputChunk(chunk_ptr, chunk_float_count);
if (!SDL_PutAudioStreamData(stream, chunk_ptr, chunk_bytes)) {
REXAPU_ERROR("SDLCallback: SDL_PutAudioStreamData(audio) failed: {}", SDL_GetError());
break;
}
bytes_written += chunk_bytes;
++audio_chunks;
MergeOutputChunkStats(aggregate_stats, chunk_stats);
stream_queued_peak = std::max(stream_queued_peak, SDL_GetAudioStreamQueued(stream));
driver->pending_output_float_offset_ += chunk_float_count;
if (driver->pending_output_float_offset_ == driver->pending_output_float_count_) {
driver->pending_output_float_count_ = 0;
driver->pending_output_float_offset_ = 0;
const uint32_t consumed =
driver->consumed_frames_.fetch_add(1, std::memory_order_relaxed) + 1;
if ((REXCVAR_GET(audio_trace_telemetry) || IsDeepTraceEnabled()) &&
(consumed <= 12 || (consumed % 120) == 0)) {
REXAPU_DEBUG(
"SDLCallback: consumed={} queued_depth={} peak={} underruns={} silence_injections={}",
consumed, driver->queued_depth_.load(std::memory_order_relaxed),
driver->peak_queued_depth_.load(std::memory_order_relaxed),
driver->underrun_count_.load(std::memory_order_relaxed),
driver->silence_injections_.load(std::memory_order_relaxed));
}
auto ret = driver->semaphore_->Release(1, nullptr);
assert_true(ret);
}
additional_amount -= chunk_bytes;
}
if (REXCVAR_GET(audio_trace_render_driver_verbose) || IsDeepTraceEnabled()) {
const auto callback_end = Clock::now();
const int stream_queued_after = SDL_GetAudioStreamQueued(stream);
const int oversupply_bytes = bytes_written - requested_amount;
const double output_rms =
aggregate_stats.sample_count == 0
? 0.0
: std::sqrt(aggregate_stats.sum_squares /
static_cast<double>(aggregate_stats.sample_count));
const double zeroish_pct =
aggregate_stats.sample_count == 0
? 0.0
: (static_cast<double>(aggregate_stats.zeroish_samples) * 100.0) /
static_cast<double>(aggregate_stats.sample_count);
REXAPU_DEBUG(
"SDLCallback end stream={:p} duration_ms={:.3f} queued_depth={} submitted={} consumed={} "
"underruns={} silence_injections={} requested_bytes={} written_bytes={} "
"oversupply_bytes={} stream_queued_before={} stream_queued_after={} "
"stream_queued_peak={} audio_chunks={} silence_chunks={} output_min={:.6f} "
"output_max={:.6f} output_rms={:.6f} zeroish_pct={:.2f} nonfinite={}",
static_cast<void*>(stream),
std::chrono::duration<double, std::milli>(callback_end - callback_start).count(),
driver->queued_depth_.load(std::memory_order_relaxed),
driver->submitted_frames_.load(std::memory_order_relaxed),
driver->consumed_frames_.load(std::memory_order_relaxed),
driver->underrun_count_.load(std::memory_order_relaxed),
driver->silence_injections_.load(std::memory_order_relaxed), requested_amount,
bytes_written, oversupply_bytes, stream_queued_before, stream_queued_after,
stream_queued_peak, audio_chunks, silence_chunks, aggregate_stats.min_sample,
aggregate_stats.max_sample, output_rms, zeroish_pct, aggregate_stats.has_nonfinite);
}
last_callback_time = callback_start;
SDL_stack_free(data);
}
} // namespace rex::audio::sdl
@@ -1,66 +0,0 @@
/**
******************************************************************************
* Xenia : Xbox 360 Emulator Research Project *
******************************************************************************
* Copyright 2020 Ben Vanik. All rights reserved. *
* Released under the BSD license - see LICENSE in the root for more details. *
******************************************************************************
*
* @modified Tom Clay, 2026 - Adapted for ReXGlue runtime
*/
#include <rex/audio/flags.h>
#include <rex/audio/sdl/sdl_audio_driver.h>
#include <rex/audio/sdl/sdl_audio_system.h>
#include <rex/cvar.h>
#include <rex/logging.h>
REXCVAR_DECLARE(bool, audio_trace_telemetry);
namespace rex::audio::sdl {
std::unique_ptr<AudioSystem> SDLAudioSystem::Create(
runtime::FunctionDispatcher* function_dispatcher) {
return std::make_unique<SDLAudioSystem>(function_dispatcher);
}
SDLAudioSystem::SDLAudioSystem(runtime::FunctionDispatcher* function_dispatcher)
: AudioSystem(function_dispatcher) {}
SDLAudioSystem::~SDLAudioSystem() {}
void SDLAudioSystem::Initialize() {
AudioSystem::Initialize();
}
X_STATUS SDLAudioSystem::CreateDriver([[maybe_unused]] size_t index,
rex::thread::Semaphore* semaphore, AudioDriver** out_driver) {
assert_not_null(out_driver);
auto driver = new SDLAudioDriver(memory_, semaphore);
if (!driver->Initialize()) {
driver->Shutdown();
delete driver;
return X_STATUS_UNSUCCESSFUL;
}
*out_driver = driver;
return X_STATUS_SUCCESS;
}
void SDLAudioSystem::DestroyDriver(AudioDriver* driver) {
assert_not_null(driver);
const auto telemetry = driver->GetTelemetry();
if (REXCVAR_GET(audio_trace_telemetry)) {
REXAPU_DEBUG(
"SDLAudioSystem::DestroyDriver telemetry: submitted={} consumed={} underruns={} "
"silence_injections={} queued_depth={} peak={}",
telemetry.submitted_frames, telemetry.consumed_frames, telemetry.underrun_count,
telemetry.silence_injections, telemetry.queued_depth, telemetry.peak_queued_depth);
}
auto sdldriver = dynamic_cast<SDLAudioDriver*>(driver);
assert_not_null(sdldriver);
sdldriver->Shutdown();
delete sdldriver;
}
} // namespace rex::audio::sdl
@@ -1,760 +0,0 @@
/**
******************************************************************************
* Xenia : Xbox 360 Emulator Research Project *
******************************************************************************
* Copyright 2021 Ben Vanik. All rights reserved. *
* Released under the BSD license - see LICENSE in the root for more details. *
******************************************************************************
*
* @modified Tom Clay, 2026 - Adapted for ReXGlue runtime
*/
#include <algorithm>
#include <cstring>
#include <rex/audio/xma/context.h>
#include <rex/audio/xma/decoder.h>
#include <rex/audio/xma/helpers.h>
#include <rex/dbg.h>
#include <rex/logging.h>
#include <rex/memory/ring_buffer.h>
#include <rex/platform.h>
#include <rex/stream.h>
extern "C" {
#if REX_COMPILER_MSVC
#pragma warning(push)
#pragma warning(disable : 4101 4244 5033)
#endif
#include "libavcodec/avcodec.h"
#if REX_COMPILER_MSVC
#pragma warning(pop)
#endif
} // extern "C"
// Credits for most of this code goes to:
// https://github.com/koolkdev/libertyv/blob/master/libav_wrapper/xma2dec.c
namespace rex::audio {
using stream::BitStream;
XmaContext::XmaContext()
: work_completion_event_(rex::thread::Event::CreateAutoResetEvent(false)) {}
XmaContext::~XmaContext() {
if (av_context_) {
if (avcodec_is_open(av_context_)) {
avcodec_close(av_context_);
}
av_free(av_context_);
}
if (av_frame_) {
av_frame_free(&av_frame_);
}
}
int XmaContext::Setup(uint32_t id, memory::Memory* memory, uint32_t guest_ptr) {
id_ = id;
memory_ = memory;
guest_ptr_ = guest_ptr;
// Allocate ffmpeg stuff:
av_packet_ = av_packet_alloc();
assert_not_null(av_packet_);
// find the XMA2 audio decoder
av_codec_ = avcodec_find_decoder(AV_CODEC_ID_XMAFRAMES);
if (!av_codec_) {
REXAPU_ERROR("XmaContext {}: Codec not found", id);
return 1;
}
av_context_ = avcodec_alloc_context3(av_codec_);
if (!av_context_) {
REXAPU_ERROR("XmaContext {}: Couldn't allocate context", id);
return 1;
}
// Initialize these to 0. They'll actually be set later.
av_context_->channels = 0;
av_context_->sample_rate = 0;
av_frame_ = av_frame_alloc();
if (!av_frame_) {
REXAPU_ERROR("XmaContext {}: Couldn't allocate frame", id);
return 1;
}
// FYI: We're purposely not opening the codec here. That is done later.
return 0;
}
bool XmaContext::Work() {
if (!is_allocated() || !is_enabled()) {
return false;
}
std::lock_guard<std::mutex> lock(lock_);
set_is_enabled(false);
auto context_ptr = memory()->TranslateVirtual(guest_ptr());
XMA_CONTEXT_DATA data(context_ptr);
const XMA_CONTEXT_DATA initial_data = data;
if (!data.output_buffer_valid) {
return true;
}
memory::RingBuffer output_rb = PrepareOutputRingBuffer(&data);
// Consume-only context: no input, just drain remaining subframes.
if (data.IsConsumeOnlyContext()) {
if (current_frame_remaining_subframes_ == 0) {
return true;
}
Consume(&output_rb, &data);
data.output_buffer_write_offset = output_rb.write_offset() / kOutputBytesPerBlock;
StoreContextMerged(data, initial_data, context_ptr);
return true;
}
// Minimum free blocks needed before attempting a decode.
// Use subframe_decode_count (clamped to 1) instead of full frame size.
const uint32_t effective_sdc = std::max(static_cast<uint32_t>(1), data.subframe_decode_count);
const int32_t minimum_subframe_decode_count =
static_cast<int32_t>(effective_sdc) + data.output_buffer_padding;
if (minimum_subframe_decode_count > remaining_subframe_blocks_in_output_buffer_) {
StoreContextMerged(data, initial_data, context_ptr);
return true;
}
while (remaining_subframe_blocks_in_output_buffer_ >= minimum_subframe_decode_count) {
Decode(&data);
Consume(&output_rb, &data);
if (!data.IsAnyInputBufferValid() || data.error_status == 4) {
break;
}
}
data.output_buffer_write_offset = output_rb.write_offset() / kOutputBytesPerBlock;
if (output_rb.empty()) {
data.output_buffer_valid = 0;
}
StoreContextMerged(data, initial_data, context_ptr);
return true;
}
void XmaContext::Enable() {
std::lock_guard<std::mutex> lock(lock_);
set_is_enabled(true);
}
bool XmaContext::Block(bool poll) {
if (!lock_.try_lock()) {
if (poll) {
return false;
}
lock_.lock();
}
lock_.unlock();
return true;
}
void XmaContext::Clear() {
std::lock_guard<std::mutex> lock(lock_);
REXAPU_DEBUG("XmaContext: reset context {}", id());
auto context_ptr = memory()->TranslateVirtual(guest_ptr());
XMA_CONTEXT_DATA data(context_ptr);
ClearLocked(&data);
data.Store(context_ptr);
}
void XmaContext::ClearLocked(XMA_CONTEXT_DATA* data) {
data->input_buffer_0_valid = 0;
data->input_buffer_1_valid = 0;
data->output_buffer_valid = 0;
data->input_buffer_read_offset = kBitsPerPacketHeader;
data->output_buffer_read_offset = 0;
data->output_buffer_write_offset = 0;
current_frame_remaining_subframes_ = 0;
loop_frame_output_limit_ = 0;
loop_start_skip_pending_ = false;
}
void XmaContext::Disable() {
std::lock_guard<std::mutex> lock(lock_);
set_is_enabled(false);
}
void XmaContext::Release() {
std::lock_guard<std::mutex> lock(lock_);
assert_true(is_allocated());
set_is_allocated(false);
auto context_ptr = memory()->TranslateVirtual(guest_ptr());
std::memset(context_ptr, 0, sizeof(XMA_CONTEXT_DATA));
}
void XmaContext::SwapInputBuffer(XMA_CONTEXT_DATA* data) {
if (data->current_buffer == 0) {
data->input_buffer_0_valid = 0;
} else {
data->input_buffer_1_valid = 0;
}
data->current_buffer ^= 1;
data->input_buffer_read_offset = kBitsPerPacketHeader;
}
void XmaContext::UpdateLoopStatus(XMA_CONTEXT_DATA* data) {
if (data->loop_count == 0) {
return;
}
const uint32_t loop_start = std::max(kBitsPerPacketHeader, data->loop_start);
const uint32_t loop_end = std::max(kBitsPerPacketHeader, data->loop_end);
if (data->input_buffer_read_offset != loop_end) {
return;
}
data->input_buffer_read_offset = loop_start;
loop_start_skip_pending_ = true;
if (data->loop_count < 255) {
data->loop_count--;
}
}
int XmaContext::GetSampleRate(int id) {
return kIdToSampleRate[std::min(id, 3)];
}
int16_t XmaContext::GetPacketNumber(size_t size, size_t bit_offset) {
if (bit_offset < kBitsPerPacketHeader) {
assert_always();
return -1;
}
if (bit_offset >= (size << 3)) {
assert_always();
return -1;
}
size_t byte_offset = bit_offset >> 3;
size_t packet_number = byte_offset / kBytesPerPacket;
return static_cast<int16_t>(packet_number);
}
uint32_t XmaContext::GetCurrentInputBufferSize(XMA_CONTEXT_DATA* data) {
return data->GetCurrentInputBufferPacketCount() * kBytesPerPacket;
}
uint8_t* XmaContext::GetCurrentInputBuffer(XMA_CONTEXT_DATA* data) {
return memory()->TranslatePhysical(data->GetCurrentInputBufferAddress());
}
uint32_t XmaContext::GetAmountOfBitsToRead(uint32_t remaining_stream_bits, uint32_t frame_size) {
return std::min(remaining_stream_bits, frame_size);
}
const uint8_t* XmaContext::GetNextPacket(XMA_CONTEXT_DATA* data, uint32_t next_packet_index,
uint32_t current_input_packet_count) {
if (next_packet_index < current_input_packet_count) {
return memory()->TranslatePhysical(data->GetCurrentInputBufferAddress()) +
next_packet_index * kBytesPerPacket;
}
const uint8_t next_buffer_index = data->current_buffer ^ 1;
if (!data->IsInputBufferValid(next_buffer_index)) {
return nullptr;
}
const uint32_t next_buffer_address = data->GetInputBufferAddress(next_buffer_index);
if (!next_buffer_address) {
REXAPU_ERROR("XmaContext {}: Buffer marked valid but has null pointer!", id());
return nullptr;
}
return memory()->TranslatePhysical(next_buffer_address);
}
uint32_t XmaContext::GetNextPacketReadOffset(uint8_t* buffer, uint32_t next_packet_index,
uint32_t current_input_packet_count) {
while (next_packet_index < current_input_packet_count) {
uint8_t* next_packet = buffer + (next_packet_index * kBytesPerPacket);
const uint32_t packet_frame_offset = xma::GetPacketFrameOffset(next_packet);
if (packet_frame_offset <= kMaxFrameSizeinBits) {
return (next_packet_index * kBitsPerPacket) + packet_frame_offset;
}
next_packet_index++;
}
return kBitsPerPacketHeader;
}
memory::RingBuffer XmaContext::PrepareOutputRingBuffer(XMA_CONTEXT_DATA* data) {
const uint32_t output_capacity = data->output_buffer_block_count * kOutputBytesPerBlock;
const uint32_t output_read_offset = data->output_buffer_read_offset * kOutputBytesPerBlock;
const uint32_t output_write_offset = data->output_buffer_write_offset * kOutputBytesPerBlock;
if (output_capacity > kOutputMaxSizeBytes) {
REXAPU_WARN(
"XmaContext {}: Output buffer exceeds expected size! "
"(Actual: {} Max: {})",
id(), output_capacity, kOutputMaxSizeBytes);
}
uint8_t* output_buffer = memory()->TranslatePhysical(data->output_buffer_ptr);
memory::RingBuffer output_rb(output_buffer, output_capacity);
output_rb.set_read_offset(output_read_offset);
output_rb.set_write_offset(output_write_offset);
remaining_subframe_blocks_in_output_buffer_ =
static_cast<int32_t>(output_rb.write_count()) / kOutputBytesPerBlock;
return output_rb;
}
kPacketInfo XmaContext::GetPacketInfo(uint8_t* packet, uint32_t frame_offset) {
kPacketInfo packet_info = {};
const uint32_t first_frame_offset = xma::GetPacketFrameOffset(packet);
BitStream stream(packet, kBitsPerPacket);
stream.SetOffset(first_frame_offset);
if (frame_offset < first_frame_offset) {
packet_info.current_frame_ = 0;
packet_info.current_frame_size_ = first_frame_offset - frame_offset;
}
while (true) {
if (stream.BitsRemaining() < kBitsPerFrameHeader) {
break;
}
const uint64_t frame_size = stream.Peek(kBitsPerFrameHeader);
if (frame_size == 0 || frame_size == xma::kMaxFrameLength) {
break;
}
if (stream.offset_bits() == frame_offset) {
packet_info.current_frame_ = packet_info.frame_count_;
packet_info.current_frame_size_ = static_cast<uint32_t>(frame_size);
}
packet_info.frame_count_++;
if (frame_size > stream.BitsRemaining()) {
break;
}
stream.Advance(frame_size - 1);
if (stream.Read(1) == 0) {
break;
}
}
if (xma::IsPacketXma2Type(packet)) {
const uint8_t xma2_frame_count = xma::GetPacketFrameCount(packet);
if (xma2_frame_count > packet_info.frame_count_) {
if (packet_info.current_frame_size_ == 0) {
packet_info.current_frame_ = packet_info.frame_count_;
}
packet_info.frame_count_ = xma2_frame_count;
}
}
return packet_info;
}
void XmaContext::StoreContextMerged(const XMA_CONTEXT_DATA& data,
const XMA_CONTEXT_DATA& initial_data, uint8_t* context_ptr) {
XMA_CONTEXT_DATA fresh(context_ptr);
fresh.loop_count = data.loop_count;
fresh.output_buffer_write_offset = data.output_buffer_write_offset;
if (initial_data.input_buffer_0_valid && !data.input_buffer_0_valid) {
fresh.input_buffer_0_valid = 0;
}
if (initial_data.input_buffer_1_valid && !data.input_buffer_1_valid) {
fresh.input_buffer_1_valid = 0;
}
if (initial_data.output_buffer_valid && !data.output_buffer_valid) {
fresh.output_buffer_valid = 0;
}
fresh.input_buffer_read_offset = data.input_buffer_read_offset;
fresh.error_status = data.error_status;
fresh.current_buffer = data.current_buffer;
fresh.output_buffer_read_offset = data.output_buffer_read_offset;
fresh.Store(context_ptr);
}
void XmaContext::Consume(memory::RingBuffer* output_rb, const XMA_CONTEXT_DATA* data) {
if (!current_frame_remaining_subframes_) {
return;
}
if (loop_frame_output_limit_ > 0) {
const uint8_t total_subframes = (kBytesPerFrameChannel / kOutputBytesPerBlock)
<< data->is_stereo;
const uint8_t consumed = total_subframes - current_frame_remaining_subframes_;
if (consumed >= loop_frame_output_limit_) {
remaining_subframe_blocks_in_output_buffer_ -= data->output_buffer_padding;
current_frame_remaining_subframes_ = 0;
loop_frame_output_limit_ = 0;
return;
}
}
const uint8_t effective_sdc = std::max(static_cast<uint32_t>(1), data->subframe_decode_count);
int8_t subframes_to_write = std::min(static_cast<int8_t>(current_frame_remaining_subframes_),
static_cast<int8_t>(effective_sdc));
if (loop_frame_output_limit_ > 0) {
const uint8_t total_subframes = (kBytesPerFrameChannel / kOutputBytesPerBlock)
<< data->is_stereo;
const uint8_t consumed = total_subframes - current_frame_remaining_subframes_;
const int8_t remaining_until_limit = static_cast<int8_t>(loop_frame_output_limit_ - consumed);
if (subframes_to_write > remaining_until_limit) {
subframes_to_write = remaining_until_limit;
}
}
const int8_t raw_frame_read_offset =
((kBytesPerFrameChannel / kOutputBytesPerBlock) << data->is_stereo) -
current_frame_remaining_subframes_;
output_rb->Write(raw_frame_.data() + (kOutputBytesPerBlock * raw_frame_read_offset),
subframes_to_write * kOutputBytesPerBlock);
const int8_t headroom = (current_frame_remaining_subframes_ - subframes_to_write == 0)
? data->output_buffer_padding
: 0;
remaining_subframe_blocks_in_output_buffer_ -= subframes_to_write + headroom;
current_frame_remaining_subframes_ -= subframes_to_write;
}
int XmaContext::PrepareDecoder(int sample_rate, bool is_two_channel) {
sample_rate = GetSampleRate(sample_rate);
uint32_t channels = is_two_channel ? 2 : 1;
if (av_context_->sample_rate != sample_rate ||
av_context_->channels != static_cast<int>(channels)) {
avcodec_close(av_context_);
av_free(av_context_);
av_context_ = avcodec_alloc_context3(av_codec_);
av_context_->sample_rate = sample_rate;
av_context_->channels = channels;
if (avcodec_open2(av_context_, av_codec_, NULL) < 0) {
REXAPU_ERROR("XmaContext: Failed to reopen FFmpeg context");
return -1;
}
return 1;
}
return 0;
}
void XmaContext::PreparePacket(uint32_t frame_size, uint32_t frame_padding) {
av_packet_->data = xma_frame_.data();
av_packet_->size = static_cast<int>(1 + ((frame_padding + frame_size) / 8) +
(((frame_padding + frame_size) % 8) ? 1 : 0));
auto padding_end = av_packet_->size * 8 - (8 + frame_padding + frame_size);
assert_true(padding_end < 8);
xma_frame_[0] = ((frame_padding & 7) << 5) | ((padding_end & 7) << 2);
}
bool XmaContext::DecodePacket(AVCodecContext* av_context, const AVPacket* av_packet,
AVFrame* av_frame) {
auto ret = avcodec_send_packet(av_context, av_packet);
if (ret < 0) {
REXAPU_ERROR("XmaContext {}: Error sending packet for decoding ({})", id(), ret);
return false;
}
ret = avcodec_receive_frame(av_context, av_frame);
if (ret == AVERROR(EAGAIN)) {
return false;
}
if (ret < 0) {
REXAPU_ERROR("XmaContext {}: Error during decoding ({})", id(), ret);
return false;
}
return true;
}
void XmaContext::Decode(XMA_CONTEXT_DATA* data) {
SCOPE_profile_cpu_f("apu");
if (!data->IsAnyInputBufferValid()) {
return;
}
if (current_frame_remaining_subframes_ > 0) {
return;
}
if (!data->IsCurrentInputBufferValid()) {
SwapInputBuffer(data);
if (!data->IsCurrentInputBufferValid()) {
return;
}
}
uint8_t* current_input_buffer = GetCurrentInputBuffer(data);
input_buffer_.fill(0);
// Detect loop end frame before UpdateLoopStatus resets the offset.
bool is_loop_end_frame = false;
if (data->loop_count > 0) {
const uint32_t loop_end = std::max(kBitsPerPacketHeader, data->loop_end);
is_loop_end_frame = (data->input_buffer_read_offset == loop_end);
}
UpdateLoopStatus(data);
if (!data->output_buffer_block_count) {
REXAPU_ERROR("XmaContext {}: Error - Received 0 for output_buffer_block_count!", id());
return;
}
if (data->input_buffer_read_offset < kBitsPerPacketHeader) {
data->input_buffer_read_offset = kBitsPerPacketHeader;
}
const uint32_t current_input_size = GetCurrentInputBufferSize(data);
const uint32_t current_input_packet_count = current_input_size / kBytesPerPacket;
const int16_t packet_index = GetPacketNumber(current_input_size, data->input_buffer_read_offset);
if (packet_index == -1) {
REXAPU_ERROR("XmaContext {}: Invalid packet index. Input read offset: {}", id(),
static_cast<uint32_t>(data->input_buffer_read_offset));
return;
}
uint8_t* packet = current_input_buffer + (packet_index * kBytesPerPacket);
const uint32_t packet_first_frame_offset = xma::GetPacketFrameOffset(packet);
uint32_t relative_offset = data->input_buffer_read_offset % kBitsPerPacket;
if (relative_offset < packet_first_frame_offset) {
data->input_buffer_read_offset = (packet_index * kBitsPerPacket) + packet_first_frame_offset;
relative_offset = packet_first_frame_offset;
}
const uint8_t skip_count = xma::GetPacketSkipCount(packet);
// Full packet skip (0xFF) -- no new frames begin in this packet.
if (skip_count == 0xFF) {
uint32_t next_input_offset =
GetNextPacketReadOffset(current_input_buffer, packet_index + 1, current_input_packet_count);
if (next_input_offset == kBitsPerPacketHeader) {
SwapInputBuffer(data);
}
data->input_buffer_read_offset = next_input_offset;
return;
}
kPacketInfo packet_info = GetPacketInfo(packet, relative_offset);
const uint32_t packet_to_skip = skip_count + 1;
const uint32_t next_packet_index = packet_index + packet_to_skip;
// Frame header split across packet boundary.
if (packet_info.current_frame_size_ == 0) {
const uint8_t* next_packet = GetNextPacket(data, next_packet_index, current_input_packet_count);
if (!next_packet) {
SwapInputBuffer(data);
return;
}
std::memcpy(input_buffer_.data(), packet + kBytesPerPacketHeader, kBytesPerPacketData);
std::memcpy(input_buffer_.data() + kBytesPerPacketData, next_packet + kBytesPerPacketHeader,
kBytesPerPacketData);
BitStream combined(input_buffer_.data(), (kBitsPerPacket - kBitsPerPacketHeader) * 2);
combined.SetOffset(relative_offset - kBitsPerPacketHeader);
uint64_t frame_size = combined.Peek(kBitsPerFrameHeader);
if (frame_size == xma::kMaxFrameLength) {
data->error_status = 4;
return;
}
packet_info.current_frame_size_ = static_cast<uint32_t>(frame_size);
}
BitStream stream(current_input_buffer, (packet_index + 1) * kBitsPerPacket);
stream.SetOffset(data->input_buffer_read_offset);
const uint64_t bits_to_copy = GetAmountOfBitsToRead(static_cast<uint32_t>(stream.BitsRemaining()),
packet_info.current_frame_size_);
if (bits_to_copy == 0) {
REXAPU_ERROR("XmaContext {}: There are no bits to copy!", id());
SwapInputBuffer(data);
return;
}
if (packet_info.isLastFrameInPacket()) {
if (stream.BitsRemaining() < packet_info.current_frame_size_) {
const uint8_t* next_packet =
GetNextPacket(data, next_packet_index, current_input_packet_count);
if (!next_packet) {
data->error_status = 4;
return;
}
std::memcpy(input_buffer_.data() + kBytesPerPacketData, next_packet + kBytesPerPacketHeader,
kBytesPerPacketData);
}
}
std::memcpy(input_buffer_.data(), packet + kBytesPerPacketHeader, kBytesPerPacketData);
stream = BitStream(input_buffer_.data(), (kBitsPerPacket - kBitsPerPacketHeader) * 2);
stream.SetOffset(relative_offset - kBitsPerPacketHeader);
xma_frame_.fill(0);
const uint32_t padding_start =
static_cast<uint8_t>(stream.Copy(xma_frame_.data() + 1, packet_info.current_frame_size_));
raw_frame_.fill(0);
PrepareDecoder(data->sample_rate, bool(data->is_stereo));
PreparePacket(packet_info.current_frame_size_, padding_start);
if (DecodePacket(av_context_, av_packet_, av_frame_)) {
ConvertFrame(reinterpret_cast<const uint8_t**>(&av_frame_->data), bool(data->is_stereo),
raw_frame_.data());
current_frame_remaining_subframes_ = 4 << data->is_stereo;
// Loop end: limit output to subframes 0..loop_subframe_end.
if (is_loop_end_frame) {
loop_frame_output_limit_ = (data->loop_subframe_end + 1) << data->is_stereo;
} else {
loop_frame_output_limit_ = 0;
}
// Loop start: skip leading subframes per loop_subframe_skip.
if (loop_start_skip_pending_) {
const uint8_t skip = data->loop_subframe_skip << data->is_stereo;
if (skip < current_frame_remaining_subframes_) {
current_frame_remaining_subframes_ -= skip;
}
loop_start_skip_pending_ = false;
}
}
// Compute where to go next.
if (!packet_info.isLastFrameInPacket()) {
const uint32_t next_frame_offset =
(data->input_buffer_read_offset + bits_to_copy) % kBitsPerPacket;
data->input_buffer_read_offset = (packet_index * kBitsPerPacket) + next_frame_offset;
return;
}
uint32_t next_input_offset =
GetNextPacketReadOffset(current_input_buffer, next_packet_index, current_input_packet_count);
if (next_input_offset == kBitsPerPacketHeader) {
SwapInputBuffer(data);
if (data->IsAnyInputBufferValid()) {
next_input_offset = xma::GetPacketFrameOffset(
memory()->TranslatePhysical(data->GetCurrentInputBufferAddress()));
if (next_input_offset > kMaxFrameSizeinBits) {
SwapInputBuffer(data);
return;
}
}
}
data->input_buffer_read_offset = next_input_offset;
}
void XmaContext::ConvertFrame(const uint8_t** samples, bool is_two_channel,
uint8_t* output_buffer) {
// Loop through every sample, convert and drop it into the output array.
// If more than one channel, we need to interleave the samples from each
// channel next to each other. Always saturate because FFmpeg output is
// not limited to [-1, 1] (for example 1.095 as seen in 5454082B).
constexpr float scale = (1 << 15) - 1;
auto out = reinterpret_cast<int16_t*>(output_buffer);
// For testing of vectorized versions, stereo audio is common in 4D5307E6,
// since the first menu frame; the intro cutscene also has more than 2
// channels.
#if REX_ARCH_AMD64
static_assert(kSamplesPerFrame % 8 == 0);
const auto in_channel_0 = reinterpret_cast<const float*>(samples[0]);
const __m128 scale_mm = _mm_set1_ps(scale);
if (is_two_channel) {
const auto in_channel_1 = reinterpret_cast<const float*>(samples[1]);
const __m128i shufmask = _mm_set_epi8(14, 15, 6, 7, 12, 13, 4, 5, 10, 11, 2, 3, 8, 9, 0, 1);
for (uint32_t i = 0; i < kSamplesPerFrame; i += 4) {
// Load 8 samples, 4 for each channel.
__m128 in_mm0 = _mm_loadu_ps(&in_channel_0[i]);
__m128 in_mm1 = _mm_loadu_ps(&in_channel_1[i]);
// Rescale.
in_mm0 = _mm_mul_ps(in_mm0, scale_mm);
in_mm1 = _mm_mul_ps(in_mm1, scale_mm);
// Cast to int32.
__m128i out_mm0 = _mm_cvtps_epi32(in_mm0);
__m128i out_mm1 = _mm_cvtps_epi32(in_mm1);
// Saturated cast and pack to int16.
__m128i out_mm = _mm_packs_epi32(out_mm0, out_mm1);
// Interleave channels and byte swap.
out_mm = _mm_shuffle_epi8(out_mm, shufmask);
// Store, as [out + i * 4] movdqu.
_mm_storeu_si128(reinterpret_cast<__m128i*>(&out[i * 2]), out_mm);
}
} else {
const __m128i shufmask = _mm_set_epi8(14, 15, 12, 13, 10, 11, 8, 9, 6, 7, 4, 5, 2, 3, 0, 1);
for (uint32_t i = 0; i < kSamplesPerFrame; i += 8) {
// Load 8 samples, as [in_channel_0 + i * 4] and
// [in_channel_0 + i * 4 + 16] movups.
__m128 in_mm0 = _mm_loadu_ps(&in_channel_0[i]);
__m128 in_mm1 = _mm_loadu_ps(&in_channel_0[i + 4]);
// Rescale.
in_mm0 = _mm_mul_ps(in_mm0, scale_mm);
in_mm1 = _mm_mul_ps(in_mm1, scale_mm);
// Cast to int32.
__m128i out_mm0 = _mm_cvtps_epi32(in_mm0);
__m128i out_mm1 = _mm_cvtps_epi32(in_mm1);
// Saturated cast and pack to int16.
__m128i out_mm = _mm_packs_epi32(out_mm0, out_mm1);
// Byte swap.
out_mm = _mm_shuffle_epi8(out_mm, shufmask);
// Store, as [out + i * 2] movdqu.
_mm_storeu_si128(reinterpret_cast<__m128i*>(&out[i]), out_mm);
}
}
#else
uint32_t o = 0;
for (uint32_t i = 0; i < kSamplesPerFrame; i++) {
for (uint32_t j = 0; j <= uint32_t(is_two_channel); j++) {
// Select the appropriate array based on the current channel.
auto in = reinterpret_cast<const float*>(samples[j]);
// Raw samples sometimes aren't within [-1, 1]
float scaled_sample = rex::clamp_float(in[i], -1.0f, 1.0f) * scale;
// Convert the sample and output it in big endian.
auto sample = static_cast<int16_t>(scaled_sample);
out[o++] = rex::byte_swap(sample);
}
}
#endif
}
} // namespace rex::audio
@@ -1,365 +0,0 @@
/**
******************************************************************************
* Xenia : Xbox 360 Emulator Research Project *
******************************************************************************
* Copyright 2022 Ben Vanik. All rights reserved. *
* Released under the BSD license - see LICENSE in the root for more details. *
******************************************************************************
*
* @modified Tom Clay, 2026 - Adapted for ReXGlue runtime
*/
#include <rex/audio/xma/context.h>
#include <rex/audio/xma/decoder.h>
#include <rex/cvar.h>
#include <rex/dbg.h>
#include <rex/logging.h>
#include <rex/math.h>
#include <rex/memory/ring_buffer.h>
#include <rex/string/buffer.h>
#include <rex/system/function_dispatcher.h>
#include <rex/system/thread_state.h>
#include <rex/system/xthread.h>
extern "C" {
#include "libavutil/log.h"
} // extern "C"
REXCVAR_DEFINE_BOOL(ffmpeg_verbose, false, "Audio", "Verbose FFmpeg output (debug and above)");
// As with normal Microsoft, there are like twelve different ways to access
// the audio APIs. Early games use XMA*() methods almost exclusively to touch
// decoders. Later games use XAudio*() and direct memory writes to the XMA
// structures (as opposed to the XMA* calls), meaning that we have to support
// both.
//
// The XMA*() functions just manipulate the audio system in the guest context
// and let the normal XmaDecoder handling take it, to prevent duplicate
// implementations. They can be found in xboxkrnl_audio_xma.cc
//
// XMA details:
// https://devel.nuclex.org/external/svn/directx/trunk/include/xma2defs.h
// https://github.com/gdawg/fsbext/blob/master/src/xma_header.h
//
// XAudio2 uses XMA under the covers, and seems to map with the same
// restrictions of frame/subframe/etc:
// https://msdn.microsoft.com/en-us/library/windows/desktop/microsoft.directx_sdk.xaudio2.xaudio2_buffer(v=vs.85).aspx
//
// XMA contexts are 64b in size and tight bitfields. They are in physical
// memory not usually available to games. Games will use MmMapIoSpace to get
// the 64b pointer in user memory so they can party on it. If the game doesn't
// do this, it's likely they are either passing the context to XAudio or
// using the XMA* functions.
namespace rex::audio {
XmaDecoder::XmaDecoder(runtime::FunctionDispatcher* function_dispatcher)
: memory_(function_dispatcher->memory()), function_dispatcher_(function_dispatcher) {}
XmaDecoder::~XmaDecoder() = default;
void av_log_callback(void* avcl, int level, const char* fmt, va_list va) {
if (!REXCVAR_GET(ffmpeg_verbose) && level > AV_LOG_WARNING) {
return;
}
string::StringBuffer buff;
buff.AppendVarargs(fmt, va);
auto msg = buff.to_string_view();
switch (level) {
case AV_LOG_ERROR:
REXAPU_ERROR("ffmpeg: {}", msg);
break;
case AV_LOG_WARNING:
REXAPU_WARN("ffmpeg: {}", msg);
break;
case AV_LOG_INFO:
REXAPU_INFO("ffmpeg: {}", msg);
break;
case AV_LOG_VERBOSE:
case AV_LOG_DEBUG:
default:
REXAPU_DEBUG("ffmpeg: {}", msg);
break;
}
}
X_STATUS XmaDecoder::Setup(system::KernelState* kernel_state) {
// Setup ffmpeg logging callback
av_log_set_callback(av_log_callback);
// Register APU/XMA MMIO handlers
// XMA registers are at 0x7FEA0000-0x7FEAFFFF
memory()->AddVirtualMappedRange(
0x7FEA0000, // base address
0xFFFF0000, // mask
0x0000FFFF, // size (64KB)
this, // context (XmaDecoder*)
reinterpret_cast<runtime::MMIOReadCallback>(MMIOReadRegisterThunk),
reinterpret_cast<runtime::MMIOWriteCallback>(MMIOWriteRegisterThunk));
REXAPU_DEBUG("XMA: Registered MMIO handlers at 0x7FEA0000-0x7FEAFFFF");
// Setup XMA context data.
// The Xbox 360 kernel allocates the contexts with X_PAGE_NOCACHE |
// X_PAGE_READWRITE and writes MmGetPhysicalAddress for the address to the
// register.
context_data_first_ptr_ = memory()->SystemHeapAlloc(sizeof(XMA_CONTEXT_DATA) * kContextCount, 256,
memory::kSystemHeapPhysical);
context_data_last_ptr_ = context_data_first_ptr_ + (sizeof(XMA_CONTEXT_DATA) * kContextCount - 1);
register_file_[XmaRegister::ContextArrayAddress] =
memory()->GetPhysicalAddress(context_data_first_ptr_);
// Setup XMA contexts.
for (size_t i = 0; i < kContextCount; ++i) {
uint32_t guest_ptr = context_data_first_ptr_ + i * sizeof(XMA_CONTEXT_DATA);
XmaContext& context = contexts_[i];
if (context.Setup(i, memory(), guest_ptr)) {
assert_always();
}
}
register_file_[XmaRegister::NextContextIndex] = 1;
context_bitmap_.Resize(kContextCount);
worker_running_ = true;
work_event_ = rex::thread::Event::CreateAutoResetEvent(false);
assert_not_null(work_event_);
worker_thread_ = system::object_ref<system::XHostThread>(
new system::XHostThread(kernel_state, 128 * 1024, 0, [this]() {
WorkerThreadMain();
return 0;
}));
worker_thread_->set_name("XMA Decoder");
worker_thread_->Create();
return X_STATUS_SUCCESS;
}
void XmaDecoder::WorkerThreadMain() {
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];
bool worked = context.Work();
if (worked) {
context.SignalWorkDone();
}
did_work = did_work || worked;
}
if (paused_) {
pause_fence_.Signal();
resume_fence_.Wait();
}
if (did_work) {
continue;
}
// No work done this iteration, block until signaled.
rex::thread::Wait(work_event_.get(), false);
}
}
void XmaDecoder::Shutdown() {
if (!worker_thread_) {
return;
}
worker_running_ = false;
if (work_event_) {
work_event_->Set();
}
if (paused_) {
Resume();
}
// Wait up to 2 seconds for worker thread to exit gracefully.
auto result = rex::thread::Wait(worker_thread_->thread(), false, std::chrono::milliseconds(2000));
if (result == rex::thread::WaitResult::kTimeout) {
REXAPU_WARN("XMA: Worker thread did not exit within 2s, abandoning");
}
worker_thread_.reset();
if (context_data_first_ptr_) {
memory()->SystemHeapFree(context_data_first_ptr_);
}
context_data_first_ptr_ = 0;
context_data_last_ptr_ = 0;
}
int XmaDecoder::GetContextId(uint32_t guest_ptr) {
static_assert_size(XMA_CONTEXT_DATA, 64);
if (guest_ptr < context_data_first_ptr_ || guest_ptr > context_data_last_ptr_) {
return -1;
}
assert_zero(guest_ptr & 0x3F);
return (guest_ptr - context_data_first_ptr_) >> 6;
}
uint32_t XmaDecoder::AllocateContext() {
size_t index = context_bitmap_.Acquire();
if (index == -1) {
// Out of contexts.
return 0;
}
XmaContext& context = contexts_[index];
assert_false(context.is_allocated());
context.set_is_allocated(true);
return context.guest_ptr();
}
void XmaDecoder::ReleaseContext(uint32_t guest_ptr) {
auto context_id = GetContextId(guest_ptr);
assert_true(context_id >= 0);
XmaContext& context = contexts_[context_id];
assert_true(context.is_allocated());
context.Release();
context_bitmap_.Release(context_id);
}
bool XmaDecoder::BlockOnContext(uint32_t guest_ptr, bool poll) {
auto context_id = GetContextId(guest_ptr);
assert_true(context_id >= 0);
XmaContext& context = contexts_[context_id];
return context.Block(poll);
}
uint32_t XmaDecoder::ReadRegister(uint32_t addr) {
auto r = (addr & 0xFFFF) / 4;
assert_true(r < XmaRegisterFile::kRegisterCount);
switch (r) {
case XmaRegister::ContextArrayAddress:
break;
case XmaRegister::CurrentContextIndex: {
// 0606h (1818h) is rotating context processing # set to hardware ID of
// context being processed.
// If bit 200h is set, the locking code will possibly collide on hardware
// IDs and error out, so we should never set it (I think?).
uint32_t& current_context_index = register_file_[XmaRegister::CurrentContextIndex];
uint32_t& next_context_index = register_file_[XmaRegister::NextContextIndex];
// To prevent games from seeing a stuck XMA context, return a rotating
// number.
current_context_index = next_context_index;
next_context_index = (next_context_index + 1) % kContextCount;
break;
}
default:
const auto register_info = register_file_.GetRegisterInfo(r);
if (register_info) {
REXAPU_DEBUG("XMA: Read from unhandled register ({:04X}, {})", r, register_info->name);
} else {
REXAPU_DEBUG("XMA: Read from unknown register ({:04X})", r);
}
break;
}
return rex::byte_swap(register_file_[r]);
}
void XmaDecoder::WriteRegister(uint32_t addr, uint32_t value) {
SCOPE_profile_cpu_f("apu");
uint32_t r = (addr & 0xFFFF) / 4;
value = rex::byte_swap(value);
assert_true(r < XmaRegisterFile::kRegisterCount);
register_file_[r] = value;
if (r >= XmaRegister::Context0Kick && r <= XmaRegister::Context9Kick) {
// Context kick command.
// This will kick off the given hardware contexts.
// Basically, this kicks the SPU and says "hey, decode that audio!"
// XMAEnableContext
// 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;
auto& context = contexts_[context_id];
context.Enable();
}
}
// 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.
// XMADisableContext
uint32_t base_context_id = (r - XmaRegister::Context0Lock) * 32;
for (int i = 0; value && i < 32; ++i, value >>= 1) {
if (value & 1) {
uint32_t context_id = base_context_id + i;
auto& context = contexts_[context_id];
context.Disable();
}
}
// Signal the decoder thread to start processing.
// work_event_->Set();
} else if (r >= XmaRegister::Context0Clear && r <= XmaRegister::Context9Clear) {
// Context clear command.
// This will reset the given hardware contexts.
uint32_t base_context_id = (r - XmaRegister::Context0Clear) * 32;
for (int i = 0; value && i < 32; ++i, value >>= 1) {
if (value & 1) {
uint32_t context_id = base_context_id + i;
XmaContext& context = contexts_[context_id];
context.Clear();
}
}
} else {
// 0601h (1804h) is written to with 0x02000000 and 0x03000000 around a lock
// operation
switch (r) {
default: {
const auto register_info = register_file_.GetRegisterInfo(r);
if (register_info) {
REXAPU_DEBUG("XMA: Write to unhandled register ({:04X}, {}): {:08X}", r,
register_info->name, value);
} else {
REXAPU_DEBUG("XMA: Write to unknown register ({:04X}): {:08X}", r, value);
}
break;
}
#pragma warning(suppress : 4065)
}
}
}
void XmaDecoder::Pause() {
if (paused_) {
return;
}
paused_ = true;
pause_fence_.Wait();
}
void XmaDecoder::Resume() {
if (!paused_) {
return;
}
paused_ = false;
resume_fence_.Signal();
}
} // namespace rex::audio
@@ -1,39 +0,0 @@
/**
******************************************************************************
* Xenia : Xbox 360 Emulator Research Project *
******************************************************************************
* Copyright 2021 Ben Vanik. All rights reserved. *
* Released under the BSD license - see LICENSE in the root for more details. *
******************************************************************************
*
* @modified Tom Clay, 2026 - Adapted for ReXGlue runtime
*/
#include <cstring>
#include <rex/audio/xma/register_file.h>
#include <rex/math.h>
namespace rex::audio {
XmaRegisterFile::XmaRegisterFile() {
std::memset(values, 0, sizeof(values));
}
const XmaRegisterInfo* XmaRegisterFile::GetRegisterInfo(uint32_t index) {
switch (index) {
#define XE_XMA_REGISTER(index, name) \
case index: { \
static const XmaRegisterInfo reg_info = { \
#name, \
}; \
return &reg_info; \
}
#include <rex/audio/xma/register_table.inc>
#undef XE_XMA_REGISTER
default:
return nullptr;
}
}
} // namespace rex::audio
@@ -1,85 +0,0 @@
# rexkernel: Xbox 360 kernel/XAM export implementations (ported from Xenia)
# Namespace: rex::kernel
# System emulation layer has moved to rexsystem (rex::system)
set(REXKERNEL_SOURCES
kernel_init.cpp
# XAM subsystem - provides __imp__Xam* symbols
xam/xam_module.cpp
xam/xam_avatar.cpp
xam/xam_content.cpp
xam/xam_content_aggregate.cpp
xam/xam_content_device.cpp
xam/xam_debug.cpp
xam/xam_enum.cpp
xam/xam_info.cpp
xam/xam_input.cpp
xam/xam_locale.cpp
xam/xam_msg.cpp
xam/xam_net.cpp
xam/xam_notify.cpp
xam/xam_nui.cpp
xam/xam_party.cpp
xam/xam_task.cpp
xam/xam_ui.cpp
xam/xam_user.cpp
xam/xam_video.cpp
xam/xam_voice.cpp
xam/xam_misc.cpp
xam/apps/xam_app.cpp
xam/apps/xgi_app.cpp
xam/apps/xlivebase_app.cpp
xam/apps/xmp_app.cpp
# xboxkrnl exports - provides __imp__ symbols for generated code
# xboxkrnl/cert_monitor.cpp # TODO: Translate JIT methods for AOT
# xboxkrnl/debug_monitor.cpp # TODO: Translate JIT methods for AOT
xboxkrnl/xboxkrnl_crypt.cpp
xboxkrnl/xboxkrnl_debug.cpp
xboxkrnl/xboxkrnl_error.cpp
xboxkrnl/xboxkrnl_hal.cpp
xboxkrnl/xboxkrnl_hid.cpp
xboxkrnl/xboxkrnl_io.cpp
xboxkrnl/xboxkrnl_io_info.cpp
xboxkrnl/xboxkrnl_memory.cpp
xboxkrnl/xboxkrnl_misc.cpp
xboxkrnl/xboxkrnl_module.cpp
xboxkrnl/xboxkrnl_modules.cpp
xboxkrnl/xboxkrnl_ob.cpp
xboxkrnl/xboxkrnl_rtl.cpp
xboxkrnl/xboxkrnl_strings.cpp
xboxkrnl/xboxkrnl_threading.cpp
# xboxkrnl/xboxkrnl_usbcam.cpp # TODO: lol eventually.
xboxkrnl/xboxkrnl_video.cpp
xboxkrnl/xboxkrnl_xconfig.cpp
xboxkrnl/xboxkrnl_audio.cpp
xboxkrnl/xboxkrnl_audio_xma.cpp
crt/heap.cpp
crt/file.cpp
crt/memory.cpp
crt/string.cpp
)
add_library(rexkernel STATIC ${REXKERNEL_SOURCES})
add_library(rex::kernel ALIAS rexkernel)
target_include_directories(rexkernel PUBLIC
$<BUILD_INTERFACE:${REXGLUE_ROOT}/include>
$<INSTALL_INTERFACE:${CMAKE_INSTALL_INCLUDEDIR}>
)
target_include_directories(rexkernel PRIVATE
${REXGLUE_ROOT}/thirdparty
)
target_link_libraries(rexkernel
PUBLIC
rexsystem
PRIVATE
rexgraphics
rexinput
rexaudio
o1heap
)
@@ -1,263 +0,0 @@
/**
******************************************************************************
* Xenia : Xbox 360 Emulator Research Project *
******************************************************************************
* Copyright 2022 Ben Vanik. All rights reserved. *
* Released under the BSD license - see LICENSE in the root for more details. *
******************************************************************************
*
* @modified Tom Clay, 2026 - Adapted for ReXGlue runtime
*/
// Disable warnings about unused parameters for kernel functions
#pragma GCC diagnostic ignored "-Wunused-parameter"
#include <rex/audio/audio_system.h>
#include <rex/cvar.h>
#include <rex/kernel/xboxkrnl/private.h>
#include <rex/logging.h>
#include <rex/ppc/function.h>
#include <rex/ppc/types.h>
#include <rex/runtime.h>
#include <rex/system/kernel_state.h>
#include <rex/system/xtypes.h>
REXCVAR_DECLARE(bool, audio_trace_telemetry);
REXCVAR_DECLARE(bool, audio_trace_render_driver_verbose);
REXCVAR_DECLARE(bool, ac6_audio_deep_trace);
namespace rex::kernel::xboxkrnl {
using namespace rex::system;
namespace {
constexpr uint32_t kRenderDriverTagMask = 0xFFFF0000;
constexpr uint32_t kRenderDriverTagValue = 0x41550000;
size_t ResolveRenderDriverIndex(ppc_pvoid_t driver_ptr) {
assert_true((driver_ptr.guest_address() & kRenderDriverTagMask) == kRenderDriverTagValue);
return driver_ptr.guest_address() & 0x0000FFFF;
}
audio::AudioSystem* GetAudioSystem() {
return static_cast<audio::AudioSystem*>(REX_KERNEL_STATE()->emulator()->audio_system());
}
bool IsDeepTraceEnabled() {
return REXCVAR_GET(ac6_audio_deep_trace);
}
} // namespace
ppc_u32_result_t XAudioGetSpeakerConfig_entry(ppc_pu32_t config_ptr) {
*config_ptr = 0x00010001;
return X_ERROR_SUCCESS;
}
ppc_u32_result_t XAudioGetVoiceCategoryVolumeChangeMask_entry(ppc_pvoid_t driver_ptr,
ppc_pu32_t out_ptr) {
assert_true((driver_ptr.guest_address() & 0xFFFF0000) == 0x41550000);
rex::thread::Sleep(std::chrono::microseconds(1));
// Checking these bits to see if any voice volume changed.
// I think.
*out_ptr = 0;
return X_ERROR_SUCCESS;
}
ppc_u32_result_t XAudioGetVoiceCategoryVolume_entry(ppc_u32_t unk, ppc_pf32_t out_ptr) {
// Expects a floating point single. Volume %?
*out_ptr = 1.0f;
return X_ERROR_SUCCESS;
}
ppc_u32_result_t XAudioEnableDucker_entry(ppc_u32_t unk) {
return X_ERROR_SUCCESS;
}
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];
size_t index;
auto result = GetAudioSystem()->RegisterClient(callback, callback_arg, &index);
if (XFAILED(result)) {
REXKRNL_WARN(
"XAudioRegisterRenderDriverClient failed callback={:08X} callback_arg={:08X} "
"status={:08X}",
callback, callback_arg, static_cast<uint32_t>(result));
return result;
}
assert_true(!(index & ~0x0000FFFF));
const uint32_t assigned_driver =
0x41550000 | (static_cast<uint32_t>(index) & 0x0000FFFF);
*driver_ptr = assigned_driver;
if (REXCVAR_GET(audio_trace_render_driver_verbose) || IsDeepTraceEnabled()) {
REXKRNL_DEBUG(
"XAudioRegisterRenderDriverClient result callback={:08X} callback_arg={:08X} "
"index={} driver={:08X}",
callback, callback_arg, index, assigned_driver);
}
return X_ERROR_SUCCESS;
}
ppc_u32_result_t XAudioUnregisterRenderDriverClient_entry(ppc_pvoid_t driver_ptr) {
const size_t index = ResolveRenderDriverIndex(driver_ptr);
auto* audio_system = GetAudioSystem();
if (REXCVAR_GET(audio_trace_render_driver_verbose) || IsDeepTraceEnabled()) {
REXKRNL_DEBUG("XAudioUnregisterRenderDriverClient driver={:08X} index={}",
driver_ptr.guest_address(), index);
}
if (REXCVAR_GET(audio_trace_telemetry) || IsDeepTraceEnabled()) {
const auto telemetry = audio_system->GetClientTelemetry(index);
REXKRNL_DEBUG(
"XAudioUnregisterRenderDriverClient telemetry: driver={:08X} submitted={} consumed={} "
"underruns={} silence_injections={} queued_depth={} peak={}",
driver_ptr.guest_address(), telemetry.submitted_frames, telemetry.consumed_frames,
telemetry.underrun_count, telemetry.silence_injections, telemetry.queued_depth,
telemetry.peak_queued_depth);
}
audio_system->UnregisterClient(index);
return X_ERROR_SUCCESS;
}
ppc_u32_result_t XAudioSubmitRenderDriverFrame_entry(ppc_pvoid_t driver_ptr,
ppc_pvoid_t samples_ptr) {
const size_t index = ResolveRenderDriverIndex(driver_ptr);
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 = GetAudioSystem();
audio_system->SubmitFrame(index, samples_ptr.guest_address());
if (REXCVAR_GET(audio_trace_render_driver_verbose) || IsDeepTraceEnabled()) {
const auto telemetry = audio_system->GetClientTelemetry(index);
if (telemetry.submitted_frames <= 24 || (telemetry.submitted_frames % 60) == 0 ||
telemetry.queued_depth <= 1 || telemetry.underrun_count != 0) {
REXKRNL_DEBUG(
"XAudioSubmitRenderDriverFrame verbose driver={:08X} index={} samples={:08X} "
"submitted={} consumed={} queued_depth={} peak={} underruns={} silence_injections={}",
driver_ptr.guest_address(), index, samples_ptr.guest_address(),
telemetry.submitted_frames, telemetry.consumed_frames, telemetry.queued_depth,
telemetry.peak_queued_depth, telemetry.underrun_count,
telemetry.silence_injections);
}
}
if (REXCVAR_GET(audio_trace_telemetry) || IsDeepTraceEnabled()) {
const auto telemetry = audio_system->GetClientTelemetry(index);
if (telemetry.submitted_frames <= 12 || (telemetry.submitted_frames % 120) == 0) {
REXKRNL_DEBUG(
"XAudioSubmitRenderDriverFrame telemetry: driver={:08X} submitted={} consumed={} "
"underruns={} queued_depth={} peak={}",
driver_ptr.guest_address(), telemetry.submitted_frames, telemetry.consumed_frames,
telemetry.underrun_count, telemetry.queued_depth, telemetry.peak_queued_depth);
}
}
return X_ERROR_SUCCESS;
}
ppc_u32_result_t XAudioGetRenderDriverTic_entry(ppc_pvoid_t driver_ptr) {
if (!driver_ptr) {
return 0;
}
const size_t index = ResolveRenderDriverIndex(driver_ptr);
auto* audio_system = GetAudioSystem();
const uint32_t tic = audio_system->GetClientRenderDriverTic(index);
if (REXCVAR_GET(audio_trace_render_driver_verbose) || IsDeepTraceEnabled()) {
const auto telemetry = audio_system->GetClientTelemetry(index);
if (telemetry.consumed_frames <= 24 || (telemetry.consumed_frames % 120) == 0) {
REXKRNL_DEBUG(
"XAudioGetRenderDriverTic driver={:08X} index={} tic={} consumed={} queued_depth={} "
"underruns={}",
driver_ptr.guest_address(), index, tic, telemetry.consumed_frames, telemetry.queued_depth,
telemetry.underrun_count);
}
}
return tic;
}
ppc_u32_result_t XAudioGetUnderrunCount_entry(ppc_pvoid_t driver_ptr) {
if (!driver_ptr) {
return 0;
}
const size_t index = ResolveRenderDriverIndex(driver_ptr);
auto* audio_system = GetAudioSystem();
const auto telemetry = audio_system->GetClientTelemetry(index);
if (REXCVAR_GET(audio_trace_render_driver_verbose) || IsDeepTraceEnabled()) {
if (telemetry.underrun_count != 0 || telemetry.consumed_frames <= 24 ||
(telemetry.consumed_frames % 120) == 0) {
REXKRNL_DEBUG(
"XAudioGetUnderrunCount driver={:08X} index={} underruns={} consumed={} queued_depth={}",
driver_ptr.guest_address(), index, telemetry.underrun_count, telemetry.consumed_frames,
telemetry.queued_depth);
}
}
return telemetry.underrun_count;
}
} // namespace rex::kernel::xboxkrnl
XBOXKRNL_EXPORT(__imp__XAudioGetSpeakerConfig, rex::kernel::xboxkrnl::XAudioGetSpeakerConfig_entry)
XBOXKRNL_EXPORT(__imp__XAudioGetVoiceCategoryVolumeChangeMask,
rex::kernel::xboxkrnl::XAudioGetVoiceCategoryVolumeChangeMask_entry)
XBOXKRNL_EXPORT(__imp__XAudioGetVoiceCategoryVolume,
rex::kernel::xboxkrnl::XAudioGetVoiceCategoryVolume_entry)
XBOXKRNL_EXPORT(__imp__XAudioEnableDucker, rex::kernel::xboxkrnl::XAudioEnableDucker_entry)
XBOXKRNL_EXPORT(__imp__XAudioRegisterRenderDriverClient,
rex::kernel::xboxkrnl::XAudioRegisterRenderDriverClient_entry)
XBOXKRNL_EXPORT(__imp__XAudioUnregisterRenderDriverClient,
rex::kernel::xboxkrnl::XAudioUnregisterRenderDriverClient_entry)
XBOXKRNL_EXPORT(__imp__XAudioSubmitRenderDriverFrame,
rex::kernel::xboxkrnl::XAudioSubmitRenderDriverFrame_entry)
XBOXKRNL_EXPORT_STUB(__imp__XAudioRenderDriverInitialize);
XBOXKRNL_EXPORT_STUB(__imp__XAudioRenderDriverLock);
XBOXKRNL_EXPORT_STUB(__imp__XAudioSetVoiceCategoryVolume);
XBOXKRNL_EXPORT_STUB(__imp__XAudioBeginDigitalBypassMode);
XBOXKRNL_EXPORT_STUB(__imp__XAudioEndDigitalBypassMode);
XBOXKRNL_EXPORT_STUB(__imp__XAudioSubmitDigitalPacket);
XBOXKRNL_EXPORT_STUB(__imp__XAudioQueryDriverPerformance);
XBOXKRNL_EXPORT_STUB(__imp__XAudioGetRenderDriverThread);
XBOXKRNL_EXPORT_STUB(__imp__XAudioSetSpeakerConfig);
XBOXKRNL_EXPORT_STUB(__imp__XAudioOverrideSpeakerConfig);
XBOXKRNL_EXPORT_STUB(__imp__XAudioSuspendRenderDriverClients);
XBOXKRNL_EXPORT_STUB(__imp__XAudioRegisterRenderDriverMECClient);
XBOXKRNL_EXPORT_STUB(__imp__XAudioUnregisterRenderDriverMECClient);
XBOXKRNL_EXPORT_STUB(__imp__XAudioCaptureRenderDriverFrame);
XBOXKRNL_EXPORT(__imp__XAudioGetRenderDriverTic,
rex::kernel::xboxkrnl::XAudioGetRenderDriverTic_entry)
XBOXKRNL_EXPORT_STUB(__imp__XAudioSetDuckerLevel);
XBOXKRNL_EXPORT_STUB(__imp__XAudioIsDuckerEnabled);
XBOXKRNL_EXPORT_STUB(__imp__XAudioGetDuckerLevel);
XBOXKRNL_EXPORT_STUB(__imp__XAudioGetDuckerThreshold);
XBOXKRNL_EXPORT_STUB(__imp__XAudioSetDuckerThreshold);
XBOXKRNL_EXPORT_STUB(__imp__XAudioGetDuckerAttackTime);
XBOXKRNL_EXPORT_STUB(__imp__XAudioSetDuckerAttackTime);
XBOXKRNL_EXPORT_STUB(__imp__XAudioGetDuckerReleaseTime);
XBOXKRNL_EXPORT_STUB(__imp__XAudioSetDuckerReleaseTime);
XBOXKRNL_EXPORT_STUB(__imp__XAudioGetDuckerHoldTime);
XBOXKRNL_EXPORT_STUB(__imp__XAudioSetDuckerHoldTime);
XBOXKRNL_EXPORT(__imp__XAudioGetUnderrunCount,
rex::kernel::xboxkrnl::XAudioGetUnderrunCount_entry)
XBOXKRNL_EXPORT_STUB(__imp__XAudioSetProcessFrameCallback);
@@ -1,389 +0,0 @@
/**
******************************************************************************
* Xenia : Xbox 360 Emulator Research Project *
******************************************************************************
* Copyright 2022 Ben Vanik. All rights reserved. *
* Released under the BSD license - see LICENSE in the root for more details. *
******************************************************************************
*
* @modified Tom Clay, 2026 - Adapted for ReXGlue runtime
*/
// Disable warnings about unused parameters for kernel functions
#pragma GCC diagnostic ignored "-Wunused-parameter"
#include <cstring>
#include <rex/assert.h>
#include <rex/audio/audio_system.h>
#include <rex/audio/xma/decoder.h>
#include <rex/kernel/xboxkrnl/private.h>
#include <rex/logging.h>
#include <rex/ppc/function.h>
#include <rex/ppc/types.h>
#include <rex/runtime.h>
#include <rex/system/kernel_state.h>
#include <rex/system/xtypes.h>
namespace rex::kernel::xboxkrnl {
using namespace rex::system;
using rex::audio::XMA_CONTEXT_DATA;
// See audio_system.cc for implementation details.
//
// XMA details:
// https://devel.nuclex.org/external/svn/directx/trunk/include/xma2defs.h
// https://github.com/gdawg/fsbext/blob/master/src/xma_header.h
//
// XMA is undocumented, but the methods are pretty simple.
// Games do this sequence to decode (now):
// (not sure we are setting buffer validity/offsets right)
// d> XMACreateContext(20656800)
// d> XMAIsInputBuffer0Valid(000103E0)
// d> XMAIsInputBuffer1Valid(000103E0)
// d> XMADisableContext(000103E0, 0)
// d> XMABlockWhileInUse(000103E0)
// d> XMAInitializeContext(000103E0, 20008810)
// d> XMASetOutputBufferValid(000103E0)
// d> XMASetInputBuffer0Valid(000103E0)
// d> XMAEnableContext(000103E0)
// d> XMAGetOutputBufferWriteOffset(000103E0)
// d> XMAGetOutputBufferReadOffset(000103E0)
// d> XMAIsOutputBufferValid(000103E0)
// d> XMAGetOutputBufferReadOffset(000103E0)
// d> XMAGetOutputBufferWriteOffset(000103E0)
// d> XMAIsInputBuffer0Valid(000103E0)
// d> XMAIsInputBuffer1Valid(000103E0)
// d> XMAIsInputBuffer0Valid(000103E0)
// d> XMAIsInputBuffer1Valid(000103E0)
// d> XMAReleaseContext(000103E0)
//
// XAudio2 uses XMA under the covers, and seems to map with the same
// restrictions of frame/subframe/etc:
// 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*>(REX_KERNEL_STATE()->emulator()->audio_system())
->xma_decoder();
uint32_t context_ptr = xma_decoder->AllocateContext();
*context_out_ptr = context_ptr;
if (!context_ptr) {
return X_STATUS_NO_MEMORY;
}
return X_STATUS_SUCCESS;
}
ppc_u32_result_t XMAReleaseContext_entry(ppc_pvoid_t context_ptr) {
auto xma_decoder =
static_cast<audio::AudioSystem*>(REX_KERNEL_STATE()->emulator()->audio_system())
->xma_decoder();
xma_decoder->ReleaseContext(context_ptr.guest_address());
return 0;
}
void StoreXmaContextIndexedRegister(system::KernelState* kernel_state, uint32_t base_reg,
uint32_t context_ptr) {
uint32_t context_physical_address = REX_KERNEL_MEMORY()->GetPhysicalAddress(context_ptr);
assert_true(context_physical_address != UINT32_MAX);
auto xma_decoder =
static_cast<audio::AudioSystem*>(kernel_state->emulator()->audio_system())->xma_decoder();
uint32_t hw_index =
(context_physical_address - xma_decoder->context_array_ptr()) / sizeof(XMA_CONTEXT_DATA);
uint32_t reg_num = base_reg + (hw_index >> 5) * 4;
uint32_t reg_value = 1 << (hw_index & 0x1F);
xma_decoder->WriteRegister(reg_num, rex::byte_swap(reg_value));
}
struct XMA_LOOP_DATA {
rex::be<uint32_t> loop_start;
rex::be<uint32_t> loop_end;
uint8_t loop_count;
uint8_t loop_subframe_end;
uint8_t loop_subframe_skip;
};
static_assert_size(XMA_LOOP_DATA, 12);
struct XMA_CONTEXT_INIT {
rex::be<uint32_t> input_buffer_0_ptr;
rex::be<uint32_t> input_buffer_0_packet_count;
rex::be<uint32_t> input_buffer_1_ptr;
rex::be<uint32_t> input_buffer_1_packet_count;
rex::be<uint32_t> input_buffer_read_offset;
rex::be<uint32_t> output_buffer_ptr;
rex::be<uint32_t> output_buffer_block_count;
rex::be<uint32_t> work_buffer;
rex::be<uint32_t> subframe_decode_count;
rex::be<uint32_t> channel_count;
rex::be<uint32_t> sample_rate;
XMA_LOOP_DATA loop_data;
};
static_assert_size(XMA_CONTEXT_INIT, 56);
ppc_u32_result_t XMAInitializeContext_entry(ppc_pvoid_t context_ptr,
ppc_ptr_t<XMA_CONTEXT_INIT> context_init) {
// Input buffers may be null (buffer 1 in 415607D4).
// Convert to host endianness.
uint32_t input_buffer_0_guest_ptr = context_init->input_buffer_0_ptr;
uint32_t input_buffer_0_physical_address = 0;
if (input_buffer_0_guest_ptr) {
input_buffer_0_physical_address =
REX_KERNEL_MEMORY()->GetPhysicalAddress(input_buffer_0_guest_ptr);
// Xenia-specific safety check.
assert_true(input_buffer_0_physical_address != UINT32_MAX);
if (input_buffer_0_physical_address == UINT32_MAX) {
REXKRNL_ERROR("XMAInitializeContext: Invalid input buffer 0 virtual address {:08X}",
input_buffer_0_guest_ptr);
return X_E_FALSE;
}
}
uint32_t input_buffer_1_guest_ptr = context_init->input_buffer_1_ptr;
uint32_t input_buffer_1_physical_address = 0;
if (input_buffer_1_guest_ptr) {
input_buffer_1_physical_address =
REX_KERNEL_MEMORY()->GetPhysicalAddress(input_buffer_1_guest_ptr);
assert_true(input_buffer_1_physical_address != UINT32_MAX);
if (input_buffer_1_physical_address == UINT32_MAX) {
REXKRNL_ERROR("XMAInitializeContext: Invalid input buffer 1 virtual address {:08X}",
input_buffer_1_guest_ptr);
return X_E_FALSE;
}
}
uint32_t output_buffer_guest_ptr = context_init->output_buffer_ptr;
assert_not_zero(output_buffer_guest_ptr);
uint32_t output_buffer_physical_address =
REX_KERNEL_MEMORY()->GetPhysicalAddress(output_buffer_guest_ptr);
assert_true(output_buffer_physical_address != UINT32_MAX);
if (output_buffer_physical_address == UINT32_MAX) {
REXKRNL_ERROR("XMAInitializeContext: Invalid output buffer virtual address {:08X}",
output_buffer_guest_ptr);
return X_E_FALSE;
}
std::memset(context_ptr, 0, sizeof(XMA_CONTEXT_DATA));
XMA_CONTEXT_DATA context(context_ptr);
context.input_buffer_0_ptr = input_buffer_0_physical_address;
context.input_buffer_0_packet_count = context_init->input_buffer_0_packet_count;
context.input_buffer_1_ptr = input_buffer_1_physical_address;
context.input_buffer_1_packet_count = context_init->input_buffer_1_packet_count;
context.input_buffer_read_offset = context_init->input_buffer_read_offset;
context.output_buffer_ptr = output_buffer_physical_address;
context.output_buffer_block_count = context_init->output_buffer_block_count;
// context.work_buffer = context_init->work_buffer; // ?
context.subframe_decode_count = context_init->subframe_decode_count;
context.is_stereo = context_init->channel_count >= 1;
context.sample_rate = context_init->sample_rate;
context.loop_start = context_init->loop_data.loop_start;
context.loop_end = context_init->loop_data.loop_end;
context.loop_count = context_init->loop_data.loop_count;
context.loop_subframe_end = context_init->loop_data.loop_subframe_end;
context.loop_subframe_skip = context_init->loop_data.loop_subframe_skip;
context.Store(context_ptr);
StoreXmaContextIndexedRegister(REX_KERNEL_STATE(), 0x1A80, context_ptr.guest_address());
return 0;
}
ppc_u32_result_t XMASetLoopData_entry(ppc_pvoid_t context_ptr,
ppc_ptr_t<XMA_CONTEXT_DATA> loop_data) {
XMA_CONTEXT_DATA context(context_ptr);
context.loop_start = loop_data->loop_start;
context.loop_end = loop_data->loop_end;
context.loop_count = loop_data->loop_count;
context.loop_subframe_end = loop_data->loop_subframe_end;
context.loop_subframe_skip = loop_data->loop_subframe_skip;
context.Store(context_ptr);
return 0;
}
ppc_u32_result_t XMAGetInputBufferReadOffset_entry(ppc_pvoid_t context_ptr) {
XMA_CONTEXT_DATA context(context_ptr);
return context.input_buffer_read_offset;
}
ppc_u32_result_t XMASetInputBufferReadOffset_entry(ppc_pvoid_t context_ptr, ppc_u32_t value) {
XMA_CONTEXT_DATA context(context_ptr);
context.input_buffer_read_offset = value;
context.Store(context_ptr);
return 0;
}
ppc_u32_result_t XMASetInputBuffer0_entry(ppc_pvoid_t context_ptr, ppc_pvoid_t buffer,
ppc_u32_t packet_count) {
uint32_t buffer_physical_address =
REX_KERNEL_MEMORY()->GetPhysicalAddress(buffer.guest_address());
assert_true(buffer_physical_address != UINT32_MAX);
if (buffer_physical_address == UINT32_MAX) {
// Xenia-specific safety check.
REXKRNL_ERROR("XMASetInputBuffer0: Invalid buffer virtual address {:08X}",
buffer.guest_address());
return X_E_FALSE;
}
XMA_CONTEXT_DATA context(context_ptr);
context.input_buffer_0_ptr = buffer_physical_address;
context.input_buffer_0_packet_count = packet_count;
context.Store(context_ptr);
return 0;
}
ppc_u32_result_t XMAIsInputBuffer0Valid_entry(ppc_pvoid_t context_ptr) {
XMA_CONTEXT_DATA context(context_ptr);
return context.input_buffer_0_valid;
}
ppc_u32_result_t XMASetInputBuffer0Valid_entry(ppc_pvoid_t context_ptr) {
XMA_CONTEXT_DATA context(context_ptr);
context.input_buffer_0_valid = 1;
context.Store(context_ptr);
return 0;
}
ppc_u32_result_t XMASetInputBuffer1_entry(ppc_pvoid_t context_ptr, ppc_pvoid_t buffer,
ppc_u32_t packet_count) {
uint32_t buffer_physical_address =
REX_KERNEL_MEMORY()->GetPhysicalAddress(buffer.guest_address());
assert_true(buffer_physical_address != UINT32_MAX);
if (buffer_physical_address == UINT32_MAX) {
// Xenia-specific safety check.
REXKRNL_ERROR("XMASetInputBuffer1: Invalid buffer virtual address {:08X}",
buffer.guest_address());
return X_E_FALSE;
}
XMA_CONTEXT_DATA context(context_ptr);
context.input_buffer_1_ptr = buffer_physical_address;
context.input_buffer_1_packet_count = packet_count;
context.Store(context_ptr);
return 0;
}
ppc_u32_result_t XMAIsInputBuffer1Valid_entry(ppc_pvoid_t context_ptr) {
XMA_CONTEXT_DATA context(context_ptr);
return context.input_buffer_1_valid;
}
ppc_u32_result_t XMASetInputBuffer1Valid_entry(ppc_pvoid_t context_ptr) {
XMA_CONTEXT_DATA context(context_ptr);
context.input_buffer_1_valid = 1;
context.Store(context_ptr);
return 0;
}
ppc_u32_result_t XMAIsOutputBufferValid_entry(ppc_pvoid_t context_ptr) {
XMA_CONTEXT_DATA context(context_ptr);
return context.output_buffer_valid;
}
ppc_u32_result_t XMASetOutputBufferValid_entry(ppc_pvoid_t context_ptr) {
XMA_CONTEXT_DATA context(context_ptr);
context.output_buffer_valid = 1;
context.Store(context_ptr);
return 0;
}
ppc_u32_result_t XMAGetOutputBufferReadOffset_entry(ppc_pvoid_t context_ptr) {
XMA_CONTEXT_DATA context(context_ptr);
return context.output_buffer_read_offset;
}
ppc_u32_result_t XMASetOutputBufferReadOffset_entry(ppc_pvoid_t context_ptr, ppc_u32_t value) {
XMA_CONTEXT_DATA context(context_ptr);
context.output_buffer_read_offset = value;
context.Store(context_ptr);
return 0;
}
ppc_u32_result_t XMAGetOutputBufferWriteOffset_entry(ppc_pvoid_t context_ptr) {
XMA_CONTEXT_DATA context(context_ptr);
return context.output_buffer_write_offset;
}
ppc_u32_result_t XMAGetPacketMetadata_entry(ppc_pvoid_t context_ptr) {
XMA_CONTEXT_DATA context(context_ptr);
return context.packet_metadata;
}
ppc_u32_result_t XMAEnableContext_entry(ppc_pvoid_t context_ptr) {
StoreXmaContextIndexedRegister(REX_KERNEL_STATE(), 0x1940, context_ptr.guest_address());
return 0;
}
ppc_u32_result_t XMADisableContext_entry(ppc_pvoid_t context_ptr, ppc_u32_t wait) {
X_HRESULT result = X_E_SUCCESS;
StoreXmaContextIndexedRegister(REX_KERNEL_STATE(), 0x1A40, context_ptr.guest_address());
if (!static_cast<audio::AudioSystem*>(REX_KERNEL_STATE()->emulator()->audio_system())
->xma_decoder()
->BlockOnContext(context_ptr.guest_address(), !wait)) {
result = X_E_FALSE;
}
return result;
}
ppc_u32_result_t XMABlockWhileInUse_entry(ppc_pvoid_t context_ptr) {
do {
XMA_CONTEXT_DATA context(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;
}
} // namespace rex::kernel::xboxkrnl
XBOXKRNL_EXPORT(__imp__XMACreateContext, rex::kernel::xboxkrnl::XMACreateContext_entry)
XBOXKRNL_EXPORT(__imp__XMAReleaseContext, rex::kernel::xboxkrnl::XMAReleaseContext_entry)
XBOXKRNL_EXPORT(__imp__XMAInitializeContext, rex::kernel::xboxkrnl::XMAInitializeContext_entry)
XBOXKRNL_EXPORT(__imp__XMASetLoopData, rex::kernel::xboxkrnl::XMASetLoopData_entry)
XBOXKRNL_EXPORT(__imp__XMAGetInputBufferReadOffset,
rex::kernel::xboxkrnl::XMAGetInputBufferReadOffset_entry)
XBOXKRNL_EXPORT(__imp__XMASetInputBufferReadOffset,
rex::kernel::xboxkrnl::XMASetInputBufferReadOffset_entry)
XBOXKRNL_EXPORT(__imp__XMASetInputBuffer0, rex::kernel::xboxkrnl::XMASetInputBuffer0_entry)
XBOXKRNL_EXPORT(__imp__XMAIsInputBuffer0Valid, rex::kernel::xboxkrnl::XMAIsInputBuffer0Valid_entry)
XBOXKRNL_EXPORT(__imp__XMASetInputBuffer0Valid,
rex::kernel::xboxkrnl::XMASetInputBuffer0Valid_entry)
XBOXKRNL_EXPORT(__imp__XMASetInputBuffer1, rex::kernel::xboxkrnl::XMASetInputBuffer1_entry)
XBOXKRNL_EXPORT(__imp__XMAIsInputBuffer1Valid, rex::kernel::xboxkrnl::XMAIsInputBuffer1Valid_entry)
XBOXKRNL_EXPORT(__imp__XMASetInputBuffer1Valid,
rex::kernel::xboxkrnl::XMASetInputBuffer1Valid_entry)
XBOXKRNL_EXPORT(__imp__XMAIsOutputBufferValid, rex::kernel::xboxkrnl::XMAIsOutputBufferValid_entry)
XBOXKRNL_EXPORT(__imp__XMASetOutputBufferValid,
rex::kernel::xboxkrnl::XMASetOutputBufferValid_entry)
XBOXKRNL_EXPORT(__imp__XMAGetOutputBufferReadOffset,
rex::kernel::xboxkrnl::XMAGetOutputBufferReadOffset_entry)
XBOXKRNL_EXPORT(__imp__XMASetOutputBufferReadOffset,
rex::kernel::xboxkrnl::XMASetOutputBufferReadOffset_entry)
XBOXKRNL_EXPORT(__imp__XMAGetOutputBufferWriteOffset,
rex::kernel::xboxkrnl::XMAGetOutputBufferWriteOffset_entry)
XBOXKRNL_EXPORT(__imp__XMAGetPacketMetadata, rex::kernel::xboxkrnl::XMAGetPacketMetadata_entry)
XBOXKRNL_EXPORT(__imp__XMAEnableContext, rex::kernel::xboxkrnl::XMAEnableContext_entry)
XBOXKRNL_EXPORT(__imp__XMADisableContext, rex::kernel::xboxkrnl::XMADisableContext_entry)
XBOXKRNL_EXPORT(__imp__XMABlockWhileInUse, rex::kernel::xboxkrnl::XMABlockWhileInUse_entry)
@@ -1,312 +0,0 @@
/**
* @file ui/rex_app.cpp
* @brief ReXApp implementation — compiled as part of the consumer executable
*
* @copyright Copyright (c) 2026 Tom Clay <tomc@tctechstuff.com>
* All rights reserved.
*
* @license BSD 3-Clause License
* See LICENSE file in the project root for full license text.
*/
#include <rex/rex_app.h>
#include <rex/cvar.h>
#include <rex/ui/flags.h>
#include <rex/kernel/crt/heap.h>
#include <rex/filesystem.h>
#include <rex/logging/sink.h>
#include <rex/logging.h>
#include <rex/ui/overlay/console_overlay.h>
#include <rex/ui/overlay/debug_overlay.h>
#include <rex/ui/overlay/settings_overlay.h>
#include <rex/graphics/graphics_system.h>
#if REX_HAS_VULKAN
#include <rex/graphics/vulkan/graphics_system.h>
#endif
#if REX_HAS_D3D12
#include <rex/graphics/d3d12/graphics_system.h>
#endif
#include <rex/audio/audio_system.h>
#include <rex/audio/sdl/sdl_audio_system.h>
#include <rex/input/input_system.h>
#include <rex/kernel/init.h>
#include <rex/system/kernel_state.h>
#include <rex/system/xthread.h>
#include <rex/ui/graphics_provider.h>
#include <rex/ui/keybinds.h>
#include <rex/version.h>
#include <imgui.h>
#include <filesystem>
REXCVAR_DEFINE_STRING(user_data_root, "", "Runtime", "Override user data path");
REXCVAR_DEFINE_STRING(update_data_root, "", "Runtime", "Override update data path");
namespace rex {
// --- ReXApp ---
ReXApp::~ReXApp() = default;
ReXApp::ReXApp(ui::WindowedAppContext& ctx, std::string_view name, PPCImageInfo ppc_info,
std::string_view usage)
: WindowedApp(ctx, name, usage), ppc_info_(ppc_info) {
AddPositionalOption("game_directory");
}
bool ReXApp::OnInitialize() {
auto exe_dir = rex::filesystem::GetExecutableFolder();
auto config_path = exe_dir / (std::string(GetName()) + ".toml");
// Load saved config (CVARs) before anything reads them.
if (std::filesystem::exists(config_path)) {
rex::cvar::LoadConfig(config_path);
}
// Game directory: positional arg or default to exe_dir/assets
std::filesystem::path game_dir;
if (auto arg = GetArgument("game_directory")) {
game_dir = *arg;
} else {
game_dir = exe_dir / "assets";
}
// User data: cvar override, or platform user directory
std::filesystem::path user_dir;
std::string user_data_cvar = REXCVAR_GET(user_data_root);
if (!user_data_cvar.empty()) {
user_dir = user_data_cvar;
} else {
user_dir = rex::filesystem::GetUserFolder() / GetName();
}
// Update data: cvar override, or empty (opt-in)
std::filesystem::path update_dir;
std::string update_data_cvar = REXCVAR_GET(update_data_root);
if (!update_data_cvar.empty()) {
update_dir = update_data_cvar;
}
// Allow subclass to override path defaults
PathConfig path_config{game_dir, user_dir, update_dir};
OnConfigurePaths(path_config);
game_data_root_ = std::move(path_config.game_data_root);
user_data_root_ = std::move(path_config.user_data_root);
update_data_root_ = std::move(path_config.update_data_root);
// Logging setup from CVARs
std::string log_file_cvar = REXCVAR_GET(log_file);
std::string log_level_str = REXCVAR_GET(log_level);
if (REXCVAR_GET(log_verbose) && log_level_str == "info") {
log_level_str = "trace";
}
auto log_config = rex::BuildLogConfig(log_file_cvar.empty() ? nullptr : log_file_cvar.c_str(),
log_level_str, {});
rex::InitLogging(log_config);
rex::RegisterLogLevelCallback();
// Attach log capture sink to all loggers for the console overlay
log_sink_ = std::make_shared<rex::LogCaptureSink>();
rex::AddSink(log_sink_);
if (std::filesystem::exists(config_path)) {
REXLOG_INFO("Loaded config: {}", config_path.filename().string());
}
REXLOG_INFO("{} starting", GetName());
REXLOG_INFO(" Game directory: {}", game_data_root_.string());
if (!user_data_root_.empty()) {
REXLOG_INFO(" User data: {}", user_data_root_.string());
}
if (!update_data_root_.empty()) {
REXLOG_INFO(" Update data: {}", update_data_root_.string());
}
// Create runtime
runtime_ = std::make_unique<rex::Runtime>(game_data_root_, user_data_root_, update_data_root_);
runtime_->set_app_context(&app_context());
// Build runtime config with default platform backends
rex::RuntimeConfig config;
#if REX_HAS_D3D12
config.graphics = REX_GRAPHICS_BACKEND(rex::graphics::d3d12::D3D12GraphicsSystem);
#elif REX_HAS_VULKAN
config.graphics = REX_GRAPHICS_BACKEND(rex::graphics::vulkan::VulkanGraphicsSystem);
#endif
config.audio_factory = REX_AUDIO_BACKEND(rex::audio::sdl::SDLAudioSystem);
config.input_factory = REX_INPUT_BACKEND(rex::input::CreateDefaultInputSystem);
config.kernel_init = rex::kernel::InitializeKernel;
// Allow subclass to customize config
OnPreSetup(config);
auto status = runtime_->Setup(ppc_info_.code_base, ppc_info_.code_size, ppc_info_.image_base,
ppc_info_.image_size, ppc_info_.func_mappings, std::move(config));
if (XFAILED(status)) {
REXLOG_ERROR("Runtime setup failed: {:08X}", status);
return false;
}
std::string xex_image = "game:\\default.xex";
// Allow subclass to override xex image
OnLoadXexImage(xex_image);
// Load XEX image
status = runtime_->LoadXexImage(xex_image);
if (XFAILED(status)) {
REXLOG_ERROR("Failed to load XEX: {:08X}", status);
return false;
}
// Initialize rexcrt heap after LoadXexImage to avoid guest memory writes
// corrupting the heap region. rexcrt_heap is set by codegen (REXCRT_HEAP)
// when [rexcrt] contains heap functions -- originals are stripped so init
// is required. Size is controlled by the rexcrt_heap_size_mb CVAR.
if (ppc_info_.rexcrt_heap) {
if (!rex::kernel::crt::InitHeap(REXCVAR_GET(rexcrt_heap_size_mb), runtime_->memory())) {
REXLOG_ERROR("Failed to initialize rexcrt heap");
return false;
}
}
// Notify subclass
OnPostSetup();
// Create window
window_ = rex::ui::Window::Create(app_context(), GetName(), 1280, 720);
if (!window_) {
REXLOG_ERROR("Failed to create window");
return false;
}
// Set window title with SDK build stamp
std::string title = std::string(GetName()) + " " + REXGLUE_BUILD_TITLE;
window_->SetTitle(title);
window_->AddListener(this);
window_->AddInputListener(this, 0);
// Attach window to input system so deferred drivers (e.g. MnK) can register
if (runtime_ && runtime_->input_system()) {
static_cast<rex::input::InputSystem*>(runtime_->input_system())->AttachWindow(window_.get());
}
if (REXCVAR_GET(fullscreen)) {
window_->SetFullscreen(true);
}
window_->Open();
// Setup graphics presenter and ImGui
auto* graphics_system = static_cast<rex::graphics::GraphicsSystem*>(runtime_->graphics_system());
if (graphics_system && graphics_system->presenter()) {
auto* presenter = graphics_system->presenter();
auto* provider = graphics_system->provider();
if (provider) {
immediate_drawer_ = provider->CreateImmediateDrawer();
if (immediate_drawer_) {
immediate_drawer_->SetPresenter(presenter);
imgui_drawer_ = std::make_unique<rex::ui::ImGuiDrawer>(window_.get(), 64);
imgui_drawer_->SetPresenterAndImmediateDrawer(presenter, immediate_drawer_.get());
// Built-in overlays
debug_overlay_ = std::make_unique<ui::DebugOverlayDialog>(imgui_drawer_.get());
console_overlay_ = std::make_unique<ui::ConsoleDialog>(imgui_drawer_.get(), log_sink_);
settings_overlay_ = std::make_unique<ui::SettingsDialog>(
imgui_drawer_.get(), exe_dir / (std::string(GetName()) + ".toml"));
// Allow subclass to add custom dialogs
OnCreateDialogs(imgui_drawer_.get());
runtime_->set_display_window(window_.get());
runtime_->set_imgui_drawer(imgui_drawer_.get());
// Tell input drivers to suppress input when ImGui wants the mouse
// (e.g. overlay is open). This controls MnK mouse capture.
auto* input_sys = static_cast<rex::input::InputSystem*>(runtime_->input_system());
if (input_sys) {
input_sys->SetActiveCallback([]() { return !ImGui::GetIO().WantCaptureMouse; });
}
}
}
window_->SetPresenter(presenter);
}
// Launch module in background
app_context().CallInUIThreadDeferred([this]() {
auto main_thread = runtime_->LaunchModule();
if (!main_thread) {
REXLOG_ERROR("Failed to launch module");
app_context().QuitFromUIThread();
return;
}
module_thread_ = std::thread([this, main_thread = std::move(main_thread)]() mutable {
main_thread->Wait(0, 0, 0, nullptr);
REXLOG_INFO("Execution complete");
if (!shutting_down_.load(std::memory_order_acquire)) {
app_context().CallInUIThread([this]() { app_context().QuitFromUIThread(); });
}
});
});
return true;
}
void ReXApp::OnKeyDown(ui::KeyEvent& e) {
rex::ui::ProcessKeyEvent(e);
}
void ReXApp::OnClosing(ui::UIEvent& e) {
(void)e;
REXLOG_INFO("Window closing, shutting down...");
shutting_down_.store(true, std::memory_order_release);
if (runtime_ && runtime_->kernel_state()) {
runtime_->kernel_state()->TerminateTitle();
}
app_context().QuitFromUIThread();
}
void ReXApp::OnDestroy() {
// Notify subclass before cleanup
OnShutdown();
// ImGui cleanup (reverse of setup)
settings_overlay_.reset();
console_overlay_.reset();
debug_overlay_.reset();
if (imgui_drawer_) {
imgui_drawer_->SetPresenterAndImmediateDrawer(nullptr, nullptr);
imgui_drawer_.reset();
}
if (immediate_drawer_) {
immediate_drawer_->SetPresenter(nullptr);
immediate_drawer_.reset();
}
if (runtime_) {
runtime_->set_display_window(nullptr);
runtime_->set_imgui_drawer(nullptr);
}
// Window/runtime cleanup
if (window_) {
window_->SetPresenter(nullptr);
}
if (module_thread_.joinable()) {
module_thread_.join();
}
if (window_) {
window_->RemoveInputListener(this);
window_->RemoveListener(this);
}
window_.reset();
runtime_.reset();
}
void ReXApp::SetGuestFrameStats(ui::DebugOverlayDialog::FrameStatsProvider provider) {
if (debug_overlay_) {
debug_overlay_->SetStatsProvider(std::move(provider));
}
}
} // namespace rex
File diff suppressed because it is too large Load Diff
@@ -1,559 +0,0 @@
#include "ac6_audio_policy.h"
#include <algorithm>
#include <chrono>
#include <mutex>
#include <vector>
#include <rex/cvar.h>
#include <rex/logging.h>
REXCVAR_DEFINE_BOOL(ac6_audio_deep_trace, false, "AC6",
"Enable high-volume AC6 audio diagnostics across AC6 hooks, kernel audio, "
"worker cadence, and host queue telemetry");
REXCVAR_DEFINE_BOOL(ac6_unlock_fps_video_safe, true, "AC6",
"Keep stock timing while AC6 movie-audio clients are active");
REXCVAR_DEFINE_BOOL(ac6_movie_audio_trace, false, "AC6",
"Trace AC6 movie-audio registration, submission, and timing transitions");
REXCVAR_DEFINE_BOOL(ac6_movie_audio_trace_verbose, false, "AC6",
"Trace AC6 movie-audio cadence, draw stats, and per-frame timing while "
"movie audio is active");
REXCVAR_DEFINE_INT32(ac6_movie_audio_startup_silence_frames, 96, "AC6",
"Extra silence frames to inject before the first AC6 movie-audio submit")
.range(0, 1024);
REXCVAR_DEFINE_INT32(ac6_movie_audio_gate_rearm_ms, 1000, "AC6",
"Minimum gap between movie-gate epochs before a new cutscene-start silence budget is armed")
.range(0, 10000);
REXCVAR_DEFINE_INT32(ac6_movie_audio_gate_min_client_age_ms, 5000, "AC6",
"Minimum age of the active movie-audio client before movie-gate silence can arm")
.range(0, 30000);
REXCVAR_DEFINE_INT32(ac6_movie_audio_visual_arm_consecutive_frames, 2, "AC6",
"Number of consecutive cinematic-looking presents required before movie-audio silence arms")
.range(1, 8);
REXCVAR_DEFINE_INT32(ac6_movie_audio_visual_max_draw_calls, 8, "AC6",
"Maximum draw calls for a present to count as cinematic-looking")
.range(0, 256);
REXCVAR_DEFINE_INT32(ac6_movie_audio_visual_max_texture_sets, 8, "AC6",
"Maximum texture binds for a present to count as cinematic-looking")
.range(0, 256);
REXCVAR_DEFINE_INT32(ac6_movie_audio_visual_max_resolves, 0, "AC6",
"Maximum resolves for a present to count as cinematic-looking")
.range(0, 16);
REXCVAR_DECLARE(bool, ac6_timing_hooks_enabled);
using Clock = std::chrono::steady_clock;
namespace {
struct MovieAudioState {
uint32_t owner_ptr{0};
uint32_t callback_ptr{0};
uint32_t callback_arg{0};
uint32_t driver_ptr{0};
uint32_t last_samples_ptr{0};
uint64_t register_count{0};
uint64_t unregister_count{0};
uint64_t submit_count{0};
Clock::time_point last_register{};
Clock::time_point last_submit{};
};
std::mutex g_movie_audio_mutex;
std::vector<MovieAudioState> g_movie_audio_clients{};
bool g_movie_audio_last_reported_active{false};
uint64_t g_movie_audio_duplicate_events{0};
uint64_t g_movie_audio_active_frame_trace_count{0};
uint32_t g_movie_gate_silence_frames_pending{0};
Clock::time_point g_last_movie_gate_entry{};
uint64_t g_movie_gate_epoch_count{0};
bool g_movie_gate_visual_candidate_pending{false};
uint32_t g_movie_gate_visual_candidate_owner{0};
uint32_t g_movie_gate_visual_candidate_driver{0};
uint32_t g_movie_gate_visual_present_streak{0};
double MillisecondsBetween(const Clock::time_point newer,
const Clock::time_point older) {
if (older.time_since_epoch().count() == 0 ||
newer.time_since_epoch().count() == 0) {
return -1.0;
}
return std::chrono::duration<double, std::milli>(newer - older).count();
}
MovieAudioState* FindMovieAudioClientLocked(const uint32_t owner_ptr,
const uint32_t driver_ptr) {
if (driver_ptr != 0) {
for (auto& client : g_movie_audio_clients) {
if (client.driver_ptr == driver_ptr) {
return &client;
}
}
}
if (owner_ptr != 0) {
for (auto& client : g_movie_audio_clients) {
if (client.owner_ptr == owner_ptr) {
return &client;
}
}
}
return nullptr;
}
MovieAudioState& UpsertMovieAudioClientLocked(const uint32_t owner_ptr,
const uint32_t driver_ptr) {
if (auto* existing = FindMovieAudioClientLocked(owner_ptr, driver_ptr)) {
return *existing;
}
g_movie_audio_clients.emplace_back();
return g_movie_audio_clients.back();
}
const MovieAudioState* SelectPrimaryMovieAudioClientLocked() {
if (g_movie_audio_clients.empty()) {
return nullptr;
}
return &*std::max_element(
g_movie_audio_clients.begin(), g_movie_audio_clients.end(),
[](const MovieAudioState& lhs, const MovieAudioState& rhs) {
const auto lhs_activity =
lhs.last_submit.time_since_epoch().count() != 0 ? lhs.last_submit
: lhs.last_register;
const auto rhs_activity =
rhs.last_submit.time_since_epoch().count() != 0 ? rhs.last_submit
: rhs.last_register;
return lhs_activity < rhs_activity;
});
}
bool ComputeMovieAudioActiveLocked() {
return !g_movie_audio_clients.empty();
}
void ReportMovieAudioStateTransitionLocked() {
const bool active = ComputeMovieAudioActiveLocked();
if (active == g_movie_audio_last_reported_active) {
return;
}
g_movie_audio_last_reported_active = active;
if (!active) {
g_movie_audio_active_frame_trace_count = 0;
}
if (!(REXCVAR_GET(ac6_movie_audio_trace) ||
ac6::audio_policy::IsDeepTraceEnabled())) {
return;
}
const MovieAudioState* primary = SelectPrimaryMovieAudioClientLocked();
double since_submit_ms = -1.0;
double since_register_ms = -1.0;
if (primary) {
const auto now = Clock::now();
since_submit_ms = MillisecondsBetween(now, primary->last_submit);
since_register_ms = MillisecondsBetween(now, primary->last_register);
}
REXAPU_DEBUG(
"AC6 movie-audio timing {} active_clients={} primary_owner={:08X} primary_driver={:08X} "
"since_submit_ms={:.3f} since_register_ms={:.3f}",
active ? "enabled" : "restored",
g_movie_audio_clients.size(),
primary ? primary->owner_ptr : 0u,
primary ? primary->driver_ptr : 0u,
since_submit_ms,
since_register_ms);
}
void MaybeReportDuplicateMovieAudioClientsLocked(const char* reason) {
if (g_movie_audio_clients.size() <= 1 ||
!(REXCVAR_GET(ac6_movie_audio_trace) ||
ac6::audio_policy::IsDeepTraceEnabled())) {
return;
}
++g_movie_audio_duplicate_events;
const MovieAudioState* primary = SelectPrimaryMovieAudioClientLocked();
REXAPU_WARN(
"AC6 movie-audio duplicate clients after {}: active_clients={} duplicate_events={} "
"primary_owner={:08X} primary_driver={:08X}",
reason,
g_movie_audio_clients.size(),
g_movie_audio_duplicate_events,
primary ? primary->owner_ptr : 0u,
primary ? primary->driver_ptr : 0u);
}
} // namespace
namespace ac6::audio_policy {
bool IsDeepTraceEnabled() {
return REXCVAR_GET(ac6_audio_deep_trace);
}
bool IsMovieAudioActive() {
std::lock_guard<std::mutex> lock(g_movie_audio_mutex);
ReportMovieAudioStateTransitionLocked();
return ComputeMovieAudioActiveLocked();
}
bool ShouldKeepStockTimingForMovieAudio() {
return REXCVAR_GET(ac6_timing_hooks_enabled) &&
REXCVAR_GET(ac6_unlock_fps_video_safe) && IsMovieAudioActive();
}
MovieAudioSnapshot GetMovieAudioSnapshot() {
std::lock_guard<std::mutex> lock(g_movie_audio_mutex);
ReportMovieAudioStateTransitionLocked();
uint64_t register_count = 0;
uint64_t submit_count = 0;
for (const auto& client : g_movie_audio_clients) {
register_count += client.register_count;
submit_count += client.submit_count;
}
const MovieAudioState* primary = SelectPrimaryMovieAudioClientLocked();
return MovieAudioSnapshot{
ComputeMovieAudioActiveLocked(),
static_cast<uint32_t>(g_movie_audio_clients.size()),
register_count,
submit_count,
primary ? primary->owner_ptr : 0u,
primary ? primary->driver_ptr : 0u,
};
}
void OnMovieAudioClientRegistered(const uint32_t owner_ptr,
const uint32_t callback_ptr,
const uint32_t callback_arg,
const uint32_t driver_ptr) {
const auto now = Clock::now();
std::lock_guard<std::mutex> lock(g_movie_audio_mutex);
const bool had_no_clients = g_movie_audio_clients.empty();
MovieAudioState& client = UpsertMovieAudioClientLocked(owner_ptr, driver_ptr);
const double since_prev_register_ms =
MillisecondsBetween(now, client.last_register);
const double since_prev_submit_ms =
MillisecondsBetween(now, client.last_submit);
client.owner_ptr = owner_ptr;
client.callback_ptr = callback_ptr;
client.callback_arg = callback_arg;
client.driver_ptr = driver_ptr;
client.register_count++;
client.last_register = now;
if (had_no_clients) {
g_movie_gate_silence_frames_pending = 0;
g_movie_gate_epoch_count = 0;
g_last_movie_gate_entry = Clock::time_point{};
g_movie_gate_visual_candidate_pending = false;
g_movie_gate_visual_candidate_owner = 0;
g_movie_gate_visual_candidate_driver = 0;
g_movie_gate_visual_present_streak = 0;
}
if (REXCVAR_GET(ac6_movie_audio_trace) || IsDeepTraceEnabled()) {
REXAPU_DEBUG(
"AC6 movie-audio register owner={:08X} callback={:08X} arg={:08X} driver={:08X} "
"registers={} submits={} active_clients={} since_prev_register_ms={:.3f} "
"since_prev_submit_ms={:.3f}",
owner_ptr,
callback_ptr,
callback_arg,
driver_ptr,
client.register_count,
client.submit_count,
g_movie_audio_clients.size(),
since_prev_register_ms,
since_prev_submit_ms);
}
MaybeReportDuplicateMovieAudioClientsLocked("register");
ReportMovieAudioStateTransitionLocked();
}
void OnMovieAudioClientUnregistered(const uint32_t owner_ptr,
const uint32_t driver_ptr) {
std::lock_guard<std::mutex> lock(g_movie_audio_mutex);
auto it = std::remove_if(
g_movie_audio_clients.begin(), g_movie_audio_clients.end(),
[owner_ptr, driver_ptr](const MovieAudioState& client) {
const bool matches_owner =
owner_ptr != 0 && owner_ptr == client.owner_ptr;
const bool matches_driver =
driver_ptr != 0 && driver_ptr == client.driver_ptr;
return matches_owner || matches_driver;
});
if (it == g_movie_audio_clients.end()) {
return;
}
if (REXCVAR_GET(ac6_movie_audio_trace) || IsDeepTraceEnabled()) {
for (auto iter = it; iter != g_movie_audio_clients.end(); ++iter) {
const double since_submit_ms =
MillisecondsBetween(Clock::now(), iter->last_submit);
REXAPU_DEBUG(
"AC6 movie-audio unregister owner={:08X} driver={:08X} submits={} registers={} "
"unregisters={} since_submit_ms={:.3f}",
iter->owner_ptr,
iter->driver_ptr,
iter->submit_count,
iter->register_count,
iter->unregister_count + 1,
since_submit_ms);
}
}
for (auto iter = it; iter != g_movie_audio_clients.end(); ++iter) {
iter->unregister_count++;
}
g_movie_audio_clients.erase(it, g_movie_audio_clients.end());
if (g_movie_audio_clients.empty()) {
g_movie_gate_silence_frames_pending = 0;
g_movie_gate_epoch_count = 0;
g_last_movie_gate_entry = Clock::time_point{};
g_movie_gate_visual_candidate_pending = false;
g_movie_gate_visual_candidate_owner = 0;
g_movie_gate_visual_candidate_driver = 0;
g_movie_gate_visual_present_streak = 0;
}
ReportMovieAudioStateTransitionLocked();
}
void OnMovieAudioFrameSubmitted(const uint32_t owner_ptr,
const uint32_t driver_ptr,
const uint32_t samples_ptr) {
const auto now = Clock::now();
std::lock_guard<std::mutex> lock(g_movie_audio_mutex);
MovieAudioState& client = UpsertMovieAudioClientLocked(owner_ptr, driver_ptr);
const double since_prev_submit_ms =
MillisecondsBetween(now, client.last_submit);
const double since_prev_register_ms =
MillisecondsBetween(now, client.last_register);
client.owner_ptr = owner_ptr;
client.driver_ptr = driver_ptr;
client.last_samples_ptr = samples_ptr;
client.submit_count++;
client.last_submit = now;
if ((REXCVAR_GET(ac6_movie_audio_trace) || IsDeepTraceEnabled()) &&
(client.submit_count <= 24 || (client.submit_count % 60) == 0 ||
since_prev_submit_ms >= 40.0)) {
REXAPU_DEBUG(
"AC6 movie-audio submit owner={:08X} driver={:08X} samples={:08X} submits={} "
"active_clients={} since_prev_submit_ms={:.3f} since_prev_register_ms={:.3f}",
owner_ptr,
driver_ptr,
samples_ptr,
client.submit_count,
g_movie_audio_clients.size(),
since_prev_submit_ms,
since_prev_register_ms);
}
MaybeReportDuplicateMovieAudioClientsLocked("submit");
ReportMovieAudioStateTransitionLocked();
}
void OnMovieGateDriverEntered(const uint32_t caller_lr, const uint32_t /*movie_state_ptr*/) {
const auto now = Clock::now();
std::lock_guard<std::mutex> lock(g_movie_audio_mutex);
if (g_movie_audio_clients.empty()) {
g_movie_gate_epoch_count = 0;
g_movie_gate_silence_frames_pending = 0;
g_last_movie_gate_entry = now;
g_movie_gate_visual_candidate_pending = false;
g_movie_gate_visual_candidate_owner = 0;
g_movie_gate_visual_candidate_driver = 0;
g_movie_gate_visual_present_streak = 0;
return;
}
if (caller_lr != 0x823B9810) {
return;
}
const double since_prev_gate_ms = MillisecondsBetween(now, g_last_movie_gate_entry);
g_last_movie_gate_entry = now;
if (g_movie_gate_silence_frames_pending != 0) {
return;
}
const double required_gap_ms =
static_cast<double>(REXCVAR_GET(ac6_movie_audio_gate_rearm_ms));
if (since_prev_gate_ms >= 0.0 && since_prev_gate_ms < required_gap_ms) {
return;
}
++g_movie_gate_epoch_count;
const MovieAudioState* primary = SelectPrimaryMovieAudioClientLocked();
const double since_register_ms =
primary ? MillisecondsBetween(now, primary->last_register) : -1.0;
const double required_client_age_ms =
static_cast<double>(REXCVAR_GET(ac6_movie_audio_gate_min_client_age_ms));
if (g_movie_gate_epoch_count == 1 ||
(since_register_ms >= 0.0 && since_register_ms < required_client_age_ms)) {
if (REXCVAR_GET(ac6_movie_audio_trace) || IsDeepTraceEnabled()) {
REXAPU_DEBUG(
"AC6 movie-audio gate epoch ignored owner={:08X} driver={:08X} epoch={} "
"since_prev_gate_ms={:.3f} since_register_ms={:.3f} caller_lr={:08X}",
primary ? primary->owner_ptr : 0u,
primary ? primary->driver_ptr : 0u,
g_movie_gate_epoch_count,
since_prev_gate_ms,
since_register_ms,
caller_lr);
}
return;
}
if (REXCVAR_GET(ac6_movie_audio_startup_silence_frames) == 0) {
return;
}
g_movie_gate_visual_candidate_pending = true;
g_movie_gate_visual_candidate_owner = primary ? primary->owner_ptr : 0u;
g_movie_gate_visual_candidate_driver = primary ? primary->driver_ptr : 0u;
g_movie_gate_visual_present_streak = 0;
if (REXCVAR_GET(ac6_movie_audio_trace) || IsDeepTraceEnabled()) {
REXAPU_DEBUG(
"AC6 movie-audio visual candidate armed owner={:08X} driver={:08X} "
"epoch={} since_prev_gate_ms={:.3f} since_register_ms={:.3f} caller_lr={:08X}",
primary ? primary->owner_ptr : 0u,
primary ? primary->driver_ptr : 0u,
g_movie_gate_epoch_count,
since_prev_gate_ms,
since_register_ms,
caller_lr);
}
}
uint32_t ConsumeMovieAudioStartupSilenceFrames(const uint32_t owner_ptr,
const uint32_t driver_ptr) {
std::lock_guard<std::mutex> lock(g_movie_audio_mutex);
MovieAudioState* client = FindMovieAudioClientLocked(owner_ptr, driver_ptr);
if (!client) {
return 0;
}
if (g_movie_gate_silence_frames_pending == 0) {
return 0;
}
const uint32_t silence_frames = g_movie_gate_silence_frames_pending;
g_movie_gate_silence_frames_pending = 0;
if (REXCVAR_GET(ac6_movie_audio_trace) || IsDeepTraceEnabled()) {
REXAPU_DEBUG(
"AC6 movie-audio gate silence consume owner={:08X} driver={:08X} frames={}",
owner_ptr,
driver_ptr,
silence_frames);
}
return silence_frames;
}
void OnPresentFrame(const double frame_time_ms, const double fps,
const uint64_t frame_count,
const ac6::d3d::DrawStatsSnapshot& draw_stats) {
const auto now = Clock::now();
uint32_t active_clients = 0;
uint32_t primary_owner = 0;
uint32_t primary_driver = 0;
uint64_t primary_submits = 0;
double since_primary_submit_ms = -1.0;
bool armed_from_visual_present = false;
uint32_t visual_streak = 0;
{
std::lock_guard<std::mutex> movie_lock(g_movie_audio_mutex);
if (!g_movie_audio_clients.empty()) {
++g_movie_audio_active_frame_trace_count;
active_clients = static_cast<uint32_t>(g_movie_audio_clients.size());
const MovieAudioState* primary = SelectPrimaryMovieAudioClientLocked();
if (primary) {
primary_owner = primary->owner_ptr;
primary_driver = primary->driver_ptr;
primary_submits = primary->submit_count;
since_primary_submit_ms =
MillisecondsBetween(now, primary->last_submit);
}
if (g_movie_gate_visual_candidate_pending &&
g_movie_gate_silence_frames_pending == 0 &&
primary_owner == g_movie_gate_visual_candidate_owner &&
primary_driver == g_movie_gate_visual_candidate_driver) {
const bool cinematic_present =
draw_stats.draw_calls <=
static_cast<uint32_t>(REXCVAR_GET(ac6_movie_audio_visual_max_draw_calls)) &&
draw_stats.set_texture_calls <=
static_cast<uint32_t>(REXCVAR_GET(ac6_movie_audio_visual_max_texture_sets)) &&
draw_stats.resolve_calls <=
static_cast<uint32_t>(REXCVAR_GET(ac6_movie_audio_visual_max_resolves));
if (cinematic_present) {
++g_movie_gate_visual_present_streak;
const uint32_t required_frames = static_cast<uint32_t>(
REXCVAR_GET(ac6_movie_audio_visual_arm_consecutive_frames));
if (g_movie_gate_visual_present_streak >= required_frames) {
g_movie_gate_silence_frames_pending = static_cast<uint32_t>(
REXCVAR_GET(ac6_movie_audio_startup_silence_frames));
g_movie_gate_visual_candidate_pending = false;
g_movie_gate_visual_present_streak = 0;
armed_from_visual_present = g_movie_gate_silence_frames_pending != 0;
}
} else {
g_movie_gate_visual_present_streak = 0;
}
visual_streak = g_movie_gate_visual_present_streak;
} else if (!g_movie_gate_visual_candidate_pending) {
g_movie_gate_visual_present_streak = 0;
}
}
}
if (active_clients == 0) {
return;
}
if (armed_from_visual_present &&
(REXCVAR_GET(ac6_movie_audio_trace) || IsDeepTraceEnabled())) {
REXAPU_DEBUG(
"AC6 movie-audio visual silence armed owner={:08X} driver={:08X} frames={} "
"frame={} draws={} textures={} resolves={}",
primary_owner,
primary_driver,
static_cast<uint32_t>(REXCVAR_GET(ac6_movie_audio_startup_silence_frames)),
frame_count,
draw_stats.draw_calls,
draw_stats.set_texture_calls,
draw_stats.resolve_calls);
}
if ((REXCVAR_GET(ac6_movie_audio_trace_verbose) || IsDeepTraceEnabled()) &&
(g_movie_audio_active_frame_trace_count <= 180 ||
(g_movie_audio_active_frame_trace_count % 60) == 0 ||
frame_time_ms >= 40.0 || since_primary_submit_ms >= 40.0 || visual_streak != 0)) {
REXAPU_DEBUG(
"AC6 movie-audio frame frame={} active_clients={} primary_owner={:08X} "
"primary_driver={:08X} primary_submits={} frame_ms={:.3f} fps={:.3f} "
"since_primary_submit_ms={:.3f} draws={} prim={} idx={} idx_shared={} "
"textures={} resolves={} visual_streak={}",
frame_count,
active_clients,
primary_owner,
primary_driver,
primary_submits,
frame_time_ms,
fps,
since_primary_submit_ms,
draw_stats.draw_calls,
draw_stats.draw_calls_primitive,
draw_stats.draw_calls_indexed,
draw_stats.draw_calls_indexed_shared,
draw_stats.set_texture_calls,
draw_stats.resolve_calls,
visual_streak);
}
}
} // namespace ac6::audio_policy
@@ -1,42 +0,0 @@
#pragma once
#include <cstdint>
#include <rex/cvar.h>
#include "d3d_state.h"
REXCVAR_DECLARE(bool, ac6_audio_deep_trace);
REXCVAR_DECLARE(bool, ac6_unlock_fps_video_safe);
REXCVAR_DECLARE(bool, ac6_movie_audio_trace);
REXCVAR_DECLARE(bool, ac6_movie_audio_trace_verbose);
namespace ac6::audio_policy {
struct MovieAudioSnapshot {
bool movie_audio_active{false};
uint32_t active_client_count{0};
uint64_t register_count{0};
uint64_t submit_count{0};
uint32_t primary_owner{0};
uint32_t primary_driver{0};
};
bool IsDeepTraceEnabled();
bool IsMovieAudioActive();
bool ShouldKeepStockTimingForMovieAudio();
MovieAudioSnapshot GetMovieAudioSnapshot();
void OnMovieAudioClientRegistered(uint32_t owner_ptr, uint32_t callback_ptr,
uint32_t callback_arg, uint32_t driver_ptr);
void OnMovieAudioClientUnregistered(uint32_t owner_ptr, uint32_t driver_ptr);
void OnMovieGateDriverEntered(uint32_t caller_lr, uint32_t movie_state_ptr);
uint32_t ConsumeMovieAudioStartupSilenceFrames(uint32_t owner_ptr, uint32_t driver_ptr);
void OnMovieAudioFrameSubmitted(uint32_t owner_ptr, uint32_t driver_ptr,
uint32_t samples_ptr);
void OnPresentFrame(double frame_time_ms, double fps, uint64_t frame_count,
const ac6::d3d::DrawStatsSnapshot& draw_stats);
} // namespace ac6::audio_policy
@@ -1,73 +0,0 @@
#pragma once
#include <rex/cvar.h>
#include <rex/audio/audio_system.h>
#include <rex/graphics/flags.h>
#include <rex/logging.h>
#include <rex/rex_app.h>
#include <rex/ui/overlay/debug_overlay.h>
#include "ac6_audio_policy.h"
#include "render_hooks.h"
REXCVAR_DECLARE(bool, vfetch_index_rounding_bias);
REXCVAR_DECLARE(bool, guest_vblank_sync_to_refresh);
class Ac6recompApp : public rex::ReXApp {
public:
using rex::ReXApp::ReXApp;
static std::unique_ptr<rex::ui::WindowedApp> Create(
rex::ui::WindowedAppContext& ctx) {
return std::unique_ptr<Ac6recompApp>(new Ac6recompApp(ctx, "ac6recomp",
PPCImageConfig));
}
protected:
void OnPreSetup(rex::RuntimeConfig& config) override {
REXCVAR_SET(vfetch_index_rounding_bias, true);
if (REXCVAR_GET(ac6_unlock_fps)) {
REXCVAR_SET(vsync, false);
REXCVAR_SET(guest_vblank_sync_to_refresh, false);
}
if (REXCVAR_GET(ac6_audio_deep_trace)) {
if (REXCVAR_GET(log_level) == "info") {
REXCVAR_SET(log_level, "debug");
}
if (REXCVAR_GET(log_file).empty()) {
REXCVAR_SET(log_file, "ac6_audio_trace.log");
}
}
}
void OnPostSetup() override {
auto* audio_system =
static_cast<rex::audio::AudioSystem*>(runtime()->audio_system());
if (!audio_system) {
REXLOG_INFO("AC6 audio path: runtime audio system unavailable");
return;
}
const auto snapshot = audio_system->GetTelemetrySnapshot();
const auto movie_audio = ac6::audio_policy::GetMovieAudioSnapshot();
REXLOG_INFO(
"AC6 audio path: runtime=AudioRuntime backend={} active_clients={} queued_frames={} "
"trace_events={}",
audio_system->GetBackendName(), snapshot.active_clients, snapshot.queued_frames,
snapshot.trace_event_count);
REXLOG_INFO(
"AC6 audio policy: unlock_fps={} video_safe={} movie_audio_active={}",
REXCVAR_GET(ac6_unlock_fps), REXCVAR_GET(ac6_unlock_fps_video_safe),
movie_audio.movie_audio_active);
REXLOG_INFO("AC6 presentation policy: vsync={} guest_vblank_sync_to_refresh={}",
REXCVAR_GET(vsync), REXCVAR_GET(guest_vblank_sync_to_refresh));
}
void OnCreateDialogs(rex::ui::ImGuiDrawer* drawer) override {
debug_overlay()->SetStatsProvider([] {
auto gs = ac6::GetFrameStats();
return rex::ui::FrameStats{gs.frame_time_ms, gs.fps, gs.frame_count};
});
}
};
@@ -1,478 +0,0 @@
#include "d3d_hooks.h"
#include <shared_mutex>
#include <rex/cvar.h>
#include <rex/logging.h>
#include <rex/ppc.h>
REXCVAR_DEFINE_BOOL(ac6_d3d_trace, false, "AC6/Render",
"Log every D3D device state change and draw call");
namespace {
const rex::LogCategoryId kLogGPU = rex::log::GPU;
} // namespace
namespace {
std::shared_mutex g_shadow_mutex;
ac6::d3d::ShadowState g_shadow{};
ac6::d3d::DrawStats g_live_stats{};
std::shared_mutex g_snapshot_mutex;
ac6::d3d::DrawStatsSnapshot g_snapshot{};
} // namespace
PPC_EXTERN_FUNC(__imp__rex_sub_821DEF18); // DrawIndexedVertices
PPC_EXTERN_FUNC(__imp__rex_sub_821DF300); // DrawIndexedVertices_Shared
PPC_EXTERN_FUNC(__imp__rex_sub_821DEA48); // DrawPrimitive
PPC_EXTERN_FUNC(__imp__rex_sub_821DD0A8); // SetTexture
PPC_EXTERN_FUNC(__imp__rex_sub_821D95C8); // SetRenderTarget
PPC_EXTERN_FUNC(__imp__rex_sub_821D9D38); // SetDepthStencil
PPC_EXTERN_FUNC(__imp__rex_sub_821DE7D0); // SetVertexDeclaration
PPC_EXTERN_FUNC(__imp__rex_sub_821DD1C8); // SetIndexBuffer
PPC_EXTERN_FUNC(__imp__rex_sub_821DA698); // SetViewport
PPC_EXTERN_FUNC(__imp__rex_sub_821DC538); // SetStreamSource
PPC_EXTERN_FUNC(__imp__rex_sub_821DC6C8); // SetSamplerState_MagFilter
PPC_EXTERN_FUNC(__imp__rex_sub_821DC9C0); // SetSamplerState_C
PPC_EXTERN_FUNC(__imp__rex_sub_821DCA68); // SetSamplerState_B
PPC_EXTERN_FUNC(__imp__rex_sub_821DCB08); // SetSamplerState_MipLevel
PPC_EXTERN_FUNC(__imp__rex_sub_821DCB88); // SetSamplerState_A
PPC_EXTERN_FUNC(__imp__rex_sub_821DBAF8); // SetShaderGPRAlloc
PPC_EXTERN_FUNC(__imp__rex_sub_821E2380); // Clear
PPC_EXTERN_FUNC(__imp__rex_sub_821E10C8); // SetTextureFetchConstant
PPC_EXTERN_FUNC(__imp__rex_sub_821E2BB8); // Resolve
// D3DDevice_DrawIndexedVertices (0x821DEF18)
PPC_FUNC_IMPL(rex_sub_821DEF18) {
PPC_FUNC_PROLOGUE();
uint32_t index_count = ctx.r6.u32;
g_live_stats.draw_calls.fetch_add(1, std::memory_order_relaxed);
g_live_stats.draw_calls_indexed.fetch_add(1, std::memory_order_relaxed);
g_live_stats.total_indices.fetch_add(index_count, std::memory_order_relaxed);
if (REXCVAR_GET(ac6_d3d_trace)) {
REXLOG_CAT_TRACE(kLogGPU,
"DrawIndexedVertices: prim={} start={} count={}",
ctx.r4.u32, ctx.r5.u32, index_count);
}
__imp__rex_sub_821DEF18(ctx, base);
}
// D3DDevice_DrawIndexedVertices_Shared (0x821DF300)
PPC_FUNC_IMPL(rex_sub_821DF300) {
PPC_FUNC_PROLOGUE();
uint32_t index_count = ctx.r7.u32;
g_live_stats.draw_calls.fetch_add(1, std::memory_order_relaxed);
g_live_stats.draw_calls_indexed_shared.fetch_add(1, std::memory_order_relaxed);
g_live_stats.total_indices.fetch_add(index_count, std::memory_order_relaxed);
if (REXCVAR_GET(ac6_d3d_trace)) {
REXLOG_CAT_TRACE(kLogGPU,
"DrawIndexedVertices_Shared: prim={} flags={} start={} count={}",
ctx.r4.u32, ctx.r5.u32, ctx.r6.u32, index_count);
}
__imp__rex_sub_821DF300(ctx, base);
}
// D3DDevice_SetTexture (0x821DD0A8)
PPC_FUNC_IMPL(rex_sub_821DD0A8) {
PPC_FUNC_PROLOGUE();
uint32_t slot = ctx.r4.u32;
uint32_t texture_ptr = ctx.r5.u32;
if (slot < ac6::d3d::kMaxTextures) {
std::unique_lock<std::shared_mutex> lock(g_shadow_mutex);
g_shadow.textures[slot] = texture_ptr;
}
g_live_stats.set_texture_calls.fetch_add(1, std::memory_order_relaxed);
if (REXCVAR_GET(ac6_d3d_trace)) {
REXLOG_CAT_TRACE(kLogGPU,
"SetTexture: slot={} texture=0x{:08X}",
slot, texture_ptr);
}
__imp__rex_sub_821DD0A8(ctx, base);
}
// D3DDevice_SetRenderTarget (0x821D95C8)
PPC_FUNC_IMPL(rex_sub_821D95C8) {
PPC_FUNC_PROLOGUE();
uint32_t index = ctx.r4.u32;
uint32_t surface = ctx.r5.u32;
if (index < ac6::d3d::kMaxRenderTargets) {
std::unique_lock<std::shared_mutex> lock(g_shadow_mutex);
g_shadow.render_targets[index] = surface;
}
g_live_stats.set_render_target_calls.fetch_add(1, std::memory_order_relaxed);
if (REXCVAR_GET(ac6_d3d_trace)) {
REXLOG_CAT_TRACE(kLogGPU,
"SetRenderTarget: index={} surface=0x{:08X}",
index, surface);
}
__imp__rex_sub_821D95C8(ctx, base);
}
// D3DDevice_SetDepthStencil (0x821D9D38)
PPC_FUNC_IMPL(rex_sub_821D9D38) {
PPC_FUNC_PROLOGUE();
uint32_t surface = ctx.r4.u32;
{
std::unique_lock<std::shared_mutex> lock(g_shadow_mutex);
g_shadow.depth_stencil = surface;
}
g_live_stats.set_depth_stencil_calls.fetch_add(1, std::memory_order_relaxed);
if (REXCVAR_GET(ac6_d3d_trace)) {
REXLOG_CAT_TRACE(kLogGPU,
"SetDepthStencil: surface=0x{:08X}", surface);
}
__imp__rex_sub_821D9D38(ctx, base);
}
// D3DDevice_SetVertexDeclaration (0x821DE7D0)
PPC_FUNC_IMPL(rex_sub_821DE7D0) {
PPC_FUNC_PROLOGUE();
uint32_t decl = ctx.r4.u32;
{
std::unique_lock<std::shared_mutex> lock(g_shadow_mutex);
g_shadow.vertex_declaration = decl;
}
g_live_stats.set_vertex_decl_calls.fetch_add(1, std::memory_order_relaxed);
if (REXCVAR_GET(ac6_d3d_trace)) {
REXLOG_CAT_TRACE(kLogGPU,
"SetVertexDeclaration: decl=0x{:08X}", decl);
}
__imp__rex_sub_821DE7D0(ctx, base);
}
// D3DDevice_SetIndexBuffer (0x821DD1C8)
PPC_FUNC_IMPL(rex_sub_821DD1C8) {
PPC_FUNC_PROLOGUE();
uint32_t buffer = ctx.r4.u32;
{
std::unique_lock<std::shared_mutex> lock(g_shadow_mutex);
g_shadow.index_buffer = buffer;
}
g_live_stats.set_index_buffer_calls.fetch_add(1, std::memory_order_relaxed);
if (REXCVAR_GET(ac6_d3d_trace)) {
REXLOG_CAT_TRACE(kLogGPU,
"SetIndexBuffer: buffer=0x{:08X}", buffer);
}
__imp__rex_sub_821DD1C8(ctx, base);
}
// D3DDevice_SetViewport (0x821DA698)
PPC_FUNC_IMPL(rex_sub_821DA698) {
PPC_FUNC_PROLOGUE();
{
std::unique_lock<std::shared_mutex> lock(g_shadow_mutex);
g_shadow.viewport.x = ctx.r4.u32;
g_shadow.viewport.y = ctx.r5.u32;
g_shadow.viewport.width = ctx.r6.u32;
g_shadow.viewport.height = ctx.r7.u32;
}
g_live_stats.set_viewport_calls.fetch_add(1, std::memory_order_relaxed);
if (REXCVAR_GET(ac6_d3d_trace)) {
REXLOG_CAT_TRACE(kLogGPU,
"SetViewport: {}x{} at ({},{})",
g_shadow.viewport.width, g_shadow.viewport.height,
g_shadow.viewport.x, g_shadow.viewport.y);
}
__imp__rex_sub_821DA698(ctx, base);
}
// D3DDevice_Resolve (0x821E2BB8)
PPC_FUNC_IMPL(rex_sub_821E2BB8) {
PPC_FUNC_PROLOGUE();
g_live_stats.resolve_calls.fetch_add(1, std::memory_order_relaxed);
if (REXCVAR_GET(ac6_d3d_trace)) {
REXLOG_CAT_TRACE(kLogGPU, "Resolve");
}
__imp__rex_sub_821E2BB8(ctx, base);
}
// D3DDevice_DrawPrimitive (0x821DEA48)
// r3=pDevice, r4=PrimitiveType, r5=VertexCount
PPC_FUNC_IMPL(rex_sub_821DEA48) {
PPC_FUNC_PROLOGUE();
uint32_t prim_type = ctx.r4.u32;
uint32_t vertex_count = ctx.r5.u32;
g_live_stats.draw_calls.fetch_add(1, std::memory_order_relaxed);
g_live_stats.draw_calls_primitive.fetch_add(1, std::memory_order_relaxed);
g_live_stats.total_vertices.fetch_add(vertex_count, std::memory_order_relaxed);
if (REXCVAR_GET(ac6_d3d_trace)) {
REXLOG_CAT_TRACE(kLogGPU,
"DrawPrimitive: prim={} count={}",
prim_type, vertex_count);
}
__imp__rex_sub_821DEA48(ctx, base);
}
// D3DDevice_SetStreamSource (0x821DC538)
// r3=pDevice, r4=StreamNumber, r5=pStreamData, r6=OffsetInBytes, r7=Stride
PPC_FUNC_IMPL(rex_sub_821DC538) {
PPC_FUNC_PROLOGUE();
uint32_t stream = ctx.r4.u32;
uint32_t buffer = ctx.r5.u32;
uint32_t offset = ctx.r6.u32;
uint32_t stride = ctx.r7.u32;
if (stream < ac6::d3d::kMaxStreams) {
std::unique_lock<std::shared_mutex> lock(g_shadow_mutex);
g_shadow.streams[stream].buffer = buffer;
g_shadow.streams[stream].offset = offset;
g_shadow.streams[stream].stride = stride;
}
g_live_stats.set_stream_source_calls.fetch_add(1, std::memory_order_relaxed);
if (REXCVAR_GET(ac6_d3d_trace)) {
REXLOG_CAT_TRACE(kLogGPU,
"SetStreamSource: stream={} buffer=0x{:08X} offset={} stride={}",
stream, buffer, offset, stride);
}
__imp__rex_sub_821DC538(ctx, base);
}
// D3DDevice_SetSamplerState_MagFilter (0x821DC6C8)
// r3=pDevice, r4=Sampler, r5=Value
PPC_FUNC_IMPL(rex_sub_821DC6C8) {
PPC_FUNC_PROLOGUE();
uint32_t sampler = ctx.r4.u32;
uint32_t value = ctx.r5.u32;
if (sampler < ac6::d3d::kMaxSamplers) {
std::unique_lock<std::shared_mutex> lock(g_shadow_mutex);
g_shadow.samplers[sampler].mag_filter = value;
}
g_live_stats.set_sampler_state_calls.fetch_add(1, std::memory_order_relaxed);
if (REXCVAR_GET(ac6_d3d_trace)) {
REXLOG_CAT_TRACE(kLogGPU,
"SetSamplerState_MagFilter: sampler={} value={}",
sampler, value);
}
__imp__rex_sub_821DC6C8(ctx, base);
}
// D3DDevice_SetSamplerState_A (0x821DCB88) — min filter
// r3=pDevice, r4=Sampler, r5=Value
PPC_FUNC_IMPL(rex_sub_821DCB88) {
PPC_FUNC_PROLOGUE();
uint32_t sampler = ctx.r4.u32;
uint32_t value = ctx.r5.u32;
if (sampler < ac6::d3d::kMaxSamplers) {
std::unique_lock<std::shared_mutex> lock(g_shadow_mutex);
g_shadow.samplers[sampler].min_filter = value;
}
g_live_stats.set_sampler_state_calls.fetch_add(1, std::memory_order_relaxed);
if (REXCVAR_GET(ac6_d3d_trace)) {
REXLOG_CAT_TRACE(kLogGPU,
"SetSamplerState_A: sampler={} value={}",
sampler, value);
}
__imp__rex_sub_821DCB88(ctx, base);
}
// D3DDevice_SetSamplerState_B (0x821DCA68) — mip filter
// r3=pDevice, r4=Sampler, r5=Value
PPC_FUNC_IMPL(rex_sub_821DCA68) {
PPC_FUNC_PROLOGUE();
uint32_t sampler = ctx.r4.u32;
uint32_t value = ctx.r5.u32;
if (sampler < ac6::d3d::kMaxSamplers) {
std::unique_lock<std::shared_mutex> lock(g_shadow_mutex);
g_shadow.samplers[sampler].mip_filter = value;
}
g_live_stats.set_sampler_state_calls.fetch_add(1, std::memory_order_relaxed);
if (REXCVAR_GET(ac6_d3d_trace)) {
REXLOG_CAT_TRACE(kLogGPU,
"SetSamplerState_B: sampler={} value={}",
sampler, value);
}
__imp__rex_sub_821DCA68(ctx, base);
}
// D3DDevice_SetSamplerState_C (0x821DC9C0) — border color
// r3=pDevice, r4=Sampler, r5=Value
PPC_FUNC_IMPL(rex_sub_821DC9C0) {
PPC_FUNC_PROLOGUE();
uint32_t sampler = ctx.r4.u32;
uint32_t value = ctx.r5.u32;
if (sampler < ac6::d3d::kMaxSamplers) {
std::unique_lock<std::shared_mutex> lock(g_shadow_mutex);
g_shadow.samplers[sampler].border_color = value;
}
g_live_stats.set_sampler_state_calls.fetch_add(1, std::memory_order_relaxed);
if (REXCVAR_GET(ac6_d3d_trace)) {
REXLOG_CAT_TRACE(kLogGPU,
"SetSamplerState_C: sampler={} value={}",
sampler, value);
}
__imp__rex_sub_821DC9C0(ctx, base);
}
// D3DDevice_SetSamplerState_MipLevel (0x821DCB08)
// r3=pDevice, r4=Sampler, r5=Value
PPC_FUNC_IMPL(rex_sub_821DCB08) {
PPC_FUNC_PROLOGUE();
uint32_t sampler = ctx.r4.u32;
uint32_t value = ctx.r5.u32;
if (sampler < ac6::d3d::kMaxSamplers) {
std::unique_lock<std::shared_mutex> lock(g_shadow_mutex);
g_shadow.samplers[sampler].mip_level = value;
}
g_live_stats.set_sampler_state_calls.fetch_add(1, std::memory_order_relaxed);
if (REXCVAR_GET(ac6_d3d_trace)) {
REXLOG_CAT_TRACE(kLogGPU,
"SetSamplerState_MipLevel: sampler={} value={}",
sampler, value);
}
__imp__rex_sub_821DCB08(ctx, base);
}
// D3DDevice_SetShaderGPRAlloc (0x821DBAF8)
// r3=pDevice, r4=Flags
PPC_FUNC_IMPL(rex_sub_821DBAF8) {
PPC_FUNC_PROLOGUE();
uint32_t flags = ctx.r4.u32;
{
std::unique_lock<std::shared_mutex> lock(g_shadow_mutex);
g_shadow.shader_gpr_alloc = flags;
}
if (REXCVAR_GET(ac6_d3d_trace)) {
REXLOG_CAT_TRACE(kLogGPU,
"SetShaderGPRAlloc: flags=0x{:08X}", flags);
}
__imp__rex_sub_821DBAF8(ctx, base);
}
// D3DDevice_Clear (0x821E2380)
// r3=pDevice, r4=Count, r5=pRects, r6=Flags, r7=Color, f1=Z, r8=Stencil, r9=EDRAMClear
PPC_FUNC_IMPL(rex_sub_821E2380) {
PPC_FUNC_PROLOGUE();
g_live_stats.clear_calls.fetch_add(1, std::memory_order_relaxed);
if (REXCVAR_GET(ac6_d3d_trace)) {
REXLOG_CAT_TRACE(kLogGPU,
"Clear: count={} flags=0x{:X} color=0x{:08X} stencil={}",
ctx.r4.u32, ctx.r6.u32, ctx.r7.u32, ctx.r8.u32);
}
__imp__rex_sub_821E2380(ctx, base);
}
// D3DDevice_SetTextureFetchConstant (0x821E10C8)
// r3=pDevice, r4=Register, r5=pTexture
PPC_FUNC_IMPL(rex_sub_821E10C8) {
PPC_FUNC_PROLOGUE();
uint32_t reg = ctx.r4.u32;
uint32_t texture = ctx.r5.u32;
if (reg < ac6::d3d::kMaxFetchConstants) {
std::unique_lock<std::shared_mutex> lock(g_shadow_mutex);
g_shadow.texture_fetch_ptrs[reg] = texture;
}
g_live_stats.set_texture_fetch_calls.fetch_add(1, std::memory_order_relaxed);
if (REXCVAR_GET(ac6_d3d_trace)) {
REXLOG_CAT_TRACE(kLogGPU,
"SetTextureFetchConstant: reg={} texture=0x{:08X}",
reg, texture);
}
__imp__rex_sub_821E10C8(ctx, base);
}
namespace ac6::d3d {
void OnFrameBoundary() {
std::unique_lock<std::shared_mutex> lock(g_snapshot_mutex);
g_snapshot.draw_calls = g_live_stats.draw_calls.load(std::memory_order_relaxed);
g_snapshot.draw_calls_indexed = g_live_stats.draw_calls_indexed.load(std::memory_order_relaxed);
g_snapshot.draw_calls_indexed_shared = g_live_stats.draw_calls_indexed_shared.load(std::memory_order_relaxed);
g_snapshot.draw_calls_primitive = g_live_stats.draw_calls_primitive.load(std::memory_order_relaxed);
g_snapshot.total_indices = g_live_stats.total_indices.load(std::memory_order_relaxed);
g_snapshot.total_vertices = g_live_stats.total_vertices.load(std::memory_order_relaxed);
g_snapshot.set_texture_calls = g_live_stats.set_texture_calls.load(std::memory_order_relaxed);
g_snapshot.set_render_target_calls = g_live_stats.set_render_target_calls.load(std::memory_order_relaxed);
g_snapshot.set_depth_stencil_calls = g_live_stats.set_depth_stencil_calls.load(std::memory_order_relaxed);
g_snapshot.set_vertex_decl_calls = g_live_stats.set_vertex_decl_calls.load(std::memory_order_relaxed);
g_snapshot.set_index_buffer_calls = g_live_stats.set_index_buffer_calls.load(std::memory_order_relaxed);
g_snapshot.set_stream_source_calls = g_live_stats.set_stream_source_calls.load(std::memory_order_relaxed);
g_snapshot.set_viewport_calls = g_live_stats.set_viewport_calls.load(std::memory_order_relaxed);
g_snapshot.set_sampler_state_calls = g_live_stats.set_sampler_state_calls.load(std::memory_order_relaxed);
g_snapshot.set_texture_fetch_calls = g_live_stats.set_texture_fetch_calls.load(std::memory_order_relaxed);
g_snapshot.clear_calls = g_live_stats.clear_calls.load(std::memory_order_relaxed);
g_snapshot.resolve_calls = g_live_stats.resolve_calls.load(std::memory_order_relaxed);
g_live_stats.Reset();
}
DrawStatsSnapshot GetDrawStats() {
std::shared_lock<std::shared_mutex> lock(g_snapshot_mutex);
return g_snapshot;
}
ShadowState GetShadowState() {
std::shared_lock<std::shared_mutex> lock(g_shadow_mutex);
return g_shadow;
}
} // namespace ac6::d3d
@@ -1,21 +0,0 @@
/**
* @file d3d_hooks.h
* @brief Public interface for the D3D state shadowing layer.
*
* Provides per-frame draw statistics and a read-only view of the current
* D3D device shadow state. Call OnFrameBoundary() once per frame (typically
* from the present/swap hook) to snapshot stats and reset counters.
*/
#pragma once
#include "d3d_state.h"
namespace ac6::d3d {
void OnFrameBoundary();
DrawStatsSnapshot GetDrawStats();
ShadowState GetShadowState();
} // namespace ac6::d3d
@@ -1,93 +0,0 @@
#include "render_hooks.h"
#include "ac6_audio_policy.h"
#include "d3d_hooks.h"
#include <chrono>
#include <mutex>
#include <rex/cvar.h>
REXCVAR_DEFINE_BOOL(ac6_unlock_fps, false, "AC6", "Unlock frame rate to 60fps");
REXCVAR_DEFINE_BOOL(ac6_timing_hooks_enabled, true, "AC6",
"Enable AC6 timing hooks that alter the game's presentation cadence");
using Clock = std::chrono::steady_clock;
namespace {
std::mutex g_frame_mutex;
double g_frame_time_ms{0.0};
double g_fps{0.0};
uint64_t g_frame_count{0};
Clock::time_point g_frame_start{};
bool AreTimingHooksActive() {
return REXCVAR_GET(ac6_timing_hooks_enabled) && REXCVAR_GET(ac6_unlock_fps);
}
} // namespace
bool ac6FlipIntervalHook() {
return AreTimingHooksActive() &&
!ac6::audio_policy::ShouldKeepStockTimingForMovieAudio();
}
bool ac6PresentIntervalHook(PPCRegister& r10) {
if (!AreTimingHooksActive() ||
ac6::audio_policy::ShouldKeepStockTimingForMovieAudio()) {
return false;
}
r10.u64 = 1;
return true;
}
void ac6DeltaDivisorHook(PPCRegister& r29) {
if (!AreTimingHooksActive() ||
ac6::audio_policy::ShouldKeepStockTimingForMovieAudio()) {
return;
}
r29.u64 = 30;
}
void ac6PresentTimingHook(PPCRegister& /*r31*/) {
ac6::d3d::OnFrameBoundary();
const auto now = Clock::now();
double frame_time_ms = 0.0;
double fps = 0.0;
uint64_t frame_count = 0;
{
std::lock_guard<std::mutex> lock(g_frame_mutex);
if (g_frame_start.time_since_epoch().count() != 0) {
g_frame_time_ms =
std::chrono::duration<double, std::milli>(now - g_frame_start).count();
g_fps = g_frame_time_ms > 0.0001 ? (1000.0 / g_frame_time_ms) : 0.0;
++g_frame_count;
}
g_frame_start = now;
frame_time_ms = g_frame_time_ms;
fps = g_fps;
frame_count = g_frame_count;
}
ac6::audio_policy::OnPresentFrame(frame_time_ms, fps, frame_count,
ac6::d3d::GetDrawStats());
}
namespace ac6 {
FrameStats GetFrameStats() {
std::lock_guard<std::mutex> lock(g_frame_mutex);
const auto movie_audio = audio_policy::GetMovieAudioSnapshot();
return FrameStats{g_frame_time_ms,
g_fps,
g_frame_count,
movie_audio.movie_audio_active,
movie_audio.active_client_count,
movie_audio.register_count,
movie_audio.submit_count,
movie_audio.primary_owner,
movie_audio.primary_driver};
}
} // namespace ac6
@@ -1,31 +0,0 @@
#pragma once
#include <cstdint>
#include <rex/cvar.h>
#include <rex/ppc/types.h>
REXCVAR_DECLARE(bool, ac6_unlock_fps);
namespace ac6 {
struct FrameStats {
double frame_time_ms{0.0};
double fps{0.0};
uint64_t frame_count{0};
bool movie_audio_active{false};
uint32_t movie_audio_client_count{0};
uint64_t movie_audio_register_count{0};
uint64_t movie_audio_submit_count{0};
uint32_t movie_audio_owner{0};
uint32_t movie_audio_driver{0};
};
FrameStats GetFrameStats();
} // namespace ac6
bool ac6FlipIntervalHook();
bool ac6PresentIntervalHook(PPCRegister& r10);
void ac6DeltaDivisorHook(PPCRegister& r29);
void ac6PresentTimingHook(PPCRegister& r31);
@@ -1,44 +0,0 @@
/**
******************************************************************************
* Xenia : Xbox 360 Emulator Research Project *
******************************************************************************
* Copyright 2013 Ben Vanik. All rights reserved. *
* Released under the BSD license - see LICENSE in the root for more details. *
******************************************************************************
*
* @modified Tom Clay, 2026 - Adapted for ReXGlue runtime
*/
#pragma once
#include <cstdint>
#include <rex/audio/audio_client.h>
#include <rex/kernel.h>
#include <rex/memory.h>
namespace rex::audio {
class AudioDriver {
public:
explicit AudioDriver(memory::Memory* memory);
virtual ~AudioDriver();
virtual bool Initialize() = 0;
virtual void Shutdown() = 0;
virtual void SubmitFrame(uint32_t samples_ptr) = 0;
virtual void SubmitSilenceFrame() = 0;
virtual AudioDriverTelemetry GetTelemetry() const;
virtual const char* backend_name() const = 0;
virtual uint32_t queue_low_water_frames() const { return 1; }
virtual uint32_t queue_target_frames() const { return 2; }
protected:
inline uint8_t* TranslatePhysical(uint32_t guest_address) const {
return memory_->TranslatePhysical(guest_address);
}
memory::Memory* memory_ = nullptr;
};
} // namespace rex::audio
@@ -1,825 +0,0 @@
/**
******************************************************************************
* Xenia : Xbox 360 Emulator Research Project *
******************************************************************************
* Copyright 2026 Tom Clay. All rights reserved. *
* Released under the BSD license - see LICENSE in the root for more details. *
******************************************************************************
*/
#include <algorithm>
#include <array>
#include <chrono>
#include <cstring>
#include <limits>
#include <memory>
#include <rex/audio/audio_runtime.h>
#include <rex/audio/sdl/sdl_audio_driver.h>
#if REX_PLATFORM_WINDOWS
#include <rex/audio/wasapi/wasapi_audio_driver.h>
#endif
#include <rex/audio/xma/xma_context_pool.h>
#include <rex/cvar.h>
#include <rex/memory.h>
#include <rex/stream.h>
#if REX_PLATFORM_WINDOWS
REXCVAR_DEFINE_STRING(audio_backend, "wasapi", "Audio", "Audio backend: wasapi, sdl")
#else
REXCVAR_DEFINE_STRING(audio_backend, "sdl", "Audio", "Audio backend: wasapi, sdl")
#endif
.allowed({"wasapi", "sdl"})
.lifecycle(rex::cvar::Lifecycle::kRequiresRestart);
REXCVAR_DEFINE_INT32(audio_max_queue_depth, 8, "Audio",
"Maximum queued render-driver frames per client");
REXCVAR_DEFINE_INT32(audio_callback_low_water_frames, 1, "Audio",
"Request a new guest callback when the runtime queue falls to this depth");
REXCVAR_DEFINE_INT32(audio_callback_target_queue_depth, 2, "Audio",
"Initial and refill queue target for runtime-owned render-driver frames");
namespace rex::audio {
namespace {
std::unique_ptr<AudioDriver> CreateConfiguredDriver(memory::Memory* memory, AudioRuntime* runtime,
const size_t client_index) {
const std::string backend = REXCVAR_GET(audio_backend);
#if REX_PLATFORM_WINDOWS
if (backend == "wasapi") {
return std::make_unique<wasapi::WasapiAudioDriver>(memory, runtime, client_index);
}
#endif
return std::make_unique<sdl::SDLAudioDriver>(memory, runtime, client_index);
}
std::unique_ptr<AudioDriver> CreateFallbackDriver(memory::Memory* memory, AudioRuntime* runtime,
const size_t client_index) {
return std::make_unique<sdl::SDLAudioDriver>(memory, runtime, client_index);
}
uint32_t QueueDepthLimit() {
return std::max(REXCVAR_GET(audio_max_queue_depth), 1);
}
uint32_t CallbackLowWaterFrames() {
const int32_t queue_limit = static_cast<int32_t>(QueueDepthLimit());
return static_cast<uint32_t>(
std::clamp(REXCVAR_GET(audio_callback_low_water_frames), 0, queue_limit));
}
uint32_t CallbackTargetQueueDepth() {
const int32_t queue_limit = static_cast<int32_t>(QueueDepthLimit());
const int32_t low_water = static_cast<int32_t>(CallbackLowWaterFrames());
return static_cast<uint32_t>(std::clamp(REXCVAR_GET(audio_callback_target_queue_depth),
std::max(low_water, 1), queue_limit));
}
uint32_t EffectiveCallbackTargetQueueDepth(const AudioClientState& client) {
const uint32_t requested_target = CallbackTargetQueueDepth();
const uint32_t driver_target = client.driver ? client.driver->queue_target_frames() : 1;
return std::clamp(std::max(requested_target, driver_target), 1u, QueueDepthLimit());
}
uint32_t EffectiveCallbackLowWaterFrames(const AudioClientState& client) {
const uint32_t target = EffectiveCallbackTargetQueueDepth(client);
const uint32_t requested_low_water = CallbackLowWaterFrames();
const uint32_t driver_low_water = client.driver ? client.driver->queue_low_water_frames() : 0;
return std::clamp(std::max(requested_low_water, driver_low_water), 0u, target - 1);
}
uint64_t ElapsedSamplesSince(const AudioClock::time_point start_time,
const AudioClock::time_point end_time) {
if (start_time.time_since_epoch().count() == 0 ||
end_time.time_since_epoch().count() == 0 || end_time <= start_time) {
return 0;
}
const auto elapsed_us =
std::chrono::duration_cast<std::chrono::microseconds>(end_time - start_time).count();
return (static_cast<uint64_t>(elapsed_us) * kAudioFrameSampleRate) / 1000000ull;
}
AudioDriverTelemetry MergeDriverTelemetry(const AudioClientState& client) {
AudioDriverTelemetry merged = client.telemetry;
if (!client.driver) {
return merged;
}
const auto driver_telemetry = client.driver->GetTelemetry();
merged.submitted_frames =
std::max(merged.submitted_frames, driver_telemetry.submitted_frames);
merged.consumed_frames = std::max(merged.consumed_frames, driver_telemetry.consumed_frames);
merged.underrun_count = std::max(merged.underrun_count, driver_telemetry.underrun_count);
merged.silence_injections =
std::max(merged.silence_injections, driver_telemetry.silence_injections);
merged.queued_depth = std::max(merged.queued_depth, driver_telemetry.queued_depth);
merged.peak_queued_depth =
std::max(merged.peak_queued_depth, driver_telemetry.peak_queued_depth);
merged.dropped_frames = std::max(merged.dropped_frames, driver_telemetry.dropped_frames);
merged.malformed_frames =
std::max(merged.malformed_frames, driver_telemetry.malformed_frames);
merged.callback_dispatch_count =
std::max(merged.callback_dispatch_count, driver_telemetry.callback_dispatch_count);
return merged;
}
uint32_t ComputeRenderDriverTic(const AudioClientState& client) {
uint64_t tic = client.clock.consumed_samples();
// AC6 can query tic before the first guest frame is ever submitted. If tic
// stays pinned to consumed samples only, the guest never observes forward
// progress and can deadlock its movie-audio startup path before submission.
if (tic == 0 && client.telemetry.callback_dispatch_count != 0) {
const auto now = AudioClock::clock_type::now();
const uint64_t callback_floor =
static_cast<uint64_t>(client.telemetry.callback_dispatch_count) *
kRenderDriverTicSamplesPerFrame;
const uint64_t host_elapsed =
ElapsedSamplesSince(client.first_callback_dispatch_time, now);
tic = std::max(callback_floor, host_elapsed);
}
return tic > std::numeric_limits<uint32_t>::max() ? std::numeric_limits<uint32_t>::max()
: static_cast<uint32_t>(tic);
}
AudioClientTimingSnapshot BuildTimingSnapshot(const AudioClientState& client) {
AudioClientTimingSnapshot snapshot;
snapshot.consumed_samples = client.clock.consumed_samples();
snapshot.consumed_frames = client.clock.consumed_frames();
snapshot.callback_dispatch_count = client.telemetry.callback_dispatch_count;
snapshot.callback_floor_tic =
static_cast<uint64_t>(client.telemetry.callback_dispatch_count) *
kRenderDriverTicSamplesPerFrame;
if (client.first_callback_dispatch_time.time_since_epoch().count() != 0) {
snapshot.host_elapsed_tic = ElapsedSamplesSince(client.first_callback_dispatch_time,
AudioClock::clock_type::now());
}
snapshot.render_driver_tic = ComputeRenderDriverTic(client);
return snapshot;
}
void ResetClientState(AudioClientState& client, const size_t client_index) {
client = AudioClientState{};
client.client_index = client_index;
client.clock.Reset();
}
} // namespace
AudioRuntime::AudioRuntime(memory::Memory* memory, runtime::FunctionDispatcher* function_dispatcher)
: memory_(memory), function_dispatcher_(function_dispatcher) {}
AudioRuntime::~AudioRuntime() {
Shutdown();
}
X_STATUS AudioRuntime::Setup(system::KernelState* kernel_state) {
std::lock_guard<std::mutex> lock(mutex_);
if (worker_running_.load(std::memory_order_acquire)) {
return X_STATUS_SUCCESS;
}
kernel_state_ = kernel_state;
peak_queued_frames_ = 0;
paused_ = false;
tick_counter_ = 0;
worker_iteration_count_ = 0;
backend_name_ = REXCVAR_GET(audio_backend);
trace_buffer_.Reset();
for (size_t i = 0; i < clients_.size(); ++i) {
ResetClientState(clients_[i], i);
}
shutdown_event_ = rex::thread::Event::CreateAutoResetEvent(false);
worker_wake_event_ = rex::thread::Event::CreateAutoResetEvent(false);
xma_context_pool_ = std::make_unique<xma::XmaContextPool>();
xma_context_pool_->Setup(memory_, &trace_buffer_);
worker_running_.store(true, std::memory_order_release);
worker_thread_ = system::object_ref<system::XHostThread>(
new system::XHostThread(kernel_state_, 128 * 1024, 0, [this]() {
WorkerThreadMain();
return 0;
}));
worker_thread_->set_name("Audio Worker");
worker_thread_->Create();
return X_STATUS_SUCCESS;
}
void AudioRuntime::Shutdown() {
{
std::lock_guard<std::mutex> lock(mutex_);
if (worker_running_.load(std::memory_order_acquire)) {
worker_running_.store(false, std::memory_order_release);
if (shutdown_event_) {
shutdown_event_->Set();
}
if (worker_wake_event_) {
worker_wake_event_->Set();
}
}
}
if (worker_thread_) {
worker_thread_->Terminate(0);
worker_thread_.reset();
}
std::lock_guard<std::mutex> lock(mutex_);
for (size_t i = 0; i < clients_.size(); ++i) {
if (clients_[i].driver) {
clients_[i].driver->Shutdown();
delete clients_[i].driver;
clients_[i].driver = nullptr;
}
if (clients_[i].wrapped_callback_arg) {
memory_->SystemHeapFree(clients_[i].wrapped_callback_arg);
}
ResetClientState(clients_[i], i);
}
if (xma_context_pool_) {
xma_context_pool_->Shutdown();
xma_context_pool_.reset();
}
worker_wake_event_.reset();
shutdown_event_.reset();
paused_ = false;
kernel_state_ = nullptr;
}
X_STATUS AudioRuntime::RegisterClient(const uint32_t callback, const uint32_t callback_arg,
size_t* out_index) {
if (!out_index || callback == 0) {
return X_E_INVALIDARG;
}
std::lock_guard<std::mutex> lock(mutex_);
for (size_t i = 0; i < clients_.size(); ++i) {
auto& client = clients_[i];
if (client.in_use) {
continue;
}
auto driver = CreateConfiguredDriver(memory_, this, i);
if (!driver || !driver->Initialize()) {
const std::string requested_backend = REXCVAR_GET(audio_backend);
if (driver) {
driver->Shutdown();
}
if (requested_backend != "sdl") {
driver = CreateFallbackDriver(memory_, this, i);
if (driver && driver->Initialize()) {
REXAPU_WARN("AudioRuntime falling back from backend={} to backend={}",
requested_backend, driver->backend_name());
} else {
if (driver) {
driver->Shutdown();
}
return X_STATUS_UNSUCCESSFUL;
}
} else {
return X_STATUS_UNSUCCESSFUL;
}
}
const uint32_t wrapped_callback_arg = memory_->SystemHeapAlloc(0x4);
memory::store_and_swap<uint32_t>(memory_->TranslateVirtual(wrapped_callback_arg), callback_arg);
client = AudioClientState{};
client.in_use = true;
client.client_index = i;
client.callback = callback;
client.callback_arg = callback_arg;
client.wrapped_callback_arg = wrapped_callback_arg;
client.driver = driver.release();
backend_name_ = client.driver->backend_name();
client.clock.Reset();
client.telemetry = {};
trace_buffer_.Record(AudioTraceSubsystem::kCore, AudioTraceEventType::kClientRegistered,
static_cast<uint32_t>(i), callback, callback_arg);
*out_index = i;
// Wake the worker so it can start dispatching callbacks for this client
if (worker_wake_event_) {
worker_wake_event_->Set();
}
return X_STATUS_SUCCESS;
}
return X_STATUS_NO_MEMORY;
}
bool AudioRuntime::UnregisterClient(const size_t index) {
std::lock_guard<std::mutex> lock(mutex_);
if (index >= clients_.size() || !clients_[index].in_use) {
return false;
}
if (clients_[index].driver) {
clients_[index].driver->Shutdown();
delete clients_[index].driver;
}
if (clients_[index].wrapped_callback_arg) {
memory_->SystemHeapFree(clients_[index].wrapped_callback_arg);
}
trace_buffer_.Record(AudioTraceSubsystem::kCore, AudioTraceEventType::kClientUnregistered,
static_cast<uint32_t>(index));
ResetClientState(clients_[index], index);
return true;
}
bool AudioRuntime::SubmitFrame(const size_t index, const uint32_t samples_ptr) {
std::lock_guard<std::mutex> lock(mutex_);
if (index >= clients_.size() || !clients_[index].in_use || !clients_[index].driver) {
return false;
}
auto& client = clients_[index];
if (!samples_ptr) {
++client.telemetry.malformed_frames;
trace_buffer_.Record(AudioTraceSubsystem::kCore, AudioTraceEventType::kMalformedFrame,
static_cast<uint32_t>(index));
return false;
}
AudioFrame frame;
frame.source_client_id = static_cast<uint32_t>(index);
frame.sequence_number = client.next_sequence_number++;
frame.guest_submit_ptr = samples_ptr;
const auto* guest_words = memory_->TranslateVirtual<uint32_t*>(samples_ptr);
if (!guest_words) {
++client.telemetry.malformed_frames;
trace_buffer_.Record(AudioTraceSubsystem::kCore, AudioTraceEventType::kMalformedFrame,
static_cast<uint32_t>(index), samples_ptr);
return false;
}
std::memcpy(frame.guest_frame_words.data(), guest_words,
sizeof(frame.guest_frame_words));
// Keep the runtime queue as bounded shadow state for telemetry/tracing only.
// The active host driver still owns actual buffering and must not be blocked
// by runtime metadata overflow.
if (client.queued_frames.size() >= QueueDepthLimit()) {
++client.telemetry.dropped_frames;
trace_buffer_.Record(AudioTraceSubsystem::kCore, AudioTraceEventType::kFrameDropped,
static_cast<uint32_t>(index), samples_ptr,
static_cast<uint32_t>(client.queued_frames.size()));
client.queued_frames.pop_front();
}
client.queued_frames.push_back(frame);
++client.telemetry.submitted_frames;
client.telemetry.queued_depth = static_cast<uint32_t>(client.queued_frames.size());
client.telemetry.peak_queued_depth =
std::max(client.telemetry.peak_queued_depth, client.telemetry.queued_depth);
client.telemetry.last_submit_ticks = static_cast<uint64_t>(NextTickLocked());
peak_queued_frames_ = std::max(peak_queued_frames_, client.telemetry.queued_depth);
trace_buffer_.Record(AudioTraceSubsystem::kCore, AudioTraceEventType::kFrameSubmitted,
static_cast<uint32_t>(index), samples_ptr, client.telemetry.queued_depth,
static_cast<uint32_t>(frame.sequence_number));
client.driver->SubmitFrame(samples_ptr);
return true;
}
bool AudioRuntime::SubmitSilenceFrame(const size_t index) {
std::lock_guard<std::mutex> lock(mutex_);
if (index >= clients_.size() || !clients_[index].in_use || !clients_[index].driver) {
return false;
}
auto& client = clients_[index];
AudioFrame frame;
frame.source_client_id = static_cast<uint32_t>(index);
frame.sequence_number = client.next_sequence_number++;
frame.guest_submit_ptr = 0;
frame.is_silence = true;
if (client.queued_frames.size() >= QueueDepthLimit()) {
++client.telemetry.dropped_frames;
trace_buffer_.Record(AudioTraceSubsystem::kCore, AudioTraceEventType::kFrameDropped,
static_cast<uint32_t>(index), 0,
static_cast<uint32_t>(client.queued_frames.size()));
client.queued_frames.pop_front();
}
client.queued_frames.push_back(frame);
++client.telemetry.submitted_frames;
++client.telemetry.silence_injections;
client.telemetry.queued_depth = static_cast<uint32_t>(client.queued_frames.size());
client.telemetry.peak_queued_depth =
std::max(client.telemetry.peak_queued_depth, client.telemetry.queued_depth);
client.telemetry.last_submit_ticks = static_cast<uint64_t>(NextTickLocked());
peak_queued_frames_ = std::max(peak_queued_frames_, client.telemetry.queued_depth);
trace_buffer_.Record(AudioTraceSubsystem::kCore, AudioTraceEventType::kFrameSubmitted,
static_cast<uint32_t>(index), 0, client.telemetry.queued_depth,
static_cast<uint32_t>(frame.sequence_number));
client.driver->SubmitSilenceFrame();
return true;
}
bool AudioRuntime::ConsumeNextFrameForClient(const size_t index, AudioFrame* out_frame) {
std::lock_guard<std::mutex> lock(mutex_);
if (index >= clients_.size() || !clients_[index].in_use) {
return false;
}
auto& client = clients_[index];
if (client.queued_frames.empty()) {
client.telemetry.queued_depth = 0;
++client.telemetry.underrun_count;
++client.telemetry.silence_injections;
return false;
}
AudioFrame frame = client.queued_frames.front();
client.queued_frames.pop_front();
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,
client.telemetry.queued_depth,
static_cast<uint32_t>(frame.sequence_number));
if (out_frame) {
*out_frame = std::move(frame);
}
return true;
}
void AudioRuntime::ReportSamplesConsumedForClient(const size_t index,
const uint32_t sample_count) {
if (!sample_count) {
return;
}
std::lock_guard<std::mutex> lock(mutex_);
if (index >= clients_.size() || !clients_[index].in_use) {
return;
}
auto& client = clients_[index];
client.telemetry.last_consume_ticks = static_cast<uint64_t>(NextTickLocked());
client.clock.AdvanceSamples(sample_count);
client.telemetry.consumed_frames =
static_cast<uint32_t>(std::min<uint64_t>(client.clock.consumed_frames(),
std::numeric_limits<uint32_t>::max()));
}
bool AudioRuntime::ShouldRequestCallbackForClient(const size_t index) const {
std::lock_guard<std::mutex> lock(mutex_);
if (index >= clients_.size() || !clients_[index].in_use) {
return false;
}
return clients_[index].queued_frames.size() <= EffectiveCallbackLowWaterFrames(clients_[index]);
}
AudioDriverTelemetry AudioRuntime::GetClientTelemetry(const size_t index) const {
std::lock_guard<std::mutex> lock(mutex_);
if (index >= clients_.size() || !clients_[index].in_use) {
return {};
}
return MergeDriverTelemetry(clients_[index]);
}
uint32_t AudioRuntime::GetClientRenderDriverTic(const size_t index) const {
std::lock_guard<std::mutex> lock(mutex_);
if (index >= clients_.size() || !clients_[index].in_use) {
return 0;
}
return ComputeRenderDriverTic(clients_[index]);
}
AudioClientTimingSnapshot AudioRuntime::GetClientTimingSnapshot(const size_t index) const {
std::lock_guard<std::mutex> lock(mutex_);
if (index >= clients_.size() || !clients_[index].in_use) {
return {};
}
return BuildTimingSnapshot(clients_[index]);
}
AudioTelemetrySnapshot AudioRuntime::GetTelemetrySnapshot() const {
std::lock_guard<std::mutex> lock(mutex_);
AudioTelemetrySnapshot snapshot;
snapshot.backend_name = backend_name();
for (size_t i = 0; i < clients_.size(); ++i) {
const auto& client = clients_[i];
auto& client_snapshot = snapshot.clients[i];
client_snapshot.in_use = client.in_use;
client_snapshot.callback = client.callback;
client_snapshot.callback_arg = client.callback_arg;
client_snapshot.telemetry = MergeDriverTelemetry(client);
client_snapshot.render_driver_tic = ComputeRenderDriverTic(client);
if (!client.in_use) {
continue;
}
++snapshot.active_clients;
snapshot.queued_frames += client_snapshot.telemetry.queued_depth;
snapshot.peak_queued_frames =
std::max(snapshot.peak_queued_frames, client_snapshot.telemetry.peak_queued_depth);
snapshot.dropped_frames += client_snapshot.telemetry.dropped_frames;
snapshot.underruns += client_snapshot.telemetry.underrun_count;
snapshot.silence_injections += client_snapshot.telemetry.silence_injections;
}
snapshot.peak_queued_frames =
std::max(snapshot.peak_queued_frames, peak_queued_frames_);
snapshot.trace_event_count = trace_buffer_.size();
return snapshot;
}
bool AudioRuntime::Save(stream::ByteStream* stream) {
if (!stream) {
return false;
}
std::lock_guard<std::mutex> lock(mutex_);
stream->Write(static_cast<uint32_t>(paused_ ? 1 : 0));
for (size_t i = 0; i < clients_.size(); ++i) {
const auto& client = clients_[i];
stream->Write(static_cast<uint32_t>(client.in_use ? 1 : 0));
if (!client.in_use) {
continue;
}
stream->Write(static_cast<uint32_t>(client.client_index));
stream->Write(client.callback);
stream->Write(client.callback_arg);
stream->Write(client.wrapped_callback_arg);
}
return xma_context_pool_ ? xma_context_pool_->Save(stream) : true;
}
bool AudioRuntime::Restore(stream::ByteStream* stream) {
if (!stream) {
return false;
}
paused_ = stream->Read<uint32_t>() != 0;
for (size_t i = 0; i < clients_.size(); ++i) {
const bool in_use = stream->Read<uint32_t>() != 0;
if (!in_use) {
continue;
}
size_t index = stream->Read<uint32_t>();
uint32_t callback = stream->Read<uint32_t>();
uint32_t callback_arg = stream->Read<uint32_t>();
uint32_t wrapped_callback_arg = stream->Read<uint32_t>();
auto driver = CreateConfiguredDriver(memory_, this, index);
if (!driver || !driver->Initialize()) {
const std::string requested_backend = REXCVAR_GET(audio_backend);
if (driver) {
driver->Shutdown();
}
if (requested_backend != "sdl") {
driver = CreateFallbackDriver(memory_, this, index);
if (!driver || !driver->Initialize()) {
if (driver) {
driver->Shutdown();
}
return false;
}
} else {
return false;
}
}
auto& client = clients_[index];
client.in_use = true;
client.client_index = index;
client.callback = callback;
client.callback_arg = callback_arg;
client.wrapped_callback_arg = wrapped_callback_arg;
client.driver = driver.release();
backend_name_ = client.driver->backend_name();
client.clock.Reset();
client.telemetry = {};
}
return xma_context_pool_ ? xma_context_pool_->Restore(stream) : true;
}
void AudioRuntime::Pause() {
std::lock_guard<std::mutex> lock(mutex_);
paused_ = true;
}
void AudioRuntime::Resume() {
std::lock_guard<std::mutex> lock(mutex_);
if (!paused_) {
return;
}
paused_ = false;
if (worker_wake_event_) {
worker_wake_event_->Set();
}
}
bool AudioRuntime::is_paused() const {
return paused_;
}
size_t AudioRuntime::ConsumeQueuedFramesForClient(const size_t index, const size_t max_frames) {
std::lock_guard<std::mutex> lock(mutex_);
if (index >= clients_.size() || !clients_[index].in_use) {
return 0;
}
auto& client = clients_[index];
size_t consumed = 0;
while (consumed < max_frames && !client.queued_frames.empty()) {
const AudioFrame frame = client.queued_frames.front();
client.queued_frames.pop_front();
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 =
static_cast<uint32_t>(std::min<uint64_t>(client.clock.consumed_frames(),
std::numeric_limits<uint32_t>::max()));
trace_buffer_.Record(AudioTraceSubsystem::kCore, AudioTraceEventType::kFrameConsumed,
static_cast<uint32_t>(index), frame.guest_submit_ptr,
client.telemetry.queued_depth,
static_cast<uint32_t>(frame.sequence_number));
++consumed;
}
return consumed;
}
size_t AudioRuntime::ConsumeAllAvailableFrames() {
size_t consumed = 0;
for (size_t i = 0; i < clients_.size(); ++i) {
consumed += ConsumeQueuedFramesForClient(i, std::numeric_limits<size_t>::max());
}
return consumed;
}
xma::XmaContextPool& AudioRuntime::xma_context_pool() {
return *xma_context_pool_;
}
const xma::XmaContextPool& AudioRuntime::xma_context_pool() const {
return *xma_context_pool_;
}
std::string AudioRuntime::backend_name() const {
return backend_name_;
}
void AudioRuntime::WorkerThreadMain() {
REXAPU_INFO("AudioRuntime worker started: backend={} scheduling=queue-depth-driven "
"low_water={} target={} max={}",
backend_name_, CallbackLowWaterFrames(), CallbackTargetQueueDepth(),
QueueDepthLimit());
rex::thread::WaitHandle* wait_handles[2]{};
wait_handles[0] = worker_wake_event_.get();
wait_handles[1] = shutdown_event_.get();
while (worker_running_.load(std::memory_order_acquire)) {
// Wait for: host driver wake, shutdown, or 5ms timeout
rex::thread::WaitAny(wait_handles, 2, true, std::chrono::milliseconds(5));
if (!worker_running_.load(std::memory_order_acquire)) {
break;
}
++worker_iteration_count_;
const bool startup_trace = worker_iteration_count_ <= 60;
// Skip dispatch while paused
{
std::lock_guard<std::mutex> lock(mutex_);
if (paused_) {
continue;
}
}
// Check each active client and dispatch callbacks to fill queue to target
for (size_t i = 0; i < clients_.size(); ++i) {
// Dispatch up to (target - current) callbacks per client per iteration
// to fill the queue towards the target depth
uint32_t dispatch = 0;
while (true) {
uint32_t client_callback = 0;
uint32_t client_callback_arg = 0;
bool needs_callback = false;
size_t current_depth = 0;
uint32_t target = 0;
{
std::lock_guard<std::mutex> lock(mutex_);
if (!clients_[i].in_use) {
break;
}
target = EffectiveCallbackTargetQueueDepth(clients_[i]);
if (dispatch >= target) {
break;
}
current_depth = clients_[i].queued_frames.size();
needs_callback = current_depth <= EffectiveCallbackLowWaterFrames(clients_[i]);
if (needs_callback) {
const auto now = AudioClock::clock_type::now();
client_callback = clients_[i].callback;
client_callback_arg = clients_[i].wrapped_callback_arg;
if (clients_[i].first_callback_dispatch_time.time_since_epoch().count() == 0) {
clients_[i].first_callback_dispatch_time = now;
}
clients_[i].last_callback_dispatch_time = now;
++clients_[i].telemetry.callback_dispatch_count;
clients_[i].telemetry.last_callback_request_ticks =
static_cast<uint64_t>(NextTickLocked());
}
}
if (!needs_callback) {
break;
}
if (!client_callback || !function_dispatcher_ || !worker_thread_) {
break;
}
if (startup_trace) {
REXAPU_INFO(
"AudioRuntime dispatch: iter={} client={} depth={} dispatch_round={} "
"callback_count={}",
worker_iteration_count_, i, current_depth, dispatch,
GetClientTelemetry(i).callback_dispatch_count);
}
uint64_t args[] = {client_callback_arg};
function_dispatcher_->Execute(worker_thread_->thread_state(), client_callback, args,
rex::countof(args));
++dispatch;
}
}
// Startup telemetry - first 60 iterations at INFO level
if (startup_trace && (worker_iteration_count_ % 10) == 0) {
std::lock_guard<std::mutex> lock(mutex_);
for (size_t i = 0; i < clients_.size(); ++i) {
if (!clients_[i].in_use) continue;
const auto& c = clients_[i];
REXAPU_INFO(
"AudioRuntime startup: iter={} client={} queued={} target={} low_water={} "
"submitted={} consumed={} underruns={} callbacks={} tic={} 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.peak_queued_depth,
c.clock.drift_ms());
}
}
// Periodic telemetry - every ~3 seconds at DEBUG level
if (!startup_trace && (worker_iteration_count_ % 600) == 0) {
std::lock_guard<std::mutex> lock(mutex_);
for (size_t i = 0; i < clients_.size(); ++i) {
if (!clients_[i].in_use) continue;
const auto& c = clients_[i];
REXAPU_DEBUG(
"AudioRuntime periodic: client={} queued={} target={} low_water={} submitted={} "
"consumed={} underruns={} callbacks={} tic={} 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.callback_dispatch_count,
ComputeRenderDriverTic(c), c.telemetry.peak_queued_depth,
c.clock.drift_ms());
}
}
}
REXAPU_INFO("AudioRuntime worker stopped after {} iterations", worker_iteration_count_);
}
uint64_t AudioRuntime::NextTickLocked() {
return ++tick_counter_;
}
void AudioRuntime::PumpBackendIfNeeded() {}
void AudioRuntime::WakeWorker() {
if (worker_wake_event_) {
worker_wake_event_->Set();
}
}
} // namespace rex::audio
@@ -1,524 +0,0 @@
/**
******************************************************************************
* ReXGlue native WASAPI audio driver *
******************************************************************************
*/
#include <rex/audio/wasapi/wasapi_audio_driver.h>
#include <algorithm>
#include <cstring>
#include <limits>
#include <memory>
#include <string>
#include <rex/audio/audio_runtime.h>
#include <rex/audio/conversion.h>
#include <rex/audio/flags.h>
#include <rex/cvar.h>
#include <rex/logging.h>
#define WIN32_LEAN_AND_MEAN
#define NOMINMAX
#define COBJMACROS
#include <Windows.h>
#include <audioclient.h>
#include <ksmedia.h>
#include <mmdeviceapi.h>
REXCVAR_DECLARE(bool, audio_trace_render_driver_verbose);
REXCVAR_DEFINE_INT32(audio_wasapi_buffer_frames, 256, "Audio",
"Requested WASAPI shared-mode period in frames").range(64, 2048);
namespace rex::audio::wasapi {
namespace {
template <typename T>
void SafeRelease(T*& value) {
if (value) {
value->Release();
value = nullptr;
}
}
uint32_t ClampRequestedFrames() {
return static_cast<uint32_t>(std::clamp(REXCVAR_GET(audio_wasapi_buffer_frames), 64, 2048));
}
uint32_t RequiredQueueFramesForDevice(const uint32_t device_buffer_frames) {
const uint32_t buffer_frames = std::max(device_buffer_frames, 1u);
return std::max(2u, (buffer_frames + kRenderDriverTicSamplesPerFrame - 1) /
kRenderDriverTicSamplesPerFrame);
}
REFERENCE_TIME FramesToHundredsOfNanoseconds(const uint32_t frame_count,
const uint32_t sample_rate) {
return static_cast<REFERENCE_TIME>((10000000ull * frame_count) / sample_rate);
}
WAVEFORMATEXTENSIBLE BuildStereoFloatFormat() {
WAVEFORMATEXTENSIBLE format = {};
format.Format.wFormatTag = WAVE_FORMAT_EXTENSIBLE;
format.Format.nChannels = 2;
format.Format.nSamplesPerSec = kAudioFrameSampleRate;
format.Format.wBitsPerSample = sizeof(float) * 8;
format.Format.nBlockAlign =
static_cast<WORD>(format.Format.nChannels * (format.Format.wBitsPerSample / 8));
format.Format.nAvgBytesPerSec =
format.Format.nSamplesPerSec * static_cast<uint32_t>(format.Format.nBlockAlign);
format.Format.cbSize =
static_cast<WORD>(sizeof(WAVEFORMATEXTENSIBLE) - sizeof(WAVEFORMATEX));
format.Samples.wValidBitsPerSample = format.Format.wBitsPerSample;
format.dwChannelMask = SPEAKER_FRONT_LEFT | SPEAKER_FRONT_RIGHT;
format.SubFormat = KSDATAFORMAT_SUBTYPE_IEEE_FLOAT;
return format;
}
} // namespace
WasapiAudioDriver::WasapiAudioDriver(memory::Memory* memory, AudioRuntime* runtime,
const size_t client_index)
: AudioDriver(memory), runtime_(runtime), client_index_(client_index) {}
WasapiAudioDriver::~WasapiAudioDriver() {
Shutdown();
}
bool WasapiAudioDriver::Initialize() {
shutting_down_.store(false, std::memory_order_release);
{
std::lock_guard<std::mutex> lock(init_mutex_);
init_done_ = false;
init_success_ = false;
init_error_.clear();
}
render_thread_ = std::thread([this]() { RenderThreadMain(); });
std::unique_lock<std::mutex> lock(init_mutex_);
init_cv_.wait(lock, [this]() { return init_done_; });
const bool success = init_success_;
lock.unlock();
if (!success && render_thread_.joinable()) {
render_thread_.join();
}
if (!success) {
REXAPU_ERROR("WasapiAudioDriver initialization failed for client {}: {}", client_index_,
init_error_);
}
return success;
}
void WasapiAudioDriver::Shutdown() {
const bool was_shutting_down = shutting_down_.exchange(true, std::memory_order_acq_rel);
if (was_shutting_down) {
if (render_thread_.joinable()) {
render_thread_.join();
}
return;
}
if (render_event_) {
SetEvent(render_event_);
}
if (render_thread_.joinable()) {
render_thread_.join();
}
std::unique_lock<std::mutex> guard(frames_mutex_);
while (!frames_unused_.empty()) {
delete[] frames_unused_.top();
frames_unused_.pop();
}
while (!frames_queued_.empty()) {
delete[] frames_queued_.front();
frames_queued_.pop();
}
pending_output_float_count_ = 0;
pending_output_float_offset_ = 0;
}
void WasapiAudioDriver::SubmitFrame(const uint32_t frame_ptr) {
if (shutting_down_.load(std::memory_order_acquire)) {
return;
}
const auto input_frame = memory_->TranslateVirtual<float*>(frame_ptr);
if (!input_frame) {
return;
}
float* output_frame = nullptr;
{
std::unique_lock<std::mutex> guard(frames_mutex_);
if (frames_unused_.empty()) {
output_frame = new float[kAudioFrameTotalSamples];
} else {
output_frame = frames_unused_.top();
frames_unused_.pop();
}
}
std::memcpy(output_frame, input_frame, sizeof(float) * kAudioFrameTotalSamples);
{
std::unique_lock<std::mutex> guard(frames_mutex_);
frames_queued_.push(output_frame);
}
const uint32_t submitted = submitted_frames_.fetch_add(1, std::memory_order_relaxed) + 1;
const uint32_t queued_depth = queued_depth_.fetch_add(1, std::memory_order_relaxed) + 1;
uint32_t previous_peak = peak_queued_depth_.load(std::memory_order_relaxed);
while (queued_depth > previous_peak &&
!peak_queued_depth_.compare_exchange_weak(previous_peak, queued_depth,
std::memory_order_relaxed)) {
}
if (REXCVAR_GET(audio_trace_render_driver_verbose) &&
(submitted <= 24 || (submitted % 60) == 0 || queued_depth <= 1)) {
REXAPU_DEBUG(
"WasapiAudioDriver::SubmitFrame frame_ptr={:08X} submitted={} consumed={} queued_depth={} peak={} underruns={}",
frame_ptr, submitted, consumed_frames_.load(std::memory_order_relaxed), queued_depth,
peak_queued_depth_.load(std::memory_order_relaxed),
underrun_count_.load(std::memory_order_relaxed));
}
}
void WasapiAudioDriver::SubmitSilenceFrame() {
if (shutting_down_.load(std::memory_order_acquire)) {
return;
}
float* output_frame = nullptr;
{
std::unique_lock<std::mutex> guard(frames_mutex_);
if (frames_unused_.empty()) {
output_frame = new float[kAudioFrameTotalSamples];
} else {
output_frame = frames_unused_.top();
frames_unused_.pop();
}
}
std::fill_n(output_frame, kAudioFrameTotalSamples, 0.0f);
{
std::unique_lock<std::mutex> guard(frames_mutex_);
frames_queued_.push(output_frame);
}
const uint32_t submitted = submitted_frames_.fetch_add(1, std::memory_order_relaxed) + 1;
const uint32_t queued_depth = queued_depth_.fetch_add(1, std::memory_order_relaxed) + 1;
silence_injections_.fetch_add(1, std::memory_order_relaxed);
uint32_t previous_peak = peak_queued_depth_.load(std::memory_order_relaxed);
while (queued_depth > previous_peak &&
!peak_queued_depth_.compare_exchange_weak(previous_peak, queued_depth,
std::memory_order_relaxed)) {
}
if (REXCVAR_GET(audio_trace_render_driver_verbose) &&
(submitted <= 24 || (submitted % 60) == 0 || queued_depth <= 1)) {
REXAPU_DEBUG(
"WasapiAudioDriver::SubmitSilenceFrame submitted={} consumed={} queued_depth={} peak={} underruns={} silence_injections={}",
submitted, consumed_frames_.load(std::memory_order_relaxed), queued_depth,
peak_queued_depth_.load(std::memory_order_relaxed),
underrun_count_.load(std::memory_order_relaxed),
silence_injections_.load(std::memory_order_relaxed));
}
}
AudioDriverTelemetry WasapiAudioDriver::GetTelemetry() const {
return AudioDriverTelemetry{
submitted_frames_.load(std::memory_order_relaxed),
consumed_frames_.load(std::memory_order_relaxed),
underrun_count_.load(std::memory_order_relaxed),
silence_injections_.load(std::memory_order_relaxed),
queued_depth_.load(std::memory_order_relaxed),
peak_queued_depth_.load(std::memory_order_relaxed),
};
}
uint32_t WasapiAudioDriver::queue_low_water_frames() const {
return std::max(1u, queue_target_frames() - 1);
}
uint32_t WasapiAudioDriver::queue_target_frames() const {
return RequiredQueueFramesForDevice(device_buffer_frames_);
}
void WasapiAudioDriver::RenderThreadMain() {
HRESULT hr = CoInitializeEx(nullptr, COINIT_MULTITHREADED);
const bool co_initialized = SUCCEEDED(hr);
if (FAILED(hr) && hr != RPC_E_CHANGED_MODE) {
SignalInitResult(false, "CoInitializeEx failed");
return;
}
IMMDeviceEnumerator* enumerator = nullptr;
IMMDevice* device = nullptr;
IAudioClient* audio_client = nullptr;
#ifdef __IAudioClient2_INTERFACE_DEFINED__
IAudioClient2* audio_client2 = nullptr;
#endif
#ifdef __IAudioClient3_INTERFACE_DEFINED__
IAudioClient3* audio_client3 = nullptr;
#endif
IAudioRenderClient* render_client = nullptr;
do {
hr = CoCreateInstance(__uuidof(MMDeviceEnumerator), nullptr, CLSCTX_INPROC_SERVER,
__uuidof(IMMDeviceEnumerator),
reinterpret_cast<void**>(&enumerator));
if (FAILED(hr)) {
SignalInitResult(false, "CoCreateInstance(MMDeviceEnumerator) failed");
break;
}
hr = enumerator->GetDefaultAudioEndpoint(eRender, eConsole, &device);
if (FAILED(hr)) {
SignalInitResult(false, "GetDefaultAudioEndpoint failed");
break;
}
hr = device->Activate(__uuidof(IAudioClient), CLSCTX_INPROC_SERVER, nullptr,
reinterpret_cast<void**>(&audio_client));
if (FAILED(hr)) {
SignalInitResult(false, "IMMDevice::Activate(IAudioClient) failed");
break;
}
#ifdef __IAudioClient2_INTERFACE_DEFINED__
hr = audio_client->QueryInterface(__uuidof(IAudioClient2),
reinterpret_cast<void**>(&audio_client2));
if (SUCCEEDED(hr) && audio_client2) {
AudioClientProperties properties = {};
properties.cbSize = sizeof(properties);
properties.eCategory = AudioCategory_GameMedia;
properties.Options = AUDCLNT_STREAMOPTIONS_NONE;
audio_client2->SetClientProperties(&properties);
}
#endif
const WAVEFORMATEXTENSIBLE requested_format = BuildStereoFloatFormat();
WAVEFORMATEX* closest_match = nullptr;
hr = audio_client->IsFormatSupported(AUDCLNT_SHAREMODE_SHARED,
reinterpret_cast<const WAVEFORMATEX*>(&requested_format),
&closest_match);
if (closest_match) {
CoTaskMemFree(closest_match);
closest_match = nullptr;
}
const DWORD base_stream_flags = AUDCLNT_STREAMFLAGS_EVENTCALLBACK |
AUDCLNT_STREAMFLAGS_AUTOCONVERTPCM |
AUDCLNT_STREAMFLAGS_SRC_DEFAULT_QUALITY;
const uint32_t requested_frames = ClampRequestedFrames();
uint32_t initialized_frames = requested_frames;
bool initialized = false;
#ifdef __IAudioClient3_INTERFACE_DEFINED__
hr = audio_client->QueryInterface(__uuidof(IAudioClient3),
reinterpret_cast<void**>(&audio_client3));
if (SUCCEEDED(hr) && audio_client3) {
UINT32 default_period = 0;
UINT32 fundamental_period = 0;
UINT32 min_period = 0;
UINT32 max_period = 0;
hr = audio_client3->GetSharedModeEnginePeriod(
reinterpret_cast<const WAVEFORMATEX*>(&requested_format), &default_period,
&fundamental_period, &min_period, &max_period);
if (SUCCEEDED(hr) && fundamental_period != 0) {
initialized_frames =
std::clamp(((requested_frames + fundamental_period - 1) / fundamental_period) *
fundamental_period,
min_period, max_period);
hr = audio_client3->InitializeSharedAudioStream(
AUDCLNT_STREAMFLAGS_EVENTCALLBACK, initialized_frames,
reinterpret_cast<const WAVEFORMATEX*>(&requested_format), nullptr);
initialized = SUCCEEDED(hr);
}
}
#endif
if (!initialized) {
initialized_frames = requested_frames;
hr = audio_client->Initialize(
AUDCLNT_SHAREMODE_SHARED, base_stream_flags,
FramesToHundredsOfNanoseconds(initialized_frames, kAudioFrameSampleRate), 0,
reinterpret_cast<const WAVEFORMATEX*>(&requested_format), nullptr);
if (FAILED(hr)) {
SignalInitResult(false, "IAudioClient initialization failed");
break;
}
}
render_event_ = CreateEventW(nullptr, FALSE, FALSE, nullptr);
if (!render_event_) {
SignalInitResult(false, "CreateEventW failed");
break;
}
hr = audio_client->SetEventHandle(render_event_);
if (FAILED(hr)) {
SignalInitResult(false, "IAudioClient::SetEventHandle failed");
break;
}
hr = audio_client->GetService(__uuidof(IAudioRenderClient),
reinterpret_cast<void**>(&render_client));
if (FAILED(hr)) {
SignalInitResult(false, "IAudioClient::GetService(IAudioRenderClient) failed");
break;
}
UINT32 buffer_frame_count = 0;
hr = audio_client->GetBufferSize(&buffer_frame_count);
if (FAILED(hr)) {
SignalInitResult(false, "IAudioClient::GetBufferSize failed");
break;
}
device_buffer_frames_ = buffer_frame_count;
BYTE* initial_buffer = nullptr;
hr = render_client->GetBuffer(buffer_frame_count, &initial_buffer);
if (FAILED(hr)) {
SignalInitResult(false, "IAudioRenderClient::GetBuffer initial fill failed");
break;
}
std::memset(initial_buffer, 0, sizeof(float) * 2 * buffer_frame_count);
hr = render_client->ReleaseBuffer(buffer_frame_count, 0);
if (FAILED(hr)) {
SignalInitResult(false, "IAudioRenderClient::ReleaseBuffer initial fill failed");
break;
}
hr = audio_client->Start();
if (FAILED(hr)) {
SignalInitResult(false, "IAudioClient::Start failed");
break;
}
REXAPU_INFO(
"WasapiAudioDriver initialized: client={} channels=2 freq={} requested_frames={} initialized_frames={} buffer_frames={}",
client_index_, kAudioFrameSampleRate, requested_frames, initialized_frames,
buffer_frame_count);
SignalInitResult(true);
while (!shutting_down_.load(std::memory_order_acquire)) {
const DWORD wait_result = WaitForSingleObject(render_event_, 10);
if (wait_result != WAIT_OBJECT_0 && wait_result != WAIT_TIMEOUT) {
break;
}
UINT32 padding_frames = 0;
hr = audio_client->GetCurrentPadding(&padding_frames);
if (FAILED(hr) || padding_frames > buffer_frame_count) {
continue;
}
UINT32 available_frames = buffer_frame_count - padding_frames;
while (available_frames > 0 && !shutting_down_.load(std::memory_order_acquire)) {
if (pending_output_float_offset_ == pending_output_float_count_) {
pending_output_float_count_ = 0;
pending_output_float_offset_ = 0;
float* buffer = nullptr;
{
std::unique_lock<std::mutex> guard(frames_mutex_);
if (!frames_queued_.empty()) {
buffer = frames_queued_.front();
frames_queued_.pop();
queued_depth_.fetch_sub(1, std::memory_order_relaxed);
}
}
if (buffer) {
if (!REXCVAR_GET(audio_mute)) {
conversion::sequential_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());
}
pending_output_float_count_ = pending_output_frame_.size();
{
std::unique_lock<std::mutex> guard(frames_mutex_);
frames_unused_.push(buffer);
}
}
}
const UINT32 pending_frames =
static_cast<UINT32>((pending_output_float_count_ - pending_output_float_offset_) / 2);
const UINT32 frames_to_write =
pending_frames != 0 ? std::min(available_frames, pending_frames) : available_frames;
BYTE* target = nullptr;
hr = render_client->GetBuffer(frames_to_write, &target);
if (FAILED(hr)) {
break;
}
if (pending_frames == 0) {
++underrun_count_;
++silence_injections_;
std::memset(target, 0, sizeof(float) * 2 * frames_to_write);
} 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_,
sizeof(float) * float_count);
pending_output_float_offset_ += float_count;
if (runtime_) {
runtime_->ReportSamplesConsumedForClient(client_index_, frames_to_write);
}
if (pending_output_float_offset_ == pending_output_float_count_) {
pending_output_float_count_ = 0;
pending_output_float_offset_ = 0;
consumed_frames_.fetch_add(1, std::memory_order_relaxed);
if (runtime_) {
runtime_->ConsumeQueuedFramesForClient(client_index_, 1);
runtime_->WakeWorker();
}
}
}
hr = render_client->ReleaseBuffer(frames_to_write, 0);
if (FAILED(hr)) {
break;
}
available_frames -= frames_to_write;
}
}
audio_client->Stop();
} while (false);
if (render_event_) {
CloseHandle(render_event_);
render_event_ = nullptr;
}
SafeRelease(render_client);
#ifdef __IAudioClient3_INTERFACE_DEFINED__
SafeRelease(audio_client3);
#endif
#ifdef __IAudioClient2_INTERFACE_DEFINED__
SafeRelease(audio_client2);
#endif
SafeRelease(audio_client);
SafeRelease(device);
SafeRelease(enumerator);
if (co_initialized) {
CoUninitialize();
}
}
void WasapiAudioDriver::SignalInitResult(const bool success, std::string error_message) {
std::lock_guard<std::mutex> lock(init_mutex_);
init_success_ = success;
init_done_ = true;
init_error_ = std::move(error_message);
init_cv_.notify_all();
}
} // namespace rex::audio::wasapi
File diff suppressed because it is too large Load Diff
@@ -1,374 +0,0 @@
# Audio Native Migration Files
Date: 2026-04-06
Repo: `C:\AC6Recomp_ext`
## Purpose
This document maps the current audio stack from the old mixed emulator/rewrite state to the target
native recomp-oriented state.
The rule is:
- preserve guest-visible `XAudio*` / `XMA*` behavior AC6 depends on
- replace host/runtime/decode internals that are still emulator-shaped
This is **not** a plan to delete guest contracts.
It is a plan to replace emulator internals with native recomp internals.
## Current Regression Note
Current native-host experiment status:
- audio output works on the new WASAPI path
- gameplay is reported as sped up
- latest trace shows:
- backend `wasapi`
- requested period `256`
- actual endpoint buffer `562`
- persistent runtime drift around `784 ms`
This means the host sink is alive, but the runtime/clock/callback contract above it is still not
native enough. Host replacement alone is not sufficient.
## Replace vs Preserve
### Preserve
These are guest contract surfaces and should remain, though their internals can change:
- `thirdparty/rexglue-sdk/src/kernel/xboxkrnl/xboxkrnl_audio.cpp`
- `thirdparty/rexglue-sdk/src/kernel/xboxkrnl/xboxkrnl_audio_xma.cpp`
- guest-visible `XAudio*` handles and callback registration
- guest-visible `XMA*` context semantics
### Replace
These are the real migration targets:
- SDL-first host playback
- driver-owned queueing
- frame-count-only tic progression
- callback-credit style scheduling
- legacy Xenia-centric XMA control flow
- AC6 hook sprawl used as permanent architecture
## File Map
### 1. Host Output Layer
Current primary/fallback files:
- `thirdparty/rexglue-sdk/src/audio/sdl/sdl_audio_driver.cpp`
- `thirdparty/rexglue-sdk/include/rex/audio/sdl/sdl_audio_driver.h`
- `thirdparty/rexglue-sdk/src/audio/sdl/sdl_audio_system.cpp`
Native replacements / targets:
- `thirdparty/rexglue-sdk/src/audio/wasapi/wasapi_audio_driver.cpp`
- `thirdparty/rexglue-sdk/include/rex/audio/wasapi/wasapi_audio_driver.h`
- optional future:
- `thirdparty/rexglue-sdk/src/audio/host/xaudio2_audio_backend.cpp`
- `thirdparty/rexglue-sdk/include/rex/audio/host/xaudio2_audio_backend.h`
Instructions:
- keep SDL only as fallback
- make WASAPI the real Windows primary path
- keep output event-driven
- report actual host latency / engine period
- do not let host buffering decide guest semantics
- add device reset / unplug recovery
### 2. Audio Runtime Core
Current files:
- `thirdparty/rexglue-sdk/src/audio/audio_runtime.cpp`
- `thirdparty/rexglue-sdk/include/rex/audio/audio_runtime.h`
- `thirdparty/rexglue-sdk/include/rex/audio/audio_client.h`
- `thirdparty/rexglue-sdk/src/audio/audio_clock.cpp`
Native replacement direction:
- keep these files, but make them authoritative instead of partially shadowing driver state
Instructions:
- move real queue ownership into `AudioRuntime`
- stop relying on per-driver private queues as the active source of truth
- convert `AudioDriver` into a sink that consumes runtime-owned frames
- make `PumpBackendIfNeeded()` real or remove it
- replace `submitted/consumed` shadow accounting with one authoritative runtime path
- make tic sample-accurate and derived from real consumption, not frame counts alone
- expose queue lead, host period, callback cadence, and drift as first-class telemetry
### 3. Host Backend Abstraction
Current files:
- `thirdparty/rexglue-sdk/include/rex/audio/host/host_audio_backend.h`
- `thirdparty/rexglue-sdk/src/audio/host/null_audio_backend.cpp`
Native replacements / additions:
- `thirdparty/rexglue-sdk/src/audio/host/wasapi_audio_backend.cpp`
- `thirdparty/rexglue-sdk/include/rex/audio/host/wasapi_audio_backend.h`
Instructions:
- stop leaving `IHostAudioBackend` mostly unused
- either:
- finish the backend abstraction and move WASAPI into it
- or remove the abstraction and make runtime->driver ownership explicit
- do not leave both models half-alive
Recommendation:
- long term, prefer `IHostAudioBackend` as the top-level host sink abstraction
- short term, the current native `WasapiAudioDriver` is acceptable as a migration bridge
### 4. Guest Kernel Audio Contract
Current files:
- `thirdparty/rexglue-sdk/src/kernel/xboxkrnl/xboxkrnl_audio.cpp`
- `thirdparty/rexglue-sdk/src/kernel/xboxkrnl/xboxkrnl_audio_xma.cpp`
Replacement direction:
- keep file locations
- narrow responsibilities to guest validation / translation / status handling only
Instructions:
- preserve:
- `XAudioRegisterRenderDriverClient`
- `XAudioSubmitRenderDriverFrame`
- `XAudioGetRenderDriverTic`
- `XMA*` guest memory semantics
- remove host scheduling assumptions from these files
- avoid backend-specific logic here
- make `XAudioGetRenderDriverTic` reflect authoritative runtime clock state only
### 5. XMA Context and Decode Layer
Current mixed files:
- `thirdparty/rexglue-sdk/src/audio/xma/context.cpp`
- `thirdparty/rexglue-sdk/include/rex/audio/xma/context.h`
- `thirdparty/rexglue-sdk/src/audio/xma/decoder.cpp`
- `thirdparty/rexglue-sdk/src/audio/xma/xma_context_pool.cpp`
- `thirdparty/rexglue-sdk/src/audio/xma/xma_decoder_backend.cpp`
- `thirdparty/rexglue-sdk/src/audio/xma/xma_packet_parser.cpp`
Replacement direction:
- make context pool authoritative
- keep FFmpeg backend swappable, not architectural center
Instructions:
- preserve guest-visible:
- valid bits
- read/write offsets
- loop state
- block/enable/disable semantics
- move decode ownership away from synchronous kick/wait control
- remove polling-sleep behavior from guest-observable paths
- keep packet assembly and guest state mutation deterministic
### 6. Audio System Entry Layer
Current file:
- `thirdparty/rexglue-sdk/src/audio/audio_system.cpp`
Replacement direction:
- keep file
- simplify startup so it clearly states active backend, active scheduler model, and active XMA path
Instructions:
- log the actual backend selected after fallback
- log whether runtime queue ownership is authoritative
- log whether XMA path is legacy/bridge/native
### 7. AC6 Policy Layer
Current files:
- `src/ac6_audio_policy.cpp`
- `src/ac6_audio_policy.h`
- `src/ac6_audio_hooks.cpp`
Replacement direction:
- keep `ac6_audio_policy.*`
- reduce `ac6_audio_hooks.cpp` over time
Instructions:
- title-specific workarounds belong in `ac6_audio_policy.*`
- diagnostic trampolines and emergency workarounds in `ac6_audio_hooks.cpp` should be retired as
core runtime/backend correctness improves
- do not let hook-based behavior remain the permanent engine architecture
### 8. Diagnostics and Trace
Current files:
- `thirdparty/rexglue-sdk/src/audio/audio_trace.cpp`
- `thirdparty/rexglue-sdk/include/rex/audio/audio_trace.h`
- `C:/AC6Recomp_ext/ac6_audio_trace.log`
Replacement direction:
- keep trace system
- reduce ad hoc AC6-only hook logging once runtime/backend signals are sufficient
Instructions:
- keep categories for:
- audio core
- kernel
- clock
- host
- xma
- AC6 policy
- add explicit host-period / endpoint-buffer / padding / drift fields to runtime-side logs
- rely less on giant hook traces
### 9. Build and Config Surface
Current files:
- `thirdparty/rexglue-sdk/src/audio/CMakeLists.txt`
- `out/build/win-amd64-relwithdebinfo/ac6recomp.toml`
Replacement direction:
- keep these files
- make backend choice explicit and restart-scoped
Instructions:
- backend config should support:
- `wasapi`
- `sdl`
- keep `null` only if reintroduced safely through runtime-owned deterministic consumption
- WASAPI must stay Windows-only in CMake
## Concrete Replacement Order
### Phase A: Stop SDL Being the Default Architecture
Files:
- `thirdparty/rexglue-sdk/src/audio/audio_runtime.cpp`
- `thirdparty/rexglue-sdk/src/audio/CMakeLists.txt`
- `thirdparty/rexglue-sdk/src/audio/wasapi/wasapi_audio_driver.cpp`
Instructions:
- done in part
- finish by making fallback/reporting robust
- measure real host period and actual device cadence
### Phase B: Make Runtime Queue Ownership Real
Files:
- `thirdparty/rexglue-sdk/src/audio/audio_runtime.cpp`
- `thirdparty/rexglue-sdk/include/rex/audio/audio_client.h`
- `thirdparty/rexglue-sdk/include/rex/audio/audio_driver.h`
- `thirdparty/rexglue-sdk/src/audio/sdl/sdl_audio_driver.cpp`
- `thirdparty/rexglue-sdk/src/audio/wasapi/wasapi_audio_driver.cpp`
Instructions:
- drivers should pull/copy from runtime-owned frame objects
- drivers should stop owning the primary queue model
- runtime should account partial consumption precisely
### Phase C: Replace Frame-Based Tic With Real Clock Semantics
Files:
- `thirdparty/rexglue-sdk/src/audio/audio_clock.cpp`
- `thirdparty/rexglue-sdk/src/audio/audio_runtime.cpp`
- `thirdparty/rexglue-sdk/src/kernel/xboxkrnl/xboxkrnl_audio.cpp`
Instructions:
- tic should reflect real consumed samples
- partial-frame host writes must affect tic correctly
- callback floor should remain a startup safety net, not the main clock
### Phase D: Remove Legacy XMA Control Flow
Files:
- `thirdparty/rexglue-sdk/src/audio/xma/decoder.cpp`
- `thirdparty/rexglue-sdk/src/audio/xma/xma_context_pool.cpp`
- `thirdparty/rexglue-sdk/src/kernel/xboxkrnl/xboxkrnl_audio_xma.cpp`
Instructions:
- replace synchronous kick/wait assumptions
- move to runtime-owned state transitions
- preserve guest memory semantics exactly
### Phase E: Retire Hook Sprawl
Files:
- `src/ac6_audio_hooks.cpp`
- `src/ac6_audio_policy.cpp`
Instructions:
- remove diagnostics/workarounds that were only needed to discover the bug
- keep only title policy that still has evidence behind it
## Files That Likely Stay But Shrink
- `src/ac6_audio_hooks.cpp`
- `thirdparty/rexglue-sdk/src/audio/sdl/sdl_audio_driver.cpp`
- `thirdparty/rexglue-sdk/src/audio/host/null_audio_backend.cpp`
Expected end state:
- hooks: debug/compat only
- SDL: fallback only
- null backend: deterministic test mode only
## Files That Become Primary
- `thirdparty/rexglue-sdk/src/audio/audio_runtime.cpp`
- `thirdparty/rexglue-sdk/src/audio/audio_clock.cpp`
- `thirdparty/rexglue-sdk/src/audio/wasapi/wasapi_audio_driver.cpp`
- `thirdparty/rexglue-sdk/src/audio/xma/xma_context_pool.cpp`
- `src/ac6_audio_policy.cpp`
## Immediate Next Technical Fix
The current “game sped up” regression says the next fix is **not** another host driver swap.
The next fix is:
- rework runtime consumption/tic to be driven by actual host sample progress
- stop the new native sink from inheriting the old queue-depth callback contract unchanged
Concrete focus files:
- `thirdparty/rexglue-sdk/src/audio/audio_runtime.cpp`
- `thirdparty/rexglue-sdk/src/audio/audio_clock.cpp`
- `thirdparty/rexglue-sdk/src/kernel/xboxkrnl/xboxkrnl_audio.cpp`
- `thirdparty/rexglue-sdk/src/audio/wasapi/wasapi_audio_driver.cpp`
## Build Note
No build was run from this editing session.
User builds locally.
@@ -1,38 +0,0 @@
# rexaudio - Audio subsystem library
# Placeholder audio surface used while the project rewrites audio from scratch.
add_library(rexaudio STATIC
audio_driver.cpp
audio_clock.cpp
audio_runtime.cpp
audio_system.cpp
audio_trace.cpp
host/null_audio_backend.cpp
nop/nop_audio_system.cpp
sdl/sdl_audio_driver.cpp
sdl/sdl_audio_system.cpp
xma/context.cpp
xma/decoder.cpp
xma/register_file.cpp
xma/xma_context_pool.cpp
xma/xma_decoder_backend.cpp
xma/xma_packet_parser.cpp
)
add_library(rex::audio ALIAS rexaudio)
if(WIN32)
target_sources(rexaudio PRIVATE wasapi/wasapi_audio_driver.cpp)
endif()
target_include_directories(rexaudio PUBLIC
$<BUILD_INTERFACE:${PROJECT_SOURCE_DIR}/include>
$<INSTALL_INTERFACE:${CMAKE_INSTALL_INCLUDEDIR}>
)
target_link_libraries(rexaudio
PUBLIC rexcore SDL3::SDL3 libavcodec libavutil
)
if(WIN32)
target_link_libraries(rexaudio PUBLIC ole32 avrt uuid)
endif()
@@ -1,75 +0,0 @@
/**
******************************************************************************
* Xenia : Xbox 360 Emulator Research Project *
******************************************************************************
* Copyright 2013 Ben Vanik. All rights reserved. *
* Released under the BSD license - see LICENSE in the root for more details. *
******************************************************************************
*
* @modified Tom Clay, 2026 - Audio runtime client model for ReXGlue
*/
#pragma once
#include <array>
#include <cstddef>
#include <cstdint>
#include <deque>
#include <rex/audio/audio_clock.h>
namespace rex::audio {
class AudioDriver;
constexpr size_t kMaximumAudioClientCount = 8;
constexpr uint32_t kRenderDriverTicSamplesPerFrame = 256;
constexpr uint32_t kAudioFrameChannelCount = 6;
constexpr uint32_t kAudioFrameSampleRate = 48000;
constexpr uint32_t kAudioFrameTotalSamples =
kAudioFrameChannelCount * kRenderDriverTicSamplesPerFrame;
struct AudioDriverTelemetry {
uint32_t submitted_frames{0};
uint32_t consumed_frames{0};
uint32_t underrun_count{0};
uint32_t silence_injections{0};
uint32_t queued_depth{0};
uint32_t peak_queued_depth{0};
uint32_t dropped_frames{0};
uint32_t malformed_frames{0};
uint32_t callback_dispatch_count{0};
uint64_t last_submit_ticks{0};
uint64_t last_consume_ticks{0};
uint64_t last_callback_request_ticks{0};
};
struct AudioFrame {
uint32_t source_client_id{0};
uint64_t sequence_number{0};
uint32_t guest_submit_ptr{0};
uint32_t sample_count{kRenderDriverTicSamplesPerFrame};
uint32_t channel_count{kAudioFrameChannelCount};
uint32_t sample_rate{kAudioFrameSampleRate};
bool is_silence{false};
bool is_discontinuity{false};
bool is_malformed{false};
std::array<uint32_t, kAudioFrameTotalSamples> guest_frame_words{};
};
struct AudioClientState {
bool in_use{false};
size_t client_index{0};
uint32_t callback{0};
uint32_t callback_arg{0};
uint32_t wrapped_callback_arg{0};
uint64_t next_sequence_number{1};
AudioDriverTelemetry telemetry{};
AudioClock clock{};
AudioDriver* driver{nullptr};
std::deque<AudioFrame> queued_frames{};
AudioClock::time_point first_callback_dispatch_time{};
AudioClock::time_point last_callback_dispatch_time{};
};
} // namespace rex::audio
@@ -1,66 +0,0 @@
/**
******************************************************************************
* Xenia : Xbox 360 Emulator Research Project *
******************************************************************************
* Copyright 2026 Tom Clay. All rights reserved. *
* Released under the BSD license - see LICENSE in the root for more details. *
******************************************************************************
*/
#include <limits>
#include <rex/audio/audio_client.h>
#include <rex/audio/audio_clock.h>
namespace rex::audio {
void AudioClock::Reset() {
consumed_samples_ = 0;
consumed_frames_ = 0;
paused_ = false;
first_advance_time_ = {};
last_advance_time_ = {};
}
void AudioClock::AdvanceFrames(const uint32_t frame_count) {
AdvanceSamples(static_cast<uint64_t>(frame_count) * kRenderDriverTicSamplesPerFrame);
}
void AudioClock::AdvanceSamples(const uint64_t sample_count) {
consumed_samples_ += sample_count;
consumed_frames_ = consumed_samples_ / kRenderDriverTicSamplesPerFrame;
const auto now = clock_type::now();
if (first_advance_time_.time_since_epoch().count() == 0) {
first_advance_time_ = now;
}
last_advance_time_ = now;
}
void AudioClock::SetPaused(const bool paused) {
paused_ = paused;
}
uint32_t AudioClock::render_driver_tic() const {
const auto tic = consumed_samples_;
return tic > std::numeric_limits<uint32_t>::max() ? std::numeric_limits<uint32_t>::max()
: static_cast<uint32_t>(tic);
}
double AudioClock::drift_ms() const {
if (first_advance_time_.time_since_epoch().count() == 0 ||
last_advance_time_.time_since_epoch().count() == 0 ||
consumed_samples_ == 0) {
return 0.0;
}
const double host_elapsed_ms =
std::chrono::duration<double, std::milli>(last_advance_time_ - first_advance_time_).count();
const double audio_elapsed_ms =
(static_cast<double>(consumed_samples_) / static_cast<double>(kAudioFrameSampleRate)) * 1000.0;
return host_elapsed_ms - audio_elapsed_ms;
}
} // namespace rex::audio
@@ -1,46 +0,0 @@
/**
******************************************************************************
* Xenia : Xbox 360 Emulator Research Project *
******************************************************************************
* Copyright 2026 Tom Clay. All rights reserved. *
* Released under the BSD license - see LICENSE in the root for more details. *
******************************************************************************
*/
#pragma once
#include <chrono>
#include <cstdint>
namespace rex::audio {
class AudioClock {
public:
using clock_type = std::chrono::steady_clock;
using time_point = clock_type::time_point;
void Reset();
void AdvanceFrames(uint32_t frame_count);
void AdvanceSamples(uint64_t sample_count);
void SetPaused(bool paused);
bool is_paused() const { return paused_; }
uint64_t consumed_samples() const { return consumed_samples_; }
uint64_t consumed_frames() const { return consumed_frames_; }
uint32_t render_driver_tic() const;
/// Returns approximate drift between host clock and sample-derived time.
/// Positive = host clock ahead of audio clock.
double drift_ms() const;
private:
uint64_t consumed_samples_{0};
uint64_t consumed_frames_{0};
bool paused_{false};
time_point first_advance_time_{};
time_point last_advance_time_{};
};
} // namespace rex::audio
@@ -1,24 +0,0 @@
/**
******************************************************************************
* Xenia : Xbox 360 Emulator Research Project *
******************************************************************************
* Copyright 2013 Ben Vanik. All rights reserved. *
* Released under the BSD license - see LICENSE in the root for more details. *
******************************************************************************
*
* @modified Tom Clay, 2026 - Adapted for ReXGlue runtime
*/
#include <rex/audio/audio_driver.h>
namespace rex::audio {
AudioDriver::AudioDriver(memory::Memory* memory) : memory_(memory) {}
AudioDriver::~AudioDriver() = default;
AudioDriverTelemetry AudioDriver::GetTelemetry() const {
return {};
}
} // namespace rex::audio
@@ -1,44 +0,0 @@
/**
******************************************************************************
* Xenia : Xbox 360 Emulator Research Project *
******************************************************************************
* Copyright 2013 Ben Vanik. All rights reserved. *
* Released under the BSD license - see LICENSE in the root for more details. *
******************************************************************************
*
* @modified Tom Clay, 2026 - Adapted for ReXGlue runtime
*/
#pragma once
#include <cstdint>
#include <rex/audio/audio_client.h>
#include <rex/kernel.h>
#include <rex/memory.h>
namespace rex::audio {
class AudioDriver {
public:
explicit AudioDriver(memory::Memory* memory);
virtual ~AudioDriver();
virtual bool Initialize() = 0;
virtual void Shutdown() = 0;
virtual void SubmitFrame(uint32_t samples_ptr) = 0;
virtual void SubmitSilenceFrame() = 0;
virtual AudioDriverTelemetry GetTelemetry() const;
virtual const char* backend_name() const = 0;
virtual uint32_t queue_low_water_frames() const { return 1; }
virtual uint32_t queue_target_frames() const { return 2; }
protected:
inline uint8_t* TranslatePhysical(uint32_t guest_address) const {
return memory_->TranslatePhysical(guest_address);
}
memory::Memory* memory_ = nullptr;
};
} // namespace rex::audio
@@ -1,837 +0,0 @@
/**
******************************************************************************
* Xenia : Xbox 360 Emulator Research Project *
******************************************************************************
* Copyright 2026 Tom Clay. All rights reserved. *
* Released under the BSD license - see LICENSE in the root for more details. *
******************************************************************************
*/
#include <algorithm>
#include <array>
#include <chrono>
#include <cstring>
#include <limits>
#include <memory>
#include <rex/audio/audio_runtime.h>
#include <rex/audio/sdl/sdl_audio_driver.h>
#if REX_PLATFORM_WINDOWS
#include <rex/audio/wasapi/wasapi_audio_driver.h>
#endif
#include <rex/audio/xma/xma_context_pool.h>
#include <rex/cvar.h>
#include <rex/memory.h>
#include <rex/stream.h>
#if REX_PLATFORM_WINDOWS
REXCVAR_DEFINE_STRING(audio_backend, "wasapi", "Audio", "Audio backend: wasapi, sdl")
#else
REXCVAR_DEFINE_STRING(audio_backend, "sdl", "Audio", "Audio backend: wasapi, sdl")
#endif
.allowed({"wasapi", "sdl"})
.lifecycle(rex::cvar::Lifecycle::kRequiresRestart);
REXCVAR_DEFINE_INT32(audio_max_queue_depth, 8, "Audio",
"Maximum queued render-driver frames per client");
REXCVAR_DEFINE_INT32(audio_callback_low_water_frames, 1, "Audio",
"Request a new guest callback when the runtime queue falls to this depth");
REXCVAR_DEFINE_INT32(audio_callback_target_queue_depth, 2, "Audio",
"Initial and refill queue target for runtime-owned render-driver frames");
REXCVAR_DEFINE_INT32(audio_force_queue_low_water_frames, -1, "Audio",
"Override runtime low-water threshold when >= 0").range(-1, 32);
REXCVAR_DEFINE_INT32(audio_force_queue_target_queue_depth, 0, "Audio",
"Override runtime queue target when > 0").range(0, 32);
namespace rex::audio {
namespace {
std::unique_ptr<AudioDriver> CreateConfiguredDriver(memory::Memory* memory, AudioRuntime* runtime,
const size_t client_index) {
const std::string backend = REXCVAR_GET(audio_backend);
#if REX_PLATFORM_WINDOWS
if (backend == "wasapi") {
return std::make_unique<wasapi::WasapiAudioDriver>(memory, runtime, client_index);
}
#endif
return std::make_unique<sdl::SDLAudioDriver>(memory, runtime, client_index);
}
std::unique_ptr<AudioDriver> CreateFallbackDriver(memory::Memory* memory, AudioRuntime* runtime,
const size_t client_index) {
return std::make_unique<sdl::SDLAudioDriver>(memory, runtime, client_index);
}
uint32_t QueueDepthLimit() {
return std::max(REXCVAR_GET(audio_max_queue_depth), 1);
}
uint32_t CallbackLowWaterFrames() {
const int32_t queue_limit = static_cast<int32_t>(QueueDepthLimit());
return static_cast<uint32_t>(
std::clamp(REXCVAR_GET(audio_callback_low_water_frames), 0, queue_limit));
}
uint32_t CallbackTargetQueueDepth() {
const int32_t queue_limit = static_cast<int32_t>(QueueDepthLimit());
const int32_t low_water = static_cast<int32_t>(CallbackLowWaterFrames());
return static_cast<uint32_t>(std::clamp(REXCVAR_GET(audio_callback_target_queue_depth),
std::max(low_water, 1), queue_limit));
}
uint32_t EffectiveCallbackTargetQueueDepth(const AudioClientState& client) {
const int32_t forced_target = REXCVAR_GET(audio_force_queue_target_queue_depth);
if (forced_target > 0) {
return std::clamp(static_cast<uint32_t>(forced_target), 1u, QueueDepthLimit());
}
const uint32_t requested_target = CallbackTargetQueueDepth();
const uint32_t driver_target = client.driver ? client.driver->queue_target_frames() : 1;
return std::clamp(std::max(requested_target, driver_target), 1u, QueueDepthLimit());
}
uint32_t EffectiveCallbackLowWaterFrames(const AudioClientState& client) {
const uint32_t target = EffectiveCallbackTargetQueueDepth(client);
const int32_t forced_low_water = REXCVAR_GET(audio_force_queue_low_water_frames);
if (forced_low_water >= 0) {
return std::clamp(static_cast<uint32_t>(forced_low_water), 0u, target - 1);
}
const uint32_t requested_low_water = CallbackLowWaterFrames();
const uint32_t driver_low_water = client.driver ? client.driver->queue_low_water_frames() : 0;
return std::clamp(std::max(requested_low_water, driver_low_water), 0u, target - 1);
}
uint64_t ElapsedSamplesSince(const AudioClock::time_point start_time,
const AudioClock::time_point end_time) {
if (start_time.time_since_epoch().count() == 0 ||
end_time.time_since_epoch().count() == 0 || end_time <= start_time) {
return 0;
}
const auto elapsed_us =
std::chrono::duration_cast<std::chrono::microseconds>(end_time - start_time).count();
return (static_cast<uint64_t>(elapsed_us) * kAudioFrameSampleRate) / 1000000ull;
}
AudioDriverTelemetry MergeDriverTelemetry(const AudioClientState& client) {
AudioDriverTelemetry merged = client.telemetry;
if (!client.driver) {
return merged;
}
const auto driver_telemetry = client.driver->GetTelemetry();
merged.submitted_frames =
std::max(merged.submitted_frames, driver_telemetry.submitted_frames);
merged.consumed_frames = std::max(merged.consumed_frames, driver_telemetry.consumed_frames);
merged.underrun_count = std::max(merged.underrun_count, driver_telemetry.underrun_count);
merged.silence_injections =
std::max(merged.silence_injections, driver_telemetry.silence_injections);
merged.queued_depth = std::max(merged.queued_depth, driver_telemetry.queued_depth);
merged.peak_queued_depth =
std::max(merged.peak_queued_depth, driver_telemetry.peak_queued_depth);
merged.dropped_frames = std::max(merged.dropped_frames, driver_telemetry.dropped_frames);
merged.malformed_frames =
std::max(merged.malformed_frames, driver_telemetry.malformed_frames);
merged.callback_dispatch_count =
std::max(merged.callback_dispatch_count, driver_telemetry.callback_dispatch_count);
return merged;
}
uint32_t ComputeRenderDriverTic(const AudioClientState& client) {
uint64_t tic = client.clock.consumed_samples();
// AC6 can query tic before the first guest frame is ever submitted. If tic
// stays pinned to consumed samples only, the guest never observes forward
// progress and can deadlock its movie-audio startup path before submission.
if (tic == 0 && client.telemetry.callback_dispatch_count != 0) {
const auto now = AudioClock::clock_type::now();
const uint64_t callback_floor =
static_cast<uint64_t>(client.telemetry.callback_dispatch_count) *
kRenderDriverTicSamplesPerFrame;
const uint64_t host_elapsed =
ElapsedSamplesSince(client.first_callback_dispatch_time, now);
tic = std::max(callback_floor, host_elapsed);
}
return tic > std::numeric_limits<uint32_t>::max() ? std::numeric_limits<uint32_t>::max()
: static_cast<uint32_t>(tic);
}
AudioClientTimingSnapshot BuildTimingSnapshot(const AudioClientState& client) {
AudioClientTimingSnapshot snapshot;
snapshot.consumed_samples = client.clock.consumed_samples();
snapshot.consumed_frames = client.clock.consumed_frames();
snapshot.callback_dispatch_count = client.telemetry.callback_dispatch_count;
snapshot.callback_floor_tic =
static_cast<uint64_t>(client.telemetry.callback_dispatch_count) *
kRenderDriverTicSamplesPerFrame;
if (client.first_callback_dispatch_time.time_since_epoch().count() != 0) {
snapshot.host_elapsed_tic = ElapsedSamplesSince(client.first_callback_dispatch_time,
AudioClock::clock_type::now());
}
snapshot.render_driver_tic = ComputeRenderDriverTic(client);
return snapshot;
}
void ResetClientState(AudioClientState& client, const size_t client_index) {
client = AudioClientState{};
client.client_index = client_index;
client.clock.Reset();
}
} // namespace
AudioRuntime::AudioRuntime(memory::Memory* memory, runtime::FunctionDispatcher* function_dispatcher)
: memory_(memory), function_dispatcher_(function_dispatcher) {}
AudioRuntime::~AudioRuntime() {
Shutdown();
}
X_STATUS AudioRuntime::Setup(system::KernelState* kernel_state) {
std::lock_guard<std::mutex> lock(mutex_);
if (worker_running_.load(std::memory_order_acquire)) {
return X_STATUS_SUCCESS;
}
kernel_state_ = kernel_state;
peak_queued_frames_ = 0;
paused_ = false;
tick_counter_ = 0;
worker_iteration_count_ = 0;
backend_name_ = REXCVAR_GET(audio_backend);
trace_buffer_.Reset();
for (size_t i = 0; i < clients_.size(); ++i) {
ResetClientState(clients_[i], i);
}
shutdown_event_ = rex::thread::Event::CreateAutoResetEvent(false);
worker_wake_event_ = rex::thread::Event::CreateAutoResetEvent(false);
xma_context_pool_ = std::make_unique<xma::XmaContextPool>();
xma_context_pool_->Setup(memory_, &trace_buffer_);
worker_running_.store(true, std::memory_order_release);
worker_thread_ = system::object_ref<system::XHostThread>(
new system::XHostThread(kernel_state_, 128 * 1024, 0, [this]() {
WorkerThreadMain();
return 0;
}));
worker_thread_->set_name("Audio Worker");
worker_thread_->Create();
return X_STATUS_SUCCESS;
}
void AudioRuntime::Shutdown() {
{
std::lock_guard<std::mutex> lock(mutex_);
if (worker_running_.load(std::memory_order_acquire)) {
worker_running_.store(false, std::memory_order_release);
if (shutdown_event_) {
shutdown_event_->Set();
}
if (worker_wake_event_) {
worker_wake_event_->Set();
}
}
}
if (worker_thread_) {
worker_thread_->Terminate(0);
worker_thread_.reset();
}
std::lock_guard<std::mutex> lock(mutex_);
for (size_t i = 0; i < clients_.size(); ++i) {
if (clients_[i].driver) {
clients_[i].driver->Shutdown();
delete clients_[i].driver;
clients_[i].driver = nullptr;
}
if (clients_[i].wrapped_callback_arg) {
memory_->SystemHeapFree(clients_[i].wrapped_callback_arg);
}
ResetClientState(clients_[i], i);
}
if (xma_context_pool_) {
xma_context_pool_->Shutdown();
xma_context_pool_.reset();
}
worker_wake_event_.reset();
shutdown_event_.reset();
paused_ = false;
kernel_state_ = nullptr;
}
X_STATUS AudioRuntime::RegisterClient(const uint32_t callback, const uint32_t callback_arg,
size_t* out_index) {
if (!out_index || callback == 0) {
return X_E_INVALIDARG;
}
std::lock_guard<std::mutex> lock(mutex_);
for (size_t i = 0; i < clients_.size(); ++i) {
auto& client = clients_[i];
if (client.in_use) {
continue;
}
auto driver = CreateConfiguredDriver(memory_, this, i);
if (!driver || !driver->Initialize()) {
const std::string requested_backend = REXCVAR_GET(audio_backend);
if (driver) {
driver->Shutdown();
}
if (requested_backend != "sdl") {
driver = CreateFallbackDriver(memory_, this, i);
if (driver && driver->Initialize()) {
REXAPU_WARN("AudioRuntime falling back from backend={} to backend={}",
requested_backend, driver->backend_name());
} else {
if (driver) {
driver->Shutdown();
}
return X_STATUS_UNSUCCESSFUL;
}
} else {
return X_STATUS_UNSUCCESSFUL;
}
}
const uint32_t wrapped_callback_arg = memory_->SystemHeapAlloc(0x4);
memory::store_and_swap<uint32_t>(memory_->TranslateVirtual(wrapped_callback_arg), callback_arg);
client = AudioClientState{};
client.in_use = true;
client.client_index = i;
client.callback = callback;
client.callback_arg = callback_arg;
client.wrapped_callback_arg = wrapped_callback_arg;
client.driver = driver.release();
backend_name_ = client.driver->backend_name();
client.clock.Reset();
client.telemetry = {};
trace_buffer_.Record(AudioTraceSubsystem::kCore, AudioTraceEventType::kClientRegistered,
static_cast<uint32_t>(i), callback, callback_arg);
*out_index = i;
// Wake the worker so it can start dispatching callbacks for this client
if (worker_wake_event_) {
worker_wake_event_->Set();
}
return X_STATUS_SUCCESS;
}
return X_STATUS_NO_MEMORY;
}
bool AudioRuntime::UnregisterClient(const size_t index) {
std::lock_guard<std::mutex> lock(mutex_);
if (index >= clients_.size() || !clients_[index].in_use) {
return false;
}
if (clients_[index].driver) {
clients_[index].driver->Shutdown();
delete clients_[index].driver;
}
if (clients_[index].wrapped_callback_arg) {
memory_->SystemHeapFree(clients_[index].wrapped_callback_arg);
}
trace_buffer_.Record(AudioTraceSubsystem::kCore, AudioTraceEventType::kClientUnregistered,
static_cast<uint32_t>(index));
ResetClientState(clients_[index], index);
return true;
}
bool AudioRuntime::SubmitFrame(const size_t index, const uint32_t samples_ptr) {
std::lock_guard<std::mutex> lock(mutex_);
if (index >= clients_.size() || !clients_[index].in_use || !clients_[index].driver) {
return false;
}
auto& client = clients_[index];
if (!samples_ptr) {
++client.telemetry.malformed_frames;
trace_buffer_.Record(AudioTraceSubsystem::kCore, AudioTraceEventType::kMalformedFrame,
static_cast<uint32_t>(index));
return false;
}
AudioFrame frame;
frame.source_client_id = static_cast<uint32_t>(index);
frame.sequence_number = client.next_sequence_number++;
frame.guest_submit_ptr = samples_ptr;
const auto* guest_words = memory_->TranslateVirtual<uint32_t*>(samples_ptr);
if (!guest_words) {
++client.telemetry.malformed_frames;
trace_buffer_.Record(AudioTraceSubsystem::kCore, AudioTraceEventType::kMalformedFrame,
static_cast<uint32_t>(index), samples_ptr);
return false;
}
std::memcpy(frame.guest_frame_words.data(), guest_words,
sizeof(frame.guest_frame_words));
// Keep the runtime queue as bounded shadow state for telemetry/tracing only.
// The active host driver still owns actual buffering and must not be blocked
// by runtime metadata overflow.
if (client.queued_frames.size() >= QueueDepthLimit()) {
++client.telemetry.dropped_frames;
trace_buffer_.Record(AudioTraceSubsystem::kCore, AudioTraceEventType::kFrameDropped,
static_cast<uint32_t>(index), samples_ptr,
static_cast<uint32_t>(client.queued_frames.size()));
client.queued_frames.pop_front();
}
client.queued_frames.push_back(frame);
++client.telemetry.submitted_frames;
client.telemetry.queued_depth = static_cast<uint32_t>(client.queued_frames.size());
client.telemetry.peak_queued_depth =
std::max(client.telemetry.peak_queued_depth, client.telemetry.queued_depth);
client.telemetry.last_submit_ticks = static_cast<uint64_t>(NextTickLocked());
peak_queued_frames_ = std::max(peak_queued_frames_, client.telemetry.queued_depth);
trace_buffer_.Record(AudioTraceSubsystem::kCore, AudioTraceEventType::kFrameSubmitted,
static_cast<uint32_t>(index), samples_ptr, client.telemetry.queued_depth,
static_cast<uint32_t>(frame.sequence_number));
client.driver->SubmitFrame(samples_ptr);
return true;
}
bool AudioRuntime::SubmitSilenceFrame(const size_t index) {
std::lock_guard<std::mutex> lock(mutex_);
if (index >= clients_.size() || !clients_[index].in_use || !clients_[index].driver) {
return false;
}
auto& client = clients_[index];
AudioFrame frame;
frame.source_client_id = static_cast<uint32_t>(index);
frame.sequence_number = client.next_sequence_number++;
frame.guest_submit_ptr = 0;
frame.is_silence = true;
if (client.queued_frames.size() >= QueueDepthLimit()) {
++client.telemetry.dropped_frames;
trace_buffer_.Record(AudioTraceSubsystem::kCore, AudioTraceEventType::kFrameDropped,
static_cast<uint32_t>(index), 0,
static_cast<uint32_t>(client.queued_frames.size()));
client.queued_frames.pop_front();
}
client.queued_frames.push_back(frame);
++client.telemetry.submitted_frames;
++client.telemetry.silence_injections;
client.telemetry.queued_depth = static_cast<uint32_t>(client.queued_frames.size());
client.telemetry.peak_queued_depth =
std::max(client.telemetry.peak_queued_depth, client.telemetry.queued_depth);
client.telemetry.last_submit_ticks = static_cast<uint64_t>(NextTickLocked());
peak_queued_frames_ = std::max(peak_queued_frames_, client.telemetry.queued_depth);
trace_buffer_.Record(AudioTraceSubsystem::kCore, AudioTraceEventType::kFrameSubmitted,
static_cast<uint32_t>(index), 0, client.telemetry.queued_depth,
static_cast<uint32_t>(frame.sequence_number));
client.driver->SubmitSilenceFrame();
return true;
}
bool AudioRuntime::ConsumeNextFrameForClient(const size_t index, AudioFrame* out_frame) {
std::lock_guard<std::mutex> lock(mutex_);
if (index >= clients_.size() || !clients_[index].in_use) {
return false;
}
auto& client = clients_[index];
if (client.queued_frames.empty()) {
client.telemetry.queued_depth = 0;
++client.telemetry.underrun_count;
++client.telemetry.silence_injections;
return false;
}
AudioFrame frame = client.queued_frames.front();
client.queued_frames.pop_front();
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,
client.telemetry.queued_depth,
static_cast<uint32_t>(frame.sequence_number));
if (out_frame) {
*out_frame = std::move(frame);
}
return true;
}
void AudioRuntime::ReportSamplesConsumedForClient(const size_t index,
const uint32_t sample_count) {
if (!sample_count) {
return;
}
std::lock_guard<std::mutex> lock(mutex_);
if (index >= clients_.size() || !clients_[index].in_use) {
return;
}
auto& client = clients_[index];
client.telemetry.last_consume_ticks = static_cast<uint64_t>(NextTickLocked());
client.clock.AdvanceSamples(sample_count);
client.telemetry.consumed_frames =
static_cast<uint32_t>(std::min<uint64_t>(client.clock.consumed_frames(),
std::numeric_limits<uint32_t>::max()));
}
bool AudioRuntime::ShouldRequestCallbackForClient(const size_t index) const {
std::lock_guard<std::mutex> lock(mutex_);
if (index >= clients_.size() || !clients_[index].in_use) {
return false;
}
return clients_[index].queued_frames.size() <= EffectiveCallbackLowWaterFrames(clients_[index]);
}
AudioDriverTelemetry AudioRuntime::GetClientTelemetry(const size_t index) const {
std::lock_guard<std::mutex> lock(mutex_);
if (index >= clients_.size() || !clients_[index].in_use) {
return {};
}
return MergeDriverTelemetry(clients_[index]);
}
uint32_t AudioRuntime::GetClientRenderDriverTic(const size_t index) const {
std::lock_guard<std::mutex> lock(mutex_);
if (index >= clients_.size() || !clients_[index].in_use) {
return 0;
}
return ComputeRenderDriverTic(clients_[index]);
}
AudioClientTimingSnapshot AudioRuntime::GetClientTimingSnapshot(const size_t index) const {
std::lock_guard<std::mutex> lock(mutex_);
if (index >= clients_.size() || !clients_[index].in_use) {
return {};
}
return BuildTimingSnapshot(clients_[index]);
}
AudioTelemetrySnapshot AudioRuntime::GetTelemetrySnapshot() const {
std::lock_guard<std::mutex> lock(mutex_);
AudioTelemetrySnapshot snapshot;
snapshot.backend_name = backend_name();
for (size_t i = 0; i < clients_.size(); ++i) {
const auto& client = clients_[i];
auto& client_snapshot = snapshot.clients[i];
client_snapshot.in_use = client.in_use;
client_snapshot.callback = client.callback;
client_snapshot.callback_arg = client.callback_arg;
client_snapshot.telemetry = MergeDriverTelemetry(client);
client_snapshot.render_driver_tic = ComputeRenderDriverTic(client);
if (!client.in_use) {
continue;
}
++snapshot.active_clients;
snapshot.queued_frames += client_snapshot.telemetry.queued_depth;
snapshot.peak_queued_frames =
std::max(snapshot.peak_queued_frames, client_snapshot.telemetry.peak_queued_depth);
snapshot.dropped_frames += client_snapshot.telemetry.dropped_frames;
snapshot.underruns += client_snapshot.telemetry.underrun_count;
snapshot.silence_injections += client_snapshot.telemetry.silence_injections;
}
snapshot.peak_queued_frames =
std::max(snapshot.peak_queued_frames, peak_queued_frames_);
snapshot.trace_event_count = trace_buffer_.size();
return snapshot;
}
bool AudioRuntime::Save(stream::ByteStream* stream) {
if (!stream) {
return false;
}
std::lock_guard<std::mutex> lock(mutex_);
stream->Write(static_cast<uint32_t>(paused_ ? 1 : 0));
for (size_t i = 0; i < clients_.size(); ++i) {
const auto& client = clients_[i];
stream->Write(static_cast<uint32_t>(client.in_use ? 1 : 0));
if (!client.in_use) {
continue;
}
stream->Write(static_cast<uint32_t>(client.client_index));
stream->Write(client.callback);
stream->Write(client.callback_arg);
stream->Write(client.wrapped_callback_arg);
}
return xma_context_pool_ ? xma_context_pool_->Save(stream) : true;
}
bool AudioRuntime::Restore(stream::ByteStream* stream) {
if (!stream) {
return false;
}
paused_ = stream->Read<uint32_t>() != 0;
for (size_t i = 0; i < clients_.size(); ++i) {
const bool in_use = stream->Read<uint32_t>() != 0;
if (!in_use) {
continue;
}
size_t index = stream->Read<uint32_t>();
uint32_t callback = stream->Read<uint32_t>();
uint32_t callback_arg = stream->Read<uint32_t>();
uint32_t wrapped_callback_arg = stream->Read<uint32_t>();
auto driver = CreateConfiguredDriver(memory_, this, index);
if (!driver || !driver->Initialize()) {
const std::string requested_backend = REXCVAR_GET(audio_backend);
if (driver) {
driver->Shutdown();
}
if (requested_backend != "sdl") {
driver = CreateFallbackDriver(memory_, this, index);
if (!driver || !driver->Initialize()) {
if (driver) {
driver->Shutdown();
}
return false;
}
} else {
return false;
}
}
auto& client = clients_[index];
client.in_use = true;
client.client_index = index;
client.callback = callback;
client.callback_arg = callback_arg;
client.wrapped_callback_arg = wrapped_callback_arg;
client.driver = driver.release();
backend_name_ = client.driver->backend_name();
client.clock.Reset();
client.telemetry = {};
}
return xma_context_pool_ ? xma_context_pool_->Restore(stream) : true;
}
void AudioRuntime::Pause() {
std::lock_guard<std::mutex> lock(mutex_);
paused_ = true;
}
void AudioRuntime::Resume() {
std::lock_guard<std::mutex> lock(mutex_);
if (!paused_) {
return;
}
paused_ = false;
if (worker_wake_event_) {
worker_wake_event_->Set();
}
}
bool AudioRuntime::is_paused() const {
return paused_;
}
size_t AudioRuntime::ConsumeQueuedFramesForClient(const size_t index, const size_t max_frames) {
std::lock_guard<std::mutex> lock(mutex_);
if (index >= clients_.size() || !clients_[index].in_use) {
return 0;
}
auto& client = clients_[index];
size_t consumed = 0;
while (consumed < max_frames && !client.queued_frames.empty()) {
const AudioFrame frame = client.queued_frames.front();
client.queued_frames.pop_front();
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 =
static_cast<uint32_t>(std::min<uint64_t>(client.clock.consumed_frames(),
std::numeric_limits<uint32_t>::max()));
trace_buffer_.Record(AudioTraceSubsystem::kCore, AudioTraceEventType::kFrameConsumed,
static_cast<uint32_t>(index), frame.guest_submit_ptr,
client.telemetry.queued_depth,
static_cast<uint32_t>(frame.sequence_number));
++consumed;
}
return consumed;
}
size_t AudioRuntime::ConsumeAllAvailableFrames() {
size_t consumed = 0;
for (size_t i = 0; i < clients_.size(); ++i) {
consumed += ConsumeQueuedFramesForClient(i, std::numeric_limits<size_t>::max());
}
return consumed;
}
xma::XmaContextPool& AudioRuntime::xma_context_pool() {
return *xma_context_pool_;
}
const xma::XmaContextPool& AudioRuntime::xma_context_pool() const {
return *xma_context_pool_;
}
std::string AudioRuntime::backend_name() const {
return backend_name_;
}
void AudioRuntime::WorkerThreadMain() {
REXAPU_INFO("AudioRuntime worker started: backend={} scheduling=queue-depth-driven "
"low_water={} target={} max={}",
backend_name_, CallbackLowWaterFrames(), CallbackTargetQueueDepth(),
QueueDepthLimit());
rex::thread::WaitHandle* wait_handles[2]{};
wait_handles[0] = worker_wake_event_.get();
wait_handles[1] = shutdown_event_.get();
while (worker_running_.load(std::memory_order_acquire)) {
// Wait for: host driver wake, shutdown, or 5ms timeout
rex::thread::WaitAny(wait_handles, 2, true, std::chrono::milliseconds(5));
if (!worker_running_.load(std::memory_order_acquire)) {
break;
}
++worker_iteration_count_;
const bool startup_trace = worker_iteration_count_ <= 60;
// Skip dispatch while paused
{
std::lock_guard<std::mutex> lock(mutex_);
if (paused_) {
continue;
}
}
// Check each active client and dispatch callbacks to fill queue to target
for (size_t i = 0; i < clients_.size(); ++i) {
// Dispatch up to (target - current) callbacks per client per iteration
// to fill the queue towards the target depth
uint32_t dispatch = 0;
while (true) {
uint32_t client_callback = 0;
uint32_t client_callback_arg = 0;
bool needs_callback = false;
size_t current_depth = 0;
uint32_t target = 0;
{
std::lock_guard<std::mutex> lock(mutex_);
if (!clients_[i].in_use) {
break;
}
target = EffectiveCallbackTargetQueueDepth(clients_[i]);
if (dispatch >= target) {
break;
}
current_depth = clients_[i].queued_frames.size();
needs_callback = current_depth <= EffectiveCallbackLowWaterFrames(clients_[i]);
if (needs_callback) {
const auto now = AudioClock::clock_type::now();
client_callback = clients_[i].callback;
client_callback_arg = clients_[i].wrapped_callback_arg;
if (clients_[i].first_callback_dispatch_time.time_since_epoch().count() == 0) {
clients_[i].first_callback_dispatch_time = now;
}
clients_[i].last_callback_dispatch_time = now;
++clients_[i].telemetry.callback_dispatch_count;
clients_[i].telemetry.last_callback_request_ticks =
static_cast<uint64_t>(NextTickLocked());
}
}
if (!needs_callback) {
break;
}
if (!client_callback || !function_dispatcher_ || !worker_thread_) {
break;
}
if (startup_trace) {
REXAPU_INFO(
"AudioRuntime dispatch: iter={} client={} depth={} dispatch_round={} "
"callback_count={}",
worker_iteration_count_, i, current_depth, dispatch,
GetClientTelemetry(i).callback_dispatch_count);
}
uint64_t args[] = {client_callback_arg};
function_dispatcher_->Execute(worker_thread_->thread_state(), client_callback, args,
rex::countof(args));
++dispatch;
}
}
// Startup telemetry - first 60 iterations at INFO level
if (startup_trace && (worker_iteration_count_ % 10) == 0) {
std::lock_guard<std::mutex> lock(mutex_);
for (size_t i = 0; i < clients_.size(); ++i) {
if (!clients_[i].in_use) continue;
const auto& c = clients_[i];
REXAPU_INFO(
"AudioRuntime startup: iter={} client={} queued={} target={} low_water={} "
"submitted={} consumed={} underruns={} callbacks={} tic={} 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.peak_queued_depth,
c.clock.drift_ms());
}
}
// Periodic telemetry - every ~3 seconds at DEBUG level
if (!startup_trace && (worker_iteration_count_ % 600) == 0) {
std::lock_guard<std::mutex> lock(mutex_);
for (size_t i = 0; i < clients_.size(); ++i) {
if (!clients_[i].in_use) continue;
const auto& c = clients_[i];
REXAPU_DEBUG(
"AudioRuntime periodic: client={} queued={} target={} low_water={} submitted={} "
"consumed={} underruns={} callbacks={} tic={} 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.callback_dispatch_count,
ComputeRenderDriverTic(c), c.telemetry.peak_queued_depth,
c.clock.drift_ms());
}
}
}
REXAPU_INFO("AudioRuntime worker stopped after {} iterations", worker_iteration_count_);
}
uint64_t AudioRuntime::NextTickLocked() {
return ++tick_counter_;
}
void AudioRuntime::PumpBackendIfNeeded() {}
void AudioRuntime::WakeWorker() {
if (worker_wake_event_) {
worker_wake_event_->Set();
}
}
} // namespace rex::audio
@@ -1,136 +0,0 @@
/**
******************************************************************************
* Xenia : Xbox 360 Emulator Research Project *
******************************************************************************
* Copyright 2026 Tom Clay. All rights reserved. *
* Released under the BSD license - see LICENSE in the root for more details. *
******************************************************************************
*/
#pragma once
#include <atomic>
#include <array>
#include <cstddef>
#include <cstdint>
#include <memory>
#include <mutex>
#include <string>
#include <rex/audio/audio_client.h>
#include <rex/audio/audio_trace.h>
#include <rex/audio/host/host_audio_backend.h>
#include <rex/memory.h>
#include <rex/system/function_dispatcher.h>
#include <rex/system/xthread.h>
#include <rex/thread.h>
namespace rex::stream {
class ByteStream;
}
namespace rex::system {
class KernelState;
}
namespace rex::audio::xma {
class XmaContextPool;
}
namespace rex::audio {
struct AudioClientSnapshot {
bool in_use{false};
uint32_t callback{0};
uint32_t callback_arg{0};
uint32_t render_driver_tic{0};
AudioDriverTelemetry telemetry{};
};
struct AudioTelemetrySnapshot {
uint32_t active_clients{0};
uint32_t queued_frames{0};
uint32_t peak_queued_frames{0};
uint32_t dropped_frames{0};
uint32_t underruns{0};
uint32_t silence_injections{0};
uint64_t trace_event_count{0};
std::string backend_name;
std::array<AudioClientSnapshot, kMaximumAudioClientCount> clients{};
};
struct AudioClientTimingSnapshot {
uint64_t consumed_samples{0};
uint64_t consumed_frames{0};
uint64_t callback_floor_tic{0};
uint64_t host_elapsed_tic{0};
uint32_t render_driver_tic{0};
uint32_t callback_dispatch_count{0};
};
class AudioRuntime {
public:
AudioRuntime(memory::Memory* memory, runtime::FunctionDispatcher* function_dispatcher);
~AudioRuntime();
X_STATUS Setup(system::KernelState* kernel_state);
void Shutdown();
X_STATUS RegisterClient(uint32_t callback, uint32_t callback_arg, size_t* out_index);
bool UnregisterClient(size_t index);
bool SubmitFrame(size_t index, uint32_t samples_ptr);
bool SubmitSilenceFrame(size_t index);
bool ConsumeNextFrameForClient(size_t index, AudioFrame* out_frame);
void ReportSamplesConsumedForClient(size_t index, uint32_t sample_count);
bool ShouldRequestCallbackForClient(size_t index) const;
AudioDriverTelemetry GetClientTelemetry(size_t index) const;
uint32_t GetClientRenderDriverTic(size_t index) const;
AudioClientTimingSnapshot GetClientTimingSnapshot(size_t index) const;
AudioTelemetrySnapshot GetTelemetrySnapshot() const;
bool Save(stream::ByteStream* stream);
bool Restore(stream::ByteStream* stream);
void Pause();
void Resume();
bool is_paused() const;
size_t ConsumeQueuedFramesForClient(size_t index, size_t max_frames);
size_t ConsumeAllAvailableFrames();
xma::XmaContextPool& xma_context_pool();
const xma::XmaContextPool& xma_context_pool() const;
AudioTraceBuffer& trace_buffer() { return trace_buffer_; }
const AudioTraceBuffer& trace_buffer() const { return trace_buffer_; }
std::string backend_name() const;
/// Wake the audio worker thread to re-evaluate callback demand.
/// Called by the host backend when it consumes frames or detects underruns.
void WakeWorker();
private:
void WorkerThreadMain();
uint64_t NextTickLocked();
void PumpBackendIfNeeded();
mutable std::mutex mutex_;
memory::Memory* memory_{nullptr};
runtime::FunctionDispatcher* function_dispatcher_{nullptr};
system::KernelState* kernel_state_{nullptr};
std::array<AudioClientState, kMaximumAudioClientCount> clients_{};
std::unique_ptr<host::IHostAudioBackend> host_backend_;
std::unique_ptr<xma::XmaContextPool> xma_context_pool_;
AudioTraceBuffer trace_buffer_;
bool paused_{false};
std::atomic<bool> worker_running_{false};
system::object_ref<system::XHostThread> worker_thread_;
std::unique_ptr<rex::thread::Event> worker_wake_event_;
std::unique_ptr<rex::thread::Event> shutdown_event_;
uint64_t tick_counter_{0};
uint32_t peak_queued_frames_{0};
uint64_t worker_iteration_count_{0};
std::string backend_name_{"none"};
};
} // namespace rex::audio
@@ -1,154 +0,0 @@
/**
******************************************************************************
* Xenia : Xbox 360 Emulator Research Project *
******************************************************************************
* Copyright 2013 Ben Vanik. All rights reserved. *
* Released under the BSD license - see LICENSE in the root for more details. *
******************************************************************************
*
* @modified Tom Clay, 2026 - Adapted for ReXGlue runtime
*/
#include <rex/audio/audio_system.h>
#include <rex/audio/xma/decoder.h>
#include <rex/cvar.h>
#include <rex/logging.h>
#include <rex/system/kernel_state.h>
REXCVAR_DEFINE_BOOL(audio_mute, false, "Audio", "Mute host audio output");
REXCVAR_DEFINE_BOOL(ffmpeg_verbose, false, "Audio",
"Legacy placeholder while the audio stack is stubbed");
REXCVAR_DEFINE_BOOL(audio_trace_telemetry, false, "Audio",
"Trace audio runtime telemetry");
REXCVAR_DEFINE_BOOL(audio_trace_render_driver_verbose, false, "Audio",
"Trace render-driver activity");
namespace rex::audio {
std::unique_ptr<AudioSystem> AudioSystem::Create(
runtime::FunctionDispatcher* function_dispatcher) {
return std::unique_ptr<AudioSystem>(new AudioSystem(function_dispatcher));
}
AudioSystem::AudioSystem(runtime::FunctionDispatcher* function_dispatcher)
: function_dispatcher_(function_dispatcher),
xma_decoder_(std::make_unique<XmaDecoder>(function_dispatcher)) {}
AudioSystem::~AudioSystem() {
Shutdown();
}
X_STATUS AudioSystem::Setup(system::KernelState* kernel_state) {
memory_ = kernel_state ? kernel_state->memory() : nullptr;
if (xma_decoder_) {
const X_STATUS xma_status = xma_decoder_->Setup(kernel_state);
if (XFAILED(xma_status)) {
return xma_status;
}
}
runtime_ = std::make_unique<AudioRuntime>(memory_, function_dispatcher_);
const X_STATUS runtime_status = runtime_->Setup(kernel_state);
if (XFAILED(runtime_status) && xma_decoder_) {
xma_decoder_->Shutdown();
return runtime_status;
}
if (!XFAILED(runtime_status)) {
REXAPU_INFO(
"AudioSystem setup complete: runtime=AudioRuntime backend={} trace_telemetry={} "
"trace_verbose={} mute={}",
runtime_->backend_name(), REXCVAR_GET(audio_trace_telemetry),
REXCVAR_GET(audio_trace_render_driver_verbose), REXCVAR_GET(audio_mute));
}
return runtime_status;
}
void AudioSystem::Shutdown() {
if (runtime_) {
runtime_->Shutdown();
runtime_.reset();
}
if (xma_decoder_) {
xma_decoder_->Shutdown();
}
}
X_STATUS AudioSystem::RegisterClient(const uint32_t callback, const uint32_t callback_arg,
size_t* out_index) {
return runtime_ ? runtime_->RegisterClient(callback, callback_arg, out_index) : X_E_FAIL;
}
void AudioSystem::UnregisterClient(const size_t index) {
if (runtime_) {
runtime_->UnregisterClient(index);
}
}
void AudioSystem::SubmitFrame(const size_t index, const uint32_t samples_ptr) {
if (runtime_) {
runtime_->SubmitFrame(index, samples_ptr);
}
}
void AudioSystem::SubmitSilenceFrame(const size_t index) {
if (runtime_) {
runtime_->SubmitSilenceFrame(index);
}
}
AudioDriverTelemetry AudioSystem::GetClientTelemetry(const size_t index) {
return runtime_ ? runtime_->GetClientTelemetry(index) : AudioDriverTelemetry{};
}
uint32_t AudioSystem::GetClientRenderDriverTic(const size_t index) {
return runtime_ ? runtime_->GetClientRenderDriverTic(index) : 0;
}
AudioClientTimingSnapshot AudioSystem::GetClientTimingSnapshot(const size_t index) {
return runtime_ ? runtime_->GetClientTimingSnapshot(index) : AudioClientTimingSnapshot{};
}
AudioTelemetrySnapshot AudioSystem::GetTelemetrySnapshot() const {
return runtime_ ? runtime_->GetTelemetrySnapshot() : AudioTelemetrySnapshot{};
}
const AudioTraceBuffer& AudioSystem::trace_buffer() const {
static const AudioTraceBuffer empty_trace;
return runtime_ ? runtime_->trace_buffer() : empty_trace;
}
std::string AudioSystem::GetBackendName() const {
return runtime_ ? runtime_->backend_name() : "none";
}
bool AudioSystem::Save(stream::ByteStream* stream) {
return runtime_ ? runtime_->Save(stream) : false;
}
bool AudioSystem::Restore(stream::ByteStream* stream) {
return runtime_ ? runtime_->Restore(stream) : false;
}
void AudioSystem::Pause() {
if (runtime_) {
runtime_->Pause();
}
if (xma_decoder_) {
xma_decoder_->Pause();
}
}
void AudioSystem::Resume() {
if (runtime_) {
runtime_->Resume();
}
if (xma_decoder_) {
xma_decoder_->Resume();
}
}
bool AudioSystem::is_paused() const {
return runtime_ ? runtime_->is_paused() : false;
}
} // namespace rex::audio
@@ -1,75 +0,0 @@
/**
******************************************************************************
* Xenia : Xbox 360 Emulator Research Project *
******************************************************************************
* Copyright 2013 Ben Vanik. All rights reserved. *
* Released under the BSD license - see LICENSE in the root for more details. *
******************************************************************************
*
* @modified Tom Clay, 2026 - Adapted for ReXGlue runtime
*/
#pragma once
#include <cstddef>
#include <cstdint>
#include <memory>
#include <rex/audio/audio_driver.h>
#include <rex/audio/audio_runtime.h>
#include <rex/kernel.h>
#include <rex/system/function_dispatcher.h>
#include <rex/system/interfaces/audio.h>
namespace rex::stream {
class ByteStream;
} // namespace rex::stream
namespace rex::audio {
constexpr memory::fourcc_t kAudioSaveSignature = memory::make_fourcc("XAUD");
class XmaDecoder;
class AudioSystem : public system::IAudioSystem {
public:
static std::unique_ptr<AudioSystem> Create(runtime::FunctionDispatcher* function_dispatcher);
virtual ~AudioSystem();
memory::Memory* memory() const { return memory_; }
runtime::FunctionDispatcher* function_dispatcher() const { return function_dispatcher_; }
AudioRuntime* runtime() const { return runtime_.get(); }
XmaDecoder* xma_decoder() const { return xma_decoder_.get(); }
X_STATUS Setup(system::KernelState* kernel_state) override;
void Shutdown() override;
X_STATUS RegisterClient(uint32_t callback, uint32_t callback_arg, size_t* out_index);
void UnregisterClient(size_t index);
void SubmitFrame(size_t index, uint32_t samples_ptr);
void SubmitSilenceFrame(size_t index);
AudioDriverTelemetry GetClientTelemetry(size_t index);
uint32_t GetClientRenderDriverTic(size_t index);
AudioClientTimingSnapshot GetClientTimingSnapshot(size_t index);
AudioTelemetrySnapshot GetTelemetrySnapshot() const;
const AudioTraceBuffer& trace_buffer() const;
std::string GetBackendName() const;
bool Save(stream::ByteStream* stream);
bool Restore(stream::ByteStream* stream);
bool is_paused() const;
void Pause();
void Resume();
protected:
explicit AudioSystem(runtime::FunctionDispatcher* function_dispatcher);
memory::Memory* memory_ = nullptr;
runtime::FunctionDispatcher* function_dispatcher_ = nullptr;
std::unique_ptr<AudioRuntime> runtime_;
std::unique_ptr<XmaDecoder> xma_decoder_;
};
} // namespace rex::audio
@@ -1,53 +0,0 @@
/**
******************************************************************************
* Xenia : Xbox 360 Emulator Research Project *
******************************************************************************
* Copyright 2026 Tom Clay. All rights reserved. *
* Released under the BSD license - see LICENSE in the root for more details. *
******************************************************************************
*/
#include <chrono>
#include <rex/audio/audio_trace.h>
namespace rex::audio {
namespace {
uint64_t CurrentTimestampUs() {
const auto now = std::chrono::steady_clock::now().time_since_epoch();
return static_cast<uint64_t>(
std::chrono::duration_cast<std::chrono::microseconds>(now).count());
}
} // namespace
void AudioTraceBuffer::Reset() {
std::lock_guard<std::mutex> lock(mutex_);
events_.clear();
}
void AudioTraceBuffer::Record(const AudioTraceSubsystem subsystem,
const AudioTraceEventType event_type, const uint32_t client_id,
const uint32_t value_0, const uint32_t value_1,
const uint32_t value_2) {
std::lock_guard<std::mutex> lock(mutex_);
if (events_.size() >= kMaximumTraceEventCount) {
events_.pop_front();
}
events_.push_back(
{CurrentTimestampUs(), subsystem, event_type, client_id, value_0, value_1, value_2});
}
size_t AudioTraceBuffer::size() const {
std::lock_guard<std::mutex> lock(mutex_);
return events_.size();
}
std::vector<AudioTraceEvent> AudioTraceBuffer::Snapshot() const {
std::lock_guard<std::mutex> lock(mutex_);
return {events_.begin(), events_.end()};
}
} // namespace rex::audio
@@ -1,65 +0,0 @@
/**
******************************************************************************
* Xenia : Xbox 360 Emulator Research Project *
******************************************************************************
* Copyright 2026 Tom Clay. All rights reserved. *
* Released under the BSD license - see LICENSE in the root for more details. *
******************************************************************************
*/
#pragma once
#include <cstddef>
#include <cstdint>
#include <deque>
#include <mutex>
#include <vector>
namespace rex::audio {
enum class AudioTraceSubsystem : uint8_t {
kCore = 0,
kKernel = 1,
kHost = 2,
kXma = 3,
};
enum class AudioTraceEventType : uint8_t {
kClientRegistered = 0,
kClientUnregistered = 1,
kFrameSubmitted = 2,
kFrameConsumed = 3,
kFrameDropped = 4,
kMalformedFrame = 5,
kXmaAllocated = 6,
kXmaReleased = 7,
kXmaStateUpdated = 8,
};
struct AudioTraceEvent {
uint64_t timestamp_us{0};
AudioTraceSubsystem subsystem{AudioTraceSubsystem::kCore};
AudioTraceEventType event_type{AudioTraceEventType::kClientRegistered};
uint32_t client_id{0};
uint32_t value_0{0};
uint32_t value_1{0};
uint32_t value_2{0};
};
class AudioTraceBuffer {
public:
void Reset();
void Record(AudioTraceSubsystem subsystem, AudioTraceEventType event_type, uint32_t client_id,
uint32_t value_0 = 0, uint32_t value_1 = 0, uint32_t value_2 = 0);
size_t size() const;
std::vector<AudioTraceEvent> Snapshot() const;
private:
static constexpr size_t kMaximumTraceEventCount = 2048;
mutable std::mutex mutex_;
std::deque<AudioTraceEvent> events_;
};
} // namespace rex::audio
@@ -1,103 +0,0 @@
/**
******************************************************************************
* Xenia : Xbox 360 Emulator Research Project *
******************************************************************************
* Copyright 2021 Ben Vanik. All rights reserved. *
* Released under the BSD license - see LICENSE in the root for more details. *
******************************************************************************
*
* @modified Tom Clay, 2026 - Adapted for ReXGlue runtime
*/
#pragma once
#include <cstdint>
#include <rex/platform.h>
#include <rex/types.h>
namespace rex::audio::conversion {
#if REX_ARCH_AMD64
inline void sequential_6_BE_to_interleaved_6_LE(float* output, const float* input,
size_t ch_sample_count) {
const uint32_t* in = reinterpret_cast<const uint32_t*>(input);
uint32_t* out = reinterpret_cast<uint32_t*>(output);
const __m128i byte_swap_shuffle =
_mm_set_epi8(12, 13, 14, 15, 8, 9, 10, 11, 4, 5, 6, 7, 0, 1, 2, 3);
for (size_t sample = 0; sample < ch_sample_count; sample++) {
__m128i sample0 =
_mm_set_epi32(in[3 * ch_sample_count + sample], in[2 * ch_sample_count + sample],
in[1 * ch_sample_count + sample], in[0 * ch_sample_count + sample]);
uint32_t sample1 = in[4 * ch_sample_count + sample];
uint32_t sample2 = in[5 * ch_sample_count + sample];
sample0 = _mm_shuffle_epi8(sample0, byte_swap_shuffle);
_mm_storeu_si128(reinterpret_cast<__m128i*>(&out[sample * 6]), sample0);
sample1 = rex::byte_swap(sample1);
out[sample * 6 + 4] = sample1;
sample2 = rex::byte_swap(sample2);
out[sample * 6 + 5] = sample2;
}
}
inline void sequential_6_BE_to_interleaved_2_LE(float* output, const float* input,
size_t ch_sample_count) {
assert_true(ch_sample_count % 4 == 0);
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);
// put center on left and right, discard low frequency
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 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));
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));
__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));
}
}
#else
inline void sequential_6_BE_to_interleaved_6_LE(float* output, const float* input,
size_t ch_sample_count) {
for (size_t sample = 0; sample < ch_sample_count; sample++) {
for (size_t channel = 0; channel < 6; channel++) {
output[sample * 6 + channel] = rex::byte_swap(input[channel * ch_sample_count + sample]);
}
}
}
inline void sequential_6_BE_to_interleaved_2_LE(float* output, const float* input,
size_t ch_sample_count) {
// 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);
}
}
#endif
} // namespace rex::audio::conversion
@@ -1,17 +0,0 @@
/**
******************************************************************************
* Xenia : Xbox 360 Emulator Research Project *
******************************************************************************
* Copyright 2013 Ben Vanik. All rights reserved. *
* Released under the BSD license - see LICENSE in the root for more details. *
******************************************************************************
*
* @modified Tom Clay, 2026 - Adapted for ReXGlue runtime
*/
#pragma once
#include <rex/cvar.h>
REXCVAR_DECLARE(bool, audio_mute);
REXCVAR_DECLARE(bool, ffmpeg_verbose);
@@ -1,35 +0,0 @@
/**
******************************************************************************
* Xenia : Xbox 360 Emulator Research Project *
******************************************************************************
* Copyright 2026 Tom Clay. All rights reserved. *
* Released under the BSD license - see LICENSE in the root for more details. *
******************************************************************************
*/
#pragma once
#include <memory>
#include <string_view>
#include <rex/system/xtypes.h>
namespace rex::audio {
class AudioRuntime;
}
namespace rex::audio::host {
class IHostAudioBackend {
public:
virtual ~IHostAudioBackend() = default;
virtual X_STATUS Setup() = 0;
virtual void Shutdown() = 0;
virtual std::string_view backend_name() const = 0;
virtual void Pump(AudioRuntime& runtime) = 0;
};
std::unique_ptr<IHostAudioBackend> CreateNullAudioBackend();
} // namespace rex::audio::host
@@ -1,31 +0,0 @@
/**
******************************************************************************
* Xenia : Xbox 360 Emulator Research Project *
******************************************************************************
* Copyright 2026 Tom Clay. All rights reserved. *
* Released under the BSD license - see LICENSE in the root for more details. *
******************************************************************************
*/
#include <rex/audio/audio_runtime.h>
#include <rex/audio/host/host_audio_backend.h>
namespace rex::audio::host {
namespace {
class NullAudioBackend final : public IHostAudioBackend {
public:
X_STATUS Setup() override { return X_STATUS_SUCCESS; }
void Shutdown() override {}
std::string_view backend_name() const override { return "null"; }
void Pump(AudioRuntime& runtime) override { runtime.ConsumeAllAvailableFrames(); }
};
} // namespace
std::unique_ptr<IHostAudioBackend> CreateNullAudioBackend() {
return std::make_unique<NullAudioBackend>();
}
} // namespace rex::audio::host
@@ -1,26 +0,0 @@
/**
******************************************************************************
* Xenia : Xbox 360 Emulator Research Project *
******************************************************************************
* Copyright 2013 Ben Vanik. All rights reserved. *
* Released under the BSD license - see LICENSE in the root for more details. *
******************************************************************************
*
* @modified Tom Clay, 2026 - Adapted for ReXGlue runtime
*/
#include <rex/audio/nop/nop_audio_system.h>
namespace rex::audio::nop {
std::unique_ptr<AudioSystem> NopAudioSystem::Create(
runtime::FunctionDispatcher* function_dispatcher) {
return std::make_unique<NopAudioSystem>(function_dispatcher);
}
NopAudioSystem::NopAudioSystem(runtime::FunctionDispatcher* function_dispatcher)
: AudioSystem(function_dispatcher) {}
NopAudioSystem::~NopAudioSystem() = default;
} // namespace rex::audio::nop
@@ -1,30 +0,0 @@
/**
******************************************************************************
* Xenia : Xbox 360 Emulator Research Project *
******************************************************************************
* Copyright 2013 Ben Vanik. All rights reserved. *
* Released under the BSD license - see LICENSE in the root for more details. *
******************************************************************************
*
* @modified Tom Clay, 2026 - Adapted for ReXGlue runtime
*/
#pragma once
#include <memory>
#include <rex/audio/audio_system.h>
namespace rex::audio::nop {
class NopAudioSystem : public AudioSystem {
public:
explicit NopAudioSystem(runtime::FunctionDispatcher* function_dispatcher);
~NopAudioSystem() override;
static bool IsAvailable() { return true; }
static std::unique_ptr<AudioSystem> Create(runtime::FunctionDispatcher* function_dispatcher);
};
} // namespace rex::audio::nop
@@ -1,493 +0,0 @@
/**
******************************************************************************
* Xenia : Xbox 360 Emulator Research Project *
******************************************************************************
* Copyright 2020 Ben Vanik. All rights reserved. *
* Released under the BSD license - see LICENSE in the root for more details. *
******************************************************************************
*
* @modified Tom Clay, 2026 - Adapted for ReXGlue runtime
*/
#include <algorithm>
#include <array>
#include <chrono>
#include <cmath>
#include <cstring>
#include <limits>
#include <string>
#include <rex/assert.h>
#include <rex/audio/conversion.h>
#include <rex/audio/flags.h>
#include <rex/audio/audio_runtime.h>
#include <rex/audio/sdl/sdl_audio_driver.h>
#include <rex/cvar.h>
#include <rex/dbg.h>
#include <rex/logging.h>
#include <SDL3/SDL.h>
REXCVAR_DECLARE(bool, audio_mute);
REXCVAR_DECLARE(bool, audio_trace_telemetry);
REXCVAR_DECLARE(bool, audio_trace_render_driver_verbose);
REXCVAR_DECLARE(bool, ac6_audio_deep_trace);
REXCVAR_DEFINE_INT32(audio_sdl_device_sample_frames, 256, "Audio",
"Requested SDL device buffer size in sample frames for low-latency playback");
namespace rex::audio::sdl {
using Clock = std::chrono::steady_clock;
namespace {
uint32_t RequiredQueueFramesForDevice(const uint32_t device_sample_frames) {
const uint32_t device_frames = std::max(device_sample_frames, 1u);
return std::max(2u, (device_frames + kRenderDriverTicSamplesPerFrame - 1) /
kRenderDriverTicSamplesPerFrame + 1);
}
bool IsDeepTraceEnabled() {
return REXCVAR_GET(ac6_audio_deep_trace);
}
struct OutputChunkStats {
float min_sample = std::numeric_limits<float>::infinity();
float max_sample = -std::numeric_limits<float>::infinity();
double sum_squares = 0.0;
uint32_t sample_count = 0;
uint32_t zeroish_samples = 0;
bool has_nonfinite = false;
};
float ByteSwapFloatWord(uint32_t value) {
value = rex::byte_swap(value);
float result = 0.0f;
std::memcpy(&result, &value, sizeof(result));
return result;
}
OutputChunkStats AnalyzeOutputChunk(const float* data, size_t sample_count) {
OutputChunkStats stats;
for (size_t i = 0; i < sample_count; ++i) {
const float sample = data[i];
if (!std::isfinite(sample)) {
stats.has_nonfinite = true;
continue;
}
stats.min_sample = std::min(stats.min_sample, sample);
stats.max_sample = std::max(stats.max_sample, sample);
stats.sum_squares += static_cast<double>(sample) * static_cast<double>(sample);
++stats.sample_count;
if (std::fabs(sample) <= 1.0e-6f) {
++stats.zeroish_samples;
}
}
if (stats.sample_count == 0) {
stats.min_sample = 0.0f;
stats.max_sample = 0.0f;
}
return stats;
}
OutputChunkStats AnalyzeGuestFrame(const float* data, size_t sample_count) {
OutputChunkStats stats;
const auto* words = reinterpret_cast<const uint32_t*>(data);
for (size_t i = 0; i < sample_count; ++i) {
const float sample = ByteSwapFloatWord(words[i]);
if (!std::isfinite(sample)) {
stats.has_nonfinite = true;
continue;
}
stats.min_sample = std::min(stats.min_sample, sample);
stats.max_sample = std::max(stats.max_sample, sample);
stats.sum_squares += static_cast<double>(sample) * static_cast<double>(sample);
++stats.sample_count;
if (std::fabs(sample) <= 1.0e-6f) {
++stats.zeroish_samples;
}
}
if (stats.sample_count == 0) {
stats.min_sample = 0.0f;
stats.max_sample = 0.0f;
}
return stats;
}
void MergeOutputChunkStats(OutputChunkStats& dst, const OutputChunkStats& src) {
dst.min_sample = std::min(dst.min_sample, src.min_sample);
dst.max_sample = std::max(dst.max_sample, src.max_sample);
dst.sum_squares += src.sum_squares;
dst.sample_count += src.sample_count;
dst.zeroish_samples += src.zeroish_samples;
dst.has_nonfinite = dst.has_nonfinite || src.has_nonfinite;
if (dst.sample_count == 0) {
dst.min_sample = 0.0f;
dst.max_sample = 0.0f;
}
}
} // namespace
SDLAudioDriver::SDLAudioDriver(memory::Memory* memory, AudioRuntime* runtime,
size_t client_index)
: AudioDriver(memory),
runtime_(runtime),
client_index_(client_index) {}
SDLAudioDriver::~SDLAudioDriver() = default;
bool SDLAudioDriver::Initialize() {
SDL_SetHintWithPriority(SDL_HINT_TIMER_RESOLUTION, "0", SDL_HINT_OVERRIDE);
SDL_SetHint(SDL_HINT_AUDIO_CATEGORY, "playback");
const int32_t requested_sample_frames =
std::max(REXCVAR_GET(audio_sdl_device_sample_frames), 1);
const std::string requested_sample_frames_string =
std::to_string(requested_sample_frames);
SDL_SetHint(SDL_HINT_AUDIO_DEVICE_SAMPLE_FRAMES,
requested_sample_frames_string.c_str());
SDL_SetAppMetadataProperty(SDL_PROP_APP_METADATA_NAME_STRING, "rexglue");
if (!SDL_InitSubSystem(SDL_INIT_AUDIO)) {
REXAPU_ERROR("SDL_InitSubSystem(SDL_INIT_AUDIO) failed: {}", SDL_GetError());
return false;
}
sdl_initialized_ = true;
SDL_AudioSpec desired_spec = {};
SDL_AudioSpec obtained_spec;
int obtained_sample_frames = 0;
desired_spec.freq = frame_frequency_;
desired_spec.format = SDL_AUDIO_F32LE;
desired_spec.channels = frame_channels_;
sdl_device_channels_ = frame_channels_;
sdl_stream_ = SDL_OpenAudioDeviceStream(SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK, &desired_spec,
SDLCallback, this);
if (!sdl_stream_) {
REXAPU_ERROR("SDL_OpenAudioDevice() failed: {}", SDL_GetError());
return false;
}
SDL_GetAudioDeviceFormat(SDL_GetAudioStreamDevice(sdl_stream_), &obtained_spec,
&obtained_sample_frames);
sdl_device_sample_frames_ = static_cast<uint32_t>(std::max(obtained_sample_frames, 1));
if (obtained_spec.channels == 2) {
SDL_DestroyAudioStream(sdl_stream_);
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_OpenAudioDevice() stereo reopen failed: {}", SDL_GetError());
return false;
}
SDL_GetAudioDeviceFormat(SDL_GetAudioStreamDevice(sdl_stream_), &obtained_spec,
&obtained_sample_frames);
sdl_device_sample_frames_ = static_cast<uint32_t>(std::max(obtained_sample_frames, 1));
}
SDL_ResumeAudioDevice(SDL_GetAudioStreamDevice(sdl_stream_));
REXAPU_INFO("SDLAudioDriver initialized: client={} freq={} channels={} device_channels={} "
"format={:#x} device_id={} sample_frames_requested={} sample_frames_obtained={}",
client_index_, frame_frequency_, frame_channels_, sdl_device_channels_,
static_cast<uint32_t>(obtained_spec.format),
SDL_GetAudioStreamDevice(sdl_stream_), requested_sample_frames,
obtained_sample_frames);
return true;
}
void SDLAudioDriver::SubmitFrame(uint32_t frame_ptr) {
if (shutting_down_.load(std::memory_order_acquire)) {
return;
}
refill_requested_.store(false, std::memory_order_release);
const auto input_frame = memory_->TranslateVirtual<float*>(frame_ptr);
const auto guest_frame_stats = AnalyzeGuestFrame(input_frame, frame_samples_);
float* output_frame = nullptr;
{
std::unique_lock<std::mutex> guard(frames_mutex_);
if (frames_unused_.empty()) {
output_frame = new float[frame_samples_];
} else {
output_frame = frames_unused_.top();
frames_unused_.pop();
}
}
std::memcpy(output_frame, input_frame, frame_samples_ * sizeof(float));
{
std::unique_lock<std::mutex> guard(frames_mutex_);
frames_queued_.push(output_frame);
}
const uint32_t submitted = submitted_frames_.fetch_add(1, std::memory_order_relaxed) + 1;
const uint32_t queued_depth = queued_depth_.fetch_add(1, std::memory_order_relaxed) + 1;
uint32_t previous_peak = peak_queued_depth_.load(std::memory_order_relaxed);
while (queued_depth > previous_peak &&
!peak_queued_depth_.compare_exchange_weak(previous_peak, queued_depth,
std::memory_order_relaxed)) {
}
if (((REXCVAR_GET(audio_trace_render_driver_verbose) &&
(submitted <= 24 || (submitted % 60) == 0 || queued_depth <= 1)) ||
(IsDeepTraceEnabled() &&
(submitted <= 200 || guest_frame_stats.zeroish_samples == guest_frame_stats.sample_count)))) {
const double guest_rms =
guest_frame_stats.sample_count == 0
? 0.0
: std::sqrt(guest_frame_stats.sum_squares /
static_cast<double>(guest_frame_stats.sample_count));
const double guest_zeroish_pct =
guest_frame_stats.sample_count == 0
? 0.0
: (static_cast<double>(guest_frame_stats.zeroish_samples) * 100.0) /
static_cast<double>(guest_frame_stats.sample_count);
REXAPU_DEBUG(
"SDLAudioDriver::SubmitFrame frame_ptr={:08X} submitted={} consumed={} queued_depth={} "
"peak={} underruns={} silence_injections={} guest_min={:.6f} guest_max={:.6f} "
"guest_rms={:.6f} guest_zeroish_pct={:.2f} guest_nonfinite={} deep_trace={}",
frame_ptr, submitted, consumed_frames_.load(std::memory_order_relaxed), queued_depth,
peak_queued_depth_.load(std::memory_order_relaxed),
underrun_count_.load(std::memory_order_relaxed),
silence_injections_.load(std::memory_order_relaxed), guest_frame_stats.min_sample,
guest_frame_stats.max_sample, guest_rms, guest_zeroish_pct,
guest_frame_stats.has_nonfinite, IsDeepTraceEnabled());
}
}
void SDLAudioDriver::SubmitSilenceFrame() {
if (shutting_down_.load(std::memory_order_acquire)) {
return;
}
refill_requested_.store(false, std::memory_order_release);
float* output_frame = nullptr;
{
std::unique_lock<std::mutex> guard(frames_mutex_);
if (frames_unused_.empty()) {
output_frame = new float[frame_samples_];
} else {
output_frame = frames_unused_.top();
frames_unused_.pop();
}
}
std::fill_n(output_frame, frame_samples_, 0.0f);
{
std::unique_lock<std::mutex> guard(frames_mutex_);
frames_queued_.push(output_frame);
}
const uint32_t submitted = submitted_frames_.fetch_add(1, std::memory_order_relaxed) + 1;
const uint32_t queued_depth = queued_depth_.fetch_add(1, std::memory_order_relaxed) + 1;
silence_injections_.fetch_add(1, std::memory_order_relaxed);
uint32_t previous_peak = peak_queued_depth_.load(std::memory_order_relaxed);
while (queued_depth > previous_peak &&
!peak_queued_depth_.compare_exchange_weak(previous_peak, queued_depth,
std::memory_order_relaxed)) {
}
if (REXCVAR_GET(audio_trace_render_driver_verbose) &&
(submitted <= 24 || (submitted % 60) == 0 || queued_depth <= 1)) {
REXAPU_DEBUG(
"SDLAudioDriver::SubmitSilenceFrame submitted={} consumed={} queued_depth={} peak={} underruns={} silence_injections={}",
submitted, consumed_frames_.load(std::memory_order_relaxed), queued_depth,
peak_queued_depth_.load(std::memory_order_relaxed),
underrun_count_.load(std::memory_order_relaxed),
silence_injections_.load(std::memory_order_relaxed));
}
}
void SDLAudioDriver::Shutdown() {
if (sdl_stream_) {
shutting_down_.store(true, std::memory_order_release);
if (!SDL_PauseAudioStreamDevice(sdl_stream_)) {
REXAPU_WARN("SDL_PauseAudioStreamDevice failed during shutdown: {}", SDL_GetError());
}
if (!SDL_SetAudioStreamGetCallback(sdl_stream_, nullptr, nullptr)) {
REXAPU_WARN("SDL_SetAudioStreamGetCallback(nullptr) failed during shutdown: {}",
SDL_GetError());
}
if (SDL_LockAudioStream(sdl_stream_)) {
if (!SDL_ClearAudioStream(sdl_stream_)) {
REXAPU_WARN("SDL_ClearAudioStream failed during shutdown: {}", SDL_GetError());
}
if (!SDL_UnlockAudioStream(sdl_stream_)) {
REXAPU_WARN("SDL_UnlockAudioStream failed during shutdown: {}", SDL_GetError());
}
}
SDL_DestroyAudioStream(sdl_stream_);
sdl_stream_ = nullptr;
}
if (sdl_initialized_) {
SDL_QuitSubSystem(SDL_INIT_AUDIO);
sdl_initialized_ = false;
}
std::unique_lock<std::mutex> guard(frames_mutex_);
while (!frames_unused_.empty()) {
delete[] frames_unused_.top();
frames_unused_.pop();
}
while (!frames_queued_.empty()) {
delete[] frames_queued_.front();
frames_queued_.pop();
}
pending_output_float_count_ = 0;
pending_output_float_offset_ = 0;
}
AudioDriverTelemetry SDLAudioDriver::GetTelemetry() const {
return AudioDriverTelemetry{
submitted_frames_.load(std::memory_order_relaxed),
consumed_frames_.load(std::memory_order_relaxed),
underrun_count_.load(std::memory_order_relaxed),
silence_injections_.load(std::memory_order_relaxed),
queued_depth_.load(std::memory_order_relaxed),
peak_queued_depth_.load(std::memory_order_relaxed),
};
}
uint32_t SDLAudioDriver::queue_low_water_frames() const {
return std::max(1u, queue_target_frames() - 1);
}
uint32_t SDLAudioDriver::queue_target_frames() const {
return RequiredQueueFramesForDevice(sdl_device_sample_frames_);
}
void SDLAudioDriver::SDLCallback(void* userdata, SDL_AudioStream* stream, int additional_amount,
int total_amount) {
SCOPE_profile_cpu_f("apu");
if (!userdata || !stream) {
return;
}
const auto driver = static_cast<SDLAudioDriver*>(userdata);
if (driver->shutting_down_.load(std::memory_order_acquire)) {
return;
}
const int len = static_cast<int>(sizeof(float) * channel_samples_ * driver->sdl_device_channels_);
float* data = SDL_stack_alloc(float, len / static_cast<int>(sizeof(float)));
const size_t output_frame_float_count =
channel_samples_ * static_cast<size_t>(driver->sdl_device_channels_);
OutputChunkStats aggregate_stats;
const uint32_t callback_count =
driver->callback_count_.fetch_add(1, std::memory_order_relaxed) + 1;
while (additional_amount > 0) {
if (driver->pending_output_float_offset_ == driver->pending_output_float_count_) {
driver->pending_output_float_count_ = 0;
driver->pending_output_float_offset_ = 0;
if (driver->shutting_down_.load(std::memory_order_acquire)) {
break;
}
float* buffer = nullptr;
{
std::unique_lock<std::mutex> guard(driver->frames_mutex_);
if (!driver->frames_queued_.empty()) {
buffer = driver->frames_queued_.front();
driver->frames_queued_.pop();
driver->queued_depth_.fetch_sub(1, std::memory_order_relaxed);
}
}
if (buffer) {
const float* input_frame = buffer;
if (!REXCVAR_GET(audio_mute)) {
switch (driver->sdl_device_channels_) {
case 2:
conversion::sequential_6_BE_to_interleaved_2_LE(
driver->pending_output_frame_.data(), input_frame, channel_samples_);
break;
case 6:
conversion::sequential_6_BE_to_interleaved_6_LE(
driver->pending_output_frame_.data(), input_frame, channel_samples_);
break;
default:
assert_unhandled_case(driver->sdl_device_channels_);
break;
}
} else {
std::memset(driver->pending_output_frame_.data(), 0,
output_frame_float_count * sizeof(float));
}
driver->pending_output_float_count_ = output_frame_float_count;
{
std::unique_lock<std::mutex> guard(driver->frames_mutex_);
driver->frames_unused_.push(buffer);
}
if (REXCVAR_GET(audio_trace_render_driver_verbose) &&
(callback_count <= 24 || (callback_count % 60) == 0)) {
const auto output_stats = AnalyzeOutputChunk(driver->pending_output_frame_.data(),
output_frame_float_count);
const double output_rms =
output_stats.sample_count == 0
? 0.0
: std::sqrt(output_stats.sum_squares /
static_cast<double>(output_stats.sample_count));
REXAPU_DEBUG(
"SDLAudioDriver callback client={} callback_count={} samples={} output_min={:.6f} "
"output_max={:.6f} output_rms={:.6f} output_nonfinite={}",
driver->client_index_, callback_count, 0u,
output_stats.min_sample, output_stats.max_sample, output_rms,
output_stats.has_nonfinite);
}
}
}
if (driver->pending_output_float_count_ == 0) {
const int chunk_bytes = std::min(additional_amount, len);
driver->underrun_count_.fetch_add(1, std::memory_order_relaxed);
driver->silence_injections_.fetch_add(1, std::memory_order_relaxed);
std::memset(data, 0, chunk_bytes);
MergeOutputChunkStats(
aggregate_stats,
AnalyzeOutputChunk(data, chunk_bytes / static_cast<int>(sizeof(float))));
if (!SDL_PutAudioStreamData(stream, data, chunk_bytes)) {
break;
}
if (driver->runtime_) {
driver->runtime_->WakeWorker();
}
additional_amount -= chunk_bytes;
continue;
}
const size_t pending_float_count =
driver->pending_output_float_count_ - driver->pending_output_float_offset_;
const int chunk_bytes =
std::min(additional_amount, static_cast<int>(pending_float_count * sizeof(float)));
const size_t chunk_float_count =
static_cast<size_t>(chunk_bytes / static_cast<int>(sizeof(float)));
const float* chunk_ptr =
driver->pending_output_frame_.data() + driver->pending_output_float_offset_;
MergeOutputChunkStats(aggregate_stats, AnalyzeOutputChunk(chunk_ptr, chunk_float_count));
if (!SDL_PutAudioStreamData(stream, chunk_ptr, chunk_bytes)) {
break;
}
driver->pending_output_float_offset_ += chunk_float_count;
if (driver->runtime_) {
driver->runtime_->ReportSamplesConsumedForClient(
driver->client_index_,
static_cast<uint32_t>(chunk_float_count / driver->sdl_device_channels_));
}
if (driver->pending_output_float_offset_ == driver->pending_output_float_count_) {
driver->pending_output_float_count_ = 0;
driver->pending_output_float_offset_ = 0;
driver->consumed_frames_.fetch_add(1, std::memory_order_relaxed);
if (driver->runtime_) {
driver->runtime_->ConsumeQueuedFramesForClient(driver->client_index_, 1);
driver->runtime_->WakeWorker();
}
}
additional_amount -= chunk_bytes;
}
SDL_stack_free(data);
}
} // namespace rex::audio::sdl
@@ -1,79 +0,0 @@
/**
******************************************************************************
* Xenia : Xbox 360 Emulator Research Project *
******************************************************************************
* Copyright 2020 Ben Vanik. All rights reserved. *
* Released under the BSD license - see LICENSE in the root for more details. *
******************************************************************************
*
* @modified Tom Clay, 2026 - Adapted for ReXGlue runtime
*/
#pragma once
#include <array>
#include <atomic>
#include <mutex>
#include <queue>
#include <stack>
#include <rex/audio/audio_driver.h>
#include <rex/thread.h>
#include <SDL3/SDL.h>
namespace rex::audio {
class AudioRuntime;
}
namespace rex::audio::sdl {
class SDLAudioDriver : public AudioDriver {
public:
SDLAudioDriver(memory::Memory* memory, AudioRuntime* runtime, size_t client_index);
~SDLAudioDriver() override;
bool Initialize() override;
void SubmitFrame(uint32_t frame_ptr) override;
void SubmitSilenceFrame() override;
AudioDriverTelemetry GetTelemetry() const override;
void Shutdown() override;
const char* backend_name() const override { return "sdl"; }
uint32_t queue_low_water_frames() const override;
uint32_t queue_target_frames() const override;
protected:
static void SDLCallback(void* userdata, SDL_AudioStream* stream, int additional_amount,
int total_amount);
AudioRuntime* runtime_ = nullptr;
size_t client_index_ = 0;
SDL_AudioStream* sdl_stream_ = nullptr;
bool sdl_initialized_ = false;
uint8_t sdl_device_channels_ = 0;
uint32_t sdl_device_sample_frames_ = kRenderDriverTicSamplesPerFrame;
std::atomic<bool> shutting_down_ = false;
static const uint32_t frame_frequency_ = 48000;
static const uint32_t frame_channels_ = 6;
static const uint32_t channel_samples_ = 256;
static const uint32_t frame_samples_ = frame_channels_ * channel_samples_;
static const uint32_t frame_size_ = sizeof(float) * frame_samples_;
std::atomic<uint32_t> callback_count_ = 0;
std::atomic<bool> refill_requested_ = false;
std::atomic<uint32_t> submitted_frames_ = 0;
std::atomic<uint32_t> consumed_frames_ = 0;
std::atomic<uint32_t> underrun_count_ = 0;
std::atomic<uint32_t> silence_injections_ = 0;
std::atomic<uint32_t> queued_depth_ = 0;
std::atomic<uint32_t> peak_queued_depth_ = 0;
std::queue<float*> frames_queued_ = {};
std::stack<float*> frames_unused_ = {};
std::mutex frames_mutex_ = {};
std::array<float, frame_samples_> pending_output_frame_ = {};
size_t pending_output_float_count_ = 0;
size_t pending_output_float_offset_ = 0;
};
} // namespace rex::audio::sdl
@@ -1,26 +0,0 @@
/**
******************************************************************************
* Xenia : Xbox 360 Emulator Research Project *
******************************************************************************
* Copyright 2020 Ben Vanik. All rights reserved. *
* Released under the BSD license - see LICENSE in the root for more details. *
******************************************************************************
*
* @modified Tom Clay, 2026 - Adapted for ReXGlue runtime
*/
#include <rex/audio/sdl/sdl_audio_system.h>
namespace rex::audio::sdl {
std::unique_ptr<AudioSystem> SDLAudioSystem::Create(
runtime::FunctionDispatcher* function_dispatcher) {
return std::make_unique<SDLAudioSystem>(function_dispatcher);
}
SDLAudioSystem::SDLAudioSystem(runtime::FunctionDispatcher* function_dispatcher)
: AudioSystem(function_dispatcher) {}
SDLAudioSystem::~SDLAudioSystem() = default;
} // namespace rex::audio::sdl
@@ -1,30 +0,0 @@
/**
******************************************************************************
* Xenia : Xbox 360 Emulator Research Project *
******************************************************************************
* Copyright 2020 Ben Vanik. All rights reserved. *
* Released under the BSD license - see LICENSE in the root for more details. *
******************************************************************************
*
* @modified Tom Clay, 2026 - Adapted for ReXGlue runtime
*/
#pragma once
#include <memory>
#include <rex/audio/audio_system.h>
namespace rex::audio::sdl {
class SDLAudioSystem : public AudioSystem {
public:
explicit SDLAudioSystem(runtime::FunctionDispatcher* function_dispatcher);
~SDLAudioSystem() override;
static bool IsAvailable() { return true; }
static std::unique_ptr<AudioSystem> Create(runtime::FunctionDispatcher* function_dispatcher);
};
} // namespace rex::audio::sdl
@@ -1,524 +0,0 @@
/**
******************************************************************************
* ReXGlue native WASAPI audio driver *
******************************************************************************
*/
#include <rex/audio/wasapi/wasapi_audio_driver.h>
#include <algorithm>
#include <cstring>
#include <limits>
#include <memory>
#include <string>
#include <rex/audio/audio_runtime.h>
#include <rex/audio/conversion.h>
#include <rex/audio/flags.h>
#include <rex/cvar.h>
#include <rex/logging.h>
#define WIN32_LEAN_AND_MEAN
#define NOMINMAX
#define COBJMACROS
#include <Windows.h>
#include <audioclient.h>
#include <ksmedia.h>
#include <mmdeviceapi.h>
REXCVAR_DECLARE(bool, audio_trace_render_driver_verbose);
REXCVAR_DEFINE_INT32(audio_wasapi_buffer_frames, 256, "Audio",
"Requested WASAPI shared-mode period in frames").range(64, 2048);
namespace rex::audio::wasapi {
namespace {
template <typename T>
void SafeRelease(T*& value) {
if (value) {
value->Release();
value = nullptr;
}
}
uint32_t ClampRequestedFrames() {
return static_cast<uint32_t>(std::clamp(REXCVAR_GET(audio_wasapi_buffer_frames), 64, 2048));
}
uint32_t RequiredQueueFramesForDevice(const uint32_t device_buffer_frames) {
const uint32_t buffer_frames = std::max(device_buffer_frames, 1u);
return std::max(2u, (buffer_frames + kRenderDriverTicSamplesPerFrame - 1) /
kRenderDriverTicSamplesPerFrame + 1);
}
REFERENCE_TIME FramesToHundredsOfNanoseconds(const uint32_t frame_count,
const uint32_t sample_rate) {
return static_cast<REFERENCE_TIME>((10000000ull * frame_count) / sample_rate);
}
WAVEFORMATEXTENSIBLE BuildStereoFloatFormat() {
WAVEFORMATEXTENSIBLE format = {};
format.Format.wFormatTag = WAVE_FORMAT_EXTENSIBLE;
format.Format.nChannels = 2;
format.Format.nSamplesPerSec = kAudioFrameSampleRate;
format.Format.wBitsPerSample = sizeof(float) * 8;
format.Format.nBlockAlign =
static_cast<WORD>(format.Format.nChannels * (format.Format.wBitsPerSample / 8));
format.Format.nAvgBytesPerSec =
format.Format.nSamplesPerSec * static_cast<uint32_t>(format.Format.nBlockAlign);
format.Format.cbSize =
static_cast<WORD>(sizeof(WAVEFORMATEXTENSIBLE) - sizeof(WAVEFORMATEX));
format.Samples.wValidBitsPerSample = format.Format.wBitsPerSample;
format.dwChannelMask = SPEAKER_FRONT_LEFT | SPEAKER_FRONT_RIGHT;
format.SubFormat = KSDATAFORMAT_SUBTYPE_IEEE_FLOAT;
return format;
}
} // namespace
WasapiAudioDriver::WasapiAudioDriver(memory::Memory* memory, AudioRuntime* runtime,
const size_t client_index)
: AudioDriver(memory), runtime_(runtime), client_index_(client_index) {}
WasapiAudioDriver::~WasapiAudioDriver() {
Shutdown();
}
bool WasapiAudioDriver::Initialize() {
shutting_down_.store(false, std::memory_order_release);
{
std::lock_guard<std::mutex> lock(init_mutex_);
init_done_ = false;
init_success_ = false;
init_error_.clear();
}
render_thread_ = std::thread([this]() { RenderThreadMain(); });
std::unique_lock<std::mutex> lock(init_mutex_);
init_cv_.wait(lock, [this]() { return init_done_; });
const bool success = init_success_;
lock.unlock();
if (!success && render_thread_.joinable()) {
render_thread_.join();
}
if (!success) {
REXAPU_ERROR("WasapiAudioDriver initialization failed for client {}: {}", client_index_,
init_error_);
}
return success;
}
void WasapiAudioDriver::Shutdown() {
const bool was_shutting_down = shutting_down_.exchange(true, std::memory_order_acq_rel);
if (was_shutting_down) {
if (render_thread_.joinable()) {
render_thread_.join();
}
return;
}
if (render_event_) {
SetEvent(render_event_);
}
if (render_thread_.joinable()) {
render_thread_.join();
}
std::unique_lock<std::mutex> guard(frames_mutex_);
while (!frames_unused_.empty()) {
delete[] frames_unused_.top();
frames_unused_.pop();
}
while (!frames_queued_.empty()) {
delete[] frames_queued_.front();
frames_queued_.pop();
}
pending_output_float_count_ = 0;
pending_output_float_offset_ = 0;
}
void WasapiAudioDriver::SubmitFrame(const uint32_t frame_ptr) {
if (shutting_down_.load(std::memory_order_acquire)) {
return;
}
const auto input_frame = memory_->TranslateVirtual<float*>(frame_ptr);
if (!input_frame) {
return;
}
float* output_frame = nullptr;
{
std::unique_lock<std::mutex> guard(frames_mutex_);
if (frames_unused_.empty()) {
output_frame = new float[kAudioFrameTotalSamples];
} else {
output_frame = frames_unused_.top();
frames_unused_.pop();
}
}
std::memcpy(output_frame, input_frame, sizeof(float) * kAudioFrameTotalSamples);
{
std::unique_lock<std::mutex> guard(frames_mutex_);
frames_queued_.push(output_frame);
}
const uint32_t submitted = submitted_frames_.fetch_add(1, std::memory_order_relaxed) + 1;
const uint32_t queued_depth = queued_depth_.fetch_add(1, std::memory_order_relaxed) + 1;
uint32_t previous_peak = peak_queued_depth_.load(std::memory_order_relaxed);
while (queued_depth > previous_peak &&
!peak_queued_depth_.compare_exchange_weak(previous_peak, queued_depth,
std::memory_order_relaxed)) {
}
if (REXCVAR_GET(audio_trace_render_driver_verbose) &&
(submitted <= 24 || (submitted % 60) == 0 || queued_depth <= 1)) {
REXAPU_DEBUG(
"WasapiAudioDriver::SubmitFrame frame_ptr={:08X} submitted={} consumed={} queued_depth={} peak={} underruns={}",
frame_ptr, submitted, consumed_frames_.load(std::memory_order_relaxed), queued_depth,
peak_queued_depth_.load(std::memory_order_relaxed),
underrun_count_.load(std::memory_order_relaxed));
}
}
void WasapiAudioDriver::SubmitSilenceFrame() {
if (shutting_down_.load(std::memory_order_acquire)) {
return;
}
float* output_frame = nullptr;
{
std::unique_lock<std::mutex> guard(frames_mutex_);
if (frames_unused_.empty()) {
output_frame = new float[kAudioFrameTotalSamples];
} else {
output_frame = frames_unused_.top();
frames_unused_.pop();
}
}
std::fill_n(output_frame, kAudioFrameTotalSamples, 0.0f);
{
std::unique_lock<std::mutex> guard(frames_mutex_);
frames_queued_.push(output_frame);
}
const uint32_t submitted = submitted_frames_.fetch_add(1, std::memory_order_relaxed) + 1;
const uint32_t queued_depth = queued_depth_.fetch_add(1, std::memory_order_relaxed) + 1;
silence_injections_.fetch_add(1, std::memory_order_relaxed);
uint32_t previous_peak = peak_queued_depth_.load(std::memory_order_relaxed);
while (queued_depth > previous_peak &&
!peak_queued_depth_.compare_exchange_weak(previous_peak, queued_depth,
std::memory_order_relaxed)) {
}
if (REXCVAR_GET(audio_trace_render_driver_verbose) &&
(submitted <= 24 || (submitted % 60) == 0 || queued_depth <= 1)) {
REXAPU_DEBUG(
"WasapiAudioDriver::SubmitSilenceFrame submitted={} consumed={} queued_depth={} peak={} underruns={} silence_injections={}",
submitted, consumed_frames_.load(std::memory_order_relaxed), queued_depth,
peak_queued_depth_.load(std::memory_order_relaxed),
underrun_count_.load(std::memory_order_relaxed),
silence_injections_.load(std::memory_order_relaxed));
}
}
AudioDriverTelemetry WasapiAudioDriver::GetTelemetry() const {
return AudioDriverTelemetry{
submitted_frames_.load(std::memory_order_relaxed),
consumed_frames_.load(std::memory_order_relaxed),
underrun_count_.load(std::memory_order_relaxed),
silence_injections_.load(std::memory_order_relaxed),
queued_depth_.load(std::memory_order_relaxed),
peak_queued_depth_.load(std::memory_order_relaxed),
};
}
uint32_t WasapiAudioDriver::queue_low_water_frames() const {
return std::max(1u, queue_target_frames() - 1);
}
uint32_t WasapiAudioDriver::queue_target_frames() const {
return RequiredQueueFramesForDevice(device_buffer_frames_);
}
void WasapiAudioDriver::RenderThreadMain() {
HRESULT hr = CoInitializeEx(nullptr, COINIT_MULTITHREADED);
const bool co_initialized = SUCCEEDED(hr);
if (FAILED(hr) && hr != RPC_E_CHANGED_MODE) {
SignalInitResult(false, "CoInitializeEx failed");
return;
}
IMMDeviceEnumerator* enumerator = nullptr;
IMMDevice* device = nullptr;
IAudioClient* audio_client = nullptr;
#ifdef __IAudioClient2_INTERFACE_DEFINED__
IAudioClient2* audio_client2 = nullptr;
#endif
#ifdef __IAudioClient3_INTERFACE_DEFINED__
IAudioClient3* audio_client3 = nullptr;
#endif
IAudioRenderClient* render_client = nullptr;
do {
hr = CoCreateInstance(__uuidof(MMDeviceEnumerator), nullptr, CLSCTX_INPROC_SERVER,
__uuidof(IMMDeviceEnumerator),
reinterpret_cast<void**>(&enumerator));
if (FAILED(hr)) {
SignalInitResult(false, "CoCreateInstance(MMDeviceEnumerator) failed");
break;
}
hr = enumerator->GetDefaultAudioEndpoint(eRender, eConsole, &device);
if (FAILED(hr)) {
SignalInitResult(false, "GetDefaultAudioEndpoint failed");
break;
}
hr = device->Activate(__uuidof(IAudioClient), CLSCTX_INPROC_SERVER, nullptr,
reinterpret_cast<void**>(&audio_client));
if (FAILED(hr)) {
SignalInitResult(false, "IMMDevice::Activate(IAudioClient) failed");
break;
}
#ifdef __IAudioClient2_INTERFACE_DEFINED__
hr = audio_client->QueryInterface(__uuidof(IAudioClient2),
reinterpret_cast<void**>(&audio_client2));
if (SUCCEEDED(hr) && audio_client2) {
AudioClientProperties properties = {};
properties.cbSize = sizeof(properties);
properties.eCategory = AudioCategory_GameMedia;
properties.Options = AUDCLNT_STREAMOPTIONS_NONE;
audio_client2->SetClientProperties(&properties);
}
#endif
const WAVEFORMATEXTENSIBLE requested_format = BuildStereoFloatFormat();
WAVEFORMATEX* closest_match = nullptr;
hr = audio_client->IsFormatSupported(AUDCLNT_SHAREMODE_SHARED,
reinterpret_cast<const WAVEFORMATEX*>(&requested_format),
&closest_match);
if (closest_match) {
CoTaskMemFree(closest_match);
closest_match = nullptr;
}
const DWORD base_stream_flags = AUDCLNT_STREAMFLAGS_EVENTCALLBACK |
AUDCLNT_STREAMFLAGS_AUTOCONVERTPCM |
AUDCLNT_STREAMFLAGS_SRC_DEFAULT_QUALITY;
const uint32_t requested_frames = ClampRequestedFrames();
uint32_t initialized_frames = requested_frames;
bool initialized = false;
#ifdef __IAudioClient3_INTERFACE_DEFINED__
hr = audio_client->QueryInterface(__uuidof(IAudioClient3),
reinterpret_cast<void**>(&audio_client3));
if (SUCCEEDED(hr) && audio_client3) {
UINT32 default_period = 0;
UINT32 fundamental_period = 0;
UINT32 min_period = 0;
UINT32 max_period = 0;
hr = audio_client3->GetSharedModeEnginePeriod(
reinterpret_cast<const WAVEFORMATEX*>(&requested_format), &default_period,
&fundamental_period, &min_period, &max_period);
if (SUCCEEDED(hr) && fundamental_period != 0) {
initialized_frames =
std::clamp(((requested_frames + fundamental_period - 1) / fundamental_period) *
fundamental_period,
min_period, max_period);
hr = audio_client3->InitializeSharedAudioStream(
AUDCLNT_STREAMFLAGS_EVENTCALLBACK, initialized_frames,
reinterpret_cast<const WAVEFORMATEX*>(&requested_format), nullptr);
initialized = SUCCEEDED(hr);
}
}
#endif
if (!initialized) {
initialized_frames = requested_frames;
hr = audio_client->Initialize(
AUDCLNT_SHAREMODE_SHARED, base_stream_flags,
FramesToHundredsOfNanoseconds(initialized_frames, kAudioFrameSampleRate), 0,
reinterpret_cast<const WAVEFORMATEX*>(&requested_format), nullptr);
if (FAILED(hr)) {
SignalInitResult(false, "IAudioClient initialization failed");
break;
}
}
render_event_ = CreateEventW(nullptr, FALSE, FALSE, nullptr);
if (!render_event_) {
SignalInitResult(false, "CreateEventW failed");
break;
}
hr = audio_client->SetEventHandle(render_event_);
if (FAILED(hr)) {
SignalInitResult(false, "IAudioClient::SetEventHandle failed");
break;
}
hr = audio_client->GetService(__uuidof(IAudioRenderClient),
reinterpret_cast<void**>(&render_client));
if (FAILED(hr)) {
SignalInitResult(false, "IAudioClient::GetService(IAudioRenderClient) failed");
break;
}
UINT32 buffer_frame_count = 0;
hr = audio_client->GetBufferSize(&buffer_frame_count);
if (FAILED(hr)) {
SignalInitResult(false, "IAudioClient::GetBufferSize failed");
break;
}
device_buffer_frames_ = buffer_frame_count;
BYTE* initial_buffer = nullptr;
hr = render_client->GetBuffer(buffer_frame_count, &initial_buffer);
if (FAILED(hr)) {
SignalInitResult(false, "IAudioRenderClient::GetBuffer initial fill failed");
break;
}
std::memset(initial_buffer, 0, sizeof(float) * 2 * buffer_frame_count);
hr = render_client->ReleaseBuffer(buffer_frame_count, 0);
if (FAILED(hr)) {
SignalInitResult(false, "IAudioRenderClient::ReleaseBuffer initial fill failed");
break;
}
hr = audio_client->Start();
if (FAILED(hr)) {
SignalInitResult(false, "IAudioClient::Start failed");
break;
}
REXAPU_INFO(
"WasapiAudioDriver initialized: client={} channels=2 freq={} requested_frames={} initialized_frames={} buffer_frames={}",
client_index_, kAudioFrameSampleRate, requested_frames, initialized_frames,
buffer_frame_count);
SignalInitResult(true);
while (!shutting_down_.load(std::memory_order_acquire)) {
const DWORD wait_result = WaitForSingleObject(render_event_, 10);
if (wait_result != WAIT_OBJECT_0 && wait_result != WAIT_TIMEOUT) {
break;
}
UINT32 padding_frames = 0;
hr = audio_client->GetCurrentPadding(&padding_frames);
if (FAILED(hr) || padding_frames > buffer_frame_count) {
continue;
}
UINT32 available_frames = buffer_frame_count - padding_frames;
while (available_frames > 0 && !shutting_down_.load(std::memory_order_acquire)) {
if (pending_output_float_offset_ == pending_output_float_count_) {
pending_output_float_count_ = 0;
pending_output_float_offset_ = 0;
float* buffer = nullptr;
{
std::unique_lock<std::mutex> guard(frames_mutex_);
if (!frames_queued_.empty()) {
buffer = frames_queued_.front();
frames_queued_.pop();
queued_depth_.fetch_sub(1, std::memory_order_relaxed);
}
}
if (buffer) {
if (!REXCVAR_GET(audio_mute)) {
conversion::sequential_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());
}
pending_output_float_count_ = pending_output_frame_.size();
{
std::unique_lock<std::mutex> guard(frames_mutex_);
frames_unused_.push(buffer);
}
}
}
const UINT32 pending_frames =
static_cast<UINT32>((pending_output_float_count_ - pending_output_float_offset_) / 2);
const UINT32 frames_to_write =
pending_frames != 0 ? std::min(available_frames, pending_frames) : available_frames;
BYTE* target = nullptr;
hr = render_client->GetBuffer(frames_to_write, &target);
if (FAILED(hr)) {
break;
}
if (pending_frames == 0) {
++underrun_count_;
++silence_injections_;
std::memset(target, 0, sizeof(float) * 2 * frames_to_write);
} 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_,
sizeof(float) * float_count);
pending_output_float_offset_ += float_count;
if (runtime_) {
runtime_->ReportSamplesConsumedForClient(client_index_, frames_to_write);
}
if (pending_output_float_offset_ == pending_output_float_count_) {
pending_output_float_count_ = 0;
pending_output_float_offset_ = 0;
consumed_frames_.fetch_add(1, std::memory_order_relaxed);
if (runtime_) {
runtime_->ConsumeQueuedFramesForClient(client_index_, 1);
runtime_->WakeWorker();
}
}
}
hr = render_client->ReleaseBuffer(frames_to_write, 0);
if (FAILED(hr)) {
break;
}
available_frames -= frames_to_write;
}
}
audio_client->Stop();
} while (false);
if (render_event_) {
CloseHandle(render_event_);
render_event_ = nullptr;
}
SafeRelease(render_client);
#ifdef __IAudioClient3_INTERFACE_DEFINED__
SafeRelease(audio_client3);
#endif
#ifdef __IAudioClient2_INTERFACE_DEFINED__
SafeRelease(audio_client2);
#endif
SafeRelease(audio_client);
SafeRelease(device);
SafeRelease(enumerator);
if (co_initialized) {
CoUninitialize();
}
}
void WasapiAudioDriver::SignalInitResult(const bool success, std::string error_message) {
std::lock_guard<std::mutex> lock(init_mutex_);
init_success_ = success;
init_done_ = true;
init_error_ = std::move(error_message);
init_cv_.notify_all();
}
} // namespace rex::audio::wasapi
@@ -1,77 +0,0 @@
/**
******************************************************************************
* ReXGlue native WASAPI audio driver *
******************************************************************************
*/
#pragma once
#include <array>
#include <atomic>
#include <condition_variable>
#include <cstdint>
#include <mutex>
#include <queue>
#include <stack>
#include <string>
#include <thread>
#define WIN32_LEAN_AND_MEAN
#define NOMINMAX
#include <Windows.h>
#include <rex/audio/audio_driver.h>
namespace rex::audio {
class AudioRuntime;
}
namespace rex::audio::wasapi {
class WasapiAudioDriver : public AudioDriver {
public:
WasapiAudioDriver(memory::Memory* memory, AudioRuntime* runtime, size_t client_index);
~WasapiAudioDriver() override;
bool Initialize() override;
void Shutdown() override;
void SubmitFrame(uint32_t frame_ptr) override;
void SubmitSilenceFrame() override;
AudioDriverTelemetry GetTelemetry() const override;
const char* backend_name() const override { return "wasapi"; }
uint32_t queue_low_water_frames() const override;
uint32_t queue_target_frames() const override;
private:
void RenderThreadMain();
void SignalInitResult(bool success, std::string error_message = {});
AudioRuntime* runtime_{nullptr};
size_t client_index_{0};
std::thread render_thread_;
std::mutex init_mutex_;
std::condition_variable init_cv_;
bool init_done_{false};
bool init_success_{false};
std::string init_error_;
HANDLE render_event_{nullptr};
uint32_t device_buffer_frames_{256};
std::atomic<bool> shutting_down_{false};
std::atomic<uint32_t> submitted_frames_{0};
std::atomic<uint32_t> consumed_frames_{0};
std::atomic<uint32_t> underrun_count_{0};
std::atomic<uint32_t> silence_injections_{0};
std::atomic<uint32_t> queued_depth_{0};
std::atomic<uint32_t> peak_queued_depth_{0};
std::queue<float*> frames_queued_{};
std::stack<float*> frames_unused_{};
std::mutex frames_mutex_{};
std::array<float, 256 * 2> pending_output_frame_{};
size_t pending_output_float_count_{0};
size_t pending_output_float_offset_{0};
};
} // namespace rex::audio::wasapi
@@ -1,890 +0,0 @@
/**
******************************************************************************
* Xenia : Xbox 360 Emulator Research Project *
******************************************************************************
* Copyright 2021 Ben Vanik. All rights reserved. *
* Released under the BSD license - see LICENSE in the root for more details. *
******************************************************************************
*
* @modified Tom Clay, 2026 - Adapted for ReXGlue runtime
*/
#include <algorithm>
#include <cstring>
#include <rex/audio/xma/context.h>
#include <rex/audio/xma/decoder.h>
#include <rex/audio/xma/helpers.h>
#include <rex/cvar.h>
#include <rex/dbg.h>
#include <rex/logging.h>
#include <rex/memory/ring_buffer.h>
#include <rex/platform.h>
#include <rex/stream.h>
extern "C" {
#if REX_COMPILER_MSVC
#pragma warning(push)
#pragma warning(disable : 4101 4244 5033)
#endif
#include "libavcodec/avcodec.h"
#if REX_COMPILER_MSVC
#pragma warning(pop)
#endif
} // extern "C"
REXCVAR_DECLARE(bool, ac6_audio_deep_trace);
namespace rex::audio {
using stream::BitStream;
namespace {
bool IsDeepTraceEnabled() {
return REXCVAR_GET(ac6_audio_deep_trace);
}
} // namespace
XmaContext::XmaContext()
: work_completion_event_(rex::thread::Event::CreateAutoResetEvent(false)) {}
XmaContext::~XmaContext() {
if (av_context_) {
if (avcodec_is_open(av_context_)) {
avcodec_close(av_context_);
}
av_free(av_context_);
}
if (av_frame_) {
av_frame_free(&av_frame_);
}
}
int XmaContext::Setup(uint32_t id, memory::Memory* memory, uint32_t guest_ptr) {
id_ = id;
memory_ = memory;
guest_ptr_ = guest_ptr;
av_packet_ = av_packet_alloc();
assert_not_null(av_packet_);
av_codec_ = avcodec_find_decoder(AV_CODEC_ID_XMAFRAMES);
if (!av_codec_) {
REXAPU_ERROR("XmaContext {}: Codec not found", id);
return 1;
}
av_context_ = avcodec_alloc_context3(av_codec_);
if (!av_context_) {
REXAPU_ERROR("XmaContext {}: Couldn't allocate context", id);
return 1;
}
av_context_->channels = 0;
av_context_->sample_rate = 0;
av_frame_ = av_frame_alloc();
if (!av_frame_) {
REXAPU_ERROR("XmaContext {}: Couldn't allocate frame", id);
return 1;
}
return 0;
}
bool XmaContext::Work() {
if (!is_allocated() || !is_enabled()) {
return false;
}
std::lock_guard<std::mutex> lock(lock_);
set_is_enabled(false);
auto context_ptr = memory()->TranslateVirtual(guest_ptr());
XMA_CONTEXT_DATA data(context_ptr);
const XMA_CONTEXT_DATA initial_data = data;
if (!data.output_buffer_valid) {
return true;
}
memory::RingBuffer output_rb = PrepareOutputRingBuffer(&data);
if (data.IsConsumeOnlyContext()) {
if (current_frame_remaining_subframes_ == 0) {
return true;
}
Consume(&output_rb, &data);
data.output_buffer_write_offset = output_rb.write_offset() / kOutputBytesPerBlock;
StoreContextMerged(data, initial_data, context_ptr);
return true;
}
const uint32_t effective_sdc = std::max(static_cast<uint32_t>(1), data.subframe_decode_count);
const int32_t minimum_subframe_decode_count =
static_cast<int32_t>(effective_sdc) + data.output_buffer_padding;
if (minimum_subframe_decode_count > remaining_subframe_blocks_in_output_buffer_) {
StoreContextMerged(data, initial_data, context_ptr);
return true;
}
while (remaining_subframe_blocks_in_output_buffer_ >= minimum_subframe_decode_count) {
Decode(&data);
Consume(&output_rb, &data);
if (!data.IsAnyInputBufferValid() || data.error_status == 4) {
if (data.error_status == 4) {
REXAPU_WARN(
"XmaContext {}: decode aborted with error_status=4 packet_index={} next_packet={} "
"read_before={} read_after={} frame_bits={} bits_to_copy={} skip={} "
"cross_packet={} swapped={}",
id(), last_packet_index_, last_next_packet_index_, last_input_read_offset_before_,
last_input_read_offset_after_, last_frame_size_bits_, last_bits_to_copy_,
last_skip_count_, last_cross_packet_copy_, last_swapped_input_buffer_);
}
break;
}
}
data.output_buffer_write_offset = output_rb.write_offset() / kOutputBytesPerBlock;
if (output_rb.empty()) {
data.output_buffer_valid = 0;
}
StoreContextMerged(data, initial_data, context_ptr);
return true;
}
void XmaContext::Enable() {
std::lock_guard<std::mutex> lock(lock_);
set_is_enabled(true);
}
bool XmaContext::Block(bool poll) {
if (!lock_.try_lock()) {
if (poll) {
return false;
}
lock_.lock();
}
lock_.unlock();
return true;
}
void XmaContext::Clear() {
std::lock_guard<std::mutex> lock(lock_);
auto context_ptr = memory()->TranslateVirtual(guest_ptr());
XMA_CONTEXT_DATA data(context_ptr);
REXAPU_DEBUG(
"XmaContext {} guest-state before reset: current_buffer={} valid0={} valid1={} "
"output_valid={} input_read_offset={} output_read_offset={} output_write_offset={} "
"error_status={}",
id(), static_cast<uint32_t>(data.current_buffer),
static_cast<uint32_t>(data.input_buffer_0_valid),
static_cast<uint32_t>(data.input_buffer_1_valid),
static_cast<uint32_t>(data.output_buffer_valid),
static_cast<uint32_t>(data.input_buffer_read_offset),
static_cast<uint32_t>(data.output_buffer_read_offset),
static_cast<uint32_t>(data.output_buffer_write_offset),
static_cast<uint32_t>(data.error_status));
REXAPU_DEBUG(
"XmaContext {} last decode snapshot before reset: current_buffer={} read_before={} "
"read_after={} packet_count={} decode_attempt={} last_packet={} next_packet={} "
"last_frame_bits={} bits_to_copy={} skip={} cross_packet={} swapped={} "
"decode_ok={} last_error_status={}",
id(), last_current_buffer_, last_input_read_offset_before_, last_input_read_offset_after_,
last_current_input_packet_count_, decode_attempt_count_, last_packet_index_,
last_next_packet_index_, last_frame_size_bits_, last_bits_to_copy_, last_skip_count_,
last_cross_packet_copy_, last_swapped_input_buffer_, last_decode_succeeded_,
last_error_status_);
ClearLocked(&data);
data.Store(context_ptr);
}
void XmaContext::ClearLocked(XMA_CONTEXT_DATA* data) {
data->input_buffer_0_valid = 0;
data->input_buffer_1_valid = 0;
data->output_buffer_valid = 0;
data->input_buffer_read_offset = kBitsPerPacketHeader;
data->output_buffer_read_offset = 0;
data->output_buffer_write_offset = 0;
current_frame_remaining_subframes_ = 0;
loop_frame_output_limit_ = 0;
loop_start_skip_pending_ = false;
}
void XmaContext::Disable() {
std::lock_guard<std::mutex> lock(lock_);
set_is_enabled(false);
}
void XmaContext::Release() {
std::lock_guard<std::mutex> lock(lock_);
assert_true(is_allocated());
set_is_allocated(false);
auto context_ptr = memory()->TranslateVirtual(guest_ptr());
std::memset(context_ptr, 0, sizeof(XMA_CONTEXT_DATA));
}
void XmaContext::SwapInputBuffer(XMA_CONTEXT_DATA* data) {
if (data->current_buffer == 0) {
data->input_buffer_0_valid = 0;
} else {
data->input_buffer_1_valid = 0;
}
data->current_buffer ^= 1;
data->input_buffer_read_offset = kBitsPerPacketHeader;
}
void XmaContext::UpdateLoopStatus(XMA_CONTEXT_DATA* data) {
if (data->loop_count == 0) {
return;
}
const uint32_t loop_start = std::max(kBitsPerPacketHeader, data->loop_start);
const uint32_t loop_end = std::max(kBitsPerPacketHeader, data->loop_end);
if (data->input_buffer_read_offset != loop_end) {
return;
}
data->input_buffer_read_offset = loop_start;
loop_start_skip_pending_ = true;
if (data->loop_count < 255) {
data->loop_count--;
}
}
int XmaContext::GetSampleRate(int id) {
return kIdToSampleRate[std::min(id, 3)];
}
int16_t XmaContext::GetPacketNumber(size_t size, size_t bit_offset) {
if (bit_offset < kBitsPerPacketHeader) {
assert_always();
return -1;
}
if (bit_offset >= (size << 3)) {
assert_always();
return -1;
}
size_t byte_offset = bit_offset >> 3;
size_t packet_number = byte_offset / kBytesPerPacket;
return static_cast<int16_t>(packet_number);
}
uint32_t XmaContext::GetCurrentInputBufferSize(XMA_CONTEXT_DATA* data) {
return data->GetCurrentInputBufferPacketCount() * kBytesPerPacket;
}
uint8_t* XmaContext::GetCurrentInputBuffer(XMA_CONTEXT_DATA* data) {
return memory()->TranslatePhysical(data->GetCurrentInputBufferAddress());
}
uint32_t XmaContext::GetAmountOfBitsToRead(uint32_t remaining_stream_bits, uint32_t frame_size) {
return std::min(remaining_stream_bits, frame_size);
}
const uint8_t* XmaContext::GetNextPacket(XMA_CONTEXT_DATA* data, uint32_t next_packet_index,
uint32_t current_input_packet_count) {
if (next_packet_index < current_input_packet_count) {
return memory()->TranslatePhysical(data->GetCurrentInputBufferAddress()) +
next_packet_index * kBytesPerPacket;
}
const uint8_t next_buffer_index = data->current_buffer ^ 1;
if (!data->IsInputBufferValid(next_buffer_index)) {
return nullptr;
}
const uint32_t next_buffer_packet_count = data->GetInputBufferPacketCount(next_buffer_index);
const uint32_t next_buffer_packet_index = next_packet_index - current_input_packet_count;
if (next_buffer_packet_index >= next_buffer_packet_count) {
return nullptr;
}
const uint32_t next_buffer_address = data->GetInputBufferAddress(next_buffer_index);
if (!next_buffer_address) {
REXAPU_ERROR("XmaContext {}: Buffer marked valid but has null pointer!", id());
return nullptr;
}
return memory()->TranslatePhysical(next_buffer_address) +
next_buffer_packet_index * kBytesPerPacket;
}
uint32_t XmaContext::GetNextPacketReadOffset(XMA_CONTEXT_DATA* data, uint32_t next_packet_index,
uint32_t current_input_packet_count) {
if (next_packet_index < current_input_packet_count) {
uint8_t* buffer = memory()->TranslatePhysical(data->GetCurrentInputBufferAddress());
while (next_packet_index < current_input_packet_count) {
uint8_t* next_packet = buffer + (next_packet_index * kBytesPerPacket);
const uint32_t packet_frame_offset = xma::GetPacketFrameOffset(next_packet);
if (packet_frame_offset <= kMaxFrameSizeinBits) {
return (next_packet_index * kBitsPerPacket) + packet_frame_offset;
}
next_packet_index++;
}
return kBitsPerPacketHeader;
}
const uint8_t next_buffer_index = data->current_buffer ^ 1;
if (!data->IsInputBufferValid(next_buffer_index)) {
return kBitsPerPacketHeader;
}
const uint32_t next_buffer_address = data->GetInputBufferAddress(next_buffer_index);
if (!next_buffer_address) {
REXAPU_ERROR("XmaContext {}: Buffer marked valid but has null pointer!", id());
return kBitsPerPacketHeader;
}
uint32_t next_buffer_packet_index = next_packet_index - current_input_packet_count;
const uint32_t next_buffer_packet_count = data->GetInputBufferPacketCount(next_buffer_index);
uint8_t* next_buffer = memory()->TranslatePhysical(next_buffer_address);
while (next_buffer_packet_index < next_buffer_packet_count) {
uint8_t* next_packet = next_buffer + (next_buffer_packet_index * kBytesPerPacket);
const uint32_t packet_frame_offset = xma::GetPacketFrameOffset(next_packet);
if (packet_frame_offset <= kMaxFrameSizeinBits) {
return (next_buffer_packet_index * kBitsPerPacket) + packet_frame_offset;
}
next_buffer_packet_index++;
}
return kBitsPerPacketHeader;
}
memory::RingBuffer XmaContext::PrepareOutputRingBuffer(XMA_CONTEXT_DATA* data) {
const uint32_t output_capacity = data->output_buffer_block_count * kOutputBytesPerBlock;
const uint32_t output_read_offset = data->output_buffer_read_offset * kOutputBytesPerBlock;
const uint32_t output_write_offset = data->output_buffer_write_offset * kOutputBytesPerBlock;
if (output_capacity > kOutputMaxSizeBytes) {
REXAPU_WARN(
"XmaContext {}: Output buffer exceeds expected size! "
"(Actual: {} Max: {})",
id(), output_capacity, kOutputMaxSizeBytes);
}
uint8_t* output_buffer = memory()->TranslatePhysical(data->output_buffer_ptr);
memory::RingBuffer output_rb(output_buffer, output_capacity);
output_rb.set_read_offset(output_read_offset);
output_rb.set_write_offset(output_write_offset);
remaining_subframe_blocks_in_output_buffer_ =
static_cast<int32_t>(output_rb.write_count()) / kOutputBytesPerBlock;
return output_rb;
}
kPacketInfo XmaContext::GetPacketInfo(uint8_t* packet, uint32_t frame_offset) {
kPacketInfo packet_info = {};
const uint32_t first_frame_offset = xma::GetPacketFrameOffset(packet);
BitStream stream(packet, kBitsPerPacket);
stream.SetOffset(first_frame_offset);
if (frame_offset < first_frame_offset) {
packet_info.current_frame_ = 0;
packet_info.current_frame_size_ = first_frame_offset - frame_offset;
}
while (true) {
if (stream.BitsRemaining() < kBitsPerFrameHeader) {
break;
}
const uint64_t frame_size = stream.Peek(kBitsPerFrameHeader);
if (frame_size == 0 || frame_size == xma::kMaxFrameLength) {
break;
}
if (stream.offset_bits() == frame_offset) {
packet_info.current_frame_ = packet_info.frame_count_;
packet_info.current_frame_size_ = static_cast<uint32_t>(frame_size);
}
packet_info.frame_count_++;
if (frame_size > stream.BitsRemaining()) {
break;
}
stream.Advance(frame_size - 1);
if (stream.Read(1) == 0) {
break;
}
}
if (xma::IsPacketXma2Type(packet)) {
const uint8_t xma2_frame_count = xma::GetPacketFrameCount(packet);
if (xma2_frame_count > packet_info.frame_count_) {
if (packet_info.current_frame_size_ == 0) {
packet_info.current_frame_ = packet_info.frame_count_;
}
packet_info.frame_count_ = xma2_frame_count;
}
}
return packet_info;
}
void XmaContext::StoreContextMerged(const XMA_CONTEXT_DATA& data,
const XMA_CONTEXT_DATA& initial_data, uint8_t* context_ptr) {
XMA_CONTEXT_DATA fresh(context_ptr);
fresh.loop_count = data.loop_count;
fresh.output_buffer_write_offset = data.output_buffer_write_offset;
if (initial_data.input_buffer_0_valid && !data.input_buffer_0_valid) {
fresh.input_buffer_0_valid = 0;
}
if (initial_data.input_buffer_1_valid && !data.input_buffer_1_valid) {
fresh.input_buffer_1_valid = 0;
}
if (initial_data.output_buffer_valid && !data.output_buffer_valid) {
fresh.output_buffer_valid = 0;
}
fresh.input_buffer_read_offset = data.input_buffer_read_offset;
fresh.error_status = data.error_status;
fresh.current_buffer = data.current_buffer;
fresh.output_buffer_read_offset = data.output_buffer_read_offset;
fresh.Store(context_ptr);
}
void XmaContext::Consume(memory::RingBuffer* output_rb, const XMA_CONTEXT_DATA* data) {
if (!current_frame_remaining_subframes_) {
return;
}
if (loop_frame_output_limit_ > 0) {
const uint8_t total_subframes =
(kBytesPerFrameChannel / kOutputBytesPerBlock) << data->is_stereo;
const uint8_t consumed = total_subframes - current_frame_remaining_subframes_;
if (consumed >= loop_frame_output_limit_) {
remaining_subframe_blocks_in_output_buffer_ -= data->output_buffer_padding;
current_frame_remaining_subframes_ = 0;
loop_frame_output_limit_ = 0;
return;
}
}
const uint8_t effective_sdc = std::max(static_cast<uint32_t>(1), data->subframe_decode_count);
int8_t subframes_to_write = std::min(static_cast<int8_t>(current_frame_remaining_subframes_),
static_cast<int8_t>(effective_sdc));
if (loop_frame_output_limit_ > 0) {
const uint8_t total_subframes =
(kBytesPerFrameChannel / kOutputBytesPerBlock) << data->is_stereo;
const uint8_t consumed = total_subframes - current_frame_remaining_subframes_;
const int8_t remaining_until_limit = static_cast<int8_t>(loop_frame_output_limit_ - consumed);
if (subframes_to_write > remaining_until_limit) {
subframes_to_write = remaining_until_limit;
}
}
const int8_t raw_frame_read_offset =
((kBytesPerFrameChannel / kOutputBytesPerBlock) << data->is_stereo) -
current_frame_remaining_subframes_;
output_rb->Write(raw_frame_.data() + (kOutputBytesPerBlock * raw_frame_read_offset),
subframes_to_write * kOutputBytesPerBlock);
const int8_t headroom = (current_frame_remaining_subframes_ - subframes_to_write == 0)
? data->output_buffer_padding
: 0;
remaining_subframe_blocks_in_output_buffer_ -= subframes_to_write + headroom;
current_frame_remaining_subframes_ -= subframes_to_write;
}
int XmaContext::PrepareDecoder(int sample_rate, bool is_two_channel) {
sample_rate = GetSampleRate(sample_rate);
uint32_t channels = is_two_channel ? 2 : 1;
if (av_context_->sample_rate != sample_rate ||
av_context_->channels != static_cast<int>(channels)) {
avcodec_close(av_context_);
av_free(av_context_);
av_context_ = avcodec_alloc_context3(av_codec_);
av_context_->sample_rate = sample_rate;
av_context_->channels = channels;
if (avcodec_open2(av_context_, av_codec_, nullptr) < 0) {
REXAPU_ERROR("XmaContext: Failed to reopen FFmpeg context");
return -1;
}
return 1;
}
return 0;
}
void XmaContext::PreparePacket(uint32_t frame_size, uint32_t frame_padding) {
av_packet_->data = xma_frame_.data();
av_packet_->size = static_cast<int>(1 + ((frame_padding + frame_size) / 8) +
(((frame_padding + frame_size) % 8) ? 1 : 0));
auto padding_end = av_packet_->size * 8 - (8 + frame_padding + frame_size);
assert_true(padding_end < 8);
xma_frame_[0] = ((frame_padding & 7) << 5) | ((padding_end & 7) << 2);
}
bool XmaContext::DecodePacket(AVCodecContext* av_context, const AVPacket* av_packet,
AVFrame* av_frame) {
auto ret = avcodec_send_packet(av_context, av_packet);
if (ret < 0) {
REXAPU_ERROR(
"XmaContext {}: Error sending packet for decoding ({}) packet_index={} next_packet={} "
"read_before={} read_after={} frame_bits={} bits_to_copy={} skip={} "
"cross_packet={} swapped={} decode_attempt={}",
id(), ret, last_packet_index_, last_next_packet_index_, last_input_read_offset_before_,
last_input_read_offset_after_, last_frame_size_bits_, last_bits_to_copy_, last_skip_count_,
last_cross_packet_copy_, last_swapped_input_buffer_, decode_attempt_count_);
return false;
}
ret = avcodec_receive_frame(av_context, av_frame);
if (ret == AVERROR(EAGAIN)) {
return false;
}
if (ret < 0) {
REXAPU_ERROR(
"XmaContext {}: Error during decoding ({}) packet_index={} next_packet={} "
"read_before={} read_after={} frame_bits={} bits_to_copy={} skip={} "
"cross_packet={} swapped={} decode_attempt={}",
id(), ret, last_packet_index_, last_next_packet_index_, last_input_read_offset_before_,
last_input_read_offset_after_, last_frame_size_bits_, last_bits_to_copy_, last_skip_count_,
last_cross_packet_copy_, last_swapped_input_buffer_, decode_attempt_count_);
return false;
}
return true;
}
void XmaContext::Decode(XMA_CONTEXT_DATA* data) {
SCOPE_profile_cpu_f("apu");
++decode_attempt_count_;
last_input_read_offset_before_ = static_cast<uint32_t>(data->input_buffer_read_offset);
last_input_read_offset_after_ = static_cast<uint32_t>(data->input_buffer_read_offset);
last_current_input_packet_count_ = 0;
last_frame_size_bits_ = 0;
last_bits_to_copy_ = 0;
last_next_packet_index_ = 0;
last_current_buffer_ = data->current_buffer;
last_skip_count_ = 0;
last_packet_index_ = -1;
last_cross_packet_copy_ = false;
last_swapped_input_buffer_ = false;
last_decode_succeeded_ = false;
last_error_status_ = static_cast<uint32_t>(data->error_status);
auto log_decode_state = [&](const char* reason) {
REXAPU_WARN(
"XmaContext {}: {} current_buffer={} valid0={} valid1={} output_valid={} "
"read_before={} read_after={} packet_index={} next_packet={} packet_count={} "
"skip={} frame_bits={} bits_to_copy={} loop_count={} err={} cross_packet={} "
"swapped={} decode_attempt={}",
id(), reason, static_cast<uint32_t>(data->current_buffer),
static_cast<uint32_t>(data->input_buffer_0_valid),
static_cast<uint32_t>(data->input_buffer_1_valid),
static_cast<uint32_t>(data->output_buffer_valid), last_input_read_offset_before_,
last_input_read_offset_after_,
last_packet_index_, last_next_packet_index_, last_current_input_packet_count_,
last_skip_count_, last_frame_size_bits_, last_bits_to_copy_,
static_cast<uint32_t>(data->loop_count), static_cast<uint32_t>(data->error_status),
last_cross_packet_copy_, last_swapped_input_buffer_, decode_attempt_count_);
};
if (!data->IsAnyInputBufferValid()) {
return;
}
if (current_frame_remaining_subframes_ > 0) {
return;
}
if (!data->IsCurrentInputBufferValid()) {
last_swapped_input_buffer_ = true;
SwapInputBuffer(data);
if (!data->IsCurrentInputBufferValid()) {
last_input_read_offset_after_ = static_cast<uint32_t>(data->input_buffer_read_offset);
return;
}
}
uint8_t* current_input_buffer = GetCurrentInputBuffer(data);
input_buffer_.fill(0);
bool is_loop_end_frame = false;
if (data->loop_count > 0) {
const uint32_t loop_end = std::max(kBitsPerPacketHeader, data->loop_end);
is_loop_end_frame = (data->input_buffer_read_offset == loop_end);
}
UpdateLoopStatus(data);
if (!data->output_buffer_block_count) {
REXAPU_ERROR("XmaContext {}: Error - Received 0 for output_buffer_block_count!", id());
return;
}
if (data->input_buffer_read_offset < kBitsPerPacketHeader) {
data->input_buffer_read_offset = kBitsPerPacketHeader;
}
const uint32_t current_input_size = GetCurrentInputBufferSize(data);
const uint32_t current_input_packet_count = current_input_size / kBytesPerPacket;
last_current_input_packet_count_ = current_input_packet_count;
const int16_t packet_index = GetPacketNumber(current_input_size, data->input_buffer_read_offset);
last_packet_index_ = packet_index;
if (packet_index == -1) {
REXAPU_ERROR("XmaContext {}: Invalid packet index. Input read offset: {}", id(),
static_cast<uint32_t>(data->input_buffer_read_offset));
log_decode_state("invalid-packet-index");
return;
}
uint8_t* packet = current_input_buffer + (packet_index * kBytesPerPacket);
const uint32_t packet_first_frame_offset = xma::GetPacketFrameOffset(packet);
uint32_t relative_offset = data->input_buffer_read_offset % kBitsPerPacket;
if (relative_offset < packet_first_frame_offset) {
data->input_buffer_read_offset = (packet_index * kBitsPerPacket) + packet_first_frame_offset;
relative_offset = packet_first_frame_offset;
}
const uint8_t skip_count = xma::GetPacketSkipCount(packet);
last_skip_count_ = skip_count;
if (skip_count == 0xFF) {
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_ = next_input_offset;
if (IsDeepTraceEnabled()) {
REXAPU_DEBUG(
"XmaContext {}: skip packet packet_index={} next_input_offset={} packet_count={}",
id(), packet_index, next_input_offset, current_input_packet_count);
}
return;
}
kPacketInfo packet_info = GetPacketInfo(packet, relative_offset);
const uint32_t packet_to_skip = skip_count + 1;
const uint32_t next_packet_index = packet_index + packet_to_skip;
last_next_packet_index_ = next_packet_index;
if (packet_info.current_frame_size_ == 0) {
const uint8_t* next_packet = GetNextPacket(data, next_packet_index, current_input_packet_count);
if (!next_packet) {
last_swapped_input_buffer_ = true;
SwapInputBuffer(data);
last_input_read_offset_after_ = static_cast<uint32_t>(data->input_buffer_read_offset);
log_decode_state("missing-next-packet-for-split-frame");
return;
}
last_cross_packet_copy_ = true;
std::memcpy(input_buffer_.data(), packet + kBytesPerPacketHeader, kBytesPerPacketData);
std::memcpy(input_buffer_.data() + kBytesPerPacketData, next_packet + kBytesPerPacketHeader,
kBytesPerPacketData);
BitStream combined(input_buffer_.data(), (kBitsPerPacket - kBitsPerPacketHeader) * 2);
combined.SetOffset(relative_offset - kBitsPerPacketHeader);
uint64_t frame_size = combined.Peek(kBitsPerFrameHeader);
if (frame_size == xma::kMaxFrameLength) {
data->error_status = 4;
last_error_status_ = static_cast<uint32_t>(data->error_status);
log_decode_state("split-frame-size-invalid");
return;
}
packet_info.current_frame_size_ = static_cast<uint32_t>(frame_size);
}
last_frame_size_bits_ = packet_info.current_frame_size_;
BitStream stream(current_input_buffer, (packet_index + 1) * kBitsPerPacket);
stream.SetOffset(data->input_buffer_read_offset);
const uint64_t bits_to_copy =
GetAmountOfBitsToRead(static_cast<uint32_t>(stream.BitsRemaining()),
packet_info.current_frame_size_);
last_bits_to_copy_ = static_cast<uint32_t>(bits_to_copy);
if (bits_to_copy == 0) {
REXAPU_ERROR("XmaContext {}: There are no bits to copy!", id());
last_swapped_input_buffer_ = true;
SwapInputBuffer(data);
last_input_read_offset_after_ = static_cast<uint32_t>(data->input_buffer_read_offset);
log_decode_state("zero-bits-to-copy");
return;
}
if (packet_info.isLastFrameInPacket()) {
if (stream.BitsRemaining() < packet_info.current_frame_size_) {
const uint8_t* next_packet =
GetNextPacket(data, next_packet_index, current_input_packet_count);
if (!next_packet) {
data->error_status = 4;
last_error_status_ = static_cast<uint32_t>(data->error_status);
log_decode_state("missing-next-packet-last-frame");
return;
}
last_cross_packet_copy_ = true;
std::memcpy(input_buffer_.data() + kBytesPerPacketData, next_packet + kBytesPerPacketHeader,
kBytesPerPacketData);
}
}
std::memcpy(input_buffer_.data(), packet + kBytesPerPacketHeader, kBytesPerPacketData);
stream = BitStream(input_buffer_.data(), (kBitsPerPacket - kBitsPerPacketHeader) * 2);
stream.SetOffset(relative_offset - kBitsPerPacketHeader);
xma_frame_.fill(0);
const uint32_t padding_start =
static_cast<uint8_t>(stream.Copy(xma_frame_.data() + 1, packet_info.current_frame_size_));
raw_frame_.fill(0);
PrepareDecoder(data->sample_rate, bool(data->is_stereo));
if (IsDeepTraceEnabled() &&
(last_cross_packet_copy_ || (decode_attempt_count_ % 512) == 0)) {
REXAPU_DEBUG(
"XmaContext {}: decode candidate packet_index={} next_packet={} frame_bits={} "
"bits_to_copy={} current_buffer={} packet_count={} skip={} cross_packet={} "
"sample_rate={} stereo={}",
id(), packet_index, next_packet_index, packet_info.current_frame_size_,
static_cast<uint32_t>(bits_to_copy), static_cast<uint32_t>(data->current_buffer),
current_input_packet_count,
skip_count, last_cross_packet_copy_, GetSampleRate(data->sample_rate), bool(data->is_stereo));
}
PreparePacket(packet_info.current_frame_size_, padding_start);
if (DecodePacket(av_context_, av_packet_, av_frame_)) {
ConvertFrame(reinterpret_cast<const uint8_t**>(&av_frame_->data), bool(data->is_stereo),
raw_frame_.data());
current_frame_remaining_subframes_ = 4 << data->is_stereo;
last_decode_succeeded_ = true;
if (is_loop_end_frame) {
loop_frame_output_limit_ = (data->loop_subframe_end + 1) << data->is_stereo;
} else {
loop_frame_output_limit_ = 0;
}
if (loop_start_skip_pending_) {
const uint8_t skip = data->loop_subframe_skip << data->is_stereo;
if (skip < current_frame_remaining_subframes_) {
current_frame_remaining_subframes_ -= skip;
}
loop_start_skip_pending_ = false;
}
}
if (!packet_info.isLastFrameInPacket()) {
const uint32_t next_frame_offset =
(data->input_buffer_read_offset + bits_to_copy) % kBitsPerPacket;
data->input_buffer_read_offset = (packet_index * kBitsPerPacket) + next_frame_offset;
last_input_read_offset_after_ = static_cast<uint32_t>(data->input_buffer_read_offset);
return;
}
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) {
last_swapped_input_buffer_ = true;
SwapInputBuffer(data);
} else if (next_input_offset == kBitsPerPacketHeader) {
last_swapped_input_buffer_ = true;
SwapInputBuffer(data);
if (data->IsAnyInputBufferValid()) {
next_input_offset =
xma::GetPacketFrameOffset(memory()->TranslatePhysical(data->GetCurrentInputBufferAddress()));
if (next_input_offset > kMaxFrameSizeinBits) {
log_decode_state("next-packet-frame-offset-invalid");
last_swapped_input_buffer_ = true;
SwapInputBuffer(data);
return;
}
}
}
data->input_buffer_read_offset = next_input_offset;
last_input_read_offset_after_ = static_cast<uint32_t>(data->input_buffer_read_offset);
}
void XmaContext::ConvertFrame(const uint8_t** samples, bool is_two_channel,
uint8_t* output_buffer) {
constexpr float scale = (1 << 15) - 1;
auto out = reinterpret_cast<int16_t*>(output_buffer);
#if REX_ARCH_AMD64
static_assert(kSamplesPerFrame % 8 == 0);
const auto in_channel_0 = reinterpret_cast<const float*>(samples[0]);
const __m128 scale_mm = _mm_set1_ps(scale);
if (is_two_channel) {
const auto in_channel_1 = reinterpret_cast<const float*>(samples[1]);
const __m128i shufmask = _mm_set_epi8(14, 15, 6, 7, 12, 13, 4, 5, 10, 11, 2, 3, 8, 9, 0, 1);
for (uint32_t i = 0; i < kSamplesPerFrame; i += 4) {
__m128 in_mm0 = _mm_loadu_ps(&in_channel_0[i]);
__m128 in_mm1 = _mm_loadu_ps(&in_channel_1[i]);
in_mm0 = _mm_mul_ps(in_mm0, scale_mm);
in_mm1 = _mm_mul_ps(in_mm1, scale_mm);
__m128i out_mm0 = _mm_cvtps_epi32(in_mm0);
__m128i out_mm1 = _mm_cvtps_epi32(in_mm1);
__m128i out_mm = _mm_packs_epi32(out_mm0, out_mm1);
out_mm = _mm_shuffle_epi8(out_mm, shufmask);
_mm_storeu_si128(reinterpret_cast<__m128i*>(&out[i * 2]), out_mm);
}
} else {
const __m128i shufmask = _mm_set_epi8(14, 15, 12, 13, 10, 11, 8, 9, 6, 7, 4, 5, 2, 3, 0, 1);
for (uint32_t i = 0; i < kSamplesPerFrame; i += 8) {
__m128 in_mm0 = _mm_loadu_ps(&in_channel_0[i]);
__m128 in_mm1 = _mm_loadu_ps(&in_channel_0[i + 4]);
in_mm0 = _mm_mul_ps(in_mm0, scale_mm);
in_mm1 = _mm_mul_ps(in_mm1, scale_mm);
__m128i out_mm0 = _mm_cvtps_epi32(in_mm0);
__m128i out_mm1 = _mm_cvtps_epi32(in_mm1);
__m128i out_mm = _mm_packs_epi32(out_mm0, out_mm1);
out_mm = _mm_shuffle_epi8(out_mm, shufmask);
_mm_storeu_si128(reinterpret_cast<__m128i*>(&out[i]), out_mm);
}
}
#else
uint32_t o = 0;
for (uint32_t i = 0; i < kSamplesPerFrame; i++) {
for (uint32_t j = 0; j <= uint32_t(is_two_channel); j++) {
auto in = reinterpret_cast<const float*>(samples[j]);
float scaled_sample = rex::clamp_float(in[i], -1.0f, 1.0f) * scale;
auto sample = static_cast<int16_t>(scaled_sample);
out[o++] = rex::byte_swap(sample);
}
}
#endif
}
} // namespace rex::audio
@@ -1,301 +0,0 @@
/**
******************************************************************************
* Xenia : Xbox 360 Emulator Research Project *
******************************************************************************
* Copyright 2021 Ben Vanik. All rights reserved. *
* Released under the BSD license - see LICENSE in the root for more details. *
******************************************************************************
*
* @modified Tom Clay, 2026 - Adapted for ReXGlue runtime
*/
#pragma once
#include <array>
#include <atomic>
#include <mutex>
#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
// a few (very slight) modifications.
// XMA2 is fully backwards-compatible with XMA1.
// Helpful resources:
// https://github.com/koolkdev/libertyv/blob/master/libav_wrapper/xma2dec.c
// https://hcs64.com/mboard/forum.php?showthread=14818
// https://github.com/hrydgard/minidx9/blob/master/Include/xma2defs.h
// Forward declarations
struct AVCodec;
struct AVCodecParserContext;
struct AVCodecContext;
struct AVFrame;
struct AVPacket;
namespace rex::audio {
// This is stored in guest space in big-endian order.
// We load and swap the whole thing to splat here so that we can
// use bitfields.
// This could be important:
// https://www.fmod.org/questions/question/forum-15859
// Appears to be dumped in order (for the most part)
struct XMA_CONTEXT_DATA {
// DWORD 0
uint32_t input_buffer_0_packet_count : 12; // XMASetInputBuffer0, number of
// 2KB packets. Max 4095 packets.
// These packets form a block.
uint32_t loop_count : 8; // +12bit, XMASetLoopData NumLoops
uint32_t input_buffer_0_valid : 1; // +20bit, XMAIsInputBuffer0Valid
uint32_t input_buffer_1_valid : 1; // +21bit, XMAIsInputBuffer1Valid
uint32_t output_buffer_block_count : 5; // +22bit SizeWrite 256byte blocks
uint32_t output_buffer_write_offset : 5; // +27bit
// XMAGetOutputBufferWriteOffset
// AKA OffsetWrite
// DWORD 1
uint32_t input_buffer_1_packet_count : 12; // XMASetInputBuffer1, number of
// 2KB packets. Max 4095 packets.
// These packets form a block.
uint32_t loop_subframe_start : 2; // +12bit, XMASetLoopData
uint32_t loop_subframe_end : 3; // +14bit, XMASetLoopData
uint32_t loop_subframe_skip : 3; // +17bit, XMASetLoopData might be
// subframe_decode_count
uint32_t subframe_decode_count : 4; // +20bit
uint32_t output_buffer_padding : 3; // +24bit, extra output buffer blocks
// reserved per decoded frame
uint32_t sample_rate : 2; // +27bit enum of sample rates
uint32_t is_stereo : 1; // +29bit
uint32_t unk_dword_1_c : 1; // +30bit
uint32_t output_buffer_valid : 1; // +31bit, XMAIsOutputBufferValid
// DWORD 2
uint32_t input_buffer_read_offset : 26; // XMAGetInputBufferReadOffset
uint32_t error_status : 5; // ErrorStatus
uint32_t error_set : 1; // ErrorSet
// DWORD 3
uint32_t loop_start : 26; // XMASetLoopData LoopStartOffset
// frame offset in bits
uint32_t parser_error_status : 5; // ParserErrorStatus
uint32_t parser_error_set : 1; // ParserErrorSet
// DWORD 4
uint32_t loop_end : 26; // XMASetLoopData LoopEndOffset
// frame offset in bits
uint32_t packet_metadata : 5; // XMAGetPacketMetadata
uint32_t current_buffer : 1; // ?
// DWORD 5
uint32_t input_buffer_0_ptr; // physical address
// DWORD 6
uint32_t input_buffer_1_ptr; // physical address
// DWORD 7
uint32_t output_buffer_ptr; // physical address
// DWORD 8
uint32_t work_buffer_ptr; // PtrOverlapAdd(?)
// DWORD 9
// +0bit, XMAGetOutputBufferReadOffset AKA WriteBufferOffsetRead
uint32_t output_buffer_read_offset : 5;
uint32_t : 25;
uint32_t stop_when_done : 1; // +30bit
uint32_t interrupt_when_done : 1; // +31bit
// DWORD 10-15
uint32_t unk_dwords_10_15[6]; // reserved?
explicit XMA_CONTEXT_DATA(const void* ptr) {
memory::copy_and_swap(reinterpret_cast<uint32_t*>(this), reinterpret_cast<const uint32_t*>(ptr),
sizeof(XMA_CONTEXT_DATA) / 4);
}
void Store(void* ptr) {
memory::copy_and_swap(reinterpret_cast<uint32_t*>(ptr), reinterpret_cast<const uint32_t*>(this),
sizeof(XMA_CONTEXT_DATA) / 4);
}
bool IsInputBufferValid(uint8_t buffer_index) const {
return buffer_index == 0 ? input_buffer_0_valid : input_buffer_1_valid;
}
bool IsCurrentInputBufferValid() const { return IsInputBufferValid(current_buffer); }
bool IsAnyInputBufferValid() const { return input_buffer_0_valid || input_buffer_1_valid; }
uint32_t GetInputBufferAddress(uint8_t buffer_index) const {
return buffer_index == 0 ? input_buffer_0_ptr : input_buffer_1_ptr;
}
uint32_t GetCurrentInputBufferAddress() const { return GetInputBufferAddress(current_buffer); }
uint32_t GetInputBufferPacketCount(uint8_t buffer_index) const {
return buffer_index == 0 ? input_buffer_0_packet_count : input_buffer_1_packet_count;
}
uint32_t GetCurrentInputBufferPacketCount() const {
return GetInputBufferPacketCount(current_buffer);
}
bool IsConsumeOnlyContext() const {
return (input_buffer_0_packet_count | input_buffer_1_packet_count) == 0;
}
};
static_assert_size(XMA_CONTEXT_DATA, 64);
#pragma pack(push, 1)
// XMA2WAVEFORMATEX
struct Xma2ExtraData {
uint8_t raw[34];
};
static_assert_size(Xma2ExtraData, 34);
#pragma pack(pop)
struct kPacketInfo {
uint8_t frame_count_ = 0;
uint8_t current_frame_ = 0;
uint32_t current_frame_size_ = 0;
bool isLastFrameInPacket() const {
return frame_count_ == 0 || current_frame_ == frame_count_ - 1;
}
};
static constexpr int kIdToSampleRate[4] = {24000, 32000, 44100, 48000};
class XmaContext {
public:
static const uint32_t kBytesPerPacket = 2048;
static const uint32_t kBitsPerPacket = kBytesPerPacket * 8;
static const uint32_t kBitsPerPacketHeader = 32;
static const uint32_t kBitsPerFrameHeader = 15;
static const uint32_t kBytesPerPacketHeader = 4;
static const uint32_t kBytesPerPacketData = kBytesPerPacket - kBytesPerPacketHeader;
static const uint32_t kBytesPerSample = 2;
static const uint32_t kSamplesPerFrame = 512;
static const uint32_t kSamplesPerSubframe = 128;
static const uint32_t kBytesPerFrameChannel = kSamplesPerFrame * kBytesPerSample;
static const uint32_t kBytesPerSubframeChannel = kSamplesPerSubframe * kBytesPerSample;
static const uint32_t kOutputBytesPerBlock = 256;
static const uint32_t kOutputMaxSizeBytes = 31 * kOutputBytesPerBlock;
static const uint32_t kMaxFrameSizeinBits = 0x4000 - kBitsPerPacketHeader;
explicit XmaContext();
~XmaContext();
int Setup(uint32_t id, memory::Memory* memory, uint32_t guest_ptr);
bool Work();
void Enable();
bool Block(bool poll);
void Clear();
void Disable();
void Release();
memory::Memory* memory() const { return memory_; }
uint32_t id() { return id_; }
uint32_t guest_ptr() { return guest_ptr_; }
bool is_allocated() { return is_allocated_.load(std::memory_order_acquire); }
bool is_enabled() { return is_enabled_.load(std::memory_order_acquire); }
void set_is_allocated(bool is_allocated) {
is_allocated_.store(is_allocated, std::memory_order_release);
}
void set_is_enabled(bool is_enabled) { is_enabled_.store(is_enabled, std::memory_order_release); }
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 int GetSampleRate(int id);
static int16_t GetPacketNumber(size_t size, size_t bit_offset);
static uint32_t GetCurrentInputBufferSize(XMA_CONTEXT_DATA* data);
kPacketInfo GetPacketInfo(uint8_t* packet, uint32_t frame_offset);
uint32_t GetAmountOfBitsToRead(uint32_t remaining_stream_bits, uint32_t frame_size);
const uint8_t* GetNextPacket(XMA_CONTEXT_DATA* data, uint32_t next_packet_index,
uint32_t current_input_packet_count);
uint32_t GetNextPacketReadOffset(XMA_CONTEXT_DATA* data, uint32_t next_packet_index,
uint32_t current_input_packet_count);
uint8_t* GetCurrentInputBuffer(XMA_CONTEXT_DATA* data);
void Decode(XMA_CONTEXT_DATA* data);
void Consume(memory::RingBuffer* output_rb, const XMA_CONTEXT_DATA* data);
void UpdateLoopStatus(XMA_CONTEXT_DATA* data);
void ClearLocked(XMA_CONTEXT_DATA* data);
memory::RingBuffer PrepareOutputRingBuffer(XMA_CONTEXT_DATA* data);
int PrepareDecoder(int sample_rate, bool is_two_channel);
void PreparePacket(uint32_t frame_size, uint32_t frame_padding);
bool DecodePacket(AVCodecContext* av_context, const AVPacket* av_packet, AVFrame* av_frame);
void StoreContextMerged(const XMA_CONTEXT_DATA& data, const XMA_CONTEXT_DATA& initial_data,
uint8_t* context_ptr);
static void ConvertFrame(const uint8_t** samples, bool is_two_channel, uint8_t* output_buffer);
memory::Memory* memory_ = nullptr;
std::unique_ptr<rex::thread::Event> work_completion_event_;
uint32_t id_ = 0;
uint32_t guest_ptr_ = 0;
std::mutex lock_;
std::atomic<bool> is_allocated_ = false;
std::atomic<bool> is_enabled_ = false;
// ffmpeg structures
AVPacket* av_packet_ = nullptr;
AVCodec* av_codec_ = nullptr;
AVCodecContext* av_context_ = nullptr;
AVFrame* av_frame_ = nullptr;
// Packet data buffer (two packets worth for split frame handling)
std::array<uint8_t, kBytesPerPacketData * 2> input_buffer_;
// First byte contains bit offset information
std::array<uint8_t, 1 + 4096> xma_frame_;
// Conversion buffer for up to 2-channel frame
std::array<uint8_t, kBytesPerFrameChannel * 2> raw_frame_;
// Output buffer tracking
int32_t remaining_subframe_blocks_in_output_buffer_ = 0;
uint8_t current_frame_remaining_subframes_ = 0;
// Loop subframe precision state
uint8_t loop_frame_output_limit_ = 0;
bool loop_start_skip_pending_ = false;
// Last decode snapshot for cutscene/XMA corruption diagnostics.
uint64_t decode_attempt_count_ = 0;
uint32_t last_input_read_offset_before_ = 0;
uint32_t last_input_read_offset_after_ = 0;
uint32_t last_current_input_packet_count_ = 0;
uint32_t last_frame_size_bits_ = 0;
uint32_t last_bits_to_copy_ = 0;
uint32_t last_next_packet_index_ = 0;
uint8_t last_current_buffer_ = 0;
uint8_t last_skip_count_ = 0;
int32_t last_packet_index_ = -1;
bool last_cross_packet_copy_ = false;
bool last_swapped_input_buffer_ = false;
bool last_decode_succeeded_ = false;
uint32_t last_error_status_ = 0;
};
} // namespace rex::audio
@@ -1,307 +0,0 @@
/**
******************************************************************************
* Xenia : Xbox 360 Emulator Research Project *
******************************************************************************
* Copyright 2022 Ben Vanik. All rights reserved. *
* Released under the BSD license - see LICENSE in the root for more details. *
******************************************************************************
*
* @modified Tom Clay, 2026 - Adapted for ReXGlue runtime
*/
#include <chrono>
#include <rex/audio/xma/context.h>
#include <rex/audio/xma/decoder.h>
#include <rex/cvar.h>
#include <rex/dbg.h>
#include <rex/logging.h>
#include <rex/math.h>
#include <rex/memory/ring_buffer.h>
#include <rex/string/buffer.h>
#include <rex/system/function_dispatcher.h>
#include <rex/system/thread_state.h>
#include <rex/system/xthread.h>
extern "C" {
#include "libavutil/log.h"
} // extern "C"
REXCVAR_DECLARE(bool, ffmpeg_verbose);
namespace rex::audio {
XmaDecoder::XmaDecoder(runtime::FunctionDispatcher* function_dispatcher)
: memory_(function_dispatcher->memory()), function_dispatcher_(function_dispatcher) {}
XmaDecoder::~XmaDecoder() = default;
void av_log_callback(void* avcl, int level, const char* fmt, va_list va) {
if (!REXCVAR_GET(ffmpeg_verbose) && level > AV_LOG_WARNING) {
return;
}
string::StringBuffer buff;
buff.AppendVarargs(fmt, va);
auto msg = buff.to_string_view();
switch (level) {
case AV_LOG_ERROR:
REXAPU_ERROR("ffmpeg: {}", msg);
break;
case AV_LOG_WARNING:
REXAPU_WARN("ffmpeg: {}", msg);
break;
case AV_LOG_INFO:
REXAPU_INFO("ffmpeg: {}", msg);
break;
case AV_LOG_VERBOSE:
case AV_LOG_DEBUG:
default:
REXAPU_DEBUG("ffmpeg: {}", msg);
break;
}
}
X_STATUS XmaDecoder::Setup(system::KernelState* kernel_state) {
av_log_set_callback(av_log_callback);
memory()->AddVirtualMappedRange(
0x7FEA0000, 0xFFFF0000, 0x0000FFFF, this,
reinterpret_cast<runtime::MMIOReadCallback>(MMIOReadRegisterThunk),
reinterpret_cast<runtime::MMIOWriteCallback>(MMIOWriteRegisterThunk));
REXAPU_DEBUG("XMA: Registered MMIO handlers at 0x7FEA0000-0x7FEAFFFF");
context_data_first_ptr_ =
memory()->SystemHeapAlloc(sizeof(XMA_CONTEXT_DATA) * kContextCount, 256,
memory::kSystemHeapPhysical);
context_data_last_ptr_ =
context_data_first_ptr_ + (sizeof(XMA_CONTEXT_DATA) * kContextCount - 1);
register_file_[XmaRegister::ContextArrayAddress] =
memory()->GetPhysicalAddress(context_data_first_ptr_);
for (size_t i = 0; i < kContextCount; ++i) {
uint32_t guest_ptr = context_data_first_ptr_ + i * sizeof(XMA_CONTEXT_DATA);
XmaContext& context = contexts_[i];
if (context.Setup(i, memory(), guest_ptr)) {
assert_always();
}
}
register_file_[XmaRegister::NextContextIndex] = 1;
context_bitmap_.Resize(kContextCount);
worker_running_ = true;
work_event_ = rex::thread::Event::CreateAutoResetEvent(false);
assert_not_null(work_event_);
worker_thread_ = system::object_ref<system::XHostThread>(
new system::XHostThread(kernel_state, 128 * 1024, 0, [this]() {
WorkerThreadMain();
return 0;
}));
worker_thread_->set_name("XMA Decoder");
worker_thread_->Create();
return X_STATUS_SUCCESS;
}
void XmaDecoder::WorkerThreadMain() {
while (worker_running_) {
bool did_work = false;
for (uint32_t n = 0; n < kContextCount && worker_running_; n++) {
XmaContext& context = contexts_[n];
bool worked = context.Work();
if (worked) {
context.SignalWorkDone();
}
did_work = did_work || worked;
}
if (paused_) {
pause_fence_.Signal();
resume_fence_.Wait();
}
if (did_work) {
continue;
}
rex::thread::Wait(work_event_.get(), false);
}
}
void XmaDecoder::Shutdown() {
if (!worker_thread_) {
return;
}
worker_running_ = false;
if (work_event_) {
work_event_->Set();
}
if (paused_) {
Resume();
}
auto result =
rex::thread::Wait(worker_thread_->thread(), false, std::chrono::milliseconds(2000));
if (result == rex::thread::WaitResult::kTimeout) {
REXAPU_WARN("XMA: Worker thread did not exit within 2s, abandoning");
}
worker_thread_.reset();
if (context_data_first_ptr_) {
memory()->SystemHeapFree(context_data_first_ptr_);
}
context_data_first_ptr_ = 0;
context_data_last_ptr_ = 0;
}
int XmaDecoder::GetContextId(uint32_t guest_ptr) {
static_assert_size(XMA_CONTEXT_DATA, 64);
if (guest_ptr < context_data_first_ptr_ || guest_ptr > context_data_last_ptr_) {
return -1;
}
assert_zero(guest_ptr & 0x3F);
return (guest_ptr - context_data_first_ptr_) >> 6;
}
uint32_t XmaDecoder::AllocateContext() {
size_t index = context_bitmap_.Acquire();
if (index == -1) {
return 0;
}
XmaContext& context = contexts_[index];
assert_false(context.is_allocated());
context.set_is_allocated(true);
return context.guest_ptr();
}
void XmaDecoder::ReleaseContext(uint32_t guest_ptr) {
auto context_id = GetContextId(guest_ptr);
assert_true(context_id >= 0);
XmaContext& context = contexts_[context_id];
assert_true(context.is_allocated());
context.Release();
context_bitmap_.Release(context_id);
}
bool XmaDecoder::BlockOnContext(uint32_t guest_ptr, bool poll) {
auto context_id = GetContextId(guest_ptr);
assert_true(context_id >= 0);
XmaContext& context = contexts_[context_id];
return context.Block(poll);
}
uint32_t XmaDecoder::ReadRegister(uint32_t addr) {
auto r = (addr & 0xFFFF) / 4;
assert_true(r < XmaRegisterFile::kRegisterCount);
switch (r) {
case XmaRegister::ContextArrayAddress:
break;
case XmaRegister::CurrentContextIndex: {
uint32_t& current_context_index = register_file_[XmaRegister::CurrentContextIndex];
uint32_t& next_context_index = register_file_[XmaRegister::NextContextIndex];
current_context_index = next_context_index;
next_context_index = (next_context_index + 1) % kContextCount;
break;
}
default:
const auto register_info = register_file_.GetRegisterInfo(r);
if (register_info) {
REXAPU_DEBUG("XMA: Read from unhandled register ({:04X}, {})", r, register_info->name);
} else {
REXAPU_DEBUG("XMA: Read from unknown register ({:04X})", r);
}
break;
}
return rex::byte_swap(register_file_[r]);
}
void XmaDecoder::WriteRegister(uint32_t addr, uint32_t value) {
SCOPE_profile_cpu_f("apu");
uint32_t r = (addr & 0xFFFF) / 4;
value = rex::byte_swap(value);
assert_true(r < XmaRegisterFile::kRegisterCount);
register_file_[r] = value;
if (r >= XmaRegister::Context0Kick && r <= XmaRegister::Context9Kick) {
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;
auto& context = contexts_[context_id];
context.Enable();
}
}
work_event_->Set();
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) {
uint32_t base_context_id = (r - XmaRegister::Context0Lock) * 32;
for (int i = 0; value && i < 32; ++i, value >>= 1) {
if (value & 1) {
uint32_t context_id = base_context_id + i;
auto& context = contexts_[context_id];
context.Disable();
}
}
} else if (r >= XmaRegister::Context0Clear && r <= XmaRegister::Context9Clear) {
uint32_t base_context_id = (r - XmaRegister::Context0Clear) * 32;
for (int i = 0; value && i < 32; ++i, value >>= 1) {
if (value & 1) {
uint32_t context_id = base_context_id + i;
XmaContext& context = contexts_[context_id];
context.Clear();
}
}
} else {
switch (r) {
default: {
const auto register_info = register_file_.GetRegisterInfo(r);
if (register_info) {
REXAPU_DEBUG("XMA: Write to unhandled register ({:04X}, {}): {:08X}", r,
register_info->name, value);
} else {
REXAPU_DEBUG("XMA: Write to unknown register ({:04X}): {:08X}", r, value);
}
break;
}
#pragma warning(suppress : 4065)
}
}
}
void XmaDecoder::Pause() {
if (paused_) {
return;
}
paused_ = true;
pause_fence_.Wait();
}
void XmaDecoder::Resume() {
if (!paused_) {
return;
}
paused_ = false;
resume_fence_.Signal();
}
} // namespace rex::audio
@@ -1,92 +0,0 @@
/**
******************************************************************************
* Xenia : Xbox 360 Emulator Research Project *
******************************************************************************
* Copyright 2021 Ben Vanik. All rights reserved. *
* Released under the BSD license - see LICENSE in the root for more details. *
******************************************************************************
*
* @modified Tom Clay, 2026 - Adapted for ReXGlue runtime
*/
#pragma once
#include <atomic>
#include <mutex>
#include <queue>
#include <rex/audio/xma/context.h>
#include <rex/audio/xma/register_file.h>
#include <rex/bit.h>
#include <rex/kernel.h>
#include <rex/system/xthread.h>
namespace rex::runtime {
class FunctionDispatcher;
} // namespace rex::runtime
namespace rex::audio {
struct XMA_CONTEXT_DATA;
class XmaDecoder {
public:
explicit XmaDecoder(runtime::FunctionDispatcher* function_dispatcher);
~XmaDecoder();
memory::Memory* memory() const { return memory_; }
runtime::FunctionDispatcher* function_dispatcher() const { return function_dispatcher_; }
X_STATUS Setup(system::KernelState* kernel_state);
void Shutdown();
uint32_t context_array_ptr() const { return register_file_[XmaRegister::ContextArrayAddress]; }
uint32_t AllocateContext();
void ReleaseContext(uint32_t guest_ptr);
bool BlockOnContext(uint32_t guest_ptr, bool poll);
uint32_t ReadRegister(uint32_t addr);
void WriteRegister(uint32_t addr, uint32_t value);
bool is_paused() const { return paused_; }
void Pause();
void Resume();
protected:
int GetContextId(uint32_t guest_ptr);
private:
void WorkerThreadMain();
static uint32_t MMIOReadRegisterThunk(void* ppc_context, XmaDecoder* as, uint32_t addr) {
return as->ReadRegister(addr);
}
static void MMIOWriteRegisterThunk(void* ppc_context, XmaDecoder* as, uint32_t addr,
uint32_t value) {
as->WriteRegister(addr, value);
}
protected:
memory::Memory* memory_ = nullptr;
runtime::FunctionDispatcher* function_dispatcher_ = nullptr;
std::atomic<bool> worker_running_ = {false};
system::object_ref<system::XHostThread> worker_thread_;
std::unique_ptr<rex::thread::Event> work_event_ = nullptr;
std::atomic<bool> paused_ = false;
rex::thread::Fence pause_fence_; // Signaled when worker paused.
rex::thread::Fence resume_fence_; // Signaled when resume requested.
XmaRegisterFile register_file_;
static const uint32_t kContextCount = 320;
XmaContext contexts_[kContextCount];
bit::BitMap context_bitmap_;
uint32_t context_data_first_ptr_ = 0;
uint32_t context_data_last_ptr_ = 0;
};
} // namespace rex::audio
@@ -1,46 +0,0 @@
/**
******************************************************************************
* Xenia : Xbox 360 Emulator Research Project *
******************************************************************************
* Copyright 2021 Ben Vanik. All rights reserved. *
* Released under the BSD license - see LICENSE in the root for more details. *
******************************************************************************
*
* @modified Tom Clay, 2026 - Adapted for ReXGlue runtime
*/
// This file contains some functions used to help parse XMA data.
#pragma once
#include <stdint.h>
namespace rex::audio::xma {
static constexpr uint32_t kMaxFrameLength = 0x7FFF;
// Get number of frames that /begin/ in this packet. Valid only for XMA2 packets.
inline uint8_t GetPacketFrameCount(const uint8_t* packet) {
return packet[0] >> 2;
}
// Get the first frame offset in bits
inline uint32_t GetPacketFrameOffset(const uint8_t* packet) {
uint32_t val =
static_cast<uint16_t>(((packet[0] & 0x3) << 13) | (packet[1] << 5) | (packet[2] >> 3));
return val + 32;
}
inline uint8_t GetPacketMetadata(const uint8_t* packet) {
return packet[2] & 0x7;
}
inline bool IsPacketXma2Type(const uint8_t* packet) {
return GetPacketMetadata(packet) == 1;
}
inline uint8_t GetPacketSkipCount(const uint8_t* packet) {
return packet[3];
}
} // namespace rex::audio::xma
@@ -1,39 +0,0 @@
/**
******************************************************************************
* Xenia : Xbox 360 Emulator Research Project *
******************************************************************************
* Copyright 2021 Ben Vanik. All rights reserved. *
* Released under the BSD license - see LICENSE in the root for more details. *
******************************************************************************
*
* @modified Tom Clay, 2026 - Adapted for ReXGlue runtime
*/
#include <cstring>
#include <rex/audio/xma/register_file.h>
#include <rex/math.h>
namespace rex::audio {
XmaRegisterFile::XmaRegisterFile() {
std::memset(values, 0, sizeof(values));
}
const XmaRegisterInfo* XmaRegisterFile::GetRegisterInfo(uint32_t index) {
switch (index) {
#define XE_XMA_REGISTER(index, name) \
case index: { \
static const XmaRegisterInfo reg_info = { \
#name, \
}; \
return &reg_info; \
}
#include <rex/audio/xma/register_table.inc>
#undef XE_XMA_REGISTER
default:
return nullptr;
}
}
} // namespace rex::audio
@@ -1,42 +0,0 @@
/**
******************************************************************************
* Xenia : Xbox 360 Emulator Research Project *
******************************************************************************
* Copyright 2021 Ben Vanik. All rights reserved. *
* Released under the BSD license - see LICENSE in the root for more details. *
******************************************************************************
*
* @modified Tom Clay, 2026 - Adapted for ReXGlue runtime
*/
#pragma once
#include <cstdint>
#include <cstdlib>
namespace rex::audio {
struct XmaRegister {
#define XE_XMA_REGISTER(index, name) static const uint32_t name = index;
#include <rex/audio/xma/register_table.inc>
#undef XE_XMA_REGISTER
};
struct XmaRegisterInfo {
const char* name;
};
class XmaRegisterFile {
public:
XmaRegisterFile();
static const XmaRegisterInfo* GetRegisterInfo(uint32_t index);
static const size_t kRegisterCount = (0xFFFF + 1) / 4;
uint32_t values[kRegisterCount];
uint32_t operator[](uint32_t reg) const { return values[reg]; }
uint32_t& operator[](uint32_t reg) { return values[reg]; }
};
} // namespace rex::audio
@@ -1,81 +0,0 @@
/**
******************************************************************************
* Xenia : Xbox 360 Emulator Research Project *
******************************************************************************
* Copyright 2021 Ben Vanik. All rights reserved. *
* Released under the BSD license - see LICENSE in the root for more details. *
******************************************************************************
*/
// This is a partial file designed to be included by other files when
// constructing various tables.
#ifndef XE_XMA_REGISTER
#define XE_XMA_REGISTER(index, name)
#define __XE_XMA_REGISTER_UNSET
#endif
#ifndef XE_XMA_REGISTER_CONTEXT_GROUP
#define XE_XMA_REGISTER_CONTEXT_GROUP(index, suffix) \
XE_XMA_REGISTER(index + 0, Context0##suffix) \
XE_XMA_REGISTER(index + 1, Context1##suffix) \
XE_XMA_REGISTER(index + 2, Context2##suffix) \
XE_XMA_REGISTER(index + 3, Context3##suffix) \
XE_XMA_REGISTER(index + 4, Context4##suffix) \
XE_XMA_REGISTER(index + 5, Context5##suffix) \
XE_XMA_REGISTER(index + 6, Context6##suffix) \
XE_XMA_REGISTER(index + 7, Context7##suffix) \
XE_XMA_REGISTER(index + 8, Context8##suffix) \
XE_XMA_REGISTER(index + 9, Context9##suffix)
#endif
// 0x0000..0x001F : ???
// 0x0020..0x03FF : all 0xFFs?
// 0x0400..0x043F : ???
// 0x0440..0x047F : all 0xFFs?
// 0x0480..0x048B : ???
// 0x048C..0x04C0 : all 0xFFs?
// 0x04C1..0x04CB : ???
// 0x04CC..0x04FF : all 0xFFs?
// 0x0500..0x051F : ???
// 0x0520..0x057F : all 0xFFs?
// 0x0580..0x058F : ???
// 0x0590..0x05FF : all 0xFFs?
// XMA stuff is probably only 0x0600..0x06FF
//---------------------------------------------------------------------------//
XE_XMA_REGISTER(0x0600, ContextArrayAddress)
// 0x0601..0x0605 : ???
XE_XMA_REGISTER(0x0606, CurrentContextIndex)
XE_XMA_REGISTER(0x0607, NextContextIndex)
// 0x0608 : ???
// 0x0609..0x060F : zero?
XE_XMA_REGISTER_CONTEXT_GROUP(0x0610, Unknown610)
// 0x061A..0x061F : zero?
XE_XMA_REGISTER_CONTEXT_GROUP(0x0620, Unknown620)
// 0x062A..0x0641 : zero?
// 0x0642..0x0644 : ???
// 0x0645..0x064F : zero?
XE_XMA_REGISTER_CONTEXT_GROUP(0x0650, Kick)
// 0x065A..0x065F : zero?
XE_XMA_REGISTER_CONTEXT_GROUP(0x0660, Unknown660)
// 0x066A..0x0681 : zero?
// 0x0682..0x0684 : ???
// 0x0685..0x068F : zero?
XE_XMA_REGISTER_CONTEXT_GROUP(0x0690, Lock)
// 0x069A..0x069F : zero?
XE_XMA_REGISTER_CONTEXT_GROUP(0x06A0, Clear)
//---------------------------------------------------------------------------//
// 0x0700..0x07FF : all 0xFFs?
// 0x0800..0x17FF : ???
// 0x1800..0x2FFF : all 0xFFs?
// 0x3000..0x30FF : ???
// 0x3100..0x3FFF : all 0xFFs?
#ifdef __XE_XMA_REGISTER_UNSET
#undef __XE_XMA_REGISTER_UNSET
#undef XE_XMA_REGISTER
#endif
@@ -1,383 +0,0 @@
/**
******************************************************************************
* Xenia : Xbox 360 Emulator Research Project *
******************************************************************************
* Copyright 2026 Tom Clay. All rights reserved. *
* Released under the BSD license - see LICENSE in the root for more details. *
******************************************************************************
*/
#include <cstring>
#include <rex/audio/xma/xma_context_pool.h>
#include <rex/kernel.h>
#include <rex/stream.h>
namespace rex::audio::xma {
XmaContextPool::XmaContextPool() = default;
XmaContextPool::~XmaContextPool() = default;
void XmaContextPool::Setup(memory::Memory* memory, AudioTraceBuffer* trace_buffer) {
std::lock_guard<std::mutex> lock(mutex_);
memory_ = memory;
trace_buffer_ = trace_buffer;
records_.clear();
tick_counter_ = 0;
}
void XmaContextPool::Shutdown() {
std::lock_guard<std::mutex> lock(mutex_);
if (memory_) {
for (const auto& [guest_context_ptr, record] : records_) {
memory_->SystemHeapFree(guest_context_ptr);
if (trace_buffer_) {
trace_buffer_->Record(AudioTraceSubsystem::kXma, AudioTraceEventType::kXmaReleased,
guest_context_ptr);
}
}
}
records_.clear();
}
uint32_t XmaContextPool::AllocateContext() {
std::lock_guard<std::mutex> lock(mutex_);
if (!memory_) {
return 0;
}
const uint32_t guest_context_ptr =
memory_->SystemHeapAlloc(sizeof(XMA_CONTEXT_DATA), 256, memory::kSystemHeapPhysical);
if (guest_context_ptr == 0) {
return 0;
}
std::memset(memory_->TranslateVirtual(guest_context_ptr), 0, sizeof(XMA_CONTEXT_DATA));
XmaContextRecord record;
record.guest_context_ptr = guest_context_ptr;
record.last_update_ticks = NextTickLocked();
records_[guest_context_ptr] = record;
if (trace_buffer_) {
trace_buffer_->Record(AudioTraceSubsystem::kXma, AudioTraceEventType::kXmaAllocated,
guest_context_ptr);
}
return guest_context_ptr;
}
bool XmaContextPool::ReleaseContext(const uint32_t guest_context_ptr) {
std::lock_guard<std::mutex> lock(mutex_);
auto* record = LookupRecordLocked(guest_context_ptr);
if (!record || !memory_) {
return false;
}
memory_->SystemHeapFree(guest_context_ptr);
records_.erase(guest_context_ptr);
if (trace_buffer_) {
trace_buffer_->Record(AudioTraceSubsystem::kXma, AudioTraceEventType::kXmaReleased,
guest_context_ptr);
}
return true;
}
bool XmaContextPool::InitializeContext(const uint32_t guest_context_ptr,
const XMA_CONTEXT_DATA& context_data) {
std::lock_guard<std::mutex> lock(mutex_);
auto* record = LookupRecordLocked(guest_context_ptr);
if (!record || !memory_) {
return false;
}
std::memset(memory_->TranslateVirtual(guest_context_ptr), 0, sizeof(XMA_CONTEXT_DATA));
record->initialized = true;
record->enabled = false;
record->blocked = false;
record->input_buffer_0_physical_address = context_data.input_buffer_0_ptr;
record->input_buffer_1_physical_address = context_data.input_buffer_1_ptr;
record->output_buffer_physical_address = context_data.output_buffer_ptr;
record->input_buffer_0_packet_count = context_data.input_buffer_0_packet_count;
record->input_buffer_1_packet_count = context_data.input_buffer_1_packet_count;
record->output_buffer_block_count = context_data.output_buffer_block_count;
return WriteContextDataLocked(guest_context_ptr, context_data, context_data.input_buffer_0_ptr,
context_data.output_buffer_ptr);
}
bool XmaContextPool::SetLoopData(const uint32_t guest_context_ptr, const XMA_CONTEXT_DATA& loop_data) {
std::lock_guard<std::mutex> lock(mutex_);
if (!LookupRecordLocked(guest_context_ptr) || !memory_) {
return false;
}
XMA_CONTEXT_DATA data(memory_->TranslateVirtual(guest_context_ptr));
data.loop_start = loop_data.loop_start;
data.loop_end = loop_data.loop_end;
data.loop_count = loop_data.loop_count;
data.loop_subframe_end = loop_data.loop_subframe_end;
data.loop_subframe_skip = loop_data.loop_subframe_skip;
return WriteContextDataLocked(guest_context_ptr, data, data.loop_start, data.loop_end);
}
uint32_t XmaContextPool::GetInputBufferReadOffset(const uint32_t guest_context_ptr) const {
std::lock_guard<std::mutex> lock(mutex_);
if (!LookupRecordLocked(guest_context_ptr) || !memory_) {
return 0;
}
XMA_CONTEXT_DATA data(memory_->TranslateVirtual(guest_context_ptr));
return data.input_buffer_read_offset;
}
bool XmaContextPool::SetInputBufferReadOffset(const uint32_t guest_context_ptr,
const uint32_t value) {
std::lock_guard<std::mutex> lock(mutex_);
if (!LookupRecordLocked(guest_context_ptr) || !memory_) {
return false;
}
XMA_CONTEXT_DATA data(memory_->TranslateVirtual(guest_context_ptr));
data.input_buffer_read_offset = value;
return WriteContextDataLocked(guest_context_ptr, data, value, 0);
}
bool XmaContextPool::SetInputBuffer0(const uint32_t guest_context_ptr,
const uint32_t buffer_physical_address,
const uint32_t packet_count) {
std::lock_guard<std::mutex> lock(mutex_);
auto* record = LookupRecordLocked(guest_context_ptr);
if (!record || !memory_) {
return false;
}
XMA_CONTEXT_DATA data(memory_->TranslateVirtual(guest_context_ptr));
data.input_buffer_0_ptr = buffer_physical_address;
data.input_buffer_0_packet_count = packet_count;
record->input_buffer_0_physical_address = buffer_physical_address;
record->input_buffer_0_packet_count = packet_count;
return WriteContextDataLocked(guest_context_ptr, data, buffer_physical_address, packet_count);
}
bool XmaContextPool::IsInputBuffer0Valid(const uint32_t guest_context_ptr) const {
std::lock_guard<std::mutex> lock(mutex_);
if (!LookupRecordLocked(guest_context_ptr) || !memory_) {
return false;
}
return XMA_CONTEXT_DATA(memory_->TranslateVirtual(guest_context_ptr)).input_buffer_0_valid != 0;
}
bool XmaContextPool::SetInputBuffer0Valid(const uint32_t guest_context_ptr, const bool valid) {
std::lock_guard<std::mutex> lock(mutex_);
if (!LookupRecordLocked(guest_context_ptr) || !memory_) {
return false;
}
XMA_CONTEXT_DATA data(memory_->TranslateVirtual(guest_context_ptr));
data.input_buffer_0_valid = valid ? 1 : 0;
return WriteContextDataLocked(guest_context_ptr, data, valid ? 1u : 0u, 0);
}
bool XmaContextPool::SetInputBuffer1(const uint32_t guest_context_ptr,
const uint32_t buffer_physical_address,
const uint32_t packet_count) {
std::lock_guard<std::mutex> lock(mutex_);
auto* record = LookupRecordLocked(guest_context_ptr);
if (!record || !memory_) {
return false;
}
XMA_CONTEXT_DATA data(memory_->TranslateVirtual(guest_context_ptr));
data.input_buffer_1_ptr = buffer_physical_address;
data.input_buffer_1_packet_count = packet_count;
record->input_buffer_1_physical_address = buffer_physical_address;
record->input_buffer_1_packet_count = packet_count;
return WriteContextDataLocked(guest_context_ptr, data, buffer_physical_address, packet_count);
}
bool XmaContextPool::IsInputBuffer1Valid(const uint32_t guest_context_ptr) const {
std::lock_guard<std::mutex> lock(mutex_);
if (!LookupRecordLocked(guest_context_ptr) || !memory_) {
return false;
}
return XMA_CONTEXT_DATA(memory_->TranslateVirtual(guest_context_ptr)).input_buffer_1_valid != 0;
}
bool XmaContextPool::SetInputBuffer1Valid(const uint32_t guest_context_ptr, const bool valid) {
std::lock_guard<std::mutex> lock(mutex_);
if (!LookupRecordLocked(guest_context_ptr) || !memory_) {
return false;
}
XMA_CONTEXT_DATA data(memory_->TranslateVirtual(guest_context_ptr));
data.input_buffer_1_valid = valid ? 1 : 0;
return WriteContextDataLocked(guest_context_ptr, data, valid ? 1u : 0u, 1);
}
bool XmaContextPool::IsOutputBufferValid(const uint32_t guest_context_ptr) const {
std::lock_guard<std::mutex> lock(mutex_);
if (!LookupRecordLocked(guest_context_ptr) || !memory_) {
return false;
}
return XMA_CONTEXT_DATA(memory_->TranslateVirtual(guest_context_ptr)).output_buffer_valid != 0;
}
bool XmaContextPool::SetOutputBufferValid(const uint32_t guest_context_ptr, const bool valid) {
std::lock_guard<std::mutex> lock(mutex_);
if (!LookupRecordLocked(guest_context_ptr) || !memory_) {
return false;
}
XMA_CONTEXT_DATA data(memory_->TranslateVirtual(guest_context_ptr));
data.output_buffer_valid = valid ? 1 : 0;
return WriteContextDataLocked(guest_context_ptr, data, valid ? 1u : 0u, 2);
}
uint32_t XmaContextPool::GetOutputBufferReadOffset(const uint32_t guest_context_ptr) const {
std::lock_guard<std::mutex> lock(mutex_);
if (!LookupRecordLocked(guest_context_ptr) || !memory_) {
return 0;
}
return XMA_CONTEXT_DATA(memory_->TranslateVirtual(guest_context_ptr)).output_buffer_read_offset;
}
bool XmaContextPool::SetOutputBufferReadOffset(const uint32_t guest_context_ptr,
const uint32_t value) {
std::lock_guard<std::mutex> lock(mutex_);
if (!LookupRecordLocked(guest_context_ptr) || !memory_) {
return false;
}
XMA_CONTEXT_DATA data(memory_->TranslateVirtual(guest_context_ptr));
data.output_buffer_read_offset = value;
return WriteContextDataLocked(guest_context_ptr, data, value, 3);
}
uint32_t XmaContextPool::GetOutputBufferWriteOffset(const uint32_t guest_context_ptr) const {
std::lock_guard<std::mutex> lock(mutex_);
if (!LookupRecordLocked(guest_context_ptr) || !memory_) {
return 0;
}
return XMA_CONTEXT_DATA(memory_->TranslateVirtual(guest_context_ptr)).output_buffer_write_offset;
}
uint32_t XmaContextPool::GetPacketMetadata(const uint32_t guest_context_ptr) const {
std::lock_guard<std::mutex> lock(mutex_);
if (!LookupRecordLocked(guest_context_ptr) || !memory_) {
return 0;
}
return XMA_CONTEXT_DATA(memory_->TranslateVirtual(guest_context_ptr)).packet_metadata;
}
bool XmaContextPool::SetEnabled(const uint32_t guest_context_ptr, const bool enabled) {
std::lock_guard<std::mutex> lock(mutex_);
auto* record = LookupRecordLocked(guest_context_ptr);
if (!record) {
return false;
}
record->enabled = enabled;
record->last_update_ticks = NextTickLocked();
if (trace_buffer_) {
trace_buffer_->Record(AudioTraceSubsystem::kXma, AudioTraceEventType::kXmaStateUpdated,
guest_context_ptr, enabled ? 1u : 0u, 4);
}
return true;
}
bool XmaContextPool::BlockWhileInUse(const uint32_t guest_context_ptr, const bool wait) {
std::lock_guard<std::mutex> lock(mutex_);
auto* record = LookupRecordLocked(guest_context_ptr);
if (!record) {
return false;
}
record->blocked = wait;
record->last_update_ticks = NextTickLocked();
if (trace_buffer_) {
trace_buffer_->Record(AudioTraceSubsystem::kXma, AudioTraceEventType::kXmaStateUpdated,
guest_context_ptr, wait ? 1u : 0u, 5);
}
return true;
}
bool XmaContextPool::Save(stream::ByteStream* stream) const {
if (!stream) {
return false;
}
std::lock_guard<std::mutex> lock(mutex_);
stream->Write(static_cast<uint32_t>(records_.size()));
for (const auto& [guest_context_ptr, record] : records_) {
stream->Write(guest_context_ptr);
stream->Write(record.input_buffer_0_physical_address);
stream->Write(record.input_buffer_1_physical_address);
stream->Write(record.output_buffer_physical_address);
stream->Write(record.input_buffer_0_packet_count);
stream->Write(record.input_buffer_1_packet_count);
stream->Write(record.output_buffer_block_count);
stream->Write(static_cast<uint32_t>(record.initialized ? 1 : 0));
stream->Write(static_cast<uint32_t>(record.enabled ? 1 : 0));
stream->Write(static_cast<uint32_t>(record.blocked ? 1 : 0));
stream->Write(record.last_update_ticks);
}
return true;
}
bool XmaContextPool::Restore(stream::ByteStream* stream) {
if (!stream) {
return false;
}
std::lock_guard<std::mutex> lock(mutex_);
records_.clear();
const uint32_t record_count = stream->Read<uint32_t>();
for (uint32_t i = 0; i < record_count; ++i) {
XmaContextRecord record;
record.guest_context_ptr = stream->Read<uint32_t>();
record.input_buffer_0_physical_address = stream->Read<uint32_t>();
record.input_buffer_1_physical_address = stream->Read<uint32_t>();
record.output_buffer_physical_address = stream->Read<uint32_t>();
record.input_buffer_0_packet_count = stream->Read<uint32_t>();
record.input_buffer_1_packet_count = stream->Read<uint32_t>();
record.output_buffer_block_count = stream->Read<uint32_t>();
record.initialized = stream->Read<uint32_t>() != 0;
record.enabled = stream->Read<uint32_t>() != 0;
record.blocked = stream->Read<uint32_t>() != 0;
record.last_update_ticks = stream->Read<uint64_t>();
records_[record.guest_context_ptr] = record;
}
return true;
}
bool XmaContextPool::ReadContextDataLocked(const uint32_t guest_context_ptr,
XMA_CONTEXT_DATA* out_data) const {
if (!out_data || !LookupRecordLocked(guest_context_ptr) || !memory_) {
return false;
}
*out_data = XMA_CONTEXT_DATA(memory_->TranslateVirtual(guest_context_ptr));
return true;
}
bool XmaContextPool::WriteContextDataLocked(const uint32_t guest_context_ptr,
XMA_CONTEXT_DATA data,
const uint32_t trace_value_0,
const uint32_t trace_value_1) {
auto* record = LookupRecordLocked(guest_context_ptr);
if (!record || !memory_) {
return false;
}
data.Store(memory_->TranslateVirtual(guest_context_ptr));
record->last_update_ticks = NextTickLocked();
if (trace_buffer_) {
trace_buffer_->Record(AudioTraceSubsystem::kXma, AudioTraceEventType::kXmaStateUpdated,
guest_context_ptr, trace_value_0, trace_value_1);
}
return true;
}
XmaContextRecord* XmaContextPool::LookupRecordLocked(const uint32_t guest_context_ptr) {
const auto it = records_.find(guest_context_ptr);
return it == records_.end() ? nullptr : &it->second;
}
const XmaContextRecord* XmaContextPool::LookupRecordLocked(const uint32_t guest_context_ptr) const {
const auto it = records_.find(guest_context_ptr);
return it == records_.end() ? nullptr : &it->second;
}
uint64_t XmaContextPool::NextTickLocked() {
return ++tick_counter_;
}
} // namespace rex::audio::xma
@@ -1,96 +0,0 @@
/**
******************************************************************************
* Xenia : Xbox 360 Emulator Research Project *
******************************************************************************
* Copyright 2026 Tom Clay. All rights reserved. *
* Released under the BSD license - see LICENSE in the root for more details. *
******************************************************************************
*/
#pragma once
#include <cstdint>
#include <mutex>
#include <unordered_map>
#include <rex/audio/audio_trace.h>
#include <rex/audio/xma/context.h>
#include <rex/memory.h>
namespace rex::stream {
class ByteStream;
}
namespace rex::audio::xma {
struct XmaContextRecord {
uint32_t guest_context_ptr{0};
uint32_t input_buffer_0_physical_address{0};
uint32_t input_buffer_1_physical_address{0};
uint32_t output_buffer_physical_address{0};
uint32_t input_buffer_0_packet_count{0};
uint32_t input_buffer_1_packet_count{0};
uint32_t output_buffer_block_count{0};
bool initialized{false};
bool enabled{false};
bool blocked{false};
uint64_t last_update_ticks{0};
};
class XmaContextPool {
public:
XmaContextPool();
~XmaContextPool();
void Setup(memory::Memory* memory, AudioTraceBuffer* trace_buffer);
void Shutdown();
uint32_t AllocateContext();
bool ReleaseContext(uint32_t guest_context_ptr);
bool InitializeContext(uint32_t guest_context_ptr, const XMA_CONTEXT_DATA& context_data);
bool SetLoopData(uint32_t guest_context_ptr, const XMA_CONTEXT_DATA& loop_data);
uint32_t GetInputBufferReadOffset(uint32_t guest_context_ptr) const;
bool SetInputBufferReadOffset(uint32_t guest_context_ptr, uint32_t value);
bool SetInputBuffer0(uint32_t guest_context_ptr, uint32_t buffer_physical_address,
uint32_t packet_count);
bool IsInputBuffer0Valid(uint32_t guest_context_ptr) const;
bool SetInputBuffer0Valid(uint32_t guest_context_ptr, bool valid);
bool SetInputBuffer1(uint32_t guest_context_ptr, uint32_t buffer_physical_address,
uint32_t packet_count);
bool IsInputBuffer1Valid(uint32_t guest_context_ptr) const;
bool SetInputBuffer1Valid(uint32_t guest_context_ptr, bool valid);
bool IsOutputBufferValid(uint32_t guest_context_ptr) const;
bool SetOutputBufferValid(uint32_t guest_context_ptr, bool valid);
uint32_t GetOutputBufferReadOffset(uint32_t guest_context_ptr) const;
bool SetOutputBufferReadOffset(uint32_t guest_context_ptr, uint32_t value);
uint32_t GetOutputBufferWriteOffset(uint32_t guest_context_ptr) const;
uint32_t GetPacketMetadata(uint32_t guest_context_ptr) const;
bool SetEnabled(uint32_t guest_context_ptr, bool enabled);
bool BlockWhileInUse(uint32_t guest_context_ptr, bool wait);
bool Save(stream::ByteStream* stream) const;
bool Restore(stream::ByteStream* stream);
private:
bool ReadContextDataLocked(uint32_t guest_context_ptr, XMA_CONTEXT_DATA* out_data) const;
bool WriteContextDataLocked(uint32_t guest_context_ptr, XMA_CONTEXT_DATA data,
uint32_t trace_value_0, uint32_t trace_value_1);
XmaContextRecord* LookupRecordLocked(uint32_t guest_context_ptr);
const XmaContextRecord* LookupRecordLocked(uint32_t guest_context_ptr) const;
uint64_t NextTickLocked();
memory::Memory* memory_{nullptr};
AudioTraceBuffer* trace_buffer_{nullptr};
mutable std::mutex mutex_;
std::unordered_map<uint32_t, XmaContextRecord> records_;
uint64_t tick_counter_{0};
};
} // namespace rex::audio::xma
@@ -1,23 +0,0 @@
/**
******************************************************************************
* Xenia : Xbox 360 Emulator Research Project *
******************************************************************************
* Copyright 2026 Tom Clay. All rights reserved. *
* Released under the BSD license - see LICENSE in the root for more details. *
******************************************************************************
*/
#include <rex/audio/xma/xma_decoder_backend.h>
namespace rex::audio::xma {
bool NullXmaDecoderBackend::IsAvailable() const {
return false;
}
bool NullXmaDecoderBackend::DecodePacket([[maybe_unused]] std::span<const uint8_t> packet_data,
[[maybe_unused]] std::vector<float>* out_pcm) {
return false;
}
} // namespace rex::audio::xma
@@ -1,33 +0,0 @@
/**
******************************************************************************
* Xenia : Xbox 360 Emulator Research Project *
******************************************************************************
* Copyright 2026 Tom Clay. All rights reserved. *
* Released under the BSD license - see LICENSE in the root for more details. *
******************************************************************************
*/
#pragma once
#include <span>
#include <vector>
namespace rex::audio::xma {
class XmaDecoderBackend {
public:
virtual ~XmaDecoderBackend() = default;
virtual bool IsAvailable() const = 0;
virtual bool DecodePacket(std::span<const uint8_t> packet_data,
std::vector<float>* out_pcm) = 0;
};
class NullXmaDecoderBackend final : public XmaDecoderBackend {
public:
bool IsAvailable() const override;
bool DecodePacket(std::span<const uint8_t> packet_data,
std::vector<float>* out_pcm) override;
};
} // namespace rex::audio::xma
@@ -1,23 +0,0 @@
/**
******************************************************************************
* Xenia : Xbox 360 Emulator Research Project *
******************************************************************************
* Copyright 2026 Tom Clay. All rights reserved. *
* Released under the BSD license - see LICENSE in the root for more details. *
******************************************************************************
*/
#include <rex/audio/xma/xma_packet_parser.h>
namespace rex::audio::xma {
XmaPacketParseResult XmaPacketParser::Parse(const std::span<const uint8_t> packet_data) const {
XmaPacketParseResult result;
result.valid = !packet_data.empty();
result.packet_count = result.valid ? 1 : 0;
result.frame_count = 0;
result.total_payload_bytes = static_cast<uint32_t>(packet_data.size());
return result;
}
} // namespace rex::audio::xma
@@ -1,29 +0,0 @@
/**
******************************************************************************
* Xenia : Xbox 360 Emulator Research Project *
******************************************************************************
* Copyright 2026 Tom Clay. All rights reserved. *
* Released under the BSD license - see LICENSE in the root for more details. *
******************************************************************************
*/
#pragma once
#include <cstdint>
#include <span>
namespace rex::audio::xma {
struct XmaPacketParseResult {
bool valid{false};
uint32_t packet_count{0};
uint32_t frame_count{0};
uint32_t total_payload_bytes{0};
};
class XmaPacketParser {
public:
XmaPacketParseResult Parse(std::span<const uint8_t> packet_data) const;
};
} // namespace rex::audio::xma
@@ -1,88 +0,0 @@
# rexkernel: Xbox 360 kernel/XAM export implementations (ported from Xenia)
# Namespace: rex::kernel
# System emulation layer has moved to rexsystem (rex::system)
set(REXKERNEL_SOURCES
kernel_init.cpp
# XAM subsystem - provides __imp__Xam* symbols
xam/xam_module.cpp
xam/xam_avatar.cpp
xam/xam_content.cpp
xam/xam_content_aggregate.cpp
xam/xam_content_device.cpp
xam/xam_debug.cpp
xam/xam_enum.cpp
xam/xam_info.cpp
xam/xam_input.cpp
xam/xam_locale.cpp
xam/xam_msg.cpp
xam/xam_net.cpp
xam/xam_notify.cpp
xam/xam_nui.cpp
xam/xam_party.cpp
xam/xam_task.cpp
xam/xam_ui.cpp
xam/xam_user.cpp
xam/xam_video.cpp
xam/xam_voice.cpp
xam/xam_misc.cpp
xam/apps/xam_app.cpp
xam/apps/xgi_app.cpp
xam/apps/xlivebase_app.cpp
xam/apps/xmp_app.cpp
# xboxkrnl exports - provides __imp__ symbols for generated code
# xboxkrnl/cert_monitor.cpp # TODO: Translate JIT methods for AOT
# xboxkrnl/debug_monitor.cpp # TODO: Translate JIT methods for AOT
xboxkrnl/xboxkrnl_crypt.cpp
xboxkrnl/xboxkrnl_debug.cpp
xboxkrnl/xboxkrnl_error.cpp
xboxkrnl/xboxkrnl_hal.cpp
xboxkrnl/xboxkrnl_hid.cpp
xboxkrnl/xboxkrnl_io.cpp
xboxkrnl/xboxkrnl_io_info.cpp
xboxkrnl/xboxkrnl_memory.cpp
xboxkrnl/xboxkrnl_misc.cpp
xboxkrnl/xboxkrnl_module.cpp
xboxkrnl/xboxkrnl_modules.cpp
xboxkrnl/xboxkrnl_ob.cpp
xboxkrnl/xboxkrnl_rtl.cpp
xboxkrnl/xboxkrnl_strings.cpp
xboxkrnl/xboxkrnl_threading.cpp
# xboxkrnl/xboxkrnl_usbcam.cpp # TODO: lol eventually.
xboxkrnl/xboxkrnl_video.cpp
xboxkrnl/xboxkrnl_xconfig.cpp
xboxkrnl/xboxkrnl_audio.cpp
xboxkrnl/xboxkrnl_audio_xma.cpp
crt/heap.cpp
crt/file.cpp
crt/memory.cpp
crt/string.cpp
)
add_library(rexkernel STATIC ${REXKERNEL_SOURCES})
add_library(rex::kernel ALIAS rexkernel)
target_include_directories(rexkernel PUBLIC
$<BUILD_INTERFACE:${REXGLUE_ROOT}/include>
$<INSTALL_INTERFACE:${CMAKE_INSTALL_INCLUDEDIR}>
)
target_include_directories(rexkernel PRIVATE
${REXGLUE_ROOT}/thirdparty
)
target_link_libraries(rexkernel
PUBLIC
rexsystem
PRIVATE
rexgraphics
rexinput
rexaudio
o1heap
)
if(WIN32)
target_link_libraries(rexkernel PRIVATE bcrypt)
endif()
@@ -1,605 +0,0 @@
/**
* @file kernel/crt/file.cpp
*
* @brief rexcrt File I/O hooks -- Win32-style CRT wrappers backed by VFS.
* Generic implementations with no game-specific logic.
*
* @copyright Copyright (c) 2026 Tom Clay <tomc@tctechstuff.com>
* @license BSD 3-Clause License
*/
#include <cstring>
#include <memory>
#include <span>
#include <rex/filesystem.h>
#include <rex/filesystem/device.h>
#include <rex/filesystem/entry.h>
#include <rex/filesystem/vfs.h>
#include <rex/logging.h>
#include <rex/memory.h>
#include <rex/ppc/function.h>
#include <rex/ppc/types.h>
#include <rex/string.h>
#include <rex/system/kernel_state.h>
#include <rex/system/thread_state.h>
#include <rex/system/xfile.h>
#include <rex/system/xtypes.h>
using rex::X_STATUS;
using namespace rex::ppc;
namespace rex::kernel::crt {
constexpr uint32_t kCreateNew = 1;
constexpr uint32_t kCreateAlways = 2;
constexpr uint32_t kOpenExisting = 3;
constexpr uint32_t kOpenAlways = 4;
constexpr uint32_t kTruncateExisting = 5;
constexpr uint32_t kFileBegin = 0;
constexpr uint32_t kFileCurrent = 1;
constexpr uint32_t kFileEnd = 2;
constexpr uint32_t kInvalidHandleValue = 0xFFFFFFFF;
static rex::filesystem::FileDisposition MapDisposition(uint32_t win32_disp) {
using FD = rex::filesystem::FileDisposition;
switch (win32_disp) {
case kCreateNew:
return FD::kCreate;
case kCreateAlways:
return FD::kOverwriteIf;
case kOpenExisting:
return FD::kOpen;
case kOpenAlways:
return FD::kOpenIf;
case kTruncateExisting:
return FD::kOverwrite;
default:
return FD::kOpen;
}
}
ppc_u32_result_t CreateFileA_entry(ppc_pchar_t lpFileName, ppc_u32_t dwDesiredAccess,
ppc_u32_t dwShareMode, ppc_pvoid_t lpSecurityAttributes,
ppc_u32_t dwCreationDisposition, ppc_u32_t dwFlagsAndAttributes,
ppc_u32_t hTemplateFile) {
const char* path = static_cast<const char*>(lpFileName);
auto* ks = REX_KERNEL_STATE();
auto disposition = MapDisposition(static_cast<uint32_t>(dwCreationDisposition));
rex::filesystem::File* vfs_file = nullptr;
rex::filesystem::FileAction action;
X_STATUS status = ks->file_system()->OpenFile(nullptr, path, disposition,
static_cast<uint32_t>(dwDesiredAccess), false, true,
&vfs_file, &action);
if (XFAILED(status) || !vfs_file) {
REXKRNL_DEBUG("rexcrt_CreateFileA: FAILED path='{}' status={:#x}", path, status);
return kInvalidHandleValue;
}
auto* xfile = new rex::system::XFile(ks, vfs_file, true);
auto handle = xfile->handle();
REXKRNL_DEBUG("rexcrt_CreateFileA: '{}' -> handle={:#x}", path, handle);
return handle;
}
ppc_u32_result_t ReadFile_entry(ppc_u32_t hFile, ppc_pvoid_t lpBuffer,
ppc_u32_t nNumberOfBytesToRead, ppc_pu32_t lpNumberOfBytesRead,
ppc_pvoid_t lpOverlapped) {
auto file = REX_KERNEL_OBJECTS()->LookupObject<rex::system::XFile>(static_cast<uint32_t>(hFile));
if (!file) {
REXKRNL_WARN("rexcrt_ReadFile: invalid handle {:#x}", static_cast<uint32_t>(hFile));
if (lpNumberOfBytesRead)
*lpNumberOfBytesRead = 0;
return 0;
}
uint64_t offset = static_cast<uint64_t>(-1);
if (lpOverlapped) {
auto* ov = reinterpret_cast<rex::be<uint32_t>*>(
static_cast<uint8_t*>(static_cast<void*>(lpOverlapped)));
offset =
(static_cast<uint64_t>(static_cast<uint32_t>(ov[3])) << 32) | static_cast<uint32_t>(ov[2]);
}
uint32_t bytes_read = 0;
X_STATUS status = file->Read(lpBuffer.guest_address(),
static_cast<uint32_t>(nNumberOfBytesToRead), offset, &bytes_read, 0);
if (lpOverlapped) {
auto* ov = reinterpret_cast<rex::be<uint32_t>*>(
static_cast<uint8_t*>(static_cast<void*>(lpOverlapped)));
ov[0] = 0;
ov[1] = bytes_read;
} else if (lpNumberOfBytesRead) {
*lpNumberOfBytesRead = bytes_read;
}
return XSUCCEEDED(status) ? 1u : 0u;
}
ppc_u32_result_t WriteFile_entry(ppc_u32_t hFile, ppc_pvoid_t lpBuffer,
ppc_u32_t nNumberOfBytesToWrite, ppc_pu32_t lpNumberOfBytesWritten,
ppc_pvoid_t lpOverlapped) {
auto file = REX_KERNEL_OBJECTS()->LookupObject<rex::system::XFile>(static_cast<uint32_t>(hFile));
if (!file) {
if (lpNumberOfBytesWritten)
*lpNumberOfBytesWritten = 0;
return 0;
}
uint64_t offset = static_cast<uint64_t>(-1);
if (lpOverlapped) {
auto* ov = reinterpret_cast<rex::be<uint32_t>*>(
static_cast<uint8_t*>(static_cast<void*>(lpOverlapped)));
offset =
(static_cast<uint64_t>(static_cast<uint32_t>(ov[3])) << 32) | static_cast<uint32_t>(ov[2]);
}
uint32_t bytes_written = 0;
X_STATUS status =
file->Write(lpBuffer.guest_address(), static_cast<uint32_t>(nNumberOfBytesToWrite), offset,
&bytes_written, 0);
if (lpOverlapped) {
auto* ov = reinterpret_cast<rex::be<uint32_t>*>(
static_cast<uint8_t*>(static_cast<void*>(lpOverlapped)));
ov[0] = 0;
ov[1] = bytes_written;
} else if (lpNumberOfBytesWritten) {
*lpNumberOfBytesWritten = bytes_written;
}
return XSUCCEEDED(status) ? 1u : 0u;
}
ppc_u32_result_t SetFilePointer_entry(ppc_u32_t hFile, ppc_u32_t lDistanceToMove,
ppc_pu32_t lpDistanceToMoveHigh, ppc_u32_t dwMoveMethod) {
auto file = REX_KERNEL_OBJECTS()->LookupObject<rex::system::XFile>(static_cast<uint32_t>(hFile));
if (!file)
return kInvalidHandleValue;
int64_t distance = static_cast<int32_t>(static_cast<uint32_t>(lDistanceToMove));
if (lpDistanceToMoveHigh) {
distance |=
static_cast<int64_t>(static_cast<int32_t>(static_cast<uint32_t>(*lpDistanceToMoveHigh)))
<< 32;
}
uint64_t new_pos = 0;
switch (static_cast<uint32_t>(dwMoveMethod)) {
case kFileBegin:
new_pos = static_cast<uint64_t>(distance);
break;
case kFileCurrent:
new_pos = file->position() + distance;
break;
case kFileEnd:
new_pos = file->entry()->size() + distance;
break;
default:
return kInvalidHandleValue;
}
file->set_position(new_pos);
if (lpDistanceToMoveHigh)
*lpDistanceToMoveHigh = static_cast<uint32_t>(new_pos >> 32);
return static_cast<uint32_t>(new_pos & 0xFFFFFFFF);
}
ppc_u32_result_t GetFileSize_entry(ppc_u32_t hFile, ppc_pu32_t lpFileSizeHigh) {
auto file = REX_KERNEL_OBJECTS()->LookupObject<rex::system::XFile>(static_cast<uint32_t>(hFile));
if (!file)
return kInvalidHandleValue;
uint64_t size = file->entry()->size();
if (lpFileSizeHigh)
*lpFileSizeHigh = static_cast<uint32_t>(size >> 32);
return static_cast<uint32_t>(size & 0xFFFFFFFF);
}
ppc_u32_result_t GetFileSizeEx_entry(ppc_u32_t hFile, ppc_pvoid_t lpFileSize) {
auto file = REX_KERNEL_OBJECTS()->LookupObject<rex::system::XFile>(static_cast<uint32_t>(hFile));
if (!file)
return 0;
uint64_t size = file->entry()->size();
if (lpFileSize) {
auto* out =
reinterpret_cast<rex::be<uint32_t>*>(static_cast<uint8_t*>(static_cast<void*>(lpFileSize)));
out[0] = static_cast<uint32_t>(size >> 32);
out[1] = static_cast<uint32_t>(size & 0xFFFFFFFF);
}
return 1;
}
ppc_u32_result_t SetEndOfFile_entry(ppc_u32_t hFile) {
auto file = REX_KERNEL_OBJECTS()->LookupObject<rex::system::XFile>(static_cast<uint32_t>(hFile));
if (!file)
return 0;
X_STATUS status = file->SetLength(file->position());
return XSUCCEEDED(status) ? 1u : 0u;
}
ppc_u32_result_t FlushFileBuffers_entry(ppc_u32_t hFile) {
(void)hFile;
return 1;
}
ppc_u32_result_t DeleteFileA_entry(ppc_pchar_t lpFileName) {
const char* path = static_cast<const char*>(lpFileName);
bool ok = REX_KERNEL_FS()->DeletePath(path);
if (!ok)
REXKRNL_DEBUG("rexcrt_DeleteFileA: FAILED '{}'", path);
return ok ? 1u : 0u;
}
ppc_u32_result_t CloseHandle_entry(ppc_u32_t hObject) {
uint32_t h = static_cast<uint32_t>(hObject);
if (h == kInvalidHandleValue || h == 0)
return 0;
auto status = REX_KERNEL_OBJECTS()->ReleaseHandle(h);
if (XFAILED(status)) {
REXKRNL_WARN("rexcrt_CloseHandle: unknown handle {:#x}", h);
return 0;
}
return 1;
}
static void FillFindData(ppc_pvoid_t lpFindFileData, rex::filesystem::Entry* entry) {
auto* buf = static_cast<uint8_t*>(static_cast<void*>(lpFindFileData));
std::memset(buf, 0, 0x140);
auto* fields = reinterpret_cast<be<uint32_t>*>(buf);
fields[0] = entry->attributes(); // 0x00 dwFileAttributes
fields[1] =
static_cast<uint32_t>(entry->create_timestamp() & 0xFFFFFFFF); // 0x04 ftCreationTime.Low
fields[2] = static_cast<uint32_t>(entry->create_timestamp() >> 32); // 0x08 ftCreationTime.High
fields[3] =
static_cast<uint32_t>(entry->access_timestamp() & 0xFFFFFFFF); // 0x0C ftLastAccessTime.Low
fields[4] = static_cast<uint32_t>(entry->access_timestamp() >> 32); // 0x10 ftLastAccessTime.High
fields[5] =
static_cast<uint32_t>(entry->write_timestamp() & 0xFFFFFFFF); // 0x14 ftLastWriteTime.Low
fields[6] = static_cast<uint32_t>(entry->write_timestamp() >> 32); // 0x18 ftLastWriteTime.High
fields[7] = static_cast<uint32_t>(entry->size() >> 32); // 0x1C nFileSizeHigh
fields[8] = static_cast<uint32_t>(entry->size() & 0xFFFFFFFF); // 0x20 nFileSizeLow
// 0x24 dwReserved0, 0x28 dwReserved1 already zero
// 0x2C cFileName[260]
const auto& name = entry->name();
std::strncpy(reinterpret_cast<char*>(buf + 0x2C), name.c_str(), 259);
// 0x130 cAlternateFileName[14] already zero
}
ppc_u32_result_t FindFirstFileA_entry(ppc_pchar_t lpFileName, ppc_pvoid_t lpFindFileData) {
const char* path = static_cast<const char*>(lpFileName);
auto dir = rex::string::utf8_find_base_guest_path(path);
auto pattern = rex::string::utf8_find_name_from_guest_path(path);
auto* ks = REX_KERNEL_STATE();
rex::filesystem::File* vfs_file = nullptr;
rex::filesystem::FileAction action;
X_STATUS status = ks->file_system()->OpenFile(
nullptr, dir, rex::filesystem::FileDisposition::kOpen, 0, true, false, &vfs_file, &action);
if (XFAILED(status) || !vfs_file) {
REXKRNL_DEBUG("rexcrt_FindFirstFileA: dir not found '{}'", dir);
return kInvalidHandleValue;
}
auto* xfile = new rex::system::XFile(ks, vfs_file, true);
xfile->SetFindPattern(pattern);
auto* entry = xfile->FindNext();
if (!entry) {
REXKRNL_DEBUG("rexcrt_FindFirstFileA: no matches for '{}' in '{}'", pattern, dir);
REX_KERNEL_OBJECTS()->ReleaseHandle(xfile->handle());
return kInvalidHandleValue;
}
FillFindData(lpFindFileData, entry);
REXKRNL_DEBUG("rexcrt_FindFirstFileA: '{}' first match='{}' handle={:#x}", path, entry->name(),
xfile->handle());
return xfile->handle();
}
ppc_u32_result_t FindNextFileA_entry(ppc_u32_t hFindFile, ppc_pvoid_t lpFindFileData) {
auto file =
REX_KERNEL_OBJECTS()->LookupObject<rex::system::XFile>(static_cast<uint32_t>(hFindFile));
if (!file)
return 0;
auto* entry = file->FindNext();
if (!entry)
return 0;
FillFindData(lpFindFileData, entry);
return 1;
}
ppc_u32_result_t FindClose_entry(ppc_u32_t hFindFile) {
return CloseHandle_entry(hFindFile);
}
ppc_u32_result_t CreateDirectoryA_entry(ppc_pchar_t lpPathName, ppc_pvoid_t lpSecurityAttributes) {
const char* path = static_cast<const char*>(lpPathName);
auto* entry = REX_KERNEL_FS()->CreatePath(path, rex::filesystem::kFileAttributeDirectory);
return entry ? 1u : 0u;
}
ppc_u32_result_t MoveFileA_entry(ppc_pchar_t lpExistingFileName, ppc_pchar_t lpNewFileName) {
REXKRNL_WARN("rexcrt_MoveFileA: STUB '{}' -> '{}'", static_cast<const char*>(lpExistingFileName),
static_cast<const char*>(lpNewFileName));
return 1;
}
ppc_u32_result_t SetFileAttributesA_entry(ppc_pchar_t lpFileName, ppc_u32_t dwFileAttributes) {
(void)lpFileName;
(void)dwFileAttributes;
return 1;
}
ppc_u32_result_t GetFileAttributesA_entry(ppc_pchar_t lpFileName) {
const char* path = static_cast<const char*>(lpFileName);
auto* entry = REX_KERNEL_FS()->ResolvePath(path);
if (!entry) {
REXKRNL_DEBUG("rexcrt_GetFileAttributesA: not found '{}'", path);
return kInvalidHandleValue; // INVALID_FILE_ATTRIBUTES
}
REXKRNL_DEBUG("rexcrt_GetFileAttributesA: '{}' -> attrs={:#x}", path, entry->attributes());
return entry->attributes();
}
ppc_u32_result_t GetFileAttributesExA_entry(ppc_u32_t fInfoLevelId, ppc_pchar_t lpFileName,
ppc_pvoid_t lpFileInformation) {
const char* path = static_cast<const char*>(lpFileName);
auto* entry = REX_KERNEL_FS()->ResolvePath(path);
if (!entry) {
REXKRNL_DEBUG("rexcrt_GetFileAttributesExA: not found '{}'", path);
return 0;
}
// Fill WIN32_FILE_ATTRIBUTE_DATA (GetFileExInfoStandard = 0)
auto* buf =
reinterpret_cast<be<uint32_t>*>(static_cast<uint8_t*>(static_cast<void*>(lpFileInformation)));
buf[0] = entry->attributes(); // dwFileAttributes
buf[1] = static_cast<uint32_t>(entry->create_timestamp() & 0xFFFFFFFF); // ftCreationTime.Low
buf[2] = static_cast<uint32_t>(entry->create_timestamp() >> 32); // ftCreationTime.High
buf[3] = static_cast<uint32_t>(entry->access_timestamp() & 0xFFFFFFFF); // ftLastAccessTime.Low
buf[4] = static_cast<uint32_t>(entry->access_timestamp() >> 32); // ftLastAccessTime.High
buf[5] = static_cast<uint32_t>(entry->write_timestamp() & 0xFFFFFFFF); // ftLastWriteTime.Low
buf[6] = static_cast<uint32_t>(entry->write_timestamp() >> 32); // ftLastWriteTime.High
buf[7] = static_cast<uint32_t>(entry->size() >> 32); // nFileSizeHigh
buf[8] = static_cast<uint32_t>(entry->size() & 0xFFFFFFFF); // nFileSizeLow
return 1;
}
ppc_u32_result_t SetFilePointerEx_entry(ppc_u32_t hFile, ppc_u32_t distHigh, ppc_u32_t distLow,
ppc_pvoid_t lpNewFilePointer, ppc_u32_t dwMoveMethod) {
auto file = REX_KERNEL_OBJECTS()->LookupObject<rex::system::XFile>(static_cast<uint32_t>(hFile));
if (!file)
return 0;
int64_t distance =
static_cast<int64_t>((static_cast<uint64_t>(static_cast<uint32_t>(distHigh)) << 32) |
static_cast<uint32_t>(distLow));
uint64_t new_pos = 0;
switch (static_cast<uint32_t>(dwMoveMethod)) {
case kFileBegin:
new_pos = static_cast<uint64_t>(distance);
break;
case kFileCurrent:
new_pos = file->position() + distance;
break;
case kFileEnd:
new_pos = file->entry()->size() + distance;
break;
default:
return 0;
}
file->set_position(new_pos);
if (lpNewFilePointer) {
auto* out = reinterpret_cast<be<uint32_t>*>(
static_cast<uint8_t*>(static_cast<void*>(lpNewFilePointer)));
out[0] = static_cast<uint32_t>(new_pos & 0xFFFFFFFF); // LowPart
out[1] = static_cast<uint32_t>(new_pos >> 32); // HighPart
}
return 1;
}
ppc_u32_result_t SetFileTime_entry(ppc_u32_t hFile, ppc_pvoid_t lpCreationTime,
ppc_pvoid_t lpLastAccessTime, ppc_pvoid_t lpLastWriteTime) {
// VFS doesn't support modifying timestamps; report success.
(void)hFile;
(void)lpCreationTime;
(void)lpLastAccessTime;
(void)lpLastWriteTime;
return 1;
}
ppc_u32_result_t CompareFileTime_entry(ppc_pvoid_t lpFileTime1, ppc_pvoid_t lpFileTime2) {
auto* ft1 =
reinterpret_cast<be<uint32_t>*>(static_cast<uint8_t*>(static_cast<void*>(lpFileTime1)));
auto* ft2 =
reinterpret_cast<be<uint32_t>*>(static_cast<uint8_t*>(static_cast<void*>(lpFileTime2)));
// FILETIME: { dwLowDateTime, dwHighDateTime }
uint64_t t1 =
(static_cast<uint64_t>(static_cast<uint32_t>(ft1[1])) << 32) | static_cast<uint32_t>(ft1[0]);
uint64_t t2 =
(static_cast<uint64_t>(static_cast<uint32_t>(ft2[1])) << 32) | static_cast<uint32_t>(ft2[0]);
if (t1 < t2)
return static_cast<uint32_t>(-1);
if (t1 > t2)
return 1u;
return 0u;
}
ppc_u32_result_t CopyFileA_entry(ppc_pchar_t lpExistingFileName, ppc_pchar_t lpNewFileName,
ppc_u32_t bFailIfExists) {
const char* src = static_cast<const char*>(lpExistingFileName);
const char* dst = static_cast<const char*>(lpNewFileName);
auto* ks = REX_KERNEL_STATE();
// Open source for reading
rex::filesystem::File* src_file = nullptr;
rex::filesystem::FileAction action;
X_STATUS status = ks->file_system()->OpenFile(
nullptr, src, rex::filesystem::FileDisposition::kOpen,
rex::filesystem::FileAccess::kFileReadData, false, true, &src_file, &action);
if (XFAILED(status) || !src_file) {
REXKRNL_DEBUG("rexcrt_CopyFileA: failed to open source '{}'", src);
return 0;
}
// Open/create destination
auto disp = static_cast<uint32_t>(bFailIfExists) ? rex::filesystem::FileDisposition::kCreate
: rex::filesystem::FileDisposition::kOverwriteIf;
rex::filesystem::File* dst_file = nullptr;
status =
ks->file_system()->OpenFile(nullptr, dst, disp, rex::filesystem::FileAccess::kFileWriteData,
false, true, &dst_file, &action);
if (XFAILED(status) || !dst_file) {
src_file->Destroy();
REXKRNL_DEBUG("rexcrt_CopyFileA: failed to open dest '{}'", dst);
return 0;
}
// Copy data in 64KB chunks
constexpr size_t kBufSize = 65536;
auto buf = std::make_unique<uint8_t[]>(kBufSize);
uint64_t offset = 0;
bool ok = true;
for (;;) {
size_t bytes_read = 0;
status = src_file->ReadSync(std::span<uint8_t>(buf.get(), kBufSize), offset, &bytes_read);
if (XFAILED(status) || bytes_read == 0)
break;
size_t bytes_written = 0;
status = dst_file->WriteSync(std::span<const uint8_t>(buf.get(), bytes_read), offset,
&bytes_written);
if (XFAILED(status) || bytes_written != bytes_read) {
ok = false;
break;
}
offset += bytes_read;
}
dst_file->Destroy();
src_file->Destroy();
REXKRNL_DEBUG("rexcrt_CopyFileA: '{}' -> '{}' {}", src, dst, ok ? "OK" : "FAILED");
return ok ? 1u : 0u;
}
ppc_u32_result_t RemoveDirectoryA_entry(ppc_pchar_t lpPathName) {
const char* path = static_cast<const char*>(lpPathName);
bool ok = REX_KERNEL_FS()->DeletePath(path);
if (!ok)
REXKRNL_DEBUG("rexcrt_RemoveDirectoryA: FAILED '{}'", path);
return ok ? 1u : 0u;
}
ppc_u32_result_t GetFileType_entry(ppc_u32_t hFile) {
auto file = REX_KERNEL_OBJECTS()->LookupObject<rex::system::XFile>(static_cast<uint32_t>(hFile));
if (!file)
return 0; // FILE_TYPE_UNKNOWN
return 1; // FILE_TYPE_DISK
}
ppc_u32_result_t GetDiskFreeSpaceExA_entry(ppc_pchar_t lpDirectoryName,
ppc_pvoid_t lpFreeBytesAvailableToCaller,
ppc_pvoid_t lpTotalNumberOfBytes,
ppc_pvoid_t lpTotalNumberOfFreeBytes) {
const char* path = static_cast<const char*>(lpDirectoryName);
auto* entry = REX_KERNEL_FS()->ResolvePath(path);
if (!entry) {
REXKRNL_DEBUG("rexcrt_GetDiskFreeSpaceExA: path not found '{}'", path);
return 0;
}
auto* dev = entry->device();
uint64_t bytes_per_au =
static_cast<uint64_t>(dev->sectors_per_allocation_unit()) * dev->bytes_per_sector();
uint64_t total_bytes = static_cast<uint64_t>(dev->total_allocation_units()) * bytes_per_au;
uint64_t free_bytes = static_cast<uint64_t>(dev->available_allocation_units()) * bytes_per_au;
if (lpFreeBytesAvailableToCaller) {
auto* out = reinterpret_cast<be<uint32_t>*>(
static_cast<uint8_t*>(static_cast<void*>(lpFreeBytesAvailableToCaller)));
out[0] = static_cast<uint32_t>(free_bytes & 0xFFFFFFFF);
out[1] = static_cast<uint32_t>(free_bytes >> 32);
}
if (lpTotalNumberOfBytes) {
auto* out = reinterpret_cast<be<uint32_t>*>(
static_cast<uint8_t*>(static_cast<void*>(lpTotalNumberOfBytes)));
out[0] = static_cast<uint32_t>(total_bytes & 0xFFFFFFFF);
out[1] = static_cast<uint32_t>(total_bytes >> 32);
}
if (lpTotalNumberOfFreeBytes) {
auto* out = reinterpret_cast<be<uint32_t>*>(
static_cast<uint8_t*>(static_cast<void*>(lpTotalNumberOfFreeBytes)));
out[0] = static_cast<uint32_t>(free_bytes & 0xFFFFFFFF);
out[1] = static_cast<uint32_t>(free_bytes >> 32);
}
REXKRNL_DEBUG("rexcrt_GetDiskFreeSpaceExA: '{}' total={}MB free={}MB", path,
total_bytes / (1024 * 1024), free_bytes / (1024 * 1024));
return 1;
}
} // namespace rex::kernel::crt
REXCRT_EXPORT(rexcrt_CreateFileA, rex::kernel::crt::CreateFileA_entry)
REXCRT_EXPORT(rexcrt_ReadFile, rex::kernel::crt::ReadFile_entry)
REXCRT_EXPORT(rexcrt_WriteFile, rex::kernel::crt::WriteFile_entry)
REXCRT_EXPORT(rexcrt_SetFilePointer, rex::kernel::crt::SetFilePointer_entry)
REXCRT_EXPORT(rexcrt_GetFileSize, rex::kernel::crt::GetFileSize_entry)
REXCRT_EXPORT(rexcrt_GetFileSizeEx, rex::kernel::crt::GetFileSizeEx_entry)
REXCRT_EXPORT(rexcrt_SetEndOfFile, rex::kernel::crt::SetEndOfFile_entry)
REXCRT_EXPORT(rexcrt_FlushFileBuffers, rex::kernel::crt::FlushFileBuffers_entry)
REXCRT_EXPORT(rexcrt_DeleteFileA, rex::kernel::crt::DeleteFileA_entry)
REXCRT_EXPORT(rexcrt_CloseHandle, rex::kernel::crt::CloseHandle_entry)
REXCRT_EXPORT(rexcrt_FindFirstFileA, rex::kernel::crt::FindFirstFileA_entry)
REXCRT_EXPORT(rexcrt_FindNextFileA, rex::kernel::crt::FindNextFileA_entry)
REXCRT_EXPORT(rexcrt_FindClose, rex::kernel::crt::FindClose_entry)
REXCRT_EXPORT(rexcrt_CreateDirectoryA, rex::kernel::crt::CreateDirectoryA_entry)
REXCRT_EXPORT(rexcrt_MoveFileA, rex::kernel::crt::MoveFileA_entry)
REXCRT_EXPORT(rexcrt_SetFileAttributesA, rex::kernel::crt::SetFileAttributesA_entry)
REXCRT_EXPORT(rexcrt_GetFileAttributesA, rex::kernel::crt::GetFileAttributesA_entry)
REXCRT_EXPORT(rexcrt_GetFileAttributesExA, rex::kernel::crt::GetFileAttributesExA_entry)
REXCRT_EXPORT(rexcrt_SetFilePointerEx, rex::kernel::crt::SetFilePointerEx_entry)
REXCRT_EXPORT(rexcrt_SetFileTime, rex::kernel::crt::SetFileTime_entry)
REXCRT_EXPORT(rexcrt_CompareFileTime, rex::kernel::crt::CompareFileTime_entry)
REXCRT_EXPORT(rexcrt_CopyFileA, rex::kernel::crt::CopyFileA_entry)
REXCRT_EXPORT(rexcrt_RemoveDirectoryA, rex::kernel::crt::RemoveDirectoryA_entry)
REXCRT_EXPORT(rexcrt_GetFileType, rex::kernel::crt::GetFileType_entry)
// XAM exports -- same implementations, for games that import file I/O from xam.xex
XAM_EXPORT(__imp__CreateFileA, rex::kernel::crt::CreateFileA_entry)
XAM_EXPORT(__imp__ReadFile, rex::kernel::crt::ReadFile_entry)
XAM_EXPORT(__imp__WriteFile, rex::kernel::crt::WriteFile_entry)
XAM_EXPORT(__imp__SetFilePointer, rex::kernel::crt::SetFilePointer_entry)
XAM_EXPORT(__imp__GetFileSize, rex::kernel::crt::GetFileSize_entry)
XAM_EXPORT(__imp__GetFileSizeEx, rex::kernel::crt::GetFileSizeEx_entry)
XAM_EXPORT(__imp__SetEndOfFile, rex::kernel::crt::SetEndOfFile_entry)
XAM_EXPORT(__imp__FlushFileBuffers, rex::kernel::crt::FlushFileBuffers_entry)
XAM_EXPORT(__imp__DeleteFileA, rex::kernel::crt::DeleteFileA_entry)
XAM_EXPORT(__imp__CloseHandle, rex::kernel::crt::CloseHandle_entry)
XAM_EXPORT(__imp__FindFirstFileA, rex::kernel::crt::FindFirstFileA_entry)
XAM_EXPORT(__imp__FindNextFileA, rex::kernel::crt::FindNextFileA_entry)
XAM_EXPORT(__imp__CreateDirectoryA, rex::kernel::crt::CreateDirectoryA_entry)
XAM_EXPORT(__imp__MoveFileA, rex::kernel::crt::MoveFileA_entry)
XAM_EXPORT(__imp__SetFileAttributesA, rex::kernel::crt::SetFileAttributesA_entry)
XAM_EXPORT(__imp__GetFileAttributesA, rex::kernel::crt::GetFileAttributesA_entry)
XAM_EXPORT(__imp__GetFileAttributesExA, rex::kernel::crt::GetFileAttributesExA_entry)
XAM_EXPORT(__imp__SetFilePointerEx, rex::kernel::crt::SetFilePointerEx_entry)
XAM_EXPORT(__imp__SetFileTime, rex::kernel::crt::SetFileTime_entry)
XAM_EXPORT(__imp__CompareFileTime, rex::kernel::crt::CompareFileTime_entry)
XAM_EXPORT(__imp__CopyFileA, rex::kernel::crt::CopyFileA_entry)
XAM_EXPORT(__imp__GetDiskFreeSpaceExA, rex::kernel::crt::GetDiskFreeSpaceExA_entry)
@@ -1,388 +0,0 @@
/**
* @file kernel/crt/heap.cpp
*
* @brief ReXHeap implementation using o1heap with size header pattern.
* Hooks RtlAllocateHeap, RtlFreeHeap, RtlSizeHeap, RtlReAllocateHeap.
*
* @copyright Copyright (c) 2026 Tom Clay <tomc@tctechstuff.com>
* @license BSD 3-Clause License
*/
#include <rex/kernel/crt/heap.h>
#include <algorithm>
#include <cstring>
#include <o1heap.h>
#include <rex/cvar.h>
#include <rex/math.h>
#include <rex/platform.h>
#include <rex/ppc/function.h>
#include <rex/system/xmemory.h>
#include <rex/logging.h>
REXCVAR_DEFINE_BOOL(rexcrt_heap_enable, false, "crt",
"Enable o1heap-backed CRT heap for RtlAllocateHeap/Free/Size/ReAlloc")
.lifecycle(rex::cvar::Lifecycle::kInitOnly);
REXCVAR_DEFINE_UINT32(rexcrt_heap_size_mb, 256, "crt", "Heap size in megabytes")
.lifecycle(rex::cvar::Lifecycle::kInitOnly)
.range(1, 2048);
using namespace rex::ppc;
// ---------------------------------------------------------------------------
// Size header: prepended to every allocation so we can answer RtlSizeHeap
// without o1heap exposing per-allocation usable size.
// ---------------------------------------------------------------------------
namespace {
struct SizeHeader {
uint64_t requested_size;
uint64_t reserved; // padding to O1HEAP_ALIGNMENT
};
static_assert(sizeof(SizeHeader) == O1HEAP_ALIGNMENT,
"SizeHeader must be exactly one O1HEAP_ALIGNMENT unit");
constexpr uint32_t kHeaderSize = static_cast<uint32_t>(O1HEAP_ALIGNMENT);
constexpr uint32_t kMinSegmentSize = 4u * 1024u * 1024u;
constexpr uint32_t kDefaultGrowthSegmentSize = 64u * 1024u * 1024u;
#ifndef HEAP_ZERO_MEMORY
constexpr uint32_t HEAP_ZERO_MEMORY = 0x00000008;
#endif
} // namespace
// ---------------------------------------------------------------------------
// ReXHeap implementation
// ---------------------------------------------------------------------------
namespace rex::kernel::crt {
void* ReXHeap::GuestToHost(uint32_t guest_addr) const {
return membase_ + guest_addr;
}
uint32_t ReXHeap::HostToGuest(void* host_ptr) const {
return static_cast<uint32_t>(static_cast<uint8_t*>(host_ptr) - membase_);
}
bool ReXHeap::InHeap(uint32_t guest_addr) const {
std::lock_guard lock(mutex_);
return FindSegmentByGuestLocked(guest_addr) != nullptr;
}
bool ReXHeap::Init(uint32_t heap_size_bytes, rex::memory::Memory* memory) {
memory_ = memory;
auto* mem = memory_;
if (!mem) {
REXKRNL_ERROR("rexcrt_heap: memory is null");
return false;
}
membase_ = mem->virtual_membase();
if (!membase_) {
REXKRNL_ERROR("rexcrt_heap: virtual_membase is null");
return false;
}
initial_segment_size_ =
rex::align<uint32_t>(std::max(heap_size_bytes, kMinSegmentSize), O1HEAP_ALIGNMENT);
std::lock_guard lock(mutex_);
segments_.clear();
if (!AllocateSegmentLocked(initial_segment_size_)) {
return false;
}
const auto diagnostics = GetDiagnosticsLocked();
REXKRNL_INFO("rexcrt_heap: initialized with {} segment(s), capacity={}MB", segments_.size(),
diagnostics.capacity / (1024 * 1024));
return true;
}
uint32_t ReXHeap::Alloc(uint32_t size, bool zero) {
std::lock_guard lock(mutex_);
return AllocLocked(size, zero);
}
void ReXHeap::Free(uint32_t guest_addr) {
if (!guest_addr)
return;
std::lock_guard lock(mutex_);
auto* segment = FindSegmentByGuestLocked(guest_addr);
if (!segment || guest_addr < segment->guest_base + kHeaderSize) {
REXKRNL_WARN("rexcrt_RtlFreeHeap: skipping OOB ptr 0x{:08X}", guest_addr);
return;
}
void* real_host = GuestToHost(guest_addr - kHeaderSize);
o1heapFree(segment->heap, real_host);
}
uint32_t ReXHeap::Size(uint32_t guest_addr) {
if (!guest_addr)
return ~0u;
std::lock_guard lock(mutex_);
auto* segment = FindSegmentByGuestLocked(guest_addr);
if (!segment || guest_addr < segment->guest_base + kHeaderSize) {
return ~0u;
}
auto* hdr = static_cast<SizeHeader*>(GuestToHost(guest_addr - kHeaderSize));
return static_cast<uint32_t>(hdr->requested_size);
}
uint32_t ReXHeap::Realloc(uint32_t guest_addr, uint32_t new_size, bool zero_new) {
std::lock_guard lock(mutex_);
if (!guest_addr) {
return AllocLocked(new_size, zero_new);
}
if (new_size == 0)
new_size = 1;
auto* segment = FindSegmentByGuestLocked(guest_addr);
if (!segment || guest_addr < segment->guest_base + kHeaderSize) {
// Pre-hook allocation outside our heap -- treat as fresh alloc.
REXKRNL_WARN("rexcrt_RtlReAllocateHeap: OOB ptr 0x{:08X}, treating as new alloc({})",
guest_addr, new_size);
return AllocLocked(new_size, zero_new);
}
void* real_host = GuestToHost(guest_addr - kHeaderSize);
auto* old_hdr = static_cast<SizeHeader*>(real_host);
uint32_t old_size = static_cast<uint32_t>(old_hdr->requested_size);
void* new_ptr = o1heapReallocate(segment->heap, real_host, new_size + kHeaderSize);
if (!new_ptr) {
// Cross-segment fallback: allocate a fresh block, copy, then free old.
uint32_t new_guest = AllocLocked(new_size, false);
if (!new_guest) {
REXKRNL_WARN("rexcrt_RtlReAllocateHeap: o1heapReallocate({}) failed", new_size);
return 0;
}
void* new_user_ptr = GuestToHost(new_guest);
void* old_user_ptr = GuestToHost(guest_addr);
uint32_t copy_size = std::min(old_size, new_size);
std::memcpy(new_user_ptr, old_user_ptr, copy_size);
if (zero_new && new_size > old_size) {
std::memset(static_cast<uint8_t*>(new_user_ptr) + old_size, 0, new_size - old_size);
}
o1heapFree(segment->heap, real_host);
return new_guest;
}
auto* new_hdr = static_cast<SizeHeader*>(new_ptr);
new_hdr->requested_size = new_size;
void* user_ptr = static_cast<uint8_t*>(new_ptr) + kHeaderSize;
if (zero_new && new_size > old_size) {
std::memset(static_cast<uint8_t*>(user_ptr) + old_size, 0, new_size - old_size);
}
return HostToGuest(user_ptr);
}
HeapDiagnostics ReXHeap::GetDiagnostics() const {
std::lock_guard lock(mutex_);
return GetDiagnosticsLocked();
}
bool ReXHeap::AllocateSegmentLocked(uint32_t segment_size_bytes) {
auto* mem = memory_;
if (!mem) {
REXKRNL_ERROR("rexcrt_heap: kernel memory is null during segment allocation");
return false;
}
segment_size_bytes =
rex::align<uint32_t>(std::max(segment_size_bytes, kMinSegmentSize), O1HEAP_ALIGNMENT);
uint32_t guest_base = 0;
bool used_system_heap = false;
auto alloc_regular_virtual_heap = [&]() -> bool {
auto* heap = mem->LookupHeapByType(false, 4096);
if (!heap ||
!heap->Alloc(segment_size_bytes, O1HEAP_ALIGNMENT,
rex::memory::kMemoryAllocationReserve | rex::memory::kMemoryAllocationCommit,
rex::memory::kMemoryProtectRead | rex::memory::kMemoryProtectWrite, true,
&guest_base)) {
return false;
}
return true;
};
if (!alloc_regular_virtual_heap()) {
// CRT heap segments are title heap data, not kernel-object metadata, so
// they should live in the general virtual heap. Fall back to the system
// heap only if the normal heap allocator can't place the segment.
guest_base = mem->SystemHeapAlloc(segment_size_bytes);
used_system_heap = guest_base != 0;
if (!guest_base) {
REXKRNL_ERROR(
"rexcrt_heap: failed to allocate {} bytes from both regular and system heaps",
segment_size_bytes);
return false;
}
REXKRNL_WARN("rexcrt_heap: allocated fallback segment from system heap: {} bytes",
segment_size_bytes);
}
uint8_t* host_base = mem->TranslateVirtual<uint8_t*>(guest_base);
if (!host_base) {
REXKRNL_ERROR("rexcrt_heap: TranslateVirtual failed for guest base 0x{:08X}", guest_base);
if (used_system_heap) {
mem->SystemHeapFree(guest_base);
}
return false;
}
O1HeapInstance* heap = o1heapInit(host_base, segment_size_bytes);
if (!heap) {
REXKRNL_ERROR("rexcrt_heap: o1heapInit failed for segment 0x{:08X} size {}", guest_base,
segment_size_bytes);
if (used_system_heap) {
mem->SystemHeapFree(guest_base);
}
return false;
}
segments_.push_back(
HeapSegment{heap, guest_base, static_cast<uint32_t>(guest_base + segment_size_bytes)});
REXKRNL_INFO("rexcrt_heap: added segment guest=0x{:08X}-0x{:08X} size={}MB", guest_base,
guest_base + segment_size_bytes, segment_size_bytes / (1024 * 1024));
return true;
}
uint32_t ReXHeap::AllocLocked(uint32_t size, bool zero) {
if (size == 0) {
size = 1;
}
const uint32_t alloc_size = size + kHeaderSize;
auto try_allocate_in_segment = [&](HeapSegment& segment) -> uint32_t {
void* ptr = o1heapAllocate(segment.heap, alloc_size);
if (!ptr) {
return 0;
}
auto* hdr = static_cast<SizeHeader*>(ptr);
hdr->requested_size = size;
hdr->reserved = 0;
void* user_ptr = static_cast<uint8_t*>(ptr) + kHeaderSize;
if (zero) {
std::memset(user_ptr, 0, size);
}
return HostToGuest(user_ptr);
};
for (auto& segment : segments_) {
if (uint32_t guest = try_allocate_in_segment(segment)) {
return guest;
}
}
const uint32_t min_required_segment =
rex::align<uint32_t>(alloc_size + kHeaderSize, O1HEAP_ALIGNMENT);
const uint32_t growth_segment_size =
std::max({initial_segment_size_, kDefaultGrowthSegmentSize, min_required_segment});
if (!AllocateSegmentLocked(growth_segment_size)) {
const auto diagnostics = GetDiagnosticsLocked();
bool invariants_ok = true;
for (const auto& segment : segments_) {
invariants_ok = invariants_ok && o1heapDoInvariantsHold(segment.heap);
}
const uint64_t free_bytes = diagnostics.capacity - diagnostics.allocated;
REXKRNL_WARN(
"rexcrt_RtlAllocateHeap: o1heapAllocate({}) failed (segments={}, capacity={}MB, "
"allocated={}MB, free={}MB, oom_count={}, invariants_ok={})",
size, segments_.size(), diagnostics.capacity / (1024 * 1024),
diagnostics.allocated / (1024 * 1024), free_bytes / (1024 * 1024), diagnostics.oom_count,
invariants_ok);
return 0;
}
if (uint32_t guest = try_allocate_in_segment(segments_.back())) {
return guest;
}
const auto diagnostics = GetDiagnosticsLocked();
const uint64_t free_bytes = diagnostics.capacity - diagnostics.allocated;
REXKRNL_WARN(
"rexcrt_RtlAllocateHeap: allocation still failed after growth for {} bytes "
"(segments={}, capacity={}MB, allocated={}MB, free={}MB, oom_count={})",
size, segments_.size(), diagnostics.capacity / (1024 * 1024),
diagnostics.allocated / (1024 * 1024), free_bytes / (1024 * 1024), diagnostics.oom_count);
return 0;
}
ReXHeap::HeapSegment* ReXHeap::FindSegmentByGuestLocked(uint32_t guest_addr) {
auto it = std::find_if(segments_.begin(), segments_.end(), [&](const HeapSegment& segment) {
return guest_addr >= segment.guest_base && guest_addr < segment.guest_end;
});
return it != segments_.end() ? &(*it) : nullptr;
}
const ReXHeap::HeapSegment* ReXHeap::FindSegmentByGuestLocked(uint32_t guest_addr) const {
auto it = std::find_if(segments_.begin(), segments_.end(), [&](const HeapSegment& segment) {
return guest_addr >= segment.guest_base && guest_addr < segment.guest_end;
});
return it != segments_.end() ? &(*it) : nullptr;
}
HeapDiagnostics ReXHeap::GetDiagnosticsLocked() const {
HeapDiagnostics diagnostics{};
for (const auto& segment : segments_) {
const auto d = o1heapGetDiagnostics(segment.heap);
diagnostics.capacity += d.capacity;
diagnostics.allocated += d.allocated;
diagnostics.peak_allocated += d.peak_allocated;
diagnostics.peak_request_size = std::max(diagnostics.peak_request_size, d.peak_request_size);
diagnostics.oom_count += d.oom_count;
}
return diagnostics;
}
// ---------------------------------------------------------------------------
// Global instance + RTL hooks
// ---------------------------------------------------------------------------
ReXHeap g_heap;
ppc_u32_result_t RtlAllocateHeap_entry(ppc_u32_t hHeap, ppc_u32_t dwFlags, ppc_u32_t dwBytes) {
return g_heap.Alloc(dwBytes, dwFlags & HEAP_ZERO_MEMORY);
}
ppc_u32_result_t RtlFreeHeap_entry(ppc_u32_t hHeap, ppc_u32_t dwFlags, ppc_u32_t ptr) {
g_heap.Free(static_cast<uint32_t>(ptr));
return 1;
}
ppc_u32_result_t RtlSizeHeap_entry(ppc_u32_t hHeap, ppc_u32_t dwFlags, ppc_u32_t ptr) {
return g_heap.Size(static_cast<uint32_t>(ptr));
}
ppc_u32_result_t RtlReAllocateHeap_entry(ppc_u32_t hHeap, ppc_u32_t dwFlags, ppc_u32_t ptr,
ppc_u32_t dwBytes) {
return g_heap.Realloc(static_cast<uint32_t>(ptr), dwBytes, dwFlags & HEAP_ZERO_MEMORY);
}
bool InitHeap(uint32_t heap_size_mb, rex::memory::Memory* memory) {
return g_heap.Init(heap_size_mb * 1024u * 1024u, memory);
}
ReXHeap& GetHeap() {
return g_heap;
}
} // namespace rex::kernel::crt
REXCRT_EXPORT(rexcrt_RtlAllocateHeap, rex::kernel::crt::RtlAllocateHeap_entry)
REXCRT_EXPORT(rexcrt_RtlFreeHeap, rex::kernel::crt::RtlFreeHeap_entry)
REXCRT_EXPORT(rexcrt_RtlSizeHeap, rex::kernel::crt::RtlSizeHeap_entry)
REXCRT_EXPORT(rexcrt_RtlReAllocateHeap, rex::kernel::crt::RtlReAllocateHeap_entry)
@@ -1,85 +0,0 @@
/**
* @file kernel/crt/memory.cpp
*
* @brief Native memory operation hooks -- replaces recompiled PPC
* implementations of memcpy, memmove, memset, etc.
*
* @copyright Copyright (c) 2026 Tom Clay <tomc@tctechstuff.com>
* @license BSD 3-Clause License
*/
#include <cstring>
#include <rex/ppc/function.h>
namespace rex::kernel::crt {
// ---------------------------------------------------------------------------
// Standard memory operations
// ---------------------------------------------------------------------------
static void* native_memcpy(void* dst, const void* src, size_t n) {
return std::memcpy(dst, src, n);
}
static void* native_memmove(void* dst, const void* src, size_t n) {
return std::memmove(dst, src, n);
}
static void* native_memset(void* dst, int val, size_t n) {
return std::memset(dst, val, n);
}
static void* native_memchr(const void* ptr, int val, size_t n) {
return const_cast<void*>(std::memchr(ptr, val, n));
}
// ---------------------------------------------------------------------------
// Xbox/VMX-optimized variants (same semantics, native speed)
// ---------------------------------------------------------------------------
static void* native_XMemCpy(void* dst, const void* src, size_t n) {
return std::memcpy(dst, src, n);
}
static void* native_XMemSet(void* dst, int val, size_t n) {
return std::memset(dst, val, n);
}
static void* native_XMemSet128(void* dst, int val, size_t n) {
return std::memset(dst, val, n);
}
static void* native_memset_vmx(void* dst, int val, size_t n) {
return std::memset(dst, val, n);
}
// ---------------------------------------------------------------------------
// Secure variants (return errno_t)
// ---------------------------------------------------------------------------
static int native_memcpy_s(void* dst, size_t dstsz, const void* src, size_t count) {
if (!dst || !src || count > dstsz)
return 22; // EINVAL
std::memcpy(dst, src, count);
return 0;
}
static int native_memmove_s(void* dst, size_t dstsz, const void* src, size_t count) {
if (!dst || !src || count > dstsz)
return 22; // EINVAL
std::memmove(dst, src, count);
return 0;
}
} // namespace rex::kernel::crt
REXCRT_EXPORT(rexcrt_memcpy, rex::kernel::crt::native_memcpy)
REXCRT_EXPORT(rexcrt_memmove, rex::kernel::crt::native_memmove)
REXCRT_EXPORT(rexcrt_memset, rex::kernel::crt::native_memset)
REXCRT_EXPORT(rexcrt_memchr, rex::kernel::crt::native_memchr)
REXCRT_EXPORT(rexcrt_XMemCpy, rex::kernel::crt::native_XMemCpy)
REXCRT_EXPORT(rexcrt_XMemSet, rex::kernel::crt::native_XMemSet)
REXCRT_EXPORT(rexcrt_XMemSet128, rex::kernel::crt::native_XMemSet128)
REXCRT_EXPORT(rexcrt_memset_vmx, rex::kernel::crt::native_memset_vmx)
REXCRT_EXPORT(rexcrt_memcpy_s, rex::kernel::crt::native_memcpy_s)
REXCRT_EXPORT(rexcrt_memmove_s, rex::kernel::crt::native_memmove_s)
@@ -1,120 +0,0 @@
/**
* @file kernel/crt/string.cpp
*
* @brief Native string operation hooks -- replaces recompiled PPC
* implementations of strncmp, strchr, lstrlenA, etc.
*
* @copyright Copyright (c) 2026 Tom Clay <tomc@tctechstuff.com>
* @license BSD 3-Clause License
*/
#include <cstring>
#include <rex/platform.h>
#if !REX_PLATFORM_WIN32
#include <strings.h>
#endif
#include <rex/ppc/function.h>
namespace rex::kernel::crt {
// ---------------------------------------------------------------------------
// C string operations
// ---------------------------------------------------------------------------
static int native_strncmp(const char* s1, const char* s2, size_t n) {
return std::strncmp(s1, s2, n);
}
static char* native_strncpy(char* dst, const char* src, size_t n) {
return std::strncpy(dst, src, n);
}
static char* native_strchr(const char* s, int c) {
return const_cast<char*>(std::strchr(s, c));
}
static char* native_strstr(const char* haystack, const char* needle) {
return const_cast<char*>(std::strstr(haystack, needle));
}
static char* native_strrchr(const char* s, int c) {
return const_cast<char*>(std::strrchr(s, c));
}
static char* native_strtok(char* s, const char* delim) {
return std::strtok(s, delim);
}
static int native_stricmp(const char* s1, const char* s2) {
#if REX_PLATFORM_WIN32
return _stricmp(s1, s2);
#else
return strcasecmp(s1, s2);
#endif
}
static int native_strcpy_s(char* dst, size_t dstsz, const char* src) {
if (!dst || !src || dstsz == 0)
return 22; // EINVAL
#if REX_PLATFORM_WIN32
return strcpy_s(dst, dstsz, src);
#else
const size_t src_len = std::strlen(src);
if (src_len + 1 > dstsz) {
dst[0] = '\0';
return 34; // ERANGE
}
std::memcpy(dst, src, src_len + 1);
return 0;
#endif
}
// ---------------------------------------------------------------------------
// Win32 string functions (lstr*)
// ---------------------------------------------------------------------------
static int native_lstrlenA(const char* s) {
return s ? static_cast<int>(std::strlen(s)) : 0;
}
static char* native_lstrcpyA(char* dst, const char* src) {
return std::strcpy(dst, src);
}
static char* native_lstrcpynA(char* dst, const char* src, int maxlen) {
if (maxlen <= 0)
return dst;
std::strncpy(dst, src, maxlen - 1);
dst[maxlen - 1] = '\0';
return dst;
}
static char* native_lstrcatA(char* dst, const char* src) {
return std::strcat(dst, src);
}
static int native_lstrcmpiA(const char* s1, const char* s2) {
#if REX_PLATFORM_WIN32
return _stricmp(s1, s2);
#else
return strcasecmp(s1, s2);
#endif
}
} // namespace rex::kernel::crt
REXCRT_EXPORT(rexcrt_strncmp, rex::kernel::crt::native_strncmp)
REXCRT_EXPORT(rexcrt_strncpy, rex::kernel::crt::native_strncpy)
REXCRT_EXPORT(rexcrt_strchr, rex::kernel::crt::native_strchr)
REXCRT_EXPORT(rexcrt_strstr, rex::kernel::crt::native_strstr)
REXCRT_EXPORT(rexcrt_strrchr, rex::kernel::crt::native_strrchr)
REXCRT_EXPORT(rexcrt_strtok, rex::kernel::crt::native_strtok)
REXCRT_EXPORT(rexcrt__stricmp, rex::kernel::crt::native_stricmp)
REXCRT_EXPORT(rexcrt_strcpy_s, rex::kernel::crt::native_strcpy_s)
REXCRT_EXPORT(rexcrt_lstrlenA, rex::kernel::crt::native_lstrlenA)
REXCRT_EXPORT(rexcrt_lstrcpyA, rex::kernel::crt::native_lstrcpyA)
REXCRT_EXPORT(rexcrt_lstrcpynA, rex::kernel::crt::native_lstrcpynA)
REXCRT_EXPORT(rexcrt_lstrcatA, rex::kernel::crt::native_lstrcatA)
REXCRT_EXPORT(rexcrt_lstrcmpiA, rex::kernel::crt::native_lstrcmpiA)
@@ -1,13 +0,0 @@
/**
******************************************************************************
* Xenia : Xbox 360 Emulator Research Project *
******************************************************************************
* Copyright 2021 Ben Vanik. All rights reserved. *
* Released under the BSD license - see LICENSE in the root for more details. *
******************************************************************************
*/
// Post-include file for an export table.
#undef FLAG
#undef XE_EXPORT
@@ -1,28 +0,0 @@
/**
******************************************************************************
* Xenia : Xbox 360 Emulator Research Project *
******************************************************************************
* Copyright 2021 Ben Vanik. All rights reserved. *
* Released under the BSD license - see LICENSE in the root for more details. *
******************************************************************************
*/
/**
* Pre-include file for an export table.
* Use this to build tables of exports:
*
* // Build the export table used for resolution.
* #include "rex/kernel/util/export_table_pre.inc"
* static Export my_module_export_table[] = {
* #include "xenia/kernel/my_module/my_module_table.inc"
* };
* #include "rex/kernel/util/export_table_post.inc"
* export_resolver_->RegisterTable(
* "my_module.xex",
* my_module_export_table, rex::countof(my_module_export_table));
*/
#define XE_EXPORT(module, ordinal, name, type) \
rex::runtime::Export(ordinal, rex::runtime::Export::Type::type, #name)
#define FLAG(t) kXEKernelExportFlag##t
@@ -1,35 +0,0 @@
/**
* @file kernel/kernel_init.cpp
* @brief Kernel initialization - loads kernel modules and registers apps
*
* @copyright Copyright (c) 2026 Tom Clay <tomc@tctechstuff.com>
* All rights reserved.
*
* @license BSD 3-Clause License
* See LICENSE file in the project root for full license text.
*/
#include <rex/kernel/init.h>
#include <rex/kernel/xam/apps/app.h>
#include <rex/kernel/xam/apps/xgi_app.h>
#include <rex/kernel/xam/apps/xlivebase_app.h>
#include <rex/kernel/xam/apps/xmp_app.h>
#include <rex/kernel/xam/module.h>
#include <rex/kernel/xboxkrnl/module.h>
#include <rex/runtime.h>
#include <rex/system/kernel_state.h>
namespace rex::kernel {
void InitializeKernel(Runtime* runtime, system::KernelState* kernel_state) {
auto* app_mgr = kernel_state->app_manager();
app_mgr->RegisterApp(std::make_unique<xam::apps::XmpApp>(kernel_state));
app_mgr->RegisterApp(std::make_unique<xam::apps::XgiApp>(kernel_state));
app_mgr->RegisterApp(std::make_unique<xam::apps::XLiveBaseApp>(kernel_state));
app_mgr->RegisterApp(std::make_unique<xam::apps::XamApp>(kernel_state));
kernel_state->LoadKernelModule<xboxkrnl::XboxkrnlModule>();
kernel_state->LoadKernelModule<xam::XamModule>();
}
} // namespace rex::kernel
@@ -1,113 +0,0 @@
/**
******************************************************************************
* Xenia : Xbox 360 Emulator Research Project *
******************************************************************************
* Copyright 2021 Ben Vanik. All rights reserved. *
* Released under the BSD license - see LICENSE in the root for more details. *
******************************************************************************
*
* @modified Tom Clay, 2026 - Adapted for ReXGlue runtime
*/
#include <rex/kernel/xam/apps/app.h>
#include <rex/logging.h>
#include <rex/system/kernel_state.h>
#include <rex/system/xenumerator.h>
#include <rex/thread.h>
namespace rex {
namespace kernel {
namespace xam {
using namespace rex::system;
using namespace rex::system::xam;
namespace apps {
using namespace rex::system;
XamApp::XamApp(KernelState* kernel_state) : App(kernel_state, 0xFE) {}
X_HRESULT XamApp::DispatchMessageSync(uint32_t message, uint32_t buffer_ptr,
uint32_t buffer_length) {
// NOTE: buffer_length may be zero or valid.
auto buffer = memory_->TranslateVirtual(buffer_ptr);
switch (message) {
case 0x0002000E: {
struct message_data {
rex::be<uint32_t> user_index;
rex::be<uint32_t> unk_04;
rex::be<uint32_t> extra_ptr;
rex::be<uint32_t> buffer_ptr;
rex::be<uint32_t> buffer_size;
rex::be<uint32_t> unk_14;
rex::be<uint32_t> length_ptr;
rex::be<uint32_t> unk_1C;
}* data = reinterpret_cast<message_data*>(buffer);
REXKRNL_DEBUG(
"XamAppEnumerateContentAggregate({}, {:08X}, {:08X}, {:08X}, {}, "
"{:08X}, {:08X}, {:08X})",
(uint32_t)data->user_index, (uint32_t)data->unk_04, (uint32_t)data->extra_ptr,
(uint32_t)data->buffer_ptr, (uint32_t)data->buffer_size, (uint32_t)data->unk_14,
(uint32_t)data->length_ptr, (uint32_t)data->unk_1C);
auto extra = memory_->TranslateVirtual<X_KENUMERATOR_CONTENT_AGGREGATE*>(data->extra_ptr);
auto buffer = memory_->TranslateVirtual(data->buffer_ptr);
auto e = kernel_state_->object_table()->LookupObject<XEnumerator>(extra->handle);
if (!e || !buffer || !extra) {
return X_E_INVALIDARG;
}
assert_true(extra->magic == kXObjSignature);
if (data->buffer_size) {
std::memset(buffer, 0, data->buffer_size);
}
uint32_t item_count = 0;
auto result = e->WriteItems(data->buffer_ptr, buffer, &item_count);
if (result == X_ERROR_SUCCESS && item_count >= 1) {
if (data->length_ptr) {
auto length_ptr = memory_->TranslateVirtual<be<uint32_t>*>(data->length_ptr);
*length_ptr = 1;
}
return X_E_SUCCESS;
}
return X_E_NO_MORE_FILES;
}
case 0x00020021: {
struct message_data {
char unk_00[64];
rex::be<uint32_t> unk_40; // KeGetCurrentProcessType() < 1 ? 1 : 0
rex::be<uint32_t> unk_44; // ? output_ptr ?
rex::be<uint32_t> unk_48; // ? overlapped_ptr ?
}* data = reinterpret_cast<message_data*>(buffer);
assert_true(buffer_length == sizeof(message_data));
auto unk = memory_->TranslateVirtual<rex::be<uint32_t>*>(data->unk_44);
*unk = 0;
REXKRNL_DEBUG("XamApp(0x00020021)('{}', {:08X}, {:08X}, {:08X})", data->unk_00,
(uint32_t)data->unk_40, (uint32_t)data->unk_44, (uint32_t)data->unk_48);
return X_E_SUCCESS;
}
case 0x00021012: {
REXKRNL_DEBUG("XamApp(0x00021012)");
return X_E_SUCCESS;
}
case 0x00022005: {
struct message_data {
rex::be<uint32_t> unk_00; // ? output_ptr ?
rex::be<uint32_t> unk_04; // ? value/jump to? ?
}* data = reinterpret_cast<message_data*>(buffer);
assert_true(buffer_length == sizeof(message_data));
auto unk = memory_->TranslateVirtual<rex::be<uint32_t>*>(data->unk_00);
auto adr = *unk;
REXKRNL_DEBUG("XamApp(0x00022005)(%.8X, %.8X)", (uint32_t)data->unk_00,
(uint32_t)data->unk_04);
return X_E_SUCCESS;
}
}
REXKRNL_ERROR(
"Unimplemented XAM message app={:08X}, msg={:08X}, arg1={:08X}, "
"arg2={:08X}",
app_id(), message, buffer_ptr, buffer_length);
return X_E_FAIL;
}
} // namespace apps
} // namespace xam
} // namespace kernel
} // namespace rex
@@ -1,145 +0,0 @@
/**
******************************************************************************
* Xenia : Xbox 360 Emulator Research Project *
******************************************************************************
* Copyright 2021 Ben Vanik. All rights reserved. *
* Released under the BSD license - see LICENSE in the root for more details. *
******************************************************************************
*
* @modified Tom Clay, 2026 - Adapted for ReXGlue runtime
*/
#include <rex/kernel/xam/apps/xgi_app.h>
#include <rex/logging.h>
#include <rex/thread.h>
namespace rex {
namespace kernel {
namespace xam {
using namespace rex::system;
using namespace rex::system::xam;
namespace apps {
using namespace rex::system;
XgiApp::XgiApp(KernelState* kernel_state) : App(kernel_state, 0xFB) {}
// http://mb.mirage.org/bugzilla/xliveless/main.c
X_HRESULT XgiApp::DispatchMessageSync(uint32_t message, uint32_t buffer_ptr,
uint32_t buffer_length) {
// NOTE: buffer_length may be zero or valid.
auto buffer = memory_->TranslateVirtual(buffer_ptr);
switch (message) {
case 0x000B0006: {
assert_true(!buffer_length || buffer_length == 24);
// dword r3 user index
// dword (unwritten?)
// qword 0
// dword r4 context enum
// dword r5 value
uint32_t user_index = memory::load_and_swap<uint32_t>(buffer + 0);
uint32_t context_id = memory::load_and_swap<uint32_t>(buffer + 16);
uint32_t context_value = memory::load_and_swap<uint32_t>(buffer + 20);
REXKRNL_DEBUG("XGIUserSetContextEx({:08X}, {:08X}, {:08X})", user_index, context_id,
context_value);
return X_E_SUCCESS;
}
case 0x000B0007: {
uint32_t user_index = memory::load_and_swap<uint32_t>(buffer + 0);
uint32_t property_id = memory::load_and_swap<uint32_t>(buffer + 16);
uint32_t value_size = memory::load_and_swap<uint32_t>(buffer + 20);
uint32_t value_ptr = memory::load_and_swap<uint32_t>(buffer + 24);
REXKRNL_DEBUG("XGIUserSetPropertyEx({:08X}, {:08X}, {}, {:08X})", user_index, property_id,
value_size, value_ptr);
return X_E_SUCCESS;
}
case 0x000B0008: {
assert_true(!buffer_length || buffer_length == 8);
uint32_t achievement_count = memory::load_and_swap<uint32_t>(buffer + 0);
uint32_t achievements_ptr = memory::load_and_swap<uint32_t>(buffer + 4);
REXKRNL_DEBUG("XGIUserWriteAchievements({:08X}, {:08X})", achievement_count,
achievements_ptr);
return X_E_SUCCESS;
}
case 0x000B0010: {
assert_true(!buffer_length || buffer_length == 28);
// Sequence:
// - XamSessionCreateHandle
// - XamSessionRefObjByHandle
// - [this]
// - CloseHandle
uint32_t session_ptr = memory::load_and_swap<uint32_t>(buffer + 0x0);
uint32_t flags = memory::load_and_swap<uint32_t>(buffer + 0x4);
uint32_t num_slots_public = memory::load_and_swap<uint32_t>(buffer + 0x8);
uint32_t num_slots_private = memory::load_and_swap<uint32_t>(buffer + 0xC);
uint32_t user_xuid = memory::load_and_swap<uint32_t>(buffer + 0x10);
uint32_t session_info_ptr = memory::load_and_swap<uint32_t>(buffer + 0x14);
uint32_t nonce_ptr = memory::load_and_swap<uint32_t>(buffer + 0x18);
REXKRNL_DEBUG(
"XGISessionCreateImpl({:08X}, {:08X}, {}, {}, {:08X}, {:08X}, "
"{:08X})",
session_ptr, flags, num_slots_public, num_slots_private, user_xuid, session_info_ptr,
nonce_ptr);
return X_E_SUCCESS;
}
case 0x000B0011: {
// TODO(PermaNull): reverse buffer contents.
REXKRNL_DEBUG("XGISessionDelete");
return X_STATUS_SUCCESS;
}
case 0x000B0012: {
assert_true(buffer_length == 0x14);
uint32_t session_ptr = memory::load_and_swap<uint32_t>(buffer + 0x0);
uint32_t user_count = memory::load_and_swap<uint32_t>(buffer + 0x4);
uint32_t unk_0 = memory::load_and_swap<uint32_t>(buffer + 0x8);
uint32_t user_index_array = memory::load_and_swap<uint32_t>(buffer + 0xC);
uint32_t private_slots_array = memory::load_and_swap<uint32_t>(buffer + 0x10);
assert_zero(unk_0);
REXKRNL_DEBUG("XGISessionJoinLocal({:08X}, {}, {}, {:08X}, {:08X})", session_ptr, user_count,
unk_0, user_index_array, private_slots_array);
return X_E_SUCCESS;
}
case 0x000B0014: {
// Gets 584107FB in game.
// get high score table?
REXKRNL_DEBUG("XGI_unknown");
return X_STATUS_SUCCESS;
}
case 0x000B0015: {
// send high scores?
REXKRNL_DEBUG("XGI_unknown");
return X_STATUS_SUCCESS;
}
case 0x000B0041: {
assert_true(!buffer_length || buffer_length == 32);
// 00000000 2789fecc 00000000 00000000 200491e0 00000000 200491f0 20049340
uint32_t user_index = memory::load_and_swap<uint32_t>(buffer + 0);
uint32_t context_ptr = memory::load_and_swap<uint32_t>(buffer + 16);
auto context = context_ptr ? memory_->TranslateVirtual(context_ptr) : nullptr;
uint32_t context_id = context ? memory::load_and_swap<uint32_t>(context + 0) : 0;
REXKRNL_DEBUG("XGIUserGetContext({:08X}, {:08X}{:08X}))", user_index, context_ptr,
context_id);
uint32_t value = 0;
if (context) {
memory::store_and_swap<uint32_t>(context + 4, value);
}
return X_E_FAIL;
}
case 0x000B0071: {
REXKRNL_DEBUG("XGI 0x000B0071, unimplemented");
return X_E_SUCCESS;
}
}
REXKRNL_ERROR(
"Unimplemented XGI message app={:08X}, msg={:08X}, arg1={:08X}, "
"arg2={:08X}",
app_id(), message, buffer_ptr, buffer_length);
return X_E_FAIL;
}
} // namespace apps
} // namespace xam
} // namespace kernel
} // namespace rex

Some files were not shown because too many files have changed in this diff Show More