GPU-accelerated UI toolkit (Vulkan)
git clone https://git.lucas.co/cce-ui.git
fix: describe vertex location 4 on the image pipeline
The image pipeline reuses the glyph shader, whose vertex entry point declares
locations 0..=4, but its VkVertexInputAttributeDescription list stopped at 3 —
ImageVertex was missing GlyphVertex's trailing clip_extents. The validation
layer flagged it (VUID-VkGraphicsPipelineCreateInfo-Input-07904) and, without
vertexAttributeRobustness, location 4 read undefined data.
Inert in practice today: images pass clip_circle = [0;3], so the shader's clip
branch never runs and the garbage is never consumed. It would have become a
real bug the moment image clipping was switched on.
ImageVertex now matches GlyphVertex field-for-field (52 bytes, offsets
0/8/16/32/44) with clip_extents = [0;2], the shader's documented
plain-circle degenerate case.
Verified: validation error gone, and the demo gallery's gradient panels --
which go through this pipeline -- are pixel-identical to the pre-fix capture.
Co-Authored-By: Claude Fable 5 <[email protected]>
src/vk/image.rs | 25 +++++++++++++++++++++----
1 file changed, 21 insertions(+), 4 deletions(-)
diff --git a/src/vk/image.rs b/src/vk/image.rs
index cbb2264..08ab1b5 100644
--- a/src/vk/image.rs
+++ b/src/vk/image.rs
@@ -58,11 +58,17 @@ pub fn free_image(id: u32) {
#[repr(C)]
#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
+/// Must match `GlyphVertex` field-for-field: both pipelines are fed by the same
+/// glyph shader, whose vertex entry point declares locations 0..=4. Omitting
+/// `clip_extents` here left location 4 with no `VkVertexInputAttributeDescription`,
+/// which the validation layer flags (VUID-VkGraphicsPipelineCreateInfo-Input-07904)
+/// and which reads undefined data without `vertexAttributeRobustness`.
struct ImageVertex {
position: [f32; 2],
uv: [f32; 2],
color: [f32; 4],
clip_circle: [f32; 3],
+ clip_extents: [f32; 2],
}
struct GpuImage {
@@ -160,6 +166,13 @@ impl ImageStage {
.binding(0)
.format(vk::Format::R32G32B32_SFLOAT)
.offset(32),
+ // Location 4 is declared by the shared glyph shader; without this
+ // entry the pipeline is invalid and the attribute reads undefined.
+ vk::VertexInputAttributeDescription::default()
+ .location(4)
+ .binding(0)
+ .format(vk::Format::R32G32_SFLOAT)
+ .offset(44),
];
let vertex_input = vk::PipelineVertexInputStateCreateInfo::default()
.vertex_binding_descriptions(&vertex_bindings)
@@ -495,10 +508,14 @@ impl ImageStage {
let ndc = |px: f32, py: f32| [(px / sw) * 2.0 - 1.0, 1.0 - (py / sh) * 2.0];
let color = [1.0, 1.0, 1.0, q.alpha];
let clip_circle = [0.0; 3];
- let tl = ImageVertex { position: ndc(x, y), uv: [0.0, 0.0], color, clip_circle };
- let tr = ImageVertex { position: ndc(x + w, y), uv: [1.0, 0.0], color, clip_circle };
- let bl = ImageVertex { position: ndc(x, y + h), uv: [0.0, 1.0], color, clip_circle };
- let br = ImageVertex { position: ndc(x + w, y + h), uv: [1.0, 1.0], color, clip_circle };
+ // Zero extents = the shader's plain-circle clip degenerate case. Inert
+ // while clip_circle.z is 0 (the clip branch never runs), but it must be
+ // a defined value, not whatever the missing attribute used to read.
+ let clip_extents = [0.0; 2];
+ let tl = ImageVertex { position: ndc(x, y), uv: [0.0, 0.0], color, clip_circle, clip_extents };
+ let tr = ImageVertex { position: ndc(x + w, y), uv: [1.0, 0.0], color, clip_circle, clip_extents };
+ let bl = ImageVertex { position: ndc(x, y + h), uv: [0.0, 1.0], color, clip_circle, clip_extents };
+ let br = ImageVertex { position: ndc(x + w, y + h), uv: [1.0, 1.0], color, clip_circle, clip_extents };
verts.extend([tl, tr, bl, tr, br, bl]);
}
let bytes: &[u8] = bytemuck::cast_slice(&verts);