A game framework written with osu! in mind.
1// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
2// See the LICENCE file in the repository root for full licence text.
3
4using System;
5using System.Collections.Generic;
6using osu.Framework.Graphics.OpenGL;
7using osu.Framework.Threading;
8using osuTK;
9using osuTK.Graphics.ES30;
10using static osu.Framework.Threading.ScheduledDelegate;
11
12namespace osu.Framework.Graphics.Shaders
13{
14 public class Shader : IShader, IDisposable
15 {
16 private readonly string name;
17 private readonly List<ShaderPart> parts;
18
19 private readonly ScheduledDelegate shaderCompileDelegate;
20
21 internal readonly Dictionary<string, IUniform> Uniforms = new Dictionary<string, IUniform>();
22
23 /// <summary>
24 /// Holds all the <see cref="Uniforms"/> values for faster access than iterating on <see cref="Dictionary{TKey,TValue}.Values"/>.
25 /// </summary>
26 private IUniform[] uniformsValues;
27
28 public bool IsLoaded { get; private set; }
29
30 internal bool IsBound { get; private set; }
31
32 private int programID = -1;
33
34 internal Shader(string name, List<ShaderPart> parts)
35 {
36 this.name = name;
37 this.parts = parts;
38
39 GLWrapper.ScheduleExpensiveOperation(shaderCompileDelegate = new ScheduledDelegate(compile));
40 }
41
42 private void compile()
43 {
44 if (IsDisposed)
45 throw new ObjectDisposedException(ToString(), "Can not compile a disposed shader.");
46
47 if (IsLoaded)
48 throw new InvalidOperationException("Attempting to compile an already-compiled shader.");
49
50 parts.RemoveAll(p => p == null);
51 if (parts.Count == 0)
52 return;
53
54 programID = CreateProgram();
55
56 if (!CompileInternal())
57 throw new ProgramLinkingFailedException(name, GetProgramLog());
58
59 IsLoaded = true;
60
61 SetupUniforms();
62
63 GlobalPropertyManager.Register(this);
64 }
65
66 internal void EnsureShaderCompiled()
67 {
68 if (IsDisposed)
69 throw new ObjectDisposedException(ToString(), "Can not compile a disposed shader.");
70
71 if (shaderCompileDelegate.State == RunState.Waiting)
72 shaderCompileDelegate.RunTask();
73 }
74
75 public void Bind()
76 {
77 if (IsDisposed)
78 throw new ObjectDisposedException(ToString(), "Can not bind a disposed shader.");
79
80 if (IsBound)
81 return;
82
83 EnsureShaderCompiled();
84
85 GLWrapper.UseProgram(this);
86
87 foreach (var uniform in uniformsValues)
88 uniform?.Update();
89
90 IsBound = true;
91 }
92
93 public void Unbind()
94 {
95 if (!IsBound)
96 return;
97
98 GLWrapper.UseProgram(null);
99
100 IsBound = false;
101 }
102
103 /// <summary>
104 /// Returns a uniform from the shader.
105 /// </summary>
106 /// <param name="name">The name of the uniform.</param>
107 /// <returns>Returns a base uniform.</returns>
108 public Uniform<T> GetUniform<T>(string name)
109 where T : struct, IEquatable<T>
110 {
111 if (IsDisposed)
112 throw new ObjectDisposedException(ToString(), "Can not retrieve uniforms from a disposed shader.");
113
114 EnsureShaderCompiled();
115
116 return (Uniform<T>)Uniforms[name];
117 }
118
119 private protected virtual bool CompileInternal()
120 {
121 foreach (ShaderPart p in parts)
122 {
123 if (!p.Compiled) p.Compile();
124 GL.AttachShader(this, p);
125
126 foreach (ShaderInputInfo input in p.ShaderInputs)
127 GL.BindAttribLocation(this, input.Location, input.Name);
128 }
129
130 GL.LinkProgram(this);
131 GL.GetProgram(this, GetProgramParameterName.LinkStatus, out int linkResult);
132
133 foreach (var part in parts)
134 GL.DetachShader(this, part);
135
136 return linkResult == 1;
137 }
138
139 private protected virtual void SetupUniforms()
140 {
141 GL.GetProgram(this, GetProgramParameterName.ActiveUniforms, out int uniformCount);
142
143 uniformsValues = new IUniform[uniformCount];
144
145 for (int i = 0; i < uniformCount; i++)
146 {
147 GL.GetActiveUniform(this, i, 100, out _, out _, out ActiveUniformType type, out string uniformName);
148
149 IUniform uniform;
150
151 switch (type)
152 {
153 case ActiveUniformType.Bool:
154 uniform = createUniform<bool>(uniformName);
155 break;
156
157 case ActiveUniformType.Float:
158 uniform = createUniform<float>(uniformName);
159 break;
160
161 case ActiveUniformType.Int:
162 uniform = createUniform<int>(uniformName);
163 break;
164
165 case ActiveUniformType.FloatMat3:
166 uniform = createUniform<Matrix3>(uniformName);
167 break;
168
169 case ActiveUniformType.FloatMat4:
170 uniform = createUniform<Matrix4>(uniformName);
171 break;
172
173 case ActiveUniformType.FloatVec2:
174 uniform = createUniform<Vector2>(uniformName);
175 break;
176
177 case ActiveUniformType.FloatVec3:
178 uniform = createUniform<Vector3>(uniformName);
179 break;
180
181 case ActiveUniformType.FloatVec4:
182 uniform = createUniform<Vector4>(uniformName);
183 break;
184
185 case ActiveUniformType.Sampler2D:
186 uniform = createUniform<int>(uniformName);
187 break;
188
189 default:
190 continue;
191 }
192
193 Uniforms.Add(uniformName, uniform);
194 uniformsValues[i] = uniform;
195 }
196
197 IUniform createUniform<T>(string name)
198 where T : struct, IEquatable<T>
199 {
200 int location = GL.GetUniformLocation(this, name);
201
202 if (GlobalPropertyManager.CheckGlobalExists(name)) return new GlobalUniform<T>(this, name, location);
203
204 return new Uniform<T>(this, name, location);
205 }
206 }
207
208 private protected virtual string GetProgramLog() => GL.GetProgramInfoLog(this);
209
210 private protected virtual int CreateProgram() => GL.CreateProgram();
211
212 private protected virtual void DeleteProgram(int id) => GL.DeleteProgram(id);
213
214 public override string ToString() => $@"{name} Shader (Compiled: {programID != -1})";
215
216 public static implicit operator int(Shader shader) => shader.programID;
217
218 #region IDisposable Support
219
220 protected internal bool IsDisposed { get; private set; }
221
222 ~Shader()
223 {
224 GLWrapper.ScheduleDisposal(() => Dispose(false));
225 }
226
227 public void Dispose()
228 {
229 Dispose(true);
230 GC.SuppressFinalize(this);
231 }
232
233 protected virtual void Dispose(bool disposing)
234 {
235 if (!IsDisposed)
236 {
237 IsDisposed = true;
238
239 shaderCompileDelegate?.Cancel();
240
241 GlobalPropertyManager.Unregister(this);
242
243 if (programID != -1)
244 DeleteProgram(this);
245 }
246 }
247
248 #endregion
249
250 public class PartCompilationFailedException : Exception
251 {
252 public PartCompilationFailedException(string partName, string log)
253 : base($"A {typeof(ShaderPart)} failed to compile: {partName}:\n{log.Trim()}")
254 {
255 }
256 }
257
258 public class ProgramLinkingFailedException : Exception
259 {
260 public ProgramLinkingFailedException(string programName, string log)
261 : base($"A {typeof(Shader)} failed to link: {programName}:\n{log.Trim()}")
262 {
263 }
264 }
265 }
266}