Skip to content

Commit 997d2a4

Browse files
committed
Global texture tracking and reference counting
This is a preliminary step to putting all the textures in a big array on the GPU so they don't have to be bound repeatedly during rendering. That should improve performance, and it should also make it easier to implement draw indirect earlier.
1 parent 56f59f1 commit 997d2a4

File tree

9 files changed

+111
-23
lines changed

9 files changed

+111
-23
lines changed

Nanoforge/Gui/ViewModels/Documents/ChunkViewer/ChunkViewerDocumentViewModel.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
using Nanoforge.Render.Resources;
1919
using Nanoforge.Rfg;
2020
using RFGM.Formats.Materials;
21+
using RFGM.Formats.Math;
2122
using RFGM.Formats.Meshes;
2223
using RFGM.Formats.Meshes.Chunks;
2324
using RFGM.Formats.Meshes.Shared;
@@ -339,6 +340,11 @@ private RenderChunk CreateRenderChunkFromDestroyable(RenderContext context, Mesh
339340
{
340341
try
341342
{
343+
if (TextureManager.GetTexture(tgaName) is { } cachedTexture)
344+
{
345+
return cachedTexture;
346+
}
347+
342348
if (chunkFileEntry.Parent is not DirectoryEntry parent)
343349
{
344350
Log.Error($"Passed chunk file entry with no parent directory into TryLoadTextureFromTgaName(). This should never happen. Chunks should always be in a vpp_pc or str2_pc.");
@@ -379,6 +385,7 @@ private RenderChunk CreateRenderChunkFromDestroyable(RenderContext context, Mesh
379385
texture.SetPixels(pixels, pool, queue);
380386
texture.CreateTextureSampler();
381387
texture.CreateImageView();
388+
TextureManager.NewTexture(tgaName, texture);
382389

383390
return texture;
384391
}

Nanoforge/Gui/ViewModels/Documents/RendererTestDocumentViewModel.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,12 @@ private Texture2D LoadTextureFromPackfile(RenderContext context, string cpuFileP
210210
List<LogicalTexture> textures = result.LogicalTextures.ToList();
211211
LogicalTexture logicalTexture = textures[logicalTextureIndex];
212212

213+
string tgaName = logicalTexture.Name;
214+
if (TextureManager.GetTexture(tgaName) is { } cachedTexture)
215+
{
216+
return cachedTexture;
217+
}
218+
213219
using var memoryStream = new MemoryStream();
214220
logicalTexture.Data.CopyTo(memoryStream);
215221
byte[] pixels = memoryStream.ToArray();
@@ -233,6 +239,7 @@ private Texture2D LoadTextureFromPackfile(RenderContext context, string cpuFileP
233239
texture.SetPixels(pixels, context.TransferCommandPool, context.TransferQueue);
234240
texture.CreateTextureSampler();
235241
texture.CreateImageView();
242+
TextureManager.NewTexture(tgaName, texture);
236243

237244
return texture;
238245
}

Nanoforge/Render/Renderer.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,11 @@ public void Cleanup()
118118
scene.Destroy();
119119
}
120120

121+
if (TextureManager.Textures.Count > 0)
122+
{
123+
Log.Warning($"There are {TextureManager.Textures.Count} textures that haven't been destroyed.");
124+
}
125+
121126
if (_perFrameUniformBuffers != null)
122127
{
123128
foreach (VkBuffer buffer in _perFrameUniformBuffers)

Nanoforge/Render/Resources/RenderChunk.cs

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -125,10 +125,7 @@ public override void Destroy()
125125
{
126126
foreach (Texture2D texture in textures)
127127
{
128-
if (texture != Texture2D.DefaultTexture && texture != Texture2D.MissingTexture && texture != Texture2D.FlatNormalMap)
129-
{
130-
texture.Destroy();
131-
}
128+
TextureManager.RemoveReference(texture);
132129
}
133130
}
134131
}

Nanoforge/Render/Resources/SimpleRenderObject.cs

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -81,11 +81,7 @@ public override void Destroy()
8181
Mesh.Destroy();
8282
foreach (Texture2D texture in Textures)
8383
{
84-
//TODO: Add reference counting system for renderer resources to avoid needing checks like this
85-
if (texture != Texture2D.DefaultTexture && texture != Texture2D.MissingTexture && texture != Texture2D.FlatNormalMap)
86-
{
87-
texture.Destroy();
88-
}
84+
TextureManager.RemoveReference(texture);
8985
}
9086
}
9187
}

Nanoforge/Render/Scene.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -235,6 +235,7 @@ public void Destroy()
235235

236236
PrimitiveRenderer.Destroy();
237237
CleanupRenderTextures();
238+
TextureManager.DestroyUnusedTextures();
238239
Destroyed = true;
239240
}
240241
}

