Skip to content

feat: Terrain conforming particles - #3245

Open
stephanmeesters wants to merge 7 commits into
TheSuperHackers:mainfrom
stephanmeesters:feat/terrain-conforming-particles
Open

feat: Terrain conforming particles#3245
stephanmeesters wants to merge 7 commits into
TheSuperHackers:mainfrom
stephanmeesters:feat/terrain-conforming-particles

Conversation

@stephanmeesters

@stephanmeesters stephanmeesters commented Sep 1, 2026

Copy link
Copy Markdown

Merge by rebase

Summary

Makes it possible to render particles correctly on uneven terrain.

  • Can be enabled for a particle system by using the INI option IsGroundAligned = CONFORMING.
  • The feature can be enabled by using the define ENABLE_TERRAIN_CONFORMING_PARTICLES.
  • Rendering ground-conforming particles correctly on bridges is out of scope.

Method

Testing

How-to

Using a test build

See this test build.

Using data patch

Building Patch2 with a modification to particle systems see the data PR.

Using code

Alternatively, you can cherry-pick this commit which force-enables the right particles:

git cherry-pick f8a9ce11247d4b5d8b28e28a0e0feb0aa7dd1667

Performance impact

The test build was used to test a moderately heavy scene (3 nuke cannons + 3 nukes shooting at a hill in Alpine Assault) using a Macbook Pro 2013 base spec aka Potato, which showed a 5.5% average FPS loss.

Graphics Settings Resolution Particle Rendering FPS
Lowest 800x600 Original 57
Lowest 800x600 Conforming 56
Very High 800x600 Original 53
Very High 800x600 Conforming 50
Low 1920x1080 Original 24
Low 1920x1080 Conforming 24
Very High 1920x1080 Original 22
Very High 1920x1080 Conforming 20
Very High 3072x1920 Original 9
Very High 3072x1920 Conforming 8

Image gallery

Screenshot From 2026-09-01 19-46-07
More Screenshot From 2026-09-01 19-46-52 Screenshot From 2026-08-22 17-29-21

Todo

  • Some more polish on code comments
  • Add pull ID to commits
  • Clean up commits after review
  • Replicate in Generals

@stephanmeesters stephanmeesters added Enhancement Is new feature or request Gen Relates to Generals ZH Relates to Zero Hour Rendering Is Rendering related labels Sep 1, 2026
@stephanmeesters
stephanmeesters force-pushed the feat/terrain-conforming-particles branch 2 times, most recently from b0d2d9b to 4b2af0a Compare September 6, 2026 19:24
@stephanmeesters
stephanmeesters force-pushed the feat/terrain-conforming-particles branch 3 times, most recently from 9491602 to 11b2e6b Compare September 13, 2026 11:56
@stephanmeesters
stephanmeesters force-pushed the feat/terrain-conforming-particles branch from 11b2e6b to 60d17b0 Compare September 13, 2026 12:10
@stephanmeesters
stephanmeesters marked this pull request as ready for review September 13, 2026 12:16
@qodo-free-for-open-source-projects

Copy link
Copy Markdown

PR Summary by Qodo

Add terrain-conforming particle rendering

✨ Enhancement ⚙️ Configuration changes 🕐 40+ Minutes

Grey Divider

AI Description

• Adds INI-selectable terrain-conforming particles that follow uneven terrain.
• Recursively coarsens flat regions and batches generated meshes for practical rendering
 performance.
• Feature-gates the mode, exposes rendering statistics, and leaves bridge conformance out of scope.
Diagram

graph TD
  A["Particle INI"] --> B["Alignment enum"] --> C{"Conforming?"} -->|Yes| D["Terrain renderer"] --> E["Height map"] --> F["Adaptive mesh"] --> G["DX8 draw"]
  C -->|No| H["Point group"] --> G
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Extend the scorch/decal renderer
  • ➕ Reuses an established terrain-overlay implementation.
  • ➕ Could consolidate terrain clipping and topology handling across overlay types.
  • ➖ Existing scorch rendering is not structured around particle arrays or particle batching.
  • ➖ Adapting it may couple short-lived particle behavior to persistent decal lifecycle assumptions.
2. Project particles in the GPU
  • ➕ Avoids CPU-side recursive tessellation and dynamic mesh construction.
  • ➕ Could reduce geometry generation costs for dense particle scenes.
  • ➖ Requires shader and depth/terrain data capabilities that may not fit the legacy DX8 pipeline.
  • ➖ Introduces broader renderer compatibility and visual-consistency risks.

Recommendation: The PR's dedicated batched renderer is the best fit for the existing DX8 architecture because it preserves current particle data flow while matching terrain topology and reducing flat-region geometry. Reusing shared terrain-overlay tessellation utilities could be considered later, but directly extending scorch rendering or introducing GPU projection would create substantially broader coupling and compatibility work.

Files changed (14) +612 / -32

Enhancement (12) +605 / -32
ParticleSys.hExpose conforming particle alignment +5/-1

Expose conforming particle alignment

• Adds CONFORMING to the particle alignment enum and INI name table. Exposes alignment access and classifies planar and conforming particles together as field particles.

Core/GameEngine/Include/GameClient/ParticleSys.h

ParticleSys.cppApply conforming alignment limits and fallback +7/-1

Apply conforming alignment limits and fallback

• Uses the shared field-particle predicate for area-effect limits. Downgrades conforming alignment to XY-planar when the feature switch is disabled.

Core/GameEngine/Source/GameClient/System/ParticleSys.cpp

W3DParticleSys.hIntegrate terrain rendering into particle batches +5/-2

Integrate terrain rendering into particle batches

• Adds ownership of the terrain-particle renderer and records full alignment and bounding-box state for each batch instead of a billboard boolean.

Core/GameEngineDevice/Include/W3DDevice/GameClient/W3DParticleSys.h

W3DTerrainParticle.hDeclare adaptive terrain particle renderer +98/-0

Declare adaptive terrain particle renderer

• Introduces the renderer interface, mesh buffers, terrain lookup state, particle attributes, and recursive subdivision helpers used to draw particle textures over terrain.

Core/GameEngineDevice/Include/W3DDevice/GameClient/W3DTerrainParticle.h

WorldHeightMap.hExpose fast terrain sampling helpers +18/-0

Expose fast terrain sampling helpers

• Adds logical terrain bounds, unchecked height lookup, and regional flatness detection for adaptive particle mesh generation.

Core/GameEngineDevice/Include/W3DDevice/GameClient/WorldHeightMap.h

W3DParticleSys.cppRoute conforming batches through terrain meshes +47/-28

Route conforming batches through terrain meshes

• Creates and manages the terrain renderer, batches particles by complete alignment mode, and forwards conforming batches with their visible bounds. Existing billboard and planar modes continue through PointGroupClass.

Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DParticleSys.cpp

W3DTerrainParticle.cppImplement terrain-conforming particle meshes +375/-0

Implement terrain-conforming particle meshes

• Builds terrain-matching particle geometry by recursively subdividing uneven regions and collapsing flat regions into quads. It computes rotated UVs, culls fully transparent triangles, batches dynamic buffers, configures render state, and submits DX8 draw calls.

Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DTerrainParticle.cpp

WorldHeightMap.cppDetect flat height-map regions +15/-0

Detect flat height-map regions

• Implements a direct height-buffer scan that determines whether every terrain vertex within a bounded region has the same elevation.

Core/GameEngineDevice/Source/W3DDevice/GameClient/WorldHeightMap.cpp

statistics.cppTrack terrain particle rendering load +24/-0

Track terrain particle rendering load

• Records terrain-particle triangle and batch counts, resets them at frame start, and publishes the completed frame's values.

Core/Libraries/Source/WWVegas/WW3D2/statistics.cpp

statistics.hExpose terrain particle statistics +4/-0

Expose terrain particle statistics

• Declares terrain-particle statistics APIs and adds a recording macro for renderer draw batches.

Core/Libraries/Source/WWVegas/WW3D2/statistics.h

W3DDisplay.hReserve terrain particle debug display entry +1/-0

Reserve terrain particle debug display entry

• Adds a dedicated debug-display slot for terrain-conforming particle metrics.

GeneralsMD/Code/GameEngineDevice/Include/W3DDevice/GameClient/W3DDisplay.h

W3DDisplay.cppDisplay terrain particle draw statistics +6/-0

Display terrain particle draw statistics

• Shows the previous frame's terrain-particle triangle and draw-batch counts in the runtime debug statistics overlay.

GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DDisplay.cpp

Other (2) +7 / -0
GameDefines.hAdd terrain-particle feature switch +5/-0

Add terrain-particle feature switch

• Defines ENABLE_TERRAIN_CONFORMING_PARTICLES with an enabled-by-default value so builds can disable the new rendering behavior.

Core/GameEngine/Include/Common/GameDefines.h

CMakeLists.txtCompile the terrain particle renderer +2/-0

Compile the terrain particle renderer

• Adds the new W3DTerrainParticle header and implementation to the GameEngineDevice source list.

Core/GameEngineDevice/CMakeLists.txt

@greptile-apps

greptile-apps Bot commented Sep 13, 2026

Copy link
Copy Markdown

Greptile Summary

This PR adds an opt-in terrain-conforming particle alignment, routes conforming particle batches through a new terrain mesh generator, adds terrain-bound and flatness helpers, and exposes rendering statistics.

  • Extends particle alignment parsing and field-particle accounting with CONFORMING.
  • Generates terrain-matching particle geometry through recursive subdivision and dynamic vertex/index buffers.
  • Adds terrain-particle triangle and draw-call diagnostics.
  • The mesh generator needs bounds protection for its fixed lookup buffer and validation of particle sizes.

Confidence Score: 4/5

The PR should not merge until large conforming particles can no longer index beyond the terrain vertex lookup buffer.

