GPU-accelerated UI toolkit (Vulkan)
git clone https://git.lucas.co/cce-ui.git
fix: sample user images as sRGB, not UNORM
The swapchain is an sRGB format (renderer.rs prefers one), so the hardware
encodes shader output on write. User images were created as
R8G8B8A8_UNORM, which hands the sampler their sRGB bytes as if they were
linear — encoding them a second time and lightening every midtone. Only
pure black and white survived, so it read as "images look washed out"
rather than as a color cast.
Measured in cce-preview on a synthetic swatch, before -> after:
#101010 (71,71,71) -> (16,16,16); #404040 (137) -> (64); #808080 (188) ->
(128); #204060 (99,137,165) -> (32,64,96); #c87828 (229,182,110) ->
(200,120,40). Exact round trip on every patch, same in cce-browser on a
rendered page. sRGB formats leave alpha linear, so icon edges and any
straight-alpha artwork blend as before.
This is only about pixels that arrive already sRGB-encoded — rasterized
SVGs, decoded PNGs, Servo's page readback. The vector palette shares the
same double-encode but as a CONVENTION, not a bug: those constants are
linear floats tuned by eye in this pipeline (cce-preview's [0.13, 0.13,
0.14] backdrop measures (101,101,104), linear->sRGB of 0.13). Converting
them would darken the whole DE and invalidate every tuned color, so they
are deliberately left alone.
Takes effect per binary: an app shows the corrected images only once it
is rebuilt against this cce-ui.
Co-Authored-By: Claude Opus 5 <[email protected]>
src/vk/image.rs | 12 ++++++++++--
1 file changed, 10 insertions(+), 2 deletions(-)
diff --git a/src/vk/image.rs b/src/vk/image.rs
index c970497..c4bf01c 100644
--- a/src/vk/image.rs
+++ b/src/vk/image.rs
@@ -348,7 +348,15 @@ impl ImageStage {
.create_image(
&vk::ImageCreateInfo::default()
.image_type(vk::ImageType::TYPE_2D)
- .format(vk::Format::R8G8B8A8_UNORM)
+ // SRGB, not UNORM: uploaded pixels are sRGB-encoded
+ // (rasterized SVGs, decoded PNGs, Servo page readback),
+ // and the swapchain is an sRGB format, so the hardware
+ // encodes shader output on write. Sampling as UNORM
+ // fed those bytes through as if linear and encoded
+ // them a second time, lightening every midtone —
+ // a page's #101010 measured (71,71,71) on screen.
+ // Decoding on sample makes the round trip exact.
+ .format(vk::Format::R8G8B8A8_SRGB)
.extent(vk::Extent3D { width, height, depth: 1 })
.mip_levels(1)
.array_layers(1)
@@ -465,7 +473,7 @@ impl ImageStage {
&vk::ImageViewCreateInfo::default()
.image(image)
.view_type(vk::ImageViewType::TYPE_2D)
- .format(vk::Format::R8G8B8A8_UNORM)
+ .format(vk::Format::R8G8B8A8_SRGB)
.subresource_range(range),
None,
)