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 + + + 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