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.
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.
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.
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\0 → Fals\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.
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.
Date: 2026-06-23 03:00 — 03:30.
Setup: decrypted PS3 EBOOT.BIN → EBOOT.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.
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.
Date: 2026-06-27 ~22:00.
A third-party commenter described the UE3 X360 post-process recipe:
DepthOfField=FALSEdisablesUberPostProcessentirely — that's the sledgehammer covered in Approach 2c, and it was rejected.BlurKernelSize=0inGaussianBlurFilterBufferis 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.
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.0Tested: 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.
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.
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):
- Build
liblzxas a tiny CLI:lzx_pack input.bin output.lzx. - Wrap each compressed chunk in the XMemCompress per-block header
(format documented in Xenia's
src/xenia/cpu/lzx.ccdecoder). - Validate via round-trip with Gildor's
decompress.exeon a small sample chunk — make suredecompress(encode(decompressed)) == decompressed. - Wire into the
patch_blur_kernel.pydecompress → patch → recompress pipeline. - 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.
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
BlurKernelSizeat the FName lookup. - Patching the 4 bytes after the tag header before
FArchivereads 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.
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.
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).