aboutsummaryrefslogtreecommitdiff
path: root/src/Ryujinx.Graphics.Nvdec/Image/SurfaceCache.cs
blob: 7359b3309e98501923b16bf9381407af08b5e9ef (plain) (blame)
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
using Ryujinx.Graphics.Device;
using Ryujinx.Graphics.Video;
using System;
using System.Diagnostics;

namespace Ryujinx.Graphics.Nvdec.Image
{
    class SurfaceCache
    {
        // Must be equal to at least the maximum number of surfaces
        // that can be in use simultaneously (which is 17, since H264
        // can have up to 16 reference frames, and we need another one
        // for the current frame).
        // Realistically, most codecs won't ever use more than 4 simultaneously.
        private const int MaxItems = 17;

        private struct CacheItem
        {
            public int ReferenceCount;
            public uint LumaOffset;
            public uint ChromaOffset;
            public int Width;
            public int Height;
            public IDecoder Owner;
            public ISurface Surface;
        }

        private readonly CacheItem[] _pool = new CacheItem[MaxItems];

        private readonly DeviceMemoryManager _mm;

        public SurfaceCache(DeviceMemoryManager mm)
        {
            _mm = mm;
        }

        public ISurface Get(IDecoder decoder, uint lumaOffset, uint chromaOffset, int width, int height)
        {
            lock (_pool)
            {
                ISurface surface = null;

                // Try to find a compatible surface with same parameters, and same offsets.
                for (int i = 0; i < MaxItems; i++)
                {
                    ref CacheItem item = ref _pool[i];

                    if (item.LumaOffset == lumaOffset &&
                        item.ChromaOffset == chromaOffset &&
                        item.Owner == decoder &&
                        item.Width == width &&
                        item.Height == height)
                    {
                        item.ReferenceCount++;
                        surface = item.Surface;
                        MoveToFront(i);
                        break;
                    }
                }

                // If we failed to find a perfect match, now ignore the offsets.
                // Search backwards to replace the oldest compatible surface,
                // this avoids thrashing frequently used surfaces.
                // Now we need to ensure that the surface is not in use, as we'll change the data.
                if (surface == null)
                {
                    for (int i = MaxItems - 1; i >= 0; i--)
                    {
                        ref CacheItem item = ref _pool[i];

                        if (item.ReferenceCount == 0 && item.Owner == decoder && item.Width == width && item.Height == height)
                        {
                            item.ReferenceCount = 1;
                            item.LumaOffset = lumaOffset;
                            item.ChromaOffset = chromaOffset;
                            surface = item.Surface;

                            if ((lumaOffset | chromaOffset) != 0)
                            {
                                SurfaceReader.Read(_mm, surface, lumaOffset, chromaOffset);
                            }

                            MoveToFront(i);
                            break;
                        }
                    }
                }

                // If everything else failed, we try to create a new surface,
                // and insert it on the pool. We replace the oldest item on the
                // pool to avoid thrashing frequently used surfaces.
                // If even the oldest item is in use, that means that the entire pool
                // is in use, in that case we throw as there's no place to insert
                // the new surface.
                if (surface == null)
                {
                    if (_pool[MaxItems - 1].ReferenceCount == 0)
                    {
                        surface = decoder.CreateSurface(width, height);

                        if ((lumaOffset | chromaOffset) != 0)
                        {
                            SurfaceReader.Read(_mm, surface, lumaOffset, chromaOffset);
                        }

                        MoveToFront(MaxItems - 1);
                        ref CacheItem item = ref _pool[0];
                        item.Surface?.Dispose();
                        item.ReferenceCount = 1;
                        item.LumaOffset = lumaOffset;
                        item.ChromaOffset = chromaOffset;
                        item.Width = width;
                        item.Height = height;
                        item.Owner = decoder;
                        item.Surface = surface;
                    }
                    else
                    {
                        throw new InvalidOperationException("No free slot on the surface pool.");
                    }
                }

                return surface;
            }
        }

        public void Put(ISurface surface)
        {
            lock (_pool)
            {
                for (int i = 0; i < MaxItems; i++)
                {
                    ref CacheItem item = ref _pool[i];

                    if (item.Surface == surface)
                    {
                        item.ReferenceCount--;
                        Debug.Assert(item.ReferenceCount >= 0);
                        break;
                    }
                }
            }
        }

        private void MoveToFront(int index)
        {
            // If index is 0 we don't need to do anything,
            // as it's already on the front.
            if (index != 0)
            {
                CacheItem temp = _pool[index];
                Array.Copy(_pool, 0, _pool, 1, index);
                _pool[0] = temp;
            }
        }

        public void Trim()
        {
            lock (_pool)
            {
                for (int i = 0; i < MaxItems; i++)
                {
                    ref CacheItem item = ref _pool[i];

                    if (item.ReferenceCount == 0)
                    {
                        item.Surface?.Dispose();
                        item = default;
                    }
                }
            }
        }
    }
}