Compare commits

...
Author SHA1 Message Date
the-e 76286c5332 Remove unused graphics settings from EditorViewport
Eliminate redundant graphics preference fields (post-processing, shadow quality, MSAA, texture filtering, anisotropy, gamma) that were initialized but unused in `EditorViewport`. Clean up associated setting writes for consistency.
2026-11-07 19:46:00 +01:00
the-eandClaude Opus 5 e4546de24e Update the design notes and help for the resize rework
documentation/qtfred-post-processing-viewport-resize.md still described the
Gr_min_render_target_w/h floor, which no longer exists, and listed
reallocate-on-resize as rejected -- which is now the implemented approach.

Rewrite it around gf_resize_render_targets, and keep the two failed attempts as
history, because the reason they failed is the reason the current one works.
That document had already identified the prerequisite correctly: the scene
teardown left FBOs attached to stale texture handles, and draws to an incomplete
FBO go nowhere, hence the black viewport. Completing the teardown was what made
reallocation viable. Also records the two properties that are easy to get wrong
(clamp to the hardware limit before deciding whether to resize, or a viewport
past that limit rebuilds every frame; never resize mid-frame), the sites fixed
in the deferred and Vulkan paths, and why the volumetric nebula pass is
deliberately left unscaled.

Corrections to the Preferences help page from the same review:

 - Enabling post-processing does not by itself produce shadows. Shadow quality
   defaults to Disabled and needs a restart to change, so the page said the
   opposite of what a user will experience.
 - The anisotropy control also greys out when the GPU reports a maximum of 2x,
   not only when it lacks the feature outright.

The help keyword index is left alone: keywords there are page-level, with
multiple entries only ever used as synonyms for one page, so per-setting
keywords would be inventing a convention rather than following one.

Finally, note in the qtfred module guide that its viewport calls
gr_screen_resize() every frame -- the assumption whose violation caused all of
the above -- and where the graphics preferences now live.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-11-07 19:46:00 +01:00
the-eandClaude Opus 5 336f198594 Document the Graphics preferences in the help that ships
The Graphics tab was documented in help-src/doc/dialogs/PreferencesDialog.html,
which is not listed in doc/qtfred.qhp and so is never compiled into
qtfred_help.qch. It is a stale duplicate of general/PreferencesDialog.html --
the page the table of contents, the Preferences keyword, and Viewport.html all
point at -- and one of only two orphaned files under help-src/doc. None of the
new documentation reached the Help viewer.

Document the tab on the page that actually ships, and revert the edit to the
orphan so nothing is left stranded there. The orphan itself predates this
branch and is left alone; it should probably be deleted, but that is a separate
question from this branch.

The content is also corrected against what the settings now do:

 - Shadow quality requires a restart. The orphan did not say so, and the
   dropdown entries no longer carry a "(restart required)" suffix now that
   their labels come from the engine's own option definition.
 - Texture filtering and anisotropy default to the engine's own choice
   (the config default, and the hardware maximum) until the user picks one.
 - Anisotropy is disabled outright on hardware that does not support it.
 - Gamma documents its actual range and default.
 - The page's "changes take effect immediately" claim is qualified, since it
   is not true of the four restart-only settings.

Also notes the View > Enable Post Processing menu equivalent, which had no
documentation anywhere -- no help page covers the View menu's display toggles.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-11-07 19:46:00 +01:00
the-eandClaude Opus 5 5c7059b451 Give qtFRED's graphics preferences a single owner
The Preferences > Graphics settings were read from QSettings in two places --
management.cpp before gr_init(), and EditorViewport once the editor exists --
each with its own copy of the key strings and default values, so renaming a key
in one would silently stop the other from working. The split between settings
that apply live and settings that need a restart was recorded only in comments
that had to stay in sync across five files.

Move both into GraphicsSettings, which owns the keys, the defaults, and the two
apply paths (applyLive() and applyBeforeGrInit()). ViewSettings holds one of
these instead of seven loose fields.

Behaviour fixes that fall out of having one reader:

 - Values are range-checked before being cast to ShadowQuality/AntiAliasMode
   and validated against the MSAA list, matching what the neighbouring
   DataMenuStyle load already did. A hand-edited settings file no longer
   produces an out-of-range enum.
 - The two OptionsManager overrides are now consistent. Texture filtering was
   overridden unconditionally with a hardcoded default while anisotropy was
   guarded, so a fresh install masked the engine's own texture-filter default
   for no reason. Both now use a sentinel for "the user has not chosen" and
   leave the engine option alone until there is a real choice.
 - gr_set_gamma(3.0f) is no longer duplicated as a literal next to the struct
   default.

The dialog's value lists now come from the engine option definitions
(Graphics.Shadows, Graphics.AAMode, Graphics.TextureFilter) instead of the
hardcoded .ui items that duplicated them, and anisotropy uses the shared
gr_get_supported_anisotropy_levels() rather than a near-verbatim copy of the
engine's enumerator. Adding an AA mode upstream no longer silently desyncs
qtFRED's dropdown. Note that the shadow-quality entries therefore lose the
"(restart required)" suffix they carried in the .ui; the control's tooltip and
the help page still say it.

Populating combos now blocks signals: setupUi() has already run
connectSlotsByName(), so filling them would otherwise fire the change slots and
mark the model modified before the user touched anything.

Finally, render_frame() gets its three correlated EnablePostProcessing branches
replaced by a scoped ScenePostProcessing guard, and the shadow pass -- with the
HTL matrix-stack dance it needs -- moves into its own named function.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-11-07 19:45:48 +01:00
the-eandClaude Opus 5 4c0ff8414d Grow the scene/post render targets when the viewport does
The offscreen targets backing the scene texture and post-processing were
allocated once from gr_screen at gr_init() and never revisited. Anything drawn
while gr_screen was larger got clipped to their edge and stretched back over
the viewport. The game hits this on the SDL window-resize path
(osapi.cpp calls gr_screen_resize()); qtFRED hits it constantly, since its 3D
viewport is a resizable dock widget.

Add gf_resize_render_targets, called from gr_screen_resize(), and implement it
for OpenGL: grow the targets to cover the new gr_screen, rebuilding only the
resolution-dependent resources. The post-processing table, the compiled
shaders and the SMAA lookup textures are resolution-independent and stay
alive, which is what makes this cheap enough to run off a window drag. This
mirrors what the Vulkan backend already does in VulkanPostProcessor::resize();
Vulkan leaves the hook unset because recreateSwapChain() owns it there.

The allocation only ever grows: gr_screen_resize() is called every frame by
qtFRED and repeatedly by the briefing map widget, so tracking the high-water
mark avoids thrashing, and the shrunk state is already handled by
Scene_texture_u_scale/v_scale. The size is also clamped to the hardware limit
up front, so a viewport past that limit stops asking to be resized instead of
rebuilding every frame. If the larger allocation fails outright -- most likely
precisely when growing -- the resize stops before rebuilding the
post-processing targets on top of scene textures that no longer exist.

This replaces Gr_min_render_target_w/h, which sized the targets for the
largest attached display up front -- on a 4K display at 2x scaling that was
over a gigabyte of VRAM, allocated at launch, for a feature that is off by
default and may never be switched on.

Deletions now go through GL_state.Texture.Delete(): a freed texture name the
state cache still holds would make a later Enable() of the recycled name a
silent no-op. This mattered little when teardown only ran at shutdown. While
there, the scene teardown now releases everything setup allocates -- it was
leaking Scene_ldr/composite/luminance/Cockpit_depth and all six MSAA targets --
and post-processing shutdown releases the SMAA lookup textures.

Separately, fix the sampling extents that the above makes reachable:

 - deferred-f.sdr turns gl_FragCoord into a G-buffer coordinate using
   invScreenWidth/Height, which described gr_screen rather than the G-buffer.
   Fixed in both backends; it is currently a no-op under Vulkan, where
   resize() keeps the extent equal to gr_screen, but states the requirement
   instead of relying on that.
 - the MSAA scene-colour copy, the MSAA resolve and the fog pass sampled the
   full [0,1] range of targets that are only filled to the u/v scale.
 - fxaa-v.sdr derived its texcoord from vertPosition, ignoring the
   sub-rectangle the draw call asked for.

Those extents now come from opengl_draw_full_screen_scene_texture() rather
than being open-coded, so a new pass cannot quietly reintroduce the bug. The
volumetric nebula pass is deliberately left unscaled and commented: it uses
fragTexCoord both to reconstruct a ray direction and to sample, which needs a
shader change to separate.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-11-07 19:45:48 +01:00
the-eandClaude Opus 5 019c4bacea Fix texture filtering silently defaulting to bilinear
opengl_tcache_init() read GL_mipmap_filter out of TextureFilteringOption
before seeding it from the legacy config key. That option's default_func
returns GL_mipmap_filter itself, so with no persisted "Graphics.TextureFilter"
value getValue() fell through to a still-zero-initialized global -- bilinear,
where the config default is trilinear. Since in-game options are on by default,
that affected every player who had never explicitly set the option, and it
ignored the legacy TextureFilter key existing installs were configured through.

Seed from config first, then let the option override, matching the order the
anisotropy setting below already uses (its default_func queries the hardware
directly, which is why it was never affected).

Also route the option's value enumerator through the new shared
gr_get_supported_anisotropy_levels() rather than a private copy.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-11-07 19:45:48 +01:00
the-e e7d58d85cb Add configurable graphics settings to Preferences dialog
Introduce post-processing, shadow quality, anti-aliasing, anisotropic filtering, texture filtering, MSAA, and gamma controls to qtFRED's Preferences. Ensures real-time application of certain settings and requires restart for others. Adjust graphics initialization to support baked settings before `gr_init()`. Update documentation to reflect these additions.
2026-11-07 19:45:48 +01:00
the-eandClaude Opus 5 84fa0a622f Size qtFRED's post-processing targets for its resizable viewport
The offscreen render targets that back post-processing -- the scene
textures and the post-processing surfaces -- are allocated once from
gr_screen.max_w/max_h at renderer init and never resized. The game never
notices, because its window size is fixed after gr_init(). qtFRED does:
FredRenderer::render_frame() calls gr_screen_resize() every frame to match
a dockable, resizable widget, so growing the viewport past the startup size
clipped the render to the old, smaller texture and then stretched it back
over the new, larger viewport.

Add Gr_min_render_target_w/h as a floor those allocations respect, and have
qtFRED set it to the largest size its viewport can reach (the biggest screen
the window could be maximized onto, in device pixels) before calling
gr_init(). Left at 0 for the game, which keeps sizing them purely from
gr_screen.

That makes Scene_texture_u_scale/v_scale meaningful outside the game for the
first time, which turns up two latent bugs:

  - Seven post-processing passes passed Scene_texture_u_scale for both axes.
    Harmless while the scene texture matches the screen exactly (u == v),
    wrong the moment it doesn't.
  - The bloom bright pass hardcoded 1.0/1.0. Unlike the passes after it, it
    reads the scene texture directly rather than an already-cropped
    intermediate, so it has to confine itself to the sub-rectangle that was
    actually rendered into.

Finally, expose the pipeline in qtFRED at all: a View menu toggle brackets
the 3D scene in gr_scene_texture_begin/end, the same way game_render_frame()
brackets its own. Off by default, and it covers only the 3D world content --
the 2D overlays stay outside it.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-11-07 19:45:48 +01:00
the-eandClaude Sonnet 5.5 c5b761ca5d Apply review feedback on softer shadows
- Bind the GL raw-depth sampler on unit 11 in the deferred pass and
  unbind it after the pass.
- Enable RTAO only when shadows are on, and remove the RTAO option when
  ray tracing is not supported.
- Use every second Poisson tap in the PCSS blocker search.
- Use TLAS-relative ray origins for RTAO.
- Gate +AngularSize: at version 27.0.
- Measure an .eff sun from its first frame, and skip the measurement
  when shadows are off.