The new renderer maps an area-sized terrain-grid index into a fixed 32,768-entry lookup without enforcing that the particle bounds fit, creating a reachable memory-corruption path; it also needs a smaller guard against zero and invalid particle sizes.

Files Needing Attention: Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DTerrainParticle.cpp

Important Files Changed

Filename Overview
Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DTerrainParticle.cpp Implements terrain mesh generation and rendering, but unchecked lookup indexing can corrupt memory and unchecked sizes can generate invalid UVs.
Core/GameEngineDevice/Include/W3DDevice/GameClient/W3DTerrainParticle.h Defines the new renderer and fixed-capacity mesh storage whose lookup capacity is not tied to terrain-bound area.
Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DParticleSys.cpp Integrates conforming particles into existing batching while preserving texture, shader, and alignment batch boundaries.
Core/GameEngine/Include/GameClient/ParticleSys.h Adds the conforming alignment value, INI name, and alignment accessors consistently.
Core/GameEngineDevice/Source/W3DDevice/GameClient/WorldHeightMap.cpp Adds a bounded terrain-flatness query used to coarsen generated particle meshes.
Core/Libraries/Source/WWVegas/WW3D2/statistics.cpp Adds terrain-particle counters to the existing per-frame statistics lifecycle.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart LR
    A[Particle systems] --> B[Compatible particle batch]
    B --> C{Alignment}
    C -->|Billboard or planar| D[PointGroup renderer]
    C -->|Conforming| E[Terrain bounds calculation]
    E --> F[Flatness test and recursive subdivision]
    F --> G[Vertex lookup and index generation]
    G --> H[Dynamic DX8 buffers]
    H --> I[Terrain-conforming overlay]
Loading
Prompt To Fix All With AI
### Issue 1
Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DTerrainParticle.cpp:203-205
**Lookup Buffer Overflow**

When a conforming particle covers more than 32,768 terrain grid points, `gridLocation` can exceed the fixed 32,768-entry `m_vertexLookup`. Particle size has no upper bound, and the bounds are clipped only to the map and visible area, so a sufficiently large particle can access memory outside the lookup buffer, corrupt memory, or crash the renderer.

### Issue 2
Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DTerrainParticle.cpp:221-222
**Invalid Particle Size Division**

A zero-sized conforming particle can reach these UV calculations because particle parsing and runtime size updates do not require a positive value. If its bounds still form a quad, dividing by `2.0f * particle.size` produces infinite or NaN texture coordinates, resulting in malformed or unpredictable rendering. Non-positive and non-finite sizes should be skipped before generating geometry.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Reviews (1): Last reviewed commit: "feat(terrainparticle): Add define guard ..." | Re-trigger Greptile