Nanoforge/Render/TextureManager.cs

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Linq;
4+
using Nanoforge.Render.Resources;
5+
6+
namespace Nanoforge.Render;
7+
8+
//Tracks all textures used by the renderer and their reference count
9+
public static class TextureManager
10+
{
11+
public class TextureMetadata
12+
{
13+
public string Name;
14+
public readonly Texture2D Texture;
15+
public int ReferenceCount;
16+
17+
public TextureMetadata(string name, Texture2D texture)
18+
{
19+
Name = name;
20+
Texture = texture;
21+
ReferenceCount = 0;
22+
}
23+
}
24+
25+
public static List<TextureMetadata> Textures = new();
26+
27+
public static bool IsTextureLoaded(string textureName)
28+
{
29+
return Textures.Any(t => t.Name == textureName);
30+
}
31+
32+
public static void NewTexture(string textureName, Texture2D texture)
33+
{
34+
var metadata = new TextureMetadata(textureName, texture)
35+
{
36+
ReferenceCount = 1,
37+
};
38+
Textures.Add(metadata);
39+
}
40+
41+
public static Texture2D? GetTexture(string textureName)
42+
{
43+
TextureMetadata? metadata = Textures.FirstOrDefault(t => t.Name == textureName);
44+
if (metadata != null)
45+
{
46+
metadata.ReferenceCount++;
47+
}
48+
49+
return metadata?.Texture;
50+
}
51+
52+
public static void RemoveReference(Texture2D texture)
53+
{
54+
TextureMetadata? metadata = Textures.FirstOrDefault(t => t.Texture == texture);
55+
if (metadata == null)
56+
return;
57+
58+
metadata.ReferenceCount = Math.Clamp(--metadata.ReferenceCount, 0, int.MaxValue);
59+
if (metadata.ReferenceCount == 0)
60+
{
61+
metadata.Texture.Destroy();
62+
Textures.Remove(metadata);
63+
}
64+
}
65+
66+
public static void DestroyUnusedTextures()
67+
{
68+
foreach (TextureMetadata metadata in Textures.Where(metadata => metadata.ReferenceCount == 0).ToArray())
69+
{
70+
Textures.Remove(metadata);
71+
metadata.Texture.Destroy();
72+
}
73+
}
74+
}

