From 08935ec07a1da7c7808d48844a4d06dbc4358bd2 Mon Sep 17 00:00:00 2001 From: TheLostInPlace <42595640+TheLostInPlace@users.noreply.github.com> Date: Sun, 19 Jul 2026 00:50:50 -0500 Subject: [PATCH 1/4] staging: fresh commit for PR --- .gitignore | 6 +- gamedata/configs/mod_system_pip_tonemap.ltx | 16 + gamedata/configs/pip_objective_mm.ltx | 78 + .../configs/text/eng/ui_mm_modded_exes.xml | 50 + gamedata/scripts/_imgui_groups.script | 2 +- .../scripts/options_modded_exes_pip.script | 64 + .../scripts/options_modded_exes_visual.script | 2 + .../scripts/zzz_extra_scope_features.script | 167 +++ gamedata/shaders/r3/scope_color_write.s | 20 + gamedata/shaders/r3/scope_custom_depth.h | 26 + gamedata/shaders/r3/scope_debug.ps | Bin 0 -> 1091 bytes gamedata/shaders/r3/scope_debug.s | 15 + gamedata/shaders/r3/scope_defines.h | 19 + gamedata/shaders/r3/scope_depth_write.ps | Bin 0 -> 1026 bytes gamedata/shaders/r3/scope_depth_write.s | 5 + gamedata/shaders/r3/scope_vertex.vs | 55 + gamedata/shaders/r3/svp_distort_stamp.ps | Bin 0 -> 339 bytes gamedata/shaders/r3/svp_distort_stamp.s | 6 + gamedata/shaders/r3/svp_hooks_common.h | 41 + gamedata/shaders/r3/svp_hooks_image.h | 208 +++ gamedata/shaders/r3/svp_hooks_lens.h | 15 + gamedata/shaders/r3/svp_hooks_reticle.h | 55 + gamedata/shaders/r3/svp_hooks_shadow.h | 30 + gamedata/shaders/r3/svp_nearblur.ps | Bin 0 -> 2649 bytes gamedata/shaders/r3/svp_nearblur.s | 8 + gamedata/shaders/r3/svp_taa_stamp.ps | Bin 0 -> 307 bytes gamedata/shaders/r3/svp_taa_stamp.s | 6 + gamedata/textures/fx/svp_rain.dds | Bin 0 -> 349652 bytes src/Include/xrRender/Kinematics.h | 19 + src/Include/xrRender/ParticleCustom.h | 3 + .../Blender_Recorder_StandartBinding.cpp | 97 +- src/Layers/xrRender/Debug/dxPixEventWrapper.h | 5 + src/Layers/xrRender/Debug/renderdoc_app.h | 875 +++++++++++ src/Layers/xrRender/DetailManager_VS.cpp | 17 +- src/Layers/xrRender/FTreeVisual.cpp | 11 +- src/Layers/xrRender/HW.h | 4 + src/Layers/xrRender/Light_DB.cpp | 8 +- src/Layers/xrRender/ParticleEffect.cpp | 20 +- src/Layers/xrRender/ParticleEffect.h | 4 + src/Layers/xrRender/ParticleGroup.cpp | 22 + src/Layers/xrRender/ParticleGroup.h | 3 + src/Layers/xrRender/R_Backend.h | 2 +- src/Layers/xrRender/R_Backend_Runtime.h | 48 +- src/Layers/xrRender/R_Backend_xform.cpp | 73 +- src/Layers/xrRender/R_Backend_xform.h | 24 +- src/Layers/xrRender/R_calculate.cpp | 9 + src/Layers/xrRender/R_sun.cpp | 21 + src/Layers/xrRender/Shader.h | 2 +- src/Layers/xrRender/SkeletonCustom.h | 5 +- src/Layers/xrRender/SkeletonX.cpp | 26 +- src/Layers/xrRender/SkeletonX.h | 5 + src/Layers/xrRender/dxEnvironmentRender.cpp | 9 +- src/Layers/xrRender/dxRenderDeviceRender.cpp | 5 +- src/Layers/xrRender/lights_render.cpp | 140 +- src/Layers/xrRender/r__dsgraph_build.cpp | 106 +- src/Layers/xrRender/r__dsgraph_manager.h | 36 +- src/Layers/xrRender/r__dsgraph_render.cpp | 105 +- src/Layers/xrRender/r__dsgraph_types.h | 6 + .../rendertarget_phase_nightvision.cpp | 17 +- .../xrRender/rendertarget_phase_smaa.cpp | 25 + src/Layers/xrRender/svp_console.cpp | 256 ++++ src/Layers/xrRender/svp_console.h | 131 ++ src/Layers/xrRender/svp_constants.cpp | 185 +++ src/Layers/xrRender/svp_constants.h | 14 + src/Layers/xrRender/svp_dsgraph_cull.cpp | 67 + src/Layers/xrRender/svp_hud_latch.cpp | 273 ++++ src/Layers/xrRender/svp_lens_detect.cpp | 251 ++++ src/Layers/xrRender/svp_lens_detect.h | 8 + src/Layers/xrRender/svp_lens_detect_math.cpp | 359 +++++ src/Layers/xrRender/svp_lens_detect_math.h | 67 + src/Layers/xrRender/xrRender_console.cpp | 65 + src/Layers/xrRender/xrRender_console.h | 4 + .../xrRenderDX10/dx10DetailManager_VS.cpp | 15 +- src/Layers/xrRenderDX10/dx10EventWrapper.cpp | 42 +- src/Layers/xrRenderDX10/dx10EventWrapper.h | 25 +- src/Layers/xrRenderDX10/dx10HW.cpp | 21 +- .../dx10ResourceManager_Scripting.cpp | 6 + src/Layers/xrRenderDX10/dx10SH_RT.cpp | 8 + src/Layers/xrRenderPC_R1/xrRender_R1.vcxproj | 12 + .../xrRenderPC_R1/xrRender_R1.vcxproj.filters | 30 + src/Layers/xrRenderPC_R2/xrRender_R2.vcxproj | 12 + .../xrRenderPC_R2/xrRender_R2.vcxproj.filters | 30 + src/Layers/xrRenderPC_R3/xrRender_R3.vcxproj | 12 + .../xrRenderPC_R3/xrRender_R3.vcxproj.filters | 30 + src/Layers/xrRenderPC_R4/SvpDlss.h | 65 + src/Layers/xrRenderPC_R4/r4.cpp | 48 +- src/Layers/xrRenderPC_R4/r4.h | 10 +- src/Layers/xrRenderPC_R4/r4_R_render.cpp | 467 +++++- src/Layers/xrRenderPC_R4/r4_rendertarget.cpp | 297 ++-- src/Layers/xrRenderPC_R4/r4_rendertarget.h | 42 + .../r4_rendertarget_phase_combine.cpp | 236 ++- .../r4_rendertarget_phase_luminance.cpp | 9 +- src/Layers/xrRenderPC_R4/svp_camera.cpp | 1308 +++++++++++++++++ src/Layers/xrRenderPC_R4/svp_camera.h | 13 + src/Layers/xrRenderPC_R4/svp_optics.cpp | 991 +++++++++++++ src/Layers/xrRenderPC_R4/svp_stats.cpp | 524 +++++++ src/Layers/xrRenderPC_R4/svp_stats.h | 43 + src/Layers/xrRenderPC_R4/svp_target.cpp | 121 ++ src/Layers/xrRenderPC_R4/svp_target.h | 11 + src/Layers/xrRenderPC_R4/xrRender_R4.vcxproj | 19 + .../xrRenderPC_R4/xrRender_R4.vcxproj.filters | 51 + src/xrCore/Xr_ini.cpp | 13 + src/xrCore/_matrix.h | 12 + src/xrCore/log.h | 1 + src/xrCore/svp_log.cpp | 45 + src/xrCore/svp_log.h | 8 + src/xrCore/xrCore.vcxproj | 2 + src/xrCore/xrCore.vcxproj.filters | 6 + src/xrCore/xr_ini.h | 3 + src/xrEngine/CameraManager.cpp | 59 +- src/xrEngine/CameraManager.h | 1 + src/xrEngine/XR_IOConsole.cpp | 2 + src/xrEngine/bone.h | 4 +- src/xrEngine/device.cpp | 33 +- src/xrEngine/device.h | 84 +- src/xrEngine/imgui_base.cpp | 5 + src/xrEngine/svp_crash_context.cpp | 31 + src/xrEngine/svp_crash_context.h | 23 + src/xrEngine/svp_gameplay_cvars.cpp | 35 + src/xrEngine/svp_gameplay_cvars.h | 27 + src/xrEngine/svp_state.h | 122 ++ src/xrEngine/x_ray.cpp | 3 + src/xrEngine/xrEngine.vcxproj | 5 + src/xrEngine/xrEngine.vcxproj.filters | 15 + src/xrEngine/xr_ioc_cmd.cpp | 2 + src/xrGame/Actor.cpp | 14 +- src/xrGame/Actor.h | 2 +- src/xrGame/ActorCameras.cpp | 9 + src/xrGame/ActorEffector.cpp | 9 +- src/xrGame/ActorInput.cpp | 12 +- src/xrGame/Actor_Weapon.cpp | 4 + src/xrGame/HudItem.cpp | 21 +- src/xrGame/Level.cpp | 3 +- src/xrGame/ParticlesObject.cpp | 10 + src/xrGame/ParticlesObject.h | 1 + src/xrGame/ShootingObject.cpp | 5 + src/xrGame/UIGameCustom.cpp | 43 +- src/xrGame/UIGameCustom.h | 3 + src/xrGame/Weapon.cpp | 256 +++- src/xrGame/Weapon.h | 34 +- src/xrGame/WeaponAK74.cpp | 11 +- src/xrGame/WeaponBinoculars.cpp | 49 +- src/xrGame/WeaponBinoculars.h | 4 + src/xrGame/WeaponBinocularsVision.cpp | 50 +- src/xrGame/console_registrator_script.cpp | 50 + src/xrGame/level_script.cpp | 16 + src/xrGame/svp_mags.cpp | 102 ++ src/xrGame/svp_mags.h | 19 + src/xrGame/svp_weapon.cpp | 243 +++ src/xrGame/xrGame.vcxproj | 3 + src/xrGame/xrGame.vcxproj.filters | 9 + 151 files changed, 9846 insertions(+), 501 deletions(-) create mode 100644 gamedata/configs/mod_system_pip_tonemap.ltx create mode 100644 gamedata/configs/pip_objective_mm.ltx create mode 100644 gamedata/scripts/options_modded_exes_pip.script create mode 100644 gamedata/scripts/zzz_extra_scope_features.script create mode 100644 gamedata/shaders/r3/scope_color_write.s create mode 100644 gamedata/shaders/r3/scope_custom_depth.h create mode 100644 gamedata/shaders/r3/scope_debug.ps create mode 100644 gamedata/shaders/r3/scope_debug.s create mode 100644 gamedata/shaders/r3/scope_defines.h create mode 100644 gamedata/shaders/r3/scope_depth_write.ps create mode 100644 gamedata/shaders/r3/scope_depth_write.s create mode 100644 gamedata/shaders/r3/scope_vertex.vs create mode 100644 gamedata/shaders/r3/svp_distort_stamp.ps create mode 100644 gamedata/shaders/r3/svp_distort_stamp.s create mode 100644 gamedata/shaders/r3/svp_hooks_common.h create mode 100644 gamedata/shaders/r3/svp_hooks_image.h create mode 100644 gamedata/shaders/r3/svp_hooks_lens.h create mode 100644 gamedata/shaders/r3/svp_hooks_reticle.h create mode 100644 gamedata/shaders/r3/svp_hooks_shadow.h create mode 100644 gamedata/shaders/r3/svp_nearblur.ps create mode 100644 gamedata/shaders/r3/svp_nearblur.s create mode 100644 gamedata/shaders/r3/svp_taa_stamp.ps create mode 100644 gamedata/shaders/r3/svp_taa_stamp.s create mode 100644 gamedata/textures/fx/svp_rain.dds create mode 100644 src/Layers/xrRender/Debug/renderdoc_app.h create mode 100644 src/Layers/xrRender/svp_console.cpp create mode 100644 src/Layers/xrRender/svp_console.h create mode 100644 src/Layers/xrRender/svp_constants.cpp create mode 100644 src/Layers/xrRender/svp_constants.h create mode 100644 src/Layers/xrRender/svp_dsgraph_cull.cpp create mode 100644 src/Layers/xrRender/svp_hud_latch.cpp create mode 100644 src/Layers/xrRender/svp_lens_detect.cpp create mode 100644 src/Layers/xrRender/svp_lens_detect.h create mode 100644 src/Layers/xrRender/svp_lens_detect_math.cpp create mode 100644 src/Layers/xrRender/svp_lens_detect_math.h create mode 100644 src/Layers/xrRenderPC_R4/SvpDlss.h create mode 100644 src/Layers/xrRenderPC_R4/svp_camera.cpp create mode 100644 src/Layers/xrRenderPC_R4/svp_camera.h create mode 100644 src/Layers/xrRenderPC_R4/svp_optics.cpp create mode 100644 src/Layers/xrRenderPC_R4/svp_stats.cpp create mode 100644 src/Layers/xrRenderPC_R4/svp_stats.h create mode 100644 src/Layers/xrRenderPC_R4/svp_target.cpp create mode 100644 src/Layers/xrRenderPC_R4/svp_target.h create mode 100644 src/xrCore/svp_log.cpp create mode 100644 src/xrCore/svp_log.h create mode 100644 src/xrEngine/svp_crash_context.cpp create mode 100644 src/xrEngine/svp_crash_context.h create mode 100644 src/xrEngine/svp_gameplay_cvars.cpp create mode 100644 src/xrEngine/svp_gameplay_cvars.h create mode 100644 src/xrEngine/svp_state.h create mode 100644 src/xrGame/svp_mags.cpp create mode 100644 src/xrGame/svp_mags.h create mode 100644 src/xrGame/svp_weapon.cpp 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..d0e77e2b4e --- /dev/null +++ b/gamedata/shaders/r3/scope_custom_depth.h @@ -0,0 +1,26 @@ +/* + ===================================================================== + 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; +} \ 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 0000000000000000000000000000000000000000..5384c3d0fefc1d13b37b62e12546daf1c8b0c2e3 GIT binary patch literal 1091 zcmaJGsifs~#) z<2P^Kyu4FYi}j|Ia8j$Mm1kS!R8!N{2|3-rh(_zKN0xHuH&&*S6dlR;4fH-XVqg9^ck)p$>AUAMv<0lvzA3*MLXh_Fan}l^%-r8ynr7!kkIrZRH}m53%Y|_n9~H_-cTQX!RnkQL1(F);I_>1 z?|y~F4c)dFQ8UO88n}8pdOGX^8lgw8qt;pyLHJ#TjbVS<4g@t}e?l)WaXx(eF@~v~ zbdw!6%K{c1I*yG)&W=4#;XFz3jBxUYBD@Z1zbYEN>4>6lutCw$=TC}0?zJ^dUI?XKTOcJnpiF5$mIM7LriS+C1RO|83n+_y7O^ literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..1d4cea4421ef6922180f20d2aa9747379b74b82e GIT binary patch literal 1026 zcmaJ=U2EGg6n(CK#ifOjKu8?crr%f?j*m!?!L7Ny$nm7MM_ z8Jo@Ei_*_rS5RTuaE(wqSz4i_fm&&JLTgb2YE3_TG=^`{u)0y)!ja7xtM)~Ot=)~f z!Zqw#Wyay(!UO2L{7qGXRbZU1d5P2o(fExWmsNw%YAl7Jc0d^8NcyV6T1bR=lO=cQ z8iHvM1oOJHlGN~N3Bd%OwXg^rOiQP0Q9><#qPIxo-ZOwR^zRKCGAQI0BytU;2{o-6 zWrf;rSHf5>OJq(8g70Byz2hqmym1&ZW~_Flg-w<^+d9jBG1BSr*RU-p65(@&1!Uif zoa-&xAUwxZqOPek7-HssU=G-*QrMRv=3~e%`o{YFI*G`;4T0?DX_PKtMUigNXG{8B z@vBBo(~W;P-mo{a@ZLM@xsannp`3+eJalp#&=R`Fy}|w#v^!3He}HV2ZsKBddy~b* z>M_gH`yz@r`7M0@*L6vz-Ay@iE&jvRk$ith^7zCwNdH986HZ^d%-F@r4;i76^hx&L z(>|$XZ-~5mFUQhgSj*!8N5LeRbVnw^IP_S`RmtWBO8;p1laH#apA(&?bw)Iw%;|ZY YDV|1u1`JL>5AIHAv`))ot08;)4LWQ`i2wiq literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..ce0ea7feb4aae6f1db5b321010a9fcb63e0ac08e GIT binary patch literal 339 zcmZXOK}*9x5QWd~uXxl;3X;TPDd=7BtauIE?6g_T?u41y77_ouA@(5l_~w1{-fJ41 zB^oJgAcUyKVn|*;kg1sX5z33#bK1vDID4586){k@XrJH{$wG{j)w$c4`>_PVN_(ED zoBtB$dRRHvG 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..9d3c474a2d --- /dev/null +++ b/gamedata/shaders/r3/svp_hooks_shadow.h @@ -0,0 +1,30 @@ +// 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; +} + +#endif diff --git a/gamedata/shaders/r3/svp_nearblur.ps b/gamedata/shaders/r3/svp_nearblur.ps new file mode 100644 index 0000000000000000000000000000000000000000..5dd3d999d9929ff5270d92cd751166eb1ab6798f GIT binary patch literal 2649 zcmai0e@`Pf5dAy*DW-Q)yMZJmy9Wp5Xhi{CktiS%om5qAc1bPQFL%X zIWJT;Go&QhM^3{}_hd3va}pJs@(xW^nRD4?a$0gSwa~1jiF)FXG-WuTi4_)qKhBJ0 zg)vIHr17Lw)81Sy*;L7%;q4C1q$rfG$grwLCge22t~$6>NGh?SOy~?>R6&`N#mopm zViPs99$3wAbIF$E;YM$T%*t8L=}o4p3YR>-iJ}o-+L`9bC7H3DjC}>CFL!HYPNQf8 zrzjP(@4n(^?5?WJDWQ<&I;uLf@=&`T>J!z=J?yQi0wT?F0S4=3hZa=L)@xUv03R~r zRDE+*nwxONIQ4g=m%|~|Tz3QSIkU{&c190Zqw9;CtMTaf`>PHeW2Y#YJ(vgy#Y(2B zW|Egt^cETl$?5i&@yENH%d6Y*`R&J>D@uCl$;ru4nxsV(`9cY-}!L;ZGdeHyhi2j7z7HGQCdub&ic`k4lO z{Pg+-{YYzH;7l&Gu$D`jPG&^`x5VaY)rQP7X$>($1=G=&9gIe-d4Ndkw$s6Rw_6Ba z=5(PhHjKkLH5AgS@)l8(zjR?o#|Ds$!T^hKk4Z$*1f z)_CA*IPm{mqvBrw5ao5L*?#RL4B!fHgBf6U?c{J%z|CcjT7#JvoYhMH0ALH*IL1R2 za!P6rf)wObnPsS@X2z1RQOhCT1LR66`)Dk${L zikhtQ;X^Mb5x`pG-69kn(h1|i~5hjS;&-;fC( zKe+N=vAOGC)cM3YP{OLsY-gd2!C{{)s|_t4nZ*#$J|;}M4(!M^vl!5Z{&fxV@K=IP ziIY79z#^>Vz!&7)?f{1CDpyR$p9yXDfP<-@7alecxlve)B`*mCab0qBLEfFZyJ3y4 zR05J}Wjz2qB$?8hV=#j9ojOb)K0H;LGuM2E!r{^RIU%XW17|ik#Z&@ep;;CmCcTs6 zmQj5Aqg1N?6Ez@3GcfT|BgoC6)6Jax5cr0# z+{X{}&hvfhSNmr#-wjCj8Se7(C1l;0gbnvY!zY**CU$A|od$T=4d%l0+WGXWzQ#Hr z2inFG;lBXibhynNYhT5@Xz`tcKGb#5&K_E0cL*R?D|~IzWF_9T=78G@)4J0f@#fQ{m#T~LsXTNG6IH`670t|V=CFB+NucM@5QV?3{Z616b&ojV{>$#o6x z-3m{OvYXp{UNn@y(b)#Ejc-~FTI_5+gTq@nzPLBVCp>c*QFQYxG!w;t?1XXSo`U}X DOGR1T literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..0705602162f4be7816e8c0b4bbc97d04e5e5fbcc GIT binary patch literal 307 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≺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

)3c19LAu_47}figBsb*A_;LK3cVc^x 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 gvDwyXvaV>-Q%YNOl~Nu{so=BL; // 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..195ce05d52 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 the sss aim dof reads the scope content depth and smears the whole frame, off while scoped +static bool svp_scoped_now() +{ + return 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_scoped_now()) + { + 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_scoped_now()) + { + 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..a57874b723 --- /dev/null +++ b/src/Layers/xrRender/svp_console.cpp @@ -0,0 +1,256 @@ +#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) +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_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_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_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_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_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..47c36d0e08 --- /dev/null +++ b/src/Layers/xrRender/svp_console.h @@ -0,0 +1,131 @@ +#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_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..1df688d5b8 --- /dev/null +++ b/src/Layers/xrRender/svp_constants.cpp @@ -0,0 +1,185 @@ +#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; + +// 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 cur = r * g_pip_scope_magnification; + float mn = cur - (zoom_ratio - 1.0f) * 2.5f; // (cur - mn) * 0.4 + 1 == zoom_ratio + if (mn < 0.01f) mn = 0.01f; // floor above zero, the shader divides by minMag + const float mx = 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 }, +}; + +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..3519ae006b --- /dev/null +++ b/src/Layers/xrRender/svp_hud_latch.cpp @@ -0,0 +1,273 @@ +#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; + 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; + } + if (diag) + { + auto tx = V->GetTexture(); + PipMsg("[SVP-HUD] %s t=%.2f rad=%.1fcm R=%.1fcm %s", drop ? "SKIP" : "keep", + t, rad * 100.f, V->vis.sphere.R * 100.f, tx ? tx->cName.c_str() : "?"); + } + if (drop && skip_ok) 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..d8c4033297 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,156 @@ 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); } // 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 +827,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 +987,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 +1007,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 +1027,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 +1052,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 +1078,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 +1118,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..16167c0860 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 @@ -163,6 +177,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 +244,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 +421,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 +463,11 @@ 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 + 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 +515,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..32d5047f0d --- /dev/null +++ b/src/Layers/xrRenderPC_R4/svp_camera.cpp @@ -0,0 +1,1308 @@ +#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) + { + 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 config bounds ride the 75 base and rescale to the aim fov, the dynamic min + // already derives from the aim fov and passes through + const float fscale = rad2deg(fov_aim) / 75.f; + const float yscale = (_abs(fovp.y - fovp.x) < 0.01f) ? fscale : 1.f; + if (flat_window) + { + 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) + 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); + } + } + 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 (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; + PipMsg("[SVP-BARREL] sway=%.2fdeg fovmatch=%d out=(%.0f,%.0f) in=(%.0f,%.0f) delta=(%.0f,%.0f)px", + sway, ps_r__svp_hud_fov_match, px_out.x, px_out.y, bx, by, bx - px_out.x, by - px_out.y); + } + } + } + } + + // 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 + Msg("[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) { Msg("[SVP-CFG] %s", line); line[0] = 0; grp = 0; } + } + if (line[0]) Msg("[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); + Msg("[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) + { + Msg("[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(); + Msg("[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, "*"); + Msg("[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")) + { + Msg("[SVP-FILES] mounted mods archive %s", A.path.c_str()); + ++n_arch; + } + Msg("[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_optics.cpp b/src/Layers/xrRenderPC_R4/svp_optics.cpp new file mode 100644 index 0000000000..624e1999fd --- /dev/null +++ b/src/Layers/xrRenderPC_R4/svp_optics.cpp @@ -0,0 +1,991 @@ +#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 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) + { + { + ref_texture t; t.create("$user$svp_nearblur_src"); + t->surface_set(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); + // 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); + RCache.set_c("svp_nearblur_params", _max(ps_r__svp_focus_m, 1.f), k, h * 0.06f, md); + } + RCache.Render(D3DPT_TRIANGLELIST, Offset, 0, 3, 0, 1); + 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 = free, 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; + RCache.set_c("svp_glass3", ps_r__svp_sharpen, 0.f, 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..92dc956ae9 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,10 @@ + + + + diff --git a/src/Layers/xrRenderPC_R4/xrRender_R4.vcxproj.filters b/src/Layers/xrRenderPC_R4/xrRender_R4.vcxproj.filters index 5a751ca2f2..0e68fee13a 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,18 @@ Core + + Core + + + Core_Target + + + Core + + + Core + Core @@ -831,6 +867,18 @@ Core + + Core + + + Core + + + Core + + + Core + Core @@ -1209,6 +1257,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..bc27afc3ec --- /dev/null +++ b/src/xrEngine/svp_state.h @@ -0,0 +1,122 @@ +#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 + + // 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..4c52bfcbca --- /dev/null +++ b/src/xrGame/svp_weapon.cpp @@ -0,0 +1,243 @@ +#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; + } + + // 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 From b891b940022695495682d30bdbd60433e7af5aa5 Mon Sep 17 00:00:00 2001 From: TheLostInPlace <42595640+TheLostInPlace@users.noreply.github.com> Date: Mon, 20 Jul 2026 00:12:56 -0500 Subject: [PATCH 2/4] pip: near blur on the physical coc, barrel seam switches and the field stop the near field blur moves into its own module and rides the real circle of confusion at the svp near plane, samples a mip prefiltered source and advances geometrically so thin blur bands are not skipped. r__svp_near_pupil and r__svp_drain_clip sit default off for barrel seam testing and the ocular field stop derives its rim band from the exit pupil geometry --- gamedata/shaders/r3/svp_hooks_common.h | 2 +- gamedata/shaders/r3/svp_hooks_shadow.h | 17 +++ gamedata/shaders/r3/svp_nearblur.ps | Bin 2649 -> 3863 bytes src/Layers/xrRender/svp_console.cpp | 6 + src/Layers/xrRender/svp_constants.cpp | 11 ++ src/Layers/xrRender/svp_hud_latch.cpp | 11 +- src/Layers/xrRenderPC_R4/r4_rendertarget.cpp | 14 +++ src/Layers/xrRenderPC_R4/r4_rendertarget.h | 3 + src/Layers/xrRenderPC_R4/svp_camera.cpp | 30 ++++- src/Layers/xrRenderPC_R4/svp_nearblur.cpp | 112 ++++++++++++++++++ src/Layers/xrRenderPC_R4/svp_optics.cpp | 77 ++++-------- src/Layers/xrRenderPC_R4/xrRender_R4.vcxproj | 1 + .../xrRenderPC_R4/xrRender_R4.vcxproj.filters | 3 + 13 files changed, 225 insertions(+), 62 deletions(-) create mode 100644 src/Layers/xrRenderPC_R4/svp_nearblur.cpp diff --git a/gamedata/shaders/r3/svp_hooks_common.h b/gamedata/shaders/r3/svp_hooks_common.h index d88ebdb399..07013c079d 100644 --- a/gamedata/shaders/r3/svp_hooks_common.h +++ b/gamedata/shaders/r3/svp_hooks_common.h @@ -31,7 +31,7 @@ uniform float4 svp_glass2; // engine glass optics 2, x = coating, y = heat mirag #endif #ifndef SVP_GLASS3_DECLARED #define SVP_GLASS3_DECLARED -uniform float4 svp_glass3; // engine glass optics 3, x = sharpen amount, y = free, z = sharpen radial falloff, w = sharpen inner crisp radius +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 diff --git a/gamedata/shaders/r3/svp_hooks_shadow.h b/gamedata/shaders/r3/svp_hooks_shadow.h index 9d3c474a2d..26ad16ccf3 100644 --- a/gamedata/shaders/r3/svp_hooks_shadow.h +++ b/gamedata/shaders/r3/svp_hooks_shadow.h @@ -27,4 +27,21 @@ float4 svp_shadow_soften(float4 shadow) 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 index 5dd3d999d9929ff5270d92cd751166eb1ab6798f..14e36fd2fa4864340a40697bf9ed1ca34313977a 100644 GIT binary patch delta 1490 zcmZWp&1xJ+5Jpb0JbN<4H$$ipaAuv^A893v;3&rWK@f~=jC@{tXL@EE_e>AnJ<^Jh z?8R@ehde-Dz~GagAy1G;$jNzxeATnFvXX^edaJ6dzWVB`{_DZ-U%&rn>rGRs+}M(G zZG_(=cUey>DeO!)c3PTDKBdu!7UWXly|lz8ipNw4KbJP4osCDNX;U^@cw_skPg{>3 zew)c$Rg%t+rWem&O^@E3oSk0K&i;5f{_xeEhhHuWX-es>O3A9Kpu(CfKc~6TnImK; zFKS9nQ+Z-U!8B{KLV5yX64f6TM>1(=D0HjS3mtyg{--`lCWXE(!JHv7Qw6hd2$VDBm zLqm9ixIO_&_`O9!p9Z*pXPmD--u<)$+|*P6i$)7HFROv53P7^P3$IK?xiuxzc=qze z$?5s@`T5z4(*y`5fpDrZ7N*L|T8k9DrN)h%s(MjK+fH_OvCA1=zq|m{&;~5=U_3ln zP7Wqd@`P}HaYXf!%L3QD54Qq~x|$;9<{4xF2}y=Z2P-stP_|Jocb=qG^PU;VK?xjUg;*yGjv z?FR)87smf*NRyzVS5`7-YYrwc#26axem8xF=wow

$)^UrP>FJ6`?0ZN2~-AAY-c zf9o4y&R~WFc6t7BL60~3WU=&D@1kc*1oelum?2-Ja*Ee1L0i!;__B6`*z17Pt6D3W zk-5Z-1vf~xsVd$U0h1^sOCJtnC1l$`;F)qM6aa6#L|4Y@j5CL7OHtNZp2!-y{_&7f zSz)QeUq;YEh#0_{r|-dfxN>YXcJ48i3_J=UOb-05$ES62^7WQQ5$EUcx&*jd{TYpyk;j}ri{p@1pO|vo3LaWhHe_y} z3y!}T4l3f(?NK0myXrwxES9d74mjh)7A=LJ&~X^p;pg%i5i5Z0APJA89PH%*LHmFR qq;$eJR(RP(;ib6!MsNu$5rq8@Z*$y{n6xDm68Erot+pQZCjSCxsL$L0 delta 298 zcmbO(cT;4;2gb>6OsbnZm?T;1vr|(G6iPBu6^aWoixP7b5{vRn^HLNHO$`)E5(|nI z$}o)>3gU~C6LV5E zxj55u@)JuG;35jPAe9;hdZuYQU_A<2K#Hd#2&mK8z(CI+O>^=>_V<&oa_R|#bO9{_ zYJqE?+`^Tw1~$@2BRRiVBQY;sL0h3HzeH0shader._get()); bool drop = false; + bool clip_obj = false; float t = 0.f, rad = -1.f; if (measure_ok && item.pMatrix) { @@ -243,14 +244,20 @@ void CDSGraphManager::r_dsgraph_render_hud_svp() 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 ? "SKIP" : "keep", + 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) continue; + 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); diff --git a/src/Layers/xrRenderPC_R4/r4_rendertarget.cpp b/src/Layers/xrRenderPC_R4/r4_rendertarget.cpp index d8c4033297..d5fad07400 100644 --- a/src/Layers/xrRenderPC_R4/r4_rendertarget.cpp +++ b/src/Layers/xrRenderPC_R4/r4_rendertarget.cpp @@ -674,6 +674,20 @@ CRenderTarget::CRenderTarget(LPCSTR name, u32 width, u32 height) 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] = createUnique(r4_RT_HDR10_halfres0, w/2, h/2, D3DFMT_A16B16G16R16F); diff --git a/src/Layers/xrRenderPC_R4/r4_rendertarget.h b/src/Layers/xrRenderPC_R4/r4_rendertarget.h index 16167c0860..a4cb30eb1d 100644 --- a/src/Layers/xrRenderPC_R4/r4_rendertarget.h +++ b/src/Layers/xrRenderPC_R4/r4_rendertarget.h @@ -139,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 @@ -464,6 +466,7 @@ class CRenderTarget : public IRender_Target 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 diff --git a/src/Layers/xrRenderPC_R4/svp_camera.cpp b/src/Layers/xrRenderPC_R4/svp_camera.cpp index 32d5047f0d..c82a630f15 100644 --- a/src/Layers/xrRenderPC_R4/svp_camera.cpp +++ b/src/Layers/xrRenderPC_R4/svp_camera.cpp @@ -501,6 +501,11 @@ void svpCamera() 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; @@ -663,7 +668,7 @@ void svpCamera() 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 (c.w < EPS) return false; + 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; }; @@ -690,8 +695,27 @@ void svpCamera() { 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; - PipMsg("[SVP-BARREL] sway=%.2fdeg fovmatch=%d out=(%.0f,%.0f) in=(%.0f,%.0f) delta=(%.0f,%.0f)px", - sway, ps_r__svp_hud_fov_match, px_out.x, px_out.y, bx, by, bx - px_out.x, by - px_out.y); + // 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); } } } 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 index 624e1999fd..ba66939f8f 100644 --- a/src/Layers/xrRenderPC_R4/svp_optics.cpp +++ b/src/Layers/xrRenderPC_R4/svp_optics.cpp @@ -183,60 +183,8 @@ void CRenderTarget::phase_svp_capture() EvalSVP_DLSS(in); return; } - // 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) - { - { - ref_texture t; t.create("$user$svp_nearblur_src"); - t->surface_set(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); - // 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); - RCache.set_c("svp_nearblur_params", _max(ps_r__svp_focus_m, 1.f), k, h * 0.06f, md); - } - RCache.Render(D3DPT_TRIANGLELIST, Offset, 0, 3, 0, 1); - 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); @@ -503,9 +451,26 @@ void CRenderTarget::draw_scope(ref_shader se, std::function bind) / 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 = free, z = sharpen radial falloff, w = sharpen inner crisp radius + // 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; - RCache.set_c("svp_glass3", ps_r__svp_sharpen, 0.f, 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, diff --git a/src/Layers/xrRenderPC_R4/xrRender_R4.vcxproj b/src/Layers/xrRenderPC_R4/xrRender_R4.vcxproj index 92dc956ae9..bb7ceb3459 100644 --- a/src/Layers/xrRenderPC_R4/xrRender_R4.vcxproj +++ b/src/Layers/xrRenderPC_R4/xrRender_R4.vcxproj @@ -777,6 +777,7 @@ + diff --git a/src/Layers/xrRenderPC_R4/xrRender_R4.vcxproj.filters b/src/Layers/xrRenderPC_R4/xrRender_R4.vcxproj.filters index 0e68fee13a..0455282b69 100644 --- a/src/Layers/xrRenderPC_R4/xrRender_R4.vcxproj.filters +++ b/src/Layers/xrRenderPC_R4/xrRender_R4.vcxproj.filters @@ -846,6 +846,9 @@ Core_Target + + Core_Target + Core From 57c00eca0a0c9e10fa08fa9a54843eac42ab018e Mon Sep 17 00:00:00 2001 From: TheLostInPlace <42595640+TheLostInPlace@users.noreply.github.com> Date: Mon, 20 Jul 2026 00:13:10 -0500 Subject: [PATCH 3/4] pip: sss weapon dof runs while scoped, the far stamp holds only the glass disc the wpn dof constant zero gate now rides r__svp_wpn_dof default 1 so the sss aim blur reaches the gun again. the custom depth stamp clips to the glass disc so the housing keeps real gbuffer depth and blurs with the hands, the magnified image stays far and sharp --- gamedata/shaders/r3/scope_custom_depth.h | 8 ++++++++ gamedata/shaders/r3/scope_depth_write.ps | Bin 1026 -> 1084 bytes .../Blender_Recorder_StandartBinding.cpp | 10 +++++----- src/Layers/xrRender/svp_console.cpp | 2 ++ src/Layers/xrRender/svp_console.h | 1 + 5 files changed, 16 insertions(+), 5 deletions(-) diff --git a/gamedata/shaders/r3/scope_custom_depth.h b/gamedata/shaders/r3/scope_custom_depth.h index d0e77e2b4e..dfdf26a7db 100644 --- a/gamedata/shaders/r3/scope_custom_depth.h +++ b/gamedata/shaders/r3/scope_custom_depth.h @@ -23,4 +23,12 @@ float scope_custom_depth(float4 hpos) { 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_depth_write.ps b/gamedata/shaders/r3/scope_depth_write.ps index 1d4cea4421ef6922180f20d2aa9747379b74b82e..a4bf7c8390d298040b1e27e58f25ca06cdfd3156 100644 GIT binary patch delta 52 zcmZqT*uybl?!+<`9vy|WoczQRBZZP=gN?Jam;`hbJoSKVO)dp6u$tV+B)$0slN2KW Dt%VNN delta 15 XcmdnP(Zn%f?#2u1Oq>5P*)akDG@k|- diff --git a/src/Layers/xrRender/Blender_Recorder_StandartBinding.cpp b/src/Layers/xrRender/Blender_Recorder_StandartBinding.cpp index 195ce05d52..4a4d2c38a8 100644 --- a/src/Layers/xrRender/Blender_Recorder_StandartBinding.cpp +++ b/src/Layers/xrRender/Blender_Recorder_StandartBinding.cpp @@ -925,17 +925,17 @@ static class dev_param_8 : public R_constant_setup } } dev_param_8; -// pip the sss aim dof reads the scope content depth and smears the whole frame, off while scoped -static bool svp_scoped_now() +// pip sss weapon dof while scoped rides r__svp_wpn_dof, 0 zeroes the constants +static bool svp_wpn_dof_off() { - return Device.true_pip_on && Device.m_SecondViewport.IsSVPActive(); + 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_scoped_now()) + if (svp_wpn_dof_off()) { RCache.set_c(C, 0.f, 0.f, 0.f, 0.f); return; @@ -948,7 +948,7 @@ static class ssfx_wpn_dof_2 : public R_constant_setup { virtual void setup(R_constant* C) { - if (svp_scoped_now()) + if (svp_wpn_dof_off()) { RCache.set_c(C, 0.f, 0, 0, 0); return; diff --git a/src/Layers/xrRender/svp_console.cpp b/src/Layers/xrRender/svp_console.cpp index f0ac8c0a0c..a18424d38f 100644 --- a/src/Layers/xrRender/svp_console.cpp +++ b/src/Layers/xrRender/svp_console.cpp @@ -113,6 +113,7 @@ int ps_r__svp_skip_motionblur = 0; // svp skip motion blur on the scope pass, ma 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 @@ -200,6 +201,7 @@ void svp_console_init() 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) diff --git a/src/Layers/xrRender/svp_console.h b/src/Layers/xrRender/svp_console.h index 47c36d0e08..1d6c70b48e 100644 --- a/src/Layers/xrRender/svp_console.h +++ b/src/Layers/xrRender/svp_console.h @@ -57,6 +57,7 @@ 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; From b2e874b0f1cd9429493a9c7452847417e29a06e7 Mon Sep 17 00:00:00 2001 From: TheLostInPlace <42595640+TheLostInPlace@users.noreply.github.com> Date: Mon, 20 Jul 2026 00:13:10 -0500 Subject: [PATCH 4/4] pip: reticle scale holds at any fov, authored flat optic zoom, one tester log authored and detent base scope mins rescale to the aim fov like the max bound and the scope params floor scales the whole encoding by one factor, so ffp reticle size no longer moves with either fov slider. flat window optics with authored magnifications zoom with the click and the cfg and files report prints to pip.log for a single tester file --- src/Layers/xrRender/svp_constants.cpp | 12 ++++--- src/Layers/xrRenderPC_R4/svp_camera.cpp | 45 ++++++++++++++++--------- src/xrEngine/svp_state.h | 2 ++ src/xrGame/svp_weapon.cpp | 4 +++ 4 files changed, 43 insertions(+), 20 deletions(-) diff --git a/src/Layers/xrRender/svp_constants.cpp b/src/Layers/xrRender/svp_constants.cpp index 2ab5a5a67c..43edb02b9e 100644 --- a/src/Layers/xrRender/svp_constants.cpp +++ b/src/Layers/xrRender/svp_constants.cpp @@ -117,10 +117,14 @@ static class shader_scope_params : public R_constant_setup 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 cur = r * g_pip_scope_magnification; - float mn = cur - (zoom_ratio - 1.0f) * 2.5f; // (cur - mn) * 0.4 + 1 == zoom_ratio - if (mn < 0.01f) mn = 0.01f; // floor above zero, the shader divides by minMag - const float mx = r * ((g_pip_scope_max_mag > 0.01f) ? g_pip_scope_max_mag : g_pip_scope_magnification); + 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); diff --git a/src/Layers/xrRenderPC_R4/svp_camera.cpp b/src/Layers/xrRenderPC_R4/svp_camera.cpp index c82a630f15..ed06fde8c5 100644 --- a/src/Layers/xrRenderPC_R4/svp_camera.cpp +++ b/src/Layers/xrRenderPC_R4/svp_camera.cpp @@ -353,8 +353,10 @@ void svpCamera() }; if (flat_window) { - scope_magnification = fov_aim / window_fov(g_pGamePersistent->m_pGShaderConstants->hud_params.y > 1.f - ? g_pGamePersistent->m_pGShaderConstants->hud_params.y : svp_fov); + // 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 @@ -444,11 +446,11 @@ void svpCamera() 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 config bounds ride the 75 base and rescale to the aim fov, the dynamic min - // already derives from the aim fov and passes through + // 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) ? fscale : 1.f; - if (flat_window) + 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; @@ -464,7 +466,18 @@ void svpCamera() 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) - vFov = fov / scope_magnification; + { + 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 @@ -771,7 +784,7 @@ void svpCamera() s_cfg_logged = true; ps_r__svp_report = 0; // build fingerprint header first, testers diff their log against this - Msg("[SVP-CFG] build %s mode=%d", __DATE__, scope_svp_enabled); + 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) @@ -785,9 +798,9 @@ void svpCamera() 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) { Msg("[SVP-CFG] %s", line); line[0] = 0; grp = 0; } + if (++grp == 8) { PipMsg("[SVP-CFG] %s", line); line[0] = 0; grp = 0; } } - if (line[0]) Msg("[SVP-CFG] %s", line); + 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) { @@ -795,7 +808,7 @@ void svpCamera() 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); - Msg("[SVP-CFG] aa ssfx_taa=%s r2_aa=%s r2_aa_kernel=%s", a_taa, a_aa, 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 @@ -825,7 +838,7 @@ void svpCamera() const CLocatorAPI::file* f = FS.exist("$game_shaders$", rel); if (!f) { - Msg("[SVP-FILES] %-32s MISSING", fn); + PipMsg("[SVP-FILES] %-32s MISSING", fn); continue; } u32 crc = 0; @@ -853,23 +866,23 @@ void svpCamera() const char* src = "loose"; if (f->vfs != 0xffffffff && f->vfs < FS.m_archives.size()) src = FS.m_archives[f->vfs].path.c_str(); - Msg("[SVP-FILES] %-32s %6u bytes crc %08x ver[%s] %s", fn, f->size_real, crc, ver, src); + 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, "*"); - Msg("[SVP-FILES] dispatcher precompiled blobs %u, rs_precompiled_shaders %d", + 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")) { - Msg("[SVP-FILES] mounted mods archive %s", A.path.c_str()); + PipMsg("[SVP-FILES] mounted mods archive %s", A.path.c_str()); ++n_arch; } - Msg("[SVP-FILES] mods archives mounted %u", n_arch); + PipMsg("[SVP-FILES] mods archives mounted %u", n_arch); } } } diff --git a/src/xrEngine/svp_state.h b/src/xrEngine/svp_state.h index bc27afc3ec..67c26201df 100644 --- a/src/xrEngine/svp_state.h +++ b/src/xrEngine/svp_state.h @@ -77,6 +77,8 @@ class ENGINE_API CSecondVPParams //--#SM+#-- +SecondVP+ 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 = {}; diff --git a/src/xrGame/svp_weapon.cpp b/src/xrGame/svp_weapon.cpp index 4c52bfcbca..027ac764f0 100644 --- a/src/xrGame/svp_weapon.cpp +++ b/src/xrGame/svp_weapon.cpp @@ -168,6 +168,10 @@ void CWeapon::UpdateSecondVP() // 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: