Skip to content

Latest commit

 

History

History
337 lines (258 loc) · 14.7 KB

File metadata and controls

337 lines (258 loc) · 14.7 KB

Depth-of-field disable — full investigation history

The X360 build of Silent Hill: Downpour renders a low-resolution depth-of-field blur that, on a modern 1080p+ display, manifests as a screen-door pixelation pattern on close foreground objects ("ефект скла"). Most visible in indoor cutscenes (bathroom intro, motel interiors). On PS3 the community RPCS3 patches.yml has a 3-instruction surgical disable that nails this without touching face-composite blur; on X360 the equivalent has been elusive.

This document is the forensic record of every approach attempted between mid-June and 2026-06-27. It is intentionally exhaustive — if you're considering the next attempt, please read this first so you don't re-walk a dead end.

Approach 2a — Ghidra forensic on base XEX

Date: 2026-06-20 night.

Attempt: walk the base XEX in Ghidra looking for any direct references to DoF.

Findings:

Site Outcome
sub_829F2A78 cluster (0x829F2A94 / 2AD8 / 2B14) 3 adjacent li r5, 1 calls. Patched to 0 in the recomp .cpp. No visual change — target addresses 0x83BDDDA4 etc. were BSS runtime data, not DoF properties. Reverted.
bEnableDOF string @ 0x82156128 0 cross-references. UE3 stores property names in an FName table at startup; runtime code reads via uint32 index, never via the string pointer.
USE_DOF shader macro @ 0x821C54A8 4 xrefs, all li r5, 0. All four sites turned out to be IMPLEMENT_SHADER_TYPE permutation descriptor builders, not runtime decision sites.
USE_DOF_HIGHQUALITY @ 0x8210b4a0 0 xrefs.
USE_DOF_BLUR_BUFFER @ 0x8210b478 0 xrefs.

Conclusion: the high-quality DoF code path is NOT compiled into the X360 build at all. There's no "switch to high-quality" branch to flip. The string-based forensic search ran out of cliff edges.

Approach 2b — Ghidra forensic on TU1 PE

Date: 2026-06-22 11:25 — 12:05.

Major find: the complete UOurPostProcessManager native dispatch table sits at .data 0x83743AB0. Direct addresses for:

Method Address
RemovePostProcess 0x825A26C0
AddPostProcess 0x82A06B90
SetPostProcess 0x82A06DF8
ResetManager 0x82B87FF0
ForceApplyChanges 0x8252B850

Decoded execRemovePostProcess: takes an FName argument from the script frame, then calls *(vtable + 296) (slot 74) for the real RemovePostProcess(FName).

Second-pass scan over both the recomp .cpp output and the PE strings confirmed: the ONLY clean "PP effect name" C string anywhere in the entire binary is "Default". Every other name (DOF / Bloom / MotionBlur / etc.) lives inside cooked Engine.xxx as an FName index stream.

Conclusion: string-based binary patching for DoF is architecturally infeasible. Vtable offset 296 is known, but reaching it requires FName resolution at runtime via singleton lookup. That's not a quick binary patch.

Secondary find at 2026-06-22 12:00: default.tu1.pe.bin bytes at sub_82A0DFF8 do NOT match the recomp .cpp output at the same VA. This blocks any future direct PE byte-patch attempt — we'd be patching something the runtime never actually loads.

Approach 2c — Coalesced.ini DepthOfField=False (BREAKTHROUGH then REJECTED)

Date: 2026-06-22 evening; rejected 2026-06-23 02:50.

Found that the cooked Game/SHGame/CookedXenon/Coalesced_INT.bin contains UTF-16 [SystemSettings] keys including DepthOfField=True. Same-length swap True\0Fals\0 at offset 0xC400 (4 bytes) is honoured by UE3's FString::ToBool() and turns DoF off at the SystemSettings level.

Why rejected: setting DepthOfField=False breaks UE3's face- composite pipeline. The composite step expects DoF to have downsampled the face source RT before it reads from it; with DoF disabled the composite reads a zero / uninitialised input and the player's face renders blurred or smeared. Too coarse.

The RPCS3 community fix is 3 surgical li r5, 0 at PS3 offsets 0x000fa828 / 0x00270c60 / 0x00310b28, which disable only the low-res variant while leaving full-res DoF on for natural face blur. Coalesced's all-or-nothing toggle can't replicate that.

Approach 2d — bUseMaxQualityMode=True (short-lived "breakthrough 2")

Date: 2026-06-22 late evening.

Setting bUseMaxQualityMode=True in Coalesced [SystemSettings] forces UE3 to allocate full-resolution post-process buffers. With DepthOfField=True + bUseMaxQualityMode=True, in-game testing confirmed the "glass" pattern was gone and menu blur preserved. This was thought to be the cleanest X360 equivalent of the PS3 patch.