- Guard the GL 3.3 sampler check for GLES builds.
- Restore the lab session overrides when the lab closes.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-11-07 19:45:48 +01:00
the-e 9443c4abe0 Add shadow contact hardening via PCSS blocker search
The shadow-mapped path's penumbra width was a fixed per-cascade value
(shadow_smoothness_scale()'s ceiling), so every occluder blurred its
shadow by the same amount regardless of how close it actually was to
the receiver -- unlike the raytraced path, which gets that for free
from traceShadowRayCone()'s real occluder distance. Close that gap
with PCSS: search nearby texels for occluders, then scale the filter
radius by how far away they were.

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

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

Drops RT_SHADOW_SUN_SIZE_CALIBRATION accordingly: that constant existed
only to fake a blocker-distance-driven penumbra on the shadow-mapped path
by pre-scaling the raytraced cone to match it, and the shadow-mapped path
now has a real blocker search of its own, so the raytraced cone goes back
to its physically correct size.
2026-11-07 19:45:48 +01:00
the-eandClaude Opus 5 5e745f7535 Add RTAO and unify raytraced/shadow-mapped shadow softness on sun angular size
Raytraced shadow penumbras needed a hand-authored $SunAngularSize: in
stars.tbl, which no shipped content sets -- so in practice nothing ever
got a penumbra, and the shadow-mapped path's $Shadow Smoothness Factor:
had no relation to it at all. Derive a size from the sun bitmap instead
and drive both methods from the same number.

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

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

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

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

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-11-07 19:45:47 +01:00
the-eandClaude Opus 5.5 c165b82996 Address third round of review feedback on the profiler overlay
- Move the overlay option strings to XSTR 1940 and 1941. The touch
  overlay on master uses 1936 to 1939.
- Give FrameProfiler the main thread ID when it is made. Before, it
  used the thread of the first event, which was a race.
- Let mouse motion go to the game while the pointer is over the
  overlay, so that mouse flight does not stop there.
- Find the dynamic bindings of the PerDraw key with
  dynamic_offset_slot(), as the other call sites do.
- Make disable_inside_open_scope count the GPU timestamp queries, so
  that it fails if end() does not close the GPU event.
- Make the copy test copy the category, not bind a reference.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 19:36:52 +01:00
Fabian WoltermannandClaude Opus 5.5 4f3a0d4cff Return size_t from Category::getCount() and tidy the registry code
- Use structured bindings and std::scoped_lock in the category registry.
- Return size_t from getCount() and update its callers and tests.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 19:36:42 +01:00
Fabian WoltermannandClaude Opus 5.5 8266392f00 Address second round of review feedback on the profiler overlay
- Make the category registry thread-safe and give categories with the
  same name one id, so the registry does not grow without limit.
- Add tests that toggle the profiler inside an open trace scope.
- Correct the next free XSTR id note to 1938.
- Use the existing Windows include block for psapi.h in osapi.cpp.
- Use list_range in ship_get_num_ships() and shorten some comments.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 19:36:42 +01:00
the-eandClaude Sonnet 5.5 68166ed419 Use wall-clock frame time and read the history without a copy
The Avg and FPS line used traced self-time, which reads too high.
Measure the time between overlay frames instead. Draw the graph with
PlotLineG so the history deque needs no per-frame copy.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-11-07 19:36:31 +01:00
the-eandClaude Sonnet 5.5 fa4b755b94 Address review feedback on the profiler overlay
Skip complete::end for a scope that did not start. This fixes a crash
when Lua turns the overlay on while a scope is open. Track the GPU begin
event so that turning the overlay off mid-scope still closes it.

Use XSTR 1936 and 1937 for the overlay option. Master now uses 1932
and 1933.

Remove the default value of dynamicOffsets in bindDescriptorSet. Use the
PerDraw binding constants in the PerDraw key.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-11-07 19:36:31 +01:00
the-e b5bcfeb8d1 Refactor profiler overlay to use snapshot-based frame timing 2026-11-07 19:36:31 +01:00
the-e b1c90ea5c5 Replace category pointers with IDs to prevent use-after-free
- Updated `trace_event` to store `category_id` instead of a pointer to prevent dangling references to category objects that may already be destroyed.
- Modified all references to category names to use `Category::getNameById()` instead of dereferencing pointers.
- Expanded memory statistics in the profiler overlay with new sound, Lua, and OS-level memory usage tracking.
- Added new tests to verify proper use of category IDs in frame profiling and event tracing.
- Improved memory safety and stability across tracing, profiling, and overlay systems.
2026-11-07 19:36:31 +01:00
the-eandClaude Opus 5 dd4a08925c Fix profiler overlay crash on a category that is not a static
The category registry kept a Category pointer for each id. The comment said that all categories
are global statics, but that is not correct. The Lua API tracing_category constructs a Category
and copies it into a script-owned object. The registry then kept a pointer to the dead temporary.
build_overlay_snapshot called getName() on that pointer, made an SCP_string from the garbage
char*, and the process stopped with SIGABRT.

The registry now keeps the name by value and never keeps a Category pointer. getById() becomes
getNameById(), which is all that build_overlay_snapshot needs. A copy of a Category keeps the id
of the original, which stays correct because the id only selects a name.

Add two tests: one shows that the name stays available after the Category object is dead, and one
drives the full path that failed, from processEvent to the overlay snapshot.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-11-07 19:36:13 +01:00
the-eandClaude Opus 5 647554e182 Add OpenGL memory statistics to the profiler overlay
The memory panel showed no texture bytes and no geometry bytes under
OpenGL. Only the depth renderbuffers went into the render target group.
All three groups now give a value.

Textures: GL_texture_bytes_used holds the bytes of every live texture
cache slot. opengl_texture_release_bytes() removes the bytes of a slot at
each site that drops its texture data. The bitmap render targets record
the bytes of their color texture.

Geometry: the unused GL_vertex_data_in global becomes
GL_geometry_bytes_used, which counts only vertex and index buffers. This
matches the geometry group of the Vulkan backend.
gr_opengl_delete_buffer() sets the size of the buffer to 0, because
opengl_destroy_all_buffers() calls it a second time for a buffer that the
game already deleted.

Render targets: three new functions add the scene textures, the
post-processing textures, and the shadow map to the group. They sum the
bytes at the moment of the call from the textures that exist, thus they
need no bookkeeping at the create and delete sites.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-11-07 19:36:13 +01:00
the-e 57c28798a7 fix ci issues 2026-11-07 19:36:13 +01:00
the-e 3c456a088c Add memory statistics and tracking to profiler overlay 2026-11-07 19:36:13 +01:00
the-e cd48b5b500 Replace exception with sentinel for non-dynamic Vulkan descriptor bindings
- Introduced `DYNAMIC_SLOT_INVALID` sentinel to replace the throw in `dynamic_offset_slot` for MSVC compatibility.
- Added `static_assert` checks to validate binding constants, ensuring compile-time validation for dynamic offsets.
- Updated comments to clarify design rationale and avoid potential misuse.
2026-11-07 19:36:13 +01:00
Fabian Woltermann f1812e212d Enable VK_KHR_portability_subset for compatibility 2026-11-07 19:36:13 +01:00
the-e 817f641478 Enable primitive restart for supported topologies in Vulkan pipeline
- Added `topologySupportsPrimitiveRestart` to determine restart eligibility for each topology.
- Updated `primitiveRestartEnable` to be conditionally set based on topology, addressing compatibility issues with Metal/MoltenVK.
- Prevented crash scenarios on macOS by ensuring pipelines are valid for strip/fan topologies requiring primitive restart.
2026-11-07 19:36:13 +01:00
the-e eec4a9ce3e Refactor Vulkan descriptor set binding to reduce dynamic offset churn
- Consolidated dynamic descriptor offset logic into `DescriptorWriter::bindSets`, replacing repetitive `vkCmdBindDescriptorSets` patterns across Vulkan modules.
- Eliminated redundant descriptor binding operations for Material and PerDraw sets by centralizing logic in `bindSets`.
- Simplified descriptors by directly splitting offset changes from descriptor contents, avoiding unnecessary updates.
- Updated comments and documentation to align with the new approach while maintaining reference consistency.
- Applied optimizations across Vulkan modules (`VulkanPostProcessing`, `VulkanDescriptorManager`, etc.) to reduce per-draw overhead and improve readability.
2026-11-07 19:36:13 +01:00
the-eandClaude Opus 5 b2de895055 Use dynamic uniform buffers to stop per-draw descriptor set churn in Vulkan
The Material and PerDraw descriptor sets were reallocated and fully rewritten on
essentially every draw call. model_draw_list::render_buffer rebinds ModelData with
a fresh uniform buffer offset for each queued draw, and that offset was part of the
Material set's memoization key, so the cache in VulkanDrawManager::applyMaterial
missed every time - taking a frame-pool allocation, a full template write, the
16-entry material texture resolution and a vkUpdateDescriptorSets with it.
GenericData/Matrices did the same to the PerDraw set. renderShadowDraw was worse
still: all three sets allocated and rewritten per shadow draw, with no memoization
at all.

A large asteroid field (Solaris m21) measured 2566 descriptor sets and 19107
descriptor writes per frame at 1606 model draws, and grew an extra descriptor pool
chunk every frame.

Declare the bindings whose offset moves per draw - MaterialBinding::ModelData,
MaterialBinding::ShadowMapData, PerDrawBinding::GenericData and
PerDrawBinding::Matrices - as eUniformBufferDynamic. DescriptorWriter::setBuffer
now splits a dynamic binding's offset out of the descriptor, writing
{buffer, 0, range} and stashing the offset for vkCmdBindDescriptorSets'
pDynamicOffsets, so an offset-only change leaves the set's contents identical and
the memoization caches start hitting. Shader sources are unchanged.

VulkanStateTracker::bindDescriptorSet now compares the dynamic offsets as well as
the set handle. This is required, not an optimization: the common case is now the
same set rebound with a new offset, and the old handle-only redundancy check would
have skipped that bind and fed every draw the previous draw's uniforms.

renderShadowDraw memoizes its three sets the way applyMaterial does. It does not
invalidate applyMaterial's caches - those record what a set contains, the shadow
pass allocates its own sets rather than overwriting theirs, and it binds through
the state tracker, so the next material cache hit still rebinds correctly.

Also switch allocateFrameSet to the non-allocating allocateDescriptorSets overload;
the vector-returning one heap-allocated once per set, thousands of times a frame.
MAX_SETS_PER_POOL stays at 1024 - the per-frame pool growth it used to log was a
symptom of the churn, and one chunk now covers the scene outright.

Measured on that mission in a Debug build, A/B on the same tree with and without
this change:

  descriptor sets/frame  @ ~1370 draws  2566   -> 250-345
  descriptor writes/frame                19107 -> ~1700-2300
  median frametime                       92.9/83.6 ms -> 72.0/66.5 ms
  Render Buffer (profiler overlay)       24.68 ms (30.8%) -> 12.73 ms (18.4%)
  Build Shadow Map                       5.44 ms -> off the top-5 entirely

Release builds already inlined most of this away and are unchanged, as expected.
Verified with the validation layers (sync validation enabled): no new VUIDs versus
the same build without the change.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-11-07 19:36:13 +01:00
the-e 7d4d61042e Refactor frame profiler to use per-category self-time accumulation
- Introduced `accumulate_self_times` for single-pass exclusive time computation, improving performance and accuracy.
- Updated `FrameProfiler` and overlay snapshot logic to leverage `Category`'s stable `getId` and `getCount`.
- Replaced pie chart with a stacked bar for frame budget visualization, optimizing rendering cost.
- Cached `get_tid` and `get_pid` with thread-local storage to minimize syscall overhead.
- Added support for unit testing frame profiling functionality and included new test cases.
- Improved code comments and clarified documentation.
2026-11-07 19:36:13 +01:00
the-e fd77b84c41 Update ImPlot to v1.1 WIP and improve internal consistency
- Adjust copyright notices for extended license coverage (2025-2026).
- Modernize constants with `constexpr` replacements, improving type safety and clarity.
- Enforce `nullptr` usage over `NULL` throughout, aligning with modern C++ practices.
- Add various enhancements to ImPlot legends, markers, and axes behavior.
- Introduce additional helper methods for time handling and timestamp calculations.
- Address UI and typo improvements in comments and documentation, refining accuracy.
2026-11-07 19:36:13 +01:00
the-e 2166d253a8 Add graphics and Vulkan debug stats to ImGui profiler overlay
- Integrated per-backend diagnostic counters into the ImGui frame profiler overlay, triggered by `-gr_debug`.
- Consolidated and cleaned up Vulkan rendering state and descriptor management for compatibility with ImGui's Vulkan pipeline.
- Enabled uniform buffer and draw statistics reporting for enhanced performance insights during debugging.
2026-11-07 19:36:13 +01:00
the-e f5b29e721b feat: add ImGui frame profiler overlay
Replaces the old text-dump frame profile display (-profile_frame_time)
with a runtime-toggleable ImGui/ImPlot overlay showing average/median
frametime, a scrolling frametime graph, and a pie chart of the top
contributors to frame time. Toggled via a new "Frame Profiler Overlay"
option, which also seeds from -profile_frame_time for backward
compatibility and now drives frame profiling collection at runtime
instead of only at startup.
2026-11-07 19:36:13 +01:00
the-e 9763c7a312 chore: vendor ImPlot library
Adds ImPlot as a build dependency, to be used for the upcoming ImGui
frame profiler overlay's frametime graph and pie chart.
2026-11-07 19:36:01 +01:00
Boutros Tawaifi 49464c56f6 Add an "On Ship Damage" script hook (#7894)
* Add an "On Ship Damage" script hook

Runs after resolved damage has been applied to a ship (lethal hits run before
the destruction callback). Hook variables: Target, Source, Attacker,
WeaponClass, WeaponSubtype, SourceKind (PROJECTILE, BEAM, SHOCKWAVE, COLLISION,
OTHER), DamageType, ShieldDamage, HullDamage, Quadrant, Ballistic, BeamWeapon
and LethalHit. Conditions can filter on the target ship. Nothing is computed
unless a script has registered the hook.

* Move the On Ship Damage hook call out of a lambda

ship_do_damage() now calls a file-level static function, ship_run_damage_hook(), that takes the values it needs as arguments instead of capturing the function state. Hook behaviour is unchanged.

* Report subsystem damage in the On Ship Damage hook

Adds SubsystemDamage, the total subsystem strength a damage application removed, so a hit absorbed by subsystems no longer looks like a small hull hit. The hook description now says it only runs where damage is applied, which excludes multiplayer clients.

* Address On Ship Damage review comments

- Remove WeaponClass, WeaponSubtype, Ballistic and BeamWeapon from the
  hook. Scripts read these from Source when it is a weapon.
- Add a "Weapon class" condition for the On Ship Damage hook. It matches
  only when the damage source is a weapon of that class; a weapon name
  that does not exist never matches.
- SubsystemDamage is now a table keyed by subsystem handle that lists only
  the subsystems that lost strength. The total is removed.
- Rename PROJECTILE to NON_BEAM_WEAPON.
- Classify SourceKind by damage source: COLLISION for ship, debris and
  asteroid collisions; EXPLOSION for ship-death and docked-ship blasts and
  asteroid blasts; WASH for engine wash; SELF for kills where the ship
  damages itself; OTHER for other ship-sourced damage.
- Reword the hook description.

* Key On Ship Damage SubsystemDamage by canonical name

Lua tables cannot use subsystem handles as keys in a usable way, so key
the SubsystemDamage table by each subsystem's CanonicalName instead.

* Tag ship-vs-prop damage as a collision and use in_bounds

The ship-vs-prop collision call didn't pass DamageSource::Collision, so
the On Ship Damage hook reported those hits as OTHER. Every other
collision path was already tagged.

Also use SCP_vector::in_bounds() for the Weapon_info and Damage_types
index checks in ship_run_damage_hook().
2026-11-06 18:41:29 -05:00
Taylor Richards 4dc0ca7878 fix VR in OpenGL on Windows (#8009) 2026-11-06 16:27:14 -05:00
2b1759e98e Vulkan: stop mirroring light shafts top to bottom (#8006)
The Vulkan light shaft pass reuses OpenGL's sun position formula, whose
texture V grows upward. The Vulkan fullscreen triangle in post-v.sdr has
V = 0 at the top of the screen, and ls-f.sdr steps from fragTexCoord
toward sun_pos in that space, so the shafts radiated from a point mirrored
vertically from the sun. Flip the vertical coordinate before upload.

Co-authored-by: Saga port <saga-port@invalid>
Co-authored-by: Claude Opus 5.5 <[email protected]>
2026-11-06 20:32:01 +00:00
Shivansps 1d0dd687cd Android Build (#6992)
* android build support

* Update es_compatibility.h

* Support reporting flags in android

* include pref_path in json v2

* add vulkan headers

* shaderc includes

* remove headers

* force use prebuilt vulkan

* fix vulkan inverted screen

* fix find sdl3 precompiled android

* Update CMakeLists.txt

* replace sdl2 android function names for sdl3 versions

* change sdl includes in osapi

* fix sdl3 includes, again

* rename function to sdl3 name

* remove unused definition

* copy libshaderc_shared.so to output in android

* do not look for vulkan-loader in android

* revert changes to SDL2.cmake

* add comment to vulkan pretransform setup

* cleanup maxsize function

* rename new android functions in osapi

* android trap back button

* Remove calculate_max_resize_max_size

* touch ui opacity control

* change opacity control to int

* fix opacity slider

* Corrected android working folder fallback and set borderless mode as default

* Implement shared helper for JNI calls

* Add version tag and sha256 to android prebuilt libs

* vulkan android check eSuboptimal

* An already requested rebuild also avoids redundant Android surface queries

* add cache bool to cmakelist

* set string ids
2026-11-05 20:56:36 +01:00
Tyler Blount 705d115214 Merge pull request #7993 from MjnMixael/qtfred-classic-menu-layout
Add a classic menu layout option to QtFRED
2026-11-04 18:25:19 -07:00
Mike Nelson a3b4386b7a Add a classic menu layout option to QtFRED
FRED2 can switch to its classic menus, where every editor dialog is in a single
Editors menu. This adds the same option to QtFRED, as a Preferences checkbox on
the Appearance tab.

The classic Editors and Misc menus live in FredView.ui beside the grouped menus
and reuse the same actions, so shortcuts and checked states are shared. Turning
the option on hides the Objects, Environment, Mission, Narrative and Tools
menus and shows the classic ones.
2026-11-04 15:42:59 -06:00
dustinspace217andDustinspace217 246a6ee8f0 Vulkan: match OpenGL's depth bias in the shadow map pass (#7944)
OpenGL's shadow pass calls gr_zbias(-1024), which sets a polygon offset of
factor 1, units 1024 (gropengltnl.cpp, gropengl.cpp). The Vulkan shadow pass
set a constant bias of -1024 with no slope term: the opposite sign, on a
depth buffer that uses the same less-than test. Use OpenGL's values.

Co-authored-by: Dustinspace217 <[email protected]>
2026-11-04 18:21:55 +01:00
dustinspace217andDustinspace217 e66fe223bf Vulkan: use one rendering-finished semaphore per swap chain image (#7943)
Each VulkanRenderFrame signaled its own rendering-finished semaphore and
presented with it. The frame's fence only says the submit has finished, not
that the presentation engine has consumed the semaphore, so the next submit
on that frame could signal a semaphore that is still pending, which the
validation layer reports as VUID-vkQueueSubmit-pSignalSemaphores-00067.

Move the semaphore to the renderer, one per swap chain image, recreated with
the swap chain, and pass the one for the acquired image to submitAndPresent.
Acquiring an image again means its previous present has been consumed.

Co-authored-by: Dustinspace217 <[email protected]>
2026-11-04 18:21:32 +01:00
dustinspace217andDustinspace217 e5b591141c Vulkan: give the irradiance pass cube fallbacks for its cube binding (#7941)
irrmap-f.sdr declares the Material set's texture binding as a samplerCube,
but vulkan_calculate_irrmap filled the array with the 2D fallback and only
replaced element 0 with the environment map, which the validation layer
reports as VUID-vkCmdDraw-viewType-07752 on every irradiance pass. Fill it
with the cube fallback instead.

Co-authored-by: Dustinspace217 <[email protected]>
2026-11-04 18:21:15 +01:00
dustinspace217andDustinspace217 81b9edc2e3 Vulkan: sample the thruster distortion map with its repeat sampler (#7940)
VulkanDistortion creates a linear/repeat sampler for its noise textures,
matching OpenGL's GL_REPEAT on Distortion_texture, but getTextureInfo
handed out the post-processing linearSampler instead, which clamps. The
sampler that was built for it is now the one used.

Co-authored-by: Dustinspace217 <[email protected]>
2026-11-04 18:20:53 +01:00
dustinspace217andDustinspace217 259a58de60 Vulkan: apply each material's depth bias, like OpenGL (#7939)
Squad logos are drawn with a material depth bias of 1 so they sit on top
of the hull. Under Vulkan the bias was written to the dynamic state but
the pipeline was built with depth bias disabled, so it never applied.
applyMaterial now calls vulkan_zbias with the material's bias before
building the pipeline config, the same way opengl_tnl_set_material calls
gr_zbias.

vulkan_zbias also passed its two values in the wrong order:
glPolygonOffset takes (slope factor, constant units) while
setDepthBias here takes (constant, slope).

Co-authored-by: Dustinspace217 <[email protected]>
2026-11-04 18:20:24 +01:00
dustinspace217andDustinspace217 b7e7949413 Vulkan: keep the swap chain pass open when irradiance setup fails (#7938)
vulkan_calculate_irrmap ended the swap chain render pass first and then
had four early returns (pipeline, UBO create, UBO allocate, UBO map)
before resumeSwapChainPass, so any of those failures left the rest of
the frame recording draws outside a render pass. End the pass only after
the setup that can fail.

Co-authored-by: Dustinspace217 <[email protected]>
2026-11-04 18:20:02 +01:00
dustinspace217andDustinspace217 32db880e94 Vulkan: retire BLASes and the TLASes built from them safely (#7937)
A BLAS was destroyed immediately when its model was unloaded or when the
vertex heap grew and the BLAS had to be rebuilt, while the TLAS of a
frame still on the GPU could reference it by device address. Queue the
acceleration structure and its buffer on the deletion queue instead, the
same way ensureTlasCapacity already retires the TLAS. The deletion queue
is flushed after ray tracing shuts down, so shutdown defers too.

A built TLAS also kept pointing at retired BLASes for later frames: the
live shadow TLAS binding is only refreshed when a mission frame rebuilds
it, and buildTlas keeps the previous TLAS when it has no instances. With
ray-traced shadows on, flying a mission and then leaving and re-entering
the tech room (which frees every model on exit) bound such a TLAS on
every tech room frame. So when BLASes are retired, the built frame TLASes
are retired with them and the binding goes back to the empty fallback
TLAS until the next rebuild. onModelUnloaded reports whether it retired
anything, so the binding refresh only runs then.

Co-authored-by: Dustinspace217 <[email protected]>
2026-11-04 18:19:40 +01:00
dustinspace217andDustinspace217 03589b49bf Vulkan: reject truncated shader cache files (#7936)
The runtime shader compiler cached SPIR-V on disk without checking the
write, and accepted any cached file whose size was a multiple of four.
A file cut short by a crash or a full disk was handed to the driver as a
shader module. With half-length cache files in place, master crashes in
the NVIDIA driver on entering SM1-01 under -vulkan.

Cached files are now checked before use: the SPIR-V magic word, and the
instruction word counts must end exactly at the end of the file on an
OpFunctionEnd. A file that fails is logged, recompiled and rewritten.
Fresh compiler output is checked the same way before it is cached, so a
module the check would reject is never written, and the log says so.

New cache files are written to a temporary file unique to the writer and
renamed over the old .spv only after a complete write and close, so a
game crash, or a second game writing the same shader, never leaves a
partial .spv. A power loss still can; the read check catches that.
purgeOldCache now also removes old temporary files.

Co-authored-by: Dustinspace217 <[email protected]>
2026-11-04 18:19:18 +01:00
dustinspace217andDustinspace217 c58a0cd307 Vulkan: give the default material's clip plane the model matrix (#7935)
The default-material shader transforms each vertex by the model matrix
before testing it against the clip plane. Vulkan always passed the
identity, so a clip plane given in world space (for example the warp
plane on a squad logo) was tested against model-space positions. OpenGL
passes gr_model_matrix_stack.get_transform(); do the same here.

Co-authored-by: Dustinspace217 <[email protected]>
2026-11-04 18:18:59 +01:00
dustinspace217andDustinspace217 0b42d0b6e7 Vulkan: upload a mission's environment map as a cubemap (#7934)
Under Vulkan a mission's $Environment Map was never uploaded: the
irradiance and lighting passes only use ENVMAP when its texture slot
already has an image view, and nothing created one for a bitmap that was
only bm_load'ed. OpenGL uploads it the first time it binds it as a
cubemap.

Add VulkanTextureManager::ensureCubemapUploaded(), called from both
passes. It locks the map the way OpenGL does for TCACHE_TYPE_CUBEMAP.
The lock uploads through bm_data(), which now sends uncompressed DDS
cubemaps (bm_is_compressed() returns 0 for them; comp_type identifies
them) to uploadCubemap() as DXT ones already went. If the slot still
holds no cube after the lock, because the data was already resident, it
uploads it directly. A map that cannot be used is remembered by bitmap
signature (handles are reused across missions) and warned about once in
debug builds, and the irradiance pass is skipped instead of sampling a
non-cube view.

uploadCubemap() also assumed uncompressed cubemaps had one mip level and
read the six faces back to back. DDS stores face0[mip0..mipN],
face1[...], so faces 1-5 read the tail of face 0's mip chain. It now
uses the file's mip count, expands 24-bit data over the whole chain,
generates a chain for a one-level uncompressed cubemap as OpenGL does
(the lighting pass samples by roughness), copies mip 0 for every array
layer, cleans up the image when the staging map fails, and clears
isCubemap as soon as it retires the old image.

Co-authored-by: Dustinspace217 <[email protected]>
2026-11-04 18:18:40 +01:00
dustinspace217andDustinspace217 030bbaa793 Fall back to the generated environment map when a mission's is not a cubemap (#7930)
A mission's $Environment Map is used as a cubemap without checking. Naming
an ordinary 2D texture made OpenGL copy six faces out of a one-face image
and crash when the mission started (issue #2245). Check the loaded bitmap
with a new bm_is_cubemap(); if it is not a cubemap, warn, release it and
build the generated environment map, as for a mission that names none.

DX10 (BC7) DDS files carry no cube flag through the loader, so they are let
through unchecked, as before.

Co-authored-by: Dustinspace217 <[email protected]>
2026-11-04 18:18:02 +01:00
dustinspace217andDustinspace217 8ca1ad9bbc P3: Vulkan: draw decals, and draw them like OpenGL does (#7879)
* Vulkan: bind all four model-matrix attribute locations for decals

The Vulkan shader input mask had one bit per listed attribute, so the
pipeline dropped the MATRIX4 attributes at locations 9-11 and the decal
vertex shader read three of its four instance-matrix columns from unbound
inputs. No decal was ever drawn in Vulkan.

The mask now covers four locations for VATTRIB_MODEL_MATRIX. It is built
by shader_vertex_input_mask() in shader_types so both Vulkan call sites
share it and a unit test covers it without a Vulkan build.

Fixes #7866

* Vulkan: build the decal pipeline with the material's shader flags

The decal pipeline config set the shader type but not the flags, so the
decal shader variant was always compiled without USE_NORMAL_MAP and fell
back to screen-space derivative normals for angle rejection. Far more of
the decal volume passed the angle test than in OpenGL, which lit the
decals brighter and wider. The pipeline now takes the flags the material
reports, as the other draw paths already do.

* Review fixes: exact input-mask test, decal flag polarity test, mask width check

Pin the full mask per shader type instead of the presence of the matrix
bits, so an expansion of any other attribute fails; pin that a decal
without a normal map is the one that reads the scene normal buffer; add a
compile-time bound on the mask width; trim two comments.

---------

Co-authored-by: Dustinspace217 <[email protected]>
2026-11-04 18:17:31 +01:00
dustinspace217andDustinspace217 499b3fb38b P4: qtFRED: rotate around the centre of a multi-selection (#7843)
* qtFRED: rotate around the centre of a multi-selection

Rotate Around Object (Alt+R) and the orbit pivot both used the current
object's position, so with several ships marked the camera still
circled whichever one happened to be current. Add
EditorViewport::selectionCenter(), which returns the centroid of the
marked objects when more than one is marked and the given object's
position otherwise, and use it at the three places that chose the
point: the look-at camera update, orbitCameraGetPivot(), and the Alt+R
handler that turns the camera toward the target when the mode is
switched on.

orbitCameraGetPivot() also serves middle-mouse orbit and wheel zoom in
normal mode, so those pivot on the selection centre as well. Every
marked type counts at full weight, including waypoints and jump nodes.
With a single object marked nothing changes.

Fixes #7724

* Make EditorViewport::selectionCenter const

It only reads the marked-object list and the editor's marked count;
clang-tidy (readability-make-member-function-const) flagged it in CI.

* Mirror the multi-selection orbit pivot to FRED, trim comments

FRED gets the same selection_center() as qtFRED's selectionCenter(),
used at the same three places: the look-at camera update, the orbit
pivot, and the Rotate Around Object toggle. Also removes the qtFRED
comments flagged in review.

* Make the selection-center pivot optional and share object bounds

Look-at and orbit use the current object again unless the new option is
on (qtFRED: Preferences > Controls; FRED: View menu), and the selection
center is now the bounding-box midpoint. view_universe in both editors,
the pivot, and qtFRED's selection radius all use get_object_bounds() in
missioneditor/common.

* Reset the cached orbit pivot when the selection-center option changes

Wheel zoom reuses the orbit pivot until the camera moves, so without this
the option only took effect for scrolling after the next camera move.

* Address review: move and rename the selection-centroid option

- FRED: the option moves from the View menu to Settings (and Misc in the
  classic layout), under Move Ships When Undocking, with hover help, and is
  renamed Rotate Around Selection Centroid.
- qtFRED: the checkbox moves next to Move Ships When Undocking in the
  preferences dialog, with the same name.
- setOrbitSelectionCenter only resets the orbit when the value changes.
- get_object_bounds iterates with list_range.
- The bounds test resets the object system with obj_init and takes objects
  off the free list, so it leaves a valid state for other tests.

* Rename the option to Selection Midpoint, move it to qtFRED's Controls tab, and document it

* Fix overlapping Appearance rows in qtFRED Preferences, realign a settings line

The last commit renumbered the Viewport label size row of the Appearance
group from 4 to 3, which is the Long list menu style row, so the label
size pair fell out of the layout and its spin box drew over the group
title. Put it back at row 4.

---------

Co-authored-by: Dustinspace217 <[email protected]>
2026-11-04 01:17:00 -05:00
dustinspace217andDustinspace217 12bddb8366 P3: Add weapon_info::clone (#7847)
* Add weapon_info::clone

Copies every member of a weapon class, following ship_info::clone, so a
weapon can be created from another as ship templates do. The
CLONE_UNIQUE_PTR macro moves from ship.cpp to utils/string_utils.h so
both clone functions share it.

* Group weapon_info::clone fields as the struct declares them

---------

Co-authored-by: Dustinspace217 <[email protected]>
2026-11-04 01:16:41 -05:00
c47a8bf351 P4: Make cfread_string_len read no more than fits in the buffer (#7863)
* Make cfread_string_len read no more than fits in the buffer

The char* version only checked the stored length with an Assertion, so in
release builds a string longer than the buffer was read past its end, and a
negative length wrote before it. It now reads at most buffer_size - 1
characters, skips the rest so the next read stays aligned, always terminates
the buffer, and logs when characters are dropped.

The SCP_string version clamps a negative or past-the-end length to what the
file still holds, so a corrupt length can't cause a huge allocation.

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

* Warn when cfread_string_len cuts or rejects a string

The char* version now reads up to the full buffer, so a string that fills it
exactly without a terminator is caught rather than cut silently. Both versions
clamp the stored length to what the file still holds and check the read limit
before reading, since cfseek does not; inside a limited pilot section a corrupt
length, negative or too long, throws cfile::max_read_length so the loader can
drop the section. A cut string, a corrupt length or a failed read gets a
Warning, and a failed read no longer leaves the file position off by the bytes
it did read. A buffer size of 0 skips the string without writing to the buffer.

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

* Write the cfread_string_len warnings inline with format arguments

* Share one core between the cfread_string_len overloads

Both overloads now read the length, work out what is stored, read the
body and warn through the same three steps. A length that does not fit
the file throws inside a limited section even when the section ends at
the end of the file, a zero-size buffer still warns about a corrupt
length, and a cut-off length field warns (or throws inside a limited
section) instead of reading as an empty string.

* Share cfread's read-size check with cfread_string_len

The string reader had its own copy of the read-limit check, without the
clamp to end-of-file that cfread does first, so a truncated last section
threw where it used to load. Both now call cf_check_read_size. A negative
length still throws inside a limited section, a cut-off length field only
warns, the cut warning counts bytes, and the helpers sit in an anonymous
namespace.

* Keep cfread_string_len quiet at a clean end of file

A string read at a clean end of file now returns an empty string without a
warning, as cfread_int does; a length field cut partway still warns, so a
truncated pilot list no longer raises one warning per remaining entry. A
zero-size buffer warns when it drops a string, and an invalid file reads as an
empty string again. Adds tests for an empty string inside a limited section,
the one-short boundary, a length still past the limit after the clamp, a
length field cut by the limit, the string returned after a failed read, and
cfread at end of file.

* Warn when a length field is missing before the end of a file

A failed length read now counts as a clean end of file only when cfeof says
so, so a file whose recorded size is larger than its data still warns. Tests
now pin the read-limit check after the end-of-file clamp, cfread at the end of
file past the limit, a length field cut short on disk, the zero-buffer
Assertion in Debug, and an empty string into a zero-size buffer.

* Close the test file before removing it in the cfile temp-file guards

Windows cannot delete an open file, and a failed ASSERT returns with the
file still open, so the guard now closes it first.

---------

Co-authored-by: Dustinspace217 <[email protected]>
Co-authored-by: Claude Opus 5.5 <[email protected]>
2026-11-04 01:15:49 -05:00
Goober5000 3489da442a Merge pull request #7957 from BTawaifi/feature/subsystem-eq
Add __eq to the subsystem scripting handle
2026-11-04 01:04:28 -05:00
Goober5000 abf796f75f Merge pull request #7991 from Goober5000/version_26_3
bump version to 26.3
2026-11-03 23:41:25 -05:00
Goober5000 89d69189c8 bump version to 26.3 2026-11-03 22:39:54 -05:00
Goober5000andDustinspace217 b8bd1daa6e Keep the mission's own ship when a quick start finds no last flown ship (#7962)
A new pilot, or one whose last ship isn't in the current mod, has no last
flown ship. create_default_player_ship() then shows a popup and returns an
error; release builds carry on with the mission's own player ship, but
debug builds asserted. Drop the assert so both builds behave the same.

Co-authored-by: Dustinspace217 <[email protected]>
2026-11-02 22:42:30 -05:00
LuytenKy bb69b1b9db Refactor: CC_preset (#7809)
* CC_preset: Prevent post-construction type modification and handle pst duplication

* remove redundant function

* CC_preset: remove default constructor, require explicit type

* remove duplicate
2026-11-02 22:40:54 -05:00
Goober5000 9fb692f12f Merge pull request #7896 from BTawaifi/feature/noninteractive-no-focus-pause
Don't pause on lost focus in noninteractive runs
2026-11-02 22:40:15 -05:00
Boutros Tawaifi 43ba8b859e Measure afterburner cooldowns on the mission clock (#7895)
* Measure afterburner cooldowns on the mission clock

afterburners_start()/afterburners_stop() timed the +Aburn Cooldown Time delay and
the player's 1.3 s re-engage delay with timer_get_milliseconds(), the real
clock. Under time compression a cooldown therefore lasted longer in mission
time than the table says, and it kept running while the game was paused. The
cooldown was added in 2020 and reused the retail re-engage check's real-clock
"now"; the rest of the file (ABURN_DECAY_TIME, the sound timers) already uses
mission timestamps.

Both delays now use the mission clock. The mission clock does not run outside
missions, so it can still be near zero when the first mission of a session
starts; the "last stopped" and "last started" times therefore start out
invalid instead of 0, so that no delay is in effect before the afterburner has
been used.

Without time compression or pausing, the delays behave as before.

* Drop stale afterburner comment and use fl2i for the cooldown cast

The comment described an earlier startup state rather than the current
behavior. fl2i is the project's float-to-int macro and truncates the same
way the old C-style cast did.

* Remove the remaining afterburner cooldown comment

Timestamps running on mission time is the common case, so it doesn't
need a comment.
2026-11-02 22:39:31 -05:00
Shivansps 49f4b1d9c7 Support GLES in internal shaders and Vulkan software clip distance fallback (#7489)
* GLES Shaders and Vulkan Clip fallback

* clip fallback: change defined name, set from engine side and invert usage

* Proposed changes and fixes

Fixed:
-The OpenGL ES option stays on when OpenGL is off (issue #7984)
-Missing precision in copy-f, volumetric-f and smaa-resolve-f
-msaa-f including z-compress before precision quantification
-missing float literals in volumetric-f
-Fixed decals blending
-Restored glBindFragDataLocation() for use in legacy external shaders
-Enabled GL_ARB_explicit_attrib_location ext for GLSL version below 330 in GL
-Guarded enabling/disabling of GL_CLIP_DISTANCE0_EXT and GL_DEPTH_CLAMP
-Restored PI in lighting.sdr
-removed "External shaders only" indication from cmakelists.txt
2026-11-03 12:20:21 +09:00
Shivansps 9f55626272 Fix incomplete implementation of ETC2 textures in Vulkan (#7699)
* finish etc2 textures in vulkan implementation

* add etcdec implementation

* Enable textureCompressionETC2 at vulkan device creation and fixed outdated comments
2026-11-03 12:20:11 +09:00
Dustinspace217 670df9b97e Keep the mission's own ship when a quick start finds no last flown ship
A new pilot, or one whose last ship isn't in the current mod, has no last
flown ship. create_default_player_ship() then shows a popup and returns an
error; release builds carry on with the mission's own player ship, but
debug builds asserted. Drop the assert so both builds behave the same.
2026-10-29 03:50:51 -07:00
btawaifi 9a70489dbc Add __eq to the subsystem scripting handle
Two subsystem handles are equal when both are valid and refer to the same
subsystem on the same ship. Invalid handles compare as not equal, matching
the other handle types.
2026-10-29 00:56:03 +03:00
btawaifi 52fb1bdc8d Don't pause on lost focus in noninteractive runs
The game pauses when its window loses focus. With in-game options on, that is
governed by the Game.UnfocusedPause option (default on), and the legacy
-no_unfocused_pause flag is ignored, as other legacy flags are. An unattended
run started with -noninteractive therefore stopped as soon as another window
was in front, for example when several test instances run side by side.

-noninteractive means nobody is at the keyboard to resume the game, so
pause_if_unfocused() now returns false for it. This covers all three focus
paths (game_poll, game_pause, game_unpause). Interactive runs are unchanged.
2026-10-18 00:44:52 +03:00
267 changed files with 22849 additions and 1355 deletions

No files matched your search

+13 -4
View File
@@ -57,6 +57,15 @@ endif()
PROJECT(FS2_Open LANGUAGES ${FSO_LANGUAGES})
if (ANDROID)
message(STATUS "Building for Android (NDK)")
set(FSO_BUILD_INCLUDED_LIBS ON CACHE BOOL "" FORCE)
set(USING_PREBUILT_LIBS ON CACHE BOOL "" FORCE)
set(SDL3_USE_PRECOMPILED ON CACHE BOOL "" FORCE)
set(OPENAL_USE_PRECOMPILED ON CACHE BOOL "" FORCE)
set(USING_PREBUILT_VULKAN ON CACHE BOOL "" FORCE)
endif()
# Check if the external modules exists
IF(NOT IS_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/lib/libRocket/Build")
message(FATAL_ERROR "External submodules could not be found. Please make sure you have updated your submodules.")
@@ -109,8 +118,8 @@ OPTION(FSO_BUILD_TESTS "Build unit tests" OFF)
OPTION(FSO_DEVELOPMENT_MODE "Generate binaries in development mode, only use if you know what you're doing!" OFF)
IF(WIN32 OR APPLE)
# On windows and mac the default should be to always build the included libraries
IF(WIN32 OR APPLE OR ANDROID)
# On windows, mac and android the default should be to always build the included libraries
SET(FSO_BUILD_INCLUDED_LIBS_DEFAULT ON)
ELSE()
SET(FSO_BUILD_INCLUDED_LIBS_DEFAULT OFF)
@@ -138,13 +147,13 @@ OPTION(FSO_BUILD_WITH_DISCORD "Build with Discord support" ON)
OPTION(FSO_BUILD_WITH_OPENGL "Enable compilation of the OpenGL renderer" ON)
OPTION(FSO_BUILD_WITH_OPENGL_DEBUG "Enables debug option for OpenGL" OFF)
OPTION(FSO_BUILD_WITH_OPENGL_ES "When building OpenGL, use OpenGL ES compatibility layer (External shaders only!)" OFF)
OPTION(FSO_BUILD_WITH_OPENGL_ES "When building OpenGL, use OpenGL ES compatibility layer" OFF)
OPTION(FSO_BUILD_WITH_VULKAN "Enable compilation of the Vulkan renderer" ON)
if(NOT FSO_BUILD_WITH_OPENGL AND FSO_BUILD_WITH_OPENGL_ES)
# Disable GLES if not building with OpenGL
set(FSO_BUILD_WITH_OPENGL_ES OFF FORCE)
set(FSO_BUILD_WITH_OPENGL_ES OFF CACHE BOOL "" FORCE)
endif()
+1 -1
View File
@@ -7,7 +7,7 @@ if (EXISTS "${PROJECT_SOURCE_DIR}/version_override.cmake")
endif()
set_if_not_defined(FSO_VERSION_MAJOR 26)
set_if_not_defined(FSO_VERSION_MINOR 1)
set_if_not_defined(FSO_VERSION_MINOR 3)
set_if_not_defined(FSO_VERSION_BUILD 0)
# A numeric revision e.g. the CI build number
+1
View File
@@ -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)
+1 -1
View File
@@ -1693,7 +1693,7 @@ static void asteroid_do_area_effect(object *asteroid_objp)
if ( weapon_area_calc_damage(ship_objp, &asteroid_objp->pos, asip->inner_rad, asip->outer_rad, asip->blast, asip->damage, &blast, &damage, asip->outer_rad) == -1 )
continue;
ship_apply_global_damage(ship_objp, asteroid_objp, &asteroid_objp->pos, damage, asip->damage_type_idx);
ship_apply_global_damage(ship_objp, asteroid_objp, &asteroid_objp->pos, damage, asip->damage_type_idx, DamageSource::Explosion);
weapon_area_apply_blast(nullptr, ship_objp, &asteroid_objp->pos, blast, false);
} // end for
}
+17
View File
@@ -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, "");
@@ -1036,6 +1046,13 @@ void bm_init() {
bm_inited = true;
}
bool bm_is_cubemap(int handle)
{
const BM_TYPE type = bm_get_entry(handle)->comp_type;
return type == BM_TYPE_CUBEMAP_DDS || type == BM_TYPE_CUBEMAP_DXT1 || type == BM_TYPE_CUBEMAP_DXT3 ||
type == BM_TYPE_CUBEMAP_DXT5;
}
int bm_is_compressed(int num) {
auto entry = bm_get_entry(num);
BM_TYPE type = BM_TYPE_NONE;
+15
View File
@@ -436,6 +436,13 @@ void bm_get_filename(int bitmapnum, char *filename);
*/
const char *bm_get_filename(int handle);
/**
* @brief Gets the name of the file that holds the pixels of this bitmap
*
* This is the same as bm_get_filename(), except for an EFF. For an EFF it is the file of the individual image.
*/
const char *bm_get_image_filename(int handle);
/**
* @brief Unloads all used bitmaps, should only ever be called by game_shutdown()
*
@@ -594,6 +601,14 @@ void bm_get_components(ubyte *pixel, ubyte *r, ubyte *g, ubyte *b, ubyte *a);
*/
int bm_is_compressed(int handle);
/**
* @brief Returns true if the bitmap indexed by handle was loaded from a cubemap image file
*
* @note Render targets return false, even cubemap ones, and so do BC7 (DX10) DDS cubemaps, whose cube flag the DDS
* loader does not keep
*/
bool bm_is_cubemap(int handle);
/**
* @brief Gets the correct TCACHE_TYPE for compressed graphics (uncompressed are assumed TCACHE_TYPE_NORMAL)
*/
+92 -17
View File
@@ -33,7 +33,7 @@
#include "parse/encrypt.h"
#include "cfilesystem.h"
#include <algorithm>
#include <limits>
char Cfile_root_dir[CFILE_ROOT_DIRECTORY_LEN] = "";
@@ -185,7 +185,12 @@ int cfile_init(const char *exe_dir, const char *cdrom_dir)
char buf[CFILE_ROOT_DIRECTORY_LEN];
strncpy(buf, exe_dir, CFILE_ROOT_DIRECTORY_LEN - 1);
#ifndef __ANDROID__
strncpy(buf, exe_dir, CFILE_ROOT_DIRECTORY_LEN - 1);
#else
(void)exe_dir;
strncpy(buf, os_android_get_working_folder_path().c_str(), CFILE_ROOT_DIRECTORY_LEN - 1);
#endif
buf[CFILE_ROOT_DIRECTORY_LEN - 1] = '\0';
@@ -1105,28 +1110,98 @@ void cfread_string(char *buf, int n, CFILE *file)
} while (c != 0 );
}
void cfread_string_len(char *buf, int buffer_size, CFILE *file)
{
int len;
len = cfread_int(file);
Assertion( (len <= buffer_size), "String cannot fit in buffer, len: %i, buffer: %i", len, buffer_size);
if (len) {
cfread(buf, len, 1, file);
Assertion(len != buffer_size || buf[len - 1] == 0, "Unterminated string does not fit in buffer");
}
namespace {
struct string_len_header {
int len = 0;
int stored = 0;
int start = 0;
bool length_cut = false;
};
buf[MIN(len, buffer_size - 1)] = 0;
// a past-the-end length is clamped like any cfread, but a negative one has nothing to clamp to, so inside a
// limited section it throws and the loader skips the rest of the section
string_len_header read_string_len_header(CFILE* file)
{
string_len_header h;
if (!cf_is_valid(file)) {
return h;
}
int before = cftell(file);
h.length_cut = cfread(&h.len, sizeof(h.len), 1, file) != 1;
h.start = cftell(file);
if (h.length_cut) {
// a clean end of file reads as an empty string without a warning, as cfread_int does
h.length_cut = (h.start != before) || (cfeof(file) == 0);
return h;
}
h.len = INTEL_INT(h.len);
if ((h.len < 0) && (file->max_read_len != 0)) {
throw cfile::max_read_length("Attempted to read a string of negative length " + std::to_string(h.len));
}
h.stored = static_cast<int>(cf_check_read_size(file, static_cast<size_t>(std::max(h.len, 0))));
return h;
}
// reads count of the stored characters and leaves the file just past the whole string; false if the read failed
bool read_string_len_body(const string_len_header& h, char* dest, int count, CFILE* file)
{
bool ok = (count <= 0) || (cfread(dest, count, 1, file) == 1);
if (!ok || (h.stored > count)) {
cfseek(file, h.start + h.stored, CF_SEEK_SET);
}
return ok;
}
// warned last, so the file position is right even if a debug build stops here
void warn_string_len(const string_len_header& h, bool read_ok, bool cut, int buffer_size, CFILE* file)
{
if (h.length_cut) {
Warning(LOCATION, "cfread_string_len: cut-off string length field in %s", cf_get_filename(file));
} else if (!read_ok) {
Warning(LOCATION,
"cfread_string_len: could not read a string of %d characters in %s",
h.stored,
cf_get_filename(file));
} else if (h.len != h.stored) {
Warning(LOCATION, "cfread_string_len: corrupt string length %d in %s", h.len, cf_get_filename(file));
} else if (cut) {
Warning(LOCATION,
"cfread_string_len: a string of %d bytes was cut to fit a buffer of %d in %s",
h.len,
buffer_size,
cf_get_filename(file));
}
}
} // namespace
void cfread_string_len(char* buf, int buffer_size, CFILE* file)
{
Assertion(buffer_size > 0, "cfread_string_len needs room for at least the terminator, buffer: %i", buffer_size);
string_len_header h = read_string_len_header(file);
if (buffer_size <= 0) {
bool read_ok = read_string_len_body(h, buf, 0, file);
warn_string_len(h, read_ok, h.stored > 0, buffer_size, file);
return;
}
int count = std::min(h.stored, buffer_size);
bool read_ok = read_string_len_body(h, buf, count, file);
int kept = read_ok ? count : 0;
bool cut = (kept == buffer_size) && (memchr(buf, 0, buffer_size) == nullptr);
buf[std::min(kept, buffer_size - 1)] = 0;
warn_string_len(h, read_ok, cut, buffer_size, file);
}
SCP_string cfread_string_len(CFILE *file)
{
int len = cfread_int(file);
string_len_header h = read_string_len_header(file);
SCP_string str;
str.resize(len);
if (len)
cfread(&str[0], len, 1, file);
str.resize(h.stored);
bool read_ok = read_string_len_body(h, str.data(), h.stored, file);
if (!read_ok) {
str.clear();
}
warn_string_len(h, read_ok, false, 0, file);
return str;
}
+8 -2
View File
@@ -320,20 +320,26 @@ void cfread_string(char *buf,int n, CFILE *file);
/**
* @brief Read a fixed length string that is not null-terminated, with the length stored in file.
*
* If it is not null-terminated, the length of the string *must* be less than the size of the buffer (to make room for one)
*
* Keeps at most buffer_size - 1 characters and leaves the file just past the whole string, unless it throws.
*
* @param buf Pre-allocated array to store string
* @param buffer_size Size of pre-allocated array
* @param file File to read from
*
* @note Appends NULL character to string (buf)
* @note Bad data gets a warning; at a clean end of file, or with an invalid file, the string is empty and nothing is
* warned. Inside a limited section (cf_set_max_read_len), a negative length, or a read that still passes the limit
* once clamped to the end of the file, throws cfile::max_read_length.
*/
void cfread_string_len(char *buf,int buffer_size, CFILE *file);
/**
* @brief Read a string from the file where the length is stored in the file
*
* @param file The file to read the string from
* @return The string that was read
*
* @note Warns and throws like the char* overload, except that nothing is cut.
*/
SCP_string cfread_string_len(CFILE *file);
+24 -14
View File
@@ -193,21 +193,8 @@ int cfseek( CFILE *cfile, int offset, int where )
}
// cfread() reads from a file
//
// returns: returns the number of full elements read
//
//
int cfread(void *buf, int elsize, int nelem, CFILE *cfile)
size_t cf_check_read_size(CFILE* cfile, size_t size)
{
if(!cf_is_valid(cfile))
return 0;
size_t size = elsize*nelem;
if(buf == NULL || size <= 0)
return 0;
if ( (cfile->raw_position+size) > cfile->size ) {
Assertion(cfile->raw_position <= cfile->size, "Invalid raw_position value detected!");
size = cfile->size - cfile->raw_position;
@@ -226,6 +213,29 @@ int cfread(void *buf, int elsize, int nelem, CFILE *cfile)
}
}
return size;
}
// cfread() reads from a file
//
// returns: returns the number of full elements read
//
//
int cfread(void *buf, int elsize, int nelem, CFILE *cfile)
{
if(!cf_is_valid(cfile))
return 0;
size_t size = elsize*nelem;
if(buf == nullptr || size <= 0)
return 0;
size = cf_check_read_size(cfile, size);
if (size < 1) {
return 0;
}
size_t bytes_read;
if (cfile->data != nullptr) {
// This is a file from memory
+5
View File
@@ -62,4 +62,9 @@ void cf_check_compression(CFILE* cfile);
// Used to clear compression info data and free dynamic memory used by compression
void cf_clear_compression_info(CFILE* cfile);
// Clamps a read of size bytes to the end of the file and returns the clamped size, or 0 at the end of the file
// without checking the limit; throws cfile::max_read_length if the clamped read would pass the file's read limit
// (cf_set_max_read_len).
size_t cf_check_read_size(CFILE* cfile, size_t size);
#endif
+88 -54
View File
@@ -75,6 +75,7 @@ const char *cmdline_arg_types[] =
enum class flag_output_type {
Binary,
Json_V1,
Json_V2,
};
// variables
@@ -555,7 +556,12 @@ int Cmdline_show_pos = 0;
int Cmdline_show_stats = 0;
int Cmdline_save_render_targets = 0;
bool Cmdline_window = false;
#ifdef __ANDROID__
// Default to borderless in the legacy window-mode path as well.
bool Cmdline_fullscreen_window = true;
#else
bool Cmdline_fullscreen_window = false;
#endif
std::optional<std::pair<uint16_t, uint16_t>>Cmdline_window_res = std::nullopt;
char *Cmdline_res = 0;
char *Cmdline_center_res = 0;
@@ -1358,51 +1364,58 @@ static void handle_unix_modlist(char **modlist, size_t *len)
// external entry point into this modules
static void json_add_version(json_t* root) {
auto version_obj = json_object();
json_object_set_new(version_obj, "full", json_string(FS_VERSION_FULL));
json_object_set_new(version_obj, "major", json_integer(FS_VERSION_MAJOR));
json_object_set_new(version_obj, "minor", json_integer(FS_VERSION_MINOR));
json_object_set_new(version_obj, "build", json_integer(FS_VERSION_BUILD));
json_object_set_new(version_obj, "has_revision", json_boolean(FS_VERSION_HAS_REVIS));
json_object_set_new(version_obj, "revision", json_integer(FS_VERSION_REVIS));
json_object_set_new(version_obj, "revision_str", json_string(FS_VERSION_REVIS_STR));
json_object_set_new(root, "version", version_obj);
}
static void json_add_easy_flags(json_t* root) {
auto easy_array = json_array();
for (auto& easy_flag : easy_flags) {
json_array_append_new(easy_array, json_string(easy_flag.name));
}
json_object_set_new(root, "easy_flags", easy_array);
}
static void json_add_flags(json_t* root) {
auto flags_array = json_array();
for (auto& flag : exe_params) {
auto flag_obj = json_object();
json_object_set_new(flag_obj, "name", json_string(flag.name));
json_object_set_new(flag_obj, "description", json_string(flag.desc));
json_object_set_new(flag_obj, "fso_only", json_boolean(flag.fso_only));
json_object_set_new(flag_obj, "on_flags", json_integer(flag.on_flags));
json_object_set_new(flag_obj, "off_flags", json_integer(flag.off_flags));
json_object_set_new(flag_obj, "type", json_string(flag.type));
json_object_set_new(flag_obj, "web_url", json_string(flag.web_url));
json_array_append_new(flags_array, flag_obj);
}
json_object_set_new(root, "flags", flags_array);
}
static json_t* json_get_v1() {
auto root = json_object();
{
auto version_obj = json_object();
json_add_version(root);
json_add_easy_flags(root);
json_add_flags(root);
json_object_set_new(version_obj, "full", json_string(FS_VERSION_FULL));
json_object_set_new(version_obj, "major", json_integer(FS_VERSION_MAJOR));
json_object_set_new(version_obj, "minor", json_integer(FS_VERSION_MINOR));
json_object_set_new(version_obj, "build", json_integer(FS_VERSION_BUILD));
json_object_set_new(version_obj, "has_revision", json_boolean(FS_VERSION_HAS_REVIS));
json_object_set_new(version_obj, "revision", json_integer(FS_VERSION_REVIS));
json_object_set_new(version_obj, "revision_str", json_string(FS_VERSION_REVIS_STR));
json_object_set_new(root, "version", version_obj);
}
{
auto easy_array = json_array();
for (auto& easy_flag : easy_flags) {
json_array_append_new(easy_array, json_string(easy_flag.name));
}
json_object_set_new(root, "easy_flags", easy_array);
}
{
auto flags_array = json_array();
for (auto& flag : exe_params) {
auto flag_obj = json_object();
json_object_set_new(flag_obj, "name", json_string(flag.name));
json_object_set_new(flag_obj, "description", json_string(flag.desc));
json_object_set_new(flag_obj, "fso_only", json_boolean(flag.fso_only));
json_object_set_new(flag_obj, "on_flags", json_integer(flag.on_flags));
json_object_set_new(flag_obj, "off_flags", json_integer(flag.off_flags));
json_object_set_new(flag_obj, "type", json_string(flag.type));
json_object_set_new(flag_obj, "web_url", json_string(flag.web_url));
json_array_append_new(flags_array, flag_obj);
}
json_object_set_new(root, "flags", flags_array);
}
{
auto caps_array = json_array();
@@ -1513,6 +1526,19 @@ static json_t* json_get_v1() {
return root;
}
// json_v2 reports version, supported flags, and the preferences path for launchers,
// without querying graphics, audio, or joystick devices.
static json_t* json_get_v2() {
auto root = json_object();
json_add_version(root);
json_add_easy_flags(root);
json_add_flags(root);
json_object_set_new(root, "pref_path", json_string(os_get_config_path().c_str()));
return root;
}
static void write_flags_file() {
FILE *fp = fopen("flags.lch","w");
@@ -1570,6 +1596,8 @@ static flag_output_type get_flags_output_type() {
return flag_output_type::Binary;
} else if (type == "json_v1") {
return flag_output_type::Json_V1;
} else if (type == "json_v2") {
return flag_output_type::Json_V2;
} else {
// This is supposed to make it easy for the launcher to recognize an unsupported type
printf("OUTPUT TYPE NOT SUPPORTED!\n");
@@ -1580,17 +1608,23 @@ static flag_output_type get_flags_output_type() {
static void write_flags() {
auto type = get_flags_output_type();
switch(type) {
case flag_output_type::Binary:
write_flags_file();
break;
case flag_output_type::Json_V1:
json_t* root = json_get_v1();
json_dumpf(root, stdout, JSON_INDENT(4));
json_decref(root);
break;
if (type == flag_output_type::Binary) {
write_flags_file();
return;
}
json_t* root = (type == flag_output_type::Json_V1) ? json_get_v1() : json_get_v2();
#ifdef __ANDROID__
char* dumped = json_dumps(root, JSON_INDENT(4));
os_android_set_flags_string(dumped ? dumped : "");
free(dumped);
#else
json_dumpf(root, stdout, JSON_INDENT(4));
#endif
json_decref(root);
}
bool SetCmdlineParams()
@@ -1787,6 +1821,7 @@ bool SetCmdlineParams()
options::OptionsManager::instance()->setOverride("Graphics.WindowMode",
std::unique_ptr<json_t>(json_integer(static_cast<int>(os::ViewportState::Windowed))), "-window");
Cmdline_window = true;
Cmdline_fullscreen_window = false;
}
if ( fullscreen_window_arg.found( ) )
@@ -2409,10 +2444,9 @@ bool SetCmdlineParams()
if (gr_sync_validation_arg.found()) {
// Enables the Vulkan validation layer + debug messenger + synchronization
// validation feature (see VulkanRenderer::initializeInstance), but NOT the
// engine-level graphics debug output: -gr_debug additionally draws debug
// overlays (e.g. output_uniform_debug_data), which are unrelated to GPU
// sync validation and would otherwise appear as spurious on-screen
// artifacts. Keep this a pure GPU-validation switch.
// engine-level graphics debug output: -gr_debug additionally feeds extra
// stat groups into the ImGui frame profiler overlay (see gr_get_debug_stats),
// which are unrelated to GPU sync validation. Keep this a pure GPU-validation switch.
Cmdline_gr_sync_validation = true;
}
+11 -15
View File
@@ -1322,7 +1322,7 @@ bool control_config_accept(bool API_Access)
// Check if a hardcoded preset with name already exists. If so, complain to user and force retry
auto it = std::find_if(Control_config_presets.begin(), Control_config_presets.end(), [str](CC_preset& p) {
return ((p.name == str) && ((p.type == Preset_t::tbl) || (p.type == Preset_t::hardcode)));
return ((p.name == str) && ((p.get_type() == Preset_t::tbl) || (p.get_type() == Preset_t::hardcode)));
});
if (it != Control_config_presets.end()) {
@@ -1353,15 +1353,14 @@ bool control_config_accept(bool API_Access)
}
// Pack the current bindings into a preset, then save the file
CC_preset preset;
preset.name = std::move(str);
CC_preset preset(std::move(str), Preset_t::pst);
std::copy(Control_config.begin(), Control_config.end(), std::back_inserter(preset.bindings));
Control_config_presets.push_back(preset);
save_preset_file(preset, true);
// Reload the presets from file. Do this instead of just pushing the preset to the vector direct to get
// consistant ordering
Control_config_presets.resize(1);
Control_config_presets.erase(Control_config_presets.begin() + 1, Control_config_presets.end());
load_preset_files();
// finally, save the new preset so that changes will get saved to this preset
@@ -1778,7 +1777,7 @@ bool control_config_create_new_preset(const SCP_string& newName, bool overwrite)
// Check if a hardcoded preset with name already exists. If so, complain to user and force retry
auto it = std::find_if(Control_config_presets.begin(), Control_config_presets.end(), [newName](CC_preset& p) {
return (p.name == newName) && ((p.type == Preset_t::tbl) || (p.type == Preset_t::hardcode));
return (p.name == newName) && ((p.get_type() == Preset_t::tbl) || (p.get_type() == Preset_t::hardcode));
});
if (it != Control_config_presets.end()) {
@@ -1791,9 +1790,7 @@ bool control_config_create_new_preset(const SCP_string& newName, bool overwrite)
}
// Pack the current bindings into a preset, then save the file
CC_preset preset;
preset.name = newName;
preset.type = Preset_t::pst;
CC_preset preset(newName, Preset_t::pst);
std::copy(Control_config.begin(), Control_config.end(), std::back_inserter(preset.bindings));
// Done with the file
@@ -1805,7 +1802,7 @@ bool control_config_create_new_preset(const SCP_string& newName, bool overwrite)
save_preset_file(preset, true);
// Reload the presets from file.
Control_config_presets.resize(1);
Control_config_presets.erase(Control_config_presets.begin() + 1, Control_config_presets.end());
load_preset_files(newName);
// use the newly created preset
@@ -1813,10 +1810,9 @@ bool control_config_create_new_preset(const SCP_string& newName, bool overwrite)
return true;
} else if ((clone->name != preset.name) || (clone->type != Preset_t::pst)) {
} else if ((clone->name != preset.name) || (clone->get_type() != Preset_t::pst)) {
// Complain and ignore if the preset names or the type differs
return false;
}
return false; //should be unreachable, but just in case
@@ -1826,7 +1822,7 @@ bool control_config_clone_preset(const CC_preset& preset, const SCP_string& newN
// Check if a hardcoded preset with name already exists. If so, complain to user and force retry
auto it = std::find_if(Control_config_presets.begin(), Control_config_presets.end(), [newName](CC_preset& p) {
return (p.name == newName) && ((p.type == Preset_t::tbl) || (p.type == Preset_t::hardcode));
return (p.name == newName) && ((p.get_type() == Preset_t::tbl) || (p.get_type() == Preset_t::hardcode));
});
if (it != Control_config_presets.end()) {
@@ -1838,13 +1834,13 @@ bool control_config_clone_preset(const CC_preset& preset, const SCP_string& newN
return false;
}
CC_preset newPreset = preset;
newPreset.name = newName;
CC_preset newPreset(newName, Preset_t::pst);
newPreset.bindings = preset.bindings;
bool success = save_preset_file(newPreset, false);
// Reload the presets from file.
Control_config_presets.resize(1);
Control_config_presets.erase(Control_config_presets.begin() + 1, Control_config_presets.end());
load_preset_files(newName);
// use the newly cloned preset
+18 -10
View File
@@ -572,20 +572,28 @@ public:
* A preset, a collection of bindings for use in Control_config with an associated name
*/
class CC_preset {
public:
public:
SCP_vector<CCB> bindings;
SCP_string name;
Preset_t type = Preset_t::hardcode;
public:
CC_preset() = default;
CC_preset(const CC_preset& A) = default;
private:
// No default value: the type is always set explicitly by the constructor
Preset_t type;
CC_preset& operator=(const CC_preset&);
/*!
* Checks if the given preset is a duplicate of this one
*/
bool is_duplicate_of(CC_preset&);
public:
// No default constructor: every preset must be created with an explicit type
CC_preset(SCP_string preset_name, Preset_t preset_type) : name(std::move(preset_name)), type(preset_type) {}
CC_preset(const CC_preset&) = default;
// Preserve target's preset type during copy assignment
CC_preset& operator=(const CC_preset& other);
Preset_t get_type() const
{
return type;
}
bool is_duplicate_of(const CC_preset& other) const;
};
/*!
+14 -18
View File
@@ -292,10 +292,8 @@ void control_config_common_init_bindings() {
// init default preset
Control_config_presets.clear();
CC_preset preset;
CC_preset preset("default", Preset_t::hardcode);
preset.bindings.reserve(Control_config.size());
preset.name = "default";
preset.type = Preset_t::hardcode;
for (auto &item : Control_config) {
preset.bindings.push_back(CCB(item));
@@ -1733,14 +1731,13 @@ size_t read_bind_1(CC_preset &preset) {
*/
void control_config_common_read_section(int s, bool first_override) {
Assertion((s == 0) || (s == 1), "Expected value of s to be either 0 or 1, found %i instead.", s);
CC_preset new_preset;
CC_preset new_preset("", Preset_t::tbl);
// Set references to the default preset and bindings
auto& default_preset = Control_config_presets[0];
auto& default_bindings = default_preset.bindings;
new_preset.bindings.clear();
new_preset.type = Preset_t::tbl;
if (s == 0) {
// #ControlConfigOverride
@@ -3002,23 +2999,22 @@ CCI_builder& CCI_builder::operator()(IoActionId action_id, short primary, short
return *this;
}
CC_preset& CC_preset::operator=(const CC_preset& A) {
name = A.name;
type = A.type;
std::copy(A.bindings.begin(), A.bindings.end(), bindings.begin());
CC_preset& CC_preset::operator=(const CC_preset& other)
{
if (this != &other) {
bindings = other.bindings;
name = other.name;
// 'type' is intentionally preserved to maintain instance identity
}
return *this;
};
}
bool CC_preset::is_duplicate_of(CC_preset& A) {
for (size_t i = 0; i < A.bindings.size(); ++i) {
if (bindings[i] != A.bindings[i]) {
// Found a binding that's different. Thus, this preset is not a duplicate
bool CC_preset::is_duplicate_of(const CC_preset& other) const
{
for (size_t i = 0; i < other.bindings.size(); ++i) {
if (bindings[i] != other.bindings[i]) {
return false;
}
}
// Else, did not find any differences in the bindings. Thus, this preset is a duplicate
return true;
}
+3 -5
View File
@@ -74,9 +74,7 @@ void load_preset_files(SCP_string clone) {
// Start reading in data
CC_preset preset;
preset.name = file;
preset.type = Preset_t::pst;
CC_preset preset(file, Preset_t::pst);
preset.bindings.resize(Control_config.size());
size_t size;
@@ -134,7 +132,7 @@ void load_preset_files(SCP_string clone) {
if ((clone == preset.name) || (it == Control_config_presets.end())) {
Control_config_presets.push_back(preset);
} else if ((it->name != preset.name) || (it->type != Preset_t::pst)) {
} else if ((it->name != preset.name) || ((it->get_type() != Preset_t::pst))) {
if (gameseq_get_state() == GS_STATE_CONTROL_CONFIG) {
popup(PF_TITLE_WHITE | PF_USE_AFFIRMATIVE_ICON, 1, POPUP_OK, "Preset '%s' is a duplicate of an existing preset, ignoring", preset.name.c_str());
} else {
@@ -173,7 +171,7 @@ bool delete_preset_file(CC_preset preset) {
cf_delete(filename.c_str(), CF_TYPE_PLAYER_BINDS, CF_LOCATION_ROOT_USER | CF_LOCATION_ROOT_GAME | CF_LOCATION_TYPE_ROOT);
// Reload the presets from file.
Control_config_presets.resize(1);
Control_config_presets.erase(Control_config_presets.begin() + 1, Control_config_presets.end());
load_preset_files();
return true;
+6 -4
View File
@@ -1,12 +1,16 @@
#ifdef GL_ES
precision highp float;
precision highp sampler2DArray;
#endif
#include "gamma.sdr"
layout(location=0) out vec4 fragOut0;
#ifdef VULKAN
layout (location = 0) in vec4 fragTexCoord;
layout (location = 1) in vec4 fragColor;
layout (location = 0) out vec4 fragOut0;
layout (set = 1, binding = 1) uniform sampler2DArray baseMap;
layout (set = 2, binding = 0, std140) uniform genericData {
@@ -17,8 +21,6 @@ layout (set = 2, binding = 0, std140) uniform genericData {
in vec4 fragTexCoord;
in vec4 fragColor;
out vec4 fragOut0;
uniform sampler2DArray baseMap;
layout (std140) uniform genericData {
@@ -1,3 +1,7 @@
#ifdef GL_ES
precision highp float;
#endif
#ifdef VULKAN
layout (location = 0) in vec4 vertPosition;
layout (location = 1) in vec4 vertColor;
+7 -2
View File
@@ -1,11 +1,16 @@
#ifdef GL_ES
precision highp float;
precision highp sampler2D;
#endif
layout(location=0) out vec4 fragOut0;
#ifdef VULKAN
layout(location = 0) in vec2 fragTexCoord;
layout(location = 0) out vec4 fragOut0;
layout(set = 1, binding = 1) uniform sampler2D bloomed;
#else
in vec4 fragTexCoord;
out vec4 fragOut0;
uniform sampler2D bloomed;
#endif
+8 -2
View File
@@ -1,9 +1,15 @@
#ifdef GL_ES
precision highp float;
precision highp int;
precision highp sampler2D;
#endif
layout(location = 0) out vec4 fragOut0;
#ifdef VULKAN
layout(location = 0) in vec2 fragTexCoord;
layout(location = 0) out vec4 fragOut0;
layout(set = 1, binding = 1) uniform sampler2D tex;
#else
out vec4 fragOut0;
in vec4 fragTexCoord;
uniform sampler2D tex;
#endif
+8 -2
View File
@@ -1,12 +1,18 @@
#ifdef GL_ES
precision highp float;
precision highp sampler2D;
#endif
layout(location = 0) out vec4 fragOut0;
#ifdef VULKAN
layout(location = 0) in vec2 fragTexCoord;
layout(location = 0) out vec4 fragOut0;
layout(set = 1, binding = 1) uniform sampler2D tex;
#else
in vec4 fragTexCoord;
out vec4 fragOut0;
uniform sampler2D tex;
#endif
const float Luminance = 0.08;
const float fMiddleGray = 0.2;
const float fWhiteCutoff = 0.4;
+8 -2
View File
@@ -1,10 +1,16 @@
#ifdef GL_ES
precision highp float;
precision highp sampler2D;
precision highp sampler2DArray;
#endif
layout(location = 0) out vec4 fragOut0;
#ifdef VULKAN
layout(location = 0) in vec2 fragTexCoord;
layout(location = 0) out vec4 fragOut0;
layout(set = 1, binding = 1) uniform sampler2D tex;
#else
in vec4 fragTexCoord;
out vec4 fragOut0;
#ifdef COPY_ARRAY
uniform sampler2DArray tex;
#else
+15 -4
View File
@@ -1,3 +1,9 @@
#ifdef GL_ES
precision highp float;
precision highp int;
precision highp sampler2D;
precision highp sampler2DArray;
#endif
// NOTE: The technique and some of this code is based on this tutorial:
// http://martindevans.me/game-development/2015/02/27/Drawing-Stuff-On-Other-Stuff-With-Deferred-Screenspace-Decals/
@@ -29,9 +35,14 @@ layout (set = 1, binding = 1) uniform sampler2DArray decalTextures;
layout (set = 1, binding = 4) uniform sampler2D gDepthBuffer;
layout (set = 1, binding = 6) uniform sampler2D gNormalBuffer;
#else
out vec4 fragOut0; // Diffuse buffer
out vec4 fragOut1; // Normal buffer
out vec4 fragOut2; // Emissive buffer
layout (location=0) out vec4 fragOut0; // Diffuse buffer
#ifndef GL_ES
layout (location=1) out vec4 fragOut1; // Normal buffer
layout (location=2) out vec4 fragOut2; // Emissive buffer
#else
layout (location=2) out vec4 fragOut1; // Normal buffer -> COLOR_ATTACHMENT2
layout (location=4) out vec4 fragOut2; // Emissive buffer -> COLOR_ATTACHMENT4
#endif
flat in mat4 invModelMatrix;
flat in vec3 decalDirection;
@@ -124,7 +135,7 @@ vec3 getPixelNormal(vec3 frag_position, vec2 tex_coord, inout float alpha, out v
}
// Make a smooth alpha transition leading up to an edge
alpha = alpha * (1 - smoothstep(angle_fade_start, normal_angle_cutoff, angle));
alpha = alpha * (1.0 - smoothstep(angle_fade_start, normal_angle_cutoff, angle));
return normal;
}
+4
View File
@@ -1,3 +1,7 @@
#ifdef GL_ES
precision highp float;
precision highp int;
#endif
#ifdef VULKAN
// Binding 0: box vertex positions
@@ -1,3 +1,10 @@
#ifdef GL_ES
#extension GL_EXT_clip_cull_distance : enable
precision highp float;
precision highp int;
precision highp sampler2DArray;
#endif
#ifdef VULKAN
layout(set = 2, binding = 0, std140)
#else
@@ -20,22 +27,28 @@ uniform genericData {
uint clipEnabled;
};
layout(location = 0) out vec4 fragOut0;
#ifdef VULKAN
layout(set = 1, binding = 1) uniform sampler2DArray baseMap;
layout(location = 0) in vec4 fragTexCoord;
layout(location = 1) in vec4 fragColor;
layout(location = 0) out vec4 fragOut0;
#ifdef SDR_FLAG_SW_CLIP_FALLBACK
layout(location = 2) in float vClipDist;
#endif
#else
#ifdef SDR_FLAG_SW_CLIP_FALLBACK
in float vClipDist;
#endif
uniform sampler2DArray baseMap;
in vec4 fragTexCoord;
in vec4 fragColor;
out vec4 fragOut0;
#endif
@@ -43,6 +56,11 @@ out vec4 fragOut0;
void main()
{
#ifdef SDR_FLAG_SW_CLIP_FALLBACK
if (clipEnabled != 0u && vClipDist < 0.0) {
discard;
}
#endif
vec4 baseColor = texture(baseMap, vec3(fragTexCoord.xy, float(baseMapIndex)));
if(alphaThreshold > baseColor.a) discard;
baseColor.rgb = (srgb == 1) ? srgb_to_linear(baseColor.rgb) : baseColor.rgb;
@@ -1,3 +1,9 @@
#ifdef GL_ES
#extension GL_EXT_clip_cull_distance : enable
precision highp float;
precision highp int;
#endif
#ifdef VULKAN
layout(set = 2, binding = 0, std140)
#else
@@ -38,10 +44,15 @@ layout(location = 2) in vec4 vertTexCoord;
layout(location = 0) out vec4 fragTexCoord;
layout(location = 1) out vec4 fragColor;
#ifdef SDR_FLAG_SW_CLIP_FALLBACK
layout(location = 2) out float vClipDist;
#endif
#else
out float gl_ClipDistance[1];
#ifdef SDR_FLAG_SW_CLIP_FALLBACK
out float vClipDist;
#endif
out vec4 fragTexCoord;
in vec4 vertTexCoord;
@@ -58,13 +69,16 @@ void main()
gl_Position = projMatrix * modelViewMatrix * vertPosition;
if (clipEnabled != 0u) {
#ifndef SDR_FLAG_SW_CLIP_FALLBACK
gl_ClipDistance[0] = dot(clipEquation, modelMatrix * vertPosition);
#else
vClipDist = dot(clipEquation, modelMatrix * vertPosition);
#endif
} else {
#ifdef VULKAN
// Vulkan has no glEnable(GL_CLIP_DISTANCE0) equivalent — clip distances are
// always evaluated when declared in the shader. Must write a positive value
// when clipping is disabled to prevent undefined-value vertex culling.
#ifndef SDR_FLAG_SW_CLIP_FALLBACK
gl_ClipDistance[0] = 1.0;
#endif
#else
vClipDist = 1.0;
#endif
}
}
@@ -1,9 +1,14 @@
out vec4 fragOut0;
out vec4 fragOut1;
out vec4 fragOut2;
out vec4 fragOut3;
out vec4 fragOut4;
out vec4 fragOut5;
#ifdef GL_ES
precision highp float;
#endif
layout (location=0) out vec4 fragOut0;
layout (location=1) out vec4 fragOut1;
layout (location=2) out vec4 fragOut2;
layout (location=3) out vec4 fragOut3;
layout (location=4) out vec4 fragOut4;
layout (location=5) out vec4 fragOut5;
void main()
{
fragOut0 = vec4(0.0, 0.0, 0.0, 1.0); // color
+46 -12
View File
@@ -1,13 +1,21 @@
//? #version 150
#ifdef GL_ES
precision highp float;
precision highp int;
precision highp sampler2D;
precision highp samplerCube;
precision highp sampler2DArrayShadow;
#endif
#include "lighting.sdr" //! #include "lighting.sdr"
#include "gamma.sdr" //! #include "gamma.sdr"
#include "shadows.sdr" //! #include "shadows.sdr"
#include "z-compress.sdr" //! #include "z-compress.sdr"
#ifdef VULKAN
#ifndef GL_ES
layout(location = 0) out vec4 fragOut0;
#else
out vec4 fragOut0;
layout(location = 5) out vec4 fragOut0; //COLOR_ATTACHMENTn is on location n in ES, here it uses COLOR_ATTACHMENT5
#endif
#ifdef VULKAN
@@ -18,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
@@ -33,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;
@@ -63,6 +73,10 @@ uniform shadowCascadeParams {
int cascade_count;
float rtShadowBiasMin;
float rtShadowBiasMax;
int rtShadowSampleCount;
int rtaoSampleCount;
float rtaoRadius;
float rtaoStrength;
mat4 shadow_mv_matrix;
@@ -72,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
@@ -145,7 +160,7 @@ void GetLightInfo(vec3 position, in float alpha, in vec3 reflectDir, out vec3 li
dist = length(lightDirOut);
lightDist = dist;
// Energy conservation term
float alpha_adjust = clamp(alpha + (sourceRadius/(2*dist)), 0.0, 1.0);
float alpha_adjust = clamp(alpha + (sourceRadius/(2.0*dist)), 0.0, 1.0);
area_normalisation = alpha/alpha_adjust;
area_normalisation *= area_normalisation;
//end chunk
@@ -188,7 +203,7 @@ void GetLightInfo(vec3 position, in float alpha, in vec3 reflectDir, out vec3 li
float dist = length(lightDirOut);
lightDist = dist;
// Energy conservation term
float alpha_adjust = min(alpha + (sourceRadius/(2*dist)), 1.0);
float alpha_adjust = min(alpha + (sourceRadius/(2.0*dist)), 1.0);
area_normalisation = alpha/alpha_adjust;
// don't need to square as it's a line rather than a sphere.
//end chunk
@@ -235,11 +250,11 @@ void ComputeEnvLight(float alpha, float ao, vec3 light_dir, vec3 eyeDir, vec3 no
// so let s = (2/alpha^2 - 2)
// 1/2 log(s + 1) = 1/2 log(2/alpha^2 -1)
const float ENV_REZ = 512; // Ideally this would be #define'd and shader recompiled with envmap rez changes
const float REZ_BIAS = log2(ENV_REZ * sqrt(3));
const float ENV_REZ = 512.0; // Ideally this would be #define'd and shader recompiled with envmap rez changes
const float REZ_BIAS = log2(ENV_REZ * sqrt(3.0));
float alphaSqr = alpha * alpha;
float rough_bias = 0.5 * log2(2/alphaSqr - 1);
float rough_bias = 0.5 * log2(2.0/alphaSqr - 1.0);
float mip_bias = REZ_BIAS - rough_bias;
// Sample light, using mip bias to blur it.
@@ -257,7 +272,7 @@ void ComputeEnvLight(float alpha, float ao, vec3 light_dir, vec3 eyeDir, vec3 no
// Pseudo-IBL, so use k_IBL
float k = alpha * alpha/ 2.0f;
float NdotL = max(dot(light_dir, normal),0);
float NdotL = max(dot(light_dir, normal),0.0);
float g1vNL = GeometrySchlickGGX(NdotL, k);
vec3 specEnvLighting = specEnvColour.rgb * fresnel * g1vNL;
@@ -295,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
@@ -322,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];
@@ -330,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
}
@@ -338,6 +372,6 @@ void main()
float NdotL = clamp(dot(normal, lightDir), 0.0, 1.0);
fragmentColor.rgb = computeLighting(specColor.rgb, diffColor, lightDir, normal.xyz, halfVec, eyeDir, roughness, fresnel, NdotL).rgb * diffuseLightColor * attenuation * area_normalisation;
}
fragOut0 = max(fragmentColor, vec4(0.0));
}
@@ -1,3 +1,7 @@
#ifdef GL_ES
precision highp float;
precision highp int;
#endif
#include "lighting.sdr"
@@ -1,10 +1,16 @@
#ifdef GL_ES
precision highp float;
precision highp sampler2D;
precision highp sampler2DArray;
#endif
layout(location = 0) out vec4 fragOut0;
#ifdef VULKAN
layout(location = 0) in vec4 fragTexCoord;
layout(location = 1) in vec4 fragColor;
layout(location = 2) in float fragOffset;
layout(location = 0) out vec4 fragOut0;
layout(set = 1, binding = 1) uniform sampler2DArray baseMap;
layout(set = 1, binding = 4) uniform sampler2D depthMap;
layout(set = 1, binding = 5) uniform sampler2D frameBuffer;
@@ -14,8 +20,6 @@ in vec4 fragTexCoord;
in vec4 fragColor;
in float fragOffset;
out vec4 fragOut0;
uniform sampler2DArray baseMap;
uniform sampler2D depthMap;
uniform sampler2D distMap;
@@ -1,3 +1,7 @@
#ifdef GL_ES
precision highp float;
#endif
#ifdef VULKAN
layout(location = 0) in vec4 vertPosition;
layout(location = 1) in vec4 vertColor;
+8 -4
View File
@@ -1,15 +1,21 @@
#ifdef GL_ES
precision highp float;
precision highp int;
precision highp sampler2D;
precision highp sampler2DArray;
#endif
#include "gamma.sdr"
#include "z-compress.sdr"
layout(location = 0) out vec4 fragOut0;
#ifdef VULKAN
layout(location = 0) in vec4 fragTexCoord;
layout(location = 1) in vec4 fragColor;
layout(location = 2) in float fragRadius;
layout(location = 3) in vec4 fragPosition;
layout(location = 0) out vec4 fragOut0;
layout(set = 1, binding = 1) uniform sampler2DArray baseMap;
layout(set = 1, binding = 4) uniform sampler2D depthMap;
#else
@@ -18,8 +24,6 @@ in vec4 fragPosition;
in vec4 fragTexCoord;
in vec4 fragColor;
out vec4 fragOut0;
uniform sampler2DArray baseMap;
uniform sampler2D depthMap;
#endif
+4
View File
@@ -1,3 +1,7 @@
#ifdef GL_ES
precision highp float;
#endif
layout (points) in;
layout (triangle_strip, max_vertices = 4) out;
+3
View File
@@ -1,3 +1,6 @@
#ifdef GL_ES
precision highp float;
#endif
#ifdef VULKAN
layout(location = 0) in vec4 vertPosition;
@@ -1,11 +1,14 @@
#define M_PI 3.1415926535897932384626433832795
#ifdef GL_ES
precision highp float;
precision highp samplerCube;
#endif
uniform samplerCube envmap;
in vec4 fragTexCoord;
out vec4 fragOut0;
layout (location = 0) out vec4 outColor;
layout(location=0) out vec4 fragOut0;
void main() {
const float theta = -fragTexCoord.x * (2.0 * M_PI);
+8 -5
View File
@@ -1,17 +1,20 @@
#ifdef GL_ES
precision highp float;
precision highp sampler2D;
#endif
#include "gamma.sdr"
layout(location=0) out vec4 fragOut0;
#ifdef VULKAN
layout(location = 0) in vec2 fragTexCoord;
layout(location = 0) out vec4 fragOut0;
layout(set = 1, binding = 1) uniform sampler2D tex;
layout(set = 1, binding = 4) uniform sampler2D depth_tex;
#else
in vec4 fragTexCoord;
out vec4 fragOut0;
uniform sampler2D tex;
uniform sampler2D depth_tex;
#endif
@@ -40,7 +43,7 @@ void main()
// If the scene is in fog then we change the color we operate on based on the depth of the pixel
float depth_val = texture(depth_tex, fragTexCoord.xy).x;
// Transform depth value from [0, 1] to [-1, 1]
float depth_normalized = 2 * depth_val - 1;
float depth_normalized = 2.0 * depth_val - 1.0;
// Now we compute the depth value in projection space using the clipping plane information
float view_depth = 2.0 * zNear * zFar / (zFar + zNear - depth_normalized * (zFar - zNear));
@@ -50,7 +53,7 @@ void main()
if (isinf(view_depth)) {
fragOut0.rgb = color_in.rgb;
} else {
float fog_dist = clamp(1 - pow(fog_density, view_depth - fog_start) , 0.0, 1.0);
float fog_dist = clamp(1.0 - pow(fog_density, view_depth - fog_start) , 0.0, 1.0);
vec3 finalFogColor = srgb_to_linear(fog_color);
fragOut0.rgb = mix(color_in.rgb, finalFogColor, fog_dist);
+14 -1
View File
@@ -1,3 +1,12 @@
#ifdef GL_ES
#ifdef GL_EXT_gpu_shader5
#extension GL_EXT_gpu_shader5 : enable
#define FXAA_GATHER4_ALPHA 1
#endif
precision highp float;
precision highp sampler2D;
#endif
#define FXAA_EARLY_EXIT 1
#define FXAA_DISCARD 1
#define FXAA_FAST_PIXEL_OFFSET 1 // With OpenGL 3.2 we can always do this
@@ -500,8 +509,12 @@ layout(std140, set = 2, binding = 0) uniform genericData {
#else
uniform sampler2D tex0;
in vec2 v_rcpFrame;
#ifdef GL_ES
in vec2 v_pos;
#else
noperspective in vec2 v_pos;
out vec4 fragOut0;
#endif
layout(location=0)out vec4 fragOut0;
#endif
void main() {
+11 -1
View File
@@ -1,3 +1,6 @@
#ifdef GL_ES
precision highp float;
#endif
#ifdef VULKAN
layout(location = 0) out vec2 fragTexCoord;
@@ -8,9 +11,14 @@ void main() {
}
#else
in vec4 vertPosition;
in vec4 vertTexCoord;
out vec2 v_rcpFrame;
#ifdef GL_ES
out vec2 v_pos;
#else
noperspective out vec2 v_pos;
#endif
layout (std140) uniform genericData {
float rt_w;
@@ -20,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
+8 -2
View File
@@ -1,6 +1,12 @@
#ifdef GL_ES
precision highp float;
precision highp sampler2D;
#endif
layout(location=0) out vec4 fragOut0;
#ifdef VULKAN
layout(location = 0) in vec2 fragTexCoord;
layout(location = 0) out vec4 fragOut0;
// [0]=real color (passed through as-is), [1]=luma detection proxy (see
// getAADetectionView; may be the same view as [0] when not HDR)
layout(set = 1, binding = 1) uniform sampler2D fxaaTex[2];
@@ -8,9 +14,9 @@ layout(set = 1, binding = 1) uniform sampler2D fxaaTex[2];
#define lumaTex fxaaTex[1]
#else
in vec4 fragTexCoord;
out vec4 fragOut0;
uniform sampler2D tex;
#endif
void main() {
vec4 color = texture(tex, fragTexCoord.xy);
#ifdef VULKAN
@@ -1,12 +1,17 @@
#ifdef GL_ES
precision highp float;
precision highp sampler2D;
#endif
#include "gamma.sdr"
layout(location=0)out vec4 fragOut0;
#ifdef VULKAN
layout(location = 0) in vec2 fragTexCoord;
layout(location = 0) out vec4 fragOut0;
layout(set = 1, binding = 1) uniform sampler2D tex;
#else
in vec4 fragTexCoord;
out vec4 fragOut0;
uniform sampler2D tex;
#endif
+3
View File
@@ -1,3 +1,6 @@
#ifdef GL_ES
precision highp float;
#endif
const float SRGB_GAMMA = 2.2;
const float SRGB_GAMMA_INVERSE = 1.0 / SRGB_GAMMA;
+12 -6
View File
@@ -1,15 +1,21 @@
//!#version 150
#include "gamma.sdr" //! #include "gamma.sdr"
#ifdef GL_ES
precision highp float;
precision highp int;
precision highp samplerCube;
#endif
#include "gamma.sdr"
#define PI 3.1415926535897932384626433832795
layout(location = 0) out vec4 fragOut0;
#ifdef VULKAN
layout(location = 0) in vec2 fragTexCoord;
layout(location = 0) out vec4 fragOut0;
layout(set = 1, binding = 1) uniform samplerCube envmap;
#else
uniform samplerCube envmap;
in vec4 fragTexCoord;
out vec4 fragOut0;
#endif
#ifdef VULKAN
@@ -154,8 +160,8 @@ vec3( 0.7305, 0.6698, 0.1333)
);
void main() {
float xpos = 2 * gl_FragCoord.x/16 - 1; // remap to (-1, 1)
float ypos = 2 * gl_FragCoord.y/16 - 1;
float xpos = 2.0 * gl_FragCoord.x/16.0 - 1.0; // remap to (-1, 1)
float ypos = 2.0 * gl_FragCoord.y/16.0 - 1.0;
vec3 fragDir;
vec4 fragCol = vec4(0.0);
@@ -191,5 +197,5 @@ void main() {
vec3 sampDir = points[i] + (aPdotD - PdotD) * fragDir;
fragCol += aPdotD * vec4(srgb_to_linear(texture(envmap, sampDir)).rgb, 1.0);
}
fragOut0 = linear_to_srgb(PI * fragCol * (1.0/128));
fragOut0 = linear_to_srgb(PI * fragCol * (1.0/128.0));
}
+2 -2
View File
@@ -57,7 +57,7 @@ vec3 ComputeGGX(vec3 specColor, vec3 diffColor, vec3 light, vec3 normal, vec3 ha
vec3 specular = distribution * fresnel * geometry / (4*dotNV*dotNL + 0.0001);
vec3 specular = distribution * fresnel * geometry / (4.0*dotNV*dotNL + 0.0001);
// Diffuse term - Lambertian, kD represents energy lost to specular reflection.
vec3 kD = vec3(1.0)-fresnel;
@@ -70,7 +70,7 @@ vec3 ComputeGGX(vec3 specColor, vec3 diffColor, vec3 light, vec3 normal, vec3 ha
vec3 computeLighting(vec3 specColor, vec3 diffColor, vec3 light, vec3 normal, vec3 halfVec, vec3 view, float roughness, float fresnelFactor, float dotNL)
{
#ifdef FLAG_LIGHT_MODEL_BLINN_PHONG
return SpecularBlinnPhong(specColor, light, normal, halfVec, 32, fresnelFactor, dotNL);
return SpecularBlinnPhong(specColor, light, normal, halfVec, 32.0, fresnelFactor, dotNL);
#else
return ComputeGGX(specColor, diffColor, light, normal, halfVec, view, roughness, fresnelFactor, dotNL);
#endif
+8 -3
View File
@@ -1,7 +1,14 @@
#ifdef GL_ES
precision highp float;
precision highp int;
precision highp sampler2D;
#endif
layout(location = 0) out vec4 fragOut0;
#ifdef VULKAN
layout(location = 0) in vec2 fragTexCoord;
layout(location = 0) out vec4 fragOut0;
layout(set = 1, binding = 1) uniform sampler2D textures[16];
#define scene textures[0]
@@ -9,8 +16,6 @@ layout(set = 1, binding = 1) uniform sampler2D textures[16];
#else
in vec4 fragTexCoord;
out vec4 fragOut0;
uniform sampler2D scene;
uniform sampler2D cockpit;
#endif
+44 -16
View File
@@ -1,4 +1,13 @@
//? #version 150
#ifdef GL_ES
#extension GL_EXT_clip_cull_distance: enable
precision highp float;
precision highp int;
precision highp sampler2D;
precision highp sampler2DArray;
precision highp sampler2DArrayShadow;
#endif
#include "shadows.sdr" //! #include "shadows.sdr"
#include "lighting.sdr" //! #include "lighting.sdr"
#include "gamma.sdr" //! #include "gamma.sdr"
@@ -101,6 +110,10 @@ uniform shadowCascadeParams {
int cascade_count;
float rtShadowBiasMin;
float rtShadowBiasMax;
int rtShadowSampleCount;
int rtaoSampleCount;
float rtaoRadius;
float rtaoStrength;
mat4 shadow_mv_matrix;
@@ -110,8 +123,16 @@ 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];
};
layout(location = 0) out vec4 fragOut0;
layout(location = 1) out vec4 fragOut1;
layout(location = 2) out vec4 fragOut2;
layout(location = 3) out vec4 fragOut3;
layout(location = 4) out vec4 fragOut4;
#ifdef VULKAN
layout(set = 1, binding = 1) uniform sampler2DArray materialTextures[16];
#define sBasemap materialTextures[0]
@@ -122,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
@@ -139,13 +161,12 @@ layout(location = 0) in VertexOutput {
vec4 shadowUV[NUM_SHADOW_CASCADES];
vec4 shadowPos;
#prereplace ENDIF_FLAG_COMPILED MODEL_SDR_FLAG_SHADOWS
#ifdef SDR_FLAG_SW_CLIP_FALLBACK
float vClipDist;
#endif
} vertIn;
layout(location = 0) out vec4 fragOut0;
layout(location = 1) out vec4 fragOut1;
layout(location = 2) out vec4 fragOut2;
layout(location = 3) out vec4 fragOut3;
layout(location = 4) out vec4 fragOut4;
#else
#prereplace IF_FLAG_COMPILED MODEL_SDR_FLAG_DIFFUSE
uniform sampler2DArray sBasemap;
@@ -168,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 {
@@ -185,13 +207,10 @@ in VertexOutput {
vec4 shadowUV[NUM_SHADOW_CASCADES];
vec4 shadowPos;
#prereplace ENDIF_FLAG_COMPILED MODEL_SDR_FLAG_SHADOWS
#ifdef SDR_FLAG_SW_CLIP_FALLBACK
float vClipDist;
#endif
} vertIn;
out vec4 fragOut0;
out vec4 fragOut1;
out vec4 fragOut2;
out vec4 fragOut3;
out vec4 fragOut4;
#endif
vec3 FresnelLazarovEnv(vec3 specColor, vec3 view, vec3 normal, float gloss)
@@ -255,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;
@@ -283,6 +306,10 @@ vec3 CalculateLighting(vec3 normal, vec3 diffuseMaterial, vec3 specularMaterial,
void main()
{
#ifdef SDR_FLAG_SW_CLIP_FALLBACK
if (vertIn.vClipDist < 0.0) discard;
#endif
vec3 eyeDir = vec3(normalize(-vertIn.position).xyz);
vec2 texCoord = vertIn.texCoord.xy;
@@ -423,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);
@@ -464,7 +491,7 @@ void main()
#prereplace ENDIF_FLAG //MODEL_SDR_FLAG_DIFFUSE
// This code will only be used by the forward renderer so we apply the fog effect to both the emissive and the base
// color.
float fogDensityFinal = clamp(1 - pow(fogDensity, vertIn.fogDist) , 0.0, 1.0);
float fogDensityFinal = clamp(1.0 - pow(fogDensity, vertIn.fogDist) , 0.0, 1.0);
baseColor.rgb = mix(emissiveColor.rgb + baseColor.rgb, finalFogColor, fogDensityFinal);
emissiveColor.rgb = vec3(0.0); // Zero the emissive color since it has already been added by the previous line
specColor.rgb *= fogDensityFinal;
@@ -509,11 +536,12 @@ void main()
#prereplace IF_NOT_FLAG MODEL_SDR_FLAG_LIGHT
#prereplace IF_FLAG MODEL_SDR_FLAG_SPEC
baseColor.rgb += pow(1.0 - clamp(dot(eyeDir, normal), 0.0, 1.0), 5.0 * clamp(glossData, 0.01, 1.0)) * specColor.rgb;
glossData = 0;
glossData = 0.0f;
#prereplace ENDIF_FLAG //MODEL_SDR_FLAG_SPEC
// If there is no lighting then we copy the color data so far into the emissive.
emissiveColor.rgb += baseColor.rgb;
aoFactors.x = 0;
aoFactors.x = 0.0f;
// and leave nothing for the light pass to add on top of it
baseColor.rgb = vec3(0.0);
specColor = vec4(0.0);
+26 -1
View File
@@ -1,4 +1,11 @@
#ifdef GL_ES
#extension GL_EXT_gpu_shader5: enable
#extension GL_EXT_clip_cull_distance: enable
precision highp float;
precision highp int;
#else
#extension GL_ARB_gpu_shader5: enable
#endif
#conditional_include +"LARGE_SHADER" "main_large.sdr"
#conditional_include -"LARGE_SHADER" "main_small.sdr"
@@ -100,6 +107,9 @@ in VertexOutput {
vec4 shadowUV[NUM_SHADOW_CASCADES];
vec4 shadowPos;
#prereplace ENDIF_FLAG_COMPILED MODEL_SDR_FLAG_SHADOWS
#ifdef SDR_FLAG_SW_CLIP_FALLBACK
float vClipDist;
#endif
} vertIn[];
out VertexOutput {
@@ -117,6 +127,10 @@ out VertexOutput {
vec4 shadowUV[NUM_SHADOW_CASCADES];
vec4 shadowPos;
#prereplace ENDIF_FLAG_COMPILED MODEL_SDR_FLAG_SHADOWS
#ifdef SDR_FLAG_SW_CLIP_FALLBACK
float vClipDist;
#endif
} vertOut;
#ifdef MODEL_SDR_FLAG_THICK_OUTLINES
@@ -166,8 +180,19 @@ out VertexOutput {
#prereplace ENDIF_FLAG //MODEL_SDR_FLAG_SHADOWS
if (use_clip_plane != 0) {
gl_ClipDistance[0] = gl_in[vert].gl_ClipDistance[0];
#ifdef SDR_FLAG_SW_CLIP_FALLBACK
vertOut.vClipDist = vertIn[vert].vClipDist;
#else
gl_ClipDistance[0] = gl_in[vert].gl_ClipDistance[0];
#endif
} else {
#ifdef SDR_FLAG_SW_CLIP_FALLBACK
vertOut.vClipDist = 1.0;
#else
gl_ClipDistance[0] = 1.0;
#endif
}
EmitVertex();
}
+37 -6
View File
@@ -1,8 +1,18 @@
#ifndef VULKAN
//? #version 150
#ifdef GL_ES
#extension GL_EXT_gpu_shader5: enable
#extension GL_EXT_clip_cull_distance: enable
precision highp float;
precision highp int;
precision highp samplerBuffer;
precision highp sampler2DArrayShadow;
#else
#extension GL_ARB_gpu_shader5: enable
#endif
#endif
#include "shadows.sdr"
#conditional_include +"LARGE_SHADER" "main_large.sdr"
@@ -115,6 +125,10 @@ uniform shadowCascadeParams {
int cascade_count;
float rtShadowBiasMin;
float rtShadowBiasMax;
int rtShadowSampleCount;
int rtaoSampleCount;
float rtaoRadius;
float rtaoStrength;
mat4 shadow_mv_matrix;
@@ -124,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
@@ -145,6 +160,10 @@ layout(location = 0) out VertexOutput {
vec4 shadowUV[NUM_SHADOW_CASCADES];
vec4 shadowPos;
#prereplace ENDIF_FLAG_COMPILED MODEL_SDR_FLAG_SHADOWS
#ifdef SDR_FLAG_SW_CLIP_FALLBACK
float vClipDist;
#endif
} vertOut;
#else
#prereplace IF_FLAG_COMPILED MODEL_SDR_FLAG_TRANSFORM
@@ -166,6 +185,10 @@ out VertexOutput {
vec4 shadowUV[NUM_SHADOW_CASCADES];
vec4 shadowPos;
#prereplace ENDIF_FLAG_COMPILED MODEL_SDR_FLAG_SHADOWS
#ifdef SDR_FLAG_SW_CLIP_FALLBACK
float vClipDist;
#endif
} vertOut;
#define TEXELS_PER_MATRIX 4
@@ -247,12 +270,20 @@ void main()
}
#prereplace ENDIF_FLAG //MODEL_SDR_FLAG_TRANSFORM
// Clip plane
if(use_clip_plane != 0) {
gl_ClipDistance[0] = dot(clip_equation, modelMatrix * orient * vertex);
} else {
gl_ClipDistance[0] = 1.0;
}
#ifndef SDR_FLAG_SW_CLIP_FALLBACK
if (use_clip_plane != 0) {
gl_ClipDistance[0] = dot(clip_equation, modelMatrix * orient * vertex);
} else {
gl_ClipDistance[0] = 1.0;
}
#else
if (use_clip_plane != 0) {
vertOut.vClipDist = dot(clip_equation, modelMatrix * orient * vertex);
} else {
vertOut.vClipDist = 1.0;
}
#endif
vertOut.position = position;
vertOut.normal = normal;
+13 -12
View File
@@ -1,7 +1,10 @@
#include "z-compress.sdr"
#ifdef GL_ES
precision highp float;
precision highp int;
precision highp sampler2DMS;
#endif
#ifdef VULKAN
layout(location = 0) in vec2 fragTexCoord;
#include "z-compress.sdr"
layout(location = 0) out vec4 fragOut0;
layout(location = 1) out vec4 fragOut1;
@@ -9,6 +12,9 @@ layout(location = 2) out vec4 fragOut2;
layout(location = 3) out vec4 fragOut3;
layout(location = 4) out vec4 fragOut4;
#ifdef VULKAN
layout(location = 0) in vec2 fragTexCoord;
layout(set = 1, binding = 1) uniform sampler2DMS msaaTex[6];
#define texColor msaaTex[0]
#define texPos msaaTex[1]
@@ -23,12 +29,7 @@ layout(std140, set = 2, binding = 0) uniform genericData {
};
#else
in vec4 fragTexCoord;
out vec4 fragOut0;
out vec4 fragOut1;
out vec4 fragOut2;
out vec4 fragOut3;
out vec4 fragOut4;
out vec4 fragOut5;
layout(location=5)out vec4 fragOut5;
uniform sampler2DMS texColor;
uniform sampler2DMS texPos;
@@ -176,7 +177,7 @@ float getMedianDist(ivec2 texel) {
//Fallback, do front first
float getMedianDist(ivec2 texel) {
float minDist = -1000000;
float minDist = -1000000.0;
for(int i = 0; i < samples; i++) {
minDist = max(minDist, uncompress_depth_value(texelFetch(texPos, texel, i).z));
}
@@ -190,7 +191,7 @@ float getMedianDist(ivec2 texel) {
void main()
{
vec2 texSize = textureSize(texColor);
vec2 texSize = vec2(textureSize(texColor));
#ifdef VULKAN
ivec2 texel = ivec2(texSize * fragTexCoord);
#else
@@ -206,7 +207,7 @@ void main()
vec4 normal = vec4(0);
vec4 specular = vec4(0);
vec4 emissive = vec4(0);
float depth = 0;
float depth = 0.0;
for(int i = 0; i < SAMPLE_CNT; i++) {
vec4 localPos = texelFetch(texPos, texel, i);
+5
View File
@@ -1,3 +1,8 @@
#ifdef GL_ES
precision highp float;
precision highp int;
precision highp sampler2DArray;
#endif
#ifdef VULKAN
layout (location = 0) in vec2 ftcoord;
+4
View File
@@ -1,3 +1,7 @@
#ifdef GL_ES
precision highp float;
precision highp int;
#endif
#ifdef VULKAN
layout (location = 0) in vec4 vertPosition;
+1 -1
View File
@@ -7,6 +7,6 @@
vec3 unpackNormal(vec2 normal_sample) {
vec3 normal;
normal.xy = normal_sample * 2.0 - 1.0;
normal.z = sqrt(1 - normal_sample.x * normal_sample.x - normal_sample.y * normal_sample.y);
normal.z = sqrt(1.0 - normal_sample.x * normal_sample.x - normal_sample.y * normal_sample.y);
return normal;
}
@@ -1,9 +1,14 @@
#ifdef GL_ES
precision highp float;
precision highp int;
precision highp sampler2D;
#endif
#include "gamma.sdr"
in vec4 fragTexCoord;
in vec4 fragColor;
out vec4 fragOut0;
layout(location=0) out vec4 fragOut0;
uniform sampler2D baseMap;
+7 -2
View File
@@ -1,11 +1,16 @@
#ifdef GL_ES
precision highp float;
precision highp sampler2D;
#endif
layout(location = 0) out vec4 fragOut0;
#ifdef VULKAN
layout(location = 0) in vec2 fragTexCoord;
layout(location = 0) out vec4 fragOut0;
layout(set = 1, binding = 1) uniform sampler2D tex;
layout(set = 1, binding = 4) uniform sampler2D depth_tex;
#else
in vec4 fragTexCoord;
out vec4 fragOut0;
uniform sampler2D tex;
uniform sampler2D depth_tex;
#endif
+8 -3
View File
@@ -1,10 +1,17 @@
#ifdef GL_ES
precision highp float;
precision highp int;
precision highp sampler2DArray;
#endif
layout(location = 0) out vec4 fragOut0;
#ifdef VULKAN
layout(location = 0) in vec2 fragTexCoord;
layout(location = 1) in vec4 fragColor;
layout(location = 2) in vec2 fragScreenPosition;
layout(location = 0) out vec4 fragOut0;
layout(set = 1, binding = 1) uniform sampler2DArray baseMap;
#else
@@ -12,8 +19,6 @@ in vec2 fragTexCoord;
in vec4 fragColor;
in vec2 fragScreenPosition;
out vec4 fragOut0;
uniform sampler2DArray baseMap;
#endif
@@ -1,3 +1,7 @@
#ifdef GL_ES
precision highp float;
precision highp int;
#endif
#ifdef VULKAN
layout(location = 0) in vec2 vertPosition;
@@ -1,5 +1,24 @@
//? #version 150
#ifdef GL_ES
#extension GL_EXT_clip_cull_distance: enable
precision highp float;
#endif
#ifdef SDR_FLAG_SW_CLIP_FALLBACK
#ifdef VULKAN
layout(location = 0) in VertexOutput {
float vClipDist;
} vertIn;
#else
in VertexOutput {
float vClipDist;
} vertIn;
#endif
#endif
void main()
{
#ifdef SDR_FLAG_SW_CLIP_FALLBACK
if (vertIn.vClipDist < 0.0) discard;
#endif
}
+38 -9
View File
@@ -1,17 +1,31 @@
//? #version 150
#ifdef GL_ES
#extension GL_EXT_gpu_shader5: enable
#extension GL_EXT_clip_cull_distance: enable
precision highp float;
precision highp int;
#else
#extension GL_ARB_gpu_shader5: enable
#endif
layout (triangles) in;
layout (triangle_strip, max_vertices = 3) out;
#ifdef GL_ARB_gpu_shader5
#if defined(GL_ARB_gpu_shader5) || defined(GL_EXT_gpu_shader5)
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;
@@ -21,19 +35,29 @@ 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 {
#if !defined(GL_ARB_gpu_shader5)
#if !defined(GL_ARB_gpu_shader5) && !defined(GL_EXT_gpu_shader5)
float instance;
#endif
float clipModel;
vec3 normal;
#ifdef SDR_FLAG_SW_CLIP_FALLBACK
float vClipDist;
#endif
} vertIn[];
#ifdef SDR_FLAG_SW_CLIP_FALLBACK
out VertexOutput {
float vClipDist;
} vertOut;
#endif
void main(void)
{
#ifdef GL_ARB_gpu_shader5
#if defined(GL_ARB_gpu_shader5) || defined(GL_EXT_gpu_shader5)
if (gl_InvocationID >= cascade_count) return;
int cascade_id = cascade_offset + gl_InvocationID;
#else
@@ -49,7 +73,12 @@ void main(void)
}
gl_Layer = cascade_id;
gl_ClipDistance[0] = gl_in[vert].gl_ClipDistance[0];
#ifdef SDR_FLAG_SW_CLIP_FALLBACK
vertOut.vClipDist = vertIn[vert].vClipDist;
#else
gl_ClipDistance[0] = gl_in[vert].gl_ClipDistance[0];
#endif
EmitVertex();
}
+40 -7
View File
@@ -1,9 +1,16 @@
//? #version 150
#ifdef GL_ES
#extension GL_EXT_gpu_shader5: enable
#extension GL_EXT_clip_cull_distance: enable
precision highp float;
precision highp int;
precision highp samplerBuffer;
#else
#extension GL_ARB_gpu_shader5: enable
#ifndef GEOMETRY_FALLBACK
#extension GL_ARB_shader_viewport_layer_array : enable
#endif
#endif
#ifdef VULKAN
layout(location = 0) in vec4 vertPosition;
@@ -38,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;
@@ -51,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
@@ -66,12 +78,25 @@ uniform samplerBuffer transform_tex;
// supports writing gl_Layer from the vertex shader, see CAPABILITY_FAST_SHADOWS).
#ifdef GEOMETRY_FALLBACK
out VertexOutput {
#if !defined(GL_ARB_gpu_shader5)
#if !defined(GL_ARB_gpu_shader5) && !defined(GL_EXT_gpu_shader5)
float instance;
#endif
float clipModel;
vec3 normal;
#ifdef SDR_FLAG_SW_CLIP_FALLBACK
float vClipDist;
#endif
} vertOut;
#elif defined(SDR_FLAG_SW_CLIP_FALLBACK)
#ifdef VULKAN
layout(location = 0) out VertexOutput {
float vClipDist;
} vertOut;
#else
out VertexOutput {
float vClipDist;
} vertOut;
#endif
#endif
#define TEXELS_PER_MATRIX 4
@@ -112,7 +137,7 @@ void main()
#ifdef VULKAN
int instanceIndex = gl_InstanceIndex;
#else
#if !defined(GL_ARB_gpu_shader5)
#if !defined(GL_ARB_gpu_shader5) && !defined(GL_EXT_gpu_shader5)
#ifdef APPLE
vertOut.instance = float(gl_InstanceIDARB);
#else
@@ -133,11 +158,19 @@ void main()
gl_Position = position;
}
#ifndef SDR_FLAG_SW_CLIP_FALLBACK
if (use_clip_plane != 0) {
gl_ClipDistance[0] = dot(clip_equation, modelMatrix * orient * vertex);
} else {
gl_ClipDistance[0] = 1.0;
}
#else
if (use_clip_plane != 0) {
vertOut.vClipDist = dot(clip_equation, modelMatrix * orient * vertex);
} else {
vertOut.vClipDist = 1.0;
}
#endif
#ifdef GEOMETRY_FALLBACK
vertOut.normal = normal;
+255 -32
View File
@@ -6,33 +6,47 @@
#define SAMPLES_PCSS 1
#endif
float samplePoissonPCSS(sampler2DArrayShadow shadow_map, vec4 shadowUV, float maxUVOffset)
{
// Full 16-tap Poisson disc. Unconditional (independent of SMOOTH_PCSS) because it has three
// consumers with different needs: samplePoissonPCSS() takes the first SAMPLES_PCSS of these,
// pcssBlockerSearch() always wants 8 (every second tap) regardless of SAMPLES_PCSS (a
// blocker search only needs a coarse estimate), and the raytraced cone/AO sampling further
// below always
// wants the full spread. Keeping one table means none of those three can end up indexing
// past a table sized for a different consumer.
const vec2 poissonDisc16[16] = vec2[](
vec2(-0.76275, -0.3432573),
vec2(-0.5226235, -0.8277544),
vec2(-0.3780261, 0.01528688),
vec2(-0.7742821, 0.4245702),
vec2(0.04196143, -0.02622231),
vec2(-0.2974772, -0.4722782),
vec2(-0.516093, 0.71495),
vec2(-0.3257416, 0.3910343),
vec2(0.2705966, 0.6670476),
vec2(0.4918377, 0.1853267),
vec2(0.4428544, -0.6251478),
vec2(-0.09204347, 0.9267113),
vec2(0.391505, -0.2558275),
vec2(0.05605913, -0.7570801),
vec2(0.81772, -0.02475523),
vec2(0.6890262, 0.5191521)
);
// Low-quality raster PCSS fallback: a single centered sample (i.e. no offset at all) rather
// than poissonDisc16[0], which is just one arbitrary tap among sixteen and no better centered
// than any other.
const vec2 poissonDiscCenter[1] = vec2[](vec2(0.0, 0.0));
// samplePoissonPCSS()'s tap table: the full disc when taking all SAMPLES_PCSS == 16 of them,
// or the single centered sample when SAMPLES_PCSS == 1.
#if SMOOTH_PCSS
vec2 poissonDisc[16] = vec2[](
vec2(-0.76275, -0.3432573),
vec2(-0.5226235, -0.8277544),
vec2(-0.3780261, 0.01528688),
vec2(-0.7742821, 0.4245702),
vec2(0.04196143, -0.02622231),
vec2(-0.2974772, -0.4722782),
vec2(-0.516093, 0.71495),
vec2(-0.3257416, 0.3910343),
vec2(0.2705966, 0.6670476),
vec2(0.4918377, 0.1853267),
vec2(0.4428544, -0.6251478),
vec2(-0.09204347, 0.9267113),
vec2(0.391505, -0.2558275),
vec2(0.05605913, -0.7570801),
vec2(0.81772, -0.02475523),
vec2(0.6890262, 0.5191521)
);
#define poissonDisc poissonDisc16
#else
vec2 poissonDisc[1] = vec2[](
vec2(0.0,0.0)
);
#define poissonDisc poissonDiscCenter
#endif
float samplePoissonPCSS(sampler2DArrayShadow shadow_map, vec4 shadowUV, float maxUVOffset)
{
float visibility = 0.0f;
for (int i=0; i<SAMPLES_PCSS; i++) {
vec4 uv = shadowUV;
@@ -42,8 +56,82 @@ float samplePoissonPCSS(sampler2DArrayShadow shadow_map, vec4 shadowUV, float ma
return visibility * (1.0f/float(SAMPLES_PCSS));
}
float getShadowValue(sampler2DArrayShadow shadow_map, float depth, vec4 shadowUV[NUM_SHADOW_CASCADES],
// PCSS blocker search: raw (non-compare) depth taps within searchRadiusUV, using every
// second tap of poissonDisc16. The first 8 taps are mostly on one side (x < 0), so they
// would search the penumbra on one side only. This is always the full-spread table, not
// samplePoissonPCSS()'s poissonDisc --
// a blocker search only needs a coarse estimate, not the final filtered result, but it needs
// that estimate regardless of how many taps the raster PCSS path itself is set to take).
// "Blocker" = stored < receiverDepth, matching the GL_LEQUAL/eLessOrEqual compare convention
// the hardware PCF sampler already uses (visible iff Dref <= stored) -- see the compare-sampler
// setup in gropengltnl.cpp / VulkanPostProcessingShadow.cpp.
// A negative return means "no blockers found", which the average blocker depth (always >= 0)
// can't otherwise be distinguished from "found some at depth 0".
float pcssBlockerSearch(sampler2DArray shadow_map_raw, vec4 shadowUV, float searchRadiusUV)
{
float sum = 0.0;
int blockerCount = 0;
for (int i = 0; i < 8; i++) {
vec2 uv = shadowUV.xy + poissonDisc16[i * 2] * searchRadiusUV;
float sampled = texture(shadow_map_raw, vec3(uv, shadowUV.z)).r;
if (sampled < shadowUV.w) {
sum += sampled;
blockerCount++;
}
}
return blockerCount > 0 ? (sum / float(blockerCount)) : -1.0;
}
// Computes the PCSS penumbra radius for one cascade sample: a blocker search followed by
// the orthographic-light penumbra formula (see shadow_smoothness_scale()'s doc comment in
// shadows.cpp for the derivation), clamped to [1 texel, smoothnessCeiling]. penumbraScale is
// Shadow_penumbra_scale[cascade] -- a negative value is the "unsupported or user-disabled"
// sentinel (see shadow_contact_hardening_enabled() in shadows.cpp), in which case this
// returns smoothnessCeiling immediately: bit-identical to the fixed-width behavior from
// before contact hardening existed, and skips the blocker search's extra texture reads.
float pcssPenumbraRadius(sampler2DArrayShadow shadow_map, sampler2DArray shadow_map_raw,
vec4 shadowUV, float penumbraScale, float smoothnessCeiling)
{
float texelFloor = 1.0 / float(textureSize(shadow_map, 0).x);
if (penumbraScale < 0.0) {
return smoothnessCeiling;
}
float avgBlockerDepth = pcssBlockerSearch(shadow_map_raw, shadowUV, smoothnessCeiling);
if (avgBlockerDepth < 0.0) {
// The sparse 8-tap search found nothing, but that doesn't prove there's no thin
// occluder nearby -- run the full PCF at the hardest radius rather than assuming
// fully lit, so a missed thin blocker still reads as an edge instead of popping to
// no shadow at all.
return texelFloor;
}
float deltaDepth = max(shadowUV.w - avgBlockerDepth, 0.0);
return clamp(deltaDepth * penumbraScale, texelFloor, smoothnessCeiling);
}
// PCSS-filtered shadow visibility for a single cascade: looks up its penumbra radius (see
// pcssPenumbraRadius()) and samples the shadow map at that radius. Factored out of
// getShadowValue() below so the cascade-index unpacking (v/i from cascade/4, cascade%4) and
// the penumbra-radius lookup each appear once, whichever of getShadowValue()'s two return
// paths runs.
float shadowVisibilityForCascade(sampler2DArrayShadow shadow_map, sampler2DArray shadow_map_raw,
vec4 shadowUV, int cascade, vec4 smoothness_factors[(NUM_SHADOW_CASCADES + 4 - 1) / 4],
vec4 penumbra_scale[(NUM_SHADOW_CASCADES + 4 - 1) / 4])
{
int v = cascade / 4;
int i = cascade % 4;
float radius = pcssPenumbraRadius(shadow_map, shadow_map_raw, shadowUV,
penumbra_scale[v][i], smoothness_factors[v][i]);
return samplePoissonPCSS(shadow_map, shadowUV, radius);
}
float getShadowValue(sampler2DArrayShadow shadow_map, sampler2DArray shadow_map_raw, float depth, vec4 shadowUV[NUM_SHADOW_CASCADES],
vec4 cascade_distances[(NUM_SHADOW_CASCADES + 4 - 1) / 4], vec4 smoothness_factors[(NUM_SHADOW_CASCADES + 4 - 1) / 4],
vec4 penumbra_scale[(NUM_SHADOW_CASCADES + 4 - 1) / 4],
int cascade_offset, int cascade_count)
{
int cascade = cascade_offset + cascade_count;
@@ -52,19 +140,19 @@ float getShadowValue(sampler2DArrayShadow shadow_map, float depth, vec4 shadowUV
}
if (cascade >= cascade_offset + cascade_count || cascade < cascade_offset) return 1.0;
int cascade_v = cascade / 4;
int cascade_i = cascade % 4;
float cascade_start = (cascade > cascade_offset) ? cascade_distances[(cascade - 1) / 4][(cascade - 1) % 4] : 0.0;
float cascade_end = cascade_distances[cascade_v][cascade_i];
float cascade_end = cascade_distances[cascade / 4][cascade % 4];
float dist_threshold = (cascade_end - cascade_start) * 0.2;
if (cascade_end - dist_threshold > depth || cascade >= cascade_offset + cascade_count - 1)
return samplePoissonPCSS(shadow_map, shadowUV[cascade], smoothness_factors[cascade_v][cascade_i]);
return shadowVisibilityForCascade(shadow_map, shadow_map_raw, shadowUV[cascade], cascade,
smoothness_factors, penumbra_scale);
return mix(
samplePoissonPCSS(shadow_map, shadowUV[cascade], smoothness_factors[cascade_v][cascade_i]),
samplePoissonPCSS(shadow_map, shadowUV[cascade + 1], smoothness_factors[(cascade + 1) / 4][(cascade + 1) % 4]),
shadowVisibilityForCascade(shadow_map, shadow_map_raw, shadowUV[cascade], cascade,
smoothness_factors, penumbra_scale),
shadowVisibilityForCascade(shadow_map, shadow_map_raw, shadowUV[cascade + 1], cascade + 1,
smoothness_factors, penumbra_scale),
smoothstep(cascade_end - dist_threshold, cascade_end, depth));
}
// Raytraced shadow test via inline ray query against the shadow TLAS. Only
@@ -79,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)
@@ -1,21 +1,22 @@
#ifdef GL_ES
precision highp float;
precision highp int;
precision highp sampler2DArray;
#endif
#include "gamma.sdr"
const float EMISSIVE_GAIN = 2.0;
layout(location = 0) out vec4 fragOut0;
#ifdef VULKAN
layout(location = 0) in vec4 fragImpactUV;
layout(location = 1) in float fragNormOffset;
layout(location = 0) out vec4 fragOut0;
layout(set = 1, binding = 1) uniform sampler2DArray shieldMap;
#else
in vec4 fragImpactUV;
in float fragNormOffset;
out vec4 fragOut0;
uniform sampler2DArray shieldMap;
#endif
@@ -1,3 +1,7 @@
#ifdef GL_ES
precision highp float;
precision highp int;
#endif
#ifdef VULKAN
layout(location = 0) in vec4 vertPosition;
+6 -4
View File
@@ -1,3 +1,7 @@
#ifdef GL_ES
precision highp float;
precision highp sampler2D;
#endif
#ifdef VULKAN
layout(std140, set = 2, binding = 0) uniform genericData {
@@ -16,13 +20,13 @@ layout (std140) uniform genericData {
#include "SMAA.sdr"
layout(location=0) out vec4 fragOut0;
#ifdef VULKAN
layout(location = 0) in vec2 fragTexCoord;
layout(location = 1) in vec2 fragPixcoord;
layout(location = 2) in vec4 fragOffset[3];
layout(location = 0) out vec4 fragOut0;
// [0]=edgesTex, [1]=areaTex, [2]=searchTex
layout(set = 1, binding = 1) uniform sampler2D smaaTex[3];
#define edgesTex smaaTex[0]
@@ -33,8 +37,6 @@ in vec2 fragTexCoord;
in vec2 fragPixcoord;
in vec4 fragOffset[3];
out vec4 fragOut0;
uniform sampler2D edgesTex;
uniform sampler2D areaTex;
uniform sampler2D searchTex;
+6 -4
View File
@@ -1,3 +1,7 @@
#ifdef GL_ES
precision highp float;
precision highp sampler2D;
#endif
#ifdef VULKAN
layout(std140, set = 2, binding = 0) uniform genericData {
@@ -16,19 +20,17 @@ layout (std140) uniform genericData {
#include "SMAA.sdr"
layout(location = 0) out vec4 fragOut0;
#ifdef VULKAN
layout(location = 0) in vec2 fragTexCoord;
layout(location = 1) in vec4 fragOffset[3];
layout(location = 0) out vec4 fragOut0;
layout(set = 1, binding = 1) uniform sampler2D colorTex;
#else
in vec2 fragTexCoord;
in vec4 fragOffset[3];
out vec4 fragOut0;
uniform sampler2D colorTex;
#endif
@@ -1,3 +1,7 @@
#ifdef GL_ES
precision highp float;
precision highp sampler2D;
#endif
#ifdef VULKAN
layout(std140, set = 2, binding = 0) uniform genericData {
@@ -16,11 +20,12 @@ layout (std140) uniform genericData {
#include "SMAA.sdr"
layout(location = 0) out vec4 fragOut0;
#ifdef VULKAN
layout(location = 0) in vec2 fragTexCoord;
layout(location = 1) in vec4 fragOffset;
layout(location = 0) out vec4 fragOut0;
// [0]=colorTex, [1]=blendTex
layout(set = 1, binding = 1) uniform sampler2D smaaTex[2];
@@ -30,8 +35,6 @@ layout(set = 1, binding = 1) uniform sampler2D smaaTex[2];
in vec2 fragTexCoord;
in vec4 fragOffset;
out vec4 fragOut0;
uniform sampler2D colorTex;
uniform sampler2D blendTex;
#endif
@@ -1,10 +1,15 @@
#ifdef GL_ES
precision highp float;
precision highp sampler2D;
#endif
layout(location = 0) out vec4 fragOut0;
#ifdef VULKAN
layout(location = 0) in vec2 fragTexCoord;
layout(location = 0) out vec4 fragOut0;
layout(set = 1, binding = 1) uniform sampler2D tex;
#else
in vec4 fragTexCoord;
out vec4 fragOut0;
uniform sampler2D tex;
#endif
@@ -1,13 +1,18 @@
#ifdef GL_ES
precision highp float;
precision highp int;
precision highp sampler2D;
#endif
#include "gamma.sdr"
layout(location=0) out vec4 fragOut0;
#ifdef VULKAN
layout(location = 0) in vec2 fragTexCoord;
layout(location = 0) out vec4 fragOut0;
layout(set = 1, binding = 1) uniform sampler2D tex;
#else
in vec4 fragTexCoord;
out vec4 fragOut0;
uniform sampler2D tex;
#endif
+7 -2
View File
@@ -1,6 +1,12 @@
#ifdef GL_ES
precision highp float;
precision highp sampler2DArray;
#endif
layout(location = 0) out vec4 fragOut0;
#ifdef VULKAN
layout(location = 0) in vec4 fragTexCoord;
layout(location = 0) out vec4 fragOut0;
layout(set = 1, binding = 1) uniform sampler2DArray textures[16];
#define ytex textures[0]
@@ -8,7 +14,6 @@ layout(set = 1, binding = 1) uniform sampler2DArray textures[16];
#define vtex textures[2]
#else
in vec4 fragTexCoord;
out vec4 fragOut0;
uniform sampler2DArray ytex;
uniform sampler2DArray utex;
uniform sampler2DArray vtex;
+28 -13
View File
@@ -1,7 +1,14 @@
#ifdef GL_ES
precision highp float;
precision highp int;
precision highp sampler2D;
precision highp sampler3D;
#endif
layout(location = 0) out vec4 fragOut0;
#ifdef VULKAN
layout(location = 0) in vec2 fragTexCoord;
layout(location = 0) out vec4 fragOut0;
layout(set = 1, binding = 1) uniform sampler2D tex2D[16];
#define composite tex2D[0]
#define emissive tex2D[1]
@@ -10,7 +17,7 @@ layout(set = 1, binding = 5) uniform sampler3D volume_tex;
layout(set = 1, binding = 6) uniform sampler3D noise_volume_tex;
#else
in vec4 fragTexCoord;
out vec4 fragOut0;
layout (binding = 0) uniform sampler2D composite;
layout (binding = 1) uniform sampler2D emissive;
layout (binding = 2) uniform sampler2D depth_tex;
@@ -77,7 +84,7 @@ void main()
#else
vec4 eyeDirection = p_inv * vec4(fragTexCoord.xy * 2.0 - 1.0, -1, 1);
#endif
eyeDirection.w = 0;
eyeDirection.w = 0.0;
vec3 rayDirection = normalize((v_inv * eyeDirection).xyz);
vec4 color_in = texture(composite, fragTexCoord.xy);
@@ -103,14 +110,14 @@ void main()
#endif
//The t at which the ray enters the nebula cube
float maxtMin = max(0,max(tMin.x, max(tMin.y, tMin.z)));
float maxtMin = max(0.0, max(tMin.x, max(tMin.y, tMin.z)));
//The t at which the ray leaves the nebula cube
float mintMax = min(fragDepth, min(tMax.x, min(tMax.y, tMax.z)));
//The cumulative one minus alpha value of the nebula. Corresponds to the multiplier of the current step when additively drawing to a fragment for which the steps closer to the camera are already drawn.
float cumOMAlpha = 1;
float cumOMAlpha = 1.0;
//The cumulative distance already travelled through the nebula.
float cumnebdist = 0;
float cumnebdist = 0.0;
//The color of the current fragment
vec3 cumcolor = vec3(0, 0, 0);
@@ -121,6 +128,14 @@ void main()
vec3 sidestep = 1.0 / vec3(textureSize(volume_tex, 0));
#ifdef GL_ES
// The emissive texture has a complete mip chain generated by the engine.
ivec2 emissiveSize = textureSize(emissive, 0);
float maxEmissiveLod = float(findMSB(max(emissiveSize.x, emissiveSize.y)));
#else
float maxEmissiveLod = float(textureQueryLevels(emissive) - 1);
#endif
for(float stept = maxtMin; stept < mintMax;) {
//Step setup
vec3 position = camera + rayDirection * stept - nebPos;
@@ -159,7 +174,7 @@ void main()
#ifdef NOISE
//Mix the actual nebula color with the noise color depending on the product of the noise channels multiplied with intensity, smooth-clamped from 0 to 1
vec3 stepcolor_neb = mix(nebulaColor, noiseColor,
smoothstep(0, 1, (textureGrad(noise_volume_tex, position / noiseColorScale1, gradX, gradY).r + textureGrad(noise_volume_tex, position / noiseColorScale2, gradX, gradY).g) / 2.0 * noiseIntensity));
smoothstep(0.0, 1.0, (textureGrad(noise_volume_tex, position / noiseColorScale1, gradX, gradY).r + textureGrad(noise_volume_tex, position / noiseColorScale2, gradX, gradY).g) / 2.0 * noiseIntensity));
#else
vec3 stepcolor_neb = nebulaColor;
#endif
@@ -169,19 +184,19 @@ void main()
//From the current position in the volumetric cloud, sample towards the sun to test whether or not the current part of the nebula is bright or not
for(int dlstep = 1; dlstep <= directionalLightSampleSteps; dlstep++){
//TODO Sample these in a cone towards the sun, not just a line. Also add the high-distance sample for concave nebulae
vec3 dlsteppos = (position - globalLightDirection * (dlstep * directionalLightStepSize)) / nebSize + 0.5;
float dlstepalpha = textureGrad(volume_tex, dlsteppos, gradX, gradY).a * step(0, dlsteppos.x) * step(dlsteppos.x, 1) * step(0, dlsteppos.y) * step(dlsteppos.y, 1) * step(0, dlsteppos.z) * step(dlsteppos.z, 1);
vec3 dlsteppos = (position - globalLightDirection * (float(dlstep) * directionalLightStepSize)) / nebSize + 0.5;
float dlstepalpha = textureGrad(volume_tex, dlsteppos, gradX, gradY).a * step(0.0, dlsteppos.x) * step(dlsteppos.x, 1.0) * step(0.0, dlsteppos.y) * step(dlsteppos.y, 1.0) * step(0.0, dlsteppos.z) * step(dlsteppos.z, 1.0);
directionalLightDepth += dlstepalpha * directionalLightStep;
}
stepcolor_diffuse *= beer_powder_norm * (1 - exp(-directionalLightDepth * 2.0)) * exp(-directionalLightDepth);
stepcolor_diffuse *= beer_powder_norm * (1.0 - exp(-directionalLightDepth * 2.0)) * exp(-directionalLightDepth);
//Emissive
cumnebdist += stepcolor_alpha * stepsize_current;
vec3 emissive_lod = textureLod(emissive, fragTexCoord.xy, clamp(cumnebdist * emissiveSpreadFactor, 0, float(textureQueryLevels(emissive) - 1))).rgb;
vec3 stepcolor_emissive = clamp(emissive_lod.rgb * pow(alphalim, 1.0 / (opacitydistance / ((fragDepth - stept) * emissiveFalloff + 0.01))) * emissiveIntensity, 0, 1);
vec3 emissive_lod = textureLod(emissive, fragTexCoord.xy, clamp(cumnebdist * emissiveSpreadFactor, 0.0, maxEmissiveLod)).rgb;
vec3 stepcolor_emissive = clamp(emissive_lod.rgb * pow(alphalim, 1.0 / (opacitydistance / ((fragDepth - stept) * emissiveFalloff + 0.01))) * emissiveIntensity, 0.0, 1.0);
//Step finish
vec3 stepcolor = clamp(stepcolor_diffuse + stepcolor_emissive, 0, 1);
vec3 stepcolor = clamp(stepcolor_diffuse + stepcolor_emissive, 0.0, 1.0);
cumcolor += stepalpha * cumOMAlpha * stepcolor;
}
+17
View File
@@ -28,6 +28,23 @@ protected:
return FALSE;
m_library = SDL_LoadObject(externlib);
#ifdef __ANDROID__
if (m_library == NULL) {
auto android_path = SDL_GetAndroidInternalStoragePath();
if (android_path != nullptr) {
SCP_string android_full_path = android_path;
if (android_full_path.back() != DIR_SEPARATOR_CHAR) {
android_full_path += DIR_SEPARATOR_CHAR;
}
android_full_path += SCP_string("natives") + DIR_SEPARATOR_CHAR + externlib;
#ifndef NDEBUG
mprintf(("Calling SDL_LoadObject with the following path: '%s'\n", android_full_path.c_str()));
#endif
m_library = SDL_LoadObject(android_full_path.c_str());
}
}
#endif
// check full path as a fallback for our own libraries
// NOTE: basePath is assumed to have a trailing slash!
+108 -12
View File
@@ -22,6 +22,7 @@
#include "material.h"
#include "matrix.h"
#include "bmpman/bmpman.h"
#include "cmdline/cmdline.h"
#include "debugconsole/console.h"
#include "executor/global_executors.h"
@@ -42,6 +43,7 @@
#include "render/3d.h"
#include "scripting/hook_api.h"
#include "scripting/scripting.h"
#include "tracing/ProfilerOverlay.h"
#include "tracing/tracing.h"
#include "utils/boost/hash_combine.h"
#include "utils/string_utils.h"
@@ -83,6 +85,7 @@ gr_capability_def gr_capabilities[] = {
GR_CAPABILITY_ENTRY(INSTANCED_RENDERING),
GR_CAPABILITY_ENTRY(FAST_SHADOWS),
GR_CAPABILITY_ENTRY(RAYTRACED_SHADOWS),
GR_CAPABILITY_ENTRY(SHADOW_CONTACT_HARDENING),
};
const size_t gr_capabilities_num = sizeof(gr_capabilities) / sizeof(gr_capabilities[0]);
@@ -289,7 +292,11 @@ GraphicsAPI gr_get_configured_render_api()
#endif
#ifndef __ANDROID__
os::ViewportState Gr_configured_window_state = os::ViewportState::Fullscreen;
#else
os::ViewportState Gr_configured_window_state = os::ViewportState::Borderless;
#endif
static bool mode_change_func(os::ViewportState state, bool initial)
{
@@ -920,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;
@@ -1703,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();
// The offscreen targets that back the scene/post-processing pipeline were sized for the old
// gr_screen; give the backend a chance to grow them before anything renders at the new size.
// Backends that already handle this elsewhere (Vulkan, via recreateSwapChain()) leave it unset.
if (gr_screen.gf_resize_render_targets) {
gr_screen.gf_resize_render_targets();
}
}
void gr_window_to_render_pos(float& x, float& y)
@@ -3259,24 +3296,38 @@ void gr_set_bitmap(int bitmap_num, int alphablend_mode, int bitblt_mode, float a
gr_screen.current_bitmap = bitmap_num;
}
static void output_uniform_debug_data()
gr_debug_stats gr_get_debug_stats()
{
if (gr_screen.mode == GraphicsAPI::Stub) {
return;
gr_debug_stats stats;
if (!Cmdline_graphics_debug_output) {
return stats;
}
int line_height = gr_get_font_height() + 1;
if (UniformBufferManager) {
stats.uniform_buffer_valid = true;
stats.uniform_buffer_size = UniformBufferManager->getBufferSize();
stats.uniform_buffer_used = UniformBufferManager->getCurrentlyUsedSize();
}
gr_set_color_fast(&Color_bright_white);
gr_screen.gf_get_debug_stats(stats);
gr_printf_no_resize(gr_screen.center_offset_x + 20, gr_screen.center_offset_y + 160,
"Uniform buffer size: " SIZE_T_ARG, UniformBufferManager->getBufferSize());
gr_printf_no_resize(gr_screen.center_offset_x + 20, gr_screen.center_offset_y + 160 + line_height,
"Currently used data: " SIZE_T_ARG, UniformBufferManager->getCurrentlyUsedSize());
return stats;
}
static bool Imgui_frame_active = false;
void gr_imgui_begin_frame()
{
if (Imgui_frame_active) {
return;
}
// The stub renderer (standalone server) never assigns the ImGui entry points.
if (!gr_screen.gf_imgui_new_frame || !ImGui::GetCurrentContext()) {
return;
}
gr_imgui_new_frame(); // renderer backend (OpenGL/Vulkan)
ImGui_ImplSDL3_NewFrame(); // platform backend, derives the display size from the SDL window
@@ -3294,6 +3345,23 @@ void gr_imgui_begin_frame()
}
ImGui::NewFrame();
Imgui_frame_active = true;
}
void gr_imgui_end_frame()
{
if (!Imgui_frame_active) {
return;
}
ImGui::Render();
gr_imgui_render_draw_data();
Imgui_frame_active = false;
}
bool gr_imgui_frame_active()
{
return Imgui_frame_active;
}
void gr_flip(bool execute_scripting)
@@ -3314,9 +3382,13 @@ void gr_flip(bool execute_scripting)
model_process_cached_ui_render_instances();
if (Cmdline_graphics_debug_output) {
output_uniform_debug_data();
}
// Every presented frame drains the frame profiler and contributes the overlay window to
// this frame's ImGui pass. Doing it here rather than per game state is what keeps the
// profiler's event buffer bounded: collection is global, so the drain has to be too.
tracing::profiler_overlay_frame();
// Closes whatever ImGui frame the overlay (or the lab, or the options screen) opened.
gr_imgui_end_frame();
// IMPORTANT: No rendering may happen after this point until gf_flip()/gr_setup_frame().
// gr_reset_immediate_buffer() resets the write offset to 0, so any subsequent immediate
@@ -3585,6 +3657,30 @@ void gr_heap_deallocate(GpuHeap heap_type, size_t data_offset)
gpuHeap->freeGpuData(data_offset);
}
gr_memory_stats gr_get_memory_stats()
{
gr_memory_stats stats;
// gpu_heaps[] entries stay null when gpu_heap_init() early-returned (GraphicsAPI::Stub, e.g.
// a build with both graphics backends disabled), so this must not assume a live heap.
auto vertex_heap = get_gpu_heap(GpuHeap::ModelVertex);
auto index_heap = get_gpu_heap(GpuHeap::ModelIndex);
if (vertex_heap != nullptr && index_heap != nullptr) {
stats.model_heap_valid = true;
stats.model_vertex_heap_used = vertex_heap->usedBytes();
stats.model_vertex_heap_size = vertex_heap->bufferSize();
stats.model_index_heap_used = index_heap->usedBytes();
stats.model_index_heap_size = index_heap->bufferSize();
}
stats.locked_bitmap_ram_valid = true;
stats.locked_bitmap_ram_bytes = bm_texture_ram;
gr_screen.gf_get_memory_stats(stats);
return stats;
}
void gr_set_gamma(float gamma)
{
if (gr_screen.mode == GraphicsAPI::Stub) {
+104 -1
View File
@@ -262,6 +262,7 @@ enum shader_type {
#define SDR_FLAG_ENV_MAP (1 << 0)
#define SDR_FLAG_DEFERRED_RT_SHADOWS (1 << 1)
#define SDR_FLAG_DEFERRED_RTAO (1 << 2)
#define SDR_FLAG_SHADOW_FALLBACK (1 << 0)
@@ -370,7 +371,12 @@ enum class gr_capability {
CAPABILITY_INSTANCED_RENDERING,
CAPABILITY_FAST_SHADOWS,
CAPABILITY_QUERIES_REUSABLE,
CAPABILITY_RAYTRACED_SHADOWS
CAPABILITY_RAYTRACED_SHADOWS,
// A second, non-compare sampler bound to the shadow map for raw depth reads, needed by
// the shadow-map PCSS blocker search. Vulkan always has this (samplers are independent
// of images there); OpenGL needs GL 3.3 (glGenSamplers/glBindSampler), since the compare mode is
// otherwise texture-object state shared by every sampler bound to that texture.
CAPABILITY_SHADOW_CONTACT_HARDENING
};
struct gr_capability_def {
@@ -690,6 +696,56 @@ enum class BufferUsageHint { Static, Dynamic, Streaming, PersistentMapping };
*/
typedef void* gr_sync;
/**
* @brief Per-backend diagnostic counters, populated only when -gr_debug is active
*
* Each stat group carries its own "valid" flag since not every backend collects every
* group (e.g. per-draw-call counters currently only exist in the Vulkan backend). See
* gr_get_debug_stats().
*/
struct gr_debug_stats {
bool uniform_buffer_valid = false;
size_t uniform_buffer_size = 0;
size_t uniform_buffer_used = 0;
bool draw_stats_valid = false;
int draw_calls = 0;
int draw_indexed_calls = 0;
int total_vertices = 0;
int total_indices = 0;
int apply_material_calls = 0;
int apply_material_failures = 0;
int no_pipeline_skips = 0;
int descriptor_sets_allocated = 0;
int descriptor_writes = 0;
size_t pipeline_count = 0;
int on_demand_texture_uploads = 0;
};
/**
* @brief Cross-backend memory usage snapshot for the profiler overlay's memory panel
*
* Unlike gr_debug_stats, this is always populated when queried (no -gr_debug gate) -- memory
* usage is meant to be visible to anyone with the profiler overlay open. Each group still carries
* its own "valid" flag since not every backend/build collects every group (e.g. both graphics
* backends disabled at build time). See gr_get_memory_stats().
*/
struct gr_memory_stats {
bool model_heap_valid = false;
size_t model_vertex_heap_used = 0;
size_t model_vertex_heap_size = 0;
size_t model_index_heap_used = 0;
size_t model_index_heap_size = 0;
bool locked_bitmap_ram_valid = false;
size_t locked_bitmap_ram_bytes = 0;
bool gpu_purpose_valid = false;
size_t gpu_texture_bytes = 0;
size_t gpu_geometry_bytes = 0;
size_t gpu_render_target_bytes = 0;
};
typedef struct screen {
int max_w = 0, max_h = 0; // Width and height
int max_w_unscaled = 0, max_h_unscaled = 0;
@@ -861,6 +917,12 @@ typedef struct screen {
std::function<void()> gf_scene_texture_end;
std::function<void()> gf_copy_effect_texture;
// Grow the offscreen render targets that back the scene/post-processing pipeline to cover the
// current gr_screen, if they don't already. Called from gr_screen_resize(). Optional: the
// Vulkan backend leaves this unset because VulkanRenderer::recreateSwapChain() already owns
// resizing its extent-sized targets.
std::function<void()> gf_resize_render_targets;
std::function<void(int zbias)> gf_zbias;
std::function<void(int)> gf_set_fill_mode;
@@ -963,6 +1025,14 @@ typedef struct screen {
std::function<void(const char* name)> gf_push_debug_group;
std::function<void()> gf_pop_debug_group;
// Fills in whichever debug_stats groups this backend collects. Defaults to a no-op
// so backends without per-draw-call counters (OpenGL, stub) don't have to assign it.
std::function<void(gr_debug_stats& stats)> gf_get_debug_stats = [](gr_debug_stats&) {};
// Fills in whichever memory_stats groups this backend collects (GPU-purpose byte totals).
// Defaults to a no-op so backends without per-purpose tagging don't have to assign it.
std::function<void(gr_memory_stats& stats)> gf_get_memory_stats = [](gr_memory_stats&) {};
std::function<int()> gf_create_query_object;
std::function<void(int obj, QueryType type)> gf_query_value;
std::function<bool(int obj)> gf_query_value_available;
@@ -1161,6 +1231,23 @@ bool gr_is_screenshot_requested();
//#define gr_flip GR_CALL(gr_screen.gf_flip)
void gr_flip(bool execute_scripting = true);
/**
* @brief Collects whichever graphics-API debug stats are available for the active backend
*
* Returns a default-constructed (all-invalid) gr_debug_stats unless -gr_debug is active. Safe
* to call every frame regardless of backend or debug flag.
*/
gr_debug_stats gr_get_debug_stats();
/**
* @brief Collects whichever cross-backend memory usage stats are available
*
* Unlike gr_get_debug_stats(), this is always populated (no -gr_debug gate) -- memory usage is
* meant to be visible whenever the profiler overlay is open. Safe to call every frame regardless
* of backend or build configuration; groups a backend/build doesn't support stay invalid/zero.
*/
gr_memory_stats gr_get_memory_stats();
inline void gr_setup_frame() {
gr_screen.gf_setup_frame();
}
@@ -1318,6 +1405,16 @@ inline void gr_post_process_restore_zbuffer()
*/
void gr_imgui_begin_frame();
/**
* @brief Renders and submits the open ImGui frame, if any. Called by gr_flip().
*/
void gr_imgui_end_frame();
/**
* @brief Whether an ImGui frame is currently open for contributions
*/
bool gr_imgui_frame_active();
inline void gr_render_primitives(material* material_info,
primitive_type prim_type,
vertex_layout* layout,
@@ -1411,6 +1508,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);
+4 -3
View File
@@ -32,6 +32,7 @@
#define GL_CLAMP_VERTEX_COLOR 0x891A
#define GL_CLAMP_FRAGMENT_COLOR 0x891B
#define GL_FIXED_ONLY 0x891D
#define GLAD_GL_ARB_shader_viewport_layer_array 0 // Disabled, no gl_layer in vertex shader //0x8DAE
//Enums Redefinitions
#define GL_CLIP_DISTANCE0 GL_CLIP_DISTANCE0_EXT // GL_EXT_clip_cull_distance
@@ -48,6 +49,7 @@
#define GLAD_GL_ARB_texture_storage GL_TRUE // ES 3.2
#define GLAD_GL_ARB_draw_buffers_blend GL_TRUE // ES 3.2 (glBlendFunci and glBlendEquationi)
#define GLAD_GL_ARB_vertex_attrib_binding GL_TRUE // ES 3.1
#define GL_DEPTH_CLAMP GL_DEPTH_CLAMP_EXT
//Functions Redefinitions
#define glBlendFunciARB glBlendFunci // ES 3.2
@@ -234,9 +236,7 @@ inline void glDrawBuffer(GLenum buf)
}
}
// does not exist on ES, ES uses layout(location=) directly on shader
// Notes:
// -This function is not required to run.
// ES uses layout(location = ) directly on shader
#define glBindFragDataLocation(program, colorNumber, name) ((void)0)
// glPolygonMode() is not supported on ES, no wireframe, GL_FILL is default, GL_POINTS, and GL_LINES needs an alternative path
@@ -254,6 +254,7 @@ static inline void glQueryCounter(GLuint id, GLenum target)
#else
(void)id;
#endif
(void)target;
}
// glGetCompressedTexImage not present on GLES and no equivalent
+20 -4
View File
@@ -6,7 +6,7 @@
#include <direct.h>
#endif
#if !defined __APPLE_CC__ && defined SCP_UNIX
#if !defined __APPLE_CC__ && !defined __ANDROID__ && defined SCP_UNIX
#include<glad/glad_glx.h>
//Required because X defines none and always, which is used later
#undef None
@@ -1126,6 +1126,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_resize_render_targets = gr_opengl_resize_render_targets;
gr_screen.gf_deferred_lighting_begin = gr_opengl_deferred_lighting_begin;
gr_screen.gf_deferred_lighting_msaa = gr_opengl_deferred_lighting_msaa;
@@ -1164,6 +1165,7 @@ void gr_opengl_init_function_pointers()
gr_screen.gf_is_capable = gr_opengl_is_capable;
gr_screen.gf_get_property = gr_opengl_get_property;
gr_screen.gf_get_memory_stats = gr_opengl_get_memory_stats;
gr_screen.gf_push_debug_group = gr_opengl_push_debug_group;
gr_screen.gf_pop_debug_group = gr_opengl_pop_debug_group;
@@ -1394,7 +1396,7 @@ bool gr_opengl_init(std::unique_ptr<os::GraphicsOperations>&& graphicsOps)
Error(LOCATION, "Failed to load OpenGL!");
}
#if !defined __APPLE_CC__ && defined SCP_UNIX
#if !defined __APPLE_CC__ && !defined __ANDROID__ && defined SCP_UNIX
if (!gladLoadGLXLoader(GL_context->getLoaderFunction(), nullptr, 0)) {
Error(LOCATION, "Failed to load GLX!");
}
@@ -1453,7 +1455,9 @@ bool gr_opengl_init(std::unique_ptr<os::GraphicsOperations>&& graphicsOps)
"Deferred lightning is enabled in settings but the gpu does not support the minimum of 6 draw buffers. "
"This will result in rendering errors.");
}
mprintf((" Depth Clamp support : %s\n", GLAD_GL_EXT_depth_clamp ? NOX("YES") : NOX("NO")));
mprintf((" HW Cull distance support : %s\n", GLAD_GL_EXT_clip_cull_distance ? NOX("YES") : NOX("NO")));
mprintf((" Precompiled shaders support : %s\n", GLAD_GL_OES_get_program_binary ? NOX("YES") : NOX("NO")));
mprintf((" Immutable buffer storage support : %s\n", GLAD_GL_EXT_buffer_storage ? NOX("YES") : NOX("NO")));
mprintf((" BGRA8888 format support: %s\n", GLAD_GL_EXT_texture_format_BGRA8888 ? NOX("YES") : NOX("NO")));
@@ -1513,7 +1517,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
@@ -1628,6 +1632,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
}
+27 -5
View File
@@ -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);
+225 -93
View File
@@ -70,6 +70,33 @@ int Scene_texture_height;
GLfloat Scene_texture_u_scale = 1.0f;
GLfloat Scene_texture_v_scale = 1.0f;
// Render targets are torn down and rebuilt mid-session by gr_opengl_resize_render_targets(), not
// just at shutdown, so deletion has to go through the state cache: the driver is free to hand a
// freed name straight back out, and a cache entry still holding that name would make a later
// Enable() of the recycled texture a no-op.
void opengl_delete_render_texture(GLuint& tex)
{
if ( !tex ) {
return;
}
GL_state.Texture.Delete(tex);
glDeleteTextures(1, &tex);
tex = 0;
}
// Callers must have bound something else first (the resize path binds 0); the framebuffer cache
// has no equivalent of Texture.Delete() to unbind through.
void opengl_delete_render_framebuffer(GLuint& fbo)
{
if ( !fbo ) {
return;
}
glDeleteFramebuffers(1, &fbo);
fbo = 0;
}
inline GLenum opengl_primitive_type(primitive_type prim_type)
{
switch ( prim_type ) {
@@ -98,7 +125,7 @@ void gr_opengl_sphere(material* material_def, float /*rad*/)
}
extern int opengl_check_framebuffer();
void opengl_setup_scene_textures()
void opengl_setup_scene_textures(int width, int height)
{
Scene_texture_initialized = 0;
@@ -113,10 +140,10 @@ void opengl_setup_scene_textures()
return;
}
// clamp size, if needed
Scene_texture_width = gr_screen.max_w;
Scene_texture_height = gr_screen.max_h;
Scene_texture_width = width;
Scene_texture_height = height;
// clamp size, if needed
if ( Scene_texture_width > GL_max_renderbuffer_size ) {
Scene_texture_width = GL_max_renderbuffer_size;
}
@@ -125,6 +152,13 @@ void opengl_setup_scene_textures()
Scene_texture_height = GL_max_renderbuffer_size;
}
mprintf((" Scene textures: %dx%d (screen %dx%d, max renderbuffer %d)\n",
Scene_texture_width,
Scene_texture_height,
gr_screen.max_w,
gr_screen.max_h,
GL_max_renderbuffer_size));
// create framebuffer
glGenFramebuffers(1, &Scene_framebuffer);
GL_state.BindFrameBuffer(Scene_framebuffer);
@@ -333,32 +367,15 @@ void opengl_setup_scene_textures()
if ( opengl_check_framebuffer() ) {
GL_state.BindFrameBuffer(0);
glDeleteFramebuffers(1, &Scene_framebuffer);
Scene_framebuffer = 0;
opengl_delete_render_framebuffer(Scene_framebuffer);
glDeleteTextures(1, &Scene_color_texture);
Scene_color_texture = 0;
glDeleteTextures(1, &Scene_position_texture);
Scene_position_texture = 0;
glDeleteTextures(1, &Scene_normal_texture);
Scene_normal_texture = 0;
glDeleteTextures(1, &Scene_specular_texture);
Scene_specular_texture = 0;
glDeleteTextures(1, &Scene_emissive_texture);
Scene_emissive_texture = 0;
glDeleteTextures(1, &Scene_depth_texture);
Scene_depth_texture = 0;
glDeleteTextures(1, &Scene_luminance_texture);
Scene_luminance_texture = 0;
//glDeleteTextures(1, &Scene_fxaa_output_texture);
//Scene_fxaa_output_texture = 0;
opengl_delete_render_texture(Scene_color_texture);
opengl_delete_render_texture(Scene_position_texture);
opengl_delete_render_texture(Scene_normal_texture);
opengl_delete_render_texture(Scene_specular_texture);
opengl_delete_render_texture(Scene_emissive_texture);
opengl_delete_render_texture(Scene_depth_texture);
opengl_delete_render_texture(Scene_luminance_texture);
Gr_post_processing_enabled = false;
Gr_enable_soft_particles = false;
@@ -687,79 +704,165 @@ void opengl_scene_texture_shutdown()
return;
}
if ( Scene_color_texture ) {
glDeleteTextures(1, &Scene_color_texture);
Scene_color_texture = 0;
}
// Everything opengl_setup_scene_textures() generated, in the same order. Note that
// GammaBlit_texture is 0 when the gamma pass is aliasing Scene_ldr_texture, so the shared
// texture is only released once.
opengl_delete_render_texture(Scene_color_texture);
opengl_delete_render_texture(Scene_ldr_texture);
opengl_delete_render_texture(Scene_position_texture);
opengl_delete_render_texture(Scene_normal_texture);
opengl_delete_render_texture(Scene_specular_texture);
opengl_delete_render_texture(Scene_emissive_texture);
opengl_delete_render_texture(Scene_composite_texture);
opengl_delete_render_texture(Scene_luminance_texture);
opengl_delete_render_texture(Cockpit_depth_texture);
opengl_delete_render_texture(Scene_depth_texture);
opengl_delete_render_framebuffer(Scene_framebuffer);
if ( Scene_position_texture ) {
glDeleteTextures(1, &Scene_position_texture);
Scene_position_texture = 0;
}
opengl_delete_render_texture(Scene_color_texture_ms);
opengl_delete_render_texture(Scene_position_texture_ms);
opengl_delete_render_texture(Scene_normal_texture_ms);
opengl_delete_render_texture(Scene_specular_texture_ms);
opengl_delete_render_texture(Scene_emissive_texture_ms);
opengl_delete_render_texture(Scene_depth_texture_ms);
opengl_delete_render_framebuffer(Scene_framebuffer_ms);
if ( Scene_normal_texture ) {
glDeleteTextures(1, &Scene_normal_texture);
Scene_normal_texture = 0;
}
opengl_delete_render_texture(Back_texture);
opengl_delete_render_texture(Back_depth_texture);
opengl_delete_render_framebuffer(Back_framebuffer);
if ( Scene_specular_texture ) {
glDeleteTextures(1, &Scene_specular_texture);
Scene_specular_texture = 0;
}
opengl_delete_render_texture(GammaBlit_texture);
opengl_delete_render_framebuffer(GammaBlit_framebuffer);
if (Scene_emissive_texture) {
glDeleteTextures(1, &Scene_emissive_texture);
Scene_emissive_texture = 0;
}
if ( Scene_depth_texture ) {
glDeleteTextures(1, &Scene_depth_texture);
Scene_depth_texture = 0;
}
if ( Scene_framebuffer ) {
glDeleteFramebuffers(1, &Scene_framebuffer);
Scene_framebuffer = 0;
}
if (Back_texture) {
glDeleteTextures(1, &Back_texture);
Back_texture = 0;
}
if (Back_depth_texture) {
glDeleteTextures(1, &Back_depth_texture);
Back_depth_texture = 0;
}
if (Back_framebuffer) {
glDeleteFramebuffers(1, &Back_framebuffer);
Back_framebuffer = 0;
}
if (GammaBlit_texture) {
glDeleteTextures(1, &GammaBlit_texture);
GammaBlit_texture = 0;
}
if (GammaBlit_framebuffer) {
glDeleteFramebuffers(1, &GammaBlit_framebuffer);
GammaBlit_framebuffer = 0;
}
glDeleteTextures(2, Distortion_texture);
Distortion_texture[0] = 0;
Distortion_texture[1] = 0;
if ( Distortion_framebuffer ) {
glDeleteFramebuffers(1, &Distortion_framebuffer);
Distortion_framebuffer = 0;
}
opengl_delete_render_texture(Distortion_texture[0]);
opengl_delete_render_texture(Distortion_texture[1]);
opengl_delete_render_framebuffer(Distortion_framebuffer);
Scene_texture_initialized = 0;
Scene_framebuffer_in_frame = false;
}
void gr_opengl_resize_render_targets()
{
// Nothing allocated yet (still inside gr_init()), or FBOs are unavailable entirely.
if ( !Scene_texture_initialized ) {
return;
}
// Grow only. Shrinking back would mean reallocating every G-buffer again the moment the window
// grew back, and the shrunk state is already handled correctly: Scene_texture_u_scale and
// _v_scale confine rendering to the sub-rectangle actually in use. The hardware limit is
// applied here rather than left to opengl_setup_scene_textures(), so that a viewport larger
// than anything the GPU can allocate compares equal below and stops asking.
const int new_width = MIN(MAX(gr_screen.max_w, Scene_texture_width), GL_max_renderbuffer_size);
const int new_height = MIN(MAX(gr_screen.max_h, Scene_texture_height), GL_max_renderbuffer_size);
// The overwhelmingly common case: qtFred calls gr_screen_resize() every frame and the game
// calls it on every SDL resize event, almost always at a size the current targets already
// cover -- or, past the hardware limit, at one they never will.
if ( new_width == Scene_texture_width && new_height == Scene_texture_height ) {
return;
}
// Tearing down the framebuffer we are currently rendering into would corrupt the frame rather
// than fail cleanly, so refuse rather than trying to recover. Callers resize between frames.
// Scene_framebuffer_in_frame covers the post-processing passes too: they only ever run inside
// gr_scene_texture_begin()/end(), so it is set for the whole of Post_in_frame as well.
if ( Scene_framebuffer_in_frame ) {
Assertion(false, "Tried to resize the render targets to %dx%d while a scene was being "
"rendered into them! The resize has been skipped; the frame will be stretched.",
new_width, new_height);
return;
}
mprintf(("Growing render targets from %dx%d to %dx%d to cover the new %dx%d viewport.\n",
Scene_texture_width, Scene_texture_height, new_width, new_height,
gr_screen.max_w, gr_screen.max_h));
// Leave the framebuffer cache pointing at a name that cannot be deleted out from under it.
GL_state.BindFrameBufferBoth(0, 0);
// Only the size-dependent resources are touched. The post-processing table, the compiled
// shaders and the SMAA lookup textures are all resolution-independent and stay alive, which is
// what keeps this cheap enough to run off a window drag. The post-processing targets are
// rebuilt after the scene textures because they are sized to match them.
opengl_scene_texture_shutdown();
opengl_setup_scene_textures(new_width, new_height);
// Reallocating larger is exactly when running out of video memory is most likely, and
// opengl_setup_scene_textures() reports that by leaving the scene uninitialized (having
// already turned post-processing and soft particles off). Rebuilding the post-processing
// targets on top of scene textures that don't exist would only make it worse, so stop here;
// the renderer keeps drawing without the offscreen pipeline.
if ( !Scene_texture_initialized ) {
mprintf(("Failed to allocate %dx%d render targets! The offscreen rendering pipeline has "
"been disabled for the rest of this session.\n", new_width, new_height));
return;
}
opengl_post_resize_render_targets();
}
size_t opengl_get_scene_render_target_bytes()
{
// The bytes for each pixel come from the internal formats that opengl_setup_scene_textures()
// gives to these textures: GL_RGBA16F holds 8 bytes, GL_RGBA8 and GL_DEPTH24_STENCIL8 hold 4.
// Keep the values below equal to the formats at the creation sites in that function.
constexpr size_t RGBA16F_BYTES = 8;
constexpr size_t RGBA8_BYTES = 4;
constexpr size_t DEPTH24_STENCIL8_BYTES = 4;
size_t bytes = 0;
auto add_texture = [&bytes](GLuint texture_id, size_t width, size_t height, size_t bytes_per_pixel,
size_t samples = 1) {
if (texture_id == 0) {
return;
}
bytes += width * height * bytes_per_pixel * samples;
};
const auto scene_w = static_cast<size_t>(Scene_texture_width);
const auto scene_h = static_cast<size_t>(Scene_texture_height);
add_texture(Scene_color_texture, scene_w, scene_h, RGBA16F_BYTES);
add_texture(Scene_ldr_texture, scene_w, scene_h, RGBA8_BYTES);
add_texture(Scene_position_texture, scene_w, scene_h, RGBA16F_BYTES);
add_texture(Scene_normal_texture, scene_w, scene_h, RGBA16F_BYTES);
add_texture(Scene_specular_texture, scene_w, scene_h, RGBA8_BYTES);
add_texture(Scene_emissive_texture, scene_w, scene_h, RGBA16F_BYTES);
add_texture(Scene_composite_texture, scene_w, scene_h, RGBA16F_BYTES);
add_texture(Scene_luminance_texture, scene_w, scene_h, RGBA16F_BYTES);
add_texture(Cockpit_depth_texture, scene_w, scene_h, DEPTH24_STENCIL8_BYTES);
add_texture(Scene_depth_texture, scene_w, scene_h, DEPTH24_STENCIL8_BYTES);
// The multisample textures hold one value for each sample, thus their bytes go up with the
// MSAA level. Cmdline_msaa_enabled holds the number of samples and is 0 when MSAA is off.
const auto samples = static_cast<size_t>(std::max(1, Cmdline_msaa_enabled));
add_texture(Scene_color_texture_ms, scene_w, scene_h, RGBA16F_BYTES, samples);
add_texture(Scene_position_texture_ms, scene_w, scene_h, RGBA16F_BYTES, samples);
add_texture(Scene_normal_texture_ms, scene_w, scene_h, RGBA16F_BYTES, samples);
add_texture(Scene_specular_texture_ms, scene_w, scene_h, RGBA8_BYTES, samples);
add_texture(Scene_emissive_texture_ms, scene_w, scene_h, RGBA16F_BYTES, samples);
add_texture(Scene_depth_texture_ms, scene_w, scene_h, DEPTH24_STENCIL8_BYTES, samples);
// The back buffer and gamma blit textures use the size of the screen, not the (clamped) size
// of the scene textures.
const auto screen_w = static_cast<size_t>(gr_screen.max_w);
const auto screen_h = static_cast<size_t>(gr_screen.max_h);
add_texture(Back_texture, screen_w, screen_h, RGBA16F_BYTES);
add_texture(Back_depth_texture, screen_w, screen_h, DEPTH24_STENCIL8_BYTES);
add_texture(GammaBlit_texture, screen_w, screen_h, RGBA8_BYTES);
// The two distortion textures always have the size 32x32.
add_texture(Distortion_texture[0], 32, 32, RGBA8_BYTES);
add_texture(Distortion_texture[1], 32, 32, RGBA8_BYTES);
return bytes;
}
void gr_opengl_scene_texture_begin()
{
if ( !Scene_texture_initialized ) {
@@ -800,6 +903,30 @@ void gr_opengl_scene_texture_begin()
Scene_texture_v_scale = 1.0f;
}
// A ratio above 1.0 means the viewport outgrew the allocation after the fact -- the scene
// texture can't be resized, so all we can do is render the part that fits and let the blit
// stretch it back out, which misaligns everything by a different amount on each axis. The
// floor above is meant to make this unreachable; if it does happen (a display hotplug or
// resolution change after gr_init(), or an allocation clamped by GL_max_renderbuffer_size on
// low-end hardware) say so once rather than every frame.
if (Scene_texture_u_scale > 1.0f || Scene_texture_v_scale > 1.0f) {
static bool reported_undersized_scene_texture = false;
if (!reported_undersized_scene_texture) {
reported_undersized_scene_texture = true;
nprintf(("OpenGL",
"Viewport (%dx%d) is larger than the scene texture backing it (%dx%d); "
"the post-processed image will be stretched to fit.\n",
gr_screen.max_w,
gr_screen.max_h,
Scene_texture_width,
Scene_texture_height));
}
}
CLAMP(Scene_texture_u_scale, 0.0f, 1.0f);
CLAMP(Scene_texture_v_scale, 0.0f, 1.0f);
if (!light_deferred_enabled()) {
glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
@@ -1246,6 +1373,11 @@ void opengl_draw_full_screen_textured(GLfloat u1, GLfloat v1, GLfloat u2, GLfloa
opengl_render_primitives_immediate(PRIM_TYPE_TRIS, &vert_def, 3, glVertices, sizeof(glVertices));
}
void opengl_draw_full_screen_scene_texture()
{
opengl_draw_full_screen_textured(0.0f, 0.0f, Scene_texture_u_scale, Scene_texture_v_scale);
}
void gr_opengl_render_decals(decal_material* material_info,
primitive_type prim_type,
vertex_layout* layout,
+22 -1
View File
@@ -55,8 +55,23 @@ void gr_opengl_render_shield_impact(shield_material* material_info,
gr_buffer_handle buffer_handle,
int n_verts);
void opengl_setup_scene_textures();
void opengl_setup_scene_textures(int width, int height);
void opengl_scene_texture_shutdown();
void gr_opengl_resize_render_targets();
// Release a render target, keeping the GL state cache in sync. See the definitions for why the
// cache matters now that these are rebuilt mid-session.
void opengl_delete_render_texture(GLuint& tex);
void opengl_delete_render_framebuffer(GLuint& fbo);
/**
* @brief Gives the bytes that the scene, backbuffer, and distortion textures hold
*
* The profiler overlay's memory panel adds this value to the "render targets" group. The function
* counts only the textures that exist at the moment of the call, thus it gives 0 before
* opengl_setup_scene_textures() runs and after opengl_scene_texture_shutdown() runs.
*/
size_t opengl_get_scene_render_target_bytes();
void gr_opengl_scene_texture_begin();
void gr_opengl_scene_texture_end();
void gr_opengl_copy_effect_texture();
@@ -147,6 +162,12 @@ void opengl_draw_textured_quad(GLfloat x1,
*/
void opengl_draw_full_screen_textured(GLfloat u1, GLfloat v1, GLfloat u2, GLfloat v2);
// Fullscreen pass over a source that is one of the scene/post-processing textures. Those are only
// filled out to Scene_texture_u_scale/v_scale of their allocation, so sampling them over the full
// [0,1] range would pull in whatever is beyond the rendered region. Prefer this over passing
// literal 1.0f extents whenever the bound texture came from that pipeline.
void opengl_draw_full_screen_scene_texture();
inline GLenum opengl_primitive_type(primitive_type prim_type);
void gr_opengl_start_decal_pass();
+1 -1
View File
@@ -72,7 +72,7 @@ bool gr_opengl_openxr_test_capabilities() {
#ifdef WIN32
bool gr_opengl_openxr_create_session() {
auto hdc = static_cast<HDC>(SDL_GetPointerProperty(SDL_GetWindowProperties(os::getSDLMainWindow()),
SDL_PROP_WINDOW_WIN32_HWND_POINTER,
SDL_PROP_WINDOW_WIN32_HDC_POINTER,
nullptr));
XrGraphicsBindingOpenGLWin32KHR graphicsBinding{
+123 -58
View File
@@ -28,6 +28,9 @@
#include "es_compatibility.h"
#endif
static void opengl_post_setup_render_targets();
static void opengl_post_shutdown_render_targets();
extern bool PostProcessing_override;
extern int opengl_check_framebuffer();
// Needed to track where the FXAA shaders are
@@ -99,7 +102,7 @@ void opengl_post_pass_tonemap()
GL_state.Texture.Enable(0, GL_TEXTURE_2D, Scene_color_texture);
opengl_draw_full_screen_textured(0.0f, 0.0f, Scene_texture_u_scale, Scene_texture_u_scale);
opengl_draw_full_screen_scene_texture();
}
void opengl_post_pass_bloom()
@@ -134,7 +137,10 @@ void opengl_post_pass_bloom()
GL_state.Texture.Enable(0, GL_TEXTURE_2D, Scene_color_texture);
opengl_draw_full_screen_textured(0.0f, 0.0f, 1.0f, 1.0f);
// Reads the scene texture directly rather than an already-cropped intermediate, so it is
// the scaled variant. The blur/composite passes below read Bloom_textures, which this pass
// fills edge to edge, so those stay unscaled.
opengl_draw_full_screen_scene_texture();
}
// ------ end bright pass ------
@@ -293,7 +299,7 @@ void opengl_post_pass_fxaa()
GL_state.Texture.Enable(0, GL_TEXTURE_2D, Scene_ldr_texture);
opengl_draw_full_screen_textured(0.0f, 0.0f, Scene_texture_u_scale, Scene_texture_u_scale);
opengl_draw_full_screen_scene_texture();
// set and configure post shader ..
opengl_shader_set_current(gr_opengl_maybe_create_shader(SDR_TYPE_POST_PROCESS_FXAA, 0));
@@ -310,7 +316,7 @@ void opengl_post_pass_fxaa()
GL_state.Texture.Enable(0, GL_TEXTURE_2D, Scene_luminance_texture);
opengl_draw_full_screen_textured(0.0f, 0.0f, Scene_texture_u_scale, Scene_texture_u_scale);
opengl_draw_full_screen_scene_texture();
opengl_shader_set_current();
}
@@ -333,7 +339,7 @@ static void smaa_detect_edges()
GL_state.Texture.Enable(0, GL_TEXTURE_2D, Scene_ldr_texture);
opengl_draw_full_screen_textured(0.0f, 0.0f, Scene_texture_u_scale, Scene_texture_u_scale);
opengl_draw_full_screen_scene_texture();
}
static void smaa_calculate_blending_weights()
@@ -358,7 +364,7 @@ static void smaa_calculate_blending_weights()
GL_state.Texture.Enable(1, GL_TEXTURE_2D, Smaa_area_tex);
GL_state.Texture.Enable(2, GL_TEXTURE_2D, Smaa_search_tex);
opengl_draw_full_screen_textured(0.0f, 0.0f, Scene_texture_u_scale, Scene_texture_u_scale);
opengl_draw_full_screen_scene_texture();
}
static void smaa_neighborhood_blending()
@@ -381,7 +387,7 @@ static void smaa_neighborhood_blending()
GL_state.Texture.Enable(0, GL_TEXTURE_2D, Scene_ldr_texture);
GL_state.Texture.Enable(1, GL_TEXTURE_2D, Smaa_blend_tex);
opengl_draw_full_screen_textured(0.0f, 0.0f, Scene_texture_u_scale, Scene_texture_u_scale);
opengl_draw_full_screen_scene_texture();
}
void smaa_resolve()
@@ -491,7 +497,7 @@ void opengl_post_lightshafts()
GL_state.Blend(GL_TRUE);
GL_state.SetAlphaBlendMode(ALPHA_BLEND_ADDITIVE);
opengl_draw_full_screen_textured(0.0f, 0.0f, Scene_texture_u_scale, Scene_texture_u_scale);
opengl_draw_full_screen_scene_texture();
GL_state.Blend(GL_FALSE);
break;
@@ -625,7 +631,7 @@ void gr_opengl_post_process_end()
// now render it to the screen ...
GL_state.PopFramebufferState();
opengl_draw_full_screen_textured(0.0f, 0.0f, Scene_texture_u_scale, Scene_texture_u_scale);
opengl_draw_full_screen_scene_texture();
//Shadow Map debug window
//#define SHADOW_DEBUG
@@ -1020,77 +1026,140 @@ static GLuint load_smaa_texture(GLsizei width, GLsizei height, GLenum format, co
return tex;
}
static void setup_smaa_resources()
// The SMAA area and search textures are fixed-size lookup tables baked into the binary, so unlike
// everything else here they survive a resolution change untouched.
static void setup_smaa_lookup_textures()
{
GL_state.PushFramebufferState();
Smaa_area_tex = load_smaa_texture(AREATEX_WIDTH, AREATEX_HEIGHT, GL_RG8, areaTexBytes, "SMAA Area Texture");
Smaa_search_tex =
load_smaa_texture(SEARCHTEX_WIDTH, SEARCHTEX_HEIGHT, GL_R8, searchTexBytes, "SMAA Search Texture");
}
static void setup_smaa_render_targets()
{
setup_smaa_edges_resources();
setup_smaa_blending_weight_resources();
setup_smaa_neighborhood_blending_resources();
GL_state.PopFramebufferState();
}
// generate and test the framebuffer and textures that we are going to use
static bool opengl_post_init_framebuffer()
static void shutdown_smaa_render_targets()
{
bool rval = false;
opengl_delete_render_texture(Smaa_edges_tex);
opengl_delete_render_framebuffer(Smaa_edge_detection_fb);
// clamp size, if needed
Post_texture_width = gr_screen.max_w;
Post_texture_height = gr_screen.max_h;
opengl_delete_render_texture(Smaa_blend_tex);
opengl_delete_render_framebuffer(Smaa_blending_weight_fb);
if (Post_texture_width > GL_max_renderbuffer_size) {
Post_texture_width = GL_max_renderbuffer_size;
}
opengl_delete_render_texture(Smaa_output_tex);
opengl_delete_render_framebuffer(Smaa_neighborhood_blending_fb);
}
if (Post_texture_height > GL_max_renderbuffer_size) {
Post_texture_height = GL_max_renderbuffer_size;
}
// Allocate every post-processing resource whose size follows the scene textures. Split out from
// opengl_post_process_init() so gr_opengl_resize_render_targets() can rebuild just these without
// re-parsing post_processing.tbl or recompiling shaders.
static void opengl_post_setup_render_targets()
{
// These consume the scene textures pass by pass, so they have to match them exactly rather
// than being sized from gr_screen independently -- see gr_opengl_scene_texture_begin() for
// what the two sizes diverging would mean.
Post_texture_width = Scene_texture_width;
Post_texture_height = Scene_texture_height;
GL_state.PushFramebufferState();
opengl_setup_bloom_textures();
// Always set up SMAA resources so the user can switch to an SMAA preset
// at runtime even when starting with a non-SMAA AA mode, such as None.
//if (Gr_aa_mode != AntiAliasMode::None) {
setup_smaa_resources();
//}
setup_smaa_render_targets();
GL_state.PopFramebufferState();
GL_state.BindFrameBuffer(0);
rval = true;
if ( opengl_check_for_errors("post_init_framebuffer()") ) {
rval = false;
}
return rval;
}
void opengl_post_process_shutdown_bloom()
{
if ( Bloom_textures[0] ) {
glDeleteTextures(1, &Bloom_textures[0]);
Bloom_textures[0] = 0;
opengl_delete_render_texture(Bloom_textures[0]);
opengl_delete_render_texture(Bloom_textures[1]);
opengl_delete_render_framebuffer(Bloom_framebuffer);
}
static void opengl_post_shutdown_render_targets()
{
opengl_post_process_shutdown_bloom();
shutdown_smaa_render_targets();
}
void opengl_post_resize_render_targets()
{
// Post-processing may have been disabled outright (no FBOs, missing shaders, or turned off in
// the table), in which case none of these resources exist and none should start existing now.
if ( !Post_initialized ) {
return;
}
if ( Bloom_textures[1] ) {
glDeleteTextures(1, &Bloom_textures[1]);
Bloom_textures[1] = 0;
opengl_post_shutdown_render_targets();
opengl_post_setup_render_targets();
}
// generate and test the framebuffer and textures that we are going to use
static bool opengl_post_init_framebuffer()
{
setup_smaa_lookup_textures();
opengl_post_setup_render_targets();
return !opengl_check_for_errors("post_init_framebuffer()");
}
size_t opengl_get_postprocessing_render_target_bytes()
{
// The bytes for each pixel come from the internal formats at the creation sites in this file:
// GL_RGBA16F holds 8 bytes and GL_RGBA8 holds 4. Keep these values equal to those formats.
constexpr size_t RGBA16F_BYTES = 8;
constexpr size_t RGBA8_BYTES = 4;
size_t bytes = 0;
// The bloom textures are half the size of the post-processing textures. opengl_setup_bloom_textures()
// calls glGenerateMipmap() before it sets GL_TEXTURE_MAX_LEVEL, thus the driver holds the full
// mipmap chain, not only MAX_MIP_BLUR_LEVELS levels.
for (GLuint bloom_texture : Bloom_textures) {
if (bloom_texture == 0) {
continue;
}
auto width = static_cast<size_t>(std::max(1, Post_texture_width >> 1));
auto height = static_cast<size_t>(std::max(1, Post_texture_height >> 1));
while (true) {
bytes += width * height * RGBA16F_BYTES;
if ((width == 1) && (height == 1)) {
break;
}
width = std::max<size_t>(1, width >> 1);
height = std::max<size_t>(1, height >> 1);
}
}
if ( Bloom_framebuffer > 0 ) {
glDeleteFramebuffers(1, &Bloom_framebuffer);
Bloom_framebuffer = 0;
// The SMAA area and search textures stay out of this total. They hold lookup data of a fixed
// small size, thus they are not render targets.
const auto post_w = static_cast<size_t>(Post_texture_width);
const auto post_h = static_cast<size_t>(Post_texture_height);
if (Smaa_edges_tex != 0) {
bytes += post_w * post_h * RGBA8_BYTES;
}
if (Smaa_blend_tex != 0) {
bytes += post_w * post_h * RGBA8_BYTES;
}
if (Smaa_output_tex != 0) {
bytes += post_w * post_h * RGBA8_BYTES;
}
return bytes;
}
void opengl_post_process_init()
@@ -1141,20 +1210,16 @@ void opengl_post_process_shutdown()
return;
}
if (Post_framebuffer_id[0]) {
glDeleteFramebuffers(1, &Post_framebuffer_id[0]);
Post_framebuffer_id[0] = 0;
if (Post_framebuffer_id[1]) {
glDeleteFramebuffers(1, &Post_framebuffer_id[1]);
Post_framebuffer_id[1] = 0;
}
}
opengl_delete_render_framebuffer(Post_framebuffer_id[0]);
opengl_delete_render_framebuffer(Post_framebuffer_id[1]);
graphics::Post_processing_manager->clear();
graphics::Post_processing_manager = nullptr;
opengl_post_process_shutdown_bloom();
opengl_post_shutdown_render_targets();
opengl_delete_render_texture(Smaa_area_tex);
opengl_delete_render_texture(Smaa_search_tex);
Post_in_frame = false;
Post_active_shader_index = 0;
@@ -8,6 +8,18 @@
void opengl_post_process_init();
void opengl_post_process_shutdown();
// Rebuild the resolution-dependent subset of the above for the current scene texture size, without
// re-parsing post_processing.tbl or recompiling shaders. No-op if post-processing isn't active.
void opengl_post_resize_render_targets();
/**
* @brief Gives the bytes that the bloom and SMAA render textures hold
*
* The profiler overlay's memory panel adds this value to the "render targets" group. The function
* counts only the textures that exist at the moment of the call.
*/
size_t opengl_get_postprocessing_render_target_bytes();
void gr_opengl_post_process_set_effect(const char *name, int x, const vec3d *rgb);
void gr_opengl_post_process_set_defaults();
void gr_opengl_post_process_save_zbuffer();
+13
View File
@@ -178,10 +178,20 @@ static SCP_string opengl_shader_get_header(shader_type type_id, int flags, bool
#ifndef USE_OPENGL_ES
sflags << "#version " << GLSL_version << " core\n";
if (GLSL_version < 330) {
sflags << "#extension GL_ARB_explicit_attrib_location : enable\n";
}
#else
sflags << "#version " << GLSL_version << " es\n";
#endif
sflags << "#define OPENGL\n";
#ifdef USE_OPENGL_ES
if (!GLAD_GL_EXT_clip_cull_distance) {
sflags << "#define SDR_FLAG_SW_CLIP_FALLBACK\n";
}
#endif
sflags << shader_get_shadow_cascade_defines();
sflags << shader_get_rt_shadow_light_limit_define();
@@ -423,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:
+20
View File
@@ -37,6 +37,7 @@ void opengl_texture_state::init(GLuint n_units)
for (unsigned int unit = 0; unit < num_texture_units; unit++) {
units[unit].enabled = GL_FALSE;
units[unit].bound_sampler = 0;
default_values(unit);
@@ -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) {
@@ -360,6 +373,13 @@ GLboolean opengl_state::ClipDistance(GLint num, bool state)
GLboolean save_state = clipdistance_Status[num];
#ifdef USE_OPENGL_ES
if (!GLAD_GL_EXT_clip_cull_distance) {
clipdistance_Status[num] = state;
return save_state;
}
#endif
if (state != clipdistance_Status[num]) {
if (state) {
glEnable(GL_CLIP_DISTANCE0+num);
+20 -1
View File
@@ -27,6 +27,12 @@ struct opengl_texture_unit {
GLenum texture_target;
GLuint texture_id;
// Sampler object bound to this unit, overriding the texture object's own filter/compare
// state for as long as it stays bound (0 = none, texture-level state applies as usual).
// Tracked here for the same reason texture_id is: so BindSampler() can skip a redundant
// glBindSampler call, exactly like Enable() already does for glBindTexture.
GLuint bound_sampler;
};
class opengl_texture_state
@@ -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);
};
+116 -23
View File
@@ -15,11 +15,14 @@
#include "gropenglstate.h"
#include "gropengltexture.h"
#include "gropengldraw.h"
#include "gropenglpostprocessing.h"
#include "gropengltnl.h"
#include "bmpman/bm_internal.h"
#include "bmpman/bmpman.h"
#include "cmdline/cmdline.h"
#include "ddsutils/ddsutils.h"
#include "globalincs/systemvars.h"
#include "graphics/2d.h"
#include "graphics/grinternal.h"
#include "math/vecmat.h"
#include "options/Option.h"
@@ -89,28 +92,6 @@ static auto TextureFilteringOption __UNUSED = options::OptionBuilder<int>("Graph
.parser(parse_texture_filtering_func)
.finish();
static SCP_vector<float> anisotropic_value_enumerator()
{
float max;
if (!gr_get_property(gr_property::MAX_ANISOTROPY, &max)) {
return SCP_vector<float>();
}
if (max <= 2.0f) {
return SCP_vector<float>();
}
SCP_vector<float> out;
// We assume here that the anisotropy levels are powers of two...
float current = 1.0f;
while (current <= max) {
out.push_back(current);
current *= 2.0f;
}
return out;
}
static SCP_string anisotropic_display(float val)
{
if (val < 2.0f) {
@@ -134,7 +115,7 @@ static float anisotropic_default()
static auto AnisotropyOption = options::OptionBuilder<float>("Graphics.Anisotropy",
std::pair<const char*, int>{"Anistropic filtering", 1736},
std::pair<const char*, int>{"Controls the amount of anistropic filtering of the textures", 1737})
.enumerator(anisotropic_value_enumerator)
.enumerator(gr_get_supported_anisotropy_levels)
.category(std::make_pair("Graphics", 1825))
.display(anisotropic_display)
.default_func(anisotropic_default)
@@ -146,6 +127,27 @@ static auto AnisotropyOption = options::OptionBuilder<float>("Graphics.Anisotrop
int opengl_free_texture(tcache_slot_opengl *t);
int opengl_create_texture (int bitmap_handle, int bitmap_type, tcache_slot_opengl *tslot = NULL);
// Running total of the bytes of texture data held by all live texture cache slots, for the
// profiler overlay's memory panel. Kept in sync at every site that fills or drops a slot's data
// rather than summed on demand, because the slots live in bmpman's blocks and a full walk of them
// each frame is too expensive. Animations put all their frames in one texture array but give each
// frame its own slot, so freeing some frames of an animation lowers this total while the array
// itself stays alive until the last frame goes away.
static size_t GL_texture_bytes_used = 0;
/**
* @brief Removes a texture cache slot's bytes from GL_texture_bytes_used
*
* Call this at each site that drops the texture data of a slot, immediately before the code that
* clears the slot. The function sets the size of the slot to 0, thus a second call for the same
* slot does nothing.
*/
static void opengl_texture_release_bytes(tcache_slot_opengl* t)
{
GL_texture_bytes_used -= static_cast<size_t>(t->size);
t->size = 0;
}
extern int get_num_mipmap_levels(int w, int h);
void opengl_set_additive_tex_env()
@@ -189,8 +191,15 @@ void opengl_tcache_init()
// check what mipmap filter we should be using
// 0 == Bilinear
// 1 == Trilinear
// Seed from the legacy config key first: TextureFilteringOption's default_func returns
// GL_mipmap_filter, so this read is what supplies that default. Only then let the option
// override it, the same order the anisotropy setting below uses.
GL_mipmap_filter = os_config_read_uint(NULL, "TextureFilter", 1);
if (Using_in_game_options) {
GL_mipmap_filter = TextureFilteringOption->getValue();
}
if (GL_mipmap_filter > 1) {
GL_mipmap_filter = 1;
}
@@ -236,6 +245,14 @@ void opengl_tcache_flush()
opengl_free_texture(static_cast<tcache_slot_opengl*>(slot.gr_info));
}
}
// A flush releases the texture of every slot, thus the total must go to 0. A value that is not
// 0 shows that a site which drops texture data does not call opengl_texture_release_bytes().
// The shutdown sequence is the one exception: bm_close() removes the blocks before it closes
// the graphics backend, thus the loop above then finds no slot to release.
Assertion(bm_blocks.empty() || (GL_texture_bytes_used == 0),
"OpenGL texture memory tracking lost " SIZE_T_ARG " bytes. A texture release site is missing.",
GL_texture_bytes_used);
}
extern void opengl_kill_all_render_targets();
@@ -334,6 +351,7 @@ int opengl_free_texture(tcache_slot_opengl *t)
// There are still used frames left in the animation so we can't free the texture object here
// We still need to reset this slot though since it has been "freed"
opengl_texture_release_bytes(t);
t->reset();
// Everything is fine
@@ -353,6 +371,7 @@ int opengl_free_texture(tcache_slot_opengl *t)
glDeleteTextures (1, &t->texture_id);
// Finally, reset the data of this slot
opengl_texture_release_bytes(t);
t->reset();
return 1;
@@ -731,10 +750,14 @@ static int opengl_texture_set_level(int bitmap_handle, int bitmap_type, int bmap
} // end switch
tSlot->bitmap_handle = bitmap_handle;
// Remove the previous bytes of the slot before the new size replaces them. This function also
// runs for a texture that is filled again (a streaming animation, for example).
opengl_texture_release_bytes(tSlot);
tSlot->size = (dsize) ? ((doffset + dsize) - skip_size) : (tex_w * tex_h * byte_mult);
tSlot->w = (ushort)tex_w;
tSlot->h = (ushort)tex_h;
GL_texture_bytes_used += static_cast<size_t>(tSlot->size);
GL_textures_in_frame += tSlot->size;
GL_CHECK_FOR_ERRORS("end of create_texture_sub()");
@@ -964,6 +987,7 @@ int opengl_create_texture(int bitmap_handle, int bitmap_type, tcache_slot_opengl
GL_state.Texture.Delete(start_slot->texture_id);
glDeleteTextures(1, &start_slot->texture_id);
opengl_texture_release_bytes(start_slot);
start_slot->reset();
}
}
@@ -1034,6 +1058,7 @@ int opengl_create_texture(int bitmap_handle, int bitmap_type, tcache_slot_opengl
if (tslot->texture_id == 0) {
mprintf(("!!OpenGL DEBUG!! t->texture_id == 0\n"));
opengl_texture_release_bytes(tslot);
tslot->reset();
return 0;
}
@@ -1110,7 +1135,13 @@ int opengl_create_texture(int bitmap_handle, int bitmap_type, tcache_slot_opengl
auto index = frame - animation_begin;
auto frame_slot = bm_get_gr_info<tcache_slot_opengl>(frame, true);
// The copy below overwrites the size of the slot, thus the old bytes must go away first
opengl_texture_release_bytes(frame_slot);
*frame_slot = *tslot; // Copy the existing properties to the slot of this texture
// The copy also brought in the size of tslot, which belongs to a different slot and is
// already part of GL_texture_bytes_used. opengl_texture_set_level() below gives this slot
// its own size.
frame_slot->size = 0;
frame_slot->array_index = (uint32_t) index;
// set the bits per pixel
@@ -1641,6 +1672,7 @@ struct fbo_t {
// these first vars should only be modified in opengl_make_render_target()
GLuint renderbuffer_id = 0;
GLuint framebuffer_id = 0;
size_t renderbuffer_bytes = 0; // for the profiler overlay's memory panel; see GL_renderbuffer_bytes_used
int width = 0;
int height = 0;
// these next 2 should only be modifed in opengl_set_render_target()
@@ -1653,6 +1685,12 @@ static SCP_vector<fbo_t> RenderTarget;
static fbo_t *render_target = NULL;
static int next_fbo_id = 0;
// Running total of bytes used by depth/stencil renderbuffers across all live FBOs, for the
// profiler overlay's memory panel. Kept in sync with each fbo_t's renderbuffer_bytes at every
// create/delete site below rather than summed on demand, since RenderTarget entries are reused
// (opengl_get_free_fbo) and reset in place.
static size_t GL_renderbuffer_bytes_used = 0;
static fbo_t* opengl_get_fbo(int id) {
if (id < 0) {
return nullptr;
@@ -1768,11 +1806,31 @@ void opengl_kill_render_target(bitmap_slot* slot)
if (fbo->renderbuffer_id) {
glDeleteRenderbuffers(1, &fbo->renderbuffer_id);
fbo->renderbuffer_id = 0;
GL_renderbuffer_bytes_used -= fbo->renderbuffer_bytes;
fbo->renderbuffer_bytes = 0;
}
opengl_free_fbo_slot(fbo->fbo_id);
}
void gr_opengl_get_memory_stats(gr_memory_stats& stats)
{
stats.gpu_purpose_valid = true;
// The textures of the bitmap cache. Render targets that the game creates through bmpman are
// part of this group, which matches the Vulkan backend: its bm_make_render_target() images
// also go into the texture group.
stats.gpu_texture_bytes = GL_texture_bytes_used;
stats.gpu_geometry_bytes = opengl_get_geometry_bytes();
// The render target group holds the resources of the backend itself: the depth/stencil
// renderbuffers of the bitmap render targets, the scene and backbuffer textures, the
// post-processing textures, and the shadow map.
stats.gpu_render_target_bytes = GL_renderbuffer_bytes_used + opengl_get_scene_render_target_bytes() +
opengl_get_postprocessing_render_target_bytes() + opengl_get_shadow_map_bytes();
}
void opengl_kill_all_render_targets()
{
for (size_t i = 0; i < RenderTarget.size(); i++) {
@@ -1786,10 +1844,18 @@ void opengl_kill_all_render_targets()
if (fbo->renderbuffer_id) {
glDeleteRenderbuffers(1, &fbo->renderbuffer_id);
fbo->renderbuffer_id = 0;
GL_renderbuffer_bytes_used -= fbo->renderbuffer_bytes;
fbo->renderbuffer_bytes = 0;
}
}
RenderTarget.clear();
// Every renderbuffer is gone now, thus the total must go to 0. A value that is not 0 shows
// that a site which deletes a renderbuffer does not correct GL_renderbuffer_bytes_used.
Assertion(GL_renderbuffer_bytes_used == 0,
"OpenGL renderbuffer memory tracking lost " SIZE_T_ARG " bytes. A renderbuffer delete site is missing.",
GL_renderbuffer_bytes_used);
}
int opengl_set_render_target( int slot, int face, int is_static )
@@ -1946,6 +2012,26 @@ int opengl_make_render_target( int handle, int *w, int *h, int *bpp, int *mm_lvl
glTexParameteri(GL_texture_target, GL_TEXTURE_MAX_LEVEL, ts->mipmap_levels - 1);
// Record the bytes of the color texture, which opengl_tex_array_storage() gave the format
// GL_RGBA8 (4 bytes for each pixel). A cube map holds 6 faces of that size.
size_t rt_texture_bytes = 0;
for (int level = 0; level < ts->mipmap_levels; ++level) {
auto level_w = static_cast<size_t>(std::max(1, *w >> level));
auto level_h = static_cast<size_t>(std::max(1, *h >> level));
rt_texture_bytes += level_w * level_h * 4;
}
if (flags & BMP_FLAG_CUBEMAP) {
rt_texture_bytes *= 6;
}
// A slot that held a render target before must lose its old bytes first.
opengl_texture_release_bytes(ts);
// The size field of the slot is an int, thus the value must stay inside its range. The counter
// takes the same (clamped) value, which keeps the add here and the subtract in
// opengl_texture_release_bytes() equal.
ts->size = static_cast<int>(std::min(rt_texture_bytes, static_cast<size_t>(INT_MAX)));
GL_texture_bytes_used += static_cast<size_t>(ts->size);
GL_state.Texture.Enable(0);
// render buffer (depth)
@@ -1954,6 +2040,10 @@ int opengl_make_render_target( int handle, int *w, int *h, int *bpp, int *mm_lvl
glBindRenderbuffer(GL_RENDERBUFFER, new_fbo->renderbuffer_id);
glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH24_STENCIL8, *w, *h);
glBindRenderbuffer(GL_RENDERBUFFER, 0);
// GL_DEPTH24_STENCIL8 is 4 bytes/pixel
new_fbo->renderbuffer_bytes = static_cast<size_t>(*w) * static_cast<size_t>(*h) * 4;
GL_renderbuffer_bytes_used += new_fbo->renderbuffer_bytes;
}
// frame buffer
@@ -1980,6 +2070,7 @@ int opengl_make_render_target( int handle, int *w, int *h, int *bpp, int *mm_lvl
GL_state.Texture.Delete(ts->texture_id);
glDeleteTextures(1, &ts->texture_id);
opengl_texture_release_bytes(ts);
ts->reset();
glDeleteFramebuffers(1, &new_fbo->framebuffer_id);
@@ -1987,6 +2078,8 @@ int opengl_make_render_target( int handle, int *w, int *h, int *bpp, int *mm_lvl
if (new_fbo->renderbuffer_id) {
glDeleteRenderbuffers(1, &new_fbo->renderbuffer_id);
new_fbo->renderbuffer_id = 0;
GL_renderbuffer_bytes_used -= new_fbo->renderbuffer_bytes;
new_fbo->renderbuffer_bytes = 0;
}
opengl_set_texture_target();
+2
View File
@@ -81,6 +81,8 @@ void opengl_set_additive_tex_env();
void opengl_set_modulate_tex_env();
void opengl_preload_init();
void opengl_kill_render_target(bitmap_slot* slot);
struct gr_memory_stats;
void gr_opengl_get_memory_stats(gr_memory_stats& stats);
int opengl_make_render_target(int handle, int *w, int *h, int *bpp, int *mm_lvl, int flags);
int opengl_set_render_target(int slot, int face = -1, int is_static = 0);
ubyte* gr_opengl_get_bitmap_from_texture(int bitmap_num, int* width_out, int* height_out);
+106 -23
View File
@@ -59,15 +59,35 @@ extern vec3d G3_user_clip_point;
extern bool Envmap_override;
extern bool Shadow_override;
size_t GL_vertex_data_in = 0;
// Running total of the bytes held by live vertex and index buffers, for the profiler overlay's
// memory panel (see opengl_get_geometry_bytes()). Uniform buffers and texture buffers stay out of
// this total, which keeps it equal to the "geometry" group of the Vulkan backend.
static size_t GL_geometry_bytes_used = 0;
// Tells if a buffer of this OpenGL type counts as geometry for GL_geometry_bytes_used.
static bool opengl_buffer_is_geometry(GLenum type)
{
return (type == GL_ARRAY_BUFFER) || (type == GL_ELEMENT_ARRAY_BUFFER);
}
size_t opengl_get_geometry_bytes()
{
return GL_geometry_bytes_used;
}
GLint GL_max_elements_vertices = 4096;
GLint GL_max_elements_indices = 4096;
GLuint Shadow_map_depth_texture = 0;
GLuint Shadow_map_raw_sampler = 0;
GLuint shadow_fbo = 0;
int Shadow_texture_size = 0;
// Number of layers in the shadow map texture array, kept from the moment
// opengl_init_shadow_framebuffer() created the array. opengl_get_shadow_map_bytes() uses it, thus
// a later change of the cascade counts cannot make that value wrong.
static int Shadow_texture_layers = 0;
gr_buffer_handle Transform_buffer_handle;
SCP_unordered_map<vertex_layout, GLuint> Stored_vertex_arrays;
@@ -275,9 +295,11 @@ void gr_opengl_update_buffer_data(gr_buffer_handle handle, size_t size, const vo
glBufferData(buffer_obj.type, size, data, buffer_obj.gl_usage);
}
GL_vertex_data_in -= buffer_obj.size;
if (opengl_buffer_is_geometry(buffer_obj.type)) {
GL_geometry_bytes_used -= buffer_obj.size;
GL_geometry_bytes_used += size;
}
buffer_obj.size = size;
GL_vertex_data_in += buffer_obj.size;
}
void gr_opengl_update_buffer_data_offset(gr_buffer_handle handle, size_t offset, size_t size, const void* data)
@@ -365,7 +387,12 @@ void gr_opengl_delete_buffer(gr_buffer_handle handle)
glDeleteTextures(1, &buffer_obj.texture);
}
GL_vertex_data_in -= buffer_obj.size;
if (opengl_buffer_is_geometry(buffer_obj.type)) {
GL_geometry_bytes_used -= buffer_obj.size;
}
// The size must go to 0 here. opengl_destroy_all_buffers() calls this function for every
// handle, thus a buffer that the game already deleted comes through a second time.
buffer_obj.size = 0;
glDeleteBuffers(1, &buffer_obj.buffer_id);
}
@@ -487,6 +514,25 @@ static bool opengl_init_shadow_framebuffer(int size)
// Everything is fine
mprintf(("Shadow framebuffer created successfully.\n"));
Shadow_texture_size = size;
Shadow_texture_layers = Num_shadow_cascades + Num_cockpit_shadow_cascades;
// Second sampler for Shadow_map_depth_texture, compare mode off, so PCSS blocker
// search (shadows.sdr) can read raw depth off the same texture that the compare
// sampler reads filtered visibility off -- see shadow_contact_hardening_supported()
// for why this needs a sampler object rather than a second glTexParameteri call.
if (shadow_contact_hardening_supported()) {
glGenSamplers(1, &Shadow_map_raw_sampler);
// GL_NEAREST: the blocker search wants the actual stored per-texel depth, not a
// hardware-filtered blend across the compare boundary -- deliberately different
// from the compare sampler's GL_LINEAR.
glSamplerParameteri(Shadow_map_raw_sampler, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glSamplerParameteri(Shadow_map_raw_sampler, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
glSamplerParameteri(Shadow_map_raw_sampler, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glSamplerParameteri(Shadow_map_raw_sampler, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
glSamplerParameteri(Shadow_map_raw_sampler, GL_TEXTURE_WRAP_R, GL_CLAMP_TO_EDGE);
glSamplerParameteri(Shadow_map_raw_sampler, GL_TEXTURE_COMPARE_MODE, GL_NONE);
}
return true;
}
@@ -517,29 +563,28 @@ static bool opengl_init_shadow_framebuffer(int size)
return false;
}
size_t opengl_get_shadow_map_bytes()
{
if (Shadow_map_depth_texture == 0) {
return 0;
}
// opengl_init_shadow_framebuffer() gives the texture array the internal format
// GL_DEPTH_COMPONENT24, which the driver holds in 4 bytes for each texel.
constexpr size_t DEPTH_COMPONENT24_BYTES = 4;
const auto size = static_cast<size_t>(Shadow_texture_size);
const auto layers = static_cast<size_t>(Shadow_texture_layers);
return size * size * layers * DEPTH_COMPONENT24_BYTES;
}
void opengl_tnl_init()
{
Transform_buffer_handle = opengl_create_texture_buffer_object();
if (Shadow_quality != ShadowQuality::Disabled) {
int size;
switch (Shadow_quality) {
case ShadowQuality::Low:
size = 512;
break;
case ShadowQuality::Medium:
size = 1024;
break;
case ShadowQuality::High:
size = 2048;
break;
case ShadowQuality::Ultra:
size = 4096;
break;
default:
size = 256;
break;
}
const int size = shadows_map_resolution();
if (!opengl_init_shadow_framebuffer(size)) {
mprintf(("Failed to create either shadow framebuffer. Disabling shadow support.\n"));
@@ -565,6 +610,16 @@ void opengl_tnl_shutdown()
shadow_cascade_params_shutdown();
if ( Shadow_map_raw_sampler ) {
// Clear the tracked bindings before deleting -- glDeleteSamplers silently unbinds
// the sampler from every unit on the GL side, which would leave GL_state's tracking
// stale (BindSampler() would then skip a rebind of a since-deleted sampler name).
GL_state.Texture.BindSampler(7, 0);
GL_state.Texture.BindSampler(11, 0);
glDeleteSamplers(1, &Shadow_map_raw_sampler);
Shadow_map_raw_sampler = 0;
}
if ( Shadow_map_depth_texture ) {
glDeleteTextures(1, &Shadow_map_depth_texture);
Shadow_map_depth_texture = 0;
@@ -732,7 +787,13 @@ void gr_opengl_shadow_map_start(matrix4 *shadow_view_matrix, const matrix *light
glClear(GL_DEPTH_BUFFER_BIT);
#ifdef USE_OPENGL_ES
if (GLAD_GL_EXT_depth_clamp) {
glEnable(GL_DEPTH_CLAMP);
}
#else
glEnable(GL_DEPTH_CLAMP);
#endif
Glowpoint_override_save = Glowpoint_override;
Glowpoint_override = true;
@@ -753,7 +814,13 @@ void gr_opengl_shadow_map_end()
{
gr_end_view_matrix();
#ifdef USE_OPENGL_ES
if (GLAD_GL_EXT_depth_clamp) {
glDisable(GL_DEPTH_CLAMP);
}
#else
glDisable(GL_DEPTH_CLAMP);
#endif
gr_zbuffer_set(ZBUFFER_TYPE_FULL);
GL_state.PopFramebufferState();
@@ -905,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);
@@ -989,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) {
@@ -1136,6 +1213,12 @@ void opengl_tnl_set_material_nanovg(nanovg_material* material_info) {
void opengl_tnl_set_material_decal(decal_material* material_info) {
opengl_tnl_set_material(material_info, false);
#ifdef USE_OPENGL_ES
// ES decal outputs use G-buffer locations instead of packed draw-buffer indices.
GL_state.SetAlphaBlendModei(2, material_info->get_blend_mode(1));
GL_state.SetAlphaBlendModei(4, material_info->get_blend_mode(2));
#endif
float u_scale, v_scale;
uint32_t array_index;
+21
View File
@@ -35,6 +35,11 @@ extern float shadow_fardist;
extern GLuint Shadow_map_depth_texture;
// Second sampler object bound to Shadow_map_depth_texture, compare mode off, for the raw
// (uncompared) depth reads PCSS blocker search needs. Only created when
// shadow_contact_hardening_supported() is true; 0 otherwise. See gropengltnl.cpp.
extern GLuint Shadow_map_raw_sampler;
struct opengl_vertex_bind {
vertex_format_data::vertex_format format;
GLint size;
@@ -53,6 +58,22 @@ void gr_opengl_bind_uniform_buffer(uniform_block_type bind_point, size_t offset,
void* gr_opengl_map_buffer(gr_buffer_handle handle);
void gr_opengl_flush_mapped_buffer(gr_buffer_handle handle, size_t offset, size_t size);
/**
* @brief Gives the bytes that all live vertex and index buffers hold
*
* The profiler overlay's memory panel shows this value as the "geometry" group. Uniform buffers
* and texture buffers are not part of it.
*/
size_t opengl_get_geometry_bytes();
/**
* @brief Gives the bytes that the shadow map texture array holds
*
* The profiler overlay's memory panel adds this value to the "render targets" group. The function
* gives 0 when shadows are off.
*/
size_t opengl_get_shadow_map_bytes();
/**
* @brief Retrieves the OpenGL handle of a generic buffer handle
* @param expected_type The expected type of the buffer. Mainly used for debug checking.
+50
View File
@@ -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);
}
}
+28
View File
@@ -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();
+16 -1
View File
@@ -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"},
@@ -162,6 +164,19 @@ static ShaderVariantInfo SHADER_VARIANTS[] = {
{SDR_TYPE_SHADOW_MAP_GEN, true, SDR_FLAG_SHADOW_FALLBACK, "GEOMETRY_FALLBACK", {}, "Use a geometry shader to select the cascade layer instead of writing gl_Layer from the vertex shader"}
};
static_assert(VATTRIB_MODEL_MATRIX + 4 <= 32, "vertex input mask is 32 bits wide");
uint32_t shader_vertex_input_mask(const ShaderTypeInfo& info)
{
uint32_t mask = 0;
for (auto attr : info.attributes) {
// The Vulkan pipeline drops every location the mask omits, so a mat4 needs all four of its bits.
const uint32_t locations = (attr == VATTRIB_MODEL_MATRIX) ? 4u : 1u;
mask |= ((1u << locations) - 1u) << attr;
}
return mask;
}
const ShaderTypeInfo* shader_get_type_info(shader_type type)
{
for (auto & i : SHADER_TYPES) {
@@ -208,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;
}
+8
View File
@@ -55,6 +55,14 @@ const ShaderTypeInfo* shader_get_type_info(shader_type type);
// Lookup helper: find variant info by (type, flag). Returns nullptr if not found.
const ShaderVariantInfo* shader_get_variant_info(shader_type type, int flag);
/**
* @brief Bitmask of the vertex attribute locations a shader type reads.
*
* One bit per location; VATTRIB_MODEL_MATRIX contributes bits 8-11 because a
* mat4 input occupies four consecutive locations.
*/
uint32_t shader_vertex_input_mask(const ShaderTypeInfo& info);
// Iterate all variants matching type whose flag bit is set in flags.
void shader_for_each_active_variant(shader_type type, unsigned int flags, const std::function<void(const ShaderVariantInfo&)>& fn);
+211 -15
View File
@@ -16,7 +16,9 @@
#include "cmdline/cmdline.h"
#include "debris/debris.h"
#include "graphics/matrix.h"
#include "graphics/rtao.h"
#include "lighting/lighting.h"
#include "lighting/lighting_profiles.h"
#include "math/vecmat.h"
#include "mod_table/mod_table.h"
#include "model/model.h"
@@ -27,9 +29,13 @@
#include "ship/ship.h"
#include "ship/shipfx.h"
#include "render/3d.h"
#include "starfield/starfield.h"
#include "starfield/sun_disc.h"
#include "tracing/tracing.h"
#include "util/uniform_structs.h"
#include <algorithm>
extern vec3d check_offsets[8];
matrix4 Shadow_view_matrix_light;
@@ -45,6 +51,7 @@ vec3d shadow_rt_relative(const vec3d& world_pos)
}
SCP_vector<matrix4> Shadow_proj_matrix;
SCP_vector<float> Shadow_cascade_distances;
SCP_vector<float> Shadow_penumbra_scale;
static SCP_vector<light_frustum_info> Shadow_frustums;
@@ -243,13 +250,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.
+58
View File
@@ -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,
+6
View File
@@ -70,6 +70,12 @@ void GPUMemoryHeap::freeGpuData(size_t offset) {
gr_buffer_handle GPUMemoryHeap::bufferHandle() {
return _bufferHandle;
}
size_t GPUMemoryHeap::usedBytes() const {
return _allocator->usedBytes();
}
size_t GPUMemoryHeap::bufferSize() const {
return _bufferSize;
}
}
}
+12
View File
@@ -54,6 +54,18 @@ class GPUMemoryHeap {
* @return The graphics code buffer handle.
*/
gr_buffer_handle bufferHandle();
/**
* @brief Gets the number of bytes currently allocated from this heap
* @return The sum of the sizes of all active allocations.
*/
size_t usedBytes() const;
/**
* @brief Gets the total size of the backing GPU buffer, including both allocated and free space
* @return The current buffer size.
*/
size_t bufferSize() const;
};
}
+17 -3
View File
@@ -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
+18 -2
View File
@@ -339,7 +339,22 @@ vk::BufferUsageFlags VulkanBufferManager::getVkUsageFlags(BufferType type, bool
return flags;
}
MemoryUsage VulkanBufferManager::getMemoryUsage(BufferUsageHint hint)
namespace {
MemoryPurpose bufferTypeToPurpose(BufferType type)
{
switch (type) {
case BufferType::Vertex:
case BufferType::Index:
return MemoryPurpose::Geometry;
case BufferType::Uniform:
default:
// Uniform buffers are already tracked separately via UniformBufferManager/gr_debug_stats.
return MemoryPurpose::Unknown;
}
}
}
MemoryUsage VulkanBufferManager::getMemoryUsage(BufferUsageHint hint)
{
switch (hint) {
case BufferUsageHint::Static:
@@ -455,7 +470,8 @@ bool VulkanBufferManager::createOrResizeBuffer(VulkanBufferObject& bufferObj, si
// Allocate memory
MemoryUsage memUsage = getMemoryUsage(bufferObj.usage);
if (!m_memoryManager->allocateBufferMemory(bufferObj.buffer, memUsage, bufferObj.allocation)) {
if (!m_memoryManager->allocateBufferMemory(
bufferObj.buffer, memUsage, bufferObj.allocation, bufferTypeToPurpose(bufferObj.type))) {
m_device.destroyBuffer(bufferObj.buffer);
bufferObj.buffer = oldBuffer;
bufferObj.allocation = oldAllocation;
+6
View File
@@ -118,6 +118,12 @@ public:
*/
uint32_t getCurrentFrame() const { return m_currentFrame; }
/**
* @brief Get the monotonic frame number set by setCurrentFrame (total frames
* rendered, never wraps back -- unlike getCurrentFrame()'s in-flight slot index)
*/
uint64_t getCurrentFrameNumber() const { return m_currentFrameNumber; }
/**
* @brief Get the Vulkan logical device
*/
+14
View File
@@ -132,6 +132,20 @@ vk::PrimitiveTopology convertPrimitiveType(primitive_type type)
}
}
bool topologySupportsPrimitiveRestart(vk::PrimitiveTopology topology)
{
switch (topology) {
case vk::PrimitiveTopology::eLineStrip:
case vk::PrimitiveTopology::eTriangleStrip:
case vk::PrimitiveTopology::eTriangleFan:
case vk::PrimitiveTopology::eLineStripWithAdjacency:
case vk::PrimitiveTopology::eTriangleStripWithAdjacency:
return true;
default:
return false;
}
}
vk::CullModeFlags convertCullMode(bool cullEnabled)
{
return cullEnabled ? vk::CullModeFlagBits::eBack : vk::CullModeFlagBits::eNone;
+9
View File
@@ -46,6 +46,15 @@ vk::StencilOp convertStencilOp(StencilOperation op);
*/
vk::PrimitiveTopology convertPrimitiveType(primitive_type type);
/**
* @brief Whether a topology may declare primitiveRestartEnable
*
* True for the strip/fan topologies. For list topologies the Vulkan spec requires
* primitiveRestartEnable to be VK_FALSE unless VK_EXT_primitive_topology_list_restart is
* enabled, which FSO does not request.
*/
bool topologySupportsPrimitiveRestart(vk::PrimitiveTopology topology);
/**
* @brief Convert FSO cull mode to Vulkan cull mode
* @param cullEnabled Whether culling is enabled
+32 -20
View File
@@ -19,6 +19,7 @@
#include "graphics/matrix.h"
#include "graphics/material.h"
#include "graphics/grinternal.h"
#include "graphics/rtao.h"
#include "graphics/shadows.h"
#include "lighting/lighting.h"
#include "mission/missionparse.h"
@@ -374,13 +375,13 @@ void vulkan_deferred_lighting_msaa()
// Global set (fallback — resolve shader doesn't use global bindings)
vk::DescriptorSet globalSet = descriptorMgr->allocateFrameSet(DescriptorSetIndex::Global);
Assert(globalSet);
writer.writeSet(globalSet, VulkanDescriptorManager::getSetTemplate(DescriptorSetIndex::Global));
writer.writeSet(DescriptorSetIndex::Global, globalSet);
// Material set: All 6 MSAA textures in binding 1 array (elements 0-5)
// [0]=color, [1]=position, [2]=normal, [3]=specular, [4]=emissive, [5]=depth
vk::DescriptorSet materialSet = descriptorMgr->allocateFrameSet(DescriptorSetIndex::Material);
Assert(materialSet);
writer.writeSet(materialSet, VulkanDescriptorManager::getSetTemplate(DescriptorSetIndex::Material));
writer.writeSet(DescriptorSetIndex::Material, materialSet);
// Build texture array: elements 0-5 are MSAA textures, 6-15 are fallback
vk::Sampler nearestSampler = texMgr->getSampler(
@@ -402,7 +403,7 @@ void vulkan_deferred_lighting_msaa()
// PerDraw set: GenericData UBO with {samples, fov} at binding 0
vk::DescriptorSet perDrawSet = descriptorMgr->allocateFrameSet(DescriptorSetIndex::PerDraw);
Assert(perDrawSet);
writer.writeSet(perDrawSet, VulkanDescriptorManager::getSetTemplate(DescriptorSetIndex::PerDraw));
writer.writeSet(DescriptorSetIndex::PerDraw, perDrawSet);
struct MsaaResolveData {
int samples;
@@ -416,15 +417,8 @@ void vulkan_deferred_lighting_msaa()
ring.alloc(descriptorMgr->getCurrentFrame(), &resolveData, sizeof(resolveData));
writer.setBuffer(PerDrawBinding::GenericData, {ring.buffer(), slotOffset, ring.slotSize()});
writer.flush();
cmd.bindDescriptorSets(vk::PipelineBindPoint::eGraphics,
pipelineMgr->getPipelineLayout(),
static_cast<uint32_t>(DescriptorSetIndex::Global), globalSet, {});
cmd.bindDescriptorSets(vk::PipelineBindPoint::eGraphics,
pipelineMgr->getPipelineLayout(),
static_cast<uint32_t>(DescriptorSetIndex::Material), materialSet, {});
cmd.bindDescriptorSets(vk::PipelineBindPoint::eGraphics,
pipelineMgr->getPipelineLayout(),
static_cast<uint32_t>(DescriptorSetIndex::PerDraw), perDrawSet, {});
const vk::DescriptorSet sets[] = {globalSet, materialSet, perDrawSet};
writer.bindSets(cmd, pipelineMgr->getPipelineLayout(), DescriptorSetIndex::Global, sets);
cmd.draw(3, 1, 0, 0);
}
@@ -545,10 +539,23 @@ void vulkan_deferred_lighting_finish()
auto* stateTracker = getStateTracker();
vk::CommandBuffer cmd = stateTracker->getCommandBuffer();
// 1. End G-buffer render pass
// TLAS for RTAO: when shadow rendering is off (Shadow_quality Disabled, or a
// frame that skips shadows_render_all entirely), nothing has built this
// frame's TLAS yet -- request it here. No-op when the shadow path already
// built it (buildTlas()'s per-frame guard). Acceleration-structure builds
// must be recorded outside a render pass, so this ends the G-buffer pass and
// clears it from the state tracker -- which is why step 1 below only ends
// the pass when it is still active.
if (rtao_enabled()) {
vulkan_build_shadow_tlas();
}
// 1. End G-buffer render pass (unless the RTAO TLAS build above already did)
// All 6 color attachments → eShaderReadOnlyOptimal
// Depth → eDepthStencilAttachmentOptimal
cmd.endRenderPass();
if (stateTracker->getCurrentRenderPass()) {
cmd.endRenderPass();
}
// 2. Copy emissive → composite (the emissive data becomes the base for light accumulation)
// Emissive → eShaderReadOnlyOptimal (done), composite → eColorAttachmentOptimal (for light accum)
@@ -959,6 +966,7 @@ void vulkan_render_decals(decal_material* material_info,
// Build pipeline config for decal rendering
PipelineConfig config;
config.shaderType = SDR_TYPE_DECAL;
config.shaderFlags = material_info->get_shader_flags();
config.primitiveType = prim_type;
config.depthMode = material_info->get_depth_mode();
config.depthWriteEnabled = false;
@@ -1013,12 +1021,12 @@ void vulkan_render_decals(decal_material* material_info,
// Set 0: Global (all fallback)
vk::DescriptorSet globalSet = descManager->allocateFrameSet(DescriptorSetIndex::Global);
Assert(globalSet);
writer.writeSet(globalSet, VulkanDescriptorManager::getSetTemplate(DescriptorSetIndex::Global));
writer.writeSet(DescriptorSetIndex::Global, globalSet);
// Set 1: Material
vk::DescriptorSet materialSet = descManager->allocateFrameSet(DescriptorSetIndex::Material);
Assert(materialSet);
writer.writeSet(materialSet, VulkanDescriptorManager::getSetTemplate(DescriptorSetIndex::Material));
writer.writeSet(DescriptorSetIndex::Material, materialSet);
// Binding 2: DecalGlobals UBO
writer.setBuffer(MaterialBinding::DecalGlobals,
@@ -1052,15 +1060,19 @@ void vulkan_render_decals(decal_material* material_info,
// Set 2: PerDraw
vk::DescriptorSet perDrawSet = descManager->allocateFrameSet(DescriptorSetIndex::PerDraw);
Assert(perDrawSet);
writer.writeSet(perDrawSet, VulkanDescriptorManager::getSetTemplate(DescriptorSetIndex::PerDraw));
writer.writeSet(DescriptorSetIndex::PerDraw, perDrawSet);
writer.setBuffer(PerDrawBinding::Matrices,
getPendingBufInfo(static_cast<size_t>(uniform_block_type::Matrices)));
writer.setBuffer(PerDrawBinding::DecalInfo,
getPendingBufInfo(static_cast<size_t>(uniform_block_type::DecalInfo)));
writer.flush();
stateTracker->bindDescriptorSet(DescriptorSetIndex::Global, globalSet);
stateTracker->bindDescriptorSet(DescriptorSetIndex::Material, materialSet);
stateTracker->bindDescriptorSet(DescriptorSetIndex::PerDraw, perDrawSet);
stateTracker->bindDescriptorSet(DescriptorSetIndex::Global, globalSet, nullptr);
stateTracker->bindDescriptorSet(DescriptorSetIndex::Material,
materialSet,
writer.dynamicOffsets(DescriptorSetIndex::Material));
stateTracker->bindDescriptorSet(DescriptorSetIndex::PerDraw,
perDrawSet,
writer.dynamicOffsets(DescriptorSetIndex::PerDraw));
// Bind vertex buffers: binding 0 = box VBO, binding 1 = instance buffer
vk::Buffer boxVBO = bufferManager->getVkBuffer(buffers.Vbuffer_handle);
+120 -42
View File
@@ -15,10 +15,17 @@ static constexpr DescriptorBindingTemplate s_globalBindings[] = {
{GlobalBinding::EnvMap, vk::DescriptorType::eCombinedImageSampler, 1, vk::ShaderStageFlagBits::eFragment, vk::ImageViewType::eCube},
{GlobalBinding::IrradianceMap, vk::DescriptorType::eCombinedImageSampler, 1, vk::ShaderStageFlagBits::eFragment, vk::ImageViewType::eCube},
{GlobalBinding::ShadowCascadeParams, vk::DescriptorType::eUniformBuffer, 1, vk::ShaderStageFlagBits::eVertex | vk::ShaderStageFlagBits::eFragment},
{GlobalBinding::ShadowMapRaw, vk::DescriptorType::eCombinedImageSampler, 1, vk::ShaderStageFlagBits::eFragment, vk::ImageViewType::e2DArray},
};
static constexpr DescriptorBindingTemplate s_globalTlasBinding{
GlobalBinding::Tlas, vk::DescriptorType::eAccelerationStructureKHR, 1, vk::ShaderStageFlagBits::eFragment};
// GLOBAL_DYNAMIC_OFFSET_COUNT is stated rather than derived because this set's template is
// assembled at runtime (see below); keep the two in agreement. The optional TLAS binding is not
// a dynamic UBO, so it cannot change the count either.
static_assert(dynamic_offset_count(s_globalBindings) == GLOBAL_DYNAMIC_OFFSET_COUNT,
"The Global set gained a dynamic binding; GLOBAL_DYNAMIC_OFFSET_COUNT must follow.");
// The Global set's layout must omit the TLAS binding entirely on devices without
// VK_KHR_acceleration_structure enabled -- a descriptor type the device didn't
// enable the extension for is invalid in vkCreateDescriptorSetLayout. So, unlike
@@ -27,26 +34,10 @@ static constexpr DescriptorBindingTemplate s_globalTlasBinding{
// the per-instance m_globalBindingsRuntime/m_globalTemplateRuntime members
// (see VulkanDescriptorManager.h), rather than compiled in as a fixed constexpr array.
static constexpr DescriptorBindingTemplate s_materialBindings[] = {
{MaterialBinding::ModelData, vk::DescriptorType::eUniformBuffer, 1, vk::ShaderStageFlagBits::eVertex | vk::ShaderStageFlagBits::eFragment},
{MaterialBinding::TextureArray, vk::DescriptorType::eCombinedImageSampler, 16, vk::ShaderStageFlagBits::eFragment, vk::ImageViewType::e2DArray},
{MaterialBinding::DecalGlobals, vk::DescriptorType::eUniformBuffer, 1, vk::ShaderStageFlagBits::eVertex | vk::ShaderStageFlagBits::eFragment},
{MaterialBinding::TransformSSBO, vk::DescriptorType::eStorageBuffer, 1, vk::ShaderStageFlagBits::eVertex},
{MaterialBinding::DepthMap, vk::DescriptorType::eCombinedImageSampler, 1, vk::ShaderStageFlagBits::eFragment},
{MaterialBinding::SceneColor, vk::DescriptorType::eCombinedImageSampler, 1, vk::ShaderStageFlagBits::eFragment},
{MaterialBinding::DistortionMap, vk::DescriptorType::eCombinedImageSampler, 1, vk::ShaderStageFlagBits::eFragment},
{MaterialBinding::ShadowMapData, vk::DescriptorType::eUniformBuffer, 1, vk::ShaderStageFlagBits::eVertex},
};
static constexpr DescriptorSetTemplate s_materialTemplate(s_materialBindings);
static constexpr DescriptorBindingTemplate s_perDrawBindings[] = {
{PerDrawBinding::GenericData, vk::DescriptorType::eUniformBuffer, 1, vk::ShaderStageFlagBits::eVertex | vk::ShaderStageFlagBits::eFragment},
{PerDrawBinding::Matrices, vk::DescriptorType::eUniformBuffer, 1, vk::ShaderStageFlagBits::eVertex | vk::ShaderStageFlagBits::eFragment},
{PerDrawBinding::NanoVGData, vk::DescriptorType::eUniformBuffer, 1, vk::ShaderStageFlagBits::eVertex | vk::ShaderStageFlagBits::eFragment},
{PerDrawBinding::DecalInfo, vk::DescriptorType::eUniformBuffer, 1, vk::ShaderStageFlagBits::eVertex | vk::ShaderStageFlagBits::eFragment},
{PerDrawBinding::MovieData, vk::DescriptorType::eUniformBuffer, 1, vk::ShaderStageFlagBits::eFragment},
};
static constexpr DescriptorSetTemplate s_perDrawTemplate(s_perDrawBindings);
// The binding declarations themselves live in the header (MaterialSetBindings /
// PerDrawSetBindings) because the dynamic-offset layout is derived from them there.
static constexpr DescriptorSetTemplate s_materialTemplate(MaterialSetBindings);
static constexpr DescriptorSetTemplate s_perDrawTemplate(PerDrawSetBindings);
// ========== Static uniform binding mappings ==========
@@ -92,12 +83,21 @@ const vk::DescriptorImageInfo& DescriptorFallbacks::getImage(vk::ImageViewType t
// ========== DescriptorWriter template-based methods ==========
void DescriptorWriter::writeSet(vk::DescriptorSet set, const DescriptorSetTemplate& tmpl)
void DescriptorWriter::writeSet(DescriptorSetIndex setIndex, vk::DescriptorSet set)
{
Assert(m_fallbacks);
const DescriptorSetTemplate& tmpl = VulkanDescriptorManager::getSetTemplate(setIndex);
// Clear binding slots for this set
m_bindingSlots = {};
m_currentSet = setIndex;
// Dynamic offsets are consumed in binding order, so assign slots as the template is walked
// (templates list bindings in ascending binding number). This must stay the same rule
// dynamic_offset_slot() applies — hence advancing by b.count, not by one binding.
uint32_t nextDynIndex = 0;
m_dynOffsets[static_cast<size_t>(setIndex)] = {};
for (const auto& b : tmpl) {
Assert(m_writeCount < MAX_WRITES);
@@ -116,6 +116,11 @@ void DescriptorWriter::writeSet(vk::DescriptorSet set, const DescriptorSetTempla
bool isImage = (b.type == vk::DescriptorType::eCombinedImageSampler);
bool isAccelStruct = (b.type == vk::DescriptorType::eAccelerationStructureKHR);
if (b.type == vk::DescriptorType::eUniformBufferDynamic) {
Assert(nextDynIndex + b.count <= MAX_DYNAMIC_OFFSETS_PER_SET);
slot.dynIndex = static_cast<int>(nextDynIndex);
nextDynIndex += b.count;
}
if (isImage) {
Assert(m_imageInfoCount + b.count <= MAX_IMAGE_INFOS);
auto* dst = &m_imageInfos[m_imageInfoCount];
@@ -144,12 +149,40 @@ void DescriptorWriter::writeSet(vk::DescriptorSet set, const DescriptorSetTempla
} else {
Assert(m_bufferInfoCount < MAX_BUFFER_INFOS);
m_bufferInfos[m_bufferInfoCount] = m_fallbacks->buffer;
// The fallback buffer info starts at offset 0, which is also the base a
// dynamic descriptor needs (its offset lives in the dynamic-offset array,
// left at 0 here until setBuffer supplies a real one).
w.pBufferInfo = &m_bufferInfos[m_bufferInfoCount];
slot.bufferInfo = &m_bufferInfos[m_bufferInfoCount++];
}
}
}
void DescriptorWriter::bindSets(vk::CommandBuffer cmd,
vk::PipelineLayout layout,
DescriptorSetIndex firstSet,
ArrayView<vk::DescriptorSet> sets)
{
size_t out = 0;
for (size_t i = 0; i < sets.size; ++i) {
const auto set = static_cast<DescriptorSetIndex>(static_cast<uint32_t>(firstSet) + i);
Assert(static_cast<uint32_t>(set) < static_cast<uint32_t>(DescriptorSetIndex::Count));
const uint32_t dynCount = VulkanDescriptorManager::getDynamicOffsetCount(set);
Assert(dynCount <= MAX_DYNAMIC_OFFSETS_PER_SET);
for (uint32_t j = 0; j < dynCount; ++j) {
Assert(out < m_dynOffsetScratch.size());
m_dynOffsetScratch[out++] = m_dynOffsets[static_cast<size_t>(set)][j];
}
}
cmd.bindDescriptorSets(vk::PipelineBindPoint::eGraphics,
layout,
static_cast<uint32_t>(firstSet),
vk::ArrayProxy<const vk::DescriptorSet>(static_cast<uint32_t>(sets.size), sets.data),
vk::ArrayProxy<const uint32_t>(static_cast<uint32_t>(out), m_dynOffsetScratch.data()));
}
void DescriptorWriter::flush()
{
if (m_writeCount > 0) {
@@ -167,6 +200,25 @@ void DescriptorWriter::setBuffer(uint32_t binding, const vk::DescriptorBufferInf
Assert(binding < MAX_BINDINGS_PER_SET);
auto& slot = m_bindingSlots[binding];
Assert(slot.bufferInfo);
if (slot.dynIndex >= 0) {
// Dynamic binding: the descriptor holds only {buffer, 0, range}; the caller's
// offset becomes the dynamic offset applied at bind time. Keeping it out of
// the descriptor is what lets the set survive an offset-only change.
auto& dyn = m_dynOffsets[static_cast<size_t>(m_currentSet)][static_cast<size_t>(slot.dynIndex)];
if (info.buffer) {
Assertion(info.offset <= static_cast<vk::DeviceSize>(UINT32_MAX),
"Dynamic uniform buffer offset " SIZE_T_ARG " does not fit in a uint32 dynamic offset!",
static_cast<size_t>(info.offset));
*slot.bufferInfo = vk::DescriptorBufferInfo(info.buffer, 0, info.range);
dyn = static_cast<uint32_t>(info.offset);
} else {
*slot.bufferInfo = m_fallbacks->buffer;
dyn = 0;
}
return;
}
if (info.buffer) {
*slot.bufferInfo = info;
} else {
@@ -279,6 +331,23 @@ const DescriptorSetTemplate& VulkanDescriptorManager::getSetTemplate(DescriptorS
}
}
uint32_t VulkanDescriptorManager::getDynamicOffsetCount(DescriptorSetIndex setIndex)
{
// Constant per set (see the derived *_DYNAMIC_OFFSET_COUNT values), not a scan of the
// template — this runs on every descriptor set bind.
switch (setIndex) {
case DescriptorSetIndex::Global:
return GLOBAL_DYNAMIC_OFFSET_COUNT;
case DescriptorSetIndex::Material:
return MATERIAL_DYNAMIC_OFFSET_COUNT;
case DescriptorSetIndex::PerDraw:
return PERDRAW_DYNAMIC_OFFSET_COUNT;
default:
UNREACHABLE("Invalid descriptor set index %u!", static_cast<uint32_t>(setIndex));
return 0;
}
}
vk::DescriptorSetLayout VulkanDescriptorManager::getSetLayout(DescriptorSetIndex setIndex) const
{
return m_setLayouts[static_cast<size_t>(setIndex)].get();
@@ -294,22 +363,31 @@ vk::DescriptorSet VulkanDescriptorManager::allocateFrameSet(DescriptorSetIndex s
auto& pools = m_framePools[m_currentFrame];
++m_setsAllocatedThisFrame;
// Allocate through the non-throwing, non-allocating vulkan.hpp overload: this is
// one of the hottest calls in the backend (thousands per frame), and the
// vector-returning overload heap-allocates on every single one.
vk::DescriptorSetAllocateInfo allocInfo;
allocInfo.descriptorSetCount = 1;
allocInfo.pSetLayouts = &layout;
// Try allocating from the last pool in the list
if (!pools.empty()) {
vk::DescriptorSetAllocateInfo allocInfo;
allocInfo.descriptorPool = pools.back().get();
allocInfo.descriptorSetCount = 1;
allocInfo.pSetLayouts = &layout;
try {
auto sets = m_device.allocateDescriptorSets(allocInfo);
return sets[0];
} catch (const vk::OutOfPoolMemoryError&) {
// Pool exhausted, fall through to create a new one
vk::DescriptorSet set;
vk::Result result = m_device.allocateDescriptorSets(&allocInfo, &set);
if (result == vk::Result::eSuccess) {
return set;
}
if (result == vk::Result::eErrorOutOfPoolMemory) {
nprintf(("vulkan", "VulkanDescriptorManager: frame pool exhausted, allocating an additional pool chunk\n"));
} catch (const vk::FragmentedPoolError&) {
// Pool fragmented, fall through to create a new one
} else if (result == vk::Result::eErrorFragmentedPool) {
nprintf(("vulkan", "VulkanDescriptorManager: frame pool fragmented, allocating an additional pool chunk\n"));
} else {
nprintf(("vulkan",
"VulkanDescriptorManager: descriptor set allocation failed (%s)\n",
vk::to_string(result).c_str()));
return {};
}
}
@@ -318,18 +396,17 @@ vk::DescriptorSet VulkanDescriptorManager::allocateFrameSet(DescriptorSetIndex s
nprintf(("vulkandescriptor", "VulkanDescriptorManager: Grew frame %u pool count to %zu\n",
m_currentFrame, pools.size()));
vk::DescriptorSetAllocateInfo allocInfo;
allocInfo.descriptorPool = pools.back().get();
allocInfo.descriptorSetCount = 1;
allocInfo.pSetLayouts = &layout;
try {
auto sets = m_device.allocateDescriptorSets(allocInfo);
return sets[0];
} catch (const vk::SystemError& e) {
nprintf(("vulkan", "VulkanDescriptorManager: Failed to allocate frame descriptor set after pool growth: %s\n", e.what()));
vk::DescriptorSet set;
vk::Result result = m_device.allocateDescriptorSets(&allocInfo, &set);
if (result != vk::Result::eSuccess) {
nprintf(("vulkan",
"VulkanDescriptorManager: Failed to allocate frame descriptor set after pool growth: %s\n",
vk::to_string(result).c_str()));
return {};
}
return set;
}
void VulkanDescriptorManager::beginFrame()
@@ -403,9 +480,10 @@ vk::UniqueDescriptorPool VulkanDescriptorManager::createFramePool()
// constants in VulkanDescriptorManager.h. Additional pool chunks are created
// automatically when one is exhausted.
SCP_vector<vk::DescriptorPoolSize> poolSizes = {
{ vk::DescriptorType::eUniformBuffer, MAX_UNIFORM_BUFFERS_PER_POOL },
{ vk::DescriptorType::eCombinedImageSampler, MAX_SAMPLERS_PER_POOL },
{ vk::DescriptorType::eStorageBuffer, MAX_SETS_PER_POOL },
{vk::DescriptorType::eUniformBuffer, MAX_UNIFORM_BUFFERS_PER_POOL},
{vk::DescriptorType::eUniformBufferDynamic, MAX_DYNAMIC_UNIFORM_BUFFERS_PER_POOL},
{vk::DescriptorType::eCombinedImageSampler, MAX_SAMPLERS_PER_POOL},
{vk::DescriptorType::eStorageBuffer, MAX_SETS_PER_POOL},
};
if (m_raytracingEnabled) {
poolSizes.emplace_back(vk::DescriptorType::eAccelerationStructureKHR, MAX_SETS_PER_POOL);
Loaded 100 of 267 files, more files were not shown because too many files have changed in this diff. Show more