[Kernel/XMP] Add title-specific file I/O hooks for XMP volume sync
Some games persist BGM volume through their own save files but never call XMPSetVolume. Add a TitlePatch system that intercepts NtReadFile/ NtWriteFile to apply game-specific logic as well as logic to identify the memory location that the volume data is read into and monitor that memory location for changes, updating XMP player volume if it changes during gameplay. Includes patches for known games with this issue: Dead or Alive Xtreme 2 (544307D2), PGR4 (4D5307F9) and PGR3 (4D5307D1)
This commit is contained in:
@@ -407,6 +407,9 @@ void KernelState::SetExecutableModule(object_ref<UserModule> module) {
|
||||
xboxkrnl::XboxkrnlModule::kExLoadedCommandLineSize);
|
||||
}
|
||||
|
||||
// Initialize file I/O hooks for XMP volume title-specific patches.
|
||||
InitXmpVolumePatch();
|
||||
|
||||
// Spin up deferred dispatch worker.
|
||||
// TODO(benvanik): move someplace more appropriate (out of ctor, but around
|
||||
// here).
|
||||
@@ -738,8 +741,13 @@ void KernelState::UnloadUserModule(const object_ref<UserModule>& module,
|
||||
object_table()->ReleaseHandleInLock(module->handle());
|
||||
}
|
||||
|
||||
void KernelState::InitXmpVolumePatch() {
|
||||
xmp_volume_patch_ = XmpVolumePatch::CreateForTitle(title_id(), this);
|
||||
}
|
||||
|
||||
void KernelState::TerminateTitle() {
|
||||
XELOGD("KernelState::TerminateTitle");
|
||||
xmp_volume_patch_.reset();
|
||||
auto global_lock = global_critical_region_.Acquire();
|
||||
|
||||
// Call terminate routines.
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
#include "xenia/kernel/util/kernel_fwd.h"
|
||||
#include "xenia/kernel/util/native_list.h"
|
||||
#include "xenia/kernel/util/object_table.h"
|
||||
#include "xenia/kernel/util/xmp_volume_patch.h"
|
||||
#include "xenia/kernel/xam/achievement_manager.h"
|
||||
#include "xenia/kernel/xam/app_manager.h"
|
||||
#include "xenia/kernel/xam/content_manager.h"
|
||||
@@ -196,6 +197,9 @@ class KernelState {
|
||||
return xam_state()->content_manager();
|
||||
}
|
||||
|
||||
XmpVolumePatch* xmp_volume_patch() const { return xmp_volume_patch_.get(); }
|
||||
void InitXmpVolumePatch();
|
||||
|
||||
std::bitset<4> GetConnectedUsers() const;
|
||||
|
||||
// Access must be guarded by the global critical region.
|
||||
@@ -351,6 +355,7 @@ class KernelState {
|
||||
vfs::VirtualFileSystem* file_system_;
|
||||
std::unique_ptr<xam::XamState> xam_state_;
|
||||
std::unique_ptr<SystemManagementController> smc_;
|
||||
std::unique_ptr<XmpVolumePatch> xmp_volume_patch_;
|
||||
|
||||
KernelVersion kernel_version_;
|
||||
|
||||
|
||||
130
src/xenia/kernel/util/xmp_volume_patch.cc
Normal file
130
src/xenia/kernel/util/xmp_volume_patch.cc
Normal file
@@ -0,0 +1,130 @@
|
||||
/**
|
||||
******************************************************************************
|
||||
* Xenia : Xbox 360 Emulator Research Project *
|
||||
******************************************************************************
|
||||
* Copyright 2026 Ben Vanik. All rights reserved. *
|
||||
* Released under the BSD license - see LICENSE in the root for more details. *
|
||||
******************************************************************************
|
||||
*/
|
||||
|
||||
#include "xenia/kernel/util/xmp_volume_patch.h"
|
||||
|
||||
#include "xenia/apu/audio_media_player.h"
|
||||
#include "xenia/base/logging.h"
|
||||
#include "xenia/emulator.h"
|
||||
#include "xenia/kernel/kernel_state.h"
|
||||
#include "xenia/memory.h"
|
||||
|
||||
namespace xe {
|
||||
namespace kernel {
|
||||
|
||||
// Convert a volume byte to a float in [0.0001, 1.0]. Uses a near-zero floor
|
||||
// instead of 0.0f so the audio player doesn't interpret it as "never set"
|
||||
// and override with the default volume.
|
||||
static float ByteToVolume(uint8_t value, uint8_t max_value) {
|
||||
if (value == 0) {
|
||||
return 0.0001f;
|
||||
}
|
||||
return static_cast<float>(value) / static_cast<float>(max_value);
|
||||
}
|
||||
|
||||
bool XmpVolumePatch::Matches(const std::string& path, uint32_t length) const {
|
||||
if (path.find(config_.file_name) == std::string::npos) {
|
||||
return false;
|
||||
}
|
||||
if (config_.write_length != 0) {
|
||||
return length == config_.write_length;
|
||||
}
|
||||
return length >= config_.offset + 1;
|
||||
}
|
||||
|
||||
void XmpVolumePatch::ApplyVolume(const uint8_t* buffer, uint32_t length) {
|
||||
uint8_t volume_byte = buffer[config_.offset];
|
||||
if (volume_byte > config_.max_value) {
|
||||
return;
|
||||
}
|
||||
|
||||
float volume = ByteToVolume(volume_byte, config_.max_value);
|
||||
|
||||
XELOGD("XmpVolumePatch: volume={}/{} -> {:.2f}", volume_byte,
|
||||
config_.max_value, volume);
|
||||
|
||||
last_volume_ = volume_byte;
|
||||
auto* player = kernel_state_->emulator()->audio_media_player();
|
||||
if (player) {
|
||||
player->SetVolume(volume);
|
||||
}
|
||||
}
|
||||
|
||||
void XmpVolumePatch::PollGuestVolume() {
|
||||
if (!settings_guest_addr_) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto* mem = kernel_state_->memory();
|
||||
uint32_t vol_addr = settings_guest_addr_ + config_.offset;
|
||||
uint8_t current = mem->TranslateVirtual<uint8_t*>(vol_addr)[0];
|
||||
|
||||
if (current == last_volume_ || current > config_.max_value) {
|
||||
return;
|
||||
}
|
||||
|
||||
last_volume_ = current;
|
||||
float volume = ByteToVolume(current, config_.max_value);
|
||||
|
||||
XELOGD("XmpVolumePatch: polled volume change={}/{} -> {:.2f}", current,
|
||||
config_.max_value, volume);
|
||||
|
||||
auto* player = kernel_state_->emulator()->audio_media_player();
|
||||
if (player) {
|
||||
player->SetVolume(volume);
|
||||
}
|
||||
}
|
||||
|
||||
void XmpVolumePatch::OnFileWrite(const std::string& path, const uint8_t* buffer,
|
||||
uint32_t length, uint32_t guest_address) {
|
||||
if (Matches(path, length)) {
|
||||
ApplyVolume(buffer, length);
|
||||
}
|
||||
}
|
||||
|
||||
void XmpVolumePatch::OnFileRead(const std::string& path, const uint8_t* buffer,
|
||||
uint32_t length, uint32_t guest_address) {
|
||||
if (Matches(path, length)) {
|
||||
settings_guest_addr_ = guest_address;
|
||||
ApplyVolume(buffer, length);
|
||||
}
|
||||
}
|
||||
|
||||
void XmpVolumePatch::OnInputPoll(uint32_t packet_number) {
|
||||
if (packet_number == last_packet_number_) {
|
||||
return;
|
||||
}
|
||||
last_packet_number_ = packet_number;
|
||||
PollGuestVolume();
|
||||
}
|
||||
|
||||
std::unique_ptr<XmpVolumePatch> XmpVolumePatch::CreateForTitle(
|
||||
uint32_t title_id, KernelState* ks) {
|
||||
XmpVolumeConfig config;
|
||||
|
||||
switch (title_id) {
|
||||
case 0x544307D2: // DOAX2
|
||||
config = {"ups.dat", 0, 0x50, 100};
|
||||
break;
|
||||
case 0x4D5307D1: // PGR3
|
||||
config = {"profile.save", 528, 19, 10};
|
||||
break;
|
||||
case 0x4D5307F9: // PGR4
|
||||
config = {"savegame", 344, 2, 10};
|
||||
break;
|
||||
default:
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
XELOGI("XmpVolumePatch: Loading for title {:08X}", title_id);
|
||||
return std::make_unique<XmpVolumePatch>(ks, config);
|
||||
}
|
||||
|
||||
} // namespace kernel
|
||||
} // namespace xe
|
||||
70
src/xenia/kernel/util/xmp_volume_patch.h
Normal file
70
src/xenia/kernel/util/xmp_volume_patch.h
Normal file
@@ -0,0 +1,70 @@
|
||||
/**
|
||||
******************************************************************************
|
||||
* Xenia : Xbox 360 Emulator Research Project *
|
||||
******************************************************************************
|
||||
* Copyright 2026 Ben Vanik. All rights reserved. *
|
||||
* Released under the BSD license - see LICENSE in the root for more details. *
|
||||
******************************************************************************
|
||||
*/
|
||||
|
||||
#ifndef XENIA_KERNEL_UTIL_XMP_VOLUME_PATCH_H_
|
||||
#define XENIA_KERNEL_UTIL_XMP_VOLUME_PATCH_H_
|
||||
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
namespace xe {
|
||||
namespace kernel {
|
||||
|
||||
class KernelState;
|
||||
|
||||
// Patch for games that store their BGM volume in a save file but never call
|
||||
// XMPSetVolume. Each game just needs to specify:
|
||||
// - file_name: substring to match in the file path
|
||||
// - write_length: exact length of the write containing the volume byte
|
||||
// (0 to match any write >= offset + 1)
|
||||
// - offset: byte offset of the volume value within the write buffer
|
||||
// - max_value: maximum volume value (maps to 1.0f)
|
||||
//
|
||||
// On savegame read/write, the patch reads the volume byte and syncs it to
|
||||
// the XMP audio player. It also captures the guest memory address of the
|
||||
// buffer and re-reads it on controller input changes to detect in-game
|
||||
// volume adjustments before they are saved.
|
||||
struct XmpVolumeConfig {
|
||||
const char* file_name;
|
||||
uint32_t write_length;
|
||||
uint32_t offset;
|
||||
uint8_t max_value;
|
||||
};
|
||||
|
||||
class XmpVolumePatch {
|
||||
public:
|
||||
XmpVolumePatch(KernelState* kernel_state, const XmpVolumeConfig& config)
|
||||
: kernel_state_(kernel_state), config_(config) {}
|
||||
|
||||
void OnFileWrite(const std::string& path, const uint8_t* buffer,
|
||||
uint32_t length, uint32_t guest_address);
|
||||
void OnFileRead(const std::string& path, const uint8_t* buffer,
|
||||
uint32_t length, uint32_t guest_address);
|
||||
void OnInputPoll(uint32_t packet_number);
|
||||
|
||||
static std::unique_ptr<XmpVolumePatch> CreateForTitle(uint32_t title_id,
|
||||
KernelState* ks);
|
||||
|
||||
private:
|
||||
bool Matches(const std::string& path, uint32_t length) const;
|
||||
void ApplyVolume(const uint8_t* buffer, uint32_t length);
|
||||
void PollGuestVolume();
|
||||
|
||||
KernelState* kernel_state_;
|
||||
XmpVolumeConfig config_;
|
||||
uint32_t settings_guest_addr_ = 0;
|
||||
uint8_t last_volume_ = 0xFF;
|
||||
uint32_t last_packet_number_ = 0;
|
||||
};
|
||||
|
||||
} // namespace kernel
|
||||
} // namespace xe
|
||||
|
||||
#endif // XENIA_KERNEL_UTIL_XMP_VOLUME_PATCH_H_
|
||||
@@ -121,11 +121,22 @@ dword_result_t XamInputGetState_entry(dword_t user_index, dword_t flags,
|
||||
actual_user_index = 0;
|
||||
}
|
||||
|
||||
X_RESULT result;
|
||||
auto input_system = kernel_state()->emulator()->input_system();
|
||||
auto lock = input_system->lock();
|
||||
return input_system->GetState(
|
||||
user_index, !flags ? X_INPUT_FLAG::X_INPUT_FLAG_GAMEPAD : flags,
|
||||
input_state);
|
||||
{
|
||||
auto lock = input_system->lock();
|
||||
result = input_system->GetState(
|
||||
user_index, !flags ? X_INPUT_FLAG::X_INPUT_FLAG_GAMEPAD : flags,
|
||||
input_state);
|
||||
}
|
||||
|
||||
if (input_state && result == X_ERROR_SUCCESS) {
|
||||
if (auto patch = kernel_state()->xmp_volume_patch()) {
|
||||
patch->OnInputPoll(input_state->packet_number);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
DECLARE_XAM_EXPORT2(XamInputGetState, kInput, kImplemented, kHighFrequency);
|
||||
|
||||
|
||||
@@ -171,6 +171,15 @@ dword_result_t NtReadFile_entry(dword_t file_handle, dword_t event_handle,
|
||||
// Mark that we should signal the event now. We do this after
|
||||
// we have written the info out.
|
||||
signal_event = true;
|
||||
|
||||
if (XSUCCEEDED(result)) {
|
||||
if (auto patch = kernel_state()->xmp_volume_patch()) {
|
||||
auto host_buf =
|
||||
kernel_memory()->TranslateVirtual(buffer.guest_address());
|
||||
patch->OnFileRead(file->entry()->name(), host_buf, buffer_length,
|
||||
buffer.guest_address());
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// TODO(benvanik): async.
|
||||
|
||||
@@ -346,6 +355,15 @@ dword_result_t NtWriteFile_entry(dword_t file_handle, dword_t event_handle,
|
||||
// Mark that we should signal the event now. We do this after
|
||||
// we have written the info out.
|
||||
signal_event = true;
|
||||
|
||||
if (XSUCCEEDED(result)) {
|
||||
if (auto patch = kernel_state()->xmp_volume_patch()) {
|
||||
auto host_buf =
|
||||
kernel_memory()->TranslateVirtual(buffer.guest_address());
|
||||
patch->OnFileWrite(file->entry()->name(), host_buf, buffer_length,
|
||||
buffer.guest_address());
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// X_STATUS_PENDING if not returning immediately.
|
||||
result = X_STATUS_PENDING;
|
||||
|
||||
Reference in New Issue
Block a user