Comment on lines +203 to +205
{
const Int gridLocation = (y - particle.bounds.lo.y) * particle.bounds.width() + x - particle.bounds.lo.x;
UnsignedShort& index = m_vertexLookup[gridLocation];

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Lookup Buffer Overflow

When a conforming particle covers more than 32,768 terrain grid points, gridLocation can exceed the fixed 32,768-entry m_vertexLookup. Particle size has no upper bound, and the bounds are clipped only to the map and visible area, so a sufficiently large particle can access memory outside the lookup buffer, corrupt memory, or crash the renderer.

Knowledge Base Used: Rendering and video devices

Prompt To Fix With AI
This is a comment left during a code review.
Path: Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DTerrainParticle.cpp
Line: 203-205

Comment:
**Lookup Buffer Overflow**

When a conforming particle covers more than 32,768 terrain grid points, `gridLocation` can exceed the fixed 32,768-entry `m_vertexLookup`. Particle size has no upper bound, and the bounds are clipped only to the map and visible area, so a sufficiently large particle can access memory outside the lookup buffer, corrupt memory, or crash the renderer.

**Knowledge Base Used:** [Rendering and video devices](https://app.greptile.com/thesuperhackers/-/custom-context/knowledge-base/thesuperhackers/generalsgamecode/-/docs/rendering-and-video-devices.md)

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Comment on lines +221 to +222
vertex.u1 = 0.5f - localX / (2.0f * particle.size);
vertex.v1 = 0.5f - localY / (2.0f * particle.size);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Invalid Particle Size Division

A zero-sized conforming particle can reach these UV calculations because particle parsing and runtime size updates do not require a positive value. If its bounds still form a quad, dividing by 2.0f * particle.size produces infinite or NaN texture coordinates, resulting in malformed or unpredictable rendering. Non-positive and non-finite sizes should be skipped before generating geometry.

Prompt To Fix With AI
This is a comment left during a code review.
Path: Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DTerrainParticle.cpp
Line: 221-222

Comment:
**Invalid Particle Size Division**

A zero-sized conforming particle can reach these UV calculations because particle parsing and runtime size updates do not require a positive value. If its bounds still form a quad, dividing by `2.0f * particle.size` produces infinite or NaN texture coordinates, resulting in malformed or unpredictable rendering. Non-positive and non-finite sizes should be skipped before generating geometry.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

@qodo-free-for-open-source-projects

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (6) 📘 Rule violations (0) 📎 Requirement gaps (1) 📜 Skill insights (0)

Grey Divider


Action required

1. Field effects vanish near view edges 📎 Requirement gap ≡ Correctness
Description
W3DTerrainParticle::setBoundingBox stores floor(worldMaximum / MAP_XY_FACTOR) as the exclusive
IRegion2D::hi endpoint, while mesh generation only addresses vertices through hi - 1. When a
conforming radiation or toxin field reaches the positive X or Y edge of the visible box,
intersection drops the final terrain row or column and can leave fewer than two vertices to render,
including active portions on adjacent high terrain.
Code

Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DTerrainParticle.cpp[R372-374]

+	m_terrainInViewBounds.hi.x = REAL_TO_INT_FLOOR((worldBoundingBox.Center.X + worldBoundingBox.Extent.X) / MAP_XY_FACTOR);
+	m_terrainInViewBounds.lo.y = REAL_TO_INT_FLOOR((worldBoundingBox.Center.Y - worldBoundingBox.Extent.Y) / MAP_XY_FACTOR);
+	m_terrainInViewBounds.hi.y = REAL_TO_INT_FLOOR((worldBoundingBox.Center.Y + worldBoundingBox.Extent.Y) / MAP_XY_FACTOR);
Evidence
IRegion2D::width() measures hi - lo, generated quads address the final vertex as hi - 1, and
the renderer discards intersections containing fewer than two grid vertices, establishing that these
bounds are half-open. calcTerrainBounds() accounts for that convention with ceil(max) + 1,
whereas setBoundingBox() uses an unadjusted floor before intersecting the regions, proving that
boundary vertices—and therefore required visible portions of active radiation and toxin fields
across affected elevations—can be excluded.

Render Radiation and Toxin Fields Correctly Across Uneven Terrain
Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DTerrainParticle.cpp[124-126]
Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DTerrainParticle.cpp[183-186]
Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DTerrainParticle.cpp[369-374]
Core/Libraries/Include/Lib/BaseType.h[580-618]
Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DTerrainParticle.cpp[277-288]
Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DTerrainParticle.cpp[369-375]
Core/Libraries/Include/Lib/BaseType.h[580-589]
Core/Libraries/Include/Lib/BaseType.h[616-618]
Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DScorch.cpp[188-193]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The terrain-view maximum is converted with `floor` and then used as an exclusive `IRegion2D` endpoint, causing conforming particle meshes to omit terrain cells at the positive visible boundaries.

## Fix Focus Areas
- Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DTerrainParticle.cpp[277-288]
- Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DTerrainParticle.cpp[369-375]

## Recommended Fix
Convert each world-space maximum to a conservative exclusive terrain-vertex endpoint, following the ceiling-and-increment convention used by `calcTerrainBounds`: use `REAL_TO_INT_CEIL(max / MAP_XY_FACTOR) + 1` for both `hi.x` and `hi.y`. Keep the lower bounds floored, clamp the resulting region to the map's logical bounds, and verify that particles crossing either positive view edge retain all visible terrain quads.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Terrain particles render twice as large 🐞 Bug ≡ Correctness
Description
W3DTerrainParticle treats Particle::getSize() as a half-width even though PointGroupClass
treats the same value as the quad's full width. Every conforming system therefore covers twice the
intended width and height, changing the appearance and area of existing particle definitions when
they select the new alignment.
Code

Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DTerrainParticle.cpp[R221-222]

+	vertex.u1 = 0.5f - localX / (2.0f * particle.size);
+	vertex.v1 = 0.5f - localY / (2.0f * particle.size);
Evidence
Point-group quad basis coordinates range from -0.5 to +0.5 and are multiplied directly by the
unchanged particle size, yielding total width size. The new renderer instead maps texture edges to
offsets of plus or minus size and calculates bounds with that same radius, yielding total width `2
* size`.

Core/Libraries/Source/WWVegas/WW3D2/pointgr.cpp[1172-1184]
Core/Libraries/Source/WWVegas/WW3D2/pointgr.cpp[1457-1462]
Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DParticleSys.cpp[253-266]
Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DTerrainParticle.cpp[119-124]
Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DTerrainParticle.cpp[217-223]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Terrain-conforming particles interpret the existing particle size as a half-width, making them twice as wide and tall as equivalent point-group particles.

## Fix Focus Areas
- Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DTerrainParticle.cpp[119-124]
- Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DTerrainParticle.cpp[217-223]

## Recommended Fix
Treat size as the full quad width. Compute the rotated projected radius from `size * 0.5f`, and map texture coordinates so offsets of `-size/2` and `+size/2` correspond to the texture edges.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. Large particle fields corrupt memory 🐞 Bug ☼ Reliability
Description
W3DTerrainParticle::addVertex uses a row-major offset within the particle's complete
terrain-region bounds to index m_vertexLookup, even though that vector has only MAX_VERTICES
entries and batch flushing limits generated vertices rather than covered grid positions. A
configuration-driven particle spanning more than 32,768 terrain positions can reach its far-corner
offset on a flat region that bypasses recursive subdivision and emits only a four-vertex quad,
accessing beyond the allocation before flushing can intervene.
Code

Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DTerrainParticle.cpp[R204-205]

+	const Int gridLocation = (y - particle.bounds.lo.y) * particle.bounds.width() + x - particle.bounds.lo.x;
+	UnsignedShort& index = m_vertexLookup[gridLocation];
Evidence
The lookup is fixed at MAX_VERTICES, but addVertex derives its index from the width and height
of particle.bounds. Those bounds are constrained by the map and visible regions rather than the
render-batch vertex limit, and configurable particle size can therefore make their area exceed the
lookup capacity; because flat terrain bypasses recursive subdivision and includes the region's far
corner while generating only one quad, neither the generated vertex count nor the batch-flush logic
bounds that row-major lookup offset.

Core/GameEngineDevice/Include/W3DDevice/GameClient/W3DTerrainParticle.h[30-35]
Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DTerrainParticle.cpp[70-75]
Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DTerrainParticle.cpp[149-154]
Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DTerrainParticle.cpp[183-186]
Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DTerrainParticle.cpp[202-205]
Core/GameEngineDevice/GameClient/W3DTerrainParticle.h[76-81]
Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DTerrainParticle.cpp[277-288]
Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DParticleSys.cpp[253-266]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The fixed 32,768-entry vertex lookup is indexed by positions within an individual particle's complete terrain region rather than by generated vertex count. A particle region larger than `MAX_VERTICES` can therefore access beyond the lookup allocation even when flat-terrain coarsening emits only one quad, before vertex or index batch flushing can intervene.

## Fix Focus Areas
- Core/GameEngineDevice/Include/W3DDevice/GameClient/W3DTerrainParticle.h[76-81]
- Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DTerrainParticle.cpp[124-137]
- Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DTerrainParticle.cpp[202-224]

## Recommended Fix
Before drawing each particle, safely calculate its terrain-region area and resize the lookup to that area, guarding against integer overflow and excessive allocation, or replace it with a bounds-safe sparse lookup. Alternatively, subdivide oversized regions and give each subdivision an independently bounded lookup; retain the separate vertex/index batch limits and flushing logic.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

4. Later textures gain transparent edges 🐞 Bug ≡ Correctness
Description
updateSettings() changes the global texture-stage U and V address modes to border sampling with a
transparent border, but render() only reapplies the texture's filtering afterward. A regular
particle batch or another renderer following a conforming batch can therefore inherit border
sampling and display transparent or missing texture regions outside the normalized coordinate range.
Code

Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DTerrainParticle.cpp[R331-333]

+	DX8Wrapper::Set_DX8_Texture_Stage_State(0, D3DTSS_ADDRESSU, D3DTADDRESS_BORDER);
+	DX8Wrapper::Set_DX8_Texture_Stage_State(0, D3DTSS_ADDRESSV, D3DTADDRESS_BORDER);
+	DX8Wrapper::Set_DX8_Texture_Stage_State(0, D3DTSS_BORDERCOLOR, 0x00000000);
Evidence
The renderer directly installs border addressing through DX8Wrapper, whose texture-stage state
persists globally. Its restoration path invokes only Get_Filter().Apply(0), while
TextureClass::Apply() and the filter do not reset address modes or border color.

Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DTerrainParticle.cpp[142-146]
Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DTerrainParticle.cpp[329-333]
Core/Libraries/Source/WWVegas/WW3D2/texture.cpp[920-968]
Core/Libraries/Source/WWVegas/WW3D2/dx8wrapper.h[878-900]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Terrain-particle rendering installs persistent border-addressing states and fails to restore them, allowing the state to affect later draws.

## Fix Focus Areas
- Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DTerrainParticle.cpp[101-147]
- Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DTerrainParticle.cpp[329-334]

## Recommended Fix
Save the previous U and V addressing modes and border color before changing them, then restore all three states after the terrain batch finishes. If the wrapper cannot query state, use an established scoped render-state mechanism or explicitly restore the engine's documented defaults.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


5. Zero-size particles render unpredictably 🐞 Bug ☼ Reliability
Description
addVertex() divides texture-coordinate offsets by 2.0f * particle.size without rejecting a zero
size. Particle definitions and size-rate updates can produce zero without validation, so an off-grid
zero-size conforming particle can submit infinite or undefined texture coordinates to the graphics
device.
Code

Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DTerrainParticle.cpp[R221-223]

+	vertex.u1 = 0.5f - localX / (2.0f * particle.size);
+	vertex.v1 = 0.5f - localY / (2.0f * particle.size);
+	m_outcodes[index] = getUVOutcode(vertex.u1, vertex.v1);
Evidence
Particle size starts from unconstrained template values and is updated by an unclamped addition,
while template validation does not enforce positivity. The conforming path divides by that value,
and its outcode tests cannot reliably reject non-finite coordinates because they only perform
ordinary less-than and greater-than comparisons.

Core/GameEngine/Source/GameClient/System/ParticleSys.cpp[419-421]
Core/GameEngine/Source/GameClient/System/ParticleSys.cpp[1873-1883]
Core/GameEngine/Source/GameClient/System/ParticleSys.cpp[2713-2719]
Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DTerrainParticle.cpp[46-57]
Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DTerrainParticle.cpp[112-126]
Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DTerrainParticle.cpp[217-223]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The conforming renderer divides by particle size even though the particle system permits that value to be zero.

## Fix Focus Areas
- Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DTerrainParticle.cpp[112-126]
- Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DTerrainParticle.cpp[217-223]

## Recommended Fix
Reject particles whose size is zero or nonpositive before calculating bounds or texture coordinates. Keep the check local to the conforming renderer so existing point-group behavior remains unchanged.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


6. Rotated particle edges disappear 🐞 Bug ≡ Correctness
Description
The conforming rendering path accepts particles only after the existing visibility prepass expands
the visible box by psize on each axis. Its projected radius is instead `size * (abs(cosine) +
abs(sine))`, so at 45 degrees geometry can extend about 1.414 times the particle size and is never
submitted when its center lies in that omitted band.
Code

Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DParticleSys.cpp[R457-463]

+		if (m_batchParticleAlignment == ParticleSystemInfo::PARTICLE_ALIGNMENT_CONFORMING)
+		{
+			m_terrainParticles->setTexture(m_batchTexture.Peek());
+			m_terrainParticles->setShader( shader );
+			m_terrainParticles->setArrays( m_posBuffer, m_RGBABuffer, m_sizeBuffer, m_angleBuffer, pointCount );
+			m_terrainParticles->setBoundingBox( m_batchBoundingBox );
+			m_terrainParticles->render();
Evidence
The new branch sends conforming systems to a renderer whose terrain bounds account for rotation. The
common prepass, which runs first, uses a smaller unrotated extent and marks rejected particles as
culled, so those particles cannot reach the new branch.

Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DParticleSys.cpp[171-189]
Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DParticleSys.cpp[457-464]
Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DTerrainParticle.cpp[112-125]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Conforming particles use a rotated-square terrain footprint, but the shared prepass culls based on an unrotated `psize` extent. Particles whose rotated footprint overlaps visible terrain can be rejected before the terrain renderer can clip and draw them.

## Fix Focus Areas
- Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DParticleSys.cpp[171-189]
- Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DTerrainParticle.cpp[112-125]

## Recommended Fix
For conforming particle systems, expand the X/Y prepass culling radius by the rotated footprint radius calculated from the particle angle, or conservatively use `psize * sqrt(2)`. Leave the existing radius unchanged for other alignment modes.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


View review recommended (1)
7. Editing effects removes terrain alignment 🐞 Bug ⚙ Maintainability
Description
DebugWindowDialog::getSwitchFromSystem recognizes only XY-planar alignment and
updateSwitchToSystem maps an unchecked alignment control to billboard mode. Once the new
conforming enum value is loaded into the Particle Editor, editing any switches-dialog setting writes
that unchecked state back and replaces conforming alignment with billboard.
Code

Core/GameEngine/Include/GameClient/ParticleSys.h[440]

+		PARTICLE_ALIGNMENT_CONFORMING,
Evidence
The new enum introduces a third alignment state, while the editor continues to reduce alignment to
one Boolean. The switch dialog notifies its parent for every edit, and the parent update writes the
Boolean-derived alignment back to the model.

Core/GameEngine/Include/GameClient/ParticleSys.h[437-443]
Core/Tools/ParticleEditor/ParticleEditorDialog.cpp[1085-1114]
Core/Tools/ParticleEditor/CSwitchesDialog.cpp[74-85]
Core/Tools/ParticleEditor/CSwitchesDialog.cpp[120-127]
Core/Tools/ParticleEditor/ParticleEditorDialog.cpp[1594-1602]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The Particle Editor represents alignment as a boolean XY-plane checkbox. It cannot represent the new conforming enum value and writes the unchecked value back as billboard alignment during unrelated switch edits.

## Fix Focus Areas
- Core/GameEngine/Include/GameClient/ParticleSys.h[437-443]
- Core/Tools/ParticleEditor/ParticleEditorDialog.cpp[1085-1114]
- Core/Tools/ParticleEditor/CSwitchesDialog.cpp[74-127]

## Recommended Fix
Make the editor expose all alignment enum values, preferably with a three-value control, and write the selected enum value directly. At minimum, preserve conforming alignment when the legacy XY-plane switch is not changed.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Context sources
Review mode: 🧠 Deep: This introduces substantial new terrain-mesh rendering logic across multiple engine paths, batching, height-map access, shader/state management, and runtime configuration, creating many independent opportunities for subtle rendering, bounds, performance, and compatibility defects.

Grey Divider

Tip of the day
💡 Did you know, you can start a comment with 'qodo' or '@qodo' to chat about any finding

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

Comment on lines +372 to +374
m_terrainInViewBounds.hi.x = REAL_TO_INT_FLOOR((worldBoundingBox.Center.X + worldBoundingBox.Extent.X) / MAP_XY_FACTOR);
m_terrainInViewBounds.lo.y = REAL_TO_INT_FLOOR((worldBoundingBox.Center.Y - worldBoundingBox.Extent.Y) / MAP_XY_FACTOR);
m_terrainInViewBounds.hi.y = REAL_TO_INT_FLOOR((worldBoundingBox.Center.Y + worldBoundingBox.Extent.Y) / MAP_XY_FACTOR);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

1. Field effects vanish near view edges 📎 Requirement gap ≡ Correctness

W3DTerrainParticle::setBoundingBox stores floor(worldMaximum / MAP_XY_FACTOR) as the exclusive
IRegion2D::hi endpoint, while mesh generation only addresses vertices through hi - 1. When a
conforming radiation or toxin field reaches the positive X or Y edge of the visible box,
intersection drops the final terrain row or column and can leave fewer than two vertices to render,
including active portions on adjacent high terrain.
Agent Prompt
## Issue description
The terrain-view maximum is converted with `floor` and then used as an exclusive `IRegion2D` endpoint, causing conforming particle meshes to omit terrain cells at the positive visible boundaries.

## Fix Focus Areas
- Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DTerrainParticle.cpp[277-288]
- Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DTerrainParticle.cpp[369-375]

## Recommended Fix
Convert each world-space maximum to a conservative exclusive terrain-vertex endpoint, following the ceiling-and-increment convention used by `calcTerrainBounds`: use `REAL_TO_INT_CEIL(max / MAP_XY_FACTOR) + 1` for both `hi.x` and `hi.y`. Keep the lower bounds floored, clamp the resulting region to the map's logical bounds, and verify that particles crossing either positive view edge retain all visible terrain quads.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +221 to +222
vertex.u1 = 0.5f - localX / (2.0f * particle.size);
vertex.v1 = 0.5f - localY / (2.0f * particle.size);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

2. Terrain particles render twice as large 🐞 Bug ≡ Correctness

W3DTerrainParticle treats Particle::getSize() as a half-width even though PointGroupClass
treats the same value as the quad's full width. Every conforming system therefore covers twice the
intended width and height, changing the appearance and area of existing particle definitions when
they select the new alignment.
Agent Prompt
## Issue description
Terrain-conforming particles interpret the existing particle size as a half-width, making them twice as wide and tall as equivalent point-group particles.

## Fix Focus Areas
- Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DTerrainParticle.cpp[119-124]
- Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DTerrainParticle.cpp[217-223]

## Recommended Fix
Treat size as the full quad width. Compute the rotated projected radius from `size * 0.5f`, and map texture coordinates so offsets of `-size/2` and `+size/2` correspond to the texture edges.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +331 to +333
DX8Wrapper::Set_DX8_Texture_Stage_State(0, D3DTSS_ADDRESSU, D3DTADDRESS_BORDER);
DX8Wrapper::Set_DX8_Texture_Stage_State(0, D3DTSS_ADDRESSV, D3DTADDRESS_BORDER);
DX8Wrapper::Set_DX8_Texture_Stage_State(0, D3DTSS_BORDERCOLOR, 0x00000000);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remediation recommended

4. Later textures gain transparent edges 🐞 Bug ≡ Correctness

updateSettings() changes the global texture-stage U and V address modes to border sampling with a
transparent border, but render() only reapplies the texture's filtering afterward. A regular
particle batch or another renderer following a conforming batch can therefore inherit border
sampling and display transparent or missing texture regions outside the normalized coordinate range.
Agent Prompt
## Issue description
Terrain-particle rendering installs persistent border-addressing states and fails to restore them, allowing the state to affect later draws.

## Fix Focus Areas
- Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DTerrainParticle.cpp[101-147]
- Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DTerrainParticle.cpp[329-334]

## Recommended Fix
Save the previous U and V addressing modes and border color before changing them, then restore all three states after the terrain batch finishes. If the wrapper cannot query state, use an established scoped render-state mechanism or explicitly restore the engine's documented defaults.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +221 to +223
vertex.u1 = 0.5f - localX / (2.0f * particle.size);
vertex.v1 = 0.5f - localY / (2.0f * particle.size);
m_outcodes[index] = getUVOutcode(vertex.u1, vertex.v1);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remediation recommended

5. Zero-size particles render unpredictably 🐞 Bug ☼ Reliability

addVertex() divides texture-coordinate offsets by 2.0f * particle.size without rejecting a zero
size. Particle definitions and size-rate updates can produce zero without validation, so an off-grid
zero-size conforming particle can submit infinite or undefined texture coordinates to the graphics
device.
Agent Prompt
## Issue description
The conforming renderer divides by particle size even though the particle system permits that value to be zero.

## Fix Focus Areas
- Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DTerrainParticle.cpp[112-126]
- Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DTerrainParticle.cpp[217-223]

## Recommended Fix
Reject particles whose size is zero or nonpositive before calculating bounds or texture coordinates. Keep the check local to the conforming renderer so existing point-group behavior remains unchanged.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +204 to +205
const Int gridLocation = (y - particle.bounds.lo.y) * particle.bounds.width() + x - particle.bounds.lo.x;
UnsignedShort& index = m_vertexLookup[gridLocation];

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

3. Large particle fields corrupt memory 🐞 Bug ☼ Reliability

W3DTerrainParticle::addVertex uses a row-major offset within the particle's complete
terrain-region bounds to index m_vertexLookup, even though that vector has only MAX_VERTICES
entries and batch flushing limits generated vertices rather than covered grid positions. A
configuration-driven particle spanning more than 32,768 terrain positions can reach its far-corner
offset on a flat region that bypasses recursive subdivision and emits only a four-vertex quad,
accessing beyond the allocation before flushing can intervene.
Agent Prompt
## Issue description
The fixed 32,768-entry vertex lookup is indexed by positions within an individual particle's complete terrain region rather than by generated vertex count. A particle region larger than `MAX_VERTICES` can therefore access beyond the lookup allocation even when flat-terrain coarsening emits only one quad, before vertex or index batch flushing can intervene.

## Fix Focus Areas
- Core/GameEngineDevice/Include/W3DDevice/GameClient/W3DTerrainParticle.h[76-81]
- Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DTerrainParticle.cpp[124-137]
- Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DTerrainParticle.cpp[202-224]

## Recommended Fix
Before drawing each particle, safely calculate its terrain-region area and resize the lookup to that area, guarding against integer overflow and excessive allocation, or replace it with a bounds-safe sparse lookup. Alternatively, subdivide oversized regions and give each subdivision an independently bounded lookup; retain the separate vertex/index batch limits and flushing logic.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +457 to +463
if (m_batchParticleAlignment == ParticleSystemInfo::PARTICLE_ALIGNMENT_CONFORMING)
{
m_terrainParticles->setTexture(m_batchTexture.Peek());
m_terrainParticles->setShader( shader );
m_terrainParticles->setArrays( m_posBuffer, m_RGBABuffer, m_sizeBuffer, m_angleBuffer, pointCount );
m_terrainParticles->setBoundingBox( m_batchBoundingBox );
m_terrainParticles->render();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remediation recommended

6. Rotated particle edges disappear 🐞 Bug ≡ Correctness

The conforming rendering path accepts particles only after the existing visibility prepass expands
the visible box by psize on each axis. Its projected radius is instead `size * (abs(cosine) +
abs(sine))`, so at 45 degrees geometry can extend about 1.414 times the particle size and is never
submitted when its center lies in that omitted band.
Agent Prompt
## Issue description
Conforming particles use a rotated-square terrain footprint, but the shared prepass culls based on an unrotated `psize` extent. Particles whose rotated footprint overlaps visible terrain can be rejected before the terrain renderer can clip and draw them.

## Fix Focus Areas
- Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DParticleSys.cpp[171-189]
- Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DTerrainParticle.cpp[112-125]

## Recommended Fix
For conforming particle systems, expand the X/Y prepass culling radius by the rotated footprint radius calculated from the particle angle, or conservatively use `psize * sqrt(2)`. Leave the existing radius unchanged for other alignment modes.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

{
PARTICLE_ALIGNMENT_BILLBOARD = 0,
PARTICLE_ALIGNMENT_XYPLANAR,
PARTICLE_ALIGNMENT_CONFORMING,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remediation recommended

7. Editing effects removes terrain alignment 🐞 Bug ⚙ Maintainability

DebugWindowDialog::getSwitchFromSystem recognizes only XY-planar alignment and
updateSwitchToSystem maps an unchecked alignment control to billboard mode. Once the new
conforming enum value is loaded into the Particle Editor, editing any switches-dialog setting writes
that unchecked state back and replaces conforming alignment with billboard.
Agent Prompt
## Issue description
The Particle Editor represents alignment as a boolean XY-plane checkbox. It cannot represent the new conforming enum value and writes the unchecked value back as billboard alignment during unrelated switch edits.

## Fix Focus Areas
- Core/GameEngine/Include/GameClient/ParticleSys.h[437-443]
- Core/Tools/ParticleEditor/ParticleEditorDialog.cpp[1085-1114]
- Core/Tools/ParticleEditor/CSwitchesDialog.cpp[74-127]

## Recommended Fix
Make the editor expose all alignment enum values, preferably with a three-value control, and write the selected enum value directly. At minimum, preserve conforming alignment when the legacy XY-plane switch is not changed.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Enhancement Is new feature or request Gen Relates to Generals Rendering Is Rendering related ZH Relates to Zero Hour

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant