Compare commits
14 Commits
phase-a-ar
...
capture-dr
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ea9f037b82 | ||
|
|
29efc0381a | ||
|
|
c6caa9e9c5 | ||
|
|
3c8faf03f7 | ||
|
|
16e1eb8e28 | ||
|
|
c6298dd0e3 | ||
|
|
44e1a64f35 | ||
|
|
c2650c6e58 | ||
|
|
7ff152a5a7 | ||
|
|
51322122a5 | ||
|
|
deaf21e9e1 | ||
|
|
2799234fbb | ||
|
|
e20f26963f | ||
|
|
bad346717a |
6
.github/workflows/Linux_x86.yml
vendored
6
.github/workflows/Linux_x86.yml
vendored
@@ -59,7 +59,7 @@ jobs:
|
||||
sudo update-alternatives --install /usr/bin/llvm-nm llvm-nm /usr/bin/llvm-nm-${{ inputs.llvm_version }} 200
|
||||
|
||||
# Download linuxdeploy tools if not cached
|
||||
if [ ${{ steps.cache-linuxdeploy.outputs.cache-hit }} != true ]; then
|
||||
if [ "${{ steps.cache-linuxdeploy.outputs.cache-hit }}" != "true" ]; then
|
||||
mkdir -p ~/linuxdeploy
|
||||
wget -q https://github.com/linuxdeploy/linuxdeploy/releases/download/continuous/linuxdeploy-x86_64.AppImage -O ~/linuxdeploy/linuxdeploy
|
||||
chmod +x ~/linuxdeploy/linuxdeploy
|
||||
@@ -68,7 +68,7 @@ jobs:
|
||||
|
||||
- name: Install Vulkan SDK
|
||||
run: |-
|
||||
if [ ${{ steps.cache-vulkan-sdk-linux.outputs.cache-hit }} != true ]; then
|
||||
if [ "${{ steps.cache-vulkan-sdk-linux.outputs.cache-hit }}" != "true" ]; then
|
||||
wget -qO vulkan-sdk.tar.xz https://sdk.lunarg.com/sdk/download/latest/linux/vulkan-sdk.tar.xz
|
||||
mkdir -p ~/vulkan-sdk
|
||||
tar -xf vulkan-sdk.tar.xz -C ~/vulkan-sdk
|
||||
@@ -85,7 +85,7 @@ jobs:
|
||||
|
||||
- name: Download submodules
|
||||
run: |- # Exclude unneeded submodules
|
||||
SUBMODULES=$(grep -oP '(?<=path = )(?!third_party\/DirectX(?:-Headers|ShaderCompiler)).+' .gitmodules)
|
||||
SUBMODULES=$(grep -oP '(?<=path = )(?!third_party\/(?:DirectX(?:-Headers|ShaderCompiler)|xbyak_aarch64)).+' .gitmodules)
|
||||
git submodule update --init --depth=1 -j$(getconf _NPROCESSORS_ONLN) $SUBMODULES
|
||||
|
||||
- name: Build Xenia
|
||||
|
||||
4
.github/workflows/Windows_x86.yml
vendored
4
.github/workflows/Windows_x86.yml
vendored
@@ -59,7 +59,9 @@ jobs:
|
||||
}
|
||||
|
||||
- name: Download submodules
|
||||
run: git submodule update --init --depth=1 -j $env:NUMBER_OF_PROCESSORS
|
||||
run: |- # Exclude unneeded submodules
|
||||
$SUBMODULES=(Select-String -CaseSensitive '(?<=path = )(?!third_party\/xbyak_aarch64).+' .gitmodules).Matches.Value
|
||||
git submodule update --init --depth=1 -j $env:NUMBER_OF_PROCESSORS $SUBMODULES
|
||||
|
||||
- name: Build Xenia
|
||||
run: python xenia-build.py build --config=Release --target=xenia-app
|
||||
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -116,3 +116,6 @@ node_modules/.bin/
|
||||
/cache0
|
||||
/devkit
|
||||
recent.toml
|
||||
|
||||
# Cross-compile (Wine) build output — RE handoff, never commit
|
||||
build-cross/
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
#include "xenia/base/platform.h"
|
||||
#include "xenia/base/threading_timer_queue.h"
|
||||
|
||||
#include <atomic>
|
||||
#include <pthread.h>
|
||||
#include <sched.h>
|
||||
#include <semaphore.h>
|
||||
@@ -981,11 +982,22 @@ class PosixCondition<Thread> final : public PosixConditionBase {
|
||||
static void* ThreadStartRoutine(void* parameter);
|
||||
bool signaled() const override { return signaled_; }
|
||||
void post_execution() override {
|
||||
// Reap exactly once. post_execution() runs on EVERY successful Wait(), and
|
||||
// a thread object stays signaled forever once it has exited -- so every
|
||||
// later wait on the same handle would pthread_join() a pthread_t that the
|
||||
// first join already reaped and freed. On glibc that faults or, if the tid
|
||||
// was recycled, blocks forever inside pthread_join(). The guest does
|
||||
// exactly this at mission teardown (several waiters on one thread handle),
|
||||
// which hung the waiting guest thread: the game froze to a black screen.
|
||||
if (reaped_.exchange(true)) {
|
||||
return;
|
||||
}
|
||||
if (thread_) {
|
||||
pthread_join(thread_, nullptr);
|
||||
}
|
||||
sem_destroy(&suspend_sem_);
|
||||
}
|
||||
std::atomic<bool> reaped_{false};
|
||||
pthread_t thread_;
|
||||
pid_t tid_ = 0; // Kernel TID for setpriority() fallback
|
||||
mutable bool fifo_failed_ = false; // True after SCHED_FIFO was rejected
|
||||
|
||||
@@ -601,11 +601,18 @@ struct MAX_V128 : Sequence<MAX_V128, I<OPCODE_MAX, V128Op, V128Op, V128Op>> {
|
||||
static void Emit(X64Emitter& e, const EmitArgType& i) {
|
||||
e.ChangeMxcsrMode(MXCSRMode::Vmx);
|
||||
// if 0 and -0, return 0! opposite of minfp
|
||||
auto src1 = GetInputRegOrConstant(e, i.src1, e.xmm0);
|
||||
auto src2 = GetInputRegOrConstant(e, i.src2, e.xmm1);
|
||||
const Xmm src1 = GetInputRegOrConstant(e, i.src1, e.xmm0);
|
||||
const Xmm src2 = GetInputRegOrConstant(e, i.src2, e.xmm1);
|
||||
|
||||
e.vmaxps(e.xmm2, src1, src2);
|
||||
e.vmaxps(e.xmm3, src2, src1);
|
||||
e.vorps(i.dest, e.xmm2, e.xmm3);
|
||||
e.vandps(e.xmm2, e.xmm2, e.xmm3);
|
||||
|
||||
e.vcmpunordps(e.xmm3, src1, src1); // mask: vA is NaN
|
||||
e.vblendvps(e.xmm3, src2, src1, e.xmm3);
|
||||
|
||||
e.vcmpunordps(i.dest, src1, src2); // mask: vA or vB is NaN
|
||||
e.vblendvps(i.dest, e.xmm2, e.xmm3, i.dest);
|
||||
}
|
||||
};
|
||||
EMITTER_OPCODE_TABLE(OPCODE_MAX, MAX_F32, MAX_F64, MAX_V128);
|
||||
@@ -660,11 +667,18 @@ struct MIN_F64 : Sequence<MIN_F64, I<OPCODE_MIN, F64Op, F64Op, F64Op>> {
|
||||
struct MIN_V128 : Sequence<MIN_V128, I<OPCODE_MIN, V128Op, V128Op, V128Op>> {
|
||||
static void Emit(X64Emitter& e, const EmitArgType& i) {
|
||||
e.ChangeMxcsrMode(MXCSRMode::Vmx);
|
||||
auto src1 = GetInputRegOrConstant(e, i.src1, e.xmm0);
|
||||
auto src2 = GetInputRegOrConstant(e, i.src2, e.xmm1);
|
||||
const Xmm src1 = GetInputRegOrConstant(e, i.src1, e.xmm0);
|
||||
const Xmm src2 = GetInputRegOrConstant(e, i.src2, e.xmm1);
|
||||
|
||||
e.vminps(e.xmm2, src1, src2);
|
||||
e.vminps(e.xmm3, src2, src1);
|
||||
e.vorps(i.dest, e.xmm2, e.xmm3);
|
||||
e.vorps(e.xmm2, e.xmm2, e.xmm3);
|
||||
|
||||
e.vcmpunordps(e.xmm3, src1, src1); // mask: vA is NaN
|
||||
e.vblendvps(e.xmm3, src2, src1, e.xmm3);
|
||||
|
||||
e.vcmpunordps(i.dest, src1, src2); // mask: vA or vB is NaN
|
||||
e.vblendvps(i.dest, e.xmm2, e.xmm3, i.dest);
|
||||
}
|
||||
};
|
||||
EMITTER_OPCODE_TABLE(OPCODE_MIN, MIN_I8, MIN_I16, MIN_I32, MIN_I64, MIN_F32,
|
||||
|
||||
@@ -174,8 +174,8 @@ bool PPCContext::CompareRegWithString(const char* name, const char* value,
|
||||
vec128_t expected = string_util::from_string<vec128_t>(value);
|
||||
if (this->v[n] != expected) {
|
||||
result =
|
||||
fmt::format("[{:08X}, {:08X}, {:08X}, {:08X}]", this->v[n].i32[0],
|
||||
this->v[n].i32[1], this->v[n].i32[2], this->v[n].i32[3]);
|
||||
fmt::format("[{:08X}, {:08X}, {:08X}, {:08X}]", this->v[n].u32[0],
|
||||
this->v[n].u32[1], this->v[n].u32[2], this->v[n].u32[3]);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
|
||||
@@ -6,6 +6,9 @@ add_executable(xenia-cpu-ppc-tests
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/ppc_testing_main.cc
|
||||
)
|
||||
|
||||
target_compile_definitions(xenia-cpu-ppc-tests PRIVATE
|
||||
XE_SOURCE_ROOT="${PROJECT_SOURCE_DIR}")
|
||||
|
||||
# Add platform-specific console app main
|
||||
if(WIN32)
|
||||
target_sources(xenia-cpu-ppc-tests PRIVATE
|
||||
|
||||
@@ -5848,3 +5848,20 @@ test_vmaxfp_650_GEN:
|
||||
#_ REGISTER_OUT v2 [FFFFFF80, 0000007F, FFFEFF7F, 00010080]
|
||||
#_ REGISTER_OUT v3 [FFFFFFFF, FFFFFFFF, FFFFFFFF, FFFFFFFF]
|
||||
|
||||
test_vmaxfp_651_GEN:
|
||||
#_ REGISTER_IN v1 [FFC00000, FFC00000, FFC00000, 00000000]
|
||||
#_ REGISTER_IN v2 [00000000, 00000000, 00000000, 00000000]
|
||||
vmaxfp v3, v1, v2
|
||||
blr
|
||||
#_ REGISTER_OUT v1 [FFC00000, FFC00000, FFC00000, 00000000]
|
||||
#_ REGISTER_OUT v2 [00000000, 00000000, 00000000, 00000000]
|
||||
#_ REGISTER_OUT v3 [FFC00000, FFC00000, FFC00000, 00000000]
|
||||
|
||||
test_vmaxfp_652_GEN:
|
||||
#_ REGISTER_IN v1 [7FC00000, 7FC00000, 7FC00000, 00000000]
|
||||
#_ REGISTER_IN v2 [00000000, 00000000, 00000000, 00000000]
|
||||
vmaxfp v3, v1, v2
|
||||
blr
|
||||
#_ REGISTER_OUT v1 [7FC00000, 7FC00000, 7FC00000, 00000000]
|
||||
#_ REGISTER_OUT v2 [00000000, 00000000, 00000000, 00000000]
|
||||
#_ REGISTER_OUT v3 [7FC00000, 7FC00000, 7FC00000, 00000000]
|
||||
|
||||
@@ -5848,3 +5848,20 @@ test_vminfp_650_GEN:
|
||||
#_ REGISTER_OUT v2 [FFFFFF80, 0000007F, FFFEFF7F, 00010080]
|
||||
#_ REGISTER_OUT v3 [FFFFFFFF, FFFFFFFF, FFFFFFFF, FFFFFFFF]
|
||||
|
||||
test_vminfp_651_GEN:
|
||||
#_ REGISTER_IN v1 [FFC00000, FFC00000, FFC00000, 00000000]
|
||||
#_ REGISTER_IN v2 [477FFC00, 477FFC00, 477FFC00, 477FFC00]
|
||||
vminfp v3, v1, v2
|
||||
blr
|
||||
#_ REGISTER_OUT v1 [FFC00000, FFC00000, FFC00000, 00000000]
|
||||
#_ REGISTER_OUT v2 [477FFC00, 477FFC00, 477FFC00, 477FFC00]
|
||||
#_ REGISTER_OUT v3 [FFC00000, FFC00000, FFC00000, 00000000]
|
||||
|
||||
test_vminfp_652_GEN:
|
||||
#_ REGISTER_IN v1 [7FC00000, 7FC00000, 7FC00000, 00000000]
|
||||
#_ REGISTER_IN v2 [477FFC00, 477FFC00, 477FFC00, 477FFC00]
|
||||
vminfp v3, v1, v2
|
||||
blr
|
||||
#_ REGISTER_OUT v1 [7FC00000, 7FC00000, 7FC00000, 00000000]
|
||||
#_ REGISTER_OUT v2 [477FFC00, 477FFC00, 477FFC00, 477FFC00]
|
||||
#_ REGISTER_OUT v3 [7FC00000, 7FC00000, 7FC00000, 00000000]
|
||||
|
||||
@@ -108,8 +108,10 @@ class TestSuite {
|
||||
name = name.replace_extension();
|
||||
|
||||
name_ = xe::path_to_utf8(name);
|
||||
map_file_path_ = cvars::test_bin_path / name.replace_extension(".map");
|
||||
bin_file_path_ = cvars::test_bin_path / name.replace_extension(".bin");
|
||||
map_file_path_ = std::filesystem::path(XE_SOURCE_ROOT) /
|
||||
cvars::test_bin_path / name.replace_extension(".map");
|
||||
bin_file_path_ = std::filesystem::path(XE_SOURCE_ROOT) /
|
||||
cvars::test_bin_path / name.replace_extension(".bin");
|
||||
}
|
||||
|
||||
bool Load() {
|
||||
@@ -470,7 +472,8 @@ class TestRunner {
|
||||
|
||||
bool DiscoverTests(const std::filesystem::path& test_path,
|
||||
std::vector<std::filesystem::path>& test_files) {
|
||||
auto file_infos = xe::filesystem::ListFiles(test_path);
|
||||
auto file_infos = xe::filesystem::ListFiles(
|
||||
std::filesystem::path(XE_SOURCE_ROOT) / test_path);
|
||||
for (auto& file_info : file_infos) {
|
||||
if (file_info.name.extension() == ".s") {
|
||||
// Only include test files (instr_*.s), not helper files
|
||||
@@ -669,7 +672,7 @@ bool RunTests(const std::vector<std::string>& test_names) {
|
||||
std::vector<TestSuite> test_suites;
|
||||
bool load_failed = false;
|
||||
for (auto& test_path : test_files) {
|
||||
TestSuite test_suite(test_path);
|
||||
TestSuite test_suite(std::filesystem::path(XE_SOURCE_ROOT) / test_path);
|
||||
if (!test_name_filter.empty() &&
|
||||
test_name_filter.find(test_suite.name()) == test_name_filter.end()) {
|
||||
continue;
|
||||
|
||||
@@ -9,6 +9,9 @@
|
||||
|
||||
#include "xenia/gpu/command_processor.h"
|
||||
|
||||
#include <fstream>
|
||||
#include <unordered_set>
|
||||
|
||||
#include "third_party/fmt/include/fmt/format.h"
|
||||
#include "xenia/base/byte_stream.h"
|
||||
#include "xenia/base/clock.h"
|
||||
@@ -18,6 +21,8 @@
|
||||
#include "xenia/gpu/gpu_flags.h"
|
||||
#include "xenia/gpu/graphics_system.h"
|
||||
#include "xenia/gpu/packet_disassembler.h"
|
||||
#include "xenia/gpu/registers.h"
|
||||
#include "xenia/gpu/shader.h"
|
||||
#include "xenia/gpu/sampler_info.h"
|
||||
#include "xenia/gpu/texture_info.h"
|
||||
#include "xenia/gpu/xenos_zpd_report.h"
|
||||
@@ -77,6 +82,14 @@ DEFINE_string(
|
||||
|
||||
UPDATE_from_string(readback_resolve, 2025, 12, 4, 21, "fast");
|
||||
|
||||
DEFINE_bool(
|
||||
log_draws, false,
|
||||
"Reverse-engineering aid: write each distinct draw's primitive type, index "
|
||||
"buffer, and per-stream vertex declaration (stream base/stride + per-element "
|
||||
"format/offset) to xenia_re_draws.log in the working directory. For decoding "
|
||||
"game mesh formats against GPU ground truth.",
|
||||
"GPU");
|
||||
|
||||
DEFINE_bool(
|
||||
readback_memexport, false,
|
||||
"Read data written by memory export in shaders on the CPU. "
|
||||
@@ -88,6 +101,377 @@ DEFINE_bool(
|
||||
namespace xe {
|
||||
namespace gpu {
|
||||
|
||||
namespace {
|
||||
// Short readable name for a guest vertex element format (RE logging only).
|
||||
const char* ReVertexFormatName(xenos::VertexFormat f) {
|
||||
switch (f) {
|
||||
case xenos::VertexFormat::k_32_FLOAT: return "f32";
|
||||
case xenos::VertexFormat::k_32_32_FLOAT: return "f32x2";
|
||||
case xenos::VertexFormat::k_32_32_32_FLOAT: return "f32x3";
|
||||
case xenos::VertexFormat::k_32_32_32_32_FLOAT: return "f32x4";
|
||||
case xenos::VertexFormat::k_16_16_FLOAT: return "f16x2";
|
||||
case xenos::VertexFormat::k_16_16_16_16_FLOAT: return "f16x4";
|
||||
case xenos::VertexFormat::k_16_16: return "s16x2";
|
||||
case xenos::VertexFormat::k_16_16_16_16: return "s16x4";
|
||||
case xenos::VertexFormat::k_8_8_8_8: return "8888";
|
||||
case xenos::VertexFormat::k_2_10_10_10: return "2_10_10_10";
|
||||
case xenos::VertexFormat::k_10_11_11: return "10_11_11";
|
||||
case xenos::VertexFormat::k_11_11_10: return "11_11_10";
|
||||
case xenos::VertexFormat::k_32: return "u32";
|
||||
case xenos::VertexFormat::k_32_32: return "u32x2";
|
||||
case xenos::VertexFormat::k_32_32_32_32: return "u32x4";
|
||||
default: return "?";
|
||||
}
|
||||
}
|
||||
} // namespace
|
||||
|
||||
void CommandProcessor::LogDrawForRE(uint32_t vgt_draw_initiator_value,
|
||||
const IndexBufferInfo* index_buffer_info) {
|
||||
if (!cvars::log_draws) {
|
||||
return;
|
||||
}
|
||||
static std::mutex re_mutex;
|
||||
static std::ofstream re_out;
|
||||
static std::unordered_set<uint64_t> re_seen;
|
||||
std::lock_guard<std::mutex> lock(re_mutex);
|
||||
if (!re_out.is_open()) {
|
||||
re_out.open("xenia_re_draws.log", std::ios::out | std::ios::trunc);
|
||||
XELOGI("[RE-DRAW] logging distinct draws to xenia_re_draws.log");
|
||||
}
|
||||
if (!re_out.is_open()) {
|
||||
return;
|
||||
}
|
||||
|
||||
reg::VGT_DRAW_INITIATOR init;
|
||||
init.value = vgt_draw_initiator_value;
|
||||
Shader* vs = active_vertex_shader_;
|
||||
|
||||
// De-dup by the VERTEX-DECLARATION FINGERPRINT (shader + primitive type +
|
||||
// per-stream element formats/offsets), NOT by buffer address. Animated UI
|
||||
// that redraws the same mesh format into fresh buffers every frame therefore
|
||||
// collapses to a single record, keeping the logging near-free — while every
|
||||
// distinct mesh format (the player plane, each weapon) is still captured once.
|
||||
uint64_t sig = 1469598103934665603ull; // FNV-ish seed
|
||||
auto mix = [&sig](uint64_t v) { sig = (sig ^ v) * 1099511628211ull; };
|
||||
mix(uint64_t(init.prim_type));
|
||||
if (vs) {
|
||||
mix(vs->ucode_data_hash());
|
||||
}
|
||||
// ALSO key on the index-buffer guest base. Same-format meshes drawn from
|
||||
// DISTINCT static index buffers (the player-ship body vs the hangar walls vs
|
||||
// each weapon) otherwise collapse to one record, hiding all but the first —
|
||||
// which is why the ship body never logged. Static geometry has a stable index
|
||||
// address so it still logs once; only per-frame-reallocated dynamic geometry
|
||||
// multiplies (bounded by the 4096 cap + short RE captures).
|
||||
if (index_buffer_info) {
|
||||
mix(uint64_t(index_buffer_info->guest_base));
|
||||
}
|
||||
bool analyzed = vs && vs->is_ucode_analyzed();
|
||||
if (analyzed) {
|
||||
for (const auto& binding : vs->vertex_bindings()) {
|
||||
mix(binding.fetch_constant);
|
||||
mix(binding.stride_words);
|
||||
for (const auto& attr : binding.attributes) {
|
||||
mix(uint64_t(attr.fetch_instr.attributes.data_format));
|
||||
mix(uint64_t(uint32_t(attr.fetch_instr.attributes.offset)));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
mix(uint64_t(init.num_indices));
|
||||
}
|
||||
if (!re_seen.insert(sig).second) {
|
||||
return;
|
||||
}
|
||||
// Safety cap on distinct formats, so a pathological title can't grow the log
|
||||
// (and the working set) without bound.
|
||||
if (re_seen.size() > 4096) {
|
||||
return;
|
||||
}
|
||||
|
||||
re_out << fmt::format("DRAW prim={} indices={} src={} ",
|
||||
uint32_t(init.prim_type), uint32_t(init.num_indices),
|
||||
uint32_t(init.source_select));
|
||||
if (index_buffer_info) {
|
||||
re_out << fmt::format(
|
||||
"index[base=0x{:08X} count={} fmt={} endian={} len={}] ",
|
||||
index_buffer_info->guest_base, index_buffer_info->count,
|
||||
index_buffer_info->format == xenos::IndexFormat::kInt16 ? "u16" : "u32",
|
||||
uint32_t(index_buffer_info->endianness), index_buffer_info->length);
|
||||
} else {
|
||||
re_out << "index[auto] ";
|
||||
}
|
||||
if (vs) {
|
||||
re_out << fmt::format("vs=0x{:016X}", vs->ucode_data_hash());
|
||||
}
|
||||
re_out << "\n";
|
||||
|
||||
// Dump the first index VALUES (decoded, endian-corrected) + raw index bytes.
|
||||
// Index buffers live in a separate grouped pool with no distinctive content,
|
||||
// so the vertex-position correlation can't find them; but a run of the first
|
||||
// ~32 logical indices IS a distinctive byte pattern to search for in the .xpr,
|
||||
// which pins the index buffer's file offset exactly. Guest index bytes carry
|
||||
// the fetch endianness (usually 8-in-16 for u16), so both the raw bytes and
|
||||
// the decoded values are printed — search whichever matches the file order.
|
||||
if (index_buffer_info && index_buffer_info->guest_base) {
|
||||
const uint8_t* ip = memory_->TranslatePhysical<const uint8_t*>(
|
||||
index_buffer_info->guest_base);
|
||||
bool u16 = index_buffer_info->format == xenos::IndexFormat::kInt16;
|
||||
uint32_t esz = u16 ? 2 : 4;
|
||||
uint32_t cnt = index_buffer_info->count < 48 ? index_buffer_info->count : 48;
|
||||
if (ip) {
|
||||
uint32_t rawn = cnt * esz;
|
||||
if (rawn > 128) {
|
||||
rawn = 128;
|
||||
}
|
||||
re_out << " idx_raw:";
|
||||
for (uint32_t i = 0; i < rawn; ++i) {
|
||||
re_out << fmt::format(" {:02X}", ip[i]);
|
||||
}
|
||||
re_out << "\n idx_val:";
|
||||
// Decode with 8-in-16 / 8-in-32 endian correction (endianness field).
|
||||
uint32_t en = uint32_t(index_buffer_info->endianness);
|
||||
for (uint32_t i = 0; i < cnt; ++i) {
|
||||
const uint8_t* q = ip + i * esz;
|
||||
uint32_t v;
|
||||
if (u16) {
|
||||
v = (en == 1) ? (uint32_t(q[0]) | (uint32_t(q[1]) << 8))
|
||||
: (uint32_t(q[1]) | (uint32_t(q[0]) << 8));
|
||||
} else {
|
||||
v = (uint32_t(q[3]) << 24) | (uint32_t(q[2]) << 16) |
|
||||
(uint32_t(q[1]) << 8) | uint32_t(q[0]);
|
||||
}
|
||||
re_out << fmt::format(" {}", v);
|
||||
}
|
||||
re_out << "\n";
|
||||
}
|
||||
}
|
||||
|
||||
if (analyzed) {
|
||||
for (const auto& binding : vs->vertex_bindings()) {
|
||||
xenos::xe_gpu_vertex_fetch_t fetch =
|
||||
register_file_->GetVertexFetch(binding.fetch_constant);
|
||||
re_out << fmt::format(
|
||||
" stream fc={} base=0x{:08X} stride_words={} size_words={} "
|
||||
"endian={} type={}\n",
|
||||
binding.fetch_constant, uint32_t(fetch.address) << 2,
|
||||
binding.stride_words, uint32_t(fetch.size), uint32_t(fetch.endian),
|
||||
uint32_t(fetch.type));
|
||||
for (const auto& attr : binding.attributes) {
|
||||
const auto& a = attr.fetch_instr.attributes;
|
||||
re_out << fmt::format(
|
||||
" attr fmt={}({}) offset_words={} stride_words={} signed={} "
|
||||
"int={} exp_adjust={}\n",
|
||||
uint32_t(a.data_format), ReVertexFormatName(a.data_format), a.offset,
|
||||
a.stride, a.is_signed ? 1 : 0, a.is_integer ? 1 : 0, a.exp_adjust);
|
||||
}
|
||||
// Raw bytes at the stream base (first two stride-records, capped 96 B).
|
||||
// The f32-position dump below only fires for k_32_32_32_FLOAT streams;
|
||||
// quantized-position body meshes (the multi-stream layout we still can't
|
||||
// decode) have no f32 attribute, so these hex bytes are the only
|
||||
// ground-truth we can correlate against the on-disc .xpr and validate a
|
||||
// decode against — regardless of the element format.
|
||||
{
|
||||
uint32_t sbase = uint32_t(fetch.address) << 2;
|
||||
uint32_t sbytes = uint32_t(fetch.size) * 4;
|
||||
uint32_t stride_b = binding.stride_words * 4;
|
||||
uint32_t want = stride_b ? stride_b * 2 : 32;
|
||||
uint32_t n = want < sbytes ? want : sbytes;
|
||||
if (n > 96) {
|
||||
n = 96;
|
||||
}
|
||||
const uint8_t* p = memory_->TranslatePhysical<const uint8_t*>(sbase);
|
||||
if (p && n) {
|
||||
re_out << " raw:";
|
||||
for (uint32_t i = 0; i < n; ++i) {
|
||||
re_out << fmt::format(" {:02X}", p[i]);
|
||||
}
|
||||
re_out << "\n";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Dump the first few vertex POSITIONS from guest memory. The f32 position
|
||||
// bytes are identical between the guest buffer and the on-disc .xpr (only
|
||||
// f16 pairs are rearranged on load), so these values can be searched for in
|
||||
// the file to locate a mesh whose in-file offset is otherwise unknown
|
||||
// (e.g. multi-XBG7 body meshes). See docs/re/structures/xbg7-mesh.md.
|
||||
if (!vs->vertex_bindings().empty()) {
|
||||
const auto& binding = vs->vertex_bindings()[0];
|
||||
xenos::xe_gpu_vertex_fetch_t fetch =
|
||||
register_file_->GetVertexFetch(binding.fetch_constant);
|
||||
// Position = the first f32×3 attribute (offset is in dwords).
|
||||
int32_t pos_off_bytes = -1;
|
||||
for (const auto& attr : binding.attributes) {
|
||||
if (attr.fetch_instr.attributes.data_format ==
|
||||
xenos::VertexFormat::k_32_32_32_FLOAT) {
|
||||
pos_off_bytes = attr.fetch_instr.attributes.offset * 4;
|
||||
break;
|
||||
}
|
||||
}
|
||||
uint32_t stride = binding.stride_words * 4;
|
||||
uint32_t vbase = uint32_t(fetch.address) << 2;
|
||||
uint32_t buf_bytes = uint32_t(fetch.size) * 4;
|
||||
if (pos_off_bytes >= 0 && stride > 0) {
|
||||
uint32_t max_v = buf_bytes / stride;
|
||||
uint32_t n = max_v < 8 ? max_v : 8;
|
||||
re_out << " positions:";
|
||||
for (uint32_t v = 0; v < n; ++v) {
|
||||
uint32_t a = vbase + v * stride + uint32_t(pos_off_bytes);
|
||||
const uint8_t* p = memory_->TranslatePhysical<const uint8_t*>(a);
|
||||
if (!p) {
|
||||
break;
|
||||
}
|
||||
auto be_f32 = [](const uint8_t* q) {
|
||||
uint32_t w = (uint32_t(q[0]) << 24) | (uint32_t(q[1]) << 16) |
|
||||
(uint32_t(q[2]) << 8) | uint32_t(q[3]);
|
||||
float f;
|
||||
std::memcpy(&f, &w, 4);
|
||||
return f;
|
||||
};
|
||||
re_out << fmt::format(" ({:.4f},{:.4f},{:.4f})", be_f32(p),
|
||||
be_f32(p + 4), be_f32(p + 8));
|
||||
}
|
||||
re_out << "\n";
|
||||
}
|
||||
}
|
||||
} else {
|
||||
re_out << " (vertex shader not analyzed yet)\n";
|
||||
}
|
||||
|
||||
// Bound TEXTURES for this draw (guest base address of each). Cross-referenced
|
||||
// with the file-I/O log (xenia_re_files.log), a texture's guest address maps
|
||||
// to the .xpr it was loaded from — so a draw sampling `rou_f001_*_col`
|
||||
// identifies the ship, and gives material→file assignment. Base is the fetch
|
||||
// constant's base_address in 4 KB pages (<< 12 = guest byte address).
|
||||
Shader* ps = active_pixel_shader_;
|
||||
if (ps && ps->is_ucode_analyzed() && !ps->texture_bindings().empty()) {
|
||||
re_out << " textures:\n";
|
||||
for (const auto& tb : ps->texture_bindings()) {
|
||||
xenos::xe_gpu_texture_fetch_t tf =
|
||||
register_file_->GetTextureFetch(tb.fetch_constant);
|
||||
// Dimensions are stored as (actual - 1). Only 2D fields are meaningful for
|
||||
// the ship's surface maps; other dimensions still print base/format.
|
||||
uint32_t w = tf.size_2d.width + 1;
|
||||
uint32_t h = tf.size_2d.height + 1;
|
||||
uint32_t gpu_base = uint32_t(tf.base_address) << 12;
|
||||
re_out << fmt::format(
|
||||
" [fc={} base=0x{:08X} fmt={} endian={} dim={} {}x{} tiled={} "
|
||||
"pitch={} swizzle=0x{:03X} mip_addr=0x{:08X} mip_lvls={}..{}]",
|
||||
tb.fetch_constant, gpu_base, uint32_t(tf.format),
|
||||
uint32_t(tf.endianness), uint32_t(tf.dimension), w, h,
|
||||
uint32_t(tf.tiled), uint32_t(tf.pitch), uint32_t(tf.swizzle),
|
||||
uint32_t(tf.mip_address) << 12, uint32_t(tf.mip_min_level),
|
||||
uint32_t(tf.mip_max_level));
|
||||
// Hash + sample the GPU-RESIDENT bytes at base_address. If the game
|
||||
// massaged the texture after load (recolour, recompress, generate mips,
|
||||
// re-tile), these bytes differ from the on-disc .xpr — comparing this hash
|
||||
// and sample against the file the address maps to (xenia_re_files.log)
|
||||
// tells us whether the viewer can decode the disc bytes directly or must
|
||||
// reproduce a runtime transform. 128 KiB window is format-agnostic.
|
||||
const uint8_t* tp =
|
||||
memory_->TranslatePhysical<const uint8_t*>(gpu_base);
|
||||
if (tp) {
|
||||
uint64_t th = 1469598103934665603ull;
|
||||
uint32_t win = w * h; // conservative texel-count-scaled cap
|
||||
if (win > 131072) {
|
||||
win = 131072;
|
||||
}
|
||||
if (win < 64) {
|
||||
win = 64;
|
||||
}
|
||||
for (uint32_t i = 0; i < win; ++i) {
|
||||
th = (th ^ tp[i]) * 1099511628211ull;
|
||||
}
|
||||
re_out << fmt::format(" hash=0x{:016X} bytes[0:32]:", th);
|
||||
for (uint32_t i = 0; i < 32; ++i) {
|
||||
re_out << fmt::format(" {:02X}", tp[i]);
|
||||
}
|
||||
}
|
||||
re_out << "\n";
|
||||
}
|
||||
}
|
||||
|
||||
// Dump the vertex-shader float constants c0..c31 (the transform matrices live
|
||||
// here). The game bakes each part's WORLD matrix into these constants before
|
||||
// the draw, so comparing a part's WVP block to the body's isolates the world
|
||||
// transform the shader applies (View/Proj cancel): world = inv(body_WVP) *
|
||||
// part_WVP. This is the only way to recover the runtime nacelle offset that
|
||||
// the static XBG7 node graph doesn't encode. VS constants are float slots
|
||||
// 0..255 (PS = 256..511); each slot is a float4.
|
||||
{
|
||||
re_out << " vsconst:\n";
|
||||
for (uint32_t i = 0; i < 32; ++i) {
|
||||
uint32_t base = XE_GPU_REG_SHADER_CONSTANT_000_X + 4 * i;
|
||||
float x = register_file_->Get<float>(base + 0);
|
||||
float y = register_file_->Get<float>(base + 1);
|
||||
float z = register_file_->Get<float>(base + 2);
|
||||
float w = register_file_->Get<float>(base + 3);
|
||||
re_out << fmt::format(" c{:<3} {:.5f} {:.5f} {:.5f} {:.5f}\n", i, x, y,
|
||||
z, w);
|
||||
}
|
||||
}
|
||||
|
||||
// Dump the PIXEL-shader float constants c256..c287 (the material params: light
|
||||
// directions/colours, specular exponent, tint/fresnel factors the lighting
|
||||
// shader multiplies in). Slots 256..511 are the PS bank; register base is
|
||||
// SHADER_CONSTANT_256_X. Together with the disassembled PS microcode below,
|
||||
// these are what turn the flat albedo into the game's lit look.
|
||||
if (ps) {
|
||||
re_out << fmt::format(" ps=0x{:016X}\n", ps->ucode_data_hash());
|
||||
re_out << " psconst:\n";
|
||||
// The ship's material/lighting shader references PS constants up to c76 (the
|
||||
// light dirs/colours c32/c33/c38..c41, the mask scale/remap factors
|
||||
// c64..c66/c72..c76) and the c254/c255 literals. Dump c0..c95 plus the top
|
||||
// literals so every referenced slot is captured (a 32-wide dump missed them).
|
||||
auto dump_pc = [&](uint32_t i) {
|
||||
uint32_t base = XE_GPU_REG_SHADER_CONSTANT_256_X + 4 * i;
|
||||
float x = register_file_->Get<float>(base + 0);
|
||||
float y = register_file_->Get<float>(base + 1);
|
||||
float z = register_file_->Get<float>(base + 2);
|
||||
float w = register_file_->Get<float>(base + 3);
|
||||
re_out << fmt::format(" p{:<3} {:.5f} {:.5f} {:.5f} {:.5f}\n", i, x, y,
|
||||
z, w);
|
||||
};
|
||||
for (uint32_t i = 0; i < 96; ++i) {
|
||||
dump_pc(i);
|
||||
}
|
||||
for (uint32_t i = 250; i < 256; ++i) {
|
||||
dump_pc(i);
|
||||
}
|
||||
}
|
||||
re_out.flush();
|
||||
|
||||
// Dump the full Xenos MICROCODE DISASSEMBLY of the vertex and pixel shaders
|
||||
// for this draw to a separate file, deduped by ucode hash. The PS disassembly
|
||||
// is the ground truth for the game's lighting/material model — how many
|
||||
// texture samples feed the surface, the specular math, any cubemap/fresnel
|
||||
// term — which is what the viewer's StandardMaterial has to reproduce. Kept in
|
||||
// its own file (xenia_re_shaders.log) so the per-draw log stays scannable.
|
||||
{
|
||||
static std::ofstream sh_out;
|
||||
static std::unordered_set<uint64_t> sh_seen;
|
||||
if (!sh_out.is_open()) {
|
||||
sh_out.open("xenia_re_shaders.log", std::ios::out | std::ios::trunc);
|
||||
}
|
||||
if (sh_out.is_open()) {
|
||||
auto dump_shader = [&](const char* kind, Shader* sh) {
|
||||
if (!sh || !sh->is_ucode_analyzed()) {
|
||||
return;
|
||||
}
|
||||
if (!sh_seen.insert(sh->ucode_data_hash()).second) {
|
||||
return;
|
||||
}
|
||||
sh_out << fmt::format("===== {} 0x{:016X} =====\n", kind,
|
||||
sh->ucode_data_hash());
|
||||
sh_out << sh->ucode_disassembly() << "\n\n";
|
||||
};
|
||||
dump_shader("VS", vs);
|
||||
dump_shader("PS", ps);
|
||||
sh_out.flush();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// This should be written completely differently with support for different
|
||||
// types.
|
||||
void SaveGPUSetting(GPUSetting setting, uint64_t value) {
|
||||
|
||||
@@ -446,6 +446,13 @@ class CommandProcessor {
|
||||
}
|
||||
virtual bool IssueCopy() { return false; }
|
||||
|
||||
// Reverse-engineering aid (cvar `log_draws`): dump the guest's exact
|
||||
// primitive type + index buffer + per-stream vertex declaration for each
|
||||
// distinct draw to a dedicated file, for decoding game mesh formats.
|
||||
// No-op unless the cvar is enabled. Defined in command_processor.cc.
|
||||
void LogDrawForRE(uint32_t vgt_draw_initiator_value,
|
||||
const IndexBufferInfo* index_buffer_info);
|
||||
|
||||
// "Actual" is for the command processor thread, to be read by the
|
||||
// implementations.
|
||||
SwapPostEffect GetActualSwapPostEffect() const {
|
||||
|
||||
@@ -1441,8 +1441,14 @@ bool D3D12RenderTargetCache::Resolve(const Memory& memory,
|
||||
clear_transfers_[1])) {
|
||||
uint64_t clear_values[2];
|
||||
clear_values[0] = resolve_info.rb_depth_clear;
|
||||
clear_values[1] = resolve_info.rb_color_clear |
|
||||
(uint64_t(resolve_info.rb_color_clear_lo) << 32);
|
||||
// For 64bpp formats, RB_COLOR_CLEAR_LO is the lower 32 bits of the
|
||||
// packed clear value. RB_COLOR_CLEAR is the upper 32 bits and, for
|
||||
// 32bpp formats, the whole value.
|
||||
clear_values[1] =
|
||||
resolve_info.color_edram_info.format_is_64bpp
|
||||
? resolve_info.rb_color_clear_lo |
|
||||
(uint64_t(resolve_info.rb_color_clear) << 32)
|
||||
: resolve_info.rb_color_clear;
|
||||
PerformTransfersAndResolveClears(2, clear_render_targets,
|
||||
clear_transfers_, clear_values,
|
||||
&clear_rectangle);
|
||||
|
||||
@@ -713,9 +713,19 @@ struct ResolveInfo {
|
||||
// Not doing -32...32 to -1...1 clamping here as a hack for k_16_16 and
|
||||
// k_16_16_16_16 blending emulation when using host render targets as it
|
||||
// would be inconsistent with the usual way of clearing with a depth quad.
|
||||
// TODO(Triang3l): Check which 32-bit portion is in which register.
|
||||
constants_out.rt_specific.clear_value[0] = rb_color_clear;
|
||||
constants_out.rt_specific.clear_value[1] = rb_color_clear_lo;
|
||||
if (color_edram_info.format_is_64bpp) {
|
||||
// RB_COLOR_CLEAR_LO holds the lower 32 bits.
|
||||
// Red | green << 16 for 16_16_16_16, red for 32_32_FLOAT.
|
||||
// RB_COLOR_CLEAR holds the upper 32 bits.
|
||||
// D3D builds the low dword as R | G << 16 and the high as B | A << 16,
|
||||
// and writes to the _LO and base register respectively.
|
||||
constants_out.rt_specific.clear_value[0] = rb_color_clear_lo;
|
||||
constants_out.rt_specific.clear_value[1] = rb_color_clear;
|
||||
} else {
|
||||
// 32bpp clear values are only taken from RB_COLOR_CLEAR.
|
||||
constants_out.rt_specific.clear_value[0] = rb_color_clear;
|
||||
constants_out.rt_specific.clear_value[1] = rb_color_clear;
|
||||
}
|
||||
constants_out.rt_specific.edram_info = color_edram_info;
|
||||
constants_out.coordinate_info = coordinate_info;
|
||||
}
|
||||
|
||||
@@ -1152,6 +1152,11 @@ bool COMMAND_PROCESSOR::ExecutePacketType3Draw(
|
||||
uint32_t(vgt_draw_initiator.prim_type),
|
||||
uint32_t(vgt_draw_initiator.source_select));
|
||||
}
|
||||
// Reverse-engineering aid (no-op unless the `log_draws` cvar is set):
|
||||
// record the guest's primitive type + index buffer + vertex declaration.
|
||||
// Placed after IssueDraw so the vertex shader has been analyzed.
|
||||
COMMAND_PROCESSOR::LogDrawForRE(
|
||||
vgt_draw_initiator.value, is_indexed ? &index_buffer_info : nullptr);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -15,7 +15,38 @@
|
||||
namespace xe {
|
||||
namespace gpu {
|
||||
|
||||
RegisterFile::RegisterFile() { std::memset(values, 0, sizeof(values)); }
|
||||
RegisterFile::RegisterFile() {
|
||||
std::memset(values, 0, sizeof(values));
|
||||
|
||||
// Several context registers power up with non-zero values on real Xenos
|
||||
// hardware, so a title that reads one of them before writing it would
|
||||
// otherwise observe 0. These are the hardware reset defaults, corroborated by
|
||||
// the AMD R6xx/R7xx 3D register reference and the Mesa r600g driver, which
|
||||
// programs the same registers to the same values at context init.
|
||||
values[XE_GPU_REG_VGT_MAX_VTX_INDX] = 0x0000FFFF;
|
||||
values[XE_GPU_REG_VGT_MULTI_PRIM_IB_RESET_INDX] = 0x0000FFFF;
|
||||
values[XE_GPU_REG_PA_SC_SCREEN_SCISSOR_BR] = 0x20002000; // 8192 x 8192
|
||||
values[XE_GPU_REG_RB_STENCILREFMASK_BF] = 0x00FFFF00;
|
||||
values[XE_GPU_REG_PA_SU_POINT_SIZE] = 0x00080008;
|
||||
values[XE_GPU_REG_PA_SU_POINT_MINMAX] = 0x04000010;
|
||||
values[XE_GPU_REG_PA_SU_LINE_CNTL] = 0x00000008;
|
||||
values[XE_GPU_REG_PA_SC_LINE_CNTL] = 0x00000400;
|
||||
values[XE_GPU_REG_VGT_HOS_REUSE_DEPTH] = 0x0000000E;
|
||||
values[XE_GPU_REG_VGT_VERTEX_REUSE_BLOCK_CNTL] = 0x0000000E;
|
||||
values[XE_GPU_REG_VGT_OUT_DEALLOC_CNTL] = 0x00000010;
|
||||
// Guard-band clip/discard adjust (2.0 clip, 1.0 discard); hardwired on
|
||||
// hardware.
|
||||
values[XE_GPU_REG_PA_CL_GB_VERT_CLIP_ADJ] = 0x40000000; // 2.0f
|
||||
values[XE_GPU_REG_PA_CL_GB_VERT_DISC_ADJ] = 0x3F800000; // 1.0f
|
||||
values[XE_GPU_REG_PA_CL_GB_HORZ_CLIP_ADJ] = 0x40000000; // 2.0f
|
||||
values[XE_GPU_REG_PA_CL_GB_HORZ_DISC_ADJ] = 0x3F800000; // 1.0f
|
||||
values[XE_GPU_REG_PA_SC_AA_MASK] = 0x0000FFFF;
|
||||
// Tessellation levels also reset to 1.0f on hardware, but the backends apply
|
||||
// the effective factor as (register + 1.0f), so initializing them here would
|
||||
// change the effective reset factor; left out for separate review.
|
||||
// values[XE_GPU_REG_VGT_HOS_MAX_TESS_LEVEL] = 0x3F800000; // 1.0f
|
||||
// values[XE_GPU_REG_VGT_HOS_MIN_TESS_LEVEL] = 0x3F800000; // 1.0f
|
||||
}
|
||||
constexpr unsigned int GetHighestRegisterNumber() {
|
||||
uint32_t highest = 0;
|
||||
#define XE_GPU_REGISTER(index, type, name) \
|
||||
|
||||
@@ -1316,8 +1316,14 @@ bool VulkanRenderTargetCache::Resolve(const Memory& memory,
|
||||
clear_transfers_[1])) {
|
||||
uint64_t clear_values[2];
|
||||
clear_values[0] = resolve_info.rb_depth_clear;
|
||||
clear_values[1] = resolve_info.rb_color_clear |
|
||||
(uint64_t(resolve_info.rb_color_clear_lo) << 32);
|
||||
// For 64bpp formats, RB_COLOR_CLEAR_LO is the lower 32 bits of the
|
||||
// packed clear value. RB_COLOR_CLEAR is the upper 32 bits and, for
|
||||
// 32bpp formats, the whole value.
|
||||
clear_values[1] =
|
||||
resolve_info.color_edram_info.format_is_64bpp
|
||||
? resolve_info.rb_color_clear_lo |
|
||||
(uint64_t(resolve_info.rb_color_clear) << 32)
|
||||
: resolve_info.rb_color_clear;
|
||||
PerformTransfersAndResolveClears(2, clear_render_targets,
|
||||
clear_transfers_, clear_values,
|
||||
&clear_rectangle);
|
||||
|
||||
@@ -563,6 +563,28 @@ X_HRESULT XmpApp::DispatchMessageSync(uint32_t message, uint32_t buffer_ptr,
|
||||
kernel_state_->BroadcastNotification(kXNotificationXmpDashInitChanged, 1);
|
||||
return X_E_SUCCESS;
|
||||
}
|
||||
case 0x00070031: {
|
||||
// XMPGetNumSongsInTitlePlaylist
|
||||
// Song count exist at playlist_ptr->0x78, if playlist_ptr->0xc == 0 or 1
|
||||
// return song count, else return zero
|
||||
assert_true(!buffer_length ||
|
||||
buffer_length == sizeof(XMP_GET_NUM_SONGS_IN_TITLE_PLAYLIST));
|
||||
XMP_GET_NUM_SONGS_IN_TITLE_PLAYLIST* args =
|
||||
reinterpret_cast<XMP_GET_NUM_SONGS_IN_TITLE_PLAYLIST*>(buffer);
|
||||
|
||||
XELOGD(
|
||||
"XMPGetNumSongsInTitlePlaylist({:08X}, {:08X}, {:08X}), "
|
||||
"unimplemented",
|
||||
uint32_t(args->xmp_client.get()), args->playlist_ptr.get(),
|
||||
args->song_count_ptr.get());
|
||||
|
||||
if (!args->playlist_ptr || !args->song_count_ptr) {
|
||||
return X_E_INVALIDARG;
|
||||
}
|
||||
xe::store_and_swap<uint32_t>(
|
||||
memory_->TranslateVirtual(args->song_count_ptr), 0);
|
||||
return X_E_SUCCESS;
|
||||
}
|
||||
case 0x0007003D: {
|
||||
// XMPCaptureOutput
|
||||
assert_true(!buffer_length ||
|
||||
|
||||
@@ -194,6 +194,13 @@ struct XMP_DASH_INIT {
|
||||
};
|
||||
static_assert_size(XMP_DASH_INIT, 0x18);
|
||||
|
||||
struct XMP_GET_NUM_SONGS_IN_TITLE_PLAYLIST {
|
||||
xe::be<apu::XMP_CLIENT> xmp_client;
|
||||
xe::be<uint32_t> playlist_ptr;
|
||||
xe::be<uint32_t> song_count_ptr;
|
||||
};
|
||||
static_assert_size(XMP_GET_NUM_SONGS_IN_TITLE_PLAYLIST, 0xC);
|
||||
|
||||
struct XMP_CAPTURE_OUTPUT {
|
||||
xe::be<apu::XMP_CLIENT> xmp_client;
|
||||
xe::be<uint32_t> callback;
|
||||
|
||||
@@ -57,8 +57,14 @@ namespace xam {
|
||||
// https://github.com/tpn/winsdk-10/blob/master/Include/10.0.14393.0/km/wdm.h#L15539
|
||||
typedef enum _MODE { KernelMode, UserMode, MaximumMode } MODE;
|
||||
|
||||
dword_result_t XamFeatureEnabled_entry(dword_t app_id) { return 0; }
|
||||
DECLARE_XAM_EXPORT1(XamFeatureEnabled, kNone, kStub);
|
||||
dword_result_t XamFeatureEnabled_entry(qword_t feature_bit) {
|
||||
const std::bitset<36> feature(0x40ffffffff);
|
||||
if (feature.test(feature_bit)) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
DECLARE_XAM_EXPORT1(XamFeatureEnabled, kNone, kImplemented);
|
||||
|
||||
dword_result_t XamGetStagingMode_entry() { return cvars::staging_mode; }
|
||||
DECLARE_XAM_EXPORT1(XamGetStagingMode, kNone, kStub);
|
||||
@@ -336,6 +342,32 @@ void XamLoaderLaunchTitle_entry(lpstring_t raw_name_ptr, dword_t flags) {
|
||||
DECLARE_XAM_EXPORT1(XamLoaderLaunchTitle, kNone, kSketchy);
|
||||
|
||||
void XamLoaderTerminateTitle_entry() {
|
||||
std::string title = "Title terminated";
|
||||
std::string message = "Game requested exit to dashboard.";
|
||||
assert_always("Game requested exit to dashboard via XamLoaderTerminateTitle");
|
||||
|
||||
auto display_window = kernel_state()->emulator()->display_window();
|
||||
auto imgui_drawer = kernel_state()->emulator()->imgui_drawer();
|
||||
|
||||
if (display_window && imgui_drawer) {
|
||||
display_window->app_context().CallInUIThreadSynchronous(
|
||||
[imgui_drawer, title, message]() {
|
||||
auto dialog = xe::ui::ImGuiDialog::ShowMessageBox(
|
||||
imgui_drawer, title.c_str(), message.c_str());
|
||||
|
||||
std::jthread([dialog]() {
|
||||
while (!dialog->IsClosing()) {
|
||||
std::this_thread::yield();
|
||||
}
|
||||
|
||||
config::SaveConfig();
|
||||
xe::FlushLog();
|
||||
|
||||
std::quick_exit(0);
|
||||
}).detach();
|
||||
});
|
||||
}
|
||||
|
||||
// This function does not return.
|
||||
kernel_state()->TerminateTitle();
|
||||
}
|
||||
|
||||
@@ -34,6 +34,8 @@ DEFINE_bool(storage_selection_dialog, false,
|
||||
|
||||
DECLARE_int32(license_mask);
|
||||
|
||||
constexpr std::chrono::milliseconds kUIDelayMillis(200);
|
||||
|
||||
namespace xe {
|
||||
namespace kernel {
|
||||
namespace xam {
|
||||
@@ -81,8 +83,11 @@ X_RESULT xeXamDispatchDialog(T* dialog,
|
||||
return result;
|
||||
};
|
||||
auto post = []() {
|
||||
xe::threading::Sleep(std::chrono::milliseconds(100));
|
||||
kernel_state()->BroadcastNotification(kXNotificationSystemUI, false);
|
||||
std::jthread t([] {
|
||||
xe::threading::Sleep(kUIDelayMillis);
|
||||
kernel_state()->BroadcastNotification(kXNotificationSystemUI, false);
|
||||
});
|
||||
t.detach();
|
||||
};
|
||||
if (!overlapped) {
|
||||
pre();
|
||||
@@ -123,7 +128,7 @@ X_RESULT xeXamDispatchDialogEx(
|
||||
return result;
|
||||
};
|
||||
auto post = []() {
|
||||
xe::threading::Sleep(std::chrono::milliseconds(100));
|
||||
xe::threading::Sleep(kUIDelayMillis);
|
||||
kernel_state()->BroadcastNotification(kXNotificationSystemUI, false);
|
||||
};
|
||||
if (!overlapped) {
|
||||
@@ -143,10 +148,15 @@ X_RESULT xeXamDispatchHeadless(std::function<X_RESULT()> run_callback,
|
||||
uint32_t overlapped) {
|
||||
auto pre = []() {
|
||||
kernel_state()->BroadcastNotification(kXNotificationSystemUI, true);
|
||||
xe::threading::Sleep(std::chrono::milliseconds(25));
|
||||
};
|
||||
auto post = []() {
|
||||
xe::threading::Sleep(std::chrono::milliseconds(100));
|
||||
kernel_state()->BroadcastNotification(kXNotificationSystemUI, false);
|
||||
std::jthread t([]() {
|
||||
xe::threading::Sleep(kUIDelayMillis);
|
||||
kernel_state()->BroadcastNotification(kXNotificationSystemUI, false);
|
||||
});
|
||||
|
||||
t.detach();
|
||||
};
|
||||
if (!overlapped) {
|
||||
pre();
|
||||
@@ -167,7 +177,7 @@ X_RESULT xeXamDispatchHeadlessEx(
|
||||
kernel_state()->BroadcastNotification(kXNotificationSystemUI, true);
|
||||
};
|
||||
auto post = []() {
|
||||
xe::threading::Sleep(std::chrono::milliseconds(100));
|
||||
xe::threading::Sleep(kUIDelayMillis);
|
||||
kernel_state()->BroadcastNotification(kXNotificationSystemUI, false);
|
||||
};
|
||||
if (!overlapped) {
|
||||
@@ -198,7 +208,7 @@ X_RESULT xeXamDispatchDialogAsync(T* dialog,
|
||||
kernel_state()->xam_state()->xam_dialogs_shown_--;
|
||||
|
||||
auto run = []() -> void {
|
||||
xe::threading::Sleep(std::chrono::milliseconds(100));
|
||||
xe::threading::Sleep(kUIDelayMillis);
|
||||
kernel_state()->BroadcastNotification(kXNotificationSystemUI, false);
|
||||
};
|
||||
|
||||
@@ -220,7 +230,7 @@ X_RESULT xeXamDispatchHeadlessAsync(std::function<void()> run_callback) {
|
||||
kernel_state()->xam_state()->xam_dialogs_shown_--;
|
||||
|
||||
auto run = []() -> void {
|
||||
xe::threading::Sleep(std::chrono::milliseconds(100));
|
||||
xe::threading::Sleep(kUIDelayMillis);
|
||||
kernel_state()->BroadcastNotification(kXNotificationSystemUI, false);
|
||||
};
|
||||
|
||||
|
||||
@@ -7,6 +7,12 @@
|
||||
******************************************************************************
|
||||
*/
|
||||
|
||||
#include <fstream>
|
||||
#include <mutex>
|
||||
#include <unordered_set>
|
||||
|
||||
#include "third_party/fmt/include/fmt/format.h"
|
||||
#include "xenia/base/cvar.h"
|
||||
#include "xenia/base/logging.h"
|
||||
#include "xenia/kernel/info/file.h"
|
||||
#include "xenia/kernel/kernel_state.h"
|
||||
@@ -20,10 +26,50 @@
|
||||
#include "xenia/vfs/device.h"
|
||||
#include "xenia/xbox.h"
|
||||
|
||||
DEFINE_bool(
|
||||
log_file_io, false,
|
||||
"Reverse-engineering aid: log every NtReadFile as "
|
||||
"'READ <name> off=.. len=.. -> guest=..' to xenia_re_files.log, giving the "
|
||||
"file->guest-memory mapping. Cross-referenced with the GPU draw log's buffer "
|
||||
"guest addresses, this resolves which .xpr (and where inside it) a mesh was "
|
||||
"loaded from. De-duplicated by (name, offset, guest).",
|
||||
"Kernel");
|
||||
|
||||
namespace xe {
|
||||
namespace kernel {
|
||||
namespace xboxkrnl {
|
||||
|
||||
// RE aid: record the file->guest mapping for each distinct NtReadFile.
|
||||
static void LogFileReadForRE(const std::string& name, uint64_t byte_offset,
|
||||
uint32_t length, uint32_t guest_addr,
|
||||
uint32_t bytes_read) {
|
||||
if (!cvars::log_file_io) {
|
||||
return;
|
||||
}
|
||||
static std::mutex io_mutex;
|
||||
static std::ofstream io_out;
|
||||
static std::unordered_set<uint64_t> io_seen;
|
||||
std::lock_guard<std::mutex> lock(io_mutex);
|
||||
if (!io_out.is_open()) {
|
||||
io_out.open("xenia_re_files.log", std::ios::out | std::ios::trunc);
|
||||
}
|
||||
if (!io_out.is_open()) {
|
||||
return;
|
||||
}
|
||||
// De-dup by (offset, guest) — a whole-file load or a distinct buffer read is
|
||||
// logged once; streamed re-reads of the same region collapse.
|
||||
uint64_t key = (byte_offset << 20) ^ (uint64_t(guest_addr) << 4) ^ length;
|
||||
if (!io_seen.insert(key).second) {
|
||||
return;
|
||||
}
|
||||
if (io_seen.size() > 65536) {
|
||||
return;
|
||||
}
|
||||
io_out << fmt::format("READ {} off=0x{:X} len=0x{:X} -> guest=0x{:08X} read=0x{:X}\n",
|
||||
name, byte_offset, length, guest_addr, bytes_read);
|
||||
io_out.flush();
|
||||
}
|
||||
|
||||
struct CreateOptions {
|
||||
// https://processhacker.sourceforge.io/doc/ntioapi_8h.html
|
||||
static constexpr uint32_t FILE_DIRECTORY_FILE = 0x00000001;
|
||||
@@ -152,6 +198,13 @@ dword_result_t NtReadFile_entry(dword_t file_handle, dword_t event_handle,
|
||||
io_status_block->status = result;
|
||||
io_status_block->information = bytes_read;
|
||||
}
|
||||
// RE aid: record the file->guest-memory mapping (see LogFileReadForRE).
|
||||
if (cvars::log_file_io && file && file->entry()) {
|
||||
LogFileReadForRE(
|
||||
file->entry()->name(),
|
||||
byte_offset_ptr ? static_cast<uint64_t>(*byte_offset_ptr) : 0,
|
||||
buffer_length, buffer.guest_address(), bytes_read);
|
||||
}
|
||||
|
||||
// Queue the APC callback. It must be delivered via the APC mechanism even
|
||||
// though were are completing immediately.
|
||||
|
||||
@@ -599,19 +599,21 @@ def git_submodule_update():
|
||||
if sys.platform == "linux":
|
||||
submodules_ignore = ["DirectX-Headers", "DirectXShaderCompiler"]
|
||||
else:
|
||||
submodules_ignore = None
|
||||
submodules_ignore = []
|
||||
if is_amd64():
|
||||
submodules_ignore.append("xbyak_aarch64")
|
||||
if submodules_ignore:
|
||||
with open(".gitmodules") as f:
|
||||
gitmodules = f.read()
|
||||
submodules = re_findall(r"(?<=path = )(?!third_party\/(?:" + "|".join(submodules_ignore) + r")).+", gitmodules)
|
||||
else:
|
||||
submodules = None
|
||||
submodules = []
|
||||
# Sync submodule URLs from .gitmodules to local config
|
||||
shell_call([
|
||||
"git",
|
||||
"submodule",
|
||||
"sync",
|
||||
*(submodules or []),
|
||||
*submodules,
|
||||
])
|
||||
# Then update all submodules to their recorded commits
|
||||
shell_call([
|
||||
@@ -623,7 +625,7 @@ def git_submodule_update():
|
||||
"--init",
|
||||
"--depth=1",
|
||||
"-j", f"{os.cpu_count()}",
|
||||
*(submodules or []),
|
||||
*submodules,
|
||||
])
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user