Compare commits

...
Author SHA1 Message Date
BMagnu 4a35713992 FSR Support (#8019)
* FSR4 libs

* Motion Vectors

* FSR AA

* FSR upscaling

* Cleanup

* Clang Tidy

* Address Feedback

* Improve canonical_prev handling

* Clang Tidy again

* Clean up prev_ submodel motion handling

* Address Feedback
2026-11-13 10:16:24 +09:00
dustinspace217andDustinspace217 059a8715d8 P2: Fix isWeaponAllowedOnShip secondary bank lookup (#8033)
* Fix isWeaponAllowedOnShip bank lookup for secondary banks

isWeaponAllowedOnShip numbers banks per ship class, with the secondaries
right after the primaries, but the bank restriction lists keep secondary
bank n at MAX_SHIP_PRIMARY_BANKS + n. On a ship with fewer than three
primary banks, the secondary banks were checked against the wrong list.
Map the bank before the lookup.

The restriction lists are only allocated for ship classes that have
$Allowed PBanks/SBanks, so both this function and the loadout weapon
setter read past the end of an empty vector for any other class. Skip
the bank check when there are no lists.

The loadout weapon setter also treated a primary slot the ship does not
have (bank 3 on a two-primary ship) as a secondary bank.

Fixes #7985

* Cover more bank layouts and the loadout setter in the shipclass tests

Add classes with one and three primary banks, a restricted primary bank,
the bank-omitted case, and tests for the loadout weapon setter's bank
range and restriction checks.

* Fall back to the class weapon list in loadout bank checks

The weapon loadout's slot and pool moves read a ship class's per-bank
restriction lists without checking they exist. Those lists are only
allocated for classes with $Allowed PBanks/SBanks, so a move request for
any other class (from a script through sendWeaponRequestPacket, or a
multiplayer client) read past the end of an empty vector and crashed.

When a class has no per-bank lists, check the weapon against the
class-wide allowed weapons instead. Classes with lists behave as before.

* Simplify the loadout bank checks and tidy the loadout setter

wl_bank_allows_weapon already allows a bank with no restriction, so the
four loadout checks call it directly. The comment now says what the
empty case means: a class without bank lists allows no weapons.

The Loadout_Weapon setter rejects a weapon index one past the end, and
only resets the amount when it actually stored the weapon.

* Check the class weapon list in the loadout bank check

A bank without its own list accepted any weapon of the right type, so a
move request from a script or a multiplayer client could load a weapon
the ship class can't carry. Check the class-wide list first, as the
rearm code does. An empty slot coming back from a swap has nothing to
check, which also stops it adding a -1 entry to the weapon pool.

---------

Co-authored-by: Dustinspace217 <[email protected]>
2026-11-12 17:03:34 -05:00
Fabian WoltermannandClaude Opus 5.5 7f3e5fae60 Softer shadows in raytracing and RTAO (#7695)
* Add RTAO and unify raytraced/shadow-mapped shadow softness on sun angular size

Raytraced shadow penumbras needed a hand-authored $SunAngularSize: in
stars.tbl, which no shipped content sets -- so in practice nothing ever
got a penumbra, and the shadow-mapped path's $Shadow Smoothness Factor:
had no relation to it at all. Derive a size from the sun bitmap instead
and drive both methods from the same number.

g3_render_rect_screen_aligned_2d() sizes the sun quad so its rad is the
tangent of the half-angle it subtends, making tan(angular radius) equal
to 0.05 * the mission's +Scale: * the emitting disc's fraction of the
bitmap. That fraction is the area of the pixels at or above 90% of the
brightest one, converted to an equivalent radius. Across 90 sun bitmaps
from retail, the MediaVPs, Blue Planet and BtA it lands at a median of
0.261 (retail's chunkier art at 0.487), and the threshold acts as a
constant factor rather than a per-bitmap judgement call -- Spearman rho
0.96 between a 90% and a 50% cutoff.

Taken literally that yields suns 3.9-8.5x Sol's apparent diameter once
+Scale: is folded in (median 1.55, up to 5.0 in retail's own missions),
so the measurement is scaled by 0.25 and clamped to 1.5 degrees, putting
typical content near Sol. Derivation is the default: $SunAngularSize: 0
asks for hard shadows, an explicit value still wins, and missions can
also set +AngularSize: on an individual sun to override both, with FRED
and QtFRED UI (validation and fallback included) to author it. Bitmaps
that can't be read -- and the deliberately blank sun bitmaps mods ship
to get a light source with no visible disc -- fall back to hard shadows
rather than to the widest possible penumbra.

That angular size now sizes the raytraced penumbra cone directly
(traceShadowRayCone() in shadows.sdr), with sample count following the
same Shadow_quality tier that picks the shadow map's resolution (1 ray
at Low, up to 16 at Ultra) rather than being a separate setting. The
shadow-mapped path's tabled smoothness values were tuned by eye against
Sol-sized retail/MediaVPs art, so shadow_smoothness_scale() in
shadows.cpp scales them by the active sun's size relative to Sol,
clamped to what the shadow map can represent (shadow_clamp_smoothness()).
The two methods still disagree about occluder distance -- the raytracer
uses the real one, the shadow map's tabled values stand in for a blocker
distance pinned to the cascade's own extent -- so RT_SHADOW_SUN_SIZE_CALIBRATION
in shadows.sdr is a constant factor that makes a given $SunAngularSize:
read as roughly the same softness either way; it's the number to revisit
if the shadow map ever grows a real blocker search.

Also adds RTAO (raytraced ambient occlusion) sharing the same TLAS as
the shadow rays, with lighting-profile-driven radius/strength, and lab
controls for RT shadow quality (Low/High), the local light cap, RT
shadow samples, RTAO, and an angular-size override for comparing both
shadow methods against the same sun. All are safe to change live: they
only gate which lights are picked as shadow casters or scale existing
uniforms while filling the per-frame light data.

Co-Authored-By: Claude Opus 5 <[email protected]>

* Add shadow contact hardening via PCSS blocker search

The shadow-mapped path's penumbra width was a fixed per-cascade value
(shadow_smoothness_scale()'s ceiling), so every occluder blurred its
shadow by the same amount regardless of how close it actually was to
the receiver -- unlike the raytraced path, which gets that for free
from traceShadowRayCone()'s real occluder distance. Close that gap
with PCSS: search nearby texels for occluders, then scale the filter
radius by how far away they were.

The blocker search (pcssBlockerSearch() in shadows.sdr) needs raw,
uncompared depth reads, which the existing PCF sampler can't do -- it's
bound with a compare mode for the hardware shadow-compare instructions
the filtered sampling itself relies on. So shadow maps now get a second
sampler, shadow_map_raw, bound to the same texture with compare mode
off; on OpenGL that needs a second sampler object and GL 3.3
(shadow_contact_hardening_supported()), Vulkan can always bind it.
Where the raw sampler isn't available, or the new Graphics.ShadowContactHardening
option is off, shadows_start_render() sets Shadow_penumbra_scale[cascade]
to -1 as a sentinel and pcssPenumbraRadius() (shadows.sdr) falls straight
back to the fixed radius, bit-identical to the pre-contact-hardening
behavior and skipping the extra texture reads.

When it is on, shadows_start_render() computes each cascade's penumbra
scale from its own frustum extents and the sun's tanθ; the shader turns
that plus the blocker search's average depth into a per-pixel radius,
clamped to [1 texel, the old fixed ceiling]. A blocker search that finds
nothing doesn't fall back to "no shadow" -- a sparse 8-tap search can
miss a thin occluder, so it instead runs the full filter at the hardest
(1-texel) radius, trading a touch of extra blur for not popping a real
shadow to nothing.

Drops RT_SHADOW_SUN_SIZE_CALIBRATION accordingly: that constant existed
only to fake a blocker-distance-driven penumbra on the shadow-mapped path
by pre-scaling the raytraced cone to match it, and the shadow-mapped path
now has a real blocker search of its own, so the raytraced cone goes back
to its physically correct size.

* Apply review feedback on softer shadows

- Bind the GL raw-depth sampler on unit 11 in the deferred pass and
  unbind it after the pass.
- Enable RTAO only when shadows are on, and remove the RTAO option when
  ray tracing is not supported.
- Use every second Poisson tap in the PCSS blocker search.
- Use TLAS-relative ray origins for RTAO.
- Gate +AngularSize: at version 27.0.
- Measure an .eff sun from its first frame, and skip the measurement
  when shadows are off.
- Guard the GL 3.3 sampler check for GLES builds.
- Restore the lab session overrides when the lab closes.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>

* apply review suggestions

* Fix review findings and build the ray-tracing structure once per frame

Build the ray-tracing TLAS one time in each frame, in
vulkan_scene_texture_begin(), where no render pass is open. Remove the
TLAS build from the shadow pass and the second build in
vulkan_deferred_lighting_finish(). Remove gr_build_shadow_tlas.

Move the RTAO inputs and the deferred ray inputs (ray origin, cull mask,
bias) into globalDeferredData. Write them in each frame with
shadows_fill_deferred_global_data(). RTAO now works with shadows off and
in frames with no sun.

Other fixes from the review:
- Add the sampler2DArray precision to the OpenGL ES shader blocks.
- Declare shadowCascadeParams one time, in shadow_cascade_params.sdr.
- Use one ray function for traceShadowRay() and traceShadowRayCone().
- Restore the shadow method and the raytraced light limits when the lab
  closes.
- Limit the sun angular size to 90 degrees in stars.tbl and in missions.
- Make the OpenGL sampler part of the texture unit binding, and name the
  raw shadow map unit SHADOW_MAP_RAW_TEXTURE_UNIT.
- Measure the sun disc when the sun bitmap loads, not in the draw path.
- Use starfield_copy_entry_to_instance() in stars_set_data().
- Share the sun quad radius as SUN_QUAD_RADIUS.
- Correct the sun size tooltips and comments: the size affects both
  shadow methods.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

---------

Co-authored-by: Claude Opus 5 <[email protected]>
2026-11-12 21:05:22 +01:00
Goober5000andClaude Opus 5.5 bb2b2f2869 Remove dead modelinterp code (#7782)
* Remove dead BSP-parsing and software-interp code

model_interp_defpoints has had no callers since model_interp_sub was
removed (1ef7688fa), and the parse_bsp and find_tri_counts chains have
had no entry point since bsp_polygon_data replaced them (c62037235).
Remove them, along with the globals that only they read: tri_count,
model_radius, GEOMETRY_NOISE, Interp_flags, Interp_lights,
Interp_thrust_scale_subobj and Interp_warp_scale_*.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>

* Remove the interp-data allocator; give the BE swap path its own vertex list

On big-endian builds, swap_bsp_defpoints was the only remaining caller
of model_allocate_interp_data, and swap_bsp_flatpoly the only reader of
the buffers it allocated (Interp_verts).  Pass a local vertex list
through the BSP swap functions instead, and remove the allocator, the
deallocator, and the Interp_* buffers and lighting globals it managed.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>

* Remove write-only interp state and the functions that only set it

The thruster, outline, detail-lock, insignia, forced-bitmap and box-scale
globals in modelinterp.cpp are written but never read; rendering takes
these from model_render_params instead.  Remove them, along with
model_set_thrust and model_set_outline_color (no callers),
model_set_detail_level (its calls sit beside the real
set_detail_level_lock), interp_clear_instance, and the unused
modelstats_num_sortnorms and modelstats_num_boxes counters.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* Drop the unused big-endian flat-poly recompute

swap_bsp_flatpoly recomputed the center and radius of pre-2003 flat
polygons, but nothing reads those fields on any platform: collision
reads the normal and vertices, bsp_polygon_data reads the count, color
and vertices, and flash_ball skips flat polygons.  Remove it, along with
the vertex list that only it needed, so swap_bsp_data is back to its
original shape with only the dead code deleted.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* remove unused Interp_detail_level

---------

Co-authored-by: Claude Opus 5 (1M context) <[email protected]>
2026-11-12 14:58:49 -05:00
Shivansps 72053c7ddb Merge pull request #8022 from Shivansps/fix-android-scpui
Fix SCPUI not accepting text input on Android.
2026-11-12 13:42:01 -05:00
Goober5000 8d4bde4a74 Merge pull request #8026 from Goober5000/fix/bigendian_bsp_bounds
set Macro_ubyte_bounds around swap_bsp_data in model loading
2026-11-12 13:04:10 -05:00
Goober5000andClaude Opus 5.5 5cb93b1bf8 Audio robustness: fix stale stream handles (#7995)
* reset stale audio stream handles and guard stream play/stop

The training voice stream handle was never initialized to -1 or reset
after closing, so the none.wav path could play a freed (or reassigned)
stream slot.  In release builds the status Assert is a no-op, and
AudioStream::Play() on a freed slot registered an SDL timer whose ID was
later discarded by m_timer.constructor(), leaving it running forever.

Reset the handle after every close, make audiostream_play() and
audiostream_stop() return early on a freed slot, and have Play() remove
any lingering timer instead of forgetting it.  Also reset the event
music pattern and command briefing stage handles after closing them.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* fix uninitialized size and past-the-end read in ds_init_channels

Initialize the attribute count before querying it, skip the parse if
none are reported, and walk the zero-terminated list as key/value pairs
so attrs[i+1] can't run past the end and a value can't be mistaken for
the ALC_MONO_SOURCES key.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* remove unreachable training voice branch and fix small OpenAL nits

The Training_voice == index check in message_play_training_voice can
never be true for a streamed voice, since the enclosing condition
requires Training_voice != index in that case.  Also correct the
element count passed to alcGetIntegerv for ALC_FREQUENCY and update the
stale MIN_SOURCES comment, which still described the old 32-channel
MAX_CHANNELS.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* finish fading out audio streams that are at zero volume

A stream closed with fade while playing at volume 0 computed a cutoff
of 0 and then waited for its volume to drop below it, which never
happens.  The stream was never destroyed, so its slot and service timer
stayed held for the rest of the session.  With only 30 stream slots and
briefings opening many at once, leaked slots eventually make
audiostream_open() fail, silencing briefing voices and music until the
game is restarted.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

---------

Co-authored-by: Claude Opus 5.5 <[email protected]>
2026-11-12 12:41:00 -05:00
dustinspace217 a39bb9b773 Merge pull request #7977 from dustinspace217/bmpman/eff-buffer-size
P3: Size the .eff parse buffers from the file
2026-11-12 12:40:19 -05:00
Goober5000andClaude Opus 5.5 96482c9ef6 set Macro_ubyte_bounds around swap_bsp_data in model loading
The checked byte-access macros assert against Macro_ubyte_bounds in Debug builds, but it was still nullptr when model loading byte-swapped each submodel's BSP data, so big-endian Debug builds asserted on the first access in swap_bsp_data.  Little-endian builds were unaffected because swap_bsp_data is a no-op there.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-11 02:54:52 -05:00
170 changed files with 9507 additions and 1599 deletions

No files matched your search

+2
View File
@@ -150,6 +150,7 @@ OPTION(FSO_BUILD_WITH_OPENGL_DEBUG "Enables debug option for OpenGL" OFF)
OPTION(FSO_BUILD_WITH_OPENGL_ES "When building OpenGL, use OpenGL ES compatibility layer" OFF)
OPTION(FSO_BUILD_WITH_VULKAN "Enable compilation of the Vulkan renderer" ON)
OPTION(FSO_BUILD_WITH_FFX "Enable AMD FSR in the Vulkan renderer. Downloads AMD's binaries at configure time" ON)
if(NOT FSO_BUILD_WITH_OPENGL AND FSO_BUILD_WITH_OPENGL_ES)
# Disable GLES if not building with OpenGL
@@ -194,6 +195,7 @@ mark_as_advanced(FORCE FSO_BUILD_WITH_OPENGL)
mark_as_advanced(FORCE FSO_BUILD_WITH_OPENGL_DEBUG)
mark_as_advanced(FORCE FSO_BUILD_WITH_OPENGL_ES)
mark_as_advanced(FORCE FSO_BUILD_WITH_VULKAN)
mark_as_advanced(FORCE FSO_BUILD_WITH_FFX)
mark_as_advanced(FORCE FSO_BUILD_WITH_OPENXR)
mark_as_advanced(FORCE FSO_BUILD_WITH_OPENXR_DEBUG)
+12
View File
@@ -111,6 +111,18 @@ MACRO(COPY_FILES_TO_TARGET _target)
FOREACH(file IN LISTS TARGET_COPY_FILES)
COPY_FILE_TO_TARGET("${_target}" "${file}")
ENDFOREACH(file)
# Third party licenses that have to accompany the copied binaries
get_property(_ffx_license GLOBAL PROPERTY FSO_FFX_LICENSE_FILE)
if (_ffx_license)
ADD_CUSTOM_COMMAND(
TARGET ${_target} POST_BUILD
COMMAND ${CMAKE_COMMAND} -E make_directory "$<TARGET_FILE_DIR:${_target}>/license"
COMMAND ${CMAKE_COMMAND} -E copy_if_different "${_ffx_license}" "$<TARGET_FILE_DIR:${_target}>/license/amd_ffx.txt"
COMMENT "copying AMD FidelityFX license..."
VERBATIM
)
endif()
ENDMACRO(COPY_FILES_TO_TARGET)
macro(_clear_old_libraries_state)
+5
View File
@@ -115,6 +115,11 @@ if (FSO_BUILD_WITH_VULKAN)
target_compile_definitions(code PUBLIC WITH_VULKAN VULKAN_HPP_DISPATCH_LOADER_DYNAMIC=1 VK_NO_PROTOTYPES)
target_include_directories(code PUBLIC Vulkan::Headers)
target_link_libraries(code PUBLIC VulkanMemoryAllocator)
# AMD only ships 64-bit runtimes, and 32-bit Vulkan handles aren't the pointers its API takes
if (FSO_BUILD_WITH_FFX AND IS_64BIT)
target_compile_definitions(code PRIVATE WITH_FFX)
target_link_libraries(code PRIVATE ffx_portable)
endif()
endif()
include(shaders.cmake)
+25 -6
View File
@@ -25,6 +25,7 @@
#include "io/key.h"
#include "io/timer.h"
#include "jpgutils/jpgutils.h"
#include "mod_table/mod_table.h"
#include "network/multiutil.h"
#include "parse/parselo.h"
#include "pcxutils/pcxutils.h"
@@ -883,6 +884,16 @@ const char *bm_get_filename(int handle) {
return entry->filename;
}
const char *bm_get_image_filename(int handle) {
auto entry = bm_get_entry(handle);
// an EFF entry stores the file of its individual image separately
if (entry->type == BM_TYPE_EFF)
return entry->info.ani.eff.filename;
return entry->filename;
}
void bm_get_filename(int bitmapnum, char *filename) {
if (!bm_is_valid(bitmapnum)) {
strcpy(filename, "");
@@ -1361,12 +1372,20 @@ bool bm_load_and_parse_eff(const char *filename, int dir_type, int *nframes, int
int frames = 0, fps = 30, keyframe = 0;
char ext[8];
BM_TYPE c_type = BM_TYPE_NONE;
char file_text[1024];
char file_text_raw[1024];
memset(ext, 0, sizeof(ext));
memset(file_text, 0, sizeof(file_text));
memset(file_text_raw, 0, sizeof(file_text_raw));
// cf_find_file_location reports the stored size, which is smaller than the text for a compressed file
CFILE* eff_cfp = cfopen(filename, "rb", dir_type);
const size_t file_len = (eff_cfp != nullptr) ? static_cast<size_t>(std::max(cfilelength(eff_cfp), 0)) : 0;
if (eff_cfp != nullptr) {
cfclose(eff_cfp);
}
// encrypted text unpacks 7 bytes into 8, and outside Unicode mode processing can turn one character into two
const size_t raw_len = file_len + file_len / 7 + 16;
SCP_vector<char> file_text_raw(raw_len, '\0');
SCP_vector<char> file_text(Unicode_text_mode ? raw_len : raw_len * 2, '\0');
// pause anything that may happen to be parsing right now
pause_parse();
@@ -1374,8 +1393,8 @@ bool bm_load_and_parse_eff(const char *filename, int dir_type, int *nframes, int
try
{
// now start parsing the EFF
read_file_text(filename, dir_type, file_text, file_text_raw);
reset_parse(file_text);
read_file_text(filename, dir_type, file_text.data(), file_text_raw.data());
reset_parse(file_text.data());
required_string("$Type:");
stuff_string(ext, F_NAME, sizeof(ext));
+7
View File
@@ -436,6 +436,13 @@ void bm_get_filename(int bitmapnum, char *filename);
*/
const char *bm_get_filename(int handle);
/**
* @brief Gets the name of the file that holds the pixels of this bitmap
*
* This is the same as bm_get_filename(), except for an EFF. For an EFF it is the file of the individual image.
*/
const char *bm_get_image_filename(int handle);
/**
* @brief Unloads all used bitmaps, should only ever be called by game_shutdown()
*
+4
View File
@@ -1199,6 +1199,10 @@ bool cam_set_camera(camid cid)
return false;
}
// Autopilot and scripts set the same camera again every frame
if (!(Viewer_mode & VM_FREECAMERA) || Current_camera.getSignature() != cid.getSignature()) {
gr_reset_temporal_history();
}
Viewer_mode |= VM_FREECAMERA;
Current_camera = cid;
+11
View File
@@ -354,6 +354,7 @@ cmdline_parm deprecated_fxaa_preset_arg("-fxaa_preset", "FXAA quality (0-2), req
cmdline_parm deprecated_smaa_arg("-smaa", nullptr, AT_NONE);
cmdline_parm deprecated_smaa_preset_arg("-smaa_preset", "SMAA quality (0-3), requires post-processing and -smaa", AT_INT);
cmdline_parm msaa_enabled_arg("-msaa", nullptr, AT_INT);
cmdline_parm fsr_mode_arg("-fsr", "AMD FSR: 0 off, 1 AA only, 2 upscale to the window resolution. Vulkan only", AT_INT);
cmdline_parm fb_explosions_arg("-fb_explosions", NULL, AT_NONE);
cmdline_parm fb_thrusters_arg("-fb_thrusters", NULL, AT_NONE);
cmdline_parm shadow_quality_arg("-shadow_quality", NULL, AT_INT);
@@ -2127,6 +2128,16 @@ bool SetCmdlineParams()
post_process_aa_preset_arg.found() ? "-aa_preset" : "-aa");
}
if (fsr_mode_arg.found()) {
const int mode = fsr_mode_arg.get_int();
if (mode >= static_cast<int>(FsrMode::Off) && mode <= static_cast<int>(FsrMode::Upscale)) {
// What of this can run is only known once the renderer is up; gr_init settles it on the closest offered setting
Gr_fsr_mode = static_cast<FsrMode>(mode);
Gr_fsr_major_version = 0;
options::OptionsManager::instance()->setOverride("Graphics.FSR", std::unique_ptr<json_t>(json_integer(mode)), "-fsr");
}
}
if (msaa_enabled_arg.found()) {
Cmdline_msaa_enabled = msaa_enabled_arg.get_int();
switch (Cmdline_msaa_enabled) {
+1
View File
@@ -1273,6 +1273,7 @@ void debris_render(object * obj, model_draw_list *scene)
}
model_render_params render_info;
render_info.set_object_number(OBJ_INDEX(obj));
if ( db->is_hull ) {
MONITOR_INC(NumHullDebrisRend, 1);
+13
View File
@@ -10,13 +10,23 @@ layout(location=0) out vec4 fragOut0;
#ifdef VULKAN
layout (location = 0) in vec4 fragTexCoord;
layout (location = 1) in vec4 fragColor;
layout (location = 2) in vec4 fragVelocityCurClip;
layout (location = 3) in vec4 fragVelocityPrevClip;
layout (location = 4) in float fragHasMotion;
layout (location = 6) out vec4 fragOut6;
layout (set = 1, binding = 1) uniform sampler2DArray baseMap;
layout (set = 2, binding = 0, std140) uniform genericData {
vec4 color;
float intensity;
mat4 velocityViewProj;
mat4 velocityPrevViewProj;
vec2 velocityUvScale;
};
#include "motion-vectors.sdr"
#else
in vec4 fragTexCoord;
in vec4 fragColor;
@@ -38,4 +48,7 @@ void main()
vec4 blendColor = vec4(srgb_to_linear(fragColor.rgb), fragColor.a);
fragOut0 = baseColor * blendColor * intensity;
#ifdef VULKAN
fragOut6 = effect_motion(fragVelocityCurClip, fragVelocityPrevClip, fragHasMotion, velocityUvScale, fragOut0);
#endif
}
+13
View File
@@ -6,9 +6,13 @@ precision highp float;
layout (location = 0) in vec4 vertPosition;
layout (location = 1) in vec4 vertColor;
layout (location = 2) in vec4 vertTexCoord;
layout (location = 12) in vec4 vertMotion;
layout (location = 0) out vec4 fragTexCoord;
layout (location = 1) out vec4 fragColor;
layout (location = 2) out vec4 fragVelocityCurClip;
layout (location = 3) out vec4 fragVelocityPrevClip;
layout (location = 4) out float fragHasMotion;
layout (set = 2, binding = 1, std140) uniform matrixData {
mat4 modelViewMatrix;
@@ -18,6 +22,9 @@ layout (set = 2, binding = 1, std140) uniform matrixData {
layout (set = 2, binding = 0, std140) uniform genericData {
vec4 color;
float intensity;
mat4 velocityViewProj;
mat4 velocityPrevViewProj;
vec2 velocityUvScale;
};
#else
in vec4 vertPosition;
@@ -43,4 +50,10 @@ void main()
fragColor = vertColor * color;
gl_Position = projMatrix * modelViewMatrix * vertPosition;
fragTexCoord = vertTexCoord;
#ifdef VULKAN
// Batched vertices are in world space
fragVelocityCurClip = velocityViewProj * vec4(vertPosition.xyz, 1.0);
fragVelocityPrevClip = velocityPrevViewProj * vec4(vertPosition.xyz - vertMotion.xyz, 1.0);
fragHasMotion = vertMotion.w;
#endif
}
+41 -27
View File
@@ -4,6 +4,7 @@ precision highp float;
precision highp int;
precision highp sampler2D;
precision highp samplerCube;
precision highp sampler2DArray;
precision highp sampler2DArrayShadow;
#endif
@@ -26,13 +27,14 @@ layout(set = 1, binding = 1) uniform sampler2D sTextures[16];
#define SpecBuffer sTextures[3]
layout(set = 0, binding = 2) uniform sampler2DArrayShadow shadow_map;
layout(set = 0, binding = 7) uniform sampler2DArray shadow_map_raw;
#ifdef ENV_MAP
layout(set = 0, binding = 3) uniform samplerCube sEnvmap;
layout(set = 0, binding = 4) uniform samplerCube sIrrmap;
#endif
#ifdef RT_SHADOWS
#if defined(RT_SHADOWS) || defined(RTAO)
layout(set = 0, binding = 5) uniform accelerationStructureEXT shadow_tlas;
#endif
#else
@@ -41,6 +43,7 @@ uniform sampler2D NormalBuffer;
uniform sampler2D PositionBuffer;
uniform sampler2D SpecBuffer;
uniform sampler2DArrayShadow shadow_map;
uniform sampler2DArray shadow_map_raw;
#ifdef ENV_MAP
uniform samplerCube sEnvmap;
@@ -59,28 +62,20 @@ uniform globalDeferredData {
float invScreenWidth;
float invScreenHeight;
float nearPlane;
int rtaoSampleCount;
// The ray-tracing inputs of this pass, for raytraced shadows and RTAO. See
// deferred_global_data in uniform_structs.h.
vec3 rayViewOrigin;
int rayCullMask;
float rayBiasMin;
float rayBiasMax;
float rtaoRadius;
float rtaoStrength;
};
#ifdef VULKAN
layout(set = 0, binding = 6, std140)
#else
layout(std140)
#endif
uniform shadowCascadeParams {
int cascade_offset;
int cascade_count;
float rtShadowBiasMin;
float rtShadowBiasMax;
mat4 shadow_mv_matrix;
// See shadow_cascade_static_data (uniform_structs.h).
vec3 shadow_ray_view_origin;
int shadow_ray_cull_mask;
mat4 shadow_proj_matrix[NUM_SHADOW_CASCADES];
vec4 cascade_distances[(NUM_SHADOW_CASCADES + 4 - 1) / 4];
vec4 smoothness_factors[(NUM_SHADOW_CASCADES + 4 - 1) / 4];
};
#include "shadow_cascade_params.sdr" //! #include "shadow_cascade_params.sdr"
#ifdef VULKAN
layout(set = 2, binding = 1, std140)
@@ -303,6 +298,18 @@ void main()
if (lightType == LT_AMBIENT) {
float ao = position_buffer.w;
#ifdef RTAO
{
// Raytraced AO multiplies into the same ao term as the baked AO maps,
// so the env-map/IBL ambient below darkens consistently for free.
// Ray origin in TLAS-relative space, as for the shadow rays below.
vec3 worldPos = mat3(inv_view_matrix) * position + rayViewOrigin;
vec3 worldNormal = normalize((inv_view_matrix * vec4(normal, 0.0)).xyz);
float rtaoBias = computeRtShadowBias(length(position), rayBiasMin, rayBiasMax);
ao *= traceAmbientOcclusion(shadow_tlas, worldPos, worldNormal,
rtaoRadius, rtaoStrength, rtaoSampleCount, rtaoBias, uint(rayCullMask), gl_FragCoord.xy);
}
#endif
fragmentColor.rgb = diffuseLightColor * diffColor * ao;
#ifdef ENV_MAP
@@ -322,15 +329,22 @@ void main()
if (enable_shadows != 0) {
#ifdef RT_SHADOWS
// Ray origin in TLAS-relative space; see shadow_cascade_static_data (uniform_structs.h).
// Ray origin in TLAS-relative space; see deferred_global_data (uniform_structs.h).
// Only the rotation of inv_view_matrix is used: its translation is the (large) camera
// world position, which shadow_ray_view_origin supplies already rebased onto the TLAS.
vec3 worldPos = mat3(inv_view_matrix) * position + shadow_ray_view_origin;
// world position, which rayViewOrigin supplies already rebased onto the TLAS.
vec3 worldPos = mat3(inv_view_matrix) * position + rayViewOrigin;
vec3 worldNormal = normalize((inv_view_matrix * vec4(normal, 0.0)).xyz);
vec3 worldLightDir = normalize((inv_view_matrix * vec4(lightDir, 0.0)).xyz);
float rtShadowBias = computeRtShadowBias(length(position), rtShadowBiasMin, rtShadowBiasMax);
attenuation *= traceShadowRay(shadow_tlas, worldPos, worldNormal, worldLightDir, lightDist, rtShadowBias, uint(shadow_ray_cull_mask));
float rtShadowBias = computeRtShadowBias(length(position), rayBiasMin, rayBiasMax);
// Penumbra cone: for directional lights sourceRadius IS the tangent of the
// sun's angular radius ($SunAngularSize); for local lights the subtended
// half-angle follows from the physical source radius and the light distance.
float coneTan = (lightType == LT_DIRECTIONAL)
? sourceRadius
: sourceRadius / max(lightDist, 1e-3);
attenuation *= traceShadowRayCone(shadow_tlas, worldPos, worldNormal, worldLightDir, lightDist,
rtShadowBias, uint(rayCullMask), coneTan, rtShadowSampleCount, gl_FragCoord.xy);
#else
vec4 fragShadowPos = shadow_mv_matrix * inv_view_matrix * vec4(position, 1.0);
vec4 fragShadowUV[NUM_SHADOW_CASCADES];
@@ -338,7 +352,7 @@ void main()
fragShadowUV[i] = transformToShadowMap(shadow_proj_matrix[i], i, fragShadowPos);
}
attenuation *= getShadowValue(shadow_map, -position.z, fragShadowUV, cascade_distances, smoothness_factors, cascade_offset, cascade_count);
attenuation *= getShadowValue(shadow_map, shadow_map_raw, -position.z, fragShadowUV, cascade_distances, smoothness_factors, penumbra_scale, cascade_offset, cascade_count);
#endif
}
+20
View File
@@ -15,6 +15,11 @@ layout(location = 0) in vec4 fragTexCoord;
layout(location = 1) in vec4 fragColor;
layout(location = 2) in float fragRadius;
layout(location = 3) in vec4 fragPosition;
layout(location = 4) in vec4 fragVelocityCurClip;
layout(location = 5) in vec4 fragVelocityPrevClip;
layout(location = 6) in float fragHasMotion;
layout(location = 6) out vec4 fragOut6;
layout(set = 1, binding = 1) uniform sampler2DArray baseMap;
layout(set = 1, binding = 4) uniform sampler2D depthMap;
@@ -41,8 +46,17 @@ uniform genericData {
int linear_depth;
int srgb;
int blend_alpha;
#ifdef VULKAN
mat4 velocityViewProj;
mat4 velocityPrevViewProj;
vec2 velocityUvScale;
#endif
};
#ifdef VULKAN
#include "motion-vectors.sdr"
#endif
void main()
{
@@ -53,6 +67,9 @@ void main()
float offset_len = length(offset);
if ( offset_len > fragRadius ) {
fragOut0 = vec4(0.0, 0.0, 0.0, 0.0);
#ifdef VULKAN
fragOut6 = vec4(0.0);
#endif
return;
}
vec2 depthCoord = vec2(gl_FragCoord.x / window_width, gl_FragCoord.y / window_height );
@@ -80,4 +97,7 @@ void main()
fragmentColor.rgb *= (srgb == 1) ? 1.5 : 1.0;
fragmentColor = (blend_alpha == 1) ? vec4(fragmentColor.rgb, fragmentColor.a * intensity) : vec4(fragmentColor.rgb * intensity, fragmentColor.a);
fragOut0 = max(fragmentColor, vec4(0.0));
#ifdef VULKAN
fragOut6 = effect_motion(fragVelocityCurClip, fragVelocityPrevClip, fragHasMotion, velocityUvScale, fragOut0);
#endif
}
+23
View File
@@ -7,11 +7,28 @@ layout(location = 0) in vec4 vertPosition;
layout(location = 1) in vec4 vertColor;
layout(location = 2) in vec4 vertTexCoord;
layout(location = 6) in float vertRadius;
layout(location = 12) in vec4 vertMotion;
layout(location = 0) out vec4 fragTexCoord;
layout(location = 1) out vec4 fragColor;
layout(location = 2) out float fragRadius;
layout(location = 3) out vec4 fragPosition;
layout(location = 4) out vec4 fragVelocityCurClip;
layout(location = 5) out vec4 fragVelocityPrevClip;
layout(location = 6) out float fragHasMotion;
layout(std140, set = 2, binding = 0) uniform genericData {
float window_width;
float window_height;
float nearZ;
float farZ;
int linear_depth;
int srgb;
int blend_alpha;
mat4 velocityViewProj;
mat4 velocityPrevViewProj;
vec2 velocityUvScale;
};
#else
in vec4 vertPosition;
in vec4 vertTexCoord;
@@ -57,4 +74,10 @@ void main()
fragTexCoord = vec4(vertTexCoord.xyz, 0.0);
fragColor = vertColor;
#endif
#ifdef VULKAN
// Batched vertices are in world space
fragVelocityCurClip = velocityViewProj * vec4(vertPosition.xyz, 1.0);
fragVelocityPrevClip = velocityPrevViewProj * vec4(vertPosition.xyz - vertMotion.xyz, 1.0);
fragHasMotion = vertMotion.w;
#endif
}
+31 -19
View File
@@ -98,28 +98,21 @@ uniform modelData {
int sMiscmapIndex;
float alphaMult;
int flags;
#ifdef VULKAN
mat4 prevModelMatrix;
mat4 velocityViewProj;
mat4 velocityPrevViewProj;
vec2 velocityUvScale;
int prev_transform_stride;
#endif
};
#ifdef VULKAN
layout(set = 0, binding = 6, std140)
#else
layout(std140)
#include "motion-vectors.sdr"
#endif
uniform shadowCascadeParams {
int cascade_offset;
int cascade_count;
float rtShadowBiasMin;
float rtShadowBiasMax;
mat4 shadow_mv_matrix;
// See shadow_cascade_static_data (uniform_structs.h).
vec3 shadow_ray_view_origin;
int shadow_ray_cull_mask;
mat4 shadow_proj_matrix[NUM_SHADOW_CASCADES];
vec4 cascade_distances[(NUM_SHADOW_CASCADES + 4 - 1) / 4];
vec4 smoothness_factors[(NUM_SHADOW_CASCADES + 4 - 1) / 4];
};
#include "shadow_cascade_params.sdr" //! #include "shadow_cascade_params.sdr"
layout(location = 0) out vec4 fragOut0;
@@ -138,6 +131,7 @@ layout(set = 1, binding = 1) uniform sampler2DArray materialTextures[16];
#define sMiscmap materialTextures[6]
layout(set = 0, binding = 2) uniform sampler2DArrayShadow shadow_map;
layout(set = 0, binding = 7) uniform sampler2DArray shadow_map_raw;
layout(set = 1, binding = 5) uniform sampler2D sFramebuffer;
#ifdef MODEL_SDR_FLAG_RT_SHADOWS
@@ -150,6 +144,8 @@ layout(location = 0) in VertexOutput {
vec4 texCoord;
mat3 tangentMatrix;
float fogDist;
vec4 velocityCurClip;
vec4 velocityPrevClip;
#prereplace IF_FLAG_COMPILED MODEL_SDR_FLAG_SHADOWS
vec4 shadowUV[NUM_SHADOW_CASCADES];
@@ -161,6 +157,7 @@ layout(location = 0) in VertexOutput {
#endif
} vertIn;
layout(location = 6) out vec4 fragOut6;
#else
#prereplace IF_FLAG_COMPILED MODEL_SDR_FLAG_DIFFUSE
uniform sampler2DArray sBasemap;
@@ -183,6 +180,7 @@ uniform sampler2DArray sMiscmap;
#prereplace ENDIF_FLAG_COMPILED MODEL_SDR_FLAG_MISC
#prereplace IF_FLAG_COMPILED MODEL_SDR_FLAG_SHADOWS
uniform sampler2DArrayShadow shadow_map;
uniform sampler2DArray shadow_map_raw;
#prereplace ENDIF_FLAG_COMPILED MODEL_SDR_FLAG_SHADOWS
in VertexOutput {
@@ -267,7 +265,11 @@ vec3 CalculateLighting(vec3 normal, vec3 diffuseMaterial, vec3 specularMaterial,
if (rtShadowsActive && lights[i].light_type == LT_DIRECTIONAL && shadowedDirectionalCount < MAX_RT_SHADOW_LIGHTS) {
vec3 worldSunDir = normalize((invView * vec4(lights[i].position.xyz, 0.0)).xyz);
float rtShadowBias = computeRtShadowBias(length(vertIn.position.xyz), rtShadowBiasMin, rtShadowBiasMax);
shadow = traceShadowRay(shadow_tlas, worldPos, worldNormal, worldSunDir, RT_SHADOW_MAX_DISTANCE, rtShadowBias, uint(shadow_ray_cull_mask));
// For directional lights ml_sourceRadius is the tangent of the sun's
// angular radius ($SunAngularSize) -- 0 keeps hard shadows.
shadow = traceShadowRayCone(shadow_tlas, worldPos, worldNormal, worldSunDir, RT_SHADOW_MAX_DISTANCE,
rtShadowBias, uint(shadow_ray_cull_mask), lights[i].ml_sourceRadius,
rtShadowSampleCount, gl_FragCoord.xy);
++shadowedDirectionalCount;
} else {
shadow = 1.0;
@@ -439,7 +441,7 @@ void main()
// up to MAX_RT_SHADOW_LIGHTS) inside CalculateLighting()'s loop instead --
// this single shared `shadow` value only matters for the CSM fallback below.
#ifndef MODEL_SDR_FLAG_RT_SHADOWS
shadow = getShadowValue(shadow_map, -vertIn.position.z, vertIn.shadowUV, cascade_distances, smoothness_factors, cascade_offset, cascade_count);
shadow = getShadowValue(shadow_map, shadow_map_raw, -vertIn.position.z, vertIn.shadowUV, cascade_distances, smoothness_factors, penumbra_scale, cascade_offset, cascade_count);
#endif
#prereplace ENDIF_FLAG //MODEL_SDR_FLAG_SHADOWS
baseColor.rgb = CalculateLighting(normal, baseColor.rgb, specColor.rgb, glossData, fresnelFactor, shadow, aoFactors.x);
@@ -539,6 +541,16 @@ void main()
fragOut0 = baseColor;
#ifdef VULKAN
// Alpha selects whether this replaces the motion already there: faint transparent surfaces keep what's behind them.
vec2 velocity = motion_uv(vertIn.velocityCurClip, vertIn.velocityPrevClip, velocityUvScale);
float velocityAlpha = baseColor.a >= 0.5 ? 1.0 : 0.0;
#prereplace IF_FLAG MODEL_SDR_FLAG_DEFERRED
velocityAlpha = 1.0;
#prereplace ENDIF_FLAG //MODEL_SDR_FLAG_DEFERRED
fragOut6 = vec4(velocity, motion_depth(vertIn.velocityCurClip), velocityAlpha);
#endif
#prereplace IF_FLAG MODEL_SDR_FLAG_DEFERRED
fragOut1 = vec4(vertIn.position.xy, compress_depth_value(vertIn.position.z), aoFactors.x);
fragOut2 = vec4(normal, glossData);
+22 -18
View File
@@ -7,6 +7,7 @@
precision highp float;
precision highp int;
precision highp samplerBuffer;
precision highp sampler2DArray;
precision highp sampler2DArrayShadow;
#else
#extension GL_ARB_gpu_shader5: enable
@@ -113,29 +114,18 @@ uniform modelData {
float alphaMult;
int flags;
};
#ifdef VULKAN
layout(set = 0, binding = 6, std140)
#else
layout(std140)
mat4 prevModelMatrix;
mat4 velocityViewProj;
mat4 velocityPrevViewProj;
vec2 velocityUvScale;
int prev_transform_stride;
#endif
uniform shadowCascadeParams {
int cascade_offset;
int cascade_count;
float rtShadowBiasMin;
float rtShadowBiasMax;
mat4 shadow_mv_matrix;
// See shadow_cascade_static_data (uniform_structs.h). Unused here; declared for block layout.
vec3 shadow_ray_view_origin;
int shadow_ray_cull_mask;
mat4 shadow_proj_matrix[NUM_SHADOW_CASCADES];
vec4 cascade_distances[(NUM_SHADOW_CASCADES + 4 - 1) / 4];
vec4 smoothness_factors[(NUM_SHADOW_CASCADES + 4 - 1) / 4];
};
#include "shadow_cascade_params.sdr" //! #include "shadow_cascade_params.sdr"
#ifdef VULKAN
// Transform buffer for batched submodel rendering
layout(set = 1, binding = 3, std430) readonly buffer TransformBuffer {
@@ -150,6 +140,8 @@ layout(location = 0) out VertexOutput {
vec4 texCoord;
mat3 tangentMatrix;
float fogDist;
vec4 velocityCurClip;
vec4 velocityPrevClip;
#prereplace IF_FLAG_COMPILED MODEL_SDR_FLAG_SHADOWS
vec4 shadowUV[NUM_SHADOW_CASCADES];
@@ -206,6 +198,9 @@ void main()
vec3 normal;
vec4 texCoord;
mat4 orient = mat4(1.0);
#ifdef VULKAN
mat4 prevOrient = mat4(1.0);
#endif
bool clipModel = false;
// Transform loading
@@ -215,6 +210,11 @@ void main()
orient = transformBuf.transforms[buffer_matrix_offset + id];
clipModel = (orient[3].w >= 0.9);
orient[3].w = 1.0;
prevOrient = orient;
if (prev_transform_stride > 0) {
prevOrient = transformBuf.transforms[buffer_matrix_offset + prev_transform_stride + id];
prevOrient[3].w = 1.0;
}
#else
getModelTransform(orient, clipModel, int(vertModelID), buffer_matrix_offset);
#endif
@@ -234,6 +234,10 @@ void main()
normal = normalize(mat3(modelViewMatrix) * mat3(orient) * vertNormal);
position = modelViewMatrix * orient * vertex;
gl_Position = projMatrix * position;
#ifdef VULKAN
vertOut.velocityCurClip = velocityViewProj * modelMatrix * orient * vertex;
vertOut.velocityPrevClip = velocityPrevViewProj * prevModelMatrix * prevOrient * vertex;
#endif
#prereplace IF_FLAG MODEL_SDR_FLAG_SHADOWS
vec4 shadowPos = shadow_mv_matrix * modelMatrix * orient * vertPosition;
vertOut.shadowPos = shadowPos;
@@ -0,0 +1,53 @@
layout(location = 0) in vec2 fragTexCoord;
layout(location = 0) out vec4 fragOut0;
layout(location = 1) out vec4 fragOut1;
// Scene depth, and the drawn geometry's motion with its inverse view depth
layout(set = 1, binding = 1) uniform sampler2D inputs[2];
layout(std140, set = 2, binding = 0) uniform genericData {
mat4 reprojection;
vec4 viewportRect; // xy = offset, zw = size, as fractions of the image
float sceneNear;
float cockpitNear;
float farPlane;
float pad;
};
void main()
{
// All depth goes on the cockpit's projection: its nearer plane can express every scene distance. That depth is
// linear in the inverse view depth.
float depthScale = farPlane / (farPlane - cockpitNear);
// Whatever drew its own motion brought its depth along. That keeps the two matched where the scene's depth
// doesn't show what drew the motion, like the cockpit and the player's hull, which are drawn over a cleared depth.
vec3 drawnMotion = texture(inputs[1], fragTexCoord).xyz;
if (abs(drawnMotion.x) < 100.0) {
fragOut0 = vec4(drawnMotion.xy, 0.0, 0.0);
fragOut1 = vec4(clamp(depthScale * (1.0 - cockpitNear * drawnMotion.z), 0.0, 1.0), 0.0, 0.0, 0.0);
return;
}
// Anything else still holds the clear value, so only the camera moved it.
float sceneDepth = texture(inputs[0], fragTexCoord).r;
float viewDepth = farPlane * sceneNear / (farPlane - sceneDepth * (farPlane - sceneNear));
fragOut1 = vec4(depthScale * (1.0 - cockpitNear / viewDepth), 0.0, 0.0, 0.0);
// The projection maps +Y up onto the top of the image through a negative viewport height.
vec2 viewportUv = (fragTexCoord - viewportRect.xy) / viewportRect.zw;
vec4 ndc = vec4(viewportUv.x * 2.0 - 1.0, 1.0 - viewportUv.y * 2.0, sceneDepth, 1.0);
vec4 previous = reprojection * ndc;
// Behind last frame's camera, or no previous camera yet
if (previous.w <= 0.0) {
fragOut0 = vec4(0.0);
return;
}
previous.xy /= previous.w;
vec2 previousViewportUv = vec2(previous.x * 0.5 + 0.5, 0.5 - previous.y * 0.5);
vec2 previousUv = previousViewportUv * viewportRect.zw + viewportRect.xy;
fragOut0 = vec4(previousUv - fragTexCoord, 0.0, 0.0);
}
@@ -0,0 +1,28 @@
// Screen motion since last frame in UV units, pointing at the previous position. Zero if that was behind the camera.
vec2 motion_uv(vec4 curClip, vec4 prevClip, vec2 uvScale)
{
if (prevClip.w <= 0.0) {
return vec2(0.0);
}
return (prevClip.xy / prevClip.w - curClip.xy / curClip.w) * vec2(0.5, -0.5) * uvScale;
}
// The depth stored with drawn motion: the inverse of the view depth, so that it can't overflow half floats and maps
// linearly onto the depth buffer of any projection.
float motion_depth(vec4 curClip)
{
return 1.0 / curClip.w;
}
// Motion vector output of the batched effect shaders, for location 6. Alpha 0 keeps the motion already there, through
// the attachment's blend state: effects without movement of their own, and fragments too faint to matter.
const float EFFECT_MOTION_MIN_CONTRIBUTION = 0.1;
vec4 effect_motion(vec4 curClip, vec4 prevClip, float hasMotion, vec2 uvScale, vec4 color)
{
float contribution = max(max(color.r, color.g), color.b) * color.a;
if (hasMotion < 0.5 || contribution < EFFECT_MOTION_MIN_CONTRIBUTION || prevClip.w <= 0.0) {
return vec4(0.0);
}
return vec4(motion_uv(curClip, prevClip, uvScale), motion_depth(curClip), 1.0);
}
+19 -1
View File
@@ -13,15 +13,17 @@ layout(location = 3) out vec4 fragOut3;
layout(location = 4) out vec4 fragOut4;
#ifdef VULKAN
layout(location = 6) out vec4 fragOut6;
layout(location = 0) in vec2 fragTexCoord;
layout(set = 1, binding = 1) uniform sampler2DMS msaaTex[6];
layout(set = 1, binding = 1) uniform sampler2DMS msaaTex[7];
#define texColor msaaTex[0]
#define texPos msaaTex[1]
#define texNormal msaaTex[2]
#define texSpecular msaaTex[3]
#define texEmissive msaaTex[4]
#define texDepth msaaTex[5]
#define texVelocity msaaTex[6]
layout(std140, set = 2, binding = 0) uniform genericData {
int samples;
@@ -208,6 +210,10 @@ void main()
vec4 specular = vec4(0);
vec4 emissive = vec4(0);
float depth = 0.0;
#ifdef VULKAN
vec3 velocity = vec3(0.0);
float velocityWeight = 0.0;
#endif
for(int i = 0; i < SAMPLE_CNT; i++) {
vec4 localPos = texelFetch(texPos, texel, i);
@@ -226,6 +232,14 @@ void main()
emissive += texelFetch(texEmissive, texel, i) * localWeight;
depth += texelFetch(texDepth, texel, i).x * localWeight;
weight += localWeight;
#ifdef VULKAN
// Samples still holding the "no motion written" clear value mustn't average into real ones.
vec3 localVelocity = texelFetch(texVelocity, texel, i).xyz;
if (abs(localVelocity.x) < 100.0) {
velocity += localVelocity * localWeight;
velocityWeight += localWeight;
}
#endif
}
pos /= weight;
@@ -237,4 +251,8 @@ void main()
fragOut3 = specular / weight;
fragOut4 = emissive / weight;
gl_FragDepth = depth / weight;
#ifdef VULKAN
// Alpha 0 keeps what's there: the cockpit's pass mustn't erase the main scene's motion around it.
fragOut6 = velocityWeight > 0.0f ? vec4(velocity / velocityWeight, 1.0) : vec4(0.0);
#endif
}
@@ -0,0 +1,31 @@
// The shadowCascadeParams uniform block. All shaders that read the shadow cascades include this
// file, so the block has one declaration. The C++ layout is shadow_cascade_static_data in
// uniform_structs.h. Change both declarations together.
#ifdef VULKAN
layout(set = 0, binding = 6, std140)
#else
layout(std140)
#endif
uniform shadowCascadeParams {
int cascade_offset;
int cascade_count;
float rtShadowBiasMin;
float rtShadowBiasMax;
int rtShadowSampleCount;
// Padding. The scalars must fill 32 bytes, so that shadow_mv_matrix starts on a 16-byte boundary.
int shadowParamsPad0;
int shadowParamsPad1;
int shadowParamsPad2;
mat4 shadow_mv_matrix;
// The camera position in the coordinate space of the ray-tracing structure.
// See shadow_cascade_static_data in uniform_structs.h.
vec3 shadow_ray_view_origin;
int shadow_ray_cull_mask;
mat4 shadow_proj_matrix[NUM_SHADOW_CASCADES];
vec4 cascade_distances[(NUM_SHADOW_CASCADES + 4 - 1) / 4];
vec4 smoothness_factors[(NUM_SHADOW_CASCADES + 4 - 1) / 4];
vec4 penumbra_scale[(NUM_SHADOW_CASCADES + 4 - 1) / 4];
};
+3 -15
View File
@@ -14,21 +14,9 @@ layout (triangle_strip, max_vertices = 3) out;
layout(invocations = NUM_SHADOW_CASCADES) in;
#endif
layout (std140) uniform shadowCascadeParams {
int cascade_offset;
int cascade_count;
float rtShadowBiasMin;
float rtShadowBiasMax;
mat4 shadow_mv_matrix;
// See shadow_cascade_static_data (uniform_structs.h). Unused here; declared for block layout.
vec3 shadow_ray_view_origin;
int shadow_ray_cull_mask;
mat4 shadow_proj_matrix[NUM_SHADOW_CASCADES];
vec4 cascade_distances[(NUM_SHADOW_CASCADES + 4 - 1) / 4];
vec4 smoothness_factors[(NUM_SHADOW_CASCADES + 4 - 1) / 4];
};
// OpenGL-only: the Vulkan backend has no geometry-shader stage, so the Vulkan binding in the
// include below does not apply here.
#include "shadow_cascade_params.sdr" //! #include "shadow_cascade_params.sdr"
in VertexOutput {
#if !defined(GL_ARB_gpu_shader5) && !defined(GL_EXT_gpu_shader5)
+1 -20
View File
@@ -39,26 +39,7 @@ uniform shadowMapData {
int buffer_matrix_offset;
};
#ifdef VULKAN
layout(set = 0, binding = 6, std140)
#else
layout(std140)
#endif
uniform shadowCascadeParams {
int cascade_offset;
int cascade_count;
float rtShadowBiasMin;
float rtShadowBiasMax;
mat4 shadow_mv_matrix;
// See shadow_cascade_static_data (uniform_structs.h). Unused here; declared for block layout.
vec3 shadow_ray_view_origin;
int shadow_ray_cull_mask;
mat4 shadow_proj_matrix[NUM_SHADOW_CASCADES];
vec4 cascade_distances[(NUM_SHADOW_CASCADES + 4 - 1) / 4];
vec4 smoothness_factors[(NUM_SHADOW_CASCADES + 4 - 1) / 4];
};
#include "shadow_cascade_params.sdr" //! #include "shadow_cascade_params.sdr"
#ifdef VULKAN
// Transform buffer for batched submodel rendering (matches main-v.sdr)
+260 -39
View File
@@ -6,33 +6,47 @@
#define SAMPLES_PCSS 1
#endif
float samplePoissonPCSS(sampler2DArrayShadow shadow_map, vec4 shadowUV, float maxUVOffset)
{
// Full 16-tap Poisson disc. Unconditional (independent of SMOOTH_PCSS) because it has three
// consumers with different needs: samplePoissonPCSS() takes the first SAMPLES_PCSS of these,
// pcssBlockerSearch() always wants 8 (every second tap) regardless of SAMPLES_PCSS (a
// blocker search only needs a coarse estimate), and the raytraced cone/AO sampling further
// below always
// wants the full spread. Keeping one table means none of those three can end up indexing
// past a table sized for a different consumer.
const vec2 poissonDisc16[16] = vec2[](
vec2(-0.76275, -0.3432573),
vec2(-0.5226235, -0.8277544),
vec2(-0.3780261, 0.01528688),
vec2(-0.7742821, 0.4245702),
vec2(0.04196143, -0.02622231),
vec2(-0.2974772, -0.4722782),
vec2(-0.516093, 0.71495),
vec2(-0.3257416, 0.3910343),
vec2(0.2705966, 0.6670476),
vec2(0.4918377, 0.1853267),
vec2(0.4428544, -0.6251478),
vec2(-0.09204347, 0.9267113),
vec2(0.391505, -0.2558275),
vec2(0.05605913, -0.7570801),
vec2(0.81772, -0.02475523),
vec2(0.6890262, 0.5191521)
);
// Low-quality raster PCSS fallback: a single centered sample (i.e. no offset at all) rather
// than poissonDisc16[0], which is just one arbitrary tap among sixteen and no better centered
// than any other.
const vec2 poissonDiscCenter[1] = vec2[](vec2(0.0, 0.0));
// samplePoissonPCSS()'s tap table: the full disc when taking all SAMPLES_PCSS == 16 of them,
// or the single centered sample when SAMPLES_PCSS == 1.
#if SMOOTH_PCSS
vec2 poissonDisc[16] = vec2[](
vec2(-0.76275, -0.3432573),
vec2(-0.5226235, -0.8277544),
vec2(-0.3780261, 0.01528688),
vec2(-0.7742821, 0.4245702),
vec2(0.04196143, -0.02622231),
vec2(-0.2974772, -0.4722782),
vec2(-0.516093, 0.71495),
vec2(-0.3257416, 0.3910343),
vec2(0.2705966, 0.6670476),
vec2(0.4918377, 0.1853267),
vec2(0.4428544, -0.6251478),
vec2(-0.09204347, 0.9267113),
vec2(0.391505, -0.2558275),
vec2(0.05605913, -0.7570801),
vec2(0.81772, -0.02475523),
vec2(0.6890262, 0.5191521)
);
#define poissonDisc poissonDisc16
#else
vec2 poissonDisc[1] = vec2[](
vec2(0.0,0.0)
);
#define poissonDisc poissonDiscCenter
#endif
float samplePoissonPCSS(sampler2DArrayShadow shadow_map, vec4 shadowUV, float maxUVOffset)
{
float visibility = 0.0f;
for (int i=0; i<SAMPLES_PCSS; i++) {
vec4 uv = shadowUV;
@@ -42,8 +56,82 @@ float samplePoissonPCSS(sampler2DArrayShadow shadow_map, vec4 shadowUV, float ma
return visibility * (1.0f/float(SAMPLES_PCSS));
}
float getShadowValue(sampler2DArrayShadow shadow_map, float depth, vec4 shadowUV[NUM_SHADOW_CASCADES],
// PCSS blocker search: raw (non-compare) depth taps within searchRadiusUV, using every
// second tap of poissonDisc16. The first 8 taps are mostly on one side (x < 0), so they
// would search the penumbra on one side only. This is always the full-spread table, not
// samplePoissonPCSS()'s poissonDisc --
// a blocker search only needs a coarse estimate, not the final filtered result, but it needs
// that estimate regardless of how many taps the raster PCSS path itself is set to take).
// "Blocker" = stored < receiverDepth, matching the GL_LEQUAL/eLessOrEqual compare convention
// the hardware PCF sampler already uses (visible iff Dref <= stored) -- see the compare-sampler
// setup in gropengltnl.cpp / VulkanPostProcessingShadow.cpp.
// A negative return means "no blockers found", which the average blocker depth (always >= 0)
// can't otherwise be distinguished from "found some at depth 0".
float pcssBlockerSearch(sampler2DArray shadow_map_raw, vec4 shadowUV, float searchRadiusUV)
{
float sum = 0.0;
int blockerCount = 0;
for (int i = 0; i < 8; i++) {
vec2 uv = shadowUV.xy + poissonDisc16[i * 2] * searchRadiusUV;
float sampled = texture(shadow_map_raw, vec3(uv, shadowUV.z)).r;
if (sampled < shadowUV.w) {
sum += sampled;
blockerCount++;
}
}
return blockerCount > 0 ? (sum / float(blockerCount)) : -1.0;
}
// Computes the PCSS penumbra radius for one cascade sample: a blocker search followed by
// the orthographic-light penumbra formula (see shadow_smoothness_scale()'s doc comment in
// shadows.cpp for the derivation), clamped to [1 texel, smoothnessCeiling]. penumbraScale is
// Shadow_penumbra_scale[cascade] -- a negative value is the "unsupported or user-disabled"
// sentinel (see shadow_contact_hardening_enabled() in shadows.cpp), in which case this
// returns smoothnessCeiling immediately: bit-identical to the fixed-width behavior from
// before contact hardening existed, and skips the blocker search's extra texture reads.
float pcssPenumbraRadius(sampler2DArrayShadow shadow_map, sampler2DArray shadow_map_raw,
vec4 shadowUV, float penumbraScale, float smoothnessCeiling)
{
float texelFloor = 1.0 / float(textureSize(shadow_map, 0).x);
if (penumbraScale < 0.0) {
return smoothnessCeiling;
}
float avgBlockerDepth = pcssBlockerSearch(shadow_map_raw, shadowUV, smoothnessCeiling);
if (avgBlockerDepth < 0.0) {
// The sparse 8-tap search found nothing, but that doesn't prove there's no thin
// occluder nearby -- run the full PCF at the hardest radius rather than assuming
// fully lit, so a missed thin blocker still reads as an edge instead of popping to
// no shadow at all.
return texelFloor;
}
float deltaDepth = max(shadowUV.w - avgBlockerDepth, 0.0);
return clamp(deltaDepth * penumbraScale, texelFloor, smoothnessCeiling);
}
// PCSS-filtered shadow visibility for a single cascade: looks up its penumbra radius (see
// pcssPenumbraRadius()) and samples the shadow map at that radius. Factored out of
// getShadowValue() below so the cascade-index unpacking (v/i from cascade/4, cascade%4) and
// the penumbra-radius lookup each appear once, whichever of getShadowValue()'s two return
// paths runs.
float shadowVisibilityForCascade(sampler2DArrayShadow shadow_map, sampler2DArray shadow_map_raw,
vec4 shadowUV, int cascade, vec4 smoothness_factors[(NUM_SHADOW_CASCADES + 4 - 1) / 4],
vec4 penumbra_scale[(NUM_SHADOW_CASCADES + 4 - 1) / 4])
{
int v = cascade / 4;
int i = cascade % 4;
float radius = pcssPenumbraRadius(shadow_map, shadow_map_raw, shadowUV,
penumbra_scale[v][i], smoothness_factors[v][i]);
return samplePoissonPCSS(shadow_map, shadowUV, radius);
}
float getShadowValue(sampler2DArrayShadow shadow_map, sampler2DArray shadow_map_raw, float depth, vec4 shadowUV[NUM_SHADOW_CASCADES],
vec4 cascade_distances[(NUM_SHADOW_CASCADES + 4 - 1) / 4], vec4 smoothness_factors[(NUM_SHADOW_CASCADES + 4 - 1) / 4],
vec4 penumbra_scale[(NUM_SHADOW_CASCADES + 4 - 1) / 4],
int cascade_offset, int cascade_count)
{
int cascade = cascade_offset + cascade_count;
@@ -52,19 +140,19 @@ float getShadowValue(sampler2DArrayShadow shadow_map, float depth, vec4 shadowUV
}
if (cascade >= cascade_offset + cascade_count || cascade < cascade_offset) return 1.0;
int cascade_v = cascade / 4;
int cascade_i = cascade % 4;
float cascade_start = (cascade > cascade_offset) ? cascade_distances[(cascade - 1) / 4][(cascade - 1) % 4] : 0.0;
float cascade_end = cascade_distances[cascade_v][cascade_i];
float cascade_end = cascade_distances[cascade / 4][cascade % 4];
float dist_threshold = (cascade_end - cascade_start) * 0.2;
if (cascade_end - dist_threshold > depth || cascade >= cascade_offset + cascade_count - 1)
return samplePoissonPCSS(shadow_map, shadowUV[cascade], smoothness_factors[cascade_v][cascade_i]);
return shadowVisibilityForCascade(shadow_map, shadow_map_raw, shadowUV[cascade], cascade,
smoothness_factors, penumbra_scale);
return mix(
samplePoissonPCSS(shadow_map, shadowUV[cascade], smoothness_factors[cascade_v][cascade_i]),
samplePoissonPCSS(shadow_map, shadowUV[cascade + 1], smoothness_factors[(cascade + 1) / 4][(cascade + 1) % 4]),
shadowVisibilityForCascade(shadow_map, shadow_map_raw, shadowUV[cascade], cascade,
smoothness_factors, penumbra_scale),
shadowVisibilityForCascade(shadow_map, shadow_map_raw, shadowUV[cascade + 1], cascade + 1,
smoothness_factors, penumbra_scale),
smoothstep(cascade_end - dist_threshold, cascade_end, depth));
}
// Raytraced shadow test via inline ray query against the shadow TLAS. Only
@@ -79,11 +167,11 @@ float getShadowValue(sampler2DArrayShadow shadow_map, float depth, vec4 shadowUV
// in deferred-f.sdr/main-f.sdr references it regardless of which shadow method is active.
const float RT_SHADOW_MAX_DISTANCE = 10000.0;
#if defined(MODEL_SDR_FLAG_RT_SHADOWS) || defined(RT_SHADOWS)
#if defined(MODEL_SDR_FLAG_RT_SHADOWS) || defined(RT_SHADOWS) || defined(RTAO)
#extension GL_EXT_ray_query : require
// Ray origin bias grows with distance from the camera, between the rtShadowBiasMin/
// rtShadowBiasMax bounds (see shadowCascadeParams uniform block, and the Min/Max
// rtShadowBiasMax bounds (see the shadowCascadeParams and globalDeferredData blocks, and the Min/Max
// Raytraced Shadow Bias options in shadows.cpp). Empirically, geometry close to the
// camera only needs biasMin worth of offset to clear self-intersection acne, while
// distant geometry needs more -- consistent with the error source being G-buffer/
@@ -100,6 +188,20 @@ float computeRtShadowBias(float cameraDist, float biasMin, float biasMax)
return mix(biasMin, biasMax, clamp(cameraDist / RT_SHADOW_BIAS_RAMP_DISTANCE, 0.0, 1.0));
}
// Traces one occlusion ray. traceShadowRay() and traceShadowRayCone() both use this function.
// The origin must already include the bias. The direction must be a unit vector. cullMask has
// the same meaning as in traceShadowRay(). Returns 1.0 when the ray reaches tMax with no hit, and
// 0.0 when it hits an instance.
float traceVisibilityRay(accelerationStructureEXT tlas, vec3 origin, vec3 direction, float tMax, uint cullMask)
{
rayQueryEXT rq;
rayQueryInitializeEXT(rq, tlas, gl_RayFlagsOpaqueEXT | gl_RayFlagsTerminateOnFirstHitEXT,
cullMask, origin, 0.001, direction, tMax);
while (rayQueryProceedEXT(rq)) {}
return (rayQueryGetIntersectionTypeEXT(rq, true) == gl_RayQueryCommittedIntersectionNoneEXT) ? 1.0 : 0.0;
}
// worldPos/worldNormal/worldLightDir must all be in the same world space the
// shadow TLAS was built in (see VulkanRaytracingManager::buildTlas()) -- i.e.
// FS2Open object-space positions/orientations, not view/eye-space. worldLightDir
@@ -117,20 +219,139 @@ float computeRtShadowBias(float cameraDist, float biasMin, float biasMax)
//
// cullMask selects which TLAS instances this ray can hit (Vulkan spec: an instance
// is a candidate iff (cullMask & instance.mask) != 0). Callers should pass
// shadow_ray_cull_mask (shadowCascadeParams uniform block) rather than a literal --
// the shadow ray cull mask from a uniform block rather than a literal --
// see SHADOW_RAY_CULL_MASK_EXCLUDE_VIEWER_HULL/TLAS_MASK_VIEWER_HULL (shadows.h)
// for how it excludes the viewer ship's own hull outside the cockpit pass.
float traceShadowRay(accelerationStructureEXT tlas, vec3 worldPos, vec3 worldNormal, vec3 worldLightDir, float tMax, float bias, uint cullMask)
{
return traceVisibilityRay(tlas, worldPos + worldNormal * bias, normalize(worldLightDir), tMax, cullMask);
}
// Penumbra/AO sampling: at most this many rays per fragment per light, matching the size of
// poissonDisc16 above (the RT cone/AO sampling below always wants the full 16-tap spread,
// independent of SAMPLES_PCSS/SMOOTH_PCSS -- see the comment on poissonDisc16). The
// rtShadowSampleCount and rtaoSampleCount uniforms are clamped against it, so a bad uniform value can't
// index out of the pattern.
#define RT_SHADOW_MAX_SAMPLES 16
// Orthonormal basis spanning the plane perpendicular to axis (a unit vector).
void rtBuildOnb(vec3 axis, out vec3 tangent, out vec3 bitangent)
{
vec3 up = (abs(axis.z) < 0.999) ? vec3(0.0, 0.0, 1.0) : vec3(1.0, 0.0, 0.0);
tangent = normalize(cross(up, axis));
bitangent = cross(axis, tangent);
}
// Interleaved gradient noise (Jimenez 2014) -> per-fragment [0,1) value used to
// rotate the shared disc pattern. Callers pass gl_FragCoord.xy as the seed; it is
// a parameter rather than read here because this file is also compiled in vertex
// stages, where gl_FragCoord doesn't exist.
float rtInterleavedGradientNoise(vec2 seed)
{
return fract(52.9829189 * fract(dot(seed, vec2(0.06711056, 0.00583715))));
}
// Soft (penumbra) variant of traceShadowRay: averages up to sampleCount occlusion
// rays whose directions are jittered inside a cone of half-angle
// atan(coneHalfAngleTan) around worldLightDir. Callers derive coneHalfAngleTan
// from the light's source radius -- for directional lights the light's
// source_radius IS the tangent of its angular radius (see $SunAngularSize in
// stars.tbl / starfield.cpp), for local lights it's sourceRadius / lightDist --
// so penumbras widen with occluder distance like a real area light's would.
//
// cullMask has the same meaning as in traceShadowRay().
//
// noiseSeed decorrelates the sample pattern between fragments (see
// rtInterleavedGradientNoise() for why it's a parameter). There is no temporal
// accumulation in the engine, so the penumbra must fully resolve in this one
// pass: the disc pattern is fixed and only its per-fragment rotation is
// randomized, trading a little visible grain for stable, flicker-free output.
//
// sampleCount <= 1 or coneHalfAngleTan <= 0.0 falls back to the single hard ray,
// bit-identical to traceShadowRay() -- that keeps the pre-penumbra look and cost
// as the default (mods opt in via source radii, users via the sample option).
float traceShadowRayCone(accelerationStructureEXT tlas, vec3 worldPos, vec3 worldNormal, vec3 worldLightDir,
float tMax, float bias, uint cullMask, float coneHalfAngleTan, int sampleCount, vec2 noiseSeed)
{
if (sampleCount <= 1 || coneHalfAngleTan <= 0.0) {
return traceShadowRay(tlas, worldPos, worldNormal, worldLightDir, tMax, bias, cullMask);
}
vec3 origin = worldPos + worldNormal * bias;
vec3 direction = normalize(worldLightDir);
rayQueryEXT rq;
rayQueryInitializeEXT(rq, tlas, gl_RayFlagsOpaqueEXT | gl_RayFlagsTerminateOnFirstHitEXT,
cullMask, origin, 0.001, direction, tMax);
while (rayQueryProceedEXT(rq)) {}
vec3 tangent, bitangent;
rtBuildOnb(direction, tangent, bitangent);
return (rayQueryGetIntersectionTypeEXT(rq, true) == gl_RayQueryCommittedIntersectionNoneEXT) ? 1.0 : 0.0;
float rotAngle = rtInterleavedGradientNoise(noiseSeed) * 6.2831853;
float cosR = cos(rotAngle);
float sinR = sin(rotAngle);
int samples = min(sampleCount, RT_SHADOW_MAX_SAMPLES);
float visibility = 0.0;
for (int i = 0; i < samples; ++i) {
vec2 p = poissonDisc16[i];
p = vec2(p.x * cosR - p.y * sinR, p.x * sinR + p.y * cosR);
vec3 sampleDir = normalize(direction + (tangent * p.x + bitangent * p.y) * coneHalfAngleTan);
visibility += traceVisibilityRay(tlas, origin, sampleDir, tMax, cullMask);
}
return visibility / float(samples);
}
// Raytraced ambient occlusion: averages up to sampleCount occlusion rays of
// length `radius` over the hemisphere around worldNormal and returns ambient
// visibility in [0,1] (1 = fully open), shaped by pow(visibility, strength).
// Mapping the shared disc pattern up onto the hemisphere (z = sqrt(1-|p|^2))
// makes the samples cosine-weighted, matching the diffuse ambient term being
// occluded. radius/strength come from the mod's lighting profile ($RTAO Radius /
// $RTAO Strength); scene scale varies too much (fighters vs. capships) for a
// built-in constant.
//
// Hits accumulate with a (1 - t/radius) falloff so geometry entering the radius
// occludes gradually instead of popping. That needs the *closest* hit's t, so
// these rays deliberately omit gl_RayFlagsTerminateOnFirstHitEXT (any-hit t
// would make the falloff arbitrary); the extra traversal cost is small because
// tMax = radius keeps the rays short.
//
// sampleCount < 1, radius <= 0, or strength <= 0 returns 1.0 (no occlusion) --
// callers gate on the RTAO shader variant, this just keeps bad uniforms benign.
float traceAmbientOcclusion(accelerationStructureEXT tlas, vec3 worldPos, vec3 worldNormal,
float radius, float strength, int sampleCount, float bias, uint cullMask, vec2 noiseSeed)
{
if (sampleCount < 1 || radius <= 0.0 || strength <= 0.0) {
return 1.0;
}
vec3 origin = worldPos + worldNormal * bias;
vec3 tangent, bitangent;
rtBuildOnb(worldNormal, tangent, bitangent);
float rotAngle = rtInterleavedGradientNoise(noiseSeed) * 6.2831853;
float cosR = cos(rotAngle);
float sinR = sin(rotAngle);
int samples = min(sampleCount, RT_SHADOW_MAX_SAMPLES);
float occlusion = 0.0;
for (int i = 0; i < samples; ++i) {
vec2 p = poissonDisc16[i];
p = vec2(p.x * cosR - p.y * sinR, p.x * sinR + p.y * cosR);
vec3 sampleDir = normalize(tangent * p.x + bitangent * p.y
+ worldNormal * sqrt(max(1.0 - dot(p, p), 0.0)));
rayQueryEXT rq;
rayQueryInitializeEXT(rq, tlas, gl_RayFlagsOpaqueEXT,
cullMask, origin, 0.001, sampleDir, radius);
while (rayQueryProceedEXT(rq)) {}
if (rayQueryGetIntersectionTypeEXT(rq, true) != gl_RayQueryCommittedIntersectionNoneEXT) {
float hitT = rayQueryGetIntersectionTEXT(rq, true);
occlusion += 1.0 - clamp(hitT / radius, 0.0, 1.0);
}
}
float visibility = clamp(1.0 - occlusion / float(samples), 0.0, 1.0);
return pow(visibility, strength);
}
#endif
+3 -2
View File
@@ -1212,17 +1212,18 @@ void fireball_render(object* obj, model_draw_list *scene)
Neb2_fog_visibility_fireball_const + (obj->radius * Neb2_fog_visibility_fireball_scaled_factor));
g3_transfer_vertex(&p, &obj->pos);
const vec3d motion = obj->pos - obj->last_pos;
switch ( fb->fireball_render_type ) {
case FIREBALL_MEDIUM_EXPLOSION: {
batching_add_volume_bitmap(fb->current_bitmap, &p, fb->orient, obj->radius, alpha);
batching_add_volume_bitmap(fb->current_bitmap, &p, fb->orient, obj->radius, alpha, 0.0f, &motion);
}
break;
case FIREBALL_LARGE_EXPLOSION: {
// Make the big explosions rotate with the viewer.
batching_add_volume_bitmap_rotated(fb->current_bitmap, &p, (i2fl(fb->orient)*PI) / 180.0f, obj->radius, alpha);
batching_add_volume_bitmap_rotated(fb->current_bitmap, &p, (i2fl(fb->orient)*PI) / 180.0f, obj->radius, alpha, 0.0f, &motion);
}
break;
+2
View File
@@ -788,6 +788,8 @@ void event_music_level_close()
}
else
audiostream_close_file( Patterns[i].handle, 0 );
Patterns[i].handle = -1;
}
} else {
// close em all down then
+122
View File
@@ -37,6 +37,7 @@
#include "model/modelrender.h"
#include "libs/jansson.h"
#include "options/Option.h"
#include "options/OptionsManager.h"
#include "osapi/osapi.h"
#include "parse/parselo.h"
#include "popup/popup.h"
@@ -85,6 +86,8 @@ gr_capability_def gr_capabilities[] = {
GR_CAPABILITY_ENTRY(INSTANCED_RENDERING),
GR_CAPABILITY_ENTRY(FAST_SHADOWS),
GR_CAPABILITY_ENTRY(RAYTRACED_SHADOWS),
GR_CAPABILITY_ENTRY(SHADOW_CONTACT_HARDENING),
GR_CAPABILITY_ENTRY(FSR),
};
const size_t gr_capabilities_num = sizeof(gr_capabilities) / sizeof(gr_capabilities[0]);
@@ -874,6 +877,123 @@ static auto AAOption __UNUSED = options::OptionBuilder<AntiAliasMode>("Graphics.
.parser(parse_anti_aliasing_func)
.finish();
FsrMode Gr_fsr_mode = FsrMode::Off;
int Gr_fsr_major_version = 0;
SCP_vector<SCP_string> Gr_fsr_versions;
int Gr_fsr_max_output_size = 0;
float Gr_composition_scale = 1.0f;
static float fsr_window_factor()
{
if (!Cmdline_window_res.has_value()) {
return 1.0f;
}
const float factor = std::max(i2fl(Cmdline_window_res->first) / i2fl(gr_screen.max_w),
i2fl(Cmdline_window_res->second) / i2fl(gr_screen.max_h));
if (factor <= 1.0f || std::max(gr_screen.max_w, gr_screen.max_h) * factor > Gr_fsr_max_output_size) {
return 1.0f;
}
return factor;
}
float gr_fsr_upscale_factor(FsrMode mode)
{
return mode == FsrMode::Upscale ? fsr_window_factor() : 1.0f;
}
static int fsr_setting(FsrMode mode, int major_version)
{
return mode == FsrMode::Off ? 0 : major_version * 10 + static_cast<int>(mode);
}
static SCP_vector<int> fsr_setting_enumerator()
{
SCP_vector<int> out{0};
// Options are also enumerated before gr_init has set up the renderer.
if (gr_screen.gf_is_capable && gr_is_capable(gr_capability::CAPABILITY_FSR)) {
for (const auto& version : Gr_fsr_versions) {
const int major = atoi(version.c_str());
out.push_back(fsr_setting(FsrMode::AA, major));
if (fsr_window_factor() > 1.0f) {
out.push_back(fsr_setting(FsrMode::Upscale, major));
}
}
}
return out;
}
static SCP_string fsr_setting_display(int setting)
{
const auto mode = static_cast<FsrMode>(setting % 10);
if (mode == FsrMode::Off) {
return "Off";
}
SCP_string label = "FSR";
for (const auto& name : Gr_fsr_versions) {
if (atoi(name.c_str()) == setting / 10) {
label += " " + name;
break;
}
}
return label + (mode == FsrMode::AA ? " AA" : " Upscale");
}
static void fsr_apply_setting(int setting)
{
const int mode = setting % 10;
Gr_fsr_mode = mode <= static_cast<int>(FsrMode::Upscale) ? static_cast<FsrMode>(mode) : FsrMode::Off;
Gr_fsr_major_version = setting / 10;
}
// -fsr only names a mode, and what of it can run is only known once the renderer is up. Settle it on the closest
// setting that is offered (the newest version, Upscale dropping to AA, or Off), so that both the FSR state and the
// locked option show what actually runs.
static void fsr_resolve_cmdline_setting()
{
const auto options = options::OptionsManager::instance();
if (options->getOverrideReason("Graphics.FSR") != "-fsr") {
return;
}
int resolved = 0;
if (Gr_fsr_mode != FsrMode::Off) {
// The offered settings are ordered newest version first
const auto offered = fsr_setting_enumerator();
const auto newest = [&offered](FsrMode mode) {
const auto it = std::find_if(offered.begin(), offered.end(), [mode](int setting) { return setting % 10 == static_cast<int>(mode); });
return it == offered.end() ? 0 : *it;
};
resolved = newest(Gr_fsr_mode);
if (resolved == 0) {
// Upscale is only offered with a larger window
resolved = newest(FsrMode::AA);
}
}
fsr_apply_setting(resolved);
options->setOverride("Graphics.FSR", std::unique_ptr<json_t>(json_integer(resolved)), "-fsr");
}
// coverity[GLOBAL_INIT_ORDER] -- safe; OptionBuilder::finish() uses Meyers singleton
static auto FsrOption __UNUSED = options::OptionBuilder<int>("Graphics.FSR",
std::pair<const char*, int>{"AMD FSR", -1},
std::pair<const char*, int>{"AMD's temporal anti aliasing and upscaling. AA keeps the rendering resolution. Upscale renders the 3D scene at the configured resolution and upscales it to the window/monitor resolution.", -1})
.category(std::make_pair("Graphics", 1825))
.level(options::ExpertLevel::Advanced)
.enumerator(fsr_setting_enumerator)
.display(fsr_setting_display)
.flags({options::OptionFlags::ForceMultiValueSelection})
.default_func([]() { return fsr_setting(Gr_fsr_mode, Gr_fsr_major_version); } )
.change_listener([](const int& setting, bool) {
fsr_apply_setting(setting);
return true;
})
.importance(77)
.finish();
extern int Cmdline_msaa_enabled;
static void parse_msaa_func()
@@ -2320,6 +2440,8 @@ bool gr_init(std::unique_ptr<os::GraphicsOperations>&& graphicsOps, GraphicsAPI
return false;
}
fsr_resolve_cmdline_setting();
gr_set_palette_internal(Gr_current_palette_name, NULL, 0);
bm_init();
+32 -10
View File
@@ -68,6 +68,27 @@ enum class AntiAliasMode {
extern AntiAliasMode Gr_aa_mode;
extern AntiAliasMode Gr_aa_mode_last_frame;
// AA renders at 1:1. Upscale upscales the scene from the render resolution to the window resolution, and everything
// drawn after it then draws at that size.
enum class FsrMode {
Off = 0,
AA = 1,
Upscale = 2,
};
extern FsrMode Gr_fsr_mode;
// The chosen FSR major version, or 0 for the newest the renderer offers
extern int Gr_fsr_major_version;
// The FSR versions the renderer offers, newest first, such as "4.1.2". Empty without FSR.
extern SCP_vector<SCP_string> Gr_fsr_versions;
// The largest image side the renderer supports, which limits the upscale factor. 0 without FSR.
extern int Gr_fsr_max_output_size;
// Scales the render resolution to cover the window in both axes, keeping its aspect ratio; the final blit squeezes the
// rest. 1 for Off and AA, and when the window isn't larger than the render resolution.
float gr_fsr_upscale_factor(FsrMode mode);
// Framebuffer pixels per gr_screen unit for what's drawn after the scene. Above 1 only while FSR upscales.
extern float Gr_composition_scale;
bool gr_is_fxaa_mode(AntiAliasMode mode);
bool gr_is_smaa_mode(AntiAliasMode mode);
@@ -232,6 +253,8 @@ enum shader_type {
SDR_TYPE_GAMMA_BLIT,
SDR_TYPE_MOTION_VECTORS,
NUM_SHADER_TYPES
};
@@ -262,6 +285,7 @@ enum shader_type {
#define SDR_FLAG_ENV_MAP (1 << 0)
#define SDR_FLAG_DEFERRED_RT_SHADOWS (1 << 1)
#define SDR_FLAG_DEFERRED_RTAO (1 << 2)
#define SDR_FLAG_SHADOW_FALLBACK (1 << 0)
@@ -301,6 +325,7 @@ struct vertex_format_data
RADIUS,
UVEC,
MATRIX4,
MOTION4, // Vulkan only. OpenGL's attribute tables have no slot for it.
};
vertex_format format_type;
@@ -370,7 +395,13 @@ enum class gr_capability {
CAPABILITY_INSTANCED_RENDERING,
CAPABILITY_FAST_SHADOWS,
CAPABILITY_QUERIES_REUSABLE,
CAPABILITY_RAYTRACED_SHADOWS
CAPABILITY_RAYTRACED_SHADOWS,
// A second, non-compare sampler bound to the shadow map for raw depth reads, needed by
// the shadow-map PCSS blocker search. Vulkan always has this (samplers are independent
// of images there); OpenGL needs GL 3.3 (glGenSamplers/glBindSampler), since the compare mode is
// otherwise texture-object state shared by every sampler bound to that texture.
CAPABILITY_SHADOW_CONTACT_HARDENING,
CAPABILITY_FSR
};
struct gr_capability_def {
@@ -879,13 +910,6 @@ typedef struct screen {
std::function<void(int pm_id)> gf_model_loaded = [](int) {};
std::function<void(int pm_id)> gf_model_unloaded = [](int) {};
// Rebuilds the raytraced-shadow top-level acceleration structure for the
// current frame from the live shadow-casting object set. Called once per
// frame from shadows_render_all(), alongside the cascaded shadow map pass --
// the shading pass picks between the two per shadows_use_raytracing().
// No-op default.
std::function<void()> gf_build_shadow_tlas = []() {};
std::function<void(gr_buffer_handle handle, size_t size, const void* data)> gf_update_buffer_data;
std::function<void(gr_buffer_handle handle, size_t offset, size_t size, const void* data)>
gf_update_buffer_data_offset;
@@ -1327,8 +1351,6 @@ inline void gr_model_unloaded(int pm_id)
gr_screen.gf_model_unloaded(pm_id);
}
#define gr_build_shadow_tlas GR_CALL(gr_screen.gf_build_shadow_tlas)
#define gr_update_buffer_data GR_CALL(gr_screen.gf_update_buffer_data)
#define gr_update_buffer_data_offset GR_CALL(gr_screen.gf_update_buffer_data_offset)
inline void* gr_map_buffer(gr_buffer_handle handle)
+10
View File
@@ -79,6 +79,8 @@ public:
vec4 Clr;
float Clr_scale;
int Depth_bias;
bool Velocity_output = false;
bool Texture_lod_bias = true;
bvec4 Color_mask;
bool Stencil_test = false;
uint32_t Stencil_mask = 0xFF;
@@ -131,6 +133,14 @@ public:
void set_depth_bias(int bias);
int get_depth_bias() const;
// Whether this writes its own motion vectors when the target has a place for them
void set_velocity_output(bool output) { Velocity_output = output; }
bool get_velocity_output() const { return Velocity_output; }
// Whether textures take the LOD bias of temporal upscaling, which sharpens what it reconstructs
void set_texture_lod_bias(bool bias) { Texture_lod_bias = bias; }
bool get_texture_lod_bias() const { return Texture_lod_bias; }
void set_color(float red, float green, float blue, float alpha);
void set_color(int r, int g, int b, int a);
void set_color(const color &clr_in);
+92
View File
@@ -31,6 +31,98 @@ static bool matrix_uniform_up_to_date = false;
static bool gr_ortho_override_active = false;
static float gr_ortho_override_distance = 0.0f;
bool Gr_motion_vectors_active = false;
namespace {
struct velocity_camera {
matrix4 view_proj;
matrix4 previous_view_proj;
void capture()
{
previous_view_proj = view_proj;
vm_matrix4_x_matrix4(&view_proj, &gr_projection_matrix, &gr_view_matrix);
}
};
}
static velocity_camera Scene_camera;
static velocity_camera Cockpit_camera;
static const velocity_camera* Current_velocity_camera = &Scene_camera;
static float Scene_viewport_rect[4] = {0.0f, 0.0f, 1.0f, 1.0f};
static bool Temporal_reset_requested = true;
void gr_reset_temporal_history()
{
Temporal_reset_requested = true;
}
bool gr_consume_temporal_reset()
{
const bool requested = Temporal_reset_requested;
Temporal_reset_requested = false;
return requested;
}
static matrix4 Cockpit_last_view_proj;
static vec3d Cockpit_sway = vmd_zero_vector;
static vec3d Cockpit_last_sway = vmd_zero_vector;
void gr_capture_scene_camera()
{
Scene_camera.capture();
Cockpit_last_view_proj = Cockpit_camera.view_proj;
Cockpit_last_sway = Cockpit_sway;
Scene_viewport_rect[0] = i2fl(gr_screen.offset_x) / i2fl(gr_screen.max_w);
Scene_viewport_rect[1] = i2fl(gr_screen.offset_y) / i2fl(gr_screen.max_h);
Scene_viewport_rect[2] = i2fl(gr_screen.clip_width) / i2fl(gr_screen.max_w);
Scene_viewport_rect[3] = i2fl(gr_screen.clip_height) / i2fl(gr_screen.max_h);
}
void gr_begin_cockpit_camera(const matrix* ship_orient, const matrix* ship_last_orient, const vec3d* sway)
{
vm_matrix4_x_matrix4(&Cockpit_camera.view_proj, &gr_projection_matrix, &gr_view_matrix);
Cockpit_sway = *sway;
// Everything drawn here hangs off the ship's rotation plus the sway offset, so last frame's position of a point is
// its current one moved by the change of that frame.
matrix current = *ship_orient;
matrix last = *ship_last_orient;
matrix4 current_frame, inverse_current, last_frame, frame_change;
vm_matrix4_set_transform(&current_frame, &current, &Cockpit_sway);
vm_matrix4_set_transform(&last_frame, &last, &Cockpit_last_sway);
vm_inverse_matrix4(&inverse_current, &current_frame);
vm_matrix4_x_matrix4(&frame_change, &last_frame, &inverse_current);
vm_matrix4_x_matrix4(&Cockpit_camera.previous_view_proj, &Cockpit_last_view_proj, &frame_change);
Current_velocity_camera = &Cockpit_camera;
}
void gr_end_cockpit_camera()
{
Current_velocity_camera = &Scene_camera;
}
void gr_get_velocity_camera(matrix4* view_proj, matrix4* previous_view_proj, float viewport_rect[4])
{
*view_proj = Current_velocity_camera->view_proj;
*previous_view_proj = Current_velocity_camera->previous_view_proj;
memcpy(viewport_rect, Scene_viewport_rect, sizeof(Scene_viewport_rect));
}
void gr_get_velocity_uniforms(graphics::velocity_camera_data* out)
{
// Only shaders writing motion vectors read this, and those only run in a scene that has them
if (!Gr_motion_vectors_active) {
return;
}
out->viewProj = Current_velocity_camera->view_proj;
out->prevViewProj = Current_velocity_camera->previous_view_proj;
out->uvScale[0] = Scene_viewport_rect[2];
out->uvScale[1] = Scene_viewport_rect[3];
}
static matrix4 create_view_matrix(const vec3d* pos, const matrix* orient)
{
vec3d scaled_pos;
+20
View File
@@ -3,6 +3,10 @@
#include "graphics/2d.h"
#include "render/3d.h"
namespace graphics {
struct velocity_camera_data;
}
extern transform_stack gr_model_matrix_stack;
extern matrix4 gr_view_matrix;
extern matrix4 gr_model_view_matrix;
@@ -11,6 +15,22 @@ extern matrix4 gr_last_projection_matrix;
extern matrix4 gr_env_texture_matrix;
extern float gr_near_plane;
// True while the current scene writes motion vectors. Only ever set by a renderer that supports them.
extern bool Gr_motion_vectors_active;
// Records the current view and projection as the main scene's camera, once per rendered scene.
void gr_capture_scene_camera();
// The cockpit has its own near plane, FOV and frame, so draws between these use a separate camera. It's drawn around the
// origin at offsets that turn with the ship and move with its sway, so the camera needs both, and the orientation last frame.
void gr_begin_cockpit_camera(const matrix* ship_orient, const matrix* ship_last_orient, const vec3d* sway);
void gr_end_cockpit_camera();
// The camera that draws write motion vectors against, viewport_rect being xy offset and zw size as fractions of the target.
void gr_get_velocity_camera(matrix4* view_proj, matrix4* previous_view_proj, float viewport_rect[4]);
void gr_get_velocity_uniforms(graphics::velocity_camera_data* out);
// The camera moved discontinuously, so temporal effects must not reuse earlier frames.
void gr_reset_temporal_history();
bool gr_consume_temporal_reset();
void gr_matrix_on_frame();
void gr_start_instance_matrix(const vec3d* offset, const matrix* rotation);
+14
View File
@@ -1631,6 +1631,20 @@ bool gr_opengl_is_capable(gr_capability capability)
case gr_capability::CAPABILITY_RAYTRACED_SHADOWS:
// Raytraced shadows are only implemented for the Vulkan backend.
return false;
case gr_capability::CAPABILITY_SHADOW_CONTACT_HARDENING:
// Needs a second sampler object (compare mode off) on the shadow map. Sampler
// objects are technically also available pre-3.3 via ARB_sampler_objects, but this
// project's glad build only wires glGenSamplers/glBindSampler/glSamplerParameteri
// up behind the core GL_VERSION_3_3 flag, not a separate ARB entry point -- so that's
// the check that actually reflects whether those functions are non-null here.
#ifdef USE_OPENGL_ES
// GLES has no GLAD_GL_VERSION_3_3 symbol, and the sampler setup here is desktop GL only.
return false;
#else
return GLAD_GL_VERSION_3_3 != 0;
#endif
case gr_capability::CAPABILITY_FSR:
return false;
}
+4 -1
View File
@@ -253,6 +253,9 @@ void gr_opengl_deferred_lighting_finish()
GL_state.Texture.Enable(3, GL_TEXTURE_2D, Scene_specular_texture);
if (Shadow_quality != ShadowQuality::Disabled) {
GL_state.Texture.Enable(4, GL_TEXTURE_2D_ARRAY, Shadow_map_depth_texture);
if (Shadow_map_raw_sampler) {
GL_state.Texture.Enable(SHADOW_MAP_RAW_TEXTURE_UNIT, GL_TEXTURE_2D_ARRAY, Shadow_map_depth_texture, Shadow_map_raw_sampler);
}
}
if (ENVMAP > 0) {
@@ -314,8 +317,8 @@ void gr_opengl_deferred_lighting_finish()
auto lp = ltp::current();
auto header = light_uniform_aligner.getHeader<deferred_global_data>();
shadows_fill_deferred_global_data(*header);
if (Shadow_quality != ShadowQuality::Disabled) {
vm_inverse_matrix4(&header->inv_view_matrix, &Shadow_view_matrix_render);
shadow_cascade_params_bind_deferred();
}
+1
View File
@@ -433,6 +433,7 @@ static void opengl_set_default_uniforms(const opengl_shader_t& sdr) {
Current_shader->program->Uniforms.setTextureUniform("PositionBuffer", 2);
Current_shader->program->Uniforms.setTextureUniform("SpecBuffer", 3);
Current_shader->program->Uniforms.setTextureUniform("shadow_map", 4);
Current_shader->program->Uniforms.setTextureUniform("shadow_map_raw", SHADOW_MAP_RAW_TEXTURE_UNIT);
break;
case SDR_TYPE_PASSTHROUGH_RENDER:
+25 -7
View File
@@ -37,6 +37,7 @@ void opengl_texture_state::init(GLuint n_units)
for (unsigned int unit = 0; unit < num_texture_units; unit++) {
units[unit].enabled = GL_FALSE;
units[unit].bound_sampler = 0;
default_values(unit);
@@ -93,17 +94,20 @@ void opengl_texture_state::Enable(GLuint tex_id)
return;
}
if (units[active_texture_unit].texture_id != tex_id) {
glBindTexture(units[active_texture_unit].texture_target, tex_id);
units[active_texture_unit].texture_id = tex_id;
}
glBindTexture(units[active_texture_unit].texture_target, tex_id);
units[active_texture_unit].texture_id = tex_id;
// A sampler that an earlier bind set belongs to the earlier texture. No GL call when
// the unit has no sampler.
BindSampler(active_texture_unit, 0);
}
void opengl_texture_state::Enable(GLuint unit, GLenum tex_target, GLuint tex_id) {
void opengl_texture_state::Enable(GLuint unit, GLenum tex_target, GLuint tex_id, GLuint sampler) {
Assertion(unit < num_texture_units, "Invalid texture unit value!");
if (units[unit].texture_target == tex_target && units[unit].texture_id == tex_id) {
// The texture unit already uses this texture. There is no need to change it
if (units[unit].texture_target == tex_target && units[unit].texture_id == tex_id
&& units[unit].bound_sampler == sampler) {
// The texture unit already uses this texture and sampler. There is no need to change it
return;
}
@@ -111,6 +115,19 @@ void opengl_texture_state::Enable(GLuint unit, GLenum tex_target, GLuint tex_id)
SetActiveUnit(unit);
SetTarget(tex_target);
Enable(tex_id);
BindSampler(unit, sampler);
}
void opengl_texture_state::BindSampler(GLuint unit, GLuint sampler)
{
Assertion(unit < num_texture_units, "Invalid texture unit value!");
if (units[unit].bound_sampler == sampler) {
return;
}
glBindSampler(unit, sampler);
units[unit].bound_sampler = sampler;
}
void opengl_texture_state::Delete(GLuint tex_id)
@@ -128,6 +145,7 @@ void opengl_texture_state::Delete(GLuint tex_id)
glBindTexture(units[i].texture_target, 0);
units[i].texture_id = 0;
BindSampler(i, 0);
default_values(i, units[i].texture_target);
+28 -2
View File
@@ -27,6 +27,12 @@ struct opengl_texture_unit {
GLenum texture_target;
GLuint texture_id;
// Sampler object bound to this unit, overriding the texture object's own filter/compare
// state for as long as it stays bound (0 = none, texture-level state applies as usual).
// Tracked here for the same reason texture_id is: so BindSampler() can skip a redundant
// glBindSampler call, exactly like Enable() already does for glBindTexture.
GLuint bound_sampler;
};
class opengl_texture_state
@@ -59,13 +65,33 @@ class opengl_texture_state
* @warning This may not actually set the active texture unit to the specified value! If you require that then
* you need to call SetActiveUnit!
*
* The sampler object is part of the unit binding: each call also sets it. So a later bind of
* a different texture on this unit removes a sampler that an earlier bind set.
*
* @param unit The texture unit to use
* @param tex_target The texture target of the texture id
* @param tex_id The ID of the texture to enable
* @param sampler The sampler object for this unit, or 0 for the texture's own sampling state
*/
void Enable(GLuint unit, GLenum tex_target, GLuint tex_id);
void Enable(GLuint unit, GLenum tex_target, GLuint tex_id, GLuint sampler = 0);
void Delete(GLuint tex_id);
/**
* @brief Binds a sampler object to the specified unit, or unbinds one if sampler is 0
*
* Prefer the sampler argument of Enable(). Use this function directly only to clear a
* sampler before it is deleted.
*
* Sampler objects override the bound texture's own filter/compare-mode state for
* that unit only, which is otherwise impossible to vary per-unit in this codebase
* (texture parameters like GL_TEXTURE_COMPARE_MODE live on the texture object, not
* a sampler). No-ops if the unit already has this sampler bound, mirroring Enable().
*
* @param unit The texture unit to bind the sampler to
* @param sampler The sampler object to bind, or 0 to unbind
*/
void BindSampler(GLuint unit, GLuint sampler);
inline GLenum GetTarget();
inline void SetShaderMode(GLboolean mode);
};
+39 -19
View File
@@ -79,6 +79,7 @@ GLint GL_max_elements_vertices = 4096;
GLint GL_max_elements_indices = 4096;
GLuint Shadow_map_depth_texture = 0;
GLuint Shadow_map_raw_sampler = 0;
GLuint shadow_fbo = 0;
int Shadow_texture_size = 0;
@@ -514,6 +515,24 @@ static bool opengl_init_shadow_framebuffer(int size)
mprintf(("Shadow framebuffer created successfully.\n"));
Shadow_texture_size = size;
Shadow_texture_layers = Num_shadow_cascades + Num_cockpit_shadow_cascades;
// Second sampler for Shadow_map_depth_texture, compare mode off, so PCSS blocker
// search (shadows.sdr) can read raw depth off the same texture that the compare
// sampler reads filtered visibility off -- see shadow_contact_hardening_supported()
// for why this needs a sampler object rather than a second glTexParameteri call.
if (shadow_contact_hardening_supported()) {
glGenSamplers(1, &Shadow_map_raw_sampler);
// GL_NEAREST: the blocker search wants the actual stored per-texel depth, not a
// hardware-filtered blend across the compare boundary -- deliberately different
// from the compare sampler's GL_LINEAR.
glSamplerParameteri(Shadow_map_raw_sampler, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glSamplerParameteri(Shadow_map_raw_sampler, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
glSamplerParameteri(Shadow_map_raw_sampler, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glSamplerParameteri(Shadow_map_raw_sampler, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
glSamplerParameteri(Shadow_map_raw_sampler, GL_TEXTURE_WRAP_R, GL_CLAMP_TO_EDGE);
glSamplerParameteri(Shadow_map_raw_sampler, GL_TEXTURE_COMPARE_MODE, GL_NONE);
}
return true;
}
@@ -565,24 +584,7 @@ void opengl_tnl_init()
Transform_buffer_handle = opengl_create_texture_buffer_object();
if (Shadow_quality != ShadowQuality::Disabled) {
int size;
switch (Shadow_quality) {
case ShadowQuality::Low:
size = 512;
break;
case ShadowQuality::Medium:
size = 1024;
break;
case ShadowQuality::High:
size = 2048;
break;
case ShadowQuality::Ultra:
size = 4096;
break;
default:
size = 256;
break;
}
const int size = shadows_map_resolution();
if (!opengl_init_shadow_framebuffer(size)) {
mprintf(("Failed to create either shadow framebuffer. Disabling shadow support.\n"));
@@ -608,6 +610,15 @@ void opengl_tnl_shutdown()
shadow_cascade_params_shutdown();
if ( Shadow_map_raw_sampler ) {
// Clear the tracked bindings before deleting -- glDeleteSamplers silently unbinds
// the sampler from every unit on the GL side, which would leave GL_state's tracking
// stale (BindSampler() would then skip a rebind of a since-deleted sampler name).
GL_state.Texture.BindSampler(SHADOW_MAP_RAW_TEXTURE_UNIT, 0);
glDeleteSamplers(1, &Shadow_map_raw_sampler);
Shadow_map_raw_sampler = 0;
}
if ( Shadow_map_depth_texture ) {
glDeleteTextures(1, &Shadow_map_depth_texture);
Shadow_map_depth_texture = 0;
@@ -960,8 +971,14 @@ void opengl_tnl_set_model_material(model_material *material_info)
Current_shader->program->Uniforms.setTextureUniform("sAmbientmap", 6);
if (setAllUniforms || (flags & MODEL_SDR_FLAG_MISC))
Current_shader->program->Uniforms.setTextureUniform("sMiscmap", 7);
if (setAllUniforms || (flags & MODEL_SDR_FLAG_SHADOWS))
if (setAllUniforms || (flags & MODEL_SDR_FLAG_SHADOWS)) {
Current_shader->program->Uniforms.setTextureUniform("shadow_map", 8);
// SHADOW_MAP_RAW_TEXTURE_UNIT is reserved for this -- see the raw-sampler bind alongside
// "shadow_map" below. Set unconditionally (even when contact hardening is unsupported) for the
// same AMD unbound-uniform reason as the rest of this block; the shader's own
// penumbra_scale sentinel check is what prevents the read at runtime.
Current_shader->program->Uniforms.setTextureUniform("shadow_map_raw", SHADOW_MAP_RAW_TEXTURE_UNIT);
}
Current_shader->program->Uniforms.setTextureUniform("sFramebuffer", 9);
if (setAllUniforms || (flags & MODEL_SDR_FLAG_TRANSFORM))
Current_shader->program->Uniforms.setTextureUniform("transform_tex", 10);
@@ -1044,6 +1061,9 @@ void opengl_tnl_set_model_material(model_material *material_info)
if (material_info->is_shadow_receiving()) {
GL_state.Texture.Enable(8, GL_TEXTURE_2D_ARRAY, Shadow_map_depth_texture);
if (Shadow_map_raw_sampler) {
GL_state.Texture.Enable(SHADOW_MAP_RAW_TEXTURE_UNIT, GL_TEXTURE_2D_ARRAY, Shadow_map_depth_texture, Shadow_map_raw_sampler);
}
}
if (material_info->get_animated_effect() >= 0) {
+9
View File
@@ -35,6 +35,15 @@ extern float shadow_fardist;
extern GLuint Shadow_map_depth_texture;
// Second sampler object bound to Shadow_map_depth_texture, compare mode off, for the raw
// (uncompared) depth reads PCSS blocker search needs. Only created when
// shadow_contact_hardening_supported() is true; 0 otherwise. See gropengltnl.cpp.
extern GLuint Shadow_map_raw_sampler;
// The texture unit of the "shadow_map_raw" sampler, in the model shaders and in the deferred
// lighting shader. Unit 7 is the model misc map, so the raw sampler must not use it.
constexpr GLuint SHADOW_MAP_RAW_TEXTURE_UNIT = 11;
struct opengl_vertex_bind {
vertex_format_data::vertex_format format;
GLint size;
+2 -1
View File
@@ -39,7 +39,8 @@ void PathRenderer::beginFrame() {
Assertion(!m_inFrame,
"beginFrame() was called withoug ending previous frame! Check your path renderer calls or get a coder!");
nvgBeginFrame(m_context, gr_screen.max_w, gr_screen.max_h, 1.0f);
// The pixel ratio makes text and curves rasterize at the resolution they're drawn at
nvgBeginFrame(m_context, gr_screen.max_w, gr_screen.max_h, Gr_composition_scale);
m_inFrame = true;
}
+25 -6
View File
@@ -11,13 +11,17 @@ class model_batch_buffer
SCP_vector<matrix4> Submodel_matrices;
size_t Current_offset = 0;
// While motion vectors are active, each batch's matrices are followed by the same number of previous-frame ones.
int Prev_stride = 0;
public:
void reset();
size_t get_buffer_offset() const;
int get_prev_stride() const;
void set_num_models(int n_models);
void set_model_transform(const matrix4 &transform, int model_id);
void set_prev_model_transform(const matrix4 &transform, int model_id);
void submit_buffer_data();
@@ -30,6 +34,7 @@ protected:
SCP_vector<DrawEntryT> _elements;
SCP_vector<int> _keys;
transform_stack _transforms;
transform_stack _prev_transforms; // the same stack, as it stood last frame
scene_lights _lights;
graphics::util::UniformBuffer _dataBuffer;
vec3d _scale;
@@ -49,6 +54,7 @@ public:
_elements.clear();
_keys.clear();
_transforms.clear();
_prev_transforms.clear();
_scale.xyz.x = 1.0f;
_scale.xyz.y = 1.0f;
_scale.xyz.z = 1.0f;
@@ -59,11 +65,19 @@ public:
void push_transform(const vec3d* pos, const matrix* orient)
{
_transforms.push(pos, orient);
_prev_transforms.push(pos, orient);
}
void push_transform(const vec3d* pos, const matrix* orient, const vec3d* prev_pos, const matrix* prev_orient)
{
_transforms.push(pos, orient);
_prev_transforms.push(prev_pos, prev_orient);
}
void pop_transform()
{
_transforms.pop();
_prev_transforms.pop();
}
const matrix4& get_transform() const
@@ -74,6 +88,7 @@ public:
void clear_transforms()
{
_transforms.clear();
_prev_transforms.clear();
}
void set_scale(const vec3d* s)
@@ -94,15 +109,19 @@ public:
void add_submodel_to_batch(int model_num)
{
matrix4 transform = _transforms.get_transform();
const auto batch_matrix = [this](const transform_stack& stack) {
matrix4 transform = stack.get_transform();
vm_vec_scale(&transform.vec.rvec, _scale.xyz.x);
vm_vec_scale(&transform.vec.uvec, _scale.xyz.y);
vm_vec_scale(&transform.vec.fvec, _scale.xyz.z);
vm_vec_scale(&transform.vec.rvec, _scale.xyz.x);
vm_vec_scale(&transform.vec.uvec, _scale.xyz.y);
vm_vec_scale(&transform.vec.fvec, _scale.xyz.z);
transform.a1d[15] = 0.0f;
transform.a1d[15] = 0.0f;
return transform;
};
_batchBuffer.set_model_transform(transform, model_num);
_batchBuffer.set_model_transform(batch_matrix(_transforms), model_num);
_batchBuffer.set_prev_model_transform(batch_matrix(_prev_transforms), model_num);
}
void init_render(bool sort = true)
+50
View File
@@ -0,0 +1,50 @@
#include "graphics/rtao.h"
#include "graphics/2d.h"
#include "options/Option.h"
int Rtao_samples = 0;
// coverity[GLOBAL_INIT_ORDER] -- safe; OptionBuilder::finish() uses Meyers singleton
auto RtaoQualityOption = options::OptionBuilder<int>("Graphics.RtaoQuality",
std::pair<const char*, int>{"Raytraced Ambient Occlusion", -1},
std::pair<const char*, int>{"Rays traced per pixel for ambient occlusion in the deferred ambient pass; Off keeps baked AO maps only", -1})
.enumerator([]() -> SCP_vector<int> {
if (rtao_supported()) {
return {0, 4, 8, 16};
}
return {0}; // inert default when RT isn't supported
})
.display(options::MapValueDisplay<int>({
{0, {"Off", -1}},
{4, {"Low", -1}},
{8, {"Medium", -1}},
{16, {"High", -1}}
}))
.bind_to(&Rtao_samples)
.flags({options::OptionFlags::ForceMultiValueSelection})
.level(options::ExpertLevel::Advanced)
.category(std::make_pair("Graphics", 1825))
.default_func([]() { return 0; })
.importance(71)
.finish();
bool rtao_supported()
{
return gr_is_capable(gr_capability::CAPABILITY_RAYTRACED_SHADOWS);
}
bool rtao_enabled()
{
// RTAO reads its inputs from the deferred global data, and the frame TLAS build also
// runs for RTAO alone. So RTAO does not depend on the shadow settings.
return Rtao_samples > 0 && rtao_supported();
}
void rtao_remove_unsupported_options()
{
if (!rtao_supported()) {
// The option has no effect without raytracing support, so remove it.
options::OptionsManager::instance()->removeOption(RtaoQualityOption);
}
}
+29
View File
@@ -0,0 +1,29 @@
#pragma once
// Raytraced ambient occlusion (RTAO): short hemisphere occlusion rays traced via
// inline ray query against the raytraced-shadow TLAS, inside the deferred ambient
// light pass (LT_AMBIENT branch of deferred-f.sdr, RTAO shader variant). Composes
// multiplicatively with baked AO maps through the shared G-buffer `ao` term, so
// env-map/IBL ambient darkens consistently for free. The AO radius and strength
// are content-scale-dependent and therefore mod-owned -- see $RTAO Radius: /
// $RTAO Strength: in lighting_profiles.h.
// Rays traced per pixel for ambient occlusion. 0 (the default) disables RTAO
// entirely; nonzero values only take effect when rtao_supported(). Clamped
// in-shader to RT_SHADOW_MAX_SAMPLES (16). See RtaoQualityOption in rtao.cpp.
extern int Rtao_samples;
// Whether the hardware/renderer can trace AO rays at all -- the same requirement
// set as raytraced shadows (Vulkan + VK_KHR_acceleration_structure +
// VK_KHR_ray_query), so this simply forwards to that capability.
bool rtao_supported();
// Whether the deferred ambient pass should trace AO this frame, i.e. the user has
// enabled it and the hardware supports it. The shadow settings have no effect on RTAO.
// This is the single source of truth --
// gate any RTAO shader-flag, TLAS-build, or uniform code on this, not on
// Rtao_samples/rtao_supported() separately.
bool rtao_enabled();
// Remove the RTAO option from the options list when the renderer cannot trace rays.
void rtao_remove_unsupported_options();
+7 -1
View File
@@ -116,6 +116,10 @@ static ShaderTypeInfo SHADER_TYPES[] = {
{ SDR_TYPE_GAMMA_BLIT, "post-v.sdr", "gamma-correct-f.sdr", nullptr,
{ VATTRIB_POSITION, VATTRIB_TEXCOORD }, "Gamma correct blit", false },
// Vulkan-only
{ SDR_TYPE_MOTION_VECTORS, "post-v.sdr", "motion-vector-f.sdr", nullptr,
{ VATTRIB_POSITION, VATTRIB_TEXCOORD }, "Motion vectors", false },
};
// clang-format on
@@ -133,6 +137,8 @@ static ShaderVariantInfo SHADER_VARIANTS[] = {
{SDR_TYPE_DEFERRED_LIGHTING, false, SDR_FLAG_DEFERRED_RT_SHADOWS, "RT_SHADOWS", {}, "Use raytraced (TLAS ray query) shadows instead of cascaded shadow maps"},
{SDR_TYPE_DEFERRED_LIGHTING, false, SDR_FLAG_DEFERRED_RTAO, "RTAO", {}, "Trace raytraced ambient occlusion in the ambient light pass"},
{SDR_TYPE_POST_PROCESS_BLUR, false, SDR_FLAG_BLUR_HORIZONTAL, "PASS_0", {}, "Horizontal blur pass"},
{SDR_TYPE_POST_PROCESS_BLUR, false, SDR_FLAG_BLUR_VERTICAL, "PASS_1", {}, "Vertical blur pass"},
@@ -221,7 +227,7 @@ bool shader_variant_requires_raytracing(shader_type type, unsigned int flags)
case SDR_TYPE_MODEL:
return (flags & MODEL_SDR_FLAG_RT_SHADOWS) != 0;
case SDR_TYPE_DEFERRED_LIGHTING:
return (flags & SDR_FLAG_DEFERRED_RT_SHADOWS) != 0;
return (flags & (SDR_FLAG_DEFERRED_RT_SHADOWS | SDR_FLAG_DEFERRED_RTAO)) != 0;
default:
return false;
}
+3
View File
@@ -18,6 +18,9 @@ enum VertexAttributeLocation : uint32_t {
VATTRIB_MODEL_MATRIX = 8, // Occupies locations 8-11
NUM_VERTEX_ATTRIBS = 9,
// Vulkan only, and not listed in ShaderTypeInfo::attributes, which OpenGL indexes its attribute tables with
VATTRIB_EFFECT_MOTION = 12, // vec4
};
// Shared shader type info — matches the original opengl_shader_type_t layout.
+243 -31
View File
@@ -16,7 +16,9 @@
#include "cmdline/cmdline.h"
#include "debris/debris.h"
#include "graphics/matrix.h"
#include "graphics/rtao.h"
#include "lighting/lighting.h"
#include "lighting/lighting_profiles.h"
#include "math/vecmat.h"
#include "mod_table/mod_table.h"
#include "model/model.h"
@@ -27,9 +29,13 @@
#include "ship/ship.h"
#include "ship/shipfx.h"
#include "render/3d.h"
#include "starfield/starfield.h"
#include "starfield/sun_disc.h"
#include "tracing/tracing.h"
#include "util/uniform_structs.h"
#include <algorithm>
extern vec3d check_offsets[8];
matrix4 Shadow_view_matrix_light;
@@ -45,6 +51,7 @@ vec3d shadow_rt_relative(const vec3d& world_pos)
}
SCP_vector<matrix4> Shadow_proj_matrix;
SCP_vector<float> Shadow_cascade_distances;
SCP_vector<float> Shadow_penumbra_scale;
static SCP_vector<light_frustum_info> Shadow_frustums;
@@ -243,13 +250,163 @@ auto RtShadowBiasMaxOption = options::OptionBuilder<float>("Graphics.RtShadowBia
.importance(74)
.finish();
int Rt_shadow_samples_override = -1;
bool Shadow_contact_hardening_enabled = true;
// coverity[GLOBAL_INIT_ORDER] -- safe; OptionBuilder::finish() uses Meyers singleton
auto ShadowContactHardeningOption = options::OptionBuilder<bool>("Graphics.ShadowContactHardening",
std::pair<const char*, int>{"Shadow Contact Hardening", -1},
std::pair<const char*, int>{"Sizes each shadow's penumbra by how close its occluder is, instead of a fixed blur width. Costs an extra shadow-map search per shadowed pixel", -1})
// Only ever offered on hardware that can do the raw-depth read the
// blocker search needs -- see shadow_contact_hardening_supported().
.enumerator([]() -> SCP_vector<bool> {
if (shadow_contact_hardening_supported()) {
return {true, false};
}
return {false}; // inert default when unsupported
})
// Live-apply: shadows_start_render() reads Shadow_contact_hardening_enabled()
// fresh every frame, no restart needed.
.bind_to(&Shadow_contact_hardening_enabled)
.flags({options::OptionFlags::ForceMultiValueSelection})
.level(options::ExpertLevel::Advanced)
.category(std::make_pair("Graphics", 1825))
.default_func([]() { return true; })
.importance(73)
.finish();
bool shadows_raytracing_supported()
{
return gr_is_capable(gr_capability::CAPABILITY_RAYTRACED_SHADOWS);
}
bool shadow_contact_hardening_supported()
{
return gr_is_capable(gr_capability::CAPABILITY_SHADOW_CONTACT_HARDENING);
}
bool shadow_contact_hardening_enabled()
{
return shadow_contact_hardening_supported() && Shadow_contact_hardening_enabled;
}
int shadows_map_resolution()
{
switch (Shadow_quality) {
case ShadowQuality::Low: return 512;
case ShadowQuality::Medium: return 1024;
case ShadowQuality::High: return 2048;
case ShadowQuality::Ultra: return 4096;
default: return 512;
}
}
int shadows_rt_sample_count()
{
if (Rt_shadow_samples_override > 0) {
return Rt_shadow_samples_override;
}
// The same tiers that pick the shadow map's resolution pick the ray count, so one
// setting covers "how much am I willing to spend on shadows" for either method. Low
// stays at a single ray -- that is the hard-shadow, one-ray-per-light floor, matching
// how a 512 map is the cheap floor for the other method rather than a soft-shadow
// setting the user has to find separately.
switch (Shadow_quality) {
case ShadowQuality::Low: return 1;
case ShadowQuality::Medium: return 4;
case ShadowQuality::High: return 8;
case ShadowQuality::Ultra: return 16;
default: return 1;
}
}
// The apparent sun size the $Shadow Smoothness Factor: values are taken to be tuned against.
// Sol is the natural choice: measured across the sun art of retail, the MediaVPs and several
// mods, the calibration in sun_disc.cpp lands median content near Sol, and the shipped
// smoothness defaults (1/300 .. 1/200) work out to a Sol-sized sun's penumbra at a blocker
// distance of roughly 0.7-1.1 cascade widths. So a Sol-sized sun reproduces the look every
// existing mod already tuned for, and everything else scales from there.
static float shadow_smoothness_reference_tangent()
{
return sun_disc_tangent_from_diameter(SUN_ANGULAR_SIZE_SOL);
}
// Returns the softness of the shadow-mapped sun, relative to the reference above. The tabled
// smoothness values are multiplied by this factor.
//
// The shadow map and the raytracer both use the same value: the tangent of the angular radius
// of the sun. This value comes from $SunAngularSize: or from a measurement of the sun bitmap
// (see sun_angular_radius_tangent() in starfield.cpp). When the sun becomes two times larger,
// the raytraced penumbra cone and this filter width also become two times larger. Thus a
// mission author sets one value, and both methods follow it.
//
// When shadow_contact_hardening_supported() is true, this value is the maximum penumbra radius.
// pcssPenumbraRadius() (shadows.sdr) calculates a radius for each pixel from the blocker search,
// and clamps it to this maximum (see Shadow_penumbra_scale). When contact hardening is not
// supported (OpenGL before 3.3), this value is the fixed penumbra radius. In both cases the
// tabled values keep their old meaning, so mods that tuned them see no change.
//
// This function uses Static_light.front(), which is also the light of the cascades in
// shadows_start_render(). Each directional light at this point has a source radius: suns get
// it in stars_draw_sun(), and common_setup_room_lights() gives its lights the radius of a sun.
// Thus a radius of 0 is a source with no size, for example a blank sun bitmap of a mod. Such a
// source must have a hard edge, so the shadow map uses its narrowest filter
// (see shadow_clamp_smoothness()).
static float shadow_smoothness_scale()
{
if (Static_light.empty()) {
return 1.0f;
}
const float reference = shadow_smoothness_reference_tangent();
if (reference <= 0.0f) {
return 1.0f;
}
// For directional lights source_radius is the tangent of the angular radius, not a
// world-space size -- see light_add_directional() in starfield.cpp's stars_draw_sun().
return Static_light.front().source_radius / reference;
}
// Keeps a derived filter width inside what the shadow map can actually represent.
//
// The floor is one texel: a sun small enough to want a sub-texel penumbra can't get one out of
// a shadow map, and dropping below a texel only collapses all 16 Poisson taps into the same
// texel -- paying for the taps and getting an aliased edge for it. (A raytraced shadow *does*
// go properly hard there; this is a limit of the method, not of the parameter.)
//
// The ceiling bounds how far the taps spread. Missions set +AngularSize: freely, and a large
// enough sun would scatter the 16 taps far enough apart to read as separate blobs, while the
// comparison depth stays at the receiver and turns the widened filter into acne.
//
// Note the floor also applies to the tabled values themselves, so a mod that asked for a very
// small $Shadow Smoothness Factor: gets widened to a texel at low shadow-map resolutions. That
// value was already sub-texel there -- i.e. already doing nothing except cost taps -- so this
// trades a hair of extra blur for an antialiased edge.
static float shadow_clamp_smoothness(float smoothness)
{
// shadows_map_resolution() is 512 at its smallest, so the floor always stays below the
// ceiling and std::clamp()'s lo <= hi precondition holds.
const float min_smoothness = 1.0f / static_cast<float>(shadows_map_resolution());
constexpr float max_smoothness = 0.02f;
return std::clamp(smoothness, min_smoothness, max_smoothness);
}
void shadows_remove_unsupported_options()
{
rtao_remove_unsupported_options();
if (!shadow_contact_hardening_supported()) {
// No raw-depth sampler support (pre-3.3 OpenGL): the option is inert, so drop it.
// The bound global keeps its default; shadow_contact_hardening_enabled() folding
// in shadow_contact_hardening_supported() keeps rendering on the fixed-width
// fallback either way.
options::OptionsManager::instance()->removeOption(ShadowContactHardeningOption);
}
if (shadows_raytracing_supported()) {
return;
}
@@ -272,6 +429,11 @@ bool shadows_use_raytracing()
return Shadow_render_method == ShadowRenderMethod::Raytraced && shadows_raytracing_supported();
}
bool shadows_rt_scene_needed()
{
return rtao_enabled() || (shadows_use_raytracing() && Shadow_quality != ShadowQuality::Disabled);
}
bool shadows_use_raytraced_local_lights()
{
return shadows_use_raytracing() && Rt_shadow_quality == RTShadowQuality::High;
@@ -670,6 +832,9 @@ matrix shadows_start_render(matrix *eye_orient, vec3d *eye_pos, fov_t fov, fov_t
// Only ever do cockpit cascades if there's no override
bool render_cockpit_cascades = !cascade_distances_override.has_value() && ship_render_player_has_closeup_visuals();
// Computed once, not per-cascade -- see shadow_contact_hardening_enabled().
const bool contact_hardening_active = shadow_contact_hardening_enabled();
for (int i = 0; i < num_cascades; i++) {
float z_near;
if (i == 0 || (!render_cockpit_cascades && i == Num_cockpit_shadow_cascades)) {
@@ -682,6 +847,18 @@ matrix shadows_start_render(matrix *eye_orient, vec3d *eye_pos, fov_t fov, fov_t
shadows_construct_light_frustum(&Shadow_frustums[i], &light_matrix, eye_orient, nullptr, i < Num_cockpit_shadow_cascades ? cockpit_fov : fov, aspect, z_near, z_far);
Shadow_cascade_distances[i] = cascade_distances_actual[i];
Shadow_proj_matrix[i] = Shadow_frustums[i].proj_matrix;
// PCSS penumbra scale: world-space blocker/receiver depth separation (read back from
// the shadow map by the blocker search) times this equals the UV-space penumbra
// radius directly -- see the derivation next to shadow_smoothness_scale() below.
// Sentinel -1 when unsupported OR the user has the option off tells the shader to
// fall back to the fixed smoothness_factors[cascade] (today's behavior) instead of
// reading a raw depth sampler -- which either doesn't exist on this hardware, or
// does but isn't worth the extra shadow-map search right now.
Shadow_penumbra_scale[i] = contact_hardening_active
? std::max(0.0f, (Shadow_frustums[i].max.xyz.z - Shadow_frustums[i].min.xyz.z) * lp.source_radius /
(Shadow_frustums[i].max.xyz.x - Shadow_frustums[i].min.xyz.x))
: -1.0f;
}
gr_shadow_map_start(&Shadow_view_matrix_light, &light_matrix, eye_pos, true);
@@ -837,6 +1014,10 @@ void shadows_render_all(fov_t fov, matrix *eye_orient, vec3d *eye_pos,
GR_DEBUG_SCOPE("Render shadows");
TRACE_SCOPE(tracing::BuildShadowMap);
// The deferred pass reads this matrix in each frame, also when no shadow map renders (for
// example, RTAO in a mission with no sun). Thus write it before the early returns below.
Shadow_view_matrix_render = gr_view_matrix;
if ( Static_light.empty() ) {
return;
}
@@ -845,15 +1026,8 @@ void shadows_render_all(fov_t fov, matrix *eye_orient, vec3d *eye_pos,
return;
}
// Rebuilds the raytraced-shadow TLAS alongside the cascaded shadow map below
// -- Vulkan-only, no-op everywhere else. Which one shading actually samples
// is decided per shadows_use_raytracing(). See VulkanRaytracingManager::buildTlas().
gr_build_shadow_tlas();
//shadows_debug_show_frustum(&Player_obj->orient, &Player_obj->pos, fov, gr_screen.clip_aspect, Min_draw_distance, 3000.0f);
Shadow_view_matrix_render = gr_view_matrix;
gr_end_proj_matrix();
gr_end_view_matrix();
@@ -1020,6 +1194,10 @@ void shadow_end_frame() {
static gr_buffer_handle Shadow_cascade_params_buffer;
static size_t Shadow_cascade_params_buffer_size = 0;
// Number of padded per-cascade float arrays shadow_cascade_params_bind() writes after the
// proj matrices: cascade distances, smoothness factors, PCSS penumbra scale.
static constexpr size_t Num_cascade_float_arrays = 3;
static std::pair<size_t, size_t> compute_cascade_params_size(int num_cascades) {
size_t padding_required = num_cascades % 4;
if (padding_required != 0)
@@ -1027,11 +1205,23 @@ static std::pair<size_t, size_t> compute_cascade_params_size(int num_cascades) {
return {sizeof(graphics::shadow_cascade_static_data)
+ sizeof(matrix4) * num_cascades
+ sizeof(float) * (num_cascades + padding_required)
+ sizeof(float) * (num_cascades + padding_required),
+ Num_cascade_float_arrays * sizeof(float) * (num_cascades + padding_required),
padding_required};
}
// Writes num_cascades floats (value_at(0)..value_at(num_cascades - 1)) into buffer at offset,
// advancing offset past both the values and the std140 padding that keeps the next field
// vec4-aligned. Shared by shadow_cascade_params_bind()'s three per-cascade float arrays so the
// offset/padding bookkeeping can't drift between them.
template <typename F>
static void write_padded_floats(SCP_vector<uint8_t>& buffer, size_t& offset, int num_cascades, size_t padding, F&& value_at) {
for (int i = 0; i < num_cascades; i++) {
*reinterpret_cast<float*>(buffer.data() + offset) = value_at(i);
offset += sizeof(float);
}
offset += sizeof(float) * padding;
}
void shadow_cascade_params_init() {
const int num_cascades = Num_shadow_cascades + Num_cockpit_shadow_cascades;
Shadow_cascade_params_buffer_size = compute_cascade_params_size(num_cascades).first;
@@ -1043,6 +1233,7 @@ void shadow_cascade_params_init() {
Shadow_frustums.resize(num_cascades);
Shadow_cascade_distances.resize(num_cascades);
Shadow_proj_matrix.resize(num_cascades);
Shadow_penumbra_scale.resize(num_cascades);
}
void shadow_cascade_params_shutdown() {
@@ -1062,7 +1253,7 @@ void shadow_cascade_params_bind(int cascade_offset, int cascade_count, const sha
const auto [required_size, padding] = compute_cascade_params_size(num_cascades);
Assertion(required_size <= Shadow_cascade_params_buffer_size, "The shadow cascade parameter buffer grew in size!");
Assertion(Shadow_proj_matrix.size() == static_cast<size_t>(num_cascades) && Shadow_cascade_distances.size() == static_cast<size_t>(num_cascades) && Shadow_smoothness_factor.size() == static_cast<size_t>(num_cascades), "Shadow cascade data buffers are of incorrect size! (Expected %d, got %d, %d, %d)", num_cascades, static_cast<int>(Shadow_proj_matrix.size()), static_cast<int>(Shadow_cascade_distances.size()), static_cast<int>(Shadow_smoothness_factor.size()));
Assertion(Shadow_proj_matrix.size() == static_cast<size_t>(num_cascades) && Shadow_cascade_distances.size() == static_cast<size_t>(num_cascades) && Shadow_smoothness_factor.size() == static_cast<size_t>(num_cascades) && Shadow_penumbra_scale.size() == static_cast<size_t>(num_cascades), "Shadow cascade data buffers are of incorrect size! (Expected %d, got %d, %d, %d, %d)", num_cascades, static_cast<int>(Shadow_proj_matrix.size()), static_cast<int>(Shadow_cascade_distances.size()), static_cast<int>(Shadow_smoothness_factor.size()), static_cast<int>(Shadow_penumbra_scale.size()));
Assertion(cascade_count + cascade_offset <= num_cascades, "Requested drawing out-of-range cascades!");
SCP_vector<uint8_t> buffer(required_size, 0);
@@ -1074,6 +1265,7 @@ void shadow_cascade_params_bind(int cascade_offset, int cascade_count, const sha
static_data.cascade_count = cascade_count;
static_data.rtShadowBiasMin = Rt_shadow_bias_min;
static_data.rtShadowBiasMax = Rt_shadow_bias_max;
static_data.rtShadowSampleCount = shadows_rt_sample_count();
static_data.shadow_mv_matrix = Shadow_view_matrix_light;
static_data.shadow_ray_cull_mask = ray.cull_mask;
@@ -1089,19 +1281,20 @@ void shadow_cascade_params_bind(int cascade_offset, int cascade_count, const sha
offset += sizeof(matrix4);
}
for (int i = 0; i < num_cascades; i++) {
auto& cascade_distance = *reinterpret_cast<float*>(buffer.data() + offset);
cascade_distance = Shadow_cascade_distances[i];
offset += sizeof(float);
}
offset += sizeof(float) * padding;
write_padded_floats(buffer, offset, num_cascades, padding,
[](int i) { return Shadow_cascade_distances[i]; });
for (int i = 0; i < num_cascades; i++) {
auto& smoothness_factor = *reinterpret_cast<float*>(buffer.data() + offset);
smoothness_factor = Shadow_smoothness_factor[i];
offset += sizeof(float);
}
offset += sizeof(float) * padding;
// Scaled by the sun's apparent size so shadow-mapped and raytraced shadows soften
// together -- see shadow_smoothness_scale().
const float smoothness_scale = shadow_smoothness_scale();
write_padded_floats(buffer, offset, num_cascades, padding,
[smoothness_scale](int i) { return shadow_clamp_smoothness(Shadow_smoothness_factor[i] * smoothness_scale); });
// PCSS penumbra scale, appended last so every field above keeps its existing byte
// offset -- see Shadow_penumbra_scale's derivation comment in shadows.h.
write_padded_floats(buffer, offset, num_cascades, padding,
[](int i) { return Shadow_penumbra_scale[i]; });
// Full replacement, not _offset: this runs several times per frame with different content,
// and Vulkan's streaming buffer only bump-allocates fresh memory for a full update.
@@ -1119,20 +1312,39 @@ shadow_ray_params shadow_ray_params_cockpit(const object* viewer) {
return ray;
}
// The ray origin and cull mask of the current deferred lighting pass: the cockpit pass or the
// main scene pass.
static shadow_ray_params shadow_ray_params_deferred() {
if (Lighting_mode == lighting_mode::COCKPIT && Viewer_obj != nullptr) {
shadow_ray_params ray = shadow_ray_params_cockpit(Viewer_obj);
if (ship_render_player_ship_casts_shadow_on_cockpit()) {
ray.cull_mask = TLAS_MASK_ALL;
}
return ray;
}
return shadow_ray_params{shadow_rt_relative(Eye_position)};
}
void shadow_cascade_params_bind_deferred() {
const bool cockpit = Lighting_mode == lighting_mode::COCKPIT;
const int offset = cockpit ? 0 : Num_cockpit_shadow_cascades;
const int count = cockpit ? Num_cockpit_shadow_cascades : Num_shadow_cascades;
if (cockpit && Viewer_obj != nullptr) {
shadow_ray_params ray = shadow_ray_params_cockpit(Viewer_obj);
if (ship_render_player_ship_casts_shadow_on_cockpit()) {
ray.cull_mask = TLAS_MASK_ALL;
}
shadow_cascade_params_bind(offset, count, ray);
} else {
shadow_cascade_params_bind(offset, count);
}
shadow_cascade_params_bind(offset, count, shadow_ray_params_deferred());
}
void shadows_fill_deferred_global_data(graphics::deferred_global_data& data) {
vm_inverse_matrix4(&data.inv_view_matrix, &Shadow_view_matrix_render);
const shadow_ray_params ray = shadow_ray_params_deferred();
data.rayViewOrigin = ray.view_origin;
data.rayCullMask = ray.cull_mask;
data.rayBiasMin = Rt_shadow_bias_min;
data.rayBiasMax = Rt_shadow_bias_max;
data.rtaoSampleCount = Rtao_samples;
data.rtaoRadius = lighting_profiles::current_rtao_radius();
data.rtaoStrength = lighting_profiles::current_rtao_strength();
}
bool shadows_cockpit_casts_shadow(const ship_info* sip) {
+73
View File
@@ -17,6 +17,10 @@
class ship_info;
namespace graphics {
struct deferred_global_data;
}
struct light_frustum_info
{
matrix4 proj_matrix;
@@ -67,6 +71,32 @@ extern int Max_rt_shadow_local_lights;
extern float Rt_shadow_bias_min;
extern float Rt_shadow_bias_max;
// Rays traced per pixel per shadowed light, for lights that have a source size --
// suns from their drawn size, or from $SunAngularSize in stars.tbl when that
// overrides it; local lights via their source_radius. 1 means the single hard ray
// in traceShadowRayCone() (shadows.sdr); above that it samples a penumbra.
//
// This is a cost knob, not an appearance knob: how *wide* a penumbra is comes
// solely from the light's source size, which is what also drives the shadow-map
// path and keeps the two methods consistent for content authors. The sample count
// only decides how well that penumbra resolves.
//
// Which is why it is not a setting of its own: it follows Shadow_quality
// (Low/Medium/High/Ultra -> 1/4/8/16), the same tier that picks the shadow map's
// resolution, so one control covers shadow cost whichever method is active.
// Capped in-shader at RT_SHADOW_MAX_SAMPLES (16).
int shadows_rt_sample_count();
// Session-only override for shadows_rt_sample_count(), for iterating in the lab
// without touching a persisted setting. <= 0 means "follow Shadow_quality".
extern int Rt_shadow_samples_override;
// Edge length, in texels, of one square cascade of the shadow map, as chosen by
// Shadow_quality. Meaningless when Shadow_quality is Disabled (callers guard on that
// first); the value returned in that case is only there to keep the result usable as a
// divisor.
int shadows_map_resolution();
// Whether the current hardware/renderer can do anything with ShadowRenderMethod::Raytraced
// at all (Vulkan + VK_KHR_acceleration_structure + VK_KHR_ray_query support). Independent
// of which method is currently selected -- use this to decide whether to offer the choice.
@@ -88,6 +118,11 @@ bool shadows_use_raytracing();
// i.e. shadows_use_raytracing() is true AND the user has selected RTShadowQuality::High.
bool shadows_use_raytraced_local_lights();
// Whether this frame needs the ray-tracing TLAS: raytraced shadows with shadows on, or RTAO.
// The Vulkan backend builds the TLAS one time in each frame when this is true. See
// vulkan_scene_texture_begin().
bool shadows_rt_scene_needed();
extern matrix4 Shadow_view_matrix_light;
extern matrix4 Shadow_view_matrix_render;
@@ -105,6 +140,38 @@ constexpr uint8_t TLAS_MASK_ALL = 0xFF;
constexpr uint8_t TLAS_MASK_VIEWER_HULL = 0x80;
constexpr uint8_t SHADOW_RAY_CULL_MASK_EXCLUDE_VIEWER_HULL = static_cast<uint8_t>(~TLAS_MASK_VIEWER_HULL);
// Per-cascade PCSS penumbra scale: world-space blocker/receiver depth separation, scaled
// by the sun's tanθ and converted to UV space, gives the contact-hardened penumbra radius
// directly (see the derivation comment above shadow_smoothness_scale() in shadows.cpp).
// A negative value is a sentinel meaning shadow_contact_hardening_enabled() was false
// when it was computed -- the shader falls back to the fixed smoothness_factors[cascade].
extern SCP_vector<float> Shadow_penumbra_scale;
// Whether the current renderer can do a raw (uncompared) depth read off the shadow map,
// which PCSS blocker search needs. True on Vulkan always; on OpenGL requires
// GL 3.3 (a second sampler object with compare mode off, bound to the
// same shadow texture -- see shadow_map_raw in shadows.sdr). Independent of whether the
// user currently has contact hardening turned on -- use this to decide whether to offer
// the option at all.
bool shadow_contact_hardening_supported();
// User-facing toggle for shadow contact hardening (PCSS blocker search): sizes each
// cascade's penumbra from actual occluder distance instead of the fixed
// $Shadow Smoothness Factor: width. Costs an extra raw-depth search per shadowed pixel
// (see pcssBlockerSearch() in shadows.sdr), so this is exposed as a setting rather than
// being unconditional -- persisted via Graphics.ShadowContactHardening. See
// shadow_contact_hardening_enabled() for the flag that actually gates the per-frame cost.
extern bool Shadow_contact_hardening_enabled;
// Whether shadow rendering should actually do the contact-hardening blocker search this
// frame, i.e. shadow_contact_hardening_supported() is true AND the user has the option on.
// This is the single source of truth -- gate any per-cascade penumbra-scale computation on
// this, not on shadow_contact_hardening_supported()/Shadow_contact_hardening_enabled
// separately. When false, Shadow_penumbra_scale gets the same negative sentinel as
// "unsupported", so the shader takes the pre-contact-hardening fixed-width path at zero
// extra cost (see pcssPenumbraRadius() in shadows.sdr).
bool shadow_contact_hardening_enabled();
void shadows_construct_light_frustum(vec3d *min_out, vec3d *max_out, vec3d light_vec, matrix *orient, vec3d *pos, fov_t fov, float aspect, float z_near, float z_far);
bool shadows_obj_in_frustum(object *objp, vec3d *min, vec3d *max, matrix *light_orient);
void shadows_render_all(fov_t fov, matrix *eye_orient, vec3d *eye_pos,
@@ -154,6 +221,12 @@ void shadow_cascade_params_bind(int cascade_offset, int cascade_count, const sha
// full-screen passes, which shade one frame per pass.
void shadow_cascade_params_bind_deferred();
// Writes the view and ray-tracing fields of the global data of the deferred lighting pass:
// inv_view_matrix, the ray origin and cull mask of the current Lighting_mode, the ray bias,
// and the RTAO settings. Both backends call this one time for each deferred lighting pass.
// These fields do not depend on the shadow cascades, so RTAO also works with shadows off.
void shadows_fill_deferred_global_data(graphics::deferred_global_data& data);
// True when the viewer's cockpit model also gets a shadow pass (raster and RT).
bool shadows_cockpit_casts_shadow(const ship_info* sip);
+7 -1
View File
@@ -35,7 +35,9 @@ void convert_model_material(model_uniform_data* data_out,
const model_material& material,
const matrix4& model_transform,
const vec3d& scale,
size_t transform_buffer_offset) {
size_t transform_buffer_offset,
const matrix4& prev_model_transform,
int prev_transform_stride) {
auto shader_flags = material.get_shader_flags();
Assertion(gr_model_matrix_stack.depth() == 1, "Uniform conversion does not respect previous transforms! "
@@ -45,6 +47,10 @@ void convert_model_material(model_uniform_data* data_out,
scale_matrix(scaled_matrix, scale);
data_out->modelMatrix = scaled_matrix;
data_out->prevModelMatrix = prev_model_transform;
scale_matrix(data_out->prevModelMatrix, scale);
data_out->prev_transform_stride = prev_transform_stride;
gr_get_velocity_uniforms(&data_out->velocity);
data_out->viewMatrix = gr_view_matrix;
vm_matrix4_x_matrix4(&data_out->modelViewMatrix, &gr_view_matrix, &scaled_matrix);
data_out->projMatrix = gr_projection_matrix;
+3 -1
View File
@@ -16,7 +16,9 @@ void convert_model_material(model_uniform_data* data_out,
const model_material& material,
const matrix4& model_transform,
const vec3d& scale,
size_t transform_buffer_offset);
size_t transform_buffer_offset,
const matrix4& prev_model_transform,
int prev_transform_stride);
void convert_shadow_material(shadow_uniform_data* data_out,
const matrix4& model_transform,
+51 -4
View File
@@ -2,6 +2,8 @@
#include "globalincs/pstypes.h"
#include <cstddef>
using SPIRV_FLOAT_MAT_4x4 = matrix4;
using SPIRV_FLOAT_VEC4 = vec4;
@@ -17,15 +19,37 @@ namespace graphics {
* but the TL;DR here is that everything in here must be 16-byte aligned.
*/
// The camera that draws write motion vectors against. Only the Vulkan shaders declare it.
struct velocity_camera_data {
matrix4 viewProj;
matrix4 prevViewProj;
float uvScale[2];
};
struct deferred_global_data {
matrix4 inv_view_matrix;
float invScreenWidth;
float invScreenHeight;
float nearPlane;
// RTAO rays for each pixel. 0 when RTAO is off.
int rtaoSampleCount;
float pad;
// The ray-tracing inputs of the deferred pass, for raytraced shadows and RTAO. They are
// here and not in shadowCascadeParams, because the deferred pass always binds this block,
// also when shadows are off. shadows_fill_deferred_global_data() (shadows.cpp) sets them.
// rayViewOrigin has the same meaning as shadow_cascade_static_data::shadow_ray_view_origin.
// The int after the vec3 fills the std140 padding of the vec3.
vec3d rayViewOrigin;
int rayCullMask;
float rayBiasMin;
float rayBiasMax;
float rtaoRadius;
float rtaoStrength;
};
static_assert(offsetof(deferred_global_data, rayViewOrigin) == 80 && sizeof(deferred_global_data) == 112,
"deferred_global_data must match the std140 layout of the globalDeferredData block in deferred-f.sdr");
/**
* @brief Data for one deferred light rendering call
@@ -124,6 +148,12 @@ struct model_uniform_data {
float alphaMult;
int flags;
float pad;
// Only the Vulkan shaders declare these, and they're past the end of the OpenGL block.
matrix4 prevModelMatrix;
velocity_camera_data velocity;
int prev_transform_stride;
float pad2;
};
const size_t model_uniform_data_size = sizeof(model_uniform_data);
@@ -146,6 +176,11 @@ struct shadow_cascade_static_data {
int cascade_count;
float rtShadowBiasMin;
float rtShadowBiasMax;
int rtShadowSampleCount;
// Padding. The scalars must fill 32 bytes, so that std140 puts the following mat4
// directly after them. Keep this struct and the shadowCascadeParams block in
// shadow_cascade_params.sdr the same.
int pad[3];
matrix4 shadow_mv_matrix;
// Position of the view-space origin (the camera) in TLAS-relative space. The RT shadow
@@ -173,13 +208,17 @@ struct shadow_cascade_static_data {
// main shadow cascades. Set in shadow_cascade_params_bind() (shadows.cpp).
int shadow_ray_cull_mask;
};
static_assert(offsetof(shadow_cascade_static_data, shadow_mv_matrix) == 32,
"shadow_cascade_static_data's scalar fields must total exactly 32 bytes to match std140's "
"mat4 alignment -- update the shadowCascadeParams block in shadow_cascade_params.sdr "
"if this changes.");
// Must match the GLSL shadowCascadeParams block's implicit std140 padding
// exactly (16 [4 leading scalars] + 64 [matrix4] + 12 [shadow_ray_view_origin]
// + 4 [shadow_ray_cull_mask, packed into the vec3's alignment padding] = 96) --
// exactly (32 [5 leading scalars + 3 padding] + 64 [matrix4] + 12 [shadow_ray_view_origin]
// + 4 [shadow_ray_cull_mask, packed into the vec3's alignment padding] = 112) --
// shadow_cascade_params_bind() packs shadow_proj_matrix[] immediately after
// this struct via sizeof(), so a mismatch here silently shifts every cascade
// matrix in the buffer, in both backends.
static_assert(sizeof(shadow_cascade_static_data) == 96, "shadow_cascade_static_data must match the GLSL shadowCascadeParams layout (see comment above)");
static_assert(sizeof(shadow_cascade_static_data) == 112, "shadow_cascade_static_data must match the GLSL shadowCascadeParams layout (see comment above)");
enum class NanoVGShaderType: int32_t {
FillGradient = 0, FillImage = 1, Simple = 2, Image = 3
@@ -405,6 +444,10 @@ struct effect_data {
int blend_alpha;
float pad[1];
// Past the end of the OpenGL block
velocity_camera_data velocity;
float pad2[2];
};
struct effect_distort_data {
@@ -421,6 +464,10 @@ struct batched_data {
float intensity;
float pad[3];
// Past the end of the OpenGL block
velocity_camera_data velocity;
float pad2[2];
};
struct post_data {
+6
View File
@@ -118,6 +118,12 @@ public:
*/
uint32_t getCurrentFrame() const { return m_currentFrame; }
/**
* @brief Get the monotonic frame number set by setCurrentFrame (total frames
* rendered, never wraps back -- unlike getCurrentFrame()'s in-flight slot index)
*/
uint64_t getCurrentFrameNumber() const { return m_currentFrameNumber; }
/**
* @brief Get the Vulkan logical device
*/
+69 -60
View File
@@ -47,16 +47,18 @@ static void transitionMsaaImages(vk::CommandBuffer cmd, VulkanPostProcessor* pp,
vk::AccessFlags2 depthSrcAccess, vk::AccessFlags2 depthDstAccess,
vk::PipelineStageFlags2 srcStage, vk::PipelineStageFlags2 dstStage)
{
std::array<ImageBarrier2, 6> barriers;
std::array<ImageBarrier2, 7> barriers;
std::array<vk::Image, 5> msaaImages = {
std::array<vk::Image, 6> msaaImages = {
pp->deferred().msaaColorImage(),
pp->deferred().msaaPositionImage(),
pp->deferred().msaaNormalImage(),
pp->deferred().msaaSpecularImage(),
pp->deferred().msaaEmissiveImage(),
pp->deferred().msaaVelocityImage(),
};
for (size_t i = 0; i < msaaImages.size(); ++i) {
const size_t colorCount = pp->deferred().velocityActive() ? 6 : 5;
for (size_t i = 0; i < colorCount; ++i) {
barriers[i].image = msaaImages[i];
barriers[i].levelCount = 1;
barriers[i].layerCount = 1;
@@ -68,18 +70,19 @@ static void transitionMsaaImages(vk::CommandBuffer cmd, VulkanPostProcessor* pp,
barriers[i].dstAccess = colorDstAccess;
}
barriers[5].image = pp->deferred().msaaDepthImage();
barriers[5].aspectMask = imageAspectFromFormat(pp->getDepthFormat());
barriers[5].levelCount = 1;
barriers[5].layerCount = 1;
barriers[5].oldLayout = depthOldLayout;
barriers[5].newLayout = depthNewLayout;
barriers[5].srcStage = srcStage;
barriers[5].srcAccess = depthSrcAccess;
barriers[5].dstStage = dstStage;
barriers[5].dstAccess = depthDstAccess;
auto& depthBarrier = barriers[colorCount];
depthBarrier.image = pp->deferred().msaaDepthImage();
depthBarrier.aspectMask = imageAspectFromFormat(pp->getDepthFormat());
depthBarrier.levelCount = 1;
depthBarrier.layerCount = 1;
depthBarrier.oldLayout = depthOldLayout;
depthBarrier.newLayout = depthNewLayout;
depthBarrier.srcStage = srcStage;
depthBarrier.srcAccess = depthSrcAccess;
depthBarrier.dstStage = dstStage;
depthBarrier.dstAccess = depthDstAccess;
cmdImageBarriers(cmd, ArrayView<const ImageBarrier2>(barriers.data(), barriers.size()));
cmdImageBarriers(cmd, ArrayView<const ImageBarrier2>(barriers.data(), colorCount + 1));
}
// ========== Deferred Lighting ==========
@@ -170,17 +173,20 @@ void vulkan_deferred_lighting_begin(bool clearNonColorBufs)
rpBegin.framebuffer = pp->deferred().msaaFramebuffer();
rpBegin.renderArea.offset = vk::Offset2D(0, 0);
rpBegin.renderArea.extent = extent;
std::array<vk::ClearValue, VulkanDeferredGBuffer::MSAA_COLOR_ATTACHMENT_COUNT + 1> clearValues{};
std::array<vk::ClearValue, VulkanDeferredGBuffer::MSAA_COLOR_ATTACHMENT_COUNT + 2> clearValues{};
clearValues[VulkanDeferredGBuffer::GBUF_ATT_COLOR].color.setFloat32({0.0f, 0.0f, 0.0f, 0.0f});
clearValues[VulkanDeferredGBuffer::GBUF_ATT_POSITION].color.setFloat32({0.0f, 0.0f, 0.0f, 0.0f});
clearValues[VulkanDeferredGBuffer::GBUF_ATT_NORMAL].color.setFloat32({0.0f, 0.0f, 0.0f, 0.0f});
clearValues[VulkanDeferredGBuffer::GBUF_ATT_SPECULAR].color.setFloat32({0.0f, 0.0f, 0.0f, 0.0f});
clearValues[VulkanDeferredGBuffer::MSAA_COLOR_ATTACHMENT_COUNT].depthStencil = vk::ClearDepthStencilValue(1.0f, 0);
if (pp->deferred().velocityActive()) {
clearValues[VulkanDeferredGBuffer::msaaVelocityAttachmentIndex()].color.setFloat32({VulkanMotionVectors::NO_MOTION_WRITTEN, 0.0f, 0.0f, 0.0f});
}
clearValues[pp->deferred().msaaDepthAttachmentIndex()].depthStencil = vk::ClearDepthStencilValue(1.0f, 0);
rpBegin.clearValueCount = static_cast<uint32_t>(clearValues.size());
rpBegin.pClearValues = clearValues.data();
cmd.beginRenderPass(rpBegin, vk::SubpassContents::eInline);
stateTracker->setRenderPass(pp->deferred().msaaRenderPass(), 0);
stateTracker->setColorAttachmentCount(VulkanDeferredGBuffer::MSAA_COLOR_ATTACHMENT_COUNT);
stateTracker->setColorAttachmentCount(pp->deferred().msaaColorAttachmentCount());
stateTracker->setCurrentSampleCount(renderer->getMsaaSampleCount());
}
} else {
@@ -230,15 +236,15 @@ void vulkan_deferred_lighting_begin(bool clearNonColorBufs)
rpBegin.framebuffer = pp->deferred().framebuffer();
rpBegin.renderArea.offset = vk::Offset2D(0, 0);
rpBegin.renderArea.extent = extent;
std::array<vk::ClearValue, VulkanDeferredGBuffer::GBUF_COLOR_ATTACHMENT_COUNT + 1> clearValues{};
clearValues[VulkanDeferredGBuffer::GBUF_COLOR_ATTACHMENT_COUNT].depthStencil = vk::ClearDepthStencilValue(1.0f, 0);
std::array<vk::ClearValue, VulkanDeferredGBuffer::GBUF_COLOR_ATTACHMENT_COUNT + 2> clearValues{};
clearValues[pp->deferred().depthAttachmentIndex()].depthStencil = vk::ClearDepthStencilValue(1.0f, 0);
rpBegin.clearValueCount = static_cast<uint32_t>(clearValues.size());
rpBegin.pClearValues = clearValues.data();
cmd.beginRenderPass(rpBegin, vk::SubpassContents::eInline);
stateTracker->setRenderPass(pp->deferred().renderPassLoad(), 0);
// The G-buffer pass has multiple color attachments; pipelines built while
// it is bound must match, or deferred-on cockpits render incorrectly.
stateTracker->setColorAttachmentCount(VulkanDeferredGBuffer::GBUF_COLOR_ATTACHMENT_COUNT);
stateTracker->setColorAttachmentCount(pp->deferred().colorAttachmentCount());
}
// Optionally clear non-color G-buffer attachments
@@ -315,9 +321,9 @@ void vulkan_deferred_lighting_msaa()
rpBegin.framebuffer = pp->deferred().msaaResolveFramebuffer();
rpBegin.renderArea.offset = vk::Offset2D(0, 0);
rpBegin.renderArea.extent = extent;
// 6 attachments: 5 color + depth. loadOp=eDontCare for all (fully overwritten).
std::array<vk::ClearValue, VulkanDeferredGBuffer::MSAA_COLOR_ATTACHMENT_COUNT + 1> clearValues{};
clearValues[VulkanDeferredGBuffer::MSAA_COLOR_ATTACHMENT_COUNT].depthStencil = vk::ClearDepthStencilValue(1.0f, 0);
// 5 color, optionally motion vectors, then depth. Color and depth are fully overwritten.
std::array<vk::ClearValue, VulkanDeferredGBuffer::MSAA_COLOR_ATTACHMENT_COUNT + 2> clearValues{};
clearValues[pp->deferred().msaaDepthAttachmentIndex()].depthStencil = vk::ClearDepthStencilValue(1.0f, 0);
rpBegin.clearValueCount = static_cast<uint32_t>(clearValues.size());
rpBegin.pClearValues = clearValues.data();
cmd.beginRenderPass(rpBegin, vk::SubpassContents::eInline);
@@ -348,7 +354,8 @@ void vulkan_deferred_lighting_msaa()
config.cullEnabled = false;
config.depthWriteEnabled = true;
config.renderPass = pp->deferred().msaaResolveRenderPass();
config.colorAttachmentCount = 5;
config.colorAttachmentCount = pp->deferred().msaaColorAttachmentCount();
config.writesVelocity = pp->deferred().velocityActive();
vertex_layout emptyLayout;
vk::Pipeline pipeline = pipelineMgr->getPipeline(config, emptyLayout);
@@ -377,7 +384,7 @@ void vulkan_deferred_lighting_msaa()
writer.writeSet(DescriptorSetIndex::Global, globalSet);
// Material set: All 6 MSAA textures in binding 1 array (elements 0-5)
// [0]=color, [1]=position, [2]=normal, [3]=specular, [4]=emissive, [5]=depth
// [0]=color, [1]=position, [2]=normal, [3]=specular, [4]=emissive, [5]=depth, [6]=motion vectors if active
vk::DescriptorSet materialSet = descriptorMgr->allocateFrameSet(DescriptorSetIndex::Material);
Assert(materialSet);
writer.writeSet(DescriptorSetIndex::Material, materialSet);
@@ -397,6 +404,9 @@ void vulkan_deferred_lighting_msaa()
texImages[3] = {nearestSampler, pp->deferred().msaaSpecularView(), vk::ImageLayout::eShaderReadOnlyOptimal};
texImages[4] = {nearestSampler, pp->deferred().msaaEmissiveView(), vk::ImageLayout::eShaderReadOnlyOptimal};
texImages[5] = {nearestSampler, pp->deferred().msaaDepthView(), vk::ImageLayout::eShaderReadOnlyOptimal};
if (pp->deferred().velocityActive()) {
texImages[6] = {nearestSampler, pp->deferred().msaaVelocityView(), vk::ImageLayout::eShaderReadOnlyOptimal};
}
writer.setImageArray(MaterialBinding::TextureArray, texImages);
// PerDraw set: GenericData UBO with {samples, fov} at binding 0
@@ -494,13 +504,13 @@ void vulkan_deferred_lighting_msaa()
rpBegin.framebuffer = pp->deferred().framebuffer();
rpBegin.renderArea.offset = vk::Offset2D(0, 0);
rpBegin.renderArea.extent = extent;
std::array<vk::ClearValue, 7> clearValues{};
clearValues[6].depthStencil = vk::ClearDepthStencilValue(1.0f, 0);
std::array<vk::ClearValue, VulkanDeferredGBuffer::GBUF_COLOR_ATTACHMENT_COUNT + 2> clearValues{};
clearValues[pp->deferred().depthAttachmentIndex()].depthStencil = vk::ClearDepthStencilValue(1.0f, 0);
rpBegin.clearValueCount = static_cast<uint32_t>(clearValues.size());
rpBegin.pClearValues = clearValues.data();
cmd.beginRenderPass(rpBegin, vk::SubpassContents::eInline);
stateTracker->setRenderPass(pp->deferred().renderPassLoad(), 0);
stateTracker->setColorAttachmentCount(VulkanDeferredGBuffer::GBUF_COLOR_ATTACHMENT_COUNT);
stateTracker->setColorAttachmentCount(pp->deferred().colorAttachmentCount());
}
}
@@ -596,7 +606,7 @@ void vulkan_deferred_lighting_finish()
// only write to fragOut0 — using the G-buffer pass would leave undefined
// values at attachment locations 1-5.
renderer->setUseGbufRenderPass(false);
stateTracker->setColorAttachmentCount(1);
stateTracker->setColorAttachmentCount(pp->sceneColorAttachmentCount());
// Resume scene render pass (loadOp=eLoad) with depth preserved
{
@@ -606,9 +616,7 @@ void vulkan_deferred_lighting_finish()
rpBegin.framebuffer = pp->getSceneFramebuffer();
rpBegin.renderArea.offset = vk::Offset2D(0, 0);
rpBegin.renderArea.extent = extent;
std::array<vk::ClearValue, 2> clearValues;
clearValues[0].color.setFloat32({0.0f, 0.0f, 0.0f, 1.0f});
clearValues[1].depthStencil = vk::ClearDepthStencilValue(1.0f, 0);
const auto clearValues = pp->sceneClearValues();
rpBegin.clearValueCount = static_cast<uint32_t>(clearValues.size());
rpBegin.pClearValues = clearValues.data();
cmd.beginRenderPass(rpBegin, vk::SubpassContents::eInline);
@@ -683,10 +691,7 @@ void vulkan_shadow_map_start(matrix4* shadow_view_matrix, const matrix* light_ma
}
}
// vulkan_build_shadow_tlas() (called just before this, every frame shadows
// are enabled) already ends whatever render pass was active -- TLAS builds
// are illegal inside a render pass instance -- and nulls out the tracked
// render pass. Only end it here if that didn't already happen.
// End the active render pass, if the state tracker has one.
if (stateTracker->getCurrentRenderPass()) {
cmd.endRenderPass();
}
@@ -778,13 +783,13 @@ void vulkan_shadow_map_end()
rpBegin.framebuffer = pp->deferred().msaaFramebuffer();
rpBegin.renderArea.offset = vk::Offset2D(0, 0);
rpBegin.renderArea.extent = extent;
std::array<vk::ClearValue, VulkanDeferredGBuffer::MSAA_COLOR_ATTACHMENT_COUNT + 1> clearValues{};
clearValues[VulkanDeferredGBuffer::MSAA_COLOR_ATTACHMENT_COUNT].depthStencil = vk::ClearDepthStencilValue(1.0f, 0);
std::array<vk::ClearValue, VulkanDeferredGBuffer::MSAA_COLOR_ATTACHMENT_COUNT + 2> clearValues{};
clearValues[pp->deferred().msaaDepthAttachmentIndex()].depthStencil = vk::ClearDepthStencilValue(1.0f, 0);
rpBegin.clearValueCount = static_cast<uint32_t>(clearValues.size());
rpBegin.pClearValues = clearValues.data();
cmd.beginRenderPass(rpBegin, vk::SubpassContents::eInline);
stateTracker->setRenderPass(pp->deferred().msaaRenderPassLoad(), 0);
stateTracker->setColorAttachmentCount(VulkanDeferredGBuffer::MSAA_COLOR_ATTACHMENT_COUNT);
stateTracker->setColorAttachmentCount(pp->deferred().msaaColorAttachmentCount());
stateTracker->setCurrentSampleCount(getRendererInstance()->getMsaaSampleCount());
} else {
// Transition scene color: eShaderReadOnlyOptimal → eColorAttachmentOptimal
@@ -818,35 +823,39 @@ void vulkan_shadow_map_end()
rpBegin.renderArea.offset = vk::Offset2D(0, 0);
rpBegin.renderArea.extent = extent;
std::array<vk::ClearValue, 7> clearValues{};
clearValues[6].depthStencil = vk::ClearDepthStencilValue(1.0f, 0);
std::array<vk::ClearValue, VulkanDeferredGBuffer::GBUF_COLOR_ATTACHMENT_COUNT + 2> clearValues{};
clearValues[pp->deferred().depthAttachmentIndex()].depthStencil = vk::ClearDepthStencilValue(1.0f, 0);
rpBegin.clearValueCount = static_cast<uint32_t>(clearValues.size());
rpBegin.pClearValues = clearValues.data();
cmd.beginRenderPass(rpBegin, vk::SubpassContents::eInline);
stateTracker->setRenderPass(pp->deferred().renderPassLoad(), 0);
stateTracker->setColorAttachmentCount(VulkanDeferredGBuffer::GBUF_COLOR_ATTACHMENT_COUNT);
stateTracker->setColorAttachmentCount(pp->deferred().colorAttachmentCount());
}
} else {
renderer->resumeSceneRendering();
}
// Restore viewport and scissor to scene size
{
vk::Viewport viewport;
viewport.x = static_cast<float>(gr_screen.offset_x);
viewport.y = static_cast<float>(gr_screen.offset_y);
viewport.width = static_cast<float>(gr_screen.clip_width);
viewport.height = static_cast<float>(gr_screen.clip_height);
viewport.minDepth = 0.0f;
viewport.maxDepth = 1.0f;
cmd.setViewport(0, viewport);
vulkan_restore_scene_viewport();
}
vk::Rect2D scissor;
scissor.offset = vk::Offset2D(gr_screen.offset_x, gr_screen.offset_y);
scissor.extent = vk::Extent2D(static_cast<uint32_t>(gr_screen.clip_width), static_cast<uint32_t>(gr_screen.clip_height));
cmd.setScissor(0, scissor);
}
void vulkan_restore_scene_viewport()
{
vk::CommandBuffer cmd = getStateTracker()->getCommandBuffer();
vk::Viewport viewport;
viewport.x = static_cast<float>(gr_screen.offset_x);
viewport.y = static_cast<float>(gr_screen.offset_y);
viewport.width = static_cast<float>(gr_screen.clip_width);
viewport.height = static_cast<float>(gr_screen.clip_height);
viewport.minDepth = 0.0f;
viewport.maxDepth = 1.0f;
cmd.setViewport(0, viewport);
vk::Rect2D scissor;
scissor.offset = vk::Offset2D(gr_screen.offset_x, gr_screen.offset_y);
scissor.extent = vk::Extent2D(static_cast<uint32_t>(gr_screen.clip_width), static_cast<uint32_t>(gr_screen.clip_height));
cmd.setScissor(0, scissor);
}
// ========== Decal Pass ==========
@@ -903,14 +912,14 @@ void vulkan_start_decal_pass()
rpBegin.renderArea.offset = vk::Offset2D(0, 0);
rpBegin.renderArea.extent = extent;
std::array<vk::ClearValue, 7> clearValues{};
clearValues[6].depthStencil = vk::ClearDepthStencilValue(1.0f, 0);
std::array<vk::ClearValue, VulkanDeferredGBuffer::GBUF_COLOR_ATTACHMENT_COUNT + 2> clearValues{};
clearValues[pp->deferred().depthAttachmentIndex()].depthStencil = vk::ClearDepthStencilValue(1.0f, 0);
rpBegin.clearValueCount = static_cast<uint32_t>(clearValues.size());
rpBegin.pClearValues = clearValues.data();
cmd.beginRenderPass(rpBegin, vk::SubpassContents::eInline);
stateTracker->setRenderPass(pp->deferred().renderPassLoad(), 0);
stateTracker->setColorAttachmentCount(VulkanDeferredGBuffer::GBUF_COLOR_ATTACHMENT_COUNT);
stateTracker->setColorAttachmentCount(pp->deferred().colorAttachmentCount());
}
// Restore viewport (Y-flipped for Vulkan scene rendering)
+4
View File
@@ -24,6 +24,10 @@ void vulkan_override_fog(bool set_override);
void vulkan_shadow_map_start(matrix4* shadow_view_matrix, const matrix* light_matrix, vec3d* eye_pos, bool first_pass);
void vulkan_shadow_map_end();
// Sets the viewport and scissor to the current gr_screen clip area. Call this after a render
// pass resumes in the middle of a scene, because the resume sets a full-extent viewport.
void vulkan_restore_scene_viewport();
// Decal pass
void vulkan_start_decal_pass();
void vulkan_stop_decal_pass();
@@ -15,6 +15,7 @@ static constexpr DescriptorBindingTemplate s_globalBindings[] = {
{GlobalBinding::EnvMap, vk::DescriptorType::eCombinedImageSampler, 1, vk::ShaderStageFlagBits::eFragment, vk::ImageViewType::eCube},
{GlobalBinding::IrradianceMap, vk::DescriptorType::eCombinedImageSampler, 1, vk::ShaderStageFlagBits::eFragment, vk::ImageViewType::eCube},
{GlobalBinding::ShadowCascadeParams, vk::DescriptorType::eUniformBuffer, 1, vk::ShaderStageFlagBits::eVertex | vk::ShaderStageFlagBits::eFragment},
{GlobalBinding::ShadowMapRaw, vk::DescriptorType::eCombinedImageSampler, 1, vk::ShaderStageFlagBits::eFragment, vk::ImageViewType::e2DArray},
};
static constexpr DescriptorBindingTemplate s_globalTlasBinding{
GlobalBinding::Tlas, vk::DescriptorType::eAccelerationStructureKHR, 1, vk::ShaderStageFlagBits::eFragment};
@@ -78,6 +78,7 @@ namespace GlobalBinding {
static constexpr uint32_t IrradianceMap = 4; // samplerCube: irradiance map
static constexpr uint32_t Tlas = 5; // accelerationStructureEXT: raytraced shadow TLAS (only present when raytraced shadows are supported)
static constexpr uint32_t ShadowCascadeParams = 6; // UBO: shadow cascade projection matrices/distances (per-frame, shared by all consumers)
static constexpr uint32_t ShadowMapRaw = 7; // sampler2DArray: raw (non-compare) shadow map read, for PCSS blocker search
}
// Material Set (Set 1) bindings — per-material data
+9 -3
View File
@@ -236,7 +236,7 @@ void VulkanDrawManager::clear()
// gr_screen dimensions can exceed the active render pass when rendering to a
// smaller off-screen target; vkCmdClearAttachments requires rects inside the
// render area.
clearRect.rect = stateTracker->clampToRenderArea(clearRect.rect);
clearRect.rect = stateTracker->clampToRenderArea(stateTracker->screenToFramebuffer(clearRect.rect));
if (clearRect.rect.extent.width == 0 || clearRect.rect.extent.height == 0) {
return;
}
@@ -422,7 +422,7 @@ void VulkanDrawManager::zbufferClear(int mode)
clearRect.rect.offset = vk::Offset2D(0, 0);
clearRect.rect.extent = vk::Extent2D(static_cast<uint32_t>(gr_screen.max_w),
static_cast<uint32_t>(gr_screen.max_h));
clearRect.rect = stateTracker->clampToRenderArea(clearRect.rect);
clearRect.rect = stateTracker->clampToRenderArea(stateTracker->screenToFramebuffer(clearRect.rect));
clearRect.baseArrayLayer = 0;
clearRect.layerCount = 1;
@@ -1116,6 +1116,10 @@ PipelineConfig VulkanDrawManager::buildPipelineConfig(material* mat, primitive_t
config.renderPass = stateTracker->getCurrentRenderPass();
config.colorAttachmentCount = stateTracker->getColorAttachmentCount();
config.sampleCount = stateTracker->getCurrentSampleCount();
config.writesVelocity = mat->get_velocity_output()
&& (config.shaderType == SDR_TYPE_MODEL || config.shaderType == SDR_TYPE_BATCHED_BITMAP
|| config.shaderType == SDR_TYPE_EFFECT_PARTICLE)
&& config.colorWriteMask.x;
return config;
}
@@ -1127,7 +1131,8 @@ bool VulkanDrawManager::bindMaterialTextures(material* mat, DescriptorWriter* wr
// Get sampler matching current texture addressing mode and fallback texture
vk::SamplerAddressMode addressMode = convertTextureAddressing(m_textureAddressing);
vk::Sampler sampler = texManager->getSampler(
vk::Filter::eLinear, vk::Filter::eLinear, addressMode, true, 0.0f, true);
vk::Filter::eLinear, vk::Filter::eLinear, addressMode, true, 0.0f, true,
mat->get_texture_lod_bias() ? getStateTracker()->getMaterialLodBias() : 0.0f);
// OpenGL skips applying texture addressing for AABITMAP, INTERFACE, and CUBEMAP
// types - they always stay clamped. We need a clamp sampler for those cases.
vk::Sampler clampSampler = texManager->getSampler(
@@ -1437,6 +1442,7 @@ bool VulkanDrawManager::applyMaterial(material* mat, primitive_type prim_type, v
bindPendingUBOs(DescriptorSetIndex::Global);
if (shadowReady) {
writer.setImage(GlobalBinding::ShadowMap, pp->getShadowTextureInfo());
writer.setImage(GlobalBinding::ShadowMapRaw, pp->getShadowRawTextureInfo());
}
m_cachedGlobalHadShadow = shadowReady;
m_globalSetDirty = false;
+34 -6
View File
@@ -11,6 +11,7 @@
#include "VulkanTexture.h"
#include "VulkanRenderer.h"
#include "VulkanPostProcessing.h"
#include "VulkanDeferred.h"
#include "VulkanDescriptorManager.h"
#include "VulkanDeletionQueue.h"
#include "VulkanMemory.h"
@@ -216,7 +217,30 @@ void vulkan_scene_texture_begin()
// Switch to HDR scene render pass when post-processing is enabled
auto* pp = getPostProcessor();
if (pp && pp->isInitialized() && Gr_post_processing_enabled && !PostProcessing_override) {
const bool useSceneTarget = pp && pp->isInitialized() && Gr_post_processing_enabled && !PostProcessing_override;
// Build the ray-tracing TLAS for this frame here, one time, before the scene draws start.
// The camera and the objects are final at this point. A TLAS build must be outside a render
// pass. The scene pass starts below, so the build adds no pass break to that path.
if (vulkan_frame_tlas_pending() && !renderer->isSceneRendering()) {
renderer->endCurrentRenderPass();
vulkan_build_frame_tlas();
if (!useSceneTarget) {
// No scene pass follows, so resume the pass that the build interrupted.
auto* texMgr = getTextureManager();
const int rtHandle = (renderer->isRenderTargetActive() && texMgr) ? texMgr->getCurrentRenderTarget() : -1;
auto* rtSlot = rtHandle >= 0 ? texMgr->getTextureSlot(rtHandle) : nullptr;
if (rtSlot != nullptr) {
renderer->resumeRenderTargetPass(rtSlot);
} else {
renderer->resumeSwapChainPass();
}
vulkan_restore_scene_viewport();
}
}
if (useSceneTarget) {
renderer->beginSceneRendering();
High_dynamic_range = true;
} else {
@@ -236,7 +260,7 @@ void vulkan_scene_texture_begin()
clearRect.rect.offset = vk::Offset2D(0, 0);
clearRect.rect.extent = vk::Extent2D(static_cast<uint32_t>(gr_screen.max_w),
static_cast<uint32_t>(gr_screen.max_h));
clearRect.rect = stateTracker->clampToRenderArea(clearRect.rect);
clearRect.rect = stateTracker->clampToRenderArea(stateTracker->screenToFramebuffer(clearRect.rect));
clearRect.baseArrayLayer = 0;
clearRect.layerCount = 1;
@@ -401,8 +425,10 @@ void vulkan_render_primitives_particle(particle_material* material_info,
sizeof(graphics::generic_data::effect_data));
auto* data = buffer.aligner().addTypedElement<graphics::generic_data::effect_data>();
data->window_width = static_cast<float>(gr_screen.max_w);
data->window_height = static_cast<float>(gr_screen.max_h);
// The shader divides gl_FragCoord by these, so they're the pass's size, which is larger than gr_screen on the
// composition image while FSR upscales
data->window_width = static_cast<float>(getStateTracker()->getRenderArea().width);
data->window_height = static_cast<float>(getStateTracker()->getRenderArea().height);
data->nearZ = Min_draw_distance;
data->farZ = Max_draw_distance;
data->srgb = High_dynamic_range ? 1 : 0;
@@ -411,6 +437,7 @@ void vulkan_render_primitives_particle(particle_material* material_info,
// so linear_depth=1 reads .z directly (matches OpenGL behavior).
// Otherwise use the NDC conversion path with the hardware depth copy.
data->linear_depth = usePosTexture ? 1 : 0;
gr_get_velocity_uniforms(&data->velocity);
buffer.submitData();
gr_bind_uniform_buffer(uniform_block_type::GenericData, buffer.getBufferOffset(0),
@@ -459,8 +486,8 @@ void vulkan_render_primitives_distortion(distortion_material* material_info,
sizeof(graphics::generic_data::effect_distort_data));
auto* data = buffer.aligner().addTypedElement<graphics::generic_data::effect_distort_data>();
data->window_width = static_cast<float>(gr_screen.max_w);
data->window_height = static_cast<float>(gr_screen.max_h);
data->window_width = static_cast<float>(getStateTracker()->getRenderArea().width);
data->window_height = static_cast<float>(getStateTracker()->getRenderArea().height);
data->use_offset = material_info->get_thruster_rendering() ? 1.0f : 0.0f;
buffer.submitData();
@@ -546,6 +573,7 @@ void vulkan_render_primitives_batched(batched_bitmap_material* material_info,
auto* data = buffer.aligner().addTypedElement<graphics::generic_data::batched_data>();
data->intensity = material_info->get_color_scale();
data->color = material_info->get_color();
gr_get_velocity_uniforms(&data->velocity);
buffer.submitData();
gr_bind_uniform_buffer(uniform_block_type::GenericData, buffer.getBufferOffset(0),
sizeof(graphics::generic_data::batched_data), buffer.bufferHandle());
+1 -1
View File
@@ -54,7 +54,7 @@ void blit_to_swapchain_image(VulkanRenderer* renderer, vk::Image dst, uint32_t d
{
const vk::CommandBuffer cmd = renderer->getCurrentCommandBuffer();
const vk::Image composition = renderer->getCurrentCompositionImage();
const vk::Extent2D srcExtent = renderer->getRenderExtent();
const vk::Extent2D srcExtent = renderer->getCompositionExtent();
if (!cmd || !composition || !dst) {
return;
+18
View File
@@ -52,6 +52,7 @@ bool PipelineConfig::operator==(const PipelineConfig& other) const
renderPass == other.renderPass &&
subpass == other.subpass &&
colorAttachmentCount == other.colorAttachmentCount &&
writesVelocity == other.writesVelocity &&
sampleCount == other.sampleCount &&
perAttachmentBlendEnabled == other.perAttachmentBlendEnabled &&
[&]() {
@@ -126,6 +127,7 @@ uint64_t PipelineConfig::hash() const
hashCombine(h, handleToU64(static_cast<VkRenderPass>(renderPass)));
hashCombine(h, subpass);
hashCombine(h, colorAttachmentCount);
hashCombine(h, writesVelocity ? 1u : 0u);
hashCombine(h, static_cast<uint64_t>(sampleCount));
hashCombine(h, perAttachmentBlendEnabled ? 1u : 0u);
if (perAttachmentBlendEnabled) {
@@ -456,6 +458,22 @@ vk::UniquePipeline VulkanPipelineManager::createPipeline(const PipelineConfig& c
}
}
if (config.colorAttachmentCount > PipelineConfig::VELOCITY_ATTACHMENT) {
auto& velocity = colorBlendAttachments[PipelineConfig::VELOCITY_ATTACHMENT];
velocity = vk::PipelineColorBlendAttachmentState{};
if (config.writesVelocity) {
// The shader's alpha chooses per fragment whether to replace the motion already there.
velocity.blendEnable = VK_TRUE;
velocity.srcColorBlendFactor = vk::BlendFactor::eSrcAlpha;
velocity.dstColorBlendFactor = vk::BlendFactor::eOneMinusSrcAlpha;
velocity.colorBlendOp = vk::BlendOp::eAdd;
velocity.srcAlphaBlendFactor = vk::BlendFactor::eOne;
velocity.dstAlphaBlendFactor = vk::BlendFactor::eZero;
velocity.alphaBlendOp = vk::BlendOp::eAdd;
velocity.colorWriteMask = vk::ColorComponentFlagBits::eR | vk::ColorComponentFlagBits::eG | vk::ColorComponentFlagBits::eB;
}
}
vk::PipelineColorBlendStateCreateInfo colorBlending;
colorBlending.logicOpEnable = VK_FALSE;
colorBlending.attachmentCount = static_cast<uint32_t>(colorBlendAttachments.size());
+5
View File
@@ -61,6 +61,11 @@ struct PipelineConfig {
// Color attachment count (for multiple render targets)
uint32_t colorAttachmentCount = 1;
// Motion vector output location on passes that have one (colorAttachmentCount above it). Everything else
// must leave it alone: a shader that doesn't write a location leaves the attachment undefined.
static constexpr uint32_t VELOCITY_ATTACHMENT = 6;
bool writesVelocity = false;
// MSAA sample count (default e1 = no multisampling)
vk::SampleCountFlagBits sampleCount = vk::SampleCountFlagBits::e1;
+227 -6
View File
@@ -14,6 +14,7 @@
#include "graphics/grinternal.h"
#include "graphics/shadows.h"
#include "graphics/2d.h"
#include "graphics/matrix.h"
#include "lighting/lighting_profiles.h"
#include "lighting/lighting.h"
#include "math/vecmat.h"
@@ -53,6 +54,7 @@ bool VulkanPostProcessor::init(vk::Device device, vk::PhysicalDevice physDevice,
m_ctx.device = device;
m_ctx.memoryManager = memMgr;
m_ctx.sceneExtent = extent;
m_ctx.outputExtent = extent;
m_ctx.depthFormat = depthFormat;
m_ctx.hdrActive = hdrActive;
// In HDR the tonemapped scene must be able to exceed paper white, so use fp16.
@@ -348,6 +350,8 @@ bool VulkanPostProcessor::init(vk::Device device, vk::PhysicalDevice physDevice,
// Wire the deferred light accumulation + fog subsystems (resources are lazy).
m_lighting.init(m_ctx, m_sceneColor, m_deferred, m_shadow);
m_fog.init(m_ctx, m_sceneColor, m_sceneDepth, m_sceneDepthCopy, m_deferred);
m_motionVectors.init(m_ctx);
m_upscaler.init(m_ctx);
// Initialize MSAA resources if MSAA is enabled and G-buffer is ready
if (m_deferred.isInitialized() && Cmdline_msaa_enabled > 0) {
@@ -368,6 +372,8 @@ void VulkanPostProcessor::shutdown()
if (m_ctx.device) {
m_ctx.device.waitIdle();
m_upscaler.destroyTargets();
m_motionVectors.shutdown();
m_fog.shutdown();
shutdownShadowPass();
shadow_cascade_params_shutdown();
@@ -391,6 +397,14 @@ void VulkanPostProcessor::shutdown()
}
destroySceneTargets();
if (m_velocitySceneRenderPassLoad) {
m_ctx.device.destroyRenderPass(m_velocitySceneRenderPassLoad);
m_velocitySceneRenderPassLoad = nullptr;
}
if (m_velocitySceneRenderPass) {
m_ctx.device.destroyRenderPass(m_velocitySceneRenderPass);
m_velocitySceneRenderPass = nullptr;
}
if (m_sceneRenderPassLoad) {
m_ctx.device.destroyRenderPass(m_sceneRenderPassLoad);
m_sceneRenderPassLoad = nullptr;
@@ -490,6 +504,10 @@ void VulkanPostProcessor::destroySceneTargets()
m_ctx.device.destroyFramebuffer(m_sceneFramebuffer);
m_sceneFramebuffer = nullptr;
}
if (m_velocitySceneFramebuffer) {
m_ctx.device.destroyFramebuffer(m_velocitySceneFramebuffer);
m_velocitySceneFramebuffer = nullptr;
}
m_ctx.destroyTarget(m_sceneEffect);
m_ctx.destroyTarget(m_sceneColor);
m_ctx.destroyTarget(m_sceneDepth);
@@ -518,17 +536,218 @@ bool VulkanPostProcessor::createSceneFramebuffer()
return true;
}
bool VulkanPostProcessor::resize(vk::Extent2D newExtent)
bool VulkanPostProcessor::createVelocitySceneResources()
{
if (!m_velocitySceneRenderPass) {
// Same as the plain scene passes, with motion vectors at the location the model shader writes them.
const auto createPass = [this](vk::AttachmentLoadOp loadOp, vk::ImageLayout colorInitial, vk::ImageLayout depthInitial) {
std::array<vk::AttachmentDescription, 3> attachments;
attachments[0].format = HDR_COLOR_FORMAT;
attachments[0].loadOp = loadOp;
attachments[0].storeOp = vk::AttachmentStoreOp::eStore;
attachments[0].stencilLoadOp = vk::AttachmentLoadOp::eDontCare;
attachments[0].stencilStoreOp = vk::AttachmentStoreOp::eDontCare;
attachments[0].initialLayout = colorInitial;
attachments[0].finalLayout = vk::ImageLayout::eShaderReadOnlyOptimal;
attachments[1].format = VulkanMotionVectors::DRAWN_FORMAT;
attachments[1].loadOp = loadOp;
attachments[1].storeOp = vk::AttachmentStoreOp::eStore;
attachments[1].stencilLoadOp = vk::AttachmentLoadOp::eDontCare;
attachments[1].stencilStoreOp = vk::AttachmentStoreOp::eDontCare;
attachments[1].initialLayout = colorInitial;
attachments[1].finalLayout = vk::ImageLayout::eColorAttachmentOptimal;
attachments[2].format = m_ctx.depthFormat;
attachments[2].loadOp = loadOp;
attachments[2].storeOp = vk::AttachmentStoreOp::eStore;
attachments[2].stencilLoadOp = loadOp;
attachments[2].stencilStoreOp = vk::AttachmentStoreOp::eStore;
attachments[2].initialLayout = depthInitial;
attachments[2].finalLayout = vk::ImageLayout::eDepthStencilAttachmentOptimal;
std::array<vk::AttachmentReference, VulkanDeferredGBuffer::VELOCITY_COLOR_LOCATION_COUNT> colorRefs;
for (auto& ref : colorRefs) {
ref.attachment = VK_ATTACHMENT_UNUSED;
ref.layout = vk::ImageLayout::eUndefined;
}
colorRefs[0].attachment = 0;
colorRefs[0].layout = vk::ImageLayout::eColorAttachmentOptimal;
colorRefs[PipelineConfig::VELOCITY_ATTACHMENT].attachment = 1;
colorRefs[PipelineConfig::VELOCITY_ATTACHMENT].layout = vk::ImageLayout::eColorAttachmentOptimal;
vk::AttachmentReference depthRef;
depthRef.attachment = 2;
depthRef.layout = vk::ImageLayout::eDepthStencilAttachmentOptimal;
vk::SubpassDescription subpass;
subpass.pipelineBindPoint = vk::PipelineBindPoint::eGraphics;
subpass.colorAttachmentCount = static_cast<uint32_t>(colorRefs.size());
subpass.pColorAttachments = colorRefs.data();
subpass.pDepthStencilAttachment = &depthRef;
vk::SubpassDependency dependency;
dependency.srcSubpass = VK_SUBPASS_EXTERNAL;
dependency.dstSubpass = 0;
dependency.srcStageMask = vk::PipelineStageFlagBits::eColorAttachmentOutput
| vk::PipelineStageFlagBits::eEarlyFragmentTests
| vk::PipelineStageFlagBits::eLateFragmentTests
| vk::PipelineStageFlagBits::eFragmentShader
| vk::PipelineStageFlagBits::eTransfer;
dependency.dstStageMask = vk::PipelineStageFlagBits::eColorAttachmentOutput
| vk::PipelineStageFlagBits::eEarlyFragmentTests;
dependency.srcAccessMask = vk::AccessFlagBits::eColorAttachmentWrite
| vk::AccessFlagBits::eDepthStencilAttachmentWrite
| vk::AccessFlagBits::eShaderRead
| vk::AccessFlagBits::eTransferRead;
dependency.dstAccessMask = vk::AccessFlagBits::eColorAttachmentWrite
| vk::AccessFlagBits::eColorAttachmentRead
| vk::AccessFlagBits::eDepthStencilAttachmentWrite
| vk::AccessFlagBits::eDepthStencilAttachmentRead;
vk::RenderPassCreateInfo rpInfo;
rpInfo.attachmentCount = static_cast<uint32_t>(attachments.size());
rpInfo.pAttachments = attachments.data();
rpInfo.subpassCount = 1;
rpInfo.pSubpasses = &subpass;
rpInfo.dependencyCount = 1;
rpInfo.pDependencies = &dependency;
return m_ctx.device.createRenderPass(rpInfo);
};
try {
m_velocitySceneRenderPass = createPass(vk::AttachmentLoadOp::eClear,
vk::ImageLayout::eUndefined, vk::ImageLayout::eUndefined);
m_velocitySceneRenderPassLoad = createPass(vk::AttachmentLoadOp::eLoad,
vk::ImageLayout::eColorAttachmentOptimal, vk::ImageLayout::eDepthStencilAttachmentOptimal);
} catch (const vk::SystemError& e) {
nprintf(("vulkan", "VulkanPostProcessor: Failed to create motion vector scene passes: %s\n", e.what()));
return false;
}
}
if (!m_velocitySceneFramebuffer) {
std::array<vk::ImageView, 3> views = {m_sceneColor.view, m_motionVectors.drawnMotion().view, m_sceneDepth.view};
vk::FramebufferCreateInfo fbInfo;
fbInfo.renderPass = m_velocitySceneRenderPass;
fbInfo.attachmentCount = static_cast<uint32_t>(views.size());
fbInfo.pAttachments = views.data();
fbInfo.width = m_ctx.sceneExtent.width;
fbInfo.height = m_ctx.sceneExtent.height;
fbInfo.layers = 1;
try {
m_velocitySceneFramebuffer = m_ctx.device.createFramebuffer(fbInfo);
} catch (const vk::SystemError& e) {
nprintf(("vulkan", "VulkanPostProcessor: Failed to create motion vector scene framebuffer: %s\n", e.what()));
return false;
}
}
return true;
}
void VulkanPostProcessor::prepareMotionVectors()
{
const bool active = VulkanMotionVectors::wanted()
&& m_motionVectors.createResources()
&& createVelocitySceneResources()
&& m_deferred.setVelocityTarget(&m_motionVectors.drawnMotion());
if (!active) {
m_deferred.setVelocityTarget(nullptr);
}
m_velocityActive = active;
Gr_motion_vectors_active = active;
}
void VulkanPostProcessor::setPostProcessInput(const RenderTarget& input)
{
m_bloom.setSceneColor(input);
m_ldr.setSceneColor(input);
if (m_bloom.isInitialized() && !m_bloom.resize()) {
nprintf(("vulkan", "VulkanPostProcessor: Bloom rebuild failed, disabling bloom\n"));
m_bloom.shutdown();
}
}
void VulkanPostProcessor::prepareUpscaler()
{
if (m_upscaler.versionChanged()) {
m_ctx.device.waitIdle();
m_upscaler.destroyTargets();
// Bloom and tonemapping must not keep the destroyed output, even if the new version then fails
setPostProcessInput(m_sceneColor);
m_upscalerActive = false;
}
const bool active = m_velocityActive && VulkanUpscaler::wanted() && m_upscaler.createResources();
if (active != m_upscalerActive) {
m_ctx.device.waitIdle();
m_upscalerActive = active;
setPostProcessInput(postProcessInput());
gr_reset_temporal_history();
}
float imageOffsetX = 0.0f;
float imageOffsetY = 0.0f;
float lodBias = 0.0f;
if (active) {
m_upscaler.beginFrame(imageOffsetX, imageOffsetY, lodBias);
}
getStateTracker()->setUpscalerSceneState(imageOffsetX, imageOffsetY, lodBias);
}
void VulkanPostProcessor::executeUpscaler(vk::CommandBuffer cmd)
{
if (!m_upscalerActive) {
return;
}
if (!m_upscaler.dispatch(cmd, m_sceneColor, m_motionVectors.depth(), m_motionVectors.motion())) {
// The rest of this frame must not read the output FSR never wrote. Bloom hasn't been recorded yet, and
// the frames still in flight may use its targets, which rebuilding them around the scene color replaces.
m_ctx.device.waitIdle();
m_upscalerActive = false;
setPostProcessInput(m_sceneColor);
}
}
std::array<vk::ClearValue, 3> VulkanPostProcessor::sceneClearValues() const
{
std::array<vk::ClearValue, 3> clearValues;
clearValues[0].color.setFloat32({0.0f, 0.0f, 0.0f, 1.0f});
if (m_velocityActive) {
clearValues[1].color.setFloat32({VulkanMotionVectors::NO_MOTION_WRITTEN, 0.0f, 0.0f, 0.0f});
clearValues[2].depthStencil = vk::ClearDepthStencilValue(1.0f, 0);
} else {
clearValues[1].depthStencil = vk::ClearDepthStencilValue(1.0f, 0);
}
return clearValues;
}
bool VulkanPostProcessor::resize(vk::Extent2D newExtent, vk::Extent2D outputExtent)
{
if (!m_initialized) {
return false;
}
if (newExtent == m_ctx.sceneExtent) {
if (newExtent == m_ctx.sceneExtent && outputExtent == m_ctx.outputExtent) {
return true;
}
// Caller (recreateSwapChain) has already waited for the device to go idle.
// Caller has already waited for the device to go idle.
m_ctx.sceneExtent = newExtent;
m_ctx.outputExtent = outputExtent;
// Recreated at the new size on next use. Bloom is rebuilt around the input below.
m_upscaler.destroyTargets();
m_upscalerActive = false;
m_bloom.setSceneColor(m_sceneColor);
m_ldr.setSceneColor(m_sceneColor);
// Scene targets first: every subsystem framebuffer that wires scene
// color/depth needs the recreated views.
@@ -570,6 +789,8 @@ bool VulkanPostProcessor::resize(vk::Extent2D newExtent)
nprintf(("vulkan", "VulkanPostProcessor: Fog resize failed, disabling fog\n"));
m_fog.shutdown();
}
// Recreated at the new size on next use.
m_motionVectors.destroyTargets();
// Distortion (fixed 32x32) and the shadow map (sized from Shadow_quality)
// are extent-independent.
@@ -676,8 +897,8 @@ void VulkanPostProcessor::blitToSwapChain(vk::CommandBuffer cmd)
// Set viewport (non-flipped for post-processing — textures are already
// in the correct Vulkan orientation, no Y-flip needed)
stateTracker->setViewport(0.0f, 0.0f,
static_cast<float>(m_ctx.sceneExtent.width),
static_cast<float>(m_ctx.sceneExtent.height));
static_cast<float>(m_ctx.outputExtent.width),
static_cast<float>(m_ctx.outputExtent.height));
stateTracker->applyDynamicState();
@@ -694,7 +915,7 @@ void VulkanPostProcessor::blitToSwapChain(vk::CommandBuffer cmd)
texArrayInfos[0].sampler = m_ctx.linearSampler;
texArrayInfos[0].imageView = m_ldr.postEffectsApplied() ? m_ldr.luminanceView()
: useLdr ? m_ldr.ldrView()
: m_sceneColor.view;
: postProcessInput().view;
writer.setImageArray(MaterialBinding::TextureArray, texArrayInfos);
}
+184 -16
View File
@@ -27,6 +27,8 @@ struct PostProcessContext {
vk::Device device;
VulkanMemoryManager* memoryManager = nullptr;
vk::Extent2D sceneExtent;
// Everything from tonemapping to the final blit. Larger than the scene while FSR upscales.
vk::Extent2D outputExtent;
vk::Format depthFormat = vk::Format::eUndefined;
// True when the renderer negotiated an HDR10 swap chain. Drives fp16 LDR
@@ -113,7 +115,9 @@ struct PostProcessContext {
vk::Framebuffer framebuffer, vk::Extent2D extent,
int shaderType,
const vk::ImageView* views, uint32_t viewCount,
const void* uboData, size_t uboSize);
const void* uboData, size_t uboSize,
vk::Sampler sampler = nullptr,
uint32_t colorAttachmentCount = 1);
/**
* @brief Generate a mip chain for an image (transitions mip 0 first)
@@ -204,6 +208,7 @@ public:
vk::ImageView depthView() const { return m_depth.view; }
vk::Image depthImage() const { return m_depth.image; }
vk::Sampler compareSampler() const { return m_compareSampler; }
vk::Sampler rawSampler() const { return m_rawSampler; }
vk::RenderPass renderPass() const { return m_renderPass; }
vk::Framebuffer framebuffer() const { return m_framebuffer; }
@@ -211,6 +216,7 @@ private:
PostProcessContext* m_ctx = nullptr;
RenderTarget m_depth; // D32F, 2D array (Num_shadow_cascades + Num_cockpit_shadow_cascades layers)
vk::Sampler m_compareSampler; // Depth-compare sampler for hardware PCF (sampler2DArrayShadow)
vk::Sampler m_rawSampler; // Non-compare sampler on the same view, for PCSS blocker search
vk::RenderPass m_renderPass;
vk::Framebuffer m_framebuffer;
int m_textureSize = 0;
@@ -235,6 +241,9 @@ public:
bool init(PostProcessContext& ctx, const RenderTarget& sceneColor);
void shutdown();
// The caller follows up with resize(), which rebuilds the framebuffer around it.
void setSceneColor(const RenderTarget& sceneColor) { m_sceneColor = &sceneColor; }
/**
* @brief Recreate the extent-sized targets/framebuffers (render passes kept)
*
@@ -298,6 +307,8 @@ public:
static constexpr uint32_t GBUF_COLOR_ATTACHMENT_COUNT = 6; // Full layout (with composite)
static constexpr uint32_t MSAA_COLOR_ATTACHMENT_COUNT = 5; // MSAA layout (without composite)
// With motion vectors the subpass has this many color locations, in both layouts. MSAA leaves location 5 unused.
static constexpr uint32_t VELOCITY_COLOR_LOCATION_COUNT = 7;
static constexpr vk::Format GBUF_FORMAT_COLOR = vk::Format::eR16G16B16A16Sfloat;
static constexpr vk::Format GBUF_FORMAT_POSITION = vk::Format::eR16G16B16A16Sfloat;
@@ -330,6 +341,21 @@ public:
bool isInitialized() const { return m_gbufInitialized; }
bool isMsaaInitialized() const { return m_msaaInitialized; }
/**
* @brief Selects the render pass variants with a motion vector attachment for this frame
* @param drawnMotion The single-sample image to write into. Null selects the plain variants.
* @return false if the resources couldn't be created, in which case the plain variants stay selected
*/
bool setVelocityTarget(const RenderTarget* drawnMotion);
bool velocityActive() const { return m_velocityActive; }
uint32_t colorAttachmentCount() const { return m_velocityActive ? VELOCITY_COLOR_LOCATION_COUNT : GBUF_COLOR_ATTACHMENT_COUNT; }
uint32_t msaaColorAttachmentCount() const { return m_velocityActive ? VELOCITY_COLOR_LOCATION_COUNT : MSAA_COLOR_ATTACHMENT_COUNT; }
// Attachment indices into the clear values, since the unused MSAA location takes no attachment
static uint32_t velocityAttachmentIndex() { return GBUF_COLOR_ATTACHMENT_COUNT; }
uint32_t depthAttachmentIndex() const { return GBUF_COLOR_ATTACHMENT_COUNT + (m_velocityActive ? 1 : 0); }
static uint32_t msaaVelocityAttachmentIndex() { return MSAA_COLOR_ATTACHMENT_COUNT; }
uint32_t msaaDepthAttachmentIndex() const { return MSAA_COLOR_ATTACHMENT_COUNT + (m_velocityActive ? 1 : 0); }
// Mid-frame layout transitions / copies (all outside a render pass)
void transitionForResume(vk::CommandBuffer cmd);
void copyNormal(vk::CommandBuffer cmd);
@@ -337,9 +363,9 @@ public:
void transitionMsaaForResume(vk::CommandBuffer cmd);
// G-buffer accessors
vk::RenderPass renderPass() const { return m_gbufRenderPass; }
vk::RenderPass renderPassLoad() const { return m_gbufRenderPassLoad; }
vk::Framebuffer framebuffer() const { return m_gbufFramebuffer; }
vk::RenderPass renderPass() const { return m_velocityActive ? m_velocityRenderPass : m_gbufRenderPass; }
vk::RenderPass renderPassLoad() const { return m_velocityActive ? m_velocityRenderPassLoad : m_gbufRenderPassLoad; }
vk::Framebuffer framebuffer() const { return m_velocityActive ? m_velocityFramebuffer : m_gbufFramebuffer; }
vk::ImageView positionView() const { return m_gbufPosition.view; }
vk::ImageView normalView() const { return m_gbufNormal.view; }
vk::ImageView specularView() const { return m_gbufSpecular.view; }
@@ -351,11 +377,13 @@ public:
vk::Image normalImage() const { return m_gbufNormal.image; }
// MSAA accessors
vk::RenderPass msaaRenderPass() const { return m_msaaGbufRenderPass; }
vk::RenderPass msaaRenderPassLoad() const { return m_msaaGbufRenderPassLoad; }
vk::Framebuffer msaaFramebuffer() const { return m_msaaGbufFramebuffer; }
vk::RenderPass msaaResolveRenderPass() const { return m_msaaResolveRenderPass; }
vk::Framebuffer msaaResolveFramebuffer() const { return m_msaaResolveFramebuffer; }
vk::RenderPass msaaRenderPass() const { return m_velocityActive ? m_velocityMsaaRenderPass : m_msaaGbufRenderPass; }
vk::RenderPass msaaRenderPassLoad() const { return m_velocityActive ? m_velocityMsaaRenderPassLoad : m_msaaGbufRenderPassLoad; }
vk::Framebuffer msaaFramebuffer() const { return m_velocityActive ? m_velocityMsaaFramebuffer : m_msaaGbufFramebuffer; }
vk::RenderPass msaaResolveRenderPass() const { return m_velocityActive ? m_velocityResolveRenderPass : m_msaaResolveRenderPass; }
vk::Framebuffer msaaResolveFramebuffer() const { return m_velocityActive ? m_velocityResolveFramebuffer : m_msaaResolveFramebuffer; }
vk::ImageView msaaVelocityView() const { return m_msaaVelocity.view; }
vk::Image msaaVelocityImage() const { return m_msaaVelocity.image; }
vk::RenderPass msaaEmissiveCopyRenderPass() const { return m_msaaEmissiveCopyRenderPass; }
vk::Framebuffer msaaEmissiveCopyFramebuffer() const { return m_msaaEmissiveCopyFramebuffer; }
vk::ImageView msaaColorView() const { return m_msaaColor.view; }
@@ -382,10 +410,16 @@ private:
vk::ImageLayout depthInitialLayout;
bool useResolveDependency = false;
SCP_unordered_map<uint32_t, vk::AttachmentLoadOp> attachmentLoadOpOverrides = {};
bool includeVelocity = false;
vk::AttachmentLoadOp velocityLoadOp = vk::AttachmentLoadOp::eClear;
};
vk::RenderPass createGbufRenderPass(const GbufRenderPassConfig& config);
vk::Framebuffer createGbufFramebuffer(vk::RenderPass renderPass, bool includeComposite,
bool useMsaaImages);
bool useMsaaImages, const RenderTarget* velocity = nullptr);
bool createVelocityRenderPasses();
bool createVelocityTargets(const RenderTarget& drawnMotion);
void destroyVelocityTargets();
void destroyVelocityRenderPasses();
bool createTargets();
void destroyTargets();
@@ -425,6 +459,92 @@ private:
vk::RenderPass m_msaaEmissiveCopyRenderPass; // 1 MS color att (for upsample)
vk::Framebuffer m_msaaEmissiveCopyFramebuffer;
bool m_msaaInitialized = false;
// ---- Variants with a motion vector attachment, created on first use ----
bool m_velocityActive = false;
RenderTarget m_msaaVelocity;
vk::RenderPass m_velocityRenderPass;
vk::RenderPass m_velocityRenderPassLoad;
vk::Framebuffer m_velocityFramebuffer;
vk::RenderPass m_velocityMsaaRenderPass;
vk::RenderPass m_velocityMsaaRenderPassLoad;
vk::Framebuffer m_velocityMsaaFramebuffer;
vk::RenderPass m_velocityResolveRenderPass;
vk::Framebuffer m_velocityResolveFramebuffer;
};
/**
* @brief Per-pixel screen motion for FSR
*
* Holds the total motion of each pixel since last frame, in image UV units, unjittered, pointing at where the
* pixel was. Resources are created on first use, because most configurations never need them.
*/
class VulkanMotionVectors {
public:
static constexpr vk::Format FORMAT = vk::Format::eR16G16Sfloat;
// What geometry draws: its motion in xy, and in z the inverse of its view depth, 1/w of its clip position. The
// depth comes along so that it always belongs to whatever drew the motion, on any projection.
static constexpr vk::Format DRAWN_FORMAT = vk::Format::eR16G16B16A16Sfloat;
static constexpr vk::Format DEPTH_FORMAT = vk::Format::eR32Sfloat;
// Well outside any real motion. Tested in motion-vector-f.sdr and msaa-f.sdr.
static constexpr float NO_MOTION_WRITTEN = 1000.0f;
static bool wanted();
void init(PostProcessContext& ctx);
void destroyTargets();
void shutdown();
bool createResources();
const RenderTarget& drawnMotion() const { return m_drawnMotion; }
// Outside a render pass, with the drawn motion attachment still in eColorAttachmentOptimal and sceneDepthCopy
// holding this frame's final depth.
void execute(vk::CommandBuffer cmd, vk::ImageView sceneDepthCopy);
const RenderTarget& motion() const { return m_target; }
const RenderTarget& depth() const { return m_depth; }
private:
PostProcessContext* m_ctx = nullptr;
RenderTarget m_drawnMotion; // what geometry wrote during the scene
RenderTarget m_target; // the drawn motion, with camera motion filled in everywhere else
RenderTarget m_depth; // the depth that motion belongs to, including the cockpit, on the cockpit's projection
vk::RenderPass m_renderPass;
vk::Framebuffer m_framebuffer;
};
/**
* @brief FSR: temporal reconstruction of the HDR scene, ahead of bloom and tonemapping
*
* Owns AMD's context and the image FSR writes. Resources are created on first use.
*/
class VulkanUpscaler {
public:
void init(PostProcessContext& ctx) { m_ctx = &ctx; }
void destroyTargets();
static bool wanted();
// The context was made for a different version than the one now chosen
bool versionChanged() const { return m_context && m_contextMajorVersion != Gr_fsr_major_version; }
bool createResources();
const RenderTarget& output() const { return m_output; }
// Before the scene's first pass. The image offset to draw this frame with, and the texture LOD bias to go with it.
void beginFrame(float& imageOffsetX, float& imageOffsetY, float& lodBias);
// Outside a render pass, with all inputs in eShaderReadOnlyOptimal. Leaves the output in eShaderReadOnlyOptimal.
// False if FSR failed, in which case the output holds nothing usable.
bool dispatch(vk::CommandBuffer cmd, const RenderTarget& color, const RenderTarget& depth, const RenderTarget& motion);
private:
PostProcessContext* m_ctx = nullptr;
RenderTarget m_output;
void* m_context = nullptr; // ffxContext
int m_contextMajorVersion = 0;
int m_jitterPhaseCount = 1;
int m_jitterIndex = 0;
float m_jitterX = 0.0f;
float m_jitterY = 0.0f;
};
/**
@@ -603,6 +723,8 @@ public:
const VulkanBloom& bloom);
void shutdown();
void setSceneColor(const RenderTarget& sceneColor) { m_sceneColor = &sceneColor; }
/**
* @brief Recreate the extent-sized targets/framebuffers (render passes kept)
*
@@ -776,14 +898,16 @@ public:
/**
* @brief Recreate all extent-sized targets/framebuffers for a new resolution
*
* Called from VulkanRenderer::recreateSwapChain() after the device-idle
* wait. Render passes, samplers, and the scratch UBO ring are extent-
* Called by VulkanRenderer after a device-idle wait, when the swap chain or
* the FSR output size changes. Render passes, samplers, and the scratch UBO ring are extent-
* independent and kept alive, so every cached pipeline stays valid.
* Individual subsystem failures are non-fatal (the subsystem is disabled,
* matching init()); returns false only if the core scene targets could not
* be recreated.
*
* @param outputExtent Size from tonemapping on, which FSR upscales the scene to
*/
bool resize(vk::Extent2D newExtent);
bool resize(vk::Extent2D newExtent, vk::Extent2D outputExtent);
/**
* @brief Per-frame reset (called from VulkanRenderer::setupFrame after the frame fence wait)
@@ -801,7 +925,7 @@ public:
* This render pass has RGBA16F color + depth attachments with loadOp=eClear.
* Used between scene_texture_begin() and scene_texture_end().
*/
vk::RenderPass getSceneRenderPass() const { return m_sceneRenderPass; }
vk::RenderPass getSceneRenderPass() const { return m_velocityActive ? m_velocitySceneRenderPass : m_sceneRenderPass; }
/**
* @brief Get the HDR scene render pass with loadOp=eLoad
@@ -810,12 +934,12 @@ public:
* the same framebuffer. Used to resume scene rendering after
* copy_effect_texture interrupts the pass.
*/
vk::RenderPass getSceneRenderPassLoad() const { return m_sceneRenderPassLoad; }
vk::RenderPass getSceneRenderPassLoad() const { return m_velocityActive ? m_velocitySceneRenderPassLoad : m_sceneRenderPassLoad; }
/**
* @brief Get the HDR scene framebuffer
*/
vk::Framebuffer getSceneFramebuffer() const { return m_sceneFramebuffer; }
vk::Framebuffer getSceneFramebuffer() const { return m_velocityActive ? m_velocitySceneFramebuffer : m_sceneFramebuffer; }
/**
* @brief Get the scene rendering extent
@@ -925,6 +1049,29 @@ public:
*/
void executeSMAA(vk::CommandBuffer cmd) { m_smaa.execute(cmd); }
// Selects whether this frame's scene writes motion vectors. Before the scene's first render pass.
void prepareMotionVectors();
void executeMotionVectors(vk::CommandBuffer cmd)
{
if (m_velocityActive) {
copySceneDepth(cmd);
m_motionVectors.execute(cmd, m_sceneDepthCopy.view);
}
}
// Selects whether FSR runs for this frame's scene. After prepareMotionVectors(), which it depends on.
void prepareUpscaler();
void executeUpscaler(vk::CommandBuffer cmd);
uint32_t sceneColorAttachmentCount() const
{
return m_velocityActive ? VulkanDeferredGBuffer::VELOCITY_COLOR_LOCATION_COUNT : 1;
}
// Attachment count is 2 or 3. The extra clear value is ignored on the plain passes.
std::array<vk::ClearValue, 3> sceneClearValues() const;
/**
* @brief Execute post-processing effects (saturation, brightness, etc.)
*
@@ -1111,6 +1258,16 @@ public:
return {m_shadow.compareSampler(), m_shadow.depthView(), vk::ImageLayout::eShaderReadOnlyOptimal};
}
/**
* @brief Get a ready-to-use DescriptorImageInfo for raw (uncompared) shadow map reads
*
* Same image view as getShadowTextureInfo(), but with compare mode off, for the PCSS
* blocker search (which needs actual depth values, not a compare result).
*/
vk::DescriptorImageInfo getShadowRawTextureInfo() const {
return {m_shadow.rawSampler(), m_shadow.depthView(), vk::ImageLayout::eShaderReadOnlyOptimal};
}
// ========== Fog / Volumetric Nebula ==========
/**
@@ -1217,6 +1374,17 @@ private:
// ---- Fog / volumetric nebula (self-contained subsystem) ----
VulkanFog m_fog;
VulkanMotionVectors m_motionVectors;
bool createVelocitySceneResources();
bool m_velocityActive = false;
vk::RenderPass m_velocitySceneRenderPass;
vk::RenderPass m_velocitySceneRenderPassLoad;
vk::Framebuffer m_velocitySceneFramebuffer;
VulkanUpscaler m_upscaler;
bool m_upscalerActive = false;
// Bloom and tonemapping read from here, which is FSR's output while FSR runs
const RenderTarget& postProcessInput() const { return m_upscalerActive ? m_upscaler.output() : m_sceneColor; }
void setPostProcessInput(const RenderTarget& input);
// ---- Distortion (ping-pong textures, self-contained subsystem) ----
VulkanDistortion m_distortion;
@@ -139,8 +139,9 @@ bool VulkanBloom::init(PostProcessContext& ctx, const RenderTarget& sceneColor)
bool VulkanBloom::createTargets()
{
m_width = m_ctx->sceneExtent.width / 2;
m_height = m_ctx->sceneExtent.height / 2;
// Sized from the input, which is FSR's output while it runs and the scene otherwise
m_width = m_sceneColor->width / 2;
m_height = m_sceneColor->height / 2;
const uint32_t mipLevels = MAX_MIP_BLUR_LEVELS;
@@ -238,8 +239,8 @@ bool VulkanBloom::createTargets()
fbInfo.renderPass = m_compositeRenderPass;
fbInfo.attachmentCount = 1;
fbInfo.pAttachments = &m_sceneColor->view;
fbInfo.width = m_ctx->sceneExtent.width;
fbInfo.height = m_ctx->sceneExtent.height;
fbInfo.width = m_sceneColor->width;
fbInfo.height = m_sceneColor->height;
fbInfo.layers = 1;
try {
@@ -405,7 +406,7 @@ void VulkanBloom::execute(vk::CommandBuffer cmd)
m_ctx->drawFullscreenTriangle(cmd, m_compositeRenderPass,
m_sceneColorFB,
m_ctx->sceneExtent,
vk::Extent2D(m_sceneColor->width, m_sceneColor->height),
SDR_TYPE_POST_PROCESS_BLOOM_COMP,
m_tex[0].fullView, m_ctx->mipmapSampler,
&compData, sizeof(compData),
@@ -220,7 +220,8 @@ void PostProcessContext::drawFullscreenTriangleMulti(vk::CommandBuffer cmd, vk::
vk::Framebuffer framebuffer, vk::Extent2D extent,
int shaderType,
const vk::ImageView* views, uint32_t viewCount,
const void* uboData, size_t uboSize)
const void* uboData, size_t uboSize,
vk::Sampler sampler, uint32_t colorAttachmentCount)
{
GR_DEBUG_SCOPE("Draw full screen triangle (multi-view)");
@@ -242,6 +243,7 @@ void PostProcessContext::drawFullscreenTriangleMulti(vk::CommandBuffer cmd, vk::
config.cullEnabled = false;
config.depthWriteEnabled = false;
config.renderPass = renderPass;
config.colorAttachmentCount = colorAttachmentCount;
vertex_layout emptyLayout;
vk::Pipeline pipeline = pipelineMgr->getPipeline(config, emptyLayout);
@@ -284,7 +286,7 @@ void PostProcessContext::drawFullscreenTriangleMulti(vk::CommandBuffer cmd, vk::
std::array<vk::DescriptorImageInfo, VulkanDescriptorManager::MAX_TEXTURE_BINDINGS> texArrayInfos;
texArrayInfos.fill(descriptorMgr->getFallbacks().texture2D);
for (uint32_t i = 0; i < viewCount; i++) {
texArrayInfos[i] = {linearSampler, views[i], vk::ImageLayout::eShaderReadOnlyOptimal};
texArrayInfos[i] = {sampler ? sampler : linearSampler, views[i], vk::ImageLayout::eShaderReadOnlyOptimal};
}
writer.setImageArray(MaterialBinding::TextureArray, texArrayInfos);
}
@@ -2,8 +2,11 @@
#include <array>
#include "gr_vulkan.h"
#include "VulkanRenderer.h"
#include "VulkanBarrier.h"
#include "VulkanPipeline.h"
#include "VulkanTexture.h"
#include "lighting/lighting_profiles.h"
#include "lighting/lighting.h"
#include "nebula/neb.h"
@@ -35,11 +38,12 @@ vk::RenderPass VulkanDeferredGBuffer::createGbufRenderPass(const GbufRenderPassC
const uint32_t colorCount = config.includeComposite
? GBUF_COLOR_ATTACHMENT_COUNT : MSAA_COLOR_ATTACHMENT_COUNT;
const uint32_t depthIndex = colorCount;
const uint32_t totalAttachments = colorCount + 1;
const uint32_t velocityIndex = colorCount;
const uint32_t depthIndex = colorCount + (config.includeVelocity ? 1 : 0);
const uint32_t totalAttachments = depthIndex + 1;
// Max 6 color + 1 depth = 7 attachments
std::array<vk::AttachmentDescription, 7> attachments;
// Max 6 color + 1 velocity + 1 depth = 8 attachments
std::array<vk::AttachmentDescription, 8> attachments;
for (uint32_t i = 0; i < colorCount; ++i) {
attachments[i].format = COLOR_FORMATS[i];
attachments[i].samples = config.samples;
@@ -52,6 +56,21 @@ vk::RenderPass VulkanDeferredGBuffer::createGbufRenderPass(const GbufRenderPassC
attachments[i].finalLayout = config.colorFinalLayout;
}
// Motion vectors stay an attachment through the whole scene, which spares every mid-scene copy from having
// to transition them around each pass end and resume.
if (config.includeVelocity) {
auto& velocity = attachments[velocityIndex];
velocity.format = VulkanMotionVectors::DRAWN_FORMAT;
velocity.samples = config.samples;
velocity.loadOp = config.velocityLoadOp;
velocity.storeOp = vk::AttachmentStoreOp::eStore;
velocity.stencilLoadOp = vk::AttachmentLoadOp::eDontCare;
velocity.stencilStoreOp = vk::AttachmentStoreOp::eDontCare;
velocity.initialLayout = config.velocityLoadOp == vk::AttachmentLoadOp::eLoad
? vk::ImageLayout::eColorAttachmentOptimal : vk::ImageLayout::eUndefined;
velocity.finalLayout = vk::ImageLayout::eColorAttachmentOptimal;
}
// Depth — stencil ops mirror the depth loadOp
attachments[depthIndex].format = m_ctx->depthFormat;
attachments[depthIndex].samples = config.samples;
@@ -66,11 +85,19 @@ vk::RenderPass VulkanDeferredGBuffer::createGbufRenderPass(const GbufRenderPassC
attachments[depthIndex].initialLayout = config.depthInitialLayout;
attachments[depthIndex].finalLayout = vk::ImageLayout::eDepthStencilAttachmentOptimal;
std::array<vk::AttachmentReference, 6> colorRefs;
std::array<vk::AttachmentReference, VELOCITY_COLOR_LOCATION_COUNT> colorRefs;
for (auto& ref : colorRefs) {
ref.attachment = VK_ATTACHMENT_UNUSED;
ref.layout = vk::ImageLayout::eUndefined;
}
for (uint32_t i = 0; i < colorCount; ++i) {
colorRefs[i].attachment = i;
colorRefs[i].layout = vk::ImageLayout::eColorAttachmentOptimal;
}
if (config.includeVelocity) {
colorRefs[PipelineConfig::VELOCITY_ATTACHMENT].attachment = velocityIndex;
colorRefs[PipelineConfig::VELOCITY_ATTACHMENT].layout = vk::ImageLayout::eColorAttachmentOptimal;
}
vk::AttachmentReference depthRef;
depthRef.attachment = depthIndex;
@@ -78,7 +105,7 @@ vk::RenderPass VulkanDeferredGBuffer::createGbufRenderPass(const GbufRenderPassC
vk::SubpassDescription subpass;
subpass.pipelineBindPoint = vk::PipelineBindPoint::eGraphics;
subpass.colorAttachmentCount = colorCount;
subpass.colorAttachmentCount = config.includeVelocity ? VELOCITY_COLOR_LOCATION_COUNT : colorCount;
subpass.pColorAttachments = colorRefs.data();
subpass.pDepthStencilAttachment = &depthRef;
@@ -104,7 +131,9 @@ vk::RenderPass VulkanDeferredGBuffer::createGbufRenderPass(const GbufRenderPassC
dependency.srcAccessMask = vk::AccessFlagBits::eColorAttachmentWrite
| vk::AccessFlagBits::eDepthStencilAttachmentWrite
| vk::AccessFlagBits::eShaderRead;
// The read covers the motion vectors, which the resolve loads and blends
dependency.dstAccessMask = vk::AccessFlagBits::eColorAttachmentWrite
| vk::AccessFlagBits::eColorAttachmentRead
| vk::AccessFlagBits::eDepthStencilAttachmentWrite;
} else {
// Standard G-buffer: previous pass may have done transfers (copies), and
@@ -121,7 +150,9 @@ vk::RenderPass VulkanDeferredGBuffer::createGbufRenderPass(const GbufRenderPassC
| vk::AccessFlagBits::eShaderRead
| vk::AccessFlagBits::eTransferRead
| vk::AccessFlagBits::eTransferWrite;
// The read covers attachments the load variants keep, such as the motion vectors
dependency.dstAccessMask = vk::AccessFlagBits::eColorAttachmentWrite
| vk::AccessFlagBits::eColorAttachmentRead
| vk::AccessFlagBits::eDepthStencilAttachmentWrite
| vk::AccessFlagBits::eDepthStencilAttachmentRead;
}
@@ -138,10 +169,10 @@ vk::RenderPass VulkanDeferredGBuffer::createGbufRenderPass(const GbufRenderPassC
}
vk::Framebuffer VulkanDeferredGBuffer::createGbufFramebuffer(
vk::RenderPass renderPass, bool includeComposite, bool useMsaaImages)
vk::RenderPass renderPass, bool includeComposite, bool useMsaaImages, const RenderTarget* velocity)
{
// Attachment order: color, position, normal, specular, emissive, [composite], depth
std::array<vk::ImageView, 7> views;
// Attachment order: color, position, normal, specular, emissive, [composite], [velocity], depth
std::array<vk::ImageView, 8> views;
uint32_t count = 0;
if (useMsaaImages) {
@@ -162,6 +193,10 @@ vk::Framebuffer VulkanDeferredGBuffer::createGbufFramebuffer(
views[count++] = m_gbufComposite.view;
}
if (velocity) {
views[count++] = velocity->view;
}
views[count++] = useMsaaImages ? m_msaaDepthView : m_sceneDepth->view;
vk::FramebufferCreateInfo fbInfo;
@@ -284,8 +319,143 @@ bool VulkanDeferredGBuffer::createTargets()
return true;
}
bool VulkanDeferredGBuffer::createVelocityRenderPasses()
{
if (m_velocityRenderPass) {
return true;
}
try {
m_velocityRenderPass = createGbufRenderPass({
true, vk::SampleCountFlagBits::e1,
vk::AttachmentLoadOp::eClear, vk::AttachmentLoadOp::eClear,
vk::ImageLayout::eUndefined, vk::ImageLayout::eShaderReadOnlyOptimal,
vk::ImageLayout::eUndefined,
false, {}, true, vk::AttachmentLoadOp::eClear,
});
m_velocityRenderPassLoad = createGbufRenderPass({
true, vk::SampleCountFlagBits::e1,
vk::AttachmentLoadOp::eLoad, vk::AttachmentLoadOp::eLoad,
vk::ImageLayout::eColorAttachmentOptimal, vk::ImageLayout::eShaderReadOnlyOptimal,
vk::ImageLayout::eDepthStencilAttachmentOptimal,
false, {}, true, vk::AttachmentLoadOp::eLoad,
});
if (m_msaaInitialized) {
const auto samples = getRendererInstance()->getMsaaSampleCount();
m_velocityMsaaRenderPass = createGbufRenderPass({
false, samples,
vk::AttachmentLoadOp::eClear, vk::AttachmentLoadOp::eClear,
vk::ImageLayout::eColorAttachmentOptimal, vk::ImageLayout::eColorAttachmentOptimal,
vk::ImageLayout::eDepthStencilAttachmentOptimal,
false, {{GBUF_ATT_EMISSIVE, vk::AttachmentLoadOp::eLoad}}, true, vk::AttachmentLoadOp::eClear,
});
m_velocityMsaaRenderPassLoad = createGbufRenderPass({
false, samples,
vk::AttachmentLoadOp::eLoad, vk::AttachmentLoadOp::eLoad,
vk::ImageLayout::eColorAttachmentOptimal, vk::ImageLayout::eColorAttachmentOptimal,
vk::ImageLayout::eDepthStencilAttachmentOptimal,
false, {}, true, vk::AttachmentLoadOp::eLoad,
});
// Loads rather than discards velocity, because the cockpit resolves on top of the main scene's.
m_velocityResolveRenderPass = createGbufRenderPass({
false, vk::SampleCountFlagBits::e1,
vk::AttachmentLoadOp::eDontCare, vk::AttachmentLoadOp::eDontCare,
vk::ImageLayout::eUndefined, vk::ImageLayout::eShaderReadOnlyOptimal,
vk::ImageLayout::eUndefined,
true, {}, true, vk::AttachmentLoadOp::eLoad,
});
}
} catch (const vk::SystemError& e) {
nprintf(("vulkan", "VulkanDeferredGBuffer: Failed to create motion vector render passes: %s\n", e.what()));
destroyVelocityRenderPasses();
return false;
}
return true;
}
void VulkanDeferredGBuffer::destroyVelocityRenderPasses()
{
for (auto* pass : {&m_velocityRenderPass, &m_velocityRenderPassLoad, &m_velocityMsaaRenderPass,
&m_velocityMsaaRenderPassLoad, &m_velocityResolveRenderPass}) {
if (*pass) {
m_ctx->device.destroyRenderPass(*pass);
*pass = nullptr;
}
}
}
bool VulkanDeferredGBuffer::createVelocityTargets(const RenderTarget& drawnMotion)
{
if (m_velocityFramebuffer) {
return true;
}
try {
m_velocityFramebuffer = createGbufFramebuffer(m_velocityRenderPass, true, false, &drawnMotion);
if (m_msaaInitialized) {
if (!m_ctx->createImage(m_ctx->sceneExtent.width, m_ctx->sceneExtent.height, VulkanMotionVectors::DRAWN_FORMAT,
vk::ImageUsageFlagBits::eColorAttachment | vk::ImageUsageFlagBits::eSampled,
vk::ImageAspectFlagBits::eColor,
m_msaaVelocity.image, m_msaaVelocity.view, m_msaaVelocity.allocation,
getRendererInstance()->getMsaaSampleCount())) {
nprintf(("vulkan", "VulkanDeferredGBuffer: Failed to create MSAA motion vector image!\n"));
destroyVelocityTargets();
return false;
}
m_msaaVelocity.format = VulkanMotionVectors::DRAWN_FORMAT;
m_msaaVelocity.width = m_ctx->sceneExtent.width;
m_msaaVelocity.height = m_ctx->sceneExtent.height;
m_velocityMsaaFramebuffer = createGbufFramebuffer(m_velocityMsaaRenderPass, false, true, &m_msaaVelocity);
m_velocityResolveFramebuffer = createGbufFramebuffer(m_velocityResolveRenderPass, false, false, &drawnMotion);
getTextureManager()->transitionImageLayout(m_msaaVelocity.image, m_msaaVelocity.format,
vk::ImageLayout::eUndefined, vk::ImageLayout::eColorAttachmentOptimal);
}
} catch (const vk::SystemError& e) {
nprintf(("vulkan", "VulkanDeferredGBuffer: Failed to create motion vector framebuffers: %s\n", e.what()));
destroyVelocityTargets();
return false;
}
return true;
}
void VulkanDeferredGBuffer::destroyVelocityTargets()
{
for (auto* framebuffer : {&m_velocityFramebuffer, &m_velocityMsaaFramebuffer, &m_velocityResolveFramebuffer}) {
if (*framebuffer) {
m_ctx->device.destroyFramebuffer(*framebuffer);
*framebuffer = nullptr;
}
}
m_ctx->destroyTarget(m_msaaVelocity);
}
bool VulkanDeferredGBuffer::setVelocityTarget(const RenderTarget* drawnMotion)
{
m_velocityActive = false;
if (!drawnMotion || !m_gbufInitialized) {
return drawnMotion == nullptr;
}
if (!createVelocityRenderPasses() || !createVelocityTargets(*drawnMotion)) {
return false;
}
m_velocityActive = true;
return true;
}
void VulkanDeferredGBuffer::destroyTargets()
{
// Rebuilt at the new size when next selected
destroyVelocityTargets();
if (m_gbufFramebuffer) {
m_ctx->device.destroyFramebuffer(m_gbufFramebuffer);
m_gbufFramebuffer = nullptr;
@@ -331,6 +501,7 @@ void VulkanDeferredGBuffer::shutdown()
}
destroyTargets();
destroyVelocityRenderPasses();
if (m_gbufRenderPassLoad) {
m_ctx->device.destroyRenderPass(m_gbufRenderPassLoad);
@@ -144,7 +144,7 @@ bool VulkanLDR::init(PostProcessContext& ctx, const RenderTarget& sceneColor, co
m_initialized = true;
nprintf(("vulkan", "VulkanLDR: LDR targets initialized (%ux%u, RGBA8)\n",
m_ctx->sceneExtent.width, m_ctx->sceneExtent.height));
m_ctx->outputExtent.width, m_ctx->outputExtent.height));
return true;
}
@@ -154,7 +154,7 @@ bool VulkanLDR::createTargets()
// Create Scene_ldr (full resolution) — tonemapped display-referred output.
// fp16 in HDR (carries values above paper white), 8-bit UNORM in SDR.
if (!m_ctx->createImage(m_ctx->sceneExtent.width, m_ctx->sceneExtent.height, ldrFormat,
if (!m_ctx->createImage(m_ctx->outputExtent.width, m_ctx->outputExtent.height, ldrFormat,
vk::ImageUsageFlagBits::eColorAttachment | vk::ImageUsageFlagBits::eSampled,
vk::ImageAspectFlagBits::eColor,
m_sceneLdr.image, m_sceneLdr.view, m_sceneLdr.allocation)) {
@@ -162,11 +162,11 @@ bool VulkanLDR::createTargets()
return false;
}
m_sceneLdr.format = ldrFormat;
m_sceneLdr.width = m_ctx->sceneExtent.width;
m_sceneLdr.height = m_ctx->sceneExtent.height;
m_sceneLdr.width = m_ctx->outputExtent.width;
m_sceneLdr.height = m_ctx->outputExtent.height;
// Create Scene_luminance (full resolution) — LDR with luma in alpha for FXAA
if (!m_ctx->createImage(m_ctx->sceneExtent.width, m_ctx->sceneExtent.height, ldrFormat,
if (!m_ctx->createImage(m_ctx->outputExtent.width, m_ctx->outputExtent.height, ldrFormat,
vk::ImageUsageFlagBits::eColorAttachment | vk::ImageUsageFlagBits::eSampled,
vk::ImageAspectFlagBits::eColor,
m_sceneLuminance.image, m_sceneLuminance.view, m_sceneLuminance.allocation)) {
@@ -174,13 +174,13 @@ bool VulkanLDR::createTargets()
return false;
}
m_sceneLuminance.format = ldrFormat;
m_sceneLuminance.width = m_ctx->sceneExtent.width;
m_sceneLuminance.height = m_ctx->sceneExtent.height;
m_sceneLuminance.width = m_ctx->outputExtent.width;
m_sceneLuminance.height = m_ctx->outputExtent.height;
// Create Scene_ldr_compressed (full resolution) — tonemapped to [0,1] using the
// real tone curve, purely for FXAA/SMAA edge detection while HDR output is
// active (see executeTonemap/getAADetectionView).
if (!m_ctx->createImage(m_ctx->sceneExtent.width, m_ctx->sceneExtent.height, ldrFormat,
if (!m_ctx->createImage(m_ctx->outputExtent.width, m_ctx->outputExtent.height, ldrFormat,
vk::ImageUsageFlagBits::eColorAttachment | vk::ImageUsageFlagBits::eSampled,
vk::ImageAspectFlagBits::eColor,
m_sceneLdrCompressed.image, m_sceneLdrCompressed.view, m_sceneLdrCompressed.allocation)) {
@@ -188,8 +188,8 @@ bool VulkanLDR::createTargets()
return false;
}
m_sceneLdrCompressed.format = ldrFormat;
m_sceneLdrCompressed.width = m_ctx->sceneExtent.width;
m_sceneLdrCompressed.height = m_ctx->sceneExtent.height;
m_sceneLdrCompressed.width = m_ctx->outputExtent.width;
m_sceneLdrCompressed.height = m_ctx->outputExtent.height;
// Create framebuffers
{
@@ -197,8 +197,8 @@ bool VulkanLDR::createTargets()
fbInfo.renderPass = m_ldrRenderPass;
fbInfo.attachmentCount = 1;
fbInfo.pAttachments = &m_sceneLdr.view;
fbInfo.width = m_ctx->sceneExtent.width;
fbInfo.height = m_ctx->sceneExtent.height;
fbInfo.width = m_ctx->outputExtent.width;
fbInfo.height = m_ctx->outputExtent.height;
fbInfo.layers = 1;
try {
@@ -313,7 +313,7 @@ void VulkanLDR::executeTonemap(vk::CommandBuffer cmd)
// HDR scene → Scene_ldr via tonemapping shader
m_ctx->drawFullscreenTriangle(cmd, m_ldrRenderPass,
m_sceneLdrFB, m_ctx->sceneExtent,
m_sceneLdrFB, m_ctx->outputExtent,
SDR_TYPE_POST_PROCESS_TONEMAPPING,
m_sceneColor->view, m_ctx->linearSampler,
&tmData, sizeof(tmData),
@@ -328,7 +328,7 @@ void VulkanLDR::executeTonemap(vk::CommandBuffer cmd)
// rather than re-running exposure+tonemap from Scene_color a second time.
if (m_ctx->hdrActive) {
m_ctx->drawFullscreenTriangle(cmd, m_ldrRenderPass,
m_sceneLdrCompressedFB, m_ctx->sceneExtent,
m_sceneLdrCompressedFB, m_ctx->outputExtent,
SDR_TYPE_COPY,
m_sceneLdr.view, m_ctx->linearSampler,
nullptr, 0,
@@ -357,7 +357,7 @@ void VulkanLDR::executeFXAA(vk::CommandBuffer cmd)
// alpha computed from the detection proxy, see getAADetectionView)
{
std::array<vk::ImageView, 2> views = {m_sceneLdr.view, getAADetectionView()};
m_ctx->drawFullscreenTriangleMulti(cmd, m_ldrRenderPass, m_sceneLuminanceFB, m_ctx->sceneExtent,
m_ctx->drawFullscreenTriangleMulti(cmd, m_ldrRenderPass, m_sceneLuminanceFB, m_ctx->outputExtent,
SDR_TYPE_POST_PROCESS_FXAA_PREPASS, views.data(), static_cast<uint32_t>(views.size()),
nullptr, 0);
}
@@ -388,13 +388,13 @@ void VulkanLDR::executeFXAA(vk::CommandBuffer cmd)
// FXAA main pass: Scene_luminance → Scene_ldr (loadOp=eLoad preserves non-edge pixels)
graphics::generic_data::fxaa_data fxaaData;
fxaaData.rt_w = static_cast<float>(m_ctx->sceneExtent.width);
fxaaData.rt_h = static_cast<float>(m_ctx->sceneExtent.height);
fxaaData.rt_w = static_cast<float>(m_ctx->outputExtent.width);
fxaaData.rt_h = static_cast<float>(m_ctx->outputExtent.height);
fxaaData.pad[0] = 0.0f;
fxaaData.pad[1] = 0.0f;
m_ctx->drawFullscreenTriangle(cmd, m_ldrLoadRenderPass,
m_sceneLdrFB, m_ctx->sceneExtent,
m_sceneLdrFB, m_ctx->outputExtent,
SDR_TYPE_POST_PROCESS_FXAA,
m_sceneLuminance.view, m_ctx->linearSampler,
&fxaaData, sizeof(fxaaData),
@@ -493,7 +493,7 @@ bool VulkanLDR::executePostEffects(vk::CommandBuffer cmd)
// Post-effects: Scene_ldr → Scene_luminance (reusing luminance target as temp)
m_ctx->drawFullscreenTriangle(cmd, m_ldrRenderPass,
m_sceneLuminanceFB, m_ctx->sceneExtent,
m_sceneLuminanceFB, m_ctx->outputExtent,
SDR_TYPE_POST_PROCESS_MAIN,
m_sceneLdr.view, m_ctx->linearSampler,
&uboData, sizeof(uboData),
@@ -593,7 +593,7 @@ void VulkanLDR::executeLightshafts(vk::CommandBuffer cmd)
// Additive blend lightshafts onto Scene_ldr
m_ctx->drawFullscreenTriangle(cmd, m_ldrLoadRenderPass,
m_sceneLdrFB, m_ctx->sceneExtent,
m_sceneLdrFB, m_ctx->outputExtent,
SDR_TYPE_POST_PROCESS_LIGHTSHAFTS,
m_sceneDepth->view, m_ctx->linearSampler,
&lsData, sizeof(lsData),
@@ -13,6 +13,7 @@
#include "graphics/grinternal.h"
#include "graphics/light.h"
#include "graphics/matrix.h"
#include "graphics/rtao.h"
#include "graphics/shadows.h"
#include "graphics/2d.h"
#include "bmpman/bmpman.h"
@@ -509,9 +510,9 @@ void VulkanDeferredLighting::render(vk::CommandBuffer cmd)
header->invScreenWidth = 1.0f / gr_screen.max_w;
header->invScreenHeight = 1.0f / gr_screen.max_h;
header->nearPlane = gr_near_plane;
shadows_fill_deferred_global_data(*header);
if (m_shadow->isInitialized() && Shadow_quality != ShadowQuality::Disabled) {
vm_inverse_matrix4(&header->inv_view_matrix, &Shadow_view_matrix_render);
shadow_cascade_params_bind_deferred();
}
}
@@ -687,6 +688,9 @@ void VulkanDeferredLighting::render(vk::CommandBuffer cmd)
if (rtShadowsActive) {
lightShaderFlags |= SDR_FLAG_DEFERRED_RT_SHADOWS;
}
if (rtao_enabled()) {
lightShaderFlags |= SDR_FLAG_DEFERRED_RTAO;
}
if (envMapAvailable) {
lightShaderFlags |= SDR_FLAG_ENV_MAP;
}
@@ -754,8 +758,10 @@ void VulkanDeferredLighting::render(vk::CommandBuffer cmd)
// Shadow map is depth-only, sampled with a depth-compare sampler
// (sampler2DArrayShadow) for hardware PCF.
vk::DescriptorImageInfo shadowTexInfo;
vk::DescriptorImageInfo shadowRawTexInfo;
if (m_shadow->isInitialized() && m_shadow->depthView()) {
shadowTexInfo = {m_shadow->compareSampler(), m_shadow->depthView(), vk::ImageLayout::eShaderReadOnlyOptimal};
shadowRawTexInfo = {m_shadow->rawSampler(), m_shadow->depthView(), vk::ImageLayout::eShaderReadOnlyOptimal};
}
// Shadow cascade params (projection matrices/distances) are bound per-frame via
@@ -801,6 +807,7 @@ void VulkanDeferredLighting::render(vk::CommandBuffer cmd)
writer.setImage(GlobalBinding::EnvMap, envTexInfo);
writer.setImage(GlobalBinding::IrradianceMap, irrTexInfo);
writer.setBuffer(GlobalBinding::ShadowCascadeParams, shadowCascadeParamsInfo);
writer.setImage(GlobalBinding::ShadowMapRaw, shadowRawTexInfo);
// Set 1: Material
vk::DescriptorSet materialSet = descriptorMgr->allocateFrameSet(DescriptorSetIndex::Material);
@@ -0,0 +1,202 @@
#include "VulkanPostProcessing.h"
#include "gr_vulkan.h"
#include "VulkanBarrier.h"
#include "VulkanRenderer.h"
#include "VulkanTexture.h"
#include "graphics/matrix.h"
#include "graphics/openxr.h"
namespace graphics::vulkan {
namespace {
struct MotionVectorData {
matrix4 reprojection;
float viewportRect[4];
float sceneNear;
float cockpitNear;
float farPlane;
float pad;
};
} // namespace
bool VulkanMotionVectors::wanted()
{
// The attachment needs blend state of its own, and VR isn't supported yet.
if (!getRendererInstance()->supportsIndependentBlend() || openxr_requested()) {
return false;
}
return Gr_fsr_mode != FsrMode::Off && gr_is_capable(gr_capability::CAPABILITY_FSR);
}
void VulkanMotionVectors::init(PostProcessContext& ctx)
{
m_ctx = &ctx;
}
bool VulkanMotionVectors::createResources()
{
if (m_framebuffer) {
return true;
}
if (!m_renderPass) {
std::array<vk::AttachmentDescription, 2> attachments;
std::array<vk::AttachmentReference, 2> colorRefs;
for (uint32_t i = 0; i < attachments.size(); ++i) {
attachments[i].format = i == 0 ? FORMAT : DEPTH_FORMAT;
attachments[i].loadOp = vk::AttachmentLoadOp::eDontCare;
attachments[i].storeOp = vk::AttachmentStoreOp::eStore;
attachments[i].stencilLoadOp = vk::AttachmentLoadOp::eDontCare;
attachments[i].stencilStoreOp = vk::AttachmentStoreOp::eDontCare;
attachments[i].initialLayout = vk::ImageLayout::eUndefined;
attachments[i].finalLayout = vk::ImageLayout::eShaderReadOnlyOptimal;
colorRefs[i].attachment = i;
colorRefs[i].layout = vk::ImageLayout::eColorAttachmentOptimal;
}
vk::SubpassDescription subpass;
subpass.pipelineBindPoint = vk::PipelineBindPoint::eGraphics;
subpass.colorAttachmentCount = static_cast<uint32_t>(colorRefs.size());
subpass.pColorAttachments = colorRefs.data();
vk::SubpassDependency dependency;
dependency.srcSubpass = VK_SUBPASS_EXTERNAL;
dependency.dstSubpass = 0;
dependency.srcStageMask = vk::PipelineStageFlagBits::eColorAttachmentOutput
| vk::PipelineStageFlagBits::eFragmentShader
| vk::PipelineStageFlagBits::eComputeShader;
dependency.dstStageMask = vk::PipelineStageFlagBits::eColorAttachmentOutput;
dependency.srcAccessMask = vk::AccessFlagBits::eColorAttachmentWrite | vk::AccessFlagBits::eShaderRead;
dependency.dstAccessMask = vk::AccessFlagBits::eColorAttachmentWrite;
vk::RenderPassCreateInfo rpInfo;
rpInfo.attachmentCount = static_cast<uint32_t>(attachments.size());
rpInfo.pAttachments = attachments.data();
rpInfo.subpassCount = 1;
rpInfo.pSubpasses = &subpass;
rpInfo.dependencyCount = 1;
rpInfo.pDependencies = &dependency;
try {
m_renderPass = m_ctx->device.createRenderPass(rpInfo);
} catch (const vk::SystemError& e) {
nprintf(("vulkan", "VulkanMotionVectors: Failed to create render pass: %s\n", e.what()));
return false;
}
}
const uint32_t w = m_ctx->sceneExtent.width;
const uint32_t h = m_ctx->sceneExtent.height;
const auto createTarget = [this, w, h](RenderTarget& target, vk::Format format, vk::ImageUsageFlags usage, vk::ImageAspectFlags aspect) {
target.format = format;
target.width = w;
target.height = h;
return m_ctx->createImage(w, h, format, usage, aspect, target.image, target.view, target.allocation);
};
const auto renderable = vk::ImageUsageFlagBits::eColorAttachment | vk::ImageUsageFlagBits::eSampled;
if (!createTarget(m_target, FORMAT, renderable, vk::ImageAspectFlagBits::eColor)
|| !createTarget(m_drawnMotion, DRAWN_FORMAT, renderable, vk::ImageAspectFlagBits::eColor)
|| !createTarget(m_depth, DEPTH_FORMAT, renderable, vk::ImageAspectFlagBits::eColor)) {
nprintf(("vulkan", "VulkanMotionVectors: Failed to create image!\n"));
destroyTargets();
return false;
}
const std::array<vk::ImageView, 2> views = {m_target.view, m_depth.view};
vk::FramebufferCreateInfo fbInfo;
fbInfo.renderPass = m_renderPass;
fbInfo.attachmentCount = static_cast<uint32_t>(views.size());
fbInfo.pAttachments = views.data();
fbInfo.width = w;
fbInfo.height = h;
fbInfo.layers = 1;
try {
m_framebuffer = m_ctx->device.createFramebuffer(fbInfo);
} catch (const vk::SystemError& e) {
nprintf(("vulkan", "VulkanMotionVectors: Failed to create framebuffer: %s\n", e.what()));
destroyTargets();
return false;
}
return true;
}
void VulkanMotionVectors::destroyTargets()
{
if (!m_ctx || !m_ctx->device) {
return;
}
if (m_framebuffer) {
m_ctx->device.destroyFramebuffer(m_framebuffer);
m_framebuffer = nullptr;
}
m_ctx->destroyTarget(m_target);
m_ctx->destroyTarget(m_drawnMotion);
m_ctx->destroyTarget(m_depth);
}
void VulkanMotionVectors::shutdown()
{
if (!m_ctx || !m_ctx->device) {
return;
}
destroyTargets();
if (m_renderPass) {
m_ctx->device.destroyRenderPass(m_renderPass);
m_renderPass = nullptr;
}
}
void VulkanMotionVectors::execute(vk::CommandBuffer cmd, vk::ImageView sceneDepthCopy)
{
GR_DEBUG_SCOPE("Motion vectors");
ImageBarrier2 barrier;
barrier.image = m_drawnMotion.image;
barrier.levelCount = 1;
barrier.layerCount = 1;
barrier.oldLayout = vk::ImageLayout::eColorAttachmentOptimal;
barrier.newLayout = vk::ImageLayout::eShaderReadOnlyOptimal;
barrier.srcStage = vk::PipelineStageFlagBits2::eColorAttachmentOutput;
barrier.srcAccess = vk::AccessFlagBits2::eColorAttachmentWrite;
barrier.dstStage = vk::PipelineStageFlagBits2::eFragmentShader;
barrier.dstAccess = vk::AccessFlagBits2::eShaderSampledRead;
cmdImageBarrier(cmd, barrier);
MotionVectorData data;
matrix4 viewProj, previousViewProj, inverseViewProj;
gr_get_velocity_camera(&viewProj, &previousViewProj, data.viewportRect);
// Not invertible until a scene has captured a camera
if (vm_inverse_matrix4(&inverseViewProj, &viewProj)) {
vm_matrix4_x_matrix4(&data.reprojection, &previousViewProj, &inverseViewProj);
} else {
vm_matrix4_set_identity(&data.reprojection);
}
vk::Sampler nearestSampler = getTextureManager()->getSampler(
vk::Filter::eNearest, vk::Filter::eNearest,
vk::SamplerAddressMode::eClampToEdge, false, 0.0f, false);
data.sceneNear = Min_draw_distance;
data.cockpitNear = Min_draw_distance_cockpit;
data.farPlane = Max_draw_distance;
data.pad = 0.0f;
const std::array<vk::ImageView, 2> inputs = {sceneDepthCopy, m_drawnMotion.view};
m_ctx->drawFullscreenTriangleMulti(cmd, m_renderPass, m_framebuffer, m_ctx->sceneExtent,
SDR_TYPE_MOTION_VECTORS, inputs.data(), static_cast<uint32_t>(inputs.size()), &data, sizeof(data), nearestSampler, 2);
}
} // namespace graphics::vulkan
@@ -64,15 +64,15 @@ bool VulkanSMAA::init(PostProcessContext& ctx, const VulkanLDR& ldr)
m_initialized = true;
nprintf(("vulkan", "VulkanSMAA: SMAA initialized (%ux%u)\n",
m_ctx->sceneExtent.width, m_ctx->sceneExtent.height));
m_ctx->outputExtent.width, m_ctx->outputExtent.height));
return true;
}
bool VulkanSMAA::createTargets()
{
const vk::Format ldrFormat = m_ctx->ldrFormat;
const uint32_t w = m_ctx->sceneExtent.width;
const uint32_t h = m_ctx->sceneExtent.height;
const uint32_t w = m_ctx->outputExtent.width;
const uint32_t h = m_ctx->outputExtent.height;
if (!m_ctx->createImage(w, h, ldrFormat,
vk::ImageUsageFlagBits::eColorAttachment | vk::ImageUsageFlagBits::eSampled,
@@ -194,8 +194,8 @@ void VulkanSMAA::execute(vk::CommandBuffer cmd)
GR_DEBUG_SCOPE("SMAA");
graphics::generic_data::smaa_data smaaData;
smaaData.smaa_rt_metrics.x = static_cast<float>(m_ctx->sceneExtent.width);
smaaData.smaa_rt_metrics.y = static_cast<float>(m_ctx->sceneExtent.height);
smaaData.smaa_rt_metrics.x = static_cast<float>(m_ctx->outputExtent.width);
smaaData.smaa_rt_metrics.y = static_cast<float>(m_ctx->outputExtent.height);
smaaData.pad[0] = 0.0f;
smaaData.pad[1] = 0.0f;
@@ -210,7 +210,7 @@ void VulkanSMAA::execute(vk::CommandBuffer cmd)
// the render pass's own loadOp=eDontCare, which leaves those pixels undefined.
static const std::array<float, 4> kClearColor = {0.0f, 0.0f, 0.0f, 1.0f};
m_ctx->drawFullscreenTriangle(cmd, m_ldr->renderPass(),
m_smaaEdgesFB, m_ctx->sceneExtent,
m_smaaEdgesFB, m_ctx->outputExtent,
SDR_TYPE_POST_PROCESS_SMAA_EDGE,
m_ldr->getAADetectionView(), m_ctx->linearSampler,
&smaaData, sizeof(smaaData),
@@ -224,7 +224,7 @@ void VulkanSMAA::execute(vk::CommandBuffer cmd)
GR_DEBUG_SCOPE("SMAA Blending Weights calculation");
std::array<vk::ImageView, 3> views = {m_smaaEdges.view, m_smaaAreaTexView, m_smaaSearchTexView};
m_ctx->drawFullscreenTriangleMulti(cmd, m_ldr->renderPass(), m_smaaBlendFB, m_ctx->sceneExtent,
m_ctx->drawFullscreenTriangleMulti(cmd, m_ldr->renderPass(), m_smaaBlendFB, m_ctx->outputExtent,
SDR_TYPE_POST_PROCESS_SMAA_BLENDING_WEIGHT, views.data(), static_cast<uint32_t>(views.size()),
&smaaData, sizeof(smaaData));
}
@@ -235,7 +235,7 @@ void VulkanSMAA::execute(vk::CommandBuffer cmd)
GR_DEBUG_SCOPE("SMAA Neighborhood Blending");
std::array<vk::ImageView, 2> views = {m_ldr->ldrView(), m_smaaBlend.view};
m_ctx->drawFullscreenTriangleMulti(cmd, m_ldr->renderPass(), m_ldr->luminanceFramebuffer(), m_ctx->sceneExtent,
m_ctx->drawFullscreenTriangleMulti(cmd, m_ldr->renderPass(), m_ldr->luminanceFramebuffer(), m_ctx->outputExtent,
SDR_TYPE_POST_PROCESS_SMAA_NEIGHBORHOOD_BLENDING, views.data(), static_cast<uint32_t>(views.size()),
&smaaData, sizeof(smaaData));
}
@@ -245,7 +245,7 @@ void VulkanSMAA::execute(vk::CommandBuffer cmd)
GR_DEBUG_SCOPE("SMAA Resolve");
m_ctx->drawFullscreenTriangle(cmd, m_ldr->renderPass(),
m_ldr->ldrFramebuffer(), m_ctx->sceneExtent,
m_ldr->ldrFramebuffer(), m_ctx->outputExtent,
SDR_TYPE_POST_PROCESS_SMAA_RESOLVE,
m_ldr->luminanceView(), m_ctx->linearSampler,
nullptr, 0,
@@ -34,14 +34,7 @@ bool VulkanShadowMap::init(PostProcessContext& ctx)
return false;
}
int size;
switch (Shadow_quality) {
case ShadowQuality::Low: size = 512; break;
case ShadowQuality::Medium: size = 1024; break;
case ShadowQuality::High: size = 2048; break;
case ShadowQuality::Ultra: size = 4096; break;
default: size = 512; break;
}
const int size = shadows_map_resolution();
const auto layers = static_cast<uint32_t>(Num_shadow_cascades + Num_cockpit_shadow_cascades);
nprintf(("vulkan", "VulkanPostProcessor: Creating %dx%d shadow map (%d cascades)\n", size, size, layers));
@@ -188,6 +181,32 @@ bool VulkanShadowMap::init(PostProcessContext& ctx)
}
}
// Raw (non-compare) sampler on the same depth view, for the PCSS blocker search --
// it needs actual stored depth values, not a compare result. GL_NEAREST-equivalent
// filtering: the blocker search wants the exact per-texel depth, not a hardware-
// filtered blend across the compare boundary (deliberately different from the compare
// sampler's linear filtering above). Vulkan samplers are independent of images, so this
// is always available -- no capability gate needed here (contrast the OpenGL backend,
// which needs GL 3.3 because compare mode there is texture-object state).
{
vk::SamplerCreateInfo samplerInfo;
samplerInfo.magFilter = vk::Filter::eNearest;
samplerInfo.minFilter = vk::Filter::eNearest;
samplerInfo.mipmapMode = vk::SamplerMipmapMode::eNearest;
samplerInfo.addressModeU = vk::SamplerAddressMode::eClampToEdge;
samplerInfo.addressModeV = vk::SamplerAddressMode::eClampToEdge;
samplerInfo.addressModeW = vk::SamplerAddressMode::eClampToEdge;
samplerInfo.compareEnable = VK_FALSE;
samplerInfo.borderColor = vk::BorderColor::eFloatOpaqueWhite;
try {
m_rawSampler = m_ctx->device.createSampler(samplerInfo);
} catch (const vk::SystemError& e) {
nprintf(("vulkan", "VulkanPostProcessor: Failed to create shadow raw sampler: %s\n", e.what()));
return false;
}
}
m_textureSize = size;
m_initialized = true;
nprintf(("vulkan", "VulkanPostProcessor: Shadow map initialized (%dx%d, %d cascades)\n", size, size, layers));
@@ -204,6 +223,10 @@ void VulkanShadowMap::shutdown()
m_ctx->device.destroySampler(m_compareSampler);
m_compareSampler = nullptr;
}
if (m_rawSampler) {
m_ctx->device.destroySampler(m_rawSampler);
m_rawSampler = nullptr;
}
if (m_framebuffer) {
m_ctx->device.destroyFramebuffer(m_framebuffer);
m_framebuffer = nullptr;
+13
View File
@@ -119,6 +119,15 @@ public:
*/
bool isEnabled() const { return m_enabled; }
/**
* @brief Whether buildTlas() already ran in the current frame. game_render_frame() can run
* more than once in a frame (for example, one time for each OpenXR eye), but one build in
* each frame is sufficient.
*/
bool isTlasBuiltThisFrame() const {
return m_bufferManager != nullptr && m_lastTlasBuildFrame == m_bufferManager->getCurrentFrameNumber();
}
/**
* @brief A permanent, 0-instance TLAS built once at init(). Used as a
* valid (non-null) descriptor value in place of getTlasForShaderBinding()
@@ -275,6 +284,10 @@ private:
// gated behind m_enabled/m_initialized so this only matters defensively.
uint32_t currentFrameIndex() const { return m_bufferManager != nullptr ? m_bufferManager->getCurrentFrame() : 0; }
// Monotonic frame number of the last buildTlas() that ran. buildTlas() uses it to build
// only one time in each frame. See isTlasBuiltThisFrame().
uint64_t m_lastTlasBuildFrame = UINT64_MAX;
vk::Device m_device;
vk::PhysicalDevice m_physicalDevice;
VulkanMemoryManager* m_memoryManager = nullptr;
@@ -425,6 +425,12 @@ void VulkanRaytracingManager::buildTlas()
return;
}
// Build only one time in each frame. See isTlasBuiltThisFrame().
if (isTlasBuiltThisFrame()) {
return;
}
m_lastTlasBuildFrame = m_bufferManager->getCurrentFrameNumber();
// Each frame-in-flight slot owns its own instance/TLAS/scratch buffers (see
// FrameTlasResources' declaration for why a single shared set would race
// across overlapping in-flight frames).
+8 -8
View File
@@ -50,7 +50,7 @@ void VulkanRenderer::createCompositionResources()
vk::ImageCreateInfo imageInfo;
imageInfo.imageType = vk::ImageType::e2D;
imageInfo.format = HDR_COLOR_FORMAT;
imageInfo.extent = vk::Extent3D(m_renderExtent.width, m_renderExtent.height, 1);
imageInfo.extent = vk::Extent3D(m_compositionExtent.width, m_compositionExtent.height, 1);
imageInfo.mipLevels = 1;
imageInfo.arrayLayers = 1;
imageInfo.samples = vk::SampleCountFlagBits::e1;
@@ -155,8 +155,8 @@ void VulkanRenderer::createFrameBuffers()
framebufferInfo.renderPass = m_renderPass.get();
framebufferInfo.attachmentCount = 2;
framebufferInfo.pAttachments = attachments;
framebufferInfo.width = m_renderExtent.width;
framebufferInfo.height = m_renderExtent.height;
framebufferInfo.width = m_compositionExtent.width;
framebufferInfo.height = m_compositionExtent.height;
framebufferInfo.layers = 1;
m_swapChainFramebuffers.push_back(m_device->createFramebufferUnique(framebufferInfo));
@@ -265,8 +265,8 @@ void VulkanRenderer::createDepthResources()
vk::ImageCreateInfo imageInfo;
imageInfo.imageType = vk::ImageType::e2D;
imageInfo.format = m_depthFormat;
imageInfo.extent.width = m_renderExtent.width;
imageInfo.extent.height = m_renderExtent.height;
imageInfo.extent.width = m_compositionExtent.width;
imageInfo.extent.height = m_compositionExtent.height;
imageInfo.extent.depth = 1;
imageInfo.mipLevels = 1;
imageInfo.arrayLayers = 1;
@@ -296,7 +296,7 @@ void VulkanRenderer::createDepthResources()
m_depthImageView = m_device->createImageViewUnique(viewInfo);
nprintf(("vulkan", "Vulkan: Created depth buffer (%dx%d, format %d)\n",
m_renderExtent.width, m_renderExtent.height, static_cast<int>(m_depthFormat)));
m_compositionExtent.width, m_compositionExtent.height, static_cast<int>(m_depthFormat)));
}
void VulkanRenderer::destroyDepthResources()
{
@@ -472,8 +472,8 @@ bool VulkanRenderer::readbackFramebuffer(ubyte** outPixels, uint32_t* outWidth,
const vk::PipelineStageFlags2 srcStageMask = vk::PipelineStageFlagBits2::eFragmentShader;
const vk::AccessFlags2 srcAccessMask = vk::AccessFlagBits2::eShaderSampledRead;
const uint32_t bytesPerSrcPixel = 8; // fp16 RGBA = 4 x 2 bytes
uint32_t w = m_renderExtent.width;
uint32_t h = m_renderExtent.height;
uint32_t w = m_compositionExtent.width;
uint32_t h = m_compositionExtent.height;
vk::DeviceSize bufferSize = static_cast<vk::DeviceSize>(w) * h * bytesPerSrcPixel;
// End the current render pass so we can record transfer commands
+10
View File
@@ -184,6 +184,7 @@ class VulkanRenderer {
vk::CommandBuffer getCurrentCommandBuffer() const { return m_currentCommandBuffer; }
vk::Instance getVkInstance() const { return m_vkInstance.get(); }
bool supportsIndependentBlend() const { return m_deviceFeatures.independentBlend == VK_TRUE; }
vk::PhysicalDevice getPhysicalDevice() const { return m_physicalDevice; }
vk::Device getDevice() const { return m_device.get(); }
vk::Queue getGraphicsQueue() const { return m_graphicsQueue; }
@@ -206,6 +207,9 @@ class VulkanRenderer {
*/
vk::Extent2D getRenderExtent() const { return m_renderExtent; }
// The render extent times FSR's upscale factor. Everything after the scene draws at this size.
vk::Extent2D getCompositionExtent() const { return m_compositionExtent; }
/**
* @brief Get the composition image the frame is currently being rendered into
*
@@ -464,6 +468,11 @@ class VulkanRenderer {
bool recreateSwapChain();
bool shouldRecreateSwapChain(SwapChainStatus status) const;
vk::Extent2D wantedCompositionExtent() const;
void setCompositionExtent(vk::Extent2D extent);
// Recreates the composition and output-sized targets when the FSR upscale factor changed. Outside a frame.
void updateCompositionExtent();
void createImGuiDescriptorPool();
void initImGui();
void shutdownImGui();
@@ -496,6 +505,7 @@ class VulkanRenderer {
// option), so treating the two as one is what put whole frames in the
// upper-left corner of the screen.
vk::Extent2D m_renderExtent;
vk::Extent2D m_compositionExtent;
SCP_vector<vk::Image> m_swapChainImages;
SCP_vector<vk::UniqueImageView> m_swapChainImageViews;
SCP_vector<vk::UniqueFramebuffer> m_swapChainFramebuffers;
+94 -27
View File
@@ -3,10 +3,12 @@
#include "VulkanBarrier.h"
#include "VulkanBuffer.h"
#include "VulkanTexture.h"
#include "VulkanUpscaler.h"
#include "backends/imgui_impl_vulkan.h"
#include "graphics/2d.h"
#include "graphics/matrix.h"
#include "lighting/lighting.h"
extern float flFrametime;
@@ -26,6 +28,11 @@ void VulkanRenderer::beginTrackedRenderPass(const PassBeginDesc& desc)
m_currentCommandBuffer.beginRenderPass(rpBegin, vk::SubpassContents::eInline);
m_stateTracker->setRenderPass(desc.renderPass, 0);
if (desc.renderPass == m_renderPass.get() || desc.renderPass == m_renderPassLoad.get()) {
// Everything the engine draws onto the composition image is in gr_screen units
m_stateTracker->setScreenScale(static_cast<float>(m_compositionExtent.width) / static_cast<float>(m_renderExtent.width),
static_cast<float>(m_compositionExtent.height) / static_cast<float>(m_renderExtent.height));
}
m_stateTracker->setColorAttachmentCount(desc.colorAttachmentCount);
m_stateTracker->setCurrentSampleCount(desc.sampleCount);
m_stateTracker->setRenderArea(desc.extent);
@@ -94,6 +101,48 @@ void VulkanRenderer::acquireNextSwapChainImage()
// Reserve the image as in use
m_swapChainImageRenderImage[m_currentSwapChainImage] = m_frames[m_currentFrame].get();
}
vk::Extent2D VulkanRenderer::wantedCompositionExtent() const
{
const float factor = vulkan_upscaler_usable() ? gr_fsr_upscale_factor(Gr_fsr_mode) : 1.0f;
return {static_cast<uint32_t>(std::lround(m_renderExtent.width * factor)),
static_cast<uint32_t>(std::lround(m_renderExtent.height * factor))};
}
void VulkanRenderer::setCompositionExtent(vk::Extent2D extent)
{
m_compositionExtent = extent;
Gr_composition_scale = static_cast<float>(m_compositionExtent.width) / static_cast<float>(m_renderExtent.width);
}
void VulkanRenderer::updateCompositionExtent()
{
const auto wanted = wantedCompositionExtent();
if (wanted == m_compositionExtent) {
return;
}
m_device->waitIdle();
setCompositionExtent(wanted);
mprintf(("Vulkan: Composition resolution %ux%u for render resolution %ux%u\n",
m_compositionExtent.width, m_compositionExtent.height, m_renderExtent.width, m_renderExtent.height));
destroyDepthResources();
createDepthResources();
createCompositionResources();
createFrameBuffers();
if (m_postProcessor && !m_postProcessor->resize(m_renderExtent, m_compositionExtent)) {
mprintf(("Vulkan: post-processor resize failed, disabling post-processing!\n"));
setPostProcessor(nullptr);
m_postProcessor->shutdown();
m_postProcessor.reset();
}
if (m_drawManager) {
m_drawManager->onResize();
}
m_previousSwapChainImage = UINT32_MAX;
}
void VulkanRenderer::setupFrame()
{
if (m_frameInProgress) {
@@ -109,6 +158,8 @@ void VulkanRenderer::setupFrame()
m_pipelinesStale = false;
}
updateCompositionExtent();
// Free completed texture upload command buffers
Assertion(m_textureManager, "Vulkan TextureManager not initialized in setupFrame!");
m_textureManager->frameStart();
@@ -161,7 +212,7 @@ void VulkanRenderer::setupFrame()
PassBeginDesc pass;
pass.renderPass = m_renderPass.get();
pass.framebuffer = m_swapChainFramebuffers[m_currentSwapChainImage].get();
pass.extent = m_renderExtent;
pass.extent = m_compositionExtent;
pass.clearValues = clearValues;
beginTrackedRenderPass(pass);
@@ -273,8 +324,12 @@ void VulkanRenderer::beginSceneRendering()
return;
}
// End the current swap chain render pass
m_currentCommandBuffer.endRenderPass();
// End the current swap chain render pass. vulkan_scene_texture_begin() can end it before
// this call, for the TLAS build, so end it only when it is still open.
endCurrentRenderPass();
m_postProcessor->prepareMotionVectors();
m_postProcessor->prepareUpscaler();
// Use G-buffer render pass when deferred lighting is enabled and G-buffer is ready
m_useGbufRenderPass = m_postProcessor->deferred().isInitialized() && light_deferred_enabled();
@@ -284,29 +339,33 @@ void VulkanRenderer::beginSceneRendering()
pass.extent = m_postProcessor->getSceneExtent();
if (m_useGbufRenderPass) {
// Clear values: 6 color + depth
std::array<vk::ClearValue, VulkanDeferredGBuffer::GBUF_COLOR_ATTACHMENT_COUNT + 1> clearValues{};
const auto& gbuffer = m_postProcessor->deferred();
// Clear values: 6 color, optionally motion vectors, then depth. Extras are ignored by the plain pass.
std::array<vk::ClearValue, VulkanDeferredGBuffer::GBUF_COLOR_ATTACHMENT_COUNT + 2> clearValues{};
clearValues[VulkanDeferredGBuffer::GBUF_ATT_COLOR].color.setFloat32({0.0f, 0.0f, 0.0f, 1.0f});
clearValues[VulkanDeferredGBuffer::GBUF_ATT_POSITION].color.setFloat32({0.0f, 0.0f, 0.0f, 0.0f});
clearValues[VulkanDeferredGBuffer::GBUF_ATT_NORMAL].color.setFloat32({0.0f, 0.0f, 0.0f, 0.0f});
clearValues[VulkanDeferredGBuffer::GBUF_ATT_SPECULAR].color.setFloat32({0.0f, 0.0f, 0.0f, 0.0f});
clearValues[VulkanDeferredGBuffer::GBUF_ATT_EMISSIVE].color.setFloat32({0.0f, 0.0f, 0.0f, 0.0f});
clearValues[VulkanDeferredGBuffer::GBUF_ATT_COMPOSITE].color.setFloat32({0.0f, 0.0f, 0.0f, 0.0f});
clearValues[VulkanDeferredGBuffer::GBUF_COLOR_ATTACHMENT_COUNT].depthStencil = vk::ClearDepthStencilValue(1.0f, 0);
if (gbuffer.velocityActive()) {
clearValues[VulkanDeferredGBuffer::velocityAttachmentIndex()].color.setFloat32({VulkanMotionVectors::NO_MOTION_WRITTEN, 0.0f, 0.0f, 0.0f});
}
clearValues[gbuffer.depthAttachmentIndex()].depthStencil = vk::ClearDepthStencilValue(1.0f, 0);
pass.renderPass = m_postProcessor->deferred().renderPass();
pass.framebuffer = m_postProcessor->deferred().framebuffer();
pass.renderPass = gbuffer.renderPass();
pass.framebuffer = gbuffer.framebuffer();
pass.clearValues = clearValues;
pass.colorAttachmentCount = VulkanDeferredGBuffer::GBUF_COLOR_ATTACHMENT_COUNT;
pass.colorAttachmentCount = gbuffer.colorAttachmentCount();
beginTrackedRenderPass(pass);
} else {
std::array<vk::ClearValue, 2> clearValues;
clearValues[0].color.setFloat32({0.0f, 0.0f, 0.0f, 1.0f});
clearValues[1].depthStencil = vk::ClearDepthStencilValue(1.0f, 0);
auto clearValues = m_postProcessor->sceneClearValues();
pass.renderPass = m_postProcessor->getSceneRenderPass();
pass.framebuffer = m_postProcessor->getSceneFramebuffer();
pass.clearValues = clearValues;
pass.colorAttachmentCount = m_postProcessor->sceneColorAttachmentCount();
beginTrackedRenderPass(pass);
}
@@ -319,6 +378,7 @@ void VulkanRenderer::resumeSceneRendering()
pass.renderPass = m_postProcessor->getSceneRenderPassLoad();
pass.framebuffer = m_postProcessor->getSceneFramebuffer();
pass.extent = m_postProcessor->getSceneExtent();
pass.colorAttachmentCount = m_postProcessor->sceneColorAttachmentCount();
beginTrackedRenderPass(pass);
}
@@ -334,6 +394,12 @@ void VulkanRenderer::endSceneRendering()
// End HDR scene render pass (transitions scene color to eShaderReadOnlyOptimal)
m_currentCommandBuffer.endRenderPass();
// Nothing drawn after the scene, like the HUD, has motion vectors
Gr_motion_vectors_active = false;
m_postProcessor->executeMotionVectors(m_currentCommandBuffer);
m_postProcessor->executeUpscaler(m_currentCommandBuffer);
// Update distortion ping-pong textures (every ~30ms, matching OpenGL)
if (Gr_framebuffer_effects.any_set()) {
m_postProcessor->updateDistortion(m_currentCommandBuffer, flFrametime);
@@ -357,7 +423,7 @@ void VulkanRenderer::endSceneRendering()
PassBeginDesc pass;
pass.renderPass = m_renderPassLoad.get();
pass.framebuffer = m_swapChainFramebuffers[m_currentSwapChainImage].get();
pass.extent = m_renderExtent;
pass.extent = m_compositionExtent;
pass.clearValues = clearValues;
pass.viewport = PassViewport::NoFlip;
beginTrackedRenderPass(pass);
@@ -367,9 +433,9 @@ void VulkanRenderer::endSceneRendering()
// Restore Y-flipped viewport for HUD rendering
m_stateTracker->setViewport(0.0f,
static_cast<float>(m_renderExtent.height),
static_cast<float>(m_renderExtent.width),
-static_cast<float>(m_renderExtent.height));
static_cast<float>(m_compositionExtent.height),
static_cast<float>(m_compositionExtent.width),
-static_cast<float>(m_compositionExtent.height));
m_sceneRendering = false;
m_useGbufRenderPass = false;
@@ -408,23 +474,24 @@ void VulkanRenderer::resumeScenePassAfterCopy()
pass.extent = m_postProcessor->getSceneExtent();
if (m_useGbufRenderPass) {
// Clear values ignored for eLoad but array must cover all attachments
std::array<vk::ClearValue, VulkanDeferredGBuffer::GBUF_COLOR_ATTACHMENT_COUNT + 1> clearValues{};
clearValues[VulkanDeferredGBuffer::GBUF_COLOR_ATTACHMENT_COUNT].depthStencil = vk::ClearDepthStencilValue(1.0f, 0);
const auto& gbuffer = m_postProcessor->deferred();
pass.renderPass = m_postProcessor->deferred().renderPassLoad();
pass.framebuffer = m_postProcessor->deferred().framebuffer();
// Clear values ignored for eLoad but array must cover all attachments
std::array<vk::ClearValue, VulkanDeferredGBuffer::GBUF_COLOR_ATTACHMENT_COUNT + 2> clearValues{};
clearValues[gbuffer.depthAttachmentIndex()].depthStencil = vk::ClearDepthStencilValue(1.0f, 0);
pass.renderPass = gbuffer.renderPassLoad();
pass.framebuffer = gbuffer.framebuffer();
pass.clearValues = clearValues;
pass.colorAttachmentCount = VulkanDeferredGBuffer::GBUF_COLOR_ATTACHMENT_COUNT;
pass.colorAttachmentCount = gbuffer.colorAttachmentCount();
beginTrackedRenderPass(pass);
} else {
std::array<vk::ClearValue, 2> clearValues;
clearValues[0].color.setFloat32({0.0f, 0.0f, 0.0f, 1.0f});
clearValues[1].depthStencil = vk::ClearDepthStencilValue(1.0f, 0);
auto clearValues = m_postProcessor->sceneClearValues();
pass.renderPass = m_postProcessor->getSceneRenderPassLoad();
pass.framebuffer = m_postProcessor->getSceneFramebuffer();
pass.clearValues = clearValues;
pass.colorAttachmentCount = m_postProcessor->sceneColorAttachmentCount();
beginTrackedRenderPass(pass);
}
}
@@ -611,7 +678,7 @@ void VulkanRenderer::resumeSwapChainPass()
PassBeginDesc pass;
pass.renderPass = m_renderPassLoad.get();
pass.framebuffer = m_swapChainFramebuffers[m_currentSwapChainImage].get();
pass.extent = m_renderExtent;
pass.extent = m_compositionExtent;
pass.clearValues = clearValues;
beginTrackedRenderPass(pass);
}
@@ -625,7 +692,7 @@ void VulkanRenderer::restartCompositionPass()
PassBeginDesc pass;
pass.renderPass = m_renderPass.get();
pass.framebuffer = m_swapChainFramebuffers[m_currentSwapChainImage].get();
pass.extent = m_renderExtent;
pass.extent = m_compositionExtent;
pass.clearValues = clearValues;
beginTrackedRenderPass(pass);
}
+17 -1
View File
@@ -4,6 +4,7 @@
#include "VulkanBuffer.h"
#include "VulkanOpenXR.h"
#include "VulkanTexture.h"
#include "VulkanUpscaler.h"
#include "cmdline/cmdline.h"
#include "globalincs/version.h"
@@ -535,6 +536,10 @@ bool VulkanRenderer::initialize()
m_renderExtent, m_depthFormat, m_hdrActive)) {
mprintf(("Warning: Failed to initialize Vulkan post-processor, post-processing will be disabled\n"));
m_postProcessor.reset();
} else if (!m_postProcessor->resize(m_renderExtent, m_compositionExtent)) {
mprintf(("Warning: Failed to size the Vulkan post-processor, post-processing will be disabled\n"));
m_postProcessor->shutdown();
m_postProcessor.reset();
} else {
setPostProcessor(m_postProcessor.get());
}
@@ -1057,6 +1062,15 @@ bool VulkanRenderer::createLogicalDevice(const PhysicalDeviceValues& deviceValue
deviceCreateChain.unlink<vk::PhysicalDeviceBufferDeviceAddressFeatures>();
}
// FSR adds to the feature set audited above, for AMD's shaders and host code. It's disabled in VR for now.
if (!openxr_requested()) {
vulkan_upscaler_prepare_device(m_vkInstance.get(),
deviceValues.device,
deviceValues.graphicsQueueIndex.index,
supportedFeatures,
deviceCreateChain.get<vk::DeviceCreateInfo>());
}
if (openxr_requested())
m_device = vulkan_openxr_create_device(deviceValues.device, deviceCreateChain.get<vk::DeviceCreateInfo>());
else
@@ -1064,6 +1078,7 @@ bool VulkanRenderer::createLogicalDevice(const PhysicalDeviceValues& deviceValue
// Load device-level function pointers for the dynamic dispatcher
VULKAN_HPP_DEFAULT_DISPATCHER.init(m_device.get());
vulkan_upscaler_device_created(m_device.get());
// Create queues
m_graphicsQueue = m_device->getQueue(deviceValues.graphicsQueueIndex.index, 0);
@@ -1181,6 +1196,7 @@ bool VulkanRenderer::createSwapChain(const PhysicalDeviceValues& deviceValues, v
if (!Cmdline_window_res) {
m_renderExtent = vk::Extent2D(static_cast<uint32_t>(gr_screen.max_w), static_cast<uint32_t>(gr_screen.max_h));
}
setCompositionExtent(wantedCompositionExtent());
mprintf(("Vulkan: Swap chain output mode: %s\n", m_hdrActive ? "HDR10 (PQ/BT.2020)" : "SDR (sRGB)"));
mprintf(("Vulkan: Render resolution %ux%u, window/swap chain %ux%u\n",
@@ -1325,7 +1341,7 @@ bool VulkanRenderer::recreateSwapChain()
// Recreate the post-processor's extent-sized targets (scene color/depth,
// G-buffer, bloom chains, LDR/SMAA targets, ...). Its render passes and
// samplers are extent-independent and stay alive, keeping pipelines valid.
if (m_postProcessor && !m_postProcessor->resize(m_renderExtent)) {
if (m_postProcessor && !m_postProcessor->resize(m_renderExtent, m_compositionExtent)) {
mprintf(("Vulkan: post-processor resize failed, disabling post-processing!\n"));
setPostProcessor(nullptr);
m_postProcessor->shutdown();
+12 -2
View File
@@ -8,6 +8,16 @@ namespace graphics::vulkan {
// Global shader manager pointer
static VulkanShaderManager* g_shaderManager = nullptr;
static uint32_t vertex_input_mask(const ShaderTypeInfo* typeInfo)
{
uint32_t mask = shader_vertex_input_mask(*typeInfo);
// Batched effects also read their motion, which the attribute lists shared with OpenGL can't hold
if (typeInfo->type_id == SDR_TYPE_BATCHED_BITMAP || typeInfo->type_id == SDR_TYPE_EFFECT_PARTICLE) {
mask |= (1u << VATTRIB_EFFECT_MOTION);
}
return mask;
}
VulkanShaderManager* getShaderManager()
{
Assertion(g_shaderManager != nullptr, "Vulkan ShaderManager not initialized!");
@@ -130,7 +140,7 @@ void VulkanShaderManager::recompileAllShaders(const std::function<void(size_t, s
nprintf(("vulkan", "VulkanShaderManager: Failed to create vertex module: %s\n", e.what()));
}
}
shader.vertexInputMask = shader_vertex_input_mask(*typeInfo);
shader.vertexInputMask = vertex_input_mask(typeInfo);
SCP_string fragFile = typeInfo->frag;
auto fragSpirv = m_compiler->compile(fragFile, vk::ShaderStageFlagBits::eFragment, type, flags, requiresRaytracing);
@@ -222,7 +232,7 @@ int VulkanShaderManager::loadShader(shader_type type, unsigned int flags)
vertFile.c_str(), e.what()));
}
}
shader.vertexInputMask = shader_vertex_input_mask(*typeInfo);
shader.vertexInputMask = vertex_input_mask(typeInfo);
// Compile fragment shader
SCP_string fragFile = typeInfo->frag;
+69 -7
View File
@@ -127,6 +127,30 @@ void VulkanStateTracker::setRenderPass(vk::RenderPass renderPass, uint32_t subpa
// recorders are ever removed.
m_viewportDirty = true;
m_scissorDirty = true;
m_screenScaleX = 1.0f;
m_screenScaleY = 1.0f;
}
void VulkanStateTracker::setScreenScale(float x, float y)
{
m_screenScaleX = x;
m_screenScaleY = y;
m_viewportDirty = true;
m_scissorDirty = true;
}
vk::Rect2D VulkanStateTracker::screenToFramebuffer(const vk::Rect2D& rect) const
{
if (m_screenScaleX == 1.0f && m_screenScaleY == 1.0f) {
return rect;
}
// Scale the edges, so that adjacent rects stay adjacent
const auto x0 = static_cast<int32_t>(std::lround(rect.offset.x * m_screenScaleX));
const auto y0 = static_cast<int32_t>(std::lround(rect.offset.y * m_screenScaleY));
const auto x1 = static_cast<int32_t>(std::lround((rect.offset.x + static_cast<float>(rect.extent.width)) * m_screenScaleX));
const auto y1 = static_cast<int32_t>(std::lround((rect.offset.y + static_cast<float>(rect.extent.height)) * m_screenScaleY));
return {{x0, y0}, {static_cast<uint32_t>(std::max(x1 - x0, 0)), static_cast<uint32_t>(std::max(y1 - y0, 0))}};
}
void VulkanStateTracker::invalidateExternalBindings()
@@ -165,8 +189,24 @@ vk::Rect2D VulkanStateTracker::clampToRenderArea(const vk::Rect2D& rect) const
return result;
}
void VulkanStateTracker::setScreenViewport(float x, float y, float width, float height)
{
setViewport(x, y, width, height);
m_viewportInScreenUnits = true;
m_viewportDirty = true;
if (!m_scissorEnabled) {
m_scissorDirty = true;
}
}
void VulkanStateTracker::setViewport(float x, float y, float width, float height, float minDepth, float maxDepth)
{
if (m_viewportInScreenUnits) {
m_viewportInScreenUnits = false;
m_viewportDirty = true;
m_scissorDirty = true;
}
if (m_viewport.x != x || m_viewport.y != y ||
m_viewport.width != width || m_viewport.height != height ||
m_viewport.minDepth != minDepth || m_viewport.maxDepth != maxDepth) {
@@ -187,6 +227,14 @@ void VulkanStateTracker::setViewport(float x, float y, float width, float height
}
}
void VulkanStateTracker::setUpscalerSceneState(float jitterX, float jitterY, float lodBias)
{
m_upscalerJitterX = jitterX;
m_upscalerJitterY = jitterY;
m_upscalerLodBias = lodBias;
m_viewportDirty = true;
}
void VulkanStateTracker::setScissor(int32_t x, int32_t y, uint32_t width, uint32_t height)
{
if (m_scissor.offset.x != x || m_scissor.offset.y != y ||
@@ -313,28 +361,42 @@ void VulkanStateTracker::applyDynamicState()
{
Assertion(m_cmdBuffer, "applyDynamicState called without active command buffer!");
vk::Viewport viewport = m_viewport;
if (m_viewportInScreenUnits) {
viewport.x *= m_screenScaleX;
viewport.y *= m_screenScaleY;
viewport.width *= m_screenScaleX;
viewport.height *= m_screenScaleY;
}
if (m_viewportDirty) {
m_cmdBuffer.setViewport(0, 1, &m_viewport);
vk::Viewport jittered = viewport;
if (isUpscaledScenePass()) {
jittered.x += m_upscalerJitterX;
jittered.y += m_upscalerJitterY;
}
m_cmdBuffer.setViewport(0, 1, &jittered);
m_viewportDirty = false;
}
if (m_scissorDirty) {
if (m_scissorEnabled) {
m_cmdBuffer.setScissor(0, 1, &m_scissor);
const vk::Rect2D scissor = screenToFramebuffer(m_scissor);
m_cmdBuffer.setScissor(0, 1, &scissor);
} else {
// Set scissor to full viewport when disabled.
// Handle negative viewport height (VK_KHR_maintenance1 Y-flip):
// when height < 0, the viewport covers [y+height, y] in framebuffer Y.
vk::Rect2D fullScissor;
float vy = m_viewport.y;
float vh = m_viewport.height;
float vy = viewport.y;
float vh = viewport.height;
if (vh < 0.0f) {
vy = vy + vh;
vh = -vh;
}
fullScissor.offset.x = static_cast<int32_t>(m_viewport.x);
fullScissor.offset.x = static_cast<int32_t>(viewport.x);
fullScissor.offset.y = static_cast<int32_t>(vy);
fullScissor.extent.width = static_cast<uint32_t>(m_viewport.width);
fullScissor.extent.width = static_cast<uint32_t>(viewport.width);
fullScissor.extent.height = static_cast<uint32_t>(vh);
m_cmdBuffer.setScissor(0, 1, &fullScissor);
}
@@ -399,7 +461,7 @@ void vulkan_set_viewport(int x, int y, int width, int height)
if (gr_screen.rendering_to_texture == -1) {
// Screen rendering: use negative viewport height for OpenGL-compatible Y-up NDC
// (VK_KHR_maintenance1, core since Vulkan 1.1)
stateTracker->setViewport(
stateTracker->setScreenViewport(
static_cast<float>(x),
static_cast<float>(gr_screen.max_h - y),
static_cast<float>(width),
+24
View File
@@ -69,6 +69,19 @@ class VulkanStateTracker {
*/
void setViewport(float x, float y, float width, float height, float minDepth = 0.0f, float maxDepth = 1.0f);
// In gr_screen units, scaled by the screen scale
void setScreenViewport(float x, float y, float width, float height);
// Framebuffer pixels per gr_screen unit for engine-issued viewports, scissors and clears. Above 1 only in the
// composition pass while FSR upscales; every render pass begin resets it to 1.
void setScreenScale(float x, float y);
vk::Rect2D screenToFramebuffer(const vk::Rect2D& rect) const;
// FSR's subpixel jitter and texture LOD bias, applied only in the scene's own passes. Jittering the viewport rather
// than the projection also moves CPU-projected backgrounds, and leaves the matrices motion vectors and the HUD use.
void setUpscalerSceneState(float jitterX, float jitterY, float lodBias);
float getMaterialLodBias() const { return isUpscaledScenePass() ? m_upscalerLodBias : 0.0f; }
/**
* @brief Set scissor rectangle (dynamic state)
*/
@@ -323,6 +336,17 @@ class VulkanStateTracker {
static_cast<size_t>(DescriptorSetIndex::Count)>
m_boundDynamicOffsets{};
// Only the scene's passes have the motion vector location the upscaler requires.
bool isUpscaledScenePass() const { return m_colorAttachmentCount > PipelineConfig::VELOCITY_ATTACHMENT; }
float m_screenScaleX = 1.0f;
float m_screenScaleY = 1.0f;
bool m_viewportInScreenUnits = false;
float m_upscalerJitterX = 0.0f;
float m_upscalerJitterY = 0.0f;
float m_upscalerLodBias = 0.0f;
// Dynamic state
vk::Viewport m_viewport;
vk::Rect2D m_scissor;
+9 -2
View File
@@ -195,6 +195,7 @@ bool VulkanTextureManager::init(vk::Device device, vk::PhysicalDevice physicalDe
auto properties = physicalDevice.getProperties();
m_maxTextureSize = properties.limits.maxImageDimension2D;
m_maxAnisotropy = properties.limits.maxSamplerAnisotropy;
m_maxLodBias = properties.limits.maxSamplerLodBias;
nprintf(("vulkan", "Vulkan Texture Manager initialized\n"));
nprintf(("vulkan", " Max texture size: %u\n", m_maxTextureSize));
@@ -2160,8 +2161,13 @@ void VulkanTextureManager::decodeReadbackBuffer(const void* mapped, vk::Format f
vk::Sampler VulkanTextureManager::getSampler(vk::Filter magFilter, vk::Filter minFilter,
vk::SamplerAddressMode addressMode,
bool enableAnisotropy, float maxAnisotropy,
bool enableMipmaps)
bool enableMipmaps, float negativeLodBias)
{
// The bias is quantized to the eighths the key can hold, so that the key always matches the sampler it caches
const int maxLodBiasEighths = std::min(static_cast<int>(m_maxLodBias * 8.0f), 63);
const int lodBiasEighths = std::clamp(static_cast<int>(std::lround(negativeLodBias * -8.0f)), 0, maxLodBiasEighths);
const float lodBias = static_cast<float>(lodBiasEighths) / -8.0f;
// Create a key from sampler state
uint64_t key = 0;
key |= static_cast<uint64_t>(magFilter) << 0;
@@ -2169,6 +2175,7 @@ vk::Sampler VulkanTextureManager::getSampler(vk::Filter magFilter, vk::Filter mi
key |= static_cast<uint64_t>(addressMode) << 8;
key |= static_cast<uint64_t>(enableAnisotropy) << 16;
key |= static_cast<uint64_t>(enableMipmaps) << 17;
key |= static_cast<uint64_t>(lodBiasEighths & 0x3F) << 18; // 6 bits, in eighths
key |= static_cast<uint64_t>(maxAnisotropy * 10) << 24;
auto it = m_samplerCache.find(key);
@@ -2190,7 +2197,7 @@ vk::Sampler VulkanTextureManager::getSampler(vk::Filter magFilter, vk::Filter mi
samplerInfo.compareEnable = false;
samplerInfo.compareOp = vk::CompareOp::eAlways;
samplerInfo.mipmapMode = enableMipmaps ? vk::SamplerMipmapMode::eLinear : vk::SamplerMipmapMode::eNearest;
samplerInfo.mipLodBias = 0.0f;
samplerInfo.mipLodBias = lodBias;
samplerInfo.minLod = 0.0f;
samplerInfo.maxLod = enableMipmaps ? VK_LOD_CLAMP_NONE : 0.0f;
+2 -1
View File
@@ -165,7 +165,7 @@ public:
vk::Sampler getSampler(vk::Filter magFilter, vk::Filter minFilter,
vk::SamplerAddressMode addressMode,
bool enableAnisotropy, float maxAnisotropy,
bool enableMipmaps);
bool enableMipmaps, float negativeLodBias = 0.0f);
/**
* @brief Get a ready-to-use DescriptorImageInfo for the 2D fallback texture
@@ -420,6 +420,7 @@ private:
// Device limits
uint32_t m_maxTextureSize = 4096;
float m_maxAnisotropy = 1.0f;
float m_maxLodBias = 0.0f;
// Current render target state
int m_currentRenderTarget = -1;
+443
View File
@@ -0,0 +1,443 @@
#include "graphics/vulkan/VulkanUpscaler.h"
#include "globalincs/pstypes.h"
#include "graphics/2d.h"
#include "graphics/matrix.h"
#include "graphics/vulkan/VulkanBarrier.h"
#include "graphics/vulkan/VulkanMemory.h"
#include "graphics/vulkan/VulkanPostProcessing.h"
#include "render/3d.h"
#ifdef WITH_FFX
#include <ffx_portable.h>
#endif
#include <optional>
extern float flRealframetime;
namespace graphics::vulkan {
#ifdef WITH_FFX
namespace {
// vkCreateDevice reads through pointers into this, so it has to outlive the caller's scope.
ffxPortableVkRequirements Device_requirements;
bool Upscaler_available = false;
vk::PhysicalDevice Upscaler_gpu;
// The mode and version FSR failed with. Choosing anything else gives it another try.
struct failed_setting {
FsrMode mode;
int majorVersion;
};
std::optional<failed_setting> Failed_setting;
bool failed_for_current_setting()
{
if (Failed_setting && (Failed_setting->mode != Gr_fsr_mode || Failed_setting->majorVersion != Gr_fsr_major_version)) {
Failed_setting.reset();
}
return Failed_setting.has_value();
}
void mark_failed()
{
Failed_setting = failed_setting{Gr_fsr_mode, Gr_fsr_major_version};
}
struct fsr_version {
uint64_t id = 0;
SCP_string name;
};
// AMD lists the versions this device can run, the one it selects by default first.
SCP_vector<fsr_version> query_fsr_versions(vk::Device device)
{
VkDeviceContext context{static_cast<VkDevice>(device), static_cast<VkPhysicalDevice>(Upscaler_gpu),
VULKAN_HPP_DEFAULT_DISPATCHER.vkGetDeviceProcAddr};
uint64_t count = 0;
ffxQueryDescGetVersions query{};
query.header.type = FFX_API_QUERY_DESC_TYPE_GET_VERSIONS;
query.createDescType = FFX_API_CREATE_CONTEXT_DESC_TYPE_UPSCALE;
query.device = &context;
query.outputCount = &count;
if (ffxQuery(nullptr, &query.header) != FFX_API_RETURN_OK || count == 0) {
return {};
}
const auto n = static_cast<size_t>(count);
SCP_vector<uint64_t> ids(n);
SCP_vector<const char*> names(n);
query.versionIds = ids.data();
query.versionNames = names.data();
if (ffxQuery(nullptr, &query.header) != FFX_API_RETURN_OK) {
return {};
}
SCP_vector<fsr_version> versions;
for (size_t i = 0; i < n; ++i) {
versions.push_back({ids[i], names[i] ? names[i] : ""});
}
return versions;
}
int major_version(const SCP_string& name)
{
return std::atoi(name.c_str());
}
bool is_offered(int major)
{
return major == 4 || major == 3;
}
// The version the user chose
const fsr_version* find_chosen_version(const SCP_vector<fsr_version>& versions)
{
for (const auto& version : versions) {
if (major_version(version.name) == Gr_fsr_major_version) {
return &version;
}
}
mprintf(("Vulkan: FSR %d isn't available, leaving the version to AMD's selection\n", Gr_fsr_major_version));
return nullptr;
}
void log_ffx_message(uint32_t type, const wchar_t* message)
{
mprintf(("FSR %s: %ls\n", type == FFX_API_MESSAGE_TYPE_ERROR ? "error" : "warning", message));
}
FfxApiResource ffx_image(const RenderTarget& target, FfxApiResourceState state, uint32_t usage = FFX_API_RESOURCE_USAGE_READ_ONLY)
{
FfxApiResourceDescription description{};
description.type = FFX_API_RESOURCE_TYPE_TEXTURE2D;
description.format = ffxApiGetSurfaceFormatVK(static_cast<VkFormat>(target.format));
description.width = target.width;
description.height = target.height;
description.depth = 1;
description.mipCount = 1;
description.usage = usage;
return ffxApiGetResourceVK(static_cast<VkImage>(target.image), description, state);
}
} // namespace
void vulkan_upscaler_prepare_device(vk::Instance instance,
vk::PhysicalDevice gpu,
uint32_t graphicsQueueFamily,
const vk::PhysicalDeviceFeatures& supportedFeatures,
vk::DeviceCreateInfo& createInfo)
{
Upscaler_available = false;
if (!ffxPortableCanLoad()) {
mprintf(("Vulkan: FSR unavailable: %s\n", ffxPortableLoadError()));
return;
}
const auto queueFamilies = gpu.getQueueFamilyProperties();
if (graphicsQueueFamily >= queueFamilies.size() ||
!(queueFamilies[graphicsQueueFamily].queueFlags & vk::QueueFlagBits::eCompute)) {
mprintf(("Vulkan: FSR unavailable: the graphics queue has no compute support\n"));
return;
}
if (!supportedFeatures.independentBlend) {
mprintf(("Vulkan: FSR unavailable: independentBlend is not supported\n"));
return;
}
ffxPortableVkGetDeviceRequirements(static_cast<VkInstance>(instance),
static_cast<VkPhysicalDevice>(gpu),
VULKAN_HPP_DEFAULT_DISPATCHER.vkGetInstanceProcAddr,
&Device_requirements);
if (VulkanApiVersion < Device_requirements.minApiVersion) {
mprintf(("Vulkan: FSR unavailable: needs a newer Vulkan API version than the renderer uses\n"));
return;
}
if (!ffxPortableVkApplyRequirements(&Device_requirements, reinterpret_cast<VkDeviceCreateInfo*>(&createInfo))) {
mprintf(("Vulkan: FSR unavailable: too many device extensions\n"));
return;
}
mprintf(("Vulkan: FSR device requirements applied (FP8 capable: %s, INT8 capable: %s)\n",
Device_requirements.fp8Capable ? "yes" : "no",
Device_requirements.int8Capable ? "yes" : "no"));
Upscaler_gpu = gpu;
Upscaler_available = true;
}
void vulkan_upscaler_device_created(vk::Device device)
{
if (!Upscaler_available) {
return;
}
Gr_fsr_max_output_size = static_cast<int>(Upscaler_gpu.getProperties().limits.maxImageDimension2D);
Gr_fsr_versions.clear();
const auto versions = query_fsr_versions(device);
for (const auto& version : versions) {
mprintf(("Vulkan: FSR version available: %s\n", version.name.c_str()));
if (is_offered(major_version(version.name))) {
Gr_fsr_versions.push_back(version.name);
}
}
// FSR 2 is only the fallback for hardware where neither of the newer versions is available
if (Gr_fsr_versions.empty()) {
for (const auto& version : versions) {
if (major_version(version.name) == 2) {
Gr_fsr_versions.push_back(version.name);
}
}
}
std::sort(Gr_fsr_versions.begin(), Gr_fsr_versions.end(),
[](const SCP_string& a, const SCP_string& b) { return major_version(a) > major_version(b); });
}
bool vulkan_upscaler_available()
{
return Upscaler_available;
}
bool vulkan_upscaler_usable()
{
return Upscaler_available && !failed_for_current_setting();
}
bool VulkanUpscaler::wanted()
{
return vulkan_upscaler_usable() && Gr_fsr_mode != FsrMode::Off;
}
bool VulkanUpscaler::createResources()
{
if (m_context) {
return true;
}
const uint32_t renderW = m_ctx->sceneExtent.width;
const uint32_t renderH = m_ctx->sceneExtent.height;
const uint32_t w = m_ctx->outputExtent.width;
const uint32_t h = m_ctx->outputExtent.height;
m_output.format = HDR_COLOR_FORMAT;
m_output.width = w;
m_output.height = h;
if (!m_ctx->createImage(w, h, HDR_COLOR_FORMAT,
vk::ImageUsageFlagBits::eStorage | vk::ImageUsageFlagBits::eSampled | vk::ImageUsageFlagBits::eColorAttachment,
vk::ImageAspectFlagBits::eColor,
m_output.image, m_output.view, m_output.allocation)) {
mprintf(("Vulkan: FSR disabled: could not create its output image\n"));
mark_failed();
return false;
}
ffxCreateBackendVKDesc backend{};
backend.header.type = FFX_API_CREATE_CONTEXT_DESC_TYPE_BACKEND_VK;
backend.deviceContext.vkDevice = static_cast<VkDevice>(m_ctx->device);
backend.deviceContext.vkPhysicalDevice = static_cast<VkPhysicalDevice>(Upscaler_gpu);
backend.deviceContext.vkDeviceProcAddr = VULKAN_HPP_DEFAULT_DISPATCHER.vkGetDeviceProcAddr;
ffxCreateContextDescUpscaleVersion version{};
version.header.type = FFX_API_CREATE_CONTEXT_DESC_TYPE_UPSCALE_VERSION;
version.header.pNext = &backend.header;
version.version = FFX_UPSCALER_VERSION;
ffxCreateContextDescUpscale desc{};
desc.header.type = FFX_API_CREATE_CONTEXT_DESC_TYPE_UPSCALE;
desc.header.pNext = &version.header;
desc.flags = FFX_UPSCALE_ENABLE_HIGH_DYNAMIC_RANGE | FFX_UPSCALE_ENABLE_AUTO_EXPOSURE;
desc.maxRenderSize = {renderW, renderH};
desc.maxUpscaleSize = {w, h};
desc.fpMessage = log_ffx_message;
ffxOverrideVersion chosenVersion{};
const auto versions = query_fsr_versions(m_ctx->device);
if (const auto* chosen = find_chosen_version(versions)) {
chosenVersion.header.type = FFX_API_DESC_TYPE_OVERRIDE_VERSION;
chosenVersion.header.pNext = desc.header.pNext;
chosenVersion.versionId = chosen->id;
desc.header.pNext = &chosenVersion.header;
}
m_contextMajorVersion = Gr_fsr_major_version;
const auto result = ffxCreateContext(&m_context, &desc.header, nullptr);
if (result != FFX_API_RETURN_OK) {
mprintf(("Vulkan: FSR disabled: creating its context failed with code %u\n", result));
m_context = nullptr;
m_ctx->destroyTarget(m_output);
mark_failed();
return false;
}
ffxQueryGetProviderVersion provider{};
provider.header.type = FFX_API_QUERY_DESC_TYPE_GET_PROVIDER_VERSION;
const bool versionKnown = ffxQuery(&m_context, &provider.header) == FFX_API_RETURN_OK && provider.versionName;
mprintf(("Vulkan: FSR context created for %ux%u to %ux%u, loaded FSR version: %s\n", renderW, renderH, w, h,
versionKnown ? provider.versionName : "unknown"));
ffxQueryDescUpscaleGetJitterPhaseCount phaseCount{};
phaseCount.header.type = FFX_API_QUERY_DESC_TYPE_UPSCALE_GETJITTERPHASECOUNT;
phaseCount.renderWidth = renderW;
phaseCount.displayWidth = w;
phaseCount.pOutPhaseCount = &m_jitterPhaseCount;
if (ffxQuery(&m_context, &phaseCount.header) != FFX_API_RETURN_OK || m_jitterPhaseCount < 1) {
m_jitterPhaseCount = 1;
}
m_jitterIndex = 0;
return true;
}
void VulkanUpscaler::destroyTargets()
{
if (!m_ctx) {
return;
}
if (m_context) {
ffxDestroyContext(&m_context, nullptr);
m_context = nullptr;
}
m_ctx->destroyTarget(m_output);
}
void VulkanUpscaler::beginFrame(float& imageOffsetX, float& imageOffsetY, float& lodBias)
{
m_jitterIndex = (m_jitterIndex + 1) % m_jitterPhaseCount;
ffxQueryDescUpscaleGetJitterOffset query{};
query.header.type = FFX_API_QUERY_DESC_TYPE_UPSCALE_GETJITTEROFFSET;
query.index = m_jitterIndex;
query.phaseCount = m_jitterPhaseCount;
query.pOutX = &m_jitterX;
query.pOutY = &m_jitterY;
if (ffxQuery(&m_context, &query.header) != FFX_API_RETURN_OK) {
m_jitterX = 0.0f;
m_jitterY = 0.0f;
}
// FSR takes a pixel to show the content at its position minus the jitter, so the image moves by the jitter, in
// image space with Y down.
imageOffsetX = m_jitterX;
imageOffsetY = m_jitterY;
lodBias = std::log2(static_cast<float>(m_ctx->sceneExtent.width) / static_cast<float>(m_output.width)) - 1.0f;
}
bool VulkanUpscaler::dispatch(vk::CommandBuffer cmd, const RenderTarget& color, const RenderTarget& depth, const RenderTarget& motion)
{
GR_DEBUG_SCOPE("FSR");
cmdMemoryBarrier(cmd,
vk::PipelineStageFlagBits2::eColorAttachmentOutput,
vk::AccessFlagBits2::eColorAttachmentWrite,
vk::PipelineStageFlagBits2::eComputeShader,
vk::AccessFlagBits2::eShaderRead);
ImageBarrier2 barrier;
barrier.image = m_output.image;
barrier.levelCount = 1;
barrier.layerCount = 1;
barrier.oldLayout = vk::ImageLayout::eUndefined;
barrier.newLayout = vk::ImageLayout::eGeneral;
// Last frame's bloom composited into the output, and tonemapping read it
barrier.srcStage = vk::PipelineStageFlagBits2::eColorAttachmentOutput | vk::PipelineStageFlagBits2::eFragmentShader;
barrier.srcAccess = vk::AccessFlagBits2::eColorAttachmentWrite;
barrier.dstStage = vk::PipelineStageFlagBits2::eComputeShader;
barrier.dstAccess = vk::AccessFlagBits2::eShaderWrite;
cmdImageBarrier(cmd, barrier);
ffxDispatchDescUpscale desc{};
desc.header.type = FFX_API_DISPATCH_DESC_TYPE_UPSCALE;
desc.commandList = static_cast<VkCommandBuffer>(cmd);
desc.color = ffx_image(color, FFX_API_RESOURCE_STATE_COMPUTE_READ);
desc.depth = ffx_image(depth, FFX_API_RESOURCE_STATE_COMPUTE_READ);
desc.motionVectors = ffx_image(motion, FFX_API_RESOURCE_STATE_COMPUTE_READ);
desc.output = ffx_image(m_output, FFX_API_RESOURCE_STATE_UNORDERED_ACCESS, FFX_API_RESOURCE_USAGE_UAV);
desc.jitterOffset = {m_jitterX, m_jitterY};
desc.renderSize = {color.width, color.height};
desc.upscaleSize = {m_output.width, m_output.height};
// Motion vectors are in UV units
desc.motionVectorScale = {static_cast<float>(color.width), static_cast<float>(color.height)};
desc.frameTimeDelta = flRealframetime * 1000.0f;
desc.preExposure = 1.0f;
desc.reset = gr_consume_temporal_reset();
// The motion vector pass puts all depth on the cockpit's projection
desc.cameraNear = Min_draw_distance_cockpit;
desc.cameraFar = Max_draw_distance;
desc.cameraFovAngleVertical = std::holds_alternative<float>(Proj_fov) ? std::get<float>(Proj_fov) : 1.0f;
desc.viewSpaceToMetersFactor = 1.0f;
const auto result = ffxDispatch(&m_context, &desc.header);
const bool dispatched = result == FFX_API_RETURN_OK;
if (!dispatched) {
mprintf(("Vulkan: FSR disabled: dispatch failed with code %u\n", result));
mark_failed();
}
barrier.oldLayout = vk::ImageLayout::eGeneral;
barrier.newLayout = vk::ImageLayout::eShaderReadOnlyOptimal;
barrier.srcStage = vk::PipelineStageFlagBits2::eComputeShader;
barrier.srcAccess = vk::AccessFlagBits2::eShaderWrite;
barrier.dstStage = vk::PipelineStageFlagBits2::eFragmentShader;
barrier.dstAccess = vk::AccessFlagBits2::eShaderSampledRead;
cmdImageBarrier(cmd, barrier);
return dispatched;
}
#else // WITH_FFX
void vulkan_upscaler_prepare_device(vk::Instance /*instance*/,
vk::PhysicalDevice /*gpu*/,
uint32_t /*graphicsQueueFamily*/,
const vk::PhysicalDeviceFeatures& /*supportedFeatures*/,
vk::DeviceCreateInfo& /*createInfo*/)
{
}
void vulkan_upscaler_device_created(vk::Device /*device*/)
{
}
bool vulkan_upscaler_available()
{
return false;
}
bool vulkan_upscaler_usable()
{
return false;
}
bool VulkanUpscaler::wanted()
{
return false;
}
bool VulkanUpscaler::createResources()
{
return false;
}
void VulkanUpscaler::destroyTargets()
{
}
void VulkanUpscaler::beginFrame(float& /*imageOffsetX*/, float& /*imageOffsetY*/, float& /*lodBias*/)
{
}
bool VulkanUpscaler::dispatch(vk::CommandBuffer /*cmd*/, const RenderTarget& /*color*/, const RenderTarget& /*depth*/, const RenderTarget& /*motion*/)
{
return false;
}
#endif // WITH_FFX
} // namespace graphics::vulkan
+26
View File
@@ -0,0 +1,26 @@
#pragma once
#include <vulkan/vulkan.hpp>
namespace graphics::vulkan {
/**
* @brief Adds what FSR needs to the device about to be created, if FSR can run on it
*
* Must run after the create info's pNext chain is final, and the create info must not outlive the process-lifetime
* storage this keeps for it. Failure only means FSR stays unavailable; the renderer carries on without it.
*/
void vulkan_upscaler_prepare_device(vk::Instance instance,
vk::PhysicalDevice gpu,
uint32_t graphicsQueueFamily,
const vk::PhysicalDeviceFeatures& supportedFeatures,
vk::DeviceCreateInfo& createInfo);
// Looks up which FSR version this device gets, for display.
void vulkan_upscaler_device_created(vk::Device device);
bool vulkan_upscaler_available();
// Available, and hasn't failed with the currently chosen mode and version
bool vulkan_upscaler_usable();
} // namespace graphics::vulkan
@@ -32,6 +32,8 @@ const VertexFormatMapping VERTEX_FORMAT_MAPPINGS[] = {
// Matrix format (mat4 = 4 vec4s, uses locations 8-11)
{ vertex_format_data::MATRIX4, vk::Format::eR32G32B32A32Sfloat, VATTRIB_MODEL_MATRIX, 16, 64 },
{ vertex_format_data::MOTION4, vk::Format::eR32G32B32A32Sfloat, VATTRIB_EFFECT_MOTION, 4, 16 },
};
const size_t VERTEX_FORMAT_MAPPINGS_COUNT = sizeof(VERTEX_FORMAT_MAPPINGS) / sizeof(VERTEX_FORMAT_MAPPINGS[0]);
+57 -28
View File
@@ -27,6 +27,7 @@
#include "graphics/post_processing.h"
#include "graphics/grinternal.h"
#include "graphics/shadows.h"
#include "graphics/vulkan/VulkanUpscaler.h"
#include "lighting/lighting.h"
#include "pngutils/pngutils.h"
@@ -75,32 +76,6 @@ void vulkan_model_unloaded(int pm_id)
}
}
void vulkan_build_shadow_tlas()
{
if (auto* rt = getRaytracingManager()) {
// vkCmdBuildAccelerationStructuresKHR (and the memory barrier that follows
// it in buildTlas()) must be recorded outside any render pass instance.
// This is called once per frame, before the shadow map render pass begins,
// while the previous render pass (G-buffer/scene) is typically still
// active -- end it here. vulkan_shadow_map_start()'s first_pass branch
// skips its own endRenderPass() call when it finds none active.
auto* stateTracker = getStateTracker();
if (stateTracker->getCurrentRenderPass()) {
stateTracker->getCommandBuffer().endRenderPass();
stateTracker->setRenderPass(vk::RenderPass());
}
rt->buildTlas();
// Refresh the Global set's live TLAS fallback so every writeSet(Global)
// this frame picks up the freshly built TLAS automatically -- see
// DescriptorFallbacks::shadowTlas.
getDescriptorManager()->setCurrentShadowTlas(rt->getTlasForShaderBinding());
// The memoized per-frame Global set (VulkanDrawManager) must rebuild so the
// new TLAS reaches subsequent draws' Set 0 instead of a cached stale one.
getDrawManager()->invalidateGlobalSet();
}
}
bool vulkan_is_capable(gr_capability capability)
{
switch (capability) {
@@ -162,6 +137,12 @@ bool vulkan_is_capable(gr_capability capability)
return false;
case gr_capability::CAPABILITY_RAYTRACED_SHADOWS:
return getRendererInstance()->supportsRaytracedShadows();
case gr_capability::CAPABILITY_SHADOW_CONTACT_HARDENING:
// Vulkan samplers are independent of images, so a second (non-compare) sampler on
// the same shadow map view is always available -- no capability gap here.
return true;
case gr_capability::CAPABILITY_FSR:
return vulkan_upscaler_available();
}
return false;
}
@@ -252,7 +233,14 @@ void vulkan_imgui_render_draw_data()
{
auto* renderer = getRendererInstance();
if (renderer) {
ImGui_ImplVulkan_RenderDrawData(ImGui::GetDrawData(), renderer->getVkCurrentCommandBuffer());
// The composition image is larger than ImGui's display while FSR upscales
auto* drawData = ImGui::GetDrawData();
const auto extent = renderer->getCompositionExtent();
if (drawData && drawData->DisplaySize.x > 0.0f && drawData->DisplaySize.y > 0.0f) {
drawData->FramebufferScale = ImVec2(static_cast<float>(extent.width) / drawData->DisplaySize.x,
static_cast<float>(extent.height) / drawData->DisplaySize.y);
}
ImGui_ImplVulkan_RenderDrawData(drawData, renderer->getVkCurrentCommandBuffer());
// ImGui just bound its own pipeline/descriptor set directly on the command buffer,
// inside the same (already-active) render pass FSO's own draws share -- it can't begin
@@ -316,6 +304,24 @@ int vulkan_save_screen()
return -1;
}
// While FSR upscales, the frame is larger than gr_screen, but restoring draws it 1:1 in gr_screen pixels
const auto screen_w = static_cast<uint32_t>(gr_screen.max_w);
const auto screen_h = static_cast<uint32_t>(gr_screen.max_h);
if (w != screen_w || h != screen_h) {
auto* scaled = static_cast<ubyte*>(vm_malloc(static_cast<int>(screen_w * screen_h * 4)));
for (uint32_t y = 0; y < screen_h; ++y) {
const uint32_t src_y = std::min((2 * y + 1) * h / (2 * screen_h), h - 1);
for (uint32_t x = 0; x < screen_w; ++x) {
const uint32_t src_x = std::min((2 * x + 1) * w / (2 * screen_w), w - 1);
memcpy(&scaled[(y * screen_w + x) * 4], &pixels[(src_y * w + src_x) * 4], 4);
}
}
vm_free(pixels);
pixels = scaled;
w = screen_w;
h = screen_h;
}
int bmpId = bm_create(32, static_cast<int>(w), static_cast<int>(h), pixels, 0);
if (bmpId < 0) {
vm_free(pixels);
@@ -503,7 +509,6 @@ void init_function_pointers()
gr_screen.gf_model_loaded = vulkan_model_loaded;
gr_screen.gf_model_unloaded = vulkan_model_unloaded;
gr_screen.gf_build_shadow_tlas = vulkan_build_shadow_tlas;
gr_screen.gf_update_transform_buffer = vulkan_update_transform_buffer;
gr_screen.gf_update_buffer_data = vulkan_update_buffer_data;
@@ -601,6 +606,30 @@ void init_function_pointers()
} // anonymous namespace
bool vulkan_frame_tlas_pending()
{
auto* rt = getRaytracingManager();
return rt != nullptr && rt->isEnabled() && shadows_rt_scene_needed() && !rt->isTlasBuiltThisFrame();
}
void vulkan_build_frame_tlas()
{
auto* rt = getRaytracingManager();
if (rt == nullptr) {
return;
}
// vkCmdBuildAccelerationStructuresKHR and the barrier after it must be recorded outside a
// render pass instance. The caller makes sure that no render pass is open.
rt->buildTlas();
// Refresh the live TLAS value of the Global set, so that each writeSet(Global) in this
// frame gets the new TLAS. See DescriptorFallbacks::shadowTlas.
getDescriptorManager()->setCurrentShadowTlas(rt->getTlasForShaderBinding());
// The memoized Global set of VulkanDrawManager must rebuild, so that the next draws get
// the new TLAS and not a stale cached set.
getDrawManager()->invalidateGlobalSet();
}
void initialize_function_pointers() {
init_function_pointers();
}
+9
View File
@@ -11,6 +11,15 @@ bool initialize(std::unique_ptr<os::GraphicsOperations>&& graphicsOps);
VulkanRenderer* getRendererInstance();
// True when this frame needs a ray-tracing TLAS (see shadows_rt_scene_needed()) and no build
// for this frame occurred yet.
bool vulkan_frame_tlas_pending();
// Builds the ray-tracing TLAS for this frame. vulkan_scene_texture_begin() calls this once per
// frame, before the scene render pass starts. Call it only when no render pass is open, because
// acceleration-structure builds are not permitted inside a render pass.
void vulkan_build_frame_tlas();
void cleanup();
} // namespace graphics::vulkan
-1
View File
@@ -599,7 +599,6 @@ void HudGaugeTargetBox::renderTargetSetup(vec3d *camera_eye, matrix *camera_orie
g3_start_frame(1); // Turn on zbuffering
hud_save_restore_camera_data(1);
g3_set_view_matrix( camera_eye, camera_orient, zoom);
model_set_detail_level(1); // use medium detail level
setClip(position[0] + Viewport_offsets[0], position[1] + Viewport_offsets[1], Viewport_w, Viewport_h);
+123
View File
@@ -528,6 +528,32 @@ void LabUi::build_shadow_method_combobox()
}
}
static const char* rt_shadow_quality_settings[] = {
"Low (directional lights only)",
"High (also point/tube/cone lights)",
};
void LabUi::build_rt_shadow_quality_combobox()
{
// Only meaningful when raytraced shadows are actually the active method.
if (!shadows_raytracing_supported() || Shadow_render_method != ShadowRenderMethod::Raytraced) {
return;
}
with_Combo("Raytraced Shadow Quality", rt_shadow_quality_settings[static_cast<int>(Rt_shadow_quality)])
{
for (int n = 0; n < IM_ARRAYSIZE(rt_shadow_quality_settings); n++) {
bool is_selected = static_cast<int>(Rt_shadow_quality) == n;
if (Selectable(rt_shadow_quality_settings[n], is_selected))
LabRenderer::setRtShadowQuality(static_cast<RTShadowQuality>(n));
if (is_selected)
SetItemDefaultFocus();
}
}
}
void LabUi::build_max_rt_shadow_lights_slider()
{
// Only meaningful when raytraced shadows are actually the active method.
@@ -539,6 +565,19 @@ void LabUi::build_max_rt_shadow_lights_slider()
if (SliderInt("Max Raytraced Shadow Lights", &count, 1, 8)) {
LabRenderer::setMaxRtShadowLights(count);
}
// The local-light cap is only consulted at High quality, so don't offer it at Low --
// same as how the sun size slider only appears once its override is on.
if (Rt_shadow_quality != RTShadowQuality::High) {
return;
}
// 0 is a useful setting rather than a degenerate one: it turns local-light shadows off
// without leaving High, which isolates what the directional lights are contributing.
int local_count = Max_rt_shadow_local_lights;
if (SliderInt("Max Raytraced Local Shadow Lights", &local_count, 0, 64)) {
LabRenderer::setMaxRtShadowLocalLights(local_count);
}
}
void LabUi::build_rt_shadow_bias_sliders()
@@ -559,6 +598,84 @@ void LabUi::build_rt_shadow_bias_sliders()
}
}
void LabUi::build_shadow_penumbra_sliders()
{
// The sample count is a raytracing cost knob, so it only appears when raytraced shadows
// are actually the active method. The sun size below is deliberately not gated that way.
if (shadows_raytracing_supported() && Shadow_render_method == ShadowRenderMethod::Raytraced) {
// Starts at whatever Shadow_quality implies; moving it pins a session-only
// override, so the slider stops tracking the quality tier from then on.
int samples = shadows_rt_sample_count();
if (SliderInt("Raytraced Shadow Samples", &samples, 1, 16)) {
LabRenderer::setRtShadowSamples(samples);
}
}
// Contact hardening is a shadow-map-only technique (see pcssPenumbraRadius() in
// shadows.sdr) -- raytraced shadows size their penumbra from traceShadowRayCone()'s
// cone sampling instead, so this has no effect there and isn't worth showing.
if (shadow_contact_hardening_supported() && Shadow_render_method == ShadowRenderMethod::ShadowMap) {
bool contact_hardening = Shadow_contact_hardening_enabled;
if (Checkbox("Shadow Contact Hardening", &contact_hardening)) {
LabRenderer::setShadowContactHardening(contact_hardening);
}
}
// Overrides whatever size the suns would otherwise use -- their $SunAngularSize, or
// the size measured from their bitmaps -- so penumbras can be tried out against any
// background; the checkbox's default slider value is Sol's apparent diameter.
//
// Shown for both shadow methods, because this is the one parameter that drives softness
// in both: it sizes the raytraced penumbra cone and scales the shadow map's filter width
// (see shadow_smoothness_scale() in shadows.cpp). Sitting on the slider and flipping the
// render method is the intended way to check the two against each other.
//
// It needs a mission background loaded to do anything: it overrides the size of drawn
// suns, and with no background the lab lights the scene from common_setup_room_lights()
// instead, which draws no suns for this to override.
if (Shadow_quality == ShadowQuality::Disabled) {
return;
}
bool override_sun = Sun_angular_size_override >= 0.0f;
if (Checkbox("Override Sun Angular Size", &override_sun)) {
LabRenderer::setSunAngularSizeOverride(override_sun ? SUN_ANGULAR_SIZE_SOL : SUN_ANGULAR_SIZE_UNSPECIFIED);
}
if (override_sun) {
float sun_size = Sun_angular_size_override;
if (SliderFloat("Sun Angular Size (degrees)", &sun_size, 0.0f, 10.0f)) {
LabRenderer::setSunAngularSizeOverride(sun_size);
}
}
}
void LabUi::build_rtao_sliders()
{
// Unlike the RT shadow sliders, RTAO (raytraced ambient occlusion) does not depend on
// the shadow method or the shadow quality. It only needs ray-query support.
if (!rtao_supported()) {
return;
}
int samples = Rtao_samples;
if (SliderInt("Ambient Occlusion Rays", &samples, 0, 16)) {
LabRenderer::setRtaoSamples(samples);
}
// Radius/strength are the mod-owned lighting-profile values ($RTAO Radius /
// $RTAO Strength); these override the active profile for this session only,
// same as the exposure/tonemapper controls below.
float radius = lighting_profiles::current_rtao_radius();
if (SliderFloat("Ambient Occlusion Radius", &radius, 0.0f, 200.0f)) {
lighting_profiles::lab_set_rtao_radius(radius);
}
float strength = lighting_profiles::current_rtao_strength();
if (SliderFloat("Ambient Occlusion Strength", &strength, 0.0f, 2.0f)) {
lighting_profiles::lab_set_rtao_strength(strength);
}
}
namespace ltp = lighting_profiles;
using namespace ltp;
@@ -658,10 +775,16 @@ void LabUi::show_render_options()
build_shadow_method_combobox();
build_rt_shadow_quality_combobox();
build_max_rt_shadow_lights_slider();
build_rt_shadow_bias_sliders();
build_shadow_penumbra_sliders();
build_rtao_sliders();
build_tone_mapper_combobox();
if (ltp::current_tonemapper() == TonemapperAlgorithm::PPC ||
+3
View File
@@ -44,8 +44,11 @@ class LabUi {
void build_texture_quality_combobox();
void build_antialiasing_combobox();
static void build_shadow_method_combobox();
static void build_rt_shadow_quality_combobox();
static void build_max_rt_shadow_lights_slider();
static void build_rt_shadow_bias_sliders();
static void build_shadow_penumbra_sliders();
static void build_rtao_sliders();
void build_tone_mapper_combobox();
void build_model_info_box(ship_info* sip, polymodel* pm) const;
void build_subsystem_list(object* objp, ship* shipp) const;
+49
View File
@@ -2,6 +2,9 @@
#include "lab/labv2_internal.h"
#include "lab/manager/lab_manager.h"
#include "gamesequence/gamesequence.h"
#include "graphics/rtao.h"
#include "graphics/shadows.h"
#include "starfield/starfield.h"
static std::unique_ptr<LabManager> LMGR;
@@ -13,11 +16,57 @@ const std::unique_ptr<LabManager> &getLabManager() {
return LMGR;
}
namespace {
// The lab changes these settings directly, so we save them when the lab opens and restore them when it closes.
// Each LabRenderer setter that writes a game global must have a field here.
struct LabSavedSettings {
ShadowRenderMethod shadow_render_method;
RTShadowQuality rt_shadow_quality;
int max_rt_shadow_lights;
int max_rt_shadow_local_lights;
float rt_shadow_bias_min;
float rt_shadow_bias_max;
int rt_shadow_samples_override;
float sun_angular_size_override;
int rtao_samples;
bool shadow_contact_hardening_enabled;
};
bool Lab_settings_saved = false;
LabSavedSettings Lab_saved_settings;
}
void lab_init() {
gr_set_clear_color(0, 0, 0);
Lab_saved_settings = {Shadow_render_method,
Rt_shadow_quality,
Max_rt_shadow_lights,
Max_rt_shadow_local_lights,
Rt_shadow_bias_min,
Rt_shadow_bias_max,
Rt_shadow_samples_override,
Sun_angular_size_override,
Rtao_samples,
Shadow_contact_hardening_enabled};
Lab_settings_saved = true;
}
void lab_close(bool reset) {
if (Lab_settings_saved) {
Shadow_render_method = Lab_saved_settings.shadow_render_method;
Rt_shadow_quality = Lab_saved_settings.rt_shadow_quality;
Max_rt_shadow_lights = Lab_saved_settings.max_rt_shadow_lights;
Max_rt_shadow_local_lights = Lab_saved_settings.max_rt_shadow_local_lights;
Rt_shadow_bias_min = Lab_saved_settings.rt_shadow_bias_min;
Rt_shadow_bias_max = Lab_saved_settings.rt_shadow_bias_max;
Rt_shadow_samples_override = Lab_saved_settings.rt_shadow_samples_override;
Sun_angular_size_override = Lab_saved_settings.sun_angular_size_override;
Rtao_samples = Lab_saved_settings.rtao_samples;
Shadow_contact_hardening_enabled = Lab_saved_settings.shadow_contact_hardening_enabled;
Lab_settings_saved = false;
}
if (reset) {
LMGR.reset();
}
+43
View File
@@ -3,6 +3,7 @@
#include "globalincs/pstypes.h"
#include "globalincs/flagset.h"
#include "graphics/2d.h"
#include "graphics/rtao.h"
#include "graphics/shadows.h"
#include "lighting/lighting_profiles.h"
#include "camera/camera.h"
@@ -130,12 +131,27 @@ public:
Shadow_render_method = method;
}
// Session-only override, same as setShadowRenderMethod -- does not touch the
// persisted Raytraced Shadow Quality option. Safe to change at any time: the
// only thing it gates is whether local lights are picked as shadow casters
// while filling the light uniforms, which happens fresh every frame, so
// nothing has to be rebuilt for the switch to take effect.
static void setRtShadowQuality(RTShadowQuality quality) {
Rt_shadow_quality = quality;
}
// Session-only override, same as setShadowRenderMethod -- does not touch the
// persisted Max Raytraced Shadow Lights option.
static void setMaxRtShadowLights(int count) {
Max_rt_shadow_lights = count;
}
// Session-only override, same as setMaxRtShadowLights -- does not touch the persisted
// Max Raytraced Local Shadow Lights option. Only consulted at RTShadowQuality::High.
static void setMaxRtShadowLocalLights(int count) {
Max_rt_shadow_local_lights = count;
}
// Session-only overrides, same as setMaxRtShadowLights -- do not touch the
// persisted Min/Max Raytraced Shadow Bias options. See Rt_shadow_bias_min/max
// in shadows.h.
@@ -147,6 +163,33 @@ public:
Rt_shadow_bias_max = bias;
}
// Ray count normally follows Shadow_quality (see shadows_rt_sample_count()), which is
// fixed at startup because the shadow map is sized then. The ray count isn't, so the
// lab can sweep it live through this session-only override. See
// Rt_shadow_samples_override in shadows.h.
static void setRtShadowSamples(int samples) {
Rt_shadow_samples_override = samples;
}
// Session-only override of every sun's $SunAngularSize (degrees of apparent
// diameter); pass a negative value to return to the stars.tbl values. See
// Sun_angular_size_override in starfield.h.
static void setSunAngularSizeOverride(float degrees) {
Sun_angular_size_override = degrees;
}
// Session-only override, same as setRtShadowSamples -- does not touch the
// persisted Raytraced Ambient Occlusion option. See Rtao_samples in rtao.h.
static void setRtaoSamples(int samples) {
Rtao_samples = samples;
}
// Session-only override, same as setRtaoSamples -- does not touch the persisted
// Shadow Contact Hardening option. See Shadow_contact_hardening_enabled in shadows.h.
static void setShadowContactHardening(bool enabled) {
Shadow_contact_hardening_enabled = enabled;
}
static void setTonemapper(ltp::TonemapperAlgorithm mode) {
ltp::lab_set_tonemapper(mode);
}
+38
View File
@@ -97,6 +97,16 @@ float current_exposure()
return _current.exposure;
}
float current_rtao_radius()
{
return _current.rtao_radius;
}
float current_rtao_strength()
{
return _current.rtao_strength;
}
piecewise_power_curve_intermediates current_piecewise_intermediates()
{
return calc_intermediates(_current.ppc_values);
@@ -211,6 +221,16 @@ void lab_set_exposure(float exIn)
_current.exposure = exIn;
}
void lab_set_rtao_radius(float radius)
{
_current.rtao_radius = radius;
}
void lab_set_rtao_strength(float strength)
{
_current.rtao_strength = strength;
}
void lab_set_tonemapper(TonemapperAlgorithm tnin)
{
_current.tonemapper = tnin;
@@ -381,6 +401,21 @@ void profile::parse(const char* filename, const SCP_string& profile_name, const
parsed |= parse_optional_float_into("$PPC Shoulder Angle:", &ppc_values.shoulder_angle);
parsed |= parse_optional_float_into("$Exposure:", &exposure);
if (parse_optional_float_into("$RTAO Radius:", &rtao_radius)) {
parsed = true;
if (rtao_radius < 0.0f) {
error_display(0, "$RTAO Radius: must be >= 0 (got %f); using 0.", rtao_radius);
rtao_radius = 0.0f;
}
}
if (parse_optional_float_into("$RTAO Strength:", &rtao_strength)) {
parsed = true;
if (rtao_strength < 0.0f) {
error_display(0, "$RTAO Strength: must be >= 0 (got %f); using 0.", rtao_strength);
rtao_strength = 0.0f;
}
}
parsed |= adjustment::parse(filename, "$Missile light brightness:", profile_name, &missile_light_brightness);
parsed |= adjustment::parse(filename, "$Missile light radius:", profile_name, &missile_light_radius);
@@ -444,6 +479,9 @@ void profile::reset()
exposure = 4.0f;
rtao_radius = 20.0f;
rtao_strength = 1.0f;
missile_light_brightness.reset();
missile_light_radius.reset();
missile_light_radius.base = 20.0f;
+11
View File
@@ -59,6 +59,13 @@ class profile {
TonemapperAlgorithm tonemapper;
piecewise_power_curve_values ppc_values;
float exposure;
// Raytraced ambient occlusion tuning (only sampled when rtao_enabled(), see
// graphics/rtao.h). Radius is the AO ray length in world units -- it is
// content-scale-dependent (fighters vs. capships), which is why it lives in
// the mod-owned lighting profile rather than a user option. Strength is an
// exponent on the occlusion term (1 = physical, >1 darkens, <1 lightens).
float rtao_radius;
float rtao_strength;
adjustment missile_light_brightness;
adjustment missile_light_radius;
adjustment laser_light_brightness;
@@ -97,7 +104,11 @@ const piecewise_power_curve_values& current_piecewise_values();
piecewise_power_curve_intermediates current_piecewise_intermediates();
piecewise_power_curve_intermediates calc_intermediates(piecewise_power_curve_values input);
float current_exposure();
float current_rtao_radius();
float current_rtao_strength();
void lab_set_exposure(float exIn);
void lab_set_rtao_radius(float radius);
void lab_set_rtao_strength(float strength);
void lab_set_tonemapper(TonemapperAlgorithm tnin);
void lab_set_ppc(const piecewise_power_curve_values &ppcin);
const piecewise_power_curve_values &lab_get_ppc();
+24
View File
@@ -6181,6 +6181,14 @@ void parse_one_background(background_t *background)
sle.div_x = 1;
sle.div_y = 1;
// apparent diameter in degrees, overriding both the sun's stars.tbl entry and the
// size that would otherwise be measured from its bitmap. Optional, so missions
// written before 26.3 simply don't have it
if (optional_string("+AngularSize:")) {
stuff_float(&sle.angular_size);
sle.angular_size = stars_check_angular_size(sle.angular_size, "+AngularSize:", sle.filename);
}
// add it
background->suns.push_back(sle);
}
@@ -9694,3 +9702,19 @@ bool check_for_25_1_data()
return false;
}
bool check_for_26_3_data()
{
// a sun that carries its own apparent diameter (+AngularSize:) can't be represented
// in an older mission file
for (const auto &background : Backgrounds)
{
for (const auto &sun : background.suns)
{
if (sun.angular_size >= 0.0f)
return true;
}
}
return false;
}
+1
View File
@@ -61,6 +61,7 @@ extern bool check_for_23_3_data();
extern bool check_for_24_1_data();
extern bool check_for_24_3_data();
extern bool check_for_25_1_data();
extern bool check_for_26_3_data();
#define WING_PLAYER_BASE 0x80000 // used by Fred to tell ship_index in a wing points to a player
+6 -5
View File
@@ -79,7 +79,7 @@ int Training_message_method = 1;
int Training_num_lines = 0;
int Training_voice = -1;
int Training_voice_type;
int Training_voice_soundstream;
int Training_voice_soundstream = -1;
sound_handle Training_voice_snd_handle;
int Training_flag = 0;
int Training_failure = 0;
@@ -699,6 +699,7 @@ void training_mission_shutdown()
if (Training_voice >= 0) {
if (Training_voice_type) {
audiostream_close_file(Training_voice_soundstream, false);
Training_voice_soundstream = -1;
} else {
snd_stop(Training_voice_snd_handle);
@@ -832,6 +833,7 @@ int message_play_training_voice(int index)
if (Training_voice >= 0) {
if (Training_voice_type) {
audiostream_close_file(Training_voice_soundstream, false);
Training_voice_soundstream = -1;
} else {
snd_stop(Training_voice_snd_handle);
@@ -853,10 +855,8 @@ int message_play_training_voice(int index)
if ((Training_voice < 0) || !Training_voice_type || (Training_voice != index)) {
if (Training_voice >= 0) {
if (Training_voice_type) {
if (Training_voice == index)
audiostream_stop(Training_voice_soundstream, 1, 0);
else
audiostream_close_file(Training_voice_soundstream, false);
audiostream_close_file(Training_voice_soundstream, false);
Training_voice_soundstream = -1;
} else {
snd_stop(Training_voice_snd_handle);
@@ -892,6 +892,7 @@ int message_play_training_voice(int index)
if (Training_voice >= 0) {
if (Training_voice_type) {
audiostream_close_file(Training_voice_soundstream, false);
Training_voice_soundstream = -1;
} else {
snd_stop(Training_voice_snd_handle);
+20 -1
View File
@@ -1035,6 +1035,17 @@ int Fred_mission_save::save_bitmaps()
required_string_fred("+Scale:");
parse_comments();
fout(" %f", sle->scale_x);
// apparent diameter, only written when this mission actually sets one; see
// check_for_26_3_data()
fso_comment_push(";;FSO 26.3.0;;");
FRED_ENSURE_PROPERTY_VERSION_WITH_DEFAULT_F("+AngularSize:",
1,
";;FSO 26.3.0;;",
SUN_ANGULAR_SIZE_UNSPECIFIED,
" %f",
sle->angular_size);
fso_comment_pop();
}
// save background bitmaps by filename
@@ -3171,7 +3182,15 @@ void Fred_mission_save::save_mission_internal(const char* pathname)
auto version_24_1 = gameversion::version(24, 1);
auto version_24_3 = gameversion::version(24, 3);
auto version_25_1 = gameversion::version(25, 1);
if (MISSION_VERSION >= version_25_1) {
auto version_26_3 = gameversion::version(26, 3);
if (MISSION_VERSION >= version_26_3) {
Warning(LOCATION,
"Notify an SCP coder: now that the required mission version is at least 26.3, the check_for_26_3_data(), "
"check_for_25_1_data(), check_for_24_3_data(), check_for_24_1_data(), and check_for_23_3_data() code can "
"be removed");
} else if (check_for_26_3_data()) {
The_mission.required_fso_version = version_26_3;
} else if (MISSION_VERSION >= version_25_1) {
Warning(LOCATION,
"Notify an SCP coder: now that the required mission version is at least 25.1, the check_for_25_1_data(), "
"check_for_24_3_data(), check_for_24_1_data(), and check_for_23_3_data() code can be removed");
+1 -1
View File
@@ -651,7 +651,7 @@ void cmd_brief_close()
for (i=0; i<Cur_cmd_brief->num_stages; i++) {
if (Cur_cmd_brief->stage[i].wave >= 0)
audiostream_close_file(Cur_cmd_brief->stage[i].wave, 0);
Cur_cmd_brief->stage[i].wave = -1;
}
// so that the same ani will reload properly upon return
+12 -3
View File
@@ -33,6 +33,8 @@
#include "io/timer.h"
#include "lighting/lighting.h"
#include "lighting/lighting_profiles.h"
#include "starfield/starfield.h"
#include "starfield/sun_disc.h"
#include "missionui/chatbox.h"
#include "missionui/missionbrief.h"
#include "missionui/missionscreencommon.h"
@@ -1989,16 +1991,23 @@ void draw_model_rotating(model_render_params *render_info, int ship_class, int m
*/
void common_setup_room_lights()
{
// These stand in for a sun rather than for a point source, so they get a sun's apparent
// size: for a directional light source_radius is the tangent of the angular radius, and
// it is what sizes both the raytraced penumbra cone and the shadow map's filter width
// (see shadow_smoothness_scale() in shadows.cpp). Leaving it at 0 would read as a light
// with no extent at all and give these rooms hard, aliased shadow edges.
const float room_light_source_radius = sun_disc_tangent_from_diameter(SUN_ANGULAR_SIZE_SOL);
light_reset();
auto tempv = vm_vec_new(-1.0f,0.3f,-1.0f);
auto tempc = hdr_color(1.0f,0.95f,0.9f, 0.0f, 1.5f);
light_add_directional(&tempv,-1,false,&tempc);
light_add_directional(&tempv,-1,false,&tempc,room_light_source_radius);
tempv.xyz={-0.4f,0.4f,1.1f};
tempc = hdr_color(0.788f,0.886f,1.0f,0.0f,1.5f);
light_add_directional(&tempv,-1,false,&tempc);
light_add_directional(&tempv,-1,false,&tempc,room_light_source_radius);
tempv.xyz={0.4f,0.1f,0.4f};
tempc = hdr_color(1.0f,1.0f,1.0f,0.0f,0.4f);
light_add_directional(&tempv,-1,false,&tempc);
light_add_directional(&tempv,-1,false,&tempc,room_light_source_radius);
gr_set_ambient_light(53, 53, 53);
}
+52 -44
View File
@@ -1248,6 +1248,22 @@ int eval_weapon_flag_for_game_type(int weapon_flags)
return rval;
}
static bool wl_bank_allows_weapon(const ship_info* sip, int bank, int weapon_class)
{
// an empty bank has nothing to place; this check runs on the weapon coming back from a swap
if (weapon_class < 0)
return true;
if (!eval_weapon_flag_for_game_type(sip->allowed_weapons[weapon_class]))
return false;
// allowed_weapons is built from the bank lists, so a class without them never gets here
if (!eval_weapon_flag_for_game_type(sip->restricted_loadout_flag[bank]))
return true;
return eval_weapon_flag_for_game_type(sip->allowed_bank_restricted_weapons[bank][weapon_class]) != 0;
}
/**
* Return the weapon class shown at the given on-screen row of a scrolling icon list,
* or -1 if that row is past the end of the list.
@@ -3684,34 +3700,30 @@ int wl_swap_slot_slot(int from_bank, int to_bank, int ship_slot, interface_snd_i
ship_info *sip = &Ship_info[slot->ship_class];
// check the to-bank first
if (eval_weapon_flag_for_game_type(sip->restricted_loadout_flag[to_bank])) {
if (!eval_weapon_flag_for_game_type(sip->allowed_bank_restricted_weapons[to_bank][slot->wep[from_bank]])) {
SCP_string txt;
sprintf(txt, XSTR("This bank is unable to carry %s weaponry", 1628), Weapon_info[slot->wep[from_bank]].get_display_name());
if (!wl_bank_allows_weapon(sip, to_bank, slot->wep[from_bank])) {
SCP_string txt;
sprintf(txt, XSTR("This bank is unable to carry %s weaponry", 1628), Weapon_info[slot->wep[from_bank]].get_display_name());
if ( !(Game_mode & GM_MULTIPLAYER) || (Netgame.host == pl) ) {
popup(PF_USE_AFFIRMATIVE_ICON, 1, POPUP_OK, txt.c_str());
} else if (pl != NULL) {
send_game_chat_packet(Netgame.host, txt.c_str(), MULTI_MSG_TARGET, pl, nullptr, 1);
}
return forced_update;
if ( !(Game_mode & GM_MULTIPLAYER) || (Netgame.host == pl) ) {
popup(PF_USE_AFFIRMATIVE_ICON, 1, POPUP_OK, txt.c_str());
} else if (pl != nullptr) {
send_game_chat_packet(Netgame.host, txt.c_str(), MULTI_MSG_TARGET, pl, nullptr, 1);
}
return forced_update;
}
// check the from-bank
if (eval_weapon_flag_for_game_type(sip->restricted_loadout_flag[from_bank])) {
if (!eval_weapon_flag_for_game_type(sip->allowed_bank_restricted_weapons[from_bank][slot->wep[to_bank]])) {
// going from "from" to "to" is valid (from previous if), but going the other way isn't...
// so return the "to" to the list and just move the "from"
if (!wl_bank_allows_weapon(sip, from_bank, slot->wep[to_bank])) {
// going from "from" to "to" is valid (from previous if), but going the other way isn't...
// so return the "to" to the list and just move the "from"
// put to_bank back into list
(*Wl_pool)[slot->wep[to_bank]] += slot->wep_count[to_bank]; // return to list
slot->wep[to_bank] = -1; // remove from slot
slot->wep_count[to_bank] = 0;
*sound=InterfaceSounds::ICON_DROP; // unless it changes later
forced_update = 1; // because we can't return right away
}
// put to_bank back into list
(*Wl_pool)[slot->wep[to_bank]] += slot->wep_count[to_bank]; // return to list
slot->wep[to_bank] = -1; // remove from slot
slot->wep_count[to_bank] = 0;
*sound=InterfaceSounds::ICON_DROP; // unless it changes later
forced_update = 1; // because we can't return right away
}
}
@@ -3839,19 +3851,17 @@ int wl_grab_from_list(int from_list, int to_bank, int ship_slot, interface_snd_i
ship_info *sip = &Ship_info[slot->ship_class];
// ensure that this bank will accept the weapon...
if (eval_weapon_flag_for_game_type(sip->restricted_loadout_flag[to_bank])) {
if (!eval_weapon_flag_for_game_type(sip->allowed_bank_restricted_weapons[to_bank][from_list])) {
SCP_string txt;
sprintf(txt, XSTR("This bank is unable to carry %s weaponry", 1628), Weapon_info[from_list].get_display_name());
if (!wl_bank_allows_weapon(sip, to_bank, from_list)) {
SCP_string txt;
sprintf(txt, XSTR("This bank is unable to carry %s weaponry", 1628), Weapon_info[from_list].get_display_name());
if ( !(Game_mode & GM_MULTIPLAYER) || (Netgame.host == pl) ) {
popup(PF_USE_AFFIRMATIVE_ICON, 1, POPUP_OK, txt.c_str());
} else if (pl != NULL) {
send_game_chat_packet(Netgame.host, txt.c_str(), MULTI_MSG_TARGET, pl, nullptr, 1);
}
return 0;
if ( !(Game_mode & GM_MULTIPLAYER) || (Netgame.host == pl) ) {
popup(PF_USE_AFFIRMATIVE_ICON, 1, POPUP_OK, txt.c_str());
} else if (pl != nullptr) {
send_game_chat_packet(Netgame.host, txt.c_str(), MULTI_MSG_TARGET, pl, nullptr, 1);
}
return 0;
}
// we will have some sort of success from this point
@@ -3917,19 +3927,17 @@ int wl_swap_list_slot(int from_list, int to_bank, int ship_slot, interface_snd_i
ship_info *sip = &Ship_info[slot->ship_class];
// ensure that this bank will accept the weapon
if (eval_weapon_flag_for_game_type(sip->restricted_loadout_flag[to_bank])) {
if (!eval_weapon_flag_for_game_type(sip->allowed_bank_restricted_weapons[to_bank][from_list])) {
SCP_string txt;
sprintf(txt, XSTR("This bank is unable to carry %s weaponry", 1628), Weapon_info[from_list].get_display_name());
if (!wl_bank_allows_weapon(sip, to_bank, from_list)) {
SCP_string txt;
sprintf(txt, XSTR("This bank is unable to carry %s weaponry", 1628), Weapon_info[from_list].get_display_name());
if ( !(Game_mode & GM_MULTIPLAYER) || (Netgame.host == pl) ) {
popup(PF_USE_AFFIRMATIVE_ICON, 1, POPUP_OK, txt.c_str());
} else if (pl != NULL) {
send_game_chat_packet(Netgame.host, txt.c_str(), MULTI_MSG_TARGET, pl, nullptr, 1);
}
return 0;
if ( !(Game_mode & GM_MULTIPLAYER) || (Netgame.host == pl) ) {
popup(PF_USE_AFFIRMATIVE_ICON, 1, POPUP_OK, txt.c_str());
} else if (pl != nullptr) {
send_game_chat_packet(Netgame.host, txt.c_str(), MULTI_MSG_TARGET, pl, nullptr, 1);
}
return 0;
}
// dump slot weapon back into list
+6
View File
@@ -853,6 +853,12 @@ void parse_mod_table(const char *filename)
}
}
// Per-cascade shadow map filter radius, in shadow map UV units, for a
// Sol-sized sun. It is no longer the final width: shadow_cascade_params_bind()
// scales these by the sun's apparent size relative to Sol, so that the same
// $SunAngularSize: drives both shadow-mapped and raytraced softness. A mod that
// tuned these against retail/MediaVPs sun art keeps the look it tuned for --
// see shadow_smoothness_scale() in shadows.cpp for why Sol is the reference.
if (optional_string("$Shadow Smoothness Factor:")) {
SCP_vector<float> smoothness;
stuff_float_list(smoothness);
+2 -6
View File
@@ -440,7 +440,6 @@ namespace animation {
// rotation -----
submodel->canonical_prev_orient = submodel->canonical_orient;
submodel->canonical_orient = data.orientation;
matrix delta;
@@ -453,7 +452,6 @@ namespace animation {
// translation -----
submodel->canonical_prev_offset = submodel->canonical_offset;
submodel->canonical_offset = data.position;
vec3d delta_vec;
@@ -471,7 +469,6 @@ namespace animation {
if (!submodel)
return;
submodel->canonical_prev_orient = submodel->canonical_orient;
submodel->current_turn_rate = 0.0f;
}
@@ -485,7 +482,8 @@ namespace animation {
if(!submodel.second->flags[Model::Submodel_flags::Can_move]){
mprintf(("Submodel %s of model %s is animated and has movement enabled.\n", submodel.second->name, model_get(pmi->model_num)->filename));
submodel.second->flags.set(Model::Submodel_flags::Can_move);
auto pm = model_get(pmi->model_num);
model_set_submodel_can_move(pm, static_cast<int>(submodel.second - pm->submodel.get()));
if (submodel.second->rotation_type == MOVEMENT_TYPE_NONE) {
submodel.second->rotation_type = MOVEMENT_TYPE_TRIGGERED;
@@ -618,7 +616,6 @@ namespace animation {
// rotation -----
submodel->canonical_prev_orient = submodel->canonical_orient;
submodel->canonical_orient = data.orientation;
submodel->rotation_axis = sm->rotation_axis;
@@ -631,7 +628,6 @@ namespace animation {
// translation -----
submodel->canonical_prev_offset = submodel->canonical_offset;
submodel->canonical_offset = data.position;
submodel->translation_axis = sm->translation_axis;
+11 -19
View File
@@ -121,7 +121,6 @@ struct model_electrical_arc : electrical_arc
struct submodel_instance
{
float cur_angle = 0.0f; // The current angle this thing is turned to.
float prev_angle = 0.0f;
float turret_idle_angle = 0.0f; // If this is a turret, this is the expected idling angle of the submodel
float current_turn_rate = 0.0f;
@@ -130,7 +129,6 @@ struct submodel_instance
TIMESTAMP stepped_rotation_started;
float cur_offset = 0.0f;
float prev_offset = 0.0f;
float current_shift_rate = 0.0f;
float desired_shift_rate = 0.0f;
@@ -140,8 +138,10 @@ struct submodel_instance
bool blown_off = false; // If set, this subobject is not rendered or used for collision
// These fields are the true standard reference for submodel rotation. They should seldom be read directly
// and should almost never be written directly. In most cases, coders should prefer cur_angle and prev_angle.
// and should almost never be written directly. In most cases, coders should prefer cur_angle.
matrix canonical_orient = vmd_identity_matrix;
// The pose at the end of last frame; written only by model_instance_snapshot_prev_all (for Can_move submodels),
// by direct pose setters (for others, which the snapshot skips) and copies/resets of a whole pose, never by code that moves the submodel
matrix canonical_prev_orient = vmd_identity_matrix;
// similarly for translation
vec3d canonical_offset = vmd_zero_vector;
@@ -1072,25 +1072,23 @@ model_memory_stats model_get_memory_stats();
int num_model_instances();
polymodel_instance* model_get_instance(int model_instance_num);
// Records every instance's movable submodel poses as the previous frame's. This is the only per-frame writer of the
// canonical_prev_* members; anything that moves a submodel only writes the current pose.
void model_instance_snapshot_prev_all();
// Flags a submodel, and with it all its children, as Can_move
void model_set_submodel_can_move(polymodel* pm, int submodel_num);
// routine to copy subsystems. Must be called when subsystems sets are the same -- see ship.cpp
void model_copy_subsystems(int n_subsystems, model_subsystem *d_sp, model_subsystem *s_sp);
// If MR_FLAG_OUTLINE bit set this color will be used for outlines.
// This defaults to black.
void model_set_outline_color(int r, int g, int b);
// IF MR_LOCK_DETAIL is set, then it will always draw detail level 'n'
// This defaults to 0. (0=highest, larger=lower)
void model_set_detail_level(int n);
// Flags you can pass to model_render
#define MR_NORMAL (0) // Draw a normal object
#define MR_SHOW_OUTLINE (1<<0) // Draw the object in outline mode. Color specified by model_set_outline_color
#define MR_SHOW_OUTLINE (1<<0) // Draw the object in outline mode. Color specified by model_render_params::set_color
#define MR_SKYBOX (1<<1) // Draw as a skybox
#define MR_DESATURATED (1<<2) // Draw model in monochrome using outline color
#define MR_STENCIL_WRITE (1<<3) // Write stencil buffere where the model was rendered
#define MR_STENCIL_READ (1<<4) // Only draw pixels of the model where the stencil buffer has the right value (see MR_STENCIL_WRITE)
#define MR_SHOW_THRUSTERS (1<<5) // Show the engine thrusters. See model_set_thrust for how long it draws.
#define MR_SHOW_THRUSTERS (1<<5) // Show the engine thrusters. See model_render_params::set_thruster_info for how long it draws.
#define MR_NO_COLOR_WRITES (1<<6) // Don't write anything to the color buffers (used when setting the stencil buffer)
#define MR_NO_POLYS (1<<7) // Don't draw the polygons.
#define MR_NO_LIGHTING (1<<8) // Don't perform any lighting on the model.
@@ -1181,7 +1179,6 @@ bool submodel_is_destroyed_form(const char *name);
extern int modelstats_num_polys;
extern int modelstats_num_polys_drawn;
extern int modelstats_num_verts;
extern int modelstats_num_sortnorms;
#endif
// Tries to move joints so that the turret points to the point dst.
@@ -1471,10 +1468,6 @@ typedef struct mst_info {
{}
} mst_info;
// scale the engines thrusters by this much
// Only enabled if MR_SHOW_THRUSTERS is on
void model_set_thrust(int model_num = -1, mst_info *mst = NULL);
//=======================================================================================
// Finds the closest point on a model to a point in space. Actually only finds a point
@@ -1535,7 +1528,6 @@ void model_draw_dock_points_htl(int model_num);
bool model_interp_config_buffer(indexed_vertex_source *vert_src, vertex_buffer *vb, bool update_ibuffer_only);
bool model_interp_pack_buffer(indexed_vertex_source *vert_src, vertex_buffer *vb);
void model_interp_submit_buffers(indexed_vertex_source *vert_src, size_t vertex_stride);
void model_allocate_interp_data(uint n_verts = 0, uint n_norms = 0);
void glowpoint_init();
SCP_vector<glow_point_bank_override>::iterator get_glowpoint_bank_override_by_name(const char* name);
Loaded 100 of 170 files, more files were not shown because too many files have changed in this diff. Show more