mirror of
https://github.com/scp-fs2open/fs2open.github.com.git
synced 2026-11-07 22:56:30 -10:00
Compare commits
41
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
718ee3c038 | ||
|
|
6af6a0cc99 | ||
|
|
f444d1ecf7 | ||
|
|
26d10bfb35 | ||
|
|
b8dc89aed5 | ||
|
|
60170f1af3 | ||
|
|
803b573fea | ||
|
|
bd5b6459b2 | ||
|
|
00b01e3615 | ||
|
|
76286c5332 | ||
|
|
e4546de24e | ||
|
|
336f198594 | ||
|
|
5c7059b451 | ||
|
|
4c0ff8414d | ||
|
|
019c4bacea | ||
|
|
e7d58d85cb | ||
|
|
84fa0a622f | ||
|
|
c5b761ca5d | ||
|
|
9443c4abe0 | ||
|
|
5e745f7535 | ||
|
|
c165b82996 | ||
|
|
4f3a0d4cff | ||
|
|
8266392f00 | ||
|
|
68166ed419 | ||
|
|
fa4b755b94 | ||
|
|
b5bcfeb8d1 | ||
|
|
b1c90ea5c5 | ||
|
|
dd4a08925c | ||
|
|
647554e182 | ||
|
|
57c28798a7 | ||
|
|
3c456a088c | ||
|
|
cd48b5b500 | ||
|
|
f1812e212d | ||
|
|
817f641478 | ||
|
|
eec4a9ce3e | ||
|
|
b2de895055 | ||
|
|
7d4d61042e | ||
|
|
fd77b84c41 | ||
|
|
2166d253a8 | ||
|
|
f5b29e721b | ||
|
|
9763c7a312 |
No files matched your search
@@ -77,6 +77,7 @@ TARGET_LINK_LIBRARIES(code PUBLIC jansson)
|
||||
target_link_libraries(code PUBLIC anl)
|
||||
|
||||
target_link_libraries(code PUBLIC imgui)
|
||||
target_link_libraries(code PUBLIC implot)
|
||||
|
||||
IF(FSO_BUILD_WITH_OPENXR)
|
||||
target_link_libraries(code PUBLIC OpenXR::openxr_loader)
|
||||
|
||||
@@ -883,6 +883,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, "");
|
||||
|
||||
@@ -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()
|
||||
*
|
||||
|
||||
@@ -2444,10 +2444,9 @@ bool SetCmdlineParams()
|
||||
if (gr_sync_validation_arg.found()) {
|
||||
// Enables the Vulkan validation layer + debug messenger + synchronization
|
||||
// validation feature (see VulkanRenderer::initializeInstance), but NOT the
|
||||
// engine-level graphics debug output: -gr_debug additionally draws debug
|
||||
// overlays (e.g. output_uniform_debug_data), which are unrelated to GPU
|
||||
// sync validation and would otherwise appear as spurious on-screen
|
||||
// artifacts. Keep this a pure GPU-validation switch.
|
||||
// engine-level graphics debug output: -gr_debug additionally feeds extra
|
||||
// stat groups into the ImGui frame profiler overlay (see gr_get_debug_stats),
|
||||
// which are unrelated to GPU sync validation. Keep this a pure GPU-validation switch.
|
||||
Cmdline_gr_sync_validation = true;
|
||||
}
|
||||
|
||||
|
||||
@@ -26,13 +26,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 +42,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;
|
||||
@@ -71,6 +73,10 @@ uniform shadowCascadeParams {
|
||||
int cascade_count;
|
||||
float rtShadowBiasMin;
|
||||
float rtShadowBiasMax;
|
||||
int rtShadowSampleCount;
|
||||
int rtaoSampleCount;
|
||||
float rtaoRadius;
|
||||
float rtaoStrength;
|
||||
|
||||
mat4 shadow_mv_matrix;
|
||||
|
||||
@@ -80,6 +86,7 @@ uniform shadowCascadeParams {
|
||||
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];
|
||||
};
|
||||
|
||||
#ifdef VULKAN
|
||||
@@ -303,6 +310,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 + shadow_ray_view_origin;
|
||||
vec3 worldNormal = normalize((inv_view_matrix * vec4(normal, 0.0)).xyz);
|
||||
float rtaoBias = computeRtShadowBias(length(position), rtShadowBiasMin, rtShadowBiasMax);
|
||||
ao *= traceAmbientOcclusion(shadow_tlas, worldPos, worldNormal,
|
||||
rtaoRadius, rtaoStrength, rtaoSampleCount, rtaoBias, gl_FragCoord.xy);
|
||||
}
|
||||
#endif
|
||||
fragmentColor.rgb = diffuseLightColor * diffColor * ao;
|
||||
|
||||
#ifdef ENV_MAP
|
||||
@@ -330,7 +349,14 @@ void main()
|
||||
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));
|
||||
// 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(shadow_ray_cull_mask), 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 +364,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
|
||||
}
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ void main() {
|
||||
}
|
||||
#else
|
||||
in vec4 vertPosition;
|
||||
in vec4 vertTexCoord;
|
||||
|
||||
out vec2 v_rcpFrame;
|
||||
#ifdef GL_ES
|
||||
@@ -27,6 +28,8 @@ layout (std140) uniform genericData {
|
||||
void main() {
|
||||
gl_Position = vertPosition;
|
||||
v_rcpFrame = vec2(1.0/rt_w, 1.0/rt_h);
|
||||
v_pos = vertPosition.xy*0.5 + 0.5;
|
||||
// Use the real texcoord rather than deriving it from vertPosition: the draw call may ask for a
|
||||
// sub-rectangle of the source texture, which the clip-space formula ignored. Matches post-v.sdr.
|
||||
v_pos = vertTexCoord.xy;
|
||||
}
|
||||
#endif
|
||||
@@ -110,6 +110,10 @@ uniform shadowCascadeParams {
|
||||
int cascade_count;
|
||||
float rtShadowBiasMin;
|
||||
float rtShadowBiasMax;
|
||||
int rtShadowSampleCount;
|
||||
int rtaoSampleCount;
|
||||
float rtaoRadius;
|
||||
float rtaoStrength;
|
||||
|
||||
mat4 shadow_mv_matrix;
|
||||
|
||||
@@ -119,6 +123,7 @@ uniform shadowCascadeParams {
|
||||
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];
|
||||
};
|
||||
|
||||
|
||||
@@ -138,6 +143,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
|
||||
@@ -183,6 +189,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 +274,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 +450,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);
|
||||
|
||||
@@ -125,6 +125,10 @@ uniform shadowCascadeParams {
|
||||
int cascade_count;
|
||||
float rtShadowBiasMin;
|
||||
float rtShadowBiasMax;
|
||||
int rtShadowSampleCount;
|
||||
int rtaoSampleCount;
|
||||
float rtaoRadius;
|
||||
float rtaoStrength;
|
||||
|
||||
mat4 shadow_mv_matrix;
|
||||
|
||||
@@ -134,6 +138,7 @@ uniform shadowCascadeParams {
|
||||
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];
|
||||
};
|
||||
|
||||
#ifdef VULKAN
|
||||
|
||||
@@ -14,11 +14,18 @@ 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;
|
||||
// OpenGL-only: the Vulkan backend has no geometry-shader stage, and disables the
|
||||
// shadow pass outright when it can't write gl_Layer from the vertex shader, so it
|
||||
// never requests GEOMETRY_FALLBACK. Hence no Vulkan set/binding qualifier here.
|
||||
layout(std140) uniform shadowCascadeParams {
|
||||
int cascade_offset;
|
||||
int cascade_count;
|
||||
float rtShadowBiasMin;
|
||||
float rtShadowBiasMax;
|
||||
int rtShadowSampleCount;
|
||||
int rtaoSampleCount;
|
||||
float rtaoRadius;
|
||||
float rtaoStrength;
|
||||
|
||||
mat4 shadow_mv_matrix;
|
||||
|
||||
@@ -28,6 +35,7 @@ layout (std140) uniform shadowCascadeParams {
|
||||
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];
|
||||
};
|
||||
|
||||
in VertexOutput {
|
||||
|
||||
@@ -45,10 +45,14 @@ layout(set = 0, binding = 6, std140)
|
||||
layout(std140)
|
||||
#endif
|
||||
uniform shadowCascadeParams {
|
||||
int cascade_offset;
|
||||
int cascade_count;
|
||||
float rtShadowBiasMin;
|
||||
float rtShadowBiasMax;
|
||||
int cascade_offset;
|
||||
int cascade_count;
|
||||
float rtShadowBiasMin;
|
||||
float rtShadowBiasMax;
|
||||
int rtShadowSampleCount;
|
||||
int rtaoSampleCount;
|
||||
float rtaoRadius;
|
||||
float rtaoStrength;
|
||||
|
||||
mat4 shadow_mv_matrix;
|
||||
|
||||
@@ -58,6 +62,7 @@ uniform shadowCascadeParams {
|
||||
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];
|
||||
};
|
||||
|
||||
#ifdef VULKAN
|
||||
|
||||
@@ -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,7 +167,7 @@ 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/
|
||||
@@ -132,6 +220,141 @@ float traceShadowRay(accelerationStructureEXT tlas, vec3 worldPos, vec3 worldNor
|
||||
|
||||
return (rayQueryGetIntersectionTypeEXT(rq, true) == gl_RayQueryCommittedIntersectionNoneEXT) ? 1.0 : 0.0;
|
||||
}
|
||||
|
||||
// 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). rtShadowSampleCount/
|
||||
// rtaoSampleCount (shadowCascadeParams) 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);
|
||||
|
||||
vec3 tangent, bitangent;
|
||||
rtBuildOnb(direction, 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 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);
|
||||
|
||||
rayQueryEXT rq;
|
||||
rayQueryInitializeEXT(rq, tlas, gl_RayFlagsOpaqueEXT | gl_RayFlagsTerminateOnFirstHitEXT,
|
||||
cullMask, origin, 0.001, sampleDir, tMax);
|
||||
while (rayQueryProceedEXT(rq)) {}
|
||||
|
||||
if (rayQueryGetIntersectionTypeEXT(rq, true) == gl_RayQueryCommittedIntersectionNoneEXT) {
|
||||
visibility += 1.0;
|
||||
}
|
||||
}
|
||||
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, 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,
|
||||
0xFF, 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
|
||||
|
||||
vec4 transformToShadowMap(mat4 shadow_proj_matrix, int i, vec4 pos)
|
||||
|
||||
+143
-16
@@ -22,6 +22,7 @@
|
||||
#include "material.h"
|
||||
#include "matrix.h"
|
||||
|
||||
#include "bmpman/bmpman.h"
|
||||
#include "cmdline/cmdline.h"
|
||||
#include "debugconsole/console.h"
|
||||
#include "executor/global_executors.h"
|
||||
@@ -42,6 +43,7 @@
|
||||
#include "render/3d.h"
|
||||
#include "scripting/hook_api.h"
|
||||
#include "scripting/scripting.h"
|
||||
#include "tracing/ProfilerOverlay.h"
|
||||
#include "tracing/tracing.h"
|
||||
#include "utils/boost/hash_combine.h"
|
||||
#include "utils/string_utils.h"
|
||||
@@ -83,6 +85,7 @@ 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),
|
||||
};
|
||||
|
||||
const size_t gr_capabilities_num = sizeof(gr_capabilities) / sizeof(gr_capabilities[0]);
|
||||
@@ -924,6 +927,29 @@ bool gr_is_smaa_mode(AntiAliasMode mode) {
|
||||
return mode == AntiAliasMode::SMAA_Low || mode == AntiAliasMode::SMAA_Medium || mode == AntiAliasMode::SMAA_High || mode == AntiAliasMode::SMAA_Ultra;
|
||||
}
|
||||
|
||||
SCP_vector<float> gr_get_supported_anisotropy_levels()
|
||||
{
|
||||
float max;
|
||||
if (!gr_get_property(gr_property::MAX_ANISOTROPY, &max)) {
|
||||
return {};
|
||||
}
|
||||
|
||||
if (max <= 2.0f) {
|
||||
return {};
|
||||
}
|
||||
|
||||
SCP_vector<float> out;
|
||||
|
||||
// We assume here that the anisotropy levels are powers of two...
|
||||
float current = 1.0f;
|
||||
while (current <= max) {
|
||||
out.push_back(current);
|
||||
current *= 2.0f;
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
static void parse_post_processing_func()
|
||||
{
|
||||
bool value;
|
||||
@@ -1707,6 +1733,13 @@ void gr_screen_resize(int width, int height)
|
||||
gr_screen.save_max_h_unscaled_zoomed = gr_screen.max_h_unscaled_zoomed;
|
||||
|
||||
gr_setup_viewport();
|
||||
|
||||
// Whatever the backend sized to the old gr_screen is now wrong; let it catch up before anything
|
||||
// renders at the new size. This can discard the frame in progress -- see the warning on the
|
||||
// declaration of this function.
|
||||
if (gr_screen.gf_viewport_size_changed) {
|
||||
gr_screen.gf_viewport_size_changed();
|
||||
}
|
||||
}
|
||||
|
||||
void gr_window_to_render_pos(float& x, float& y)
|
||||
@@ -2240,11 +2273,15 @@ bool gr_init(std::unique_ptr<os::GraphicsOperations>&& graphicsOps, GraphicsAPI
|
||||
center_aspect_ratio = -1.0f;
|
||||
}
|
||||
|
||||
// FRED doesn't support Vulkan yet (see qtfred/README.md for what's needed to change that), so it always
|
||||
// falls back to OpenGL regardless of what was requested. This must happen before gr_init_function_pointers()
|
||||
// below, since that's what binds gr_screen's gf_* dispatch table to the chosen API; doing the override any
|
||||
// Vulkan needs more from the windowing implementation than an OpenGL context does, and not every
|
||||
// implementation can provide it -- the MFC editor can't, and neither can a qtFRED built against a
|
||||
// Qt without Vulkan support or running on a platform plugin we have no surface extension for.
|
||||
// Fall back rather than fail. This must happen before gr_init_function_pointers() below, since
|
||||
// that's what binds gr_screen's gf_* dispatch table to the chosen API; doing the override any
|
||||
// later (e.g. in gr_init_sub()) would leave the dispatch table pointing at the wrong backend.
|
||||
if (Fred_running) {
|
||||
if (mode == GraphicsAPI::Vulkan && (graphicsOps == nullptr || graphicsOps->getVulkanSupport() == nullptr)) {
|
||||
mprintf(("Vulkan was requested but this windowing implementation cannot present through it; "
|
||||
"falling back to OpenGL.\n"));
|
||||
mode = GraphicsAPI::OpenGL;
|
||||
}
|
||||
|
||||
@@ -3263,24 +3300,38 @@ void gr_set_bitmap(int bitmap_num, int alphablend_mode, int bitblt_mode, float a
|
||||
gr_screen.current_bitmap = bitmap_num;
|
||||
}
|
||||
|
||||
static void output_uniform_debug_data()
|
||||
gr_debug_stats gr_get_debug_stats()
|
||||
{
|
||||
if (gr_screen.mode == GraphicsAPI::Stub) {
|
||||
return;
|
||||
gr_debug_stats stats;
|
||||
|
||||
if (!Cmdline_graphics_debug_output) {
|
||||
return stats;
|
||||
}
|
||||
|
||||
int line_height = gr_get_font_height() + 1;
|
||||
if (UniformBufferManager) {
|
||||
stats.uniform_buffer_valid = true;
|
||||
stats.uniform_buffer_size = UniformBufferManager->getBufferSize();
|
||||
stats.uniform_buffer_used = UniformBufferManager->getCurrentlyUsedSize();
|
||||
}
|
||||
|
||||
gr_set_color_fast(&Color_bright_white);
|
||||
gr_screen.gf_get_debug_stats(stats);
|
||||
|
||||
gr_printf_no_resize(gr_screen.center_offset_x + 20, gr_screen.center_offset_y + 160,
|
||||
"Uniform buffer size: " SIZE_T_ARG, UniformBufferManager->getBufferSize());
|
||||
gr_printf_no_resize(gr_screen.center_offset_x + 20, gr_screen.center_offset_y + 160 + line_height,
|
||||
"Currently used data: " SIZE_T_ARG, UniformBufferManager->getCurrentlyUsedSize());
|
||||
return stats;
|
||||
}
|
||||
|
||||
static bool Imgui_frame_active = false;
|
||||
|
||||
void gr_imgui_begin_frame()
|
||||
{
|
||||
if (Imgui_frame_active) {
|
||||
return;
|
||||
}
|
||||
|
||||
// The stub renderer (standalone server) never assigns the ImGui entry points.
|
||||
if (!gr_screen.gf_imgui_new_frame || !ImGui::GetCurrentContext()) {
|
||||
return;
|
||||
}
|
||||
|
||||
gr_imgui_new_frame(); // renderer backend (OpenGL/Vulkan)
|
||||
ImGui_ImplSDL3_NewFrame(); // platform backend, derives the display size from the SDL window
|
||||
|
||||
@@ -3298,6 +3349,23 @@ void gr_imgui_begin_frame()
|
||||
}
|
||||
|
||||
ImGui::NewFrame();
|
||||
Imgui_frame_active = true;
|
||||
}
|
||||
|
||||
void gr_imgui_end_frame()
|
||||
{
|
||||
if (!Imgui_frame_active) {
|
||||
return;
|
||||
}
|
||||
|
||||
ImGui::Render();
|
||||
gr_imgui_render_draw_data();
|
||||
Imgui_frame_active = false;
|
||||
}
|
||||
|
||||
bool gr_imgui_frame_active()
|
||||
{
|
||||
return Imgui_frame_active;
|
||||
}
|
||||
|
||||
void gr_flip(bool execute_scripting)
|
||||
@@ -3318,9 +3386,13 @@ void gr_flip(bool execute_scripting)
|
||||
|
||||
model_process_cached_ui_render_instances();
|
||||
|
||||
if (Cmdline_graphics_debug_output) {
|
||||
output_uniform_debug_data();
|
||||
}
|
||||
// Every presented frame drains the frame profiler and contributes the overlay window to
|
||||
// this frame's ImGui pass. Doing it here rather than per game state is what keeps the
|
||||
// profiler's event buffer bounded: collection is global, so the drain has to be too.
|
||||
tracing::profiler_overlay_frame();
|
||||
|
||||
// Closes whatever ImGui frame the overlay (or the lab, or the options screen) opened.
|
||||
gr_imgui_end_frame();
|
||||
|
||||
// IMPORTANT: No rendering may happen after this point until gf_flip()/gr_setup_frame().
|
||||
// gr_reset_immediate_buffer() resets the write offset to 0, so any subsequent immediate
|
||||
@@ -3404,6 +3476,37 @@ static void uniform_buffer_managers_retire_buffers()
|
||||
UniformBufferManager->onFrameEnd();
|
||||
}
|
||||
|
||||
bool gr_read_render_target(ubyte* out_rgba, int width, int height)
|
||||
{
|
||||
if (out_rgba == nullptr || width <= 0 || height <= 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!gr_screen.gf_read_render_target) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return gr_screen.gf_read_render_target(out_rgba, width, height);
|
||||
}
|
||||
|
||||
void gr_end_offscreen_frame()
|
||||
{
|
||||
if (gr_screen.mode == GraphicsAPI::Stub) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Same two things gr_flip() does for a presented frame, minus the presentation: retire the
|
||||
// uniform segments so the next frame starts writing at offset 0 again, then let the backend
|
||||
// recycle whatever per-frame pools it keeps. Order matters -- the backend rewinding its
|
||||
// allocator while the engine still thinks it is part-way through a segment would just make
|
||||
// the next allocation larger than the last.
|
||||
uniform_buffer_managers_retire_buffers();
|
||||
|
||||
if (gr_screen.gf_end_offscreen_frame) {
|
||||
gr_screen.gf_end_offscreen_frame();
|
||||
}
|
||||
}
|
||||
|
||||
graphics::util::UniformBuffer gr_get_uniform_buffer(uniform_block_type type, size_t num_elements, size_t element_size_override)
|
||||
{
|
||||
return UniformBufferManager->getUniformBuffer(type, num_elements, element_size_override);
|
||||
@@ -3589,6 +3692,30 @@ void gr_heap_deallocate(GpuHeap heap_type, size_t data_offset)
|
||||
gpuHeap->freeGpuData(data_offset);
|
||||
}
|
||||
|
||||
gr_memory_stats gr_get_memory_stats()
|
||||
{
|
||||
gr_memory_stats stats;
|
||||
|
||||
// gpu_heaps[] entries stay null when gpu_heap_init() early-returned (GraphicsAPI::Stub, e.g.
|
||||
// a build with both graphics backends disabled), so this must not assume a live heap.
|
||||
auto vertex_heap = get_gpu_heap(GpuHeap::ModelVertex);
|
||||
auto index_heap = get_gpu_heap(GpuHeap::ModelIndex);
|
||||
if (vertex_heap != nullptr && index_heap != nullptr) {
|
||||
stats.model_heap_valid = true;
|
||||
stats.model_vertex_heap_used = vertex_heap->usedBytes();
|
||||
stats.model_vertex_heap_size = vertex_heap->bufferSize();
|
||||
stats.model_index_heap_used = index_heap->usedBytes();
|
||||
stats.model_index_heap_size = index_heap->bufferSize();
|
||||
}
|
||||
|
||||
stats.locked_bitmap_ram_valid = true;
|
||||
stats.locked_bitmap_ram_bytes = bm_texture_ram;
|
||||
|
||||
gr_screen.gf_get_memory_stats(stats);
|
||||
|
||||
return stats;
|
||||
}
|
||||
|
||||
void gr_set_gamma(float gamma)
|
||||
{
|
||||
if (gr_screen.mode == GraphicsAPI::Stub) {
|
||||
|
||||
+168
-1
@@ -262,6 +262,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)
|
||||
|
||||
@@ -370,7 +371,12 @@ 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
|
||||
};
|
||||
|
||||
struct gr_capability_def {
|
||||
@@ -690,6 +696,56 @@ enum class BufferUsageHint { Static, Dynamic, Streaming, PersistentMapping };
|
||||
*/
|
||||
typedef void* gr_sync;
|
||||
|
||||
/**
|
||||
* @brief Per-backend diagnostic counters, populated only when -gr_debug is active
|
||||
*
|
||||
* Each stat group carries its own "valid" flag since not every backend collects every
|
||||
* group (e.g. per-draw-call counters currently only exist in the Vulkan backend). See
|
||||
* gr_get_debug_stats().
|
||||
*/
|
||||
struct gr_debug_stats {
|
||||
bool uniform_buffer_valid = false;
|
||||
size_t uniform_buffer_size = 0;
|
||||
size_t uniform_buffer_used = 0;
|
||||
|
||||
bool draw_stats_valid = false;
|
||||
int draw_calls = 0;
|
||||
int draw_indexed_calls = 0;
|
||||
int total_vertices = 0;
|
||||
int total_indices = 0;
|
||||
int apply_material_calls = 0;
|
||||
int apply_material_failures = 0;
|
||||
int no_pipeline_skips = 0;
|
||||
int descriptor_sets_allocated = 0;
|
||||
int descriptor_writes = 0;
|
||||
size_t pipeline_count = 0;
|
||||
int on_demand_texture_uploads = 0;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Cross-backend memory usage snapshot for the profiler overlay's memory panel
|
||||
*
|
||||
* Unlike gr_debug_stats, this is always populated when queried (no -gr_debug gate) -- memory
|
||||
* usage is meant to be visible to anyone with the profiler overlay open. Each group still carries
|
||||
* its own "valid" flag since not every backend/build collects every group (e.g. both graphics
|
||||
* backends disabled at build time). See gr_get_memory_stats().
|
||||
*/
|
||||
struct gr_memory_stats {
|
||||
bool model_heap_valid = false;
|
||||
size_t model_vertex_heap_used = 0;
|
||||
size_t model_vertex_heap_size = 0;
|
||||
size_t model_index_heap_used = 0;
|
||||
size_t model_index_heap_size = 0;
|
||||
|
||||
bool locked_bitmap_ram_valid = false;
|
||||
size_t locked_bitmap_ram_bytes = 0;
|
||||
|
||||
bool gpu_purpose_valid = false;
|
||||
size_t gpu_texture_bytes = 0;
|
||||
size_t gpu_geometry_bytes = 0;
|
||||
size_t gpu_render_target_bytes = 0;
|
||||
};
|
||||
|
||||
typedef struct screen {
|
||||
int max_w = 0, max_h = 0; // Width and height
|
||||
int max_w_unscaled = 0, max_h_unscaled = 0;
|
||||
@@ -761,6 +817,14 @@ typedef struct screen {
|
||||
// dumps the current screen to a html blob string
|
||||
std::function<SCP_string()> gf_blob_screen;
|
||||
|
||||
// reads the currently bound render target back into a caller-provided RGBA8 buffer.
|
||||
// Optional: backends that can't read a render target back leave this unset.
|
||||
std::function<bool(ubyte* out_rgba, int width, int height)> gf_read_render_target;
|
||||
|
||||
// recycles per-frame backend state after an off-screen render that never reaches gr_flip().
|
||||
// Optional: backends that keep no per-frame pools leave this unset.
|
||||
std::function<void()> gf_end_offscreen_frame;
|
||||
|
||||
// transforms and dumps the current environment map to a file
|
||||
std::function<void(const char* filename)> gf_dump_envmap;
|
||||
|
||||
@@ -861,6 +925,16 @@ typedef struct screen {
|
||||
std::function<void()> gf_scene_texture_end;
|
||||
std::function<void()> gf_copy_effect_texture;
|
||||
|
||||
// The viewport is now gr_screen.max_w x max_h; bring whatever the backend sized to the old one
|
||||
// into line. Called from gr_screen_resize(); see the precondition documented there, which is
|
||||
// stricter than it looks -- what a backend does here can include throwing away the frame in
|
||||
// progress. Optional: a backend with nothing sized to the viewport leaves it unset.
|
||||
//
|
||||
// OpenGL grows the scene/post-processing render targets. Vulkan rebuilds the swap chain and
|
||||
// everything sized to it, and restarts the frame; that is the only point at which it can notice
|
||||
// the window and the swap chain have diverged (see VulkanRenderer::syncToSurfaceExtent()).
|
||||
std::function<void()> gf_viewport_size_changed;
|
||||
|
||||
std::function<void(int zbias)> gf_zbias;
|
||||
|
||||
std::function<void(int)> gf_set_fill_mode;
|
||||
@@ -963,6 +1037,14 @@ typedef struct screen {
|
||||
std::function<void(const char* name)> gf_push_debug_group;
|
||||
std::function<void()> gf_pop_debug_group;
|
||||
|
||||
// Fills in whichever debug_stats groups this backend collects. Defaults to a no-op
|
||||
// so backends without per-draw-call counters (OpenGL, stub) don't have to assign it.
|
||||
std::function<void(gr_debug_stats& stats)> gf_get_debug_stats = [](gr_debug_stats&) {};
|
||||
|
||||
// Fills in whichever memory_stats groups this backend collects (GPU-purpose byte totals).
|
||||
// Defaults to a no-op so backends without per-purpose tagging don't have to assign it.
|
||||
std::function<void(gr_memory_stats& stats)> gf_get_memory_stats = [](gr_memory_stats&) {};
|
||||
|
||||
std::function<int()> gf_create_query_object;
|
||||
std::function<void(int obj, QueryType type)> gf_query_value;
|
||||
std::function<bool(int obj)> gf_query_value_available;
|
||||
@@ -972,6 +1054,12 @@ typedef struct screen {
|
||||
std::unique_ptr<os::Viewport> (*gf_create_viewport)(const os::ViewPortProperties& props);
|
||||
std::function<void(os::Viewport* view)> gf_use_viewport;
|
||||
|
||||
//! Optional. Backends that keep per-viewport GPU resources (Vulkan holds a surface, swap chain
|
||||
//! and everything sized to it) get told here that a viewport is about to be destroyed, while
|
||||
//! the device and the viewport's window are both still alive. Left unset by backends with
|
||||
//! nothing to release.
|
||||
std::function<void(os::Viewport* view)> gf_release_viewport;
|
||||
|
||||
std::function<void(uniform_block_type bind_point, size_t offset, size_t size, gr_buffer_handle buffer)>
|
||||
gf_bind_uniform_buffer;
|
||||
|
||||
@@ -1062,6 +1150,16 @@ extern GraphicsAPI gr_get_configured_render_api();
|
||||
// a real backend, such as GraphicsAPI::Stub.
|
||||
extern const char* gr_render_api_name(GraphicsAPI api);
|
||||
|
||||
/**
|
||||
* @brief Tell the engine the viewport is now @p width x @p height.
|
||||
*
|
||||
* @warning Call this between frames, never once drawing has started. It runs
|
||||
* gf_viewport_size_changed, and what a backend does there is not limited to reallocating: the
|
||||
* Vulkan backend discards the frame in progress and restarts it at the new size, so anything
|
||||
* already recorded into it is lost. OpenGL asserts rather than tear down a framebuffer it is
|
||||
* rendering into. Both are fine at the top of a frame, which is where every caller sits today --
|
||||
* an SDL resize event, or qtFRED's per-frame viewport sync.
|
||||
*/
|
||||
extern void gr_screen_resize(int width, int height);
|
||||
extern int gr_get_resolution_class(int width, int height);
|
||||
|
||||
@@ -1161,6 +1259,53 @@ bool gr_is_screenshot_requested();
|
||||
//#define gr_flip GR_CALL(gr_screen.gf_flip)
|
||||
void gr_flip(bool execute_scripting = true);
|
||||
|
||||
/**
|
||||
* @brief Collects whichever graphics-API debug stats are available for the active backend
|
||||
*
|
||||
* Returns a default-constructed (all-invalid) gr_debug_stats unless -gr_debug is active. Safe
|
||||
* to call every frame regardless of backend or debug flag.
|
||||
*/
|
||||
gr_debug_stats gr_get_debug_stats();
|
||||
|
||||
/**
|
||||
* @brief Collects whichever cross-backend memory usage stats are available
|
||||
*
|
||||
* Unlike gr_get_debug_stats(), this is always populated (no -gr_debug gate) -- memory usage is
|
||||
* meant to be visible whenever the profiler overlay is open. Safe to call every frame regardless
|
||||
* of backend or build configuration; groups a backend/build doesn't support stay invalid/zero.
|
||||
*/
|
||||
gr_memory_stats gr_get_memory_stats();
|
||||
|
||||
/**
|
||||
* @brief Read the currently bound render target back into @p out_rgba.
|
||||
*
|
||||
* For callers that composed into a render target (bm_set_render_target()) and want the pixels
|
||||
* rather than a file or a data URL -- qtFRED's briefing map. gr_blob_screen() reads the same source
|
||||
* but PNG-encodes and base64-wraps it, which is pure overhead when the destination is a bitmap
|
||||
* again.
|
||||
*
|
||||
* @param out_rgba Receives @p width * @p height * 4 bytes, RGBA order, rows top-down. Must be at
|
||||
* least that large.
|
||||
* @param width Expected width of the bound target, in pixels
|
||||
* @param height Expected height of the bound target, in pixels
|
||||
* @return false if no target is bound, if it isn't the size the caller expected, or if the backend
|
||||
* can't read one back at all. @p out_rgba is untouched in that case.
|
||||
*/
|
||||
bool gr_read_render_target(ubyte* out_rgba, int width, int height);
|
||||
|
||||
/**
|
||||
* @brief End a frame's worth of rendering that never reaches gr_flip().
|
||||
*
|
||||
* For off-screen renderers that compose into a render target and read the result back rather than
|
||||
* presenting -- qtFRED's briefing map. gr_flip() is what retires the engine's per-frame uniform
|
||||
* segments and what makes the backend recycle its per-frame pools; a renderer that never calls it
|
||||
* accumulates both for as long as it runs.
|
||||
*
|
||||
* Only call this once the frame's GPU work has actually completed -- after a readback that
|
||||
* host-waits, which is the case for gr_blob_screen() on a bound render target.
|
||||
*/
|
||||
void gr_end_offscreen_frame();
|
||||
|
||||
inline void gr_setup_frame() {
|
||||
gr_screen.gf_setup_frame();
|
||||
}
|
||||
@@ -1318,6 +1463,16 @@ inline void gr_post_process_restore_zbuffer()
|
||||
*/
|
||||
void gr_imgui_begin_frame();
|
||||
|
||||
/**
|
||||
* @brief Renders and submits the open ImGui frame, if any. Called by gr_flip().
|
||||
*/
|
||||
void gr_imgui_end_frame();
|
||||
|
||||
/**
|
||||
* @brief Whether an ImGui frame is currently open for contributions
|
||||
*/
|
||||
bool gr_imgui_frame_active();
|
||||
|
||||
inline void gr_render_primitives(material* material_info,
|
||||
primitive_type prim_type,
|
||||
vertex_layout* layout,
|
||||
@@ -1411,6 +1566,12 @@ inline bool gr_get_property(gr_property property, void* destination)
|
||||
return gr_screen.gf_get_property(property, destination);
|
||||
}
|
||||
|
||||
// Anisotropic filtering levels the current hardware supports: 1.0 (off), then powers of two up to
|
||||
// the reported maximum. Empty if anisotropy is unavailable or the hardware caps out below 4x, in
|
||||
// which case there is nothing meaningful to offer. Backs both the in-game option's enumerator and
|
||||
// qtFRED's Preferences combo, so the two can't drift.
|
||||
SCP_vector<float> gr_get_supported_anisotropy_levels();
|
||||
|
||||
inline void gr_push_debug_group(const char* name)
|
||||
{
|
||||
gr_screen.gf_push_debug_group(name);
|
||||
@@ -1454,6 +1615,12 @@ inline void gr_use_viewport(os::Viewport* view)
|
||||
{
|
||||
gr_screen.gf_use_viewport(view);
|
||||
}
|
||||
inline void gr_release_viewport(os::Viewport* view)
|
||||
{
|
||||
if (gr_screen.gf_release_viewport) {
|
||||
gr_screen.gf_release_viewport(view);
|
||||
}
|
||||
}
|
||||
inline void gr_set_viewport(int x, int y, int width, int height)
|
||||
{
|
||||
gr_screen.gf_set_viewport(x, y, width, height);
|
||||
|
||||
@@ -461,6 +461,42 @@ SCP_string gr_opengl_blob_screen()
|
||||
return "data:image/png;base64," + result;
|
||||
}
|
||||
|
||||
bool gr_opengl_read_render_target(ubyte* out_rgba, int width, int height)
|
||||
{
|
||||
const GLuint render_target = opengl_get_rtt_framebuffer();
|
||||
if (render_target == 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// The caller sized its buffer from the bitmap it bound, so a disagreement means it is reading
|
||||
// something other than what it thinks. Refuse rather than overrun or return a wrong-shaped image.
|
||||
if (width != gr_screen.max_w || height != gr_screen.max_h) {
|
||||
nprintf(("OpenGL", "gr_opengl_read_render_target: caller expected %dx%d but the bound target "
|
||||
"is %dx%d\n", width, height, gr_screen.max_w, gr_screen.max_h));
|
||||
return false;
|
||||
}
|
||||
|
||||
GL_state.PushFramebufferState();
|
||||
GL_state.BindFrameBuffer(render_target, GL_FRAMEBUFFER);
|
||||
glReadBuffer(GL_COLOR_ATTACHMENT0);
|
||||
|
||||
// Row 0 first, which for a render target FSO composed into is the top row -- matching the
|
||||
// top-down order gr_read_render_target() promises. Deliberately not the flip gr_blob_screen()
|
||||
// applies: that one exists to make the PNG come out upright, and there is no PNG here.
|
||||
glReadPixels(0, 0, width, height, GL_RGBA, GL_UNSIGNED_BYTE, out_rgba);
|
||||
glFlush();
|
||||
|
||||
GL_state.PopFramebufferState();
|
||||
|
||||
// Reported, not returned. glGetError drains one global queue, so an entry left by anything
|
||||
// earlier in the frame is not evidence about this readback -- and callers use the return value
|
||||
// to decide whether the frame's work has completed (gr_end_offscreen_frame()). Failing on
|
||||
// somebody else's error would silently skip that.
|
||||
opengl_check_for_errors("gr_opengl_read_render_target");
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void gr_opengl_dump_envmap(const char* filename)
|
||||
{
|
||||
char tmp[MAX_PATH_LEN];
|
||||
@@ -1066,6 +1102,7 @@ void gr_opengl_init_function_pointers()
|
||||
|
||||
gr_screen.gf_print_screen = gr_opengl_print_screen;
|
||||
gr_screen.gf_blob_screen = gr_opengl_blob_screen;
|
||||
gr_screen.gf_read_render_target = gr_opengl_read_render_target;
|
||||
gr_screen.gf_dump_envmap = gr_opengl_dump_envmap;
|
||||
gr_screen.gf_calculate_irrmap = gr_opengl_calculate_irrmap;
|
||||
|
||||
@@ -1126,6 +1163,7 @@ void gr_opengl_init_function_pointers()
|
||||
gr_screen.gf_scene_texture_begin = gr_opengl_scene_texture_begin;
|
||||
gr_screen.gf_scene_texture_end = gr_opengl_scene_texture_end;
|
||||
gr_screen.gf_copy_effect_texture = gr_opengl_copy_effect_texture;
|
||||
gr_screen.gf_viewport_size_changed = gr_opengl_resize_render_targets;
|
||||
|
||||
gr_screen.gf_deferred_lighting_begin = gr_opengl_deferred_lighting_begin;
|
||||
gr_screen.gf_deferred_lighting_msaa = gr_opengl_deferred_lighting_msaa;
|
||||
@@ -1164,6 +1202,7 @@ void gr_opengl_init_function_pointers()
|
||||
|
||||
gr_screen.gf_is_capable = gr_opengl_is_capable;
|
||||
gr_screen.gf_get_property = gr_opengl_get_property;
|
||||
gr_screen.gf_get_memory_stats = gr_opengl_get_memory_stats;
|
||||
|
||||
gr_screen.gf_push_debug_group = gr_opengl_push_debug_group;
|
||||
gr_screen.gf_pop_debug_group = gr_opengl_pop_debug_group;
|
||||
@@ -1515,7 +1554,7 @@ bool gr_opengl_init(std::unique_ptr<os::GraphicsOperations>&& graphicsOps)
|
||||
opengl_shader_init();
|
||||
|
||||
// post processing effects, after shaders are initialized
|
||||
opengl_setup_scene_textures();
|
||||
opengl_setup_scene_textures(gr_screen.max_w, gr_screen.max_h);
|
||||
opengl_post_process_init();
|
||||
|
||||
// must be called after extensions are setup
|
||||
@@ -1630,6 +1669,18 @@ 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
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -86,7 +86,7 @@ void gr_opengl_deferred_lighting_begin(bool clearNonColorBufs)
|
||||
Current_shader->program->Uniforms.setTextureUniform("tex", 0);
|
||||
GL_state.SetAlphaBlendMode(gr_alpha_blend::ALPHA_BLEND_NONE);
|
||||
GL_state.SetZbufferType(ZBUFFER_TYPE_NONE);
|
||||
opengl_draw_full_screen_textured(0, 0, 1, 1);
|
||||
opengl_draw_full_screen_scene_texture();
|
||||
} else {
|
||||
// Copy the existing color data into the emissive part of the G-buffer since everything that already existed is
|
||||
// treated as emissive
|
||||
@@ -159,7 +159,9 @@ void gr_opengl_deferred_lighting_msaa()
|
||||
});
|
||||
GL_state.SetAlphaBlendMode(gr_alpha_blend::ALPHA_BLEND_NONE);
|
||||
GL_state.SetZbufferType(ZBUFFER_TYPE_WRITE);
|
||||
opengl_draw_full_screen_textured(0, 0, 1, 1);
|
||||
// msaa-f.sdr resolves via ivec2(textureSize(texColor) * fragTexCoord), so the texcoords have to
|
||||
// stay inside the rendered sub-rectangle of the multisampled G-buffer.
|
||||
opengl_draw_full_screen_scene_texture();
|
||||
}
|
||||
|
||||
void gr_opengl_deferred_lighting_end()
|
||||
@@ -174,6 +176,11 @@ void gr_opengl_deferred_lighting_end()
|
||||
glDrawBuffer(GL_COLOR_ATTACHMENT0);
|
||||
|
||||
GL_state.ColorMask(true, true, true, false);
|
||||
|
||||
// Do not leave the raw-depth sampler bound after the pass.
|
||||
if (Shadow_map_raw_sampler) {
|
||||
GL_state.Texture.BindSampler(11, 0);
|
||||
}
|
||||
}
|
||||
|
||||
static GLuint deferred_light_cylinder_vbo = 0;
|
||||
@@ -253,6 +260,10 @@ 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(11, GL_TEXTURE_2D_ARRAY, Shadow_map_depth_texture);
|
||||
GL_state.Texture.BindSampler(11, Shadow_map_raw_sampler);
|
||||
}
|
||||
}
|
||||
|
||||
if (ENVMAP > 0) {
|
||||
@@ -319,8 +330,12 @@ void gr_opengl_deferred_lighting_finish()
|
||||
shadow_cascade_params_bind_deferred();
|
||||
}
|
||||
|
||||
header->invScreenWidth = 1.0f / gr_screen.max_w;
|
||||
header->invScreenHeight = 1.0f / gr_screen.max_h;
|
||||
// deferred-f.sdr turns gl_FragCoord into a G-buffer texture coordinate with these, so they
|
||||
// have to normalize against the G-buffer's own dimensions. Those only equal gr_screen while
|
||||
// the viewport exactly fills the scene textures -- not after a shrink, and not when the
|
||||
// allocation was clamped by GL_max_renderbuffer_size.
|
||||
header->invScreenWidth = 1.0f / Scene_texture_width;
|
||||
header->invScreenHeight = 1.0f / Scene_texture_height;
|
||||
header->nearPlane = gr_near_plane;
|
||||
|
||||
{
|
||||
@@ -555,7 +570,8 @@ void gr_opengl_deferred_lighting_finish()
|
||||
data->clip_dist = Neb2_fog_clip_distance;
|
||||
});
|
||||
|
||||
opengl_draw_full_screen_textured(0.0f, 0.0f, 1.0f, 1.0f);
|
||||
// fog-f.sdr samples the composite and depth targets straight off fragTexCoord.
|
||||
opengl_draw_full_screen_scene_texture();
|
||||
|
||||
if (bDrawNebVolumetrics) {
|
||||
glReadBuffer(GL_COLOR_ATTACHMENT0);
|
||||
@@ -651,6 +667,12 @@ void gr_opengl_deferred_lighting_finish()
|
||||
|
||||
{
|
||||
GR_DEBUG_SCOPE("Volumetric Nebulae Draw");
|
||||
// Deliberately unscaled. volumetric-f.sdr uses fragTexCoord for two incompatible
|
||||
// things: reconstructing an eye-space ray direction, which needs the full 0..1 range
|
||||
// across the viewport, and sampling composite/depth/emissive, which needs the
|
||||
// rendered sub-rectangle. Scaling here would fix the sampling and skew every ray.
|
||||
// Separating the two needs a second varying (or a scale uniform) in the shader; until
|
||||
// then volumetrics are only correct while the targets exactly match the viewport.
|
||||
opengl_draw_full_screen_textured(0.0f, 0.0f, 1.0f, 1.0f);
|
||||
}
|
||||
GL_state.Texture.Enable(Scene_emissive_texture);
|
||||
|
||||
@@ -70,6 +70,33 @@ int Scene_texture_height;
|
||||
GLfloat Scene_texture_u_scale = 1.0f;
|
||||
GLfloat Scene_texture_v_scale = 1.0f;
|
||||
|
||||
// Render targets are torn down and rebuilt mid-session by gr_opengl_resize_render_targets(), not
|
||||
// just at shutdown, so deletion has to go through the state cache: the driver is free to hand a
|
||||
// freed name straight back out, and a cache entry still holding that name would make a later
|
||||
// Enable() of the recycled texture a no-op.
|
||||
void opengl_delete_render_texture(GLuint& tex)
|
||||
{
|
||||
if ( !tex ) {
|
||||
return;
|
||||
}
|
||||
|
||||
GL_state.Texture.Delete(tex);
|
||||
glDeleteTextures(1, &tex);
|
||||
tex = 0;
|
||||
}
|
||||
|
||||
// Callers must have bound something else first (the resize path binds 0); the framebuffer cache
|
||||
// has no equivalent of Texture.Delete() to unbind through.
|
||||
void opengl_delete_render_framebuffer(GLuint& fbo)
|
||||
{
|
||||
if ( !fbo ) {
|
||||
return;
|
||||
}
|
||||
|
||||
glDeleteFramebuffers(1, &fbo);
|
||||
fbo = 0;
|
||||
}
|
||||
|
||||
inline GLenum opengl_primitive_type(primitive_type prim_type)
|
||||
{
|
||||
switch ( prim_type ) {
|
||||
@@ -98,7 +125,7 @@ void gr_opengl_sphere(material* material_def, float /*rad*/)
|
||||
}
|
||||
|
||||
extern int opengl_check_framebuffer();
|
||||
void opengl_setup_scene_textures()
|
||||
void opengl_setup_scene_textures(int width, int height)
|
||||
{
|
||||
Scene_texture_initialized = 0;
|
||||
|
||||
@@ -113,10 +140,10 @@ void opengl_setup_scene_textures()
|
||||
return;
|
||||
}
|
||||
|
||||
// clamp size, if needed
|
||||
Scene_texture_width = gr_screen.max_w;
|
||||
Scene_texture_height = gr_screen.max_h;
|
||||
Scene_texture_width = width;
|
||||
Scene_texture_height = height;
|
||||
|
||||
// clamp size, if needed
|
||||
if ( Scene_texture_width > GL_max_renderbuffer_size ) {
|
||||
Scene_texture_width = GL_max_renderbuffer_size;
|
||||
}
|
||||
@@ -125,6 +152,13 @@ void opengl_setup_scene_textures()
|
||||
Scene_texture_height = GL_max_renderbuffer_size;
|
||||
}
|
||||
|
||||
mprintf((" Scene textures: %dx%d (screen %dx%d, max renderbuffer %d)\n",
|
||||
Scene_texture_width,
|
||||
Scene_texture_height,
|
||||
gr_screen.max_w,
|
||||
gr_screen.max_h,
|
||||
GL_max_renderbuffer_size));
|
||||
|
||||
// create framebuffer
|
||||
glGenFramebuffers(1, &Scene_framebuffer);
|
||||
GL_state.BindFrameBuffer(Scene_framebuffer);
|
||||
@@ -333,32 +367,15 @@ void opengl_setup_scene_textures()
|
||||
|
||||
if ( opengl_check_framebuffer() ) {
|
||||
GL_state.BindFrameBuffer(0);
|
||||
glDeleteFramebuffers(1, &Scene_framebuffer);
|
||||
Scene_framebuffer = 0;
|
||||
opengl_delete_render_framebuffer(Scene_framebuffer);
|
||||
|
||||
glDeleteTextures(1, &Scene_color_texture);
|
||||
Scene_color_texture = 0;
|
||||
|
||||
glDeleteTextures(1, &Scene_position_texture);
|
||||
Scene_position_texture = 0;
|
||||
|
||||
glDeleteTextures(1, &Scene_normal_texture);
|
||||
Scene_normal_texture = 0;
|
||||
|
||||
glDeleteTextures(1, &Scene_specular_texture);
|
||||
Scene_specular_texture = 0;
|
||||
|
||||
glDeleteTextures(1, &Scene_emissive_texture);
|
||||
Scene_emissive_texture = 0;
|
||||
|
||||
glDeleteTextures(1, &Scene_depth_texture);
|
||||
Scene_depth_texture = 0;
|
||||
|
||||
glDeleteTextures(1, &Scene_luminance_texture);
|
||||
Scene_luminance_texture = 0;
|
||||
|
||||
//glDeleteTextures(1, &Scene_fxaa_output_texture);
|
||||
//Scene_fxaa_output_texture = 0;
|
||||
opengl_delete_render_texture(Scene_color_texture);
|
||||
opengl_delete_render_texture(Scene_position_texture);
|
||||
opengl_delete_render_texture(Scene_normal_texture);
|
||||
opengl_delete_render_texture(Scene_specular_texture);
|
||||
opengl_delete_render_texture(Scene_emissive_texture);
|
||||
opengl_delete_render_texture(Scene_depth_texture);
|
||||
opengl_delete_render_texture(Scene_luminance_texture);
|
||||
|
||||
Gr_post_processing_enabled = false;
|
||||
Gr_enable_soft_particles = false;
|
||||
@@ -687,79 +704,165 @@ void opengl_scene_texture_shutdown()
|
||||
return;
|
||||
}
|
||||
|
||||
if ( Scene_color_texture ) {
|
||||
glDeleteTextures(1, &Scene_color_texture);
|
||||
Scene_color_texture = 0;
|
||||
}
|
||||
// Everything opengl_setup_scene_textures() generated, in the same order. Note that
|
||||
// GammaBlit_texture is 0 when the gamma pass is aliasing Scene_ldr_texture, so the shared
|
||||
// texture is only released once.
|
||||
opengl_delete_render_texture(Scene_color_texture);
|
||||
opengl_delete_render_texture(Scene_ldr_texture);
|
||||
opengl_delete_render_texture(Scene_position_texture);
|
||||
opengl_delete_render_texture(Scene_normal_texture);
|
||||
opengl_delete_render_texture(Scene_specular_texture);
|
||||
opengl_delete_render_texture(Scene_emissive_texture);
|
||||
opengl_delete_render_texture(Scene_composite_texture);
|
||||
opengl_delete_render_texture(Scene_luminance_texture);
|
||||
opengl_delete_render_texture(Cockpit_depth_texture);
|
||||
opengl_delete_render_texture(Scene_depth_texture);
|
||||
opengl_delete_render_framebuffer(Scene_framebuffer);
|
||||
|
||||
if ( Scene_position_texture ) {
|
||||
glDeleteTextures(1, &Scene_position_texture);
|
||||
Scene_position_texture = 0;
|
||||
}
|
||||
opengl_delete_render_texture(Scene_color_texture_ms);
|
||||
opengl_delete_render_texture(Scene_position_texture_ms);
|
||||
opengl_delete_render_texture(Scene_normal_texture_ms);
|
||||
opengl_delete_render_texture(Scene_specular_texture_ms);
|
||||
opengl_delete_render_texture(Scene_emissive_texture_ms);
|
||||
opengl_delete_render_texture(Scene_depth_texture_ms);
|
||||
opengl_delete_render_framebuffer(Scene_framebuffer_ms);
|
||||
|
||||
if ( Scene_normal_texture ) {
|
||||
glDeleteTextures(1, &Scene_normal_texture);
|
||||
Scene_normal_texture = 0;
|
||||
}
|
||||
opengl_delete_render_texture(Back_texture);
|
||||
opengl_delete_render_texture(Back_depth_texture);
|
||||
opengl_delete_render_framebuffer(Back_framebuffer);
|
||||
|
||||
if ( Scene_specular_texture ) {
|
||||
glDeleteTextures(1, &Scene_specular_texture);
|
||||
Scene_specular_texture = 0;
|
||||
}
|
||||
opengl_delete_render_texture(GammaBlit_texture);
|
||||
opengl_delete_render_framebuffer(GammaBlit_framebuffer);
|
||||
|
||||
if (Scene_emissive_texture) {
|
||||
glDeleteTextures(1, &Scene_emissive_texture);
|
||||
Scene_emissive_texture = 0;
|
||||
}
|
||||
|
||||
if ( Scene_depth_texture ) {
|
||||
glDeleteTextures(1, &Scene_depth_texture);
|
||||
Scene_depth_texture = 0;
|
||||
}
|
||||
|
||||
if ( Scene_framebuffer ) {
|
||||
glDeleteFramebuffers(1, &Scene_framebuffer);
|
||||
Scene_framebuffer = 0;
|
||||
}
|
||||
|
||||
if (Back_texture) {
|
||||
glDeleteTextures(1, &Back_texture);
|
||||
Back_texture = 0;
|
||||
}
|
||||
|
||||
if (Back_depth_texture) {
|
||||
glDeleteTextures(1, &Back_depth_texture);
|
||||
Back_depth_texture = 0;
|
||||
}
|
||||
|
||||
if (Back_framebuffer) {
|
||||
glDeleteFramebuffers(1, &Back_framebuffer);
|
||||
Back_framebuffer = 0;
|
||||
}
|
||||
|
||||
if (GammaBlit_texture) {
|
||||
glDeleteTextures(1, &GammaBlit_texture);
|
||||
GammaBlit_texture = 0;
|
||||
}
|
||||
|
||||
if (GammaBlit_framebuffer) {
|
||||
glDeleteFramebuffers(1, &GammaBlit_framebuffer);
|
||||
GammaBlit_framebuffer = 0;
|
||||
}
|
||||
|
||||
glDeleteTextures(2, Distortion_texture);
|
||||
Distortion_texture[0] = 0;
|
||||
Distortion_texture[1] = 0;
|
||||
|
||||
if ( Distortion_framebuffer ) {
|
||||
glDeleteFramebuffers(1, &Distortion_framebuffer);
|
||||
Distortion_framebuffer = 0;
|
||||
}
|
||||
opengl_delete_render_texture(Distortion_texture[0]);
|
||||
opengl_delete_render_texture(Distortion_texture[1]);
|
||||
opengl_delete_render_framebuffer(Distortion_framebuffer);
|
||||
|
||||
Scene_texture_initialized = 0;
|
||||
Scene_framebuffer_in_frame = false;
|
||||
}
|
||||
|
||||
void gr_opengl_resize_render_targets()
|
||||
{
|
||||
// Nothing allocated yet (still inside gr_init()), or FBOs are unavailable entirely.
|
||||
if ( !Scene_texture_initialized ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Grow only. Shrinking back would mean reallocating every G-buffer again the moment the window
|
||||
// grew back, and the shrunk state is already handled correctly: Scene_texture_u_scale and
|
||||
// _v_scale confine rendering to the sub-rectangle actually in use. The hardware limit is
|
||||
// applied here rather than left to opengl_setup_scene_textures(), so that a viewport larger
|
||||
// than anything the GPU can allocate compares equal below and stops asking.
|
||||
const int new_width = MIN(MAX(gr_screen.max_w, Scene_texture_width), GL_max_renderbuffer_size);
|
||||
const int new_height = MIN(MAX(gr_screen.max_h, Scene_texture_height), GL_max_renderbuffer_size);
|
||||
|
||||
// The overwhelmingly common case: qtFred calls gr_screen_resize() every frame and the game
|
||||
// calls it on every SDL resize event, almost always at a size the current targets already
|
||||
// cover -- or, past the hardware limit, at one they never will.
|
||||
if ( new_width == Scene_texture_width && new_height == Scene_texture_height ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Tearing down the framebuffer we are currently rendering into would corrupt the frame rather
|
||||
// than fail cleanly, so refuse rather than trying to recover. Callers resize between frames.
|
||||
// Scene_framebuffer_in_frame covers the post-processing passes too: they only ever run inside
|
||||
// gr_scene_texture_begin()/end(), so it is set for the whole of Post_in_frame as well.
|
||||
if ( Scene_framebuffer_in_frame ) {
|
||||
Assertion(false, "Tried to resize the render targets to %dx%d while a scene was being "
|
||||
"rendered into them! The resize has been skipped; the frame will be stretched.",
|
||||
new_width, new_height);
|
||||
return;
|
||||
}
|
||||
|
||||
mprintf(("Growing render targets from %dx%d to %dx%d to cover the new %dx%d viewport.\n",
|
||||
Scene_texture_width, Scene_texture_height, new_width, new_height,
|
||||
gr_screen.max_w, gr_screen.max_h));
|
||||
|
||||
// Leave the framebuffer cache pointing at a name that cannot be deleted out from under it.
|
||||
GL_state.BindFrameBufferBoth(0, 0);
|
||||
|
||||
// Only the size-dependent resources are touched. The post-processing table, the compiled
|
||||
// shaders and the SMAA lookup textures are all resolution-independent and stay alive, which is
|
||||
// what keeps this cheap enough to run off a window drag. The post-processing targets are
|
||||
// rebuilt after the scene textures because they are sized to match them.
|
||||
opengl_scene_texture_shutdown();
|
||||
opengl_setup_scene_textures(new_width, new_height);
|
||||
|
||||
// Reallocating larger is exactly when running out of video memory is most likely, and
|
||||
// opengl_setup_scene_textures() reports that by leaving the scene uninitialized (having
|
||||
// already turned post-processing and soft particles off). Rebuilding the post-processing
|
||||
// targets on top of scene textures that don't exist would only make it worse, so stop here;
|
||||
// the renderer keeps drawing without the offscreen pipeline.
|
||||
if ( !Scene_texture_initialized ) {
|
||||
mprintf(("Failed to allocate %dx%d render targets! The offscreen rendering pipeline has "
|
||||
"been disabled for the rest of this session.\n", new_width, new_height));
|
||||
return;
|
||||
}
|
||||
|
||||
opengl_post_resize_render_targets();
|
||||
}
|
||||
|
||||
size_t opengl_get_scene_render_target_bytes()
|
||||
{
|
||||
// The bytes for each pixel come from the internal formats that opengl_setup_scene_textures()
|
||||
// gives to these textures: GL_RGBA16F holds 8 bytes, GL_RGBA8 and GL_DEPTH24_STENCIL8 hold 4.
|
||||
// Keep the values below equal to the formats at the creation sites in that function.
|
||||
constexpr size_t RGBA16F_BYTES = 8;
|
||||
constexpr size_t RGBA8_BYTES = 4;
|
||||
constexpr size_t DEPTH24_STENCIL8_BYTES = 4;
|
||||
|
||||
size_t bytes = 0;
|
||||
|
||||
auto add_texture = [&bytes](GLuint texture_id, size_t width, size_t height, size_t bytes_per_pixel,
|
||||
size_t samples = 1) {
|
||||
if (texture_id == 0) {
|
||||
return;
|
||||
}
|
||||
bytes += width * height * bytes_per_pixel * samples;
|
||||
};
|
||||
|
||||
const auto scene_w = static_cast<size_t>(Scene_texture_width);
|
||||
const auto scene_h = static_cast<size_t>(Scene_texture_height);
|
||||
|
||||
add_texture(Scene_color_texture, scene_w, scene_h, RGBA16F_BYTES);
|
||||
add_texture(Scene_ldr_texture, scene_w, scene_h, RGBA8_BYTES);
|
||||
add_texture(Scene_position_texture, scene_w, scene_h, RGBA16F_BYTES);
|
||||
add_texture(Scene_normal_texture, scene_w, scene_h, RGBA16F_BYTES);
|
||||
add_texture(Scene_specular_texture, scene_w, scene_h, RGBA8_BYTES);
|
||||
add_texture(Scene_emissive_texture, scene_w, scene_h, RGBA16F_BYTES);
|
||||
add_texture(Scene_composite_texture, scene_w, scene_h, RGBA16F_BYTES);
|
||||
add_texture(Scene_luminance_texture, scene_w, scene_h, RGBA16F_BYTES);
|
||||
add_texture(Cockpit_depth_texture, scene_w, scene_h, DEPTH24_STENCIL8_BYTES);
|
||||
add_texture(Scene_depth_texture, scene_w, scene_h, DEPTH24_STENCIL8_BYTES);
|
||||
|
||||
// The multisample textures hold one value for each sample, thus their bytes go up with the
|
||||
// MSAA level. Cmdline_msaa_enabled holds the number of samples and is 0 when MSAA is off.
|
||||
const auto samples = static_cast<size_t>(std::max(1, Cmdline_msaa_enabled));
|
||||
|
||||
add_texture(Scene_color_texture_ms, scene_w, scene_h, RGBA16F_BYTES, samples);
|
||||
add_texture(Scene_position_texture_ms, scene_w, scene_h, RGBA16F_BYTES, samples);
|
||||
add_texture(Scene_normal_texture_ms, scene_w, scene_h, RGBA16F_BYTES, samples);
|
||||
add_texture(Scene_specular_texture_ms, scene_w, scene_h, RGBA8_BYTES, samples);
|
||||
add_texture(Scene_emissive_texture_ms, scene_w, scene_h, RGBA16F_BYTES, samples);
|
||||
add_texture(Scene_depth_texture_ms, scene_w, scene_h, DEPTH24_STENCIL8_BYTES, samples);
|
||||
|
||||
// The back buffer and gamma blit textures use the size of the screen, not the (clamped) size
|
||||
// of the scene textures.
|
||||
const auto screen_w = static_cast<size_t>(gr_screen.max_w);
|
||||
const auto screen_h = static_cast<size_t>(gr_screen.max_h);
|
||||
|
||||
add_texture(Back_texture, screen_w, screen_h, RGBA16F_BYTES);
|
||||
add_texture(Back_depth_texture, screen_w, screen_h, DEPTH24_STENCIL8_BYTES);
|
||||
add_texture(GammaBlit_texture, screen_w, screen_h, RGBA8_BYTES);
|
||||
|
||||
// The two distortion textures always have the size 32x32.
|
||||
add_texture(Distortion_texture[0], 32, 32, RGBA8_BYTES);
|
||||
add_texture(Distortion_texture[1], 32, 32, RGBA8_BYTES);
|
||||
|
||||
return bytes;
|
||||
}
|
||||
|
||||
void gr_opengl_scene_texture_begin()
|
||||
{
|
||||
if ( !Scene_texture_initialized ) {
|
||||
@@ -800,6 +903,30 @@ void gr_opengl_scene_texture_begin()
|
||||
Scene_texture_v_scale = 1.0f;
|
||||
}
|
||||
|
||||
// A ratio above 1.0 means the viewport outgrew the allocation after the fact -- the scene
|
||||
// texture can't be resized, so all we can do is render the part that fits and let the blit
|
||||
// stretch it back out, which misaligns everything by a different amount on each axis. The
|
||||
// floor above is meant to make this unreachable; if it does happen (a display hotplug or
|
||||
// resolution change after gr_init(), or an allocation clamped by GL_max_renderbuffer_size on
|
||||
// low-end hardware) say so once rather than every frame.
|
||||
if (Scene_texture_u_scale > 1.0f || Scene_texture_v_scale > 1.0f) {
|
||||
static bool reported_undersized_scene_texture = false;
|
||||
|
||||
if (!reported_undersized_scene_texture) {
|
||||
reported_undersized_scene_texture = true;
|
||||
nprintf(("OpenGL",
|
||||
"Viewport (%dx%d) is larger than the scene texture backing it (%dx%d); "
|
||||
"the post-processed image will be stretched to fit.\n",
|
||||
gr_screen.max_w,
|
||||
gr_screen.max_h,
|
||||
Scene_texture_width,
|
||||
Scene_texture_height));
|
||||
}
|
||||
}
|
||||
|
||||
CLAMP(Scene_texture_u_scale, 0.0f, 1.0f);
|
||||
CLAMP(Scene_texture_v_scale, 0.0f, 1.0f);
|
||||
|
||||
if (!light_deferred_enabled()) {
|
||||
glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
|
||||
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
|
||||
@@ -1246,6 +1373,11 @@ void opengl_draw_full_screen_textured(GLfloat u1, GLfloat v1, GLfloat u2, GLfloa
|
||||
opengl_render_primitives_immediate(PRIM_TYPE_TRIS, &vert_def, 3, glVertices, sizeof(glVertices));
|
||||
}
|
||||
|
||||
void opengl_draw_full_screen_scene_texture()
|
||||
{
|
||||
opengl_draw_full_screen_textured(0.0f, 0.0f, Scene_texture_u_scale, Scene_texture_v_scale);
|
||||
}
|
||||
|
||||
void gr_opengl_render_decals(decal_material* material_info,
|
||||
primitive_type prim_type,
|
||||
vertex_layout* layout,
|
||||
|
||||
@@ -55,8 +55,23 @@ void gr_opengl_render_shield_impact(shield_material* material_info,
|
||||
gr_buffer_handle buffer_handle,
|
||||
int n_verts);
|
||||
|
||||
void opengl_setup_scene_textures();
|
||||
void opengl_setup_scene_textures(int width, int height);
|
||||
void opengl_scene_texture_shutdown();
|
||||
void gr_opengl_resize_render_targets();
|
||||
|
||||
// Release a render target, keeping the GL state cache in sync. See the definitions for why the
|
||||
// cache matters now that these are rebuilt mid-session.
|
||||
void opengl_delete_render_texture(GLuint& tex);
|
||||
void opengl_delete_render_framebuffer(GLuint& fbo);
|
||||
|
||||
/**
|
||||
* @brief Gives the bytes that the scene, backbuffer, and distortion textures hold
|
||||
*
|
||||
* The profiler overlay's memory panel adds this value to the "render targets" group. The function
|
||||
* counts only the textures that exist at the moment of the call, thus it gives 0 before
|
||||
* opengl_setup_scene_textures() runs and after opengl_scene_texture_shutdown() runs.
|
||||
*/
|
||||
size_t opengl_get_scene_render_target_bytes();
|
||||
void gr_opengl_scene_texture_begin();
|
||||
void gr_opengl_scene_texture_end();
|
||||
void gr_opengl_copy_effect_texture();
|
||||
@@ -147,6 +162,12 @@ void opengl_draw_textured_quad(GLfloat x1,
|
||||
*/
|
||||
void opengl_draw_full_screen_textured(GLfloat u1, GLfloat v1, GLfloat u2, GLfloat v2);
|
||||
|
||||
// Fullscreen pass over a source that is one of the scene/post-processing textures. Those are only
|
||||
// filled out to Scene_texture_u_scale/v_scale of their allocation, so sampling them over the full
|
||||
// [0,1] range would pull in whatever is beyond the rendered region. Prefer this over passing
|
||||
// literal 1.0f extents whenever the bound texture came from that pipeline.
|
||||
void opengl_draw_full_screen_scene_texture();
|
||||
|
||||
inline GLenum opengl_primitive_type(primitive_type prim_type);
|
||||
|
||||
void gr_opengl_start_decal_pass();
|
||||
|
||||
@@ -28,6 +28,9 @@
|
||||
#include "es_compatibility.h"
|
||||
#endif
|
||||
|
||||
static void opengl_post_setup_render_targets();
|
||||
static void opengl_post_shutdown_render_targets();
|
||||
|
||||
extern bool PostProcessing_override;
|
||||
extern int opengl_check_framebuffer();
|
||||
// Needed to track where the FXAA shaders are
|
||||
@@ -99,7 +102,7 @@ void opengl_post_pass_tonemap()
|
||||
|
||||
GL_state.Texture.Enable(0, GL_TEXTURE_2D, Scene_color_texture);
|
||||
|
||||
opengl_draw_full_screen_textured(0.0f, 0.0f, Scene_texture_u_scale, Scene_texture_u_scale);
|
||||
opengl_draw_full_screen_scene_texture();
|
||||
}
|
||||
|
||||
void opengl_post_pass_bloom()
|
||||
@@ -134,7 +137,10 @@ void opengl_post_pass_bloom()
|
||||
|
||||
GL_state.Texture.Enable(0, GL_TEXTURE_2D, Scene_color_texture);
|
||||
|
||||
opengl_draw_full_screen_textured(0.0f, 0.0f, 1.0f, 1.0f);
|
||||
// Reads the scene texture directly rather than an already-cropped intermediate, so it is
|
||||
// the scaled variant. The blur/composite passes below read Bloom_textures, which this pass
|
||||
// fills edge to edge, so those stay unscaled.
|
||||
opengl_draw_full_screen_scene_texture();
|
||||
}
|
||||
// ------ end bright pass ------
|
||||
|
||||
@@ -293,7 +299,7 @@ void opengl_post_pass_fxaa()
|
||||
|
||||
GL_state.Texture.Enable(0, GL_TEXTURE_2D, Scene_ldr_texture);
|
||||
|
||||
opengl_draw_full_screen_textured(0.0f, 0.0f, Scene_texture_u_scale, Scene_texture_u_scale);
|
||||
opengl_draw_full_screen_scene_texture();
|
||||
|
||||
// set and configure post shader ..
|
||||
opengl_shader_set_current(gr_opengl_maybe_create_shader(SDR_TYPE_POST_PROCESS_FXAA, 0));
|
||||
@@ -310,7 +316,7 @@ void opengl_post_pass_fxaa()
|
||||
|
||||
GL_state.Texture.Enable(0, GL_TEXTURE_2D, Scene_luminance_texture);
|
||||
|
||||
opengl_draw_full_screen_textured(0.0f, 0.0f, Scene_texture_u_scale, Scene_texture_u_scale);
|
||||
opengl_draw_full_screen_scene_texture();
|
||||
|
||||
opengl_shader_set_current();
|
||||
}
|
||||
@@ -333,7 +339,7 @@ static void smaa_detect_edges()
|
||||
|
||||
GL_state.Texture.Enable(0, GL_TEXTURE_2D, Scene_ldr_texture);
|
||||
|
||||
opengl_draw_full_screen_textured(0.0f, 0.0f, Scene_texture_u_scale, Scene_texture_u_scale);
|
||||
opengl_draw_full_screen_scene_texture();
|
||||
}
|
||||
|
||||
static void smaa_calculate_blending_weights()
|
||||
@@ -358,7 +364,7 @@ static void smaa_calculate_blending_weights()
|
||||
GL_state.Texture.Enable(1, GL_TEXTURE_2D, Smaa_area_tex);
|
||||
GL_state.Texture.Enable(2, GL_TEXTURE_2D, Smaa_search_tex);
|
||||
|
||||
opengl_draw_full_screen_textured(0.0f, 0.0f, Scene_texture_u_scale, Scene_texture_u_scale);
|
||||
opengl_draw_full_screen_scene_texture();
|
||||
}
|
||||
|
||||
static void smaa_neighborhood_blending()
|
||||
@@ -381,7 +387,7 @@ static void smaa_neighborhood_blending()
|
||||
GL_state.Texture.Enable(0, GL_TEXTURE_2D, Scene_ldr_texture);
|
||||
GL_state.Texture.Enable(1, GL_TEXTURE_2D, Smaa_blend_tex);
|
||||
|
||||
opengl_draw_full_screen_textured(0.0f, 0.0f, Scene_texture_u_scale, Scene_texture_u_scale);
|
||||
opengl_draw_full_screen_scene_texture();
|
||||
}
|
||||
|
||||
void smaa_resolve()
|
||||
@@ -491,7 +497,7 @@ void opengl_post_lightshafts()
|
||||
GL_state.Blend(GL_TRUE);
|
||||
GL_state.SetAlphaBlendMode(ALPHA_BLEND_ADDITIVE);
|
||||
|
||||
opengl_draw_full_screen_textured(0.0f, 0.0f, Scene_texture_u_scale, Scene_texture_u_scale);
|
||||
opengl_draw_full_screen_scene_texture();
|
||||
|
||||
GL_state.Blend(GL_FALSE);
|
||||
break;
|
||||
@@ -625,7 +631,7 @@ void gr_opengl_post_process_end()
|
||||
// now render it to the screen ...
|
||||
GL_state.PopFramebufferState();
|
||||
|
||||
opengl_draw_full_screen_textured(0.0f, 0.0f, Scene_texture_u_scale, Scene_texture_u_scale);
|
||||
opengl_draw_full_screen_scene_texture();
|
||||
|
||||
//Shadow Map debug window
|
||||
//#define SHADOW_DEBUG
|
||||
@@ -1020,77 +1026,140 @@ static GLuint load_smaa_texture(GLsizei width, GLsizei height, GLenum format, co
|
||||
return tex;
|
||||
}
|
||||
|
||||
static void setup_smaa_resources()
|
||||
// The SMAA area and search textures are fixed-size lookup tables baked into the binary, so unlike
|
||||
// everything else here they survive a resolution change untouched.
|
||||
static void setup_smaa_lookup_textures()
|
||||
{
|
||||
GL_state.PushFramebufferState();
|
||||
|
||||
Smaa_area_tex = load_smaa_texture(AREATEX_WIDTH, AREATEX_HEIGHT, GL_RG8, areaTexBytes, "SMAA Area Texture");
|
||||
Smaa_search_tex =
|
||||
load_smaa_texture(SEARCHTEX_WIDTH, SEARCHTEX_HEIGHT, GL_R8, searchTexBytes, "SMAA Search Texture");
|
||||
}
|
||||
|
||||
static void setup_smaa_render_targets()
|
||||
{
|
||||
setup_smaa_edges_resources();
|
||||
|
||||
setup_smaa_blending_weight_resources();
|
||||
|
||||
setup_smaa_neighborhood_blending_resources();
|
||||
|
||||
GL_state.PopFramebufferState();
|
||||
}
|
||||
|
||||
// generate and test the framebuffer and textures that we are going to use
|
||||
static bool opengl_post_init_framebuffer()
|
||||
static void shutdown_smaa_render_targets()
|
||||
{
|
||||
bool rval = false;
|
||||
opengl_delete_render_texture(Smaa_edges_tex);
|
||||
opengl_delete_render_framebuffer(Smaa_edge_detection_fb);
|
||||
|
||||
// clamp size, if needed
|
||||
Post_texture_width = gr_screen.max_w;
|
||||
Post_texture_height = gr_screen.max_h;
|
||||
opengl_delete_render_texture(Smaa_blend_tex);
|
||||
opengl_delete_render_framebuffer(Smaa_blending_weight_fb);
|
||||
|
||||
if (Post_texture_width > GL_max_renderbuffer_size) {
|
||||
Post_texture_width = GL_max_renderbuffer_size;
|
||||
}
|
||||
opengl_delete_render_texture(Smaa_output_tex);
|
||||
opengl_delete_render_framebuffer(Smaa_neighborhood_blending_fb);
|
||||
}
|
||||
|
||||
if (Post_texture_height > GL_max_renderbuffer_size) {
|
||||
Post_texture_height = GL_max_renderbuffer_size;
|
||||
}
|
||||
// Allocate every post-processing resource whose size follows the scene textures. Split out from
|
||||
// opengl_post_process_init() so gr_opengl_resize_render_targets() can rebuild just these without
|
||||
// re-parsing post_processing.tbl or recompiling shaders.
|
||||
static void opengl_post_setup_render_targets()
|
||||
{
|
||||
// These consume the scene textures pass by pass, so they have to match them exactly rather
|
||||
// than being sized from gr_screen independently -- see gr_opengl_scene_texture_begin() for
|
||||
// what the two sizes diverging would mean.
|
||||
Post_texture_width = Scene_texture_width;
|
||||
Post_texture_height = Scene_texture_height;
|
||||
|
||||
GL_state.PushFramebufferState();
|
||||
|
||||
opengl_setup_bloom_textures();
|
||||
|
||||
// Always set up SMAA resources so the user can switch to an SMAA preset
|
||||
// at runtime even when starting with a non-SMAA AA mode, such as None.
|
||||
//if (Gr_aa_mode != AntiAliasMode::None) {
|
||||
setup_smaa_resources();
|
||||
//}
|
||||
setup_smaa_render_targets();
|
||||
|
||||
GL_state.PopFramebufferState();
|
||||
|
||||
GL_state.BindFrameBuffer(0);
|
||||
|
||||
rval = true;
|
||||
|
||||
if ( opengl_check_for_errors("post_init_framebuffer()") ) {
|
||||
rval = false;
|
||||
}
|
||||
|
||||
return rval;
|
||||
}
|
||||
|
||||
|
||||
|
||||
void opengl_post_process_shutdown_bloom()
|
||||
{
|
||||
if ( Bloom_textures[0] ) {
|
||||
glDeleteTextures(1, &Bloom_textures[0]);
|
||||
Bloom_textures[0] = 0;
|
||||
opengl_delete_render_texture(Bloom_textures[0]);
|
||||
opengl_delete_render_texture(Bloom_textures[1]);
|
||||
opengl_delete_render_framebuffer(Bloom_framebuffer);
|
||||
}
|
||||
|
||||
static void opengl_post_shutdown_render_targets()
|
||||
{
|
||||
opengl_post_process_shutdown_bloom();
|
||||
shutdown_smaa_render_targets();
|
||||
}
|
||||
|
||||
void opengl_post_resize_render_targets()
|
||||
{
|
||||
// Post-processing may have been disabled outright (no FBOs, missing shaders, or turned off in
|
||||
// the table), in which case none of these resources exist and none should start existing now.
|
||||
if ( !Post_initialized ) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ( Bloom_textures[1] ) {
|
||||
glDeleteTextures(1, &Bloom_textures[1]);
|
||||
Bloom_textures[1] = 0;
|
||||
opengl_post_shutdown_render_targets();
|
||||
opengl_post_setup_render_targets();
|
||||
}
|
||||
|
||||
// generate and test the framebuffer and textures that we are going to use
|
||||
static bool opengl_post_init_framebuffer()
|
||||
{
|
||||
setup_smaa_lookup_textures();
|
||||
|
||||
opengl_post_setup_render_targets();
|
||||
|
||||
return !opengl_check_for_errors("post_init_framebuffer()");
|
||||
}
|
||||
|
||||
size_t opengl_get_postprocessing_render_target_bytes()
|
||||
{
|
||||
// The bytes for each pixel come from the internal formats at the creation sites in this file:
|
||||
// GL_RGBA16F holds 8 bytes and GL_RGBA8 holds 4. Keep these values equal to those formats.
|
||||
constexpr size_t RGBA16F_BYTES = 8;
|
||||
constexpr size_t RGBA8_BYTES = 4;
|
||||
|
||||
size_t bytes = 0;
|
||||
|
||||
// The bloom textures are half the size of the post-processing textures. opengl_setup_bloom_textures()
|
||||
// calls glGenerateMipmap() before it sets GL_TEXTURE_MAX_LEVEL, thus the driver holds the full
|
||||
// mipmap chain, not only MAX_MIP_BLUR_LEVELS levels.
|
||||
for (GLuint bloom_texture : Bloom_textures) {
|
||||
if (bloom_texture == 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
auto width = static_cast<size_t>(std::max(1, Post_texture_width >> 1));
|
||||
auto height = static_cast<size_t>(std::max(1, Post_texture_height >> 1));
|
||||
while (true) {
|
||||
bytes += width * height * RGBA16F_BYTES;
|
||||
if ((width == 1) && (height == 1)) {
|
||||
break;
|
||||
}
|
||||
width = std::max<size_t>(1, width >> 1);
|
||||
height = std::max<size_t>(1, height >> 1);
|
||||
}
|
||||
}
|
||||
|
||||
if ( Bloom_framebuffer > 0 ) {
|
||||
glDeleteFramebuffers(1, &Bloom_framebuffer);
|
||||
Bloom_framebuffer = 0;
|
||||
// The SMAA area and search textures stay out of this total. They hold lookup data of a fixed
|
||||
// small size, thus they are not render targets.
|
||||
const auto post_w = static_cast<size_t>(Post_texture_width);
|
||||
const auto post_h = static_cast<size_t>(Post_texture_height);
|
||||
|
||||
if (Smaa_edges_tex != 0) {
|
||||
bytes += post_w * post_h * RGBA8_BYTES;
|
||||
}
|
||||
if (Smaa_blend_tex != 0) {
|
||||
bytes += post_w * post_h * RGBA8_BYTES;
|
||||
}
|
||||
if (Smaa_output_tex != 0) {
|
||||
bytes += post_w * post_h * RGBA8_BYTES;
|
||||
}
|
||||
|
||||
return bytes;
|
||||
}
|
||||
|
||||
void opengl_post_process_init()
|
||||
@@ -1141,20 +1210,16 @@ void opengl_post_process_shutdown()
|
||||
return;
|
||||
}
|
||||
|
||||
if (Post_framebuffer_id[0]) {
|
||||
glDeleteFramebuffers(1, &Post_framebuffer_id[0]);
|
||||
Post_framebuffer_id[0] = 0;
|
||||
|
||||
if (Post_framebuffer_id[1]) {
|
||||
glDeleteFramebuffers(1, &Post_framebuffer_id[1]);
|
||||
Post_framebuffer_id[1] = 0;
|
||||
}
|
||||
}
|
||||
opengl_delete_render_framebuffer(Post_framebuffer_id[0]);
|
||||
opengl_delete_render_framebuffer(Post_framebuffer_id[1]);
|
||||
|
||||
graphics::Post_processing_manager->clear();
|
||||
graphics::Post_processing_manager = nullptr;
|
||||
|
||||
opengl_post_process_shutdown_bloom();
|
||||
opengl_post_shutdown_render_targets();
|
||||
|
||||
opengl_delete_render_texture(Smaa_area_tex);
|
||||
opengl_delete_render_texture(Smaa_search_tex);
|
||||
|
||||
Post_in_frame = false;
|
||||
Post_active_shader_index = 0;
|
||||
|
||||
@@ -8,6 +8,18 @@
|
||||
void opengl_post_process_init();
|
||||
void opengl_post_process_shutdown();
|
||||
|
||||
// Rebuild the resolution-dependent subset of the above for the current scene texture size, without
|
||||
// re-parsing post_processing.tbl or recompiling shaders. No-op if post-processing isn't active.
|
||||
void opengl_post_resize_render_targets();
|
||||
|
||||
/**
|
||||
* @brief Gives the bytes that the bloom and SMAA render textures hold
|
||||
*
|
||||
* The profiler overlay's memory panel adds this value to the "render targets" group. The function
|
||||
* counts only the textures that exist at the moment of the call.
|
||||
*/
|
||||
size_t opengl_get_postprocessing_render_target_bytes();
|
||||
|
||||
void gr_opengl_post_process_set_effect(const char *name, int x, const vec3d *rgb);
|
||||
void gr_opengl_post_process_set_defaults();
|
||||
void gr_opengl_post_process_save_zbuffer();
|
||||
|
||||
@@ -433,6 +433,9 @@ 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);
|
||||
// Unit 11 matches the model pass. Unit 7 is the model misc map, so do not use it
|
||||
// -- see the raw-sampler bind in gropengldeferred.cpp.
|
||||
Current_shader->program->Uniforms.setTextureUniform("shadow_map_raw", 11);
|
||||
break;
|
||||
|
||||
case SDR_TYPE_PASSTHROUGH_RENDER:
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -113,6 +114,18 @@ void opengl_texture_state::Enable(GLuint unit, GLenum tex_target, GLuint tex_id)
|
||||
Enable(tex_id);
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
if (tex_id == 0) {
|
||||
|
||||
@@ -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
|
||||
@@ -65,7 +71,20 @@ class opengl_texture_state
|
||||
*/
|
||||
void Enable(GLuint unit, GLenum tex_target, GLuint tex_id);
|
||||
void Delete(GLuint tex_id);
|
||||
|
||||
|
||||
/**
|
||||
* @brief Binds a sampler object to the specified unit, or unbinds one if sampler is 0
|
||||
*
|
||||
* 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);
|
||||
};
|
||||
|
||||
@@ -15,11 +15,14 @@
|
||||
#include "gropenglstate.h"
|
||||
#include "gropengltexture.h"
|
||||
#include "gropengldraw.h"
|
||||
#include "gropenglpostprocessing.h"
|
||||
#include "gropengltnl.h"
|
||||
#include "bmpman/bm_internal.h"
|
||||
#include "bmpman/bmpman.h"
|
||||
#include "cmdline/cmdline.h"
|
||||
#include "ddsutils/ddsutils.h"
|
||||
#include "globalincs/systemvars.h"
|
||||
#include "graphics/2d.h"
|
||||
#include "graphics/grinternal.h"
|
||||
#include "math/vecmat.h"
|
||||
#include "options/Option.h"
|
||||
@@ -89,28 +92,6 @@ static auto TextureFilteringOption __UNUSED = options::OptionBuilder<int>("Graph
|
||||
.parser(parse_texture_filtering_func)
|
||||
.finish();
|
||||
|
||||
static SCP_vector<float> anisotropic_value_enumerator()
|
||||
{
|
||||
float max;
|
||||
if (!gr_get_property(gr_property::MAX_ANISOTROPY, &max)) {
|
||||
return SCP_vector<float>();
|
||||
}
|
||||
|
||||
if (max <= 2.0f) {
|
||||
return SCP_vector<float>();
|
||||
}
|
||||
|
||||
SCP_vector<float> out;
|
||||
|
||||
// We assume here that the anisotropy levels are powers of two...
|
||||
float current = 1.0f;
|
||||
while (current <= max) {
|
||||
out.push_back(current);
|
||||
current *= 2.0f;
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
static SCP_string anisotropic_display(float val)
|
||||
{
|
||||
if (val < 2.0f) {
|
||||
@@ -134,7 +115,7 @@ static float anisotropic_default()
|
||||
static auto AnisotropyOption = options::OptionBuilder<float>("Graphics.Anisotropy",
|
||||
std::pair<const char*, int>{"Anistropic filtering", 1736},
|
||||
std::pair<const char*, int>{"Controls the amount of anistropic filtering of the textures", 1737})
|
||||
.enumerator(anisotropic_value_enumerator)
|
||||
.enumerator(gr_get_supported_anisotropy_levels)
|
||||
.category(std::make_pair("Graphics", 1825))
|
||||
.display(anisotropic_display)
|
||||
.default_func(anisotropic_default)
|
||||
@@ -146,6 +127,27 @@ static auto AnisotropyOption = options::OptionBuilder<float>("Graphics.Anisotrop
|
||||
int opengl_free_texture(tcache_slot_opengl *t);
|
||||
int opengl_create_texture (int bitmap_handle, int bitmap_type, tcache_slot_opengl *tslot = NULL);
|
||||
|
||||
// Running total of the bytes of texture data held by all live texture cache slots, for the
|
||||
// profiler overlay's memory panel. Kept in sync at every site that fills or drops a slot's data
|
||||
// rather than summed on demand, because the slots live in bmpman's blocks and a full walk of them
|
||||
// each frame is too expensive. Animations put all their frames in one texture array but give each
|
||||
// frame its own slot, so freeing some frames of an animation lowers this total while the array
|
||||
// itself stays alive until the last frame goes away.
|
||||
static size_t GL_texture_bytes_used = 0;
|
||||
|
||||
/**
|
||||
* @brief Removes a texture cache slot's bytes from GL_texture_bytes_used
|
||||
*
|
||||
* Call this at each site that drops the texture data of a slot, immediately before the code that
|
||||
* clears the slot. The function sets the size of the slot to 0, thus a second call for the same
|
||||
* slot does nothing.
|
||||
*/
|
||||
static void opengl_texture_release_bytes(tcache_slot_opengl* t)
|
||||
{
|
||||
GL_texture_bytes_used -= static_cast<size_t>(t->size);
|
||||
t->size = 0;
|
||||
}
|
||||
|
||||
extern int get_num_mipmap_levels(int w, int h);
|
||||
|
||||
void opengl_set_additive_tex_env()
|
||||
@@ -189,8 +191,15 @@ void opengl_tcache_init()
|
||||
// check what mipmap filter we should be using
|
||||
// 0 == Bilinear
|
||||
// 1 == Trilinear
|
||||
// Seed from the legacy config key first: TextureFilteringOption's default_func returns
|
||||
// GL_mipmap_filter, so this read is what supplies that default. Only then let the option
|
||||
// override it, the same order the anisotropy setting below uses.
|
||||
GL_mipmap_filter = os_config_read_uint(NULL, "TextureFilter", 1);
|
||||
|
||||
if (Using_in_game_options) {
|
||||
GL_mipmap_filter = TextureFilteringOption->getValue();
|
||||
}
|
||||
|
||||
if (GL_mipmap_filter > 1) {
|
||||
GL_mipmap_filter = 1;
|
||||
}
|
||||
@@ -236,6 +245,14 @@ void opengl_tcache_flush()
|
||||
opengl_free_texture(static_cast<tcache_slot_opengl*>(slot.gr_info));
|
||||
}
|
||||
}
|
||||
|
||||
// A flush releases the texture of every slot, thus the total must go to 0. A value that is not
|
||||
// 0 shows that a site which drops texture data does not call opengl_texture_release_bytes().
|
||||
// The shutdown sequence is the one exception: bm_close() removes the blocks before it closes
|
||||
// the graphics backend, thus the loop above then finds no slot to release.
|
||||
Assertion(bm_blocks.empty() || (GL_texture_bytes_used == 0),
|
||||
"OpenGL texture memory tracking lost " SIZE_T_ARG " bytes. A texture release site is missing.",
|
||||
GL_texture_bytes_used);
|
||||
}
|
||||
|
||||
extern void opengl_kill_all_render_targets();
|
||||
@@ -334,6 +351,7 @@ int opengl_free_texture(tcache_slot_opengl *t)
|
||||
// There are still used frames left in the animation so we can't free the texture object here
|
||||
|
||||
// We still need to reset this slot though since it has been "freed"
|
||||
opengl_texture_release_bytes(t);
|
||||
t->reset();
|
||||
|
||||
// Everything is fine
|
||||
@@ -353,6 +371,7 @@ int opengl_free_texture(tcache_slot_opengl *t)
|
||||
glDeleteTextures (1, &t->texture_id);
|
||||
|
||||
// Finally, reset the data of this slot
|
||||
opengl_texture_release_bytes(t);
|
||||
t->reset();
|
||||
|
||||
return 1;
|
||||
@@ -731,10 +750,14 @@ static int opengl_texture_set_level(int bitmap_handle, int bitmap_type, int bmap
|
||||
} // end switch
|
||||
|
||||
tSlot->bitmap_handle = bitmap_handle;
|
||||
// Remove the previous bytes of the slot before the new size replaces them. This function also
|
||||
// runs for a texture that is filled again (a streaming animation, for example).
|
||||
opengl_texture_release_bytes(tSlot);
|
||||
tSlot->size = (dsize) ? ((doffset + dsize) - skip_size) : (tex_w * tex_h * byte_mult);
|
||||
tSlot->w = (ushort)tex_w;
|
||||
tSlot->h = (ushort)tex_h;
|
||||
|
||||
GL_texture_bytes_used += static_cast<size_t>(tSlot->size);
|
||||
GL_textures_in_frame += tSlot->size;
|
||||
|
||||
GL_CHECK_FOR_ERRORS("end of create_texture_sub()");
|
||||
@@ -964,6 +987,7 @@ int opengl_create_texture(int bitmap_handle, int bitmap_type, tcache_slot_opengl
|
||||
GL_state.Texture.Delete(start_slot->texture_id);
|
||||
glDeleteTextures(1, &start_slot->texture_id);
|
||||
|
||||
opengl_texture_release_bytes(start_slot);
|
||||
start_slot->reset();
|
||||
}
|
||||
}
|
||||
@@ -1034,6 +1058,7 @@ int opengl_create_texture(int bitmap_handle, int bitmap_type, tcache_slot_opengl
|
||||
|
||||
if (tslot->texture_id == 0) {
|
||||
mprintf(("!!OpenGL DEBUG!! t->texture_id == 0\n"));
|
||||
opengl_texture_release_bytes(tslot);
|
||||
tslot->reset();
|
||||
return 0;
|
||||
}
|
||||
@@ -1110,7 +1135,13 @@ int opengl_create_texture(int bitmap_handle, int bitmap_type, tcache_slot_opengl
|
||||
auto index = frame - animation_begin;
|
||||
|
||||
auto frame_slot = bm_get_gr_info<tcache_slot_opengl>(frame, true);
|
||||
// The copy below overwrites the size of the slot, thus the old bytes must go away first
|
||||
opengl_texture_release_bytes(frame_slot);
|
||||
*frame_slot = *tslot; // Copy the existing properties to the slot of this texture
|
||||
// The copy also brought in the size of tslot, which belongs to a different slot and is
|
||||
// already part of GL_texture_bytes_used. opengl_texture_set_level() below gives this slot
|
||||
// its own size.
|
||||
frame_slot->size = 0;
|
||||
frame_slot->array_index = (uint32_t) index;
|
||||
|
||||
// set the bits per pixel
|
||||
@@ -1641,6 +1672,7 @@ struct fbo_t {
|
||||
// these first vars should only be modified in opengl_make_render_target()
|
||||
GLuint renderbuffer_id = 0;
|
||||
GLuint framebuffer_id = 0;
|
||||
size_t renderbuffer_bytes = 0; // for the profiler overlay's memory panel; see GL_renderbuffer_bytes_used
|
||||
int width = 0;
|
||||
int height = 0;
|
||||
// these next 2 should only be modifed in opengl_set_render_target()
|
||||
@@ -1653,6 +1685,12 @@ static SCP_vector<fbo_t> RenderTarget;
|
||||
static fbo_t *render_target = NULL;
|
||||
static int next_fbo_id = 0;
|
||||
|
||||
// Running total of bytes used by depth/stencil renderbuffers across all live FBOs, for the
|
||||
// profiler overlay's memory panel. Kept in sync with each fbo_t's renderbuffer_bytes at every
|
||||
// create/delete site below rather than summed on demand, since RenderTarget entries are reused
|
||||
// (opengl_get_free_fbo) and reset in place.
|
||||
static size_t GL_renderbuffer_bytes_used = 0;
|
||||
|
||||
static fbo_t* opengl_get_fbo(int id) {
|
||||
if (id < 0) {
|
||||
return nullptr;
|
||||
@@ -1768,11 +1806,31 @@ void opengl_kill_render_target(bitmap_slot* slot)
|
||||
if (fbo->renderbuffer_id) {
|
||||
glDeleteRenderbuffers(1, &fbo->renderbuffer_id);
|
||||
fbo->renderbuffer_id = 0;
|
||||
GL_renderbuffer_bytes_used -= fbo->renderbuffer_bytes;
|
||||
fbo->renderbuffer_bytes = 0;
|
||||
}
|
||||
|
||||
opengl_free_fbo_slot(fbo->fbo_id);
|
||||
}
|
||||
|
||||
void gr_opengl_get_memory_stats(gr_memory_stats& stats)
|
||||
{
|
||||
stats.gpu_purpose_valid = true;
|
||||
|
||||
// The textures of the bitmap cache. Render targets that the game creates through bmpman are
|
||||
// part of this group, which matches the Vulkan backend: its bm_make_render_target() images
|
||||
// also go into the texture group.
|
||||
stats.gpu_texture_bytes = GL_texture_bytes_used;
|
||||
|
||||
stats.gpu_geometry_bytes = opengl_get_geometry_bytes();
|
||||
|
||||
// The render target group holds the resources of the backend itself: the depth/stencil
|
||||
// renderbuffers of the bitmap render targets, the scene and backbuffer textures, the
|
||||
// post-processing textures, and the shadow map.
|
||||
stats.gpu_render_target_bytes = GL_renderbuffer_bytes_used + opengl_get_scene_render_target_bytes() +
|
||||
opengl_get_postprocessing_render_target_bytes() + opengl_get_shadow_map_bytes();
|
||||
}
|
||||
|
||||
void opengl_kill_all_render_targets()
|
||||
{
|
||||
for (size_t i = 0; i < RenderTarget.size(); i++) {
|
||||
@@ -1786,10 +1844,18 @@ void opengl_kill_all_render_targets()
|
||||
if (fbo->renderbuffer_id) {
|
||||
glDeleteRenderbuffers(1, &fbo->renderbuffer_id);
|
||||
fbo->renderbuffer_id = 0;
|
||||
GL_renderbuffer_bytes_used -= fbo->renderbuffer_bytes;
|
||||
fbo->renderbuffer_bytes = 0;
|
||||
}
|
||||
}
|
||||
|
||||
RenderTarget.clear();
|
||||
|
||||
// Every renderbuffer is gone now, thus the total must go to 0. A value that is not 0 shows
|
||||
// that a site which deletes a renderbuffer does not correct GL_renderbuffer_bytes_used.
|
||||
Assertion(GL_renderbuffer_bytes_used == 0,
|
||||
"OpenGL renderbuffer memory tracking lost " SIZE_T_ARG " bytes. A renderbuffer delete site is missing.",
|
||||
GL_renderbuffer_bytes_used);
|
||||
}
|
||||
|
||||
int opengl_set_render_target( int slot, int face, int is_static )
|
||||
@@ -1946,6 +2012,26 @@ int opengl_make_render_target( int handle, int *w, int *h, int *bpp, int *mm_lvl
|
||||
|
||||
glTexParameteri(GL_texture_target, GL_TEXTURE_MAX_LEVEL, ts->mipmap_levels - 1);
|
||||
|
||||
// Record the bytes of the color texture, which opengl_tex_array_storage() gave the format
|
||||
// GL_RGBA8 (4 bytes for each pixel). A cube map holds 6 faces of that size.
|
||||
size_t rt_texture_bytes = 0;
|
||||
for (int level = 0; level < ts->mipmap_levels; ++level) {
|
||||
auto level_w = static_cast<size_t>(std::max(1, *w >> level));
|
||||
auto level_h = static_cast<size_t>(std::max(1, *h >> level));
|
||||
rt_texture_bytes += level_w * level_h * 4;
|
||||
}
|
||||
if (flags & BMP_FLAG_CUBEMAP) {
|
||||
rt_texture_bytes *= 6;
|
||||
}
|
||||
// A slot that held a render target before must lose its old bytes first.
|
||||
opengl_texture_release_bytes(ts);
|
||||
|
||||
// The size field of the slot is an int, thus the value must stay inside its range. The counter
|
||||
// takes the same (clamped) value, which keeps the add here and the subtract in
|
||||
// opengl_texture_release_bytes() equal.
|
||||
ts->size = static_cast<int>(std::min(rt_texture_bytes, static_cast<size_t>(INT_MAX)));
|
||||
GL_texture_bytes_used += static_cast<size_t>(ts->size);
|
||||
|
||||
GL_state.Texture.Enable(0);
|
||||
|
||||
// render buffer (depth)
|
||||
@@ -1954,6 +2040,10 @@ int opengl_make_render_target( int handle, int *w, int *h, int *bpp, int *mm_lvl
|
||||
glBindRenderbuffer(GL_RENDERBUFFER, new_fbo->renderbuffer_id);
|
||||
glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH24_STENCIL8, *w, *h);
|
||||
glBindRenderbuffer(GL_RENDERBUFFER, 0);
|
||||
|
||||
// GL_DEPTH24_STENCIL8 is 4 bytes/pixel
|
||||
new_fbo->renderbuffer_bytes = static_cast<size_t>(*w) * static_cast<size_t>(*h) * 4;
|
||||
GL_renderbuffer_bytes_used += new_fbo->renderbuffer_bytes;
|
||||
}
|
||||
|
||||
// frame buffer
|
||||
@@ -1980,6 +2070,7 @@ int opengl_make_render_target( int handle, int *w, int *h, int *bpp, int *mm_lvl
|
||||
|
||||
GL_state.Texture.Delete(ts->texture_id);
|
||||
glDeleteTextures(1, &ts->texture_id);
|
||||
opengl_texture_release_bytes(ts);
|
||||
ts->reset();
|
||||
|
||||
glDeleteFramebuffers(1, &new_fbo->framebuffer_id);
|
||||
@@ -1987,6 +2078,8 @@ int opengl_make_render_target( int handle, int *w, int *h, int *bpp, int *mm_lvl
|
||||
if (new_fbo->renderbuffer_id) {
|
||||
glDeleteRenderbuffers(1, &new_fbo->renderbuffer_id);
|
||||
new_fbo->renderbuffer_id = 0;
|
||||
GL_renderbuffer_bytes_used -= new_fbo->renderbuffer_bytes;
|
||||
new_fbo->renderbuffer_bytes = 0;
|
||||
}
|
||||
|
||||
opengl_set_texture_target();
|
||||
|
||||
@@ -81,6 +81,8 @@ void opengl_set_additive_tex_env();
|
||||
void opengl_set_modulate_tex_env();
|
||||
void opengl_preload_init();
|
||||
void opengl_kill_render_target(bitmap_slot* slot);
|
||||
struct gr_memory_stats;
|
||||
void gr_opengl_get_memory_stats(gr_memory_stats& stats);
|
||||
int opengl_make_render_target(int handle, int *w, int *h, int *bpp, int *mm_lvl, int flags);
|
||||
int opengl_set_render_target(int slot, int face = -1, int is_static = 0);
|
||||
ubyte* gr_opengl_get_bitmap_from_texture(int bitmap_num, int* width_out, int* height_out);
|
||||
|
||||
@@ -59,15 +59,35 @@ extern vec3d G3_user_clip_point;
|
||||
extern bool Envmap_override;
|
||||
extern bool Shadow_override;
|
||||
|
||||
size_t GL_vertex_data_in = 0;
|
||||
// Running total of the bytes held by live vertex and index buffers, for the profiler overlay's
|
||||
// memory panel (see opengl_get_geometry_bytes()). Uniform buffers and texture buffers stay out of
|
||||
// this total, which keeps it equal to the "geometry" group of the Vulkan backend.
|
||||
static size_t GL_geometry_bytes_used = 0;
|
||||
|
||||
// Tells if a buffer of this OpenGL type counts as geometry for GL_geometry_bytes_used.
|
||||
static bool opengl_buffer_is_geometry(GLenum type)
|
||||
{
|
||||
return (type == GL_ARRAY_BUFFER) || (type == GL_ELEMENT_ARRAY_BUFFER);
|
||||
}
|
||||
|
||||
size_t opengl_get_geometry_bytes()
|
||||
{
|
||||
return GL_geometry_bytes_used;
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
// Number of layers in the shadow map texture array, kept from the moment
|
||||
// opengl_init_shadow_framebuffer() created the array. opengl_get_shadow_map_bytes() uses it, thus
|
||||
// a later change of the cascade counts cannot make that value wrong.
|
||||
static int Shadow_texture_layers = 0;
|
||||
|
||||
gr_buffer_handle Transform_buffer_handle;
|
||||
|
||||
SCP_unordered_map<vertex_layout, GLuint> Stored_vertex_arrays;
|
||||
@@ -275,9 +295,11 @@ void gr_opengl_update_buffer_data(gr_buffer_handle handle, size_t size, const vo
|
||||
glBufferData(buffer_obj.type, size, data, buffer_obj.gl_usage);
|
||||
}
|
||||
|
||||
GL_vertex_data_in -= buffer_obj.size;
|
||||
if (opengl_buffer_is_geometry(buffer_obj.type)) {
|
||||
GL_geometry_bytes_used -= buffer_obj.size;
|
||||
GL_geometry_bytes_used += size;
|
||||
}
|
||||
buffer_obj.size = size;
|
||||
GL_vertex_data_in += buffer_obj.size;
|
||||
}
|
||||
|
||||
void gr_opengl_update_buffer_data_offset(gr_buffer_handle handle, size_t offset, size_t size, const void* data)
|
||||
@@ -365,7 +387,12 @@ void gr_opengl_delete_buffer(gr_buffer_handle handle)
|
||||
glDeleteTextures(1, &buffer_obj.texture);
|
||||
}
|
||||
|
||||
GL_vertex_data_in -= buffer_obj.size;
|
||||
if (opengl_buffer_is_geometry(buffer_obj.type)) {
|
||||
GL_geometry_bytes_used -= buffer_obj.size;
|
||||
}
|
||||
// The size must go to 0 here. opengl_destroy_all_buffers() calls this function for every
|
||||
// handle, thus a buffer that the game already deleted comes through a second time.
|
||||
buffer_obj.size = 0;
|
||||
|
||||
glDeleteBuffers(1, &buffer_obj.buffer_id);
|
||||
}
|
||||
@@ -487,6 +514,25 @@ static bool opengl_init_shadow_framebuffer(int size)
|
||||
// Everything is fine
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -517,29 +563,28 @@ static bool opengl_init_shadow_framebuffer(int size)
|
||||
return false;
|
||||
}
|
||||
|
||||
size_t opengl_get_shadow_map_bytes()
|
||||
{
|
||||
if (Shadow_map_depth_texture == 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// opengl_init_shadow_framebuffer() gives the texture array the internal format
|
||||
// GL_DEPTH_COMPONENT24, which the driver holds in 4 bytes for each texel.
|
||||
constexpr size_t DEPTH_COMPONENT24_BYTES = 4;
|
||||
|
||||
const auto size = static_cast<size_t>(Shadow_texture_size);
|
||||
const auto layers = static_cast<size_t>(Shadow_texture_layers);
|
||||
|
||||
return size * size * layers * DEPTH_COMPONENT24_BYTES;
|
||||
}
|
||||
|
||||
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"));
|
||||
@@ -565,6 +610,16 @@ 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(7, 0);
|
||||
GL_state.Texture.BindSampler(11, 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;
|
||||
@@ -917,8 +972,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);
|
||||
// Unit 11 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", 11);
|
||||
}
|
||||
Current_shader->program->Uniforms.setTextureUniform("sFramebuffer", 9);
|
||||
if (setAllUniforms || (flags & MODEL_SDR_FLAG_TRANSFORM))
|
||||
Current_shader->program->Uniforms.setTextureUniform("transform_tex", 10);
|
||||
@@ -1001,6 +1062,10 @@ 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(11, GL_TEXTURE_2D_ARRAY, Shadow_map_depth_texture);
|
||||
GL_state.Texture.BindSampler(11, Shadow_map_raw_sampler);
|
||||
}
|
||||
}
|
||||
|
||||
if (material_info->get_animated_effect() >= 0) {
|
||||
|
||||
@@ -35,6 +35,11 @@ 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;
|
||||
|
||||
struct opengl_vertex_bind {
|
||||
vertex_format_data::vertex_format format;
|
||||
GLint size;
|
||||
@@ -53,6 +58,22 @@ void gr_opengl_bind_uniform_buffer(uniform_block_type bind_point, size_t offset,
|
||||
void* gr_opengl_map_buffer(gr_buffer_handle handle);
|
||||
void gr_opengl_flush_mapped_buffer(gr_buffer_handle handle, size_t offset, size_t size);
|
||||
|
||||
/**
|
||||
* @brief Gives the bytes that all live vertex and index buffers hold
|
||||
*
|
||||
* The profiler overlay's memory panel shows this value as the "geometry" group. Uniform buffers
|
||||
* and texture buffers are not part of it.
|
||||
*/
|
||||
size_t opengl_get_geometry_bytes();
|
||||
|
||||
/**
|
||||
* @brief Gives the bytes that the shadow map texture array holds
|
||||
*
|
||||
* The profiler overlay's memory panel adds this value to the "render targets" group. The function
|
||||
* gives 0 when shadows are off.
|
||||
*/
|
||||
size_t opengl_get_shadow_map_bytes();
|
||||
|
||||
/**
|
||||
* @brief Retrieves the OpenGL handle of a generic buffer handle
|
||||
* @param expected_type The expected type of the buffer. Mainly used for debug checking.
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
#include "graphics/rtao.h"
|
||||
|
||||
#include "graphics/2d.h"
|
||||
#include "graphics/shadows.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()
|
||||
{
|
||||
// The shadow cascade parameters that RTAO reads are only created when shadows are on.
|
||||
return Rtao_samples > 0 && rtao_supported() && Shadow_quality != ShadowQuality::Disabled;
|
||||
}
|
||||
|
||||
void rtao_remove_unsupported_options()
|
||||
{
|
||||
if (!rtao_supported()) {
|
||||
// The option has no effect without raytracing support, so remove it.
|
||||
options::OptionsManager::instance()->removeOption(RtaoQualityOption);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
#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, the hardware supports it, AND shadows are on. 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();
|
||||
@@ -133,6 +133,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 +223,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;
|
||||
}
|
||||
|
||||
+211
-15
@@ -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,165 @@ 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);
|
||||
}
|
||||
|
||||
// Softness of the shadow-mapped sun relative to that reference, i.e. the factor the tabled
|
||||
// smoothness values get scaled by.
|
||||
//
|
||||
// This is what makes shadow softness method-agnostic: the shadow map and the raytracer are
|
||||
// driven by the same number -- the tangent of the sun's angular radius, from $SunAngularSize:
|
||||
// or measured off the sun bitmap (see sun_angular_radius_tangent() in starfield.cpp). Doubling
|
||||
// a sun's apparent size doubles the raytraced penumbra cone and this filter width alike, so a
|
||||
// mission author tunes one parameter and both methods follow.
|
||||
//
|
||||
// Where shadow_contact_hardening_supported() is true, the value returned here no longer *is*
|
||||
// the penumbra radius -- it's the ceiling pcssPenumbraRadius() (shadows.sdr) clamps a real,
|
||||
// per-pixel, blocker-search-driven radius to (see Shadow_penumbra_scale). Where it's false
|
||||
// (GL below 3.3), it's still used directly as the fixed radius, exactly
|
||||
// as before contact hardening existed -- this function's output means "the widest the penumbra
|
||||
// is allowed to get" either way, which is why the tabled values keep their old meaning for mods
|
||||
// that already tuned them.
|
||||
//
|
||||
// Uses Static_light.front(), the same light shadows_start_render() builds the cascades from.
|
||||
// Every directional light that reaches this point carries a source radius: suns get theirs in
|
||||
// stars_draw_sun(), and common_setup_room_lights() -- the only other producer -- gives its
|
||||
// stand-in lights a sun's. A zero radius therefore means a genuinely sizeless source (a mod's
|
||||
// deliberately blank sun bitmap, say), which asks for a hard edge; the shadow map answers with
|
||||
// the narrowest filter it can, 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;
|
||||
}
|
||||
@@ -670,6 +829,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 +844,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);
|
||||
@@ -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,10 @@ 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.rtaoSampleCount = Rtao_samples;
|
||||
static_data.rtaoRadius = lighting_profiles::current_rtao_radius();
|
||||
static_data.rtaoStrength = lighting_profiles::current_rtao_strength();
|
||||
static_data.shadow_mv_matrix = Shadow_view_matrix_light;
|
||||
|
||||
static_data.shadow_ray_cull_mask = ray.cull_mask;
|
||||
@@ -1089,19 +1284,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.
|
||||
|
||||
@@ -67,6 +67,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.
|
||||
@@ -105,6 +131,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,
|
||||
|
||||
@@ -70,6 +70,12 @@ void GPUMemoryHeap::freeGpuData(size_t offset) {
|
||||
gr_buffer_handle GPUMemoryHeap::bufferHandle() {
|
||||
return _bufferHandle;
|
||||
}
|
||||
size_t GPUMemoryHeap::usedBytes() const {
|
||||
return _allocator->usedBytes();
|
||||
}
|
||||
size_t GPUMemoryHeap::bufferSize() const {
|
||||
return _bufferSize;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -54,6 +54,18 @@ class GPUMemoryHeap {
|
||||
* @return The graphics code buffer handle.
|
||||
*/
|
||||
gr_buffer_handle bufferHandle();
|
||||
|
||||
/**
|
||||
* @brief Gets the number of bytes currently allocated from this heap
|
||||
* @return The sum of the sizes of all active allocations.
|
||||
*/
|
||||
size_t usedBytes() const;
|
||||
|
||||
/**
|
||||
* @brief Gets the total size of the backing GPU buffer, including both allocated and free space
|
||||
* @return The current buffer size.
|
||||
*/
|
||||
size_t bufferSize() const;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
#include "globalincs/pstypes.h"
|
||||
|
||||
#include <cstddef>
|
||||
|
||||
using SPIRV_FLOAT_MAT_4x4 = matrix4;
|
||||
using SPIRV_FLOAT_VEC4 = vec4;
|
||||
|
||||
@@ -146,6 +148,14 @@ struct shadow_cascade_static_data {
|
||||
int cascade_count;
|
||||
float rtShadowBiasMin;
|
||||
float rtShadowBiasMax;
|
||||
int rtShadowSampleCount;
|
||||
int rtaoSampleCount;
|
||||
float rtaoRadius;
|
||||
float rtaoStrength;
|
||||
// The scalars above come to exactly 32 bytes, so std140 puts the following mat4
|
||||
// straight after them with no padding. Adding or removing a scalar here changes
|
||||
// that -- keep the C++ layout in lockstep with the shadowCascadeParams block
|
||||
// declarations in deferred-f/main-f/main-v/shadow_map-g/shadow_map-v.sdr.
|
||||
matrix4 shadow_mv_matrix;
|
||||
|
||||
// Position of the view-space origin (the camera) in TLAS-relative space. The RT shadow
|
||||
@@ -173,13 +183,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 every .sdr file that declares it "
|
||||
"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 [8 leading scalars] + 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
|
||||
|
||||
@@ -148,42 +148,78 @@ size_t VulkanBufferManager::bumpAllocate(size_t size)
|
||||
size_t alignedOffset = (alloc.cursor + m_uboAlignment - 1) & ~(static_cast<size_t>(m_uboAlignment) - 1);
|
||||
|
||||
if (alignedOffset + size > alloc.capacity) {
|
||||
growFrameAllocator();
|
||||
// After growth, cursor is 0 so alignedOffset is 0
|
||||
alignedOffset = 0;
|
||||
Assertion(size <= alloc.capacity, "Frame allocator growth failed to provide enough capacity");
|
||||
// Growth preserves the cursor and copies the live contents across, so every offset handed
|
||||
// out before the growth still addresses the same bytes -- alignedOffset stays valid.
|
||||
growFrameAllocator(alignedOffset + size);
|
||||
Assertion(alignedOffset + size <= alloc.capacity,
|
||||
"Frame allocator growth failed to provide enough capacity");
|
||||
}
|
||||
|
||||
alloc.cursor = alignedOffset + size;
|
||||
return alignedOffset;
|
||||
}
|
||||
|
||||
void VulkanBufferManager::growFrameAllocator()
|
||||
void VulkanBufferManager::growFrameAllocator(size_t requiredEnd)
|
||||
{
|
||||
auto& alloc = m_frameAllocs[m_currentFrame];
|
||||
|
||||
// Double capacity until sufficient
|
||||
// Double capacity until sufficient. requiredEnd covers the allocation that triggered the
|
||||
// growth; the cursor is carried over, so the new buffer has to fit both.
|
||||
size_t newCapacity = alloc.capacity > 0 ? alloc.capacity * 2 : FRAME_ALLOC_INITIAL_SIZE;
|
||||
// Ensure at least the current cursor position can fit (handles pathological single-alloc case)
|
||||
while (newCapacity < alloc.cursor) {
|
||||
while (newCapacity < requiredEnd) {
|
||||
newCapacity *= 2;
|
||||
}
|
||||
|
||||
nprintf(("vulkan", "Growing frame allocator %u: %zuKB -> %zuKB\n",
|
||||
m_currentFrame, alloc.capacity / 1024, newCapacity / 1024));
|
||||
|
||||
// Queue old buffer for deferred destruction - the deletion queue's FRAMES_TO_WAIT=2
|
||||
// ensures the old buffer survives through current frame's GPU execution.
|
||||
// Existing handles with frameAllocBuffer pointing to the old buffer remain valid.
|
||||
auto* deletionQueue = getDeletionQueue();
|
||||
if (alloc.mappedPtr) {
|
||||
m_memoryManager->unmapMemory(alloc.allocation);
|
||||
}
|
||||
deletionQueue->queueBuffer(alloc.buffer, alloc.allocation);
|
||||
// Growth must not disturb allocations already handed out this frame. Callers keep a
|
||||
// (frameAllocBuffer, frameAllocOffset) pair and go on writing through the *current*
|
||||
// allocator's mapping while binding the buffer recorded at allocation time -- so if growth
|
||||
// swapped the buffer out from under them, their writes would land in the new buffer while
|
||||
// their draws kept reading the old one. Normally the next frame's index change forces a
|
||||
// realloc before that can be observed, which is why it only shows up where the frame index
|
||||
// stands still (qtFRED's briefing map renders outside flip()).
|
||||
//
|
||||
// So: carry the cursor over, copy the live bytes to the same offsets in the new buffer, and
|
||||
// repoint every handle still referencing the old buffer. Offsets stay valid, contents stay
|
||||
// intact, and writes and reads agree again.
|
||||
const vk::Buffer oldBuffer = alloc.buffer;
|
||||
VulkanAllocation oldAllocation = alloc.allocation; // unmapMemory takes a non-const reference
|
||||
void* oldMapped = alloc.mappedPtr;
|
||||
const size_t liveBytes = alloc.cursor;
|
||||
|
||||
// Create new buffer
|
||||
alloc = {};
|
||||
Verification(createFrameAllocBuffer(alloc, newCapacity), "Failed to grow Vulkan frame-allocator buffer");
|
||||
FrameBumpAllocator grown = {};
|
||||
Verification(createFrameAllocBuffer(grown, newCapacity), "Failed to grow Vulkan frame-allocator buffer");
|
||||
|
||||
if (liveBytes > 0 && oldMapped != nullptr && grown.mappedPtr != nullptr) {
|
||||
memcpy(grown.mappedPtr, oldMapped, liveBytes);
|
||||
m_memoryManager->flushMemory(grown.allocation, 0, liveBytes);
|
||||
}
|
||||
grown.cursor = liveBytes;
|
||||
// Growth is not a rewind -- offsets survive it -- so the generation has to carry over too, or
|
||||
// handles allocated before it would compare equal to a later generation and look fresh.
|
||||
grown.generation = alloc.generation;
|
||||
|
||||
// The old buffer stays alive through the deletion queue so draws already recorded against it
|
||||
// keep reading intact data until it retires.
|
||||
auto* deletionQueue = getDeletionQueue();
|
||||
if (oldMapped) {
|
||||
m_memoryManager->unmapMemory(oldAllocation);
|
||||
}
|
||||
if (oldBuffer) {
|
||||
deletionQueue->queueBuffer(oldBuffer, oldAllocation);
|
||||
}
|
||||
|
||||
alloc = grown;
|
||||
|
||||
if (oldBuffer) {
|
||||
for (auto& bufferObj : m_buffers) {
|
||||
if (bufferObj.valid && bufferObj.isStreaming() && bufferObj.frameAllocBuffer == oldBuffer) {
|
||||
bufferObj.frameAllocBuffer = alloc.buffer;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ========== Init / Shutdown ==========
|
||||
@@ -306,6 +342,7 @@ void VulkanBufferManager::setCurrentFrame(uint32_t frameIndex, uint64_t frameNum
|
||||
// Reset bump cursor — safe because the GPU fence for this frame-in-flight
|
||||
// was already waited on before setCurrentFrame is called.
|
||||
m_frameAllocs[m_currentFrame].cursor = 0;
|
||||
++m_frameAllocs[m_currentFrame].generation;
|
||||
}
|
||||
|
||||
// ========== Buffer usage / memory helpers ==========
|
||||
@@ -339,7 +376,22 @@ vk::BufferUsageFlags VulkanBufferManager::getVkUsageFlags(BufferType type, bool
|
||||
return flags;
|
||||
}
|
||||
|
||||
MemoryUsage VulkanBufferManager::getMemoryUsage(BufferUsageHint hint)
|
||||
namespace {
|
||||
MemoryPurpose bufferTypeToPurpose(BufferType type)
|
||||
{
|
||||
switch (type) {
|
||||
case BufferType::Vertex:
|
||||
case BufferType::Index:
|
||||
return MemoryPurpose::Geometry;
|
||||
case BufferType::Uniform:
|
||||
default:
|
||||
// Uniform buffers are already tracked separately via UniformBufferManager/gr_debug_stats.
|
||||
return MemoryPurpose::Unknown;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MemoryUsage VulkanBufferManager::getMemoryUsage(BufferUsageHint hint)
|
||||
{
|
||||
switch (hint) {
|
||||
case BufferUsageHint::Static:
|
||||
@@ -455,7 +507,8 @@ bool VulkanBufferManager::createOrResizeBuffer(VulkanBufferObject& bufferObj, si
|
||||
|
||||
// Allocate memory
|
||||
MemoryUsage memUsage = getMemoryUsage(bufferObj.usage);
|
||||
if (!m_memoryManager->allocateBufferMemory(bufferObj.buffer, memUsage, bufferObj.allocation)) {
|
||||
if (!m_memoryManager->allocateBufferMemory(
|
||||
bufferObj.buffer, memUsage, bufferObj.allocation, bufferTypeToPurpose(bufferObj.type))) {
|
||||
m_device.destroyBuffer(bufferObj.buffer);
|
||||
bufferObj.buffer = oldBuffer;
|
||||
bufferObj.allocation = oldAllocation;
|
||||
@@ -539,17 +592,20 @@ void VulkanBufferManager::updateBufferData(gr_buffer_handle handle, size_t size,
|
||||
bufferObj.frameAllocOffset = offset;
|
||||
bufferObj.dataSize = size;
|
||||
bufferObj.frameAllocFrame = m_currentFrame;
|
||||
bufferObj.frameAllocGeneration = alloc.generation;
|
||||
} else {
|
||||
// Pattern B: pre-alloc for offset writes (null data)
|
||||
if (bufferObj.frameAllocFrame != m_currentFrame || size > bufferObj.dataSize) {
|
||||
// First allocation this frame, or need more space
|
||||
if (bufferObj.frameAllocFrame != m_currentFrame ||
|
||||
bufferObj.frameAllocGeneration != alloc.generation || size > bufferObj.dataSize) {
|
||||
// First allocation this frame, the cursor was rewound under us, or need more space
|
||||
size_t offset = bumpAllocate(size);
|
||||
bufferObj.frameAllocBuffer = alloc.buffer;
|
||||
bufferObj.frameAllocOffset = offset;
|
||||
bufferObj.dataSize = size;
|
||||
bufferObj.frameAllocFrame = m_currentFrame;
|
||||
bufferObj.frameAllocGeneration = alloc.generation;
|
||||
}
|
||||
// Otherwise: same frame and size fits — keep current allocation
|
||||
// Otherwise: same frame, same generation and size fits — keep current allocation
|
||||
}
|
||||
} else {
|
||||
// Static / PersistentMapping path.
|
||||
@@ -604,15 +660,16 @@ void VulkanBufferManager::updateBufferDataOffset(gr_buffer_handle handle, size_t
|
||||
// Auto-allocate if not yet allocated this frame. This happens when
|
||||
// the caller skips updateBufferData (e.g. gr_add_to_immediate_buffer
|
||||
// when the data fits the existing buffer size).
|
||||
if (bufferObj.frameAllocFrame != m_currentFrame) {
|
||||
auto& fa = m_frameAllocs[m_currentFrame];
|
||||
if (bufferObj.frameAllocFrame != m_currentFrame || bufferObj.frameAllocGeneration != fa.generation) {
|
||||
size_t allocSize = std::max(bufferObj.dataSize, offset + size);
|
||||
Assert(allocSize > 0);
|
||||
auto& fa = m_frameAllocs[m_currentFrame];
|
||||
size_t allocOffset = bumpAllocate(allocSize);
|
||||
bufferObj.frameAllocBuffer = fa.buffer;
|
||||
bufferObj.frameAllocOffset = allocOffset;
|
||||
bufferObj.dataSize = allocSize;
|
||||
bufferObj.frameAllocFrame = m_currentFrame;
|
||||
bufferObj.frameAllocGeneration = fa.generation;
|
||||
}
|
||||
|
||||
Assert(offset + size <= bufferObj.dataSize);
|
||||
@@ -649,7 +706,7 @@ void* VulkanBufferManager::mapBuffer(gr_buffer_handle handle)
|
||||
}
|
||||
|
||||
if (bufferObj.isStreaming()) {
|
||||
Assert(bufferObj.frameAllocFrame == m_currentFrame);
|
||||
Assert(isFrameAllocCurrent(bufferObj));
|
||||
auto& alloc = m_frameAllocs[m_currentFrame];
|
||||
return static_cast<uint8_t*>(alloc.mappedPtr) + bufferObj.frameAllocOffset;
|
||||
}
|
||||
@@ -678,7 +735,7 @@ void VulkanBufferManager::flushMappedBuffer(gr_buffer_handle handle, size_t offs
|
||||
|
||||
if (bufferObj.isStreaming()) {
|
||||
// Adjust offset for current frame's allocation
|
||||
Assert(bufferObj.frameAllocFrame == m_currentFrame);
|
||||
Assert(isFrameAllocCurrent(bufferObj));
|
||||
auto& alloc = m_frameAllocs[m_currentFrame];
|
||||
m_memoryManager->flushMemory(alloc.allocation, bufferObj.frameAllocOffset + offset, size);
|
||||
} else {
|
||||
@@ -703,6 +760,30 @@ void VulkanBufferManager::bindUniformBuffer(uniform_block_type blockType, size_t
|
||||
|
||||
// ========== Buffer queries ==========
|
||||
|
||||
bool VulkanBufferManager::isFrameAllocCurrent(const VulkanBufferObject& bufferObj) const
|
||||
{
|
||||
if (!bufferObj.isStreaming()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return bufferObj.frameAllocFrame == m_currentFrame &&
|
||||
bufferObj.frameAllocGeneration == m_frameAllocs[m_currentFrame].generation;
|
||||
}
|
||||
|
||||
bool VulkanBufferManager::isFrameAllocCurrent(gr_buffer_handle handle) const
|
||||
{
|
||||
if (!isValidHandle(handle)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const VulkanBufferObject& bufferObj = m_buffers[handle.value()];
|
||||
if (!bufferObj.valid) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return isFrameAllocCurrent(bufferObj);
|
||||
}
|
||||
|
||||
vk::Buffer VulkanBufferManager::getVkBuffer(gr_buffer_handle handle) const
|
||||
{
|
||||
if (!isValidHandle(handle)) {
|
||||
@@ -716,7 +797,12 @@ vk::Buffer VulkanBufferManager::getVkBuffer(gr_buffer_handle handle) const
|
||||
|
||||
if (bufferObj.isStreaming()) {
|
||||
// Streaming buffers return the frame allocator buffer they were uploaded to
|
||||
Assert(bufferObj.frameAllocFrame == m_currentFrame);
|
||||
Assertion(isFrameAllocCurrent(bufferObj),
|
||||
"Streaming buffer %d was allocated in frame %u (generation %u) but fetched in frame %u "
|
||||
"(generation %u) -- it must be uploaded again before it can be drawn with. A binding kept "
|
||||
"from an earlier frame belongs on getVkBufferForBinding().",
|
||||
handle.value(), bufferObj.frameAllocFrame, bufferObj.frameAllocGeneration, m_currentFrame,
|
||||
m_frameAllocs[m_currentFrame].generation);
|
||||
return bufferObj.frameAllocBuffer;
|
||||
} else {
|
||||
// Record that this frame (potentially) references the buffer -- consulted
|
||||
@@ -726,6 +812,21 @@ vk::Buffer VulkanBufferManager::getVkBuffer(gr_buffer_handle handle) const
|
||||
}
|
||||
}
|
||||
|
||||
vk::Buffer VulkanBufferManager::getVkBufferForBinding(gr_buffer_handle handle) const
|
||||
{
|
||||
if (!isFrameAllocCurrent(handle)) {
|
||||
// Either the handle no longer resolves at all, or it is a streaming buffer whose
|
||||
// sub-allocation belongs to a frame that has since been recycled. Both mean the caller
|
||||
// has to fall back; only getVkBuffer() treats the latter as a bug.
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// Delegated rather than inlined: getVkBuffer()'s non-streaming branch stamps
|
||||
// lastUsedFrameNumber, which is what updateBufferData() consults to decide whether a rewrite
|
||||
// has to orphan. Duplicating the lookup here would mean remembering to keep that stamp.
|
||||
return getVkBuffer(handle);
|
||||
}
|
||||
|
||||
size_t VulkanBufferManager::getBufferSize(gr_buffer_handle handle) const
|
||||
{
|
||||
if (!isValidHandle(handle)) {
|
||||
@@ -767,11 +868,10 @@ size_t VulkanBufferManager::getFrameBaseOffset(gr_buffer_handle handle) const
|
||||
|
||||
if (bufferObj.isStreaming()) {
|
||||
// Return the bump allocator offset for the most recent upload this frame.
|
||||
// Stale handle detection: if frameAllocFrame != m_currentFrame, this buffer
|
||||
// was not uploaded this frame and the offset would be meaningless (the bump
|
||||
// allocator has been reset). This indicates a buffer marked Streaming/Dynamic
|
||||
// is being bound for rendering without being uploaded first.
|
||||
Assert(bufferObj.frameAllocFrame == m_currentFrame);
|
||||
// Stale handle detection: a sub-allocation that is not the live one has a meaningless
|
||||
// offset (the bump allocator has rewound since), which means a buffer marked
|
||||
// Streaming/Dynamic is being bound for rendering without being uploaded first.
|
||||
Assert(isFrameAllocCurrent(bufferObj));
|
||||
return bufferObj.frameAllocOffset;
|
||||
} else {
|
||||
return 0;
|
||||
|
||||
@@ -23,6 +23,10 @@ struct FrameBumpAllocator {
|
||||
void* mappedPtr = nullptr;
|
||||
size_t capacity = 0;
|
||||
size_t cursor = 0;
|
||||
// Bumped every time the cursor is rewound, so sub-allocations handed out before the rewind can
|
||||
// be told apart from ones handed out after it. Growth deliberately does NOT bump this: it
|
||||
// preserves offsets and repoints handles, so those allocations stay valid.
|
||||
uint32_t generation = 0;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -64,6 +68,7 @@ struct VulkanBufferObject {
|
||||
vk::Buffer frameAllocBuffer; // VkBuffer at upload time (may be old allocator buffer after growth)
|
||||
size_t frameAllocOffset = 0; // Byte offset within the frame allocator buffer
|
||||
uint32_t frameAllocFrame = UINT32_MAX; // Frame index when last allocated
|
||||
uint32_t frameAllocGeneration = UINT32_MAX; // Allocator generation when last allocated
|
||||
|
||||
bool isStreaming() const {
|
||||
return usage == BufferUsageHint::Streaming || usage == BufferUsageHint::Dynamic;
|
||||
@@ -111,6 +116,14 @@ public:
|
||||
* @param frameNumber The monotonic total frame number (for in-use tracking
|
||||
* of static buffers; see VulkanBufferObject::lastUsedFrameNumber)
|
||||
*/
|
||||
/**
|
||||
* @brief Point the manager at a frame-in-flight slot and rewind that slot's bump allocator.
|
||||
*
|
||||
* Passing the *current* index is legal and is what an off-screen frame end does: the cursor
|
||||
* rewinds and the generation bumps, so sub-allocations from the frame just finished are
|
||||
* invalidated rather than silently overlapped, while the swap-chain-tied index stays put.
|
||||
* Only safe once the work referencing those sub-allocations has retired.
|
||||
*/
|
||||
void setCurrentFrame(uint32_t frameIndex, uint64_t frameNumber);
|
||||
|
||||
/**
|
||||
@@ -118,6 +131,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
|
||||
*/
|
||||
@@ -193,11 +212,40 @@ public:
|
||||
|
||||
/**
|
||||
* @brief Get the Vulkan buffer handle for the current frame
|
||||
*
|
||||
* For a streaming buffer this asserts that the buffer was uploaded into the frame
|
||||
* allocator as it stands right now: a caller reaching a buffer directly is uploading and
|
||||
* drawing within one frame, so a stale sub-allocation is a bug, not a state to tolerate.
|
||||
* Consumers of a binding recorded earlier want getVkBufferForBinding() instead.
|
||||
*
|
||||
* @param handle The buffer handle
|
||||
* @return The VkBuffer, or VK_NULL_HANDLE if invalid
|
||||
*/
|
||||
vk::Buffer getVkBuffer(gr_buffer_handle handle) const;
|
||||
|
||||
/**
|
||||
* @brief Resolve a buffer for a binding that was recorded before this frame
|
||||
*
|
||||
* Same lookup as getVkBuffer(), but a streaming buffer whose sub-allocation no longer
|
||||
* belongs to the current frame allocator resolves to VK_NULL_HANDLE rather than tripping
|
||||
* that function's assert. Bindings outlive the frame they were made in -- a uniform block
|
||||
* bound once for a whole pass stays bound, mirroring glBindBufferRange() -- while a
|
||||
* streaming sub-allocation does not, so the two can legitimately disagree and the caller
|
||||
* has to fall back to the placeholder buffer.
|
||||
*
|
||||
* @param handle The buffer handle
|
||||
* @return The VkBuffer, or VK_NULL_HANDLE if the binding no longer resolves this frame
|
||||
*/
|
||||
vk::Buffer getVkBufferForBinding(gr_buffer_handle handle) const;
|
||||
|
||||
/**
|
||||
* @brief Whether a streaming buffer's sub-allocation belongs to the current frame allocator
|
||||
*
|
||||
* False for a streaming buffer that has not been uploaded since the allocator last rewound.
|
||||
* Always true for a non-streaming buffer, which owns its VkBuffer outright.
|
||||
*/
|
||||
bool isFrameAllocCurrent(gr_buffer_handle handle) const;
|
||||
|
||||
/**
|
||||
* @brief Get buffer size
|
||||
* For streaming buffers, returns the current frame allocation size.
|
||||
@@ -286,6 +334,17 @@ private:
|
||||
VulkanBufferObject* getBufferObject(gr_buffer_handle handle);
|
||||
const VulkanBufferObject* getBufferObject(gr_buffer_handle handle) const;
|
||||
|
||||
/**
|
||||
* @brief Whether a streaming buffer object's sub-allocation is the one live right now
|
||||
*
|
||||
* The frame index alone is not enough: setCurrentFrame() rewinds the bump cursor and bumps
|
||||
* the generation, and endOffscreenFrame() calls it with the frame index deliberately
|
||||
* unchanged. A sub-allocation from before that call therefore matches on index while
|
||||
* pointing at memory that has since been handed out again, so the generation has to match
|
||||
* too. Same pair updateBufferData() compares before reusing a pre-allocation.
|
||||
*/
|
||||
bool isFrameAllocCurrent(const VulkanBufferObject& bufferObj) const;
|
||||
|
||||
// Frame bump allocator
|
||||
static constexpr size_t FRAME_ALLOC_INITIAL_SIZE = 4 * 1024 * 1024;
|
||||
|
||||
@@ -293,7 +352,7 @@ private:
|
||||
void initFrameAllocators();
|
||||
void shutdownFrameAllocators();
|
||||
size_t bumpAllocate(size_t size);
|
||||
void growFrameAllocator();
|
||||
void growFrameAllocator(size_t requiredEnd);
|
||||
|
||||
std::array<FrameBumpAllocator, MAX_FRAMES_IN_FLIGHT> m_frameAllocs;
|
||||
uint32_t m_uboAlignment = 256;
|
||||
|
||||
@@ -132,6 +132,20 @@ vk::PrimitiveTopology convertPrimitiveType(primitive_type type)
|
||||
}
|
||||
}
|
||||
|
||||
bool topologySupportsPrimitiveRestart(vk::PrimitiveTopology topology)
|
||||
{
|
||||
switch (topology) {
|
||||
case vk::PrimitiveTopology::eLineStrip:
|
||||
case vk::PrimitiveTopology::eTriangleStrip:
|
||||
case vk::PrimitiveTopology::eTriangleFan:
|
||||
case vk::PrimitiveTopology::eLineStripWithAdjacency:
|
||||
case vk::PrimitiveTopology::eTriangleStripWithAdjacency:
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
vk::CullModeFlags convertCullMode(bool cullEnabled)
|
||||
{
|
||||
return cullEnabled ? vk::CullModeFlagBits::eBack : vk::CullModeFlagBits::eNone;
|
||||
|
||||
@@ -46,6 +46,15 @@ vk::StencilOp convertStencilOp(StencilOperation op);
|
||||
*/
|
||||
vk::PrimitiveTopology convertPrimitiveType(primitive_type type);
|
||||
|
||||
/**
|
||||
* @brief Whether a topology may declare primitiveRestartEnable
|
||||
*
|
||||
* True for the strip/fan topologies. For list topologies the Vulkan spec requires
|
||||
* primitiveRestartEnable to be VK_FALSE unless VK_EXT_primitive_topology_list_restart is
|
||||
* enabled, which FSO does not request.
|
||||
*/
|
||||
bool topologySupportsPrimitiveRestart(vk::PrimitiveTopology topology);
|
||||
|
||||
/**
|
||||
* @brief Convert FSO cull mode to Vulkan cull mode
|
||||
* @param cullEnabled Whether culling is enabled
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
#include "graphics/matrix.h"
|
||||
#include "graphics/material.h"
|
||||
#include "graphics/grinternal.h"
|
||||
#include "graphics/rtao.h"
|
||||
#include "graphics/shadows.h"
|
||||
#include "lighting/lighting.h"
|
||||
#include "mission/missionparse.h"
|
||||
@@ -374,13 +375,13 @@ void vulkan_deferred_lighting_msaa()
|
||||
// Global set (fallback — resolve shader doesn't use global bindings)
|
||||
vk::DescriptorSet globalSet = descriptorMgr->allocateFrameSet(DescriptorSetIndex::Global);
|
||||
Assert(globalSet);
|
||||
writer.writeSet(globalSet, VulkanDescriptorManager::getSetTemplate(DescriptorSetIndex::Global));
|
||||
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
|
||||
vk::DescriptorSet materialSet = descriptorMgr->allocateFrameSet(DescriptorSetIndex::Material);
|
||||
Assert(materialSet);
|
||||
writer.writeSet(materialSet, VulkanDescriptorManager::getSetTemplate(DescriptorSetIndex::Material));
|
||||
writer.writeSet(DescriptorSetIndex::Material, materialSet);
|
||||
|
||||
// Build texture array: elements 0-5 are MSAA textures, 6-15 are fallback
|
||||
vk::Sampler nearestSampler = texMgr->getSampler(
|
||||
@@ -402,7 +403,7 @@ void vulkan_deferred_lighting_msaa()
|
||||
// PerDraw set: GenericData UBO with {samples, fov} at binding 0
|
||||
vk::DescriptorSet perDrawSet = descriptorMgr->allocateFrameSet(DescriptorSetIndex::PerDraw);
|
||||
Assert(perDrawSet);
|
||||
writer.writeSet(perDrawSet, VulkanDescriptorManager::getSetTemplate(DescriptorSetIndex::PerDraw));
|
||||
writer.writeSet(DescriptorSetIndex::PerDraw, perDrawSet);
|
||||
|
||||
struct MsaaResolveData {
|
||||
int samples;
|
||||
@@ -416,15 +417,8 @@ void vulkan_deferred_lighting_msaa()
|
||||
ring.alloc(descriptorMgr->getCurrentFrame(), &resolveData, sizeof(resolveData));
|
||||
writer.setBuffer(PerDrawBinding::GenericData, {ring.buffer(), slotOffset, ring.slotSize()});
|
||||
writer.flush();
|
||||
cmd.bindDescriptorSets(vk::PipelineBindPoint::eGraphics,
|
||||
pipelineMgr->getPipelineLayout(),
|
||||
static_cast<uint32_t>(DescriptorSetIndex::Global), globalSet, {});
|
||||
cmd.bindDescriptorSets(vk::PipelineBindPoint::eGraphics,
|
||||
pipelineMgr->getPipelineLayout(),
|
||||
static_cast<uint32_t>(DescriptorSetIndex::Material), materialSet, {});
|
||||
cmd.bindDescriptorSets(vk::PipelineBindPoint::eGraphics,
|
||||
pipelineMgr->getPipelineLayout(),
|
||||
static_cast<uint32_t>(DescriptorSetIndex::PerDraw), perDrawSet, {});
|
||||
const vk::DescriptorSet sets[] = {globalSet, materialSet, perDrawSet};
|
||||
writer.bindSets(cmd, pipelineMgr->getPipelineLayout(), DescriptorSetIndex::Global, sets);
|
||||
|
||||
cmd.draw(3, 1, 0, 0);
|
||||
}
|
||||
@@ -545,10 +539,23 @@ void vulkan_deferred_lighting_finish()
|
||||
auto* stateTracker = getStateTracker();
|
||||
vk::CommandBuffer cmd = stateTracker->getCommandBuffer();
|
||||
|
||||
// 1. End G-buffer render pass
|
||||
// TLAS for RTAO: when shadow rendering is off (Shadow_quality Disabled, or a
|
||||
// frame that skips shadows_render_all entirely), nothing has built this
|
||||
// frame's TLAS yet -- request it here. No-op when the shadow path already
|
||||
// built it (buildTlas()'s per-frame guard). Acceleration-structure builds
|
||||
// must be recorded outside a render pass, so this ends the G-buffer pass and
|
||||
// clears it from the state tracker -- which is why step 1 below only ends
|
||||
// the pass when it is still active.
|
||||
if (rtao_enabled()) {
|
||||
vulkan_build_shadow_tlas();
|
||||
}
|
||||
|
||||
// 1. End G-buffer render pass (unless the RTAO TLAS build above already did)
|
||||
// All 6 color attachments → eShaderReadOnlyOptimal
|
||||
// Depth → eDepthStencilAttachmentOptimal
|
||||
cmd.endRenderPass();
|
||||
if (stateTracker->getCurrentRenderPass()) {
|
||||
cmd.endRenderPass();
|
||||
}
|
||||
|
||||
// 2. Copy emissive → composite (the emissive data becomes the base for light accumulation)
|
||||
// Emissive → eShaderReadOnlyOptimal (done), composite → eColorAttachmentOptimal (for light accum)
|
||||
@@ -996,11 +1003,13 @@ void vulkan_render_decals(decal_material* material_info,
|
||||
|
||||
stateTracker->bindPipeline(pipeline, pipelineManager->getPipelineLayout());
|
||||
|
||||
// Helper: get DescriptorBufferInfo from pending binding (null buffer = fallback)
|
||||
// Helper: get DescriptorBufferInfo from pending binding (null buffer = fallback).
|
||||
// getVkBufferForBinding() because a binding outlives the frame it was made in while a
|
||||
// streaming sub-allocation does not -- see bindPendingUBOs in VulkanDrawManager::applyMaterial.
|
||||
auto getPendingBufInfo = [&](size_t blockIdx) -> vk::DescriptorBufferInfo {
|
||||
const auto& pending = drawManager->getPendingUniformBinding(blockIdx);
|
||||
if (pending.valid) {
|
||||
vk::Buffer buf = bufferManager->getVkBuffer(pending.bufferHandle);
|
||||
vk::Buffer buf = bufferManager->getVkBufferForBinding(pending.bufferHandle);
|
||||
if (buf) {
|
||||
return {buf, pending.offset, pending.size};
|
||||
}
|
||||
@@ -1014,12 +1023,12 @@ void vulkan_render_decals(decal_material* material_info,
|
||||
// Set 0: Global (all fallback)
|
||||
vk::DescriptorSet globalSet = descManager->allocateFrameSet(DescriptorSetIndex::Global);
|
||||
Assert(globalSet);
|
||||
writer.writeSet(globalSet, VulkanDescriptorManager::getSetTemplate(DescriptorSetIndex::Global));
|
||||
writer.writeSet(DescriptorSetIndex::Global, globalSet);
|
||||
|
||||
// Set 1: Material
|
||||
vk::DescriptorSet materialSet = descManager->allocateFrameSet(DescriptorSetIndex::Material);
|
||||
Assert(materialSet);
|
||||
writer.writeSet(materialSet, VulkanDescriptorManager::getSetTemplate(DescriptorSetIndex::Material));
|
||||
writer.writeSet(DescriptorSetIndex::Material, materialSet);
|
||||
|
||||
// Binding 2: DecalGlobals UBO
|
||||
writer.setBuffer(MaterialBinding::DecalGlobals,
|
||||
@@ -1053,15 +1062,19 @@ void vulkan_render_decals(decal_material* material_info,
|
||||
// Set 2: PerDraw
|
||||
vk::DescriptorSet perDrawSet = descManager->allocateFrameSet(DescriptorSetIndex::PerDraw);
|
||||
Assert(perDrawSet);
|
||||
writer.writeSet(perDrawSet, VulkanDescriptorManager::getSetTemplate(DescriptorSetIndex::PerDraw));
|
||||
writer.writeSet(DescriptorSetIndex::PerDraw, perDrawSet);
|
||||
writer.setBuffer(PerDrawBinding::Matrices,
|
||||
getPendingBufInfo(static_cast<size_t>(uniform_block_type::Matrices)));
|
||||
writer.setBuffer(PerDrawBinding::DecalInfo,
|
||||
getPendingBufInfo(static_cast<size_t>(uniform_block_type::DecalInfo)));
|
||||
writer.flush();
|
||||
stateTracker->bindDescriptorSet(DescriptorSetIndex::Global, globalSet);
|
||||
stateTracker->bindDescriptorSet(DescriptorSetIndex::Material, materialSet);
|
||||
stateTracker->bindDescriptorSet(DescriptorSetIndex::PerDraw, perDrawSet);
|
||||
stateTracker->bindDescriptorSet(DescriptorSetIndex::Material,
|
||||
materialSet,
|
||||
writer.dynamicOffsets(DescriptorSetIndex::Material));
|
||||
stateTracker->bindDescriptorSet(DescriptorSetIndex::PerDraw,
|
||||
perDrawSet,
|
||||
writer.dynamicOffsets(DescriptorSetIndex::PerDraw));
|
||||
|
||||
// Bind vertex buffers: binding 0 = box VBO, binding 1 = instance buffer
|
||||
vk::Buffer boxVBO = bufferManager->getVkBuffer(buffers.Vbuffer_handle);
|
||||
|
||||
@@ -11,14 +11,21 @@ namespace graphics::vulkan {
|
||||
static constexpr DescriptorBindingTemplate s_globalBindings[] = {
|
||||
{GlobalBinding::Lights, vk::DescriptorType::eUniformBuffer, 1, vk::ShaderStageFlagBits::eVertex | vk::ShaderStageFlagBits::eFragment},
|
||||
{GlobalBinding::DeferredData, vk::DescriptorType::eUniformBuffer, 1, vk::ShaderStageFlagBits::eVertex | vk::ShaderStageFlagBits::eFragment},
|
||||
{GlobalBinding::ShadowMap, vk::DescriptorType::eCombinedImageSampler, 1, vk::ShaderStageFlagBits::eFragment, vk::ImageViewType::e2DArray},
|
||||
{GlobalBinding::ShadowMap, vk::DescriptorType::eCombinedImageSampler, 1, vk::ShaderStageFlagBits::eFragment, vk::ImageViewType::e2DArray, /*depthCompare=*/true},
|
||||
{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};
|
||||
|
||||
// GLOBAL_DYNAMIC_OFFSET_COUNT is stated rather than derived because this set's template is
|
||||
// assembled at runtime (see below); keep the two in agreement. The optional TLAS binding is not
|
||||
// a dynamic UBO, so it cannot change the count either.
|
||||
static_assert(dynamic_offset_count(s_globalBindings) == GLOBAL_DYNAMIC_OFFSET_COUNT,
|
||||
"The Global set gained a dynamic binding; GLOBAL_DYNAMIC_OFFSET_COUNT must follow.");
|
||||
|
||||
// The Global set's layout must omit the TLAS binding entirely on devices without
|
||||
// VK_KHR_acceleration_structure enabled -- a descriptor type the device didn't
|
||||
// enable the extension for is invalid in vkCreateDescriptorSetLayout. So, unlike
|
||||
@@ -27,26 +34,10 @@ static constexpr DescriptorBindingTemplate s_globalTlasBinding{
|
||||
// the per-instance m_globalBindingsRuntime/m_globalTemplateRuntime members
|
||||
// (see VulkanDescriptorManager.h), rather than compiled in as a fixed constexpr array.
|
||||
|
||||
static constexpr DescriptorBindingTemplate s_materialBindings[] = {
|
||||
{MaterialBinding::ModelData, vk::DescriptorType::eUniformBuffer, 1, vk::ShaderStageFlagBits::eVertex | vk::ShaderStageFlagBits::eFragment},
|
||||
{MaterialBinding::TextureArray, vk::DescriptorType::eCombinedImageSampler, 16, vk::ShaderStageFlagBits::eFragment, vk::ImageViewType::e2DArray},
|
||||
{MaterialBinding::DecalGlobals, vk::DescriptorType::eUniformBuffer, 1, vk::ShaderStageFlagBits::eVertex | vk::ShaderStageFlagBits::eFragment},
|
||||
{MaterialBinding::TransformSSBO, vk::DescriptorType::eStorageBuffer, 1, vk::ShaderStageFlagBits::eVertex},
|
||||
{MaterialBinding::DepthMap, vk::DescriptorType::eCombinedImageSampler, 1, vk::ShaderStageFlagBits::eFragment},
|
||||
{MaterialBinding::SceneColor, vk::DescriptorType::eCombinedImageSampler, 1, vk::ShaderStageFlagBits::eFragment},
|
||||
{MaterialBinding::DistortionMap, vk::DescriptorType::eCombinedImageSampler, 1, vk::ShaderStageFlagBits::eFragment},
|
||||
{MaterialBinding::ShadowMapData, vk::DescriptorType::eUniformBuffer, 1, vk::ShaderStageFlagBits::eVertex},
|
||||
};
|
||||
static constexpr DescriptorSetTemplate s_materialTemplate(s_materialBindings);
|
||||
|
||||
static constexpr DescriptorBindingTemplate s_perDrawBindings[] = {
|
||||
{PerDrawBinding::GenericData, vk::DescriptorType::eUniformBuffer, 1, vk::ShaderStageFlagBits::eVertex | vk::ShaderStageFlagBits::eFragment},
|
||||
{PerDrawBinding::Matrices, vk::DescriptorType::eUniformBuffer, 1, vk::ShaderStageFlagBits::eVertex | vk::ShaderStageFlagBits::eFragment},
|
||||
{PerDrawBinding::NanoVGData, vk::DescriptorType::eUniformBuffer, 1, vk::ShaderStageFlagBits::eVertex | vk::ShaderStageFlagBits::eFragment},
|
||||
{PerDrawBinding::DecalInfo, vk::DescriptorType::eUniformBuffer, 1, vk::ShaderStageFlagBits::eVertex | vk::ShaderStageFlagBits::eFragment},
|
||||
{PerDrawBinding::MovieData, vk::DescriptorType::eUniformBuffer, 1, vk::ShaderStageFlagBits::eFragment},
|
||||
};
|
||||
static constexpr DescriptorSetTemplate s_perDrawTemplate(s_perDrawBindings);
|
||||
// The binding declarations themselves live in the header (MaterialSetBindings /
|
||||
// PerDrawSetBindings) because the dynamic-offset layout is derived from them there.
|
||||
static constexpr DescriptorSetTemplate s_materialTemplate(MaterialSetBindings);
|
||||
static constexpr DescriptorSetTemplate s_perDrawTemplate(PerDrawSetBindings);
|
||||
|
||||
// ========== Static uniform binding mappings ==========
|
||||
|
||||
@@ -77,8 +68,17 @@ static constexpr VulkanDescriptorManager::UniformBindingEntry s_perDrawUBOs[] =
|
||||
|
||||
// ========== DescriptorFallbacks ==========
|
||||
|
||||
const vk::DescriptorImageInfo& DescriptorFallbacks::getImage(vk::ImageViewType t) const
|
||||
const vk::DescriptorImageInfo& DescriptorFallbacks::getImage(vk::ImageViewType t, bool depthCompare) const
|
||||
{
|
||||
if (depthCompare) {
|
||||
// A shadow sampler must get a depth view whatever its view type. Only the 2D-array
|
||||
// shape exists today (GlobalBinding::ShadowMap); assert rather than hand back a
|
||||
// color view, which is the exact validation error this fallback removes.
|
||||
Assertion(t == vk::ImageViewType::e2DArray,
|
||||
"DescriptorFallbacks::getImage: no depth fallback for ImageViewType %d", static_cast<int>(t));
|
||||
return shadowMap;
|
||||
}
|
||||
|
||||
switch (t) {
|
||||
case vk::ImageViewType::e2D: return texture2D;
|
||||
case vk::ImageViewType::e2DArray: return texture2DArray;
|
||||
@@ -92,12 +92,21 @@ const vk::DescriptorImageInfo& DescriptorFallbacks::getImage(vk::ImageViewType t
|
||||
|
||||
// ========== DescriptorWriter template-based methods ==========
|
||||
|
||||
void DescriptorWriter::writeSet(vk::DescriptorSet set, const DescriptorSetTemplate& tmpl)
|
||||
void DescriptorWriter::writeSet(DescriptorSetIndex setIndex, vk::DescriptorSet set)
|
||||
{
|
||||
Assert(m_fallbacks);
|
||||
|
||||
const DescriptorSetTemplate& tmpl = VulkanDescriptorManager::getSetTemplate(setIndex);
|
||||
|
||||
// Clear binding slots for this set
|
||||
m_bindingSlots = {};
|
||||
m_currentSet = setIndex;
|
||||
|
||||
// Dynamic offsets are consumed in binding order, so assign slots as the template is walked
|
||||
// (templates list bindings in ascending binding number). This must stay the same rule
|
||||
// dynamic_offset_slot() applies — hence advancing by b.count, not by one binding.
|
||||
uint32_t nextDynIndex = 0;
|
||||
m_dynOffsets[static_cast<size_t>(setIndex)] = {};
|
||||
|
||||
for (const auto& b : tmpl) {
|
||||
Assert(m_writeCount < MAX_WRITES);
|
||||
@@ -113,13 +122,19 @@ void DescriptorWriter::writeSet(vk::DescriptorSet set, const DescriptorSetTempla
|
||||
auto& slot = m_bindingSlots[b.binding];
|
||||
slot.count = b.count;
|
||||
slot.viewType = b.viewType;
|
||||
slot.depthCompare = b.depthCompare;
|
||||
|
||||
bool isImage = (b.type == vk::DescriptorType::eCombinedImageSampler);
|
||||
bool isAccelStruct = (b.type == vk::DescriptorType::eAccelerationStructureKHR);
|
||||
if (b.type == vk::DescriptorType::eUniformBufferDynamic) {
|
||||
Assert(nextDynIndex + b.count <= MAX_DYNAMIC_OFFSETS_PER_SET);
|
||||
slot.dynIndex = static_cast<int>(nextDynIndex);
|
||||
nextDynIndex += b.count;
|
||||
}
|
||||
if (isImage) {
|
||||
Assert(m_imageInfoCount + b.count <= MAX_IMAGE_INFOS);
|
||||
auto* dst = &m_imageInfos[m_imageInfoCount];
|
||||
const auto& fallbackImg = m_fallbacks->getImage(b.viewType);
|
||||
const auto& fallbackImg = m_fallbacks->getImage(b.viewType, b.depthCompare);
|
||||
for (uint32_t j = 0; j < b.count; ++j) {
|
||||
dst[j] = fallbackImg;
|
||||
}
|
||||
@@ -144,12 +159,40 @@ void DescriptorWriter::writeSet(vk::DescriptorSet set, const DescriptorSetTempla
|
||||
} else {
|
||||
Assert(m_bufferInfoCount < MAX_BUFFER_INFOS);
|
||||
m_bufferInfos[m_bufferInfoCount] = m_fallbacks->buffer;
|
||||
// The fallback buffer info starts at offset 0, which is also the base a
|
||||
// dynamic descriptor needs (its offset lives in the dynamic-offset array,
|
||||
// left at 0 here until setBuffer supplies a real one).
|
||||
w.pBufferInfo = &m_bufferInfos[m_bufferInfoCount];
|
||||
slot.bufferInfo = &m_bufferInfos[m_bufferInfoCount++];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void DescriptorWriter::bindSets(vk::CommandBuffer cmd,
|
||||
vk::PipelineLayout layout,
|
||||
DescriptorSetIndex firstSet,
|
||||
ArrayView<vk::DescriptorSet> sets)
|
||||
{
|
||||
size_t out = 0;
|
||||
for (size_t i = 0; i < sets.size; ++i) {
|
||||
const auto set = static_cast<DescriptorSetIndex>(static_cast<uint32_t>(firstSet) + i);
|
||||
Assert(static_cast<uint32_t>(set) < static_cast<uint32_t>(DescriptorSetIndex::Count));
|
||||
|
||||
const uint32_t dynCount = VulkanDescriptorManager::getDynamicOffsetCount(set);
|
||||
Assert(dynCount <= MAX_DYNAMIC_OFFSETS_PER_SET);
|
||||
for (uint32_t j = 0; j < dynCount; ++j) {
|
||||
Assert(out < m_dynOffsetScratch.size());
|
||||
m_dynOffsetScratch[out++] = m_dynOffsets[static_cast<size_t>(set)][j];
|
||||
}
|
||||
}
|
||||
|
||||
cmd.bindDescriptorSets(vk::PipelineBindPoint::eGraphics,
|
||||
layout,
|
||||
static_cast<uint32_t>(firstSet),
|
||||
vk::ArrayProxy<const vk::DescriptorSet>(static_cast<uint32_t>(sets.size), sets.data),
|
||||
vk::ArrayProxy<const uint32_t>(static_cast<uint32_t>(out), m_dynOffsetScratch.data()));
|
||||
}
|
||||
|
||||
void DescriptorWriter::flush()
|
||||
{
|
||||
if (m_writeCount > 0) {
|
||||
@@ -167,6 +210,25 @@ void DescriptorWriter::setBuffer(uint32_t binding, const vk::DescriptorBufferInf
|
||||
Assert(binding < MAX_BINDINGS_PER_SET);
|
||||
auto& slot = m_bindingSlots[binding];
|
||||
Assert(slot.bufferInfo);
|
||||
|
||||
if (slot.dynIndex >= 0) {
|
||||
// Dynamic binding: the descriptor holds only {buffer, 0, range}; the caller's
|
||||
// offset becomes the dynamic offset applied at bind time. Keeping it out of
|
||||
// the descriptor is what lets the set survive an offset-only change.
|
||||
auto& dyn = m_dynOffsets[static_cast<size_t>(m_currentSet)][static_cast<size_t>(slot.dynIndex)];
|
||||
if (info.buffer) {
|
||||
Assertion(info.offset <= static_cast<vk::DeviceSize>(UINT32_MAX),
|
||||
"Dynamic uniform buffer offset " SIZE_T_ARG " does not fit in a uint32 dynamic offset!",
|
||||
static_cast<size_t>(info.offset));
|
||||
*slot.bufferInfo = vk::DescriptorBufferInfo(info.buffer, 0, info.range);
|
||||
dyn = static_cast<uint32_t>(info.offset);
|
||||
} else {
|
||||
*slot.bufferInfo = m_fallbacks->buffer;
|
||||
dyn = 0;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (info.buffer) {
|
||||
*slot.bufferInfo = info;
|
||||
} else {
|
||||
@@ -182,7 +244,7 @@ void DescriptorWriter::setImage(uint32_t binding, const vk::DescriptorImageInfo&
|
||||
if (info.imageView) {
|
||||
*slot.imageInfo = info;
|
||||
} else {
|
||||
*slot.imageInfo = m_fallbacks->getImage(slot.viewType);
|
||||
*slot.imageInfo = m_fallbacks->getImage(slot.viewType, slot.depthCompare);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -259,6 +321,7 @@ void VulkanDescriptorManager::buildFallbacks(VulkanBufferManager* bufMgr, Vulkan
|
||||
m_fallbacks.texture2DArray = texMgr->getFallbackTextureInfo2DArray();
|
||||
m_fallbacks.textureCube = texMgr->getFallbackTextureInfoCube();
|
||||
m_fallbacks.texture3D = texMgr->getFallbackTextureInfo3D();
|
||||
m_fallbacks.shadowMap = texMgr->getFallbackShadowMapInfo();
|
||||
if (m_raytracingEnabled && rtMgr != nullptr) {
|
||||
// Seed with the permanent empty TLAS; setCurrentShadowTlas() takes over
|
||||
// once the first real TLAS is built.
|
||||
@@ -279,6 +342,23 @@ const DescriptorSetTemplate& VulkanDescriptorManager::getSetTemplate(DescriptorS
|
||||
}
|
||||
}
|
||||
|
||||
uint32_t VulkanDescriptorManager::getDynamicOffsetCount(DescriptorSetIndex setIndex)
|
||||
{
|
||||
// Constant per set (see the derived *_DYNAMIC_OFFSET_COUNT values), not a scan of the
|
||||
// template — this runs on every descriptor set bind.
|
||||
switch (setIndex) {
|
||||
case DescriptorSetIndex::Global:
|
||||
return GLOBAL_DYNAMIC_OFFSET_COUNT;
|
||||
case DescriptorSetIndex::Material:
|
||||
return MATERIAL_DYNAMIC_OFFSET_COUNT;
|
||||
case DescriptorSetIndex::PerDraw:
|
||||
return PERDRAW_DYNAMIC_OFFSET_COUNT;
|
||||
default:
|
||||
UNREACHABLE("Invalid descriptor set index %u!", static_cast<uint32_t>(setIndex));
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
vk::DescriptorSetLayout VulkanDescriptorManager::getSetLayout(DescriptorSetIndex setIndex) const
|
||||
{
|
||||
return m_setLayouts[static_cast<size_t>(setIndex)].get();
|
||||
@@ -294,22 +374,31 @@ vk::DescriptorSet VulkanDescriptorManager::allocateFrameSet(DescriptorSetIndex s
|
||||
auto& pools = m_framePools[m_currentFrame];
|
||||
++m_setsAllocatedThisFrame;
|
||||
|
||||
// Allocate through the non-throwing, non-allocating vulkan.hpp overload: this is
|
||||
// one of the hottest calls in the backend (thousands per frame), and the
|
||||
// vector-returning overload heap-allocates on every single one.
|
||||
vk::DescriptorSetAllocateInfo allocInfo;
|
||||
allocInfo.descriptorSetCount = 1;
|
||||
allocInfo.pSetLayouts = &layout;
|
||||
|
||||
// Try allocating from the last pool in the list
|
||||
if (!pools.empty()) {
|
||||
vk::DescriptorSetAllocateInfo allocInfo;
|
||||
allocInfo.descriptorPool = pools.back().get();
|
||||
allocInfo.descriptorSetCount = 1;
|
||||
allocInfo.pSetLayouts = &layout;
|
||||
|
||||
try {
|
||||
auto sets = m_device.allocateDescriptorSets(allocInfo);
|
||||
return sets[0];
|
||||
} catch (const vk::OutOfPoolMemoryError&) {
|
||||
// Pool exhausted, fall through to create a new one
|
||||
vk::DescriptorSet set;
|
||||
vk::Result result = m_device.allocateDescriptorSets(&allocInfo, &set);
|
||||
if (result == vk::Result::eSuccess) {
|
||||
return set;
|
||||
}
|
||||
if (result == vk::Result::eErrorOutOfPoolMemory) {
|
||||
nprintf(("vulkan", "VulkanDescriptorManager: frame pool exhausted, allocating an additional pool chunk\n"));
|
||||
} catch (const vk::FragmentedPoolError&) {
|
||||
// Pool fragmented, fall through to create a new one
|
||||
} else if (result == vk::Result::eErrorFragmentedPool) {
|
||||
nprintf(("vulkan", "VulkanDescriptorManager: frame pool fragmented, allocating an additional pool chunk\n"));
|
||||
} else {
|
||||
nprintf(("vulkan",
|
||||
"VulkanDescriptorManager: descriptor set allocation failed (%s)\n",
|
||||
vk::to_string(result).c_str()));
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -318,18 +407,17 @@ vk::DescriptorSet VulkanDescriptorManager::allocateFrameSet(DescriptorSetIndex s
|
||||
nprintf(("vulkandescriptor", "VulkanDescriptorManager: Grew frame %u pool count to %zu\n",
|
||||
m_currentFrame, pools.size()));
|
||||
|
||||
vk::DescriptorSetAllocateInfo allocInfo;
|
||||
allocInfo.descriptorPool = pools.back().get();
|
||||
allocInfo.descriptorSetCount = 1;
|
||||
allocInfo.pSetLayouts = &layout;
|
||||
|
||||
try {
|
||||
auto sets = m_device.allocateDescriptorSets(allocInfo);
|
||||
return sets[0];
|
||||
} catch (const vk::SystemError& e) {
|
||||
nprintf(("vulkan", "VulkanDescriptorManager: Failed to allocate frame descriptor set after pool growth: %s\n", e.what()));
|
||||
vk::DescriptorSet set;
|
||||
vk::Result result = m_device.allocateDescriptorSets(&allocInfo, &set);
|
||||
if (result != vk::Result::eSuccess) {
|
||||
nprintf(("vulkan",
|
||||
"VulkanDescriptorManager: Failed to allocate frame descriptor set after pool growth: %s\n",
|
||||
vk::to_string(result).c_str()));
|
||||
return {};
|
||||
}
|
||||
return set;
|
||||
}
|
||||
|
||||
void VulkanDescriptorManager::beginFrame()
|
||||
@@ -403,9 +491,10 @@ vk::UniqueDescriptorPool VulkanDescriptorManager::createFramePool()
|
||||
// constants in VulkanDescriptorManager.h. Additional pool chunks are created
|
||||
// automatically when one is exhausted.
|
||||
SCP_vector<vk::DescriptorPoolSize> poolSizes = {
|
||||
{ vk::DescriptorType::eUniformBuffer, MAX_UNIFORM_BUFFERS_PER_POOL },
|
||||
{ vk::DescriptorType::eCombinedImageSampler, MAX_SAMPLERS_PER_POOL },
|
||||
{ vk::DescriptorType::eStorageBuffer, MAX_SETS_PER_POOL },
|
||||
{vk::DescriptorType::eUniformBuffer, MAX_UNIFORM_BUFFERS_PER_POOL},
|
||||
{vk::DescriptorType::eUniformBufferDynamic, MAX_DYNAMIC_UNIFORM_BUFFERS_PER_POOL},
|
||||
{vk::DescriptorType::eCombinedImageSampler, MAX_SAMPLERS_PER_POOL},
|
||||
{vk::DescriptorType::eStorageBuffer, MAX_SETS_PER_POOL},
|
||||
};
|
||||
if (m_raytracingEnabled) {
|
||||
poolSizes.emplace_back(vk::DescriptorType::eAccelerationStructureKHR, MAX_SETS_PER_POOL);
|
||||
|
||||
@@ -22,11 +22,18 @@ struct DescriptorBindingTemplate {
|
||||
uint32_t count; // 1 for most, 16 for texture array
|
||||
vk::ShaderStageFlags stages;
|
||||
vk::ImageViewType viewType; // only meaningful for eCombinedImageSampler
|
||||
// True when the shader declares this binding as a shadow sampler (samplerXShadow) and
|
||||
// therefore samples it with depth comparison. Such a binding needs a depth-format
|
||||
// fallback view: a color view fails the format-feature check in
|
||||
// VUID-vkCmdDraw-None-06479 even on draws that never read the descriptor.
|
||||
bool depthCompare;
|
||||
|
||||
constexpr DescriptorBindingTemplate(uint32_t binding_, vk::DescriptorType type_,
|
||||
uint32_t count_, vk::ShaderStageFlags stages_,
|
||||
vk::ImageViewType viewType_ = vk::ImageViewType::e2D)
|
||||
: binding(binding_), type(type_), count(count_), stages(stages_), viewType(viewType_) {}
|
||||
vk::ImageViewType viewType_ = vk::ImageViewType::e2D,
|
||||
bool depthCompare_ = false)
|
||||
: binding(binding_), type(type_), count(count_), stages(stages_), viewType(viewType_),
|
||||
depthCompare(depthCompare_) {}
|
||||
};
|
||||
|
||||
struct DescriptorSetTemplate : ArrayView<DescriptorBindingTemplate> {
|
||||
@@ -40,6 +47,12 @@ struct DescriptorFallbacks {
|
||||
vk::DescriptorImageInfo textureCube;
|
||||
vk::DescriptorImageInfo texture3D;
|
||||
|
||||
// Depth-format (SHADOW_DEPTH_FORMAT) 1x1 2D-array view plus a compare-enabled sampler,
|
||||
// for bindings declared as samplerXShadow. Used whenever no real shadow map is bound:
|
||||
// shadows turned off entirely, and also every frame before VulkanShadowMap::init()
|
||||
// runs its lazy first-use setup.
|
||||
vk::DescriptorImageInfo shadowMap;
|
||||
|
||||
// Unlike the fields above (fixed dummy defaults, set once by buildFallbacks()),
|
||||
// this is refreshed every frame by VulkanDescriptorManager::setCurrentShadowTlas()
|
||||
// to the live shadow TLAS -- or the permanent 0-instance fallback before the
|
||||
@@ -49,72 +62,7 @@ struct DescriptorFallbacks {
|
||||
// up automatically, so callers never need to bind it explicitly.
|
||||
vk::AccelerationStructureKHR shadowTlas;
|
||||
|
||||
const vk::DescriptorImageInfo& getImage(vk::ImageViewType t) const;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Stack-allocated batch writer for descriptor set updates.
|
||||
*
|
||||
* Usage: reset() + writeSet() (pre-fills all bindings with fallbacks)
|
||||
* + setBuffer/setImage overrides for real data + flush().
|
||||
*/
|
||||
class DescriptorWriter {
|
||||
public:
|
||||
// Fixed capacities for the stack-allocated staging arrays below — NOT Vulkan
|
||||
// hardware limits. They bound a single vkUpdateDescriptorSets batch (one
|
||||
// writeSet), sized to comfortably cover the largest descriptor set layout the
|
||||
// engine builds. Because the backing arrays live on the stack (see m_writes
|
||||
// etc.), the numbers carry no runtime cost beyond that fixed footprint; the
|
||||
// only failure mode is under-sizing, which is Assert-guarded at fill time in
|
||||
// DescriptorWriter::writeSet (e.g. `Assert(m_writeCount < MAX_WRITES)`) so an
|
||||
// oversized layout trips loudly in debug rather than silently overflowing.
|
||||
// Raise these if a future set layout legitimately needs more bindings/infos.
|
||||
static constexpr uint32_t MAX_WRITES = 32; // distinct bindings written per batch
|
||||
static constexpr uint32_t MAX_BUFFER_INFOS = 20; // buffer descriptors staged per batch
|
||||
static constexpr uint32_t MAX_IMAGE_INFOS = 24; // image/sampler descriptors staged per batch
|
||||
static constexpr uint32_t MAX_ACCEL_STRUCT_INFOS = 2; // TLAS descriptors staged per batch (RT)
|
||||
static constexpr uint32_t MAX_BINDINGS_PER_SET = 16; // highest binding number addressable in a set
|
||||
|
||||
void reset(vk::Device device, const DescriptorFallbacks& fallbacks) {
|
||||
m_device = device;
|
||||
m_fallbacks = &fallbacks;
|
||||
m_writeCount = 0;
|
||||
m_bufferInfoCount = 0;
|
||||
m_imageInfoCount = 0;
|
||||
m_accelStructInfoCount = 0;
|
||||
}
|
||||
|
||||
void writeSet(vk::DescriptorSet set, const DescriptorSetTemplate& tmpl);
|
||||
|
||||
void setBuffer(uint32_t binding, const vk::DescriptorBufferInfo& info);
|
||||
void setImage(uint32_t binding, const vk::DescriptorImageInfo& info);
|
||||
void setImageArray(uint32_t binding, ArrayView<vk::DescriptorImageInfo> infos);
|
||||
|
||||
void flush(); // defined in VulkanDescriptorManager.cpp (reports write stats to the manager)
|
||||
|
||||
private:
|
||||
// Per-binding lookup for the current writeSet, indexed by binding number.
|
||||
// Populated by writeSet, used by setBuffer/setImage/setImageArray for O(1) access.
|
||||
struct BindingSlot {
|
||||
vk::DescriptorBufferInfo* bufferInfo = nullptr; // non-null for buffer bindings
|
||||
vk::DescriptorImageInfo* imageInfo = nullptr; // non-null for image bindings
|
||||
uint32_t count = 0; // descriptor count (1 or 16 for arrays)
|
||||
vk::ImageViewType viewType = vk::ImageViewType::e2D; // for fallback lookup
|
||||
};
|
||||
|
||||
vk::Device m_device;
|
||||
const DescriptorFallbacks* m_fallbacks = nullptr;
|
||||
|
||||
std::array<vk::WriteDescriptorSet, MAX_WRITES> m_writes;
|
||||
std::array<vk::DescriptorBufferInfo, MAX_BUFFER_INFOS> m_bufferInfos;
|
||||
std::array<vk::DescriptorImageInfo, MAX_IMAGE_INFOS> m_imageInfos;
|
||||
std::array<vk::AccelerationStructureKHR, MAX_ACCEL_STRUCT_INFOS> m_accelStructInfos;
|
||||
std::array<vk::WriteDescriptorSetAccelerationStructureKHR, MAX_ACCEL_STRUCT_INFOS> m_asWriteInfos;
|
||||
std::array<BindingSlot, MAX_BINDINGS_PER_SET> m_bindingSlots;
|
||||
uint32_t m_writeCount = 0;
|
||||
uint32_t m_bufferInfoCount = 0;
|
||||
uint32_t m_imageInfoCount = 0;
|
||||
uint32_t m_accelStructInfoCount = 0;
|
||||
const vk::DescriptorImageInfo& getImage(vk::ImageViewType t, bool depthCompare) const;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -143,18 +91,22 @@ 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
|
||||
// ModelData and ShadowMapData are *dynamic* UBOs: their offset moves every draw
|
||||
// while the rest of the set is unchanged, so keeping the offset out of the
|
||||
// descriptor is what makes the set cacheable across draws.
|
||||
namespace MaterialBinding {
|
||||
static constexpr uint32_t ModelData = 0; // UBO: model/material data
|
||||
static constexpr uint32_t TextureArray = 1; // sampler2D[16]: material textures
|
||||
static constexpr uint32_t DecalGlobals = 2; // UBO: decal globals
|
||||
static constexpr uint32_t TransformSSBO = 3; // SSBO: batched transforms
|
||||
static constexpr uint32_t DepthMap = 4; // sampler2D: depth (soft particles)
|
||||
static constexpr uint32_t SceneColor = 5; // sampler2D: scene color (distortion)
|
||||
static constexpr uint32_t DistortionMap = 6; // sampler2D: distortion texture
|
||||
static constexpr uint32_t ShadowMapData = 7; // UBO: shadow map generation per-draw data (shadow_render_list)
|
||||
static constexpr uint32_t ModelData = 0; // dynamic UBO: model/material data
|
||||
static constexpr uint32_t TextureArray = 1; // sampler2D[16]: material textures
|
||||
static constexpr uint32_t DecalGlobals = 2; // UBO: decal globals
|
||||
static constexpr uint32_t TransformSSBO = 3; // SSBO: batched transforms
|
||||
static constexpr uint32_t DepthMap = 4; // sampler2D: depth (soft particles)
|
||||
static constexpr uint32_t SceneColor = 5; // sampler2D: scene color (distortion)
|
||||
static constexpr uint32_t DistortionMap = 6; // sampler2D: distortion texture
|
||||
static constexpr uint32_t ShadowMapData = 7; // dynamic UBO: shadow map generation per-draw data (shadow_render_list)
|
||||
}
|
||||
|
||||
// Texture array slot indices (elements within MaterialBinding::TextureArray)
|
||||
@@ -169,14 +121,257 @@ namespace TextureSlot {
|
||||
}
|
||||
|
||||
// PerDraw Set (Set 2) bindings — per-draw-call data
|
||||
// GenericData and Matrices are *dynamic* UBOs, for the same reason as the
|
||||
// Material set's ModelData.
|
||||
namespace PerDrawBinding {
|
||||
static constexpr uint32_t GenericData = 0; // UBO: generic shader data
|
||||
static constexpr uint32_t Matrices = 1; // UBO: transform matrices
|
||||
static constexpr uint32_t NanoVGData = 2; // UBO: NanoVG data
|
||||
static constexpr uint32_t DecalInfo = 3; // UBO: per-decal info
|
||||
static constexpr uint32_t MovieData = 4; // UBO: movie playback data
|
||||
static constexpr uint32_t GenericData = 0; // dynamic UBO: generic shader data
|
||||
static constexpr uint32_t Matrices = 1; // dynamic UBO: transform matrices
|
||||
static constexpr uint32_t NanoVGData = 2; // UBO: NanoVG data
|
||||
static constexpr uint32_t DecalInfo = 3; // UBO: per-decal info
|
||||
static constexpr uint32_t MovieData = 4; // UBO: movie playback data
|
||||
}
|
||||
|
||||
// ========== Set layout templates ==========
|
||||
//
|
||||
// These describe the fixed set layouts. Everything downstream that needs to know
|
||||
// which bindings are dynamic, how many there are, or which dynamic-offset slot a
|
||||
// given binding occupies is derived from them below rather than restated, so the
|
||||
// declarations here are the only place that ordering exists.
|
||||
|
||||
inline constexpr DescriptorBindingTemplate MaterialSetBindings[] = {
|
||||
{MaterialBinding::ModelData,
|
||||
vk::DescriptorType::eUniformBufferDynamic,
|
||||
1,
|
||||
vk::ShaderStageFlagBits::eVertex | vk::ShaderStageFlagBits::eFragment},
|
||||
{MaterialBinding::TextureArray,
|
||||
vk::DescriptorType::eCombinedImageSampler,
|
||||
16,
|
||||
vk::ShaderStageFlagBits::eFragment,
|
||||
vk::ImageViewType::e2DArray},
|
||||
{MaterialBinding::DecalGlobals,
|
||||
vk::DescriptorType::eUniformBuffer,
|
||||
1,
|
||||
vk::ShaderStageFlagBits::eVertex | vk::ShaderStageFlagBits::eFragment},
|
||||
{MaterialBinding::TransformSSBO, vk::DescriptorType::eStorageBuffer, 1, vk::ShaderStageFlagBits::eVertex},
|
||||
{MaterialBinding::DepthMap, vk::DescriptorType::eCombinedImageSampler, 1, vk::ShaderStageFlagBits::eFragment},
|
||||
{MaterialBinding::SceneColor, vk::DescriptorType::eCombinedImageSampler, 1, vk::ShaderStageFlagBits::eFragment},
|
||||
{MaterialBinding::DistortionMap, vk::DescriptorType::eCombinedImageSampler, 1, vk::ShaderStageFlagBits::eFragment},
|
||||
{MaterialBinding::ShadowMapData, vk::DescriptorType::eUniformBufferDynamic, 1, vk::ShaderStageFlagBits::eVertex},
|
||||
};
|
||||
|
||||
inline constexpr DescriptorBindingTemplate PerDrawSetBindings[] = {
|
||||
{PerDrawBinding::GenericData,
|
||||
vk::DescriptorType::eUniformBufferDynamic,
|
||||
1,
|
||||
vk::ShaderStageFlagBits::eVertex | vk::ShaderStageFlagBits::eFragment},
|
||||
{PerDrawBinding::Matrices,
|
||||
vk::DescriptorType::eUniformBufferDynamic,
|
||||
1,
|
||||
vk::ShaderStageFlagBits::eVertex | vk::ShaderStageFlagBits::eFragment},
|
||||
{PerDrawBinding::NanoVGData,
|
||||
vk::DescriptorType::eUniformBuffer,
|
||||
1,
|
||||
vk::ShaderStageFlagBits::eVertex | vk::ShaderStageFlagBits::eFragment},
|
||||
{PerDrawBinding::DecalInfo,
|
||||
vk::DescriptorType::eUniformBuffer,
|
||||
1,
|
||||
vk::ShaderStageFlagBits::eVertex | vk::ShaderStageFlagBits::eFragment},
|
||||
{PerDrawBinding::MovieData, vk::DescriptorType::eUniformBuffer, 1, vk::ShaderStageFlagBits::eFragment},
|
||||
};
|
||||
|
||||
// ========== Dynamic-offset layout, derived from the templates above ==========
|
||||
//
|
||||
// vkCmdBindDescriptorSets consumes a set's dynamic offsets in ascending binding
|
||||
// order, one entry per eUniformBufferDynamic descriptor. Both the count and the
|
||||
// slot each binding occupies are computed from the set templates so a call site
|
||||
// can never disagree with the layout it is binding against — reordering a template
|
||||
// moves the derived slots with it instead of silently feeding a shader the wrong
|
||||
// uniforms.
|
||||
|
||||
template <size_t N>
|
||||
constexpr uint32_t dynamic_offset_count(const DescriptorBindingTemplate (&bindings)[N])
|
||||
{
|
||||
uint32_t count = 0;
|
||||
for (size_t i = 0; i < N; ++i) {
|
||||
if (bindings[i].type == vk::DescriptorType::eUniformBufferDynamic) {
|
||||
count += bindings[i].count;
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
// Returned by dynamic_offset_slot() when the binding is not a dynamic descriptor of the set.
|
||||
constexpr uint32_t DYNAMIC_SLOT_INVALID = ~0u;
|
||||
|
||||
/**
|
||||
* @brief Position of a dynamic binding within its set's dynamic-offset array
|
||||
*
|
||||
* If @c binding is not a dynamic binding of the given set, @c DYNAMIC_SLOT_INVALID is returned.
|
||||
* Every use below pairs its constant with a static_assert against that sentinel, so a mistyped or
|
||||
* non-dynamic binding constant is a compile error rather than a silent 0 — these constants index
|
||||
* fixed-size dynamic-offset arrays at the bind call sites, so a leaked sentinel would be an
|
||||
* out-of-bounds write.
|
||||
*
|
||||
* Do not "clean this up" back into a `throw` on the not-found path: MSVC rejects a constexpr
|
||||
* function whose flow analysis can reach a throw (C3615), even when no evaluation takes that path.
|
||||
*/
|
||||
template <size_t N>
|
||||
constexpr uint32_t dynamic_offset_slot(const DescriptorBindingTemplate (&bindings)[N], uint32_t binding)
|
||||
{
|
||||
uint32_t slot = 0;
|
||||
for (size_t i = 0; i < N; ++i) {
|
||||
if (bindings[i].type != vk::DescriptorType::eUniformBufferDynamic) {
|
||||
continue;
|
||||
}
|
||||
if (bindings[i].binding == binding) {
|
||||
return slot;
|
||||
}
|
||||
slot += bindings[i].count;
|
||||
}
|
||||
return DYNAMIC_SLOT_INVALID;
|
||||
}
|
||||
|
||||
// The Global set declares no dynamic bindings; it contributes nothing to a bind call.
|
||||
constexpr uint32_t GLOBAL_DYNAMIC_OFFSET_COUNT = 0;
|
||||
constexpr uint32_t MATERIAL_DYNAMIC_OFFSET_COUNT = dynamic_offset_count(MaterialSetBindings);
|
||||
constexpr uint32_t PERDRAW_DYNAMIC_OFFSET_COUNT = dynamic_offset_count(PerDrawSetBindings);
|
||||
|
||||
namespace MaterialDynamicSlot {
|
||||
constexpr uint32_t ModelData = dynamic_offset_slot(MaterialSetBindings, MaterialBinding::ModelData);
|
||||
constexpr uint32_t ShadowMapData = dynamic_offset_slot(MaterialSetBindings, MaterialBinding::ShadowMapData);
|
||||
static_assert(ModelData != DYNAMIC_SLOT_INVALID,
|
||||
"MaterialBinding::ModelData is not a dynamic descriptor of the Material set.");
|
||||
static_assert(ShadowMapData != DYNAMIC_SLOT_INVALID,
|
||||
"MaterialBinding::ShadowMapData is not a dynamic descriptor of the Material set.");
|
||||
}
|
||||
|
||||
namespace PerDrawDynamicSlot {
|
||||
constexpr uint32_t GenericData = dynamic_offset_slot(PerDrawSetBindings, PerDrawBinding::GenericData);
|
||||
constexpr uint32_t Matrices = dynamic_offset_slot(PerDrawSetBindings, PerDrawBinding::Matrices);
|
||||
static_assert(GenericData != DYNAMIC_SLOT_INVALID,
|
||||
"PerDrawBinding::GenericData is not a dynamic descriptor of the PerDraw set.");
|
||||
static_assert(Matrices != DYNAMIC_SLOT_INVALID,
|
||||
"PerDrawBinding::Matrices is not a dynamic descriptor of the PerDraw set.");
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Stack-allocated batch writer for descriptor set updates.
|
||||
*
|
||||
* Usage: reset() + writeSet() (pre-fills all bindings with fallbacks)
|
||||
* + setBuffer/setImage overrides for real data + flush().
|
||||
*
|
||||
* Bindings declared as eUniformBufferDynamic get their offset split out of the
|
||||
* descriptor and into the dynamic-offset array (see dynamicOffsets()): setBuffer
|
||||
* writes {buffer, 0, range} into the descriptor and stashes the caller's offset.
|
||||
* That is what lets a set whose only per-draw change is a UBO offset be written
|
||||
* once and rebound many times. Callers must hand dynamicOffsets()/
|
||||
* VulkanDescriptorManager::getDynamicOffsetCount() for a set to
|
||||
* vkCmdBindDescriptorSets when binding it.
|
||||
*/
|
||||
class DescriptorWriter {
|
||||
public:
|
||||
// Fixed capacities for the stack-allocated staging arrays below — NOT Vulkan
|
||||
// hardware limits. They bound a single vkUpdateDescriptorSets batch (one
|
||||
// writeSet), sized to comfortably cover the largest descriptor set layout the
|
||||
// engine builds. Because the backing arrays live on the stack (see m_writes
|
||||
// etc.), the numbers carry no runtime cost beyond that fixed footprint; the
|
||||
// only failure mode is under-sizing, which is Assert-guarded at fill time in
|
||||
// DescriptorWriter::writeSet (e.g. `Assert(m_writeCount < MAX_WRITES)`) so an
|
||||
// oversized layout trips loudly in debug rather than silently overflowing.
|
||||
// Raise these if a future set layout legitimately needs more bindings/infos.
|
||||
static constexpr uint32_t MAX_WRITES = 32; // distinct bindings written per batch
|
||||
static constexpr uint32_t MAX_BUFFER_INFOS = 20; // buffer descriptors staged per batch
|
||||
static constexpr uint32_t MAX_IMAGE_INFOS = 24; // image/sampler descriptors staged per batch
|
||||
static constexpr uint32_t MAX_ACCEL_STRUCT_INFOS = 2; // TLAS descriptors staged per batch (RT)
|
||||
static constexpr uint32_t MAX_BINDINGS_PER_SET = 16; // highest binding number addressable in a set
|
||||
// Most dynamic (eUniformBufferDynamic) bindings any one set layout declares. Derived from
|
||||
// the templates rather than counted by hand, so adding a dynamic binding resizes the
|
||||
// storage that holds its offset automatically.
|
||||
static constexpr uint32_t MAX_DYNAMIC_OFFSETS_PER_SET =
|
||||
MATERIAL_DYNAMIC_OFFSET_COUNT > PERDRAW_DYNAMIC_OFFSET_COUNT ? MATERIAL_DYNAMIC_OFFSET_COUNT
|
||||
: PERDRAW_DYNAMIC_OFFSET_COUNT;
|
||||
|
||||
void reset(vk::Device device, const DescriptorFallbacks& fallbacks) {
|
||||
m_device = device;
|
||||
m_fallbacks = &fallbacks;
|
||||
m_writeCount = 0;
|
||||
m_bufferInfoCount = 0;
|
||||
m_imageInfoCount = 0;
|
||||
m_accelStructInfoCount = 0;
|
||||
m_dynOffsets = {};
|
||||
}
|
||||
|
||||
void writeSet(DescriptorSetIndex setIndex, vk::DescriptorSet set);
|
||||
|
||||
void setBuffer(uint32_t binding, const vk::DescriptorBufferInfo& info);
|
||||
void setImage(uint32_t binding, const vk::DescriptorImageInfo& info);
|
||||
void setImageArray(uint32_t binding, ArrayView<vk::DescriptorImageInfo> infos);
|
||||
|
||||
/**
|
||||
* @brief Dynamic offsets accumulated for a single set, ordered by binding number.
|
||||
*
|
||||
* Only meaningful for a set this writer actually wrote; a set reused from a
|
||||
* memoization cache was not written here, so its caller owns the offsets.
|
||||
* Covers the set layout's dynamic descriptor count with room to spare (offsets
|
||||
* for bindings left at their fallback stay 0). Views stable per-set storage —
|
||||
* valid until the next writeSet() of that set.
|
||||
*/
|
||||
ArrayView<uint32_t> dynamicOffsets(DescriptorSetIndex setIndex) const
|
||||
{
|
||||
return m_dynOffsets[static_cast<size_t>(setIndex)];
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Binds a contiguous run of sets in one vkCmdBindDescriptorSets call
|
||||
*
|
||||
* Concatenates the run's dynamic offsets in set order, which is the layout
|
||||
* vkCmdBindDescriptorSets expects: each set contributes exactly its layout's
|
||||
* dynamic descriptor count (a set declaring none contributes nothing, rather
|
||||
* than padding). @c sets must list the sets for @c firstSet onwards, in order.
|
||||
*
|
||||
* Binding here rather than handing the offsets back to the caller keeps the
|
||||
* concatenation buffer's lifetime contained: it is writer-owned scratch, reused
|
||||
* by the next call, and never outlives the command it was built for.
|
||||
*/
|
||||
void bindSets(vk::CommandBuffer cmd,
|
||||
vk::PipelineLayout layout,
|
||||
DescriptorSetIndex firstSet,
|
||||
ArrayView<vk::DescriptorSet> sets);
|
||||
|
||||
void flush(); // defined in VulkanDescriptorManager.cpp (reports write stats to the manager)
|
||||
|
||||
private:
|
||||
// Per-binding lookup for the current writeSet, indexed by binding number.
|
||||
// Populated by writeSet, used by setBuffer/setImage/setImageArray for O(1) access.
|
||||
struct BindingSlot {
|
||||
vk::DescriptorBufferInfo* bufferInfo = nullptr; // non-null for buffer bindings
|
||||
vk::DescriptorImageInfo* imageInfo = nullptr; // non-null for image bindings
|
||||
uint32_t count = 0; // descriptor count (1 or 16 for arrays)
|
||||
vk::ImageViewType viewType = vk::ImageViewType::e2D; // for fallback lookup
|
||||
bool depthCompare = false; // for fallback lookup
|
||||
int dynIndex = -1; // slot in m_dynOffsets, or -1 if not dynamic
|
||||
};
|
||||
|
||||
vk::Device m_device;
|
||||
const DescriptorFallbacks* m_fallbacks = nullptr;
|
||||
|
||||
std::array<vk::WriteDescriptorSet, MAX_WRITES> m_writes;
|
||||
std::array<vk::DescriptorBufferInfo, MAX_BUFFER_INFOS> m_bufferInfos;
|
||||
std::array<vk::DescriptorImageInfo, MAX_IMAGE_INFOS> m_imageInfos;
|
||||
std::array<vk::AccelerationStructureKHR, MAX_ACCEL_STRUCT_INFOS> m_accelStructInfos;
|
||||
std::array<vk::WriteDescriptorSetAccelerationStructureKHR, MAX_ACCEL_STRUCT_INFOS> m_asWriteInfos;
|
||||
std::array<BindingSlot, MAX_BINDINGS_PER_SET> m_bindingSlots;
|
||||
std::array<std::array<uint32_t, MAX_DYNAMIC_OFFSETS_PER_SET>, static_cast<size_t>(DescriptorSetIndex::Count)>
|
||||
m_dynOffsets{};
|
||||
// Scratch used by bindSets() to concatenate a run of sets' offsets.
|
||||
std::array<uint32_t, static_cast<size_t>(DescriptorSetIndex::Count) * MAX_DYNAMIC_OFFSETS_PER_SET>
|
||||
m_dynOffsetScratch{};
|
||||
DescriptorSetIndex m_currentSet = DescriptorSetIndex::Global;
|
||||
uint32_t m_writeCount = 0;
|
||||
uint32_t m_bufferInfoCount = 0;
|
||||
uint32_t m_imageInfoCount = 0;
|
||||
uint32_t m_accelStructInfoCount = 0;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Manages Vulkan descriptor sets, pools, and layouts
|
||||
@@ -195,8 +390,19 @@ public:
|
||||
// larger chunks waste memory, smaller chunks allocate pools more often.
|
||||
// MAX_SETS_PER_POOL supports ~330 draw calls (3 sets each) per chunk; the per-
|
||||
// type multipliers cover the worst-case bindings a single set can request.
|
||||
// One chunk now covers a dense scene outright: with the dynamic-UBO descriptor
|
||||
// memoization in applyMaterial/renderShadowDraw, a big asteroid field at ~1350
|
||||
// model draws settles at ~250 sets/frame, so growth is back to being the rare
|
||||
// case it was meant to be. (The same scene needed ~2500 sets/frame before that.)
|
||||
// Deliberately left at the original size rather than raised: the per-frame
|
||||
// growth this used to log was a symptom of the churn, not of undersizing, and
|
||||
// FSO targets low-end GPUs where a larger pool is real wasted memory.
|
||||
static constexpr uint32_t MAX_SETS_PER_POOL = 1024;
|
||||
static constexpr uint32_t MAX_UNIFORM_BUFFERS_PER_POOL = MAX_SETS_PER_POOL * 9; // up to 9 UBOs per draw
|
||||
// Worst case per set is 3 static UBOs (Global, PerDraw) and 2 dynamic (Material:
|
||||
// ModelData + ShadowMapData; PerDraw: GenericData + Matrices). Total UBO
|
||||
// capacity per chunk is slightly below what this was before the dynamic split.
|
||||
static constexpr uint32_t MAX_UNIFORM_BUFFERS_PER_POOL = MAX_SETS_PER_POOL * 5; // static UBOs
|
||||
static constexpr uint32_t MAX_DYNAMIC_UNIFORM_BUFFERS_PER_POOL = MAX_SETS_PER_POOL * 2; // dynamic UBOs
|
||||
static constexpr uint32_t MAX_SAMPLERS_PER_POOL = MAX_SETS_PER_POOL * 16; // up to 16 samplers per material set
|
||||
|
||||
VulkanDescriptorManager() = default;
|
||||
@@ -254,6 +460,15 @@ public:
|
||||
*/
|
||||
static const DescriptorSetTemplate& getSetTemplate(DescriptorSetIndex setIndex);
|
||||
|
||||
/**
|
||||
* @brief Number of dynamic (eUniformBufferDynamic) descriptors in a set layout
|
||||
*
|
||||
* vkCmdBindDescriptorSets requires exactly this many entries in pDynamicOffsets,
|
||||
* so every bind site must agree with the layout. VulkanStateTracker asserts on
|
||||
* it; raw cmd.bindDescriptorSets() callers should pass the matching count.
|
||||
*/
|
||||
static uint32_t getDynamicOffsetCount(DescriptorSetIndex setIndex);
|
||||
|
||||
/**
|
||||
* @brief Get descriptor set layout for a given set index
|
||||
*/
|
||||
|
||||
@@ -686,8 +686,12 @@ void VulkanDrawManager::renderModel(model_material* material_info, indexed_verte
|
||||
stateTracker->getCommandBuffer().drawIndexed(dp.indexCount, 1, dp.firstIndex, dp.baseVertex, 0);
|
||||
}
|
||||
|
||||
void VulkanDrawManager::renderShadowDraw(gr_buffer_handle ubo_handle, size_t ubo_offset, size_t ubo_size,
|
||||
vertex_buffer* buffer, indexed_vertex_source* vert_src, size_t texi) const
|
||||
void VulkanDrawManager::renderShadowDraw(gr_buffer_handle ubo_handle,
|
||||
size_t ubo_offset,
|
||||
size_t ubo_size,
|
||||
vertex_buffer* buffer,
|
||||
indexed_vertex_source* vert_src,
|
||||
size_t texi)
|
||||
{
|
||||
if (!buffer || !vert_src) {
|
||||
return;
|
||||
@@ -754,51 +758,99 @@ void VulkanDrawManager::renderShadowDraw(gr_buffer_handle ubo_handle, size_t ubo
|
||||
// (bound separately, per-frame, via shadow_cascade_params_bind) both live in
|
||||
// the fixed 3-tier layout alongside the batched-submodel transform buffer;
|
||||
// no material textures are needed for depth-only rendering.
|
||||
//
|
||||
// All three sets are memoized across the shadow pass (see m_cachedShadow).
|
||||
// ShadowMapData is a dynamic binding, so the only thing that changes from one
|
||||
// shadow draw to the next -- its UBO offset -- rides in the dynamic-offset array
|
||||
// and leaves the set contents untouched. Without that this path allocated and
|
||||
// fully rewrote three descriptor sets per shadow draw, times the cascade count.
|
||||
{
|
||||
DescriptorWriter writer;
|
||||
writer.reset(descManager->getDevice(), descManager->getFallbacks());
|
||||
|
||||
vk::DescriptorSet globalSet = descManager->allocateFrameSet(DescriptorSetIndex::Global);
|
||||
Assert(globalSet);
|
||||
writer.writeSet(globalSet, VulkanDescriptorManager::getSetTemplate(DescriptorSetIndex::Global));
|
||||
ShadowSetInputs shadowInputs;
|
||||
{
|
||||
const auto& pending = getPendingUniformBinding(static_cast<size_t>(uniform_block_type::ShadowCascadeParams));
|
||||
if (pending.valid) {
|
||||
vk::Buffer buf = bufferManager->getVkBuffer(pending.bufferHandle);
|
||||
const auto& cascade =
|
||||
getPendingUniformBinding(static_cast<size_t>(uniform_block_type::ShadowCascadeParams));
|
||||
shadowInputs.cascadeHandle = cascade.bufferHandle.value();
|
||||
shadowInputs.cascadeOffset = cascade.offset;
|
||||
shadowInputs.cascadeSize = cascade.size;
|
||||
// Whether the binding resolves *this frame*, not merely whether one was ever made:
|
||||
// the block lives in a streaming buffer, so a binding from an earlier frame no longer
|
||||
// points at anything (see bindPendingUBOs in applyMaterial). Folded into the key so a
|
||||
// cached set written with the placeholder buffer cannot be reused once the real
|
||||
// binding lands -- the bump offset alone can repeat from one frame to the next.
|
||||
shadowInputs.cascadeValid =
|
||||
cascade.valid && bufferManager->isFrameAllocCurrent(cascade.bufferHandle);
|
||||
shadowInputs.shadowDataHandle = ubo_handle.value();
|
||||
shadowInputs.shadowDataSize = ubo_size; // offset is dynamic, so not part of the key
|
||||
auto& tf = g_transformBuffers[descManager->getCurrentFrame()];
|
||||
if (tf.buffer && tf.lastUploadSize > 0) {
|
||||
shadowInputs.transformBuffer = tf.buffer;
|
||||
shadowInputs.transformOffset = tf.lastUploadOffset;
|
||||
shadowInputs.transformSize = tf.lastUploadSize;
|
||||
}
|
||||
}
|
||||
|
||||
if (!m_cachedShadow.hits(shadowInputs)) {
|
||||
DescriptorWriter writer;
|
||||
writer.reset(descManager->getDevice(), descManager->getFallbacks());
|
||||
|
||||
ShadowSets sets;
|
||||
|
||||
sets.global = descManager->allocateFrameSet(DescriptorSetIndex::Global);
|
||||
Assert(sets.global);
|
||||
writer.writeSet(DescriptorSetIndex::Global, sets.global);
|
||||
if (shadowInputs.cascadeValid) {
|
||||
vk::Buffer buf =
|
||||
bufferManager->getVkBufferForBinding(gr_buffer_handle(shadowInputs.cascadeHandle));
|
||||
if (buf) {
|
||||
writer.setBuffer(GlobalBinding::ShadowCascadeParams, {buf, pending.offset, pending.size});
|
||||
writer.setBuffer(GlobalBinding::ShadowCascadeParams,
|
||||
{buf, shadowInputs.cascadeOffset, shadowInputs.cascadeSize});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
vk::DescriptorSet materialSet = descManager->allocateFrameSet(DescriptorSetIndex::Material);
|
||||
Assert(materialSet);
|
||||
writer.writeSet(materialSet, VulkanDescriptorManager::getSetTemplate(DescriptorSetIndex::Material));
|
||||
{
|
||||
vk::Buffer buf = bufferManager->getVkBuffer(ubo_handle);
|
||||
if (buf) {
|
||||
writer.setBuffer(MaterialBinding::ShadowMapData,
|
||||
{buf, static_cast<vk::DeviceSize>(ubo_offset), static_cast<vk::DeviceSize>(ubo_size)});
|
||||
sets.material = descManager->allocateFrameSet(DescriptorSetIndex::Material);
|
||||
Assert(sets.material);
|
||||
writer.writeSet(DescriptorSetIndex::Material, sets.material);
|
||||
{
|
||||
vk::Buffer buf = bufferManager->getVkBuffer(ubo_handle);
|
||||
if (buf) {
|
||||
// Offset 0: the real offset is supplied per draw as a dynamic offset.
|
||||
writer.setBuffer(MaterialBinding::ShadowMapData, {buf, 0, static_cast<vk::DeviceSize>(ubo_size)});
|
||||
}
|
||||
}
|
||||
}
|
||||
{
|
||||
uint32_t tfIdx = descManager->getCurrentFrame();
|
||||
auto& tf = g_transformBuffers[tfIdx];
|
||||
if (tf.buffer && tf.lastUploadSize > 0) {
|
||||
writer.setBuffer(MaterialBinding::TransformSSBO, {tf.buffer,
|
||||
static_cast<vk::DeviceSize>(tf.lastUploadOffset),
|
||||
static_cast<vk::DeviceSize>(tf.lastUploadSize)});
|
||||
if (shadowInputs.transformBuffer) {
|
||||
writer.setBuffer(MaterialBinding::TransformSSBO,
|
||||
{shadowInputs.transformBuffer,
|
||||
static_cast<vk::DeviceSize>(shadowInputs.transformOffset),
|
||||
static_cast<vk::DeviceSize>(shadowInputs.transformSize)});
|
||||
}
|
||||
|
||||
sets.perDraw = descManager->allocateFrameSet(DescriptorSetIndex::PerDraw);
|
||||
Assert(sets.perDraw);
|
||||
writer.writeSet(DescriptorSetIndex::PerDraw, sets.perDraw);
|
||||
writer.flush();
|
||||
|
||||
m_cachedShadow.store(sets, shadowInputs);
|
||||
}
|
||||
|
||||
vk::DescriptorSet perDrawSet = descManager->allocateFrameSet(DescriptorSetIndex::PerDraw);
|
||||
Assert(perDrawSet);
|
||||
writer.writeSet(perDrawSet, VulkanDescriptorManager::getSetTemplate(DescriptorSetIndex::PerDraw));
|
||||
writer.flush();
|
||||
// applyMaterial's caches deliberately are NOT invalidated here. They record
|
||||
// what a descriptor set *contains*, and the shadow pass allocates its own
|
||||
// sets rather than overwriting theirs. Which set is currently bound is
|
||||
// tracked separately by VulkanStateTracker, and this path binds through it,
|
||||
// so the next applyMaterial cache hit still rebinds its set correctly. (That
|
||||
// is what separates this from the ImGui case invalidateDrawStateCaches()
|
||||
// exists for: ImGui binds on the command buffer behind the tracker's back.)
|
||||
|
||||
stateTracker->bindDescriptorSet(DescriptorSetIndex::Global, globalSet);
|
||||
stateTracker->bindDescriptorSet(DescriptorSetIndex::Material, materialSet);
|
||||
stateTracker->bindDescriptorSet(DescriptorSetIndex::PerDraw, perDrawSet);
|
||||
// ShadowMapData is the one thing that moves per shadow draw; every other dynamic binding
|
||||
// on this path holds the fallback buffer and so must stay at offset 0.
|
||||
uint32_t materialDynOffsets[MATERIAL_DYNAMIC_OFFSET_COUNT] = {};
|
||||
materialDynOffsets[MaterialDynamicSlot::ShadowMapData] = static_cast<uint32_t>(ubo_offset);
|
||||
|
||||
static constexpr uint32_t perDrawDynOffsets[PERDRAW_DYNAMIC_OFFSET_COUNT] = {};
|
||||
|
||||
const auto& sets = m_cachedShadow.payload();
|
||||
stateTracker->bindDescriptorSet(DescriptorSetIndex::Global, sets.global);
|
||||
stateTracker->bindDescriptorSet(DescriptorSetIndex::Material, sets.material, materialDynOffsets);
|
||||
stateTracker->bindDescriptorSet(DescriptorSetIndex::PerDraw, sets.perDraw, perDrawDynOffsets);
|
||||
}
|
||||
|
||||
vk::Buffer vbuffer = bufferManager->getVkBuffer(vert_src->Vbuffer_handle);
|
||||
@@ -906,9 +958,18 @@ void VulkanDrawManager::clearStates()
|
||||
stateTracker->setDepthBias(0.0f, 0.0f);
|
||||
stateTracker->setLineWidth(1.0f);
|
||||
|
||||
// Clear pending uniform bindings
|
||||
clearPendingUniformBindings();
|
||||
|
||||
// NOTE: Do NOT clear the pending uniform bindings here. gr_opengl_clear_states() leaves
|
||||
// glBindBufferRange() alone, so under OpenGL a uniform block stays bound across a
|
||||
// clear_states(); dropping them here made Vulkan diverge. It matters because
|
||||
// model_render_immediate() ends with gr_clear_states(), so anything bound once for a whole
|
||||
// pass -- ShadowCascadeParams above all, which nothing re-binds per model -- survived only
|
||||
// until the first model finished drawing. Every model after that fell back to the zeroed
|
||||
// placeholder UBO, which reads as rtShadowSampleCount == 0 and collapses
|
||||
// traceShadowRayCone() to a single hard ray. A binding that has outlived what it points at is
|
||||
// handled where it is consumed: the descriptor writers resolve it through
|
||||
// getVkBufferForBinding(), which hands back the zeroed placeholder buffer for a deleted buffer
|
||||
// or for a streaming sub-allocation from an earlier frame.
|
||||
//
|
||||
// NOTE: Do NOT call resetClip() here. OpenGL's gr_opengl_clear_states() does
|
||||
// not reset the clip region, and callers (e.g. model_render_immediate) rely on
|
||||
// the clip/offset state surviving through clear_states for subsequent 2D draws.
|
||||
@@ -936,16 +997,6 @@ void VulkanDrawManager::setPendingUniformBinding(uniform_block_type blockType, g
|
||||
}
|
||||
}
|
||||
|
||||
void VulkanDrawManager::clearPendingUniformBindings()
|
||||
{
|
||||
for (auto& binding : m_pendingUniformBindings) {
|
||||
binding.valid = false;
|
||||
binding.bufferHandle = gr_buffer_handle();
|
||||
binding.offset = 0;
|
||||
binding.size = 0;
|
||||
}
|
||||
}
|
||||
|
||||
void VulkanDrawManager::resetFrameStats()
|
||||
{
|
||||
m_frameStats = {};
|
||||
@@ -956,10 +1007,9 @@ void VulkanDrawManager::resetFrameStats()
|
||||
// the frame's first applyMaterial().
|
||||
m_cachedGlobalSet = nullptr;
|
||||
m_globalSetDirty = true;
|
||||
m_cachedMaterialSet = nullptr;
|
||||
m_cachedMaterialValid = false;
|
||||
m_cachedPerDrawSet = nullptr;
|
||||
m_cachedPerDrawValid = false;
|
||||
m_cachedMaterial.invalidate();
|
||||
m_cachedPerDraw.invalidate();
|
||||
m_cachedShadow.invalidate();
|
||||
}
|
||||
|
||||
void VulkanDrawManager::printFrameStats()
|
||||
@@ -1334,13 +1384,20 @@ bool VulkanDrawManager::applyMaterial(material* mat, primitive_type prim_type, v
|
||||
DescriptorWriter writer;
|
||||
writer.reset(descManager->getDevice(), descManager->getFallbacks());
|
||||
|
||||
// Bind pending UBOs for a given descriptor set
|
||||
// Bind pending UBOs for a given descriptor set.
|
||||
//
|
||||
// getVkBufferForBinding() rather than getVkBuffer(): a binding survives the frame it was
|
||||
// made in (clearStates() deliberately leaves it alone, see there), but a streaming
|
||||
// buffer's sub-allocation does not, so a block bound once per frame -- ShadowCascadeParams
|
||||
// -- is stale for every draw that precedes this frame's bind. Resolving to nullptr puts
|
||||
// the zeroed placeholder buffer in the descriptor, which is what those draws would have
|
||||
// got before anything bound the block at all.
|
||||
auto bindPendingUBOs = [&](DescriptorSetIndex targetSet) {
|
||||
for (const auto& entry : VulkanDescriptorManager::getUniformBindings(targetSet)) {
|
||||
vk::DescriptorBufferInfo bufInfo;
|
||||
const auto& pending = m_pendingUniformBindings[static_cast<size_t>(entry.blockType)];
|
||||
if (pending.valid) {
|
||||
vk::Buffer buf = bufferManager->getVkBuffer(pending.bufferHandle);
|
||||
vk::Buffer buf = bufferManager->getVkBufferForBinding(pending.bufferHandle);
|
||||
if (buf) {
|
||||
bufInfo = vk::DescriptorBufferInfo(buf, pending.offset, pending.size);
|
||||
}
|
||||
@@ -1349,6 +1406,28 @@ bool VulkanDrawManager::applyMaterial(material* mat, primitive_type prim_type, v
|
||||
}
|
||||
};
|
||||
|
||||
// Dynamic offsets for the Material/PerDraw sets, ordered by binding number.
|
||||
// Computed here rather than read back from the writer because the whole point
|
||||
// of the dynamic bindings is that these change on draws where the set itself
|
||||
// is reused from cache and never written.
|
||||
const auto dynOffsetOf = [&](uniform_block_type blockType) -> uint32_t {
|
||||
const auto& pending = m_pendingUniformBindings[static_cast<size_t>(blockType)];
|
||||
if (!pending.valid || !bufferManager->getVkBufferForBinding(pending.bufferHandle)) {
|
||||
return 0; // falls back to the dummy buffer, which is bound at offset 0
|
||||
}
|
||||
return static_cast<uint32_t>(pending.offset);
|
||||
};
|
||||
// Zero-initialized, then filled by name: MaterialDynamicSlot::ShadowMapData is left at 0
|
||||
// because ShadowMapData is never bound on this path, so its descriptor still holds the
|
||||
// fallback buffer -- bound as {buffer, 0, FALLBACK_UNIFORM_BUFFER_SIZE}, i.e. spanning the
|
||||
// whole buffer, so any non-zero dynamic offset would push offset+range past the end of it.
|
||||
uint32_t materialDynOffsets[MATERIAL_DYNAMIC_OFFSET_COUNT] = {};
|
||||
materialDynOffsets[MaterialDynamicSlot::ModelData] = dynOffsetOf(uniform_block_type::ModelData);
|
||||
|
||||
uint32_t perDrawDynOffsets[PERDRAW_DYNAMIC_OFFSET_COUNT] = {};
|
||||
perDrawDynOffsets[PerDrawDynamicSlot::GenericData] = dynOffsetOf(uniform_block_type::GenericData);
|
||||
perDrawDynOffsets[PerDrawDynamicSlot::Matrices] = dynOffsetOf(uniform_block_type::Matrices);
|
||||
|
||||
// Set 0: Global (memoized per frame — see m_cachedGlobalSet). Rebuilt only
|
||||
// when a Global input changed: a pending Global UBO (m_globalSetDirty set in
|
||||
// setPendingUniformBinding), the shadow TLAS (invalidateGlobalSet from
|
||||
@@ -1360,17 +1439,18 @@ bool VulkanDrawManager::applyMaterial(material* mat, primitive_type prim_type, v
|
||||
if (m_globalSetDirty || !m_cachedGlobalSet || shadowReady != m_cachedGlobalHadShadow) {
|
||||
m_cachedGlobalSet = descManager->allocateFrameSet(DescriptorSetIndex::Global);
|
||||
Assert(m_cachedGlobalSet);
|
||||
writer.writeSet(m_cachedGlobalSet, VulkanDescriptorManager::getSetTemplate(DescriptorSetIndex::Global));
|
||||
writer.writeSet(DescriptorSetIndex::Global, m_cachedGlobalSet);
|
||||
bindPendingUBOs(DescriptorSetIndex::Global);
|
||||
if (shadowReady) {
|
||||
writer.setImage(GlobalBinding::ShadowMap, pp->getShadowTextureInfo());
|
||||
writer.setImage(GlobalBinding::ShadowMapRaw, pp->getShadowRawTextureInfo());
|
||||
}
|
||||
m_cachedGlobalHadShadow = shadowReady;
|
||||
m_globalSetDirty = false;
|
||||
}
|
||||
vk::DescriptorSet globalSet = m_cachedGlobalSet;
|
||||
|
||||
// Set 1: Material (previous-set memoized — see m_cachedMaterialSet).
|
||||
// Set 1: Material (previous-set memoized — see m_cachedMaterial).
|
||||
// Snapshot every input; reuse the cached set only on an exact match.
|
||||
MaterialSetInputs matInputs;
|
||||
matInputs.texHandles[0] = mat->get_texture_map(TM_BASE_TYPE);
|
||||
@@ -1397,23 +1477,28 @@ bool VulkanDrawManager::applyMaterial(material* mat, primitive_type prim_type, v
|
||||
matInputs.sceneColorInfo = m_sceneColorInfo;
|
||||
matInputs.distMapInfo = m_distMapInfo;
|
||||
{
|
||||
// ModelData's offset is deliberately NOT part of the key: it is a dynamic
|
||||
// binding, so an offset-only change is carried by materialDynOffsets and
|
||||
// leaves the set's contents identical. Buffer and size still matter --
|
||||
// both live in the descriptor.
|
||||
const auto& md = m_pendingUniformBindings[static_cast<size_t>(uniform_block_type::ModelData)];
|
||||
const auto& dg = m_pendingUniformBindings[static_cast<size_t>(uniform_block_type::DecalGlobals)];
|
||||
matInputs.uboHandle[0] = md.bufferHandle.value(); matInputs.uboOffset[0] = md.offset;
|
||||
matInputs.uboHandle[0] = md.bufferHandle.value();
|
||||
matInputs.uboOffset[0] = 0;
|
||||
matInputs.uboSize[0] = md.size; matInputs.uboValid[0] = md.valid;
|
||||
matInputs.uboHandle[1] = dg.bufferHandle.value(); matInputs.uboOffset[1] = dg.offset;
|
||||
matInputs.uboSize[1] = dg.size; matInputs.uboValid[1] = dg.valid;
|
||||
}
|
||||
|
||||
vk::DescriptorSet materialSet;
|
||||
if (m_cachedMaterialValid && m_cachedMaterialSet && matInputs == m_cachedMaterialInputs) {
|
||||
if (m_cachedMaterial.hits(matInputs)) {
|
||||
// Identical to the previous draw: reuse the set (skips allocation, the
|
||||
// template write, texture resolution/upload, and the override/UBO writes).
|
||||
materialSet = m_cachedMaterialSet;
|
||||
materialSet = m_cachedMaterial.payload();
|
||||
} else {
|
||||
materialSet = descManager->allocateFrameSet(DescriptorSetIndex::Material);
|
||||
Assert(materialSet);
|
||||
writer.writeSet(materialSet, VulkanDescriptorManager::getSetTemplate(DescriptorSetIndex::Material));
|
||||
writer.writeSet(DescriptorSetIndex::Material, materialSet);
|
||||
bindPendingUBOs(DescriptorSetIndex::Material);
|
||||
if (matInputs.transformBuffer) {
|
||||
writer.setBuffer(MaterialBinding::TransformSSBO,
|
||||
@@ -1425,43 +1510,48 @@ bool VulkanDrawManager::applyMaterial(material* mat, primitive_type prim_type, v
|
||||
writer.setImage(MaterialBinding::SceneColor, m_sceneColorInfo);
|
||||
writer.setImage(MaterialBinding::DistortionMap, m_distMapInfo);
|
||||
bindMaterialTextures(mat, &writer);
|
||||
m_cachedMaterialSet = materialSet;
|
||||
m_cachedMaterialInputs = matInputs;
|
||||
m_cachedMaterialValid = true;
|
||||
m_cachedMaterial.store(materialSet, matInputs);
|
||||
}
|
||||
|
||||
// Set 2: PerDraw (previous-set memoized — see m_cachedPerDrawSet)
|
||||
// Set 2: PerDraw (previous-set memoized — see m_cachedPerDraw)
|
||||
PerDrawSetInputs pdInputs;
|
||||
{
|
||||
static constexpr uniform_block_type pdTypes[NUM_PERDRAW_UBOS] = {
|
||||
uniform_block_type::GenericData, uniform_block_type::Matrices,
|
||||
uniform_block_type::NanoVGData, uniform_block_type::DecalInfo,
|
||||
uniform_block_type::MovieData};
|
||||
struct PerDrawUbo {
|
||||
uniform_block_type blockType;
|
||||
uint32_t binding;
|
||||
};
|
||||
static constexpr PerDrawUbo pdUbos[NUM_PERDRAW_UBOS] = {
|
||||
{uniform_block_type::GenericData, PerDrawBinding::GenericData},
|
||||
{uniform_block_type::Matrices, PerDrawBinding::Matrices},
|
||||
{uniform_block_type::NanoVGData, PerDrawBinding::NanoVGData},
|
||||
{uniform_block_type::DecalInfo, PerDrawBinding::DecalInfo},
|
||||
{uniform_block_type::MovieData, PerDrawBinding::MovieData}};
|
||||
for (int i = 0; i < NUM_PERDRAW_UBOS; ++i) {
|
||||
const auto& p = m_pendingUniformBindings[static_cast<size_t>(pdTypes[i])];
|
||||
const auto& p = m_pendingUniformBindings[static_cast<size_t>(pdUbos[i].blockType)];
|
||||
pdInputs.uboHandle[i] = p.bufferHandle.value();
|
||||
pdInputs.uboOffset[i] = p.offset;
|
||||
// The offset of a dynamic binding rides in perDrawDynOffsets and is not part
|
||||
// of the set's contents.
|
||||
const bool dynamic = dynamic_offset_slot(PerDrawSetBindings, pdUbos[i].binding) != DYNAMIC_SLOT_INVALID;
|
||||
pdInputs.uboOffset[i] = dynamic ? 0 : p.offset;
|
||||
pdInputs.uboSize[i] = p.size;
|
||||
pdInputs.uboValid[i] = p.valid;
|
||||
}
|
||||
}
|
||||
|
||||
vk::DescriptorSet perDrawSet;
|
||||
if (m_cachedPerDrawValid && m_cachedPerDrawSet && pdInputs == m_cachedPerDrawInputs) {
|
||||
perDrawSet = m_cachedPerDrawSet;
|
||||
if (m_cachedPerDraw.hits(pdInputs)) {
|
||||
perDrawSet = m_cachedPerDraw.payload();
|
||||
} else {
|
||||
perDrawSet = descManager->allocateFrameSet(DescriptorSetIndex::PerDraw);
|
||||
Assert(perDrawSet);
|
||||
writer.writeSet(perDrawSet, VulkanDescriptorManager::getSetTemplate(DescriptorSetIndex::PerDraw));
|
||||
writer.writeSet(DescriptorSetIndex::PerDraw, perDrawSet);
|
||||
bindPendingUBOs(DescriptorSetIndex::PerDraw);
|
||||
m_cachedPerDrawSet = perDrawSet;
|
||||
m_cachedPerDrawInputs = pdInputs;
|
||||
m_cachedPerDrawValid = true;
|
||||
m_cachedPerDraw.store(perDrawSet, pdInputs);
|
||||
}
|
||||
writer.flush();
|
||||
stateTracker->bindDescriptorSet(DescriptorSetIndex::Global, globalSet);
|
||||
stateTracker->bindDescriptorSet(DescriptorSetIndex::Material, materialSet);
|
||||
stateTracker->bindDescriptorSet(DescriptorSetIndex::PerDraw, perDrawSet);
|
||||
stateTracker->bindDescriptorSet(DescriptorSetIndex::Material, materialSet, materialDynOffsets);
|
||||
stateTracker->bindDescriptorSet(DescriptorSetIndex::PerDraw, perDrawSet, perDrawDynOffsets);
|
||||
}
|
||||
|
||||
// Update tracked state for FSO compatibility
|
||||
|
||||
@@ -28,6 +28,72 @@ struct PendingUniformBinding {
|
||||
bool valid = false;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief A descriptor set, or a fixed group of them, remembered alongside the inputs it was built from
|
||||
*
|
||||
* Writing and allocating a descriptor set per draw is the cost this exists to avoid: consecutive
|
||||
* draws very often want a set identical to the one before it. This is a *previous-only* cache --
|
||||
* any difference in the inputs rebuilds, so a hit is never stale -- and it holds for one frame,
|
||||
* because the sets are frame-pool allocated (see resetFrameStats()).
|
||||
*
|
||||
* @tparam Inputs everything the set's contents are derived from; needs operator==. Anything carried
|
||||
* as a dynamic offset must be left OUT of it, since an offset-only change rides in the
|
||||
* dynamic-offset array and leaves the set itself identical -- that is what makes the hit
|
||||
* rate worth having.
|
||||
* @tparam Payload what a hit hands back. A parameter because the shadow pass memoizes all three of
|
||||
* its sets against a single key, while applyMaterial() memoizes one set per key.
|
||||
*/
|
||||
template <typename Inputs, typename Payload = vk::DescriptorSet>
|
||||
class MemoizedDescriptorSet {
|
||||
public:
|
||||
/**
|
||||
* @brief Whether @p inputs matches what the stored payload was built from
|
||||
*
|
||||
* A true result means payload() can be rebound as-is, with no allocation or write.
|
||||
*/
|
||||
bool hits(const Inputs& inputs) const { return m_valid && m_inputs == inputs; }
|
||||
|
||||
/**
|
||||
* @brief The stored sets. Only meaningful after hits() returned true.
|
||||
*/
|
||||
const Payload& payload() const { return m_payload; }
|
||||
|
||||
void store(const Payload& payload, const Inputs& inputs)
|
||||
{
|
||||
// Never memoize an incomplete payload. allocateFrameSet() hands back a null handle when
|
||||
// the pool is exhausted, and the Assert() that catches it at the call site is compiled
|
||||
// out of release builds -- caching that would turn one failed allocation into every
|
||||
// later draw binding a null set, long after the pressure that caused it passed.
|
||||
if (!payload) {
|
||||
invalidate();
|
||||
return;
|
||||
}
|
||||
|
||||
m_payload = payload;
|
||||
m_inputs = inputs;
|
||||
m_valid = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Forget the stored sets
|
||||
*
|
||||
* Every caller has the same reason: the handles can no longer be assumed current, either
|
||||
* because the frame pool that owns them was reset or because something rebound descriptor
|
||||
* sets behind applyMaterial()'s back. Clearing the payload as well as the flag means a
|
||||
* missing hits() check binds a null set (which asserts) rather than a recycled one.
|
||||
*/
|
||||
void invalidate()
|
||||
{
|
||||
m_payload = Payload{};
|
||||
m_valid = false;
|
||||
}
|
||||
|
||||
private:
|
||||
Payload m_payload{};
|
||||
Inputs m_inputs;
|
||||
bool m_valid = false;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Handles Vulkan draw command recording
|
||||
*
|
||||
@@ -212,7 +278,7 @@ class VulkanDrawManager {
|
||||
size_t ubo_size,
|
||||
vertex_buffer* buffer,
|
||||
indexed_vertex_source* vert_src,
|
||||
size_t texi) const;
|
||||
size_t texi);
|
||||
|
||||
/**
|
||||
* @brief Draw a unit sphere with the given material
|
||||
@@ -286,6 +352,26 @@ class VulkanDrawManager {
|
||||
*/
|
||||
void invalidateGlobalSet() { m_globalSetDirty = true; }
|
||||
|
||||
/**
|
||||
* @brief Invalidate every memoized descriptor-set cache (Global + Material + PerDraw)
|
||||
*
|
||||
* These previous-set caches (see the class comments above m_cachedGlobalSet /
|
||||
* m_cachedMaterial / m_cachedPerDraw) assume nothing rebinds a *different*
|
||||
* descriptor set on the command buffer between applyMaterial() calls behind their
|
||||
* back. Code that renders directly on the command buffer without going through
|
||||
* applyMaterial (currently: ImGui's Vulkan backend, drawing into the same active
|
||||
* composition pass) breaks that assumption -- call this right afterward, alongside
|
||||
* VulkanStateTracker::invalidateExternalBindings(), so the next applyMaterial() rebuilds
|
||||
* and rebinds every set instead of trusting stale cached handles.
|
||||
*/
|
||||
void invalidateDrawStateCaches()
|
||||
{
|
||||
m_globalSetDirty = true;
|
||||
m_cachedMaterial.invalidate();
|
||||
m_cachedPerDraw.invalidate();
|
||||
m_cachedShadow.invalidate();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Get current texture addressing mode
|
||||
*/
|
||||
@@ -313,11 +399,6 @@ class VulkanDrawManager {
|
||||
vk::DeviceSize offset,
|
||||
vk::DeviceSize size);
|
||||
|
||||
/**
|
||||
* @brief Clear all pending uniform bindings
|
||||
*/
|
||||
void clearPendingUniformBindings();
|
||||
|
||||
/**
|
||||
* @brief Get a pending uniform binding by block type index
|
||||
*/
|
||||
@@ -449,6 +530,14 @@ class VulkanDrawManager {
|
||||
mutable FrameStats m_frameStats;
|
||||
int m_frameStatsFrameNum = 0;
|
||||
|
||||
public:
|
||||
/**
|
||||
* @brief Read-only access to this frame's diagnostic counters, e.g. for the ImGui profiler
|
||||
* overlay's -gr_debug section (see printFrameStats() for the nprintf equivalent)
|
||||
*/
|
||||
const FrameStats& getFrameStats() const { return m_frameStats; }
|
||||
|
||||
private:
|
||||
// First-N debug-log counter for on-demand texture binds; a member rather
|
||||
// than a function-local static so it resets on renderer restart. mutable because
|
||||
// bindMaterialTextures is const. Gates nprintf spam only.
|
||||
@@ -510,11 +599,8 @@ class VulkanDrawManager {
|
||||
imgEq(depthInfo, o.depthInfo) && imgEq(sceneColorInfo, o.sceneColorInfo) &&
|
||||
imgEq(distMapInfo, o.distMapInfo);
|
||||
}
|
||||
bool operator!=(const MaterialSetInputs& o) const { return !(*this == o); }
|
||||
};
|
||||
vk::DescriptorSet m_cachedMaterialSet = nullptr;
|
||||
MaterialSetInputs m_cachedMaterialInputs;
|
||||
bool m_cachedMaterialValid = false;
|
||||
MemoizedDescriptorSet<MaterialSetInputs> m_cachedMaterial;
|
||||
|
||||
// ---- PerDraw (Set 2) previous-set memoization ----
|
||||
// PerDraw holds only the pending PerDraw UBO bindings (GenericData, Matrices,
|
||||
@@ -536,9 +622,43 @@ class VulkanDrawManager {
|
||||
return true;
|
||||
}
|
||||
};
|
||||
vk::DescriptorSet m_cachedPerDrawSet = nullptr;
|
||||
PerDrawSetInputs m_cachedPerDrawInputs;
|
||||
bool m_cachedPerDrawValid = false;
|
||||
MemoizedDescriptorSet<PerDrawSetInputs> m_cachedPerDraw;
|
||||
|
||||
// ---- Shadow-pass descriptor memoization (renderShadowDraw) ----
|
||||
// The shadow pass doesn't go through applyMaterial, so it gets its own cache.
|
||||
// All three of its sets are constant across the whole pass: the only per-draw
|
||||
// input, the ShadowMapData UBO offset, is a dynamic binding and therefore not
|
||||
// part of the set contents. Reset per frame with the rest (resetFrameStats).
|
||||
struct ShadowSetInputs {
|
||||
int cascadeHandle = -1;
|
||||
vk::DeviceSize cascadeOffset = 0;
|
||||
vk::DeviceSize cascadeSize = 0;
|
||||
bool cascadeValid = false;
|
||||
int shadowDataHandle = -1;
|
||||
size_t shadowDataSize = 0;
|
||||
vk::Buffer transformBuffer = nullptr;
|
||||
size_t transformOffset = 0;
|
||||
size_t transformSize = 0;
|
||||
|
||||
bool operator==(const ShadowSetInputs& o) const
|
||||
{
|
||||
return cascadeHandle == o.cascadeHandle && cascadeOffset == o.cascadeOffset &&
|
||||
cascadeSize == o.cascadeSize && cascadeValid == o.cascadeValid &&
|
||||
shadowDataHandle == o.shadowDataHandle && shadowDataSize == o.shadowDataSize &&
|
||||
transformBuffer == o.transformBuffer && transformOffset == o.transformOffset &&
|
||||
transformSize == o.transformSize;
|
||||
}
|
||||
};
|
||||
// The whole set of three, memoized together: they share one key, so they are only ever all
|
||||
// valid or all rebuilt.
|
||||
struct ShadowSets {
|
||||
vk::DescriptorSet global;
|
||||
vk::DescriptorSet material;
|
||||
vk::DescriptorSet perDraw;
|
||||
|
||||
explicit operator bool() const { return global && material && perDraw; }
|
||||
};
|
||||
MemoizedDescriptorSet<ShadowSetInputs, ShadowSets> m_cachedShadow;
|
||||
|
||||
// Texture overrides for material bindings 4-6.
|
||||
vk::DescriptorImageInfo m_depthTextureInfo; // binding 4: depth/position for soft particles
|
||||
|
||||
@@ -214,9 +214,13 @@ void vulkan_scene_texture_begin()
|
||||
|
||||
auto* renderer = getRendererInstance();
|
||||
|
||||
// Switch to HDR scene render pass when post-processing is enabled
|
||||
// Switch to HDR scene render pass when post-processing is enabled. The post-processor's targets
|
||||
// are sized for the main viewport's swap chain, so this stays off anywhere else -- qtFRED's
|
||||
// briefing map renders through brief_render_map() and never opens a scene-texture scope, so
|
||||
// nothing is lost by that today.
|
||||
auto* pp = getPostProcessor();
|
||||
if (pp && pp->isInitialized() && Gr_post_processing_enabled && !PostProcessing_override) {
|
||||
if (pp && pp->isInitialized() && Gr_post_processing_enabled && !PostProcessing_override &&
|
||||
renderer->isMainTargetCurrent()) {
|
||||
renderer->beginSceneRendering();
|
||||
High_dynamic_range = true;
|
||||
} else {
|
||||
@@ -755,12 +759,12 @@ void vulkan_calculate_irrmap()
|
||||
// Set 0: Global (all fallback)
|
||||
vk::DescriptorSet globalSet = descManager->allocateFrameSet(DescriptorSetIndex::Global);
|
||||
Assert(globalSet);
|
||||
writer.writeSet(globalSet, VulkanDescriptorManager::getSetTemplate(DescriptorSetIndex::Global));
|
||||
writer.writeSet(DescriptorSetIndex::Global, globalSet);
|
||||
|
||||
// Set 1: Material (envmap cubemap at element 0 of texture array)
|
||||
vk::DescriptorSet materialSet = descManager->allocateFrameSet(DescriptorSetIndex::Material);
|
||||
Assert(materialSet);
|
||||
writer.writeSet(materialSet, VulkanDescriptorManager::getSetTemplate(DescriptorSetIndex::Material));
|
||||
writer.writeSet(DescriptorSetIndex::Material, materialSet);
|
||||
{
|
||||
std::array<vk::DescriptorImageInfo, VulkanDescriptorManager::MAX_TEXTURE_BINDINGS> texImages;
|
||||
// irrmap-f.sdr declares this binding as samplerCube, so every element needs a cube view
|
||||
@@ -772,14 +776,14 @@ void vulkan_calculate_irrmap()
|
||||
// Set 2: PerDraw (face UBO at binding 0)
|
||||
vk::DescriptorSet perDrawSet = descManager->allocateFrameSet(DescriptorSetIndex::PerDraw);
|
||||
Assert(perDrawSet);
|
||||
writer.writeSet(perDrawSet, VulkanDescriptorManager::getSetTemplate(DescriptorSetIndex::PerDraw));
|
||||
writer.writeSet(DescriptorSetIndex::PerDraw, perDrawSet);
|
||||
writer.setBuffer(PerDrawBinding::GenericData, {faceUBO,
|
||||
static_cast<vk::DeviceSize>(face) * UBO_SLOT_SIZE, UBO_SLOT_SIZE});
|
||||
writer.flush();
|
||||
|
||||
// Bind all descriptor sets
|
||||
cmd.bindDescriptorSets(vk::PipelineBindPoint::eGraphics, pipelineLayout,
|
||||
0, {globalSet, materialSet, perDrawSet}, {});
|
||||
const vk::DescriptorSet sets[] = {globalSet, materialSet, perDrawSet};
|
||||
writer.bindSets(cmd, pipelineLayout, DescriptorSetIndex::Global, sets);
|
||||
|
||||
// Draw fullscreen triangle
|
||||
cmd.draw(3, 1, 0, 0);
|
||||
|
||||
@@ -140,7 +140,8 @@ VmaMemoryUsage VulkanMemoryManager::toVmaUsage(MemoryUsage usage)
|
||||
}
|
||||
}
|
||||
|
||||
bool VulkanMemoryManager::allocateBufferMemory(vk::Buffer buffer, MemoryUsage usage, VulkanAllocation& allocation)
|
||||
bool VulkanMemoryManager::allocateBufferMemory(vk::Buffer buffer, MemoryUsage usage, VulkanAllocation& allocation,
|
||||
MemoryPurpose purpose)
|
||||
{
|
||||
if (!m_initialized) {
|
||||
nprintf(("vulkan", "VulkanMemoryManager::allocateBufferMemory called before initialization!\n"));
|
||||
@@ -176,14 +177,17 @@ bool VulkanMemoryManager::allocateBufferMemory(vk::Buffer buffer, MemoryUsage us
|
||||
|
||||
allocation.size = allocInfo.size;
|
||||
allocation.mappedPtr = allocInfo.pMappedData;
|
||||
allocation.purpose = purpose;
|
||||
|
||||
++m_allocationCount;
|
||||
m_totalAllocatedBytes += static_cast<size_t>(allocation.size);
|
||||
m_bytesByPurpose[static_cast<size_t>(purpose)] += static_cast<size_t>(allocation.size);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool VulkanMemoryManager::allocateImageMemory(vk::Image image, MemoryUsage usage, VulkanAllocation& allocation)
|
||||
bool VulkanMemoryManager::allocateImageMemory(vk::Image image, MemoryUsage usage, VulkanAllocation& allocation,
|
||||
MemoryPurpose purpose)
|
||||
{
|
||||
if (!m_initialized) {
|
||||
nprintf(("vulkan", "VulkanMemoryManager::allocateImageMemory called before initialization!\n"));
|
||||
@@ -219,9 +223,11 @@ bool VulkanMemoryManager::allocateImageMemory(vk::Image image, MemoryUsage usage
|
||||
|
||||
allocation.size = allocInfo.size;
|
||||
allocation.mappedPtr = allocInfo.pMappedData;
|
||||
allocation.purpose = purpose;
|
||||
|
||||
++m_allocationCount;
|
||||
m_totalAllocatedBytes += static_cast<size_t>(allocation.size);
|
||||
m_bytesByPurpose[static_cast<size_t>(purpose)] += static_cast<size_t>(allocation.size);
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -242,10 +248,12 @@ void VulkanMemoryManager::freeAllocation(VulkanAllocation& allocation)
|
||||
|
||||
--m_allocationCount;
|
||||
m_totalAllocatedBytes -= static_cast<size_t>(allocation.size);
|
||||
m_bytesByPurpose[static_cast<size_t>(allocation.purpose)] -= static_cast<size_t>(allocation.size);
|
||||
|
||||
allocation.vmaAlloc = VK_NULL_HANDLE;
|
||||
allocation.size = 0;
|
||||
allocation.mappedPtr = nullptr;
|
||||
allocation.purpose = MemoryPurpose::Unknown;
|
||||
}
|
||||
|
||||
void* VulkanMemoryManager::mapMemory(VulkanAllocation& allocation)
|
||||
|
||||
@@ -30,6 +30,19 @@ class VulkanRenderer;
|
||||
// above in lockstep, or VMA's internal assert will fire.
|
||||
constexpr uint32_t VulkanApiVersion = VK_API_VERSION_1_2;
|
||||
|
||||
/**
|
||||
* @brief What an allocation is used for, for the profiler overlay's per-purpose GPU memory
|
||||
* breakdown. Unknown covers anything not worth categorizing for that panel (transient staging
|
||||
* buffers, uniform buffers -- the latter are already tracked separately via
|
||||
* UniformBufferManager/gr_debug_stats).
|
||||
*/
|
||||
enum class MemoryPurpose {
|
||||
Unknown,
|
||||
Texture,
|
||||
Geometry,
|
||||
RenderTarget
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Memory allocation info returned when allocating GPU memory.
|
||||
*
|
||||
@@ -40,6 +53,7 @@ struct VulkanAllocation {
|
||||
VmaAllocation vmaAlloc = VK_NULL_HANDLE;
|
||||
vk::DeviceSize size = 0;
|
||||
void* mappedPtr = nullptr; // Non-null if memory is persistently mapped
|
||||
MemoryPurpose purpose = MemoryPurpose::Unknown;
|
||||
|
||||
bool isValid() const { return vmaAlloc != VK_NULL_HANDLE; }
|
||||
};
|
||||
@@ -94,18 +108,24 @@ public:
|
||||
* @param buffer The buffer to allocate memory for
|
||||
* @param usage The intended memory usage pattern
|
||||
* @param[out] allocation Output allocation info
|
||||
* @param purpose What the buffer is used for, for the per-purpose memory breakdown. Defaults
|
||||
* to Unknown for callers that don't care about that breakdown (staging buffers, etc).
|
||||
* @return true on success
|
||||
*/
|
||||
bool allocateBufferMemory(vk::Buffer buffer, MemoryUsage usage, VulkanAllocation& allocation);
|
||||
bool allocateBufferMemory(vk::Buffer buffer, MemoryUsage usage, VulkanAllocation& allocation,
|
||||
MemoryPurpose purpose = MemoryPurpose::Unknown);
|
||||
|
||||
/**
|
||||
* @brief Allocate memory for an image
|
||||
* @param image The image to allocate memory for
|
||||
* @param usage The intended memory usage pattern
|
||||
* @param[out] allocation Output allocation info
|
||||
* @param purpose What the image is used for, for the per-purpose memory breakdown. Defaults
|
||||
* to Unknown for callers that don't care about that breakdown.
|
||||
* @return true on success
|
||||
*/
|
||||
bool allocateImageMemory(vk::Image image, MemoryUsage usage, VulkanAllocation& allocation);
|
||||
bool allocateImageMemory(vk::Image image, MemoryUsage usage, VulkanAllocation& allocation,
|
||||
MemoryPurpose purpose = MemoryPurpose::Unknown);
|
||||
|
||||
/**
|
||||
* @brief Free a previous allocation
|
||||
@@ -148,6 +168,10 @@ public:
|
||||
size_t getAllocationCount() const { return m_allocationCount; }
|
||||
size_t getTotalAllocatedBytes() const { return m_totalAllocatedBytes; }
|
||||
|
||||
size_t getTextureBytes() const { return m_bytesByPurpose[static_cast<size_t>(MemoryPurpose::Texture)]; }
|
||||
size_t getGeometryBytes() const { return m_bytesByPurpose[static_cast<size_t>(MemoryPurpose::Geometry)]; }
|
||||
size_t getRenderTargetBytes() const { return m_bytesByPurpose[static_cast<size_t>(MemoryPurpose::RenderTarget)]; }
|
||||
|
||||
private:
|
||||
static VmaMemoryUsage toVmaUsage(MemoryUsage usage);
|
||||
|
||||
@@ -155,6 +179,7 @@ private:
|
||||
|
||||
size_t m_allocationCount = 0;
|
||||
size_t m_totalAllocatedBytes = 0;
|
||||
size_t m_bytesByPurpose[4] = {}; // indexed by MemoryPurpose
|
||||
|
||||
bool m_initialized = false;
|
||||
};
|
||||
|
||||
@@ -394,9 +394,26 @@ vk::UniquePipeline VulkanPipelineManager::createPipeline(const PipelineConfig& c
|
||||
}
|
||||
|
||||
// Input assembly
|
||||
//
|
||||
// Primitive restart is enabled for every topology that permits it, rather than disabled
|
||||
// outright. Metal always performs primitive restart on indexed draws and cannot turn it off, so
|
||||
// MoltenVK rejects a strip/fan pipeline that asks to disable it -- vkCreateGraphicsPipelines
|
||||
// fails with VK_ERROR_FEATURE_NOT_PRESENT ("Metal does not support disabling primitive
|
||||
// restart"). getPipeline() then returns a null pipeline and the draw is skipped, which on macOS
|
||||
// silently dropped the entire 2D/UI/HUD path (gr_bitmap and friends render quads as
|
||||
// PRIM_TYPE_TRISTRIP; see graphics/render.cpp) while leaving ImGui -- which builds its own
|
||||
// TRIANGLE_LIST pipeline -- as the only thing still on screen.
|
||||
//
|
||||
// Enabling it changes nothing for FSO's geometry: an index buffer is promoted to 32-bit as soon
|
||||
// as a mesh reaches USHRT_MAX vertices (VB_FLAG_LARGE_INDEX, see modelinterp.cpp), so 0xFFFF is
|
||||
// never a real 16-bit index, and 0xFFFFFFFF is unreachable for 32-bit ones. Neither restart
|
||||
// sentinel can collide with live index data.
|
||||
//
|
||||
// List topologies must keep it disabled: the spec only allows primitiveRestartEnable on them
|
||||
// with VK_EXT_primitive_topology_list_restart, which FSO does not request.
|
||||
vk::PipelineInputAssemblyStateCreateInfo inputAssembly;
|
||||
inputAssembly.topology = convertPrimitiveType(config.primitiveType);
|
||||
inputAssembly.primitiveRestartEnable = VK_FALSE;
|
||||
inputAssembly.primitiveRestartEnable = topologySupportsPrimitiveRestart(inputAssembly.topology);
|
||||
|
||||
// Viewport state (dynamic)
|
||||
vk::PipelineViewportStateCreateInfo viewportState;
|
||||
|
||||
@@ -121,12 +121,6 @@ public:
|
||||
*/
|
||||
vk::Pipeline getPipeline(const PipelineConfig& config, const vertex_layout& vertexLayout);
|
||||
|
||||
/**
|
||||
* @brief Destroy all cached pipelines; later getPipeline calls build them again
|
||||
* @note No submitted or recording command buffer may still use them
|
||||
*/
|
||||
void forgetAllPipelines() { m_pipelines.clear(); }
|
||||
|
||||
/**
|
||||
* @brief Get the common pipeline layout
|
||||
*
|
||||
|
||||
@@ -687,7 +687,7 @@ void VulkanPostProcessor::blitToSwapChain(vk::CommandBuffer cmd)
|
||||
// Set 1: Material — source texture at array slot 0
|
||||
vk::DescriptorSet materialSet = descriptorMgr->allocateFrameSet(DescriptorSetIndex::Material);
|
||||
Assert(materialSet);
|
||||
writer.writeSet(materialSet, VulkanDescriptorManager::getSetTemplate(DescriptorSetIndex::Material));
|
||||
writer.writeSet(DescriptorSetIndex::Material, materialSet);
|
||||
{
|
||||
std::array<vk::DescriptorImageInfo, VulkanDescriptorManager::MAX_TEXTURE_BINDINGS> texArrayInfos;
|
||||
texArrayInfos.fill(descriptorMgr->getFallbacks().texture2D);
|
||||
@@ -701,7 +701,7 @@ void VulkanPostProcessor::blitToSwapChain(vk::CommandBuffer cmd)
|
||||
// Set 2: PerDraw — tonemapping UBO (from the per-frame scratch ring)
|
||||
vk::DescriptorSet perDrawSet = descriptorMgr->allocateFrameSet(DescriptorSetIndex::PerDraw);
|
||||
Assert(perDrawSet);
|
||||
writer.writeSet(perDrawSet, VulkanDescriptorManager::getSetTemplate(DescriptorSetIndex::PerDraw));
|
||||
writer.writeSet(DescriptorSetIndex::PerDraw, perDrawSet);
|
||||
|
||||
// LDR path: passthrough (tonemapping already ran). Fallback path: live
|
||||
// parameters from the engine lighting profile.
|
||||
@@ -732,8 +732,12 @@ void VulkanPostProcessor::blitToSwapChain(vk::CommandBuffer cmd)
|
||||
{m_ctx.scratchRing.buffer(), slotOffset, m_ctx.scratchRing.slotSize()});
|
||||
}
|
||||
writer.flush();
|
||||
stateTracker->bindDescriptorSet(DescriptorSetIndex::Material, materialSet);
|
||||
stateTracker->bindDescriptorSet(DescriptorSetIndex::PerDraw, perDrawSet);
|
||||
stateTracker->bindDescriptorSet(DescriptorSetIndex::Material,
|
||||
materialSet,
|
||||
writer.dynamicOffsets(DescriptorSetIndex::Material));
|
||||
stateTracker->bindDescriptorSet(DescriptorSetIndex::PerDraw,
|
||||
perDrawSet,
|
||||
writer.dynamicOffsets(DescriptorSetIndex::PerDraw));
|
||||
|
||||
// Draw fullscreen triangle (3 vertices from gl_VertexIndex, no vertex buffer)
|
||||
cmd.draw(3, 1, 0, 0);
|
||||
@@ -800,7 +804,7 @@ void VulkanPostProcessor::encodeToSwapChainPass(vk::CommandBuffer cmd, vk::Rende
|
||||
|
||||
vk::DescriptorSet materialSet = descriptorMgr->allocateFrameSet(DescriptorSetIndex::Material);
|
||||
Assert(materialSet);
|
||||
writer.writeSet(materialSet, VulkanDescriptorManager::getSetTemplate(DescriptorSetIndex::Material));
|
||||
writer.writeSet(DescriptorSetIndex::Material, materialSet);
|
||||
{
|
||||
std::array<vk::DescriptorImageInfo, VulkanDescriptorManager::MAX_TEXTURE_BINDINGS> texArrayInfos;
|
||||
texArrayInfos.fill(descriptorMgr->getFallbacks().texture2D);
|
||||
@@ -811,7 +815,7 @@ void VulkanPostProcessor::encodeToSwapChainPass(vk::CommandBuffer cmd, vk::Rende
|
||||
|
||||
vk::DescriptorSet perDrawSet = descriptorMgr->allocateFrameSet(DescriptorSetIndex::PerDraw);
|
||||
Assert(perDrawSet);
|
||||
writer.writeSet(perDrawSet, VulkanDescriptorManager::getSetTemplate(DescriptorSetIndex::PerDraw));
|
||||
writer.writeSet(DescriptorSetIndex::PerDraw, perDrawSet);
|
||||
{
|
||||
vk::DeviceSize slotOffset =
|
||||
m_ctx.scratchRing.alloc(descriptorMgr->getCurrentFrame(), uboData, uboSize);
|
||||
@@ -820,9 +824,8 @@ void VulkanPostProcessor::encodeToSwapChainPass(vk::CommandBuffer cmd, vk::Rende
|
||||
}
|
||||
writer.flush();
|
||||
|
||||
cmd.bindDescriptorSets(vk::PipelineBindPoint::eGraphics, pipelineLayout,
|
||||
static_cast<uint32_t>(DescriptorSetIndex::Material),
|
||||
{materialSet, perDrawSet}, {});
|
||||
const vk::DescriptorSet sets[] = {materialSet, perDrawSet};
|
||||
writer.bindSets(cmd, pipelineLayout, DescriptorSetIndex::Material, sets);
|
||||
|
||||
cmd.draw(3, 1, 0, 0);
|
||||
cmd.endRenderPass();
|
||||
|
||||
@@ -204,6 +204,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 +212,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;
|
||||
@@ -1111,6 +1113,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 ==========
|
||||
|
||||
/**
|
||||
|
||||
@@ -170,7 +170,8 @@ bool VulkanBloom::createTargets()
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!m_ctx->memoryManager->allocateImageMemory(m_tex[i].image, MemoryUsage::GpuOnly, m_tex[i].allocation)) {
|
||||
if (!m_ctx->memoryManager->allocateImageMemory(
|
||||
m_tex[i].image, MemoryUsage::GpuOnly, m_tex[i].allocation, MemoryPurpose::RenderTarget)) {
|
||||
nprintf(("vulkan", "VulkanBloom: Failed to allocate bloom image %zu memory!\n", i));
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -176,13 +176,13 @@ void PostProcessContext::drawFullscreenTriangle(vk::CommandBuffer cmd, vk::Rende
|
||||
if (bindGlobalSet) {
|
||||
globalSet = descriptorMgr->allocateFrameSet(DescriptorSetIndex::Global);
|
||||
Assert(globalSet);
|
||||
writer.writeSet(globalSet, VulkanDescriptorManager::getSetTemplate(DescriptorSetIndex::Global));
|
||||
writer.writeSet(DescriptorSetIndex::Global, globalSet);
|
||||
}
|
||||
|
||||
// Set 1: Material
|
||||
vk::DescriptorSet materialSet = descriptorMgr->allocateFrameSet(DescriptorSetIndex::Material);
|
||||
Assert(materialSet);
|
||||
writer.writeSet(materialSet, VulkanDescriptorManager::getSetTemplate(DescriptorSetIndex::Material));
|
||||
writer.writeSet(DescriptorSetIndex::Material, materialSet);
|
||||
{
|
||||
std::array<vk::DescriptorImageInfo, VulkanDescriptorManager::MAX_TEXTURE_BINDINGS> texArrayInfos;
|
||||
texArrayInfos.fill(descriptorMgr->getFallbacks().texture2D);
|
||||
@@ -194,7 +194,7 @@ void PostProcessContext::drawFullscreenTriangle(vk::CommandBuffer cmd, vk::Rende
|
||||
// Set 2: PerDraw
|
||||
vk::DescriptorSet perDrawSet = descriptorMgr->allocateFrameSet(DescriptorSetIndex::PerDraw);
|
||||
Assert(perDrawSet);
|
||||
writer.writeSet(perDrawSet, VulkanDescriptorManager::getSetTemplate(DescriptorSetIndex::PerDraw));
|
||||
writer.writeSet(DescriptorSetIndex::PerDraw, perDrawSet);
|
||||
if (uboData != nullptr && uboSize > 0 && scratchRing.isValid()) {
|
||||
vk::DeviceSize slotOffset = scratchRing.alloc(descriptorMgr->getCurrentFrame(), uboData, uboSize);
|
||||
writer.setBuffer(PerDrawBinding::GenericData, {scratchRing.buffer(), slotOffset, scratchRing.slotSize()});
|
||||
@@ -205,13 +205,11 @@ void PostProcessContext::drawFullscreenTriangle(vk::CommandBuffer cmd, vk::Rende
|
||||
// contiguous (0,1,2), so when Global is (re)bound here it and Material/PerDraw
|
||||
// go in one call; otherwise Set 0 is left as whatever frame setup bound.
|
||||
if (bindGlobalSet) {
|
||||
cmd.bindDescriptorSets(vk::PipelineBindPoint::eGraphics, pipelineLayout,
|
||||
static_cast<uint32_t>(DescriptorSetIndex::Global),
|
||||
{globalSet, materialSet, perDrawSet}, {});
|
||||
const vk::DescriptorSet sets[] = {globalSet, materialSet, perDrawSet};
|
||||
writer.bindSets(cmd, pipelineLayout, DescriptorSetIndex::Global, sets);
|
||||
} else {
|
||||
cmd.bindDescriptorSets(vk::PipelineBindPoint::eGraphics, pipelineLayout,
|
||||
static_cast<uint32_t>(DescriptorSetIndex::Material),
|
||||
{materialSet, perDrawSet}, {});
|
||||
const vk::DescriptorSet sets[] = {materialSet, perDrawSet};
|
||||
writer.bindSets(cmd, pipelineLayout, DescriptorSetIndex::Material, sets);
|
||||
}
|
||||
|
||||
cmd.draw(3, 1, 0, 0);
|
||||
@@ -281,7 +279,7 @@ void PostProcessContext::drawFullscreenTriangleMulti(vk::CommandBuffer cmd, vk::
|
||||
|
||||
vk::DescriptorSet materialSet = descriptorMgr->allocateFrameSet(DescriptorSetIndex::Material);
|
||||
Assert(materialSet);
|
||||
writer.writeSet(materialSet, VulkanDescriptorManager::getSetTemplate(DescriptorSetIndex::Material));
|
||||
writer.writeSet(DescriptorSetIndex::Material, materialSet);
|
||||
{
|
||||
std::array<vk::DescriptorImageInfo, VulkanDescriptorManager::MAX_TEXTURE_BINDINGS> texArrayInfos;
|
||||
texArrayInfos.fill(descriptorMgr->getFallbacks().texture2D);
|
||||
@@ -293,16 +291,15 @@ void PostProcessContext::drawFullscreenTriangleMulti(vk::CommandBuffer cmd, vk::
|
||||
|
||||
vk::DescriptorSet perDrawSet = descriptorMgr->allocateFrameSet(DescriptorSetIndex::PerDraw);
|
||||
Assert(perDrawSet);
|
||||
writer.writeSet(perDrawSet, VulkanDescriptorManager::getSetTemplate(DescriptorSetIndex::PerDraw));
|
||||
writer.writeSet(DescriptorSetIndex::PerDraw, perDrawSet);
|
||||
if (uboData != nullptr && uboSize > 0 && scratchRing.isValid()) {
|
||||
vk::DeviceSize slotOffset = scratchRing.alloc(descriptorMgr->getCurrentFrame(), uboData, uboSize);
|
||||
writer.setBuffer(PerDrawBinding::GenericData, {scratchRing.buffer(), slotOffset, scratchRing.slotSize()});
|
||||
}
|
||||
writer.flush();
|
||||
|
||||
cmd.bindDescriptorSets(vk::PipelineBindPoint::eGraphics, pipelineLayout,
|
||||
static_cast<uint32_t>(DescriptorSetIndex::Material),
|
||||
{materialSet, perDrawSet}, {});
|
||||
const vk::DescriptorSet sets[] = {materialSet, perDrawSet};
|
||||
writer.bindSets(cmd, pipelineLayout, DescriptorSetIndex::Material, sets);
|
||||
|
||||
cmd.draw(3, 1, 0, 0);
|
||||
cmd.endRenderPass();
|
||||
@@ -337,7 +334,7 @@ bool PostProcessContext::createImage(uint32_t width, uint32_t height, vk::Format
|
||||
}
|
||||
|
||||
// Allocate memory
|
||||
if (!memoryManager->allocateImageMemory(outImage, MemoryUsage::GpuOnly, outAllocation)) {
|
||||
if (!memoryManager->allocateImageMemory(outImage, MemoryUsage::GpuOnly, outAllocation, MemoryPurpose::RenderTarget)) {
|
||||
nprintf(("vulkan", "VulkanPostProcessor: Failed to allocate image memory!\n"));
|
||||
device.destroyImage(outImage);
|
||||
outImage = nullptr;
|
||||
|
||||
@@ -304,7 +304,7 @@ void VulkanFog::renderScene(vk::CommandBuffer cmd)
|
||||
// Set 1: Material
|
||||
vk::DescriptorSet materialSet = descriptorMgr->allocateFrameSet(DescriptorSetIndex::Material);
|
||||
Assert(materialSet);
|
||||
writer.writeSet(materialSet, VulkanDescriptorManager::getSetTemplate(DescriptorSetIndex::Material));
|
||||
writer.writeSet(DescriptorSetIndex::Material, materialSet);
|
||||
{
|
||||
std::array<vk::DescriptorImageInfo, VulkanDescriptorManager::MAX_TEXTURE_BINDINGS> texArrayInfos;
|
||||
texArrayInfos.fill(descriptorMgr->getFallbacks().texture2D);
|
||||
@@ -316,7 +316,7 @@ void VulkanFog::renderScene(vk::CommandBuffer cmd)
|
||||
// Set 2: PerDraw — fog UBO (from the per-frame scratch ring)
|
||||
vk::DescriptorSet perDrawSet = descriptorMgr->allocateFrameSet(DescriptorSetIndex::PerDraw);
|
||||
Assert(perDrawSet);
|
||||
writer.writeSet(perDrawSet, VulkanDescriptorManager::getSetTemplate(DescriptorSetIndex::PerDraw));
|
||||
writer.writeSet(DescriptorSetIndex::PerDraw, perDrawSet);
|
||||
{
|
||||
vk::DeviceSize slotOffset =
|
||||
m_ctx->scratchRing.alloc(descriptorMgr->getCurrentFrame(), &fogData, sizeof(fogData));
|
||||
@@ -326,9 +326,8 @@ void VulkanFog::renderScene(vk::CommandBuffer cmd)
|
||||
writer.flush();
|
||||
|
||||
// Bind descriptor sets and draw
|
||||
cmd.bindDescriptorSets(vk::PipelineBindPoint::eGraphics, pipelineLayout,
|
||||
static_cast<uint32_t>(DescriptorSetIndex::Material),
|
||||
{materialSet, perDrawSet}, {});
|
||||
const vk::DescriptorSet sets[] = {materialSet, perDrawSet};
|
||||
writer.bindSets(cmd, pipelineLayout, DescriptorSetIndex::Material, sets);
|
||||
|
||||
cmd.draw(3, 1, 0, 0);
|
||||
cmd.endRenderPass();
|
||||
@@ -420,7 +419,8 @@ void VulkanFog::renderVolumetric(vk::CommandBuffer cmd)
|
||||
return;
|
||||
}
|
||||
|
||||
Verification(m_ctx->memoryManager->allocateImageMemory(m_emissiveMipmapped.image, MemoryUsage::GpuOnly, m_emissiveMipmapped.allocation),
|
||||
Verification(m_ctx->memoryManager->allocateImageMemory(m_emissiveMipmapped.image, MemoryUsage::GpuOnly,
|
||||
m_emissiveMipmapped.allocation, MemoryPurpose::RenderTarget),
|
||||
"Failed to allocate memory for mipmapped emissive image");
|
||||
|
||||
// Create full-mip-chain view for LOD sampling
|
||||
@@ -614,7 +614,7 @@ void VulkanFog::renderVolumetric(vk::CommandBuffer cmd)
|
||||
// Set 1: Material
|
||||
vk::DescriptorSet materialSet = descriptorMgr->allocateFrameSet(DescriptorSetIndex::Material);
|
||||
Assert(materialSet);
|
||||
writer.writeSet(materialSet, VulkanDescriptorManager::getSetTemplate(DescriptorSetIndex::Material));
|
||||
writer.writeSet(DescriptorSetIndex::Material, materialSet);
|
||||
{
|
||||
std::array<vk::DescriptorImageInfo, VulkanDescriptorManager::MAX_TEXTURE_BINDINGS> texArrayInfos;
|
||||
texArrayInfos.fill(descriptorMgr->getFallbacks().texture2D);
|
||||
@@ -638,7 +638,7 @@ void VulkanFog::renderVolumetric(vk::CommandBuffer cmd)
|
||||
// Set 2: PerDraw — volumetric fog UBO (from the per-frame scratch ring)
|
||||
vk::DescriptorSet perDrawSet = descriptorMgr->allocateFrameSet(DescriptorSetIndex::PerDraw);
|
||||
Assert(perDrawSet);
|
||||
writer.writeSet(perDrawSet, VulkanDescriptorManager::getSetTemplate(DescriptorSetIndex::PerDraw));
|
||||
writer.writeSet(DescriptorSetIndex::PerDraw, perDrawSet);
|
||||
{
|
||||
vk::DeviceSize slotOffset =
|
||||
m_ctx->scratchRing.alloc(descriptorMgr->getCurrentFrame(), &volData, sizeof(volData));
|
||||
@@ -648,9 +648,8 @@ void VulkanFog::renderVolumetric(vk::CommandBuffer cmd)
|
||||
writer.flush();
|
||||
|
||||
// Bind descriptor sets and draw
|
||||
cmd.bindDescriptorSets(vk::PipelineBindPoint::eGraphics, pipelineLayout,
|
||||
static_cast<uint32_t>(DescriptorSetIndex::Material),
|
||||
{materialSet, perDrawSet}, {});
|
||||
const vk::DescriptorSet sets[] = {materialSet, perDrawSet};
|
||||
writer.bindSets(cmd, pipelineLayout, DescriptorSetIndex::Material, sets);
|
||||
|
||||
cmd.draw(3, 1, 0, 0);
|
||||
cmd.endRenderPass();
|
||||
|
||||
@@ -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"
|
||||
@@ -70,7 +71,8 @@ bool VulkanDeferredLighting::initLightVolumes()
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!m_ctx->memoryManager->allocateBufferMemory(m_sphereMesh.vbo, MemoryUsage::CpuToGpu, m_sphereMesh.vboAlloc)) {
|
||||
if (!m_ctx->memoryManager->allocateBufferMemory(
|
||||
m_sphereMesh.vbo, MemoryUsage::CpuToGpu, m_sphereMesh.vboAlloc, MemoryPurpose::Geometry)) {
|
||||
m_ctx->device.destroyBuffer(m_sphereMesh.vbo);
|
||||
m_sphereMesh.vbo = nullptr;
|
||||
return false;
|
||||
@@ -95,7 +97,8 @@ bool VulkanDeferredLighting::initLightVolumes()
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!m_ctx->memoryManager->allocateBufferMemory(m_sphereMesh.ibo, MemoryUsage::CpuToGpu, m_sphereMesh.iboAlloc)) {
|
||||
if (!m_ctx->memoryManager->allocateBufferMemory(
|
||||
m_sphereMesh.ibo, MemoryUsage::CpuToGpu, m_sphereMesh.iboAlloc, MemoryPurpose::Geometry)) {
|
||||
m_ctx->device.destroyBuffer(m_sphereMesh.ibo);
|
||||
m_sphereMesh.ibo = nullptr;
|
||||
return false;
|
||||
@@ -126,7 +129,8 @@ bool VulkanDeferredLighting::initLightVolumes()
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!m_ctx->memoryManager->allocateBufferMemory(m_cylinderMesh.vbo, MemoryUsage::CpuToGpu, m_cylinderMesh.vboAlloc)) {
|
||||
if (!m_ctx->memoryManager->allocateBufferMemory(
|
||||
m_cylinderMesh.vbo, MemoryUsage::CpuToGpu, m_cylinderMesh.vboAlloc, MemoryPurpose::Geometry)) {
|
||||
m_ctx->device.destroyBuffer(m_cylinderMesh.vbo);
|
||||
m_cylinderMesh.vbo = nullptr;
|
||||
return false;
|
||||
@@ -150,7 +154,8 @@ bool VulkanDeferredLighting::initLightVolumes()
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!m_ctx->memoryManager->allocateBufferMemory(m_cylinderMesh.ibo, MemoryUsage::CpuToGpu, m_cylinderMesh.iboAlloc)) {
|
||||
if (!m_ctx->memoryManager->allocateBufferMemory(
|
||||
m_cylinderMesh.ibo, MemoryUsage::CpuToGpu, m_cylinderMesh.iboAlloc, MemoryPurpose::Geometry)) {
|
||||
m_ctx->device.destroyBuffer(m_cylinderMesh.ibo);
|
||||
m_cylinderMesh.ibo = nullptr;
|
||||
return false;
|
||||
@@ -502,8 +507,12 @@ void VulkanDeferredLighting::render(vk::CommandBuffer cmd)
|
||||
{
|
||||
auto* header = reinterpret_cast<graphics::deferred_global_data*>(uboMapped + regionBase);
|
||||
memset(header, 0, sizeof(graphics::deferred_global_data));
|
||||
header->invScreenWidth = 1.0f / gr_screen.max_w;
|
||||
header->invScreenHeight = 1.0f / gr_screen.max_h;
|
||||
// Same as the OpenGL backend: deferred-f.sdr normalizes gl_FragCoord against these to
|
||||
// sample the G-buffer, so they must describe the G-buffer, not gr_screen. resize() keeps
|
||||
// sceneExtent equal to gr_screen today, which is why deriving it from the extent is a
|
||||
// no-op here -- but it states the actual requirement instead of relying on that.
|
||||
header->invScreenWidth = 1.0f / static_cast<float>(m_ctx->sceneExtent.width);
|
||||
header->invScreenHeight = 1.0f / static_cast<float>(m_ctx->sceneExtent.height);
|
||||
header->nearPlane = gr_near_plane;
|
||||
|
||||
if (m_shadow->isInitialized() && Shadow_quality != ShadowQuality::Disabled) {
|
||||
@@ -683,6 +692,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;
|
||||
}
|
||||
@@ -750,8 +762,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
|
||||
@@ -763,7 +777,10 @@ void VulkanDeferredLighting::render(vk::CommandBuffer cmd)
|
||||
auto* drawManager = getDrawManager();
|
||||
const auto& pending = drawManager->getPendingUniformBinding(static_cast<size_t>(uniform_block_type::ShadowCascadeParams));
|
||||
if (pending.valid) {
|
||||
vk::Buffer buf = bufferMgr->getVkBuffer(pending.bufferHandle);
|
||||
// Resolved through getVkBufferForBinding() like applyMaterial's own pending UBOs: the
|
||||
// block lives in a streaming buffer, so a binding made in an earlier frame no longer
|
||||
// points at anything and has to fall back to the placeholder rather than assert.
|
||||
vk::Buffer buf = bufferMgr->getVkBufferForBinding(pending.bufferHandle);
|
||||
if (buf) {
|
||||
shadowCascadeParamsInfo = vk::DescriptorBufferInfo(buf, pending.offset, pending.size);
|
||||
}
|
||||
@@ -788,7 +805,7 @@ void VulkanDeferredLighting::render(vk::CommandBuffer cmd)
|
||||
// Set 0: Global
|
||||
vk::DescriptorSet globalSet = descriptorMgr->allocateFrameSet(DescriptorSetIndex::Global);
|
||||
if (!globalSet) return false;
|
||||
writer.writeSet(globalSet, VulkanDescriptorManager::getSetTemplate(DescriptorSetIndex::Global));
|
||||
writer.writeSet(DescriptorSetIndex::Global, globalSet);
|
||||
writer.setBuffer(GlobalBinding::Lights, {m_deferredUBO,
|
||||
lightDataOffset + (li * lightDataSize), sizeof(graphics::deferred_light_data)});
|
||||
writer.setBuffer(GlobalBinding::DeferredData, {m_deferredUBO,
|
||||
@@ -797,23 +814,24 @@ 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);
|
||||
if (!materialSet) return false;
|
||||
writer.writeSet(materialSet, VulkanDescriptorManager::getSetTemplate(DescriptorSetIndex::Material));
|
||||
writer.writeSet(DescriptorSetIndex::Material, materialSet);
|
||||
writer.setImageArray(MaterialBinding::TextureArray, gbufTexArray);
|
||||
|
||||
// Set 2: PerDraw
|
||||
vk::DescriptorSet perDrawSet = descriptorMgr->allocateFrameSet(DescriptorSetIndex::PerDraw);
|
||||
if (!perDrawSet) return false;
|
||||
writer.writeSet(perDrawSet, VulkanDescriptorManager::getSetTemplate(DescriptorSetIndex::PerDraw));
|
||||
writer.writeSet(DescriptorSetIndex::PerDraw, perDrawSet);
|
||||
writer.setBuffer(PerDrawBinding::Matrices, {m_deferredUBO,
|
||||
matrixDataOffset + (li * matrixDataSize), sizeof(graphics::matrix_uniforms)});
|
||||
writer.flush();
|
||||
|
||||
std::array<vk::DescriptorSet, 3> sets = { globalSet, materialSet, perDrawSet };
|
||||
cmd.bindDescriptorSets(vk::PipelineBindPoint::eGraphics, pipelineLayout, 0, sets, {});
|
||||
const vk::DescriptorSet sets[] = {globalSet, materialSet, perDrawSet};
|
||||
writer.bindSets(cmd, pipelineLayout, DescriptorSetIndex::Global, sets);
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
@@ -206,7 +206,8 @@ bool VulkanDeferredGBuffer::createMsaaTargets()
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!m_ctx->memoryManager->allocateImageMemory(m_msaaDepthImage, MemoryUsage::GpuOnly, m_msaaDepthAlloc)) {
|
||||
if (!m_ctx->memoryManager->allocateImageMemory(
|
||||
m_msaaDepthImage, MemoryUsage::GpuOnly, m_msaaDepthAlloc, MemoryPurpose::RenderTarget)) {
|
||||
nprintf(("vulkan", "VulkanPostProcessor: Failed to allocate MSAA depth memory!\n"));
|
||||
m_ctx->device.destroyImage(m_msaaDepthImage);
|
||||
m_msaaDepthImage = nullptr;
|
||||
|
||||
@@ -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));
|
||||
@@ -70,7 +63,8 @@ bool VulkanShadowMap::init(PostProcessContext& ctx)
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!m_ctx->memoryManager->allocateImageMemory(m_depth.image, MemoryUsage::GpuOnly, m_depth.allocation)) {
|
||||
if (!m_ctx->memoryManager->allocateImageMemory(
|
||||
m_depth.image, MemoryUsage::GpuOnly, m_depth.allocation, MemoryPurpose::RenderTarget)) {
|
||||
m_ctx->device.destroyImage(m_depth.image);
|
||||
m_depth.image = nullptr;
|
||||
return false;
|
||||
@@ -187,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));
|
||||
@@ -203,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;
|
||||
|
||||
@@ -0,0 +1,809 @@
|
||||
|
||||
#include "VulkanPresentTarget.h"
|
||||
|
||||
#include "VulkanRenderer.h"
|
||||
|
||||
#include "cmdline/cmdline.h"
|
||||
#include "graphics/2d.h"
|
||||
#include "graphics/grinternal.h"
|
||||
#include "mod_table/mod_table.h"
|
||||
|
||||
namespace graphics::vulkan {
|
||||
|
||||
namespace {
|
||||
|
||||
vk::SurfaceFormatKHR chooseSurfaceFormat(const PhysicalDeviceValues& values)
|
||||
{
|
||||
// When HDR output is requested, prefer a 10-bit HDR10 (PQ / ST.2084) surface
|
||||
// using BT.2020 primaries. The final output-encode pass writes PQ-encoded
|
||||
// BT.2020 values into this surface.
|
||||
//
|
||||
// Never in the editor: its surface is a window embedded in a desktop-composited
|
||||
// application, so what an HDR10 swap chain would actually look like there is not
|
||||
// something we can verify. It would also drag in the format-change limitation
|
||||
// recreateSwapChain() documents.
|
||||
if (Gr_enable_hdr && !Fred_running) {
|
||||
for (const auto& availableFormat : values.surfaceFormats) {
|
||||
if ((availableFormat.format == vk::Format::eA2B10G10R10UnormPack32 ||
|
||||
availableFormat.format == vk::Format::eA2R10G10B10UnormPack32) &&
|
||||
availableFormat.colorSpace == vk::ColorSpaceKHR::eHdr10St2084EXT) {
|
||||
nprintf(("vulkan", "Vulkan: Selected HDR10 surface (10-bit, ST.2084/BT.2020)\n"));
|
||||
return availableFormat;
|
||||
}
|
||||
}
|
||||
nprintf(("vulkan", "Vulkan: HDR requested but no HDR10 surface format available; falling back to SDR\n"));
|
||||
}
|
||||
|
||||
// Use a non-sRGB (UNORM) format to match OpenGL's default framebuffer behavior.
|
||||
// The FSO shaders handle gamma correction manually in the fragment shader and
|
||||
// post-processing pipeline, so hardware sRGB conversion would double-correct.
|
||||
for (const auto& availableFormat : values.surfaceFormats) {
|
||||
if (availableFormat.format == vk::Format::eB8G8R8A8Unorm &&
|
||||
availableFormat.colorSpace == vk::ColorSpaceKHR::eSrgbNonlinear) {
|
||||
return availableFormat;
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: no preferred format matched. Pick the first concrete format,
|
||||
// defensively skipping any eUndefined entry (the legacy "any format allowed"
|
||||
// sentinel), and log the actual choice so it's visible in the log.
|
||||
for (const auto& availableFormat : values.surfaceFormats) {
|
||||
if (availableFormat.format != vk::Format::eUndefined) {
|
||||
nprintf(("vulkan", "Vulkan: no preferred surface format available; falling back to format=%d colorSpace=%d\n",
|
||||
static_cast<int>(availableFormat.format), static_cast<int>(availableFormat.colorSpace)));
|
||||
return availableFormat;
|
||||
}
|
||||
}
|
||||
|
||||
// Degenerate list (all eUndefined) — return the front entry and warn.
|
||||
nprintf(("vulkan", "Vulkan: surface format list has no concrete entry; using front (format=%d)\n",
|
||||
static_cast<int>(values.surfaceFormats.front().format)));
|
||||
return values.surfaceFormats.front();
|
||||
}
|
||||
|
||||
vk::PresentModeKHR choosePresentMode(const PhysicalDeviceValues& values)
|
||||
{
|
||||
// With vsync requested, use FIFO: it is the only spec-guaranteed mode and
|
||||
// the only one that actually caps the frame rate to the display. Mailbox is
|
||||
// tear-free but uncapped ("fast vsync") and must not be silently substituted
|
||||
// for requested vsync. Without vsync prefer Immediate (true uncapped), then
|
||||
// Mailbox (uncapped but tear-free), then the guaranteed FIFO fallback.
|
||||
vk::PresentModeKHR chosen = vk::PresentModeKHR::eFifo;
|
||||
|
||||
if (!Gr_enable_vsync) {
|
||||
for (const auto& availablePresentMode : values.presentModes) {
|
||||
if (availablePresentMode == vk::PresentModeKHR::eImmediate) {
|
||||
chosen = availablePresentMode;
|
||||
break;
|
||||
}
|
||||
if (availablePresentMode == vk::PresentModeKHR::eMailbox) {
|
||||
chosen = availablePresentMode;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const char* name = "Unknown";
|
||||
switch (chosen) {
|
||||
case vk::PresentModeKHR::eImmediate: name = "Immediate"; break;
|
||||
case vk::PresentModeKHR::eMailbox: name = "Mailbox"; break;
|
||||
case vk::PresentModeKHR::eFifo: name = "FIFO (vsync)"; break;
|
||||
case vk::PresentModeKHR::eFifoRelaxed: name = "FIFO Relaxed"; break;
|
||||
default: break;
|
||||
}
|
||||
mprintf(("Vulkan: Present mode: %s (Gr_enable_vsync=%d)\n", name, Gr_enable_vsync ? 1 : 0));
|
||||
|
||||
return chosen;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief The size of the actual window, which is not the render resolution
|
||||
*
|
||||
* Only used where the surface declines to tell us its extent (Wayland reports
|
||||
* UINT32_MAX and lets the client pick). gr_screen.max_w/max_h is the wrong
|
||||
* answer there: with -window_res set -- which -vr forces -- the window is
|
||||
* smaller than the resolution the engine draws at.
|
||||
*
|
||||
* The target's own viewport answers first, so qtFRED's second window gets its own size rather
|
||||
* than the main one's.
|
||||
*/
|
||||
vk::Extent2D windowExtent(const VulkanPresentTarget& target)
|
||||
{
|
||||
if (target.viewport != nullptr) {
|
||||
const auto size = target.viewport->getSize();
|
||||
if (size.first > 0 && size.second > 0) {
|
||||
return {size.first, size.second};
|
||||
}
|
||||
}
|
||||
|
||||
if (Cmdline_window_res) {
|
||||
return {Cmdline_window_res->first, Cmdline_window_res->second};
|
||||
}
|
||||
|
||||
return {static_cast<uint32_t>(gr_screen.max_w), static_cast<uint32_t>(gr_screen.max_h)};
|
||||
}
|
||||
|
||||
vk::Extent2D chooseSwapChainExtent(const vk::SurfaceCapabilitiesKHR& capabilities, uint32_t width, uint32_t height)
|
||||
{
|
||||
if (capabilities.currentExtent.width != UINT32_MAX) {
|
||||
return capabilities.currentExtent;
|
||||
} else {
|
||||
VkExtent2D actualExtent = {width, height};
|
||||
|
||||
actualExtent.width = std::max(capabilities.minImageExtent.width,
|
||||
std::min(capabilities.maxImageExtent.width, actualExtent.width));
|
||||
actualExtent.height = std::max(capabilities.minImageExtent.height,
|
||||
std::min(capabilities.maxImageExtent.height, actualExtent.height));
|
||||
|
||||
return actualExtent;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
bool checkSwapChainSupport(PhysicalDeviceValues& values, vk::SurfaceKHR surface)
|
||||
{
|
||||
values.surfaceCapabilities = values.device.getSurfaceCapabilitiesKHR(surface);
|
||||
auto fmts = values.device.getSurfaceFormatsKHR(surface);
|
||||
values.surfaceFormats.assign(fmts.begin(), fmts.end());
|
||||
auto modes = values.device.getSurfacePresentModesKHR(surface);
|
||||
values.presentModes.assign(modes.begin(), modes.end());
|
||||
|
||||
return !values.surfaceFormats.empty() && !values.presentModes.empty();
|
||||
}
|
||||
|
||||
VulkanSurfaceHandle::VulkanSurfaceHandle(os::VulkanSurfaceProvider* provider,
|
||||
vk::Instance instance,
|
||||
vk::SurfaceKHR surface)
|
||||
: m_provider(provider), m_instance(instance), m_surface(surface)
|
||||
{
|
||||
}
|
||||
VulkanSurfaceHandle::~VulkanSurfaceHandle()
|
||||
{
|
||||
reset();
|
||||
}
|
||||
VulkanSurfaceHandle::VulkanSurfaceHandle(VulkanSurfaceHandle&& other) noexcept
|
||||
: m_provider(other.m_provider), m_instance(other.m_instance), m_surface(other.m_surface)
|
||||
{
|
||||
other.m_provider = nullptr;
|
||||
other.m_instance = vk::Instance();
|
||||
other.m_surface = vk::SurfaceKHR();
|
||||
}
|
||||
VulkanSurfaceHandle& VulkanSurfaceHandle::operator=(VulkanSurfaceHandle&& other) noexcept
|
||||
{
|
||||
if (this != &other) {
|
||||
reset();
|
||||
|
||||
m_provider = other.m_provider;
|
||||
m_instance = other.m_instance;
|
||||
m_surface = other.m_surface;
|
||||
|
||||
other.m_provider = nullptr;
|
||||
other.m_instance = vk::Instance();
|
||||
other.m_surface = vk::SurfaceKHR();
|
||||
}
|
||||
return *this;
|
||||
}
|
||||
void VulkanSurfaceHandle::reset()
|
||||
{
|
||||
if (m_provider != nullptr && m_surface) {
|
||||
m_provider->destroyVulkanSurface(static_cast<VkInstance>(m_instance),
|
||||
os::vulkan_handle_value(static_cast<VkSurfaceKHR>(m_surface)));
|
||||
}
|
||||
|
||||
m_provider = nullptr;
|
||||
m_instance = vk::Instance();
|
||||
m_surface = vk::SurfaceKHR();
|
||||
}
|
||||
|
||||
bool VulkanRenderer::createTargetSurface(VulkanPresentTarget& target)
|
||||
{
|
||||
auto* vulkanSupport = m_graphicsOps->getVulkanSupport();
|
||||
Assertion(vulkanSupport != nullptr, "initializeInstance() should have rejected this already!");
|
||||
|
||||
const auto surface =
|
||||
vulkanSupport->createVulkanSurface(target.viewport, static_cast<VkInstance>(*m_vkInstance));
|
||||
if (surface == 0) {
|
||||
nprintf(("vulkan", "Vulkan: failed to create a surface for this viewport.\n"));
|
||||
return false;
|
||||
}
|
||||
|
||||
target.surface = VulkanSurfaceHandle(vulkanSupport,
|
||||
*m_vkInstance,
|
||||
vk::SurfaceKHR(os::vulkan_handle_cast<VkSurfaceKHR>(surface)));
|
||||
return true;
|
||||
}
|
||||
|
||||
// ========== Extent-sized resources ==========
|
||||
//
|
||||
// Every one of these is rebuilt whenever the swap chain is, which is why they live next to
|
||||
// it rather than with the renderer-wide setup: their size comes from the target's surface.
|
||||
|
||||
void VulkanRenderer::createCompositionResources(VulkanPresentTarget& target)
|
||||
{
|
||||
// Free any previous composition resources (swap chain recreation path)
|
||||
target.compositionImageViews.clear();
|
||||
target.compositionImages.clear();
|
||||
for (auto& alloc : target.compositionAllocations) {
|
||||
if (alloc.isValid()) {
|
||||
m_memoryManager->freeAllocation(alloc);
|
||||
}
|
||||
}
|
||||
target.compositionAllocations.clear();
|
||||
|
||||
const size_t count = target.imageViews.size();
|
||||
target.compositionImages.reserve(count);
|
||||
target.compositionImageViews.reserve(count);
|
||||
target.compositionAllocations.reserve(count);
|
||||
|
||||
for (size_t i = 0; i < count; ++i) {
|
||||
vk::ImageCreateInfo imageInfo;
|
||||
imageInfo.imageType = vk::ImageType::e2D;
|
||||
imageInfo.format = HDR_COLOR_FORMAT;
|
||||
imageInfo.extent = vk::Extent3D(target.renderExtent.width, target.renderExtent.height, 1);
|
||||
imageInfo.mipLevels = 1;
|
||||
imageInfo.arrayLayers = 1;
|
||||
imageInfo.samples = vk::SampleCountFlagBits::e1;
|
||||
imageInfo.tiling = vk::ImageTiling::eOptimal;
|
||||
imageInfo.usage = vk::ImageUsageFlagBits::eColorAttachment | vk::ImageUsageFlagBits::eSampled |
|
||||
vk::ImageUsageFlagBits::eTransferSrc;
|
||||
imageInfo.sharingMode = vk::SharingMode::eExclusive;
|
||||
imageInfo.initialLayout = vk::ImageLayout::eUndefined;
|
||||
|
||||
auto image = m_device->createImageUnique(imageInfo);
|
||||
|
||||
VulkanAllocation alloc{};
|
||||
m_memoryManager->allocateImageMemory(image.get(), MemoryUsage::GpuOnly, alloc, MemoryPurpose::RenderTarget);
|
||||
|
||||
vk::ImageViewCreateInfo viewInfo;
|
||||
viewInfo.image = image.get();
|
||||
viewInfo.viewType = vk::ImageViewType::e2D;
|
||||
viewInfo.format = HDR_COLOR_FORMAT;
|
||||
viewInfo.subresourceRange = {vk::ImageAspectFlagBits::eColor, 0, 1, 0, 1};
|
||||
auto view = m_device->createImageViewUnique(viewInfo);
|
||||
|
||||
target.compositionImages.push_back(std::move(image));
|
||||
target.compositionAllocations.push_back(alloc);
|
||||
target.compositionImageViews.push_back(std::move(view));
|
||||
}
|
||||
|
||||
// Sampler used by the output-encode pass to read the composition image.
|
||||
if (!m_compositionSampler) {
|
||||
vk::SamplerCreateInfo sampInfo;
|
||||
sampInfo.magFilter = vk::Filter::eNearest;
|
||||
sampInfo.minFilter = vk::Filter::eNearest;
|
||||
sampInfo.mipmapMode = vk::SamplerMipmapMode::eNearest;
|
||||
sampInfo.addressModeU = vk::SamplerAddressMode::eClampToEdge;
|
||||
sampInfo.addressModeV = vk::SamplerAddressMode::eClampToEdge;
|
||||
sampInfo.addressModeW = vk::SamplerAddressMode::eClampToEdge;
|
||||
m_compositionSampler = m_device->createSamplerUnique(sampInfo);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void VulkanRenderer::createFrameBuffers(VulkanPresentTarget& target)
|
||||
{
|
||||
target.framebuffers.clear();
|
||||
target.encodeFramebuffers.clear();
|
||||
|
||||
// Composition framebuffers: color = fp16 composition image, depth shared.
|
||||
// Indexed by swap chain image so each in-flight frame uses its own image.
|
||||
target.framebuffers.reserve(target.compositionImageViews.size());
|
||||
for (const auto& compView : target.compositionImageViews) {
|
||||
const vk::ImageView attachments[] = {
|
||||
compView.get(),
|
||||
target.depthImageView.get(),
|
||||
};
|
||||
|
||||
vk::FramebufferCreateInfo framebufferInfo;
|
||||
framebufferInfo.renderPass = m_renderPass.get();
|
||||
framebufferInfo.attachmentCount = 2;
|
||||
framebufferInfo.pAttachments = attachments;
|
||||
framebufferInfo.width = target.renderExtent.width;
|
||||
framebufferInfo.height = target.renderExtent.height;
|
||||
framebufferInfo.layers = 1;
|
||||
|
||||
target.framebuffers.push_back(m_device->createFramebufferUnique(framebufferInfo));
|
||||
}
|
||||
|
||||
// Encode framebuffers: color = actual swap chain image.
|
||||
target.encodeFramebuffers.reserve(target.imageViews.size());
|
||||
for (const auto& scView : target.imageViews) {
|
||||
const vk::ImageView attachments[] = { scView.get() };
|
||||
|
||||
vk::FramebufferCreateInfo framebufferInfo;
|
||||
framebufferInfo.renderPass = m_encodeRenderPass.get();
|
||||
framebufferInfo.attachmentCount = 1;
|
||||
framebufferInfo.pAttachments = attachments;
|
||||
framebufferInfo.width = target.extent.width;
|
||||
framebufferInfo.height = target.extent.height;
|
||||
framebufferInfo.layers = 1;
|
||||
|
||||
target.encodeFramebuffers.push_back(m_device->createFramebufferUnique(framebufferInfo));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
vk::Format VulkanRenderer::findDepthFormat()
|
||||
{
|
||||
// Prefer D32_SFLOAT for best precision, fall back to D32_SFLOAT_S8 or D24_UNORM_S8
|
||||
const vk::Format candidates[] = {
|
||||
vk::Format::eD32Sfloat,
|
||||
vk::Format::eD32SfloatS8Uint,
|
||||
vk::Format::eD24UnormS8Uint,
|
||||
};
|
||||
|
||||
for (auto format : candidates) {
|
||||
auto props = m_physicalDevice.getFormatProperties(format);
|
||||
if (props.optimalTilingFeatures & vk::FormatFeatureFlagBits::eDepthStencilAttachment) {
|
||||
return format;
|
||||
}
|
||||
}
|
||||
|
||||
// Should never happen on any real GPU
|
||||
Error(LOCATION, "Failed to find supported depth format!");
|
||||
return vk::Format::eD32Sfloat;
|
||||
}
|
||||
void VulkanRenderer::createDepthResources(VulkanPresentTarget& target)
|
||||
{
|
||||
const vk::Format depthFormat = findDepthFormat();
|
||||
// The render passes (m_renderPass, scene/G-buffer passes, ...) bake in the
|
||||
// depth format, and they are deliberately kept alive across swap chain
|
||||
// recreation. A driver changing its supported depth formats mid-session
|
||||
// would make them all incompatible with the new attachment.
|
||||
if (m_depthFormat != vk::Format::eUndefined && depthFormat != m_depthFormat) {
|
||||
Error(LOCATION, "Vulkan: depth format changed across swap chain recreation (%d -> %d)!",
|
||||
static_cast<int>(m_depthFormat), static_cast<int>(depthFormat));
|
||||
}
|
||||
m_depthFormat = depthFormat;
|
||||
|
||||
// Create depth image
|
||||
vk::ImageCreateInfo imageInfo;
|
||||
imageInfo.imageType = vk::ImageType::e2D;
|
||||
imageInfo.format = m_depthFormat;
|
||||
imageInfo.extent.width = target.renderExtent.width;
|
||||
imageInfo.extent.height = target.renderExtent.height;
|
||||
imageInfo.extent.depth = 1;
|
||||
imageInfo.mipLevels = 1;
|
||||
imageInfo.arrayLayers = 1;
|
||||
imageInfo.samples = vk::SampleCountFlagBits::e1;
|
||||
imageInfo.tiling = vk::ImageTiling::eOptimal;
|
||||
imageInfo.usage = vk::ImageUsageFlagBits::eDepthStencilAttachment;
|
||||
imageInfo.sharingMode = vk::SharingMode::eExclusive;
|
||||
imageInfo.initialLayout = vk::ImageLayout::eUndefined;
|
||||
|
||||
target.depthImage = m_device->createImageUnique(imageInfo);
|
||||
|
||||
// Allocate GPU memory for the depth image
|
||||
m_memoryManager->allocateImageMemory(
|
||||
target.depthImage.get(), MemoryUsage::GpuOnly, target.depthImageMemory, MemoryPurpose::RenderTarget);
|
||||
|
||||
// Create depth image view
|
||||
vk::ImageViewCreateInfo viewInfo;
|
||||
viewInfo.image = target.depthImage.get();
|
||||
viewInfo.viewType = vk::ImageViewType::e2D;
|
||||
viewInfo.format = m_depthFormat;
|
||||
viewInfo.subresourceRange.aspectMask = imageAspectFromFormat(m_depthFormat);
|
||||
viewInfo.subresourceRange.baseMipLevel = 0;
|
||||
viewInfo.subresourceRange.levelCount = 1;
|
||||
viewInfo.subresourceRange.baseArrayLayer = 0;
|
||||
viewInfo.subresourceRange.layerCount = 1;
|
||||
|
||||
target.depthImageView = m_device->createImageViewUnique(viewInfo);
|
||||
|
||||
nprintf(("vulkan", "Vulkan: Created depth buffer (%dx%d, format %d)\n",
|
||||
target.renderExtent.width, target.renderExtent.height, static_cast<int>(m_depthFormat)));
|
||||
}
|
||||
|
||||
|
||||
|
||||
void VulkanRenderer::createPresentSyncObjects(VulkanPresentTarget& target)
|
||||
{
|
||||
for (size_t i = 0; i < MAX_FRAMES_IN_FLIGHT; ++i) {
|
||||
target.frames[i] = std::make_unique<VulkanRenderFrame>(m_device.get(), target.swapChain.get(), m_graphicsQueue, m_presentQueue);
|
||||
}
|
||||
|
||||
target.imageRenderFrame.resize(target.images.size(), nullptr);
|
||||
|
||||
// One more than the frames in flight: at any moment the in-flight frames can each be holding
|
||||
// one, and a viewport switch can have retained one on top of that.
|
||||
constexpr vk::SemaphoreCreateInfo semaphoreCreateInfo;
|
||||
target.acquireSemaphores.clear();
|
||||
for (size_t i = 0; i < MAX_FRAMES_IN_FLIGHT + 1; ++i) {
|
||||
VulkanPresentTarget::AcquireSemaphore entry;
|
||||
entry.semaphore = m_device->createSemaphoreUnique(semaphoreCreateInfo);
|
||||
target.acquireSemaphores.push_back(std::move(entry));
|
||||
}
|
||||
target.nextAcquire = 0;
|
||||
target.currentAcquire = 0;
|
||||
target.hasRetainedAcquire = false;
|
||||
|
||||
createRenderFinishedSemaphores(target);
|
||||
}
|
||||
void VulkanRenderer::createRenderFinishedSemaphores(VulkanPresentTarget& target)
|
||||
{
|
||||
constexpr vk::SemaphoreCreateInfo semaphoreCreateInfo;
|
||||
|
||||
target.renderFinishedSemaphores.clear();
|
||||
target.renderFinishedSemaphores.reserve(target.images.size());
|
||||
for (size_t i = 0; i < target.images.size(); ++i) {
|
||||
target.renderFinishedSemaphores.push_back(m_device->createSemaphoreUnique(semaphoreCreateInfo));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
bool VulkanRenderer::createTargetResources(VulkanPresentTarget& target)
|
||||
{
|
||||
if (!createTargetSurface(target)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// The device was already chosen against the main surface, so only the parts that are per-surface
|
||||
// get re-queried here. The present queue is checked rather than assumed: a device is allowed to
|
||||
// support presentation to one surface and not another.
|
||||
PhysicalDeviceValues values;
|
||||
values.device = m_physicalDevice;
|
||||
values.graphicsQueueIndex = {true, m_graphicsQueueFamilyIndex};
|
||||
values.presentQueueIndex = {true, m_presentQueueFamilyIndex};
|
||||
|
||||
if (!m_physicalDevice.getSurfaceSupportKHR(m_presentQueueFamilyIndex, target.surface.get())) {
|
||||
mprintf(("Vulkan: the present queue cannot present to this viewport's surface.\n"));
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!checkSwapChainSupport(values, target.surface.get())) {
|
||||
mprintf(("Vulkan: this viewport's surface reports no usable formats or present modes.\n"));
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!createSwapChain(target, values)) {
|
||||
mprintf(("Vulkan: failed to create a swap chain for this viewport.\n"));
|
||||
return false;
|
||||
}
|
||||
|
||||
// m_encodeRenderPass is shared and bakes in the swap chain format it was built for, so a target
|
||||
// that negotiates a different one cannot present through it. In practice every surface here is
|
||||
// the same device, driver and window system and they agree -- which is exactly why this is
|
||||
// checked rather than assumed, since a mismatch would otherwise be silent and only appear on
|
||||
// somebody else's hardware. Fail the target instead; the caller falls back.
|
||||
if (target.imageFormat != m_mainTarget->imageFormat) {
|
||||
mprintf(("Vulkan: this viewport's surface negotiated format %d but the output-encode pass was "
|
||||
"built for %d; cannot present to it.\n",
|
||||
static_cast<int>(target.imageFormat), static_cast<int>(m_mainTarget->imageFormat)));
|
||||
return false;
|
||||
}
|
||||
|
||||
createDepthResources(target);
|
||||
createCompositionResources(target);
|
||||
createFrameBuffers(target);
|
||||
createPresentSyncObjects(target);
|
||||
|
||||
nprintf(("vulkan", "Vulkan: created a present target for a second viewport (%ux%u, %zu images)\n",
|
||||
target.extent.width, target.extent.height, target.images.size()));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void VulkanRenderer::destroyDepthResources(VulkanPresentTarget& target)
|
||||
{
|
||||
target.depthImageView.reset();
|
||||
target.depthImage.reset();
|
||||
if (m_memoryManager && target.depthImageMemory.isValid()) {
|
||||
m_memoryManager->freeAllocation(target.depthImageMemory);
|
||||
target.depthImageMemory = {};
|
||||
}
|
||||
}
|
||||
|
||||
void VulkanRenderer::destroyTargetSwapChain(VulkanPresentTarget& target)
|
||||
{
|
||||
// Framebuffers and views reference the swap chain images, and the frames hold the swap chain
|
||||
// handle for their acquires and presents, so all of them go first.
|
||||
target.framebuffers.clear();
|
||||
target.encodeFramebuffers.clear();
|
||||
target.imageViews.clear();
|
||||
target.images.clear();
|
||||
target.imageRenderFrame.clear();
|
||||
|
||||
for (auto& frame : target.frames) {
|
||||
frame.reset();
|
||||
}
|
||||
target.acquireSemaphores.clear();
|
||||
target.renderFinishedSemaphores.clear();
|
||||
target.hasRetainedAcquire = false;
|
||||
|
||||
target.swapChain.reset();
|
||||
}
|
||||
|
||||
void VulkanRenderer::releaseTargetMemory(VulkanPresentTarget& target)
|
||||
{
|
||||
destroyDepthResources(target);
|
||||
|
||||
target.compositionImageViews.clear();
|
||||
target.compositionImages.clear();
|
||||
if (m_memoryManager) {
|
||||
for (auto& alloc : target.compositionAllocations) {
|
||||
if (alloc.isValid()) {
|
||||
m_memoryManager->freeAllocation(alloc);
|
||||
}
|
||||
}
|
||||
}
|
||||
target.compositionAllocations.clear();
|
||||
}
|
||||
|
||||
bool VulkanRenderer::createSwapChain(VulkanPresentTarget& target,
|
||||
const PhysicalDeviceValues& deviceValues,
|
||||
vk::SwapchainKHR oldSwapchain)
|
||||
{
|
||||
// Choose one more than the minimum to avoid driver synchronization if it is not done with a thread yet
|
||||
uint32_t imageCount = deviceValues.surfaceCapabilities.minImageCount + 1;
|
||||
if (deviceValues.surfaceCapabilities.maxImageCount > 0 &&
|
||||
imageCount > deviceValues.surfaceCapabilities.maxImageCount) {
|
||||
imageCount = deviceValues.surfaceCapabilities.maxImageCount;
|
||||
}
|
||||
|
||||
const auto surfaceFormat = chooseSurfaceFormat(deviceValues);
|
||||
|
||||
const vk::Extent2D window = windowExtent(target);
|
||||
|
||||
vk::SwapchainCreateInfoKHR createInfo;
|
||||
createInfo.surface = target.surface.get();
|
||||
createInfo.minImageCount = imageCount;
|
||||
createInfo.imageFormat = surfaceFormat.format;
|
||||
createInfo.imageColorSpace = surfaceFormat.colorSpace;
|
||||
createInfo.imageExtent = chooseSwapChainExtent(deviceValues.surfaceCapabilities, window.width, window.height);
|
||||
createInfo.imageArrayLayers = 1;
|
||||
createInfo.imageUsage = vk::ImageUsageFlagBits::eColorAttachment
|
||||
| vk::ImageUsageFlagBits::eTransferSrc
|
||||
| vk::ImageUsageFlagBits::eTransferDst;
|
||||
|
||||
const uint32_t queueFamilyIndices[] = {deviceValues.graphicsQueueIndex.index, deviceValues.presentQueueIndex.index};
|
||||
if (deviceValues.graphicsQueueIndex.index != deviceValues.presentQueueIndex.index) {
|
||||
createInfo.imageSharingMode = vk::SharingMode::eConcurrent;
|
||||
createInfo.queueFamilyIndexCount = 2;
|
||||
createInfo.pQueueFamilyIndices = queueFamilyIndices;
|
||||
} else {
|
||||
createInfo.imageSharingMode = vk::SharingMode::eExclusive;
|
||||
}
|
||||
|
||||
// preTransform describes the transform already applied to the rendered content.
|
||||
// FSO does not pre-rotate it, so request identity whenever supported. If this
|
||||
// differs from currentTransform (e.g. landscape on an Android phone), the
|
||||
// presentation engine compensates during presentation. Passing currentTransform
|
||||
// instead would claim that FSO had already performed that rotation.
|
||||
auto supported = deviceValues.surfaceCapabilities.supportedTransforms;
|
||||
if (supported & vk::SurfaceTransformFlagBitsKHR::eIdentity) {
|
||||
createInfo.preTransform = vk::SurfaceTransformFlagBitsKHR::eIdentity;
|
||||
} else {
|
||||
createInfo.preTransform = deviceValues.surfaceCapabilities.currentTransform;
|
||||
}
|
||||
createInfo.compositeAlpha = vk::CompositeAlphaFlagBitsKHR::eOpaque;
|
||||
createInfo.presentMode = choosePresentMode(deviceValues);
|
||||
createInfo.clipped = true;
|
||||
createInfo.oldSwapchain = oldSwapchain;
|
||||
|
||||
auto newSwapChain = m_device->createSwapchainKHRUnique(createInfo);
|
||||
|
||||
// Clear old resources before replacing the swap chain
|
||||
target.framebuffers.clear();
|
||||
target.imageViews.clear();
|
||||
|
||||
target.swapChain = std::move(newSwapChain);
|
||||
|
||||
auto swapChainImages = m_device->getSwapchainImagesKHR(target.swapChain.get());
|
||||
target.images.assign(swapChainImages.begin(), swapChainImages.end());
|
||||
target.imageFormat = surfaceFormat.format;
|
||||
target.colorSpace = surfaceFormat.colorSpace;
|
||||
target.hdrActive = (surfaceFormat.colorSpace == vk::ColorSpaceKHR::eHdr10St2084EXT);
|
||||
Gr_hdr_output_active = target.hdrActive;
|
||||
target.extent = createInfo.imageExtent;
|
||||
target.preTransform = createInfo.preTransform;
|
||||
|
||||
// Only the main target can draw at a size other than its window. Without -window_res the render
|
||||
// resolution simply is the window, so follow gr_screen (which gr_screen_resize() has already
|
||||
// updated for this recreation). With it, the two are deliberately independent -- the window is
|
||||
// whatever Cmdline_window_res says (SDLGraphicsOperations::createViewport forces it) while the
|
||||
// engine keeps drawing at gr_screen.max_w/max_h -- so the value captured in initialize() must
|
||||
// survive untouched. qtFRED's extra viewports have no such split: useViewport() resizes
|
||||
// gr_screen to whichever one is current, so they draw at their own swap chain extent.
|
||||
if (&target == m_mainTarget) {
|
||||
if (!Cmdline_window_res) {
|
||||
m_renderExtent =
|
||||
vk::Extent2D(static_cast<uint32_t>(gr_screen.max_w), static_cast<uint32_t>(gr_screen.max_h));
|
||||
}
|
||||
target.renderExtent = m_renderExtent;
|
||||
} else {
|
||||
target.renderExtent = target.extent;
|
||||
}
|
||||
|
||||
mprintf(("Vulkan: Swap chain output mode: %s\n", target.hdrActive ? "HDR10 (PQ/BT.2020)" : "SDR (sRGB)"));
|
||||
mprintf(("Vulkan: Render resolution %ux%u, window/swap chain %ux%u\n",
|
||||
target.renderExtent.width, target.renderExtent.height,
|
||||
target.extent.width, target.extent.height));
|
||||
|
||||
target.imageViews.reserve(target.images.size());
|
||||
for (const auto& image : target.images) {
|
||||
vk::ImageViewCreateInfo viewCreateInfo;
|
||||
viewCreateInfo.image = image;
|
||||
viewCreateInfo.viewType = vk::ImageViewType::e2D;
|
||||
viewCreateInfo.format = target.imageFormat;
|
||||
|
||||
viewCreateInfo.components.r = vk::ComponentSwizzle::eIdentity;
|
||||
viewCreateInfo.components.g = vk::ComponentSwizzle::eIdentity;
|
||||
viewCreateInfo.components.b = vk::ComponentSwizzle::eIdentity;
|
||||
viewCreateInfo.components.a = vk::ComponentSwizzle::eIdentity;
|
||||
|
||||
viewCreateInfo.subresourceRange.aspectMask = vk::ImageAspectFlagBits::eColor;
|
||||
viewCreateInfo.subresourceRange.baseMipLevel = 0;
|
||||
viewCreateInfo.subresourceRange.levelCount = 1;
|
||||
viewCreateInfo.subresourceRange.baseArrayLayer = 0;
|
||||
viewCreateInfo.subresourceRange.layerCount = 1;
|
||||
|
||||
target.imageViews.push_back(m_device->createImageViewUnique(viewCreateInfo));
|
||||
}
|
||||
|
||||
// No layout transition needed for the new images: the only pass that writes
|
||||
// them (m_encodeRenderPass) uses initialLayout=eUndefined with
|
||||
// loadOp=eDontCare, so their first use never reads prior contents.
|
||||
|
||||
// Advertise HDR10 mastering/content metadata to the compositor when active.
|
||||
if (target.hdrActive && m_hdrMetadataSupported) {
|
||||
vk::HdrMetadataEXT metadata;
|
||||
// BT.2020 display primaries and D65 white point
|
||||
metadata.displayPrimaryRed = vk::XYColorEXT{0.708f, 0.292f};
|
||||
metadata.displayPrimaryGreen = vk::XYColorEXT{0.170f, 0.797f};
|
||||
metadata.displayPrimaryBlue = vk::XYColorEXT{0.131f, 0.046f};
|
||||
metadata.whitePoint = vk::XYColorEXT{0.3127f, 0.3290f};
|
||||
metadata.maxLuminance = Gr_hdr_peak_nits;
|
||||
metadata.minLuminance = 0.0f;
|
||||
metadata.maxContentLightLevel = Gr_hdr_peak_nits;
|
||||
metadata.maxFrameAverageLightLevel = Gr_hdr_paperwhite_nits;
|
||||
m_device->setHdrMetadataEXT(target.swapChain.get(), metadata);
|
||||
mprintf(("Vulkan: HDR10 metadata set (peak %.0f nits, paper white %.0f nits)\n",
|
||||
Gr_hdr_peak_nits, Gr_hdr_paperwhite_nits));
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool VulkanRenderer::shouldRecreateSwapChain(const VulkanPresentTarget& target, SwapChainStatus status) const
|
||||
{
|
||||
// An already requested rebuild also avoids redundant Android surface queries.
|
||||
if (target.needsRecreation || status == SwapChainStatus::eOutOfDate) {
|
||||
return true;
|
||||
}
|
||||
if (status != SwapChainStatus::eSuboptimal) {
|
||||
return false;
|
||||
}
|
||||
|
||||
#ifdef __ANDROID__
|
||||
if (target.preTransform == vk::SurfaceTransformFlagBitsKHR::eIdentity) {
|
||||
vk::SurfaceCapabilitiesKHR capabilities;
|
||||
try {
|
||||
capabilities = m_physicalDevice.getSurfaceCapabilitiesKHR(target.surface.get());
|
||||
} catch (const vk::SystemError&) {
|
||||
// Let the normal recreation path handle a failed surface query.
|
||||
return true;
|
||||
}
|
||||
if (capabilities.currentTransform != target.preTransform &&
|
||||
(capabilities.supportedTransforms & target.preTransform)) {
|
||||
const auto window = windowExtent(target);
|
||||
const auto extent = chooseSwapChainExtent(capabilities, window.width, window.height);
|
||||
if (extent == target.extent) {
|
||||
// Android can report SUBOPTIMAL on every present while the compositor
|
||||
// compensates for our identity preTransform. Rebuilding with the same
|
||||
// extent and preTransform would just repeat that condition next frame.
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool VulkanRenderer::recreateSwapChain(VulkanPresentTarget& target)
|
||||
{
|
||||
nprintf(("vulkan", "Vulkan: Recreating swap chain...\n"));
|
||||
|
||||
// Wait for all frames to finish so no resources are in use
|
||||
for (uint32_t i = 0; i < MAX_FRAMES_IN_FLIGHT; ++i) {
|
||||
target.frames[i]->waitForFinish();
|
||||
}
|
||||
m_device->waitIdle();
|
||||
|
||||
// Re-query surface state (may have changed due to resize/compositor)
|
||||
PhysicalDeviceValues freshValues;
|
||||
freshValues.device = m_physicalDevice;
|
||||
freshValues.graphicsQueueIndex = {true, m_graphicsQueueFamilyIndex};
|
||||
freshValues.presentQueueIndex = {true, m_presentQueueFamilyIndex};
|
||||
|
||||
if (!checkSwapChainSupport(freshValues, target.surface.get())) {
|
||||
nprintf(("vulkan", "Vulkan: surface no longer reports usable formats or present modes, "
|
||||
"deferring swap chain recreation\n"));
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check for 0x0 extent (minimized window) — caller should retry later
|
||||
const vk::Extent2D window = windowExtent(target);
|
||||
auto extent = chooseSwapChainExtent(freshValues.surfaceCapabilities, window.width, window.height);
|
||||
if (extent.width == 0 || extent.height == 0) {
|
||||
nprintf(("vulkan", "Vulkan: Surface extent is 0x0 (minimized), deferring swap chain recreation\n"));
|
||||
return false;
|
||||
}
|
||||
|
||||
// Recreate all size-dependent resources. The render passes (including
|
||||
// m_encodeRenderPass) are intentionally NOT recreated so cached pipelines
|
||||
// remain valid; only images, views, and framebuffers are rebuilt.
|
||||
const vk::Format oldSwapChainFormat = target.imageFormat;
|
||||
createSwapChain(target, freshValues, target.swapChain.get());
|
||||
|
||||
// Known limitation: if the surface format changes across recreation (e.g.
|
||||
// the window moves to a display that flips HDR10 availability),
|
||||
// m_encodeRenderPass and the post-processor's LDR format would need a full
|
||||
// rebuild, which we don't support yet. Log it loudly.
|
||||
if (target.imageFormat != oldSwapChainFormat) {
|
||||
mprintf(("Vulkan: WARNING - swap chain surface format changed across recreation (%d -> %d); "
|
||||
"rendering may be broken until restart\n",
|
||||
static_cast<int>(oldSwapChainFormat), static_cast<int>(target.imageFormat)));
|
||||
}
|
||||
|
||||
// The depth buffer is extent-sized; recreate it before the framebuffers
|
||||
// that attach its view. createDepthResources() verifies the format is stable
|
||||
// (the kept render passes bake it in).
|
||||
destroyDepthResources(target);
|
||||
createDepthResources(target);
|
||||
|
||||
createCompositionResources(target);
|
||||
createFrameBuffers(target);
|
||||
|
||||
// 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(target.renderExtent)) {
|
||||
mprintf(("Vulkan: post-processor resize failed, disabling post-processing!\n"));
|
||||
setPostProcessor(nullptr);
|
||||
m_postProcessor->shutdown();
|
||||
m_postProcessor.reset();
|
||||
}
|
||||
|
||||
// Drop renderer-side cached state that may reference destroyed views
|
||||
if (m_drawManager) {
|
||||
m_drawManager->onResize();
|
||||
}
|
||||
m_sceneDepthCopiedThisFrame = false;
|
||||
|
||||
// Update VulkanRenderFrame handles to point to the new swap chain.
|
||||
for (auto& frame : target.frames) {
|
||||
frame->updateSwapChain(target.swapChain.get());
|
||||
}
|
||||
|
||||
// The render-finished semaphores are keyed on swap chain image, and the image count can change
|
||||
// across recreation, so they go with the images they belonged to. Any of them still held by the
|
||||
// presentation engine belonged to the old swap chain, which is retired here. All frames are
|
||||
// idle (waited above), so nothing is still signalling one.
|
||||
createRenderFinishedSemaphores(target);
|
||||
|
||||
// The acquire semaphores were signalled against the swap chain that just went away, and any
|
||||
// image a viewport switch was holding on to belonged to it too. Start both over.
|
||||
constexpr vk::SemaphoreCreateInfo semaphoreCreateInfo;
|
||||
for (auto& acquire : target.acquireSemaphores) {
|
||||
acquire.semaphore = m_device->createSemaphoreUnique(semaphoreCreateInfo);
|
||||
acquire.consumer = nullptr;
|
||||
}
|
||||
target.nextAcquire = 0;
|
||||
target.currentAcquire = 0;
|
||||
target.hasRetainedAcquire = false;
|
||||
|
||||
// Reset swap chain image tracking
|
||||
target.imageRenderFrame.clear();
|
||||
target.imageRenderFrame.resize(target.images.size(), nullptr);
|
||||
target.previousImage = UINT32_MAX;
|
||||
|
||||
target.needsRecreation = false;
|
||||
|
||||
nprintf(("vulkan", "Vulkan: Swap chain recreated successfully (%ux%u, %zu images)\n",
|
||||
target.extent.width, target.extent.height, target.images.size()));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace graphics::vulkan
|
||||
@@ -0,0 +1,173 @@
|
||||
#pragma once
|
||||
|
||||
#include "osapi/osapi.h"
|
||||
#include "osapi/vulkan_surface.h"
|
||||
|
||||
#include "VulkanConstants.h"
|
||||
#include "VulkanMemory.h"
|
||||
#include "VulkanRenderFrame.h"
|
||||
|
||||
#include <vulkan/vulkan.hpp>
|
||||
|
||||
#include <array>
|
||||
#include <memory>
|
||||
|
||||
namespace graphics::vulkan {
|
||||
|
||||
// Filled in by checkSwapChainSupport() below; defined in VulkanRenderer.h, which owns
|
||||
// device selection. Only referenced here, so a declaration is enough.
|
||||
struct PhysicalDeviceValues;
|
||||
|
||||
/**
|
||||
* @brief Owns the VkSurfaceKHR the windowing system handed us
|
||||
*
|
||||
* Not a vk::UniqueSurfaceKHR, because the surface is not ours to destroy with vkDestroySurfaceKHR:
|
||||
* os::VulkanSurfaceProvider created it and only it knows how to get rid of it (a Qt-backed
|
||||
* implementation, for instance, hands out a surface owned by QVulkanInstance). Destruction order is
|
||||
* still the usual one -- declare this after the instance and before the swap chain, so the swap
|
||||
* chain goes first, then the surface, then the instance.
|
||||
*/
|
||||
class VulkanSurfaceHandle {
|
||||
public:
|
||||
VulkanSurfaceHandle() = default;
|
||||
VulkanSurfaceHandle(os::VulkanSurfaceProvider* provider, vk::Instance instance, vk::SurfaceKHR surface);
|
||||
~VulkanSurfaceHandle();
|
||||
|
||||
VulkanSurfaceHandle(const VulkanSurfaceHandle&) = delete;
|
||||
VulkanSurfaceHandle& operator=(const VulkanSurfaceHandle&) = delete;
|
||||
VulkanSurfaceHandle(VulkanSurfaceHandle&& other) noexcept;
|
||||
VulkanSurfaceHandle& operator=(VulkanSurfaceHandle&& other) noexcept;
|
||||
|
||||
vk::SurfaceKHR get() const { return m_surface; }
|
||||
explicit operator bool() const { return static_cast<bool>(m_surface); }
|
||||
|
||||
void reset();
|
||||
|
||||
private:
|
||||
os::VulkanSurfaceProvider* m_provider = nullptr;
|
||||
vk::Instance m_instance;
|
||||
vk::SurfaceKHR m_surface;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief One presentable surface and everything sized to it
|
||||
*
|
||||
* The renderer used to hold exactly one of each of these, which is all the game ever needs. qtFRED
|
||||
* presents through two independent windows -- its main viewport and the briefing map, which renders
|
||||
* on its own timer -- and switches between them with gr_use_viewport(), so each needs its own
|
||||
* surface, swap chain and extent-sized resources.
|
||||
*
|
||||
* What is *not* here is as deliberate as what is: the render passes, the post-processor and the
|
||||
* frame-in-flight cursor stay on the renderer. See the comments on those members.
|
||||
*
|
||||
* The sync objects are per-target but indexed by the renderer's shared m_currentFrame, so a target
|
||||
* that has not been drawn to for a while still has its slot waited on before reuse.
|
||||
*/
|
||||
struct VulkanPresentTarget {
|
||||
os::Viewport* viewport = nullptr;
|
||||
|
||||
VulkanSurfaceHandle surface;
|
||||
|
||||
vk::UniqueSwapchainKHR swapChain;
|
||||
vk::Format imageFormat = vk::Format::eUndefined;
|
||||
vk::ColorSpaceKHR colorSpace = vk::ColorSpaceKHR::eSrgbNonlinear;
|
||||
bool hdrActive = false; // True when an HDR10 (PQ/BT.2020) swap chain was negotiated
|
||||
vk::Extent2D extent;
|
||||
// The transform the swap chain was made with. See VulkanRenderer::shouldRecreateSwapChain().
|
||||
vk::SurfaceTransformFlagBitsKHR preTransform = vk::SurfaceTransformFlagBitsKHR::eIdentity;
|
||||
|
||||
// The size everything before the output-encode pass is drawn at: the composition image, the
|
||||
// depth buffer and the post-processor's targets. Usually the same as extent, but the main
|
||||
// target keeps the two apart when -window_res is set (which -vr forces): the engine goes on
|
||||
// drawing at gr_screen.max_w/max_h while the window is smaller, and the encode pass scales one
|
||||
// into the other. See VulkanRenderer::m_renderExtent and createSwapChain().
|
||||
vk::Extent2D renderExtent;
|
||||
|
||||
SCP_vector<vk::Image> images;
|
||||
SCP_vector<vk::UniqueImageView> imageViews;
|
||||
SCP_vector<vk::UniqueFramebuffer> framebuffers;
|
||||
SCP_vector<VulkanRenderFrame*> imageRenderFrame;
|
||||
|
||||
// HDR composition pipeline: the whole frame is rendered into these fp16 images (via
|
||||
// m_renderPass / framebuffers) instead of directly into the swap chain image.
|
||||
// encodeToSwapChain() converts composition -> swap chain: a direct blit (or
|
||||
// encodeOutputPassthrough() as a fallback) for SDR, or encodeOutput() (m_encodeRenderPass +
|
||||
// encodeFramebuffers) for the HDR10 PQ/BT.2020 transfer.
|
||||
SCP_vector<vk::UniqueImage> compositionImages;
|
||||
SCP_vector<vk::UniqueImageView> compositionImageViews;
|
||||
SCP_vector<VulkanAllocation> compositionAllocations;
|
||||
SCP_vector<vk::UniqueFramebuffer> encodeFramebuffers;
|
||||
|
||||
// Depth buffer
|
||||
vk::UniqueImage depthImage;
|
||||
vk::UniqueImageView depthImageView;
|
||||
VulkanAllocation depthImageMemory;
|
||||
|
||||
std::array<std::unique_ptr<VulkanRenderFrame>, MAX_FRAMES_IN_FLIGHT> frames;
|
||||
|
||||
// Acquire semaphores live here rather than in the frames because an acquire can outlive the
|
||||
// frame slot that made it -- see the retained acquire below. Each remembers the frame whose
|
||||
// submit waits on it, so it is not handed out again until that frame has completed.
|
||||
struct AcquireSemaphore {
|
||||
vk::UniqueSemaphore semaphore;
|
||||
VulkanRenderFrame* consumer = nullptr;
|
||||
};
|
||||
SCP_vector<AcquireSemaphore> acquireSemaphores;
|
||||
uint32_t nextAcquire = 0; // round-robin cursor into acquireSemaphores
|
||||
uint32_t currentAcquire = 0; // the one the in-progress frame will present with
|
||||
|
||||
// One per swap chain image, indexed by image index -- not per frame-in-flight. A binary
|
||||
// semaphore handed to vkQueuePresentKHR stays in use by the presentation engine until that
|
||||
// image is acquired again, so the only safe moment to signal it once more is after an acquire
|
||||
// has returned that same image. Keying it on the image is what makes that automatic; keying it
|
||||
// on the frame slot (2 of them against 4 images) meant a submit could re-signal a semaphore the
|
||||
// presentation engine still held (VUID-vkQueueSubmit-pSignalSemaphores-00067). That normally
|
||||
// resolves itself once the image comes round again -- but a target that stops presenting (the
|
||||
// main viewport, once qtFRED's briefing map is driving the render loop) never re-acquires it,
|
||||
// and the pending question hangs the validation layer's state tracking for good.
|
||||
SCP_vector<vk::UniqueSemaphore> renderFinishedSemaphores;
|
||||
|
||||
// vkAcquireNextImageKHR hands out an image that only a present gives back, so a viewport switch
|
||||
// cannot simply walk away from one: doing that leaks an image per switch and wedges the swap
|
||||
// chain within a few frames. The acquire is kept here instead and reused when this target
|
||||
// becomes current again.
|
||||
bool hasRetainedAcquire = false;
|
||||
uint32_t retainedAcquire = 0;
|
||||
uint32_t retainedImage = 0;
|
||||
|
||||
uint32_t currentImage = 0;
|
||||
uint32_t previousImage = UINT32_MAX; // For saveScreen() readback of previous frame
|
||||
|
||||
bool needsRecreation = false;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Identifies the exact fence a gr_sync_fence() was taken against
|
||||
*
|
||||
* All three fields are needed to find it back. The frame number says which frame's work is meant,
|
||||
* but it does not locate the fence: those live on the frame-in-flight slots of a *target*, and both
|
||||
* of the other two can have moved on by the time the wait happens -- a viewport switch changes the
|
||||
* target, and the slot cycles every MAX_FRAMES_IN_FLIGHT frames.
|
||||
*/
|
||||
struct FrameSyncPoint {
|
||||
// The target's viewport rather than the target itself, deliberately: a target is destroyed when
|
||||
// its viewport closes, and a sync point can outlive it. Looking the viewport up in m_targets
|
||||
// answers "is that target still around?" instead of dereferencing a dangling pointer.
|
||||
os::Viewport* viewport = nullptr;
|
||||
|
||||
uint32_t slot = 0; // index into VulkanPresentTarget::frames
|
||||
uint64_t frameNumber = 0; // VulkanRenderer::m_frameNumber at the time
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Fill in the parts of @p values that depend on a particular surface
|
||||
*
|
||||
* Every one of these can differ per surface, so this has to be re-run for each one rather than
|
||||
* carried over from the surface the device was picked against -- see createTargetResources().
|
||||
*
|
||||
* @return false if the surface reports no usable formats or present modes, i.e. cannot be presented
|
||||
* to at all
|
||||
*/
|
||||
bool checkSwapChainSupport(PhysicalDeviceValues& values, vk::SurfaceKHR surface);
|
||||
|
||||
} // namespace graphics::vulkan
|
||||
@@ -275,6 +275,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 actually ran, for the
|
||||
// per-frame idempotence guard (multiple callers may request a TLAS per frame).
|
||||
uint64_t m_lastTlasBuildFrame = UINT64_MAX;
|
||||
|
||||
vk::Device m_device;
|
||||
vk::PhysicalDevice m_physicalDevice;
|
||||
VulkanMemoryManager* m_memoryManager = nullptr;
|
||||
|
||||
@@ -425,6 +425,16 @@ void VulkanRaytracingManager::buildTlas()
|
||||
return;
|
||||
}
|
||||
|
||||
// Idempotence guard: both the shadow path (shadows_render_all ->
|
||||
// gr_build_shadow_tlas) and the RTAO fallback trigger
|
||||
// (vulkan_deferred_lighting_finish, for when shadow rendering is disabled)
|
||||
// may request a TLAS in the same frame -- only the first request builds.
|
||||
const uint64_t frameNumber = m_bufferManager->getCurrentFrameNumber();
|
||||
if (m_lastTlasBuildFrame == frameNumber) {
|
||||
return;
|
||||
}
|
||||
m_lastTlasBuildFrame = frameNumber;
|
||||
|
||||
// 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).
|
||||
|
||||
@@ -6,10 +6,8 @@ namespace graphics::vulkan {
|
||||
VulkanRenderFrame::VulkanRenderFrame(vk::Device device, vk::SwapchainKHR swapChain, vk::Queue graphicsQueue, vk::Queue presentQueue)
|
||||
: m_device(device), m_swapChain(swapChain), m_graphicsQueue(graphicsQueue), m_presentQueue(presentQueue)
|
||||
{
|
||||
constexpr vk::SemaphoreCreateInfo semaphoreCreateInfo;
|
||||
constexpr vk::FenceCreateInfo fenceCreateInfo;
|
||||
|
||||
m_imageAvailableSemaphore = device.createSemaphoreUnique(semaphoreCreateInfo);
|
||||
m_frameInFlightFence = device.createFenceUnique(fenceCreateInfo);
|
||||
}
|
||||
bool VulkanRenderFrame::waitForFinish(uint64_t timeoutNs)
|
||||
@@ -39,7 +37,7 @@ void VulkanRenderFrame::onFrameFinished(std::function<void()> finishFunc)
|
||||
{
|
||||
m_frameFinishedCallbacks.push_back(std::move(finishFunc));
|
||||
}
|
||||
SwapChainStatus VulkanRenderFrame::acquireSwapchainImage(uint32_t& outImageIndex)
|
||||
SwapChainStatus VulkanRenderFrame::acquireSwapchainImage(uint32_t& outImageIndex, vk::Semaphore imageAvailable)
|
||||
{
|
||||
Assertion(!m_inFlight, "Cannot acquire swapchain image when frame is still in flight.");
|
||||
|
||||
@@ -50,7 +48,7 @@ SwapChainStatus VulkanRenderFrame::acquireSwapchainImage(uint32_t& outImageIndex
|
||||
try {
|
||||
res = m_device.acquireNextImageKHR(m_swapChain,
|
||||
std::numeric_limits<uint64_t>::max(),
|
||||
m_imageAvailableSemaphore.get(),
|
||||
imageAvailable,
|
||||
nullptr,
|
||||
&imageIndex);
|
||||
} catch (const vk::OutOfDateKHRError&) {
|
||||
@@ -73,7 +71,6 @@ SwapChainStatus VulkanRenderFrame::acquireSwapchainImage(uint32_t& outImageIndex
|
||||
return SwapChainStatus::eOutOfDate;
|
||||
}
|
||||
|
||||
m_swapChainIdx = imageIndex;
|
||||
outImageIndex = imageIndex;
|
||||
|
||||
if (res == vk::Result::eSuboptimalKHR) {
|
||||
@@ -82,14 +79,18 @@ SwapChainStatus VulkanRenderFrame::acquireSwapchainImage(uint32_t& outImageIndex
|
||||
return SwapChainStatus::eSuccess;
|
||||
}
|
||||
SwapChainStatus VulkanRenderFrame::submitAndPresent(const SCP_vector<vk::CommandBuffer>& cmdBuffers,
|
||||
vk::Semaphore renderingFinished)
|
||||
vk::Semaphore imageAvailable, vk::Semaphore renderFinished, uint32_t imageIndex, uint64_t frameNumber)
|
||||
{
|
||||
Assertion(!m_inFlight, "Cannot submit a frame for presentation when it is still in flight.");
|
||||
|
||||
// Record before the submit rather than after: from the moment the queue takes the work, the
|
||||
// fence guards this frame number, and a sync point resolved in between must see that.
|
||||
m_submittedFrameNumber = frameNumber;
|
||||
|
||||
// Wait at color attachment output stage — the first use of the swap chain image
|
||||
// is loadOp=eClear at the start of the render pass, which is a color attachment write.
|
||||
const std::array<vk::PipelineStageFlags, 1> waitStages = {vk::PipelineStageFlagBits::eColorAttachmentOutput};
|
||||
const std::array<vk::Semaphore, 1> waitSemaphores = {m_imageAvailableSemaphore.get()};
|
||||
const std::array<vk::Semaphore, 1> waitSemaphores = {imageAvailable};
|
||||
|
||||
vk::SubmitInfo submitInfo;
|
||||
submitInfo.waitSemaphoreCount = 1;
|
||||
@@ -99,7 +100,7 @@ SwapChainStatus VulkanRenderFrame::submitAndPresent(const SCP_vector<vk::Command
|
||||
submitInfo.commandBufferCount = static_cast<uint32_t>(cmdBuffers.size());
|
||||
submitInfo.pCommandBuffers = cmdBuffers.data();
|
||||
|
||||
const std::array<vk::Semaphore, 1> signalSemaphores = {renderingFinished};
|
||||
const std::array<vk::Semaphore, 1> signalSemaphores = {renderFinished};
|
||||
submitInfo.signalSemaphoreCount = 1;
|
||||
submitInfo.pSignalSemaphores = signalSemaphores.data();
|
||||
|
||||
@@ -115,7 +116,7 @@ SwapChainStatus VulkanRenderFrame::submitAndPresent(const SCP_vector<vk::Command
|
||||
const std::array<vk::SwapchainKHR, 1> swapChains = {m_swapChain};
|
||||
presentInfo.swapchainCount = 1;
|
||||
presentInfo.pSwapchains = swapChains.data();
|
||||
presentInfo.pImageIndices = &m_swapChainIdx;
|
||||
presentInfo.pImageIndices = &imageIndex;
|
||||
presentInfo.pResults = nullptr;
|
||||
|
||||
vk::Result res;
|
||||
@@ -141,12 +142,5 @@ void VulkanRenderFrame::updateSwapChain(vk::SwapchainKHR swapChain)
|
||||
{
|
||||
m_swapChain = swapChain;
|
||||
}
|
||||
void VulkanRenderFrame::recreateSyncObjects()
|
||||
{
|
||||
Assertion(!m_inFlight, "Cannot recreate sync objects while the frame is in flight.");
|
||||
|
||||
constexpr vk::SemaphoreCreateInfo semaphoreCreateInfo;
|
||||
m_imageAvailableSemaphore = m_device.createSemaphoreUnique(semaphoreCreateInfo);
|
||||
}
|
||||
|
||||
} // namespace graphics::vulkan
|
||||
@@ -26,22 +26,52 @@ class VulkanRenderFrame {
|
||||
*/
|
||||
bool waitForFinish(uint64_t timeoutNs = std::numeric_limits<uint64_t>::max());
|
||||
|
||||
SwapChainStatus acquireSwapchainImage(uint32_t& outImageIndex);
|
||||
/**
|
||||
* @brief Acquire an image, signalling a semaphore the caller owns
|
||||
*
|
||||
* The semaphore belongs to the present target rather than to this frame: an acquire can outlive
|
||||
* the frame slot that made it, because switching viewports mid-frame retains the acquired image
|
||||
* and hands it back when that viewport becomes current again -- by which time the shared
|
||||
* frame-in-flight cursor has moved on, and it cannot move back (the descriptor manager asserts
|
||||
* that it only ever advances).
|
||||
*/
|
||||
SwapChainStatus acquireSwapchainImage(uint32_t& outImageIndex, vk::Semaphore imageAvailable);
|
||||
|
||||
void onFrameFinished(std::function<void()> finishFunc);
|
||||
|
||||
SwapChainStatus submitAndPresent(const SCP_vector<vk::CommandBuffer>& cmdBuffers, vk::Semaphore renderingFinished);
|
||||
|
||||
void updateSwapChain(vk::SwapchainKHR swapChain);
|
||||
/**
|
||||
* @brief Submit this frame's work and present the acquired image
|
||||
*
|
||||
* Neither semaphore belongs to this frame. Both are owned by the present target: the acquire
|
||||
* because it can outlive the frame slot that made it (see acquireSwapchainImage()), and the
|
||||
* render-finished one because it is keyed on the swap chain image rather than on the frame
|
||||
* slot -- the presentation engine keeps hold of it until that image is acquired again.
|
||||
*
|
||||
* @param imageAvailable the semaphore the acquire signalled
|
||||
* @param renderFinished the semaphore for @p imageIndex; signalled by the submit, waited on by
|
||||
* the present
|
||||
* @param imageIndex the image that acquire returned
|
||||
* @param frameNumber the monotonic frame number this submission covers; remembered so a
|
||||
* sync point taken during that frame can tell whether this fence is
|
||||
* still the one guarding its work (see getSubmittedFrameNumber())
|
||||
*/
|
||||
SwapChainStatus submitAndPresent(const SCP_vector<vk::CommandBuffer>& cmdBuffers,
|
||||
vk::Semaphore imageAvailable, vk::Semaphore renderFinished, uint32_t imageIndex,
|
||||
uint64_t frameNumber);
|
||||
|
||||
/**
|
||||
* @brief Recreate the per-frame semaphores (frame must not be in flight)
|
||||
* @brief The frame number of the submission this fence currently guards
|
||||
*
|
||||
* Called during swap chain recreation: an acquire that succeeded against the
|
||||
* old swap chain but was never consumed by a submit leaves the
|
||||
* image-available semaphore signaled, which would corrupt the next acquire.
|
||||
* NEVER_SUBMITTED until the first submit. A frame slot is reused every MAX_FRAMES_IN_FLIGHT
|
||||
* frames, so the fence alone does not say *which* frame's work it covers -- this does, which is
|
||||
* what lets VulkanRenderer::waitForSyncPoint() tell "still guarding the work I care about" from
|
||||
* "already recycled by a later frame".
|
||||
*/
|
||||
void recreateSyncObjects();
|
||||
uint64_t getSubmittedFrameNumber() const { return m_submittedFrameNumber; }
|
||||
|
||||
static constexpr uint64_t NEVER_SUBMITTED = std::numeric_limits<uint64_t>::max();
|
||||
|
||||
void updateSwapChain(vk::SwapchainKHR swapChain);
|
||||
|
||||
private:
|
||||
vk::Device m_device;
|
||||
@@ -49,13 +79,12 @@ class VulkanRenderFrame {
|
||||
vk::Queue m_graphicsQueue;
|
||||
vk::Queue m_presentQueue;
|
||||
|
||||
vk::UniqueSemaphore m_imageAvailableSemaphore;
|
||||
vk::UniqueFence m_frameInFlightFence;
|
||||
SCP_vector<std::function<void()>> m_frameFinishedCallbacks;
|
||||
|
||||
bool m_inFlight = false;
|
||||
|
||||
uint32_t m_swapChainIdx = 0;
|
||||
uint64_t m_submittedFrameNumber = NEVER_SUBMITTED;
|
||||
};
|
||||
|
||||
} // namespace graphics::vulkan
|
||||
@@ -24,78 +24,20 @@ extern float flFrametime;
|
||||
|
||||
namespace graphics::vulkan {
|
||||
|
||||
// VulkanSurfaceHandle's implementation lives in VulkanRendererSetup.cpp, next to
|
||||
// createTargetSurface()/createTargetResources() -- the rest of a target's surface/swap-chain
|
||||
// lifecycle.
|
||||
|
||||
VulkanRenderer::VulkanRenderer(std::unique_ptr<os::GraphicsOperations> graphicsOps)
|
||||
: m_graphicsOps(std::move(graphicsOps))
|
||||
{
|
||||
}
|
||||
void VulkanRenderer::createCompositionResources()
|
||||
{
|
||||
// Free any previous composition resources (swap chain recreation path)
|
||||
m_compositionImageViews.clear();
|
||||
m_compositionImages.clear();
|
||||
for (auto& alloc : m_compositionAllocations) {
|
||||
if (alloc.isValid()) {
|
||||
m_memoryManager->freeAllocation(alloc);
|
||||
}
|
||||
}
|
||||
m_compositionAllocations.clear();
|
||||
|
||||
const size_t count = m_swapChainImageViews.size();
|
||||
m_compositionImages.reserve(count);
|
||||
m_compositionImageViews.reserve(count);
|
||||
m_compositionAllocations.reserve(count);
|
||||
|
||||
for (size_t i = 0; i < count; ++i) {
|
||||
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.mipLevels = 1;
|
||||
imageInfo.arrayLayers = 1;
|
||||
imageInfo.samples = vk::SampleCountFlagBits::e1;
|
||||
imageInfo.tiling = vk::ImageTiling::eOptimal;
|
||||
imageInfo.usage = vk::ImageUsageFlagBits::eColorAttachment | vk::ImageUsageFlagBits::eSampled |
|
||||
vk::ImageUsageFlagBits::eTransferSrc;
|
||||
imageInfo.sharingMode = vk::SharingMode::eExclusive;
|
||||
imageInfo.initialLayout = vk::ImageLayout::eUndefined;
|
||||
|
||||
auto image = m_device->createImageUnique(imageInfo);
|
||||
|
||||
VulkanAllocation alloc{};
|
||||
m_memoryManager->allocateImageMemory(image.get(), MemoryUsage::GpuOnly, alloc);
|
||||
|
||||
vk::ImageViewCreateInfo viewInfo;
|
||||
viewInfo.image = image.get();
|
||||
viewInfo.viewType = vk::ImageViewType::e2D;
|
||||
viewInfo.format = HDR_COLOR_FORMAT;
|
||||
viewInfo.subresourceRange = {vk::ImageAspectFlagBits::eColor, 0, 1, 0, 1};
|
||||
auto view = m_device->createImageViewUnique(viewInfo);
|
||||
|
||||
m_compositionImages.push_back(std::move(image));
|
||||
m_compositionAllocations.push_back(alloc);
|
||||
m_compositionImageViews.push_back(std::move(view));
|
||||
}
|
||||
|
||||
// Sampler used by the output-encode pass to read the composition image.
|
||||
if (!m_compositionSampler) {
|
||||
vk::SamplerCreateInfo sampInfo;
|
||||
sampInfo.magFilter = vk::Filter::eNearest;
|
||||
sampInfo.minFilter = vk::Filter::eNearest;
|
||||
sampInfo.mipmapMode = vk::SamplerMipmapMode::eNearest;
|
||||
sampInfo.addressModeU = vk::SamplerAddressMode::eClampToEdge;
|
||||
sampInfo.addressModeV = vk::SamplerAddressMode::eClampToEdge;
|
||||
sampInfo.addressModeW = vk::SamplerAddressMode::eClampToEdge;
|
||||
m_compositionSampler = m_device->createSamplerUnique(sampInfo);
|
||||
}
|
||||
}
|
||||
|
||||
void VulkanRenderer::createEncodeRenderPass()
|
||||
void VulkanRenderer::createEncodeRenderPass(vk::Format swapChainFormat)
|
||||
{
|
||||
// Color-only pass that writes the actual swap chain image. The fullscreen
|
||||
// encode draw overwrites the whole image, so the prior contents are discarded.
|
||||
vk::AttachmentDescription colorAttachment;
|
||||
colorAttachment.format = m_swapChainImageFormat;
|
||||
colorAttachment.format = swapChainFormat;
|
||||
colorAttachment.samples = vk::SampleCountFlagBits::e1;
|
||||
colorAttachment.loadOp = vk::AttachmentLoadOp::eDontCare;
|
||||
colorAttachment.storeOp = vk::AttachmentStoreOp::eStore;
|
||||
@@ -137,62 +79,21 @@ void VulkanRenderer::createEncodeRenderPass()
|
||||
m_encodeRenderPass = m_device->createRenderPassUnique(rpInfo);
|
||||
}
|
||||
|
||||
void VulkanRenderer::createFrameBuffers()
|
||||
{
|
||||
m_swapChainFramebuffers.clear();
|
||||
m_encodeFramebuffers.clear();
|
||||
|
||||
// Composition framebuffers: color = fp16 composition image, depth shared.
|
||||
// Indexed by swap chain image so each in-flight frame uses its own image.
|
||||
m_swapChainFramebuffers.reserve(m_compositionImageViews.size());
|
||||
for (const auto& compView : m_compositionImageViews) {
|
||||
const vk::ImageView attachments[] = {
|
||||
compView.get(),
|
||||
m_depthImageView.get(),
|
||||
};
|
||||
|
||||
vk::FramebufferCreateInfo framebufferInfo;
|
||||
framebufferInfo.renderPass = m_renderPass.get();
|
||||
framebufferInfo.attachmentCount = 2;
|
||||
framebufferInfo.pAttachments = attachments;
|
||||
framebufferInfo.width = m_renderExtent.width;
|
||||
framebufferInfo.height = m_renderExtent.height;
|
||||
framebufferInfo.layers = 1;
|
||||
|
||||
m_swapChainFramebuffers.push_back(m_device->createFramebufferUnique(framebufferInfo));
|
||||
}
|
||||
|
||||
// Encode framebuffers: color = actual swap chain image.
|
||||
m_encodeFramebuffers.reserve(m_swapChainImageViews.size());
|
||||
for (const auto& scView : m_swapChainImageViews) {
|
||||
const vk::ImageView attachments[] = { scView.get() };
|
||||
|
||||
vk::FramebufferCreateInfo framebufferInfo;
|
||||
framebufferInfo.renderPass = m_encodeRenderPass.get();
|
||||
framebufferInfo.attachmentCount = 1;
|
||||
framebufferInfo.pAttachments = attachments;
|
||||
framebufferInfo.width = m_swapChainExtent.width;
|
||||
framebufferInfo.height = m_swapChainExtent.height;
|
||||
framebufferInfo.layers = 1;
|
||||
|
||||
m_encodeFramebuffers.push_back(m_device->createFramebufferUnique(framebufferInfo));
|
||||
}
|
||||
}
|
||||
|
||||
vk::Image VulkanRenderer::getCurrentCompositionImage() const
|
||||
{
|
||||
if (m_currentSwapChainImage >= m_compositionImages.size()) {
|
||||
if (m_current == nullptr || m_current->currentImage >= m_current->compositionImages.size()) {
|
||||
return {};
|
||||
}
|
||||
|
||||
return m_compositionImages[m_currentSwapChainImage].get();
|
||||
return m_current->compositionImages[m_current->currentImage].get();
|
||||
}
|
||||
|
||||
void VulkanRenderer::encodeToSwapChain()
|
||||
{
|
||||
if (!m_postProcessor || m_currentSwapChainImage >= m_swapChainImages.size()) {
|
||||
if (!m_postProcessor || m_current->currentImage >= m_current->images.size()) {
|
||||
return;
|
||||
}
|
||||
if (m_currentSwapChainImage >= m_encodeFramebuffers.size()) {
|
||||
if (m_current->currentImage >= m_current->encodeFramebuffers.size()) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -200,13 +101,13 @@ void VulkanRenderer::encodeToSwapChain()
|
||||
|
||||
// HDR10: PQ/BT.2020 encode plus the user gamma slider, must run as a
|
||||
// shader (encodeOutput()).
|
||||
if (m_hdrActive) {
|
||||
if (m_current->hdrActive) {
|
||||
m_postProcessor->encodeOutput(
|
||||
m_currentCommandBuffer,
|
||||
m_encodeRenderPass.get(),
|
||||
m_encodeFramebuffers[m_currentSwapChainImage].get(),
|
||||
m_swapChainExtent,
|
||||
m_compositionImageViews[m_currentSwapChainImage].get(),
|
||||
m_current->encodeFramebuffers[m_current->currentImage].get(),
|
||||
m_current->extent,
|
||||
m_current->compositionImageViews[m_current->currentImage].get(),
|
||||
m_compositionSampler.get(),
|
||||
Gr_hdr_paperwhite_nits,
|
||||
Gr_hdr_peak_nits,
|
||||
@@ -222,90 +123,12 @@ void VulkanRenderer::encodeToSwapChain()
|
||||
m_postProcessor->encodeOutputSdr(
|
||||
m_currentCommandBuffer,
|
||||
m_encodeRenderPass.get(),
|
||||
m_encodeFramebuffers[m_currentSwapChainImage].get(),
|
||||
m_swapChainExtent,
|
||||
m_compositionImageViews[m_currentSwapChainImage].get(),
|
||||
m_current->encodeFramebuffers[m_current->currentImage].get(),
|
||||
m_current->extent,
|
||||
m_current->compositionImageViews[m_current->currentImage].get(),
|
||||
m_compositionSampler.get(),
|
||||
gamma);
|
||||
}
|
||||
vk::Format VulkanRenderer::findDepthFormat()
|
||||
{
|
||||
// Prefer D32_SFLOAT for best precision, fall back to D32_SFLOAT_S8 or D24_UNORM_S8
|
||||
const vk::Format candidates[] = {
|
||||
vk::Format::eD32Sfloat,
|
||||
vk::Format::eD32SfloatS8Uint,
|
||||
vk::Format::eD24UnormS8Uint,
|
||||
};
|
||||
|
||||
for (auto format : candidates) {
|
||||
auto props = m_physicalDevice.getFormatProperties(format);
|
||||
if (props.optimalTilingFeatures & vk::FormatFeatureFlagBits::eDepthStencilAttachment) {
|
||||
return format;
|
||||
}
|
||||
}
|
||||
|
||||
// Should never happen on any real GPU
|
||||
Error(LOCATION, "Failed to find supported depth format!");
|
||||
return vk::Format::eD32Sfloat;
|
||||
}
|
||||
void VulkanRenderer::createDepthResources()
|
||||
{
|
||||
const vk::Format depthFormat = findDepthFormat();
|
||||
// The render passes (m_renderPass, scene/G-buffer passes, ...) bake in the
|
||||
// depth format, and they are deliberately kept alive across swap chain
|
||||
// recreation. A driver changing its supported depth formats mid-session
|
||||
// would make them all incompatible with the new attachment.
|
||||
if (m_depthFormat != vk::Format::eUndefined && depthFormat != m_depthFormat) {
|
||||
Error(LOCATION, "Vulkan: depth format changed across swap chain recreation (%d -> %d)!",
|
||||
static_cast<int>(m_depthFormat), static_cast<int>(depthFormat));
|
||||
}
|
||||
m_depthFormat = depthFormat;
|
||||
|
||||
// Create depth image
|
||||
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.depth = 1;
|
||||
imageInfo.mipLevels = 1;
|
||||
imageInfo.arrayLayers = 1;
|
||||
imageInfo.samples = vk::SampleCountFlagBits::e1;
|
||||
imageInfo.tiling = vk::ImageTiling::eOptimal;
|
||||
imageInfo.usage = vk::ImageUsageFlagBits::eDepthStencilAttachment;
|
||||
imageInfo.sharingMode = vk::SharingMode::eExclusive;
|
||||
imageInfo.initialLayout = vk::ImageLayout::eUndefined;
|
||||
|
||||
m_depthImage = m_device->createImageUnique(imageInfo);
|
||||
|
||||
// Allocate GPU memory for the depth image
|
||||
m_memoryManager->allocateImageMemory(m_depthImage.get(), MemoryUsage::GpuOnly, m_depthImageMemory);
|
||||
|
||||
// Create depth image view
|
||||
vk::ImageViewCreateInfo viewInfo;
|
||||
viewInfo.image = m_depthImage.get();
|
||||
viewInfo.viewType = vk::ImageViewType::e2D;
|
||||
viewInfo.format = m_depthFormat;
|
||||
viewInfo.subresourceRange.aspectMask = imageAspectFromFormat(m_depthFormat);
|
||||
viewInfo.subresourceRange.baseMipLevel = 0;
|
||||
viewInfo.subresourceRange.levelCount = 1;
|
||||
viewInfo.subresourceRange.baseArrayLayer = 0;
|
||||
viewInfo.subresourceRange.layerCount = 1;
|
||||
|
||||
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)));
|
||||
}
|
||||
void VulkanRenderer::destroyDepthResources()
|
||||
{
|
||||
m_depthImageView.reset();
|
||||
m_depthImage.reset();
|
||||
if (m_memoryManager && m_depthImageMemory.isValid()) {
|
||||
m_memoryManager->freeAllocation(m_depthImageMemory);
|
||||
m_depthImageMemory = {};
|
||||
}
|
||||
}
|
||||
void VulkanRenderer::createRenderPass()
|
||||
{
|
||||
// Attachment 0: Color - clear each frame
|
||||
@@ -351,7 +174,7 @@ void VulkanRenderer::createRenderPass()
|
||||
|
||||
// External dependency must make the PREVIOUS frame's accesses to these
|
||||
// attachments available/ordered before this frame writes them again. The
|
||||
// depth buffer is a single shared image (one m_depthImage for every
|
||||
// depth buffer is a single shared image (one m_current->depthImage for every
|
||||
// swap-chain framebuffer), so frame N+1's loadOp=eClear collides with frame
|
||||
// N's depth writes (WRITE_AFTER_WRITE) unless srcAccessMask lists the prior
|
||||
// depth write; likewise the composition color's store when a swap image
|
||||
@@ -394,7 +217,7 @@ void VulkanRenderer::createRenderPass()
|
||||
// Create a second render pass with loadOp=eLoad for resuming the composition
|
||||
// pass after post-processing. Render-pass compatibility (which is what lets a
|
||||
// pipeline built against m_renderPass bind under m_renderPassLoad, and lets
|
||||
// both share m_swapChainFramebuffers) is defined ONLY by matching attachment
|
||||
// both share m_current->framebuffers) is defined ONLY by matching attachment
|
||||
// formats/sample counts and subpass structure -- it deliberately ignores
|
||||
// load/store ops, layouts, AND subpass dependencies. So this variant reuses
|
||||
// the same renderPassInfo/dependency but only overrides the ops/layouts below.
|
||||
@@ -423,31 +246,13 @@ void VulkanRenderer::createCommandPool(const PhysicalDeviceValues& values)
|
||||
|
||||
m_graphicsCommandPool = m_device->createCommandPoolUnique(poolCreate);
|
||||
}
|
||||
void VulkanRenderer::createPresentSyncObjects()
|
||||
{
|
||||
for (size_t i = 0; i < MAX_FRAMES_IN_FLIGHT; ++i) {
|
||||
m_frames[i] = std::make_unique<VulkanRenderFrame>(m_device.get(), m_swapChain.get(), m_graphicsQueue, m_presentQueue);
|
||||
}
|
||||
|
||||
m_swapChainImageRenderImage.resize(m_swapChainImages.size(), nullptr);
|
||||
createRenderingFinishedSemaphores();
|
||||
}
|
||||
void VulkanRenderer::createRenderingFinishedSemaphores()
|
||||
{
|
||||
constexpr vk::SemaphoreCreateInfo semaphoreCreateInfo;
|
||||
m_renderingFinishedSemaphores.clear();
|
||||
for (size_t i = 0; i < m_swapChainImages.size(); ++i) {
|
||||
m_renderingFinishedSemaphores.push_back(m_device->createSemaphoreUnique(semaphoreCreateInfo));
|
||||
}
|
||||
}
|
||||
|
||||
bool VulkanRenderer::readbackFramebuffer(ubyte** outPixels, uint32_t* outWidth, uint32_t* outHeight)
|
||||
{
|
||||
*outPixels = nullptr;
|
||||
*outWidth = 0;
|
||||
*outHeight = 0;
|
||||
|
||||
if (m_previousSwapChainImage == UINT32_MAX) {
|
||||
if (m_current->previousImage == UINT32_MAX) {
|
||||
nprintf(("vulkan", "VulkanRenderer::readbackFramebuffer - no previous frame available\n"));
|
||||
return false;
|
||||
}
|
||||
@@ -466,13 +271,13 @@ bool VulkanRenderer::readbackFramebuffer(ubyte** outPixels, uint32_t* outWidth,
|
||||
// (1.0 == paper white) before the user gamma slider is applied -- the same
|
||||
// fidelity class as OpenGL's back-buffer read. Converted to BGRA8 on the
|
||||
// CPU below.
|
||||
const vk::Image srcImage = m_compositionImages[m_previousSwapChainImage].get();
|
||||
const vk::Image srcImage = m_current->compositionImages[m_current->previousImage].get();
|
||||
const vk::ImageLayout srcInitialLayout = vk::ImageLayout::eShaderReadOnlyOptimal;
|
||||
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_current->renderExtent.width;
|
||||
uint32_t h = m_current->renderExtent.height;
|
||||
vk::DeviceSize bufferSize = static_cast<vk::DeviceSize>(w) * h * bytesPerSrcPixel;
|
||||
|
||||
// End the current render pass so we can record transfer commands
|
||||
@@ -770,8 +575,12 @@ bool VulkanRenderer::readbackRenderTarget(tcache_slot_vulkan* ts, ubyte** outPix
|
||||
// A fresh command buffer starts with no Vulkan state, so re-point the state tracker at
|
||||
// it and invalidate every cached binding/dynamic-state value. beginFrame() does exactly
|
||||
// this (and touches nothing else), so the next tracked draw re-binds pipeline,
|
||||
// descriptors, viewport, scissor, etc. Deliberately NOT resetting the descriptor pool
|
||||
// mid-frame (segment 2 keeps allocating past segment 1's sets).
|
||||
// descriptors, viewport, scissor, etc.
|
||||
// NOTE: the fence wait above leaves the device provably idle here, which makes this the only
|
||||
// point in an off-screen capture where frame-scoped pools could be recycled. That is
|
||||
// deliberately NOT done unconditionally -- this path is shared with gr.screenToBlob(), which
|
||||
// mods call mid-frame with plenty of live allocations still to come. Callers that have
|
||||
// genuinely finished a frame's worth of work opt in via gr_end_offscreen_frame() instead.
|
||||
m_stateTracker->beginFrame(m_currentCommandBuffer);
|
||||
|
||||
// Resume drawing into the target (loadOp=eLoad) so content survives the flush.
|
||||
@@ -833,27 +642,59 @@ void VulkanRenderer::waitIdle()
|
||||
}
|
||||
}
|
||||
|
||||
bool VulkanRenderer::waitForFrame(uint64_t frameNumber, uint64_t timeoutNs)
|
||||
void VulkanRenderer::endOffscreenFrame()
|
||||
{
|
||||
// Fast path: if enough frames have elapsed, the work is definitely done --
|
||||
// the frame's fence was waited before its slot was reused (see
|
||||
// acquireNextSwapChainImage).
|
||||
if (m_frameNumber >= frameNumber + MAX_FRAMES_IN_FLIGHT) {
|
||||
return true;
|
||||
}
|
||||
++m_frameNumber;
|
||||
|
||||
// Not submitted yet: flip() advances m_frameNumber only after submission,
|
||||
// so frameNumber >= m_frameNumber means the fence was taken during the
|
||||
// frame currently being recorded. Its work cannot be complete, and blocking
|
||||
// here would deadlock (submission happens on this thread).
|
||||
if (frameNumber >= m_frameNumber) {
|
||||
// Same call flip() makes, with the frame-in-flight index deliberately unchanged -- it rewinds
|
||||
// the bump cursor and bumps its generation so sub-allocations from the frame just finished are
|
||||
// invalidated rather than silently overlapped.
|
||||
if (m_bufferManager) {
|
||||
m_bufferManager->setCurrentFrame(m_currentFrame, m_frameNumber);
|
||||
}
|
||||
}
|
||||
|
||||
FrameSyncPoint VulkanRenderer::captureSyncPoint() const
|
||||
{
|
||||
FrameSyncPoint point;
|
||||
point.viewport = m_current != nullptr ? m_current->viewport : nullptr;
|
||||
point.slot = m_currentFrame;
|
||||
point.frameNumber = m_frameNumber;
|
||||
return point;
|
||||
}
|
||||
|
||||
bool VulkanRenderer::waitForSyncPoint(const FrameSyncPoint& point, uint64_t timeoutNs)
|
||||
{
|
||||
// Taken during the frame still being recorded: both flip() and endOffscreenFrame() advance
|
||||
// m_frameNumber only once the frame is closed out, so this means nothing has been submitted for
|
||||
// it yet. Its work cannot be complete, and blocking here would deadlock -- submission happens on
|
||||
// this thread.
|
||||
if (point.frameNumber >= m_frameNumber) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Remaining case: frameNumber < m_frameNumber < frameNumber + MAX_FRAMES_IN_FLIGHT,
|
||||
// so the slot still belongs to exactly that frame -- wait on its fence.
|
||||
auto frameIndex = static_cast<uint32_t>(frameNumber % MAX_FRAMES_IN_FLIGHT);
|
||||
return m_frames[frameIndex]->waitForFinish(timeoutNs);
|
||||
// The target went away with its viewport. releaseViewport() drains every frame it owned before
|
||||
// letting it go, so all of its work has retired.
|
||||
const auto entry = m_targets.find(point.viewport);
|
||||
if (entry == m_targets.end()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const auto& frame = entry->second->frames[point.slot];
|
||||
|
||||
// The frame has been closed out (checked above) but this slot's fence is not the one guarding
|
||||
// it. Three ways to get here, all of them meaning the work has retired:
|
||||
// - the slot was recycled by a later frame, which waitForFrameSlot() only allows once this
|
||||
// fence has been waited on;
|
||||
// - the frame never went through a submit at all, which is the off-screen path -- and
|
||||
// gr_end_offscreen_frame()'s precondition is that its GPU work had already completed;
|
||||
// - the swap chain was rebuilt, which waits for every frame before replacing them.
|
||||
// Waiting on the fence now would be waiting for whatever holds the slot today, not for this.
|
||||
if (!frame || frame->getSubmittedFrameNumber() != point.frameNumber) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return frame->waitForFinish(timeoutNs);
|
||||
}
|
||||
|
||||
VkCommandBuffer VulkanRenderer::getVkCurrentCommandBuffer() const
|
||||
@@ -863,9 +704,16 @@ VkCommandBuffer VulkanRenderer::getVkCurrentCommandBuffer() const
|
||||
|
||||
void VulkanRenderer::shutdown()
|
||||
{
|
||||
// Wait for all frames to complete to ensure no drawing is in progress when we destroy the device
|
||||
for (uint32_t i = 0; i < MAX_FRAMES_IN_FLIGHT; ++i) {
|
||||
m_frames[i]->waitForFinish();
|
||||
// Wait for all frames to complete to ensure no drawing is in progress when we destroy the
|
||||
// device. Every target has its own sync objects, and a target that has not been presented to
|
||||
// for a while can still have work in flight, so all of them have to be drained -- not just the
|
||||
// one that happens to be current.
|
||||
for (auto& entry : m_targets) {
|
||||
for (auto& frame : entry.second->frames) {
|
||||
if (frame) {
|
||||
frame->waitForFinish();
|
||||
}
|
||||
}
|
||||
}
|
||||
// For good measure, also wait until the device is idle
|
||||
m_device->waitIdle();
|
||||
@@ -941,20 +789,14 @@ void VulkanRenderer::shutdown()
|
||||
m_bufferManager.reset();
|
||||
}
|
||||
|
||||
// Destroy depth resources before memory manager
|
||||
destroyDepthResources();
|
||||
|
||||
// Destroy composition resources before memory manager
|
||||
m_compositionImageViews.clear();
|
||||
m_compositionImages.clear();
|
||||
if (m_memoryManager) {
|
||||
for (auto& alloc : m_compositionAllocations) {
|
||||
if (alloc.isValid()) {
|
||||
m_memoryManager->freeAllocation(alloc);
|
||||
}
|
||||
}
|
||||
// Depth and composition images are backed by the memory manager, so every target has to give
|
||||
// them up before it shuts down. The rest of a target (swap chain, views, framebuffers) is
|
||||
// vk::Unique* and goes when m_targets does.
|
||||
for (auto& entry : m_targets) {
|
||||
releaseTargetMemory(*entry.second);
|
||||
destroyTargetSwapChain(*entry.second);
|
||||
entry.second->surface.reset();
|
||||
}
|
||||
m_compositionAllocations.clear();
|
||||
|
||||
// Deletion queue must be flushed before memory manager shutdown
|
||||
if (m_deletionQueue) {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include "osapi/osapi.h"
|
||||
#include "osapi/vulkan_surface.h"
|
||||
|
||||
#include "VulkanMemory.h"
|
||||
#include "VulkanBuffer.h"
|
||||
@@ -15,6 +16,7 @@
|
||||
#include "VulkanQuery.h"
|
||||
#include "VulkanRaytracing.h"
|
||||
#include "VulkanRenderFrame.h"
|
||||
#include "VulkanPresentTarget.h"
|
||||
|
||||
#include <vulkan/vulkan.hpp>
|
||||
|
||||
@@ -96,10 +98,62 @@ class VulkanRenderer {
|
||||
void setupFrame();
|
||||
|
||||
/**
|
||||
* @brief Drop every cached pipeline at the start of the next frame; each is rebuilt on its next use
|
||||
* Called after the shaders are recompiled, since a pipeline keeps the shader code it was built from
|
||||
* @brief Rebuild the swap chain if the window no longer matches it, restarting the frame
|
||||
*
|
||||
* The swap chain's extent is sampled at the end of the previous flip(), but gr_screen is set at
|
||||
* the start of the current frame's drawing, so anything that resizes the window between those
|
||||
* two points leaves the frame rendering into a swap chain of the wrong size: gr_setup_viewport()
|
||||
* sets the viewport from gr_screen while the render pass area comes from the swap chain, and the
|
||||
* difference shows up as a clipped image with unpainted bars around it. That gap is invisible in
|
||||
* the game, where a resize is a window event outside rendering, but qtFRED calls
|
||||
* gr_screen_resize() every single frame from its (freely resizable) viewport widget.
|
||||
*
|
||||
* Hooked up as gf_viewport_size_changed so gr_screen_resize() calls it, which is the moment both
|
||||
* sizes can be sampled together. If the surface really has changed size, the in-progress frame
|
||||
* is discarded (nothing has been drawn into it yet at that point), the swap chain and every
|
||||
* extent-sized resource is rebuilt, and a fresh frame is started at the new size.
|
||||
*
|
||||
* The comparison is surface-against-swap-chain rather than gr_screen-against-swap-chain, on
|
||||
* purpose: both of those come from the surface, so they agree exactly and this cannot thrash.
|
||||
* gr_screen is computed independently by the caller and can be a pixel off from rounding.
|
||||
*
|
||||
* @return true if the swap chain was rebuilt
|
||||
*/
|
||||
void requestPipelineRebuild() { m_pipelinesStale = true; }
|
||||
bool syncToSurfaceExtent();
|
||||
|
||||
/**
|
||||
* @brief Make a viewport's surface the one subsequent drawing presents to
|
||||
*
|
||||
* Backs gr_use_viewport(). Creating the target on first use is deliberate: qtFRED's briefing map
|
||||
* appears and disappears with its dialog, so there is no point in the session at which the full
|
||||
* set of viewports is known.
|
||||
*
|
||||
* A frame is always already open when this is called -- gr_flip() ends with gr_setup_frame() --
|
||||
* and it belongs to the outgoing target, which has also already acquired an image there. That
|
||||
* frame is discarded rather than presented; see discardFrame().
|
||||
*
|
||||
* @return false if the viewport cannot be presented to, in which case the current target is left
|
||||
* alone and the caller keeps drawing where it was
|
||||
*/
|
||||
bool useViewport(os::Viewport* viewport);
|
||||
|
||||
/**
|
||||
* @brief Drop the target belonging to a viewport that is going away
|
||||
*
|
||||
* Must be called while the renderer is still alive and before the viewport's window is
|
||||
* destroyed. qtFRED's briefing editor is opened with WA_DeleteOnClose, so this happens against a
|
||||
* live device every time the user closes the dialog.
|
||||
*/
|
||||
void releaseViewport(os::Viewport* viewport);
|
||||
|
||||
/**
|
||||
* @brief Whether the current target is the main one
|
||||
*
|
||||
* The post-processor is sized for and bound to the main target, so the scene-texture path stays
|
||||
* off anywhere else. Nothing is lost by that today: qtFRED's briefing map renders through
|
||||
* brief_render_map() and never opens a ScenePostProcessing scope.
|
||||
*/
|
||||
bool isMainTargetCurrent() const { return m_current == m_mainTarget; }
|
||||
|
||||
/**
|
||||
* @brief End frame - ends render pass, submits, and presents
|
||||
@@ -151,22 +205,41 @@ class VulkanRenderer {
|
||||
uint32_t getMinUniformBufferOffsetAlignment() const;
|
||||
|
||||
/**
|
||||
* @brief Get the current frame number (total frames rendered)
|
||||
* @brief Close out a frame's worth of work that completed without going through flip().
|
||||
*
|
||||
* Advances the monotonic frame counter that sync objects are stamped with, and rewinds the
|
||||
* frame-scoped bump allocator. Deliberately does NOT touch m_currentFrame: that index selects
|
||||
* the swap-chain sync objects, and the image for this index has already been acquired for the
|
||||
* flip that will eventually present.
|
||||
*
|
||||
* Without this, an off-screen renderer leaves m_frameNumber frozen, and waitForSyncPoint()
|
||||
* reports every fence taken since as "still recording" -- which is what makes
|
||||
* UniformBufferManager's segment rotation give up and Error() out.
|
||||
*
|
||||
* Only valid once the work in question has actually retired; see gr_end_offscreen_frame().
|
||||
*/
|
||||
uint64_t getCurrentFrameNumber() const { return m_frameNumber; }
|
||||
void endOffscreenFrame();
|
||||
|
||||
/**
|
||||
* @brief Wait for a specific frame's GPU work to complete
|
||||
* @brief Stamp the frame currently being recorded, so it can be waited on later
|
||||
*
|
||||
* Waits on that frame's fence rather than stalling the entire device.
|
||||
* Backs gr_sync_fence(). Records *which* fence, not just when: the frame number alone cannot
|
||||
* find it back, because the fences are per-target while the frame number is global, and because
|
||||
* endOffscreenFrame() advances the frame number without moving the frame-in-flight cursor.
|
||||
*/
|
||||
FrameSyncPoint captureSyncPoint() const;
|
||||
|
||||
/**
|
||||
* @brief Wait for the GPU work recorded during the frame @p point was taken in
|
||||
*
|
||||
* Waits on that frame's own fence rather than stalling the entire device.
|
||||
*
|
||||
* @param timeoutNs Maximum wait in nanoseconds (0 = poll)
|
||||
* @return true if the frame is known complete. false if the timeout expired,
|
||||
* or if the frame has not been submitted yet (a fence taken during
|
||||
* the currently-recording frame cannot complete until flip();
|
||||
* waiting here would deadlock, so it reports "not complete").
|
||||
* @return true if the work is known complete. false if the timeout expired, or if the sync
|
||||
* point was taken during the frame still being recorded (submission happens on this
|
||||
* thread, so waiting here would deadlock -- it reports "not complete" instead).
|
||||
*/
|
||||
bool waitForFrame(uint64_t frameNumber, uint64_t timeoutNs = UINT64_MAX);
|
||||
bool waitForSyncPoint(const FrameSyncPoint& point, uint64_t timeoutNs = UINT64_MAX);
|
||||
|
||||
/**
|
||||
* @brief Wait for all GPU work to complete
|
||||
@@ -190,9 +263,14 @@ class VulkanRenderer {
|
||||
uint32_t getGraphicsQueueFamilyIndex() const { return m_graphicsQueueFamilyIndex; }
|
||||
|
||||
/**
|
||||
* @brief Size of the actual window / swap chain images
|
||||
* @brief The extent the current target actually presents at, in device pixels
|
||||
*
|
||||
* The size of its window and of its swap chain images. This is also what the encode pass and
|
||||
* its framebuffers are sized to, so it is what gr_screen has to agree with. Callers must not
|
||||
* compute it themselves from a window's logical size and a scale factor -- that rounds
|
||||
* differently from the way the surface was sized.
|
||||
*/
|
||||
vk::Extent2D getSwapChainExtent() const { return m_swapChainExtent; }
|
||||
vk::Extent2D getSwapChainExtent() const { return m_current != nullptr ? m_current->extent : vk::Extent2D(); }
|
||||
|
||||
/**
|
||||
* @brief Size everything is rendered at, which is not always the window size
|
||||
@@ -203,13 +281,15 @@ class VulkanRenderer {
|
||||
* composition image, depth, and the post-processor's scene and G-buffer
|
||||
* targets -- is sized to this, and the encode pass scales it into the swap
|
||||
* chain image. OpenGL does the same thing with Back_framebuffer.
|
||||
*
|
||||
* Per target, because each one draws at its own size; see VulkanPresentTarget::renderExtent.
|
||||
*/
|
||||
vk::Extent2D getRenderExtent() const { return m_renderExtent; }
|
||||
vk::Extent2D getRenderExtent() const { return m_current != nullptr ? m_current->renderExtent : m_renderExtent; }
|
||||
|
||||
/**
|
||||
* @brief Get the composition image the frame is currently being rendered into
|
||||
*
|
||||
* This is the fp16 image behind m_swapChainFramebuffers -- the whole frame
|
||||
* This is the fp16 image behind the current target's framebuffers -- the whole frame
|
||||
* (3D scene, HUD and menus alike) lands here, and encodeToSwapChain() later
|
||||
* converts it into the actual swap chain image. Returns a null handle if
|
||||
* composition resources do not exist yet.
|
||||
@@ -432,37 +512,125 @@ class VulkanRenderer {
|
||||
|
||||
bool initializeInstance();
|
||||
|
||||
bool initializeSurface();
|
||||
/**
|
||||
* @brief Ask the windowing system for @p target's viewport surface and take ownership of it
|
||||
*/
|
||||
bool createTargetSurface(VulkanPresentTarget& target);
|
||||
|
||||
bool pickPhysicalDevice(PhysicalDeviceValues& deviceValues);
|
||||
|
||||
bool createLogicalDevice(const PhysicalDeviceValues& deviceValues);
|
||||
|
||||
bool createSwapChain(const PhysicalDeviceValues& deviceValues, vk::SwapchainKHR oldSwapchain = nullptr);
|
||||
// Everything a target owns is built by one of these. They take the target explicitly rather than
|
||||
// working on m_current: the setup path builds targets that are not current yet (and, when
|
||||
// creation fails part-way, never become current), so "the current target" is the wrong answer
|
||||
// there -- and an implicit one is impossible to check at the call site.
|
||||
bool createSwapChain(VulkanPresentTarget& target,
|
||||
const PhysicalDeviceValues& deviceValues,
|
||||
vk::SwapchainKHR oldSwapchain = nullptr);
|
||||
|
||||
void createRenderPass();
|
||||
|
||||
void createFrameBuffers();
|
||||
void createFrameBuffers(VulkanPresentTarget& target);
|
||||
|
||||
// HDR composition + output-encode resources
|
||||
void createCompositionResources();
|
||||
void createEncodeRenderPass();
|
||||
void createCompositionResources(VulkanPresentTarget& target);
|
||||
|
||||
/**
|
||||
* @brief Build the shared output-encode render pass for @p swapChainFormat
|
||||
*
|
||||
* Takes the format rather than a target because, unlike its neighbours here, it does not build
|
||||
* into one: m_encodeRenderPass is shared by every target. That is also the constraint
|
||||
* createTargetResources() has to check -- a target whose surface negotiates a different format
|
||||
* cannot use this pass.
|
||||
*/
|
||||
void createEncodeRenderPass(vk::Format swapChainFormat);
|
||||
void encodeToSwapChain();
|
||||
|
||||
void createDepthResources();
|
||||
void destroyDepthResources();
|
||||
void createDepthResources(VulkanPresentTarget& target);
|
||||
void destroyDepthResources(VulkanPresentTarget& target);
|
||||
|
||||
/**
|
||||
* @brief Give back everything in a target that the memory manager owns
|
||||
*
|
||||
* The depth and composition images are the only parts of a target backed by VulkanMemoryManager
|
||||
* allocations, so they have to be released before it shuts down; everything else is vk::Unique*
|
||||
* and can wait for the target's own destructor.
|
||||
*/
|
||||
void releaseTargetMemory(VulkanPresentTarget& target);
|
||||
|
||||
/**
|
||||
* @brief Tear down everything a target derived from its surface, in the order Vulkan requires
|
||||
*
|
||||
* A VkSurfaceKHR must outlive every swap chain made from it. Relying on member-declaration order
|
||||
* to get that right is too subtle to be safe here, because the surface is not destroyed by us at
|
||||
* all: it belongs to the windowing system, and under Qt it goes when the window does. So the
|
||||
* swap chain and everything holding its images are released explicitly first.
|
||||
*/
|
||||
static void destroyTargetSwapChain(VulkanPresentTarget& target);
|
||||
|
||||
vk::Format findDepthFormat();
|
||||
|
||||
void createCommandPool(const PhysicalDeviceValues& values);
|
||||
|
||||
void createPresentSyncObjects();
|
||||
void createRenderingFinishedSemaphores();
|
||||
void createPresentSyncObjects(VulkanPresentTarget& target);
|
||||
|
||||
/**
|
||||
* @brief Build a target's per-image render-finished semaphores
|
||||
*
|
||||
* Sized to the swap chain's image count and indexed by image index, so it has to be rebuilt
|
||||
* whenever the swap chain is, alongside the images themselves.
|
||||
*/
|
||||
void createRenderFinishedSemaphores(VulkanPresentTarget& target);
|
||||
|
||||
/**
|
||||
* @brief Build a target's surface, swap chain and everything sized to it
|
||||
*
|
||||
* Leaves @p target untouched by the renderer's notion of "current": it is only safe to present
|
||||
* to once this has returned true, and useViewport() switches to it then.
|
||||
*
|
||||
* @return false if the surface could not be created, or if it negotiated a format the shared
|
||||
* encode render pass was not built for
|
||||
*/
|
||||
bool createTargetResources(VulkanPresentTarget& target);
|
||||
|
||||
void acquireNextSwapChainImage();
|
||||
|
||||
bool recreateSwapChain();
|
||||
bool shouldRecreateSwapChain(SwapChainStatus status) const;
|
||||
/**
|
||||
* @brief Wait until every target has finished the work it put in a frame-in-flight slot
|
||||
*
|
||||
* The slot indexes per-target sync objects but also the shared command pool and descriptor
|
||||
* pools, so it cannot be recycled until all targets are done with it.
|
||||
*/
|
||||
void waitForFrameSlot(uint32_t slot);
|
||||
|
||||
/**
|
||||
* @brief Wait for a frame, naming it in the log if the wait is not a normal one
|
||||
*
|
||||
* Every wait on a fence in the present path goes through here. A legitimate wait is one frame
|
||||
* time; anything beyond a fraction of a second means the queue is wedged, and blocking forever
|
||||
* on it just produces an editor that stops responding and gets killed before any long timeout
|
||||
* could say which wait it was.
|
||||
*
|
||||
* @param what description of the wait, for the log -- caller-built so the message names the
|
||||
* target and slot involved
|
||||
*/
|
||||
static void waitOrReport(VulkanRenderFrame& frame, const char* what);
|
||||
|
||||
/**
|
||||
* @brief Throw away the in-progress frame without submitting or presenting it
|
||||
*
|
||||
* Ends the open render pass and command buffer and frees it -- safe to free immediately, since
|
||||
* nothing was ever submitted and so no GPU work can reference it. The swap chain image this
|
||||
* frame acquired is left unconsumed, which leaves its image-available semaphore signaled; the
|
||||
* caller must therefore recreate the swap chain (which recreates every frame's sync objects)
|
||||
* before the next acquire.
|
||||
*/
|
||||
void discardFrame();
|
||||
|
||||
bool recreateSwapChain(VulkanPresentTarget& target);
|
||||
|
||||
bool shouldRecreateSwapChain(const VulkanPresentTarget& target, SwapChainStatus status) const;
|
||||
|
||||
void createImGuiDescriptorPool();
|
||||
void initImGui();
|
||||
@@ -474,20 +642,27 @@ class VulkanRenderer {
|
||||
vk::UniqueDebugReportCallbackEXT m_debugReport; // legacy fallback (no VK_EXT_debug_utils)
|
||||
vk::UniqueDebugUtilsMessengerEXT m_debugMessenger; // preferred debug callback
|
||||
|
||||
vk::UniqueSurfaceKHR m_vkSurface;
|
||||
|
||||
vk::UniqueDevice m_device;
|
||||
|
||||
vk::Queue m_graphicsQueue;
|
||||
vk::Queue m_presentQueue;
|
||||
|
||||
vk::UniqueSwapchainKHR m_swapChain;
|
||||
vk::Format m_swapChainImageFormat;
|
||||
vk::ColorSpaceKHR m_swapChainColorSpace = vk::ColorSpaceKHR::eSrgbNonlinear;
|
||||
bool m_hdrActive = false; // True when an HDR10 (PQ/BT.2020) swap chain was negotiated
|
||||
// Everything downstream of a surface lives in the target it belongs to. There is exactly one in
|
||||
// the game; qtFRED presents through two (its main viewport and the briefing map).
|
||||
//
|
||||
// m_current means only "where drawing goes right now" -- the frame loop reads it, the setup path
|
||||
// does not. Everything that builds or tears down a target takes it as a parameter, so a target
|
||||
// can be built before it is ever current and abandoned if that fails.
|
||||
//
|
||||
// Keyed by os::Viewport* rather than by index into os::viewports, deliberately: qtFRED's
|
||||
// briefing map hands its viewport straight to gr_use_viewport() and never registers it with
|
||||
// os::addViewport(), so that list does not contain every viewport we present to.
|
||||
SCP_unordered_map<os::Viewport*, std::unique_ptr<VulkanPresentTarget>> m_targets;
|
||||
VulkanPresentTarget* m_mainTarget = nullptr;
|
||||
VulkanPresentTarget* m_current = nullptr;
|
||||
|
||||
bool m_hdrMetadataSupported = false; // VK_EXT_hdr_metadata device extension enabled
|
||||
vk::Extent2D m_swapChainExtent;
|
||||
vk::SurfaceTransformFlagBitsKHR m_swapChainPreTransform = vk::SurfaceTransformFlagBitsKHR::eIdentity;
|
||||
|
||||
// The resolution the engine draws at (gr_screen.max_w/max_h), which is not
|
||||
// always the window size: when Cmdline_window_res is set,
|
||||
// SDLGraphicsOperations::createViewport sizes the window from that instead,
|
||||
@@ -495,43 +670,32 @@ class VulkanRenderer {
|
||||
// exactly that split (Cmdline_window_res 1000x1000 vs. the VR resolution
|
||||
// option), so treating the two as one is what put whole frames in the
|
||||
// upper-left corner of the screen.
|
||||
//
|
||||
// Captured in initialize() before the window exists, and only ever applied to the main target:
|
||||
// see createSwapChain(), which is where each target picks up its own render extent.
|
||||
vk::Extent2D m_renderExtent;
|
||||
SCP_vector<vk::Image> m_swapChainImages;
|
||||
SCP_vector<vk::UniqueImageView> m_swapChainImageViews;
|
||||
SCP_vector<vk::UniqueFramebuffer> m_swapChainFramebuffers;
|
||||
SCP_vector<VulkanRenderFrame*> m_swapChainImageRenderImage;
|
||||
// per image, not per frame: a frame's fence does not tell us when present has consumed the semaphore
|
||||
SCP_vector<vk::UniqueSemaphore> m_renderingFinishedSemaphores;
|
||||
|
||||
// HDR composition pipeline: the whole frame is rendered into these fp16
|
||||
// images (via m_renderPass / m_swapChainFramebuffers) instead of directly
|
||||
// into the swap chain image. encodeToSwapChain() converts composition ->
|
||||
// swap chain: a direct blit (or encodeOutputPassthrough() as a fallback)
|
||||
// for SDR, or encodeOutput() (m_encodeRenderPass + m_encodeFramebuffers)
|
||||
// for the HDR10 PQ/BT.2020 transfer.
|
||||
SCP_vector<vk::UniqueImage> m_compositionImages;
|
||||
SCP_vector<vk::UniqueImageView> m_compositionImageViews;
|
||||
SCP_vector<VulkanAllocation> m_compositionAllocations;
|
||||
// Shared by every target, and deliberately so: the render passes bake in the composition (fp16)
|
||||
// and depth formats, which are the same everywhere, so keeping one set keeps every cached
|
||||
// pipeline valid across a target switch. m_encodeRenderPass is the exception -- it bakes in the
|
||||
// *swap chain* format, so a target whose surface negotiates a different one cannot use it; see
|
||||
// createTargetResources().
|
||||
vk::UniqueSampler m_compositionSampler;
|
||||
SCP_vector<vk::UniqueFramebuffer> m_encodeFramebuffers;
|
||||
vk::UniqueRenderPass m_encodeRenderPass;
|
||||
|
||||
uint32_t m_currentSwapChainImage = 0;
|
||||
uint32_t m_previousSwapChainImage = UINT32_MAX; // For saveScreen() readback of previous frame
|
||||
|
||||
// Depth buffer
|
||||
vk::UniqueImage m_depthImage;
|
||||
vk::UniqueImageView m_depthImageView;
|
||||
VulkanAllocation m_depthImageMemory;
|
||||
vk::Format m_depthFormat = vk::Format::eUndefined;
|
||||
|
||||
vk::UniqueRenderPass m_renderPass; // Swap chain pass with loadOp=eClear
|
||||
vk::UniqueRenderPass m_renderPassLoad; // Swap chain pass with loadOp=eLoad (resumed after post-processing)
|
||||
vk::UniqueDescriptorPool m_imguiDescriptorPool;
|
||||
bool m_imguiInitialized = false; // false in the editors, which have no ImGui context at all
|
||||
|
||||
// The frame-in-flight cursor stays global rather than moving into the target: the buffer and
|
||||
// descriptor managers keep one ring keyed off it (setCurrentFrame() below), so a per-target
|
||||
// cursor would hand them conflicting indices and corrupt descriptors a few frames later. Each
|
||||
// target instead keeps its own sync objects and indexes them with this shared cursor.
|
||||
uint32_t m_currentFrame = 0;
|
||||
uint64_t m_frameNumber = 0; // Total frames rendered (for sync tracking)
|
||||
std::array<std::unique_ptr<VulkanRenderFrame>, MAX_FRAMES_IN_FLIGHT> m_frames;
|
||||
|
||||
vk::UniqueCommandPool m_graphicsCommandPool;
|
||||
|
||||
@@ -539,10 +703,6 @@ class VulkanRenderer {
|
||||
vk::CommandBuffer m_currentCommandBuffer;
|
||||
SCP_vector<vk::CommandBuffer> m_currentCommandBuffers; // For cleanup
|
||||
bool m_frameInProgress = false;
|
||||
bool m_pipelinesStale = false;
|
||||
|
||||
// Swap chain recreation
|
||||
bool m_swapChainNeedsRecreation = false;
|
||||
|
||||
// Physical device info (needed for memory manager)
|
||||
vk::PhysicalDevice m_physicalDevice;
|
||||
|
||||
@@ -25,6 +25,14 @@ void VulkanRenderer::createImGuiDescriptorPool()
|
||||
|
||||
void VulkanRenderer::initImGui()
|
||||
{
|
||||
// Only freespace2 creates an ImGui context (game_init()); the editors never do, and they don't
|
||||
// open the debug window that would draw through it. Without that context ImGui_ImplVulkan_Init()
|
||||
// asserts inside ImGui::GetIO(), so there is nothing to set up here.
|
||||
if (ImGui::GetCurrentContext() == nullptr) {
|
||||
nprintf(("vulkan", "Vulkan: no ImGui context exists, skipping the ImGui backend\n"));
|
||||
return;
|
||||
}
|
||||
|
||||
createImGuiDescriptorPool();
|
||||
|
||||
// Load Vulkan function pointers for imgui (required with VK_NO_PROTOTYPES)
|
||||
@@ -43,7 +51,7 @@ void VulkanRenderer::initImGui()
|
||||
initInfo.PipelineCache = VK_NULL_HANDLE;
|
||||
initInfo.DescriptorPool = static_cast<VkDescriptorPool>(*m_imguiDescriptorPool);
|
||||
initInfo.MinImageCount = 2;
|
||||
initInfo.ImageCount = static_cast<uint32_t>(m_swapChainImages.size());
|
||||
initInfo.ImageCount = static_cast<uint32_t>(m_mainTarget->images.size());
|
||||
initInfo.Allocator = nullptr;
|
||||
initInfo.CheckVkResultFn = nullptr;
|
||||
initInfo.PipelineInfoMain.Subpass = 0;
|
||||
@@ -51,13 +59,19 @@ void VulkanRenderer::initImGui()
|
||||
initInfo.PipelineInfoMain.RenderPass = static_cast<VkRenderPass>(*m_renderPass);
|
||||
|
||||
ImGui_ImplVulkan_Init(&initInfo);
|
||||
m_imguiInitialized = true;
|
||||
|
||||
nprintf(("vulkan", "Vulkan: ImGui backend initialized successfully\n"));
|
||||
}
|
||||
|
||||
void VulkanRenderer::shutdownImGui()
|
||||
{
|
||||
if (!m_imguiInitialized) {
|
||||
return;
|
||||
}
|
||||
|
||||
ImGui_ImplVulkan_Shutdown();
|
||||
m_imguiInitialized = false;
|
||||
m_imguiDescriptorPool.reset();
|
||||
nprintf(("vulkan", "Vulkan: ImGui backend shut down\n"));
|
||||
}
|
||||
|
||||
@@ -48,13 +48,67 @@ void VulkanRenderer::beginTrackedRenderPass(const PassBeginDesc& desc)
|
||||
}
|
||||
}
|
||||
|
||||
void VulkanRenderer::waitForFrameSlot(uint32_t slot)
|
||||
{
|
||||
// Every target has its own sync objects, but the command pool and the descriptor manager's
|
||||
// pools are shared and keyed on this slot alone. So recycling the slot is only safe once the
|
||||
// work *every* target put in it has completed -- waiting on the target that happens to be
|
||||
// current is not enough. Getting this wrong let a viewport switch reset a descriptor pool and
|
||||
// free command buffers that the other target's still-pending submit was using
|
||||
// (VUID-vkResetDescriptorPool-descriptorPool-00313,
|
||||
// VUID-vkFreeCommandBuffers-pCommandBuffers-00047), which wedged the queue.
|
||||
for (auto& entry : m_targets) {
|
||||
auto& frame = entry.second->frames[slot];
|
||||
if (!frame) {
|
||||
continue;
|
||||
}
|
||||
|
||||
SCP_string what;
|
||||
sprintf(what, "frame slot %u of the %s target (slot wait)", slot,
|
||||
entry.second.get() == m_mainTarget ? "main" : "secondary");
|
||||
waitOrReport(*frame, what.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
void VulkanRenderer::waitOrReport(VulkanRenderFrame& frame, const char* what)
|
||||
{
|
||||
constexpr uint64_t PROBE_NS = 200000000ULL; // 0.2s -- far longer than any real frame
|
||||
constexpr uint64_t GIVE_UP_NS = 2000000000ULL; // 2s more before calling it wedged
|
||||
|
||||
if (frame.waitForFinish(PROBE_NS)) {
|
||||
return;
|
||||
}
|
||||
|
||||
mprintf(("Vulkan: still waiting on %s after 0.2s; the queue looks wedged.\n", what));
|
||||
|
||||
if (!frame.waitForFinish(GIVE_UP_NS)) {
|
||||
Error(LOCATION, "Vulkan: %s never completed. This is a renderer synchronisation bug, not bad data.",
|
||||
what);
|
||||
}
|
||||
}
|
||||
|
||||
void VulkanRenderer::acquireNextSwapChainImage()
|
||||
{
|
||||
m_frames[m_currentFrame]->waitForFinish();
|
||||
waitForFrameSlot(m_currentFrame);
|
||||
|
||||
// Acquire an image, recreating the swap chain as often as needed (bounded).
|
||||
// The frame must never proceed without an acquired image: its image-available
|
||||
// semaphore would never be signaled and the submit would deadlock/corrupt.
|
||||
// A viewport switch retains this target's acquired image rather than abandoning it, since only a
|
||||
// present hands one back. Reuse it instead of acquiring a second one.
|
||||
if (m_current->hasRetainedAcquire && !m_current->needsRecreation) {
|
||||
m_current->currentAcquire = m_current->retainedAcquire;
|
||||
m_current->currentImage = m_current->retainedImage;
|
||||
m_current->hasRetainedAcquire = false;
|
||||
|
||||
if (m_current->imageRenderFrame[m_current->currentImage]) {
|
||||
waitOrReport(*m_current->imageRenderFrame[m_current->currentImage],
|
||||
"the frame still rendering into a retained image");
|
||||
}
|
||||
m_current->imageRenderFrame[m_current->currentImage] = m_current->frames[m_currentFrame].get();
|
||||
return;
|
||||
}
|
||||
|
||||
constexpr int MAX_ACQUIRE_ATTEMPTS = 5;
|
||||
uint32_t imageIndex = 0;
|
||||
SwapChainStatus status = SwapChainStatus::eOutOfDate;
|
||||
@@ -62,18 +116,30 @@ void VulkanRenderer::acquireNextSwapChainImage()
|
||||
for (int attempt = 0; attempt < MAX_ACQUIRE_ATTEMPTS; ++attempt) {
|
||||
// Recreate if flagged (from a previous frame, a failed acquire below, or
|
||||
// a suboptimal present). Waits out a minimized window (0x0 extent).
|
||||
if (m_swapChainNeedsRecreation) {
|
||||
while (!recreateSwapChain()) {
|
||||
if (m_current->needsRecreation) {
|
||||
while (!recreateSwapChain(*m_current)) {
|
||||
os_sleep(100);
|
||||
SDL_PumpEvents();
|
||||
}
|
||||
}
|
||||
|
||||
status = m_frames[m_currentFrame]->acquireSwapchainImage(imageIndex);
|
||||
// Take the next acquire semaphore round-robin, making sure whatever last presented with it
|
||||
// has finished before it is signalled again.
|
||||
auto& acquire = m_current->acquireSemaphores[m_current->nextAcquire];
|
||||
if (acquire.consumer != nullptr) {
|
||||
waitOrReport(*acquire.consumer, "the frame that last presented with this acquire semaphore");
|
||||
acquire.consumer = nullptr;
|
||||
}
|
||||
m_current->currentAcquire = m_current->nextAcquire;
|
||||
m_current->nextAcquire = (m_current->nextAcquire + 1) %
|
||||
static_cast<uint32_t>(m_current->acquireSemaphores.size());
|
||||
|
||||
status = m_current->frames[m_currentFrame]->acquireSwapchainImage(imageIndex,
|
||||
acquire.semaphore.get());
|
||||
if (status != SwapChainStatus::eOutOfDate) {
|
||||
break;
|
||||
}
|
||||
m_swapChainNeedsRecreation = true;
|
||||
m_current->needsRecreation = true;
|
||||
}
|
||||
|
||||
if (status == SwapChainStatus::eOutOfDate) {
|
||||
@@ -81,18 +147,19 @@ void VulkanRenderer::acquireNextSwapChainImage()
|
||||
MAX_ACQUIRE_ATTEMPTS);
|
||||
}
|
||||
|
||||
if (shouldRecreateSwapChain(status)) {
|
||||
m_swapChainNeedsRecreation = true;
|
||||
if (shouldRecreateSwapChain(*m_current, status)) {
|
||||
m_current->needsRecreation = true;
|
||||
}
|
||||
|
||||
m_currentSwapChainImage = imageIndex;
|
||||
m_current->currentImage = imageIndex;
|
||||
|
||||
// Ensure that this image is no longer in use
|
||||
if (m_swapChainImageRenderImage[m_currentSwapChainImage]) {
|
||||
m_swapChainImageRenderImage[m_currentSwapChainImage]->waitForFinish();
|
||||
if (m_current->imageRenderFrame[m_current->currentImage]) {
|
||||
waitOrReport(*m_current->imageRenderFrame[m_current->currentImage],
|
||||
"the frame still rendering into the newly acquired image");
|
||||
}
|
||||
// Reserve the image as in use
|
||||
m_swapChainImageRenderImage[m_currentSwapChainImage] = m_frames[m_currentFrame].get();
|
||||
m_current->imageRenderFrame[m_current->currentImage] = m_current->frames[m_currentFrame].get();
|
||||
}
|
||||
void VulkanRenderer::setupFrame()
|
||||
{
|
||||
@@ -101,18 +168,6 @@ void VulkanRenderer::setupFrame()
|
||||
return;
|
||||
}
|
||||
|
||||
// before recording: the recompile can run mid-frame, while the frame being recorded still holds old pipelines
|
||||
if (m_pipelinesStale) {
|
||||
mprintf(("Vulkan: shaders were recompiled, rebuilding pipelines\n"));
|
||||
m_device->waitIdle();
|
||||
m_pipelineManager->forgetAllPipelines();
|
||||
m_pipelinesStale = false;
|
||||
}
|
||||
|
||||
// Free completed texture upload command buffers
|
||||
Assertion(m_textureManager, "Vulkan TextureManager not initialized in setupFrame!");
|
||||
m_textureManager->frameStart();
|
||||
|
||||
// Allocate command buffer for this frame
|
||||
vk::CommandBufferAllocateInfo cmdBufferAlloc;
|
||||
cmdBufferAlloc.commandPool = m_graphicsCommandPool.get();
|
||||
@@ -160,14 +215,184 @@ void VulkanRenderer::setupFrame()
|
||||
|
||||
PassBeginDesc pass;
|
||||
pass.renderPass = m_renderPass.get();
|
||||
pass.framebuffer = m_swapChainFramebuffers[m_currentSwapChainImage].get();
|
||||
pass.extent = m_renderExtent;
|
||||
pass.framebuffer = m_current->framebuffers[m_current->currentImage].get();
|
||||
pass.extent = m_current->renderExtent;
|
||||
pass.clearValues = clearValues;
|
||||
beginTrackedRenderPass(pass);
|
||||
|
||||
m_frameInProgress = true;
|
||||
}
|
||||
|
||||
void VulkanRenderer::discardFrame()
|
||||
{
|
||||
if (!m_frameInProgress) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Whatever pass is open (composition, or a post-processing/shadow pass if this is ever called
|
||||
// from somewhere less tidy) has to be closed before the command buffer can be ended.
|
||||
if (m_stateTracker->getCurrentRenderPass()) {
|
||||
m_currentCommandBuffer.endRenderPass();
|
||||
m_stateTracker->setRenderPass(vk::RenderPass());
|
||||
}
|
||||
|
||||
m_currentCommandBuffer.end();
|
||||
m_device->freeCommandBuffers(m_graphicsCommandPool.get(), m_currentCommandBuffers);
|
||||
|
||||
m_currentCommandBuffer = nullptr;
|
||||
m_currentCommandBuffers.clear();
|
||||
m_frameInProgress = false;
|
||||
|
||||
// This frame never submitted, so it does not own the image any more.
|
||||
if (m_current->currentImage < m_current->imageRenderFrame.size()) {
|
||||
m_current->imageRenderFrame[m_current->currentImage] = nullptr;
|
||||
}
|
||||
|
||||
// Keep the acquire rather than dropping it. vkAcquireNextImageKHR hands out an image that only
|
||||
// vkQueuePresentKHR gives back, so abandoning one here would leak an image on this target every
|
||||
// time a viewport switch passed through -- and with a handful of images per swap chain, that
|
||||
// wedges the next acquire within a few frames. The semaphore it signalled is owned by the
|
||||
// target, so it survives the frame slot moving on.
|
||||
m_current->hasRetainedAcquire = true;
|
||||
m_current->retainedAcquire = m_current->currentAcquire;
|
||||
m_current->retainedImage = m_current->currentImage;
|
||||
}
|
||||
|
||||
bool VulkanRenderer::useViewport(os::Viewport* viewport)
|
||||
{
|
||||
if (viewport == nullptr) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (m_current != nullptr && m_current->viewport == viewport) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Resolve the target -- building it on first use -- before disturbing anything. Creating one
|
||||
// touches only its own resources, so a failure here leaves the renderer exactly as it was: the
|
||||
// frame in progress is still the outgoing target's, and the caller keeps drawing where it was.
|
||||
auto entry = m_targets.find(viewport);
|
||||
if (entry == m_targets.end()) {
|
||||
auto created = std::make_unique<VulkanPresentTarget>();
|
||||
created->viewport = viewport;
|
||||
|
||||
if (!createTargetResources(*created)) {
|
||||
mprintf(("Vulkan: could not present to this viewport; staying on the previous one.\n"));
|
||||
releaseTargetMemory(*created);
|
||||
return false;
|
||||
}
|
||||
|
||||
entry = m_targets.emplace(viewport, std::move(created)).first;
|
||||
}
|
||||
|
||||
// A frame is always open here: gr_flip() ends with gr_setup_frame(), so the outgoing target has
|
||||
// both an open command buffer and an already-acquired image. Neither survives the switch.
|
||||
const bool restartFrame = m_frameInProgress;
|
||||
discardFrame();
|
||||
|
||||
m_current = entry->second.get();
|
||||
|
||||
if (restartFrame) {
|
||||
acquireNextSwapChainImage();
|
||||
setupFrame();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void VulkanRenderer::releaseViewport(os::Viewport* viewport)
|
||||
{
|
||||
auto entry = m_targets.find(viewport);
|
||||
if (entry == m_targets.end()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (entry->second.get() == m_mainTarget) {
|
||||
// The main target lives as long as the renderer does; letting it go here would leave
|
||||
// nothing to fall back to.
|
||||
return;
|
||||
}
|
||||
|
||||
// Anything still reading this target's images has to be done before they are destroyed. The
|
||||
// frames are per-target, so this waits only on that target's work -- but the surface is about to
|
||||
// go with the window, so be blunt about it and drain the device too.
|
||||
for (auto& frame : entry->second->frames) {
|
||||
if (frame) {
|
||||
frame->waitForFinish();
|
||||
}
|
||||
}
|
||||
m_device->waitIdle();
|
||||
|
||||
if (m_current == entry->second.get()) {
|
||||
// Whatever was set up against this target cannot be presented now.
|
||||
discardFrame();
|
||||
m_current = m_mainTarget;
|
||||
acquireNextSwapChainImage();
|
||||
setupFrame();
|
||||
}
|
||||
|
||||
// Order matters and is not left to member destruction: the swap chain has to go before the
|
||||
// surface, and the surface goes as soon as the handle releases it back to the window system.
|
||||
releaseTargetMemory(*entry->second);
|
||||
destroyTargetSwapChain(*entry->second);
|
||||
entry->second->surface.reset();
|
||||
|
||||
m_targets.erase(entry);
|
||||
}
|
||||
|
||||
bool VulkanRenderer::syncToSurfaceExtent()
|
||||
{
|
||||
if (!m_current->surface || !m_current->swapChain) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const auto capabilities = m_physicalDevice.getSurfaceCapabilitiesKHR(m_current->surface.get());
|
||||
|
||||
// 0xFFFFFFFF means "the surface takes its size from the swap chain", so there is nothing to
|
||||
// follow. A 0x0 extent is a minimized window; leave the swap chain alone and let the regular
|
||||
// acquire path wait it out rather than recreating into an unusable size here.
|
||||
if (capabilities.currentExtent.width == UINT32_MAX ||
|
||||
capabilities.currentExtent.width == 0 || capabilities.currentExtent.height == 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (capabilities.currentExtent == m_current->extent) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Rebuilding means discarding the frame in progress, which is only free while nothing has been
|
||||
// drawn into it -- the top of a frame, where every gr_screen_resize() caller sits today.
|
||||
//
|
||||
// This catches only the narrowest case of getting that wrong: a resize from inside an open
|
||||
// scene-texture scope, which would throw away a composed scene. It is deliberately not the
|
||||
// equivalent of OpenGL's Scene_framebuffer_in_frame check, which is broader --
|
||||
// beginSceneRendering() never runs at all when post-processing is off, and off is qtFRED's
|
||||
// default, so this is false for the whole frame in the common case. Widening it would mean
|
||||
// tracking "has anything been recorded into this command buffer", which nothing needs yet.
|
||||
if (m_sceneRendering) {
|
||||
Assertion(false, "Tried to resize the Vulkan swap chain to %ux%u while a scene was being "
|
||||
"rendered into it! The resize has been deferred to the next frame.",
|
||||
capabilities.currentExtent.width, capabilities.currentExtent.height);
|
||||
return false;
|
||||
}
|
||||
|
||||
nprintf(("vulkan", "Vulkan: window is %ux%u but the swap chain is %ux%u, rebuilding\n",
|
||||
capabilities.currentExtent.width, capabilities.currentExtent.height,
|
||||
m_current->extent.width, m_current->extent.height));
|
||||
|
||||
const bool restartFrame = m_frameInProgress;
|
||||
discardFrame();
|
||||
|
||||
m_current->needsRecreation = true;
|
||||
acquireNextSwapChainImage();
|
||||
|
||||
if (restartFrame) {
|
||||
setupFrame();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void VulkanRenderer::flip()
|
||||
{
|
||||
if (!m_frameInProgress) {
|
||||
@@ -197,9 +422,9 @@ void VulkanRenderer::flip()
|
||||
// unreliable (tearing/garbage in the composition image once the encode pass
|
||||
// samples it). RE-TEST on macOS hardware; if MoltenVK now honors the subpass
|
||||
// dependency, this explicit barrier can be removed.
|
||||
if (m_currentSwapChainImage < m_compositionImages.size()) {
|
||||
if (m_current->currentImage < m_current->compositionImages.size()) {
|
||||
ImageBarrier2 compositionBarrier;
|
||||
compositionBarrier.image = m_compositionImages[m_currentSwapChainImage].get();
|
||||
compositionBarrier.image = m_current->compositionImages[m_current->currentImage].get();
|
||||
compositionBarrier.levelCount = 1;
|
||||
compositionBarrier.layerCount = 1;
|
||||
compositionBarrier.oldLayout = vk::ImageLayout::eShaderReadOnlyOptimal;
|
||||
@@ -221,16 +446,21 @@ void VulkanRenderer::flip()
|
||||
|
||||
// Set up cleanup callback for command buffers
|
||||
auto buffersToFree = m_currentCommandBuffers;
|
||||
m_frames[m_currentFrame]->onFrameFinished([this, buffersToFree]() mutable {
|
||||
m_current->frames[m_currentFrame]->onFrameFinished([this, buffersToFree]() mutable {
|
||||
m_device->freeCommandBuffers(m_graphicsCommandPool.get(), buffersToFree);
|
||||
});
|
||||
|
||||
// Submit and present
|
||||
auto presentStatus = m_frames[m_currentFrame]->submitAndPresent(m_currentCommandBuffers,
|
||||
m_renderingFinishedSemaphores[m_currentSwapChainImage].get());
|
||||
auto& acquire = m_current->acquireSemaphores[m_current->currentAcquire];
|
||||
acquire.consumer = m_current->frames[m_currentFrame].get();
|
||||
auto presentStatus = m_current->frames[m_currentFrame]->submitAndPresent(m_currentCommandBuffers,
|
||||
acquire.semaphore.get(),
|
||||
m_current->renderFinishedSemaphores[m_current->currentImage].get(),
|
||||
m_current->currentImage,
|
||||
m_frameNumber);
|
||||
|
||||
if (shouldRecreateSwapChain(presentStatus)) {
|
||||
m_swapChainNeedsRecreation = true;
|
||||
if (shouldRecreateSwapChain(*m_current, presentStatus)) {
|
||||
m_current->needsRecreation = true;
|
||||
}
|
||||
|
||||
// Notify query manager that this frame's command buffer was submitted
|
||||
@@ -239,7 +469,7 @@ void VulkanRenderer::flip()
|
||||
}
|
||||
|
||||
// Track which swap chain image was just presented so saveScreen() can read it
|
||||
m_previousSwapChainImage = m_currentSwapChainImage;
|
||||
m_current->previousImage = m_current->currentImage;
|
||||
|
||||
// Clear current command buffer reference
|
||||
m_currentCommandBuffer = nullptr;
|
||||
@@ -262,6 +492,18 @@ void VulkanRenderer::flip()
|
||||
// acquireNextSwapChainImage, so we know the previous frame's commands
|
||||
// (including async upload CBs) have completed before destroying resources.
|
||||
m_deletionQueue->processDestructions();
|
||||
|
||||
// Retire finished texture upload command buffers. This belongs here rather than in
|
||||
// setupFrame(): the texture manager frees them on a countdown of FRAMES_TO_WAIT calls, which
|
||||
// only means "the GPU has moved on" if each call corresponds to a frame that actually
|
||||
// completed. setupFrame() stopped being that the moment viewports could be switched --
|
||||
// useViewport() sets a frame up on the incoming target without ever flipping it, so with
|
||||
// qtFRED's briefing map running there are about three setupFrame() calls per completed frame.
|
||||
// The countdown then ran out while uploads were still executing and freed command buffers in
|
||||
// the pending state (VUID-vkFreeCommandBuffers-pCommandBuffers-00047). Ticking it here, next
|
||||
// to the deletion queue and after the fence wait above, ties it back to real frame completion.
|
||||
Assertion(m_textureManager, "Vulkan TextureManager not initialized in flip!");
|
||||
m_textureManager->frameStart();
|
||||
}
|
||||
|
||||
void VulkanRenderer::beginSceneRendering()
|
||||
@@ -356,8 +598,8 @@ void VulkanRenderer::endSceneRendering()
|
||||
|
||||
PassBeginDesc pass;
|
||||
pass.renderPass = m_renderPassLoad.get();
|
||||
pass.framebuffer = m_swapChainFramebuffers[m_currentSwapChainImage].get();
|
||||
pass.extent = m_renderExtent;
|
||||
pass.framebuffer = m_current->framebuffers[m_current->currentImage].get();
|
||||
pass.extent = m_current->renderExtent;
|
||||
pass.clearValues = clearValues;
|
||||
pass.viewport = PassViewport::NoFlip;
|
||||
beginTrackedRenderPass(pass);
|
||||
@@ -367,9 +609,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_current->renderExtent.height),
|
||||
static_cast<float>(m_current->renderExtent.width),
|
||||
-static_cast<float>(m_current->renderExtent.height));
|
||||
|
||||
m_sceneRendering = false;
|
||||
m_useGbufRenderPass = false;
|
||||
@@ -610,8 +852,8 @@ void VulkanRenderer::resumeSwapChainPass()
|
||||
|
||||
PassBeginDesc pass;
|
||||
pass.renderPass = m_renderPassLoad.get();
|
||||
pass.framebuffer = m_swapChainFramebuffers[m_currentSwapChainImage].get();
|
||||
pass.extent = m_renderExtent;
|
||||
pass.framebuffer = m_current->framebuffers[m_current->currentImage].get();
|
||||
pass.extent = m_current->renderExtent;
|
||||
pass.clearValues = clearValues;
|
||||
beginTrackedRenderPass(pass);
|
||||
}
|
||||
@@ -624,8 +866,8 @@ void VulkanRenderer::restartCompositionPass()
|
||||
|
||||
PassBeginDesc pass;
|
||||
pass.renderPass = m_renderPass.get();
|
||||
pass.framebuffer = m_swapChainFramebuffers[m_currentSwapChainImage].get();
|
||||
pass.extent = m_renderExtent;
|
||||
pass.framebuffer = m_current->framebuffers[m_current->currentImage].get();
|
||||
pass.extent = m_current->renderExtent;
|
||||
pass.clearValues = clearValues;
|
||||
beginTrackedRenderPass(pass);
|
||||
}
|
||||
|
||||
@@ -16,7 +16,6 @@
|
||||
#include "libs/renderdoc/renderdoc.h"
|
||||
#include "mod_table/mod_table.h"
|
||||
|
||||
#include <SDL3/SDL_vulkan.h>
|
||||
#include <cstdint>
|
||||
|
||||
#include "graphics/openxr.h"
|
||||
@@ -90,18 +89,7 @@ bool checkDeviceExtensionSupport(PhysicalDeviceValues& values)
|
||||
return requiredExtensions.empty();
|
||||
}
|
||||
|
||||
bool checkSwapChainSupport(PhysicalDeviceValues& values, const vk::UniqueSurfaceKHR& surface)
|
||||
{
|
||||
values.surfaceCapabilities = values.device.getSurfaceCapabilitiesKHR(surface.get());
|
||||
auto fmts = values.device.getSurfaceFormatsKHR(surface.get());
|
||||
values.surfaceFormats.assign(fmts.begin(), fmts.end());
|
||||
auto modes = values.device.getSurfacePresentModesKHR(surface.get());
|
||||
values.presentModes.assign(modes.begin(), modes.end());
|
||||
|
||||
return !values.surfaceFormats.empty() && !values.presentModes.empty();
|
||||
}
|
||||
|
||||
bool isDeviceUnsuitable(PhysicalDeviceValues& values, const vk::UniqueSurfaceKHR& surface)
|
||||
bool isDeviceUnsuitable(PhysicalDeviceValues& values, vk::SurfaceKHR surface)
|
||||
{
|
||||
// We need a GPU. Reject CPU or "other" types.
|
||||
if (values.properties.deviceType != vk::PhysicalDeviceType::eDiscreteGpu &&
|
||||
@@ -125,7 +113,7 @@ bool isDeviceUnsuitable(PhysicalDeviceValues& values, const vk::UniqueSurfaceKHR
|
||||
// queue (which implicitly supports transfer). Async transfer on a separate
|
||||
// queue is future work and must be reintroduced end-to-end, including
|
||||
// queue-family ownership transfers -- not half-wired.
|
||||
if (!values.presentQueueIndex.initialized && values.device.getSurfaceSupportKHR(i, surface.get())) {
|
||||
if (!values.presentQueueIndex.initialized && values.device.getSurfaceSupportKHR(i, surface)) {
|
||||
values.presentQueueIndex.initialized = true;
|
||||
values.presentQueueIndex.index = i;
|
||||
}
|
||||
@@ -224,124 +212,6 @@ void printPhysicalDevice(const PhysicalDeviceValues& values)
|
||||
scoreDevice(values)));
|
||||
}
|
||||
|
||||
vk::SurfaceFormatKHR chooseSurfaceFormat(const PhysicalDeviceValues& values)
|
||||
{
|
||||
// When HDR output is requested, prefer a 10-bit HDR10 (PQ / ST.2084) surface
|
||||
// using BT.2020 primaries. The final output-encode pass writes PQ-encoded
|
||||
// BT.2020 values into this surface.
|
||||
if (Gr_enable_hdr) {
|
||||
for (const auto& availableFormat : values.surfaceFormats) {
|
||||
if ((availableFormat.format == vk::Format::eA2B10G10R10UnormPack32 ||
|
||||
availableFormat.format == vk::Format::eA2R10G10B10UnormPack32) &&
|
||||
availableFormat.colorSpace == vk::ColorSpaceKHR::eHdr10St2084EXT) {
|
||||
nprintf(("vulkan", "Vulkan: Selected HDR10 surface (10-bit, ST.2084/BT.2020)\n"));
|
||||
return availableFormat;
|
||||
}
|
||||
}
|
||||
nprintf(("vulkan", "Vulkan: HDR requested but no HDR10 surface format available; falling back to SDR\n"));
|
||||
}
|
||||
|
||||
// Use a non-sRGB (UNORM) format to match OpenGL's default framebuffer behavior.
|
||||
// The FSO shaders handle gamma correction manually in the fragment shader and
|
||||
// post-processing pipeline, so hardware sRGB conversion would double-correct.
|
||||
for (const auto& availableFormat : values.surfaceFormats) {
|
||||
if (availableFormat.format == vk::Format::eB8G8R8A8Unorm &&
|
||||
availableFormat.colorSpace == vk::ColorSpaceKHR::eSrgbNonlinear) {
|
||||
return availableFormat;
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: no preferred format matched. Pick the first concrete format,
|
||||
// defensively skipping any eUndefined entry (the legacy "any format allowed"
|
||||
// sentinel), and log the actual choice so it's visible in the log.
|
||||
for (const auto& availableFormat : values.surfaceFormats) {
|
||||
if (availableFormat.format != vk::Format::eUndefined) {
|
||||
nprintf(("vulkan", "Vulkan: no preferred surface format available; falling back to format=%d colorSpace=%d\n",
|
||||
static_cast<int>(availableFormat.format), static_cast<int>(availableFormat.colorSpace)));
|
||||
return availableFormat;
|
||||
}
|
||||
}
|
||||
|
||||
// Degenerate list (all eUndefined) — return the front entry and warn.
|
||||
nprintf(("vulkan", "Vulkan: surface format list has no concrete entry; using front (format=%d)\n",
|
||||
static_cast<int>(values.surfaceFormats.front().format)));
|
||||
return values.surfaceFormats.front();
|
||||
}
|
||||
|
||||
vk::PresentModeKHR choosePresentMode(const PhysicalDeviceValues& values)
|
||||
{
|
||||
// With vsync requested, use FIFO: it is the only spec-guaranteed mode and
|
||||
// the only one that actually caps the frame rate to the display. Mailbox is
|
||||
// tear-free but uncapped ("fast vsync") and must not be silently substituted
|
||||
// for requested vsync. Without vsync prefer Immediate (true uncapped), then
|
||||
// Mailbox (uncapped but tear-free), then the guaranteed FIFO fallback.
|
||||
vk::PresentModeKHR chosen = vk::PresentModeKHR::eFifo;
|
||||
|
||||
if (!Gr_enable_vsync) {
|
||||
for (const auto& availablePresentMode : values.presentModes) {
|
||||
if (availablePresentMode == vk::PresentModeKHR::eImmediate) {
|
||||
chosen = availablePresentMode;
|
||||
break;
|
||||
}
|
||||
if (availablePresentMode == vk::PresentModeKHR::eMailbox) {
|
||||
chosen = availablePresentMode;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const char* name = "Unknown";
|
||||
switch (chosen) {
|
||||
case vk::PresentModeKHR::eImmediate: name = "Immediate"; break;
|
||||
case vk::PresentModeKHR::eMailbox: name = "Mailbox"; break;
|
||||
case vk::PresentModeKHR::eFifo: name = "FIFO (vsync)"; break;
|
||||
case vk::PresentModeKHR::eFifoRelaxed: name = "FIFO Relaxed"; break;
|
||||
default: break;
|
||||
}
|
||||
mprintf(("Vulkan: Present mode: %s (Gr_enable_vsync=%d)\n", name, Gr_enable_vsync ? 1 : 0));
|
||||
|
||||
return chosen;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief The size of the actual window, which is not the render resolution
|
||||
*
|
||||
* Only used where the surface declines to tell us its extent (Wayland reports
|
||||
* UINT32_MAX and lets the client pick). gr_screen.max_w/max_h is the wrong
|
||||
* answer there: with -window_res set -- which -vr forces -- the window is
|
||||
* smaller than the resolution the engine draws at.
|
||||
*/
|
||||
vk::Extent2D windowExtent()
|
||||
{
|
||||
if (auto* viewport = os::getMainViewport()) {
|
||||
const auto size = viewport->getSize();
|
||||
if (size.first > 0 && size.second > 0) {
|
||||
return {size.first, size.second};
|
||||
}
|
||||
}
|
||||
|
||||
if (Cmdline_window_res) {
|
||||
return {Cmdline_window_res->first, Cmdline_window_res->second};
|
||||
}
|
||||
|
||||
return {static_cast<uint32_t>(gr_screen.max_w), static_cast<uint32_t>(gr_screen.max_h)};
|
||||
}
|
||||
|
||||
vk::Extent2D chooseSwapChainExtent(const vk::SurfaceCapabilitiesKHR& capabilities, uint32_t width, uint32_t height)
|
||||
{
|
||||
if (capabilities.currentExtent.width != UINT32_MAX) {
|
||||
return capabilities.currentExtent;
|
||||
} else {
|
||||
VkExtent2D actualExtent = {width, height};
|
||||
|
||||
actualExtent.width = std::max(capabilities.minImageExtent.width,
|
||||
std::min(capabilities.maxImageExtent.width, actualExtent.width));
|
||||
actualExtent.height = std::max(capabilities.minImageExtent.height,
|
||||
std::min(capabilities.maxImageExtent.height, actualExtent.height));
|
||||
|
||||
return actualExtent;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
bool VulkanRenderer::initialize()
|
||||
{
|
||||
@@ -364,6 +234,15 @@ bool VulkanRenderer::initialize()
|
||||
return false;
|
||||
}
|
||||
|
||||
// Everything from the surface down hangs off a target, so the main one has to exist before
|
||||
// createTargetSurface() has anywhere to put its handle. The game only ever has this one; qtFRED
|
||||
// adds a second when the briefing map first asks to be rendered into.
|
||||
auto mainTarget = std::make_unique<VulkanPresentTarget>();
|
||||
mainTarget->viewport = os::getMainViewport();
|
||||
m_mainTarget = mainTarget.get();
|
||||
m_current = m_mainTarget;
|
||||
m_targets.emplace(mainTarget->viewport, std::move(mainTarget));
|
||||
|
||||
try {
|
||||
if (!initializeInstance()) {
|
||||
mprintf(("Failed to create Vulkan instance!\n"));
|
||||
@@ -374,7 +253,7 @@ bool VulkanRenderer::initialize()
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!initializeSurface()) {
|
||||
if (!createTargetSurface(*m_mainTarget)) {
|
||||
nprintf(("vulkan", "Failed to create Vulkan surface!\n"));
|
||||
return false;
|
||||
}
|
||||
@@ -436,18 +315,20 @@ bool VulkanRenderer::initialize()
|
||||
|
||||
createCommandPool(deviceValues);
|
||||
|
||||
if (!createSwapChain(deviceValues)) {
|
||||
if (!createSwapChain(*m_mainTarget, deviceValues)) {
|
||||
nprintf(("vulkan", "Failed to create swap chain.\n"));
|
||||
return false;
|
||||
}
|
||||
|
||||
createDepthResources();
|
||||
createCompositionResources();
|
||||
createEncodeRenderPass();
|
||||
createDepthResources(*m_mainTarget);
|
||||
createCompositionResources(*m_mainTarget);
|
||||
// Shared by every target, and built here from the main target's negotiated format. Any later
|
||||
// target has to match it -- see createTargetResources().
|
||||
createEncodeRenderPass(m_mainTarget->imageFormat);
|
||||
createRenderPass();
|
||||
createFrameBuffers();
|
||||
createFrameBuffers(*m_mainTarget);
|
||||
|
||||
createPresentSyncObjects();
|
||||
createPresentSyncObjects(*m_mainTarget);
|
||||
|
||||
// Initialize texture manager (needs command pool for uploads)
|
||||
m_textureManager = std::make_unique<VulkanTextureManager>();
|
||||
@@ -532,7 +413,7 @@ bool VulkanRenderer::initialize()
|
||||
// Initialize post-processing
|
||||
m_postProcessor = std::make_unique<VulkanPostProcessor>();
|
||||
if (!m_postProcessor->init(m_device.get(), m_physicalDevice, m_memoryManager.get(),
|
||||
m_renderExtent, m_depthFormat, m_hdrActive)) {
|
||||
m_mainTarget->renderExtent, m_depthFormat, m_mainTarget->hdrActive)) {
|
||||
mprintf(("Warning: Failed to initialize Vulkan post-processor, post-processing will be disabled\n"));
|
||||
m_postProcessor.reset();
|
||||
} else {
|
||||
@@ -635,29 +516,40 @@ bool VulkanRenderer::initDisplayDevice() const
|
||||
}
|
||||
bool VulkanRenderer::initializeInstance()
|
||||
{
|
||||
auto* vulkanSupport = m_graphicsOps->getVulkanSupport();
|
||||
if (vulkanSupport == nullptr) {
|
||||
mprintf(("Vulkan: The windowing implementation in use cannot present through Vulkan!\n"));
|
||||
return false;
|
||||
}
|
||||
|
||||
const auto vkGetInstanceProcAddr =
|
||||
reinterpret_cast<PFN_vkGetInstanceProcAddr>(SDL_Vulkan_GetVkGetInstanceProcAddr());
|
||||
reinterpret_cast<PFN_vkGetInstanceProcAddr>(vulkanSupport->getVulkanProcAddr());
|
||||
if (vkGetInstanceProcAddr == nullptr) {
|
||||
mprintf(("Vulkan: Could not get vkGetInstanceProcAddr from the windowing system!\n"));
|
||||
return false;
|
||||
}
|
||||
|
||||
VULKAN_HPP_DEFAULT_DISPATCHER.init(vkGetInstanceProcAddr);
|
||||
|
||||
VkInstanceCreateFlags createFlags = 0;
|
||||
uint32_t count = 0;
|
||||
|
||||
auto extPtr = SDL_Vulkan_GetInstanceExtensions(&count);
|
||||
|
||||
if ( !extPtr ) {
|
||||
mprintf(("Error in SDL_Vulkan_GetInstanceExtensions: %s\n", SDL_GetError()));
|
||||
// The windowing system only knows about the extensions its own surfaces need; everything else
|
||||
// (debug utils, swap chain color space, portability) is decided below against what the driver
|
||||
// actually supports. This must outlive `extensions`, which only holds views into it.
|
||||
SCP_vector<SCP_string> windowExtensions;
|
||||
if (!vulkanSupport->getVulkanInstanceExtensions(windowExtensions)) {
|
||||
mprintf(("Vulkan: Could not determine the instance extensions required by the window system!\n"));
|
||||
return false;
|
||||
}
|
||||
|
||||
SCP_vector<const char*> extensions;
|
||||
extensions.reserve(count);
|
||||
extensions.reserve(windowExtensions.size());
|
||||
|
||||
for (uint32_t i = 0; i < count; ++i) {
|
||||
for (const auto& windowExtension : windowExtensions) {
|
||||
// SDL 3.2 will include portability enueration extension even if it's not
|
||||
// supported, so make sure not to add it blindly, and check for it later
|
||||
if (SDL_strcmp(VK_KHR_PORTABILITY_ENUMERATION_EXTENSION_NAME, extPtr[i])) {
|
||||
extensions.push_back(extPtr[i]);
|
||||
if (stricmp(windowExtension.c_str(), VK_KHR_PORTABILITY_ENUMERATION_EXTENSION_NAME) != 0) {
|
||||
extensions.push_back(windowExtension.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -819,23 +711,6 @@ bool VulkanRenderer::initializeInstance()
|
||||
return true;
|
||||
}
|
||||
|
||||
bool VulkanRenderer::initializeSurface()
|
||||
{
|
||||
const auto window = os::getSDLMainWindow();
|
||||
|
||||
VkSurfaceKHR surface;
|
||||
if (!SDL_Vulkan_CreateSurface(window, static_cast<VkInstance>(*m_vkInstance), nullptr, &surface)) {
|
||||
nprintf(("vulkan", "Failed to create vulkan surface: %s\n", SDL_GetError()));
|
||||
return false;
|
||||
}
|
||||
|
||||
const vk::detail::ObjectDestroy<vk::Instance, VULKAN_HPP_DEFAULT_DISPATCHER_TYPE> deleter(*m_vkInstance,
|
||||
nullptr,
|
||||
VULKAN_HPP_DEFAULT_DISPATCHER);
|
||||
m_vkSurface = vk::UniqueSurfaceKHR(vk::SurfaceKHR(surface), deleter);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool VulkanRenderer::pickPhysicalDevice(PhysicalDeviceValues& deviceValues)
|
||||
{
|
||||
const auto devices = m_vkInstance->enumeratePhysicalDevices();
|
||||
@@ -891,7 +766,7 @@ bool VulkanRenderer::pickPhysicalDevice(PhysicalDeviceValues& deviceValues)
|
||||
// Remove devices that do not have the features we need
|
||||
values.erase(std::remove_if(values.begin(),
|
||||
values.end(),
|
||||
[this](PhysicalDeviceValues& value) { return isDeviceUnsuitable(value, m_vkSurface); }),
|
||||
[this](PhysicalDeviceValues& value) { return isDeviceUnsuitable(value, m_mainTarget->surface.get()); }),
|
||||
values.end());
|
||||
if (values.empty()) {
|
||||
return false;
|
||||
@@ -947,6 +822,15 @@ bool VulkanRenderer::createLogicalDevice(const PhysicalDeviceValues& deviceValue
|
||||
enabledExtensions.push_back(VK_EXT_HDR_METADATA_EXTENSION_NAME);
|
||||
mprintf(("Vulkan: Enabling %s (HDR10 metadata)\n", VK_EXT_HDR_METADATA_EXTENSION_NAME));
|
||||
}
|
||||
// Required by the Vulkan Portability subset whenever the physical device
|
||||
// advertises it (MoltenVK / macOS). Creating a device without it is
|
||||
// undefined behavior and trips VUID-VkDeviceCreateInfo-pProperties-04451.
|
||||
// The extension-name macro lives in vulkan_beta.h (VK_ENABLE_BETA_EXTENSIONS);
|
||||
// our headers do not enable that, so use the literal string instead.
|
||||
if (strcmp(ext.extensionName, "VK_KHR_portability_subset") == 0) {
|
||||
enabledExtensions.push_back("VK_KHR_portability_subset");
|
||||
mprintf(("Vulkan: Enabling VK_KHR_portability_subset\n"));
|
||||
}
|
||||
if (strcmp(ext.extensionName, VK_KHR_ACCELERATION_STRUCTURE_EXTENSION_NAME) == 0) {
|
||||
hasAccelerationStructureExt = true;
|
||||
}
|
||||
@@ -1095,263 +979,4 @@ bool VulkanRenderer::createLogicalDevice(const PhysicalDeviceValues& deviceValue
|
||||
|
||||
return true;
|
||||
}
|
||||
bool VulkanRenderer::createSwapChain(const PhysicalDeviceValues& deviceValues, vk::SwapchainKHR oldSwapchain)
|
||||
{
|
||||
// Choose one more than the minimum to avoid driver synchronization if it is not done with a thread yet
|
||||
uint32_t imageCount = deviceValues.surfaceCapabilities.minImageCount + 1;
|
||||
if (deviceValues.surfaceCapabilities.maxImageCount > 0 &&
|
||||
imageCount > deviceValues.surfaceCapabilities.maxImageCount) {
|
||||
imageCount = deviceValues.surfaceCapabilities.maxImageCount;
|
||||
}
|
||||
|
||||
const auto surfaceFormat = chooseSurfaceFormat(deviceValues);
|
||||
|
||||
const vk::Extent2D window = windowExtent();
|
||||
|
||||
vk::SwapchainCreateInfoKHR createInfo;
|
||||
createInfo.surface = m_vkSurface.get();
|
||||
createInfo.minImageCount = imageCount;
|
||||
createInfo.imageFormat = surfaceFormat.format;
|
||||
createInfo.imageColorSpace = surfaceFormat.colorSpace;
|
||||
createInfo.imageExtent = chooseSwapChainExtent(deviceValues.surfaceCapabilities, window.width, window.height);
|
||||
createInfo.imageArrayLayers = 1;
|
||||
createInfo.imageUsage = vk::ImageUsageFlagBits::eColorAttachment
|
||||
| vk::ImageUsageFlagBits::eTransferSrc
|
||||
| vk::ImageUsageFlagBits::eTransferDst;
|
||||
|
||||
const uint32_t queueFamilyIndices[] = {deviceValues.graphicsQueueIndex.index, deviceValues.presentQueueIndex.index};
|
||||
if (deviceValues.graphicsQueueIndex.index != deviceValues.presentQueueIndex.index) {
|
||||
createInfo.imageSharingMode = vk::SharingMode::eConcurrent;
|
||||
createInfo.queueFamilyIndexCount = 2;
|
||||
createInfo.pQueueFamilyIndices = queueFamilyIndices;
|
||||
} else {
|
||||
createInfo.imageSharingMode = vk::SharingMode::eExclusive;
|
||||
}
|
||||
|
||||
// preTransform describes the transform already applied to the rendered content.
|
||||
// FSO does not pre-rotate it, so request identity whenever supported. If this
|
||||
// differs from currentTransform (e.g. landscape on an Android phone), the
|
||||
// presentation engine compensates during presentation. Passing currentTransform
|
||||
// instead would claim that FSO had already performed that rotation.
|
||||
auto supported = deviceValues.surfaceCapabilities.supportedTransforms;
|
||||
if (supported & vk::SurfaceTransformFlagBitsKHR::eIdentity) {
|
||||
createInfo.preTransform = vk::SurfaceTransformFlagBitsKHR::eIdentity;
|
||||
}
|
||||
else {
|
||||
createInfo.preTransform = deviceValues.surfaceCapabilities.currentTransform;
|
||||
}
|
||||
|
||||
createInfo.compositeAlpha = vk::CompositeAlphaFlagBitsKHR::eOpaque;
|
||||
createInfo.presentMode = choosePresentMode(deviceValues);
|
||||
createInfo.clipped = true;
|
||||
createInfo.oldSwapchain = oldSwapchain;
|
||||
|
||||
auto newSwapChain = m_device->createSwapchainKHRUnique(createInfo);
|
||||
|
||||
// Clear old resources before replacing the swap chain
|
||||
m_swapChainFramebuffers.clear();
|
||||
m_swapChainImageViews.clear();
|
||||
|
||||
m_swapChain = std::move(newSwapChain);
|
||||
|
||||
auto swapChainImages = m_device->getSwapchainImagesKHR(m_swapChain.get());
|
||||
m_swapChainImages.assign(swapChainImages.begin(), swapChainImages.end());
|
||||
m_swapChainImageFormat = surfaceFormat.format;
|
||||
m_swapChainColorSpace = surfaceFormat.colorSpace;
|
||||
m_hdrActive = (surfaceFormat.colorSpace == vk::ColorSpaceKHR::eHdr10St2084EXT);
|
||||
Gr_hdr_output_active = m_hdrActive;
|
||||
m_swapChainExtent = createInfo.imageExtent;
|
||||
m_swapChainPreTransform = createInfo.preTransform;
|
||||
|
||||
// Without -window_res the render resolution simply is the window, so follow
|
||||
// gr_screen (which gr_screen_resize() has already updated for this
|
||||
// recreation). With it, the two are deliberately independent -- the window
|
||||
// is whatever Cmdline_window_res says (SDLGraphicsOperations::createViewport
|
||||
// forces it) while the engine keeps drawing at gr_screen.max_w/max_h -- so
|
||||
// the value captured in initialize() must survive untouched.
|
||||
if (!Cmdline_window_res) {
|
||||
m_renderExtent = vk::Extent2D(static_cast<uint32_t>(gr_screen.max_w), static_cast<uint32_t>(gr_screen.max_h));
|
||||
}
|
||||
|
||||
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",
|
||||
m_renderExtent.width, m_renderExtent.height,
|
||||
m_swapChainExtent.width, m_swapChainExtent.height));
|
||||
|
||||
m_swapChainImageViews.reserve(m_swapChainImages.size());
|
||||
for (const auto& image : m_swapChainImages) {
|
||||
vk::ImageViewCreateInfo viewCreateInfo;
|
||||
viewCreateInfo.image = image;
|
||||
viewCreateInfo.viewType = vk::ImageViewType::e2D;
|
||||
viewCreateInfo.format = m_swapChainImageFormat;
|
||||
|
||||
viewCreateInfo.components.r = vk::ComponentSwizzle::eIdentity;
|
||||
viewCreateInfo.components.g = vk::ComponentSwizzle::eIdentity;
|
||||
viewCreateInfo.components.b = vk::ComponentSwizzle::eIdentity;
|
||||
viewCreateInfo.components.a = vk::ComponentSwizzle::eIdentity;
|
||||
|
||||
viewCreateInfo.subresourceRange.aspectMask = vk::ImageAspectFlagBits::eColor;
|
||||
viewCreateInfo.subresourceRange.baseMipLevel = 0;
|
||||
viewCreateInfo.subresourceRange.levelCount = 1;
|
||||
viewCreateInfo.subresourceRange.baseArrayLayer = 0;
|
||||
viewCreateInfo.subresourceRange.layerCount = 1;
|
||||
|
||||
m_swapChainImageViews.push_back(m_device->createImageViewUnique(viewCreateInfo));
|
||||
}
|
||||
|
||||
// No layout transition needed for the new images: the only pass that writes
|
||||
// them (m_encodeRenderPass) uses initialLayout=eUndefined with
|
||||
// loadOp=eDontCare, so their first use never reads prior contents.
|
||||
|
||||
// Advertise HDR10 mastering/content metadata to the compositor when active.
|
||||
if (m_hdrActive && m_hdrMetadataSupported) {
|
||||
vk::HdrMetadataEXT metadata;
|
||||
// BT.2020 display primaries and D65 white point
|
||||
metadata.displayPrimaryRed = vk::XYColorEXT{0.708f, 0.292f};
|
||||
metadata.displayPrimaryGreen = vk::XYColorEXT{0.170f, 0.797f};
|
||||
metadata.displayPrimaryBlue = vk::XYColorEXT{0.131f, 0.046f};
|
||||
metadata.whitePoint = vk::XYColorEXT{0.3127f, 0.3290f};
|
||||
metadata.maxLuminance = Gr_hdr_peak_nits;
|
||||
metadata.minLuminance = 0.0f;
|
||||
metadata.maxContentLightLevel = Gr_hdr_peak_nits;
|
||||
metadata.maxFrameAverageLightLevel = Gr_hdr_paperwhite_nits;
|
||||
m_device->setHdrMetadataEXT(m_swapChain.get(), metadata);
|
||||
mprintf(("Vulkan: HDR10 metadata set (peak %.0f nits, paper white %.0f nits)\n",
|
||||
Gr_hdr_peak_nits, Gr_hdr_paperwhite_nits));
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool VulkanRenderer::shouldRecreateSwapChain(SwapChainStatus status) const
|
||||
{
|
||||
// An already requested rebuild also avoids redundant Android surface queries.
|
||||
if (m_swapChainNeedsRecreation || status == SwapChainStatus::eOutOfDate) {
|
||||
return true;
|
||||
}
|
||||
if (status != SwapChainStatus::eSuboptimal) {
|
||||
return false;
|
||||
}
|
||||
|
||||
#ifdef __ANDROID__
|
||||
if (m_swapChainPreTransform == vk::SurfaceTransformFlagBitsKHR::eIdentity) {
|
||||
vk::SurfaceCapabilitiesKHR capabilities;
|
||||
try {
|
||||
capabilities = m_physicalDevice.getSurfaceCapabilitiesKHR(m_vkSurface.get());
|
||||
} catch (const vk::SystemError&) {
|
||||
// Let the normal recreation path handle a failed surface query.
|
||||
return true;
|
||||
}
|
||||
if (capabilities.currentTransform != m_swapChainPreTransform &&
|
||||
(capabilities.supportedTransforms & m_swapChainPreTransform)) {
|
||||
const auto window = windowExtent();
|
||||
const auto extent = chooseSwapChainExtent(capabilities, window.width, window.height);
|
||||
if (extent == m_swapChainExtent) {
|
||||
// Android can report SUBOPTIMAL on every present while the compositor
|
||||
// compensates for our identity preTransform. Rebuilding with the same
|
||||
// extent and preTransform would just repeat that condition next frame.
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool VulkanRenderer::recreateSwapChain()
|
||||
{
|
||||
nprintf(("vulkan", "Vulkan: Recreating swap chain...\n"));
|
||||
|
||||
// Wait for all frames to finish so no resources are in use
|
||||
for (uint32_t i = 0; i < MAX_FRAMES_IN_FLIGHT; ++i) {
|
||||
m_frames[i]->waitForFinish();
|
||||
}
|
||||
m_device->waitIdle();
|
||||
|
||||
// Re-query surface state (may have changed due to resize/compositor)
|
||||
PhysicalDeviceValues freshValues;
|
||||
freshValues.device = m_physicalDevice;
|
||||
freshValues.surfaceCapabilities = m_physicalDevice.getSurfaceCapabilitiesKHR(m_vkSurface.get());
|
||||
auto fmts = m_physicalDevice.getSurfaceFormatsKHR(m_vkSurface.get());
|
||||
freshValues.surfaceFormats.assign(fmts.begin(), fmts.end());
|
||||
auto modes = m_physicalDevice.getSurfacePresentModesKHR(m_vkSurface.get());
|
||||
freshValues.presentModes.assign(modes.begin(), modes.end());
|
||||
freshValues.graphicsQueueIndex = {true, m_graphicsQueueFamilyIndex};
|
||||
freshValues.presentQueueIndex = {true, m_presentQueueFamilyIndex};
|
||||
|
||||
// Check for 0x0 extent (minimized window) — caller should retry later
|
||||
const vk::Extent2D window = windowExtent();
|
||||
auto extent = chooseSwapChainExtent(freshValues.surfaceCapabilities, window.width, window.height);
|
||||
if (extent.width == 0 || extent.height == 0) {
|
||||
nprintf(("vulkan", "Vulkan: Surface extent is 0x0 (minimized), deferring swap chain recreation\n"));
|
||||
return false;
|
||||
}
|
||||
|
||||
// Recreate all size-dependent resources. The render passes (including
|
||||
// m_encodeRenderPass) are intentionally NOT recreated so cached pipelines
|
||||
// remain valid; only images, views, and framebuffers are rebuilt.
|
||||
const vk::Format oldSwapChainFormat = m_swapChainImageFormat;
|
||||
createSwapChain(freshValues, m_swapChain.get());
|
||||
|
||||
// Known limitation: if the surface format changes across recreation (e.g.
|
||||
// the window moves to a display that flips HDR10 availability),
|
||||
// m_encodeRenderPass and the post-processor's LDR format would need a full
|
||||
// rebuild, which we don't support yet. Log it loudly.
|
||||
if (m_swapChainImageFormat != oldSwapChainFormat) {
|
||||
mprintf(("Vulkan: WARNING - swap chain surface format changed across recreation (%d -> %d); "
|
||||
"rendering may be broken until restart\n",
|
||||
static_cast<int>(oldSwapChainFormat), static_cast<int>(m_swapChainImageFormat)));
|
||||
}
|
||||
|
||||
// The depth buffer is extent-sized; recreate it before the framebuffers
|
||||
// that attach its view. createDepthResources() verifies the format is stable
|
||||
// (the kept render passes bake it in).
|
||||
destroyDepthResources();
|
||||
createDepthResources();
|
||||
|
||||
createCompositionResources();
|
||||
createFrameBuffers();
|
||||
|
||||
// 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)) {
|
||||
mprintf(("Vulkan: post-processor resize failed, disabling post-processing!\n"));
|
||||
setPostProcessor(nullptr);
|
||||
m_postProcessor->shutdown();
|
||||
m_postProcessor.reset();
|
||||
}
|
||||
|
||||
// Drop renderer-side cached state that may reference destroyed views
|
||||
if (m_drawManager) {
|
||||
m_drawManager->onResize();
|
||||
}
|
||||
m_sceneDepthCopiedThisFrame = false;
|
||||
m_sceneDepthSaved = false;
|
||||
|
||||
// Update VulkanRenderFrame handles to point to the new swap chain, and
|
||||
// recreate their semaphores: an acquire that succeeded against the old swap
|
||||
// chain but was never consumed by a submit leaves the image-available
|
||||
// semaphore signaled. All frames are idle here (waited above), so
|
||||
// recreating is safe and unambiguous.
|
||||
for (auto& frame : m_frames) {
|
||||
frame->updateSwapChain(m_swapChain.get());
|
||||
frame->recreateSyncObjects();
|
||||
}
|
||||
|
||||
// Reset swap chain image tracking
|
||||
m_swapChainImageRenderImage.clear();
|
||||
m_swapChainImageRenderImage.resize(m_swapChainImages.size(), nullptr);
|
||||
createRenderingFinishedSemaphores();
|
||||
m_previousSwapChainImage = UINT32_MAX;
|
||||
|
||||
m_swapChainNeedsRecreation = false;
|
||||
|
||||
nprintf(("vulkan", "Vulkan: Swap chain recreated successfully (%ux%u, %zu images)\n",
|
||||
m_swapChainExtent.width, m_swapChainExtent.height, m_swapChainImages.size()));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace graphics::vulkan
|
||||
@@ -1,7 +1,5 @@
|
||||
#include "VulkanShader.h"
|
||||
#include "VulkanShaderCompiler.h"
|
||||
#include "VulkanRenderer.h"
|
||||
#include "gr_vulkan.h"
|
||||
|
||||
namespace graphics::vulkan {
|
||||
|
||||
@@ -31,7 +29,6 @@ void vulkan_recompile_all_shaders(const std::function<void(size_t, size_t)>& pro
|
||||
{
|
||||
auto* shaderManager = getShaderManager();
|
||||
shaderManager->recompileAllShaders(progressCallback);
|
||||
getRendererInstance()->requestPipelineRebuild();
|
||||
}
|
||||
|
||||
VulkanShaderManager::VulkanShaderManager() = default;
|
||||
@@ -146,19 +143,13 @@ void VulkanShaderManager::recompileAllShaders(const std::function<void(size_t, s
|
||||
}
|
||||
|
||||
shader.valid = shader.vertexModule && shader.fragmentModule;
|
||||
// the inactive anti-aliasing family cannot compile under the current mode, so build it again on next use
|
||||
if (!shader.valid) {
|
||||
m_shaderMap.erase(shader_key_t(static_cast<int>(type), flags));
|
||||
m_freeSlots.push_back(static_cast<size_t>(&shader - m_shaders.data()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// callers expect 0..total-1, like OpenGL; the retail options popup waits for current + 1 == total
|
||||
++current;
|
||||
if (progressCallback) {
|
||||
progressCallback(current, total);
|
||||
}
|
||||
++current;
|
||||
}
|
||||
|
||||
nprintf(("vulkan", "VulkanShaderManager: Recompiled %zu shaders\n", total));
|
||||
|
||||
@@ -251,25 +251,52 @@ void VulkanStateTracker::bindPipeline(vk::Pipeline pipeline, vk::PipelineLayout
|
||||
}
|
||||
}
|
||||
|
||||
void VulkanStateTracker::bindDescriptorSet(DescriptorSetIndex setIndex, vk::DescriptorSet set,
|
||||
const SCP_vector<uint32_t>& dynamicOffsets)
|
||||
void VulkanStateTracker::bindDescriptorSet(DescriptorSetIndex setIndex,
|
||||
vk::DescriptorSet set,
|
||||
ArrayView<uint32_t> dynamicOffsets)
|
||||
{
|
||||
Assertion(m_cmdBuffer, "bindDescriptorSet called without active command buffer!");
|
||||
Assertion(m_currentPipelineLayout, "bindDescriptorSet called without bound pipeline layout!");
|
||||
Assertion(set, "bindDescriptorSet called with null descriptor set!");
|
||||
|
||||
auto index = static_cast<uint32_t>(setIndex);
|
||||
const uint32_t dynCount = VulkanDescriptorManager::getDynamicOffsetCount(setIndex);
|
||||
// vkCmdBindDescriptorSets reads exactly dynCount entries, so a caller that hands over
|
||||
// fewer would have it read past the end of their array. The view carries its own length,
|
||||
// so that is a check rather than a convention.
|
||||
Assertion(dynamicOffsets.size >= dynCount,
|
||||
"bindDescriptorSet: set %u declares %u dynamic descriptors but only " SIZE_T_ARG
|
||||
" offsets were supplied!",
|
||||
index,
|
||||
dynCount,
|
||||
dynamicOffsets.size);
|
||||
|
||||
if (m_boundDescriptorSets[index] != set) {
|
||||
m_cmdBuffer.bindDescriptorSets(
|
||||
vk::PipelineBindPoint::eGraphics,
|
||||
// The redundancy check has to cover the dynamic offsets too, not just the set
|
||||
// handle: with dynamic UBOs the common case is the *same* set rebound with a new
|
||||
// offset every draw, and skipping that bind would silently feed every draw the
|
||||
// previous draw's uniforms.
|
||||
auto& boundDyn = m_boundDynamicOffsets[index];
|
||||
bool offsetsChanged = false;
|
||||
for (uint32_t i = 0; i < dynCount; ++i) {
|
||||
if (boundDyn[i] != dynamicOffsets.data[i]) {
|
||||
offsetsChanged = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (m_boundDescriptorSets[index] != set || offsetsChanged) {
|
||||
m_cmdBuffer.bindDescriptorSets(vk::PipelineBindPoint::eGraphics,
|
||||
m_currentPipelineLayout,
|
||||
index,
|
||||
1, &set,
|
||||
static_cast<uint32_t>(dynamicOffsets.size()),
|
||||
dynamicOffsets.empty() ? nullptr : dynamicOffsets.data());
|
||||
1,
|
||||
&set,
|
||||
dynCount,
|
||||
dynCount > 0 ? dynamicOffsets.data : nullptr);
|
||||
|
||||
m_boundDescriptorSets[index] = set;
|
||||
for (uint32_t i = 0; i < dynCount; ++i) {
|
||||
boundDyn[i] = dynamicOffsets.data[i];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -134,10 +134,18 @@ class VulkanStateTracker {
|
||||
|
||||
/**
|
||||
* @brief Bind descriptor set
|
||||
*
|
||||
* @param dynamicOffsets Offsets for the set layout's eUniformBufferDynamic
|
||||
* bindings, ordered by binding number. Must hold at least
|
||||
* VulkanDescriptorManager::getDynamicOffsetCount(setIndex) entries --
|
||||
* asserted here rather than trusted, which is why this is a view and not
|
||||
* a bare pointer. DescriptorWriter::dynamicOffsets() and the
|
||||
* *_DYNAMIC_OFFSET_COUNT-sized arrays at the call sites both convert
|
||||
* implicitly. May be empty only for a set that declares none.
|
||||
*/
|
||||
void bindDescriptorSet(DescriptorSetIndex setIndex,
|
||||
vk::DescriptorSet set,
|
||||
const SCP_vector<uint32_t>& dynamicOffsets = {});
|
||||
ArrayView<uint32_t> dynamicOffsets = {});
|
||||
|
||||
// ========== Buffer Binding ==========
|
||||
|
||||
@@ -312,6 +320,11 @@ class VulkanStateTracker {
|
||||
|
||||
// Descriptor sets
|
||||
std::array<vk::DescriptorSet, static_cast<size_t>(DescriptorSetIndex::Count)> m_boundDescriptorSets;
|
||||
// Dynamic offsets the currently-bound set was bound with, so an offset-only
|
||||
// change still forces a rebind (see bindDescriptorSet).
|
||||
std::array<std::array<uint32_t, DescriptorWriter::MAX_DYNAMIC_OFFSETS_PER_SET>,
|
||||
static_cast<size_t>(DescriptorSetIndex::Count)>
|
||||
m_boundDynamicOffsets{};
|
||||
|
||||
// Dynamic state
|
||||
vk::Viewport m_viewport;
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
#include <algorithm>
|
||||
#include "VulkanBarrier.h"
|
||||
#include "VulkanBuffer.h"
|
||||
#include "VulkanConstants.h"
|
||||
#include "VulkanDeletionQueue.h"
|
||||
#include "VulkanRenderer.h"
|
||||
#include "gr_vulkan.h"
|
||||
@@ -247,6 +248,9 @@ bool VulkanTextureManager::init(vk::Device device, vk::PhysicalDevice physicalDe
|
||||
m_fallback3DView, ImageViewType::Volume3D, 1, false, vk::ImageType::e3D)) {
|
||||
return false;
|
||||
}
|
||||
if (!createFallbackShadowTexture()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check BCx and ETC2 Support
|
||||
auto features = m_physicalDevice.getFeatures();
|
||||
@@ -349,12 +353,27 @@ void VulkanTextureManager::shutdown()
|
||||
if (m_fallback2DArrayAllocation.isValid()) {
|
||||
m_memoryManager->freeAllocation(m_fallback2DArrayAllocation);
|
||||
}
|
||||
if (m_fallbackShadowView) {
|
||||
m_device.destroyImageView(m_fallbackShadowView);
|
||||
m_fallbackShadowView = nullptr;
|
||||
}
|
||||
if (m_fallbackShadowTexture) {
|
||||
m_device.destroyImage(m_fallbackShadowTexture);
|
||||
m_fallbackShadowTexture = nullptr;
|
||||
}
|
||||
if (m_fallbackShadowAllocation.isValid()) {
|
||||
m_memoryManager->freeAllocation(m_fallbackShadowAllocation);
|
||||
}
|
||||
|
||||
// Destroy samplers
|
||||
if (m_defaultSampler) {
|
||||
m_device.destroySampler(m_defaultSampler);
|
||||
m_defaultSampler = nullptr;
|
||||
}
|
||||
if (m_shadowCompareSampler) {
|
||||
m_device.destroySampler(m_shadowCompareSampler);
|
||||
m_shadowCompareSampler = nullptr;
|
||||
}
|
||||
|
||||
for (auto& pair : m_samplerCache) {
|
||||
m_device.destroySampler(pair.second);
|
||||
@@ -2225,6 +2244,12 @@ vk::DescriptorImageInfo VulkanTextureManager::getFallbackTextureInfo3D()
|
||||
vk::ImageLayout::eShaderReadOnlyOptimal};
|
||||
}
|
||||
|
||||
vk::DescriptorImageInfo VulkanTextureManager::getFallbackShadowMapInfo()
|
||||
{
|
||||
return {m_shadowCompareSampler, m_fallbackShadowView,
|
||||
vk::ImageLayout::eShaderReadOnlyOptimal};
|
||||
}
|
||||
|
||||
tcache_slot_vulkan* VulkanTextureManager::getTextureSlot(int handle)
|
||||
{
|
||||
(void)this;
|
||||
@@ -2528,7 +2553,7 @@ bool VulkanTextureManager::createImage(uint32_t width, uint32_t height, uint32_t
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!m_memoryManager->allocateImageMemory(image, memUsage, allocation)) {
|
||||
if (!m_memoryManager->allocateImageMemory(image, memUsage, allocation, MemoryPurpose::Texture)) {
|
||||
m_device.destroyImage(image);
|
||||
image = nullptr;
|
||||
return false;
|
||||
@@ -2576,6 +2601,74 @@ vk::ImageView VulkanTextureManager::createImageView(vk::Image image, vk::Format
|
||||
}
|
||||
}
|
||||
|
||||
bool VulkanTextureManager::createFallbackShadowTexture()
|
||||
{
|
||||
// One layer is enough: sampling clamps the array coordinate to [0, layerCount - 1],
|
||||
// so a cascade index past the end is well defined. The real cascade count is not
|
||||
// known this early anyway -- it follows Shadow_quality, which the renderer applies later.
|
||||
if (!createImage(1, 1, 1, SHADOW_DEPTH_FORMAT, vk::ImageTiling::eOptimal,
|
||||
vk::ImageUsageFlagBits::eTransferDst | vk::ImageUsageFlagBits::eSampled,
|
||||
MemoryUsage::GpuOnly, m_fallbackShadowTexture, m_fallbackShadowAllocation)) {
|
||||
nprintf(("vulkan", "Failed to create fallback shadow map image!\n"));
|
||||
return false;
|
||||
}
|
||||
|
||||
m_fallbackShadowView = createImageView(m_fallbackShadowTexture, SHADOW_DEPTH_FORMAT,
|
||||
vk::ImageAspectFlagBits::eDepth, 1, ImageViewType::Array2D);
|
||||
if (!m_fallbackShadowView) {
|
||||
nprintf(("vulkan", "Failed to create fallback shadow map view!\n"));
|
||||
m_device.destroyImage(m_fallbackShadowTexture);
|
||||
m_fallbackShadowTexture = nullptr;
|
||||
m_memoryManager->freeAllocation(m_fallbackShadowAllocation);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Clear to the far plane so a shader that does sample this reads "nothing occluding".
|
||||
// The clear also gets the image out of eUndefined, which the descriptor's declared
|
||||
// eShaderReadOnlyOptimal layout requires.
|
||||
transitionImageLayout(m_fallbackShadowTexture, SHADOW_DEPTH_FORMAT,
|
||||
vk::ImageLayout::eUndefined, vk::ImageLayout::eTransferDstOptimal);
|
||||
|
||||
{
|
||||
vk::ImageSubresourceRange range;
|
||||
range.aspectMask = vk::ImageAspectFlagBits::eDepth;
|
||||
range.baseMipLevel = 0;
|
||||
range.levelCount = 1;
|
||||
range.baseArrayLayer = 0;
|
||||
range.layerCount = 1;
|
||||
|
||||
vk::CommandBuffer cmd = beginSingleTimeCommands();
|
||||
cmd.clearDepthStencilImage(m_fallbackShadowTexture, vk::ImageLayout::eTransferDstOptimal,
|
||||
vk::ClearDepthStencilValue(1.0f, 0), range);
|
||||
endSingleTimeCommands(cmd);
|
||||
}
|
||||
|
||||
transitionImageLayout(m_fallbackShadowTexture, SHADOW_DEPTH_FORMAT,
|
||||
vk::ImageLayout::eTransferDstOptimal, vk::ImageLayout::eShaderReadOnlyOptimal);
|
||||
|
||||
// The renderer's own compare sampler belongs to VulkanShadowMap, which never
|
||||
// initializes while shadows are off. So this fallback needs its own.
|
||||
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_TRUE;
|
||||
samplerInfo.compareOp = vk::CompareOp::eLessOrEqual;
|
||||
samplerInfo.borderColor = vk::BorderColor::eFloatOpaqueWhite;
|
||||
|
||||
try {
|
||||
m_shadowCompareSampler = m_device.createSampler(samplerInfo);
|
||||
} catch (const vk::SystemError& e) {
|
||||
nprintf(("vulkan", "Failed to create fallback shadow compare sampler: %s\n", e.what()));
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool VulkanTextureManager::createFallbackTexture(vk::Image& outImage, VulkanAllocation& outAlloc,
|
||||
vk::ImageView& outView, ImageViewType viewType,
|
||||
uint32_t arrayLayers, bool cubemap,
|
||||
|
||||
@@ -187,6 +187,14 @@ public:
|
||||
*/
|
||||
vk::DescriptorImageInfo getFallbackTextureInfo3D();
|
||||
|
||||
/**
|
||||
* @brief Get a ready-to-use DescriptorImageInfo for the shadow map fallback
|
||||
*
|
||||
* A depth-format 2D array view plus a compare-enabled sampler, for the
|
||||
* sampler2DArrayShadow binding when no real shadow map is bound.
|
||||
*/
|
||||
vk::DescriptorImageInfo getFallbackShadowMapInfo();
|
||||
|
||||
// Texture access
|
||||
|
||||
/**
|
||||
@@ -267,6 +275,14 @@ private:
|
||||
/**
|
||||
* @brief Create a 1x1 white fallback texture (image + view + upload)
|
||||
*/
|
||||
/**
|
||||
* @brief Create the 1x1 depth fallback for shadow sampler bindings (image + view + clear)
|
||||
*
|
||||
* Cleared to depth 1.0 (the far plane), so a shader that does sample it reads
|
||||
* "nothing occluding" rather than undefined memory.
|
||||
*/
|
||||
bool createFallbackShadowTexture();
|
||||
|
||||
bool createFallbackTexture(vk::Image& outImage, VulkanAllocation& outAlloc,
|
||||
vk::ImageView& outView, ImageViewType viewType,
|
||||
uint32_t arrayLayers = 1, bool cubemap = false,
|
||||
@@ -413,6 +429,13 @@ private:
|
||||
VulkanAllocation m_fallbackCubeAllocation;
|
||||
|
||||
// Fallback 1x1x1 white 3D texture for unbound sampler3D slots
|
||||
// Depth fallback for the sampler2DArrayShadow binding. Sampled through
|
||||
// m_shadowCompareSampler, not m_defaultSampler.
|
||||
vk::Image m_fallbackShadowTexture;
|
||||
vk::ImageView m_fallbackShadowView; // 2D_ARRAY depth view (for sampler2DArrayShadow)
|
||||
VulkanAllocation m_fallbackShadowAllocation;
|
||||
vk::Sampler m_shadowCompareSampler;
|
||||
|
||||
vk::Image m_fallback3DTexture;
|
||||
vk::ImageView m_fallback3DView; // 3D view (for sampler3D)
|
||||
VulkanAllocation m_fallback3DAllocation;
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
#include "VulkanTexture.h"
|
||||
#include "VulkanShader.h"
|
||||
#include "VulkanDescriptorManager.h"
|
||||
#include "VulkanDeletionQueue.h"
|
||||
#include "VulkanPipeline.h"
|
||||
#include "VulkanQuery.h"
|
||||
#include "VulkanState.h"
|
||||
@@ -42,7 +43,7 @@ std::unique_ptr<VulkanRenderer> renderer_instance;
|
||||
|
||||
// Sync object for tracking frame completion
|
||||
struct VulkanSyncObject {
|
||||
uint64_t frameNumber;
|
||||
FrameSyncPoint point;
|
||||
};
|
||||
|
||||
// ========== Renderer-level functions ==========
|
||||
@@ -53,6 +54,22 @@ void vulkan_setup_frame()
|
||||
renderer->setupFrame();
|
||||
}
|
||||
|
||||
void vulkan_viewport_size_changed()
|
||||
{
|
||||
auto* renderer = getRendererInstance();
|
||||
if (renderer != nullptr) {
|
||||
renderer->syncToSurfaceExtent();
|
||||
}
|
||||
}
|
||||
|
||||
void vulkan_release_viewport(os::Viewport* view)
|
||||
{
|
||||
auto* renderer = getRendererInstance();
|
||||
if (renderer != nullptr && view != nullptr) {
|
||||
renderer->releaseViewport(view);
|
||||
}
|
||||
}
|
||||
|
||||
void vulkan_flip()
|
||||
{
|
||||
renderer_instance->flip();
|
||||
@@ -75,32 +92,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 +153,10 @@ 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;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
@@ -185,6 +180,38 @@ bool vulkan_get_property(gr_property prop, void* dest)
|
||||
}
|
||||
}
|
||||
|
||||
void vulkan_get_debug_stats(gr_debug_stats& stats)
|
||||
{
|
||||
const auto& frameStats = getDrawManager()->getFrameStats();
|
||||
|
||||
stats.draw_stats_valid = true;
|
||||
stats.draw_calls = frameStats.drawCalls;
|
||||
stats.draw_indexed_calls = frameStats.drawIndexedCalls;
|
||||
stats.total_vertices = frameStats.totalVertices;
|
||||
stats.total_indices = frameStats.totalIndices;
|
||||
stats.apply_material_calls = frameStats.applyMaterialCalls;
|
||||
stats.apply_material_failures = frameStats.applyMaterialFailures;
|
||||
stats.no_pipeline_skips = frameStats.noPipelineSkips;
|
||||
stats.on_demand_texture_uploads = frameStats.onDemandTextureUploads;
|
||||
|
||||
stats.descriptor_sets_allocated = static_cast<int>(getDescriptorManager()->getSetsAllocatedThisFrame());
|
||||
stats.descriptor_writes = static_cast<int>(getDescriptorManager()->getWritesThisFrame());
|
||||
stats.pipeline_count = getPipelineManager()->getPipelineCount();
|
||||
}
|
||||
|
||||
void vulkan_get_memory_stats(gr_memory_stats& stats)
|
||||
{
|
||||
auto* memoryManager = getMemoryManager();
|
||||
if (memoryManager == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
stats.gpu_purpose_valid = true;
|
||||
stats.gpu_texture_bytes = memoryManager->getTextureBytes();
|
||||
stats.gpu_geometry_bytes = memoryManager->getGeometryBytes();
|
||||
stats.gpu_render_target_bytes = memoryManager->getRenderTargetBytes();
|
||||
}
|
||||
|
||||
void vulkan_push_debug_group(const char* name)
|
||||
{
|
||||
auto* renderer = getRendererInstance();
|
||||
@@ -222,12 +249,16 @@ void vulkan_imgui_render_draw_data()
|
||||
if (renderer) {
|
||||
ImGui_ImplVulkan_RenderDrawData(ImGui::GetDrawData(), renderer->getVkCurrentCommandBuffer());
|
||||
|
||||
// ImGui recorded its own pipeline/descriptor/viewport/scissor binds
|
||||
// directly on the command buffer, mid-pass. Anything the engine draws
|
||||
// before the next pass boundary (e.g. gr_flip's debug overlay or cached
|
||||
// UI model instances) would otherwise run with ImGui's pipeline still
|
||||
// bound because the tracker believes its own pipeline is current.
|
||||
// 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
|
||||
// its own pass here, unlike the other raw recorders whose staleness setRenderPass()
|
||||
// alone recovers from. Without this, the next tracked draw (e.g. a HUD gauge rendered
|
||||
// after the profiler overlay) can skip rebinding its own pipeline/descriptor set because
|
||||
// the tracker's/draw-manager's cached handles still match what THEY last bound, even
|
||||
// though ImGui has since changed what's actually bound on the command buffer -- see
|
||||
// VulkanStateTracker::invalidateExternalBindings().
|
||||
getStateTracker()->invalidateExternalBindings();
|
||||
getDrawManager()->invalidateDrawStateCaches();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -235,7 +266,7 @@ gr_sync vulkan_sync_fence()
|
||||
{
|
||||
auto* renderer = getRendererInstance();
|
||||
auto* sync = new VulkanSyncObject();
|
||||
sync->frameNumber = renderer->getCurrentFrameNumber();
|
||||
sync->point = renderer->captureSyncPoint();
|
||||
return static_cast<gr_sync>(sync);
|
||||
}
|
||||
|
||||
@@ -252,7 +283,7 @@ bool vulkan_sync_wait(gr_sync sync, uint64_t timeoutns)
|
||||
// timeout or when the fence was taken during the still-recording frame --
|
||||
// callers (e.g. UniformBufferManager's segment fences) depend on this to
|
||||
// know whether the GPU is done with a resource.
|
||||
return renderer->waitForFrame(syncObj->frameNumber, timeoutns);
|
||||
return renderer->waitForSyncPoint(syncObj->point, timeoutns);
|
||||
}
|
||||
|
||||
void vulkan_sync_delete(gr_sync sync)
|
||||
@@ -349,6 +380,71 @@ void vulkan_print_screen(const char* filename)
|
||||
vm_free(pixels);
|
||||
}
|
||||
|
||||
void vulkan_end_offscreen_frame()
|
||||
{
|
||||
// Everything frame-scoped that setupFrame()/flip() would recycle. An off-screen renderer never
|
||||
// reaches either, so without this the descriptor pool chain grows a chunk every few frames, the
|
||||
// deletion queue's retirement clock never ticks, and the bump allocator climbs until it doubles
|
||||
// -- and a mid-frame doubling used to hand draws stale uniforms (the qtFRED briefing icon
|
||||
// flicker). Minutes with the briefing editor open reached multiple GB.
|
||||
//
|
||||
// Safe only because gr_end_offscreen_frame()'s contract is that the frame's GPU work has
|
||||
// already completed: the readback that produced the image host-waits on a fence, and queue
|
||||
// submissions execute in order, so every submission up to that point has retired.
|
||||
if (auto* renderer = getRendererInstance()) {
|
||||
// Advances the sync frame counter and rewinds the bump allocator. The counter matters as
|
||||
// much as the memory: sync objects are stamped with it, and UniformBufferManager's segment
|
||||
// fences can only ever resolve if it moves.
|
||||
renderer->endOffscreenFrame();
|
||||
}
|
||||
|
||||
if (auto* descriptorManager = getDescriptorManager()) {
|
||||
descriptorManager->beginFrame();
|
||||
}
|
||||
|
||||
if (auto* deletionQueue = getDeletionQueue()) {
|
||||
deletionQueue->processDestructions();
|
||||
}
|
||||
}
|
||||
|
||||
bool vulkan_read_render_target(ubyte* out_rgba, int width, int height)
|
||||
{
|
||||
auto* texManager = getTextureManager();
|
||||
const int rtHandle = texManager ? texManager->getCurrentRenderTarget() : -1;
|
||||
if (rtHandle < 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
auto* ts = texManager->getTextureSlot(rtHandle);
|
||||
if (ts == nullptr) {
|
||||
return false;
|
||||
}
|
||||
|
||||
ubyte* pixels = nullptr;
|
||||
uint32_t w = 0;
|
||||
uint32_t h = 0;
|
||||
if (!renderer_instance->readbackRenderTarget(ts, &pixels, &w, &h)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// The caller sized its buffer from the bitmap it bound, so a disagreement means it is reading
|
||||
// something other than what it thinks. Refuse rather than overrun.
|
||||
const bool sizeMatches = w == static_cast<uint32_t>(width) && h == static_cast<uint32_t>(height);
|
||||
if (!sizeMatches) {
|
||||
nprintf(("vulkan", "vulkan_read_render_target: caller expected %dx%d but the bound target is "
|
||||
"%ux%u\n", width, height, w, h));
|
||||
} else {
|
||||
// R8G8B8A8_UNORM, already RGBA order with real alpha, and row 0 is the top row -- which is
|
||||
// the top-down order gr_read_render_target() promises. Deliberately not the flip
|
||||
// vulkan_blob_screen() applies below: that one exists only to make its PNG match OpenGL's.
|
||||
memcpy(out_rgba, pixels, static_cast<size_t>(w) * h * 4);
|
||||
}
|
||||
|
||||
vm_free(pixels);
|
||||
|
||||
return sizeMatches;
|
||||
}
|
||||
|
||||
SCP_string vulkan_blob_screen()
|
||||
{
|
||||
ubyte* pixels = nullptr;
|
||||
@@ -409,7 +505,26 @@ std::unique_ptr<os::Viewport> stub_create_viewport(const os::ViewPortProperties&
|
||||
{
|
||||
return {};
|
||||
}
|
||||
void stub_use_viewport(os::Viewport* /*view*/) {}
|
||||
void vulkan_use_viewport(os::Viewport* view)
|
||||
{
|
||||
auto* renderer = getRendererInstance();
|
||||
if (renderer == nullptr || view == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!renderer->useViewport(view)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Match gr_opengl_use_viewport(): the engine's idea of the screen follows whichever surface is
|
||||
// being drawn to now. The swap chain extent is used rather than the viewport's own getSize(),
|
||||
// because that reports logical pixels and the surface was sized in device pixels -- scaling one
|
||||
// into the other by hand is what leaves gr_screen disagreeing with what is being presented.
|
||||
const auto extent = renderer->getSwapChainExtent();
|
||||
if (extent.width > 0 && extent.height > 0) {
|
||||
gr_screen_resize(static_cast<int>(extent.width), static_cast<int>(extent.height));
|
||||
}
|
||||
}
|
||||
|
||||
// ========== Function pointer table ==========
|
||||
// Implementations are defined in their respective files:
|
||||
@@ -419,6 +534,7 @@ void init_function_pointers()
|
||||
{
|
||||
// function pointers...
|
||||
gr_screen.gf_setup_frame = vulkan_setup_frame;
|
||||
gr_screen.gf_viewport_size_changed = vulkan_viewport_size_changed;
|
||||
gr_screen.gf_set_clip = vulkan_set_clip;
|
||||
gr_screen.gf_reset_clip = vulkan_reset_clip;
|
||||
|
||||
@@ -426,6 +542,8 @@ void init_function_pointers()
|
||||
|
||||
gr_screen.gf_print_screen = vulkan_print_screen;
|
||||
gr_screen.gf_blob_screen = vulkan_blob_screen;
|
||||
gr_screen.gf_read_render_target = vulkan_read_render_target;
|
||||
gr_screen.gf_end_offscreen_frame = vulkan_end_offscreen_frame;
|
||||
|
||||
gr_screen.gf_zbuffer_get = vulkan_zbuffer_get;
|
||||
gr_screen.gf_zbuffer_set = vulkan_zbuffer_set;
|
||||
@@ -532,6 +650,8 @@ void init_function_pointers()
|
||||
|
||||
gr_screen.gf_is_capable = vulkan_is_capable;
|
||||
gr_screen.gf_get_property = vulkan_get_property;
|
||||
gr_screen.gf_get_debug_stats = vulkan_get_debug_stats;
|
||||
gr_screen.gf_get_memory_stats = vulkan_get_memory_stats;
|
||||
|
||||
gr_screen.gf_push_debug_group = vulkan_push_debug_group;
|
||||
gr_screen.gf_pop_debug_group = vulkan_pop_debug_group;
|
||||
@@ -543,7 +663,8 @@ void init_function_pointers()
|
||||
gr_screen.gf_delete_query_object = vulkan_delete_query_object;
|
||||
|
||||
gr_screen.gf_create_viewport = stub_create_viewport;
|
||||
gr_screen.gf_use_viewport = stub_use_viewport;
|
||||
gr_screen.gf_use_viewport = vulkan_use_viewport;
|
||||
gr_screen.gf_release_viewport = vulkan_release_viewport;
|
||||
|
||||
gr_screen.gf_bind_uniform_buffer = vulkan_bind_uniform_buffer;
|
||||
|
||||
@@ -563,6 +684,35 @@ void init_function_pointers()
|
||||
|
||||
} // anonymous namespace
|
||||
|
||||
// Outside the anonymous namespace: declared in gr_vulkan.h so the RTAO fallback
|
||||
// trigger in vulkan_deferred_lighting_finish() (VulkanDeferred.cpp) can call it
|
||||
// when shadow rendering didn't build this frame's TLAS.
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
void initialize_function_pointers() {
|
||||
init_function_pointers();
|
||||
}
|
||||
|
||||
@@ -11,6 +11,13 @@ bool initialize(std::unique_ptr<os::GraphicsOperations>&& graphicsOps);
|
||||
|
||||
VulkanRenderer* getRendererInstance();
|
||||
|
||||
// Build (or refresh) this frame's raytraced-shadow/RTAO TLAS. Safe to call more
|
||||
// than once per frame (VulkanRaytracingManager::buildTlas() is frame-guarded),
|
||||
// but acceleration-structure builds must be recorded outside a render pass, so
|
||||
// this ends the state tracker's current render pass (and clears it in the
|
||||
// tracker) if one is active.
|
||||
void vulkan_build_shadow_tlas();
|
||||
|
||||
void cleanup();
|
||||
|
||||
} // namespace graphics::vulkan
|
||||
@@ -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("RT 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("RT 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 (deg)", &sun_size, 0.0f, 10.0f)) {
|
||||
LabRenderer::setSunAngularSizeOverride(sun_size);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void LabUi::build_rtao_sliders()
|
||||
{
|
||||
// Unlike the RT shadow sliders, RTAO is independent of the shadow method --
|
||||
// it only needs ray-query support.
|
||||
if (!rtao_supported()) {
|
||||
return;
|
||||
}
|
||||
|
||||
int samples = Rtao_samples;
|
||||
if (SliderInt("RTAO Samples", &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("RTAO Radius", &radius, 0.0f, 200.0f)) {
|
||||
lighting_profiles::lab_set_rtao_radius(radius);
|
||||
}
|
||||
|
||||
float strength = lighting_profiles::current_rtao_strength();
|
||||
if (SliderFloat("RTAO 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 ||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,47 @@ 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.
|
||||
struct LabSavedSettings {
|
||||
RTShadowQuality rt_shadow_quality;
|
||||
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 = {Rt_shadow_quality,
|
||||
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) {
|
||||
Rt_shadow_quality = Lab_saved_settings.rt_shadow_quality;
|
||||
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();
|
||||
}
|
||||
|
||||
@@ -412,12 +412,11 @@ void LabManager::onFrame(float frametime) {
|
||||
|
||||
if (Cmdline_show_imgui_debug)
|
||||
ImGui::ShowDemoWindow();
|
||||
ImGui::Render();
|
||||
gr_imgui_render_draw_data();
|
||||
|
||||
if (CloseThis)
|
||||
close();
|
||||
|
||||
// gr_flip() renders and submits the ImGui frame opened above.
|
||||
gr_flip();
|
||||
}
|
||||
|
||||
|
||||
@@ -256,8 +256,9 @@ void LabRenderer::renderModel(float frametime) {
|
||||
|
||||
gr_reset_clip();
|
||||
gr_set_color_fast(&HUD_color_debug);
|
||||
if (Cmdline_frame_profile) {
|
||||
tracing::frame_profile_process_frame();
|
||||
// The legacy -profile_frame_time text dump. gr_flip() drains the profiler, so this only
|
||||
// renders what the previous frame produced.
|
||||
if (Cmdline_frame_profile && tracing::frame_profiling_active()) {
|
||||
gr_string(gr_screen.center_offset_x + 20, gr_screen.center_offset_y + 100 + gr_get_font_height() + 1,
|
||||
tracing::get_frame_profile_output().c_str(), GR_RESIZE_NONE);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -65,7 +65,7 @@ bool *Lcl_unexpected_tstring_check = nullptr;
|
||||
// NOTE: with map storage of XSTR strings, the indexes no longer need to be contiguous,
|
||||
// but internal strings should still increment XSTR_SIZE to avoid collisions.
|
||||
// retail XSTR_SIZE = 1570
|
||||
// #define XSTR_SIZE 1940 // This is the next available ID
|
||||
// #define XSTR_SIZE 1942 // This is the next available ID
|
||||
|
||||
// struct to allow for strings.tbl-determined x offset
|
||||
// offset is 0 for english, by default
|
||||
|
||||
@@ -6278,6 +6278,17 @@ 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 27.0 simply don't have it
|
||||
if (optional_string("+AngularSize:")) {
|
||||
stuff_float(&sle.angular_size);
|
||||
if (sle.angular_size < 0.0f) {
|
||||
error_display(0, "+AngularSize: for sun '%s' must be >= 0 (got %f); ignoring it.", sle.filename, sle.angular_size);
|
||||
sle.angular_size = SUN_ANGULAR_SIZE_UNSPECIFIED;
|
||||
}
|
||||
}
|
||||
|
||||
// add it
|
||||
background->suns.push_back(sle);
|
||||
}
|
||||
@@ -9791,3 +9802,19 @@ bool check_for_25_1_data()
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool check_for_27_0_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;
|
||||
}
|
||||
@@ -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_27_0_data();
|
||||
|
||||
#define WING_PLAYER_BASE 0x80000 // used by Fred to tell ship_index in a wing points to a player
|
||||
|
||||
|
||||
@@ -1035,6 +1035,15 @@ 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_27_0_data()
|
||||
FRED_ENSURE_PROPERTY_VERSION_WITH_DEFAULT_F("+AngularSize:",
|
||||
1,
|
||||
";;FSO 27.0.0;;",
|
||||
SUN_ANGULAR_SIZE_UNSPECIFIED,
|
||||
" %f",
|
||||
sle->angular_size);
|
||||
}
|
||||
|
||||
// save background bitmaps by filename
|
||||
@@ -3171,7 +3180,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_27_0 = gameversion::version(27, 0);
|
||||
if (MISSION_VERSION >= version_27_0) {
|
||||
Warning(LOCATION,
|
||||
"Notify an SCP coder: now that the required mission version is at least 27.0, the check_for_27_0_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_27_0_data()) {
|
||||
The_mission.required_fso_version = version_27_0;
|
||||
} 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");
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
#include "popup/popup.h"
|
||||
#include "sound/audiostr.h"
|
||||
#include "ui/ui.h"
|
||||
#include "weapon/weapon.h"
|
||||
#include "weapon/weapon.h"
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -1056,6 +1056,19 @@ void model_load_texture(polymodel *pm, int i, const char *file);
|
||||
// Returns a pointer to the polymodel structure for model 'n'
|
||||
polymodel *model_get(int model_num);
|
||||
|
||||
/**
|
||||
* @brief Memory usage summary across all currently loaded polygon models, for the profiler
|
||||
* overlay's memory panel
|
||||
*/
|
||||
struct model_memory_stats {
|
||||
bool valid = false;
|
||||
int model_count = 0;
|
||||
size_t vertex_bytes = 0; // sum of vert_source.Vertex_list_size across loaded models
|
||||
size_t index_bytes = 0; // sum of vert_source.Index_list_size across loaded models
|
||||
size_t bsp_data_bytes = 0; // sum of submodel[].bsp_data_size (collision/interp tree data)
|
||||
};
|
||||
model_memory_stats model_get_memory_stats();
|
||||
|
||||
int num_model_instances();
|
||||
polymodel_instance* model_get_instance(int model_instance_num);
|
||||
|
||||
|
||||
@@ -411,6 +411,28 @@ void model_page_in_stop()
|
||||
}
|
||||
}
|
||||
|
||||
model_memory_stats model_get_memory_stats()
|
||||
{
|
||||
model_memory_stats stats;
|
||||
stats.valid = true;
|
||||
|
||||
for (const auto pm : Polygon_models) {
|
||||
if (pm == nullptr) {
|
||||
continue;
|
||||
}
|
||||
|
||||
stats.model_count++;
|
||||
stats.vertex_bytes += pm->vert_source.Vertex_list_size;
|
||||
stats.index_bytes += pm->vert_source.Index_list_size;
|
||||
|
||||
for (int j = 0; j < pm->n_models; j++) {
|
||||
stats.bsp_data_bytes += pm->submodel[j].bsp_data_size;
|
||||
}
|
||||
}
|
||||
|
||||
return stats;
|
||||
}
|
||||
|
||||
void model_init()
|
||||
{
|
||||
int i;
|
||||
|
||||
@@ -1116,7 +1116,7 @@ void nebl_generate_section(bolt_type *bi, float width, l_node *a, l_node *b, l_s
|
||||
}
|
||||
}
|
||||
// calculate the glow points
|
||||
nebl_calc_facing_pts_smart(&glow_a, &glow_b, &dir_normal, &a->pos, pinch_a ? 0.5f : width * 6.0f, bi->b_add);
|
||||
nebl_calc_facing_pts_smart(&glow_a, &glow_b, &dir_normal, &a->pos, pinch_a ? 0.5f : width * 6.0f, Nebl_type->b_add);
|
||||
g3_transfer_vertex(&c->glow_vex[0], &glow_a);
|
||||
g3_transfer_vertex(&c->glow_vex[1], &glow_b);
|
||||
|
||||
|
||||
@@ -535,6 +535,19 @@ int obj_allocate(bool essential)
|
||||
return objnum;
|
||||
}
|
||||
|
||||
object_memory_stats obj_get_memory_stats()
|
||||
{
|
||||
object_memory_stats stats;
|
||||
|
||||
stats.objects_used = Num_objects;
|
||||
stats.objects_peak = num_objects_hwm;
|
||||
|
||||
stats.ships_used = ship_get_num_ships();
|
||||
stats.weapons_used = Num_weapons;
|
||||
|
||||
return stats;
|
||||
}
|
||||
|
||||
/**
|
||||
* Frees up an object
|
||||
*
|
||||
|
||||
@@ -432,5 +432,23 @@ void physics_apply_pstate_to_object(object* objp, const physics_snapshot& source
|
||||
*/
|
||||
int obj_raw_pof_create(const char* pof_filename, const matrix* orient, const vec3d* pos);
|
||||
|
||||
/**
|
||||
* @brief Occupancy snapshot of the object/ship/weapon pools, for the profiler overlay's memory panel
|
||||
*
|
||||
* @details These pools are fixed-size static arrays, so their footprint is a compile-time
|
||||
* constant; what's actually useful to report is how full they are.
|
||||
*/
|
||||
struct object_memory_stats {
|
||||
int objects_used = 0;
|
||||
int objects_max = MAX_OBJECTS;
|
||||
int objects_peak = 0;
|
||||
|
||||
int ships_used = 0;
|
||||
int ships_max = MAX_SHIPS;
|
||||
|
||||
int weapons_used = 0;
|
||||
int weapons_max = MAX_WEAPONS;
|
||||
};
|
||||
object_memory_stats obj_get_memory_stats();
|
||||
|
||||
#endif
|
||||
@@ -176,12 +176,11 @@ void OptConfigurator::onFrame() {
|
||||
|
||||
if (Cmdline_show_imgui_debug)
|
||||
ImGui::ShowDemoWindow();
|
||||
ImGui::Render();
|
||||
gr_imgui_render_draw_data();
|
||||
|
||||
if (CloseThis) {
|
||||
close();
|
||||
}
|
||||
|
||||
// gr_flip() renders and submits the ImGui frame opened above.
|
||||
gr_flip();
|
||||
}
|
||||
+48
-2
@@ -15,6 +15,7 @@
|
||||
#include "graphics/2d.h"
|
||||
#include "graphics/openxr.h"
|
||||
#include "io/joy_ff.h"
|
||||
#include "tracing/tracing.h"
|
||||
|
||||
#include <fcntl.h>
|
||||
#include <utf8.h>
|
||||
@@ -38,6 +39,12 @@
|
||||
#include "utils/finally.h"
|
||||
#endif
|
||||
|
||||
#if defined(__APPLE__)
|
||||
#include <mach/mach.h>
|
||||
#elif defined(__linux__)
|
||||
#include <unistd.h>
|
||||
#endif
|
||||
|
||||
namespace
|
||||
{
|
||||
const char* ORGANIZATION_NAME = "HardLightProductions";
|
||||
@@ -254,6 +261,7 @@ namespace
|
||||
// Windows specific includes
|
||||
#define WIN32_LEAN_AND_MEAN
|
||||
#include <windows.h>
|
||||
#include <psapi.h>
|
||||
#include <backends/imgui_impl_sdl3.h>
|
||||
|
||||
// go through all windows and try and find the one that matches the search string
|
||||
@@ -833,18 +841,23 @@ static void handle_sdl_event(const SDL_Event& event) {
|
||||
using namespace os::events;
|
||||
|
||||
bool imgui_processed_this = false;
|
||||
if ((gameseq_get_state() == GS_STATE_LAB) || (gameseq_get_state() == GS_STATE_INGAME_OPTIONS)) {
|
||||
// The profiler overlay is drawn from gr_flip(), so it can be on screen in any state — it
|
||||
// needs input forwarded wherever it is active, not in a fixed list of states.
|
||||
const bool imgui_state = (gameseq_get_state() == GS_STATE_LAB) || (gameseq_get_state() == GS_STATE_INGAME_OPTIONS);
|
||||
if (imgui_state || tracing::frame_profiling_active()) {
|
||||
//In these states, we always need to forward inputs to ImGUI, and depending on the ImGUI state and the input type, we must consume it here instead of passing it to FSO.
|
||||
const SDL_Event imgui_event = scale_imgui_mouse_event(event);
|
||||
ImGui_ImplSDL3_ProcessEvent(&imgui_event);
|
||||
|
||||
// In other states, the game also needs mouse motion while the pointer is over the overlay.
|
||||
// If the overlay consumed the motion, mouse flight would stop there.
|
||||
imgui_processed_this = (ImGui::GetIO().WantCaptureKeyboard &&
|
||||
(event.type == SDL_EventType::SDL_EVENT_KEY_UP ||
|
||||
event.type == SDL_EventType::SDL_EVENT_KEY_DOWN)) ||
|
||||
(ImGui::GetIO().WantCaptureMouse &&
|
||||
(event.type == SDL_EventType::SDL_EVENT_MOUSE_BUTTON_UP ||
|
||||
event.type == SDL_EventType::SDL_EVENT_MOUSE_BUTTON_DOWN||
|
||||
event.type == SDL_EventType::SDL_EVENT_MOUSE_MOTION));
|
||||
(imgui_state && event.type == SDL_EventType::SDL_EVENT_MOUSE_MOTION)));
|
||||
}
|
||||
|
||||
if (!imgui_processed_this) {
|
||||
@@ -1105,3 +1118,36 @@ void os_android_touch_overlay_init()
|
||||
}
|
||||
#endif
|
||||
|
||||
os_memory_stats os_get_memory_stats()
|
||||
{
|
||||
os_memory_stats stats;
|
||||
|
||||
#if defined(WIN32)
|
||||
PROCESS_MEMORY_COUNTERS counters;
|
||||
if (GetProcessMemoryInfo(GetCurrentProcess(), &counters, sizeof(counters))) {
|
||||
stats.valid = true;
|
||||
stats.resident_bytes = counters.WorkingSetSize;
|
||||
}
|
||||
#elif defined(__APPLE__)
|
||||
mach_task_basic_info_data_t info;
|
||||
mach_msg_type_number_t count = MACH_TASK_BASIC_INFO_COUNT;
|
||||
if (task_info(mach_task_self(), MACH_TASK_BASIC_INFO, reinterpret_cast<task_info_t>(&info), &count) ==
|
||||
KERN_SUCCESS) {
|
||||
stats.valid = true;
|
||||
stats.resident_bytes = info.resident_size;
|
||||
}
|
||||
#elif defined(__linux__)
|
||||
// Field 2 of /proc/self/statm is the resident set size, in pages -- see proc(5).
|
||||
if (FILE* statm = fopen("/proc/self/statm", "r")) {
|
||||
long resident_pages = 0;
|
||||
if (fscanf(statm, "%*d %ld", &resident_pages) == 1) {
|
||||
stats.valid = true;
|
||||
stats.resident_bytes = static_cast<size_t>(resident_pages) * static_cast<size_t>(sysconf(_SC_PAGESIZE));
|
||||
}
|
||||
fclose(statm);
|
||||
}
|
||||
#endif
|
||||
|
||||
return stats;
|
||||
}
|
||||
|
||||
@@ -107,6 +107,20 @@ void os_android_touch_overlay_toggle(bool status);
|
||||
void os_android_set_flags_string(const char* json);
|
||||
#endif
|
||||
|
||||
/**
|
||||
* @brief Process-wide memory usage snapshot, for the profiler overlay's memory panel
|
||||
*
|
||||
* @details Reports the resident set size (RSS) of the FSO process: the physical RAM the operating
|
||||
* system currently has mapped for this process, across every subsystem (engine heap, Lua heap,
|
||||
* OpenAL buffers, driver-side graphics allocations). Not available on every platform, hence the
|
||||
* "valid" flag -- see os_get_memory_stats().
|
||||
*/
|
||||
struct os_memory_stats {
|
||||
bool valid = false;
|
||||
size_t resident_bytes = 0;
|
||||
};
|
||||
os_memory_stats os_get_memory_stats();
|
||||
|
||||
namespace os
|
||||
{
|
||||
/**
|
||||
@@ -115,6 +129,11 @@ namespace os
|
||||
* @ingroup osapi
|
||||
*/
|
||||
|
||||
// Declared in osapi/vulkan_surface.h, which is only included by the few files that actually
|
||||
// deal with Vulkan -- osapi.h is included nearly everywhere and has no business pulling in the
|
||||
// Vulkan headers.
|
||||
class VulkanSurfaceProvider;
|
||||
|
||||
/**
|
||||
* @brief Flags for OpenGL context creation
|
||||
* @ingroup os_graphics_api
|
||||
@@ -348,6 +367,19 @@ namespace os
|
||||
* @return The created viewport, may be @c nullptr if the viewport can't be created
|
||||
*/
|
||||
virtual std::unique_ptr<Viewport> createViewport(const ViewPortProperties& props) = 0;
|
||||
|
||||
/**
|
||||
* @brief Gets the Vulkan support of this implementation
|
||||
*
|
||||
* Vulkan needs more from the windowing system than an OpenGL context does (loader, instance
|
||||
* extensions, surface creation), so it gets its own interface. Implementations that can't
|
||||
* present through Vulkan return @c nullptr here, which makes @ref gr_init fall back to
|
||||
* OpenGL instead of failing.
|
||||
*
|
||||
* @return The Vulkan support interface, or @c nullptr if this implementation has none. The
|
||||
* returned pointer is owned by the graphics operations and stays valid for their lifetime.
|
||||
*/
|
||||
virtual VulkanSurfaceProvider* getVulkanSupport() { return nullptr; }
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
#pragma once
|
||||
|
||||
#include "globalincs/pstypes.h"
|
||||
|
||||
#include <type_traits>
|
||||
|
||||
namespace os {
|
||||
|
||||
class Viewport;
|
||||
|
||||
/**
|
||||
* @brief The windowing-system half of Vulkan initialization
|
||||
* @ingroup os_graphics_api
|
||||
*
|
||||
* Three things the Vulkan renderer cannot work out for itself, because all three depend on which
|
||||
* windowing toolkit created the window: where the Vulkan loader is, which instance extensions that
|
||||
* toolkit's surfaces need, and how to turn one of its windows into a VkSurfaceKHR.
|
||||
*
|
||||
* Handles are passed as @c void* (VkInstance, always a pointer) and @c uint64_t (VkSurfaceKHR, a
|
||||
* pointer on 64-bit targets but a plain integer on 32-bit ones) so this header stays free of the
|
||||
* Vulkan headers -- it has to compile in the @c FSO_BUILD_WITH_VULKAN=OFF configuration too. Use
|
||||
* vulkan_handle_cast() / vulkan_handle_value() to convert at the ends.
|
||||
*/
|
||||
class VulkanSurfaceProvider {
|
||||
public:
|
||||
virtual ~VulkanSurfaceProvider() = default;
|
||||
|
||||
/**
|
||||
* @brief Loads the Vulkan loader and returns @c vkGetInstanceProcAddr
|
||||
*
|
||||
* @return The function pointer, or @c nullptr if the loader is unavailable
|
||||
*/
|
||||
virtual void* getVulkanProcAddr() = 0;
|
||||
|
||||
/**
|
||||
* @brief The instance extensions this windowing system's surfaces require
|
||||
*
|
||||
* These are merged into the extension list the renderer builds; the renderer still adds its own
|
||||
* (debug utils, swap chain color space, ...) on top.
|
||||
*
|
||||
* @param[out] extensions Receives the extension names
|
||||
* @return @c true on success
|
||||
*/
|
||||
virtual bool getVulkanInstanceExtensions(SCP_vector<SCP_string>& extensions) = 0;
|
||||
|
||||
/**
|
||||
* @brief Creates a Vulkan surface for a viewport
|
||||
*
|
||||
* @param view The viewport to create the surface for
|
||||
* @param vkInstance The @c VkInstance the surface belongs to
|
||||
* @return The @c VkSurfaceKHR handle, or 0 on failure
|
||||
*/
|
||||
virtual uint64_t createVulkanSurface(Viewport* view, void* vkInstance) = 0;
|
||||
|
||||
/**
|
||||
* @brief Destroys a surface previously returned by createVulkanSurface()
|
||||
*
|
||||
* @note The renderer must always go through this rather than calling @c vkDestroySurfaceKHR
|
||||
* itself: an implementation may not own the surface it handed out.
|
||||
*/
|
||||
virtual void destroyVulkanSurface(void* vkInstance, uint64_t surface) = 0;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Converts a surface handle from its transport type back to the Vulkan handle type
|
||||
* @ingroup os_graphics_api
|
||||
*/
|
||||
template <typename HandleType>
|
||||
inline HandleType vulkan_handle_cast(uint64_t handle)
|
||||
{
|
||||
if constexpr (std::is_pointer<HandleType>::value) {
|
||||
return reinterpret_cast<HandleType>(static_cast<uintptr_t>(handle));
|
||||
} else {
|
||||
return static_cast<HandleType>(handle);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Converts a Vulkan handle to the transport type used by VulkanSurfaceProvider
|
||||
* @ingroup os_graphics_api
|
||||
*/
|
||||
template <typename HandleType>
|
||||
inline uint64_t vulkan_handle_value(HandleType handle)
|
||||
{
|
||||
if constexpr (std::is_pointer<HandleType>::value) {
|
||||
return static_cast<uint64_t>(reinterpret_cast<uintptr_t>(handle));
|
||||
} else {
|
||||
return static_cast<uint64_t>(handle);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace os
|
||||
@@ -1067,6 +1067,21 @@ void scripting_state_close()
|
||||
scripting_state_inited = 0;
|
||||
}
|
||||
|
||||
scripting_memory_stats scripting_get_memory_stats()
|
||||
{
|
||||
scripting_memory_stats stats;
|
||||
|
||||
lua_State* L = Script_system.GetLuaSession();
|
||||
if (L == nullptr) {
|
||||
return stats;
|
||||
}
|
||||
|
||||
stats.valid = true;
|
||||
stats.lua_heap_bytes = static_cast<size_t>(lua_gc(L, LUA_GCCOUNT, 0)) * 1024 +
|
||||
static_cast<size_t>(lua_gc(L, LUA_GCCOUNTB, 0));
|
||||
return stats;
|
||||
}
|
||||
|
||||
void scripting_state_do_frame(float /*frametime*/, bool doKeys)
|
||||
{
|
||||
// just incase something is wrong
|
||||
|
||||
@@ -88,6 +88,18 @@ void scripting_state_init();
|
||||
void scripting_state_close();
|
||||
void scripting_state_do_frame(float frametime, bool doKeys);
|
||||
|
||||
/**
|
||||
* @brief Lua heap usage snapshot, for the profiler overlay's memory panel
|
||||
*
|
||||
* @details Reports Lua's own GC byte count for the main script state (see lua_gc(),
|
||||
* LUA_GCCOUNT/LUA_GCCOUNTB). Invalid before the Lua state is created or after it is torn down.
|
||||
*/
|
||||
struct scripting_memory_stats {
|
||||
bool valid = false;
|
||||
size_t lua_heap_bytes = 0;
|
||||
};
|
||||
scripting_memory_stats scripting_get_memory_stats();
|
||||
|
||||
const scripting::HookBase* scripting_string_to_action(const char* action);
|
||||
ConditionalType scripting_string_to_condition(const char* condition);
|
||||
|
||||
|
||||
Loaded 100 of 167 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user