1
1
mirror of https://github.com/wez/wezterm.git synced 2024-12-23 21:32:13 +03:00

split shaders, adjust srgb opengl render settings

https://learnopengl.com/Advanced-Lighting/Gamma-Correction suggests
some good practices:

* Only enable SRGB output on the final draw call, so that all prior
  stages can operate on linear values and avoid converting to/from
  linear multiple times.
* The SRGBA textures automatically linearize when sampled, but:
  * The RGB data must be SRGB (non-linear)
  * The A channel is assumed to be linear!

This commit nudges us closer to that by:

* Converting the freetype coverage map from its linear value to
  non-linear when rasterizing.
* Splitting the shader files into one per stage (background, lines,
  glyphs) and only setting outputs_srgb for the glyph stage

refs: #491
This commit is contained in:
Wez Furlong 2021-02-20 17:02:21 -08:00
parent dd70a8a53b
commit 4e2b2eddba
13 changed files with 336 additions and 197 deletions

View File

@ -7,6 +7,7 @@ use anyhow::bail;
use std::cell::RefCell;
use std::mem;
use std::slice;
use window::color::linear_u8_to_srgb8;
pub struct FreeTypeRasterizer {
has_color: bool,
@ -112,12 +113,19 @@ impl FreeTypeRasterizer {
let src_offset = y * pitch;
let dest_offset = y * width * 4;
for x in 0..width {
let gray = data[src_offset + x];
let linear_gray = data[src_offset + x];
let gray = linear_u8_to_srgb8(linear_gray);
// Texture is SRGBA, which in OpenGL means
// that the RGB values are gamma adjusted
// non-linear values, but the A value is
// linear!
rgba[dest_offset + (x * 4)] = gray;
rgba[dest_offset + (x * 4) + 1] = gray;
rgba[dest_offset + (x * 4) + 2] = gray;
rgba[dest_offset + (x * 4) + 3] = gray;
rgba[dest_offset + (x * 4) + 3] = linear_gray;
}
}
RasterizedGlyph {
@ -151,6 +159,16 @@ impl FreeTypeRasterizer {
let green = data[src_offset + (x * 3) + 1];
let blue = data[src_offset + (x * 3) + 2];
let alpha = red.min(green).min(blue);
// Texture is SRGBA, which in OpenGL means
// that the RGB values are gamma adjusted
// non-linear values, but the A value is
// linear!
let red = linear_u8_to_srgb8(red);
let green = linear_u8_to_srgb8(green);
let blue = linear_u8_to_srgb8(blue);
rgba[dest_offset + (x * 4)] = red;
rgba[dest_offset + (x * 4) + 1] = green;
rgba[dest_offset + (x * 4) + 2] = blue;

View File

@ -0,0 +1,20 @@
// This shader is responsible for coloring the window background.
// Note: fragment-common.glsl is automatically prepended!
uniform sampler2D atlas_linear_sampler;
void main() {
if (o_has_color == 2.0) {
// We're the window background image.
color = texture(atlas_linear_sampler, o_tex);
// Apply window_background_image_opacity to the background image
color.a = o_bg_color.a;
} else if (o_has_color == 3.0) {
color = o_bg_color;
} else {
// Nothing else should render on the background layer
discard;
}
color = apply_hsv(color, o_hsv);
}

View File

@ -0,0 +1,16 @@
// This is the window background shader.
// It places the background image in the full viewport.
// Note: vertex-common.glsl is automatically prepended!
void main() {
pass_through_vertex();
if (o_has_color == 2.0) {
// Background image takes up its full coordinates
gl_Position = projection * vec4(position, 0.0, 1.0);
} else {
// Nothing else should render on the background layer
gl_Position = off_screen();
}
}

View File

@ -1,3 +1,5 @@
// This file is automatically prepended to the various -frag shaders.
precision highp float;
in float o_has_color;
@ -10,18 +12,10 @@ in vec4 o_cursor_color;
in vec4 o_fg_color;
in vec4 o_underline_color;
uniform mat4 projection;
uniform bool window_bg_layer;
uniform bool bg_and_line_layer;
uniform bool has_background_image;
uniform sampler2D atlas_nearest_sampler;
uniform sampler2D atlas_linear_sampler;
out vec4 color;
uniform vec3 foreground_text_hsb;
out vec4 color;
float multiply_one(float src, float dst, float inv_dst_alpha, float inv_src_alpha) {
return (src * dst) + (src * (inv_dst_alpha)) + (dst * (inv_src_alpha));
}
@ -56,9 +50,9 @@ vec3 hsv2rgb(vec3 c)
return c.z * mix(K.xxx, clamp(p - K.xxx, 0.0, 1.0), c.y);
}
vec4 apply_hsv(vec4 c)
vec4 apply_hsv(vec4 c, vec3 transform)
{
vec3 hsv = rgb2hsv(c.rgb) * o_hsv;
vec3 hsv = rgb2hsv(c.rgb) * transform;
return vec4(hsv2rgb(hsv).rgb, c.a);
}
@ -93,73 +87,4 @@ vec4 colorize_hsv(vec4 glyph, vec4 color) {
return vec4(hsv2rgb(hsv * foreground_text_hsb), glyph.a);
}
void main() {
if (window_bg_layer) {
if (o_has_color == 2.0) {
// We're the window background image.
color = texture(atlas_linear_sampler, o_tex);
// Apply window_background_image_opacity to the background image
color.a = o_bg_color.a;
} else if (o_has_color == 3.0) {
color = o_bg_color;
} else {
// Nothing else should render on the background layer
discard;
}
} else if (bg_and_line_layer) {
if (o_has_color >= 2.0) {
// Don't render the background image on anything other than
// the window_bg_layer.
discard;
return;
}
// Note that o_bg_color is set to transparent if the background
// color is "default" and there is a window background attachment
color = o_bg_color;
// Sample the underline glyph texture for this location.
// Note that the texture is whitespace in the case where this is
// no underline or strikethrough.
vec4 under_color = texture(atlas_nearest_sampler, o_underline);
if (under_color.a != 0.0) {
// if the underline glyph isn't transparent in this position then
// we take the underline color, otherwise we'll leave the color
// at the background color.
color = o_underline_color;
}
// Similar to the above: if the cursor texture isn't transparent
// in this location, we'll use the cursor color instead of the background.
// The cursor color overrides any underline color we might have picked
// in the section above.
vec4 cursor_outline = texture(atlas_nearest_sampler, o_cursor);
if (cursor_outline.a != 0.0) {
color = o_cursor_color;
}
} else {
if (o_has_color >= 2.0) {
// Don't render the background image on anything other than
// the window_bg_layer.
discard;
} else {
color = texture(atlas_nearest_sampler, o_tex);
if (o_has_color == 0.0) {
// if it's not a color emoji it will be grayscale
// and we need to tint with the fg_color
if (o_fg_color == o_bg_color) {
// However, if we're a monochrome glyph and the foreground and
// background colors are the same, just render a transparent pixel
// instead; this avoids generating shadowy anti-aliasing artifacts
// for something that should otherwise be invisible.
color = vec4(0.0, 0.0, 0.0, 0.0);
discard;
return;
} else {
color = colorize_hsv(color, o_fg_color);
}
}
}
}
color = apply_hsv(color);
}

View File

@ -0,0 +1,35 @@
// This is the Glyph fragment shader.
// It is the last stage in drawing, and is responsible
// for laying down the glyph graphics on top of the other layers.
// Note: fragment-common.glsl is automatically prepended!
uniform sampler2D atlas_nearest_sampler;
void main() {
if (o_has_color >= 2.0) {
// Don't render the background image on anything other than
// the window_bg_layer.
discard;
return;
}
color = texture(atlas_nearest_sampler, o_tex);
if (o_has_color == 0.0) {
// if it's not a color emoji it will be grayscale
// and we need to tint with the fg_color
if (o_fg_color == o_bg_color) {
// However, if we're a monochrome glyph and the foreground and
// background colors are the same, just render a transparent pixel
// instead; this avoids generating shadowy anti-aliasing artifacts
// for something that should otherwise be invisible.
color = vec4(0.0, 0.0, 0.0, 0.0);
discard;
return;
} else {
color = colorize_hsv(color, o_fg_color);
}
}
color = apply_hsv(color, o_hsv);
}

View File

@ -0,0 +1,18 @@
// This is the Glyph vertex shader.
// It is responsible for placing the glyph images in the
// correct place on screen.
// Note: vertex-common.glsl is automatically prepended!
void main() {
pass_through_vertex();
if (o_has_color == 2.0) {
// If we're the background image and we're not rendering
// the background layer, then move this off screen
gl_Position = off_screen();
} else {
// Use only the adjusted cell position to render the glyph
gl_Position = projection * vec4(position + adjust, 0.0, 1.0);
}
}

View File

@ -0,0 +1,41 @@
// This shader is responsible for coloring the underline and
// glyph background graphics.
// Note: fragment-common.glsl is automatically prepended!
uniform sampler2D atlas_nearest_sampler;
void main() {
if (o_has_color >= 2.0) {
// Don't render the background image on anything other than
// the window_bg_layer.
discard;
return;
}
// Note that o_bg_color is set to transparent if the background
// color is "default" and there is a window background attachment
color = o_bg_color;
// Sample the underline glyph texture for this location.
// Note that the texture is whitespace in the case where this is
// no underline or strikethrough.
vec4 under_color = texture(atlas_nearest_sampler, o_underline);
if (under_color.a != 0.0) {
// if the underline glyph isn't transparent in this position then
// we take the underline color, otherwise we'll leave the color
// at the background color.
color = o_underline_color;
}
// Similar to the above: if the cursor texture isn't transparent
// in this location, we'll use the cursor color instead of the background.
// The cursor color overrides any underline color we might have picked
// in the section above.
vec4 cursor_outline = texture(atlas_nearest_sampler, o_cursor);
if (cursor_outline.a != 0.0) {
color = o_cursor_color;
}
color = apply_hsv(color, o_hsv);
}

View File

@ -0,0 +1,17 @@
// This is the underline/strikethrough and text-background color
// shader. It is responsible for locating the cell boundaries.
// Note: vertex-common.glsl is automatically prepended!
void main() {
pass_through_vertex();
if (o_has_color == 2.0) {
// If we're the background image and we're not rendering
// the background layer, then move this off screen
gl_Position = off_screen();
} else {
// Want to fill the whole cell when painting backgrounds
gl_Position = projection * vec4(position, 0.0, 1.0);
}
}

View File

@ -6,7 +6,6 @@ use ::window::glium::backend::Context as GliumContext;
use ::window::glium::texture::SrgbTexture2d;
use ::window::glium::{IndexBuffer, VertexBuffer};
use ::window::*;
use anyhow::anyhow;
use config::configuration;
use std::cell::RefCell;
use std::rc::Rc;
@ -16,7 +15,9 @@ pub struct RenderState {
pub context: Rc<GliumContext>,
pub glyph_cache: RefCell<GlyphCache<SrgbTexture2d>>,
pub util_sprites: UtilSprites<SrgbTexture2d>,
pub program: glium::Program,
pub background_prog: glium::Program,
pub line_prog: glium::Program,
pub glyph_prog: glium::Program,
pub glyph_vertex_buffer: RefCell<VertexBuffer<Vertex>>,
pub glyph_index_buffer: IndexBuffer<u32>,
pub quads: Quads,
@ -37,32 +38,12 @@ impl RenderState {
let result = UtilSprites::new(&mut *glyph_cache.borrow_mut(), metrics);
match result {
Ok(util_sprites) => {
let mut errors = vec![];
let mut program = None;
for version in &["330", "300 es"] {
let source = glium::program::ProgramCreationInput::SourceCode {
vertex_shader: &Self::vertex_shader(version),
fragment_shader: &Self::fragment_shader(version),
outputs_srgb: true,
tessellation_control_shader: None,
tessellation_evaluation_shader: None,
transform_feedback_varyings: None,
uses_point_size: false,
geometry_shader: None,
};
log::trace!("compiling a prog with version {}", version);
match glium::Program::new(&context, source) {
Ok(prog) => {
program = Some(prog);
break;
}
Err(err) => errors.push(err.to_string()),
};
}
let background_prog =
Self::compile_prog(&context, false, Self::background_shader)?;
let line_prog = Self::compile_prog(&context, false, Self::line_shader)?;
let program = program.ok_or_else(|| {
anyhow!("Failed to compile shaders: {}", errors.join("\n"))
})?;
// Last prog outputs srgb for gamma correction
let glyph_prog = Self::compile_prog(&context, true, Self::glyph_shader)?;
let (glyph_vertex_buffer, glyph_index_buffer, quads) = Self::compute_vertices(
&context,
@ -75,7 +56,9 @@ impl RenderState {
context,
glyph_cache,
util_sprites,
program,
background_prog,
line_prog,
glyph_prog,
glyph_vertex_buffer: RefCell::new(glyph_vertex_buffer),
glyph_index_buffer,
quads,
@ -91,6 +74,36 @@ impl RenderState {
}
}
fn compile_prog(
context: &Rc<GliumContext>,
outputs_srgb: bool,
fragment_shader: fn(&str) -> (String, String),
) -> anyhow::Result<glium::Program> {
let mut errors = vec![];
for version in &["330", "300 es"] {
let (vertex_shader, fragment_shader) = fragment_shader(version);
let source = glium::program::ProgramCreationInput::SourceCode {
vertex_shader: &vertex_shader,
fragment_shader: &fragment_shader,
outputs_srgb,
tessellation_control_shader: None,
tessellation_evaluation_shader: None,
transform_feedback_varyings: None,
uses_point_size: false,
geometry_shader: None,
};
log::trace!("compiling a prog with version {}", version);
match glium::Program::new(context, source) {
Ok(prog) => {
return Ok(prog);
}
Err(err) => errors.push(err.to_string()),
};
}
anyhow::bail!("Failed to compile shaders: {}", errors.join("\n"))
}
pub fn advise_of_window_size_change(
&mut self,
metrics: &RenderMetrics,
@ -110,12 +123,55 @@ impl RenderState {
Ok(())
}
fn vertex_shader(version: &str) -> String {
format!("#version {}\n{}", version, include_str!("vertex.glsl"))
fn glyph_shader(version: &str) -> (String, String) {
(
format!(
"#version {}\n{}\n{}",
version,
include_str!("vertex-common.glsl"),
include_str!("glyph-vertex.glsl")
),
format!(
"#version {}\n{}\n{}",
version,
include_str!("fragment-common.glsl"),
include_str!("glyph-frag.glsl")
),
)
}
fn fragment_shader(version: &str) -> String {
format!("#version {}\n{}", version, include_str!("fragment.glsl"))
fn line_shader(version: &str) -> (String, String) {
(
format!(
"#version {}\n{}\n{}",
version,
include_str!("vertex-common.glsl"),
include_str!("line-vertex.glsl")
),
format!(
"#version {}\n{}\n{}",
version,
include_str!("fragment-common.glsl"),
include_str!("line-frag.glsl")
),
)
}
fn background_shader(version: &str) -> (String, String) {
(
format!(
"#version {}\n{}\n{}",
version,
include_str!("vertex-common.glsl"),
include_str!("background-vertex.glsl")
),
format!(
"#version {}\n{}\n{}",
version,
include_str!("fragment-common.glsl"),
include_str!("background-frag.glsl")
),
)
}
/// Compute a vertex buffer to hold the quads that comprise the visible

View File

@ -2774,8 +2774,6 @@ impl TermWindow {
.magnify_filter(MagnifySamplerFilter::Linear)
.minify_filter(MinifySamplerFilter::Linear);
let has_background_image = self.window_background.is_some();
let foreground_text_hsb = configuration().foreground_text_hsb;
let foreground_text_hsb = (
foreground_text_hsb.hue,
@ -2787,14 +2785,10 @@ impl TermWindow {
frame.draw(
&*vb,
&gl_state.glyph_index_buffer,
&gl_state.program,
&gl_state.background_prog,
&uniform! {
projection: projection,
atlas_nearest_sampler: atlas_nearest_sampler,
atlas_linear_sampler: atlas_linear_sampler,
window_bg_layer: true,
bg_and_line_layer: false,
has_background_image: has_background_image,
foreground_text_hsb: foreground_text_hsb,
},
&alpha_blending,
@ -2804,14 +2798,11 @@ impl TermWindow {
frame.draw(
&*vb,
&gl_state.glyph_index_buffer,
&gl_state.program,
&gl_state.line_prog,
&uniform! {
projection: projection,
atlas_nearest_sampler: atlas_nearest_sampler,
atlas_linear_sampler: atlas_linear_sampler,
window_bg_layer: false,
bg_and_line_layer: true,
has_background_image: has_background_image,
foreground_text_hsb: foreground_text_hsb,
},
&alpha_blending,
@ -2861,14 +2852,11 @@ impl TermWindow {
frame.draw(
&*vb,
&gl_state.glyph_index_buffer,
&gl_state.program,
&gl_state.glyph_prog,
&uniform! {
projection: projection,
atlas_nearest_sampler: atlas_nearest_sampler,
atlas_linear_sampler: atlas_linear_sampler,
window_bg_layer: false,
bg_and_line_layer: false,
has_background_image: has_background_image,
foreground_text_hsb: foreground_text_hsb,
},
&blend_but_set_alpha_to_one,

View File

@ -0,0 +1,49 @@
// This file is automatically prepended to the various
// vertex.glsl files.
precision highp float;
in vec2 position;
in vec2 adjust;
in vec2 tex;
in vec2 underline;
in vec4 bg_color;
in vec4 fg_color;
in vec4 underline_color;
in float has_color;
in vec2 cursor;
in vec4 cursor_color;
in vec3 hsv;
uniform mat4 projection;
out float o_has_color;
out vec2 o_cursor;
out vec2 o_tex;
out vec2 o_underline;
out vec3 o_hsv;
out vec4 o_bg_color;
out vec4 o_cursor_color;
out vec4 o_fg_color;
out vec4 o_underline_color;
void pass_through_vertex() {
o_tex = tex;
o_has_color = has_color;
o_fg_color = fg_color;
o_bg_color = bg_color;
o_underline = underline;
o_underline_color = underline_color;
o_cursor = cursor;
o_cursor_color = cursor_color;
o_hsv = hsv;
}
// Returns a position that is outside of the viewport,
// such that this vertex effectively won't contribute
// the scene being rendered.
// There may be a better way to do this.
vec4 off_screen() {
return vec4(100.0, 100.0, 100.0, 100.0);
}

View File

@ -1,68 +0,0 @@
precision highp float;
in vec2 position;
in vec2 adjust;
in vec2 tex;
in vec2 underline;
in vec4 bg_color;
in vec4 fg_color;
in vec4 underline_color;
in float has_color;
in vec2 cursor;
in vec4 cursor_color;
in vec3 hsv;
uniform mat4 projection;
uniform bool window_bg_layer;
uniform bool bg_and_line_layer;
uniform bool has_background_image;
out float o_has_color;
out vec2 o_cursor;
out vec2 o_tex;
out vec2 o_underline;
out vec3 o_hsv;
out vec4 o_bg_color;
out vec4 o_cursor_color;
out vec4 o_fg_color;
out vec4 o_underline_color;
// Returns a position that is outside of the viewport,
// such that this vertex effectively won't contribute
// the scene being rendered.
// There may be a better way to do this.
vec4 off_screen() {
return vec4(100.0, 100.0, 100.0, 100.0);
}
void main() {
o_tex = tex;
o_has_color = has_color;
o_fg_color = fg_color;
o_bg_color = bg_color;
o_underline = underline;
o_underline_color = underline_color;
o_cursor = cursor;
o_cursor_color = cursor_color;
o_hsv = hsv;
if (window_bg_layer) {
if (o_has_color == 2.0) {
// Background image takes up its full coordinates
gl_Position = projection * vec4(position, 0.0, 1.0);
} else {
// Nothing else should render on the background layer
gl_Position = off_screen();
}
} else if (o_has_color == 2.0) {
// If we're the background image and we're not rendering
// the background layer, then move this off screen
gl_Position = off_screen();
} else if (bg_and_line_layer) {
// Want to fill the whole cell when painting backgrounds
gl_Position = projection * vec4(position, 0.0, 1.0);
} else {
// Use only the adjusted cell position to render the glyph
gl_Position = projection * vec4(position + adjust, 0.0, 1.0);
}
}

View File

@ -4,6 +4,16 @@ use palette::{Blend, LinSrgb, LinSrgba, Srgb, Srgba};
lazy_static::lazy_static! {
static ref SRGB_TO_F32_TABLE: [f32;256] = generate_srgb8_to_linear_f32_table();
static ref F32_TO_U8_TABLE: [u32;104] = generate_linear_f32_to_srgb8_table();
static ref RGB_TO_SRGB_TABLE: [u8;256] = generate_rgb_to_srgb8_table();
}
fn generate_rgb_to_srgb8_table() -> [u8; 256] {
let mut table = [0; 256];
for (val, entry) in table.iter_mut().enumerate() {
let linear = (val as f32) / 255.0;
*entry = linear_f32_to_srgb8_using_table(linear);
}
table
}
fn generate_srgb8_to_linear_f32_table() -> [f32; 256] {
@ -107,6 +117,10 @@ fn linear_f32_to_srgbf32(f: f32) -> f32 {
}
*/
pub fn linear_u8_to_srgb8(f: u8) -> u8 {
unsafe { *RGB_TO_SRGB_TABLE.get_unchecked(f as usize) }
}
#[allow(clippy::unreadable_literal)]
const ALMOST_ONE: u32 = 0x3f7fffff;
#[allow(clippy::unreadable_literal)]
@ -289,6 +303,16 @@ impl Color {
Color(word.to_be())
}
#[inline]
pub fn with_linear_rgba_u8(red: u8, green: u8, blue: u8, alpha: u8) -> Color {
Self::rgba(
linear_u8_to_srgb8(red),
linear_u8_to_srgb8(green),
linear_u8_to_srgb8(blue),
linear_u8_to_srgb8(alpha),
)
}
#[inline]
pub fn as_rgba(self) -> (u8, u8, u8, u8) {
let host = u32::from_be(self.0);