Compare commits
1 Commits
auto/re-fi
...
auto/re-fr
| Author | SHA1 | Date | |
|---|---|---|---|
| 4040c701c5 |
197
src/xenia/kernel/util/frame_state_probe.h
Normal file
197
src/xenia/kernel/util/frame_state_probe.h
Normal file
@@ -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 <frame> H <host_ns> G <guest_ticks> R0 <hex> R1 <hex> ...
|
||||
//
|
||||
// Disabled unless --frame_probe_log is set, and then costs one stat() plus the
|
||||
// listed reads per frame.
|
||||
|
||||
#include <cinttypes>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include <sys/stat.h>
|
||||
|
||||
#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 '<hex_va> <len>' 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<int64_t>(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<Region> 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_
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user