diff --git a/src/xenia/kernel/util/frame_state_probe.h b/src/xenia/kernel/util/frame_state_probe.h new file mode 100644 index 000000000..ff165a3cf --- /dev/null +++ b/src/xenia/kernel/util/frame_state_probe.h @@ -0,0 +1,197 @@ +/** + ****************************************************************************** + * 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_FRAME_STATE_PROBE_H_ +#define XENIA_KERNEL_UTIL_FRAME_STATE_PROBE_H_ + +// A per-guest-frame sampler of guest memory, for reverse engineering. +// +// Reading guest RAM from outside the emulator (Canary backs it with a +// /dev/shm file, so a host process can pread it) is easy but UNSYNCHRONISED: +// the reader has no idea where the guest is in its update, so successive reads +// are separated by an unknown, jittering number of guest updates. Measuring a +// rate that way aliases badly -- a project measuring the craft's angular +// velocity got 3x swings between adjacent 0.25 s windows purely from sampling. +// +// This samples from INSIDE, once per VdSwap, i.e. exactly once per guest frame +// at a fixed point in it. Consecutive lines are then one frame apart by +// construction, and the frame counter is exact. +// +// Which bytes to sample is not known at launch (object addresses are found by +// scanning at runtime), so the regions are read from a small control file that +// is re-read whenever its mtime changes -- the same trick the file input pad +// uses. Format, one region per line, '#' comments ignored: +// +// 0x40D10590 128 a guest VA and a byte count +// +// Output, one line per frame, appended to --frame_probe_log: +// +// F H G R0 R1 ... +// +// Disabled unless --frame_probe_log is set, and then costs one stat() plus the +// listed reads per frame. + +#include +#include +#include +#include +#include + +#include + +#include "xenia/base/cvar.h" +#include "xenia/base/clock.h" +#include "xenia/base/logging.h" +#include "xenia/memory.h" + +DEFINE_string(frame_probe_log, "", + "RE: append one line of guest state per frame to this file. " + "Empty disables the probe entirely.", + "RE"); +DEFINE_string(frame_probe, "/tmp/xenia_frame_probe.txt", + "RE: control file listing the guest regions --frame_probe_log " + "samples, one ' ' per line. Re-read when it changes.", + "RE"); + +namespace xe { +namespace kernel { +namespace util { + +class FrameStateProbe { + public: + static FrameStateProbe& instance() { + static FrameStateProbe probe; + return probe; + } + + // Called once per guest frame, from VdSwap. + void Sample(Memory* memory) { + if (cvars::frame_probe_log.empty() || !memory) { + return; + } + ++frame_; + ReloadIfChanged(); + if (regions_.empty()) { + return; + } + if (!out_) { + out_ = std::fopen(cvars::frame_probe_log.c_str(), "a"); + if (!out_) { + XELOGE("[frame-probe] cannot open {}", cvars::frame_probe_log); + // Do not retry every frame on a bad path. + cvars::frame_probe_log.clear(); + return; + } + } + std::fprintf(out_, "F %" PRIu64 " H %" PRIu64 " G %" PRIu64, frame_, + Clock::QueryHostSystemTime(), Clock::QueryGuestTickCount()); + for (size_t i = 0; i < regions_.size(); ++i) { + const auto& r = regions_[i]; + std::fprintf(out_, " R%zu ", i); + if (!ReadRegion(memory, r.address, r.length)) { + std::fprintf(out_, "-"); + continue; + } + for (uint32_t b = 0; b < r.length; ++b) { + std::fprintf(out_, "%02x", scratch_[b]); + } + } + std::fputc('\n', out_); + // Flushed per frame on purpose: the host analysis reads this file while the + // game is still flying, and a run can end in a crash or a pkill -9. + std::fflush(out_); + } + + private: + static constexpr uint32_t kMaxRegionBytes = 1024; + static constexpr size_t kMaxRegions = 8; + + struct Region { + uint32_t address; + uint32_t length; + }; + + // Copies out first so the line cannot be torn across a guest write mid-print. + bool ReadRegion(Memory* memory, uint32_t address, uint32_t length) { + auto heap = memory->LookupHeap(address); + if (!heap) { + return false; + } + uint32_t protect = 0; + if (!heap->QueryProtect(address, &protect) || !protect) { + return false; + } + if (!heap->QueryProtect(address + length - 1, &protect) || !protect) { + return false; + } + std::memcpy(scratch_, memory->TranslateVirtual(address), length); + return true; + } + + void ReloadIfChanged() { + struct stat st; + if (::stat(cvars::frame_probe.c_str(), &st) != 0) { + if (!regions_.empty()) { + regions_.clear(); + XELOGI("[frame-probe] control file gone -- sampling stopped"); + } + return; + } + const int64_t stamp = static_cast(st.st_mtim.tv_sec) * 1000000000 + + st.st_mtim.tv_nsec; + if (stamp == stamp_ && st.st_size == size_) { + return; + } + stamp_ = stamp; + size_ = st.st_size; + Parse(); + } + + void Parse() { + regions_.clear(); + FILE* f = std::fopen(cvars::frame_probe.c_str(), "r"); + if (!f) { + return; + } + char line[256]; + while (std::fgets(line, sizeof(line), f)) { + char* p = line; + while (*p == ' ' || *p == '\t') ++p; + if (*p == '#' || *p == '\n' || *p == '\0') { + continue; + } + uint32_t addr = 0, len = 0; + if (std::sscanf(p, "%" SCNx32 " %" SCNu32, &addr, &len) != 2) { + continue; + } + if (!len || len > kMaxRegionBytes || regions_.size() >= kMaxRegions) { + continue; + } + regions_.push_back({addr, len}); + } + std::fclose(f); + for (size_t i = 0; i < regions_.size(); ++i) { + XELOGI("[frame-probe] R{} = {:08X} +{}", i, regions_[i].address, + regions_[i].length); + } + } + + std::vector regions_; + uint8_t scratch_[kMaxRegionBytes] = {}; + FILE* out_ = nullptr; + uint64_t frame_ = 0; + int64_t stamp_ = -1; + off_t size_ = -1; +}; + +} // namespace util +} // namespace kernel +} // namespace xe + +#endif // XENIA_KERNEL_UTIL_FRAME_STATE_PROBE_H_ diff --git a/src/xenia/kernel/xboxkrnl/xboxkrnl_video.cc b/src/xenia/kernel/xboxkrnl/xboxkrnl_video.cc index 12835232b..267e153e5 100644 --- a/src/xenia/kernel/xboxkrnl/xboxkrnl_video.cc +++ b/src/xenia/kernel/xboxkrnl/xboxkrnl_video.cc @@ -13,6 +13,7 @@ #include "xenia/emulator.h" #include "xenia/gpu/graphics_system.h" #include "xenia/kernel/kernel_state.h" +#include "xenia/kernel/util/frame_state_probe.h" #include "xenia/kernel/util/shim_utils.h" #include "xenia/kernel/xboxkrnl/xboxkrnl_private.h" #include "xenia/kernel/xboxkrnl/xboxkrnl_rtl.h" @@ -472,6 +473,11 @@ void VdSwap_entry( lpdword_t frontbuffer_ptr, // ptr to frontbuffer address lpdword_t texture_format_ptr, lpdword_t color_space_ptr, lpdword_t width, lpdword_t height) { + // RE probe: one guest frame has just finished, and we are on the guest thread + // that finished it -- the only place a sample of guest state is guaranteed to + // be exactly one frame after the previous one. No-op unless --frame_probe_log. + util::FrameStateProbe::instance().Sample(kernel_memory()); + // All of these parameters are REQUIRED. assert(buffer_ptr); assert(fetch_ptr);