diff --git a/.gitignore b/.gitignore
index 79ddee2722..2a7492b2c4 100644
--- a/.gitignore
+++ b/.gitignore
@@ -33,8 +33,12 @@ src/3rd party/luajit-2/src/host/*.exe
# compressor runtime artifacts
compressor/engine.log
+
+# local build logs
+/build_*.log
compressor/mod/
+compressor/mod.db0
# emacs temp file
\#*\#
-*~
\ No newline at end of file
+*~
diff --git a/gamedata/configs/mod_system_pip_tonemap.ltx b/gamedata/configs/mod_system_pip_tonemap.ltx
new file mode 100644
index 0000000000..7003731bae
--- /dev/null
+++ b/gamedata/configs/mod_system_pip_tonemap.ltx
@@ -0,0 +1,16 @@
+; PiP per-scope SVP tonemapper overrides. Optional per-optic exposure control for the
+; second-viewport render, pushed by zzz_extra_scope_features on aim change. Both keys default
+; to 0 = inherit the main tonemap, so an optic with no entry here behaves byte-identical to stock.
+;
+; s3ds_middle_grey target middle-grey for the scope's own adaptation (lower = darker scope image,
+; the anti-blowout lever for bright-sky / NVG / thermal optics). ~0.05-0.5.
+; s3ds_adapt_speed how fast the scope re-exposes (lower = slower, holds exposure through a muzzle
+; flash or a sky pan instead of snapping bright). ~1-8.
+;
+; Authored game-feel, not spec-sheet data. Seed conservatively per optic and tune in-game with the
+; live console cvars s3ds_middle_grey / s3ds_adapt_speed before committing values here.
+; Example (commented out until tuned):
+;
+; ![some_nvg_scope_section]
+; s3ds_middle_grey = 0.18
+; s3ds_adapt_speed = 3
diff --git a/gamedata/configs/pip_objective_mm.ltx b/gamedata/configs/pip_objective_mm.ltx
new file mode 100644
index 0000000000..684e280cb4
--- /dev/null
+++ b/gamedata/configs/pip_objective_mm.ltx
@@ -0,0 +1,78 @@
+; PiP per-scope objective clear aperture (mm), from manufacturer spec sheets.
+; Feeds the true exit pupil (objective / magnification) for twilight dimming and the
+; eyebox, absolute mm so it is independent of the mesh-relative scope_objective_lens_offset.w.
+; Read directly by zzz_extra_scope_features (plain sections, not a dltx merge target) and
+; pushed on aim change, a section-authored s3ds_objective_mm still wins
+
+[pn23]
+s3ds_objective_mm = 50
+
+[wpn_addon_scope_pu]
+s3ds_objective_mm = 22
+
+[marchf]
+s3ds_objective_mm = 42
+
+; leupold is the Mark 8 CQBSS 1.1-8x24, the earlier 40mm came from a mismatched Mark 4 LR/T
+[leupold]
+s3ds_objective_mm = 24
+
+[mark8_rmr]
+s3ds_objective_mm = 24
+
+[razorhd]
+s3ds_objective_mm = 24
+
+[march_f_shorty]
+s3ds_objective_mm = 24
+
+[march_f_shorty_alt]
+s3ds_objective_mm = 24
+
+[wpn_l96a1]
+s3ds_objective_mm = 50
+
+[wpn_l96a1m]
+s3ds_objective_mm = 50
+
+[c79]
+s3ds_objective_mm = 28
+
+[acog]
+s3ds_objective_mm = 32
+
+[1p29]
+s3ds_objective_mm = 26
+
+[usp1]
+s3ds_objective_mm = 26
+
+[ps01]
+s3ds_objective_mm = 24
+
+[pso2]
+s3ds_objective_mm = 24
+
+[pso1m21]
+s3ds_objective_mm = 24
+
+[pritseldob]
+s3ds_objective_mm = 24
+
+[vulcan]
+s3ds_objective_mm = 48
+
+[wpn_fn2000]
+s3ds_objective_mm = 15
+
+[wpn_addon_scope_po4x34]
+s3ds_objective_mm = 34
+
+[chs]
+s3ds_objective_mm = 20
+
+[wpn_remington700]
+s3ds_objective_mm = 40
+
+[wpn_m98b]
+s3ds_objective_mm = 40
diff --git a/gamedata/configs/text/eng/ui_mm_modded_exes.xml b/gamedata/configs/text/eng/ui_mm_modded_exes.xml
index 8903b49976..54b8cc170c 100644
--- a/gamedata/configs/text/eng/ui_mm_modded_exes.xml
+++ b/gamedata/configs/text/eng/ui_mm_modded_exes.xml
@@ -295,6 +295,56 @@
Fog Max Travel Distance (scope_fog_travel)
+
+
+
+ Picture in Picture
+
+
+ Picture in Picture Mode (r__svpscope)
+
+
+ True scope rendering, a real second render of the scene through the lens. Eyepiece places the scope camera at the ocular lens, Objective at the front lens
+
+
+ Zoom Smoothing Rate (g_zoom_smooth)
+
+
+ Lerp speed of the dynamic zoom transition, higher is faster, 0 is instant stepped
+
+
+ Analog Zoom Steps (g_zoom_analog)
+
+
+ 0 uses the scope's configured zoom steps, higher gives finer continuous zoom across the range
+
+
+ Scope Render Scale [DLSS only] (r__svp_render_scale)
+
+
+ Resolution scale of the scope render, inert until DLSS upscaling is enabled, applies on video restart
+
+
+ DLSS Scope Upscaling [experimental] (r__svp_dlss)
+
+
+ Experimental DLSS-SR path for the scope render, currently a passthrough stub
+
+
+ Scope Debug Overlay (r__scope_debug)
+
+
+ Developer overlay, draws the derived eyepiece, objective and camera lenses in the world
+
+
+ Eyepiece
+
+
+ Objective
+
+
Particles
diff --git a/gamedata/scripts/_imgui_groups.script b/gamedata/scripts/_imgui_groups.script
index ef6dac91a3..5fb808b995 100644
--- a/gamedata/scripts/_imgui_groups.script
+++ b/gamedata/scripts/_imgui_groups.script
@@ -136,7 +136,7 @@ ImGui.Groups.Add("MenuBar")
--- Unique, renders unconditionally
ImGui.Groups.Add("Unique")
-AddUniqueCall(ImGui.Groups.Unique.Render)
+-- Rendered by the engine every frame (ide::OnFrame), frame safe with mt_level_call
--- Debug, provides a shared menu for tooling (inspectors, editors, etc.)
ImGui.Groups.Add("Debug")
diff --git a/gamedata/scripts/options_modded_exes_pip.script b/gamedata/scripts/options_modded_exes_pip.script
new file mode 100644
index 0000000000..b56ff6deda
--- /dev/null
+++ b/gamedata/scripts/options_modded_exes_pip.script
@@ -0,0 +1,64 @@
+--- Picture in Picture scope options tree
+
+-- Import smart constructors
+options_builder.import_into(this)
+
+-- a matched PiP exe registers the PiP cvars; on a non-PiP engine the controls below would read cvars that
+-- do not exist and crash the menu on open. probe one PiP cvar safely (get_variable_bounds returns an empty
+-- table for a missing command) and only expose the controls when the engine actually supports them
+local function pip_engine_present()
+ local ok, b = pcall(function() return get_console():get_variable_bounds("r__svpscope") end)
+ return ok and b ~= nil and b.max ~= nil
+end
+
+local _full = page {
+ { id = "pip" },
+
+ list_enum {
+ id = "r__svpscope",
+ content = {
+ "OFF",
+ "Eyepiece",
+ "Objective"
+ },
+ },
+ track {
+ id = "g_zoom_smooth",
+ def = 12,
+ min = 0,
+ max = 60,
+ step = 1,
+ },
+ track {
+ id = "g_zoom_analog",
+ def = 0,
+ min = 0,
+ max = 200,
+ step = 5,
+ },
+
+ line {},
+
+ track {
+ id = "r__svp_render_scale",
+ def = 1.0,
+ min = 0.4,
+ max = 1.0,
+ step = 0.05,
+ },
+ list_bool { id = "r__svp_dlss" },
+
+ line {},
+
+ track {
+ id = "r__scope_debug",
+ def = 0,
+ min = 0,
+ max = 4,
+ step = 1,
+ },
+
+}
+
+-- expose the controls only on a matched PiP engine; otherwise an empty page so the menu never reads the missing cvars
+PAGE = pip_engine_present() and _full or page { { id = "pip" }, line {} }
diff --git a/gamedata/scripts/options_modded_exes_visual.script b/gamedata/scripts/options_modded_exes_visual.script
index 56c0c96078..9d2ba1283a 100644
--- a/gamedata/scripts/options_modded_exes_visual.script
+++ b/gamedata/scripts/options_modded_exes_visual.script
@@ -4,6 +4,7 @@ local page_ui_hud = options_modded_exes_ui_hud.PAGE
local page_crosshair = options_modded_exes_crosshair.PAGE
local page_hdr10 = options_modded_exes_hdr10.PAGE
local page_3d_scopes = options_modded_exes_3d_scopes.PAGE
+local page_pip = options_modded_exes_pip.PAGE
local page_particles = options_modded_exes_particles.PAGE
local page_graphics = options_modded_exes_graphics.PAGE
@@ -17,6 +18,7 @@ GROUP = group {
page_ui_hud,
page_crosshair,
page_3d_scopes,
+ page_pip,
page_particles,
page_hdr10,
}
\ No newline at end of file
diff --git a/gamedata/scripts/zzz_extra_scope_features.script b/gamedata/scripts/zzz_extra_scope_features.script
new file mode 100644
index 0000000000..2d56e0773d
--- /dev/null
+++ b/gamedata/scripts/zzz_extra_scope_features.script
@@ -0,0 +1,167 @@
+--[[
+ zzz_extra_scope_features (true-PiP)
+
+ Pushes the per-scope objective lens geometry to the engine so the SVP can place the front plane
+ and size the defocus aperture per optic. The scope_objective_lens_offset key (3DSS objective-lens
+ data, x/y lateral, z forward, w radius, all in eyepiece-radius units) is resolved from the active
+ optic's section and written to the scope_objective_lens_offset console command on every aim change.
+
+ Zoom is owned by the engine (curMag drive) and the 3DSS/Godis controllers. This script NEVER
+ touches zoom. On a non-PiP exe the command is unregistered, get_string returns nil, and the whole
+ script bails - stock behaviour, no side effects.
+]]--
+
+-- Taken from weapon_cover_tilt.script
+function IsBinoc(sec)
+ return SYS_GetParam(0, sec, "ammo_class", "") == "ammo_binoc"
+ or SYS_GetParam(0, sec, "class", "") == "WP_BINOC"
+end
+
+local RETRY_EV = "pip_scope_push"
+
+-- read a key from the attached-scope section first, then the weapon section, absent = nil
+local function read_str(scope_sec, weap_sec, key)
+ return (scope_sec and ini_sys:r_string_ex(scope_sec, key)) or (weap_sec and ini_sys:r_string_ex(weap_sec, key))
+end
+local function read_num(scope_sec, weap_sec, key)
+ return (scope_sec and ini_sys:r_float_ex(scope_sec, key)) or (weap_sec and ini_sys:r_float_ex(weap_sec, key))
+end
+
+-- per-scope SVP tonemapper and ballistic zero, absent keys push the safe default
+local function push_aux(scope_sec, weap_sec)
+ exec_console_cmd("s3ds_middle_grey " .. (read_num(scope_sec, weap_sec, "s3ds_middle_grey") or 0))
+ exec_console_cmd("s3ds_adapt_speed " .. (read_num(scope_sec, weap_sec, "s3ds_adapt_speed") or 0))
+ local zero = read_num(scope_sec, weap_sec, "scope_zero_m")
+ exec_console_cmd("g_svp_zero " .. ((zero and zero > 0) and zero or 100))
+end
+
+-- spec-sheet mm rides its own plain-section ltx read directly, a section-authored key still wins
+local mm_ini
+local function read_mm(scope_sec, weap_sec)
+ local mm = read_num(scope_sec, weap_sec, "s3ds_objective_mm")
+ if mm then
+ return mm
+ end
+ mm_ini = mm_ini or ini_file("pip_objective_mm.ltx")
+ return (scope_sec and mm_ini:r_float_ex(scope_sec, "s3ds_objective_mm"))
+ or (weap_sec and mm_ini:r_float_ex(weap_sec, "s3ds_objective_mm"))
+end
+
+-- authored offset (scope then weapon) beats the engine measured detection, returns true when a value landed
+local function push_lens(scope_sec, weap_sec)
+ local off = read_str(scope_sec, weap_sec, "scope_objective_lens_offset")
+ local src = off and "authored"
+ if not off then
+ local det = svp_detected_offset and svp_detected_offset()
+ if det and det ~= "" then off, src = det, "detected" end
+ end
+ if not off then
+ return false
+ end
+ exec_console_cmd("scope_objective_lens_offset " .. off)
+ local obj = read_mm(scope_sec, weap_sec)
+ if not obj then
+ local dmm = svp_detected_obj_mm and svp_detected_obj_mm()
+ if dmm and dmm > 0 then obj = dmm end
+ end
+ exec_console_cmd("s3ds_objective_mm " .. (obj or 0))
+ printf("[SVP-PUSH] sec=%s src=%s off=%s mm=%s", scope_sec or weap_sec or "?", src, off, tostring(obj or 0))
+ return true
+end
+
+-- resolve and push for the live aim, returns true when settled (pushed, alt-cleared or unscoped)
+local function try_push()
+ local item = db.actor and db.actor:active_item()
+ local weapon = item and item:cast_Weapon()
+ if not (item and weapon and weapon:IsZoomed()) then
+ return true
+ end
+ if IsBinoc(item:section()) then
+ return true
+ end
+ local scope_sec = utils_item.get_attached_scope(item)
+ local weap_sec = item:section()
+ push_aux(scope_sec, weap_sec)
+ -- the resolved value wins even on an alt zoom type, gamma analog zoom raises the type on the
+ -- same primary optic right after zoom in and must not stomp the just pushed value
+ if push_lens(scope_sec, weap_sec) then
+ return true
+ end
+ -- genuinely no objective, an alt sight clears the stale primary value, the primary optic keeps
+ -- its last value and retries until the detection cache warms
+ if weapon:GetZoomType() ~= 0 then
+ exec_console_cmd("scope_objective_lens_offset 0,0,0,0")
+ exec_console_cmd("s3ds_objective_mm 0")
+ return true
+ end
+ return false
+end
+
+-- half second cadence so the retry is no busy loop, the cap rides out a slow first load
+-- (a fresh session held the weapon unsettled past 9s) yet ends a sight with nothing to resolve
+local RETRY_STEP_S = 0.5
+local RETRY_MAX = 60
+
+local function push_offset()
+ if try_push() then
+ return
+ end
+ -- detection cache is cold on the zoom-in frame, leave the previous value and re-resolve until it warms
+ printf("[SVP-PUSH] skip kept, retrying the live aim")
+ RemoveTimeEvent(RETRY_EV, "retry")
+ local tries = 0
+ CreateTimeEvent(RETRY_EV, "retry", RETRY_STEP_S, function()
+ tries = tries + 1
+ if try_push() then
+ return true
+ end
+ if tries >= RETRY_MAX then
+ printf("[SVP-PUSH] unresolved after retry, kept")
+ return true
+ end
+ ResetTimeEvent(RETRY_EV, "retry", RETRY_STEP_S)
+ return false
+ end)
+end
+
+local function clear_offset()
+ RemoveTimeEvent(RETRY_EV, "retry")
+ -- the objective/mm are read only while scoped, leave them for the next aim to re-resolve so a
+ -- zoom out never stomps a just pushed value, only the scope tonemapper returns to the default view
+ exec_console_cmd("s3ds_middle_grey 0")
+ exec_console_cmd("s3ds_adapt_speed 0")
+end
+
+-- one-shot per install, resets only the listed cvars and leaves every other setting alone
+local PIP_MIGRATION_VERSION = 20260712
+local function run_migrations()
+ local saved = axr_main.config:r_value("pip", "migration_version", 2, 0)
+ if saved >= PIP_MIGRATION_VERSION then
+ return
+ end
+ if saved < 20260710 then
+ -- 20260710 experimental glass optics ship default-off, clear values saved by older builds
+ exec_console_cmd("r__svp_veiling_glare 0")
+ exec_console_cmd("r__svp_coating 0")
+ exec_console_cmd("r__svp_mirage 0")
+ exec_console_cmd("r__svp_reticle_washout 0")
+ end
+ if saved < 20260712 then
+ -- 20260712 objective fallback re-sourced to the authored-set median
+ exec_console_cmd("r__svp_obj_size 1.0")
+ end
+ axr_main.config:w_value("pip", "migration_version", PIP_MIGRATION_VERSION)
+ axr_main.config:save()
+ printf("pip migration %s applied", PIP_MIGRATION_VERSION)
+end
+
+function on_game_start()
+ if not get_console():get_string("scope_objective_lens_offset") then
+ printf("dev/svp exes not installed - extra scope features inert")
+ return
+ end
+ run_migrations()
+ RegisterScriptCallback("actor_on_weapon_zoom_in", push_offset)
+ RegisterScriptCallback("actor_on_weapon_zoom_type_changed", push_offset)
+ RegisterScriptCallback("actor_on_weapon_zoom_out", clear_offset)
+end
diff --git a/gamedata/shaders/r3/scope_color_write.s b/gamedata/shaders/r3/scope_color_write.s
new file mode 100644
index 0000000000..4d0c751800
--- /dev/null
+++ b/gamedata/shaders/r3/scope_color_write.s
@@ -0,0 +1,20 @@
+function normal(shader, t_base, t_second, t_detail)
+ shader:begin("scope_vertex", "scope_color_write")
+ : zb(true, false)
+ : blend(true, blend.srcalpha, blend.invsrcalpha)
+ : scopelense(3)
+ shader:dx10texture("s_reticle", "$user$reticle")
+ shader:dx10texture("s_image", "$user$viewport2")
+ shader:dx10texture("s_tonemap", "$user$tonemap")
+ shader:dx10texture("s_tonemap_svp", "$user$svp_tonemap")
+ shader:dx10texture("s_heat", "$user$heat")
+ shader:dx10texture("s_distort", "$user$generic1")
+ shader:dx10texture("s_position", "$user$generic2")
+ shader:dx10texture("s_inside", "wpn\\scope_utility\\inside")
+ shader:dx10texture("s_dirt", "wpn\\scope_utility\\dirt")
+ shader:dx10texture("s_reflection", "wpn\\scope_utility\\reflection")
+ shader:dx10texture("s_heat_map", "wpn\\scope_utility\\heat_map")
+ shader:dx10texture("s_svp_rain", "fx\\svp_rain")
+ shader:dx10sampler("smp_base")
+ shader:dx10sampler("smp_rtlinear");
+end
\ No newline at end of file
diff --git a/gamedata/shaders/r3/scope_custom_depth.h b/gamedata/shaders/r3/scope_custom_depth.h
new file mode 100644
index 0000000000..dfdf26a7db
--- /dev/null
+++ b/gamedata/shaders/r3/scope_custom_depth.h
@@ -0,0 +1,34 @@
+/*
+ =====================================================================
+ Addon : Shader 3D Scopes
+ Link : https://www.moddb.com/mods/stalker-anomaly/addons/shader-3d-scopes
+ Authors : LVutner, party_50
+
+ All credit to original authors.
+ =====================================================================
+*/
+
+#include "scope_3dss_common.h"
+
+float scope_custom_depth(float4 hpos) {
+ bool nvg_blur = SETTING(SETTINGS, ST_NVG_BLUR) && floor(shader_param_8.x) != 0 || m_hud_params.x == 0;
+
+ float NO_BLUR = 100.0;
+
+ // true pip under nvg stamps the disc as unwritten sky, the nightvision depth terms
+ // otherwise read the 100m marker as mid range scenery and boost it into washout
+ if (shader_scope_params.w < -1.5 && floor(shader_param_8.x) != 0)
+ return 0.0;
+
+ return nvg_blur
+ ? hpos.z
+ : NO_BLUR;
+}
+
+// true pip clips the far stamp to the glass disc so the housing keeps its
+// real gbuffer depth for weapon dof, nvg and fake pip keep the whole mesh
+float scope_custom_depth(float4 hpos, float2 tc0) {
+ if (shader_scope_params.w < -1.5 && floor(shader_param_8.x) == 0)
+ clip(1.0 - length((tc0 - 0.5) * 2.0));
+ return scope_custom_depth(hpos);
+}
\ No newline at end of file
diff --git a/gamedata/shaders/r3/scope_debug.ps b/gamedata/shaders/r3/scope_debug.ps
new file mode 100644
index 0000000000..5384c3d0fe
Binary files /dev/null and b/gamedata/shaders/r3/scope_debug.ps differ
diff --git a/gamedata/shaders/r3/scope_debug.s b/gamedata/shaders/r3/scope_debug.s
new file mode 100644
index 0000000000..33b6983102
--- /dev/null
+++ b/gamedata/shaders/r3/scope_debug.s
@@ -0,0 +1,15 @@
+function normal(shader, t_base, t_second, t_detail)
+ shader:begin("stub_notransform_t", "scope_debug")
+ : zb(false, false)
+ : blend(true, blend.srcalpha, blend.invsrcalpha)
+ shader:dx10texture("s_3dss_tex", "$user$viewport2$main")
+ shader:dx10texture("s_pip_tex", "$user$viewport2$svp")
+ shader:dx10sampler("smp_base")
+ shader:dx10texture("pp_m", "$user$ssfx_prev_p$main")
+ shader:dx10texture("pp_s", "$user$ssfx_prev_p$svp")
+ shader:dx10texture("mv_m", "$user$ssfx_motion_vectors$main")
+ shader:dx10texture("mv_s", "$user$ssfx_motion_vectors$svp")
+ shader:dx10texture("pf_m", "$user$ssfx_prev_frame$main")
+ shader:dx10texture("pf_s", "$user$ssfx_prev_frame$svp")
+ shader:dx10texture("sm" , "$user$smap_depth")
+end
\ No newline at end of file
diff --git a/gamedata/shaders/r3/scope_defines.h b/gamedata/shaders/r3/scope_defines.h
new file mode 100644
index 0000000000..1babad5d51
--- /dev/null
+++ b/gamedata/shaders/r3/scope_defines.h
@@ -0,0 +1,19 @@
+// DO NOT EDIT, OR INCLUDE IN MODS
+
+#ifndef SCOPE_DEFINES_H
+#define SCOPE_DEFINES_H
+
+
+#define SCOPE_PHASE_GBUFFER 1
+#define SCOPE_PHASE_DEPTHWRITE 2
+#define SCOPE_PHASE_IMAGE 8
+#define SCOPE_PHASE_RETICLE 16
+#define SCOPE_PHASE_CUSTOM_DEPTH 32
+#define SCOPE_PHASE_SHADOW 64
+#define SCOPE_PHASE_LENS 128
+
+// Hacky solution to fix jitter/ring around scope
+#define SCOPE_PHASE_JITTERFIX 256
+
+
+#endif
\ No newline at end of file
diff --git a/gamedata/shaders/r3/scope_depth_write.ps b/gamedata/shaders/r3/scope_depth_write.ps
new file mode 100644
index 0000000000..a4bf7c8390
Binary files /dev/null and b/gamedata/shaders/r3/scope_depth_write.ps differ
diff --git a/gamedata/shaders/r3/scope_depth_write.s b/gamedata/shaders/r3/scope_depth_write.s
new file mode 100644
index 0000000000..4f8ecc9db3
--- /dev/null
+++ b/gamedata/shaders/r3/scope_depth_write.s
@@ -0,0 +1,5 @@
+function normal(shader, t_base, t_second, t_detail)
+ shader:begin("scope_vertex","scope_depth_write")
+ : zb(false, true)
+ : scopelense(2)
+end
diff --git a/gamedata/shaders/r3/scope_vertex.vs b/gamedata/shaders/r3/scope_vertex.vs
new file mode 100644
index 0000000000..98972517b6
--- /dev/null
+++ b/gamedata/shaders/r3/scope_vertex.vs
@@ -0,0 +1,55 @@
+// PIP_COMPAT_PATCH scope_vertex 20260715
+// DO NOT EDIT, OR INCLUDE IN MODS
+
+#include "scope_common.h"
+
+// self-contained TAA jitter so the scope compiles without SSS's screenspace_mvectors.h
+// ssfx_jitter is engine-bound and zero when TAA is off, the formula matches SSS exactly
+#ifndef SSFX_MV_LOADED
+#define SSFX_MV_LOADED
+uniform float4 ssfx_jitter;
+float2 ssfx_taa_jitter(float4 hpos) { return hpos.xy + ssfx_jitter.xy * hpos.w; }
+#endif
+
+struct v_in
+{
+ float4 P : POSITION; // (float,float,float,1)
+ float4 Nh : NORMAL; // (nx,ny,nz,hemi occlusion)
+ float3 T : TANGENT; // (nx,ny,nz)
+ float3 B : BINORMAL; // (nx,ny,nz)
+ float2 tc : TEXCOORD0; // (u,v)
+};
+
+v_out main (v_in v)
+{
+ v.Nh = unpack_D3DCOLOR(v.Nh);
+
+ v_out o;
+
+ o.hpos = mul(m_WVP, v.P);
+
+
+
+ o.tc0 = v.tc.xy;
+
+ o.w_P = mul(m_W, v.P).xyz;
+ o.w_T = mul(m_W, v.T).xyz;
+ o.w_B = mul(m_W, v.B).xyz;
+ o.w_N = mul((float3x3)m_W, unpack_bx2(v.Nh));
+
+ o.v_P = float4(mul(m_WV, v.P).xyz, v.Nh.w );
+ o.v_N = mul((float3x3)m_WV, unpack_bx2(v.Nh));
+
+ o.ssp_jitter = float2(0,0);
+
+ // true pip draws every phase jittered so the main taa resolve treats the lens like any surface
+ if ((scope_phase & SCOPE_PHASE_JITTERFIX) || shader_scope_params.w < -1.5) {
+ // pixel space jitter of this draw, consumers subtract it to read unjittered screen positions
+ o.ssp_jitter = ssfx_jitter.xy * float2(0.5, -0.5) * screen_res.xy;
+
+ // Jitter the output position to ensure no ring around lens
+ o.hpos.xy = ssfx_taa_jitter(o.hpos);
+ }
+
+ return o;
+}
\ No newline at end of file
diff --git a/gamedata/shaders/r3/svp_distort_stamp.ps b/gamedata/shaders/r3/svp_distort_stamp.ps
new file mode 100644
index 0000000000..ce0ea7feb4
Binary files /dev/null and b/gamedata/shaders/r3/svp_distort_stamp.ps differ
diff --git a/gamedata/shaders/r3/svp_distort_stamp.s b/gamedata/shaders/r3/svp_distort_stamp.s
new file mode 100644
index 0000000000..576f9efe6b
--- /dev/null
+++ b/gamedata/shaders/r3/svp_distort_stamp.s
@@ -0,0 +1,6 @@
+-- stamps the distort mask neutral over the composited lens
+function normal(shader, t_base, t_second, t_detail)
+ shader:begin("scope_vertex", "svp_distort_stamp")
+ : zb(true, false)
+ : scopelense(2)
+end
diff --git a/gamedata/shaders/r3/svp_hooks_common.h b/gamedata/shaders/r3/svp_hooks_common.h
new file mode 100644
index 0000000000..07013c079d
--- /dev/null
+++ b/gamedata/shaders/r3/svp_hooks_common.h
@@ -0,0 +1,41 @@
+// svp_hooks_common 20260715 thinhook
+// shared SVP uniform + texture decls for the hook headers and monolith scopes, no shader_scope_params here
+#ifndef SVP_HOOKS_COMMON_INCLUDED
+#define SVP_HOOKS_COMMON_INCLUDED
+
+Texture2D s_tonemap_svp;
+Texture2D s_svp_rain; // objective-glass rain droplet atlas, x=Fade y=NormalY z=NormalX w=TimeOffset
+#ifndef SVP_EXPOSURE_DECLARED
+#define SVP_EXPOSURE_DECLARED
+uniform float4 svp_exposure; // x = 0 off else 2^bias, y = twilight dim, zw = crescent swing side offset
+#endif
+#ifndef SVP_OPTICS_DECLARED
+#define SVP_OPTICS_DECLARED
+uniform float4 svp_optics; // x = 2*ocular_radius/eye_distance, y = true-scale parallax, z = dead lane
+#endif
+#ifndef SVP_CONTROL_DECLARED
+#define SVP_CONTROL_DECLARED
+uniform float4 svp_control; // engine intent, x/y/z = strip parallax shadow/chromatism/nvg blur under true PiP
+#endif
+#ifndef SVP_GLASS_DECLARED
+#define SVP_GLASS_DECLARED
+uniform float4 svp_glass; // engine glass tunables, x = reticle washout, y = field curvature, z = ACOG fiber sun mode
+#endif
+#ifndef SVP_ENV_DECLARED
+#define SVP_ENV_DECLARED
+uniform float4 svp_env; // engine glass environment, x = veiling glare, y = rain intensity
+#endif
+#ifndef SVP_GLASS2_DECLARED
+#define SVP_GLASS2_DECLARED
+uniform float4 svp_glass2; // engine glass optics 2, x = coating, y = heat mirage, w = flat-panel V-crop
+#endif
+#ifndef SVP_GLASS3_DECLARED
+#define SVP_GLASS3_DECLARED
+uniform float4 svp_glass3; // engine glass optics 3, x = sharpen amount, y = field-stop onset, z = sharpen radial falloff, w = sharpen inner crisp radius
+#endif
+#ifndef SVP_GLASS4_DECLARED
+#define SVP_GLASS4_DECLARED
+uniform float4 svp_glass4; // engine glass optics 4, x = nvg bleach roll-off, y = nvg auto-gain, w = shadow swing envelope
+#endif
+
+#endif
diff --git a/gamedata/shaders/r3/svp_hooks_image.h b/gamedata/shaders/r3/svp_hooks_image.h
new file mode 100644
index 0000000000..dd9561ce10
--- /dev/null
+++ b/gamedata/shaders/r3/svp_hooks_image.h
@@ -0,0 +1,208 @@
+// svp_hooks_image 20260715 thinhook
+// relocated true-PiP scope image effects, included by scope_custom_image.h after LCD_RES
+#ifndef SVP_HOOKS_IMAGE_INCLUDED
+#define SVP_HOOKS_IMAGE_INCLUDED
+
+#include "svp_hooks_common.h"
+
+// contrast-adaptive sharpen delta (FidelityFX CAS core, cross taps), added onto the composited
+// sample so chroma and blur stay intact, amp backs off at local contrast so nothing rings
+float3 cas_delta(float2 tc, float amount)
+{
+ float2 ts = screen_res.zw;
+ float3 c = sampleScopeTex(tc).rgb;
+ float3 u = sampleScopeTex(tc + float2(0, -ts.y)).rgb;
+ float3 d = sampleScopeTex(tc + float2(0, ts.y)).rgb;
+ float3 l = sampleScopeTex(tc + float2(-ts.x, 0)).rgb;
+ float3 r = sampleScopeTex(tc + float2(ts.x, 0)).rgb;
+ float3 mn = min(c, min(min(u, d), min(l, r)));
+ float3 mx = max(c, max(max(u, d), max(l, r)));
+ float3 amp = sqrt(saturate(min(mn, 1.0 - mx) / max(mx, 0.001)));
+ float3 w = amp * (-0.2 * amount);
+ return (c + (u + d + l + r) * w) / (1.0 + 4.0 * w) - c;
+}
+
+// true PiP NVG highlight roll-off, bright sources compress toward a phosphor bleach instead of the
+// stock hard clamp to flat white. bleach tint/onset are authored placeholders
+float3 apply_nvg_bleach(float2 tc, float3 img, float bleach, float sens)
+{
+ img = BlackandWhite(img);
+ img = Brightness(img, 0.45, 7);
+ float lum = dot(img, float3(0.299, 0.587, 0.114));
+ float3 compressed = img / (1.0 + img);
+ float roll = saturate((lum * sens - 1.0) * 2.0);
+ float3 tube_bleach = float3(0.85, 1.0, 0.85);
+ img = lerp(saturate(img), max(compressed, tube_bleach * roll), roll * bleach);
+ img = LevelsPass(img);
+ img = Grain1(img, tc);
+ img = Grain2(img, tc);
+ return img;
+}
+
+float svp_img_chroma_power(float CHROMA_POWER, Scope s)
+{
+ // true PiP chromatic aberration grows with magnification and linearly off-axis, the
+ // optical center stays clean like real first-order lateral CA
+ if (shader_scope_params.w < -1.5)
+ {
+ CHROMA_POWER *= clamp(curMag() / max(minMag(), 1.0), 1.0, 3.0);
+ float2 ca_r = (s.tc0.xy - 0.5) * 2.0;
+ CHROMA_POWER *= saturate(length(ca_r));
+ }
+ return CHROMA_POWER;
+}
+
+void svp_img_flat_window(inout float2 scope_tc, Scope s)
+{
+ // true PiP flat screen (binocular), the engine sizes the SVP to the panel aspect so tc0 fills the
+ // panel directly, svp_glass2.w = the exact V-crop (1 = no crop when the SVP matches the panel)
+ if (RETICLE_TYPE == RT_FLAT_SCREEN && shader_scope_params.w < -1.5)
+ {
+ float vcrop = (svp_glass2.w > 0.0001) ? svp_glass2.w : 1.0;
+ scope_tc = float2(s.tc0.x, 0.5 + (s.tc0.y - 0.5) * vcrop);
+ }
+}
+
+void svp_img_fix_flipped_lens(inout float2 scope_tc, Scope s)
+{
+ // auto-correct a mesh-inverted lens: some scope models author the lens V flipped so the world renders
+ // upside down. test the raw mesh UV gradient so the image and reticle always agree
+ if (shader_scope_params.w < -1.5 && svp_env.w > 0.5 && ddy(s.tc0.y) < 0.0)
+ scope_tc.y = 1.0 - scope_tc.y;
+}
+
+void svp_img_thermal_fill(inout float2 scope_tc, Scope s)
+{
+ // true PiP panel fill, tc0 spans the panel exactly, svp_glass2.w = the engine V-crop
+ // (1 = no crop when the SVP is sized to the panel aspect)
+ if (shader_scope_params.w < -1.5)
+ {
+ float hw = (svp_glass2.w > 0.0001) ? svp_glass2.w : 1.0;
+ scope_tc = float2(s.tc0.x, 0.5 + (s.tc0.y - 0.5) * hw);
+ // authored pixelation on the filled frame at the optical mag (engine curMag is
+ // ratio*optical), the fixed sensor grid is the fallback when the MCM toggle is off
+ if (SETTING(SETTINGS, ST_THERMAL_PIXELATION)) {
+ float pixelate = max(curMag() / max(svp_optics.w, 1.0), 1.0);
+ scope_tc = (floor(scope_tc * LCD_RES / pixelate) * pixelate + 0.5) / LCD_RES;
+ }
+ else if (svp_control.w > 0.5)
+ scope_tc = (floor(scope_tc * LCD_RES) + 0.5) / LCD_RES;
+ }
+}
+
+void svp_img_sensor_noise(inout float3 back, float2 scope_tc)
+{
+ // per-cell sensor noise, reseeded ~12Hz
+ if (shader_scope_params.w < -1.5 && svp_control.w > 0.5)
+ {
+ float2 cell = floor(scope_tc * LCD_RES);
+ float n = frac(sin(dot(cell, float2(12.9898, 78.233)) + floor(timers.x * 12.0) * 3.7) * 43758.5453);
+ back *= 0.92 + 0.16 * n;
+ }
+}
+
+void svp_img_thermal_veil(inout float3 back, gbuffer_data gbd)
+{
+ // front-lens veil retired, svp_optics.z is a dead lane, no-op kept for the compat patch call
+}
+
+void svp_img_lcd_mask(inout float3 back)
+{
+ // LCD subpixel mask, the stock lcd_effect look re-enabled deliberately for the panel
+ if (shader_scope_params.w < -1.5 && svp_control.w > 0.5)
+ {
+ int2 px = int2(scope.hpos.xy);
+ float3 m = float3(0.4, 0.4, 0.4);
+ int c = px.x % 3;
+ if (c == 1) m.r = 1; else if (c == 2) m.g = 1; else m.b = 1;
+ if (px.y % 3 == 0) m = float3(0.25, 0.25, 0.25);
+ back *= lerp(float3(1, 1, 1), m * 2.0, 0.6);
+ }
+}
+
+void svp_image_glass_fx(inout float3 back, float2 scope_tc, Scope s)
+{
+ // true PiP contrast-adaptive sharpen, recovers upscale/TAA softness without ringing
+ // svp_glass3.z tapers the sharpen from the disc center to the rim, .w holds an inner crisp zone
+ if (shader_scope_params.w < -1.5 && svp_glass3.x > 0.001)
+ {
+ float sharp_r = length((s.tc0.xy - 0.5) * 2.0);
+ float sharp_knee = saturate((sharp_r - svp_glass3.w) / max(1.0 - svp_glass3.w, 1e-3));
+ float sharp_taper = 1.0 - svp_glass3.z * smoothstep(0.0, 1.0, sharp_knee);
+ back += cas_delta(scope_tc, svp_glass3.x * sharp_taper) * LENS_COLOR;
+ }
+ // true PiP heat mirage, hot distant ground shimmers, engine gates strength by sun heat + magnification
+ if (shader_scope_params.w < -1.5 && svp_glass2.y > 0.001)
+ {
+ gbuffer_data mgb = gbuffer_load_data(scope_tc, scope_tc * screen_res.xy, 0);
+ float farg = smoothstep(35.0, 120.0, mgb.P.z);
+ float ground = saturate((s.tc0.y - 0.45) * 2.2);
+ float amp = svp_glass2.y * farg * ground * 0.006;
+ if (amp > 0.0001)
+ {
+ float wob = sin(s.tc0.x * 34.0 + timers.x * 7.0) * sin(s.tc0.y * 21.0 + timers.x * 5.0);
+ back = sampleScopeTex(scope_tc + float2(0.0, wob * amp)).rgb * LENS_COLOR;
+ }
+ }
+ // true PiP field curvature, the outer field softens like a real non-flat-field scope
+ // flat screens have no eyepiece field, the UV radius would draw an ellipse on the wide panel
+ if (shader_scope_params.w < -1.5 && svp_glass.y > 0.001 && RETICLE_TYPE != RT_FLAT_SCREEN)
+ {
+ float soft = smoothstep(0.65, 1.0, length((s.tc0.xy - 0.5) * 2.0)) * svp_glass.y;
+ // skip the blur sample when the field curvature has no visible contribution
+ if (soft > 0.001)
+ back = lerp(back, blur_sample(scope_tc).rgb * LENS_COLOR, saturate(soft));
+ }
+ // true PiP rain on the objective glass, animated droplets refract the world (SSS hud-raindrop path)
+ if (shader_scope_params.w < -1.5 && svp_env.y > 0.001)
+ {
+ float rain = saturate(svp_env.y * 4.0);
+ float2 luv = s.tc0.xy;
+ // low tiling = big coalesced droplets, not a fine mist; two layers merge into blobs
+ float4 l0 = s_svp_rain.Sample(smp_base, luv * 1.8 + float2(0.0, -timers.x * 0.005));
+ float4 l1 = s_svp_rain.Sample(smp_base, luv * 2.6 + 0.5);
+ float4 d = max(l0, l1);
+ // light rain lands sparse but real drops, the cutoff never fully starves below heavy
+ float mask = saturate((1.0 - frac(d.w + timers.x * 0.35)) * d.x - (1.0 - rain) * 0.85);
+ if (mask > 0.001)
+ {
+ float2 nrm = (float2(d.z, d.y) - 0.5) * 2.0;
+ float3 refr = sampleScopeTex(scope_tc + nrm * 0.07 * mask).rgb * LENS_COLOR;
+ back = lerp(back, refr, saturate(mask * 3.5)) + mask * 0.06;
+ }
+ }
+ // true PiP scope-local eye adaptation, the capture is pre-tonemap and the main pass grades
+ // it with the MAIN exposure, rescale by the scope's own measured exposure
+ if (shader_scope_params.w < -1.5 && svp_exposure.x > 0)
+ {
+ float lm = s_tonemap.Load(int3(0, 0, 0)).x;
+ float ls = s_tonemap_svp.Load(int3(0, 0, 0)).x;
+ // s_tonemap_svp is bound by our packed scope_color_write.s, guard the 0 read so a
+ // foreign loose scope_color_write.s (if one ever wins the VFS) can't floor the image
+ if (lm > 0.0001 && ls > 0.0001)
+ back *= clamp(svp_exposure.x * ls / lm, 0.25, 4.0);
+ }
+ // true PiP exit-pupil twilight dimming, engine-bound (zoom shrinks the exit pupil below the dark-adapted eye)
+ if (shader_scope_params.w < -1.5 && svp_exposure.y > 0)
+ back *= svp_exposure.y;
+ // true PiP veiling glare, off-axis sun scatters off the coatings and washes the image near the sun
+ if (shader_scope_params.w < -1.5 && svp_env.x > 0.001)
+ {
+ float g = saturate(svp_env.x);
+ // the scatter carries the live sun chroma at the calibrated energy, neutral warm when the sun is dark
+ float sun_l = dot(L_sun_color.rgb, float3(0.299, 0.587, 0.114));
+ float3 scat = sun_l > 0.001 ? L_sun_color.rgb * (0.55 / sun_l) : float3(0.6, 0.55, 0.45);
+ back = back * (1.0 - 0.5 * g) + scat * g * 0.85;
+ }
+ // true PiP lens coating, typical multi-coated glass transmission with a faint warm tint (AR reflects blue)
+ if (shader_scope_params.w < -1.5 && svp_glass2.x > 0.001)
+ {
+ float ct = saturate(svp_glass2.x);
+ back *= lerp(float3(1.0, 1.0, 1.0), float3(0.93, 0.915, 0.888), ct);
+ }
+ // lens UV debug: green top / red bottom of the SVP sample coord, a flipped gradient = flipped lens V
+ if (shader_scope_params.w < -1.5 && svp_env.z > 0.5)
+ back = float3(scope_tc.y, 1.0 - scope_tc.y, 0.2);
+}
+
+#endif
diff --git a/gamedata/shaders/r3/svp_hooks_lens.h b/gamedata/shaders/r3/svp_hooks_lens.h
new file mode 100644
index 0000000000..b1687f0dd9
--- /dev/null
+++ b/gamedata/shaders/r3/svp_hooks_lens.h
@@ -0,0 +1,15 @@
+// svp_hooks_lens 20260715 thinhook
+// relocated true-PiP flat-screen lens cull, included by scope_custom_lens.h after scope_3dss_common.h
+#ifndef SVP_HOOKS_LENS_INCLUDED
+#define SVP_HOOKS_LENS_INCLUDED
+
+#include "svp_hooks_common.h"
+
+// a flat screen is a see-through window, not coated glass, drop the whole lens layer under
+// true PiP so the world fills the panel (reflections/specular/dirt/vignette would ellipse it)
+bool svp_lens_flat_window_cull()
+{
+ return RETICLE_TYPE == RT_FLAT_SCREEN && shader_scope_params.w < -1.5;
+}
+
+#endif
diff --git a/gamedata/shaders/r3/svp_hooks_reticle.h b/gamedata/shaders/r3/svp_hooks_reticle.h
new file mode 100644
index 0000000000..f0ef43815f
--- /dev/null
+++ b/gamedata/shaders/r3/svp_hooks_reticle.h
@@ -0,0 +1,55 @@
+// svp_hooks_reticle 20260715 thinhook
+// relocated true-PiP sight-reticle terms, included by scope_custom_reticle.h after scope_3dss_common.h
+#ifndef SVP_HOOKS_RETICLE_INCLUDED
+#define SVP_HOOKS_RETICLE_INCLUDED
+
+#include "svp_hooks_common.h"
+
+bool svp_reticle_kill_chroma()
+{
+ return shader_scope_params.w < -1.5 && svp_control.y > 0.5 && IMAGE_TYPE != IT_THERMAL && IMAGE_TYPE != IT_THERMAL_COLOR;
+}
+
+// ACOG fiber brightness source, sun visibility (fiber gathers sunlight) or scene luminance
+float svp_reticle_acog_fiber(float lum)
+{
+ return (shader_scope_params.w < -1.5 && svp_glass.z > 0.5)
+ ? saturate(dot(L_hemi_color.rgb, float3(0.299, 0.587, 0.114)) * 3.0)
+ : lum;
+}
+
+// Sight reticle. true PiP (w < -1.5) swaps the eye-coupled field for a centered one of the
+// same slope plus a true-scale parallax term (svp_optics.y, ~0.15 mrad at full eye deflection)
+float2 svp_reticle_t_field(Scope S, float2 V_tangent)
+{
+ float2 t_field = V_tangent.xy * mas_scale();
+ if (shader_scope_params.w < -1.5)
+ t_field = -(S.tc0 - 0.5) * (mas_scale() * svp_optics.x) + V_tangent.xy * (mas_scale() * svp_optics.y);
+ return t_field;
+}
+
+float svp_reticle_pip_pin()
+{
+ return (shader_scope_params.w < -1.5) ? 0.0 : 1.0;
+}
+
+// mirror the sample V around the reticle center under true PiP so it matches the auto-flipped world
+// same raw mesh UV test as the image chunk so the two can never disagree
+void svp_reticle_flip(inout float2 reticle_tc, inout float2 reticle_lens_tc, Scope S)
+{
+ if (RETICLE_TYPE != RT_SCREEN && RETICLE_TYPE != RT_FLAT_SCREEN
+ && shader_scope_params.w < -1.5 && svp_glass.w > 0.5 && ddy(S.tc0.y) < 0.0)
+ {
+ reticle_tc.y = 1.0 - reticle_tc.y;
+ reticle_lens_tc.y = 1.0 - reticle_lens_tc.y;
+ }
+}
+
+// true PiP illuminated reticle wash-out, the glow loses contrast against a bright background (black lines are rgb 0, untouched)
+void svp_reticle_washout(inout float4 result, float lum)
+{
+ if (shader_scope_params.w < -1.5 && svp_glass.x > 0.001)
+ result.rgb *= max(0.65, 1.0 - svp_glass.x * (1.0 - lum) * 0.3);
+}
+
+#endif
diff --git a/gamedata/shaders/r3/svp_hooks_shadow.h b/gamedata/shaders/r3/svp_hooks_shadow.h
new file mode 100644
index 0000000000..26ad16ccf3
--- /dev/null
+++ b/gamedata/shaders/r3/svp_hooks_shadow.h
@@ -0,0 +1,47 @@
+// PIP_HOOK svp_hooks_shadow 20260715
+// true pip logic for the 3DSS shadow chunk, rides mod.db0, the compat patch includes it
+
+#ifndef SVP_HOOKS_SHADOW_H
+#define SVP_HOOKS_SHADOW_H
+
+#include "svp_hooks_common.h"
+
+// the swing side slides the pupil center so the shadow enters from the side of the motion
+// and a symmetric ring cannot form at center
+float2 svp_shadow_swing(float2 off)
+{
+ if (shader_scope_params.w < -1.5)
+ off += svp_exposure.zw;
+ return off;
+}
+
+// the parallax crescent rides the swing envelope, calm aim shows none and a hard weapon
+// swing sweeps in a dark crescent that fades back out quickly
+float4 svp_shadow_soften(float4 shadow)
+{
+ if (shader_scope_params.w < -1.5)
+ {
+ shadow.rgb = 0;
+ shadow.a *= saturate((svp_glass4.w - 0.3) / 0.35);
+ }
+ return shadow;
+}
+
+// the ocular field stop, a static rim vignette where the physical field ends
+// onset rides the exit-pupil geometry and the field-curve control widens the edge
+float svp_field_stop_alpha(Scope S)
+{
+ if (shader_scope_params.w < -1.5 && svp_glass.y > 0.001 && RETICLE_TYPE != RT_FLAT_SCREEN)
+ {
+ float onset = svp_glass3.y;
+ float band = (1.0 - onset) * svp_glass.y;
+ if (band > 0.0)
+ {
+ float r = length((S.tc0.xy - 0.5) * 2.0);
+ return saturate((r - onset) / band);
+ }
+ }
+ return 0.0;
+}
+
+#endif
diff --git a/gamedata/shaders/r3/svp_nearblur.ps b/gamedata/shaders/r3/svp_nearblur.ps
new file mode 100644
index 0000000000..14e36fd2fa
Binary files /dev/null and b/gamedata/shaders/r3/svp_nearblur.ps differ
diff --git a/gamedata/shaders/r3/svp_nearblur.s b/gamedata/shaders/r3/svp_nearblur.s
new file mode 100644
index 0000000000..b4ff65ebcb
--- /dev/null
+++ b/gamedata/shaders/r3/svp_nearblur.s
@@ -0,0 +1,8 @@
+function normal(shader, t_base, t_second, t_detail)
+ shader:begin("stub_notransform_t", "svp_nearblur")
+ : zb(false, false)
+ shader:dx10texture("s_nb_image", "$user$svp_nearblur_src")
+ shader:dx10texture("s_nb_pos", "$user$svp_nearblur_pos")
+ shader:dx10sampler("smp_base")
+ shader:dx10sampler("smp_nofilter")
+end
diff --git a/gamedata/shaders/r3/svp_taa_stamp.ps b/gamedata/shaders/r3/svp_taa_stamp.ps
new file mode 100644
index 0000000000..0705602162
Binary files /dev/null and b/gamedata/shaders/r3/svp_taa_stamp.ps differ
diff --git a/gamedata/shaders/r3/svp_taa_stamp.s b/gamedata/shaders/r3/svp_taa_stamp.s
new file mode 100644
index 0000000000..94f98f57f2
--- /dev/null
+++ b/gamedata/shaders/r3/svp_taa_stamp.s
@@ -0,0 +1,6 @@
+-- stamps taa mask alpha 1 over the composited lens so the main resolve skips it
+function normal(shader, t_base, t_second, t_detail)
+ shader:begin("scope_vertex", "svp_taa_stamp")
+ : zb(false, false)
+ : scopelense(2)
+end
diff --git a/gamedata/textures/fx/svp_rain.dds b/gamedata/textures/fx/svp_rain.dds
new file mode 100644
index 0000000000..6f5d4f2ae6
Binary files /dev/null and b/gamedata/textures/fx/svp_rain.dds differ
diff --git a/src/Include/xrRender/Kinematics.h b/src/Include/xrRender/Kinematics.h
index 9d232618db..2673470026 100644
--- a/src/Include/xrRender/Kinematics.h
+++ b/src/Include/xrRender/Kinematics.h
@@ -20,6 +20,22 @@ using ISpatialShared = intrusive_ptr;
// 10 fps
#define UCalc_Interval (u32(100))
+// pip measured lens geometry for one visual, bind-pose model space, metres
+// filled by CKinematics::GetLensDetection, ok == false when no fittable lens
+struct SLensDetection
+{
+ Fvector eye_center;
+ Fvector eye_normal;
+ float eye_radius;
+ Fvector obj_center;
+ float obj_radius;
+ bool has_objective;
+ Fvector4 offset; // scope_objective_lens_offset x,y,z,w in eyepiece-radius units
+ float mm; // s3ds_objective_mm 2000 x obj_radius, 0 when no objective
+ int source; // 0 lens verts, 1 near bone, 2 tube march objective
+ bool ok;
+};
+
class IKinematics
{
public:
@@ -46,6 +62,9 @@ class IKinematics
const Fvector& dir, u16 bone_id) = 0;
virtual void EnumBoneVertices(SEnumVerticesCallback& C, u16 bone_id) = 0;
+ // pip run-or-cached measured lens fit for this visual, false for non-skinned or no lens
+ virtual bool GetLensDetection(SLensDetection& out) { return false; }
+
// Low level interface
virtual u16 _BCL LL_BoneID(LPCSTR B) = 0;
virtual u16 _BCL LL_BoneID(const shared_str& B) = 0;
diff --git a/src/Include/xrRender/ParticleCustom.h b/src/Include/xrRender/ParticleCustom.h
index 12d270edc2..09bf8408e6 100644
--- a/src/Include/xrRender/ParticleCustom.h
+++ b/src/Include/xrRender/ParticleCustom.h
@@ -25,6 +25,9 @@ class IParticleCustom
virtual const shared_str Name() =0;
virtual void SetHudMode(BOOL b) =0;
virtual BOOL GetHudMode() =0;
+ // pip marks the held weapon's own muzzle fx so scope-pass drops cannot catch other hud particles
+ virtual void SetWeaponFX(BOOL b) { ; }
+ virtual BOOL GetWeaponFX() { return FALSE; }
virtual void SetLiveUpdate(BOOL b) =0;
virtual BOOL GetLiveUpdate() =0;
};
diff --git a/src/Layers/xrRender/Blender_Recorder_StandartBinding.cpp b/src/Layers/xrRender/Blender_Recorder_StandartBinding.cpp
index 6eaf600966..4a4d2c38a8 100644
--- a/src/Layers/xrRender/Blender_Recorder_StandartBinding.cpp
+++ b/src/Layers/xrRender/Blender_Recorder_StandartBinding.cpp
@@ -15,6 +15,8 @@
#include "dxRenderDeviceRender.h"
+#include "svp_constants.h"
+
// matrices
#define BIND_DECLARE(xf) \
class cl_xform_##xf : public R_constant_setup { virtual void setup (R_constant* C) { RCache.xforms.set_c_##xf (C); } }; \
@@ -396,12 +398,44 @@ extern Fvector4 ps_s3ds_param_1;
extern Fvector4 ps_s3ds_param_2;
extern Fvector4 ps_s3ds_param_3;
extern Fvector4 ps_s3ds_param_4;
+extern float g_pip_scope_magnification;
+extern float g_pip_scope_min_mag;
+extern float g_pip_scope_max_mag;
+extern int g_svp_crescent;
static class s3ds_param_1 : public R_constant_setup
{
virtual void setup(R_constant* C)
{
- RCache.set_c(C, ps_s3ds_param_1.x, ps_s3ds_param_1.y, ps_s3ds_param_1.z, ps_s3ds_param_1.w);
+ // the crescent swing envelope runs only when g_svp_crescent is on, off (default) the 3DSS
+ // exit-pupil value passes through raw so a non-crescent pip scope stays stock 3DSS faithful
+ float z = ps_s3ds_param_1.z;
+ const bool scoped = Device.true_pip_on && Device.m_SecondViewport.IsSVPActive();
+ if (g_svp_crescent && scoped && z > 0.001f)
+ {
+ // aggressiveness tracks absolute magnification, loose at 1x and snug at high power
+ float t = (g_pip_scope_magnification > 0.01f) ? (g_pip_scope_magnification - 1.f) / 7.f : 0.f;
+ clamp(t, 0.f, 1.f);
+ const float snug = 1.3f + (0.55f - 1.3f) * t;
+ // geometry snaps snug early and holds through the drain, the alpha fade in the
+ // shader finishes before the pupil reopens so the crescent never pops out
+ float gp = Device.m_SecondViewport.svp_shadow_gain * 4.f;
+ clamp(gp, 0.f, 1.f);
+ z *= 3.0f + (snug - 3.0f) * gp;
+ if (z < 0.08f) z = 0.08f;
+ }
+ // pip execution proof, at crescent 0 final tracks raw so the ledger sees the passthrough
+ if (ps_r__svp_cop_diag && scoped)
+ {
+ static u32 s_pupil_ms = 0;
+ if (Device.dwTimeGlobal - s_pupil_ms > 1000)
+ {
+ s_pupil_ms = Device.dwTimeGlobal;
+ PipMsg("[SVP-PUPIL] crescent=%d raw=%.3f final=%.3f gain=%.2f",
+ g_svp_crescent, ps_s3ds_param_1.z, z, Device.m_SecondViewport.svp_shadow_gain);
+ }
+ }
+ RCache.set_c(C, ps_s3ds_param_1.x, ps_s3ds_param_1.y, z, ps_s3ds_param_1.w);
}
} s3ds_param_1;
@@ -409,7 +443,20 @@ static class s3ds_param_2 : public R_constant_setup
{
virtual void setup(R_constant* C)
{
- RCache.set_c(C, ps_s3ds_param_2.x, ps_s3ds_param_2.y, ps_s3ds_param_2.z, ps_s3ds_param_2.w);
+ // r__svp_clean_optics normalizes the mas_scale fractional to neutral 1.0 while the svp renders,
+ // hip fire keeps the authored payload, zoom_factor + see-through scopes stay bit-exact
+ float w = ps_s3ds_param_2.w;
+ const bool see_through = (int(ps_s3ds_param_4.w) & (1 << 2)) != 0;
+ if (ps_r__svp_clean_optics && Device.true_pip_on && Device.m_SecondViewport.IsSVPActive()
+ && ps_s3ds_param_3.x <= 1.5f && !see_through)
+ {
+ // only a genuine mas payload renormalizes, a clean zoom factor stays bit-exact for
+ // third-party shaders that threshold or cast this value
+ const float frac = fmodf(w, 0.01f);
+ if (frac > 0.0005f && frac < 0.0095f)
+ w = w - frac + 0.001f;
+ }
+ RCache.set_c(C, ps_s3ds_param_2.x, ps_s3ds_param_2.y, ps_s3ds_param_2.z, w);
}
} s3ds_param_2;
@@ -864,14 +911,35 @@ static class dev_param_8 : public R_constant_setup
{
virtual void setup(R_constant* C)
{
- RCache.set_c(C, ps_dev_param_8.x, ps_dev_param_8.y, ps_dev_param_8.z, ps_dev_param_8.w);
+ float x = ps_dev_param_8.x;
+ // beefs nvg packs the tube layout in the x fraction, the disc pass and the svp render
+ // pass both read an offset single tube as centered so encode and decode agree on the disc
+ if (Device.m_SecondViewport.svp_nvg_disc_pass
+ || (Device.true_pip_on && Device.m_SecondViewport.m_render_pass_is_svp))
+ {
+ const float fr = x - floorf(x);
+ if (_abs(fr - 0.11f) < 0.005f || _abs(fr - 0.12f) < 0.005f)
+ x = floorf(x) + 0.10f;
+ }
+ RCache.set_c(C, x, ps_dev_param_8.y, ps_dev_param_8.z, ps_dev_param_8.w);
}
} dev_param_8;
+// pip sss weapon dof while scoped rides r__svp_wpn_dof, 0 zeroes the constants
+static bool svp_wpn_dof_off()
+{
+ return !ps_r__svp_wpn_dof && Device.true_pip_on && Device.m_SecondViewport.IsSVPActive();
+}
+
static class ssfx_wpn_dof_1 : public R_constant_setup
{
virtual void setup(R_constant* C)
{
+ if (svp_wpn_dof_off())
+ {
+ RCache.set_c(C, 0.f, 0.f, 0.f, 0.f);
+ return;
+ }
RCache.set_c(C, ps_ssfx_wpn_dof_1.x, ps_ssfx_wpn_dof_1.y, ps_ssfx_wpn_dof_1.z, ps_ssfx_wpn_dof_1.w);
}
} ssfx_wpn_dof_1;
@@ -880,6 +948,11 @@ static class ssfx_wpn_dof_2 : public R_constant_setup
{
virtual void setup(R_constant* C)
{
+ if (svp_wpn_dof_off())
+ {
+ RCache.set_c(C, 0.f, 0, 0, 0);
+ return;
+ }
RCache.set_c(C, ps_ssfx_wpn_dof_2, 0, 0, 0);
}
} ssfx_wpn_dof_2;
@@ -1100,14 +1173,6 @@ static class ssfx_hud_hemi : public R_constant_setup
}
} ssfx_hud_hemi;
-static class ssfx_issvp : public R_constant_setup
-{
- virtual void setup(R_constant* C)
- {
- RCache.set_c(C, Device.m_SecondViewport.IsSVPFrame(), 0, 0, 0);
- }
-} ssfx_issvp;
-
static class ssfx_bloom_1 : public R_constant_setup
{
virtual void setup(R_constant* C)
@@ -1162,9 +1227,11 @@ static class ssfx_jitter : public R_constant_setup
float JitterY = 0;
#if defined(USE_DX11)
- if (ps_ssfx_taa.x > 0 && RImplementation.o.ssfx_taa)
+ // the svp is rendered unjittered (svp jitter only under r__svp_dlss), so skip the main jitter
+ // on the svp pass or its gbuffer decode over-corrects a jitter that was never applied
+ if (ps_ssfx_taa.x > 0 && RImplementation.o.ssfx_taa && !(Device.true_pip_on && Device.m_SecondViewport.m_render_pass_is_svp))
{
- static Fvector2 TAA_Offset[4] =
+ static Fvector2 TAA_Offset[4] =
{
{ 0.0f, -1.0f },
{ -1.0f, 0.0f },
@@ -1422,6 +1489,8 @@ void CBlender_Compile::SetMapping()
#endif
r_Constant("screen_res", &binder_screen_res);
+ // beef nvg rework declares this alias, unbound it reads (0,0) and its depth taps collapse to texel 0
+ r_Constant("screen_res_real", &binder_screen_res);
r_Constant("ogse_c_screen", &binder_screen_params);
r_Constant("near_far_plane", &binder_near_far_plane);
// misc
@@ -1462,7 +1531,6 @@ void CBlender_Compile::SetMapping()
r_Constant("ssfx_bloom_1", &ssfx_bloom_1);
r_Constant("ssfx_bloom_2", &ssfx_bloom_2);
- r_Constant("ssfx_issvp", &ssfx_issvp);
r_Constant("ssfx_hud_hemi", &ssfx_hud_hemi);
r_Constant("ssfx_il_setup", &ssfx_il);
r_Constant("ssfx_il_setup2", &ssfx_il_setup1);
@@ -1515,6 +1583,7 @@ void CBlender_Compile::SetMapping()
r_Constant("s3ds_param_2", &s3ds_param_2);
r_Constant("s3ds_param_3", &s3ds_param_3);
r_Constant("s3ds_param_4", &s3ds_param_4);
+ RegisterSvpConstants(*this); // pip binders and their names live in svp_constants.cpp
// crookr
r_Constant("fakescope_params1", &binder_fakescope_params);
diff --git a/src/Layers/xrRender/Debug/dxPixEventWrapper.h b/src/Layers/xrRender/Debug/dxPixEventWrapper.h
index ba3b99c31a..4b87551c8b 100644
--- a/src/Layers/xrRender/Debug/dxPixEventWrapper.h
+++ b/src/Layers/xrRender/Debug/dxPixEventWrapper.h
@@ -18,4 +18,9 @@ class dxPixEventWrapper
#endif // DEBUG
+// PIX_EVENT_F is implemented only on DX11/R4 (dx10EventWrapper.h), no-op here so shared files still build
+#ifndef PIX_EVENT_F
+#define PIX_EVENT_F(...) {;}
+#endif
+
#endif // dxPixEventWrapper_included
diff --git a/src/Layers/xrRender/Debug/renderdoc_app.h b/src/Layers/xrRender/Debug/renderdoc_app.h
new file mode 100644
index 0000000000..3cee3bd486
--- /dev/null
+++ b/src/Layers/xrRender/Debug/renderdoc_app.h
@@ -0,0 +1,875 @@
+/******************************************************************************
+ * The MIT License (MIT)
+ *
+ * Copyright (c) 2015-2026 Baldur Karlsson
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ ******************************************************************************/
+
+#pragma once
+
+//////////////////////////////////////////////////////////////////////////////////////////////////
+//
+// Documentation for the API is available at https://renderdoc.org/docs/in_application_api.html
+//
+
+#if !defined(RENDERDOC_NO_STDINT)
+#include
+#endif
+
+#if defined(WIN32) || defined(__WIN32__) || defined(_WIN32) || defined(_MSC_VER)
+#define RENDERDOC_CC __cdecl
+#elif defined(__linux__) || defined(__FreeBSD__) || defined(__sun__) || defined(__OpenBSD__)
+#define RENDERDOC_CC
+#elif defined(__APPLE__)
+#define RENDERDOC_CC
+#else
+#error "Unknown platform"
+#endif
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+//////////////////////////////////////////////////////////////////////////////////////////////////
+// Constants not used directly in below API
+
+// This is a GUID/magic value used for when applications pass a path where shader debug
+// information can be found to match up with a stripped shader.
+// the define can be used like so: const GUID RENDERDOC_ShaderDebugMagicValue =
+// RENDERDOC_ShaderDebugMagicValue_value
+#define RENDERDOC_ShaderDebugMagicValue_struct \
+ { \
+ 0xeab25520, 0x6670, 0x4865, 0x84, 0x29, 0x6c, 0x8, 0x51, 0x54, 0x00, 0xff \
+ }
+
+// as an alternative when you want a byte array (assuming x86 endianness):
+#define RENDERDOC_ShaderDebugMagicValue_bytearray \
+ { \
+ 0x20, 0x55, 0xb2, 0xea, 0x70, 0x66, 0x65, 0x48, 0x84, 0x29, 0x6c, 0x8, 0x51, 0x54, 0x00, 0xff \
+ }
+
+// truncated version when only a uint64_t is available (e.g. Vulkan tags):
+#define RENDERDOC_ShaderDebugMagicValue_truncated 0x48656670eab25520ULL
+
+// this is a magic value for vulkan user tags to indicate which dispatchable API objects are which
+// for object annotations
+#define RENDERDOC_APIObjectAnnotationHelper 0xfbb3b337b664d0adULL
+
+//////////////////////////////////////////////////////////////////////////////////////////////////
+// RenderDoc capture options
+//
+
+typedef enum RENDERDOC_CaptureOption
+{
+ // Allow the application to enable vsync
+ //
+ // Default - enabled
+ //
+ // 1 - The application can enable or disable vsync at will
+ // 0 - vsync is force disabled
+ eRENDERDOC_Option_AllowVSync = 0,
+
+ // Allow the application to enable fullscreen
+ //
+ // Default - enabled
+ //
+ // 1 - The application can enable or disable fullscreen at will
+ // 0 - fullscreen is force disabled
+ eRENDERDOC_Option_AllowFullscreen = 1,
+
+ // Record API debugging events and messages
+ //
+ // Default - disabled
+ //
+ // 1 - Enable built-in API debugging features and records the results into
+ // the capture, which is matched up with events on replay
+ // 0 - no API debugging is forcibly enabled
+ eRENDERDOC_Option_APIValidation = 2,
+ eRENDERDOC_Option_DebugDeviceMode = 2, // deprecated name of this enum
+
+ // Capture CPU callstacks for API events
+ //
+ // Default - disabled
+ //
+ // 1 - Enables capturing of callstacks
+ // 0 - no callstacks are captured
+ eRENDERDOC_Option_CaptureCallstacks = 3,
+
+ // When capturing CPU callstacks, only capture them from actions.
+ // This option does nothing without the above option being enabled
+ //
+ // Default - disabled
+ //
+ // 1 - Only captures callstacks for actions.
+ // Ignored if CaptureCallstacks is disabled
+ // 0 - Callstacks, if enabled, are captured for every event.
+ eRENDERDOC_Option_CaptureCallstacksOnlyDraws = 4,
+ eRENDERDOC_Option_CaptureCallstacksOnlyActions = 4,
+
+ // Specify a delay in seconds to wait for a debugger to attach, after
+ // creating or injecting into a process, before continuing to allow it to run.
+ //
+ // 0 indicates no delay, and the process will run immediately after injection
+ //
+ // Default - 0 seconds
+ //
+ eRENDERDOC_Option_DelayForDebugger = 5,
+
+ // Verify buffer access. This includes checking the memory returned by a Map() call to
+ // detect any out-of-bounds modification, as well as initialising buffers with undefined contents
+ // to a marker value to catch use of uninitialised memory.
+ //
+ // NOTE: This option is only valid for OpenGL and D3D11. Explicit APIs such as D3D12 and Vulkan do
+ // not do the same kind of interception & checking and undefined contents are really undefined.
+ //
+ // Default - disabled
+ //
+ // 1 - Verify buffer access
+ // 0 - No verification is performed, and overwriting bounds may cause crashes or corruption in
+ // RenderDoc.
+ eRENDERDOC_Option_VerifyBufferAccess = 6,
+
+ // The old name for eRENDERDOC_Option_VerifyBufferAccess was eRENDERDOC_Option_VerifyMapWrites.
+ // This option now controls the filling of uninitialised buffers with 0xdddddddd which was
+ // previously always enabled
+ eRENDERDOC_Option_VerifyMapWrites = eRENDERDOC_Option_VerifyBufferAccess,
+
+ // Hooks any system API calls that create child processes, and injects
+ // RenderDoc into them recursively with the same options.
+ //
+ // Default - disabled
+ //
+ // 1 - Hooks into spawned child processes
+ // 0 - Child processes are not hooked by RenderDoc
+ eRENDERDOC_Option_HookIntoChildren = 7,
+
+ // By default RenderDoc only includes resources in the final capture necessary
+ // for that frame, this allows you to override that behaviour.
+ //
+ // Default - disabled
+ //
+ // 1 - all live resources at the time of capture are included in the capture
+ // and available for inspection
+ // 0 - only the resources referenced by the captured frame are included
+ eRENDERDOC_Option_RefAllResources = 8,
+
+ // **NOTE**: As of RenderDoc v1.1 this option has been deprecated. Setting or
+ // getting it will be ignored, to allow compatibility with older versions.
+ // In v1.1 the option acts as if it's always enabled.
+ //
+ // By default RenderDoc skips saving initial states for resources where the
+ // previous contents don't appear to be used, assuming that writes before
+ // reads indicate previous contents aren't used.
+ //
+ // Default - disabled
+ //
+ // 1 - initial contents at the start of each captured frame are saved, even if
+ // they are later overwritten or cleared before being used.
+ // 0 - unless a read is detected, initial contents will not be saved and will
+ // appear as black or empty data.
+ eRENDERDOC_Option_SaveAllInitials = 9,
+
+ // In APIs that allow for the recording of command lists to be replayed later,
+ // RenderDoc may choose to not capture command lists before a frame capture is
+ // triggered, to reduce overheads. This means any command lists recorded once
+ // and replayed many times will not be available and may cause a failure to
+ // capture.
+ //
+ // NOTE: This is only true for APIs where multithreading is difficult or
+ // discouraged. Newer APIs like Vulkan and D3D12 will ignore this option
+ // and always capture all command lists since the API is heavily oriented
+ // around it and the overheads have been reduced by API design.
+ //
+ // 1 - All command lists are captured from the start of the application
+ // 0 - Command lists are only captured if their recording begins during
+ // the period when a frame capture is in progress.
+ eRENDERDOC_Option_CaptureAllCmdLists = 10,
+
+ // Mute API debugging output when the API validation mode option is enabled
+ //
+ // Default - enabled
+ //
+ // 1 - Mute any API debug messages from being displayed or passed through
+ // 0 - API debugging is displayed as normal
+ eRENDERDOC_Option_DebugOutputMute = 11,
+
+ // Option to allow vendor extensions to be used even when they may be
+ // incompatible with RenderDoc and cause corrupted replays or crashes.
+ //
+ // Default - inactive
+ //
+ // No values are documented, this option should only be used when absolutely
+ // necessary as directed by a RenderDoc developer.
+ eRENDERDOC_Option_AllowUnsupportedVendorExtensions = 12,
+
+ // Define a soft memory limit which some APIs may aim to keep overhead under where
+ // possible. Anything above this limit will where possible be saved directly to disk during
+ // capture.
+ // This will cause increased disk space use (which may cause a capture to fail if disk space is
+ // exhausted) as well as slower capture times.
+ //
+ // Not all memory allocations may be deferred like this so it is not a guarantee of a memory
+ // limit.
+ //
+ // Units are in MBs, suggested values would range from 200MB to 1000MB.
+ //
+ // Default - 0 Megabytes
+ eRENDERDOC_Option_SoftMemoryLimit = 13,
+} RENDERDOC_CaptureOption;
+
+// Sets an option that controls how RenderDoc behaves on capture.
+//
+// Returns 1 if the option and value are valid
+// Returns 0 if either is invalid and the option is unchanged
+typedef int(RENDERDOC_CC *pRENDERDOC_SetCaptureOptionU32)(RENDERDOC_CaptureOption opt, uint32_t val);
+typedef int(RENDERDOC_CC *pRENDERDOC_SetCaptureOptionF32)(RENDERDOC_CaptureOption opt, float val);
+
+// Gets the current value of an option as a uint32_t
+//
+// If the option is invalid, 0xffffffff is returned
+typedef uint32_t(RENDERDOC_CC *pRENDERDOC_GetCaptureOptionU32)(RENDERDOC_CaptureOption opt);
+
+// Gets the current value of an option as a float
+//
+// If the option is invalid, -FLT_MAX is returned
+typedef float(RENDERDOC_CC *pRENDERDOC_GetCaptureOptionF32)(RENDERDOC_CaptureOption opt);
+
+typedef enum RENDERDOC_InputButton
+{
+ // '0' - '9' matches ASCII values
+ eRENDERDOC_Key_0 = 0x30,
+ eRENDERDOC_Key_1 = 0x31,
+ eRENDERDOC_Key_2 = 0x32,
+ eRENDERDOC_Key_3 = 0x33,
+ eRENDERDOC_Key_4 = 0x34,
+ eRENDERDOC_Key_5 = 0x35,
+ eRENDERDOC_Key_6 = 0x36,
+ eRENDERDOC_Key_7 = 0x37,
+ eRENDERDOC_Key_8 = 0x38,
+ eRENDERDOC_Key_9 = 0x39,
+
+ // 'A' - 'Z' matches ASCII values
+ eRENDERDOC_Key_A = 0x41,
+ eRENDERDOC_Key_B = 0x42,
+ eRENDERDOC_Key_C = 0x43,
+ eRENDERDOC_Key_D = 0x44,
+ eRENDERDOC_Key_E = 0x45,
+ eRENDERDOC_Key_F = 0x46,
+ eRENDERDOC_Key_G = 0x47,
+ eRENDERDOC_Key_H = 0x48,
+ eRENDERDOC_Key_I = 0x49,
+ eRENDERDOC_Key_J = 0x4A,
+ eRENDERDOC_Key_K = 0x4B,
+ eRENDERDOC_Key_L = 0x4C,
+ eRENDERDOC_Key_M = 0x4D,
+ eRENDERDOC_Key_N = 0x4E,
+ eRENDERDOC_Key_O = 0x4F,
+ eRENDERDOC_Key_P = 0x50,
+ eRENDERDOC_Key_Q = 0x51,
+ eRENDERDOC_Key_R = 0x52,
+ eRENDERDOC_Key_S = 0x53,
+ eRENDERDOC_Key_T = 0x54,
+ eRENDERDOC_Key_U = 0x55,
+ eRENDERDOC_Key_V = 0x56,
+ eRENDERDOC_Key_W = 0x57,
+ eRENDERDOC_Key_X = 0x58,
+ eRENDERDOC_Key_Y = 0x59,
+ eRENDERDOC_Key_Z = 0x5A,
+
+ // leave the rest of the ASCII range free
+ // in case we want to use it later
+ eRENDERDOC_Key_NonPrintable = 0x100,
+
+ eRENDERDOC_Key_Divide,
+ eRENDERDOC_Key_Multiply,
+ eRENDERDOC_Key_Subtract,
+ eRENDERDOC_Key_Plus,
+
+ eRENDERDOC_Key_F1,
+ eRENDERDOC_Key_F2,
+ eRENDERDOC_Key_F3,
+ eRENDERDOC_Key_F4,
+ eRENDERDOC_Key_F5,
+ eRENDERDOC_Key_F6,
+ eRENDERDOC_Key_F7,
+ eRENDERDOC_Key_F8,
+ eRENDERDOC_Key_F9,
+ eRENDERDOC_Key_F10,
+ eRENDERDOC_Key_F11,
+ eRENDERDOC_Key_F12,
+
+ eRENDERDOC_Key_Home,
+ eRENDERDOC_Key_End,
+ eRENDERDOC_Key_Insert,
+ eRENDERDOC_Key_Delete,
+ eRENDERDOC_Key_PageUp,
+ eRENDERDOC_Key_PageDn,
+
+ eRENDERDOC_Key_Backspace,
+ eRENDERDOC_Key_Tab,
+ eRENDERDOC_Key_PrtScrn,
+ eRENDERDOC_Key_Pause,
+
+ eRENDERDOC_Key_Max,
+} RENDERDOC_InputButton;
+
+// Sets which key or keys can be used to toggle focus between multiple windows
+//
+// If keys is NULL or num is 0, toggle keys will be disabled
+typedef void(RENDERDOC_CC *pRENDERDOC_SetFocusToggleKeys)(RENDERDOC_InputButton *keys, int num);
+
+// Sets which key or keys can be used to capture the next frame
+//
+// If keys is NULL or num is 0, captures keys will be disabled
+typedef void(RENDERDOC_CC *pRENDERDOC_SetCaptureKeys)(RENDERDOC_InputButton *keys, int num);
+
+typedef enum RENDERDOC_OverlayBits
+{
+ // This single bit controls whether the overlay is enabled or disabled globally
+ eRENDERDOC_Overlay_Enabled = 0x1,
+
+ // Show the average framerate over several seconds as well as min/max
+ eRENDERDOC_Overlay_FrameRate = 0x2,
+
+ // Show the current frame number
+ eRENDERDOC_Overlay_FrameNumber = 0x4,
+
+ // Show a list of recent captures, and how many captures have been made
+ eRENDERDOC_Overlay_CaptureList = 0x8,
+
+ // Default values for the overlay mask
+ eRENDERDOC_Overlay_Default = (eRENDERDOC_Overlay_Enabled | eRENDERDOC_Overlay_FrameRate |
+ eRENDERDOC_Overlay_FrameNumber | eRENDERDOC_Overlay_CaptureList),
+
+ // Enable all bits
+ eRENDERDOC_Overlay_All = 0x7ffffff,
+
+ // Disable all bits
+ eRENDERDOC_Overlay_None = 0,
+} RENDERDOC_OverlayBits;
+
+// returns the overlay bits that have been set
+typedef uint32_t(RENDERDOC_CC *pRENDERDOC_GetOverlayBits)(void);
+// sets the overlay bits with an and & or mask
+typedef void(RENDERDOC_CC *pRENDERDOC_MaskOverlayBits)(uint32_t And, uint32_t Or);
+
+// this function will attempt to remove RenderDoc's hooks in the application.
+//
+// Note: that this can only work correctly if done immediately after
+// the module is loaded, before any API work happens. RenderDoc will remove its
+// injected hooks and shut down. Behaviour is undefined if this is called
+// after any API functions have been called, and there is still no guarantee of
+// success.
+typedef void(RENDERDOC_CC *pRENDERDOC_RemoveHooks)(void);
+
+// DEPRECATED: compatibility for code compiled against pre-1.4.1 headers.
+typedef pRENDERDOC_RemoveHooks pRENDERDOC_Shutdown;
+
+// This function will unload RenderDoc's crash handler.
+//
+// If you use your own crash handler and don't want RenderDoc's handler to
+// intercede, you can call this function to unload it and any unhandled
+// exceptions will pass to the next handler.
+typedef void(RENDERDOC_CC *pRENDERDOC_UnloadCrashHandler)(void);
+
+// Sets the capture file path template
+//
+// pathtemplate is a UTF-8 string that gives a template for how captures will be named
+// and where they will be saved.
+//
+// Any extension is stripped off the path, and captures are saved in the directory
+// specified, and named with the filename and the frame number appended. If the
+// directory does not exist it will be created, including any parent directories.
+//
+// If pathtemplate is NULL, the template will remain unchanged
+//
+// Example:
+//
+// SetCaptureFilePathTemplate("my_captures/example");
+//
+// Capture #1 -> my_captures/example_frame123.rdc
+// Capture #2 -> my_captures/example_frame456.rdc
+typedef void(RENDERDOC_CC *pRENDERDOC_SetCaptureFilePathTemplate)(const char *pathtemplate);
+
+// returns the current capture path template, see SetCaptureFileTemplate above, as a UTF-8 string
+typedef const char *(RENDERDOC_CC *pRENDERDOC_GetCaptureFilePathTemplate)(void);
+
+// DEPRECATED: compatibility for code compiled against pre-1.1.2 headers.
+typedef pRENDERDOC_SetCaptureFilePathTemplate pRENDERDOC_SetLogFilePathTemplate;
+typedef pRENDERDOC_GetCaptureFilePathTemplate pRENDERDOC_GetLogFilePathTemplate;
+
+// returns the number of captures that have been made
+typedef uint32_t(RENDERDOC_CC *pRENDERDOC_GetNumCaptures)(void);
+
+// This function returns the details of a capture, by index. New captures are added
+// to the end of the list.
+//
+// filename will be filled with the absolute path to the capture file, as a UTF-8 string
+// pathlength will be written with the length in bytes of the filename string
+// timestamp will be written with the time of the capture, in seconds since the Unix epoch
+//
+// Any of the parameters can be NULL and they'll be skipped.
+//
+// The function will return 1 if the capture index is valid, or 0 if the index is invalid
+// If the index is invalid, the values will be unchanged
+//
+// Note: when captures are deleted in the UI they will remain in this list, so the
+// capture path may not exist anymore.
+typedef uint32_t(RENDERDOC_CC *pRENDERDOC_GetCapture)(uint32_t idx, char *filename,
+ uint32_t *pathlength, uint64_t *timestamp);
+
+// Sets the comments associated with a capture file. These comments are displayed in the
+// UI program when opening.
+//
+// filePath should be a path to the capture file to add comments to. If set to NULL or ""
+// the most recent capture file created made will be used instead.
+// comments should be a NULL-terminated UTF-8 string to add as comments.
+//
+// Any existing comments will be overwritten.
+typedef void(RENDERDOC_CC *pRENDERDOC_SetCaptureFileComments)(const char *filePath,
+ const char *comments);
+
+// returns 1 if the RenderDoc UI is connected to this application, 0 otherwise
+typedef uint32_t(RENDERDOC_CC *pRENDERDOC_IsTargetControlConnected)(void);
+
+// DEPRECATED: compatibility for code compiled against pre-1.1.1 headers.
+// This was renamed to IsTargetControlConnected in API 1.1.1, the old typedef is kept here for
+// backwards compatibility with old code, it is castable either way since it's ABI compatible
+// as the same function pointer type.
+typedef pRENDERDOC_IsTargetControlConnected pRENDERDOC_IsRemoteAccessConnected;
+
+// This function will launch the Replay UI associated with the RenderDoc library injected
+// into the running application.
+//
+// if connectTargetControl is 1, the Replay UI will be launched with a command line parameter
+// to connect to this application
+// cmdline is the rest of the command line, as a UTF-8 string. E.g. a captures to open
+// if cmdline is NULL, the command line will be empty.
+//
+// returns the PID of the replay UI if successful, 0 if not successful.
+typedef uint32_t(RENDERDOC_CC *pRENDERDOC_LaunchReplayUI)(uint32_t connectTargetControl,
+ const char *cmdline);
+
+// RenderDoc can return a higher version than requested if it's backwards compatible,
+// this function returns the actual version returned. If a parameter is NULL, it will be
+// ignored and the others will be filled out.
+typedef void(RENDERDOC_CC *pRENDERDOC_GetAPIVersion)(int *major, int *minor, int *patch);
+
+// Requests that the replay UI show itself (if hidden or not the current top window). This can be
+// used in conjunction with IsTargetControlConnected and LaunchReplayUI to intelligently handle
+// showing the UI after making a capture.
+//
+// This will return 1 if the request was successfully passed on, though it's not guaranteed that
+// the UI will be on top in all cases depending on OS rules. It will return 0 if there is no current
+// target control connection to make such a request, or if there was another error
+typedef uint32_t(RENDERDOC_CC *pRENDERDOC_ShowReplayUI)(void);
+
+//////////////////////////////////////////////////////////////////////////
+// Capturing functions
+//
+
+// A device pointer is a pointer to the API's root handle.
+//
+// This would be an ID3D11Device, HGLRC/GLXContext, ID3D12Device, etc
+typedef void *RENDERDOC_DevicePointer;
+
+// A window handle is the OS's native window handle
+//
+// This would be an HWND, GLXDrawable, etc
+typedef void *RENDERDOC_WindowHandle;
+
+// A helper macro for Vulkan, where the device handle cannot be used directly.
+//
+// Passing the VkInstance to this macro will return the RENDERDOC_DevicePointer to use.
+//
+// Specifically, the value needed is the dispatch table pointer, which sits as the first
+// pointer-sized object in the memory pointed to by the VkInstance. Thus we cast to a void** and
+// indirect once.
+#define RENDERDOC_DEVICEPOINTER_FROM_VKINSTANCE(inst) (*((void **)(inst)))
+
+// This sets the RenderDoc in-app overlay in the API/window pair as 'active' and it will
+// respond to keypresses. Neither parameter can be NULL
+typedef void(RENDERDOC_CC *pRENDERDOC_SetActiveWindow)(RENDERDOC_DevicePointer device,
+ RENDERDOC_WindowHandle wndHandle);
+
+// capture the next frame on whichever window and API is currently considered active
+typedef void(RENDERDOC_CC *pRENDERDOC_TriggerCapture)(void);
+
+// capture the next N frames on whichever window and API is currently considered active
+typedef void(RENDERDOC_CC *pRENDERDOC_TriggerMultiFrameCapture)(uint32_t numFrames);
+
+// When choosing either a device pointer or a window handle to capture, you can pass NULL.
+// Passing NULL specifies a 'wildcard' match against anything. This allows you to specify
+// any API rendering to a specific window, or a specific API instance rendering to any window,
+// or in the simplest case of one window and one API, you can just pass NULL for both.
+//
+// In either case, if there are two or more possible matching (device,window) pairs it
+// is undefined which one will be captured.
+//
+// Note: for headless rendering you can pass NULL for the window handle and either specify
+// a device pointer or leave it NULL as above.
+
+// Immediately starts capturing API calls on the specified device pointer and window handle.
+//
+// If there is no matching thing to capture (e.g. no supported API has been initialised),
+// this will do nothing.
+//
+// The results are undefined (including crashes) if two captures are started overlapping,
+// even on separate devices and/oror windows.
+typedef void(RENDERDOC_CC *pRENDERDOC_StartFrameCapture)(RENDERDOC_DevicePointer device,
+ RENDERDOC_WindowHandle wndHandle);
+
+// Returns whether or not a frame capture is currently ongoing anywhere.
+//
+// This will return 1 if a capture is ongoing, and 0 if there is no capture running
+typedef uint32_t(RENDERDOC_CC *pRENDERDOC_IsFrameCapturing)(void);
+
+// Ends capturing immediately.
+//
+// This will return 1 if the capture succeeded, and 0 if there was an error capturing.
+typedef uint32_t(RENDERDOC_CC *pRENDERDOC_EndFrameCapture)(RENDERDOC_DevicePointer device,
+ RENDERDOC_WindowHandle wndHandle);
+
+// Ends capturing immediately and discard any data stored without saving to disk.
+//
+// This will return 1 if the capture was discarded, and 0 if there was an error or no capture
+// was in progress
+typedef uint32_t(RENDERDOC_CC *pRENDERDOC_DiscardFrameCapture)(RENDERDOC_DevicePointer device,
+ RENDERDOC_WindowHandle wndHandle);
+
+// Only valid to be called between a call to StartFrameCapture and EndFrameCapture. Gives a custom
+// title to the capture produced which will be displayed in the UI.
+//
+// If multiple captures are ongoing, this title will be applied to the first capture to end after
+// this call. The second capture to end will have no title, unless this function is called again.
+//
+// Calling this function has no effect if no capture is currently running, and if it is called
+// multiple times only the last title will be used.
+typedef void(RENDERDOC_CC *pRENDERDOC_SetCaptureTitle)(const char *title);
+
+// Annotations API:
+//
+// These functions allow you to specify annotations either on a per-command level, or a per-object
+// level.
+//
+// Basic types of annotations are supported, as well as vector versions and references to API objects.
+//
+// The annotations are stored as keys, with the key being a dot-separated path allowing arbitrary
+// nesting and user organisation. The keys are sorted in human order so `foo.2.bar` will be displayed
+// before `foo.10.bar` to allow creation of arrays if desired.
+//
+// Deleting an annotation can be done by assigning an empty value to it.
+
+// the type of an annotation value, or Empty to delete an annotation
+typedef enum RENDERDOC_AnnotationType
+{
+ eRENDERDOC_Empty,
+ eRENDERDOC_Bool,
+ eRENDERDOC_Int32,
+ eRENDERDOC_UInt32,
+ eRENDERDOC_Int64,
+ eRENDERDOC_UInt64,
+ eRENDERDOC_Float,
+ eRENDERDOC_Double,
+ eRENDERDOC_String,
+ eRENDERDOC_APIObject,
+ eRENDERDOC_AnnotationMax = 0x7FFFFFFF,
+} RENDERDOC_AnnotationType;
+
+// a union with vector annotation value data
+typedef union RENDERDOC_AnnotationVectorValue
+{
+ bool boolean[4];
+ int32_t int32[4];
+ int64_t int64[4];
+ uint32_t uint32[4];
+ uint64_t uint64[4];
+ float float32[4];
+ double float64[4];
+} RENDERDOC_AnnotationVectorValue;
+
+// a union with scalar annotation value data
+typedef union RENDERDOC_AnnotationValue
+{
+ bool boolean;
+ int32_t int32;
+ int64_t int64;
+ uint32_t uint32;
+ uint64_t uint64;
+ float float32;
+ double float64;
+
+ RENDERDOC_AnnotationVectorValue vector;
+
+ const char *string;
+ void *apiObject;
+} RENDERDOC_AnnotationValue;
+
+// a struct for specifying a GL object, as we don't have pointers we can use so instead we specify a
+// pointer to this struct giving both the type and the name
+typedef struct RENDERDOC_GLResourceReference
+{
+ // this is the same GLenum identifier as passed to glObjectLabel
+ uint32_t identifier;
+ uint32_t name;
+} GLResourceReference;
+
+// simple C++ helpers to avoid the need for a temporary objects for value passing and GL object specification
+#ifdef __cplusplus
+struct RDGLObjectHelper
+{
+ RENDERDOC_GLResourceReference gl;
+
+ RDGLObjectHelper(uint32_t identifier, uint32_t name)
+ {
+ gl.identifier = identifier;
+ gl.name = name;
+ }
+
+ operator RENDERDOC_GLResourceReference *() { return ≷ }
+};
+
+struct RDAnnotationHelper
+{
+ RENDERDOC_AnnotationValue val;
+
+ RDAnnotationHelper(bool b) { val.boolean = b; }
+ RDAnnotationHelper(int32_t i) { val.int32 = i; }
+ RDAnnotationHelper(int64_t i) { val.int64 = i; }
+ RDAnnotationHelper(uint32_t i) { val.uint32 = i; }
+ RDAnnotationHelper(uint64_t i) { val.uint64 = i; }
+ RDAnnotationHelper(float f) { val.float32 = f; }
+ RDAnnotationHelper(double d) { val.float64 = d; }
+ RDAnnotationHelper(const char *s) { val.string = s; }
+
+ operator RENDERDOC_AnnotationValue *() { return &val; }
+};
+#endif
+
+// The device is specified in the same way as other API calls that take a RENDERDOC_DevicePointer
+// to specify the device.
+//
+// The object or queue/commandbuffer will depend on the graphics API in question.
+//
+// Return value:
+// 0 - The annotation was applied successfully.
+// 1 - The device is unknown/invalid
+// 2 - The device is valid but the annotation is not supported for API-specific reasons, such as an
+// unrecognised or invalid object or queue/commandbuffer
+// 3 - The call is ill-formed or invalid e.g. empty is specified with a value pointer, or non-empty
+// is specified with a NULL value pointer
+typedef uint32_t(RENDERDOC_CC *pRENDERDOC_SetObjectAnnotation)(RENDERDOC_DevicePointer device,
+ void *object, const char *key,
+ RENDERDOC_AnnotationType valueType,
+ uint32_t valueVectorWidth,
+ const RENDERDOC_AnnotationValue *value);
+
+typedef uint32_t(RENDERDOC_CC *pRENDERDOC_SetCommandAnnotation)(
+ RENDERDOC_DevicePointer device, void *queueOrCommandBuffer, const char *key,
+ RENDERDOC_AnnotationType valueType, uint32_t valueVectorWidth,
+ const RENDERDOC_AnnotationValue *value);
+
+//////////////////////////////////////////////////////////////////////////////////////////////////
+// RenderDoc API versions
+//
+
+// RenderDoc uses semantic versioning (http://semver.org/).
+//
+// MAJOR version is incremented when incompatible API changes happen.
+// MINOR version is incremented when functionality is added in a backwards-compatible manner.
+// PATCH version is incremented when backwards-compatible bug fixes happen.
+//
+// Note that this means the API returned can be higher than the one you might have requested.
+// e.g. if you are running against a newer RenderDoc that supports 1.0.1, it will be returned
+// instead of 1.0.0. You can check this with the GetAPIVersion entry point
+typedef enum RENDERDOC_Version
+{
+ eRENDERDOC_API_Version_1_0_0 = 10000, // RENDERDOC_API_1_0_0 = 1 00 00
+ eRENDERDOC_API_Version_1_0_1 = 10001, // RENDERDOC_API_1_0_1 = 1 00 01
+ eRENDERDOC_API_Version_1_0_2 = 10002, // RENDERDOC_API_1_0_2 = 1 00 02
+ eRENDERDOC_API_Version_1_1_0 = 10100, // RENDERDOC_API_1_1_0 = 1 01 00
+ eRENDERDOC_API_Version_1_1_1 = 10101, // RENDERDOC_API_1_1_1 = 1 01 01
+ eRENDERDOC_API_Version_1_1_2 = 10102, // RENDERDOC_API_1_1_2 = 1 01 02
+ eRENDERDOC_API_Version_1_2_0 = 10200, // RENDERDOC_API_1_2_0 = 1 02 00
+ eRENDERDOC_API_Version_1_3_0 = 10300, // RENDERDOC_API_1_3_0 = 1 03 00
+ eRENDERDOC_API_Version_1_4_0 = 10400, // RENDERDOC_API_1_4_0 = 1 04 00
+ eRENDERDOC_API_Version_1_4_1 = 10401, // RENDERDOC_API_1_4_1 = 1 04 01
+ eRENDERDOC_API_Version_1_4_2 = 10402, // RENDERDOC_API_1_4_2 = 1 04 02
+ eRENDERDOC_API_Version_1_5_0 = 10500, // RENDERDOC_API_1_5_0 = 1 05 00
+ eRENDERDOC_API_Version_1_6_0 = 10600, // RENDERDOC_API_1_6_0 = 1 06 00
+ eRENDERDOC_API_Version_1_7_0 = 10700, // RENDERDOC_API_1_7_0 = 1 07 00
+} RENDERDOC_Version;
+
+// API version changelog:
+//
+// 1.0.0 - initial release
+// 1.0.1 - Bugfix: IsFrameCapturing() was returning false for captures that were triggered
+// by keypress or TriggerCapture, instead of Start/EndFrameCapture.
+// 1.0.2 - Refactor: Renamed eRENDERDOC_Option_DebugDeviceMode to eRENDERDOC_Option_APIValidation
+// 1.1.0 - Add feature: TriggerMultiFrameCapture(). Backwards compatible with 1.0.x since the new
+// function pointer is added to the end of the struct, the original layout is identical
+// 1.1.1 - Refactor: Renamed remote access to target control (to better disambiguate from remote
+// replay/remote server concept in replay UI)
+// 1.1.2 - Refactor: Renamed "log file" in function names to just capture, to clarify that these
+// are captures and not debug logging files. This is the first API version in the v1.0
+// branch.
+// 1.2.0 - Added feature: SetCaptureFileComments() to add comments to a capture file that will be
+// displayed in the UI program on load.
+// 1.3.0 - Added feature: New capture option eRENDERDOC_Option_AllowUnsupportedVendorExtensions
+// which allows users to opt-in to allowing unsupported vendor extensions to function.
+// Should be used at the user's own risk.
+// Refactor: Renamed eRENDERDOC_Option_VerifyMapWrites to
+// eRENDERDOC_Option_VerifyBufferAccess, which now also controls initialisation to
+// 0xdddddddd of uninitialised buffer contents.
+// 1.4.0 - Added feature: DiscardFrameCapture() to discard a frame capture in progress and stop
+// capturing without saving anything to disk.
+// 1.4.1 - Refactor: Renamed Shutdown to RemoveHooks to better clarify what is happening
+// 1.4.2 - Refactor: Renamed 'draws' to 'actions' in callstack capture option.
+// 1.5.0 - Added feature: ShowReplayUI() to request that the replay UI show itself if connected
+// 1.6.0 - Added feature: SetCaptureTitle() which can be used to set a title for a
+// capture made with StartFrameCapture() or EndFrameCapture()
+// 1.7.0 - Added feature: SetObjectAnnotation() / SetCommandAnnotation() for adding rich
+// annotations to objects and command streams
+
+typedef struct RENDERDOC_API_1_7_0
+{
+ pRENDERDOC_GetAPIVersion GetAPIVersion;
+
+ pRENDERDOC_SetCaptureOptionU32 SetCaptureOptionU32;
+ pRENDERDOC_SetCaptureOptionF32 SetCaptureOptionF32;
+
+ pRENDERDOC_GetCaptureOptionU32 GetCaptureOptionU32;
+ pRENDERDOC_GetCaptureOptionF32 GetCaptureOptionF32;
+
+ pRENDERDOC_SetFocusToggleKeys SetFocusToggleKeys;
+ pRENDERDOC_SetCaptureKeys SetCaptureKeys;
+
+ pRENDERDOC_GetOverlayBits GetOverlayBits;
+ pRENDERDOC_MaskOverlayBits MaskOverlayBits;
+
+ // Shutdown was renamed to RemoveHooks in 1.4.1.
+ // These unions allow old code to continue compiling without changes
+ union
+ {
+ pRENDERDOC_Shutdown Shutdown;
+ pRENDERDOC_RemoveHooks RemoveHooks;
+ };
+ pRENDERDOC_UnloadCrashHandler UnloadCrashHandler;
+
+ // Get/SetLogFilePathTemplate was renamed to Get/SetCaptureFilePathTemplate in 1.1.2.
+ // These unions allow old code to continue compiling without changes
+ union
+ {
+ // deprecated name
+ pRENDERDOC_SetLogFilePathTemplate SetLogFilePathTemplate;
+ // current name
+ pRENDERDOC_SetCaptureFilePathTemplate SetCaptureFilePathTemplate;
+ };
+ union
+ {
+ // deprecated name
+ pRENDERDOC_GetLogFilePathTemplate GetLogFilePathTemplate;
+ // current name
+ pRENDERDOC_GetCaptureFilePathTemplate GetCaptureFilePathTemplate;
+ };
+
+ pRENDERDOC_GetNumCaptures GetNumCaptures;
+ pRENDERDOC_GetCapture GetCapture;
+
+ pRENDERDOC_TriggerCapture TriggerCapture;
+
+ // IsRemoteAccessConnected was renamed to IsTargetControlConnected in 1.1.1.
+ // This union allows old code to continue compiling without changes
+ union
+ {
+ // deprecated name
+ pRENDERDOC_IsRemoteAccessConnected IsRemoteAccessConnected;
+ // current name
+ pRENDERDOC_IsTargetControlConnected IsTargetControlConnected;
+ };
+ pRENDERDOC_LaunchReplayUI LaunchReplayUI;
+
+ pRENDERDOC_SetActiveWindow SetActiveWindow;
+
+ pRENDERDOC_StartFrameCapture StartFrameCapture;
+ pRENDERDOC_IsFrameCapturing IsFrameCapturing;
+ pRENDERDOC_EndFrameCapture EndFrameCapture;
+
+ // new function in 1.1.0
+ pRENDERDOC_TriggerMultiFrameCapture TriggerMultiFrameCapture;
+
+ // new function in 1.2.0
+ pRENDERDOC_SetCaptureFileComments SetCaptureFileComments;
+
+ // new function in 1.4.0
+ pRENDERDOC_DiscardFrameCapture DiscardFrameCapture;
+
+ // new function in 1.5.0
+ pRENDERDOC_ShowReplayUI ShowReplayUI;
+
+ // new function in 1.6.0
+ pRENDERDOC_SetCaptureTitle SetCaptureTitle;
+
+ // new functions in 1.7.0
+ pRENDERDOC_SetObjectAnnotation SetObjectAnnotation;
+ pRENDERDOC_SetCommandAnnotation SetCommandAnnotation;
+} RENDERDOC_API_1_7_0;
+
+typedef RENDERDOC_API_1_7_0 RENDERDOC_API_1_0_0;
+typedef RENDERDOC_API_1_7_0 RENDERDOC_API_1_0_1;
+typedef RENDERDOC_API_1_7_0 RENDERDOC_API_1_0_2;
+typedef RENDERDOC_API_1_7_0 RENDERDOC_API_1_1_0;
+typedef RENDERDOC_API_1_7_0 RENDERDOC_API_1_1_1;
+typedef RENDERDOC_API_1_7_0 RENDERDOC_API_1_1_2;
+typedef RENDERDOC_API_1_7_0 RENDERDOC_API_1_2_0;
+typedef RENDERDOC_API_1_7_0 RENDERDOC_API_1_3_0;
+typedef RENDERDOC_API_1_7_0 RENDERDOC_API_1_4_0;
+typedef RENDERDOC_API_1_7_0 RENDERDOC_API_1_4_1;
+typedef RENDERDOC_API_1_7_0 RENDERDOC_API_1_4_2;
+typedef RENDERDOC_API_1_7_0 RENDERDOC_API_1_5_0;
+typedef RENDERDOC_API_1_7_0 RENDERDOC_API_1_6_0;
+
+//////////////////////////////////////////////////////////////////////////////////////////////////
+// RenderDoc API entry point
+//
+// This entry point can be obtained via GetProcAddress/dlsym if RenderDoc is available.
+//
+// The name is the same as the typedef - "RENDERDOC_GetAPI"
+//
+// This function is not thread safe, and should not be called on multiple threads at once.
+// Ideally, call this once as early as possible in your application's startup, before doing
+// any API work, since some configuration functionality etc has to be done also before
+// initialising any APIs.
+//
+// Parameters:
+// version is a single value from the RENDERDOC_Version above.
+//
+// outAPIPointers will be filled out with a pointer to the corresponding struct of function
+// pointers.
+//
+// Returns:
+// 1 - if the outAPIPointers has been filled with a pointer to the API struct requested
+// 0 - if the requested version is not supported or the arguments are invalid.
+//
+typedef int(RENDERDOC_CC *pRENDERDOC_GetAPI)(RENDERDOC_Version version, void **outAPIPointers);
+
+#ifdef __cplusplus
+} // extern "C"
+#endif
diff --git a/src/Layers/xrRender/DetailManager_VS.cpp b/src/Layers/xrRender/DetailManager_VS.cpp
index cae9626547..99c0043280 100644
--- a/src/Layers/xrRender/DetailManager_VS.cpp
+++ b/src/Layers/xrRender/DetailManager_VS.cpp
@@ -17,6 +17,9 @@ const int quant = 16384;
const int c_hdr = 10;
const int c_size = 4;
+// pip set by the main gbuffer pass while a SVP pass follows, the SVP drain then clears the set
+bool g_svp_defer_detail_clear = false;
+
static D3DVERTEXELEMENT9 dwDecl[] =
{
{0, 0, D3DDECLTYPE_FLOAT3, D3DDECLMETHOD_DEFAULT, D3DDECLUSAGE_POSITION, 0}, // pos
@@ -256,6 +259,11 @@ void CDetailManager::hw_Render_dump(ref_constant x_array, u32 var_id, u32 lod_id
VERIFY(objects.size()<=list.size());
+ // pip grass cull, only on the SVP gbuffer pass with the frustum armed, the main pass pays one bool
+ const bool svp_grass_cull = ps_r__svp_cull_grass && CDSGraphManager::svp_cull_active();
+ // pip the main-pass drain keeps the visible set when the SVP pass draws the scope grass second
+ extern bool g_svp_defer_detail_clear;
+
// Iterate
for (u32 O = 0; O < objects.size(); O++)
{
@@ -282,6 +290,10 @@ void CDetailManager::hw_Render_dump(ref_constant x_array, u32 var_id, u32 lod_id
{
SlotItem& Instance = **_iI;
+ // pip drop grass instances off the scope cone, radius covers a blade past its root
+ if (svp_grass_cull && CDSGraphManager::svp_cull_reject_sphere(Instance.position, dm_slot_size))
+ continue;
+
if (!RImplementation.GMBase.is_sector_visible(RImplementation.pOutdoorSector))
continue;
@@ -350,8 +362,11 @@ void CDetailManager::hw_Render_dump(ref_constant x_array, u32 var_id, u32 lod_id
// KD: we must not clear vis on r2 since we want details shadows
#if RENDER==R_R2
if (!psDeviceFlags2.test(rsGrassShadow) || RImplementation.PHASE_NORMAL == RImplementation.phase) // phase normal without shadows
-#endif
vis.clear_not_free();
+#else
+ if (!g_svp_defer_detail_clear)
+ vis.clear_not_free();
+#endif
}
vOffset += hw_BatchSize * Object.number_vertices;
iOffset += hw_BatchSize * Object.number_indices;
diff --git a/src/Layers/xrRender/FTreeVisual.cpp b/src/Layers/xrRender/FTreeVisual.cpp
index 75e53f7a49..2411c2b07e 100644
--- a/src/Layers/xrRender/FTreeVisual.cpp
+++ b/src/Layers/xrRender/FTreeVisual.cpp
@@ -151,10 +151,11 @@ struct FTreeVisual_setup
void FTreeVisual::Render(float LOD)
{
PROF_EVENT("FTreeVisual::Render");
- static FTreeVisual_setup tvs, prev_tvs;
- if (tvs.dwFrame != Device.dwFrame)
+ const bool svp = Device.m_SecondViewport.IsSVPFrame(); // pip this viewport's prev-wind slot
+ static FTreeVisual_setup tvs, prev_tvs[2];
+ if (tvs.dwFrame != Device.dwViewport)
{
- prev_tvs = tvs; // Save previous frame calculations
+ prev_tvs[svp] = tvs; // Save previous frame calculations
tvs.calculate();
}
// setup constants
@@ -169,8 +170,8 @@ void FTreeVisual::Render(float LOD)
RCache.tree.set_wave(tvs.wave); // wave
RCache.tree.set_wind(tvs.wind); // wind
- RCache.set_c(c_prev_wave, prev_tvs.wave);
- RCache.set_c(c_prev_wind, prev_tvs.wind);
+ RCache.set_c(c_prev_wave, prev_tvs[svp].wave);
+ RCache.set_c(c_prev_wind, prev_tvs[svp].wind);
#if RENDER!=R_R1
s *= 1.3333f;
diff --git a/src/Layers/xrRender/HW.h b/src/Layers/xrRender/HW.h
index 71cf6a02bf..1c572e2b63 100644
--- a/src/Layers/xrRender/HW.h
+++ b/src/Layers/xrRender/HW.h
@@ -74,6 +74,10 @@ class CHW
IDXGISwapChain1* m_pSwapChain;
ID3D11RenderTargetView* pBaseRT; // combine with DX9 pBaseRT via typedef
ID3D11DepthStencilView* pBaseZB;
+ // persistent swapchain RTV/DSV snapshotted in UpdateViews, SetActive clobbers pBaseRT per
+ // target so the main target restores from these and the SVP RT never stays bound
+ ID3D11RenderTargetView* secret_pBaseRT = nullptr;
+ ID3D11DepthStencilView* secret_pBaseZB = nullptr;
ID3DUserDefinedAnnotation* pAnnotation;
CHWCaps Caps;
diff --git a/src/Layers/xrRender/Light_DB.cpp b/src/Layers/xrRender/Light_DB.cpp
index e15f39fdb6..373fc7467b 100644
--- a/src/Layers/xrRender/Light_DB.cpp
+++ b/src/Layers/xrRender/Light_DB.cpp
@@ -197,7 +197,13 @@ void CLight_DB::add_light(light* L)
L->frame_render = Device.dwFrame;
if (RImplementation.o.noshadows) L->flags.bShadow = FALSE;
if (L->flags.bStatic && !ps_r2_ls_flags.test(R2FLAG_R1LIGHTS)) return;
- if(Device.vCameraPosition.distance_to_sqr(L->SpatialComponent->spatial.sphere.P)>_sqr(g_pGamePersistent->Environment().CurrentEnv->fog_distance)) return;
+ // pip the fog-distance cull is a naked-eye heuristic, the scope sees far past it
+ float lim = g_pGamePersistent->Environment().CurrentEnv->fog_distance;
+ {
+ if (ps_r__svp_light_capture && Device.true_pip_on && Device.m_SecondViewport.IsSVPActive())
+ lim = _max(lim, g_pGamePersistent->Environment().CurrentEnv->far_plane);
+ }
+ if (Device.vCameraPosition.distance_to_sqr(L->SpatialComponent->spatial.sphere.P) > _sqr(lim)) return;
L->export_();
}
diff --git a/src/Layers/xrRender/ParticleEffect.cpp b/src/Layers/xrRender/ParticleEffect.cpp
index c798ea534a..dbe1f0d36d 100644
--- a/src/Layers/xrRender/ParticleEffect.cpp
+++ b/src/Layers/xrRender/ParticleEffect.cpp
@@ -681,9 +681,23 @@ void CParticleEffect::Render(float)
CHudInitializer initalizer(false);
if (GetHudMode())
{
- initalizer.SetHudMode();
- RImplementation.rmNear();
- ApplyTexgen(Device.mFullTransform);
+ if (Device.true_pip_on && Device.m_SecondViewport.m_render_pass_is_svp)
+ {
+ // scope pass, the hud matrices would pair the main view with the svp projection,
+ // bind the pass camera instead so the flash sits on the drained muzzle with real depth
+ Device.mView.set(Device.matrices[1].mView);
+ Device.mProject.set(Device.matrices[1].mProject);
+ Device.mFullTransform.mul(Device.mProject, Device.mView);
+ Device.m_pRender->SetCacheXform(Device.mView, Device.mProject);
+ RImplementation.rmNormal();
+ ApplyTexgen(Device.mFullTransform);
+ }
+ else
+ {
+ initalizer.SetHudMode();
+ RImplementation.rmNear();
+ ApplyTexgen(Device.mFullTransform);
+ }
}
#endif
diff --git a/src/Layers/xrRender/ParticleEffect.h b/src/Layers/xrRender/ParticleEffect.h
index c59f541c03..0679c6de4d 100644
--- a/src/Layers/xrRender/ParticleEffect.h
+++ b/src/Layers/xrRender/ParticleEffect.h
@@ -42,6 +42,7 @@ namespace PS
flRT_XFORM = (1 << 2),
flRT_HUDmode = (1 << 3),
flRT_LiveUpdate = (1 << 4),
+ flRT_WeaponFX = (1 << 5),
};
Flags8 m_RT_Flags;
@@ -78,6 +79,9 @@ namespace PS
virtual void SetHudMode(BOOL b) { m_RT_Flags.set(flRT_HUDmode, b); }
virtual BOOL GetHudMode() { return m_RT_Flags.is(flRT_HUDmode); }
+ virtual void SetWeaponFX(BOOL b) { m_RT_Flags.set(flRT_WeaponFX, b); }
+ virtual BOOL GetWeaponFX() { return m_RT_Flags.is(flRT_WeaponFX); }
+
virtual void SetLiveUpdate(BOOL b) { m_RT_Flags.set(flRT_LiveUpdate, b); }
virtual BOOL GetLiveUpdate() { return m_RT_Flags.is(flRT_LiveUpdate); }
diff --git a/src/Layers/xrRender/ParticleGroup.cpp b/src/Layers/xrRender/ParticleGroup.cpp
index c04e722bd3..14258bbd7c 100644
--- a/src/Layers/xrRender/ParticleGroup.cpp
+++ b/src/Layers/xrRender/ParticleGroup.cpp
@@ -713,6 +713,28 @@ BOOL CParticleGroup::GetHudMode()
return FALSE;
}
+void CParticleGroup::SetWeaponFX(BOOL b)
+{
+ xrCriticalSectionGuard guard(&onframe_lock);
+ for (SItem& item : items)
+ {
+ CParticleEffect* E = static_cast(item._effect);
+ E->SetWeaponFX(b);
+ }
+}
+
+BOOL CParticleGroup::GetWeaponFX()
+{
+ xrCriticalSectionGuard guard(&onframe_lock);
+ if (items.size())
+ {
+ CParticleEffect* E = static_cast(items[0]._effect);
+ return E->GetWeaponFX();
+ }
+
+ return FALSE;
+}
+
void CParticleGroup::SetLiveUpdate(BOOL b)
{
xrCriticalSectionGuard guard(&onframe_lock);
diff --git a/src/Layers/xrRender/ParticleGroup.h b/src/Layers/xrRender/ParticleGroup.h
index 230159c6ba..3e81b58bf0 100644
--- a/src/Layers/xrRender/ParticleGroup.h
+++ b/src/Layers/xrRender/ParticleGroup.h
@@ -162,6 +162,9 @@ namespace PS
virtual void SetHudMode(BOOL b);
virtual BOOL GetHudMode();
+ virtual void SetWeaponFX(BOOL b);
+ virtual BOOL GetWeaponFX();
+
virtual void SetLiveUpdate(BOOL b);
virtual BOOL GetLiveUpdate();
diff --git a/src/Layers/xrRender/R_Backend.h b/src/Layers/xrRender/R_Backend.h
index 24b4575bc3..dc361ac4c9 100644
--- a/src/Layers/xrRender/R_Backend.h
+++ b/src/Layers/xrRender/R_Backend.h
@@ -194,8 +194,8 @@ class ECORE_API CBackend
CMatrix* matrices [8 ]; // matrices are supported only for FFP
#endif
- void Invalidate();
public:
+ void Invalidate(); // pip: SetActive needs a full cache reset per viewport (was private)
struct _stats
{
u32 polys;
diff --git a/src/Layers/xrRender/R_Backend_Runtime.h b/src/Layers/xrRender/R_Backend_Runtime.h
index 3a94ef2963..762b46679d 100644
--- a/src/Layers/xrRender/R_Backend_Runtime.h
+++ b/src/Layers/xrRender/R_Backend_Runtime.h
@@ -50,40 +50,40 @@ IC void R_xforms::set_c_wvp(R_constant* C)
RCache.set_c(C, m_wvp);
};
-IC void R_xforms::set_c_w_prev(R_constant* C)
-{
- c_w_prev = C;
- RCache.set_c(C, m_w_prev);
+IC void R_xforms::set_c_w_prev(R_constant* C)
+{
+ c_w_prev[Device.m_SecondViewport.IsSVPFrame()] = C;
+ RCache.set_c(C, m_w_prev[Device.m_SecondViewport.IsSVPFrame()]);
};
-IC void R_xforms::set_c_v_prev(R_constant* C)
-{
- c_v_prev = C;
- RCache.set_c(C, m_v_prev);
+IC void R_xforms::set_c_v_prev(R_constant* C)
+{
+ c_v_prev[Device.m_SecondViewport.IsSVPFrame()] = C;
+ RCache.set_c(C, m_v_prev[Device.m_SecondViewport.IsSVPFrame()]);
};
-IC void R_xforms::set_c_p_prev(R_constant* C)
-{
- c_p_prev = C;
- RCache.set_c(C, m_p_prev);
+IC void R_xforms::set_c_p_prev(R_constant* C)
+{
+ c_p_prev[Device.m_SecondViewport.IsSVPFrame()] = C;
+ RCache.set_c(C, m_p_prev[Device.m_SecondViewport.IsSVPFrame()]);
};
-IC void R_xforms::set_c_wv_prev(R_constant* C)
-{
- c_wv_prev = C;
- RCache.set_c(C, m_wv_prev);
+IC void R_xforms::set_c_wv_prev(R_constant* C)
+{
+ c_wv_prev[Device.m_SecondViewport.IsSVPFrame()] = C;
+ RCache.set_c(C, m_wv_prev[Device.m_SecondViewport.IsSVPFrame()]);
};
-IC void R_xforms::set_c_vp_prev(R_constant* C)
-{
- c_vp_prev = C;
- RCache.set_c(C, m_vp_prev);
+IC void R_xforms::set_c_vp_prev(R_constant* C)
+{
+ c_vp_prev[Device.m_SecondViewport.IsSVPFrame()] = C;
+ RCache.set_c(C, m_vp_prev[Device.m_SecondViewport.IsSVPFrame()]);
};
-IC void R_xforms::set_c_wvp_prev(R_constant* C)
-{
- c_wvp_prev = C;
- RCache.set_c(C, m_wvp_prev);
+IC void R_xforms::set_c_wvp_prev(R_constant* C)
+{
+ c_wvp_prev[Device.m_SecondViewport.IsSVPFrame()] = C;
+ RCache.set_c(C, m_wvp_prev[Device.m_SecondViewport.IsSVPFrame()]);
};
IC void CBackend::set_xform_world(const Fmatrix& M)
diff --git a/src/Layers/xrRender/R_Backend_xform.cpp b/src/Layers/xrRender/R_Backend_xform.cpp
index a9dc6d409a..22d1005cb3 100644
--- a/src/Layers/xrRender/R_Backend_xform.cpp
+++ b/src/Layers/xrRender/R_Backend_xform.cpp
@@ -43,34 +43,37 @@ void R_xforms::set_P(const Fmatrix& m)
void R_xforms::set_W_prev(const Fmatrix& m)
{
- m_w_prev.set(m);
- m_wv_prev.mul_43(m_v_prev, m_w_prev);
- m_wvp_prev.mul(m_p_prev, m_wv_prev);
+ const bool svp = Device.m_SecondViewport.IsSVPFrame(); // pip pick this viewport's prev-matrix slot
+ m_w_prev[svp].set(m);
+ m_wv_prev[svp].mul_43(m_v_prev[svp], m_w_prev[svp]);
+ m_wvp_prev[svp].mul(m_p_prev[svp], m_wv_prev[svp]);
- if (c_w_prev) RCache.set_c(c_w_prev, m_w_prev);
- if (c_wv_prev) RCache.set_c(c_wv_prev, m_wv_prev);
- if (c_wvp_prev) RCache.set_c(c_wvp_prev, m_wvp_prev);
+ if (c_w_prev[svp]) RCache.set_c(c_w_prev[svp], m_w_prev[svp]);
+ if (c_wv_prev[svp]) RCache.set_c(c_wv_prev[svp], m_wv_prev[svp]);
+ if (c_wvp_prev[svp]) RCache.set_c(c_wvp_prev[svp], m_wvp_prev[svp]);
}
void R_xforms::set_V_prev(const Fmatrix& m)
{
- m_v_prev.set(m);
- m_wv_prev.mul_43(m_v_prev, m_w_prev);
- m_vp_prev.mul(m_p_prev, m_v_prev);
- m_wvp_prev.mul(m_p_prev, m_wv_prev);
+ const bool svp = Device.m_SecondViewport.IsSVPFrame();
+ m_v_prev[svp].set(m);
+ m_wv_prev[svp].mul_43(m_v_prev[svp], m_w_prev[svp]);
+ m_vp_prev[svp].mul(m_p_prev[svp], m_v_prev[svp]);
+ m_wvp_prev[svp].mul(m_p_prev[svp], m_wv_prev[svp]);
- if (c_v_prev) RCache.set_c(c_v_prev, m_v_prev);
- if (c_vp_prev) RCache.set_c(c_vp_prev, m_vp_prev);
- if (c_wv_prev) RCache.set_c(c_wv_prev, m_wv_prev);
- if (c_wvp_prev) RCache.set_c(c_wvp_prev, m_wvp_prev);
+ if (c_v_prev[svp]) RCache.set_c(c_v_prev[svp], m_v_prev[svp]);
+ if (c_vp_prev[svp]) RCache.set_c(c_vp_prev[svp], m_vp_prev[svp]);
+ if (c_wv_prev[svp]) RCache.set_c(c_wv_prev[svp], m_wv_prev[svp]);
+ if (c_wvp_prev[svp]) RCache.set_c(c_wvp_prev[svp], m_wvp_prev[svp]);
}
void R_xforms::set_P_prev(const Fmatrix& m)
{
- m_p_prev.set(m);
- m_vp_prev.mul(m_p_prev, m_v_prev);
- m_wvp_prev.mul(m_p_prev, m_wv_prev);
- if (c_p_prev) RCache.set_c(c_p_prev, m_p_prev);
- if (c_vp_prev) RCache.set_c(c_vp_prev, m_vp_prev);
- if (c_wvp_prev) RCache.set_c(c_wvp_prev, m_wvp_prev);
+ const bool svp = Device.m_SecondViewport.IsSVPFrame();
+ m_p_prev[svp].set(m);
+ m_vp_prev[svp].mul(m_p_prev[svp], m_v_prev[svp]);
+ m_wvp_prev[svp].mul(m_p_prev[svp], m_wv_prev[svp]);
+ if (c_p_prev[svp]) RCache.set_c(c_p_prev[svp], m_p_prev[svp]);
+ if (c_vp_prev[svp]) RCache.set_c(c_vp_prev[svp], m_vp_prev[svp]);
+ if (c_wvp_prev[svp]) RCache.set_c(c_wvp_prev[svp], m_wvp_prev[svp]);
}
void R_xforms::apply_invw()
@@ -96,12 +99,15 @@ void R_xforms::unmap()
c_vp = NULL;
c_wvp = NULL;
- c_w_prev = NULL;
- c_v_prev = NULL;
- c_p_prev = NULL;
- c_wv_prev = NULL;
- c_vp_prev = NULL;
- c_wvp_prev = NULL;
+ for (int i = 0; i < 2; i++)
+ {
+ c_w_prev[i] = NULL;
+ c_v_prev[i] = NULL;
+ c_p_prev[i] = NULL;
+ c_wv_prev[i] = NULL;
+ c_vp_prev[i] = NULL;
+ c_wvp_prev[i] = NULL;
+ }
}
R_xforms::R_xforms()
@@ -115,12 +121,15 @@ R_xforms::R_xforms()
m_vp.identity();
m_wvp.identity();
- m_w_prev.identity();
- m_v_prev.identity();
- m_p_prev.identity();
- m_wv_prev.identity();
- m_vp_prev.identity();
- m_wvp_prev.identity();
+ for (int i = 0; i < 2; i++)
+ {
+ m_w_prev[i].identity();
+ m_v_prev[i].identity();
+ m_p_prev[i].identity();
+ m_wv_prev[i].identity();
+ m_vp_prev[i].identity();
+ m_wvp_prev[i].identity();
+ }
m_bInvWValid = true;
}
diff --git a/src/Layers/xrRender/R_Backend_xform.h b/src/Layers/xrRender/R_Backend_xform.h
index 2e7362ca3b..a59034c54b 100644
--- a/src/Layers/xrRender/R_Backend_xform.h
+++ b/src/Layers/xrRender/R_Backend_xform.h
@@ -13,12 +13,12 @@ class ECORE_API R_xforms
Fmatrix m_vp; // Derived - view2projection
Fmatrix m_wvp; // Derived - world2view2projection
- Fmatrix m_w_prev;
- Fmatrix m_v_prev;
- Fmatrix m_p_prev;
- Fmatrix m_wv_prev;
- Fmatrix m_vp_prev;
- Fmatrix m_wvp_prev;
+ Fmatrix m_w_prev[2]; // prev-frame matrices, per viewport (main, SVP)
+ Fmatrix m_v_prev[2];
+ Fmatrix m_p_prev[2];
+ Fmatrix m_wv_prev[2];
+ Fmatrix m_vp_prev[2];
+ Fmatrix m_wvp_prev[2];
R_constant* c_w;
R_constant* c_invw;
@@ -28,12 +28,12 @@ class ECORE_API R_xforms
R_constant* c_vp;
R_constant* c_wvp;
- R_constant* c_w_prev;
- R_constant* c_v_prev;
- R_constant* c_p_prev;
- R_constant* c_wv_prev;
- R_constant* c_vp_prev;
- R_constant* c_wvp_prev;
+ R_constant* c_w_prev[2];
+ R_constant* c_v_prev[2];
+ R_constant* c_p_prev[2];
+ R_constant* c_wv_prev[2];
+ R_constant* c_vp_prev[2];
+ R_constant* c_wvp_prev[2];
private:
bool m_bInvWValid;
diff --git a/src/Layers/xrRender/R_calculate.cpp b/src/Layers/xrRender/R_calculate.cpp
index 89e762f8e9..e22c75ff14 100644
--- a/src/Layers/xrRender/R_calculate.cpp
+++ b/src/Layers/xrRender/R_calculate.cpp
@@ -13,6 +13,15 @@ extern float r_ssaGLOD_start, r_ssaGLOD_end;
void CRender::Calculate()
{
+ // once per frame, gates the hybrid IsSVPFrame and the legacy SVP paths. true PiP is DX11-only, so it
+ // stays off on the DX10/9/8 renderers even if r__svpscope is set, otherwise the shared scope capture
+ // would divert the lens meshes into PiP maps those renderers never render or clear
+#if defined(USE_DX11)
+ Device.true_pip_on = (scope_svp_enabled != 0);
+#else
+ Device.true_pip_on = false;
+#endif
+
// Transfer to global space to avoid deep pointer access
IRender_Target* T = getTarget();
float fov_factor = _sqr(90.f / Device.fFOV);
diff --git a/src/Layers/xrRender/R_sun.cpp b/src/Layers/xrRender/R_sun.cpp
index 970eece9d6..3c460d0743 100644
--- a/src/Layers/xrRender/R_sun.cpp
+++ b/src/Layers/xrRender/R_sun.cpp
@@ -358,6 +358,27 @@ void CRender::render_sun_cascade(u32 cascade_ind)
Target->accum_direct_cascade(SE_SUN_FAR, cascade.xform,
m_sun_cascades[cascade_ind - 1].xform, cascade.bias);
+ // pip shared-shadow, the cascade smap was just built on the main atlas, re-accumulate it into the
+ // SVP (the hook re-points the atlas at the main maps so this reads them, no second smap render),
+ // the minmax SM above is generation and is skipped, the SVP reads the shared minmax
+ if (Device.m_SecondViewport.dual_accum)
+ {
+ auto svp_accum = [&]
+ {
+ Target->phase_accumulator();
+ if (cascade_ind == 0)
+ Target->accum_direct_cascade(SE_SUN_NEAR, cascade.xform, cascade.xform,
+ cascade.bias);
+ else if (cascade_ind < m_sun_cascades.size() - 1)
+ Target->accum_direct_cascade(SE_SUN_MIDDLE, cascade.xform,
+ m_sun_cascades[cascade_ind - 1].xform, cascade.bias);
+ else
+ Target->accum_direct_cascade(SE_SUN_FAR, cascade.xform,
+ m_sun_cascades[cascade_ind - 1].xform, cascade.bias);
+ };
+ Device.m_SecondViewport.dual_accum(svp_accum);
+ }
+
// Restore XForms
RCache.set_xform_world(Fidentity);
RCache.set_xform_view(Device.mView);
diff --git a/src/Layers/xrRender/Shader.h b/src/Layers/xrRender/Shader.h
index 76fa848888..e55856dfcb 100644
--- a/src/Layers/xrRender/Shader.h
+++ b/src/Layers/xrRender/Shader.h
@@ -145,7 +145,7 @@ struct ECORE_API ShaderElement : public xr_resource_flagged
u32 isLandscape : 1;
u32 isWater : 1;
- u32 iScopeLense : 2; // Redotix99: for 3D Shader Based Scopes
+ u32 iScopeLense : 4; // Redotix99: for 3D Shader Based Scopes / pip widened to 4 bits (reflex sights use ==10)
};
public:
diff --git a/src/Layers/xrRender/SkeletonCustom.h b/src/Layers/xrRender/SkeletonCustom.h
index e79e83dafd..dbe7909588 100644
--- a/src/Layers/xrRender/SkeletonCustom.h
+++ b/src/Layers/xrRender/SkeletonCustom.h
@@ -126,8 +126,8 @@ class CKinematics : public FHierrarhyVisual, public IKinematics
SkeletonWMVec wallmarks;
u32 wm_frame;
u32 CurrentFrame;
- Fmatrix Matrix_Prev;
- Fmatrix Matrix_Temp;
+ Fmatrix Matrix_Prev[2]; // per viewport (main, SVP)
+ Fmatrix Matrix_Temp[2];
// Globals
CInifile* pUserData;
@@ -178,6 +178,7 @@ class CKinematics : public FHierrarhyVisual, public IKinematics
bool PickBone(const Fmatrix& parent_xform, IKinematics::pick_result& r, float dist, const Fvector& start,
const Fvector& dir, u16 bone_id);
virtual void EnumBoneVertices(SEnumVerticesCallback& C, u16 bone_id);
+ virtual bool GetLensDetection(SLensDetection& out);
public:
CKinematics();
virtual ~CKinematics();
diff --git a/src/Layers/xrRender/SkeletonX.cpp b/src/Layers/xrRender/SkeletonX.cpp
index f4e4cd25ab..0bcbc94ac3 100644
--- a/src/Layers/xrRender/SkeletonX.cpp
+++ b/src/Layers/xrRender/SkeletonX.cpp
@@ -65,21 +65,22 @@ void CSkeletonX::_Render(ref_geom& hGeom, u32 vCount, u32 iOffset, u32 pCount)
#ifdef USE_DX11 //
if (RImplementation.o.ssfx_motionvectors)
{
- if (Device.dwFrame > Parent->CurrentFrame)
+ const bool svp = Device.m_SecondViewport.IsSVPFrame(); // pip this viewport's prev-matrix slot
+ if (Device.dwViewport > Parent->CurrentFrame)
{
// Save current frame
- Parent->CurrentFrame = Device.dwFrame;
+ Parent->CurrentFrame = Device.dwViewport;
// Save prev m_W and save current m_W for the next frame
- Parent->Matrix_Prev.set(Parent->Matrix_Temp);
- Parent->Matrix_Temp.set(RCache.xforms.m_w);
+ Parent->Matrix_Prev[svp].set(Parent->Matrix_Temp[svp]);
+ Parent->Matrix_Temp[svp].set(RCache.xforms.m_w);
// Save bone matrix to use in the next frame
for (u16 b = 0; b < Parent->LL_BoneCount(); b++)
{
CBoneInstance& Bone = Parent->LL_GetBoneInstance(b);
- Bone.mRenderTransform_prev.set(Bone.mRenderTransform_temp);
- Bone.mRenderTransform_temp.set(Bone.mRenderTransform);
+ Bone.mRenderTransform_prev[svp].set(Bone.mRenderTransform_temp[svp]);
+ Bone.mRenderTransform_temp[svp].set(Bone.mRenderTransform);
}
}
@@ -88,15 +89,15 @@ void CSkeletonX::_Render(ref_geom& hGeom, u32 vCount, u32 iOffset, u32 pCount)
{
// RM_SINGLE
Fmatrix Bone_Prev;
- Bone_Prev.mul_43(Parent->Matrix_Prev, Parent->LL_GetBoneInstance(u16(RMS_boneid)).mRenderTransform_prev);
- p_WV.mul_43(RCache.xforms.m_v_prev, Bone_Prev);
- p_WVP.mul(RCache.xforms.m_p_prev, p_WV);
+ Bone_Prev.mul_43(Parent->Matrix_Prev[svp], Parent->LL_GetBoneInstance(u16(RMS_boneid)).mRenderTransform_prev[svp]);
+ p_WV.mul_43(RCache.xforms.m_v_prev[svp], Bone_Prev);
+ p_WVP.mul(RCache.xforms.m_p_prev[svp], p_WV);
}
else
{
// RM_SKINNING_1B ~ RM_SKINNING_4B
- p_WV.mul_43(RCache.xforms.m_v_prev, Parent->Matrix_Prev);
- p_WVP.mul(RCache.xforms.m_p_prev, p_WV);
+ p_WV.mul_43(RCache.xforms.m_v_prev[svp], Parent->Matrix_Prev[svp]);
+ p_WVP.mul(RCache.xforms.m_p_prev[svp], p_WV);
}
RCache.set_c("m_wvp_prev", p_WVP); // Apply prev matrix
@@ -149,7 +150,7 @@ void CSkeletonX::_Render(ref_geom& hGeom, u32 vCount, u32 iOffset, u32 pCount)
if (RImplementation.o.ssfx_motionvectors)
{
// Save previous transform
- Fmatrix& Mprev = Parent->LL_GetBoneInstance(u16(mid)).mRenderTransform_prev;
+ Fmatrix& Mprev = Parent->LL_GetBoneInstance(u16(mid)).mRenderTransform_prev[Device.m_SecondViewport.IsSVPFrame()];
RCache.set_ca(&*array_prev, id + 0, Mprev._11, Mprev._21, Mprev._31, Mprev._41);
RCache.set_ca(&*array_prev, id + 1, Mprev._12, Mprev._22, Mprev._32, Mprev._42);
RCache.set_ca(&*array_prev, id + 2, Mprev._13, Mprev._23, Mprev._33, Mprev._43);
@@ -503,6 +504,7 @@ void get_pos_bones(const vertBoned4W& vert, Fvector& p, CKinematics* Parent)
p.add(P3);
}
+
//-----------------------------------------------------------------------------------------------------
// Wallmarks
//-----------------------------------------------------------------------------------------------------
diff --git a/src/Layers/xrRender/SkeletonX.h b/src/Layers/xrRender/SkeletonX.h
index 95b108e403..98a74130f6 100644
--- a/src/Layers/xrRender/SkeletonX.h
+++ b/src/Layers/xrRender/SkeletonX.h
@@ -99,6 +99,11 @@ class CSkeletonX
u16 bone_id, u32 iBase, u32 iCount) =0;
public:
BOOL has_visible_bones();
+ bool SVP_LensBoneXform(Fmatrix& out); // pip lens bone object-space skinning matrix for the SVP eyepiece
+ bool SVP_LensBoneVisible(); // pip lens bone visibility for the eyepiece pick
+ bool SVP_GetLensDetection(SLensDetection& out); // pip forward the owning kinematics measured lens fit for the render-side diag
+ // pip append each dedup bind-pose vertex, model space, with its global dominant bone id
+ void SVP_GatherVerts(xr_vector& positions, xr_vector& bones);
//--DSR-- SilencerOverheat_start
BOOL has_bone_id(u16 bone_id)
diff --git a/src/Layers/xrRender/dxEnvironmentRender.cpp b/src/Layers/xrRender/dxEnvironmentRender.cpp
index e2f57a5be4..90b9334af3 100644
--- a/src/Layers/xrRender/dxEnvironmentRender.cpp
+++ b/src/Layers/xrRender/dxEnvironmentRender.cpp
@@ -8,7 +8,9 @@
#include "../../xrEngine/xr_efflensflare.h"
-static Fmatrix mSky_prev = Fidentity;
+// pip per-viewport sky prev: the main and SVP passes each keep their own last-frame transform, a single
+// shared one cross-contaminates the sky motion vectors (TAA tolerates it, upscaling shows the ghosting)
+static Fmatrix mSky_prev[2] = { Fidentity, Fidentity };
//////////////////////////////////////////////////////////////////////////
// half box def
@@ -326,8 +328,9 @@ void dxEnvironmentRender::RenderSky(CEnvironment& env, bool OnlyMV)
if (OnlyMV)
{
- RCache.set_xform_world_prev(mSky_prev);
- mSky_prev = mSky;
+ const u32 svp_vp = Device.m_SecondViewport.IsSVPFrame();
+ RCache.set_xform_world_prev(mSky_prev[svp_vp]);
+ mSky_prev[svp_vp] = mSky;
RCache.set_Geometry(sh_2geom);
RCache.set_Shader(sh_2sky);
diff --git a/src/Layers/xrRender/dxRenderDeviceRender.cpp b/src/Layers/xrRender/dxRenderDeviceRender.cpp
index 911bfaea5f..4636b6ee0e 100644
--- a/src/Layers/xrRender/dxRenderDeviceRender.cpp
+++ b/src/Layers/xrRender/dxRenderDeviceRender.cpp
@@ -407,13 +407,14 @@ void dxRenderDeviceRender::End()
}
# endif
- if (!Device.m_SecondViewport.IsSVPFrame() && !Device.m_SecondViewport.isCamReady) {
+ // true PiP presents every frame, legacy suppresses on the hidden SVP frame
+ if (Device.true_pip_on || (!Device.m_SecondViewport.IsSVPFrame() && !Device.m_SecondViewport.isCamReady)) {
HW.m_pSwapChain->Present(present_interval, present_flags);
}
#else //!USE_DX10 || USE_DX11
CHK_DX(HW.pDevice->EndScene());
- if (!Device.m_SecondViewport.IsSVPFrame() && !Device.m_SecondViewport.isCamReady)
+ if (Device.true_pip_on || (!Device.m_SecondViewport.IsSVPFrame() && !Device.m_SecondViewport.isCamReady))
HW.pDevice->Present(NULL, NULL, NULL, NULL);
#endif //-USE_DX10
//HRESULT _hr = HW.pDevice->Present( NULL, NULL, NULL, NULL );
diff --git a/src/Layers/xrRender/lights_render.cpp b/src/Layers/xrRender/lights_render.cpp
index a28276c44b..6634f05c67 100644
--- a/src/Layers/xrRender/lights_render.cpp
+++ b/src/Layers/xrRender/lights_render.cpp
@@ -2,6 +2,9 @@
#include "../../xrEngine/xr_object.h"
#include "FBasicVisual.h"
#include "SkeletonCustom.h"
+#include "../../Include/xrAPI/xrAPI.h" // pip DRender, debug-line backend for r__scope_debug 3+
+#include "../../Include/xrRender/DebugRender.h" // pip IDebugRender::add_lines
+#include "xrRender_console.h" // pip scope_debug
extern int ps_r2_shadow_omnipart_vischeck;
@@ -62,6 +65,41 @@ void CRender::render_lights(light_Package& LP)
hud_light_apply(saved_pos, LP.v_point);
hud_light_apply(saved_pos, LP.v_spot);
+ // pip build the scope cone once so the mirrored svp blends can drop a light whose sphere never
+ // meets it, main accumulation below always runs the full list, only the svp mirror is filtered
+ CFrustum svp_cone;
+ const bool svp_cull_lights = Device.m_SecondViewport.dual_accum && ps_r__svp_light_cull;
+ if (svp_cull_lights)
+ {
+ Fmatrix svp_full;
+ svp_full.mul(Device.matrices[1].mProject, Device.matrices[1].mView);
+ svp_cone.CreateFromMatrix(svp_full, FRUSTUM_P_LRTB + FRUSTUM_P_FAR);
+ }
+ static xr_vector svp_mirror_subset;
+ // returns the batch subset inside the cone (cull off returns the whole batch), empty = skip the mirror
+ auto svp_cone_subset = [&](xr_vector& src) -> xr_vector&
+ {
+ if (!svp_cull_lights)
+ return src;
+ svp_mirror_subset.clear();
+ for (light* L : src)
+ {
+ Fvector wc = L->position;
+ if (svp_cone.testSphere_dirty(wc, L->range))
+ {
+ svp_mirror_subset.push_back(L);
+ if (ps_r__svp_stats) ++svp_stats_lights_mirrored; // overlay cone-cull tally
+ if (!svp_ledger_lights_mirrored) svp_ledger_lights_mirrored = 1;
+ }
+ else
+ {
+ if (ps_r__svp_stats) ++svp_stats_lights_skipped;
+ if (!svp_ledger_lights_skipped) svp_ledger_lights_skipped = 1;
+ }
+ }
+ return svp_mirror_subset;
+ };
+
{
#if defined(USE_DX10) || defined(USE_DX11)
PIX_EVENT(SHADOWED_LIGHTS);
@@ -94,6 +132,13 @@ void CRender::render_lights(light_Package& LP)
if (!L->vis.visible)
{
RImplementation.stats.ls_shadowed_invisible_skipped++;
+ if (scope_debug >= 3)
+ {
+ // pip grey world direction/range vector for each culled shadowed light (r__scope_debug 3+)
+ Fvector v[2] = { L->position, Fvector(L->direction).mul(L->range).add(L->position) };
+ u16 idx[2] = { 0, 1 };
+ DRender->add_lines(v, 2, idx, 1, 0xff999999, false);
+ }
return true;
}
@@ -241,31 +286,44 @@ void CRender::render_lights(light_Package& LP)
{
PROF_EVENT("ACCUM_SPOT");
stats.ls_shadowed_rendered += (u32)L_spot_s.size();
- for (light* L : L_spot_s)
+ // pip shared-shadow, this group's smaps are built on the main atlas, accumulate the
+ // group into the main viewport then replay it into the SVP (the hook re-points the atlas
+ // at the main maps so the SVP reads them, no second smap render)
+ auto accum_group = [&](xr_vector& list)
{
- Target->accum_spot(L);
- render_indirect(L);
- if (L->flags.bVolumetric && RImplementation.o.advancedpp && ps_r2_ls_flags.is(R2FLAG_VOLUMETRIC_LIGHTS))
+ for (light* L : list)
{
+ Target->accum_spot(L);
+ render_indirect(L);
+ if (L->flags.bVolumetric && RImplementation.o.advancedpp && ps_r2_ls_flags.is(R2FLAG_VOLUMETRIC_LIGHTS))
+ {
#ifdef USE_DX11
- float w = float(Device.dwWidth);
- float h = float(Device.dwHeight);
+ float w = float(Device.dwWidth);
+ float h = float(Device.dwHeight);
- if (RImplementation.o.ssfx_volumetric)
- Target->set_viewport_size(HW.pContext, w / RImplementation.o.volsize, h / RImplementation.o.volsize);
+ if (RImplementation.o.ssfx_volumetric)
+ Target->set_viewport_size(HW.pContext, w / RImplementation.o.volsize, h / RImplementation.o.volsize);
#endif
- if (ps_pfx_volumetric_mode == 1)
- Target->accum_volumetric_lv(L);
- else
- Target->accum_volumetric(L);
+ if (ps_pfx_volumetric_mode == 1)
+ Target->accum_volumetric_lv(L);
+ else
+ Target->accum_volumetric(L);
#ifdef USE_DX11
- // Restore resolution
- if (RImplementation.o.ssfx_volumetric)
- Target->set_viewport_size(HW.pContext, w, h);
+ // Restore resolution
+ if (RImplementation.o.ssfx_volumetric)
+ Target->set_viewport_size(HW.pContext, w, h);
#endif
+ }
}
+ };
+ accum_group(L_spot_s);
+ if (Device.m_SecondViewport.dual_accum)
+ {
+ xr_vector& mlist = svp_cone_subset(L_spot_s);
+ if (!mlist.empty())
+ Device.m_SecondViewport.dual_accum([&] { accum_group(mlist); });
}
L_spot_s.clear();
@@ -282,18 +340,28 @@ void CRender::render_lights(light_Package& LP)
#if defined(USE_DX10) || defined(USE_DX11)
PIX_EVENT(POINT_LIGHTS_ACCUM_UNSH);
#endif
- // Point lighting (unshadowed, if left)
+ // Point lighting (unshadowed, if left), pip accumulate into both viewports (no smap)
if (!LP.v_point.empty())
{
- for (light* L : LP.v_point)
+ auto accum_point = [&](xr_vector& list)
{
- L->vis_update();
- if (!L->vis.visible)
- continue;
+ for (light* L : list)
+ {
+ L->vis_update();
+ if (!L->vis.visible)
+ continue;
- Target->accum_point(L);
- ++stats.ls_unshadowed_point_rendered;
- render_indirect(L);
+ Target->accum_point(L);
+ ++stats.ls_unshadowed_point_rendered;
+ render_indirect(L);
+ }
+ };
+ accum_point(LP.v_point);
+ if (Device.m_SecondViewport.dual_accum)
+ {
+ xr_vector& mlist = svp_cone_subset(LP.v_point);
+ if (!mlist.empty())
+ Device.m_SecondViewport.dual_accum([&] { accum_point(mlist); });
}
LP.v_point.clear();
}
@@ -302,18 +370,28 @@ void CRender::render_lights(light_Package& LP)
#if defined(USE_DX10) || defined(USE_DX11)
PIX_EVENT(SPOT_LIGHTS_ACCUM_UNSH);
#endif
- // Spot lighting (unshadowed, if left)
+ // Spot lighting (unshadowed, if left), pip accumulate into both viewports (no smap)
if (!LP.v_spot.empty())
{
- for (light* L : LP.v_spot)
+ auto accum_spot = [&](xr_vector& list)
{
- L->vis_update();
- if (!L->vis.visible)
- continue;
+ for (light* L : list)
+ {
+ L->vis_update();
+ if (!L->vis.visible)
+ continue;
- Target->accum_spot(L);
- ++stats.ls_unshadowed_spot_rendered;
- render_indirect(L);
+ Target->accum_spot(L);
+ ++stats.ls_unshadowed_spot_rendered;
+ render_indirect(L);
+ }
+ };
+ accum_spot(LP.v_spot);
+ if (Device.m_SecondViewport.dual_accum)
+ {
+ xr_vector& mlist = svp_cone_subset(LP.v_spot);
+ if (!mlist.empty())
+ Device.m_SecondViewport.dual_accum([&] { accum_spot(mlist); });
}
LP.v_spot.clear();
}
diff --git a/src/Layers/xrRender/r__dsgraph_build.cpp b/src/Layers/xrRender/r__dsgraph_build.cpp
index 003c9be7e3..e050b18a73 100644
--- a/src/Layers/xrRender/r__dsgraph_build.cpp
+++ b/src/Layers/xrRender/r__dsgraph_build.cpp
@@ -42,7 +42,20 @@ void CDSGraphManager::r_dsgraph_insert_dynamic(dxRender_Visual *pVisual, Fmatrix
ShaderElement* sh_d = &*pVisual->shader->E[4];
if (!(flags.test(IRenderVisualFlags::eIgnoreOptimization) || (sh_d && sh_d->flags.bEmissive)))
{
- if (SSA < r_ssaDISCARD)
+ // pip the ssa is main-view, the magnified scope replays this shared graph and resolves
+ // mag-times smaller parts (distant NPC heads), scale the discard by the scope coverage
+ float ssa_discard = r_ssaDISCARD;
+ {
+ extern float g_pip_scope_ratio;
+ extern float g_pip_scope_magnification;
+ if (ps_r__svp_npc_detail && Device.true_pip_on && Device.m_SecondViewport.IsSVPActive()
+ && g_pip_scope_magnification > 1.f)
+ {
+ const float m = g_pip_scope_ratio * g_pip_scope_magnification;
+ ssa_discard /= m * m;
+ }
+ }
+ if (SSA < ssa_discard)
{
//Msg("SSA %.2f discarded", SSA);
return;
@@ -71,10 +84,21 @@ void CDSGraphManager::r_dsgraph_insert_dynamic(dxRender_Visual *pVisual, Fmatrix
if (sh_d && sh_d->flags.bDistort && i_mask[sh_d->flags.iPriority/2])
{
- if (i_mask[CDSGraphManager::fl_hud])
- RGraph.mapHUDSorted.Distort.emplace_back(distSQ, SSA, val_pObject, pVisual, xform, sh_d, i_mask[CDSGraphManager::fl_hud]);
- else
- RGraph.mapDynamicSorted.Distort.emplace_back(distSQ, SSA, val_pObject, pVisual, xform, sh_d, i_mask[CDSGraphManager::fl_hud]);
+ // pip the weapon's own hud-mode heat haze paints its warp across the composited lens while
+ // scoped, drop just that, psi and controller filter overlays keep rendering
+ bool drop = false;
+ if (ps_r__svp_skip_hud_distort && Device.true_pip_on && Device.m_SecondViewport.IsSVPActive())
+ {
+ IParticleCustom* pc = pVisual->dcast_ParticleCustom();
+ drop = pc && pc->GetHudMode() && pc->GetWeaponFX();
+ }
+ if (!drop)
+ {
+ if (i_mask[CDSGraphManager::fl_hud])
+ RGraph.mapHUDSorted.Distort.emplace_back(distSQ, SSA, val_pObject, pVisual, xform, sh_d, i_mask[CDSGraphManager::fl_hud]);
+ else
+ RGraph.mapDynamicSorted.Distort.emplace_back(distSQ, SSA, val_pObject, pVisual, xform, sh_d, i_mask[CDSGraphManager::fl_hud]);
+ }
}
// Select shader
@@ -89,8 +113,76 @@ void CDSGraphManager::r_dsgraph_insert_dynamic(dxRender_Visual *pVisual, Fmatrix
// Create common node
// NOTE: Invisible elements exist only in R1
-#if defined(USE_DX11) // Redotix99: for 3D Shader Based Scopes
- switch (sh->flags.iScopeLense) {
+#if defined(USE_DX11) // Redotix99: for 3D Shader Based Scopes
+ // pip true-PiP scope capture, keep the eye-nearest eyepiece lens (==3) for the SVP composite, and
+ // once the SVP is active drop the back-glass (==1) / zwrite (==2) since the SVP draws the whole lens
+ if (Device.true_pip_on && sh->flags.iScopeLense > 0)
+ {
+ if (sh->flags.iScopeLense == 3)
+ {
+ // a scope can flag several lens surfaces (objective + ocular), keep the one in front of the eye
+ // and nearest it (the ocular the player looks through) for the SVP composite, and separately
+ // keep the FARTHEST in-front surface (the objective) as real geometry for the svpscope-2 camera
+ Fvector lp, to;
+ xform->transform_tiny(lp, pVisual->getVisData().sphere.P);
+ to.sub(lp, Device.vCameraPosition);
+ const bool ahead = to.dotproduct(Device.vCameraDirection) > 0.f;
+ const float score = ahead ? to.square_magnitude() : (to.square_magnitude() + 1.0e6f);
+
+ // ocular = nearest in-front lens (the SVP composite surface, also the drawn lens)
+ auto& M = RGraph.mapScopeHUDSorted;
+ bool keep_oc = M.empty();
+ if (!keep_oc)
+ {
+ auto& f = M.front();
+ Fvector ep, te;
+ f.pMatrix->transform_tiny(ep, f.pVisual->getVisData().sphere.P);
+ te.sub(ep, Device.vCameraPosition);
+ const float fscore = (te.dotproduct(Device.vCameraDirection) > 0.f) ? te.square_magnitude() : (te.square_magnitude() + 1.0e6f);
+ keep_oc = score < fscore;
+ }
+ if (keep_oc)
+ {
+ M.clear();
+ M.emplace_back(distSQ, SSA, val_pObject, pVisual, xform, sh, i_mask[CDSGraphManager::fl_hud]);
+ }
+
+ // objective = farthest in-front lens (front of the scope), geometry only for the camera, never drawn
+ if (ahead)
+ {
+ auto& O = RGraph.mapScopeHUDObjective;
+ bool keep_obj = O.empty();
+ if (!keep_obj)
+ {
+ auto& f = O.front();
+ Fvector op, te;
+ f.pMatrix->transform_tiny(op, f.pVisual->getVisData().sphere.P);
+ te.sub(op, Device.vCameraPosition);
+ keep_obj = (te.dotproduct(Device.vCameraDirection) > 0.f) && (score > te.square_magnitude());
+ }
+ if (keep_obj)
+ {
+ O.clear();
+ O.emplace_back(distSQ, SSA, val_pObject, pVisual, xform, sh, i_mask[CDSGraphManager::fl_hud]);
+ }
+ }
+ return;
+ }
+ if (sh->flags.iScopeLense == 10)
+ {
+ // dedup per visual, the HUD capture inserts the same reflex mesh many times per frame and
+ // draw_reflex would otherwise draw it hundreds of times
+ for (const auto& n : RGraph.mapReflexHUDSorted)
+ if (n.pVisual == pVisual)
+ return;
+ RGraph.mapReflexHUDSorted.emplace_back(distSQ, SSA, val_pObject, pVisual, xform, sh, i_mask[CDSGraphManager::fl_hud]);
+ return;
+ }
+ if (Device.m_SecondViewport.IsSVPActive())
+ return; // the SVP composite draws the whole lens, skip the back-glass / zwrite
+ // a fake / not-yet-active optic, fall through to the legacy ==1/==2 handling below
+ }
+ switch (sh->flags.iScopeLense) {
case 0:
break;
diff --git a/src/Layers/xrRender/r__dsgraph_manager.h b/src/Layers/xrRender/r__dsgraph_manager.h
index ec358b80d7..01071c8a94 100644
--- a/src/Layers/xrRender/r__dsgraph_manager.h
+++ b/src/Layers/xrRender/r__dsgraph_manager.h
@@ -75,6 +75,16 @@ class CDSGraphManager : public IDSGraphManager
void AddToRenderQueue(R_dsgraph::RenderQueue& queue, const R_dsgraph::DSGraphItem& item, const SPass& pass);
void r_dsgraph_render_graph(R_dsgraph::RenderQueueArray& queue, u32 _priority, bool _clear = true, bool static_geometry = true);
+
+ // pip SVP geometry cull, active only between begin/end which renderGBuffer brackets around the SVP pass
+ static void svp_cull_begin(Fmatrix& full_xform, bool cull_world);
+ static void svp_cull_end();
+ static bool svp_cull_active();
+ static void svp_set_lod_scale(float s); // pip SVP LOD: scale captured ssa to the SVP's pixel coverage
+ static void svp_set_ssa_cull(float strength, float cov); // pip SVP small-object cull threshold
+ static bool svp_cull_reject(dxRender_Visual* V, Fmatrix* M);
+ static bool svp_cull_reject_sphere(const Fvector& c, float r);
+ static float svp_drain_lod(float ssa, float R); // pip lod for the drained weapon via the inline lod helpers
IC void r_dsgraph_render_graph(u32 _priority, bool _clear = true)
{
r_dsgraph_render_static(_priority, _clear);
@@ -94,7 +104,29 @@ class CDSGraphManager : public IDSGraphManager
template
void r_dsgraph_render_graph_sorted(R_dsgraph::mapDSGraphItems& graph, bool _clear = true);
void r_dsgraph_capture_hud();
- void r_dsgraph_render_hud();
+ void r_dsgraph_render_hud(bool _clear = true);
+ void r_dsgraph_render_hud_svp();
+ // pip hud pose latch, logic rewrites the hud matrices mid render, the SVP drain and the
+ // late scope draws reuse the exact pose the housing drew with
+ struct
+ {
+ Fmatrix* src[8];
+ Fmatrix val[8];
+ u32 count{};
+ u32 frame{u32(-1)};
+ } m_svp_pose;
+ void svp_latch_hud_poses();
+ Fmatrix* svp_pose_of(Fmatrix* p);
+ // pip lens bone latch, skinned lens bones recompute on the logic thread mid render, the camera
+ // derivation and the late lens draws reuse the bone sampled at the housing draw
+ struct
+ {
+ dxRender_Visual* vis[4];
+ Fmatrix bone[4];
+ u32 count{};
+ u32 frame{u32(-1)};
+ } m_svp_bone;
+ bool svp_lens_bone_of(dxRender_Visual* v, Fmatrix& out);
void r_dsgraph_render_hud_ui();
void r_dsgraph_render_lods(bool _setup_zb, bool _clear);
void r_dsgraph_render_sorted(bool render_hud = true);
@@ -109,7 +141,7 @@ class CDSGraphManager : public IDSGraphManager
#endif
void r_dsgraph_render_cam_ui();
void r_dsgraph_render_water_ssr();
- void r_dsgraph_render_water();
+ void r_dsgraph_render_water(bool clearGraph = true); // pip clearGraph false keeps mapWater for the next viewport
void r_dsgraph_capture(bool lights = false, bool dynamic = false, CObject* O = nullptr);
void r_dsgraph_capture_lights();
diff --git a/src/Layers/xrRender/r__dsgraph_render.cpp b/src/Layers/xrRender/r__dsgraph_render.cpp
index eb2664b506..9d186a6f5f 100644
--- a/src/Layers/xrRender/r__dsgraph_render.cpp
+++ b/src/Layers/xrRender/r__dsgraph_render.cpp
@@ -11,6 +11,7 @@
#include "fhierrarhyvisual.h"
#include "SkeletonCustom.h"
+#include "SkeletonX.h"
#include "../../xrEngine/fmesh.h"
#include "flod.h"
@@ -23,11 +24,47 @@ extern float r_ssaDONTSORT;
extern float r_ssaHZBvsTEX;
extern float r_ssaGLOD_start, r_ssaGLOD_end;
+// pip svp stats overlay, tallies the cull skips only while the overlay is on
+extern int ps_r__svp_stats;
+extern u32 svp_stats_ssa_culled;
+extern u32 svp_stats_cull_reject;
+extern u32 svp_stats_lod_scale;
+extern u32 svp_ledger_ssa_culled;
+extern u32 svp_ledger_lod_scale;
+
ICF float calcLOD(float ssa/*fDistSq*/, float R)
{
return _sqrt(clampr((ssa - r_ssaGLOD_end) / (r_ssaGLOD_start - r_ssaGLOD_end), 0.f, 1.f));
}
+// pip SVP LOD, scales the captured main-frustum ssa to the SVP's true pixel coverage,
+// armed only for the SVP gbuffer pass
+static float s_svp_ssa_scale = 1.f;
+static bool s_svp_lod_on = false;
+ICF float svp_ssa(float ssa) { return s_svp_lod_on ? ssa * s_svp_ssa_scale : ssa; }
+
+// pip SVP small-object cull, skips items below an ssa threshold pre-scaled for the SVP
+// coverage, 0 = off, armed only for the SVP gbuffer pass
+static float s_svp_ssa_cull = 0.f;
+
+// pip lod for the SVP weapon drain, the inline lod helpers stay reachable from the drain's own TU
+float CDSGraphManager::svp_drain_lod(float ssa, float R) { return calcLOD(svp_ssa(ssa), R); }
+
+void CDSGraphManager::svp_set_lod_scale(float s)
+{
+ s_svp_ssa_scale = s;
+ s_svp_lod_on = (s < 0.999f); // active only when it actually lowers detail
+ if (s_svp_lod_on && ps_r__svp_stats) ++svp_stats_lod_scale; // overlay proof the lod scale armed this frame
+ if (s_svp_lod_on) svp_ledger_lod_scale = 1; // ledger ever-armed latch
+}
+
+// strength scales the LOD-out ssa, cov divides it back to a main-frustum ssa so the loop can test item.ssa
+// directly. higher strength culls bigger items, lower cov (low mag) culls more (objects are tiny in the scope)
+void CDSGraphManager::svp_set_ssa_cull(float strength, float cov)
+{
+ s_svp_ssa_cull = (strength > 0.f && cov > 1e-4f) ? (r_ssaGLOD_end * strength / cov) : 0.f;
+}
+
template
void CDSGraphManager::r_dsgraph_render_graph_sorted(R_dsgraph::mapDSGraphItems& graph, bool _clear)
{
@@ -38,17 +75,20 @@ void CDSGraphManager::r_dsgraph_render_graph_sorted(R_dsgraph::mapDSGraphItems 0.f && !item.pMatrix && item.ssa < s_svp_ssa_cull) { if (ps_r__svp_stats) ++svp_stats_ssa_culled; if (!svp_ledger_ssa_culled) svp_ledger_ssa_culled = 1; continue; } // pip skip tiny STATIC clutter only (null matrix), never dynamic NPCs/items (matrix carriers)
dxRender_Visual* V = item.pVisual;
VERIFY(V && V->shader._get());
RCache.set_Element(item.pSE);
- RCache.set_xform_world(*item.pMatrix);
+ // hud matrices resolve through the pose latch, everything else passes through untouched
+ RCache.set_xform_world(*svp_pose_of(item.pMatrix));
RImplementation.apply_object(item.pObject);
RImplementation.apply_lmaterial();
//if (item.b_hud_mode)
//{
// //new feature
//}
- V->Render(calcLOD(item.ssa, V->vis.sphere.R));
+ V->Render(calcLOD(svp_ssa(item.ssa), V->vis.sphere.R));
}
if (_clear)
@@ -95,6 +135,8 @@ void CDSGraphManager::r_dsgraph_render_graph(RenderQueueArray& queues, u32 _prio
for (auto& packet : queue)
{
+ if (svp_cull_reject(packet.item.pVisual, packet.item.pMatrix)) { if (ps_r__svp_stats) ++svp_stats_cull_reject; continue; } // pip skip off-cone SVP geometry
+ if (s_svp_ssa_cull > 0.f && !packet.item.pMatrix && packet.item.ssa < s_svp_ssa_cull) { if (ps_r__svp_stats) ++svp_stats_ssa_culled; if (!svp_ledger_ssa_culled) svp_ledger_ssa_culled = 1; continue; } // pip skip tiny STATIC clutter only (null matrix), never dynamic NPCs/items (matrix carriers)
auto& currentKey = packet.sortKey;
if (currentKey.high != high)
{
@@ -162,7 +204,7 @@ void CDSGraphManager::r_dsgraph_render_graph(RenderQueueArray& queues, u32 _prio
RImplementation.apply_lmaterial();
}
- float LOD = calcLOD(item.ssa, item.pVisual->vis.sphere.R);
+ float LOD = calcLOD(svp_ssa(item.ssa), item.pVisual->vis.sphere.R);
#ifdef USE_DX11
RCache.LOD.set_LOD(LOD);
#endif
@@ -176,14 +218,14 @@ void CDSGraphManager::r_dsgraph_render_graph(RenderQueueArray& queues, u32 _prio
//////////////////////////////////////////////////////////////////////////
// HUD render
-void CDSGraphManager::r_dsgraph_render_hud()
+void CDSGraphManager::r_dsgraph_render_hud(bool _clear)
{
PROF_EVENT("r_dsgraph_render_hud");
CHudInitializer initializer(true);
// Rendering
RImplementation.rmNear();
- r_dsgraph_render_graph_sorted(RGraph.mapHUD);
+ r_dsgraph_render_graph_sorted(RGraph.mapHUD, _clear);
RImplementation.rmNormal();
@@ -244,9 +286,10 @@ void CDSGraphManager::r_dsgraph_render_sorted(bool render_hud)
{
{
PROF_EVENT("r_dsgraph_render_sorted");
- // Rendering
- r_dsgraph_render_graph_sorted(RGraph.mapStaticSorted.Sorted, true);
- r_dsgraph_render_graph_sorted(RGraph.mapDynamicSorted.Sorted, true);
+ // combine order, only the last pass (main) clears the shared sorted list, else main loses translucents
+ const bool clear_sorted = svp_clear_shared_list(true);
+ r_dsgraph_render_graph_sorted(RGraph.mapStaticSorted.Sorted, clear_sorted);
+ r_dsgraph_render_graph_sorted(RGraph.mapDynamicSorted.Sorted, clear_sorted);
}
if (render_hud)
@@ -260,7 +303,8 @@ void CDSGraphManager::r_dsgraph_render_sorted(bool render_hud)
CHudInitializer initializer(2);
// Rendering
- r_dsgraph_render_graph_sorted(RGraph.mapCamAttachedSorted.Sorted, true);
+ // combine order, main clears last so cam-attached overlays survive the scope pass
+ r_dsgraph_render_graph_sorted(RGraph.mapCamAttachedSorted.Sorted, svp_clear_shared_list(true));
RImplementation.rmNormal();
}
}
@@ -293,12 +337,23 @@ void CDSGraphManager::r_dsgraph_render_sorted_hud()
{
PROF_EVENT("r_dsgraph_render_sorted_hud");
#if RENDER==R_R4
- HW.pContext->CopyResource(RImplementation.Target->rt_Accumulator->pSurface, RImplementation.Target->rt_Generic_0->pSurface);
+ {
+ // guard the seed CopyResource, it is a silent no-op + debug-layer spam when accum and generic0
+ // formats mismatch (MSAA vs 1x, or HDR-off A8 vs A16F)
+ auto* dst = (ID3DTexture2D*)RImplementation.Target->rt_Accumulator->pSurface;
+ auto* src = (ID3DTexture2D*)RImplementation.Target->rt_Generic_0->pSurface;
+ D3D_TEXTURE2D_DESC dd, sd;
+ dst->GetDesc(&dd);
+ src->GetDesc(&sd);
+ if (dd.Format == sd.Format && dd.SampleDesc.Count == sd.SampleDesc.Count && dd.Width == sd.Width && dd.Height == sd.Height)
+ HW.pContext->CopyResource(dst, src);
+ }
#endif
CHudInitializer initializer(true);
RImplementation.rmNear();
- r_dsgraph_render_graph_sorted(RGraph.mapHUDSorted.Sorted, true);
+ // combine order, main clears last so hud-sorted overlays survive the scope pass
+ r_dsgraph_render_graph_sorted(RGraph.mapHUDSorted.Sorted, svp_clear_shared_list(true));
RImplementation.rmNormal();
}
@@ -351,7 +406,7 @@ void CDSGraphManager::r_dsgraph_render_water_ssr()
#endif
}
-void CDSGraphManager::r_dsgraph_render_water()
+void CDSGraphManager::r_dsgraph_render_water(bool clearGraph)
{
PROF_EVENT("r_dsgraph_render_water_ssr");
std::sort(RGraph.mapWater.begin(), RGraph.mapWater.end());
@@ -378,7 +433,10 @@ void CDSGraphManager::r_dsgraph_render_water()
V->Render(calcLOD(N.ssa, V->vis.sphere.R));
}
- RGraph.mapWater.clear();
+ // pip keep the shared captured water list for the next viewport, only the final (main) pass clears
+ // it, the SVP combine runs first and must not consume mapWater or the main loses its periphery water
+ if (clearGraph)
+ RGraph.mapWater.clear();
}
//////////////////////////////////////////////////////////////////////////
@@ -387,14 +445,15 @@ void CDSGraphManager::r_dsgraph_render_wmarks()
{
PROF_EVENT("r_dsgraph_render_wmarks");
#if RENDER!=R_R1
- // Rendering
- r_dsgraph_render_graph_sorted(RGraph.mapStaticSorted.Wmark);
- r_dsgraph_render_graph_sorted(RGraph.mapDynamicSorted.Wmark);
+ // gbuffer order, main drains wmarks first so the last pass (SVP when scoped) clears
+ const bool clear_wmark = svp_clear_shared_list(false);
+ r_dsgraph_render_graph_sorted(RGraph.mapStaticSorted.Wmark, clear_wmark);
+ r_dsgraph_render_graph_sorted(RGraph.mapDynamicSorted.Wmark, clear_wmark);
// HACK: Calculate this only once
CHudInitializer initalizer(true);
RImplementation.rmNear();
- r_dsgraph_render_graph_sorted(RGraph.mapHUDSorted.Wmark);
+ r_dsgraph_render_graph_sorted(RGraph.mapHUDSorted.Wmark, clear_wmark);
RImplementation.rmNormal();
#endif
}
@@ -404,14 +463,20 @@ void CDSGraphManager::r_dsgraph_render_wmarks()
void CDSGraphManager::r_dsgraph_render_distort()
{
PROF_EVENT("r_dsgraph_render_distort");
+ // combine order, main clears distort last, else main loses distortion + the bDistort-gated screen filters
+ const bool _clear = svp_clear_shared_list(true);
// Rendering
- r_dsgraph_render_graph_sorted(RGraph.mapStaticSorted.Distort, true);
- r_dsgraph_render_graph_sorted(RGraph.mapDynamicSorted.Distort, true);
+ r_dsgraph_render_graph_sorted(RGraph.mapStaticSorted.Distort, _clear);
+ r_dsgraph_render_graph_sorted(RGraph.mapDynamicSorted.Distort, _clear);
+ // pip the muzzle heat billboard sits on the entrance pupil in the scope pass and warps the whole
+ // image, world distortion above stays, the hud layer is skipped
+ if (ps_r__svp_skip_hud_distort && Device.true_pip_on && Device.m_SecondViewport.m_render_pass_is_svp)
+ return;
// HACK: Calculate this only once
CHudInitializer initalizer(true);
RImplementation.rmNear();
- r_dsgraph_render_graph_sorted(RGraph.mapHUDSorted.Distort);
+ r_dsgraph_render_graph_sorted(RGraph.mapHUDSorted.Distort, _clear);
RImplementation.rmNormal();
}
diff --git a/src/Layers/xrRender/r__dsgraph_types.h b/src/Layers/xrRender/r__dsgraph_types.h
index 21b9e6b1bb..466a930d3d 100644
--- a/src/Layers/xrRender/r__dsgraph_types.h
+++ b/src/Layers/xrRender/r__dsgraph_types.h
@@ -251,6 +251,8 @@ namespace R_dsgraph
mapDSGraphItems mapWater;
#ifdef USE_DX11
mapDSGraphItems mapScopeHUDSorted;
+ mapDSGraphItems mapScopeHUDObjective; // pip the FRONT (objective) scope lens, real geometry for the svpscope-2 camera, never drawn
+ mapDSGraphItems mapReflexHUDSorted; // pip reflex-sight lenses (iScopeLense==10)
mapDSGraphItems mapScopeHUD;
#endif
template
@@ -329,6 +331,8 @@ namespace R_dsgraph
#ifdef USE_DX11
mapScopeHUD.clear_and_free();
mapScopeHUDSorted.clear_and_free();
+ mapScopeHUDObjective.clear_and_free();
+ mapReflexHUDSorted.clear_and_free();
#endif
}
else
@@ -347,6 +351,8 @@ namespace R_dsgraph
#ifdef USE_DX11
mapScopeHUD.clear();
mapScopeHUDSorted.clear();
+ mapScopeHUDObjective.clear();
+ mapReflexHUDSorted.clear();
#endif
}
}
diff --git a/src/Layers/xrRender/rendertarget_phase_nightvision.cpp b/src/Layers/xrRender/rendertarget_phase_nightvision.cpp
index 790ebe2b21..5beaa7ddf1 100644
--- a/src/Layers/xrRender/rendertarget_phase_nightvision.cpp
+++ b/src/Layers/xrRender/rendertarget_phase_nightvision.cpp
@@ -1,4 +1,5 @@
#include "stdafx.h"
+#include "../../xrEngine/environment.h"
void CRenderTarget::phase_nightvision()
{
@@ -198,19 +199,3 @@ void CRenderTarget::phase_heatvision()
};
//--DSR-- HeatVision_start
-#if defined(USE_DX11) // Redotix99: for 3D Shader Based Scopes (sorry for using the nightvision phase file)
-void CRenderTarget::phase_3DSSReticle()
-{
- HW.pContext->CopyResource(rt_Generic_2->pTexture->surface_get(), RImplementation.Target->rt_Position->pTexture->surface_get());
-
- HW.pContext->CopyResource(rt_Generic_temp->pTexture->surface_get(), rt_Generic_0->pTexture->surface_get());
-
- u_setrt(RImplementation.Target->rt_Generic_0, RImplementation.Target->rt_Position, 0, HW.pBaseZB);
-
- RCache.set_CullMode(CULL_CCW);
- RCache.set_Stencil(FALSE);
- RCache.set_ColorWriteEnable();
-
- RImplementation.render_Reticle();
-};
-#endif
diff --git a/src/Layers/xrRender/rendertarget_phase_smaa.cpp b/src/Layers/xrRender/rendertarget_phase_smaa.cpp
index aedf7b320c..aa32e09e1e 100644
--- a/src/Layers/xrRender/rendertarget_phase_smaa.cpp
+++ b/src/Layers/xrRender/rendertarget_phase_smaa.cpp
@@ -104,6 +104,14 @@ void CRenderTarget::phase_smaa()
void CRenderTarget::phase_ssfx_taa()
{
+ // a scope edge swaps the content source under the resolve, seed the history with the current
+ // frame so a stale scene never ghosts through
+ if (m_taa_seed_history)
+ {
+ m_taa_seed_history = false;
+ HW.pContext->CopyResource(rt_ssfx_prev_frame->pTexture->surface_get(), rt_Generic_0->pTexture->surface_get());
+ }
+
u32 Offset = 0;
Fvector2 p0, p1;
@@ -136,6 +144,23 @@ void CRenderTarget::phase_ssfx_taa()
HW.pContext->CopyResource(rt_ssfx_taa->pTexture->surface_get(), rt_ssfx_accum->pTexture->surface_get());
+ // main pass only, stamp taa alpha 1 over the scope footprint so the resolve keeps the svp
+ // viewport taa there instead of smearing on the near-zero hud motion vectors
+ if (ps_r__svp_taa_mask && Device.true_pip_on && !Device.m_SecondViewport.m_render_pass_is_svp
+ && this == RImplementation.TargetMain && Device.m_SecondViewport.IsSVPActive()
+ && !RImplementation.GMBase.RGraph.mapScopeHUDSorted.empty())
+ {
+ EnsureScopeShaders();
+ u_setrt(rt_ssfx_taa, nullptr, nullptr, nullptr);
+ draw_scope(s_svp_taa_stamp, []()
+ {
+ RCache.set_c("scope_phase", 0);
+ RCache.set_ColorWriteEnable(D3DCOLORWRITEENABLE_ALPHA);
+ });
+ RCache.set_ColorWriteEnable();
+ if (ps_r__svp_stats) ++svp_stats_taa_stamp; // overlay proof the sovereignty stamp fired
+ }
+
// TAA
ref_rt& dest_rt = RImplementation.o.dx10_msaa ? rt_Generic : rt_Color;
diff --git a/src/Layers/xrRender/svp_console.cpp b/src/Layers/xrRender/svp_console.cpp
new file mode 100644
index 0000000000..a18424d38f
--- /dev/null
+++ b/src/Layers/xrRender/svp_console.cpp
@@ -0,0 +1,264 @@
+#include "stdafx.h"
+#pragma hdrstop
+
+#include "svp_console.h"
+
+#include "../../xrEngine/xr_ioconsole.h"
+#include "../../xrEngine/xr_ioc_cmd.h"
+
+int scope_svp_enabled = 0; // true PiP second viewport scope (0 off, 1 eyepiece, 2 objective)
+float ps_r__svp_render_scale = 1.0f; // SVP render scale, 1.0 keeps the dwHeight/2 per side
+float ps_r__svp_supersample = 1.0f; // SVP supersample: render the SVP square larger so the eyepiece downsamples it (SSAA), 1.0 = off, 2.0 = 4x SVP pixels
+int ps_r__svp_diag = 0; // SVP perf diagnostics: throttled log of [SVP-RES] over-render ratio + [SVP-ALLOC] target size while scoped, 0 = off
+int ps_r__svp_cop_diag = 0; // svp optics diagnostics, 1 = throttled [SVPCOP]/[SVP-HUD]/[SVP-BARREL], 2 = adds the per-frame [SVP-AIM] reticle-vs-screen-center delta
+int ps_r__svp_report = 0; // svp one-shot report, 1 re-dumps [SVP-CFG] + [SVP-FILES] on the next scoped frame then clears itself
+int ps_r__svp_stats = 0; // per-viewport render stats overlay, 0 off, 1 compact table, 2 adds the per-section breakdown
+u32 svp_stats_ssa_culled = 0; // svp small-object cull tally the overlay reads, incremented in r__dsgraph_render while the overlay is on
+u32 svp_stats_cull_reject = 0; // svp off-cone frustum-reject tally the overlay reads, incremented in r__dsgraph_render
+u32 svp_stats_cull_reject_ident = 0; // svp identity-matrix sorted world statics the cone rejects, incremented in svp_cull_reject
+u32 svp_stats_lights_mirrored = 0; // svp lights the cone cull mirrors into the scope, incremented in lights_render
+u32 svp_stats_lights_skipped = 0; // svp lights the cone cull drops, incremented in lights_render
+u32 svp_stats_taa_stamp = 0; // svp taa sovereignty stamp fires the overlay reads, incremented in phase_ssfx_taa
+u32 svp_stats_nvg_split = 0; // svp nvg tube split fires the overlay reads, incremented in phase_combine
+u32 svp_stats_lod_scale = 0; // svp lod scale armed frames the overlay reads, incremented in svp_set_lod_scale
+u32 svp_stats_hud_cull_reject = 0; // svp hud drain cone rejects the overlay reads, incremented in svp_hud_latch
+u32 svp_stats_grass_cull_reject = 0; // svp grass cone rejects the overlay reads, incremented in the detail manager
+u32 svp_stats_reflex_proxy = 0; // svp reflex proxy draws the overlay reads, incremented in draw_reflex_proxy
+u32 svp_stats_distort_guard = 0; // svp distort guard stamps the overlay reads, incremented in phase_combine
+u32 svp_stats_nvg_sky = 0; // svp nvg sky lum remaps the overlay reads, incremented in phase_combine
+u32 svp_stats_disc_latch = 0; // svp adaptive-res disc latch moves the overlay reads, incremented in phase_3DSSReticle
+u32 svp_stats_fwd_keep = 0; // svp forward-list keeps the overlay reads, incremented in render_forward
+u32 svp_stats_optic_resolve = 0; // svp optics bus resolve fires the overlay reads, incremented in svp_optics_resolve
+// session-lifetime ever-fired ledger latches, one per instrumented gated path, never per-frame
+// reset, the [SVP-LEDGER] sweep flags any still zero while its gate cvar is on
+u32 svp_ledger_cull_reject = 0;
+u32 svp_ledger_ssa_culled = 0;
+u32 svp_ledger_cull_reject_ident = 0;
+u32 svp_ledger_lights_mirrored = 0;
+u32 svp_ledger_lights_skipped = 0;
+u32 svp_ledger_lod_scale = 0;
+u32 svp_ledger_hud_cull_reject = 0;
+u32 svp_ledger_grass_cull_reject = 0;
+u32 svp_ledger_reflex_proxy = 0;
+u32 svp_ledger_distort_guard = 0;
+u32 svp_ledger_nvg_sky = 0;
+u32 svp_ledger_disc_latch = 0;
+u32 svp_ledger_fwd_keep = 0;
+int ps_r__3db_debug = 0; // 3db ballistics overlay, 1 = bone markers + fire axis + crosshair ray + sight line + mrad log, 2 = adds the zeroed ray, 3 = adds fading shot tracers
+float ps_r__svp_adaptive_res = 1.2f; // adaptive SVP resolution: size the SVP render to the on-screen eyepiece disc * this margin. 0 = off (full svp_height), 1.0 = render exactly at the disc (sharpest, mild pan shimmer), 1.2 = keep ~1.2x SSAA. big oculars clamp to no-op
+float ps_r__svp_lod = 0.0f; // SVP LOD reduction strength [0..1]: scale the scope's LOD selection to its true pixel coverage (coarser at low mag, capped at the main view so zoomed detail is never worse). 0 = off
+float ps_r__svp_cull_ssa = 4.0f; // SVP small-object cull strength: skip scope geometry below this * the LOD-out ssa, scaled by magnification (tiny distant clutter at low mag). 0 = off, higher = more aggressive
+int ps_r__svp_dlss = 0; // SVP DLSS-SR master gate, 0 = stock (render_scale inert), nonzero = scaffolding active
+// svpscope 2 geometric objective (single-lens scopes have no distinct front lens to capture, derive it
+// along the optical axis from the eyepiece), lengths are in eyepiece radii (the only mesh-scale-robust
+// unit), refined per scope from real objective_mm later
+float ps_r__svp_obj_dist = 1.0f; // svpscope 2 objective: scale on the AUTO geomscan front distance (1.0 = raw auto, fixed 14r fallback when geomscan finds nothing)
+float ps_r__svp_obj_size = 1.0f; // svpscope 2 objective radius = eyepiece_radius * this (authored-set median, mod_system_3dss_objective_lenses n=67, 0.65 = the old tuned value)
+int ps_r__svp_focal_derive = 1; // svpscope 2 focal anchors derive from eye relief + tube length (0 = the original 0.4/0.6 split)
+int ps_r__svp_glare_model = 1; // veiling glare falloff: 1 = Stiles-Holladay 1/theta^2 vs the scope's half fov, 0 = legacy pow6 cone
+int ps_r__svp_photo_model = 1; // eye photometrics: 1 = Moon-Spencer pupil + squared relative brightness, 0 = legacy linear models
+int ps_r__svp_adaptive_grow = 0; // 1 sizes the SVP up to the disc past the half-height base, costs up to 4x the pixels on large oculars (0 = shrink-only, stock cost)
+int ps_r__svp_drain_anchor = 1; // the drain clip-on window widens to the authored front plane (0 = the fixed 1.4-2.6 window)
+int ps_r__svp_settle_derive = 1; // eyebox settle + recoil windows derive from the weapon's authored timing (0 = flat 400/250/1000ms)
+int ps_r__svp_ratio_derive = 1; // ratio latch band derives from the measured noise, never looser than the fixed band (0 = fixed 25%/2%)
+int ps_r__svp_lens_reject = 1; // while aimed, reject an eyepiece pose that jumps far (a stale or exo-arm pose) and hold the last good one (0 = take every pose)
+int ps_r__svp_recoil_hold = 1; // the scope optics derivation (fov reference + fit band) holds through the post-shot window (0 = live)
+int ps_r__svp_roll_stabilize = 0; // svp level the scope world on lean/cant (0 = realistic image tilts with the cant, default; 1 = leveled)
+int ps_r__svp_clean_optics = 1; // svp strip the 3DSS fake cosmetics (parallax shadow, chromatism, nvg blur, fisheye) for a clean scope (1 = stripped, default; 0 = full 3DSS look)
+int ps_r__svp_distort_guard = 1; // svp stamp the distort mask neutral over the composited lens so the combine warp is identity there (0 = let the lens warp with the main view)
+int ps_r__svp_jitterfix = 1; // svp lens edge jitter pre-pass, likely superseded by the sentinel jitter, 0 skips it for the a/b
+int ps_r__svp_taa_mask = 1; // svp stamp the taa mask over the composited lens so the main resolve keeps the svp viewport taa there (0 = let the main taa re-resolve the lens)
+int ps_r__svp_hud_fov_match = 2; // svp barrel mapping: 2 magnifies with the wheel, 1 fixed 1:1 window, 0 legacy
+int ps_r__svp_bloom = 1; // svp bloom on the scope pass so magnified bright sources flare (0 = off)
+int ps_r__svp_local_exposure = 1; // svp scope-local eye adaptation, the image grades by the scope's own measured exposure (0 = main exposure)
+float ps_r__svp_exposure_bias = 0.0f; // svp local-exposure bias in stops
+int ps_r__svp_light_capture = 1; // svp capture lights for the scope cone past the main frustum/fog cull (0 = main-view lights only)
+int ps_r__svp_npc_detail = 1; // svp keep dynamic parts the main view discards as subpixel (distant NPC heads), the scope resolves them (0 = stock discard)
+int ps_r__svp_thermal_sim = 1; // svp digital-sensor sim on thermal displays: sensor cell grid + per-cell noise (0 = clean optical image)
+float ps_r__svp_twilight = 1.0f; // svp exit-pupil twilight dimming: zooming shrinks the exit pupil below the dark-adapted eye and the image dims, day scenes unaffected (0 = off)
+float ps_r__svp_parallax = 0.0f; // svp reticle parallax, 0 = pinned center (default), 1 = the real eye deflection response
+float ps_r__svp_near_blur = 1.0f; // svp near-field defocus strength on the scope image (svpscope 2, 0 = off)
+int ps_r__svp_nearblur_scatter = 0; // svp near-blur composite, 0 = gather default look, 1 = scatter accumulator
+float ps_r__svp_focus_m = 100.0f; // svp parallax focus distance in meters, objects off this plane defocus by the thin lens law
+int ps_r__svp_authored_optics = 1; // svp use the authored per-scope scope_objective_lens_offset for the front plane + defocus aperture (0 = measured geometry only)
+int ps_r__svp_measured_optics = 0; // svp fill the objective offset + mm from mesh detection when authored ltx is absent (0 = authored/live only, byte-identical)
+float ps_r__svp_eyebox = 1.0f; // svp eyebox strength, console unregistered, 1.0 keeps the anchor bound publish alive
+int ps_r__svp_reflex_capture = 0; // svp reflex/holo dot path, 0 = legacy 1x main-view overlay, 1 = collimated proxy inside the magnifier viewport (default 0 until in-game confirms the proxy)
+float ps_s3ds_objective_mm = 0.f; // per-scope objective clear aperture mm from spec sheets, pushed by zzz_extra_scope_features (0 = mesh-relative fallback)
+float ps_s3ds_middle_grey = 0.f; // per-scope SVP tonemap middle-grey override, pushed by zzz_extra_scope_features (0 = inherit main)
+float ps_s3ds_adapt_speed = 0.f; // per-scope SVP tonemap adaptation speed override (0 = inherit main)
+int ps_r__svp_chroma = 1; // svp keep the authored per-scope chromatic aberration on glass under true PiP, scaled by zoom (0 = stripped with clean optics)
+float ps_r__svp_reticle_washout = 0.0f; // svp illuminated reticle wash-out vs a bright background, glow only (0 = off)
+float ps_r__svp_field_curve = 1.0f; // svp field curvature edge softness, outer field blurs like a real non-flat-field scope (0 = flat)
+int ps_r__svp_acog_fiber = 0; // svp ACOG fiber reticle brightness source, 1 = sun visibility (fiber gathers sunlight), 0 = scene luminance
+float ps_r__svp_veiling_glare = 0.0f; // svp veiling glare strength, off-axis sun scatter washes the image near the sun (0 = off)
+float ps_r__svp_rain_optic = 1.0f; // svp rain droplets on the objective glass, scaled by rain density (0 = off)
+float ps_r__svp_rain_debug = 0.0f; // svp forces the scope rain regardless of weather, the value stands in for rain density (0 = live weather)
+int ps_r__svp_uv_debug = 0; // svp debug: paint the scope by the sample-coord V (green top red bottom) to spot a flipped lens UV
+int ps_r__svp_autoflip = 1; // svp auto-correct a mesh-inverted lens (world upside down) by detecting the flipped sample-coord V (1 = on)
+int ps_r__svp_autoflip_reticle = 1; // svp auto-correct a mesh-inverted reticle to match the auto-flipped world (1 = on)
+float ps_r__svp_coating = 0.0f; // svp lens coating, typical multi-coated glass transmission loss + faint warm tint (0 = off, 1 = physical)
+float ps_r__svp_mirage = 0.0f; // svp heat mirage on distant ground, grows with sun heat and magnification (0 = off)
+int ps_r__svp_flat_window = 1; // svp flat-screen optics render the panel's see-through window subtense so magnification tracks the stock look (1 = on)
+float ps_r__svp_sharpen = 0.35f; // svp contrast-adaptive sharpen on the scope image (0 = off, 1 = max)
+float ps_r__svp_sharpen_falloff = 0.0f; // svp sharpen radial falloff toward the disc rim, crisp center soft edge (0 = uniform)
+float ps_r__svp_sharpen_inner = 0.0f; // svp sharpen inner crisp-zone radius before the falloff starts (0 = from center)
+float ps_r__svp_nvg_bleach = 0.0f; // svp NVG highlight bleach roll-off, replaces the hard clamp so bright sources compress not clip (0 = off, stock)
+float ps_r__svp_nvg_sensitivity = 1.0f; // svp NVG bleach onset sensitivity, higher rolls off dimmer sources
+int ps_r__svp_hud_full = 2; // svpscope 1 body skip only, mode 2 clips at the pupil camera near plane instead renders (default), 1 = full barrel from the eye + near-blur, 0 = objective clip with the body rendered
+int ps_r__svp_near_pupil = 0; // svp mode 2 push the scope near clip to the pupil front plane so behind-pupil barrel falls behind it (0 = main-view near plane)
+int ps_r__svp_drain_clip = 0; // svp clip the drained barrel wholly behind the authored objective plane, w2 near-blur dissolves the surviving sliver (0 = drain the whole barrel)
+int ps_r__svp_cull = 1; // svp cull the scope geometry to the scope frustum, the SVP re-submits the whole main-frustum world otherwise (1 = on)
+int ps_r__svp_skip_motionblur = 0; // svp skip motion blur on the scope pass, magnified blur is an artifact and a small cost (0 = keep)
+int ps_r__svp_skip_hud_distort = 1; // svp skip hud-layer distortion on the scope pass, the muzzle heat billboard sits on the entrance pupil and warps the image (1 = skip)
+// note r__svp_distort_guard owns the lens footprint in the main view, 0 here shows warp there only with the guard also 0
+int ps_r__svp_skip_dof = 0; // svp skip the scope-internal dof pass, main dof still covers the composited lens (0 = current doubled behavior)
+int ps_r__svp_wpn_dof = 1; // svp pass the sss weapon dof constants through while scoped, 0 zeroes them like before
+int ps_r__svp_skip_lut = 0; // svp skip the scope-internal lut grade, main grade still covers the composited lens (0 = current doubled behavior)
+int ps_r__svp_emissive = 1; // svp replay self-illum geometry into the scope image so lamps glow through the scope (0 = main-only emissive)
+int ps_r__svp_skip_ssr = 1; // svp scope reflections, 0 reflective water + SSR, 1 flat water + SSR (default), 2 flat water + no SSR
+int ps_r__svp_skip_volumetric = 0; // svp skip volumetric lights on the scope pass, subtle at magnification (0 = keep)
+int ps_r__svp_skip_grass = 0; // svp skip grass/details on the scope pass, near grass is mostly off a zoomed cone (0 = keep)
+int ps_r__svp_sss_sun = 0; // svp compute the scope SSS pass and keep the sun contact shadow term on the scope, expensive (0 = off)
+int ps_r__svp_cull_grass = 1; // svp cull grass instances to the scope cone instead of replaying the whole main field (1 = on)
+int ps_r__svp_light_cull = 1; // svp cone-cull the mirrored light blends, skip a light whose sphere never meets the scope cone (1 = on, 0 = mirror everything)
+int ps_r__svp_corner_mask = 1; // svp stencil the dead corners outside the eyepiece disc so the lighting + combine passes skip them (1 = on)
+int scope_debug = 0;
+
+void svp_console_init()
+{
+#if defined(USE_DX11)
+ // true PiP scope cvars are DX11-only, do not register them on the DX10/9/8 renderers (the backing
+ // vars keep their 0 defaults so the shared code still reads them as off)
+ CMD4(CCC_Integer, "r__svpscope", &scope_svp_enabled, 0, 2);
+ CMD4(CCC_Float, "r__svp_render_scale", &ps_r__svp_render_scale, 0.5f, 1.0f); // takes effect on vid_restart, floor matches the runtime clamp
+ CMD4(CCC_Float, "r__svp_supersample", &ps_r__svp_supersample, 1.0f, 2.0f); // SSAA the magnified scope image, 1.0 = off (4x SVP cost at 2.0)
+ CMD4(CCC_Integer, "r__svp_diag", &ps_r__svp_diag, 0, 1); // SVP perf diagnostics log (0 = off)
+ CMD4(CCC_Integer, "r__svp_cop_diag", &ps_r__svp_cop_diag, 0, 2); // svp optics log (1 = throttled, 2 = + per-frame [SVP-AIM])
+ CMD4(CCC_Integer, "r__svp_report", &ps_r__svp_report, 0, 1); // svp one-shot [SVP-CFG]+[SVP-FILES] re-dump, self-clearing
+ CMD4(CCC_Integer, "r__svp_stats", &ps_r__svp_stats, 0, 2); // per-viewport render stats overlay (1 = compact, 2 = per-section breakdown)
+ CMD4(CCC_Integer, "r__3db_debug", &ps_r__3db_debug, 0, 3); // 3db overlay (1 = markers + axes, 2 = + zeroed ray, 3 = + tracers)
+ CMD4(CCC_Float, "r__svp_adaptive_res", &ps_r__svp_adaptive_res, 0.0f, 2.0f); // size SVP render to the eyepiece disc * margin (0 = off, 1.2 recommended)
+ CMD4(CCC_Float, "r__svp_lod", &ps_r__svp_lod, 0.0f, 1.0f); // SVP LOD reduction strength (0 = off)
+ CMD4(CCC_Float, "r__svp_cull_ssa", &ps_r__svp_cull_ssa, 0.0f, 8.0f); // SVP small-object cull strength (0 = off)
+ CMD4(CCC_Integer, "r__svp_dlss", &ps_r__svp_dlss, 0, 1); // SVP DLSS-SR scaffolding gate, 0 = stock
+ CMD4(CCC_Float, "r__svp_obj_dist", &ps_r__svp_obj_dist, 0.0f, 3.0f); // svpscope 2 objective: scale on the auto geomscan front distance
+ CMD4(CCC_Float, "r__svp_obj_size", &ps_r__svp_obj_size, 0.1f, 6.0f); // svpscope 2 geometric objective: objective radius (eyepiece radii)
+ CMD4(CCC_Integer, "r__svp_focal_derive", &ps_r__svp_focal_derive, 0, 1); // svpscope 2 focal anchors from eye relief + tube length (0 = 0.4/0.6 split)
+ CMD4(CCC_Integer, "r__svp_glare_model", &ps_r__svp_glare_model, 0, 1); // veiling glare falloff model (0 = legacy pow6)
+ CMD4(CCC_Integer, "r__svp_photo_model", &ps_r__svp_photo_model, 0, 1); // eye photometric models (0 = legacy linear)
+ CMD4(CCC_Integer, "r__svp_adaptive_grow", &ps_r__svp_adaptive_grow, 0, 1); // adaptive res grows to the disc (0 = shrink-only)
+ CMD4(CCC_Integer, "r__svp_drain_anchor", &ps_r__svp_drain_anchor, 0, 1); // drain clip-on window anchors on the authored front (0 = fixed window)
+ CMD4(CCC_Integer, "r__svp_settle_derive", &ps_r__svp_settle_derive, 0, 1); // eyebox windows from authored weapon timing (0 = flat ms)
+ CMD4(CCC_Integer, "r__svp_ratio_derive", &ps_r__svp_ratio_derive, 0, 1); // ratio latch band from measured noise (0 = fixed band)
+ CMD4(CCC_Integer, "r__svp_lens_reject", &ps_r__svp_lens_reject, 0, 1); // while aimed, reject a far-jumping eyepiece pose and hold the last good (0 = take every pose)
+ CMD4(CCC_Integer, "r__svp_recoil_hold", &ps_r__svp_recoil_hold, 0, 1); // scope optics derivation holds through the post-shot window (0 = live)
+ CMD4(CCC_Integer, "r__svp_roll_stabilize", &ps_r__svp_roll_stabilize, 0, 1); // svp keep the scope world level on lean/cant (0 = realistic image-tilts-with-cant)
+ CMD4(CCC_Integer, "r__svp_clean_optics", &ps_r__svp_clean_optics, 0, 1); // strip 3DSS fake cosmetics (parallax shadow/chromatism/nvg blur/fisheye), 0 = full look
+ CMD4(CCC_Integer, "r__svp_distort_guard", &ps_r__svp_distort_guard, 0, 1); // stamp distort-mask neutral over the composited lens (0 = lens warps with main view)
+ CMD4(CCC_Integer, "r__svp_jitterfix", &ps_r__svp_jitterfix, 0, 1); // svp lens-edge jitter pre-pass (0 skips, superseded-by-sentinel a/b)
+ CMD4(CCC_Integer, "r__svp_taa_mask", &ps_r__svp_taa_mask, 0, 1); // stamp the taa mask over the composited lens (0 = let the main taa re-resolve it)
+ CMD4(CCC_Integer, "r__svp_hud_fov_match", &ps_r__svp_hud_fov_match, 0, 2); // svp barrel mapping: 2 magnified, 1 window, 0 legacy
+ CMD4(CCC_Integer, "r__svp_bloom", &ps_r__svp_bloom, 0, 1); // svp bloom on the scope pass
+ CMD4(CCC_Integer, "r__svp_local_exposure", &ps_r__svp_local_exposure, 0, 1); // svp scope-local eye adaptation
+ CMD4(CCC_Float, "r__svp_exposure_bias", &ps_r__svp_exposure_bias, -3.0f, 3.0f); // svp local-exposure bias in stops
+ CMD4(CCC_Integer, "r__svp_light_capture", &ps_r__svp_light_capture, 0, 1); // svp scope-cone light capture
+ CMD4(CCC_Integer, "r__svp_npc_detail", &ps_r__svp_npc_detail, 0, 1); // svp keep subpixel dynamic parts for the scope (0 = stock)
+ CMD4(CCC_Integer, "r__svp_thermal_sim", &ps_r__svp_thermal_sim, 0, 1); // svp thermal digital-sensor sim (0 = clean)
+ CMD4(CCC_Float, "r__svp_twilight", &ps_r__svp_twilight, 0.0f, 1.0f); // svp exit-pupil twilight dimming strength (0 = off)
+ CMD4(CCC_Float, "r__svp_parallax", &ps_r__svp_parallax, 0.0f, 10.0f); // svp true-scale reticle parallax, 1 = physical sub-pixel (0 = pinned)
+ CMD4(CCC_Float, "r__svp_near_blur", &ps_r__svp_near_blur, 0.0f, 3.0f); // svp near-field defocus strength (0 = off)
+ CMD4(CCC_Integer, "r__svp_nearblur_scatter", &ps_r__svp_nearblur_scatter, 0, 1); // svp near-blur composite (0 = gather default, 1 = scatter)
+ CMD4(CCC_Float, "r__svp_focus_m", &ps_r__svp_focus_m, 10.0f, 1000.0f); // svp parallax focus distance m (fixed-parallax scopes sit at 100)
+ CMD4(CCC_Integer, "r__svp_authored_optics", &ps_r__svp_authored_optics, 0, 1); // svp use authored per-scope objective offset for front plane + aperture (0 = measured only)
+ CMD4(CCC_Integer, "r__svp_measured_optics", &ps_r__svp_measured_optics, 0, 1); // measured lens geometry fills unauthored optics (0 = off, authored/live only)
+ CMD4(CCC_Integer, "r__svp_reflex_capture", &ps_r__svp_reflex_capture, 0, 1); // svp reflex/holo dot path (0 = legacy 1x main-view overlay, 1 = collimated proxy in the magnifier viewport)
+ CMD4(CCC_Float, "s3ds_objective_mm", &ps_s3ds_objective_mm, 0.0f, 200.0f); // per-scope objective clear aperture mm for the true exit pupil
+ CMD4(CCC_Float, "s3ds_middle_grey", &ps_s3ds_middle_grey, 0.0f, 2.0f); // per-scope SVP tonemap middle-grey (0 = inherit main)
+ CMD4(CCC_Float, "s3ds_adapt_speed", &ps_s3ds_adapt_speed, 0.0f, 20.0f); // per-scope SVP tonemap adaptation speed (0 = inherit main)
+ CMD4(CCC_Integer, "r__svp_chroma", &ps_r__svp_chroma, 0, 1); // svp keep authored chromatic aberration on glass, zoom scaled (0 = stripped)
+ CMD4(CCC_Float, "r__svp_reticle_washout", &ps_r__svp_reticle_washout, 0.0f, 2.0f); // svp illuminated reticle wash-out strength (0 = off)
+ CMD4(CCC_Float, "r__svp_field_curve", &ps_r__svp_field_curve, 0.0f, 3.0f); // svp field curvature edge softness (0 = flat field)
+ CMD4(CCC_Integer, "r__svp_acog_fiber", &ps_r__svp_acog_fiber, 0, 1); // svp ACOG fiber brightness source (1 = sun visibility, 0 = scene luminance)
+ CMD4(CCC_Float, "r__svp_veiling_glare", &ps_r__svp_veiling_glare, 0.0f, 3.0f); // svp veiling glare near the sun (0 = off)
+ CMD4(CCC_Float, "r__svp_rain_optic", &ps_r__svp_rain_optic, 0.0f, 3.0f); // svp rain droplets on the objective (0 = off)
+ CMD4(CCC_Float, "r__svp_rain_debug", &ps_r__svp_rain_debug, 0.0f, 3.0f); // svp force scope rain, value = density stand-in (0 = live weather)
+ CMD4(CCC_Integer, "r__svp_uv_debug", &ps_r__svp_uv_debug, 0, 1); // svp debug: paint sample-coord V to spot a flipped lens UV
+ CMD4(CCC_Integer, "r__svp_autoflip", &ps_r__svp_autoflip, 0, 1); // svp auto-correct a mesh-inverted lens (1 = on)
+ CMD4(CCC_Integer, "r__svp_autoflip_reticle", &ps_r__svp_autoflip_reticle, 0, 1); // svp auto-correct a mesh-inverted reticle (1 = on)
+ CMD4(CCC_Float, "r__svp_coating", &ps_r__svp_coating, 0.0f, 1.0f); // svp lens coating transmission loss + warm tint (0 = off, 1 = physical)
+ CMD4(CCC_Float, "r__svp_mirage", &ps_r__svp_mirage, 0.0f, 3.0f); // svp heat mirage on distant ground, sun + magnification driven (0 = off)
+ CMD4(CCC_Integer, "r__svp_flat_window", &ps_r__svp_flat_window, 0, 1); // svp flat-screen window subtense fov (1 = on)
+ CMD4(CCC_Float, "r__svp_sharpen", &ps_r__svp_sharpen, 0.0f, 1.0f); // svp contrast-adaptive sharpen (0 = off)
+ CMD4(CCC_Float, "r__svp_sharpen_falloff", &ps_r__svp_sharpen_falloff, 0.0f, 1.0f); // svp sharpen radial falloff (0 = uniform)
+ CMD4(CCC_Float, "r__svp_sharpen_inner", &ps_r__svp_sharpen_inner, 0.0f, 1.0f); // svp sharpen inner crisp radius (0 = from center)
+ CMD4(CCC_Float, "r__svp_nvg_bleach", &ps_r__svp_nvg_bleach, 0.0f, 1.0f); // svp NVG highlight bleach roll-off (0 = off, stock clamp)
+ CMD4(CCC_Float, "r__svp_nvg_sensitivity", &ps_r__svp_nvg_sensitivity, 0.1f, 4.0f); // svp NVG bleach onset sensitivity
+ CMD4(CCC_Integer, "r__svp_hud_full", &ps_r__svp_hud_full, 0, 2); // svp barrel: 2 front-plane clip (default), 1 full barrel from the eye, 0 objective clip + body
+ CMD4(CCC_Integer, "r__svp_near_pupil", &ps_r__svp_near_pupil, 0, 1); // svp mode 2 push the near clip to the pupil front plane (0 = main-view near)
+ CMD4(CCC_Integer, "r__svp_drain_clip", &ps_r__svp_drain_clip, 0, 1); // svp clip the drained barrel behind the authored objective plane (0 = whole barrel)
+ CMD4(CCC_Integer, "r__svp_cull", &ps_r__svp_cull, 0, 1); // svp frustum cull the scope geometry (1 = on)
+ CMD4(CCC_Integer, "r__svp_skip_motionblur", &ps_r__svp_skip_motionblur, 0, 1); // svp skip motion blur on the scope
+ CMD4(CCC_Integer, "r__svp_skip_hud_distort", &ps_r__svp_skip_hud_distort, 0, 1); // svp skip hud-layer distortion on the scope
+ CMD4(CCC_Integer, "r__svp_skip_dof", &ps_r__svp_skip_dof, 0, 1); // svp scope-internal dof once-only gate
+ CMD4(CCC_Integer, "r__svp_wpn_dof", &ps_r__svp_wpn_dof, 0, 1); // svp sss weapon dof while scoped
+ CMD4(CCC_Integer, "r__svp_skip_lut", &ps_r__svp_skip_lut, 0, 1); // svp scope-internal lut once-only gate
+ CMD4(CCC_Integer, "r__svp_emissive", &ps_r__svp_emissive, 0, 1); // svp self-illum in the scope image
+ CMD4(CCC_Integer, "r__svp_skip_ssr", &ps_r__svp_skip_ssr, 0, 2); // svp scope reflections level (0 expensive, 1 regular, 2 cheapest)
+ CMD4(CCC_Integer, "r__svp_skip_volumetric", &ps_r__svp_skip_volumetric, 0, 1); // svp skip volumetric lights on the scope
+ CMD4(CCC_Integer, "r__svp_skip_grass", &ps_r__svp_skip_grass, 0, 1); // svp skip grass/details on the scope
+ CMD4(CCC_Integer, "r__svp_sss_sun", &ps_r__svp_sss_sun, 0, 1); // svp SSS sun contact shadows on the scope
+ CMD4(CCC_Integer, "r__svp_cull_grass", &ps_r__svp_cull_grass, 0, 1); // svp cull grass to the scope cone
+ CMD4(CCC_Integer, "r__svp_light_cull", &ps_r__svp_light_cull, 0, 1); // svp cone-cull the mirrored light blends (1 = on)
+ CMD4(CCC_Integer, "r__svp_corner_mask", &ps_r__svp_corner_mask, 0, 1); // svp stencil-skip the dead disc corners (1 = on)
+ CMD4(CCC_Integer, "r__scope_debug", &scope_debug, 0, 4);
+#endif
+}
+
+// [SVP-LEDGER] {counter, gate} table, only rows where a gate on means the counter should tick in
+// normal scoped play, the situational latches (hud drain, reflex proxy, nvg sky) stay out of it
+namespace
+{
+ struct svp_ledger_row
+ {
+ const char* name;
+ const u32* counter; // session latch
+ const int* gate_i; // int cvar gate, nonzero = on (null skips this test)
+ const float* gate_f; // float cvar gate, > thr = on (null skips this test)
+ float gate_thr;
+ };
+ static const svp_ledger_row s_svp_ledger[] = {
+ { "cull_reject", &svp_ledger_cull_reject, &ps_r__svp_cull, nullptr, 0.f },
+ { "ssa_culled", &svp_ledger_ssa_culled, nullptr, &ps_r__svp_cull_ssa, 0.f },
+ { "cull_reject_ident", &svp_ledger_cull_reject_ident, &ps_r__svp_cull, nullptr, 0.f },
+ { "lights_mirrored", &svp_ledger_lights_mirrored, &ps_r__svp_light_cull, nullptr, 0.f },
+ { "lights_skipped", &svp_ledger_lights_skipped, &ps_r__svp_light_cull, nullptr, 0.f },
+ { "lod_scale", &svp_ledger_lod_scale, nullptr, &ps_r__svp_lod, 0.f },
+ { "grass_cull_reject", &svp_ledger_grass_cull_reject, &ps_r__svp_cull_grass, nullptr, 0.f },
+ { "distort_guard", &svp_ledger_distort_guard, &ps_r__svp_distort_guard, nullptr, 0.f },
+ { "disc_latch", &svp_ledger_disc_latch, nullptr, &ps_r__svp_adaptive_res, 0.f },
+ { "fwd_keep", &svp_ledger_fwd_keep, nullptr, nullptr, 0.f },
+ };
+}
+
+// prints [SVP-LEDGER] once per session, a gate on with its latch still zero is a maybe-inert path
+void svp_ledger_sweep()
+{
+ u32 checked = 0, inert = 0;
+ for (const svp_ledger_row& r : s_svp_ledger)
+ {
+ const bool on = (r.gate_i ? (*r.gate_i != 0) : true) && (r.gate_f ? (*r.gate_f > r.gate_thr) : true);
+ if (!on)
+ continue;
+ ++checked;
+ if (*r.counter == 0)
+ {
+ PipMsg("[SVP-LEDGER] INERT? %s gate on counter zero", r.name);
+ ++inert;
+ }
+ }
+ if (inert == 0)
+ PipMsg("[SVP-LEDGER] ledger ok (%u checked)", checked);
+ else
+ PipMsg("[SVP-LEDGER] %u inert of %u checked", inert, checked);
+}
diff --git a/src/Layers/xrRender/svp_console.h b/src/Layers/xrRender/svp_console.h
new file mode 100644
index 0000000000..1d6c70b48e
--- /dev/null
+++ b/src/Layers/xrRender/svp_console.h
@@ -0,0 +1,132 @@
+#ifndef svp_consoleH
+#define svp_consoleH
+#pragma once
+
+// true PiP second viewport (SVP) console vars, the complete extern set
+// definitions and registration live in svp_console.cpp
+
+extern ECORE_API int scope_svp_enabled; // true PiP scope mode (0 off, 1 eyepiece, 2 objective)
+extern ECORE_API int scope_debug; // scope debug overlay level
+extern ECORE_API int ps_r__3db_debug; // ballistics overlay level
+
+// render / perf
+extern ECORE_API float ps_r__svp_render_scale;
+extern ECORE_API float ps_r__svp_supersample;
+extern ECORE_API int ps_r__svp_diag;
+extern ECORE_API int ps_r__svp_cop_diag;
+extern ECORE_API int ps_r__svp_report;
+extern ECORE_API int ps_r__svp_stats; // per-viewport render stats overlay (0 off, 1 compact, 2 breakdown)
+extern ECORE_API u32 svp_stats_ssa_culled; // svp small-object cull tally read by the overlay, incremented in r__dsgraph_render
+extern ECORE_API u32 svp_stats_cull_reject; // svp off-cone frustum-reject tally read by the overlay, incremented in r__dsgraph_render
+extern ECORE_API u32 svp_stats_cull_reject_ident; // svp identity-matrix sorted world statics the cone rejects, incremented in svp_cull_reject
+extern ECORE_API u32 svp_stats_lights_mirrored; // svp lights the cone cull mirrors into the scope, incremented in lights_render
+extern ECORE_API u32 svp_stats_lights_skipped; // svp lights the cone cull drops, incremented in lights_render
+extern ECORE_API u32 svp_stats_taa_stamp; // svp taa sovereignty stamp fires read by the overlay, incremented in phase_ssfx_taa
+extern ECORE_API u32 svp_stats_nvg_split; // svp nvg tube split fires read by the overlay, incremented in phase_combine
+extern ECORE_API u32 svp_stats_lod_scale; // svp lod scale armed frames read by the overlay, incremented in svp_set_lod_scale
+extern ECORE_API u32 svp_stats_hud_cull_reject; // svp hud drain cone rejects read by the overlay, incremented in svp_hud_latch
+extern ECORE_API u32 svp_stats_grass_cull_reject; // svp grass cone rejects read by the overlay, incremented in the detail manager
+extern ECORE_API u32 svp_stats_reflex_proxy; // svp reflex proxy draws read by the overlay, incremented in draw_reflex_proxy
+extern ECORE_API u32 svp_stats_distort_guard; // svp distort guard stamps read by the overlay, incremented in phase_combine
+extern ECORE_API u32 svp_stats_nvg_sky; // svp nvg sky lum remaps read by the overlay, incremented in phase_combine
+extern ECORE_API u32 svp_stats_disc_latch; // svp adaptive-res disc latch moves read by the overlay, incremented in phase_3DSSReticle
+extern ECORE_API u32 svp_stats_fwd_keep; // svp forward-list keeps read by the overlay, incremented in render_forward
+// session-lifetime ever-fired ledger latches, set at the gated call sites, swept once per session
+extern ECORE_API u32 svp_ledger_cull_reject;
+extern ECORE_API u32 svp_ledger_ssa_culled;
+extern ECORE_API u32 svp_ledger_cull_reject_ident;
+extern ECORE_API u32 svp_ledger_lights_mirrored;
+extern ECORE_API u32 svp_ledger_lights_skipped;
+extern ECORE_API u32 svp_ledger_lod_scale;
+extern ECORE_API u32 svp_ledger_hud_cull_reject;
+extern ECORE_API u32 svp_ledger_grass_cull_reject;
+extern ECORE_API u32 svp_ledger_reflex_proxy;
+extern ECORE_API u32 svp_ledger_distort_guard;
+extern ECORE_API u32 svp_ledger_nvg_sky;
+extern ECORE_API u32 svp_ledger_disc_latch;
+extern ECORE_API u32 svp_ledger_fwd_keep;
+extern ECORE_API float ps_r__svp_adaptive_res;
+extern ECORE_API float ps_r__svp_lod;
+extern ECORE_API float ps_r__svp_cull_ssa;
+extern ECORE_API int ps_r__svp_dlss;
+extern ECORE_API int ps_r__svp_adaptive_grow;
+extern ECORE_API int ps_r__svp_cull;
+extern ECORE_API int ps_r__svp_cull_grass;
+extern ECORE_API int ps_r__svp_light_cull;
+extern ECORE_API int ps_r__svp_corner_mask;
+extern ECORE_API int ps_r__svp_skip_motionblur;
+extern ECORE_API int ps_r__svp_skip_hud_distort;
+extern ECORE_API int ps_r__svp_skip_dof;
+extern ECORE_API int ps_r__svp_wpn_dof;
+extern ECORE_API int ps_r__svp_skip_lut;
+extern ECORE_API int ps_r__svp_emissive;
+extern ECORE_API int ps_r__svp_skip_ssr;
+extern ECORE_API int ps_r__svp_skip_volumetric;
+extern ECORE_API int ps_r__svp_skip_grass;
+extern ECORE_API int ps_r__svp_sss_sun;
+
+// optics derivation
+extern ECORE_API float ps_r__svp_obj_dist;
+extern ECORE_API float ps_r__svp_obj_size;
+extern ECORE_API int ps_r__svp_focal_derive;
+extern ECORE_API int ps_r__svp_glare_model;
+extern ECORE_API int ps_r__svp_photo_model;
+extern ECORE_API int ps_r__svp_drain_anchor;
+extern ECORE_API int ps_r__svp_settle_derive;
+extern ECORE_API int ps_r__svp_ratio_derive;
+extern ECORE_API int ps_r__svp_lens_reject;
+extern ECORE_API int ps_r__svp_recoil_hold;
+extern ECORE_API int ps_r__svp_roll_stabilize;
+extern ECORE_API int ps_r__svp_clean_optics;
+extern ECORE_API int ps_r__svp_distort_guard;
+extern ECORE_API int ps_r__svp_jitterfix;
+extern ECORE_API int ps_r__svp_taa_mask;
+extern ECORE_API int ps_r__svp_hud_fov_match;
+extern ECORE_API int ps_r__svp_bloom;
+extern ECORE_API int ps_r__svp_local_exposure;
+extern ECORE_API float ps_r__svp_exposure_bias;
+extern ECORE_API int ps_r__svp_light_capture;
+extern ECORE_API int ps_r__svp_npc_detail;
+extern ECORE_API int ps_r__svp_thermal_sim;
+extern ECORE_API float ps_r__svp_twilight;
+extern ECORE_API float ps_r__svp_parallax;
+extern ECORE_API float ps_r__svp_near_blur;
+extern ECORE_API float ps_r__svp_focus_m;
+extern ECORE_API int ps_r__svp_authored_optics;
+extern ECORE_API float ps_r__svp_eyebox;
+extern ECORE_API int ps_r__svp_reflex_capture;
+extern ECORE_API int ps_r__svp_measured_optics; // measured lens geometry fills unauthored optics, registered in svp_console.cpp
+
+// per-scope overrides pushed by zzz_extra_scope_features.script
+extern ECORE_API float ps_s3ds_objective_mm;
+extern ECORE_API float ps_s3ds_middle_grey;
+extern ECORE_API float ps_s3ds_adapt_speed;
+
+// glass / reticle cosmetics
+extern ECORE_API int ps_r__svp_chroma;
+extern ECORE_API float ps_r__svp_reticle_washout;
+extern ECORE_API float ps_r__svp_field_curve;
+extern ECORE_API int ps_r__svp_acog_fiber;
+extern ECORE_API float ps_r__svp_veiling_glare;
+extern ECORE_API float ps_r__svp_rain_optic;
+extern ECORE_API float ps_r__svp_rain_debug;
+extern ECORE_API int ps_r__svp_uv_debug;
+extern ECORE_API int ps_r__svp_autoflip;
+extern ECORE_API int ps_r__svp_autoflip_reticle;
+extern ECORE_API float ps_r__svp_coating;
+extern ECORE_API float ps_r__svp_mirage;
+extern ECORE_API int ps_r__svp_flat_window;
+extern ECORE_API float ps_r__svp_sharpen;
+extern ECORE_API float ps_r__svp_sharpen_falloff;
+extern ECORE_API float ps_r__svp_sharpen_inner;
+extern ECORE_API float ps_r__svp_nvg_bleach;
+extern ECORE_API float ps_r__svp_nvg_sensitivity;
+extern ECORE_API int ps_r__svp_hud_full;
+
+// registers every svp console command, called once from xrRender_initconsole
+extern void svp_console_init();
+
+// prints the [SVP-LEDGER] inert-path sweep, called once per session from svpCamera
+extern void svp_ledger_sweep();
+
+#endif
diff --git a/src/Layers/xrRender/svp_constants.cpp b/src/Layers/xrRender/svp_constants.cpp
new file mode 100644
index 0000000000..43edb02b9e
--- /dev/null
+++ b/src/Layers/xrRender/svp_constants.cpp
@@ -0,0 +1,200 @@
+#include "stdafx.h"
+#pragma hdrstop
+
+#include "ResourceManager.h"
+#include "blenders\Blender_Recorder.h"
+#include "blenders\Blender.h"
+
+#include "../../xrEngine/igame_persistent.h"
+#include "../../xrEngine/environment.h"
+
+#include "dxRenderDeviceRender.h"
+
+#include "svp_constants.h"
+
+// pip intent flags for the patched 3DSS shaders, x/y/z = strip parallax shadow / chromatism /
+// nvg blur under true PiP, the per-image-type judgment lives in the shader
+static class svp_control_binder : public R_constant_setup
+{
+ virtual void setup(R_constant* C)
+ {
+ extern int ps_r__svp_clean_optics;
+ extern int ps_r__svp_thermal_sim;
+ extern int ps_r__svp_chroma;
+ const float k = (ps_r__svp_clean_optics && Device.true_pip_on) ? 1.f : 0.f;
+ // chromatism kept when r__svp_chroma is on so real per-scope CA survives clean optics
+ const float ky = (ps_r__svp_clean_optics && !ps_r__svp_chroma && Device.true_pip_on) ? 1.f : 0.f;
+ const float ts = (ps_r__svp_thermal_sim && Device.true_pip_on) ? 1.f : 0.f;
+ RCache.set_c(C, k, ky, k, ts);
+ }
+} binder_svp_control;
+
+// glass optics tunables, x = illuminated reticle washout, y = field curvature edge softness, z = ACOG fiber sun mode
+static class svp_glass_binder : public R_constant_setup
+{
+ virtual void setup(R_constant* C)
+ {
+ extern float ps_r__svp_reticle_washout;
+ extern float ps_r__svp_field_curve;
+ extern int ps_r__svp_acog_fiber;
+ extern int ps_r__svp_autoflip_reticle;
+ if (Device.true_pip_on)
+ RCache.set_c(C, ps_r__svp_reticle_washout, ps_r__svp_field_curve, ps_r__svp_acog_fiber ? 1.f : 0.f, ps_r__svp_autoflip_reticle ? 1.f : 0.f);
+ else
+ RCache.set_c(C, 0.f, 0.f, 0.f, 0.f);
+ }
+} binder_svp_glass;
+
+// near-blur composite selector, x = scatter mode (0 gather default, 1 scatter accumulator)
+static class svp_nearblur_binder : public R_constant_setup
+{
+ virtual void setup(R_constant* C)
+ {
+ extern int ps_r__svp_nearblur_scatter;
+ RCache.set_c(C, ps_r__svp_nearblur_scatter ? 1.f : 0.f, 0.f, 0.f, 0.f);
+ }
+} binder_svp_nearblur;
+
+// glass environment data, x = veiling glare (sun near the scope boresight scatters off the coatings), y = rain
+static class svp_env_binder : public R_constant_setup
+{
+ virtual void setup(R_constant* C)
+ {
+ extern float ps_r__svp_veiling_glare;
+ if (!Device.true_pip_on)
+ {
+ RCache.set_c(C, 0.f, 0.f, 0.f, 0.f);
+ return;
+ }
+ CEnvDescriptor& desc = *g_pGamePersistent->Environment().CurrentEnv;
+ const Fvector& fwd = Device.m_SecondViewport.svp_fwd;
+ // sun_dir points FROM the sun, so -dot is how much the scope points TOWARD it
+ float align = -desc.sun_dir.dotproduct(fwd);
+ float sun_luma = 0.299f * desc.sun_color.x + 0.587f * desc.sun_color.y + 0.114f * desc.sun_color.z;
+ float glare = 0.f;
+ if (align > 0.f && ps_r__svp_veiling_glare > 0.f)
+ {
+ extern int ps_r__svp_glare_model;
+ float fall;
+ if (ps_r__svp_glare_model)
+ {
+ // Stiles-Holladay disability glare, veil falls as 1/theta^2 off axis, the floor is
+ // the scope's own half fov where the sun enters the image and stops being veil
+ const float t0 = _max(rad2deg(Device.m_SecondViewport.svp_fov) * 0.5f, 2.f);
+ const float th = _max(rad2deg(acosf(_min(align, 1.f))), t0);
+ fall = (t0 / th) * (t0 / th);
+ }
+ else
+ fall = powf(align, 6.f);
+ glare = fall * (sun_luma > 1.f ? 1.f : sun_luma) * ps_r__svp_veiling_glare;
+ }
+ extern float ps_r__svp_rain_optic;
+ extern float ps_r__svp_rain_debug;
+ extern int ps_r__svp_uv_debug;
+ extern int ps_r__svp_autoflip;
+ float rain = (ps_r__svp_rain_optic > 0.f) ? desc.rain_density * ps_r__svp_rain_optic : 0.f;
+ // debug forces the droplets in any weather, the value stands in for the density product
+ if (ps_r__svp_rain_debug > 0.f)
+ rain = ps_r__svp_rain_debug;
+ RCache.set_c(C, glare, rain, ps_r__svp_uv_debug ? 1.f : 0.f, ps_r__svp_autoflip ? 1.f : 0.f);
+ }
+} binder_svp_env;
+
+// scope magnification (curMag/minMag/maxMag/fov), pip overrides with the engine range, else 3DSS Lua passthrough
+extern Fvector4 ps_shader_scope_params;
+extern float g_pip_scope_magnification;
+extern float g_pip_scope_min_mag;
+extern float g_pip_scope_max_mag;
+extern float g_pip_scope_ratio;
+static class shader_scope_params : public R_constant_setup
+{
+ virtual void setup(R_constant* C)
+ {
+#if 1 // pip drive curMag/minMag so the reticle current_zoom lands on mag/min (matches the 2D reticle)
+ if (Device.true_pip_on && g_pip_scope_magnification > 0.01f)
+ {
+ // curMag = ratio*scope bounds the exit-pupil shadow, minMag solved so current_zoom = mag/min
+ const float r = g_pip_scope_ratio;
+ const float mn_eng = (g_pip_scope_min_mag > 0.01f) ? g_pip_scope_min_mag : g_pip_scope_magnification;
+ const float zoom_ratio = g_pip_scope_magnification / mn_eng; // 1.0 at min zoom, scope zoom ratio at max
+ const float cur0 = r * g_pip_scope_magnification;
+ const float span = (zoom_ratio - 1.0f) * 2.5f; // (cur - mn) * 0.4 + 1 == zoom_ratio
+ // the shader divides by minMag so it stays above zero, scaling cur and mx by the
+ // same factor keeps the zoom span and the dial ratio exact instead of a floor
+ const float k = (cur0 < span + 0.01f) ? (span + 0.01f) / cur0 : 1.0f;
+ const float cur = cur0 * k;
+ const float mn = cur - span;
+ const float mx = k * r * ((g_pip_scope_max_mag > 0.01f) ? g_pip_scope_max_mag : g_pip_scope_magnification);
+ // w = -2 is the true-PiP sentinel, the legacy Lua writes -1 so patched shaders gate on
+ // w < -1.5 and stay inert at svpscope 0
+ RCache.set_c(C, cur, mn, mx, Device.m_SecondViewport.IsSVPActive() ? -2.f : ps_shader_scope_params.w);
+ }
+ else
+#endif
+ if (ps_shader_scope_params.y > 0.f)
+ RCache.set_c(C, ps_shader_scope_params.x, ps_shader_scope_params.y, ps_shader_scope_params.z, ps_shader_scope_params.w);
+ else if (Device.true_pip_on)
+ {
+ // engine fallback for a scope with no Lua data, only meaningful while PiP drives the mags
+ const float cur = g_pip_scope_magnification;
+ const float mn = (g_pip_scope_min_mag > 0.f) ? g_pip_scope_min_mag : cur;
+ const float mx = (g_pip_scope_max_mag > 0.f) ? g_pip_scope_max_mag : cur;
+ RCache.set_c(C, cur, mn, mx, 0.0f);
+ }
+ else
+ // svpscope 0 passes the authored vector through untouched
+ RCache.set_c(C, ps_shader_scope_params.x, ps_shader_scope_params.y, ps_shader_scope_params.z, ps_shader_scope_params.w);
+ }
+} shader_scope_params;
+
+static class ssfx_issvp : public R_constant_setup
+{
+ virtual void setup(R_constant* C)
+ {
+ // pip force_water_reflect and force_svp_sss make this read 0 so the water and sun take their non SVP path
+ const bool force0 = Device.m_SecondViewport.force_water_reflect || Device.m_SecondViewport.force_svp_sss;
+ const float issvp = force0 ? 0.f : float(Device.m_SecondViewport.IsSVPFrame());
+ RCache.set_c(C, issvp, 0, 0, 0);
+ }
+} ssfx_issvp;
+
+// one table drives both registration and the startup guard so the two never drift
+static const struct { const char* name; R_constant_setup* setup; } s_svp_binders[] = {
+ { "shader_scope_params", &shader_scope_params }, // scope magnification
+ { "svp_control", &binder_svp_control }, // pip clean-optics intent flags
+ { "svp_glass", &binder_svp_glass }, // pip glass optics tunables
+ { "svp_env", &binder_svp_env }, // pip glass environment data (glare, rain)
+ { "ssfx_issvp", &ssfx_issvp },
+ { "svp_nearblur_mode", &binder_svp_nearblur }, // pip near-blur composite selector
+};
+
+void RegisterSvpConstants(CBlender_Compile& dst)
+{
+ static bool s_validated = false;
+ if (!s_validated) { s_validated = true; ValidateSvpConstants(); }
+ for (const auto& b : s_svp_binders)
+ dst.r_Constant(b.name, b.setup);
+}
+
+// name-matched binders fail silently, so log the table health once at first use
+void ValidateSvpConstants()
+{
+ const int n = (int)(sizeof(s_svp_binders) / sizeof(s_svp_binders[0]));
+ int bad = 0;
+ for (int i = 0; i < n; ++i)
+ {
+ if (!s_svp_binders[i].setup)
+ {
+ Msg("! [SVP-GUARD] MISSING %s (null binder)", s_svp_binders[i].name);
+ ++bad;
+ continue;
+ }
+ for (int j = 0; j < i; ++j)
+ if (0 == xr_strcmp(s_svp_binders[j].name, s_svp_binders[i].name))
+ {
+ Msg("! [SVP-GUARD] MISSING %s (duplicate registration)", s_svp_binders[i].name);
+ ++bad;
+ }
+ }
+ Msg("[SVP-GUARD] binders ok (%d)", n - bad);
+}
diff --git a/src/Layers/xrRender/svp_constants.h b/src/Layers/xrRender/svp_constants.h
new file mode 100644
index 0000000000..b46b847db0
--- /dev/null
+++ b/src/Layers/xrRender/svp_constants.h
@@ -0,0 +1,14 @@
+#ifndef svp_constantsH
+#define svp_constantsH
+#pragma once
+
+class CBlender_Compile;
+
+// registers every PiP auto-bound shader constant with its binder, called once from
+// CBlender_Compile::SetMapping, a name registered without its binder never binds
+void RegisterSvpConstants(CBlender_Compile& dst);
+
+// startup guard, logs the svp binder table health once and flags any null or duplicate
+void ValidateSvpConstants();
+
+#endif
diff --git a/src/Layers/xrRender/svp_dsgraph_cull.cpp b/src/Layers/xrRender/svp_dsgraph_cull.cpp
new file mode 100644
index 0000000000..fd6a4c53e9
--- /dev/null
+++ b/src/Layers/xrRender/svp_dsgraph_cull.cpp
@@ -0,0 +1,67 @@
+#include "stdafx.h"
+
+#include "../../xrEngine/render.h"
+#include "FBasicVisual.h"
+
+// pip SVP geometry cull, rejects items outside the SVP frustum before the draw, armed only
+// between svp_cull_begin/end around the SVP gbuffer pass
+static CFrustum s_svp_cull_frustum;
+static bool s_svp_cull_on = false; // frustum armed for this SVP gbuffer pass
+static bool s_svp_cull_world = false; // also reject world geometry, grass culling can arm the frustum alone
+
+// svp stats overlay counters, mirror the r__dsgraph_render extern style
+extern int ps_r__svp_stats;
+extern u32 svp_stats_cull_reject_ident;
+extern u32 svp_ledger_cull_reject;
+extern u32 svp_ledger_cull_reject_ident;
+
+void CDSGraphManager::svp_cull_begin(Fmatrix& full_xform, bool cull_world)
+{
+ s_svp_cull_frustum.CreateFromMatrix(full_xform, FRUSTUM_P_LRTB + FRUSTUM_P_FAR);
+ s_svp_cull_on = true;
+ s_svp_cull_world = cull_world;
+}
+
+void CDSGraphManager::svp_cull_end()
+{
+ s_svp_cull_on = false;
+ s_svp_cull_world = false;
+}
+
+bool CDSGraphManager::svp_cull_active()
+{
+ return s_svp_cull_on;
+}
+
+// grass cull, the detail manager replays the main frustum field on the SVP pass, reject instances off
+// the scope cone, the radius covers a blade so the root can sit a little outside without popping
+bool CDSGraphManager::svp_cull_reject_sphere(const Fvector& c, float r)
+{
+ if (!s_svp_cull_on)
+ return false;
+ Fvector wc = c;
+ return !s_svp_cull_frustum.testSphere_dirty(wc, r);
+}
+
+bool CDSGraphManager::svp_cull_reject(dxRender_Visual* V, Fmatrix* M)
+{
+ if (!s_svp_cull_on || !s_svp_cull_world || !V)
+ return false;
+ // dynamic vis spheres are rest-pose baked and drop animated parts, only world statics cull,
+ // the sorted lists tag world statics with &Fidentity so accept that alongside a null matrix
+ if (M && M != &Fidentity)
+ return false;
+ // world static: the geometry (and so vis.sphere) is already in world space
+ Fvector wc; wc.set(V->vis.sphere.P);
+ float wr = V->vis.sphere.R;
+ const bool reject = !s_svp_cull_frustum.testSphere_dirty(wc, wr);
+ // count the identity-matrix rejects distinctly, the ledger proof the sorted cull now fires
+ if (reject && M == &Fidentity && ps_r__svp_stats)
+ ++svp_stats_cull_reject_ident;
+ if (reject)
+ {
+ if (!svp_ledger_cull_reject) svp_ledger_cull_reject = 1; // any world-static cone reject
+ if (M == &Fidentity && !svp_ledger_cull_reject_ident) svp_ledger_cull_reject_ident = 1;
+ }
+ return reject;
+}
diff --git a/src/Layers/xrRender/svp_hud_latch.cpp b/src/Layers/xrRender/svp_hud_latch.cpp
new file mode 100644
index 0000000000..868679aa26
--- /dev/null
+++ b/src/Layers/xrRender/svp_hud_latch.cpp
@@ -0,0 +1,280 @@
+#include "stdafx.h"
+
+#include "../../xrEngine/render.h"
+#include "../../xrEngine/igame_persistent.h"
+#include "../../xrEngine/environment.h"
+#include "../../xrEngine/CustomHUD.h"
+
+#include "FBasicVisual.h"
+#include "CHudInitializer.h"
+
+#include "fhierrarhyvisual.h"
+#include "SkeletonCustom.h"
+#include "SkeletonX.h"
+#include "../../xrEngine/fmesh.h"
+#include "flod.h"
+
+#include "../../xrEngine/xr_object.h"
+
+using namespace R_dsgraph;
+
+// pip object-space render (skinning) matrix of the lens bone, the SVP eyepiece uses it so a skinned
+// scope lens follows its bone through ADS and sway instead of the kinematics root
+bool CSkeletonX::SVP_LensBoneXform(Fmatrix& out)
+{
+ if (!Parent)
+ return false;
+ u16 bone;
+ if (RenderMode == RM_SINGLE)
+ bone = (u16)RMS_boneid;
+ else if (BonesUsed.size())
+ bone = BonesUsed[0];
+ else
+ return false;
+ out = Parent->LL_GetBoneInstance(bone).mRenderTransform;
+ return true;
+}
+
+// pip lens bone visibility, hidden markswitch lens meshes must not win the eyepiece pick
+bool CSkeletonX::SVP_LensBoneVisible()
+{
+ if (!Parent)
+ return true;
+ u16 bone;
+ if (RenderMode == RM_SINGLE)
+ bone = (u16)RMS_boneid;
+ else if (BonesUsed.size())
+ bone = BonesUsed[0];
+ else
+ return true;
+ return !!Parent->LL_GetBoneVisible(bone);
+}
+
+// pip forward the owning kinematics measured lens fit, lets the render side query detection
+// from a lens leaf visual, false when unparented
+bool CSkeletonX::SVP_GetLensDetection(SLensDetection& out)
+{
+ if (!Parent)
+ return false;
+ return Parent->GetLensDetection(out);
+}
+
+// pip snapshot the distinct hud matrices as the housing draws, logic keeps rewriting them mid
+// render and the scope draws later in the frame must not pick up a newer pose
+void CDSGraphManager::svp_latch_hud_poses()
+{
+ m_svp_pose.count = 0;
+ m_svp_pose.frame = Device.dwFrame;
+ auto grab = [this](Fmatrix* p)
+ {
+ if (!p || m_svp_pose.count >= 8)
+ return;
+ for (u32 i = 0; i < m_svp_pose.count; ++i)
+ if (m_svp_pose.src[i] == p)
+ return;
+ m_svp_pose.src[m_svp_pose.count] = p;
+ m_svp_pose.val[m_svp_pose.count] = *p;
+ ++m_svp_pose.count;
+ };
+ for (auto& n : RGraph.mapHUD)
+ grab(n.pMatrix);
+#if defined(USE_DX11)
+ for (auto& n : RGraph.mapScopeHUDSorted)
+ grab(n.pMatrix);
+ for (auto& n : RGraph.mapScopeHUDObjective)
+ grab(n.pMatrix);
+ for (auto& n : RGraph.mapReflexHUDSorted)
+ grab(n.pMatrix);
+
+ // lens bones move on the logic thread too, sample them once beside the poses
+ m_svp_bone.count = 0;
+ m_svp_bone.frame = Device.dwFrame;
+ auto grab_bone = [this](dxRender_Visual* v)
+ {
+ if (!v || m_svp_bone.count >= 4)
+ return;
+ for (u32 i = 0; i < m_svp_bone.count; ++i)
+ if (m_svp_bone.vis[i] == v)
+ return;
+ CSkeletonX* sk = fast_dynamic_cast(v);
+ Fmatrix b;
+ if (sk && sk->SVP_LensBoneXform(b))
+ {
+ m_svp_bone.vis[m_svp_bone.count] = v;
+ m_svp_bone.bone[m_svp_bone.count] = b;
+ ++m_svp_bone.count;
+ }
+ };
+ for (auto& n : RGraph.mapScopeHUDSorted)
+ grab_bone(n.pVisual);
+ for (auto& n : RGraph.mapScopeHUDObjective)
+ grab_bone(n.pVisual);
+ for (auto& n : RGraph.mapReflexHUDSorted)
+ grab_bone(n.pVisual);
+#endif
+}
+
+// the latched pose while this frame's latch is live, else the caller's pointer unchanged
+Fmatrix* CDSGraphManager::svp_pose_of(Fmatrix* p)
+{
+ if (m_svp_pose.frame == Device.dwFrame)
+ for (u32 i = 0; i < m_svp_pose.count; ++i)
+ if (m_svp_pose.src[i] == p)
+ return &m_svp_pose.val[i];
+ return p;
+}
+
+// the latched lens bone while this frame's latch is live, false falls back to the live read
+bool CDSGraphManager::svp_lens_bone_of(dxRender_Visual* v, Fmatrix& out)
+{
+ if (m_svp_bone.frame == Device.dwFrame)
+ for (u32 i = 0; i < m_svp_bone.count; ++i)
+ if (m_svp_bone.vis[i] == v)
+ {
+ out = m_svp_bone.bone[i];
+ return true;
+ }
+ return false;
+}
+
+// pip drain only the weapon list into the scope image, the caller owns the view/projection
+int g_svp_hud_skip_scope = 0; // hud_full mode: 1 drops the scope body, 2 also drops pieces fully behind the front lens
+float g_svp_hud_front_m = 0.f; // scope-body front extent along the axis from the eyepiece (m), clip-ons push it past the objective
+void CDSGraphManager::r_dsgraph_render_hud_svp()
+{
+ PROF_EVENT("r_dsgraph_render_hud_svp");
+ // mode 1 keeps the heuristic body skip, the mode 2 pupil camera near plane replaces it
+ {
+ extern int ps_r__svp_hud_full;
+ extern int scope_svp_enabled;
+ g_svp_hud_skip_scope = (scope_svp_enabled == 1) ? ps_r__svp_hud_full : 0;
+ }
+ // same camera and projection as the world, real depth so the scene occludes the weapon
+ // sliver and vice versa (rmNear depth-fronting belonged to the retired separate camera)
+ RImplementation.rmNormal();
+ auto& graph = RGraph.mapHUD;
+ if (!graph.empty())
+ {
+ std::sort(graph.begin(), graph.end());
+ // the scope body hugs the eyepiece-objective axis, everything else on the weapon (barrel,
+ // sights, hands) sits off-axis. the size cap keeps a merged whole-gun mesh from matching
+ const auto& vp = Device.m_SecondViewport;
+ const Fvector A = vp.eyepiece.m_W.c;
+ Fvector ax; ax.sub(vp.objective.m_W.c, A);
+ const float len2 = ax.square_magnitude();
+ const float tube = _sqrt(len2);
+ const float rcyl = std::max(vp.eyepiece.radius, vp.objective.radius) * 1.75f + 0.01f;
+ const bool measure_ok = len2 > EPS && vp.eyepiece.radius > EPS;
+ const bool skip_ok = g_svp_hud_skip_scope && measure_ok;
+ float front = 0.f;
+ float clipon_front = 0.f;
+ // the clip-on window widens to the authored front plane when the per-scope data reaches
+ // past the fixed bound, widening only so no rig loses a classification it had
+ float clip_hi = 2.6f;
+ {
+ extern int ps_r__svp_drain_anchor;
+ const float auth_z = vp.svp_opt_offset.z; // bus resolved, authored_optics folded in
+ if (ps_r__svp_drain_anchor && auth_z > EPS && tube > EPS)
+ {
+ const float t_front = auth_z * vp.eyepiece.radius / tube;
+ if (t_front > clip_hi)
+ clip_hi = t_front;
+ }
+ }
+ extern int ps_r__svp_cop_diag;
+ static u32 s_hud_diag_ms = 0;
+ const bool diag = ps_r__svp_cop_diag && (Device.dwTimeGlobal - s_hud_diag_ms > 3000);
+ if (diag)
+ {
+ s_hud_diag_ms = Device.dwTimeGlobal;
+ PipMsg("[SVP-HUD] tube=%.1fcm rcyl=%.1fcm cap=%.1fcm items=%u skip_ok=%d front=%.1fcm",
+ tube * 100.f, rcyl * 100.f, tube * 1.75f * 100.f, (u32)graph.size(), (int)skip_ok,
+ g_svp_hud_front_m * 100.f);
+ }
+ extern int ps_r__svp_stats; extern u32 svp_stats_hud_cull_reject; extern u32 svp_ledger_hud_cull_reject;
+ for (auto& item : graph)
+ {
+ if (svp_cull_reject(item.pVisual, svp_pose_of(item.pMatrix))) { if (ps_r__svp_stats) ++svp_stats_hud_cull_reject; svp_ledger_hud_cull_reject = 1; continue; } // pip skip off-cone SVP geometry
+ dxRender_Visual* V = item.pVisual;
+ VERIFY(V && V->shader._get());
+ bool drop = false;
+ bool clip_obj = false;
+ float t = 0.f, rad = -1.f;
+ if (measure_ok && item.pMatrix)
+ {
+ // skinned parts (addon scopes) sit at their bone, the rest-pose sphere lies elsewhere
+ Fmatrix W = *svp_pose_of(item.pMatrix);
+ CSkeletonX* sk = fast_dynamic_cast(V);
+ if (sk)
+ {
+ Fmatrix boneR;
+ if (svp_lens_bone_of(V, boneR) || sk->SVP_LensBoneXform(boneR))
+ W.mulB_43(boneR);
+ }
+ Fvector c; W.transform_tiny(c, V->vis.sphere.P);
+ Fvector ac; ac.sub(c, A);
+ t = ac.dotproduct(ax) / len2;
+ // radial distance to the axis LINE, the ocular stack (eyecup, rear housing) sits
+ // on-axis behind the eyepiece and must match too or it shows as rings in the image
+ Fvector p; p.set(A); p.mad(ax, t);
+ rad = p.distance_to(c);
+ // mounts wrap the tube with the center pulled off-axis, the size cap keeps a
+ // barrel sphere from impersonating a clamp
+ const float R = V->vis.sphere.R;
+ const bool wrap = (rad < R * 0.6f) && (R < tube * 0.3f);
+ // clip-on optics sit coaxial PAST the objective, the tight radial keeps the
+ // under-slung barrel out of this branch
+ const bool clipon = (t >= 1.4f && t < clip_hi) && (rad < rcyl * 0.8f);
+ drop = (R < tube * 1.75f) &&
+ (clipon || ((t > -0.6f && t < 1.4f) && (rad < rcyl || wrap)));
+ // only coaxial optic bodies define the front plane, a wrap mount's sphere is
+ // length-dominated and says nothing about the front lens
+ if (drop && (rad < rcyl || clipon))
+ front = _max(front, t * tube + R);
+ // a real clip-on front is a flat round disc, its mesh box has two long axes and a thin
+ // one, a front-sight post is a spike and must not push the plane past the objective
+ Fvector bs; V->vis.box.getsize(bs);
+ const float bmax = std::max(bs.x, std::max(bs.y, bs.z));
+ const float bmin = std::min(bs.x, std::min(bs.y, bs.z));
+ const float bmid = bs.x + bs.y + bs.z - bmax - bmin;
+ const bool disc_like = (bmax > EPS) && (bmid > 0.5f * bmax) && (bmin < 0.5f * bmax);
+ if (clipon && disc_like)
+ clipon_front = _max(clipon_front, t * tube + R);
+ // mode 2 drops pieces wholly behind the front plane, spanning pieces render whole
+ const float plane = (g_svp_hud_front_m > EPS) ? g_svp_hud_front_m : tube;
+ if (!drop && g_svp_hud_skip_scope >= 2 && (t * tube + R) < plane)
+ drop = true;
+ // the objective clip drops the barrel wholly behind the authored front lens, w2
+ // near-blur dissolves the surviving forward sliver edge
+ extern int ps_r__svp_drain_clip;
+ if (ps_r__svp_drain_clip && vp.svp_opt_offset.z > EPS && vp.eyepiece.radius > EPS
+ && (t * tube + R) < vp.svp_opt_offset.z * vp.eyepiece.radius)
+ clip_obj = true;
+ }
+ if (diag)
+ {
+ auto tx = V->GetTexture();
+ PipMsg("[SVP-HUD] %s t=%.2f rad=%.1fcm R=%.1fcm %s", (drop || clip_obj) ? "SKIP" : "keep",
+ t, rad * 100.f, V->vis.sphere.R * 100.f, tx ? tx->cName.c_str() : "?");
+ }
+ if ((drop && skip_ok) || clip_obj) continue;
+ RCache.set_Element(item.pSE);
+ RCache.set_xform_world(*svp_pose_of(item.pMatrix));
+ RImplementation.apply_object(item.pObject);
+ RImplementation.apply_lmaterial();
+ V->Render(svp_drain_lod(item.ssa, V->vis.sphere.R));
+ }
+ // consumed by next frame's near plane, the authored (or detected) objective plane wins,
+ // a confirmed clip-on front sitting beyond it keeps the plane ahead of the ring
+ const float auth_z = vp.svp_opt_offset.z; // bus resolved, authored_optics folded in
+ g_svp_hud_front_m = _max(front, clipon_front);
+ if (auth_z > EPS && vp.eyepiece.radius > EPS)
+ {
+ const float authored = auth_z * vp.eyepiece.radius;
+ g_svp_hud_front_m = _max(authored, clipon_front);
+ }
+ graph.clear();
+ }
+ RCache.set_xform_world(Fidentity);
+ RImplementation.rmNormal();
+}
diff --git a/src/Layers/xrRender/svp_lens_detect.cpp b/src/Layers/xrRender/svp_lens_detect.cpp
new file mode 100644
index 0000000000..573ac7439f
--- /dev/null
+++ b/src/Layers/xrRender/svp_lens_detect.cpp
@@ -0,0 +1,251 @@
+#include "stdafx.h"
+
+#include "SkeletonCustom.h"
+#include "SkeletonX.h"
+#include "fhierrarhyvisual.h"
+#include "../../xrEngine/bone.h"
+#include "svp_lens_detect.h"
+
+// measured optical lens discs from the weapon hud mesh, ported from the 3DB viewer detection
+// runs once per unique visual and caches, inert until IKinematics::GetLensDetection is queried
+
+// pure pipeline stage, compiles in every renderer, only ever reached from the DX11 query below
+bool svp_lens_fit(const std::vector& candidates, const svp_v3& axis_seed, int source,
+ const std::vector& all_verts, SLensDetection& out)
+{
+ out.ok = false;
+ out.has_objective = false;
+ out.source = source;
+ out.mm = 0.f;
+ out.offset.set(0.f, 0.f, 0.f, 0.f);
+ out.eye_center.set(0.f, 0.f, 0.f);
+ out.eye_normal.set(0.f, 0.f, 1.f);
+ out.eye_radius = 0.f;
+ out.obj_center.set(0.f, 0.f, 0.f);
+ out.obj_radius = 0.f;
+
+ SLensDetectResult r = svp_detect_lens_discs(candidates, axis_seed, source);
+ if (!r.ok || !r.eyepiece.valid)
+ return false;
+
+ out.eye_center.set(r.eyepiece.center.x, r.eyepiece.center.y, r.eyepiece.center.z);
+ out.eye_normal.set(r.eyepiece.normal.x, r.eyepiece.normal.y, r.eyepiece.normal.z);
+ out.eye_radius = r.eyepiece.radius;
+ out.ok = true;
+
+ SDiscFit obj = r.objective;
+ int objsrc = source;
+ if (!obj.valid)
+ {
+ // stage E recovers a measured objective from the housing tube mouth
+ svp_v3 ec = { out.eye_center.x, out.eye_center.y, out.eye_center.z };
+ svp_v3 en = { out.eye_normal.x, out.eye_normal.y, out.eye_normal.z };
+ SDiscFit m;
+ if (svp_tube_march_objective(all_verts, ec, en, out.eye_radius, m))
+ {
+ obj = m;
+ objsrc = 2;
+ }
+ }
+
+ if (obj.valid)
+ {
+ out.has_objective = true;
+ out.obj_center.set(obj.center.x, obj.center.y, obj.center.z);
+ out.obj_radius = obj.radius;
+ svp_v3 axis = { out.eye_normal.x, out.eye_normal.y, out.eye_normal.z };
+ svp_v3 ec = { out.eye_center.x, out.eye_center.y, out.eye_center.z };
+ svp_v4 off = svp_lens_offset_from_centers(axis, ec, out.eye_radius, obj.center, obj.radius);
+ out.offset.set(off.x, off.y, off.z, off.w);
+ out.mm = svp_mm_suggestion(obj.radius);
+ out.source = objsrc;
+ }
+ return out.ok;
+}
+
+#if defined(USE_DX10) || defined(USE_DX11)
+
+// lens naming the modeler committed, case-insensitive substring of lens or glass
+static bool svp_name_is_lens(LPCSTR n)
+{
+ if (!n)
+ return false;
+ string256 low;
+ xr_strcpy(low, n);
+ xr_strlwr(low);
+ return (strstr(low, "lens") != nullptr) || (strstr(low, "glass") != nullptr);
+}
+
+static inline svp_v3 svp_from(const Fvector& v) { return { v.x, v.y, v.z }; }
+
+// append each dedup vertex as its bind-pose model position with the global dominant bone id
+// the stored bone id is a global skeleton id, no palette remap (skinning indexes LL_GetTransform_R by it)
+void CSkeletonX::SVP_GatherVerts(xr_vector& positions, xr_vector& bones)
+{
+ if (*Vertices1W)
+ {
+ const vertBoned1W* v = *Vertices1W;
+ for (u32 i = 0, n = Vertices1W.size(); i < n; ++i)
+ {
+ positions.push_back(v[i].P);
+ bones.push_back((u16)v[i].matrix);
+ }
+ }
+ else if (*Vertices2W)
+ {
+ const vertBoned2W* v = *Vertices2W;
+ for (u32 i = 0, n = Vertices2W.size(); i < n; ++i)
+ {
+ positions.push_back(v[i].P);
+ bones.push_back((v[i].w > 0.5f) ? v[i].matrix1 : v[i].matrix0);
+ }
+ }
+ else if (*Vertices3W)
+ {
+ const vertBoned3W* v = *Vertices3W;
+ for (u32 i = 0, n = Vertices3W.size(); i < n; ++i)
+ {
+ positions.push_back(v[i].P);
+ const float w2 = 1.f - v[i].w[0] - v[i].w[1];
+ u16 b = v[i].m[0];
+ float best = v[i].w[0];
+ if (v[i].w[1] > best) { best = v[i].w[1]; b = v[i].m[1]; }
+ if (w2 > best) { b = v[i].m[2]; }
+ bones.push_back(b);
+ }
+ }
+ else if (*Vertices4W)
+ {
+ const vertBoned4W* v = *Vertices4W;
+ for (u32 i = 0, n = Vertices4W.size(); i < n; ++i)
+ {
+ positions.push_back(v[i].P);
+ const float w3 = 1.f - v[i].w[0] - v[i].w[1] - v[i].w[2];
+ u16 b = v[i].m[0];
+ float best = v[i].w[0];
+ if (v[i].w[1] > best) { best = v[i].w[1]; b = v[i].m[1]; }
+ if (v[i].w[2] > best) { best = v[i].w[2]; b = v[i].m[2]; }
+ if (w3 > best) { b = v[i].m[3]; }
+ bones.push_back(b);
+ }
+ }
+}
+
+#endif // USE_DX10 || USE_DX11
+
+bool CKinematics::GetLensDetection(SLensDetection& out)
+{
+ out.ok = false;
+ out.has_objective = false;
+ out.source = 0;
+ out.mm = 0.f;
+ out.offset.set(0.f, 0.f, 0.f, 0.f);
+ out.eye_center.set(0.f, 0.f, 0.f);
+ out.eye_normal.set(0.f, 0.f, 1.f);
+ out.eye_radius = 0.f;
+ out.obj_center.set(0.f, 0.f, 0.f);
+ out.obj_radius = 0.f;
+
+#if defined(USE_DX10) || defined(USE_DX11)
+ // process-lifetime cache keyed by visual path, negatives cached too, deterministic per mesh
+ static xr_map s_cache;
+ static xrCriticalSection s_cache_cs;
+ shared_str key = getDebugName();
+ {
+ xrCriticalSectionGuard g(&s_cache_cs);
+ xr_map::iterator it = s_cache.find(key);
+ if (it != s_cache.end())
+ {
+ out = it->second;
+ return out.ok;
+ }
+ }
+
+ SLensDetection res = out;
+
+ // bind-pose bone transforms, model space and pose independent
+ xr_vector binds;
+ LL_GetBindTransform(binds);
+ const u16 bc = LL_BoneCount();
+
+ // lens bones by name, the first match seeds the split axis and the near-bone fallback
+ xr_vector lens_ids;
+ int seed = -1;
+ for (u16 b = 0; b < bc; ++b)
+ {
+ if (svp_name_is_lens(LL_GetData(b).name.c_str()))
+ {
+ lens_ids.push_back(b);
+ if (seed < 0)
+ seed = b;
+ }
+ }
+
+ // every child mesh vertex with its global dominant bone, visible and hidden alike
+ xr_vector pos;
+ xr_vector bone;
+ for (u32 i = 0; i < children.size(); ++i)
+ {
+ CSkeletonX* C = LL_GetChild(i);
+ if (C)
+ C->SVP_GatherVerts(pos, bone);
+ }
+ for (u32 i = 0; i < children_invisible.size(); ++i)
+ {
+ CSkeletonX* C = fast_dynamic_cast(children_invisible[i]);
+ if (C)
+ C->SVP_GatherVerts(pos, bone);
+ }
+
+ if (seed >= 0 && !pos.empty())
+ {
+ // stage A, primary dominant-bone candidates then the near-bone fallback
+ std::vector cand;
+ for (u32 i = 0; i < pos.size(); ++i)
+ {
+ for (u32 k = 0; k < lens_ids.size(); ++k)
+ {
+ if (bone[i] == lens_ids[k])
+ {
+ cand.push_back(svp_from(pos[i]));
+ break;
+ }
+ }
+ }
+
+ int source = 0;
+ if ((int)cand.size() < FIT_MIN_POINTS)
+ {
+ cand.clear();
+ source = 1;
+ const Fvector sp = binds[seed].c;
+ for (u32 i = 0; i < pos.size(); ++i)
+ {
+ Fvector d;
+ d.sub(pos[i], sp);
+ if (d.magnitude() <= NEAR_BONE_RADIUS)
+ cand.push_back(svp_from(pos[i]));
+ }
+ }
+
+ if ((int)cand.size() >= FIT_MIN_POINTS)
+ {
+ svp_v3 axis_seed = svp_axis_seed_for(svp_from(binds[seed].k));
+ std::vector allv;
+ allv.reserve(pos.size());
+ for (u32 i = 0; i < pos.size(); ++i)
+ allv.push_back(svp_from(pos[i]));
+ svp_lens_fit(cand, axis_seed, source, allv, res);
+ }
+ }
+
+ {
+ xrCriticalSectionGuard g(&s_cache_cs);
+ s_cache[key] = res;
+ }
+ out = res;
+ return res.ok;
+#else
+ return false;
+#endif
+}
diff --git a/src/Layers/xrRender/svp_lens_detect.h b/src/Layers/xrRender/svp_lens_detect.h
new file mode 100644
index 0000000000..7174e13041
--- /dev/null
+++ b/src/Layers/xrRender/svp_lens_detect.h
@@ -0,0 +1,8 @@
+#pragma once
+#include "svp_lens_detect_math.h"
+#include "../../Include/xrRender/Kinematics.h"
+
+// engine wrapper over the pure detection math, candidate selection is done engine-side
+// candidates already selected, all_verts the whole visual for the stage-E tube march
+bool svp_lens_fit(const std::vector& candidates, const svp_v3& axis_seed, int source,
+ const std::vector& all_verts, SLensDetection& out);
diff --git a/src/Layers/xrRender/svp_lens_detect_math.cpp b/src/Layers/xrRender/svp_lens_detect_math.cpp
new file mode 100644
index 0000000000..28b8091bba
--- /dev/null
+++ b/src/Layers/xrRender/svp_lens_detect_math.cpp
@@ -0,0 +1,359 @@
+#include
+#include
+#include
+#include "svp_lens_detect_math.h"
+
+// stage-E tube-march tuning, design-intent per the doc A3.2 spec
+static const float TUBE_SLAB_M = 0.005f; // 5 mm march step
+static const float TUBE_RADIUS_SCALE = 3.0f; // cylinder radius = this x eye radius
+static const int TUBE_EMPTY_RUN = 3; // consecutive empty slabs terminate the march
+static const int TUBE_SLAB_MIN_PTS = 3; // a slab counts as dense at this many verts
+static const float TUBE_MIN_LENGTH = 0.05f; // reject tubes shorter than this
+static const float TUBE_MAX_LENGTH = 0.45f; // reject tubes longer than this, survey longest optic ~0.39m
+static const float TUBE_RING_INNER_PCT = 0.15f; // low-percentile mouth radial = clear aperture
+static const float TUBE_CONCENTRIC_TOL = 0.5f; // mouth centroid lateral offset / eye radius
+static const float TUBE_MAX_MID_GAP_M = 0.05f; // reject a long gap before the mouth
+
+// local vector helpers, model.js:15-31
+static inline svp_v3 vsub(const svp_v3& a, const svp_v3& b) { return { a.x - b.x, a.y - b.y, a.z - b.z }; }
+static inline svp_v3 vscale(const svp_v3& a, float s) { return { a.x * s, a.y * s, a.z * s }; }
+static inline float vdot(const svp_v3& a, const svp_v3& b) { return a.x * b.x + a.y * b.y + a.z * b.z; }
+static inline svp_v3 vcross(const svp_v3& a, const svp_v3& b)
+{
+ return { a.y * b.z - a.z * b.y, a.z * b.x - a.x * b.z, a.x * b.y - a.y * b.x };
+}
+static inline float vlen(const svp_v3& a) { return sqrtf(vdot(a, a)); }
+static inline svp_v3 vnorm(const svp_v3& a)
+{
+ float L = vlen(a);
+ if (L < 1e-12f) return { 0.f, 0.f, 0.f };
+ return { a.x / L, a.y / L, a.z / L };
+}
+
+// symmetric 3x3 Jacobi eigen, optics-math.js:131-162, double for the convergence gates
+struct SEig3 { double values[3]; double vectors[3][3]; };
+static SEig3 jacobi_eigen3(const double m[3][3])
+{
+ double a[3][3] = { { m[0][0], m[0][1], m[0][2] }, { m[1][0], m[1][1], m[1][2] }, { m[2][0], m[2][1], m[2][2] } };
+ double v[3][3] = { { 1, 0, 0 }, { 0, 1, 0 }, { 0, 0, 1 } };
+ static const int pqs[3][2] = { { 0, 1 }, { 0, 2 }, { 1, 2 } };
+ for (int sweep = 0; sweep < 32; ++sweep)
+ {
+ if (fabs(a[0][1]) + fabs(a[0][2]) + fabs(a[1][2]) < 1e-18) break;
+ for (int e = 0; e < 3; ++e)
+ {
+ int p = pqs[e][0], q = pqs[e][1];
+ if (fabs(a[p][q]) < 1e-20) continue;
+ double theta = (a[q][q] - a[p][p]) / (2.0 * a[p][q]);
+ double t = (theta >= 0 ? 1.0 : -1.0) / (fabs(theta) + sqrt(theta * theta + 1.0));
+ double c = 1.0 / sqrt(t * t + 1.0), s = t * c;
+ for (int k = 0; k < 3; ++k) { double akp = a[k][p], akq = a[k][q]; a[k][p] = c * akp - s * akq; a[k][q] = s * akp + c * akq; }
+ for (int k = 0; k < 3; ++k) { double apk = a[p][k], aqk = a[q][k]; a[p][k] = c * apk - s * aqk; a[q][k] = s * apk + c * aqk; }
+ for (int k = 0; k < 3; ++k) { double vkp = v[k][p], vkq = v[k][q]; v[k][p] = c * vkp - s * vkq; v[k][q] = s * vkp + c * vkq; }
+ }
+ }
+ SEig3 out;
+ out.values[0] = a[0][0]; out.values[1] = a[1][1]; out.values[2] = a[2][2];
+ for (int k = 0; k < 3; ++k) { out.vectors[k][0] = v[0][k]; out.vectors[k][1] = v[1][k]; out.vectors[k][2] = v[2][k]; }
+ return out;
+}
+
+// PCA disc fit, optics-math.js:166-196
+SDiscFit svp_fit_disc(const svp_v3* pts, int n)
+{
+ SDiscFit r;
+ r.center = { 0, 0, 0 }; r.normal = { 0, 0, 1 }; r.radius = 0.f; r.rms = 0.f; r.valid = false;
+ if (!pts || n < FIT_MIN_POINTS) return r;
+
+ double cx = 0, cy = 0, cz = 0;
+ for (int i = 0; i < n; ++i) { cx += pts[i].x; cy += pts[i].y; cz += pts[i].z; }
+ cx /= n; cy /= n; cz /= n;
+
+ double xx = 0, xy = 0, xz = 0, yy = 0, yz = 0, zz = 0;
+ for (int i = 0; i < n; ++i)
+ {
+ double dx = pts[i].x - cx, dy = pts[i].y - cy, dz = pts[i].z - cz;
+ xx += dx * dx; xy += dx * dy; xz += dx * dz; yy += dy * dy; yz += dy * dz; zz += dz * dz;
+ }
+ double m[3][3] = { { xx, xy, xz }, { xy, yy, yz }, { xz, yz, zz } };
+ SEig3 eig = jacobi_eigen3(m);
+
+ // normal is the smallest-eigenvalue direction, signed toward +Z
+ int mi = 0;
+ if (eig.values[1] < eig.values[mi]) mi = 1;
+ if (eig.values[2] < eig.values[mi]) mi = 2;
+ double nx = eig.vectors[mi][0], ny = eig.vectors[mi][1], nz = eig.vectors[mi][2];
+ double nl = sqrt(nx * nx + ny * ny + nz * nz);
+ if (nl < 1e-12) { nx = 0; ny = 0; nz = 1; }
+ else { nx /= nl; ny /= nl; nz /= nl; }
+ if (nz < 0) { nx = -nx; ny = -ny; nz = -nz; }
+
+ // radial spread off the normal and the out-of-plane residual
+ std::vector radial; radial.reserve(n);
+ double outSq = 0;
+ for (int i = 0; i < n; ++i)
+ {
+ double dx = pts[i].x - cx, dy = pts[i].y - cy, dz = pts[i].z - cz;
+ double out = dx * nx + dy * ny + dz * nz;
+ outSq += out * out;
+ double rx = dx - nx * out, ry = dy - ny * out, rz = dz - nz * out;
+ radial.push_back(sqrt(rx * rx + ry * ry + rz * rz));
+ }
+ std::sort(radial.begin(), radial.end());
+ int qi = (int)floor(n * 0.9);
+ if (qi > n - 1) qi = n - 1;
+ double radius = radial[qi];
+ double rms = sqrt(outSq / n);
+ if (radius > FIT_MAX_RADIUS || rms > FIT_MAX_RMS_RATIO * radius) return r;
+
+ r.center = { (float)cx, (float)cy, (float)cz };
+ r.normal = { (float)nx, (float)ny, (float)nz };
+ r.radius = (float)radius;
+ r.rms = (float)rms;
+ r.valid = true;
+ return r;
+}
+
+// 1D clustering along an axis, optics-math.js:200-217, clusters come back rear -> front
+void svp_split_along_axis(const svp_v3* pts, int n, const svp_v3& axis,
+ std::vector>& clusters)
+{
+ clusters.clear();
+ if (!pts || n <= 0) return;
+ svp_v3 dir = vnorm(axis);
+
+ std::vector> proj; proj.reserve(n);
+ for (int i = 0; i < n; ++i) proj.push_back({ vdot(pts[i], dir), i });
+ std::sort(proj.begin(), proj.end(), [](const std::pair& a, const std::pair& b) { return a.first < b.first; });
+
+ std::vector gaps; gaps.reserve(n > 1 ? n - 1 : 0);
+ for (int i = 1; i < n; ++i) gaps.push_back(proj[i].first - proj[i - 1].first);
+ std::vector sorted = gaps;
+ std::sort(sorted.begin(), sorted.end());
+ float median = sorted.empty() ? 0.f : sorted[sorted.size() / 2];
+ float threshold = std::max(SPLIT_MIN_GAP, 2.f * median);
+
+ std::vector cur;
+ cur.push_back(pts[proj[0].second]);
+ for (int i = 1; i < n; ++i)
+ {
+ if (proj[i].first - proj[i - 1].first > threshold) { clusters.push_back(cur); cur.clear(); }
+ cur.push_back(pts[proj[i].second]);
+ }
+ clusters.push_back(cur);
+}
+
+// bone axis is a usable split seed only when it roughly agrees with +Z, optics-math.js:220-226
+svp_v3 svp_axis_seed_for(const svp_v3& model_axis_k)
+{
+ svp_v3 k = vnorm(model_axis_k);
+ float d = k.z;
+ if (d >= AXIS_SEED_MAX_COS) return k;
+ if (-d >= AXIS_SEED_MAX_COS) return vscale(k, -1.f);
+ return { 0.f, 0.f, 1.f };
+}
+
+// split -> fit -> filter -> assign, optics-math.js:230-263 assignment half
+SLensDetectResult svp_detect_lens_discs(const std::vector& candidates,
+ const svp_v3& axis_seed, int source)
+{
+ SLensDetectResult res;
+ res.eyepiece.valid = false; res.objective.valid = false;
+ res.source = source; res.vert_count = (int)candidates.size(); res.ok = false;
+ if (candidates.size() < (size_t)FIT_MIN_POINTS) return res;
+
+ std::vector> clusters;
+ svp_split_along_axis(candidates.data(), (int)candidates.size(), axis_seed, clusters);
+
+ std::vector discs;
+ for (size_t i = 0; i < clusters.size(); ++i)
+ {
+ SDiscFit d = svp_fit_disc(clusters[i].data(), (int)clusters[i].size());
+ if (d.valid) discs.push_back(d);
+ }
+ if (discs.empty()) return res;
+
+ res.eyepiece = discs[0];
+ for (int i = (int)discs.size() - 1; i >= 1; --i)
+ {
+ svp_v3 delta = vsub(discs[i].center, res.eyepiece.center);
+ if (vdot(delta, res.eyepiece.normal) > OBJECTIVE_MIN_FORWARD) { res.objective = discs[i]; break; }
+ }
+ res.ok = true;
+ return res;
+}
+
+// orthonormal frame from the axis alone, model.js:41-48
+struct SLensFrame { svp_v3 i, j, k; };
+static SLensFrame bone_local_frame(const svp_v3& kAxis)
+{
+ svp_v3 k = vnorm(kAxis);
+ svp_v3 worldUp = { 0.f, 1.f, 0.f };
+ svp_v3 tmp = (fabsf(vdot(k, worldUp)) > 0.99f) ? svp_v3{ 1.f, 0.f, 0.f } : svp_v3{ 0.f, 1.f, 0.f };
+ svp_v3 i = vnorm(vcross(tmp, k));
+ svp_v3 j = vcross(k, i);
+ return { i, j, k };
+}
+
+// objective center in the eyepiece local frame divided by eye radius, optics-math.js:93-97
+svp_v4 svp_lens_offset_from_centers(const svp_v3& axis_dir, const svp_v3& eye_c,
+ float eye_r, const svp_v3& obj_c, float obj_r)
+{
+ SLensFrame f = bone_local_frame(axis_dir);
+ svp_v3 delta = vsub(obj_c, eye_c);
+ float r = (eye_r != 0.f) ? eye_r : 1e-9f;
+ svp_v4 o;
+ o.x = vdot(delta, f.i) / r;
+ o.y = vdot(delta, f.j) / r;
+ o.z = vdot(delta, f.k) / r;
+ o.w = obj_r / r;
+ return o;
+}
+
+// stage-E tube march, doc A3.2, recovers the objective from the housing when Stage D found only the ocular
+bool svp_tube_march_objective(const std::vector& all_verts,
+ const svp_v3& eye_center, const svp_v3& eye_normal,
+ float eye_radius, SDiscFit& obj_out)
+{
+ obj_out.valid = false;
+ if (all_verts.empty() || eye_radius <= 0.f) return false;
+ svp_v3 axis = vnorm(eye_normal);
+ float cyl_r = TUBE_RADIUS_SCALE * eye_radius;
+
+ // keep forward verts inside the cylinder with their axial and radial distances
+ struct SVertTR { float t; float radial; svp_v3 p; };
+ std::vector col;
+ float max_t = 0.f;
+ for (size_t vi = 0; vi < all_verts.size(); ++vi)
+ {
+ svp_v3 d = vsub(all_verts[vi], eye_center);
+ float t = vdot(d, axis);
+ if (t < 0.f) continue;
+ svp_v3 perp = vsub(d, vscale(axis, t));
+ float rad = vlen(perp);
+ if (rad > cyl_r) continue;
+ col.push_back({ t, rad, all_verts[vi] });
+ if (t > max_t) max_t = t;
+ }
+ if (col.empty()) return false;
+
+ int nslabs = (int)floor(max_t / TUBE_SLAB_M) + 1;
+ if (nslabs <= 0) return false;
+ std::vector slab_count(nslabs, 0);
+ for (size_t i = 0; i < col.size(); ++i)
+ {
+ int si = (int)floor(col[i].t / TUBE_SLAB_M);
+ if (si >= 0 && si < nslabs) slab_count[si]++;
+ }
+
+ // march forward to the last dense slab, terminate at K consecutive empties
+ int empty_run = 0, mouth_slab = -1, cur_gap = 0, max_mid_gap = 0;
+ bool hit_cap = false;
+ for (int si = 0; si < nslabs; ++si)
+ {
+ if (slab_count[si] >= TUBE_SLAB_MIN_PTS)
+ {
+ mouth_slab = si;
+ if (cur_gap > max_mid_gap) max_mid_gap = cur_gap;
+ cur_gap = 0; empty_run = 0;
+ }
+ else
+ {
+ empty_run++; cur_gap++;
+ if (empty_run >= TUBE_EMPTY_RUN) break;
+ }
+ if ((float)si * TUBE_SLAB_M > TUBE_MAX_LENGTH) { hit_cap = true; break; }
+ }
+ if (mouth_slab < 0) return false;
+ // a solid barrel stays dense out to the cap without the tube's open mouth, reject rather than latch the muzzle
+ if (hit_cap) return false;
+
+ float mouth_t = (float)mouth_slab * TUBE_SLAB_M;
+ if (mouth_t < TUBE_MIN_LENGTH || mouth_t > TUBE_MAX_LENGTH) return false;
+ if ((float)max_mid_gap * TUBE_SLAB_M > TUBE_MAX_MID_GAP_M) return false;
+
+ // gather the terminal-slab ring
+ std::vector mouth_pts;
+ std::vector mouth_rad;
+ svp_v3 ring_sum = { 0, 0, 0 };
+ int ring_n = 0;
+ for (size_t i = 0; i < col.size(); ++i)
+ {
+ if ((int)floor(col[i].t / TUBE_SLAB_M) == mouth_slab)
+ {
+ mouth_pts.push_back(col[i].p);
+ mouth_rad.push_back(col[i].radial);
+ ring_sum.x += col[i].p.x; ring_sum.y += col[i].p.y; ring_sum.z += col[i].p.z;
+ ring_n++;
+ }
+ }
+ if (ring_n <= 0) return false;
+
+ // concentricity gate on the mouth centroid
+ svp_v3 center = { ring_sum.x / ring_n, ring_sum.y / ring_n, ring_sum.z / ring_n };
+ svp_v3 cdelta = vsub(center, eye_center);
+ float ct = vdot(cdelta, axis);
+ svp_v3 cperp = vsub(cdelta, vscale(axis, ct));
+ if (vlen(cperp) > TUBE_CONCENTRIC_TOL * eye_radius) return false;
+
+ // route a, a clean disc fit on the mouth verts
+ if ((int)mouth_pts.size() >= FIT_MIN_POINTS)
+ {
+ SDiscFit d = svp_fit_disc(mouth_pts.data(), (int)mouth_pts.size());
+ if (d.valid) { obj_out = d; return true; }
+ }
+
+ // route b, inner-radius aperture of the mouth ring
+ std::sort(mouth_rad.begin(), mouth_rad.end());
+ int qi = (int)floor(mouth_rad.size() * TUBE_RING_INNER_PCT);
+ if (qi > (int)mouth_rad.size() - 1) qi = (int)mouth_rad.size() - 1;
+ if (qi < 0) qi = 0;
+ obj_out.center = center;
+ obj_out.normal = axis;
+ obj_out.radius = mouth_rad[qi];
+ obj_out.rms = 0.f;
+ obj_out.valid = true;
+ return true;
+}
+
+// sv98 fixture, doc 3.1 and Addendum 2, synthesizes a 379-point ocular disc and checks the fit
+bool svp_lens_detect_selftest()
+{
+ const svp_v3 center = { 0.000517f, 0.09351f, 0.016879f };
+ const float target_r = 0.014699f;
+ const float target_rms = 0.000528f;
+ const int N = 379;
+ const float golden = 2.399963f;
+
+ // pick the outer radius so the 90th-percentile radial lands on the measured ocular radius
+ float qf = (floorf(N * 0.9f) + 0.5f) / (float)N;
+ float Rmax = target_r / sqrtf(qf);
+ std::vector pts; pts.reserve(N);
+ for (int i = 0; i < N; ++i)
+ {
+ float ri = Rmax * sqrtf((i + 0.5f) / (float)N);
+ float th = i * golden;
+ float dz = (i & 1) ? -target_rms : target_rms;
+ pts.push_back({ center.x + ri * cosf(th), center.y + ri * sinf(th), center.z + dz });
+ }
+
+ svp_v3 seed = svp_axis_seed_for(svp_v3{ 0.f, 0.f, 1.f });
+ SLensDetectResult res = svp_detect_lens_discs(pts, seed, 0);
+ if (!res.ok || !res.eyepiece.valid || res.objective.valid) return false;
+
+ const SDiscFit& e = res.eyepiece;
+ if (fabsf(e.center.x - center.x) > 1e-3f) return false;
+ if (fabsf(e.center.y - center.y) > 1e-3f) return false;
+ if (fabsf(e.center.z - center.z) > 1e-3f) return false;
+ if (fabsf(e.radius - target_r) > 1e-3f) return false;
+ if (fabsf(e.rms - target_rms) > 1e-3f) return false;
+ if (e.normal.z < 0.99f) return false;
+
+ // export identities, mm from the objective radius and z from the forward distance
+ if (fabsf(svp_mm_suggestion(0.017675f) - 35.35f) > 0.05f) return false;
+ float eye_r = 0.014699f, obj_r = 0.017675f, fwd = 14.639f * eye_r;
+ svp_v4 off = svp_lens_offset_from_centers(svp_v3{ 0, 0, 1 }, svp_v3{ 0, 0, 0 }, eye_r, svp_v3{ 0, 0, fwd }, obj_r);
+ if (fabsf(off.z - 14.639f) > 0.01f) return false;
+ if (fabsf(off.w - 1.2025f) > 0.01f) return false;
+ return true;
+}
diff --git a/src/Layers/xrRender/svp_lens_detect_math.h b/src/Layers/xrRender/svp_lens_detect_math.h
new file mode 100644
index 0000000000..6bb9f07b6a
--- /dev/null
+++ b/src/Layers/xrRender/svp_lens_detect_math.h
@@ -0,0 +1,67 @@
+#pragma once
+#include
+
+// pure lens-disc detection ported from the 3DB viewer optics-math.js
+// engine-free split -> fit -> assign core plus the stage-E tube march
+
+// svp_v3/svp_v4 are layout-compatible with Fvector/Fvector4 so WO-B reinterprets its arrays
+struct svp_v3 { float x, y, z; };
+struct svp_v4 { float x, y, z, w; };
+
+// fitDisc output, valid == false means the cluster was rejected
+struct SDiscFit {
+ svp_v3 center;
+ svp_v3 normal; // signed toward +Z
+ float radius; // 90th-percentile in-plane radius in metres
+ float rms; // out-of-plane RMS residual in metres
+ bool valid;
+};
+
+// one entry per skeleton bone the detector needs, filled by WO-B
+struct SBoneInfo {
+ svp_v3 model_pos; // bind-pose model-space bone origin
+ svp_v3 model_axis_k; // bind-pose forward column normalized
+ bool is_lens; // name matched /lens|glass/i by the caller
+};
+
+struct SLensDetectResult {
+ SDiscFit eyepiece; // rear-most valid disc
+ SDiscFit objective; // front-most valid disc past the forward gate, valid == false if none
+ int source; // 0 lens_verts, 1 near_bone
+ int vert_count; // candidate count actually fit
+ bool ok; // false means no fittable lens, caller keeps authored/live
+};
+
+// detection constants, verbatim from optics-math.js:121-128
+static const int FIT_MIN_POINTS = 12;
+static const float FIT_MAX_RADIUS = 0.08f;
+static const float FIT_MAX_RMS_RATIO = 0.35f;
+static const float SPLIT_MIN_GAP = 0.015f;
+static const float NEAR_BONE_RADIUS = 0.05f; // used by WO-B for the fallback selection
+static const float AXIS_SEED_MAX_COS = 0.8660254f; // cos 30
+static const float OBJECTIVE_MIN_FORWARD = 0.03f;
+
+// core stages, free functions with no state
+SDiscFit svp_fit_disc(const svp_v3* pts, int n);
+void svp_split_along_axis(const svp_v3* pts, int n, const svp_v3& axis,
+ std::vector>& clusters_rear_to_front);
+svp_v3 svp_axis_seed_for(const svp_v3& model_axis_k);
+
+// full pipeline on the already-selected candidate cloud plus the lens seed axis
+// positions in bind-pose model space in metres
+SLensDetectResult svp_detect_lens_discs(const std::vector& candidates,
+ const svp_v3& axis_seed, int source);
+
+// stage-E tube march recovers a measured objective from housing verts when the
+// pipeline returned eyepiece-only, all_verts is the whole visual or scope subtree
+bool svp_tube_march_objective(const std::vector& all_verts,
+ const svp_v3& eye_center, const svp_v3& eye_normal,
+ float eye_radius, SDiscFit& obj_out);
+
+// export identities, offset in eyepiece-radius units and mm from the fitted radius
+svp_v4 svp_lens_offset_from_centers(const svp_v3& axis_dir, const svp_v3& eye_c,
+ float eye_r, const svp_v3& obj_c, float obj_r);
+inline float svp_mm_suggestion(float obj_radius_m) { return obj_radius_m * 2.f * 1000.f; }
+
+// asserts the sv98 fixture, returns true on pass, gated caller lives in WO-D
+bool svp_lens_detect_selftest();
diff --git a/src/Layers/xrRender/xrRender_console.cpp b/src/Layers/xrRender/xrRender_console.cpp
index 4702019d90..cd21f8bd0f 100644
--- a/src/Layers/xrRender/xrRender_console.cpp
+++ b/src/Layers/xrRender/xrRender_console.cpp
@@ -5,6 +5,7 @@
#include "dxRenderDeviceRender.h"
#include "../../build_config_defines.h"
+#include "Debug/renderdoc_app.h" // r__rdc_capture
u32 ps_Preset = 2;
xr_token qpreset_token [ ] = {
@@ -291,6 +292,12 @@ Fvector4 heat_vision_args_2 = { .0f, .0f, .0f, .0f };
//crookr
int scope_fake_enabled = 1;
int scope_3D_fake_enabled = 0; // Redotix99: for 3D Shader Based Scopes
+Fvector4 scope_objective_lens_offset = { .0f, .0f, .0f, .0f };
+// RenderDoc instrumentation, default off
+// r__gpu_markers: per-batch GPU events + resource naming, runtime-toggleable
+// r__shader_debug: HLSL debug build (D3DCOMPILE_DEBUG, no optimization), needs a shaders_cache clear + reload
+int r__gpu_markers = 0;
+int r__shader_debug = 0;
//string32 scope_fake_texture = "wpn\\wpn_crosshair_pso1";
float ps_r2_ss_sunshafts_length = 1.f;
@@ -388,6 +395,11 @@ Fvector4 ps_s3ds_param_1 = { 0, 0, 0, 0 };
Fvector4 ps_s3ds_param_2 = { 0, 0, 0, 0 };
Fvector4 ps_s3ds_param_3 = { 0, 0, 0, 0 };
Fvector4 ps_s3ds_param_4 = { 0, 0, 0, 0 };
+Fvector4 ps_shader_scope_params = { 0, 0, 0, 0 }; // scope magnification (curMag/minMag/maxMag/fov), set by the 3DSS Lua
+float g_pip_scope_magnification = 0.f; // engine fallback magnification when the Lua has not set the cvar
+float g_pip_scope_min_mag = 0.f; // fallback min magnification (least zoom, widest fov) from hud_fov_params
+float g_pip_scope_max_mag = 0.f; // fallback max magnification (most zoom, narrowest fov)
+float g_pip_scope_ratio = 1.f; // eyepiece-fit factor, effective on-screen magnification = ratio * scope mag
float hud_fov_aim_factor = 0;
@@ -707,6 +719,46 @@ class CCC_R2GM : public CCC_Float
}
};
+// RenderDoc in-application capture trigger, no-op when RenderDoc is not attached
+// renderdoc.dll is only present in the process when the game is launched under RenderDoc
+static void rdc_trigger_capture(u32 frames)
+{
+ HMODULE mod = GetModuleHandleA("renderdoc.dll");
+ if (!mod)
+ {
+ Msg("~ r__rdc_capture: RenderDoc not attached (renderdoc.dll not loaded)");
+ return;
+ }
+
+ pRENDERDOC_GetAPI getApi = (pRENDERDOC_GetAPI)GetProcAddress(mod, "RENDERDOC_GetAPI");
+ if (!getApi)
+ return;
+
+ RENDERDOC_API_1_1_2* api = nullptr;
+ if (getApi(eRENDERDOC_API_Version_1_1_2, (void**)&api) != 1 || !api)
+ return;
+
+ if (frames > 1)
+ api->TriggerMultiFrameCapture(frames);
+ else
+ api->TriggerCapture();
+
+ Msg("~ r__rdc_capture: queued capture of %u frame(s)", frames > 1 ? frames : 1);
+}
+
+class CCC_RdcCapture : public IConsole_Command
+{
+public:
+ CCC_RdcCapture(LPCSTR N) : IConsole_Command(N) { bEmptyArgsHandled = TRUE; };
+
+ virtual void Execute(LPCSTR args)
+ {
+ u32 frames = 0;
+ sscanf(args, "%u", &frames);
+ rdc_trigger_capture(frames);
+ }
+};
+
class CCC_Screenshot : public IConsole_Command
{
public:
@@ -1097,6 +1149,7 @@ void xrRender_initconsole()
// Common
CMD1(CCC_Screenshot, "screenshot");
+ CMD1(CCC_RdcCapture, "r__rdc_capture"); // trigger a RenderDoc frame capture, no-op without RenderDoc
// Igor: just to test bug with rain/particles corruption
CMD1(CCC_RestoreQuadIBData, "r_restore_quad_ib_data");
@@ -1328,6 +1381,11 @@ void xrRender_initconsole()
CMD4(CCC_Integer, "r__fakescope", &scope_fake_enabled, 0, 1); //crookr for fake scope
CMD4(CCC_Integer, "r__3Dfakescope", &scope_3D_fake_enabled, 0, 1); // Redotix99: for 3D Shader Based Scopes
+ svp_console_init(); // true PiP cvars register in svp_console.cpp, DX11-gated inside
+ CMD4(CCC_Integer, "r__gpu_markers", &r__gpu_markers, 0, 1); // per-batch events + resource names
+ CMD4(CCC_Integer, "r__shader_debug", &r__shader_debug, 0, 1); // HLSL debug build, clear shaders_cache + reload
+ // -gpu_markers / -shader_debug are forced on in execUserScript() after user.ltx loads
+ // forcing them at registration would be pointless, the config runs later and resets them
CMD4(CCC_Integer, "r__heatvision", &ps_r2_heatvision, 0, 1); //--DSR-- HeatVision
CMD3(CCC_Mask, "r2_terrain_z_prepass", &ps_r2_ls_flags, R2FLAG_TERRAIN_PREPASS); //Terrain Z Prepass @Zagolski
@@ -1365,6 +1423,13 @@ void xrRender_initconsole()
CMD4(CCC_Vector4, "s3ds_param_2", &ps_s3ds_param_2, tw2_min, tw2_max);
CMD4(CCC_Vector4, "s3ds_param_3", &ps_s3ds_param_3, tw2_min, tw2_max);
CMD4(CCC_Vector4, "s3ds_param_4", &ps_s3ds_param_4, tw2_min, tw2_max);
+ CMD4(CCC_Vector4, "shader_scope_params", &ps_shader_scope_params, tw2_min, tw2_max); // scope magnification
+ // per-scope objective geometry x,y lateral, z forward, w radius (eyepiece-radius units), pushed by
+ // zzz_extra_scope_features on aim change. registering the command un-bails that script, which is now
+ // a zoom-neutral pusher (the zoom hijack was stripped), so smooth scroll + the Godis controllers stay
+ Fvector4 sol_min = { -8.f, -8.f, -8.f, -8.f }; // authored envelope, ltx z [2.2,57.8] w [0.325,1.8] x/y [-1.8,2.66]
+ Fvector4 sol_max = { 64.f, 64.f, 64.f, 64.f };
+ CMD4(CCC_Vector4, "scope_objective_lens_offset", &scope_objective_lens_offset, sol_min, sol_max);
CMD4(CCC_Float, "hud_fov_aim_factor", &hud_fov_aim_factor, 0.0f, 1.0f);
diff --git a/src/Layers/xrRender/xrRender_console.h b/src/Layers/xrRender/xrRender_console.h
index 2fd58bc6cd..5db0013853 100644
--- a/src/Layers/xrRender/xrRender_console.h
+++ b/src/Layers/xrRender/xrRender_console.h
@@ -188,6 +188,10 @@ extern ECORE_API Fvector ps_r2_drops_control; // r2-only
extern ECORE_API int ps_r2_nightvision;
extern ECORE_API int scope_fake_enabled; //crookr
extern ECORE_API int scope_3D_fake_enabled; // Redotix99: for 3D Shader Based Scopes
+#include "svp_console.h" // true PiP svp cvars, the complete extern set
+extern ECORE_API Fvector4 scope_objective_lens_offset;
+extern ECORE_API int r__gpu_markers; // RenderDoc: per-batch GPU debug events + resource naming
+extern ECORE_API int r__shader_debug; // RenderDoc: compile shaders with D3DCOMPILE_DEBUG | SKIP_OPTIMIZATION
extern ECORE_API int ps_r2_heatvision; //--DSR-- HeatVision
extern ECORE_API int heat_vision_cooldown; //--DSR-- HeatVision
extern ECORE_API float heat_vision_cooldown_time; //--DSR-- HeatVision
diff --git a/src/Layers/xrRenderDX10/dx10DetailManager_VS.cpp b/src/Layers/xrRenderDX10/dx10DetailManager_VS.cpp
index 6be0bd9981..334cb11644 100644
--- a/src/Layers/xrRenderDX10/dx10DetailManager_VS.cpp
+++ b/src/Layers/xrRenderDX10/dx10DetailManager_VS.cpp
@@ -188,6 +188,11 @@ void CDetailManager::hw_Render_dump(const Fvector4& consts, const Fvector4& wave
c_ambient.set(desc.ambient.x, desc.ambient.y, desc.ambient.z);
c_hemi.set(desc.hemi_color.x, desc.hemi_color.y, desc.hemi_color.z);
+ // pip grass cull, only armed on the SVP gbuffer pass, the main pass pays one bool
+ const bool svp_grass_cull = ps_r__svp_cull_grass && CDSGraphManager::svp_cull_active();
+ // pip the main-pass drain keeps the visible set when the SVP pass draws the scope grass second
+ extern bool g_svp_defer_detail_clear;
+
// Iterate
for (u32 O = 0; O < objects.size(); O++)
{
@@ -292,6 +297,13 @@ void CDetailManager::hw_Render_dump(const Fvector4& consts, const Fvector4& wave
{
SlotItem& Instance = **_iI;
+ if (svp_grass_cull && CDSGraphManager::svp_cull_reject_sphere(Instance.position, dm_slot_size))
+ {
+ if (ps_r__svp_stats) ++svp_stats_grass_cull_reject; // overlay proof the grass cone cull fired
+ if (!svp_ledger_grass_cull_reject) svp_ledger_grass_cull_reject = 1;
+ continue;
+ }
+
if (!RImplementation.GMBase.is_sector_visible(RImplementation.pOutdoorSector))
continue;
@@ -380,7 +392,8 @@ void CDetailManager::hw_Render_dump(const Fvector4& consts, const Fvector4& wave
}
// Clean up
// KD: we must not clear vis on r2 since we want details shadows
- if (ps_ssfx_grass_shadows.x <= 0)
+ // pip the deferred clear keeps the set alive for the SVP drain, its own pass clears after
+ if (ps_ssfx_grass_shadows.x <= 0 && !g_svp_defer_detail_clear)
{
if (!psDeviceFlags2.test(rsGrassShadow) || RImplementation.PHASE_NORMAL == RImplementation.phase) // phase normal without shadows
vis.clear_not_free();
diff --git a/src/Layers/xrRenderDX10/dx10EventWrapper.cpp b/src/Layers/xrRenderDX10/dx10EventWrapper.cpp
index 04d9c2be71..52fa23016e 100644
--- a/src/Layers/xrRenderDX10/dx10EventWrapper.cpp
+++ b/src/Layers/xrRenderDX10/dx10EventWrapper.cpp
@@ -2,6 +2,9 @@
#pragma hdrstop
#include "dx10EventWrapper.h"
#include "../xrRender/HW.h"
+#include "../xrRender/xrRender_console.h" // r__gpu_markers
+
+#include
dxPixEventWrapper::dxPixEventWrapper(LPCWSTR wszName)
{
@@ -13,4 +16,41 @@ dxPixEventWrapper::~dxPixEventWrapper()
{
if (HW.pAnnotation)
HW.pAnnotation->EndEvent();
-}
\ No newline at end of file
+}
+
+// per-batch marker, skipped when r__gpu_markers is off
+dxPixEventScope::dxPixEventScope(const char* fmt, ...) : active(false)
+{
+ if (!r__gpu_markers || !HW.pAnnotation)
+ return;
+
+ char nbuf[256];
+ va_list ap;
+ va_start(ap, fmt);
+ _vsnprintf_s(nbuf, sizeof(nbuf), _TRUNCATE, fmt, ap);
+ va_end(ap);
+
+ wchar_t wbuf[256];
+ ::MultiByteToWideChar(CP_ACP, 0, nbuf, -1, wbuf, (int)(sizeof(wbuf) / sizeof(wbuf[0])));
+
+ HW.pAnnotation->BeginEvent(wbuf);
+ active = true;
+}
+
+dxPixEventScope::~dxPixEventScope()
+{
+ if (active)
+ HW.pAnnotation->EndEvent();
+}
+
+// WKPDID_D3DDebugObjectName, defined locally to avoid a dxguid.lib dependency
+static const GUID kD3DDebugObjectName =
+ { 0x429b8c22, 0x9188, 0x4b0c, { 0x87, 0x42, 0xac, 0xb0, 0xbf, 0x85, 0xc2, 0x00 } };
+
+void dx10_set_debug_name(ID3D11DeviceChild* res, const char* name)
+{
+ if (!r__gpu_markers || !res || !name || !name[0])
+ return;
+
+ res->SetPrivateData(kD3DDebugObjectName, (UINT)xr_strlen(name), name);
+}
diff --git a/src/Layers/xrRenderDX10/dx10EventWrapper.h b/src/Layers/xrRenderDX10/dx10EventWrapper.h
index d09ef8814f..82df01c829 100644
--- a/src/Layers/xrRenderDX10/dx10EventWrapper.h
+++ b/src/Layers/xrRenderDX10/dx10EventWrapper.h
@@ -1,5 +1,9 @@
#pragma once
+// GPU debug-event markers over ID3DUserDefinedAnnotation (HW.pAnnotation), no-op when no debugger attached
+// PIX_EVENT(Name) always-on phase marker, Name is a bare identifier
+// PIX_EVENT_F(fmt,...) per-batch marker with a printf label, uniqued by __LINE__, gated on r__gpu_markers
+
#define PIX_EVENT(Name) dxPixEventWrapper pixEvent##Name(L#Name)
class dxPixEventWrapper
@@ -7,4 +11,23 @@ class dxPixEventWrapper
public:
dxPixEventWrapper(LPCWSTR wszName);
~dxPixEventWrapper();
-};
\ No newline at end of file
+};
+
+// uniqued by __LINE__ so several can share one block
+#define PIX_EVENT_F_CAT_(a, b) a##b
+#define PIX_EVENT_F_CAT(a, b) PIX_EVENT_F_CAT_(a, b)
+#define PIX_EVENT_F(...) dxPixEventScope PIX_EVENT_F_CAT(pixScope_, __LINE__)(__VA_ARGS__)
+
+class dxPixEventScope
+{
+ bool active;
+
+public:
+ explicit dxPixEventScope(const char* fmt, ...);
+ ~dxPixEventScope();
+};
+
+// tag a D3D11 resource/view with a debug name (WKPDID_D3DDebugObjectName) so RenderDoc shows readable
+// names instead of "Render Target NNNN", gated on r__gpu_markers, null-safe
+struct ID3D11DeviceChild;
+void dx10_set_debug_name(ID3D11DeviceChild* res, const char* name);
diff --git a/src/Layers/xrRenderDX10/dx10HW.cpp b/src/Layers/xrRenderDX10/dx10HW.cpp
index bde69ad8ca..c0bfe9795e 100644
--- a/src/Layers/xrRenderDX10/dx10HW.cpp
+++ b/src/Layers/xrRenderDX10/dx10HW.cpp
@@ -849,6 +849,12 @@ void CHW::Reset(HWND hwnd)
_SHOW_REF("refCount:pBaseZB", pBaseZB);
_SHOW_REF("refCount:pBaseRT", pBaseRT);
+ // flip model ResizeBuffers needs every back buffer reference released and unbound from the context
+ // the previous frame's final composite leaves pBaseRT bound, so without this unbind ResizeBuffers
+ // fails with DXGI_ERROR_INVALID_CALL, and the shipping CHK_DX does nothing so that failure is
+ // swallowed and the GetBuffer in UpdateViews then faults inside DXGI
+ pContext->OMSetRenderTargets(0, nullptr, nullptr);
+
_RELEASE(pBaseZB);
_RELEASE(pBaseRT);
@@ -858,12 +864,16 @@ void CHW::Reset(HWND hwnd)
flags |= DXGI_SWAP_CHAIN_FLAG_ALLOW_TEARING;
}
- CHK_DX(m_pSwapChain->ResizeBuffers(
+ // capture explicitly (the shipping CHK_DX does nothing) so a resize failure or a removed device (TDR)
+ // is logged instead of silently corrupting the swapchain and faulting in the UpdateViews GetBuffer
+ const HRESULT _hr_resize = m_pSwapChain->ResizeBuffers(
cd.BufferCount,
cd.Width,
cd.Height,
cd.Format,
- flags));
+ flags);
+ if (FAILED(_hr_resize) || pDevice->GetDeviceRemovedReason() != S_OK)
+ Msg("! [CHW::Reset] ResizeBuffers hr=0x%08x removed=0x%08x", _hr_resize, pDevice->GetDeviceRemovedReason());
#elif defined(USE_DX10)
CHK_DX(m_pSwapChain->ResizeBuffers(
cd.BufferCount,
@@ -1507,5 +1517,12 @@ void CHW::UpdateViews()
R_CHK(R);
pDepthStencil->Release();
+
+#if defined(USE_DX11)
+ // snapshot the true swapchain RTV/DSV, re-run on resize, SetActive (R4) restores the main
+ // target from these so a clobbered pBaseRT from the SVP pass never sticks
+ secret_pBaseRT = pBaseRT;
+ secret_pBaseZB = pBaseZB;
+#endif
}
#endif
diff --git a/src/Layers/xrRenderDX10/dx10ResourceManager_Scripting.cpp b/src/Layers/xrRenderDX10/dx10ResourceManager_Scripting.cpp
index f81405e198..97f8b043ea 100644
--- a/src/Layers/xrRenderDX10/dx10ResourceManager_Scripting.cpp
+++ b/src/Layers/xrRenderDX10/dx10ResourceManager_Scripting.cpp
@@ -632,6 +632,12 @@ ShaderElement* CBlender_Compile::_lua_Compile(LPCSTR namesp, LPCSTR name)
bool bFirstPass = false;
adopt_compiler ac = adopt_compiler(this, bFirstPass);
element(ac, t_0, t_1, t_d);
+
+ // pip auto-flag the 3DSS reflex reticle blenders as scopelense(10) so the engine routes them through
+ // draw_reflex (the GAMMA 4.17 3DSS port dropped the flag), reticle variants only and only if unset
+ if (E.flags.iScopeLense == 0 && namesp && strstr(namesp, "models_reflex_reticle") == namesp)
+ E.flags.iScopeLense = 10;
+
r_End();
ShaderElement* _r = dxRenderDeviceRender::Instance().Resources->_CreateElement(E);
return _r;
diff --git a/src/Layers/xrRenderDX10/dx10SH_RT.cpp b/src/Layers/xrRenderDX10/dx10SH_RT.cpp
index 4ee562b677..f951098b75 100644
--- a/src/Layers/xrRenderDX10/dx10SH_RT.cpp
+++ b/src/Layers/xrRenderDX10/dx10SH_RT.cpp
@@ -6,6 +6,7 @@
#include "../xrRender/dxRenderDeviceRender.h"
#include "dx10TextureUtils.h"
+#include "dx10EventWrapper.h" // dx10_set_debug_name
CRT::CRT()
{
@@ -184,6 +185,13 @@ void CRT::create(LPCSTR Name, u32 w, u32 h, D3DFORMAT f, u32 SampleCount)
UAVDesc.Buffer.NumElements = dwWidth * dwHeight;
CHK_DX(HW.pDevice->CreateUnorderedAccessView( pSurface, &UAVDesc, &pUAView ));
}
+
+ // tag the RT texture + its views with the RT name so captures show readable names instead of
+ // "Render Target NNNN", gated on r__gpu_markers
+ dx10_set_debug_name(pSurface, Name);
+ dx10_set_debug_name(pRT, Name);
+ dx10_set_debug_name(pZRT, Name);
+ dx10_set_debug_name(pUAView, Name);
#endif
pTexture = DEV->_CreateTexture(Name);
diff --git a/src/Layers/xrRenderPC_R1/xrRender_R1.vcxproj b/src/Layers/xrRenderPC_R1/xrRender_R1.vcxproj
index d40c1c2f58..8db34babee 100644
--- a/src/Layers/xrRenderPC_R1/xrRender_R1.vcxproj
+++ b/src/Layers/xrRenderPC_R1/xrRender_R1.vcxproj
@@ -499,6 +499,10 @@
+
+
+
+
@@ -535,6 +539,7 @@
+
@@ -613,6 +618,12 @@
+
+
+ NotUsing
+
+
+
@@ -634,6 +645,7 @@
+
diff --git a/src/Layers/xrRenderPC_R1/xrRender_R1.vcxproj.filters b/src/Layers/xrRenderPC_R1/xrRender_R1.vcxproj.filters
index 3202175f36..985d4ab3b2 100644
--- a/src/Layers/xrRenderPC_R1/xrRender_R1.vcxproj.filters
+++ b/src/Layers/xrRenderPC_R1/xrRender_R1.vcxproj.filters
@@ -171,6 +171,18 @@
Kernel
+
+ Kernel
+
+
+ Kernel
+
+
+ Kernel
+
+
+ Refactored\Execution & 3D\Shaders\Blender
+
Core
@@ -633,6 +645,9 @@
Kernel
+
+ Kernel
+
Kernel
@@ -657,6 +672,18 @@
Core
+
+ Core
+
+
+ Core
+
+
+ Core
+
+
+ Core
+
Core
@@ -939,6 +966,9 @@
Refactored\Execution & 3D\Shaders\Blender
+
+ Refactored\Execution & 3D\Shaders\Blender
+
Refactored\Execution & 3D\Shaders\Blender
diff --git a/src/Layers/xrRenderPC_R2/xrRender_R2.vcxproj b/src/Layers/xrRenderPC_R2/xrRender_R2.vcxproj
index c6e5e27ca1..834aab8b3b 100644
--- a/src/Layers/xrRenderPC_R2/xrRender_R2.vcxproj
+++ b/src/Layers/xrRenderPC_R2/xrRender_R2.vcxproj
@@ -493,6 +493,10 @@
+
+
+
+
@@ -546,6 +550,7 @@
+
@@ -639,6 +644,12 @@
+
+
+ NotUsing
+
+
+
@@ -663,6 +674,7 @@
+
diff --git a/src/Layers/xrRenderPC_R2/xrRender_R2.vcxproj.filters b/src/Layers/xrRenderPC_R2/xrRender_R2.vcxproj.filters
index 56e398d04e..bd12e640a3 100644
--- a/src/Layers/xrRenderPC_R2/xrRender_R2.vcxproj.filters
+++ b/src/Layers/xrRenderPC_R2/xrRender_R2.vcxproj.filters
@@ -174,6 +174,18 @@
Kernel
+
+ Kernel
+
+
+ Kernel
+
+
+ Kernel
+
+
+ Refactored\Execution & 3D\Shaders\Blender
+
Core
@@ -690,6 +702,9 @@
Kernel
+
+ Kernel
+
Kernel
@@ -714,6 +729,18 @@
Core
+
+ Core
+
+
+ Core
+
+
+ Core
+
+
+ Core
+
Core
@@ -1044,6 +1071,9 @@
Refactored\Execution & 3D\Shaders\Blender
+
+ Refactored\Execution & 3D\Shaders\Blender
+
Refactored\Execution & 3D\Shaders\Blender
diff --git a/src/Layers/xrRenderPC_R3/xrRender_R3.vcxproj b/src/Layers/xrRenderPC_R3/xrRender_R3.vcxproj
index eb8e775616..b862cc85e4 100644
--- a/src/Layers/xrRenderPC_R3/xrRender_R3.vcxproj
+++ b/src/Layers/xrRenderPC_R3/xrRender_R3.vcxproj
@@ -523,6 +523,10 @@
+
+
+
+
@@ -604,6 +608,7 @@
+
@@ -695,6 +700,12 @@
+
+
+ NotUsing
+
+
+
@@ -718,6 +729,7 @@
+
diff --git a/src/Layers/xrRenderPC_R3/xrRender_R3.vcxproj.filters b/src/Layers/xrRenderPC_R3/xrRender_R3.vcxproj.filters
index 625bb1a527..451fd36932 100644
--- a/src/Layers/xrRenderPC_R3/xrRender_R3.vcxproj.filters
+++ b/src/Layers/xrRenderPC_R3/xrRender_R3.vcxproj.filters
@@ -198,6 +198,18 @@
Kernel
+
+ Kernel
+
+
+ Kernel
+
+
+ Kernel
+
+
+ Refactored\Execution & 3D\Shaders\Blender
+
Core
@@ -773,6 +785,9 @@
Kernel
+
+ Kernel
+
Kernel
@@ -803,6 +818,18 @@
Core
+
+ Core
+
+
+ Core
+
+
+ Core
+
+
+ Core
+
Core
@@ -1166,6 +1193,9 @@
Refactored\Execution & 3D\Shaders\Blender
+
+ Refactored\Execution & 3D\Shaders\Blender
+
Refactored\Execution & 3D\Shaders\Blender
diff --git a/src/Layers/xrRenderPC_R4/SvpDlss.h b/src/Layers/xrRenderPC_R4/SvpDlss.h
new file mode 100644
index 0000000000..5e699f88da
--- /dev/null
+++ b/src/Layers/xrRenderPC_R4/SvpDlss.h
@@ -0,0 +1,65 @@
+#pragma once
+
+// pip engine-side contract for the SVP DLSS-SR upscale. OUR struct, consumed by Ascii's future
+// EvalSVP_DLSS body. ZERO Streamline/NGX/sl:: dependency, engine + D3D11 types only.
+//
+// MV convention (rt_ssfx_motion_vectors, ScreenSpaceShaders screenspace_mvectors.h ssfx_mv_calc):
+// value = ((cur.xy/cur.w) - (prev.xy/prev.w)), x kept, y negated, whole thing * 0.5
+// units = normalized UV / texture-space (NDC delta * 0.5), NOT pixels
+// dir = current - previous (this frame minus last)
+// Y = texture-space, Y down (the y component is negated)
+// jitter is baked into proj at gate 1 so the SVP MV carries the camera-jitter delta, jitter_px
+// below is the raw offset so Ascii can strip it if the eval wants jitter-free MVs
+// FTreeVisual foliage emits per-viewport camera-motion velocity (wind delta ~0), interactive grass
+// bend is once-per-frame (not per-viewport) so grass velocity is shared with the main camera.
+//
+// OPEN ITEMS to confirm against your working main-view DLSS wrapper (the engine side is plumbed, these
+// depend on what your Streamline integration expects):
+// - DEPTH form: depth_srv is the SVP hardware Z (R24G8 typeless, D24_UNORM readable). if your main view
+// feeds DLSS a linear / view-space depth, provide the SVP equivalent instead (e.g. rt_Position$svp)
+// - COLOR space: color_srv is taken post-combine (post tonemap for LDR, or HDR10) where the lens samples,
+// confirm it matches the color space your main-view DLSS consumes
+// - OUTPUT UAV: out_uav is non-null ONLY when rt_secondVP is the HDR (A16B16G16R16F) format, a BGRA
+// A8R8G8B8 output cannot take a typed UAV, so a UAV-capable SVP output is required for a compute eval
+// - EXPOSURE: not provided (auto-exposure assumed), use exposure_srv if your eval needs an explicit one
+// - JITTER: the stub generator + apply are placeholders, make the offset baked into proj and reported in
+// jitter_px use your DLSS phase + convention (both live in the swappable svp_jitter_* fns)
+// - RESOURCE STATE: the seam does not unbind the combine RTV before the eval nor rebind after (the stub
+// needs neither), a compute eval must bind inputs as SRVs / output as UAV and restore (see EvalSVP_DLSS)
+
+struct ID3D11ShaderResourceView;
+struct ID3D11RenderTargetView;
+struct ID3D11UnorderedAccessView;
+
+struct SvpDlssInputs
+{
+ struct uint2 { u32 x = 0, y = 0; };
+
+ u32 viewport_id = 1; // mirrors Device.dwViewport for the SVP
+
+ // inputs, pre-AA SVP scene at render extent
+ ID3D11ShaderResourceView* color_srv = nullptr; // rt_Generic_0$svp
+ uint2 render_extent;
+ ID3D11ShaderResourceView* depth_srv = nullptr; // baseZB$svp (optional, Ascii may not need it)
+ ID3D11ShaderResourceView* mvec_srv = nullptr; // rt_ssfx_motion_vectors$svp
+ ID3D11ShaderResourceView* exposure_srv = nullptr; // optional, engine does not populate it (auto-exposure)
+
+ // output at display extent
+ ID3D11RenderTargetView* out_rtv = nullptr; // rt_secondVP (stub keeps it render-extent + lens upscales, make it display-res)
+ ID3D11UnorderedAccessView* out_uav = nullptr; // rt_secondVP UAV, null unless r__svp_dlss != 0 (Task 9)
+ uint2 display_extent;
+
+ // raw camera matrices from Device.matrices[1] / matrices_previous[1], row-major row-vector,
+ // jitter is baked into proj, Ascii builds clipToPrevClip / prevClipToClip his own way
+ Fmatrix view, proj, view_proj;
+ Fmatrix prev_view, prev_proj, prev_view_proj;
+
+ Fvector2 jitter_px; // raw sub-pixel jitter baked into proj, pixels
+ bool depth_inverted = false; // X-Ray depth is 0 near to 1 far (not reversed-Z)
+
+ // cached scene constants from the svpCamera tail (near_plane/far_plane avoid the windef near/far macros)
+ float near_plane = 0.f, far_plane = 0.f, fov = 0.f, aspect = 1.f; // fov in radians
+ Fvector cam_pos, up, right, fwd;
+
+ bool reset = false; // snapshot of dlss_reset_next at consume time, history invalidate
+};
diff --git a/src/Layers/xrRenderPC_R4/r4.cpp b/src/Layers/xrRenderPC_R4/r4.cpp
index 1d4853f060..e8c63171b1 100644
--- a/src/Layers/xrRenderPC_R4/r4.cpp
+++ b/src/Layers/xrRenderPC_R4/r4.cpp
@@ -10,6 +10,8 @@
#include "../xrRender/dxRenderDeviceRender.h"
#include "../xrRender/dxWallMarkArray.h"
#include "../xrRender/dxUIShader.h"
+#include "../xrRender/xrRender_console.h" // r__shader_debug
+#include "svp_stats.h" // pip render-stats overlay, query pool release on reset/destroy
#include "../xrRenderDX10/3DFluid/dx103DFluidManager.h"
#include "../xrRender/ShaderResourceTraits.h"
@@ -530,6 +532,8 @@ void CRender::create()
m_bMakeAsyncSS = false;
Target = xr_new(); // Main target
+ TargetMain = Target;
+ TargetSVP = nullptr; // created lazily on first SVP use
Models = xr_new();
PSLibrary.OnCreate();
@@ -552,8 +556,11 @@ void CRender::destroy()
GMBase.destroy();
HWOCC.occq_destroy();
+ svp_stats::release(); // pip drop the stats query pool + font with the device
xr_delete(Models);
xr_delete(Target);
+ TargetMain = nullptr;
+ xr_delete(TargetSVP); // pip: only non-null if an SVP pass ran
PSLibrary.OnDestroy();
Device.seqFrame.Remove(this);
Device.ModelDefferClear = nullptr;
@@ -571,7 +578,10 @@ void CRender::reset_begin()
//-AVO
xr_delete(Target);
+ TargetMain = nullptr;
+ xr_delete(TargetSVP); // pip: drop the SVP target across vid_restart, re-lazy-alloc after reset
HWOCC.occq_destroy();
+ svp_stats::release(); // pip release the stats queries before ResizeBuffers, re-lazy-alloc on next enabled frame
}
void CRender::reset_end()
@@ -579,6 +589,7 @@ void CRender::reset_end()
HWOCC.occq_create(occq_size);
Target = xr_new();
+ TargetMain = Target;
//AVO: let's reload details while changed details options on vid_restart
if (b_loaded && ((dm_current_size != dm_size) || (ps_r__Detail_density != ps_current_detail_density) || (
@@ -769,7 +780,8 @@ FSlideWindowItem* CRender::getSWI(int id)
return &SWIs[id];
}
-IRender_Target* CRender::getTarget() { return Target; }
+// pip external callers (ppe params, UI metrics) get the main target
+IRender_Target* CRender::getTarget() { return TargetMain ? (IRender_Target*)TargetMain : Target; }
IRender_Light* CRender::light_create() { return Lights.Create(); }
IRender_Glow* CRender::glow_create() { return xr_new(); }
@@ -852,7 +864,8 @@ void CRender::add_Occluder(Fbox2& bb_screenspace)
void CRender::rmNear()
{
- IRender_Target* T = getTarget();
+ // the viewport matches the ACTIVE target
+ IRender_Target* T = Target;
D3D_VIEWPORT VP = {0, 0, (float)T->get_width(), (float)T->get_height(), 0, 0.02f};
HW.pContext->RSSetViewports(1, &VP);
@@ -861,7 +874,7 @@ void CRender::rmNear()
void CRender::rmFar()
{
- IRender_Target* T = getTarget();
+ IRender_Target* T = Target;
D3D_VIEWPORT VP = {0, 0, (float)T->get_width(), (float)T->get_height(), 0.99999f, 1.f};
HW.pContext->RSSetViewports(1, &VP);
@@ -870,7 +883,7 @@ void CRender::rmFar()
void CRender::rmNormal()
{
- IRender_Target* T = getTarget();
+ IRender_Target* T = Target;
D3D_VIEWPORT VP = {0, 0, (float)T->get_width(), (float)T->get_height(), 0, 1.f};
HW.pContext->RSSetViewports(1, &VP);
@@ -1997,7 +2010,8 @@ HRESULT CRender::shader_compile(
if (useGeneratedShaderCache)
source_crc = getShaderSourceCrc32(pSrcData, SrcDataLen, ::Render->getShaderPath());
- if (FS.exist(file_name))
+ // r__shader_debug skips the cache read so a cached optimized blob does not shadow the debug build
+ if (!r__shader_debug && FS.exist(file_name))
{
IReader* file = FS.r_open(file_name);
if (useGeneratedShaderCache)
@@ -2041,6 +2055,10 @@ HRESULT CRender::shader_compile(
includer Includer;
LPD3DBLOB pShaderBuf = NULL;
LPD3DBLOB pErrorBuf = NULL;
+ // r__shader_debug emits debug info + skips optimization so RenderDoc can step the HLSL source
+ DWORD compileFlags = Flags;
+ if (r__shader_debug)
+ compileFlags |= D3DCOMPILE_DEBUG | D3DCOMPILE_SKIP_OPTIMIZATION;
_result =
D3DCompile(
pSrcData,
@@ -2048,23 +2066,27 @@ HRESULT CRender::shader_compile(
"", //NULL, //LPCSTR pFileName, // NVPerfHUD bug workaround.
defines, &Includer, pFunctionName,
pTarget,
- Flags, 0,
+ compileFlags, 0,
&pShaderBuf,
&pErrorBuf
);
if (SUCCEEDED(_result))
{
- IWriter* file = FS.w_open(file_name);
+ // don't cache the debug blob, keyed by source CRC, it would later load as if it were a normal build
+ if (!r__shader_debug)
+ {
+ IWriter* file = FS.w_open(file_name);
- u32 const crc = crc32(pShaderBuf->GetBufferPointer(), pShaderBuf->GetBufferSize());
+ u32 const crc = crc32(pShaderBuf->GetBufferPointer(), pShaderBuf->GetBufferSize());
- if (useGeneratedShaderCache)
- file->w_u32(source_crc);
+ if (useGeneratedShaderCache)
+ file->w_u32(source_crc);
- file->w_u32(crc);
- file->w(pShaderBuf->GetBufferPointer(), (u32)pShaderBuf->GetBufferSize());
- FS.w_close(file);
+ file->w_u32(crc);
+ file->w(pShaderBuf->GetBufferPointer(), (u32)pShaderBuf->GetBufferSize());
+ FS.w_close(file);
+ }
_result = create_shader(pTarget, (DWORD*)pShaderBuf->GetBufferPointer(), (u32)pShaderBuf->GetBufferSize(),
file_name, result, o.disasm);
diff --git a/src/Layers/xrRenderPC_R4/r4.h b/src/Layers/xrRenderPC_R4/r4.h
index af5a6e0ebd..385f256a7d 100644
--- a/src/Layers/xrRenderPC_R4/r4.h
+++ b/src/Layers/xrRenderPC_R4/r4.h
@@ -171,7 +171,9 @@ class CRender : public IRender_interface, public pureFrame
CModelPool* Models;
CWallmarksEngine* Wallmarks;
- CRenderTarget* Target; // Render-target
+ CRenderTarget* Target; // Render-target (currently active, set by SetActive)
+ CRenderTarget* TargetMain; // full-screen main viewport
+ CRenderTarget* TargetSVP; // second viewport (square) for true PiP, created lazily on first SVP use
CLight_DB Lights;
SMAP_Allocator LP_smap_pool;
@@ -328,6 +330,8 @@ class CRender : public IRender_interface, public pureFrame
virtual IRenderVisual* getVisual(int id);
virtual IRender_Sector* detectSector(const Fvector& P);
virtual IRender_Target* getTarget();
+ void SetMatrices(Fmatrix view, Fmatrix projection, Fmatrix projection_hud);
+ void EnsureTargetSVP(); // pip: lazy-alloc TargetSVP on the first SVP pass (never when off)
// Main
virtual void flush();
@@ -419,6 +423,10 @@ class CRender : public IRender_interface, public pureFrame
// Main
virtual void Calculate();
virtual void Render();
+ void renderGBuffer(bool clearGraph); // pip per-viewport gbuffer fill, clearGraph false keeps the graph for the SVP pass
+ void renderSceneLighting(BOOL bSUN, bool svp); // pip shared lighting, generate shadows once, accumulate per viewport
+ void share_main_smaps(); // pip: re-point the shadow atlas textures at the main maps for the SVP accumulate
+ void deriveScopeLens(); // pip extract the eyepiece/objective lens from the captured scope meshes
virtual void Screenshot(ScreenshotMode mode = SM_NORMAL, LPCSTR name = 0);
virtual void Screenshot(ScreenshotMode mode, CMemoryWriter& memory_writer);
virtual void ScreenshotAsyncBegin();
diff --git a/src/Layers/xrRenderPC_R4/r4_R_render.cpp b/src/Layers/xrRenderPC_R4/r4_R_render.cpp
index a8bf2a8fb8..5fb12c6406 100644
--- a/src/Layers/xrRenderPC_R4/r4_R_render.cpp
+++ b/src/Layers/xrRenderPC_R4/r4_R_render.cpp
@@ -7,6 +7,46 @@
#include "../../xrParticles/ParticlesAsyncManager.h"
#include "../xrRender/QueryHelper.h"
+#include "../../Include/xrAPI/xrAPI.h" // pip DRender, the debug-line backend for the scope_debug world overlay
+#include "../../Include/xrRender/DebugRender.h" // pip IDebugRender::add_lines
+#include "../xrRender/SkeletonX.h" // pip CSkeletonX for the skinned lens bone transform
+#include "svp_camera.h" // pip svpCamera + the hud geometry snapshot
+#include "svp_stats.h" // pip per-viewport render-stats overlay
+
+// set all per-viewport camera globals from an explicit view/proj/proj_hud
+// called by SetActive, the main path still uses on_idle
+void CRender::SetMatrices(Fmatrix view, Fmatrix projection, Fmatrix projection_hud)
+{
+ Device.mView.set(view);
+ Device.mProject.set(projection);
+ Device.mProjectHud.set(projection_hud);
+ Device.mFullTransform.mul(Device.mProject, Device.mView);
+ Device.mFullTransformHud.mul(Device.mProjectHud, Device.mView);
+
+ Device.mInvView.invert(view);
+ Device.mInvProject.invert(projection);
+ Device.mInvProjectHud.invert(projection_hud);
+ D3DXMatrixInverse((D3DXMATRIX*)&Device.mInvFullTransform, 0, (D3DXMATRIX*)&Device.mFullTransform);
+
+ Device.mInvView.transform(Device.vCameraPosition.set(0, 0, 0));
+ Device.mInvView.transform_dir(Device.vCameraDirection.set(0, 0, 1));
+ Device.mInvView.transform_dir(Device.vCameraTop.set(0, 1, 0));
+ Device.mInvView.transform_dir(Device.vCameraRight.set(1, 0, 0));
+
+ Device.vCameraPosition_saved.set(Device.vCameraPosition);
+ Device.mView_saved.set(Device.mView);
+ Device.mProject_saved.set(Device.mProject);
+ Device.mFullTransform_saved.set(Device.mFullTransform);
+
+ float fFov, fAspect, _;
+ projection.decompose_projection(fFov, fAspect, _, _);
+ Device.fFOV = rad2deg(fFov);
+ Device.fASPECT = fAspect;
+
+ Device.m_pRender->SetCacheXform(Device.mView, Device.mProject);
+ Device.prepare_matrices();
+}
+
#include "../xrRender/dxRenderDeviceRender.h"
@@ -94,6 +134,10 @@ void CRender::Render()
return;
}
+ // pip svp render-stats frame boundary, self-gates to nothing when r__svp_stats is 0
+ svp_stats::frame_begin();
+ svp_stats::section_begin(svp_stats::SEC_FRAME);
+
if ((Device.dwFrame % (u32)ps_r__tex_evict_interval) == 0)
dxRenderDeviceRender::Instance().Resources->EvictStalledTextures();
@@ -110,9 +154,6 @@ void CRender::Render()
ViewBase.CreateFromMatrix(Device.mFullTransform, FRUSTUM_P_LRTB + FRUSTUM_P_FAR);
HOM.MT_RENDER();
- Target->phase_scene_prepare();
-
-
//******* Main calc - DEFERRER RENDERER
phase = PHASE_NORMAL;
@@ -133,6 +174,199 @@ void CRender::Render()
GMBase.r_dsgraph_capture_static();
GMBase.r_dsgraph_capture_dynamic();
+ // pip reset the per-frame lens, it is re-derived each frame so an un-scoped frame leaves radius 0
+ // the debug overlays hold the last radii while the svp stays active so a culled weapon keeps its lines
+ {
+ auto& vp = Device.m_SecondViewport;
+ if (vp.eyepiece.radius > EPS)
+ {
+ vp.dbg_eyepiece_r = vp.eyepiece.radius;
+ vp.dbg_objective_r = vp.objective.radius;
+ }
+ if (!(Device.true_pip_on && vp.IsSVPActive()))
+ {
+ vp.dbg_eyepiece_r = 0.f;
+ vp.dbg_objective_r = 0.f;
+ }
+ vp.eyepiece.radius = 0;
+ vp.objective.radius = 0;
+ }
+
+ // pip a stale hook must never survive into this frame (it captures this + TargetSVP)
+ Device.m_SecondViewport.dual_accum = nullptr;
+ // pip double-pass, the captured graph renders once for the main viewport and, when a scope drives
+ // the SVP, a second time into TargetSVP, true_pip off keeps the single stock main pass
+ bool svp = Device.true_pip_on && Device.m_SecondViewport.IsSVPActive();
+ // pip lock the adaptive SVP size at ADS-in (svp false -> true) from the disc learned on the last aim, so the
+ // target is sized once per aim and never resized mid-ADS (the live disc keeps learning for next time)
+ static bool s_prev_svp = false;
+ static u32 s_disc_epoch = 0;
+ // an optic swap while aimed re-locks from the re-learned disc so the target resizes once for the
+ // new optic, the ads-in edge still owns the first lock
+ const bool disc_swap = (svp && Device.m_SecondViewport.svp_optic_epoch != s_disc_epoch);
+ if ((svp && !s_prev_svp) || disc_swap)
+ Device.m_SecondViewport.svp_disc_applied = Device.m_SecondViewport.svp_disc_px;
+ s_disc_epoch = Device.m_SecondViewport.svp_optic_epoch;
+ const bool svp_edge = (svp != s_prev_svp);
+ s_prev_svp = svp;
+ // pip allocate the SVP target before the main pass derives the camera into it (lazy, only while a
+ // PiP scope is aimed, never when off)
+ if (Device.true_pip_on && g_pGamePersistent &&
+ g_pGamePersistent->m_pGShaderConstants->hud_params.y > 0.005f)
+ EnsureTargetSVP();
+ // pip the lens content swaps at the scope edges, both taa histories seed from the current
+ // frame so the stale scene never ghosts through the resolve
+ if (svp_edge)
+ {
+ if (TargetMain)
+ TargetMain->m_taa_seed_history = true;
+ if (TargetSVP)
+ TargetSVP->m_taa_seed_history = true;
+ // the svp water reflection accumulator still holds the last scope session and its
+ // blend converges too slowly for the raise, a horizon tint start hides the stale box
+ if (TargetSVP && TargetSVP->rt_ssfx_water && g_pGamePersistent
+ && g_pGamePersistent->Environment().CurrentEnv)
+ {
+ const Fvector3& fc = g_pGamePersistent->Environment().CurrentEnv->fog_color;
+ FLOAT cc[4] = {fc.x, fc.y, fc.z, 1.f};
+ HW.pContext->ClearRenderTargetView(TargetSVP->rt_ssfx_water->pRT, cc);
+ }
+ }
+ // creation can fail at VRAM exhaustion, fall back to the single stock pass this frame
+ if (svp && !TargetSVP)
+ svp = false;
+ // pip diag, an aimed weapon without an svp pass (y is nonzero even at hip, gate on x)
+ {
+ if (ps_r__svp_diag && Device.true_pip_on && !svp && g_pGamePersistent
+ && g_pGamePersistent->m_pGShaderConstants->hud_params.x > 0.05f)
+ {
+ static u32 s_ns_ms = 0;
+ if (Device.dwTimeGlobal - s_ns_ms > 500)
+ {
+ s_ns_ms = Device.dwTimeGlobal;
+ PipMsg("[SVP-STATE] zoomed hud without an svp pass, active=%d target=%d",
+ (int)Device.m_SecondViewport.IsSVPActive(), TargetSVP ? 1 : 0);
+ }
+ }
+ }
+ // pip [SVP-NVG] tuning diag, edge on generation change plus a slow heartbeat while active
+ {
+ extern Fvector4 ps_dev_param_7;
+ extern Fvector4 ps_dev_param_8;
+ extern int ps_r2_nightvision;
+ static float s_nvg_gen = -1.f;
+ static u32 s_nvg_ms = 0;
+ const float genf = floorf(ps_dev_param_8.x);
+ const bool edge = _abs(ps_dev_param_8.x - s_nvg_gen) > 0.001f;
+ if (ps_r__svp_diag && (edge || (genf >= 1.f && Device.dwTimeGlobal - s_nvg_ms > 2000)))
+ {
+ s_nvg_gen = ps_dev_param_8.x;
+ s_nvg_ms = Device.dwTimeGlobal;
+ const float gain_off = floorf(ps_dev_param_8.w) / 10.f;
+ PipMsg("[SVP-NVG] gen=%.0f tubes=%.1f gain_cur=%.2f gain_off=%.2f tm_scale=%.1f vig=%.2f r2nv=%d scoped=%d aim=%.2f p7=%.2f/%.2f/%.2f/%.2f",
+ genf, (ps_dev_param_8.x - genf) * 10.f,
+ floorf(ps_dev_param_8.y) / 10.f, gain_off, 10.f * gain_off,
+ floorf(ps_dev_param_8.z) / 100.f, ps_r2_nightvision,
+ (int)(Device.true_pip_on && Device.m_SecondViewport.IsSVPActive()),
+ g_pGamePersistent ? g_pGamePersistent->m_pGShaderConstants->hud_params.x : 0.f,
+ ps_dev_param_7.x, ps_dev_param_7.y, ps_dev_param_7.z, ps_dev_param_7.w);
+ }
+ }
+ if (Device.true_pip_on)
+ TargetMain->SetActive();
+ svp_stats::section_begin(svp_stats::SEC_MAIN_GBUFFER);
+ renderGBuffer(!svp); // keep the priority-0 graph when an SVP pass follows
+ svp_stats::section_end(svp_stats::SEC_MAIN_GBUFFER);
+ if (svp)
+ {
+ EnsureTargetSVP();
+ // pip CPU-side cost probe for the SVP gbuffer, throttled log while r__svp_diag is on
+ CTimer svp_t; svp_t.Start();
+ const u32 calls0 = RCache.stat.calls;
+ const u32 verts0 = RCache.stat.verts;
+ TargetSVP->SetActive();
+ svp_stats::section_begin(svp_stats::SEC_SVP_GBUFFER);
+ renderGBuffer(true);
+ svp_stats::section_end(svp_stats::SEC_SVP_GBUFFER);
+ if (ps_r__svp_diag)
+ {
+ static u32 s_perf_ms = 0;
+ if (Device.dwTimeGlobal - s_perf_ms > 1000)
+ {
+ s_perf_ms = Device.dwTimeGlobal;
+ PipMsg("[SVP-PERF] gbuffer %.2fms calls %u verts %uk", svp_t.GetElapsed_sec() * 1000.f,
+ RCache.stat.calls - calls0, (RCache.stat.verts - verts0) / 1000);
+ }
+ }
+ // pip stencil the dead corners outside the eyepiece disc while the svp target is still bound,
+ // so the lighting and combine passes below skip them
+ if (ps_r__svp_corner_mask && !RImplementation.o.dx10_msaa && TargetSVP)
+ TargetSVP->stamp_svp_corner_mask();
+ TargetMain->SetActive(); // shadow generation + main accumulation run on the main target
+
+ // pip install the dual-accumulate hook, each shadow unit builds its map once on the main atlas
+ // then this re-accumulates it into the SVP with no second shadow render
+ Device.m_SecondViewport.dual_accum = [this](const std::function& accum)
+ {
+ TargetSVP->SetActive(); // SVP gbuffer + accumulator (and, for now, the SVP shadow atlas)
+ share_main_smaps(); // re-point the shadow atlas at the main maps the generation built
+ Device.m_SecondViewport.force_svp_sss = (ps_r__svp_sss_sun != 0); // sun keeps the SSS contact term
+ svp_stats::note_svp_blend();
+ svp_stats::section_begin(svp_stats::SEC_SVP_LIGHTS);
+ { PIX_EVENT(SVP_ACCUM); accum(); } // SVP marginal lighting cost: accumulate this unit into the SVP (shared maps)
+ svp_stats::section_end(svp_stats::SEC_SVP_LIGHTS);
+ Device.m_SecondViewport.force_svp_sss = false;
+ TargetMain->SetActive(); // restore for the next unit's generation on the main atlas
+ };
+ }
+
+ // single shared lighting pass, when svp the render_sun_cascades/render_lights dual-accumulate via
+ // the hook and the SVP is combined + captured before the main combine composites it into the lens
+ renderSceneLighting(bSUN, svp);
+ Device.m_SecondViewport.dual_accum = nullptr;
+
+ // pip close the whole-frame window then draw the stats panel over the finished image
+ svp_stats::section_end(svp_stats::SEC_FRAME);
+ svp_stats::frame_end(svp);
+ svp_stats::draw_overlay();
+}
+
+void CRender::renderGBuffer(bool clearGraph)
+{
+ // label the pass so the SVP (scope) gbuffer is distinguishable from the main one in a capture
+ PIX_EVENT_F("RENDER_GBUFFER[%s]", Target == TargetMain ? "MAIN" : "SVP");
+ Device.dwViewport++; // pip per-viewport cache counter
+
+ // pip cull the SVP geometry to the scope cone, the captured graph is main frustum so the SVP would
+ // otherwise resubmit the whole world through a cone that sees a fraction
+ const bool svp_pass = (Target == TargetSVP) && Device.true_pip_on;
+ const bool svp_cull = svp_pass && ps_r__svp_cull;
+ const bool svp_cull_grass = svp_pass && ps_r__svp_cull_grass && !ps_r__svp_skip_grass;
+ if (svp_cull || svp_cull_grass)
+ {
+ Fmatrix svp_full;
+ svp_full.mul(Device.matrices[1].mProject, Device.matrices[1].mView);
+ CDSGraphManager::svp_cull_begin(svp_full, svp_cull);
+ }
+ // pip SVP coverage = (mag * svp_side / main_height)^2, capped at 1: the fraction of the main view's
+ // pixel area an object covers in the scope. feeds the LOD scale and the small-object cull threshold
+ if (svp_pass && TargetSVP && (ps_r__svp_lod > 0.f || ps_r__svp_cull_ssa > 0.f))
+ {
+ extern float g_pip_scope_magnification;
+ const float mh = (float)Device.dwHeight;
+ const float svp_eff = sqrtf((float)TargetSVP->Width * (float)TargetSVP->Height); // geometric-mean side (== Width when square)
+ float cov = (mh > 1.f) ? (g_pip_scope_magnification * svp_eff / mh) : 1.f;
+ cov *= cov;
+ if (cov > 1.f) cov = 1.f;
+ if (ps_r__svp_lod > 0.f)
+ CDSGraphManager::svp_set_lod_scale(1.f + (cov - 1.f) * ps_r__svp_lod);
+ if (ps_r__svp_cull_ssa > 0.f)
+ CDSGraphManager::svp_set_ssa_cull(ps_r__svp_cull_ssa, cov);
+ }
+
+ phase = PHASE_NORMAL;
+ Target->phase_scene_prepare(); // clears + binds this viewport's gbuffer and depth
+
if (RImplementation.o.ssfx_motionvectors)
{
Target->u_setrt(Device.dwWidth, Device.dwHeight, 0, 0, Target->rt_ssfx_motion_vectors->pRT, 0);
@@ -159,13 +393,15 @@ void CRender::Render()
PIX_EVENT(DEFER_PART0_SPLIT);
// level, SPLIT
Target->phase_scene_begin();
- GMBase.r_dsgraph_render_static(0);
- GMBase.r_dsgraph_render_dynamic(0);
+ GMBase.r_dsgraph_render_static(0, clearGraph);
+ GMBase.r_dsgraph_render_dynamic(0, clearGraph);
Target->disable_aniso();
}
- // Redotix99: for 3D Shader Based Scopes
- if (scope_3D_fake_enabled)
+ // Redotix99: for 3D Shader Based Scopes
+ // legacy fallback, main view only, skip under true PiP where the fakescope consumer is excluded
+ if (scope_3D_fake_enabled && Target == TargetMain
+ && !(Device.true_pip_on && Device.m_SecondViewport.IsSVPActive()))
{
ID3D11Resource* zbuffer_res;
HW.pBaseZB->GetResource(&zbuffer_res);
@@ -175,6 +411,7 @@ void CRender::Render()
if (RImplementation.o.dx10_msaa)
RCache.set_ZB(RImplementation.Target->rt_MSAADepth->pZRT);
+ if (Target == TargetMain) // pip lights captured once on main, the SVP reuses them
{
PIX_EVENT(DEFER_TEST_LIGHT_VIS);
//******* Occlusion testing of volume-limited light-sources
@@ -182,6 +419,13 @@ void CRender::Render()
LP_normal.clear();
LP_pending.clear();
GMBase.r_dsgraph_capture_lights();
+ if (ps_r__svp_stats)
+ {
+ const u32 lp_total = (u32)(LP_normal.v_point.size() + LP_normal.v_spot.size() + LP_normal.v_shadowed.size()
+ + LP_pending.v_point.size() + LP_pending.v_spot.size() + LP_pending.v_shadowed.size());
+ const u32 lp_shadowed = (u32)(LP_normal.v_shadowed.size() + LP_pending.v_shadowed.size());
+ svp_stats::note_main_lights(lp_total, lp_shadowed);
+ }
}
//******* Main render :: PART-1 (second)
@@ -189,13 +433,87 @@ void CRender::Render()
PIX_EVENT(DEFER_PART1_SPLIT);
// level
Target->phase_scene_begin();
- GMBase.r_dsgraph_capture_hud();
- GMBase.r_dsgraph_render_hud();
- GMBase.r_dsgraph_render_lods(true,true);
- if (Details) Details->Render();
+ if (Target == TargetMain) // pip weapon HUD only in the main view, not the scope image
+ {
+ GMBase.r_dsgraph_capture_hud();
+ // pip snapshot HUD geometry centers before render_hud clears the lists, so the geomscan (in
+ // deriveScopeLens, after the clear) can auto-derive the objective distance against the optical axis
+ if (scope_svp_enabled || scope_debug >= 2)
+ svp_snapshot_hud_geom();
+ // pip latch the hud poses this draw uses, logic rewrites the matrices mid render and the
+ // scope draws later in the frame must match the housing
+ if (Device.true_pip_on)
+ GMBase.svp_latch_hud_poses();
+ // keep the weapon list when an SVP pass follows, the scope image drains it second
+ GMBase.r_dsgraph_render_hud(clearGraph);
+
+ // pip derive the scope lens then build the SVP camera (matrices[1]) while a PiP scope
+ // is aimed, zoom-0 tube sights have no zoom fov so ADS + a captured lens also qualifies
+ if (scope_svp_enabled && g_pGamePersistent &&
+ (g_pGamePersistent->m_pGShaderConstants->hud_params.y > 0.005f
+ || (g_pGamePersistent->m_pGShaderConstants->hud_params.x > 0.05f
+ && !GMBase.RGraph.mapScopeHUDSorted.empty())))
+ {
+ deriveScopeLens();
+ // a culled weapon mid-aim re-arms the stale lens radius, m_W persists on its own
+ {
+ auto& vp = Device.m_SecondViewport;
+ if (vp.eyepiece.radius <= EPS && vp.dbg_eyepiece_r > EPS)
+ {
+ vp.eyepiece.radius = vp.dbg_eyepiece_r;
+ vp.objective.radius = vp.dbg_objective_r;
+ }
+ }
+ if (Device.m_SecondViewport.eyepiece.radius > EPS && TargetSVP)
+ svpCamera();
+ }
+ // pip [3DB] for aimed sights without a captured lens, reflex and irons keep 3d
+ // ballistics so the sight independent markers draw, the pip overlay owns the rest
+ {
+ extern int ps_r__3db_debug;
+ extern void ballistics_debug_overlay();
+ if (ps_r__3db_debug > 0 && scope_svp_enabled && g_pGamePersistent
+ && g_pGamePersistent->m_pGShaderConstants->hud_params.x > 0.05f
+ && !(Device.m_SecondViewport.eyepiece.radius > EPS && TargetSVP))
+ ballistics_debug_overlay();
+ }
+ }
+ else if (svp_pass) // pip the weapon renders through the scope at low mag, one unit inside and out
+ {
+ extern float g_pip_scope_magnification;
+ auto& vp = Device.m_SecondViewport;
+ // the weapon drains through the same entrance-pupil camera as the world, the near
+ // plane hides everything behind the front lens
+ if (scope_svp_enabled >= 2 && vp.eyepiece.radius > EPS)
+ {
+ RCache.set_xform_view(Device.matrices[1].mView);
+ RCache.set_xform_project(Device.matrices[1].mProject);
+ GMBase.r_dsgraph_render_hud_svp();
+ }
+ else
+ GMBase.RGraph.mapHUD.clear(); // consume the deferred main-pass clear
+ }
+ GMBase.r_dsgraph_render_lods(true, clearGraph);
+ // pip r__svp_skip_grass drops the near-grass field on the scope pass (mostly off a zoomed cone)
+ if (Details && !(svp_pass && ps_r__svp_skip_grass))
+ {
+ // keep the grass visible set on the main drain when the SVP pass draws it second
+ extern bool g_svp_defer_detail_clear;
+ g_svp_defer_detail_clear = (!svp_pass && !clearGraph && !ps_r__svp_skip_grass);
+ Details->Render();
+ g_svp_defer_detail_clear = false;
+ }
Target->phase_scene_end();
}
+ if (svp_cull || svp_cull_grass)
+ CDSGraphManager::svp_cull_end(); // pip end SVP cull, the shared shadow/light passes below are unaffected
+ if (svp_pass)
+ {
+ CDSGraphManager::svp_set_lod_scale(1.f); // pip restore full LOD + no cull before the shared light passes
+ CDSGraphManager::svp_set_ssa_cull(0.f, 1.f);
+ }
+
// Wall marks
if (Wallmarks)
{
@@ -230,9 +548,22 @@ void CRender::Render()
Target->Matrix_current.set(Device.mProject);
mm_saved_viewproj.set(Device.mFullTransform);
}
+ else if (svp_pass)
+ {
+ // pip the scope water SSR and SSDO reproject against these, the setter blocks are main only so
+ // the SVP would accumulate against stale matrices, build them from the per viewport matrices
+ Fmatrix svp_prev_full, svp_inv_view, svp_prev_inv_view;
+ svp_prev_full.mul(Device.matrices_previous[1].mProject, Device.matrices_previous[1].mView);
+ svp_inv_view.invert(Device.matrices[1].mView);
+ Target->Matrix_previous.mul(svp_prev_full, svp_inv_view);
+ Target->Matrix_current.set(Device.matrices[1].mProject);
+ svp_prev_inv_view.invert(Device.matrices_previous[1].mView);
+ Target->Position_previous.set(svp_prev_inv_view.c);
+ }
}
- if (RImplementation.o.ssfx_sss && !Device.m_SecondViewport.IsSVPFrame())
+ // pip the SVP skips its own SSS pass, r__svp_sss_sun computes it so the scope sun keeps the contact term
+ if (RImplementation.o.ssfx_sss && (!Device.m_SecondViewport.IsSVPFrame() || (svp_pass && ps_r__svp_sss_sun)))
{
static bool sss_rendered, sss_extended_rendered;
@@ -269,26 +600,79 @@ void CRender::Render()
}
}
}
+}
+
+// pip re-point the shadow-atlas textures at the main maps so the SVP accumulation reads the shared
+// shadow maps with no second shadow render (SetActive(SVP) had pointed them at the SVP's empty atlas)
+void CRender::share_main_smaps()
+{
+ for (auto& r : TargetMain->RenderTargetRemaps)
+ if (r.second == TargetMain->rt_smap_depth ||
+ (TargetMain->rt_smap_depth_minmax && r.second == TargetMain->rt_smap_depth_minmax))
+ r.first->surface_set(r.second->pSurface);
+ RCache.Invalidate();
+}
- // Directional light - fucking sun
+void CRender::renderSceneLighting(BOOL bSUN, bool svp)
+{
+ // Directional light - fucking sun, cascades build their shadow map once on the main atlas and the
+ // accumulate is replayed into the SVP by the dual_accum hook (inside render_sun_cascade + below)
if (bSUN) //bSUN && Device.dwFrame & 1 --Delayed sun update. Worth to check it in future
{
PIX_EVENT(DEFER_SUN);
+ svp_stats::note_sun();
+ svp_stats::section_begin(svp_stats::SEC_MAIN_LIGHTS);
RImplementation.stats.l_visible ++;
render_sun_cascades();
- Target->increment_light_marker();
- Target->accum_direct_blend();
+ auto sun_blend = [this] { Target->increment_light_marker(); Target->accum_direct_blend(); };
+ sun_blend();
+ if (Device.m_SecondViewport.dual_accum)
+ Device.m_SecondViewport.dual_accum(sun_blend);
+ svp_stats::section_end(svp_stats::SEC_MAIN_LIGHTS);
}
phase = PHASE_NORMAL;
+ // pip gated self-illum replay into the SVP before the main drain touches the shared list
+ if (svp && ps_r__svp_emissive)
+ {
+ PIX_EVENT(SVP_SELF_ILLUM);
+ svp_stats::section_begin(svp_stats::SEC_SVP_EMISSIVE);
+ TargetSVP->SetActive();
+ TargetSVP->phase_accumulator();
+ RCache.set_xform_project(Device.mProject);
+ RCache.set_xform_view(Device.mView);
+ if (!RImplementation.o.dx10_msaa)
+ RCache.set_Stencil(TRUE, D3DCMP_ALWAYS, 0x01, 0xff, 0xff, D3DSTENCILOP_KEEP, D3DSTENCILOP_REPLACE,
+ D3DSTENCILOP_KEEP);
+ else
+ RCache.set_Stencil(TRUE, D3DCMP_ALWAYS, 0x01, 0xff, 0x7f, D3DSTENCILOP_KEEP, D3DSTENCILOP_REPLACE,
+ D3DSTENCILOP_KEEP);
+ RCache.set_CullMode(CULL_CCW);
+ RCache.set_ColorWriteEnable();
+ // the shared list is main frustum wide, reject static items outside the scope cone
+ if (ps_r__svp_cull)
+ {
+ Fmatrix svp_full;
+ svp_full.mul(Device.matrices[1].mProject, Device.matrices[1].mView);
+ CDSGraphManager::svp_cull_begin(svp_full, true);
+ }
+ GMBase.r_dsgraph_render_emissive(false);
+ // the cone must not leak into the main emissive drain below
+ CDSGraphManager::svp_cull_end();
+ TargetMain->SetActive();
+ svp_stats::section_end(svp_stats::SEC_SVP_EMISSIVE);
+ }
+
+ // emissive runs on the main viewport only (active here), the SVP skips self-illum (minor) so it does
+ // not re-render + clear the shared GMBase emissive list that the main pass still needs
{
PIX_EVENT(DEFER_SELF_ILLUM);
Target->phase_accumulator();
// Render emissive geometry, stencil - write 0x0 at pixel pos
RCache.set_xform_project(Device.mProject);
RCache.set_xform_view(Device.mView);
- // Stencil - write 0x1 at pixel pos -
+ // Stencil - write 0x1 at pixel pos -
if (!RImplementation.o.dx10_msaa)
RCache.set_Stencil(TRUE, D3DCMP_ALWAYS, 0x01, 0xff, 0xff, D3DSTENCILOP_KEEP, D3DSTENCILOP_REPLACE,
D3DSTENCILOP_KEEP);
@@ -310,7 +694,9 @@ void CRender::Render()
GMBase.r_dsgraph_render_emissive(true, true);
}
- // Lighting, non dependant on OCCQ
+ // Lighting, shadow maps build once on the main atlas, render_lights accumulates per viewport
+ // (it replays the accumulate into the SVP via dual_accum, sharing the maps), non dependant on OCCQ
+ svp_stats::section_begin(svp_stats::SEC_MAIN_LIGHTS); // svp mirror nests here through the hook
{
PIX_EVENT(DEFER_LIGHT_NO_OCCQ);
Target->phase_accumulator();
@@ -322,6 +708,37 @@ void CRender::Render()
PIX_EVENT(DEFER_LIGHT_OCCQ);
render_lights(LP_pending);
}
+ svp_stats::section_end(svp_stats::SEC_MAIN_LIGHTS);
+
+ // pip volumetric blur + combine per viewport, the SVP is combined and captured first so the main
+ // combine can composite the ready SVP image into the lens
+ if (svp)
+ {
+ svp_stats::section_begin(svp_stats::SEC_SVP_COMBINE);
+ TargetSVP->SetActive();
+ if (RImplementation.o.ssfx_volumetric)
+ Target->phase_ssfx_volumetric_blur();
+ phase = PHASE_NORMAL;
+ {
+ PIX_EVENT(COMBINE_SVP);
+ Target->phase_combine();
+ }
+
+ TargetSVP->phase_svp_capture(); // SVP combined color -> rt_secondVP, ready for the main lens
+
+ // pip a hybrid magnifier draws the holo dot as a collimated proxy into the captured svp image
+ // so it magnifies and tracks, success gates the 1x main-view overlay suppression
+ Device.m_SecondViewport.svp_reflex_proxy_ok = false;
+ if (ps_r__svp_reflex_capture
+ && !GMBase.RGraph.mapReflexHUDSorted.empty()
+ && !GMBase.RGraph.mapScopeHUDSorted.empty())
+ {
+ Device.m_SecondViewport.svp_reflex_proxy_ok = TargetSVP->draw_reflex_proxy();
+ }
+
+ TargetMain->SetActive();
+ svp_stats::section_end(svp_stats::SEC_SVP_COMBINE);
+ }
{
if (RImplementation.o.ssfx_volumetric)
@@ -333,21 +750,26 @@ void CRender::Render()
// Postprocess
{
PIX_EVENT(DEFER_LIGHT_COMBINE);
+ svp_stats::section_begin(svp_stats::SEC_MAIN_COMBINE);
Target->phase_combine();
+ svp_stats::section_end(svp_stats::SEC_MAIN_COMBINE);
}
+ // pip r__scope_debug on-screen inspector overlay (gated + main-only inside the call)
+ Target->phase_scope_debug();
+
+ // detail-clear + HUD UI on the main viewport, the scope image has no separate HUD UI
if (Details)
Details->details_clear();
if (g_hud)
{
- PROF_EVENT("render_hud");
+ PROF_EVENT("render_hud");
if (g_hud->RenderActiveItemUIQuery())
GMBase.r_dsgraph_render_hud_ui();
if (g_hud->RenderCamAttachedUIQuery())
GMBase.r_dsgraph_render_cam_ui();
}
-
}
#include "../xrRender/CHudInitializer.h"
@@ -357,13 +779,16 @@ void CRender::render_forward()
//******* Main render - second order geometry (the one, that doesn't support deffering)
//.todo: should be done inside "combine" with estimation of of luminance, tone-mapping, etc.
+ // combine order, main clears the shared priority-1 forward lists last (keeps smoke + blended fx)
+ const bool fwd_clear = svp_clear_shared_list(true);
+ if (!fwd_clear) { if (ps_r__svp_stats) ++svp_stats_fwd_keep; svp_ledger_fwd_keep = 1; } // overlay + ledger proof a forward-list keep fired
{
// level
phase = PHASE_NORMAL;
// Igor: we don't want to render old lods on next frame.
- GMBase.r_dsgraph_render_static(1); // normal level, secondary priority
+ GMBase.r_dsgraph_render_static(1, fwd_clear); // normal level, secondary priority
CParticlesAsync::Wait();
- GMBase.r_dsgraph_render_dynamic(1);
+ GMBase.r_dsgraph_render_dynamic(1, fwd_clear);
GMBase.fade_render(); // faded-portals
GMBase.r_dsgraph_render_sorted(false); // strict-sorted geoms
g_pGamePersistent->Environment().RenderLast(); // rain/thunder-bolts
diff --git a/src/Layers/xrRenderPC_R4/r4_rendertarget.cpp b/src/Layers/xrRenderPC_R4/r4_rendertarget.cpp
index 03db2f91b8..d5fad07400 100644
--- a/src/Layers/xrRenderPC_R4/r4_rendertarget.cpp
+++ b/src/Layers/xrRenderPC_R4/r4_rendertarget.cpp
@@ -347,7 +347,69 @@ void generate_jitter(DWORD* dest, u32 elem_count)
*dest = color_rgba(samples[2 * it].x, samples[2 * it].y, samples[2 * it + 1].y, samples[2 * it + 1].x);
}
+#if USE_DX11
+void CRenderTarget::SetActive(bool force)
+{
+ const bool isMain = this == RImplementation.TargetMain;
+ if (isMain)
+ {
+ // main restores from the persistent swapchain RTV/DSV, not HW.pBaseRT, which SetActive
+ // clobbers per target so re-reading it would capture the SVP RT and freeze the screen
+ baseRT = HW.secret_pBaseRT;
+ baseZB = HW.secret_pBaseZB;
+ }
+ HW.pBaseRT = baseRT;
+ HW.pBaseZB = baseZB;
+
+ if (!force && RImplementation.Target == this)
+ {
+ for (auto& r : RenderTargetRemaps)
+ r.first->surface_set(r.second->pSurface);
+ return;
+ }
+
+ auto m = Device.matrices[isMain ? 0 : 1];
+ if (g_pGamePersistent)
+ {
+ g_pGamePersistent->m_pGShaderConstants->hud_params.w = !isMain;
+ Device.m_SecondViewport.m_render_pass_is_svp = !isMain;
+ RImplementation.SetMatrices(m.mView, m.mProject, m.mProjectHud);
+ }
+ RImplementation.Target = this;
+
+ RImplementation.ViewBase.CreateFromMatrix(Device.mFullTransform, FRUSTUM_P_LRTB + FRUSTUM_P_FAR);
+
+ Device.dwWidth = Width;
+ Device.dwHeight = Height;
+ Device.fASPECT = (float)Height / (float)Width;
+ Device.fWidth_2 = Width >> 1;
+ Device.fHeight_2 = Height >> 1;
+
+ // the bind cache keys on STextureList/CTexture identity (R_Backend set_Textures), so the
+ // surface_set remap below is invisible to it, a full invalidate forces every RT/texture/
+ // matrix/constant to re-bind for this viewport (non-MT calls RCache.Invalidate here too)
+ RCache.Invalidate();
+
+ // point the stock-named textures at this viewport's RTs (restore for the main)
+ for (auto& r : RenderTargetRemaps)
+ r.first->surface_set(r.second->pSurface);
+
+ set_viewport_size(HW.pContext, (float)Width, (float)Height);
+ RCache.set_RT(baseRT, 0);
+ RCache.set_RT(nullptr, 1);
+ RCache.set_RT(nullptr, 2);
+ RCache.set_RT(nullptr, 3);
+ RCache.set_ZB(baseZB);
+}
+#endif
+
CRenderTarget::CRenderTarget()
+ : CRenderTarget(nullptr, Device.dwWidth, Device.dwHeight)
+{
+}
+
+CRenderTarget::CRenderTarget(LPCSTR name, u32 width, u32 height)
+ : Width(width), Height(height)
{
u32 SampleCount = 1;
@@ -484,28 +546,67 @@ CRenderTarget::CRenderTarget()
static_cast(b_ssao_msaa[i])->SetDefine("ISAMPLE", SampleDefs[i]);
}
}
+ // SVP target builds $id-suffixed RTs and remaps them over the stock names in SetActive
+ // for the main target (name == nullptr) createUnique is a verbatim passthrough to rt.create
+ const bool isMain = (name == nullptr);
+ auto createUnique = [this, isMain, name](LPCSTR Name, u32 cw, u32 ch, D3DFORMAT f, u32 sc = 1, bool uav = false) -> ref_rt
+ {
+ ref_rt rt;
+ if (isMain)
+ rt.create(Name, cw, ch, f, sc, uav);
+ else
+ {
+ string256 rtname;
+ xr_sprintf(rtname, "%s$%s", Name, name);
+ rt.create(rtname, cw, ch, f, sc, uav);
+ }
+ // remap entry, stock-named texture -> this target's RT, SetActive surface_sets the texture
+ // to rt->pSurface so the deferred shaders sample the active viewport, main restores the stock
+ // surface, SVP points it at the SVP RT, never applied when PiP is off (SetActive gated)
+ ref_texture t;
+ t.create(Name);
+ RenderTargetRemaps.push_back({ t, rt });
+ return rt;
+ };
+
// NORMAL
{
- u32 w = Device.dwWidth, h = Device.dwHeight;
- rt_Position.create(r2_RT_P, w, h, D3DFMT_A16B16G16R16F, SampleCount);
+ u32 w = Width, h = Height;
+
+ // PiP base RT/ZB, main restores from the swapchain, SVP renders into its own
+ if (isMain)
+ {
+ baseRT = HW.secret_pBaseRT;
+ baseZB = HW.secret_pBaseZB;
+ }
+ else
+ {
+ D3DFORMAT colorfmt = RImplementation.o.dx11_hdr10 ? D3DFMT_A16B16G16R16F : D3DFMT_A8R8G8B8;
+ rt_baseRT = createUnique("$user$baseRT", w, h, colorfmt);
+ rt_baseZB = createUnique("$user$baseZB", w, h, D3DFMT_D24S8);
+ baseRT = rt_baseRT->pRT;
+ baseZB = rt_baseZB->pZRT;
+ }
+
+ rt_Position = createUnique(r2_RT_P, w, h, D3DFMT_A16B16G16R16F, SampleCount);
if (RImplementation.o.dx10_msaa)
{
- rt_MSAADepth.create(r2_RT_MSAAdepth, w, h, D3DFMT_D24S8, SampleCount);
+ rt_MSAADepth = createUnique(r2_RT_MSAAdepth, w, h, D3DFMT_D24S8, SampleCount);
}
- rt_tempzb.create("$user$temp_zb", w, h, D3DFMT_D24S8); // Redotix99: for 3D Shader Based Scopes
+ rt_tempzb = createUnique("$user$temp_zb", w, h, D3DFMT_D24S8); // Redotix99: for 3D Shader Based Scopes
// select albedo & accum
if (RImplementation.o.mrtmixdepth)
{
// NV50
if (RImplementation.o.dx11_hdr10) {
- rt_Color.create(r2_RT_albedo, w, h, D3DFMT_A16B16G16R16F, SampleCount);
+ rt_Color = createUnique(r2_RT_albedo, w, h, D3DFMT_A16B16G16R16F, SampleCount);
} else {
- rt_Color.create(r2_RT_albedo, w, h, D3DFMT_A8R8G8B8, SampleCount);
+ rt_Color = createUnique(r2_RT_albedo, w, h, D3DFMT_A8R8G8B8, SampleCount);
}
- rt_Accumulator.create(r2_RT_accum, w, h, D3DFMT_A16B16G16R16F, SampleCount);
+ rt_Accumulator = createUnique(r2_RT_accum, w, h, D3DFMT_A16B16G16R16F, SampleCount);
}
else
{
@@ -513,143 +614,170 @@ CRenderTarget::CRenderTarget()
if (RImplementation.o.fp16_blend)
{
// NV40
- rt_Color.create(r2_RT_albedo, w, h, D3DFMT_A16B16G16R16F, SampleCount); // expand to full
- rt_Accumulator.create(r2_RT_accum, w, h, D3DFMT_A16B16G16R16F, SampleCount);
+ rt_Color = createUnique(r2_RT_albedo, w, h, D3DFMT_A16B16G16R16F, SampleCount); // expand to full
+ rt_Accumulator = createUnique(r2_RT_accum, w, h, D3DFMT_A16B16G16R16F, SampleCount);
}
else
{
// R4xx, no-fp-blend,-> albedo_wo
VERIFY(RImplementation.o.albedo_wo);
- rt_Color.create(r2_RT_albedo, w, h, D3DFMT_A8R8G8B8, SampleCount); // normal
- rt_Accumulator.create(r2_RT_accum, w, h, D3DFMT_A16B16G16R16F, SampleCount);
- rt_Accumulator_temp.create(r2_RT_accum_temp, w, h, D3DFMT_A16B16G16R16F, SampleCount);
+ rt_Color = createUnique(r2_RT_albedo, w, h, D3DFMT_A8R8G8B8, SampleCount); // normal
+ rt_Accumulator = createUnique(r2_RT_accum, w, h, D3DFMT_A16B16G16R16F, SampleCount);
+ rt_Accumulator_temp = createUnique(r2_RT_accum_temp, w, h, D3DFMT_A16B16G16R16F, SampleCount);
}
}
// generic(LDR) RTs
//LV - we should change their formats into D3DFMT_A16B16G16R16F for better HDR support.
if (RImplementation.o.dx11_hdr10) {
- rt_Generic_0.create(r2_RT_generic0, w, h, D3DFMT_A16B16G16R16F, 1);
- rt_Generic_1.create(r2_RT_generic1, w, h, D3DFMT_A16B16G16R16F, 1);
- rt_Generic.create(r2_RT_generic, w, h, D3DFMT_A16B16G16R16F, 1);
+ rt_Generic_0 = createUnique(r2_RT_generic0, w, h, D3DFMT_A16B16G16R16F, 1);
+ rt_Generic_1 = createUnique(r2_RT_generic1, w, h, D3DFMT_A16B16G16R16F, 1);
+ rt_Generic = createUnique(r2_RT_generic, w, h, D3DFMT_A16B16G16R16F, 1);
} else {
- rt_Generic_0.create(r2_RT_generic0, w, h, D3DFMT_A8R8G8B8, 1);
- rt_Generic_1.create(r2_RT_generic1, w, h, D3DFMT_A8R8G8B8, 1);
- rt_Generic.create(r2_RT_generic, w, h, D3DFMT_A8R8G8B8, 1);
+ rt_Generic_0 = createUnique(r2_RT_generic0, w, h, D3DFMT_A8R8G8B8, 1);
+ rt_Generic_1 = createUnique(r2_RT_generic1, w, h, D3DFMT_A8R8G8B8, 1);
+ rt_Generic = createUnique(r2_RT_generic, w, h, D3DFMT_A8R8G8B8, 1);
}
- rt_fakescope.create(r2_RT_scopert, w, h, D3DFMT_A8R8G8B8, 1); //crookr fakescope
+ rt_fakescope = createUnique(r2_RT_scopert, w, h, D3DFMT_A8R8G8B8, 1); //crookr fakescope
//--DSR-- HeatVision_start
- rt_Heat.create(r2_RT_heat, w, h, D3DFMT_A8R8G8B8, SampleCount);
+ rt_Heat = createUnique(r2_RT_heat, w, h, D3DFMT_A8R8G8B8, SampleCount);
//--DSR-- HeatVision_end
if (RImplementation.o.dx11_hdr10) {
- rt_Generic_temp.create("$user$generic_temp", w, h, D3DFMT_A16B16G16R16F, RImplementation.o.dx10_msaa ? SampleCount : 1);
+ rt_Generic_temp = createUnique("$user$generic_temp", w, h, D3DFMT_A16B16G16R16F, RImplementation.o.dx10_msaa ? SampleCount : 1);
} else {
- rt_Generic_temp.create("$user$generic_temp", w, h, D3DFMT_A8R8G8B8, RImplementation.o.dx10_msaa ? SampleCount : 1);
+ rt_Generic_temp = createUnique("$user$generic_temp", w, h, D3DFMT_A8R8G8B8, RImplementation.o.dx10_msaa ? SampleCount : 1);
}
- rt_dof.create(r2_RT_dof, w, h, RImplementation.o.dx11_hdr10 ? D3DFMT_A16B16G16R16F : D3DFMT_A8R8G8B8);
-
+ rt_dof = createUnique(r2_RT_dof, w, h, RImplementation.o.dx11_hdr10 ? D3DFMT_A16B16G16R16F : D3DFMT_A8R8G8B8);
+
+ // pip rt_secondVP format must match rt_Generic_0/rt_Color so phase_svp_capture's CopyResource
+ // (and the legacy fakescope copy) have matching formats, else D3D rejects the copy
+ // pip the SVP rt_secondVP is the eval OUTPUT, pin it to display-res (svp_height) so a render-
+ // extent SVP upsamples into it, at gate 0 the SVP target already is svp_height so this is a no-op
+ // pip rt_secondVP follows the SVP target (render extent), the DLSS stub copies the scene into it and
+ // the scope lens upscales it. Ascii repoints out_rtv to a display-res UAV target for the real eval
+ u32 svp_out_w = w;
+ u32 svp_out_h = h;
+ // pip DLSS output usually needs a UAV, add it ONLY on the SVP rt_secondVP at gate != 0. a BGRA
+ // (A8R8G8B8) target cannot take a typed UAV on many GPUs, so create it only on the UAV-safe HDR
+ // format, Ascii must pick a UAV-capable SVP output for real DLSS. gate 0 keeps the stock target
+ m_svp_dlss_built = (!isMain && ps_r__svp_dlss != 0);
+ bool svp_uav = (m_svp_dlss_built && rt_Color->fmt == D3DFMT_A16B16G16R16F);
if (RImplementation.o.dx11_hdr10) {
- rt_secondVP.create(r2_RT_secondVP, w, h, D3DFMT_A2R10G10B10, 1); //--#SM+#-- +SecondVP+ // NOTE: this is a hack to use DXGI R10G10B10A2_UNORM
- rt_ui_pda.create(r2_RT_ui, w, h, D3DFMT_A2R10G10B10); // NOTE: this is a hack to use DXGI R10G10B10A2_UNORM
+ rt_secondVP = createUnique(r2_RT_secondVP, svp_out_w, svp_out_h, rt_Color->fmt, 1, svp_uav); //--#SM+#-- +SecondVP+
+ rt_ui_pda = createUnique(r2_RT_ui, w, h, D3DFMT_A2R10G10B10); // NOTE: this is a hack to use DXGI R10G10B10A2_UNORM
} else {
- rt_secondVP.create(r2_RT_secondVP, w, h, D3DFMT_A8R8G8B8, 1); //--#SM+#-- +SecondVP+
- rt_ui_pda.create(r2_RT_ui, w, h, D3DFMT_A8R8G8B8);
+ rt_secondVP = createUnique(r2_RT_secondVP, svp_out_w, svp_out_h, rt_Color->fmt, 1, svp_uav); //--#SM+#-- +SecondVP+
+ rt_ui_pda = createUnique(r2_RT_ui, w, h, D3DFMT_A8R8G8B8);
+ }
+
+ // pip mipped svp color source, feeds the near-blur prefilter so each sparse ring area-averages
+ if (!isMain)
+ {
+ D3D_TEXTURE2D_DESC md;
+ rt_secondVP->pSurface->GetDesc(&md);
+ md.MipLevels = 0;
+ md.BindFlags = D3D_BIND_SHADER_RESOURCE | D3D_BIND_RENDER_TARGET;
+ md.MiscFlags = D3D_RESOURCE_MISC_GENERATE_MIPS;
+ md.CPUAccessFlags = 0;
+ R_CHK(HW.pDevice->CreateTexture2D(&md, 0, &m_svp_nb_mip_surf));
+ m_svp_nb_mip_tex = dxRenderDeviceRender::Instance().Resources->_CreateTexture("$user$svp_nb_mip");
+ m_svp_nb_mip_tex->surface_set(m_svp_nb_mip_surf);
}
// TODO: R11G11B10F? needs another horrible hack + cast + update to converter function
if (RImplementation.o.dx11_hdr10) {
- rt_HDR10_HalfRes[0].create(r4_RT_HDR10_halfres0, w/2, h/2, D3DFMT_A16B16G16R16F);
- rt_HDR10_HalfRes[1].create(r4_RT_HDR10_halfres1, w/2, h/2, D3DFMT_A16B16G16R16F);
+ rt_HDR10_HalfRes[0] = createUnique(r4_RT_HDR10_halfres0, w/2, h/2, D3DFMT_A16B16G16R16F);
+ rt_HDR10_HalfRes[1] = createUnique(r4_RT_HDR10_halfres1, w/2, h/2, D3DFMT_A16B16G16R16F);
}
// PDA, probably not ideal though
// RT - KD
- rt_sunshafts_0.create(r2_RT_sunshafts0, w, h, D3DFMT_A8R8G8B8);
- rt_sunshafts_1.create(r2_RT_sunshafts1, w, h, D3DFMT_A8R8G8B8);
+ rt_sunshafts_0 = createUnique(r2_RT_sunshafts0, w, h, D3DFMT_A8R8G8B8);
+ rt_sunshafts_1 = createUnique(r2_RT_sunshafts1, w, h, D3DFMT_A8R8G8B8);
// RT Blur
- rt_blur_h_2.create(r2_RT_blur_h_2, u32(w/2), u32(h/2), D3DFMT_A8R8G8B8);
- rt_blur_2.create(r2_RT_blur_2, u32(w/2), u32(h/2), D3DFMT_A8R8G8B8);
+ rt_blur_h_2 = createUnique(r2_RT_blur_h_2, u32(w/2), u32(h/2), D3DFMT_A8R8G8B8);
+ rt_blur_2 = createUnique(r2_RT_blur_2, u32(w/2), u32(h/2), D3DFMT_A8R8G8B8);
- rt_blur_h_4.create(r2_RT_blur_h_4, u32(w/4), u32(h/4), D3DFMT_A8R8G8B8);
- rt_blur_4.create(r2_RT_blur_4, u32(w/4), u32(h/4), D3DFMT_A8R8G8B8);
+ rt_blur_h_4 = createUnique(r2_RT_blur_h_4, u32(w/4), u32(h/4), D3DFMT_A8R8G8B8);
+ rt_blur_4 = createUnique(r2_RT_blur_4, u32(w/4), u32(h/4), D3DFMT_A8R8G8B8);
- rt_blur_h_8.create(r2_RT_blur_h_8, u32(w/8), u32(h/8), D3DFMT_A8R8G8B8);
- rt_blur_8.create(r2_RT_blur_8, u32(w/8), u32(h/8), D3DFMT_A8R8G8B8);
+ rt_blur_h_8 = createUnique(r2_RT_blur_h_8, u32(w/8), u32(h/8), D3DFMT_A8R8G8B8);
+ rt_blur_8 = createUnique(r2_RT_blur_8, u32(w/8), u32(h/8), D3DFMT_A8R8G8B8);
- rt_pp_bloom.create(r2_RT_pp_bloom, w, h, D3DFMT_A8R8G8B8);
+ rt_pp_bloom = createUnique(r2_RT_pp_bloom, w, h, D3DFMT_A8R8G8B8);
// Screen Space Shaders Stuff
- rt_ssfx_taa.create(r2_RT_ssfx_taa, w, h, D3DFMT_A16B16G16R16F, SampleCount); // Temp RT
+ rt_ssfx_taa = createUnique(r2_RT_ssfx_taa, w, h, D3DFMT_A16B16G16R16F, SampleCount); // Temp RT
if (RImplementation.o.dx11_hdr10)
- rt_ssfx_prev_frame.create(r2_RT_ssfx_prev_frame, w, h, D3DFMT_A16B16G16R16F); // Temp RT
+ rt_ssfx_prev_frame = createUnique(r2_RT_ssfx_prev_frame, w, h, D3DFMT_A16B16G16R16F); // Temp RT
else
- rt_ssfx_prev_frame.create(r2_RT_ssfx_prev_frame, w, h, D3DFMT_A8R8G8B8); // Temp RT
+ rt_ssfx_prev_frame = createUnique(r2_RT_ssfx_prev_frame, w, h, D3DFMT_A8R8G8B8); // Temp RT
- rt_ssfx_motion_vectors.create(r2_RT_ssfx_motion_vectors, w, h, D3DFMT_A16B16G16R16F, SampleCount); // HUD mask & Velocity buffer
+ rt_ssfx_motion_vectors = createUnique(r2_RT_ssfx_motion_vectors, w, h, D3DFMT_A16B16G16R16F, SampleCount); // HUD mask & Velocity buffer
- rt_ssfx.create(r2_RT_ssfx, w, h, D3DFMT_A8R8G8B8); // Temp RT
- rt_ssfx_temp.create(r2_RT_ssfx_temp, w, h, D3DFMT_A8R8G8B8); // Temp RT
- rt_ssfx_temp2.create(r2_RT_ssfx_temp2, w, h, D3DFMT_A8R8G8B8); // Temp RT
- rt_ssfx_temp3.create(r2_RT_ssfx_temp3, w, h, D3DFMT_A8R8G8B8); // Temp RT
+ rt_ssfx = createUnique(r2_RT_ssfx, w, h, D3DFMT_A8R8G8B8); // Temp RT
+ rt_ssfx_temp = createUnique(r2_RT_ssfx_temp, w, h, D3DFMT_A8R8G8B8); // Temp RT
+ rt_ssfx_temp2 = createUnique(r2_RT_ssfx_temp2, w, h, D3DFMT_A8R8G8B8); // Temp RT
+ rt_ssfx_temp3 = createUnique(r2_RT_ssfx_temp3, w, h, D3DFMT_A8R8G8B8); // Temp RT
- rt_ssfx_accum.create(r2_RT_ssfx_accum, w, h, D3DFMT_A16B16G16R16F, SampleCount); // Volumetric Acc
- rt_ssfx_ssr.create(r2_RT_ssfx_ssr, w, h, D3DFMT_A8R8G8B8); // SSR Acc
- rt_ssfx_water.create(r2_RT_ssfx_water, w, h, D3DFMT_A8R8G8B8); // Water Acc
- rt_ssfx_ao.create(r2_RT_ssfx_ao, w, h, D3DFMT_A8R8G8B8); // AO Acc
- rt_ssfx_il.create(r2_RT_ssfx_il, w, h, D3DFMT_A8R8G8B8); // IL Acc
+ rt_ssfx_accum = createUnique(r2_RT_ssfx_accum, w, h, D3DFMT_A16B16G16R16F, SampleCount); // Volumetric Acc
+ rt_ssfx_ssr = createUnique(r2_RT_ssfx_ssr, w, h, D3DFMT_A8R8G8B8); // SSR Acc
+ rt_ssfx_water = createUnique(r2_RT_ssfx_water, w, h, D3DFMT_A8R8G8B8); // Water Acc
+ rt_ssfx_ao = createUnique(r2_RT_ssfx_ao, w, h, D3DFMT_A8R8G8B8); // AO Acc
+ rt_ssfx_il = createUnique(r2_RT_ssfx_il, w, h, D3DFMT_A8R8G8B8); // IL Acc
if (RImplementation.o.ssfx_sss)
{
- rt_ssfx_sss.create(r2_RT_ssfx_sss, w, h, D3DFMT_A8R8G8B8); // SSS Acc
- rt_ssfx_sss_ext.create(r2_RT_ssfx_sss_ext, w, h, D3DFMT_A8R8G8B8); // SSS EXT Acc
- rt_ssfx_sss_ext2.create(r2_RT_ssfx_sss_ext2, w, h, D3DFMT_A8R8G8B8); // SSS EXT Acc
- rt_ssfx_sss_tmp.create(r2_RT_ssfx_sss_tmp, w, h, D3DFMT_A8R8G8B8); // SSS EXT Acc
+ rt_ssfx_sss = createUnique(r2_RT_ssfx_sss, w, h, D3DFMT_A8R8G8B8); // SSS Acc
+ rt_ssfx_sss_ext = createUnique(r2_RT_ssfx_sss_ext, w, h, D3DFMT_A8R8G8B8); // SSS EXT Acc
+ rt_ssfx_sss_ext2 = createUnique(r2_RT_ssfx_sss_ext2, w, h, D3DFMT_A8R8G8B8); // SSS EXT Acc
+ rt_ssfx_sss_tmp = createUnique(r2_RT_ssfx_sss_tmp, w, h, D3DFMT_A8R8G8B8); // SSS EXT Acc
}
if (RImplementation.o.ssfx_bloom)
{
- rt_ssfx_bloom1.create(r2_RT_ssfx_bloom1, w / 2.0f, h / 2.0f, D3DFMT_A16B16G16R16F); // Bloom
- rt_ssfx_bloom_emissive.create(r2_RT_ssfx_bloom_emissive, w, h, D3DFMT_A8R8G8B8, SampleCount); // Emissive
- rt_ssfx_bloom_lens.create(r2_RT_ssfx_bloom_lens, w / 4.0f, h / 4.0f, D3DFMT_A8R8G8B8); // Lens
-
- rt_ssfx_bloom_tmp2.create(r2_RT_ssfx_bloom_tmp2, w / 2.0f, h / 2.0f, D3DFMT_A16B16G16R16F); // Bloom / 2
- rt_ssfx_bloom_tmp4.create(r2_RT_ssfx_bloom_tmp4, w / 4.0f, h / 4.0f, D3DFMT_A16B16G16R16F); // Bloom / 4
- rt_ssfx_bloom_tmp8.create(r2_RT_ssfx_bloom_tmp8, w / 8.0f, h / 8.0f, D3DFMT_A16B16G16R16F); // Bloom / 8
- rt_ssfx_bloom_tmp16.create(r2_RT_ssfx_bloom_tmp16, w / 16.0f, h / 16.0f, D3DFMT_A16B16G16R16F); // Bloom / 16
- rt_ssfx_bloom_tmp32.create(r2_RT_ssfx_bloom_tmp32, w / 32.0f, h / 32.0f, D3DFMT_A16B16G16R16F); // Bloom / 32
- rt_ssfx_bloom_tmp64.create(r2_RT_ssfx_bloom_tmp64, w / 64.0f, h / 64.0f, D3DFMT_A16B16G16R16F); // Bloom / 64
-
- rt_ssfx_bloom_tmp32_2.create(r2_RT_ssfx_bloom_tmp32_2, w / 32.0f, h / 32.0f, D3DFMT_A16B16G16R16F); // Bloom / 32
- rt_ssfx_bloom_tmp16_2.create(r2_RT_ssfx_bloom_tmp16_2, w / 16.0f, h / 16.0f, D3DFMT_A16B16G16R16F); // Bloom / 16
- rt_ssfx_bloom_tmp8_2.create(r2_RT_ssfx_bloom_tmp8_2, w / 8.0f, h / 8.0f, D3DFMT_A16B16G16R16F); // Bloom / 8
- rt_ssfx_bloom_tmp4_2.create(r2_RT_ssfx_bloom_tmp4_2, w / 4.0f, h / 4.0f, D3DFMT_A16B16G16R16F); // Bloom / 4
+ rt_ssfx_bloom1 = createUnique(r2_RT_ssfx_bloom1, w / 2.0f, h / 2.0f, D3DFMT_A16B16G16R16F); // Bloom
+ rt_ssfx_bloom_emissive = createUnique(r2_RT_ssfx_bloom_emissive, w, h, D3DFMT_A8R8G8B8, SampleCount); // Emissive
+ rt_ssfx_bloom_lens = createUnique(r2_RT_ssfx_bloom_lens, w / 4.0f, h / 4.0f, D3DFMT_A8R8G8B8); // Lens
+
+ rt_ssfx_bloom_tmp2 = createUnique(r2_RT_ssfx_bloom_tmp2, w / 2.0f, h / 2.0f, D3DFMT_A16B16G16R16F); // Bloom / 2
+ rt_ssfx_bloom_tmp4 = createUnique(r2_RT_ssfx_bloom_tmp4, w / 4.0f, h / 4.0f, D3DFMT_A16B16G16R16F); // Bloom / 4
+ rt_ssfx_bloom_tmp8 = createUnique(r2_RT_ssfx_bloom_tmp8, w / 8.0f, h / 8.0f, D3DFMT_A16B16G16R16F); // Bloom / 8
+ rt_ssfx_bloom_tmp16 = createUnique(r2_RT_ssfx_bloom_tmp16, w / 16.0f, h / 16.0f, D3DFMT_A16B16G16R16F); // Bloom / 16
+ rt_ssfx_bloom_tmp32 = createUnique(r2_RT_ssfx_bloom_tmp32, w / 32.0f, h / 32.0f, D3DFMT_A16B16G16R16F); // Bloom / 32
+ rt_ssfx_bloom_tmp64 = createUnique(r2_RT_ssfx_bloom_tmp64, w / 64.0f, h / 64.0f, D3DFMT_A16B16G16R16F); // Bloom / 64
+
+ rt_ssfx_bloom_tmp32_2 = createUnique(r2_RT_ssfx_bloom_tmp32_2, w / 32.0f, h / 32.0f, D3DFMT_A16B16G16R16F); // Bloom / 32
+ rt_ssfx_bloom_tmp16_2 = createUnique(r2_RT_ssfx_bloom_tmp16_2, w / 16.0f, h / 16.0f, D3DFMT_A16B16G16R16F); // Bloom / 16
+ rt_ssfx_bloom_tmp8_2 = createUnique(r2_RT_ssfx_bloom_tmp8_2, w / 8.0f, h / 8.0f, D3DFMT_A16B16G16R16F); // Bloom / 8
+ rt_ssfx_bloom_tmp4_2 = createUnique(r2_RT_ssfx_bloom_tmp4_2, w / 4.0f, h / 4.0f, D3DFMT_A16B16G16R16F); // Bloom / 4
}
- rt_ssfx_volumetric.create(r2_RT_ssfx_volumetric, w / 8.0f, h / 8.0f, D3DFMT_A16B16G16R16F); // Volumetric
- rt_ssfx_volumetric_tmp.create(r2_RT_ssfx_volumetric_tmp, w / 8.0f, h / 8.0f, D3DFMT_A16B16G16R16F); // Volumetric
- rt_ssfx_rain.create(r2_RT_ssfx_rain, w / 8.0f, h / 8.0f, D3DFMT_A8R8G8B8); // Rain refraction buffer
- rt_ssfx_water_waves.create(r2_RT_ssfx_water_waves, 512, 512, D3DFMT_A8R8G8B8); // Water Waves
+ rt_ssfx_volumetric = createUnique(r2_RT_ssfx_volumetric, w / 8.0f, h / 8.0f, D3DFMT_A16B16G16R16F); // Volumetric
+ rt_ssfx_volumetric_tmp = createUnique(r2_RT_ssfx_volumetric_tmp, w / 8.0f, h / 8.0f, D3DFMT_A16B16G16R16F); // Volumetric
+ rt_ssfx_rain = createUnique(r2_RT_ssfx_rain, w / 8.0f, h / 8.0f, D3DFMT_A8R8G8B8); // Rain refraction buffer
+ rt_ssfx_water_waves = createUnique(r2_RT_ssfx_water_waves, 512, 512, D3DFMT_A8R8G8B8); // Water Waves
- rt_ssfx_prevPos.create(r2_RT_ssfx_prevPos, w, h, D3DFMT_A16B16G16R16F, SampleCount);
+ rt_ssfx_prevPos = createUnique(r2_RT_ssfx_prevPos, w, h, D3DFMT_A16B16G16R16F, SampleCount);
- //rt_ssfx_hud.create(r2_RT_ssfx_hud, w, h, D3DFMT_A16B16G16R16F); // Deprecated
+ //rt_ssfx_hud = createUnique(r2_RT_ssfx_hud, w, h, D3DFMT_A16B16G16R16F); // Deprecated
if (RImplementation.o.dx10_msaa)
{
- rt_Generic_0_r.create(r2_RT_generic0_r, w, h, ps_r4_hdr10_on ? D3DFMT_A16B16G16R16F : D3DFMT_A8R8G8B8, SampleCount);
- rt_Generic_1_r.create(r2_RT_generic1_r, w, h, ps_r4_hdr10_on ? D3DFMT_A16B16G16R16F : D3DFMT_A8R8G8B8, SampleCount);
+ rt_Generic_0_r = createUnique(r2_RT_generic0_r, w, h, ps_r4_hdr10_on ? D3DFMT_A16B16G16R16F : D3DFMT_A8R8G8B8, SampleCount);
+ rt_Generic_1_r = createUnique(r2_RT_generic1_r, w, h, ps_r4_hdr10_on ? D3DFMT_A16B16G16R16F : D3DFMT_A8R8G8B8, SampleCount);
//rt_Generic.create (r2_RT_generic,w,h, D3DFMT_A8R8G8B8, 1 );
}
// Igor: for volumetric lights
//rt_Generic_2.create (r2_RT_generic2,w,h,D3DFMT_A8R8G8B8 );
// temp: for higher quality blends
if (RImplementation.o.advancedpp)
- rt_Generic_2.create(r2_RT_generic2, w, h, D3DFMT_A16B16G16R16F, SampleCount);
+ rt_Generic_2 = createUnique(r2_RT_generic2, w, h, D3DFMT_A16B16G16R16F, SampleCount);
}
s_hdr10_bloom_downsample.create(b_hdr10_bloom_downsample, "hdr10_bloom_downsample");
@@ -713,11 +841,11 @@ CRenderTarget::CRenderTarget()
if (RImplementation.o.nullrt) nullrt = (D3DFORMAT)MAKEFOURCC('N', 'U', 'L', 'L');
u32 size = RImplementation.o.smapsize;
- rt_smap_depth.create(r2_RT_smap_depth, size, size, depth_format);
+ rt_smap_depth = createUnique(r2_RT_smap_depth, size, size, depth_format);
if (RImplementation.o.dx10_minmax_sm)
{
- rt_smap_depth_minmax.create(r2_RT_smap_depth_minmax, size / 4, size / 4, D3DFMT_R32F);
+ rt_smap_depth_minmax = createUnique(r2_RT_smap_depth_minmax, size / 4, size / 4, D3DFMT_R32F);
CBlender_createminmax TempBlender;
s_create_minmax_sm.create(&TempBlender, "null");
}
@@ -873,8 +1001,8 @@ CRenderTarget::CRenderTarget()
u32 fvf_filter = (u32)D3DFVF_XYZRHW | D3DFVF_TEX8 | D3DFVF_TEXCOORDSIZE4(0) | D3DFVF_TEXCOORDSIZE4(1) |
D3DFVF_TEXCOORDSIZE4(2) | D3DFVF_TEXCOORDSIZE4(3) | D3DFVF_TEXCOORDSIZE4(4) | D3DFVF_TEXCOORDSIZE4(5) |
D3DFVF_TEXCOORDSIZE4(6) | D3DFVF_TEXCOORDSIZE4(7);
- rt_Bloom_1.create(r2_RT_bloom1, w, h, fmt);
- rt_Bloom_2.create(r2_RT_bloom2, w, h, fmt);
+ rt_Bloom_1 = createUnique(r2_RT_bloom1, w, h, fmt);
+ rt_Bloom_2 = createUnique(r2_RT_bloom2, w, h, fmt);
g_bloom_build.create(fvf_build, RCache.Vertex.Buffer(), RCache.QuadIB);
g_bloom_filter.create(fvf_filter, RCache.Vertex.Buffer(), RCache.QuadIB);
s_bloom_dbg_1.create("effects\\screen_set", r2_RT_bloom1);
@@ -893,16 +1021,16 @@ CRenderTarget::CRenderTarget()
u32 w = Device.dwWidth;
u32 h = Device.dwHeight;
- rt_smaa_edgetex.create(r2_RT_smaa_edgetex, w, h, D3DFMT_A8R8G8B8);
- rt_smaa_blendtex.create(r2_RT_smaa_blendtex, w, h, D3DFMT_A8R8G8B8);
+ rt_smaa_edgetex = createUnique(r2_RT_smaa_edgetex, w, h, D3DFMT_A8R8G8B8);
+ rt_smaa_blendtex = createUnique(r2_RT_smaa_blendtex, w, h, D3DFMT_A8R8G8B8);
s_smaa.create(b_smaa, "r3\\smaa");
}
// TONEMAP
{
- rt_LUM_64.create(r2_RT_luminance_t64, 64, 64, D3DFMT_A16B16G16R16F);
- rt_LUM_8.create(r2_RT_luminance_t8, 8, 8, D3DFMT_A16B16G16R16F);
+ rt_LUM_64 = createUnique(r2_RT_luminance_t64, 64, 64, D3DFMT_A16B16G16R16F);
+ rt_LUM_8 = createUnique(r2_RT_luminance_t8, 8, 8, D3DFMT_A16B16G16R16F);
s_luminance.create(b_luminance, "r2\\luminance");
f_luminance_adapt = 0.5f;
@@ -913,7 +1041,7 @@ CRenderTarget::CRenderTarget()
for (u32 it = 0; it < 2; it++)
{
shared_str name; name.printf("%s_%d", r2_RT_luminance_pool, it);
- rt_LUM_pool[it].create(name.c_str(), 1, 1, D3DFMT_R32F);
+ rt_LUM_pool[it] = createUnique(name.c_str(), 1, 1, D3DFMT_R32F);
FLOAT ColorRGBA[4] = { 127.0f / 255.0f, 127.0f / 255.0f, 127.0f / 255.0f, 127.0f / 255.0f };
HW.pContext->ClearRenderTargetView(rt_LUM_pool[it]->pRT, ColorRGBA);
@@ -938,7 +1066,7 @@ CRenderTarget::CRenderTarget()
}
D3DFORMAT fmt = HW.Caps.id_vendor == 0x10DE ? D3DFMT_R32F : D3DFMT_R16F;
- rt_half_depth.create(r2_RT_half_depth, w, h, fmt);
+ rt_half_depth = createUnique(r2_RT_half_depth, w, h, fmt);
s_ssao.create(b_ssao, "r2\\ssao");
}
@@ -964,7 +1092,7 @@ CRenderTarget::CRenderTarget()
if (RImplementation.o.ssao_hdao && RImplementation.o.ssao_ultra)
{
u32 w = Device.dwWidth, h = Device.dwHeight;
- rt_ssao_temp.create(r2_RT_ssao_temp, w, h, D3DFMT_R16F, 1, true);
+ rt_ssao_temp = createUnique(r2_RT_ssao_temp, w, h, D3DFMT_R16F, 1, true);
s_hdao_cs.create(b_hdao_cs, "r2\\ssao");
if (RImplementation.o.dx10_msaa)
{
@@ -1004,6 +1132,7 @@ CRenderTarget::CRenderTarget()
t_envmap_0.create(r2_T_envs0);
t_envmap_1.create(r2_T_envs1);
}
+ t_reticle.create("$user$reticle"); // pip per-lens reticle texture for the scope color shader
// Build textures
{
diff --git a/src/Layers/xrRenderPC_R4/r4_rendertarget.h b/src/Layers/xrRenderPC_R4/r4_rendertarget.h
index 2b56a81308..a4cb30eb1d 100644
--- a/src/Layers/xrRenderPC_R4/r4_rendertarget.h
+++ b/src/Layers/xrRenderPC_R4/r4_rendertarget.h
@@ -1,9 +1,15 @@
#pragma once
+#include // pip draw_scope phase bind callback
#include "../xrRender/ColorMapManager.h"
#include "../xrRender/light_db.h"
+#include "SvpDlss.h" // pip SVP DLSS-SR input contract
class light;
+// pip SVP scene render extent for the color/depth/MV targets, full display-res (svp_height) at gate 0
+// so r__svp_render_scale is inert until the DLSS scaffolding is active (r__svp_dlss != 0)
+u32 svp_render_extent();
+
//#define DU_SPHERE_NUMVERTEX 92
//#define DU_SPHERE_NUMFACES 180
//#define DU_CONE_NUMVERTEX 18
@@ -101,6 +107,14 @@ class CRenderTarget : public IRender_Target
xr_vector dbg_planes;
#endif
+ // PiP base RT/ZB, main = HW.secret_pBaseRT, SVP renders into its own rt_baseRT/baseZB
+ ID3D11RenderTargetView* baseRT;
+ ID3D11DepthStencilView* baseZB;
+ ref_rt rt_baseRT;
+ ref_rt rt_baseZB;
+ // SVP RTs are created $svp-suffixed and remapped over the stock names in SetActive
+ xr_vector> RenderTargetRemaps;
+
// MRT-path
ref_rt rt_Depth; // Z-buffer like - initial depth
ref_rt rt_MSAADepth; // z-buffer for MSAA deferred shading
@@ -125,6 +139,8 @@ class CRenderTarget : public IRender_Target
resptr_core rt_Generic_temp;
ref_rt rt_secondVP; // 32bit (r,g,b,a) --//#SM+#-- +SecondVP+
+ ID3DTexture2D* m_svp_nb_mip_surf = nullptr; // pip mip-prefiltered svp color for near-blur
+ ref_texture m_svp_nb_mip_tex; // pip holds the all-mip srv for GenerateMips
ref_rt rt_fakescope; // crookr fakescope
@@ -163,6 +179,7 @@ class CRenderTarget : public IRender_Target
// env
ref_texture t_envmap_0; // env-0
ref_texture t_envmap_1; // env-1
+ ref_texture t_reticle; // pip per-lens reticle art bound for the scope color shader ($user$reticle)
// smap
ref_rt rt_smap_surf; // 32bit, color
@@ -229,6 +246,21 @@ class CRenderTarget : public IRender_Target
ref_shader s_ssfx_dumb;
+ // pip scope lens glue shaders, created lazily (never at init) so a setup without the PiP mod is unaffected
+ ref_shader s_scope_color_write;
+ ref_shader s_scope_depth_write;
+ ref_shader s_scope_debug; // pip r__scope_debug on-screen multi-view inspector overlay
+ ref_shader s_scope_lensfx; // pip r__svp_lensfx additive lens FX (CA, distortion, exit-pupil dimming)
+ ref_shader s_svp_nearblur; // pip near-field defocus on the scope image (svpscope 2)
+ ref_shader s_svp_distort_stamp; // pip neutral distort-mask stamp over the composited lens
+ ref_shader s_svp_taa_stamp; // pip taa alpha stamp over the composited lens so the main resolve skips it
+ bool m_scope_shaders_ready = false;
+ // taa history seeds from the current frame on the next resolve, true at creation and on scope edges
+ bool m_taa_seed_history = true;
+ bool m_svp_dlss_built = false; // pip this SVP target was built with the DLSS gate on (toggle-recreate key)
+ void EnsureScopeShaders();
+ void phase_scope_debug(); // pip draw the scope debug overlay (main+SVP views, ssfx, smap), main only
+
// Igor: for async screenshots
ID3DTexture2D* t_ss_async; //32bit (r,g,b,a) is situated in the system memory
@@ -391,8 +423,14 @@ class CRenderTarget : public IRender_Target
bool m_bHasActiveVolumetric;
bool m_bHasActiveVolumetric_spot;
public:
+ // dims at creation, the SVP target is square so svpCamera reads these for its aspect
+ const u32 Width;
+ const u32 Height;
+
CRenderTarget();
+ CRenderTarget(LPCSTR name, u32 width, u32 height);
~CRenderTarget();
+ void SetActive(bool force = false);
void accum_point_geom_create();
void accum_point_geom_destroy();
void accum_omnip_geom_create();
@@ -427,6 +465,12 @@ class CRenderTarget : public IRender_Target
void phase_fakescope(); //crookr
void phase_heatvision(); //--DSR-- HeatVision
void phase_3DSSReticle(); // Redotix99: for 3D Shader Based Scopes
+ void phase_svp_capture(); // pip copy the SVP combined color into rt_secondVP for the lens to sample
+ bool svp_nearblur_pass(); // pip near-field defocus dispatch, true when it ran else the caller copies
+ void draw_scope(ref_shader se, std::function bind); // pip render the scope lens meshes forcing se per phase
+ void EvalSVP_DLSS(const SvpDlssInputs& in); // pip DLSS-SR eval, bilinear-passthrough stub for now (Task 7)
+ void draw_reflex(bool svp = false); // pip render reflex-sight lenses (mapReflexHUDSorted), svp draws them through the entrance-pupil camera
+ bool draw_reflex_proxy(); // pip collimated reflex proxy drawn into rt_secondVP after capture, returns true only on a proven draw
void phase_lut();
void phase_smaa();
void phase_scene_prepare();
@@ -474,6 +518,7 @@ class CRenderTarget : public IRender_Target
void draw_rain(light& RainSetup);
void mark_msaa_edges();
+ void stamp_svp_corner_mask(); // pip zero the geometry stencil in the dead disc corners so the svp lighting skips them
bool need_to_render_sunshafts();
bool use_minmax_sm_this_frame();
diff --git a/src/Layers/xrRenderPC_R4/r4_rendertarget_phase_combine.cpp b/src/Layers/xrRenderPC_R4/r4_rendertarget_phase_combine.cpp
index 17346a0f5a..0753ed3549 100644
--- a/src/Layers/xrRenderPC_R4/r4_rendertarget_phase_combine.cpp
+++ b/src/Layers/xrRenderPC_R4/r4_rendertarget_phase_combine.cpp
@@ -51,20 +51,10 @@ void CRenderTarget::phase_combine()
Fvector2 p0, p1;
//*** exposure-pipeline
- if (Device.m_SecondViewport.IsSVPActive()) //--#SM+#-- +SecondVP+ Fix for screen flickering
- {
- if (t_LUM_src != rt_LUM_pool[0]->pTexture)
- t_LUM_src->surface_set(rt_LUM_pool[0]->pSurface);
- if (t_LUM_dest != rt_LUM_pool[1]->pTexture)
- t_LUM_dest->surface_set(rt_LUM_pool[1]->pSurface);
- }
- else
- {
- if (t_LUM_src != rt_LUM_pool[0]->pTexture)
- t_LUM_src->surface_set(rt_LUM_pool[0]->pSurface);
- if (t_LUM_dest != rt_LUM_pool[1]->pTexture)
- t_LUM_dest->surface_set(rt_LUM_pool[1]->pSurface);
- }
+ if (t_LUM_src != rt_LUM_pool[0]->pTexture)
+ t_LUM_src->surface_set(rt_LUM_pool[0]->pSurface);
+ if (t_LUM_dest != rt_LUM_pool[1]->pTexture)
+ t_LUM_dest->surface_set(rt_LUM_pool[1]->pSurface);
if (RImplementation.o.ssao_hdao && RImplementation.o.ssao_ultra)
{
@@ -105,8 +95,9 @@ void CRenderTarget::phase_combine()
}
{
- // Disable when rendering SecondViewport
- if (!Device.m_SecondViewport.IsSVPFrame())
+ // pip AO + IL must run for the true-PiP SVP or its shadows go near-black (no ambient/indirect
+ // fill), the legacy fake-SVP frame still skips them (stock !IsSVPFrame) so off is unchanged
+ if (Device.true_pip_on || !Device.m_SecondViewport.IsSVPFrame())
{
// Clear RT
FLOAT ColorRGBA[4] = { 0.0f, 0.0f, 0.0f, 1.0f };
@@ -154,12 +145,25 @@ void CRenderTarget::phase_combine()
//RCache.set_ColorWriteEnable ();
// Moved to shader!
//RCache.set_Z(FALSE);
+
+ // nvg clouds prescale by s_tonemap in the vs and the svp meter mismeasures packed channels,
+ // pin the svp sky pass to the main view adaptation so the scope clouds match the eye view
+ extern Fvector4 ps_dev_param_8;
+ const bool svp_nvg_sky = Device.true_pip_on && Device.m_SecondViewport.m_render_pass_is_svp
+ && ps_dev_param_8.x >= 1.f && RImplementation.TargetMain && this != RImplementation.TargetMain;
+ if (svp_nvg_sky) { if (ps_r__svp_stats) ++svp_stats_nvg_sky; svp_ledger_nvg_sky = 1; } // overlay + ledger proof the nvg sky lum remap fired
+ if (svp_nvg_sky)
+ t_LUM_dest->surface_set(RImplementation.TargetMain->rt_LUM_pool[0]->pSurface);
+
g_pGamePersistent->Environment().RenderSky();
// Igor: Render clouds before compine without Z-test
// to avoid siluets. HOwever, it's a bit slower process.
g_pGamePersistent->Environment().RenderClouds();
+ if (svp_nvg_sky)
+ t_LUM_dest->surface_set(rt_LUM_pool[1]->pSurface);
+
// Moved to shader!
//RCache.set_Z(TRUE);
}
@@ -334,14 +338,18 @@ void CRenderTarget::phase_combine()
else
HW.pContext->CopyResource(rt_Generic_temp->pTexture->surface_get(), rt_Generic_0_r->pTexture->surface_get());
- if (RImplementation.o.ssfx_ssr && !Device.m_SecondViewport.IsSVPFrame())
+ // pip the SVP runs SSR + water like the main view, off and legacy fake SVP keep the stock skip
+ const bool svp_pass = Device.true_pip_on && Device.m_SecondViewport.m_render_pass_is_svp;
+ // pip the deferred SSR runs on the scope at levels 0/1 (reflective surfaces), skipped at 2 (matte)
+ if (RImplementation.o.ssfx_ssr && ((svp_pass && ps_r__svp_skip_ssr < 2) || !Device.m_SecondViewport.IsSVPFrame()))
{
ssfx_PrevPos_Requiered = true;
phase_ssfx_ssr(); // [SSFX] - New SSR Phase
}
- // [SSFX] - Water SSR rendering
- if (RImplementation.o.ssfx_water && !Device.m_SecondViewport.IsSVPFrame())
+ // pip water SSR only at level 0 (the reflective water below needs it, the SSS shader discards it
+ // otherwise), always on the main
+ if (RImplementation.o.ssfx_water && ((svp_pass && ps_r__svp_skip_ssr == 0) || !Device.m_SecondViewport.IsSVPFrame()))
{
FLOAT ColorRGBA[4] = { 0.0f, 0.0f, 0.0f, 1.0f };
HW.pContext->ClearRenderTargetView(rt_ssfx_temp->pRT, ColorRGBA);
@@ -381,8 +389,12 @@ void CRenderTarget::phase_combine()
u_setrt(rt_Generic_0_r, 0, 0, rt_MSAADepth->pZRT);
// Final water rendering ( All the code above can be omitted if the Water module isn't installed )
+ // pip the SSS water shader flattens the scope water (ssfx_issvp), force_water_reflect turns the
+ // reflection back on for the SVP draw at level 0, do not clear the shared mapWater (main pass needs it)
+ Device.m_SecondViewport.force_water_reflect = svp_pass && (ps_r__svp_skip_ssr == 0);
RCache.set_xform_world(Fidentity);
- RImplementation.GMBase.r_dsgraph_render_water();
+ RImplementation.GMBase.r_dsgraph_render_water(!svp_pass);
+ Device.m_SecondViewport.force_water_reflect = false;
{
if (RImplementation.o.ssfx_rain)
@@ -432,15 +444,19 @@ void CRenderTarget::phase_combine()
// Igor: for volumetric lights
// combine light volume here
- if (RImplementation.o.ssfx_volumetric)
+ // pip r__svp_skip_volumetric drops god rays on the scope pass (subtle at magnification)
+ if (!(svp_pass && ps_r__svp_skip_volumetric))
{
- if (m_bHasActiveVolumetric || m_bHasActiveVolumetric_spot)
- phase_combine_volumetric();
- }
- else
- {
- if (m_bHasActiveVolumetric)
- phase_combine_volumetric();
+ if (RImplementation.o.ssfx_volumetric)
+ {
+ if (m_bHasActiveVolumetric || m_bHasActiveVolumetric_spot)
+ phase_combine_volumetric();
+ }
+ else
+ {
+ if (m_bHasActiveVolumetric)
+ phase_combine_volumetric();
+ }
}
// Perform blooming filter and distortion if needed
@@ -492,6 +508,16 @@ void CRenderTarget::phase_combine()
//CHK_DX(HW.pDevice->Clear ( 0L, NULL, D3DCLEAR_TARGET, color_rgba(127,127,0,127), 1.0f, 0L));
RImplementation.GMBase.r_dsgraph_render_distort();
if (g_pGamePersistent) g_pGamePersistent->OnRenderPPUI_PP(); // PP-UI
+ // pip the composited lens must not warp, stamp the mask neutral over its footprint
+ if (ps_r__svp_distort_guard && Device.true_pip_on && this == RImplementation.TargetMain
+ && Device.m_SecondViewport.IsSVPActive()
+ && !RImplementation.GMBase.RGraph.mapScopeHUDSorted.empty())
+ {
+ if (ps_r__svp_stats) ++svp_stats_distort_guard; // overlay proof the distort guard stamped
+ svp_ledger_distort_guard = 1;
+ EnsureScopeShaders();
+ draw_scope(s_svp_distort_stamp, []() { RCache.set_c("scope_phase", 0); });
+ }
}
}
@@ -519,24 +545,48 @@ void CRenderTarget::phase_combine()
phase_ssfx_fog_scattering();
}
- if (RImplementation.o.ssfx_motionblur && ps_ssfx_motionblur.y > 0)
+ // pip with DLSS on, skip the SVP engine AA/post so rt_Generic_0$svp stays aliased + jittered for the
+ // eval (DLSS does its own reconstruction), main + off + gate-0 keep the stock AA path unchanged
+ const bool svp_dlss_skip_aa = (ps_r__svp_dlss != 0 && Device.true_pip_on && Device.m_SecondViewport.m_render_pass_is_svp);
+
+ // pip the SVP runs TAA before motion blur + the lens paint so it reprojects with the SVP's own
+ // motion vectors, the main view keeps its TAA at the end (below)
+ if (RImplementation.o.ssfx_taa && ps_ssfx_taa.x > 0 &&
+ Device.true_pip_on && Device.m_SecondViewport.m_render_pass_is_svp && !svp_dlss_skip_aa)
+ {
+ phase_ssfx_taa();
+ }
+
+ // pip r__svp_skip_motionblur drops motion blur on the scope pass, magnified blur is an artifact
+ if (RImplementation.o.ssfx_motionblur && ps_ssfx_motionblur.y > 0 && !svp_dlss_skip_aa
+ && !(svp_pass && ps_r__svp_skip_motionblur))
{
phase_ssfx_motion_blur();
}
- if (scope_3D_fake_enabled)
+ // pip composite the lens once on the MAIN view only, whenever a 3DSS scope is aimed under true_pip
+ // (magnified or a 1x reflex/eyepiece was captured), true_pip off keeps the stock scope_3D_fake_enabled gate
+ if (this == RImplementation.TargetMain
+ && (scope_3D_fake_enabled
+ || (Device.true_pip_on
+ && (Device.m_SecondViewport.IsSVPActive()
+ || !RImplementation.GMBase.RGraph.mapReflexHUDSorted.empty()
+ || !RImplementation.GMBase.RGraph.mapScopeHUDSorted.empty()))))
{
- phase_3DSSReticle(); // Redotix99: for 3D Shader Based Scopes
+ phase_3DSSReticle(); // Redotix99 3D Shader Based Scopes / pip true-PiP lens composite
}
- //Compute blur textures
- if (!Device.m_SecondViewport.IsSVPFrame()) // Temp fix for blur buffer and SVP
+ // Compute blur textures, pip run it for the SVP too so the scope lens is anti-aliased like the main
+ // view (its buffers are per-target), off keeps the stock !IsSVPFrame skip
+ if ((Device.true_pip_on && Device.m_SecondViewport.m_render_pass_is_svp && !svp_dlss_skip_aa) || !Device.m_SecondViewport.IsSVPFrame())
phase_blur();
//Compute bloom (new)
if (RImplementation.o.ssfx_bloom)
{
- if (!Device.m_SecondViewport.IsSVPFrame())
+ // pip run bloom on the SVP pass too so magnified bright sources flare (per-target buffers)
+ if ((Device.true_pip_on && Device.m_SecondViewport.m_render_pass_is_svp && ps_r__svp_bloom)
+ || !Device.m_SecondViewport.IsSVPFrame())
phase_ssfx_bloom();
else
HW.pContext->ClearRenderTargetView(rt_ssfx_bloom1->pRT, ColorRGBA);
@@ -546,14 +596,19 @@ void CRenderTarget::phase_combine()
phase_pp_bloom();
}
- if (ps_r2_ls_flags.test(R2FLAG_DOF))
- {
+ // pip dof/lut run in the SVP combine AND again over the composited lens in the main pass, the
+ // skip cvars land each exactly once on scope pixels (default 0 keeps the current doubled look)
+ const bool svp_pass_now = Device.true_pip_on && Device.m_SecondViewport.m_render_pass_is_svp;
+ if (ps_r2_ls_flags.test(R2FLAG_DOF) && !(svp_pass_now && ps_r__svp_skip_dof))
+ {
phase_dof();
}
- phase_lut();
+ if (!(svp_pass_now && ps_r__svp_skip_lut))
+ phase_lut();
- if(ps_r2_mask_control.x > 0)
+ // pip the eye-side overlays (gasmask, nvg, heatvision) apply once on the main pass
+ if(ps_r2_mask_control.x > 0 && !svp_pass_now)
{
phase_gasmask_dudv();
if (ps_r2_drops_control.x > 0)
@@ -561,33 +616,119 @@ void CRenderTarget::phase_combine()
phase_gasmask_drops();
}
}
-
- if(ps_r2_nightvision > 0)
- phase_nightvision();
+
+ if(ps_r2_nightvision > 0 && !svp_pass_now)
+ {
+ // pip the offset nvg tube mask both draws the tube and clips the composited scope disc, a
+ // lens stencil stamp splits the post so the disc greens centered while the tube stays put
+ extern Fvector4 ps_dev_param_8;
+ const float nv_fr = ps_dev_param_8.x - floorf(ps_dev_param_8.x);
+ const bool nv_offset_tube = _abs(nv_fr - 0.11f) < 0.005f || _abs(nv_fr - 0.12f) < 0.005f;
+ const bool nv_split = Device.true_pip_on && Device.m_SecondViewport.IsSVPActive()
+ && nv_offset_tube && !RImplementation.o.dx10_msaa
+ && this == RImplementation.TargetMain
+ && !RImplementation.GMBase.RGraph.mapScopeHUDSorted.empty();
+ if (!nv_split)
+ phase_nightvision();
+ else
+ {
+ PIX_EVENT(svp_nvg_tube_split);
+ if (ps_r__svp_stats) ++svp_stats_nvg_split; // overlay proof the tube split fired
+ EnsureScopeShaders();
+ const u32 nv_c = color_rgba(0, 0, 0, 255);
+ const float nv_w = float(Device.dwWidth);
+ const float nv_h = float(Device.dwHeight);
+
+ // the stock dest plus the base depth-stencil, one surface carries the stamp and both passes
+ u_setrt(rt_Color, 0, 0, HW.pBaseZB);
+ RCache.set_CullMode(CULL_NONE);
+
+ auto nv_quad = [&]()
+ {
+ FVF::TL* pv = (FVF::TL*)RCache.Vertex.Lock(4, g_combine->vb_stride, Offset);
+ pv->set(0, nv_h, EPS_S, 1.f, nv_c, 0.f, 1.f); pv++;
+ pv->set(0, 0, EPS_S, 1.f, nv_c, 0.f, 0.f); pv++;
+ pv->set(nv_w, nv_h, EPS_S, 1.f, nv_c, 1.f, 1.f); pv++;
+ pv->set(nv_w, 0, EPS_S, 1.f, nv_c, 1.f, 0.f); pv++;
+ RCache.Vertex.Unlock(4, g_combine->vb_stride);
+ RCache.set_Geometry(g_combine);
+ RCache.Render(D3DPT_TRIANGLELIST, Offset, 0, 4, 0, 2);
+ };
+
+ // zero bit 0x80 screen wide first, the light maskers replace whole stencil bytes so
+ // stale marker garbage can sit in it
+ RCache.set_Element(s_nightvision->E[0]);
+ RCache.set_ColorWriteEnable(0);
+ RCache.set_Stencil(TRUE, D3DCMP_ALWAYS, 0x00, 0xff, 0x80,
+ D3DSTENCILOP_KEEP, D3DSTENCILOP_REPLACE, D3DSTENCILOP_KEEP);
+ nv_quad();
+
+ // stamp bit 0x80 over the lens footprint, the same draw the distort guard ships
+ draw_scope(s_svp_distort_stamp, []()
+ {
+ RCache.set_c("scope_phase", 0);
+ RCache.set_ColorWriteEnable(0);
+ RCache.set_Stencil(TRUE, D3DCMP_ALWAYS, 0x80, 0xff, 0x80,
+ D3DSTENCILOP_KEEP, D3DSTENCILOP_REPLACE, D3DSTENCILOP_KEEP);
+ });
+ RCache.set_ColorWriteEnable();
+
+ // disc pass, the flag routes the binder remap so the tube reads centered here only
+ Device.m_SecondViewport.svp_nvg_disc_pass = true;
+ RCache.set_Constants((R_constant_table*)nullptr); // the ctable cache would skip the loaders
+ RCache.set_Element(s_nightvision->E[ps_r2_nightvision]);
+ RCache.set_Stencil(TRUE, D3DCMP_EQUAL, 0x80, 0x80, 0x00);
+ nv_quad();
+
+ // periphery pass, honest offset value
+ Device.m_SecondViewport.svp_nvg_disc_pass = false;
+ RCache.set_Constants((R_constant_table*)nullptr);
+ RCache.set_Element(s_nightvision->E[ps_r2_nightvision]);
+ RCache.set_Stencil(TRUE, D3DCMP_EQUAL, 0x00, 0x80, 0x00);
+ nv_quad();
+
+ RCache.set_Stencil(FALSE);
+ // the stock terminal copy back into the shared color chain
+ HW.pContext->CopyResource(rt_Generic_0->pSurface, rt_Color->pSurface);
+ }
+ }
//--DSR-- HeatVision_start
- if (ps_r2_heatvision > 0)
+ if (ps_r2_heatvision > 0 && !svp_pass_now)
phase_heatvision();
//--DSR-- HeatVision_end
- if (scope_fake_enabled)
+ // pip the physical scope renders the sight when the true-PiP SVP is live, the 2D shader-scope
+ // overlay (crookr fakescope) would paint its own floating reticle over the rigid image
+ if (scope_fake_enabled && !(Device.true_pip_on && Device.m_SecondViewport.IsSVPActive()))
{
phase_fakescope(); //crookr
}
//SMAA
- if (ps_smaa_quality)
+ if (ps_smaa_quality && !svp_dlss_skip_aa) // pip skip on the DLSS SVP pass (eval reconstructs)
{
//PIX_EVENT(SMAA);
phase_smaa();
RCache.set_Stencil(FALSE);
- }
+ }
- if (RImplementation.o.ssfx_taa && ps_ssfx_taa.x > 0)
+ // main + off path TAA at the stock position (the true-PiP SVP ran its TAA early, above)
+ if (RImplementation.o.ssfx_taa && ps_ssfx_taa.x > 0 &&
+ !(Device.true_pip_on && Device.m_SecondViewport.m_render_pass_is_svp))
{
phase_ssfx_taa();
}
+ // pip clear the scope capture maps at the main-pass tail after every consumer (lens composite,
+ // nvg split, taa mask stamp), unconditional of the taa path, frame-start clear covers staleness
+ if (this == RImplementation.TargetMain)
+ {
+ RImplementation.GMBase.RGraph.mapScopeHUDSorted.clear();
+ RImplementation.GMBase.RGraph.mapScopeHUDObjective.clear();
+ RImplementation.GMBase.RGraph.mapReflexHUDSorted.clear();
+ }
+
if (ssfx_PrevPos_Requiered)
HW.pContext->CopyResource(rt_ssfx_prevPos->pTexture->surface_get(), rt_Position->pTexture->surface_get());
@@ -740,7 +881,8 @@ void CRenderTarget::phase_combine()
g_pGamePersistent->Environment().RenderFlares(); // lens-flares
// PP-if required
- if (PP_Complex)
+ // pip the svp pass skips phase_pp, it writes the real backbuffer
+ if (PP_Complex && !(Device.true_pip_on && Device.m_SecondViewport.m_render_pass_is_svp))
{
PIX_EVENT(phase_pp);
phase_pp();
diff --git a/src/Layers/xrRenderPC_R4/r4_rendertarget_phase_luminance.cpp b/src/Layers/xrRenderPC_R4/r4_rendertarget_phase_luminance.cpp
index 4748dcc356..6d446a4688 100644
--- a/src/Layers/xrRenderPC_R4/r4_rendertarget_phase_luminance.cpp
+++ b/src/Layers/xrRenderPC_R4/r4_rendertarget_phase_luminance.cpp
@@ -148,11 +148,16 @@ void CRenderTarget::phase_luminance()
pv++;
RCache.Vertex.Unlock(4, g_bloom_filter->vb_stride);
- f_luminance_adapt = .9f * f_luminance_adapt + .1f * Device.fTimeDelta * ps_r2_tonemap_adaptation;
+ // per-scope SVP tonemapper override: authored middle-grey / adaption slow the scope's own
+ // adaptation so a bright sky or NVG source does not blow the disc, main view is untouched
+ const bool svp_tm = Device.true_pip_on && Device.m_SecondViewport.m_render_pass_is_svp;
+ const float tm_adapt = (svp_tm && ps_s3ds_adapt_speed > 0.f) ? ps_s3ds_adapt_speed : ps_r2_tonemap_adaptation;
+ const float tm_mgrey = (svp_tm && ps_s3ds_middle_grey > 0.f) ? ps_s3ds_middle_grey : ps_r2_tonemap_middlegray;
+ f_luminance_adapt = .9f * f_luminance_adapt + .1f * Device.fTimeDelta * tm_adapt;
float amount = ps_r2_ls_flags.test(R2FLAG_TONEMAP) ? ps_r2_tonemap_amount : 0;
Fvector3 _none, _full, _result;
_none.set(1, 0, 1);
- _full.set(ps_r2_tonemap_middlegray, 1.f, ps_r2_tonemap_low_lum);
+ _full.set(tm_mgrey, 1.f, ps_r2_tonemap_low_lum);
_result.lerp(_none, _full, amount);
RCache.set_Element(s_luminance->E[2]);
diff --git a/src/Layers/xrRenderPC_R4/svp_camera.cpp b/src/Layers/xrRenderPC_R4/svp_camera.cpp
new file mode 100644
index 0000000000..ed06fde8c5
--- /dev/null
+++ b/src/Layers/xrRenderPC_R4/svp_camera.cpp
@@ -0,0 +1,1345 @@
+#include "stdafx.h"
+#include "../../xrEngine/igame_persistent.h"
+#include "../xrRender/FBasicVisual.h"
+#include "../../xrEngine/customhud.h"
+#include "../../xrEngine/xr_object.h"
+#include "../xrRender/SkeletonCustom.h"
+#include "../xrRender/QueryHelper.h"
+#include "../../Include/xrAPI/xrAPI.h" // pip DRender, the debug-line backend for the scope_debug world overlay
+#include "../../Include/xrRender/DebugRender.h" // pip IDebugRender::add_lines
+#include "../xrRender/SkeletonX.h" // pip CSkeletonX for the skinned lens bone transform
+#include "../../xrEngine/svp_crash_context.h" // pip svp state ring for tester crash reports
+#include "../../xrEngine/xr_ioconsole.h" // pip Console registry for the [SVP-CFG] fingerprint
+#include "../../xrEngine/xr_ioc_cmd.h" // pip IConsole_Command Name/Status/TStatus
+#include "svp_camera.h"
+
+// pip scope_debug >= 2 world overlay, eyepiece (blue) objective (yellow) camera (white)
+// via DRender->add_lines, flushed by the stock debug render
+void debug_scope(Fmatrix scope_camera, float vfov, float aspect)
+{
+ auto dbg_line = [](const Fvector& a, const Fvector& b, u32 color, bool bHud) {
+ Fvector v[2] = { a, b };
+ u16 idx[2] = { 0, 1 };
+ DRender->add_lines(v, 2, idx, 1, color, bHud);
+ };
+
+ auto draw_circle = [&](Fmatrix m, u32 color, bool bHud) {
+ const int n = 100;
+ Fvector v0 = { 0, 0, 0 };
+ for (int i = 0; i <= n; i++) {
+ float angle = float(i) / float(n) * PI * 2.0f;
+ Fvector v1 = { cosf(angle), sinf(angle), 0.f };
+ m.transform(v1);
+ if (i > 0) dbg_line(v0, v1, color, bHud);
+ v0 = v1;
+ }
+ };
+
+ auto draw_lens = [&](CRenderDevice::CSecondVPParams::Lens lens, u32 color) {
+ draw_circle(Fmatrix(lens.m_W).mulB_43(Fmatrix().scale(lens.radius, lens.radius, 0.f)), color, true);
+ Fvector v0 = { 0, 0, 0 }, v1 = { 0, 0, 100 };
+ lens.m_W.transform(v0);
+ lens.m_W.transform(v1);
+ dbg_line(v0, v1, color, true);
+ // up-spoke, a circle + optical axis can't show a ROLL about the axis (rotationally symmetric),
+ // so draw the lens up-vector as a radial spoke, a rolled lens shows the spoke pointing off-up
+ Fvector u0 = { 0, 0, 0 }, u1 = { 0, lens.radius, 0 };
+ lens.m_W.transform(u0);
+ lens.m_W.transform(u1);
+ dbg_line(u0, u1, color, true);
+ };
+
+ auto draw_camera = [&](u32 color) {
+ const float cm = 1.0f / 100.0f;
+ draw_circle(Fmatrix(scope_camera).mulB_43(Fmatrix().scale(.25f * cm, .25f * cm, 0.f)), color, true);
+ };
+
+ // pip wireframe cube at a transform (orientation + position), half-extent h, 12 edges
+ auto draw_cube = [&](const Fmatrix& m, float h, u32 color) {
+ Fvector c[8];
+ for (int i = 0; i < 8; i++) {
+ Fvector q; q.set((i & 1) ? h : -h, (i & 2) ? h : -h, (i & 4) ? h : -h);
+ m.transform_tiny(c[i], q);
+ }
+ static const int e[12][2] = { {0,1},{2,3},{4,5},{6,7}, {0,2},{1,3},{4,6},{5,7}, {0,4},{1,5},{2,6},{3,7} };
+ for (int k = 0; k < 12; k++)
+ dbg_line(c[e[k][0]], c[e[k][1]], color, true);
+ };
+
+ auto& p = Device.m_SecondViewport;
+ // a culled weapon leaves the live radii 0 this frame, fall back to the held debug radii
+ CRenderDevice::CSecondVPParams::Lens eye = p.eyepiece;
+ if (eye.radius <= EPS)
+ eye.radius = p.dbg_eyepiece_r;
+ draw_lens(eye, 0xff0000ff); // eyepiece blue
+ // objective yellow at the CAMERA (the real entrance the scope views from), the stored p.objective.m_W
+ // is a forward math intermediate (it derives the camera pull-back d), not the visible front lens
+ CRenderDevice::CSecondVPParams::Lens objAtCam = p.objective;
+ if (objAtCam.radius <= EPS)
+ objAtCam.radius = p.dbg_objective_r;
+ objAtCam.m_W = scope_camera;
+ draw_lens(objAtCam, 0xffffff00); // objective yellow (at the camera/entrance)
+ // orange disc at the true derived objective, its gap from the yellow camera disc shows the pull-back
+ CRenderDevice::CSecondVPParams::Lens objTrue = p.objective;
+ if (objTrue.radius <= EPS)
+ objTrue.radius = p.dbg_objective_r;
+ draw_circle(Fmatrix(objTrue.m_W).mulB_43(Fmatrix().scale(objTrue.radius, objTrue.radius, 0.f)), 0xffff8000, true);
+ draw_camera(0xffffffff); // scope cam white
+ // pip magenta cube at the live SVP camera position, updates each frame so the camera move between
+ // svpscope 1 (eyepiece) and 2 (objective) is obvious at a glance
+ draw_cube(scope_camera, eye.radius * 0.6f, 0xffff00ff);
+
+ // green world-view frustum from the SVP camera, the exact cone the scope image renders
+ if (vfov > EPS)
+ {
+ const float d = 30.f;
+ const float hh = tanf(vfov * 0.5f) * d;
+ const float hw = hh * ((aspect > EPS) ? aspect : 1.f);
+ Fvector corner[4];
+ for (int i = 0; i < 4; i++)
+ {
+ const float sx = (i == 0 || i == 3) ? -1.f : 1.f;
+ const float sy = (i < 2) ? 1.f : -1.f;
+ corner[i].mad(scope_camera.c, scope_camera.k, d);
+ corner[i].mad(scope_camera.i, sx * hw);
+ corner[i].mad(scope_camera.j, sy * hh);
+ dbg_line(scope_camera.c, corner[i], 0xff00ff00, true);
+ }
+ for (int i = 0; i < 4; i++)
+ dbg_line(corner[i], corner[(i + 1) & 3], 0xff00ff00, true);
+ }
+
+ // cyan front-plane disc on the optical axis, mode 2 drops pieces wholly behind it
+ extern float g_svp_hud_front_m;
+ {
+ Fvector ax; ax.sub(p.objective.m_W.c, p.eyepiece.m_W.c);
+ const float tube = ax.magnitude();
+ if (tube > EPS)
+ {
+ ax.div(tube);
+ const float front = (g_svp_hud_front_m > EPS) ? g_svp_hud_front_m : tube;
+ Fmatrix fm; fm.identity();
+ fm.k.set(ax);
+ Fvector seed = (_abs(ax.y) < 0.9f) ? Fvector{0, 1, 0} : Fvector{1, 0, 0};
+ fm.i.crossproduct(seed, ax); fm.i.normalize();
+ fm.j.crossproduct(ax, fm.i);
+ fm.c.mad(p.eyepiece.m_W.c, ax, front);
+ const float fr = std::max(eye.radius, objAtCam.radius) * 1.75f;
+ draw_circle(Fmatrix(fm).mulB_43(Fmatrix().scale(fr, fr, 0.f)), 0xff00ffff, true);
+ extern int ps_r__svp_cop_diag;
+ static u32 s_cam_ms = 0;
+ if (ps_r__svp_cop_diag && Device.dwTimeGlobal - s_cam_ms > 1000)
+ {
+ s_cam_ms = Device.dwTimeGlobal;
+ Fvector ec; ec.sub(scope_camera.c, p.eyepiece.m_W.c);
+ extern Fvector4 ps_s3ds_param_3;
+ extern int ps_r__svp_hud_full;
+ PipMsg("[SVP-CAM] pos=(%.2f,%.2f,%.2f) fwd=(%.2f,%.2f,%.2f) vfov=%.2fdeg cam2eye=%.1fcm tube=%.1fcm frontplane=%.1fcm it=%.0f full=%d",
+ scope_camera.c.x, scope_camera.c.y, scope_camera.c.z,
+ scope_camera.k.x, scope_camera.k.y, scope_camera.k.z,
+ rad2deg(vfov), ec.magnitude() * 100.f, tube * 100.f, front * 100.f,
+ ps_s3ds_param_3.x, ps_r__svp_hud_full);
+ }
+ }
+ }
+}
+
+// pip STUB sub-pixel jitter for the SVP scene projection (DLSS scaffolding), swap for Ascii's
+// shared helper. Halton(2,3), 16-sample phase, returns a centered offset in [-0.5,0.5] px
+static float svp_halton(u32 i, u32 b)
+{
+ float f = 1.0f, r = 0.0f;
+ while (i > 0) { f /= (float)b; r += f * (float)(i % b); i /= b; }
+ return r;
+}
+static Fvector2 svp_jitter_offset(u32 frame)
+{
+ const u32 phase = 16; // TODO match Ascii's confirmed sequence length
+ u32 i = (frame % phase) + 1; // Halton is 1-based
+ Fvector2 o;
+ o.set(svp_halton(i, 2) - 0.5f, svp_halton(i, 3) - 0.5f);
+ return o;
+}
+// apply a pixel jitter to a projection by shifting the post-perspective NDC center, the axis/sign/
+// remap convention lives here so the eval swap is one line
+static void svp_apply_jitter(Fmatrix& proj, Fvector2 px, float w, float h)
+{
+ proj.m[2][0] += 2.0f * px.x / w; // NDC x, clip.x gains z * this, the w-divide cancels z
+ proj.m[2][1] -= 2.0f * px.y / h; // NDC y, negated for texture-down
+}
+
+// pip [3DB] ballistics overlay core, has_sight draws the captured red sight line and gaps the fire
+// axis against it (else the camera axis), draw_lens adds the eyepiece/objective markers + zeroed ray
+static void svp_3db_overlay(float fNearPlane, bool has_sight, const Fvector& sight_org, const Fvector& sight_axis,
+ bool draw_lens, const Fvector& eyepiece_pos, bool has_objective, const Fvector& objective_pos)
+{
+ extern int ps_r__3db_debug;
+ auto& vp = Device.m_SecondViewport;
+ // display distance, the ranged zero re-picks every tick and would teleport every
+ // endpoint while panning, settle it for drawing, the log keeps the live number
+ const float D_live = (vp.fire_ray_zero > 0.f) ? vp.fire_ray_zero : 100.f;
+ static float s_disp_D = 0.f;
+ static u32 s_disp_frame = 0;
+ if (Device.dwFrame != s_disp_frame + 1 || s_disp_D <= 0.f)
+ s_disp_D = D_live;
+ else
+ s_disp_D += (D_live - s_disp_D) * (1.f - expf(-Device.fTimeDelta / 0.25f));
+ s_disp_frame = Device.dwFrame;
+ const float D = s_disp_D;
+ Fmatrix eyeW2; eyeW2.invert(Device.matrices[0].mView);
+ // clip to the near plane, a behind-plane endpoint rasterizes as a screen streak
+ const float znear = fNearPlane + 0.01f;
+ auto line = [&](const Fvector& a, const Fvector& b, u32 color) {
+ Fvector ca; ca.sub(a, eyeW2.c);
+ Fvector cb; cb.sub(b, eyeW2.c);
+ const float za = ca.dotproduct(eyeW2.k), zb = cb.dotproduct(eyeW2.k);
+ if (za < znear && zb < znear)
+ return;
+ Fvector aa = a, bb = b;
+ if (za < znear)
+ aa.lerp(a, b, (znear - za) / (zb - za));
+ else if (zb < znear)
+ bb.lerp(a, b, (znear - za) / (zb - za));
+ Fvector v[2] = { aa, bb }; u16 i[2] = { 0, 1 };
+ DRender->add_lines(v, 2, i, 1, color, true);
+ };
+ auto cross = [&](const Fvector& p, float s, u32 color) {
+ Fvector a, b;
+ a.set(p); a.mad(eyeW2.i, -s); b.set(p); b.mad(eyeW2.i, s);
+ line(a, b, color);
+ a.set(p); a.mad(eyeW2.j, -s); b.set(p); b.mad(eyeW2.j, s);
+ line(a, b, color);
+ };
+
+ // yellow muzzle marker, red eyepiece and cyan objective only with a captured lens
+ cross(vp.muzzle_pos, 0.01f, 0xffffff00);
+ if (draw_lens)
+ {
+ cross(eyepiece_pos, 0.01f, 0xffff0000);
+ if (has_objective)
+ cross(objective_pos, 0.01f, 0xff00ffff);
+ }
+
+ // the sight line to the zero distance, drawn from the stable published copy ballistics converge on
+ if (has_sight)
+ {
+ Fvector sight; sight.mad(sight_org, sight_axis, D);
+ line(sight_org, sight, 0xffff0000);
+ cross(sight, D * 0.02f, 0xffff0000);
+ }
+
+ // the camera crosshair ray from the mirrored actor eye, the ballistic truth when aimpos
+ // is off, stays the shooter's eye while demo_record flies the device camera
+ Fvector cpos, cfwd;
+ if (vp.eye_ray_dir.square_magnitude() > EPS)
+ {
+ cpos.set(vp.eye_ray_pos);
+ cfwd.set(vp.eye_ray_dir);
+ }
+ else
+ {
+ cpos.set(eyeW2.c);
+ cfwd.set(eyeW2.k);
+ }
+ cfwd.normalize_safe();
+ Fvector chp; chp.mad(cpos, cfwd, D);
+ line(cpos, chp, 0xff4080ff);
+ cross(chp, D * 0.02f, 0xff4080ff);
+
+ // the raw fire axis, where bullets go with g_svp_zero 0
+ if (Device.dwFrame - vp.fire_ray_frame < 8)
+ {
+ Fvector faxis; faxis.set(vp.fire_ray_dir); faxis.normalize_safe();
+ Fvector fire; fire.mad(vp.fire_ray_pos, faxis, D);
+ line(vp.fire_ray_pos, fire, 0xff00ff00);
+ cross(fire, D * 0.02f, 0xff00ff00);
+ // the gap reads the fire axis against the sight line when captured, else the aim axis
+ float c = has_sight ? sight_axis.dotproduct(faxis) : cfwd.dotproduct(faxis);
+ clamp(c, -1.f, 1.f);
+ static u32 s_aim_ms = 0;
+ if (Device.dwTimeGlobal - s_aim_ms > 1000)
+ {
+ s_aim_ms = Device.dwTimeGlobal;
+ if (has_sight)
+ PipMsg("[3DB] axis gap %.2f mrad at %.0fm (zero %.0f)", acosf(c) * 1000.f, D, vp.fire_ray_zero);
+ else
+ PipMsg("[3DB] fire-aim gap %.2f mrad at %.0fm (zero %.0f)", acosf(c) * 1000.f, D, vp.fire_ray_zero);
+ }
+
+ // the zeroed departure ray the shot actually flies, only with a captured sight line
+ if (has_sight && ps_r__3db_debug >= 2 && vp.fire_ray_zero > 0.f)
+ {
+ Fvector zp; zp.mad(sight_org, sight_axis, D);
+ line(vp.fire_ray_pos, zp, 0xffffffff);
+ }
+ }
+
+ // fading shot tracers, brightness decays over 5s
+ if (ps_r__3db_debug >= 3)
+ {
+ for (const auto& tr : vp.fire_traces)
+ {
+ const u32 age = Device.dwTimeGlobal - tr.time_ms;
+ if (!tr.time_ms || age >= 5000)
+ continue;
+ const u32 k = 255u - age * 255u / 5000u;
+ const u32 color = 0xff000000 | (k << 16) | (k << 8);
+ Fvector end; end.mad(tr.pos, tr.dir, D * 2.f);
+ line(tr.pos, end, color);
+ }
+ }
+}
+
+// pip [3DB] overlay for sights without a captured pip lens, reflex and irons keep 3d
+// ballistics so the sight independent markers draw here, the pip overlay owns the rest
+void ballistics_debug_overlay()
+{
+ float _, fov, fNearPlane, fFarPlane;
+ Device.matrices[0].mProject.decompose_projection(fov, _, fNearPlane, fFarPlane);
+ const Fvector zero = { 0.f, 0.f, 0.f };
+ svp_3db_overlay(fNearPlane, false, zero, zero, false, zero, false, zero);
+}
+
+// pip build the SVP camera (fills Device.matrices[1]) from the captured lens + the weapon
+// zoom factor, called after the lens derives so TargetSVP->SetActive reads it ready
+void svpCamera()
+{
+ // the published zoom is raise transient free, unset falls back to the shader constant
+ const float zoom_src = (Device.m_SecondViewport.svp_zoom_pub > 1.f)
+ ? Device.m_SecondViewport.svp_zoom_pub
+ : g_pGamePersistent->m_pGShaderConstants->hud_params.y;
+ // the scale rides the live fov so the scope keeps its fov-75 look at any user fov
+ float svp_fov = zoom_src * 0.75f * Device.m_SecondViewport.svp_fov_scale;
+ float _, fov, fNearPlane, fFarPlane;
+ Device.matrices[0].mProject.decompose_projection(fov, _, fNearPlane, fFarPlane);
+ // the mag reads the steady wide aim fov (punch free from the weapon publish), the live decomposed
+ // fov keeps feeding the vFov/projection so the scope image tracks the actual main view
+ const float aim_fov_pub = Device.m_SecondViewport.svp_aim_fov;
+ const float fov_aim = (aim_fov_pub > 1.f) ? deg2rad(aim_fov_pub) : fov;
+
+ // a zoom-0 tube sight (1x thermal/nv) has no zoom fov and re-images at 1x, the near-0 value
+ // would also blow up the vFov/offset tan() math
+ if (svp_fov < 1.0f) svp_fov = rad2deg(fov_aim);
+
+
+ auto mm = Device.matrices[0];
+ auto& params = Device.m_SecondViewport;
+
+ // analytic eyepiece fit, a disc of radius r at view depth d projects to ndc height 2*r*_22/d
+ // under mProjectHud, the fit is screen height 2 over that. depth on the view forward, exact off axis
+ Fmatrix eyeW0; eyeW0.invert(mm.mView);
+ Fvector camfwd; camfwd.set(eyeW0.k); camfwd.normalize();
+ Fvector eyed; eyed.sub(params.svp_sight_pos, eyeW0.c);
+ const float lens_depth = eyed.dotproduct(camfwd);
+ const float ndc_height = (lens_depth > EPS)
+ ? (2.f * params.svp_lens_r * mm.mProjectHud._22 / lens_depth) : 0.f;
+ const bool analytic_ok = params.eyepiece.radius > EPS && params.svp_lens_r > EPS
+ && lens_depth > EPS && _valid(ndc_height) && ndc_height > 1e-4f;
+ const float ratio_analytic = analytic_ok ? (2.f / ndc_height) : 0.f;
+ float ratio_magnification = 1.f;
+
+ // the magnification of the scope (1X 4X etc)
+ float scope_magnification = fov_aim / deg2rad(svp_fov);
+
+ // flat screen optic (binocular), the panel is a see-through window so the svp fov is the
+ // panel's angular subtense at the weapon zoom, magnification then tracks the stock look
+ extern int ps_r__svp_flat_window;
+ extern Fvector4 ps_s3ds_param_3;
+ const bool flat_window = ps_r__svp_flat_window && (int)ps_s3ds_param_3.y == 8
+ && Device.m_SecondViewport.svp_disc_px > 1.f;
+ auto window_fov = [&](float zdeg) {
+ const float p = _min(Device.m_SecondViewport.svp_disc_px / (float)Device.dwHeight, 1.5f);
+ return 2.f * atanf(p * tanf(deg2rad(zdeg) * 0.5f));
+ };
+ if (flat_window)
+ {
+ // authored mag flat optics keep the clean optical mag, only the see-through panel takes the subtense ratio
+ if (!params.svp_authored_mag)
+ scope_magnification = fov_aim / window_fov(g_pGamePersistent->m_pGShaderConstants->hud_params.y > 1.f
+ ? g_pGamePersistent->m_pGShaderConstants->hud_params.y : svp_fov);
+ ratio_magnification = 1.f;
+ }
+ else
+ {
+ // coast the analytic fit through the raise and one pole it while settled, the raw lens pose
+ // bobs but the along axis depth barely moves so an unsettled aim or alt sight holds the value
+ const float aim_rot = g_pGamePersistent->m_pGShaderConstants->hud_params.x;
+ const bool alt_sight = Device.m_SecondViewport.svp_alt_sight;
+ static float s_ratio = 0.f;
+ static u32 s_ratio_frame = 0;
+ static u32 s_ratio_epoch = 0;
+ const bool gap = (Device.dwFrame != s_ratio_frame + 1);
+ s_ratio_frame = Device.dwFrame;
+ // subscribe to the optic epoch, a magnifier flip or scope swap bumps it with no frame gap so
+ // the fit drops the old optic's value and reseeds at the next settle
+ const bool epoch = (params.svp_optic_epoch != s_ratio_epoch);
+ s_ratio_epoch = params.svp_optic_epoch;
+ const bool can_track = (aim_rot > 0.999f) && !alt_sight;
+ if (analytic_ok)
+ {
+ // a session gap or an optic swap drops the latch, the raise value is meaningless so the
+ // seed waits for the first settled frame, then the one pole eases residual pose noise
+ if (gap || epoch || !_valid(s_ratio) || s_ratio <= 0.f)
+ s_ratio = can_track ? ratio_analytic : 0.f;
+ else if (can_track)
+ s_ratio += (ratio_analytic - s_ratio) * (1.f - expf(-Device.fTimeDelta / 0.15f));
+ }
+ ratio_magnification = (s_ratio > 0.f) ? s_ratio : ratio_analytic;
+ }
+
+ // magnification ceiling, the optic's authored config max zoom (hud_fov_params.x, the same source
+ // as g_pip_scope_max_mag), held through section churn so a flip cannot push the mag past the optic
+ {
+ const Fvector4& fovp_c = g_pGamePersistent->m_pGShaderConstants->hud_fov_params;
+ const float fscale_c = rad2deg(fov_aim) / 75.f;
+ const float cfg_max = (fovp_c.x > EPS) ? fov_aim / deg2rad(fovp_c.x * 0.75f * fscale_c) : 0.f;
+ static float s_mag_ceiling = 0.f;
+ static u32 s_ceiling_frame = 0;
+ if (Device.dwFrame != s_ceiling_frame + 1) s_mag_ceiling = 0.f; // session gap drops the hold
+ s_ceiling_frame = Device.dwFrame;
+ const float rot_c = g_pGamePersistent->m_pGShaderConstants->hud_params.x;
+ if (cfg_max > EPS && rot_c > 0.999f) s_mag_ceiling = cfg_max; // latch the settled config max
+ const float ceiling = (s_mag_ceiling > EPS) ? s_mag_ceiling : cfg_max;
+ if (ceiling > EPS && scope_magnification > ceiling) scope_magnification = ceiling;
+ }
+
+ // pip ratio pipeline diag ([SVP-RATIO]), cross checks the analytic fit against the measured
+ // screen ndc extent, they agree for a settled on axis pose, measured lives here for the check
+ {
+ extern int ps_r__svp_diag;
+ if (ps_r__svp_diag)
+ {
+ static u32 s_rat_ms = 0;
+ if (Device.dwTimeGlobal - s_rat_ms > 1000)
+ {
+ s_rat_ms = Device.dwTimeGlobal;
+ Fvector4 top, bot;
+ Fmatrix m_WVP = Fmatrix().mul(mm.mProjectHud, Fmatrix().mul(mm.mView, params.eyepiece.m_W));
+ m_WVP.transform(top, {0, params.eyepiece.radius, 0, 1});
+ m_WVP.transform(bot, {0, -params.eyepiece.radius, 0, 1});
+ top.div(top.w); bot.div(bot.w);
+ float meas_h = abs(top.y - bot.y);
+ if (!_valid(meas_h) || meas_h < 0.001f) meas_h = 0.001f;
+ float hf, ha, hn, hff;
+ mm.mProjectHud.decompose_projection(hf, ha, hn, hff);
+ PipMsg("[SVP-RATIO] meas %.3f analytic %.3f final %.3f flat %d p3y %.1f measH %.3f ndcH %.3f depth %.1fcm r %.2fcm aim %.2f alt %d ok %d hfov %.1f",
+ 2.f / meas_h, ratio_analytic, ratio_magnification, (int)flat_window,
+ ps_s3ds_param_3.y, meas_h, ndc_height,
+ lens_depth * 100.f, params.svp_lens_r * 100.f,
+ g_pGamePersistent->m_pGShaderConstants->hud_params.x,
+ (int)Device.m_SecondViewport.svp_alt_sight, (int)analytic_ok,
+ rad2deg(hf));
+ }
+ }
+ }
+
+ // expose engine magnification so the shader has a curMag with no 3DSS config
+ extern float g_pip_scope_magnification;
+ extern float g_pip_scope_min_mag;
+ extern float g_pip_scope_max_mag;
+ extern float g_pip_scope_ratio;
+ // eyepiece-fit factor, rated on-screen magnification = ratio * scope, clamped for degenerate geometry
+ const float ratio_use = (ratio_magnification > 0.5f) ? ((ratio_magnification < 8.f) ? ratio_magnification : 8.f) : 1.f;
+ if (svp_fov > EPS)
+ {
+ g_pip_scope_magnification = scope_magnification;
+ g_pip_scope_ratio = ratio_use;
+ // derive min/max mag from hud_fov_params for variable reticles (fixed scope: x == y)
+ const Fvector4& fovp = g_pGamePersistent->m_pGShaderConstants->hud_fov_params;
+ // the 75-base bounds rescale to the aim fov, authored mins are 75-base too,
+ // only the legacy optical-model min already rides the aim fov and passes through
+ const float fscale = rad2deg(fov_aim) / 75.f;
+ const float yscale = (_abs(fovp.y - fovp.x) < 0.01f || params.svp_min_75base) ? fscale : 1.f;
+ if (flat_window && !params.svp_authored_mag)
+ {
+ g_pip_scope_max_mag = (fovp.x > EPS) ? fov_aim / window_fov(fovp.x * fscale) : scope_magnification;
+ g_pip_scope_min_mag = (fovp.y > EPS) ? fov_aim / window_fov(fovp.y * yscale) : scope_magnification;
+ }
+ else
+ {
+ g_pip_scope_max_mag = (fovp.x > EPS) ? fov_aim / deg2rad(fovp.x * 0.75f * fscale) : scope_magnification;
+ g_pip_scope_min_mag = (fovp.y > EPS) ? fov_aim / deg2rad(fovp.y * 0.75f * yscale) : scope_magnification;
+ }
+ }
+
+ // the fov we render at to get the correct zoom, the eyepiece-fit ratio scales the vFov
+ float vFov = 2.0f * atan(tan(fov * 0.5f) / (ratio_use * scope_magnification));
+ // flat window renders exactly the panel subtense (tan-correct, the mag division is not)
+ if (flat_window)
+ {
+ if (params.svp_authored_mag)
+ {
+ // authored mag flat optic renders the floor panel subtense then the optical mag crops in
+ const Fvector4& fovp = g_pGamePersistent->m_pGShaderConstants->hud_fov_params;
+ const float fscale = rad2deg(fov_aim) / 75.f;
+ const float yscale = (_abs(fovp.y - fovp.x) < 0.01f) ? fscale : 1.f;
+ vFov = window_fov(fovp.y * yscale) / scope_magnification;
+ }
+ else
+ vFov = fov / scope_magnification;
+ }
+
+ auto near_plane = fNearPlane;
+ auto m_W_svpcam = params.eyepiece.m_W; // svpscope 1 places the camera on the eyepiece
+ // svpscope 2 renders the world from the entrance pupil, the eye slid up the optical
+ // axis to the measured front plane
+ if (scope_svp_enabled >= 2)
+ {
+ Fmatrix eyeW; eyeW.invert(Device.matrices[0].mView);
+ m_W_svpcam.c.set(eyeW.c);
+ extern float g_svp_hud_front_m;
+ // latch the front plane at the settle edge, the script push lands frames after zoom in so a
+ // live per-frame read would step the camera mid aim, hold the settled value for the aim
+ static float s_front_hold = 0.f;
+ static u32 s_front_frame = 0;
+ static bool s_front_latched = false;
+ const float aim_r = g_pGamePersistent ? g_pGamePersistent->m_pGShaderConstants->hud_params.x : 0.f;
+ if (Device.dwFrame != s_front_frame + 1) s_front_latched = false; // session gap re-latches
+ s_front_frame = Device.dwFrame;
+ if (aim_r > 0.999f) { if (!s_front_latched) { s_front_hold = g_svp_hud_front_m; s_front_latched = true; } }
+ else s_front_latched = false; // unsettled, re-latch at the next settle
+ const float front_use = s_front_latched ? s_front_hold : g_svp_hud_front_m;
+ if (front_use > EPS)
+ {
+ // the entrance pupil sits ON the axis, no eye lateral residue in the formed image
+ // (eye movement belongs to the exit-pupil crescent, not the camera)
+ Fvector ax; ax.set(params.eyepiece.m_W.k); ax.normalize();
+ m_W_svpcam.c.mad(params.eyepiece.m_W.c, ax, front_use);
+ // side/top mounted optics view the world from the objective's real lateral position
+ const Fvector4& off = params.svp_opt_offset;
+ if (params.eyepiece.radius > EPS && (_abs(off.x) > EPS || _abs(off.y) > EPS))
+ {
+ Fvector ri; ri.set(params.eyepiece.m_W.i); ri.normalize();
+ Fvector up; up.set(params.eyepiece.m_W.j); up.normalize();
+ m_W_svpcam.c.mad(ri, off.x * params.eyepiece.radius);
+ m_W_svpcam.c.mad(up, off.y * params.eyepiece.radius);
+ }
+ // mode 2 pushes the scope near clip to the pupil front plane so behind-pupil barrel
+ // falls behind it, world geometry sits far past this
+ extern int ps_r__svp_near_pupil;
+ if (ps_r__svp_near_pupil && front_use > near_plane)
+ near_plane = front_use;
+ }
+ extern int ps_r__svp_cop_diag;
+ static u32 s_frontcam_ms = 0;
+ if (ps_r__svp_cop_diag && Device.dwTimeGlobal - s_frontcam_ms > 500)
+ {
+ s_frontcam_ms = Device.dwTimeGlobal;
+ PipMsg("[SVP-CAM] frontplane hold=%.1fcm live=%.1fcm aim=%.3f latched=%d",
+ front_use * 100.f, g_svp_hud_front_m * 100.f, aim_r, (int)s_front_latched);
+ }
+ }
+
+ // pip roll_stabilize aligns the SVP camera up to the view up, dropping mount cant/flip
+ // (0 = raw mesh tilt)
+ extern int ps_r__svp_roll_stabilize;
+ if (ps_r__svp_roll_stabilize)
+ {
+ Fvector fwd, wup, right, up;
+ fwd.set(m_W_svpcam.k.x, m_W_svpcam.k.y, m_W_svpcam.k.z);
+ fwd.normalize();
+ wup.set(Device.vCameraTop.x, Device.vCameraTop.y, Device.vCameraTop.z);
+ right.crossproduct(wup, fwd);
+ if (right.magnitude() > EPS_S)
+ {
+ right.normalize();
+ up.crossproduct(fwd, right);
+ up.normalize();
+ m_W_svpcam.i.x = right.x; m_W_svpcam.i.y = right.y; m_W_svpcam.i.z = right.z;
+ m_W_svpcam.j.x = up.x; m_W_svpcam.j.y = up.y; m_W_svpcam.j.z = up.z;
+ m_W_svpcam.k.x = fwd.x; m_W_svpcam.k.y = fwd.y; m_W_svpcam.k.z = fwd.z;
+ }
+ }
+
+
+ // pip lens flip diagnostic ([SVP-ORIENT]), the mesh basis vs the final svp camera basis
+ {
+ extern int ps_r__svp_cop_diag;
+ if (ps_r__svp_cop_diag && params.eyepiece.radius > EPS)
+ {
+ static u32 s_orient_ms = 0;
+ if (Device.dwTimeGlobal - s_orient_ms > 500)
+ {
+ s_orient_ms = Device.dwTimeGlobal;
+ PipMsg("[SVP-ORIENT] meshUp=(%.2f,%.2f,%.2f) meshFwd=(%.2f,%.2f,%.2f) camUp=(%.2f,%.2f,%.2f) rollStab=%d camUp_out=(%.2f,%.2f,%.2f) camRight_out=(%.2f,%.2f,%.2f) camFwd_out=(%.2f,%.2f,%.2f)",
+ params.eyepiece.m_W.j.x, params.eyepiece.m_W.j.y, params.eyepiece.m_W.j.z,
+ params.eyepiece.m_W.k.x, params.eyepiece.m_W.k.y, params.eyepiece.m_W.k.z,
+ Device.vCameraTop.x, Device.vCameraTop.y, Device.vCameraTop.z,
+ ps_r__svp_roll_stabilize,
+ m_W_svpcam.j.x, m_W_svpcam.j.y, m_W_svpcam.j.z,
+ m_W_svpcam.i.x, m_W_svpcam.i.y, m_W_svpcam.i.z,
+ m_W_svpcam.k.x, m_W_svpcam.k.y, m_W_svpcam.k.z);
+ }
+ }
+ }
+
+ // aspect = height/width (engine convention, matches SetActive's fASPECT); square == 1
+ // derived from the same policy that sizes the target this frame, not the pre-frame target shape
+ extern void svp_target_wh(u32&, u32&);
+ u32 tw, th;
+ svp_target_wh(tw, th);
+ float aspect = (float)th / (float)tw;
+ // a non-square SVP keeps the horizontal window subtense at vFov, the vertical follows the aspect
+ float vfov_use = (aspect < 0.999f) ? (2.f * atanf(aspect * tanf(vFov * 0.5f))) : vFov;
+
+ float fNearPlane_hud, fFarPlane_hud;
+ Device.matrices[0].mProject.decompose_projection(_, _, fNearPlane_hud, fFarPlane_hud);
+ auto svp_proj = Fmatrix().build_projection(vfov_use, aspect, near_plane, fFarPlane);
+ auto svp_proj_hud = Fmatrix().build_projection(vfov_use, aspect, near_plane, fFarPlane_hud);
+
+ // pip DLSS jitter the SVP scene projection (gated), {0,0} otherwise, applied to mProject only
+ Device.m_SecondViewport.svp_jitter_px.set(0, 0);
+ if (ps_r__svp_dlss != 0)
+ {
+ const u32 jf = Device.dwFrame; // latch once, stable on the render thread this frame
+ Fvector2 jpx = svp_jitter_offset(jf);
+ svp_apply_jitter(svp_proj, jpx, (float)RImplementation.TargetSVP->Width, (float)RImplementation.TargetSVP->Height);
+ Device.m_SecondViewport.svp_jitter_px = jpx;
+ }
+
+ // the held dbg radius keeps the lines through a culled weapon, it zeroes on unscope
+ if (scope_debug >= 2 && (params.eyepiece.radius > EPS || params.dbg_eyepiece_r > EPS))
+ debug_scope(m_W_svpcam, vFov, (float)aspect);
+
+ Device.matrices[1].mView.invert(m_W_svpcam);
+ Device.matrices[1].mProject = svp_proj;
+ Device.matrices[1].mProjectHud = svp_proj_hud;
+
+ // pip cache the SVP scene constants for the DLSS eval inputs and the defocus bind (render thread,
+ // written then read the same frame). svp_fov is radians from the projection, the basis is the camera world
+ {
+ auto& vp = Device.m_SecondViewport;
+ Device.matrices[1].mProject.decompose_projection(vp.svp_fov, vp.svp_aspect, vp.svp_near, vp.svp_far);
+ vp.svp_cam_pos = m_W_svpcam.c;
+ vp.svp_right = m_W_svpcam.i;
+ vp.svp_up = m_W_svpcam.j;
+ vp.svp_fwd = m_W_svpcam.k;
+ }
+
+ // pip snapshot the svp runtime state into the crash-context ring for tester crash logs
+ {
+ extern float g_svp_hud_front_m;
+ auto& vp = Device.m_SecondViewport;
+ SvpCrashFrame cf;
+ cf.frame = Device.dwFrame;
+ cf.mode = scope_svp_enabled;
+ cf.active = vp.IsSVPActive();
+ cf.render_pass_is_svp = vp.m_render_pass_is_svp;
+ cf.hud_front_m = g_svp_hud_front_m;
+ cf.mag = vp.svp_mag;
+ cf.fov_deg = rad2deg(vp.svp_fov);
+ cf.disc_px = vp.svp_disc_px;
+ svp_crash_context_push(cf);
+ }
+
+ // pip optics diagnostic: throttled [SVPCOP] log of the camera center-of-projection offset from the
+ // eye, settled frames only (ADS transitions blow up ratio_magnification)
+ extern int ps_r__svp_cop_diag;
+ if (ps_r__svp_cop_diag && params.eyepiece.radius > EPS
+ && ratio_magnification > 1.0f && ratio_magnification < 8.0f)
+ {
+ static u32 s_last_ms = 0;
+ static float s_last_mag = 0.f;
+ const float eff_mag = ratio_magnification * scope_magnification;
+ const bool mag_moved = (s_last_mag < EPS) || (fabsf(eff_mag - s_last_mag) > 0.03f * s_last_mag);
+ if (Device.dwTimeGlobal - s_last_ms > 400 || mag_moved)
+ {
+ s_last_ms = Device.dwTimeGlobal;
+ s_last_mag = eff_mag;
+ Fmatrix eyeW; eyeW.invert(Device.matrices[0].mView);
+ Fvector camdir; camdir.set(eyeW.k); camdir.normalize();
+ Fvector d; d.sub(m_W_svpcam.c, eyeW.c);
+ const float fwd = d.dotproduct(camdir);
+ Fvector fwd_v; fwd_v.set(camdir); fwd_v.mul(fwd);
+ Fvector lat_v; lat_v.sub(d, fwd_v);
+ Fvector eyefwd; eyefwd.set(params.eyepiece.m_W.k); eyefwd.normalize();
+ Fvector od; od.sub(params.objective.m_W.c, params.eyepiece.m_W.c);
+ // signed scope cant vs world up, proves whether lean actually rolls the weapon on a rig
+ float cant = 0.f;
+ {
+ Fvector jup; jup.set(params.eyepiece.m_W.j); jup.normalize();
+ Fvector lvl; lvl.set(0.f, 1.f, 0.f); lvl.mad(eyefwd, -lvl.dotproduct(eyefwd));
+ if (lvl.magnitude() > EPS)
+ {
+ lvl.normalize();
+ Fvector cx; cx.crossproduct(lvl, jup);
+ cant = rad2deg(atan2f(cx.dotproduct(eyefwd), lvl.dotproduct(jup)));
+ }
+ }
+ PipMsg("[SVPCOP] mode=%d mag=%.3f eff=%.3f min=%.3f max=%.3f ratio=%.3f svpfov=%.2f vfov=%.2f cop_cm=%.2f fwd_cm=%.2f lat_cm=%.2f eye_r_cm=%.2f obj_fwd_cm=%.2f obj_r_cm=%.2f cant=%.1f",
+ scope_svp_enabled, scope_magnification, eff_mag, g_pip_scope_min_mag, g_pip_scope_max_mag, ratio_magnification,
+ svp_fov, rad2deg(vFov), d.magnitude() * 100.f, fwd * 100.f, lat_v.magnitude() * 100.f,
+ params.eyepiece.radius * 100.f, od.dotproduct(eyefwd) * 100.f, params.objective.radius * 100.f,
+ cant);
+
+ // barrel-continuity probe: one weapon-fixed point (the objective) through both pipelines,
+ // a steady delta while sway swings = static projection mismatch, delta tracking sway = lag
+ const float disc_px_probe = (params.svp_disc_px > 1.f)
+ ? params.svp_disc_px : (float)Device.dwHeight / _max(ratio_magnification, 1.f);
+ if (params.svp_fov > EPS)
+ {
+ auto to_px = [](const Fvector& w, const Fmatrix& v, const Fmatrix& pr, float W, float H, Fvector2& o) -> bool {
+ Fmatrix vpm; vpm.mul(pr, v);
+ Fvector4 c; vpm.transform(c, {w.x, w.y, w.z, 1});
+ if (!_valid(c.w) || c.w < EPS) return false;
+ o.set((c.x / c.w * 0.5f + 0.5f) * W, (0.5f - c.y / c.w * 0.5f) * H);
+ return true;
+ };
+ float dt_cd = camdir.dotproduct(eyefwd);
+ clamp(dt_cd, -1.f, 1.f);
+ const float sway = rad2deg(acosf(dt_cd));
+ float hud_fov_d = params.svp_fov;
+ extern int ps_r__svp_hud_fov_match;
+ if (ps_r__svp_hud_fov_match == 1 && g_pip_scope_ratio > EPS)
+ {
+ float hf, _a2, _n2, _f2;
+ Device.matrices[0].mProjectHud.decompose_projection(hf, _a2, _n2, _f2);
+ if (hf > EPS) hud_fov_d = 2.f * atanf(tanf(hf * 0.5f) / g_pip_scope_ratio);
+ }
+ Fmatrix hp; hp.build_projection(hud_fov_d, params.svp_aspect, 0.10f, params.svp_far);
+ Fmatrix hv = Device.matrices[1].mView;
+ Fvector hvd; hvd.sub(params.eyepiece.m_W.c, m_W_svpcam.c);
+ if (ps_r__svp_hud_fov_match && hvd.magnitude() > 0.01f)
+ hv.build_camera(m_W_svpcam.c, params.eyepiece.m_W.c, params.eyepiece.m_W.j);
+ Fvector2 px_out, uv_in, px_disc;
+ if (to_px(params.objective.m_W.c, Device.matrices[0].mView, Device.matrices[0].mProjectHud, float(Device.dwWidth), float(Device.dwHeight), px_out)
+ && to_px(params.objective.m_W.c, hv, hp, 1.f, 1.f, uv_in)
+ && to_px(params.eyepiece.m_W.c, Device.matrices[0].mView, Device.matrices[0].mProjectHud, float(Device.dwWidth), float(Device.dwHeight), px_disc))
+ {
+ const float bx = px_disc.x + (uv_in.x - 0.5f) * disc_px_probe;
+ const float by = px_disc.y + (uv_in.y - 0.5f) * disc_px_probe;
+ // near-field coc at the objective probe from the near-blur thin-lens formula, in svp px
+ // covered means the blur radius spans the pipeline mismatch at the disc rim
+ extern float ps_r__svp_near_blur, ps_r__svp_focus_m;
+ const float omm = Device.m_SecondViewport.svp_opt_obj_mm;
+ const float A = (omm > 0.01f) ? omm * 0.001f : 0.024f;
+ const float vfov = (Device.m_SecondViewport.svp_fov > 0.01f) ? Device.m_SecondViewport.svp_fov : 0.35f;
+ const float k = A * (float)Device.svp_height() / vfov * _min(ps_r__svp_near_blur, 3.f);
+ const float z = od.dotproduct(eyefwd);
+ const float coc = k * _max(1.f / _max(z, 0.05f) - 1.f / _max(ps_r__svp_focus_m, 1.f), 0.f);
+ Fvector2 rdir; rdir.set(uv_in.x - 0.5f, uv_in.y - 0.5f);
+ const float rlen = sqrtf(rdir.x * rdir.x + rdir.y * rdir.y);
+ Fvector2 rim;
+ if (rlen > EPS)
+ rim.set(px_disc.x + rdir.x / rlen * 0.5f * disc_px_probe, px_disc.y + rdir.y / rlen * 0.5f * disc_px_probe);
+ else
+ rim.set(px_disc.x, px_disc.y);
+ const float mismatch = sqrtf((bx - px_out.x) * (bx - px_out.x) + (by - px_out.y) * (by - px_out.y));
+ const int covered = (coc >= mismatch) ? 1 : 0;
+ PipMsg("[SVP-BARREL] sway=%.2fdeg fovmatch=%d out=(%.0f,%.0f) in=(%.0f,%.0f) delta=(%.0f,%.0f)px rim=(%.0f,%.0f) coc=%.0fpx mismatch=%.0fpx covered=%d",
+ sway, ps_r__svp_hud_fov_match, px_out.x, px_out.y, bx, by, bx - px_out.x, by - px_out.y,
+ rim.x, rim.y, coc, mismatch, covered);
+ }
+ }
+ }
+ }
+
+ // pip [SVP-AIM] composite-centering log (r__svp_cop_diag 2): the scope disc projected on screen
+ // vs the screen center, the lens compositing alignment, not a ballistic statement
+ if (ps_r__svp_cop_diag >= 2 && params.eyepiece.radius > EPS)
+ {
+ Fmatrix vpm; vpm.mul(Device.matrices[0].mProjectHud, Device.matrices[0].mView);
+ Fvector4 rc; vpm.transform(rc, {params.eyepiece.m_W.c.x, params.eyepiece.m_W.c.y, params.eyepiece.m_W.c.z, 1});
+ if (rc.w > EPS)
+ {
+ const float rx = (rc.x / rc.w * 0.5f + 0.5f) * float(Device.dwWidth);
+ const float ry = (0.5f - rc.y / rc.w * 0.5f) * float(Device.dwHeight);
+ const float dx = rx - float(Device.dwWidth) * 0.5f;
+ const float dy = ry - float(Device.dwHeight) * 0.5f;
+ PipMsg("[SVP-AIM] disc=(%.1f,%.1f) d=(%.1f,%.1f)px |d|=%.1f", rx, ry, dx, dy, sqrtf(dx * dx + dy * dy));
+ }
+ }
+
+ // pip [3DB] overlay, 1 = markers + fire (green) camera (blue) sight (red) rays,
+ // 2 = zeroed departure ray (white), 3 = fading shot tracers
+ {
+ extern int ps_r__3db_debug;
+ if (ps_r__3db_debug > 0 && (params.eyepiece.radius > EPS || params.dbg_eyepiece_r > EPS))
+ {
+ auto& vp = Device.m_SecondViewport;
+ // the sight line from the stable published copy, else the eyepiece axis
+ Fvector s_org, saxis;
+ if (vp.svp_sight_ok)
+ {
+ s_org.set(vp.svp_sight_pos);
+ saxis.set(vp.svp_sight_dir);
+ }
+ else
+ {
+ s_org.set(params.eyepiece.m_W.c);
+ saxis.set(params.eyepiece.m_W.k);
+ }
+ saxis.normalize_safe();
+ const bool has_obj = (params.objective.radius > EPS || params.dbg_objective_r > EPS);
+ svp_3db_overlay(fNearPlane, true, s_org, saxis, true, params.eyepiece.m_W.c, has_obj, params.objective.m_W.c);
+ }
+ }
+
+ // pip one-shot config fingerprint on the first scoped frame so any tester log diffs against ours
+ {
+ extern int ps_r__svp_report;
+ static bool s_cfg_logged = false;
+ if (!s_cfg_logged || ps_r__svp_report)
+ {
+ s_cfg_logged = true;
+ ps_r__svp_report = 0;
+ // build fingerprint header first, testers diff their log against this
+ PipMsg("[SVP-CFG] build %s mode=%d", __DATE__, scope_svp_enabled);
+ // generate the cvar list from the console registry so it never drifts from the registered
+ // set, every r__svp_/g_svp_/s3ds_ command with its live value grouped about eight per line
+ if (Console)
+ {
+ char line[1024]; line[0] = 0; u32 grp = 0;
+ for (const auto& c : Console->Commands)
+ {
+ const char* nm = c.second->Name();
+ if (!(0 == strncmp(nm, "r__svp_", 7) || 0 == strncmp(nm, "g_svp_", 6) || 0 == strncmp(nm, "s3ds_", 5)))
+ continue;
+ IConsole_Command::TStatus st; st[0] = 0; c.second->Status(st);
+ char tok[288]; xr_sprintf(tok, "%s%s=%s", grp ? " " : "", nm, st);
+ xr_strcat(line, tok);
+ if (++grp == 8) { PipMsg("[SVP-CFG] %s", line); line[0] = 0; grp = 0; }
+ }
+ if (line[0]) PipMsg("[SVP-CFG] %s", line);
+ // aa state read the same registry way, ssfx_taa is the script-driven taa control
+ IConsole_Command::TStatus a_taa, a_aa, a_ker; a_taa[0] = a_aa[0] = a_ker[0] = 0;
+ auto st_of = [](const char* name, IConsole_Command::TStatus& out) {
+ auto it = Console->Commands.find(name);
+ if (it != Console->Commands.end()) it->second->Status(out);
+ };
+ st_of("ssfx_taa", a_taa); st_of("r2_aa", a_aa); st_of("r2_aa_kernel", a_ker);
+ PipMsg("[SVP-CFG] aa ssfx_taa=%s r2_aa=%s r2_aa_kernel=%s", a_taa, a_aa, a_ker);
+ }
+
+ // pip [SVP-FILES] shader source truth: where each scope file resolves from (loose or which
+ // archive), size and source crc, plus the precompiled-blob and mounted-archive taints
+ {
+ static const char* s_scope_files[] = {
+ "scope_color_write.ps", "scope_3dss_common.h", "scope_common.h",
+ "scope_custom_image.h", "scope_custom_reticle.h", "scope_custom_shadow.h",
+ "scope_custom_lens.h", "mark_adjust.h",
+ "svp_hooks_common.h", "svp_hooks_image.h",
+ "svp_hooks_reticle.h", "svp_hooks_lens.h", "svp_hooks_shadow.h",
+ "models_scope_reticle.ps", "models_scope_reticle_precise.ps",
+ "models_reflex_reticle.ps", "models_reflex_reticle_3db.ps",
+ "models_reflex_reticle_simple.ps", "models_reflex_reticle_simple_3db.ps",
+ "models_reflex_lens.ps", "models_scope_zwrite.ps",
+ "models_scope_reticle.s", "models_scope_reticle_precise.s",
+ "models_reflex_reticle.s", "models_reflex_reticle_3db.s",
+ "models_reflex_reticle_simple.s", "models_reflex_reticle_simple_3db.s",
+ "models_reflex_lens.s", "models_scope_zwrite.s", "models_scope_back.s",
+ "models_scope_reticle.vs", "models_reflex_reticle.vs", "models_reflex_lens.vs",
+ "scope_vertex.vs", "scope_defines.h", "svp_nearblur.ps",
+ "gbuffer_stage.h", "nv_utils.h", "thermal_utils.h" };
+ for (const char* fn : s_scope_files)
+ {
+ string_path rel;
+ strconcat(sizeof(rel), rel, "r3\\", fn);
+ const CLocatorAPI::file* f = FS.exist("$game_shaders$", rel);
+ if (!f)
+ {
+ PipMsg("[SVP-FILES] %-32s MISSING", fn);
+ continue;
+ }
+ u32 crc = 0;
+ char ver[96] = "unmarked";
+ if (IReader* r = FS.r_open("$game_shaders$", rel))
+ {
+ crc = crc32(r->pointer(), r->length());
+ // the resolved source carries our PIP_COMPAT_PATCH tag, a foreign shader that
+ // shadowed ours or a stale pre-marker file reads unmarked
+ const char* p = (const char*)r->pointer();
+ const u32 n = r->length();
+ const char* key = "PIP_COMPAT_PATCH";
+ const u32 klen = (u32)xr_strlen(key);
+ for (u32 i = 0; n > klen && i <= n - klen; ++i)
+ if (0 == strncmp(p + i, key, klen))
+ {
+ u32 j = i + klen, k = 0;
+ while (j < n && p[j] != '\r' && p[j] != '\n' && k < sizeof(ver) - 1)
+ ver[k++] = p[j++];
+ ver[k] = 0;
+ break;
+ }
+ FS.r_close(r);
+ }
+ const char* src = "loose";
+ if (f->vfs != 0xffffffff && f->vfs < FS.m_archives.size())
+ src = FS.m_archives[f->vfs].path.c_str();
+ PipMsg("[SVP-FILES] %-32s %6u bytes crc %08x ver[%s] %s", fn, f->size_real, crc, ver, src);
+ }
+ // shipped precompiled blobs load with no source crc and shadow every source edit
+ FS_FileSet blobs;
+ string_path bdir;
+ FS.update_path(bdir, "$game_shaders$", "r3\objects\r4\scope_color_write.ps\\");
+ FS.file_list(blobs, bdir, FS_ListFiles | FS_RootOnly, "*");
+ PipMsg("[SVP-FILES] dispatcher precompiled blobs %u, rs_precompiled_shaders %d",
+ (u32)blobs.size(), psDeviceFlags2.test(rsPrecompiledShaders) ? 1 : 0);
+ u32 n_arch = 0;
+ for (const auto& A : FS.m_archives)
+ if (strstr(A.path.c_str(), "mods"))
+ {
+ PipMsg("[SVP-FILES] mounted mods archive %s", A.path.c_str());
+ ++n_arch;
+ }
+ PipMsg("[SVP-FILES] mods archives mounted %u", n_arch);
+ }
+ }
+ }
+
+ // pip [SVP-LEDGER] once-per-session inert-path sweep, after enough scoped time to exercise the
+ // gated paths, flags any instrumented counter still zero while its gate cvar is on
+ {
+ extern void svp_ledger_sweep();
+ const float SVP_LEDGER_SETTLE_S = 60.f; // diagnostic patience window in scoped seconds, not a rendering threshold
+ static float s_ledger_scoped_s = 0.f;
+ static bool s_ledger_swept = false;
+ if (!s_ledger_swept)
+ {
+ s_ledger_scoped_s += Device.fTimeDelta;
+ if (s_ledger_scoped_s >= SVP_LEDGER_SETTLE_S)
+ {
+ s_ledger_swept = true;
+ svp_ledger_sweep();
+ }
+ }
+ }
+}
+
+// pip front/second focal-plane world points (scope_w_ffp/sfp); the scope shader projects the SVP image through them
+void ffp_sfp()
+{
+ auto e = Device.m_SecondViewport.eyepiece;
+ auto o = Device.m_SecondViewport.objective;
+
+ Fvector p_e = {0, 0, 0}; e.m_W.transform(p_e);
+ Fvector p_o = {0, 0, 0}; o.m_W.transform(p_o);
+
+ if (o.radius < EPS)
+ {
+ // no objective captured, place one at the authored-set front distance in eyepiece radii
+ // (mod_system_3dss_objective_lenses median 12.7 mean 14.0, same 14r as the geomscan fallback)
+ float distance = e.radius * 14.0f;
+ o.radius = e.radius;
+ p_o = {0, 0, distance};
+ e.m_W.transform(p_o);
+ }
+
+ {
+ // reproject the objective directly in front of the eyepiece (not all scopes are inline)
+ float distance = p_o.distance_to(p_e);
+ Fvector dir = {0, 0, 1};
+ o.m_W.transform_dir(dir);
+ p_o.set(dir.mul(distance).add(p_e));
+ }
+
+ Fvector p_d = Fvector(p_o).sub(p_e);
+
+ // each focal anchor sits one eyepiece focal length inside its end of the tube (f_e ~ the
+ // authored eye relief, symmetric relay), the old 0.2/0.8 split stays as the cvar fallback
+ float t1 = 0.2f, t2 = 0.8f;
+ {
+ extern int ps_r__svp_focal_derive;
+ extern Fvector4 ps_s3ds_param_1;
+ const float L = p_d.magnitude();
+ const float fe = ps_s3ds_param_1.y * 0.01f;
+ if (ps_r__svp_focal_derive && L > EPS && fe > 0.01f)
+ {
+ t1 = clampr(fe / L, 0.05f, 0.45f);
+ t2 = clampr(1.f - fe / L, 0.55f, 0.95f);
+ }
+ }
+ Device.m_SecondViewport.w_ffp = Fvector(p_d).mul(t1).add(p_e);
+ Device.m_SecondViewport.w_sfp = Fvector(p_d).mul(t2).add(p_e);
+}
+
+// pip derive the eyepiece/objective from the captured scope-lens meshes, consumed by
+// svpCamera and the activation gate (GetSVPCameraMatrix)
+static xr_vector g_pip_hud_geom; // pip diag: snapshot of HUD geometry centers (xyz) + radius (w), captured before render_hud clears the lists
+
+// pip snapshot HUD geometry centers before render_hud clears the lists, deriveScopeLens
+// geomscans them after the clear, the call site keeps its gate inline
+void svp_snapshot_hud_geom()
+{
+ g_pip_hud_geom.clear();
+ auto snap = [](auto& lst) { for (auto& H : lst) { if (!H.pVisual || !H.pMatrix) continue; auto& VV = H.pVisual->getVisData(); Fvector w; H.pMatrix->transform_tiny(w, VV.sphere.P); Fvector4 e; e.set(w.x, w.y, w.z, VV.sphere.R); g_pip_hud_geom.push_back(e); } };
+ snap(RImplementation.GMBase.RGraph.mapHUDSorted.Sorted);
+ snap(RImplementation.GMBase.RGraph.mapHUD);
+}
+
+// pip one owner of the current optic identity, the lens visual and sphere radius mark the aimed
+// optic, a magnifier flip or scope swap changes them with no frame gap so the subscribers reseed
+static u32 svp_epoch_update(const void* lens_visual, float lens_radius)
+{
+ // relative eyepiece-radius jump marking a different optic, migrated from the ratio reseed seed
+ const float optic_change = 0.05f;
+ static const void* s_prev_vis = nullptr;
+ static float s_prev_r = 0.f;
+ static u32 s_epoch = 0;
+ if (lens_radius <= EPS)
+ return s_epoch; // no valid lens this frame holds the identity
+ const bool have_prev = (s_prev_vis != nullptr) && (s_prev_r > EPS);
+ const bool vis_change = have_prev && (lens_visual != s_prev_vis);
+ const bool r_change = have_prev && (_abs(lens_radius - s_prev_r) > s_prev_r * optic_change);
+ if (vis_change || r_change)
+ {
+ ++s_epoch;
+ extern int ps_r__svp_cop_diag;
+ if (ps_r__svp_cop_diag)
+ PipMsg("[SVP-EPOCH] %u %s r %.2f->%.2fcm", s_epoch,
+ vis_change ? (r_change ? "both" : "visual") : "radius",
+ s_prev_r * 100.f, lens_radius * 100.f);
+ }
+ s_prev_vis = lens_visual;
+ s_prev_r = lens_radius;
+ return s_epoch;
+}
+
+// pip the optics bus, resolve the per-optic inputs once from the fresh eyepiece so one precedence
+// and one eps gate feed every consumer, authored_optics folds into the offset, mm from spec then w
+static void svp_optics_resolve(CSecondVPParams* p, float er)
+{
+ extern int ps_r__svp_authored_optics;
+ extern Fvector4 scope_objective_lens_offset;
+ extern float ps_s3ds_objective_mm;
+ Fvector4 off;
+ if (ps_r__svp_authored_optics) off = scope_objective_lens_offset;
+ else off.set(0.f, 0.f, 0.f, 0.f);
+ p->svp_opt_offset = off;
+ float mm = 0.f;
+ if (ps_s3ds_objective_mm > EPS)
+ mm = ps_s3ds_objective_mm; // spec-sheet clear aperture
+ else if (off.w > EPS)
+ mm = 2000.f * off.w * er; // authored w radius in eyepiece radii to mm
+ p->svp_opt_obj_mm = mm;
+ extern int ps_r__svp_stats; extern u32 svp_stats_optic_resolve;
+ if (ps_r__svp_stats) ++svp_stats_optic_resolve;
+ extern int ps_r__svp_cop_diag;
+ if (ps_r__svp_cop_diag)
+ {
+ static u32 s_opt_ms = 0;
+ if (Device.dwTimeGlobal - s_opt_ms > 1000)
+ {
+ s_opt_ms = Device.dwTimeGlobal;
+ PipMsg("[SVP-OPTICS] epoch %u off %.2f,%.2f,%.2f,%.2f mm %.1f src %s", p->svp_optic_epoch,
+ off.x, off.y, off.z, off.w, mm,
+ (ps_s3ds_objective_mm > EPS) ? "spec" : ((off.w > EPS) ? "w" : "none"));
+ }
+ }
+}
+
+void CRender::deriveScopeLens()
+{
+ // pip clear the flat-panel publish, re-set below when a flat window lens is the eyepiece
+ Device.m_SecondViewport.svp_panel_flat = false;
+
+ // multi-lens weapons carry several scope-lens meshes, pick the aimed one, a visible
+ // lens bone nearest the camera ray
+ const void* best = nullptr;
+ {
+ extern int ps_r__svp_cop_diag;
+ static u32 s_lens_diag_ms = 0;
+ const bool diag = ps_r__svp_cop_diag && (Device.dwTimeGlobal - s_lens_diag_ms > 3000);
+ if (diag)
+ s_lens_diag_ms = Device.dwTimeGlobal;
+ float best_score = 1e9f;
+ const Fvector cam_p = Device.vCameraPosition;
+ const Fvector cam_f = Device.vCameraDirection;
+ for (auto& N : GMBase.RGraph.mapScopeHUDSorted)
+ {
+ if (!N.pVisual || !N.pMatrix)
+ continue;
+ Fmatrix lensX = *GMBase.svp_pose_of(N.pMatrix);
+ bool bone_vis = true;
+ CSkeletonX* sk = fast_dynamic_cast(N.pVisual);
+ if (sk)
+ {
+ bone_vis = sk->SVP_LensBoneVisible();
+ Fmatrix boneR;
+ if (GMBase.svp_lens_bone_of(N.pVisual, boneR) || sk->SVP_LensBoneXform(boneR))
+ lensX.mulB_43(boneR);
+ }
+ auto& V = N.pVisual->getVisData();
+ Fvector c; V.box.getcenter(c);
+ Fvector cw; lensX.transform_tiny(cw, c);
+ Fvector d; d.sub(cw, cam_p);
+ const float dist = d.magnitude();
+ if (dist < 0.01f)
+ continue;
+ d.div(dist);
+ const float fwd = d.dotproduct(cam_f);
+ if (diag)
+ {
+ auto tx = N.pVisual->GetTexture();
+ Fvector bd; V.box.getsize(bd);
+ PipMsg("[SVP-LENS] %s dist=%.1fcm fwd=%.3f r=%.1fcm box=%.1fx%.1fx%.1fcm %s",
+ bone_vis ? "vis " : "HIDE", dist * 100.f, fwd, V.sphere.R * 100.f,
+ bd.x * 100.f, bd.y * 100.f, bd.z * 100.f,
+ tx ? tx->cName.c_str() : "?");
+ }
+ if (!bone_vis || fwd < 0.2f)
+ continue;
+ const float score = (1.f - fwd) + dist * 0.02f;
+ if (score < best_score)
+ {
+ best_score = score;
+ best = &N;
+ }
+ }
+ }
+
+ // pip diag, an aimed frame that derives no lens (hip frames also derive, gate on the raise)
+ if (!best)
+ {
+ extern int ps_r__svp_diag;
+ if (ps_r__svp_diag && g_pGamePersistent
+ && g_pGamePersistent->m_pGShaderConstants->hud_params.x > 0.05f)
+ {
+ static u32 s_nb_ms = 0;
+ if (Device.dwTimeGlobal - s_nb_ms > 500)
+ {
+ s_nb_ms = Device.dwTimeGlobal;
+ PipMsg("[SVP-LENS] no aimed lens picked, nodes=%u stale_r=%.1fcm",
+ (u32)GMBase.RGraph.mapScopeHUDSorted.size(),
+ Device.m_SecondViewport.dbg_eyepiece_r * 100.f);
+ }
+ }
+ }
+
+ for (auto& N : GMBase.RGraph.mapScopeHUDSorted)
+ {
+ if (&N != best)
+ continue;
+
+ // a skinned scope lens is positioned by its bone, the captured matrix is only the kinematics
+ // root, fold in the lens bone skinning matrix so the eyepiece follows the glass on ADS and sway
+ Fmatrix lensX = *GMBase.svp_pose_of(N.pMatrix);
+ CSkeletonX* sk = fast_dynamic_cast(N.pVisual);
+ if (sk)
+ {
+ Fmatrix boneR;
+ if (GMBase.svp_lens_bone_of(N.pVisual, boneR) || sk->SVP_LensBoneXform(boneR))
+ lensX.mulB_43(boneR);
+ }
+
+ auto& V = N.pVisual->getVisData();
+ Fvector c;
+ V.box.getcenter(c); // AABB center fits a flat lens disc tighter than the bounding sphere center
+ Fmatrix m_W = lensX;
+ m_W.mulB_43(Fmatrix().translate(c));
+ float radius = V.sphere.R;
+
+ auto* p = &Device.m_SecondViewport;
+
+ // publish the optic epoch from the fresh lens identity, before the pose hold so a swap the
+ // hold would reject still bumps it, the visual and radius are pose invariant per optic
+ p->svp_optic_epoch = svp_epoch_update(N.pVisual, radius);
+
+ // while aimed the eyepiece cannot lurch far from the camera, a stale or exo-arm pose reads
+ // double the distance and whips the fit, hold the last good pose instead
+ extern int ps_r__svp_lens_reject;
+ static Fmatrix s_eyep_hold; static float s_eyep_holdr = 0.f, s_eyep_holdd = 0.f;
+ static u32 s_eyep_frame = 0; static bool s_eyep_ok = false;
+ static u32 s_eyep_epoch = 0;
+ // a session gap or an optic swap drops the hold so the new optic's fresh pose is taken
+ if (Device.dwFrame != s_eyep_frame + 1 || p->svp_optic_epoch != s_eyep_epoch) s_eyep_ok = false;
+ s_eyep_frame = Device.dwFrame;
+ s_eyep_epoch = p->svp_optic_epoch;
+ const float aim_x = g_pGamePersistent ? g_pGamePersistent->m_pGShaderConstants->hud_params.x : 0.f;
+ const float new_d = m_W.c.distance_to(Device.vCameraPosition);
+ if (ps_r__svp_lens_reject && s_eyep_ok && aim_x > 0.9f && new_d > s_eyep_holdd * 1.6f)
+ {
+ m_W = s_eyep_hold;
+ radius = s_eyep_holdr;
+ }
+ else
+ {
+ s_eyep_hold = m_W; s_eyep_holdr = radius; s_eyep_holdd = new_d; s_eyep_ok = true;
+ }
+
+ p->eyepiece.m_W = m_W;
+ p->eyepiece.radius = radius;
+
+ // resolve the optics bus from the fresh eyepiece, the consumers below and downstream read
+ // the record instead of the raw cvars
+ svp_optics_resolve(p, radius);
+
+ // ballistics sight line, a whole stable copy, logic fires while this frame re-derives
+ if (radius > EPS)
+ {
+ p->svp_sight_pos.set(m_W.c);
+ Fvector sk;
+ sk.set(m_W.k);
+ sk.normalize_safe();
+ p->svp_sight_dir.set(sk);
+ p->svp_lens_r = radius;
+ p->svp_sight_frame = Device.dwFrame;
+ p->svp_sight_ok = true;
+ }
+
+ // panel aspect from the lens AABB, the two largest extents are the plane W:H (the ~0 axis is
+ // the depth), ~1 for a round lens so a conventional scope keeps its square SVP
+ Fvector bsz; V.box.getsize(bsz);
+ float ea[3] = { _abs(bsz.x), _abs(bsz.y), _abs(bsz.z) };
+ if (ea[0] < ea[1]) { float t = ea[0]; ea[0] = ea[1]; ea[1] = t; }
+ if (ea[1] < ea[2]) { float t = ea[1]; ea[1] = ea[2]; ea[2] = t; }
+ if (ea[0] < ea[1]) { float t = ea[0]; ea[0] = ea[1]; ea[1] = t; }
+ p->svp_panel_aspect = (ea[1] > EPS) ? (ea[0] / ea[1]) : 1.f;
+
+ // pip flat-panel on-screen quad for the binocular brackets, plane half-extent world vectors
+ // (mesh local x = width, y = height, z = normal)
+ {
+ extern int ps_r__svp_flat_window;
+ extern Fvector4 ps_s3ds_param_3;
+ if (ps_r__svp_flat_window && (int)ps_s3ds_param_3.y == 8)
+ {
+ Fvector we, he;
+ m_W.transform_tiny(we, Fvector().set(bsz.x * 0.5f, 0.f, 0.f));
+ m_W.transform_tiny(he, Fvector().set(0.f, bsz.y * 0.5f, 0.f));
+ p->svp_panel_ax_w.sub(we, m_W.c);
+ p->svp_panel_ax_h.sub(he, m_W.c);
+ p->svp_panel_flat = true;
+ }
+ }
+
+ if (p->eyepiece.radius > EPS)
+ {
+ // pip objective, prefer the real captured front lens on the optical axis, the
+ // geomscan finds the forward-most on-axis node as the fallback plane
+ float geom_front = -1.f;
+ {
+ const Fvector eye = p->eyepiece.m_W.c;
+ Fvector axis; axis.set(p->eyepiece.m_W.k); axis.normalize();
+ const float rr = p->eyepiece.radius;
+ if (rr > EPS)
+ {
+ float fr = -1e9f;
+ for (auto& g : g_pip_hud_geom)
+ {
+ Fvector wc; wc.set(g.x, g.y, g.z);
+ Fvector d; d.sub(wc, eye);
+ const float fwd = d.dotproduct(axis);
+ if (fwd <= 0.f) continue;
+ Fvector proj; proj.mad(eye, axis, fwd);
+ const float perp = wc.distance_to(proj);
+ if (perp < rr * 2.0f && (fwd + g.w) > fr) fr = fwd + g.w;
+ }
+ if (fr > 0.f) { geom_front = fr / rr; if (geom_front < 4.f) geom_front = 4.f; else if (geom_front > 30.f) geom_front = 30.f; }
+ }
+ }
+
+ bool have_obj = false;
+ float dbg_cand_dist = -1.f; // objective-capture distance, used by the capture gate below
+ // authored objective wins, place the front lens at the resolved z along the optical axis
+ // with the lateral x/y offset and w radius, all in eyepiece radii
+ const Fvector4& off = p->svp_opt_offset;
+ if (off.z > EPS)
+ {
+ const float er = p->eyepiece.radius;
+ Fvector fwd; fwd.set(p->eyepiece.m_W.k); fwd.normalize();
+ Fvector ri; ri.set(p->eyepiece.m_W.i); ri.normalize();
+ Fvector up; up.set(p->eyepiece.m_W.j); up.normalize();
+ p->objective.m_W = p->eyepiece.m_W;
+ p->objective.m_W.c.mad(fwd, off.z * er);
+ p->objective.m_W.c.mad(ri, off.x * er);
+ p->objective.m_W.c.mad(up, off.y * er);
+ p->objective.radius = (off.w > EPS)
+ ? off.w * er : p->eyepiece.radius * ps_r__svp_obj_size;
+ have_obj = true;
+ }
+ for (auto& N : GMBase.RGraph.mapScopeHUDObjective)
+ {
+ if (have_obj)
+ break; // authored objective already placed, mesh detection is the next fallback
+ if (!N.pVisual || !N.pMatrix)
+ break;
+ Fmatrix oX = *GMBase.svp_pose_of(N.pMatrix);
+ if (CSkeletonX* sk = fast_dynamic_cast(N.pVisual))
+ {
+ Fmatrix boneR;
+ if (GMBase.svp_lens_bone_of(N.pVisual, boneR) || sk->SVP_LensBoneXform(boneR))
+ oX.mulB_43(boneR);
+ }
+ auto& OV = N.pVisual->getVisData();
+ Fvector oc; OV.box.getcenter(oc);
+ Fvector ow; oX.transform_tiny(ow, oc);
+ dbg_cand_dist = ow.distance_to(p->eyepiece.m_W.c);
+ // distinct from the eyepiece (a single-lens scope captures the same disc for both)
+ if (OV.sphere.R > EPS && dbg_cand_dist > p->eyepiece.radius * 0.5f)
+ {
+ p->objective.m_W = p->eyepiece.m_W; // stabilized optical axis
+ p->objective.m_W.c.set(ow); // real front-lens world position
+ p->objective.radius = OV.sphere.R;
+ have_obj = true;
+ }
+ break;
+ }
+ if (!have_obj)
+ {
+ // no distinct objective mesh, derive it geometrically along the optical axis,
+ // the eyepiece radius is the only mesh-scale-robust unit
+ Fvector fwd; fwd.set(p->eyepiece.m_W.k); fwd.normalize();
+ p->objective.m_W = p->eyepiece.m_W;
+ // 14r fallback = the authored-set front distance (mod_system_3dss_objective_lenses mean, median 12.7)
+ const float dist_r = (geom_front > 0.f ? geom_front : 14.0f) * ps_r__svp_obj_dist;
+ p->objective.m_W.c.mad(fwd, p->eyepiece.radius * dist_r);
+ p->objective.radius = p->eyepiece.radius * ps_r__svp_obj_size; // eyepiece-relative objective radius (one global knob): the front-node geomscan swung ~6x conflating glass vs bell housing, eyepiece radius is the only mesh-scale-stable unit
+ }
+ ffp_sfp(); // focal-plane points for the scope shader
+
+ // pip [SVP-DETECT]/[SVP-EYEDIV] one-shot a/b, measured mesh geometry vs authored ltx vs
+ // the live derivation, off unless r__svp_diag, latched per aimed optic
+ {
+ extern int ps_r__svp_diag;
+ if (ps_r__svp_diag)
+ {
+ static const void* s_detect_last = nullptr;
+ if (N.pVisual != s_detect_last)
+ {
+ extern int ps_r__svp_measured_optics;
+ const float er = p->eyepiece.radius;
+ // authored objective resolved by the optics bus, no second precedence copy
+ const float auth_w = p->svp_opt_offset.w;
+ const float auth_mm = p->svp_opt_obj_mm;
+ // live objective this frame, forward distance and radius in eyepiece radii
+ Fvector le; le.sub(p->objective.m_W.c, p->eyepiece.m_W.c);
+ const float live_z = (er > EPS) ? le.magnitude() / er : 0.f;
+ const float live_w = (er > EPS) ? p->objective.radius / er : 0.f;
+ // a weapon transition frame can carry a garbage pose, nan or inf skips the
+ // latch so the next frame retries with a real one
+ if (_valid(er) && _valid(live_z) && _valid(live_w))
+ {
+ s_detect_last = N.pVisual;
+ SLensDetection d;
+ const bool have = sk && sk->SVP_GetLensDetection(d);
+ PipMsg("[SVP-DETECT] switch=%d er=%.4f | authored z=%.2f w=%.3f mm=%.1f | live z=%.2f w=%.3f | detected ok=%d z=%.2f w=%.3f mm=%.1f src=%d",
+ ps_r__svp_measured_optics, er,
+ p->svp_opt_offset.z, auth_w, auth_mm,
+ live_z, live_w,
+ (int)(have && d.ok),
+ (have && d.ok) ? d.offset.z : 0.f,
+ (have && d.ok) ? d.offset.w : 0.f,
+ (have && d.ok) ? d.mm : 0.f,
+ (have && d.ok) ? d.source : -1);
+ // live vs detected eyepiece radius, surfaces the near-bone contamination that skews
+ // the analytic zoom ratio, read-only, the override stays deferred
+ const float det_r = (have && d.ok) ? d.eye_radius : 0.f;
+ const float eratio = (det_r > EPS && er > EPS) ? er / det_r : 0.f;
+ PipMsg("[SVP-EYEDIV] live_r=%.4f detected_r=%.4f ratio=%.2f src=%d %s",
+ er, det_r, eratio, (have && d.ok) ? d.source : -1,
+ (eratio > 1.4f || (eratio > EPS && eratio < 0.7f)) ? "DIVERGE" : "agree");
+ }
+ }
+ }
+ }
+ }
+ break; // the first captured lens is the eyepiece
+ }
+
+ // pip DLSS reset when the lens first becomes valid (active can flip a frame before the capture),
+ // render-thread edge state, inert at gate 0
+ bool lens_valid = (Device.m_SecondViewport.eyepiece.radius > EPS);
+ if (lens_valid && !Device.m_SecondViewport.m_lens_prev_valid)
+ Device.m_SecondViewport.dlss_reset_next = true;
+ Device.m_SecondViewport.m_lens_prev_valid = lens_valid;
+}
diff --git a/src/Layers/xrRenderPC_R4/svp_camera.h b/src/Layers/xrRenderPC_R4/svp_camera.h
new file mode 100644
index 0000000000..3efc1eff47
--- /dev/null
+++ b/src/Layers/xrRenderPC_R4/svp_camera.h
@@ -0,0 +1,13 @@
+#ifndef svp_cameraH
+#define svp_cameraH
+#pragma once
+
+// pip build Device.matrices[1] (the SVP camera) from the captured lens and the weapon zoom,
+// called from renderGBuffer after the hud derive
+void svpCamera();
+
+// pip snapshot HUD geometry centers into the module's geomscan list before render_hud clears
+// the lists, the caller keeps its gate inline
+void svp_snapshot_hud_geom();
+
+#endif
diff --git a/src/Layers/xrRenderPC_R4/svp_nearblur.cpp b/src/Layers/xrRenderPC_R4/svp_nearblur.cpp
new file mode 100644
index 0000000000..2d28f6ec58
--- /dev/null
+++ b/src/Layers/xrRenderPC_R4/svp_nearblur.cpp
@@ -0,0 +1,112 @@
+#include "stdafx.h"
+#include "../xrRender/FBasicVisual.h" // pip dxRender_Visual (GetTexture/Render) for draw_scope
+#include "../xrRender/SkeletonX.h" // pip lens bone latch compensation for the skinned lens draws
+#include "../../xrEngine/igame_persistent.h" // pip env-driven eye pupil for the exit-pupil twilight dimming
+#include "../../xrEngine/environment.h"
+#if defined(USE_DX11)
+#include "../../../gamedata/shaders/r3/scope_defines.h" // SCOPE_PHASE_* (kept in sync with the shader)
+#endif
+
+#if defined(USE_DX11)
+
+// per-scope objective diameter in mm, resolved by the optics bus (spec cvar then authored w), 0 = none
+static float svp_objective_mm()
+{
+ return Device.m_SecondViewport.svp_opt_obj_mm;
+}
+
+// pip electronic overlay actually on screen, NV shows only with its overlay on (markswitch 0),
+// thermal shows until the overlay is dropped (markswitch < 2)
+static bool svp_overlay_active(float param3x, int markswitch)
+{
+ return (param3x >= 0.5f) && ((param3x < 1.5f) ? (markswitch == 0) : (markswitch < 2));
+}
+// pip thermal-typed optic with its overlay on, near-blur skips these so they keep full DoF
+static bool svp_thermal_active(float param3x, int markswitch)
+{
+ return (param3x >= 1.5f) && svp_overlay_active(param3x, markswitch);
+}
+
+// pip near-field defocus pass, true when it dispatched else the caller copies
+bool CRenderTarget::svp_nearblur_pass()
+{
+ // pip near-field defocus replaces the plain copy in the realism mode, thermal sensors keep
+ // full depth of field so they take the plain copy
+ extern float ps_r__svp_near_blur;
+ extern Fvector4 ps_s3ds_param_3;
+ extern int ps_markswitch_current;
+ // thermal keeps full DoF only while its overlay is on, markswitch >= 2 falls back to a plain
+ // relayed image that should defocus like any optic
+ const bool thermal_active = svp_thermal_active(ps_s3ds_param_3.x, ps_markswitch_current);
+ if (ps_r__svp_near_blur > 0.01f && scope_svp_enabled >= 2 && !thermal_active
+ && rt_Position && rt_secondVP->pRT)
+ {
+ EnsureScopeShaders();
+ if (s_svp_nearblur)
+ {
+ // pip prefilter the svp color into a mip chain so each sparse ring area-averages its cell
+ if (m_svp_nb_mip_surf)
+ {
+ HW.pContext->CopySubresourceRegion(m_svp_nb_mip_surf, 0, 0, 0, 0, rt_Generic_0->pSurface, 0, nullptr);
+ HW.pContext->GenerateMips(m_svp_nb_mip_tex->get_SRView());
+ }
+ {
+ ref_texture t; t.create("$user$svp_nearblur_src");
+ t->surface_set(m_svp_nb_mip_surf ? m_svp_nb_mip_surf : rt_Generic_0->pSurface);
+ }
+ {
+ ref_texture t; t.create("$user$svp_nearblur_pos");
+ t->surface_set(rt_Position->pSurface);
+ }
+ u_setrt(Width, Height, rt_secondVP->pRT, nullptr, nullptr, nullptr);
+ const float w = float(Width), h = float(Height);
+ u32 Offset = 0;
+ const float d_Z = EPS_S, d_W = 1.f;
+ const u32 C = color_rgba(255, 255, 255, 255);
+ FVF::TL* pv = (FVF::TL*)RCache.Vertex.Lock(3, g_combine->vb_stride, Offset);
+ pv->set(0, h * 2, d_Z, d_W, C, 0.f, 2.f); pv++;
+ pv->set(0, 0, d_Z, d_W, C, 0.f, 0.f); pv++;
+ pv->set(w * 2, 0, d_Z, d_W, C, 2.f, 0.f); pv++;
+ RCache.Vertex.Unlock(3, g_combine->vb_stride);
+ RCache.set_Geometry(g_combine);
+ RCache.set_Element(s_svp_nearblur->E[1]);
+ // set_c is stateful, screen_res must be the SVP dims for the SV_Position uv
+ RCache.set_c("screen_res", w, h, 1.f / w, 1.f / h);
+ // near-field defocus by the real thin-lens CoC of the objective aperture
+ // y = k in px*m (CoC px = k * (1/z - 1/focus))
+ {
+ extern float ps_r__svp_focus_m;
+ const float omm = svp_objective_mm();
+ const float A = (omm > 0.01f) ? omm * 0.001f : 0.024f; // typical 24mm when no per-scope data
+ const float vfov = (Device.m_SecondViewport.svp_fov > 0.01f) ? Device.m_SecondViewport.svp_fov : 0.35f;
+ const float k = A * h / vfov * _min(ps_r__svp_near_blur, 3.f);
+ const float focus = _max(ps_r__svp_focus_m, 1.f);
+ // the near field ends at the real muzzle, per weapon, clamped to the old design bound
+ float md = 1.5f;
+ if (Device.m_SecondViewport.muzzle_pos.square_magnitude() > EPS)
+ md = clampr(Device.m_SecondViewport.muzzle_pos.distance_to(Device.vCameraPosition), 0.5f, 3.f);
+ // max radius is the physical coc at the svp near plane capped at the inscribed disc
+ const float z_near = Device.m_SecondViewport.svp_near;
+ const float coc_near = k * _max(1.f / _max(z_near, 0.05f) - 1.f / focus, 0.f);
+ const float maxR = _min(coc_near, 0.5f * _min(w, h));
+ RCache.set_c("svp_nearblur_params", focus, k, maxR, md);
+ // pip one-shot proof that the pass ran, throttled while r__svp_diag is on
+ extern int ps_r__svp_diag;
+ extern int ps_r__svp_nearblur_scatter;
+ static u32 s_nb_ms = 0;
+ if (ps_r__svp_diag && Device.dwTimeGlobal - s_nb_ms > 1000)
+ {
+ s_nb_ms = Device.dwTimeGlobal;
+ const float g_dbg = powf(_max(maxR, 1.f), 1.f / 150.f) - 1.f; // 150 mirrors the shader NB_TAP_BUDGET
+ u32 mips = 0;
+ if (m_svp_nb_mip_surf) { D3D_TEXTURE2D_DESC d; m_svp_nb_mip_surf->GetDesc(&d); mips = d.MipLevels; }
+ PipMsg("[SVP-NB] maxR=%.0fpx g=%.4f lodk=%.4f scatter=%d mips=%u", maxR, g_dbg, g_dbg, ps_r__svp_nearblur_scatter, mips);
+ }
+ }
+ RCache.Render(D3DPT_TRIANGLELIST, Offset, 0, 3, 0, 1);
+ return true;
+ }
+ }
+ return false;
+}
+#endif
diff --git a/src/Layers/xrRenderPC_R4/svp_optics.cpp b/src/Layers/xrRenderPC_R4/svp_optics.cpp
new file mode 100644
index 0000000000..ba66939f8f
--- /dev/null
+++ b/src/Layers/xrRenderPC_R4/svp_optics.cpp
@@ -0,0 +1,956 @@
+#include "stdafx.h"
+#include "../xrRender/FBasicVisual.h" // pip dxRender_Visual (GetTexture/Render) for draw_scope
+#include "../xrRender/SkeletonX.h" // pip lens bone latch compensation for the skinned lens draws
+#include "../../xrEngine/igame_persistent.h" // pip env-driven eye pupil for the exit-pupil twilight dimming
+#include "../../xrEngine/environment.h"
+#if defined(USE_DX11)
+#include "../../../gamedata/shaders/r3/scope_defines.h" // SCOPE_PHASE_* (kept in sync with the shader)
+#endif
+
+#if defined(USE_DX11) // Redotix99: for 3D Shader Based Scopes (sorry for using the nightvision phase file)
+// pip load the scope glue shaders lazily on first PiP use, they ship in the PiP mod (gamedata/shaders/r3)
+// per-scope objective diameter in mm, resolved by the optics bus (spec cvar then authored w), 0 = none
+static float svp_objective_mm()
+{
+ return Device.m_SecondViewport.svp_opt_obj_mm;
+}
+
+// pip electronic overlay actually on screen, NV shows only with its overlay on (markswitch 0),
+// thermal shows until the overlay is dropped (markswitch < 2)
+static bool svp_overlay_active(float param3x, int markswitch)
+{
+ return (param3x >= 0.5f) && ((param3x < 1.5f) ? (markswitch == 0) : (markswitch < 2));
+}
+// pip thermal-typed optic with its overlay on, near-blur skips these so they keep full DoF
+static bool svp_thermal_active(float param3x, int markswitch)
+{
+ return (param3x >= 1.5f) && svp_overlay_active(param3x, markswitch);
+}
+
+void CRenderTarget::EnsureScopeShaders()
+{
+ if (m_scope_shaders_ready)
+ return;
+ s_scope_color_write.create("scope_color_write");
+ s_scope_depth_write.create("scope_depth_write");
+ s_scope_debug.create("scope_debug");
+ s_svp_nearblur.create("svp_nearblur");
+ s_svp_distort_stamp.create("svp_distort_stamp");
+ s_svp_taa_stamp.create("svp_taa_stamp");
+ m_scope_shaders_ready = true;
+}
+
+// pip r__scope_debug overlay, a top-left grid of the main + SVP views, their ssfx buffers (prev-frame,
+// prev-pos, motion vectors) and the shadow map, main viewport only, binds each $main/$svp RT by name
+void CRenderTarget::phase_scope_debug()
+{
+ if (!scope_debug || Device.m_SecondViewport.IsSVPFrame())
+ return;
+
+ EnsureScopeShaders();
+ if (!s_scope_debug)
+ return;
+
+ // snapshot the finished main view so the overlay can sample it without reading the RT it draws into
+ HW.pContext->CopyResource(rt_secondVP->pSurface, rt_Generic_0->pSurface);
+
+ auto M = RImplementation.TargetMain;
+ auto S = RImplementation.TargetSVP;
+ auto bind = [M](LPCSTR name, ref_rt& rt)
+ {
+ // fall back to a valid RT when the source is absent (no SSS = no ssfx buffers) so the debug
+ // technique bind always resolves to a real texture instead of an unregistered name
+ ref_rt& src = rt ? rt : M->rt_Generic_0;
+ if (!src)
+ return;
+ ref_texture t;
+ t.create(name);
+ // raw pSurface, surface_get would AddRef a reference nobody releases (per-frame leak)
+ t->surface_set(src->pSurface);
+ };
+ bind("$user$viewport2$main", M->rt_secondVP);
+ bind("$user$ssfx_prev_p$main", M->rt_Position); // no MT prev-pos buffer, show the gbuffer position
+ bind("$user$ssfx_motion_vectors$main", M->rt_ssfx_motion_vectors);
+ bind("$user$ssfx_prev_frame$main", M->rt_ssfx_prev_frame);
+ bind("$user$smap_depth", M->rt_smap_depth);
+ if (S)
+ {
+ bind("$user$viewport2$svp", S->rt_secondVP);
+ bind("$user$ssfx_prev_p$svp", S->rt_Position);
+ bind("$user$ssfx_motion_vectors$svp", S->rt_ssfx_motion_vectors);
+ bind("$user$ssfx_prev_frame$svp", S->rt_ssfx_prev_frame);
+ }
+ // the bind cache keys on CTexture identity so the remaps above are invisible to it
+ RCache.Invalidate();
+
+ // draw onto the CURRENT target phase_combine left bound (the final LDR image), not a fresh RT or the
+ // following HUD/UI passes would render offscreen
+ RCache.set_CullMode(CULL_NONE);
+ RCache.set_Stencil(FALSE);
+
+ u32 Offset = 0;
+ u32 C = color_rgba(0, 0, 0, 255);
+ float d_Z = EPS_S;
+ float d_W = 1.0f;
+ float w = float(Device.dwWidth);
+ float h = float(Device.dwHeight);
+
+ // fullscreen triangle, the shader discards everything outside the top-left quarter grid
+ FVF::TL* pv = (FVF::TL*)RCache.Vertex.Lock(3, g_combine->vb_stride, Offset);
+ pv->set(0, h * 2, d_Z, d_W, C, 0.f, 2.f); pv++;
+ pv->set(0, 0, d_Z, d_W, C, 0.f, 0.f); pv++;
+ pv->set(w * 2, 0, d_Z, d_W, C, 2.f, 0.f); pv++;
+ RCache.Vertex.Unlock(3, g_combine->vb_stride);
+
+ RCache.set_Geometry(g_combine);
+ RCache.set_Element(s_scope_debug->E[1]);
+ RCache.Render(D3DPT_TRIANGLELIST, Offset, 0, 3, 0, 1);
+}
+
+// pip zero the geometry stencil in the four dead corners outside the inscribed eyepiece disc so
+// every downstream >=1 test (lights, combine, wallmarks) skips them, no shader or accum edits
+void CRenderTarget::stamp_svp_corner_mask()
+{
+ // square rt only, a flat panel fills the whole rect and has no dead corner
+ extern int ps_r__svp_flat_window;
+ extern Fvector4 ps_s3ds_param_3;
+ if (Width != Height || (ps_r__svp_flat_window && (int)ps_s3ds_param_3.y == 8))
+ return;
+
+ // corner-triangle leg, a quarter of the side stays clear of the inscribed disc edge
+ const float side = float(Width);
+ const float leg = side * 0.25f;
+ const float z = EPS_S;
+ const u32 C = color_rgba(255, 255, 255, 255);
+
+ // each corner is one triangle packed as a quad, the duplicated vertex collapses the second tri
+ u32 Offset;
+ FVF::TL* pv = (FVF::TL*)RCache.Vertex.Lock(16, g_combine->vb_stride, Offset);
+ auto corner = [&](float cx, float cy, float ax, float ay, float bx, float by)
+ {
+ pv->set(cx, cy, z, 1.f, C, 0, 0); pv++;
+ pv->set(ax, ay, z, 1.f, C, 0, 0); pv++;
+ pv->set(bx, by, z, 1.f, C, 0, 0); pv++;
+ pv->set(ax, ay, z, 1.f, C, 0, 0); pv++;
+ };
+ corner(0.f, 0.f, leg, 0.f, 0.f, leg);
+ corner(side, 0.f, side - leg, 0.f, side, leg);
+ corner(0.f, side, leg, side, 0.f, side - leg);
+ corner(side, side, side - leg, side, side, side - leg);
+ RCache.Vertex.Unlock(16, g_combine->vb_stride);
+
+ u_setrt(NULL, NULL, NULL, baseZB); // only the svp depth-stencil, no color target
+ RCache.set_Element(s_occq->E[1]);
+ RCache.set_Geometry(g_combine);
+ // replace the whole stencil byte with 0 in the corners, clearing the bit0 geometry marker
+ StateManager.SetStencil(TRUE, D3DCMP_ALWAYS, 0x00, 0xff, 0xff,
+ D3DSTENCILOP_KEEP, D3DSTENCILOP_REPLACE, D3DSTENCILOP_KEEP);
+ StateManager.SetColorWriteEnable(0);
+ StateManager.SetDepthFunc(D3DCMP_ALWAYS);
+ StateManager.SetDepthEnable(FALSE);
+ StateManager.SetCullMode(D3DCULL_NONE);
+ RCache.Render(D3DPT_TRIANGLELIST, Offset, 0, 16, 0, 8);
+ StateManager.SetColorWriteEnable(D3D_COLOR_WRITE_ENABLE_ALL);
+}
+
+// pip stash the SVP combined color in rt_secondVP so the scope lens can sample it
+void CRenderTarget::phase_svp_capture()
+{
+ PIX_EVENT(PHASE_SCOPE_SVP_CAPTURE);
+ if (ps_r__svp_dlss != 0)
+ {
+ // pip DLSS seam: assemble SvpDlssInputs from the SVP target + cached consts, then EvalSVP_DLSS (stub for now)
+ SvpDlssInputs in;
+ auto& vp = Device.m_SecondViewport;
+ in.viewport_id = 1; // stable SVP handle for DLSS history (main = 0), NOT the per-frame dwViewport
+ in.color_srv = rt_Generic_0->pTexture->get_SRView();
+ in.render_extent = { (u32)Width, (u32)Height };
+ in.depth_srv = rt_baseZB ? rt_baseZB->pTexture->get_SRView() : nullptr;
+ in.mvec_srv = rt_ssfx_motion_vectors->pTexture->get_SRView();
+ in.out_rtv = rt_secondVP->pRT;
+ in.out_uav = rt_secondVP->pUAView;
+ in.display_extent = { (u32)Width, (u32)Height }; // stub rt_secondVP follows the render extent, Ascii makes it display-res
+ in.view = Device.matrices[1].mView;
+ in.proj = Device.matrices[1].mProject;
+ in.view_proj.mul(Device.matrices[1].mProject, Device.matrices[1].mView);
+ in.prev_view = Device.matrices_previous[1].mView;
+ in.prev_proj = Device.matrices_previous[1].mProject;
+ in.prev_view_proj.mul(Device.matrices_previous[1].mProject, Device.matrices_previous[1].mView);
+ in.jitter_px = vp.svp_jitter_px;
+ in.near_plane = vp.svp_near; in.far_plane = vp.svp_far; in.fov = vp.svp_fov; in.aspect = vp.svp_aspect;
+ in.cam_pos = vp.svp_cam_pos; in.up = vp.svp_up; in.right = vp.svp_right; in.fwd = vp.svp_fwd;
+ in.reset = vp.dlss_reset_next.exchange(false);
+ EvalSVP_DLSS(in);
+ return;
+ }
+ // pip near-field defocus, thermal falls through to the plain copy
+ if (svp_nearblur_pass()) return;
+ // rt_secondVP alpha is garbage (nothing writes it) and must stay UNREAD, the scope shaders sample
+ // .rgb only and the lens composite blends srcalpha with its OWN forced o.a, never the source alpha
+ HW.pContext->CopyResource(rt_secondVP->pSurface, rt_Generic_0->pSurface);
+}
+
+// pip DLSS-SR eval, a passthrough stub. TODO Ascii replaces the body with the Streamline eval,
+// the SvpDlssInputs signature and the seam call are frozen, no sl::/NGX symbols here
+void CRenderTarget::EvalSVP_DLSS(const SvpDlssInputs& in)
+{
+ // a real eval restores via Target->SetActive(true) then unbinds the CS stage, the copy needs none
+ HW.pContext->CopyResource(rt_secondVP->pSurface, rt_Generic_0->pSurface);
+}
+
+// pip render the captured lens meshes with shader se, the bind callback sets the scope_phase
+// (IMAGE/RETICLE/SHADOW/LENS) that scope_color_write composites into the lens
+void CRenderTarget::draw_scope(ref_shader se, std::function bind)
+{
+ auto elem = se ? se->E[0] : nullptr;
+ if (!elem)
+ return;
+
+ Fmatrix FTold = Device.mFullTransform;
+ Device.mFullTransform = Device.mFullTransformHud;
+ RCache.set_xform_project(Device.mProjectHud);
+ RImplementation.rmNear();
+
+ // per-lens dump, which texture each lens draw samples as s_reticle + the optic type/geometry
+ extern int ps_r__svp_diag;
+ static u32 s_lens_ms = 0;
+ const bool lens_diag = ps_r__svp_diag && Device.dwTimeGlobal - s_lens_ms > 1000;
+ if (lens_diag)
+ s_lens_ms = Device.dwTimeGlobal;
+
+ u32 lens_idx = 0;
+ for (auto& N : RImplementation.GMBase.RGraph.mapScopeHUDSorted)
+ {
+ dxRender_Visual* V = N.pVisual;
+ if (!V || !N.pMatrix)
+ continue;
+
+ CTexture* tex = V->GetTexture();
+ // per-lens marker, names the reticle source texture so a capture shows which texture each
+ // scope_color_write draw samples as s_reticle (gated on r__gpu_markers)
+ PIX_EVENT_F("scope_lens tex=%s", tex ? tex->cName.c_str() : "none");
+ if (lens_diag)
+ {
+ extern Fvector4 ps_s3ds_param_3;
+ extern float g_pip_scope_magnification, g_pip_scope_min_mag, g_pip_scope_ratio;
+ PipMsg("[SVP-LENS] %u tex %s rtype %d itype %d eyep_r %.4f mag %.2f min %.2f ratio %.2f",
+ lens_idx, tex ? tex->cName.c_str() : "none", (int)ps_s3ds_param_3.y, (int)ps_s3ds_param_3.x,
+ Device.m_SecondViewport.eyepiece.radius, g_pip_scope_magnification, g_pip_scope_min_mag, g_pip_scope_ratio);
+ }
+ ++lens_idx;
+ if (tex)
+ {
+ // surface_get AddRefs (and services staging), release the ref once the alias holds its own
+ ID3DBaseTexture* s = tex->surface_get();
+ t_reticle->surface_set(s);
+ _RELEASE(s);
+ }
+
+ RCache.set_Element(elem);
+ // the housing pose from the main hud pass, the live matrix may hold a newer logic write by now
+ Fmatrix lensW = *RImplementation.GMBase.svp_pose_of(N.pMatrix);
+ // the quad skins from the live bone palette, folding latched bone x live inverse into the
+ // world cancels any bone step since the housing draw (the lens glass rides one bone)
+ {
+ Fmatrix bL, bNow;
+ CSkeletonX* sk = fast_dynamic_cast(V);
+ if (sk && RImplementation.GMBase.svp_lens_bone_of(V, bL) && sk->SVP_LensBoneXform(bNow))
+ {
+ Fmatrix inv;
+ inv.invert(bNow);
+ lensW.mulB_43(bL);
+ lensW.mulB_43(inv);
+ }
+ }
+ RCache.set_xform_world(lensW);
+ RImplementation.apply_object(N.pObject);
+ RImplementation.apply_lmaterial();
+
+ RCache.set_c("scope_svp", (int)Device.m_SecondViewport.IsSVPActive());
+ RCache.set_c("scope_debug", (int)scope_debug);
+ Fvector pt = {0, 0, 0};
+ Device.m_SecondViewport.eyepiece.m_W.transform(pt);
+ RCache.set_c("scope_w_eyepiece", pt.x, pt.y, pt.z, 1.0f);
+ const Fvector& w_ffp = Device.m_SecondViewport.w_ffp;
+ const Fvector& w_sfp = Device.m_SecondViewport.w_sfp;
+ RCache.set_c("scope_w_ffp", w_ffp.x, w_ffp.y, w_ffp.z, 1.0f);
+ RCache.set_c("scope_w_sfp", w_sfp.x, w_sfp.y, w_sfp.z, 1.0f);
+ // pip reticle collimator geometry 2*ocular_radius/eye_distance, the shader rebuilds the
+ // authored reticle magnification as a centered field under true PiP
+ {
+ Fvector ed; ed.sub(Device.m_SecondViewport.eyepiece.m_W.c, Device.vCameraPosition);
+ const float dist = _max(ed.magnitude(), 0.02f);
+ float kg = 2.f * Device.m_SecondViewport.eyepiece.radius / dist;
+ clamp(kg, 0.02f, 3.f);
+ // y = true-scale parallax, the real reticle shift is ~0.15 mrad at full eye deflection
+ extern float ps_r__svp_parallax;
+ extern float g_pip_scope_magnification;
+ extern float g_pip_scope_ratio;
+ float par = 0.f;
+ if (ps_r__svp_parallax > 0.f && g_pip_scope_magnification > 0.01f)
+ {
+ const float eff_mag = _max(g_pip_scope_ratio * g_pip_scope_magnification, 1.f);
+ const float hfov = deg2rad(_max(Device.fFOV, 1.f));
+ par = ps_r__svp_parallax * 0.00075f * kg * eff_mag / hfov;
+ }
+ // z dead lane, no shader reads it. w = eyepiece-fit ratio for the shader optical mag
+ RCache.set_c("svp_optics", kg, par, 0.f, _max(g_pip_scope_ratio, 1.f));
+ }
+ // pip scope-local exposure, x = 0 off else 2^bias
+ {
+ extern int ps_r__svp_local_exposure;
+ extern float ps_r__svp_exposure_bias;
+ // y = exit-pupil twilight dimming, exit pupil (ocular*ratio shrunk by zoom) vs the
+ // env-adapted eye pupil squared, electronic sights exempt
+ extern float ps_r__svp_twilight;
+ extern Fvector4 ps_s3ds_param_1;
+ extern Fvector4 ps_s3ds_param_3;
+ extern float g_pip_scope_magnification;
+ extern float g_pip_scope_min_mag;
+ // nvg on = the tube gain owns the scope brightness, our exposure lifts stand down
+ extern Fvector4 ps_dev_param_8;
+ const bool nvg_on = ps_dev_param_8.x >= 1.f;
+ // env-adapted eye pupil (mm), shared by the twilight dimming and the eyebox
+ float envb = 0.f, pupil_mm = 0.f;
+ extern int ps_r__svp_photo_model;
+ if (g_pGamePersistent)
+ {
+ CEnvDescriptor& E = *g_pGamePersistent->Environment().CurrentEnv;
+ envb = 0.299f * E.sun_color.x + 0.587f * E.sun_color.y + 0.114f * E.sun_color.z
+ + 0.5f * (0.299f * E.hemi_color.x + 0.587f * E.hemi_color.y + 0.114f * E.hemi_color.z);
+ if (ps_r__svp_photo_model)
+ {
+ // Moon-Spencer pupil response, luminance anchored so envb 1 reads as an
+ // overcast day (~2500 cd/m2) and envb 0.01 as moonlight (~0.25)
+ const float L = 2500.f * envb * envb;
+ pupil_mm = 4.9f - 3.f * tanhf(0.4f * log10f(_max(L, 1e-4f)));
+ clamp(pupil_mm, 2.f, 8.f);
+ }
+ else
+ pupil_mm = 6.f - 3.5f * _min(envb / 0.25f, 1.f);
+ }
+ float dim = 0.f;
+ extern int ps_markswitch_current;
+ // twilight dims passive optics, NV/thermal are exempt only while their overlay is actually
+ // active (markswitch 0 for NV, < 2 for thermal), else the scope shows a plain image
+ const bool overlay_active = svp_overlay_active(ps_s3ds_param_3.x, ps_markswitch_current);
+ if (ps_r__svp_twilight > 0.f && !overlay_active && g_pip_scope_magnification > 0.01f && pupil_mm > EPS)
+ {
+ const float mn = (g_pip_scope_min_mag > 0.01f) ? g_pip_scope_min_mag : g_pip_scope_magnification;
+ // exit pupil = objective diameter / magnification, real per-scope objective when
+ // known, else the ocular-ratio proxy
+ const float omm = svp_objective_mm();
+ float ep_mm;
+ if (omm > 0.01f)
+ ep_mm = omm / g_pip_scope_magnification;
+ else
+ {
+ const float xp = (ps_s3ds_param_1.z > 0.01f) ? ps_s3ds_param_1.z : 0.5f;
+ ep_mm = xp * Device.m_SecondViewport.eyepiece.radius * 2000.f * (mn / g_pip_scope_magnification);
+ }
+ // relative brightness is the pupil ratio squared, the legacy model used it linearly
+ const float dr = _min(ep_mm / pupil_mm, 1.f);
+ const float d = ps_r__svp_photo_model ? dr * dr : dr;
+ dim = 1.f + (_max(d, 0.6f) - 1.f) * _min(ps_r__svp_twilight, 1.f);
+ extern int ps_r__svp_diag;
+ if (ps_r__svp_diag)
+ {
+ static u32 s_twl_ms = 0;
+ if (Device.dwTimeGlobal - s_twl_ms > 1000)
+ {
+ s_twl_ms = Device.dwTimeGlobal;
+ PipMsg("[SVP-TWL] ep %.1fmm pupil %.1fmm env %.2f dim %.2f", ep_mm, pupil_mm, envb, dim);
+ }
+ }
+ }
+ // pip eyebox half angle from the real exit pupil, the sight anchor bound reads it
+ {
+ extern float ps_r__svp_eyebox;
+ extern int ps_r__svp_authored_optics;
+ extern float g_pip_scope_magnification;
+ extern int ps_r__svp_diag;
+ const float eb_omm = svp_objective_mm();
+ if (ps_r__svp_eyebox > 0.f && ps_r__svp_authored_optics && eb_omm > 0.01f && g_pip_scope_magnification > 0.01f && pupil_mm > EPS)
+ {
+ const float ep_r = eb_omm * 0.0005f / g_pip_scope_magnification;
+ const float p_r = pupil_mm * 0.0005f;
+ // full-blackout half angle for the sight anchor bound, arm = authored eye relief
+ const float eb_arm = _max(ps_s3ds_param_1.y * 0.01f, 0.05f);
+ Device.m_SecondViewport.svp_eyebox_rad = atanf((ep_r + p_r) / eb_arm);
+ }
+ else
+ {
+ // no authored bound on this optic, drop the previous scope's so the anchor
+ // falls back to its own default instead of a stale narrow eyebox
+ Device.m_SecondViewport.svp_eyebox_rad = 0.f;
+ if (ps_r__svp_eyebox > 0.f && ps_r__svp_diag)
+ {
+ static u32 s_ebg_ms = 0;
+ if (Device.dwTimeGlobal - s_ebg_ms > 1000)
+ {
+ s_ebg_ms = Device.dwTimeGlobal;
+ PipMsg("[SVP-EYEBOX] gated off, authored %d obj_w %.3f obj_mm %.1f mag %.2f pupil %.1f",
+ ps_r__svp_authored_optics, scope_objective_lens_offset.w, ps_s3ds_objective_mm,
+ g_pip_scope_magnification, pupil_mm);
+ }
+ }
+ }
+ }
+ // crescent drive, exposure zw carries the latched swing side, the tangent offset is
+ // scaled by pupil over eye relief so the bite depth reads the same on every scope
+ float st = 0.f;
+ float sw_k = 0.f;
+ const float shadow_g = Device.m_SecondViewport.svp_shadow_gain;
+ {
+ extern float g_pip_scope_magnification;
+ extern Fvector4 ps_s3ds_param_1;
+ st = (g_pip_scope_magnification > 0.01f) ? (g_pip_scope_magnification - 1.f) / 7.f : 0.f;
+ clamp(st, 0.f, 1.f);
+ Device.m_SecondViewport.svp_mag = _max(g_pip_scope_magnification, 0.f);
+ const float snug = 1.3f + (0.55f - 1.3f) * st;
+ float gp = shadow_g * 4.f;
+ clamp(gp, 0.f, 1.f);
+ float z_eff = ps_s3ds_param_1.z * (3.0f + (snug - 3.0f) * gp);
+ if (z_eff < 0.08f) z_eff = 0.08f;
+ sw_k = 0.4f * z_eff / _max(ps_s3ds_param_1.y, 0.05f);
+ }
+ RCache.set_c("svp_exposure", (ps_r__svp_local_exposure && !nvg_on) ? powf(2.f, ps_r__svp_exposure_bias) : 0.f,
+ nvg_on ? 0.f : dim,
+ Device.m_SecondViewport.svp_swing_x * shadow_g * sw_k,
+ Device.m_SecondViewport.svp_swing_y * shadow_g * sw_k);
+ // pip glass2: x = lens coating strength, y = heat mirage (sun elevation + magnification)
+ {
+ extern float ps_r__svp_coating;
+ extern float ps_r__svp_mirage;
+ extern float g_pip_scope_magnification;
+ float mirage = 0.f;
+ if (ps_r__svp_mirage > 0.f && g_pip_scope_magnification > 0.01f && g_pGamePersistent)
+ {
+ CEnvDescriptor& Em = *g_pGamePersistent->Environment().CurrentEnv;
+ const float heat = _max(-Em.sun_dir.y, 0.f); // high overhead sun heats the ground
+ const float clear = 1.f - _min(Em.rain_density, 1.f);
+ const float magf = _min(_max((g_pip_scope_magnification - 2.f) / 6.f, 0.f), 1.f);
+ mirage = ps_r__svp_mirage * heat * clear * magf;
+ extern int ps_r__svp_diag;
+ if (ps_r__svp_diag)
+ {
+ static u32 s_mir_ms = 0;
+ if (Device.dwTimeGlobal - s_mir_ms > 1000)
+ {
+ s_mir_ms = Device.dwTimeGlobal;
+ PipMsg("[SVP-MIRAGE] heat %.2f clear %.2f magf %.2f -> %.3f", heat, clear, magf, mirage);
+ }
+ }
+ }
+ // w = flat-panel V-crop: (svp W/H) / panel aspect. 1 when the SVP already matches the
+ // panel (non-square path, no crop needed); 1/aspect when the SVP is square (fallback)
+ float vcrop = 1.f;
+ if (RImplementation.TargetSVP && RImplementation.TargetSVP->Height > 0
+ && Device.m_SecondViewport.svp_panel_aspect > 0.01f)
+ vcrop = ((float)RImplementation.TargetSVP->Width / (float)RImplementation.TargetSVP->Height)
+ / Device.m_SecondViewport.svp_panel_aspect;
+ RCache.set_c("svp_glass2", ps_r__svp_coating, mirage, 0.f, vcrop);
+ Device.m_SecondViewport.svp_panel_vcrop = vcrop; // pip binocular bracket mapping reads it
+ // pip glass3: x = sharpen amount, y = field-stop onset, z = sharpen radial falloff, w = sharpen inner crisp radius
+ extern float ps_r__svp_sharpen, ps_r__svp_sharpen_falloff, ps_r__svp_sharpen_inner;
+ // pip field-stop onset, the stop sits at the field edge and the exit pupil
+ // blurs it by a penumbra whose inner half shows inside the field, 1 = off
+ float fs_onset = 1.f;
+ {
+ extern float g_pip_scope_magnification;
+ extern Fvector4 ps_s3ds_param_1;
+ const float fs_omm = svp_objective_mm();
+ const float fs_fov = Device.m_SecondViewport.svp_fov;
+ if (fs_omm > 0.01f && g_pip_scope_magnification > 0.01f && fs_fov > 0.01f)
+ {
+ const float ep_r = fs_omm * 0.0005f / g_pip_scope_magnification;
+ const float er = _max(ps_s3ds_param_1.y * 0.01f, 0.05f);
+ const float app_half = g_pip_scope_magnification * fs_fov * 0.5f;
+ const float penumbra = _min(atanf(ep_r / er) / app_half, 1.f);
+ fs_onset = 1.f - 0.5f * penumbra;
+ }
+ }
+ RCache.set_c("svp_glass3", ps_r__svp_sharpen, fs_onset, ps_r__svp_sharpen_falloff, ps_r__svp_sharpen_inner);
+ // pip glass4: x = nvg bleach roll-off, y = nvg auto-gain, w = shadow swing envelope
+ extern float ps_r__svp_nvg_bleach, ps_r__svp_nvg_sensitivity;
+ RCache.set_c("svp_glass4", ps_r__svp_nvg_bleach, ps_r__svp_nvg_sensitivity, 0.f,
+ shadow_g);
+ }
+ }
+
+ bind();
+ V->Render(0);
+ }
+
+ RImplementation.rmNormal();
+ Device.mFullTransform = FTold;
+ RCache.set_xform_project(Device.mProject);
+}
+
+// pip render reflex-sight lenses (iScopeLense==10) with their own shaders, no-op for an eyepiece-only scope
+// svp draws them through the entrance-pupil camera so a hybrid holo dot magnifies and tracks with the world
+void CRenderTarget::draw_reflex(bool svp)
+{
+ PIX_EVENT_F("RENDER_REFLEX_SIGHTS x%u", (u32)RImplementation.GMBase.RGraph.mapReflexHUDSorted.size());
+
+ Fmatrix FTold = Device.mFullTransform;
+ if (svp)
+ {
+ // the svp camera is already active, keep it so the dot lands in the magnified scene
+ Device.mFullTransform.mul(Device.matrices[1].mProject, Device.matrices[1].mView);
+ RCache.set_xform_view(Device.matrices[1].mView);
+ RCache.set_xform_project(Device.matrices[1].mProject);
+ }
+ else
+ {
+ Device.mFullTransform = Device.mFullTransformHud;
+ RCache.set_xform_project(Device.mProjectHud);
+ }
+ RImplementation.rmNear();
+
+ for (auto& N : RImplementation.GMBase.RGraph.mapReflexHUDSorted)
+ {
+ if (!N.pVisual || !N.pSE || !N.pMatrix)
+ continue;
+ RCache.set_Element(N.pSE);
+ Fmatrix refW = *RImplementation.GMBase.svp_pose_of(N.pMatrix);
+ // same bone step cancel as draw_scope, the reflex mesh rides one bone too
+ {
+ Fmatrix bL, bNow;
+ CSkeletonX* sk = fast_dynamic_cast(N.pVisual);
+ if (sk && RImplementation.GMBase.svp_lens_bone_of(N.pVisual, bL) && sk->SVP_LensBoneXform(bNow))
+ {
+ Fmatrix inv;
+ inv.invert(bNow);
+ refW.mulB_43(bL);
+ refW.mulB_43(inv);
+ }
+ }
+ RCache.set_xform_world(refW);
+ RImplementation.apply_object(N.pObject);
+ RImplementation.apply_lmaterial();
+ N.pVisual->Render(0);
+ }
+
+ RImplementation.rmNormal();
+ Device.mFullTransform = FTold;
+ if (svp)
+ RCache.set_xform_view(Device.mView);
+ RCache.set_xform_project(Device.mProject);
+}
+
+// pip collimated reflex proxy, place a scaled copy of the reflex mesh at a virtual distance on the
+// boresight and draw it into the captured svp image so the magnifier carries it, sharp and tracking
+bool CRenderTarget::draw_reflex_proxy()
+{
+ extern Fvector4 ps_s3ds_param_3;
+ extern int ps_markswitch_current;
+ // thermal image is pixelated and carries its own reticle, keep the proxy out of it
+ if (svp_thermal_active(ps_s3ds_param_3.x, ps_markswitch_current))
+ return false;
+
+ auto& G = RImplementation.GMBase.RGraph;
+ auto& vp = Device.m_SecondViewport;
+
+ // bone-folded world pose of a reflex node, same bone step cancel draw_reflex uses so the proxy
+ // matches the legacy 1x dot
+ auto pose_of = [&](auto& N) -> Fmatrix {
+ Fmatrix W = *RImplementation.GMBase.svp_pose_of(N.pMatrix);
+ Fmatrix bL, bNow;
+ CSkeletonX* sk = fast_dynamic_cast(N.pVisual);
+ if (sk && RImplementation.GMBase.svp_lens_bone_of(N.pVisual, bL) && sk->SVP_LensBoneXform(bNow))
+ {
+ Fmatrix inv; inv.invert(bNow);
+ W.mulB_43(bL);
+ W.mulB_43(inv);
+ }
+ return W;
+ };
+
+ // primary node, measure boresight + subtense from the first valid reflex mesh
+ bool have_prim = false;
+ Fmatrix refW0; Fvector C0 = {}; float r0 = 0.f;
+ for (auto& N : G.mapReflexHUDSorted)
+ {
+ if (!N.pVisual || !N.pSE || !N.pMatrix)
+ continue;
+ refW0 = pose_of(N);
+ auto& V = N.pVisual->getVisData();
+ Fvector lc; V.box.getcenter(lc);
+ refW0.transform_tiny(C0, lc);
+ r0 = V.sphere.R;
+ have_prim = true;
+ break;
+ }
+ if (!have_prim)
+ return false;
+
+ // classify by aperture overlap, a coaxial hybrid (magnifier + holo) sits close enough that the
+ // reflex glass overlaps the eyepiece aperture, a stacked secondary (rmr) sits beyond both
+ {
+ auto& ep = Device.m_SecondViewport.eyepiece;
+ bool coax = ep.radius > EPS;
+ float lateral = 0.f;
+ if (coax)
+ {
+ Fvector eax; eax.set(ep.m_W.k); eax.normalize();
+ Fvector rel; rel.sub(C0, ep.m_W.c);
+ Fvector proj; proj.mad(ep.m_W.c, eax, rel.dotproduct(eax));
+ lateral = C0.distance_to(proj);
+ // r0 is the measured reflex glass radius, ep.radius the eyepiece, overlap when the
+ // lateral gap is under their radii summed
+ if (lateral > ep.radius + r0)
+ coax = false;
+ }
+ const char* cls = !(ep.radius > EPS) ? "legacy" : (coax ? "coax" : "stacked");
+ extern int ps_r__svp_diag;
+ static u32 s_cls_ms = 0;
+ if (ps_r__svp_diag && Device.dwTimeGlobal - s_cls_ms > 1000)
+ {
+ s_cls_ms = Device.dwTimeGlobal;
+ PipMsg("[SVP-RET] proxy class=%s lat=%.2fcm eye_r=%.2fcm", cls, lateral * 100.f, ep.radius * 100.f);
+ }
+ if (!coax)
+ return false; // stacked or no eyepiece, the real top sight draws via the 1x overlay
+ }
+
+ const float eye_dist = C0.distance_to(Device.vCameraPosition);
+ if (!(eye_dist > 0.02f)) // false for NaN too
+ return false;
+
+ // boresight, the reflex mesh normal is the real collimated axis, the stable sight line is fallback
+ Fvector b; b.set(refW0.k);
+ if (b.magnitude() < EPS)
+ {
+ if (vp.svp_sight_ok && (Device.dwFrame - vp.svp_sight_frame) < 8)
+ b.set(vp.svp_sight_dir);
+ else
+ return false;
+ }
+ b.normalize_safe();
+ if (b.magnitude() < 0.5f)
+ return false;
+
+ const float D = 10.0f; // virtual distance past the svp near plane, the projected size is D invariant
+ // the scale draws the reticle at the reflex glass angular size r0/eye_dist, the physical aperture
+ // bound, a collimated reticle cannot appear larger than its own window so no extra cap is applied
+ const float s = D / eye_dist;
+ Fvector target0; target0.mad(vp.svp_cam_pos, b, D);
+
+ // in-front + in-frame test against the svp camera, a wildly off-axis reflex is degenerate
+ Fmatrix FT; FT.mul(Device.matrices[1].mProject, Device.matrices[1].mView);
+ const float cx = target0.x*FT._11 + target0.y*FT._21 + target0.z*FT._31 + FT._41;
+ const float cy = target0.x*FT._12 + target0.y*FT._22 + target0.z*FT._32 + FT._42;
+ const float cw = target0.x*FT._14 + target0.y*FT._24 + target0.z*FT._34 + FT._44;
+ const bool front_ok = b.dotproduct(vp.svp_fwd) > 0.f;
+ const bool inframe = (cw > 0.f) && (_abs(cx) < 2.f*cw) && (_abs(cy) < 2.f*cw);
+ if (!front_ok || !inframe)
+ return false;
+
+ // bind the captured svp color with no depth so the additive dot always lands on top
+ u_setrt(Width, Height, rt_secondVP->pRT, nullptr, nullptr, nullptr);
+ RCache.set_CullMode(CULL_CCW);
+ RCache.set_Stencil(FALSE);
+ RCache.set_ColorWriteEnable();
+
+ Fmatrix FTold = Device.mFullTransform;
+ Device.mFullTransform.mul(Device.matrices[1].mProject, Device.matrices[1].mView);
+ RCache.set_xform_view(Device.matrices[1].mView);
+ RCache.set_xform_project(Device.matrices[1].mProject);
+ RImplementation.rmNormal();
+
+ u32 drawn = 0;
+ for (auto& N : G.mapReflexHUDSorted)
+ {
+ if (!N.pVisual || !N.pSE || !N.pMatrix)
+ continue;
+ Fmatrix refW = pose_of(N);
+ auto& V = N.pVisual->getVisData();
+ Fvector lc; V.box.getcenter(lc);
+ Fvector Cn; refW.transform_tiny(Cn, lc);
+
+ // scale the mesh uniformly and anchor its box center on the boresight, extra meshes keep
+ // their scaled offset from the primary so a multi-element reticle stays rigid
+ Fmatrix proxyW = refW;
+ proxyW.i.mul(s); proxyW.j.mul(s); proxyW.k.mul(s);
+ Fvector off; proxyW.transform_dir(off, lc);
+ Fvector rel; rel.sub(Cn, C0); rel.mul(s);
+ proxyW.c.add(target0, rel);
+ proxyW.c.sub(off);
+
+ RCache.set_Element(N.pSE);
+ RCache.set_xform_world(proxyW);
+ RImplementation.apply_object(N.pObject);
+ RImplementation.apply_lmaterial();
+ N.pVisual->Render(0);
+ drawn++;
+ }
+
+ RImplementation.rmNormal();
+ Device.mFullTransform = FTold;
+ RCache.set_xform_view(Device.mView);
+ RCache.set_xform_project(Device.mProject);
+
+ {
+ extern int ps_r__svp_diag;
+ static u32 s_prox_ms = 0;
+ if (ps_r__svp_diag && Device.dwTimeGlobal - s_prox_ms > 1000)
+ {
+ s_prox_ms = Device.dwTimeGlobal;
+ PipMsg("[SVP-RET] proxy drew=%u eye=%.1fcm theta=%.2fmrad s=%.2f front=%d in=%d",
+ drawn, eye_dist * 100.f, (r0 / eye_dist) * 1000.f, s, (int)front_ok, (int)inframe);
+ }
+ }
+
+ if (drawn > 0) { if (ps_r__svp_stats) ++svp_stats_reflex_proxy; svp_ledger_reflex_proxy = 1; } // overlay + ledger proof the collimated proxy drew
+ return drawn > 0;
+}
+
+void CRenderTarget::phase_3DSSReticle()
+{
+ PIX_EVENT(PHASE_SCOPE_RETICLE);
+
+ // pip take the PiP path only when the active optic can drive the SVP, an optic that
+ // captures nothing falls through to the stock render_Reticle
+ const bool svp = Device.m_SecondViewport.IsSVPActive() && RImplementation.TargetSVP;
+ const bool has_lens = !RImplementation.GMBase.RGraph.mapScopeHUDSorted.empty() && Device.m_SecondViewport.eyepiece.radius > EPS;
+
+ // pip [SVP-RET] reticle pipeline diag, dumps the capture maps before the draws consume them
+ {
+ extern int ps_r__svp_diag;
+ static u32 s_ret_ms = 0;
+ if (ps_r__svp_diag && Device.dwTimeGlobal - s_ret_ms > 1000)
+ {
+ s_ret_ms = Device.dwTimeGlobal;
+ auto& G = RImplementation.GMBase.RGraph;
+ extern Fvector4 ps_s3ds_param_1;
+ extern Fvector4 ps_s3ds_param_3;
+ extern Fvector4 ps_shader_scope_params;
+ PipMsg("[SVP-RET] path=%s svp=%d lens=%d scope=%u reflex=%u obj=%u rsize=%.2f rtype=%.0f mag=%.2f/%.2f/%.2f w=%.1f hudy=%.1f",
+ (Device.true_pip_on && (svp || has_lens)) ? "pip" : "stock",
+ (int)svp, (int)has_lens,
+ (u32)G.mapScopeHUDSorted.size(), (u32)G.mapReflexHUDSorted.size(), (u32)G.mapScopeHUDObjective.size(),
+ ps_s3ds_param_1.x, ps_s3ds_param_3.y,
+ ps_shader_scope_params.x, ps_shader_scope_params.y, ps_shader_scope_params.z, ps_shader_scope_params.w,
+ g_pGamePersistent ? g_pGamePersistent->m_pGShaderConstants->hud_params.y : 0.f);
+ auto dump = [&](const char* tag, auto& map) {
+ u32 i = 0;
+ for (auto& N : map)
+ {
+ if (!N.pVisual || !N.pMatrix) { i++; continue; }
+ Fmatrix W = *RImplementation.GMBase.svp_pose_of(N.pMatrix);
+ Fvector c; N.pVisual->getVisData().box.getcenter(c);
+ Fvector wc; W.transform_tiny(wc, c);
+ auto tx = N.pVisual->GetTexture();
+ PipMsg("[SVP-RET] %s[%u] tex=%s pos=(%.2f %.2f %.2f) d=%.1fcm r=%.1fcm",
+ tag, i, tx ? tx->cName.c_str() : "?", wc.x, wc.y, wc.z,
+ wc.distance_to(Device.vCameraPosition) * 100.f,
+ N.pVisual->getVisData().sphere.R * 100.f);
+ i++;
+ }
+ };
+ dump("scope", G.mapScopeHUDSorted);
+ dump("reflex", G.mapReflexHUDSorted);
+ dump("obj", G.mapScopeHUDObjective);
+ }
+ }
+
+ if (Device.true_pip_on && (svp || has_lens))
+ {
+ EnsureScopeShaders(); // glue shaders (lazy)
+
+ auto M = RImplementation.TargetMain;
+ auto S = RImplementation.TargetSVP;
+
+ // a hybrid magnifier drew the holo dot inside the svp already, skip the 1x main-view overlay
+ extern int ps_r__svp_reflex_capture;
+ // suppress the 1x overlay only when the collimated proxy actually drew this frame, never blank the dot
+ const bool reflex_in_svp = ps_r__svp_reflex_capture && svp
+ && Device.m_SecondViewport.svp_reflex_proxy_ok;
+
+ // the scope shader reads generic2 as the gbuffer position for the holepunch/depth
+ HW.pContext->CopyResource(rt_Generic_2->pTexture->surface_get(), RImplementation.Target->rt_Position->pTexture->surface_get());
+
+ u_setrt(RImplementation.Target->rt_Generic_0, nullptr, RImplementation.Target->rt_Position, RImplementation.Target->baseZB);
+ RCache.set_CullMode(CULL_CCW);
+ RCache.set_Stencil(FALSE);
+ RCache.set_ColorWriteEnable();
+
+ if (!reflex_in_svp)
+ draw_reflex(); // reflex / red dot, both 1x and magnifier
+
+ // composite the eyepiece lens when magnified or a 1x eyepiece was captured, magnified samples
+ // the SVP image, 1x / fake-PiP samples a main-frame copy, a pure reflex optic captures no ==3
+ if (svp || (!RImplementation.GMBase.RGraph.mapScopeHUDSorted.empty() && Device.m_SecondViewport.eyepiece.radius > EPS))
+ {
+ // fake-PiP, off-SVP the lens reads a copy of the finished main frame (magnified already filled rt_secondVP)
+ if (!svp)
+ HW.pContext->CopyResource(M->rt_secondVP->pSurface, M->rt_Generic_0->pSurface);
+
+ // JITTERFIX, cancel the TAA jitter in the VS so the lens edge has no ring, cvar 0 skips for the a/b
+ extern int ps_r__svp_jitterfix;
+ if (ps_r__svp_jitterfix)
+ { PIX_EVENT(SCOPE_PHASE_JITTERFIX); draw_scope(s_scope_color_write, []() { RCache.set_c("scope_phase", SCOPE_PHASE_JITTERFIX); }); }
+
+ // point the stock-named textures at this viewport's RTs, the SVP image when magnified else
+ // the main-frame copy + the main gbuffer (rt_Generic_2 already holds the copied position)
+ auto remap = [](LPCSTR name, ref_rt& target) {
+ ref_texture t;
+ t.create(name);
+ // raw pSurface, surface_get would AddRef a reference nobody releases (per-frame leak)
+ t->surface_set(target->pSurface);
+ };
+ remap(r2_RT_secondVP, svp ? S->rt_secondVP : M->rt_secondVP);
+ remap(r2_RT_generic2, svp ? S->rt_Position : M->rt_Generic_2);
+ remap(r2_RT_heat, svp ? S->rt_Heat : M->rt_Heat);
+ // pip the scope's own measured exposure for the local-exposure image grade
+ {
+ ref_texture t;
+ t.create("$user$svp_tonemap");
+ ID3DBaseTexture* s = (svp ? S : M)->t_LUM_dest->surface_get();
+ t->surface_set(s);
+ _RELEASE(s);
+ }
+ // invalidate so the IMAGE pass picks up the remapped surfaces (the bind cache keys on CTexture identity)
+ RCache.Invalidate();
+
+ u_setrt(M->rt_Generic_0, nullptr, M->rt_Position, M->baseZB);
+ RCache.set_CullMode(CULL_CCW);
+ RCache.set_Stencil(FALSE);
+ RCache.set_ColorWriteEnable();
+
+ // IMAGE, the magnified SVP scene (or the main frame under fake-PiP)
+ { PIX_EVENT(SCOPE_PHASE_IMAGE);
+ draw_scope(s_scope_color_write, [svp]() {
+ RCache.set_c("scope_phase", SCOPE_PHASE_IMAGE);
+ auto ts = svp ? RImplementation.TargetSVP : RImplementation.TargetMain;
+ Fvector4 sr; sr.set((float)ts->Width, (float)ts->Height, 1.0f / (float)ts->Width, 1.0f / (float)ts->Height);
+ RCache.set_c("screen_res", sr);
+ auto tm = RImplementation.TargetMain;
+ Fvector4 outr; outr.set((float)tm->Width, (float)tm->Height, 1.0f / (float)tm->Width, 1.0f / (float)tm->Height);
+ RCache.set_c("output_res", outr);
+ // lens roll, project the objective up-vector to screen so the reticle stays upright
+ Fvector up = {0, 1, 0};
+ Device.m_SecondViewport.objective.m_W.transform_dir(up);
+ Device.mView.transform_dir(up);
+ up.z = 0.0f;
+ up.normalize();
+ float angle = acosf(up.dotproduct({0, 1, 0})) * (up.x > 0 ? 1.0f : -1.0f);
+ RCache.set_c("hack_tex_angle", angle);
+ });
+ }
+
+
+ // latch the on-screen eyepiece disc px for adaptive SVP resolution, learn only the
+ // settled aimed disc so a raise transient or quick peek never freezes a partial value
+ if (svp && RImplementation.TargetSVP && Device.m_SecondViewport.eyepiece.radius > EPS)
+ {
+ auto& vpd = Device.m_SecondViewport;
+ const Fmatrix& MH = Device.mFullTransformHud;
+ auto toPx = [&](const Fvector& wp, float& sx, float& sy) {
+ const float x = wp.x*MH._11 + wp.y*MH._21 + wp.z*MH._31 + MH._41;
+ const float y = wp.x*MH._12 + wp.y*MH._22 + wp.z*MH._32 + MH._42;
+ const float w = wp.x*MH._14 + wp.y*MH._24 + wp.z*MH._34 + MH._44;
+ const float iw = (fabsf(w) > 1e-6f) ? 1.0f/w : 0.0f;
+ sx = (x*iw*0.5f + 0.5f) * (float)M->Width;
+ sy = (1.0f - (y*iw*0.5f + 0.5f)) * (float)M->Height;
+ };
+ Fvector ei, li; li.set(vpd.eyepiece.radius, 0.f, 0.f);
+ vpd.eyepiece.m_W.transform_tiny(ei, li);
+ float cx, cy, ix, iy; toPx(vpd.eyepiece.m_W.c, cx, cy); toPx(ei, ix, iy);
+ const float disc = 2.0f * sqrtf((ix-cx)*(ix-cx) + (iy-cy)*(iy-cy)); // on-screen disc diameter px
+ // weapon raise factor, 1 only when fully aimed, gates learning to the settled pose
+ const float rot = (g_pGamePersistent && g_pGamePersistent->m_pGShaderConstants)
+ ? g_pGamePersistent->m_pGShaderConstants->hud_params.x : 0.f;
+ // subscribe to the optic epoch, a swap under the jump threshold still re-learns the
+ // new optic's disc, the epoch is consumed only after a settled re-learn
+ static u32 s_disc_px_epoch = 0;
+ const bool disc_epoch_swap = (vpd.svp_optic_epoch != s_disc_px_epoch);
+ if (disc > 1.f && disc == disc && rot > 0.999f) // settled, ignore NaN / degenerate
+ {
+ float& latched = vpd.svp_disc_px;
+ const float prev_latched = latched;
+ if (latched <= 0.f || disc > latched + 24.f || disc_epoch_swap) // first settle, a jump, or an optic swap
+ latched = disc;
+ else if (disc < latched * 0.85f) // big drop, swapped to a smaller optic
+ latched = disc;
+ if (latched != prev_latched) { if (ps_r__svp_stats) ++svp_stats_disc_latch; svp_ledger_disc_latch = 1; } // overlay + ledger proof the latch moved
+ s_disc_px_epoch = vpd.svp_optic_epoch;
+ }
+ extern int ps_r__svp_diag;
+ static u32 s_svpres_t = 0;
+ if (ps_r__svp_diag && disc > 1.f && Device.dwTimeGlobal - s_svpres_t > 700)
+ {
+ s_svpres_t = Device.dwTimeGlobal;
+ const u32 sres = RImplementation.TargetSVP->Width;
+ extern float g_pip_scope_magnification; extern float ps_r__svp_adaptive_res;
+ const float lin = (float)sres / disc;
+ PipMsg("[SVP-RES] mag=%.1f svp=%ux%u disc=%.0fpx latch=%.0f adapt=%.2f overrender=%.2fx_linear %.2fx_area",
+ g_pip_scope_magnification, sres, sres, disc, vpd.svp_disc_px, ps_r__svp_adaptive_res, lin, lin*lin);
+ }
+ }
+
+
+ // restore the stock textures for the reticle/shadow/lens draws
+ M->SetActive(true);
+ u_setrt(M->rt_Generic_0, nullptr, M->rt_Position, M->baseZB);
+ RCache.set_CullMode(CULL_CCW);
+ RCache.set_Stencil(FALSE);
+ RCache.set_ColorWriteEnable();
+
+ { PIX_EVENT(SCOPE_PHASE_RETICLE); draw_scope(s_scope_color_write, []() { RCache.set_c("scope_phase", SCOPE_PHASE_RETICLE); }); }
+ { PIX_EVENT(SCOPE_PHASE_SHADOW); draw_scope(s_scope_color_write, []() { RCache.set_c("scope_phase", SCOPE_PHASE_SHADOW); }); }
+ { PIX_EVENT(SCOPE_PHASE_LENS); draw_scope(s_scope_color_write, []() { RCache.set_c("scope_phase", SCOPE_PHASE_LENS); }); }
+
+ // CUSTOM_DEPTH, let the scope override depth so DOF focuses on the lens image
+ { PIX_EVENT(SCOPE_PHASE_CUSTOM_DEPTH);
+ u_setrt(RImplementation.Target->rt_Position, 0, 0, 0, RImplementation.Target->baseZB);
+ draw_scope(s_scope_depth_write, []() {
+ RCache.set_c("scope_phase", SCOPE_PHASE_DEPTHWRITE | SCOPE_PHASE_CUSTOM_DEPTH);
+ RCache.set_c("scope_depth_value", 1.0f);
+ });
+ }
+
+ // re-draw the reflex over the composited lens at the main-view position, the hybrid
+ // magnifier already has it in the svp image so skip the 1x overlay there
+ if (!reflex_in_svp)
+ {
+ u_setrt(M->rt_Generic_0, nullptr, M->rt_Position, M->baseZB);
+ RCache.set_CullMode(CULL_CCW);
+ RCache.set_Stencil(FALSE);
+ RCache.set_ColorWriteEnable();
+ draw_reflex();
+ }
+ }
+
+ // the capture maps clear at the main-pass combine tail now, past the nvg split and taa mask
+ // stamp that read them (deriveScopeLens already read them this frame)
+ u_setrt(RImplementation.Target->rt_Generic_0, RImplementation.Target->rt_Position, 0, HW.pBaseZB);
+ return;
+ }
+
+ // legacy 3D-fake / fake-SVP reticle, the stock path when true_pip is off
+ HW.pContext->CopyResource(rt_Generic_2->pTexture->surface_get(), RImplementation.Target->rt_Position->pTexture->surface_get());
+
+ HW.pContext->CopyResource(rt_Generic_temp->pTexture->surface_get(), rt_Generic_0->pTexture->surface_get());
+
+ u_setrt(RImplementation.Target->rt_Generic_0, RImplementation.Target->rt_Position, 0, HW.pBaseZB);
+
+ RCache.set_CullMode(CULL_CCW);
+ RCache.set_Stencil(FALSE);
+ RCache.set_ColorWriteEnable();
+
+ RImplementation.render_Reticle();
+
+ // pip reflexes captured into mapReflexHUDSorted draw here for the fallback optics,
+ // the map is empty when true_pip is off
+ u_setrt(RImplementation.Target->rt_Generic_0, RImplementation.Target->rt_Position, 0, HW.pBaseZB);
+ RCache.set_CullMode(CULL_CCW);
+ RCache.set_Stencil(FALSE);
+ RCache.set_ColorWriteEnable();
+ draw_reflex();
+
+ // the capture maps clear at the main-pass combine tail now, past every consumer, the frame-start
+ // clear + per-frame rebuild cover staleness on the fallback path too
+};
+#endif
diff --git a/src/Layers/xrRenderPC_R4/svp_stats.cpp b/src/Layers/xrRenderPC_R4/svp_stats.cpp
new file mode 100644
index 0000000000..5517923ee0
--- /dev/null
+++ b/src/Layers/xrRenderPC_R4/svp_stats.cpp
@@ -0,0 +1,524 @@
+#include "stdafx.h"
+#include "r4.h"
+#include "svp_stats.h"
+
+#include "../../xrEngine/GameFont.h"
+#include "../../Include/xrRender/UIRender.h"
+#include "../../Include/xrRender/UIShader.h"
+#include "../../Include/xrRender/FactoryPtr.h"
+
+// gate cvar + the shared cull counters, all defined in svp_console.cpp
+extern int ps_r__svp_stats;
+extern u32 svp_stats_ssa_culled;
+extern u32 svp_stats_cull_reject;
+extern u32 svp_stats_cull_reject_ident;
+extern u32 svp_stats_lights_mirrored;
+extern u32 svp_stats_lights_skipped;
+extern u32 svp_stats_taa_stamp;
+extern u32 svp_stats_nvg_split;
+extern u32 svp_stats_lod_scale;
+extern u32 svp_stats_hud_cull_reject;
+extern u32 svp_stats_grass_cull_reject;
+extern u32 svp_stats_reflex_proxy;
+extern u32 svp_stats_distort_guard;
+extern u32 svp_stats_nvg_sky;
+extern u32 svp_stats_disc_latch;
+extern u32 svp_stats_fwd_keep;
+extern u32 svp_stats_optic_resolve;
+// adaptive-res grow gate, defined in svp_console.cpp
+extern int ps_r__svp_adaptive_grow;
+// engine scope magnification, defined in xrRender_console.cpp
+extern float g_pip_scope_magnification;
+
+namespace
+{
+ using namespace svp_stats;
+
+ // gpu-ms color thresholds, cell turns yellow past warn, red past crit
+ const double GPU_WARN_MS = 3.0;
+ const double GPU_CRIT_MS = 6.0;
+
+ const u32 RING = 4; // frames of query latency, readback is non-blocking on the oldest
+ const u32 MAX_PAIRS = 128; // timestamp pairs per frame, past this the frame flags overflow
+ const u32 FOOT_MAX = 10; // free-form footer lines the panel sizes its width to
+
+ struct sec_data
+ {
+ double cpu_ms;
+ double gpu_ms;
+ u32 calls, verts, polys;
+ };
+
+ struct stats_frame
+ {
+ sec_data sec[SEC_COUNT];
+ u32 main_lights, main_shadowed, svp_blends, sun_passes, ssa_culled;
+ u32 cull_reject, cull_reject_ident, lights_mirrored, lights_skipped;
+ u32 taa_stamp, nvg_split;
+ u32 lod_scale, hud_cull_reject, grass_cull_reject, reflex_proxy;
+ u32 distort_guard, nvg_sky, disc_latch, fwd_keep;
+ u32 svp_w, svp_h;
+ u32 svp_epoch, optic_resolve;
+ float svp_disc, svp_disc_learned, svp_mag;
+ bool svp_grow;
+ double frame_ms;
+ bool svp_active;
+ };
+
+ struct frame_slot
+ {
+ ID3D11Query* disjoint;
+ ID3D11Query* ts[MAX_PAIRS * 2];
+ u8 sec_of_pair[MAX_PAIRS];
+ u32 pair_count;
+ bool overflow;
+ bool in_flight;
+ stats_frame data;
+ };
+
+ bool s_created = false;
+ frame_slot s_frames[RING];
+ u32 s_frame_no = 0;
+ frame_slot* s_cur = nullptr;
+
+ // per-section transient begin snapshots, a section never re-enters itself so one slot each
+ CTimer s_sec_timer[SEC_COUNT];
+ u32 s_sec_calls0[SEC_COUNT];
+ u32 s_sec_verts0[SEC_COUNT];
+ u32 s_sec_polys0[SEC_COUNT];
+ int s_sec_open_pair[SEC_COUNT];
+
+ // last fully-resolved frame, what the panel draws
+ stats_frame s_snap;
+ bool s_snap_valid = false;
+
+ CTimer s_frame_timer;
+ bool s_frame_timer_started = false;
+
+ // rolling ~1s frame-time window for the min/avg/max spike readout, each sample tagged with dwTimeGlobal
+ const u32 FT_WIN = 512;
+ float s_ft_ms[FT_WIN];
+ u32 s_ft_time[FT_WIN];
+ u32 s_ft_head = 0;
+
+ CGameFont* s_font = nullptr;
+ FactoryPtr* s_shader = nullptr;
+
+ bool ensure_created()
+ {
+ if (s_created)
+ return true;
+ if (!HW.pDevice)
+ return false;
+ D3D11_QUERY_DESC dd = {}; dd.Query = D3D11_QUERY_TIMESTAMP_DISJOINT;
+ D3D11_QUERY_DESC td = {}; td.Query = D3D11_QUERY_TIMESTAMP;
+ for (u32 f = 0; f < RING; ++f)
+ {
+ frame_slot& s = s_frames[f];
+ s.disjoint = nullptr;
+ s.pair_count = 0; s.overflow = false; s.in_flight = false;
+ HW.pDevice->CreateQuery(&dd, &s.disjoint);
+ for (u32 i = 0; i < MAX_PAIRS * 2; ++i)
+ {
+ s.ts[i] = nullptr;
+ HW.pDevice->CreateQuery(&td, &s.ts[i]);
+ }
+ }
+ // stat_font is the engine stats face, device-independent so it tracks screen height
+ s_font = xr_new("stat_font", CGameFont::fsDeviceIndependent);
+ s_shader = xr_new>();
+ // ui_console is an opaque white 32x32, hud_default multiplies it by the vertex color so the
+ // backing fill is the vertex color, ui_empty is fully transparent and drew nothing
+ (*s_shader)->create("hud\\default", "ui\\ui_console");
+ s_created = true;
+ s_snap_valid = false;
+ s_frame_no = 0;
+ s_ft_head = 0;
+ ZeroMemory(s_ft_time, sizeof(s_ft_time)); // drop any stale window samples from a prior session
+ return true;
+ }
+
+ void resolve_slot(frame_slot& s)
+ {
+ if (!s.in_flight)
+ return;
+ s.in_flight = false;
+ D3D11_QUERY_DATA_TIMESTAMP_DISJOINT dj;
+ if (HW.pContext->GetData(s.disjoint, &dj, sizeof(dj), D3D11_ASYNC_GETDATA_DONOTFLUSH) != S_OK)
+ return; // not ready, keep the last snapshot
+ if (dj.Disjoint || dj.Frequency == 0)
+ return; // clock skipped this frame, drop it
+ for (u32 i = 0; i < SEC_COUNT; ++i)
+ s.data.sec[i].gpu_ms = 0.0;
+ for (u32 p = 0; p < s.pair_count; ++p)
+ {
+ UINT64 t0 = 0, t1 = 0;
+ if (HW.pContext->GetData(s.ts[p * 2], &t0, sizeof(t0), D3D11_ASYNC_GETDATA_DONOTFLUSH) != S_OK)
+ continue;
+ if (HW.pContext->GetData(s.ts[p * 2 + 1], &t1, sizeof(t1), D3D11_ASYNC_GETDATA_DONOTFLUSH) != S_OK)
+ continue;
+ if (t1 > t0)
+ s.data.sec[s.sec_of_pair[p]].gpu_ms += double(t1 - t0) * 1000.0 / double(dj.Frequency);
+ }
+ s_snap = s.data;
+ s_snap_valid = true;
+ }
+
+ u32 gpu_color(double ms)
+ {
+ if (ms > GPU_CRIT_MS) return color_rgba(255, 90, 90, 255);
+ if (ms > GPU_WARN_MS) return color_rgba(255, 215, 90, 255);
+ return color_rgba(205, 225, 205, 255);
+ }
+
+ void fmt_count(char* out, size_t n, u32 v)
+ {
+ if (v >= 1000000) xr_sprintf(out, n, "%.1fM", v / 1000000.0);
+ else if (v >= 10000) xr_sprintf(out, n, "%uk", v / 1000);
+ else xr_sprintf(out, n, "%u", v);
+ }
+}
+
+namespace svp_stats
+{
+ void frame_begin()
+ {
+ if (ps_r__svp_stats == 0)
+ {
+ if (s_created)
+ release();
+ return;
+ }
+ if (!ensure_created())
+ return;
+
+ double fms = 0.0;
+ if (s_frame_timer_started)
+ fms = s_frame_timer.GetElapsed_sec() * 1000.0;
+ s_frame_timer.Start();
+ s_frame_timer_started = true;
+
+ ++s_frame_no;
+ s_cur = &s_frames[s_frame_no % RING];
+ // the reclaimed slot is RING frames old, resolve it before overwriting its queries
+ if (s_cur->in_flight)
+ resolve_slot(*s_cur);
+ s_cur->pair_count = 0;
+ s_cur->overflow = false;
+ s_cur->in_flight = false;
+ for (u32 i = 0; i < SEC_COUNT; ++i)
+ {
+ s_cur->data.sec[i].cpu_ms = 0.0;
+ s_cur->data.sec[i].gpu_ms = 0.0;
+ s_cur->data.sec[i].calls = s_cur->data.sec[i].verts = s_cur->data.sec[i].polys = 0;
+ s_sec_open_pair[i] = -1;
+ }
+ s_cur->data.main_lights = s_cur->data.main_shadowed = 0;
+ s_cur->data.svp_blends = s_cur->data.sun_passes = 0;
+ s_cur->data.frame_ms = fms;
+ // shared cull counters, one frame of accumulation each
+ svp_stats_ssa_culled = 0;
+ svp_stats_cull_reject = 0;
+ svp_stats_cull_reject_ident = 0;
+ svp_stats_lights_mirrored = 0;
+ svp_stats_lights_skipped = 0;
+ svp_stats_taa_stamp = 0;
+ svp_stats_nvg_split = 0;
+ svp_stats_lod_scale = 0;
+ svp_stats_hud_cull_reject = 0;
+ svp_stats_grass_cull_reject = 0;
+ svp_stats_reflex_proxy = 0;
+ svp_stats_distort_guard = 0;
+ svp_stats_nvg_sky = 0;
+ svp_stats_disc_latch = 0;
+ svp_stats_fwd_keep = 0;
+ svp_stats_optic_resolve = 0;
+ // feed the rolling ~1s window for the spike readout, skip the first frame's null delta
+ if (fms > 0.0)
+ {
+ s_ft_ms[s_ft_head] = (float)fms;
+ s_ft_time[s_ft_head] = Device.dwTimeGlobal;
+ s_ft_head = (s_ft_head + 1) % FT_WIN;
+ }
+ HW.pContext->Begin(s_cur->disjoint);
+ }
+
+ void section_begin(section_e s)
+ {
+ if (ps_r__svp_stats == 0 || !s_cur)
+ return;
+ s_sec_calls0[s] = RCache.stat.calls;
+ s_sec_verts0[s] = RCache.stat.verts;
+ s_sec_polys0[s] = RCache.stat.polys;
+ s_sec_timer[s].Start();
+ if (s_cur->pair_count < MAX_PAIRS)
+ {
+ u32 p = s_cur->pair_count++;
+ s_cur->sec_of_pair[p] = (u8)s;
+ s_sec_open_pair[s] = (int)p;
+ HW.pContext->End(s_cur->ts[p * 2]); // timestamp query, End marks the instant
+ }
+ else
+ {
+ s_cur->overflow = true;
+ s_sec_open_pair[s] = -1;
+ }
+ }
+
+ void section_end(section_e s)
+ {
+ if (ps_r__svp_stats == 0 || !s_cur)
+ return;
+ s_cur->data.sec[s].cpu_ms += s_sec_timer[s].GetElapsed_sec() * 1000.0;
+ s_cur->data.sec[s].calls += RCache.stat.calls - s_sec_calls0[s];
+ s_cur->data.sec[s].verts += RCache.stat.verts - s_sec_verts0[s];
+ s_cur->data.sec[s].polys += RCache.stat.polys - s_sec_polys0[s];
+ int p = s_sec_open_pair[s];
+ if (p >= 0)
+ {
+ HW.pContext->End(s_cur->ts[p * 2 + 1]);
+ s_sec_open_pair[s] = -1;
+ }
+ }
+
+ void note_main_lights(u32 total, u32 shadowed)
+ {
+ if (ps_r__svp_stats == 0 || !s_cur)
+ return;
+ s_cur->data.main_lights += total;
+ s_cur->data.main_shadowed += shadowed;
+ }
+
+ void note_svp_blend()
+ {
+ if (ps_r__svp_stats == 0 || !s_cur)
+ return;
+ ++s_cur->data.svp_blends;
+ }
+
+ void note_sun()
+ {
+ if (ps_r__svp_stats == 0 || !s_cur)
+ return;
+ ++s_cur->data.sun_passes;
+ }
+
+ void frame_end(bool svp_active)
+ {
+ if (ps_r__svp_stats == 0 || !s_cur)
+ return;
+ stats_frame& d = s_cur->data;
+ d.svp_active = svp_active;
+ d.ssa_culled = svp_stats_ssa_culled;
+ d.cull_reject = svp_stats_cull_reject;
+ d.cull_reject_ident = svp_stats_cull_reject_ident;
+ d.lights_mirrored = svp_stats_lights_mirrored;
+ d.lights_skipped = svp_stats_lights_skipped;
+ d.taa_stamp = svp_stats_taa_stamp;
+ d.nvg_split = svp_stats_nvg_split;
+ d.lod_scale = svp_stats_lod_scale;
+ d.hud_cull_reject = svp_stats_hud_cull_reject;
+ d.grass_cull_reject = svp_stats_grass_cull_reject;
+ d.reflex_proxy = svp_stats_reflex_proxy;
+ d.distort_guard = svp_stats_distort_guard;
+ d.nvg_sky = svp_stats_nvg_sky;
+ d.disc_latch = svp_stats_disc_latch;
+ d.fwd_keep = svp_stats_fwd_keep;
+ CRenderTarget* S = RImplementation.TargetSVP;
+ d.svp_w = (svp_active && S) ? S->Width : 0;
+ d.svp_h = (svp_active && S) ? S->Height : 0;
+ d.svp_disc = Device.m_SecondViewport.svp_disc_applied;
+ d.svp_disc_learned = Device.m_SecondViewport.svp_disc_px;
+ d.svp_epoch = Device.m_SecondViewport.svp_optic_epoch;
+ d.optic_resolve = svp_stats_optic_resolve;
+ d.svp_grow = (ps_r__svp_adaptive_grow != 0);
+ d.svp_mag = g_pip_scope_magnification;
+ HW.pContext->End(s_cur->disjoint);
+ s_cur->in_flight = true;
+ }
+
+ void draw_overlay()
+ {
+ if (ps_r__svp_stats == 0 || !s_created)
+ return;
+ if (!s_snap_valid || !s_font || !s_shader || !UIRender)
+ return;
+
+ const stats_frame& d = s_snap;
+ const bool full = (ps_r__svp_stats >= 2);
+
+ // section totals per viewport, svp mirrors nest inside the main lighting so its column is the
+ // scope-only slice while the main column carries the whole pass
+ double svp_gpu = d.sec[SEC_SVP_GBUFFER].gpu_ms + d.sec[SEC_SVP_LIGHTS].gpu_ms
+ + d.sec[SEC_SVP_EMISSIVE].gpu_ms + d.sec[SEC_SVP_COMBINE].gpu_ms;
+ double main_gpu = d.sec[SEC_MAIN_GBUFFER].gpu_ms + d.sec[SEC_MAIN_LIGHTS].gpu_ms
+ + d.sec[SEC_MAIN_COMBINE].gpu_ms;
+ double svp_cpu = d.sec[SEC_SVP_GBUFFER].cpu_ms + d.sec[SEC_SVP_LIGHTS].cpu_ms
+ + d.sec[SEC_SVP_EMISSIVE].cpu_ms + d.sec[SEC_SVP_COMBINE].cpu_ms;
+ double main_cpu = d.sec[SEC_MAIN_GBUFFER].cpu_ms + d.sec[SEC_MAIN_LIGHTS].cpu_ms
+ + d.sec[SEC_MAIN_COMBINE].cpu_ms;
+ u32 svp_calls = d.sec[SEC_SVP_GBUFFER].calls + d.sec[SEC_SVP_LIGHTS].calls
+ + d.sec[SEC_SVP_EMISSIVE].calls + d.sec[SEC_SVP_COMBINE].calls;
+ u32 main_calls = d.sec[SEC_MAIN_GBUFFER].calls + d.sec[SEC_MAIN_LIGHTS].calls
+ + d.sec[SEC_MAIN_COMBINE].calls;
+ u32 svp_verts = d.sec[SEC_SVP_GBUFFER].verts + d.sec[SEC_SVP_LIGHTS].verts
+ + d.sec[SEC_SVP_EMISSIVE].verts + d.sec[SEC_SVP_COMBINE].verts;
+ u32 main_verts = d.sec[SEC_MAIN_GBUFFER].verts + d.sec[SEC_MAIN_LIGHTS].verts
+ + d.sec[SEC_MAIN_COMBINE].verts;
+ u32 svp_polys = d.sec[SEC_SVP_GBUFFER].polys + d.sec[SEC_SVP_LIGHTS].polys
+ + d.sec[SEC_SVP_EMISSIVE].polys + d.sec[SEC_SVP_COMBINE].polys;
+ u32 main_polys = d.sec[SEC_MAIN_GBUFFER].polys + d.sec[SEC_MAIN_LIGHTS].polys
+ + d.sec[SEC_MAIN_COMBINE].polys;
+
+ CGameFont& F = *s_font;
+ const float H = (float)Device.dwHeight;
+ const float W = (float)Device.dwWidth;
+ const float hi = 0.0135f; // font height as a screen fraction, readable at 1080p and 4k
+ F.SetHeightI(hi);
+ const float line = H * hi;
+ const float step = line * 1.32f;
+ const float pad = line * 0.7f;
+ const float digit = F.SizeOf_("0");
+ const float cell = digit * 7.5f; // widest data cell "1234567" / "123.45"
+ const float label_w = F.SizeOf_("emissive") + digit;
+ const float gap = digit * 1.5f;
+
+ // rolling ~1s frame-time window, min/avg/max exposes hitches vs steady cost
+ float ft_min = 0.f, ft_max = 0.f, ft_avg = 0.f;
+ {
+ const u32 now = Device.dwTimeGlobal;
+ double sum = 0.0; u32 cnt = 0;
+ for (u32 i = 0; i < FT_WIN; ++i)
+ {
+ if (s_ft_time[i] == 0 || now - s_ft_time[i] > 1000)
+ continue;
+ const float v = s_ft_ms[i];
+ if (cnt == 0 || v < ft_min) ft_min = v;
+ if (v > ft_max) ft_max = v;
+ sum += v; ++cnt;
+ }
+ if (cnt) ft_avg = (float)(sum / cnt);
+ }
+
+ // whole scene-render cpu vs gpu, the frame section wraps gbuffer through combine
+ const double fcpu = d.sec[SEC_FRAME].cpu_ms;
+ const double fgpu = d.sec[SEC_FRAME].gpu_ms;
+ LPCSTR bound = (fcpu > fgpu * 1.15) ? "cpu-bound" : (fgpu > fcpu * 1.15) ? "gpu-bound" : "even";
+
+ // free-form footer lines, built first so the panel sizes its width to the widest of them
+ char foot[FOOT_MAX][96];
+ u32 nf = 0;
+ xr_sprintf(foot[nf++], "frame %.2f ms %.0f fps", d.frame_ms, d.frame_ms > 0.01 ? 1000.0 / d.frame_ms : 0.0);
+ xr_sprintf(foot[nf++], "cpu %.2f gpu %.2f ms", fcpu, fgpu);
+ // bound verdict on its own row so it stays legible against the moving numbers
+ xr_sprintf(foot[nf++], "%s", bound);
+ xr_sprintf(foot[nf++], "1s min %.2f avg %.2f max %.2f", ft_min, ft_avg, ft_max);
+ xr_sprintf(foot[nf++], "svp %ux%u mag %.1fx epoch %u res %u", d.svp_w, d.svp_h, d.svp_mag, d.svp_epoch, d.optic_resolve);
+ xr_sprintf(foot[nf++], "res learn %.0f apply %.0f side %u grow %s", d.svp_disc_learned, d.svp_disc, d.svp_w, d.svp_grow ? "on" : "off");
+ xr_sprintf(foot[nf++], "cull ssa %u rej %u i%u hud %u grass %u lights m%u s%u", d.ssa_culled, d.cull_reject, d.cull_reject_ident, d.hud_cull_reject, d.grass_cull_reject, d.lights_mirrored, d.lights_skipped);
+ if (full)
+ {
+ xr_sprintf(foot[nf++], "stamp taa %u nvg %u distort %u nvgsky %u", d.taa_stamp, d.nvg_split, d.distort_guard, d.nvg_sky);
+ xr_sprintf(foot[nf++], "fire lod %u reflex %u disc %u fwd %u", d.lod_scale, d.reflex_proxy, d.disc_latch, d.fwd_keep);
+ }
+
+ const u32 lines = 1u + (full ? 4u : 0u) + 7u + nf; // header + section gpu + fixed columns + footer
+
+ const float col_w = label_w + cell + gap + cell;
+ float content_w = col_w;
+ for (u32 i = 0; i < nf; ++i)
+ content_w = _max(content_w, F.SizeOf_(foot[i]));
+ const float panel_w = content_w + 2.f * pad;
+ const float panel_h = lines * step + 2.f * pad;
+ const float right = W - W * 0.012f;
+ const float panel_l = right - panel_w;
+ const float top = H * 0.02f;
+ const float label_l = panel_l + pad;
+ const float svp_r = label_l + label_w + cell;
+ const float main_r = svp_r + gap + cell;
+
+ // dark translucent backing rect at ~75% over the opaque-white ui texture
+ u32 back = color_rgba(14, 17, 21, 191);
+ UIRender->SetShader(**s_shader);
+ UIRender->StartPrimitive(6, IUIRender::ptTriList, IUIRender::pttTL);
+ UIRender->PushPoint(panel_l, top, 0.f, back, 0.f, 0.f);
+ UIRender->PushPoint(panel_l + panel_w, top, 0.f, back, 1.f, 0.f);
+ UIRender->PushPoint(panel_l + panel_w, top + panel_h, 0.f, back, 1.f, 1.f);
+ UIRender->PushPoint(panel_l, top, 0.f, back, 0.f, 0.f);
+ UIRender->PushPoint(panel_l + panel_w, top + panel_h, 0.f, back, 1.f, 1.f);
+ UIRender->PushPoint(panel_l, top + panel_h, 0.f, back, 0.f, 1.f);
+ UIRender->FlushPrimitive();
+
+ const u32 c_hdr = color_rgba(175, 200, 235, 255);
+ const u32 c_txt = color_rgba(205, 218, 218, 255);
+ const u32 c_dim = color_rgba(120, 130, 130, 255);
+ float y = top + pad;
+
+ auto row = [&](u32 lc, LPCSTR label, u32 sc, LPCSTR sv, u32 mc, LPCSTR mv, bool has_m)
+ {
+ F.SetAligment(CGameFont::alLeft); F.SetColor(lc); F.Out(label_l, y, "%s", label);
+ F.SetAligment(CGameFont::alRight); F.SetColor(sc); F.Out(svp_r, y, "%s", sv);
+ if (has_m) { F.SetColor(mc); F.Out(main_r, y, "%s", mv); }
+ y += step;
+ };
+
+ row(c_hdr, "svp stats", c_hdr, "SVP", c_hdr, "MAIN", true);
+
+ char sb[24], mb[24];
+ if (full)
+ {
+ xr_sprintf(sb, "%.2f", d.sec[SEC_SVP_GBUFFER].gpu_ms); xr_sprintf(mb, "%.2f", d.sec[SEC_MAIN_GBUFFER].gpu_ms);
+ row(c_txt, "gbuffer", gpu_color(d.sec[SEC_SVP_GBUFFER].gpu_ms), sb, gpu_color(d.sec[SEC_MAIN_GBUFFER].gpu_ms), mb, true);
+ xr_sprintf(sb, "%.2f", d.sec[SEC_SVP_LIGHTS].gpu_ms); xr_sprintf(mb, "%.2f", d.sec[SEC_MAIN_LIGHTS].gpu_ms);
+ row(c_txt, "lights", gpu_color(d.sec[SEC_SVP_LIGHTS].gpu_ms), sb, gpu_color(d.sec[SEC_MAIN_LIGHTS].gpu_ms), mb, true);
+ xr_sprintf(sb, "%.2f", d.sec[SEC_SVP_EMISSIVE].gpu_ms);
+ row(c_txt, "emissive", gpu_color(d.sec[SEC_SVP_EMISSIVE].gpu_ms), sb, c_dim, "-", true);
+ xr_sprintf(sb, "%.2f", d.sec[SEC_SVP_COMBINE].gpu_ms); xr_sprintf(mb, "%.2f", d.sec[SEC_MAIN_COMBINE].gpu_ms);
+ row(c_txt, "combine", gpu_color(d.sec[SEC_SVP_COMBINE].gpu_ms), sb, gpu_color(d.sec[SEC_MAIN_COMBINE].gpu_ms), mb, true);
+ }
+
+ xr_sprintf(sb, "%.2f", svp_gpu); xr_sprintf(mb, "%.2f", main_gpu);
+ row(c_txt, "gpu ms", gpu_color(svp_gpu), sb, gpu_color(main_gpu), mb, true);
+ xr_sprintf(sb, "%.2f", svp_cpu); xr_sprintf(mb, "%.2f", main_cpu);
+ row(c_txt, "cpu ms", c_txt, sb, c_txt, mb, true);
+ fmt_count(sb, sizeof(sb), svp_calls); fmt_count(mb, sizeof(mb), main_calls);
+ row(c_txt, "draws", c_txt, sb, c_txt, mb, true);
+ fmt_count(sb, sizeof(sb), svp_verts); fmt_count(mb, sizeof(mb), main_verts);
+ row(c_txt, "verts", c_txt, sb, c_txt, mb, true);
+ fmt_count(sb, sizeof(sb), svp_polys); fmt_count(mb, sizeof(mb), main_polys);
+ row(c_txt, "polys", c_txt, sb, c_txt, mb, true);
+ xr_sprintf(sb, "%u", d.svp_blends); xr_sprintf(mb, "%u", d.main_lights);
+ row(c_txt, "lights", c_txt, sb, c_txt, mb, true);
+ xr_sprintf(mb, "%u", d.main_shadowed);
+ row(c_txt, "shadow", c_dim, "-", c_txt, mb, true);
+
+ // footer, the free-form lines built above, first line highlighted
+ F.SetAligment(CGameFont::alLeft);
+ for (u32 i = 0; i < nf; ++i)
+ {
+ F.SetColor(i == 0 ? c_hdr : c_txt);
+ F.Out(label_l, y, "%s", foot[i]);
+ y += step;
+ }
+
+ F.OnRender();
+ }
+
+ void release()
+ {
+ for (u32 f = 0; f < RING; ++f)
+ {
+ frame_slot& s = s_frames[f];
+ _RELEASE(s.disjoint);
+ for (u32 i = 0; i < MAX_PAIRS * 2; ++i)
+ _RELEASE(s.ts[i]);
+ s.pair_count = 0; s.overflow = false; s.in_flight = false;
+ }
+ xr_delete(s_font);
+ if (s_shader) { xr_delete(s_shader); s_shader = nullptr; }
+ s_cur = nullptr;
+ s_created = false;
+ s_snap_valid = false;
+ s_frame_timer_started = false;
+ }
+}
diff --git a/src/Layers/xrRenderPC_R4/svp_stats.h b/src/Layers/xrRenderPC_R4/svp_stats.h
new file mode 100644
index 0000000000..b22921a3ce
--- /dev/null
+++ b/src/Layers/xrRenderPC_R4/svp_stats.h
@@ -0,0 +1,43 @@
+#ifndef svp_statsH
+#define svp_statsH
+#pragma once
+
+// pip per-viewport render-stats overlay, gated by r__svp_stats
+// every call self-gates, at r__svp_stats 0 no queries exist and no work runs
+
+namespace svp_stats
+{
+ enum section_e
+ {
+ SEC_MAIN_GBUFFER = 0,
+ SEC_MAIN_LIGHTS,
+ SEC_MAIN_COMBINE,
+ SEC_SVP_GBUFFER,
+ SEC_SVP_LIGHTS,
+ SEC_SVP_EMISSIVE,
+ SEC_SVP_COMBINE,
+ SEC_FRAME,
+ SEC_COUNT
+ };
+
+ // frame boundary, disjoint query + ring advance, render thread inside Render() only
+ void frame_begin();
+ void frame_end(bool svp_active);
+
+ // timing window, cpu qpc + gpu timestamp pair + rcache deltas, multiple pairs per frame accumulate
+ void section_begin(section_e s);
+ void section_end(section_e s);
+
+ // light and shadow tallies at the accumulation sites
+ void note_main_lights(u32 total, u32 shadowed);
+ void note_svp_blend();
+ void note_sun();
+
+ // two-column panel, main viewport, drawn onto the bound ldr target
+ void draw_overlay();
+
+ // release the query pool + font, device reset or disable
+ void release();
+}
+
+#endif
diff --git a/src/Layers/xrRenderPC_R4/svp_target.cpp b/src/Layers/xrRenderPC_R4/svp_target.cpp
new file mode 100644
index 0000000000..e7f0a1198c
--- /dev/null
+++ b/src/Layers/xrRenderPC_R4/svp_target.cpp
@@ -0,0 +1,121 @@
+#include "stdafx.h"
+#include "r4.h"
+#include "svp_target.h"
+
+// pip SVP scene render extent, EXACT svp_height at gate 0 (byte-identical to stock, no scale and no
+// even rounding), scaled + even-rounded for the upscaler only once the DLSS scaffolding is on
+u32 svp_render_extent()
+{
+ if (ps_r__svp_dlss == 0)
+ {
+ // base SVP square = full display-derived svp_height, or (adaptive) just past the on-screen eyepiece
+ // disc so we never rasterise pixels the lens cannot show. supersample then scales the chosen base up
+ u32 base = Device.svp_height();
+ const float disc = Device.m_SecondViewport.svp_disc_applied;
+ extern int ps_r__svp_flat_window;
+ extern Fvector4 ps_s3ds_param_3;
+ extern float ps_r__svp_adaptive_res;
+ if (ps_r__svp_flat_window && (int)ps_s3ds_param_3.y == 8 && disc > 1.0f)
+ {
+ // a flat panel (binocular) fills far more screen than a round ocular disc, size the square SVP
+ // up to the panel extent so the wide window is not upscaled, capped at the main height for cost
+ u32 want = u32(disc) & ~1u;
+ const u32 cap = Device.dwHeight & ~1u;
+ if (want > cap) want = cap;
+ if (want > base) base = want;
+ }
+ else if (ps_r__svp_adaptive_res > 0.f)
+ {
+ if (disc > 1.0f)
+ {
+ u32 want = u32(disc * ps_r__svp_adaptive_res) & ~1u; // disc * margin, even side
+ if (want < 256) want = 256; // floor: never degenerate on a tiny/far ocular
+ extern int ps_r__svp_adaptive_grow;
+ if (ps_r__svp_adaptive_grow)
+ {
+ // the disc is the requirement in both directions, a big ocular renders 1:1
+ // instead of upscaling soft, capped at the display height for cost
+ const u32 cap = Device.dwHeight & ~1u;
+ base = (want > cap) ? cap : want;
+ }
+ else if (want < base)
+ base = want; // legacy shrink-only under the half-height base
+ }
+ }
+ float ss = ps_r__svp_supersample;
+ clamp(ss, 1.0f, 2.0f);
+ if (base == Device.svp_height() && ss <= 1.0f)
+ return Device.svp_height(); // exact stock, byte-identical (off == unchanged)
+ u32 e = u32(base * ss) & ~1u;
+ return (e < 2) ? 2 : e;
+ }
+ float eff = ps_r__svp_render_scale;
+ clamp(eff, 0.5f, 1.0f);
+ if (eff >= 1.0f)
+ return Device.svp_height(); // no downscale, full res so the eval is a pass-through (CopyResource)
+ u32 e = u32(Device.svp_height() * eff) & ~1u; // even side for the upscaler
+ if (e < 2)
+ e = 2;
+ return e;
+}
+
+// pip round a target side UP to a 32px step so a few-px disc wobble reuses the target instead of
+// reallocating the whole SVP rt chain on every ADS, quality only rounds up never below the ask
+static inline u32 svp_quant32(u32 v) { return (v + 31u) & ~31u; }
+
+// pip the SVP target extent, one policy for the allocation and the camera projection. a flat panel
+// shapes the square down to the lens aspect (aspect is major/minor so always >= 1), else square
+void svp_target_wh(u32& svp_w, u32& svp_h)
+{
+ u32 svp_side = svp_quant32(svp_render_extent());
+ if (svp_side < 64)
+ svp_side = 64;
+ svp_w = svp_h = svp_side;
+ extern int ps_r__svp_flat_window;
+ extern Fvector4 ps_s3ds_param_3;
+ const float pa = Device.m_SecondViewport.svp_panel_aspect;
+ if (ps_r__svp_flat_window && (int)ps_s3ds_param_3.y == 8 && pa > 1.01f)
+ {
+ svp_h = svp_quant32(u32(svp_side / pa));
+ if (svp_h < 64)
+ svp_h = 64;
+ }
+}
+
+// pip allocate the square SVP target the first time an SVP pass runs, with PiP off this is never
+// called so the SVP RTs cost nothing, the scene render uses svp_render_extent (display-res at gate 0)
+void CRender::EnsureTargetSVP()
+{
+ if (TargetMain)
+ TargetMain->EnsureScopeShaders(); // pip load the lens glue shaders on first aim (idempotent)
+ u32 svp_w, svp_h;
+ svp_target_wh(svp_w, svp_h);
+ if (TargetSVP)
+ {
+ // pip recreate if the DLSS gate flipped (gbuffer + rt_secondVP UAV change at gate != 0) or the
+ // supersample / panel-aspect changed the size, so r__svp_supersample applies live (no vid_restart)
+ const bool gate_ok = ((ps_r__svp_dlss != 0) == TargetSVP->m_svp_dlss_built);
+ const bool size_ok = (TargetSVP->Width == svp_w && TargetSVP->Height == svp_h);
+ if (gate_ok && size_ok)
+ return;
+ xr_delete(TargetSVP);
+ }
+ TargetSVP = xr_new("svp", svp_w, svp_h);
+ // CHK_DX is a no-op so a VRAM-exhausted creation fails silently, never keep a half-built target
+ const bool svp_rt_ok = TargetSVP->rt_secondVP && TargetSVP->rt_secondVP->pSurface
+ && TargetSVP->rt_baseRT && TargetSVP->rt_baseRT->pRT
+ && TargetSVP->rt_baseZB && TargetSVP->rt_baseZB->pZRT
+ && TargetSVP->rt_Color && TargetSVP->rt_Color->pRT
+ && TargetSVP->rt_Position && TargetSVP->rt_Position->pRT;
+ if (!svp_rt_ok)
+ {
+ Msg("! [SVP-ALLOC] SVP target creation FAILED (out of VRAM?), scope pass disabled");
+ xr_delete(TargetSVP);
+ return;
+ }
+ Device.m_SecondViewport.dlss_reset_next = true; // pip DLSS history reset, SVP surface (re)created incl. res change
+ extern int ps_r__svp_diag; extern float ps_r__svp_supersample;
+ if (ps_r__svp_diag)
+ PipMsg("[SVP-ALLOC] svp=%ux%u (svp_height=%u aspect=%.2f supersample=%.2f dlss=%d)",
+ svp_w, svp_h, Device.svp_height(), Device.m_SecondViewport.svp_panel_aspect, ps_r__svp_supersample, ps_r__svp_dlss);
+}
diff --git a/src/Layers/xrRenderPC_R4/svp_target.h b/src/Layers/xrRenderPC_R4/svp_target.h
new file mode 100644
index 0000000000..315019e78a
--- /dev/null
+++ b/src/Layers/xrRenderPC_R4/svp_target.h
@@ -0,0 +1,11 @@
+#ifndef svp_targetH
+#define svp_targetH
+#pragma once
+
+// pip SVP scene render extent for the color/depth/MV targets, exact svp_height at gate 0
+u32 svp_render_extent();
+
+// pip the SVP target extent, one policy for the allocation and the camera projection
+void svp_target_wh(u32& svp_w, u32& svp_h);
+
+#endif
diff --git a/src/Layers/xrRenderPC_R4/xrRender_R4.vcxproj b/src/Layers/xrRenderPC_R4/xrRender_R4.vcxproj
index 274570d703..bb7ceb3459 100644
--- a/src/Layers/xrRenderPC_R4/xrRender_R4.vcxproj
+++ b/src/Layers/xrRenderPC_R4/xrRender_R4.vcxproj
@@ -525,6 +525,10 @@
+
+
+
+
@@ -562,6 +566,9 @@
+
+
+
@@ -613,6 +620,7 @@
+
@@ -704,6 +712,12 @@
+
+
+ NotUsing
+
+
+
@@ -727,6 +741,7 @@
+
@@ -760,6 +775,11 @@
+
+
+
+
+
diff --git a/src/Layers/xrRenderPC_R4/xrRender_R4.vcxproj.filters b/src/Layers/xrRenderPC_R4/xrRender_R4.vcxproj.filters
index 5a751ca2f2..0455282b69 100644
--- a/src/Layers/xrRenderPC_R4/xrRender_R4.vcxproj.filters
+++ b/src/Layers/xrRenderPC_R4/xrRender_R4.vcxproj.filters
@@ -201,6 +201,18 @@
Kernel
+
+ Kernel
+
+
+ Kernel
+
+
+ Kernel
+
+
+ Refactored\Execution & 3D\Shaders\Blender
+
Core
@@ -384,6 +396,15 @@
Core_Target
+
+ Core_Target
+
+
+ Core
+
+
+ Core
+
Core_Target
@@ -801,6 +822,9 @@
Kernel
+
+ Kernel
+
Kernel
@@ -816,6 +840,21 @@
Core
+
+ Core
+
+
+ Core_Target
+
+
+ Core_Target
+
+
+ Core
+
+
+ Core
+
Core
@@ -831,6 +870,18 @@
Core
+
+ Core
+
+
+ Core
+
+
+ Core
+
+
+ Core
+
Core
@@ -1209,6 +1260,9 @@
Refactored\Execution & 3D\Shaders\Blender
+
+ Refactored\Execution & 3D\Shaders\Blender
+
Refactored\Execution & 3D\Shaders\Blender
diff --git a/src/xrCore/Xr_ini.cpp b/src/xrCore/Xr_ini.cpp
index 371d807fa5..e0f472044a 100644
--- a/src/xrCore/Xr_ini.cpp
+++ b/src/xrCore/Xr_ini.cpp
@@ -1599,6 +1599,19 @@ BOOL CInifile::line_exist(const shared_str& S, const shared_str& L) const { retu
u32 CInifile::line_count(const shared_str& S) const { return line_count(*S); }
BOOL CInifile::section_exist(const shared_str& S) const { return section_exist(*S); }
+// pip read-only view of a section's direct inheritance parents (override declarations take priority over
+// base), or nullptr if the section has none / does not exist
+const RStringVec* CInifile::get_section_parents(const shared_str& S) const
+{
+ auto ito = OverrideParentDataMap.find(S);
+ if (ito != OverrideParentDataMap.end())
+ return &ito->second;
+ auto itb = BaseParentDataMap.find(S);
+ if (itb != BaseParentDataMap.end())
+ return &itb->second;
+ return nullptr;
+}
+
//--------------------------------------------------------------------------------------
// Read functions
//--------------------------------------------------------------------------------------
diff --git a/src/xrCore/_matrix.h b/src/xrCore/_matrix.h
index 0f061fc0e3..49dddba739 100644
--- a/src/xrCore/_matrix.h
+++ b/src/xrCore/_matrix.h
@@ -718,6 +718,18 @@ struct _matrix
return *this;
}
+ // inverse of build_projection
+ IC void decompose_projection(T& fFov, T& fAspect, T& fNearPlane, T& fFarPlane)
+ {
+ T Q = _33;
+ T w = _11;
+ T h = _22;
+ fNearPlane = _43 / -Q;
+ fFarPlane = (fNearPlane * Q) / (Q - 1.0f);
+ fAspect = w / h;
+ fFov = 2.0 * atanf(1.0f / h);
+ }
+
IC SelfRef build_projection_ortho(T w, T h, T zn, T zf)
{
_11 = T(2) / w;
diff --git a/src/xrCore/log.h b/src/xrCore/log.h
index ac17469419..f376bff474 100644
--- a/src/xrCore/log.h
+++ b/src/xrCore/log.h
@@ -4,6 +4,7 @@
#define VPUSH(a) ((a).x), ((a).y), ((a).z)
void XRCORE_API Msg (const char* format, ...);
+#include "svp_log.h" // pip PipMsg, chain-included so every consumer keeps seeing it
// Old shit
void XRCORE_API Log (const char* msg);
diff --git a/src/xrCore/svp_log.cpp b/src/xrCore/svp_log.cpp
new file mode 100644
index 0000000000..7d05751a5c
--- /dev/null
+++ b/src/xrCore/svp_log.cpp
@@ -0,0 +1,45 @@
+#include "stdafx.h"
+
+#include "svp_log.h"
+
+// pip diagnostics land in $logs$\pip.log, one stamped line per call, flushed per line
+void PipMsg(const char* format, ...)
+{
+ if (!format)
+ return;
+
+ static xrCriticalSection pipGuard;
+ static FILE* pipFile = nullptr;
+ static bool pipFailed = false;
+ static u64 pipT0 = 0;
+
+ string2048 buf;
+ va_list mark;
+ va_start(mark, format);
+ int sz = _vsnprintf(buf, sizeof(buf) - 1, format, mark);
+ buf[sizeof(buf) - 1] = 0;
+ va_end(mark);
+ if (sz <= 0)
+ return;
+
+ xrCriticalSectionGuard g(&pipGuard);
+ if (pipFailed)
+ return;
+ if (!pipFile)
+ {
+ string_path fn;
+ xr_strcpy(fn, "pip.log");
+ if (FS.path_exist("$logs$"))
+ FS.update_path(fn, "$logs$", "pip.log");
+ pipFile = fopen(fn, "w");
+ if (!pipFile)
+ {
+ pipFailed = true;
+ return;
+ }
+ pipT0 = GetTickCount64();
+ }
+ const u64 ms = GetTickCount64() - pipT0;
+ fprintf(pipFile, "[%6llu.%03llu] %s\n", ms / 1000, ms % 1000, buf);
+ fflush(pipFile);
+}
diff --git a/src/xrCore/svp_log.h b/src/xrCore/svp_log.h
new file mode 100644
index 0000000000..88e312a161
--- /dev/null
+++ b/src/xrCore/svp_log.h
@@ -0,0 +1,8 @@
+#ifndef svp_logH
+#define svp_logH
+#pragma once
+
+// pip diagnostics land in $logs$\pip.log so the main log stays readable
+void XRCORE_API PipMsg (const char* format, ...);
+
+#endif
diff --git a/src/xrCore/xrCore.vcxproj b/src/xrCore/xrCore.vcxproj
index e09f8ca46e..93d9cfede3 100644
--- a/src/xrCore/xrCore.vcxproj
+++ b/src/xrCore/xrCore.vcxproj
@@ -574,6 +574,7 @@
+
@@ -699,6 +700,7 @@
+
diff --git a/src/xrCore/xrCore.vcxproj.filters b/src/xrCore/xrCore.vcxproj.filters
index 0da4d164cd..50f84c6698 100644
--- a/src/xrCore/xrCore.vcxproj.filters
+++ b/src/xrCore/xrCore.vcxproj.filters
@@ -144,6 +144,9 @@
FS
+
+ FS
+
FS
@@ -444,6 +447,9 @@
FS
+
+ FS
+
FS
diff --git a/src/xrCore/xr_ini.h b/src/xrCore/xr_ini.h
index 621d496568..97822c4e99 100644
--- a/src/xrCore/xr_ini.h
+++ b/src/xrCore/xr_ini.h
@@ -272,6 +272,9 @@ class XRCORE_API CInifile
u32 section_count() const;
BOOL section_exist(LPCSTR S) const;
BOOL section_exist(const shared_str& S) const;
+ // pip direct inheritance parents of a section ([sect]:A, B), override map first then base, lets the
+ // game recover a scope's true config when X-Ray's first-parent-wins merge let a base override the value
+ const RStringVec* get_section_parents(const shared_str& S) const;
Root& sections() { return DATA; }
Root const& sections() const { return DATA; }
diff --git a/src/xrEngine/CameraManager.cpp b/src/xrEngine/CameraManager.cpp
index 9471a2db1f..a0c8f15952 100644
--- a/src/xrEngine/CameraManager.cpp
+++ b/src/xrEngine/CameraManager.cpp
@@ -501,7 +501,7 @@ void CCameraManager::ApplyDevice(float _viewport_near)
Device.fASPECT = m_cam_info.fAspect;
//--#SM+# Begin-- +SecondVP+
// Recalculate scene FOV for SecondVP frame
- if (Device.m_SecondViewport.IsSVPFrame())
+ if (!Device.true_pip_on && Device.m_SecondViewport.IsSVPFrame())
{
// For the second viewport, set FOV from HUD shader constants
Device.fFOV = g_pGamePersistent->m_pGShaderConstants->hud_params.y;
@@ -513,7 +513,12 @@ void CCameraManager::ApplyDevice(float _viewport_near)
Device.m_SecondViewport.isCamReady = false;
Device.mProject.build_projection(deg2rad(Device.fFOV), m_cam_info.fAspect, _viewport_near, m_cam_info.fFar);
- Device.mProjectHud.build_projection(deg2rad(psHUD_FOV * 83.f), Device.fASPECT, R_VIEWPORT_NEAR, m_cam_info.fFar);
+ // pip true hud fov renders the weapon at the scene perspective while fully aimed through a PiP scope
+ extern int g_svp_hud_true_fov;
+ const float hud_fov_deg = (g_svp_hud_true_fov && Device.true_pip_on && Device.m_SecondViewport.IsSVPActive()
+ && g_pGamePersistent && g_pGamePersistent->m_pGShaderConstants->hud_params.x > 0.999f)
+ ? Device.fFOV : psHUD_FOV * 83.f;
+ Device.mProjectHud.build_projection(deg2rad(hud_fov_deg), Device.fASPECT, R_VIEWPORT_NEAR, m_cam_info.fFar);
Device.mInvProject.invert(Device.mProject);
Device.mInvProjectHud.invert(Device.mProjectHud);
@@ -522,29 +527,33 @@ void CCameraManager::ApplyDevice(float _viewport_near)
if (g_pGamePersistent && g_pGamePersistent->m_pMainMenu->IsActive())
ResetPP();
else
- {
- pp_affected.validate("apply device");
- // postprocess
- IRender_Target* T = ::Render->getTarget();
- T->set_duality_h(pp_affected.duality.h);
- T->set_duality_v(pp_affected.duality.v);
- T->set_blur(pp_affected.blur);
- T->set_gray(pp_affected.gray);
- T->set_noise(pp_affected.noise.intensity);
-
- clamp(pp_affected.noise.grain, EPS_L, 1000.0f);
-
- T->set_noise_scale(pp_affected.noise.grain);
-
- T->set_noise_fps(pp_affected.noise.fps);
- T->set_color_base(pp_affected.color_base);
- T->set_color_gray(pp_affected.color_gray);
- T->set_color_add(pp_affected.color_add);
-
- T->set_cm_imfluence(pp_affected.cm_influence);
- T->set_cm_interpolate(pp_affected.cm_interpolate);
- T->set_cm_textures(pp_affected.cm_tex1, pp_affected.cm_tex2);
- }
+ ApplyPP();
+}
+
+// pip the pp half of ApplyDevice, callable on its own
+void CCameraManager::ApplyPP()
+{
+ pp_affected.validate("apply device");
+ // postprocess
+ IRender_Target* T = ::Render->getTarget();
+ T->set_duality_h(pp_affected.duality.h);
+ T->set_duality_v(pp_affected.duality.v);
+ T->set_blur(pp_affected.blur);
+ T->set_gray(pp_affected.gray);
+ T->set_noise(pp_affected.noise.intensity);
+
+ clamp(pp_affected.noise.grain, EPS_L, 1000.0f);
+
+ T->set_noise_scale(pp_affected.noise.grain);
+
+ T->set_noise_fps(pp_affected.noise.fps);
+ T->set_color_base(pp_affected.color_base);
+ T->set_color_gray(pp_affected.color_gray);
+ T->set_color_add(pp_affected.color_add);
+
+ T->set_cm_imfluence(pp_affected.cm_influence);
+ T->set_cm_interpolate(pp_affected.cm_interpolate);
+ T->set_cm_textures(pp_affected.cm_tex1, pp_affected.cm_tex2);
}
void CCameraManager::ResetPP()
diff --git a/src/xrEngine/CameraManager.h b/src/xrEngine/CameraManager.h
index c9cde19a99..9ea89cdef7 100644
--- a/src/xrEngine/CameraManager.h
+++ b/src/xrEngine/CameraManager.h
@@ -178,6 +178,7 @@ class ENGINE_API CCameraManager
void UpdateFromCamera(const CCameraBase* C);
void ApplyDevice(float _viewport_near);
+ void ApplyPP();
static void ResetPP();
CCameraManager(bool bApplyOnUpdate);
diff --git a/src/xrEngine/XR_IOConsole.cpp b/src/xrEngine/XR_IOConsole.cpp
index b73496ecf3..b8cd09d17d 100644
--- a/src/xrEngine/XR_IOConsole.cpp
+++ b/src/xrEngine/XR_IOConsole.cpp
@@ -12,6 +12,7 @@
#include "xr_input.h"
#include "xr_ioc_cmd.h"
#include "GameFont.h"
+#include "svp_crash_context.h"
#include "../Include/xrRender/UIRender.h"
@@ -43,6 +44,7 @@ static void DumpConsoleVariablesOnCrash()
{
if (Console)
Console->Execute("dump_cvar");
+ svp_crash_context_dump(); // svp runtime latch state alongside the cvar dump
}
extern char const* const ioc_prompt;
diff --git a/src/xrEngine/bone.h b/src/xrEngine/bone.h
index a15138e230..a1794f041f 100644
--- a/src/xrEngine/bone.h
+++ b/src/xrEngine/bone.h
@@ -30,8 +30,8 @@ class ENGINE_API CBoneInstance
Fmatrix mTransform; // final x-form matrix (local to model)
Fmatrix mTransformHidden; // hidden x-form matrix (local to model)
Fmatrix mRenderTransform; // final x-form matrix (model_base -> bone -> model)
- Fmatrix mRenderTransform_prev; // Prev x-form matrix
- Fmatrix mRenderTransform_temp; // Temp var
+ Fmatrix mRenderTransform_prev[2]; // Prev x-form matrix, per viewport (main, SVP)
+ Fmatrix mRenderTransform_temp[2]; // Temp var, per viewport
private:
BoneCallback Callback;
void* Callback_Param;
diff --git a/src/xrEngine/device.cpp b/src/xrEngine/device.cpp
index b161e00a9e..56e48dbafd 100644
--- a/src/xrEngine/device.cpp
+++ b/src/xrEngine/device.cpp
@@ -420,6 +420,14 @@ void CRenderDevice::on_idle()
mFullTransformCam.mul(mProjectCam, mView);
m_pRender->SetCacheXform(mView, mProject);
+ // advance per-viewport history and store the main camera in slot 0
+ // slot 1 = SVP, filled by svpCamera in the render layer
+ Device.matrices_previous[0] = Device.matrices[0];
+ Device.matrices_previous[1] = Device.matrices[1];
+ Device.matrices[0].mView = mView;
+ Device.matrices[0].mProject = mProject;
+ Device.matrices[0].mProjectHud = mProjectHud;
+
mViewHud_prev = mViewHud;
mProjectHud_prev = mProjectHud;
mFullTransformHud_prev = mFullTransformHud;
@@ -434,7 +442,11 @@ void CRenderDevice::on_idle()
m_pRender->SetCacheXform_prev(mView_prev, mProject_prev);
- mProjectHud.build_projection(deg2rad(psHUD_FOV * 83.f), fASPECT, R_VIEWPORT_NEAR, g_pGamePersistent->Environment().CurrentEnv->far_plane);
+ // pip true hud fov renders the weapon at the scene perspective while fully aimed through a PiP scope
+ extern int g_svp_hud_true_fov;
+ const float hud_fov_deg = (g_svp_hud_true_fov && true_pip_on && m_SecondViewport.IsSVPActive()
+ && g_pGamePersistent && g_pGamePersistent->m_pGShaderConstants->hud_params.x > 0.999f) ? fFOV : psHUD_FOV * 83.f;
+ mProjectHud.build_projection(deg2rad(hud_fov_deg), fASPECT, R_VIEWPORT_NEAR, g_pGamePersistent->Environment().CurrentEnv->far_plane);
mProjectCam.build_projection(deg2rad(83.f), fASPECT, R_VIEWPORT_NEAR, g_pGamePersistent->Environment().CurrentEnv->far_plane);
mViewHud.set(mView);
@@ -905,14 +917,29 @@ void CLoadScreenRenderer::OnRender()
pApp->load_draw_internal();
}
-void CRenderDevice::CSecondVPParams::SetSVPActive(bool bState) //--#SM+#-- +SecondVP+
+void CSecondVPParams::SetSVPActive(bool bState) //--#SM+#-- +SecondVP+
{
+ if (bState && !isActive)
+ dlss_reset_next = true; // pip DLSS history reset on ADS-in (logic thread)
isActive = bState;
if (g_pGamePersistent != NULL)
g_pGamePersistent->m_pGShaderConstants->m_blender_mode.z = (isActive ? 1.0f : 0.0f);
}
-bool CRenderDevice::CSecondVPParams::IsSVPFrame() //--#SM+#-- +SecondVP+
+bool CSecondVPParams::IsSVPFrame() //--#SM+#-- +SecondVP+
{
+ if (Device.true_pip_on)
+ return m_render_pass_is_svp;
return IsSVPActive() && Device.dwFrame % frameDelay == 0;
}
+
+void CRenderDevice::prepare_matrices()
+{
+ auto svp = m_SecondViewport.IsSVPFrame();
+ // per-viewport previous matrices (0 = main, 1 = SVP) for motion vectors
+ mView_prev = Device.matrices_previous[svp].mView;
+ mProject_prev = Device.matrices_previous[svp].mProject;
+ m_pRender->SetCacheXform_prev(mView_prev, mProject_prev);
+ // grass + wind prev stay once-per-frame in the device frame fn, not here, because
+ // prepare_matrices runs per SetActive and wind prev=saved/saved=cur is not idempotent
+}
diff --git a/src/xrEngine/device.h b/src/xrEngine/device.h
index c649b73655..8fb43bdf6e 100644
--- a/src/xrEngine/device.h
+++ b/src/xrEngine/device.h
@@ -7,6 +7,9 @@
//class ENGINE_API CResourceManager;
//class ENGINE_API CGammaControl;
+#include
+#include
+
#include "pure.h"
//#include "hw.h"
#include "../xrcore/ftimer.h"
@@ -59,6 +62,11 @@ class ENGINE_API CRenderDeviceData
// Engine flow-control
u32 dwFrame;
+ // cache-clear key, bumped per viewport in SVP mode
+ u32 dwViewport = 0;
+ // sampled once per frame from r__svpscope, gates the hybrid IsSVPFrame
+ bool true_pip_on = false;
+
float fTimeDelta;
float fTimeGlobal;
u32 dwTimeDelta;
@@ -104,6 +112,16 @@ class ENGINE_API CRenderDeviceData
float fFOV;
float fASPECT;
float ViewportNear = 0.2f;
+
+ // per-viewport camera data, [0] = main, [1] = SVP
+ struct MatrixData
+ {
+ Fmatrix mView;
+ Fmatrix mProject;
+ Fmatrix mProjectHud;
+ };
+ MatrixData matrices[2];
+ MatrixData matrices_previous[2];
protected:
u32 Timer_MM_Delta;
@@ -137,29 +155,12 @@ class ENGINE_API CRenderDeviceBase :
};
// refs
+#include "svp_state.h" // pip the SVP cross-thread data bus
class ENGINE_API CRenderDevice : public CRenderDeviceBase
{
public:
- class ENGINE_API CSecondVPParams //--#SM+#-- +SecondVP+
- {
- bool isActive; // Oeaa aeoeaaoee ?aiaa?a ai aoi?ie au?ii?o
- u8 frameDelay; // Ia eaeii eaa?a n iiiaioa i?ioeiai ?aiaa?a ai aoi?ie au?ii?o iu ia?i?i iiaue
- //(ia ii?ao auou iaiuoa 2 - ea?aue aoi?ie eaa?, ?ai aieuoa oai aieaa ieceee FPS ai aoi?ii au?ii?oa)
-
- public:
- bool isCamReady; // Oeaa aioiaiinoe eaia?u (FOV, iiceoey, e o.i) e ?aiaa?o aoi?iai au?ii?oa
-
- IC bool IsSVPActive() { return isActive; }
- void SetSVPActive(bool bState);
- bool IsSVPFrame();
-
- IC u8 GetSVPFrameDelay() { return frameDelay; }
- void SetSVPFrameDelay(u8 iDelay)
- {
- frameDelay = iDelay;
- clamp(frameDelay, 2, u8(-1));
- }
- };
+ // pip the SVP cross-thread data bus lives in svp_state.h, the alias keeps qualified names resolving
+ using CSecondVPParams = ::CSecondVPParams;
private:
// Main objects used for creating and rendering the 3D scene
@@ -215,6 +216,7 @@ class ENGINE_API CRenderDevice : public CRenderDeviceBase
}
void DumpResourcesMemoryUsage() { m_pRender->ResourcesDumpMemoryUsage(); }
+ void prepare_matrices();
public:
// Registrators
//CRegistrator seqRender;
@@ -271,6 +273,10 @@ class ENGINE_API CRenderDevice : public CRenderDeviceBase
CSecondVPParams m_SecondViewport; //--#SM+#-- +SecondVP+
+ // SVP target is square, half the main height per side
+ u32 svp_width() { return svp_height(); }
+ u32 svp_height() { return dwHeight >> 1; }
+
//float fFOV;
//float fASPECT;
@@ -395,6 +401,36 @@ class ENGINE_API CRenderDevice : public CRenderDeviceBase
return hud_to_world(m, mProjectHud);
}
+ // pip explicit-camera variants, the defaults read the live device camera which demo_record
+ // replaces with the fly camera, callers can remap through the actor's real eye instead
+ Fvector& hud_to_world(Fvector& v, const Fmatrix& p, const Fmatrix& view, const Fmatrix& inv_view, const Fmatrix& inv_proj)
+ {
+ view.transform_tiny(v);
+ p.transform_tiny(v);
+ v.z -= ViewportNear;
+ inv_proj.transform_tiny(v);
+ inv_view.transform_tiny(v);
+ return v;
+ }
+
+ Fvector& hud_to_world_dir(Fvector& v, const Fmatrix& p, const Fmatrix& view, const Fmatrix& inv_view, const Fmatrix& inv_proj)
+ {
+ view.transform_dir(v);
+ p.transform_dir(v);
+ inv_proj.transform_dir(v);
+ inv_view.transform_dir(v);
+ return v;
+ }
+
+ Fmatrix& hud_to_world(Fmatrix& m, const Fmatrix& p, const Fmatrix& view, const Fmatrix& inv_view, const Fmatrix& inv_proj)
+ {
+ hud_to_world(m.c, p, view, inv_view, inv_proj);
+ hud_to_world_dir(m.i, p, view, inv_view, inv_proj).normalize();
+ hud_to_world_dir(m.j, p, view, inv_view, inv_proj).normalize();
+ hud_to_world_dir(m.k, p, view, inv_view, inv_proj).normalize();
+ return m;
+ }
+
Fvector& world_to_hud(Fvector& v, const Fmatrix& p)
{
mInvView.transform_tiny(v);
@@ -520,6 +556,14 @@ class ENGINE_API CRenderDevice : public CRenderDeviceBase
extern ENGINE_API CRenderDevice Device;
extern ENGINE_API CRenderDevice* DevicePtr;
+// pip clear a draw-list shared across the main + SVP passes only on the last pass to drain it
+// svp_first = SVP pass drains before main (combine order), false = main first (gbuffer/wmark order)
+inline bool svp_clear_shared_list(bool svp_first)
+{
+ if (!(Device.true_pip_on && Device.m_SecondViewport.IsSVPActive())) return true;
+ return svp_first ? !Device.m_SecondViewport.m_render_pass_is_svp : Device.m_SecondViewport.m_render_pass_is_svp;
+}
+
#ifndef _EDITOR
#define RDEVICE Device
#else
diff --git a/src/xrEngine/imgui_base.cpp b/src/xrEngine/imgui_base.cpp
index 6d334c9801..97a342adce 100644
--- a/src/xrEngine/imgui_base.cpp
+++ b/src/xrEngine/imgui_base.cpp
@@ -166,6 +166,11 @@ namespace xr_imgui
if (imgui_metrics) ImGui::ShowMetricsWindow(&imgui_metrics);
}
+ // Unique group draws every frame here, main thread inside the open frame
+ // replaces the script side AddUniqueCall pump which races render with mt_level_call
+ if (g_pGamePersistent)
+ g_pGamePersistent->ImGui_OnRender("Unique");
+
if (io.WantSetMousePos)
{
POINT pos = { (int)io.MousePos.x, (int)io.MousePos.y };
diff --git a/src/xrEngine/svp_crash_context.cpp b/src/xrEngine/svp_crash_context.cpp
new file mode 100644
index 0000000000..60dc6924cf
--- /dev/null
+++ b/src/xrEngine/svp_crash_context.cpp
@@ -0,0 +1,31 @@
+#include "stdafx.h"
+#include "svp_crash_context.h"
+
+// plain ring of the last few scoped frames, render thread writes, the crash path reads
+static const u32 SVP_CRASH_RING = 3;
+static SvpCrashFrame s_ring[SVP_CRASH_RING] = {};
+static u32 s_pushed = 0;
+
+void svp_crash_context_push(const SvpCrashFrame& f)
+{
+ s_ring[s_pushed % SVP_CRASH_RING] = f;
+ ++s_pushed;
+}
+
+void svp_crash_context_dump()
+{
+ if (s_pushed == 0)
+ {
+ Msg("[SVP-CRASH] no scoped frames captured this session");
+ return;
+ }
+ const u32 n = (s_pushed < SVP_CRASH_RING) ? s_pushed : SVP_CRASH_RING;
+ Msg("[SVP-CRASH] last %u scoped frame(s), newest first:", n);
+ for (u32 i = 0; i < n; ++i)
+ {
+ const SvpCrashFrame& f = s_ring[(s_pushed - 1 - i) % SVP_CRASH_RING];
+ Msg("[SVP-CRASH] frame=%u mode=%d active=%d rpass=%d front=%.3fm mag=%.2f fov=%.1fdeg disc=%.0fpx",
+ f.frame, f.mode, f.active ? 1 : 0, f.render_pass_is_svp ? 1 : 0,
+ f.hud_front_m, f.mag, f.fov_deg, f.disc_px);
+ }
+}
diff --git a/src/xrEngine/svp_crash_context.h b/src/xrEngine/svp_crash_context.h
new file mode 100644
index 0000000000..3bd3378643
--- /dev/null
+++ b/src/xrEngine/svp_crash_context.h
@@ -0,0 +1,23 @@
+#ifndef svp_crash_contextH
+#define svp_crash_contextH
+#pragma once
+
+// snapshot of svp (true-PiP) runtime latch state for one scoped frame, pushed from the renderer
+// and dumped into the crash log on the crash path (cvar values ride the existing dump_cvar dump)
+struct SvpCrashFrame
+{
+ u32 frame;
+ int mode; // r__svpscope
+ bool active; // IsSVPActive
+ bool render_pass_is_svp;
+ float hud_front_m;
+ float mag;
+ float fov_deg;
+ float disc_px;
+};
+
+// render thread pushes once per scoped frame, the crash handler reads the ring
+ENGINE_API void svp_crash_context_push(const SvpCrashFrame& f);
+ENGINE_API void svp_crash_context_dump();
+
+#endif
diff --git a/src/xrEngine/svp_gameplay_cvars.cpp b/src/xrEngine/svp_gameplay_cvars.cpp
new file mode 100644
index 0000000000..95ab651793
--- /dev/null
+++ b/src/xrEngine/svp_gameplay_cvars.cpp
@@ -0,0 +1,35 @@
+#include "stdafx.h"
+#include "svp_gameplay_cvars.h"
+#include "xr_ioconsole.h"
+#include "xr_ioc_cmd.h"
+
+float g_zoom_smooth = 12.f; // pip dynamic-scope zoom smoothing rate (lerp per second), 0 = instant stepped feel
+float g_zoom_analog = 0.f; // pip dynamic-scope analog zoom, 0 = discrete config steps, >0 = fine steps across the range
+int g_zoom_clicks = 1; // pip a scope authoring zoom_step_count 1 clicks between its two detents, ignoring analog and smoothing, 0 = off
+int g_svp_zoom_base = 1; // pip true svp scopes derive the bottom detent in the authored 75 base so it renders 1x, 0 = legacy fov derivation
+int g_svp_authored_mags = 1; // pip svp scopes read authored magnifications from the scope section, 0 = legacy scope_zoom_factor derivation
+float g_svp_zero = 100.f; // pip auto-zero cap in meters, shots converge on the aimed surface up to this range, 0 = raw fire axis
+int g_svp_unify_cam_fx = 1; // pip camera-only effector kicks also rotate the weapon while a PiP scope is aimed, 0 = stock split
+int g_svp_world_cam_fx = 1; // pip cam effectors keep driving the main view while a PiP scope is aimed, 0 = frozen surround
+int g_svp_hud_true_fov = 0; // pip the hud weapon renders at the true scene fov while a PiP scope is aimed (0 = stock viewmodel fov)
+int g_svp_zoom_sync = 1; // pip the svp renders the dialed magnification through the raise, 0 = track the live zoom factor
+int g_svp_crescent = 0; // pip swing crescent master switch, 0 hides the parallax crescent under a pip scope
+float g_svp_sens = 1.f; // pip scoped mouse sensitivity multiplier, applies only while the svp renders
+float g_svp_sens_curve = 1.f; // pip zoom sens response exponent, 1 = proportional to magnification, 0 = flat
+
+void svp_gameplay_cvars_init()
+{
+ CMD4(CCC_Float, "g_zoom_smooth", &g_zoom_smooth, 0.f, 60.f);
+ CMD4(CCC_Float, "g_zoom_analog", &g_zoom_analog, 0.f, 200.f);
+ CMD4(CCC_Integer, "g_zoom_clicks", &g_zoom_clicks, 0, 1);
+ CMD4(CCC_Integer, "g_svp_zoom_base", &g_svp_zoom_base, 0, 1);
+ CMD4(CCC_Integer, "g_svp_authored_mags", &g_svp_authored_mags, 0, 1);
+ CMD4(CCC_Float, "g_svp_zero", &g_svp_zero, 0.f, 1000.f);
+ CMD4(CCC_Integer, "g_svp_unify_cam_fx", &g_svp_unify_cam_fx, 0, 1);
+ CMD4(CCC_Integer, "g_svp_world_cam_fx", &g_svp_world_cam_fx, 0, 1);
+ CMD4(CCC_Integer, "g_svp_hud_true_fov", &g_svp_hud_true_fov, 0, 1);
+ CMD4(CCC_Integer, "g_svp_zoom_sync", &g_svp_zoom_sync, 0, 1);
+ CMD4(CCC_Integer, "g_svp_crescent", &g_svp_crescent, 0, 1);
+ CMD4(CCC_Float, "g_svp_sens", &g_svp_sens, 0.1f, 3.f);
+ CMD4(CCC_Float, "g_svp_sens_curve", &g_svp_sens_curve, 0.f, 2.f);
+}
diff --git a/src/xrEngine/svp_gameplay_cvars.h b/src/xrEngine/svp_gameplay_cvars.h
new file mode 100644
index 0000000000..c0940ca059
--- /dev/null
+++ b/src/xrEngine/svp_gameplay_cvars.h
@@ -0,0 +1,27 @@
+#ifndef svp_gameplay_cvarsH
+#define svp_gameplay_cvarsH
+#pragma once
+
+// true PiP gameplay cvars, xrEngine resident so xrGame reads them without linking xrRender
+
+extern float g_zoom_smooth;
+extern float g_zoom_analog;
+extern int g_zoom_clicks;
+extern int g_svp_zoom_base;
+extern int g_svp_authored_mags;
+extern float g_svp_zero;
+extern int g_svp_unify_cam_fx;
+extern int g_svp_world_cam_fx;
+extern int g_svp_hud_true_fov;
+extern int g_svp_zoom_sync;
+extern int g_svp_crescent;
+extern float g_svp_sens;
+extern float g_svp_sens_curve;
+
+// authored zoom factor convention base, a factor f renders mag (SVP_ZOOM_BASE_FOV / 0.75) / f
+constexpr float SVP_ZOOM_BASE_FOV = 75.f;
+
+// registers the svp gameplay console commands, called once from CCC_Register
+extern void svp_gameplay_cvars_init();
+
+#endif
diff --git a/src/xrEngine/svp_state.h b/src/xrEngine/svp_state.h
new file mode 100644
index 0000000000..67c26201df
--- /dev/null
+++ b/src/xrEngine/svp_state.h
@@ -0,0 +1,124 @@
+#ifndef svp_stateH
+#define svp_stateH
+#pragma once
+
+#include
+#include
+
+// the SVP (PiP second viewport) cross-thread data bus, logic publishes and render consumes by
+// field name, device.h includes this just before CRenderDevice and aliases it back inside
+class ENGINE_API CSecondVPParams //--#SM+#-- +SecondVP+
+{
+ bool isActive; // Oeaa aeoeaaoee ?aiaa?a ai aoi?ie au?ii?o
+ u8 frameDelay; // Ia eaeii eaa?a n iiiaioa i?ioeiai ?aiaa?a ai aoi?ie au?ii?o iu ia?i?i iiaue
+ //(ia ii?ao auou iaiuoa 2 - ea?aue aoi?ie eaa?, ?ai aieuoa oai aieaa ieceee FPS ai aoi?ii au?ii?oa)
+
+public:
+ bool isCamReady; // Oeaa aioiaiinoe eaia?u (FOV, iiceoey, e o.i) e ?aiaa?o aoi?iai au?ii?oa
+
+ IC bool IsSVPActive() { return isActive; }
+ void SetSVPActive(bool bState);
+ bool IsSVPFrame();
+
+ IC u8 GetSVPFrameDelay() { return frameDelay; }
+ void SetSVPFrameDelay(u8 iDelay)
+ {
+ frameDelay = iDelay;
+ clamp(frameDelay, 2, u8(-1));
+ }
+
+ // true PiP additions
+ struct Lens { Fmatrix m_W; float radius; };
+ Lens eyepiece;
+ Lens objective;
+ Fvector3 w_ffp;
+ Fvector3 w_sfp;
+ // pip held lens radii for the debug overlays, cleared when the svp deactivates
+ float dbg_eyepiece_r = 0.f;
+ float dbg_objective_r = 0.f;
+
+ // pip DLSS-SR scaffolding, all inert at gate 0. cached SVP scene constants refreshed at the
+ // svpCamera tail (render thread, written then read same frame) for the eval inputs
+ float svp_near = 0.f, svp_far = 0.f, svp_fov = 0.f, svp_aspect = 1.f;
+ Fvector svp_cam_pos = {}, svp_up = {}, svp_right = {}, svp_fwd = {};
+ Fvector2 svp_jitter_px = {}; // raw sub-pixel jitter baked into matrices[1].mProject, {0,0} at gate 0
+ bool m_lens_prev_valid = false; // render-thread edge state for the lens-appears reset trigger
+
+ float svp_disc_px = 0.f; // pip on-screen eyepiece disc diameter (px), learned in the lens composite
+ float svp_disc_applied = 0.f; // pip disc px the SVP target is sized to, locked at ADS-in so it never resizes mid-ADS
+ float svp_panel_aspect = 1.f; // pip flat-panel lens W:H from the lens AABB (1 = round/square scope)
+
+ // pip flat-panel on-screen quad for the binocular target brackets, plane half-extent world vectors
+ // (logic projects them through the hud transform), the shader V-crop, and the active-panel flag
+ Fvector svp_panel_ax_w = {}; // panel center -> width edge (svp ndc +x)
+ Fvector svp_panel_ax_h = {}; // panel center -> height edge (svp ndc -y)
+ float svp_panel_vcrop = 1.f; // svp_glass2.w flat-panel V-crop (1 = svp matches the panel)
+ bool svp_panel_flat = false; // a reticle_type 8 flat window drives the svp this frame
+
+ // pip stable sight line for ballistics, published whole by deriveScopeLens. the eyepiece
+ // fields are render scratch that zero at frame start while logic fires concurrently
+ Fvector svp_sight_pos = {};
+ Fvector svp_sight_dir = {};
+ bool svp_sight_ok = false;
+ u32 svp_sight_frame = 0; // publish frame, staleness gates the svp readiness
+ float svp_lens_r = 0.f; // stable lens radius published with the sight line, logic reads it for activation
+ u32 svp_optic_epoch = 0; // pip optic identity counter, bumps on a lens visual or radius change, subscribers reseed
+ // pip resolved per-optic optics inputs, the bus fills these once at the lens derive so one
+ // precedence and one eps gate govern every consumer instead of each re-reading the raw cvars
+ Fvector4 svp_opt_offset = { 0.f, 0.f, 0.f, 0.f }; // xy lateral zw front/radius (eyepiece radii), authored_optics gated, 0 = none
+ float svp_opt_obj_mm = 0.f; // objective clear aperture mm, the spec cvar then the authored w fallback, 0 = none
+ bool svp_alt_sight = false; // aimed on a non scope sight, the fit latch freezes on the wrong pose
+ float svp_recoil_relax_s = 0.f; // authored one shot recovery, dispersion over relax speed
+ float svp_zoom_pub = 0.f; // raise transient free zoom for the svp camera, 0 = unset falls back to hud_params.y
+ float svp_eyebox_rad = 0.f; // eyebox half angle (rad) from the real exit pupil and eye relief
+ float svp_shadow_gain = 0.f; // swing envelope 0..1, hard weapon motion sweeps the crescent in
+ u32 svp_lever_ms = 0; // pip lever throw stamp, a click flip pulses the transition shadow for the throw
+ float svp_swing_x = 0.f, svp_swing_y = 0.f; // latched swing side unit vector for the crescent
+ float svp_mag = 0.f; // current scope magnification for the zoom scaled trigger, 0 unknown
+ float svp_fov_scale = 1.f; // config zoom factors ride the 75 base, this rescales them to the live fov, 1 for script authored
+ float svp_aim_fov = 0.f; // steady wide main view fov (deg) through a pip scope, the mag reads it so recoil fov punches never wobble it, 0 unset
+ bool svp_authored_mag = false; // pip flat optic carries authored mags, keep the clean optical mag not the panel subtense ratio
+ bool svp_min_75base = false; // pip the min zoom bound was computed in the 75 base so it rescales to the aim fov
+
+ // pip live ballistic ray of the active weapon (logic thread writes, render diag reads)
+ Fvector fire_ray_pos = {};
+ Fvector fire_ray_dir = {};
+ float fire_ray_zero = 0.f; // meters, mirrors g_svp_zero for the render-side overlay
+ u32 fire_ray_frame = 0;
+ Fvector muzzle_pos = {}; // pip fire bone + fire_point of the active weapon, for the [3DB] markers
+ Fvector eye_ray_pos = {}; // pip actor first-eye mirror (logic writes), stays the shooter's
+ Fvector eye_ray_dir = {}; // eye while demo_record flies the device camera
+
+ // pip [3DB] shot tracer ring, one entry per fired bullet (logic writes, overlay reads + fades)
+ struct FireTrace { Fvector pos; Fvector dir; u32 time_ms; };
+ FireTrace fire_traces[16] = {};
+ u32 fire_trace_head = 0;
+
+ // history reset for the eval, set by the triggers (logic + render threads), consumed render-side
+ // at the seam via exchange, atomic because the logic-thread writers race the render-thread read
+ std::atomic dlss_reset_next{ false };
+
+ // set by the double-pass, read by the hybrid IsSVPFrame when true_pip is on
+ bool m_render_pass_is_svp = false;
+
+ // pip set around the disc pass of the nvg tube split, the dev_param_8 binder centers the
+ // offset tube only while it holds (render thread only)
+ bool svp_nvg_disc_pass = false;
+
+ // pip set true only when the collimated reflex proxy drew into rt_secondVP this frame
+ bool svp_reflex_proxy_ok = false;
+
+ // pip set only around the SVP water surface draw, makes ssfx_issvp read 0 so the SSS water shader
+ // uses the SVP reflection instead of its flat-scope fallback (ssfx_water.ps reflection = turbidity)
+ bool force_water_reflect = false;
+
+ // pip set around the SVP sun accum, makes ssfx_issvp read 0 so the sun keeps the SSS contact term
+ bool force_svp_sss = false;
+
+ // pip shared-shadow hook, called as accum(); if (dual_accum) dual_accum(accum), the lambda
+ // re-accumulates the shadow unit into the SVP, null for R2/R3 and when PiP is off
+ std::function&)> dual_accum;
+
+};
+
+#endif
diff --git a/src/xrEngine/x_ray.cpp b/src/xrEngine/x_ray.cpp
index 28b6491b4c..6be24723d1 100644
--- a/src/xrEngine/x_ray.cpp
+++ b/src/xrEngine/x_ray.cpp
@@ -321,6 +321,9 @@ void execUserScript()
{
Console->Execute("default_controls");
Console->ExecuteScript(Console->ConfigFile);
+ // re-apply -gpu_markers / -shader_debug after user.ltx so a saved cvar value can't override them
+ if (strstr(Core.Params, "-gpu_markers")) Console->Execute("r__gpu_markers 1");
+ if (strstr(Core.Params, "-shader_debug")) Console->Execute("r__shader_debug 1");
Console->Execute("dump_cvar");
}
diff --git a/src/xrEngine/xrEngine.vcxproj b/src/xrEngine/xrEngine.vcxproj
index 3b80d77a57..a7d2385603 100644
--- a/src/xrEngine/xrEngine.vcxproj
+++ b/src/xrEngine/xrEngine.vcxproj
@@ -928,6 +928,7 @@
+
@@ -1007,6 +1008,8 @@
+
+
@@ -1129,6 +1132,8 @@
+
+
diff --git a/src/xrEngine/xrEngine.vcxproj.filters b/src/xrEngine/xrEngine.vcxproj.filters
index feeda89df2..4640ab9539 100644
--- a/src/xrEngine/xrEngine.vcxproj.filters
+++ b/src/xrEngine/xrEngine.vcxproj.filters
@@ -378,6 +378,12 @@
Engine\Console\commands
+
+ Engine\Console\commands
+
+
+ Engine\Console\commands
+
Engine\Core
@@ -411,6 +417,9 @@
RenderRef\Execution & 3D\Device
+
+ RenderRef\Execution & 3D\Device
+
RenderRef\Execution & 3D\Device
@@ -684,6 +693,12 @@
Engine\Console\commands
+
+ Engine\Console\commands
+
+
+ Engine\Console\commands
+
Engine\Core
diff --git a/src/xrEngine/xr_ioc_cmd.cpp b/src/xrEngine/xr_ioc_cmd.cpp
index 1f256fb95c..c9c70e4eef 100644
--- a/src/xrEngine/xr_ioc_cmd.cpp
+++ b/src/xrEngine/xr_ioc_cmd.cpp
@@ -5,6 +5,7 @@
#include "x_ray.h"
#include "xr_ioconsole.h"
#include "xr_ioc_cmd.h"
+#include "svp_gameplay_cvars.h"
//#include "fbasicvisual.h"
#include "cameramanager.h"
#include "environment.h"
@@ -1243,6 +1244,7 @@ void CCC_Register()
CMD4(CCC_Float, "g_freelook_z_offset_factor", &g_freelook_z_offset, -3.f, 3.f);
CMD4(CCC_Float, "g_ironsights_zoom_factor", &g_ironsights_factor, 1.f, 2.f);
+ svp_gameplay_cvars_init(); // pip gameplay cvars register in svp_gameplay_cvars.cpp
CMD4(CCC_Vector3, "ssfx_wetness_multiplier", &ssfx_wetness_multiplier, Fvector3().set(0.1f, 0.1f, 0.0f), Fvector3().set(20.0f, 20.0f, 0.0f));
// - CrookR
diff --git a/src/xrGame/Actor.cpp b/src/xrGame/Actor.cpp
index 60cbd98f20..fc53a11a74 100644
--- a/src/xrGame/Actor.cpp
+++ b/src/xrGame/Actor.cpp
@@ -29,6 +29,7 @@
// breakpoints
#include "../xrEngine/xr_input.h"
+#include "../xrEngine/svp_gameplay_cvars.h" // pip the 75 base for the through-scope sens fov
//
#include "Actor.h"
#include "ActorAnimation.h"
@@ -1098,7 +1099,7 @@ void CActor::g_Physics(Fvector& _accel, float jump, float dt)
float g_fov = 55.0f;
extern float g_ironsights_factor;
-float CActor::currentFOV()
+float CActor::currentFOV(bool wantSVPFov)
{
if (!psHUD_Flags.is(HUD_WEAPON | HUD_WEAPON_RT | HUD_WEAPON_RT2))
return g_fov;
@@ -1112,6 +1113,13 @@ float CActor::currentFOV()
{
if (pWeapon->GetZoomFactor() == 0)
return atan(tan(g_fov * (0.5 * PI / 180)) / g_ironsights_factor) / (0.5 * PI / 180);
+ // pip when the SVP renders the zoom, the main view stays wide (the zoom lives in the scope image),
+ // wantSVPFov asks for the real zoomed fov (svpCamera, mouse sensitivity)
+ else if (Device.true_pip_on && Device.m_SecondViewport.IsSVPActive() && !wantSVPFov)
+ return g_fov;
+ else if (Device.true_pip_on && Device.m_SecondViewport.IsSVPActive() && wantSVPFov)
+ // pip the true through-scope fov, config factors ride the 75 base and rescale to the live fov
+ return pWeapon->GetZoomFactor() * 0.75f * (pWeapon->IsScriptedZoom() ? 1.f : g_fov / SVP_ZOOM_BASE_FOV);
else
return pWeapon->GetZoomFactor() * (0.75f);
}
@@ -1126,6 +1134,7 @@ float CActor::currentFOV()
void CActor::UpdateCL()
{
PROF_EVENT("CActor UpdateCL");
+
if (g_Alive() && Level().CurrentViewEntity() == this)
{
if (CurrentGameUI() && (!CurrentGameUI()->TopInputReceiver() || (CurrentGameUI()->TopInputReceiver() && !CurrentGameUI()->TopInputReceiver()->StopAnyMove())) && !m_holder)
@@ -1247,6 +1256,9 @@ void CActor::UpdateCL()
HUD().ShowCrosshair(true);
g_pGamePersistent->m_pGShaderConstants->hud_params.set(0.f, 0.f, 0.f, 0.f);
g_pGamePersistent->m_pGShaderConstants->m_blender_mode.set(0.f, 0.f, 0.f, 0.f);
+ // pip a mount right after ads left the svp rendering a dead second pass all mount long
+ if (Device.true_pip_on)
+ Device.m_SecondViewport.SetSVPActive(false);
}
#endif
else
diff --git a/src/xrGame/Actor.h b/src/xrGame/Actor.h
index 3b6071a446..f892de4f47 100644
--- a/src/xrGame/Actor.h
+++ b/src/xrGame/Actor.h
@@ -377,7 +377,7 @@ class CActor :
void cam_SetFreelook();
void cam_UnsetFreelook();
bool CanUseFreelook();
- float currentFOV();
+ float currentFOV(bool wantSVPFov = false); // pip wantSVPFov true returns the zoomed fov, false keeps the main view wide while the SVP zooms
// Cameras
CCameraBase* cameras[eacMaxCam];
diff --git a/src/xrGame/ActorCameras.cpp b/src/xrGame/ActorCameras.cpp
index e538c43e20..a55cead389 100644
--- a/src/xrGame/ActorCameras.cpp
+++ b/src/xrGame/ActorCameras.cpp
@@ -703,6 +703,15 @@ void CActor::cam_Update(float dt, float fFOV)
{
Cameras().ApplyDevice(_viewport_near);
}
+ // pip the effector driven actor camera keeps the main view while scoped, 0 = frozen surround, ppe only
+ else if (Device.true_pip_on && eacFirstEye == cam_active && !Level().Cameras().GetCamEffector(cefDemo))
+ {
+ extern int g_svp_world_cam_fx;
+ if (g_svp_world_cam_fx)
+ Cameras().ApplyDevice(_viewport_near);
+ else
+ Cameras().ApplyPP();
+ }
}
}
diff --git a/src/xrGame/ActorEffector.cpp b/src/xrGame/ActorEffector.cpp
index 8940ad457d..f85bfcc3ac 100644
--- a/src/xrGame/ActorEffector.cpp
+++ b/src/xrGame/ActorEffector.cpp
@@ -2,6 +2,8 @@
#include "ActorEffector.h"
#include "PostprocessAnimator.h"
+#include "../xrEngine/svp_gameplay_cvars.h"
+#include "../Layers/xrRender/xrRender_console.h"
#include "../xrEngine/effectorPP.h"
#include "../xrEngine/ObjectAnimator.h"
#include "object_broker.h"
@@ -495,7 +497,12 @@ bool CActorCameraManager::ProcessCameraEffector(CEffectorCam* eff)
bool res = inherited::ProcessCameraEffector(eff);
if (res)
{
- if (eff->GetHudAffect())
+ // pip while a true PiP scope is aimed the view and the weapon must rotate together, a
+ // camera-only kick points the scope away from the view and the split magnifies in glass,
+ // hud_affect false was authored for 2d scopes that always drew at screen center
+ const bool unify = g_svp_unify_cam_fx && scope_svp_enabled
+ && Device.m_SecondViewport.IsSVPActive();
+ if (eff->GetHudAffect() || unify)
{
SCamEffectorInfo affected = m_cam_info;
SCamEffectorInfo diff;
diff --git a/src/xrGame/ActorInput.cpp b/src/xrGame/ActorInput.cpp
index 7755cd7a8c..11c8cbd649 100644
--- a/src/xrGame/ActorInput.cpp
+++ b/src/xrGame/ActorInput.cpp
@@ -467,7 +467,17 @@ void CActor::IR_OnMouseMove(int dx, int dy)
const float LookFactor = GetLookFactor();
CCameraBase* C = cameras[cam_active];
- float scale = (C->f_fov / g_fov) * (psMouseSens * sens_multiple) * psMouseSensScale / 50.f / LookFactor;
+ // pip when the SVP is active the main view stays wide, so first-eye sensitivity must track the SVP
+ // zoom fov instead of C->f_fov, off (no SVP) keeps the stock C->f_fov so mouse feel is unchanged
+ float ratio = C->f_fov / g_fov;
+ if (cam_active == eacFirstEye && Device.true_pip_on && Device.m_SecondViewport.IsSVPActive())
+ {
+ // the curve exponent shapes the zoom response, 1 rides the svp fov, 0 goes flat,
+ // the multiplier trims the whole scoped feel
+ extern float g_svp_sens, g_svp_sens_curve;
+ ratio = powf(currentFOV(true) / g_fov, g_svp_sens_curve) * g_svp_sens;
+ }
+ float scale = ratio * (psMouseSens * sens_multiple) * psMouseSensScale / 50.f / LookFactor;
if (dx)
{
float d = float(dx) * scale;
diff --git a/src/xrGame/Actor_Weapon.cpp b/src/xrGame/Actor_Weapon.cpp
index 1f73c6c458..1e21a08750 100644
--- a/src/xrGame/Actor_Weapon.cpp
+++ b/src/xrGame/Actor_Weapon.cpp
@@ -125,6 +125,10 @@ void CActor::g_fireParams(const CHudItem* pHudItem, Fvector& fire_pos, Fvector&
fire_pos = pp.defs.start;
fire_dir = pp.defs.dir;
+
+ // pip zeroing + the [3DB] tracer ring, in svp_weapon.cpp
+ extern void svp_apply_zero_and_trace(const SPickParam& pp, Fvector& fire_pos, Fvector& fire_dir);
+ svp_apply_zero_and_trace(pp, fire_pos, fire_dir);
}
void CActor::g_WeaponBones(int& L, int& R1, int& R2)
diff --git a/src/xrGame/HudItem.cpp b/src/xrGame/HudItem.cpp
index a1d8b4f644..52148701ec 100644
--- a/src/xrGame/HudItem.cpp
+++ b/src/xrGame/HudItem.cpp
@@ -1092,7 +1092,26 @@ void CHudItem::Ray(SPickParam& pp)
);
// Transform from non-offset HUD space to world space
- Device.hud_to_world(matrix, proj);
+ // the pick remaps through the actor's real eye under demo_record
+ if (Level().Cameras().GetCamEffector(cefDemo))
+ {
+ CCameraBase* c = const_cast(pActor)->cam_FirstEye();
+ Fmatrix view;
+ view.build_camera_dir(c->vPosition, c->vDirection, c->vNormal);
+ Fmatrix inv_view;
+ inv_view.invert(view);
+ Fmatrix mproj;
+ mproj.build_projection(
+ deg2rad(c->f_fov),
+ Device.fASPECT,
+ R_VIEWPORT_NEAR,
+ g_pGamePersistent->Environment().CurrentEnv->far_plane);
+ Fmatrix inv_proj;
+ inv_proj.invert(mproj);
+ Device.hud_to_world(matrix, proj, view, inv_view, inv_proj);
+ }
+ else
+ Device.hud_to_world(matrix, proj);
}
// Detect wall penetration
diff --git a/src/xrGame/Level.cpp b/src/xrGame/Level.cpp
index 44450ed445..ec21bf8730 100644
--- a/src/xrGame/Level.cpp
+++ b/src/xrGame/Level.cpp
@@ -1245,6 +1245,7 @@ extern int ps_r4_hdr10_pda; // NOTE: this is a hack to avoid double HDR tonemapp
void CLevel::OnRender()
{
+ Device.dwViewport++; // pip advance the per-viewport cache key once per frame (keeps non-R4 invalidating)
// PDA
if (game && CurrentGameUI() && &CurrentGameUI()->GetPdaMenu() != nullptr)
{
@@ -1309,7 +1310,7 @@ void CLevel::OnRender()
Game().OnRender();
BulletManager().Render();
- if (Device.m_SecondViewport.IsSVPFrame())
+ if (!Device.true_pip_on && Device.m_SecondViewport.IsSVPFrame())
Render->RenderToTarget(Render->rtSVP);
if (use_reshade)
diff --git a/src/xrGame/ParticlesObject.cpp b/src/xrGame/ParticlesObject.cpp
index 0e76fdd421..4ab57d1ac4 100644
--- a/src/xrGame/ParticlesObject.cpp
+++ b/src/xrGame/ParticlesObject.cpp
@@ -269,6 +269,16 @@ void CParticlesObject::SetHudMode(bool bHudMode)
V->SetHudMode(bHudMode);
}
+// pip tags the held weapon's own muzzle fx so the scope pass can drop only these
+void CParticlesObject::MarkWeaponFX()
+{
+ if (g_dedicated_server) return;
+
+ IParticleCustom* V = renderable.visual ? renderable.visual->dcast_ParticleCustom() : NULL;
+ if (V)
+ V->SetWeaponFX(TRUE);
+}
+
void CParticlesObject::SetLiveUpdate(BOOL b)
{
if(g_dedicated_server) return;
diff --git a/src/xrGame/ParticlesObject.h b/src/xrGame/ParticlesObject.h
index acbb848ba1..c789f0e4a8 100644
--- a/src/xrGame/ParticlesObject.h
+++ b/src/xrGame/ParticlesObject.h
@@ -44,6 +44,7 @@ class CParticlesObject : public CPS_Instance
bool IsPlaying();
void SetAutoRemove(bool auto_remove);
void SetHudMode(bool bHudMode);
+ void MarkWeaponFX();
const shared_str Name();
void SetLiveUpdate(BOOL b);
diff --git a/src/xrGame/ShootingObject.cpp b/src/xrGame/ShootingObject.cpp
index 38b8e50c03..abf097546c 100644
--- a/src/xrGame/ShootingObject.cpp
+++ b/src/xrGame/ShootingObject.cpp
@@ -250,6 +250,7 @@ void CShootingObject::StartParticles(CParticlesObject*& pParticles, LPCSTR parti
{
in_hud_mode = false;
}
+ pParticles->MarkWeaponFX();
pParticles->Play(in_hud_mode);
}
@@ -339,6 +340,7 @@ void CShootingObject::OnShellDrop(const Fvector& play_pos,
{
in_hud_mode = false;
}
+ pShellParticles->MarkWeaponFX();
pShellParticles->Play(in_hud_mode);
}
@@ -378,7 +380,10 @@ void CShootingObject::StartFlameParticles()
in_hud_mode = false;
}
if(m_pFlameParticles)
+ {
+ m_pFlameParticles->MarkWeaponFX();
m_pFlameParticles->Play(in_hud_mode);
+ }
}
void CShootingObject::StopFlameParticles()
diff --git a/src/xrGame/UIGameCustom.cpp b/src/xrGame/UIGameCustom.cpp
index 7f383f9501..bcd8843bbe 100644
--- a/src/xrGame/UIGameCustom.cpp
+++ b/src/xrGame/UIGameCustom.cpp
@@ -59,22 +59,25 @@ void CUIGameCustom::OnFrame()
{
PROF_EVENT("CUIGameCustom::OnFrame");
CDialogHolder::OnFrame();
- for (auto item : CustomStatics)
- item->Update();
- auto comparer = [](const StaticDrawableWrapper* s1, const StaticDrawableWrapper* s2)
{
- return s1->IsActual() > s2->IsActual();
- };
- std::sort(CustomStatics.begin(), CustomStatics.end(), comparer);
- while (!CustomStatics.empty() && !CustomStatics.back()->IsActual())
- {
- delete_data(CustomStatics.back());
- CustomStatics.pop_back();
- }
- if (g_b_ClearGameCaptions)
- {
- delete_data(CustomStatics);
- g_b_ClearGameCaptions = false;
+ std::scoped_lock lock(m_customs_lock);
+ for (auto item : CustomStatics)
+ item->Update();
+ auto comparer = [](const StaticDrawableWrapper* s1, const StaticDrawableWrapper* s2)
+ {
+ return s1->IsActual() > s2->IsActual();
+ };
+ std::sort(CustomStatics.begin(), CustomStatics.end(), comparer);
+ while (!CustomStatics.empty() && !CustomStatics.back()->IsActual())
+ {
+ delete_data(CustomStatics.back());
+ CustomStatics.pop_back();
+ }
+ if (g_b_ClearGameCaptions)
+ {
+ delete_data(CustomStatics);
+ g_b_ClearGameCaptions = false;
+ }
}
Window->Update();
//update windows
@@ -86,8 +89,11 @@ void CUIGameCustom::OnFrame()
void CUIGameCustom::Render()
{
PROF_EVENT("CUIGameCustom::Render");
- for (StaticDrawableWrapper* item : CustomStatics)
- item->Draw();
+ {
+ std::scoped_lock lock(m_customs_lock);
+ for (StaticDrawableWrapper* item : CustomStatics)
+ item->Draw();
+ }
Window->Draw();
CEntity* pEntity = smart_cast(Level().CurrentEntity());
if (pEntity)
@@ -114,6 +120,7 @@ void CUIGameCustom::Render()
StaticDrawableWrapper* CUIGameCustom::AddCustomStatic(const char* id, bool singleInstance)
{
+ std::scoped_lock lock(m_customs_lock);
if (singleInstance)
{
auto it = std::find_if(CustomStatics.begin(), CustomStatics.end(), predicate_find_stat(id));
@@ -134,6 +141,7 @@ StaticDrawableWrapper* CUIGameCustom::AddCustomStatic(const char* id, bool singl
StaticDrawableWrapper* CUIGameCustom::GetCustomStatic(const char* id)
{
+ std::scoped_lock lock(m_customs_lock);
auto it = std::find_if(CustomStatics.begin(), CustomStatics.end(), predicate_find_stat(id));
if (it != CustomStatics.end())
return *it;
@@ -142,6 +150,7 @@ StaticDrawableWrapper* CUIGameCustom::GetCustomStatic(const char* id)
void CUIGameCustom::RemoveCustomStatic(const char* id)
{
+ std::scoped_lock lock(m_customs_lock);
auto it = std::find_if(CustomStatics.begin(), CustomStatics.end(), predicate_find_stat(id));
if (it != CustomStatics.end())
{
diff --git a/src/xrGame/UIGameCustom.h b/src/xrGame/UIGameCustom.h
index fd95fd4451..d3a0fdc94f 100644
--- a/src/xrGame/UIGameCustom.h
+++ b/src/xrGame/UIGameCustom.h
@@ -1,5 +1,6 @@
#pragma once
+#include
#include "script_export_space.h"
#include "object_interfaces.h"
#include "inventory_space.h"
@@ -85,6 +86,8 @@ class CUIGameCustom :
CUIWindow* Window;
CUIXml* MsgConfig;
xr_vector CustomStatics;
+ // captions are mutated by game logic and drawn by the render thread, guard every access
+ std::mutex m_customs_lock;
CUIActorMenu* ActorMenu;
CUIPdaWnd* PdaMenu;
bool showGameIndicators;
diff --git a/src/xrGame/Weapon.cpp b/src/xrGame/Weapon.cpp
index 25697dc864..905a622aff 100644
--- a/src/xrGame/Weapon.cpp
+++ b/src/xrGame/Weapon.cpp
@@ -11,6 +11,7 @@
#include "xrserver_objects_alife_items.h"
#include "actor.h"
#include "actoreffector.h"
+#include "../xrEngine/CameraBase.h"
#include "level.h"
#include "xr_level_controller.h"
#include "game_cl_base.h"
@@ -34,6 +35,8 @@
#include "WeaponMagazinedWGrenade.h"
#include "../xrEngine/GameMtlLib.h"
#include "../Layers/xrRender/xrRender_console.h"
+#include "../xrEngine/svp_gameplay_cvars.h"
+#include "svp_mags.h"
#include "pch_script.h"
#include "script_game_object.h"
@@ -120,6 +123,7 @@ CWeapon::CWeapon()
m_APk = 1.0f;
m_zoom_params.m_fCurrentZoomFactor = g_fov;
+ m_zoom_params.m_fZoomTargetFactor = g_fov;
m_zoom_params.m_fZoomRotationFactor = 0.f;
m_zoom_params.m_pVision = NULL;
m_zoom_params.m_pNight_vision = NULL;
@@ -339,6 +343,12 @@ void CWeapon::UpdateZoomParams() {
m_zoom_params.m_bUseDynamicZoom = READ_IF_EXISTS(pSettings, r_bool, GetScopeName(), "scope_dynamic_zoom", false);
m_zoom_params.m_fMinBaseZoomFactor = READ_IF_EXISTS(pSettings, r_float, GetScopeName(), "min_scope_zoom_factor", 200.0f);
stepCount = READ_IF_EXISTS(pSettings, r_float, GetScopeName(), "zoom_step_count", 0);
+ } else {
+ // pip honor the scope's own zoom config on non-modular hosts too, OR in scope_dynamic_zoom
+ // (never disable what the weapon/upgrade enabled) and adopt the scope's min/step when present
+ m_zoom_params.m_bUseDynamicZoom = m_zoom_params.m_bUseDynamicZoom || READ_IF_EXISTS(pSettings, r_bool, GetScopeName(), "scope_dynamic_zoom", false);
+ m_zoom_params.m_fMinBaseZoomFactor = READ_IF_EXISTS(pSettings, r_float, GetScopeName(), "min_scope_zoom_factor", m_zoom_params.m_fMinBaseZoomFactor);
+ stepCount = READ_IF_EXISTS(pSettings, r_float, GetScopeName(), "zoom_step_count", stepCount);
}
} else
{
@@ -349,6 +359,46 @@ void CWeapon::UpdateZoomParams() {
m_zoom_params.m_fZoomStepCount = stepCount;
}
+ // pip recover a section-variant scope's max zoom, the parent merge can leave a base
+ // weapon's 0 in place of the scope's real value
+ if (m_zoom_params.m_bUseDynamicZoom && m_zoom_params.m_fScopeZoomFactor < 1.0f)
+ {
+ float recovered = 0.0f;
+ if (const RStringVec* parents = pSettings->get_section_parents(cNameSect()))
+ for (const shared_str& p : *parents)
+ {
+ float v = READ_IF_EXISTS(pSettings, r_float, p.c_str(), "scope_zoom_factor", 0.0f);
+ if (v >= 1.0f)
+ recovered = v; // last valid parent wins; the scope _s settings is the inheriting one
+ }
+ // 20.0 last-resort cap if no parent carries a usable value, so the lens still cannot break
+ m_zoom_params.m_fScopeZoomFactor = (recovered >= 1.0f ? recovered : 20.0f) / zoom_multiple;
+ }
+
+ // pip svp scopes may author true magnifications directly, engine derives the 75 base factors
+ m_zoom_params.m_bSvpAuthoredMin = false;
+ if (m_zoomtype == 0 && scope_svp_enabled >= 2 && g_svp_authored_mags && SvpMagsEligible())
+ {
+ const bool scope_attached = (ALife::eAddonPermanent != m_eScopeStatus
+ && 0 != (m_flagsAddOnState & CSE_ALifeItemWeapon::eWeaponAddonScope) && m_scopes.size());
+ const shared_str sect = scope_attached ? GetScopeName() : cNameSect();
+ const svp_mags_data mags = svp_mags_resolve(sect.c_str(), zoom_multiple);
+ if (mags.mode != svp_mag_none)
+ {
+ m_zoom_params.m_fScopeZoomFactor = mags.f_top;
+ if (mags.mode != svp_mag_fixed)
+ {
+ m_zoom_params.m_fMinBaseZoomFactor = mags.f_floor;
+ m_zoom_params.m_bUseDynamicZoom = TRUE;
+ m_zoom_params.m_bSvpAuthoredMin = true;
+ if (mags.mode == svp_mag_stepped)
+ m_zoom_params.m_fZoomStepCount = 1;
+ }
+ else
+ m_zoom_params.m_bUseDynamicZoom = FALSE;
+ }
+ }
+
if (IsZoomed()) {
scope_radius = SDS_Radius(m_zoomtype == 1);
if (m_zoomtype == 0 && zoomFlags.test(SDS_SPEED) && (scope_radius > 0.0)) {
@@ -502,6 +552,9 @@ void CWeapon::SetZoomType(u8 new_zoom_type)
{
funct(this->lua_game_object(), previous_zoom_type, m_zoomtype);
}
+
+ Device.m_SecondViewport.dlss_reset_next = true; // pip DLSS history reset on magnification change (logic thread)
+ UpdateSecondVP(); // pip re-evaluate SVP activation when the zoom type changes
}
extern float g_ironsights_factor;
@@ -955,14 +1008,16 @@ void GetZoomData(const float scope_factor, const float zoom_step_count, const fl
void newGetZoomDelta(const float scope_factor, float& delta, const float min_zoom_factor, float steps);
extern BOOL useNewZoomDeltaAlgorithm;
-void NewGetZoomData(const float scope_factor, const float zoom_step_count, float& delta, float& min_zoom_factor, float zoom, float min_zoom)
+void NewGetZoomData(const float scope_factor, const float zoom_step_count, float& delta, float& min_zoom_factor, float zoom, float min_zoom, bool svp_base, bool authored_min = false)
{
- float def_fov = float(g_fov);
+ // pip a true svp scope derives its floor in the authored 75 base, factor 100 renders 1x at any fov
+ float def_fov = svp_base ? SVP_ZOOM_BASE_FOV : float(g_fov);
float delta_factor_total = def_fov - scope_factor;
VERIFY(delta_factor_total > 0);
float loc_min_zoom_factor = ((atan(tan(def_fov * (0.5 * PI / 180)) / g_ironsights_factor) / (0.5 * PI / 180)) / 0.75f) * (scope_radius > 0.0 ? scope_scrollpower : 1);
- if (min_zoom < loc_min_zoom_factor) {
+ // pip authored magnifications carry their own floor, bypass the optical-model cap
+ if (authored_min || min_zoom < loc_min_zoom_factor) {
min_zoom_factor = min_zoom;
} else {
min_zoom_factor = loc_min_zoom_factor;
@@ -984,7 +1039,7 @@ BOOL CWeapon::net_Spawn(CSE_Abstract* DC)
float delta, min_zoom_factor;
float power = scope_radius > 0.0 ? scope_scrollpower : 1;
if (zoomFlags.test(NEW_ZOOM)) {
- NewGetZoomData(m_zoom_params.m_fScopeZoomFactor * power, m_zoom_params.m_fZoomStepCount, delta, min_zoom_factor, GetZoomFactor() * power, m_zoom_params.m_fMinBaseZoomFactor);
+ NewGetZoomData(m_zoom_params.m_fScopeZoomFactor * power, m_zoom_params.m_fZoomStepCount, delta, min_zoom_factor, GetZoomFactor() * power, m_zoom_params.m_fMinBaseZoomFactor, SvpDetentBase(), m_zoom_params.m_bSvpAuthoredMin);
} else {
GetZoomData(m_zoom_params.m_fScopeZoomFactor * power, m_zoom_params.m_fZoomStepCount, m_zoom_params.m_fMinBaseZoomFactor, delta, min_zoom_factor);
}
@@ -1333,6 +1388,22 @@ bool CWeapon::AllowBore()
void CWeapon::UpdateCL()
{
inherited::UpdateCL();
+
+ // pip ease the dynamic zoom toward its scroll target, g_zoom_smooth 0 = instant
+ if (g_zoom_smooth > 0.f && m_zoom_params.m_bUseDynamicZoom && IsZoomed())
+ {
+ float& cur = m_zoom_params.m_fCurrentZoomFactor;
+ const float tgt = m_zoom_params.m_fZoomTargetFactor;
+ if (fsimilar(cur, tgt, 0.001f))
+ cur = tgt;
+ else
+ {
+ float a = g_zoom_smooth * Device.fTimeDelta;
+ if (a > 1.f) a = 1.f;
+ cur += (tgt - cur) * a;
+ }
+ }
+
UpdateHUDAddonsVisibility();
//ïîäñâåòêà îò âûñòðåëà
UpdateLight();
@@ -1403,7 +1474,10 @@ void CWeapon::EnableActorNVisnAfterZoom()
bool CWeapon::need_renderable()
{
- return !Device.m_SecondViewport.IsSVPFrame() && !(IsZoomed() && ZoomTexture() && !IsRotatingToZoom());
+ // pip with an objective lens the weapon must render so the SVP sees it through the scope tube
+ bool svp_has_objective_lens = (scope_svp_enabled >= 2 && Device.m_SecondViewport.objective.radius > EPS);
+ bool not_in_scope = !Device.m_SecondViewport.IsSVPFrame() && !(IsZoomed() && ZoomTexture() && !IsRotatingToZoom());
+ return svp_has_objective_lens || not_in_scope;
}
void CWeapon::renderable_Render(IDSGraphManager* DM)
@@ -2070,7 +2144,7 @@ void CWeapon::OnZoomIn()
float power = scope_radius > 0.0 ? scope_scrollpower : 1;
if (zoomFlags.test(NEW_ZOOM)) {
- NewGetZoomData(m_zoom_params.m_fScopeZoomFactor * power, m_zoom_params.m_fZoomStepCount, delta, min_zoom_factor, GetZoomFactor() * power, m_zoom_params.m_fMinBaseZoomFactor);
+ NewGetZoomData(m_zoom_params.m_fScopeZoomFactor * power, m_zoom_params.m_fZoomStepCount, delta, min_zoom_factor, GetZoomFactor() * power, m_zoom_params.m_fMinBaseZoomFactor, SvpDetentBase(), m_zoom_params.m_bSvpAuthoredMin);
} else {
GetZoomData(m_zoom_params.m_fScopeZoomFactor * power, m_zoom_params.m_fZoomStepCount, m_zoom_params.m_fMinBaseZoomFactor, delta, min_zoom_factor);
}
@@ -2081,6 +2155,27 @@ void CWeapon::OnZoomIn()
//Msg("m_fRTZoomFactor %f, scope_scrollpower %f", m_fRTZoomFactor, scope_scrollpower);
+ // pip re-derive the detent range at the live fov and pull a stale dialed factor into it,
+ // click optics land on the nearer detent, scripted factors stay untouched
+ if (m_zoom_params.m_bUseDynamicZoom && !m_zoom_params.m_bScriptedZoom && SvpDetentBase())
+ {
+ float delta, min_zoom_factor;
+ float power = scope_radius > 0.0 ? scope_scrollpower : 1;
+ if (zoomFlags.test(NEW_ZOOM)) {
+ NewGetZoomData(m_zoom_params.m_fScopeZoomFactor * power, m_zoom_params.m_fZoomStepCount, delta, min_zoom_factor, GetZoomFactor() * power, m_zoom_params.m_fMinBaseZoomFactor, SvpDetentBase(), m_zoom_params.m_bSvpAuthoredMin);
+ } else {
+ GetZoomData(m_zoom_params.m_fScopeZoomFactor * power, m_zoom_params.m_fZoomStepCount, m_zoom_params.m_fMinBaseZoomFactor, delta, min_zoom_factor);
+ }
+ const float full = m_zoom_params.m_fScopeZoomFactor * power;
+ const float before = m_fRTZoomFactor;
+ if (g_zoom_clicks && m_zoom_params.m_fZoomStepCount == 1.f)
+ m_fRTZoomFactor = (m_fRTZoomFactor - full < min_zoom_factor - m_fRTZoomFactor) ? full : min_zoom_factor;
+ else
+ clamp(m_fRTZoomFactor, full, min_zoom_factor);
+ if (!fsimilar(before, m_fRTZoomFactor))
+ PipMsg("[SVP-SEED] %s f=%.1f->%.1f range=[%.1f..%.1f]", cNameSect().c_str(), before, m_fRTZoomFactor, full, min_zoom_factor);
+ }
+
if (m_zoom_params.m_bUseDynamicZoom)
SetZoomFactor(scope_radius > 0.0 ? m_fRTZoomFactor / scope_scrollpower : m_fRTZoomFactor);
else
@@ -2109,6 +2204,8 @@ void CWeapon::OnZoomIn()
}
g_player_hud->updateMovementLayerState();
+
+ UpdateSecondVP(); // pip re-evaluate SVP activation on ADS-in
}
void CWeapon::OnZoomOut()
@@ -2116,10 +2213,13 @@ void CWeapon::OnZoomOut()
m_zoom_params.m_bIsZoomModeNow = false;
if (m_zoom_params.m_bUseDynamicZoom)
{
- m_fRTZoomFactor = scope_radius > 0.0 ? GetZoomFactor() * scope_scrollpower : GetZoomFactor(); //store current
+ // store the dialed zoom, under smoothing the target (the current factor can be mid-glide)
+ const float dialed = (g_zoom_smooth > 0.f) ? m_zoom_params.m_fZoomTargetFactor : GetZoomFactor();
+ m_fRTZoomFactor = scope_radius > 0.0 ? dialed * scope_scrollpower : dialed;
}
m_zoom_params.m_fCurrentZoomFactor = g_fov;
+ m_zoom_params.m_fZoomTargetFactor = g_fov; // pip snap the smooth-zoom target on ADS-out
GamePersistent().RestoreEffectorDOF();
@@ -2930,6 +3030,9 @@ void CWeapon::UpdateHudAdditional(Fmatrix& trans)
hud_rotation.identity();
hud_rotation.translate_over(curr_offs);
trans.mulB_43(hud_rotation);
+
+ // pip swing envelope for the scope shadow crescent
+ ApplySvpSightAnchor(pActor, trans);
}
// Добавить эффект сдвига оружия от выстрела
@@ -3221,7 +3324,7 @@ float CWeapon::GetMinScopeZoomFactor() const
float delta, min_zoom_factor;
float power = scope_radius > 0.0 ? scope_scrollpower : 1;
if (zoomFlags.test(NEW_ZOOM)) {
- NewGetZoomData(m_zoom_params.m_fScopeZoomFactor * power, m_zoom_params.m_fZoomStepCount, delta, min_zoom_factor, GetZoomFactor() * power, m_zoom_params.m_fMinBaseZoomFactor);
+ NewGetZoomData(m_zoom_params.m_fScopeZoomFactor * power, m_zoom_params.m_fZoomStepCount, delta, min_zoom_factor, GetZoomFactor() * power, m_zoom_params.m_fMinBaseZoomFactor, SvpDetentBase(), m_zoom_params.m_bSvpAuthoredMin);
}
else {
GetZoomData(m_zoom_params.m_fScopeZoomFactor * power, m_zoom_params.m_fZoomStepCount, m_zoom_params.m_fMinBaseZoomFactor, delta, min_zoom_factor);
@@ -3229,50 +3332,140 @@ float CWeapon::GetMinScopeZoomFactor() const
return min_zoom_factor;
}
+float CWeapon::GetZoomFactorScript() const
+{
+ // under smoothing lua sees the commanded detent, steps land instantly and the clamp goes quiet
+ extern float g_zoom_smooth;
+ return (g_zoom_smooth > 0.f) ? m_zoom_params.m_fZoomTargetFactor : m_zoom_params.m_fCurrentZoomFactor;
+}
+
+void CWeapon::SetZoomFactorScript(float f)
+{
+ // script authored factors already carry the user fov, the svp scale passes them through
+ SetZoomFactor(f);
+ m_zoom_params.m_bScriptedZoom = true;
+}
+
void CWeapon::ZoomInc()
{
- if (!IsScopeAttached()) return;
+ // pip no IsScopeAttached gate, integrated scopes report none, dynamic zoom is the real gate
if (!m_zoom_params.m_bUseDynamicZoom) return;
+ // pip an authored single-throw scope clicks between its two detents, no analog, no smoothing
+ const bool click = g_zoom_clicks && m_zoom_params.m_fZoomStepCount == 1.f;
+ const bool smooth = g_zoom_smooth > 0.f && !click;
float delta, min_zoom_factor;
float power = scope_radius > 0.0 ? scope_scrollpower : 1;
+ // pip when smoothing, advance from the TARGET (the current factor is mid-glide) so rapid scrolls accumulate
+ float base = smooth ? m_zoom_params.m_fZoomTargetFactor : GetZoomFactor();
if (zoomFlags.test(NEW_ZOOM)) {
- NewGetZoomData(m_zoom_params.m_fScopeZoomFactor * power, m_zoom_params.m_fZoomStepCount, delta, min_zoom_factor, GetZoomFactor() * power, m_zoom_params.m_fMinBaseZoomFactor);
+ NewGetZoomData(m_zoom_params.m_fScopeZoomFactor * power, m_zoom_params.m_fZoomStepCount, delta, min_zoom_factor, base * power, m_zoom_params.m_fMinBaseZoomFactor, SvpDetentBase(), m_zoom_params.m_bSvpAuthoredMin);
} else {
GetZoomData(m_zoom_params.m_fScopeZoomFactor * power, m_zoom_params.m_fZoomStepCount, m_zoom_params.m_fMinBaseZoomFactor, delta, min_zoom_factor);
}
- float f = GetZoomFactor() * power - delta;
- if (useNewZoomDeltaAlgorithm)
- f = GetZoomFactor() * power * delta;
+ float f;
+ if (click)
+ // pip a lever optic wheel event lands the top detent absolutely, no relative step
+ f = m_zoom_params.m_fScopeZoomFactor * power;
+ else if (g_zoom_analog > 0.f)
+ {
+ // pip continuous/analog, step by a fine fraction of the FOV range (ignoring the config step
+ // count + delta algorithm) so any magnification in the scope's range is reachable
+ float fine = (min_zoom_factor - m_zoom_params.m_fScopeZoomFactor * power) / g_zoom_analog;
+ f = base * power - fine;
+ }
+ else
+ {
+ f = base * power - delta;
+ if (useNewZoomDeltaAlgorithm)
+ f = base * power * delta;
+ }
clamp(f, m_zoom_params.m_fScopeZoomFactor * power, min_zoom_factor);
- SetZoomFactor(f / power);
+ if (smooth)
+ m_zoom_params.m_fZoomTargetFactor = f / power; // pip target, UpdateCL eases the current toward it, the step inherits the base factor's author
+ else
+ SetZoomFactor(f / power);
- m_fRTZoomFactor = GetZoomFactor() * power;
+ // pip capture the commanded zoom (the smooth target, not the mid-glide current) so a later
+ // alt-aim / UpdateZoomParams restore lands on the dialed magnification, not a stale snapshot
+ m_fRTZoomFactor = (smooth ? m_zoom_params.m_fZoomTargetFactor : GetZoomFactor()) * power;
+
+ // pip the lever throw sweeps the transition shadow while the prism seats
+ if (click && scope_svp_enabled >= 2 && Device.m_SecondViewport.IsSVPActive() && !fsimilar(base, f / power))
+ {
+ auto& vp = Device.m_SecondViewport;
+ vp.svp_lever_ms = Device.dwTimeGlobal;
+ vp.svp_swing_x = 1.f; // the lever throws across the tube, the side flips with the throw
+ vp.svp_swing_y = 0.f;
+ }
+
+ // pip click flip proof, one line per wheel event on a lever optic
+ if (click && !fsimilar(base, f / power))
+ PipMsg("[SVP-CLICK] %s steps=1 path=click f=%.1f->%.1f (mag %.1f)",
+ cNameSect().c_str(), base, f / power, min_zoom_factor / f);
}
void CWeapon::ZoomDec()
{
- if (!IsScopeAttached()) return;
+ // pip no IsScopeAttached gate, integrated scopes report none, dynamic zoom is the real gate
if (!m_zoom_params.m_bUseDynamicZoom) return;
+ // pip an authored single-throw scope clicks between its two detents, no analog, no smoothing
+ const bool click = g_zoom_clicks && m_zoom_params.m_fZoomStepCount == 1.f;
+ const bool smooth = g_zoom_smooth > 0.f && !click;
float delta, min_zoom_factor;
float power = scope_radius > 0.0 ? scope_scrollpower : 1;
+ // pip when smoothing, advance from the TARGET (the current factor is mid-glide) so rapid scrolls accumulate
+ float base = smooth ? m_zoom_params.m_fZoomTargetFactor : GetZoomFactor();
if (zoomFlags.test(NEW_ZOOM)) {
- NewGetZoomData(m_zoom_params.m_fScopeZoomFactor * power, m_zoom_params.m_fZoomStepCount, delta, min_zoom_factor, GetZoomFactor() * power, m_zoom_params.m_fMinBaseZoomFactor);
+ NewGetZoomData(m_zoom_params.m_fScopeZoomFactor * power, m_zoom_params.m_fZoomStepCount, delta, min_zoom_factor, base * power, m_zoom_params.m_fMinBaseZoomFactor, SvpDetentBase(), m_zoom_params.m_bSvpAuthoredMin);
} else {
GetZoomData(m_zoom_params.m_fScopeZoomFactor * power, m_zoom_params.m_fZoomStepCount, m_zoom_params.m_fMinBaseZoomFactor, delta, min_zoom_factor);
}
- float f = GetZoomFactor() * power + delta;
- if (useNewZoomDeltaAlgorithm)
- f = GetZoomFactor() * power / std::max(delta, 0.001f);
+ float f;
+ if (click)
+ // pip a lever optic wheel event lands the bottom detent absolutely, no relative step
+ f = min_zoom_factor;
+ else if (g_zoom_analog > 0.f)
+ {
+ // pip continuous/analog, step by a fine fraction of the FOV range (ignoring the config step
+ // count + delta algorithm) so any magnification in the scope's range is reachable
+ float fine = (min_zoom_factor - m_zoom_params.m_fScopeZoomFactor * power) / g_zoom_analog;
+ f = base * power + fine;
+ }
+ else
+ {
+ f = base * power + delta;
+ if (useNewZoomDeltaAlgorithm)
+ f = base * power / std::max(delta, 0.001f);
+ }
clamp(f, m_zoom_params.m_fScopeZoomFactor * power, min_zoom_factor);
- SetZoomFactor(f / power);
-
- m_fRTZoomFactor = GetZoomFactor() * power;
+ if (smooth)
+ m_zoom_params.m_fZoomTargetFactor = f / power; // pip target, UpdateCL eases the current toward it, the step inherits the base factor's author
+ else
+ SetZoomFactor(f / power);
+
+ // pip capture the commanded zoom (the smooth target, not the mid-glide current) so a later
+ // alt-aim / UpdateZoomParams restore lands on the dialed magnification, not a stale snapshot
+ m_fRTZoomFactor = (smooth ? m_zoom_params.m_fZoomTargetFactor : GetZoomFactor()) * power;
+
+ // pip the lever throw sweeps the transition shadow while the prism seats
+ if (click && scope_svp_enabled >= 2 && Device.m_SecondViewport.IsSVPActive() && !fsimilar(base, f / power))
+ {
+ auto& vp = Device.m_SecondViewport;
+ vp.svp_lever_ms = Device.dwTimeGlobal;
+ vp.svp_swing_x = -1.f; // the lever throws across the tube, the side flips with the throw
+ vp.svp_swing_y = 0.f;
+ }
+
+ // pip click flip proof, one line per wheel event on a lever optic
+ if (click && !fsimilar(base, f / power))
+ PipMsg("[SVP-CLICK] %s steps=1 path=click f=%.1f->%.1f (mag %.1f)",
+ cNameSect().c_str(), base, f / power, min_zoom_factor / f);
}
u32 CWeapon::Cost() const
@@ -3301,23 +3494,6 @@ u32 CWeapon::Cost() const
return res;
}
-float CWeapon::GetSecondVPFov() const
-{
- if (m_zoom_params.m_bUseDynamicZoom && IsSecondVPZoomPresent())
- return (m_fRTZoomFactor / 100.f) * g_fov;
-
- return GetSecondVPZoomFactor() * g_fov;
-}
-
-void CWeapon::UpdateSecondVP()
-{
- if (!(ParentIsActor() && (m_pInventory != NULL) && (m_pInventory->ActiveItem() == this)))
- return;
-
- CActor* pActor = smart_cast(H_Parent());
- Device.m_SecondViewport.SetSVPActive(m_zoomtype == 0 && pActor->cam_Active() == pActor->cam_FirstEye() && IsSecondVPZoomPresent() && m_zoom_params.m_fZoomRotationFactor > 0.05f);
-}
-
Fmatrix CWeapon::RayTransform()
{
attachable_hud_item* hi = HudItemData();
diff --git a/src/xrGame/Weapon.h b/src/xrGame/Weapon.h
index bca657fd49..937224ae19 100644
--- a/src/xrGame/Weapon.h
+++ b/src/xrGame/Weapon.h
@@ -15,6 +15,8 @@
#include "CameraRecoil.h"
#include "NewZoomFlag.h"
+#include "../Layers/xrRender/xrRender_console.h" // pip scope_svp_enabled for the SVP zoom accessors
+#include "../xrEngine/svp_gameplay_cvars.h" // pip g_svp_zoom_base and the 75 base for the detent gate
class CEntity;
class ENGINE_API CMotionDef;
@@ -22,6 +24,7 @@ class CSE_ALifeItemWeapon;
class CSE_ALifeItemWeaponAmmo;
class CWeaponMagazined;
class CWeaponMagazinedWGrenade;
+class CActor;
class CWeaponBinoculars;
class CWeaponKnife;
class CWeaponBM16;
@@ -89,10 +92,17 @@ class CWeapon : public CHudItemObject,
return inherited::net_SaveRelevant();
}
- float CWeapon::GetSecondVPFov() const;
IC float GetZRotatingFactor() const { return m_zoom_params.m_fZoomRotationFactor; }
- IC float GetSecondVPZoomFactor() const { return m_zoom_params.m_fSecondVPFovFactor; }
- IC float IsSecondVPZoomPresent() const { return GetSecondVPZoomFactor() > 0.005f; }
+ // pip when scope_svp_enabled the SVP zoom comes from the live zoom factor, else the legacy scope_lense_fov key
+ IC float GetSecondVPZoomFactor() const { return scope_svp_enabled ? GetZoomFactor() : m_zoom_params.m_fSecondVPFovFactor; }
+ // pip on -> a real captured ocular is the presence signal, zoom-0 tube sights (1x thermal/nv)
+ // re-image at 1x instead of falling to the fake screen-window path. off -> legacy test, unchanged
+ float IsSecondVPZoomPresent()
+ {
+ if (!scope_svp_enabled)
+ return GetSecondVPZoomFactor() > 0.005f;
+ return GetSVPCameraMatrix();
+ }
// Up
// Magazine system & etc
@@ -118,6 +128,8 @@ class CWeapon : public CHudItemObject,
virtual void HUD_VisualBulletUpdate(bool force = false, int force_idx = -1);
void UpdateSecondVP();
+ bool GetSVPCameraMatrix(); // pip SVP readiness, a fresh captured lens is present
+ void ApplySvpSightAnchor(CActor* pActor, Fmatrix& trans); // pip swing envelope for the scope shadow
virtual void UpdateCL();
virtual void shedule_Update(u32 dt);
@@ -383,6 +395,9 @@ class CWeapon : public CHudItemObject,
bool m_bZoomDofEnabled;
bool m_bIsZoomModeNow;
float m_fCurrentZoomFactor;
+ float m_fZoomTargetFactor; // pip smooth-zoom target, the current factor eases toward this (dynamic scopes)
+ bool m_bScriptedZoom = false; // pip true when a script authored the live factor, those carry the user fov already
+ bool m_bSvpAuthoredMin = false; // pip authored magnifications set the floor directly, skip the optical-model cap
float m_fZoomRotateTime;
float m_fBaseZoomFactor;
float m_fScopeZoomFactor;
@@ -415,6 +430,10 @@ class CWeapon : public CHudItemObject,
}
virtual float GetMinScopeZoomFactor() const;
+ // pip true svp scopes derive their zoom floor in the authored 75 base so the bottom detent renders 1x
+ virtual bool SvpDetentBase() const { return g_svp_zoom_base && scope_svp_enabled >= 2 && m_zoom_params.m_bUseDynamicZoom && !m_UIScope; }
+ // pip whether this weapon class may take authored magnifications
+ virtual bool SvpMagsEligible() const { return true; }
virtual void ZoomInc();
virtual void ZoomDec();
virtual void OnZoomIn();
@@ -432,9 +451,18 @@ class CWeapon : public CHudItemObject,
return m_zoom_params.m_fCurrentZoomFactor;
}
+ IC bool IsScriptedZoom() const { return m_zoom_params.m_bScriptedZoom; }
+
+ // the lua binding reads this, scripts see the commanded detent while the fov keeps easing
+ float GetZoomFactorScript() const;
+ // the lua binding writes through this, it marks the factor script authored
+ void SetZoomFactorScript(float f);
+
IC void SetZoomFactor(float f)
{
m_zoom_params.m_fCurrentZoomFactor = f;
+ m_zoom_params.m_fZoomTargetFactor = f; // pip keep the smooth-zoom target synced, only scroll (ZoomInc/Dec) pushes it ahead
+ m_zoom_params.m_bScriptedZoom = false; // pip engine and config writes land here, the lua wrapper re-marks
}
virtual float CurrentZoomFactor();
diff --git a/src/xrGame/WeaponAK74.cpp b/src/xrGame/WeaponAK74.cpp
index 8f3c0e113b..d352002301 100644
--- a/src/xrGame/WeaponAK74.cpp
+++ b/src/xrGame/WeaponAK74.cpp
@@ -38,8 +38,15 @@ void CWeaponAK74::script_register (lua_State *L)
.def("IsZoomEnabled", &CWeapon::IsZoomEnabled)
.def("IsZoomed", &CWeapon::IsZoomed)
- .def("GetZoomFactor", &CWeapon::GetZoomFactor)
- .def("SetZoomFactor", &CWeapon::SetZoomFactor)
+ .def("GetZoomFactor", &CWeapon::GetZoomFactorScript)
+ .def("SetZoomFactor", &CWeapon::SetZoomFactorScript)
+ // pip expose the zoom steppers + force-ADS so a gamedata script can drive the scope (lens FX tuning menu)
+ .def("ZoomInc", &CWeapon::ZoomInc)
+ .def("ZoomDec", &CWeapon::ZoomDec)
+ .def("GetMinScopeZoomFactor", &CWeapon::GetMinScopeZoomFactor)
+ .def("CurrentZoomFactor", &CWeapon::CurrentZoomFactor)
+ .def("OnZoomIn", &CWeapon::OnZoomIn)
+ .def("OnZoomOut", &CWeapon::OnZoomOut)
.def("IsSingleHanded", &CWeapon::IsSingleHanded)
diff --git a/src/xrGame/WeaponBinoculars.cpp b/src/xrGame/WeaponBinoculars.cpp
index 8026d7e9e4..6ec2474177 100644
--- a/src/xrGame/WeaponBinoculars.cpp
+++ b/src/xrGame/WeaponBinoculars.cpp
@@ -1,5 +1,6 @@
#include "stdafx.h"
#include "WeaponBinoculars.h"
+#include "../xrEngine/svp_gameplay_cvars.h"
#include "xr_level_controller.h"
@@ -112,6 +113,21 @@ void CWeaponBinoculars::net_Destroy()
void CWeaponBinoculars::UpdateCL()
{
inherited::UpdateCL();
+ // ease the wheel zoom toward its target even without scope_dynamic_zoom, the binocular
+ // wheel bypass works while the base easing gate stays closed
+ if (g_zoom_smooth > 0.f && !m_zoom_params.m_bUseDynamicZoom && IsZoomed())
+ {
+ float& cur = m_zoom_params.m_fCurrentZoomFactor;
+ const float tgt = m_zoom_params.m_fZoomTargetFactor;
+ if (fsimilar(cur, tgt, 0.001f))
+ cur = tgt;
+ else
+ {
+ float a = g_zoom_smooth * Device.fTimeDelta;
+ if (a > 1.f) a = 1.f;
+ cur += (tgt - cur) * a;
+ }
+ }
//manage visible entities here...
if (H_Parent() && IsZoomed() && !IsRotatingToZoom() && m_binoc_vision)
m_binoc_vision->Update();
@@ -163,6 +179,10 @@ void CWeaponBinoculars::ZoomInc()
{
if (binoculars_dynamic_zoom_check && !m_zoom_params.m_bUseDynamicZoom) return;
+ const bool smooth = g_zoom_smooth > 0.f;
+ // when smoothing, advance from the target (the current factor is mid-glide) so rapid scrolls accumulate
+ float base = smooth ? m_zoom_params.m_fZoomTargetFactor : GetZoomFactor();
+
float delta, min_zoom_factor;
if (zoomFlags.test(NEW_ZOOM)) {
newGetZoomData(m_zoom_params.m_fScopeZoomFactor, m_zoom_params.m_fZoomStepCount, delta, min_zoom_factor, czoom);
@@ -170,16 +190,26 @@ void CWeaponBinoculars::ZoomInc()
GetZoomData(m_zoom_params.m_fScopeZoomFactor, m_zoom_params.m_fZoomStepCount, m_zoom_params.m_fMinBaseZoomFactor, delta, min_zoom_factor);
}
- float f = useNewZoomDeltaAlgorithm ? GetZoomFactor() * delta : GetZoomFactor() - delta;
+ float f;
+ if (g_zoom_analog > 0.f)
+ f = base - (min_zoom_factor - m_zoom_params.m_fScopeZoomFactor) / g_zoom_analog;
+ else
+ f = useNewZoomDeltaAlgorithm ? base * delta : base - delta;
clamp(f, m_zoom_params.m_fScopeZoomFactor, min_zoom_factor);
- SetZoomFactor(f);
- czoom = f;
+ if (smooth)
+ m_zoom_params.m_fZoomTargetFactor = f; // UpdateCL eases the current factor toward it
+ else
+ SetZoomFactor(f);
+ czoom = f;
}
void CWeaponBinoculars::ZoomDec()
{
if (binoculars_dynamic_zoom_check && !m_zoom_params.m_bUseDynamicZoom) return;
+ const bool smooth = g_zoom_smooth > 0.f;
+ float base = smooth ? m_zoom_params.m_fZoomTargetFactor : GetZoomFactor();
+
float delta, min_zoom_factor;
if (zoomFlags.test(NEW_ZOOM)) {
newGetZoomData(m_zoom_params.m_fScopeZoomFactor, m_zoom_params.m_fZoomStepCount, delta, min_zoom_factor, czoom);
@@ -187,10 +217,17 @@ void CWeaponBinoculars::ZoomDec()
GetZoomData(m_zoom_params.m_fScopeZoomFactor, m_zoom_params.m_fZoomStepCount, m_zoom_params.m_fMinBaseZoomFactor, delta, min_zoom_factor);
}
- float f = useNewZoomDeltaAlgorithm ? GetZoomFactor() / std::max(delta, 0.001f) : GetZoomFactor() + delta;
+ float f;
+ if (g_zoom_analog > 0.f)
+ f = base + (min_zoom_factor - m_zoom_params.m_fScopeZoomFactor) / g_zoom_analog;
+ else
+ f = useNewZoomDeltaAlgorithm ? base / std::max(delta, 0.001f) : base + delta;
clamp(f, m_zoom_params.m_fScopeZoomFactor, min_zoom_factor);
- SetZoomFactor(f);
- czoom = f;
+ if (smooth)
+ m_zoom_params.m_fZoomTargetFactor = f;
+ else
+ SetZoomFactor(f);
+ czoom = f;
}
void CWeaponBinoculars::save(NET_Packet& output_packet)
diff --git a/src/xrGame/WeaponBinoculars.h b/src/xrGame/WeaponBinoculars.h
index 81bbe386b5..c93b4f8a98 100644
--- a/src/xrGame/WeaponBinoculars.h
+++ b/src/xrGame/WeaponBinoculars.h
@@ -28,6 +28,10 @@ class CWeaponBinoculars : public CWeaponCustomPistol
virtual void OnZoomOut();
virtual void ZoomInc();
virtual void ZoomDec();
+ // pip binocs are flat-window optics with their own zoom chain, keep the legacy fov derivation
+ virtual bool SvpDetentBase() const override { return false; }
+ // pip binocs keep the legacy zoom derivation, authored mags never apply
+ virtual bool SvpMagsEligible() const override { return false; }
virtual void net_Destroy();
virtual BOOL net_Spawn(CSE_Abstract* DC);
bool can_kill() const;
diff --git a/src/xrGame/WeaponBinocularsVision.cpp b/src/xrGame/WeaponBinocularsVision.cpp
index 17750adf0e..5f729735cf 100644
--- a/src/xrGame/WeaponBinocularsVision.cpp
+++ b/src/xrGame/WeaponBinocularsVision.cpp
@@ -106,8 +106,22 @@ void SBinocVisibleObj::Update()
Fbox b = m_object->Visual()->getVisData().box;
+ // under an active true-PiP binocular the player sees the magnified SVP image in the flat panel,
+ // project the target through the SVP camera so the bracket tracks the magnified view
+ const bool svp_panel = Device.true_pip_on
+ && Device.m_SecondViewport.IsSVPActive()
+ && Device.m_SecondViewport.svp_panel_flat;
+
Fmatrix xform;
- xform.mul(Device.mFullTransform, m_object->XFORM());
+ if (svp_panel)
+ {
+ // matrices[1] carries no full transform, compose the SVP view-projection (already magnified)
+ Fmatrix svp_vp;
+ svp_vp.mul(Device.matrices[1].mProject, Device.matrices[1].mView);
+ xform.mul(svp_vp, m_object->XFORM());
+ }
+ else
+ xform.mul(Device.mFullTransform, m_object->XFORM());
Fvector2 mn = {flt_max,flt_max}, mx = {flt_min,flt_min};
for (u32 k = 0; k < 8; ++k)
@@ -129,11 +143,35 @@ void SBinocVisibleObj::Update()
if (FALSE == screen_rect.intersected(new_rect)) return;
if (new_rect.in(screen_rect.lt) && new_rect.in(screen_rect.rb)) return;
- std::swap(mn.y, mx.y);
- mn.x = (1.f + mn.x) / 2.f * UI_BASE_WIDTH;
- mx.x = (1.f + mx.x) / 2.f * UI_BASE_WIDTH;
- mn.y = (1.f - mn.y) / 2.f * UI_BASE_HEIGHT;
- mx.y = (1.f - mx.y) / 2.f * UI_BASE_HEIGHT;
+ if (svp_panel)
+ {
+ // place the bracket on the on-screen panel (the flat lens mesh projected through the hud
+ // transform), the panel rides level so map the svp frame axis-aligned, +x right +y up
+ const Fvector cw = Device.m_SecondViewport.eyepiece.m_W.c;
+ Fvector4 pc, pw, ph;
+ Device.mFullTransformHud.transform(pc, cw);
+ Device.mFullTransformHud.transform(pw, Fvector(cw).add(Device.m_SecondViewport.svp_panel_ax_w));
+ Device.mFullTransformHud.transform(ph, Fvector(cw).add(Device.m_SecondViewport.svp_panel_ax_h));
+ const float vcrop = (Device.m_SecondViewport.svp_panel_vcrop > 0.0001f)
+ ? Device.m_SecondViewport.svp_panel_vcrop : 1.f;
+ const float half_u = _abs(pw.x - pc.x); // panel half-width (svp ndc +/-1)
+ const float half_v = _abs(ph.y - pc.y) / vcrop; // panel half-height, shader V-crop
+ // svp ndc -> panel ndc, then to UI (ndc y up flips to UI y down)
+ const float lx = pc.x + mn.x * half_u, rx = pc.x + mx.x * half_u;
+ const float ty = pc.y + mx.y * half_v, bt = pc.y + mn.y * half_v;
+ mn.x = (1.f + lx) / 2.f * UI_BASE_WIDTH;
+ mx.x = (1.f + rx) / 2.f * UI_BASE_WIDTH;
+ mn.y = (1.f - ty) / 2.f * UI_BASE_HEIGHT;
+ mx.y = (1.f - bt) / 2.f * UI_BASE_HEIGHT;
+ }
+ else
+ {
+ std::swap(mn.y, mx.y);
+ mn.x = (1.f + mn.x) / 2.f * UI_BASE_WIDTH;
+ mx.x = (1.f + mx.x) / 2.f * UI_BASE_WIDTH;
+ mn.y = (1.f - mn.y) / 2.f * UI_BASE_HEIGHT;
+ mx.y = (1.f - mx.y) / 2.f * UI_BASE_HEIGHT;
+ }
if (mx.x - mn.x < RECT_SIZE)
mx.x = mn.x + RECT_SIZE;
diff --git a/src/xrGame/console_registrator_script.cpp b/src/xrGame/console_registrator_script.cpp
index 5a78d3c45b..685290d46c 100644
--- a/src/xrGame/console_registrator_script.cpp
+++ b/src/xrGame/console_registrator_script.cpp
@@ -2,9 +2,11 @@
#include "console_registrator.h"
#include "../xrEngine/xr_ioconsole.h"
#include "../xrEngine/xr_ioc_cmd.h"
+#include "../xrEngine/device.h"
#include "ai_space.h"
#include "script_engine.h"
#include "../xrSound/Sound.h"
+#include "player_hud.h"
using namespace luabind;
@@ -82,12 +84,60 @@ ::luabind::object get_console_token_list(CConsole* c, LPCSTR cmd)
return table;
}
+// pip true if a real PiP scope is rendering via the second viewport (reflex/iron/non-PiP sights never set it)
+bool is_svp_active()
+{
+ return Device.m_SecondViewport.IsSVPActive();
+}
+
+// pip measured lens optics, kill-switch gated so a default build never runs detection
+extern int ps_r__svp_measured_optics;
+
+// run the mesh detection on one hud model, true when a lens with an objective is found
+static bool svp_detect_hud(attachable_hud_item* h, SLensDetection& d)
+{
+ return h && h->m_model && h->m_model->GetLensDetection(d) && d.ok && d.has_objective;
+}
+
+// the active scope hud, the attached scope model carries an addon lens, the weapon model an integral one
+static bool svp_detect_active(SLensDetection& d)
+{
+ if (!ps_r__svp_measured_optics || !g_player_hud)
+ return false;
+ if (svp_detect_hud(g_player_hud->attached_item(SCOPE_ATTACH_IDX), d))
+ return true;
+ return svp_detect_hud(g_player_hud->attached_item(0), d);
+}
+
+// scope_objective_lens_offset x,y,z,w in eyepiece-radius units, "" when off or nothing fits
+LPCSTR svp_detected_offset()
+{
+ static string128 s;
+ SLensDetection d;
+ if (!svp_detect_active(d))
+ return "";
+ xr_sprintf(s, "%f,%f,%f,%f", d.offset.x, d.offset.y, d.offset.z, d.offset.w);
+ return s;
+}
+
+// s3ds_objective_mm 2000 x obj_radius, -1 when off or nothing fits
+float svp_detected_obj_mm()
+{
+ SLensDetection d;
+ if (!svp_detect_active(d))
+ return -1.f;
+ return d.mm > 0.f ? d.mm : -1.f;
+}
+
#pragma optimize("s",on)
void console_registrator::script_register(lua_State* L)
{
module(L)
[
def("get_console", &console),
+ def("is_svp_active", &is_svp_active),
+ def("svp_detected_offset", &svp_detected_offset),
+ def("svp_detected_obj_mm", &svp_detected_obj_mm),
class_("CConsole")
.def("execute", &CConsole::Execute)
diff --git a/src/xrGame/level_script.cpp b/src/xrGame/level_script.cpp
index 3b9433e718..1de7711cce 100644
--- a/src/xrGame/level_script.cpp
+++ b/src/xrGame/level_script.cpp
@@ -951,6 +951,20 @@ float get_cam_effector_length(int id)
return e ? e->GetAnimatorLength() : 0.0f;
}
+// drives the effector fov live without forcing absolute positioning, 0 clears
+void set_cam_effector_fov(int id, float fov)
+{
+ CAnimatorCamEffectorScriptCB* e = smart_cast(Actor()->Cameras().GetCamEffector((ECamEffectorType)id));
+ if (e)
+ e->m_fov = clampr(fov, 0.f, 170.f);
+}
+
+float get_cam_effector_fov(int id)
+{
+ CAnimatorCamEffectorScriptCB* e = smart_cast(Actor()->Cameras().GetCamEffector((ECamEffectorType)id));
+ return e ? e->m_fov : 0.0f;
+}
+
bool check_cam_effector(int id)
{
@@ -2630,6 +2644,8 @@ void CLevel::script_register(lua_State* L)
def("set_cam_effector_factor", &set_cam_effector_factor),
def("get_cam_effector_factor", &get_cam_effector_factor),
def("get_cam_effector_length", &get_cam_effector_length),
+ def("set_cam_effector_fov", &set_cam_effector_fov),
+ def("get_cam_effector_fov", &get_cam_effector_fov),
def("check_cam_effector", &check_cam_effector),
def("add_pp_effector", &add_pp_effector),
def("set_pp_effector_factor", &set_pp_effector_factor),
diff --git a/src/xrGame/svp_mags.cpp b/src/xrGame/svp_mags.cpp
new file mode 100644
index 0000000000..a1dc3a853b
--- /dev/null
+++ b/src/xrGame/svp_mags.cpp
@@ -0,0 +1,102 @@
+#include "stdafx.h"
+#include "svp_mags.h"
+#include "../xrEngine/svp_gameplay_cvars.h"
+
+// pip parse a 3dss magnifications string, single N fixed, dash M-N dynamic, comma M,N stepped toggle
+// returns svp_mag_none on empty or malformed so the caller keeps the legacy factors
+static svp_mag_mode parse_svp_magnifications(LPCSTR raw, float& min_mag, float& max_mag)
+{
+ if (!raw || !raw[0]) return svp_mag_none;
+ string256 buf;
+ u32 j = 0;
+ for (LPCSTR p = raw; *p && j + 1 < sizeof(buf); ++p)
+ if (*p != ' ' && *p != '\t') buf[j++] = *p;
+ buf[j] = 0;
+ if (!buf[0]) return svp_mag_none;
+
+ float a = 0.f, b = 0.f; char extra = 0;
+ if (strchr(buf, '-'))
+ {
+ if (sscanf(buf, "%f-%f%c", &a, &b, &extra) != 2) return svp_mag_none;
+ min_mag = a; max_mag = b; return svp_mag_dynamic;
+ }
+ if (strchr(buf, ','))
+ {
+ if (sscanf(buf, "%f,%f%c", &a, &b, &extra) != 2) return svp_mag_none;
+ min_mag = a; max_mag = b; return svp_mag_stepped;
+ }
+ if (sscanf(buf, "%f%c", &a, &extra) != 1) return svp_mag_none;
+ min_mag = max_mag = a; return svp_mag_fixed;
+}
+
+// pip three tier resolution with the [SVP-MAGS] proof line, mode none keeps the legacy factors
+// true when the section authors either tier, malformed stays loud and terminal
+static bool resolve_from_section(LPCSTR sect, float zoom_multiple, svp_mags_data& out)
+{
+ float min_mag = 0.f, max_mag = 0.f;
+ bool authored = false, dyn = false, stepped = false;
+
+ LPCSTR raw = READ_IF_EXISTS(pSettings, r_string, sect, "magnifications", NULL);
+ svp_mag_mode mode = parse_svp_magnifications(raw, min_mag, max_mag);
+ if (mode != svp_mag_none)
+ {
+ authored = true;
+ dyn = (mode != svp_mag_fixed);
+ stepped = (mode == svp_mag_stepped);
+ }
+ else if (pSettings->line_exist(sect, "magnification"))
+ {
+ // singular tier, magnification is the top, min from min_magnification (default 1x) when dynamic
+ max_mag = pSettings->r_float(sect, "magnification");
+ const bool dzoom = READ_IF_EXISTS(pSettings, r_bool, sect, "scope_dynamic_zoom", false);
+ min_mag = dzoom ? READ_IF_EXISTS(pSettings, r_float, sect, "min_magnification", 1.f) : max_mag;
+ raw = "magnification";
+ authored = true;
+ dyn = dzoom;
+ }
+
+ if (!authored)
+ return false;
+
+ const float svp_mag_base = SVP_ZOOM_BASE_FOV / 0.75f; // 100, mag = base / f
+ // physical band, 0.5x floor mirrors the 200 default min factor (100/200), 100x holds the top factor >= 1
+ clamp(min_mag, svp_mag_base / 200.f, svp_mag_base);
+ clamp(max_mag, svp_mag_base / 200.f, svp_mag_base);
+ const float f_top = svp_mag_base / max_mag / zoom_multiple;
+ const float f_floor = svp_mag_base / min_mag;
+ // dynamic detents need an ordered range and the top factor under the base fov (NewGetZoomData VERIFY)
+ if (min_mag > max_mag || (dyn && f_top >= SVP_ZOOM_BASE_FOV))
+ {
+ PipMsg("[SVP-MAGS] %s '%s' malformed, legacy retained", sect, raw ? raw : "");
+ return true;
+ }
+
+ out.mode = dyn ? (stepped ? svp_mag_stepped : svp_mag_dynamic) : svp_mag_fixed;
+ out.f_top = f_top;
+ out.f_floor = f_floor;
+ PipMsg("[SVP-MAGS] %s '%s' mag=[%.2f..%.2f] f=[%.1f..%.1f]",
+ sect, raw ? raw : "", min_mag, max_mag, f_floor, f_top);
+ return true;
+}
+
+// pip tries the passed section then the parent stripped scope section
+// an unauthored wpn_x_1p59 with parent_section wpn_x resolves from 1p59
+svp_mags_data svp_mags_resolve(LPCSTR sect, float zoom_multiple)
+{
+ svp_mags_data out;
+ if (resolve_from_section(sect, zoom_multiple, out))
+ return out;
+
+ LPCSTR parent = READ_IF_EXISTS(pSettings, r_string, sect, "parent_section", NULL);
+ if (parent && parent[0])
+ {
+ const u32 plen = xr_strlen(parent);
+ if (xr_strlen(sect) > plen + 1 && 0 == strncmp(sect, parent, plen) && sect[plen] == '_')
+ {
+ LPCSTR suffix = sect + plen + 1;
+ if (pSettings->section_exist(suffix))
+ resolve_from_section(suffix, zoom_multiple, out);
+ }
+ }
+ return out;
+}
diff --git a/src/xrGame/svp_mags.h b/src/xrGame/svp_mags.h
new file mode 100644
index 0000000000..3b65b6f8d5
--- /dev/null
+++ b/src/xrGame/svp_mags.h
@@ -0,0 +1,19 @@
+#ifndef svp_magsH
+#define svp_magsH
+#pragma once
+
+// pip authored magnifications, derives 75 base zoom endpoints from a scope section
+
+enum svp_mag_mode { svp_mag_none, svp_mag_fixed, svp_mag_dynamic, svp_mag_stepped };
+
+struct svp_mags_data
+{
+ svp_mag_mode mode = svp_mag_none;
+ float f_top = 0.f;
+ float f_floor = 0.f;
+};
+
+// magnifications string wins, then the magnification/min_magnification floats, none = legacy
+svp_mags_data svp_mags_resolve(LPCSTR sect, float zoom_multiple);
+
+#endif
diff --git a/src/xrGame/svp_weapon.cpp b/src/xrGame/svp_weapon.cpp
new file mode 100644
index 0000000000..027ac764f0
--- /dev/null
+++ b/src/xrGame/svp_weapon.cpp
@@ -0,0 +1,247 @@
+#include "stdafx.h"
+#include "Weapon.h"
+#include "actor.h"
+#include "actoreffector.h"
+#include "../xrEngine/CameraBase.h"
+#include "../xrEngine/CameraManager.h"
+#include "level.h"
+#include "player_hud.h"
+
+// pip weapon raise settled threshold, GetZRotatingFactor at or above this reads as fully aimed
+static const float SVP_SETTLED_ROT = 0.999f;
+
+// pip hosts the swing envelope that drives the scope shadow crescent
+void CWeapon::ApplySvpSightAnchor(CActor* pActor, Fmatrix& trans)
+{
+ auto& vp = Device.m_SecondViewport;
+ // the aim reference axis for the swing envelope
+ {
+ const Fvector cr = pActor->cam_FirstEye()->vDirection;
+ // swing envelope for the shadow, aim axis angular acceleration reads recoil kicks and
+ // hard swings, smooth tracking holds near zero so calm aim never charges it
+ {
+ static Fvector s_env_dir = {0.f, 0.f, 1.f};
+ static float s_env_w = 0.f;
+ static float s_env_accel = 0.f;
+ const float dt = _max(Device.fTimeDelta, 0.001f);
+ // cross magnitude keeps the tiny frame angles numerically clean, a short smooth
+ // window drops the single frame spikes that flickered the crescent at rest
+ Fvector cx;
+ cx.crossproduct(s_env_dir, cr);
+ float sind = cx.magnitude();
+ clamp(sind, 0.f, 1.f);
+ const float w = asinf(sind) / dt;
+ s_env_accel += (_abs(w - s_env_w) / dt - s_env_accel) * (1.f - expf(-dt / 0.04f));
+ // the smoothed lateral rate picks the crescent side, the direction latches so the
+ // fading crescent keeps its side instead of recentering into a ring
+ {
+ static Fvector2 s_env_swing = {0.f, 0.f};
+ Fvector dm;
+ dm.sub(cr, s_env_dir);
+ Fvector up;
+ up.set(pActor->cam_FirstEye()->vNormal);
+ Fvector rt;
+ rt.crossproduct(up, cr);
+ rt.normalize_safe();
+ const float ks = 1.f - expf(-dt / 0.12f);
+ s_env_swing.x += (dm.dotproduct(rt) / dt - s_env_swing.x) * ks;
+ s_env_swing.y += (dm.dotproduct(up) / dt - s_env_swing.y) * ks;
+ const float sm = _sqrt(s_env_swing.x * s_env_swing.x + s_env_swing.y * s_env_swing.y);
+ if (sm > 0.05f)
+ {
+ vp.svp_swing_x = s_env_swing.x / sm;
+ vp.svp_swing_y = s_env_swing.y / sm;
+ }
+ }
+ s_env_dir.set(cr);
+ s_env_w = w;
+ // charges only at settled aim so the raise and zoom rotate never draw the tunnel
+ static int s_prev_ammo = -1;
+ const int ammo = GetAmmoElapsed();
+ const bool shot = (s_prev_ammo != -1 && ammo < s_prev_ammo);
+ s_prev_ammo = ammo;
+ extern int g_svp_crescent;
+ if (g_svp_crescent && vp.IsSVPActive() && GetZRotatingFactor() > 0.999f)
+ {
+ // every shot kicks the crescent in, swings must clear a gate the walk bob cannot
+ const float dz = _max(24.f / _max(vp.svp_mag, 1.f), 12.f);
+ float sw = (s_env_accel - dz) / (0.5f * dz);
+ clamp(sw, 0.f, 1.f);
+ if (shot)
+ {
+ vp.svp_shadow_gain += 0.45f;
+ clamp(vp.svp_shadow_gain, 0.f, 1.f);
+ }
+ else if (sw > vp.svp_shadow_gain)
+ vp.svp_shadow_gain += (sw - vp.svp_shadow_gain) * (1.f - expf(-dt / 0.06f));
+ // tuning numbers for the feel report, quiet at true rest
+ if (s_env_accel > 2.f || vp.svp_shadow_gain > 0.05f)
+ {
+ static u32 s_sw_ms = 0;
+ if (Device.dwTimeGlobal - s_sw_ms > 1000)
+ {
+ s_sw_ms = Device.dwTimeGlobal;
+ PipMsg("[SVP-SWING] accel %.1f dz %.1f gain %.2f mag %.1f",
+ s_env_accel, dz, vp.svp_shadow_gain, vp.svp_mag);
+ }
+ }
+ }
+ }
+ }
+}
+
+void CWeapon::UpdateSecondVP()
+{
+ if (!(ParentIsActor() && (m_pInventory != NULL) && (m_pInventory->ActiveItem() == this)))
+ return;
+
+ CActor* pActor = smart_cast(H_Parent());
+ if (!scope_svp_enabled)
+ {
+ // legacy fake-SVP, stock activation condition unchanged
+ Device.m_SecondViewport.SetSVPActive(m_zoomtype == 0 && pActor->cam_Active() == pActor->cam_FirstEye() && IsSecondVPZoomPresent() && m_zoom_params.m_fZoomRotationFactor > 0.05f);
+ return;
+ }
+
+ // pip true SVP, the scope_debug force-path still needs IsZoomed so a debug cvar can't drive it off-ADS
+ const bool svp_act = (scope_debug && IsSecondVPZoomPresent() && IsZoomed())
+ || (m_zoomtype == 0 && pActor->cam_Active() == pActor->cam_FirstEye() && IsSecondVPZoomPresent() && IsZoomed());
+ // pip activation edge diag, every flip logs every term
+ {
+ auto& vp = Device.m_SecondViewport;
+ static bool s_prev_act = false;
+ if (svp_act != s_prev_act)
+ {
+ s_prev_act = svp_act;
+ PipMsg("[SVP-ACT] %d zt=%d cam=%d zoomed=%d ok=%d lens_r=%.3f stale=%u zf=%.1f sec=%s",
+ (int)svp_act, m_zoomtype,
+ (int)(pActor->cam_Active() == pActor->cam_FirstEye()), (int)IsZoomed(),
+ (int)vp.svp_sight_ok, vp.svp_lens_r,
+ Device.dwFrame - vp.svp_sight_frame, GetZoomFactor(), cNameSect().c_str());
+ }
+ }
+ Device.m_SecondViewport.SetSVPActive(svp_act);
+ // the shadow swing envelope drains steadily, the charge site outruns it on real kicks
+ Device.m_SecondViewport.svp_shadow_gain += (0.f - Device.m_SecondViewport.svp_shadow_gain)
+ * (1.f - expf(-Device.fTimeDelta / 0.7f));
+ // the lever throw holds the envelope up for the real throw time, the drain takes the tail
+ {
+ const float SVP_LEVER_THROW_S = 0.2f; // measured SpecterDR lever throw
+ auto& vp = Device.m_SecondViewport;
+ if (vp.svp_lever_ms)
+ {
+ const float t = (Device.dwTimeGlobal - vp.svp_lever_ms) / 1000.f;
+ if (t >= 0.f && t < SVP_LEVER_THROW_S)
+ vp.svp_shadow_gain = _max(vp.svp_shadow_gain, 1.f - t / SVP_LEVER_THROW_S);
+ else
+ vp.svp_lever_ms = 0;
+ }
+ }
+ // aimed on irons or the launcher, the scope lens still renders but its pose is not the scope's
+ Device.m_SecondViewport.svp_alt_sight = (m_zoomtype != 0 && IsZoomed());
+ // authored recoil relax time publish, the recoil settle window derives from it
+ Device.m_SecondViewport.svp_recoil_relax_s = (zoom_cam_recoil.RelaxSpeed > EPS)
+ ? zoom_cam_recoil.Dispersion / zoom_cam_recoil.RelaxSpeed : 0.f;
+
+ // pip publish a raise transient free zoom for the svp camera, the raise holds the dialed
+ // magnification and hands off to the live target after settle
+ {
+ extern int g_svp_zoom_sync;
+ extern float scope_radius;
+ auto& vp = Device.m_SecondViewport;
+ // a selector, not an integrator, the raise publishes the steady dialed magnification and the
+ // settled aim publishes the weapon's own glided factor so one integrator drives image + reticle
+ float pub;
+ if (!g_svp_zoom_sync || !vp.IsSVPActive() || !IsZoomed())
+ pub = GetZoomFactor(); // fallback publishes current, the >1 guard falls back to hud_params.y when unset
+ else if (GetZRotatingFactor() > SVP_SETTLED_ROT)
+ pub = GetZoomFactor(); // settled, the weapon glide is the single integrator
+ else
+ pub = m_zoom_params.m_bUseDynamicZoom
+ ? (scope_radius > 0.0 ? m_fRTZoomFactor / scope_scrollpower : m_fRTZoomFactor)
+ : CurrentZoomFactor(); // raise, hold the steady dialed magnification, no easing needed
+ vp.svp_zoom_pub = pub;
+ // config factors ride the 75 base and rescale to the live fov downstream, script
+ // authored factors already carry the user fov and pass through
+ extern float g_fov;
+ vp.svp_fov_scale = m_zoom_params.m_bScriptedZoom ? 1.f : (g_fov / SVP_ZOOM_BASE_FOV);
+ // the main view stays wide at g_fov through a pip scope, publish it as the punch free mag
+ // reference so a recoil fov effector never reaches scope_magnification
+ vp.svp_aim_fov = g_fov;
+ // authored mag flat optics keep the clean optical mag, the panel subtense override stays off
+ vp.svp_authored_mag = m_zoom_params.m_bSvpAuthoredMin;
+ // authored mins and detent base mins are 75 base, the legacy optical model min rides g_fov
+ vp.svp_min_75base = m_zoom_params.m_bSvpAuthoredMin || SvpDetentBase();
+ }
+
+ // pip mirror the live ballistic ray + muzzle point for the [3DB] overlay, and range the zero:
+ // the shot converges where the sight line meets the aimed surface, capped at g_svp_zero
+ {
+ extern float g_svp_zero;
+ auto& vp = Device.m_SecondViewport;
+ // the pick remaps through the actor's real eye under demo_record (CHudItem::Ray)
+ const SPickParam& pp = GetPick();
+ vp.fire_ray_pos.set(pp.defs.start);
+ vp.fire_ray_dir.set(pp.defs.dir);
+ vp.muzzle_pos.set(get_LastFP());
+ // the actor's true eye, immune to the demo camera, the overlay's crosshair ray
+ vp.eye_ray_pos.set(pActor->cam_FirstEye()->vPosition);
+ vp.eye_ray_dir.set(pActor->cam_FirstEye()->vDirection);
+ float zero_eff = g_svp_zero;
+ if (g_svp_zero > 0.f && vp.IsSVPActive() && vp.svp_sight_ok)
+ {
+ // the published sight line, never the render scratch (zeroes at frame start mid tick)
+ Fvector so = vp.svp_sight_pos;
+ Fvector sd = vp.svp_sight_dir;
+ collide::rq_result RQ;
+ if (Level().ObjectSpace.RayPick(so, sd, g_svp_zero, collide::rqtBoth, RQ, H_Parent()))
+ zero_eff = _max(RQ.range, 2.f);
+ }
+ vp.fire_ray_zero = zero_eff;
+ vp.fire_ray_frame = Device.dwFrame;
+ }
+}
+
+// pip SVP readiness, true when a fresh captured lens exists, reads the stable sight publish
+bool CWeapon::GetSVPCameraMatrix()
+{
+ auto& vp = Device.m_SecondViewport;
+ return vp.svp_sight_ok && vp.svp_lens_r > EPS && Device.dwFrame - vp.svp_sight_frame < 8;
+}
+
+// pip zeroing, the shot converges onto the sight line at the ranged zero, then the tracer
+// ring records the final departure ray, called from CActor::g_fireParams
+void svp_apply_zero_and_trace(const SPickParam& pp, Fvector& fire_pos, Fvector& fire_dir)
+{
+ // pip zeroing, the shot converges onto the sight line at the ranged zero (0 disables)
+ extern float g_svp_zero;
+ auto& vp = Device.m_SecondViewport;
+ if (g_svp_zero > 0.f && Device.true_pip_on && vp.IsSVPActive() && vp.svp_sight_ok)
+ {
+ const float zero_m = (vp.fire_ray_zero > 0.f) ? vp.fire_ray_zero : g_svp_zero;
+ // scoped shots depart the muzzle, the stock ray stays when the barrel is blocked
+ if (!pp.barrel_blocked && vp.muzzle_pos.square_magnitude() > EPS)
+ fire_pos.set(vp.muzzle_pos);
+ // the stable published sight line
+ Fvector so = vp.svp_sight_pos;
+ Fvector axis = vp.svp_sight_dir;
+ Fvector zero_pt;
+ zero_pt.mad(so, axis, zero_m);
+ Fvector d;
+ d.sub(zero_pt, fire_pos);
+ if (d.magnitude() > 1.f)
+ {
+ d.normalize();
+ fire_dir.set(d);
+ }
+ }
+
+ // pip [3DB] tracer ring, records the final departure ray of every shot for the fading overlay
+ {
+ auto& tr = vp.fire_traces[vp.fire_trace_head % 16];
+ tr.pos.set(fire_pos);
+ tr.dir.set(fire_dir);
+ tr.time_ms = Device.dwTimeGlobal;
+ vp.fire_trace_head++;
+ }
+}
diff --git a/src/xrGame/xrGame.vcxproj b/src/xrGame/xrGame.vcxproj
index 632f6076b5..32d1d72787 100644
--- a/src/xrGame/xrGame.vcxproj
+++ b/src/xrGame/xrGame.vcxproj
@@ -2025,6 +2025,7 @@
+
@@ -4244,6 +4245,8 @@
+
+
pch_script.h
$(IntDir)$(ProjectName)_script.pch
diff --git a/src/xrGame/xrGame.vcxproj.filters b/src/xrGame/xrGame.vcxproj.filters
index dd2acbff5f..cb95d7e9d5 100644
--- a/src/xrGame/xrGame.vcxproj.filters
+++ b/src/xrGame/xrGame.vcxproj.filters
@@ -5721,6 +5721,9 @@
Core\Client\Objects\items & weapons\Weapons\Custom Weapon\Weapon Generic
+
+ Core\Client\Objects\items & weapons\Weapons\Custom Weapon\Weapon Generic
+
Core\Client\Objects\items & weapons\Weapons\Custom Weapon\Ammo
@@ -9269,6 +9272,12 @@
Core\Client\Objects\items & weapons\Weapons\Custom Weapon\Weapon Generic
+
+ Core\Client\Objects\items & weapons\Weapons\Custom Weapon\Weapon Generic
+
+
+ Core\Client\Objects\items & weapons\Weapons\Custom Weapon\Weapon Generic
+
Core\Client\Objects\items & weapons\Weapons\Custom Weapon\Weapon Generic