golem/shader.rs
1use crate::*;
2use alloc::borrow::ToOwned;
3use alloc::vec::Vec;
4use core::mem::size_of;
5use core::ops::Range;
6
7/// The parameters to create a [`ShaderProgram`]
8pub struct ShaderDescription<'a> {
9 /// The inputs to the vertex shader stage, which are also the inputs to the whole shader
10 pub vertex_input: &'a [Attribute],
11 /// The inputs to the fragment shader stage, which are also the outputs from the vertex shader
12 pub fragment_input: &'a [Attribute],
13 /// The uniform values available to all shader stages, across all vertices of a draw call
14 ///
15 /// Uniforms can be bound with [`ShaderProgram::set_uniform`]
16 pub uniforms: &'a [Uniform],
17 /// The text of the vertex shader stage
18 ///
19 /// Do not include the vertex inputs, outputs, or uniforms, use the [`vertex_input`],
20 /// [`fragment_input`], and [`uniforms`] fields instead. Just provide the 'main' function, as
21 /// well as any helpers. The shader inputs, outputs, and uniforms will be generated for you.
22 ///
23 /// The inputs to this stage are defined as the [`vertex_input`] and the ouptuts are the
24 /// [`fragment_input`] as well as `gl_Position`, a vec4 that represents the vertex's position.
25 ///
26 /// [`vertex_input`]: ShaderDescription::vertex_input
27 /// [`fragment_input`]: ShaderDescription::fragment_input
28 /// [`uniforms`]: ShaderDescription::uniforms
29 pub vertex_shader: &'a str,
30 /// The text of the fragment shader stage
31 ///
32 /// See the documentation of the [`vertex_shader`]. The inputs to this stage are
33 /// defined as the [`fragment_input`] and the ouptut is `gl_FragColor`, a vec4 that represents
34 /// the RGBA color of the fragment. Use the function `texture` to read values from GLSL
35 /// textures; it will be converted to `texture2D` on the web backend.
36 ///
37 /// [`vertex_shader`]: ShaderDescription::vertex_shader
38 /// [`fragment_input`]: ShaderDescription::fragment_input
39 pub fragment_shader: &'a str,
40}
41
42/// A GPU program that draws data to the screen
43pub struct ShaderProgram {
44 ctx: crate::Context,
45 id: GlProgram,
46 vertex: GlShader,
47 fragment: GlShader,
48 input: Vec<Attribute>,
49}
50
51fn generate_shader_text(
52 is_vertex: bool,
53 body: &str,
54 inputs: &[Attribute],
55 outputs: &[Attribute],
56 uniforms: &[Uniform],
57) -> String {
58 let mut shader = String::new();
59
60 #[cfg(not(target_arch = "wasm32"))]
61 shader.push_str("#version 150\n");
62
63 shader.push_str("precision mediump float;\n");
64 for attr in inputs.iter() {
65 attr.as_glsl(is_vertex, Position::Input, &mut shader);
66 }
67 for attr in outputs.iter() {
68 attr.as_glsl(is_vertex, Position::Output, &mut shader);
69 }
70 for uniform in uniforms.iter() {
71 uniform.as_glsl(&mut shader);
72 }
73 shader.push_str(body);
74
75 shader
76}
77
78impl ShaderProgram {
79 /// Create a shader program with the given [`ShaderDescription`]
80 pub fn new(ctx: &Context, desc: ShaderDescription) -> Result<ShaderProgram, GolemError> {
81 let gl = &ctx.0.gl;
82 unsafe {
83 // https://www.khronos.org/registry/OpenGL-Refpages/gl4/html/glCreateShader.xhtml
84 // Errors:
85 // 1. An error occurred creating the shader (handled by glow's error layer)
86 // 2. An invalid value was passed (VERTEX_SHADER is valid)
87 let vertex = gl.create_shader(glow::VERTEX_SHADER)?;
88 let vertex_source = generate_shader_text(
89 true,
90 desc.vertex_shader,
91 desc.vertex_input,
92 desc.fragment_input,
93 desc.uniforms,
94 );
95 log::debug!("Vertex shader source: {}", vertex_source);
96 // https://www.khronos.org/registry/OpenGL-Refpages/gl4/html/glShaderSource.xhtml
97 // Errror conditions:
98 // 1 & 2. Vertex isn't a GL shader (it always will be)
99 // 3. Shader size is handled by glow
100 gl.shader_source(vertex, &vertex_source);
101 // https://www.khronos.org/registry/OpenGL-Refpages/gl4/html/glCompileShader.xhtml
102 // Errror conditions: Vertex isn't a GL shader (it always will be)
103 gl.compile_shader(vertex);
104 if !gl.get_shader_compile_status(vertex) {
105 let info = gl.get_shader_info_log(vertex);
106 log::error!("Failed to compile vertex shader: {}", info);
107 return Err(GolemError::ShaderCompilationError(info));
108 }
109 log::trace!("Compiled vertex shader succesfully");
110
111 // For GL pre/post condition explanations, see vertex shader compilation above
112 let fragment = gl.create_shader(glow::FRAGMENT_SHADER)?;
113 // Handle creating the output color and giving it a name, but only on desktop gl
114 #[cfg(target_arch = "wasm32")]
115 let (fragment_output, fragment_body) =
116 { (&[], &desc.fragment_shader.replace("texture", "texture2D")) };
117
118 #[cfg(not(target_arch = "wasm32"))]
119 let (fragment_output, fragment_body) = {
120 (
121 &[Attribute::new(
122 "outputColor",
123 AttributeType::Vector(Dimension::D4),
124 )],
125 &desc.fragment_shader.replace("gl_FragColor", "outputColor"),
126 )
127 };
128 let fragment_source = generate_shader_text(
129 false,
130 fragment_body,
131 desc.fragment_input,
132 fragment_output,
133 desc.uniforms,
134 );
135 log::debug!("Fragment shader source: {}", vertex_source);
136 gl.shader_source(fragment, &fragment_source);
137 gl.compile_shader(fragment);
138 if !gl.get_shader_compile_status(fragment) {
139 let info = gl.get_shader_info_log(fragment);
140 log::error!("Failed to compile vertex shader: {}", info);
141 return Err(GolemError::ShaderCompilationError(info));
142 }
143 log::trace!("Compiled fragment shader succesfully");
144
145 // https://www.khronos.org/registry/OpenGL-Refpages/gl4/html/glCreateProgram.xhtml
146 // Failing to create a program is handled by glow
147 let id = gl.create_program()?;
148
149 // https://www.khronos.org/registry/OpenGL-Refpages/gl4/html/glAttachShader.xhtml
150 // Errors:
151 // 1, 2, 3: id, vertex, and fragment are all assigned to once, by the correct GL calls
152 // 4: vertex and fragment are generated then immediately attached exactly once
153 gl.attach_shader(id, vertex);
154 gl.attach_shader(id, fragment);
155
156 // Bind the color output for desktop GL
157 // https://www.khronos.org/registry/OpenGL-Refpages/gl4/html/glBindFragDataLocation.xhtml
158 // Errors:
159 // 1. colorNumber will always be 0, and therefore cannot overrun the bounds
160 // 2. 'outputColor' does not started with the reserved 'gl_' prefix
161 // 3. 'id' is generated by create_program above
162 #[cfg(not(target_arch = "wasm32"))]
163 gl.bind_frag_data_location(id, 0, "outputColor");
164
165 for (index, attr) in desc.vertex_input.iter().enumerate() {
166 gl.bind_attrib_location(id, index as u32, attr.name());
167 }
168
169 gl.link_program(id);
170 if !gl.get_program_link_status(id) {
171 let info = gl.get_program_info_log(id);
172 log::error!("Failed to link the shader program: {}", info);
173 return Err(GolemError::ShaderCompilationError(info));
174 }
175 log::trace!("Linked shader program succesfully");
176
177 Ok(ShaderProgram {
178 ctx: Context(ctx.0.clone()),
179 id,
180 vertex,
181 fragment,
182 input: desc.vertex_input.to_vec(),
183 })
184 }
185 }
186
187 /// Check if this shader program is currently bound to be operated on
188 pub fn is_bound(&self) -> bool {
189 match *self.ctx.0.current_program.borrow() {
190 Some(program) => self.id == program,
191 None => false,
192 }
193 }
194
195 /// Set a uniform value, assuming the shader is bound by [`ShaderProgram::bind`]
196 pub fn set_uniform(&self, name: &str, uniform: UniformValue) -> Result<(), GolemError> {
197 if self.is_bound() {
198 let gl = &self.ctx.0.gl;
199 let location = unsafe { gl.get_uniform_location(self.id, name) };
200 if location.is_none() {
201 return Err(GolemError::NoSuchUniform(name.to_owned()));
202 }
203 use UniformValue::*;
204 unsafe {
205 match uniform {
206 Int(x) => gl.uniform_1_i32(location, x),
207 IVector2([x, y]) => gl.uniform_2_i32(location, x, y),
208 IVector3([x, y, z]) => gl.uniform_3_i32(location, x, y, z),
209 IVector4([x, y, z, w]) => gl.uniform_4_i32(location, x, y, z, w),
210 Float(x) => gl.uniform_1_f32(location, x),
211 Vector2([x, y]) => gl.uniform_2_f32(location, x, y),
212 Vector3([x, y, z]) => gl.uniform_3_f32(location, x, y, z),
213 Vector4([x, y, z, w]) => gl.uniform_4_f32(location, x, y, z, w),
214 Matrix2(mat) => gl.uniform_matrix_2_f32_slice(location, false, &mat),
215 Matrix3(mat) => gl.uniform_matrix_3_f32_slice(location, false, &mat),
216 Matrix4(mat) => gl.uniform_matrix_4_f32_slice(location, false, &mat),
217 }
218 }
219
220 Ok(())
221 } else {
222 Err(GolemError::NotCurrentProgram)
223 }
224 }
225
226 /// Bind this shader to use it, either to [`set a uniform`] or to [`draw`]
227 ///
228 /// [`set a uniform`]: ShaderProgram::set_uniform
229 /// [`draw`]: ShaderProgram::draw
230 pub fn bind(&mut self) {
231 let gl = &self.ctx.0.gl;
232 log::trace!("Binding the shader and buffers");
233 unsafe {
234 gl.use_program(Some(self.id));
235 }
236 *self.ctx.0.current_program.borrow_mut() = Some(self.id);
237 }
238
239 /// Draw the given elements from the element buffer with this shader
240 ///
241 /// The range should fall within the elements of the buffer (which is checked for via an
242 /// `assert!`.) The GeometryMode determines what the set of indices produces: triangles
243 /// consumes 3 vertices into a filled triangle, lines consumes 2 vertices into a thin line,
244 /// etc.
245 ///
246 /// The `ShaderProgram` must be bound first, see [`ShaderProgram::bind`].
247 ///
248 /// # Safety
249 ///
250 /// The safety concerns to keep in mind:
251 ///
252 /// 1. The elements in the [`ElementBuffer`] are not checked against the size of the
253 /// [`VertexBuffer`]. If they are illegal indices, this will result in out-of-bounds reads on
254 /// the GPU and therefore undefined behavior. The caller is responsible for ensuring all
255 /// elements are valid and in-bounds.
256 ///
257 /// [`Surface::bind`]: crate::Surface::bind
258 pub unsafe fn draw(
259 &self,
260 vb: &VertexBuffer,
261 eb: &ElementBuffer,
262 range: Range<usize>,
263 geometry: GeometryMode,
264 ) -> Result<(), GolemError> {
265 assert!(
266 range.end <= eb.size(),
267 "The range exceeded the size of the element buffer"
268 );
269 // prepare_draw also takes care of ensuring this program is current
270 self.prepare_draw(vb, eb)?;
271 self.draw_prepared(range, geometry);
272 Ok(())
273 }
274
275 /// Set up a [`VertexBuffer`] and [`ElementBuffer`] to draw multiple times with the same
276 /// buffers.
277 ///
278 /// The `ShaderProgram` must be bound first, see [`ShaderProgram::bind`].
279 ///
280 /// See [`ShaderProgram::draw_prepared`] to execute the draw calls. If you're only drawing the
281 /// buffers once before replacing their data, see [`ShaderProgram::draw`].
282 pub fn prepare_draw(&self, vb: &VertexBuffer, eb: &ElementBuffer) -> Result<(), GolemError> {
283 if !self.is_bound() {
284 Err(GolemError::NotCurrentProgram)
285 } else {
286 eb.bind();
287 vb.bind();
288 let stride: i32 = self.input.iter().map(|attr| attr.size()).sum();
289 let stride = stride * size_of::<f32>() as i32;
290 let mut offset = 0;
291 log::trace!("Binding the attributes to draw");
292 let gl = &self.ctx.0.gl;
293 for (index, attr) in self.input.iter().enumerate() {
294 let size = attr.size();
295 unsafe {
296 let pos_attrib = index as u32;
297 gl.enable_vertex_attrib_array(pos_attrib);
298 gl.vertex_attrib_pointer_f32(
299 pos_attrib,
300 size,
301 glow::FLOAT,
302 false,
303 stride,
304 offset,
305 );
306 }
307 offset += size * size_of::<f32>() as i32;
308 }
309 // Disable any dangling vertex attributes
310 let current_max_attrib = self.input.len() as u32;
311 let previous_max_attrib = self.ctx.max_attrib(current_max_attrib);
312 for i in current_max_attrib..previous_max_attrib {
313 unsafe {
314 gl.disable_vertex_attrib_array(i);
315 }
316 }
317
318 Ok(())
319 }
320 }
321
322 /// Draw the given elements from the prepared element buffer with this shader
323 ///
324 /// This relies on the caller having a valid prepared state: see [`prepare_draw`].
325 ///
326 /// # Safety
327 ///
328 /// The safety concerns to keep in mind:
329 ///
330 /// 1. [`prepare_draw`] *must* be called before this method, and the buffers passed to it
331 /// *must* not have their underlying storage changed. Their values can change, but calls to
332 /// `set_data` may cause them to expand and move to a new memory location on the GPU,
333 /// invalidating the cal to preparation. Some calls to [`set_data`] are optimized to calls
334 /// to [`set_sub_data`]; do not rely on this implementation detail.
335 /// 2. No other buffers may be operated on between [`prepare_draw`] and `draw_prepared`. Any
336 /// calls to [`set_data`] or [`set_sub_data`] from a buffer that wasn't passed to
337 /// [`prepare_draw`] will result in the wrong buffer being bound when `draw_prepared` is
338 /// called.
339 /// 3. The elements in the prepared buffer must correspond to valid locations within the vertex
340 /// buffer. See [`draw`] for details.
341 /// 4. This shader must still be bound (see [`bind`])
342 ///
343 /// [`prepare_draw`]: ShaderProgram::prepare_draw
344 /// [`draw`]: ShaderProgram::draw
345 /// [`bind`]: ShaderProgram::bind
346 /// [`set_data`]: crate::Buffer::set_data
347 /// [`set_sub_data`]: crate::Buffer::set_sub_data
348 pub unsafe fn draw_prepared(&self, range: Range<usize>, geometry: GeometryMode) {
349 log::trace!("Dispatching draw command");
350 let length = range.end - range.start;
351 self.ctx.0.gl.draw_elements(
352 ShaderProgram::shape_type(geometry),
353 length as i32,
354 glow::UNSIGNED_INT,
355 (range.start * size_of::<u32>()) as i32,
356 );
357 }
358
359 fn shape_type(geometry: GeometryMode) -> u32 {
360 use GeometryMode::*;
361 match geometry {
362 Points => glow::POINTS,
363 Lines => glow::LINES,
364 LineStrip => glow::LINE_STRIP,
365 LineLoop => glow::LINE_LOOP,
366 TriangleStrip => glow::TRIANGLE_STRIP,
367 TriangleFan => glow::TRIANGLE_FAN,
368 Triangles => glow::TRIANGLES,
369 }
370 }
371}
372
373impl Drop for ShaderProgram {
374 fn drop(&mut self) {
375 let gl = &self.ctx.0.gl;
376 unsafe {
377 gl.delete_program(self.id);
378 gl.delete_shader(self.fragment);
379 gl.delete_shader(self.vertex);
380 }
381 }
382}