diff --git a/src/xenia/kernel/kernel_state.cc b/src/xenia/kernel/kernel_state.cc index e7a646674..3bbad27be 100644 --- a/src/xenia/kernel/kernel_state.cc +++ b/src/xenia/kernel/kernel_state.cc @@ -407,6 +407,9 @@ void KernelState::SetExecutableModule(object_ref 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& 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. diff --git a/src/xenia/kernel/kernel_state.h b/src/xenia/kernel/kernel_state.h index 8095c3fa7..2e1a99a76 100644 --- a/src/xenia/kernel/kernel_state.h +++ b/src/xenia/kernel/kernel_state.h @@ -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_state_; std::unique_ptr smc_; + std::unique_ptr xmp_volume_patch_; KernelVersion kernel_version_; diff --git a/src/xenia/kernel/util/xmp_volume_patch.cc b/src/xenia/kernel/util/xmp_volume_patch.cc new file mode 100644 index 000000000..02fa661e0 --- /dev/null +++ b/src/xenia/kernel/util/xmp_volume_patch.cc @@ -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(value) / static_cast(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(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::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(ks, config); +} + +} // namespace kernel +} // namespace xe diff --git a/src/xenia/kernel/util/xmp_volume_patch.h b/src/xenia/kernel/util/xmp_volume_patch.h new file mode 100644 index 000000000..9ff9a9fa9 --- /dev/null +++ b/src/xenia/kernel/util/xmp_volume_patch.h @@ -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 +#include +#include + +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 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_ diff --git a/src/xenia/kernel/xam/xam_input.cc b/src/xenia/kernel/xam/xam_input.cc index 2a54e58fd..4d8487b15 100644 --- a/src/xenia/kernel/xam/xam_input.cc +++ b/src/xenia/kernel/xam/xam_input.cc @@ -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); diff --git a/src/xenia/kernel/xboxkrnl/xboxkrnl_io.cc b/src/xenia/kernel/xboxkrnl/xboxkrnl_io.cc index 63755db85..bed943647 100644 --- a/src/xenia/kernel/xboxkrnl/xboxkrnl_io.cc +++ b/src/xenia/kernel/xboxkrnl/xboxkrnl_io.cc @@ -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;