From eb4e87a1b198a04339501c17c38456dfea23b652 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Benoi=CC=82t=20Rouleau?= Date: Sat, 17 Oct 2026 14:35:01 -0400 Subject: [PATCH 1/2] gl: start compiling a shader without waiting for it `new_shader` compiles a program's shaders, links it and asks how each step went, and the asking waits for the driver to finish. An app that makes many shaders as it loads waits for each in turn, even where the driver could compile them all at once. WebKit is the slowest at it: 38 programs from a macroquad game (CHOMP) take 4.8 s to compile one after the other on a Mac with nothing cached, and 1.0 s when every one starts before any is checked. `RenderingBackend::start_shader` compiles and links a shader without asking how it went, and `finish_shader` waits for it and returns what `new_shader` would have. In between, `is_shader_compiling` says whether `finish_shader` would still wait, with `KHR_parallel_shader_compile` (or `ARB_parallel_shader_compile`), so an app can wait for its shaders across frames instead of stopping on one. Without the extension it says `false`. Metal compiles in `start_shader`, as in `new_shader`. WebGL only enables an extension once it's asked for, so gl.js gains `webgl_enable_extension`, which also gives `has_extension` a wasm version. `new_shader` is now `start_shader` and `finish_shader`. Co-Authored-By: Claude Opus 5.5 --- js/gl.js | 3 ++ src/graphics.rs | 29 +++++++++++ src/graphics/gl.rs | 109 +++++++++++++++++++++++++++++++++++---- src/native/gl.rs | 1 + src/native/wasm/webgl.rs | 3 ++ 5 files changed, 136 insertions(+), 9 deletions(-) diff --git a/js/gl.js b/js/gl.js index df0116ac9..28c229c91 100644 --- a/js/gl.js +++ b/js/gl.js @@ -1010,6 +1010,9 @@ var importObject = { stringToUTF8(parameter, array, 0, len); return msg; }, + webgl_enable_extension: function (name, length) { + return gl.getExtension(UTF8ToString(name, length)) !== null; + }, glCompileShader: function (shader, count, string, length) { GL.validateGLObjectID(GL.shaders, shader, 'glCompileShader', 'shader'); gl.compileShader(GL.shaders[shader]); diff --git a/src/graphics.rs b/src/graphics.rs index dbdfffc65..05b034ca5 100644 --- a/src/graphics.rs +++ b/src/graphics.rs @@ -282,6 +282,15 @@ impl Display for ShaderType { } } +/// A shader [`RenderingBackend::start_shader`] started, for +/// [`RenderingBackend::finish_shader`] to finish. +pub struct PendingShader(PendingShaderInner); + +enum PendingShaderInner { + Finished(Result), + Gl(gl::LinkingProgram), +} + #[derive(Clone, Debug)] pub enum ShaderError { CompilationError { @@ -1128,6 +1137,26 @@ pub trait RenderingBackend { shader: ShaderSource, meta: ShaderMeta, ) -> Result; + /// Starts making a shader as [`new_shader`](Self::new_shader) does, without + /// waiting for it to compile, so the shaders started before any is finished + /// compile at once where the driver can. [`finish_shader`](Self::finish_shader) + /// then returns what `new_shader` would have. + fn start_shader(&mut self, shader: ShaderSource, meta: ShaderMeta) -> PendingShader { + PendingShader(PendingShaderInner::Finished(self.new_shader(shader, meta))) + } + /// Whether `shader` is still compiling, so [`finish_shader`](Self::finish_shader) + /// would wait for it. Only OpenGL with `KHR_parallel_shader_compile` can tell; + /// otherwise this is `false`. + fn is_shader_compiling(&self, _shader: &PendingShader) -> bool { + false + } + /// Waits for `shader` to compile, if it still is, and returns it. + fn finish_shader(&mut self, shader: PendingShader) -> Result { + match shader.0 { + PendingShaderInner::Finished(result) => result, + PendingShaderInner::Gl(_) => panic!("OpenGL shader on another context"), + } + } fn new_texture( &mut self, access: TextureAccess, diff --git a/src/graphics/gl.rs b/src/graphics/gl.rs index 5fb34c096..f11aaa459 100644 --- a/src/graphics/gl.rs +++ b/src/graphics/gl.rs @@ -519,6 +519,9 @@ pub struct GlContext { default_framebuffer: GLuint, pub(crate) cache: GlCache, pub(crate) info: ContextInfo, + /// Whether the driver says when a program is done compiling and + /// linking, with `KHR_parallel_shader_compile`. + parallel_shader_compile: bool, } impl Default for GlContext { @@ -548,6 +551,8 @@ impl GlContext { buffers: ResourceManager::default(), textures: Textures(vec![]), info, + parallel_shader_compile: has_extension("GL_KHR_parallel_shader_compile") + || has_extension("GL_ARB_parallel_shader_compile"), cache: GlCache { stored_index_buffer: 0, stored_index_type: None, @@ -579,20 +584,51 @@ impl GlContext { } } -fn load_shader_internal( +/// A program linking, and the shaders it links, which [`finish_program`] checks. +pub(super) struct LinkingProgram { + program: GLuint, + vertex_shader: GLuint, + fragment_shader: GLuint, + meta: ShaderMeta, +} + +/// Compiles and links a program without asking how either went, which +/// would wait for them. +fn start_program( vertex_shader: &str, fragment_shader: &str, meta: ShaderMeta, -) -> Result { +) -> Result { unsafe { - let vertex_shader = load_shader(GL_VERTEX_SHADER, vertex_shader)?; - let fragment_shader = load_shader(GL_FRAGMENT_SHADER, fragment_shader)?; + let vertex_shader = compile_shader(GL_VERTEX_SHADER, vertex_shader)?; + let fragment_shader = compile_shader(GL_FRAGMENT_SHADER, fragment_shader)?; let program = glCreateProgram(); glAttachShader(program, vertex_shader); glAttachShader(program, fragment_shader); glLinkProgram(program); + Ok(LinkingProgram { + program, + vertex_shader, + fragment_shader, + meta, + }) + } +} + +/// Waits for `linking` to compile and link, and finds its uniforms. +fn finish_program(linking: LinkingProgram) -> Result { + let LinkingProgram { + program, + vertex_shader, + fragment_shader, + meta, + } = linking; + unsafe { + check_shader(GL_VERTEX_SHADER, vertex_shader)?; + check_shader(GL_FRAGMENT_SHADER, fragment_shader)?; + // delete no longer used shaders glDetachShader(program, vertex_shader); glDeleteShader(vertex_shader); @@ -643,7 +679,7 @@ fn load_shader_internal( } } -pub fn load_shader(shader_type: GLenum, source: &str) -> Result { +fn compile_shader(shader_type: GLenum, source: &str) -> Result { unsafe { let shader = glCreateShader(shader_type); assert!(shader != 0); @@ -653,6 +689,12 @@ pub fn load_shader(shader_type: GLenum, source: &str) -> Result Result<(), ShaderError> { + unsafe { let mut is_compiled = 0; glGetShaderiv(shader, GL_COMPILE_STATUS, &mut is_compiled as *mut _); if is_compiled == 0 { @@ -686,8 +728,7 @@ pub fn load_shader(shader_type: GLenum, source: &str) -> Result ContextInfo { } } +/// Whether the context lists `name` among its extensions. +#[cfg(not(target_arch = "wasm32"))] +fn has_extension(name: &str) -> bool { + let mut count = 0; + unsafe { glGetIntegerv(GL_NUM_EXTENSIONS, &mut count) }; + (0..count as GLuint).any(|index| { + let extension = unsafe { glGetStringi(GL_EXTENSIONS, index) }; + !extension.is_null() + && unsafe { std::ffi::CStr::from_ptr(extension as _) }.to_bytes() == name.as_bytes() + }) +} + +/// Whether the context has extension `name`, which WebGL names without +/// its `GL_` prefix, and enables only once it's asked for. +#[cfg(target_arch = "wasm32")] +fn has_extension(name: &str) -> bool { + let name = name.strip_prefix("GL_").unwrap_or(name); + unsafe { webgl_enable_extension(name.as_ptr(), name.len()) } +} + impl RenderingBackend for GlContext { fn info(&self) -> ContextInfo { self.info.clone() @@ -891,12 +952,42 @@ impl RenderingBackend for GlContext { shader: ShaderSource, meta: ShaderMeta, ) -> Result { + let shader = self.start_shader(shader, meta); + self.finish_shader(shader) + } + + fn start_shader(&mut self, shader: ShaderSource, meta: ShaderMeta) -> PendingShader { let (fragment, vertex) = match shader { ShaderSource::Glsl { fragment, vertex } => (fragment, vertex), _ => panic!("Metal source on OpenGl context"), }; - let shader = load_shader_internal(vertex, fragment, meta)?; - Ok(ShaderId(self.shaders.add(shader))) + PendingShader(match start_program(vertex, fragment, meta) { + Ok(linking) => PendingShaderInner::Gl(linking), + Err(error) => PendingShaderInner::Finished(Err(error)), + }) + } + + fn is_shader_compiling(&self, shader: &PendingShader) -> bool { + match &shader.0 { + PendingShaderInner::Gl(linking) if self.parallel_shader_compile => { + let mut completed = 0; + unsafe { + glGetProgramiv(linking.program, GL_COMPLETION_STATUS_KHR, &mut completed) + }; + completed == 0 + } + _ => false, + } + } + + fn finish_shader(&mut self, shader: PendingShader) -> Result { + match shader.0 { + PendingShaderInner::Finished(result) => result, + PendingShaderInner::Gl(linking) => { + let shader = finish_program(linking)?; + Ok(ShaderId(self.shaders.add(shader))) + } + } } fn new_texture( diff --git a/src/native/gl.rs b/src/native/gl.rs index 145b74034..578d6a553 100644 --- a/src/native/gl.rs +++ b/src/native/gl.rs @@ -53,6 +53,7 @@ pub const GL_SHORT: u32 = 0x1402; pub const GL_DEPTH_TEST: u32 = 0x0B71; pub const GL_TEXTURE_CUBE_MAP_NEGATIVE_Y: u32 = 0x8518; pub const GL_LINK_STATUS: u32 = 0x8B82; +pub const GL_COMPLETION_STATUS_KHR: u32 = 0x91B1; pub const GL_TEXTURE_CUBE_MAP_POSITIVE_Y: u32 = 0x8517; pub const GL_SAMPLE_ALPHA_TO_COVERAGE: u32 = 0x809E; pub const GL_RGBA16F: u32 = 0x881A; diff --git a/src/native/wasm/webgl.rs b/src/native/wasm/webgl.rs index 3061f0d02..d04f86445 100644 --- a/src/native/wasm/webgl.rs +++ b/src/native/wasm/webgl.rs @@ -58,6 +58,7 @@ pub const GL_SHORT: u32 = 0x1402; pub const GL_DEPTH_TEST: u32 = 0x0B71; pub const GL_TEXTURE_CUBE_MAP_NEGATIVE_Y: u32 = 0x8518; pub const GL_LINK_STATUS: u32 = 0x8B82; +pub const GL_COMPLETION_STATUS_KHR: u32 = 0x91B1; pub const GL_TEXTURE_CUBE_MAP_POSITIVE_Y: u32 = 0x8517; pub const GL_SAMPLE_ALPHA_TO_COVERAGE: u32 = 0x809E; pub const GL_RGBA16F: u32 = 0x881A; @@ -853,6 +854,8 @@ extern "C" { pub fn glClearBufferfv(buffer: GLenum, drawbuffer: GLint, value: *const GLfloat); pub fn glClearBufferfi(buffer: GLenum, drawbuffer: GLint, depth: GLfloat, stencil: GLint); pub fn glGetStringi(name: GLenum, index: GLuint) -> *const GLubyte; + /// Enables the WebGL extension `name`, and says whether the context has it. + pub fn webgl_enable_extension(name: *const u8, length: usize) -> bool; pub fn glCopyBufferSubData( readTarget: GLenum, writeTarget: GLenum, From bcf844c60dbf0dbdae5eb95b371fcb97ffe1f0f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Benoi=CC=82t=20Rouleau?= Date: Sat, 17 Oct 2026 14:41:37 -0400 Subject: [PATCH 2/2] js: fill a program's uniform table once a uniform is asked for `glLinkProgram` read every active uniform right after linking, which waits for the link to finish, so on WebGL every program still compiled and linked one after the other: 150 ms each in WebKit with nothing cached. The table now fills on the first `glGetUniformLocation` after a link, and `glGetProgramiv` checks the program itself rather than its table, so it can ask for `COMPLETION_STATUS_KHR` without waiting. Co-Authored-By: Claude Opus 5.5 --- js/gl.js | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/js/gl.js b/js/gl.js index 28c229c91..f735d90b6 100644 --- a/js/gl.js +++ b/js/gl.js @@ -795,7 +795,10 @@ var importObject = { name = name.slice(0, leftBrace); } - var uniformInfo = GL.programInfos[program] && GL.programInfos[program].uniforms[name]; // returns pair [ dimension_of_uniform_array, uniform_location ] + if (!GL.programInfos[program]) { + GL.populateUniformTable(program); + } + var uniformInfo = GL.programInfos[program].uniforms[name]; // returns pair [ dimension_of_uniform_array, uniform_location ] if (uniformInfo && arrayIndex >= 0 && arrayIndex < uniformInfo[0]) { // Check if user asked for an out-of-bounds element, i.e. for 'vec4 colors[3];' user could ask for 'colors[10]' which should return -1. return uniformInfo[1] + arrayIndex; } else { @@ -891,7 +894,9 @@ var importObject = { glLinkProgram: function (program) { GL.validateGLObjectID(GL.programs, program, 'glLinkProgram', 'program'); gl.linkProgram(GL.programs[program]); - GL.populateUniformTable(program); + // Reading the uniforms waits for the link to finish, so the + // table fills once a uniform is first asked for. + delete GL.programInfos[program]; }, glPixelStorei: function (pname, param) { gl.pixelStorei(pname, param); @@ -907,8 +912,7 @@ var importObject = { console.error("GL_INVALID_VALUE in glGetProgramiv"); return; } - var ptable = GL.programInfos[program]; - if (!ptable) { + if (!GL.programs[program]) { console.error('GL_INVALID_OPERATION in glGetProgramiv(program=' + program + ', pname=' + pname + ', p=0x' + p.toString(16) + '): The specified GL object name does not refer to a program object!'); return; }