renderer/vulkan: real bg_color UBO renders configured theme color

Drop the diagnostic purple shader; the bg_color pipeline now reads
the configured background color out of the Globals UBO and the
window paints the actual theme color (e.g. #192742 for the user
that triggered this work).

Two pieces had to land together:

1. Real shaders + UBO descriptor wiring. The bg_color fragment
   source goes back to the include-expanded `bg_color.f.glsl`, and
   the full-screen vertex source goes back to `full_screen.v.glsl`
   (`vulkanizeGlsl` rewrites `gl_VertexID` → `gl_VertexIndex` at
   compile time so it works under glslang's Vulkan client). The
   pipeline is built with the descriptor pool + bg_color descriptor
   set layout again, so `RenderPass.step` can update the UBO binding
   each frame with `frame.uniforms.buffer`.

2. sRGB render target + premultiplied QImage. The shader emits
   linear premultiplied alpha (`load_color` linearizes then
   premultiplies). Without sRGB encoding on the framebuffer write,
   the bytes in memory are linear premultiplied and any consumer
   that assumes sRGB premultiplied (e.g. Qt's
   `Format_ARGB32_Premultiplied`) renders the colors far too dark.
   Render targets and the custom-shader back_texture now use
   `VK_FORMAT_B8G8R8A8_SRGB`; the bg_color pipeline's
   `color_format` follows. On the Qt side the QImage import
   switches from `Format_ARGB32` to `Format_ARGB32_Premultiplied`
   so Qt interprets the bytes correctly.

The DRM fourcc the host sees is still `AR24` — sRGB encoding is a
Vulkan-side framebuffer-write concern; the dmabuf byte layout is
identical.

Co-Authored-By: claude-flow <ruv@ruv.net>
pull/12846/head
Nathan 2026-05-24 14:22:13 -05:00
parent 98dcdf5307
commit 5f54571aff
3 changed files with 28 additions and 40 deletions

View File

@ -1333,21 +1333,25 @@ void GhosttySurface::presentVulkanDmabuf(
return;
}
// QImage holds the pixel data by copying when constructed with
// `Format_ARGB32` from a buffer with explicit stride. We then
// detach (copy()) so the QImage survives the unmap.
// `Format_ARGB32_Premultiplied` from a buffer with explicit stride.
// We then detach (copy()) so the QImage survives the unmap.
//
// drm_format ARGB8888 (0x34325241 = "AR24") matches QImage's
// Format_ARGB32 byte order on little-endian (B,G,R,A in memory).
// We unconditionally use ARGB32 here because the renderer currently
// emits BGRA only — extend with a format switch when other formats
// come online.
// ARGB32 byte order on little-endian (B,G,R,A in memory).
//
// We use the *premultiplied* variant because the renderer's
// fragment shaders output premultiplied alpha and the render
// target is `VK_FORMAT_B8G8R8A8_SRGB` (hardware gamma-encodes the
// linear shader output at framebuffer-write time). The bytes
// landing in this buffer are therefore sRGB-encoded premultiplied
// ARGB — exactly what Format_ARGB32_Premultiplied expects.
(void)drm_format;
const QImage stamped(
static_cast<const uchar *>(mapped),
static_cast<int>(width),
static_cast<int>(height),
static_cast<int>(stride),
QImage::Format_ARGB32);
QImage::Format_ARGB32_Premultiplied);
QImage owned = stamped.copy();
::munmap(mapped, bytes);

View File

@ -255,9 +255,17 @@ pub fn initShaders(
pub fn initTarget(self: *const Vulkan, width: usize, height: usize) !Target {
_ = self;
// SRGB format so the hardware gamma-encodes the linear premultiplied
// shader output at framebuffer-write time. The renderer's shaders
// produce linear premultiplied alpha; without an sRGB format the
// bytes in memory would be linear and Qt (which expects sRGB
// premultiplied) would render them as if they were already gamma
// encoded colors would look way too dark. The DRM fourcc the
// host sees is still ARGB8888; SRGB encoding is a Vulkan-side
// concern only.
return try Target.init(.{
.device = devicePtr(),
.format = vk.VK_FORMAT_B8G8R8A8_UNORM,
.format = vk.VK_FORMAT_B8G8R8A8_SRGB,
.width = @intCast(width),
.height = @intCast(height),
});
@ -400,11 +408,11 @@ pub fn textureOptions(_: *const Vulkan) Texture.Options {
// back_texture (which is BOTH sampled AND a render target).
// We hand back the wider usage set so both work. The format
// matches the renderer's `initTarget` choice
// (`B8G8R8A8_UNORM`) so a render sample render chain
// (`B8G8R8A8_SRGB`) so a render sample render chain
// through the custom-shader pass keeps the same color format.
return .{
.device = devicePtr(),
.format = vk.VK_FORMAT_B8G8R8A8_UNORM,
.format = vk.VK_FORMAT_B8G8R8A8_SRGB,
.usage = vk.VK_IMAGE_USAGE_SAMPLED_BIT |
vk.VK_IMAGE_USAGE_TRANSFER_DST_BIT |
vk.VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT,

View File

@ -46,35 +46,13 @@ pub const source = struct {
// include contents inline same approach `opengl/shaders.zig`
// uses via its `loadShaderCode`.
// DIAGNOSTIC: override bg_color.f.glsl with a hardcoded purple
// color so we can verify the pipeline + descriptor binding +
// draw recording work end-to-end without depending on the
// Uniforms.bg_color data path being correct. Once a colored
// window confirms the pipeline runs, revert to the real
// include-expanded source.
pub const bg_color_frag: [:0]const u8 =
\\#version 450
\\layout(location = 0) out vec4 out_FragColor;
\\void main() {
\\ out_FragColor = vec4(0.5, 0.0, 0.5, 1.0); // debug: opaque purple
\\}
;
pub const bg_color_frag_real = processIncludes(@embedFile("../shaders/glsl/bg_color.f.glsl"));
pub const bg_color_frag = processIncludes(@embedFile("../shaders/glsl/bg_color.f.glsl"));
pub const bg_image_frag = processIncludes(@embedFile("../shaders/glsl/bg_image.f.glsl"));
pub const bg_image_vert = processIncludes(@embedFile("../shaders/glsl/bg_image.v.glsl"));
pub const cell_bg_frag = processIncludes(@embedFile("../shaders/glsl/cell_bg.f.glsl"));
pub const cell_text_frag = processIncludes(@embedFile("../shaders/glsl/cell_text.f.glsl"));
pub const cell_text_vert = processIncludes(@embedFile("../shaders/glsl/cell_text.v.glsl"));
// DIAGNOSTIC: inline a known-good fullscreen-triangle vertex
// shader to rule out any vulkanizeGlsl rewrite issues.
pub const full_screen_vert: [:0]const u8 =
\\#version 450
\\void main() {
\\ vec2 pos[3] = vec2[3](vec2(-1.0, -1.0), vec2(3.0, -1.0), vec2(-1.0, 3.0));
\\ gl_Position = vec4(pos[gl_VertexIndex], 0.0, 1.0);
\\}
;
pub const full_screen_vert_real = processIncludes(@embedFile("../shaders/glsl/full_screen.v.glsl"));
pub const full_screen_vert = processIncludes(@embedFile("../shaders/glsl/full_screen.v.glsl"));
pub const image_frag = processIncludes(@embedFile("../shaders/glsl/image.f.glsl"));
pub const image_vert = processIncludes(@embedFile("../shaders/glsl/image.v.glsl"));
};
@ -565,17 +543,15 @@ pub const Shaders = struct {
}
errdefer device.dispatch.destroyDescriptorSetLayout(device.device, bg_color_dsl, null);
// DIAGNOSTIC: the debug bg_color shader has no inputs, so
// build the pipeline WITHOUT a descriptor set layout. The
// `bg_color_dsl` is still kept around it gets stored in
// `Shaders.bg_color_set_layout` and torn down on deinit.
const bg_color_dsls = [_]vk.VkDescriptorSetLayout{bg_color_dsl};
const bg_color_pipeline = try Pipeline.init(.{
.device = device,
.descriptor_pool = &pool,
.vertex_module = modules.full_screen_vert.handle,
.fragment_module = modules.bg_color_frag.handle,
.vertex_input = null,
.descriptor_set_layouts = &.{},
.color_format = vk.VK_FORMAT_B8G8R8A8_UNORM,
.descriptor_set_layouts = &bg_color_dsls,
.color_format = vk.VK_FORMAT_B8G8R8A8_SRGB,
.blending_enabled = false,
.topology = vk.VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST,
});