emilk/egui · error

Tried to update user texture.

Error message

Tried to update user texture.

What it means

egui-wgpu's update_texture panics with "Tried to update user texture." when an ImageDelta carries a position (an update to an existing texture) but the Texture entry stored under that egui TextureId has texture == None. A None texture means the id refers to a user-managed native wgpu texture registered via register_native_texture, not one egui itself allocated, so egui has no texture of its own to re-upload pixels into. The library panics instead of returning a Result because texture updates happen during the render callback.

Solutions

  1. Never send ImageDelta updates with pos for a TextureId registered via register_native_texture; user textures must be updated by you with your own wgpu queue.write_texture call.
  2. If the image should be managed by egui, allocate it through egui (e.g. ColorImage loaded into a TextureHandle) so the Texture entry has Some(texture) and Some(options).
  3. To replace a native texture's contents, create the new texture, then re-register it (register_native_texture) or use update_texture_with_options on the new id rather than issuing a pos-update on the old id.
  4. Verify which code path produced the TextureId (allocate_user_texture vs register_native_texture) before issuing deltas.

Example fix

// before: egui tries to partially update a native texture
let id = renderer.register_native_texture(&device, &view, wgpu::FilterMode::Linear);
ui.image(egui::ImageSource::Texture(egui::TextureId::User(id.into())));
textures.set(id, ImageDelta { image, pos: Some([0, 0]), options: Default::default() }); // panics
// after: update the native texture yourself
queue.write_texture(
    wgpu::TexelCopyTextureInfo { texture: &native_texture, mip_level: 0, origin: wgpu::Origin3d::ZERO, aspect: wgpu::TextureAspect::All },
    &pixels,
    wgpu::TexelCopyBufferLayout { offset: 0, bytes_per_row: Some(4 * width), rows_per_image: None },
    size,
);
Defensive patterns

Strategy: validation

Validate before calling

// Rust: verify the id was allocated by egui, not registered as a native/user texture
fn can_send_pos_delta(managed_ids: &HashSet<egui::TextureId>, id: egui::TextureId) -> bool {
    managed_ids.contains(&id)
}

Type guard

fn is_egui_managed(managed_ids: &HashSet<egui::TextureId>, id: egui::TextureId) -> bool {
    managed_ids.contains(&id)
}

Prevention

When it happens

Trigger: Calling Renderer::update_texture/update (or painting an egui Image) with a TextureId created by register_native_texture, while the ImageDelta has Some(pos), i.e. egui treats it as a partial update of an existing texture. Also triggered when egui's TextureManager emits a delta with pos for a handle that was backed by a native texture.

Common situations: Mixing egui::load::TextureHandle images with register_native_texture-created ids; passing an egui user texture id into code that assumes egui allocated it; screen-reader/font-atlas partial updates applied to a native texture; version migrations where the same TextureId is now backed by a native texture.

Understand the failure class

Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.

Related errors


AI-assisted analysis of emilk/egui@441971a776 (2026-09-12). Data as JSON: /api/errors/65c8e3cc251a79b4. Report an issue: GitHub.

Appendix: source

Thrown at crates/egui-wgpu/src/renderer.rs:700

                size,
            );
        };

        // Use same label for all resources associated with this texture id (no point in retyping the type)
        let label_str = format!("egui_texid_{id:?}");
        let label = Some(label_str.as_str());

        let (texture, origin, bind_group) = if let Some(pos) = image_delta.pos {
            // update the existing texture
            let Texture {
                texture,
                bind_group,
                options,
            } = self
                .textures
                .remove(&id)
                .expect("Tried to update a texture that has not been allocated yet.");
            let texture = texture.expect("Tried to update user texture.");
            let options = options.expect("Tried to update user texture.");
            let origin = wgpu::Origin3d {
                x: pos[0] as u32,
                y: pos[1] as u32,
                z: 0,
            };

            (
                texture,
                origin,
                // If the TextureOptions are the same as the previous ones, we can reuse the bind group. Otherwise we
                // have to recreate it.
                if image_delta.options == options {
                    Some(bind_group)
                } else {
                    None
                },
            )

View on GitHub (pinned to 441971a776)