From 850c8d5b2d5862447141a7386dbb4cac8411a5e6 Mon Sep 17 00:00:00 2001 From: mchughalex <25671999+mchughalex@users.noreply.github.com> Date: Mon, 13 Jul 2026 18:25:25 +0100 Subject: [PATCH] scene: native photo-editor postfx (exact ucode ports) + grab readback window --- cmake/EmbedShaders.cmake | 2 + src/native/shaders/photo_fx.hlsl | 583 +++++++++++++++++ src/skate3_native_render.cpp | 14 + src/skate3_native_scene.cpp | 1049 +++++++++++++++++++++++++++++- src/skate3_native_scene.h | 19 + 5 files changed, 1655 insertions(+), 12 deletions(-) create mode 100644 src/native/shaders/photo_fx.hlsl diff --git a/cmake/EmbedShaders.cmake b/cmake/EmbedShaders.cmake index e4d6d8a..12c8fa5 100644 --- a/cmake/EmbedShaders.cmake +++ b/cmake/EmbedShaders.cmake @@ -13,6 +13,7 @@ set(SHADER_VARS kOutlineShaderSource kShader2dSource kShaderSplineSource + kPhotoFxShaderSource ) set(SHADER_FILES scene.hlsl @@ -22,6 +23,7 @@ set(SHADER_FILES outline.hlsl overlay2d.hlsl spline.hlsl + photo_fx.hlsl ) set(content "// Generated by cmake/EmbedShaders.cmake from src/native/shaders/*.hlsl.\n") diff --git a/src/native/shaders/photo_fx.hlsl b/src/native/shaders/photo_fx.hlsl new file mode 100644 index 0000000..58e4a56 --- /dev/null +++ b/src/native/shaders/photo_fx.hlsl @@ -0,0 +1,583 @@ +// Photo-editor postfx chain: exact ports of the game's shaders, transcribed +// mechanically from their Xenos ucode (dumped via an F11 capture) and +// validated bit-exact against an offline ucode interpreter before HLSL +// conversion. Pass order +// mirrors the game's chain while a photo-mission photo editor is up: +// visualfx (full res) scene+accum color grade / vignette / depth->CoC +// dof_down (576x320) 4-tap alpha(CoC)-weighted downsample +// dof_mb (576x320) 9-tap motion-blur+DOF pass (motion from depth) +// dof (576x320) 9-tap variable-radius CoC blur +// uber (full res) temporal-jitter combine + grade LUT + grain +// fisheye (full res) lens warp + vignette gradient + tint -> screen +// Constants come LIVE from the game's PS/VS shadow banks captured at the +// SetPending_AluConstants hook per pass (rows c0..c31 + VS rows in c240..247); +// rows c250..c255 are the shaders' LITERAL tables, loaded by the game via +// PM4 LOAD_ALU_CONSTANT from the shader asset footers and baked here by the +// app layer (values read from a live capture). +// +// Register indices are kept IDENTICAL to the ucode. Do not simplify. + +cbuffer Consts : register(b0) { float4 c[256]; }; +// c[240..247] = the pass's VS c0..c7 (uvOffset/uvScale/oC1 rows). +// c[248].x = output width, .y = output height (depth pack / debug). + +Texture2D t0 : register(t0); +Texture2D t1 : register(t1); +Texture2D t2 : register(t2); +Texture2D t3 : register(t3); +Texture3D t4 : register(t4); +Texture2D t6 : register(t6); +Texture2D t7 : register(t7); +SamplerState s_lin : register(s0); // linear clamp (color/gradient fetches) +SamplerState s_pt : register(s1); // point clamp (packed depth) +SamplerState s_wrap : register(s2); // linear wrap (grain) + +struct VsOut { + float4 pos : SV_Position; + float4 r0 : TEXCOORD0; + float4 r1 : TEXCOORD1; +}; + +// Fullscreen triangle; per-pass interpolators mirror the game quad VSs +// (uv attr = screen uv; ucode: oC0 = (uv + c3.xy, uv), oC1 = uv.x*c0 + +// uv.y*c1 + c2; the offset/scale rows are the captured VS constants). +VsOut vs_offset(uint vid : SV_VertexID) { + VsOut o; + float2 t = float2((vid << 1) & 2, vid & 2); + o.pos = float4(t * float2(2, -2) + float2(-1, 1), 0, 1); + o.r0 = float4(t + c[243].xy, t); + o.r1 = t.x * c[240] + t.y * c[241] + c[242]; + return o; +} + +// uber: oC0 = raw uv4. +VsOut vs_raw(uint vid : SV_VertexID) { + VsOut o; + float2 t = float2((vid << 1) & 2, vid & 2); + o.pos = float4(t * float2(2, -2) + float2(-1, 1), 0, 1); + o.r0 = float4(t, t); + o.r1 = 0; + return o; +} + +// fisheye: oC0.xy = uv*c3.xy + c2.xy (uvScale/uvTrans), oC0.zw = raw uv. +VsOut vs_scaled(uint vid : SV_VertexID) { + VsOut o; + float2 t = float2((vid << 1) & 2, vid & 2); + o.pos = float4(t * float2(2, -2) + float2(-1, 1), 0, 1); + o.r0 = float4(t * c[243].xy + c[242].xy, t); + o.r1 = 0; + return o; +} + +// --------------------------------------------------------------------------- +// Depth pack: native D32 MSAA depth (sample 0) -> the console D24S8-as-8888 +// texel layout the ported shaders reconstruct: sample.x = mid byte, +// sample.y = low byte, sample.z = stencil (0 natively), sample.w = high +// byte. dp3 with the baked weights (255/16776960, 255/256, 255/65536) then +// yields the exact 24-bit depth (visualfx c254.xyz, dof_mb c255.xyz). +#ifdef PFX_MSAA +Texture2DMS depth_ms : register(t5); +#else +Texture2D depth_1x : register(t5); +#endif + +float4 ps_depthpack(VsOut i) : SV_Target { +#ifdef PFX_MSAA + float d = depth_ms.Load(int2(i.pos.xy), 0); +#else + float d = depth_1x.Load(int3(int2(i.pos.xy), 0)); +#endif + float D = min(floor(d * 16777216.0), 16777215.0); + float hi = floor(D / 65536.0); + float mid = floor((D - hi * 65536.0) / 256.0); + float lo = D - hi * 65536.0 - mid * 256.0; + return float4(mid, lo, 0.0, hi) * (1.0 / 255.0); +} + +// Plain bilinear blit: the quarter-res accumulation update (the game feeds +// visualfx tf3 from a motion-accumulation buffer; natively it is approximated +// per skate3_native_render_scene_photo_native_accum). +float4 ps_blit(VsOut i) : SV_Target { + return t0.SampleLevel(s_lin, i.r0.zw, 0); +} + +// --------------------------------------------------------------------------- +// postfx_visualfxPS (bit-exact transcription). t0 = scene, +// t1 = packed depth, t3 = quarter-res accumulation buffer. +float4 ps_visualfx(VsOut i) : SV_Target { + float2 uv = i.r0.xy; + float4 r0 = i.r0; + float4 r1 = i.r1; + float4 r2 = 0, r3 = 0, r4 = 0, r5 = 0, r6 = 0; + float4 oC0 = 0; + float nx, ny, nz, nw, dw, rx, old, r1x_old; + + r2 = t1.SampleLevel(s_pt, uv, 0); // tfetch2D r2, r0.xy, tf1 + r4.xyz = t0.SampleLevel(s_lin, uv, 0).xyz; // tfetch2D r4.xyz_, r0.xy, tf0 + r3.xyz = t3.SampleLevel(s_lin, uv, 0).xyz; // tfetch2D r3.xyz_, r0.xy, tf3 + + // 7: mad r3.xyz_, r4.zxyy, c253.zzzz, r3.zxyy + nx = r4.z * c[253].z + r3.z; + ny = r4.x * c[253].z + r3.x; + nz = r4.y * c[253].z + r3.y; + r3.x = nx; r3.y = ny; r3.z = nz; + // 8: dp3 r0._y__, r2.ywxx, c254.xyzz (24-bit depth reconstruction) + r0.y = r2.y * c[254].x + r2.w * c[254].y + r2.x * c[254].z; + // 9: add r6.__zw, c2.yyyx, -c1.yyyx ; + subsc r3.___w, -c4.x, -r0.y + r6.z = c[2].y - c[1].y; + r6.w = c[2].x - c[1].x; + r3.w = r0.y - c[4].x; + // 10: mad r1.xyz_, r0.yyyy, c6.xyzz, r1.xyzz + r1.x = r0.y * c[6].x + r1.x; + r1.y = r0.y * c[6].y + r1.y; + r1.z = r0.y * c[6].z + r1.z; + // 11: mad r0._yz_, r1.zzzz, r0.zzww, r1.xxyy + ny = r1.z * r0.z + r1.x; + nz = r1.z * r0.w + r1.y; + r0.y = ny; r0.z = nz; + // 12: dp3 r0.___w, r3.xyzz, c255.xyzz ; + rcp r0.x___, r3.w + dw = r3.x * c[255].x + r3.y * c[255].y + r3.z * c[255].z; + rx = 1.0 / r3.w; + r0.w = dw; + r0.x = rx; + // 13: mad r1.xyz_, r0.wwww, c0.zyxx, -r3.xzyy + r1.x = r0.w * c[0].z + (-r3.x); + r1.y = r0.w * c[0].y + (-r3.z); + r1.z = r0.w * c[0].x + (-r3.y); + // 14: mad r4.xyz_, r1.xyzz, c0.wwww, r3.xzyy + r4.x = r1.x * c[0].w + r3.x; + r4.y = r1.y * c[0].w + r3.z; + r4.z = r1.z * c[0].w + r3.y; + // 15: mul r0.x__w, r0.xxxx, c253.xyyy + old = r0.x; + r0.x = old * c[253].x; + r0.w = old * c[253].y; + // 16: mul r0.x__w, r0.xwww, c3.xxxx ; + maxs r4.___w, c255.ww + nx = r0.x * c[3].x; + nw = r0.w * c[3].x; + r0.x = nx; r0.w = nw; + r4.w = c[255].w; + // 17: mad_sat r5.xy__, r0.wwww, c13.xyyy, c14.xyyy + r5.x = saturate(r0.w * c[13].x + c[14].x); + r5.y = saturate(r0.w * c[13].y + c[14].y); + +#define DP4PERM(CR) ((CR).z * r4.x + (CR).x * r4.z + (CR).y * r4.y + (CR).w * r4.w) + // 18-24: the FX color matrix + gates + r3.x = DP4PERM(c[7]); + r3.y = DP4PERM(c[8]); + r2.x = saturate(r2.z); + r3.z = DP4PERM(c[9]); + r2.y = r2.x - c[255].w; + r3.w = DP4PERM(c[10]); + r1.y = c[5].x * r0.y; + r1.x = (r0.w >= c[1].y) ? 1.0 : 0.0; + r0.y = c[5].x * r0.z; + r6.x = DP4PERM(c[12]); + r2.z = r1.y * r1.y; + r6.y = DP4PERM(c[11]); + r2.w = r0.y * r0.y; +#undef DP4PERM + // 25: add_sat r1._y_w, r2.xxyy, r2.xxxx + r1.y = saturate(r2.x + r2.x); + r1.w = saturate(r2.y + r2.x); + // 26: mad r1.x_z_, r6.zwww, r1.xxxx, c1.yxxx + r1x_old = r1.x; + r1.x = r6.z * r1x_old + c[1].y; + r1.z = r6.w * r1x_old + c[1].x; + // 27: mul r3, r3, r5.xxxy ; + addsc r0.__z_, c253.w, r0.x + r3.x *= r5.x; + r3.y *= r5.x; + r3.z *= r5.x; + r3.w *= r5.y; + r0.z = c[253].w + r0.x; + // 28: mad r3._yz_, r6.xxyy, r5.yyyy, r3.zzyy + ny = r6.x * r5.y + r3.z; + nz = r6.y * r5.y + r3.y; + r3.y = ny; r3.z = nz; + // 29: add r0._y__, r1.xxxx, -r0.wwww ; + adds r0.___w, r2.zw + r0.y = r1.x + (-r0.w); + r0.w = r2.z + r2.w; + // 30: mul_sat r0.xy__, r0.yzzz, r1.zwww ; + adds r3.x___, r3.xw + nx = saturate(r0.y * r1.z); + ny = saturate(r0.z * r1.w); + r0.x = nx; r0.y = ny; + r3.x = r3.x + r3.w; + // 31: add r4.xyz_, r4.zxyy, -r3.xyzz ; + adds r0.__z_, r2.xx + nx = r4.z - r3.x; + ny = r4.x - r3.y; + nz = r4.y - r3.z; + r4.x = nx; r4.y = ny; r4.z = nz; + r0.z = r2.x + r2.x; + // 32: add r4.___w, -r0.yyyy, c255.wwww ; + mulsc r0._y__, c16.x, r1.y + r4.w = (-r0.y) + c[255].w; + r0.y = c[16].x * r1.y; + // 33: mul r1, r0.yyyw, r4.xzyw ; + subsc_sat r0.__z_, c255.w, r0.z + r1.x = r0.y * r4.x; + r1.y = r0.y * r4.z; + r1.z = r0.y * r4.y; + r1.w = r0.w * r4.w; + r0.z = saturate(c[255].w - r0.z); + // 34: mul_sat r0.__z_, r1.wwww, r0.zzzz ; + mulsc r0._y__, c255.z, r0.x + r0.z = saturate(r1.w * r0.z); + r0.y = c[255].z * r0.x; + // 35: add oC0.xyz_, r3.xzyy, r1.xyzz + oC0.x = r3.x + r1.x; + oC0.y = r3.z + r1.y; + oC0.z = r3.y + r1.z; + // 36: max r0.__z_, r0.zzzz, c254.wwww ; + mulsc r0.x___, c15.x, r0.y + r0.z = max(r0.z, c[254].w); + r0.x = c[15].x * r0.y; + // 37: max_sat oC0.___w, r0.zzzz, r0.xxxx + oC0.w = saturate(max(r0.z, r0.x)); + return oC0; +} + +// --------------------------------------------------------------------------- +// bloom_dof_motionblur_dof_dowsamplePS (bit-exact): 4-tap +// alpha-weighted downsample of the visualfx output into the DOF half buffer. +float4 ps_dof_down(VsOut i) : SV_Target { + float4 r0 = i.r0; + float4 r1 = 0, r2 = 0, r3 = 0, r4 = 0, r5 = 0; + float4 c255 = c[255]; + + r1.y = r0.y; + r1.z = r0.x; + r1.x = r0.x + c255.x; + r1.w = r0.y + c255.y; + r4 = t0.SampleLevel(s_lin, float2(r1.x, r1.y), 0); + r5 = t0.SampleLevel(s_lin, float2(r1.z, r1.w), 0); + r1 = t0.SampleLevel(s_lin, float2(r1.x, r1.w), 0); + r2 = t0.SampleLevel(s_lin, float2(r0.x, r0.y), 0); + r3.x = r2.w + c255.w; + r0.x = r1.x * r1.w; + r0.y = r1.z * r1.w; + r0.z = r1.y * r1.w; + { + float ny = r5.x * r5.w + r0.x; + float nz = r5.y * r5.w + r0.z; + float nw = r5.z * r5.w + r0.y; + r0.y = ny; r0.z = nz; r0.w = nw; + } + r0.x = r3.x + r4.w; + r0.x = r0.x + r5.w; + { + float ny = r4.x * r4.w + r0.y; + float nz = r4.z * r4.w + r0.w; + float nw = r4.y * r4.w + r0.z; + r0.y = ny; r0.z = nz; r0.w = nw; + } + r1.x = r3.x * r2.x + r0.y; + r1.y = r3.x * r2.y + r0.w; + r1.z = r3.x * r2.z + r0.z; + r0.x = r0.x + r1.w; + r0.y = 1.0 / r0.x; + float4 oC0; + oC0.x = r1.x * r0.y; + oC0.y = r1.y * r0.y; + oC0.z = r1.z * r0.y; + oC0.w = c255.z * r0.x; + return oC0; +} + +// --------------------------------------------------------------------------- +// bloom_dof_tap9dofMotionBlurPS (bit-exact): 9-tap motion-blur + +// DOF pass. t0 = DOF half buffer, t1 = packed depth (motion reconstructed +// from depth + camera rows in c1). +float4 ps_dof_mb(VsOut i) : SV_Target { + float4 r0 = i.r0; + float4 r1 = 0, r2 = 0, r3 = 0, r4 = 0, r5 = 0, r6 = 0, r11 = 0; + float ps = 0; + float4 c254 = c[254], c255 = c[255]; + + // /*6*/ + r2.x = r0.x + c[7].x; r2.y = r0.y + c[7].y; + // /*7*/ tfetch2D r1.___w, r2.xy, tf0 + r1.w = t0.SampleLevel(s_lin, r2.xy, 0).w; + // /*8*/ tfetch2D r2.xyw_, r0.xy, tf1 (dest xyw_ -> r2.x,r2.y,r2.z <- s.x,s.y,s.w) + { float4 vel = t1.SampleLevel(s_pt, r0.xy, 0); r2.x = vel.x; r2.y = vel.y; r2.z = vel.w; } + // /*9*/ dp3 r2._y__, r2.yzxx, c255.xyzz + mulsc r2.x, c2.x, r1.w + { float dp = r2.y * c255.x + r2.z * c255.y + r2.x * c255.z; r2.y = dp; } + r2.x = c[2].x * r1.w; + // /*10*/ + r2.x = max(r2.x, c254.y); + // /*11*/ mad r1.xyz_, r2.yyyy, c1.xzyy, r1.xzyy (r1.xyz==0) + { float nx = r2.y * c[1].x + r1.x, ny = r2.y * c[1].z + r1.z, nz = r2.y * c[1].y + r1.y; r1.x = nx; r1.y = ny; r1.z = nz; } + // /*12*/ mad r1.xy__, r1.yyyy, r0.zwww, r1.xzzz + { float nx = r1.y * r0.z + r1.x, ny = r1.y * r0.w + r1.z; r1.x = nx; r1.y = ny; } + // /*13*/ mul r1._yz_, r1.xxyy, c0.xxxx + { float ny = r1.x * c[0].x, nz = r1.y * c[0].x; r1.y = ny; r1.z = nz; } + // /*14*/ dp2add r0.___w, r1.yzzz, r1.yzzz, c255.wwww + r0.w = r1.y * r1.y + r1.z * r1.z + c255.w; + // /*15*/ add r1.x, r1.y, -r2.x + sqrt_sat r0.w, |r0.w| + r1.x = r1.y - r2.x; + r0.w = saturate(sqrt(abs(r0.w))); + // /*16*/ mul r1._yz_, r1.zzxx, r0.wwww + addsc r0.w, c254.z, r0.w + { float ny = r1.z * r0.w, nz = r1.x * r0.w; r1.y = ny; r1.z = nz; } + r0.w = c254.z + r0.w; + // /*17*/ + r1.x = r1.z + r2.x; + + // /*18-27*/ tap centers + r2.x = r1.x * c[11].w + r0.x; r2.y = r1.y * c[11].w + r0.y; + r4.xy = r1.xy * c[9].w; + r6.xy = r1.xy * c[7].w; + r5.xy = r1.xy * c[5].w; r4.z = c[10].w * r1.x; + r3.xy = r1.xy * c[3].w; r6.z = c[8].w * r1.x; + r3.z = r1.x * c[4].w; r3.w = r1.y * c[4].w; r5.z = c[6].w * r1.x; + r3 += r0.xyxy; r5.w = c[6].w * r1.y; + r5 += r0.xyxy; r6.w = c[8].w * r1.y; + r6 += r0.xyxy; r4.w = c[10].w * r1.y; + r1 = r4 + r0.xyxy; + + // /*28-37*/ taps + r4 = t0.SampleLevel(s_lin, r1.zw, 0); + r1 = t0.SampleLevel(s_lin, r1.xy, 0); + float4 r12 = t0.SampleLevel(s_lin, r6.zw, 0); + float4 r10 = t0.SampleLevel(s_lin, r6.xy, 0); + float4 r9 = t0.SampleLevel(s_lin, r5.zw, 0); + float4 r8 = t0.SampleLevel(s_lin, r5.xy, 0); + float4 r7 = t0.SampleLevel(s_lin, r3.zw, 0); + r5 = t0.SampleLevel(s_lin, r3.xy, 0); + r2 = t0.SampleLevel(s_lin, r2.xy, 0); + r0.x = t0.SampleLevel(s_lin, r0.xy, 0).w; // /*37*/ r0.w___ -> r0.x <- s.w + + // /*38-49*/ coc weights (r0.w = base blur radius term) + r3.x = saturate(r0.w + c[9].z); r0.y = saturate(c[10].z + r0.w); + r3.y = saturate(r0.w + c[11].z); r0.z = saturate(c[3].z + r0.w); + r3.w = saturate(r3.y * r2.w + r0.w); + r6.x = r0.z * r5.w; r0.z = saturate(c[4].z + r0.w); + r6.y = r0.z * r7.w; r0.z = saturate(c[5].z + r0.w); + r6.z = r0.z * r8.w; r0.z = saturate(c[6].z + r0.w); + r6.w = r0.z * r9.w; r0.z = saturate(c[7].z + r0.w); + r11.x = r0.z * r10.w; r0.z = saturate(c[8].z + r0.w); + r11.z = r3.x * r1.w; ps = r0.z; // maxs r0._,r0.zz + r11.w = r0.y * r4.w; r11.y = r12.w * ps; ps = r12.w * ps; + r11 = saturate(r0.w + r11); ps = r0.x; // maxs r0.x,r0.xx + r6 = saturate(r0.w + r6); ps = r11.w; // maxs r0._,r11.ww + + // /*50-62*/ weighted color accumulation + r5.w = (r6.w + r6.z + r6.x + r6.y) * c254.x; r3.x = r4.x * ps; ps = r4.x * ps; + { float nx = r11.z * r1.x, ny = r11.z * r1.y, nz = r11.z * r1.z; r1.x = nx; r1.y = ny; r1.z = nz; } ps = r11.w; + r1.w = (r11.w + r11.z + r11.y + r11.x) * c254.x; r3.y = r4.y * ps; ps = r4.y * ps; + r0.y = r11.y * r12.x; r0.z = r11.y * r12.y; r0.w = r11.y * r12.z; ps = r11.w; + // /*54*/ mad r0._yzw, r11.xxxx, r10.xxzy, r0.yywz + { float ny = r11.x * r10.x + r0.y, nz = r11.x * r10.z + r0.w, nw = r11.x * r10.y + r0.z; r0.y = ny; r0.z = nz; r0.w = nw; } + // /*55*/ mad r0._yzw, r6.wwww, r9.xxyz, r0.yywz + { float ny = r6.w * r9.x + r0.y, nz = r6.w * r9.y + r0.w, nw = r6.w * r9.z + r0.z; r0.y = ny; r0.z = nz; r0.w = nw; } + // /*56*/ mad r0._yzw, r6.zzzz, r8.xxzy, r0.yywz + { float ny = r6.z * r8.x + r0.y, nz = r6.z * r8.z + r0.w, nw = r6.z * r8.y + r0.z; r0.y = ny; r0.z = nz; r0.w = nw; } + // /*57*/ mad r0._yzw, r6.yyyy, r7.xxyz, r0.yywz + { float ny = r6.y * r7.x + r0.y, nz = r6.y * r7.y + r0.w, nw = r6.y * r7.z + r0.z; r0.y = ny; r0.z = nz; r0.w = nw; } + // /*58*/ mad r5.xyz_, r6.xxxx, r5.xzyy, r0.ywzz + { float nx = r6.x * r5.x + r0.y, ny = r6.x * r5.z + r0.w, nz = r6.x * r5.y + r0.z; r5.x = nx; r5.y = ny; r5.z = nz; } + // /*59*/ add r1, r5.xzyw, r1 + muls_prev r3.z, r4.z + r1 = float4(r5.x + r1.x, r5.z + r1.y, r5.y + r1.z, r5.w + r1.w); + r3.z = r4.z * ps; ps = r4.z * ps; + // /*60*/ add r1, r1.xzyw, r3.xzyw + maxs r0.y, c254.ww + r1 = float4(r1.x + r3.x, r1.z + r3.z, r1.y + r3.y, r1.w + r3.w); + r0.y = c254.w; ps = c254.w; + // /*61*/ rcp r0.z, r1.w + r0.z = 1.0 / r1.w; + // /*62*/ mad r1.xyz_, r3.wwww, r2.xyzz, r1.xzyy + { float nx = r3.w * r2.x + r1.x, ny = r3.w * r2.y + r1.z, nz = r3.w * r2.z + r1.y; r1.x = nx; r1.y = ny; r1.z = nz; } + // /*63*/ mul oC0.xyz_, r1.xyzz, r0.zzzz + maxs oC0.w, r0.xy + float4 oC0; + oC0.x = r1.x * r0.z; + oC0.y = r1.y * r0.z; + oC0.z = r1.z * r0.z; + oC0.w = max(r0.x, r0.y); + return oC0; +} + +// --------------------------------------------------------------------------- +// bloom_dof_tap9dofPS (bit-exact): 9-tap variable-radius CoC blur. +float4 ps_dof(VsOut i) : SV_Target { + float4 r0 = i.r0; + float4 r2 = 0, r3 = 0, r4 = 0, r5 = 0; + + float4 r1 = t0.SampleLevel(s_lin, float2(r0.x, r0.y), 0); + r0.z = c[0].x * r1.w; + r0.z = max(r0.z, c[255].x); + float rz = r0.z; + + r5.xy = rz * c[8].xy; + r4.xy = rz * c[6].xy; + r3.xy = rz * c[3].xy; r5.z = c[9].x * rz; + r2.xy = rz * c[1].xy; r4.z = c[7].x * rz; + r2.z = rz * c[2].x; r2.w = rz * c[2].y; r3.z = c[4].x * rz; + r2 += r0.xyxy; r3.w = c[4].y * rz; + r3 += r0.xyxy; r4.w = c[7].y * rz; + r4 += r0.xyxy; r5.w = c[9].y * rz; + r0 = r5 + r0.xyxy; + + float4 r12 = t0.SampleLevel(s_lin, r0.zw, 0); + float4 r11 = t0.SampleLevel(s_lin, r0.xy, 0); + float4 r10 = t0.SampleLevel(s_lin, r4.zw, 0); + float4 r9 = t0.SampleLevel(s_lin, r4.xy, 0); + float4 r8 = t0.SampleLevel(s_lin, r3.zw, 0); + float4 r7 = t0.SampleLevel(s_lin, r3.xy, 0); + float4 r6 = t0.SampleLevel(s_lin, r2.zw, 0); + r5 = t0.SampleLevel(s_lin, r2.xy, 0); + + r3.y = r8.w * c[4].z; r0.x = r7.w; + r3.x = r1.w * c[5].z; r3.z = c[3].z * r0.x; + r2.z = r10.w * c[7].z; r0.y = r9.w; + r2.x = r12.w * c[9].z; r0.x = r11.w; + r4.x = r2.x * r12.x; r4.y = r2.x * r12.y; r4.z = r2.x * r12.z; + r2.y = c[8].z * r0.x; + { + float nx = r2.y * r11.x, nz = r2.y * r11.y, nw = r2.y * r11.z; + r0.x = nx; r0.z = nz; r0.w = nw; + r2.w = c[6].z * r0.y; + } + { float nx = r2.z * r10.x + r0.x, ny = r2.z * r10.z + r0.w, nz = r2.z * r10.y + r0.z; r0.x = nx; r0.y = ny; r0.z = nz; } + { float nx = r2.w * r9.x + r0.x, ny = r2.w * r9.y + r0.z, nz = r2.w * r9.z + r0.y; r0.x = nx; r0.y = ny; r0.z = nz; } + { float nx = r3.x * r1.x + r0.x, ny = r3.x * r1.z + r0.z, nz = r3.x * r1.y + r0.y; r0.x = nx; r0.y = ny; r0.z = nz; } + { float nx = r3.y * r8.x + r0.x, ny = r3.y * r8.y + r0.z, nz = r3.y * r8.z + r0.y; r0.x = nx; r0.y = ny; r0.z = nz; } + { float nx = r3.z * r7.x + r0.x, ny = r3.z * r7.z + r0.z, nz = r3.z * r7.y + r0.y; r0.x = nx; r0.y = ny; r0.z = nz; } + r4.w = r6.w * c[2].z; r5.w = c[1].z * r5.w; + { float nx = r4.w * r6.x + r0.x, ny = r4.w * r6.y + r0.z, nz = r4.w * r6.z + r0.y; r0.x = nx; r0.y = ny; r0.z = nz; } + { float nx = r5.w * r5.x + r0.x, ny = r5.w * r5.z + r0.z, nz = r5.w * r5.y + r0.y; r5.x = nx; r5.y = ny; r5.z = nz; } + r0 = float4(r5.w + r4.w, r5.x + r4.x, r5.z + r4.y, r5.y + r4.z); + r3.z = r0.x + r3.z; + r3.y = r3.z + r3.y; + r3.x = r3.y + r3.x; + r2.w = r3.x + r2.w; + r2.z = r2.w + r2.z; + r2.y = r2.z + r2.y; + r2.x = r2.y + r2.x; + r0.x = 1.0 / r2.x; + float4 oC0; + oC0.x = r0.y * r0.x; + oC0.y = r0.z * r0.x; + oC0.z = r0.w * r0.x; + oC0.w = r1.w; + return oC0; +} + +// --------------------------------------------------------------------------- +// postfx_uberPS (bit-exact): temporal-jitter combine + 3D grade +// LUT + grain + border mask. t0 = graded sharp scene, t1 = packed depth, +// t4 = 3D LUT (identity volume natively unless the game's is resolvable), +// t6 = grain, t7 = DOF-blurred half buffer. +float4 ps_uber(VsOut i) : SV_Target { + float4 r0 = i.r0; + float4 r1 = 0, r2 = 0, r3 = 0, r4 = 0, r5 = 0; + + r2.x = r0.x * c[253].x + c[253].z; + r1.x = c[2].x + r0.y; + r4.y = frac(r1.x); + r2.y = r4.y * c[253].x + c[253].z; + r1.z = r2.x * c[0].y - r2.x; + r1.w = r2.y * c[0].y - r2.y; + r1.x = abs(r2.x) - c[1].w; + r1.y = abs(r2.y) - c[1].w; + r1.x = max(r1.x, c[253].w); + r1.y = max(r1.y, c[253].w); + { + float2 nr = float2(abs(r2.x) - c[0].z, abs(r2.y) - c[0].z); + r2.x = nr.x; r2.y = nr.y; + r2.z = r1.x + r1.y; + } + r2.x = r2.x * c[0].z; + r2.y = r2.y * c[0].z; + r2.z = r2.z * c[0].w; + r1.x = max(r2.x, c[253].w); + r1.y = max(r2.y, c[253].w); + r4.z = r1.z * r1.x; + r4.w = r1.w * r1.y; + r4.x = r0.x; + r3 = t7.SampleLevel(s_lin, r4.xy, 0); + r1 = t0.SampleLevel(s_lin, r4.xy, 0); + r5 = r3 - r1; + r3 = r5 * r3.w + r1; + { + float3 t19; + t19.x = r3.y * c[254].z + c[254].x; + t19.y = r3.x * c[254].w + c[254].y; + t19.z = r3.z * c[254].z + c[254].x; + r1.xyz = t19; + } + r5.y = r4.w + r4.y; + r5.x = r0.x + r0.x; + { + float2 t21 = float2(r5.x + r4.z, r5.y + r4.y); + r0.x = t21.x; r0.y = t21.y; + } + r0.x = r0.x * c[253].y; + r0.y = r0.y * c[253].y; + r0 = t0.SampleLevel(s_lin, r0.xy, 0); + r5 = t4.SampleLevel(s_lin, r1.xyz, 0); + r1 = t6.SampleLevel(s_wrap, r4.xy, 0); + r2.w = t1.SampleLevel(s_pt, r4.xy, 0).z; + r4 = r2.w * c[4]; + r1 = r1 * c[253].x + c[253].z; + r1 = r1 * c[4] + c[255].x; + r5 = r5 - r3; + r5 = r5 * c[3].x + r3; + r1 = r5 * r1; + r3 = r3 - r1; + r1 = r4 * r3 + r1; + r3 = r1 - r0; + r0 = r3 * c[0].x + r0; + r1 = r1 + r0; + r0 = r1 * c[253].y; + r3.x = c[1].x * c[253].y; + r3.y = c[1].y * c[253].y; + r3.z = c[1].z * c[253].y; + r1.w = 0.0; + r1.x = r3.x * r1.x - r0.x; + r1.y = r3.y * r1.y - r0.y; + r1.z = r3.z * r1.z - r0.z; + r0 = r2.z * r1 + r0; + return r0 * c[5]; +} + +// --------------------------------------------------------------------------- +// postfx_basictex_fisheye_opaquePS (bit-exact): lens warp + +// vignette gradient + tint. t0 = uber output, t2 = the 512x2 vignette +// gradient texture (resolved from the game's own asset via fetch words). +float4 ps_fisheye(VsOut i) : SV_Target { + float4 r0 = i.r0; + float4 r1 = 0; + + r1.y = max(c[0].y, c[0].y); + { + float r0x_old = r0.x; + float3 nr; + nr.x = r0x_old * c[255].w + c[255].y; + nr.y = r0.y * c[255].z + c[255].x; + nr.z = r0x_old * c[255].z + c[255].x; + r0.xyz = nr; + } + r1.x = r0.x * r0.x; + r1.z = r0.y * r0.y; + r1.w = r0.z * r0.z; + float ps = c[0].x; + { + float r0_x = r1.x + r1.z; + float r0_w = r1.w + r1.z; + r0.x = r0_x; r0.w = r0_w; + } + r1.x = c[255].z + ps; + r1.z = r0.w * c[1].x; + r1.w = sqrt(abs(r0.x)); + r1.x = r1.w * (-c[1].z) + r1.x; + r0.x = r1.z * r0.w + c[1].y; + { + float r0x_new = r0.x; + float rX = r0.z * r0x_new + c[254].y; + float rY = r0.y * r0x_new + c[254].y; + r0.x = rX; r0.y = rY; + } + r0.xyz = t0.SampleLevel(s_lin, r0.xy, 0).xyz; + r1.xyz = t2.SampleLevel(s_lin, r1.xy, 0).xyz; + r1.xyz = saturate(r1.xyz + c[0].w); + r1.xyz = r1.xyz + c[254].x; + r1.xyz = r1.xyz * c[2].y + c[255].z; + float4 oC0; + oC0.xyz = r1.xyz * r0.xyz; + oC0.w = 1.0; + return oC0; +} diff --git a/src/skate3_native_render.cpp b/src/skate3_native_render.cpp index 09da79c..997feff 100644 --- a/src/skate3_native_render.cpp +++ b/src/skate3_native_render.cpp @@ -751,6 +751,20 @@ extern "C" REX_FUNC(sub_825623F0) { __imp__sub_825623F0(ctx, base); } +// Sk8::FE::FrontEndState_Replay2::TakePhoto(): fires once when the player +// takes a photo (replay editor / photo mission Select). The game then +// renders the shot into the 1152x640 PostFX screenshot target, resolves it, +// and ScreenshotBackEnd::GrabScreenshot CPU-reads the resolved guest memory +// to JPEG-encode it, which is all zeros unless resolve readback is forced +// (the invisible-final-photo bug: an F11 capture showed the grab texture +// 0x04911000 memory fully zero). Arms the photo-grab readback window. +extern "C" REX_FUNC(sub_826147C8) { + if (skate3::native_render::Enabled()) { + skate3::native_scene::OnTakePhoto(); + } + __imp__sub_826147C8(ctx, base); +} + // rw::movie::MovieDecoder::Decode(int, VideoRenderable**, // SubtitleRenderable*): fires per decoded FMV frame while any movie plays // (boot intro logos and all other rw::movie playback). Heartbeat for the diff --git a/src/skate3_native_scene.cpp b/src/skate3_native_scene.cpp index 7ecaf22..7d7341c 100644 --- a/src/skate3_native_scene.cpp +++ b/src/skate3_native_scene.cpp @@ -140,6 +140,41 @@ REXCVAR_DEFINE_BOOL(skate3_native_render_scene_photo_yield, true, "Skate 3", "is fine. Detected by polling the FrontEndManager NIS push-state " "stack (plus the PhotoReplayController heartbeat as a backup).") .lifecycle(rex::cvar::Lifecycle::kHotReload); +REXCVAR_DEFINE_BOOL( + skate3_native_render_scene_photo_readback, true, "Skate 3", + "While a photo flow is active (the photo-mission editor, or a few " + "seconds after any TakePhoto), arm the SDK's forced small-resolve CPU " + "readback (native_render_force_resolve_readback_max_length) and lift " + "emulated-draw suppression. The game takes photos by CPU-reading a " + "resolved 1152x640 PostFX screenshot target from guest memory " + "(ScreenshotBackEnd::GrabScreenshot -> JPEG); photo missions keep the " + "gameplay presence context so no readback path ever ran and the grabbed " + "memory stayed all-zero; the final photo display and the saved photo " + "were BLACK. Costs one synchronous readback per small resolve, photo-flow " + "frames only.") + .lifecycle(rex::cvar::Lifecycle::kHotReload); +REXCVAR_DEFINE_BOOL( + skate3_native_render_scene_photo_native, true, "Skate 3", + "Render the photo-mission photo editor NATIVELY, applying the game's " + "own postfx chain (depth of field / saturation / brightness / contrast " + "/ lens vignette) as exact ucode ports (photo_fx.hlsl: visualfx -> DOF " + "downsample -> tap9dofMotionBlur -> tap9dof -> uber -> fisheye) driven " + "by the LIVE constants the game stages for its own (suppressed) postfx " + "draws each frame. Takes precedence over " + "skate3_native_render_scene_photo_yield; if the pass captures are not " + "yet fresh (first frames of the editor), the scene renders without the " + "effects until they land. Known deltas: the motion-accumulation feed " + "(see photo_native_accum), the bloom pyramid contribution (disabled in " + "every editor capture), grade LUT served as identity, deck AO.") + .lifecycle(rex::cvar::Lifecycle::kHotReload); +REXCVAR_DEFINE_INT32( + skate3_native_render_scene_photo_native_accum, 0, "Skate 3", + "Source for the visualfx pass's quarter-res motion-accumulation input " + "(the game feeds a jitter-accumulated buffer natively unmodeled): 0 = " + "black, 1 = downsampled scene (pre-grade), 2 = downsampled final frame. " + "Compare against the emulated editor (F11 pair) and keep the match.") + .range(0, 2) + .lifecycle(rex::cvar::Lifecycle::kHotReload); REXCVAR_DEFINE_BOOL(skate3_native_render_scene_pause_native, true, "Skate 3", "Keep the NATIVE renderer active through the in-game pause menu " "instead of yielding to the emulated output. Pause is told apart " @@ -568,6 +603,13 @@ REXCVAR_DECLARE(bool, native_render_suppress_emulated_draws); // (pitch > 512, in the mode-2 suppressed band) execute; see // skate3_native_render_scene_menu_rtt_passes. REXCVAR_DECLARE(int32_t, native_render_suppress_mode); +// SDK-level forced resolve readback window (rexglue command_processor.cpp / +// both backends' IssueCopy): when > 0, resolves up to that byte length are +// synchronously read back to CPU-visible guest memory regardless of +// readback_resolve and gameplay state. Armed by UpdatePhotoGrabWindow while +// a photo flow is active; the game CPU-reads the resolved screenshot +// target to build the photo JPEG. +REXCVAR_DECLARE(int32_t, native_render_force_resolve_readback_max_length); // SDK-level async pipeline compilation (rexglue command_processor.cpp): the // d3d12 backend SKIPS draws whose pipeline is still compiling. Forced // synchronous during menu contexts by YieldForMenus so one-shot portrait @@ -2811,6 +2853,120 @@ void OnAddRenderInstance(uint8_t* base, uint32_t instance) { QueueModelMeshes(base, model); } +// ---- Photo-editor postfx capture (FrameScene::PhotoFx / photo_fx.hlsl) ---- +// While a photo flow is active (g_photo_flow_frame, armed by +// UpdatePhotoGrabWindow), the SetPending_AluConstants hook snapshots each +// postfx pass's final PS/VS constant rows plus the device fetch-constant +// shadow (device+0x480, the source SetPending_FetchConstants emits to the +// ring) at the moment the game flushes them, the only reliable point: +// draw-entry bank reads return stale values for these postfx draws (the +// same staleness class the blur capture works around with its c0 gate). +enum PfxPass { + kPfxVisualFx = 0, + kPfxDofDown, + kPfxDofMB, + kPfxDof, + kPfxUber, + kPfxFisheye, + kPfxPassCount +}; +struct PfxCapture { + float ps[32][4]; + float vs[8][4]; + uint32_t fetch[8][6]; + int64_t ps_ns; // PerfClock stamp of the last PS-row capture (0 = never) + bool vs_seen; +}; +PfxCapture g_pfx_cap[kPfxPassCount] = {}; +std::atomic g_photo_flow_frame{false}; +// Defined with the yield helpers below (anonymous namespace); used by the +// BuildFrameScene publish gate (the chain must only run while the editor +// itself is up). +namespace { +const char* PhotoEditorSignal(uint8_t* base); +} // namespace + +// Debug-path classification, cached per shader object (guest render thread +// only, like the blur classifier). -1 = not a photo postfx shader. +int ClassifyPfxShader(uint8_t* base, uint32_t obj) { + if (obj == 0 || !GuestReadableApprox(base, obj)) { + return -1; + } + static std::unordered_map cache; + auto it = cache.find(obj); + if (it != cache.end()) { + return it->second; + } + char text[112] = {}; + for (int k = 0; k < 111; ++k) { + text[k] = char(REX_LOAD_U8(obj + 0x54 + k)); + if (text[k] == '\0') break; + } + int pass = -1; + if (std::strstr(text, "postfx_visualfxPS") != nullptr) { + pass = kPfxVisualFx; + } else if (std::strstr(text, "bloom_dof_motionblur_dof_dowsample") != nullptr) { + // sic: the game's own shader name carries the 'dowsample' typo. + pass = kPfxDofDown; + } else if (std::strstr(text, "bloom_dof_tap9dofMotionBlur") != nullptr) { + pass = kPfxDofMB; + } else if (std::strstr(text, "bloom_dof_tap9dofPS") != nullptr) { + pass = kPfxDof; + } else if (std::strstr(text, "postfx_uberPS") != nullptr) { + pass = kPfxUber; + } else if (std::strstr(text, "postfx_basictex_fisheye") != nullptr) { + pass = kPfxFisheye; + } + if (cache.size() < 4096) { + cache.emplace(obj, pass); + } + return pass; +} + +void CapturePfxConstants(uint8_t* base, uint32_t bank_ptr, uint32_t device, + bool pixel) { + // Shader labels can be swapped in the OnSetShader hook; classify both. + int pass = ClassifyPfxShader(base, g_cur_ps_obj.load(std::memory_order_relaxed)); + if (pass < 0) { + pass = ClassifyPfxShader(base, g_cur_vs_obj.load(std::memory_order_relaxed)); + } + if (pass < 0) { + return; + } + PfxCapture& cap = g_pfx_cap[pass]; + // First-hit diagnostics: which passes ever capture, and on which side. + static uint8_t s_seen[kPfxPassCount][2] = {}; + if (!s_seen[pass][pixel ? 0 : 1]) { + s_seen[pass][pixel ? 0 : 1] = 1; + REXLOG_INFO("native-scene: pfx capture first hit pass={} {} bank={:08X}", pass, + pixel ? "PS" : "VS", bank_ptr); + } + if (pixel) { + for (int r = 0; r < 32; ++r) { + for (int i = 0; i < 4; ++i) { + cap.ps[r][i] = LoadGuestF32(base, bank_ptr + uint32_t(r * 4 + i) * 4); + } + } + if (device != 0 && GuestReadableApprox(base, device + 0x480)) { + for (int s = 0; s < 8; ++s) { + for (int w = 0; w < 6; ++w) { + cap.fetch[s][w] = REX_LOAD_U32(device + 0x480 + uint32_t(s * 6 + w) * 4); + } + } + } + cap.ps_ns = std::chrono::duration_cast( + PerfClock::now().time_since_epoch()) + .count(); + } else { + for (int r = 0; r < 8; ++r) { + for (int i = 0; i < 4; ++i) { + cap.vs[r][i] = LoadGuestF32(base, bank_ptr + uint32_t(r * 4 + i) * 4); + } + } + cap.vs_seen = true; + } +} + void OnVsConstantUpload(uint8_t* base, uint64_t mask, uint32_t bank, uint32_t ptr, uint32_t device) { (void)base; @@ -2823,6 +2979,9 @@ void OnVsConstantUpload(uint8_t* base, uint64_t mask, uint32_t bank, uint32_t pt } if (bank == 0x4400) { g_ps_bank.store(ptr, std::memory_order_relaxed); + if (g_photo_flow_frame.load(std::memory_order_relaxed)) { + CapturePfxConstants(base, ptr, device, /*pixel=*/true); + } return; } if (bank != 0x4000) { @@ -2830,6 +2989,9 @@ void OnVsConstantUpload(uint8_t* base, uint64_t mask, uint32_t bank, uint32_t pt } g_vs_uploads.fetch_add(1, std::memory_order_relaxed); g_vs_bank.store(ptr, std::memory_order_relaxed); + if (g_photo_flow_frame.load(std::memory_order_relaxed)) { + CapturePfxConstants(base, ptr, device, /*pixel=*/false); + } } // Last PhotoReplayController::Update heartbeat, nanoseconds on PerfClock @@ -2845,6 +3007,21 @@ void OnPhotoReplayUpdate() { std::memory_order_relaxed); } +// Last FrontEndState_Replay2::TakePhoto heartbeat: a photo was just taken +// (replay editor or photo mission). The frames that follow render and +// CPU-grab the 1152x640 screenshot target, so the forced-readback window +// (UpdatePhotoGrabWindow) stays armed for a few seconds after it. Same +// thread contract as the photo-replay heartbeat above. +static std::atomic g_take_photo_last_ns{-1}; + +void OnTakePhoto() { + g_take_photo_last_ns.store( + std::chrono::duration_cast( + PerfClock::now().time_since_epoch()) + .count(), + std::memory_order_relaxed); +} + // Last rw::movie::MovieDecoder::Decode heartbeat (FMV playback: intro // logos, any full-motion video). Same contract as the photo-replay // heartbeat above. @@ -4418,6 +4595,29 @@ void OnDrawDone(uint8_t* base, uint32_t func, uint32_t r4, uint32_t r5, uint32_t } } } + // Photo-editor postfx capture FALLBACK at the draw hook: the SetPending + // hook is the preferred site, but if the postfx constants never flush + // through it (or classification misses there), capture here from the + // last-known bank pointers; a draw capture proved the + // visualfx PS rows read correctly from g_ps_bank at this point. + if (flags2d == 0 && SceneEnabled() && + g_photo_flow_frame.load(std::memory_order_relaxed)) { + int pfx_pass = ClassifyPfxShader(base, g_cur_ps_obj.load(std::memory_order_relaxed)); + if (pfx_pass < 0) { + pfx_pass = ClassifyPfxShader(base, g_cur_vs_obj.load(std::memory_order_relaxed)); + } + if (pfx_pass >= 0) { + const uint32_t ps_bank = g_ps_bank.load(std::memory_order_relaxed); + const uint32_t vs_bank = g_vs_bank.load(std::memory_order_relaxed); + const uint32_t dev = g_device.load(std::memory_order_relaxed); + if (ps_bank != 0) { + CapturePfxConstants(base, ps_bank, dev, /*pixel=*/true); + } + if (vs_bank != 0) { + CapturePfxConstants(base, vs_bank, dev, /*pixel=*/false); + } + } + } // UI background blur: while a frontend popup is up, the game appends a // dedicated pass chain after the postfx uber: blur_hBlurPS + blur_vBlurPS // (func-2 inline quads OUTSIDE the 2D phase) then a postfx_basictex @@ -8290,6 +8490,74 @@ void BuildFrameScene(uint8_t* base, const SubmitRecord* records, size_t count) { } } g_ui_blur_seen = false; + // Photo-editor postfx: publish the live pass captures when the photo + // EDITOR itself is up (not the wider TakePhoto readback window: the + // chain must never run over ordinary gameplay frames) and every pass has + // fresh rows (the game stages its postfx constants each frame regardless + // of draw suppression). The vignette and grain fetch words come from the + // fisheye/uber fetch-shadow snapshots (slot 2 / slot 6 per the + // ring-verified bindings). + if (uint8_t* pfx_base = g_guest_base.load(std::memory_order_relaxed); + pfx_base != nullptr && g_photo_flow_frame.load(std::memory_order_relaxed) && + PhotoEditorSignal(pfx_base) != nullptr) { + const int64_t now_ns = std::chrono::duration_cast( + PerfClock::now().time_since_epoch()) + .count(); + bool fresh = true; + for (int p = 0; p < kPfxPassCount; ++p) { + if (g_pfx_cap[p].ps_ns == 0 || now_ns - g_pfx_cap[p].ps_ns > 1'000'000'000 || + !g_pfx_cap[p].vs_seen) { + fresh = false; + } + } + if (!fresh) { + // Name the missing pass(es), once per ~2 s while in the window. + static int64_t s_diag_ns = 0; + if (now_ns - s_diag_ns > 2'000'000'000) { + s_diag_ns = now_ns; + char buf[160]; + int off = 0; + for (int p = 0; p < kPfxPassCount; ++p) { + const int64_t age_ms = + g_pfx_cap[p].ps_ns == 0 ? -1 : (now_ns - g_pfx_cap[p].ps_ns) / 1'000'000; + off += std::snprintf(buf + off, sizeof(buf) - off, " p%d[ps%lldms vs%d]", p, + (long long)age_ms, g_pfx_cap[p].vs_seen ? 1 : 0); + if (off >= int(sizeof(buf)) - 24) break; + } + REXLOG_INFO("native-scene: pfx captures NOT fresh:{}", buf); + } + } + if (fresh) { + scene.photo_fx.valid = true; + for (int p = 0; p < kPfxPassCount; ++p) { + std::memcpy(scene.photo_fx.ps[p], g_pfx_cap[p].ps, + sizeof(scene.photo_fx.ps[p])); + std::memcpy(scene.photo_fx.vs[p], g_pfx_cap[p].vs, + sizeof(scene.photo_fx.vs[p])); + } + std::memcpy(scene.photo_fx.vignette_fetch, g_pfx_cap[kPfxFisheye].fetch[2], + sizeof(scene.photo_fx.vignette_fetch)); + std::memcpy(scene.photo_fx.grain_fetch, g_pfx_cap[kPfxUber].fetch[6], + sizeof(scene.photo_fx.grain_fetch)); + static bool s_pfx_logged = false; + if (!s_pfx_logged) { + s_pfx_logged = true; + REXLOG_INFO( + "native-scene: photo postfx captures LIVE (visualfx c0=({:.3f},{:.3f}," + "{:.3f},{:.3f}) dof c0.x={:.4f} uber c5=({:.3f},{:.3f},{:.3f},{:.3f}) " + "fisheye c1=({:.3f},{:.3f},{:.3f}) vignette=[{:08X} {:08X}] " + "grain=[{:08X} {:08X}])", + g_pfx_cap[kPfxVisualFx].ps[0][0], g_pfx_cap[kPfxVisualFx].ps[0][1], + g_pfx_cap[kPfxVisualFx].ps[0][2], g_pfx_cap[kPfxVisualFx].ps[0][3], + g_pfx_cap[kPfxDof].ps[0][0], g_pfx_cap[kPfxUber].ps[5][0], + g_pfx_cap[kPfxUber].ps[5][1], g_pfx_cap[kPfxUber].ps[5][2], + g_pfx_cap[kPfxUber].ps[5][3], g_pfx_cap[kPfxFisheye].ps[1][0], + g_pfx_cap[kPfxFisheye].ps[1][1], g_pfx_cap[kPfxFisheye].ps[1][2], + scene.photo_fx.vignette_fetch[0], scene.photo_fx.vignette_fetch[1], + scene.photo_fx.grain_fetch[0], scene.photo_fx.grain_fetch[1]); + } + } + } std::memcpy(g_fog_cam, scene.cam_pos, sizeof(g_fog_cam)); g_fog_frame_done = false; g_shadow_frame_done = false; @@ -9007,6 +9275,34 @@ struct RendererState { bool outline_mask_srv_allocated = false; ID3D12PipelineState* pso_outline_mask = nullptr; ID3D12PipelineState* pso_outline_edge = nullptr; + // Photo-editor postfx chain (photo_fx.hlsl: exact ucode ports, see + // FrameScene::PhotoFx). Own root signature: root CBV b0 (the pass's 256 + // captured/baked constant rows) + eight single-SRV tables t0..t7 + three + // static samplers. Intermediates at the game's exact half/quarter sizes + // (the DOF tap offsets are baked for them); the full-res stages run at + // output resolution. pfx_quarter persists across frames (accumulation + // input). All pfx color targets idle in RENDER_TARGET state. + static constexpr uint32_t kPfxHalfW = 576, kPfxHalfH = 320; + static constexpr uint32_t kPfxQuarterW = 288, kPfxQuarterH = 160; + ID3D12RootSignature* pfx_root_sig = nullptr; + // PSO order: depthpack, visualfx, dof_down, dof_mb, dof, uber, fisheye, blit. + ID3D12PipelineState* pfx_pso[8] = {}; + ID3D12Resource* pfx_full[2] = {}; // output-res RGBA8 (visualfx out, uber out) + ID3D12Resource* pfx_half[2] = {}; // 576x320 RGBA8 + ID3D12Resource* pfx_quarter = nullptr; // 288x160 RGBA8 accumulation + ID3D12Resource* pfx_depth = nullptr; // output-res packed-depth RGBA8 + ID3D12Resource* pfx_lut = nullptr; // 32^3 identity grade LUT (RGBA8) + ID3D12Resource* pfx_lut_upload = nullptr; + bool pfx_lut_uploaded = false; + ID3D12Resource* pfx_cb = nullptr; // upload heap, persistently mapped + uint8_t* pfx_cb_ptr = nullptr; + // Fixed SRV slots: 0/1 = pfx_full, 2/3 = pfx_half, 4 = quarter, 5 = + // packed depth, 6 = LUT, 7 = the native MSAA/1x depth resource. + uint32_t pfx_srv[8] = {}; + bool pfx_srv_allocated = false; + uint32_t pfx_width = 0, pfx_height = 0; + bool pfx_ready = false; + bool pfx_failed = false; uint32_t rtv_size = 0; std::unordered_map meshes; // Buffers replaced by re-decode, kept alive until the GPU has finished the @@ -11668,8 +11964,9 @@ bool EnsureHeapsAndRings(const NativeGuestOutputRenderContext& context) { ID3D12Device* device = context.d3d12.device; if (!g_r.rtv_heap) { // Slots 0/1 = guest output / MSAA color; 2+ = APT render-to-texture - // targets. - D3D12_DESCRIPTOR_HEAP_DESC heap{D3D12_DESCRIPTOR_HEAP_TYPE_RTV, 8, + // targets; 5/6 = blur intermediates; 7 = outline mask; 8..13 = the + // photo-editor postfx chain (full x2, half x2, quarter, packed depth). + D3D12_DESCRIPTOR_HEAP_DESC heap{D3D12_DESCRIPTOR_HEAP_TYPE_RTV, 16, D3D12_DESCRIPTOR_HEAP_FLAG_NONE, 0}; if (FAILED(device->CreateDescriptorHeap(&heap, IID_PPV_ARGS(&g_r.rtv_heap)))) { g_r.failed = true; @@ -11727,6 +12024,258 @@ bool EnsureHeapsAndRings(const NativeGuestOutputRenderContext& context) { return true; } +// ---- Photo-editor postfx chain (photo_fx.hlsl) ----------------------------- +// Root signature + eight PSOs + the fixed-size intermediates. Lazy: built on +// the first photo-editor frame (a one-time ~100 ms compile the frozen-scene +// editor absorbs invisibly). Output-sized targets are (re)built per frame by +// the render block on size change. +bool EnsurePhotoFxPipeline(const NativeGuestOutputRenderContext& context) { + if (g_r.pfx_ready) { + return true; + } + if (g_r.pfx_failed) { + return false; + } + ID3D12Device* device = context.d3d12.device; + const auto fail = [&](const char* what) { + REXLOG_ERROR("native-scene: photo postfx pipeline setup failed ({})", what); + g_r.pfx_failed = true; + return false; + }; + // Root signature: root CBV b0 + eight single-SRV tables (t0..t7; each can + // point anywhere in the SRV heap, the established pattern) + samplers. + if (g_r.pfx_root_sig == nullptr) { + D3D12_ROOT_PARAMETER params[9] = {}; + params[0].ParameterType = D3D12_ROOT_PARAMETER_TYPE_CBV; + params[0].Descriptor.ShaderRegister = 0; + params[0].ShaderVisibility = D3D12_SHADER_VISIBILITY_ALL; + D3D12_DESCRIPTOR_RANGE ranges[8] = {}; + const uint32_t regs[8] = {0, 1, 2, 3, 4, 6, 7, 5}; + for (int i = 0; i < 8; ++i) { + ranges[i].RangeType = D3D12_DESCRIPTOR_RANGE_TYPE_SRV; + ranges[i].NumDescriptors = 1; + ranges[i].BaseShaderRegister = regs[i]; + params[1 + i].ParameterType = D3D12_ROOT_PARAMETER_TYPE_DESCRIPTOR_TABLE; + params[1 + i].DescriptorTable.NumDescriptorRanges = 1; + params[1 + i].DescriptorTable.pDescriptorRanges = &ranges[i]; + params[1 + i].ShaderVisibility = D3D12_SHADER_VISIBILITY_PIXEL; + } + D3D12_STATIC_SAMPLER_DESC smp[3] = {}; + smp[0].Filter = D3D12_FILTER_MIN_MAG_MIP_LINEAR; + smp[0].AddressU = D3D12_TEXTURE_ADDRESS_MODE_CLAMP; + smp[0].AddressV = D3D12_TEXTURE_ADDRESS_MODE_CLAMP; + smp[0].AddressW = D3D12_TEXTURE_ADDRESS_MODE_CLAMP; + smp[0].MaxLOD = D3D12_FLOAT32_MAX; + smp[0].ShaderRegister = 0; + smp[0].ShaderVisibility = D3D12_SHADER_VISIBILITY_PIXEL; + smp[1] = smp[0]; + smp[1].Filter = D3D12_FILTER_MIN_MAG_MIP_POINT; // packed depth + smp[1].ShaderRegister = 1; + smp[2] = smp[0]; + smp[2].AddressU = D3D12_TEXTURE_ADDRESS_MODE_WRAP; // grain + smp[2].AddressV = D3D12_TEXTURE_ADDRESS_MODE_WRAP; + smp[2].AddressW = D3D12_TEXTURE_ADDRESS_MODE_WRAP; + smp[2].ShaderRegister = 2; + D3D12_ROOT_SIGNATURE_DESC desc{}; + desc.NumParameters = 9; + desc.pParameters = params; + desc.NumStaticSamplers = 3; + desc.pStaticSamplers = smp; + desc.Flags = D3D12_ROOT_SIGNATURE_FLAG_NONE; + if (!context.d3d12.create_root_signature(context.d3d12.command_processor_user_data, + &desc, &g_r.pfx_root_sig)) { + return fail("root signature"); + } + } + // Shaders + PSOs. Pass order matches RendererState::pfx_pso. + struct Entry { + const char* vs; + const char* ps; + DXGI_FORMAT rtv; + }; + const Entry entries[8] = { + {"vs_raw", "ps_depthpack", DXGI_FORMAT_R8G8B8A8_UNORM}, + {"vs_offset", "ps_visualfx", DXGI_FORMAT_R8G8B8A8_UNORM}, + {"vs_offset", "ps_dof_down", DXGI_FORMAT_R8G8B8A8_UNORM}, + {"vs_offset", "ps_dof_mb", DXGI_FORMAT_R8G8B8A8_UNORM}, + {"vs_offset", "ps_dof", DXGI_FORMAT_R8G8B8A8_UNORM}, + {"vs_raw", "ps_uber", DXGI_FORMAT_R8G8B8A8_UNORM}, + {"vs_scaled", "ps_fisheye", context.d3d12.guest_output_format}, + {"vs_raw", "ps_blit", DXGI_FORMAT_R8G8B8A8_UNORM}, + }; + const D3D_SHADER_MACRO msaa_defines[] = {{"PFX_MSAA", "1"}, {nullptr, nullptr}}; + for (int i = 0; i < 8; ++i) { + ID3DBlob* vs = nullptr; + ID3DBlob* ps = nullptr; + ID3DBlob* errors = nullptr; + const D3D_SHADER_MACRO* defs = (i == 0 && g_r.msaa > 1) ? msaa_defines : nullptr; + if (FAILED(D3DCompile(kPhotoFxShaderSource, sizeof(kPhotoFxShaderSource) - 1, + "photo_fx", defs, nullptr, entries[i].vs, "vs_5_0", 0, 0, + &vs, &errors)) || + FAILED(D3DCompile(kPhotoFxShaderSource, sizeof(kPhotoFxShaderSource) - 1, + "photo_fx", defs, nullptr, entries[i].ps, "ps_5_0", 0, 0, + &ps, &errors))) { + REXLOG_ERROR("native-scene: photo postfx shader compile failed ({}): {}", + entries[i].ps, + errors ? static_cast(errors->GetBufferPointer()) : "?"); + g_r.pfx_failed = true; + return false; + } + D3D12_GRAPHICS_PIPELINE_STATE_DESC pso{}; + pso.pRootSignature = g_r.pfx_root_sig; + pso.VS = {vs->GetBufferPointer(), vs->GetBufferSize()}; + pso.PS = {ps->GetBufferPointer(), ps->GetBufferSize()}; + pso.BlendState.RenderTarget[0].RenderTargetWriteMask = D3D12_COLOR_WRITE_ENABLE_ALL; + pso.SampleMask = UINT_MAX; + pso.RasterizerState.FillMode = D3D12_FILL_MODE_SOLID; + pso.RasterizerState.CullMode = D3D12_CULL_MODE_NONE; + pso.RasterizerState.DepthClipEnable = TRUE; + pso.PrimitiveTopologyType = D3D12_PRIMITIVE_TOPOLOGY_TYPE_TRIANGLE; + pso.NumRenderTargets = 1; + pso.RTVFormats[0] = entries[i].rtv; + pso.SampleDesc.Count = 1; + const HRESULT hr = device->CreateGraphicsPipelineState(&pso, IID_PPV_ARGS(&g_r.pfx_pso[i])); + vs->Release(); + ps->Release(); + if (FAILED(hr)) { + return fail(entries[i].ps); + } + } + // Fixed-size intermediates + the identity grade LUT + the CB ring. + { + D3D12_HEAP_PROPERTIES heap{}; + heap.Type = D3D12_HEAP_TYPE_DEFAULT; + D3D12_RESOURCE_DESC desc{}; + desc.Dimension = D3D12_RESOURCE_DIMENSION_TEXTURE2D; + desc.DepthOrArraySize = 1; + desc.MipLevels = 1; + desc.Format = DXGI_FORMAT_R8G8B8A8_UNORM; + desc.SampleDesc.Count = 1; + desc.Flags = D3D12_RESOURCE_FLAG_ALLOW_RENDER_TARGET; + D3D12_CLEAR_VALUE clear{}; + clear.Format = desc.Format; + struct Fixed { + ID3D12Resource** res; + uint32_t w, h; + }; + const Fixed fixed[3] = { + {&g_r.pfx_half[0], RendererState::kPfxHalfW, RendererState::kPfxHalfH}, + {&g_r.pfx_half[1], RendererState::kPfxHalfW, RendererState::kPfxHalfH}, + {&g_r.pfx_quarter, RendererState::kPfxQuarterW, RendererState::kPfxQuarterH}, + }; + for (const Fixed& f : fixed) { + if (*f.res != nullptr) { + continue; + } + desc.Width = f.w; + desc.Height = f.h; + if (FAILED(device->CreateCommittedResource(&heap, D3D12_HEAP_FLAG_NONE, &desc, + D3D12_RESOURCE_STATE_RENDER_TARGET, + &clear, IID_PPV_ARGS(f.res)))) { + return fail("intermediate target"); + } + } + if (g_r.pfx_lut == nullptr) { + // 32^3 identity grade LUT (the editor captures run with the LUT blend + // weight at 0; identity keeps any treatment that enables it neutral + // instead of garbage). Coordinate mapping mirrors the uber literals: + // u = g*0.96875 + 0.015625, v = r*(-0.96875) + 0.984375 (flipped), + // w = b*0.96875 + 0.015625, so voxel (ix,iy,iz) stores + // r = (0.984375 - v)/0.96875, g = (u - 0.015625)/0.96875, b likewise. + D3D12_RESOURCE_DESC lut{}; + lut.Dimension = D3D12_RESOURCE_DIMENSION_TEXTURE3D; + lut.Width = 32; + lut.Height = 32; + lut.DepthOrArraySize = 32; + lut.MipLevels = 1; + lut.Format = DXGI_FORMAT_R8G8B8A8_UNORM; + lut.SampleDesc.Count = 1; + if (FAILED(device->CreateCommittedResource(&heap, D3D12_HEAP_FLAG_NONE, &lut, + D3D12_RESOURCE_STATE_COPY_DEST, nullptr, + IID_PPV_ARGS(&g_r.pfx_lut)))) { + return fail("LUT"); + } + // Upload: 32 rows of 32 texels x 32 slices, 256-byte row pitch. + const uint32_t row_pitch = 256; + const uint32_t slice_pitch = row_pitch * 32; + g_r.pfx_lut_upload = CreateUploadBuffer(device, size_t(slice_pitch) * 32); + if (g_r.pfx_lut_upload == nullptr) { + return fail("LUT upload"); + } + uint8_t* p = nullptr; + if (FAILED(g_r.pfx_lut_upload->Map(0, nullptr, reinterpret_cast(&p)))) { + return fail("LUT map"); + } + for (uint32_t iz = 0; iz < 32; ++iz) { + for (uint32_t iy = 0; iy < 32; ++iy) { + uint8_t* row = p + size_t(iz) * slice_pitch + size_t(iy) * row_pitch; + const float v = (float(iy) + 0.5f) / 32.0f; + const float w = (float(iz) + 0.5f) / 32.0f; + const float r = (0.984375f - v) / 0.96875f; + const float b = (w - 0.015625f) / 0.96875f; + for (uint32_t ix = 0; ix < 32; ++ix) { + const float u = (float(ix) + 0.5f) / 32.0f; + const float g = (u - 0.015625f) / 0.96875f; + row[ix * 4 + 0] = uint8_t(std::clamp(r, 0.0f, 1.0f) * 255.0f + 0.5f); + row[ix * 4 + 1] = uint8_t(std::clamp(g, 0.0f, 1.0f) * 255.0f + 0.5f); + row[ix * 4 + 2] = uint8_t(std::clamp(b, 0.0f, 1.0f) * 255.0f + 0.5f); + row[ix * 4 + 3] = 255; + } + } + } + g_r.pfx_lut_upload->Unmap(0, nullptr); + } + if (g_r.pfx_cb == nullptr) { + // 8 pass slots x 4 KB x 4 frames in flight. + g_r.pfx_cb = CreateUploadBuffer(device, 8u * 4096u * 4u); + if (g_r.pfx_cb == nullptr || + FAILED(g_r.pfx_cb->Map(0, nullptr, + reinterpret_cast(&g_r.pfx_cb_ptr)))) { + return fail("constant ring"); + } + } + } + if (!g_r.pfx_srv_allocated) { + for (uint32_t& s : g_r.pfx_srv) { + s = g_r.srv_next++; + } + g_r.pfx_srv_allocated = true; + } + // Fixed-target RTVs (heap slots 10/11 = halves, 12 = quarter) + SRVs. + { + const auto make_views = [&](ID3D12Resource* res, uint32_t rtv_slot, + uint32_t srv_slot) { + D3D12_CPU_DESCRIPTOR_HANDLE rtv = g_r.rtv_heap->GetCPUDescriptorHandleForHeapStart(); + rtv.ptr += size_t(rtv_slot) * g_r.rtv_size; + device->CreateRenderTargetView(res, nullptr, rtv); + D3D12_SHADER_RESOURCE_VIEW_DESC srv{}; + srv.Format = DXGI_FORMAT_R8G8B8A8_UNORM; + srv.ViewDimension = D3D12_SRV_DIMENSION_TEXTURE2D; + srv.Shader4ComponentMapping = D3D12_DEFAULT_SHADER_4_COMPONENT_MAPPING; + srv.Texture2D.MipLevels = 1; + D3D12_CPU_DESCRIPTOR_HANDLE slot = g_r.srv_heap->GetCPUDescriptorHandleForHeapStart(); + slot.ptr += size_t(srv_slot) * g_r.srv_size; + device->CreateShaderResourceView(res, &srv, slot); + }; + make_views(g_r.pfx_half[0], 10, g_r.pfx_srv[2]); + make_views(g_r.pfx_half[1], 11, g_r.pfx_srv[3]); + make_views(g_r.pfx_quarter, 12, g_r.pfx_srv[4]); + // LUT SRV (Texture3D). + D3D12_SHADER_RESOURCE_VIEW_DESC srv{}; + srv.Format = DXGI_FORMAT_R8G8B8A8_UNORM; + srv.ViewDimension = D3D12_SRV_DIMENSION_TEXTURE3D; + srv.Shader4ComponentMapping = D3D12_DEFAULT_SHADER_4_COMPONENT_MAPPING; + srv.Texture3D.MipLevels = 1; + D3D12_CPU_DESCRIPTOR_HANDLE slot = g_r.srv_heap->GetCPUDescriptorHandleForHeapStart(); + slot.ptr += size_t(g_r.pfx_srv[6]) * g_r.srv_size; + device->CreateShaderResourceView(g_r.pfx_lut, &srv, slot); + } + g_r.pfx_ready = true; + REXLOG_INFO("native-scene: photo postfx pipeline ready (8 passes, msaa={})", + g_r.msaa); + return true; +} + // Shadow atlas targets + the always-bound b1 receiver constant buffer. bool EnsureShadowResources(const NativeGuestOutputRenderContext& context) { ID3D12Device* device = context.d3d12.device; @@ -13231,11 +13780,9 @@ bool YieldForMovie() { return yield; } -bool YieldForPhotoEditor(uint8_t* base) { - if (!REXCVAR_GET(skate3_native_render_scene_photo_yield)) { - return false; - } - static bool s_in_photo_editor = false; +// The photo-editor detection, shared by the yield and the photo-grab +// readback window: nullptr when inactive, else the name of the signal. +const char* PhotoEditorSignal(uint8_t* base) { const char* signal = nullptr; { constexpr uint32_t kFrontEndManagerPtr = 0x830CFE14; @@ -13263,19 +13810,101 @@ bool YieldForPhotoEditor(uint8_t* base) { signal = "PhotoReplayController heartbeat"; } } + return signal; +} + +// The photo-grab readback window (see skate3_native_render_scene_photo_readback): +// while the photo editor is up OR a TakePhoto fired within the last few +// seconds, (a) arm the SDK's forced small-resolve CPU readback so the +// resolved screenshot target actually lands in guest memory for the CPU +// JPEG encode, and (b) lift emulated-draw suppression so the passes that +// render that target execute even when the native output is active (a +// no-op while the photo yield has the native output inactive anyway). +// Runs every frame from RenderScene regardless of the yield decisions. +void UpdatePhotoGrabWindow(uint8_t* base) { + // The grab target is 1152x640x4 = 0x2D0000 bytes (the same surface the + // SDK's Import-Skater special case reads); the thumb path is smaller. + // Framebuffer-sized resolves (0x384000 at 1280x720) stay excluded. + constexpr int32_t kForceReadbackMaxLength = 0x2D0000; + static bool s_armed = false; + static int32_t s_readback_saved = 0; + static bool s_suppress_saved = false; + bool want = false; + if (REXCVAR_GET(skate3_native_render_scene_photo_readback)) { + want = PhotoEditorSignal(base) != nullptr; + if (!want) { + const int64_t last_ns = g_take_photo_last_ns.load(std::memory_order_relaxed); + if (last_ns >= 0) { + const int64_t now_ns = + std::chrono::duration_cast( + PerfClock::now().time_since_epoch()) + .count(); + want = now_ns - last_ns < 3'000'000'000; + } + } + } + // Same window arms the photo postfx constant capture (CapturePfxConstants + // runs on the guest thread at the SetPending hook). + g_photo_flow_frame.store(want, std::memory_order_relaxed); + if (want && !s_armed) { + s_readback_saved = REXCVAR_GET(native_render_force_resolve_readback_max_length); + if (s_readback_saved <= 0) { + REXCVAR_SET(native_render_force_resolve_readback_max_length, + kForceReadbackMaxLength); + } + s_suppress_saved = REXCVAR_GET(native_render_suppress_emulated_draws); + if (s_suppress_saved) { + REXCVAR_SET(native_render_suppress_emulated_draws, false); + } + REXLOG_INFO( + "native-scene: photo flow - forcing small-resolve CPU readback " + "(the photo grab reads the resolved screenshot target from guest " + "memory){}", + s_suppress_saved ? " + emulated draw suppression OFF" : ""); + s_armed = true; + } else if (!want && s_armed) { + if (s_readback_saved <= 0) { + REXCVAR_SET(native_render_force_resolve_readback_max_length, + s_readback_saved); + } + if (s_suppress_saved) { + REXCVAR_SET(native_render_suppress_emulated_draws, true); + } + REXLOG_INFO( + "native-scene: photo flow ended - resolve readback{} restored", + s_suppress_saved ? " + suppression" : ""); + s_armed = false; + } +} + +bool YieldForPhotoEditor(uint8_t* base) { + // The native photo-fx chain (photo_fx.hlsl, photo_native cvar) takes + // precedence: the editor stays native and RenderScene applies the game's + // postfx as exact ported passes with live-captured constants. + const bool native_fx = REXCVAR_GET(skate3_native_render_scene_photo_native); + if (!native_fx && !REXCVAR_GET(skate3_native_render_scene_photo_yield)) { + return false; + } + static bool s_in_photo_editor = false; + const char* signal = PhotoEditorSignal(base); const bool photo_active = signal != nullptr; if (photo_active != s_in_photo_editor) { s_in_photo_editor = photo_active; if (photo_active) { REXLOG_INFO( - "native-scene: photo editor - yielding to emulated output ({}; the " - "game's postfx applies the photo effects)", + "native-scene: photo editor - {} ({})", + native_fx ? "staying NATIVE (ported postfx chain)" + : "yielding to emulated output (the game's postfx applies " + "the photo effects)", signal); } else { - REXLOG_INFO("native-scene: photo editor closed - native output resumes"); + REXLOG_INFO("native-scene: photo editor closed"); } } - return photo_active; + if (native_fx) { + return false; + } + return photo_active && REXCVAR_GET(skate3_native_render_scene_photo_yield); } // Create-a-skater editor (the 'Edit Skater' screen: skater + garage wall, @@ -13841,10 +14470,16 @@ bool RenderScene(const NativeGuestOutputRenderContext& context, void* /*user_dat // same captured-APT 2D replay as the gameplay HUD, and the backdrop is the // ordinary native world. Loading screens and the boot frontend still // render emulated (complete and correct there). + // The photo-grab readback window runs before any yield decision so it + // updates every frame in every mode (the grab must work whether the photo + // flow renders yielded-emulated or native). + uint8_t* base = g_guest_base.load(std::memory_order_relaxed); + if (base != nullptr) { + UpdatePhotoGrabWindow(base); + } if (YieldForMenus(context)) { return false; } - uint8_t* base = g_guest_base.load(std::memory_order_relaxed); if (base == nullptr) { return false; } @@ -15860,6 +16495,396 @@ bool RenderScene(const NativeGuestOutputRenderContext& context, void* /*user_dat RenderOutlineComposite(context, scene, output_rtv, viewport, scissor); } + // ---- Photo-editor postfx chain (photo_fx.hlsl: exact ucode ports) ---- + // While the photo-mission photo editor is up and every pass's live + // constants were captured this frame, apply the game's own chain over the + // resolved native frame: depth pack -> visualfx (grade/vignette/CoC) -> + // DOF downsample -> tap9dofMotionBlur -> tap9dof -> uber -> fisheye. + if (scene.photo_fx.valid && + REXCVAR_GET(skate3_native_render_scene_photo_native) && + EnsurePhotoFxPipeline(context)) { + ID3D12Device* device = context.d3d12.device; + const auto pfx_to_srv = [&](ID3D12Resource* r) { + context.d3d12.push_transition_barrier(context.d3d12.command_processor_user_data, r, + D3D12_RESOURCE_STATE_RENDER_TARGET, + D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE); + }; + const auto pfx_to_rt = [&](ID3D12Resource* r) { + context.d3d12.push_transition_barrier(context.d3d12.command_processor_user_data, r, + D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE, + D3D12_RESOURCE_STATE_RENDER_TARGET); + }; + const auto pfx_flush = [&] { + context.d3d12.submit_barriers(context.d3d12.command_processor_user_data); + }; + // Output-sized targets (visualfx out, uber out, packed depth). + bool pfx_ok = true; + if (g_r.pfx_width != context.guest_output_width || + g_r.pfx_height != context.guest_output_height || g_r.pfx_full[0] == nullptr) { + ID3D12Resource** res[3] = {&g_r.pfx_full[0], &g_r.pfx_full[1], &g_r.pfx_depth}; + const uint32_t rtv_slots[3] = {8, 9, 13}; + const uint32_t srv_slots[3] = {g_r.pfx_srv[0], g_r.pfx_srv[1], g_r.pfx_srv[5]}; + D3D12_HEAP_PROPERTIES heap{}; + heap.Type = D3D12_HEAP_TYPE_DEFAULT; + D3D12_RESOURCE_DESC desc{}; + desc.Dimension = D3D12_RESOURCE_DIMENSION_TEXTURE2D; + desc.Width = context.guest_output_width; + desc.Height = context.guest_output_height; + desc.DepthOrArraySize = 1; + desc.MipLevels = 1; + desc.Format = DXGI_FORMAT_R8G8B8A8_UNORM; + desc.SampleDesc.Count = 1; + desc.Flags = D3D12_RESOURCE_FLAG_ALLOW_RENDER_TARGET; + D3D12_CLEAR_VALUE clear{}; + clear.Format = desc.Format; + for (int i = 0; i < 3 && pfx_ok; ++i) { + if (*res[i] != nullptr) { + g_r.retired.emplace_back(*res[i], command_processor->GetCurrentSubmission()); + *res[i] = nullptr; + } + if (FAILED(device->CreateCommittedResource(&heap, D3D12_HEAP_FLAG_NONE, &desc, + D3D12_RESOURCE_STATE_RENDER_TARGET, + &clear, IID_PPV_ARGS(res[i])))) { + pfx_ok = false; + break; + } + D3D12_CPU_DESCRIPTOR_HANDLE rtv = g_r.rtv_heap->GetCPUDescriptorHandleForHeapStart(); + rtv.ptr += size_t(rtv_slots[i]) * g_r.rtv_size; + device->CreateRenderTargetView(*res[i], nullptr, rtv); + D3D12_SHADER_RESOURCE_VIEW_DESC srv{}; + srv.Format = desc.Format; + srv.ViewDimension = D3D12_SRV_DIMENSION_TEXTURE2D; + srv.Shader4ComponentMapping = D3D12_DEFAULT_SHADER_4_COMPONENT_MAPPING; + srv.Texture2D.MipLevels = 1; + D3D12_CPU_DESCRIPTOR_HANDLE slot = g_r.srv_heap->GetCPUDescriptorHandleForHeapStart(); + slot.ptr += size_t(srv_slots[i]) * g_r.srv_size; + device->CreateShaderResourceView(*res[i], &srv, slot); + } + if (pfx_ok) { + g_r.pfx_width = context.guest_output_width; + g_r.pfx_height = context.guest_output_height; + } + } + if (pfx_ok) { + // Identity grade-LUT upload (once). + if (!g_r.pfx_lut_uploaded) { + D3D12_TEXTURE_COPY_LOCATION dst{}; + dst.pResource = g_r.pfx_lut; + dst.Type = D3D12_TEXTURE_COPY_TYPE_SUBRESOURCE_INDEX; + dst.SubresourceIndex = 0; + D3D12_TEXTURE_COPY_LOCATION src{}; + src.pResource = g_r.pfx_lut_upload; + src.Type = D3D12_TEXTURE_COPY_TYPE_PLACED_FOOTPRINT; + src.PlacedFootprint.Footprint.Format = DXGI_FORMAT_R8G8B8A8_UNORM; + src.PlacedFootprint.Footprint.Width = 32; + src.PlacedFootprint.Footprint.Height = 32; + src.PlacedFootprint.Footprint.Depth = 32; + src.PlacedFootprint.Footprint.RowPitch = 256; + list.D3DCopyTextureRegion(&dst, 0, 0, 0, &src, nullptr); + context.d3d12.push_transition_barrier( + context.d3d12.command_processor_user_data, g_r.pfx_lut, + D3D12_RESOURCE_STATE_COPY_DEST, D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE); + g_r.pfx_lut_uploaded = true; + } + // Native depth SRV (re-created every photo frame; the depth resource + // is rebuilt on output resize). + { + D3D12_SHADER_RESOURCE_VIEW_DESC sd{}; + sd.Format = DXGI_FORMAT_R32_FLOAT; + sd.Shader4ComponentMapping = D3D12_DEFAULT_SHADER_4_COMPONENT_MAPPING; + if (g_r.msaa > 1) { + sd.ViewDimension = D3D12_SRV_DIMENSION_TEXTURE2DMS; + } else { + sd.ViewDimension = D3D12_SRV_DIMENSION_TEXTURE2D; + sd.Texture2D.MipLevels = 1; + } + D3D12_CPU_DESCRIPTOR_HANDLE slot = g_r.srv_heap->GetCPUDescriptorHandleForHeapStart(); + slot.ptr += size_t(g_r.pfx_srv[7]) * g_r.srv_size; + device->CreateShaderResourceView(g_r.depth, &sd, slot); + } + // Guest-output SRV (the finished native frame = the chain's scene + // input), re-pointed like the blur block does. + { + D3D12_SHADER_RESOURCE_VIEW_DESC srv{}; + srv.Format = context.d3d12.guest_output_format; + srv.ViewDimension = D3D12_SRV_DIMENSION_TEXTURE2D; + srv.Shader4ComponentMapping = D3D12_DEFAULT_SHADER_4_COMPONENT_MAPPING; + srv.Texture2D.MipLevels = 1; + D3D12_CPU_DESCRIPTOR_HANDLE slot = g_r.srv_heap->GetCPUDescriptorHandleForHeapStart(); + slot.ptr += size_t(g_r.output_srv_slot) * g_r.srv_size; + device->CreateShaderResourceView(context.d3d12.guest_output_resource, &srv, slot); + } + // The two static input textures (fetch words captured at the game's + // own uber/fisheye flushes): the 512x2 vignette gradient + the grain. + const GuestTexture* vig = + resolve_2d_texture(scene.photo_fx.vignette_fetch, /*force_inline=*/true); + const GuestTexture* grain = + resolve_2d_texture(scene.photo_fx.grain_fetch, /*force_inline=*/true); + const uint32_t vig_slot = + (vig != nullptr && vig->valid) ? vig->srv_slot : g_r.white.srv_slot; + const uint32_t grain_slot = + (grain != nullptr && grain->valid) ? grain->srv_slot : g_r.white.srv_slot; + + // Baked literal rows c250..c255 per pass (the shader asset footers the + // game loads via PM4 LOAD_ALU_CONSTANT to PS rows 252+; values read + // from capture). + static constexpr float kPfxLiterals[kPfxPassCount][6][4] = { + // visualfx + {{0, 0, 0, 0}, + {0, 0, 0, 0}, + {0, 0, 0, 0}, + {-0.200000003f, 1.0f, 1.41412354f, 2.0f}, + {1.51991853e-05f, 0.99609381f, 0.00389099144f, 0.00392156886f}, + {0.200000003f, 0.300000012f, 0.5f, 1.0f}}, + // dof downsample + {{0, 0, 0, 0}, + {0, 0, 0, 0}, + {0, 0, 0, 0}, + {0, 0, 0, 0}, + {0, 0, 0, 0}, + {0.000868055562f, 0.00156250002f, 0.25f, 0.00100000005f}}, + // tap9dofMotionBlur + {{0, 0, 0, 0}, + {0, 0, 0, 0}, + {0, 0, 0, 0}, + {0, 0, 0, 0}, + {1.0f, 1.5f, 9.99999975e-05f, 0.00392156886f}, + {1.51991853e-05f, 0.99609381f, 0.00389099144f, 0.0f}}, + // tap9dof + {{0, 0, 0, 0}, + {0, 0, 0, 0}, + {0, 0, 0, 0}, + {0, 0, 0, 0}, + {0, 0, 0, 0}, + {0.100000001f, 0, 0, 0}}, + // uber + {{0, 0, 0, 0}, + {0, 0, 0, 0}, + {0, 0, 0, 0}, + {2.0f, 0.5f, -1.0f, 0.0f}, + {0.015625f, 0.984375f, 0.96875f, -0.96875f}, + {1.0f, 0, 0, 0}}, + // fisheye + {{0, 0, 0, 0}, + {0, 0, 0, 0}, + {0, 0, 0, 0}, + {0, 0, 0, 0}, + {-1.0f, 0.5f, 0, 0}, + {-0.5f, -0.888888896f, 1.0f, 1.77777779f}}, + }; + const uint32_t cb_slot_base = uint32_t(frame_number % 4) * 8; + uint32_t cb_slot_next = 0; + const auto fill_cb = [&](int pass) -> D3D12_GPU_VIRTUAL_ADDRESS { + const uint32_t slot = cb_slot_base + (cb_slot_next++); + uint8_t* dst = g_r.pfx_cb_ptr + size_t(slot) * 4096; + std::memset(dst, 0, 4096); + float* rows = reinterpret_cast(dst); + if (pass >= 0) { + std::memcpy(rows, scene.photo_fx.ps[pass], sizeof(scene.photo_fx.ps[pass])); + std::memcpy(rows + 240 * 4, scene.photo_fx.vs[pass], + sizeof(scene.photo_fx.vs[pass])); + std::memcpy(rows + 250 * 4, kPfxLiterals[pass], sizeof(kPfxLiterals[pass])); + } + rows[248 * 4 + 0] = float(context.guest_output_width); + rows[248 * 4 + 1] = float(context.guest_output_height); + return g_r.pfx_cb->GetGPUVirtualAddress() + size_t(slot) * 4096; + }; + const D3D12_GPU_DESCRIPTOR_HANDLE pfx_heap_start = + g_r.srv_heap->GetGPUDescriptorHandleForHeapStart(); + const auto pfx_bind = [&](uint32_t param, uint32_t slot) { + D3D12_GPU_DESCRIPTOR_HANDLE h = pfx_heap_start; + h.ptr += size_t(slot) * g_r.srv_size; + context.d3d12.set_graphics_root_descriptor_table( + context.d3d12.command_processor_user_data, param, h); + }; + // Root parameter map: 1=t0 2=t1 3=t2 4=t3 5=t4 6=t6 7=t7 8=t5. + const auto pfx_bind_all = [&](uint32_t t0, uint32_t t1, uint32_t t2, + uint32_t t3, uint32_t t4, uint32_t t6, + uint32_t t7, uint32_t t5) { + pfx_bind(1, t0); + pfx_bind(2, t1); + pfx_bind(3, t2); + pfx_bind(4, t3); + pfx_bind(5, t4); + pfx_bind(6, t6); + pfx_bind(7, t7); + pfx_bind(8, t5); + }; + const uint32_t W = g_r.white.srv_slot; + D3D12_VIEWPORT half_vp{0.0f, 0.0f, float(RendererState::kPfxHalfW), + float(RendererState::kPfxHalfH), 0.0f, 1.0f}; + D3D12_RECT half_sc{0, 0, LONG(RendererState::kPfxHalfW), + LONG(RendererState::kPfxHalfH)}; + D3D12_VIEWPORT quarter_vp{0.0f, 0.0f, float(RendererState::kPfxQuarterW), + float(RendererState::kPfxQuarterH), 0.0f, 1.0f}; + D3D12_RECT quarter_sc{0, 0, LONG(RendererState::kPfxQuarterW), + LONG(RendererState::kPfxQuarterH)}; + const auto set_rtv = [&](uint32_t rtv_slot) { + D3D12_CPU_DESCRIPTOR_HANDLE rtv = g_r.rtv_heap->GetCPUDescriptorHandleForHeapStart(); + rtv.ptr += size_t(rtv_slot) * g_r.rtv_size; + list.D3DOMSetRenderTargets(1, &rtv, FALSE, nullptr); + }; + list.D3DSetGraphicsRootSignature(g_r.pfx_root_sig); + list.D3DIASetPrimitiveTopology(D3D_PRIMITIVE_TOPOLOGY_TRIANGLELIST); + const int32_t accum_mode = + REXCVAR_GET(skate3_native_render_scene_photo_native_accum); + + // 1) Depth pack: native depth (sample 0) -> the console D24-as-8888 + // layout at output res. + context.d3d12.push_transition_barrier( + context.d3d12.command_processor_user_data, g_r.depth, + D3D12_RESOURCE_STATE_DEPTH_WRITE, D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE); + pfx_flush(); + set_rtv(13); + list.RSSetViewport(viewport); + list.RSSetScissorRect(scissor); + list.D3DSetPipelineState(g_r.pfx_pso[0]); + list.D3DSetGraphicsRootConstantBufferView(0, fill_cb(-1)); + pfx_bind_all(W, W, W, W, g_r.pfx_srv[6], W, W, g_r.pfx_srv[7]); + list.D3DDrawInstanced(3, 1, 0, 0); + context.d3d12.push_transition_barrier( + context.d3d12.command_processor_user_data, g_r.depth, + D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE, D3D12_RESOURCE_STATE_DEPTH_WRITE); + pfx_to_srv(g_r.pfx_depth); + // Scene input -> SRV for the rest of the chain. + context.d3d12.push_transition_barrier( + context.d3d12.command_processor_user_data, context.d3d12.guest_output_resource, + D3D12_RESOURCE_STATE_RENDER_TARGET, D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE); + pfx_flush(); + + // 2) Accumulation input (visualfx t3). Mode 0 = black, 1 = the scene + // downsampled (this frame, the editor scene is frozen). + if (accum_mode == 0) { + D3D12_CPU_DESCRIPTOR_HANDLE rtv = g_r.rtv_heap->GetCPUDescriptorHandleForHeapStart(); + rtv.ptr += size_t(12) * g_r.rtv_size; + const float black[4] = {0, 0, 0, 0}; + list.D3DClearRenderTargetView(rtv, black, 0, nullptr); + } else if (accum_mode == 1) { + set_rtv(12); + list.RSSetViewport(quarter_vp); + list.RSSetScissorRect(quarter_sc); + list.D3DSetPipelineState(g_r.pfx_pso[7]); + list.D3DSetGraphicsRootConstantBufferView(0, fill_cb(-1)); + pfx_bind_all(g_r.output_srv_slot, W, W, W, g_r.pfx_srv[6], W, W, W); + list.D3DDrawInstanced(3, 1, 0, 0); + } + pfx_to_srv(g_r.pfx_quarter); + pfx_flush(); + + // 3) visualfx (full res): scene + depth + accumulation -> pfx_full[0]. + set_rtv(8); + list.RSSetViewport(viewport); + list.RSSetScissorRect(scissor); + list.D3DSetPipelineState(g_r.pfx_pso[1]); + list.D3DSetGraphicsRootConstantBufferView(0, fill_cb(kPfxVisualFx)); + pfx_bind_all(g_r.output_srv_slot, g_r.pfx_srv[5], W, g_r.pfx_srv[4], + g_r.pfx_srv[6], W, W, W); + list.D3DDrawInstanced(3, 1, 0, 0); + pfx_to_srv(g_r.pfx_full[0]); + pfx_flush(); + + // 4) DOF downsample: pfx_full[0] -> pfx_half[0]. + set_rtv(10); + list.RSSetViewport(half_vp); + list.RSSetScissorRect(half_sc); + list.D3DSetPipelineState(g_r.pfx_pso[2]); + list.D3DSetGraphicsRootConstantBufferView(0, fill_cb(kPfxDofDown)); + pfx_bind_all(g_r.pfx_srv[0], W, W, W, g_r.pfx_srv[6], W, W, W); + list.D3DDrawInstanced(3, 1, 0, 0); + pfx_to_srv(g_r.pfx_half[0]); + pfx_flush(); + + // 5) tap9dofMotionBlur: pfx_half[0] + depth -> pfx_half[1]. + set_rtv(11); + list.D3DSetPipelineState(g_r.pfx_pso[3]); + list.D3DSetGraphicsRootConstantBufferView(0, fill_cb(kPfxDofMB)); + pfx_bind_all(g_r.pfx_srv[2], g_r.pfx_srv[5], W, W, g_r.pfx_srv[6], W, W, W); + list.D3DDrawInstanced(3, 1, 0, 0); + pfx_to_srv(g_r.pfx_half[1]); + pfx_to_rt(g_r.pfx_half[0]); + pfx_flush(); + + // 6) tap9dof: pfx_half[1] -> pfx_half[0]. + set_rtv(10); + list.D3DSetPipelineState(g_r.pfx_pso[4]); + list.D3DSetGraphicsRootConstantBufferView(0, fill_cb(kPfxDof)); + pfx_bind_all(g_r.pfx_srv[3], W, W, W, g_r.pfx_srv[6], W, W, W); + list.D3DDrawInstanced(3, 1, 0, 0); + pfx_to_srv(g_r.pfx_half[0]); + pfx_flush(); + + // 7) uber (full res): graded sharp + depth + LUT + grain + blurred + // half -> pfx_full[1]. + set_rtv(9); + list.RSSetViewport(viewport); + list.RSSetScissorRect(scissor); + list.D3DSetPipelineState(g_r.pfx_pso[5]); + list.D3DSetGraphicsRootConstantBufferView(0, fill_cb(kPfxUber)); + pfx_bind_all(g_r.pfx_srv[0], g_r.pfx_srv[5], W, W, g_r.pfx_srv[6], + grain_slot, g_r.pfx_srv[2], W); + list.D3DDrawInstanced(3, 1, 0, 0); + pfx_to_srv(g_r.pfx_full[1]); + // The finished chain replaces the output: back to RT for fisheye. + context.d3d12.push_transition_barrier( + context.d3d12.command_processor_user_data, context.d3d12.guest_output_resource, + D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE, D3D12_RESOURCE_STATE_RENDER_TARGET); + pfx_flush(); + + // 8) fisheye (to screen): lens warp + vignette gradient + tint. + list.D3DOMSetRenderTargets(1, &output_rtv, FALSE, nullptr); + list.D3DSetPipelineState(g_r.pfx_pso[6]); + list.D3DSetGraphicsRootConstantBufferView(0, fill_cb(kPfxFisheye)); + pfx_bind_all(g_r.pfx_srv[1], W, vig_slot, W, g_r.pfx_srv[6], W, W, W); + list.D3DDrawInstanced(3, 1, 0, 0); + + // 9) Accumulation mode 2: next frame's visualfx t3 = the finished + // frame downsampled (pfx_full[1] is still in SRV state here). + if (accum_mode == 2) { + pfx_to_rt(g_r.pfx_quarter); + pfx_flush(); + set_rtv(12); + list.RSSetViewport(quarter_vp); + list.RSSetScissorRect(quarter_sc); + list.D3DSetPipelineState(g_r.pfx_pso[7]); + list.D3DSetGraphicsRootConstantBufferView(0, fill_cb(-1)); + pfx_bind_all(g_r.pfx_srv[1], W, W, W, g_r.pfx_srv[6], W, W, W); + list.D3DDrawInstanced(3, 1, 0, 0); + } + + // Restore steady states (all pfx color targets idle as RENDER_TARGET) + // + the main pass's root bindings for the 2D overlay. + pfx_to_rt(g_r.pfx_full[0]); + pfx_to_rt(g_r.pfx_full[1]); + pfx_to_rt(g_r.pfx_half[0]); + pfx_to_rt(g_r.pfx_half[1]); + if (accum_mode != 2) { + pfx_to_rt(g_r.pfx_quarter); + } + pfx_to_rt(g_r.pfx_depth); + pfx_flush(); + list.RSSetViewport(viewport); + list.RSSetScissorRect(scissor); + list.D3DSetGraphicsRootSignature(g_r.root_signature); + if (g_r.shadow_cb != nullptr) { + const uint32_t cb_offset = + uint32_t(frame_number % RendererState::kShadowCbRegions) * 256u; + list.D3DSetGraphicsRootConstantBufferView( + 6, g_r.shadow_cb->GetGPUVirtualAddress() + cb_offset); + list.D3DSetGraphicsRootConstantBufferView( + 9, g_r.bone_ring->GetGPUVirtualAddress() + bone_region); + } + static bool s_pfx_first = true; + if (s_pfx_first) { + s_pfx_first = false; + REXLOG_INFO( + "native-scene: photo editor postfx chain LIVE (native; accum mode {}, " + "vignette {}, grain {})", + accum_mode, vig_slot == W ? "WHITE-fallback" : "resolved", + grain_slot == W ? "WHITE-fallback" : "resolved"); + } + } + } + // Popup background blur: exact port of the game's blur_hBlur/vBlur + // postfx_basictex chain (see kBlurShaderSource): H blur of the finished // frame into a 1152x640 intermediate, V blur, then a fullscreen bilinear diff --git a/src/skate3_native_scene.h b/src/skate3_native_scene.h index 2b3dc46..5415111 100644 --- a/src/skate3_native_scene.h +++ b/src/skate3_native_scene.h @@ -309,6 +309,20 @@ struct FrameScene { // blue in every capture), refreshed from the guest edge-detect draw when // it runs. Consumed by the outline composite when any item is selected. float outline_color[4] = {0.21569f, 0.64706f, 1.0f, 1.0f}; + // Photo-editor postfx chain (photo_fx.hlsl, exact ucode ports): the live + // PS/VS constant rows of the game's own postfx passes, captured per frame + // at the SetPending_AluConstants hook while a photo-mission photo editor + // is up, plus the fetch words of the two static input textures the chain + // samples (the 512x2 vignette gradient and the grain texture). Pass order: + // visualfx, dof downsample, tap9dofMotionBlur, tap9dof, uber, fisheye. + struct PhotoFx { + bool valid = false; + float ps[6][32][4] = {}; + float vs[6][8][4] = {}; + uint32_t vignette_fetch[6] = {}; + uint32_t grain_fetch[6] = {}; + }; + PhotoFx photo_fx; std::vector items; }; @@ -364,6 +378,11 @@ void OnVsConstantUpload(uint8_t* base, uint64_t mask, uint32_t bank, uint32_t pt // / contrast effects are the game's own postfx chain, which only the // emulated path executes. void OnPhotoReplayUpdate(); +// Called from the Sk8::FE::FrontEndState_Replay2::TakePhoto hook: a photo +// was just taken (replay editor / photo mission). Arms the photo-grab +// forced-readback window for the frames that render and CPU-grab the +// screenshot target. +void OnTakePhoto(); void OnMovieDecode(); void OnMovieFrame(uint8_t* base, uint32_t renderer);