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

Adds terrain-conforming particle rendering for uneven ground.

  • Introduces CONFORMING particle alignment and integrates it with particle limits and batching.
  • Generates terrain-matching particle meshes with recursive subdivision and flat-region optimization.
  • Adds terrain-particle rendering statistics to the debug display.
  • Replaces the fixed lookup-table assumption with per-particle capacity growth and resets lookup state after batch flushes.

Confidence Score: 5/5

The PR appears safe to merge, with only the existing non-blocking invalid-size thread still outstanding.

The previous fixed-capacity lookup overflow was resolved by sizing the lookup table to each particle’s clipped terrain bounds and resetting it after flushes. The unresolved size-validation thread is only partially fixed: stephanmeesters added a lower-bound check, which handles zero and negative sizes, but non-finite values such as NaN still bypass the comparison and can propagate invalid bounds and texture coordinates.

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

Important Files Changed

Filename Overview
Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DTerrainParticle.cpp Implements terrain-bound calculation, recursive mesh generation, dynamic lookup sizing, batching, and rendering; the previous non-finite-size concern remains in its existing thread.
Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DParticleSys.cpp Routes conforming particle batches through the new renderer while preserving texture, shader, alignment, and capacity flush boundaries.
Core/GameEngine/Include/GameClient/ParticleSys.h Adds the conforming alignment option and classifies it as a field-particle mode.
Core/GameEngineDevice/Source/W3DDevice/GameClient/WorldHeightMap.cpp Adds a terrain-flatness query used to coarsen conforming particle meshes.
GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DDisplay.cpp Displays terrain-particle triangle and draw-call statistics.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart LR
    PS[Particle system] --> B[Particle batch manager]
    B -->|Billboard or planar| PG[PointGroup renderer]
    B -->|Conforming| TP[Terrain particle renderer]
    TP --> TB[Intersect particle and visible terrain bounds]
    TB --> SR[Recursively subdivide non-flat regions]
    SR --> M[Generate terrain-aligned mesh]
    M --> DX[Submit dynamic vertex and index buffers]
Loading

Reviews (3): 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.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Fixed. This would have given problems only for extremely large particles, over 180x180 terrain tiles,

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.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Added a size check to render().

Would have expected a cull of zero-sized particles somewhere earlier but that does not appear to be the case.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

That’s the right place for the check: before calculating projectedRadius, terrain bounds, or UVs. There does not appear to be a guaranteed earlier cull for zero or invalid runtime-updated sizes, so guarding in render() is appropriate. A size <= 0.0f check resolves the reported division-by-zero case; non-finite values would need a separate finite-value check if they can reach the particle arrays.

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

qodo-free-for-open-source-projects Bot commented Sep 13, 2026

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (4) 📘 Rule violations (0) 📎 Requirement gaps (0) 🎨 UX issues (0) 🔗 Cross-repo conflicts (0) 📜 Skill insights (0)

Grey Divider


Action required

1. Field effects vanish near view edges ✓ Resolved 📎 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 ✓ Resolved 🐞 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 ✓ Resolved 🐞 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

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 thread Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DTerrainParticle.cpp Outdated
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

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

It is rendered at the same size as the pointgroup flat version, so it looks as expected on flat terrain.

Interestingly billboarded particles interpret particle size differently, and are rendered at half the size compared to the same flat particle.

Comment thread Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DTerrainParticle.cpp Outdated
{
PARTICLE_ALIGNMENT_BILLBOARD = 0,
PARTICLE_ALIGNMENT_XYPLANAR,
PARTICLE_ALIGNMENT_CONFORMING,

@qodo-free-for-open-source-projects qodo-free-for-open-source-projects Bot Sep 13, 2026

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

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

ParticleEditor support for conforming particles should be handled in a later PR

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Dismissed — deferred.

@stephanmeesters
stephanmeesters force-pushed the feat/terrain-conforming-particles branch from 60d17b0 to e389227 Compare September 13, 2026 21:16
@stephanmeesters
stephanmeesters force-pushed the feat/terrain-conforming-particles branch from e389227 to 4a54116 Compare September 13, 2026 21:52
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