Caveat: this only works because Konami evidently added the config modifier to bUseMaxQualityMode (it IS read from .ini in this fork). Later scene-by-scene comparison vs RPCS3 showed divergent behaviour in some specific scenes, which is why the DoF investigation continued past this point. v1.0 still respects this cvar — users who want to try it can flip ue3_bUseMaxQualityMode = true in the launcher.

Approach 2e — PS3 patch port

Date: 2026-06-23 03:00 — 03:30.

Setup: decrypted PS3 EBOOT.BINEBOOT.elf (SHA1 f126406e...). RPCS3 patches.yml at lines 17806-17814 anchors the DoF patch to PPU hash PPU-11f50d10f4626cdd59837e2fe44b701a9d9bfeac with the 3 offsets above.

Blocked: bytes at those PS3 offsets in our decrypted ELF are NOT 38 a0 XX XX (which would be li r5, X) under either raw-file-offset or vaddr-minus-0x10000 interpretation. RPCS3's hash-anchored address translation scheme is not fully understood.

Java Ghidra scan: wrote FindDofLowresPatches.java to scan all 111,276 bl/bla/bctrl instructions in the ELF, looking for li r5, 0 candidates feeding into DoF-related call sites. Produced 1,378 candidates. Top hit was a false positive (USystemSettings::LoadFromIni). The X360 render-time DoF decision code references NO DoF-related strings, so heuristic Ghidra hunts dead-end without a string-anchored handhold.

Conclusion: porting the PS3 patch directly requires understanding RPCS3's address translation. Not a quick fix.

Approach 2f — Engine.xxx byte patch (INFEASIBLE without repack)

Date: 2026-06-22 14:00.

Gildor's decompress.exe decompresses Engine.xxx (1.7 MB LZX → 6.7 MB raw) and OurEngine.xxx (32 KB → 79 KB). UELib parses both cleanly. Property tag format documented.

DoF default-property values live as Default__DOFEffect etc., all inheriting bActiveOnX360 = TRUE from the base. Patching the base = disabling ALL post-FX (bloom, motion blur, AO, the lot). The surgical fix would need to insert a 25-byte property tag in each subclass and shift all 28,208 subsequent export SerialOffset values by +25 each — plus repack the 16-byte LZX wrapper that Konami uses around each chunk.

Required tool missing: see Approach 2j below.

Approach 2g — Community lead: BlurKernelSize=0 (scalpel)

Date: 2026-06-27 ~22:00.

A third-party commenter described the UE3 X360 post-process recipe:

  • DepthOfField=FALSE disables UberPostProcess entirely — that's the sledgehammer covered in Approach 2c, and it was rejected.
  • BlurKernelSize=0 in GaussianBlurFilterBuffer is the targeted scalpel — disables only the actual blur kernel, leaves the rest of the post-process chain (bloom, colour grade, etc.) intact.

Symbol grep against the unpacked Engine.xxx + OurEngine.xxx confirmed the keys exist:

Symbol Count
BlurKernelSize 5
DepthOfField 1
UberPostProcess 13
MLAA 7
DOF_BlurKernelSize 5
DOF_BlurBloomKernelSize 5

If we can write 0.0f at the right 13 byte offsets (3 in Engine.xxx + 10 in OurEngine.xxx), the in-engine kernel collapses to zero radius = no actual blur.

Approach 2h — BlurKernelSize Coalesced override (TRIED, FAILED, REVERTED)

Date: 2026-06-27 21:24.

Added 5 sections to Xe-SHEngine.ini inside Coalesced_INT.bin:

[Engine.DOFEffect]
BlurKernelSize=0.0
MaxNearBlurAmount=0.0
MaxFarBlurAmount=0.0

[Engine.DOFAndBloomEffect]
BlurKernelSize=0.0
MaxNearBlurAmount=0.0
MaxFarBlurAmount=0.0

[Engine.UberPostProcessEffect]
BlurKernelSize=0.0
MaxNearBlurAmount=0.0
MaxFarBlurAmount=0.0

[OurEngine.OurUberPostProcessEffect]
DOF_BlurKernelSize=0.0
DOF_BlurBloomKernelSize=0.0
BlurKernelSize=0.0

[OurEngine.OurPostProcessSettings]
DOF_BlurKernelSize=0.0
DOF_BlurBloomKernelSize=0.0

Tested: relaunched game, walked into Murphy intro bridge scene — visually unchanged. The grey "glass" haze was still present in the background behind Murphy on the bridge.

Conclusion: BlurKernelSize is NOT marked config in Konami's UE3 fork. The .ini sections we added are silently ignored because the property doesn't have the runtime config-reading wiring.

Coalesced reverted byte-perfect from backup Coalesced_INT.bin.backup-pre-blur-coalesced-2026-06-27-2117.

Approach 2i — BlurKernelSize cook-data byte patch (TRIED, FAILED, REVERTED)

Date: 2026-06-27 20:09.

tools/patch_blur_kernel.py (script lives in the SDK research repo, not in DownpourRecomp) replaces the live LZX-compressed Engine.xxx (1.7 MB) and OurEngine.xxx (32 KB) with byte-identical Gildor- decompressed reference copies (6.7 MB / 79 KB), then writes 13 BE-float zeros at known offsets:

