mirror of
https://github.com/sal063/AC6_recomp
synced 2026-06-26 18:42:04 -04:00
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:
+2
-2
@@ -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
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
-46
@@ -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
|
||||
-105
@@ -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
|
||||
-103
@@ -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
|
||||
-17
@@ -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);
|
||||
Vendored
-32
@@ -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
|
||||
Vendored
-67
@@ -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
|
||||
Vendored
-35
@@ -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
|
||||
-285
@@ -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
|
||||
-92
@@ -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
|
||||
-46
@@ -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
|
||||
Vendored
-42
@@ -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
|
||||
Vendored
-81
@@ -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
|
||||
-27
@@ -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
|
||||
)
|
||||
-24
@@ -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
|
||||
-493
@@ -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
|
||||
Vendored
-37
@@ -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
|
||||
Vendored
-516
@@ -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
|
||||
Vendored
-66
@@ -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
|
||||
-760
@@ -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
|
||||
-365
@@ -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
|
||||
-39
@@ -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 ®_info; \
|
||||
}
|
||||
#include <rex/audio/xma/register_table.inc>
|
||||
#undef XE_XMA_REGISTER
|
||||
default:
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace rex::audio
|
||||
-85
@@ -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
|
||||
)
|
||||
|
||||
Vendored
-263
@@ -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);
|
||||
Vendored
-389
@@ -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);
|
||||
backup/pre_audio_clean_slate_20260406_213859/thirdparty/rexglue-sdk/include/rex/audio/audio_driver.h
Vendored
-44
@@ -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
|
||||
Vendored
-825
@@ -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
|
||||
-524
@@ -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 ®_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
Reference in New Issue
Block a user