Vastly improve shroud rendering performance.
Changes in the shroud are now tracked. If a cell changes it will mark itself and its neighbors as dirty. During the render phase all dirty cells will have their vertices calculated and cached. If a cell is not dirty, the pre-calculated vertices are retrieved from cache. Then the sprite renderer is provided the sprite and the pre-calculated vertices to draw. This prevents constant recalculation of vertices for the shroud in the render phase, requiring instead only dirty cells in the visible area. The update phase is reduced to a practical noop, instead incurring the cost only of changed cells each frame, rather than checking the visible area.
This commit is contained in:
@@ -8,6 +8,7 @@
|
||||
*/
|
||||
#endregion
|
||||
|
||||
using System;
|
||||
using System.Drawing;
|
||||
|
||||
namespace OpenRA.Graphics
|
||||
@@ -49,6 +50,17 @@ namespace OpenRA.Graphics
|
||||
}
|
||||
}
|
||||
|
||||
void SetRenderStateForSprite(Sprite s)
|
||||
{
|
||||
renderer.CurrentBatchRenderer = this;
|
||||
|
||||
if (s.Sheet != currentSheet || s.BlendMode != currentBlend || nv + 4 > renderer.TempBufferSize)
|
||||
Flush();
|
||||
|
||||
currentBlend = s.BlendMode;
|
||||
currentSheet = s.Sheet;
|
||||
}
|
||||
|
||||
public void DrawSprite(Sprite s, float2 location, PaletteReference pal)
|
||||
{
|
||||
DrawSprite(s, location, pal.TextureIndex, s.Size);
|
||||
@@ -61,19 +73,7 @@ namespace OpenRA.Graphics
|
||||
|
||||
void DrawSprite(Sprite s, float2 location, float paletteTextureIndex, float2 size)
|
||||
{
|
||||
renderer.CurrentBatchRenderer = this;
|
||||
|
||||
if (s.Sheet != currentSheet)
|
||||
Flush();
|
||||
|
||||
if (s.BlendMode != currentBlend)
|
||||
Flush();
|
||||
|
||||
if (nv + 4 > renderer.TempBufferSize)
|
||||
Flush();
|
||||
|
||||
currentBlend = s.BlendMode;
|
||||
currentSheet = s.Sheet;
|
||||
SetRenderStateForSprite(s);
|
||||
Util.FastCreateQuad(vertices, location + s.FractionalOffset * size, s, paletteTextureIndex, nv, size);
|
||||
nv += 4;
|
||||
}
|
||||
@@ -91,23 +91,18 @@ namespace OpenRA.Graphics
|
||||
|
||||
public void DrawSprite(Sprite s, float2 a, float2 b, float2 c, float2 d)
|
||||
{
|
||||
renderer.CurrentBatchRenderer = this;
|
||||
|
||||
if (s.Sheet != currentSheet)
|
||||
Flush();
|
||||
|
||||
if (s.BlendMode != currentBlend)
|
||||
Flush();
|
||||
|
||||
if (nv + 4 > renderer.TempBufferSize)
|
||||
Flush();
|
||||
|
||||
currentSheet = s.Sheet;
|
||||
currentBlend = s.BlendMode;
|
||||
SetRenderStateForSprite(s);
|
||||
Util.FastCreateQuad(vertices, a, b, c, d, s, 0, nv);
|
||||
nv += 4;
|
||||
}
|
||||
|
||||
public void DrawSprite(Sprite s, Vertex[] sourceVertices, int offset)
|
||||
{
|
||||
SetRenderStateForSprite(s);
|
||||
Array.Copy(sourceVertices, offset, vertices, nv, 4);
|
||||
nv += 4;
|
||||
}
|
||||
|
||||
public void DrawVertexBuffer(IVertexBuffer<Vertex> buffer, int start, int length, PrimitiveType type, Sheet sheet)
|
||||
{
|
||||
shader.SetTexture("DiffuseTexture", sheet.GetTexture());
|
||||
|
||||
@@ -65,38 +65,35 @@ namespace OpenRA.Mods.Common.Traits
|
||||
All = Top | Right | Bottom | Left
|
||||
}
|
||||
|
||||
struct ShroudTile
|
||||
struct TileInfo
|
||||
{
|
||||
public readonly float2 ScreenPosition;
|
||||
public readonly byte Variant;
|
||||
|
||||
public Sprite Fog;
|
||||
public Sprite Shroud;
|
||||
|
||||
public ShroudTile(float2 screenPosition, byte variant)
|
||||
public TileInfo(float2 screenPosition, byte variant)
|
||||
{
|
||||
ScreenPosition = screenPosition;
|
||||
Variant = variant;
|
||||
|
||||
Fog = null;
|
||||
Shroud = null;
|
||||
}
|
||||
}
|
||||
|
||||
readonly ShroudRendererInfo info;
|
||||
readonly Sprite[] shroudSprites, fogSprites;
|
||||
readonly byte[] spriteMap;
|
||||
readonly CellLayer<ShroudTile> tiles;
|
||||
readonly byte variantStride;
|
||||
readonly Map map;
|
||||
readonly Edges notVisibleEdges;
|
||||
readonly byte variantStride;
|
||||
readonly byte[] edgesToSpriteIndexOffset;
|
||||
|
||||
bool clearedForNullShroud;
|
||||
int lastShroudHash;
|
||||
CellRegion updatedRegion;
|
||||
readonly CellLayer<TileInfo> tileInfos;
|
||||
readonly CellLayer<bool> shroudDirty;
|
||||
|
||||
readonly Vertex[] fogVertices, shroudVertices;
|
||||
readonly Sprite[] fogSprites, shroudSprites;
|
||||
readonly CellLayer<Sprite> fogSpriteLayer, shroudSpriteLayer;
|
||||
PaletteReference fogPalette, shroudPalette;
|
||||
|
||||
Shroud currentShroud;
|
||||
bool mapBorderShroudIsCached;
|
||||
|
||||
public ShroudRenderer(World world, ShroudRendererInfo info)
|
||||
{
|
||||
if (info.ShroudVariants.Length != info.FogVariants.Length)
|
||||
@@ -114,7 +111,13 @@ namespace OpenRA.Mods.Common.Traits
|
||||
this.info = info;
|
||||
map = world.Map;
|
||||
|
||||
tiles = new CellLayer<ShroudTile>(map);
|
||||
tileInfos = new CellLayer<TileInfo>(map);
|
||||
shroudDirty = new CellLayer<bool>(map);
|
||||
var verticesLength = map.MapSize.X * map.MapSize.Y * 4;
|
||||
fogVertices = new Vertex[verticesLength];
|
||||
shroudVertices = new Vertex[verticesLength];
|
||||
fogSpriteLayer = new CellLayer<Sprite>(map);
|
||||
shroudSpriteLayer = new CellLayer<Sprite>(map);
|
||||
|
||||
// Load sprite variants
|
||||
var variantCount = info.ShroudVariants.Length;
|
||||
@@ -141,16 +144,31 @@ namespace OpenRA.Mods.Common.Traits
|
||||
}
|
||||
|
||||
// Mapping of shrouded directions -> sprite index
|
||||
spriteMap = new byte[(byte)(info.UseExtendedIndex ? Edges.All : Edges.AllCorners) + 1];
|
||||
edgesToSpriteIndexOffset = new byte[(byte)(info.UseExtendedIndex ? Edges.All : Edges.AllCorners) + 1];
|
||||
for (var i = 0; i < info.Index.Length; i++)
|
||||
spriteMap[info.Index[i]] = (byte)i;
|
||||
edgesToSpriteIndexOffset[info.Index[i]] = (byte)i;
|
||||
|
||||
if (info.OverrideFullShroud != null)
|
||||
spriteMap[info.OverrideShroudIndex] = (byte)(variantStride - 1);
|
||||
edgesToSpriteIndexOffset[info.OverrideShroudIndex] = (byte)(variantStride - 1);
|
||||
|
||||
notVisibleEdges = info.UseExtendedIndex ? Edges.AllSides : Edges.AllCorners;
|
||||
}
|
||||
|
||||
public void WorldLoaded(World w, WorldRenderer wr)
|
||||
{
|
||||
// Initialize tile cache
|
||||
// Adds a 1-cell border around the border to cover any sprites peeking outside the map
|
||||
foreach (var uv in CellRegion.Expand(w.Map.Cells, 1).MapCoords)
|
||||
{
|
||||
var screen = wr.ScreenPosition(w.Map.CenterOfCell(uv.ToCPos(map)));
|
||||
var variant = (byte)Game.CosmeticRandom.Next(info.ShroudVariants.Length);
|
||||
tileInfos[uv] = new TileInfo(screen, variant);
|
||||
}
|
||||
|
||||
fogPalette = wr.Palette(info.FogPalette);
|
||||
shroudPalette = wr.Palette(info.ShroudPalette);
|
||||
}
|
||||
|
||||
Edges GetEdges(MPos uv, Func<MPos, bool> isVisible)
|
||||
{
|
||||
if (!isVisible(uv))
|
||||
@@ -158,7 +176,7 @@ namespace OpenRA.Mods.Common.Traits
|
||||
|
||||
var cell = uv.ToCPos(map);
|
||||
|
||||
// If a side is shrouded then we also count the corners
|
||||
// If a side is shrouded then we also count the corners.
|
||||
var edge = Edges.None;
|
||||
if (!isVisible((cell + new CVec(0, -1)).ToMPos(map))) edge |= Edges.Top;
|
||||
if (!isVisible((cell + new CVec(1, 0)).ToMPos(map))) edge |= Edges.Right;
|
||||
@@ -180,125 +198,171 @@ namespace OpenRA.Mods.Common.Traits
|
||||
return info.UseExtendedIndex ? edge ^ ucorner : edge & Edges.AllCorners;
|
||||
}
|
||||
|
||||
Edges GetObserverEdges(CPos p)
|
||||
public void RenderShroud(WorldRenderer wr, Shroud shroud)
|
||||
{
|
||||
var u = Edges.None;
|
||||
if (!map.Contains(p + new CVec(0, -1))) u |= Edges.Top;
|
||||
if (!map.Contains(p + new CVec(1, 0))) u |= Edges.Right;
|
||||
if (!map.Contains(p + new CVec(0, 1))) u |= Edges.Bottom;
|
||||
if (!map.Contains(p + new CVec(-1, 0))) u |= Edges.Left;
|
||||
|
||||
var ucorner = u & Edges.AllCorners;
|
||||
if (!map.Contains(p + new CVec(-1, -1))) u |= Edges.TopLeft;
|
||||
if (!map.Contains(p + new CVec(1, -1))) u |= Edges.TopRight;
|
||||
if (!map.Contains(p + new CVec(1, 1))) u |= Edges.BottomRight;
|
||||
if (!map.Contains(p + new CVec(-1, 1))) u |= Edges.BottomLeft;
|
||||
|
||||
return info.UseExtendedIndex ? u ^ ucorner : u & Edges.AllCorners;
|
||||
Update(shroud);
|
||||
Render(wr.Viewport.VisibleCells);
|
||||
}
|
||||
|
||||
public void WorldLoaded(World w, WorldRenderer wr)
|
||||
void Update(Shroud newShroud)
|
||||
{
|
||||
// Initialize tile cache
|
||||
// Adds a 1-cell border around the border to cover any sprites peeking outside the map
|
||||
foreach (var uv in CellRegion.Expand(w.Map.Cells, 1).MapCoords)
|
||||
if (currentShroud != newShroud)
|
||||
{
|
||||
var screen = wr.ScreenPosition(w.Map.CenterOfCell(uv.ToCPos(map)));
|
||||
var variant = (byte)Game.CosmeticRandom.Next(info.ShroudVariants.Length);
|
||||
tiles[uv] = new ShroudTile(screen, variant);
|
||||
if (currentShroud != null)
|
||||
currentShroud.CellEntryChanged -= MarkCellAndNeighborsDirty;
|
||||
|
||||
// Set the cells outside the border so they don't need to be touched again
|
||||
if (!map.Contains(uv))
|
||||
if (newShroud != null)
|
||||
{
|
||||
var shroudTile = tiles[uv];
|
||||
shroudTile.Shroud = GetTile(shroudSprites, notVisibleEdges, variant);
|
||||
tiles[uv] = shroudTile;
|
||||
shroudDirty.Clear(true);
|
||||
newShroud.CellEntryChanged += MarkCellAndNeighborsDirty;
|
||||
}
|
||||
|
||||
currentShroud = newShroud;
|
||||
}
|
||||
|
||||
if (currentShroud != null)
|
||||
{
|
||||
mapBorderShroudIsCached = false;
|
||||
}
|
||||
else if (!mapBorderShroudIsCached)
|
||||
{
|
||||
mapBorderShroudIsCached = true;
|
||||
CacheMapBorderShroud();
|
||||
}
|
||||
}
|
||||
|
||||
fogPalette = wr.Palette(info.FogPalette);
|
||||
shroudPalette = wr.Palette(info.ShroudPalette);
|
||||
void MarkCellAndNeighborsDirty(CPos cell)
|
||||
{
|
||||
// Mark this cell and its 8 neighbors as being out of date.
|
||||
// We don't want to do anything more than this for several performance reasons:
|
||||
// - If the cells remain off-screen for a long time, they may change several times before we next view
|
||||
// them, so calculating their new vertices is wasted effort since we may recalculate them again before we
|
||||
// even get a chance to render them.
|
||||
// - Cells tend to be invalidated in groups (imagine as a unit moves, it advances a wave of sight and
|
||||
// leaves a trail of fog filling in behind). If we recalculated a cell and its neighbors when the first
|
||||
// cell in a group changed, many cells would be recalculated again when the second cell, right next to the
|
||||
// first, is updated. In fact we might do on the order of 3x the work we needed to!
|
||||
shroudDirty[cell + new CVec(-1, -1)] = true;
|
||||
shroudDirty[cell + new CVec(0, -1)] = true;
|
||||
shroudDirty[cell + new CVec(1, -1)] = true;
|
||||
shroudDirty[cell + new CVec(-1, 0)] = true;
|
||||
shroudDirty[cell] = true;
|
||||
shroudDirty[cell + new CVec(1, 0)] = true;
|
||||
shroudDirty[cell + new CVec(-1, 1)] = true;
|
||||
shroudDirty[cell + new CVec(0, 1)] = true;
|
||||
shroudDirty[cell + new CVec(1, 1)] = true;
|
||||
}
|
||||
|
||||
Sprite GetTile(Sprite[] sprites, Edges edges, int variant)
|
||||
void CacheMapBorderShroud()
|
||||
{
|
||||
// Cache the whole of the map border shroud ahead of time, since it never changes.
|
||||
Func<MPos, bool> mapContains = map.Contains;
|
||||
foreach (var uv in CellRegion.Expand(map.Cells, 1).MapCoords)
|
||||
{
|
||||
var offset = VertexArrayOffset(uv);
|
||||
var edges = GetEdges(uv, mapContains);
|
||||
var tileInfo = tileInfos[uv];
|
||||
CacheTile(uv, offset, edges, tileInfo, shroudSprites, shroudVertices, shroudPalette, shroudSpriteLayer);
|
||||
CacheTile(uv, offset, edges, tileInfo, fogSprites, fogVertices, fogPalette, fogSpriteLayer);
|
||||
}
|
||||
}
|
||||
|
||||
void Render(CellRegion visibleRegion)
|
||||
{
|
||||
var renderRegion = CellRegion.Expand(visibleRegion, 1).MapCoords;
|
||||
|
||||
if (currentShroud == null)
|
||||
{
|
||||
RenderMapBorderShroud(renderRegion);
|
||||
return;
|
||||
}
|
||||
|
||||
RenderPlayerShroud(visibleRegion, renderRegion);
|
||||
}
|
||||
|
||||
void RenderMapBorderShroud(CellRegion.MapCoordsRegion renderRegion)
|
||||
{
|
||||
// Render the shroud that just encroaches at the map border. This shroud is always fully cached, so we can
|
||||
// just render straight from the cache.
|
||||
foreach (var uv in renderRegion)
|
||||
{
|
||||
var offset = VertexArrayOffset(uv);
|
||||
RenderCachedTile(shroudSpriteLayer[uv], shroudVertices, offset);
|
||||
RenderCachedTile(fogSpriteLayer[uv], fogVertices, offset);
|
||||
}
|
||||
}
|
||||
|
||||
void RenderPlayerShroud(CellRegion visibleRegion, CellRegion.MapCoordsRegion renderRegion)
|
||||
{
|
||||
// Render the shroud by drawing the appropriate tile over each cell that is visible on-screen.
|
||||
// For performance we keep a cache tiles we have drawn previously so we don't have to recalculate the
|
||||
// vertices for tiles every frame, since this is costly.
|
||||
// Any shroud marked as dirty has either never been calculated, or has changed since we last drew that
|
||||
// tile. We will calculate the vertices for that tile and cache them before drawing it.
|
||||
// Any shroud that is not marked as dirty means our cached tile is still correct - we can just draw the
|
||||
// cached vertices.
|
||||
var visibleUnderShroud = currentShroud.IsExploredTest(visibleRegion);
|
||||
var visibleUnderFog = currentShroud.IsVisibleTest(visibleRegion);
|
||||
foreach (var uv in renderRegion)
|
||||
{
|
||||
var offset = VertexArrayOffset(uv);
|
||||
if (shroudDirty[uv])
|
||||
{
|
||||
shroudDirty[uv] = false;
|
||||
RenderDirtyTile(uv, offset, visibleUnderShroud, shroudSprites, shroudVertices, shroudPalette, shroudSpriteLayer);
|
||||
RenderDirtyTile(uv, offset, visibleUnderFog, fogSprites, fogVertices, fogPalette, fogSpriteLayer);
|
||||
}
|
||||
else
|
||||
{
|
||||
RenderCachedTile(shroudSpriteLayer[uv], shroudVertices, offset);
|
||||
RenderCachedTile(fogSpriteLayer[uv], fogVertices, offset);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int VertexArrayOffset(MPos uv)
|
||||
{
|
||||
return 4 * (uv.V * map.MapSize.X + uv.U);
|
||||
}
|
||||
|
||||
void RenderDirtyTile(MPos uv, int offset, Func<MPos, bool> isVisible,
|
||||
Sprite[] sprites, Vertex[] vertices, PaletteReference palette, CellLayer<Sprite> spriteLayer)
|
||||
{
|
||||
var tile = tileInfos[uv];
|
||||
var edges = GetEdges(uv, isVisible);
|
||||
var sprite = CacheTile(uv, offset, edges, tile, sprites, vertices, palette, spriteLayer);
|
||||
RenderCachedTile(sprite, vertices, offset);
|
||||
}
|
||||
|
||||
void RenderCachedTile(Sprite sprite, Vertex[] vertices, int offset)
|
||||
{
|
||||
if (sprite != null)
|
||||
Game.Renderer.WorldSpriteRenderer.DrawSprite(sprite, vertices, offset);
|
||||
}
|
||||
|
||||
Sprite CacheTile(MPos uv, int offset, Edges edges, TileInfo tileInfo,
|
||||
Sprite[] sprites, Vertex[] vertices, PaletteReference palette, CellLayer<Sprite> spriteLayer)
|
||||
{
|
||||
var sprite = GetSprite(sprites, edges, tileInfo.Variant);
|
||||
if (sprite != null)
|
||||
{
|
||||
var size = sprite.Size;
|
||||
var location = tileInfo.ScreenPosition - 0.5f * size;
|
||||
OpenRA.Graphics.Util.FastCreateQuad(
|
||||
vertices, location + sprite.FractionalOffset * size,
|
||||
sprite, palette.TextureIndex, offset, size);
|
||||
}
|
||||
|
||||
spriteLayer[uv] = sprite;
|
||||
return sprite;
|
||||
}
|
||||
|
||||
Sprite GetSprite(Sprite[] sprites, Edges edges, int variant)
|
||||
{
|
||||
if (edges == Edges.None)
|
||||
return null;
|
||||
|
||||
return sprites[variant * variantStride + spriteMap[(byte)edges]];
|
||||
}
|
||||
|
||||
void Update(Shroud shroud, CellRegion region)
|
||||
{
|
||||
if (shroud != null)
|
||||
{
|
||||
// If the current shroud hasn't changed and we have already updated the specified area, we don't need to do anything.
|
||||
if (lastShroudHash == shroud.Hash && !clearedForNullShroud && updatedRegion != null && updatedRegion.Contains(region))
|
||||
return;
|
||||
|
||||
lastShroudHash = shroud.Hash;
|
||||
clearedForNullShroud = false;
|
||||
updatedRegion = region;
|
||||
UpdateShroud(shroud);
|
||||
}
|
||||
else if (!clearedForNullShroud)
|
||||
{
|
||||
// We need to clear any applied shroud.
|
||||
clearedForNullShroud = true;
|
||||
updatedRegion = new CellRegion(map.TileShape, new CPos(0, 0), new CPos(-1, -1));
|
||||
UpdateNullShroud();
|
||||
}
|
||||
}
|
||||
|
||||
void UpdateShroud(Shroud shroud)
|
||||
{
|
||||
var visibleUnderShroud = shroud.IsExploredTest(updatedRegion);
|
||||
var visibleUnderFog = shroud.IsVisibleTest(updatedRegion);
|
||||
foreach (var uv in updatedRegion.MapCoords)
|
||||
{
|
||||
var shrouded = GetEdges(uv, visibleUnderShroud);
|
||||
var fogged = GetEdges(uv, visibleUnderFog);
|
||||
var shroudTile = tiles[uv];
|
||||
var variant = shroudTile.Variant;
|
||||
shroudTile.Shroud = GetTile(shroudSprites, shrouded, variant);
|
||||
shroudTile.Fog = GetTile(fogSprites, fogged, variant);
|
||||
tiles[uv] = shroudTile;
|
||||
}
|
||||
}
|
||||
|
||||
void UpdateNullShroud()
|
||||
{
|
||||
foreach (var cell in map.Cells)
|
||||
{
|
||||
var edges = GetObserverEdges(cell);
|
||||
var shroudTile = tiles[cell];
|
||||
var variant = shroudTile.Variant;
|
||||
shroudTile.Shroud = GetTile(shroudSprites, edges, variant);
|
||||
shroudTile.Fog = GetTile(fogSprites, edges, variant);
|
||||
tiles[cell] = shroudTile;
|
||||
}
|
||||
}
|
||||
|
||||
public void RenderShroud(WorldRenderer wr, Shroud shroud)
|
||||
{
|
||||
Update(shroud, wr.Viewport.VisibleCells);
|
||||
|
||||
foreach (var uv in CellRegion.Expand(wr.Viewport.VisibleCells, 1).MapCoords)
|
||||
{
|
||||
var t = tiles[uv];
|
||||
|
||||
if (t.Shroud != null)
|
||||
{
|
||||
var pos = t.ScreenPosition - 0.5f * t.Shroud.Size;
|
||||
Game.Renderer.WorldSpriteRenderer.DrawSprite(t.Shroud, pos, shroudPalette);
|
||||
}
|
||||
|
||||
if (t.Fog != null)
|
||||
{
|
||||
var pos = t.ScreenPosition - 0.5f * t.Fog.Size;
|
||||
Game.Renderer.WorldSpriteRenderer.DrawSprite(t.Fog, pos, fogPalette);
|
||||
}
|
||||
}
|
||||
return sprites[variant * variantStride + edgesToSpriteIndexOffset[(byte)edges]];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user