Skip to content

Ghost of Tsushima Director's Cut (PPSA02225, Windows): black frames despite continuing draw and presentation activity #5166

Open

Description

@sktharunbalaji13-cmd

Summary

I'm investigating a persistent black-screen problem in Ghost of Tsushima Director's Cut running under AnyPS5 on Windows.

The available traces show that drawing and presentation activity continues while sampled output frames are black. Shader-preparation failures are also reproducible across several diagnostic runs, but we have not established whether those failures are related to the black output.

I'm sharing the findings to ask whether anyone recognizes a related issue, knows of relevant upstream fixes, or can suggest a better way to trace shader selection through GPU submission.

Observed evidence

1. Black output with continued graphics activity

During an October 11 diagnostic session:

  • Five sampled 960×540 BMP frames were byte-identical black RGB images with an opaque alpha channel.
  • A separate draw trace recorded 6,791 completed VulkanDevice::Draw entries.
  • Another trace recorded 4,989 draw events and 2,493 presents, with double-buffer alternation and approximately one draw between consecutive presents.

These observations establish black output in the sampled frames, not that every frame throughout the run was black. Successful draw calls and presentation activity also do not establish that the resulting pixels were correct.

2. Repeated shader-preparation failures

Across four October 12 diagnostic runs, shader preparation reported 26, 27, 28 and 28 failure lines. Twenty failing shader identities were common across all four runs.

The relevant error includes:

GetImageResource dword 0 is not a valid runtime value

The error is logged in the shader-registration/preparation path at ShaderRegistry.cpp:632.

The associated resource-validation and SRT diagnostic records provide more information about the failures, but the current evidence does not show that the failing shader identities were selected for the draws responsible for the black output.

3. Drawing and presentation continued during another diagnostic run

The October 12 14:48 diagnostic window recorded:

  • 282 draw submissions
  • 1,060 submit batches
  • 141 flips/presents
  • 28 shader-preparation failure lines

That run did not include a pixel capture verifying its visible output, so it cannot be treated as another confirmed black-frame capture.

4. The critical correlation is still missing

The registration-time shader failure log records a shader code address but no draw or submission identifier. The existing [draw-programs] records are deduplicated configurations, not a complete per-draw execution history.

Consequently, we currently cannot establish whether any of the failing shader identities were selected by a particular program, associated with a recorded draw, or included in submitted GPU work.

The investigation has also identified large serialized RecompileRequest error payloads and an older logging implementation that writes long messages through fprintf. Upstream PR #3871 addresses consolidated log-line writes. Logging contention remains a hypothesis, not an established cause of Ghost's black output.

What we have not established

We have not established that:

  • The shader-preparation failures cause the black frames.
  • A shader associated with one of those failures was submitted for execution.
  • Logging contention caused a stall or incomplete scene initialization.
  • The October 12 diagnostic runs produced black pixels; those runs lack confirming pixel captures.

The root cause remains unresolved.

Questions for the community

  1. Does the GetImageResource dword 0 is not a valid runtime value failure resemble a known issue in shader resource tracking, runtime image-table resolution, or shader preparation?
  2. Are there existing fixes or related investigations that we should examine before implementing additional diagnostics?
  3. What is the most reliable way to correlate a registered shader identity with the selected graphics program, recorded draw, GPU submission and presented surface, including cached pipeline or draw-recipe paths?
  4. Could the large shader-recompilation error payloads warrant a targeted logging-contention test, and are there known limitations in the current logging path that we should account for?

Pointers to relevant PRs, issues, discussions, or technical approaches would be very helpful. I'm happy to continue collecting evidence and testing a narrowly scoped hypothesis rather than assume a cause prematurely.

Further information

The chronological investigation and existing evidence are documented in Discussion #4461.

Related upstream work includes PR #3871 — fix(libc): write each log line in one call. This is relevant prior art to examine, not a confirmed fix for this issue.

The exact game version and a clean, single-build reproduction have not yet been fully established. The diagnostic runs used different deployed driver builds, so comparisons between runs need to account for that.

Current status: Investigating. The black-screen cause remains unknown.

