-
Notifications
You must be signed in to change notification settings - Fork 398
/
VeldridTexture.cs
521 lines (406 loc) · 20.4 KB
/
VeldridTexture.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Numerics;
using osu.Framework.Development;
using osu.Framework.Extensions.ImageExtensions;
using osu.Framework.Graphics.Primitives;
using osu.Framework.Graphics.Rendering;
using osu.Framework.Graphics.Textures;
using osu.Framework.Logging;
using osu.Framework.Platform;
using osuTK.Graphics;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.PixelFormats;
using Veldrid;
using PixelFormat = Veldrid.PixelFormat;
using Texture = Veldrid.Texture;
namespace osu.Framework.Graphics.Veldrid.Textures
{
internal class VeldridTexture : IVeldridTexture
{
private readonly Queue<ITextureUpload> uploadQueue = new Queue<ITextureUpload>();
IRenderer INativeTexture.Renderer => Renderer;
public string Identifier
{
get
{
if (!Available || resources == null)
return "-";
return resources.Texture.Name;
}
}
public int MaxSize => Renderer.MaxTextureSize;
public virtual int Width { get; set; }
public virtual int Height { get; set; }
public virtual int GetByteSize() => Width * Height * 4;
public bool Available { get; private set; } = true;
ulong INativeTexture.TotalBindCount { get; set; }
public bool BypassTextureUploadQueueing { get; set; }
private readonly bool manualMipmaps;
private readonly List<RectangleI> uploadedRegions = new List<RectangleI>();
private readonly SamplerFilter filteringMode;
private readonly Color4? initialisationColour;
public ulong BindCount { get; protected set; }
public RectangleI Bounds => new RectangleI(0, 0, Width, Height);
protected virtual TextureUsage Usages
{
get
{
var usages = TextureUsage.Sampled | TextureUsage.RenderTarget;
if (!manualMipmaps)
usages |= TextureUsage.GenerateMipmaps;
return usages;
}
}
protected readonly IVeldridRenderer Renderer;
/// <summary>
/// Creates a new <see cref="VeldridTexture"/>.
/// </summary>
/// <param name="renderer">The renderer.</param>
/// <param name="width">The width of the texture.</param>
/// <param name="height">The height of the texture.</param>
/// <param name="manualMipmaps">Whether manual mipmaps will be uploaded to the texture. If false, the texture will compute mipmaps automatically.</param>
/// <param name="filteringMode">The filtering mode.</param>
/// <param name="initialisationColour">The colour to initialise texture levels with (in the case of sub region initial uploads). If null, no initialisation is provided out-of-the-box.</param>
public VeldridTexture(IVeldridRenderer renderer, int width, int height, bool manualMipmaps = false, SamplerFilter filteringMode = SamplerFilter.MinLinear_MagLinear_MipLinear,
Color4? initialisationColour = null)
{
this.manualMipmaps = manualMipmaps;
this.filteringMode = filteringMode;
this.initialisationColour = initialisationColour;
Renderer = renderer;
Width = width;
Height = height;
}
#region Disposal
~VeldridTexture()
{
Dispose(false);
}
private bool isDisposed;
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool isDisposing)
{
if (isDisposed)
return;
isDisposed = true;
Renderer.ScheduleDisposal(texture =>
{
while (texture.tryGetNextUpload(out var upload))
upload.Dispose();
texture.memoryLease?.Dispose();
texture.resources?.Dispose();
texture.resources = null;
texture.Available = false;
}, this);
}
#endregion
#region Memory Tracking
private readonly List<long> levelMemoryUsage = new List<long>();
private NativeMemoryTracker.NativeMemoryLease? memoryLease;
private void updateMemoryUsage(int level, long newUsage)
{
while (level >= levelMemoryUsage.Count)
levelMemoryUsage.Add(0);
levelMemoryUsage[level] = newUsage;
memoryLease?.Dispose();
memoryLease = NativeMemoryTracker.AddMemory(this, getMemoryUsage());
}
private long getMemoryUsage()
{
long usage = 0;
for (int i = 0; i < levelMemoryUsage.Count; i++)
usage += levelMemoryUsage[i];
return usage;
}
#endregion
private readonly VeldridTextureResources?[] resourcesArray = new VeldridTextureResources?[1];
private VeldridTextureResources? resources
{
get => resourcesArray[0];
set => resourcesArray[0] = value;
}
public virtual IReadOnlyList<VeldridTextureResources> GetResourceList()
{
if (resources == null)
return Array.Empty<VeldridTextureResources>();
return resourcesArray!;
}
public void FlushUploads()
{
while (tryGetNextUpload(out var upload))
upload.Dispose();
}
public void SetData(ITextureUpload upload)
{
lock (uploadQueue)
{
if (uploadQueue.Count >= 100 && uploadQueue.Count % 100 == 0)
Logger.Log($"Texture {Identifier}'s upload queue is large ({uploadQueue.Count})");
bool requireUpload = uploadQueue.Count == 0;
uploadQueue.Enqueue(upload);
if (requireUpload && !BypassTextureUploadQueueing)
Renderer.EnqueueTextureUpload(this);
}
}
public bool Upload()
{
if (!Available)
return false;
// We should never run raw Veldrid calls on another thread than the draw thread due to race conditions.
ThreadSafety.EnsureDrawThread();
uploadedRegions.Clear();
while (tryGetNextUpload(out ITextureUpload? upload))
{
using (upload)
{
DoUpload(upload);
uploadedRegions.Add(upload.Bounds);
}
}
#region Custom mipmap generation (disabled)
// Generate mipmaps for just the updated regions of the texture.
// This implementation is functionally equivalent to CommandList.GenerateMipmaps(),
// only that it is much more efficient if only small parts of the texture
// have been updated.
// The implementation has been tried in a release, but the user reception has been mixed
// due to various issues on various platforms (most prominently android).
// As it's difficult to ascertain a reasonable heuristic as to when it should be safe to use this implementation,
// for now it is unconditionally disabled, and it can be revisited at a later date.
// if (uploadedRegions.Count != 0 && !manualMipmaps)
// {
// // Merge overlapping upload regions to prevent redundant mipmap generation.
// // i goes through the list left-to-right, j goes through it right-to-left
// // until both indices meet somewhere in the middle.
// // This algorithm needs multiple passes until no possible merges are found.
// bool mergeFound;
// do
// {
// mergeFound = false;
// for (int i = 0; i < uploadedRegions.Count; ++i)
// {
// RectangleI toMerge = uploadedRegions[i];
// for (int j = uploadedRegions.Count - 1; j > i; --j)
// {
// RectangleI mergeCandidate = uploadedRegions[j];
// if (!toMerge.Intersect(mergeCandidate).IsEmpty)
// {
// uploadedRegions[i] = toMerge = RectangleI.Union(toMerge, mergeCandidate);
// uploadedRegions.RemoveAt(j);
// mergeFound = true;
// }
// }
// }
// } while (mergeFound);
// // Mipmap generation using the merged upload regions follows
// BlendingParameters previousBlendingParameters = Renderer.CurrentBlendingParameters;
// // Use a simple render state (no blending, masking, scissoring, stenciling, etc.)
// Renderer.SetBlend(BlendingParameters.None);
// Renderer.PushDepthInfo(new DepthInfo(false, false));
// Renderer.PushStencilInfo(new StencilInfo(false));
// Renderer.PushScissorState(false);
// // Create render state for mipmap generation
// Renderer.BindTexture(this);
// Renderer.GetMipmapShader().Bind();
// using var samplingTexture = Renderer.Factory.CreateTexture(TextureDescription.Texture2D((uint)Width, (uint)Height, resources!.Texture.MipLevels, 1, resources!.Texture.Format, TextureUsage.Sampled));
// using var samplingResources = new VeldridTextureResources(samplingTexture, null);
// while (uploadedRegions.Count > 0)
// {
// int width = Width;
// int height = Height;
// int count = Math.Min(uploadedRegions.Count, IRenderer.MAX_QUADS);
// // Generate quad buffer that will hold all the updated regions
// var quadBuffer = new VeldridQuadBuffer<UncolouredVertex2D>(Renderer, count, BufferUsage.Dynamic);
// // Compute mipmap by iteratively blitting coarser and coarser versions of the updated regions
// for (int level = 1; level < IRenderer.MAX_MIPMAP_LEVELS + 1 && (width > 1 || height > 1); ++level)
// {
// width /= 2;
// height /= 2;
// // Fill quad buffer with downscaled (and conservatively rounded) draw rectangles
// for (int i = 0; i < count; ++i)
// {
// // Conservatively round the draw rectangles. Rounding to integer coords is required
// // in order to ensure all the texels affected by linear interpolation are touched.
// // We could skip the rounding & use a single vertex buffer for all levels if we had
// // conservative raster, but alas, that's only supported on NV and Intel.
// Vector2I topLeft = uploadedRegions[i].TopLeft;
// topLeft = new Vector2I(topLeft.X / 2, topLeft.Y / 2);
// Vector2I bottomRight = uploadedRegions[i].BottomRight;
// bottomRight = new Vector2I(MathUtils.DivideRoundUp(bottomRight.X, 2), MathUtils.DivideRoundUp(bottomRight.Y, 2));
// uploadedRegions[i] = new RectangleI(topLeft.X, topLeft.Y, bottomRight.X - topLeft.X, bottomRight.Y - topLeft.Y);
// // Normalize the draw rectangle into the unit square, which doubles as texture sampler coordinates.
// RectangleF r = (RectangleF)uploadedRegions[i] / new Vector2(width, height);
// quadBuffer.SetVertex(i * 4 + 0, new UncolouredVertex2D { Position = r.BottomLeft });
// quadBuffer.SetVertex(i * 4 + 1, new UncolouredVertex2D { Position = r.BottomRight });
// quadBuffer.SetVertex(i * 4 + 2, new UncolouredVertex2D { Position = r.TopRight });
// quadBuffer.SetVertex(i * 4 + 3, new UncolouredVertex2D { Position = r.TopLeft });
// }
// // this intentionally runs a CopyTexture command on the render pass command list as it has to be executed after the previous level is rendered by the GPU.
// Renderer.Commands.CopyTexture(resources!.Texture, samplingTexture, (uint)level - 1, 0);
// // Read the texture from 1 mip level higher...
// samplingResources.Sampler = Renderer.Factory.CreateSampler(new SamplerDescription
// {
// AddressModeU = SamplerAddressMode.Clamp,
// AddressModeV = SamplerAddressMode.Clamp,
// AddressModeW = SamplerAddressMode.Clamp,
// Filter = filteringMode,
// MinimumLod = (uint)level - 1,
// MaximumLod = (uint)level - 1,
// MaximumAnisotropy = 0,
// });
// Renderer.BindTextureResource(samplingResources, 0);
// // ...than the one we're writing to via frame buffer.
// using (var frameBuffer = new VeldridFrameBuffer(Renderer, this, level))
// {
// Renderer.BindFrameBuffer(frameBuffer);
// Renderer.PushViewport(new RectangleI(0, 0, width, height));
// // Perform the actual mip level draw
// quadBuffer.Update();
// quadBuffer.Draw();
// Renderer.PopViewport();
// Renderer.UnbindFrameBuffer(frameBuffer);
// }
// }
// uploadedRegions.RemoveRange(0, count);
// }
// // Restore previous render state
// Renderer.GetMipmapShader().Unbind();
// Renderer.PopScissorState();
// Renderer.PopStencilInfo();
// Renderer.PopDepthInfo();
// Renderer.SetBlend(previousBlendingParameters);
// }
#endregion
#region Veldrid-provided mipmap generation
if (uploadedRegions.Count != 0 && !manualMipmaps)
{
Debug.Assert(resources != null);
Renderer.GenerateMipmaps(this);
}
#endregion
return uploadedRegions.Count != 0;
}
public bool UploadComplete
{
get
{
lock (uploadQueue)
return uploadQueue.Count == 0;
}
}
/// <summary>
/// Whether the texture is currently queued for upload.
/// </summary>
public bool IsQueuedForUpload { get; set; }
private bool tryGetNextUpload([NotNullWhen(true)] out ITextureUpload? upload)
{
lock (uploadQueue)
{
if (uploadQueue.Count == 0)
{
upload = null;
return false;
}
upload = uploadQueue.Dequeue();
return true;
}
}
private int? mipLevel;
public int? MipLevel
{
get => mipLevel;
set
{
if (mipLevel == value)
return;
mipLevel = value;
if (resources != null)
resources.Sampler = createSampler();
}
}
/// <summary>
/// The maximum number of mip levels provided by an <see cref="ITextureUpload"/>.
/// </summary>
/// <remarks>
/// This excludes automatic generation of mipmaps via the graphics backend.
/// </remarks>
private int maximumUploadedLod;
private Sampler createSampler()
{
bool useUploadMipmaps = manualMipmaps || maximumUploadedLod > 0;
int maximumLod = useUploadMipmaps ? maximumUploadedLod : IRenderer.MAX_MIPMAP_LEVELS;
var samplerDescription = new SamplerDescription
{
AddressModeU = SamplerAddressMode.Clamp,
AddressModeV = SamplerAddressMode.Clamp,
AddressModeW = SamplerAddressMode.Clamp,
Filter = filteringMode,
MinimumLod = (uint)(MipLevel ?? 0),
MaximumLod = (uint)(MipLevel ?? maximumLod),
MaximumAnisotropy = 0,
};
return Renderer.Factory.CreateSampler(ref samplerDescription);
}
protected virtual void DoUpload(ITextureUpload upload)
{
Texture? texture = resources?.Texture;
Sampler? sampler = resources?.Sampler;
if (texture == null || texture.Width != Width || texture.Height != Height)
{
texture?.Dispose();
var textureDescription = TextureDescription.Texture2D((uint)Width, (uint)Height, (uint)CalculateMipmapLevels(Width, Height), 1, PixelFormat.R8_G8_B8_A8_UNorm, Usages);
texture = Renderer.Factory.CreateTexture(ref textureDescription);
// todo: we may want to look into not having to allocate chunks of zero byte region for initialising textures
// similar to how OpenGL allows calling glTexImage2D with null data pointer.
initialiseLevel(texture, 0, Width, Height);
maximumUploadedLod = 0;
}
int lastMaximumUploadedLod = maximumUploadedLod;
if (!upload.Data.IsEmpty)
{
// ensure all mip levels up to the target level are initialised.
// generally we always upload at level 0, so this won't run.
if (upload.Level > maximumUploadedLod)
{
for (int i = maximumUploadedLod + 1; i <= upload.Level; i++)
initialiseLevel(texture, i, Width >> i, Height >> i);
maximumUploadedLod = upload.Level;
}
Renderer.UpdateTexture(texture, upload.Bounds.X >> upload.Level, upload.Bounds.Y >> upload.Level, upload.Bounds.Width >> upload.Level, upload.Bounds.Height >> upload.Level,
upload.Level, upload.Data);
}
if (sampler == null || maximumUploadedLod > lastMaximumUploadedLod)
{
sampler?.Dispose();
sampler = createSampler();
}
resources = new VeldridTextureResources(texture, sampler);
}
private unsafe void initialiseLevel(Texture texture, int level, int width, int height)
{
if (initialisationColour == null)
return;
var rgbaColour = new Rgba32(new Vector4(initialisationColour.Value.R, initialisationColour.Value.G, initialisationColour.Value.B, initialisationColour.Value.A));
// it is faster to initialise without a background specification if transparent black is all that's required.
using var image = initialisationColour == default
? new Image<Rgba32>(width, height)
: new Image<Rgba32>(width, height, rgbaColour);
using (var pixels = image.CreateReadOnlyPixelSpan())
{
updateMemoryUsage(level, (long)width * height * sizeof(Rgba32));
Renderer.UpdateTexture(texture, 0, 0, width, height, level, pixels.Span);
}
}
// todo: should this be limited to MAX_MIPMAP_LEVELS or was that constant supposed to be for automatic mipmap generation only?
// previous implementation was allocating mip levels all the way to 1x1 size when an ITextureUpload.Level > 0, therefore it's not limited there.
protected static int CalculateMipmapLevels(int width, int height) => 1 + (int)Math.Floor(Math.Log(Math.Max(width, height), 2));
}
}