Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
745a590
chore: vendor ImPlot library
The-E Jul 8, 2026
4601f33
feat: add ImGui frame profiler overlay
The-E Jul 8, 2026
8a1f4dd
Add graphics and Vulkan debug stats to ImGui profiler overlay
The-E Jul 14, 2026
03f09ff
Update ImPlot to v1.1 WIP and improve internal consistency
The-E Jul 24, 2026
2137f4a
Refactor frame profiler to use per-category self-time accumulation
The-E Jul 24, 2026
d7ca656
Use dynamic uniform buffers to stop per-draw descriptor set churn in …
The-E Jul 24, 2026
cae0935
Refactor Vulkan descriptor set binding to reduce dynamic offset churn
The-E Jul 26, 2026
e79cc04
Enable primitive restart for supported topologies in Vulkan pipeline
The-E Jul 26, 2026
25fc6cc
Enable VK_KHR_portability_subset for compatibility
The-E Jul 26, 2026
2b08515
Replace exception with sentinel for non-dynamic Vulkan descriptor bin…
The-E Jul 26, 2026
491d27d
Add memory statistics and tracking to profiler overlay
The-E Aug 1, 2026
951facf
fix ci issues
The-E Aug 2, 2026
bf28ce8
Add RTAO and unify raytraced/shadow-mapped shadow softness on sun ang…
The-E Jul 18, 2026
77aa029
Add shadow contact hardening via PCSS blocker search
The-E Aug 7, 2026
50baf38
Size qtFRED's post-processing targets for its resizable viewport
The-E Jul 30, 2026
df88088
Add configurable graphics settings to Preferences dialog
The-E Jul 30, 2026
98dde7d
Fix texture filtering silently defaulting to bilinear
The-E Jul 30, 2026
097c9c2
Grow the scene/post render targets when the viewport does
The-E Jul 30, 2026
b24f86c
Give qtFRED's graphics preferences a single owner
The-E Jul 30, 2026
5690d3b
Document the Graphics preferences in the help that ships
The-E Jul 30, 2026
ca5ad2e
Update the design notes and help for the resize rework
The-E Jul 30, 2026
5984f06
Update Vulkan backend to support off-screen frame recycling
The-E Jul 30, 2026
a0f34c4
fix bad string in anisotropy setting and make the shadow method combo…
The-E Aug 1, 2026
25ec0bd
Carry the length of a descriptor bind's dynamic offsets
The-E Aug 7, 2026
562d2e8
Give the present target its own home
The-E Aug 7, 2026
177a4ac
Give the draw manager's descriptor-set caches one shape
The-E Aug 7, 2026
4b83843
Resolve stale uniform bindings and improve Vulkan streaming buffer al…
The-E Aug 7, 2026
0a21329
Add text-to-speech Lua API to briefing/debrief/tech room UI sections …
MjnMixael Aug 7, 2026
2863725
refactor loadout pools for vectorized ship and weapon classes
Goober5000 Aug 2, 2026
3353e88
Fix two small issues (#7690)
wookieejedi Aug 8, 2026
d3fabf0
Update the design notes and help for the resize rework
The-E Jul 30, 2026
1e532ac
Add physically-based lens flares
The-E Jul 30, 2026
4109b10
Distinguish "<none>" and "<default>", and let suns opt into the camer…
The-E Jul 30, 2026
a1c1755
Flare ship engines through the camera lens
The-E Jul 30, 2026
90aff06
Let a -lens.tbm override a lens instead of restating it
The-E Jul 27, 2026
575d4f4
Flare firing beam weapons through the camera lens
The-E Jul 30, 2026
c0fd755
Occlusion-test thruster and beam flares before queuing a draw
The-E Jul 30, 2026
8d198a4
Add per-mission camera lens overrides: aperture, anamorphic look, and…
The-E Jul 30, 2026
0f75ef0
Share the finite-source calibration and the gather tail
The-E Jul 30, 2026
37567fb
fix failing test
The-E Jul 31, 2026
65b3c32
fix ci issues
The-E Aug 2, 2026
6f06bd3
Introduce depth buffer isolation for cockpit rendering in Vulkan
The-E Aug 14, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions code/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ TARGET_LINK_LIBRARIES(code PUBLIC jansson)
target_link_libraries(code PUBLIC anl)

target_link_libraries(code PUBLIC imgui)
target_link_libraries(code PUBLIC implot)

IF(FSO_BUILD_WITH_OPENXR)
target_link_libraries(code PUBLIC OpenXR::openxr_loader)
Expand Down
3 changes: 2 additions & 1 deletion code/ai/aiturret.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1095,7 +1095,8 @@ int find_turret_enemy(const ship_subsys *turret_subsys, int objnum, const vec3d
int target_objnum = aip->target_objnum;

if (Objects[target_objnum].signature == aip->target_signature) {
ship* target_shipp = &Ships[Objects[target_objnum].instance];
// The parent's target can be a weapon or an asteroid, so make sure it's a ship before indexing Ships[]
ship* target_shipp = (Objects[target_objnum].type == OBJ_SHIP) ? &Ships[Objects[target_objnum].instance] : nullptr;
if (target_shipp && iff_matches_mask(target_shipp->team, enemy_team_mask)) {
if (!(Objects[target_objnum].flags[Object::Object_Flags::Protected])) { // check this flag as well
// nprintf(("AI", "Frame %i: Object %i resuming goal of object %i\n", AI_FrameCount, objnum,
Expand Down
7 changes: 3 additions & 4 deletions code/cmdline/cmdline.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2339,10 +2339,9 @@ bool SetCmdlineParams()
if (gr_sync_validation_arg.found()) {
// Enables the Vulkan validation layer + debug messenger + synchronization
// validation feature (see VulkanRenderer::initializeInstance), but NOT the
// engine-level graphics debug output: -gr_debug additionally draws debug
// overlays (e.g. output_uniform_debug_data), which are unrelated to GPU
// sync validation and would otherwise appear as spurious on-screen
// artifacts. Keep this a pure GPU-validation switch.
// engine-level graphics debug output: -gr_debug additionally feeds extra
// stat groups into the ImGui frame profiler overlay (see gr_get_debug_stats),
// which are unrelated to GPU sync validation. Keep this a pure GPU-validation switch.
Cmdline_gr_sync_validation = true;
}

Expand Down
31 changes: 28 additions & 3 deletions code/def_files/data/effects/deferred-f.sdr
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,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
Expand All @@ -33,6 +34,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;
Expand Down Expand Up @@ -63,11 +65,16 @@ uniform shadowCascadeParams {
int cascade_count;
float rtShadowBiasMin;
float rtShadowBiasMax;
int rtShadowSampleCount;
int rtaoSampleCount;
float rtaoRadius;
float rtaoStrength;

mat4 shadow_mv_matrix;
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
Expand Down Expand Up @@ -291,6 +298,17 @@ 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.
vec3 worldPos = (inv_view_matrix * vec4(position, 1.0)).xyz;
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
Expand All @@ -315,15 +333,22 @@ 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);
// 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, coneTan, rtShadowSampleCount, gl_FragCoord.xy);
#else
vec4 fragShadowPos = shadow_mv_matrix * inv_view_matrix * vec4(position, 1.0);
vec4 fragShadowUV[NUM_SHADOW_CASCADES];
for (int i = 0; i < NUM_SHADOW_CASCADES; i++) {
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
}

Expand Down
5 changes: 4 additions & 1 deletion code/def_files/data/effects/fxaa-v.sdr
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ void main() {
}
#else
in vec4 vertPosition;
in vec4 vertTexCoord;

out vec2 v_rcpFrame;
noperspective out vec2 v_pos;
Expand All @@ -20,6 +21,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
139 changes: 139 additions & 0 deletions code/def_files/data/effects/lensflare-f.sdr
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
// Physically-based lens flare (Lee & Eisemann 2013 matrix method).
// Each ghost is the image of the iris (aperture texture) clipped by the image
// of the circular entrance pupil, evaluated per color channel for chromatic
// fringing. The starburst instance samples the precomputed FFT texture instead.

struct lens_flare_instance {
vec4 center;
vec4 halfext;
vec4 apscale;
vec4 apoff;
vec4 color;
};

// Which artifact this quad draws. Mirrored from LENS_QUAD_* in
// graphics/util/uniform_structs.h, which also tabulates what each kind reads out
// of the fields above.
#define LENS_QUAD_GHOST 0.0
#define LENS_QUAD_STARBURST 1.0
#define LENS_QUAD_STREAK 2.0

// The tag travels as a float, so match it with a half-step tolerance rather than
// by equality.
bool quad_is(float tag, float kind) { return abs(tag - kind) < 0.5; }

#ifdef VULKAN
layout(location = 0) in vec2 sensorPos;
layout(location = 1) flat in vec4 g_center;
layout(location = 2) flat in vec4 g_halfext;
layout(location = 3) flat in vec4 g_apscale;
layout(location = 4) flat in vec4 g_apoff;
layout(location = 5) flat in vec4 g_color;
layout(location = 6) flat in float g_origin;

layout(location = 0) out vec4 fragOut0;

layout(set = 1, binding = 1) uniform sampler2D textures[16];
#define apertureMap textures[0]
#define starburstMap textures[1]
#else
in vec2 sensorPos;
flat in vec4 g_center;
flat in vec4 g_halfext;
flat in vec4 g_apscale;
flat in vec4 g_apoff;
flat in vec4 g_color;
flat in float g_origin;

out vec4 fragOut0;

uniform sampler2D apertureMap;
uniform sampler2D starburstMap;
#endif

#ifdef VULKAN
layout(std140, set = 2, binding = 0)
#else
layout(std140)
#endif
uniform genericData {
vec2 axis;
vec2 ndc_scale;
vec4 tint;
int n_instances;
float squeeze; // anamorphic horizontal stretch, 1.0 = spherical
// keep the array size in sync with MAX_LENS_FLARE_INSTANCES (uniform_structs.h)
lens_flare_instance instances[64];
};

// Undo the anamorphic stretch the vertex shader applied to this quad, so
// everything below stays in the rotationally-symmetric frame the optics were
// solved in. Both shaders stretch about the instance's axial centre, which the
// vertex shader hands over in g_origin -- they have to agree on that origin, or
// off-axis ghosts shear instead of stretching. At squeeze == 1.0 this is exactly
// the identity, so a spherical lens renders bit-for-bit as it did before.
vec2 unsqueeze(vec2 p)
{
vec2 d = p - axis * g_origin;
d.x /= squeeze;
return axis * g_origin + d;
}

// The anamorphic streak, generated rather than sampled: it is a smooth 1D
// profile, so a texture would cost an upload and a sampler binding to store a
// curve that two lines of arithmetic describe exactly.
//
// `d` is the offset from the sun's image, `halfext` the half-length (x) and
// half-thickness (y) of the bar. The bar tapers toward the tips instead of
// keeping a constant width, because a streak of even thickness reads as a drawn
// line rather than a lens artifact; the taper is floored so the tip never
// narrows past the point where it would alias.
float streak_profile(vec2 d, vec2 halfext)
{
float u = abs(d.x) / halfext.x; // 0 at the sun, 1 at the tip
if (u >= 1.0) {
return 0.0;
}

float taper = 1.0 - u;
float v = d.y / (halfext.y * max(taper, 0.15));
float across = exp(-v * v * 4.0);
// an even falloff down the length, plus a hot core so the streak visibly
// has a source rather than floating over the sun
float along = taper * taper + exp(-u * 12.0);
return across * along;
}

float ghost_channel(vec2 sp, float center, float halfext, float apscale, float apoff, float intensity)
{
vec2 q = (sp - axis * center) / halfext;
// image of the circular entrance pupil clips the bundle
float pupil = clamp((1.0 - length(q)) * 8.0, 0.0, 1.0);
vec2 uv = q * apscale + axis * apoff;
return texture(apertureMap, uv * 0.5 + 0.5).r * pupil * intensity;
}

void main()
{
vec3 result;

if (quad_is(g_center.w, LENS_QUAD_STREAK)) {
// The streak is laid out in sensor space by the vertex shader, so unlike
// the ghosts it must not be un-squeezed on the way back. halfext.xy is a
// half-length and a half-thickness here, not a chromatic triple.
result = streak_profile(sensorPos - axis * g_center.x, g_halfext.xy) * g_color.rgb;
} else {
vec2 sp = unsqueeze(sensorPos);
if (quad_is(g_center.w, LENS_QUAD_STARBURST)) {
// starburst billboard centered on the sun
vec2 q = (sp - axis * g_center.x) / g_halfext.x;
result = texture(starburstMap, q * 0.5 + 0.5).rgb * g_color.rgb;
} else {
result.r = ghost_channel(sp, g_center.x, g_halfext.x, g_apscale.x, g_apoff.x, g_color.x);
result.g = ghost_channel(sp, g_center.y, g_halfext.y, g_apscale.y, g_apoff.y, g_color.y);
result.b = ghost_channel(sp, g_center.z, g_halfext.z, g_apscale.z, g_apoff.z, g_color.z);
}
}

fragOut0 = vec4(result * tint.rgb, 1.0);
}
123 changes: 123 additions & 0 deletions code/def_files/data/effects/lensflare-v.sdr
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
// Physically-based lens flare (Lee & Eisemann 2013 matrix method).
// Instanced draw: one 4-vertex triangle-strip quad per ghost, plus one for the
// starburst billboard. All placement math was precomputed on the CPU into the
// per-instance data below; positions are in sensor-plane millimeters.

struct lens_flare_instance {
vec4 center; // w = LENS_QUAD_*, the kind tag; xyz meaning depends on it
vec4 halfext;
vec4 apscale;
vec4 apoff;
vec4 color;
};

// Which artifact this slot draws. Mirrored from LENS_QUAD_* in
// graphics/util/uniform_structs.h, which also tabulates what each kind reads out
// of the fields above.
#define LENS_QUAD_GHOST 0.0
#define LENS_QUAD_STARBURST 1.0
#define LENS_QUAD_STREAK 2.0

// The tag travels as a float, so match it with a half-step tolerance rather than
// by equality.
bool quad_is(float tag, float kind) { return abs(tag - kind) < 0.5; }

#ifdef VULKAN
layout(location = 0) out vec2 sensorPos;
layout(location = 1) flat out vec4 g_center;
layout(location = 2) flat out vec4 g_halfext;
layout(location = 3) flat out vec4 g_apscale;
layout(location = 4) flat out vec4 g_apoff;
layout(location = 5) flat out vec4 g_color;
layout(location = 6) flat out float g_origin;
#define INSTANCE_INDEX gl_InstanceIndex
#else
in vec4 vertPosition;
out vec2 sensorPos;
flat out vec4 g_center;
flat out vec4 g_halfext;
flat out vec4 g_apscale;
flat out vec4 g_apoff;
flat out vec4 g_color;
flat out float g_origin;
#define INSTANCE_INDEX gl_InstanceID
#endif

#ifdef VULKAN
layout(std140, set = 2, binding = 0)
#else
layout(std140)
#endif
uniform genericData {
vec2 axis; // unit flare axis in sensor space
vec2 ndc_scale; // sensor mm -> NDC
vec4 tint; // sun color * visibility * lens intensity
int n_instances;
float squeeze; // anamorphic horizontal stretch, 1.0 = spherical
// keep the array size in sync with MAX_LENS_FLARE_INSTANCES (uniform_structs.h)
lens_flare_instance instances[64];
};

void main()
{
#ifdef VULKAN
vec2 corner = vec2(float(gl_VertexIndex & 1), float((gl_VertexIndex >> 1) & 1)) * 2.0 - 1.0;
#else
vec2 corner = vertPosition.xy;
#endif

lens_flare_instance inst = instances[INSTANCE_INDEX];

vec2 p;
float origin;

if (quad_is(inst.center.w, LENS_QUAD_STREAK)) {
// Anamorphic streak: a screen-horizontal bar centred on the sun's image,
// built straight in sensor space rather than in the axis/perp frame. The
// streak lies along the cylindrical element, not along the line to the
// frame centre, so it stays horizontal wherever the sun sits. It is also
// exempt from `squeeze` -- its length is an explicit control, and
// stretching it as well would count the anamorphic twice.
// The vertical bound is padded well past the half-thickness: the gaussian
// across the bar is still ~2% of peak at one half-thickness, so a quad
// cut exactly there would leave a hard horizontal line down the whole
// streak. At 3x the profile has decayed to exp(-36), i.e. nothing. This
// only pads the geometry -- halfext.y still means half-thickness, so
// +Thickness: keeps its meaning.
origin = inst.center.x;
p = axis * origin + vec2(corner.x * inst.halfext.x, corner.y * inst.halfext.y * 3.0);
} else {
// Ghosts and the starburst share this path: both are laid out per channel
// in the axis/perp frame, the starburst simply with all three channels
// equal.
// Bounds of the union of the three chromatic quads, along/around the axis
float cmin = min(inst.center.x - inst.halfext.x, min(inst.center.y - inst.halfext.y, inst.center.z - inst.halfext.z));
float cmax = max(inst.center.x + inst.halfext.x, max(inst.center.y + inst.halfext.y, inst.center.z + inst.halfext.z));
float caxis = 0.5 * (cmin + cmax);
float haxis = 0.5 * (cmax - cmin);
float hperp = max(inst.halfext.x, max(inst.halfext.y, inst.halfext.z));

// The anamorphic stretch is linear, so applying it to the corner offset
// takes the oriented rectangle to a parallelogram that still bounds the
// stretched footprint exactly -- no need to widen out to a screen-aligned
// box. The stretch is about the instance's axial centre, not the sensor
// origin, so ghosts stay put and only grow: on a desqueezed anamorphic
// frame the horizontal squeeze of capture cancels for positions but not
// for footprints.
vec2 perp = vec2(-axis.y, axis.x);
vec2 off = axis * (corner.x * haxis) + perp * (corner.y * hperp);
off.x *= squeeze;
origin = caxis;
p = axis * caxis + off;
}

sensorPos = p;
g_origin = origin;
g_center = inst.center;
g_halfext = inst.halfext;
g_apscale = inst.apscale;
g_apoff = inst.apoff;
g_color = inst.color;

gl_Position = vec4(p * ndc_scale, 0.0, 1.0);
}
Loading
Loading