File Offset Original value Patched
Engine.xxx 0x002941AE 2.0 0.0
Engine.xxx 0x0031E2CD 16.0 0.0
Engine.xxx 0x003E3B4F 5.0 0.0
OurEngine.xxx 0x0000C5FB 16.0 0.0
OurEngine.xxx 0x0000C617 16.0 0.0
OurEngine.xxx 0x0000CB15 16.0 0.0
OurEngine.xxx 0x0000CB31 16.0 0.0
OurEngine.xxx 0x0000D85D 16.0 0.0
OurEngine.xxx 0x0000D879 16.0 0.0
OurEngine.xxx 0x0000DBF2 16.0 0.0
OurEngine.xxx 0x0000DC0E 16.0 0.0
OurEngine.xxx 0x0000DF87 16.0 0.0
OurEngine.xxx 0x0000DFA3 16.0 0.0

All 13 byte writes verified by read-back (= 0.0f). Game launched. Game hung on infinite loading screen with no log output (logs were off in the same session).

Root cause: the statically-recompiled X360 cook loader does NOT accept Gildor-decompressed packages. Gildor produces a PC-format PackageFileSummary (FileVersion 0x000002F4 = 756 at offset 4) but the loader expects the X360 cooked-wrapper variant (different layout with chunk table preserved). Both share the magic bytes 9E 2A 83 C1 at offset 0, but their summaries are NOT interchangeable.

Backups restored from *.backup-pre-blurkernel-2026-06-27-2009, game boots normally on the original LZX-wrapped files.

The byte patches are correct. The problem is the format of the file we'd need to ship them in.

Approach 2j — liblzx-based LZX encoder (NEXT-SESSION WORK)

Status: not started; reconnaissance done 2026-06-27 21:18.

The only thing missing for Approach 2i to work is an X360-compatible LZX encoder so we can round-trip the patched bytes back into the on-disk LZX-wrapped format.

Option Status
xcompress32.dll (Xbox 360 SDK proprietary) Not present on test bench. Sourcing the leaked SDK has unclear redistribution rights. Likely a dead-end.
gibbed/XCompression (.NET wrapper around above) Useless without the proprietary DLL.
elasota/liblzx (pure C, CAB-LZX encoder) Best open candidate. https://github.com/elasota/liblzx — fork of wimlib's LZX encoder, tuned for CAB-LZX format which is the closest open analogue to Microsoft's XMemCompress wire format.
wimlib lzx-compress.c Fallback if liblzx's window-size tuning diverges from the Xbox stream.
coderforlife/ms-compress LZX Removed from the upstream tree due to stability issues. Skip.
bnnm LZX-XNB gist Decoder only; useful as a reference for the XMemCompress chunk header (4-byte BE compressed-size + uncompressed-size).

Realistic plan (~4-8 hours):

  1. Build liblzx as a tiny CLI: lzx_pack input.bin output.lzx.
  2. Wrap each compressed chunk in the XMemCompress per-block header (format documented in Xenia's src/xenia/cpu/lzx.cc decoder).
  3. Validate via round-trip with Gildor's decompress.exe on a small sample chunk — make sure decompress(encode(decompressed)) == decompressed.
  4. Wire into the patch_blur_kernel.py decompress → patch → recompress pipeline.
  5. Apply to the live Engine.xxx + OurEngine.xxx; ship the patched files alongside the launcher (or have the launcher apply at first run).

Once an encoder is wired up, the 13 byte writes from Approach 2i become a single tool invocation.

Approach 2k — SDK runtime hook (last-resort plan)

If the LZX encoder path turns out to be intractable, the fallback is to hook the SDK side of FArchive::operator<<(float) during CDO construction and force 0.0f for the BlurKernelSize property tag.

This requires:

  • Identifying which recomp function performs the CDO load for each of the 4 owning classes (DOFEffect, DOFAndBloomEffect, UberPostProcessEffect, OurUberPostProcessEffect).
  • Confirming the property tag's name-table index resolves to BlurKernelSize at the FName lookup.
  • Patching the 4 bytes after the tag header before FArchive reads them into the CDO.

Multi-day Ghidra session. Not yet started. Listed here so a future session knows it's the bottom-of-the-stack option.

What ships in v1.0

v1.0 ships with the DoF artifact still present. The 13 byte patches are documented above so a future v1.0.1 with a working LZX encoder can apply them in a single run.

In the meantime, the launcher's UE3 Engine tab gives users a way to turn off the sledgehammer (DepthOfField=False) or force max-quality mode (bUseMaxQualityMode=True) if they want to experiment. Neither is enabled by default.

See also

  • v1.0-known-issues.md — the user-facing short summary of why DoF haze is still there.
  • chromatic-noise-fix.md — a different RenderDoc forensic from earlier in the project that DID find a fix (depth → 7e3 ownership transfer).