Skip to content

WebGL2/wasm/Android: GLES buffer, texture, and fence fixes; GLES/Vulkan + egui robustness#353

Open
ohsalmeron wants to merge 3 commits into
kvark:mainfrom
ohsalmeron:main
Open

WebGL2/wasm/Android: GLES buffer, texture, and fence fixes; GLES/Vulkan + egui robustness#353
ohsalmeron wants to merge 3 commits into
kvark:mainfrom
ohsalmeron:main

Conversation

@ohsalmeron
Copy link
Copy Markdown

Problem

Running Blade on WebGL2 / wasm hit spec and browser constraints: the same WebGLBuffer cannot be bound as both ARRAY_BUFFER and ELEMENT_ARRAY_BUFFER; clientWaitSync cannot block on the main thread; texture unit / unpack state could produce wrong sampling or extra copies (e.g. Firefox). On GLES, legacy drivers needed safer uniform-block lookup, correct scissor/wrap/format handling, and explicit buffer sync. blade-egui needed a proper vertex layout/fetch path and deferred texture destruction after submit. Vulkan debug utils were assumed present.

What changed

  • wasm / WebGL2: separate index buffer path, client_wait_sync timeout 0 on wasm (non-wasm clamped to MAX_CLIENT_WAIT_TIMEOUT_WEBGL), texture target unbind hygiene, UNPACK_ROW_LENGTH only when needed (restored to 0), shader texture slots tracked and set once.
  • GLES: scissor Y flip for GL coordinates, integer internal formats, default CLAMP_TO_EDGE, attribute location binding (incl. e_ prefix), uniform-block reflection fallbacks + logging, BufferBelt::sync, EGL platform fallback when surfaceless init fails.
  • blade-egui: vertex input layout + fetch, WGSL vertex inputs / gamma tweak, textures-to-free queue + sync() after submit.
  • Vulkan: enable and use VK_EXT_debug_utils only when the extension exists; guard naming calls.

Testing

  • wasm32-unknown-unknown — Chrome / Firefox WebGL2
  • Android GLES (e.g. older Mali) if you have a device
  • [X ] Native Vulkan smoke (adapter without debug utils)

Note

The web backend sets the canvas pixel size from config (element id "blade" in our tree). Happy to make that id/config upstream-friendly if you want a different default.

Multiple fixes and improvements across graphics backends and egui integration:

- blade-egui: Replace raw vertex storage with a proper vertex input layout (pos/uv/color), add egui vertex VertexLayout and VertexFetchState, bind vertex buffers, add textures_to_free queue and sync() method, and move texture free logic into after_submit. Update WGSL shader to use vec2 inputs and simplify gamma conversion with mix.

- GLES: PassEncoder now carries target_size; scissor rect Y is inverted to match GL window coordinates. Use integer enums for integer texture formats, set texture wrap to CLAMP_TO_EDGE by default. Pipeline now binds attribute locations (including an e_ prefixed name) and adds robust uniform-block lookup with fallbacks and logging of available blocks. EGL platform selection improved with error handling and fallback to default when surfaceless fails.

- Vulkan: Make debug utils optional and only enable the extension when available; guard debug utils calls, conditionally create device debug utils, and log adapter inspection rejections. Ensure debug object naming is invoked only when extension is present.

- Shader processing: Only replace module types when bindings were actually modified and ensure attributes have bindings assigned or assert appropriately.

- Utility: Add BufferBelt::sync to flush active buffers to the GPU.

These changes improve robustness, platform compatibility, and correctness of vertex/texture handling across backends.
Add WebAssembly-specific handling and performance fixes for GLES backend:

- Introduce a separate raw_index buffer for ELEMENT_ARRAY_BUFFER on wasm and create/bind/upload/delete it alongside ARRAY_BUFFER to satisfy WebGL requirements.
- Use raw_index when binding index buffers on wasm builds.
- Avoid setting UNPACK_ROW_LENGTH when row length equals width (prevents Firefox defensive copy); restore UNPACK_ROW_LENGTH to 0 after operations.
- On texture unit binding for wasm, explicitly unbind other texture targets when binding a target to avoid stale bindings.
- Adjust client_wait_sync timeout handling for WebGL2: use 0 on wasm (blocking forbidden) and clamp non-wasm timeouts to MAX_TIMEOUT.
- Improve shader texture binding: track assigned texture slots and set uniform once (avoid repeatedly querying uniform slot values).
- Set canvas element size from config in web backend (lookup element with id "blade" and set width/height).

These changes improve WebGL compatibility, avoid browser-specific performance pitfalls, and ensure correct buffer/texture behavior on wasm targets.
Copy link
Copy Markdown
Owner

@kvark kvark left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks to be very well thought, thank you for filing!

target,
} => {
gl.active_texture(glow::TEXTURE0 + slot);
#[cfg(target_arch = "wasm32")]
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we turn this into a normal if condition, e.g. if cfg!(target_arch = "wasm32") {...}

ptr::null_mut(),
&display_attributes,
)
.unwrap(),
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this was actually intentional: we don't expect it to fail, and we do want to see the error, not just skip over it

);
match d {
Ok(display) => (display, None),
Err(e) => {
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why would this fail?

#[cfg(target_arch = "wasm32")]
let timeout_ns_i32 = 0i32;
#[cfg(not(target_arch = "wasm32"))]
let timeout_ns_i32 = {
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

let's prefer run-time checks over compile time: more compilation code coverage

Tf::R32Uint => (glow::R32UI, glow::RED, glow::UNSIGNED_INT),
Tf::Rg32Uint => (glow::RG32UI, glow::RG, glow::UNSIGNED_INT),
Tf::Rgba32Uint => (glow::RGBA32UI, glow::RGBA, glow::UNSIGNED_INT),
Tf::R32Uint => (glow::R32UI, glow::RED_INTEGER, glow::UNSIGNED_INT),
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice catch!


let location = attributes.len() as u32;
gl.bind_attrib_location(program, location, attrib_name);
gl.bind_attrib_location(program, location, &format!("e_{}", attrib_name));
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what's happening here? why are we binding it twice?

gl.get_uniform_i32(program, location, &mut slots);
targets.push(slots[0] as u32);
let slot = *texture_slots
.entry(glsl_name.to_string())
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

when would this happen? is this about using the same texture in multiple stages?

let mut index_opt = gl.get_uniform_block_index(program, glsl_name);
if index_opt.is_none() {
if let Some(ref type_name) = sf.shader.module.types[var.ty].name
{
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: not formatted?

}
}
if index_opt.is_none() {
let type_name = format!("type_{}", var.ty.index());
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why so many paths to bind?

pub struct Buffer {
raw: glow::Buffer,
#[cfg(target_arch = "wasm32")]
raw_index: glow::Buffer,
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is really unfortunate. It makes all of the buffers on WebGL less efficient, just because there is one quirk in the spec.
What do you think of a counter-proposal: we just assert that the user isn't hitting this path on webgl but keep the implementation clean? that would lift the burden to the user.

Remove redundant unsafe blocks and clean up Metal API usage across the Metal backend. Changes touch command.rs, mod.rs, pipeline.rs and surface.rs: create_* and new() calls no longer wrapped in unsafe, autoreleasepool closures simplified, render layer configuration unwrapped, and vertex/attribute descriptor handling reformatted. The MTLCounterSampleBufferDescriptor sample count is still set inside an unsafe block where required. These edits improve safety/readability without changing runtime behavior.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants