From 4d5101a7c4929c7cf6a7ca3b923c73cb0708a54b Mon Sep 17 00:00:00 2001 From: RoosterDragon Date: Sun, 7 Dec 2014 23:02:15 +0000 Subject: [PATCH] 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. --- OpenRA.Game/Graphics/SpriteRenderer.cs | 47 ++- .../Traits/World/ShroudRenderer.cs | 310 +++++++++++------- 2 files changed, 208 insertions(+), 149 deletions(-) diff --git a/OpenRA.Game/Graphics/SpriteRenderer.cs b/OpenRA.Game/Graphics/SpriteRenderer.cs index b5ec0c2e60..356819eb2f 100644 --- a/OpenRA.Game/Graphics/SpriteRenderer.cs +++ b/OpenRA.Game/Graphics/SpriteRenderer.cs @@ -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 buffer, int start, int length, PrimitiveType type, Sheet sheet) { shader.SetTexture("DiffuseTexture", sheet.GetTexture()); diff --git a/OpenRA.Mods.Common/Traits/World/ShroudRenderer.cs b/OpenRA.Mods.Common/Traits/World/ShroudRenderer.cs index 0251d4dd21..787e1f73d3 100644 --- a/OpenRA.Mods.Common/Traits/World/ShroudRenderer.cs +++ b/OpenRA.Mods.Common/Traits/World/ShroudRenderer.cs @@ -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 tiles; - readonly byte variantStride; readonly Map map; readonly Edges notVisibleEdges; + readonly byte variantStride; + readonly byte[] edgesToSpriteIndexOffset; - bool clearedForNullShroud; - int lastShroudHash; - CellRegion updatedRegion; + readonly CellLayer tileInfos; + readonly CellLayer shroudDirty; + readonly Vertex[] fogVertices, shroudVertices; + readonly Sprite[] fogSprites, shroudSprites; + readonly CellLayer 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(map); + tileInfos = new CellLayer(map); + shroudDirty = new CellLayer(map); + var verticesLength = map.MapSize.X * map.MapSize.Y * 4; + fogVertices = new Vertex[verticesLength]; + shroudVertices = new Vertex[verticesLength]; + fogSpriteLayer = new CellLayer(map); + shroudSpriteLayer = new CellLayer(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 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; } - fogPalette = wr.Palette(info.FogPalette); - shroudPalette = wr.Palette(info.ShroudPalette); + if (currentShroud != null) + { + mapBorderShroudIsCached = false; + } + else if (!mapBorderShroudIsCached) + { + mapBorderShroudIsCached = true; + CacheMapBorderShroud(); + } } - Sprite GetTile(Sprite[] sprites, Edges edges, int variant) + 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; + } + + void CacheMapBorderShroud() + { + // Cache the whole of the map border shroud ahead of time, since it never changes. + Func 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 isVisible, + Sprite[] sprites, Vertex[] vertices, PaletteReference palette, CellLayer 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 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]]; } } }