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/emulator.h"
|
||||||
#include "xenia/gpu/graphics_system.h"
|
#include "xenia/gpu/graphics_system.h"
|
||||||
#include "xenia/kernel/kernel_state.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/util/shim_utils.h"
|
||||||
#include "xenia/kernel/xboxkrnl/xboxkrnl_private.h"
|
#include "xenia/kernel/xboxkrnl/xboxkrnl_private.h"
|
||||||
#include "xenia/kernel/xboxkrnl/xboxkrnl_rtl.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 frontbuffer_ptr, // ptr to frontbuffer address
|
||||||
lpdword_t texture_format_ptr, lpdword_t color_space_ptr, lpdword_t width,
|
lpdword_t texture_format_ptr, lpdword_t color_space_ptr, lpdword_t width,
|
||||||
lpdword_t height) {
|
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.
|
// All of these parameters are REQUIRED.
|
||||||
assert(buffer_ptr);
|
assert(buffer_ptr);
|
||||||
assert(fetch_ptr);
|
assert(fetch_ptr);
|
||||||
|
|||||||
Reference in New Issue
Block a user