From 2263470723dfe7da987cc539d07864e88eb3c435 Mon Sep 17 00:00:00 2001 From: Sylpheed RE agent Date: Tue, 18 Aug 2026 18:57:18 +0000 Subject: [PATCH 1/9] [RE] log_ui_draws: a per-draw, submission-order capture for 2D screens MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Neither existing RE hook can say in what order the game paints a UI screen: `log_draws` de-dups by vertex-declaration fingerprint, so a screen's sprites — which share a declaration — collapse into one record; and the F10 ship capture returns early on any draw without an f32x3 position stream, which is every UI quad. So: `--log_ui_draws` arms (on F10, alongside the ship capture) a snapshot of the next `--ui_draw_capture_frames` submitted frames, undeduplicated, writing every draw with its primitive type, index count, VS/PS hashes, each bound texture's base and dimensions, and the quad's vertex positions. The positions are the part that matters: a screen's sprites all share one shader and sample big texture pages, so the geometry is what names an element. Frames are counted here rather than from `counter_`. `counter_` looks like a frame number — the VdSwap packet increments it — but MarkVblank() increments it too, from the vblank thread, so it advances between two draws of the SAME frame. Bounded by it, the first capture ended after one draw having "covered 6 frames". Measured on the title screen: 11 draws a frame, every frame. --- src/xenia/app/emulator_window.cc | 5 + src/xenia/gpu/command_processor.cc | 195 +++++++++++++++++- src/xenia/gpu/command_processor.h | 27 +++ .../gpu/pm4_command_processor_implement.h | 4 + 4 files changed, 230 insertions(+), 1 deletion(-) diff --git a/src/xenia/app/emulator_window.cc b/src/xenia/app/emulator_window.cc index 6f2e01201..f62243e91 100644 --- a/src/xenia/app/emulator_window.cc +++ b/src/xenia/app/emulator_window.cc @@ -1071,6 +1071,11 @@ void EmulatorWindow::OnKeyDown(ui::KeyEvent& e) { // RE: snapshot the next frame's draws (world-space vertex positions) to // xenia_ship_capture.log for capital-ship placement correlation. xe::gpu::RequestShipCaptureFrame(); + // …and, when `log_ui_draws` is set, the next few frames' draws in + // SUBMISSION ORDER for 2D screen paint-order correlation. It shares this + // hotkey because every F-key in this switch is already bound; the cvar + // keeps it off the normal ship-capture path. + xe::gpu::RequestUiDrawCapture(); } break; case ui::VirtualKey::kEscape: { diff --git a/src/xenia/gpu/command_processor.cc b/src/xenia/gpu/command_processor.cc index 245543515..b52a46d7d 100644 --- a/src/xenia/gpu/command_processor.cc +++ b/src/xenia/gpu/command_processor.cc @@ -90,6 +90,30 @@ DEFINE_bool( "game mesh formats against GPU ground truth.", "GPU"); +DEFINE_bool( + log_ui_draws, false, + "Reverse-engineering aid: arm a UI DRAW-ORDER capture with F10. Writes " + "every draw of the next few swapped frames to xenia_re_ui_draws_NN.log in " + "SUBMISSION ORDER and undeduplicated, with each draw's bound texture base " + "and dimensions. For recovering the paint order of a 2D screen, which " + "log_draws cannot give (it de-dups by vertex declaration) and the F10 ship " + "capture cannot either (it skips draws with no f32x3 position stream).", + "GPU"); + +DEFINE_int32( + ui_draw_capture_frames, 3, + "Reverse-engineering aid: how many submitted frames one `log_ui_draws` " + "capture covers. 3 is enough for a screen that redraws its elements every " + "frame; a screen that composites once and then re-blits needs a window long " + "enough to contain the rebuild.", + "GPU"); + +DEFINE_int32( + ui_draw_capture_max, 20000, + "Reverse-engineering aid: hard stop on the number of draws one " + "`log_ui_draws` capture records, whatever the frame window says.", + "GPU"); + DEFINE_bool( readback_memexport, false, "Read data written by memory export in shaders on the CPU. " @@ -146,6 +170,174 @@ void RequestShipCaptureFrame() { XELOGI("[SHIP-CAP] capture armed → xenia_ship_capture_{:02d}.log", gen); } +// ── UI draw-order capture (RE) ────────────────────────────────────────────── +// F9 arms a snapshot of the next few swapped frames. Unlike the two hooks +// above this one does NOT de-duplicate: the whole question it answers is what +// order the guest submits a 2D screen's sprites in, and a de-duplicating log +// cannot answer that. Bounded by a frame count rather than a draw count so the +// records line up with what is on screen. +namespace { +std::atomic g_ui_capture_gen{0}; +std::atomic g_ui_capture_armed{false}; +// Frames are counted HERE rather than from `counter_`, which looks like a frame +// number and is not one: MarkVblank() also increments it, from the vblank +// thread, so it advances between two draws of the SAME frame. A capture bounded +// by it ended after a single draw. This one is bumped only by the VdSwap packet, +// on the command-processor thread, so it is exactly "frames submitted". +std::atomic g_ui_swap_count{0}; +} // namespace + +void NoteUiDrawCaptureSwap() { + if (g_ui_capture_armed.load(std::memory_order_relaxed)) { + g_ui_swap_count.fetch_add(1, std::memory_order_relaxed); + } +} + +void RequestUiDrawCapture() { + if (!cvars::log_ui_draws) { + return; + } + uint32_t gen = g_ui_capture_gen.fetch_add(1, std::memory_order_relaxed) + 1; + g_ui_swap_count.store(0, std::memory_order_relaxed); + g_ui_capture_armed.store(true, std::memory_order_relaxed); + XELOGI("[UI-CAP] capture armed -> xenia_re_ui_draws_{:02d}.log", gen); +} + +void CommandProcessor::CaptureUiDrawForRE( + uint32_t vgt_draw_initiator_value, + const IndexBufferInfo* index_buffer_info) { + if (!g_ui_capture_armed.load(std::memory_order_relaxed)) { + return; + } + static std::mutex ui_mutex; + static std::ofstream ui_out; + static uint32_t ui_gen = 0; + static uint32_t ui_first_frame = 0; + static int ui_draws = 0; + static uint32_t ui_last_frame = 0; + std::lock_guard lock(ui_mutex); + if (!g_ui_capture_armed.load(std::memory_order_relaxed)) { + return; + } + + uint32_t gen = g_ui_capture_gen.load(std::memory_order_relaxed); + if (gen != ui_gen || !ui_out.is_open()) { + if (ui_out.is_open()) { + ui_out.close(); + } + auto name = fmt::format("xenia_re_ui_draws_{:02d}.log", gen); + ui_out.open(name, std::ios::out | std::ios::trunc); + ui_gen = gen; + ui_first_frame = g_ui_swap_count.load(std::memory_order_relaxed); + ui_last_frame = ui_first_frame; + ui_draws = 0; + XELOGI("[UI-CAP] writing {} (from frame {})", name, ui_first_frame); + ui_out << fmt::format( + "# every draw in SUBMISSION ORDER, undeduplicated, frames {}..{}\n" + "# tex dimensions identify the sprite; base is the guest address\n", + ui_first_frame, + ui_first_frame + uint32_t(std::max(1, cvars::ui_draw_capture_frames)) - + 1); + } + if (!ui_out.is_open()) { + g_ui_capture_armed.store(false, std::memory_order_relaxed); + return; + } + uint32_t frame = g_ui_swap_count.load(std::memory_order_relaxed); + const uint32_t capture_frames = + uint32_t(std::max(1, cvars::ui_draw_capture_frames)); + if (frame - ui_first_frame >= capture_frames || + ui_draws >= std::max(1, cvars::ui_draw_capture_max)) { + ui_out.flush(); + g_ui_capture_armed.store(false, std::memory_order_relaxed); + XELOGI("[UI-CAP] done: {} draws over {} frames", ui_draws, + frame - ui_first_frame); + return; + } + if (frame != ui_last_frame) { + ui_out << fmt::format("--- frame {} ---\n", frame); + ui_last_frame = frame; + } + + reg::VGT_DRAW_INITIATOR init; + init.value = vgt_draw_initiator_value; + ui_out << fmt::format("{:4} prim={} indices={}", ui_draws++, + uint32_t(init.prim_type), + uint32_t(init.num_indices)); + if (index_buffer_info) { + ui_out << fmt::format(" ib=0x{:08X}", index_buffer_info->guest_base); + } + if (active_vertex_shader_) { + ui_out << fmt::format(" vs=0x{:016X}", + active_vertex_shader_->ucode_data_hash()); + } + Shader* ps = active_pixel_shader_; + if (ps) { + ui_out << fmt::format(" ps=0x{:016X}", ps->ucode_data_hash()); + } + if (ps && ps->is_ucode_analyzed()) { + 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). + ui_out << fmt::format(" tex[base=0x{:08X} {}x{} fmt={}]", + uint32_t(tf.base_address) << 12, + uint32_t(tf.size_2d.width) + 1, + uint32_t(tf.size_2d.height) + 1, + uint32_t(tf.format)); + } + } + // The quad's geometry is the only thing that says WHICH element a draw is: + // these sprites all share one shader and sample big texture pages, so the + // vertex data is the identity. Dump attribute 0 of binding 0 for the first + // few vertices, raw and as big-endian floats — the format is not assumed. + Shader* vs = active_vertex_shader_; + if (vs && vs->is_ucode_analyzed() && !vs->vertex_bindings().empty()) { + const auto& binding = vs->vertex_bindings()[0]; + xenos::xe_gpu_vertex_fetch_t fetch = + register_file_->GetVertexFetch(binding.fetch_constant); + uint32_t stride = binding.stride_words * 4; + uint32_t vbase = uint32_t(fetch.address) << 2; + ui_out << fmt::format("\n vb=0x{:08X} stride={} attrs=[", vbase, stride); + int32_t pos_off = -1; + uint32_t pos_fmt = 0; + for (const auto& attr : binding.attributes) { + uint32_t f = uint32_t(attr.fetch_instr.attributes.data_format); + uint32_t off = attr.fetch_instr.attributes.offset * 4; + ui_out << fmt::format("{}@{} ", f, off); + if (pos_off < 0) { + pos_off = int32_t(off); + pos_fmt = f; + } + } + ui_out << "]"; + if (stride && vbase && pos_off >= 0) { + auto be_u32 = [](const uint8_t* q) { + return (uint32_t(q[0]) << 24) | (uint32_t(q[1]) << 16) | + (uint32_t(q[2]) << 8) | uint32_t(q[3]); + }; + uint32_t nv = uint32_t(init.num_indices); + if (nv > 8) { + nv = 8; + } + ui_out << fmt::format(" fmt0={} v:", pos_fmt); + for (uint32_t v = 0; v < nv; ++v) { + const uint8_t* q = memory_->TranslatePhysical( + vbase + v * stride + uint32_t(pos_off)); + if (!q) { + break; + } + uint32_t w0 = be_u32(q), w1 = be_u32(q + 4); + float f0, f1; + std::memcpy(&f0, &w0, 4); + std::memcpy(&f1, &w1, 4); + ui_out << fmt::format(" [{:08X},{:08X}={:.2f},{:.2f}]", w0, w1, f0, f1); + } + } + } + ui_out << "\n"; +} + void CommandProcessor::CaptureShipDrawForRE( uint32_t vgt_draw_initiator_value, const IndexBufferInfo* index_buffer_info) { @@ -327,8 +519,9 @@ void CommandProcessor::CaptureShipDrawForRE( void CommandProcessor::LogDrawForRE(uint32_t vgt_draw_initiator_value, const IndexBufferInfo* index_buffer_info) { - // One-shot ship-placement capture runs independently of the log_draws cvar. + // One-shot captures run independently of the log_draws cvar. CaptureShipDrawForRE(vgt_draw_initiator_value, index_buffer_info); + CaptureUiDrawForRE(vgt_draw_initiator_value, index_buffer_info); if (!cvars::log_draws) { return; } diff --git a/src/xenia/gpu/command_processor.h b/src/xenia/gpu/command_processor.h index 3214d6c8c..b1f1cde23 100644 --- a/src/xenia/gpu/command_processor.h +++ b/src/xenia/gpu/command_processor.h @@ -39,6 +39,15 @@ namespace gpu { // Called from the UI thread (F10 hotkey); thread-safe. Defined in command_processor.cc. void RequestShipCaptureFrame(); +// Arm a one-shot UI DRAW-ORDER capture (see CommandProcessor::CaptureUiDrawForRE). +// Shares the F10 hotkey with the ship capture; the `log_ui_draws` cvar is what +// separates them. +void RequestUiDrawCapture(); + +// Count one submitted frame for that capture. Called from the VdSwap packet +// handler, on the command-processor thread. +void NoteUiDrawCaptureSwap(); + enum class GPUSetting { ClearMemoryPageState, ReadbackMemexport }; enum class ReadbackResolveMode { @@ -465,6 +474,24 @@ class CommandProcessor { void CaptureShipDrawForRE(uint32_t vgt_draw_initiator_value, const IndexBufferInfo* index_buffer_info); + // UI draw-order capture (RE): a one-shot snapshot armed by + // RequestUiDrawCapture() (F9 hotkey). Writes EVERY draw of the next few + // swapped frames to xenia_re_ui_draws_NN.log **in submission order and + // undeduplicated**, with each draw's bound texture base and dimensions. + // + // This exists because neither of the other two hooks can answer "in what + // order does the game paint a 2D screen": + // * `log_draws` de-dups by vertex-declaration fingerprint, so a screen's + // sprites — which share a declaration — collapse to one record, and what + // survives is first-seen order, not per-frame submission order; + // * CaptureShipDrawForRE returns early on any draw with no f32x3 position + // stream, which is every UI quad. + // Texture dimensions are the identity here: a screen's sprites decode to + // near-unique sizes, so `640x360` names the title screen's background as + // surely as a resource name would. Defined in command_processor.cc. + void CaptureUiDrawForRE(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 { diff --git a/src/xenia/gpu/pm4_command_processor_implement.h b/src/xenia/gpu/pm4_command_processor_implement.h index b09e0c877..3ef27d527 100644 --- a/src/xenia/gpu/pm4_command_processor_implement.h +++ b/src/xenia/gpu/pm4_command_processor_implement.h @@ -663,6 +663,10 @@ bool COMMAND_PROCESSOR::ExecutePacketType3_XE_SWAP(uint32_t packet, COMMAND_PROCESSOR::IssueSwap(frontbuffer_ptr, frontbuffer_width, frontbuffer_height); + // RE: bracket the UI draw-order capture on real submitted frames (`counter_` + // below is also bumped by MarkVblank, from another thread, so it cannot). + xe::gpu::NoteUiDrawCaptureSwap(); + ++counter_; return true; } From 8cca105eb9287afeef2a8c47f0f0f4b42e3099d7 Mon Sep 17 00:00:00 2001 From: Sylpheed RE agent Date: Tue, 18 Aug 2026 20:15:35 +0000 Subject: [PATCH 2/9] [RE] log_ui_draws: bound the capture by cvar, and log the quad's Z MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three changes, each paid for by a measurement that could not be made without it: * `--ui_draw_capture_frames` / `--ui_draw_capture_max` replace the compiled-in 3-frame, 20k-draw bounds. A screen that redraws every frame needs 3; finding the frames in which a screen is BUILT needs hundreds, and that answer (the title screen never rebuilds — it submits the same 11 draws every frame) is only reachable by turning the window up. * the vertex dump prints all three floats. Attribute 0 of a UI quad is k_32_32_32_FLOAT, so the stream carries a Z — the game's own layer key, if it had one. It does not: every Z is 0.00000, which is what makes submission order the whole of the paint order. * positions print as floats rather than raw words, now that the format is known. --- src/xenia/gpu/command_processor.cc | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/xenia/gpu/command_processor.cc b/src/xenia/gpu/command_processor.cc index b52a46d7d..cac3a14a2 100644 --- a/src/xenia/gpu/command_processor.cc +++ b/src/xenia/gpu/command_processor.cc @@ -327,11 +327,15 @@ void CommandProcessor::CaptureUiDrawForRE( if (!q) { break; } - uint32_t w0 = be_u32(q), w1 = be_u32(q + 4); - float f0, f1; + // Three floats, not two: attribute 0 of a UI quad is k_32_32_32_FLOAT, + // so the stream carries a Z per vertex — which is the game's own layer + // key if it has one, and the whole question this capture is asked. + uint32_t w0 = be_u32(q), w1 = be_u32(q + 4), w2 = be_u32(q + 8); + float f0, f1, f2; std::memcpy(&f0, &w0, 4); std::memcpy(&f1, &w1, 4); - ui_out << fmt::format(" [{:08X},{:08X}={:.2f},{:.2f}]", w0, w1, f0, f1); + std::memcpy(&f2, &w2, 4); + ui_out << fmt::format(" [{:.2f},{:.2f},z={:.5f}]", f0, f1, f2); } } } From 7dbb24e64f9e8a41b54ace39489f9e33b16cfe34 Mon Sep 17 00:00:00 2001 From: Sylpheed RE agent Date: Tue, 18 Aug 2026 20:15:35 +0000 Subject: [PATCH 3/9] [RE] --create_profile_if_none: bootstrap a profile for scripted runs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The title screen's (A) opens the Sign In dialog when no profile exists, and the game goes no further. That dialog cannot be completed from a scripted container: "Create Profile" asks for a gamertag in an ImGui text field, and synthetic X key events do not reach it — tried with the window focused, via XTEST and via --window, char by char, after clicking the field. Mouse clicks work; text entry does not. So: when no profile exists on disk and this cvar is set, create one at startup and sign it in. It uses ProfileManager's fixed `default_xuid` (B13EBABEBABEBABE), which is what makes it usable from a script — a later run passes --logged_profile_slot_0_xuid=B13EBABEBABEBABE and is deterministic. Ignored when a profile already exists, so it is safe to leave in a launcher. Verified: "RE bootstrap profile 'SylphRE' -> created", then on the next boot "Found 1 Profiles" / "Loaded SylphRE (GUID: B13EBABEBABEBABE) to slot 0". This does NOT get the game past the title — see the Reborn repo's docs/re/canary-scripted-input-traps.md for what does and does not, and for the content-path crash that is the actual blocker. --- src/xenia/kernel/xam/profile_manager.cc | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/src/xenia/kernel/xam/profile_manager.cc b/src/xenia/kernel/xam/profile_manager.cc index 33bb26479..66535d619 100644 --- a/src/xenia/kernel/xam/profile_manager.cc +++ b/src/xenia/kernel/xam/profile_manager.cc @@ -27,6 +27,16 @@ DEFINE_string(logged_profile_slot_2_xuid, "", DEFINE_string(logged_profile_slot_3_xuid, "", "XUID of the profile to load on boot in slot 3", "Profiles"); +DEFINE_string( + create_profile_if_none, "", + "Reverse-engineering aid: if no profile exists on disk, create one with " + "this gamertag at startup and sign it in. Without a signed-in profile the " + "title screen's (A) opens the Sign In dialog and the game goes no further, " + "and this container has no way to type a gamertag into it: the dialog is " + "an ImGui text field and synthetic X key events never reach it. Ignored " + "when a profile already exists.", + "Profiles"); + namespace xe { namespace kernel { namespace xam { @@ -105,6 +115,17 @@ ProfileManager::ProfileManager(KernelState* kernel_state, LoadAccount(account_xuid); } + // RE aid: bootstrap a profile for scripted runs (see the cvar's help). The + // fixed XUID is what makes it usable from a script — `default_xuid` gives + // 0xB13EBABEBABEBABE every time, so a run can pass + // --logged_profile_slot_0_xuid=B13EBABEBABEBABE and be deterministic. + if (accounts_.empty() && !cvars::create_profile_if_none.empty()) { + const bool ok = + CreateProfile(cvars::create_profile_if_none, true, true); + XELOGI("ProfileManager: RE bootstrap profile '{}' -> {}", + cvars::create_profile_if_none, ok ? "created" : "FAILED"); + } + if (!cvars::logged_profile_slot_0_xuid.empty()) { Login(xe::string_util::from_string( cvars::logged_profile_slot_0_xuid, true), From 043002a871d0fbadb0538d160456231f760cfd8b Mon Sep 17 00:00:00 2001 From: Sylpheed RE agent Date: Tue, 18 Aug 2026 22:18:38 +0000 Subject: [PATCH 4/9] [RE] Say so when a guest keystroke is swallowed by IsUIActive MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit XamInputGetKeystrokeEx returns X_ERROR_SUCCESS with a zeroed keystroke whenever any Xenia dialog is up, before consulting a driver. That is correct behaviour and an invisible one: from outside, scripted input simply stops working, the pad driver logs nothing because it is never asked, and the guest keeps polling. It is easy to enter that state by accident — F10 is both the RE capture hotkey and the toolkit's menu-bar key, and a dialog closed by its own [x] rather than by the menu toggle leaves the counter incremented. So log it, rate-limited to one line per 600 swallowed calls, with the dialog counters. On the runs this was written to diagnose it stays silent, which is what ruled the theory out — a diagnostic that is useful when it does not fire. --- src/xenia/kernel/xam/xam_input.cc | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/xenia/kernel/xam/xam_input.cc b/src/xenia/kernel/xam/xam_input.cc index 6179f7e5f..6b869916d 100644 --- a/src/xenia/kernel/xam/xam_input.cc +++ b/src/xenia/kernel/xam/xam_input.cc @@ -193,6 +193,21 @@ dword_result_t XamInputGetKeystrokeEx_entry( keystroke.Zero(); if (kernel_state()->xam_state()->IsUIActive()) { + // RE aid: this early return hands the guest a SUCCESS with an empty + // keystroke without ever asking a driver, so scripted input goes silently + // dead for as long as any Xenia UI is up — including a dialog that was + // closed by its own [x] rather than by the menu toggle that decrements the + // counter. An invisible input blackout is expensive to diagnose from the + // outside; say so, rarely enough not to spam. + static std::atomic swallowed{0}; + const uint32_t n = swallowed.fetch_add(1, std::memory_order_relaxed); + if ((n % 600) == 0) { + XELOGW( + "[RE-INPUT] XamInputGetKeystrokeEx swallowed by IsUIActive " + "(dialogs={} nui={}, {} so far)", + kernel_state()->xam_state()->xam_dialogs_shown_.load(), + kernel_state()->xam_state()->xam_nui_dialogs_shown_.load(), n + 1); + } return X_ERROR_SUCCESS; } From 4cd019b7697663d81ef8673be9aed5e9d19a07b8 Mon Sep 17 00:00:00 2001 From: Sylpheed RE agent Date: Wed, 19 Aug 2026 00:36:34 +0000 Subject: [PATCH 5/9] [RE] Arm the UI draw capture on F10 unconditionally MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The capture used to require --log_ui_draws at LAUNCH, which put an unexplained variable into every navigation run: across 12 runs, launching with the flag correlates with the title screen refusing (A) — 0 of 7 with it, 4 of 5 without. No mechanism was found. The cvar is read in exactly one place, when F10 arms a capture, and F10 was never pressed in those runs; the per-draw hook is a single relaxed atomic load; the startup config dumps of the two arms are byte-identical. An interleaved A/B also ruled out the obvious confound (boot duration): the latest title of all, 268 s, ACCEPTED (A), while a 232 s title refused. So rather than keep a variable nobody can explain in the path of every run, arm on F10 the way the ship capture next to it already does. The cvar stays, marked obsolete, so existing command lines still parse. Verified: F10 with no capture flags at all writes xenia_re_ui_draws_01.log. --- src/xenia/gpu/command_processor.cc | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/src/xenia/gpu/command_processor.cc b/src/xenia/gpu/command_processor.cc index cac3a14a2..1d2afb53b 100644 --- a/src/xenia/gpu/command_processor.cc +++ b/src/xenia/gpu/command_processor.cc @@ -92,7 +92,9 @@ DEFINE_bool( DEFINE_bool( log_ui_draws, false, - "Reverse-engineering aid: arm a UI DRAW-ORDER capture with F10. Writes " + "OBSOLETE — the UI draw-order capture is armed by F10 unconditionally now, " + "so this flag is no longer needed and is kept only so old command lines " + "still parse. Reverse-engineering aid: F10 writes " "every draw of the next few swapped frames to xenia_re_ui_draws_NN.log in " "SUBMISSION ORDER and undeduplicated, with each draw's bound texture base " "and dimensions. For recovering the paint order of a 2D screen, which " @@ -194,9 +196,14 @@ void NoteUiDrawCaptureSwap() { } void RequestUiDrawCapture() { - if (!cvars::log_ui_draws) { - return; - } + // No cvar gate. It used to require `log_ui_draws`, which meant a run that + // wanted a UI capture had to pass the flag at LAUNCH — and launching with it + // correlates, across 12 runs, with the title screen refusing (A) (0 of 7 with + // the flag, 4 of 5 without). No mechanism was found for that and boot time + // does not explain it either, so rather than keep an unexplained variable in + // every navigation run, the arming is now unconditional: F10 arms it, exactly + // as it arms the ship capture next to it. The cost when nobody presses F10 is + // one relaxed atomic load per draw. uint32_t gen = g_ui_capture_gen.fetch_add(1, std::memory_order_relaxed) + 1; g_ui_swap_count.store(0, std::memory_order_relaxed); g_ui_capture_armed.store(true, std::memory_order_relaxed); From 53b208bf4d8a7c5c79ca9334b920799c5e74b401 Mon Sep 17 00:00:00 2001 From: Sylpheed RE agent Date: Wed, 19 Aug 2026 01:09:04 +0000 Subject: [PATCH 6/9] [RE] log_ui_draws: log the UI quad's colour attribute MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A UI quad carries a k_8_8_8_8 colour next to its position, and its alpha is the fade the bundle's keyframes animate. Logging it costs one more read per vertex and turns the capture from "where is this quad" into "where is it and how faded". It was added to separate two elements that decode to the same 1133x280 and sit on opposite sides of a third in the declaration table. It does NOT separate them — both rest at 255 — but it does check the fade decode against the running game for the first time: every static element draws at exactly the resting alpha the bundle predicts, and the only two quads whose alpha changes between consecutive frames are the rotating effect pair and the PRESS (A) glow, which are the two things visibly animating. --- src/xenia/gpu/command_processor.cc | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/src/xenia/gpu/command_processor.cc b/src/xenia/gpu/command_processor.cc index 1d2afb53b..1c1376b9b 100644 --- a/src/xenia/gpu/command_processor.cc +++ b/src/xenia/gpu/command_processor.cc @@ -308,6 +308,11 @@ void CommandProcessor::CaptureUiDrawForRE( ui_out << fmt::format("\n vb=0x{:08X} stride={} attrs=[", vbase, stride); int32_t pos_off = -1; uint32_t pos_fmt = 0; + // The k_8_8_8_8 attribute is the per-vertex colour, and its ALPHA is what + // separates two elements that decode to the same size: the bundle gives + // them different resting fade alphas. Without it a draw of, say, 1133x280 + // could be either `ptlogo_back2eff` or `ptlogo_back2eff5`. + int32_t col_off = -1; for (const auto& attr : binding.attributes) { uint32_t f = uint32_t(attr.fetch_instr.attributes.data_format); uint32_t off = attr.fetch_instr.attributes.offset * 4; @@ -316,6 +321,11 @@ void CommandProcessor::CaptureUiDrawForRE( pos_off = int32_t(off); pos_fmt = f; } + if (col_off < 0 && + attr.fetch_instr.attributes.data_format == + xenos::VertexFormat::k_8_8_8_8) { + col_off = int32_t(off); + } } ui_out << "]"; if (stride && vbase && pos_off >= 0) { @@ -342,7 +352,15 @@ void CommandProcessor::CaptureUiDrawForRE( std::memcpy(&f0, &w0, 4); std::memcpy(&f1, &w1, 4); std::memcpy(&f2, &w2, 4); - ui_out << fmt::format(" [{:.2f},{:.2f},z={:.5f}]", f0, f1, f2); + ui_out << fmt::format(" [{:.2f},{:.2f},z={:.5f}", f0, f1, f2); + if (col_off >= 0) { + const uint8_t* c = memory_->TranslatePhysical( + vbase + v * stride + uint32_t(col_off)); + if (c) { + ui_out << fmt::format(",col={:08X}", be_u32(c)); + } + } + ui_out << "]"; } } } From f9170fd3fe07bf5c4d75f7d19e15ea58e7ed9895 Mon Sep 17 00:00:00 2001 From: Sylpheed RE agent Date: Wed, 19 Aug 2026 08:35:26 +0000 Subject: [PATCH 7/9] [RE] xam_input: log the keystroke path that REACHES the driver The existing [RE-INPUT] line only fires when IsUIActive() swallows a keystroke, so its silence is ambiguous: it means either 'the game is polling and getting nothing' or 'the game is not polling at all'. That is exactly the fork the title-screen investigation is stuck on - with the sign-in dialog fixed the swallow count is 0 and the title still does not respond to (A). Adds two logs on the other side: a rate-limited count of calls that get past IsUIActive to a driver, and one line per keystroke actually handed to the guest (rare by construction - one per physical press - so every one is logged, with user index, virtual key and flags). --- src/xenia/kernel/xam/xam_input.cc | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/src/xenia/kernel/xam/xam_input.cc b/src/xenia/kernel/xam/xam_input.cc index 6b869916d..2034e35b1 100644 --- a/src/xenia/kernel/xam/xam_input.cc +++ b/src/xenia/kernel/xam/xam_input.cc @@ -211,6 +211,21 @@ dword_result_t XamInputGetKeystrokeEx_entry( return X_ERROR_SUCCESS; } + // RE aid, the other half of the pair above: count the calls that DO reach a + // driver, and log every keystroke actually handed to the guest. Without this, + // "the swallow log is silent" is ambiguous between "the game is polling and + // getting nothing" and "the game is not polling at all" — which is exactly + // the fork the title-screen investigation got stuck on + // (docs/re/ui-paint-order-third-permutation.md in the Reborn tree). + { + static std::atomic polled{0}; + const uint32_t n = polled.fetch_add(1, std::memory_order_relaxed); + if ((n % 600) == 0) { + XELOGI("[RE-INPUT] XamInputGetKeystrokeEx reached the driver ({} so far)", + n + 1); + } + } + uint32_t user_index = *user_index_ptr; auto input_system = kernel_state()->emulator()->input_system(); auto lock = input_system->lock(); @@ -239,6 +254,12 @@ dword_result_t XamInputGetKeystrokeEx_entry( if (XSUCCEEDED(result)) { *user_index_ptr = keystroke->user_index; + // Rare by construction — one per physical press — so log every one. + XELOGI( + "[RE-INPUT] XamInputGetKeystrokeEx -> user={} vk={:04X} flags={:04X} " + "(call flags {:08X})", + (uint32_t)keystroke->user_index, (uint32_t)keystroke->virtual_key, + (uint32_t)keystroke->flags, (uint32_t)flags); } return result; } From 8d3299975ebf14772addef997489302d19136b3c Mon Sep 17 00:00:00 2001 From: Sylpheed RE agent Date: Wed, 19 Aug 2026 08:43:43 +0000 Subject: [PATCH 8/9] [RE] xam_input: test X_ERROR_SUCCESS exactly, not XSUCCEEDED X_ERROR_EMPTY is 0x10D2 and XSUCCEEDED is ((s & 0xC0000000) == 0), so an empty keystroke poll passes it. The first run of this instrumentation therefore logged every empty poll: 6499 lines of vk=0000 burying the two that mattered. The pre-existing assignment above shares the quirk - it writes user_index_ptr on an empty poll too. Left alone: it is upstream's and harmless, since the struct is zeroed first. The measurement itself survives, and is worth stating: at the title screen the game polls constantly and receives EXACTLY the two events sent - vk=5800 flags=0001 (KEYDOWN) and flags=0002 (KEYUP). Input delivery is not the problem. --- src/xenia/kernel/xam/xam_input.cc | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/xenia/kernel/xam/xam_input.cc b/src/xenia/kernel/xam/xam_input.cc index 2034e35b1..c355b3a8e 100644 --- a/src/xenia/kernel/xam/xam_input.cc +++ b/src/xenia/kernel/xam/xam_input.cc @@ -254,6 +254,13 @@ dword_result_t XamInputGetKeystrokeEx_entry( if (XSUCCEEDED(result)) { *user_index_ptr = keystroke->user_index; + } + // Test the code EXACTLY, not with XSUCCEEDED. `X_ERROR_EMPTY` is `0x10D2` + // and XSUCCEEDED is `(s & 0xC0000000) == 0`, so an empty poll "succeeds" — + // logging on XSUCCEEDED buried two real keystrokes under 6 499 empties in + // the first run of this instrumentation. (The assignment above shares the + // quirk; it is upstream's and harmless, since the struct is zeroed.) + if (result == X_ERROR_SUCCESS) { // Rare by construction — one per physical press — so log every one. XELOGI( "[RE-INPUT] XamInputGetKeystrokeEx -> user={} vk={:04X} flags={:04X} " From a60fe7d11c71cb09773aa7c8b5e7c4891f94a381 Mon Sep 17 00:00:00 2001 From: Sylpheed RE agent Date: Wed, 19 Aug 2026 10:41:05 +0000 Subject: [PATCH 9/9] [RE] threading_posix: fix a lost resume that left CREATE_SUSPENDED threads dead A thread created suspended publishes its state and its suspend count in TWO separate lock scopes: { lock; state_ = kSuspended; notify_all(); } // lock released here if (create_suspended) { lock; suspend_count_ = 1; wait(count == 0); } and Resume() does WaitStarted() - which waits only for state_ != kUninitialized - followed by `if (suspend_count_ == 0) return false;`. So a resumer can slip into the gap: it sees the thread started, sees suspend_count_ still 0, drops the resume and returns false. The new thread then sets the count to 1 and waits on it forever. A textbook lost wakeup. Measured in Project Sylpheed. Pressing (A) on the title makes the game do XamUserGetXUID -> NtCreateEvent -> ExCreateThread(entry=821748F0, CREATE_SUSPENDED) -> NtResumeThread, and the loader thread then never ran: zero kernel calls of its own (it appeared in the log only as an argument) and 00:00:00 host CPU time, while the emulator sat at 546% CPU. Boots reached the main menu 1 time in 6. Fixed by publishing state_ and suspend_count_ under one lock and waiting without releasing it, so a resumer past WaitStarted() always observes 1. On the first clean boot after the fix the same loader thread is the CALLER on 20 kernel-call lines and issues 4 ResolvePath asset reads. Every failed boot before it had exactly zero of both. Also logs when the host resume is refused. XThread::Resume's Linux path discarded that bool - the Windows path turns it into X_STATUS_UNSUCCESSFUL - so a dropped resume was invisible from both sides. Note the log is not by itself a defect: resuming a thread that is not suspended legitimately returns false, and it fires ~7 times in a normal boot. --- .claude/settings.local.json | 101 ++++++++++++++++++++++++++++++ src/xenia/base/threading_posix.cc | 29 ++++++--- src/xenia/kernel/xthread.cc | 10 ++- 3 files changed, 132 insertions(+), 8 deletions(-) create mode 100644 .claude/settings.local.json diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 000000000..5965a1de0 --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,101 @@ +{ + "permissions": { + "allow": [ + "Bash(ls -1 /home/fabi/xenia-canary/src/xenia/kernel/xboxkrnl/*.cc)", + "Bash(grep -l \"DECLARE_XBOXKRNL_EXPORT\\\\|DECLARE_XAM_EXPORT\\\\|DECLARE_XBDM_EXPORT\" /home/fabi/xenia-canary/src/xenia/kernel/xboxkrnl/*.cc)", + "Bash(grep -E \"\\\\.\\(cc|h\\)$\")", + "Bash(ls -1 /home/fabi/xenia-canary/src/xenia/kernel/xam/*.cc)", + "Bash(ls -1 /home/fabi/xenia-canary/src/xenia/kernel/xbdm/*.cc)", + "Bash(grep -r DECLARE_XBOXKRNL_EXPORT /home/fabi/xenia-canary/src/xenia/kernel/xboxkrnl/*.cc)", + "Bash(grep -r DECLARE_XAM_EXPORT /home/fabi/xenia-canary/src/xenia/kernel/xam/*.cc)", + "Bash(grep -r DECLARE_XBDM_EXPORT /home/fabi/xenia-canary/src/xenia/kernel/xbdm/*.cc)", + "Bash(/tmp/detailed_exports.sh:*)", + "Bash(chmod +x /tmp/detailed_exports.sh)", + "Bash(/tmp/detailed_exports.sh)", + "Bash(grep -c 'DECLARE_XBOXKRNL_EXPORT' /home/fabi/xenia-canary/src/xenia/kernel/xboxkrnl/*.cc)", + "Bash(grep -c 'DECLARE_XAM_EXPORT' /home/fabi/xenia-canary/src/xenia/kernel/xam/*.cc)", + "Bash(grep -c 'DECLARE_XAM_EXPORT' /home/fabi/xenia-canary/src/xenia/kernel/xam/*.h)", + "Bash(grep -c 'DECLARE_XBDM_EXPORT' /home/fabi/xenia-canary/src/xenia/kernel/xbdm/*.cc /home/fabi/xenia-canary/src/xenia/kernel/xbdm/*.h)", + "Bash(grep -h 'DECLARE_XBOXKRNL_EXPORT' /home/fabi/xenia-canary/src/xenia/kernel/xboxkrnl/*.cc)", + "Bash(grep -h 'DECLARE_XAM_EXPORT' /home/fabi/xenia-canary/src/xenia/kernel/xam/*.cc)", + "Bash(grep -ch 'DECLARE_XBOXKRNL_EXPORT' /home/fabi/xenia-canary/src/xenia/kernel/xboxkrnl/*.cc)", + "Read(//home/fabi/xenia-canary/**)", + "Bash(grep -ch 'DECLARE_XAM_EXPORT' /home/fabi/xenia-canary/src/xenia/kernel/xam/*.cc)", + "Bash(grep -ch DECLARE_XBDM_EXPORT /home/fabi/xenia-canary/src/xenia/kernel/xbdm/*.cc /home/fabi/xenia-canary/src/xenia/kernel/xbdm/*.h)", + "Bash(grep -oh 'DECLARE_XBOXKRNL_EXPORT[0-9_]*' /home/fabi/xenia-canary/src/xenia/kernel/xboxkrnl/*.cc)", + "Bash(grep -oh 'DECLARE_XAM_EXPORT[0-9_]*' /home/fabi/xenia-canary/src/xenia/kernel/xam/*.cc)", + "Bash(grep -oh 'DECLARE_XBDM_EXPORT[0-9_]*' /home/fabi/xenia-canary/src/xenia/kernel/xbdm/*.cc)", + "Bash(grep -h 'DECLARE_XBOXKRNL_EXPORT[34]' /home/fabi/xenia-canary/src/xenia/kernel/xboxkrnl/*.cc)", + "Bash(python3 tools/generate_export_docs.py)", + "Bash(python3 -c \":*)", + "Bash(grep -n \"RegisterOpcodeEmitter\\\\|RegisterEmitCategory\" /home/fabi/xenia-canary/src/xenia/cpu/ppc/*.cc)", + "Bash(find /home/fabi/xenia-canary -name *ppc*table*gen* -o -name *ppc-table*)", + "Bash(find /home/fabi/xenia-canary/tools -name *.xml -o -name *insn*)", + "Bash(wc -l /home/fabi/xenia-canary/src/xenia/cpu/ppc/ppc_emit_*.cc)", + "Bash(grep \"^int InstrEmit_\" /home/fabi/xenia-canary/src/xenia/cpu/ppc/ppc_emit_*.cc)", + "Bash(python3:*)", + "Bash(clang --version)", + "Bash(clang-19 --version)", + "Bash(cmake --version)", + "Bash(ninja --version)", + "Bash(dpkg -l)", + "Bash(git -C /home/fabi/xenia-canary submodule status)", + "Bash(apt-cache policy:*)", + "Bash(dpkg -l spirv-tools)", + "Bash(sudo apt-get:*)", + "Bash(/home/fabi/xenia-canary/build/bin/Linux/Debug/xenia_canary --help)", + "Bash(/home/fabi/xenia-canary/build/bin/Linux/Debug/xenia_canary --version)", + "Bash(wine --version)", + "Bash(cargo --version)", + "Bash(rustc --version)", + "Bash(awk '{print $2, $3}')", + "Read(//usr/bin/**)", + "Read(//usr/lib/llvm-19/bin/**)", + "Bash(dpkg -l '*clang*')", + "Bash(cargo install:*)", + "Bash(mkdir -p /home/fabi/.xwin-cache)", + "Bash(xwin --accept-license splat --output /home/fabi/.xwin)", + "Read(//home/fabi/.xwin/**)", + "Read(//home/fabi/.xwin-cache/**)", + "Bash(git submodule:*)", + "Bash(spirv-opt --version)", + "Bash(apt-cache search:*)", + "Bash(cmake -S . -B build-wine-xc -G 'Ninja Multi-Config' -DCMAKE_TOOLCHAIN_FILE=cmake/toolchains/linux-msvc-wine.cmake -DCMAKE_BUILD_TYPE=Release)", + "Bash(ls /usr/bin/*rc*)", + "Bash(llvm-rc --help)", + "Bash(cmake --build build-wine-xc --config Release --target xenia-app)", + "Bash(cmake -S . -B build-wine-xc -G 'Ninja Multi-Config' -DCMAKE_TOOLCHAIN_FILE=cmake/toolchains/linux-msvc-wine.cmake -DCMAKE_BUILD_TYPE=Release -DHAVE_SYS_UIO_H=FALSE -DHAVE_SYS_MMAN_H=FALSE -DHAVE_SYS_RESOURCE_H=FALSE -DHAVE_SYS_TIME_H=FALSE -DHAVE_UNISTD_H=FALSE)", + "Bash(grep -r \"PPCFrontend\\\\|Translate\" /home/fabi/xenia-canary/src/xenia/cpu/ppc/*.h)", + "Bash(grep -l \"SDL\\\\|sdl\" /home/fabi/xenia-canary/src/xenia/ui/*.h)", + "Bash(grep -r \"SDL_Init\\\\|SDL\\\\|OpenGL\\\\|Vulkan\" /home/fabi/xenia-canary/src/xenia/ui/*.cc)", + "Bash(grep -l \"struct.*Modifier\\\\|class.*Shader\" /home/fabi/xenia-canary/src/xenia/gpu/d3d12/*.h)", + "Bash(grep \"class.*App\\\\|class.*Main\" /home/fabi/xenia-canary/src/xenia/app/*.h)", + "Bash(grep -r \"WinMain\\\\|main\\(\" /home/fabi/xenia-canary/src/xenia/app/*.cc)", + "Bash(grep \"REGISTER_MODULE\\\\|MODULE_INIT\" /home/fabi/xenia-canary/src/xenia/kernel/xboxkrnl/*.cc)", + "Bash(wc -l /home/fabi/xenia-canary/src/xenia/cpu/ppc/ppc_emit_*.cc /home/fabi/xenia-canary/src/xenia/cpu/ppc/ppc_opcode.h)", + "Bash(cargo check:*)", + "Bash(cargo test:*)", + "Bash(cargo build:*)", + "Bash(cargo clippy:*)", + "Bash(cargo run:*)", + "Bash(grep -r \"memmap\\\\|mmap\" /home/fabi/xenia-canary/xenia-rs/Cargo.toml /home/fabi/xenia-canary/xenia-rs/crates/*/Cargo.toml)", + "Bash(cargo search:*)", + "Bash(cargo info:*)", + "Bash(cargo doc:*)", + "Read(//home/fabi/.cargo/registry/src/**)", + "Bash(find ~/.cargo/registry/src -path \"*/lzxd-0.2*/src/lib.rs\" -exec grep -n \"pub fn\\\\|pub struct\\\\|pub enum\\\\|WindowSize\" {} \\\\;)", + "Bash(find ~/.cargo/registry/src -path \"*/lzxd-0.2*/src/window.rs\" -exec grep -n \"WindowSize\" {} \\\\;)", + "Bash(find ~/.cargo/registry/src -path \"*/lzxd-0.2*/src/window.rs\" -exec sed -n '12,70p' {} \\\\;)", + "Bash(find ~/.cargo/registry/src -path \"*/lzxd-0.2*/src/lib.rs\" -exec sed -n '280,310p' {} \\\\;)", + "Bash(grep -r \"aes\\\\|cipher\" Cargo.toml crates/*/Cargo.toml)", + "Bash(grep -v \"^warning\\\\|^\\\\s*-->\\\\|^\\\\s*|\\\\|^\\\\s*=\\\\|Compiling\\\\|Finished\\\\|Running\\\\|Detected\\\\|debug\\\\|^$\")", + "Read(//tmp/**)", + "Bash(echo \"exit code: $?\")", + "Bash(echo \"exit: $?\")", + "Bash(git add:*)", + "Bash(git commit -m ':*)", + "Bash(git push:*)", + "Bash(git config:*)" + ] + } +} diff --git a/src/xenia/base/threading_posix.cc b/src/xenia/base/threading_posix.cc index 38cb2cb4c..7198055c4 100644 --- a/src/xenia/base/threading_posix.cc +++ b/src/xenia/base/threading_posix.cc @@ -1328,18 +1328,33 @@ void* PosixCondition::ThreadStartRoutine(void* parameter) { current_thread_ = thread; thread->handle_.tid_ = static_cast(syscall(SYS_gettid)); + // Publish the state AND the initial suspend count under ONE lock, then wait + // without ever dropping it. + // + // These used to be two separate lock scopes, and that lost a resume. Resume() + // does `WaitStarted()` — which only waits for state_ != kUninitialized — and + // then `if (suspend_count_ == 0) return false;`. So a thread created + // suspended could publish kSuspended, release the lock, and be resumed in the + // gap before it had set suspend_count_ = 1: the resume saw 0, returned false, + // and this thread then set the count and waited on it forever. + // + // Measured in Project Sylpheed: pressing (A) on the title screen makes the + // game ExCreateThread(CREATE_SUSPENDED) + NtResumeThread, and about 2 boots + // in 3 the loader thread never ran — zero kernel calls, zero host CPU time, + // while the emulator stayed at 546%. Holding the lock across the wait closes + // the window: a resumer past WaitStarted() now always observes 1. { std::unique_lock lock(thread->handle_.state_mutex_); thread->handle_.state_ = create_suspended ? State::kSuspended : State::kRunning; + if (create_suspended) { + thread->handle_.suspend_count_ = 1; + } thread->handle_.state_signal_.notify_all(); - } - - if (create_suspended) { - std::unique_lock lock(thread->handle_.state_mutex_); - thread->handle_.suspend_count_ = 1; - thread->handle_.state_signal_.wait( - lock, [thread] { return thread->handle_.suspend_count_ == 0; }); + if (create_suspended) { + thread->handle_.state_signal_.wait( + lock, [thread] { return thread->handle_.suspend_count_ == 0; }); + } } start_routine(); diff --git a/src/xenia/kernel/xthread.cc b/src/xenia/kernel/xthread.cc index 657c0fb37..756e2d8da 100644 --- a/src/xenia/kernel/xthread.cc +++ b/src/xenia/kernel/xthread.cc @@ -932,7 +932,15 @@ X_STATUS XThread::Resume(uint32_t* out_suspend_count) { // Try to resume host thread if fully resumed (for non-self-suspended case). if (should_resume_host) { - thread_->Resume(&unused_host_suspend_count); + if (!thread_->Resume(&unused_host_suspend_count)) { + // The Windows path above turns this into X_STATUS_UNSUCCESSFUL; here it + // was discarded, so a dropped resume was invisible from both sides — the + // guest saw success and the thread never ran. Keep returning success (the + // guest's own bookkeeping is done), but say so, because the only symptom + // otherwise is a thread with no CPU time and no kernel calls. + XELOGW("XThread::Resume: host resume was refused for thread {:08X}", + handle()); + } } return X_STATUS_SUCCESS; #endif