Activity

  1. MarcZX10 commented on Oct 13, 2026

    @MarcZX10
    Contributor

    I opened PR #5238 with opt-in per-draw shader stage/role and code-address tracing, including submission serial and queue context. This provides a direct address comparison with shader-preparation failures; it does not claim those failures caused the black output.

    Validation on Linux: the focused CTest passed (1/1), the production Draw.cpp object compiled, and the repository convention check passed. No GPU or game runtime tests were run.

  2. sktharunbalaji13-cmd commented on Oct 13, 2026

    @sktharunbalaji13-cmd
    ContributorAuthor

    Investigation progress: draw-to-submission correlation

    This is a factual progress update on local, uncommitted diagnostic work. No fix is claimed.

    New instrumentation:

    • Early draw-attempt correlation before Pm4::ResolveDraw.
    • Distinct attempt-failure, shader-request-failure, materialization-failure, submission-failure and draw-recorded events.
    • Deduplicated token sets attached to batches containing recorded draws.
    • Distinct pre-submit and post-success submission records.
    • Bounded, best-effort diagnostic logging.

    Validation:

    • Relevant production and test objects compiled.
    • Seven CaptureTrace utility tests passed.
    • No completed GPU, game-runtime or full AGC integration validation is claimed.

    Remaining blockers:

    • Windows PRX relink still fails with Permission denied.
    • The read-only write-open probe returned Win32 error 32 (ERROR_SHARING_VIOLATION), and the incompatible handle owner remains unidentified.
    • Runtime edge cases and complete draw-to-present correlation remain unverified.
    • Some correlation-log call sites still need non-throwing handling.

    Conclusion: the cause of the black screen remains unresolved.

  3. sktharunbalaji13-cmd commented on Oct 13, 2026

    @sktharunbalaji13-cmd
    ContributorAuthor

    Diagnostic correlation PR opened

    A focused PR has been opened to add opt-in correlation from draw attempts through shader selection/preparation failures, actual draw-command recording, and recorder batch submission.

    The implementation uses draw-correlation tokens and deduplicated token sets per recorder batch, distinguishing a submission attempt from successful queue submission. Best-effort diagnostic logging is designed not to mask original failures or prevent normal submission.

    Validation: The clean PR branch (feat/shader-draw-submission-correlation-5166-fresh, base origin/main dacd78525e537a779682032c0ace7f7af120af05, commit aef5a14cf1210d2c96941f5648801e3eb1dd700c) configured successfully, built the full instrumented libSceAgcDriver.prx, completed NID patching (OK), and passed 8/8 CaptureTrace tests. No GPU/game-runtime validation was added.

    Remaining limitations: Game-runtime validation has not established whether shader-preparation failures cause the black screen. The earlier original-build ERROR_SHARING_VIOLATION remains a development blocker rather than evidence of the game's root cause; this PR validation avoided it by using an isolated build directory.

    PR: #5431

  4. sktharunbalaji13-cmd commented on Oct 13, 2026

    @sktharunbalaji13-cmd
    ContributorAuthor

    End-of-day update ? isolated Windows build and first runtime correlation trace

    Today we moved the Ghost of Tsushima PPSA02225 investigation from source-level instrumentation work to a successful isolated Windows build and one controlled runtime experiment. This is a diagnostic milestone, not a confirmed black-screen fix or root-cause finding.

    1. Shader-failure-to-submission correlation instrumentation

    The correlation implementation is available in PR #5431.

    The instrumentation adds opt-in correlation across:

    • Draw-attempt creation and early failures, including Pm4::ResolveDraw.
    • Shader selection and shader-request/preparation failures.
    • Post-selection materialization and draw-setup failures.
    • Actual draw-command recording.
    • Deduplicated sets of nonzero correlation tokens associated with recorder batches.
    • Distinct submission-attempt and successful-submission-confirmation records.
    • Bounded, best-effort diagnostic logging intended to avoid masking original failures or preventing normal submission.
    • Recorder ownership and cleanup handling for command-buffer-end and queue-submit failure paths.

    The recorder metadata permits one batch to contain multiple draw tokens or no draw tokens, instead of incorrectly assigning a single token to every draw in a batch.

    2. Successful isolated Windows build

    The full Windows Release build of the instrumented libSceAgcDriver.prx completed successfully in an isolated build directory. NID patching also completed successfully.

    The patched artifact SHA-256 is:

    d41824bc58673697126da9465cda3bf0b21ca4b524f59dca08b5981b38e9da9f

    Size: 11,589,697 bytes.

    The four affected production objects compiled, and the focused CaptureTrace suite passed 8/8 tests.

    The original build directory had repeatedly failed when opening its PRX output for writing with Win32 error 32 (ERROR_SHARING_VIOLATION). We avoided overwriting or force-unlocking that original artifact by producing and validating a separate build. The original PRX and existing source/Git WIP were preserved.

    This validates compilation, full driver linking, NID patching, and focused utility tests. It does not by itself validate GPU rendering or prove the black-screen cause.

    3. Isolated runtime staging

    A separate runtime was staged at:

    C:\Games\ghost\anyps5-pps02225-probe\runtime-correlation-validation-20261014

    The instrumented PRX was installed only in that isolated runtime's libs directory. Its hash and size matched the build artifact.

    Robocopy copied 43,642 runtime files, excluding six known root diagnostic outputs: the existing capture-trace.log and frame_000.bmp through frame_004.bmp. The copied app0, download0, shader_cache, and _sd_mem directories were verified to be independent of the original runtime rather than junctions back to it.

    The original runtime executable, original driver, original trace, and previous evidence remained unchanged.

    4. First controlled instrumented runtime run

    The isolated executable was launched exactly once:

    C:\Games\ghost\anyps5-pps02225-probe\runtime-correlation-validation-20261014\ghost-upstream.exe

    The process ran with CaptureTrace, shader-correlation tracing, and GPU diagnostics enabled, and then exited naturally.

    The fresh evidence is preserved under:

    C:\Games\ghost\anyps5-pps02225-probe\startup-correlation-validation-20261014

    Key files include:

    • capture-trace.log
    • stdout.log
    • stderr.log
    • runtime-run-report.txt
    • missing-draw-recorded-analysis.txt
    • staging-manifest.txt

    The finalized CaptureTrace file size was 26,866,261 bytes.

    5. Final runtime event counts

    The completed trace contained:

    Event Count
    draw-attempt 797
    draw-selected 1,594
    draw-shader-request-failed 0
    draw-materialization-failed 0
    draw-submission-failed 0
    draw-recorded 600
    Generic draw verdict records 797
    submit batch attempts 2,807
    submit-confirmed 2,807
    flip 398
    present 202
    blit 202

    All generic draw-verdict records had verdict=0, which the current source enum maps to Drawn.

    6. Draw-to-submission correlation

    Of 600 tokens with draw-recorded entries, 599 appeared in both their matching submit and submit-confirmed records.

    One recorded token, token 797 in batch 2808, had no corresponding submission or confirmation record because the run ended before that batch was flushed. This is an incomplete final-batch correlation, not proof that the batch was successfully submitted.

    The trace contained 2,807 submission-attempt records and 2,807 confirmation records. At the completed-trace snapshot, every non-empty batch contained one token, while 2,208 batches had empty token sets.

    Queue submission confirmation means that vkQueueSubmit returned successfully and the required recorder bookkeeping was finalized. It does not establish GPU completion or visible pixel output.

    7. The 197 attempts without draw-recorded

    There were 197 attempt tokens without a corresponding draw-recorded record.

    Source inspection associated these attempts with a documented no-draw/recipe path in which prepareDrawInputs can mark inputs.nothing=true, and DrawWithRecipe can return from recipe handling without calling recordDrawCommands.

    The generic draw-verdict records still used Drawn=0, so these verdicts must not be confused with proof that a Vulkan draw command was actually recorded. Their detailed per-token context is preserved in missing-draw-recorded-analysis.txt.

    The repeated no-draw/recipe path associated with address 0x1504910000 remains a candidate for further investigation, not a confirmed explanation for the black output.

    8. Shader-preparation hypothesis: current result

    No draw-shader-request-failed, draw-materialization-failed, or draw-submission-failed event occurred in the completed runtime trace.

    Therefore, this run provides no captured token-level evidence attributing the black-screen condition to a logged shader-preparation failure.

    Earlier diagnostic runs did report shader-preparation failures, including the GetImageResource dword 0 is not a valid runtime value error. However, those earlier failures have not been shown to cause the black output.

    A contemporaneous screenshot showed a black rendering window during the runtime run. The logs show continued draw, submission, and presentation-path activity, but there is no validated pixel-level capture from this run.

    The trace reports presentation-path activity at 3840?2160, but flip, present, and blit records do not prove that the expected nonblack pixels appeared on screen.

    9. Preservation and safety

    • The original runtime driver, executable, and original capture-trace.log remained unchanged.
    • The new runtime used the instrumented PRX only in its isolated libs directory.
    • The runtime exited naturally; no forced process termination was used.
    • The protected C:\dev\AnyPS5 worktree was not touched.
    • The investigation source worktree was not modified by the runtime run.
    • No source changes, Git history mutations, or original PRX replacement occurred.

    Current conclusion

    We now have a successful isolated Windows production build, NID patching, eight passing focused CaptureTrace tests, and a first completed runtime trace connecting hundreds of recorded draw tokens to their confirmed submission batches.

    This is meaningful diagnostic progress, but the root cause of the black screen remains unresolved. The first runtime trace did not capture shader-preparation or later materialization/submission failures, and it does not contain validated pixel output.

    Related work:

  5. oneandonlydean commented on Oct 14, 2026

    @oneandonlydean
    Collaborator

    Which main commit and game version are these runs on? Please post one run on a clean main build (no local changes) with the log, so the black output can be tied to a single build.

  6. sktharunbalaji13-cmd commented on Oct 14, 2026

    @sktharunbalaji13-cmd
    ContributorAuthor

    Thanks for calling this out. The October 14 runtime run was performed using the instrumented driver from the shader-failure-to-submission correlation work in PR #5431, staged in an isolated runtime. It was not a clean main baseline run.

    Build identification

    • Repository: boykopovar/AnyPS5
    • PR base commit recorded in PR feat(agc): trace shader failures through draw submission batches #5431: dacd78525e537a779682032c0ace7f7af120af05
    • Current PR head reported by GitHub: 6b5c7fbda03e9b9469e786b34d98a87dde438383
    • Game: Ghost of Tsushima, title ID PPSA02225
    • Exact game version: Not recorded in the runtime report currently available.

    The PR base and head identify the PR's recorded lineage; they do not independently establish the exact commit used to produce the driver in the previous runtime run. I won't present either SHA as a verified clean-main runtime build.

    Next validation step

    The required baseline is a separate run built from a recorded main commit with a clean working tree and no local modifications. The run should record the exact source SHA, game version, driver artifact hash, launch configuration, and fresh runtime log.

    The existing trace showed draw recording, successful queue-submission confirmations, and presentation-path activity, but it did not establish the root cause of the black output. I have not yet verified a clean-main reproduction, so I won't claim that this validation is complete.

    I'll keep the baseline evidence separate from the instrumented PR results so the black-output observation can be tied to a specific, reproducible build.

  7. milescoviello commented on Oct 14, 2026

    @milescoviello
    Contributor

    A Linux data point on the shader-preparation lead: on current main the failures are gone and the output is still black.

    Ubuntu 24.04, RTX 4070 Ti, NVIDIA 595.99.02, Release, PPSA02225 relinked with --to-intel, main ed835aa plus some open PRs (#5511, #5523, #5536, #5660, #5910, #5937, #5975, #5982):

    • From an empty shader cache, 240 s: 711 shaders compiled and no could not be prepared line at all. With main d8888df on 10-13 the same title logged 28 shaders that failed to prepare.
    • Runs from the filled cache present at 59.95 FPS for 300 s and stay black: 8 frames dumped with APS5_DUMP_FRAMES=8 APS5_DUMP_FRAMES_EVERY=1000 are black at every sampled pixel, also with APS5_STORE_AT_FLIP=1, and pad input (cross, options, circle) does not change it.
    • The log has no rejected draw or unimplemented path. The only GPU warnings are eight storage image 0x1504910000 / 0x1506940000: guest memory changed while GPU results were pending; keeping the CPU's blocks, both 3840x2160 storage images.
    • A gdb trace of service calls returning error codes shows only expected offline results (sceUserServiceGetEvent, sceSystemServiceReceiveEvent no event, sceNetCtlGetInfo, DLC entitlement), plus sceAudioOut2PortGetState returning SCE_AUDIO_OUT2_ERROR_INVALID_HANDLE 3 times.

    AI-assisted: yes

  8. milescoviello commented on Oct 16, 2026

    @milescoviello
    Contributor

    More data from plain main 5d9dcc4 on Linux (RTX 4070 Ti, NVIDIA 595.99), PPSA02225, warm shader cache, scripted pad input. It still presents black frames at about 60 FPS. What the title itself is doing:

    • It draws almost nothing. APS5_PROFILE_DRAW=1 counts 1,200 draws per 10 s, 2 per presented frame, while compute dispatches keep running. It looks like an early state rather than a scene that renders black.
    • The main thread paces frames. In four gdb samples 30 s apart, it sits in the same guest loop. That loop calls sceVideoOutIsFlipPending until fewer than 2 flips are pending, busy-waiting with rdtsc between calls. Flips complete, so this is frame pacing, not a hang.
    • No other guest thread is stuck. Ten job-pool threads sleep on one condition variable. The rest wait on condition variables, event flags and equeues, and APS5_TRACE_WAITS shows every site waking normally (milliseconds per wait, no long waits). All the event flags are audio: 13 Wwise AkEvent flags and one AudioThread flag.
    • Audio runs. APS5_TRACE_AUDIOOUT2 shows one context pushing 93.8 grains per second, 512 samples at 48 kHz, with a mix peak of 0, i.e. silence.
    • File loading stops after the boot assets. APS5_TRACE_APR shows 75 reads in 150 s. They are all_shaders.xpps, lang_english_text.xpps, the init, common, pulse and single-player sound banks, three music_*.psarc and two bitmaps. Nothing else after that, so it never asks for front-end or world data.

    So on this build the title loops each frame without its state advancing, with nothing blocked. It may be waiting on a result rather than a lock, perhaps a value the GPU writes or a service it polls. I haven't found which one yet.

    AI-assisted: yes

  9. milescoviello commented on Oct 16, 2026

    @milescoviello
    Contributor

    Ruling out the system-service side, on Linux (RTX 4070 Ti, NVIDIA 595.99), main 82ddae7 + #6814 (current main segfaults at 1 s here, #6805), warm cache, pad input. Ghost still presents black frames.

    Under gdb with dprintf on 400 entry points (NpManager, NpTrophy2, NpUniversalDataSystem, NpSessionSignaling, NpEntitlementAccess, UserService, SystemService, SaveData, AppContent, Http/Ssl, PlayGo), 150 s:

    • Every frame (about 5,600 calls each): sceUserServiceGetEvent, sceNpCheckCallback, sceSystemServiceReceiveEvent, sceSystemServiceGetStatus. Our status is all false (no overlay, no background execution).
    • Once at boot:
      • PlayGo: Initialize, Open, GetChunkId, GetLocus ×2, Set/GetToDoList, GetProgress.
      • SaveData memory setup.
      • Trophy2 and UniversalDataSystem contexts.
      • sceNpRegisterStateCallbackA, sceNpRegisterNpReachabilityStateCallback, sceNpGetState.
      • sceNpEntitlementAccessGetAddcontEntitlementInfo for 13 add-on labels, twice. Each returns NOT_FOUND with no anyps5-entitlements.ini.
    • Nothing is called repeatedly that could be a wait loop.

    One guess I tested and ruled out: our sceNpCheckCallback never calls the registered state callback. A throwaway build that delivers one initial SIGNED_OUT callback from the first check changes nothing: the same 75 file reads (APS5_TRACE_APR) and the same draw count over 150 s.

    AI-assisted: yes

  10. sktharunbalaji13-cmd commented on Oct 17, 2026

    @sktharunbalaji13-cmd
    ContributorAuthor

    @milescoviello Thanks for running such a detailed investigation and testing the system-service callback hypothesis. The result is useful, especially the observation that the game keeps presenting frames but appears not to advance beyond its initial state.

    My investigation on Windows has reached a similar unresolved point from the rendering side: the sampled color targets remain black, and although the shader-capacity preparation failure has been addressed, I have not established that those failures caused the black output.

    Your finding that asset loading stops after the initial boot resources seems particularly important. It raises the possibility that the title is waiting for a particular game-state transition or result rather than simply failing to render an otherwise active scene.

    Could you share which specific guest-state transition or condition you think should trigger front-end/world asset loading, and how best to trace why it is not reached? In particular, I would be interested in a narrowly scoped trace of the relevant guest branch, polled value, or result-producing operation, rather than another broad system-service trace.

    I also want to avoid conflating the Linux and Windows runs because they use different revisions and environments. For now, I am keeping the shader-preparation hypothesis separate from the question of why the game state appears not to advance.

    Thanks again for sharing the evidence. It gives us a more focused direction for the next investigation.

  11. sktharunbalaji13-cmd commented on Oct 17, 2026

    @sktharunbalaji13-cmd
    ContributorAuthor

    Root Cause Analysis & Technical Diagnostic Update (Ghost of Tsushima PPSA02225)

    Over the past week, we conducted an in-depth execution probe and disassembly investigation into the black-screen rendering issue on Windows. Below is the detailed breakdown of the exact mechanisms causing the black screen and preventing 3D scene rendering.


    1. Primary Cause: Upstream Engine Viewport Gating (ghost-upstream.exe)

    Through disassembly of ghost-upstream.exe around 0x1408e4cc0, we identified that the engine manages 7 distinct rendering passes/viewports located at base address 0x1416e52e0 (stride 0x32c40).

    Within the pass execution routine at 0x14068a470:

    14068a48f: cmpq $0x0, (%rdi)       ; Check if viewport structure is active / non-null
    14068a493: je   0x14068a5db       ; If NULL -> EARLY EXIT (skips all draw submissions)
    14068a499: cmpl $0x0, 0xf0(%rdi)   ; Check draw/layer count
    14068a4a3: je   0x14068a591       ; If 0 -> clear target sizes and return
    

    At engine startup and during steady state:

    • The pointer (%rdi) is NULL (0x0) across all 7 viewport slots.
    • Consequently, the main 3D scene/geometry dispatch function at 0x140689e00 is completely skipped every single frame.
    • The engine's command stream submits only two draw operations per frame:
      1. Draw 53 (Letterbox / Clear pass): A fullscreen triangle using fragment shader 0x20001F3A00, reading constant FP32 (0.0, 0.0, 0.0, 1.0) from guest VA 0x110A64E600, writing opaque black into intermediate render target 0x1500010000.
      2. Draw 55 (Composition / Present pass): A fullscreen blit sampling 0x1500010000 into the active swapchain backbuffer (0x1504910000 / 0x1506940000).

    2. Secondary Emulator Defect: Host-Import Overwrite in Texture.cpp

    In core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp (lines 1778–1808 and 1932–1968):

    • Direct host-imported memory ranges bypass the Windows VEH write-watch tracking. In scanBlocks, GuestMemory::ChangedBlocks() returns false and populates stampedBlocks with BlockWritten (1).
    • Because layerOriginal is invalidated whenever the GPU renders into the texture, compareUntracked() evaluates to false.
    • This causes changed[layer] to be flagged as true on every frame, even though cpuWrote == false.
    • The runtime logs:
      [gpu] storage image 0x1504910000: guest memory changed while GPU results were pending; keeping the CPU's blocks
      
    • Refresh() initiates an asynchronous writeBackLayers() and immediately follows with upload(&changed), which reads uninitialized / zeroed CPU memory and uploads it directly over the Vulkan image, clobbering the GPU's rendered output.

    3. Engine Thread Stall & System Service Polling

    • Following the initial save-data initialization ([SAVEDATA:native] setupMemory2 ... existed=0) and shadow seed generation, the engine spawns ~50 worker threads (JobWorker1-5, FaWorkerIo1, MovieDecoder, etc.).
    • All worker threads park indefinitely inside libkernel.prx!scePthreadCondWait -> waitUntil -> TimedWait::Condition::waitSignal.
    • The main thread enters an infinite polling loop on sceSystemServiceReceiveEvent, which continuously returns SYSTEM_SERVICE_ERROR_NO_EVENT (0x80B80001).
    • The engine appears to be waiting on a specific system event (or condvar signal) to activate its main game state machine and populate the viewport descriptors.

    4. Display Window Focus

    In core/libs/prx/libSceVideoOut/src/DisplayWindow.cpp:63:

    • The SDL window is initialized with SDL_WINDOW_SHOWN, but lacks an explicit SDL_RaiseWindow(window) or Win32 SetForegroundWindow(). On certain Windows compositors and multi-monitor setups, the created window fails to take foreground focus or remains minimized/hidden behind other windows unless manually focused.

    Recommended Remediation Steps

    1. Texture.cpp: Guard upload(&changed) when memory is untracked host import and cpuWrote == false to prevent overwriting GPU-rendered backbuffers with zeroed CPU memory.
    2. System Service / Event Delivery: Trace which event ID ghost-upstream.exe expects from sceSystemServiceReceiveEvent (e.g. Resume, Focus, User Login, or Display Status).
    3. Condition Variable Wakeup: Verify if any condition variable or futex wake signal (scePthreadCondSignal / scePthreadCondBroadcast) is being lost or dropped between workers and the main thread.
    4. Display Window: Add SDL_RaiseWindow in DisplayWindow.cpp to ensure the render window is brought to the foreground properly.
  12. MarcZX10 commented on Oct 17, 2026

    @MarcZX10
    Contributor

    Thanks for adding the disassembly and draw details. For the run described here, the reported null viewport entries and two submitted draws (a fullscreen black clear followed by composition) explain the captured black output: no 3D scene draw is reported in that run. That does not establish why the viewports remain null, and it does not yet connect the earlier shader-preparation failures or the Texture.cpp upload path to the image.

    Could you pin this diagnostic to the exact AnyPS5 commit/build and include the trace window for those two draws? The issue has earlier captures with thousands of completed draws, so keeping the separate runs and build identities explicit is important. For the Texture.cpp hypothesis, evidence that the relevant presentation target was actually uploaded from guest memory after its GPU draw would distinguish it from the title never submitting the scene in this run. I would hold off on a code change until that path is shown.

    AI-assisted: yes

  13. sktharunbalaji13-cmd commented on Oct 17, 2026

    @sktharunbalaji13-cmd
    ContributorAuthor

    @MarcZX100 Thanks for the precise follow-up — here is the exact commit/build identity, the draw trace window, and the evidence clarifying the Texture.cpp hypothesis versus the upstream submission state:


    1. Exact Commit, Build Identity & Runtime Environment

    • Production Candidate Baseline: Commit f40b6930ff156834b786730771518b3bd7fe8648 (branch fix/ghost-tsushmina-black-screen) + the 6 approved modified PRX files (Texture.cpp, ShaderInfo.hpp, BindingAllocator.cpp, ResourceTracker.cpp, SrtRuntimeValidator.cpp, RuntimeAbi.hpp).
    • Production PRX Driver Hash: SHA-256 803D5652517984976BB5D51A450266173B2CC92E481DEB72D6A49982D9A2C7A9.
    • Diagnostic Worktree: C:\dev\AnyPS5-fragment-diag-20261017 at commit f40b6930ff15 (TraceRevision = f40b6930ff15, DIRTY 1 with diagnostic logging only, no functional logic alterations).
    • Guest Executable: ghost-upstream.exe, SHA-256 A13603833F7450795BDC0D718B704E188574A8D658313CCF5061C6C3C76BBCDE (65,096,704 bytes, ImageBase 0x140000000).

    2. Trace Window for the Two Draws (First Correlated Frame Timeline)

    Captured from fragdiag-np-probe-evidence-20261017\runtime-outputs\capture-trace.log (events 519–790):

    Event Timestamp (µs) Operation & Log Record Details
    519 4769777 packet submission=53 offset=28 header=c0012d00 PM4 DRAW_INDEX_AUTO (Fill draw packet)
    521 5205209 diag-fragment index=0 value=00000000,00000000,00000000,3f800000 target=1500010000 Target 0x1500010000 (intermediate buffer)
    522 5550857 diag-input index=0 srt_words=(0,0,0,1.0) push_word2=e4e4e4e4 push_sel=e4 Shader 0x20001F3A00 reads guest constant (0, 0, 0, 1.0) from 0x110A64E600
    523 5695027 draw submission=53 offset=28 target=1500010000 mask=f verdict=0 Fill draw completes (indices=3 instances=1 colorTarget=1)
    556 5824711 packet submission=55 offset=158 header=c0012d00 PM4 DRAW_INDEX_AUTO (Composition draw packet)
    558 5865208 sampled-lookup address=1500010000 width=3840 height=2160 dcc=1501ff0000 Compose pass samples intermediate target 0x1500010000
    561 7407584 draw submission=55 offset=158 target=1504910000 mask=f verdict=0 Compose draw into double-buffer target 0 (0x1504910000)
    656 9195246 flip frame=1 submission=55 offset=282 batch=11 unsignaled=0 Swapchain flip issued
    788 9655304 present address=1504910000 width=3840 height=2160 Present target 0x1504910000
    790 9656100 blit batch=15 slot=0 image=0 direct=0 -> swapchain 1152x648 Downscaled to swapchain 1152×648 (VulkanDevice:1717)

    Draw Count Reconciliation:
    Regarding the "thousands of completed draws" mentioned in earlier captures:

    • Over a full 300-second (5-minute) continuous observation window, exactly 1,051 total draws were submitted and completed by the emulator.
    • Every single draw across the entire 5 minutes was a full-screen triangle (indices=3, instances=1, shaders=2, colorTarget=1).
    • The 1,051 draws simply represent the two-draw loop (Draw 53 clear + Draw 55 compose) running across ~525 presented frames. There are zero 3D scene mesh draws or UI geometry draws submitted anywhere in the log.

    3. Clarification on the Texture.cpp Upload Hypothesis

    You are completely right to pump the brakes on modifying Texture.cpp:

    • While scanBlocks logged [gpu] storage image 0x1504910000: guest memory changed while GPU results were pending; keeping the CPU's blocks during host-import checks, our reconstruct probe (reconstruct-0.txt) demonstrated that the presented frame was already opaque black directly out of the GPU fragment shader stage.
    • Shader 0x20001F3A00 consumes the guest-supplied constant (0, 0, 0, 1.0) at 0x110A64E600 (which is already populated prior to import in the guest's 64 MiB AGC buffer at 0x1108000000..0x110C000000), and writes pure black to 0x1500010000.
    • The presentation path is a faithful render and blit of what the guest explicitly requested. The black screen is not caused by an emulator clobber/upload of a valid scene.
    • We agree 100%: no code changes should be made to Texture.cpp for this issue.

    4. Why the Viewports Remain Null (Upstream State)

    The core reason the viewports at 0x1416e52e0 remain null is upstream in the engine's lifecycle:

    1. Immediately after [SAVEDATA:native] setupMemory2 ... existed=0 and shadow seed setup, the title spawns its worker pool (JobWorker1-5, FaWorkerIo1, MovieDecoder, etc.).
    2. All worker threads park in libkernel.prx!scePthreadCondWait (ntdll.dll alertable wait).
    3. The main thread enters a steady poll of sceSystemServiceReceiveEvent, receiving SYSTEM_SERVICE_ERROR_NO_EVENT (0x80B80001).
    4. The engine's state machine is idling in a pre-scene boot state, waiting on an external event / synchronization condition before advancing to build and populate its 3D scene viewports.
  14. MarcZX10 commented on Oct 17, 2026

    @MarcZX10
    Contributor

    Thanks for the exact build and draw timeline. This rules out the proposed Texture.cpp explanation for this run: both the intermediate render and composition receive black content from the guest, with no scene draws in the capture. I checked current main as a separate lead: sceSystemServiceReceiveEvent in core/libs/prx/libSceSystemService/Export.cpp validates the pointer and then unconditionally returns SYSTEM_SERVICE_ERROR_NO_EVENT. That could explain a title waiting on this API, but your evidence does not yet show that the polled event is required to build the scene or that returning an event would be correct. Could you capture the caller/RVA and the event structure before the call, and (if the guest code indicates it) the branch taken after NO_EVENT? That would distinguish a normal empty poll from a missing system-service event the engine needs.

  15. sktharunbalaji13-cmd commented on Oct 17, 2026

    @sktharunbalaji13-cmd
    ContributorAuthor

    @MarcZX100 We just directly tested and instrumented this in our latest 5-minute runtime observation run (Section 23b report), and the result definitively clarifies this path:

    1. sceSystemServiceReceiveEvent is Not Polled by the Title

    To test whether sceSystemServiceReceiveEvent was gating the engine, we instrumented core/libs/prx/libSceSystemService/Export.cpp:

    • Added diagnostic logging before pointer validation / return:
      if (SysEventTrace()) std::fprintf(stderr, "[sysevent] ReceiveEvent -> ...\n");
    • Implemented an env-gated synthetic lifecycle injection (APS5_SYSEVENT_ONCE=<type>) to deliver a simulated active/resume lifecycle event on the first poll.
    • Built a coherent runtime (fragdiag-s23-runtime-20261017) and ran a full 5-minute continuous observation with APS5_SYSEVENT_ONCE=2.

    Result:

    • Total [sysevent] invocations logged across the entire 5-minute run (6,302 completed draws): 0.
    • The title never called sceSystemServiceReceiveEvent a single time.
    • Cross-referencing the verified PLT import mapping confirmed that while ghost-upstream.exe imports two entries from #o (libSceSystemService, PLT indices 149–150), neither is invoked during the steady-state black screen loop.

    2. What the Main Thread Is Actually Polling

    In our sampled hang dumps (Section 20), the guest PC on the main thread was resolved to guest RVA 0xbc7c12 in .elf0:

    bc7bb0: movslq 0xcd766d(%rip),%rax   # 0x189f224 (index)
    bc7bb7: mov    0x263040a(%rip),%rcx  # 0x31f7fc8 (counter table base)
    bc7bc2: mov    (%rcx,%rax,1),%rax
    bc7bc6: cmp    %eax,%r13d            # compare with bound 0x189f220
    bc7bc9: jbe    0xbc7c57              # counter advanced -> proceed
    ...
    bc7c10: rdtsc                        # TSC delay spin
    bc7c12: mov    %eax,%eax             # <-- SAMPLED PC
    bc7c1b: cmp    %rcx,%rdx ; jl 0xbc7c10
    bc7c20: jmp    0xbc7bb0              # re-poll counter table
    

    This is a standard frame/present pacing loop that queries flip status and syncs against a flip counter. It is not an idle hang or exception handler.

    3. Current Focus

    Because sceSystemServiceReceiveEvent is completely inactive, the missing trigger lies elsewhere:

    1. Save-Data / Dialog Completion: Immediately before entering the draw loop, the engine completes [SAVEDATA:native] setupMemory2 ... existed=0. We are checking whether the title awaits an asynchronous callback or dialog event (e.g., from sceSaveDataDialog or sceUserServiceGetEvent).
    2. Viewport Array Initialization: The 7 rendering viewports at 0x1416e52e0 remain null. We are statically tracing the writer/allocator function for 0x1416e52e0 in ghost-upstream.exe to isolate the exact guest branch that guards viewport population.
  16. 1 remaining item

  17. sktharunbalaji13-cmd commented on Oct 17, 2026

    @sktharunbalaji13-cmd
    ContributorAuthor

    @MarcZX100 Will do. We are currently performing the static cross-reference and disassembly trace on ghost-upstream.exe targeting both:

    1. The Viewport Array Writer (0x1416e52e0): Identifying the specific function and controlling branch that allocates/populates the 7 viewport descriptor structs checked at 0x14068a48f.
    2. The Counter Table Base Producer (0x31f7fc8): Tracing the non-NULL store to 0x31f7fc8 (which is zero-initialized at 0x5c7b77) and its connection to the 0xbc2e60 initializer / libSceSaveData callback path.

    As soon as the writer call-sites, RVAs, and controlling branch conditions are mapped out, we will share the full trace here.

  18. sktharunbalaji13-cmd commented on Oct 17, 2026

    @sktharunbalaji13-cmd
    ContributorAuthor

    @MarcZX100 Here is the complete disassembly, RVA mapping, and call-site trace resolving both the Viewport Array Writer and the Command-Buffer Submission Boundary:


    1. The Viewport Array Writer (0x1416e52e0)

    The 7 rendering viewports checked at 0x14068a48f live at base address 0x1416e52e0 with stride 0x32c40:

    • Slot 0: 0x1416e52e0
    • Slot 1: 0x141717f20
    • Slot 2: 0x14174ab60
    • Slot 3: 0x14177d7a0
    • Slot 4: 0x1417b03e0
    • Slot 5: 0x1417e3020
    • Slot 6: 0x141815c60

    The Writer Function: 0x140683a70 (RVA 0x683a70 in .elf0)

    140683a70: push   rbp
    140683a71: mov    rbp,rsp
    ...
    140683a8b: movsxd rax, DWORD PTR [rdi+0x20]   ; Read viewport/camera index
    140683a8f: cmp    rax, 0x2
    140683a93: jne    0x140683acb
    140683a95: mov    QWORD PTR [0x1416e52e8], rdi ; Secondary slot assignment
    ...
    140683acb: imul   rbx, rax, 0x32c40            ; index * stride (0x32c40)
    140683ad2: lea    r15, [rip+0x1061807]         ; 0x1416e52e0 (viewport array base)
    ...
    140683aeb: mov    QWORD PTR [rbx+r15*1], rdi   ; <--- STORES POINTER INTO [0x1416e52e0 + index*0x32c40]
    

    Component System Registration

    0x140683a70 is not called via direct call rel32; it is registered as the component constructor/setter in the engine's Component Dispatch Table at file offset 0x15b1678 (VMA 0x1415b4e78 in .elf4):

    • Component ID: 0x2bc (700, Camera / Viewport Component).
    • Setter: 0x140683a70 (populates [0x1416e52e0 + index*0x32c40]).
    • Clearer: 0x140683b90 (at VMA 0x1415b4e98, clears [0x1416e52e0 + index*0x32c40] back to 0x0).

    When the game instantiates a Camera/Viewport entity in the scene, component 0x2bc is registered, writing the non-null pointer into [0x1416e52e0]. At startup, the game has not yet instantiated any scene camera entities because the title has not advanced past the initial save-data / boot state into the active world.


    2. The Command-Buffer Submission Boundary & Return Address

    In our driver submission trace (s24build), the submission of the black fill buffer (0x110a646600, size 36 dwords, containing Draw 53 at cursor 28) produced:

    [gpu] submit serial=40 queue=0x0 guest=0x110a646600 dwords=36 flags=0x0 caller=0x7ff67ccbb7f4 host_ret=0x7ffcae1f52ee entry=sceAgcDriverSubmitDcb
    

    The Submission Call-Site: 0x14074b7ee (RVA 0x74b7ee in .elf0)

    With module load base 0x7ff67c570000, runtime address 0x7ff67ccbb7f4 maps exactly to .elf0 RVA 0x74b7f4:

    14074b7d2: mov    rdi, r14
    14074b7d5: shr    eax, 0x2                       ; dwords = size_bytes >> 2
    14074b7d8: mov    QWORD PTR [rsp+0x1980], rcx     ; Packet.addr
    14074b7e0: mov    DWORD PTR [rsp+0x1988], eax     ; Packet.dw_num
    14074b7e7: mov    BYTE PTR [rsp+0x198c], 0x0      ; Packet.flags = 0
    14074b7ee: call   0x141138f50                     ; PLT stub for sceAgcDriverSubmitDcb
    14074b7f4: jmp    0x14074b7b0                     ; <--- RETURN ADDRESS (caller=0x7ff67ccbb7f4)
    

    This loop at 0x14074b750–0x14074b7f4 iterates over all built DCB command buffers for the current frame. Because no viewports are registered, the only buffers present in the submission list are the clear pass DCB (0x110a646600) and the compose pass DCB.


    3. The Counter Table (0x1431f7fc8) & Producer

    • The global pointer 0x1431f7fc8 points to a 32-byte strided table of synchronization counters (shl rax, 0x5).
    • The submission function at 0x14074b1e6 and the frame pacer at 0xbc7bb0 poll [0x1431f7fc8 + slot*32] until counter >= bound.
    • The Producers:
      Stores into [0x1431f7fc8 + slot*32] are executed at 0x1403e2e1d and 0x1405c8012:
      1405c8004: mov    rdx, QWORD PTR [rip+0x2c2ffbd]   # 0x1431f7fc8
      1405c800b: mov    rsi, rax                         ; slot index
      1405c800e: shl    rsi, 0x5                         ; slot * 32
      1405c8012: mov    QWORD PTR [rdx+rsi*1], rcx       ; Write counter value!
      
      These updates are triggered from task completion callbacks at 0x14053f368 and 0x140f315f8, which interface with the libSceSaveData completion chain.

    Conclusion & Next Boundary

    The pipeline from command buffer generation $\to$ DCB submission $\to$ GPU execution $\to$ flip is functioning without defect. The fact that the viewports at 0x1416e52e0 remain null is because Camera Component 0x2bc has not yet been registered by the game world. The title's state machine is holding back camera instantiation pending resolution of the initial save-data / start-up task sequence.

  19. sktharunbalaji13-cmd commented on Oct 18, 2026

    @sktharunbalaji13-cmd
    ContributorAuthor

    End-of-day update: hardware/software context, and the logs behind today's findings

    Two days of bounded experiments on this title. Status unchanged: black frames persist, no root cause, no fix proposed. What improved is that several earlier observations can now be classified, and the logs below are the proof for each. A fuller narrative version of this is in the discussion #4461.

    All excerpts are trimmed copies; full logs are retained locally per experiment.


    Build/revision context (results do not transfer across these)

    Artifact Revision / hash
    Official control runtime AnyPS5 v0.1.2, app.exe 5a9fd879…129282, 126 .prx
    Fork trial runtime fork source 1d0537772, app.exe 7bd003eb…402852, libSceAgcDriver.prx 6dde73c8…ace66e, 127 .prx
    Patch target source current HEAD a5cdfa9e4 (not a descendant of 1d0537772; 31 audio files differ)
    Guest input decrypted eboot.bin.esbak ab0e3fbc…951d52, unmodified

    Host: Windows 11 26200, 15.29 GiB RAM, RTX 4060 Laptop + AMD iGPU, MinGW-w64 (WinLibs) + Ninja.


    1. The fork's default bindless image heaps regress a title that works on v0.1.2

    Control title (Dreaming Sarah, PPSA02929), same staging and same window-capture code in every cell; only the executable/library set and one variable differ:

    Cell exe libs knob captured game window
    B official official — mean 46/27/42, 272–322 unique colours → gameplay
    A fork-relinked official — mean 46/28/42, 369–383 unique → gameplay
    C fork-relinked fork — mean 15/15/15, 20–21 unique → black
    D fork-relinked fork APS5_SYNC_PREPARE_AT_USE=1 black
    F fork-relinked fork APS5_NO_BINDLESS_HEAPS=1 42→46 mean, 134→257 unique → gameplay restored
    E fork-relinked fork + official libSceAgcDriver.prx — aborts at load

    Cell E proves the fork libraries are an interdependent set (single-PRX cross-revision swap is not valid):

    Failed to load module: …\libs\libSceAgc.prx
    GetLastError: 127 - The specified procedure could not be found.
    

    And the fork's bindless path is active by default in both titles:

    [bindless] image heaps enabled: 16384 slots per kind, set 2
    

    For Ghost, the same knob changes nothing (still black, identical reads) — so Ghost's failure is not the bindless path, but the fork cannot be treated as a drop-in runtime either.


    2. The 25 shader-preparation failures are not on any draw path (480 s fork run, bindless off)

    [Shaders/ShaderRegistry.cpp:1132 operator()] Shader 0x2000165d00 could not be prepared after registration:
    shader 0x2000165d00 stage 1: ShaderRecompiler::Recompile: GetImageResource dword 0 is not a valid runtime value;
    chain: LoadAddressU32@pc=0x00000954(GetAddressResource(GetUserData(Void), GetUserData(Void)),
    IAdd32(IMul32(Phi(Immediate, …)), …))
    
    [Shaders/ShaderRegistry.cpp:1132 operator()] Shader 0x20001a3f00 could not be prepared after registration:
    shader 0x20001a3f00 stage 0: ShaderRecompiler::Recompile: GetImageResource dword 0 is not a valid runtime value
    (bindless table check 3); chain: ReadConstBuffer@pc=0x000003e4(GetBufferResource(ReadConst(GetSrtResource, …)), …)
    

    Counts and the decisive negative:

    • 25 records / 19 distinct states (4 Vertex, 15 Compute); identical address/retry sets in two independent runs.
    • [shader-list] recorded 24 states reached by a draw or dispatch — disjoint from the failing 19.
    • All 25 records occur before the first sound-bank read (reads 8 and 17 of 75).
    • The official v0.1.2 runtime logs the same defect, 28 times: could not be prepared after registration: … GetImageResource dword 0 is not a valid runtime value.

    Conclusion recorded: a real recompiler defect, but on these runs it never reaches scene draws, so a fix cannot be shown to produce pixels.


    3. The host-import refusal counter is one range retried ~57,000 times

    [gpu] host import budget: 6254 MiB live of 6261 MiB (APS5_HOST_IMPORT_MIB);
           1 imports (2438 MiB) refused so far, last 0x1500000000+0x98600000
    [gpu] host import budget: 6254 MiB live of 6261 MiB (APS5_HOST_IMPORT_MIB);
       57482 imports (140102236 MiB) refused so far, last 0x1500000000+0x98600000
    
    • 0x98600000 = 2,438.0 MiB; 140,102,236 ÷ 57,482 = 2,437.3 MiB — the same range every sample.
    • The counter is process-lifetime and increments per attempt; the budget branch returns before any failure caching, so later uses retry.
    • No import-creation failure was logged in any run: host import of 0x…+0x… failed at …; falling back to copies = 0 occurrences.
    • Fallbacks exist on the paths inspected (CPU fills, CPU-resolved indirect dispatch, CPU label stores). The label fallback counter observed was zero: fallbacks: idle 37, completions 0, not imported 0, undecodable 0.
    • Honest limit: no diagnostic identifies the caller of that 2.4 GiB range (the API has no site parameter; the refusal returns before every trace).

    So: 57k refusals ≠ 57k missing resources.


    4. One concrete skipped draw (guest format 56 sampled through an sRGB-forcing sampler)

    [gpu] skipped draw: AGC graphics: a texture of guest format 56, which is not sRGB,
    is sampled through a sampler that forces sRGB decoding, which is not implemented
    

    The draw is not submitted. Caveats: reportSkip prints one line per distinct reason, so this does not count occurrences; APS5_TRACE_SKIPS=1 (existing) would. Recurrence is currently unmeasured.


    5. Audio: the earlier "audio never runs" negative was run-specific

    A hang dump of the official app.exe contains a live SDL playback thread:

    thread 832 'SDLAudioP2': rip 0x00007ffd75f21dd4 ntdll.dll+0x161dd4
    

    In the audited source the only path creating that thread is sceAudioOutOpen → openDevice → SDL_OpenAudioDevice, and SDL names it SDLAudioP<id> — so a port open succeeded in that run, while two earlier runs with APS5_TRACE_AUDIOOUT=1 APS5_TRACE_AJM=1 logged none. Both facts stand; the negative was configuration-specific.

    The 480 s audio probe could not answer the remaining question — it was safety-stopped during startup:

    t=1 s   available 6.14 GiB  committed 17.88 GiB
    t=6 s   available 5.99 GiB  committed 21.10 GiB
    t=11 s  available 5.89 GiB  committed 23.58 GiB
    t=16 s  available 2.94 GiB  committed 26.01 GiB   -> abort at t=19 s
    

    Before the stop it had reached guest entry, a dozen guest threads, startup file opens and a 1152×648 swapchain — not the content phase. Its zero audio/APR/AJM/shader counters are therefore inconclusive, not a negative.


    6. Unknown sysmodule 0x127

    [libSceSysmodule/Export.cpp:141 sceSysmoduleLoadModule] unknown id: 295
    

    The guest passes the constant 0x127, checks the result and takes its success path. sceSysmoduleLoadModule only increments a counter and returns 0 for unknown ids; the API that maps a host module (sceSysmoduleGetModuleHandleInternal) throws, and no throw occurred. The id is absent from every table examined (285 entries, including HEAD), and the same message occurs in the PAC-MAN report (#3443 item 4, "I do not know what 0x127 is"), where the title boots, renders and plays audio. #2541 maps only 0x110.

    Recorded as a table gap with no demonstrated effect; no mapping was invented.


    7. Also worth reporting: the fork build needed MinGW on the Windows PATH

    The default build failed only on the relinker's resource step:

    [2240/3175] Building RC object core/relinker/CMakeFiles/relinker.dir/relinker.rc.obj
    FAILED: windres -O coff … relinker.rc
    'gcc' is not recognized as an internal or external command
    windres: preprocessing failed.
    

    windres spawns its preprocessor by bare name, so C:\winlibs\mingw64\bin must be on the Windows PATH (an MSYS-only export PATH is not enough). After that, 921 targets and all 127 PRX build cleanly.


    8. Prepared, statically validated, not runtime-tested: opt-in audio lifecycle logging

    New gate only, off by default: APS5_TRACE_AUDIO_LIFECYCLE. Existing gates untouched (0 occurrences in added/removed lines).

    File +/−
    libSceAudioOut/src/AudioOut.cpp +228 / −4
    libkernel/Apr/src/Apr.cpp +88 / −0
    libSceAjm.native/src/Ajm.cpp +96 / −0

    Adds: init/open/close events with result+duration; per-call counters only (submissions vs null-drain, per-port counts, total/max duration) with 10 s summaries; drain/pacing accumulation; a rate-limited drain_cap event recording bytes discarded before the existing 200 ms clear; APR _waitSubmitted timing with the queue's own submitted/completed/id, individual records only ≥ 1 ms (≤ 8 per 10 s); AJM per-batch id/jobs/duration + per-kind totals, honouring inline execution; tid from std::hash<std::thread::id>.

    Shape: [audiolc] t=<sec> tid=<id> sub=<audioout|apr|ajm> ev=<event> <k=v…>.

    Validated: git apply --check (also --whitespace=error-all), disposable-copy apply (identical, CRLF preserved), and -fsyntax-only -std=gnu++20 -Wformat -Werror=format for all three TUs with each target's real CMake defines/includes.

    Not compiled to objects, linked, installed or run. sha256 b8c42d3f5d76c904b61b980aa409e6fec1f950194d6fc6594d322ee0e9888719. It does not address the black frames.


    9. What is still not established

    • That any failing shader state reaches a scene draw (they do not, in the observation).
    • That refused imports removed resources (fallbacks exist; no creation failure logged).
    • That the format-56 skip is the scene blocker (recurrence unmeasured).
    • That audio output/AJM is inactive in the current fork runtime (probe never reached the content phase).
    • That 0x127, the import budget or audio are causal for the black frames.

    10. Next

    1. Workspace/revision inventory before any further runs (multiple isolated trees and staged runtimes).
    2. Rendering/progression tracing — the open question is the transition at which the title stops advancing (guest milestones, completion callbacks, sync counters, viewport population, scene draw selection, recorded→submitted→presented), reviewed against existing draw/submission correlation work first.
    3. Any launch gated on a realistic memory assessment for this configuration (~6.1 GiB of pinned imports alone); the 6 GiB pre-launch gate has already been shown insufficient once.
    4. No speculative changes to the import budget, module table, bindless heaps or shader recompiler.

    Success criterion unchanged: recognizable in-game visuals persisting for five continuous minutes. Full logs available on request.

  20. milescoviello commented on Oct 18, 2026

    @milescoviello
    Contributor

    @sktharunbalaji13-cmd sorry for the slow answer. I don't know which transition it is. What I have narrows it down without naming it:

    • Nothing is blocked. The main thread's frame loop paces on sceVideoOutIsFlipPending, and all ten job-pool threads sleep on the same condition variable. So no job is ever queued that would load front-end or world data. The decision not to load is made on the main thread, inside that loop.
    • None of the services it polls every frame returns anything unusual, and a forced initial NP state callback changed nothing.

    How I'd trace it, narrowly:

    1. Take the frame function from the gdb samples (the caller of the sceVideoOutIsFlipPending loop) and disassemble it to find the branch that decides whether to post the next load job. In a boot state machine that is usually a compare on a small state or phase integer near the top of the frame function.
    2. Set a hardware watchpoint (watch -l) on that state variable. If it never changes, step back to the compare that guards the transition and read what it tests. That is often a flag written by another subsystem, or a value the GPU writes, such as a label a compute dispatch sets.
    3. If the guard is a value in GPU-written memory, compare it after the dispatches that should set it. APS5_STORE_AT_FLIP=1 didn't change the frames, but it doesn't cover labels a compute job writes and the CPU polls.

    I can do step 1 on Linux if you'd like. Say if you already have the frame function's address.

    AI-assisted: yes (Claude Code)

  21. MarcZX10 commented on Oct 18, 2026

    @MarcZX10
    Contributor

    Please do step 1 on Linux. I don't have the frame-function address from your samples. A useful result would be the function VA, the disassembly around the branch that posts the next load job, and the state/flag value tested there at the stalled frame. Please include the exact build and input timeline for the capture so we can compare it with the black-output run. That should identify the next trace point without assuming the GPU or shader failures are causal.

  22. milescoviello commented on Oct 18, 2026

    @milescoviello
    Contributor

    @MarcZX10 will do. I'll use main 13e48ed (before #6112, so #7636's start-up abort can't interfere) with no other PRs, and the scripted pad input run_title.sh uses. I'll record the exact press timeline. At the stalled frame I'll sample the main thread's guest stack under gdb, dump the frame function and its callees, and identify the branch that would post the next load job and the value it tests. The function VAs will be relative to the converted eboot's base, so they compare across runs. I'll post it here with the build and input timeline.

    AI-assisted: yes (Claude Code)

  23. milescoviello commented on Oct 18, 2026

    @milescoviello
    Contributor

    @MarcZX10 here's step 1 so far: the frame function is located. The branch inside it isn't pinned down yet.

    Build and input. main 13e48ed with no other PRs, Linux, RTX 4070 Ti (595.99.02). PPSA02225 converted from the same dump as my earlier runs, warm shader cache, 120 to 150 s. Pad input starts at 8 s: cross, down, cross, options, cross, right, cross, up, circle, cross, one press every 1.5 s, repeating. Black frames as before.

    Main thread, sampled at 70, 90, 110 and 130 s, then 80, 95 and 110 s in a second run. It's at the same PC every time, app.elf+0x73bb62. Addresses are app.elf virtual addresses, i.e. offsets in the converted eboot. gdb can't unwind there (no unwind info), but the functions keep frame pointers, so I walked the rbp chain. It's identical in all three samples:

    Function Calls next at What it is
    0x738950 (0x1a00 bytes of locals, r13 = a display object starting 3840, 2160) spinning at 0x73bb62 frame pacing: polls the import at 0x112cf60 with [r13+0x14] (the sceVideoOutIsFlipPending pattern) while 2 or more flips are pending, busy-waiting on rdtsc up to a 30 or 60 FPS deadline
    0x8c7a80, about 53 KB 0x8d4d55 the frame function. Right before present it calls 0x67a470 on 0x17a43e0, 0x17d7020 and 0x1809c60, then 0x67a370, then 0x738950. After present it flips the buffer index at 0x31ebd88
    0x8c7a60 0x8c7a70 for (;;) frame();
    0x8c1420 0x8c4453 start-up, then enters the loop

    So the decision to post load jobs is in 0x8c7a80's body, before the present call. It isn't in the pacing loop, which only waits on flips.

    Next step. One-shot breakpoints on every direct call in 0x8c7a80 for one frame, recording which call sites run and which don't. The ones that never run include the load-posting path. I'll then read the compares guarding them and their operands at the stalled frame, and post the guard's address, its disassembly and the value it tests.

    AI-assisted: yes (Claude Code)

  24. milescoviello commented on Oct 18, 2026

    @milescoviello
    Contributor

    @MarcZX10 @sktharunbalaji13-cmd step 1 is done. Ghost waits in boot phase 0, and the guard that would move it to phase 1 tests the PSN sign-in state. AnyPS5 always reports that as signed out.

    Same build and input as above (main 13e48ed, no other PRs). Addresses are app.elf virtual addresses.

    Correction to my last comment first. I wrote some addresses in file-offset space, 0x4000 above their virtual addresses: the pacing import is at 0x1128f60, not 0x112cf60, and the globals I listed are at 0x17a03e0, 0x17d3020, 0x1805c60 and 0x31e7d88. The function addresses were correct. Every address below is a virtual address. I named the imports by computing NIDs from the names in AnyPS5's sources, which covers 561 of the 645 jump slots. 0x1128f60 is sceVideoOutIsFlipPending, as guessed.

    Coverage. Over one full frame at the stalled state (90 s), 147 of the frame function's 794 call sites run. The imports that never run include sceSystemServiceHideSplashScreen. The game calls it once a boot step completes, and on its first call only (guarded by a flag at 0x16d2fc4).

    The phase. The frame function (0x8c7a80) copies a requested phase from 0x1bb44b8 into a current phase at 0x1bb4510 (at 0x8c8cee-0x8c8d08). Its boot state machine at 0x8cc87b (state 0x1bb2f28, a jump table at 0x137da80) leaves state 0 only when the current phase is 6 (0x8cc89d: cmp $0x6,%eax). Under gdb from launch, with hardware write watchpoints on the requested phase, the current phase and the state, 180 s:

    • The state is written twice: -1 at start-up (0x870127) and 0 at 2.9 s (0x8c7096).
    • Neither phase variable is written even once, so both stay 0 for the whole run.

    The guard. Apart from the callback below, the only instruction that stores a nonzero requested phase is in start-up (the other direct store, 0xbb0e03, is a 64-bit initialization):

    0x8c6c4a: mov    $0xcc,%edi
    0x8c6c4f: call   sceSysmoduleLoadModule
    0x8c6c54: call   sceGameUpdateInitialize
    0x8c6c59: mov    0x32d80c0,%rax          ; the user
    0x8c6c60: lea    0xe0(%rsp),%rsi
    0x8c6c68: mov    (%rax),%edi
    0x8c6c6a: call   sceNpGetState
    0x8c6c6f: cmpl   $0x2,0xe0(%rsp)         ; SCE_NP_STATE_SIGNED_IN
    0x8c6c77: jne    0x8c6cb0
    0x8c6c79: movb   $0x1,0x1bb44c0
    0x8c6c80: mov    0x1bb44b8,%eax
    0x8c6c86: cmp    $0x5,%eax ; je 0x8c6c8f ; test %eax,%eax ; jne 0x8c6cb0
    0x8c6c8f: movl   $0x1,0x1bb44b8          ; requested phase 0 or 5 -> 1
    

    At 0x8c6cb0, whichever way the check went, it registers an NP state callback (0xbb1150, user data 0x1bb4430, so the phase is its field +0x88) and a reachability callback (0xbb11c0). The state callback does the same thing: state 2 sets the phase to 1 if it's 0 or 5, and state 1 resets it to 0:

    0xbb115b: cmp $0x2,%esi ; je 0xbb1181   ; signed in
    0xbb1160: cmp $0x1,%esi ; jne ret       ; signed out:
    0xbb1165:   phase != 0 -> phase = 0
    0xbb1181: (signed in) flag +0x90 = 1; phase 0 or 5 -> 1
    

    sceNpGetState in libSceNpManager/Export.cpp always stores NP_STATE_SIGNED_OUT (1), and main's callbacks deliver 1 too. So the test at 0x8c6c6f fails at start-up, no callback can ever pass it, and the phase never leaves 0.

    What I haven't shown is that signing in is enough. Phase 1 may need more online answers. To check, I've queued a throwaway build of the same main with sceNpGetState returning 2, with the same watchpoints plus a plain run that traces file reads, draws and frames. I'll post what it does. If it gets further, the decision for AnyPS5 is how a signed-in state should be reported while the online calls (sceNpGetAccountIdA, reachability) still fail. That's for the maintainers rather than for me to pick in a PR.

    AI-assisted: yes (Claude Code)

  25. milescoviello commented on Oct 19, 2026

    @milescoviello
    Contributor

    @MarcZX10 @sktharunbalaji13-cmd the two throwaway builds I queued. Same base (main 13e48ed), title, input and watchpoints as above, 180 s each. Neither change is meant for a PR as is.

    1. sceNpGetState reports SIGNED_IN (2). The start-up check passes and the requested phase becomes 1 at 2.9 s. Then thread 71, Ghost's NP worker (0xbb1200), runs sceNpCheckNpReachability and sets phase 5 in the same moment. The phase stays 5. That call's result goes through a jump table at 0xbb1340. For 0x80550003, 05, 06 (SIGNED_OUT, AnyPS5's current answer), 07, 0c, 12, 14 and 18 it sets phase 5. Other nonzero results also set 5 and pass the code to 0xcbc820. A result of 0 sets phase 6.

    2. Also sceNpCheckNpReachability returns 0. The NP worker sets phase 6 at 3.05 s. The frame function copies it to the current phase, and the boot state machine leaves state 0 for state 1 (0x1bb2f28 = 1, written at 0x8cd630). That's the first time it gets past state 0 in any of my runs. Then it stops again, at the same 75 file reads and 2 draws per frame as main.

    Where it waits now. State 1 (0x8cce40) checks two things:

    • It moves to state 3 when [0x1bb3fc0] becomes 1, or when a counter at 0x371d59c reaches the deadline in 0x1bb3f88. [0x1bb3fc0] holds -1 while something is pending.
    • It moves to state 2 when the flag at 0x1bb3fc4 is set and that counter reaches 0x1bb3f84.

    Only the game's own code writes [0x1bb3fc0]: 1 at 0x7076f2 (after 0x703780) and at 0x708aa3, 0 at 0xeb07cf. [0x1bb3fc4] is written at 0xeb07f6. No imports are next to those writes, so the next gate isn't another NP answer, or not directly. I'll take the same per-call coverage of the frame function in state 1 to see which of those paths ever run.

    What this means for AnyPS5. Ghost's boot needs a signed-in user whose PSN reachability check succeeds. With AnyPS5's consistent "signed out, PSN unreachable" answers, it waits in phase 0 forever. Ghost can be played offline on a console, so a console without a network presumably reports something this code accepts. Which values a real offline console returns from sceNpGetState and sceNpCheckNpReachability is a console measurement I can't make here. That, and whether AnyPS5 should report a signed-in, reachable user, is the maintainers' call.

    AI-assisted: yes (Claude Code)

  26. milescoviello commented on Oct 19, 2026

    @milescoviello
    Contributor

    @MarcZX10 @sktharunbalaji13-cmd a correction to my last two comments, and what actually holds Ghost back.

    Correction. The state machine at 0x1bb2f28 isn't Ghost's boot flow. Its state names come from the table at 0x1467ec0 (Offline, Idle, Query, Submit): it's the game's online cloud-data service, and the NP sign-in and reachability answers only drive that service. With main's answers (signed out, unreachable) it stays Offline, which an offline boot doesn't mind. So the two NP changes in my last comment aren't needed, and neither PR below touches NP.

    The stop is PlayGo. At start Ghost reads its own chunk map, cache_ps5/playgo.pgm. It then asks scePlayGoGetChunkId for up to 1000 ids, builds a set from them, and keeps only the playgo.pgm entries whose chunk is in that set (0xb76f00 to 0xb773f5). This dump has only sce_sys/playgo-scenario.json. For that layout AnyPS5 already answers every chunk id as installed, but scePlayGoGetChunkId listed only chunk 0. So nothing in the other chunks was ever marked installed or read: that's the 75-read stall.

    With both, it gets past the splash screen and stops 41 s in at the next check: a depth-only, alpha-tested pass (KILL_ENABLE) with PS_ITER_SAMPLE into a one-sample depth target under a 2x rasterizer without MSAA_ENABLE. Since every sample sits at the pixel center there, the per-sample invocations see the same inputs. I have a change that draws such a pass once per pixel in testing now, and I'll post where Ghost gets with it.

    For comparison, on main 43797ca alone Ghost runs 301 s on those 75 reads, and all 40 frames I dumped are black.

    AI-assisted: yes (Claude Code)

  27. dantdmdarealguy commented on Oct 22, 2026

    @dantdmdarealguy
    Contributor

    Windows data point on current main 4777cac: a clean build with no local changes. Windows 11 Pro, Intel Core i9-14900K, 128 GB, NVIDIA GeForce RTX 5080 (driver 32.0.16.1742), WinLibs GCC 15.2.0, Release. Ghost of Tsushima DC (PPSA02225) 02.024.000.

    Ruled out on this machine: no host import budget refusal, and no WAIT_REG_MEM timeout. The dump is unchanged after the runs. Logs are available on request.

  28. EagleBearerYS commented on Oct 22, 2026

    @EagleBearerYS
    Contributor

    @dantdmdarealguy thanks for the Windows runs of Ghost of Tsushima Director's Cut (PPSA02225), and for taking it past the alias stop to see what follows. The follow-up for that depth-plane alias is up as #9742, stacked on #9533. It accepts the colour target over the depth plane only where the depth-only HTILE proves every tile against DB_DEPTH_CLEAR, so depth reads the clear value there, never the plane's bytes. A draw or clear into that target afterwards still refuses. If you can run #9742's head (219e925) in place of your local skip, with #9717 and the other PRs as before, we'd like to know whether it reaches the mask screen the same way. #9742 and #9717 are both on #9533's 514643a, and merging them together only meets in adjacent lines (the test registrations, the test file and the debt entry), where keeping both sides resolves it.

  29. MarcZX10 commented on Oct 22, 2026

    @MarcZX10
    Contributor

    The Windows run on #9533 reaches the depth/HTILE alias refusal, and #9742 is the targeted follow-up now stacked on that base. Could you post the same launch path and stopping point on #9742 head 219e925509, with #9717 and the other stated prerequisites held constant? Please include whether it reaches the mask screen and any new refusal; that will test the proposed clear-only alias boundary without treating the local skip as a fix. AI-assisted review.

  30. milescoviello commented on Oct 23, 2026

    @milescoviello
    Contributor

    @MarcZX10 for the Linux side of that question: I've run Ghost of Tsushima Director's Cut (PPSA02225) on #9742's head 219e925509, then with #9717 and my PRs added, all with release libs, warm and cold (RTX 4070 Ti, 20 GiB cap):

    Since then, with #8213, #8608 and #9318 added as well, a 40-minute run got past the brightness screen into the opening cinematic. It was then killed by the 20 GiB memory cap at 1622 s, while building the opening's pipelines. Memory is the next stop on Linux, not a refusal.

    AI-assisted: yes (Claude Code)

  31. dantdmdarealguy commented on Oct 23, 2026

    @dantdmdarealguy
    Contributor

    @EagleBearerYS thanks, will do: I'll run #9742 at 219e925 in place of the local skip, with #9533, #9717, #7985, #8115 and #8121 as before (cold and warm, 600 s each) and post the result here. It'll be after the 25th, when my test machine's queue resumes.

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