Skip to content

Sync upstream into main - #4

Open
github-actions[bot] wants to merge 171 commits into
mainfrom
master
Open

Sync upstream into main#4
github-actions[bot] wants to merge 171 commits into
mainfrom
master

Conversation

@github-actions

Copy link
Copy Markdown

Automated upstream sync hit conflicts. Merge master (now level with beyond-all-reason/RecoilEngine) into main and resolve here.

MarkKropf and others added 30 commits June 18, 2026 21:11
A handful of SolLua bindings still referenced sol::nil / sol::type::nil
while the rest of the same files already use sol::lua_nil. sol::nil is a
thin alias for lua_nil (see lib/sol2/sol.hpp) and collides with the `nil`
macro that Objective-C headers define, so lua_nil is the portable
spelling. This brings the remaining spots in line with the prevailing
convention.

Extracted from cc3cad9 in beyond-all-reason#2991; cross-platform, no behavior change.
float3.h included lib/streflop/streflop_cond.h directly, ahead of
FastMath.h. FastMath.h defines MATH_SQRT_OVERRIDE before it includes
streflop_cond.h (so streflop does not define its own math::sqrt(float) --
FastMath provides a faster one). The direct include meant streflop_cond.h
could be processed before that override was set.

Drop the direct include; FastMath.h pulls in streflop_cond.h transitively
with the override in place. creg_cond.h keeps its original position --
it does not pull in streflop, so it does not need to move.

