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
- Run rio with the native Vulkan backend on Linux.
- 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
- 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.
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
SugarloafBackend::Vulkan). The wgpu backend is not affected.Steps to reproduce
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 callsrender_image_overlaystwice in one frame: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]):Vulkan draw commands are recorded into the command buffer but execute later at queue submit. So the second
render_image_overlayscall 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, therender<'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_overlayscall, 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.