From 33f83ff96e5c1dd0135ba8ec7d541670b60bb224 Mon Sep 17 00:00:00 2001 From: Laurenz Stampfl Date: Tue, 10 Mar 2026 15:20:57 +0100 Subject: [PATCH 1/2] vello_hybrid: Avoid unnecessarily allocating `Uint32Array`s --- .../vello_hybrid/src/render/webgl.rs | 86 ++++++++++++------- 1 file changed, 55 insertions(+), 31 deletions(-) diff --git a/sparse_strips/vello_hybrid/src/render/webgl.rs b/sparse_strips/vello_hybrid/src/render/webgl.rs index cbba6f7d26..13774c9ae6 100644 --- a/sparse_strips/vello_hybrid/src/render/webgl.rs +++ b/sparse_strips/vello_hybrid/src/render/webgl.rs @@ -1072,22 +1072,43 @@ impl WebGlPrograms { Some(&self.resources.alphas_texture), ); - // Pack alpha values into RGBA uint32 texture - let alpha_data_as_u32 = bytemuck::cast_slice::(alphas); - let packed_array = js_sys::Uint32Array::from(alpha_data_as_u32); + { + // Pack alpha values into RGBA uint32 texture + let alpha_data_as_u32 = bytemuck::cast_slice::(alphas); + + // Safety: This calling `Uint32Array::view` is unsafe because it provides a view into + // WASM linear memory, and any additional allocations might invalidate that view. + // In our case, this is not an issue because we only use this view once for uploading + // data to the GPU below, and no allocations happen between that. + // The `tex_image_2d` method is synchronous in the sense that once it returns, it is guaranteed + // that all necessary data has already been read, so any allocations that happen + // after this block don't affect this anymore. + // + // See also: https://wikis.khronos.org/opengl/Synchronization + // >> There are several OpenGL functions that can pull data directly from client-side memory, + // >> or push data directly into client-side memory. Functions like `glTexSubImage2D`, + // >> `glReadPixels`, `glBufferSubData` and so forth. + // + // >> Because OpenGL is defined to be synchronous, when any of these functions have + // >> returned, they must have finished with the client memory. When `glReadPixels` returns, + // >> the pixel data is in your client memory (unless you are reading into a buffer object). + // >> When `glBufferSubData` returns, you can immediately modify or delete whatever memory + // >> pointer you gave it, as OpenGL has already read as much as it wants. + let packed_array = unsafe { js_sys::Uint32Array::view(alpha_data_as_u32) }; - gl.tex_image_2d_with_i32_and_i32_and_i32_and_format_and_type_and_opt_array_buffer_view( - WebGl2RenderingContext::TEXTURE_2D, - 0, - WebGl2RenderingContext::RGBA32UI as i32, - alpha_texture_width as i32, - alpha_texture_height as i32, - 0, - WebGl2RenderingContext::RGBA_INTEGER, - WebGl2RenderingContext::UNSIGNED_INT, - Some(&packed_array), - ) - .unwrap(); + gl.tex_image_2d_with_i32_and_i32_and_i32_and_format_and_type_and_opt_array_buffer_view( + WebGl2RenderingContext::TEXTURE_2D, + 0, + WebGl2RenderingContext::RGBA32UI as i32, + alpha_texture_width as i32, + alpha_texture_height as i32, + 0, + WebGl2RenderingContext::RGBA_INTEGER, + WebGl2RenderingContext::UNSIGNED_INT, + Some(&packed_array), + ) + .unwrap(); + } // Truncate back to the original size. alphas.truncate(original_len); @@ -1111,23 +1132,26 @@ impl WebGlPrograms { Some(&self.resources.encoded_paints_texture), ); - // Pack encoded paints into RGBA uint32 texture - let encoded_paints_data_as_u32 = - bytemuck::cast_slice::(&self.encoded_paints_data); - let packed_array = js_sys::Uint32Array::from(encoded_paints_data_as_u32); + { + // Pack encoded paints into RGBA uint32 texture + let encoded_paints_data_as_u32 = + bytemuck::cast_slice::(&self.encoded_paints_data); + // Safety: See the comment in `upload_alpha_texture` + let packed_array = unsafe { js_sys::Uint32Array::view(encoded_paints_data_as_u32) }; - gl.tex_image_2d_with_i32_and_i32_and_i32_and_format_and_type_and_opt_array_buffer_view( - WebGl2RenderingContext::TEXTURE_2D, - 0, - WebGl2RenderingContext::RGBA32UI as i32, - encoded_paints_texture_width as i32, - encoded_paints_texture_height as i32, - 0, - WebGl2RenderingContext::RGBA_INTEGER, - WebGl2RenderingContext::UNSIGNED_INT, - Some(&packed_array), - ) - .unwrap(); + gl.tex_image_2d_with_i32_and_i32_and_i32_and_format_and_type_and_opt_array_buffer_view( + WebGl2RenderingContext::TEXTURE_2D, + 0, + WebGl2RenderingContext::RGBA32UI as i32, + encoded_paints_texture_width as i32, + encoded_paints_texture_height as i32, + 0, + WebGl2RenderingContext::RGBA_INTEGER, + WebGl2RenderingContext::UNSIGNED_INT, + Some(&packed_array), + ) + .unwrap(); + } } } From aa190ba27d7f30f20bdacecef01e67b4ba6826c2 Mon Sep 17 00:00:00 2001 From: Laurenz Stampfl Date: Thu, 12 Mar 2026 08:34:52 +0100 Subject: [PATCH 2/2] Encapsulate --- .../vello_hybrid/src/render/webgl.rs | 110 +++++++++--------- 1 file changed, 53 insertions(+), 57 deletions(-) diff --git a/sparse_strips/vello_hybrid/src/render/webgl.rs b/sparse_strips/vello_hybrid/src/render/webgl.rs index 13774c9ae6..960ff423f2 100644 --- a/sparse_strips/vello_hybrid/src/render/webgl.rs +++ b/sparse_strips/vello_hybrid/src/render/webgl.rs @@ -1072,43 +1072,12 @@ impl WebGlPrograms { Some(&self.resources.alphas_texture), ); - { - // Pack alpha values into RGBA uint32 texture - let alpha_data_as_u32 = bytemuck::cast_slice::(alphas); - - // Safety: This calling `Uint32Array::view` is unsafe because it provides a view into - // WASM linear memory, and any additional allocations might invalidate that view. - // In our case, this is not an issue because we only use this view once for uploading - // data to the GPU below, and no allocations happen between that. - // The `tex_image_2d` method is synchronous in the sense that once it returns, it is guaranteed - // that all necessary data has already been read, so any allocations that happen - // after this block don't affect this anymore. - // - // See also: https://wikis.khronos.org/opengl/Synchronization - // >> There are several OpenGL functions that can pull data directly from client-side memory, - // >> or push data directly into client-side memory. Functions like `glTexSubImage2D`, - // >> `glReadPixels`, `glBufferSubData` and so forth. - // - // >> Because OpenGL is defined to be synchronous, when any of these functions have - // >> returned, they must have finished with the client memory. When `glReadPixels` returns, - // >> the pixel data is in your client memory (unless you are reading into a buffer object). - // >> When `glBufferSubData` returns, you can immediately modify or delete whatever memory - // >> pointer you gave it, as OpenGL has already read as much as it wants. - let packed_array = unsafe { js_sys::Uint32Array::view(alpha_data_as_u32) }; - - gl.tex_image_2d_with_i32_and_i32_and_i32_and_format_and_type_and_opt_array_buffer_view( - WebGl2RenderingContext::TEXTURE_2D, - 0, - WebGl2RenderingContext::RGBA32UI as i32, - alpha_texture_width as i32, - alpha_texture_height as i32, - 0, - WebGl2RenderingContext::RGBA_INTEGER, - WebGl2RenderingContext::UNSIGNED_INT, - Some(&packed_array), - ) - .unwrap(); - } + upload_data_to_rgba32_texture( + gl, + bytemuck::cast_slice::(alphas), + alpha_texture_width, + alpha_texture_height, + ); // Truncate back to the original size. alphas.truncate(original_len); @@ -1132,26 +1101,12 @@ impl WebGlPrograms { Some(&self.resources.encoded_paints_texture), ); - { - // Pack encoded paints into RGBA uint32 texture - let encoded_paints_data_as_u32 = - bytemuck::cast_slice::(&self.encoded_paints_data); - // Safety: See the comment in `upload_alpha_texture` - let packed_array = unsafe { js_sys::Uint32Array::view(encoded_paints_data_as_u32) }; - - gl.tex_image_2d_with_i32_and_i32_and_i32_and_format_and_type_and_opt_array_buffer_view( - WebGl2RenderingContext::TEXTURE_2D, - 0, - WebGl2RenderingContext::RGBA32UI as i32, - encoded_paints_texture_width as i32, - encoded_paints_texture_height as i32, - 0, - WebGl2RenderingContext::RGBA_INTEGER, - WebGl2RenderingContext::UNSIGNED_INT, - Some(&packed_array), - ) - .unwrap(); - } + upload_data_to_rgba32_texture( + gl, + bytemuck::cast_slice::(&self.encoded_paints_data), + encoded_paints_texture_width, + encoded_paints_texture_height, + ); } } @@ -2259,3 +2214,44 @@ fn copy_to_texture_array_layer( // Clean up gl.delete_framebuffer(Some(&read_framebuffer)); } + +// Upload the data to the currently bound texture assuming a RGBA32UI format. +fn upload_data_to_rgba32_texture( + gl: &WebGl2RenderingContext, + data: &[u32], + texture_width: u32, + texture_height: u32, +) { + // Safety: This calling `Uint32Array::view` is unsafe because it provides a view into + // WASM linear memory, and any additional allocations might invalidate that view. + // In our case, this is not an issue because we only use this view once for uploading + // data to the GPU below, and no allocations happen between that. + // The `tex_image_2d` method is synchronous in the sense that once it returns, it is guaranteed + // that all necessary data has already been read, so any allocations that happen + // after this block don't affect this anymore. + // + // See also: https://wikis.khronos.org/opengl/Synchronization + // >> There are several OpenGL functions that can pull data directly from client-side memory, + // >> or push data directly into client-side memory. Functions like `glTexSubImage2D`, + // >> `glReadPixels`, `glBufferSubData` and so forth. + // + // >> Because OpenGL is defined to be synchronous, when any of these functions have + // >> returned, they must have finished with the client memory. When `glReadPixels` returns, + // >> the pixel data is in your client memory (unless you are reading into a buffer object). + // >> When `glBufferSubData` returns, you can immediately modify or delete whatever memory + // >> pointer you gave it, as OpenGL has already read as much as it wants. + let packed_array = unsafe { js_sys::Uint32Array::view(data) }; + + gl.tex_image_2d_with_i32_and_i32_and_i32_and_format_and_type_and_opt_array_buffer_view( + WebGl2RenderingContext::TEXTURE_2D, + 0, + WebGl2RenderingContext::RGBA32UI as i32, + texture_width as i32, + texture_height as i32, + 0, + WebGl2RenderingContext::RGBA_INTEGER, + WebGl2RenderingContext::UNSIGNED_INT, + Some(&packed_array), + ) + .unwrap(); +}