Nanoforge/Rfg/ProjectTexture.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,10 @@ public static Silk.NET.Vulkan.Format PegFormatToVulkanFormat(RfgCpeg.Entry.Bitma
5757
{
5858
if (Data == null)
5959
return null;
60+
if (TextureManager.GetTexture(Name) is { } cachedTexture)
61+
{
62+
return cachedTexture;
63+
}
6064

6165
byte[] pixels = Data.Load();
6266
Texture2D texture = new(renderer.Context, (uint)Width, (uint)Height, (uint)NumMipLevels, Format, ImageTiling.Optimal,
@@ -66,6 +70,7 @@ public static Silk.NET.Vulkan.Format PegFormatToVulkanFormat(RfgCpeg.Entry.Bitma
6670
texture.SetPixels(pixels, pool, queue);
6771
texture.CreateTextureSampler();
6872
texture.CreateImageView();
73+
TextureManager.NewTexture(Name, texture);
6974

7075
return texture;
7176
}

Nanoforge/Rfg/Territory.cs

Lines changed: 10 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -37,14 +37,13 @@ public bool Load(Renderer renderer, Scene scene)
3737
//TODO: Maybe make a wrapper class for command pools / command buffers that knows which pool and queue it belongs to so less params need to get passed around.
3838
//TODO: Maybe even have a separate "ThreadRenderContext" that gets passed around
3939
Dictionary<ProjectMesh, Mesh> meshCache = new();
40-
Dictionary<ProjectTexture, Texture2D> textureCache = new();
4140

4241
//Load low lod terrain meshes
4342
foreach (Zone zone in Zones)
4443
{
4544
ZoneTerrain terrain = zone.Terrain ?? throw new Exception("Zone.Terrain is null");
4645
List<Texture2D> lowLodTextures = new List<Texture2D>();
47-
Texture2D? combTexture = LoadTexture(terrain.CombTexture, textureCache, renderer, scene);
46+
Texture2D? combTexture = LoadTexture(terrain.CombTexture, renderer, scene);
4847
if (combTexture != null)
4948
{
5049
lowLodTextures.Add(combTexture);
@@ -55,7 +54,7 @@ public bool Load(Renderer renderer, Scene scene)
5554
Log.Warning("Failed to load comb texture for {}. Terrain may look wrong.", Name);
5655
}
5756

58-
Texture2D? ovlTexture = LoadTexture(terrain.OvlTexture, textureCache, renderer, scene);
57+
Texture2D? ovlTexture = LoadTexture(terrain.OvlTexture, renderer, scene);
5958
if (ovlTexture != null)
6059
{
6160
lowLodTextures.Add(ovlTexture);
@@ -66,7 +65,7 @@ public bool Load(Renderer renderer, Scene scene)
6665
Log.Warning("Failed to load ovl texture for {}. Terrain may look wrong.", Name);
6766
}
6867

69-
Texture2D? splatmap = LoadTexture(terrain.Splatmap, textureCache, renderer, scene);
68+
Texture2D? splatmap = LoadTexture(terrain.Splatmap, renderer, scene);
7069
if (splatmap != null)
7170
{
7271
lowLodTextures.Add(splatmap);
@@ -101,7 +100,7 @@ public bool Load(Renderer renderer, Scene scene)
101100
for (int textureIndex = 2; textureIndex < 10; textureIndex++)
102101
{
103102
ProjectTexture? projectTexture = terrain.SplatmapTextures[textureIndex - 2];
104-
Texture2D? texture = LoadTexture(projectTexture, textureCache, renderer, scene);
103+
Texture2D? texture = LoadTexture(projectTexture, renderer, scene);
105104
if (texture != null)
106105
{
107106
subzoneTextures[textureIndex] = texture;
@@ -147,7 +146,7 @@ public bool Load(Renderer renderer, Scene scene)
147146

148147
foreach (Rock rock in Rocks)
149148
{
150-
Texture2D? diffuse = LoadTexture(rock.DiffuseTexture, textureCache, renderer, scene);
149+
Texture2D? diffuse = LoadTexture(rock.DiffuseTexture, renderer, scene);
151150
if (diffuse is null)
152151
{
153152
diffuse = Texture2D.MissingTexture;
@@ -182,7 +181,7 @@ public bool Load(Renderer renderer, Scene scene)
182181
{
183182
//RenderChunk only works with chunks with destroyables currently. So in this case we render the chunk as a normal mesh
184183
Log.Warning($"Mover ({mover.UID}, {mover.Handle}, {mover.Num}) has a chunk no destroyables ({mover.ChunkData.Name}). Rendering as simple render object.");
185-
LoadChunkAsSimpleMesh(mover, mover.ChunkData, renderer, scene, meshCache, textureCache);
184+
LoadChunkAsSimpleMesh(mover, mover.ChunkData, renderer, scene, meshCache);
186185
continue;
187186
}
188187
Chunk? chunk = mover.ChunkData;
@@ -222,7 +221,7 @@ public bool Load(Renderer renderer, Scene scene)
222221
var texture = projectTextures[textureIndex];
223222
if (texture is not null)
224223
{
225-
renderTexture = LoadTexture(texture, textureCache, renderer, scene);
224+
renderTexture = LoadTexture(texture, renderer, scene);
226225
}
227226

228227
renderTexture ??= textureIndex switch
@@ -249,7 +248,7 @@ public bool Load(Renderer renderer, Scene scene)
249248
}
250249
}
251250

252-
private void LoadChunkAsSimpleMesh(ObjectMover mover, Chunk chunk, Renderer renderer, Scene scene, Dictionary<ProjectMesh, Mesh> meshCache, Dictionary<ProjectTexture, Texture2D> textureCache)
251+
private void LoadChunkAsSimpleMesh(ObjectMover mover, Chunk chunk, Renderer renderer, Scene scene, Dictionary<ProjectMesh, Mesh> meshCache)
253252
{
254253
RfgMaterial material = chunk.Materials[0];
255254
List<ProjectTexture?> projectTextures = chunk.Textures[0];
@@ -262,7 +261,7 @@ private void LoadChunkAsSimpleMesh(ObjectMover mover, Chunk chunk, Renderer rend
262261
Texture2D? renderTexture = null;
263262
if (texture is not null)
264263
{
265-
renderTexture = LoadTexture(texture, textureCache, renderer, scene);
264+
renderTexture = LoadTexture(texture, renderer, scene);
266265
}
267266

268267
renderTexture ??= textureIndex switch
@@ -284,14 +283,12 @@ private void LoadChunkAsSimpleMesh(ObjectMover mover, Chunk chunk, Renderer rend
284283
scene.CreateRenderObject(chunk.Mesh!.VertexFormat.ToString(), mover.Position, mover.Orient, mesh, renderTextures);
285284
}
286285

287-
private Texture2D? LoadTexture(ProjectTexture? projectTexture, Dictionary<ProjectTexture, Texture2D> textureCache, Renderer renderer, Scene scene)
286+
private Texture2D? LoadTexture(ProjectTexture? projectTexture, Renderer renderer, Scene scene)
288287
{
289288
try
290289
{
291290
if (projectTexture == null)
292291
return null;
293-
if (textureCache.ContainsKey(projectTexture))
294-
return textureCache[projectTexture];
295292

296293
Texture2D? texture = projectTexture.CreateRenderTexture(renderer, renderer.Context.TransferCommandPool, renderer.Context.TransferQueue);
297294
if (texture == null)
@@ -300,7 +297,6 @@ private void LoadChunkAsSimpleMesh(ObjectMover mover, Chunk chunk, Renderer rend
300297
return null;
301298
}
302299

303-
textureCache[projectTexture] = texture;
304300
return texture;
305301
}
306302
catch (Exception ex)

0 commit comments

Comments
 (0)