14 Commits

Author SHA1 Message Date
MechaCat02
ea9f037b82 [GPU,Kernel] RE instrumentation: per-buffer draw log + file-I/O guest map
Some checks failed
Orchestrator / Commit Message Validation (push) Has been skipped
Orchestrator / Lint (push) Failing after 2m35s
Orchestrator / Windows (x86-64) (push) Has been skipped
Orchestrator / Linux (x86-64) (push) Has been skipped
Orchestrator / Create Release (push) Has been skipped
Draw logger (command_processor.cc): also key each record on the index-buffer
guest base so distinct static index buffers (ship body vs hangar vs weapons) no
longer collapse to one record; dump the first decoded index VALUES + raw index
bytes (8-in-16/32 endian-corrected) and the stream-base hex, to pin an index
buffer's exact .xpr file offset and validate a mesh decode against ground truth.

File-I/O log (xboxkrnl_io.cc): --log_file_io cvar writes each distinct NtReadFile
as 'READ <name> off=.. len=.. -> guest=..' to xenia_re_files.log, giving the
file->guest-memory mapping. Cross-referenced with the draw log's buffer guest
addresses this resolves which .xpr (and where inside it) a mesh loaded from.
De-duped by (offset, guest), capped at 65536 entries.

Also ignore build-cross/ (Wine cross-build output).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-19 20:19:12 +02:00
MechaCat02
29efc0381a [GPU] log_draws: also dump vertex positions for file correlation
Extend the RE draw-logger to dump the first few vertex POSITIONS (read
from guest memory) under each draw. 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 grep'd for in a resource file
to locate a mesh whose in-file offset is otherwise unknown.

Validated: world-space stage geometry byte-matches Stage_*.xpr at exact
offsets. (Load-time-transformed meshes like the player ship don't match,
which is itself a useful finding.)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-18 14:28:47 +02:00
MechaCat02
c6caa9e9c5 [GPU] Add log_draws: dump per-draw vertex declaration for RE
Reverse-engineering aid for decoding game mesh formats against GPU ground
truth. When the `log_draws` cvar is set, each distinct draw's primitive
type, index buffer (guest base / count / format / endianness), and full
per-stream vertex declaration (fetch-constant base + stride, and every
element's format + offset) is written to xenia_re_draws.log.

- command_processor.{h,cc}: CommandProcessor::LogDrawForRE(), no-op unless
  the cvar is set. De-dups by the vertex-declaration fingerprint (shader +
  primitive + element formats/offsets), so animated UI that redraws the
  same format into fresh buffers every frame collapses to one record --
  keeping it near-free (an earlier address-keyed de-dup flooded the log and
  stalled the GPU thread). Capped at 4096 distinct formats.
- pm4_command_processor_implement.h: call it from ExecutePacketType3Draw
  after IssueDraw (so the vertex shader has been analyzed). Backend-agnostic
  base path -- works for the Vulkan build.

Used to confirm Project Sylpheed's XBG7 mesh layout (triangle list, pos
f32x3 / normal f16x4 / uv f16x2, variable stride).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-18 14:28:47 +02:00
MechaCat02
3c8faf03f7 [Fix] threading_posix: reap thread handle once (mission-end freeze)
post_execution() runs on every successful Wait() and a Thread handle stays
signaled forever after exit, so multiple guest waits on the same exited handle
each call pthread_join() on an already-reaped pthread_t. On glibc that hangs
(recycled tid) -> the black-screen freeze at mission teardown. Guard reap with
an atomic exchange so it happens exactly once. Isolated from the audio/crash
WIP on phase-a-args-fileread; only this hunk, on stock 16e1eb8e2.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-17 22:05:49 +02:00
goldislead
16e1eb8e28 [GPU] Clarify 64bpp clear register order
This resolves the TODO in GetColorClearShaderConstants about which 32 bit portion is in which register.

For 64bpp formats, RB_COLOR_CLEAR_LO holds the lower 32 bits of the packed clear value, and RB_COLOR_CLEAR holds the upper 32 bits.
2026-07-17 08:18:56 +02:00
NicknineTheEagle
c6298dd0e3 [XAM] Implemented game exit behavior in XamLoaderTerminateTitle 2026-07-16 11:48:30 +02:00
Gliniak
44e1a64f35 [XAM] Extend UI delay on close. Added delay to headless after notification sent.
Created thread to do async notification broadcast on close for headless.

This fixes game freeze in 534407FC
2026-07-16 11:17:16 +02:00
Gliniak
c2650c6e58 [Testing] Define full test paths, unify test results of vectors to unsigned type 2026-07-16 10:47:15 +02:00
Gliniak
7ff152a5a7 [X64] MAX_V128/MIN_V128 Proper NaN handling.
According to console. FFC00000->FFC00000 instead of FFFFFC00 as it was previously.

Test case: 415607E8 - "Evil Queen's Castle" level
2026-07-15 23:31:38 +02:00
The-Little-Wolf
51322122a5 [Xam/Info] - Implement XamFeatureEnabled
- Implement XamFeatureEnabled
2026-07-15 19:55:04 +02:00
Gliniak
deaf21e9e1 [CI] Fixed CI failure caused by incorrect variable check 2026-07-15 19:53:27 +02:00
The-Little-Wolf
2799234fbb [Xam/XMP] - Stub XMPGetNumSongsInTitlePlaylist
- Stub XMPGetNumSongsInTitlePlaylist
2026-07-15 09:18:46 +02:00
SaveEditors
e20f26963f [GPU] Initialize GPU registers to hardware reset defaults
RegisterFile zeroes the whole register file at construction, but a real
console comes up with non-zero values in several of the context
registers. If a game reads one of those before writing it, it gets 0
under emulation and the real default on hardware.

I read the reset values off a retail console (read-only) and checked
them against the AMD R6xx register reference and the r600g defaults in
Mesa, which set the same registers to the same values at context init.
Where the hardware read matches the driver default it's a genuine
power-on default, not leftover runtime state.

Left the tessellation levels out on purpose: they reset to 1.0f too, but
the backends do register + 1.0f, so seeding them here would change the
effective reset factor. That needs its own change.
2026-07-14 21:07:49 +02:00
Margen67
bad346717a [xb] Skip aarch64 submodule for x64 target 2026-07-14 03:24:00 -07:00
23 changed files with 684 additions and 38 deletions

View File

@@ -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

View File

@@ -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
View File

@@ -116,3 +116,6 @@ node_modules/.bin/
/cache0
/devkit
recent.toml
# Cross-compile (Wine) build output — RE handoff, never commit
build-cross/

View File

@@ -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

View File

@@ -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,

View File

@@ -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;

View File

@@ -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

View File

@@ -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]

View File

@@ -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]

View File

@@ -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;

View File

@@ -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) {

View File

@@ -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 {

View File

@@ -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);

View File

@@ -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;
}

View File

@@ -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);
}
}

View File

@@ -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) \

View File

@@ -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);

View File

@@ -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 ||

View File

@@ -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;

View File

@@ -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();
}

View File

@@ -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);
};

View File

@@ -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.

View File

@@ -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,
])