Compare commits

...
Author SHA1 Message Date
dustinspace217andDustinspace217 d27de962b2 P3: Parse HUD firepoint multipliers as floats (#7841)
* Parse HUD firepoint multipliers as floats

The reticle gauge's "Firepoint X coordinate multiplier:" and
"Firepoint Y coordinate multiplier:" were read with stuff_int into int
members, so a table value like 6.0 trips the strict parser ("Expected
int, found [.0]" followed by "Detected invalid tokens while parsing HUD
gauges") and fractional multipliers were never possible. This makes the
two multipliers float at the parse site, in HudGaugeReticle, and in
initFirepointDisplay. The render path already multiplied a float
coordinate by them and rounded with fl2i, so integer table values draw
exactly where they did before. "Firepoint size:" stays an int, since it
is a pixel radius.

Fixes #3424

* Put initFirepointDisplay's opening brace on its own line

---------

Co-authored-by: Dustinspace217 <[email protected]>
2026-11-08 21:08:02 -05:00
Goober5000 cc6f50682b Merge pull request #8007 from Goober5000/fix/fred_event_editor_messages
Fix FRED event editor not listing newly created messages in sexp trees
2026-11-08 21:05:34 -05:00
Goober5000andClaude Opus 5.5 6ce6ba8eb1 list the event editor's working messages in FRED sexp trees
The sexp tree refactor (#7301) moved the message listing behind
SexpTreeEditorInterface, whose default reads the global Messages[]
array.  FRED's event editor edits a local copy of the messages and
only commits them on save, but did not override getMessages() or
hasDefaultMessageParameter(), so newly created messages could not be
used in an event until the editor was saved and closed.  Restore the
pre-refactor behavior by overriding both, as QtFRED already does.

Also remove current_message_name() and get_message_list_item(), which
have had no callers since the refactor.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-08 19:52:44 -05:00
Goober5000andClaude Opus 5.5 61ad242997 fix lightning glow offset and collision LOD clamp (#8012)
Section-start glows used the b_add of the most recently created bolt
type (Nebl_type) rather than the bolt being drawn, so concurrent bolts
of different types got the wrong glow offset.  The cap glow already
used the correct type.

Clamp $Collision LOD to num_detail_levels - 1, the highest valid
detail level.  The previous bound was harmless because the collision
code falls back to the lowest existing LOD, but it was off by one.

Co-authored-by: Claude Opus 5.5 <[email protected]>
2026-11-07 19:58:34 -05:00
dustinspace217andDustinspace217 9f2a41032d Vulkan: apply Lighting changes without a restart (#7974)
Changing Lighting in the options recompiles every shader, but Vulkan kept drawing with the pipelines it had already
built, and a pipeline keeps the shader code it was created from. So the new lighting level only showed up after a
restart. OpenGL applies it at once.

After the recompile, mark the cached pipelines stale. At the start of the next frame, before anything is recorded,
wait for the GPU and drop them; the next draws build them again from the new shaders. The recompile can run in the
middle of a frame (from the options menu or a script), so the drop waits for the next frame instead of happening there.

The recompile also rebuilds the anti-aliasing family that is not in use, which cannot compile under the current mode.
Its cached pipeline used to hide that; now the failed variant is dropped so it compiles again when that mode returns.

Fixes #7967

Co-authored-by: Dustinspace217 <[email protected]>
2026-11-07 19:56:30 +01:00
Goober5000andClaude Opus 5.5 5b6579ac0a fix lightning glow offset and collision LOD clamp
Section-start glows used the b_add of the most recently created bolt
type (Nebl_type) rather than the bolt being drawn, so concurrent bolts
of different types got the wrong glow offset.  The cap glow already
used the correct type.

Clamp $Collision LOD to num_detail_levels - 1, the highest valid
detail level.  The previous bound was harmless because the collision
code falls back to the lowest existing LOD, but it was off by one.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 20:54:01 -05: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
BMagnu 8afa318428 Clamp requested particle spawn time in both directions (#7927) 2026-10-27 13:00:00 +09:00
Goober5000 cf9c5590e3 Merge pull request #7926 from Goober5000/fix/jumpnode_rename
Fix jump node renames being lost in FRED2
2026-10-26 23:00:31 -04:00
Goober5000andClaude Opus 5.5 9879110c1f Fix jump node renames being lost in FRED2
The jump node dialog only copied control text into its member variables
in OnClose(), so update_data() calls from selection changes, saves, and
the dialog's own node menu applied stale values and discarded the edit.
Call UpdateData(TRUE) inside update_data(), as the waypoint path dialog
does.  This regressed when the dialog was split out of the waypoint
dialog in 78b957982.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 19:15:00 -04:00
Goober5000 a8d5b3d6f9 Merge pull request #7921 from Goober5000/fix/vulkan_texture_addressing
honor material texture addressing in Vulkan
2026-10-24 03:10:07 -04:00
Goober5000andClaude Fable 5.1 acf613c13c Fix FastDebug builds linking the non-redistributable debug CRT (#7919)
* fix FastDebug QtFRED linking the debug CRT

FastDebug builds in the DLL-runtime tree (used whenever qtFRED is
enabled) compiled with /MDd and, since #7450, linked Qt's debug
libraries.  Both import ucrtbased.dll, MSVCP140D.dll and
VCRUNTIME140D.dll, which Microsoft does not license for redistribution,
so end users could not run the FastDebug qtFRED shipped in nightlies
and releases.  Static Qt is ruled out by the LGPL, so the only shippable
shape is the release CRT plus release Qt.

On MSVC, _DEBUG is the CRT's own contract: /MTd and /MDd define it
themselves, and the STL headers select msvcprtd.lib and
_ITERATOR_DEBUG_LEVEL=2 from it.  Stop force-defining it for FastDebug
on MSVC; the engine keys its debug behaviour on NDEBUG, and the few
_DEBUG guards in code/ are switched to NDEBUG so they keep working.
The static tree is unchanged: /MTd still defines _DEBUG there.

Fixes #7915

Co-Authored-By: Claude Fable 5.1 <[email protected]>

* derive the MSVC runtime choice once

The "DLL runtime or not" condition was spelled out in the toolchain and
twice in fred2 with "keep in sync" comments, and gtest_force_shared_crt
and antlr4's WITH_STATIC_CRT had already drifted: they only looked at
MSVC_USE_RUNTIME_DLL, so a qtFRED tree with tests enabled would have
asked both for the static CRT.  That is harmless today only because our
flag strings carry no literal /MD for them to rewrite.

Compute FSO_MSVC_RUNTIME_DLL once in toolchain-msvc.cmake and use it
everywhere.  InstallRequiredSystemLibraries deliberately stays keyed on
MSVC_USE_RUNTIME_DLL: the qtFRED deploy ships no compiler runtime.

Co-Authored-By: Claude Fable 5.1 <[email protected]>

* remove FastDebug runtime leftovers

The /MD -> /MDd substitution on the FastDebug flag strings has done
nothing since the runtime moved to CMAKE_MSVC_RUNTIME_LIBRARY: the
Release flags it copies contain no runtime switch.  The commented-out
_ITERATOR_DEBUG_LEVEL=0 lines for gtest and the OpenXR loader belong to
the scheme #7450 removed from the toolchain.

Co-Authored-By: Claude Fable 5.1 <[email protected]>

* use the release CRT for all FastDebug builds

Microsoft's documentation marks the static debug libraries (libcmtd,
libucrtd, libvcruntimed) "Not redistributable" and states that debug
versions of an application may only be copied to machines used for
testing, and the license grants distribution rights only for code on
the REDIST list, which contains no debug library in any form.  So the
statically linked FastDebug game and FRED2 are no more shippable than
the /MDd qtFRED was.

Link every FastDebug build against the release runtime.  The engine's
debug behaviour keys on NDEBUG, which FastDebug still leaves undefined,
so asserts, warnings and mprintf are unaffected; only the debug CRT's
heap checking goes, and that remains available in Debug builds.  FRED2's
static MFC library follows the CRT: the debug variant requires the debug
runtime.

Co-Authored-By: Claude Fable 5.1 <[email protected]>

* keep the debug icon, version flag and FRED2 asserts without _DEBUG

The resource compiler only ever sees the explicit definitions, so with
_DEBUG no longer defined for FastDebug the game lost its debug icon and
both editors lost the VS_FF_DEBUG version flag.  Key those on NDEBUG,
which FastDebug still leaves undefined.

FRED2 also used MFC's ASSERT, VERIFY, ASSERT_VALID and TRACE0, which are
all compiled out without _DEBUG.  Convert them to the FSO equivalents
FRED2 already uses nearly everywhere else: Assert, Verify, a null check
in place of ASSERT_VALID (AssertValid only exists in debug MFC), and
mprintf.  The AssertValid/Dump overrides and DEBUG_NEW stay under
_DEBUG, as they must.

Co-Authored-By: Claude Fable 5.1 <[email protected]>

---------

Co-authored-by: Claude Fable 5.1 <[email protected]>
2026-10-24 03:09:57 -04:00
Goober5000andClaude Opus 5.5 afd92bb3f9 honor material texture addressing in Vulkan
Vulkan never read the material's texture addressing, so MR_FORCE_CLAMP
skyboxes were sampled with a wrapping sampler, producing seams.  Set it
on every material draw, as OpenGL does in opengl_tnl_set_material().

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-22 23:47:54 -04:00
Taylor Richards 76219fd1e2 lower Vulkan requirement and fix shaderc includes (#7902)
Lower Vulkan version requirement to 1.4.304, which should be the lowest
version supported without requiring code changes. This should also offer
out of the box support on current Linux distros, such as Debian 13 (trixie)
and Ubuntu 25.10 (questing) & 26.04 (resolute).

Also make sure that the shaderc headers are available and added to the
include path on Linux when using system libraries.
2026-10-18 05:53:10 -04: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
Taylor Richards 12a36dc1a3 fix various macOS/Xcode project issues (#7875)
Silence CMake build type warnings when using a multi-config generator.

Fix macOS deployment target specification. It must be set before
the first project() command to work properly, and needs to be a cached
value so that it can be overridden by the user. This in particular is
required for Xcode 27 where the minimum deployment target is 12.0 and
using anything less is a hard fail rather than a warning.

Fix Xcode scheme only containing qtFred as a buildable target.

Fix various Xcode warnings when a static library contains files without
any symbols.

Allow FSO/qtFRED cmdline arguments and working directory to be set via their
respective options (FSO_RUN_ARGUMENTS & FSO_FREESPACE_PATH) in Xcode.

Fix product identifier mismatch and unset minimum system version warnings
in Info.plist file.
2026-10-17 06:16:10 -04:00
Goober5000andClaude Opus 5.5 c28032a2af Make quickstart options quick, and fix several associated bugs (#7834)
* only strip the legacy M/S suffix from the last pilot when needed

player_select_get_last_pilot_info() chopped any trailing uppercase M or S
from the LastPlayer value, a holdover from the pre-pilot-file era.  Pilots
whose callsigns end in M or S (e.g. BOSS) were never found as the last
pilot.  Now the suffix is only removed when the full callsign is not a
pilot but the shortened one is.  This also avoids indexing before the
string when LastPlayer is empty.

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

* route all pilot selection through one commit function

player_select_close() and player_finish_select() each had their own copy
of the code that loads a pilot as the active Player, and the copies had
drifted apart.  Both now call player_select_commit_pilot().

This fixes two bugs:
* -pilot did not work with the retail pilot select screen.
  player_finish_select() loaded the requested pilot, but then
  player_select_close() loaded the highlighted pilot (the most recent
  one) over it.  The close handler now skips the load if a pilot has
  already been committed.
* player_finish_select() ignored its is_multi parameter when setting
  player_was_multi, so scripted selection of a multiplayer pilot saved
  it as single-player.

Scripted selection also now saves the pilot file, as the retail screen
already did.  The background bitmap handle starts out as -1 so that the
close handler doesn't release bitmap 0 when the screen was never set up.

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

* select pilots automatically at game init, through the normal commit path

-startgame, -connect and -almission (via Cmdline_use_last_pilot) used
player_select_get_last_pilot(), which loaded the last pilot directly
without most of the setup that a normal selection does.  It skipped
valid_pilot(), multi_options_init_globals() and saving the pilot file.
Because it never entered the pilot select state, it also turned off the
main hall's fallback for an unavailable campaign.  Meanwhile -pilot and
-benchmark_mode were handled on the pilot select screen itself, which
scripted UIs replace entirely.

All of these now go through player_select_try_auto_select() at
GS_EVENT_GAME_INIT.  It picks the -pilot callsign, else the last pilot
if it still exists, else the most recently saved pilot.  It shows the
startup warning popups, validates the pilot, and commits it through
player_select_commit_pilot().  The pilot is multiplayer if -startgame or
-connect was given, and single-player otherwise.  If there are no
pilots, or validation fails, the pilot select screen is shown as usual.

The main hall now allows the campaign fallback whenever a pilot has just
been committed, not only when coming from the pilot select state.

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

* select a pilot automatically for -start_mission

-start_mission skips the main hall, but it still waited on the pilot
select screen.  It now uses the same automatic pilot selection as the
other quickstart options, so the screen only appears if there are no
pilots.  The pilot is committed as single-player, consistent with the
main hall forcing single-player mode for -start_mission.

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

* skip the intro movie for quickstart options

When the command line asks to skip pilot selection (-start_mission,
-pilot, -startgame, -connect, -almission, -benchmark_mode), the intro
movie now doesn't play either.  It could take several minutes before the
game even reached the pilot.  The OnIntroAboutToPlay hook still runs, as
it does when the mod table disables the intro.

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

---------

Co-authored-by: Claude Opus 5.5 <[email protected]>
2026-10-16 20:57:07 -04:00
Taylor Richards b477dd5ca9 lower required SDL3 version to 3.2.8 (#7876)
This should be a safe minimum for us when system libraries are either
required or preferred. Most current Linux distros that have SDL3 should
support this version or better.
2026-10-16 04:36:51 -04:00
Goober5000 7b80cd4897 Merge pull request #7871 from notimaginative/prebuilt_update
bump prebuilt libs for SDL 3.4.18
2026-10-15 23:19:01 -04:00
LuytenKy e76e844d02 Vulkan: Skip MSAA resolve when no deferred pass is active (#7854)
* stub check for msaa

* initialize viewport variables
2026-10-15 18:04:32 +02:00
Taylor Richards 2ab398b34a bump prebuilt libs for SDL 3.4.18 2026-10-15 03:47:06 -04:00
BMagnu 88669e4518 Fix VR cursor issues and warn about crash hazard (#7868)
* Add warning for OpenXR toolkit

* Properly handle mouse position

* GCC 9 issue
2026-10-15 00:48:46 +00:00
BMagnu f6f1aac574 Fix fullscreen with resolutions not available for modesetting (#7867) 2026-10-15 02:01:11 +02:00
Goober5000andClaude Fable 5.1 b37cfda56d Fix a model refcount bug on loadout change, and harden model_unload against stale handles (#7851)
* take a model reference when a ship changes class

ship_model_change() only loaded the model when the class had none, so a ship
switched to a class whose model had been loaded only by a select-screen
preview adopted that preview's single reference.  Closing the screen then
freed the model from under the ship, and the first simulation frame asserted
in model_get().  This is what happened on a replayed mission with a restored
loadout: ship select was skipped, the weapon-select overhead loaded the new
class once, the commit switched the player to it, and the screen close
released it.

Load unconditionally, as ship_create() already does, so the ship holds its own
reference.

Co-Authored-By: Claude Fable 5.1 <[email protected]>

* reject stale model ids in model_unload and release select-screen handles at level close

model_unload only used the slot part of the id it was given, so a handle kept
across missions would free whatever model had since been loaded into that slot.
Validate the id against the slot's occupant and warn instead.

Two holders could present such handles: the weapon-select icon models under a
scripted UI that overrides the state hooks (the native close never runs), and
the ship-select 3D model global, which was never reset between missions.
Release the select-screen handles from game_level_close, and unload the
ship-select model on screen close and on every selection change.

Co-Authored-By: Claude Fable 5.1 <[email protected]>

* consolidate the select-screen resource release

The weapon side released its icons, anims, and ship class data from two
functions keyed on whether the screen was open, and the level-close path added
a third copy.  Give the weapon side the same shape as the ship side: one
unconditional weapon_select_common_close(), with weapon_select_close() left as
pure UI teardown.  common_select_level_close() now just calls the two common
closes, and common_select_close() calls it in turn.  The ship-select 3D model
is released from ship_select_common_close() so both paths cover it.

Co-Authored-By: Claude Fable 5.1 <[email protected]>

---------

Co-authored-by: Claude Fable 5.1 <[email protected]>
2026-10-13 02:19:48 -04:00
Goober5000 085ff7a70b Merge pull request #7852 from Goober5000/fix/loadout_wireframe_wipe
restore the wireframe wipe in the FS2 loadout effect
2026-10-12 23:32:32 -04:00
Goober5000 72d579e010 Merge pull request #7818 from Goober5000/fix/ai_target_recovery
apply $fix ai target recovery's pursuit filter only to targets picked for pursuit
2026-10-12 22:59:38 -04:00
Goober5000andClaude Opus 5.5 2f6bca06f7 restore the wireframe wipe in the FS2 loadout effect
During phase 1 of the FS2 ship/weapon select animation, the wireframe
is supposed to be revealed as the scanline moves down.  That relied on
the clip plane set up for the grid carrying over to the wireframe
render.  90a2676f1 replaced the grid's clip plane with line trimming,
which left the phase 1 wireframe unclipped, so it popped in all at once.

Clip the wireframe in both phase 1 and phase 2.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-12 22:31:50 -04:00
Goober5000 904edfee44 Merge pull request #7842 from dustinspace217/qtfred/hidpi-font
qtFRED: scale viewport labels by the device pixel ratio
2026-10-12 21:35:46 -04:00
Goober5000 d9b4d81d93 Merge pull request #7844 from Goober5000/fix/mission_splash_scaling
Scale mission load screen backgrounds to fit the screen
2026-10-12 21:29:00 -04:00
Goober5000 4fcd470135 Merge pull request #7846 from scp-fs2open/dependabot/pip/ci/post/mako-1.4.2
Bump mako from 1.3.12 to 1.4.2 in /ci/post
2026-10-12 21:28:26 -04:00
Goober5000andClaude Opus 5.5 1af0eb82bf apply $fix ai target recovery's pursuit filter only to targets picked for pursuit
The actively-pursues filter added to "$fix ai target recovery:" in #7736 was applied to every target search.  But a ship's target is not only used for pursuit: it also drives the ship's turrets, other ships' guard reactions, and attacker counts.  In retail SM1-04, the friendly Knossos (which does not pursue fighters) was made to target the Behemoth instead of a Mara, which caused the Mara guards to react to the protected Knossos endlessly instead of rushing the player.

The filter now applies only where the target is chosen for maneuvering:
* find_enemy() and get_nearest_objnum() take a required for_pursuit parameter; automatic searches pass ai_targets_for_pursuit(), which is true only in AIM_CHASE or AIM_STRAFE
* guard reactions, retaliation (dynamic chase), and the retargets in ai_big_chase_attack() and ai_big_maybe_enter_strafe_mode() skip ships the attacker won't pursue, via the new ai_declines_pursuit()
* ai_attack_wing() prefers living wing members the ship will pursue; if there are none, it holds a member as an explicit order to attack an unpursued ship does
* get_nearest_objnum() holds an unpursued member of the enemy wing rather than leaving the wing

Since refused targets are no longer picked for pursuit, ai_chase() no longer needs to drop them.  Retail behavior is unchanged when the flag is disabled.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
2026-10-12 21:26:53 -04:00
Goober5000 6e469a2180 Merge pull request #7837 from Goober5000/fix/ai_mode_fields
reset stale AI order state and track previous_mode through dynamic chases
2026-10-12 21:24:31 -04:00
Goober5000 f64d940963 capitalization 2026-10-12 20:09:29 -04:00
Goober5000andClaude Fable 5.1 2052dcaeb0 reset stale AI order state and track previous_mode through dynamic chases
Several ai_info fields remember which order set them up (enemy_wing,
guard_objnum/signature/wingnum) or which mode a dynamic goal interrupted
(previous_mode, resume_goal_time), but nothing cleared them when a new
order took over.  A ship that once guarded a wing would be yanked back
into AIM_GUARD whenever a wingmate died; a stale previous_mode of
AIM_GUARD could make a former guard abandon a counterattack; a stale
enemy_wing biased every find_enemy() toward a long-ago target; and a
resume_goal_time left over from a dynamic chase suppressed target
acquisition for up to 20 seconds into the next order.

Add ai_reset_order_state(), called when ai_process_mission_orders()
takes up a goal and when ai_do_default_behavior() runs.  Each order's
own setup repopulates the fields it needs.  enemy_wing is reset under
either $fix stale ai order state or $fix ai target recovery, since it
affects target selection; the rest only under the new flag.

maybe_set_dynamic_chase() wrote aip->mode into previous_submode, a
retail typo that left previous_mode untracked for dynamic chases.  Under
the flag, save both fields properly, but not when a dynamic goal is
already in progress, so a nested interruption does not clobber the mode
to return to.  Apply the same guard to the strafe/chase transitions in
aibig.cpp.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
2026-10-12 20:09:29 -04:00
dependabot[bot] cba5d1822f Bump mako from 1.3.12 to 1.4.2 in /ci/post
Bumps [mako](https://github.com/sqlalchemy/mako) from 1.3.12 to 1.4.2.
- [Release notes](https://github.com/sqlalchemy/mako/releases)
- [Changelog](https://github.com/sqlalchemy/mako/blob/main/CHANGES)
- [Commits](https://github.com/sqlalchemy/mako/commits)

---
updated-dependencies:
- dependency-name: mako
  dependency-version: 1.4.2
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <[email protected]>
2026-10-12 05:34:09 +00:00
Goober5000 94dd3af03c Merge pull request #7832 from notimaginative/vs2026_workflow
switch Windows workflows to Visual Studio 2026
2026-10-12 01:33:04 -04:00
Dustinspace217 0966d94dc4 Trim comments 2026-10-11 22:11:02 -07:00
Goober5000andClaude Opus 5.5 6e4a55263e Scale mission load screen backgrounds to fit the screen
The load screen background was drawn in the fixed 1024x768 layout, so an
image larger than that overshot the screen and only a corner was visible.
Set the screen scale to the background's dimensions, as the main hall and
title splash already do, and draw the loading animation in the same
layout.

The loading animation is assumed to match the default loading background
(or the default layout, if there is none).  That reference is fit into
the mission's background the way menus are fit into the screen: scaled by
the limiting dimension and centered in the other.  So a mission can use a
background of any resolution or aspect ratio without the animation
changing size or position relative to it.  Retail assets behave as
before.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-11 23:10:06 -04:00
Dustinspace217 d5ca49f916 qtFRED: scale viewport labels by the device pixel ratio
On a HiDPI monitor with OS scaling, every Qt widget in qtFRED scales
but the ship labels, waypoint labels and coordinate readout drawn by
the engine inside the viewport do not, because the engine draws them in
physical pixels. At 200% they come out half the size of everything
around them.

FredRenderer already receives the window's device pixel ratio as the
scale argument of render_frame(). Remember it there and multiply it into
the label font scale at every label site, so the user's Preferences
value stays "100% = normal size on screen" regardless of OS scaling.

Fixes #7727
2026-10-11 03:20:33 -07:00
Goober5000 a1250d7168 Merge pull request #7836 from Goober5000/cleanup/remove_glow_warning
Remove 3D shockwave glow map warning
2026-10-09 01:39:13 -04:00
Goober5000andClaude Sonnet 5.5 014e14af6f Remove 3D shockwave glow map warning
The warning fired for any 3D shockwave model without a glow map, but it turns out that situation is fine.  Drop the warning along with the cmdline include it required.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-09 00:38:27 -04:00
Mike Nelson 0c0320bb87 Null the multi SEXP packet getters' out-pointers on failure (#7828)
get_ship(), get_wing(), get_object() and get_parse_object() only assigned
their out-pointer on success. When a lookup failed (no bytes left, or a net
signature that no longer resolves, e.g. a ship destroyed before the packet
was processed) the caller's uninitialized local was left as stack garbage,
so its nullptr check could pass and the next line would write through it.
multi_sexp_set_camera_target() used the object without checking at all.

Set the out-pointer to nullptr up front, so every caller's existing nullptr
check works. This also clears the C4701/C4703 "potentially uninitialized"
warnings MSVC 2026 reports for the callers in sexp.cpp when it inlines these
getters at link time.
2026-10-09 10:54:31 +09:00
Goober5000 82e7a0793f Merge pull request #7821 from Goober5000/fix/pof_parsing
Harden POF loading: texture limits, file handles, virtual POF failures
2026-10-08 12:06:50 -04:00
Goober5000andClaude Opus 5.5 5dd9f56deb release textures and model memory when a model load fails
The failure checks after parsing (bad detail levels, debris objects or
parents, and too many textures) run after the TXTR and GLOW chunks have
already loaded bitmaps, but model_load only deleted the polymodel, so
those bitmap slots were never released.  Page out the textures and free
the model through model_free, as the virtual POF cache already does for
models it loads.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-08 03:04:02 -04:00
Goober5000andClaude Opus 5.5 4ff6c8c447 accept a cross section count of -1 without a warning
Retail models (111 of them in sparky_fs2.vp) store -1 as their cross
section count, meaning none, which the original loader silently accepted.
Routing the count through pof_read_count made every such model warn on
load.  Read it directly and floor it at zero, as before.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-08 03:02:24 -04:00
Goober5000andClaude Opus 5.5 2f131db53a propagate POF load failures through virtual POFs
A virtual POF ignored the load status of its base POF, so a base that
failed to load (missing, bad version, or rejected after parsing) was
still built upon and reported as a successful load.  Likewise, every
operation that copies from another POF used it even if that POF had
failed to load.

Return FAIL from read_virtual_model_file when the base POF fails, and
have the operations skip, with a warning, any source POF that failed.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-08 03:02:24 -04:00
Goober5000andClaude Opus 5.5 7f71b169dd map appended-submodel textures before changing the virtual POF
When appending submodels would take the combined POF past
MAX_MODEL_TEXTURES, the limit was only noticed partway through, after the
submodel array had been grown, subsystems copied, and n_textures
incremented past the limit, which later loops over maps[] then overran.
Work out the texture mapping first, and give up before touching the model.

Also skip the -1 texture id of untextured flat polygons, which was being
assigned a texture slot, and which replace_textures_used then used to
index Num_verts and Num_polies.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-08 03:02:23 -04:00
Goober5000andClaude Opus 5.5 1d13fc2a83 reject models with too many textures once parsing is finished
Clamping the TXTR count keeps maps[] safe, but texture indices are baked
into the BSP data, so a model declaring more than MAX_MODEL_TEXTURES
textures cannot be built correctly.  Keep the clamp so that the chunk is
still read in full, then fail the load after parsing.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-08 03:02:23 -04:00
Goober5000andClaude Opus 5.5 2563a746aa reject out-of-range BSP texture indices when building polygon data
process_tmap and process_tmap2 used the texture index from the BSP data
to index Num_polies and Num_verts without checking it, so a corrupt
model wrote past both arrays while its vertex buffers were built.  The
collision tree parser already treats such an index as a fatal error, but
it runs afterward.  Raise the same error here, and skip the polygon in
case the error is continued past.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-08 03:02:23 -04:00
Goober5000andClaude Opus 5.5 604eb39519 close the model file on every exit path
The post-parse sanity checks and the bad-version check returned FAIL
without closing the POF.  Nothing reads the file after the chunk loop, so
close it there, and close it before the bad-version return too.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-08 03:02:23 -04:00
Goober5000andClaude Opus 5.5 9ffa5192b0 stay on the v143 toolset and install its ATL/MFC
MSVC 14.50 (v145) no longer targets Windows 7, 8, or 8.1, so building
with it would quietly drop those platforms.  The VS 2026 runner images
include the v143 compiler but ship ATL and MFC only for the latest
toolset, and both are needed: ATL by SAPI speech (sphelper.h) and MFC
by FRED2.  Install the 14.44 ATL/MFC components before configuring and
keep building with v143, matching the 26.0 release.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-07 13:02:32 -04:00
Taylor Richards 521ddcbcca bump platform toolset to 145 (Visual Studio 2026)
The Github runners do not currently include a ATL/MFC version for older
platform versions. As we still need MFC for FRED2 this is the easiest fix.
2026-10-06 23:46:02 -04:00
Goober5000 242eb19ef5 Merge pull request #7808 from Goober5000/cleanup/post_option_logging
log game settings after all tables have been parsed
2026-10-06 19:35:06 -04:00
Goober5000 6115df3e08 Merge pull request #7822 from Goober5000/fix/subsystem_dump
restore the debug subsystem dump
2026-10-06 19:34:49 -04:00
Taylor Richards f54b3fdc5c switch Windows workflows to Visual Studio 2026
All default Windows runners should now be moved over the Visual Studio 2026,
with the exception of windows-2022 (which may be retired before long). So
move to windows-2025 and update the CMake generator to use vs2026 as well.
2026-10-06 19:01:43 -04:00
Goober5000 45b9d31d76 Merge pull request #7820 from Goober5000/fix/insignia_lighting
light insignias using lights rotated into their frame of reference
2026-10-05 21:05:30 -04:00
Goober5000andClaude Opus 5.5 571e63d659 value-initialize lights when adding them
Directional, point, and cone lights left local_vec, local_vec2, and some of
the type-specific fields uninitialized until something rotated or set them.
The sun copies in Static_light were never rotated at all until the previous
commit, so light_apply_rgb's sun term read uninitialized memory.  Zero every
field up front, which also covers what the tube light did by hand.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-05 00:17:02 -04:00
Goober5000andClaude Opus 5.5 d6c253bda3 light insignias using lights rotated into their frame of reference
light_apply_rgb works in the model's local frame, but nothing rotated the
lights into that frame before insignias were drawn: light_rotate_all() was
only called for the briefing and tech room lights, and even then it skipped
Static_light, the separate copies of the sun lights that light_apply_rgb
reads.  So the sun term used a stale, never-set direction and the point
lights were treated as if they sat at the model's origin.

Rotate the lights, including the sun copies, once the insignia's instance
is set up, and light each vertex at its actual position, which includes
the insignia's offset.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-05 00:17:02 -04:00
Goober5000 05a3df4bdd Merge pull request #7819 from Goober5000/fix/norms_faces
store insignia normals per face, and light insignia faces with them
2026-10-05 00:16:01 -04:00
Goober5000 3c7dc9bb35 Merge pull request #7717 from JohnAFernandez/Resize-qtfred-Messages-properties
QTFRED: Event Editor, move Items to a grid layout to better utilize spacing
2026-10-04 15:23:01 -04:00
Goober5000andClaude Opus 5.5 89bad012a2 restore the debug subsystem dump
In debug builds, subsystems found in a model but missing from ships.tbl
are meant to be dumped to data/tables/<model>.subsystems as stub table
entries.  Since subsystem matching was deferred until after the model
file is read (#4590), the dump file was already closed by the time
do_new_subsystem ran, so it was always empty and deleted.  Worse, ss_fp
was left pointing at a closed CFILE slot, which another file could reuse
before do_new_subsystem wrote to it, and the dump file leaked whenever a
load failed.

Open the dump file on the first unmatched subsystem instead, and close it
once the deferred subsystems have been matched.  This also means no file
is created and deleted for models without unmatched subsystems.  Reset
the once-per-model warning flags there as well, rather than only when
the dump file opened successfully.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-04 14:42:50 -04:00
Goober5000 2838d25ab7 Merge pull request #7816 from TheForce172/fix/Qtfred-object-z-axis
QTFred: Object Dialog: Matches Z POS spinner limits with other axis
2026-10-04 13:21:01 -04:00
Goober5000 b666d7eebc Merge pull request #7817 from TheForce172/fix/7815
QTFRED: Fix #7815: Arrival/Departure Cue Update
2026-10-04 13:17:02 -04:00
Goober5000andClaude Opus 5.5 52e5652624 store insignia normals per face, and light insignia faces with them
The insignia normal array was declared with the vertex limit, filled in by
face index, and read by vertex index.  An insignia with more than 81 faces
therefore wrote past the array, into the following insignia or, for the
sixth and final slot, past the insignia list into the rest of the
polymodel.  And the renderer lit each vertex with the normal of whichever
face shared its index, or with a zero normal (ambient light only) when
there was no such face.

A normal belongs to a face, so size the array by the face limit and light
all three corners of each face with that face's normal.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-04 13:14:32 -04:00
Goober5000 b483d30b20 Merge pull request #7804 from Goober5000/fix/misc
four miscellaneous fixes
2026-10-04 13:08:13 -04:00
Goober5000andClaude Opus 5.5 dd77f7afbf break submodel parent cycles when reading SOBJ chunks
A subobject that named itself, or one of its own descendants, as its parent
made the depth walk loop forever, and would have done the same to every
later walk up the hierarchy.  Since each SOBJ chunk adds only one link, any
cycle must lead back to the subobject being read, so detect that during the
walk and treat the subobject as parentless.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-03 23:48:51 -04:00
Goober5000 1db8c586c5 Merge pull request #7813 from Goober5000/fix/bay_path_speed_ramp
Fix the fighterbay speed ramp on detoured departure paths
2026-10-02 21:42:03 -04:00
The Force fc626df89b Fix #7815 2026-10-02 20:52:51 +01:00
The Force d4b868daa0 Match Z Axis spinner limits with other axis 2026-10-02 20:30:16 +01:00
Goober5000andClaude Opus 5.5 b0d8862f18 Fix the fighterbay speed ramp on detoured departure paths
When a ship flying into a fighterbay has to route around the carrier,
create_model_path puts tangent and bisect points ahead of the bay path.
The departure speed ramp treated the first point of the AI path as the
start of the bay path: it started once the ship passed that point and
measured the path length from there.  On a detoured path the ramp
therefore began slowing the ship while it was still flying around the
hull, and the ship entered the bay slower than intended.  The ramp
fraction was also not clamped, so the target speed could exceed max
speed.

Under the new ai_profiles flag "$fix fighterbay speed ramp:"
(Fix_bay_speed_ramp), the departure ramp starts at the first point of
the bay path itself and the ramp fraction is clamped to [0, 1].  The
flag is enabled by default for mods targeting 26.2.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
2026-10-01 21:16:46 -04:00
Goober5000 a93c808df3 Merge pull request #7814 from Goober5000/fix/model_path_refresh
Fix two issues with refreshing model paths in place
2026-10-01 21:13:06 -04:00
Goober5000andClaude Fable 5.1 048310ca25 log game settings after all tables have been parsed
Settings in game_settings.tbl were logged as they were parsed, so a setting was only logged if it appeared in a table.  A setting enabled through $Target Version was never logged, and a setting that appeared in more than one table was logged once per table, possibly with different values.

The settings are now logged by mod_table_log_settings, which runs at the end of mod_table_init.  The version-gated settings that previously had no log line now have one: framerate independent turning, scripted velocity, asteroid bounding box checks, the expensive turret target check, skybox depth consistency, zero-radius explosions, and insignias as decals.

Messages that describe the table text itself, such as deprecated options, removed file extensions, and out-of-range values, are still logged at parse time.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-01 20:05:09 -04:00
Goober5000 80652b13ea Merge pull request #7807 from Goober5000/fix/moment_of_inertia
fix the collision moment of inertia rotation
2026-10-01 20:02:11 -04:00
Goober5000andClaude Fable 5.1 d2f2374f5c fix the collision moment of inertia rotation, and flag the density fix
get_I_inv converts an object's inverse moment of inertia tensor from body to world coordinates for collision physics.  It was written as though vm_matrix_x_matrix(dest, a, b) computes a * b, but that function computes b * a, so the result was (R)(I_inv_body)(Rt) rather than the (Rt)(I_inv_body)(R) stated in the comment.  The collision response therefore used the tensor of an object facing the inverse of its actual orientation.  This dates back to retail.

The result is only correct for an isotropic tensor or an unrotated object.  Every retail ship has an anisotropic tensor, with a median principal-axis ratio of about 2.5 and as high as 13 for capital ships, so the spin imparted by a collision was typically about the wrong axis.

Since this changes collision behavior for every mod, the fix is controlled by the new game_settings.tbl option "$Fix collision moment of inertia:", which defaults to true for mods targeting 26.2 or later.  The retail path is otherwise unchanged.

PR #2575 fixed a related bug in physics_ship_init: retail scaled the inverse moment of inertia by $Density rather than by its inverse, so a denser ship was easier to rotate rather than harder.  That fix was merged without a flag.  For consistency it is now controlled by "$Fix density moment of inertia:", which defaults to true for mods targeting 21.0 or later.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-01 16:59:38 -04:00
Goober5000 89dedeb08e Merge pull request #7812 from Goober5000/fix/bay_path_name
Fix bay path speed lookup reading paths[-1] on detoured paths
2026-10-01 14:54:57 -04:00
Goober5000andClaude Opus 5.5 da08ae5b6e Fix bay path speed lookup reading paths[-1] on detoured paths
set_accel_for_docking took the model path from the first point of the
AI path. When a departing ship has to route around the carrier,
create_model_path prepends detour points whose path_num is -1, so the
path name was read from pm->paths[-1]. That usually yields junk and a
missed metadata lookup, but crashes when the memory before the paths
array is unmapped. Use aip->mp_index, which both bay path builders set
to the model path index.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
2026-10-01 13:49:26 -04:00
Goober5000andClaude Opus 5.5 6fb9312982 Fix two issues with refreshing model paths in place
When the ship a path leads into moves, modify_model_path_points
re-globalizes the path's model points in place.

It always rewrote nverts points, but create_model_exit_path can build
a path with fewer, in which case the rewrite would run past the end of
the path into Path_points entries that may belong to other ships.  No
current caller builds such a path, but the rewrite now covers only the
points the path actually holds.

A subsystem path gives its second-to-last point a random offset when
created, but the refresh did not pass that point back in, so the
offset was lost the first time the target moved.  Record the
randomized vertex in ai_info and, under the new ai_profiles flag
"$fix subsystem path randomization on refresh:"
(Fix_model_path_refresh_randomization), reapply it on refresh.  The
flag is enabled by default for mods targeting 26.2.

Also rename a parameter in `get_base_path_info` (which is currently
unused) for clarity.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
2026-10-01 13:47:07 -04:00
wookieejedi c23c69a538 Fix primary thruster glows rendering through geometry (#7805) 2026-10-01 11:18:58 -04:00
Goober5000 b37c6bcd93 Merge pull request #7811 from Goober5000/fix/subsys_path_bounds
fix off-by-one in the subsystem path bounds check
2026-10-01 10:53:00 -04:00
Goober5000 69e5017050 Merge pull request #7803 from Goober5000/fix/misc_loadout
two loadout-related fixes for FRED and QtFRED
2026-09-30 22:08:19 -04:00
Goober5000 976edb1b0f Merge pull request #7810 from Goober5000/fix/destroyed_submodels
handle submodel repair/destruction consistently
2026-09-30 22:07:18 -04:00
Goober5000 4a633c86cb fix off-by-one in the subsystem path bounds check
Fix indexing of `pm->paths` in `ship_return_subsys_path_normal`.
2026-09-30 22:06:22 -04:00
Goober5000andClaude Opus 5.5 f7eb2ca419 restore subsystem sounds when a subsystem is repaired
A subsystem's looping sounds are now switched by ship_update_subsys_sounds, split out of ship_recalc_subsys_strength, which the support-ship and automatic repair paths now also call when a subsystem comes back from zero.  Previously those paths left the subsystem playing its dead sound.

Both the recalc and do_subobj_destroyed_stuff also cleared the dead-sound flag immediately after assigning the dead sound, so a revived subsystem never stopped it and a destroyed one gained another copy on each recalc.  They now set the flag.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
2026-09-29 02:58:50 -04:00
Goober5000andClaude Fable 5.1 112ca2fc4b handle submodel repair/destruction consistently
When a destroyed subsystem was repaired, its submodels were only restored if the repair was done through a SEXP.  A subsystem repaired by a support ship, by automatic repair, by a script, or through the debug console regained its hitpoints but not its submodels, so a ship could have a working turret with no model.  Any -destroyed form also remained visible.

All of these now call restore_subsystem_submodels, which brings back the subsystem's submodels and hides the -destroyed submodel linked to a special-point subsystem.  The -destroyed form of an ordinary submodel is hidden when the submodels are next replicated.  The SEXP code uses the same function, with no change in its behavior.

Destruction outside do_subobj_destroyed_stuff now goes through a new blow_off_subsystem_submodels function, which do_subobj_destroyed_stuff also uses.  It blows off both of the subsystem's submodels and shows any linked -destroyed submodel, unless the subsystem has "no disappear".  This handles the cases where a subsystem appears destroyed without going through do_subobj_destroyed_stuff:
- the subsystem is destroyed at the start of the mission
- the subsystem was destroyed in the previous mission of a red-alert sequence
- the ship changes class while the subsystem's submodels are blown off
Previously the first two blew off only the first submodel, ignored "no disappear" for it, and never showed a linked -destroyed submodel; the third did nothing.  For red alert, subsystems with no hitpoints at all are not treated as destroyed.  For a class change, the visible state is carried over rather than inferred from hitpoints, so a subsystem set to zero without being blown up, or repaired without its submodels, keeps its appearance.

Red alert also restores the submodels of a subsystem that the mission file starts destroyed if the previous mission's data gives it hitpoints.  Ship-copy-damage now shows or hides the -destroyed submodel linked to a special-point subsystem to match the copied state.

As before, the link between special-point subsystems and -destroyed submodels requires "$Link special-point subsystems to -destroyed submodels:" in game_settings.tbl.

The multiplayer hitpoint-update path is not changed.  A client receives subsystem hitpoints separately from the notification that a subsystem was destroyed, so a hitpoint update sent just before the destruction could arrive after it and wrongly restore the submodels.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
2026-09-29 02:58:02 -04:00
Goober5000andClaude Opus 5 e65145a6d6 fix qtFRED's team loadout combo in single-team multiplayer missions
qtFRED's team loadout dialog enabled its team combo and its copy-to-other-teams button on MISSION_TYPE_MULTI, which encompasses more than just Team-versus-Team.  Gate both on Num_teams, which is what FRED's own loadout editor uses, and build the team list from the teams the mission actually has.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
2026-09-25 03:22:23 -04:00
Goober5000andClaude Opus 5 a159e67a10 share the loadout usage-list code between the two editors
Add generate_ship_usage_list_team alongside the weaponry twin that already existed in missioneditor/common, so both loadout editors can drop their TVT/non-TVT branches and simply iterate Num_teams.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
2026-09-25 03:22:23 -04:00
Goober5000andClaude Opus 5.5 8c8620ef0f don't look up the operator of an empty SEXP list
An empty list such as `( )` leaves get_sexp() with no start node, but it still passed that -1 to get_operator_const() for the ai-chase-any-except compatibility check and, outside FRED, for preloading.  This tripped the out-of-range assertion in debug builds.  Return the empty list directly instead.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
2026-09-25 02:28:24 -04:00
Goober5000 33c4bb0222 make is_special_sender static 2026-09-25 01:52:15 -04:00
Goober5000andClaude Opus 5 fd40df069c validate counts and indices when reading POF chunks
Element counts were taken straight from the file, so a corrupt POF could
supply a negative count (stored alongside a null array) or one larger than
the fixed-size array it feeds.  Add pof_read_count() to floor every count at
zero and clamp it to the destination's capacity, reading past the discarded
entries where more data follows in the same chunk.

The upper bounds that were checked at all were checked with debug-only
Asserts, or with an Error() that returns if the user picks Debug, so the
overflowing loop after it was reachable in both cases.

Also validate the submodel indices in SOBJ and TGUN/TMIS, the vertex indices
in insignia faces, and bounds-check the split plane array before writing to
it rather than after.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
2026-09-25 01:52:15 -04:00
Goober5000andClaude Opus 5 c10df878ba remove Current_filename_sub and tidy up stop_parse
Current_filename_sub dates back to the use of *_ex() parsing functions for parsing .eff files, which were superseded by the Bookmarks feature.  Today it is entirely redundant to Current_filename.  A read that throws now leaves Current_filename naming the file that failed, rather than the file parsed before it, as it should.

Also clear Current_filename in stop_parse, so that a warning issued between the end of one parse and the start of the next no longer blames the last file parsed, and null out Mp there so it stops dangling into the freed parse buffer.  Also delete the vestigial Mp_save.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
2026-09-25 01:52:15 -04:00
John Fernandez 9b2fedbf65 Move Items to a grid layout for easy sizing 2026-08-17 14:18:05 -04:00
244 changed files with 6215 additions and 2095 deletions

No files matched your search

+8 -5
View File
@@ -102,7 +102,7 @@ jobs:
strategy:
matrix:
configuration: [FastDebug, Release]
os: [windows-2022, windows-11-arm]
os: [windows-2025, windows-11-arm]
arch: [Win32, x64, ARM64]
simd: [SSE2, ""]
exclude:
@@ -111,8 +111,8 @@ jobs:
- arch: x64
os: windows-11-arm
- arch: ARM64
os: windows-2022
- os: windows-2022
os: windows-2025
- os: windows-2025
simd: ""
- os: windows-11-arm
simd: SSE2
@@ -126,6 +126,9 @@ jobs:
- name: Set workspace as safe
# This appears to be broken in current actions, so do it manually
run: git config --global --add safe.directory "$GITHUB_WORKSPACE"
- name: Install v143 ATL/MFC
shell: pwsh
run: ci/windows/install_v143_atlmfc.ps1
# Tree A: static CRT (/MT) for FSO + FRED2 (no Qt dependency needed)
- name: Configure CMake (static CRT /MT - FSO + FRED2)
env:
@@ -139,7 +142,7 @@ jobs:
cmake -DCMAKE_INSTALL_PREFIX="$(pwd)/install" -DFSO_USE_SPEECH=ON \
-DFSO_USE_VOICEREC=ON -DFORCED_SIMD_INSTRUCTIONS="$SIMD" \
-DFSO_BUILD_TESTS=ON -DFSO_INSTALL_DEBUG_FILES=ON -A "$ARCHITECTURE" \
-DFSO_BUILD_QTFRED=OFF -G "Visual Studio 17 2022" -T "v143" \
-DFSO_BUILD_QTFRED=OFF -G "Visual Studio 18 2026" -T "v143" \
-DCMAKE_BUILD_TYPE=$CONFIGURATION ..
- name: Compile (static CRT)
working-directory: ./build-mt
@@ -168,7 +171,7 @@ jobs:
-DFSO_USE_VOICEREC=ON -DFORCED_SIMD_INSTRUCTIONS="$SIMD" \
-DFSO_BUILD_TESTS=OFF -DFSO_INSTALL_DEBUG_FILES=ON -A "$ARCHITECTURE" \
-DFSO_BUILD_QTFRED=ON -DFSO_BUILD_FRED2=OFF -DQT_USE_PRECOMPILED=ON \
-G "Visual Studio 17 2022" -T "v143" -DCMAKE_BUILD_TYPE=$CONFIGURATION ..
-G "Visual Studio 18 2026" -T "v143" -DCMAKE_BUILD_TYPE=$CONFIGURATION ..
- name: Compile (dynamic CRT)
if: matrix.arch != 'Win32'
working-directory: ./build-md
+8 -5
View File
@@ -152,7 +152,7 @@ jobs:
strategy:
matrix:
configuration: [FastDebug, Release]
os: [windows-2022, windows-11-arm]
os: [windows-2025, windows-11-arm]
arch: [Win32, x64, ARM64]
simd: [SSE2, AVX, ""]
exclude:
@@ -161,8 +161,8 @@ jobs:
- arch: x64
os: windows-11-arm
- arch: ARM64
os: windows-2022
- os: windows-2022
os: windows-2025
- os: windows-2025
simd: ""
- os: windows-11-arm
simd: SSE2
@@ -181,6 +181,9 @@ jobs:
- name: Set workspace as safe
# This appears to be broken in current actions, so do it manually
run: git config --global --add safe.directory "$GITHUB_WORKSPACE"
- name: Install v143 ATL/MFC
shell: pwsh
run: ci/windows/install_v143_atlmfc.ps1
# Tree A: static CRT (/MT) for FSO + FRED2 (no Qt dependency needed)
- name: Configure CMake (static CRT /MT - FSO + FRED2)
env:
@@ -194,7 +197,7 @@ jobs:
cmake -DCMAKE_INSTALL_PREFIX="$(pwd)/install" -DFSO_USE_SPEECH=ON \
-DFSO_USE_VOICEREC=ON -DFORCED_SIMD_INSTRUCTIONS="$SIMD" \
-DFSO_BUILD_TESTS=ON -DFSO_INSTALL_DEBUG_FILES=ON -A "$ARCHITECTURE" \
-DFSO_BUILD_QTFRED=OFF -G "Visual Studio 17 2022" -T "v143" \
-DFSO_BUILD_QTFRED=OFF -G "Visual Studio 18 2026" -T "v143" \
-DCMAKE_BUILD_TYPE=$CONFIGURATION ..
- name: Compile (static CRT)
working-directory: ./build-mt
@@ -223,7 +226,7 @@ jobs:
-DFSO_USE_VOICEREC=ON -DFORCED_SIMD_INSTRUCTIONS="$SIMD" \
-DFSO_BUILD_TESTS=OFF -DFSO_INSTALL_DEBUG_FILES=ON -A "$ARCHITECTURE" \
-DFSO_BUILD_QTFRED=ON -DFSO_BUILD_FRED2=OFF -DQT_USE_PRECOMPILED=ON \
-G "Visual Studio 17 2022" -T "v143" -DCMAKE_BUILD_TYPE=$CONFIGURATION ..
-G "Visual Studio 18 2026" -T "v143" -DCMAKE_BUILD_TYPE=$CONFIGURATION ..
- name: Compile (dynamic CRT)
if: matrix.arch != 'Win32'
working-directory: ./build-md
+7 -4
View File
@@ -109,7 +109,7 @@ jobs:
strategy:
matrix:
configuration: [FastDebug, Release]
os: [windows-2022, windows-11-arm]
os: [windows-2025, windows-11-arm]
arch: [Win32, x64, ARM64]
simd: [SSE2, ""]
exclude:
@@ -118,8 +118,8 @@ jobs:
- arch: x64
os: windows-11-arm
- arch: ARM64
os: windows-2022
- os: windows-2022
os: windows-2025
- os: windows-2025
simd: ""
- os: windows-11-arm
simd: SSE2
@@ -139,6 +139,9 @@ jobs:
[[ "${{ github.ref }}" =~ ^refs\/heads\/test\/(.*)$ ]]
# Override the revision string so that the builds are named correctly
echo "set(FSO_VERSION_REVISION_STR ${BASH_REMATCH[1]})" > "version_override.cmake"
- name: Install v143 ATL/MFC
shell: pwsh
run: ci/windows/install_v143_atlmfc.ps1
- name: Configure CMake
env:
CONFIGURATION: ${{ matrix.configuration }}
@@ -151,7 +154,7 @@ jobs:
cmake -DCMAKE_INSTALL_PREFIX="$(pwd)/install" -DFSO_USE_SPEECH=ON \
-DFSO_USE_VOICEREC=ON -DFORCED_SIMD_INSTRUCTIONS="$SIMD" \
-DFSO_BUILD_TESTS=ON -DFSO_INSTALL_DEBUG_FILES=ON -A "$ARCHITECTURE" \
-DQT_USE_PRECOMPILED=ON -G "Visual Studio 17 2022" -T "v143" \
-DQT_USE_PRECOMPILED=ON -G "Visual Studio 18 2026" -T "v143" \
-DCMAKE_BUILD_TYPE=$CONFIGURATION ..
- name: Compile
working-directory: ./build
+6 -2
View File
@@ -78,7 +78,7 @@ jobs:
compiler: [MSVC]
arch: [Win32, x64]
name: Windows
runs-on: windows-2022
runs-on: windows-2025
steps:
- name: Prepare Environment
run: choco install ninja
@@ -89,6 +89,10 @@ jobs:
- name: Set workspace as safe
# This appears to be broken in current actions, so do it manually
run: git config --global --add safe.directory "$GITHUB_WORKSPACE"
- name: Install v143 ATL/MFC
if: matrix.compiler == 'MSVC'
shell: pwsh
run: ci/windows/install_v143_atlmfc.ps1
- name: Configure CMake
env:
CONFIGURATION: ${{ matrix.configuration }}
@@ -104,7 +108,7 @@ jobs:
-DFSO_BUILD_FRED2=OFF -DQT_USE_PRECOMPILED=ON -DCMAKE_BUILD_TYPE=$CONFIGURATION -G "Ninja" ..
else
cmake -DFSO_USE_SPEECH=ON -DFSO_FATAL_WARNINGS=ON -DFSO_USE_VOICEREC=OFF -DFSO_BUILD_TESTS=ON \
-DFORCED_SIMD_INSTRUCTIONS=SSE2 -G "Visual Studio 17 2022" -T "v143" -A "$ARCHITECTURE" \
-DFORCED_SIMD_INSTRUCTIONS=SSE2 -G "Visual Studio 18 2026" -T "v143" -A "$ARCHITECTURE" \
-DQT_USE_PRECOMPILED=ON -DCMAKE_BUILD_TYPE=$CONFIGURATION ..
fi
- name: Compile
+34 -15
View File
@@ -29,24 +29,43 @@ ENDIF(NOT DEFINED CMAKE_INSTALL_PREFIX)
SET(TARGET_COPY_FILES "" CACHE INTERNAL "" FORCE)
SET(TARGET_COPY_DIRS "" CACHE INTERNAL "" FORCE)
### Define options and editable variables in the section below:
# This is only needed for single configuration generators so don't annoy others
if(NOT CMAKE_BUILD_TYPE AND NOT DEFINED CMAKE_CONFIGURATION_TYPES)
set(CMAKE_BUILD_TYPE "Release" CACHE STRING "Default build configuration used." FORCE)
message(WARNING "CMAKE_BUILD_TYPE was not specified, defaulting to Release configuration.")
endif(NOT CMAKE_BUILD_TYPE AND NOT DEFINED CMAKE_CONFIGURATION_TYPES)
get_property(is_multi_config GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG)
# Override standard CMake configurations
set(CMAKE_CONFIGURATION_TYPES Debug Release FastDebug CACHE STRING "Available configurations" FORCE)
set(FSO_BUILD_TYPES Debug Release FastDebug)
if(is_multi_config)
set(CMAKE_CONFIGURATION_TYPES "${FSO_BUILD_TYPES}" CACHE STRING "Configuration types" FORCE)
elseif(NOT CMAKE_BUILD_TYPE)
set(CMAKE_BUILD_TYPE "Release" CACHE STRING "Default build configuration used." FORCE)
message(WARNING "CMAKE_BUILD_TYPE was not specified, defaulting to Release configuration.")
set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS "${FSO_BUILD_TYPES}")
endif()
set(FSO_LANGUAGES C CXX)
if(APPLE)
set(FSO_LANGUAGES ${FSO_LANGUAGES} OBJC OBJCXX)
# 10.15 for c++17 std::filesystem
# NOTE: this must be set *before* the project() call to work properly!!
set(CMAKE_OSX_DEPLOYMENT_TARGET "10.15" CACHE STRING "macOS deployment target")
# fix all targets in scheme (so it's not just qtfred)
set(CMAKE_XCODE_GENERATE_SCHEME TRUE)
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.")
@@ -99,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)
@@ -128,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()
@@ -196,11 +215,11 @@ include(clang-tidy)
include(CheckIPOSupported)
check_ipo_supported(RESULT LTO_AVAILABLE)
if (CMAKE_BUILD_TYPE STREQUAL "Release" AND FSO_USE_LTO AND LTO_AVAILABLE)
if (FSO_USE_LTO AND LTO_AVAILABLE)
set(CMAKE_POLICY_DEFAULT_CMP0069 NEW)
message("Enabled LTO")
set(CMAKE_INTERPROCEDURAL_OPTIMIZATION TRUE)
endif (CMAKE_BUILD_TYPE STREQUAL "Release" AND FSO_USE_LTO AND LTO_AVAILABLE)
message("Enabled LTO for Release builds")
set(CMAKE_INTERPROCEDURAL_OPTIMIZATION_RELEASE TRUE)
endif (FSO_USE_LTO AND LTO_AVAILABLE)
# This includes source code for some tools, either used in the build or also for something else
ADD_SUBDIRECTORY(tools)
+1 -1
View File
@@ -1,3 +1,3 @@
Mako==1.3.12
Mako==1.4.2
requests==2.33.0
semantic-version==2.8.5
+34
View File
@@ -0,0 +1,34 @@
# Installs ATL and MFC for the v143 toolset, which the Visual Studio 2026 runner images
# only ship for the latest toolset. ATL is needed by SAPI speech (sphelper.h) and MFC by FRED2.
$ErrorActionPreference = 'Stop'
$msvcVersion = '14.44'
$component = 'Microsoft.VisualStudio.Component.VC.14.44.17.14'
$suffix = if ($env:RUNNER_ARCH -eq 'ARM64') { '.ARM64' } else { '' }
$installerDir = "${env:ProgramFiles(x86)}\Microsoft Visual Studio\Installer"
$installPath = & "$installerDir\vswhere.exe" -version '[18.0,19.0)' -latest -property installationPath
if (-not $installPath) {
Write-Error 'Visual Studio 2026 was not found'
}
$arguments = @(
'/c', 'vs_installer.exe', 'modify',
'--installPath', "`"$installPath`"",
'--add', "$component.ATL$suffix",
'--add', "$component.MFC$suffix",
'--quiet', '--norestart', '--nocache'
)
$process = Start-Process -FilePath cmd.exe -ArgumentList $arguments -WorkingDirectory $installerDir -Wait -PassThru -NoNewWindow
# 3010 = success, reboot required
if ($process.ExitCode -notin 0, 3010) {
Write-Error "vs_installer exited with code $($process.ExitCode)"
}
# fail here rather than with a confusing compile error if the components didn't install
foreach ($header in 'atlbase.h', 'afxwin.h') {
if (-not (Test-Path "$installPath\VC\Tools\MSVC\$msvcVersion.*\atlmfc\include\$header")) {
Write-Error "$header for MSVC $msvcVersion is missing after install"
}
}
-4
View File
@@ -11,10 +11,6 @@ endif()
target_link_libraries(platform INTERFACE ${COCOA_LIBRARY})
# To support C++17 we need to target at least 10.13. maybe 10.14 for some rare features
# bump to 10.15 for c++17 std::filesystem usage in qtFRED
SET(CMAKE_OSX_DEPLOYMENT_TARGET "10.15")
SET(CMAKE_SKIP_RPATH FALSE)
SET(CMAKE_BUILD_WITH_INSTALL_RPATH TRUE)
SET(CMAKE_INSTALL_RPATH @loader_path/../Frameworks/)
+11 -4
View File
@@ -13,6 +13,13 @@ option(MSVC_RELEASE_DEBUGGING "Set options that allow to debug release builds" O
MARK_AS_ADVANCED(FORCE MSVC_USE_RUNTIME_DLL)
MARK_AS_ADVANCED(FORCE MSVC_RELEASE_DEBUGGING)
# Qt is only available as DLL-runtime builds, so qtFRED forces the DLL runtime for the whole tree
if(MSVC_USE_RUNTIME_DLL OR FSO_BUILD_QTFRED)
set(FSO_MSVC_RUNTIME_DLL ON)
else()
set(FSO_MSVC_RUNTIME_DLL OFF)
endif()
# These are the warnings we disable
set(WARNING_FLAGS
/wd4100 # unreferenced formal parameters
@@ -71,12 +78,12 @@ if (MSVC_RELEASE_DEBUGGING)
endif()
endif()
# This should be kept in sync with the corresponding IF() in fred2/CMakeLists.txt
IF(MSVC_USE_RUNTIME_DLL OR FSO_BUILD_QTFRED)
set(CMAKE_MSVC_RUNTIME_LIBRARY "MultiThreaded$<$<NOT:$<CONFIG:Release>>:Debug>DLL")
# The debug runtime is not redistributable (statically linked or not), so only Debug may use it
IF(FSO_MSVC_RUNTIME_DLL)
set(CMAKE_MSVC_RUNTIME_LIBRARY "MultiThreaded$<$<CONFIG:Debug>:Debug>DLL")
add_compile_definitions(_AFXDLL)
ELSE()
set(CMAKE_MSVC_RUNTIME_LIBRARY "MultiThreaded$<$<NOT:$<CONFIG:Release>>:Debug>")
set(CMAKE_MSVC_RUNTIME_LIBRARY "MultiThreaded$<$<CONFIG:Debug>:Debug>")
ENDIF()
# Debug
+7 -10
View File
@@ -3,7 +3,13 @@
add_library(compiler INTERFACE)
target_compile_definitions(compiler INTERFACE "$<$<CONFIG:Release>:NDEBUG>;$<$<CONFIG:Debug>:_DEBUG>;$<$<CONFIG:FastDebug>:_DEBUG>")
set(FSO_CONFIG_DEFINITIONS "$<$<CONFIG:Release>:NDEBUG>" "$<$<CONFIG:Debug>:_DEBUG>")
if(NOT MSVC)
# On MSVC the debug runtime flags (/MTd, /MDd) define _DEBUG themselves, and a FastDebug
# build on the release runtime must not have it
list(APPEND FSO_CONFIG_DEFINITIONS "$<$<CONFIG:FastDebug>:_DEBUG>")
endif()
target_compile_definitions(compiler INTERFACE ${FSO_CONFIG_DEFINITIONS})
set(FORCED_NATIVE_SIMD_INSTRUCTIONS ON CACHE BOOL "Override instruction set detection and compile with the maximum possible instructions for the current system.")
IF(IS_X86)
@@ -74,12 +80,3 @@ set(CMAKE_C_FLAGS_FASTDEBUG "${CMAKE_C_FLAGS_RELEASE}")
set(CMAKE_CXX_FLAGS_FASTDEBUG "${CMAKE_CXX_FLAGS_RELEASE}")
set(CMAKE_EXE_LINKER_FLAGS_FASTDEBUG "${CMAKE_EXE_LINKER_FLAGS_RELEASE}")
set(CMAKE_STATIC_LINKER_FLAGS_FASTDEBUG "${CMAKE_STATIC_LINKER_FLAGS_RELEASE}")
if (MSVC)
# Adjust runtime library
string(REPLACE "/MD" "/MDd" CMAKE_C_FLAGS_FASTDEBUG "${CMAKE_C_FLAGS_FASTDEBUG}")
string(REPLACE "/MT" "/MTd" CMAKE_C_FLAGS_FASTDEBUG "${CMAKE_C_FLAGS_FASTDEBUG}")
string(REPLACE "/MD" "/MDd" CMAKE_CXX_FLAGS_FASTDEBUG "${CMAKE_CXX_FLAGS_FASTDEBUG}")
string(REPLACE "/MT" "/MTd" CMAKE_CXX_FLAGS_FASTDEBUG "${CMAKE_CXX_FLAGS_FASTDEBUG}")
endif()
+11
View File
@@ -241,6 +241,17 @@ function(suppress_file_warnings)
endif()
endfunction(suppress_file_warnings)
# Suppress no symbols warnings in static libraries (on Xcode)
function(suppress_no_symbol_warnings _target)
if(XCODE)
get_target_property(target_type ${_target} TYPE)
if(target_type STREQUAL "STATIC_LIBRARY")
set_property(TARGET ${_target} APPEND PROPERTY
STATIC_LIBRARY_OPTIONS "-no_warning_for_no_symbols"
)
endif()
endif()
endfunction(suppress_no_symbol_warnings)
function(list_target_dependencies _target _out_var)
set(out_list)
+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
+3
View File
@@ -130,5 +130,8 @@ set_target_properties(code PROPERTIES XCODE_ATTRIBUTE_COPY_PHASE_STRIP[variant=D
set_target_properties(code PROPERTIES XCODE_ATTRIBUTE_STRIP_INSTALLED_PRODUCT[variant=Debug] "NO")
set_target_properties(code PROPERTIES XCODE_ATTRIBUTE_GCC_SYMBOLS_PRIVATE_EXTERN[variant=Debug] "NO")
# Prevent warnings on files with no symbols on Mac
suppress_no_symbol_warnings(code)
# Prevent GCC warnings in third-party BitOp... see GitHub #4366
suppress_file_warnings(scripting/lua/bitop/bit.c)
+6 -2
View File
@@ -295,6 +295,7 @@ typedef struct ai_info {
vec3d path_create_pos; // Object's position at time of global path creation.
matrix path_create_orient; // Object's orientation at time of global path creation.
int mp_index; // Model path index. Index in polymodel:model_paths
int mp_randomized_vert; // Vertex of the model path that was given a random offset, or -1
fix path_next_check_time; // Last time checked to see if would collide with model.
int path_goal_dist; // minimum distance to first path point to consider path reached
int path_subsystem_next_check; // timestamp to next check if subsystem is still visible
@@ -534,6 +535,7 @@ extern void ai_ignore_wing(object *ignorer, int wingnum);
extern void ai_dock_with_object(object *docker, int docker_index, object *dockee, int dockee_index, int dock_type);
extern void ai_stay_still(object *still_objp, vec3d *view_pos);
extern void ai_do_default_behavior(object *obj);
extern void ai_reset_order_state(ai_info *aip);
extern void ai_start_waypoints(object *objp, int wl_index, int wp_flags, int start_index, bool force = false);
extern void ai_ship_hit(object *objp_ship, object *hit_objp, const vec3d *hit_normal);
extern void ai_ship_destroy(int shipnum);
@@ -596,7 +598,9 @@ extern int ai_maybe_fire_afterburner(object *objp, ai_info *aip);
extern void set_predicted_enemy_pos(vec3d *predicted_enemy_pos, object *pobjp, vec3d *enemy_pos, vec3d *enemy_vel, ai_info *aip);
extern int is_instructor(object *objp);
extern int find_enemy(int objnum, float range, int max_attackers, int ship_info_index = -1, int class_type = -1);
extern int find_enemy(int objnum, float range, int max_attackers, bool for_pursuit, int ship_info_index = -1, int class_type = -1);
extern bool ai_targets_for_pursuit(const ai_info *aip);
extern bool ai_declines_pursuit(const object *attacker_objp, const object *target_objp);
float ai_get_weapon_speed(const ship_weapon *swp);
void set_predicted_enemy_pos_turret(vec3d *predicted_enemy_pos, const vec3d *gun_pos, const object *pobjp, const vec3d *enemy_pos, const vec3d *enemy_vel, float weapon_speed, float time_enemy_in_range);
@@ -617,7 +621,7 @@ extern float dock_orient_and_approach(object *docker_objp, int docker_index, obj
void ai_set_mode_warp_out(object *objp, ai_info *aip);
// prototyped by Goober5000
int get_nearest_objnum(int objnum, int enemy_team_mask, int enemy_wing, float range, int max_attackers, int ship_info_index, int class_type = -1);
int get_nearest_objnum(int objnum, int enemy_team_mask, int enemy_wing, float range, int max_attackers, bool for_pursuit, int ship_info_index, int class_type = -1);
// moved to header file by Goober5000
void ai_announce_ship_dying(object *dying_objp);
+10 -2
View File
@@ -178,8 +178,10 @@ namespace AI {
Fix_standard_strafe,
Fix_ai_target_recovery, // a) strafing ships that lose their target re-process their orders, like chasing ships already do
// b) ships parked in AIM_NONE while holding a standing chase order (attack-any etc.) re-process their orders;
// c) target selection skips ships outside the attacker's actively-pursues list, and self-chosen targets
// outside that list are dropped rather than held forever
// c) target selection for pursuit (chasing, guard reactions, retaliation) skips ships outside the attacker's
// actively-pursues list; targets held while not chasing are unaffected, and explicitly ordered targets are
// still assigned but not chased or strafed;
// d) enemy_wing is reset when a new order takes over (shared with Fix_stale_ai_order_state)
Standard_strafe_used_more,
Unify_usage_ai_shield_manage_delay,
Fix_AI_shield_management_bug,
@@ -200,6 +202,12 @@ namespace AI {
// b) completion no longer requires moving 0.1m in a single frame (framerate-dependent)
Fix_shockwave_expire_before_do_damage, // shockwaves whose lifetime is shorter than one frame apply their area damage at least once before expiring
Fix_small_ai_recover_after_engines_repaired, // ensure small ship AI can switch back to useful AI modes if engines get repaired
Fix_model_path_refresh_randomization, // a subsystem path keeps its randomized point when the path is refreshed because the target moved
Fix_bay_speed_ramp, // the fighterbay speed ramp covers only the bay path itself, not points that route a departing ship around the carrier, and never exceeds max speed
Fix_stale_ai_order_state, // a) enemy_wing, guard_*, previous_mode, resume_goal_time, and the targeted subsystem (with its path) are reset when a new order takes over (enemy_wing is also reset under Fix_ai_target_recovery);
// b) a dynamic chase saves previous_mode/previous_submode; it and strafe entry leave them alone during an ongoing dynamic goal (evading a weapon still overwrites them, which is harmless since the evade resume re-processes orders);
// c) resuming guard duty discards any subsystem target and path left over from the interruption;
// d) a ship ordered to guard itself, or to guard a wing it leads (including one that becomes leader mid-mission), treats that goal as unachievable instead of holding it forever
NUM_VALUES
};
+16 -7
View File
@@ -140,7 +140,7 @@ void parse_ai_profiles_tbl(const char *filename)
// don't create multiple profiles with the same name
if (previous_profile != NULL)
{
Warning(LOCATION, "An ai profile named '%s' already exists! The new one will not be created.\n", profile_name);
Warning(LOCATION, "An AI profile named '%s' already exists! The new one will not be created.\n", profile_name);
}
else
{
@@ -412,7 +412,7 @@ void parse_ai_profiles_tbl(const char *filename)
set_flag(profile, "$disable player secondary doublefire:", AI::Profile_Flags::Disable_player_secondary_doublefire);
set_flag(profile, "$disable ai secondary doublefire:", AI::Profile_Flags::Disable_ai_secondary_doublefire);
set_flag(profile, "$disable AI secondary doublefire:", AI::Profile_Flags::Disable_ai_secondary_doublefire);
set_flag(profile, "$disable weapon damage scaling:", AI::Profile_Flags::Disable_weapon_damage_scaling);
@@ -450,7 +450,7 @@ void parse_ai_profiles_tbl(const char *filename)
set_flag(profile, "$force beam turrets to use normal fov:", AI::Profile_Flags::Force_beam_turret_fov);
set_flag(profile, "$fix ai class bug:", AI::Profile_Flags::Fix_ai_class_bug);
set_flag(profile, "$fix AI class bug:", AI::Profile_Flags::Fix_ai_class_bug);
set_flag(profile, "$turrets ignore targets radius in range checks:", AI::Profile_Flags::Turrets_ignore_target_radius);
@@ -505,13 +505,13 @@ void parse_ai_profiles_tbl(const char *filename)
profile->ai_path_mode = j;
}
else {
Warning(LOCATION, "Invalid ai path mode '%s' specified", buf);
Warning(LOCATION, "Invalid AI path mode '%s' specified", buf);
}
}
set_flag(profile, "$no warp camera:", AI::Profile_Flags::No_warp_camera);
set_flag(profile, "$fix ai path order bug:", AI::Profile_Flags::Fix_ai_path_order_bug);
set_flag(profile, "$fix AI path order bug:", AI::Profile_Flags::Fix_ai_path_order_bug);
set_flag(profile, "$strict turret-tagged-only targeting:", AI::Profile_Flags::Strict_turret_tagged_only_targeting);
@@ -622,7 +622,7 @@ void parse_ai_profiles_tbl(const char *filename)
profile->ai_range_aware_secondary_select_mode = j;
}
else {
Warning(LOCATION, "Invalid ai secondary range awareness mode '%s' specified", buf);
Warning(LOCATION, "Invalid AI secondary range awareness mode '%s' specified", buf);
}
}
@@ -677,7 +677,7 @@ void parse_ai_profiles_tbl(const char *filename)
set_flag(profile, "$fix standard strafe:", AI::Profile_Flags::Fix_standard_strafe);
set_flag(profile, "$fix ai target recovery:", AI::Profile_Flags::Fix_ai_target_recovery);
set_flag(profile, "$fix AI target recovery:", AI::Profile_Flags::Fix_ai_target_recovery);
set_flag(profile, "$standard strafe used more:", AI::Profile_Flags::Standard_strafe_used_more);
@@ -767,6 +767,12 @@ void parse_ai_profiles_tbl(const char *filename)
set_flag(profile, "$fix fighter/bomber AI recovers after engines repaired:", AI::Profile_Flags::Fix_small_ai_recover_after_engines_repaired);
set_flag(profile, "$fix subsystem path randomization on refresh:", AI::Profile_Flags::Fix_model_path_refresh_randomization);
set_flag(profile, "$fix fighterbay speed ramp:", AI::Profile_Flags::Fix_bay_speed_ramp);
set_flag(profile, "$fix stale AI order state:", AI::Profile_Flags::Fix_stale_ai_order_state);
// end of options ----------------------------------------
// if we've been through once already and are at the same place, force a move
@@ -1001,5 +1007,8 @@ void ai_profile_t::reset()
flags.set(AI::Profile_Flags::Fix_small_ai_recover_after_engines_repaired);
flags.set(AI::Profile_Flags::Fix_standard_strafe);
flags.set(AI::Profile_Flags::Fix_ai_target_recovery);
flags.set(AI::Profile_Flags::Fix_model_path_refresh_randomization);
flags.set(AI::Profile_Flags::Fix_bay_speed_ramp);
flags.set(AI::Profile_Flags::Fix_stale_ai_order_state);
}
}
+22 -12
View File
@@ -66,6 +66,16 @@ static bool ai_big_strafe_maybe_retreat(const vec3d *target_pos);
extern void compute_desired_rvec(vec3d *rvec, const vec3d *goal_pos, const vec3d *cur_pos);
extern void big_ship_collide_recover_start(const object *objp, const object *big_objp, const vec3d *collision_normal);
// Record the mode being left as the one to return to, unless a dynamic goal is already in progress
// (in which case previous_mode still refers to the mode that goal interrupted).
static void ai_big_save_previous_mode(ai_info *aip)
{
if (The_mission.ai_profile->flags[AI::Profile_Flags::Fix_stale_ai_order_state] && aip->active_goal == AI_ACTIVE_GOAL_DYNAMIC)
return;
aip->previous_mode = aip->mode;
}
// Called by ai_big_pick_attack_point.
// Generates a random attack point.
@@ -499,7 +509,7 @@ bool ai_big_maybe_follow_subsys_path(bool do_dot_check)
// for now, only follow the path to the first point
if ( aip->path_cur < (aip->path_start+aip->path_length-1) ) {
if ( aip->goal_objnum != aip->target_objnum ) {
aip->previous_mode = aip->mode;
ai_big_save_previous_mode(aip);
aip->mode = AIM_NONE;
aip->submode = -1;
aip->submode_start_time = Missiontime;
@@ -706,7 +716,8 @@ void ai_big_chase_attack(ai_info *aip, ship_info *sip, vec3d *enemy_pos, float d
if (vm_vec_dot(&in_vec, &objp->orient.vec.fvec) > 0.0f) {
dist = vm_vec_normalize(&in_vec);
if ((dist < 200.0f) && (vm_vec_dot(&in_vec, &objp->orient.vec.fvec) > 0.95f)) {
if ((Objects[objp->parent].signature == objp->parent_sig) && (vm_vec_dist_quick(&objp->pos, &Objects[objp->parent].pos) < 300.0f)) {
if ((Objects[objp->parent].signature == objp->parent_sig) && (vm_vec_dist_quick(&objp->pos, &Objects[objp->parent].pos) < 300.0f)
&& !ai_declines_pursuit(Pl_objp, &Objects[objp->parent])) {
set_target_objnum(aip, objp->parent);
aip->submode = SM_ATTACK;
aip->submode_start_time = Missiontime;
@@ -727,7 +738,7 @@ void ai_big_chase_attack(ai_info *aip, ship_info *sip, vec3d *enemy_pos, float d
// including ai_profile flag and if enemy fighters are near
if (Pl_objp->phys_info.speed < The_mission.ai_profile->standard_strafe_when_below_speed &&
(The_mission.ai_profile->flags[AI::Profile_Flags::Standard_strafe_used_more] || ai_big_maybe_start_strafe(aip, sip))) {
aip->previous_mode = aip->mode;
ai_big_save_previous_mode(aip);
aip->mode = AIM_STRAFE;
aip->submode_parm0 = Missiontime; // use parm0 as time strafe mode entered (i.e. MODE start time)
ai_big_strafe_position();
@@ -736,7 +747,7 @@ void ai_big_chase_attack(ai_info *aip, ship_info *sip, vec3d *enemy_pos, float d
//Maybe enter glide strafe (check every 8 seconds, on a different schedule for each ship)
if ((sip->can_glide == true) && !(aip->ai_flags[AI::AI_Flags::Kamikaze]) && static_randf((Missiontime + static_rand(aip->shipnum)) >> 19) < aip->ai_glide_strafe_percent) {
aip->previous_mode = aip->mode;
ai_big_save_previous_mode(aip);
aip->mode = AIM_STRAFE;
aip->submode_parm0 = Missiontime; // use parm0 as time strafe mode entered (i.e. MODE start time)
aip->submode = AIS_STRAFE_GLIDE_ATTACK;
@@ -1008,7 +1019,7 @@ static void ai_big_maybe_fire_weapons(float dist_to_enemy, float dot_to_enemy)
// switch ai ship into chase mode
void ai_big_switch_to_chase_mode(ai_info *aip)
{
aip->previous_mode = aip->mode;
ai_big_save_previous_mode(aip);
aip->mode = AIM_CHASE;
aip->submode = SM_ATTACK;
aip->submode_start_time = Missiontime;
@@ -1108,7 +1119,7 @@ void ai_big_chase()
} else if (En_objp->flags[Object::Object_Flags::Protected]) { // If protected and we're not attacking a subsystem, stop attacking!
update_aspect_lock_information(aip, &vec_to_enemy, dist_to_enemy - En_objp->radius, En_objp->radius);
aip->target_objnum = -1;
if (find_enemy(OBJ_INDEX(Pl_objp), MAX_ENEMY_DISTANCE, The_mission.ai_profile->max_attackers[Game_skill_level]) == -1) {
if (find_enemy(OBJ_INDEX(Pl_objp), MAX_ENEMY_DISTANCE, The_mission.ai_profile->max_attackers[Game_skill_level], ai_targets_for_pursuit(aip)) == -1) {
ai_do_default_behavior(Pl_objp);
return;
}
@@ -1968,26 +1979,25 @@ int ai_big_maybe_enter_strafe_mode(const object *pl_objp, int weapon_objnum)
return 0;
}
if (ai_declines_pursuit(pl_objp, parent_objp)) {
return 0;
}
// Maybe the ship which fired the weapon isn't the current target
if ( OBJ_INDEX(parent_objp) != aip->target_objnum ) {
//JAS IMPOSSIBLE if (1) { // consider_target_only ) {
//JAS IMPOSSIBLE return 0;
//JAS IMPOSSIBLE } else {
// switch targets
sip = &Ship_info[Ships[parent_objp->instance].ship_info_index];
if ( !(sip->is_big_or_huge()) || (sip->flags[Ship::Info_Flags::Transport]) ) {
return 0;
}
set_target_objnum(aip, OBJ_INDEX(parent_objp));
//JAS IMPOSSIBLE }
}
ai_big_strafe_maybe_attack_turret(pl_objp, weapon_objp);
// if we've got this far, the weapon must have come from the player's target, and it is a
// big/capital ship... so enter strafe mode
aip->previous_mode = aip->mode;
ai_big_save_previous_mode(aip);
aip->mode = AIM_STRAFE;
aip->submode_parm0 = Missiontime; // use parm0 as time strafe mode entered (i.e. MODE start time)
aip->submode = AIS_STRAFE_AVOID;
+136 -42
View File
@@ -2215,16 +2215,35 @@ static bool ai_class_type_actively_pursues(int attacker_class_type, int target_c
return std::any_of(pursues.begin(), pursues.end(), [target_class_type](int pursued_type) { return pursued_type == target_class_type; });
}
// Returns true if a target this ship picks for itself will be used for pursuit, and therefore applicable to the "actively_pursues" check.
// In other modes a ship's target is held for its turrets, and is also read by other ships (guard reactions, attacker counts).
bool ai_targets_for_pursuit(const ai_info *aip)
{
return aip->mode == AIM_CHASE || aip->mode == AIM_STRAFE;
}
// Returns true if attacker_objp should not pick target_objp as a target to pursue.
bool ai_declines_pursuit(const object *attacker_objp, const object *target_objp)
{
Assertion(attacker_objp->type == OBJ_SHIP, "ai_declines_pursuit() was called with an attacker of object type %d; it must be a ship!", attacker_objp->type);
if (!The_mission.ai_profile->flags[AI::Profile_Flags::Fix_ai_target_recovery] || target_objp->type != OBJ_SHIP)
return false;
return !ai_class_type_actively_pursues(Ship_info[Ships[attacker_objp->instance].ship_info_index].class_type,
Ship_info[Ships[target_objp->instance].ship_info_index].class_type);
}
typedef struct eval_nearest_objnum {
int objnum;
object *trial_objp;
int enemy_team_mask;
int enemy_ship_info_index;
int enemy_class_type;
int attacker_class_type;
int enemy_wing;
float range;
int max_attackers;
bool for_pursuit;
int nearest_objnum;
float nearest_dist;
int check_danger_weapon_objnum;
@@ -2280,11 +2299,10 @@ void evaluate_object_as_nearest_objnum(eval_nearest_objnum *eno)
float dist;
int num_attacking;
// Don't pick a target that this ship's type refuses to chase, or it will park in AIM_NONE holding a target it never attacks.
// If pursuing, don't pick a target that this ship's type refuses to pursue, or it will park in AIM_NONE holding a target it never attacks.
// Only for unconstrained searches: an explicit chase-ship-class/type order should still produce a target even if ai_chase() doesn't chase it.
if (The_mission.ai_profile->flags[AI::Profile_Flags::Fix_ai_target_recovery]
&& eno->enemy_ship_info_index < 0 && eno->enemy_class_type < 0
&& !ai_class_type_actively_pursues(eno->attacker_class_type, Ship_info[shipp->ship_info_index].class_type))
if (eno->for_pursuit && eno->enemy_ship_info_index < 0 && eno->enemy_class_type < 0
&& ai_declines_pursuit(&Objects[eno->objnum], eno->trial_objp))
return;
// Allow targeting of stealth in nebula by his firing at me
@@ -2352,10 +2370,11 @@ void evaluate_object_as_nearest_objnum(eval_nearest_objnum *eno)
* @param enemy_wing Enemy wing chosen
* @param range Ship must be within range "range".
* @param max_attackers Don't attack a ship that already has at least max_attackers attacking it.
* @param for_pursuit True if the ship will chase the enemy found; false if the target is only held (e.g. for turrets)
* @param ship_info_index If >=0, the enemy object must be of the specified ship class
* @param class_type If >=0, the enemy object must be of the specified ship type
*/
int get_nearest_objnum(int objnum, int enemy_team_mask, int enemy_wing, float range, int max_attackers, int ship_info_index, int class_type)
int get_nearest_objnum(int objnum, int enemy_team_mask, int enemy_wing, float range, int max_attackers, bool for_pursuit, int ship_info_index, int class_type)
{
object *danger_weapon_objp;
ai_info *aip;
@@ -2366,9 +2385,9 @@ int get_nearest_objnum(int objnum, int enemy_team_mask, int enemy_wing, float ra
eno.enemy_team_mask = enemy_team_mask;
eno.enemy_ship_info_index = ship_info_index;
eno.enemy_class_type = class_type;
eno.attacker_class_type = Ship_info[Ships[Objects[objnum].instance].ship_info_index].class_type;
eno.enemy_wing = enemy_wing;
eno.max_attackers = max_attackers;
eno.for_pursuit = for_pursuit;
eno.objnum = objnum;
eno.range = range;
eno.nearest_dist = range;
@@ -2409,10 +2428,17 @@ int get_nearest_objnum(int objnum, int enemy_team_mask, int enemy_wing, float ra
}
}
// If no wing member can be pursued, hold one anyway rather than leave the wing, as an explicit order to attack an unpursued ship does
if ((eno.nearest_objnum == -1) && (enemy_wing != -1) && for_pursuit && The_mission.ai_profile->flags[AI::Profile_Flags::Fix_ai_target_recovery]) {
int held_objnum = get_nearest_objnum(objnum, enemy_team_mask, enemy_wing, range, max_attackers, false, ship_info_index, class_type);
if (held_objnum >= 0 && Objects[held_objnum].type == OBJ_SHIP && Ships[Objects[held_objnum].instance].wingnum == enemy_wing)
return held_objnum;
}
// If only looking for target in certain wing and couldn't find anything in
// that wing, look for any object.
if ((eno.nearest_objnum == -1) && (enemy_wing != -1)) {
return get_nearest_objnum(objnum, enemy_team_mask, -1, range, max_attackers, ship_info_index, class_type);
return get_nearest_objnum(objnum, enemy_team_mask, -1, range, max_attackers, for_pursuit, ship_info_index, class_type);
}
return eno.nearest_objnum;
@@ -2519,10 +2545,11 @@ int get_enemy_timestamp()
* @param objnum Object number
* @param range Range within which to look
* @param max_attackers Don't attack a ship that already has at least max_attackers attacking it.
* @param for_pursuit True if the ship will chase the enemy found; false if the target is only held (e.g. for turrets)
* @param ship_info_index If specified, restrict the search to enemies with this ship class
* @param class_type If specified, restrict the search to enemies with this ship type
*/
int find_enemy(int objnum, float range, int max_attackers, int ship_info_index, int class_type)
int find_enemy(int objnum, float range, int max_attackers, bool for_pursuit, int ship_info_index, int class_type)
{
int enemy_team_mask;
@@ -2552,10 +2579,7 @@ int find_enemy(int objnum, float range, int max_attackers, int ship_info_index,
if (class_type < 0 || class_type == Ship_info[target_shipp->ship_info_index].class_type) {
if (!(target_objp->flags[Object::Object_Flags::Protected])) {
// same as get_nearest_objnum: only skip an unpursued target for unconstrained searches
if (!The_mission.ai_profile->flags[AI::Profile_Flags::Fix_ai_target_recovery]
|| ship_info_index >= 0 || class_type >= 0
|| ai_class_type_actively_pursues(Ship_info[shipp->ship_info_index].class_type,
Ship_info[target_shipp->ship_info_index].class_type)) {
if (!for_pursuit || ship_info_index >= 0 || class_type >= 0 || !ai_declines_pursuit(objp, target_objp)) {
return target_objnum;
}
}
@@ -2568,7 +2592,7 @@ int find_enemy(int objnum, float range, int max_attackers, int ship_info_index,
}
}
return get_nearest_objnum(objnum, enemy_team_mask, aip->enemy_wing, range, max_attackers, ship_info_index, class_type);
return get_nearest_objnum(objnum, enemy_team_mask, aip->enemy_wing, range, max_attackers, for_pursuit, ship_info_index, class_type);
} else {
aip->target_objnum = -1;
aip->target_signature = -1;
@@ -2642,7 +2666,7 @@ void ai_attack_object(object* attacker, object* attacked, int ship_info_index, i
if (attacked == nullptr) {
aip->choose_enemy_timestamp = timestamp(0);
// nebula safe
set_target_objnum(aip, find_enemy(OBJ_INDEX(attacker), 99999.9f, 4, ship_info_index, class_type));
set_target_objnum(aip, find_enemy(OBJ_INDEX(attacker), 99999.9f, 4, true, ship_info_index, class_type));
} else {
// check if we can see attacked in nebula
if (aip->target_objnum != OBJ_INDEX(attacked)) {
@@ -2703,10 +2727,24 @@ void ai_attack_wing(object *attacker, int wingnum)
if (count > 0) {
int index;
index = (int) (frand() * count);
// prefer wing members we will pursue; if there are none, hold any member, as an explicit order to attack an unpursued ship does
SCP_vector<int> pursued_indexes;
if (The_mission.ai_profile->flags[AI::Profile_Flags::Fix_ai_target_recovery]) {
for (int i = 0; i < count; i++) {
auto member_shipp = &Ships[Wings[wingnum].ship_index[i]];
if (!member_shipp->flags[Ship::Ship_Flags::Dying] && !ai_declines_pursuit(attacker, &Objects[member_shipp->objnum]))
pursued_indexes.push_back(i);
}
}
if (index >= count)
index = 0;
if (!pursued_indexes.empty()) {
index = pursued_indexes[Random::next(sz2i(pursued_indexes.size()))];
} else {
index = (int) (frand() * count);
if (index >= count)
index = 0;
}
set_target_objnum(aip, Ships[Wings[wingnum].ship_index[index]].objnum);
@@ -3126,6 +3164,7 @@ void create_model_path(object *pl_objp, object *mobjp, int path_num, int subsys_
aip->path_dir = PD_FORWARD;
aip->path_objnum = OBJ_INDEX(mobjp);
aip->mp_index = path_num;
aip->mp_randomized_vert = subsys_path ? mp->nverts-2 : -1;
aip->path_length = (int)(Ppfp - ppfp_start);
aip->path_next_check_time = timestamp(1);
@@ -3185,6 +3224,7 @@ void create_model_exit_path(object *pl_objp, object *mobjp, int path_num, int co
aip->path_dir = PD_FORWARD;
aip->path_objnum = OBJ_INDEX(mobjp);
aip->mp_index = path_num;
aip->mp_randomized_vert = -1;
aip->path_length = (int)(Ppfp - ppfp_start);
aip->path_next_check_time = timestamp(1);
@@ -3880,14 +3920,14 @@ void ai_afterburn_hard(object* objp, ai_info* aip) {
accelerate_ship(aip, 1.0f);
};
// Given an ai_info struct, by reading current goal and path information,
// extract base path information and return in pmp and pmpv.
// Given an ai_info struct, by reading global path information,
// extract model path information and return in pmp and pmpv.
// Return true if found, else return false.
// false means the current point is not on the original path.
int get_base_path_info(int path_cur, int goal_objnum, model_path **pmp, mp_vert **pmpv)
int get_base_path_info(int path_cur, int path_objnum, model_path **pmp, mp_vert **pmpv)
{
pnode *pn = &Path_points[path_cur];
ship *shipp = &Ships[Objects[goal_objnum].instance];
ship *shipp = &Ships[Objects[path_objnum].instance];
polymodel *pm = model_get(Ship_info[shipp->ship_info_index].model_num);
*pmpv = NULL;
@@ -3932,7 +3972,14 @@ void modify_model_path_points(object *objp)
dir = -1;
}
copy_xlate_model_path_points(mobjp, &pm->paths[path_num], dir, pm->paths[path_num].nverts, path_num, pnp);
// an exit path may have been created with fewer than nverts points, so don't write past its end
int count = aip->path_length - static_cast<int>(pnp - &Path_points[aip->path_start]);
int randomize_pnt = -1;
if (The_mission.ai_profile->flags[AI::Profile_Flags::Fix_model_path_refresh_randomization])
randomize_pnt = aip->mp_randomized_vert;
copy_xlate_model_path_points(mobjp, &pm->paths[path_num], dir, count, path_num, pnp, randomize_pnt);
}
// Return an indication of the distance between two matrices.
@@ -4035,12 +4082,18 @@ void set_accel_for_docking(object *objp, ai_info *aip, float dot, float dot_to_n
ai_afterburn_hard(Pl_objp, aip);
} else {
float max_bay_speed = sip->max_speed;
bool fix_bay_speed_ramp = The_mission.ai_profile->flags[AI::Profile_Flags::Fix_bay_speed_ramp];
ship_info *gsip = &Ship_info[Ships[gobjp->instance].ship_info_index];
polymodel *pm = model_get(gsip->model_num);
// a departure path can begin with points that route the ship around the carrier, ahead of the bay path itself
int ramp_start = aip->path_start;
if (aip->mode == AIM_BAY_DEPART && fix_bay_speed_ramp)
ramp_start = MAX(aip->path_start, aip->path_start + aip->path_length - pm->paths[aip->mp_index].nverts);
// Maybe gradually ramp up/down the speed of a ship flying a fighterbay path
if (aip->mode == AIM_BAY_EMERGE || (aip->mode == AIM_BAY_DEPART && aip->path_cur != aip->path_start)) {
ship_info *gsip = &Ship_info[Ships[gobjp->instance].ship_info_index];
polymodel *pm = model_get(gsip->model_num);
SCP_string pathName(pm->paths[Path_points[aip->path_start].path_num].name);
if (aip->mode == AIM_BAY_EMERGE || (aip->mode == AIM_BAY_DEPART && aip->path_cur > ramp_start)) {
SCP_string pathName(pm->paths[aip->mp_index].name);
float speed_mult = FLT_MIN;
if (aip->mode == AIM_BAY_EMERGE) { // Arriving
@@ -4064,7 +4117,7 @@ void set_accel_for_docking(object *objp, ai_info *aip, float dot, float dot_to_n
if (speed_mult != FLT_MIN && speed_mult != 1.0f) {
// We use the distance between the first and last point on the path here; it's not accurate
// if the path is not straight, but should be good enough usually; can be changed if necessary.
float total_path_length = vm_vec_dist_quick(&Path_points[aip->path_start].pos, &Path_points[aip->path_start + aip->path_length - 1].pos);
float total_path_length = vm_vec_dist_quick(&Path_points[ramp_start].pos, &Path_points[aip->path_start + aip->path_length - 1].pos);
float dist_to_end;
if (aip->mode == AIM_BAY_EMERGE) { // Arriving
@@ -4073,8 +4126,12 @@ void set_accel_for_docking(object *objp, ai_info *aip, float dot, float dot_to_n
dist_to_end = vm_vec_dist_quick(&Pl_objp->pos, &Path_points[aip->path_start + aip->path_length - 1].pos);
}
float ramp_fraction = dist_to_end / total_path_length;
if (fix_bay_speed_ramp)
CLAMP(ramp_fraction, 0.0f, 1.0f);
// Calculate max speed, but respect the waypoint speed cap if it's lower
max_bay_speed = sip->max_speed * (speed_mult + (1.0f - speed_mult) * (dist_to_end / total_path_length));
max_bay_speed = sip->max_speed * (speed_mult + (1.0f - speed_mult) * ramp_fraction);
}
}
@@ -5166,6 +5223,8 @@ int maybe_resume_previous_mode(object *objp, ai_info *aip)
aip->submode = AIS_GUARD_PATROL;
aip->submode_start_time = Missiontime;
aip->active_goal = AI_ACTIVE_GOAL_NONE;
if (The_mission.ai_profile->flags[AI::Profile_Flags::Fix_stale_ai_order_state])
set_targeted_subsys(aip, nullptr, -1); // also discards any subsystem path from attacking a big ship
return 1;
}
}
@@ -8331,6 +8390,7 @@ void ai_set_guard_wing(object *objp, int wingnum)
// prevents a ship from trying to guard a non-existent wing.
if (Wings[wingnum].current_count < 1) {
aip->guard_objnum = -1;
aip->guard_signature = -1;
aip->guard_wingnum = -1;
aip->mode = AIM_NONE;
} else {
@@ -9029,15 +9089,6 @@ void ai_chase()
//WMC - Guess we do need this
if (!go_after_it) {
// If we picked this target ourselves -- through auto-attack, dynamic chase, or a standing chase order --
// drop it, so that the retargeting logic in ai_frame() can find something we are willing to chase.
if (The_mission.ai_profile->flags[AI::Profile_Flags::Fix_ai_target_recovery]) {
if (aip->active_goal < 0 || aip->active_goal == AI_ACTIVE_GOAL_DYNAMIC
|| (aip->active_goal < MAX_AI_GOALS && ai_goal_is_standing_chase(aip->goals[aip->active_goal].ai_mode))) {
aip->target_objnum = -1;
aip->target_signature = -1;
}
}
aip->mode = AIM_NONE;
return;
}
@@ -10436,6 +10487,10 @@ void guard_object_was_hit(object *guard_objp, object *hitter_objp)
if (Ships[guard_objp->instance].team == Ships[hitter_objp->instance].team)
return;
// Keep guarding rather than chase a ship we won't pursue
if (ai_declines_pursuit(guard_objp, hitter_objp))
return;
// limit the number of ships attacking hitter_objnum (for now, only if hitter_objnum is player)
if ( ai_maybe_limit_attackers(hitter_objnum) == 1 ) {
return;
@@ -15477,7 +15532,7 @@ void ai_frame(int objnum)
} else if (aip->resume_goal_time == -1) {
// AL 12-9-97: Don't allow cargo and navbuoys to set their aip->target_objnum
if ( Ship_info[shipp->ship_info_index].class_type > -1 && (Ship_types[Ship_info[shipp->ship_info_index].class_type].flags[Ship::Type_Info_Flags::AI_auto_attacks]) ) {
target_objnum = find_enemy(objnum, MAX_ENEMY_DISTANCE, The_mission.ai_profile->max_attackers[Game_skill_level]); // Attack up to 2.5K units away.
target_objnum = find_enemy(objnum, MAX_ENEMY_DISTANCE, The_mission.ai_profile->max_attackers[Game_skill_level], ai_targets_for_pursuit(aip)); // Attack up to 2.5K units away.
if (target_objnum != -1) {
if (aip->target_objnum != target_objnum)
aip->aspect_locked_time = 0.0f;
@@ -15561,7 +15616,7 @@ void ai_frame(int objnum)
if ((aip->resume_goal_time > 0) && (aip->resume_goal_time < Missiontime)) {
aip->active_goal = AI_ACTIVE_GOAL_NONE;
aip->resume_goal_time = -1;
target_objnum = find_enemy(objnum, 2000.0f, The_mission.ai_profile->max_attackers[Game_skill_level]);
target_objnum = find_enemy(objnum, 2000.0f, The_mission.ai_profile->max_attackers[Game_skill_level], ai_targets_for_pursuit(aip));
if (target_objnum != -1) {
if (aip->target_objnum != target_objnum) {
aip->aspect_locked_time = 0.0f;
@@ -15881,6 +15936,7 @@ void init_ai_object(int objnum)
aip->path_start = -1;
aip->path_goal_dist = -1;
aip->path_length = 0;
aip->mp_randomized_vert = -1;
aip->path_subsystem_next_check = 1;
aip->support_ship_objnum = -1;
@@ -16039,6 +16095,29 @@ void init_aip_from_class_and_profile(ai_info *aip, ai_class *aicp, ai_profile_t
aip->ai_profile_flags = profile->flags | (aicp->ai_profile_flags & aicp->ai_profile_flags_set);
}
// Clear the fields that remember which order set them up or which order a dynamic goal interrupted.
// Call when a new order takes over; the order's own setup repopulates whatever it needs.
void ai_reset_order_state(ai_info *aip)
{
if (The_mission.ai_profile->flags[AI::Profile_Flags::Fix_stale_ai_order_state]
|| The_mission.ai_profile->flags[AI::Profile_Flags::Fix_ai_target_recovery])
aip->enemy_wing = -1;
if (The_mission.ai_profile->flags[AI::Profile_Flags::Fix_stale_ai_order_state])
{
aip->guard_objnum = -1;
aip->guard_signature = -1;
aip->guard_wingnum = -1;
aip->previous_mode = AIM_NONE;
aip->previous_submode = 0;
aip->resume_goal_time = -1;
// the player's targeted subsystem is HUD state, not order state
if (aip != Player_ai || Player_use_ai)
set_targeted_subsys(aip, nullptr, -1); // also discards any subsystem path
}
}
void ai_do_default_behavior(object *obj)
{
Assertion(obj != nullptr, "Ai_do_default_behavior() was given a bad information. Specifically obj is a nullptr. This is a coder error, please report!");
@@ -16052,6 +16131,7 @@ void ai_do_default_behavior(object *obj)
aip->mode = AIM_NONE;
aip->submode_start_time = Missiontime;
aip->active_goal = AI_ACTIVE_GOAL_NONE;
ai_reset_order_state(aip);
// if we're not docked, we may modify the behavior a bit
if (!object_is_docked(obj))
@@ -16059,7 +16139,7 @@ void ai_do_default_behavior(object *obj)
// fighters automatically chase things
if (!is_instructor(obj) && (sip->is_fighter_bomber()))
{
int enemy_objnum = find_enemy(OBJ_INDEX(obj), 1000.0f, The_mission.ai_profile->max_attackers[Game_skill_level]);
int enemy_objnum = find_enemy(OBJ_INDEX(obj), 1000.0f, The_mission.ai_profile->max_attackers[Game_skill_level], true);
set_target_objnum(aip, enemy_objnum);
aip->mode = AIM_CHASE;
aip->submode = SM_ATTACK;
@@ -16281,15 +16361,29 @@ void maybe_set_dynamic_chase(ai_info *aip, int hitter_objnum)
return;
}
if (ai_declines_pursuit(&Objects[Ships[aip->shipnum].objnum], &Objects[hitter_objnum])) {
return;
}
if (aip->target_objnum != hitter_objnum)
aip->aspect_locked_time = 0.0f;
set_target_objnum(aip, hitter_objnum);
if (The_mission.ai_profile->flags[AI::Profile_Flags::Fix_stale_ai_order_state]) {
// don't bash the mode we're returning to if this is a dynamic goal interrupting another one
if (aip->active_goal != AI_ACTIVE_GOAL_DYNAMIC) {
aip->previous_mode = aip->mode;
aip->previous_submode = aip->submode;
}
} else {
aip->previous_submode = aip->mode; // retail typo, almost certainly meant to be previous_mode
}
aip->resume_goal_time = Missiontime + i2f(20); // Only chase up to 20 seconds.
aip->active_goal = AI_ACTIVE_GOAL_DYNAMIC;
set_targeted_subsys(aip, NULL, -1); // Say not attacking any particular subsystem.
aip->previous_submode = aip->mode;
aip->mode = AIM_CHASE;
aip->submode = SM_ATTACK;
aip->submode_start_time = Missiontime;
+16 -1
View File
@@ -1803,6 +1803,20 @@ ai_achievability ai_mission_goal_achievable( int objnum, ai_goal *aigp )
auto target_ship_entry = aigp->target_name == nullptr ? nullptr : ship_registry_get(aigp->target_name);
// a ship can't guard itself, and the leader of a wing is the ship the rest of the wing guards
if (The_mission.ai_profile->flags[AI::Profile_Flags::Fix_stale_ai_order_state])
{
if (aigp->ai_mode == AI_GOAL_GUARD && target_ship_entry && target_ship_entry->has_shipp() && target_ship_entry->shipp() == shipp)
return ai_achievability::NOT_ACHIEVABLE;
if (aigp->ai_mode == AI_GOAL_GUARD_WING)
{
int wingnum = wing_name_lookup(aigp->target_name);
if (wingnum >= 0 && Wings[wingnum].ship_index[0] == objp->instance)
return ai_achievability::NOT_ACHIEVABLE;
}
}
// warp (depart) only achievable if there's somewhere to depart to
if (aigp->ai_mode == AI_GOAL_WARP)
{
@@ -2535,8 +2549,9 @@ void ai_process_mission_orders( int objnum, ai_info *aip )
if ( !(Player_use_ai) && (objp->flags[Object::Object_Flags::Player_ship]) && (current_goal->ai_mode != AI_GOAL_FORM_ON_WING) )
{
return;
}
}
ai_reset_order_state(aip);
std::unique_ptr<ai_goal> current_goal_backup;
auto current_goal_target_ship = current_goal->target_name ? ship_registry_get(current_goal->target_name) : nullptr;
+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
}
+7
View File
@@ -1036,6 +1036,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;
+8
View File
@@ -594,6 +594,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
+85 -50
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( ) )
+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
+17 -9
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
@@ -145,7 +153,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 +196,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 +243,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 +265,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;
@@ -338,6 +346,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() {
+7
View File
@@ -1,3 +1,6 @@
#ifdef GL_ES
precision highp float;
#endif
#ifdef VULKAN
layout(location = 0) out vec2 fragTexCoord;
@@ -10,7 +13,11 @@ void main() {
in vec4 vertPosition;
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;
+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
+31 -14
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"
@@ -112,6 +121,13 @@ uniform shadowCascadeParams {
vec4 smoothness_factors[(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]
@@ -139,13 +155,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;
@@ -185,13 +200,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)
@@ -283,6 +295,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;
@@ -464,7 +480,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 +525,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();
}
+32 -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"
@@ -145,6 +155,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 +180,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 +265,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
}
+25 -4
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;
#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
@@ -24,16 +31,25 @@ layout (std140) uniform shadowCascadeParams {
};
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 +65,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();
}
+31 -3
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;
@@ -66,12 +73,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 +132,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 +153,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;
@@ -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;
}
+1 -1
View File
@@ -381,7 +381,7 @@ int __cdecl RecordExceptionInfo(PEXCEPTION_POINTERS data, const char *Message)
wsprintf(DebugMessage, "%s location %08x caused an access violation.\r\n", readwrite, Exception->ExceptionInformation[1]);
#ifdef _DEBUG
#ifndef NDEBUG
// The VisualC++ debugger doesn't actually tell you whether a read
// or a write caused the access violation, nor does it tell what
// address was being read or written. So I fixed that.
+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!
+29 -2
View File
@@ -289,7 +289,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)
{
@@ -2184,7 +2188,30 @@ bool gr_init(std::unique_ptr<os::GraphicsOperations>&& graphicsOps, GraphicsAPI
//
// As such, window_res is effectively no longer optional, part of the core render path and must always be enabled.
// The only reason it is not yet refactored into an always-on thing is due to issues with FRED integration.
Cmdline_window_res.emplace(static_cast<uint16_t>(width), static_cast<uint16_t>(height));
int window_width = width;
int window_height = height;
// In exclusive fullscreen, the window must match the monitor. Using the render resolution instead would require a
// video mode switch to that resolution, which may not exist as a display mode (leaving the window at a different
// size than window_res) and is repeated on every alt-tab.
if (!gr_is_viewport_window()) {
if (SDL_InitSubSystem(SDL_INIT_VIDEO)) {
auto displayMode = SDL_GetDesktopDisplayMode(gr_get_preferred_display());
if (displayMode) {
window_width = displayMode->w;
window_height = displayMode->h;
} else {
mprintf(("Could not query desktop display mode for fullscreen window size: %s\n", SDL_GetError()));
}
SDL_QuitSubSystem(SDL_INIT_VIDEO);
} else {
mprintf(("Could not initialize SDL video for fullscreen window size: %s\n", SDL_GetError()));
}
}
mprintf(("Forcing window_res to %dx%d (render resolution %dx%d)\n", window_width, window_height, width, height));
Cmdline_window_res.emplace(static_cast<uint16_t>(window_width), static_cast<uint16_t>(window_height));
}
if (d_mode == GraphicsAPI::Default) {
@@ -2255,7 +2282,7 @@ bool gr_init(std::unique_ptr<os::GraphicsOperations>&& graphicsOps, GraphicsAPI
// built in debug mode. This helps if you run with the debugger active as the
// game won't be switching from fullscreen to minimized every time you hit a breakpoint or
// warning message.
#ifdef _DEBUG
#ifndef NDEBUG
#ifdef _FORCE_DEBUG_WIDESCREEN
width = 1280;
height = 800;
+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
+6 -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
@@ -956,7 +956,7 @@ void gr_opengl_use_viewport(os::Viewport* view) {
int opengl_init_display_device()
{
os::ViewPortProperties attrs;
os::ViewPortProperties attrs = {};
attrs.enable_opengl = true;
attrs.gl_attributes.major_version = MIN_REQUIRED_GL_VERSION / 10;
@@ -1394,7 +1394,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 +1453,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")));
+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{
+10
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();
+7
View File
@@ -360,6 +360,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);
+18
View File
@@ -732,7 +732,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 +759,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();
@@ -1136,6 +1148,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;
+20
View File
@@ -75,7 +75,27 @@ void openxr_prepare(float hudscale) {
openxr_req = true;
}
static void openxr_check_api_layers() {
uint32_t layerCount = 0;
if (xrEnumerateApiLayerProperties(0, &layerCount, nullptr) != XR_SUCCESS || layerCount == 0)
return;
SCP_vector<XrApiLayerProperties> layers(layerCount, XrApiLayerProperties { XR_TYPE_API_LAYER_PROPERTIES, nullptr, "", 0, 0, "" });
if (xrEnumerateApiLayerProperties(layerCount, &layerCount, layers.data()) != XR_SUCCESS)
return;
for (const auto& layer : layers) {
mprintf(("OpenXR API layer: %s (version %u)\n", layer.layerName, layer.layerVersion));
if (!strcmp(layer.layerName, "XR_APILAYER_NOVENDOR_toolkit")) {
ReleaseWarning(LOCATION, "OpenXR Toolkit is installed and active. There are known compatibility issues with FSO that will likely crash the game.\nPlease disable or uninstall OpenXR Toolkit to use VR with FSO.");
}
}
}
static bool openxr_init_instance() {
openxr_check_api_layers();
auto extensions = gr_openxr_get_extensions();
const gameversion::version& fso_version = gameversion::get_executable_version();
+13
View File
@@ -162,6 +162,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) {
+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);
+4
View File
@@ -271,6 +271,9 @@ void vulkan_deferred_lighting_begin(bool clearNonColorBufs)
void vulkan_deferred_lighting_msaa()
{
if (!Deferred_lighting) {
return;
}
if (Cmdline_msaa_enabled <= 0) {
return;
}
@@ -956,6 +959,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;
+6 -5
View File
@@ -748,7 +748,7 @@ void VulkanDrawManager::renderShadowDraw(gr_buffer_handle ubo_handle, size_t ubo
}
stateTracker->bindPipeline(pipeline, pipelineManager->getPipelineLayout());
stateTracker->setDepthBias(-1024.0f, 0.0f);
stateTracker->setDepthBias(1024.0f, 1.0f); // GL shadow pass: gr_zbias(-1024) = polygon offset factor 1, units 1024
// Bind descriptor sets: shadowMapData (per-draw) and shadowCascadeParams
// (bound separately, per-frame, via shadow_cascade_params_bind) both live in
@@ -1281,6 +1281,10 @@ bool VulkanDrawManager::applyMaterial(material* mat, primitive_type prim_type, v
m_frameStats.applyMaterialCalls++;
// Like opengl_tnl_set_material(), every material draw sets the texture addressing (e.g. MR_FORCE_CLAMP skyboxes)
setTextureAddressing(mat->get_texture_addressing());
vulkan_zbias(mat->get_depth_bias());
// Build pipeline configuration from material
PipelineConfig config = buildPipelineConfig(mat, prim_type);
@@ -1480,9 +1484,6 @@ bool VulkanDrawManager::applyMaterial(material* mat, primitive_type prim_type, v
stateTracker->setStencilMode(GR_STENCIL_NONE);
}
// Set depth bias if needed
stateTracker->setDepthBias(static_cast<float>(mat->get_depth_bias()), 0.0f);
return true;
}
@@ -1540,7 +1541,7 @@ void VulkanDrawManager::draw(primitive_type prim_type, int first_vertex, int ver
m_frameStats.totalVertices += vertex_count;
// Flush any dirty dynamic state (viewport, scissor, depth bias, stencil ref)
// before issuing the draw command. applyMaterial sets these AFTER bindPipeline,
// before issuing the draw command. applyMaterial sets some of these AFTER bindPipeline,
// so they may be dirty even when the pipeline didn't change.
stateTracker->applyDynamicState();
+9 -6
View File
@@ -98,8 +98,7 @@ void vulkan_set_default_material_uniforms(material* material_info)
(clip_plane.normal.xyz.y * clip_plane.position.xyz.y) +
(clip_plane.normal.xyz.z * clip_plane.position.xyz.z));
// Model matrix (identity for now, material doesn't provide one)
vm_matrix4_set_identity(&data->modelMatrix);
data->modelMatrix = gr_model_matrix_stack.get_transform();
} else {
data->clipEnabled = 0;
vm_matrix4_set_identity(&data->modelMatrix);
@@ -613,6 +612,9 @@ void vulkan_calculate_irrmap()
}
// Get envmap cubemap view
if (!texManager->ensureCubemapUploaded(ENVMAP)) {
return;
}
auto* envSlot = bm_get_slot(ENVMAP, true);
if (!envSlot || !envSlot->gr_info) {
return;
@@ -638,9 +640,6 @@ void vulkan_calculate_irrmap()
vk::CommandBuffer cmd = stateTracker->getCommandBuffer();
// End the current swap chain render pass
cmd.endRenderPass();
// Create pipeline for irradiance map generation
PipelineConfig config;
config.shaderType = SDR_TYPE_IRRADIANCE_MAP_GEN;
@@ -711,6 +710,9 @@ void vulkan_calculate_irrmap()
vk::Extent2D irrExtent(irrTs->width, irrTs->height);
// End the swap chain pass only after the early returns above, which never resume it
cmd.endRenderPass();
for (size_t face = 0; face < irrTs->cubeFaceFramebuffers.size(); face++) {
vk::Framebuffer fb = irrTs->cubeFaceFramebuffers[face];
if (!fb) {
@@ -761,7 +763,8 @@ void vulkan_calculate_irrmap()
writer.writeSet(materialSet, VulkanDescriptorManager::getSetTemplate(DescriptorSetIndex::Material));
{
std::array<vk::DescriptorImageInfo, VulkanDescriptorManager::MAX_TEXTURE_BINDINGS> texImages;
texImages.fill(descManager->getFallbacks().texture2D);
// irrmap-f.sdr declares this binding as samplerCube, so every element needs a cube view
texImages.fill(descManager->getFallbacks().textureCube);
texImages[0].imageView = envmapView;
writer.setImageArray(MaterialBinding::TextureArray, texImages);
}
+6
View File
@@ -121,6 +121,12 @@ public:
*/
vk::Pipeline getPipeline(const PipelineConfig& config, const vertex_layout& vertexLayout);
/**
* @brief Destroy all cached pipelines; later getPipeline calls build them again
* @note No submitted or recording command buffer may still use them
*/
void forgetAllPipelines() { m_pipelines.clear(); }
/**
* @brief Get the common pipeline layout
*
@@ -272,8 +272,7 @@ vk::DescriptorImageInfo VulkanDistortion::getTextureInfo() const
// Distortion_texture[!Distortion_switch] binding for thrusters).
// After update() toggles the switch, m_switch points to the old read
// source, which is the texture that was just written.
return {m_ctx->linearSampler, m_tex[m_switch].view,
vk::ImageLayout::eShaderReadOnlyOptimal};
return {m_sampler, m_tex[m_switch].view, vk::ImageLayout::eShaderReadOnlyOptimal};
}
} // namespace graphics::vulkan
@@ -531,6 +531,9 @@ void VulkanLDR::executeLightshafts(vk::CommandBuffer cmd)
if (dot > 0.7f) {
sun_x = asinf_safe(vm_vec_dot(&light_dir, &Eye_matrix.vec.rvec)) / PI * 1.5f + 0.5f;
sun_y = asinf_safe(vm_vec_dot(&light_dir, &Eye_matrix.vec.uvec)) / PI * 1.5f * gr_screen.clip_aspect + 0.5f;
// The formula above is OpenGL's, whose texture V grows upward. The Vulkan fullscreen triangle in
// post-v.sdr has V = 0 at the top of the screen, so flip to the space ls-f.sdr's fragTexCoord is in.
sun_y = 1.0f - sun_y;
found = true;
break;
}
Loaded 100 of 244 files, more files were not shown because too many files have changed in this diff. Show more