Skip to main content

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}