mirror of
https://github.com/scp-fs2open/fs2open.github.com.git
synced 2026-11-08 07:28:22 -10:00
Compare commits
20
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
718ee3c038 | ||
|
|
6af6a0cc99 | ||
|
|
f444d1ecf7 | ||
|
|
26d10bfb35 | ||
|
|
b8dc89aed5 | ||
|
|
60170f1af3 | ||
|
|
803b573fea | ||
|
|
bd5b6459b2 | ||
|
|
00b01e3615 | ||
|
|
76286c5332 | ||
|
|
e4546de24e | ||
|
|
336f198594 | ||
|
|
5c7059b451 | ||
|
|
4c0ff8414d | ||
|
|
019c4bacea | ||
|
|
e7d58d85cb | ||
|
|
84fa0a622f | ||
|
|
c5b761ca5d | ||
|
|
9443c4abe0 | ||
|
|
5e745f7535 |
No files matched your search
@@ -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()
|
||||
*
|
||||
|
||||
@@ -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)
|
||||
|
||||
+70
-4
@@ -85,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]);
|
||||
@@ -926,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;
|
||||
@@ -1709,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)
|
||||
@@ -2242,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;
|
||||
}
|
||||
|
||||
@@ -3441,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);
|
||||
|
||||
+83
-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 {
|
||||
@@ -811,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;
|
||||
|
||||
@@ -911,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;
|
||||
@@ -1030,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;
|
||||
|
||||
@@ -1120,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);
|
||||
|
||||
@@ -1236,6 +1276,36 @@ gr_debug_stats gr_get_debug_stats();
|
||||
*/
|
||||
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();
|
||||
}
|
||||
@@ -1496,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);
|
||||
@@ -1539,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;
|
||||
@@ -1516,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
|
||||
@@ -1631,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,105 @@ 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()
|
||||
@@ -860,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);
|
||||
@@ -1306,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,14 @@ 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
|
||||
@@ -156,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,93 @@ 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();
|
||||
}
|
||||
|
||||
if ( Bloom_framebuffer > 0 ) {
|
||||
glDeleteFramebuffers(1, &Bloom_framebuffer);
|
||||
Bloom_framebuffer = 0;
|
||||
}
|
||||
// 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()
|
||||
@@ -1188,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,10 @@
|
||||
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
|
||||
*
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
@@ -92,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) {
|
||||
@@ -137,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)
|
||||
@@ -213,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;
|
||||
}
|
||||
|
||||
@@ -79,6 +79,7 @@ GLint GL_max_elements_vertices = 4096;
|
||||
GLint GL_max_elements_indices = 4096;
|
||||
|
||||
GLuint Shadow_map_depth_texture = 0;
|
||||
GLuint Shadow_map_raw_sampler = 0;
|
||||
GLuint shadow_fbo = 0;
|
||||
int Shadow_texture_size = 0;
|
||||
|
||||
@@ -514,6 +515,24 @@ static bool opengl_init_shadow_framebuffer(int size)
|
||||
mprintf(("Shadow framebuffer created successfully.\n"));
|
||||
Shadow_texture_size = size;
|
||||
Shadow_texture_layers = Num_shadow_cascades + Num_cockpit_shadow_cascades;
|
||||
|
||||
// Second sampler for Shadow_map_depth_texture, compare mode off, so PCSS blocker
|
||||
// search (shadows.sdr) can read raw depth off the same texture that the compare
|
||||
// sampler reads filtered visibility off -- see shadow_contact_hardening_supported()
|
||||
// for why this needs a sampler object rather than a second glTexParameteri call.
|
||||
if (shadow_contact_hardening_supported()) {
|
||||
glGenSamplers(1, &Shadow_map_raw_sampler);
|
||||
// GL_NEAREST: the blocker search wants the actual stored per-texel depth, not a
|
||||
// hardware-filtered blend across the compare boundary -- deliberately different
|
||||
// from the compare sampler's GL_LINEAR.
|
||||
glSamplerParameteri(Shadow_map_raw_sampler, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
||||
glSamplerParameteri(Shadow_map_raw_sampler, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
||||
glSamplerParameteri(Shadow_map_raw_sampler, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
|
||||
glSamplerParameteri(Shadow_map_raw_sampler, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
|
||||
glSamplerParameteri(Shadow_map_raw_sampler, GL_TEXTURE_WRAP_R, GL_CLAMP_TO_EDGE);
|
||||
glSamplerParameteri(Shadow_map_raw_sampler, GL_TEXTURE_COMPARE_MODE, GL_NONE);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -565,24 +584,7 @@ void opengl_tnl_init()
|
||||
Transform_buffer_handle = opengl_create_texture_buffer_object();
|
||||
|
||||
if (Shadow_quality != ShadowQuality::Disabled) {
|
||||
int size;
|
||||
switch (Shadow_quality) {
|
||||
case ShadowQuality::Low:
|
||||
size = 512;
|
||||
break;
|
||||
case ShadowQuality::Medium:
|
||||
size = 1024;
|
||||
break;
|
||||
case ShadowQuality::High:
|
||||
size = 2048;
|
||||
break;
|
||||
case ShadowQuality::Ultra:
|
||||
size = 4096;
|
||||
break;
|
||||
default:
|
||||
size = 256;
|
||||
break;
|
||||
}
|
||||
const int size = shadows_map_resolution();
|
||||
|
||||
if (!opengl_init_shadow_framebuffer(size)) {
|
||||
mprintf(("Failed to create either shadow framebuffer. Disabling shadow support.\n"));
|
||||
@@ -608,6 +610,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;
|
||||
@@ -960,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);
|
||||
@@ -1044,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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 ==========
|
||||
@@ -555,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.
|
||||
@@ -620,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);
|
||||
@@ -665,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;
|
||||
}
|
||||
@@ -694,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 {
|
||||
@@ -719,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)) {
|
||||
@@ -732,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
|
||||
@@ -742,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)) {
|
||||
@@ -783,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;
|
||||
|
||||
@@ -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"
|
||||
@@ -538,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)
|
||||
@@ -989,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};
|
||||
}
|
||||
@@ -1052,7 +1068,7 @@ void vulkan_render_decals(decal_material* material_info,
|
||||
writer.setBuffer(PerDrawBinding::DecalInfo,
|
||||
getPendingBufInfo(static_cast<size_t>(uniform_block_type::DecalInfo)));
|
||||
writer.flush();
|
||||
stateTracker->bindDescriptorSet(DescriptorSetIndex::Global, globalSet, nullptr);
|
||||
stateTracker->bindDescriptorSet(DescriptorSetIndex::Global, globalSet);
|
||||
stateTracker->bindDescriptorSet(DescriptorSetIndex::Material,
|
||||
materialSet,
|
||||
writer.dynamicOffsets(DescriptorSetIndex::Material));
|
||||
|
||||
@@ -11,10 +11,11 @@ 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};
|
||||
@@ -67,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;
|
||||
@@ -112,6 +122,7 @@ void DescriptorWriter::writeSet(DescriptorSetIndex setIndex, vk::DescriptorSet s
|
||||
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);
|
||||
@@ -123,7 +134,7 @@ void DescriptorWriter::writeSet(DescriptorSetIndex setIndex, vk::DescriptorSet s
|
||||
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;
|
||||
}
|
||||
@@ -233,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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -310,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.
|
||||
|
||||
@@ -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,7 +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;
|
||||
const vk::DescriptorImageInfo& getImage(vk::ImageViewType t, bool depthCompare) const;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -78,6 +91,7 @@ namespace GlobalBinding {
|
||||
static constexpr uint32_t IrradianceMap = 4; // samplerCube: irradiance map
|
||||
static constexpr uint32_t Tlas = 5; // accelerationStructureEXT: raytraced shadow TLAS (only present when raytraced shadows are supported)
|
||||
static constexpr uint32_t ShadowCascadeParams = 6; // UBO: shadow cascade projection matrices/distances (per-frame, shared by all consumers)
|
||||
static constexpr uint32_t ShadowMapRaw = 7; // sampler2DArray: raw (non-compare) shadow map read, for PCSS blocker search
|
||||
}
|
||||
|
||||
// Material Set (Set 1) bindings — per-material data
|
||||
@@ -298,13 +312,13 @@ public:
|
||||
*
|
||||
* 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.
|
||||
* Always sized to the set layout's dynamic descriptor count (offsets for
|
||||
* bindings left at their fallback stay 0). Points at stable per-set storage —
|
||||
* 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.
|
||||
*/
|
||||
const uint32_t* dynamicOffsets(DescriptorSetIndex setIndex) const
|
||||
ArrayView<uint32_t> dynamicOffsets(DescriptorSetIndex setIndex) const
|
||||
{
|
||||
return m_dynOffsets[static_cast<size_t>(setIndex)].data();
|
||||
return m_dynOffsets[static_cast<size_t>(setIndex)];
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -334,6 +348,7 @@ private:
|
||||
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
|
||||
};
|
||||
|
||||
|
||||
@@ -759,7 +759,7 @@ void VulkanDrawManager::renderShadowDraw(gr_buffer_handle ubo_handle,
|
||||
// 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_cachedShadowSets).
|
||||
// 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
|
||||
@@ -772,7 +772,13 @@ void VulkanDrawManager::renderShadowDraw(gr_buffer_handle ubo_handle,
|
||||
shadowInputs.cascadeHandle = cascade.bufferHandle.value();
|
||||
shadowInputs.cascadeOffset = cascade.offset;
|
||||
shadowInputs.cascadeSize = cascade.size;
|
||||
shadowInputs.cascadeValid = cascade.valid;
|
||||
// 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()];
|
||||
@@ -783,24 +789,27 @@ void VulkanDrawManager::renderShadowDraw(gr_buffer_handle ubo_handle,
|
||||
}
|
||||
}
|
||||
|
||||
if (!m_cachedShadowValid || shadowInputs != m_cachedShadowInputs) {
|
||||
if (!m_cachedShadow.hits(shadowInputs)) {
|
||||
DescriptorWriter writer;
|
||||
writer.reset(descManager->getDevice(), descManager->getFallbacks());
|
||||
|
||||
m_cachedShadowGlobalSet = descManager->allocateFrameSet(DescriptorSetIndex::Global);
|
||||
Assert(m_cachedShadowGlobalSet);
|
||||
writer.writeSet(DescriptorSetIndex::Global, m_cachedShadowGlobalSet);
|
||||
ShadowSets sets;
|
||||
|
||||
sets.global = descManager->allocateFrameSet(DescriptorSetIndex::Global);
|
||||
Assert(sets.global);
|
||||
writer.writeSet(DescriptorSetIndex::Global, sets.global);
|
||||
if (shadowInputs.cascadeValid) {
|
||||
vk::Buffer buf = bufferManager->getVkBuffer(gr_buffer_handle(shadowInputs.cascadeHandle));
|
||||
vk::Buffer buf =
|
||||
bufferManager->getVkBufferForBinding(gr_buffer_handle(shadowInputs.cascadeHandle));
|
||||
if (buf) {
|
||||
writer.setBuffer(GlobalBinding::ShadowCascadeParams,
|
||||
{buf, shadowInputs.cascadeOffset, shadowInputs.cascadeSize});
|
||||
}
|
||||
}
|
||||
|
||||
m_cachedShadowMaterialSet = descManager->allocateFrameSet(DescriptorSetIndex::Material);
|
||||
Assert(m_cachedShadowMaterialSet);
|
||||
writer.writeSet(DescriptorSetIndex::Material, m_cachedShadowMaterialSet);
|
||||
sets.material = descManager->allocateFrameSet(DescriptorSetIndex::Material);
|
||||
Assert(sets.material);
|
||||
writer.writeSet(DescriptorSetIndex::Material, sets.material);
|
||||
{
|
||||
vk::Buffer buf = bufferManager->getVkBuffer(ubo_handle);
|
||||
if (buf) {
|
||||
@@ -815,13 +824,12 @@ void VulkanDrawManager::renderShadowDraw(gr_buffer_handle ubo_handle,
|
||||
static_cast<vk::DeviceSize>(shadowInputs.transformSize)});
|
||||
}
|
||||
|
||||
m_cachedShadowPerDrawSet = descManager->allocateFrameSet(DescriptorSetIndex::PerDraw);
|
||||
Assert(m_cachedShadowPerDrawSet);
|
||||
writer.writeSet(DescriptorSetIndex::PerDraw, m_cachedShadowPerDrawSet);
|
||||
sets.perDraw = descManager->allocateFrameSet(DescriptorSetIndex::PerDraw);
|
||||
Assert(sets.perDraw);
|
||||
writer.writeSet(DescriptorSetIndex::PerDraw, sets.perDraw);
|
||||
writer.flush();
|
||||
|
||||
m_cachedShadowInputs = shadowInputs;
|
||||
m_cachedShadowValid = true;
|
||||
m_cachedShadow.store(sets, shadowInputs);
|
||||
}
|
||||
|
||||
// applyMaterial's caches deliberately are NOT invalidated here. They record
|
||||
@@ -839,9 +847,10 @@ void VulkanDrawManager::renderShadowDraw(gr_buffer_handle ubo_handle,
|
||||
|
||||
static constexpr uint32_t perDrawDynOffsets[PERDRAW_DYNAMIC_OFFSET_COUNT] = {};
|
||||
|
||||
stateTracker->bindDescriptorSet(DescriptorSetIndex::Global, m_cachedShadowGlobalSet, nullptr);
|
||||
stateTracker->bindDescriptorSet(DescriptorSetIndex::Material, m_cachedShadowMaterialSet, materialDynOffsets);
|
||||
stateTracker->bindDescriptorSet(DescriptorSetIndex::PerDraw, m_cachedShadowPerDrawSet, perDrawDynOffsets);
|
||||
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);
|
||||
@@ -949,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.
|
||||
@@ -979,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 = {};
|
||||
@@ -999,14 +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_cachedShadowGlobalSet = nullptr;
|
||||
m_cachedShadowMaterialSet = nullptr;
|
||||
m_cachedShadowPerDrawSet = nullptr;
|
||||
m_cachedShadowValid = false;
|
||||
m_cachedMaterial.invalidate();
|
||||
m_cachedPerDraw.invalidate();
|
||||
m_cachedShadow.invalidate();
|
||||
}
|
||||
|
||||
void VulkanDrawManager::printFrameStats()
|
||||
@@ -1381,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);
|
||||
}
|
||||
@@ -1402,7 +1412,7 @@ bool VulkanDrawManager::applyMaterial(material* mat, primitive_type prim_type, v
|
||||
// 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->getVkBuffer(pending.bufferHandle)) {
|
||||
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);
|
||||
@@ -1433,13 +1443,14 @@ bool VulkanDrawManager::applyMaterial(material* mat, primitive_type prim_type, v
|
||||
bindPendingUBOs(DescriptorSetIndex::Global);
|
||||
if (shadowReady) {
|
||||
writer.setImage(GlobalBinding::ShadowMap, pp->getShadowTextureInfo());
|
||||
writer.setImage(GlobalBinding::ShadowMapRaw, pp->getShadowRawTextureInfo());
|
||||
}
|
||||
m_cachedGlobalHadShadow = shadowReady;
|
||||
m_globalSetDirty = false;
|
||||
}
|
||||
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);
|
||||
@@ -1480,10 +1491,10 @@ bool VulkanDrawManager::applyMaterial(material* mat, primitive_type prim_type, v
|
||||
}
|
||||
|
||||
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);
|
||||
@@ -1499,12 +1510,10 @@ 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;
|
||||
{
|
||||
struct PerDrawUbo {
|
||||
@@ -1530,19 +1539,17 @@ bool VulkanDrawManager::applyMaterial(material* mat, primitive_type prim_type, v
|
||||
}
|
||||
|
||||
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(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, nullptr);
|
||||
stateTracker->bindDescriptorSet(DescriptorSetIndex::Global, globalSet);
|
||||
stateTracker->bindDescriptorSet(DescriptorSetIndex::Material, materialSet, materialDynOffsets);
|
||||
stateTracker->bindDescriptorSet(DescriptorSetIndex::PerDraw, perDrawSet, perDrawDynOffsets);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
*
|
||||
@@ -290,7 +356,7 @@ class VulkanDrawManager {
|
||||
* @brief Invalidate every memoized descriptor-set cache (Global + Material + PerDraw)
|
||||
*
|
||||
* These previous-set caches (see the class comments above m_cachedGlobalSet /
|
||||
* m_cachedMaterialSet / m_cachedPerDrawSet) assume nothing rebinds a *different*
|
||||
* 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
|
||||
@@ -301,9 +367,9 @@ class VulkanDrawManager {
|
||||
void invalidateDrawStateCaches()
|
||||
{
|
||||
m_globalSetDirty = true;
|
||||
m_cachedMaterialValid = false;
|
||||
m_cachedPerDrawValid = false;
|
||||
m_cachedShadowValid = false;
|
||||
m_cachedMaterial.invalidate();
|
||||
m_cachedPerDraw.invalidate();
|
||||
m_cachedShadow.invalidate();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -333,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
|
||||
*/
|
||||
@@ -538,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,
|
||||
@@ -564,9 +622,7 @@ 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.
|
||||
@@ -592,16 +648,17 @@ class VulkanDrawManager {
|
||||
transformBuffer == o.transformBuffer && transformOffset == o.transformOffset &&
|
||||
transformSize == o.transformSize;
|
||||
}
|
||||
bool operator!=(const ShadowSetInputs& o) const
|
||||
{
|
||||
return !(*this == o);
|
||||
}
|
||||
};
|
||||
vk::DescriptorSet m_cachedShadowGlobalSet = nullptr;
|
||||
vk::DescriptorSet m_cachedShadowMaterialSet = nullptr;
|
||||
vk::DescriptorSet m_cachedShadowPerDrawSet = nullptr;
|
||||
ShadowSetInputs m_cachedShadowInputs;
|
||||
bool m_cachedShadowValid = false;
|
||||
// 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 {
|
||||
|
||||
@@ -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 ==========
|
||||
|
||||
/**
|
||||
|
||||
@@ -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"
|
||||
@@ -506,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) {
|
||||
@@ -687,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;
|
||||
}
|
||||
@@ -754,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
|
||||
@@ -767,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);
|
||||
}
|
||||
@@ -801,6 +814,7 @@ void VulkanDeferredLighting::render(vk::CommandBuffer cmd)
|
||||
writer.setImage(GlobalBinding::EnvMap, envTexInfo);
|
||||
writer.setImage(GlobalBinding::IrradianceMap, irrTexInfo);
|
||||
writer.setBuffer(GlobalBinding::ShadowCascadeParams, shadowCascadeParamsInfo);
|
||||
writer.setImage(GlobalBinding::ShadowMapRaw, shadowRawTexInfo);
|
||||
|
||||
// Set 1: Material
|
||||
vk::DescriptorSet materialSet = descriptorMgr->allocateFrameSet(DescriptorSetIndex::Material);
|
||||
|
||||
@@ -34,14 +34,7 @@ bool VulkanShadowMap::init(PostProcessContext& ctx)
|
||||
return false;
|
||||
}
|
||||
|
||||
int size;
|
||||
switch (Shadow_quality) {
|
||||
case ShadowQuality::Low: size = 512; break;
|
||||
case ShadowQuality::Medium: size = 1024; break;
|
||||
case ShadowQuality::High: size = 2048; break;
|
||||
case ShadowQuality::Ultra: size = 4096; break;
|
||||
default: size = 512; break;
|
||||
}
|
||||
const int size = shadows_map_resolution();
|
||||
|
||||
const auto layers = static_cast<uint32_t>(Num_shadow_cascades + Num_cockpit_shadow_cascades);
|
||||
nprintf(("vulkan", "VulkanPostProcessor: Creating %dx%d shadow map (%d cascades)\n", size, size, layers));
|
||||
@@ -188,6 +181,32 @@ bool VulkanShadowMap::init(PostProcessContext& ctx)
|
||||
}
|
||||
}
|
||||
|
||||
// Raw (non-compare) sampler on the same depth view, for the PCSS blocker search --
|
||||
// it needs actual stored depth values, not a compare result. GL_NEAREST-equivalent
|
||||
// filtering: the blocker search wants the exact per-texel depth, not a hardware-
|
||||
// filtered blend across the compare boundary (deliberately different from the compare
|
||||
// sampler's linear filtering above). Vulkan samplers are independent of images, so this
|
||||
// is always available -- no capability gate needed here (contrast the OpenGL backend,
|
||||
// which needs GL 3.3 because compare mode there is texture-object state).
|
||||
{
|
||||
vk::SamplerCreateInfo samplerInfo;
|
||||
samplerInfo.magFilter = vk::Filter::eNearest;
|
||||
samplerInfo.minFilter = vk::Filter::eNearest;
|
||||
samplerInfo.mipmapMode = vk::SamplerMipmapMode::eNearest;
|
||||
samplerInfo.addressModeU = vk::SamplerAddressMode::eClampToEdge;
|
||||
samplerInfo.addressModeV = vk::SamplerAddressMode::eClampToEdge;
|
||||
samplerInfo.addressModeW = vk::SamplerAddressMode::eClampToEdge;
|
||||
samplerInfo.compareEnable = VK_FALSE;
|
||||
samplerInfo.borderColor = vk::BorderColor::eFloatOpaqueWhite;
|
||||
|
||||
try {
|
||||
m_rawSampler = m_ctx->device.createSampler(samplerInfo);
|
||||
} catch (const vk::SystemError& e) {
|
||||
nprintf(("vulkan", "VulkanPostProcessor: Failed to create shadow raw sampler: %s\n", e.what()));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
m_textureSize = size;
|
||||
m_initialized = true;
|
||||
nprintf(("vulkan", "VulkanPostProcessor: Shadow map initialized (%dx%d, %d cascades)\n", size, size, layers));
|
||||
@@ -204,6 +223,10 @@ void VulkanShadowMap::shutdown()
|
||||
m_ctx->device.destroySampler(m_compareSampler);
|
||||
m_compareSampler = nullptr;
|
||||
}
|
||||
if (m_rawSampler) {
|
||||
m_ctx->device.destroySampler(m_rawSampler);
|
||||
m_rawSampler = nullptr;
|
||||
}
|
||||
if (m_framebuffer) {
|
||||
m_ctx->device.destroyFramebuffer(m_framebuffer);
|
||||
m_framebuffer = nullptr;
|
||||
|
||||
@@ -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, 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);
|
||||
|
||||
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,91 +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, MemoryPurpose::RenderTarget);
|
||||
|
||||
// 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
|
||||
@@ -352,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
|
||||
@@ -395,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.
|
||||
@@ -424,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;
|
||||
}
|
||||
@@ -467,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
|
||||
@@ -771,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.
|
||||
@@ -834,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
|
||||
@@ -864,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();
|
||||
@@ -942,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>
|
||||
|
||||
@@ -95,6 +97,64 @@ class VulkanRenderer {
|
||||
*/
|
||||
void setupFrame();
|
||||
|
||||
/**
|
||||
* @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
|
||||
*/
|
||||
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
|
||||
* Called at the END of each frame after all draw calls
|
||||
@@ -145,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
|
||||
@@ -184,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
|
||||
@@ -197,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.
|
||||
@@ -426,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();
|
||||
@@ -468,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,
|
||||
@@ -489,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;
|
||||
|
||||
@@ -534,9 +704,6 @@ class VulkanRenderer {
|
||||
SCP_vector<vk::CommandBuffer> m_currentCommandBuffers; // For cleanup
|
||||
bool m_frameInProgress = false;
|
||||
|
||||
// Swap chain recreation
|
||||
bool m_swapChainNeedsRecreation = false;
|
||||
|
||||
// Physical device info (needed for memory manager)
|
||||
vk::PhysicalDevice m_physicalDevice;
|
||||
// Cached once at device selection: the limit/feature getters below are
|
||||
|
||||
@@ -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,10 +168,6 @@ void VulkanRenderer::setupFrame()
|
||||
return;
|
||||
}
|
||||
|
||||
// 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();
|
||||
@@ -152,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) {
|
||||
@@ -189,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;
|
||||
@@ -213,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
|
||||
@@ -231,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;
|
||||
@@ -254,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()
|
||||
@@ -348,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);
|
||||
@@ -359,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;
|
||||
@@ -602,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);
|
||||
}
|
||||
@@ -616,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;
|
||||
@@ -1104,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
|
||||
@@ -253,7 +253,7 @@ void VulkanStateTracker::bindPipeline(vk::Pipeline pipeline, vk::PipelineLayout
|
||||
|
||||
void VulkanStateTracker::bindDescriptorSet(DescriptorSetIndex setIndex,
|
||||
vk::DescriptorSet set,
|
||||
const uint32_t* dynamicOffsets)
|
||||
ArrayView<uint32_t> dynamicOffsets)
|
||||
{
|
||||
Assertion(m_cmdBuffer, "bindDescriptorSet called without active command buffer!");
|
||||
Assertion(m_currentPipelineLayout, "bindDescriptorSet called without bound pipeline layout!");
|
||||
@@ -261,10 +261,15 @@ void VulkanStateTracker::bindDescriptorSet(DescriptorSetIndex setIndex,
|
||||
|
||||
auto index = static_cast<uint32_t>(setIndex);
|
||||
const uint32_t dynCount = VulkanDescriptorManager::getDynamicOffsetCount(setIndex);
|
||||
Assertion(dynCount == 0 || dynamicOffsets != nullptr,
|
||||
"bindDescriptorSet: set %u declares %u dynamic descriptors but no offsets were supplied!",
|
||||
// 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);
|
||||
dynCount,
|
||||
dynamicOffsets.size);
|
||||
|
||||
// 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
|
||||
@@ -273,7 +278,7 @@ void VulkanStateTracker::bindDescriptorSet(DescriptorSetIndex setIndex,
|
||||
auto& boundDyn = m_boundDynamicOffsets[index];
|
||||
bool offsetsChanged = false;
|
||||
for (uint32_t i = 0; i < dynCount; ++i) {
|
||||
if (boundDyn[i] != dynamicOffsets[i]) {
|
||||
if (boundDyn[i] != dynamicOffsets.data[i]) {
|
||||
offsetsChanged = true;
|
||||
break;
|
||||
}
|
||||
@@ -286,11 +291,11 @@ void VulkanStateTracker::bindDescriptorSet(DescriptorSetIndex setIndex,
|
||||
1,
|
||||
&set,
|
||||
dynCount,
|
||||
dynCount > 0 ? dynamicOffsets : nullptr);
|
||||
dynCount > 0 ? dynamicOffsets.data : nullptr);
|
||||
|
||||
m_boundDescriptorSets[index] = set;
|
||||
for (uint32_t i = 0; i < dynCount; ++i) {
|
||||
boundDyn[i] = dynamicOffsets[i];
|
||||
boundDyn[i] = dynamicOffsets.data[i];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -136,13 +136,16 @@ class VulkanStateTracker {
|
||||
* @brief Bind descriptor set
|
||||
*
|
||||
* @param dynamicOffsets Offsets for the set layout's eUniformBufferDynamic
|
||||
* bindings, ordered by binding number. Must point to at least
|
||||
* VulkanDescriptorManager::getDynamicOffsetCount(setIndex) entries
|
||||
* (DescriptorWriter::dynamicOffsets() produces exactly that); may be
|
||||
* null only for a set that declares none.
|
||||
* 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 uint32_t* dynamicOffsets);
|
||||
void bindDescriptorSet(DescriptorSetIndex setIndex,
|
||||
vk::DescriptorSet set,
|
||||
ArrayView<uint32_t> dynamicOffsets = {});
|
||||
|
||||
// ========== Buffer Binding ==========
|
||||
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
}
|
||||
@@ -271,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);
|
||||
}
|
||||
|
||||
@@ -288,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)
|
||||
@@ -385,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;
|
||||
@@ -445,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:
|
||||
@@ -455,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;
|
||||
|
||||
@@ -462,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;
|
||||
@@ -581,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;
|
||||
|
||||
@@ -601,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();
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -129,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
|
||||
@@ -362,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
|
||||
@@ -481,6 +481,8 @@ add_file_folder("Graphics"
|
||||
graphics/shader_types.cpp
|
||||
graphics/shader_types.h
|
||||
graphics/render_queue.h
|
||||
graphics/rtao.cpp
|
||||
graphics/rtao.h
|
||||
graphics/shadows.cpp
|
||||
graphics/shadows.h
|
||||
graphics/tmapper.h
|
||||
@@ -628,6 +630,8 @@ if (FSO_BUILD_WITH_VULKAN)
|
||||
graphics/vulkan/VulkanPostProcessingMSAA.cpp
|
||||
graphics/vulkan/VulkanPostProcessingSMAA.cpp
|
||||
graphics/vulkan/VulkanPostProcessingShadow.cpp
|
||||
graphics/vulkan/VulkanPresentTarget.cpp
|
||||
graphics/vulkan/VulkanPresentTarget.h
|
||||
graphics/vulkan/VulkanQuery.cpp
|
||||
graphics/vulkan/VulkanQuery.h
|
||||
graphics/vulkan/VulkanRaytracing.cpp
|
||||
@@ -1169,6 +1173,7 @@ add_file_folder("OsApi"
|
||||
osapi/osregistry.cpp
|
||||
osapi/outwnd.h
|
||||
osapi/outwnd.cpp
|
||||
osapi/vulkan_surface.h
|
||||
)
|
||||
|
||||
# Parse files
|
||||
@@ -1760,6 +1765,8 @@ add_file_folder("Starfield"
|
||||
starfield/nebula.h
|
||||
starfield/starfield.cpp
|
||||
starfield/starfield.h
|
||||
starfield/sun_disc.cpp
|
||||
starfield/sun_disc.h
|
||||
starfield/supernova.cpp
|
||||
starfield/supernova.h
|
||||
starfield/starfield_flags.h
|
||||
|
||||
+165
-25
@@ -34,6 +34,8 @@
|
||||
#include "render/batching.h"
|
||||
#include "starfield/nebula.h"
|
||||
#include "starfield/starfield.h"
|
||||
#include "graphics/shadows.h"
|
||||
#include "starfield/sun_disc.h"
|
||||
#include "starfield/supernova.h"
|
||||
#include "tracing/tracing.h"
|
||||
#include "utils/Random.h"
|
||||
@@ -92,6 +94,11 @@ typedef struct starfield_bitmap {
|
||||
int glow_fps;
|
||||
int xparent;
|
||||
float r, g, b, i; // only for suns
|
||||
float angular_size; // only for suns -- this sun's apparent diameter from stars.tbl;
|
||||
// see SUN_ANGULAR_SIZE_UNSPECIFIED
|
||||
float disc_fraction; // only for suns -- cached sun_disc_measure_bitmap() result for
|
||||
// disc_measured_for; negative means "not measured yet"
|
||||
int disc_measured_for; // only for suns -- bitmap_id disc_fraction was measured from
|
||||
int glare; // only for suns
|
||||
int flare; // Is there a lens-flare for this sun?
|
||||
flare_info flare_infos[MAX_FLARE_COUNT]; // each flare can use a texture in flare_bmp, with different scale
|
||||
@@ -107,11 +114,14 @@ typedef struct starfield_bitmap_instance {
|
||||
float scale_x, scale_y; // x and y scale
|
||||
int div_x, div_y; // # of x and y divisions
|
||||
angles ang; // angles from FRED
|
||||
float angular_size; // only for suns -- this mission's apparent diameter for the sun,
|
||||
// in degrees; see SUN_ANGULAR_SIZE_UNSPECIFIED
|
||||
int star_bitmap_index; // index into starfield_bitmap array
|
||||
int n_verts;
|
||||
vertex *verts;
|
||||
|
||||
starfield_bitmap_instance() : scale_x(1.0f), scale_y(1.0f), div_x(1), div_y(1), star_bitmap_index(0), n_verts(0), verts(NULL) {
|
||||
starfield_bitmap_instance() : scale_x(1.0f), scale_y(1.0f), div_x(1), div_y(1),
|
||||
angular_size(SUN_ANGULAR_SIZE_UNSPECIFIED), star_bitmap_index(0), n_verts(0), verts(nullptr) {
|
||||
ang.p = 0.0f;
|
||||
ang.b = 0.0f;
|
||||
ang.h = 0.0f;
|
||||
@@ -124,6 +134,8 @@ static SCP_vector<starfield_bitmap_instance> Starfield_bitmap_instances;
|
||||
|
||||
// sun bitmaps and sun glow bitmaps
|
||||
static SCP_vector<starfield_bitmap> Sun_bitmaps;
|
||||
|
||||
float Sun_angular_size_override = -1.0f;
|
||||
static SCP_vector<starfield_bitmap_instance> Suns;
|
||||
|
||||
// Goober5000
|
||||
@@ -400,6 +412,9 @@ static void starfield_bitmap_entry_init(starfield_bitmap *sbm)
|
||||
sbm->bitmap_id = -1;
|
||||
sbm->glow_bitmap = -1;
|
||||
sbm->glow_n_frames = 1;
|
||||
sbm->angular_size = SUN_ANGULAR_SIZE_UNSPECIFIED;
|
||||
sbm->disc_fraction = -1.0f;
|
||||
sbm->disc_measured_for = -1;
|
||||
|
||||
for (i = 0; i < MAX_FLARE_BMP; i++) {
|
||||
sbm->flare_bitmaps[i].bitmap_id = -1;
|
||||
@@ -497,6 +512,20 @@ void parse_startbl(const char *filename)
|
||||
Warning(LOCATION, "%s", warning.c_str()); // customized case is significant
|
||||
}
|
||||
|
||||
// apparent diameter of the sun's disc in degrees (Sol is ~0.53). Only affects
|
||||
// raytraced shadows, which get a penumbra sized to match. Left out, the size is
|
||||
// measured from the sun bitmap instead -- see sun_angular_radius_tangent() -- so
|
||||
// use 0 to ask for hard shadows and "derived" to say "measure it" outright.
|
||||
if (optional_string("$SunAngularSize:")) {
|
||||
if ( !optional_string("derived") ) {
|
||||
stuff_float(&sbm.angular_size);
|
||||
if (sbm.angular_size < 0.0f) {
|
||||
error_display(0, "$SunAngularSize: for sun '%s' must be >= 0 (got %f); measuring the bitmap instead.", sbm.filename, sbm.angular_size);
|
||||
sbm.angular_size = SUN_ANGULAR_SIZE_UNSPECIFIED;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// lens flare stuff
|
||||
if (optional_string("$Flare:")) {
|
||||
sbm.flare = 1;
|
||||
@@ -855,6 +884,14 @@ void stars_pre_level_init(bool clear_backgrounds)
|
||||
sb.bitmap_id = -1;
|
||||
}
|
||||
|
||||
// drop any derived angular size along with the bitmap it was measured from. The
|
||||
// handle check in sun_angular_radius_tangent() can't be relied on for this: bm_load()
|
||||
// hands back a recycled slot index, so a reloaded bitmap can come back on the handle
|
||||
// it had before. This is what makes a size change take effect when the background
|
||||
// changes -- which, outside of a mission, is every time the lab switches backgrounds.
|
||||
sb.disc_fraction = -1.0f;
|
||||
sb.disc_measured_for = -1;
|
||||
|
||||
if (sb.glow_bitmap > 0) {
|
||||
bm_release(sb.glow_bitmap);
|
||||
sb.glow_bitmap = -1;
|
||||
@@ -1270,6 +1307,107 @@ void stars_get_sun_pos(int sun_n, vec3d *pos)
|
||||
vm_vec_unrotate(pos, &temp, &rot);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief The apparent diameter this sun should use, in degrees, or negative if nothing sets one
|
||||
*
|
||||
* @param mission_angular_size this sun instance's +AngularSize: from the mission file
|
||||
*/
|
||||
static float sun_explicit_angular_size(const starfield_bitmap *bm, float mission_angular_size)
|
||||
{
|
||||
// the first layer that specifies a size wins: the LabUi session override, then the
|
||||
// mission's value for this sun instance, then the sun's stars.tbl entry
|
||||
for (float degrees : {Sun_angular_size_override, mission_angular_size, bm->angular_size}) {
|
||||
if (degrees >= 0.0f) {
|
||||
return degrees;
|
||||
}
|
||||
}
|
||||
|
||||
return SUN_ANGULAR_SIZE_UNSPECIFIED;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief How much of this sun's bitmap is its emitting disc, measuring it on first use
|
||||
*
|
||||
* @return the disc fraction, or 0 for a sun whose bitmap has no measurable disc
|
||||
*/
|
||||
static float sun_measured_disc_fraction(starfield_bitmap *bm)
|
||||
{
|
||||
// measuring reads the bitmap's pixels, so it happens once and is cached against the
|
||||
// handle it was measured from
|
||||
if ( (bm->disc_fraction >= 0.0f) && (bm->disc_measured_for == bm->bitmap_id) ) {
|
||||
return bm->disc_fraction;
|
||||
}
|
||||
|
||||
const float measured = sun_disc_measure_bitmap(bm->bitmap_id);
|
||||
|
||||
// both a bitmap we couldn't read and one with no drawn disc at all fall back to hard
|
||||
// shadows, but they are very different things -- keep them apart in the log, or a broken
|
||||
// read reads as "working as intended". Neither is worth a Warning(): this runs for every
|
||||
// sun in every mission, not just for content that asked for it, and hard shadows are a
|
||||
// perfectly serviceable outcome
|
||||
if (measured < 0.0f) {
|
||||
mprintf(("Sun '%s': could not read the bitmap to measure it, using hard shadows\n", bm->filename));
|
||||
} else if (measured == 0.0f) {
|
||||
// mods ship blank sun bitmaps to get a light source without a visible sun
|
||||
mprintf(("Sun '%s': no drawn disc, using hard shadows\n", bm->filename));
|
||||
} else {
|
||||
// logged once per sun bitmap, not per frame -- this is what a mod tunes its suns
|
||||
// against. The size is quoted at +Scale: 1.0 since the measurement is a property of
|
||||
// the bitmap; a mission scaling its sun scales this with it.
|
||||
mprintf(("Sun '%s': measured disc fraction %.3f (%.2f degrees at +Scale: 1.0)\n", bm->filename, measured,
|
||||
fl_degrees(2.0f * atanf(sun_disc_tangent_from_fraction(measured, 1.0f)))));
|
||||
}
|
||||
|
||||
bm->disc_fraction = (measured > 0.0f) ? measured : 0.0f;
|
||||
bm->disc_measured_for = bm->bitmap_id;
|
||||
|
||||
return bm->disc_fraction;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Tangent of a sun's angular radius, which is what a directional light's source_radius is
|
||||
*
|
||||
* See traceShadowRayCone() in shadows.sdr: for directional lights the source radius *is* the
|
||||
* tangent of the angular radius, so the penumbra widens with occluder distance the way a real
|
||||
* area light's would. 0 means a point source, i.e. hard shadows.
|
||||
*
|
||||
* Only a size that was *asked for* can be 0. A sun whose bitmap simply has no measurable disc
|
||||
* falls back to Sol instead -- see below.
|
||||
*
|
||||
* @param scale_x the mission's +Scale: for this sun instance
|
||||
* @param mission_angular_size this sun instance's +AngularSize: from the mission file
|
||||
*/
|
||||
static float sun_angular_radius_tangent(starfield_bitmap *bm, float scale_x, float mission_angular_size)
|
||||
{
|
||||
const float specified = sun_explicit_angular_size(bm, mission_angular_size);
|
||||
|
||||
if (specified >= 0.0f) {
|
||||
// Explicit, including an explicit 0: a mod that asks for a sizeless source gets the
|
||||
// hard edge it asked for.
|
||||
return sun_disc_tangent_from_diameter(specified);
|
||||
}
|
||||
|
||||
// the size is only used to soften shadows, so do not read the bitmap when shadows are off
|
||||
if (Shadow_quality == ShadowQuality::Disabled) {
|
||||
return 0.0f;
|
||||
}
|
||||
|
||||
|
||||
const float measured = sun_disc_tangent_from_fraction(sun_measured_disc_fraction(bm), scale_x);
|
||||
if (measured > 0.0f) {
|
||||
return measured;
|
||||
}
|
||||
|
||||
// Nothing specified a size and the bitmap has no disc we could measure (a planet or
|
||||
// nebula bitmap used as a sun, or one we failed to read). Falling back to 0 here would
|
||||
// hand that sun a single hard ray, and in a multi-sun mission one such sun is enough to
|
||||
// lay a razor-sharp shadow over every soft one -- with no clue in the editor as to which
|
||||
// sun is responsible, since the control that would fix it sits on a different sun.
|
||||
// Sol is the same default the editors and the lab start from, so an unmeasurable sun now
|
||||
// behaves like an ordinary one; hard shadows remain available by asking for 0 outright.
|
||||
return sun_disc_tangent_from_diameter(SUN_ANGULAR_SIZE_SOL);
|
||||
}
|
||||
|
||||
// draw sun
|
||||
void stars_draw_sun(int show_sun)
|
||||
{
|
||||
@@ -1318,9 +1456,13 @@ void stars_draw_sun(int show_sun)
|
||||
sun_dir = sun_pos;
|
||||
vm_vec_normalize(&sun_dir);
|
||||
|
||||
// add the light source corresponding to the sun, except when rendering to an envmap
|
||||
if ( !Rendering_to_env )
|
||||
light_add_directional(&sun_dir, idx, !bm->glare, bm->i, bm->r, bm->g, bm->b);
|
||||
// add the light source corresponding to the sun, except when rendering to an envmap.
|
||||
// For directional lights, source_radius carries the tangent of the sun's angular
|
||||
// radius (see traceShadowRayCone() in shadows.sdr), sizing its shadow penumbra.
|
||||
if ( !Rendering_to_env ) {
|
||||
light_add_directional(&sun_dir, idx, !bm->glare, bm->i, bm->r, bm->g, bm->b,
|
||||
sun_angular_radius_tangent(bm, Suns[idx].scale_x, Suns[idx].angular_size));
|
||||
}
|
||||
|
||||
// if supernova
|
||||
if ( supernova_active() && (idx == 0) )
|
||||
@@ -2449,6 +2591,20 @@ int stars_find_bitmap(const char *name)
|
||||
return -1;
|
||||
}
|
||||
|
||||
// The mission-side list entry and the in-world instance carry the same fields, so keeping the
|
||||
// copy in one place is what stops the three call sites drifting apart. angular_size is only
|
||||
// meaningful for suns, but copying it for bitmaps too is harmless and keeps every caller
|
||||
// identical -- which beats being selectively correct in a way no reader can verify.
|
||||
static void starfield_copy_entry_to_instance(const starfield_list_entry *sle, starfield_bitmap_instance &sbi)
|
||||
{
|
||||
sbi.ang = sle->ang;
|
||||
sbi.scale_x = sle->scale_x;
|
||||
sbi.scale_y = sle->scale_y;
|
||||
sbi.div_x = sle->div_x;
|
||||
sbi.div_y = sle->div_y;
|
||||
sbi.angular_size = sle->angular_size;
|
||||
}
|
||||
|
||||
// lookup a sun by bitmap filename, return index or -1 on fail
|
||||
int stars_find_sun(const char *name)
|
||||
{
|
||||
@@ -2481,6 +2637,7 @@ void stars_get_data(bool is_sun, int idx, starfield_list_entry& sle)
|
||||
sle.div_y = item.div_y;
|
||||
sle.scale_x = item.scale_x;
|
||||
sle.scale_y = item.scale_y;
|
||||
sle.angular_size = item.angular_size;
|
||||
}
|
||||
|
||||
void stars_set_data(bool is_sun, int idx, starfield_list_entry& sle)
|
||||
@@ -2497,6 +2654,7 @@ void stars_set_data(bool is_sun, int idx, starfield_list_entry& sle)
|
||||
item.div_y = sle.div_y;
|
||||
item.scale_x = sle.scale_x;
|
||||
item.scale_y = sle.scale_y;
|
||||
item.angular_size = sle.angular_size;
|
||||
|
||||
// this is necessary when modifying bitmaps, but not when modifying suns
|
||||
if (!is_sun)
|
||||
@@ -2512,13 +2670,7 @@ int stars_add_sun_entry(starfield_list_entry *sun_ptr)
|
||||
Assert(sun_ptr != NULL);
|
||||
|
||||
// copy information
|
||||
sbi.ang.p = sun_ptr->ang.p;
|
||||
sbi.ang.b = sun_ptr->ang.b;
|
||||
sbi.ang.h = sun_ptr->ang.h;
|
||||
sbi.scale_x = sun_ptr->scale_x;
|
||||
sbi.scale_y = sun_ptr->scale_y;
|
||||
sbi.div_x = sun_ptr->div_x;
|
||||
sbi.div_y = sun_ptr->div_y;
|
||||
starfield_copy_entry_to_instance(sun_ptr, sbi);
|
||||
|
||||
int idx = stars_find_sun(sun_ptr->filename);
|
||||
|
||||
@@ -2608,13 +2760,7 @@ int stars_add_bitmap_entry(starfield_list_entry *sle)
|
||||
Assert(sle != NULL);
|
||||
|
||||
// copy information
|
||||
sbi.ang.p = sle->ang.p;
|
||||
sbi.ang.b = sle->ang.b;
|
||||
sbi.ang.h = sle->ang.h;
|
||||
sbi.scale_x = sle->scale_x;
|
||||
sbi.scale_y = sle->scale_y;
|
||||
sbi.div_x = sle->div_x;
|
||||
sbi.div_y = sle->div_y;
|
||||
starfield_copy_entry_to_instance(sle, sbi);
|
||||
|
||||
idx = stars_find_bitmap(sle->filename);
|
||||
|
||||
@@ -2876,13 +3022,7 @@ void stars_modify_entry_FRED(int index, const char *name, starfield_list_entry *
|
||||
Assert( sbi_new != NULL );
|
||||
|
||||
// copy information
|
||||
sbi.ang.p = sbi_new->ang.p;
|
||||
sbi.ang.b = sbi_new->ang.b;
|
||||
sbi.ang.h = sbi_new->ang.h;
|
||||
sbi.scale_x = sbi_new->scale_x;
|
||||
sbi.scale_y = sbi_new->scale_y;
|
||||
sbi.div_x = sbi_new->div_x;
|
||||
sbi.div_y = sbi_new->div_y;
|
||||
starfield_copy_entry_to_instance(sbi_new, sbi);
|
||||
|
||||
if (is_a_sun) {
|
||||
idx = stars_find_sun((char*)name);
|
||||
|
||||
@@ -29,11 +29,35 @@
|
||||
|
||||
|
||||
// starfield list
|
||||
// A sun's apparent diameter, in degrees. Every layer that can supply one -- the mission file's
|
||||
// +AngularSize:, stars.tbl's $SunAngularSize:, and the LabUi session override -- uses this same
|
||||
// encoding, so "the first one that specifies a size wins" is all the precedence logic there is
|
||||
// (see sun_angular_radius_tangent() in starfield.cpp). Negative means that layer doesn't specify
|
||||
// a size; 0 asks for hard shadows outright. Only affects raytraced shadows.
|
||||
constexpr float SUN_ANGULAR_SIZE_UNSPECIFIED = -1.0f;
|
||||
|
||||
// Sol's apparent diameter from Earth -- the one figure anyone has an intuition for, so it's what
|
||||
// the editors and the lab start from when switching a sun's size on.
|
||||
constexpr float SUN_ANGULAR_SIZE_SOL = 0.53f;
|
||||
|
||||
// Well past anything plausible; the cap only exists to keep the value finite and non-negative.
|
||||
constexpr float SUN_ANGULAR_SIZE_MAX = 90.0f;
|
||||
|
||||
typedef struct starfield_list_entry {
|
||||
char filename[MAX_FILENAME_LEN]; // bitmap filename
|
||||
float scale_x, scale_y; // x and y scale
|
||||
int div_x, div_y; // # of x and y divisions
|
||||
angles ang; // angles from FRED
|
||||
float angular_size; // only for suns; see SUN_ANGULAR_SIZE_UNSPECIFIED
|
||||
|
||||
starfield_list_entry() : scale_x(1.0f), scale_y(1.0f), div_x(1), div_y(1),
|
||||
angular_size(SUN_ANGULAR_SIZE_UNSPECIFIED)
|
||||
{
|
||||
filename[0] = '\0';
|
||||
ang.p = 0.0f;
|
||||
ang.b = 0.0f;
|
||||
ang.h = 0.0f;
|
||||
}
|
||||
} starfield_list_entry;
|
||||
|
||||
// backgrounds
|
||||
@@ -64,6 +88,11 @@ extern float Nmodel_alpha;
|
||||
extern bool Motion_debris_override;
|
||||
extern bool Motion_debris_enabled;
|
||||
|
||||
// Session-only override (LabUi) of every sun's $SunAngularSize, in degrees of
|
||||
// apparent diameter -- sizes the penumbra of raytraced sun shadows. Negative
|
||||
// (the default) means no override: each sun uses its stars.tbl value.
|
||||
extern float Sun_angular_size_override;
|
||||
|
||||
struct motion_debris_bitmaps {
|
||||
int bm;
|
||||
int nframes;
|
||||
|
||||
@@ -0,0 +1,222 @@
|
||||
#include "starfield/sun_disc.h"
|
||||
|
||||
#include "bmpman/bmpman.h"
|
||||
#include "cfile/cfile.h"
|
||||
#include "ddsutils/ddsutils.h"
|
||||
#include "math/floating.h"
|
||||
#include "starfield/starfield.h"
|
||||
|
||||
#include <cmath>
|
||||
|
||||
// The sun quad is drawn with rad = 0.05f * scale_x (see stars_draw_sun()), and
|
||||
// g3_render_rect_screen_aligned_2d() sizes it so that rad is exactly the tangent of the
|
||||
// half-angle it subtends, independent of FOV, resolution and aspect ratio.
|
||||
static const float Sun_quad_tan_half_angle = 0.05f;
|
||||
|
||||
// How much of that drawn size to believe. The drawn sun is an art decision rather than a
|
||||
// physical one: measured across the sun art of retail, the MediaVPs and several mods, the
|
||||
// emitting disc subtends a median of ~3.9x Sol's 0.53 degrees once the mission's +Scale: is
|
||||
// applied, and up to 26x in the worst retail mission. Taken literally that gives shadow edges
|
||||
// tens of metres wide. This scales the measurement so that median MediaVPs content -- what
|
||||
// nearly all modern mods ship -- lands near Sol; installs running retail's chunkier sun art
|
||||
// land about twice as soft.
|
||||
static const float Sun_derived_size_calibration = 0.25f;
|
||||
|
||||
// Ceiling on a derived sun's apparent diameter, in degrees. Missions scale their suns freely
|
||||
// (retail's own missions go up to +Scale: 5.0), and penumbra width also decides how many rays
|
||||
// are needed to resolve it without grain -- shadows_rt_sample_count() caps at
|
||||
// RT_SHADOW_MAX_SAMPLES.
|
||||
static const float Sun_derived_max_diameter = 1.5f;
|
||||
|
||||
float sun_disc_tangent_from_diameter(float degrees)
|
||||
{
|
||||
return tanf(fl_radians(degrees) * 0.5f);
|
||||
}
|
||||
|
||||
float sun_disc_tangent_from_fraction(float disc_fraction, float scale_x)
|
||||
{
|
||||
if (disc_fraction <= 0.0f) {
|
||||
return 0.0f;
|
||||
}
|
||||
|
||||
return MIN(Sun_quad_tan_half_angle * scale_x * disc_fraction * Sun_derived_size_calibration,
|
||||
sun_disc_tangent_from_diameter(Sun_derived_max_diameter));
|
||||
}
|
||||
|
||||
void sun_disc_build_weights(const ubyte* pixels, int width, int height, int bytes_per_pixel, bool has_alpha,
|
||||
SCP_vector<ubyte>& out_weights)
|
||||
{
|
||||
Assertion(pixels != nullptr, "sun_disc_build_weights() called with no pixel data!");
|
||||
Assertion(bytes_per_pixel == 3 || bytes_per_pixel == 4, "sun_disc_build_weights() only handles BGR and BGRA, got %d bytes per pixel!", bytes_per_pixel);
|
||||
|
||||
if (width <= 0 || height <= 0) {
|
||||
out_weights.clear();
|
||||
return;
|
||||
}
|
||||
|
||||
const size_t num_pixels = static_cast<size_t>(width) * static_cast<size_t>(height);
|
||||
out_weights.resize(num_pixels);
|
||||
|
||||
const bool use_alpha = has_alpha && (bytes_per_pixel == 4);
|
||||
|
||||
for (size_t i = 0; i < num_pixels; i++) {
|
||||
const ubyte* px = pixels + (i * static_cast<size_t>(bytes_per_pixel));
|
||||
|
||||
// channel order doesn't matter to the maximum, so this works for both BGR and RGB
|
||||
ubyte weight = MAX(px[0], MAX(px[1], px[2]));
|
||||
|
||||
if (use_alpha) {
|
||||
weight = static_cast<ubyte>((static_cast<int>(weight) * static_cast<int>(px[3])) / 255);
|
||||
}
|
||||
|
||||
out_weights[i] = weight;
|
||||
}
|
||||
}
|
||||
|
||||
void sun_disc_build_weights_16(const ubyte* pixels, int width, int height, bool has_alpha,
|
||||
SCP_vector<ubyte>& out_weights)
|
||||
{
|
||||
Assertion(pixels != nullptr, "sun_disc_build_weights_16() called with no pixel data!");
|
||||
|
||||
if (width <= 0 || height <= 0) {
|
||||
out_weights.clear();
|
||||
return;
|
||||
}
|
||||
|
||||
const size_t num_pixels = static_cast<size_t>(width) * static_cast<size_t>(height);
|
||||
out_weights.resize(num_pixels);
|
||||
|
||||
// bm_get_components() reads through whichever format bmpman currently has selected, and
|
||||
// bm_load_image_data() leaves that on the screen format, so point it at the texture format
|
||||
// for the duration -- the same select/restore bmpman does around its own loaders
|
||||
BM_SELECT_TEX_FORMAT();
|
||||
|
||||
for (size_t i = 0; i < num_pixels; i++) {
|
||||
ubyte r = 0;
|
||||
ubyte g = 0;
|
||||
ubyte b = 0;
|
||||
ubyte a = 1;
|
||||
|
||||
bm_get_components(const_cast<ubyte*>(pixels + (i * 2)), &r, &g, &b, has_alpha ? &a : nullptr);
|
||||
|
||||
// the texture format only carries one bit of alpha (Gr_t_alpha), and bm_get_components()
|
||||
// hands it back as 0 or 1 rather than 0-255, so it masks rather than scales. The colour
|
||||
// channels are 5 bits each, so weights off this path are quantized to 1/31 -- coarser
|
||||
// than the 8-bit paths, but far finer than the measurement needs.
|
||||
out_weights[i] = (has_alpha && (a == 0)) ? static_cast<ubyte>(0) : MAX(r, MAX(g, b));
|
||||
}
|
||||
|
||||
BM_SELECT_SCREEN_FORMAT();
|
||||
}
|
||||
|
||||
float sun_disc_fraction_from_weights(const ubyte* weights, int width, int height)
|
||||
{
|
||||
if ((weights == nullptr) || (width <= 0) || (height <= 0)) {
|
||||
return 0.0f;
|
||||
}
|
||||
|
||||
const size_t num_pixels = static_cast<size_t>(width) * static_cast<size_t>(height);
|
||||
|
||||
ubyte peak = 0;
|
||||
for (size_t i = 0; i < num_pixels; i++) {
|
||||
if (weights[i] > peak) {
|
||||
peak = weights[i];
|
||||
}
|
||||
}
|
||||
|
||||
// blank or near-blank art has no disc to measure
|
||||
if (peak < SUN_DISC_MIN_PEAK_WEIGHT) {
|
||||
return 0.0f;
|
||||
}
|
||||
|
||||
const auto cutoff = static_cast<ubyte>(std::ceil(SUN_DISC_THRESHOLD * static_cast<float>(peak)));
|
||||
|
||||
size_t disc_pixels = 0;
|
||||
for (size_t i = 0; i < num_pixels; i++) {
|
||||
if (weights[i] >= cutoff) {
|
||||
disc_pixels++;
|
||||
}
|
||||
}
|
||||
|
||||
if (disc_pixels == 0) {
|
||||
return 0.0f;
|
||||
}
|
||||
|
||||
const float radius = fl_sqrt(static_cast<float>(disc_pixels) / PI);
|
||||
const float half_edge = 0.5f * static_cast<float>(MIN(width, height));
|
||||
|
||||
// a bitmap lit all the way into its corners measures slightly over 1; the disc still
|
||||
// can't be bigger than the quad it is drawn on
|
||||
return MIN(radius / half_edge, 1.0f);
|
||||
}
|
||||
|
||||
float sun_disc_measure_bitmap(int bitmap_handle)
|
||||
{
|
||||
if (bitmap_handle < 0) {
|
||||
return -1.0f;
|
||||
}
|
||||
|
||||
const bool has_alpha = bm_has_alpha_channel(bitmap_handle);
|
||||
|
||||
int width = 0;
|
||||
int height = 0;
|
||||
SCP_vector<ubyte> weights;
|
||||
|
||||
if (bm_is_compressed(bitmap_handle)) {
|
||||
// bm_lock() hands back block-compressed data as-is whenever the renderer supports
|
||||
// S3TC/BPTC (see bm_lock_dds()), so this path has to go around bmpman and decompress
|
||||
// the file itself. Sun art really does ship compressed, so it isn't an edge case.
|
||||
const char* filename = bm_get_image_filename(bitmap_handle);
|
||||
|
||||
if ((filename == nullptr) || (*filename == '\0')) {
|
||||
return -1.0f;
|
||||
}
|
||||
|
||||
SCP_vector<ubyte> pixels;
|
||||
if (dds_decompress_top_mip_bgra(filename, CF_TYPE_ANY, &width, &height, pixels) != DDS_ERROR_NONE) {
|
||||
return -1.0f;
|
||||
}
|
||||
|
||||
sun_disc_build_weights(pixels.data(), width, height, 4, has_alpha, weights);
|
||||
} else {
|
||||
bitmap* bmp = bm_lock(bitmap_handle, 32, BMP_TEX_XPARENT);
|
||||
|
||||
if (bmp == nullptr) {
|
||||
return -1.0f;
|
||||
}
|
||||
|
||||
if (bmp->data == 0) {
|
||||
// bm_lock() took a reference before it failed to populate the data
|
||||
bm_unlock(bitmap_handle);
|
||||
return -1.0f;
|
||||
}
|
||||
|
||||
// The requested bpp is a request, not a guarantee: bm_lock() ignores it for JPGs (always
|
||||
// 24-bit BGR) and uncompressed DDS (whatever the file uses), and for anything already
|
||||
// paged in it returns the data in the format page-in chose -- 16-bit for textures, see
|
||||
// bm_page_in_stop(). Handle each layout rather than assuming the 32 we asked for.
|
||||
width = bmp->w;
|
||||
height = bmp->h;
|
||||
|
||||
const auto* pixels = reinterpret_cast<const ubyte*>(bmp->data);
|
||||
|
||||
if (bmp->bpp == 32) {
|
||||
sun_disc_build_weights(pixels, width, height, 4, has_alpha, weights);
|
||||
} else if (bmp->bpp == 24) {
|
||||
sun_disc_build_weights(pixels, width, height, 3, false, weights);
|
||||
} else if (bmp->bpp == 16) {
|
||||
sun_disc_build_weights_16(pixels, width, height, has_alpha, weights);
|
||||
} else {
|
||||
bm_unlock(bitmap_handle);
|
||||
return -1.0f;
|
||||
}
|
||||
|
||||
bm_unlock(bitmap_handle);
|
||||
}
|
||||
|
||||
if (weights.empty()) {
|
||||
return -1.0f;
|
||||
}
|
||||
|
||||
return sun_disc_fraction_from_weights(weights.data(), width, height);
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
#pragma once
|
||||
|
||||
#include "globalincs/pstypes.h"
|
||||
|
||||
// Measures how much of a sun bitmap is the emitting disc, as opposed to the glow/corona
|
||||
// painted around it. This is how a sun's apparent angular size is worked out when its table
|
||||
// entry doesn't give one outright, which is the usual case -- see sun_angular_radius_tangent()
|
||||
// in starfield.cpp for how the measurement becomes a shadow penumbra, and $SunAngularSize: in
|
||||
// stars.tbl for how a table opts out of it.
|
||||
|
||||
// Pixels at or above this fraction of the brightest pixel count as part of the disc.
|
||||
// The measurement is only weakly sensitive to it -- across the shipped sun art of retail,
|
||||
// the MediaVPs and several mods, moving this to 0.5 rescales every bitmap by ~1.37x while
|
||||
// preserving their order almost exactly (Spearman rho 0.96) -- so it behaves as a constant
|
||||
// factor that the calibration in starfield.cpp absorbs, not as a per-bitmap judgement call.
|
||||
constexpr float SUN_DISC_THRESHOLD = 0.9f;
|
||||
|
||||
// Sun bitmaps whose brightest pixel is dimmer than this are treated as having no disc at
|
||||
// all. Mods ship deliberately blank sun bitmaps (BtA's SunAntares*-BLANK, Blue Planet's
|
||||
// SunSolDummy) to get a light source without a visible sun, and without this floor the
|
||||
// threshold above would collapse to zero, match every pixel, and hand a sun that isn't
|
||||
// drawn at all the widest penumbra in the game.
|
||||
constexpr ubyte SUN_DISC_MIN_PEAK_WEIGHT = 8;
|
||||
|
||||
/**
|
||||
* @brief Reduces BGR(A) pixels to the per-pixel weight the disc measurement works on
|
||||
*
|
||||
* The weight is how much the texel actually contributes where the sun quad is drawn, which
|
||||
* depends on how it is blended: sun bitmaps without an alpha channel are drawn additively
|
||||
* and bitmaps with one are alpha blended (see material_determine_blend_mode()).
|
||||
*
|
||||
* @param pixels BGR or BGRA pixel data, @a width * @a height * @a bytes_per_pixel bytes
|
||||
* @param bytes_per_pixel 3 (BGR) or 4 (BGRA)
|
||||
* @param has_alpha whether the alpha channel is meaningful; ignored unless BGRA
|
||||
* @param[out] out_weights resized to @a width * @a height
|
||||
*/
|
||||
void sun_disc_build_weights(const ubyte* pixels, int width, int height, int bytes_per_pixel, bool has_alpha,
|
||||
SCP_vector<ubyte>& out_weights);
|
||||
|
||||
/**
|
||||
* @brief sun_disc_build_weights() for 16-bit texture-format pixels
|
||||
*
|
||||
* Bitmaps that went through bm_page_in_texture() are locked at 16 bpp (see bm_page_in_stop()),
|
||||
* so anything but a compressed DDS is likely to be in the renderer's packed 16-bit texture
|
||||
* format by the time a sun is drawn -- retail's PCX sun art included.
|
||||
*/
|
||||
void sun_disc_build_weights_16(const ubyte* pixels, int width, int height, bool has_alpha,
|
||||
SCP_vector<ubyte>& out_weights);
|
||||
|
||||
/**
|
||||
* @brief Radius of the emitting disc, as a fraction of the drawn quad's half-width
|
||||
*
|
||||
* Coverage based: the disc's area is taken as the number of pixels at or above
|
||||
* SUN_DISC_THRESHOLD of the brightest one, converted to the radius of a circle of that
|
||||
* area. That survives off-centre discs, baked-in flare spikes and the halo tail, all of
|
||||
* which a luminance-weighted radius would be dominated by.
|
||||
*
|
||||
* Normalized against the shorter edge, because that is the edge
|
||||
* g3_render_rect_screen_aligned_2d() fits the quad to.
|
||||
*
|
||||
* @return the disc radius in [0, 1], or 0 if the bitmap has no measurable disc
|
||||
*/
|
||||
float sun_disc_fraction_from_weights(const ubyte* weights, int width, int height);
|
||||
|
||||
/**
|
||||
* @brief Tangent of the angular radius for an apparent diameter in degrees
|
||||
*
|
||||
* This is the form a directional light's source_radius takes -- see traceShadowRayCone() in
|
||||
* shadows.sdr, where it sizes the shadow penumbra. 0 in, 0 out, i.e. hard shadows.
|
||||
*/
|
||||
float sun_disc_tangent_from_diameter(float degrees);
|
||||
|
||||
/**
|
||||
* @brief Same tangent, for a sun whose size is measured from its bitmap rather than given
|
||||
*
|
||||
* Turns a disc fraction into an angular size using the geometry of the drawn sun quad, then
|
||||
* applies the calibration and ceiling that keep the result usable (see the constants in
|
||||
* sun_disc.cpp for why a measured sun can't be believed literally).
|
||||
*
|
||||
* @param scale_x the mission's +Scale: for this sun instance
|
||||
* @return the tangent, or 0 for a sun with no measurable disc
|
||||
*/
|
||||
float sun_disc_tangent_from_fraction(float disc_fraction, float scale_x);
|
||||
|
||||
/**
|
||||
* @brief Measures the disc fraction of a loaded sun bitmap
|
||||
*
|
||||
* Reads the bitmap's pixels on the CPU, so this is a one-shot operation -- callers must
|
||||
* cache the result rather than calling it per frame. For animated sun bitmaps this measures
|
||||
* the first frame, which is the handle bm_load_animation() returns.
|
||||
*
|
||||
* @return the disc fraction, or a negative value if the bitmap could not be read
|
||||
*/
|
||||
float sun_disc_measure_bitmap(int bitmap_handle);
|
||||
@@ -63,6 +63,8 @@ bg_bitmap_dlg::bg_bitmap_dlg(CWnd* pParent) : CDialog(bg_bitmap_dlg::IDD, pParen
|
||||
s_bank = 0.f;
|
||||
s_heading = 0.f;
|
||||
s_scale = 1.0f;
|
||||
s_angular_size_override = FALSE;
|
||||
s_angular_size = SUN_ANGULAR_SIZE_SOL;
|
||||
s_index = -1;
|
||||
b_pitch = 0.f;
|
||||
b_bank = 0.f;
|
||||
@@ -116,6 +118,9 @@ void bg_bitmap_dlg::DoDataExchange(CDataExchange* pDX)
|
||||
DDV_MinMaxFloat(pDX, s_heading, 0.f, DEGREE_UB);
|
||||
DDX_Text(pDX, IDC_SUN1_SCALE, s_scale);
|
||||
DDV_MinMaxFloat(pDX, s_scale, 0.1f, 50.0f);
|
||||
DDX_Check(pDX, IDC_SUN1_ANGULAR_SIZE_OVERRIDE, s_angular_size_override);
|
||||
DDX_Text(pDX, IDC_SUN1_ANGULAR_SIZE, s_angular_size);
|
||||
DDV_MinMaxFloat(pDX, s_angular_size, 0.0f, SUN_ANGULAR_SIZE_MAX);
|
||||
DDX_Text(pDX, IDC_SBITMAP, b_name);
|
||||
DDX_Text(pDX, IDC_SBITMAP_P, b_pitch);
|
||||
DDV_MinMaxFloat(pDX, b_pitch, 0.f, DEGREE_UB);
|
||||
@@ -169,6 +174,7 @@ BEGIN_MESSAGE_MAP(bg_bitmap_dlg, CDialog)
|
||||
ON_CBN_SELCHANGE(IDC_NEB2_TEXTURE, OnSelchangeNeb2Texture)
|
||||
ON_WM_HSCROLL()
|
||||
ON_LBN_SELCHANGE(IDC_SUN1_LIST, OnSunChange)
|
||||
ON_BN_CLICKED(IDC_SUN1_ANGULAR_SIZE_OVERRIDE, OnSunAngularSizeOverride)
|
||||
ON_BN_CLICKED(IDC_ADD_SUN, OnAddSun)
|
||||
ON_BN_CLICKED(IDC_DEL_SUN, OnDelSun)
|
||||
ON_CBN_SELCHANGE(IDC_SUN1, OnSunDropdownChange)
|
||||
@@ -193,6 +199,7 @@ BEGIN_MESSAGE_MAP(bg_bitmap_dlg, CDialog)
|
||||
ON_EN_KILLFOCUS(IDC_SUN1_H, OnKillfocusSun1H)
|
||||
ON_EN_KILLFOCUS(IDC_SUN1_B, OnKillfocusSun1B)
|
||||
ON_EN_KILLFOCUS(IDC_SUN1_SCALE, OnKillfocusSun1Scale)
|
||||
ON_EN_KILLFOCUS(IDC_SUN1_ANGULAR_SIZE, OnKillfocusSun1AngularSize)
|
||||
ON_BN_CLICKED(IDC_ADD_BACKGROUND, OnAddBackground)
|
||||
ON_BN_CLICKED(IDC_REMOVE_BACKGROUND, OnRemoveBackground)
|
||||
ON_BN_CLICKED(IDC_IMPORT_BACKGROUND, OnImportBackground)
|
||||
@@ -794,6 +801,10 @@ void bg_bitmap_dlg::sun_data_init()
|
||||
clb->SetCurSel(0);
|
||||
OnSunChange();
|
||||
}
|
||||
else
|
||||
{
|
||||
update_sun_angular_size_enabled();
|
||||
}
|
||||
}
|
||||
|
||||
void bg_bitmap_dlg::sun_data_close()
|
||||
@@ -822,6 +833,7 @@ void bg_bitmap_dlg::sun_data_save_current()
|
||||
sle->scale_y = 1.0f;
|
||||
sle->div_x = 1;
|
||||
sle->div_y = 1;
|
||||
sle->angular_size = s_angular_size_override ? s_angular_size : SUN_ANGULAR_SIZE_UNSPECIFIED;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -846,6 +858,11 @@ void bg_bitmap_dlg::OnSunChange()
|
||||
s_heading = fl_degrees_100ths(sle->ang.h);
|
||||
s_scale = sle->scale_x;
|
||||
|
||||
// an unset angular size leaves the edit box showing Sol's, so that ticking the
|
||||
// checkbox starts somewhere sensible rather than at whatever was last selected
|
||||
s_angular_size_override = (sle->angular_size >= 0.0f) ? TRUE : FALSE;
|
||||
s_angular_size = (sle->angular_size >= 0.0f) ? sle->angular_size : SUN_ANGULAR_SIZE_SOL;
|
||||
|
||||
// make sure angles are in the 0-359 degree range;
|
||||
// an angle of 6.28318310, which is less than 6.28318548,
|
||||
// is converted to 359.999847, which (if converted to int) is rounded to 360
|
||||
@@ -862,6 +879,8 @@ void bg_bitmap_dlg::OnSunChange()
|
||||
((CComboBox*) GetDlgItem(IDC_SUN1))->SetCurSel(drop_index);
|
||||
}
|
||||
|
||||
update_sun_angular_size_enabled();
|
||||
|
||||
// refresh the background
|
||||
stars_load_background(get_active_background());
|
||||
}
|
||||
@@ -1342,6 +1361,32 @@ void bg_bitmap_dlg::OnKillfocusSun1Scale()
|
||||
OnSunChange();
|
||||
}
|
||||
|
||||
void bg_bitmap_dlg::OnKillfocusSun1AngularSize()
|
||||
{
|
||||
if (s_index < 0) return;
|
||||
get_data_float(IDC_SUN1_ANGULAR_SIZE, &s_angular_size, 0.0f, SUN_ANGULAR_SIZE_MAX, 3);
|
||||
OnSunChange();
|
||||
}
|
||||
|
||||
void bg_bitmap_dlg::OnSunAngularSizeOverride()
|
||||
{
|
||||
UpdateData(TRUE);
|
||||
update_sun_angular_size_enabled();
|
||||
sun_data_save_current();
|
||||
}
|
||||
|
||||
// the size only means anything when the mission is actually setting one
|
||||
void bg_bitmap_dlg::update_sun_angular_size_enabled()
|
||||
{
|
||||
CWnd *size_edit = GetDlgItem(IDC_SUN1_ANGULAR_SIZE);
|
||||
if (size_edit != nullptr)
|
||||
size_edit->EnableWindow((s_index >= 0) && s_angular_size_override);
|
||||
|
||||
CWnd *size_check = GetDlgItem(IDC_SUN1_ANGULAR_SIZE_OVERRIDE);
|
||||
if (size_check != nullptr)
|
||||
size_check->EnableWindow(s_index >= 0);
|
||||
}
|
||||
|
||||
void bg_bitmap_dlg::OnDeltaposSkyboxPSpin(NMHDR* pNMHDR, LRESULT* pResult)
|
||||
{
|
||||
NM_UPDOWN* pNMUpDown = (NM_UPDOWN*)pNMHDR;
|
||||
|
||||
@@ -70,6 +70,11 @@ public:
|
||||
float s_bank;
|
||||
float s_heading;
|
||||
float s_scale;
|
||||
// Apparent diameter of this sun in degrees, and whether the mission sets one at all.
|
||||
// Unchecked leaves the sun on its stars.tbl size, or on the size measured from its
|
||||
// bitmap; see SUN_ANGULAR_SIZE_UNSPECIFIED in starfield.h.
|
||||
BOOL s_angular_size_override;
|
||||
float s_angular_size;
|
||||
int s_index;
|
||||
CString b_name;
|
||||
float b_pitch;
|
||||
@@ -116,6 +121,7 @@ protected:
|
||||
int get_active_background();
|
||||
int get_swap_background();
|
||||
void reinitialize_lists();
|
||||
void update_sun_angular_size_enabled();
|
||||
void background_flags_init();
|
||||
void background_flags_close();
|
||||
|
||||
@@ -141,6 +147,7 @@ protected:
|
||||
afx_msg void OnKillfocusNeb2FogB();
|
||||
afx_msg void OnHScroll(UINT nSBCode, UINT nPos, CScrollBar* pScrollBar);
|
||||
afx_msg void OnSunChange();
|
||||
afx_msg void OnSunAngularSizeOverride();
|
||||
afx_msg void OnAddSun();
|
||||
afx_msg void OnDelSun();
|
||||
afx_msg void OnSunDropdownChange();
|
||||
@@ -165,6 +172,7 @@ protected:
|
||||
afx_msg void OnKillfocusSun1H();
|
||||
afx_msg void OnKillfocusSun1B();
|
||||
afx_msg void OnKillfocusSun1Scale();
|
||||
afx_msg void OnKillfocusSun1AngularSize();
|
||||
afx_msg void OnAddBackground();
|
||||
afx_msg void OnRemoveBackground();
|
||||
afx_msg void OnImportBackground();
|
||||
|
||||
@@ -1808,6 +1808,8 @@ BEGIN
|
||||
EDITTEXT IDC_SUN1_H,349,87,31,14,ES_AUTOHSCROLL
|
||||
CONTROL "Spin2",IDC_SUN1_H_SPIN,"msctls_updown32",UDS_ARROWKEYS,381,87,11,14
|
||||
EDITTEXT IDC_SUN1_SCALE,362,105,31,14,ES_AUTOHSCROLL
|
||||
CONTROL "Size",IDC_SUN1_ANGULAR_SIZE_OVERRIDE,"Button",BS_AUTOCHECKBOX | WS_TABSTOP,338,123,32,10
|
||||
EDITTEXT IDC_SUN1_ANGULAR_SIZE,374,121,31,14,ES_AUTOHSCROLL
|
||||
CONTROL "Full Nebula",IDC_FULLNEB,"Button",BS_AUTOCHECKBOX | WS_TABSTOP,14,186,62,10
|
||||
EDITTEXT IDC_NEB2_INTENSITY,68,201,63,12,ES_AUTOHSCROLL
|
||||
COMBOBOX IDC_NEB2_TEXTURE,68,217,63,88,CBS_DROPDOWNLIST | WS_VSCROLL | WS_TABSTOP
|
||||
|
||||
+3
-1
@@ -1311,6 +1311,8 @@
|
||||
#define IDC_MESSAGE_MOVE_DOWN 1744
|
||||
#define IDC_MESSAGE_MOVE_TO_BOTTOM 1745
|
||||
#define IDC_INSERT_MSG 1746
|
||||
#define IDC_SUN1_ANGULAR_SIZE_OVERRIDE 1747
|
||||
#define IDC_SUN1_ANGULAR_SIZE 1748
|
||||
#define IDC_SEXP_POPUP_LIST 32770
|
||||
#define ID_FILE_MISSIONNOTES 32771
|
||||
#define ID_DUPLICATE 32774
|
||||
@@ -1639,7 +1641,7 @@
|
||||
#ifndef APSTUDIO_READONLY_SYMBOLS
|
||||
#define _APS_3D_CONTROLS 1
|
||||
#define _APS_NEXT_RESOURCE_VALUE 340
|
||||
#define _APS_NEXT_CONTROL_VALUE 1747
|
||||
#define _APS_NEXT_CONTROL_VALUE 1749
|
||||
#define _APS_NEXT_COMMAND_VALUE 33125
|
||||
#define _APS_NEXT_SYMED_VALUE 105
|
||||
#endif
|
||||
|
||||
@@ -204,8 +204,73 @@ SDLGraphicsOperations::~SDLGraphicsOperations() {
|
||||
}
|
||||
}
|
||||
|
||||
if (_vulkanLibraryLoaded) {
|
||||
SDL_Vulkan_UnloadLibrary();
|
||||
_vulkanLibraryLoaded = false;
|
||||
}
|
||||
|
||||
SDL_QuitSubSystem(SDL_INIT_VIDEO);
|
||||
}
|
||||
void* SDLGraphicsOperations::getVulkanProcAddr()
|
||||
{
|
||||
// Creating a window with SDL_WINDOW_VULKAN already loads the loader, but this must also work
|
||||
// before any such window exists, and SDL refcounts the load so the extra call is harmless.
|
||||
if (!_vulkanLibraryLoaded) {
|
||||
if (!SDL_Vulkan_LoadLibrary(nullptr)) {
|
||||
mprintf(("Failed to load the Vulkan library: %s\n", SDL_GetError()));
|
||||
return nullptr;
|
||||
}
|
||||
_vulkanLibraryLoaded = true;
|
||||
}
|
||||
|
||||
auto procAddr = reinterpret_cast<void*>(SDL_Vulkan_GetVkGetInstanceProcAddr());
|
||||
if (procAddr == nullptr) {
|
||||
mprintf(("Failed to get vkGetInstanceProcAddr: %s\n", SDL_GetError()));
|
||||
}
|
||||
|
||||
return procAddr;
|
||||
}
|
||||
bool SDLGraphicsOperations::getVulkanInstanceExtensions(SCP_vector<SCP_string>& extensions)
|
||||
{
|
||||
uint32_t count = 0;
|
||||
auto extPtr = SDL_Vulkan_GetInstanceExtensions(&count);
|
||||
|
||||
if (extPtr == nullptr) {
|
||||
mprintf(("Error in SDL_Vulkan_GetInstanceExtensions: %s\n", SDL_GetError()));
|
||||
return false;
|
||||
}
|
||||
|
||||
extensions.reserve(extensions.size() + count);
|
||||
for (uint32_t i = 0; i < count; ++i) {
|
||||
extensions.emplace_back(extPtr[i]);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
uint64_t SDLGraphicsOperations::createVulkanSurface(os::Viewport* view, void* vkInstance)
|
||||
{
|
||||
Assertion(view != nullptr, "Invalid viewport specified!");
|
||||
|
||||
// Not VK_NULL_HANDLE: this file also compiles without the Vulkan headers, where the handle type
|
||||
// comes from SDL_vulkan.h and that macro doesn't exist.
|
||||
auto surface = os::vulkan_handle_cast<VkSurfaceKHR>(0);
|
||||
if (!SDL_Vulkan_CreateSurface(view->toSDLWindow(), static_cast<VkInstance>(vkInstance), nullptr, &surface)) {
|
||||
mprintf(("Failed to create Vulkan surface: %s\n", SDL_GetError()));
|
||||
return 0;
|
||||
}
|
||||
|
||||
return os::vulkan_handle_value(surface);
|
||||
}
|
||||
void SDLGraphicsOperations::destroyVulkanSurface(void* vkInstance, uint64_t surface)
|
||||
{
|
||||
if (surface == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
SDL_Vulkan_DestroySurface(static_cast<VkInstance>(vkInstance),
|
||||
os::vulkan_handle_cast<VkSurfaceKHR>(surface),
|
||||
nullptr);
|
||||
}
|
||||
std::unique_ptr<os::Viewport> SDLGraphicsOperations::createViewport(const os::ViewPortProperties& props)
|
||||
{
|
||||
uint32_t windowflags = 0;
|
||||
|
||||
@@ -4,8 +4,9 @@
|
||||
#pragma once
|
||||
|
||||
#include "osapi/osapi.h"
|
||||
#include "osapi/vulkan_surface.h"
|
||||
|
||||
class SDLGraphicsOperations: public os::GraphicsOperations {
|
||||
class SDLGraphicsOperations: public os::GraphicsOperations, public os::VulkanSurfaceProvider {
|
||||
public:
|
||||
SDLGraphicsOperations();
|
||||
~SDLGraphicsOperations() override;
|
||||
@@ -16,6 +17,19 @@ class SDLGraphicsOperations: public os::GraphicsOperations {
|
||||
void makeOpenGLContextCurrent(os::Viewport* view, os::OpenGLContext* ctx) override;
|
||||
|
||||
std::unique_ptr<os::Viewport> createViewport(const os::ViewPortProperties& props) override;
|
||||
|
||||
os::VulkanSurfaceProvider* getVulkanSupport() override { return this; }
|
||||
|
||||
void* getVulkanProcAddr() override;
|
||||
|
||||
bool getVulkanInstanceExtensions(SCP_vector<SCP_string>& extensions) override;
|
||||
|
||||
uint64_t createVulkanSurface(os::Viewport* view, void* vkInstance) override;
|
||||
|
||||
void destroyVulkanSurface(void* vkInstance, uint64_t surface) override;
|
||||
|
||||
private:
|
||||
bool _vulkanLibraryLoaded = false;
|
||||
};
|
||||
|
||||
#endif // _SDL_GRAPHICS_OPERATIONS
|
||||
@@ -0,0 +1,10 @@
|
||||
# Module: qtfred
|
||||
|
||||
Entry-point guide for this module lives at:
|
||||
**`documentation/modules/qtfred.md`** (from repo root).
|
||||
|
||||
It covers the editor's purpose, build requirements, layout, and how it relates to
|
||||
the shared engine code in `code/`.
|
||||
|
||||
For the engine-wide architecture overview see `documentation/ARCHITECTURE.md`.
|
||||
For build/test/style conventions see the root `AGENTS.md`.
|
||||
+42
-30
@@ -42,44 +42,56 @@ functionality the DLLs will also be copied to the correct paths in the destinati
|
||||
|
||||
Rendering backend (OpenGL / Vulkan)
|
||||
------------------------------------
|
||||
qtFRED currently always initializes the OpenGL renderer, even if `-vulkan` is passed on the command line
|
||||
(`Fred_running` forces `mode = GraphicsAPI::OpenGL` in `gr_init()`, `code/graphics/2d.cpp`). This is intentional:
|
||||
qtFRED's windowing (`QtGraphicsOperations`/`QtViewport` in `qtfred/src/ui/QtGraphicsOperations.cpp`) only knows how
|
||||
to create a Qt-native (`QOpenGLWidget`-backed) render surface. Unlike the retail engine's `SDLGraphicsOperations`,
|
||||
it never creates a real `SDL_Window`, and `QtViewport::toSDLWindow()` unconditionally returns `nullptr`.
|
||||
qtFRED can render through either backend. OpenGL is still the default; pass `-vulkan` on the
|
||||
command line, or choose it in Preferences > Graphics, to use Vulkan instead. If the windowing
|
||||
implementation can't present through Vulkan (a Qt build without `QT_CONFIG(vulkan)`, or a platform
|
||||
plugin with no known surface extension), `gr_init()` logs it and falls back to OpenGL rather than
|
||||
failing (`code/graphics/2d.cpp`).
|
||||
|
||||
The Vulkan backend (`code/graphics/vulkan/`), however, was written exclusively against `SDLGraphicsOperations`'s
|
||||
windowing:
|
||||
- `VulkanRenderer::initializeInstance()` (`VulkanRendererSetup.cpp`) obtains `vkGetInstanceProcAddr` via
|
||||
`SDL_Vulkan_GetVkGetInstanceProcAddr()`, which only returns a valid pointer once SDL has loaded the Vulkan
|
||||
loader — which normally happens automatically when a window is created with the `SDL_WINDOW_VULKAN` flag
|
||||
(see `freespace2/SDLGraphicsOperations.cpp`).
|
||||
- `VulkanRenderer::initializeSurface()` creates the `VkSurfaceKHR` via `SDL_Vulkan_CreateSurface(window, ...)`,
|
||||
which likewise needs a real `SDL_Window*`.
|
||||
Vulkan presents through Qt's own Vulkan integration (`QWindow::setSurfaceType(QSurface::VulkanSurface)`
|
||||
plus `QVulkanInstance`, adopting the engine's `VkInstance`) rather than through SDL.
|
||||
`QtGraphicsOperations` implements `os::VulkanSurfaceProvider` (`code/osapi/vulkan_surface.h`), the
|
||||
interface `VulkanRenderer` uses for everything that depends on the windowing toolkit — loading the
|
||||
Vulkan loader, the required instance extensions, and `VkSurfaceKHR` creation for a given viewport.
|
||||
`SDLGraphicsOperations` implements the same interface for the game. `fred2`'s `MFCGraphicsOperations`
|
||||
does not, so `getVulkanSupport()` falls back to the `os::GraphicsOperations` base class default of
|
||||
`nullptr` there, and `-vulkan` always falls back to OpenGL in the MFC editor.
|
||||
|
||||
Since qtFRED never creates an SDL window, both calls fail: the first call aborts immediately
|
||||
(`VULKAN_HPP_DEFAULT_DISPATCHER.init()` is handed a null function pointer), and even if that were papered over,
|
||||
surface creation would fail right after.
|
||||
qtFRED presents to two independent surfaces: the main viewport, and the briefing map's own window,
|
||||
which renders on its own timer and switches between them with `gr_use_viewport()`. `VulkanRenderer`
|
||||
keeps a `VulkanPresentTarget` (surface, swap chain, framebuffers, sync objects) per viewport it has
|
||||
been asked to present to, created the first time that viewport is used and torn down when the
|
||||
viewport goes away — e.g. when the briefing editor dialog is closed, which happens against a live
|
||||
device since `BriefingEditorDialog` is built with `Qt::WA_DeleteOnClose`.
|
||||
|
||||
If you removed the `Fred_running` OpenGL override in `gr_init()` to experiment with Vulkan in the editor, this is
|
||||
the crash you'll hit. To make Vulkan actually work in qtFRED, `QtGraphicsOperations` needs a real Vulkan surface
|
||||
path, e.g. one of:
|
||||
**Known limitation:** the main viewport does not present while the briefing map's timer is driving
|
||||
the render loop (it sits on an already-acquired swap chain image the whole time). Not a regression
|
||||
and doesn't block using the backend; see that document's *Follow-up work* section.
|
||||
|
||||
1. Create a genuine (possibly hidden/embedded) `SDL_Window` with `SDL_WINDOW_VULKAN` set purely so the existing
|
||||
SDL-based Vulkan plumbing (loader + instance extensions + surface creation) keeps working unmodified, while
|
||||
still presenting through Qt.
|
||||
2. Bypass SDL for Vulkan entirely: load the Vulkan loader directly (`SDL_Vulkan_LoadLibrary(nullptr)` works
|
||||
without any window), and create the `VkSurfaceKHR` from the Qt window's native handle
|
||||
(`QWindow::winId()`/native handle APIs) using the appropriate platform extension
|
||||
(`VK_KHR_win32_surface`, `VK_KHR_xcb_surface`, `VK_KHR_wayland_surface`, etc.) instead of
|
||||
`SDL_Vulkan_CreateSurface`.
|
||||
|
||||
Either approach is a real chunk of implementation work, not a quick fix — plan for it as its own task rather than
|
||||
bundling it with unrelated changes.
|
||||
`fred2/` (the Windows MFC editor) is out of scope and stays OpenGL-only.
|
||||
|
||||
Known issues
|
||||
------------
|
||||
|
||||
### The Gamma preference does nothing on OpenGL
|
||||
Preferences > Graphics > Gamma sets `Gr_gamma`, but OpenGL only applies it inside the
|
||||
`if (Cmdline_window_res)` branch of `gr_opengl_flip()`, and `gr_init()` deliberately does not set
|
||||
`Cmdline_window_res` when `Fred_running` — so in the editor the value is stored and then ignored.
|
||||
The Vulkan backend has no such branch and does apply it.
|
||||
|
||||
This was invisible for as long as nothing looked at the value: qtFRED inherited
|
||||
`gr_set_gamma(3.0f)` verbatim from retail FRED2 (`fred2/management.cpp` still has it), where it was
|
||||
equally dead. Once the Vulkan backend started honouring it, that 3.0 became a whole-frame
|
||||
`pow(colour, 1/3)` and the viewport was visibly wrong. The default is now 1.0, which is both the
|
||||
engine's own default and what the editor has always actually rendered with.
|
||||
|
||||
Note that a default is only a default: anyone who ran an earlier build of this branch has
|
||||
`view_graphics_gamma=3` written into `qtFRED.conf` (or `%APPDATA%` on Windows) and needs to change
|
||||
it in Preferences — the stored value wins.
|
||||
|
||||
Fixing the OpenGL side means giving FRED the intermediate buffer the gamma pass reads from, which
|
||||
is exactly the `Cmdline_window_res` FRED integration work `gr_init()` already flags as unfinished.
|
||||
|
||||
### Blank/empty render viewport under Wayland
|
||||
On Linux, qtFRED's main 3D viewport can render as a completely blank panel — no starfield, no
|
||||
grid, no models, nothing — when running under a native Wayland session (confirmed on Arch Linux
|
||||
|
||||
@@ -10,7 +10,9 @@
|
||||
<h1>Preferences</h1>
|
||||
<p>Opens via <strong>File › Preferences</strong>.</p>
|
||||
<p>Controls QtFRED editor settings. Changes take effect immediately; there is no
|
||||
Apply button.</p>
|
||||
Apply button. The exception is a handful of graphics settings that the renderer
|
||||
can only read while it is starting up - those are marked below, and QtFRED
|
||||
applies them the next time it is launched.</p>
|
||||
|
||||
<h2>General</h2>
|
||||
<p>Miscellaneous editor options. Hover over each setting for a tooltip
|
||||
@@ -37,6 +39,67 @@ and <strong>Misc</strong>, which holds the object and subsystem commands and the
|
||||
remaining tools. The menu items and their shortcuts are the same in both layouts,
|
||||
and the change takes effect immediately.</p>
|
||||
|
||||
<h2>Graphics</h2>
|
||||
<p>Controls how the 3D viewport is rendered. These settings affect the editor
|
||||
only - they are stored separately from the game's own graphics options and
|
||||
never change them.</p>
|
||||
|
||||
<h3>Renderer</h3>
|
||||
<ul>
|
||||
<li><strong>Backend</strong> - the graphics API the viewport renders with,
|
||||
OpenGL or Vulkan. OpenGL is the default. Vulkan is only offered when
|
||||
this build of QtFRED was compiled with it. A <code>-vulkan</code> or
|
||||
<code>-opengl</code> command-line option always overrides this setting.
|
||||
<em>Requires a restart.</em></li>
|
||||
</ul>
|
||||
|
||||
<h3>Post-processing</h3>
|
||||
<p><strong>Enable post-processing in the viewport</strong> routes the viewport
|
||||
through the same HDR rendering pipeline the game uses, adding bloom,
|
||||
tonemapping, lightshafts, and shadows. It is off by default, so missions look
|
||||
the way they always have until you opt in. The same switch is available as
|
||||
<strong>View › Enable Post Processing</strong>.</p>
|
||||
<p>Every other setting in this section only changes what you see while
|
||||
post-processing is on, because they are all consumed by that pipeline.</p>
|
||||
|
||||
<h3>Shadows & anti-aliasing</h3>
|
||||
<ul>
|
||||
<li><strong>Shadow quality</strong> - detail level of the shadows cast by the
|
||||
mission's sun. Higher settings cost more performance. Shadows are off
|
||||
until you raise this above <em>Disabled</em>, so enabling post-processing
|
||||
alone will not produce any. <em>Requires a restart</em> - including the
|
||||
first time you turn it on.</li>
|
||||
<li><strong>Shadow method</strong> - whether shadows are rendered with
|
||||
cascaded shadow maps or hardware raytracing. Raytraced is only offered
|
||||
on the Vulkan backend, and only when your GPU and driver support it;
|
||||
the control is greyed out otherwise. Applies immediately.</li>
|
||||
<li><strong>Anti-aliasing</strong> - post-process anti-aliasing mode (FXAA or
|
||||
SMAA, in increasing quality). Applies immediately.</li>
|
||||
<li><strong>MSAA</strong> - multisample anti-aliasing for the 3D scene, off
|
||||
or 4x/8x. More expensive than the post-process modes above but higher
|
||||
quality. <em>Requires a restart.</em></li>
|
||||
</ul>
|
||||
|
||||
<h3>Textures</h3>
|
||||
<ul>
|
||||
<li><strong>Texture filtering</strong> - bilinear or trilinear mipmap
|
||||
filtering. Left alone, QtFRED uses the same default the game does.
|
||||
<em>Requires a restart.</em></li>
|
||||
<li><strong>Anisotropic filtering</strong> - sharpens textures viewed at a
|
||||
steep angle. The list is limited to the levels your GPU reports, and is
|
||||
disabled entirely if it does not support the feature. Left alone, QtFRED
|
||||
uses your hardware's maximum. <em>Requires a restart.</em></li>
|
||||
</ul>
|
||||
|
||||
<h3>Gamma</h3>
|
||||
<p>Brightness of the viewport, from 0.10 to 5.00, applied as
|
||||
<code>colour<sup>1/gamma</sup></code>. The default of 1.00 leaves the viewport
|
||||
untouched.</p>
|
||||
<p><em>This control currently only does anything when QtFRED is running on the
|
||||
Vulkan renderer.</em> On OpenGL the editor renders straight to the window
|
||||
without the intermediate buffer the gamma pass needs, so the setting is stored
|
||||
but has no visible effect.</p>
|
||||
|
||||
<h2>Grid</h2>
|
||||
<p>Configures the reference grid shown in the main viewport. You can set which
|
||||
world-space plane the grid lies on (XZ, XY, or YZ) and adjust the grid's center
|
||||
|
||||
@@ -28,6 +28,8 @@ add_file_folder("Source/Mission"
|
||||
src/mission/EditorViewport.h
|
||||
src/mission/FredRenderer.cpp
|
||||
src/mission/FredRenderer.h
|
||||
src/mission/GraphicsSettings.cpp
|
||||
src/mission/GraphicsSettings.h
|
||||
src/mission/IDialogProvider.h
|
||||
src/mission/management.cpp
|
||||
src/mission/management.h
|
||||
|
||||
@@ -123,6 +123,7 @@ EditorViewport::EditorViewport(Editor* in_editor, std::unique_ptr<FredRenderer>&
|
||||
syncMissionLayerNames();
|
||||
|
||||
loadSettings();
|
||||
view.Graphics.applyLive();
|
||||
|
||||
fredApp->runAfterInit([this]() { initialSetup(); });
|
||||
}
|
||||
@@ -185,6 +186,8 @@ void EditorViewport::loadSettings() {
|
||||
camera.setInvertOrbitY(settings.value("camera_invert_orbit_y", camera.getInvertOrbitY()).toBool());
|
||||
camera.setOrbitSelectionMidpoint(settings.value("camera_orbit_selection_midpoint", camera.getOrbitSelectionMidpoint()).toBool());
|
||||
settings.endGroup();
|
||||
|
||||
view.Graphics = GraphicsSettings::load();
|
||||
}
|
||||
|
||||
void EditorViewport::saveSettings() const {
|
||||
@@ -236,7 +239,10 @@ void EditorViewport::saveSettings() const {
|
||||
settings.setValue("camera_invert_orbit_y", camera.getInvertOrbitY());
|
||||
settings.setValue("camera_orbit_selection_midpoint", camera.getOrbitSelectionMidpoint());
|
||||
settings.endGroup();
|
||||
|
||||
view.Graphics.save();
|
||||
}
|
||||
|
||||
void EditorViewport::needsUpdate() {
|
||||
_renderer->scheduleUpdate();
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
#include "CameraController.h"
|
||||
#include "FredRenderer.h"
|
||||
#include "Editor.h"
|
||||
#include "GraphicsSettings.h"
|
||||
#include "IDialogProvider.h"
|
||||
#include "ui/ThemeMode.h"
|
||||
|
||||
@@ -64,6 +65,9 @@ struct ViewSettings {
|
||||
int Outline_lod = 1;
|
||||
float Label_font_scale = 1.0f; // multiplier applied to viewport text labels
|
||||
|
||||
//! Preferences > Graphics. Owns its own persistence and apply rules; see GraphicsSettings.
|
||||
GraphicsSettings Graphics;
|
||||
|
||||
ViewSettings();
|
||||
};
|
||||
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
#include "Editor.h"
|
||||
#include "EditorViewport.h"
|
||||
|
||||
#include <cmath>
|
||||
|
||||
#include <globalincs/alphacolors.h>
|
||||
#include <mission/missiongrid.h>
|
||||
#include <globalincs/systemvars.h>
|
||||
@@ -15,6 +17,7 @@
|
||||
#include <graphics/font.h>
|
||||
#include <graphics/matrix.h>
|
||||
#include <graphics/light.h>
|
||||
#include <graphics/shadows.h>
|
||||
#include <lighting/lighting.h>
|
||||
#include <starfield/starfield.h>
|
||||
#include <ship/ship.h>
|
||||
@@ -28,6 +31,8 @@
|
||||
#include <mod_table/mod_table.h>
|
||||
#include <cfile/cfile.h>
|
||||
|
||||
#include <optional>
|
||||
|
||||
#include "mission/object.h"
|
||||
#include "prop/prop.h"
|
||||
#include "weapon/weapon.h"
|
||||
@@ -56,6 +61,38 @@ void disable_htl() {
|
||||
gr_end_view_matrix();
|
||||
}
|
||||
|
||||
//! Scoped gr_scene_texture_begin()/end(), so the two can't drift apart as render_frame() grows.
|
||||
struct ScenePostProcessing {
|
||||
ScenePostProcessing() { gr_scene_texture_begin(); }
|
||||
~ScenePostProcessing() { gr_scene_texture_end(); }
|
||||
|
||||
ScenePostProcessing(const ScenePostProcessing&) = delete;
|
||||
ScenePostProcessing& operator=(const ScenePostProcessing&) = delete;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Render the shadow pass for the current camera.
|
||||
*
|
||||
* Only meaningful inside a ScenePostProcessing scope: the shadow pass writes into deferred
|
||||
* G-buffer surfaces that only exist while the scene texture is bound. Mirrors game_render_frame(),
|
||||
* which calls this right after its own gr_scene_texture_begin() + stars_draw().
|
||||
*
|
||||
* shadows_render_all() works on the HTL proj/view matrix stack -- the same one enable_htl() and
|
||||
* disable_htl() push and pop for the starfield -- and expects it to be open: it ends whatever is
|
||||
* active (gr_end_view_matrix() asserts on modelview_matrix_depth) and restores it when done. The
|
||||
* starfield's disable_htl() just popped that stack, hence the push here, and the matching pop
|
||||
* afterwards so the next frame's enable_htl() doesn't assert on a stack left open.
|
||||
*/
|
||||
void render_shadows() {
|
||||
gr_set_proj_matrix(Proj_fov, gr_screen.clip_aspect, Min_draw_distance, Max_draw_distance);
|
||||
gr_set_view_matrix(&Eye_position, &Eye_matrix);
|
||||
|
||||
shadows_render_all(Proj_fov, &Eye_matrix, &Eye_position, nullptr, nullptr, nullptr);
|
||||
|
||||
gr_end_proj_matrix();
|
||||
gr_end_view_matrix();
|
||||
}
|
||||
|
||||
bool fred_colors_inited = false;
|
||||
color colour_white;
|
||||
color colour_green;
|
||||
@@ -948,6 +985,16 @@ void FredRenderer::render_models(int cur_object_index) {
|
||||
bool f = false;
|
||||
enable_htl();
|
||||
|
||||
// Mirrors obj_render_queue_all()'s bind right before scene.render_all(). The model shaders
|
||||
// read rtShadowSampleCount, the RTAO params and the per-cascade smoothness out of this block,
|
||||
// but that bind lives in obj_render_queue_all() and we render through obj_render_all(), so
|
||||
// nothing here binds it for the model pass. Without it the pass runs on the zero-filled
|
||||
// fallback UBO -- a sample count of 0, which collapses traceShadowRayCone() to a single hard
|
||||
// ray and makes shadows ignore the sun's angular size entirely.
|
||||
if (Shadow_quality != ShadowQuality::Disabled) {
|
||||
shadow_cascade_params_bind(Num_cockpit_shadow_cascades, Num_shadow_cascades);
|
||||
}
|
||||
|
||||
auto render_function = [&](object* objp) {
|
||||
if (!_viewport->isObjectVisibleInLayer(objp)) {
|
||||
return;
|
||||
@@ -967,11 +1014,18 @@ void FredRenderer::render_frame(int cur_object_index,
|
||||
qreal scale)
|
||||
{
|
||||
|
||||
// Make sure our OpenGL context is used for rendering
|
||||
// Make sure our render target is the one being drawn into
|
||||
gr_use_viewport(_targetView);
|
||||
_devicePixelRatio = static_cast<float>(scale);
|
||||
uint32_t width = _targetView->getSize().first * scale;
|
||||
uint32_t height = _targetView->getSize().second * scale;
|
||||
|
||||
// Round rather than truncate: getSize() is in logical pixels, and Qt rounds when it sizes the
|
||||
// native surface from them. Truncating lands a pixel short of the real surface on fractional
|
||||
// display scaling, which leaves gr_screen (and so the viewport gr_setup_viewport derives from
|
||||
// it) disagreeing with the size the renderer is actually presenting at.
|
||||
const auto logicalSize = _targetView->getSize();
|
||||
const auto width = static_cast<uint32_t>(std::lround(logicalSize.first * scale));
|
||||
const auto height = static_cast<uint32_t>(std::lround(logicalSize.second * scale));
|
||||
|
||||
// Resize the rendering window in case the previous size was different
|
||||
gr_screen_resize(width, height);
|
||||
|
||||
@@ -995,6 +1049,15 @@ void FredRenderer::render_frame(int cur_object_index,
|
||||
|
||||
g3_set_view_matrix(&_viewport->camera.eye_pos, &_viewport->camera.eye_orient, 0.5f);
|
||||
|
||||
// Optionally run the 3D world through the game's HDR post-processing pipeline (bloom,
|
||||
// tonemapping, lightshafts, shadows) instead of drawing straight to the default framebuffer.
|
||||
// Brackets only the 3D content, the same way game_render_frame() does; the 2D overlays further
|
||||
// down (distances, ship info, tooltips) stay outside it.
|
||||
std::optional<ScenePostProcessing> postProcessing;
|
||||
if (view().Graphics.enablePostProcessing) {
|
||||
postProcessing.emplace();
|
||||
}
|
||||
|
||||
// Force max star detail so the editor always shows the full Num_stars count
|
||||
// regardless of the player's graphics quality setting (Detail.num_stars can be 0).
|
||||
int saved_detail_stars = Detail.num_stars;
|
||||
@@ -1004,6 +1067,10 @@ void FredRenderer::render_frame(int cur_object_index,
|
||||
disable_htl();
|
||||
Detail.num_stars = saved_detail_stars;
|
||||
|
||||
if (postProcessing) {
|
||||
render_shadows();
|
||||
}
|
||||
|
||||
if (view().Show_horizon) {
|
||||
gr_set_color(128, 128, 64);
|
||||
g3_draw_horizon_line();
|
||||
@@ -1020,6 +1087,8 @@ void FredRenderer::render_frame(int cur_object_index,
|
||||
render_models(cur_object_index);
|
||||
render_volumetric_overlay();
|
||||
|
||||
postProcessing.reset();
|
||||
|
||||
if (view().Show_distances) {
|
||||
display_distances();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,170 @@
|
||||
#include "mission/GraphicsSettings.h"
|
||||
|
||||
#include <cmdline/cmdline.h>
|
||||
#include <options/OptionsManager.h>
|
||||
|
||||
#include <QSettings>
|
||||
|
||||
namespace fso::fred {
|
||||
|
||||
namespace {
|
||||
|
||||
const char* SETTINGS_GROUP = "Preferences";
|
||||
|
||||
const char* KEY_BACKEND = "view_graphics_backend";
|
||||
const char* KEY_POST_PROCESSING = "view_enable_post_processing";
|
||||
const char* KEY_SHADOW_QUALITY = "view_graphics_shadow_quality";
|
||||
const char* KEY_SHADOW_METHOD = "view_graphics_shadow_method";
|
||||
const char* KEY_AA_MODE = "view_graphics_aa_mode";
|
||||
const char* KEY_MSAA_SAMPLES = "view_graphics_msaa_samples";
|
||||
const char* KEY_TEXTURE_FILTER = "view_graphics_texture_filter";
|
||||
const char* KEY_ANISOTROPY = "view_graphics_anisotropy";
|
||||
const char* KEY_GAMMA = "view_graphics_gamma";
|
||||
|
||||
// A settings file can be hand-edited or left over from a build with a different set of values, so
|
||||
// anything that ends up in an enum or a fixed value list gets checked rather than cast blindly.
|
||||
template <typename T>
|
||||
T readEnum(const QSettings& settings, const char* key, T fallback, T highest)
|
||||
{
|
||||
const int raw = settings.value(key, static_cast<int>(fallback)).toInt();
|
||||
|
||||
if (raw < 0 || raw > static_cast<int>(highest)) {
|
||||
return fallback;
|
||||
}
|
||||
|
||||
return static_cast<T>(raw);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
bool GraphicsSettings::operator==(const GraphicsSettings& rhs) const
|
||||
{
|
||||
return backend == rhs.backend && enablePostProcessing == rhs.enablePostProcessing &&
|
||||
shadowQuality == rhs.shadowQuality && shadowMethod == rhs.shadowMethod && aaMode == rhs.aaMode &&
|
||||
msaaSamples == rhs.msaaSamples && gamma == rhs.gamma && textureFilter == rhs.textureFilter &&
|
||||
anisotropy == rhs.anisotropy;
|
||||
}
|
||||
|
||||
SCP_vector<int> GraphicsSettings::validMsaaSampleCounts()
|
||||
{
|
||||
return {0, 4, 8};
|
||||
}
|
||||
|
||||
GraphicsSettings GraphicsSettings::load()
|
||||
{
|
||||
GraphicsSettings out;
|
||||
|
||||
QSettings settings;
|
||||
settings.beginGroup(SETTINGS_GROUP);
|
||||
|
||||
if (settings.contains(KEY_BACKEND)) {
|
||||
// Only OpenGL and, on a build that has it, Vulkan are valid explicit choices. Stub, a Vulkan
|
||||
// choice this build can't honour, or a garbage value from a hand-edited file all keep
|
||||
// GraphicsAPI::Default, same as an absent key -- see the field comment for why that matters.
|
||||
const auto raw = static_cast<GraphicsAPI>(settings.value(KEY_BACKEND).toInt());
|
||||
if (raw == GraphicsAPI::OpenGL) {
|
||||
out.backend = GraphicsAPI::OpenGL;
|
||||
}
|
||||
#ifdef WITH_VULKAN
|
||||
else if (raw == GraphicsAPI::Vulkan) {
|
||||
out.backend = GraphicsAPI::Vulkan;
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
out.enablePostProcessing = settings.value(KEY_POST_PROCESSING, out.enablePostProcessing).toBool();
|
||||
out.shadowQuality = readEnum(settings, KEY_SHADOW_QUALITY, out.shadowQuality, ShadowQuality::Ultra);
|
||||
out.shadowMethod =
|
||||
readEnum(settings, KEY_SHADOW_METHOD, out.shadowMethod, ShadowRenderMethod::Raytraced);
|
||||
out.aaMode = readEnum(settings, KEY_AA_MODE, out.aaMode, AntiAliasMode::SMAA_Ultra);
|
||||
out.gamma = settings.value(KEY_GAMMA, out.gamma).toFloat();
|
||||
|
||||
if (settings.contains(KEY_TEXTURE_FILTER)) {
|
||||
out.textureFilter = settings.value(KEY_TEXTURE_FILTER).toInt() != 0 ? 1 : 0;
|
||||
}
|
||||
|
||||
if (settings.contains(KEY_ANISOTROPY)) {
|
||||
out.anisotropy = settings.value(KEY_ANISOTROPY).toFloat();
|
||||
}
|
||||
|
||||
const int samples = settings.value(KEY_MSAA_SAMPLES, out.msaaSamples).toInt();
|
||||
const auto validSamples = validMsaaSampleCounts();
|
||||
if (std::find(validSamples.begin(), validSamples.end(), samples) != validSamples.end()) {
|
||||
out.msaaSamples = samples;
|
||||
}
|
||||
|
||||
settings.endGroup();
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
void GraphicsSettings::save() const
|
||||
{
|
||||
QSettings settings;
|
||||
settings.beginGroup(SETTINGS_GROUP);
|
||||
|
||||
settings.setValue(KEY_POST_PROCESSING, enablePostProcessing);
|
||||
settings.setValue(KEY_SHADOW_QUALITY, static_cast<int>(shadowQuality));
|
||||
settings.setValue(KEY_SHADOW_METHOD, static_cast<int>(shadowMethod));
|
||||
settings.setValue(KEY_AA_MODE, static_cast<int>(aaMode));
|
||||
settings.setValue(KEY_MSAA_SAMPLES, msaaSamples);
|
||||
settings.setValue(KEY_GAMMA, gamma);
|
||||
|
||||
// Don't write the sentinels back out -- an absent key is what keeps the engine default in play.
|
||||
if (backend != GraphicsAPI::Default) {
|
||||
settings.setValue(KEY_BACKEND, static_cast<int>(backend));
|
||||
}
|
||||
|
||||
if (textureFilter != NO_TEXTURE_FILTER_CHOICE) {
|
||||
settings.setValue(KEY_TEXTURE_FILTER, textureFilter);
|
||||
}
|
||||
|
||||
if (anisotropy != NO_ANISOTROPY_CHOICE) {
|
||||
settings.setValue(KEY_ANISOTROPY, anisotropy);
|
||||
}
|
||||
|
||||
settings.endGroup();
|
||||
}
|
||||
|
||||
void GraphicsSettings::applyLive() const
|
||||
{
|
||||
Gr_aa_mode = aaMode;
|
||||
gr_set_gamma(gamma);
|
||||
|
||||
// Same as Gr_aa_mode: this is what Graphics.ShadowRenderMethod's own .bind_to() does, and it is
|
||||
// harmless to set even when raytraced shadows are not actually available this session --
|
||||
// shadows_use_raytracing() re-checks shadows_raytracing_supported() before anything reads it.
|
||||
Shadow_render_method = shadowMethod;
|
||||
}
|
||||
|
||||
GraphicsSettings GraphicsSettings::applyBeforeGrInit()
|
||||
{
|
||||
const GraphicsSettings settings = load();
|
||||
|
||||
// Commandline always wins (see gr_init()'s own "Commandline ALWAYS wins" comment), so only fill
|
||||
// in the backend here if -vulkan/-opengl didn't already set it. If the user has never chosen one
|
||||
// in Preferences either, settings.backend is itself GraphicsAPI::Default, and this is a no-op --
|
||||
// gr_init() falls through to its own VideocardFs2open/OpenGL fallback exactly as it always has.
|
||||
if (Cmdline_graphics_api == GraphicsAPI::Default) {
|
||||
Cmdline_graphics_api = settings.backend;
|
||||
}
|
||||
|
||||
Cmdline_msaa_enabled = settings.msaaSamples;
|
||||
Shadow_quality = settings.shadowQuality;
|
||||
|
||||
auto* options = options::OptionsManager::instance();
|
||||
|
||||
// Overrides are in-memory only, so they steer this session's gr_init() without touching the
|
||||
// config file the game reads its own graphics settings from.
|
||||
if (settings.textureFilter != NO_TEXTURE_FILTER_CHOICE) {
|
||||
options->setOverride("Graphics.TextureFilter", std::to_string(settings.textureFilter), "qtFRED Preferences");
|
||||
}
|
||||
|
||||
if (settings.anisotropy != NO_ANISOTROPY_CHOICE) {
|
||||
options->setOverride("Graphics.Anisotropy", std::to_string(settings.anisotropy), "qtFRED Preferences");
|
||||
}
|
||||
|
||||
return settings;
|
||||
}
|
||||
|
||||
} // namespace fso::fred
|
||||
@@ -0,0 +1,146 @@
|
||||
#pragma once
|
||||
|
||||
#include <graphics/2d.h>
|
||||
#include <graphics/shadows.h>
|
||||
|
||||
namespace fso::fred {
|
||||
|
||||
/**
|
||||
* @brief qtFRED's Preferences > Graphics settings.
|
||||
*
|
||||
* Single owner of the *rules*: the QSettings keys, the defaults, and when each value can be applied
|
||||
* all live here. Two very different callers need them -- management.cpp before gr_init(), and
|
||||
* EditorViewport once the editor is up -- and previously each read QSettings with its own copy of
|
||||
* the key strings and default values.
|
||||
*
|
||||
* The values themselves are ordinary fields and have more than one writer. Preferences edits a copy
|
||||
* and writes the whole struct back on apply (PreferencesDialogModel); View > Enable Post Processing
|
||||
* flips enablePostProcessing in place (FredView::syncViewOptions). Those cannot race -- the
|
||||
* Preferences dialog is modal, and the menu action re-reads the field on every viewIdle so its
|
||||
* check state follows an edit made in the dialog. A third writer, or a modeless Preferences, would
|
||||
* break that; route it through here rather than adding another direct one.
|
||||
*
|
||||
* The settings split by *when* they can take effect, which is what the two apply functions below
|
||||
* encode:
|
||||
*
|
||||
* - applyLive() settings can be changed at any time and are re-applied whenever the user hits
|
||||
* Apply in Preferences.
|
||||
* - applyBeforeGrInit() settings are baked into GPU resources during gr_init() and cannot be
|
||||
* changed afterwards, so they are read straight out of QSettings before the editor exists.
|
||||
* Shadow quality is the sharpest example: shadow_cascade_params_init() only runs during
|
||||
* gr_init(), and only if Shadow_quality is already non-Disabled, so setting it later leaves the
|
||||
* cascade buffers unsized and crashes the next frame that renders shadows.
|
||||
*/
|
||||
struct GraphicsSettings {
|
||||
// Texture filtering and anisotropy reach the engine through the options system, whose defaults
|
||||
// are better informed than anything qtFRED could hardcode -- the user's existing config for
|
||||
// one, the hardware maximum for the other. The backend has a comparable existing default: the
|
||||
// VideocardFs2open config entry gr_init() already reads. All three therefore carry a sentinel
|
||||
// meaning "the user has not chosen one", and applyBeforeGrInit() overrides only once there is a
|
||||
// real choice.
|
||||
static constexpr int NO_TEXTURE_FILTER_CHOICE = -1;
|
||||
static constexpr float NO_ANISOTROPY_CHOICE = 0.0f;
|
||||
|
||||
/**
|
||||
* @brief Run the viewport through the game's HDR scene-texture + post-processing pipeline.
|
||||
*
|
||||
* Off by default so missions keep looking exactly as they do today unless a user opts in.
|
||||
* Everything else on this struct is only visible in the viewport while this is on.
|
||||
*/
|
||||
bool enablePostProcessing = false;
|
||||
|
||||
/**
|
||||
* @brief The renderer backend, applied before gr_init() the same way Shadow_quality is.
|
||||
*
|
||||
* GraphicsAPI::Default means "the user has not chosen one in Preferences" -- gr_init() already
|
||||
* has a fallback for that (the VideocardFs2open config entry, then OpenGL), and qtFRED must not
|
||||
* get in front of it: a user relying on that entry to launch qtFRED into Vulkan without passing
|
||||
* -vulkan every time would otherwise lose that the moment they saved any other preference, since
|
||||
* save() would start persisting an explicit OpenGL nobody chose. So this is only ever written or
|
||||
* applied once Preferences actually sets it to OpenGL or Vulkan -- see save() and
|
||||
* applyBeforeGrInit(). `-vulkan`/`-opengl` on the command line still win over an explicit choice
|
||||
* here too: applyBeforeGrInit() only sets Cmdline_graphics_api from this when parse_cmdline() left
|
||||
* it at GraphicsAPI::Default, matching the "commandline always wins" rule gr_init() enforces.
|
||||
* load() also refuses to hand back GraphicsAPI::Vulkan on a build without WITH_VULKAN, so a
|
||||
* settings file left over from a Vulkan-enabled build can't make a later build without it hit the
|
||||
* Error() in gr_init_function_pointers().
|
||||
*/
|
||||
GraphicsAPI backend = GraphicsAPI::Default;
|
||||
|
||||
ShadowQuality shadowQuality = ShadowQuality::Disabled;
|
||||
|
||||
/**
|
||||
* @brief Whether shadows are cast via cascaded shadow maps or hardware raytracing.
|
||||
*
|
||||
* A live-apply setting: unlike ShadowQuality, the engine's own Graphics.ShadowRenderMethod
|
||||
* option just binds straight to Shadow_render_method with no initial-only change_listener,
|
||||
* so applyLive() can set it directly, same as aaMode.
|
||||
*
|
||||
* Raytraced only actually renders that way when shadows_use_raytracing() agrees, which
|
||||
* requires shadows_raytracing_supported() (Vulkan plus hardware ray-query support) --
|
||||
* exactly the same check Graphics.ShadowRenderMethod's own enumerator uses to decide
|
||||
* whether to offer the value at all. So this field can hold Raytraced on a build/session
|
||||
* that can't honour it with no effect on rendering, same as the engine option it mirrors.
|
||||
* Preferences populates its combo from that option (see PreferencesDialog::initializeUi()),
|
||||
* so it reads as greyed out whenever the choice would be inert, rather than qtFRED needing
|
||||
* its own Vulkan/hardware check.
|
||||
*
|
||||
* Graphics.RTShadowQuality (Low vs. High local-light coverage) is deliberately not exposed
|
||||
* here -- not useful enough in the editor to be worth a second control.
|
||||
*/
|
||||
ShadowRenderMethod shadowMethod = ShadowRenderMethod::ShadowMap;
|
||||
|
||||
AntiAliasMode aaMode = AntiAliasMode::None;
|
||||
|
||||
int msaaSamples = 0; //!< 0 (off), 4, or 8; see validMsaaSampleCounts()
|
||||
|
||||
/**
|
||||
* @brief Viewport brightness, as the exponent in pow(colour, 1/gamma).
|
||||
*
|
||||
* 1.0 (identity, and the engine's own default) is what qtFRED has always *rendered* with,
|
||||
* whatever this said. It used to default to 3.0, copied verbatim from retail FRED2
|
||||
* (fred2/management.cpp still has the same gr_set_gamma(3.0f)), where it has never had any
|
||||
* effect: OpenGL only applies Gr_gamma inside the `if (Cmdline_window_res)` branch of
|
||||
* gr_opengl_flip(), and gr_init() deliberately does not set Cmdline_window_res for FRED.
|
||||
*
|
||||
* The Vulkan backend has no such branch -- encodeToSwapChain() always runs the gamma encode --
|
||||
* so a 3.0 default made the Vulkan viewport render every frame through pow(colour, 1/3) while
|
||||
* OpenGL rendered it untouched. Keeping the identity here is what makes the two backends agree.
|
||||
*
|
||||
* Note the control is still inert under OpenGL in the editor; see qtfred/README.md.
|
||||
*/
|
||||
float gamma = 1.0f;
|
||||
|
||||
int textureFilter = NO_TEXTURE_FILTER_CHOICE; //!< 0 = bilinear, 1 = trilinear
|
||||
float anisotropy = NO_ANISOTROPY_CHOICE; //!< 1.0 = off, otherwise a power of two
|
||||
|
||||
bool operator==(const GraphicsSettings& rhs) const;
|
||||
bool operator!=(const GraphicsSettings& rhs) const { return !(*this == rhs); }
|
||||
|
||||
/**
|
||||
* @brief The MSAA sample counts the Preferences combo offers, in combo order.
|
||||
*/
|
||||
static SCP_vector<int> validMsaaSampleCounts();
|
||||
|
||||
static GraphicsSettings load();
|
||||
void save() const;
|
||||
|
||||
/**
|
||||
* @brief Push the settings that can be changed without a restart into the engine.
|
||||
*/
|
||||
void applyLive() const;
|
||||
|
||||
/**
|
||||
* @brief Load and apply the settings that must be in place before gr_init() runs.
|
||||
*
|
||||
* The engine reads texture filtering and anisotropy through the options system, so those are
|
||||
* pushed as in-memory-only overrides rather than written out -- qtFRED must never rewrite the
|
||||
* config file the game reads its own graphics settings from.
|
||||
*
|
||||
* @return the settings that were loaded, so the caller can applyLive() them once gr_init() has
|
||||
* returned and there is a renderer to apply them to.
|
||||
*/
|
||||
static GraphicsSettings applyBeforeGrInit();
|
||||
};
|
||||
|
||||
} // namespace fso::fred
|
||||
@@ -730,6 +730,50 @@ void BackgroundEditorDialogModel::setSunScale(float v)
|
||||
refreshBackgroundPreview();
|
||||
}
|
||||
|
||||
bool BackgroundEditorDialogModel::getSunAngularSizeEnabled() const
|
||||
{
|
||||
auto* s = getActiveSun();
|
||||
if (!s)
|
||||
return false;
|
||||
|
||||
return s->angular_size >= 0.0f;
|
||||
}
|
||||
|
||||
void BackgroundEditorDialogModel::setSunAngularSizeEnabled(bool enabled)
|
||||
{
|
||||
auto* s = getActiveSun();
|
||||
if (!s)
|
||||
return;
|
||||
|
||||
modify(s->angular_size, enabled ? (s->angular_size >= 0.0f ? s->angular_size : SUN_ANGULAR_SIZE_SOL)
|
||||
: SUN_ANGULAR_SIZE_UNSPECIFIED);
|
||||
refreshBackgroundPreview();
|
||||
}
|
||||
|
||||
float BackgroundEditorDialogModel::getSunAngularSize() const
|
||||
{
|
||||
auto* s = getActiveSun();
|
||||
if (!s || s->angular_size < 0.0f)
|
||||
return SUN_ANGULAR_SIZE_SOL;
|
||||
|
||||
return s->angular_size;
|
||||
}
|
||||
|
||||
void BackgroundEditorDialogModel::setSunAngularSize(float v)
|
||||
{
|
||||
auto* s = getActiveSun();
|
||||
if (!s)
|
||||
return;
|
||||
|
||||
// don't let a value edit turn the override on by itself -- that's the checkbox's job
|
||||
if (s->angular_size < 0.0f)
|
||||
return;
|
||||
|
||||
CLAMP(v, getSunAngularSizeLimit().first, getSunAngularSizeLimit().second);
|
||||
modify(s->angular_size, v);
|
||||
refreshBackgroundPreview();
|
||||
}
|
||||
|
||||
SCP_vector<SCP_string> BackgroundEditorDialogModel::getLightningNames()
|
||||
{
|
||||
SCP_vector<SCP_string> out;
|
||||
|
||||
@@ -26,6 +26,7 @@ class BackgroundEditorDialogModel : public AbstractDialogModel {
|
||||
static std::pair<float, float> getFloatOrientLimit() { return {0.f, DEGREE_UB}; }
|
||||
static std::pair<float,float> getBitmapScaleLimit() { return {0.001f, 18.0f}; }
|
||||
static std::pair<float,float> getSunScaleLimit() { return {0.1f, 50.0f}; }
|
||||
static std::pair<float,float> getSunAngularSizeLimit() { return {0.0f, SUN_ANGULAR_SIZE_MAX}; }
|
||||
static std::pair<int, int> getDivisionLimit() { return {1, 5}; }
|
||||
static std::pair<int, int> getStarsLimit() { return {0, MAX_STARS}; }
|
||||
|
||||
@@ -82,6 +83,12 @@ class BackgroundEditorDialogModel : public AbstractDialogModel {
|
||||
void setSunHeading(float deg);
|
||||
float getSunScale() const; // uses scale_x for both x and y
|
||||
void setSunScale(float v);
|
||||
// Whether this mission sets the sun's apparent diameter at all. Off means the sun keeps
|
||||
// its stars.tbl $SunAngularSize:, or the size measured from its bitmap.
|
||||
bool getSunAngularSizeEnabled() const;
|
||||
void setSunAngularSizeEnabled(bool enabled);
|
||||
float getSunAngularSize() const; // apparent diameter in degrees
|
||||
void setSunAngularSize(float v);
|
||||
|
||||
// nebula group
|
||||
static SCP_vector<SCP_string> getLightningNames();
|
||||
|
||||
@@ -28,6 +28,7 @@ PreferencesDialogModel::PreferencesDialogModel(QObject* parent, EditorViewport*
|
||||
, _toolbarIconSize(viewport->toolbar_icon_size)
|
||||
, _outlineLod(viewport->view.Outline_lod)
|
||||
, _labelFontScale(viewport->view.Label_font_scale)
|
||||
, _graphics(viewport->view.Graphics)
|
||||
, _invertOrbitX(viewport->camera.getInvertOrbitX())
|
||||
, _invertOrbitY(viewport->camera.getInvertOrbitY())
|
||||
, _orbitSelectionMidpoint(viewport->camera.getOrbitSelectionMidpoint())
|
||||
@@ -73,6 +74,9 @@ bool PreferencesDialogModel::apply() {
|
||||
_viewport->toolbar_icon_size = _toolbarIconSize;
|
||||
_viewport->view.Outline_lod = _outlineLod;
|
||||
_viewport->view.Label_font_scale = _labelFontScale;
|
||||
_viewport->view.Graphics = _graphics;
|
||||
// Only some of these can take effect now; the rest need a restart (see GraphicsSettings).
|
||||
_viewport->view.Graphics.applyLive();
|
||||
_viewport->camera.setInvertOrbitX(_invertOrbitX);
|
||||
_viewport->camera.setInvertOrbitY(_invertOrbitY);
|
||||
_viewport->camera.setOrbitSelectionMidpoint(_orbitSelectionMidpoint);
|
||||
@@ -192,6 +196,9 @@ void PreferencesDialogModel::setOutlineLod(int value) { modify(_outlineLod, valu
|
||||
double PreferencesDialogModel::getLabelFontScale() const { return _labelFontScale; }
|
||||
void PreferencesDialogModel::setLabelFontScale(double value) { modify(_labelFontScale, static_cast<float>(value)); }
|
||||
|
||||
const GraphicsSettings& PreferencesDialogModel::getGraphics() const { return _graphics; }
|
||||
void PreferencesDialogModel::setGraphics(const GraphicsSettings& value) { modify(_graphics, value); }
|
||||
|
||||
QKeySequence PreferencesDialogModel::getControlKey(ControlAction action) const {
|
||||
auto it = _controlKeys.find(action);
|
||||
Assertion(it != _controlKeys.end(), "Unknown control action!");
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#pragma once
|
||||
|
||||
#include "mission/GraphicsSettings.h"
|
||||
#include "mission/dialogs/AbstractDialogModel.h"
|
||||
#include "ui/ControlBindings.h"
|
||||
#include "ui/ThemeMode.h"
|
||||
@@ -73,6 +74,11 @@ public:
|
||||
double getLabelFontScale() const;
|
||||
void setLabelFontScale(double value);
|
||||
|
||||
// Graphics. The dialog edits a whole GraphicsSettings rather than a field at a time; the
|
||||
// setter exists so edits still route through modify() and mark the model dirty.
|
||||
const GraphicsSettings& getGraphics() const;
|
||||
void setGraphics(const GraphicsSettings& value);
|
||||
|
||||
// Controls
|
||||
QKeySequence getControlKey(ControlAction action) const;
|
||||
void setControlKey(ControlAction action, const QKeySequence& sequence);
|
||||
@@ -119,6 +125,8 @@ private:
|
||||
int _outlineLod;
|
||||
float _labelFontScale;
|
||||
|
||||
GraphicsSettings _graphics;
|
||||
|
||||
// Controls
|
||||
std::map<ControlAction, QKeySequence> _controlKeys;
|
||||
bool _invertOrbitX;
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
|
||||
#include "object.h"
|
||||
|
||||
#include "mission/GraphicsSettings.h"
|
||||
|
||||
#include "cmdline/cmdline.h"
|
||||
|
||||
#include <asteroid/asteroid.h>
|
||||
@@ -126,9 +128,16 @@ initialize(const std::string& cfilepath, int argc, char* argv[], Editor* editor,
|
||||
// Cmdline_noglow = 1;
|
||||
Cmdline_window = 1;
|
||||
|
||||
// These have to be in place before gr_init() bakes them into GPU resources, which is well
|
||||
// before EditorViewport (and its copy of the settings) exists.
|
||||
const GraphicsSettings graphicsSettings = GraphicsSettings::applyBeforeGrInit();
|
||||
|
||||
std::unique_ptr<QtGraphicsOperations> graphicsOps(new QtGraphicsOperations(editor));
|
||||
gr_init(std::move(graphicsOps));
|
||||
gr_set_gamma(3.0f);
|
||||
|
||||
// The rest needs a live renderer. EditorViewport re-applies these once it exists, but the
|
||||
// startup screens render before that.
|
||||
graphicsSettings.applyLive();
|
||||
|
||||
io::mouse::CursorManager::get()->showCursor(false);
|
||||
|
||||
|
||||
@@ -959,6 +959,10 @@ void FredView::syncViewOptions() {
|
||||
|
||||
connectActionToViewSetting(ui->actionLighting_from_Suns, &_viewport->view.Lighting_on);
|
||||
connectActionToViewSetting(ui->actionRender_Full_Detail, &_viewport->view.FullDetail);
|
||||
// The one Preferences > Graphics setting that is also a View menu toggle. Safe to write directly:
|
||||
// nothing has to be pushed into the engine for it (render_frame() reads it each frame), and it
|
||||
// cannot fight the Preferences copy -- see the note on GraphicsSettings.
|
||||
connectActionToViewSetting(ui->actionEnable_Post_Processing, &_viewport->view.Graphics.enablePostProcessing);
|
||||
|
||||
connectActionToViewSetting(ui->actionShowDistances, &_viewport->view.Show_distances);
|
||||
|
||||
|
||||
@@ -15,8 +15,45 @@
|
||||
|
||||
#include "FredApplication.h"
|
||||
|
||||
#include <SDL3/SDL_vulkan.h>
|
||||
|
||||
#if QTFRED_HAS_VULKAN
|
||||
#include <vulkan/vulkan_core.h>
|
||||
#endif
|
||||
|
||||
namespace {
|
||||
|
||||
#if QTFRED_HAS_VULKAN
|
||||
/**
|
||||
* @brief The surface extension the current Qt platform plugin's windows need
|
||||
*
|
||||
* Deliberately derived from Qt rather than from SDL_Vulkan_GetInstanceExtensions(): SDL reports
|
||||
* what *its* video driver would need, and the two can disagree -- SDL happily picks Wayland while
|
||||
* Qt runs under XCB, which is exactly what qtFRED's own XWayland workaround asks users to do. The
|
||||
* surface comes from Qt, so the extension has to as well.
|
||||
*
|
||||
* @return The extension name, or nullptr if this platform has no known mapping
|
||||
*/
|
||||
const char* vulkanPlatformSurfaceExtension() {
|
||||
const auto platform = QGuiApplication::platformName();
|
||||
|
||||
if (platform == QLatin1String("xcb")) {
|
||||
return "VK_KHR_xcb_surface";
|
||||
}
|
||||
if (platform.startsWith(QLatin1String("wayland"))) {
|
||||
return "VK_KHR_wayland_surface";
|
||||
}
|
||||
if (platform == QLatin1String("windows")) {
|
||||
return "VK_KHR_win32_surface";
|
||||
}
|
||||
if (platform == QLatin1String("cocoa")) {
|
||||
return "VK_EXT_metal_surface";
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
#endif
|
||||
|
||||
QSurfaceFormat getSurfaceFormat(const os::ViewPortProperties& viewProps, const os::OpenGLContextAttributes& glAttrs) {
|
||||
QSurfaceFormat format;
|
||||
|
||||
@@ -63,6 +100,26 @@ QSurfaceFormat getSurfaceFormat(const os::ViewPortProperties& viewProps, const o
|
||||
namespace fso {
|
||||
namespace fred {
|
||||
|
||||
namespace {
|
||||
bool g_vulkanSurfaces = false;
|
||||
#if QTFRED_HAS_VULKAN
|
||||
// Set once QtGraphicsOperations builds the instance, cleared when it goes. Windows need to reach it
|
||||
// at construction time (see fredVulkanInstance()), which is well before they could be handed one.
|
||||
QVulkanInstance* g_vulkanInstance = nullptr;
|
||||
#endif
|
||||
} // namespace
|
||||
|
||||
bool fredUsingVulkanSurfaces() {
|
||||
return g_vulkanSurfaces;
|
||||
}
|
||||
|
||||
#if QTFRED_HAS_VULKAN
|
||||
|
||||
QVulkanInstance* fredVulkanInstance() {
|
||||
return g_vulkanInstance;
|
||||
}
|
||||
#endif
|
||||
|
||||
|
||||
QtGraphicsOperations::QtGraphicsOperations(Editor* editor) : _editor(editor) {
|
||||
if ( !SDL_InitSubSystem(SDL_INIT_VIDEO) ) {
|
||||
@@ -71,9 +128,164 @@ QtGraphicsOperations::QtGraphicsOperations(Editor* editor) : _editor(editor) {
|
||||
}
|
||||
}
|
||||
QtGraphicsOperations::~QtGraphicsOperations() {
|
||||
if (_vulkanLibraryLoaded) {
|
||||
SDL_Vulkan_UnloadLibrary();
|
||||
_vulkanLibraryLoaded = false;
|
||||
}
|
||||
|
||||
SDL_QuitSubSystem(SDL_INIT_VIDEO);
|
||||
}
|
||||
|
||||
os::VulkanSurfaceProvider* QtGraphicsOperations::getVulkanSupport() {
|
||||
#if QTFRED_HAS_VULKAN
|
||||
// gr_init() asks this before the renderer exists in order to decide whether it can honour a
|
||||
// Vulkan request, so the platform check has to live here rather than in a later step. The
|
||||
// QApplication is constructed in main() long before initialize() runs, so platformName() is
|
||||
// meaningful at this point.
|
||||
const auto* extension = vulkanPlatformSurfaceExtension();
|
||||
if (extension == nullptr) {
|
||||
mprintf(("qtFRED: no Vulkan surface extension known for Qt platform '%s'; Vulkan unavailable.\n",
|
||||
QGuiApplication::platformName().toUtf8().constData()));
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
return this;
|
||||
#else
|
||||
return nullptr;
|
||||
#endif
|
||||
}
|
||||
|
||||
void* QtGraphicsOperations::getVulkanProcAddr() {
|
||||
// SDL is still the right thing to load the loader with: libvulkan is windowing-system
|
||||
// agnostic, so nothing here depends on SDL's video driver matching Qt's platform plugin --
|
||||
// only the surface extension does, and that comes from Qt (see vulkanPlatformSurfaceExtension).
|
||||
// SDL's per-platform library naming is well tested; ours would not be.
|
||||
if (!_vulkanLibraryLoaded) {
|
||||
if (!SDL_Vulkan_LoadLibrary(nullptr)) {
|
||||
mprintf(("qtFRED: failed to load the Vulkan library: %s\n", SDL_GetError()));
|
||||
return nullptr;
|
||||
}
|
||||
_vulkanLibraryLoaded = true;
|
||||
}
|
||||
|
||||
auto procAddr = reinterpret_cast<void*>(SDL_Vulkan_GetVkGetInstanceProcAddr());
|
||||
if (procAddr == nullptr) {
|
||||
mprintf(("qtFRED: failed to get vkGetInstanceProcAddr: %s\n", SDL_GetError()));
|
||||
}
|
||||
|
||||
return procAddr;
|
||||
}
|
||||
|
||||
bool QtGraphicsOperations::getVulkanInstanceExtensions(SCP_vector<SCP_string>& extensions) {
|
||||
#if QTFRED_HAS_VULKAN
|
||||
const auto* platformExtension = vulkanPlatformSurfaceExtension();
|
||||
if (platformExtension == nullptr) {
|
||||
return false;
|
||||
}
|
||||
|
||||
extensions.emplace_back(VK_KHR_SURFACE_EXTENSION_NAME);
|
||||
extensions.emplace_back(platformExtension);
|
||||
|
||||
mprintf(("qtFRED: Vulkan surface extension for Qt platform '%s': %s\n",
|
||||
QGuiApplication::platformName().toUtf8().constData(),
|
||||
platformExtension));
|
||||
|
||||
return true;
|
||||
#else
|
||||
(void)extensions;
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
uint64_t QtGraphicsOperations::createVulkanSurface(os::Viewport* view, void* vkInstance) {
|
||||
#if QTFRED_HAS_VULKAN
|
||||
auto* qtView = dynamic_cast<QtSurfaceViewport*>(view);
|
||||
if (qtView == nullptr) {
|
||||
mprintf(("qtFRED: cannot create a Vulkan surface for a non-Qt viewport.\n"));
|
||||
return 0;
|
||||
}
|
||||
|
||||
auto* window = dynamic_cast<QWindow*>(qtView->getRenderSurface());
|
||||
if (window == nullptr) {
|
||||
mprintf(("qtFRED: render surface is not a QWindow, cannot create a Vulkan surface.\n"));
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (!_vulkanInstance) {
|
||||
_vulkanInstance.reset(new QVulkanInstance());
|
||||
_vulkanInstance->setVkInstance(static_cast<VkInstance>(vkInstance));
|
||||
|
||||
if (!_vulkanInstance->create()) {
|
||||
mprintf(("qtFRED: QVulkanInstance::create() failed with error %d.\n",
|
||||
_vulkanInstance->errorCode()));
|
||||
_vulkanInstance.reset();
|
||||
return 0;
|
||||
}
|
||||
|
||||
g_vulkanInstance = _vulkanInstance.get();
|
||||
}
|
||||
|
||||
// A window inside QWidget::createWindowContainer() cannot be rebuilt here: destroy() takes the
|
||||
// container's embedding with it and the window is never composited into the widget again, which
|
||||
// presents as a permanently blank panel rather than as an error. Windows that can be presented
|
||||
// to therefore set their surface type and instance in their own constructor (see
|
||||
// fredVulkanInstance()); all that is left to do here is realize it.
|
||||
if (window->surfaceType() != QSurface::VulkanSurface) {
|
||||
mprintf(("qtFRED: window was built as surface type %d rather than VulkanSurface; it cannot be "
|
||||
"presented to.\n", static_cast<int>(window->surfaceType())));
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (window->vulkanInstance() == nullptr) {
|
||||
window->setVulkanInstance(_vulkanInstance.get());
|
||||
}
|
||||
|
||||
if (window->handle() == nullptr) {
|
||||
window->create();
|
||||
}
|
||||
|
||||
const auto surface = QVulkanInstance::surfaceForWindow(window);
|
||||
if (surface == VK_NULL_HANDLE) {
|
||||
mprintf(("qtFRED: QVulkanInstance::surfaceForWindow() failed.\n"));
|
||||
return 0;
|
||||
}
|
||||
|
||||
const auto handle = os::vulkan_handle_value(surface);
|
||||
_vulkanSurfaceWindows[handle] = window;
|
||||
|
||||
return handle;
|
||||
#else
|
||||
(void)view;
|
||||
(void)vkInstance;
|
||||
return 0;
|
||||
#endif
|
||||
}
|
||||
|
||||
void QtGraphicsOperations::destroyVulkanSurface(void* vkInstance, uint64_t surface) {
|
||||
#if QTFRED_HAS_VULKAN
|
||||
(void)vkInstance;
|
||||
|
||||
const auto entry = _vulkanSurfaceWindows.find(surface);
|
||||
if (entry == _vulkanSurfaceWindows.end()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Qt owns the surface -- it is destroyed along with the native window, and calling
|
||||
// vkDestroySurfaceKHR on it ourselves would double-free. Destroying the native window here is
|
||||
// what releases it, and it has to happen now: qtFRED's shutdown() runs gr_close() (which
|
||||
// destroys the VkInstance) before os_cleanup() frees the viewports, so leaving it to the
|
||||
// window's own destructor would tear the surface down against a dead instance.
|
||||
if (entry->second != nullptr) {
|
||||
entry->second->destroy();
|
||||
}
|
||||
|
||||
_vulkanSurfaceWindows.erase(entry);
|
||||
#else
|
||||
(void)vkInstance;
|
||||
(void)surface;
|
||||
#endif
|
||||
}
|
||||
|
||||
std::unique_ptr<os::OpenGLContext>
|
||||
QtGraphicsOperations::createOpenGLContext(os::Viewport* viewport, const os::OpenGLContextAttributes& gl_attrs) {
|
||||
auto qtPort = static_cast<QtViewport*>(viewport);
|
||||
@@ -114,8 +326,18 @@ QtViewport::~QtViewport() {
|
||||
}
|
||||
|
||||
std::unique_ptr<os::Viewport> QtGraphicsOperations::createViewport(const os::ViewPortProperties& props) {
|
||||
// Has to be set before the first window is constructed: a presentable window picks its surface
|
||||
// type in its own constructor and cannot change it afterwards.
|
||||
g_vulkanSurfaces = props.enable_vulkan;
|
||||
|
||||
std::unique_ptr<FredView> mw(new FredView());
|
||||
mw->getRenderWidget()->setSurfaceFormat(getSurfaceFormat(props, props.gl_attributes));
|
||||
|
||||
// Under Vulkan the window must not be realized yet: its surface type and QVulkanInstance have
|
||||
// to be set first, and neither is known until the renderer has built the instance -- which
|
||||
// happens after this call. createVulkanSurface() finishes the job.
|
||||
if (!props.enable_vulkan) {
|
||||
mw->getRenderWidget()->setSurfaceFormat(getSurfaceFormat(props, props.gl_attributes));
|
||||
}
|
||||
|
||||
auto viewPtr = mw.get();
|
||||
auto view = std::unique_ptr<os::Viewport>(new QtViewport(std::move(mw), props));
|
||||
@@ -141,6 +363,12 @@ std::pair<uint32_t, uint32_t> QtViewport::getSize() {
|
||||
return std::make_pair((uint32_t) size.width(), (uint32_t) size.height());
|
||||
}
|
||||
void QtViewport::swapBuffers() {
|
||||
if (_viewProps.enable_vulkan) {
|
||||
// Presentation is the swap chain's job, inside VulkanRenderer::flip(). There is no current
|
||||
// QOpenGLContext to ask, so this would be a null dereference rather than a no-op.
|
||||
return;
|
||||
}
|
||||
|
||||
auto qSurf = dynamic_cast<QWindow*>(_viewportWindow->getRenderSurface());
|
||||
if (qSurf && qSurf->isExposed()) {
|
||||
QOpenGLContext::currentContext()->swapBuffers(qSurf);
|
||||
|
||||
@@ -1,15 +1,53 @@
|
||||
#pragma once
|
||||
|
||||
#include <osapi/osapi.h>
|
||||
#include <osapi/vulkan_surface.h>
|
||||
#include "mission/Editor.h"
|
||||
#include "FredView.h"
|
||||
|
||||
#define WIN32_LEAN_AND_MEAN
|
||||
#include <QtGui/qtguiglobal.h>
|
||||
#include <QtGui/QOpenGLContext>
|
||||
|
||||
// Vulkan needs both halves: the engine's backend has to be compiled in, and Qt has to have been
|
||||
// built with Vulkan support (it is a compile-time feature there, so the header itself is absent
|
||||
// otherwise).
|
||||
#if defined(WITH_VULKAN) && QT_CONFIG(vulkan)
|
||||
#define QTFRED_HAS_VULKAN 1
|
||||
#include <QtCore/QPointer>
|
||||
#include <QtGui/QVulkanInstance>
|
||||
#else
|
||||
#define QTFRED_HAS_VULKAN 0
|
||||
#endif
|
||||
|
||||
namespace fso {
|
||||
namespace fred {
|
||||
|
||||
/**
|
||||
* @brief Whether qtFRED's presentable windows must be built as Vulkan surfaces
|
||||
*
|
||||
* Set from ViewPortProperties::enable_vulkan when the renderer asks for its viewport, which is
|
||||
* before any window that can be presented to is constructed.
|
||||
*/
|
||||
bool fredUsingVulkanSurfaces();
|
||||
|
||||
#if QTFRED_HAS_VULKAN
|
||||
/**
|
||||
* @brief The QVulkanInstance qtFRED's Vulkan surfaces are created against, or nullptr
|
||||
*
|
||||
* A QWindow has to know both its surface type and its instance before it is first realized, and a
|
||||
* window living inside QWidget::createWindowContainer() cannot be un-realized and rebuilt later:
|
||||
* destroy() takes the container's embedding with it and the window stops being composited into the
|
||||
* widget at all (which looks exactly like a render path that draws nothing). So any window that
|
||||
* might be presented to has to be constructed as a Vulkan surface from the start, which means
|
||||
* reaching the instance from outside the graphics operations.
|
||||
*
|
||||
* Valid from the first surface creation -- which happens during gr_init(), long before any dialog
|
||||
* can be opened -- until the graphics operations are destroyed.
|
||||
*/
|
||||
QVulkanInstance* fredVulkanInstance();
|
||||
#endif
|
||||
|
||||
class QtOpenGLContext: public os::OpenGLContext {
|
||||
std::unique_ptr<QOpenGLContext> _context;
|
||||
public:
|
||||
@@ -48,10 +86,25 @@ class QtViewport: public QtSurfaceViewport {
|
||||
FredView* getWindow();
|
||||
};
|
||||
|
||||
class QtGraphicsOperations: public os::GraphicsOperations {
|
||||
class QtGraphicsOperations: public os::GraphicsOperations, public os::VulkanSurfaceProvider {
|
||||
Editor* _editor = nullptr;
|
||||
|
||||
QtOpenGLContext* _lastContext = nullptr;
|
||||
|
||||
bool _vulkanLibraryLoaded = false;
|
||||
|
||||
#if QTFRED_HAS_VULKAN
|
||||
// Adopts the engine's VkInstance rather than creating one, so Qt can hand out surfaces for its
|
||||
// own windows while the renderer keeps the instance it built (with the debug-messenger and
|
||||
// validation-feature structs chained into vkCreateInstance, which QVulkanInstance can't express).
|
||||
std::unique_ptr<QVulkanInstance> _vulkanInstance;
|
||||
|
||||
// Surfaces belong to the QWindow Qt created them for, and qtFRED tears the renderer down
|
||||
// (gr_close) before the windows (os_cleanup), so we have to release them explicitly while the
|
||||
// instance is still alive. QPointer so a window that somehow went first reads as null.
|
||||
SCP_unordered_map<uint64_t, QPointer<QWindow>> _vulkanSurfaceWindows;
|
||||
#endif
|
||||
|
||||
public:
|
||||
QtGraphicsOperations(Editor* editor);
|
||||
~QtGraphicsOperations() override;
|
||||
@@ -62,6 +115,16 @@ class QtGraphicsOperations: public os::GraphicsOperations {
|
||||
void makeOpenGLContextCurrent(os::Viewport* view, os::OpenGLContext* ctx) override;
|
||||
|
||||
std::unique_ptr<os::Viewport> createViewport(const os::ViewPortProperties& props) override;
|
||||
|
||||
os::VulkanSurfaceProvider* getVulkanSupport() override;
|
||||
|
||||
void* getVulkanProcAddr() override;
|
||||
|
||||
bool getVulkanInstanceExtensions(SCP_vector<SCP_string>& extensions) override;
|
||||
|
||||
uint64_t createVulkanSurface(os::Viewport* view, void* vkInstance) override;
|
||||
|
||||
void destroyVulkanSurface(void* vkInstance, uint64_t surface) override;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
@@ -67,6 +67,8 @@ void BackgroundEditorDialog::initializeUi()
|
||||
ui->sunPitchSpin->setRange(_model->getFloatOrientLimit().first, _model->getFloatOrientLimit().second);
|
||||
ui->sunHeadingSpin->setRange(_model->getFloatOrientLimit().first, _model->getFloatOrientLimit().second);
|
||||
ui->sunScaleDoubleSpinBox->setRange(_model->getSunScaleLimit().first, _model->getSunScaleLimit().second);
|
||||
ui->sunAngularSizeDoubleSpinBox->setRange(_model->getSunAngularSizeLimit().first,
|
||||
_model->getSunAngularSizeLimit().second);
|
||||
|
||||
const auto& sun_names = _model->getAvailableSunNames();
|
||||
for (const auto& s : sun_names) {
|
||||
@@ -253,6 +255,9 @@ void BackgroundEditorDialog::updateSunControls()
|
||||
ui->sunPitchSpin->setEnabled(enabled);
|
||||
ui->sunHeadingSpin->setEnabled(enabled);
|
||||
ui->sunScaleDoubleSpinBox->setEnabled(enabled);
|
||||
ui->sunAngularSizeCheckBox->setEnabled(enabled);
|
||||
// the value only means anything when this mission is actually setting one
|
||||
ui->sunAngularSizeDoubleSpinBox->setEnabled(enabled && _model->getSunAngularSizeEnabled());
|
||||
|
||||
const int index = ui->sunSelectionCombo->findText(QString::fromStdString(_model->getSunName()));
|
||||
ui->sunSelectionCombo->setCurrentIndex(index);
|
||||
@@ -260,6 +265,8 @@ void BackgroundEditorDialog::updateSunControls()
|
||||
ui->sunPitchSpin->setValue(_model->getSunPitch());
|
||||
ui->sunHeadingSpin->setValue(_model->getSunHeading());
|
||||
ui->sunScaleDoubleSpinBox->setValue(_model->getSunScale());
|
||||
ui->sunAngularSizeCheckBox->setChecked(_model->getSunAngularSizeEnabled());
|
||||
ui->sunAngularSizeDoubleSpinBox->setValue(_model->getSunAngularSize());
|
||||
}
|
||||
|
||||
void BackgroundEditorDialog::updateNebulaControls()
|
||||
@@ -633,6 +640,24 @@ void BackgroundEditorDialog::on_sunHeadingSpin_valueChanged(double arg1)
|
||||
_model->setSunHeading(static_cast<float>(arg1));
|
||||
}
|
||||
|
||||
void BackgroundEditorDialog::on_sunAngularSizeCheckBox_toggled(bool checked)
|
||||
{
|
||||
_model->setSunAngularSizeEnabled(checked);
|
||||
|
||||
// take whatever the box is already showing, so ticking this can't quietly set a
|
||||
// different size than the one on screen
|
||||
if (checked)
|
||||
_model->setSunAngularSize(static_cast<float>(ui->sunAngularSizeDoubleSpinBox->value()));
|
||||
|
||||
// toggling this enables or greys the value box next to it
|
||||
updateSunControls();
|
||||
}
|
||||
|
||||
void BackgroundEditorDialog::on_sunAngularSizeDoubleSpinBox_valueChanged(double arg1)
|
||||
{
|
||||
_model->setSunAngularSize(static_cast<float>(arg1));
|
||||
}
|
||||
|
||||
void BackgroundEditorDialog::on_sunScaleDoubleSpinBox_valueChanged(double arg1)
|
||||
{
|
||||
_model->setSunScale(static_cast<float>(arg1));
|
||||
|
||||
@@ -49,6 +49,8 @@ private slots:
|
||||
void on_sunPitchSpin_valueChanged(double arg1);
|
||||
void on_sunHeadingSpin_valueChanged(double arg1);
|
||||
void on_sunScaleDoubleSpinBox_valueChanged(double arg1);
|
||||
void on_sunAngularSizeCheckBox_toggled(bool checked);
|
||||
void on_sunAngularSizeDoubleSpinBox_valueChanged(double arg1);
|
||||
void on_addSunButton_clicked();
|
||||
void on_changeSunButton_clicked();
|
||||
void on_deleteSunButton_clicked();
|
||||
|
||||
Loaded 100 of 111 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user