summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--shaders/crt.frag.glsl27
-rw-r--r--shaders/prebuilt/crt.frag.glsl27
-rw-r--r--shaders/prebuilt/crt.frag.spvbin13348 -> 13832 bytes
-rw-r--r--shaders/prebuilt/ui.frag.glsl20
-rw-r--r--shaders/prebuilt/ui.frag.spvbin3908 -> 4076 bytes
-rw-r--r--shaders/prebuilt/ui.vert.glsl7
-rw-r--r--shaders/prebuilt/ui.vert.spvbin5468 -> 5468 bytes
-rw-r--r--shaders/ui.frag.glsl20
-rw-r--r--shaders/ui.vert.glsl7
-rw-r--r--src/config.zig26
-rw-r--r--src/gui/gui.zig152
-rw-r--r--src/main.zig20
12 files changed, 245 insertions, 61 deletions
diff --git a/shaders/crt.frag.glsl b/shaders/crt.frag.glsl
index e43d5b86..b0b0b93b 100644
--- a/shaders/crt.frag.glsl
+++ b/shaders/crt.frag.glsl
@@ -81,24 +81,29 @@ vec2 sceneUv(vec2 uv) {
return uv;
}
-vec3 sceneSrgb(vec2 uv) {
+// The scene, ALPHA INCLUDED: with a see-through ground the grid arrives
+// premultiplied and partly empty, and a pass that dropped .a would turn the
+// desktop behind a `*_transparent` theme into opaque black the moment Crt,
+// Ripple or Glitch came on.
+vec4 sceneSrgb(vec2 uv) {
vec2 warped = sceneUv(uv);
- vec3 center = texture(u_scene, warped).rgb;
+ vec4 center = texture(u_scene, warped);
if (u_crt.effects.z <= 0.5) return center;
float band = floor(warped.y * 96.0);
float seed = noiseAt(vec2(band, floor(u_crt.time_seconds * 13.0)));
float tear = smoothstep(0.955, 1.0, seed);
vec2 chroma = vec2((0.35 + 1.25 * tear) / u_crt.viewport.x, 0.0);
- return vec3(
+ return vec4(
texture(u_scene, warped + chroma).r,
center.g,
- texture(u_scene, warped - chroma).b
+ texture(u_scene, warped - chroma).b,
+ center.a
);
}
vec3 effectSceneLinear(vec2 uv) {
- return srgbToLinear(sceneSrgb(uv));
+ return srgbToLinear(sceneSrgb(uv).rgb);
}
void main() {
@@ -113,11 +118,16 @@ void main() {
if (u_crt.effects.x <= 0.5) {
// The scene texture is already sRGB. Avoid a needless transfer-curve
// round trip so a coordinate-only effect preserves theme bytes.
- o_col = vec4(clamp(sceneSrgb(uv), 0.0, 1.0), 1.0);
+ vec4 s = sceneSrgb(uv);
+ o_col = vec4(clamp(s.rgb, 0.0, 1.0), clamp(s.a, 0.0, 1.0));
return;
}
vec3 col = vec3(0.0);
+ // Outside the tube nothing is drawn, so nothing is covered either: the
+ // old opaque black there would have punched a rectangle out of the
+ // compositor's backdrop.
+ float alpha = 0.0;
float edge = min(min(uv.x, uv.y), min(1.0 - uv.x, 1.0 - uv.y));
if (edge > 0.0) {
vec3 center = effectSceneLinear(uv);
@@ -153,10 +163,13 @@ void main() {
float vignette = 1.0 - VIGNETTE_STRENGTH * pow(clamp(r2 * 0.5, 0.0, 1.0), 1.6);
float hum = 1.0 + HUM_STRENGTH * sin((uv.y * 1.35 - u_crt.time_seconds * 0.11) * TAU);
col *= tube * vignette * hum * EXPOSURE;
+ // The tube's edge falloff dims coverage as well as colour; everything
+ // else here is a light adjustment and leaves it alone.
+ alpha = sceneSrgb(uv).a * tube;
float luma = dot(col, vec3(0.2126, 0.7152, 0.0722));
vec2 noise_seed = gl_FragCoord.xy + vec2(u_crt.frame * 17.0, u_crt.frame * 59.0);
col += (noiseAt(noise_seed) - 0.5) * NOISE_STRENGTH * (0.15 + 0.85 * clamp(luma, 0.0, 1.0));
}
- o_col = vec4(linearToSrgb(clamp(col, 0.0, 1.0)), 1.0);
+ o_col = vec4(linearToSrgb(clamp(col, 0.0, 1.0)), clamp(alpha, 0.0, 1.0));
}
diff --git a/shaders/prebuilt/crt.frag.glsl b/shaders/prebuilt/crt.frag.glsl
index e43d5b86..b0b0b93b 100644
--- a/shaders/prebuilt/crt.frag.glsl
+++ b/shaders/prebuilt/crt.frag.glsl
@@ -81,24 +81,29 @@ vec2 sceneUv(vec2 uv) {
return uv;
}
-vec3 sceneSrgb(vec2 uv) {
+// The scene, ALPHA INCLUDED: with a see-through ground the grid arrives
+// premultiplied and partly empty, and a pass that dropped .a would turn the
+// desktop behind a `*_transparent` theme into opaque black the moment Crt,
+// Ripple or Glitch came on.
+vec4 sceneSrgb(vec2 uv) {
vec2 warped = sceneUv(uv);
- vec3 center = texture(u_scene, warped).rgb;
+ vec4 center = texture(u_scene, warped);
if (u_crt.effects.z <= 0.5) return center;
float band = floor(warped.y * 96.0);
float seed = noiseAt(vec2(band, floor(u_crt.time_seconds * 13.0)));
float tear = smoothstep(0.955, 1.0, seed);
vec2 chroma = vec2((0.35 + 1.25 * tear) / u_crt.viewport.x, 0.0);
- return vec3(
+ return vec4(
texture(u_scene, warped + chroma).r,
center.g,
- texture(u_scene, warped - chroma).b
+ texture(u_scene, warped - chroma).b,
+ center.a
);
}
vec3 effectSceneLinear(vec2 uv) {
- return srgbToLinear(sceneSrgb(uv));
+ return srgbToLinear(sceneSrgb(uv).rgb);
}
void main() {
@@ -113,11 +118,16 @@ void main() {
if (u_crt.effects.x <= 0.5) {
// The scene texture is already sRGB. Avoid a needless transfer-curve
// round trip so a coordinate-only effect preserves theme bytes.
- o_col = vec4(clamp(sceneSrgb(uv), 0.0, 1.0), 1.0);
+ vec4 s = sceneSrgb(uv);
+ o_col = vec4(clamp(s.rgb, 0.0, 1.0), clamp(s.a, 0.0, 1.0));
return;
}
vec3 col = vec3(0.0);
+ // Outside the tube nothing is drawn, so nothing is covered either: the
+ // old opaque black there would have punched a rectangle out of the
+ // compositor's backdrop.
+ float alpha = 0.0;
float edge = min(min(uv.x, uv.y), min(1.0 - uv.x, 1.0 - uv.y));
if (edge > 0.0) {
vec3 center = effectSceneLinear(uv);
@@ -153,10 +163,13 @@ void main() {
float vignette = 1.0 - VIGNETTE_STRENGTH * pow(clamp(r2 * 0.5, 0.0, 1.0), 1.6);
float hum = 1.0 + HUM_STRENGTH * sin((uv.y * 1.35 - u_crt.time_seconds * 0.11) * TAU);
col *= tube * vignette * hum * EXPOSURE;
+ // The tube's edge falloff dims coverage as well as colour; everything
+ // else here is a light adjustment and leaves it alone.
+ alpha = sceneSrgb(uv).a * tube;
float luma = dot(col, vec3(0.2126, 0.7152, 0.0722));
vec2 noise_seed = gl_FragCoord.xy + vec2(u_crt.frame * 17.0, u_crt.frame * 59.0);
col += (noiseAt(noise_seed) - 0.5) * NOISE_STRENGTH * (0.15 + 0.85 * clamp(luma, 0.0, 1.0));
}
- o_col = vec4(linearToSrgb(clamp(col, 0.0, 1.0)), 1.0);
+ o_col = vec4(linearToSrgb(clamp(col, 0.0, 1.0)), clamp(alpha, 0.0, 1.0));
}
diff --git a/shaders/prebuilt/crt.frag.spv b/shaders/prebuilt/crt.frag.spv
index 057f0e06..f0e14627 100644
--- a/shaders/prebuilt/crt.frag.spv
+++ b/shaders/prebuilt/crt.frag.spv
Binary files differ
diff --git a/shaders/prebuilt/ui.frag.glsl b/shaders/prebuilt/ui.frag.glsl
index ddc1cde0..69cb63b9 100644
--- a/shaders/prebuilt/ui.frag.glsl
+++ b/shaders/prebuilt/ui.frag.glsl
@@ -1,7 +1,10 @@
#version 450
// Sample the glyph atlas (R = coverage 0..1) and mix the cell's bg up to its fg
-// by that coverage. Fully opaque: the swapchain is SDR, no blending needed.
+// by that coverage. Output is PREMULTIPLIED: an opaque cell is unchanged, and a
+// cell whose background is the see-through ground (config.gui_transparent, a
+// theme with no bg of its own) contributes only its ink, at the ink's own
+// coverage, so the compositor keeps the rest.
layout(set = 2, binding = 0) uniform sampler2D u_atlas;
@@ -26,13 +29,20 @@ float cellNoise(uint serial, uint col, uint row) {
void main() {
float a = texture(u_atlas, v_uv).r;
bool oldLayer = (v_effect.x & 0x80000000u) != 0u;
- uint effect = v_effect.x & 0x7fffffffu;
+ // 0 where the cell's background IS the ground and the ground is nothing.
+ // src/gui/gui.zig sets that bit; the two flags above it are why the effect
+ // id is masked with 0x3fffffff here and in ui.vert.glsl.
+ float bgA = (v_effect.x & 0x40000000u) != 0u ? 0.0 : 1.0;
+ uint effect = v_effect.x & 0x3fffffffu;
float progress = clamp(uintBitsToFloat(v_effect.y), 0.0, 1.0);
uint cellCol = v_effect.w & 0xffffu;
uint cellRow = v_effect.w >> 16;
float noise = cellNoise(v_effect.z, cellCol, cellRow);
- vec3 cell = mix(v_bg, v_fg, a);
+ // Ink over ground, both premultiplied: v_fg*a + (v_bg*bgA)*(1-a), which is
+ // mix() with the ground already premultiplied. At bgA = 1 that is exactly
+ // the mix(v_bg, v_fg, a) at alpha 1 this shader always wrote.
+ vec4 cell = vec4(mix(v_bg * bgA, v_fg, a), mix(bgA, 1.0, a));
// 3 = data dissolve. Changed cells retain the frozen old cell until their
// stable threshold, then atomically become the new cell. Exact endpoints
@@ -40,11 +50,11 @@ void main() {
if (effect == 3u) {
bool reveal = progress >= 1.0 || (progress > 0.0 && noise < progress);
if (oldLayer ? reveal : !reveal) discard;
- o_col = vec4(cell, 1.0);
+ o_col = cell;
return;
}
// FreeType's hinted grayscale bitmap is already the intended coverage.
// Preserve it exactly; weight-changing gamma hacks blur small stems.
- o_col = vec4(cell, 1.0);
+ o_col = cell;
}
diff --git a/shaders/prebuilt/ui.frag.spv b/shaders/prebuilt/ui.frag.spv
index 6b8a88e0..4eb9993e 100644
--- a/shaders/prebuilt/ui.frag.spv
+++ b/shaders/prebuilt/ui.frag.spv
Binary files differ
diff --git a/shaders/prebuilt/ui.vert.glsl b/shaders/prebuilt/ui.vert.glsl
index 08627dc0..5ec596fe 100644
--- a/shaders/prebuilt/ui.vert.glsl
+++ b/shaders/prebuilt/ui.vert.glsl
@@ -14,8 +14,9 @@ layout(location = 3) in vec3 a_bg;
layout(location = 4) in vec4 a_panel_rect;
layout(location = 5) in vec4 a_present_rect;
// effect id, eased-progress float bits, pane serial, local row/column packed
-// into one uint. The high effect bit marks the frozen-old cell layer; integer
-// transport preserves the exact cross-backend hash.
+// into one uint. The TWO high effect bits are flags — the frozen-old cell
+// layer and a see-through cell background — so the id is what is left below
+// them; integer transport preserves the exact cross-backend hash.
layout(location = 6) in uvec4 a_effect;
layout(location = 0) out vec2 v_uv;
@@ -54,7 +55,7 @@ void main() {
v_cell_uv = vec2(0.0, 1.0);
}
- uint effect = a_effect.x & 0x7fffffffu;
+ uint effect = a_effect.x & 0x3fffffffu;
if (effect == 1u || effect == 2u || effect == 5u) {
// NDC y points up, so a top-left/bottom-right box has a NEGATIVE y
// delta. Preserve that sign or every local y is mirrored/outside.
diff --git a/shaders/prebuilt/ui.vert.spv b/shaders/prebuilt/ui.vert.spv
index f0d5f653..08e81e42 100644
--- a/shaders/prebuilt/ui.vert.spv
+++ b/shaders/prebuilt/ui.vert.spv
Binary files differ
diff --git a/shaders/ui.frag.glsl b/shaders/ui.frag.glsl
index ddc1cde0..69cb63b9 100644
--- a/shaders/ui.frag.glsl
+++ b/shaders/ui.frag.glsl
@@ -1,7 +1,10 @@
#version 450
// Sample the glyph atlas (R = coverage 0..1) and mix the cell's bg up to its fg
-// by that coverage. Fully opaque: the swapchain is SDR, no blending needed.
+// by that coverage. Output is PREMULTIPLIED: an opaque cell is unchanged, and a
+// cell whose background is the see-through ground (config.gui_transparent, a
+// theme with no bg of its own) contributes only its ink, at the ink's own
+// coverage, so the compositor keeps the rest.
layout(set = 2, binding = 0) uniform sampler2D u_atlas;
@@ -26,13 +29,20 @@ float cellNoise(uint serial, uint col, uint row) {
void main() {
float a = texture(u_atlas, v_uv).r;
bool oldLayer = (v_effect.x & 0x80000000u) != 0u;
- uint effect = v_effect.x & 0x7fffffffu;
+ // 0 where the cell's background IS the ground and the ground is nothing.
+ // src/gui/gui.zig sets that bit; the two flags above it are why the effect
+ // id is masked with 0x3fffffff here and in ui.vert.glsl.
+ float bgA = (v_effect.x & 0x40000000u) != 0u ? 0.0 : 1.0;
+ uint effect = v_effect.x & 0x3fffffffu;
float progress = clamp(uintBitsToFloat(v_effect.y), 0.0, 1.0);
uint cellCol = v_effect.w & 0xffffu;
uint cellRow = v_effect.w >> 16;
float noise = cellNoise(v_effect.z, cellCol, cellRow);
- vec3 cell = mix(v_bg, v_fg, a);
+ // Ink over ground, both premultiplied: v_fg*a + (v_bg*bgA)*(1-a), which is
+ // mix() with the ground already premultiplied. At bgA = 1 that is exactly
+ // the mix(v_bg, v_fg, a) at alpha 1 this shader always wrote.
+ vec4 cell = vec4(mix(v_bg * bgA, v_fg, a), mix(bgA, 1.0, a));
// 3 = data dissolve. Changed cells retain the frozen old cell until their
// stable threshold, then atomically become the new cell. Exact endpoints
@@ -40,11 +50,11 @@ void main() {
if (effect == 3u) {
bool reveal = progress >= 1.0 || (progress > 0.0 && noise < progress);
if (oldLayer ? reveal : !reveal) discard;
- o_col = vec4(cell, 1.0);
+ o_col = cell;
return;
}
// FreeType's hinted grayscale bitmap is already the intended coverage.
// Preserve it exactly; weight-changing gamma hacks blur small stems.
- o_col = vec4(cell, 1.0);
+ o_col = cell;
}
diff --git a/shaders/ui.vert.glsl b/shaders/ui.vert.glsl
index 08627dc0..5ec596fe 100644
--- a/shaders/ui.vert.glsl
+++ b/shaders/ui.vert.glsl
@@ -14,8 +14,9 @@ layout(location = 3) in vec3 a_bg;
layout(location = 4) in vec4 a_panel_rect;
layout(location = 5) in vec4 a_present_rect;
// effect id, eased-progress float bits, pane serial, local row/column packed
-// into one uint. The high effect bit marks the frozen-old cell layer; integer
-// transport preserves the exact cross-backend hash.
+// into one uint. The TWO high effect bits are flags — the frozen-old cell
+// layer and a see-through cell background — so the id is what is left below
+// them; integer transport preserves the exact cross-backend hash.
layout(location = 6) in uvec4 a_effect;
layout(location = 0) out vec2 v_uv;
@@ -54,7 +55,7 @@ void main() {
v_cell_uv = vec2(0.0, 1.0);
}
- uint effect = a_effect.x & 0x7fffffffu;
+ uint effect = a_effect.x & 0x3fffffffu;
if (effect == 1u || effect == 2u || effect == 5u) {
// NDC y points up, so a top-left/bottom-right box has a NEGATIVE y
// delta. Preserve that sign or every local y is mirrored/outside.
diff --git a/src/config.zig b/src/config.zig
index cbe75dea..1a3de86c 100644
--- a/src/config.zig
+++ b/src/config.zig
@@ -566,6 +566,32 @@ pub const gui_topbar_pane_border_px: u8 = 1;
/// build that wants a deliberate accent can pin one here.
pub const gui_topbar_pane_border_rgb: ?[3]u8 = null;
+/// Open the SDL window with a transparent buffer, so that a theme declaring NO
+/// background of its own (the curated `dark`, every vendored `*_transparent`)
+/// shows the desktop through the grid instead of a colour this shell had to
+/// invent. That is what the AppKit shell does over its NSVisualEffectView, and
+/// on linux the compositor supplies the backdrop — a niri `background-effect
+/// { blur true }` window rule, picom, whatever is running.
+///
+/// OFF by default because it is not free, and the cost is structural rather
+/// than ours: `SDL_ClaimWindowForGPUDevice` refuses a transparent window
+/// outright ("The GPU API doesn't support transparent windows", SDL_gpu.c,
+/// still upstream), because D3D12 has no transparent swapchain and the API
+/// says no everywhere rather than only where it must. A transparent window
+/// therefore has no swapchain to render into, and the frame reaches the screen
+/// down the same readback-and-blit path a compositor that cannot back a Vulkan
+/// swapchain already uses (`soft_present` in `src/gui/gui.zig`): one
+/// GPU->CPU download plus one upload per PAINTED frame, measured at 1.2 ms for
+/// 2240x1440 and 3.0 ms for 3840x2160 on an RTX 3050. Idle frames cost
+/// nothing — this shell only paints when something changed — but an animation
+/// at 60 Hz spends that every frame.
+///
+/// With a theme that DOES bring a background this changes nothing visible: the
+/// ground is painted at full alpha, exactly as an opaque window would. It
+/// still pays the readback, because window transparency is fixed at creation
+/// and a `Theme` command may reach a transparent theme later.
+pub const gui_transparent: bool = false;
+
/// Touchpad drift guard, in ticks. A two-finger swipe that is MEANT to be
/// vertical carries a little sideways drift, and the pad faithfully turns that
/// drift into wheel_left/wheel_right — so a plain scroll slides the view
diff --git a/src/gui/gui.zig b/src/gui/gui.zig
index bfd02ff9..91380e46 100644
--- a/src/gui/gui.zig
+++ b/src/gui/gui.zig
@@ -89,28 +89,45 @@ const crt_frag_spv = @embedFile("crt.frag.spv");
/// inside every cell the core left at its default background. The FALLBACK
/// for it, that is: the ground itself is the active theme's own background,
/// read off the core once a frame (see `ground`), and this is what a theme
-/// with none of its own gets.
+/// with none of its own gets in an OPAQUE window.
///
/// A theme declares no background (the curated `dark`, every vendored
/// `*_transparent`) to mean "wear whatever the terminal is wearing", and a
-/// window has nothing to wear. The AppKit shell answers that by going
-/// see-through over an NSVisualEffectView — pardes_theme_bg, docs/macos.md —
-/// and this shell cannot follow it there: SDL's GPU API refuses to claim a
-/// SDL_WINDOW_TRANSPARENT window at all ("The GPU API doesn't support
-/// transparent windows", SDL_gpu.c), because D3D12 has no transparent
-/// swapchain and the API says no everywhere rather than only where it must.
-/// So a themeless window keeps the terminal-native dark it always wore.
+/// window has nothing to wear unless it is see-through. The AppKit shell
+/// answers that by going transparent over an NSVisualEffectView
+/// (pardes_theme_bg, docs/macos.md); this shell answers it with
+/// `config.gui_transparent`, which asks SDL for a transparent window and lets
+/// the compositor be the backdrop. That knob is off by default and costs a
+/// readback per painted frame when it is on — the whole reason is written
+/// where it is declared. Without it a themeless window keeps the
+/// terminal-native dark it always wore.
const bg_default = [3]u8{ 18, 18, 18 };
const fg_default = [3]u8{ 204, 204, 204 };
+/// The ground under this frame, and whether it is a colour at all.
+///
+/// `clear` is the themeless case in a transparent window: nothing is painted
+/// there, the desktop is. `rgb` still carries the fallback colour because a
+/// reverse-video cell puts the ground in its FOREGROUND, where it is a real
+/// colour that paints — the same rule PardesView.styleFor states on macOS.
+const Ground = struct {
+ rgb: [3]u8,
+ clear: bool,
+
+ fn opaqueRgb(rgb: [3]u8) Ground {
+ return .{ .rgb = rgb, .clear = false };
+ }
+};
+
/// The ground under this frame. The theme's OWN background and not the
/// animated chrome colour: taglines fade between themes over a handful of
/// frames, document backgrounds switch the instant the theme does, and this is
/// one of those. Asked per frame, so a `Theme` command takes hold without a
/// relaunch — and asked at all because a hand-agreed constant was a black line
/// along the two edges of every light-themed window.
-fn ground(theme_bg: ?[3]u8) [3]u8 {
- return theme_bg orelse bg_default;
+fn ground(theme_bg: ?[3]u8, transparent: bool) Ground {
+ if (theme_bg) |rgb| return .opaqueRgb(rgb);
+ return .{ .rgb = bg_default, .clear = transparent };
}
// the plan9 arrow cursor, bytes verbatim from 9front /sys/src/9/port/
@@ -544,7 +561,14 @@ const ImageInstance = extern struct {
};
const initial_image_capacity: u32 = pardes.MAX_PANES;
+/// `CellInstance.effect` is an effect id in its low bits and flags in its top
+/// two. Both shaders that read the field mask the id off with `0x3fffffff`;
+/// widening this pair means widening that mask with it.
const old_layer_bit: u32 = 0x8000_0000;
+/// This cell's background IS the see-through ground: emit the glyph and let
+/// the compositor keep the rest. Only ever set when `Ground.clear` holds, so
+/// an opaque window never reaches the branch.
+const clear_bg_bit: u32 = 0x4000_0000;
/// Image placement retained across pane destruction. Deliberately does not
/// contain ImagePlace.rgba: the producer owns those bytes, while the renderer
@@ -1370,7 +1394,15 @@ const Gui = struct {
// not support presenting"). The GPU still renders, so render offscreen
// exactly like capture does and blit the readback through SDL_Renderer,
// which goes out over wl_shm.
+ //
+ // `config.gui_transparent` takes the same path deliberately rather than by
+ // failure: SDL's GPU API refuses to claim a transparent window at all, and
+ // SDL_Renderer is the presenter that does honour one.
soft_present: bool = false,
+ /// The window was created with SDL_WINDOW_TRANSPARENT, so a themeless
+ /// ground is nothing at all instead of `bg_default`. Implies
+ /// `soft_present`; read per frame by `ground`.
+ transparent: bool = false,
soft_renderer: ?*c.SDL_Renderer = null,
soft_texture: ?*c.SDL_Texture = null,
soft_tex_w: u32 = 0,
@@ -1737,6 +1769,12 @@ fn runNative(init: std.process.Init, opts_in: pardes.Options, attach: ?[]const u
}
var win_flags: c.SDL_WindowFlags = c.SDL_WINDOW_RESIZABLE;
if (!test_mode) win_flags |= c.SDL_WINDOW_HIGH_PIXEL_DENSITY;
+ // A see-through buffer, so a theme with no background of its own shows the
+ // compositor's backdrop instead of `bg_default`. Asked for at CREATION
+ // because that is the only time it can be: X11 picks the 32-bit visual
+ // here, and the Wayland backend decides here whether to keep an opaque
+ // region on the surface.
+ if (config.gui_transparent) win_flags |= c.SDL_WINDOW_TRANSPARENT;
const window = c.SDL_CreateWindow("pardes", 1120, 720, win_flags) orelse {
log.err("SDL_CreateWindow: {s}", .{c.SDL_GetError()});
return error.SdlInit;
@@ -1758,14 +1796,24 @@ fn runNative(init: std.process.Init, opts_in: pardes.Options, attach: ?[]const u
// the device and present the readback through SDL_Renderer instead.
// PARDES_SOFT_PRESENT=1 takes that path on a compositor that could present,
// which is how the path is exercised without a remote display.
+ //
+ // A transparent window does not even attempt the claim. It is not a
+ // compositor's shortcoming and there is nothing to retry: SDL_gpu.c fails
+ // SDL_ClaimWindowForGPUDevice for SDL_WINDOW_TRANSPARENT unconditionally,
+ // because D3D12 has no transparent swapchain and the API says no
+ // everywhere rather than only where it must. SDL_Renderer's own vulkan and
+ // opengl backends do honour one, and that is the presenter this path
+ // already had.
var soft_present = false;
var soft_renderer: ?*c.SDL_Renderer = null;
const force_soft = if (env.get("PARDES_SOFT_PRESENT")) |raw|
!(std.mem.eql(u8, raw, "0") or raw.len == 0)
else
false;
- if (force_soft or !c.SDL_ClaimWindowForGPUDevice(device, window)) {
- if (force_soft)
+ if (config.gui_transparent or force_soft or !c.SDL_ClaimWindowForGPUDevice(device, window)) {
+ if (config.gui_transparent)
+ log.info("transparent window: the GPU API will not claim one, presenting through SDL_Renderer", .{})
+ else if (force_soft)
log.info("PARDES_SOFT_PRESENT set: presenting in software", .{})
else
log.warn("ClaimWindowForGPUDevice: {s}; presenting in software", .{c.SDL_GetError()});
@@ -1912,6 +1960,7 @@ fn runNative(init: std.process.Init, opts_in: pardes.Options, attach: ?[]const u
.capture = test_mode,
.capture_dir = capture_dir orelse "",
.soft_present = soft_present,
+ .transparent = config.gui_transparent,
.soft_renderer = soft_renderer,
};
setGuiFontName(&g, "Adwaita Mono");
@@ -4325,7 +4374,7 @@ fn stepScroll(g: *Gui, core: *pardes.Pardes, gpa: std.mem.Allocator) void {
/// to the body, which is the whole of the clipping — the shell draws the grid
/// in one flat pass, so without it the overhanging rows would land on the
/// pane's own tag and on whatever is below it.
-fn emitScrollRows(g: *Gui, instances: [*]CellInstance, base: u32, surface: *pardes.Surface, layout: CellLayout, win_w: f32, win_h: f32, page: [3]u8) u32 {
+fn emitScrollRows(g: *Gui, instances: [*]CellInstance, base: u32, surface: *pardes.Surface, layout: CellLayout, win_w: f32, win_h: f32, page: Ground) u32 {
const scroll_pane = g.scroll_pane orelse return 0;
if (g.scroll_lag == 0) return 0;
for (surface.panelTracks()) |track|
@@ -4959,17 +5008,22 @@ fn renderFrame(
const cells: u32 = @as(u32, surface.cols) * surface.rows;
- const page = ground(theme_bg);
+ const page = ground(theme_bg, g.transparent);
const tagline_base = pardes.Cell{
.style = .{ .bg = .{ .rgb = tagline_rgb }, .font_role = .tagline },
.default = false,
};
var color_target = std.mem.zeroes(c.SDL_GPUColorTargetInfo);
color_target.texture = scene;
- color_target.clear_color = .{
- .r = @as(f32, @floatFromInt(page[0])) / 255.0,
- .g = @as(f32, @floatFromInt(page[1])) / 255.0,
- .b = @as(f32, @floatFromInt(page[2])) / 255.0,
+ // Premultiplied, because that is what both a wl_surface and an X11 ARGB
+ // visual are composited as, and the glyph pass writes premultiplied for
+ // the same reason. A see-through ground is therefore all four channels
+ // zero and not `page.rgb` at alpha zero — the leftover colour would tint
+ // every glyph edge that blends against it.
+ color_target.clear_color = if (page.clear) .{ .r = 0, .g = 0, .b = 0, .a = 0 } else .{
+ .r = @as(f32, @floatFromInt(page.rgb[0])) / 255.0,
+ .g = @as(f32, @floatFromInt(page.rgb[1])) / 255.0,
+ .b = @as(f32, @floatFromInt(page.rgb[2])) / 255.0,
.a = 1.0,
};
color_target.load_op = c.SDL_GPU_LOADOP_CLEAR;
@@ -5259,6 +5313,9 @@ const ResolvedCell = struct {
fg: [3]u8,
bg: [3]u8,
role: pardes.FontRole,
+ /// `bg` is the see-through ground rather than a colour: paint the glyph
+ /// and leave the rest of the cell to the compositor.
+ clear_bg: bool = false,
};
fn topbarPaneBorderPixels(cell_h: u32, tagline_h: u32) u32 {
@@ -5310,10 +5367,18 @@ test "tagline bands face the topbar rule and Tagbottom faces the window edge" {
try std.testing.expectEqual(@as(u32, 4), taglineBandOffset(9, canvas_h, cell_h, tagline_h));
}
-fn resolveCell(g: *Gui, cell: *const pardes.Cell, role: pardes.FontRole, is_cursor: bool, page: [3]u8) ResolvedCell {
+fn resolveCell(g: *Gui, cell: *const pardes.Cell, role: pardes.FontRole, is_cursor: bool, page: Ground) ResolvedCell {
var fg = fg_default;
- var bg = page;
+ var bg = page.rgb;
+ // Only an UNREVERSED default background is the ground; every branch below
+ // that names a real colour clears this, and the reverse at the end clears
+ // it because a reverse puts the TEXT colour there.
+ var clear_bg = page.clear;
var reverse = is_cursor;
+ // An invisible cell over a clear ground has nothing left to draw: `fg = bg`
+ // hides a glyph by painting it in the background, and a background that is
+ // not painted at all would let the ink through as a coloured silhouette.
+ var blank = false;
if (!cell.default) {
const st = cell.style;
fg = switch (st.fg) {
@@ -5321,25 +5386,38 @@ fn resolveCell(g: *Gui, cell: *const pardes.Cell, role: pardes.FontRole, is_curs
.index => |i| palColor(i),
.rgb => |rgb| rgb,
};
- bg = switch (st.bg) {
- .default => page,
- .index => |i| palColor(i),
- .rgb => |rgb| rgb,
- };
+ switch (st.bg) {
+ .default => {},
+ .index => |i| {
+ bg = palColor(i);
+ clear_bg = false;
+ },
+ .rgb => |rgb| {
+ bg = rgb;
+ clear_bg = false;
+ },
+ }
if (st.reverse) reverse = !reverse;
- if (st.invisible) fg = bg;
+ if (st.invisible) {
+ fg = bg;
+ blank = clear_bg;
+ }
if (st.dim) for (&fg) |*ch| {
ch.* = @intCast(@as(u16, ch.*) * 6 / 10);
};
}
- if (reverse) std.mem.swap([3]u8, &fg, &bg);
+ if (reverse) {
+ std.mem.swap([3]u8, &fg, &bg);
+ clear_bg = false;
+ }
- const cp = cellCodepoint(cell);
+ const cp = if (blank) ' ' else cellCodepoint(cell);
return .{
.slot = if (cp == ' ') g.space_slot else ensureGlyph(g, cp, role),
.fg = fg,
.bg = bg,
.role = role,
+ .clear_bg = clear_bg,
};
}
@@ -5401,7 +5479,7 @@ fn emitSurfaceCell(
tagline_base: *const pardes.Cell,
old_layer: bool,
is_cursor: bool,
- page: [3]u8,
+ page: Ground,
) void {
const role = drawnFontRole(core, cell, row);
if (role == .tagline) {
@@ -5437,8 +5515,9 @@ fn emitInstance(
/// change retains the correct band height on both sides of the diff.
old_layer: bool,
is_cursor: bool,
- /// the ground this frame: what a default background resolves to
- page: [3]u8,
+ /// the ground this frame: what a default background resolves to, and
+ /// whether that is a colour at all
+ page: Ground,
) void {
const resolved = resolveCell(g, cell, role, is_cursor, page);
@@ -5499,6 +5578,7 @@ fn emitInstance(
if (track) |active| panelCellCoord(active, col, row) else 0,
);
if (old_layer) instances[idx].effect |= old_layer_bit;
+ if (resolved.clear_bg) instances[idx].effect |= clear_bg_bit;
}
// EFFECT_CODE_CELL_INSTANCE_END
@@ -5946,6 +6026,11 @@ fn ensureSoftTexture(g: *Gui, width: u32, height: u32) !*c.SDL_Texture {
@intCast(height),
) orelse return error.GpuCreate;
_ = c.SDL_SetTextureScaleMode(next, c.SDL_SCALEMODE_NEAREST);
+ // The readback IS the frame, alpha included and already premultiplied.
+ // SDL's default for an alpha format is BLENDMODE_BLEND, which would blend
+ // it a second time against the cleared window and darken every glyph edge
+ // over a see-through ground.
+ _ = c.SDL_SetTextureBlendMode(next, c.SDL_BLENDMODE_NONE);
g.soft_texture = next;
g.soft_tex_w = width;
g.soft_tex_h = height;
@@ -5989,6 +6074,11 @@ fn softPresentFrame(g: *Gui, cmd: *c.SDL_GPUCommandBuffer, target: *c.SDL_GPUTex
const texture = try ensureSoftTexture(g, sw, sh);
const renderer = g.soft_renderer orelse return error.GpuCreate;
if (!c.SDL_UpdateTexture(texture, null, mapped, @intCast(sw * bpp))) return error.GpuMap;
+ // Clear to nothing rather than to opaque black: on a transparent window
+ // this is the pixel the compositor keeps wherever the frame does not
+ // cover, and BLENDMODE_NONE below writes the frame over it verbatim.
+ _ = c.SDL_SetRenderDrawBlendMode(renderer, c.SDL_BLENDMODE_NONE);
+ _ = c.SDL_SetRenderDrawColor(renderer, 0, 0, 0, if (g.transparent) 0 else 255);
_ = c.SDL_RenderClear(renderer);
_ = c.SDL_RenderTexture(renderer, texture, null, null);
_ = c.SDL_RenderPresent(renderer);
diff --git a/src/main.zig b/src/main.zig
index 004d59cb..1fcc2770 100644
--- a/src/main.zig
+++ b/src/main.zig
@@ -160,6 +160,26 @@ fn webMain() !void {
}
fn nativeMain(init: std.process.Init) !void {
+ // FIRST, before anything can log or panic. std's start code hands the
+ // debug/log stderr writer the process environ exactly as the kernel laid
+ // it out beside argv, and `std.debug.lockStderr` scans that block once,
+ // lazily, for NO_COLOR and CLICOLOR_FORCE. Lazily is the problem: libc
+ // may have rewritten the block by then. SDL_CreateWindow does, every
+ // time — `Wayland_ShowWindow` unsets XDG_ACTIVATION_TOKEN, and glibc's
+ // unsetenv compacts `environ` in place and leaves a null in the slot the
+ // captured length still counts. `Environ.scan` unwraps that null, so the
+ // FIRST log line or panic in the gui shell panicked inside the scan, and
+ // then the panic handler took the same path and deadlocked on the stderr
+ // lock it was already holding: "attempt to use null value" and a hang,
+ // with no trace and no message of its own. Every SDL_GetError report in
+ // gui.zig was that, which is to say unreachable.
+ //
+ // Locking and immediately unlocking here does the scan while the block is
+ // still the one std was given, and memoizes it. Nothing is written.
+ var stderr_probe: [64]u8 = undefined;
+ _ = std.debug.lockStderr(&stderr_probe);
+ std.debug.unlockStderr();
+
var opts: pardes.Options = .{};
const arena = init.arena.allocator();
const args = try init.minimal.args.toSlice(arena);