From 94cc30475f47ac3833b8fa9e5ae5e580b3d3c43f Mon Sep 17 00:00:00 2001 From: elwrcl Date: Sat, 3 Oct 2026 20:36:23 +0300 Subject: [PATCH] feat: paint shader renderer --- src/theme/gl.rs | 263 +++++++++++++++++++++++++++++++++++++++++++ src/theme/mod.rs | 2 + src/theme/paint.frag | 56 +++++++++ src/zoom/gl.rs | 6 +- 4 files changed, 324 insertions(+), 3 deletions(-) create mode 100644 src/theme/gl.rs create mode 100644 src/theme/paint.frag diff --git a/src/theme/gl.rs b/src/theme/gl.rs new file mode 100644 index 0000000..f74a71a --- /dev/null +++ b/src/theme/gl.rs @@ -0,0 +1,263 @@ +//! The paint shader on a Wayland surface: EGL + OpenGL ES 3, set up like +//! zoom's renderer. Optionally composites a premultiplied overlay (the +//! picker's cards) on top in the same pass. + +use anyhow::{Context, Result, anyhow}; +use glow::HasContext; +use khronos_egl as egl; +use wayland_client::{Connection, Proxy, protocol::wl_surface::WlSurface}; + +use super::Palette; +use crate::zoom::gl::{Egl, config, open}; + +const VERTEX: &str = "#version 300 es +// One triangle that covers the screen. +void main() { + vec2 p = vec2(float((gl_VertexID << 1) & 2), float(gl_VertexID & 2)); + gl_Position = vec4(p * 2.0 - 1.0, 0.0, 1.0); +} +"; + +const FRAGMENT: &str = include_str!("paint.frag"); + +/// What one frame shows. +#[derive(Debug, Clone, Copy)] +pub struct Frame { + /// Seconds since the animation started. + pub time: f32, + /// 0 = calm flow, 1 = swirl around the centre. + pub motion: f32, + pub speed: f32, + /// Blob size; 1 is the default. + pub scale: f32, + pub palette: Palette, +} + +struct Uniforms { + res: glow::UniformLocation, + t: glow::UniformLocation, + motion: glow::UniformLocation, + speed: glow::UniformLocation, + scale: glow::UniformLocation, + c1: glow::UniformLocation, + c2: glow::UniformLocation, + c3: glow::UniformLocation, + use_overlay: glow::UniformLocation, +} + +pub struct Paint { + gl: glow::Context, + u: Uniforms, + overlay: glow::Texture, + size: (u32, u32), + egl: Egl, + display: egl::Display, + context: egl::Context, + surface: egl::Surface, + // Destroyed after the EGL surface that draws into it. + window: wayland_egl::WlEglSurface, +} + +impl Paint { + /// A GL surface of `size` buffer px on `surface`. + pub fn new(conn: &Connection, surface: &WlSurface, size: (u32, u32)) -> Result { + let (egl, display) = open(conn)?; + egl.bind_api(egl::OPENGL_ES_API) + .context("no OpenGL ES in EGL")?; + let config = config(&egl, display)?; + let context = egl + .create_context( + display, + config, + None, + &[egl::CONTEXT_MAJOR_VERSION, 3, egl::NONE], + ) + .context("could not create an OpenGL ES 3 context")?; + let window = wayland_egl::WlEglSurface::new(surface.id(), size.0 as i32, size.1 as i32) + .context("could not create a wl_egl_window")?; + let egl_surface = unsafe { + egl.create_window_surface(display, config, window.ptr() as egl::NativeWindowType, None) + } + .context("could not create the EGL window surface")?; + egl.make_current(display, Some(egl_surface), Some(egl_surface), Some(context)) + .context("eglMakeCurrent failed")?; + // Paced by frame callbacks; swaps must not block. + egl.swap_interval(display, 0) + .context("eglSwapInterval failed")?; + let gl = unsafe { + glow::Context::from_loader_function(|name| { + egl.get_proc_address(name) + .map_or(std::ptr::null(), |f| f as *const _) + }) + }; + let (u, overlay) = unsafe { setup(&gl, size)? }; + Ok(Paint { + gl, + u, + overlay, + size, + egl, + display, + context, + surface: egl_surface, + window, + }) + } + + /// The surface's buffer size changed. + pub fn resize(&mut self, size: (u32, u32)) { + if size != self.size { + self.window.resize(size.0 as i32, size.1 as i32, 0, 0); + unsafe { self.gl.viewport(0, 0, size.0 as i32, size.1 as i32) }; + self.size = size; + } + } + + /// Shows `pixmap` (premultiplied RGBA, the surface's size) over the + /// paint, or nothing. + pub fn set_overlay(&mut self, pixmap: Option<&tiny_skia::Pixmap>) { + unsafe { + self.gl + .uniform_1_i32(Some(&self.u.use_overlay), pixmap.is_some() as i32); + if let Some(p) = pixmap { + self.gl.active_texture(glow::TEXTURE0); + self.gl.bind_texture(glow::TEXTURE_2D, Some(self.overlay)); + self.gl.tex_image_2d( + glow::TEXTURE_2D, + 0, + glow::RGBA8 as i32, + p.width() as i32, + p.height() as i32, + 0, + glow::RGBA, + glow::UNSIGNED_BYTE, + glow::PixelUnpackData::Slice(Some(p.data())), + ); + } + } + } + + /// Draws `frame` and swaps (which commits the surface). + pub fn draw(&self, frame: &Frame) -> Result<()> { + let p = frame.palette; + unsafe { + let gl = &self.gl; + gl.uniform_2_f32(Some(&self.u.res), self.size.0 as f32, self.size.1 as f32); + gl.uniform_1_f32(Some(&self.u.t), frame.time); + gl.uniform_1_f32(Some(&self.u.motion), frame.motion); + gl.uniform_1_f32(Some(&self.u.speed), frame.speed); + gl.uniform_1_f32(Some(&self.u.scale), frame.scale); + gl.uniform_3_f32(Some(&self.u.c1), p.base[0], p.base[1], p.base[2]); + gl.uniform_3_f32(Some(&self.u.c2), p.body[0], p.body[1], p.body[2]); + gl.uniform_3_f32( + Some(&self.u.c3), + p.highlight[0], + p.highlight[1], + p.highlight[2], + ); + gl.draw_arrays(glow::TRIANGLES, 0, 3); + } + self.egl + .swap_buffers(self.display, self.surface) + .context("eglSwapBuffers failed") + } +} + +impl Drop for Paint { + fn drop(&mut self) { + let _ = self.egl.make_current(self.display, None, None, None); + let _ = self.egl.destroy_surface(self.display, self.surface); + let _ = self.egl.destroy_context(self.display, self.context); + let _ = self.egl.terminate(self.display); + } +} + +/// Compiles the program and returns its uniforms and the overlay texture. +unsafe fn setup(gl: &glow::Context, size: (u32, u32)) -> Result<(Uniforms, glow::Texture)> { + unsafe { + let program = gl.create_program().map_err(|e| anyhow!(e))?; + for (kind, source) in [ + (glow::VERTEX_SHADER, VERTEX), + (glow::FRAGMENT_SHADER, FRAGMENT), + ] { + let shader = gl.create_shader(kind).map_err(|e| anyhow!(e))?; + gl.shader_source(shader, source); + gl.compile_shader(shader); + if !gl.get_shader_compile_status(shader) { + anyhow::bail!("paint shader: {}", gl.get_shader_info_log(shader)); + } + gl.attach_shader(program, shader); + } + gl.link_program(program); + if !gl.get_program_link_status(program) { + anyhow::bail!("paint program: {}", gl.get_program_info_log(program)); + } + gl.use_program(Some(program)); + let vao = gl.create_vertex_array().map_err(|e| anyhow!(e))?; + gl.bind_vertex_array(Some(vao)); + + let overlay = gl.create_texture().map_err(|e| anyhow!(e))?; + gl.active_texture(glow::TEXTURE0); + gl.bind_texture(glow::TEXTURE_2D, Some(overlay)); + gl.tex_parameter_i32( + glow::TEXTURE_2D, + glow::TEXTURE_MIN_FILTER, + glow::NEAREST as i32, + ); + gl.tex_parameter_i32( + glow::TEXTURE_2D, + glow::TEXTURE_MAG_FILTER, + glow::NEAREST as i32, + ); + gl.viewport(0, 0, size.0 as i32, size.1 as i32); + + let uniform = |name: &str| { + gl.get_uniform_location(program, name) + .with_context(|| format!("no uniform {name}")) + }; + if let Ok(sampler) = uniform("overlay") { + gl.uniform_1_i32(Some(&sampler), 0); + } + let u = Uniforms { + res: uniform("res")?, + t: uniform("t")?, + motion: uniform("motion")?, + speed: uniform("speed")?, + scale: uniform("scale")?, + c1: uniform("c1")?, + c2: uniform("c2")?, + c3: uniform("c3")?, + use_overlay: uniform("use_overlay")?, + }; + gl.uniform_1_i32(Some(&u.use_overlay), 0); + Ok((u, overlay)) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn shader_declares_its_uniforms() { + for name in [ + "res", + "t", + "motion", + "speed", + "scale", + "c1", + "c2", + "c3", + "overlay", + "use_overlay", + ] { + assert!( + FRAGMENT + .lines() + .any(|l| l.starts_with("uniform") && l.contains(&format!(" {name};"))), + "{name} missing" + ); + } + } +} diff --git a/src/theme/mod.rs b/src/theme/mod.rs index 32a3e12..b279a81 100644 --- a/src/theme/mod.rs +++ b/src/theme/mod.rs @@ -7,6 +7,8 @@ use std::time::{Duration, Instant}; use image::RgbaImage; +pub mod gl; + /// Three colours, dark to light, as linear 0–1 RGB. #[derive(Debug, Clone, Copy, PartialEq)] pub struct Palette { diff --git a/src/theme/paint.frag b/src/theme/paint.frag new file mode 100644 index 0000000..9d28663 --- /dev/null +++ b/src/theme/paint.frag @@ -0,0 +1,56 @@ +#version 300 es +precision highp float; +uniform vec2 res; +uniform float t; +uniform float motion; // 0 flow, 1 swirl +uniform float speed; +uniform float scale; // blob size: 1 = the spike's +uniform vec3 c1; +uniform vec3 c2; +uniform vec3 c3; +uniform sampler2D overlay; +uniform bool use_overlay; +out vec4 color; + +vec2 warp(vec2 p, float time) { + for (int i = 0; i < 6; i++) { + float fi = float(i); + p += 0.42 * sin(p.yx * vec2(1.55, 1.28) + time * vec2(0.27, 0.33) + fi * vec2(1.3, 2.1)); + p = mat2(0.82, -0.57, 0.57, 0.82) * p; + } + return p; +} + +vec3 paint(vec2 frag) { + vec2 uv = (frag - 0.5 * res) / res.y; + float time = t * speed; + if (motion > 0.0) { + float r = length(uv); + float a = atan(uv.y, uv.x) + motion * (2.2 / (r + 0.35)) + time * 0.25 * motion; + uv = r * vec2(cos(a), sin(a)); + } + vec2 p = warp(uv * 4.0 / scale, time); + p = warp(p * 1.7, time * 1.25 + 3.0); + float v = sin(p.x) * cos(p.y); + float w = sin(length(p) * 0.6 - time * 0.8); + float m = v * 0.75 + w * 0.25; + vec3 col = c1; + col = mix(col, c2, smoothstep(-0.32, -0.18, m)); + col = mix(col, c3, smoothstep(0.38, 0.5, m) * 0.85); + float rim = 1.0 - smoothstep(0.0, 0.06, abs(m + 0.25)); + col *= 1.0 - 0.3 * rim; + float gloss = pow(abs(cos(p.x * 1.3 + p.y)), 36.0); + col += gloss * 0.18 * (c3 + 0.25); + col *= 1.0 - 0.3 * dot(uv, uv); + return col; +} + +void main() { + vec2 frag = vec2(gl_FragCoord.x, res.y - gl_FragCoord.y); + vec3 col = paint(frag); + if (use_overlay) { + vec4 o = texture(overlay, frag / res); + col = o.rgb + col * (1.0 - o.a); + } + color = vec4(col, 1.0); +} diff --git a/src/zoom/gl.rs b/src/zoom/gl.rs index 6224ead..2789e96 100644 --- a/src/zoom/gl.rs +++ b/src/zoom/gl.rs @@ -9,7 +9,7 @@ use wayland_client::{Connection, Proxy, protocol::wl_surface::WlSurface}; use super::view::View; use crate::frame::Bgrx; -type Egl = egl::DynamicInstance; +pub(crate) type Egl = egl::DynamicInstance; /// `EGL_PLATFORM_WAYLAND_KHR`. const PLATFORM_WAYLAND: egl::Enum = 0x31D8; @@ -47,7 +47,7 @@ void main() { "; /// Loads libEGL and opens the compositor's EGL display. -fn open(conn: &Connection) -> Result<(Egl, egl::Display)> { +pub(crate) fn open(conn: &Connection) -> Result<(Egl, egl::Display)> { let lib = unsafe { libloading::Library::new("libEGL.so.1") }.context("could not load libEGL.so.1")?; let egl = unsafe { Egl::load_required_from(lib) } @@ -65,7 +65,7 @@ fn open(conn: &Connection) -> Result<(Egl, egl::Display)> { Ok((egl, display)) } -fn config(egl: &Egl, display: egl::Display) -> Result { +pub(crate) fn config(egl: &Egl, display: egl::Display) -> Result { #[rustfmt::skip] let attributes = [ egl::SURFACE_TYPE, egl::WINDOW_BIT,