Surfaced by the macOS port (beyond-all-reason#2991, commit cc3cad9).

Co-authored-by: Mark Kropf <markkropf@gmail.com>
…son#3025)

`SafeUtil.h` uses `<memory>` for `std::addressof`, and `<type_traits>`
for `std::is_trivially_copyable` / `std::is_trivially_constructible_v`,
but pulled them in only transitively. Include them directly so the header
is self-contained and does not rely on include order elsewhere. Also use
`std::is_trivially_default_constructible` for the default-construction.

Extracted from cc3cad9 in beyond-all-reason#2991; cross-platform, no behavior change.

Co-authored-by: Mark Kropf <markkropf@gmail.com>
Co-authored-by: sprunk <spr.ng@o2.pl>
LuaTextures::Create returned an empty string on glTexImage failure with
no diagnostics. Log target/size/format/dataFormat/dataType/glError so
texture-creation failures can be diagnosed.

Extracted from 1e75080 in beyond-all-reason#2991; cross-platform, no behavior change
beyond the added log.
Engine crash thread: https://discordapp.com/channels/549281623154229250/1516994684591931535

Bugged Scenario: A reclaimer A is guarding another reclaimer B, while both reclaiming the same wreck. A can be notified of wreck death BEFORE the guarded unit B is notified, leading to trying to reclaim the same (actively-deleting) wreck because B's reference hasn't been cleaned up yet.

This leads to an eventual segfault.

So: the short term/easy fix is to skip reclaiming of a dead/dying target.
util_fileSelector was declared with a non-const struct dirent* on
__APPLE__ and const elsewhere. macOS scandir() expects the selector
argument as int(*)(const struct dirent*), so the non-const Apple variant
failed to compile under GCC:

  Util.c:500: error: passing argument 3 of 'scandir' from incompatible
  pointer type

Both branches were otherwise identical, so drop the __APPLE__ split and
use const struct dirent* unconditionally (matches POSIX scandir). No
effect on Linux/Windows.

Assisted by Claude Code; verified by compiling the macOS headless build.
The legacy build did find_package(X11 REQUIRED) under a plain if(UNIX)
guard. macOS is UNIX in CMake but does not use X11 (it uses Cocoa), so
configuring the engine on macOS failed at find_package(X11). An if(APPLE)
block already follows for Foundation, so exclude Apple from the X11
branch: if(UNIX AND NOT APPLE).

Surfaced configuring the spring-headless target on macOS (which reuses
the legacy Game target). No effect on Linux/Windows.

Assisted by Claude Code; verified by configuring the macOS build.
Modern 7-Zip ships its CLI as '7zz' (Homebrew's 'sevenzip' formula
installs /opt/homebrew/bin/7zz; recent Linux distros likewise package
'7zz'). FindSevenZip only searched for '7z'/'7za', so configure failed
with 'Could NOT find SevenZip (missing: SEVENZIP_BIN)' on such systems.

Add '7zz' to the searched NAMES. No effect where 7z/7za already exist.
rts/System/Platform/Mac/SDLMain.m and SDLMain.h are the classic SDL 1.2
Cocoa main wrapper (the Darrell Walisser / Max Horn template). They are:

- not listed in any CMakeLists (never compiled)
- not #included by any source file
- built on Carbon (<Carbon/Carbon.h>), which is 32-bit-only and
  unavailable on modern macOS / Apple Silicon

SDL2 provides its own SDL_main, so this wrapper is obsolete. Remove the
dead files so the macOS platform layer reflects what is actually built.
Updated the link for the SplinterFaction card to include the 'https://' prefix.

This has been broken for a very long time. I've asked Skyrbunny to fix it multiple times, but it has never gotten fixed.
beyond-all-reason#3052)

setMinLevel() only searched for an existing entry on the "set back to default" (erase) path. Setting a section to a *non-default* level appended a new row unconditionally, so repeatedly changing one section's level (e.g. via Spring.SetLogSectionFilterLevel) accumulated duplicate entries and eventually filled the fixed 64-slot sectionMinLevels table -- after which every section-level change silently failed with "too many section-levels".

Fix: Look the section up before appending and, if it already has an entry, update it in place. This bounds the table at one entry per section.

This likely never happens in practice but this was found while writing other tests

Co-authored-by: Bruno Da Silva <Bruno-DaSilva@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The original code relied on implicit null-termination which is not guaranteed.
Aligns forward declarations with definitions to silence MSVC warnings.
SpringApp.cpp included <X11/Xlib.h> for every non-Windows platform, but
macOS has no X11 headers by default, breaking the native build. The only
user of it, XInitThreads(), is already excluded on __APPLE__ at its call
site, so guard the include the same way.
glad_glx.c includes X11/X.h to provide the GLX windowing-system bindings,
but macOS has no X11/GLX. The Glad CMakeLists added glad_glx.c for every
UNIX-and-not-MinGW platform, so building any GL-enabled target (e.g.
engine-legacy) on macOS failed:

    fatal error: X11/X.h: No such file or directory

macOS resolves GL entry points without GLX (the engine's glxHandler is
already #ifdef'd out on __APPLE__), so exclude glad_glx.c on Apple and
build only the core glad.c there.
…eyond-all-reason#2919)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
These three headers use std:: algorithms but rely on <algorithm> being
pulled in transitively. libstdc++ does so; libc++ (clang/macOS) does not,
so they can fail to compile under libc++ depending on the version.

Symbols that require the include:
- rts/System/Matrix44f.h    -> std::copy
- rts/System/SpringHashMap.hpp -> std::fill_n
- rts/System/SpringHashSet.hpp -> std::fill_n

Adding the include is "include what you use" correctness and is a no-op on
toolchains that already provide it transitively. No functional change.

Cherry-picked from ExaDev/RecoilEngine (0ed29d5, 0b4bd10, 3adabd0).

AI assistance: changes identified and applied with Claude (Anthropic);
verified by a human (compiled under clang/libc++, no regression).
DemoTool compiles engine FileSystem sources (FileHandler, FileSystem)
that include nowide/fstream.hpp and fmt/printf.h, but its
target_link_libraries omitted the nowide and fmt targets. The engine
builds obtain those include directories transitively through the
nowide::nowide and fmt::fmt INTERFACE targets; DemoTool linked neither,
so the build failed when the headers were not on a default search path
(observed building the demotool target on macOS).

Link nowide::nowide and fmt::fmt so their INTERFACE include directories
propagate to the demotool target.

Co-authored-by: Robert Burnham <burnhamrobertp@gmail.com>
* avoid parenthesised aggregate init and name
  the type explicitly (not supported everywhere)

* avoid narrowing conversions.
 * adds `Spring.TraceRayBetweenPositions(xA, yA, zA, xB, yB, zB, type)`
 * adds `Spring.TraceRayInDirection(x, y, z, dx, dy, dz, length, type)`
 * type is a string, "unit", "feature", or "both"
 * both return an array of `{distance, objID, objType}` sorted by increasing distance
 * these no longer did anything and just polluted the interface.
 * also removed example gadgetry using them from basecontent.
tomjn and others added 30 commits September 1, 2026 15:41
…#3274)

Spring.ReplaceMouseCursor with a name whose files load no frames
silently turns a working cursor invisible, which reads as a rendering
bug and cost a debugging session on macOS before the cause surfaced.

Warn rather than refuse: content may rely on a frameless replacement to
hide the cursor deliberately.
…am[, fullView]]) (beyond-all-reason#3206)

* Added gl.DrawMiniMapIcons(left, top, right, bottom, iconSize[, allyTeam[, fullView]])

add `gl.DrawMiniMapIcons(left, top, right, bottom, iconSize[, allyTeam[, fullView]])`, which draws all unit icons visible to the given perspective (team colors, radar dots, LOS rules, ghost dimming — exactly as on the minimap) for a map-space rectangle, mapped onto the unit square under the current transform. Composes with `gl.RenderToTexture`/FBOs, so widgets (e.g. picture-in-picture views) can render icon layers for any camera rect and zoom without maintaining their own icon pipeline.
- `allyTeam` and `fullView` default to the local view; other perspectives (e.g. for casting) require a full-read Lua handle.

* addressed sprungs comments

1. Magic 8 → named + documented. It's now constexpr float MAX_ICON_SCALE_MULT = 8.0f with a comment explaining what it bounds: the per-icon scale multiplier the drawer applies (icontypes.lua size, times radius/30 for radiusAdjust icons), and the failure mode when exceeded (an oversized icon can pop at the cull-rect edge; callers that care can pad their rectangle). Reply to sprunk: "Yes — an upper bound on the per-icon scale multiplier (icontypes size × radiusAdjust factor); now named and documented, including the pop-at-edge consequence for icons beyond it."

2. Rect validation → strict ordering with epsilon. The check is now !((right - left) > float3::cmp_eps()) || !((bottom - top) > float3::cmp_eps()) — which rejects flipped, degenerate, near-degenerate, and NaN rects (the !(a > b) form catches NaN where a < b wouldn't), instead of only exact equality. I went with your "also fail if left > right" option rather than renaming to x1/x2: mirroring costs the caller nothing since they control the transform the unit square lands in, and a strict contract keeps the docs and the cull setup simpler (the std::min/max there is gone). Docstring updated to say so.

3. fullView gate → sprunk's condition adopted, plus one addition. They were right, and it cut both ways: my symmetric fullView != gu->spectatingFullView check errored on a restricted handle requesting less visibility than the global state (harmless), and — worse — permitted a restricted handle to render godmode whenever the global state happened to be fullview, leaking past the handle's read scope. Now: (allyTeam != gu->myAllyTeam || fullView) && !fullRead errors — requesting fullView always demands fullRead, restricting never does. The addition worth mentioning in your reply: the default for the parameter is now gu->spectatingFullView && GetHandleFullRead(L) — without that clamp, sprunk's stricter check would have made a bare 5-argument call error from a restricted handle during fullview spectating, since the old default blindly inherited the global fullview state.

* Update rts/Lua/LuaOpenGL.cpp

Co-authored-by: sprunk <spr.ng@o2.pl>

* reverted changelog -> moved into doc/pr-changelogs\3206.md

* added addition param: @PARAM highlightSelected boolean? (Default: true) draw the local player's selected units white. Other players' selections are unknown to the engine — when rendering another perspective, pass false and overlay that viewer's selection yourself

added param highlightSelected (Default: true) draw the local player's selected units white.

---------

Co-authored-by: sprunk <spr.ng@o2.pl>
> warning: space between quotes and suffix is deprecated in C++23
…for unittype based draw order (beyond-all-reason#3204)

* Added configbool "UnitIconsSorted" (default disabled) to allow for unittype based draw order

* sortUnitIcons -> sortUnitIconsByDepth

removed `sortUnitIcons`, Games that define no `drawOrder` values skip sorting entirely
added  `UnitIconsSortedByDepth` config bool (default false). When enabled, overlapping icons are additionally ordered back-to-front by view depth within equal `drawOrder`; this makes overlap stacking change as units and the camera move, hence optional.

* changed comment, applied static_assert "icon sort records cannot index all units"

* moved changelog to: doc\pr-changelogs\3204.md
* Fix local demo waiting for replay players

---------

Co-authored-by: sprunk <spr.ng@o2.pl>
* Add mod rule to allow game to override map gravity value.
* ProjectileDrawer: reuse alpha particle geometry across the water passes

When water is visible the main view draws alpha particles twice per
frame: a below-water pass, then an above-water pass after the water
surface. Both passes contain the same particles, viewed from the same
camera at the same interpolation time; only the clip-plane uniform
differs. Each pass nevertheless refiltered, re-sorted, re-ran every
particle's Draw() to regenerate its quads, and re-uploaded all of it.

Fill and upload the geometry once in the below-water pass, capture the
pending index range before submitting (new TypedRenderBuffer::
GetPendingElemsRange), and have the above-water pass of the same draw
frame re-issue that range through the new DrawElementsRange, which
draws an already-uploaded range without touching the consume/rewind
bookkeeping. The reflection and refraction passes that run in between
fill and consume their own ranges, so the saved range stays valid;
stream buffer chunks only swap at end of frame. The reuse pass still
fires DrawWorldPreParticles and flushes anything appended during it,
so nothing can leak into a later submit under a different shader.

The reuse can be disabled at runtime with the new config
ProjectileDrawReuseWaterPasses (default 1, safemode 0), which forces
the previous full-refill-per-pass behavior.

The above-water particle pass drops from ~1.16 ms to ~65 us mean at
~6k visible alpha particles (deterministic map-wide effect-spawner
benchmark at 3x battle-level intensity on a water map, measured with
Tracy). The resubmitted geometry is byte-identical to what a refill
would produce, and maps without visible water (single combined pass)
are unaffected.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* ProjectileDrawer: faster sorting and threaded quad generation

Three further reductions of the alpha pass' CPU cost, measured with the
same deterministic effect-spawner benchmark as the previous commit
(~6k visible alpha particles, water map, Tracy):

1. Sort-key snapshot. The sorting predicates dereferenced two
   CProjectile pointers per comparison (drawOrder + sortDist), so
   sorting a few thousand particles was mostly cache misses (~200 us
   per fill). DrawAlpha now snapshots {drawOrder, sortDist, proj} into
   a contiguous array while filtering and sorts that instead. Ordering
   semantics are unchanged (drawOrder asc, distance desc, pointer
   tiebreak).

2. Threaded quad generation, config ProjectileDrawThreadedFill
   (default 1, safemode 0). The per-particle Draw() loops split the
   draw list into contiguous chunks; each chunk generates its quads
   into a per-chunk scratch buffer through a thread_local redirect of
   the buffer returned by CExpGenSpawnable::GetPrimaryRenderBuffer,
   and the scratch buffers are merged into the primary buffer in chunk
   order. The submitted geometry is therefore byte-identical to a
   serial fill, including back-to-front order. Each chunk opens a
   ProjectileDrawer::DrawAlpha(MTChunk) Tracy zone so the distribution
   is visible on the worker tracks; the ordered merge is zoned as
   DrawAlpha(MTMerge).

   All Draw() overrides reachable from the alpha pass were audited for
   shared-state writes: none call RNG, write statics, or hit lazily
   initialized caches (CColorMap::GetColor and the pre-resolved
   AtlasedTexture pointers are pure reads); they only write their own
   members and the buffer. The two exceptions carry the new
   CProjectile::mtDrawSafe = false and are drawn serially at their
   exact position in the sorted order: CTracerProjectile issues
   immediate GL calls on its own VA_TYPE_TC buffer inside Draw(), and
   ShieldSegmentProjectile mutates the shared per-shield
   ShieldSegmentCollection latch and can fire the DrawShield Lua
   callin. Segments shorter than 96 particles per chunk stay serial,
   so small fills (reflection pass, quiet frames) skip the for_mt
   dispatch overhead. Scratch buffers are only grown on the main
   thread since RenderBuffer registration is not thread-safe.

   On sync safety (particles that deal damage or spawn other
   particles): damage dealing and particle spawning live exclusively
   in synced simulation paths. Every guRNG call and every
   projMemPool.alloc chained spawn (CWreckProjectile smoke,
   CExpGenSpawner, FireBallProjectile sparks) sits in a ctor, Init()
   or Update(); gsRNG is not referenced by any particle code; Draw()
   bodies write only render-side members that sim code never reads.
   The draw path therefore cannot reach synced state, and neither
   reordering, deduplicating nor threading it can diverge the
   simulation.

3. ProjectileReflectionMinRadius (default 0 = off): optionally skip
   non-model alpha particles below a given draw radius in the water
   reflection pass, decided in UpdateDrawFlags before the reflection
   camera InView test. Small particles are barely visible in a wavy
   reflection, while the reflection pass pays the full fill/sort/
   quad-generation cost for them.

Also hoists the loop-invariant camera direction reads out of
CSimpleParticleSystem's non-directional draw loop.

With threading on, DrawAlpha(DS) drops ~390 us -> ~130 us and the
below-water particle pass ~1.15 ms -> ~660 us. Benchmark frame
average: 5.82 ms -> 5.22 ms; combined with the previous commit,
7.51 ms -> 5.22 ms (-30% frame time, 133 -> 192 fps) at 3x
battle-level effect load.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* ProjectileDrawer: radix-sort the alpha particles

The back-to-front ordering used std::sort with a comparison predicate;
at battle-level counts (~20k sortable alpha particles) this cost
~184 us mean and up to ~470 us per fill, with the input-dependent
variance inherent to comparison sorting.

drawOrder and view distance are now packed into a single 64-bit key at
filter time (drawOrder as a biased int in the high half, distance
mapped to an order-preserving inverted integer in the low half; an
ascending sort yields drawOrder asc, distance desc as before), sorted
with a stable LSD radix sort: 8-bit digits, passes whose digit is
constant across all keys are skipped (with drawOrder unused the whole
high half is), and fills below 1024 elements use a plain
single-integer-compare std::sort, which wins at that size. Cost is
linear and input-independent, so the worst-case spikes disappear:
measured 31 us mean / ~75 us max (down from 184/470), with the key
packing moving a fixed ~50 us into the filter pass - a net ~100 us
saved per fill.

Two deliberate semantic notes: exactly-coincident particles (equal
drawOrder and distance, e.g. multi-part effects spawned at the same
point) are now tiebroken by stable fill order instead of pointer
address, which is frame-coherent; and NaN distances can no longer feed
a comparison predicate that violates strict weak ordering (undefined
behavior for std::sort) - the radix path handles any bit pattern.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* ProjectileDrawer: reuse geometry for refraction, balance fill chunks

Two refinements adopted from bruno-dasilva's draft beyond-all-reason#3037, which
independently arrived at the same overall design as this branch (pass
geometry reuse, threaded quad fill with the same two serial-exception
classes, radix sort):

- The water refraction pass views the same particles from the same
  player camera as the main view, so it now re-submits the geometry
  saved by the below-water pass with its own clip plane instead of
  doing a filter/sort/fill of the underwater-flagged subset. Visually
  equivalent: the clip plane discards everything above the surface.
  Water maps now build alpha geometry twice per frame (main view +
  mirrored reflection camera) instead of four times. Gated by the
  existing ProjectileDrawReuseWaterPasses config.

- Fill chunks are oversubscribed 4x relative to the worker count.
  Per-particle draw cost varies wildly (a smoke trail emits dozens of
  quads, a small flash one), and with one chunk per worker a single
  heavy chunk gated the whole dispatch. MTChunk mean dropped from
  ~68 us to ~26 us and benchmark 1%-low fps improved 111 -> 125.

Benchmark frame average 5.18 ms -> 4.98 ms (193 -> 201 fps); total
against the pre-series baseline 7.51 ms -> 4.98 ms (-34% frame time)
at 3x battle-level effect load on a water map.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* System: extract the radix sort into a reusable utility

Move the stable LSD radix sort out of ProjectileDrawer into
System/RadixSort.h, templated on element type and key projection, as
suggested by review on beyond-all-reason#3037 for the equivalent code there. No
functional change: ProjectileDrawer sorts by the packed 64-bit
particle key exactly as before, and the caller-owned scratch buffer
keeps its capacity across frames.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* ProjectileDrawer: thread the shadow-transparent particle fill

DrawShadowTransparent runs whenever shadows are enabled at all (any
shadowConfig > 0 sets SHADOWGEN_BIT_PROJ), and filled shadow-camera
billboards for every shadow-casting particle serially on the render
thread. Route it through the same FillParticleGeometry helper as the
alpha pass: the multiplicative shadow blend is order-independent, so
the list needs no sorting and chunk merge order does not matter; the
mtDrawSafe exceptions are handled by the shared helper as usual.

Gated by the existing ProjectileDrawThreadedFill config; the fill is
Tracy-zoned as DrawShadowTransparent(Fill).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* ProjectileDrawer: hoist per-frame invariants out of UpdateDrawFlags

The per-particle camera loop re-evaluated per-frame constants for
every projectile: IWater::GetWater()->CanDrawReflectionPass() (a
virtual call through a unique_ptr, once per particle per frame), the
projectile shadow-gen bit, the camera pointer lookups, and repeated
GetDrawRadius() calls. Hoist them ahead of the for_mt and unroll the
three-camera loop into straight-line per-camera blocks.

Flag results and sort distances are unchanged; the reflection
min-radius guard is re-expressed equivalently (hasModel || radius >=
min instead of skip-if !hasModel && radius < min).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* ProjectileDrawer: declare the on/off toggles as CONFIG(bool)

ProjectileDrawThreadedFill and ProjectileDrawReuseWaterPasses are pure
on/off switches; declare them as bool configs (read via GetBool)
instead of int. Stored 1/0 values from existing configs parse the
same, so nothing user-facing changes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…all-reason#2797)

So we successfully update with the f-1 value, not the f value; and OwnerMoved() can be called with the proper p(f) - p(f-1) vector
Via a dummy function so that people trying to add it
in the future are likely to actually stumble upon it.
…2878)

Obsoleted by resource packs. No logic change, internals only.

Co-authored-by: TarnishedKnight <lostsquirrel43@gmail.com>
* Add KeyBindingsChanged callin

* Emit KeyBindingsChanged from keybinding commands
* Register key names for the keys that had none

* Correct the key listing in the ui-keys reference

* Deprecate KEYSYMS in favour of Spring.GetKeyCode
…ond-all-reason#3132)

* fix issue where ships could get incorrect pathmap blocking zones with underwater structures near coast lines.

* rename isSubmersible to hasUnderwaterCollision
* Stop unbind from removing longer chains that share the last key

* Match unbind keychains exactly instead of by fit()

---------

Co-authored-by: TarnishedKnight <lostsquirrel43@gmail.com>
…ed) (beyond-all-reason#3191)

* Nano Particles: add NanoParticleUpdate engine callin

Batched, unsynced lifecycle events for nano particles, so deferred-lighting
widgets can light them without polling. Nothing emits these yet; the standalone
nano particle effect added in the following commit owns the batching and the
sampling that decides which particles are reported.

Events are passed as one flat numeric array of 13-entry records rather than a
table per event, because a per-event table would dominate the cost of the callin
at the rates involved.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Nano Particles: add standalone nano particle effect

Adds an optional nano particle effect that renders build spray as
shader-generated 3D shapes with an additive halo, behind NanoParticlesGL4
(default off). Ported from BAR's gfx_nano_particles_gl4 gadget.

The effect is not a projectile. Its particles have no projectile id, take no
part in collision or quadfield work, are never handed to Lua as projectiles, and
are not serialised; motion is analytic, so the shader reconstructs position from
start/velocity/frame and the CPU only touches a particle when it homes or has to
clear terrain. All of it lives in rts/Rendering/Env/NanoParticles:

  NanoParticleConfig  every tunable, in one place
  NanoParticleDefs    the PODs the other three share
  NanoParticleSystem  the particle store, homing, ground clamp, LuaUI batching
  NanoParticleEmitter how much spray a builder produces, and reclaim bursts
  NanoParticleRenderer shaders, buffers, culling, draw

Legacy nano spray is untouched. NanoProjectile, ProjectileDrawer and
NanoPieceCache have no diff at all; when the effect is off, or no shader path is
usable, emission falls through to CNanoProjectile exactly as before. The
simulation cannot tell the difference either way: a work tick still polls
QueryNanoPiece once and draws one synced RNG value, and everything the effect
adds runs off the unsynced RNG, as nano spray already did.

Beyond the shader look the effect also carries, each behind its own setting:

* emission proportional to buildSpeed * buildPower rather than one particle per
  work tick, so a builder's spray tracks the work it is doing instead of its
  nano piece count, spread round-robin over its pieces
* NanoParticlesHoming, particles following moving targets
* NanoParticlesGroundClamp, particles routed over intervening terrain
* NanoParticlesReclaimBurst, a burst when reclaiming a unit finishes, sized by
  metal cost and split across the builders that contributed
* NanoParticlesUpdateLuaUI, the batched callin added in the previous commit

The per-unit emission accumulators and the reclaim contributor tracking are
owned by the emitter rather than by CBuilder/CUnit: they are unsynced
presentation state, they must not be serialised, and no part of the sim needs to
know they exist. No sim class gains a member.

The renderer is heap-allocated behind a pointer, as the other GL-owning drawers
are. A VBO's constructor calls VBO::IsSupported(), which latches the GLAD
extension flags into function-local statics on its first call; constructing one
before GLAD has loaded latches them all to false and silently turns every VBO in
the process into a no-op.

Particles show on the minimap as the legacy ones do, reusing the vertex arrays
the world pass already filtered so it costs one walk and no visibility work, and
filling the shared projectile minimap buffer rather than adding a draw of its
own.

The spawn gate is the effect's own rather than the legacy proportional one,
which throttles from the first particle and so makes emission approach the
budget asymptotically instead of scaling with NanoParticlesRate.

Tunables are named and documented in NanoParticleConfig.h instead of being
literals spread through the sources. The visual subset reaches both shader paths
as uniforms, so the geometry and instanced renderers cannot drift apart.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Nano Particles: changelog for the standalone effect

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* added `Engine.FeatureSupport.nanoParticlesGL4` boolean, so games can detect that the engine has the standalone nano particle effect and retire their own Lua implementation of it.

* moved changelog to: doc\pr-changelogs\3191.md

* Nano Particles: added configbool: NanoParticlesTargetLostFade

"Nano particles fade out and shrink when the unit they were aimed at is destroyed, cancelled, or finished, instead of flying on into nothing"

(ported over this feature from the gadget as well)

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
…n modifiers (beyond-all-reason#3336)

* apply terrain speed mod to max speed for drag calculations
* Sim: avoid UB in float-to-short heading casts

* Move the cast into a TAAngleToShort helper

* Cast up front through FloatToHeading, assert the range
…ll-reason#3268)

The early block check added in beyond-all-reason#2557 runs on a position snapped to 8 elmos,
while the build itself uses Pos2BuildPos, which snaps to 16 and adds half a
square for an odd footprint. For an odd footprint the check therefore
evaluates a footprint 8 elmos away from the one that would be placed, and can
drop a build order the build itself would accept.

Pos2BuildPos is idempotent, so the later call at MoveInBuildRange is left
alone rather than moved, keeping the change to the check itself.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.