Skip to content

Vulkan: only one image renders when two image layers are on screen (the other blanks) #1761

Description

@cantona

Summary

On the native Vulkan backend, when two image layers are on screen at the same time (a kitty image and a sixel/iTerm2 image, or a "below text" and an "above text" placement), only one of them renders — the other draws blank. Which one survives depends on draw order: whichever image layer is submitted last wins, and the earlier one disappears.

Environment

  • Backend: native Vulkan (Linux; SugarloafBackend::Vulkan). The wgpu backend is not affected.
  • Reproduced on Linux / Intel UHD 630 (Vulkan).

Steps to reproduce

  1. Run rio with the native Vulkan backend on Linux.
  2. Display a kitty image, then a sixel image right after (or vice versa), e.g.:
    chafa -f kitty  image.png
    chafa -f sixels image.png
    
  3. Observe: the image drawn first goes blank; only the second renders.
    • kitty → sixel: the sixel is blank.
    • sixel → kitty: the kitty image is blank.

The two images do not need to overlap on screen — a non-overlapping kitty (top) and sixel (bottom) still exhibit it.

Root cause

Renderer::render_vulkan (sugarloaf/src/renderer/mod.rs) splits placements into two layers and calls render_image_overlays twice in one frame:

let below = /* image_draws where layer == BelowText */;
let above = /* image_draws where layer == AboveText */;
...
brush.render_image_overlays(cmd, slot, viewport, &below);   // BelowText (e.g. sixel, z<0)
brush.render_quads(...);
brush.render_geometry(...);
brush.render_image_overlays(cmd, slot, viewport, &above);   // AboveText (e.g. kitty, z>=0)

But VulkanRenderer::render_image_overlays (sugarloaf/src/renderer/vulkan.rs) writes every instance from offset 0 of a single shared per-slot instance buffer (image_instance_buffers[slot]):

let dst = buf.as_mut_ptr() as *mut ImageInstance;
for (i, (_set, inst)) in draws.iter().enumerate() {
    std::ptr::write(dst.add(i), *inst);   // base offset 0, every call
}

Vulkan draw commands are recorded into the command buffer but execute later at queue submit. So the second render_image_overlays call overwrites the buffer region the first call's draw command still points at, before the GPU runs either draw. At execution time both draws read the last-written instance data → the first layer's placement is gone, and it renders blank.

The wgpu backend avoids this: its two layer passes share a single instance write and index into it, so nothing is overwritten (renderer/mod.rs, the render<'pass> path). The Vulkan path never got the equivalent treatment.

Fix

Collect both layers into one ordered list (BelowText first, then AboveText) and issue a single render_image_overlays call, so every instance keeps its own buffer offset and painter order (list order) still puts BelowText under AboveText. PR attached.

Impact

Any Vulkan-backend session that shows more than one standalone image at once (mixed kitty + sixel/iTerm2, or multiple images spanning both layers) loses all but the last-drawn one. Single-image sessions are unaffected.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions