Skip to content

Onimusha WotS (PPSA27836) hangs on black screen after successful startup #6006

Open

Description

@amiraction0938

Onimusha WotS (PPSA27836) hangs on black screen waiting on SphiaSema

Summary

The entry-point crash is resolved on current main with #4237. However, Onimusha: Way of the Sword now gets stuck on a black screen during startup.

The game window remains open, but there is no visible gameplay, audio, or NVIDIA overlay. CPU usage spikes to around 60–70% during the first second, then drops to approximately 5–9% and stays there.

I captured a full synchronization trace using APS5_TRACE_SYNC=1. The trace suggests that a guest thread is waiting indefinitely on a SphiaSema semaphore after the initial startup and NP callback stages have completed.

Environment

  • Game: Onimusha: Way of the Sword
  • Title ID: PPSA27836
  • Base version: v01.000.001
  • Executable: Unwrapped ELF
  • Emulator: AnyPS5, current main
  • OS: Windows 11, Build 28000.3086
  • GPU: NVIDIA GeForce RTX 5070 Ti
  • Additional patch: The local mprotect mask patch is still applied while waiting for fix(libkernel): drop undefined protection bits in sceKernelMprotect #5770. Everything else is stock main.

What works

The game successfully gets through the following stages:

  • ELF entry point
  • PRX library loading
  • sceHttp2Init
  • Vulkan device and swapchain initialization (1484 × 835, two images)
  • Shader registration and compilation
  • Controller initialization (PadInput, Xbox Series X controller)
  • NP initialization and callbacks, including:
    • sceNpSetNpTitleId
    • sceNpRegisterStateCallbackA
    • sceNpGetOnlineId
  • Rebe/Sphia initialization, with SPHIA-BYPASS-SUCCESS reported

The shader disk cache reports 897 hits, 0 misses, and 0 writes.

Current behavior

After initialization, the game remains on a black screen indefinitely. There is no audio, and the NVIDIA overlay does not appear. The window remains responsive enough to stay open, and the emulator does not crash.

The last normal log activity includes opening the game's PAK files:

[src\Open.cpp:214 sceKernelOpen] path=/app0/re_chunk_000.pak ...
[src\Open.cpp:214 sceKernelOpen] path=/app0/re_chunk_000.pak.patch_001.pak ...
[src\Open.cpp:214 sceKernelOpen] path=/app0/re_chunk_000.pak.sub_000.pak.patch_001.pak ...
[shader-disk-cache] (10 s): 897 hits, 0 misses, 0 writes

Synchronization trace

I ran app_v19.exe with APS5_TRACE_SYNC=1. The resulting trace is approximately 413 KB and contains 4,798 lines.

After the successful initialization stages, the trace repeatedly reports an infinite wait on SphiaSema:

[sema] SPHIA-BYPASS-SUCCESS ... 'SphiaSema' caller=0000000408778687
[sema] wait ... 'SphiaSema' need=1 have=0 timeout=-1 caller=0000000408778687

The trace contains three distinct SphiaSema objects:

  • One with max=1, which is signaled once at line 232; Rebe initialization subsequently passes.
  • One with max=2147483647, which is also signaled once at line 232.
  • Another with max=2147483647, which is waited on indefinitely by guest caller 0000000408778687.

Other observations from the trace:

  • No sceHttp2CreateRequest or sceHttp2SendRequest calls appear.
  • Only three event-flag set operations appear in the entire trace, at lines 219, 229, and 477.
  • The guest thread name RebeWebApiTh does not appear in the trace.
  • CPU usage drops from approximately 60–70% to 5–9% after the first second and stays low.

Questions

  1. How can I map the guest caller address 0x408778687 to its module, function, or PRX so I can identify which code is waiting on this semaphore?
  2. Would additional tracing, such as guest thread creation and naming, sceKernelWaitSema return addresses, or semaphore wait results, help identify the thread responsible for the hang?
  3. Is there a known pattern in AnyPS5 for games getting stuck in an indefinite semaphore wait after NP callbacks and Sphia/Rebe initialization have completed?

I can provide the full APS5_TRACE_SYNC=1 trace and app_diag_log.txt if needed.

Image

Activity

  1. MarcZX10 commented on Oct 14, 2026

    @MarcZX10
    Contributor

    The repository implements sceKernelGetModuleInfoFromAddr(address, 2, &info) on both Windows and Linux. If you can add a temporary guest-side diagnostic for 0x408778687, it should identify the loaded module containing that address and return its segment ranges. Could you share the returned module name and executable segment range (or the error if it returns ESRCH)? That will tell us whether to inspect the unwrapped eboot or a PRX before adding more trace instrumentation.

    If the unwrapped module retains symbols, llvm-addr2line/llvm-symbolizer can then resolve the PC using that exact module. If it is stripped, the module-relative offset and segment range still let us inspect the matching disassembly without assuming this is eboot code.

  2. amiraction0938 commented on Oct 14, 2026

    @amiraction0938
    Author

    @MarcZX100 — thanks for the pointer! We added the diagnostic you suggested and tracked 0x408778687 down to the unwrapped eboot. Here's what we found.

    Diagnostic output

    We added a temporary diagnostic to sceKernelWaitSema in libkernel.prx, calling sceKernelGetModuleInfoFromAddr(0x408778687, 2, &info) when RebeWebApiTh first attempted to wait on SphiaSema.

    [DIAG-0x408778687] RebeWebApiTh detected waiting on semaphore 'SphiaSema'
    [DIAG-0x408778687] sceKernelGetModuleInfoFromAddr -> SUCCESS
    [DIAG-0x408778687]   module name       : app_v25.exe
    [DIAG-0x408778687]   module id         : 0x20000000
    [DIAG-0x408778687]   segment_count     : 3
    [DIAG-0x408778687]   seg[0] address=0x400000000 size=0x8ECC000 prot=0x5
    [DIAG-0x408778687]     -> target is INSIDE seg[0], offset=+0x8778687
    [DIAG-0x408778687]   seg[1] address=0x408ECC000 size=0x13EA000 prot=0x1
    [DIAG-0x408778687]   seg[2] address=0x40A2B8000 size=0x3BA8000 prot=0x3
    

    Findings

    • sceKernelGetModuleInfoFromAddr returned SUCCESS, so the address belongs to a registered module.
    • The module is the unwrapped/relinked eboot (app_v25.exe), loaded at 0x400000000 using --windows-fixed-base.
    • The target address is inside segment 0 (R+X), at module-relative offset 0x8778687.
    • The eboot appears to be fully stripped: nm and readelf -sW found no symbols, so llvm-addr2line doesn't seem useful here.

    Disassembly around the address

    We mapped guest address 0x408778687 back to the raw eboot file and disassembled the surrounding code:

    877c66b: push   rbp
    877c66c: mov    rbp,rsp
    877c66f: push   rbx
    877c670: push   rax
    877c671: mov    rax,QWORD PTR [rdi+0x48]    ; start of loop
    877c675: mov    rbx,rdi
    877c678: mov    esi,0x1
    877c67d: xor    edx,edx
    877c67f: mov    rdi,rax
    877c682: call   0x8ecbe60                  ; sceKernelWaitSema wrapper
    877c687: mov    rdi,rbx                    ; return address: 0x408778687
    877c68a: call   0x877c770                  ; local method
    877c68f: call   0x8ecb460                  ; another call
    877c694: cmp    BYTE PTR [rbx+0x40],0x1
    877c698: mov    rdi,rbx
    877c69b: jne    0x877c671                  ; loop until the flag is set
    877c69d: add    rsp,0x8
    877c6a1: pop    rbx
    877c6a2: pop    rbp
    877c6a3: jmp    0x877c666

    This looks like a worker loop: it waits on a semaphore, calls two functions, checks a flag at [rbx+0x40], and loops while the flag is not set.

    One important detail: 0x408778687 is the return address immediately after the sceKernelWaitSema call, rather than the address where the thread is blocked. The loop itself is in the eboot, not a PRX.

    Current understanding

    We haven't identified what [rbx+0x40] represents yet. Also, based on the current trace and our understanding of the Sphia implementation, we haven't found an emulator-side path that signals this particular semaphore. That may explain why the worker never progresses, but we'd like to verify the intended behavior before patching anything.

    What's the best next step?

    Would you recommend one of these approaches?

    1. Trace semaphore creation and signaling: identify where this SphiaSema is created and whether the game ever calls sceKernelSignalSema on it.
    2. Inspect the callees: disassemble 0x8ecbe60, 0x877c770, and 0x8ecb460 to better understand what happens during each iteration.
    3. Investigate the exit flag: trace writes to [rbx+0x40] and determine what needs to happen for the worker to exit.

    I'd prefer to understand the expected synchronization behavior before forcing the flag or faking a semaphore signal, since either could just move the hang somewhere else.

    Which direction would you recommend?

    Thanks!

  3. amiraction0938 commented on Oct 14, 2026

    @amiraction0938
    Author

    Following up on my last question: I did not wait for an answer and instead used a temporary workaround for the Sphia wait so I could see what comes after it. Details below.

    Update: Onimusha now gets past Rebe/Sphia and the PAK loading, but aborts in the shader recompiler (tessellation + bindless image descriptors)

    Thanks again to @MarcZX100 for the pointer. I have more findings, and the blocker has moved from CPU-side sync to the shader recompiler. I'm sharing the exact values in case they help.

    Important: these results are on tag v0.1.2 (detached HEAD) with local, uncommitted patches, not on current main. I have not re-tested on current main.

    1. A mistake in my original report: the PAKs were not under app0

    The game opens /app0/re_chunk_000.pak etc., which resolves to <run dir>/app0/re_chunk_000.pak. My PAK files were only in the run directory root, so those opens would presumably fail.

    This was easy to miss: sceKernelOpen logs path=... flags=... before resolving the path, and I saw no failure message in the trace, so the log looked fine. It might be useful to also log the resolved native path and the errno/result on failure.

    I fixed it with NTFS hard links (mklink /H) of the 4 PAK files into app0\. After this, the same build ran much further (the trace grew from ~330 lines to 2700+).

    2. Local workarounds I applied (these are hacks, not proposed fixes)

    • Rebe/Sphia: I short-circuit sceKernelWaitSema for the semaphore named SphiaSema (it returns OK immediately). The runs in this report used an unmodified eboot: an earlier experiment that NOP'ed the jne at file offset 0x877C69B was not in the binary used for these runs (correction to my earlier wording). This only avoids the infinite wait; I don't know the intended behavior of Sphia.

    3. Hull shader / tessellation lowering failures (4 shaders, 3 different checks)

    All thrown from core/shader/recompiler/Optimization/src/TessellationLowering.cpp. For each I added a temporary fprintf to dump the values at the failing check. Every failing hull shader had inputs=1, outputs=1 (a single input and single output control point).

    (a) the hull shader reads no LDS per control point, so the local stride is unknown (shaders 0x2106876600, 0x211243e600, stage 2, pc 0x58):

    ControlInput pc=0x58 vertex=0 relative=24 constant=16 dynamic=0 components=2 inputs=1 outputs=1 totalAccesses=13 localOutputAccesses=0
    

    localStride is only derived from a LocalOutput access, a ControlInput with vertex != 0, or a dynamic ControlInput. Here the access is non-dynamic with vertex=0, so localStride stays 0 although relative / inputs = 24 is clearly the stride.

    (b) the hull shader writes off-chip memory outside its control points and patch constants (shaders 0x2106870700, 0x2106879900, stage 2, pc 0x8c):

    ControlOutput pc=0x8c vertex=0 relative=32 constant=0 dynamic=0 components=2 controlStride=0 localStride=24 lowest=0 patchesPerGroup=255 inputs=1 outputs=1
    

    With one output control point, vertex stays 0, so the write is classified as a patch-constant record and controlStride is never established (it is only set from vertex != 0 writes or from the domain shader reads).

    (c) per-patch tessellation output is not written by control point 0 alone (shader 0x2112038600, stage 2, pc 0x9c):

    FirstInvocationOnly(...) false at pc=0x9c/0xa4 (kind=5), 0xac/0xb4/0xbc (kind=4), inputs=1 outputs=1
    

    What I changed locally (guesses, unverified):

    • (a) if localStride == 0, derive it from a non-dynamic ControlInput with vertex == 0 as relative / inputs.
    • (b) if outputs == 1 and controlStride == 0, derive it from a ControlOutput with vertex == 0 and relative (when the write fits in relative), and treat that write as a control-point write.
    • (c) skip the FirstInvocationOnly requirement when inputs == 1 && outputs == 1, since there is only one invocation per patch.

    With these three relaxations the recompiler gets past all four hull shaders. I have not been able to check whether the resulting rendering is correct, because of the next failure. I would really appreciate a review from someone who knows this lowering: is "single control point, so vertex index is folded away" the right interpretation, or is there a better fix?

    Attached: tessellation_lowering_local_changes.txt (my local diff for (a)-(c), including the temporary fprintf diagnostics). It is a workaround to get past the checks, not a proposed fix.

    4. Current blocker: bindless-style image descriptor

    shader 0x211143cc00 stage 5: ShaderRecompiler::Recompile: GetImageResource dword 0 is not a valid runtime value;
    chain: ReadConstBuffer@pc=0x0000297c(GetBufferResource(ReadConst(GetSrtResource, Immediate) x4),
    IMul32(ReadConst(GetSrtResource, Immediate), Immediate))
    

    If I read the chain correctly, the image descriptor is loaded from a buffer at an offset computed from a runtime value read from the SRT (index * stride), i.e. a bindless-like table of descriptors. This is a missing recompiler feature rather than a small bug, so I stopped patching here. The process aborts with an unhandled std::runtime_error (SIGABRT). The window stays black, with no frames presented.

    Questions

    1. For (a)-(c): are the relaxations above sound, or would you handle single-control-point hull shaders differently?
    2. Is runtime-indexed image descriptor loading (the pattern in section 4) planned or already tracked in another issue?
    3. Is there a recommended way to keep debugging Onimusha from here (e.g. a branch to test), or is it better to wait for descriptor support?

    I can attach the full trace, the TessellationLowering.cpp diff, and the diagnostic output if useful.

    Environment: Windows 11 (Build 28000.3086), RTX 5070 Ti, i5-14600K, 32 GB RAM, tag v0.1.2 with local patches.

    tessellation_lowering_local_changes.txt

  4. MarcZX10 commented on Oct 14, 2026

    @MarcZX10
    Contributor

    I opened #6123 to extend APS5_TRACE_SYNC with opt-in semaphore create, signal, and wait begin/end records keyed by semaphore address, name, and guest caller. It should let a run distinguish same-named semaphore instances and show whether a signal reaches the particular waiter. The focused Linux trace regression passes locally; the emulator-linked test is blocked here by the host CPU feature gate. No need to apply a local diagnostic patch if the PR is useful after review.

  5. MarcZX10 commented on Oct 14, 2026

    @MarcZX10
    Contributor

    I opened #6132 to add failure diagnostics to sceKernelOpen: a failed native open now logs the guest path, resolved path, and returned guest error; path-resolution failures log their error and result. The regression check covers a missing .pak path. The Linux target builds, though the emulator-linked test cannot reach its body on this host because it lacks the required CPU extensions. This improves visibility into path failures; it does not establish that the Onimusha wait is caused by missing PAK files.

  6. MarcZX10 commented on Oct 15, 2026

    @MarcZX10
    Contributor

    Thanks, this moves the blocker from the semaphore to the recompiler. There is related work in #6191 and open PR #6232, but #6232 specifically follows pointer hops from an image-table record to a T#. Its description explicitly says the Ratchet chain with a second readfirstlane index is not covered. Your reported chain (GetBufferResource from SRT plus an IMul32-computed ReadConstBuffer offset) is not demonstrated to fit that record model, so I would treat it as potentially distinct rather than assume #6232 fixes it. Since these results are from v0.1.2 with local workarounds, the next useful check is whether the same planner chain still occurs on current main; if so, comparing that exact shader against #6232 would show whether a separate issue is needed. I would avoid the semaphore/tessellation workarounds as evidence for this descriptor path.

  7. MarcZX10 commented on Oct 17, 2026

    @MarcZX10
    Contributor

    For the next step, please first reproduce the descriptor failure on current main without the local semaphore/tessellation workarounds, then capture the planner chain and shader PC again. The report explicitly pins those results to v0.1.2, so we cannot yet tell whether current main has changed the chain. If it still fails, compare that exact chain against #6232 and #6191 (including the dynamic SRT-derived buffer offset); if neither models it, a focused shader fixture or issue for runtime-indexed descriptor loading would give us a testable scope. The semaphore path is separate and already has tracing work in #6123. I would not force a signal or relax the hull checks without evidence of the intended synchronization/lowering semantics.

  8. amiraction0938 commented on Oct 17, 2026

    @amiraction0938
    Author
    Image Image

    Following up on the request to retest on current main without the local semaphore/tessellation workarounds.

    Setup

    • main at 51acb79ac (6 commits behind origin/main 3f171d03e; none of those touch Graphics/src/State.cpp)
    • Eboot relinked with this build's relinker (--windows --windows-fixed-base 0x400000000)
    • Local changes, nothing else (diff attached: onimusha_main_local_patches.diff):
    • No semaphore or tessellation workaround is applied.
    • Windows 11, RTX 5070 Ti, i5-14600K, 32 GB RAM

    What happened on main

    I did not reach the descriptor failure from the first report. I can't say whether it is fixed on main or just not reached, because each run stopped earlier. The blockers, in order:

    1. a color target's sample count differs from the rasterizer's (State.cpp, color-target loop)
      colorSamples=1 rasterSamples=2 colorsSoFar=0 hasDepth=0 a2c=0, register 0x2f8=0x108001, CB_COLOR_ATTRIB=0, format 122, extent 1456x1640.
    2. the depth target's sample count differs from the rasterizer's
      depthSamples=1 rasterSamples=2, format 130, same extent 1456x1640, same 0x2f8=0x108001.
    3. MSAA_ENABLE on a single-sample target whose sample is off the pixel center
      0x2fe = 0x40c in one draw and 0xc04 in another.
    4. VGT_SHADER_STAGES_EN=0x2030: unsupported geometry input or output assembly
      primitive=1, 0x29b=0x1, 0x2ce=5, 0x1ff=255, 0x25b=0x6633. The check requires 0x29b == 2.

    Experimental workarounds (not proposed fixes)

    To see what comes after (1)-(3), I made these local edits, all in State.cpp:

    • if the color target (or depth target) is single-sample and the rasterizer is multisampled, with no alpha-to-coverage, force samples = 1 for that draw and reset the sample-location fields
    • ignore only the "off the pixel center" rejection in that downgraded case

    I don't know if this models the hardware correctly. It only moves the failure forward.

    Result with those edits

    • The game reaches the main menu (artwork visible).
    • FPS is about 3-6, GPU about 35%, process RAM about 15 GB.
    • Audio is heard only when I press a button.
    • "New Game" then aborts with error (4) above.

    Questions

    1. A draw with rasterizer samples=2 and 0x2f8=0x108001 onto single-sample color and depth targets, with off-center sample locations: is that a pattern the project models somewhere (for example sample locations used without real MSAA)? What are the intended semantics?
    2. For (4): 0x29b=0x1 looks like line-strip output to me, but I have not confirmed that against the register spec. Is line-strip GS output planned or already tracked? The amplification = verticesPerPrimitive - 2 path suggests only triangle strips are handled today.
    3. Is there a preferred way to skip an unsupported draw instead of aborting, purely for debugging?

    Attachments: onimusha_main_local_patches.diff, report_main_findings.txt.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions